可视化
Canvas
基本用法
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// 绘图基本操作
ctx.fillStyle = 'red';
ctx.fillRect(10, 10, 100, 100); // 实心矩形
ctx.strokeRect(120, 10, 100, 100); // 空心矩形
ctx.clearRect(0, 0, canvas.width, canvas.height); // 清除
// 路径
ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(150, 50);
ctx.lineTo(100, 150);
ctx.closePath();
ctx.fillStyle = 'blue';
ctx.fill();
ctx.strokeStyle = 'black';
ctx.lineWidth = 2;
ctx.stroke();
// 弧和圆形
ctx.beginPath();
ctx.arc(100, 100, 50, 0, Math.PI * 2);
ctx.fill();
// 文字
ctx.font = '24px sans-serif';
ctx.fillText('Hello Canvas', 10, 50);
ctx.textAlign = 'center';
像素操作(ImageData)
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data; // Uint8ClampedArray [R,G,B,A, R,G,B,A, ...]
// 灰度处理
for (let i = 0; i < data.length; i += 4) {
const gray = data[i] * 0.299 + data[i+1] * 0.587 + data[i+2] * 0.114;
data[i] = data[i+1] = data[i+2] = gray;
}
ctx.putImageData(imageData, 0, 0);
动画循环
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 更新状态
x += vx;
y += vy;
// 绘制
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
requestAnimationFrame(animate);
}
animate();
Canvas 性能优化
| 优化手段 |
说明 |
| 离屏 Canvas |
将静态元素提前绘制到离屏 Canvas 上,主 Canvas 直接 drawImage() 复用 |
| 分层 Canvas |
静态层 + 动态层叠加,静态层只需绘制一次,避免反复重绘 |
| 脏矩形 |
只重绘变化的区域,而不是全屏 clearRect |
| 避免浮点坐标 |
浮点坐标触发抗锯齿(sub-pixel rendering),Math.floor(x) 或 `x |
// 离屏 Canvas
const offscreen = document.createElement('canvas');
const offCtx = offscreen.getContext('2d');
// 在离屏 Canvas 上预先绘制复杂静态图形
offCtx.drawImage(background, 0, 0);
// 主 Canvas 只负责组合
ctx.drawImage(offscreen, 0, 0); // 离屏内容
ctx.fillStyle = 'red';
ctx.fillRect(100, 100, 50, 50); // 动态内容
// willReadFrequently 属性(Canvas 2D)
const ctx = canvas.getContext('2d', { willReadFrequently: true });
// 当需要频繁 getImageData / putImageData 时,开启此选项让 Canvas 使用 CPU 后备存储
// 避免 GPU → CPU 来回传输的性能损失
Canvas vs SVG
| 对比维度 |
Canvas |
SVG |
| 渲染方式 |
像素(位图) |
矢量(几何图形) |
| 缩放 |
模糊(非矢量) |
清晰(矢量) |
| 性能(大量元素) |
好(几千上万个元素仍流畅) |
差(DOM 节点多时卡顿) |
| 交互 |
需手动计算命中检测(坐标判断) |
原生 DOM 事件绑定 |
| 动画 |
需手动 requestAnimationFrame |
CSS/SMIL 动画 |
| 内存 |
低(无 DOM 节点) |
高(每个图形是一个 DOM 节点) |
| 适用场景 |
游戏、像素处理、大数据图表 |
图标、Logo、插画、小数据量交互图表 |
SVG
viewBox 坐标系统
<!-- viewBox="minX minY width height" -->
<svg width="200" height="200" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="40" fill="blue" />
</svg>
viewBox 定义了 SVG 内部的逻辑坐标系
- 内部的
cx="50" 对应外部宽度 200px 的 100px(等比缩放)
- 不设置 viewBox 时 width/height 直接映射到坐标值
基础形状和路径
<svg viewBox="0 0 200 200">
<!-- 基础形状 -->
<rect x="10" y="10" width="80" height="80" rx="5" fill="red" />
<circle cx="150" cy="50" r="40" fill="green" />
<ellipse cx="100" cy="150" rx="60" ry="30" fill="blue" />
<!-- 路径 path -->
<path d="M 10 10 L 50 10 L 50 50 Z" fill="orange" />
<!-- M 移动到 / L 直线 / C 三次贝塞尔 / Q 二次贝塞尔 / A 弧线 / Z 闭合 -->
<path d="M 100 100 C 150 20, 200 180, 250 100" stroke="black" fill="none" />
</svg>
| Path 命令 |
含义 |
示例 |
M x y |
Move To(移动到) |
M 10 10 |
L x y |
Line To(画直线到) |
L 50 50 |
C x1 y1, x2 y2, x y |
三次贝塞尔曲线 |
C 10 10, 40 40, 50 10 |
Q x1 y1, x y |
二次贝塞尔曲线 |
Q 20 10, 50 50 |
A rx ry x-axis-rotation large-arc sweep x y |
弧线 |
A 30 30 0 0 1 50 50 |
Z |
闭合路径 |
Z |
渐变与滤镜
<svg viewBox="0 0 200 200">
<defs>
<!-- 线性渐变 -->
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="red" />
<stop offset="100%" stop-color="blue" />
</linearGradient>
<!-- 滤镜:模糊 -->
<filter id="blur">
<feGaussianBlur stdDeviation="3" />
</filter>
</defs>
<rect x="10" y="10" width="100" height="100" fill="url(#grad1)" />
<rect x="80" y="60" width="100" height="100" fill="blue" filter="url(#blur)" />
</svg>
ECharts
核心配置 option
const chart = echarts.init(document.getElementById('container'));
const option = {
// 标题
title: { text: '标题', subtext: '副标题' },
// 提示框
tooltip: { trigger: 'axis', formatter: '{b}: {c}' },
// 图例
legend: { data: ['销量'] },
// X 轴
xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed'] },
// Y 轴
yAxis: { type: 'value' },
// 数据缩放
dataZoom: [
{ type: 'inside', start: 0, end: 100 }, // 内置鼠标滚轮缩放
{ type: 'slider', start: 0, end: 100 }, // 滑动条
],
// 视觉映射
visualMap: {
min: 0, max: 100,
inRange: { color: ['#50a3ba', '#eac736', '#d94e5d'] },
calculable: true,
},
// 系列
series: [{
name: '销量',
type: 'line', // line/bar/pie/scatter/map/heatmap/gauge
data: [120, 200, 150],
smooth: true, // 平滑曲线
areaStyle: {}, // 面积图
}]
};
chart.setOption(option);
// 响应式
window.addEventListener('resize', () => chart.resize());
常用图表对比
| 图表类型 |
type 值 |
适用场景 |
核心配置 |
| 折线图 |
line |
趋势、时间序列 |
xAxis.type: 'time', smooth, areaStyle |
| 柱状图 |
bar |
分类对比 |
barWidth, stack(堆叠) |
| 饼图 |
pie |
占比分布 |
roseType: 'radius'(南丁格尔玫瑰图) |
| 散点图 |
scatter |
双变量相关性 |
symbolSize 映射第三维度 |
| 热力图 |
heatmap |
二维分布密度 |
visualMap 颜色映射 |
| 地图 |
map |
地理区域分布 |
registerMap 注册地图 JSON |
| 仪表盘 |
gauge |
进度/状态指标 |
min/max, detail.formatter |
大数据量优化
| 配置 |
说明 |
dataZoom |
缩放窗口,每次只渲染可视范围内的数据,支持滑动手势 |
sampling: 'lttb' |
Largest-Triangle-Three-Bucket 采样算法,保留趋势特征 |
progressive: 500 |
分批渲染,每帧渲染 500 个点,避免一次性渲染卡死 UI |
large: true |
开启散点图大数据模式(关闭 hover 动画以换性能) |
// 大数据量折线图优化
{
series: [{
type: 'line',
sampling: 'lttb', // 降采样,保留趋势
data: hugeDataArray, // 十万级数据点
showSymbol: false, // 不显示数据点
lineStyle: { width: 1 },
}],
dataZoom: [{ type: 'inside' }],
progressive: 500, // 分批渲染
}
自定义图表
// series.renderItem — 用 Canvas 绘制自定义图形
{
series: [{
type: 'custom',
renderItem: (params, api) => {
const x = api.value(0);
const y = api.value(1);
return {
type: 'group',
children: [{
type: 'rect',
shape: {
x: api.coord([x - 0.5, y])[0],
y: api.coord([x, y])[1],
width: 20,
height: api.size([1, y])[1],
},
style: { fill: 'red' },
}]
};
},
data: [[1, 10], [2, 20], [3, 15]],
}]
}
// graphic — 直接在 canvas 上画附加元素
chart.setOption({
graphic: {
type: 'text',
left: 'center',
top: 'center',
style: { text: 'Hello', fill: '#333' },
}
});
Three.js
三大要素
import * as THREE from 'three';
// 1. Scene(场景)
const scene = new THREE.Scene();
// 2. Camera(相机)
// 透视相机(fov, aspect, near, far)
const camera = new THREE.PerspectiveCamera(
75, // 视角(FOV)
window.innerWidth / window.innerHeight, // 宽高比
0.1, // 近裁剪面
1000 // 远裁剪面
);
camera.position.z = 5;
// 正交相机(用于 2D 或等距视角)
// const camera = new THREE.OrthographicCamera(-10, 10, 10, -10, 0.1, 1000);
// 3. Renderer(渲染器)
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 动画循环
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();
| 相机类型 |
用途 |
特点 |
| PerspectiveCamera |
3D 场景 |
近大远小,fov/far/near 决定视锥体 |
| OrthographicCamera |
2D / 等距视图 |
无透视变形,大小与距离无关 |
几何体 + 材质 + 网格(Mesh)
// 几何体
const geometry = new THREE.BoxGeometry(1, 1, 1); // 盒子
// new THREE.SphereGeometry(1, 32, 32); // 球体
// new THREE.CylinderGeometry(1, 1, 2, 32); // 圆柱
// new THREE.PlaneGeometry(5, 5); // 平面
// 材质
const material = new THREE.MeshStandardMaterial({
color: 0x44aa88,
roughness: 0.4,
metalness: 0.1,
});
// 网格 = 几何体 + 材质
const mesh = new THREE.Mesh(geometry, material);
mesh.position.x = 2;
mesh.rotation.y = Math.PI / 4;
scene.add(mesh);
光照模型
// 环境光 — 均匀照亮所有表面,无方向
const ambientLight = new THREE.AmbientLight(0x404060);
scene.add(ambientLight);
// 平行光 — 模拟太阳(来自某方向,产生阴影)
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 10, 7);
directionalLight.castShadow = true; // 产生阴影
scene.add(directionalLight);
// 点光 — 从一点向四周发射
const pointLight = new THREE.PointLight(0xff0040, 2, 50);
pointLight.position.set(10, 5, 10);
scene.add(pointLight);
// 聚光 — 锥形光
const spotLight = new THREE.SpotLight(0xffffff, 1);
spotLight.position.set(0, 10, 0);
spotLight.angle = Math.PI / 6; // 锥角
spotLight.penumbra = 0.2; // 边缘模糊
spotLight.decay = 1; // 衰减
scene.add(spotLight);
| 光照类型 |
特点 |
性能 |
| AmbientLight |
无方向,均匀照亮 |
无开销 |
| DirectionalLight |
平行光线,无限远 |
中(阴影开销大) |
| PointLight |
向四周发光,有衰减 |
中 |
| SpotLight |
锥形方向光,聚光效果 |
较高 |
纹理贴图
const textureLoader = new THREE.TextureLoader();
const material = new THREE.MeshStandardMaterial({
map: textureLoader.load('diffuse.jpg'), // 颜色贴图
roughnessMap: textureLoader.load('roughness.jpg'), // 粗糙度贴图
bumpMap: textureLoader.load('bump.jpg'), // 凹凸贴图
normalMap: textureLoader.load('normal.jpg'), // 法线贴图
envMap: textureLoader.load('environment.hdr'), // 环境贴图(反射)
});
// CubeTextureLoader 用于 HDR 环境贴图(天空盒)
const cubeTextureLoader = new THREE.CubeTextureLoader();
scene.background = cubeTextureLoader.load([
'px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg'
]);
交互(Raycaster)
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
// 点击检测
renderer.domElement.addEventListener('click', (event) => {
// 将鼠标坐标映射到 NDC(-1 到 1)
pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(pointer, camera);
// 检测与哪些物体相交
const intersects = raycaster.intersectObjects(scene.children);
if (intersects.length > 0) {
const clickedObject = intersects[0].object;
console.log('点击了:', clickedObject);
clickedObject.material.color.setHex(0xff0000); // 改变颜色
}
});
动画循环与性能
import Stats from 'three/examples/jsm/libs/stats.module.js';
const stats = new Stats();
document.body.appendChild(stats.dom);
function animate() {
requestAnimationFrame(animate);
stats.begin();
// 更新逻辑
mesh.rotation.y += 0.01;
renderer.render(scene, camera);
stats.end();
}
| 性能优化 |
说明 |
| stats.js |
FPS / 帧时间 / 内存实时监控 |
| LOD(Level of Detail) |
远处用小面数模型,近处用高精度模型 |
| 合批(Batching / Instancing) |
相同几何体用 InstancedMesh,合并 draw calls |
| MergeGeometry |
将多个静态几何体合并为一个,减少 draw calls |
| 减少材质切换 |
按材质排序渲染,避免频繁切换 shader |
| 限制 shadow map 大小 |
shadow.mapSize.width = 1024(默认 2048) |