成果预览
按下右侧的“点击预览”按钮能够在以后页面预览,点击链接能够全屏预览。
https://codepen.io/comehope/pen/qBLRbBM
源代码下载
每日前端实战系列的全副源代码请从 github 下载:
https://github.com/comehope/front-end-daily-challenges
代码解读
这个撕日历的我的项目尽管代码量不大,但外面有一些精妙的技巧,值得咱们学习。
整个作品咱们分成三个步骤开发,先进行动态布局,而后实现动静更换日历页,最初退出动画成果。
一、动态布局
DOM 构造是一个 .calendar
容器,其中蕴含一个示意日历页的 .page
元素,.page
里包含示意月份的 .month
元素,示意日期的 .day
元素,示意星期的 .day-name
元素,示意年的 .year
元素。
回忆一下生存中的日历,一本日历是蕴含很多日历页的,在这里也是同样的,前面你会看到,当撕掉一个 .page
元素之后,就会减少一个新的 .page
元素。当初咱们只解决布局,所以临时 DOM 中只排版一页就够了:
<div class="calendar"> <div class="page"> <p class="month">November</p> <p class="day">1</p> <p class="day-name">Monday</p> <p class="year">2021</p> </div></div>
设置 body
的子元素,也就是 .calendar
元素居中,同时设置深色背景:
body { margin: 0; height: 100vh; display: flex; align-items: center; justify-content: center; background-color: #222;}
画出日历的轮廓,用 background-image
的线性突变函数,把 .calendar
容器分成上、中、下三局部,上局部是砖红色的日历顶部,中部是浅黄色的日历页,下部是深卡其色的日历底部:
.calendar { width: 152px; height: 218px; background-image: linear-gradient( firebrick 0, firebrick 45px, lightgoldenrodyellow 45px, lightgoldenrodyellow 208px, darkkhaki 208px, darkkhaki 218px ); position: relative;}
以后成果如下图所示:
而后,再用 ::before
伪元素画2个圆点,作为日历顶部的2个铆钉。
.calendar::before { content: ''; position: absolute; height: 45px; width: 100%; background-color: firebrick; background-image: radial-gradient(circle at 40px 20px, orange 5px, transparent 5px), radial-gradient(circle at 110px 20px, orange 5px, transparent 5px); z-index: 3;}
以后成果如下图所示:
接下来布局文字:
.page { margin-top: 45px; width: 100%; height: 163px; box-sizing: border-box; padding: 16px 0 10px 0; display: flex; flex-direction: column; justify-content: space-between; position: absolute; background-color: lightgoldenrodyellow;}.page p { color: darkslategray; margin: 0; line-height: 1em; text-align: center; text-transform: uppercase; letter-spacing: 1px; pointer-events: none; user-select: none;}.page p.month,.page p.day-name { font-weight: bold; font-size: 1.1em;}.page p.day { font-weight: bold; font-size: 4em; margin-bottom: 10px;}
整个日历的动态视觉效果曾经实现了,如下图所示:
接下来钻研如何绘制出撕纸时的锯齿成果。再回忆一下理论生存中的日历,当撕日历时,日历页是从顶部的2颗铆钉上面被撕去的,所以锯齿的地位大概在日历顶部与中部联合的地位,但而且为了好看,要把锯齿暗藏在顶部上面,那为了绘制锯齿,就要先把日历顶部暗藏起来。
.calendar::before { display: none;}
锯齿绘制在伪元素 .page::before
中。锯齿是用 background-image
的线性突变函数绘制的,原理是在 10px * 10px 的区域内用2个歪斜45度的三角组合成一个“齿”,在元素上平铺的多个“齿”相连就造成了锯齿成果。另外还有1个细节,用 filter: drop-shadow()
函数给锯齿加了暗影,使它更真切。代码如下:
.page::before { content: ''; width: 100%; height: 10px; position: absolute; top: -5px; background-image: linear-gradient(-45deg, lightgoldenrodyellow 50%, transparent 50%), linear-gradient(45deg, lightgoldenrodyellow 50%, transparent 50%); background-repeat: repeat-x; background-size: 10px 10px; filter: drop-shadow(0 -5px 1px rgba(0, 0, 0, 0.1)); z-index: 2;}
成果如下图所示:
别忘了再把方才暗藏的日历顶部再显示进去,遮盖住锯齿。
.calendar::before { display: block;}
留神后面代码中应用的 z-index
属性,通过它设置锯齿的图层在日历顶部之下。这里应用了值 2
和 3
,而值 1
提前预留给日历页 .page
应用,前面实现动画时会细说。
.calendar::before { z-index: 3;}.page::before { z-index: 2;}
至此,动态布局实现,接下来解决动态数据。
二、动态数据
JavaScript 原生的 Date
对象不易用,所以咱们引入第三方的 dayjs
库,不便接下来做日期运算。
<script src="https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.11.9/dayjs.min.js"></script>
接下来写 JavaScript 代码。
定义变量 calendar
和 date
,calendar
是对 DOM 元素 .calendar
的援用,date
存储日历上的以后日期。
创立页面初始化函数 init()
,对 calendar
和 date
函数进行初始化,先清空 calendar
元素,再调用 addPage()
函数动静增加一个 .page
元素:
let calendar, date;function addPage(d) { //todo}function init() { calendar = document.querySelector('.calendar') calendar.innerHTML = '' date = dayjs() addPage(date)}window.onload = init
addPage()
函数的实现如下,留神,咱们为 .page
减少了一个 click
事件的监听器,监听函数是 tear()
,当点击时会把这张日历页撕下:
function addPage(d) { let newPage = document.createElement('div') newPage.classList.add('page') newPage.innerHTML = ` <p class="month">${d.format('MMMM')}</p> <p class="day">${d.format('D')}</p> <p class="day-name">${d.format('dddd')}</p> <p class="year">${d.format('YYYY')}</p> ` newPage.addEventListener('click', tear) calendar.appendChild(newPage)}function tear(e) { //todo}
tear()
函数的实现如下,它把以后日历页从 .calendar
容器中删除掉,
而后再增加一个新的 .page
元素:
function tear(e) { let page = e.target calendar.removeChild(page) date = date.add(1, 'day') addPage(date);}
至此,实现了点击更换日历页的性能。
此时,点击日历页时,以后的日历页会隐没,替换为第二天的日历页。
三、动画成果
终于到最初制作动画成果了,这是本我的项目最简单的中央。
咱们把动画代码写在 .page
元素的 tear
类外面。首先看动画关键帧的代码,定义了3组关键帧:tear-down
用于使日历页向下方挪动 200px
,tear-fade
用于使日历页渐隐隐没,tear-shake
模仿日历页被撕下时的稍稍歪斜。动画总时长1秒钟。
.page.tear { position: absolute; z-index: 1; pointer-events: none; transform-origin: top left; animation: 1s linear forwards; animation-name: tear-down, tear-fade, tear-shake;}@keyframes tear-down { from, 20% {top: 0;} to {top: 200px;}}@keyframes tear-fade { from, 70% {filter: opacity(1);} to {filter: opacity(0);}}@keyframes tear-shake { from, to {transform: rotate(0deg);} 20% {transform: rotate(10deg);}}
而后,批改 JavaScript 的 tear()
函数,在删除以后日历页之前,先为以后日历页减少 .tear
的 CSS 属性,使它运行动画成果。同时,要在动画完结之后再删除日历页,所以定义一个 waitMoment()
函数,它是一个 Promise ,使删除操作提早1秒执行。
function tear(e) { let page = e.target page.classList.add('tear') waitMoment().then(() => { calendar.removeChild(page) }) date = date.add(1, 'day') addPage(date);}function waitMoment() { const interval = 1000 return new Promise(function(resolve, reject) { setTimeout(resolve, interval); })}
至此,动画成果制作实现,成果如下图所示:
功败垂成!
对于作者
张偶,网络笔名 comehope,20世纪末触网,被 Web 的无穷魅力所俘获,自此始终战斗在 Web 开发第一线。
《前端每日实战》专栏是我近年来实际我的项目式学习的笔记,以我的项目驱动学习,展示从灵感闪现到代码实现的残缺过程,亦可作为前端开发的练手习题和开发参考。
拙作《CSS3 艺术》一书已由人民邮电出版社出版,全彩印刷,用100多个活泼好看的实例,系统地分析了 CSS 与视觉效果相干的重要语法,并含有近10小时的视频演示解说。京东/天猫/当当有售。