基于DOM结构和支持响应式的“造轮子”过程

2023-12-07
来源:网络整理

教您开通聚合商家收款码,也称为三合一收款码,支持银行卡/支付宝/花呗/信用卡/微信付款,这个收款码是银联旗下的云闪付APP申请,放下安全!银联云闪付品牌,百分比值得信赖!下面提供云闪付商家收款码的申请全过程给大家!

【云闪付商家收款码 开通全过程视频!】     

               



上面就是云闪付商家收款码的申请全过程 !您学会了吗?     


          

云闪付服务商官网是:ysffws.com,进入云闪付服务商官网可以直接联系上服务商,服务商可以帮助您完成二 次认证,解决申请过程出现的任何问题,是您申请收款码的好帮手哦!~     

   



                  

从2017年到2020年,我花了大概4年的时间,从零到一,实现了一个可切换和渲染,跨平台支持浏览器、SSR、小程序,基于DOM结构并支持响应式。 高性能支持批量渲染,针对视觉场景进行优化,支持图形系统-.

在这个“造轮子”的过程中,我逐渐把一个非常简单的渲染库变成了一个相当不错的图形库,可以支持视觉应用和游戏的开发。 这里面有很多的积累和思考。 毕竟研究是在两年多前进行的,所以有些细节可能在我的记忆中不是特别清楚,有些功能可能有点过时,但我认为还是有很多内容可以带来参考的并给大家带来启发。

1.原始需求:与渲染无关

2017年底,我还在奇虎360负责奇舞团。奇舞团是一个中端前端团队,支持360很多业务需求,包括一些toB需求。 这些需求包括许多可视化图表和态势感知大屏幕。 2015-2016年左右,我们的同学开始使用D3来完成可视化项目,因为D3具有很高的灵活性。 有的同学简单地将D3归为视觉渲染框架。 事实上,这种想法是错误的。 D3不是一个可视化框架,而是一个数据驱动的引擎。

严格来说,D3关心的是数据的组织,并不关心数据最终的渲染结果。 然而D3的数据组织形式是基于树结构的,因为它天然契合树结构的渲染形式。 正因为如此,一般来说,D3的官方示例都是使用DOM或SVG来渲染的。 这是因为基于 DOM 树的渲染非常适合 D3 的树状数据组织形式。

查看代码:

查看代码:

1.1 与DOM的一致性

为了实现上述效果,参考浏览器DOM API并进行适配:

1.2 & DOM & D3

理论上,操作元素与操作 DOM 元素是完全一样的,两者之间的差异很小。

查看代码:

这种一致性使得它可以与D3结合使用,灵活地解决非常复杂的可视化问题:#/zh-cn//d3

2. 设计图形系统的“骨架”

2.1 坐标系的选择

在设计图形系统时,首先必须确定默认的坐标系。 理论上,任何直角坐标系,甚至非直角坐标系(如极坐标)都可以作为默认坐标系。 在欧几里得几何中,这些坐标系可以自由转换。 不过,考虑到与 DOM 的一致性,使用浏览器的默认坐标系是一个很好的选择。

为了渲染,我们需要将顶点坐标转换为坐标。 在这里,我们可以根据以下方式动态设置坐标:#L181

