Skip to content

Latest commit

 

History

History

图形学

canvas 能做什么

  • 画线段、矩形、圆、圆弧、椭圆、贝塞尔曲线、文本,其余图形要么是多边形(只能由多条线段来画)要么贴图,这是一切绘制的基础
  • canvas 没有状态,相当于 n 幅画快速切换,所以绘制的图形得自己保存起来,任何点选事件都得遍历我们保存的数据,通过各种计算判断选择了哪一个对象
  • canvas 操作的是图形吗?不是,操作的是数据和坐标系

每一帧的渲染过程

  • 计算:处理游戏逻辑,计算每个对象的状态,不涉及 DOM 操作(当然也包含对 Canvas 上下文的操作)。
  • 渲染:真正把对象绘制出来。
    • JavaScript 调用 DOM API(包括 Canvas API)以进行渲染。
    • 浏览器(通常是另一个渲染线程)把渲染后的结果呈现在屏幕上的过程。

canvas 中的技巧 || canvas 性能优化

擦除canvas的操作不会立即生效,因为这个调用与其余对CanvasAPI的调用一样,都会被浏览器纳入双缓冲机制中。所以说,浏览器并没有在执行那项耗时的任务之前就擦除背景图像,而稍后它将离屏缓冲区的内容复制制到屏幕上时,才算真正执行了擦除操作。

贝塞尔曲线

讲的的不错 https://juejin.cn/post/6844903760007790600 https://juejin.cn/post/6939742079295848462#heading-21

反求 t 值 1、公式法 2、二分法,牛顿迭代 3、先切分成 n 份

贝塞尔曲线的长度怎么求:

高阶bézier曲线可以用来实现较复杂的图形绘制, 但它具有致命的缺点, 导致它没有得到广泛应用:

  • 参考(https://jelly.jd.com/article/5f869632608b0a015207dbfb)
  • 与其他直线或曲线求交点:随着次数的增加, 求根公式复杂程度陡增, 因此计算高阶bézier曲线的根是不现实的
  • 如果 t ∈ (0, 1) 区间内, 任何一个点都会对较大范围的曲线形状产生影响, 这将导致的问题是: 修改任何一个控制点, 都会牵一发而动全身。工业设计中, 设计师通常是先绘制图形的整体轮廓, 然后再不断地添加局部的细节, 如果修改控制点会影响大范围曲线形状, 那图形的精度是无法保证的, 解决方案是 polybezier

电脑是怎么画的:

  • 计算机底层是没有曲线的绘制函数的, canvas 或 SVG 提供的曲线底层也是使用的分段绘制法, 它将 t 分成若干等分, 假设是100, 得到 t ∈ {0, 0.01, 0.02, ... 1}, -然后将每个 t 值代入曲线公式, 得到对应的 B 点坐标集, 将相邻的B点直线连接起来就能得到最终曲线,

贝塞尔曲线的包围盒: 求一阶导数,然后直接求导其极大值和极小值, 然后在和端点做比较, 得到 曲线在 x 和 y 方向的最大值和最小值, 从而计算出 3阶bézier曲线的 boundingbox

贝塞尔曲线斜率: bézier曲线曲线斜率 k = dy / dx = (dy / dt) / (dx / dt), dy/dt 和 dx / dt 分别是参数方程 y 和 x 方向的一阶导, 因此可以直接求出曲线斜率 k 的表达式。由于法线方向和切线垂直, 因此法线的斜率为 -1 / k。切线主要用于路径动画, 法线则通常用于复杂多边形描边和置换操作

图形库skia剖析

https://juejin.cn/post/6914188284126035981

核心动画编程指南

https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/CoreAnimationBasics/CoreAnimationBasics.html#//apple_ref/doc/uid/TP40004514-CH2-SW3

高性能Web动画和渲染原理系列

https://developer.huawei.com/consumer/cn/doc/search?type=forum&val=%E9%AB%98%E6%80%A7%E8%83%BDWeb%E5%8A%A8%E7%94%BB%E5%92%8C%E6%B8%B2%E6%9F%93%E5%8E%9F%E7%90%86%E7%B3%BB%E5%88%97

svg、canvas 和 webgl

不错的文章:https://juejin.cn/post/6912086785405386765

  • Canvas 能够直接操作绘图上下文,不需要经过 HTML、CSS 解析、构建渲染树、布局等一系列操作。因此单纯绘图的话,Canvas 比 HTML/CSS 和 SVG 要快得多、在重绘图像时,也不会发生重新解析文档和构建结构的过程,开销要小很多。
  • 如果我们要绘制的图形数量非常多,比如有多达数万个几何图形需要绘制,而且它们的位置和方向都在不停地变化,如果使用 Canvas2D 绘制,性能是会达到瓶颈的。这个时候,我们就需要使用 GPU 能力,直接用 WebGL 来绘制。
  • 如果我们要对较大图像的细节做像素处理,比如,实现物体的光影、流体效果和一些复杂的像素滤镜。由于这些效果往往要精准地改变一个图像全局或局部区域的所有像素点,要计算的像素点数量非常的多(一般是数十万甚至上百万数量级的),我们也要用 WebGL 来绘制。
  • WebGL 内置了对 3D 物体的投影、深度检测等特性,所以用它来渲染 3D 物体就不需要我们自己对坐标做底层的处理了。在这种情况下,WebGL 无论是在使用上还是性能上都有很大优势。
  • 数据经过CPU(中央处理单元,负责逻辑计算)处理,成为具有特定结构的几何信息。然后,信息会被送到GPU(图形处理单元,负责图形计算)中进行处理。在GPU中要经过两个步骤生成光栅信息(构成图像的像素矩阵),这些光栅信息会输出到帧缓存(一块内存地址)中,最后渲染到屏幕上。 GPU 是由大量的小型处理单元构成的,它可能远远没有 CPU 那么强大,但胜在数量众多,可以保证每个单元处理一个简单的任务。即使我们要处理一张 800 * 600 大小的图片,GPU 也可以保证这 48 万个像素点分别对应一个小单元,这样我们就可以同时对每个像素点进行计算了。

从写法上来看,因为 SVG 的声明式类似于 HTML 书写方式,本身对前端工程师会更加友好。但是,SVG 图形需要由浏览器负责渲染和管理,将元素节点维护在 DOM 树中。这样做的缺点是,在一些动态的场景中,也就是需要频繁地增加、删除图形元素的场景中,SVG 与一般的 HTML 元素一样会带来 DOM 操作的开销,所以 SVG 的渲染性能相对比较低。

总结来说,利用 SVG 的一个图形对应一个 svg 元素的机制,我们就可以像操作普通的 HTML 元素那样,给 svg 元素添加事件实现用户交互了。所以,SVG 有一个非常大的优点,那就是可以让图形的用户交互非常简单。我们又该怎样确定鼠标在哪个图形元素的内部呢?这对于 Canvas 来说,就是一个比较复杂的问题了。

虽然使用 SVG 绘图能够很方便地实现用户交互,但是有得必有失,SVG 这个设计给用户交互带来便利性的同时,也带来了局限性。为什么这么说呢?因为它和 DOM 元素一样,以节点的形式呈现在 HTML 文本内容中,依靠浏览器的 DOM 树渲染。如果我们要绘制的图形非常复杂,这些元素节点的数量就会非常多。而节点数量多,就会大大增加 DOM 树渲染和重绘所需要的时间。就比如说,在绘制如上的层次关系图时,我们只需要绘制数十个节点。但是如果是更复杂的应用,比如我们要绘制数百上千甚至上万个节点,这个时候,DOM 树渲染就会成为性能瓶颈。事实上,在一般情况下,当 SVG 节点超过一千个的时候,你就能很明显感觉到性能问题了。

幸运的是,对于 SVG 的性能问题,我们也是有解决方案的。比如说,我们可以使用虚拟 DOM 方案来尽可能地减少重绘,这样就可以优化 SVG 的渲染。但是这些方案只能解决一部分问题,当节点数太多时,这些方案也无能为力。这个时候,我们还是得依靠 Canvas 和 WebGL 来绘图,才能彻底解决问题。那在上万个节点的可视化应用场景中,SVG 就真的一无是处了吗?当然不是。SVG 除了嵌入 HTML 文档的用法,还可以直接作为一种图像格式使用。所以,即使是在用 Canvas 和 WebGL 渲染的应用场景中,我们也依然可能会用到 SVG,将它作为一些局部的图形使用,这也会给我们的应用实现带来方便。在后续的课程中,我们会遇到这样的案例。

将用户画出的复杂图形分割成基本的图元。

分割的好坏会直接影响到后面识别的部分。 曲率:曲线上各点沿曲线方向的变化情况,借助曲率可以刻画曲线的几何特征。

  • 曲率 - 几何特征
  • 连续零曲率 - 直线段
  • 连续非零曲率 - 弧线段
  • 局部最大曲率绝对值 - 角点
  • 局部最大曲率正值 - 凸角点
  • 局部最大曲率负值 - 凹角点
  • 曲率过零点 - 特征点

判断图形相似度

两个图形相似,那么其总可以将一个图形乘以一定的倍数,使两个图形全等。 所有的方法,都必须要构建大小差不多的两个图形。可以要计算原始图形的 boundingBox,与绘制的图形 boundingBox,然后将他们归一化到相同的大小。

有了归一化的图形,接下来就是要怎么计算他们的相似度,有两个方法:

比较挫的方法:求两个图形之间交集,差集等,然后分配一定的权重来计算,有一些 JS 库提供了求交、差、补的方法,比如供 Three.js 使用的 ThreeBSP 对图形的外边框进行分割采样,然后计算采样点偏离的方差 当然还有更高大上的方式,比如将两个图形进行填充,然后逐像素比对,求汉明距离。 还可以对图形进行特征值提取比对,甚至深入了机器学习领域。

手绘风格的 canvas

思路

  • 一条直线用两条贝塞尔曲线绘制
  • 填充算法:扫描线填充和种子填充

参考文章

https://juejin.cn/post/6942262577460314143

两大手绘风格图形库

https://app.diagrams.net/ https://excalidraw.com/?from=thosefree.com

以图搜图

均值哈希(像素域) || 感知哈希(频率域) || 颜色分布直方图 || 灰度二值化(RGB权重不同)

方法一(适合缩略图搜原图):

  1. 第一步,缩小尺寸。 将图片缩小到8x8的尺寸,总共64个像素。这一步的作用是去除图片的细节,只保留结构、明暗等基本信息,摒弃不同尺寸、比例带来的图片差异。
  2. 第二步,简化色彩。 将缩小后的图片,转为64级灰度。也就是说,所有像素点总共只有64种颜色。
  3. 第三步,计算平均值。 计算所有64个像素的灰度平均值。
  4. 第四步,比较像素的灰度。 将每个像素的灰度,与平均值进行比较。大于或等于平均值,记为1;小于平均值,记为0。
  5. 第五步,计算哈希值。 将上一步的比较结果,组合在一起,就构成了一个64位的整数,这就是这张图片的指纹。组合的次序并不重要,只要保证所有图片都采用同样次序就行了。如8f373714acfcf4d0
  6. 得到指纹以后,就可以对比不同的图片,看看64位中有多少位是不一样的。 在理论上,这等同于计算"汉明距离"(Hamming distance)。如果不相同的数据位不超过5,就说明两张图片很相似;如果大于10,就说明这是两张不同的图片。

方法二:

如果每种原色都可以取256个值,那么整个颜色空间共有1600万种颜色(256的三次方)。针对这1600万种颜色比较直方图,计算量实在太大了,因此需要采用简化方法。可以将0~255分成四个区:0~63为第0区,64~127为第1区,128~191为第2区,192~255为第3区。这意味着红绿蓝分别有4个区,总共可以构成64种组合(4的3次方)。

  1. 计算每种颜色的像素数量
  2. 把计算的数量按序组成一个64维向量(7414, 230, 0, 0, 8, ..., 109, 0, 0, 3415, 53929)。这个向量就是这张图片的特征值或者叫"指纹"。

参考文章

子图布局的实现方式、力导向图、紧凑树布局

力导布局的优势在于由于互斥力的存在,可以减少节点之间的重叠,但实际上当节点过多时,仍然会出现节点重叠的情况,在有限的空间里想要避免所有节点不重叠,这已被证明是一个 NP 难题(世界难题,目前无解)。

Excel 的实现方式

找出两个图片的差异 || 监测运动的物体 || 运动检测

具体实现方式:

  • 比较两帧图片的差别
  • 逐像素比较 || 利用 ctx.globalCompositeOperation = 'difference'
        function diffTwoImage () {
            // 设置新增元素的合成方式
            ctx.globalCompositeOperation = 'difference'
    
            // 清除画布
            ctx.clearRect(0, 0, canvas.width, canvas.height)
    
            // 假设两张图像尺寸相等
            ctx.drawImage(firstImg, 0, 0)
            ctx.drawImage(secondImg, 0, 0)
        }
  • 黑色代表该位置上的像素未发生改变,而像素越明亮则代表该点的“动作”越大。因此,当连续两帧截图合成后有明亮的像素存在时,即为一个“动作”的产生。但为了让程序不那么“敏感”,我们可以设定一个阈值。当明亮像素的个数大于该阈值时,才认为产生了一个“动作”。当然,我们也可以剔除“不足够明亮”的像素,以尽可能避免外界环境(如灯光等)的影响。
  • 找出差异之后找出包围盒绘制矩形边框即可

思维导图 || 树形图 || 布局算法 || 树形布局

像素画的实现 || 像素化的实现

锯齿 || 走样

参考:https://jelly.jd.com/article/5efd42154b54be014cf4e646

  • 在玩游戏或者使用一些图片处理软件的过程中, 经常遇到某些元素的边缘会出现阶梯状的形状, 导致画面质量大打折扣
  • 通过一些算法减弱这种锯齿状的现象来提高图像质量的过程被称为 抗锯齿 或 反走样。在3D图形生成过程中, 抗锯齿计算可以放在在图形生成的不同阶段, 这也会导致不同程度的性能开销和最终生成图像质量的不同,
  • 显示器是由若干个像素点以方阵的形式排列组成, 每个像素最终呈现的颜色是其中心所在屏幕坐标对应到原始图像上的同样坐标的颜色, 这个过程叫做 采样(sample)
  • 屏幕像素点密度越高, 采样数量越多, 使得绘制出的图像越接近原始图像, 以此类推, 点密度无限高, 绘制出的图像将无限接近于真实图像, 但这显然不可能的, 计算量和生产等多方面都不现实。
  • 图形不管是2D(svg或字体) 还是 3D 的模型, 都是存在于空间坐标中的很多点并且包含绘制规则, 他们跟数学中的抛物线、椭圆等曲线类似, 都是连续可被无限放大的形状, 都属于连续的图像信号。当他们被显示器呈现时, 每个像素都会对其进行采样, 所有像素采样的整个过程叫做 栅格化处理, 这其实也是离散化处理。 Ps 中经常对 文字、形状图层做栅格化处理实际上就是在将 矢量图 转换为 位图。 智能对象虽不是矢量图, 对其栅格化其实是对位图采样, 不属于本文讨论的范畴。
  • 除了在元素边界, 在内部也可能出现锯齿现象,比如内部的边界。
  • 归根结底, 出现锯齿的原因是 因为将连续信号转换为离散信号带来导致精度损失, 并且这是无法避免的。
  • 要让采样得到的曲线更加近似信号源曲线, 就必须缩小采样间隔, 也就是增加像素点密度, 上面已提到此方法会大大增加计算量, 所以不推荐。 另一个方法就是保持当前采样间隔,让偏离较大的值(图3.5右边高频区域)不直接取采样到的值, 而是计算周围一定区域的平均值, 这就是本文要说的 FXAA 抗锯齿算法。
  • 锯齿更容易出现在高频信号区域, 因此要抗锯齿, 首先要提取高频信号区域, 判断高频信号走向, 然后通过走向找到图像边界的切线方向, 沿着切线方向前后模糊几个像素即可。(高频区域就是像素颜色突变的地方,颜色指的是灰度亮度)
  • 取出某个像素,算出左上左下右上右下四邻域,x 方向上中点相连和 y 方向上中点相连算出 tan,求出切线归一化,在切线方向上模糊两三像素 注意:用户的缩放会导致 devicePixelRatio 的改变,就是 ctrl + 的操作

地图库的选择

目前echarts、hightcharts、腾讯地图等都可以实现地图的可视化功能,但是echarts、hightcharts对于地图可视化的实现并没有细化到区县甚至到街道的地图并且目前全国地图的展示不符合国家的一个规范展示;腾讯地图对于地图可视化的实现是有成熟的技术支持与友好的展示,并且可支持到街道、门店粒度的可视化展示。 因此,选取腾讯地图作为基础底层地图,通过腾讯地图的 JavaScript API GL + 数据可视化 JS API 实现地图上可视化的图层。

抽稀算法

在处理矢量化数据时,记录中往往会有很多重复数据,对进一步数据处理带来诸多不便。多余的数据一方面浪费了较多的存储空间,另一方面造成所要表达的图形不光滑或不符合标准。因此要通过某种规则,在保证矢量曲线形状不变的情况下, 最大限度地减少数据点个数,这个过程称为抽稀。

使用Douglas-Peuker算法,也称抽稀算法。

  • 该算法是在曲线 起始点S和结束点E 之间连接一条直线SE,该直线为曲线的弦;
  • 首先找到曲线上离直线距离最远的点A,计算其与曲线弦的距离B;
  • 比较该距离与预先给定的阈值threshold的大小,如果小于threshold,则该直线段作为曲线的近似,该段曲线处理完毕;
  • 如果距离大于阈值,则用A将曲线分为两段SA和EA,并分别对两段取信进行第1~3的处理;
  • 当所有曲线都处理完毕时,依次连接各个分割点形成的折线,即可以作为曲线的近似

参考:https://juejin.cn/post/7024400024880545829

gpu.js || 使用GPU.js提升Javascript性能

参考:https://jelly.jd.com/article/5fc99ef90f0a6d0144e53761