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

相关实践学习
Serverless极速搭建Hexo博客
本场景介绍如何使用阿里云函数计算服务命令行工具快速搭建一个Hexo博客。
目录
相关文章
|
6月前
|
移动开发 JavaScript 前端开发
分享86个鼠标特效,总有一款适合您
分享86个鼠标特效,总有一款适合您
83 1
分享86个鼠标特效,总有一款适合您
|
6月前
|
移动开发 JavaScript 前端开发
分享76个鼠标特效,总有一款适合您
分享76个鼠标特效,总有一款适合您
119 7
|
6月前
|
移动开发 JavaScript 安全
分享66个相册特效,总有一款适合您
分享66个相册特效,总有一款适合您
70 5
|
6月前
|
移动开发 JavaScript 前端开发
分享88个鼠标特效,总有一款适合您
分享88个鼠标特效,总有一款适合您
75 3
|
29天前
|
JavaScript
手搓日历组件,大屏样式最佳解决方案!
【10月更文挑战第6天】手搓日历组件,大屏样式最佳解决方案!
38 4
手搓日历组件,大屏样式最佳解决方案!
|
5天前
好看的粒子特效代码
好看的粒子特效代码,鼠标可以拖住旋转或者放大,喜欢的话可以拿去使用
13 2
|
2月前
宇宙星星转动特效带背景音乐引导页源码
宇宙星星转动特效带背景音乐引导页源码,源码由HTML+CSS+JS组成,记事本打开源码文件可以进行内容文字之类的修改,双击html文件可以本地运行效果,也可以上传到服务器里面,重定向这个界面
50 7
宇宙星星转动特效带背景音乐引导页源码
|
3月前
PPT 动画-制作一个倒酒
PPT 动画-制作一个倒酒
20 2
|
3月前
如何删除PPT中工具栏口袋动画
如何删除PPT中工具栏口袋动画
64 0
在PPT中制作倒计时效果,第二弹!
该视频中介绍的方法是使用PPT的加载项功能来完成。使用此方法问题还是比较多,比如可能制作时加载不出倒计时控制,也可能播放时一片空白……
128 2
在PPT中制作倒计时效果,第二弹!