用D3制作一张有翻页特效的手撕日历(只需100行代码)

简介: 在D3中用十分简单的代码就可以实现丰富的动画,下面来看一下手撕日历的动画效果吧

提起日历,第一感就会让人想到一页显示一个月的那种常规日历,那么你是否会想起小时候见到的挂在墙上的手撕日历呢?一页纸只显示当天一天,但现在提倡环保,再使用那种手撕日历便不太好了。不过我们可以用D3制作一个这样撕日历的效果,有趣的是制作这样的效果格式化后整好只需要100行代码。

前置准备

引入依赖

除了如文章用D3制作矩形式树状结构图(Treemapping)并设计动画效果所提到的引入D3外,还需要再引入一个日历的中文语言json,因为d3默认是不带中文的:

import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
import dateZhCN from "https://cdn.jsdelivr.net/npm/d3-time-format@3/locale/zh-CN.json" assert {
    type: "json" };

第一步就是把引入的这个dateZhCN作为D3默认语言:

d3.timeFormatDefaultLocale(dateZhCN);

然后假定一个需求,想通过这个日历翻到6天之后看看是哪一天,可以先写在html那边的body中,也顺带调一下body的样式将未来生成的canvas调到中心的位置:

<body style="text-align: center;font-size: 18px;">
    <h2>6天后是哪一天?</h2>
</body>

基本流程制定

首先可以假定这个日历要翻到6天后,那么实际上就要准备7页日历纸,因为要加上当天那一页纸嘛,一页纸就是一张svg,那先准备好一个数组,就专门供生成循环用就好了,所以这个数组也不需要具体日期,都是空的然后每次循环的时候+1天即可,这个很简单:

const current = new Date();
const dayData = Array(7).fill(null);
dayData.map(() => {
   
    ...
    current.setDate(current.getDate() + 1);
    console.log(current);
})

就是这样,先定义今天的日期,然后填充一个7个空值的数组,再循环数组,每次循环+1天,这个current就是从今天到6天之后的共7个Date对象了,所有的生成操作都会在这个循环中,循环结束之后就开始动画,那么下一步就开始生成具体的svg。

但请注意接下来的代码都是写在current.setDate(current.getDate() + 1);之前的,因为+1天之后就是从第二天开始了。

内容制作

生成结构

首先就是生成出这7张svg,以及它们基本的设置:

const _svg = d3
        .select("body")
        .append("svg")
        .style("position", "absolute")
        .style("left", "calc(50% - 150px)")
        .style(
                "z-index",
                -Number(d3.timeFormat("%Y")(current) + d3.timeFormat("%j")(current))
        )
        .attr("width", "300")
        .attr("height", "460");

为什么要做成absolute定位呢,因为所有的日历页应该折叠在一起,然后一张一张掉下来。left定位自不必说,我喜欢定在中间的位置上,z-index是因为小的日期要显示在前面,这个怎么排都可以的,这里是用年份拼接一年中的第几日,这样才有撕下的效果,然后就是操作这个_svg去构筑基本的几个元素了。

首先需要一张覆盖整张页面的rect作为底色:

_svg
    .append("rect")
    .attr("width", "100%")
    .attr("height", "100%")
    .attr("stroke", "#964d22")
    .attr("stroke-width", 5)
    .attr("fill", "#f9e9cd");

然后还需要一个圆,来包住日期:

_svg
    .append("circle")
    .attr("fill", "#f8c387")
    .attr("stroke", "#e5a256")
    .attr("stroke-width", 7)
    .attr("cx", "50%")
    .attr("cy", "45%")
    .attr("r", "120");

效果如下:
image.png

这样大致结构就完成了,然后就是填充上日期的具体数值文本了

生成文本

因为要写好几个文本,所以先准备一段通用的文本生成方法,很简单就是让文字居中,还有一个统一的边

function genText() {
   
    return _svg
            .append("text")
            .attr("text-anchor", "middle")
            .attr("dominant-baseline", "middle")
            .attr("stroke", "#513c20")
            .attr("stroke-width", 1)
            .attr("x", "50%");
}

生成年份,年份就定位在左上角好了:

genText()
    .attr("fill", "#584717")
    .style("font-size", "26px")
    .attr("x", "40px")
    .attr("y", "25px")
    .text(current.getFullYear());

