Skip to content

51. Canvas 画各种图案作为纹理

Published:

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

2025-04-08 21.51.07.gif

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

2025-04-08 01.25.01.gif

很多时候,找不到合适的图片,或者内容是动态的,都可以用 canvas 来画。

这节我们就来练习下 Canvas 画各种图案作为纹理吧。

npx create-vite canvas-texture

image.png

进入项目,安装依赖:

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

image.png

image.png

然后我们用 Canvas 画一下各种图案:

播放按钮

首先我们用 canvas 画一个播放按钮:

image.png

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

image.png

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 作为纹理才不会模糊。

当然,这里最好不要写死,而是动态的来算:

image.png

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;
}

image.png

可以看到,圆画出来了,但是颜色怪怪的,明显不是橙色。

我们要设置下颜色空间为 sRGBColorSpace,颜色贴图都要设置这个。

image.png

texture.colorSpace = THREE.SRGBColorSpace;

image.png

这样颜色就正常了。

然后来画中间的三角形:

image.png

半径 40,可以大概算一下三个点的位置。

canvas 的坐标系向下是 y 轴正方向,向右是 x 轴正方向。

image.png

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。

image.png

这样,播放按钮就完成了。

五角星

接下来我们来画一个五角星的图案:

image.png

这个比较简单,就是取 5 个点的坐标,连起来就好:

我们还是用 translate 把坐标原点移到中央再算坐标:

image.png

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

image.png

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;	
}

看下效果:

image.png

图片 + 文字

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

heart.png

绘制图片需要用 drawImage 的 api,文字用 fillText

但文字需要设置很多样式

我们先写一下:

image.png

把心形图片放到 public 目录下。

然后代码里加载:

image.png

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。

image.png

在 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;
}

image.png

这样,图片和文字就绘制好了。

但你能明显感觉到背景不是透明的,下面挡住了 x 轴:

image.png

设置 transparent 为 true 就好了:

image.png

image.png

案例代码上传了小册仓库

总结

这节我们用 canvas 绘制了一些图案作为纹理。

canvas 的画布大小一般设置为平面宽高 * dpr,这样绘制出来的不模糊。

一般都是 translate 坐标原点到画布中央之后再绘制,这样是正好在画布中央,坐标也比较好计算。

可以用 moveTo、lineTo 画直线,用 arc 画圆弧曲线,drawImage 画图片、fillText 写文字等。

创建 CanvasTexture,传入 canvas 作为参数,然后设置为材质的颜色贴图 map,这样就可以用 canvas 做纹理了。

还设置 transparent 为 true,这样就是背景透明的效果。

很多时候找不到合适的图片或者纹理需要定制,都可以用 canvas 来绘制。

评论