🌀 一、动画与 requestAnimationFrame
1. 早期的定时动画
概念
- 在
requestAnimationFrame
出现之前,开发者常用setInterval
或setTimeout
来驱动动画。
原理
setInterval(fn, 16)
→ 每 16ms 调用一次函数,理想状态下约等于 60fps。- 但问题是 浏览器渲染节奏不受控制,有时会掉帧或提前调用。
对比
-
定时器:
- 固定时间触发,与屏幕刷新率不匹配。
- 页面不可见时仍然运行(浪费性能)。
-
rAF:
- 自动匹配刷新率(通常 60Hz,即每 16.6ms)。
- 页面不可见时会自动暂停(节省性能)。
实践(定时器动画示例)
ini
let x = 0;
function moveBox() {
const box = document.getElementById("box");
x += 2;
box.style.transform = `translateX(${x}px)`;
}
// 每 16ms 调用一次
setInterval(moveBox, 16);
2. 时间间隔问题(精度)
概念
-
setTimeout
和setInterval
的 实际间隔不可靠,可能受以下因素影响:- 浏览器负载(CPU 忙时会延迟)。
- 最小时间粒度(现代浏览器最小约 4ms)。
- 不同设备刷新率不同(60Hz / 120Hz)。
rAF 的优势
- 始终在下一帧绘制之前执行,跟屏幕刷新同步。
- 内部提供时间戳参数,方便计算动画进度。
实践(rAF 动画)
ini
let x = 0;
function animate() {
const box = document.getElementById("box");
x += 2;
box.style.transform = `translateX(${x}px)`;
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
3. cancelAnimationFrame
概念
- 类似
clearInterval
,用于取消requestAnimationFrame
。
原理
requestAnimationFrame(fn)
返回一个id
。- 调用
cancelAnimationFrame(id)
即可停止。
实践
ini
let animId;
function animate() {
const box = document.getElementById("box");
let x = parseInt(box.style.left || 0);
box.style.left = (x + 2) + "px";
animId = requestAnimationFrame(animate);
}
// 启动动画
animId = requestAnimationFrame(animate);
// 3 秒后停止
setTimeout(() => cancelAnimationFrame(animId), 3000);
注释:
animId
保存了当前动画的 id。cancelAnimationFrame(animId)
→ 停止循环调用。
4. rAF 的使用 ------ 节流(以 scroll 为例)
概念
-
节流(throttle) :避免在高频事件中反复触发昂贵操作。
-
典型场景:
scroll
、resize
。 -
思路:
scroll
事件可能一秒触发几十次。- 用一个布尔标记 + rAF → 保证每帧只处理一次。
实践:scroll
事件节流
ini
let ticking = false;
window.addEventListener("scroll", () => {
// 如果已经有 rAF 在等待,就不再重复申请
if (!ticking) {
requestAnimationFrame(() => {
// 这里是安全的绘制逻辑(每帧最多执行一次)
console.log("Scroll Y:", window.scrollY);
// 标记重置,允许下一帧继续
ticking = false;
});
ticking = true;
}
});
逐行注释:
let ticking = false;
→ 标记是否已经有一个 rAF 在队列中。scroll
回调可能触发 30~100 次/秒。if (!ticking)
→ 保证只触发一次rAF
。requestAnimationFrame(...)
→ 把任务放到下一帧渲染时执行。ticking = false;
→ 动作完成后,释放标记,允许下次继续。
🎨 二、Canvas 基本画布功能
0. toDataURL()
------ 导出画布为图片
概念
-
canvas.toDataURL(type, quality?)
→ 将当前画布的像素数据导出为 Base64 图片字符串。 -
常用格式:
"image/png"
(默认,带透明)"image/jpeg"
(可调节质量)
原理
- Canvas 内部维护一个位图(像素缓冲区)。
toDataURL
会把这块内存编码为指定格式的图片,再转成 Base64 字符串。
对比
toDataURL()
→ 返回字符串,适合预览。toBlob()
→ 返回二进制数据,适合上传/下载。
示例
ini
<canvas id="c1" width="200" height="200"></canvas>
<img id="preview">
<script>
const canvas = document.getElementById("c1");
const ctx = canvas.getContext("2d");
// 绘制一个红色矩形
ctx.fillStyle = "red";
ctx.fillRect(20, 20, 150, 150);
// 导出为 PNG Base64
const imgURL = canvas.toDataURL("image/png");
// 显示到 <img> 标签
document.getElementById("preview").src = imgURL;
</script>
注释:
fillStyle = "red"
→ 设置填充颜色为红色。fillRect(20,20,150,150)
→ 绘制一个矩形。toDataURL("image/png")
→ 导出图片数据。- 把导出的字符串赋值给
<img>.src
,即可显示结果。
1. 2D 绘图上下文(CanvasRenderingContext2D
)
概念
-
通过
canvas.getContext("2d")
获取的对象,提供 绘图 API。 -
它是一个 状态机,存储:
- 当前颜色 / 线条样式
- 当前路径
- 变换矩阵(旋转、缩放)
- 全局透明度、字体、阴影等
对比
"2d"
→ 主要用于图形绘制。"webgl"
→ 用于 GPU 3D 渲染。
示例
ini
const canvas = document.getElementById("c1");
const ctx = canvas.getContext("2d");
ctx.fillStyle = "blue";
ctx.fillRect(50, 50, 100, 100);
2. 填充与描边
概念
fillStyle
→ 设置填充颜色或渐变。strokeStyle
→ 设置描边颜色。lineWidth
→ 设置描边宽度。
示例
ini
ctx.fillStyle = "green"; // 填充为绿色
ctx.strokeStyle = "black"; // 描边为黑色
ctx.lineWidth = 5; // 线宽 5px
ctx.fillRect(30, 30, 120, 80); // 绘制填充矩形
ctx.strokeRect(30, 30, 120, 80); // 绘制描边矩形
3. 绘制矩形
概念
fillRect(x,y,w,h)
→ 绘制填充矩形。strokeRect(x,y,w,h)
→ 绘制描边矩形。clearRect(x,y,w,h)
→ 擦除矩形区域。
示例:两个重叠的矩形 + 清除重叠的一小块
ini
ctx.fillStyle = "blue";
ctx.fillRect(20, 20, 150, 100); // 蓝色矩形
ctx.fillStyle = "red";
ctx.fillRect(80, 60, 150, 100); // 红色矩形,部分重叠
// 清除重叠的一小块区域 (40×40)
ctx.clearRect(100, 80, 40, 40);
4. 绘制路径(表盘示例)
概念
beginPath()
→ 开始新路径。arc(x,y,r,start,end)
→ 绘制圆。moveTo(x,y)
/lineTo(x,y)
→ 绘制线。stroke()
/fill()
→ 渲染路径。isPointInPath(x,y)
→ 判断点是否在路径内。
示例:简易时钟表盘
scss
// 外圆
ctx.beginPath();
ctx.arc(150, 150, 100, 0, Math.PI * 2);
ctx.stroke();
// 内圆(中心点)
ctx.beginPath();
ctx.arc(150, 150, 5, 0, Math.PI * 2);
ctx.fill();
// 时针
ctx.beginPath();
ctx.moveTo(150, 150);
ctx.lineTo(150, 90);
ctx.stroke();
// 分针
ctx.beginPath();
ctx.moveTo(150, 150);
ctx.lineTo(200, 150);
ctx.stroke();
判断点击位置
ini
canvas.addEventListener("click", e => {
const x = e.offsetX, y = e.offsetY;
ctx.beginPath();
ctx.arc(150, 150, 100, 0, Math.PI * 2);
if (ctx.isPointInPath(x, y)) {
console.log("点击在表盘内");
}
});
多个对象如何区分?
👉 通常需要 手动维护对象列表,例如:
ini
const objects = [
{ type: "circle", x: 150, y: 150, r: 100 },
{ type: "hand", x1: 150, y1: 150, x2: 200, y2: 150 }
];
canvas.addEventListener("click", e => {
const x = e.offsetX, y = e.offsetY;
for (let obj of objects) {
ctx.beginPath();
if (obj.type === "circle") {
ctx.arc(obj.x, obj.y, obj.r, 0, Math.PI * 2);
if (ctx.isPointInPath(x, y)) console.log("点中表盘");
}
}
});
5. 绘制文本
概念
ctx.font
→ 设置字体,如"24px serif"
。ctx.fillText(text, x, y)
→ 填充文本。ctx.strokeText(text, x, y)
→ 描边文本。ctx.textAlign
→ 控制水平对齐(left, center, right)。ctx.textBaseline
→ 控制垂直对齐(top, middle, bottom, alphabetic)。ctx.measureText(text)
→ 获取文本宽度。
示例
ini
ctx.font = "24px serif"; // 设置字体大小和样式
ctx.fillStyle = "purple"; // 设置填充颜色
ctx.fillText("Hello Canvas", 50, 200); // 绘制填充文本
ctx.strokeStyle = "black"; // 设置描边颜色
ctx.strokeText("Outline Text", 50, 240); // 绘制描边文本
🎭 三、Canvas 变换 (Transformation)
Canvas 提供了一套 变换矩阵 API ,让我们可以对绘制内容进行 平移、旋转、缩放、组合变换,而不是手动计算每个点的位置。
这些变换操作不会直接作用在图像像素上,而是作用在 坐标系统 上。
👉 也就是说:变换后再绘制,图形会自动跟随新的坐标系。
1. 平移 (translate
)
概念
ctx.translate(dx, dy)
→ 将画布原点(0,0)
平移到(dx,dy)
。- 之后绘制的图形都会基于新的坐标系。
示例
ini
ctx.fillStyle = "blue";
ctx.fillRect(0, 0, 50, 50); // 在 (0,0) 处绘制
ctx.translate(100, 50); // 平移坐标系
ctx.fillStyle = "red";
ctx.fillRect(0, 0, 50, 50); // 实际位置在 (100,50)
注释:
- 第一个矩形在原点绘制。
translate(100,50)
→ 原点移动到 (100,50)。- 第二个矩形看似在
(0,0)
,其实已经被平移。
2. 旋转 (rotate
)
概念
ctx.rotate(angle)
→ 旋转坐标系,单位是 弧度。- 旋转中心是 当前原点。
示例
ini
ctx.translate(100, 100); // 把原点移到 (100,100)
ctx.rotate(Math.PI / 4); // 旋转 45°
ctx.fillStyle = "green";
ctx.fillRect(0, 0, 80, 40); // 矩形被旋转
注释:
- 旋转前通常要
translate
,否则默认绕(0,0)
转。 Math.PI/4
→ 45 度。- 所有后续绘制都会在旋转后的坐标系下进行。
3. 缩放 (scale
)
概念
ctx.scale(sx, sy)
→ 缩放坐标系。sx > 1
→ 放大,0 < sx < 1
→ 缩小。sy
控制垂直缩放。
示例
ini
ctx.fillStyle = "orange";
ctx.fillRect(10, 10, 50, 50); // 正常大小
ctx.scale(2, 1.5); // X 方向放大 2 倍,Y 方向放大 1.5 倍
ctx.fillStyle = "purple";
ctx.fillRect(10, 10, 50, 50); // 实际大小变成 100×75
4. 综合变换 (transform
)
概念
-
ctx.transform(a, b, c, d, e, f)
→ 矩阵相乘,改变当前变换矩阵。 -
矩阵含义:
css[ a c e ] [ b d f ] [ 0 0 1 ]
-
坐标
(x,y)
变换结果:inix' = ax + cy + e y' = bx + dy + f
参数意义
a
= 水平方向缩放b
= 垂直倾斜c
= 水平倾斜d
= 垂直缩放e
= 水平平移f
= 垂直平移
示例
ini
// 水平放大 1.5 倍,垂直放大 1 倍,右移 50
ctx.transform(1.5, 0, 0, 1, 50, 0);
ctx.fillStyle = "cyan";
ctx.fillRect(0, 0, 50, 50);
5. 设置变换 (setTransform
)
概念
-
ctx.setTransform(a,b,c,d,e,f)
→ 直接重置 当前变换矩阵为指定值。 -
和
transform
的区别:transform
是 叠加。setTransform
是 覆盖(重新设定)。
示例
ini
// 先平移再旋转
ctx.translate(100, 100);
ctx.rotate(Math.PI / 6);
// 直接重置为单位矩阵 (无变换)
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.fillStyle = "red";
ctx.fillRect(0, 0, 50, 50); // 在原始坐标系绘制
6. 保存与恢复 (save
/ restore
)
概念
ctx.save()
→ 保存当前绘制状态(样式、变换矩阵、裁剪路径等)。ctx.restore()
→ 恢复到最近一次保存的状态。- 常用于:多个对象有不同变换,不想互相干扰。
示例:三个矩形不同位置
ini
// 第一个矩形
ctx.fillStyle = "blue";
ctx.fillRect(10, 10, 50, 50);
// 第二个矩形(平移+旋转)
ctx.save(); // 保存状态
ctx.translate(120, 50);
ctx.rotate(Math.PI / 4);
ctx.fillStyle = "red";
ctx.fillRect(0, 0, 50, 50);
ctx.restore(); // 恢复到之前的状态
// 第三个矩形(缩放)
ctx.save();
ctx.translate(200, 50);
ctx.scale(2, 0.5);
ctx.fillStyle = "green";
ctx.fillRect(0, 0, 50, 50);
ctx.restore();
注释:
- 第一个矩形不受影响。
- 第二个矩形只在
save/restore
内旋转。 - 第三个矩形只在
save/restore
内缩放。 restore
确保每个矩形独立。
🎨 四、Canvas 进阶功能
1. 绘制图像 (drawImage
)
概念
-
ctx.drawImage()
→ 将图像绘制到画布上。 -
常见用法:
ctx.drawImage(img, dx, dy)
→ 绘制原图。ctx.drawImage(img, dx, dy, dw, dh)
→ 缩放绘制。ctx.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh)
→ 裁剪 + 缩放绘制。
原理
- 浏览器把
<img>
或另一个<canvas>
的像素数据复制到目标区域。
示例:绘制与裁剪
ini
<canvas id="c1" width="300" height="200"></canvas>
<img id="source" src="example.jpg" hidden>
<script>
const canvas = document.getElementById("c1");
const ctx = canvas.getContext("2d");
const img = document.getElementById("source");
img.onload = () => {
// 直接绘制原图
ctx.drawImage(img, 0, 0);
// 缩放绘制到 (150,0),大小 100×100
ctx.drawImage(img, 150, 0, 100, 100);
// 裁剪 (50,50,100,100),放到 (0,100),缩放为 120×120
ctx.drawImage(img, 50, 50, 100, 100, 0, 100, 120, 120);
};
</script>
2. 阴影 (shadowColor
, shadowBlur
, shadowOffsetX
, shadowOffsetY
)
概念
shadowColor
→ 阴影颜色。shadowBlur
→ 模糊半径。shadowOffsetX
/shadowOffsetY
→ 阴影偏移。
示例
ini
ctx.shadowColor = "rgba(0,0,0,0.5)"; // 半透明黑色
ctx.shadowBlur = 10; // 模糊半径
ctx.shadowOffsetX = 10; // X 偏移
ctx.shadowOffsetY = 5; // Y 偏移
ctx.fillStyle = "orange";
ctx.fillRect(50, 50, 120, 80);
3. 渐变 (createLinearGradient
, createRadialGradient
)
概念
createLinearGradient(x1,y1,x2,y2)
→ 线性渐变。createRadialGradient(x1,y1,r1,x2,y2,r2)
→ 径向渐变。addColorStop(offset, color)
→ 添加渐变色,offset ∈ [0,1]
。
示例
ini
// 线性渐变
let grad1 = ctx.createLinearGradient(0, 0, 200, 0);
grad1.addColorStop(0, "red");
grad1.addColorStop(1, "blue");
ctx.fillStyle = grad1;
ctx.fillRect(10, 10, 200, 80);
// 径向渐变
let grad2 = ctx.createRadialGradient(150, 150, 10, 150, 150, 80);
grad2.addColorStop(0, "yellow");
grad2.addColorStop(1, "green");
ctx.fillStyle = grad2;
ctx.fillRect(100, 100, 120, 120);
4. 图案 (createPattern
)
概念
createPattern(image, repeatType)
→ 用图像创建平铺模式。repeatType
:repeat | repeat-x | repeat-y | no-repeat
。
示例
ini
const img = document.getElementById("source");
img.onload = () => {
const pattern = ctx.createPattern(img, "repeat");
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, 300, 200);
};
5. 像素操作 (getImageData
, putImageData
)
概念
ctx.getImageData(x,y,w,h)
→ 获取像素数据对象。ctx.putImageData(imageData, dx, dy)
→ 放回像素数据。imageData.data
→Uint8ClampedArray
,每 4 个数代表一个像素(RGBA)。
示例:灰度化
ini
let imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
let data = imgData.data;
for (let i = 0; i < data.length; i += 4) {
let r = data[i], g = data[i+1], b = data[i+2];
let gray = 0.3*r + 0.59*g + 0.11*b;
data[i] = data[i+1] = data[i+2] = gray; // 设置为灰度
}
ctx.putImageData(imgData, 0, 0);
6. 合成操作 (globalCompositeOperation
)
概念
-
控制新绘制内容如何与已有内容混合。
-
常用模式:
"source-over"
(默认,上层覆盖下层)"destination-over"
(下层覆盖上层)"lighter"
(颜色相加)"multiply"
(颜色相乘,变暗)"xor"
(异或,交集透明)
示例
ini
// 绘制蓝色矩形
ctx.fillStyle = "blue";
ctx.fillRect(20, 20, 120, 120);
// 改变合成模式
ctx.globalCompositeOperation = "lighter"; // 颜色相加
// 绘制红色矩形
ctx.fillStyle = "red";
ctx.fillRect(80, 80, 120, 120);
🟥 7,WebGL 基础
(0)概念与使用场景
-
WebGL (Web Graphics Library)
- 是基于 OpenGL ES 2.0 的 JavaScript API,可以在 HTML5 Canvas 中直接调用 GPU 渲染 2D/3D 图形。
- 特点:跨平台、跨浏览器、无插件。
-
使用场景
- 3D 游戏(浏览器内运行,无需安装客户端)。
- 数据可视化(如三维图表、地理地图、医学成像)。
- 图形编辑器(类似 Photoshop 在线版)。
- AI/科学计算(利用 GPU 的并行计算能力)。
(1)WebGL 上下文
概念
- WebGL 运行在
<canvas>
上,必须通过getContext("webgl")
或getContext("webgl2")
获取绘图上下文。 - 这个上下文就是和 GPU 交互的接口。
示例:获取上下文
xml
<canvas id="glcanvas" width="400" height="400"></canvas>
<script>
// 获取 Canvas
const canvas = document.getElementById("glcanvas");
// 获取 WebGL 上下文
// 如果浏览器支持 WebGL2,优先用 webgl2
let gl = canvas.getContext("webgl2") || canvas.getContext("webgl");
// 检查是否成功
if (!gl) {
alert("你的浏览器不支持 WebGL");
} else {
console.log("WebGL 上下文获取成功:", gl);
}
</script>
(2)WebGL 基础绘图
初始化参数
arduino
const gl = canvas.getContext("webgl", {
alpha: true, // 是否支持透明度
depth: true, // 是否启用深度缓冲区 (3D 计算前后遮挡)
stencil: false, // 是否启用模板缓冲区
antialias: true, // 是否抗锯齿
premultipliedAlpha: true, // 是否使用预乘透明度
preserveDrawingBuffer: false // 是否保留上一次绘制内容
});
(2.1)常量
WebGL 使用很多常量:
-
清除缓冲区
gl.COLOR_BUFFER_BIT
→ 清除颜色缓冲区gl.DEPTH_BUFFER_BIT
→ 清除深度缓冲区gl.STENCIL_BUFFER_BIT
→ 清除模板缓冲区
-
绘制模式
gl.TRIANGLES
→ 三角形gl.LINES
→ 直线gl.POINTS
→ 点
(2.2)方法命名 (uniform 变量传递)
uniform3iv(location, Int32Array)
→ 传入 3个整型数组。uniform4f(location, x, y, z, w)
→ 传入 4个浮点数。uniform3i(location, x, y, z)
→ 传入 3个整型数。
总结:
i
= intf
= floatv
= vector (数组)
(2.3)绘制前准备
scss
// 设置清屏颜色 (r,g,b,a) = 黑色
gl.clearColor(0.0, 0.0, 0.0, 1.0);
// 清除颜色缓冲区
gl.clear(gl.COLOR_BUFFER_BIT);
解释:
clearColor
→ 设置画布底色。clear
→ 按照设置的缓冲区常量清空。
(2.4)视口与坐标
arduino
// 设置视口:左下角 (0,0),宽高为整个 Canvas
gl.viewport(0, 0, canvas.width, canvas.height);
- 视口 (viewport) :WebGL 绘制的区域。
- 坐标映射:WebGL 内部坐标系是 裁剪坐标 [-1,1] → 再映射到视口范围。
(2.5)缓冲区
arduino
// 创建一个缓冲区
const buffer = gl.createBuffer();
// 绑定缓冲区为当前操作对象 (ARRAY_BUFFER 专门存顶点数据)
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
// 传入顶点数据(Float32Array)
const vertices = new Float32Array([0.0, 0.5, -0.5, -0.5, 0.5, -0.5]);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
// 删除缓冲区
// gl.deleteBuffer(buffer);
说明:
createBuffer()
→ 创建显存缓冲区对象。bindBuffer(target, buffer)
→ 绑定目标(ARRAY_BUFFER / ELEMENT_ARRAY_BUFFER)。bufferData(target, data, usage)
→ 把数据传给 GPU(usage 一般是 STATIC_DRAW)。deleteBuffer(buffer)
→ 释放缓冲区。
(2.6)错误处理
go
const error = gl.getError();
if (error !== gl.NO_ERROR) {
console.error("WebGL 错误代码:", error);
}
常见错误常量:
gl.NO_ERROR
→ 没错误gl.INVALID_ENUM
→ 无效枚举gl.INVALID_VALUE
→ 无效值gl.INVALID_OPERATION
→ 当前状态下不能执行gl.OUT_OF_MEMORY
→ 内存不足
(3)着色器
(3.1)attribute vs uniform
attribute
→ 每个顶点不同的数据(位置、颜色、法线)。uniform
→ 全局统一数据(变换矩阵、光照参数)。
(3.2)最简单 GLSL 着色器
csharp
// 顶点着色器
attribute vec4 a_Position;
void main() {
gl_Position = a_Position; // 直接传递顶点位置
}
// 片元着色器
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // 红色
}
(3.3)创建着色器程序
ini
function createShader(gl, type, source) {
const shader = gl.createShader(type); // 创建着色器对象
gl.shaderSource(shader, source); // 传入 GLSL 源码
gl.compileShader(shader); // 编译
return shader;
}
const vs = createShader(gl, gl.VERTEX_SHADER, vertexSource);
const fs = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource);
const program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
gl.useProgram(program);
(3.4)给着色器传递数据
arduino
// 获取变量位置
const a_Position = gl.getAttribLocation(program, "a_Position");
// 指定如何解析缓冲区中的数据
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
// 启用变量
gl.enableVertexAttribArray(a_Position);
(3.5)调试着色器
less
if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(vs));
}
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error(gl.getProgramInfoLog(program));
}
(3.6)GLSL 100 → GLSL 300
- GLSL 100 (WebGL1) →
attribute / varying
- GLSL 300 (WebGL2) →
in / out
示例:
ini
// GLSL 100
attribute vec4 a_Position;
varying vec4 v_Color;
// GLSL 300
in vec4 a_Position;
out vec4 v_Color;
(3.7)绘制图形
scss
// 绘制三角形
gl.drawArrays(gl.TRIANGLES, 0, 3);
// 使用索引绘制
gl.drawElements(gl.TRIANGLES, 3, gl.UNSIGNED_SHORT, 0);
(3.8)纹理
ini
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0,
gl.RGBA, gl.UNSIGNED_BYTE, imageData);
(3.9)读取像素
arduino
const pixels = new Uint8Array(canvas.width * canvas.height * 4);
gl.readPixels(0, 0, canvas.width, canvas.height,
gl.RGBA, gl.UNSIGNED_BYTE, pixels);
console.log(pixels);
(4)WebGL1 vs WebGL2
-
WebGL1 基于 OpenGL ES 2.0。
-
WebGL2 基于 OpenGL ES 3.0,新特性:
- GLSL 300 es 支持 →
in/out
替代attribute/varying
。 - 支持多重采样渲染(MSAA)。
- 支持浮点纹理。
- 支持
VAO (Vertex Array Object)
。
- GLSL 300 es 支持 →
带纹理的小球弹跳动画(WebGL 版
js
<canvas id="glCanvas"></canvas>
<script>
const canvas = document.getElementById("glCanvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// 获取 WebGL 上下文
const gl = canvas.getContext("webgl");
if (!gl) {
alert("WebGL 不支持,请换浏览器~");
}
// ========================
// 1. 定义着色器(GLSL)
// ========================
// 顶点着色器:负责计算顶点位置,并传递纹理坐标
const vsSource = `
attribute vec2 a_position; // 顶点坐标
attribute vec2 a_texCoord; // 纹理坐标
varying vec2 v_texCoord; // 传递给片元着色器
uniform vec2 u_translation; // 平移(小球位置)
uniform float u_scale; // 缩放(小球半径)
void main() {
gl_Position = vec4((a_position * u_scale) + u_translation, 0.0, 1.0);
v_texCoord = a_texCoord;
}
`;
// 片元着色器:负责渲染像素(这里使用纹理)
const fsSource = `
precision mediump float;
varying vec2 v_texCoord;
uniform sampler2D u_sampler; // 纹理采样器
void main() {
gl_FragColor = texture2D(u_sampler, v_texCoord);
}
`;
// ========================
// 2. 创建着色器程序
// ========================
function createShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
function createProgram(gl, vsSource, fsSource) {
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vsSource);
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fsSource);
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error(gl.getProgramInfoLog(program));
return null;
}
return program;
}
const program = createProgram(gl, vsSource, fsSource);
gl.useProgram(program);
// ========================
// 3. 定义小球(用圆形近似)
// ========================
const numSegments = 100; // 圆的细分数(越大越圆)
const positions = [];
const texCoords = [];
// 圆心
positions.push(0, 0);
texCoords.push(0.5, 0.5);
// 圆边
for (let i = 0; i <= numSegments; i++) {
const angle = i * Math.PI * 2 / numSegments;
const x = Math.cos(angle);
const y = Math.sin(angle);
positions.push(x, y);
texCoords.push(x * 0.5 + 0.5, y * 0.5 + 0.5);
}
// 创建缓冲区并传数据
function createBuffer(data, attribName, size) {
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
const attribLoc = gl.getAttribLocation(program, attribName);
gl.enableVertexAttribArray(attribLoc);
gl.vertexAttribPointer(attribLoc, size, gl.FLOAT, false, 0, 0);
}
createBuffer(positions, "a_position", 2);
createBuffer(texCoords, "a_texCoord", 2);
// ========================
// 4. 加载纹理
// ========================
function loadTexture(url) {
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
// 先用 1x1 像素填充,避免图像未加载时报错
gl.texImage2D(
gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0,
gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([255, 0, 0, 255])
);
const image = new Image();
image.onload = () => {
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA,
gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.generateMipmap(gl.TEXTURE_2D);
};
image.src = url;
return texture;
}
const texture = loadTexture("https://picsum.photos/200"); // 随机纹理
// ========================
// 5. 小球物理参数
// ========================
let ball = {
x: 0, y: 0, // 小球位置(范围 -1 ~ 1)
dx: 0.01, dy: 0.02, // 速度
radius: 0.2, // 半径(控制缩放)
gravity: -0.0008, // 重力
friction: 0.8 // 碰撞摩擦
};
// 获取 uniform 位置
const u_translation = gl.getUniformLocation(program, "u_translation");
const u_scale = gl.getUniformLocation(program, "u_scale");
const u_sampler = gl.getUniformLocation(program, "u_sampler");
// ========================
// 6. 动画循环
// ========================
function animate() {
gl.clearColor(0, 0, 0, 1); // 背景黑色
gl.clear(gl.COLOR_BUFFER_BIT);
// 更新小球位置
ball.dy += ball.gravity; // 受重力影响
ball.x += ball.dx;
ball.y += ball.dy;
// 边界检测(-1~1 范围)
if (ball.y - ball.radius < -1) {
ball.y = -1 + ball.radius;
ball.dy = -ball.dy * ball.friction;
}
if (ball.y + ball.radius > 1) {
ball.y = 1 - ball.radius;
ball.dy = -ball.dy * ball.friction;
}
if (ball.x - ball.radius < -1 || ball.x + ball.radius > 1) {
ball.dx = -ball.dx * ball.friction;
}
// 传 uniform(平移 + 缩放)
gl.uniform2f(u_translation, ball.x, ball.y);
gl.uniform1f(u_scale, ball.radius);
gl.uniform1i(u_sampler, 0);
// 绑定纹理并绘制
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.drawArrays(gl.TRIANGLE_FAN, 0, positions.length / 2);
requestAnimationFrame(animate);
}
animate();
</script>