typescript软渲染实现类似canvas的2d矢量图形引擎

2d图形渲染本质是只有两大类,CPU渲染和GPU渲染。

现代计算机图形基本上都采用GPU渲染了。

但在一些低端设备和钳入式小程序依然存在大量靠CPU软渲染实现2D图形渲染。

如skia它支持cpu渲染(软渲染),也支持gpu渲染。默认就是GPU渲染。前端2d渲染引擎几乎都是使用webgl(gpu)来实现的如mapbox,pixijs,mesh2d等

但我觉得CPU软渲染不会过时,用typescript写主要用于学习,性能不是主要的。相比c++主要调试更方便。光栅化的代码主要参考,c和c++图形库cairo和agg

软渲染实现流程

  1. Path 矢量路径(类似Path2D,管理绘制命令),支持绘制命令(moveTo,LineTo,cubicTo,等),计算边界、路径扁平化 、变换矩阵
  2. Context 上下文,管理paint状态栈和暴露对外的绘制命令,paint 对象(包含填充的样式和规则),clip裁剪路径功能,相当把裁剪路径渲染光栅化一遍得到span,再与实际绘制的span做一个相交处理.就是在clip路径内的像素就显示
  3. Outline 从Path对象导出矢量路径到outline中,如果是Stroke绘制,要生轮廓,专业一点叫多边形偏移,像clipperJs库就有这种功能(Stroke绘制的填充,通过LineWidth,lineJoin,lineCap,miterlimit等属性,生成对应的轮廓)
  4. Outline 26.6定点数整数路径(使用Bigint类型,后续可以进32位以上的位运算或负数的位运算),用于光栅化时遍历,生成Span {x,y,len,coverage}对象)用于渲染扫描线,
  5. 光栅化扫描转换 (光栅化的过程会生成一系列的cell和span,这种模式叫灰度抗锯齿光栅化,计算像素的覆盖率(alpha)来调整像素颜色(src*alpha+dst*(1-alpha)) 。这块参考agg,freetype),如果是GPU渲染的化,这块就更简单,直接把路径用mapbox的earcut库,把路径三角化丢给GPU自己渲染。GPU的抗锯齿方案就更多了,MSAA、FXAA、SMAA、TXAA等
  6. Blend颜色合成 (rgb颜色、渐变颜色、纹理颜色(提取图片的颜色))如果是GPU渲染,这块都在片段着色器完成了

数据结构

  1. Path,Point,Matrix,Rect 路径和矩形窗口裁剪
  2. Context,Surface 上下文和像素缓冲区
  3. Paint,Gradient,Color,Texture,LineJoin,LineCap,FillRule 图形样式
  4. Outline,Span,Cell,Raster 图形轮廓,光栅化
TypeScript 复制代码
export class Span {
    x: int = 0;              // 起始像素的 x 坐标
    len: int = 0;            // 像素段长度,从 x 开始向右延伸 len 像素
    y: int = 0;              // 当前 span 所在的 y 坐标(扫描线行号)
    coverage: int= 0;     // 灰度覆盖值(0-255),表示此 span 的覆盖程度
};
export class Cell {
    x: int = 0                // 当前 cell 的 x 坐标(整数像素位置)
    cover: int = 0            // 覆盖计数器(累加 y 子像素上的覆盖值)
    area: int = 0           // 面积累加器(用于精细抗锯齿灰度计算)
    next: TCell | null = null // 下一个 cell
}
export class Raster{
    ex: int= 0;               // 当前点的 x 坐标(子像素精度)
    ey: int= 0;               // 当前点的 y 坐标(子像素精度)

    min_ex: int= 0              // 所有轮廓点中的最小 x(边界框)
    max_ex: int= 0;             // 最大 x
    min_ey: int= 0              // 最小 y
    max_ey: int= 0;             // 最大 y

    count_ex: int= 0            // 扫描线宽度(x 像素数量)
    count_ey: int= 0;           // 扫描线高度(y 像素数量)

    area: int= 0;              // 当前正在处理的单元格的临时面积值
    cover: int = 0;               // 当前单元格的覆盖值

    invalid: int = 0;             // 标记某些内部状态是否失效

    cells: int[] = [];          // 所有 cell 的缓冲区,用于构建覆盖图像
    num_cells: int= 0;    // 当前已使用的 cell 数量

    x: int= 0                   // 逻辑处理用的当前像素 x 坐标
    y: int= 0;                  // 当前像素 y 坐标(整数)