然后生成月份,定位在最上方:

genText()
    .attr("fill", "#132c33")
    .style("font-size", "42px")
    .attr("y", "12%")
    .text(d3.timeFormat("%B")(current));

%B是D3的格式化文本,可以格式化为对应语言的月份,比喻说现在是5月,他会显示为五月,比较有意思。

接下来生成一个巨大的日期,这个当然是放在正中心了:

genText()
    .attr("fill", "#533c1b")
    .style("font-size", "160px")
    .style("font-weight", "bold")
    .attr("y", "50%")
    .attr("stroke-width", 5)
    .text(current.getDate());

最后面就放一下周吧:

genText()
    .attr("fill", "#66462a")
    .style("font-size", "64px")
    .attr("y", "85%")
    .text(d3.timeFormat("%A")(current));

%A就是周了,今天是周五,d3.timeFormat("%A")(new Date())就会显示星期五。这样就都完成了,最后别忘了,前面提到的current.setDate(current.getDate() + 1);是要放在这个循环的最后面的,这部分做好之后效果如下:
image.png

动画效果

总共分3段:

  • 第一段:往左下方向扯开纸张
  • 第二段:往右下方向撕掉
  • 第三段:纸张向下旋转滑落
dayData.pop();
d3.selectAll("svg")
    .sort(d3.ascending())
    .data(dayData)
    .transition()
    .delay(function (d, i) {
   
        return i * 300;
    })
    .style(
        "transform",
        "rotateY(30deg) translate(-100px, 50px) skew(-10deg, 10deg) scale(1.2)"
    )
    .duration(800)
    .transition()
    .style(
        "transform",
        "rotateY(210deg) translateX(-650px) skew(310deg, 55deg) scale(0.2)"
    )
    .style("opacity", "0.6")
    .duration(2400)
    .transition()
    .style(
        "transform",
        "rotateY(145deg) translate(-650px, 180px) skew(180deg, 60deg) scale(0.1)"
    )
    .style("opacity", "0.1")
    .duration(800)
    .remove();

最开头的dayData.pop();为什么要删掉一页呢,是因为最后一条的是要显示的日子,就撕掉6页纸就好了,然后就是每一段的数值调试,是略有些麻烦的,需要耐心。最后调低opacity淡化掉日历页之后就彻底删除掉。

下面就是播放的动画效果了:

效果展示

Animation-min.gif

最后贴一下整体的源码,其实非常的轻量,去掉3个空行,刚好就100行代码:

综合源代码

import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
import dateZhCN from "https://cdn.jsdelivr.net/npm/d3-time-format@3/locale/zh-CN.json" assert {
    type: "json" };

d3.timeFormatDefaultLocale(dateZhCN);

const current = new Date();
const dayData = Array(7).fill(null);
dayData.map(() => {
   
    const _svg = d3
        .select("body")
        .append("svg")
        .style("position", "absolute")
        .style("left", "calc(50% - 150px)")
        .style(
            "z-index",
            -Number(d3.timeFormat("%Y")(current) + d3.timeFormat("%j")(current))
        )
        .attr("width", "300")
        .attr("height", "460");

    _svg
        .append("rect")
        .attr("width", "100%")
        .attr("height", "100%")
        .attr("stroke", "#964d22")
        .attr("stroke-width", 5)
        .attr("fill", "#f9e9cd");

    _svg
        .append("circle")
        .attr("fill", "#f8c387")
        .attr("stroke", "#e5a256")
        .attr("stroke-width", 7)
        .attr("cx", "50%")
        .attr("cy", "45%")
        .attr("r", "120");

    function genText() {
   
        return _svg
            .append("text")
            .attr("text-anchor", "middle")
            .attr("dominant-baseline", "middle")
            .attr("stroke", "#513c20")
            .attr("stroke-width", 1)
            .attr("x", "50%");
    }
    genText()
        .attr("fill", "#584717")
        .style("font-size", "26px")
        .attr("x", "40px")
        .attr("y", "25px")
        .text(current.getFullYear());

    genText()
        .attr("fill", "#132c33")
        .style("font-size", "42px")
        .attr("y", "12%")
        .text(d3.timeFormat("%B")(current));

    genText()
        .attr("fill", "#533c1b")
        .style("font-size", "160px")
        .style("font-weight", "bold")
        .attr("y", "50%")
        .attr("stroke-width", 5)
        .text(current.getDate());

    genText()
        .attr("fill", "#66462a")
        .style("font-size", "64px")
        .attr("y", "85%")
        .text(d3.timeFormat("%A")(current));
    current.setDate(current.getDate() + 1);
});

