用D3制作一个旋转的大风车SVG庆祝国际儿童节

简介: 写于2023年的六一国际儿童节,用D3做一个大风车庆祝一下这个快乐的节日,借此机会介绍一下SVG滤镜的使用,代码也是非常简短的

国际儿童节,尽管称之为国际,但实际上这个节日并非联合国定的,联合国定的儿童节叫“世界儿童节”,在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");

image.png

然后将定位到中心,因为后续还有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");

image.png

可以看到已经定位在中间了,然后更改一下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");
});

image.png

是理想中的效果,为什么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,效果如下:

image.png

效果很完美,下一步就是做动画,转起来:

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度,不停的转,效果如下:

({X)UZJKVO8S41UH2$NHQ%J.gif

最后想起来应该加个棍,这个部分应该放在写入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),往上靠,然后棍子稍微倾斜一点好看一点:

image.png

下面再加一段动画,让这个风车整体在旋转的同时摇晃起来,毕竟风车怎么能不晃动嘛:

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,显得转的很快才抖动起来

WR(QL~1F`33YM)}6RJRWG(A.gif

完整源代码

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的阿里云开发者社区。

目录
相关文章
|
JavaScript 前端开发
揭秘 TypeScript 条件类型:超越简单类型检查
揭秘 TypeScript 条件类型:超越简单类型检查
|
5月前
|
人工智能 编解码 自然语言处理
AI电影解说的技术链路拆解:从视频理解到自动剪辑
AI电影解说的技术链路拆解:从视频理解到自动剪辑
1250 10
|
Java 数据库连接 网络安全
JDBC常用特性-SSH隧道连接
JDBC常用特性-SSH隧道连接
|
8月前
|
人工智能 机器人 物联网
提高商店客流量的智能设备选型指南:从客流分析到数字化运营
实体零售面临客流下滑与运营瓶颈,亟需从“坐商”转向“数据驱动+主动引流”。本文系统解析六大类智能设备技术架构,涵盖客流分析、智能引流、智慧收银、导视系统、数据中台与氛围营造,结合不同业态痛点提供选型策略与实施路径,助力门店实现数字化升级与效率跃迁。
|
存储 监控 固态存储
RAID10怎么创建?RAID10创建详细步骤
RAID 10(RAID 1+0)是一种结合了RAID 1镜像与RAID 0条带化技术的存储阵列,兼具高性能和数据冗余优势。其创建需7步:硬件准备、配置RAID设备、选择级别、添加硬盘、设置条带化大小、保存退出及初始化阵列。RAID 10通过两两分组构建RAID 1镜像,再以RAID 0连接提升性能。但成本较高,且若单镜像内两盘同时损坏,数据将无法恢复。未来,SSD普及、NVMe应用及软件定义存储等趋势将进一步优化RAID 10性能。创建时需注意备份数据、选用可靠硬件并定期维护监控。
1229 7
|
Linux 虚拟化 iOS开发
Debian 13 x86_64 OVF (sysin) - VMware 虚拟机模板
Debian 13 x86_64 OVF (sysin) - VMware 虚拟机模板
954 0
Debian 13 x86_64 OVF (sysin) - VMware 虚拟机模板
|
存储 人工智能 自然语言处理
让你拥有一个AI大脑,这个32.1k Github项目是你不错的选择,支持PDF、Markdown、代码、视频成为你的知识内容
Quivr 是开源全栈 RAG 平台,助你打造“第二大脑”,支持多文档类型与多种 LLM,实现智能搜索与聊天。具备语义检索、本地部署、隐私保护等功能,适用于个人知识管理与企业知识库,界面简洁易用,是高效智能问答的理想选择。
761 0
|
人工智能 自然语言处理 算法
电商行业智能搜索技术原理全解析
对于电商平台来说,智能搜索功能是至关重要的。本文剖析电商行业的搜索专属特点和业务需求,并介绍开放搜索提供的【电商行业模板】智能搜索能力,希望带给企业更多提升业务转化的思路和解决方案~
6734 1
电商行业智能搜索技术原理全解析
|
测试技术
po+selenium+unittest自动化测试项目实战
po+selenium+unittest自动化测试项目实战
4134 0
 po+selenium+unittest自动化测试项目实战
|
设计模式 前端开发 JavaScript
计算机毕业系统答辩问题汇总~~当你的系统是家乡特产平台系统
这篇文章汇总了计算机毕业设计系统答辩中可能遇到的问题,特别是针对一个基于Vue和SpringBoot开发的家乡特产平台系统,涵盖了系统设计、功能实现、技术选型、性能安全、未来展望和用户体验等方面的答辩技巧和建议。
计算机毕业系统答辩问题汇总~~当你的系统是家乡特产平台系统

热门文章

最新文章