    outline!: Outline;         // 要扫描转换的轮廓路径
    clip_box!: Rect;           // 可选裁剪框(bounding box)

    gray_spans: Span[] = new Array(MAX_GRAY_SPANS).fill(0).map(() => new Span());
                                  // 缓存生成的灰度 span,用于最终输出或回调
    num_gray_spans: int = 0;      // 当前灰度 span 数量
    skip_spans: int = 0;          // 是否跳过 span 渲染(例如用于裁剪)

    render_span: FT_Raster_Span_Func | null = null;// 渲染 span 的回调函数(最终输出 span)

    ycells: (TCell | null)[] = [];// 每个 y 扫描线所对应的 cell 
    ycount: TPos = 0;             // 当前 ycells 的总行数
}

代码API类canvas方式

TypeScript 复制代码
import { Context } from './ctx'
import { FillRule, LineCap, LineJoin } from './paint';
import {Surface} from './surface'


const surface=Surface.create(500,500)
const ctx=Context.create(surface)

const canvas=document.getElementById('canvas') as HTMLCanvasElement;
canvas.width=surface.width
canvas.height=surface.height
const nativeCtx=canvas.getContext('2d')!



ctx.save()
ctx.newPath()
ctx.setSourceRGB(1,0,0)
var gradinet=ctx.setSourceRadialGradient(250,250,80,250,250,0)
gradinet.addStopRgb(0,1,0,0)
gradinet.addStopRgb(0.5,0,1,0)
gradinet.addStopRgb(1,0,0,1)

ctx.setLineWidth(10)
ctx.setLineJoin(LineJoin.MITER)
//ctx.setDash([10,5],0)
ctx.rectangle(200,200,100,100)
ctx.fill()
ctx.restore()

ctx.save()
ctx.newPath()
ctx.setLineJoin(LineJoin.ROUND)
ctx.setLineCap(LineCap.ROUND)
ctx.setLineWidth(10)
ctx.setSourceRGB(1,0,0)
ctx.moveTo(100,100)
ctx.lineTo(200,100)
ctx.lineTo(100,200)
ctx.stroke()
ctx.restore()

ctx.save()
ctx.newPath()
ctx.setLineJoin(LineJoin.MITER)
ctx.setLineCap(LineCap.SQUARE)
ctx.setLineWidth(10)
ctx.setSourceRGB(1,0,0)
ctx.moveTo(40,300)
ctx.lineTo(160,300)
ctx.lineTo(40,400)
ctx.stroke()
ctx.restore()

ctx.save()
ctx.newPath()
ctx.setLineJoin(LineJoin.MITER)
ctx.setLineCap(LineCap.SQUARE)
ctx.setLineWidth(10)
ctx.setDash([10,25,10],0)
ctx.setSourceRGB(1,0,0)
ctx.moveTo(240,330)
ctx.lineTo(460,330)

ctx.stroke()
ctx.restore()

ctx.save()
ctx.newPath()

ctx.setSourceRGB(1,0,0)
ctx.arc(300,100,50,0,Math.PI*2,false)
ctx.clip()
ctx.rectangle(250,50,100,100)
ctx.fill()
ctx.restore()

nativeCtx.putImageData(new ImageData(surface.pixels!,surface.width,surface.height),0,0)

效果

相关推荐
神仙别闹12 分钟前
基于ASP.NET+ SQL Server实现(Web)医院信息管理系统
前端·后端·asp.net
步行cgn5 小时前
Vue 中的数据代理机制
前端·javascript·vue.js
GH小杨5 小时前
JS之Dom模型和Bom模型
前端·javascript·html
星月心城6 小时前
JS深入之从原型到原型链
前端·javascript
MessiGo6 小时前
Javascript 编程基础(5)面向对象 | 5.2、原型系统
开发语言·javascript·原型模式
你的人类朋友6 小时前
🤔Token 存储方案有哪些
前端·javascript·后端
烛阴6 小时前
从零开始:使用Node.js和Cheerio进行轻量级网页数据提取
前端·javascript·后端
liuyang___6 小时前
日期的数据格式转换
前端·后端·学习·node.js·node
西哥写代码7 小时前
基于cornerstone3D的dicom影像浏览器 第三十一章 从PACS服务加载图像
javascript·pacs·dicom
贩卖纯净水.7 小时前
webpack其余配置
前端·webpack·node.js