Skip to content

课程 18 - 渐变和重复图案

在本节课中我们将介绍如何实现渐变和重复图案

使用 CanvasGradient 实现渐变

我们可以用 CanvasGradient API 创建各种渐变效果,随后以纹理的形式消费。下面我们将介绍命令式和声明式的两种实现。

命令式创建渐变纹理

以线性渐变为例,创建 <canvas> 并得到上下文后,使用 createLinearGradient 时需要传入起点和终点,两者定义了渐变的方向。随后添加多个色标,绘制到 <canvas> 上,后续作为创建纹理的来源:

ts
const gradient = ctx.createLinearGradient(0, 0, 1, 0); // x1, y1, x2, y2

gradient.addColorStop(0, 'red');
gradient.addColorStop(1, 'blue');

ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 256, 1);

通过 Device API 创建纹理对象,最后将它传给图形的 fill 属性完成绘制。

ts
// 0. 创建渐变数据
const ramp = generateColorRamp({
    colors: [
        '#FF4818',
        '#F7B74A',
        '#FFF598',
        '#91EABC',
        '#2EA9A1',
        '#206C7C',
    ].reverse(),
    positions: [0, 0.2, 0.4, 0.6, 0.8, 1.0],
});

// 1. 获取画布设备
const device = canvas.getDevice();

// 2. 创建纹理对象
const texture = device.createTexture({
    format: Format.U8_RGBA_NORM,
    width: ramp.width,
    height: ramp.height,
    usage: TextureUsage.SAMPLED,
});
texture.setImageData([ramp.data]); // 将之前创建的 <canvas> 数据传给纹理

// 3. 将纹理对象传给图形的 `fill` 属性
rect.fill = texture;

但我们希望支持声明式语法,提升易用性的同时也便于序列化。

声明式 CSS 渐变语法

参考 CSS 的渐变语法,我们可以使用 gradient-parser 解析得到结构化的结果,后续用来调用 createLinearGradient 等 API:

ts
rect.fill = 'linear-gradient(0deg, blue, green 40%, red)';
rect.fill = 'radial-gradient(circle at center, red, blue, green 100%)';

解析结果如下:

js
linearGradient = call(() => {
    const { parseGradient } = Core;
    return parseGradient('linear-gradient(0deg, blue, green 40%, red)');
});
js
radialGradient = call(() => {
    const { parseGradient } = Core;
    return parseGradient(
        'radial-gradient(circle at center, red, blue, green 100%)',
    );
});

常见的渐变类型包括:

使用 Mesh 实现渐变

以上基于 Canvas 和 SVG 实现的渐变表现力有限,无法展示复杂的效果。一些设计工具例如 Sketch / Figma 社区中有很多基于 Mesh 的实现,例如:

我们参考一些开源的实现,有的是在 Vertex Shader 中,我们选择后者:

Warping

导出渐变成 SVG

线性渐变

SVG 提供了 linearGradientradialGradient,但支持的属性和 CanvasGradient 很不一样。以前者为例:

ts
function computeLinearGradient(
    min: [number, number],
    width: number,
    height: number,
    angle: number,
) {
    const rad = DEG_TO_RAD * angle;
    const rx = 0;
    const ry = 0;
    const rcx = rx + width / 2;
    const rcy = ry + height / 2;
    // get the length of gradient line
    // @see https://observablehq.com/@danburzo/css-gradient-line
    const length =
        Math.abs(width * Math.cos(rad)) + Math.abs(height * Math.sin(rad));
    const x1 = min[0] + rcx - (Math.cos(rad) * length) / 2;
    const y1 = min[1] + rcy - (Math.sin(rad) * length) / 2;
    const x2 = min[0] + rcx + (Math.cos(rad) * length) / 2;
    const y2 = min[1] + rcy + (Math.sin(rad) * length) / 2;

    return { x1, y1, x2, y2 };
}

圆锥渐变

参考 SVG angular gradient 可以近似实现这种效果。而 CSS conic-gradient() polyfill 的思路是使用 Canvas 渲染后导出 dataURL,再用 <image> 引用。

多个渐变叠加

对于多个渐变叠加的情况,在 Canvas API 可以多次设置 fillStyle 叠加绘制。而在声明式的 SVG 中,可以使用多个 <feBlend> 实现。

渐变编辑面板

实现重复图案

https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/createPattern#参数

CanvasImageSource supports following data types:

js
circle.style.fill = {
    image: [],
    repetition: 'repeat',
};

Pattern in ECharts: https://echarts.apache.org/en/option.html#color

js
{
  image: imageDom, // supported as HTMLImageElement, HTMLCanvasElement, but not path string of SVG
  repeat: 'repeat' // whether to tile, can be 'repeat-x', 'repeat-y', 'no-repeat'
}

Released under the MIT License.