国际儿童节,尽管称之为国际,但实际上这个节日并非联合国定的,联合国定的儿童节叫“世界儿童节”,在11月20日庆祝。那六一是怎么回事呢?追溯到1925年,第一届世界儿童福利大会在日内瓦召开,当时就宣布了6月1日为国际儿童节,但是在当时,这个节日几乎没有国家过。一直到1949年国际民主妇女联合会代表大会召开,为纪念第二次世界大战特别是利迪策大屠杀中遇难的儿童,再一次将6月1日定为国际儿童节,许多社会主义国家响应,我们也是其中一员,所以今天我们过这样一个国际儿童节的节日,而并非欧美流行的世界儿童节。
说起儿童的象征,莫过于小时候看的大风车了,借此佳节用D3制作一个这样的动画聊以自娱。
因为代码简短,可以直接都写在一个html中:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>大风车</title>
</head>
<body style="text-align: center"></body>
<script type="module">
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
const _svg = d3
.select("body")
.append("svg")
.attr("width", "500")
.attr("height", "500");
...
</script>
</html>
起手先做一个500x500
的正方形svg,然后画出一个叶片(这一步需要耐心调出来):
_svg.append("path")
.attr("stroke", "black")
.attr("fill", "transparent")
.attr("d", "M10 100 L10 50 L50 50 C100 50, 180 100, 180 100 L10 100 C20 100, 20 50, 55 50");
然后将定位到中心,因为后续还有3个叶片,所以把它包含在一个g
组中,再将这个g
进行定位:
const g = _svg
.append("g")
.style("transform", "translate(250px, 150px)");
g.append("path")
.attr("stroke", "black")
.attr("fill", "transparent")
.attr("d", "M10 100 L10 50 L50 50 C100 50, 180 100, 180 100 L10 100 C20 100, 20 50, 55 50");
可以看到已经定位在中间了,然后更改一下path
的制作,改成循环四份这样的叶片并且将其分别设置0, 90, 180, 260
四个角度旋转:
[0, 90, 180, 270].map(e => {
g.append("path")
.attr("stroke", "black")
.attr("fill", "transparent")
.attr("transform", "rotate(" + e + " 10 100)")
.attr("d", "M10 100 L10 50 L50 50 C100 50, 180 100, 180 100 L10 100 C20 100, 20 50, 55 50");
});
是理想中的效果,为什么rotate()
的后面两个参数是10 100
呢?这个其实就是叶片的左下角,也就是path
画线的起始位置M10 100
。后面就是再往上加颜色,滤镜之类的东西了,现在再看svg
元素里面4个path
就有些重复了,所以不妨改成defs
+use xlink:href
引用的方式,也是一样的效果,页面元素还会少很多,将上面的代码改成这样,效果是一样的:
const defs = _svg.append("defs");
defs.append("path")
.attr("id", "block")
.attr("stroke", "black")
.attr("fill", "transparent")
.attr("d", "M10 100 L10 50 L50 50 C100 50, 180 100, 180 100 L10 100 C20 100, 20 50, 55 50");
[0, 90, 180, 270].map(e => {
g.append("use")
.attr("xlink:href", "#block")
.attr("transform", "rotate(" + e + " 10 100)")
});
接下来就可以加滤镜,上色看一看了,滤镜是加到defs
里,我决定加个模糊滤镜,毕竟风车转起来看上去有些模糊嘛。颜色则是把数字数组改成对象数组,然后放到数组里面,就定位红黄蓝绿几个基本色:
const defs = _svg.append("defs");
defs.append("filter")
.attr("id", "f1")
.append("feGaussianBlur")
.attr("in", "SourceGraphic")
.attr("stdDeviation", "1");
defs.append("path")
.attr("id", "block")
.attr("stroke", "black")
.attr("filter", "url(#f1)")
.attr("d", "M10 100 L10 50 L50 50 C100 50, 180 100, 180 100 L10 100 C20 100, 20 50, 55 50");
const _data = [
{
color: "red", deg: 0 },
{
color: "yellow", deg: 90 },
{
color: "blue", deg: 180 },
{
color: "green", deg: 270 },
];
g.selectAll("use")
.data(_data)
.join("use")
.attr("fill", e => e.color)
.attr("transform", e => "rotate(" + e.deg + " 10 100)");
SVG滤镜就是这样用的,可以一起放到defs
里面,定好id
,然后下面的path
用filter
属性指向这个id
,效果如下:
效果很完美,下一步就是做动画,转起来:
function comic() {
g.selectAll("use")
.data(_data)
.transition()
.duration(70)
.attr("transform", e => {
if (e.deg == 0) {
e.deg = 360;
}
e.deg -= 5;
return "rotate(" + e.deg + " 10 100)";
})
.on("end", comic);
}
comic();
这段代码很简单,每隔70毫秒逆时针转5度,不停的转,效果如下:
最后想起来应该加个棍,这个部分应该放在写入use
标签之前,也就是放在叶片的底层才行:
const line = g.append("line")
.attr("stroke", "darkgoldenrod")
.attr("stroke-width", 2)
.attr("x1", 10)
.attr("y1", 100)
.attr("x2", 0)
.attr("y2", 400);
尺寸超出画布了,要调一下g
的transform
为translate(250px, 100px)
,往上靠,然后棍子稍微倾斜一点好看一点:
下面再加一段动画,让这个风车整体在旋转的同时摇晃起来,毕竟风车怎么能不晃动嘛:
const ran = d3.randomInt(20);
function shake() {
g.transition()
.duration(300)
.style("transform", `translate(${250 + ran() * 2}px, ${100 + ran()}px)`);
line
.transition()
.duration(300)
.attr("x2", ran() * 3)
.attr("y2", 400 + ran())
.on("end", shake);
}
shake();
顺便再给刚才的第一段动画调快到50
,显得转的很快才抖动起来
完整源代码
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
const _svg = d3
.select("body")
.append("svg")
.attr("width", "500")
.attr("height", "500");
const g = _svg.append("g").style("transform", "translate(250px, 100px)");
const defs = _svg.append("defs");
defs
.append("filter")
.attr("id", "f1")
.append("feGaussianBlur")
.attr("in", "SourceGraphic")
.attr("stdDeviation", "1");
defs
.append("path")
.attr("id", "block")
.attr("stroke", "black")
.attr("filter", "url(#f1)")
.attr(
"d",
"M10 100 L10 50 L50 50 C100 50, 180 100, 180 100 L10 100 C20 100, 20 50, 55 50"
);
const line = g
.append("line")
.attr("stroke", "darkgoldenrod")
.attr("stroke-width", 2)
.attr("x1", 10)
.attr("y1", 100)
.attr("x2", 0)
.attr("y2", 400);
const _data = [
{
color: "red", deg: 0 },
{
color: "yellow", deg: 90 },
{
color: "blue", deg: 180 },
{
color: "green", deg: 270 },
];
g.selectAll("use")
.data(_data)
.join("use")
.attr("xlink:href", "#block")
.attr("fill", (e) => e.color)
.attr("transform", (e) => "rotate(" + e.deg + " 10 100)");
function comic() {
g.selectAll("use")
.data(_data)
.transition()
.duration(50)
.attr("transform", (e) => {
if (e.deg == 0) {
e.deg = 360;
}
e.deg -= 5;
return "rotate(" + e.deg + " 10 100)";
})
.on("end", comic);
}
comic();
const ran = d3.randomInt(20);
function shake() {
g.transition()
.duration(300)
.style("transform", `translate(${250 + ran() * 2}px, ${100 + ran()}px)`);
line
.transition()
.duration(300)
.attr("x2", ran() * 3)
.attr("y2", 400 + ran())
.on("end", shake);
}
shake();
很可爱的小风车,以此祝大家六一国际儿童节快乐~
本文写作于2023年6月1日并发布于lyrieek的掘金,于2023年7月18日进行修订发布于lyrieek的阿里云开发者社区。