前言
有小伙伴和我说前几篇的从入门到放弃WebGL
里面的那些个类和一些方法不太看得懂,那我依次一个个来推导说一下。
方法:getMousePosInWebgl
用处
将 CSS 的位置变成 Webgl 的位置
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| function getMousePosInWebgl({ clientX, clientY }, canvas) { const { left, top, width, height } = canvas.getBoundingClientRect(); const [cssX, cssY] = [clientX - left, clientY - top]; const [halfWidth, halfHeight] = [width / 2, height / 2]; const [xBaseCenter, yBaseCenter] = [cssX - halfWidth, cssY - halfHeight]; const yBaseCenterTop = -yBaseCenter; return { x: xBaseCenter / halfWidth, y: yBaseCenterTop / halfHeight, }; }
|
方法:glToCssPos
用处
根据 webgl 坐标得到 canvas 坐标原点移动到 webgl 中的原点的位置的坐标
代码
1 2 3 4 5 6 7
| function glToCssPos({ x, y }, { width, height }) { const [halfWidth, halfHeight] = [width / 2, height / 2]; return { x: x * halfWidth, y: -y * halfHeight, }; }
|
这里很好理解,就是把上面的getMousePosInWebgl
去掉了最后一步的基底转化和方向变化,得到的 x,y 就是getMousePosInWebgl
中的xBaseCenter, yBaseCenter
,即将 canvas 坐标系的原点移动到 webgl 坐标系原点的位置的坐标
方法:ScaleLinear
用处
得到两点之间的直线关系公式(点斜式),这个直线上的所有点都满足这个点斜式关系
代码
1 2 3 4 5 6 7 8 9 10 11 12
| function ScaleLinear(ax, ay, bx, by) { const delta = { x: bx - ax, y: by - ay, }; const k = delta.y / delta.x; const b = ay - ax * k; return function (x) { return k * x + b; }; }
|
我们都知道,两个点,可以构成一个点斜式关系,就是 y=kx+b;假如我们有两个点(ax,ay),(bx,by),那么斜率 k 就是(by-ay)/(bx-ax)
,此时的 b 就等于ay-ax*k
或者by-bx*k
,然后这个函数返回了k*x+b
的关系式。
然后我们可以看到,我们在图形转面里面是这样使用的
1 2 3
| const scaleX = ScaleLinear(0, minX, 600, maxX); const scaleY = ScaleLinear(0, maxY, 600, minY);
|
也就是可以得到这个 x 和 y 分别的对应关系,这里的 maxY 和 minY 的位置互换是因为 webgl 坐标系坐标的 y 轴是向上的。
类:Compose
用处
承担容器的作用
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| export default class Compose { constructor() { this.parent = null; this.children = new Set(); } add(obj) { obj.parent = this; this.children.add(obj); } update(t) { this.children.forEach((ele) => { ele.update(t); }); } }
|
add 添加的 obj 是一个时间轨对象(Track),update 调用的也是时间轨对象,作用就是放置多个时间轨对象(Track)
类:Track
用处
用来绘制补间动画,即两个时间点,有不同的状态,从状态一到状态二的变化过程。
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| export default class Track { constructor(target) { this.target = target; this.parent = null; this.start = 0; this.timeLen = 5; this.loop = false; this.keyMap = new Map();
this.onEnd = () => {}; this.prevTime = 0; } update(t) { const { keyMap, timeLen, target, loop, start, prevTime } = this; let time = t - start; if (timeLen >= prevTime && timeLen < time) { this.onEnd(); } this.prevTime = time; if (loop) { time = time % timeLen; } for (const [key, fms] of keyMap) { const last = fms.length - 1; if (time < fms[0][0]) { target[key] = fms[0][1]; } else if (time > fms[last][0]) { target[key] = fms[last][1]; } else { target[key] = getValBetweenFms(time, fms, last); } } } }
function getValBetweenFms(time, fms, last) { for (let i = 0; i < last; i++) { const fm1 = fms[i]; const fm2 = fms[i + 1]; if (time >= fm1[0] && time <= fm2[0]) { const delta = { x: fm2[0] - fm1[0], y: fm2[1] - fm1[1], }; const k = delta.y / delta.x; const b = fm1[1] - fm1[0] * k; return k * time + b; } } }
|
然后大家可以对照星星对你眨眼睛的代码来看一下,是不是就是这么个逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| const track = new Track(obj); track.start = new Date(); track.timeLen = 2000; track.loop = true; track.keyMap = new Map([ [ "a", [ [500, a], [1000, 0], [1500, a], ], ], ]); compose.add(track); !(function ani() { compose.update(new Date()); render(); requestAnimationFrame(ani); })();
|
类:Sky
用处
本质上还是只是个容器而已,调用的都是子类Poly
的方法
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| export default class Sky { constructor(gl) { this.gl = gl; this.children = []; } add(obj) { obj.gl = this.gl; this.children.push(obj); } updateVertices(params) { this.children.forEach((ele) => { ele.updateVertices(params); }); } draw() { this.children.forEach((ele) => { ele.init(); ele.draw(); }); } }
|
类:Poly
用处
绘制多边形
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| const defAttr = () => ({ gl: null, vertices: [], geoData: [], size: 2, attrName: "a_Position", uniName: "u_IsPOINTS", count: 0, types: ["POINTS"], circleDot: false, u_IsPOINTS: null, }); export default class Poly { constructor(attr) { Object.assign(this, defAttr(), attr); this.init(); } init() { const { attrName, size, gl, circleDot } = this; if (!gl) { return; } const vertexBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); this.updateBuffer(); const a_Position = gl.getAttribLocation(gl.program, attrName); gl.vertexAttribPointer(a_Position, size, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(a_Position); if (circleDot) { this.u_IsPOINTS = gl.getUniformLocation(gl.program, "u_IsPOINTS"); } } updateBuffer() { const { gl, vertices } = this; this.updateCount(); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); } updateCount() { this.count = this.vertices.length / this.size; } addVertice(...params) { this.vertices.push(...params); this.updateBuffer(); } popVertice() { const { vertices, size } = this; const len = vertices.length; vertices.splice(len - size, len); this.updateCount(); } setVertice(ind, ...params) { const { vertices, size } = this; const i = ind * size; params.forEach((param, paramInd) => { vertices[i + paramInd] = param; }); } updateVertices(params) { const { geoData } = this; const vertices = []; geoData.forEach((data) => { params.forEach((key) => { vertices.push(data[key]); }); }); this.vertices = vertices; } draw(types = this.types) { const { gl, count, circleDot, u_IsPOINTS } = this; for (let type of types) { circleDot && gl.uniform1f(u_IsPOINTS, type === "POINTS"); gl.drawArrays(gl[type], 0, count); } } }
|
然后我们拿狮子座的代码来理解一下这个Poly
类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| const sky = new Sky(gl);
const compose = new Compose();
let poly = null;
let point = null;
canvas.addEventListener("mousedown", (event) => { if (event.button === 2) { popVertice(); } else { const { x, y } = getMousePosInWebgl(event, canvas); if (poly) { addVertice(x, y); } else { crtPoly(x, y); } } render(); });
canvas.addEventListener("mousemove", (event) => { const { x, y } = getMousePosInWebgl(event, canvas); point = hoverPoint(x, y); canvas.style.cursor = point ? "pointer" : "default"; if (poly) { const obj = poly.geoData[poly.geoData.length - 1]; obj.x = x; obj.y = y; } });
!(function ani() { compose.update(new Date()); sky.updateVertices(["x", "y", "pointSize", "alpha"]); render(); requestAnimationFrame(ani); })();
function crtPoly(x, y) { let o1 = point ? point : { x, y, pointSize: random(), alpha: 1 }; const o2 = { x, y, pointSize: random(), alpha: 1 }; poly = new Poly({ size: 4, attrName: "a_Attr", geoData: [o1, o2], types: ["POINTS", "LINE_STRIP"], circleDot: true, }); sky.add(poly); crtTrack(o1); crtTrack(o2); }
function addVertice(x, y) { const { geoData } = poly; if (point) { geoData[geoData.length - 1] = point; } let obj = { x, y, pointSize: random(), alpha: 1 }; geoData.push(obj); crtTrack(obj); }
function popVertice() { poly.geoData.pop(); const { children } = compose; const last = children[children.length - 1]; children.delete(last); poly = null; }
|
上面的代码只用到了·geoData·,而前面我们绘制内容的时候也用到了·vertices·
类:ShapeGeo
用处
根据轨迹点图形得到能涂满颜色的多个三角形,核心逻辑还是那几个
1.寻找满足以下条件的▲ABC:
▲ABC的顶点索引位置连续,如012,123、234
点C在向量AB的正开半平面里,可以理解为你站在A点,面朝B点,点C要在你的左手边
▲ABC中没有包含路径G 中的其它顶点
2.当找到▲ABC 后,就将点B从路径的顶点集合中删掉,然后继续往后找。
3.当路径的定点集合只剩下3个点时,就结束。
4.由所有满足条件的▲ABC构成的集合就是我们要求的独立三角形集合。
这下面的方式就是按照上面的核心逻辑来的
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
| export default class ShapeGeo { constructor(pathData=[]) { this.pathData = pathData; this.geoData = []; this.triangles = []; this.vertices = []; this.parsePath(); this.update(); } update() { this.vertices = []; this.triangles = []; this.findTriangle(0); this.upadateVertices() } parsePath() { this.geoData = []; const { pathData, geoData } = this for (let i = 0; i < pathData.length; i += 2) { geoData.push({ x: pathData[i], y: pathData[i + 1] }) } } findTriangle(i) { const { geoData, triangles } = this; const len = geoData.length; if (geoData.length <= 3) { triangles.push([...geoData]); } else { const [i0, i1, i2] = [ i % len, (i + 1) % len, (i + 2) % len ]; const triangle = [ geoData[i0], geoData[i1], geoData[i2], ];
if (this.cross(triangle) > 0 && !this.includePoint(triangle)) { triangles.push(triangle); geoData.splice(i1, 1); } this.findTriangle(i1); } } includePoint(triangle) { for (let ele of this.geoData) { if (!triangle.includes(ele)) { if (this.inTriangle(ele, triangle)) { return true; } } } return false; } inTriangle(p0, triangle) { let inPoly = true; for (let i = 0; i < 3; i++) { const j = (i + 1) % 3; const [p1, p2] = [triangle[i], triangle[j]]; if (this.cross([p0, p1, p2]) < 0) { inPoly = false; break } } return inPoly; } cross([p0, p1, p2]) { const [ax, ay, bx, by] = [ p1.x - p0.x, p1.y - p0.y, p2.x - p0.x, p2.y - p0.y, ]; return ax * by - bx * ay; } upadateVertices() { const arr = [] this.triangles.forEach(triangle => { for (let { x, y } of triangle) { arr.push(x, y) } }) this.vertices = arr } }
|
大概的逻辑我在上面都写了注释,接下来再讲讲一些晦涩难懂的函数,比如,正半区为什么可以使用cross(...param)>0
这里其实使用了向量的差积的理解
矢量叉积的知识:
设矢量P = ( x1, y1 ),Q = ( x2, y2 ),则矢量叉积定义为由(0,0)、p1、p2和p1+p2所组成的平行四边形的带符号
的面积,即:P × Q = x1y2 - x2y1,其结果是一个标量。显然有性质 P × Q = - ( Q × P ) 和 P × ( - Q )
= - ( P × Q )。一般在不加说明的情况下,本文下述算法中所有的点都看作矢量,两点的加减法就是矢量相加减,而点的乘法则看作矢量叉积。
叉积的一个非常重要性质是可以通过它的符号判断两矢量相互之间的顺逆时针关系:
若 P × Q > 0 , 则Q在P的逆时针方向。
若 P × Q < 0 , 则Q在P的顺时针方向。
若 P × Q = 0 , 则P与Q共线,但可能同向也可能反向。
结语
本篇文章就到这里结束了,更多内容敬请期待,债见~