瓜 要说什么事物最能代表夏天,第一个想到的就是西瓜了,不过现在的西瓜没有回忆中小时候那么甜了。 今天我们就来画西瓜,一片切开的西瓜~
笔 前段时间在 github 上看到了一个很有意思的 Javascript 伪 3D 引擎,叫做 Zdog ,可以绘制扁平风格的 3D 内容。上手简单,虽然有一些缺陷(后面会讲),但是用来做一些小玩具还是很有意思的。
官网有很多优秀的例子,可以参考学习。
先来介绍一下几个我们画西瓜会用到的 API:
Illustration Illustration
是处理 <canvas>
或者 <svg>
元素的顶级类,保存场景中的所有形状,并在元素中显示这些形状。通过设置元素匹配的选择器来设置目标渲染元素。
1 2 3 4 5 6 7 8 9 const illo = new Illustration({ element: '#zdogStage' , dragRotate: true , zoom: 20 , rotate: { x : -TAU / 16 }, onDragStart: function ( ) { ... }, });
Shape Shape
用于绘制自定义形状,通过多个节点连线形成形状,可以设置线条粗细,是否填充来完成各种形状。
平面线条:
1 2 3 4 5 6 7 8 9 10 11 12 new Shape({ addTo: illo, path: [ { x : -32 , y : -40 }, { x : 32 , y : -40 }, { x : -32 , y : 40 }, { x : 32 , y : 40 }, ], closed: false , stroke: 20 , color: '#636' , });
3D 线条:
1 2 3 4 5 6 7 8 9 10 11 12 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 一个空的形状,用来组合图形,方便定位。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 const eyeGroup = new Group({ addTo: illo, translate: { z : 20 }, }); new Ellipse({ addTo: eyeGroup, width: 160 , height: 80 , }); let iris = new Ellipse({ addTo: eyeGroup, diameter: 70 , }); iris.copy({ diameter: 30 , color: '#636' , }); iris.copy({ diameter: 30 , translate: { x : 15 , y : -15 }, color: 'white' , });
咱们画西瓜大概就是用到以上这些 API 啦。
画 首先我们看看一片西瓜的高清大图:
从上图可以看出,一片西瓜可以拆分成 7 个部分来画,具体步骤如下:
新建西瓜 html 中插入一个 canvas 标签,id 为 zdogStage,作为渲染元素。接着初始化 Zdog:
1 2 3 4 5 6 7 import { Illustration, Group, easeInOut, TAU, Shape } from 'zdog' ;const illo = new Illustration({ element: '#zdogStage' , dragRotate: true , zoom: 20 , });
绿色瓜皮正面 绿色瓜皮正面由 4 个正常节点坐标、2 个弧线顶点坐标绘制而成,注意我们给每个坐标点设置了 Z
轴为 2
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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, });
绿色瓜皮背面 背面的瓜皮与正面瓜皮形状一致,只是在 Z
轴的位置不同。 我们可以使用 Zdog 提供的 API —— copy
来快速完成,在复制的同时设置旋转属性,旋转后实际的 Z
轴位置变成了 -2
。 因为被复制的对象是添加到 illo
,在不修改的情况下,复制出的形状也是添加到 illo
。
1 2 3 const skinBack = skinFront.copy({ rotate: { y : TAU / 2 }, })
正面角度无法看出复制后的效果,这边换一个角度展示:
青色瓜皮正面 & 背面 青色瓜皮正面与绿色瓜皮正面的绘制方式一致,只是各节点的坐标不同,参照上面的代码可以很快完成绘制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 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 个坐标点用线条连接并填充。
右侧的侧面也可以通过复制加旋转来实现。
侧面使用更深一些的绿色,可以让西瓜看起来更立体。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 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 }, })
顶部瓜皮 顶部的瓜皮是弧形的,使用两根弧线加两根直线来绘制一个弯曲的面,把顶部封住。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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 引擎在渲染弧形面的时候有缺陷,具体看下图:
可以看出,一定角度的俯视、仰视渲染是没有问题的,旋转到侧面会出现渲染异常,目前文档没有相关解决方案,可能是笔者的功夫不到家,嘻嘻。
在演示的时候固定一个不会穿帮的角度,可以获得比较好的视觉效果。
瓜瓤正面 & 瓜籽 & 背面 瓜瓤是个上边弧线的三角形,弧线的参数与青色瓜皮底部的弧线一致。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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, });
虽然现在市面上无籽西瓜很多,吃起来更方便,但是在视觉上,西瓜不能没有西瓜籽就像西方不能没有耶路撒冷,让我们给瓜瓤加上瓜籽。
我们先创建一个西瓜籽的分组,并把这个分组添加到瓜瓤正面
节点:
1 2 3 4 const seedGroup = new Group({ addTo: melonFront, translate: { z : 2.3 }, });
创建一个西瓜籽形状,使用线条绘制并填充,复制多个摆放在西瓜籽分组的不同位置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 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
来复制当前节点及其子节点。
1 2 3 melonFront.copyGraph({ rotate: { y : TAU / 2 }, })
瓜瓤侧面 瓜瓤侧面的绘制方法与瓜皮侧面基本一致,用一个稍微深一些的红色,增加瓜瓤的立体感。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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 还有某些缺陷,为了有一个良好的展示效果,我们需要遮遮丑,调整到一个不会露底的视角。
1 2 3 4 5 6 7 const illo = new Illustration({ element: '#zdogStage' , resize: true , dragRotate: true , zoom: 20 , rotate: { x : -TAU / 16 , y : -TAU / 8 }, });
这样就是俯视的角度啦,效果与上面 👆🏻 的图片类似。
动 我们可以让西瓜旋转起来,全方位展示立体效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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 西瓜演示
结 Zdog 是一个比较有趣的 3D 引擎,只要你能接受它目前存在的缺陷,可能在生产环境没有啥使用场景,个人拿来做些小玩具还是很有意思的。