dayData.pop();
d3.selectAll("svg")
    .sort(d3.ascending())
    .data(dayData)
    .transition()
    .delay(function (d, i) {
   
        return i * 300;
    })
    .style(
        "transform",
        "rotateY(30deg) translate(-100px, 50px) skew(-10deg, 10deg) scale(1.2)"
    )
    .duration(800)
    .transition()
    .style(
        "transform",
        "rotateY(210deg) translateX(-650px) skew(310deg, 55deg) scale(0.2)"
    )
    .style("opacity", "0.6")
    .duration(2400)
    .transition()
    .style(
        "transform",
        "rotateY(145deg) translate(-650px, 180px) skew(180deg, 60deg) scale(0.1)"
    )
    .style("opacity", "0.1")
    .duration(800)
    .remove();

其实还有些优化,例如svg本身和下面的text的css可以统一设置,但我不想另外再写css了,本文只做一个很简单的效果展示。

本文写作于2023年5月26日并发布于lyrieek的掘金,于2023年7月17日进行修订发布于lyrieek的阿里云开发者社区。

目录
相关文章
|
移动开发 开发框架 小程序
|
小程序 JavaScript 前端开发
微信小程序 |从零实现酷炫纸质翻页效果
微信小程序 |从零实现酷炫纸质翻页效果
2845 0
微信小程序 |从零实现酷炫纸质翻页效果
|
前端开发 容器 API
基于 three.js 的 3D 粒子动效实现
作者:个推web前端开发工程师 梁神 一、背景 粒子特效是为模拟现实中的水、火、雾、气等效果由各种三维软件开发的制作模块,原理是将无数的单个粒子组合使其呈现出固定形态,借由控制器、脚本来控制其整体或单个的运动,模拟出现真实的效果。
3464 0
|
7月前
|
机器学习/深度学习 数据采集 PyTorch
用PyTorch从零开始编写DeepSeek-V2
本文详细介绍了如何使用PyTorch从零开始实现DeepSeek-V2,包括数据准备、模型构建、训练和测试等各个环节。掌握这些内容后,您可以根据自己的需求对模型进行扩展和优化,应用于更广泛的图像分析任务中。希望本指南能帮助您在深度学习领域更进一步。
496 9
|
7月前
|
机器学习/深度学习 人工智能 自然语言处理
Magma:微软放大招!新型多模态AI能看懂视频+浏览网页+UI交互+控制机器人,数字世界到物理现实无缝衔接
Magma 是微软研究院开发的多模态AI基础模型,结合语言、空间和时间智能,能够处理图像、视频和文本等多模态输入,适用于UI导航、机器人操作和复杂任务规划。
415 2
|
数据安全/隐私保护
解决使用SourceTree下载GitLab服务器上的代码每次都需要输入密码问题
解决使用SourceTree下载GitLab服务器上的代码每次都需要输入密码问题
358 2
|
JavaScript
vue 配置【详解】 vue.config.js ( 含 webpack 配置 )
vue 配置【详解】 vue.config.js ( 含 webpack 配置 )
386 0
|
弹性计算 关系型数据库 MySQL
阿里云MySQL云数据库优惠价格、购买和使用教程分享!
阿里云数据库使用流程包括购买和管理。首先,选购支持MySQL、SQL Server、PostgreSQL等的RDS实例,如选择2核2GB的MySQL,设定地域和可用区。购买后,等待实例创建。接着,创建数据库和账号,设置DB名称、字符集及账号权限。最后,通过DMS登录数据库,填写账号和密码。若ECS在同一地域和VPC内,可内网连接,记得将ECS IP加入白名单。
1142 2
|
移动开发 JavaScript 前端开发
分享996个实用的JavaScript特效你要的全在这里
分享996个实用的JavaScript特效你要的全在这里
1099 0