演示:https://rotating-chicken.vercel.app/
代码:https://codesandbox.io/s/rotating-chicken-pcj9w1
前一段时间,我找到了zdog-这是一个轻巧的3D JavaScript引擎,用于帆布或SVG。如果您查看其网站,您会发现它被描述为 pseudo 3D 。这意味着什么?基本上,ZDOG拥有3D形状的型号,但将所有内容呈现为2D平面形状。
不是什么
您应该预先知道ZDOG有其局限性。没有碰撞检测或物理,没有梯度,没有透视或摄像头,没有照明以及您可能想要的其他一些东西。如果您需要的话,您可能想查看更强大和更实用的3D引擎,例如three-js。
这是什么
听起来很有限吗?但是,在这些约束中,您实际上可以使用ZDOG做一些非常了不起的事情,而无需三个JS固有的所有开销和复杂性。只需看看demos page即可 - 有很多整洁的例子,说明了哪个Zdog最好的:一种平坦的,圆形的美学,具有简单的形状。这使其非常适合设计师。
这也意味着使事物实际上看起来不错非常容易。不再想弄清楚您是否应该使用MeshLambertMaterial
或MeshPhongMaterial
,或者在何处将点灯或区域灯与场景相关。只需创建形状并给它颜色。
另外,圆形形状是实际的圆圈 - 这意味着您在真正的3D中都不会有多边形锯齿状。
使用方便
您会发现ZDOG IS 确实易于使用。它具有声明性的API,可让您描述所需的形状,并将它们添加到场景中。文档中的示例描述了这一点。
首先,您添加画布或SVG:
<canvas class="zdog-canvas" width="240" height="240"></canvas>
然后创建形状并将它们添加到场景中:
// create illustration
let illo = new Zdog.Illustration({
// set canvas with selector
element: '.zdog-canvas',
});
// add circle
new Zdog.Ellipse({
addTo: illo,
diameter: 80,
stroke: 20,
color: '#636',
});
// update & render
illo.updateRenderGraph();
鸡
所以让我们去鸡。我不会包含所有代码(您可以在沙箱中找到它),但我将包括主要零件。首先,我们需要弄清楚我们需要哪些单独的形状。在这种情况下,它看起来像这样:
知道这一点,我们可以开始添加形状:
export let model = new Zdog.Illustration({
element: ".zdog-canvas",
resize: true,
rotate: { x: rotation.initialX, y: rotation.initialY },
onResize: function (width, height) {
this.zoom = Math.floor(Math.min(width, height) / sceneSize);
},
});
let bodyGroup = new Zdog.Group({
addTo: model
});
// head
new Zdog.Hemisphere({
stroke: 6,
diameter: 15,
addTo: bodyGroup,
color: white,
rotate: { x: Zdog.TAU / 4 },
});
// neck
new Zdog.Cylinder({
stroke: 6,
diameter: 15,
length: 14,
addTo: bodyGroup,
color: white,
frontFace: white,
backface: white,
rotate: { x: Zdog.TAU / 4, z: Zdog.TAU },
translate: { y: 8 },
});
// beak
new Zdog.Cone({
addTo: model,
diameter: 4,
length: 5.5,
stroke: 2,
color: yellow,
backface: yellow,
rotate: { y: -Zdog.TAU / 4 },
translate: { x: 11, y: 4 },
});
// beard thing
new Zdog.Shape({
addTo: model,
stroke: 3.2,
path: [
{ x: 11, y: 9 },
{ x: 11, y: 11 },
],
color: red,
});
您可以看到,创建形状很容易。除了尺寸和颜色外,我们还可以设置翻译和旋转。我们可以将形状直接添加到主model
对象,或将它们添加到形状中,然后将其添加到model
。
接下来,我们可以添加眼睛:
// eyes group
let eyesGroup = new Zdog.Group({
addTo: model,
});
// left eye
let eye1 = new Zdog.Shape({
addTo: eyesGroup,
stroke: 1.5,
color: darkGrey,
translate: { x: 8, y: -1, z: 3 },
});
// right eye
eye1.copy({
translate: { x: 8, y: -1, z: -3 },
});
最后的波峰:
// path for each shape
const crestData = [
{
path: [
{ x: -4.5, y: -11 },
{ x: -5, y: -11.5 },
],
},
...
];
let crestGroup = new Zdog.Group({
addTo: model,
});
// create all crest shapes
crestData.forEach((item) => {
new Zdog.Shape({
addTo: crestGroup,
stroke: 4.5,
color: red,
...item,
});
});
此时,我们应该有一个体面的静态鸡肉!
动画事物
接下来,我们可以添加动画。在这个场景中,有两个动画:闪烁和跳跃。
根据文档,可以通过添加“渲染循环”函数来完成更改对场景的更改。此函数通过requestAnimationFrame
:
在每个帧上运行
function animate() {
// rotate model each frame
model.rotate.y += 0.03;
model.updateRenderGraph();
// animate next frame
requestAnimationFrame( animate );
}
// start animation
animate();
我们可以使用相同的原理进行眨眼。
但是,首先,我们如何视觉闪烁?一种方法是使眼睛不可见,又可以迅速看到。但是,一个更好的方法是将白色的形状在眼睛上方,我们向下移动(遮住眼睛),然后向后移动(露出眼睛)。这样,我们得到了眼睑运动,并且由于形状与鸡头的颜色相同,因此我们只有在遮住眼睛时才能看到它:
// eyelid
export let eyelid = new Zdog.Shape({
addTo: eyesGroup,
path: [
{ x: 8, y: -3, z: -3.4 },
{ x: 8, y: -3, z: 3.4 },
],
stroke: 2,
color: white,
});
为了使形状动画,并与ZDOG保持一致,我决定使用class ZAnimation
:
export class ZAnimation {
duration: number;
frame = 0;
force: number;
running = false;
onEnd?: () => void;
apply: (val: number) => void;
constructor(options: {
duration: number;
force: number;
interval?: number;
apply: (val: number) => void;
onEnd?: () => void;
}) {
this.duration = options.duration;
this.force = options.force;
this.apply = options.apply;
this.onEnd = options.onEnd;
if (options.interval) {
this.schedule(options.interval);
}
}
schedule(interval: number) {
setInterval(() => {
this.running = true;
}, interval);
}
start() {
this.running = true;
}
stop() {
this.running = false;
}
handleFrame() {
if (!this.running) return;
this.frame++;
this.apply(this.force);
if (this.frame === this.duration) {
this.frame = 0;
this.stop();
this.onEnd?.();
}
}
}
让我们看一下类的属性:
-
duration
:它应该持续多少帧; -
frame
:这跟踪动画的进度; -
force
:动画形状应在每个帧中变化的数量; -
apply
:将发生的实际转换(例如model.rotate.y += x
); -
running
:动画是否正在运行。当frame
等于duration
时,这将设置为false
,因为那是动画的结尾; -
interval
:如果我们希望动画定期发生(眨眼时有用); -
onEnd
:动画结束时运行的可选回调。链接动画时有用。
我们可以这样使用:
const blinkUp = new ZAnimation({
duration: 18,
force: 0.1,
apply: (val) => {
eyelid.translate.y -= val;
}
});
const blinkDown = new ZAnimation({
duration: 18,
force: 0.1,
interval: 4000,
apply: (val) => {
eyelid.translate.y += val;
},
onEnd: () => {
blinkUp.start();
}
});
function animate() {
model.updateRenderGraph();
blinkDown.handleFrame();
blinkUp.handleFrame();
requestAnimationFrame(animate);
}
animate();
我们创建动画,并指定blinkDown
应该每4秒发生一次,并且结束时应调用blinkUp
。这就是它的样子:
跳跃呢?为此,我们可以添加更多链式动画,并添加一个事件侦听器,以便我们的鸡肉可以单击:
const bounceDown = new ZAnimation({
duration: 28,
force: 1,
apply: (val) => {
model.translate.y += val;
}
});
const bounceUp = new ZAnimation({
duration: 28,
force: 1,
onEnd: () => bounceDown.start(),
apply: (val) => {
model.translate.y -= val;
}
});
document.addEventListener("click", () => {
bounceUp.start();
});
voilé,它跳跃。
轻松
很好!现在,我们有眨眼和跳鸡。很好,但是您可能已经注意到跳跃有点平淡。它们看起来太机械,因为运动是线性的。为了使其看起来很自然,我们需要添加一些轻松的东西。我建议您查看easings.net,这是我在这里使用的此主题的绝佳资源。除了显示不同宽松功能的外观外,它还具有CSS和数学功能的实现。我实际上在此演示中使用了这些精确的功能。
基本上,我们希望在每个帧中施加的力都会改变,这取决于当前帧相对于我们指定的总持续时间是什么:
在第一个图中,更改是线性的,这意味着它随着时间的推移(如我们现在的鸡肉)而以一致的方式发生。在右边,我们有一个“轻松”的动作,MeshPhongMaterial
最初会缓慢增加,然后在动画的中间更快地增加,然后在末尾放慢脚步。
我们可以在我们的ZAnimation
类中添加宽松:
class ZAnimation {
// ...
easing: Easing = "easeOutCirc";
easeAcc = 0;
constructor(options: {
// ...
easing: Easing;
}) {
// ...
this.easing = options.easing;
}
// ...
handleFrame() {
if(!this.running) return;
this.frame++;
const value = this.calcEasedValue();
this.apply(value);
if (this.frame === this.duration) {
this.frame = 0;
this.easeAcc = 0;
this.stop();
this.onEnd?.();
}
}
calcEasedValue() {
const progressDecimal = (this.frame * 100) / this.duration / 100;
let easedVal = 0;
switch (this.easing) {
case "easeOutCirc":
easedVal = easeOutCirc(progressDecimal);
break;
case "easeOutBounce":
easedVal = easeOutBounce(progressDecimal);
break;
}
const frameEase = easedVal - this.easeAcc;
this.easeAcc += frameEase;
return frameEase * this.force;
}
}
让我们解决这个问题:当我们创建动画时,我们可以指定所需的宽松函数(在这种情况下,只有两个选项easeOutCirc
和easeOutBounce
)。
然后,在handleFrame
函数中,我们不仅仅是使用force
属性,而是为每个特定帧计算宽松的力。为此,我们需要确定当前的进度,从0到1:
const progressDecimal = (this.frame * 100) / this.duration / 100;
并将该值传递给easeOutCirc
或easeOutBounce
函数,然后依次返回0到1之间的值。这些函数在哪里?好吧,我从easings.net复制了它们!他们看起来像这样:
function easeInOutCubic(x: number): number {
return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
}
请注意,我们从宽松功能中获得的值是累积的,这意味着它的输出将从0
继续增加,直到上次我们称之为1
,这不是我们想要的。我们希望每个帧的增量更改。为了做到这一点
const frameEase = easedVal - this.easeAcc;
this.easeAcc += frameEase;
return frameEase * this.force;
最后,我们将框架的宽松值乘以force
。这就是我们的形状在每个帧中移动的数量。
要使用此更新的类,我们只需要在实例中添加宽松类型:
const bounceDown = new ZAnimation({
duration: 32,
force: 6.8,
easing: "easeOutBounce",
addTo: scene,
apply: (val) => {
model.translate.y += val;
},
});
const bounceUp = new ZAnimation({
duration: 28,
force: 6.8,
easing: "easeOutCirc",
addTo: scene,
onEnd: () => bounceDown.start(),
apply: (val) => {
model.translate.y -= val;
},
});
跳跃看起来 lot 现在更好。
包起来
您可能已经注意到,在演示中,鸡也跟随鼠标。您可以查看为此所需的代码,尽管我不会在这篇文章中深入研究。谁知道,我们会回来第二部分。
如果您想知道什么时候需要伪3D鸡跳和眨眼,我将无法回答。
但是,如果您需要的是一些视觉才能,并且不需要更高级的功能,那么ZDOG可能是一个绝佳的选择。它非常易于使用,并提供了一组默认形状和工具,可帮助您快速启动并运行。结合一些自定义动画和用户互动,您可以创建一个令人愉快的结果。
一如既往,感谢您的阅读!