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)

效果

相关推荐
光影少年11 分钟前
vue3.0性能提升主要通过那几方面体现的?
前端·vue.js
小磊哥er23 分钟前
【前端工程化】前端开发中的这些设计规范你知道吗
前端
江城开朗的豌豆24 分钟前
路由守卫:你的Vue路由‘保安’,全局把关还是局部盯梢?
前端·javascript·vue.js
Jinxiansen021132 分钟前
Vue 3 响应式核心源码详解(基于 @vue/reactivity)
前端·javascript·vue.js
OEC小胖胖5 小时前
去中心化身份:2025年Web3身份验证系统开发实践
前端·web3·去中心化·区块链
Cacciatore->6 小时前
Electron 快速上手
javascript·arcgis·electron
vvilkim6 小时前
Electron 进程间通信(IPC)深度优化指南
前端·javascript·electron
某公司摸鱼前端7 小时前
ES13(ES2022)新特性整理
javascript·ecmascript·es13
ai小鬼头8 小时前
百度秒搭发布:无代码编程如何让普通人轻松打造AI应用?
前端·后端·github
漂流瓶jz8 小时前
清除浮动/避开margin折叠:前端CSS中BFC的特点与限制
前端·css·面试