前面我们用 canvas 画过颜色柱状图的数字:

画过 Sprite 的纹理图片作为标注:

很多时候,找不到合适的图片,或者内容是动态的,都可以用 canvas 来画。
这节我们就来练习下 Canvas 画各种图案作为纹理吧。
npx create-vite canvas-texture

进入项目,安装依赖:
npm install
npm install --save three
npm install --save-dev @types/three
改下 src/main.js
import './style.css';
import * as THREE from 'three';
import {
OrbitControls
} from 'three/addons/controls/OrbitControls.js';
import mesh from './mesh.js';
const scene = new THREE.Scene();
scene.add(mesh);
const directionLight = new THREE.DirectionalLight(0xffffff, 2);
directionLight.position.set(500, 400, 300);
scene.add(directionLight);
const ambientLight = new THREE.AmbientLight();
scene.add(ambientLight);
const width = window.innerWidth;
const height = window.innerHeight;
const helper = new THREE.AxesHelper(500);
scene.add(helper);
const camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 10000);
camera.position.set(0, 0, 500);
camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height)
function render() {
renderer.render(scene, camera);
requestAnimationFrame(render);
}
render();
document.body.append(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
创建 Scene、Light、Camera、Renderer。
相机放在 0,0,500 的位置,正对 XY 平面。
改下 style.css
body {
margin: 0;
}
创建 mesh.js
import * as THREE from 'three';
const group = new THREE.Group();
function createPlane(x, y) {
const geometry = new THREE.PlaneGeometry(100, 100);
const material = new THREE.MeshPhongMaterial({
color: 'white'
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(x, y, 0);
return mesh;
}
group.add(createPlane(-300, 0));
group.add(createPlane(0, 0));
group.add(createPlane(300, 0));
export default group;
这里创建了 3 个平面。
跑一下:
npm run dev


然后我们用 Canvas 画一下各种图案:
播放按钮
首先我们用 canvas 画一个播放按钮:

首先,创建 CanvasTexture,传入 canvas 元素,作为材质的颜色贴图 map

function createCanvas() {
const canvas = document.createElement("canvas");
const w = canvas.width = 200;
const h = canvas.height = 200;
const c = canvas.getContext('2d');
c.translate(w / 2, h / 2);
c.arc(0, 0, 80, 0, Math.PI * 2);
c.fillStyle = "orange";
c.fill();
return canvas;
}
function createPlane(x, y) {
const texture = new THREE.CanvasTexture(createCanvas());
const geometry = new THREE.PlaneGeometry(100, 100);
const material = new THREE.MeshPhongMaterial({
// color: 'white'
map: texture
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(x, y, 0);
return mesh;
}
这里为啥平面宽高 100 * 100,而 canvas 要创建 200* 200 的大小呢?
与设备像素比 dpr 有关,如果你的 dpr 为 2 ,那就需要画两倍大小的 canvas 作为纹理才不会模糊。
当然,这里最好不要写死,而是动态的来算:

canvas.getContext(‘2d’) 是拿到绘图对象,之后就可以画了。
我们用 translate 把坐标原点移动到画布中央,然后画了一个 360 的圆,半径为 40 * dpr
设置 fillStyle 填充颜色后,调用 fill 方法就可以了。
function createCanvas() {
const dpr = window.devicePixelRatio;
const canvas = document.createElement("canvas");
const w = canvas.width = 100 * dpr;
const h = canvas.height = 100 * dpr;
const c = canvas.getContext('2d');
c.translate(w / 2, h / 2);
c.arc(0, 0, 40 * dpr, 0, Math.PI * 2);
c.fillStyle = "orange";
c.fill();
return canvas;
}

可以看到,圆画出来了,但是颜色怪怪的,明显不是橙色。
我们要设置下颜色空间为 sRGBColorSpace,颜色贴图都要设置这个。

texture.colorSpace = THREE.SRGBColorSpace;

这样颜色就正常了。
然后来画中间的三角形:

半径 40,可以大概算一下三个点的位置。
canvas 的坐标系向下是 y 轴正方向,向右是 x 轴正方向。

c.beginPath();
c.moveTo(-10 * dpr, -20 * dpr);
c.lineTo(-10 * dpr, 20 * dpr);
c.lineTo(20 * dpr, 0);
c.closePath();
c.fillStyle = "white";
c.fill();
创建一条路径,用线把三个点连起来,然后 fill。

这样,播放按钮就完成了。
五角星
接下来我们来画一个五角星的图案:

这个比较简单,就是取 5 个点的坐标,连起来就好:
我们还是用 translate 把坐标原点移到中央再算坐标:

然后用线把它们按照顺序连起来。

function createCanvas2() {
const dpr = window.devicePixelRatio;
const canvas = document.createElement("canvas");
const w = canvas.width = 100 * dpr;
const h = canvas.height = 100 * dpr;
const ctx = canvas.getContext('2d');
ctx.moveTo(30 * dpr,20 * dpr);
ctx.beginPath();
ctx.lineTo(50 * dpr,0);
ctx.lineTo(70 * dpr,20 * dpr);
ctx.lineTo(100 * dpr,30 * dpr);
ctx.lineTo(85 * dpr,60 * dpr);
ctx.lineTo(80 * dpr,90 * dpr);
ctx.lineTo(50 * dpr,80 * dpr);
ctx.lineTo(20 * dpr,90 * dpr);
ctx.lineTo(15 * dpr,60 * dpr);
ctx.lineTo(0,30 * dpr);
ctx.lineTo(30 * dpr,20 * dpr);
ctx.closePath();
ctx.fillStyle = "red";
ctx.fill();
return canvas;
}
看下效果:

图片 + 文字
如果我们想用这个心形图片作为背景,然后上面写几个文字呢?

绘制图片需要用 drawImage 的 api,文字用 fillText
但文字需要设置很多样式
我们先写一下:

把心形图片放到 public 目录下。
然后代码里加载:

const img = new Image();
img.src = './heart.png';
img.onload = function() {
group.add(createPlane(-300, 0, img));
group.add(createPlane(0, 0, img));
group.add(createPlane(300, 0, img));
}
加载完图片之后再去画 canvas。

在 translate 之前 drawImage,也就是图片刚好铺满整个 canvas。
之后 translate 原点到画布中央,之后 fillText
设置字体 font、绘制基线 textBaseLine、对齐 textAlign
function createCanvas4() {
const dpr = window.devicePixelRatio;
const canvas = document.createElement("canvas");
const w = canvas.width = 100 * dpr;
const h = canvas.height = 100 * dpr;
const c = canvas.getContext('2d');
c.drawImage(img, 0, 0, w, h);
c.translate(w / 2, h / 2);
c.fillStyle = "#ffffff";
c.font = "normal 24px 微软雅黑";
c.textBaseline = "middle";
c.textAlign = "center";
c.fillText('你好,guang', 0, 0);
return canvas;
}

这样,图片和文字就绘制好了。
但你能明显感觉到背景不是透明的,下面挡住了 x 轴:

设置 transparent 为 true 就好了:


案例代码上传了小册仓库。
总结
这节我们用 canvas 绘制了一些图案作为纹理。
canvas 的画布大小一般设置为平面宽高 * dpr,这样绘制出来的不模糊。
一般都是 translate 坐标原点到画布中央之后再绘制,这样是正好在画布中央,坐标也比较好计算。
可以用 moveTo、lineTo 画直线,用 arc 画圆弧曲线,drawImage 画图片、fillText 写文字等。
创建 CanvasTexture,传入 canvas 作为参数,然后设置为材质的颜色贴图 map,这样就可以用 canvas 做纹理了。
还设置 transparent 为 true,这样就是背景透明的效果。
很多时候找不到合适的图片或者纹理需要定制,都可以用 canvas 来绘制。