我正在参加「初夏创意投稿大赛」详情请看:初夏创意投稿大赛
瓜
要说什么事物最能代表夏天,第一个想到的就是西瓜了,不过现在的西瓜没有回忆中小时候那么甜了。
今天我们就来画西瓜,一片切开的西瓜~
笔
前段时间在 github 上看到了一个很有意思的 Javascript 伪 3D 引擎,叫做 Zdog,可以绘制扁平风格的 3D 内容。上手简单,虽然有一些缺陷(后面会讲),但是用来做一些小玩具还是很有意思的。
官网有很多优秀的例子,可以参考学习。
先来介绍一下几个我们画西瓜会用到的 API:
Illustration
Illustration
是处理 <canvas>
或者 <svg>
元素的顶级类,保存场景中的所有形状,并在元素中显示这些形状。通过设置元素匹配的选择器来设置目标渲染元素。
const illo = new Illustration({
element: '#zdogStage', // Canvas、Svg 元素的选择器
dragRotate: true, // 视角是否可拖动
zoom: 20, // 数值放大倍数,绘制时的数字将按倍数放大
rotate: { x: -TAU / 16 }, // 选装状态,可控制 x, y, z 三个维度
onDragStart: function () { // 拖拽开始事件回调
...
},
});
Shape
Shape
用于绘制自定义形状,通过多个节点连线形成形状,可以设置线条粗细,是否填充来完成各种形状。
平面线条:
new Shape({
addTo: illo,
path: [
{ x: -32, y: -40 }, // start at top left
{ x: 32, y: -40 }, // line to top right
{ x: -32, y: 40 }, // line to bottom left
{ x: 32, y: 40 }, // line to bottom right
],
closed: false,
stroke: 20,
color: '#636',
});
3D 线条:
new Shape({
addTo: illo,
path: [
{ x: -32, y: -40, z: 40 },
{ x: 32, y: -40 },
{ x: 32, y: 40, z: 40 },
{ x: 32, y: 40, z: -40 },
],
closed: false,
stroke: 20,
color: '#636',
});
Group
一个空的形状,用来组合图形,方便定位。
const eyeGroup = new Group({
addTo: illo,
translate: { z: 20 },
});
// eye white first
new Ellipse({
addTo: eyeGroup,
width: 160,
height: 80,
// ...
});
// then iris
let iris = new Ellipse({
addTo: eyeGroup,
diameter: 70,
// ...
});
// then pupil
iris.copy({
diameter: 30,
color: '#636',
});
// highlight last in front
iris.copy({
diameter: 30,
translate: { x: 15, y: -15 },
color: 'white',
});
咱们画西瓜大概就是用到以上这些 API 啦。
画
首先我们看看一片西瓜的高清大图:
从上图可以看出,一片西瓜可以拆分成 7 个部分来画,具体步骤如下:
新建西瓜
html 中插入一个 canvas 标签,id 为 zdogStage,作为渲染元素。接着初始化 Zdog:
import { Illustration, Group, easeInOut, TAU, Shape } from 'zdog';
const illo = new Illustration({
element: '#zdogStage', // 渲染元素选择器
dragRotate: true, // 允许拖拽改变视角
zoom: 20, // 数值放大倍数
});
绿色瓜皮正面
绿色瓜皮正面由 4 个正常节点坐标、2 个弧线顶点坐标绘制而成,注意我们给每个坐标点设置了 Z
轴为 2
。
const skinFront = new Shape({
addTo: illo,
path: [
{ x: -7.1, y: -6, z: 2 }, // 左下角
{ x: -8, y: -8, z: 2 }, // 左上角
{
arc: [ // 连接 左上角与右上角的弧线
{ x: 0, y: -12, z: 2 }, // 弧线顶点
{ x: 8, y: -8, z: 2 }, // 右上角
]
},
{ x: 7.1, y: -6, z: 2 }, // 右下角
{
arc: [ // 连接 右下角与左下角的弧线
{ x: 0, y: -9.5, z: 2 }, // 弧线顶点
{ x: -7.1, y: -6, z: 2 }, // 左下角
]
},
],
closed: false, // 收尾节点是否自动相连
color: '#7AB13E', // 线条
fill: true, // 是否填充线条构成的图形
stroke: 1 / illo.zoom, // 线条最终粗细为 1
});
绿色瓜皮背面
背面的瓜皮与正面瓜皮形状一致,只是在 Z
轴的位置不同。
我们可以使用 Zdog 提供的 API —— copy
来快速完成,在复制的同时设置旋转属性,旋转后实际的 Z
轴位置变成了 -2
。
因为被复制的对象是添加到 illo
,在不修改的情况下,复制出的形状也是添加到 illo
。
const skinBack = skinFront.copy({
rotate: { y: TAU / 2 }, // 围绕 Y 轴旋转半圈,TAU 为 Zdog 提供的角度单位,其值可以理解为 360°
})
正面角度无法看出复制后的效果,这边换一个角度展示:
青色瓜皮正面 & 背面
青色瓜皮正面与绿色瓜皮正面的绘制方式一致,只是各节点的坐标不同,参照上面的代码可以很快完成绘制。
// 青色瓜皮正面
const insideSkinFront = new Shape({
addTo: illo,
path: [
{ x: 6.88, y: -5.5, z: 2 },
{ x: 7.1, y: -6, z: 2 },
{
arc: [
{ x: 0, y: -9.5, z: 2 },
{ x: -7.1, y: -6, z: 2 },
]
},
{ x: -6.88, y: -5.5, z: 2 },
{
arc: [
{ x: 0, y: -8.8, z: 2 },
{ x: 6.88, y: -5.5, z: 2 }
]
},
],
closed: false,
color: '#EEEB9A',
fill: true,
stroke: 1 / illo.zoom,
});
// 青色瓜皮背面
const insideSkinBack = insideSkinFront.copy({
rotate: { y: TAU / 2 },
})
绿色瓜皮侧面 & 青色瓜皮侧面
侧面的绘制方法也很简单,找到正面和背面的左侧坐标点,一共 4 个坐标点用线条连接并填充。
右侧的侧面也可以通过复制加旋转来实现。
侧面使用更深一些的绿色,可以让西瓜看起来更立体。
// 左边侧面瓜皮
const skinLeft = new Shape({
addTo: illo,
path: [
{ x: -7.1, y: -6, z: 2 }, // 正面左下角
{ x: -8, y: -8, z: 2 }, // 正面左上角
{ x: -8, y: -8, z: -2 }, // 背面左上角
{ x: -7.1, y: -6, z: -2 }, // 背面左下角
],
closed: false,
color: '#639033',
fill: true,
stroke: 1 / illo.zoom,
});
// 右边侧面瓜皮
const skinRight = skinLeft.copy({
rotate: { y: TAU / 2 },
})
// 青色瓜皮左边侧面
const insideSkinLeft = new Shape({
addTo: illo,
path: [
{ x: -6.88, y: -5.5, z: 2 },
{ x: -7.1, y: -6, z: 2 },
{ x: -7.1, y: -6, z: -2 },
{ x: -6.88, y: -5.5, z: -2 },
],
closed: false,
color: '#EEEB9A',
fill: true,
stroke: 1 / illo.zoom,
});
// 青色瓜皮右边侧面
const insideSkinRight = insideSkinLeft.copy({
rotate: { y: TAU / 2 },
})
顶部瓜皮
顶部的瓜皮是弧形的,使用两根弧线加两根直线来绘制一个弯曲的面,把顶部封住。
const topSkin = new Shape({
addTo: illo,
path: [
{ x: -8, y: -8, z: -2 }, // 绿色瓜皮背面左上角
{
arc: [ // 连接背面左上角与背面右上角的弧线
{ x: 0, y: -12, z: -2 }, // 弧线顶点
{ x: 8, y: -8, z: -2 }, // 绿色瓜皮背面右上角
]
},
{ x: 8, y: -8, z: 2 }, // 正面瓜皮右上角
{
arc: [ // 连接正面右上角与正面左上角的弧线
{ x: 0, y: -12, z: 2 }, // 弧线顶点
{ x: -8, y: -8, z: 2 },
]
},
],
closed: true,
color: '#59812d',
fill: true,
stroke: 1 / illo.zoom,
});
其实在做完以后,才发现这个方案存在一些问题,zdog 引擎在渲染弧形面的时候有缺陷,具体看下图:
可以看出,一定角度的俯视、仰视渲染是没有问题的,旋转到侧面会出现渲染异常,目前文档没有相关解决方案,可能是笔者的功夫不到家,嘻嘻。
在演示的时候固定一个不会穿帮的角度,可以获得比较好的视觉效果。
瓜瓤正面 & 瓜籽 & 背面
瓜瓤是个上边弧线的三角形,弧线的参数与青色瓜皮底部的弧线一致。
const melonFront = new Shape({
addTo: illo,
path: [
{ x: -6.88, y: -5.5, z: 2 }, // 瓜瓤左顶点
{
arc: [ // 连接瓜瓤左顶点与右顶点的弧线
{ x: 0, y: -8.8, z: 2 },
{ x: 6.88, y: -5.5, z: 2 }
]
},
{ x: 0, y: 10, z: 2 },
],
closed: false,
color: '#FF4846',
fill: true,
stroke: 1 / illo.zoom,
});
虽然现在市面上无籽西瓜很多,吃起来更方便,但是在视觉上,西瓜不能没有西瓜籽就像西方不能没有耶路撒冷,让我们给瓜瓤加上瓜籽。
我们先创建一个西瓜籽的分组,并把这个分组添加到瓜瓤正面
节点:
const seedGroup = new Group({
addTo: melonFront,
translate: { z: 2.3 }, // 模型之间保持距离可以避免过早触发 Z 格斗(渲染层级重叠计算)
});
创建一个西瓜籽形状,使用线条绘制并填充,复制多个摆放在西瓜籽分组的不同位置:
const seed = new Shape({
addTo: seedGroup,
path: [
{ x: 0, y: 0 },
{
bezier: [ // 贝塞尔曲线
{ x: -0.5, y: 0 },
{ x: -0.5, y: 0.75 },
{ x: 0, y: 1.5 },
]
},
{ x: 0, y: 1.5 },
{
bezier: [ // 贝塞尔曲线
{ x: 0.5, y: 0.75 },
{ x: 0.5, y: 0 },
{ x: 0, y: 0 },
]
},
],
stroke: 0.2, // 稍微粗一点的线条可以做出圆角效果
fill: true,
color: '#20201E',
});
// 复制多个西瓜籽
seed.copy({
translate: { y: 2.3, x: -1.2 },
});
seed.copy({
translate: { y: -3, x: -2.2 },
});
seed.copy({
translate: { y: -2.3, x: 3.2 },
});
seed.copy({
translate: { y: -4.3, x: 1.2 },
});
接下来复制瓜瓤正面并旋转,作为瓜瓤背面,这里有个点需要注意,copy
只能复制当前节点,需要使用 copyGraph
来复制当前节点及其子节点。
melonFront.copyGraph({
rotate: { y: TAU / 2 },
})
瓜瓤侧面
瓜瓤侧面的绘制方法与瓜皮侧面基本一致,用一个稍微深一些的红色,增加瓜瓤的立体感。
const melonLeft = new Shape({
addTo: illo,
path: [
{ x: -6.88, y: -5.5, z: 2 },
{ x: 0, y: 10, z: 2 },
{ x: 0, y: 10, z: -2 },
{ x: -6.88, y: -5.5, z: -2 },
],
closed: false,
color: '#CC4846',
fill: true,
stroke: 1 / illo.zoom,
});
// 红色瓜瓤右侧面
const melonRight = melonLeft.copy({
rotate: { y: TAU / 2 },
})
遮
上面提到 Zdog 还有某些缺陷,为了有一个良好的展示效果,我们需要遮遮丑,调整到一个不会露底的视角。
const illo = new Illustration({
element: '#zdogStage',
resize: true,
dragRotate: true,
zoom: 20,
rotate: { x: -TAU / 16, y: -TAU / 8 },
});
这样就是俯视的角度啦,效果与上面 👆🏻 的图片类似。
动
我们可以让西瓜旋转起来,全方位展示立体效果:
// 自动旋转动画
let ticker = 0;
const cycleCount = 240;
const sceneStartRotation = { y: -TAU / 8 };
function animate() {
if (isSpinning) { // 判断是否自动旋转
const progress = ticker / cycleCount;
const theta = easeInOut(progress % 1) * TAU; // 根据上述参数进行缓动计算
illo.rotate.y = -theta + sceneStartRotation.y;
ticker++;
}
illo.updateRenderGraph(); // 更新视图
requestAnimationFrame(animate); // 每帧递归调用
}
animate();
秀
西瓜的演示代码放在了“码上掘金”平台,大家可以体验一下。
结
Zdog 是一个比较有趣的 3D 引擎,只要你能接受它目前存在的缺陷,可能在生产环境没有啥使用场景,个人拿来做些小玩具还是很有意思的。