updateResolution() { const {width, height} = this.canvas; const m1 = [ // translation 1, 0, 0, 0, 1, 0, -width / 2, -height / 2, 1, ]; const m2 = [ // scale 2 / width, 0, 0, 0, -2 / height, 0, 0, 0, 1, ]; const m3 = mat3(m2) * mat3(m1); this.projectionMatrix = m3; if(this[_glRenderer]) { this[_glRenderer].gl.viewport(0, 0, width, height); } }

attribute vec3 a_vertexPosition; attribute vec3 a_vertexTextureCoord; varying vec3 vTextureCoord; uniform mat3 viewMatrix; uniform mat3 projectionMatrix; void main() { gl_PointSize = 1.0; vec3 pos = projectionMatrix * viewMatrix * vec3(a_vertexPosition.xy, 1.0); gl_Position = vec4(pos.xy, 1.0, 1.0); vTextureCoord = a_vertexTextureCoord; }

2.2 层、树结构和元素类型

use代表一个场景,一个代表一层。 在这里,我的设计对应了一个画布,即默认每个都是一个独立的元素。 这有优点也有缺点,这是一种设计权衡。

优点是各个相互独立,不需要考虑绘制顺序。 可以充分利用这样的多线程来并行绘制,而且逻辑也比较简单。 如果需要响应多层事件,只需要关注事件处理的顺序即可。 缺点是如果绘制多个图层,可能会生成更多的对象实例,从而消耗更多的内存。

查看代码:

正如前面提到的,树状结构用于管理元素。 、 、 、 都是容器,而其他类型的图形元素则安装在容器上。

元素种类很多,图形元素超过十五种,如下图所示。

这些元素可以分为两类,一类是元素,包括,,,另一类是Path元素,包括各种图形。 这两类元素中,Path更类似于DOM元素,占据一个矩形区域,具有盒模型,并且可以计算大小; Path更类似于SVG元素,形成矢量形状,并且有渲染和填充两种类型,但不计算Size(无论Path如何都可以计算)。

它很特别。 在v3中,它默认不计算大小,但是继承自它的sum会计算大小。 在v2中,可以计算尺寸,并可以进行区域裁剪和设置。 在v3中,主要功能是设置分组元素的一致性。 这样设计的原因涉及到渲染模型。 这将在后面详细解释。

考虑到可扩展性,用户可以通过 . #L15

作用是注册一棵唯一的文档树,这样节点挂载后,可以通过等找到该节点。

2.3 属性更新和重绘机制

与一般图形库不同,一般图形库通常使用动画计时器以固定帧速率刷新画布。 但当属性发生变化时,它使用异步更新机制。

具体原理如下图所示:

以下是一些需要注意的细节:

并不是所有的属性改变都会触发,比如ID等改变就不会触发。 有些属性改变不仅会触发,还需要触发其他操作。 例如, 、 等属性的变化需要重新计算图形元素的轮廓(稍后讨论); 的变化,导致 的重新排列。

这种设计的好处是显而易见的,它可以最大限度地减少不必要的重绘和其他计算,从而提高整体性能。

2.4 外部

虽然它有自己的更新机制,但是一些外部库,比如or,有自己的更新逻辑,所以增加了手动控制设计,方便与外部库的配合。 #/zh-cn//

2.5 跨平台

实现时尽量不要使用浏览器原生提供的能力,除非它们是标准和API。 适配浏览器、微信小程序、微信小游戏等不同环境。

为了集成和环境,制作了以下库:

3.盒子模型、事件、动画等。

3.1 盒子模型设计

对于type元素,采用标准的DOM盒模型,可以设置各个属性,并可以通过属性切换盒模型模式。

查看代码:

3.2 事件机制

视口宽度和高度:[, ]

画布宽度和高度:[,]

抵消:[, ]

为什么会产生偏移? 详情请参见屏幕适配。

#L179

#L419

#L840

对每个三角网格使用命中检测(这里还有优化的空间,可以先排序,使用二分查找快速确定范围):

function inTriangle(p1, p2, p3, point) { const a = p2.copy().sub(p1); const b = p3.copy().sub(p2); const c = p1.copy().sub(p3); const u1 = point.copy().sub(p1); const u2 = point.copy().sub(p2); const u3 = point.copy().sub(p3); const s1 = Math.sign(a.cross(u1)); let p = a.dot(u1) / a.length ** 2; if(s1 === 0 && p >= 0 && p <= 1) return true; const s2 = Math.sign(b.cross(u2)); p = b.dot(u2) / b.length ** 2; if(s2 === 0 && p >= 0 && p <= 1) return true; const s3 = Math.sign(c.cross(u3)); p = c.dot(u3) / c.length ** 2; if(s3 === 0 && p >= 0 && p <= 1) return true; return s1 === s2 && s2 === s3; }

3.3 动画设计

为了实现动画可以在时间轴上以任意速度播放,包括正向播放和回放,可以在任意时间点跳转,并实时切换播放状态和时间轴状态,设计了一个库。

这个库的设计是:

创建一个根据当前时间线计算时间的对象,可以是任意数字,因此时间可以停止或倒带。 设置和更改都会影响对象。 除了属性之外,对象还有一个(熵)属性,它与熵的不同之处在于,如果为负数,则回溯,但总是增加。 对象可以被分叉,分叉产生的新对象具有分叉对象的时间线。 这意味着对象可以嵌套,并且默认情况下,其中的所有元素都会分叉其对象,因此当我们将 设为 0 时,其中的所有动画都将暂停。

查看代码:

基于封装,参考Web API - Web APIs | MDN ( )

#/zh-cn/?id=%e8%bf%87%e6%b8%a1-

查看代码:

查看代码:

查看代码:

4.从二维到

在1.0和2.0中,我主要使用渲染。 直到3.0,我重写了底层引擎,开始默认使用渲染。

4.1 轮廓和网格

为了方便几何体的处理,尤其是Path的解析,我实现了一个底层的渲染引擎——mesh-js/mesh.js:为()而生,将2D几何体分解为轮廓和网格对象,有点类似于 和 ,但因为我们实际上处理的是 2D 图形,所以模型更简单。

在mesh.js中,为了绘制几何形状,我们首先构建元素的轮廓(/),然后根据轮廓创建网格对象。 经过这两个步骤,我们就可以画出几何图形了。 这个过程其实是类似的,只是稍微复杂一些。

查看代码:

4.2 三角测量

众所周知,基本图元只有点、线、三角形等,要绘制多边形,就需要对图形进行三角剖分。 对任意多边形进行三角剖分有很多成熟的算法,我选择的是GLU。

我通过一系列工具库-svg-path、-svg-path、svg-path-()将其转换为多边形顶点列表。 我不会在这里重新发明轮子。 一些工具库有一些小错误,所以我修复了它们。 。

得到顶点后,对顶点进行三角剖分,得到三角网格的拓扑,并通过这个拓扑创建对象。

4.3

如果你不经常使用渲染,很难想象绘制带宽度的折线这个非常简单的需求会难倒开发人员。

其实这个问题已经有一个比较经典的解决方案,就是利用挤压( )曲线技术来实现。 有两种方法,一种是使用JS计算顶点,另一种是在. 为了灵活实现“line cap()”效果,采用JS计算来处理。

如上图所示,黑色折线是宽度为1像素的原始折线,蓝色虚线构成我们最终生成的宽度曲线,红色虚线是顶点移动的方向。 因为折线两个端点的挤压只与​​一条线段的方向有关,而拐角处顶点的挤压与相邻两条线段的方向有关,所以我们要讨论顶点移动的方向在两种情况下。

首先,有折线的端点。 假设线段的向量为(x,y)。 因为它的移动方向与线段的方向垂直,所以我们只需要沿着法线方向移动它即可。 根据垂直向量的点积为0,我们很容易得出顶点的两个移动方向为(-y,x)和(y,-x)。 如下所示:

端点的挤出方向已确定。 接下来,我们需要确定角点的挤出方向。 我们看一下示意图。

如上图所示,我们假设有一条折线abc,b为角点。 如果我们延伸ab,我们可以得到一个单位向量v1,如果我们向相反方向延伸bc,我们可以得到另一个单位向量v2。 那么挤压方向就是向量v1+v2的方向,-(v1+v2)的反方向。

现在我们有了挤压方向,我们需要确定挤压矢量的长度。

第一个是折线端点的挤出长度,它等于 的一半。 角部的挤压长度比较复杂,需要重新计算。

绿色辅助线应该等于 的一半,并且它恰好是 v1+v2 在绿色向量方向上的投影。 因此,我们可以先用向量点积求出红色虚线和绿色虚线之间夹角的余弦,然后将这个值除以二分之一,就得到了挤压向量的长度。

具体实现代码如下:

function extrudePolyline(gl, points, {thickness = 10} = {}) { const halfThick = 0.5 * thickness; const innerSide = []; const outerSide = []; // 构建挤压顶点 for(let i = 1; i < points.length - 1; i++) { const v1 = (new Vec2()).sub(points[i], points[i - 1]).normalize(); const v2 = (new Vec2()).sub(points[i], points[i + 1]).normalize(); const v = (new Vec2()).add(v1, v2).normalize(); // 得到挤压方向 const norm = new Vec2(-v1.y, v1.x); // 法线方向 const cos = norm.dot(v); const len = halfThick / cos; if(i === 1) { // 起始点 const v0 = new Vec2(...norm).scale(halfThick); outerSide.push((new Vec2()).add(points[0], v0)); innerSide.push((new Vec2()).sub(points[0], v0)); } v.scale(len); outerSide.push((new Vec2()).add(points[i], v)); innerSide.push((new Vec2()).sub(points[i], v)); if(i === points.length - 2) { // 结束点 const norm2 = new Vec2(v2.y, -v2.x); const v0 = new Vec2(...norm2).scale(halfThick); outerSide.push((new Vec2()).add(points[points.length - 1], v0)); innerSide.push((new Vec2()).sub(points[points.length - 1], v0)); } } ... }

4.4 批量绘图

因为我们绘制的是2D图形,通常这些图形可以被视为同一个材质,所以我们可以将所有这些图形网格数据压缩成一个大的类型数组来进行批量绘制。

4.5及及格

您可以使用自定义创建来指定要绘制的绘图元素。

查看代码:

我们可以在渲染管线中应用多个组件管线进行渲染。 有一个特定的渲染管线称为后处理通道,它支持后处理通道的定义。

查看代码:

5、关于性能优化的事情

5.1 性能直观体验

针对可视化场景进行了性能优化。 视觉场景中存在大量重复或相似形状的几何体,因此通过合并顶点进行批量渲染可能非常有效。

查看代码:

查看代码:

5.2 自动和轮廓更新

混色比较消耗性能,所以mesh-js对元素进行判断。 如果当前绘制的元素没有通道(透明度),则不会启用混色,否则将启用混色。

在 中,大多数元素的样式变化,例如,等等,并不涉及轮廓的变化。 在这些情况下,我们不需要重新计算轮廓,因此我们在计算后缓存元素轮廓。 大多数情况下,我们不需要重复计算。 只有当Path的一些特殊属性,如d、 、 等发生变化时,才需要重新计算轮廓。

5.3 密封&

#/zh-cn//

印章是一种特殊的方式。 当我们用一个组合一组图形时,如果只需要使用固定的图形拓扑,可以使用seal方法将子元素的几何图形合并到几何图形中。 这样的几何图形将被合并的几何图形所取代,渲染为单个元素,并且几何图形不能再更改(但位置、颜色等属性仍然可以更改)。

当封印生效时,原子元素的属性将会失效,并被原子元素的属性所取代。

当我们用它来构建组合图形时,这种特殊的方法可以极大地提高渲染性能。

查看代码:

对于绘制完全重复的几何图形,我们也可以用它来进行渲染。

查看代码:

查看代码:

5.4 关于性能开销

有一点需要特别注意:尽量使用条件编译而不是条件分支。

6.一些细节,屏幕适配等。

分享