深入浅出WebGL-08-玩一下canvas2d

1,304 阅读4分钟

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

本文发布在专栏 深入浅出WebGL 中, 点击查看目录

这篇文章我们来轻松一下, 玩一下canvas 2d, 实现一些有趣的效果, 大家可以一起尝试一下。

把canvas涂上颜色

首先先搞个html:


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>canvas 2d</title>
    <style>
        *{
            margin: 0;
            padding: 0;
        }
        canvas {
            width: 100vw;
            height: 100vh
        }
    </style>
</head>
<body>
    <canvas id="canvas"></canvas>
</body>
<script src="./index.js"></script>
</html>

canvas 像素操作

在canvas 2d中, 可以依赖ImageData来进行像素操作的。我们可以用 getImageData(), putImageData(), createImageData() 来操作像素。

createImageData() getImageData() 可以得到一个ImageData对象, 这个对象的data是一个类数组Uint8ClampedArray。还有width和height两个属性。

它表示一个宽是width高是height的图像, 其中每一个像素的颜色是在data中定义的, 从左到右从上到下排列在Uint8ClampedArray中。4个一组代表rgba。

比如前四项分别为 [255,0,0,255]。那么就代表第一个像素是红色的。那么如果我们用createImageData() 创建一个大小跟canvas一样大的空的ImageData。 然后在修改里面的data值, 最后putImageData()画到canvas 上。我们就可以随便涂鸦canvas的每一个像素了!原则上我们就可以整很多活了! 我们先试试把整个canvas 涂红色!

const canvas = document.getElementById('canvas')

function resizeCanvasSize(){
  canvas.width = canvas.clientWidth * window.devicePixelRatio
  canvas.height = canvas.clientHeight * window.devicePixelRatio
}

resizeCanvasSize()

const ctx = canvas.getContext('2d')
const imageData = ctx.createImageData(canvas.width, canvas.height)


function draw(x, y){
  return {
    x: 255,
    y: 0,
    z: 0,
    w: 255
  }
}

for( let i = 0; i < imageData.width; ++i ) {
    for( let j = 0; j < imageData.height; ++j ){
        const index = imageData.width * j * 4 + i * 4;
        
        const color = draw(i, j);

        imageData.data[index] = color.x;
        imageData.data[index+1] = color.y;
        imageData.data[index+2] =  color.z;
        imageData.data[index+3] = color.w;
    }
}

ctx.putImageData(imageData, 0, 0,0,0 ,canvas.width, canvas.height)

现在我们能够对canvas每个像素着色了。接下来我们尝试玩点花活吧!


画个三角形

我们如何利用我们能对每一个像素着色这个能力去画一个三角形呢?首先我们先明确我们需要提供给程序三个点作为三角形的顶点, 然后我们遍历每一个像素, 判断三角形是否在三角形内部就可以了。

我们可以用向量叉积的形式判断一个点是否在三角形里面。如下图:

image.png

我们看到蓝色的向量就是三角形顶点减出来的。然后我们分别用三个向量叉乘p减去三个向量的起点得到的向量。例如:

我们设置:

x=A~C~\vec x = \tilde A - \tilde C

y=B~A~\vec y = \tilde B - \tilde A

z=C~B~\vec z = \tilde C - \tilde B

q=P~C~\vec q = \tilde P - \tilde C

w=P~A~\vec w = \tilde P - \tilde A

e=P~B~\vec e = \tilde P - \tilde B

我们分别计算 x×q \vec x × \vec q y×w\vec y × \vec w z×e\vec z × \vec e。(可以右手定则比一下) 然后判断叉乘出的结果是否是同向的。(二维的相当于z都是0, 直接补0就可以了)。这样就可以知道每一个点在三角形内部还是外部了。

代码大家有兴趣可以尝试自己实现一下哈哈,接下来说一个比较重要的东西

重心坐标

我们上面遍历每个像素是否在三角形内部, 接下来我们要做的不仅仅是做到这些, 还要判断这个点在三角形的什么位置。比如说我们接下来要画的结果如下:

image.png

观察到每个顶点的位置分别为红, 绿, 蓝。其余三角形的点根据距离三个顶点多远来取有红色的比例,蓝色的比例, 绿色的比例。

这件事情很有用, WebGL中各种图像上的插值都是用这种方法做的。这里就是根据顶点的颜色值来插值。以后也可以插值法向, 图片纹理uv。因为显示东西(光栅化)这件事是要决定每一个像素的颜色。但是我们的输入仅仅是顶点和顶点上附着的信息,那么其他的点就需要程序推断出来。这个过程叫做插值。插值还可以用到运动里面。比如一个物体决定了起始位置和终点。那么它中间的状态就需要插值出来。 一些剪辑视频的软件定义关键帧, 中间过程自动生成帧的过程也是插值。

如何计算重心坐标呢?

image.png

上图, 我们说某点的重心坐标(在三角形里也叫面积坐标)是三个小三角形分别对整个大三角形的比值。每个三角形跟大三角形的比值都决定了这个点距离对面顶点的比例。例如:

ABP/ABC\triangle ABP / \triangle ABC 就是点p到c点的比例。

我们观察这个三角形比值,发现他们都有同一个底。比如上面的三角形都有共同的底AB。那么其实三角形比值就是p到AB的距离比上C到AB的距离。(点到直线的距离, 这件事相信各位在高中的时候一定滚瓜烂熟了)。

点到直线的距离公式: 设直线的方程为Ax+By+C=0,点 P 的坐标为(x0,y0)则P到直线的距离为:

首先根据P1 P2 用两点式(应该是初中学的)求出实现方程。然后发现两个点到直线距离的比值可以吧下面的根号部分直接干掉。所以计算后的结果写成代码应该是这样的:

省略了一通初中数学运算的过程...


    const u = ((P2.y - P3.y) * P.x + (P3.x - P2.x) * P.y + (P2.x * P3.y - P3.x * P2.y)) / ((P2.y - P3.y) * P1.x + (P3.x - P2.x) * P1.y + (P2.x * P3.y - P3.x * P2.y)); 
    const v = ((P1.y - P3.y) * P.x + (P3.x - P1.x) * P.y + (P1.x * P3.y - P3.x * P1.y)) / ((P1.y - P3.y) * P2.x + (P3.x - P1.x) * P2.y + (P1.x * P3.y - P3.x * P1.y));
    const w = 1 - u - v;

完整代码实现:


const canvas = document.getElementById('canvas')

function resizeCanvasSize(){
  canvas.width = canvas.clientWidth * window.devicePixelRatio;
  canvas.height = canvas.clientHeight * window.devicePixelRatio;
}

resizeCanvasSize()

const ctx = canvas.getContext('2d')

const imageData = ctx.createImageData(canvas.width, canvas.height)

const vertex = [
    400, 100,
    1200, 100,
    800, 600,
]


function draw(x,y){
 
    const P = {x, y}
    const P1 = {x: vertex[0], y: vertex[1]}
    const P2 = {x: vertex[2], y: vertex[3]}
    const P3 = {x: vertex[4], y: vertex[5]}

    const u = ((P2.y - P3.y) * P.x + (P3.x - P2.x) * P.y + (P2.x * P3.y - P3.x * P2.y)) / ((P2.y - P3.y) * P1.x + (P3.x - P2.x) * P1.y + (P2.x * P3.y - P3.x * P2.y)); 
    const v = ((P1.y - P3.y) * P.x + (P3.x - P1.x) * P.y + (P1.x * P3.y - P3.x * P1.y)) / ((P1.y - P3.y) * P2.x + (P3.x - P1.x) * P2.y + (P1.x * P3.y - P3.x * P1.y));
    const w = 1 - u - v;
    
    if(u > 0 && u< 1 && v> 0 && v< 1 && w> 0 && w< 1) {
        return {
            x: 255 * u,
            y: 255 * v,
            z: 255 * w,
            w: 255,
        }
    } 

    return {
        x: 0,
        y: 0,
        z: 0,
        w: 255,
    }
}


for( let i = 0; i < imageData.width; ++i ) {
    for( let j = 0; j < imageData.height; ++j ){
        const index = imageData.width * j * 4 + i * 4;
        
        const color = draw(i, j);

        imageData.data[index] = color.x;
        imageData.data[index+1] = color.y;
        imageData.data[index+2] =  color.z;
        imageData.data[index+3] = color.w;
    }
}

console.log(imageData)
ctx.putImageData(imageData, 0, 0,0,0 ,canvas.width, canvas.height)

计算重心坐标这件事情也是图形学中基本的知识。网络上水也不深,谁都能把握得住。这相关的文章还是有很多很多的。 简易大家还是熟悉这个计算过程。本章简单的canvas 2d的用法以后的文章也都会用到。这个canvas2d 的能力在你的手中会越来越牛逼。respect!