SegmentFault 前端每日实战最新的文章
2023-09-07T14:29:09+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
《前端每日实战》第179号作品:撕日历
https://segmentfault.com/a/1190000044191742
2023-09-07T14:29:09+08:00
2023-09-07T14:29:09+08:00
comehope
https://segmentfault.com/u/comehope
2
<p><img width="216" height="285" src="/img/bVc9Ar1" alt="" title=""></p><h2>效果预览</h2><p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p><p><a href="https://codepen.io/comehope/pen/qBLRbBM">https://codepen.io/comehope/pen/qBLRbBM</a></p><h2>源代码下载</h2><p>每日前端实战系列的全部源代码请从 github 下载:</p><p><a href="https://link.segmentfault.com/?enc=zLaKPLChH3LpM2auDf0QrA%3D%3D.Ptz6AvieDQlyDj5SZGv5oAidvhK1ETt8%2F9Vw1lUBPof0156JWh6fYuViX235Oe8yYquyDSgSExdHr3njlul76A%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p><h2>代码解读</h2><p>这个撕日历的项目虽然代码量不大,但里面有一些精妙的技巧,值得我们学习。</p><p>整个作品我们分成三个步骤开发,先进行静态布局,然后实现动态更换日历页,最后加入动画效果。</p><h3>一、静态布局</h3><p>DOM 结构是一个 <code>.calendar</code> 容器,其中包含一个表示日历页的 <code>.page</code> 元素,<code>.page</code> 里包括表示月份的 <code>.month</code> 元素,表示日期的 <code>.day</code> 元素,表示星期的 <code>.day-name</code> 元素,表示年的 <code>.year</code> 元素。</p><p>回想一下生活中的日历,一本日历是包含很多日历页的,在这里也是同样的,后面你会看到,当撕掉一个 <code>.page</code> 元素之后,就会增加一个新的 <code>.page</code> 元素。现在我们只处理布局,所以暂时 DOM 中只排版一页就够了:</p><pre><code class="html"><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></code></pre><p>设置 <code>body</code> 的子元素,也就是 <code>.calendar</code> 元素居中,同时设置深色背景:</p><pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: #222;
}</code></pre><p>画出日历的轮廓,用 <code>background-image</code> 的线性渐变函数,把 <code>.calendar</code> 容器分成上、中、下三部分,上部分是砖红色的日历顶部,中部是浅黄色的日历页,下部是深卡其色的日历底部:</p><pre><code class="css">.calendar {
width: 152px;
height: 218px;
background-image:
linear-gradient(
firebrick 0,
firebrick 45px,
lightgoldenrodyellow 45px,
lightgoldenrodyellow 208px,
darkkhaki 208px,
darkkhaki 218px
);
position: relative;
}</code></pre><p>当前效果如下图所示:</p><p><img width="216" height="285" src="/img/bVc9Ar6" alt="" title=""></p><p>然后,再用 <code>::before</code> 伪元素画2个圆点,作为日历顶部的2个铆钉。</p><pre><code class="css">.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;
}</code></pre><p>当前效果如下图所示:</p><p><img width="216" height="285" src="/img/bVc9Ar7" alt="" title=""></p><p>接下来布局文字:</p><pre><code class="css">.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;
}</code></pre><p>整个日历的静态视觉效果已经完成了,如下图所示:</p><p><img width="216" height="285" src="/img/bVc9Ar9" alt="" title=""></p><p>接下来研究如何绘制出撕纸时的锯齿效果。再回想一下实际生活中的日历,当撕日历时,日历页是从顶部的2颗铆钉下面被撕去的,所以锯齿的位置大约在日历顶部与中部结合的位置,但而且为了美观,要把锯齿隐藏在顶部下面,那为了绘制锯齿,就要先把日历顶部隐藏起来。</p><pre><code class="css">.calendar::before {
display: none;
}</code></pre><p>锯齿绘制在伪元素 <code>.page::before</code> 中。锯齿是用 <code>background-image</code> 的线性渐变函数绘制的,原理是在 10px * 10px 的区域内用2个倾斜45度的三角组合成一个“齿”,在元素上平铺的多个“齿”相连就形成了锯齿效果。另外还有1个细节,用 <code>filter: drop-shadow()</code> 函数给锯齿加了阴影,使它更逼真。代码如下:</p><pre><code class="css">.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;
}</code></pre><p>效果如下图所示:</p><p><img width="216" height="285" src="/img/bVc9Asb" alt="" title=""></p><p>别忘了再把刚才隐藏的日历顶部再显示出来,遮盖住锯齿。</p><pre><code class="css">.calendar::before {
display: block;
}</code></pre><p>注意前面代码中使用的 <code>z-index</code> 属性,通过它设置锯齿的图层在日历顶部之下。这里使用了值 <code>2</code> 和 <code>3</code>,而值 <code>1</code> 提前预留给日历页 <code>.page</code> 使用,后面实现动画时会细说。</p><pre><code class="css">.calendar::before {
z-index: 3;
}
.page::before {
z-index: 2;
}</code></pre><p>至此,静态布局完成,接下来处理动态数据。</p><h3>二、动态数据</h3><p>JavaScript 原生的 <code>Date</code> 对象不易用,所以我们引入第三方的 <code>dayjs</code> 库,方便接下来做日期运算。</p><pre><code class="html"><script src="https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.11.9/dayjs.min.js"></script></code></pre><p>接下来写 JavaScript 代码。</p><p>定义变量 <code>calendar</code> 和 <code>date</code>,<code>calendar</code> 是对 DOM 元素 <code>.calendar</code> 的引用,<code>date</code> 存储日历上的当前日期。</p><p>创建页面初始化函数 <code>init()</code>,对 <code>calendar</code> 和 <code>date</code> 函数进行初始化,先清空 <code>calendar</code> 元素,再调用 <code>addPage()</code> 函数动态添加一个 <code>.page</code> 元素:</p><pre><code class="js">let calendar, date;
function addPage(d) {
//todo
}
function init() {
calendar = document.querySelector('.calendar')
calendar.innerHTML = ''
date = dayjs()
addPage(date)
}
window.onload = init</code></pre><p><code>addPage()</code> 函数的实现如下,注意,我们为 <code>.page</code> 增加了一个 <code>click</code> 事件的监听器,监听函数是 <code>tear()</code>,当点击时会把这张日历页撕下:</p><pre><code class="js">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
}</code></pre><p><code>tear()</code> 函数的实现如下,它把当前日历页从 <code>.calendar</code> 容器中删除掉,<br>然后再添加一个新的 <code>.page</code> 元素:</p><pre><code class="js">function tear(e) {
let page = e.target
calendar.removeChild(page)
date = date.add(1, 'day')
addPage(date);
}</code></pre><p>至此,完成了点击更换日历页的功能。<br>此时,点击日历页时,当前的日历页会消失,替换为第二天的日历页。</p><h3>三、动画效果</h3><p>终于到最后制作动画效果了,这是本项目最复杂的地方。</p><p>我们把动画代码写在 <code>.page</code> 元素的 <code>tear</code> 类里面。首先看动画关键帧的代码,定义了3组关键帧:<code>tear-down</code> 用于使日历页向下方移动 <code>200px</code>,<code>tear-fade</code> 用于使日历页渐隐消失,<code>tear-shake</code> 模拟日历页被撕下时的稍稍倾斜。动画总时长1秒钟。</p><pre><code class="css">.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);}
}</code></pre><p>然后,修改 JavaScript 的 <code>tear()</code> 函数,在删除当前日历页之前,先为当前日历页增加 <code>.tear</code> 的 CSS 属性,使它运行动画效果。同时,要在动画结束之后再删除日历页,所以定义一个 <code>waitMoment()</code> 函数,它是一个 Promise ,使删除操作延迟1秒执行。</p><pre><code class="js">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);
})
}</code></pre><p>至此,动画效果制作完成,效果如下图所示:</p><p><img width="216" height="285" src="/img/bVc9Ar1" alt="" title=""></p><p>大功告成!</p><h2>关于作者</h2><p>张偶,网络笔名 comehope,20世纪末触网,被 Web 的无穷魅力所俘获,自此始终战斗在 Web 开发第一线。</p><p>《前端每日实战》专栏是我近年来实践项目式学习的笔记,以项目驱动学习,展现从灵感闪现到代码实现的完整过程,亦可作为前端开发的练手习题和开发参考。</p><p><img width="625" height="800" src="https://segmentfault.com/img/bVbCcXO" alt="" title=""></p><p>拙作《CSS3 艺术》一书已由人民邮电出版社出版,全彩印刷,用100多个生动美观的实例,系统地剖析了 CSS 与视觉效果相关的重要语法,并含有近10小时的视频演示讲解。京东/天猫/当当有售。</p>
《前端每日实战》第178号作品:地砖图案设计器
https://segmentfault.com/a/1190000040459725
2021-08-05T11:12:59+08:00
2021-08-05T11:12:59+08:00
comehope
https://segmentfault.com/u/comehope
8
<p><img src="/img/bVcTVxW" alt="" title=""></p><h2>效果预览</h2><p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p><p><a href="https://codepen.io/comehope/pen/QWvVBJq">https://codepen.io/comehope/pen/QWvVBJq</a></p><h2>源代码下载</h2><p>每日前端实战系列的全部源代码请从 github 下载:</p><p><a href="https://link.segmentfault.com/?enc=ZvxLx5BLet5MF9MDp5hDyQ%3D%3D.H%2FAkNjJPSSktJOfvGSWkR21zlHhmiitbXVK0nBMK%2F5Lg2deG1PDwzpBe%2FaO4lQypMl4S3UijI25yrxpixjANvA%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p><h2>代码解读</h2><h3>功能和概念</h3><p>这个项目的起源是我看到<a href="https://link.segmentfault.com/?enc=VuPirm8X695Ow9mkyUsN7A%3D%3D.vOF2ptkA%2BLpBRljqkJnyV5cebTcaRQ7pXUOBheYYKCgo6FR8YZHiCqzXqYeR1XCMSL%2B9lfPPqjvDBCSekckKDM7NUGfttXjbJQSpOky2eKA%3D" rel="nofollow">一个网页</a>介绍把小立方块涂上颜色拼成图案,正好我家里也有一些这样的小立方块,于是也拿来涂了色拼出了各种花样,在玩儿的过程中我产生了做一个设计器的想法。</p><p><img src="/img/bVcTVx2" alt="" title=""></p><p>设计器包括4个功能:</p><ol><li>自定义图案:在页面左侧上部;</li><li>预览图案的平铺效果:在页面右侧;</li><li>提供3种供预览的网格尺寸:在页面右侧下部;</li><li>提供12种预设的图案:在页面左侧下部。</li></ol><p>后面会提到一些业务概念,它们也是程序里的变量名:</p><ul><li>地砖:tile。左上角地砖称为样品地砖。</li><li>地砖的四分之一:block。一块地砖由4个 block 组成,每个 block 的图案是1个内含三角形的小正方形。</li><li>网格状地板:floor。地板上铺满了地砖,地板尺寸有3种:2x2、4x4、8x8。</li><li>预设图案:pattern。每个预设图案就是一块地砖。</li></ul><p>接下来就依次实现设计器的4个功能。</p><h3>第1个功能:自定义图案</h3><p>定义 dom 结构如下:</p><pre><code class="html"><main>
<div class="sample">
<div class="tile">
<div class="block"></div>
</div>
</div>
</main></code></pre><p>程序的所有元素都被包含在 <code><main></code> 元素里,<code><main></code> 元素会随着功能的扩充不断丰富。现在它里面有一个表示“样品区”的 <code>.sample</code> 子元素,其中再包含一个表示地砖的 <code>.tile</code> 元素。<code>.tile</code> 元素里面本应包含4个 <code>.block</code> 元素,不急,先用1个 <code>.block</code> 做做实验。</p><p>用 CSS 的伪元素在 <code>.block</code> 里画一个三角形:</p><pre><code class="css">.tile .block {
width: 10em;
height: 10em;
border: 1px solid grey;
box-sizing: border-box;
color: dimgray;
position: relative;
}
.tile .block::before {
content: '';
position: absolute;
border-width: calc(5em - 1px);
border-style: solid;
border-color: transparent;
border-left-color: currentColor;
}</code></pre><p>效果如下图:</p><p><img src="/img/bVcTVyh" alt="" title=""></p><p>这个三角形占据了正方形 <code>.blcok</code> 的四分之一空间,它是我们需要的三角形的一半。</p><p>接下来再画另一个三角形,为了和前一个三角形区别开,把它填充成黑色:</p><pre><code class="css">.tile .block::before {
border-top-color: black;
}</code></pre><p>效果如下图:</p><p><img src="/img/bVcTVyq" alt="" title=""></p><p>两个小三角形拼成了一个大三角形。</p><p>为什么不直接画一个大三角形呢?因为 CSS 画直角三角形的方法,是以正方形的某一条边作为斜边,然后指定三角形的高,如果直接画大三角形,那就要先构造一个大正方形,这个大正方形的边长需是小正方形边长的根号2倍,画一个大三角形比画2个小三角形还要复杂呢。</p><p>每个小三角形的高理论上应该是边长的一半 <code>5em</code>,这里取 <code>5em-1px</code>,是因为 <code>box-sizing: border-box</code> 属性导致边框向容器内侵占了 <code>1px</code>。</p><p>随手重构一下,<code>::before</code> 伪元素的最后三行有关边框颜色的代码可以合并写成一行:</p><pre><code class="css">.tile .block::before {
/*border-color: transparent;
border-left-color: currentColor;
border-top-color: black;*/
border-color: currentColor transparent transparent currentColor;
}</code></pre><p>以上代码没有在伪元素的 <code>border-color</code> 属性中指定明确色值,使用的是 <code>currentColor</code>,这样就可以由主元素来控制颜色,便于后续修改地砖的颜色。</p><p>重构后效果如下图:</p><p><img src="/img/bVcTVyr" alt="" title=""></p><p>经过上面的实验,已经成功在一个 <code>.block</code> 里画出了三角形,现在把 <code>.tile</code> 的子元素增加到4个 <code>.block</code>:</p><pre><code class="html"><main>
<div class="sample">
<div class="tile">
<div class="block"></div>
<div class="block"></div>
<div class="block"></div>
<div class="block"></div>
</div>
</div>
</main></code></pre><p>将4个 <code>.block</code> 组成一个田字格形状:</p><pre><code class="css">.tile {
width: 20em;
display: grid;
grid-template-columns: repeat(2, 1fr);
}
.sample > .tile .block {
cursor: pointer;
}</code></pre><p>效果如下图:</p><p><img src="/img/bVcTVys" alt="" title=""></p><p>至此,一块地砖的布局已完成,接下来,要解决如何控制每个 <code>.block</code> 的问题。先定义 4 个 CSS 变量:</p><pre><code class="css">:root {
--block-angle-1: 0;
--block-angle-2: 0;
--block-angle-3: 0;
--block-angle-4: 0;
}</code></pre><p>这些变量用来表示 4 个 <code>.block</code> 中三角形顶点的位置,0 表示顶点在左上,90 表示顶点在右上,180 表示顶点在右下,270 表示顶点在左下。</p><p>把这 4 个变量分配给 4 个 <code>.block</code>,应用到 <code>transform: rotate()</code> 属性中,0/90/180/270 指的就是 <code>.block</code> 元素的旋转角度:</p><pre><code class="css">.tile .block:nth-child(1) {transform: rotate(calc(var(--block-angle-1) * 1deg));}
.tile .block:nth-child(2) {transform: rotate(calc(var(--block-angle-2) * 1deg));}
.tile .block:nth-child(3) {transform: rotate(calc(var(--block-angle-3) * 1deg));}
.tile .block:nth-child(4) {transform: rotate(calc(var(--block-angle-4) * 1deg));}</code></pre><p>现在,试一试修改这4个 CSS 变量值,地砖图案会跟着被调整。</p><p>接下来写 js 代码,实现通过点击来调整地砖图案的效果。</p><p>先定义一个 <code>dom</code> 变量,用于引用 dom 元素,<code>dom.root</code> 是指 CSS 的 <code>:root</code> 元素,<code>dom.sampleTile</code> 就是我们刚刚创建的样品地砖:</p><pre><code class="js">const $ = (selector) => document.querySelector(selector)
let dom = {
root: document.documentElement,
sampleTile: $('.sample > .tile'),
}</code></pre><p>在页面加载完成后调用一个初始化函数 <code>init()</code>,在其中完成对事件的绑定:</p><pre><code class="js">window.onload = init()
function init() {
initEvent()
}</code></pre><p><code>initEvent()</code> 函数将遍历样品地砖的每一个 <code>.block</code>,令其在被点击时执行 <code>rotateBlcok()</code> 函数,传入该函数的参数分别是1、2、3、4,是4个 <code>.block</code> 元素的序号。<code>rotateBlock()</code> 函数读取该 <code>.block</code> 对应的 CSS 变量,得到它的旋转角度,然后加上 90,意即让这个 <code>.block</code> 旋转 90 度:</p><pre><code class="js">function initEvent() {
Array.from(dom.sampleTile.children).forEach((block, i) => {
block.addEventListener('click', () => {
rotateBlock(i + 1)
})
})
}
function getCssVariableName(sequenceNumberOfBlcok) {
return `--block-angle-${sequenceNumberOfBlcok}`
}
function rotateBlock(num) {
let angle = +dom.root.style.getPropertyValue(getCssVariableName(num)) + 90
setBlockAngle(num, angle)
}
function setBlockAngle(num, angle) {
dom.root.style.setProperty(getCssVariableName(num), angle)
}</code></pre><p>另外 2 个函数 <code>getCssVariableName()</code> 和 <code>setBlockAngle()</code> 不用解释,看名字就知道是什么意思了。这种简短的、甚至只有一条语句的细粒度函数,能让代码更加语义化,让阅读代码更流畅。</p><p>现在试一试,每点击一下任意一个 <code>.block</code>,它就会旋转90度。</p><p>这是一个经过设计的地砖图案:</p><p><img src="/img/bVcTVyw" alt="" title=""></p><p>为了加强动感,再给 <code>.block</code> 加一个过渡动画:</p><pre><code class="css">.tile .block {
transition: 0.2s;
}</code></pre><h3>第2个功能:平铺</h3><p>接下来实现第2个功能,在地板上平铺地砖。</p><p>先扩充 dom,为 <code><main></code> 元素增加一个表示成品的 <code>.production</code> 元素,其中包含一个表示地板的子元素 <code>.floor</code>:</p><pre><code class="html"><main>
<div class="sample">
<div class="tile">
<div class="block"></div>
<div class="block"></div>
<div class="block"></div>
<div class="block"></div>
</div>
</div>
<div class="production">
<div class="floor"></div>
</div>
</main></code></pre><p><code>.floor</code> 里面应该包含多个地砖元素,这些元素将通过程序自动创建。</p><p>扩充一下 <code>dom</code> 变量,增加对 <code>.floor</code> 的引用:</p><pre><code class="js">let dom = {
root: document.documentElement,
sampleTile: $('.sample > .tile'),
floor: $('.production .floor'),
}</code></pre><p>再扩充一下初始化函数 <code>init()</code>,在其中调用 <code>initFloor()</code>:</p><pre><code class="js">function init() {
initEvent()
initFloor()
}</code></pre><p><code>initFloor()</code> 函数再调用 <code>paveTiles()</code> 函数,传入的参数表示地板网格每边的地砖数量,作为实验,传入数字 <code>2</code>,表示要填充一个 2x2 网格的地板。<code>paveTiles()</code> 函数实现在 <code>.floor</code> 中插入若干 <code>.tile</code> 元素的操作。<code>node.cloneNode(true)</code> 用于得到 <code>node</code> 元素的深拷贝,以便复制它的所有子元素。</p><pre><code class="js">function initFloor() {
paveTiles(2)
}
function paveTiles(countOfPerSide) {
let count = Math.pow(countOfPerSide, 2)
dom.floor.innerHTML = ''
new Array(count).fill('').forEach(() => {
dom.floor.append(dom.sampleTile.cloneNode(true))
})
}</code></pre><p>现在运行一下程序,能看到确实多了很多地砖,但它们都和样品地砖竖向排在一起。没关系,用 CSS 调整一下布局。</p><p>先把 <code><main></code> 元素整体设置为左右结构布局:</p><pre><code class="css">main {
display: flex;
justify-content: space-between;
width: 65em;
}
.sample {width: 20em;}
.production {width: 40em;}</code></pre><p>然后把地板排列成网格状:</p><pre><code class="css">.production .floor {
--count-of-per-side: 2;
display: grid;
grid-template-columns: repeat(var(--count-of-per-side), 1fr);
font-size: calc(2em / var(--count-of-per-side));
}</code></pre><p>在这段 CSS 代码中,又定义了一个变量 <code>--count-of-per-side</code>,它和 <code>paveTile()</code> 函数的参数是同样的含义,都表示地板网格每边的地砖数量,而且值也保持一致,目前都是 2。注意这段代码中的 <code>font-size</code> 属性,它会根据每网格大小来调整,网格越密,字体就越小,以便在同样大小的容器内可以显示不同密度的网格。</p><p>现在试试调整左侧的样品地砖的图案,能看到右侧地板的所有地砖也跟着整齐划一地跟着变化。</p><p>效果如下图:</p><p><img src="/img/bVcTVyF" alt="" title=""></p><h3>第3个功能:切换地板网格</h3><p>接下来实现第3个功能,调整地板网格的尺寸。</p><p>先扩充 dom,在 <code>.production</code> 元素中增加 <code>grid-list</code> 元素,其中包含 3 个按钮,分别用于把地板网格切换为 2x2、4x4、8x8 的尺寸:</p><pre><code class="html"><main>
<div class="sample">
<div class="tile">
<div class="block"></div>
<div class="block"></div>
<div class="block"></div>
<div class="block"></div>
</div>
</div>
<div class="production">
<div class="floor"></div>
<div class="grid-list">
<button>2x2</button>
<button>4x4</button>
<button>8x8</button>
</div>
</div>
</main></code></pre><p>调整一下这 3 个按钮的布局,让它们均匀地排列在地板下方:</p><pre><code class="css">.production .grid-list {
display: flex;
justify-content: space-around;
margin-top: 2em;
}
.production .grid-list button {
font-size: 1.5em;
width: 6em;
letter-spacing: 0.4em;
cursor: pointer;
}</code></pre><p>接下来修改程序,让这 3 个按钮生效。</p><p>先扩充一下 <code>dom</code> 变量,增加一个表示切换按钮区 <code>dom.gridList</code> 的引用:</p><pre><code class="js">let dom = {
root: document.documentElement,
sampleTile: $('.sample > .tile'),
floor: $('.production .floor'),
gridList: $('.production .grid-list'),
}</code></pre><p>再扩充 <code>initEvent()</code> 函数,为按钮绑定点击事件,当按钮被点击时,调用 <code>paveTiles()</code> 函数重铺地板:</p><pre><code class="js">function initEvent() {
Array.from(dom.sampleTile.children).forEach((block, i) => {
block.addEventListener('click', () => {
rotateBlock(i + 1)
})
})
Array.from(dom.gridList.children).forEach(button => {
button.addEventListener('click', (e) => {
paveTiles(parseInt(e.target.innerText))
})
})
}</code></pre><p>现在刷新一下页面,发现点击 3 个按钮之后地板中的地砖确实增加了,但是地板容纳不下那么多地砖,只好向下排列。这是因为在前面的 CSS 代码中,<code>--count-of-per-side</code> 变量被赋值为 2,所以需要在 <code>paveTiles()</code> 函数中更新它,使网格密度随着地砖的数量自动调整:</p><pre><code class="js">function paveTiles(countOfPerSide) {
let count = Math.pow(countOfPerSide, 2)
dom.floor.innerHTML = ''
new Array(count).fill('').forEach(() => {
dom.floor.append(dom.sampleTile.cloneNode(true))
})
dom.floor.style.setProperty('--count-of-per-side', countOfPerSide)
}</code></pre><p>至此,切换地板网格的功能就完成了。</p><p>效果如下图:</p><p><img src="/img/bVcTVyG" alt="" title=""></p><p>回想一下之前写的 <code>initFloor()</code> 函数中有一条语句 <code>paveTiles(2)</code>,这里硬编码了一个数字 2,现在应该把它重构成读取第 1 个按钮的数值,避免使用魔法数字:</p><pre><code class="js">function initFloor() {
paveTiles(parseInt(dom.gridList.children[0].innerText))
}</code></pre><h3>第4个功能:预设图案</h3><p>接下来开发第4个功能,展示若干预设图案供选择。</p><p>先扩充 dom,在 <code>.sample</code> 元素中增加 <code>.pattern-list</code> 元素:</p><pre><code class="html"><main>
<div class="sample">
<div class="tile">
<div class="block"></div>
<div class="block"></div>
<div class="block"></div>
<div class="block"></div>
</div>
<div class="pattern-list"></div>
</div>
<div class="production">
<div class="floor"></div>
<div class="grid-list">
<button>2x2</button>
<button>4x4</button>
<button>8x8</button>
</div>
</div>
</main></code></pre><p><code>.pattern-list</code> 和前面的 <code>.grid-list</code> 类似,dom 中只定义了一个容器,其中的子元素都要由程序生成。</p><p>先定义一组预设图案数据,每个预设图案是一个含有4个数值的数组,存储着地砖的4个 block 的角度:</p><pre><code class="js">let patterns = [
[0, 0, 0, 0],
[0, 90, 270, 180],
[180, 270, 90, 0],
[270, 0, 180, 90],
[90, 270, 270, 90],
[180, 270, 0, 90],
[270, 270, 90, 90],
[270, 180, 0, 90],
[0, 270, 90, 180],
[180, 270, 180, 270],
[270, 180, 180, 270],
[180, 90, 90, 180],
]</code></pre><p>扩充 <code>dom</code> 变量,增加一个表示预设图案列表的引用 <code>dom.patternList</code>:</p><pre><code class="js">let dom = {
root: document.documentElement,
sampleTile: $('.sample > .tile'),
floor: $('.production .floor'),
gridList: $('.production .grid-list'),
patternList: $('.sample .pattern-list'),
}</code></pre><p>初始化函数 <code>init()</code> 中增加一行语句,用于调用初始化预设图案列表的函数 <code>initPatternList()</code>:</p><pre><code class="js">function init() {
initEvent()
initFloor()
initPatternList()
}</code></pre><p><code>initPatternList()</code> 函数是为 <code>.pattern-list</code> 元素填<br>充子元素的具体实现。和 <code>paveTiles()</code> 函数类似,子元素也是对样品地砖进行了多次复制。在复制时,还要把变量 <code>patterns</code> 的数据写到新生成的地砖上:</p><pre><code class="js">function initPatternList() {
patterns.forEach((pattern) => {
let $newTile = dom.sampleTile.cloneNode(true)
Array.from($newTile.children).forEach((block, i) => {
let property = `--block-angle-${i + 1}`
block.style.setProperty(property, pattern[i])
})
dom.patternList.append($newTile)
})
}</code></pre><p>现在刷新一下页面,可以看到预设图案已经显示到页面左侧了,但是和样品地砖混在一起,所以要调整一下布局,让预设图案以缩略图的形式排列在样品地砖的下方。使预设图案缩小的方法和使地板上地砖缩小的方法一样,都是通过调整 <code>font-size</code> 属性实现的:</p><pre><code class="css">.sample > .tile {
margin-bottom: 4em;
}
.sample .pattern-list {
font-size: 0.2em;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 5em;
}
.sample .pattern-list .tile {
cursor: pointer;
}</code></pre><p>再把第1个预设图案的颜色设置得浅一些,因为这个图案是默认的未经设计的图案,所以让它和其他预设图案有所区别:</p><pre><code class="css">.sample .pattern-list .tile:first-child .block {
color: lightgrey;
}</code></pre><p>现在再刷新一下页面,看到效果如下图:</p><p><img src="/img/bVcTVyI" alt="" title=""></p><p>接下来为预设图案增加点击效果。</p><p>扩充 <code>initEvent()</code> 函数,为 <code>dom.patternList</code> 的子元素绑定点击事件,令点击任意一个预设图案时,把预设图案的角度数据复制到 <code>:root</code> 元素的同名变量中,这样就可以把预设图案应用到样品地砖和地板上:</p><pre><code class="js">function initEvent() {
Array.from(dom.sampleTile.children).forEach((block, i) => {
block.addEventListener('click', () => {
rotateBlock(i + 1)
})
})
Array.from(dom.gridList.children).forEach(button => {
button.addEventListener('click', (e) => {
paveTiles(parseInt(e.target.innerText))
})
})
Array.from(dom.patternList.children).forEach((tile, i) => {
tile.addEventListener('click', () => {
patterns[i].forEach((angle, j) => setBlockAngle(j + 1, angle))
})
})
}</code></pre><p>还要调整一下初始化函数 <code>init()</code> 中语句的执行顺序,先渲染元素、再为元素绑定事件:</p><pre><code class="js">function init() {
initFloor()
initPatternList()
initEvent()
}</code></pre><p>现在刷新一下页面,试试点击预设图案,点击事件已经生效了。</p><p>至此,全部功能开发完成。</p><h3>界面美化</h3><p>最后,美化一下界面。</p><p>先在 dom 中增加一个 <code><h1></code> 元素,写上标题:</p><pre><code class="html"><h1>Tile Pattern Designer</h1>
<main>
<!-- 略 -->
</main></code></pre><p>设置页面整体样式,包括背景色、字体字号、居中对齐:</p><pre><code class="css">body {
margin: 0 auto;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
font-size: 0.75em;
font-family: sans-serif;
background: linear-gradient(to right bottom, lightcyan, lightblue);
}
h1 {
font-weight: normal;
margin: 2em;
letter-spacing: 0.1em;
}</code></pre><p>为样品图案和地板增加一个外边框,强调它们是独立的整体:</p><pre><code class="css">.sample > .tile,
.production .floor {
box-shadow:
0 0 0 9px lightcyan,
0 0 0 10px grey;
}</code></pre><p>效果如下图:</p><p><img src="/img/bVcTVxW" alt="" title=""></p><p>大功告成!</p><h2>关于作者</h2><p>张偶,网络笔名 comehope,20世纪末触网,被 Web 的无穷魅力所俘获,自此始终战斗在 Web 开发第一线。</p><p>《前端每日实战》专栏是我近年来实践项目式学习的笔记,以项目驱动学习,展现从灵感闪现到代码实现的完整过程,亦可作为前端开发的练手习题和开发参考。</p><p><img src="https://segmentfault.com/img/bVbCcXO" alt="" title=""></p><p>拙作《CSS3 艺术》一书已由人民邮电出版社出版,全彩印刷,用100多个生动美观的实例,系统地剖析了 CSS 与视觉效果相关的重要语法,并含有近10小时的视频演示讲解。京东/天猫/当当有售。</p>
《前端每日实战》第177号作品:多张图片的鼠标悬停和滑动特效
https://segmentfault.com/a/1190000037556165
2020-10-21T18:04:36+08:00
2020-10-21T18:04:36+08:00
comehope
https://segmentfault.com/u/comehope
17
<p><img src="/img/bVcHKaa" alt="image" title="image"></p><p>一种引起浏览者探索兴趣的方法是,页面打开之后并不马上把所有内容都呈现给用户,而是隐藏其中的一部分内容,其他内容则需要用户交互之后才展示出来。这种方式很合适那些小众的、要营造艺术氛围的网站,通过特效来展现后续内容,有一种与用户对话的感觉。本作品就是采用这样的方式,当页面加载之后先把图片遮住,然后当鼠标移动到元素之上时,图片才展现出来。</p><h2>效果预览</h2><p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p><p><a href="https://codepen.io/comehope/pen/MWejLqY">https://codepen.io/comehope/pen/MWejLqY</a></p><h2>源代码下载</h2><p>每日前端实战系列的全部源代码请从 github 下载:</p><p><a href="https://link.segmentfault.com/?enc=eTG4lTcn0pBstzL7fjSOsw%3D%3D.mUOEXy2ueoSLk2Ac3cNsfCTaLwpU6euXx91LGFAh%2Fvezpsc6t0iCEgHfJM3JyV%2BOaV2eGkYftmWKd3LlyAICfA%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p><h2>代码解读</h2><h3>一、DOM 结构</h3><p>容器名为 <code>.container</code>,其中包含一个名为 <code>.item</code> 的元素。<br><code>.item</code> 元素则包含 3 个子元素,<code>.picture</code> 表示图片本身,<code>.title</code> 是图片上的文字,<code>.mask</code> 是用来制作遮罩效果的元素。<br>作品完成时,会有多个 <code>.item</code> 元素,但此时我们先只展示 1 张图片,待效果完成之后,再增加其他图片。</p><pre><code class="html"><div class="container">
<div class="item">
<img class="picture" src="images/toggle.png">
<span class="title">Toggle</span>
<div class="mask"></div>
</div>
</div></code></pre><p>本作品用到的4张图片可从下列地址下载。<br><a href="https://link.segmentfault.com/?enc=doo1sZrgpppTaLYRX0CQ%2Bg%3D%3D.QfHnfouRPuGv%2FzJSnlmjbbw84o1FqEAhKz7M0GXVsum1vWqgJIGD29mOUCfjJ46mfd2BLRMwsjgG9FvomHS8ig%3D%3D" rel="nofollow">https://assets.codepen.io/947...</a><br><a href="https://link.segmentfault.com/?enc=%2B2iHTTNKYrWXqdBB33EQPw%3D%3D.ZhHXogoDf6sUZtYZZecby2AZCXgwEBdxyel9hIgr8nqqU1XpDeBaYFllNOGd7LOcbwN4iF6C6q0dpzfVBA8RIQ%3D%3D" rel="nofollow">https://assets.codepen.io/947...</a><br><a href="https://link.segmentfault.com/?enc=A8uB2M3BXT0sHvX01%2By1Ww%3D%3D.qNGDvks5qTZBSffBvu8weXLRKObjEcUHgwdTsP3pDp%2BHCnoEwmsxe%2B1alaYM0VbdtwIVkIOMBQ%2BGoAV57tN4pg%3D%3D" rel="nofollow">https://assets.codepen.io/947...</a><br><a href="https://link.segmentfault.com/?enc=BR0luMH04uXtfl7V2AYnWQ%3D%3D.oS3DHIEzdjYhJ6L9KLikt4qEvWeNA51BZ2uX%2F8j7avxuBMMaNzmImmPjL8X7nlhFTCTDmimRvCYIcfogX52pQQ%3D%3D" rel="nofollow">https://assets.codepen.io/947...</a></p><h3>二、基础布局</h3><p>设置页面背景色为深灰色,令容器居中。</p><pre><code class="css">body {
background-color: #222;
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}</code></pre><p>设置图片尺寸,用相对单位 <code>em</code>。</p><pre><code class="css">.item {
width: 18em;
height: 12em;
}
.item .picture {
width: 100%;
}</code></pre><p>效果如下图:<br><img src="/img/bVcHKai" alt="image" title="image"></p><h3>三、图片遮罩特效</h3><p>因为先处理遮罩效果,所以把暂时用不到的文字隐藏起来,避免干扰。</p><pre><code class="css">.item .title {
display: none;
}</code></pre><p>利用 <code>.mask</code> 元素为图片增加遮罩。遮罩大小是 20em * 20em 的一个大圆,背景色先暂用半透明的醒目的黄色,便于在开发过程中观察。</p><pre><code class="css">.item {
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.item .mask {
position: absolute;
width: 20em;
height: 20em;
background-color: hsla(60, 100%, 50%, 0.7);
border-radius: 50%;
}</code></pre><p>效果如下图:<br><img src="/img/bVcHKao" alt="image" title="image"></p><p>上面只是测试了遮罩的大小,把刚才的代码注释掉,改用 <code>box-shadow</code> 实现我们真正需要的遮罩效果。这个遮罩层尺寸是 <code>50em * 50em</code>,远远大于图片本身,但它的大部分区域是内阴影,在内阴影之内才透出遮罩下方的图片来。<br>内阴影的尺寸是 <code>15em</code>,这是内阴影的半径,所以内阴影的直径是 <code>30em</code>,用遮罩元素的宽高 <code>50em</code> 减去遮罩的 <code>30em</code>,剩下的就是 <code>20em</code>,和刚才测试的遮罩大小是一样的。</p><pre><code class="css">.item .mask {
/*width: 20em;*/
/*height: 20em;*/
/*background-color: hsla(60, 100%, 50%, 0.7);*/
width: 50em;
height: 50em;
color: hsla(60, 100%, 50%, 0.7);
box-shadow: inset 0 0 0 15em;
}</code></pre><p>效果如下图:<br><img src="/img/bVcHKap" alt="image" title="image"></p><p>加上鼠标悬停效果试一下。注意,这里元素上的内阴影尺寸设置为 <code>25em</code>,这是内阴影的半径尺寸,那么阴影的直径就是 <code>50em</code>,和遮罩本身的尺寸是一样大的,这表示在默认情况下,整张图片都被内阴影遮住了;而鼠标悬停时,内阴影变小,就显示出了遮罩下方的图片。另外为遮罩层增加了 <code>pointer-events: none</code> 属性,它的作用是避免遮罩层响应鼠标事件。</p><pre><code class="css">.item .mask {
box-shadow: inset 0 0 0 25em;
transition: box-shadow 0.3s;
pointer-events: none;
}
.item:hover .mask {
box-shadow: inset 0 0 0 15em;
}</code></pre><p>再下来制作鼠标滑动时遮罩跟随的效果。<br>先把遮罩移到图片的左上方。遮罩的高是 <code>50em</code>,<code>top: -25em</code> 就是令遮罩的水平中线与图片顶边对齐;同理,<code>left: -25em</code> 则是令遮罩的垂直中线与图片的左边对齐,两者叠加,就是遮罩的中心与图片的左上角对齐。</p><pre><code class="css">.item .mask {
top: -25em;
left: -25em;
}</code></pre><p>增加脚本,为 <code>.item</code> 元素绑定 <code>mousemove</code> 事件,令鼠标在 <code>.item</code> 元素上滑动时,带动 <code>.mask</code> 元素滑动。</p><pre><code class="js">window.onload = init
function init() {
let items = document.querySelectorAll('.item')
items.forEach((item) => {
item.addEventListener('mousemove', e => {
let mask = item.querySelector('.mask')
mask.style.transform = 'translate(' + e.offsetX + 'px, ' + e.offsetY + 'px)'
})
})
}</code></pre><p>至此,主要的效果已经完成了,接下来再增强一下效果。<br>稍加大图片的原始尺寸,在鼠标悬停时恢复图片大小,这样的效果是在鼠标进入图片区域时,图片能“扭曲抖动”一下,加强互动的效果。</p><pre><code class="css">.item .picture {
transform: scale(1.1);
transition: 0.3s;
}
.item:hover .picture {
transform: scale(1);
}</code></pre><p>鼠标悬停和滑动效果完成,下面这几行代码是一些收尾工作。<br>通过 <code>overflow: hidden</code> 属性隐藏掉图片之外的部分、容器加一点圆角、遮罩的颜色改用不透明的灰色。</p><pre><code class="css">.container {
border-radius: 0.3em;
}
.item {
overflow: hidden;
}
.item .mask {
/*color: hsla(60, 100%, 50%, 0.7);*/
color: #333;
}</code></pre><p>效果如下图:<br><img src="/img/bVcHKeE" alt="image" title="image"></p><h3>四、文字布局和特效</h3><p>接下来处理文字。<br>先把文字显示出来,除了注释掉 <code>display: none</code> 之外,还要设置它的 <code>z-index</code>,令它显示在遮罩层的上方,再有也要取消它的鼠标事件,防止它影响鼠标滑动效果。</p><pre><code class="css">.item .title {
/*display: none;*/
position: absolute;
color: #777;
z-index: 1;
pointer-events: none;
}</code></pre><p>设置文字样式。</p><pre><code class="css">.item .title {
font-family: sans-serif;
font-weight: bold;
text-transform: uppercase;
}</code></pre><p>增加文字特效,当鼠标滑入图片时,隐藏文字。</p><pre><code class="css">.item .title {
transition: 0.2s;
}
.item:hover .title {
opacity: 0;
}</code></pre><p>效果如下图:<br><img src="/img/bVcHKeF" alt="image" title="image"></p><p>至此,单图图片的效果都完成了。</p><h3>五、将特效应用到多张图片</h3><p>增加多个 <code>.item</code> 元素。</p><pre><code class="html"><div class="container">
<div class="item">
<img class="picture" src="images/toggle.png">
<span class="title">Toggle</span>
<div class="mask"></div>
</div>
<!-- 此处再增加3个 .item 元素,代码略 -->
</div></code></pre><p>用 <code>grid</code> 布局把图片排列成田字格形状。</p><pre><code class="css">.container {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-gap: 1em;
}</code></pre><p>效果如下图:<br><img src="/img/bVcHKaa" alt="image" title="image"></p><p>大功告成!</p><h2>关于作者</h2><p>张偶,网络笔名 @comehope,20世纪末触网,被 Web 的无穷魅力所俘获,自此始终战斗在 Web 开发第一线。</p><p>《前端每日实战》专栏是我近年来实践项目式学习的笔记,以项目驱动学习,展现从灵感闪现到代码实现的完整过程,亦可作为前端开发的练手习题和开发参考。</p><p><img src="https://segmentfault.com/img/bVbCcXO" alt="" title=""></p><p>拙作《CSS3 艺术》一书已由人民邮电出版社出版,全彩印刷,用100多个生动美观的实例,系统地剖析了 CSS 与视觉效果相关的重要语法,并含有近10小时的视频演示讲解。京东/天猫/当当有售。</p>
《前端每日实战》第176号作品:用透视图表现 HTML、CSS 和 JS 的关系
https://segmentfault.com/a/1190000037479866
2020-10-15T09:51:54+08:00
2020-10-15T09:51:54+08:00
comehope
https://segmentfault.com/u/comehope
3
<p>(图片审核原因,所以请到我的微博中查看开发过程中的7张截图:<a href="https://link.segmentfault.com/?enc=r%2FrGUDpFvreWS1x5oxRuUw%3D%3D.WIyOax84go5FLM43WlgBDxPI8jsTXscRlVpZVbNvZGuELX70QYhdOxq6cu902ir6" rel="nofollow">https://weibo.com/6063054508/...</a>)</p><p>今年年初时我偶然看到了下面这张图片,<a href="https://link.segmentfault.com/?enc=H8l3YfcLUGTeOkPUXuKo%2Bw%3D%3D.zeOHL9cJRv%2BXfmdx9K4mGWdRfLLm7mU9tBIJUSYYXngtpXixG5kxRSWzZB0HHmys" rel="nofollow">顺手把它记在了微博里,</a>近日抽时间把它们制作成了交互页面,本文记录了开发的过程。</p><h2>效果预览</h2><p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p><p><a href="https://codepen.io/comehope/pen/GRqpLGX">https://codepen.io/comehope/pen/GRqpLGX</a></p><h2>源代码下载</h2><p>每日前端实战系列的全部源代码请从 github 下载:</p><p><a href="https://link.segmentfault.com/?enc=3svgvtzTm9DsmDTgeE8S1Q%3D%3D.YrVWmty%2F%2FVrhwdOii5qz6zeMf2%2BSf5XaLFZKrGiCdR4xB7SZRJpEMlEN0XfHQiCXEOX8GEO74nPfT4A7Gj0wjA%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p><h2>代码解读</h2><h3>一、准备素材</h3><p>先准备好开发用的3张图片,文件名分别为 <code>html.png</code>、<code>css.png</code>、<code>js.png</code>,html 类比于人的骨架,css 类比于人的外貌,js 类比于人的神经系统:<br><a href="https://link.segmentfault.com/?enc=L8ZkVeo8SxUM7bns9zrwLQ%3D%3D.D%2FQTMBr%2BoHckGL1RgDEcvvb0JmEMhCcrYvYC58vjcviaDsdWMipWBmIgn1va%2Bwc9" rel="nofollow">https://assets.codepen.io/947795/html.png</a><br><a href="https://link.segmentfault.com/?enc=D45t056V0965mnNeCHaPcA%3D%3D.0bE87WUhw1fo4iY4s%2B8%2Bu7btTPz2rN8tMHK8fQUfZv2cWVJGhf7Yt1OxpvMBoJxz" rel="nofollow">https://assets.codepen.io/947795/css.png</a><br><a href="https://link.segmentfault.com/?enc=J2N%2BKvYzzMlxhRKxp3Ze9Q%3D%3D.rIzvRM0fAQl%2BzF9xZfrYcw8pV2%2BQ9W7%2FOkK7CPbav0dcoBV%2BiwouaFphOJpQGWdr" rel="nofollow">https://assets.codepen.io/947795/js.png</a></p><h3>二、DOM 结构</h3><p>容器名为 <code>.person</code>,表示一个人的外貌,它包含一个子元素 <code>.inside</code>,表示这个人被透视出的身体内部,就是骨骼、血管什么的啦。</p><pre><code class="html"><div class="person">
<div class="inside"></div>
</div></code></pre><h3>三、呈现人的正常状态</h3><p>令元素在页面居中显示:</p><pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}</code></pre><p>把图片 <code>css.png</code> 作为容器 <code>.person</code> 的背景图:</p><pre><code class="css">.person {
width: 320px;
height: 537px;
background-image: url(css.png);
}</code></pre><p>效果如下图:<br><图1></p><h3>四、呈现人的骨骼</h3><p>把图片 <code>html.png</code> 作为元素 <code>.person .inside</code> 的背景图。</p><pre><code class="css">.person .inside {
width: 100%;
height: 100%;
background-image: url(html.png);
background-position: center;
}</code></pre><p>效果如下图:<br><图2></p><p>为图片加上 <code>5px</code> 的灰色边框。</p><pre><code class="css">.person .inside {
box-sizing: border-box;
border: 5px solid gray;
border-radius: 0.3em;
}</code></pre><p>效果如下图:<br><图3></p><p>缩小边框的高度为 <code>100px</code>,并定位在容器的中上位置,这是未来上下滑动的透视窗。</p><pre><code class="css">.person {
position: relative;
}
.person .inside {
/*height: 100%;*/
height: 100px;
position: absolute;
top: 25%;
}</code></pre><p>效果如下图:<br><图4></p><p>仔细看上图,是有错误的,透视窗处于人的胸部,但透视出的区域是人的腰部。所以需要用 <code>background-attachment: fixed;</code> 把图片锁定在固定位置。请留意,这个属性值是整个作品的核心,否则不会有透视窗滑动的效果。</p><pre><code class="css">.person .inside {
background-attachment: fixed;
}</code></pre><p>效果如下图:<br><图5></p><p>至此,静态布局完成。</p><h3>五、透视窗滑动效果</h3><p>接下来处理透视窗跟随鼠标的滑动效果。</p><p>程序入口是 <code>init</code> 函数,在其中声明2个变量 <code>person</code> 和 <code>inside</code>,分别代表它们对应的 DOM 元素。</p><pre><code class="js">window.onload = init
function init() {
var person = document.querySelector('.person')
var inside = document.querySelector('.person .inside')
}</code></pre><p>为 <code>person</code> 元素绑定 <code>mousemove</code> 事件,当事件被触发时,更新 <code>inside</code> 元素的 <code>top</code> 属性值,形成滑动效果。</p><pre><code class="js">function init() {
person.addEventListener('mousemove', function (e) {
inside.style.top = getTopPosition(e.offsetY)
});
}</code></pre><p><code>top</code> 的属性值通过 <code>getTopPosition()</code> 函数计算得来,其中 <code>offset</code> 值为透视窗高度的一半,意为透视窗在滑动时,鼠标指针位于透视窗的中间位置。在检测了滑动的位置没有跑到容器外面之后,返回位置,这里要加上单位 <code>px</code>。</p><pre><code class="js">function getTopPosition(y) {
const windowHeight = 100
var offset = windowHeight / 2
if(y < offset) return
if(y > (Number.parseInt(window.getComputedStyle(person).height)) - offset) return
return y - offset + 'px'
}</code></pre><p>现在刷新页面,看到已经有滑动效果了,但是非常不流畅,这是因为子元素 <code>.inside</code> 位于容器 <code>.person</code> 之上,会先触发子元素的事件,为此,我们用 <code>pointer-events: none;</code> 禁止子元素响应鼠标事件。<br>另外,把鼠标指针显示为表示可移动的图标。</p><pre><code class="css">.person {
cursor: move;
}
.person .inside {
pointer-events: none;
}</code></pre><p>至此,本作品的核心功能开发完成了。</p><h3>六、切换透视图</h3><p>接下来增加切换透视内容的功能。</p><p>在 DOM 中增加 2 个复选框,用于选择是透视 html(人体骨骼),还是透视 js(人体神经)。</p><pre><code class="html"><div class="selector">
<input type="radio" name="options" value="html" id="html" checked>
<label for="html">HTML</label>
<input type="radio" name="options" value="js" id="js">
<label for="js">JavaScript</label>
</div></code></pre><p>定义复选框的样式,这不是本作品的重点,用的也是普通的属性,就不作详细讲解了。</p><pre><code class="css">.selector {
position: absolute;
width: 9em;
top: calc(50% - 8em / 2);
left: 3em;
display: grid;
grid-template-columns: repeat(2, 1fr);
}
.selector input {
width: 1.2em;
height: 1.2em;
}
.selector label {
font-size: 1.5em;
font-family: sans-serif;
cursor: pointer;
}
.selector input:checked + label {
color: dodgerblue;
}</code></pre><p>效果如下图:<br><图6></p><p>最后,为复选框绑定点击事件,当点击复选框时,更新 <code>.inside</code> 元素的背景图片。</p><pre><code class="js">function init() {
var options = document.getElementsByName('options')
options.forEach((option) => {
option.addEventListener('click', (e) => {
inside.style.backgroundImage = getImageUrl(e.target.value)
})
})
function getImageUrl(opt) {
return 'url(' + opt + '.png' + ')'
}
}</code></pre><p>下面是透视神经图的效果:<br><图7></p><p>大功告成!</p><h2>关于作者</h2><p>张偶,网络笔名 @comehope,20世纪末触网,被 Web 的无穷魅力所俘获,自此始终战斗在 Web 开发第一线。</p><p>《前端每日实战》专栏是我近年来实践项目式学习的笔记,以项目驱动学习,展现从灵感闪现到代码实现的完整过程,亦可作为前端开发的练手习题和开发参考。</p><p><img src="https://segmentfault.com/img/bVbCcXO" alt="" title=""></p><p>拙作《CSS3 艺术》一书已由人民邮电出版社出版,全彩印刷,用100多个生动美观的实例,系统地剖析了 CSS 与视觉效果相关的重要语法,并含有近10小时的视频演示讲解。京东/天猫/当当有售。</p>
《前端每日实战》第175号作品:纯CSS绘制的阴阳图案
https://segmentfault.com/a/1190000037425052
2020-10-10T16:24:45+08:00
2020-10-10T16:24:45+08:00
comehope
https://segmentfault.com/u/comehope
19
<p><img src="/img/bVcHb6o" alt="image" title="image"></p><p>阴阳是一个简朴而博大的中国古代哲学概念,有无相生,难易相成,长短相形,高下相倾,这是我们伟大祖先发明的二进制,所以诸位程序员更要多加学习领会。</p><h2>效果预览</h2><p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p><p><a href="https://codepen.io/comehope/pen/RwRNdLj">https://codepen.io/comehope/pen/RwRNdLj</a></p><h2>源代码下载</h2><p>每日前端实战系列的全部源代码请从 github 下载:</p><p><a href="https://link.segmentfault.com/?enc=zGOK7pf2qfIftDUt2R07KA%3D%3D.UAjAYv7ONgjBtp9m6qjakgb%2F62byhPWNPrWSLy9%2FHbdp0XGJBGrSQ1%2BlxTLzVz8Pcb%2FN6R4l8BhNUNMTu6DhSw%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p><h2>代码解读</h2><h3>一、定义 DOM 结构</h3><p>这是一个单元素作品,DOM 结构很简单,只有一个类名为 <code>yin-yang</code> 的 <code>div</code> 元素。</p><pre><code class="html"><div class="yin-yang"></div></code></pre><h3>二、定义容器</h3><p>把页面背景设置为浅灰色。</p><pre><code class="css">body {
margin: 0;
height: 100vh;
background-color: lightgrey;
}</code></pre><p>定义容器尺寸为正方形,采用相对单位 <code>em</code>,为了能看到容器占据的空间,暂时为容器填充白色。</p><pre><code class="css">body {
display: flex;
align-items: center;
justify-content: center;
}
.yin-yang {
width: 10em;
height: 10em;
font-size: 20px;
background-color: white;
}</code></pre><p>效果如下图:<br><img src="/img/bVcHb6x" alt="image" title="image"></p><h3>三、阴阳图案构图</h3><p>注释掉刚才临时设置的 <code>background-color</code> 属性,用锐利的线性渐变设置容器背景色,把容器背景用2种颜色平分。因为后面还会用到这2种颜色,所以用变量 <code>--color1</code> 和 <code>--color2</code> 来代表它们,便于复用。阴阳图案本来是黑白色的,在开发阶段为了便于观察,我们暂时用番茄色和橙色来代替。</p><pre><code class="css">.yin-yang {
/*background-color: white;*/
--color1: tomato;
--color2: orange;
background-image: linear-gradient(var(--color1) 50%, var(--color2) 50%);
}</code></pre><p>效果如下图:<br><img src="/img/bVcHb6E" alt="image" title="image"></p><p>接下来绘制中间的图案,先绘制左边的一个,用伪元素实现,它的背景色为变量 <code>--color1</code> 的颜色,通过 flex 布局令其垂直居中。<code>mix-blend-mode</code> 属性的作用是不让伪元素的颜色因和背景色完全一样而混在一起无法区分,这称为“混合模式”,有兴趣的同学可以参考 <a href="https://link.segmentfault.com/?enc=Qf0qcrvlCsEgHvm35j59Mg%3D%3D.OiSaR7Zdv7XNgsgiHV3maR3zyAWl5kGmQByDFwga10SJa1cfjvErvNx5reYXy%2FQzhjOePVtmCVz7RemC2dPLyA%3D%3D" rel="nofollow">MDN 文档</a>并亲手实验一下各种模式值的效果。</p><pre><code class="css">.yin-yang {
display: flex;
align-items: center;
}
.yin-yang::before {
content: '';
width: 50%;
height: 50%;
background-color: var(--color1);
mix-blend-mode: screen;
}</code></pre><p>效果如下图:<br><img src="/img/bVcHb6K" alt="image" title="image"></p><p>为伪元素设置一个厚厚的边框,边框用变量 <code>--color2</code> 的颜色。</p><pre><code class="css">.yin-yang::before {
box-sizing: border-box;
border: 1.5em solid var(--color2);
}</code></pre><p>效果如下图:<br><img src="/img/bVcHb6M" alt="image" title="image"></p><p>现在看着都是正方形元素,一会儿再变圆,如果着急,可以加上 <code>border-radius: 50%;</code> 属性看看效果。<br>接下来把 <code>::before</code> 伪元素的效果复制到 <code>::after</code> 伪元素上,直接在伪元素选择器里加上 <code>.yin-yang::after</code> 就可以了。<br>2个伪元素的区别仅在于它们的配色是相反的,所以我们把它们的配色属性值分别定义成 <code>--inner-color</code> 和 <code>--outer-color</code>,分别是指元素内部和外边框的颜色。</p><pre><code class="css">.yin-yang::before,
.yin-yang::after {
background-color: var(--inner-color);
border: 1.5em solid var(--outer-color);
}
.yin-yang::before {
--inner-color: var(--color1);
--outer-color: var(--color2);
}
.yin-yang::after {
--inner-color: var(--color2);
--outer-color: var(--color1);
}</code></pre><p>效果如下图:<br><img src="/img/bVcHb6N" alt="image" title="image"></p><h3>四、撤销辅助属性、上色</h3><p>好了,现在把它们都变为圆形。</p><pre><code class="css">.yin-yang,
.yin-yang::before,
.yin-yang::after {
border-radius: 50%;
}</code></pre><p>效果如下图:<br><img src="/img/bVcHb6R" alt="image" title="image"></p><p>取消掉用于辅助开发的色彩混合模式属性 <code>mix-blend-mode</code>。</p><pre><code class="css">.yin-yang::before,
.yin-yang::after {
/*mix-blend-mode: screen;*/
}</code></pre><p>效果如下图:<br><img src="/img/bVcHb60" alt="image" title="image"></p><p>再把配色改成黑白的。至此,阴阳图的静态布局完成。</p><pre><code class="css">.yin-yang {
/*--color1: tomato;
--color2: orange;*/
--color1: white;
--color2: black;
box-shadow: 0 0 1em rgba(0, 0, 0, 0.3);
}</code></pre><p>效果如下图:<br><img src="/img/bVcHb6o" alt="image" title="image"></p><h3>五、增加动画效果</h3><p>最后,加上旋转动画,以喻阴阳此消彼长相互转化动态平衡绵延无尽之意。</p><pre><code class="css">.yin-yang {
animation: rotating linear 5s infinite;
}
@keyframes rotating {
to {
transform: rotate(1turn);
}
}</code></pre><p>大功告成!</p><h2>关于作者</h2><p>张偶,网络笔名 @comehope,20世纪末触网,被 Web 的无穷魅力所俘获,自此始终战斗在 Web 开发第一线。</p><p>《前端每日实战》专栏是我近年来实践项目式学习的笔记,以项目驱动学习,展现从灵感闪现到代码实现的完整过程,亦可作为前端开发的练手习题和开发参考。</p><p><img src="https://segmentfault.com/img/bVbCcXO" alt="" title=""></p><p>拙作《CSS3 艺术》一书于2020年1月由人民邮电出版社出版,全彩印刷,使用100多个生动美观的实例,系统地剖析了 CSS 与视觉效果相关的重要语法,并含有近10小时的视频演示讲解。京东/天猫/当当有售。</p>
《前端每日实战》第174号作品:日历
https://segmentfault.com/a/1190000030693460
2020-10-09T12:04:04+08:00
2020-10-09T12:04:04+08:00
comehope
https://segmentfault.com/u/comehope
28
<p><img src="/img/bVceWSC" alt="image" title="image"></p><p>中秋国庆长假休完,又要投入到工作中了,做一个日历纪念一下吧。</p><h2>效果预览</h2><p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p><p><a href="https://codepen.io/comehope/pen/mdEyWEv">https://codepen.io/comehope/pen/mdEyWEv</a></p><h2>源代码下载</h2><p>每日前端实战系列的全部源代码请从 github 下载:</p><p><a href="https://link.segmentfault.com/?enc=Bvm8lUbDde2MQ2p5pWSPYw%3D%3D.OObLyv%2B%2BOsjIdcGfgbIt5mzqfvDE5F4u4ID%2FzVw6ALE9Vy6AvGGpYI9sBVTP5ronSS6jkXVSJzSe3HuVGZuV0g%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p><h2>代码解读</h2><p>这个日历的开发流程是,定义 DOM 结构之后,进行页面整体布局,绘制出日历薄的模样,然后分别布局上部的当前日期和下部的日期表格。完成静态布局之后,再通过脚本来动态生成日期元素,实现一个显示实时日期的动态日历。</p><h3>一、定义 DOM 结构</h3><p>dom 的整体结构是一个名为 <code>.container</code> 的容器中包含2个元素,<code>.today</code> 是当前日期,<code>.calendar</code> 是日期表格。</p><pre><code class="html"><div class="container">
<header class="today">
</header>
<main class="calendar">
</main>
</div></code></pre><p><code>.today</code> 当前日期部分包含2个元素,<code>.month</code> 显示当前月,<code>.day</code>显示当前日。</p><pre><code class="html"><header class="today">
<div class="day">9</div>
<div class="month">October</div>
</header></code></pre><p><code>.calcendar</code> 日期表格部分包含一个表头行 <code>.days</code> 和一个表格 <code>.dates</code>。表头按英文习惯以周日为每周的第一天;表格共6行,一共显示42天,可以适应任何月份。<br>表格中的日期通过类名区分为上月日期 <code>previous-month</code>、当前日期 <code>current-day</code>、下月日期 <code>next-month</code>。</p><pre><code class="html"><main class="calendar">
<div class="days">
<span>Sun</span>
<span>Mon</span>
<span>Tue</span>
<span>Wed</span>
<span>Thu</span>
<span>Fri</span>
<span>Sat</span>
</div>
<div class="dates">
<span class="previous-month">27</span>
<span class="previous-month">28</span>
<span class="previous-month">29</span>
<span class="previous-month">30</span>
<span>1</span>
<span>2</span>
<span>3</span>
<span>4</span>
<span>5</span>
<span>6</span>
<span>7</span>
<span>8</span>
<span class="current-day">9</span>
<span>10</span>
<span>11</span>
<span>12</span>
<span>13</span>
<span>14</span>
<span>15</span>
<span>16</span>
<span>17</span>
<span>18</span>
<span>19</span>
<span>20</span>
<span>21</span>
<span>22</span>
<span>23</span>
<span>24</span>
<span>25</span>
<span>26</span>
<span>27</span>
<span>28</span>
<span>29</span>
<span>30</span>
<span>31</span>
<span class="next-month">1</span>
<span class="next-month">2</span>
<span class="next-month">3</span>
<span class="next-month">4</span>
<span class="next-month">5</span>
<span class="next-month">6</span>
<span class="next-month">7</span>
</div>
</main></code></pre><h3>二、页面整体和日历容器布局</h3><p>先用线性渐变设置页面背景色为灰白过渡色。</p><pre><code class="css">body {
margin: 0;
height: 100vh;
background-image: linear-gradient(to bottom, #eee, #ccc);
}</code></pre><p>设置容器尺寸,用相对单位 em,并使容器居于页面正中。<br>为使容器可见,暂为容器填充白色背景。</p><pre><code class="css">body {
display: flex;
align-items: center;
justify-content: center;
}
.container {
width: 32em;
height: 38em;
font-size: 14px;
background-color: white;
}</code></pre><p>效果如下图:<br><img src="/img/bVceWSF" alt="image" title="image"></p><p>注释掉刚才临时定义的 <code>background-color</code> 属性,改为用锐利渐变填充,实现上部黄棕色、下部白色的效果。因为黄棕色 <code>sandybrown</code> 是日历主色,后面还会用到,所以把它定义成变量。<br>再把日历四周设为圆角,底部加双层阴影,模拟多张日历纸叠加的效果。</p><pre><code class="css">.container {
/* background-color: white; */
--main-color: sandybrown;
background-image: linear-gradient(to bottom, var(--main-color) 50%, white 50%);
border-radius: 1em;
box-shadow:
0 2px 1px rgba(0, 0, 0, 0.2),
0 3px 1px white;
}</code></pre><p>效果如下图:<br><img src="/img/bVceWSG" alt="image" title="image"></p><p>接下来绘制一个细节:环扣,它用来连接日历的上、下两部分。使用2个伪元素来绘制,这样不用显式地增加 DOM 元素,纯用 CSS 实现装饰效果。两个环扣的样式相同,所以大部分代码是共享的,仅它们所处的位置不同,一个在日历左侧,一个在日历右侧。</p><pre><code class="css">.container {
position: relative;
}
.container::before,
.container::after {
content: '';
position: absolute;
width: 0.6em;
height: 2.3em;
background-color: white;
top: calc(50% - 2.3em / 2);
border-radius: 0.3em;
box-shadow:
0 3px 1px rgba(0, 0, 0, 0.3),
0 -1px 1px rgba(0, 0, 0, 0.2);
}
.container::before {left: 2em;}
.container::after {right: 2em;}</code></pre><p>效果如下图:<br><img src="/img/bVceWSJ" alt="image" title="image"></p><p>至此,一个接近真实场景中的日历的轮廓绘制完成了,接下来布局日历上显示的文字内容。</p><h3>三、上部当前日期布局</h3><p>因为整个日历分成上下两部分,所以先令当前日期 <code>.today</code> 占据上部的50%空间,这样表格 <code>.calendar</code> 自然就被挤到下部了。</p><pre><code class="css">.today {
height: 50%;
}</code></pre><p>效果如下图:<br><img src="/img/bVceWSL" alt="image" title="image"></p><p>因为整个日历都使用同一种字体,所以把字体样式定义在容器中,采用无衬线字体。<br><code>.today</code> 的布局很简单,用的都是字号、颜色、行间距等基本属性。</p><pre><code class="css">.container {
font-family: sans-serif;
}
.today {
padding: 3em;
box-sizing: border-box;
color: white;
}
.today .day {
font-size: 8em;
line-height: 1em;
font-weight: bold;
}
.today .month {
font-size: 4em;
line-height: 1em;
text-transform: lowercase;
}</code></pre><p>效果如下图:<br><img src="/img/bVceWSN" alt="image" title="image"></p><h3>四、下部日期表格布局</h3><p>表格包括表头和表体两部分,设置好它们的宽度,然后用 flex 布局令其垂直居中排列。</p><pre><code class="css">.calendar {
padding-top: 3.5em;
display: flex;
flex-direction: column;
align-items: center;
}
.calendar .days,
.calendar .dates {
width: 28em;
}</code></pre><p>表头和表格都是一行7列,这里采用 grid 布局实现。</p><pre><code class="css">.calendar .days,
.calendar .dates {
display: grid;
grid-template-columns: repeat(7, 1fr);
line-height: 2em;
text-align: center;
}</code></pre><p>效果如下图:<br><img src="/img/bVceWSO" alt="image" title="image"></p><p>表格已经成形,剩下的细节是为文字上色。<br>表格里一共有5种语义元素:表头、当前日期、本月日期、上月日期、下月日期,这些不同语义的元素都靠 CSS 类名来区分。表头和当前日期用主色,本月日期用深灰色,上月日期和下月日期用浅灰色。</p><pre><code class="css">.calendar .days {
color: var(--main-color);
font-weight: bold;
text-transform: uppercase;
}
.calendar .dates {
color: dimgray;
}
.calendar .dates .previous-month,
.calendar .dates .next-month {
color: lightgray;
}
.calendar .dates .current-day {
color: var(--main-color);
font-weight: bold;
}</code></pre><p>效果如下图:<br><img src="/img/bVceWSQ" alt="image" title="image"></p><p>最后,再增加一个鼠标悬停特效,当在日期上悬停时令背景变灰、文字变白,并用 <code>transition</code> 实现平滑过渡。</p><pre><code class="css">.calendar .dates span:hover {
background-color: lightgray;
color: white;
}
.calendar .dates span {
transition: 0.3s;
}</code></pre><p>至此,整个日历的静态布局全部完成了。</p><h3>五、动态脚本</h3><p>程序的入口是一个名为 <code>init</code> 的函数,其中声明了一个 <code>Calendar</code> 类的实例,再调用它的 <code>render()</code> 方法来渲染页面。</p><pre><code class="js">function init() {
let calendar = new Calendar(new Date())
calendar.render()
}
window.onload = init</code></pre><p><code>Calendar</code> 类接收一个日期参数,以此来初始化年、月、日数据。<code>render()</code> 方法分别调用了 <code>renderDay()</code> 和 <code>renderMonth()</code> 来渲染当前日期和当前月份。因为 <code>Date</code> 对象返回的月份属性是数字,为了把它显示成英文月份名称,就定义了一个存放月份名称的数组。</p><pre><code class="js">let Calendar = function(date) {
let year = date.getFullYear()
let month = date.getMonth()
let day = date.getDate()
function renderDay() {
document.querySelector('.day').textContent = day
}
function renderMonth() {
const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
document.querySelector('.month').textContent = MONTHS[month]
}
this.render = function() {
renderDay()
renderMonth()
}
}</code></pre><p>接下来要渲染日期表格了。<br>我们先引入一个日历库 calendar-dates(github 地址:<a href="https://link.segmentfault.com/?enc=7FtW5ickD7Ec8bNU2hfp1A%3D%3D.StXtXzgwnSSLLXNO5ztuAt5hi5lv3IlKpMbCfrUzMqZpldAq6sY7wjeKj4nwHOHw" rel="nofollow">https://dance2die.github.io/calendar-dates/</a>),它负责计算日期、星期、月份之间的关系,为给定的年月输出对应的日历数据。通过 <code>import</code> 语句导入该库文件。</p><pre><code class="js">import CalendarDates from 'https://cdn.jsdelivr.net/npm/calendar-dates@2.6.1/dist/calendardates.esm.js'</code></pre><p><img src="/img/bVceWSV" alt="image" title="image"></p><p>注意,对于使用了 <code>import</code> 语句的脚本,在 <code><script></code> 标签中需增加 <code>type="module"</code> 属性。</p><pre><code class="html"><script src="script.js" type="module"></script></code></pre><p>然后,在 <code>Calendar</code> 类中定义一个 <code>renderDates()</code> 函数来渲染日历列表。如何获得日历可以参考官方文档,这里就不多说了,我觉得有点别扭的是必须用 async/await 的方式来调用。每个日期有 <code>date</code> 和 <code>type</code> 属性,<code>date</code> 就是日期数值,<code>type</code> 有3个值:<code>previous</code>、<code>current</code>、<code>next</code>,分别代表上月、本月、下月,我们就用这2个属性来处理日期元素的样式。<br>最后,别忘了要在 <code>render()</code> 里调用一下 <code>renderDates()</code> 函数。</p><pre><code class="js">async function renderDates() {
const calendarDates = new CalendarDates();
const domList = document.querySelector('.dates')
domList.innerHTML = ''
for (const meta of await calendarDates.getDates(new Date(year, month))) {
let span = document.createElement('span')
span.textContent = meta.date
span.className = (meta.type == 'current')
? (meta.date == day)
? 'current-day'
: ''
: meta.type + '-month'
domList.append(span)
}
}
this.render = function() {
renderDay()
renderMonth()
renderDates()
}</code></pre><p>大功告成!</p><h2>关于我</h2><p>张偶,网络笔名 @comehope,20世纪末触网,被 Web 的无穷魅力所俘获,自此始终战斗在 Web 开发第一线。</p><p>《前端每日实战》专栏是我近年来实践项目式学习方法的笔记,以项目驱动学习,展现从灵感闪现到代码实现的完整过程,亦可作为前端开发的练手习题和开发参考。</p><p><img src="https://segmentfault.com/img/bVbCcXO" alt="" title=""></p><p>拙作《CSS3 艺术》一书于2020年1月由人民邮电出版社出版,全彩印刷,使用100多个生动美观的实例,系统地剖析了 CSS 与视觉效果相关的重要语法,并含有近10小时的视频演示讲解。京东/天猫/当当有售。</p>
犀牛书《JavaScript 权威指南(第7版)》先睹为快!
https://segmentfault.com/a/1190000023651752
2020-08-17T09:52:42+08:00
2020-08-17T09:52:42+08:00
comehope
https://segmentfault.com/u/comehope
11
<p><img src="/img/bVbLo3U" alt="image" title="image"></p><p>犀牛书第7版(JavaScript: The Definitive Guide, 7th Edition)已经在3个月前(2020年5月)出版上市。</p><p>第6版是2011年出版的,距今已经9年,大约从那时起,前端岗开始成为一个独立的岗位。</p><p>第7版中增加了 ES6 语法、新的 Web API、Node、流行工具库如 Babel 等内容,令人期待。目前这本书还没有在国内出版,那我们就先通过英文版目录望梅止渴吧。</p><p>1). Introduction to JavaScript<br>第1章,概述。</p><p>2). Lexical Structure<br>第2章,词法结构,与第6版基本相同。<br>把对 unicode 转义的内容扩充为一个独立小节。</p><p>3). Types, Values, and Variables<br>第3章,类型、值和变量,与第6版基本相同。<br>增加了 Symbol 数据类型。</p><p>4). Expressions and Operators<br>第4章,表达式和运算符,与第6版基本相同。<br>增加了双引号(??)和 await 运算符。</p><p>5). Statements<br>第5章,语句,与第6版基本相同。<br>增加了 yield, const, let, import, export 的内容。</p><p>6). Objects<br>第6章,对象,与第6版基本相同。<br>增加了扩展运算符(...)的内容。</p><p>7). Arrays<br>第7章,数组,与第6版基本相同。<br>增加了 Array.from()、flat()、flatMap()、copyWithin() 的内容。</p><p>8). Fucntions<br>第8章,函数,与第6版基本相同。<br>增加了箭头函数、参数缺省值、rest 参数的内容。</p><p>9). Classes<br>第9章,类,第6版的“第9章-类和模块”被拆成了2章分别讲解。<br>增加了 class 关键字及相关的内容。</p><p>10). Modules<br>第10章,模块。<br>在第6版时还没有内建的模块语法,所以在第6版第9章用一个小节讲到了模块。第7版进行了大幅扩充,分别讲解了 Node 下的模块和 ES6 的模块。</p><p>11). The JavaScript Standard Library<br>第11章,JavaScript 标准库,这一章是全新的。<br>前面10章讲解的是 JavaScript 语言核心,这一章讲解语言集成的库和 API。内容包括 Set、Map、ArrayBuffer、正则匹配、日期时间类、Error 类、JSON 类、国际化 API、console API、URL API、计时器。<br>第6版“第10章-正则表达式的模式匹配”的内容成为了本章的一个小节。</p><p>12). Iterators and Generators<br>第12章,迭代器和生成器,这一章是全新的。</p><p>13). Asynchronous JavaScript<br>第13章,异步 JavaScript,这一章是全新的。<br>内容包括 callback 模式、Promise、async 和 await 等内容。</p><p>14). Metaprogramming<br>第14章,元编程,这一章是全新的。<br>内容包括 Proxy、Reflect 对象。</p><p>15). JavaScript in Web Browsers<br>第15章,Web 浏览器中的 JavaScript。<br>这可能是全书最长的一章,它涵盖了第6版几乎整个“第二部分-客户端 JavaScript”的全部内容,包括第6版的“第13章-Web浏览器中的JavaScript”、“第14章-Window对象”、“第15章-脚本化文档”、“第16章-脚本化CSS”、“第17章-事件处理”、“第18章-脚本化 HTTP”、“第20章-客户端存储”、“第21章-多媒体与图形编程”、“第22章 HTML5 API”。<br>除了这些,还增加了 Web 组件、Worker 的内容,最后还有一个在页面上绘制曼德博集合的实例。</p><p>16). Server-Side JavaScript with Node<br>第16章,基于 Node 的服务端 JavaScript,第6版的“第12章-服务器端JavaSript”中有一节讲到 Node,第7版扩充为一章。</p><p>17). JavaScript Tools and Extensions<br>第17章,JavaScript 工具和扩展,这一章是全新的。<br>讲解一些工程化工具,如 ESLint、Jest、npm、Babel、Flow。</p><p>总体上,第7版全书的结构是:</p><ol><li>前8章讲 JavaScript 的传统核心部分,与第6版基本相同。</li><li>第9章至第14章讲 ES6 新语法。</li><li>第15章至17章讲 JavaScript 主要的应用场景:浏览器和基于 Node 的服务端开发,最后涉及 JavaScript 生态和工程化,介绍了一些重要的流行类库。</li></ol><p>第6版全书整体分成二部分,第一部分是语言核心,第二部分是浏览器内开发,但是第7版把所有与浏览器相关的若干章节合并为一章,全书更注重 JavaScript 语言本身。</p><p>第6版中过时的内容都被删除了,比如 EX4、Rhino、JSONP、XMLHttpRequest、关于 IE 兼容性的讨论。</p><p>第6版足足300页的语言参考和客户端参考在第7版中被删除掉了。经过最近 10 年的发展,JavaScript、HTML、DOM、Web API 都变得比以前要丰富多了,不可能在一本书中再把这些参考全面列出来了,如果需要参考的话,可以到 MDN 上去看。</p><p>最后,盼望中文版或影印版尽快出版,让我们能够在临睡前手捧经典读上一段,在不知不觉中进入甜甜的梦乡……</p>
《前端每日实战》第173号作品:纪念篮球巨星科比·布莱恩特
https://segmentfault.com/a/1190000021655449
2020-01-28T22:40:35+08:00
2020-01-28T22:40:35+08:00
comehope
https://segmentfault.com/u/comehope
4
<p><img src="/img/bVbC1Ji" alt="173.png" title="173.png"></p>
<p>世事无常,巨星陨落。特以此作品纪念篮球巨星科比·布莱恩特。</p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/OJPGGmV">https://codepen.io/comehope/pen/OJPGGmV</a></p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=toRWEg1Qh1N%2BUiKtReNyRw%3D%3D.KPyF8zD7BzkcEVrpA%2FYuBhM%2FyBU5VHvrtzeqVqyaniLg1ZVHIZpPIYRFBEtvqKvRvdAzv8mfeiqsuYJKjnOkQw%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<h3>一、绘制篮球</h3>
<p>定义 DOM 结构,只有一个名为 <code>.ball</code> 的 <code><div></code> 元素,内含一个篮球图案的 unicode 字符:</p>
<pre><code class="html"><div class="ball">?</div></code></pre>
<p>令容器居中,设置页面背景色为由紫色到黑色的径向渐变:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-image: radial-gradient(circle, #542482, black);
}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbC1Jl" alt="01.png" title="01.png"></p>
<p>设置容器尺寸、字号,放大篮球,这里用 <code>vmin</code> 单位,使图案占据窗口宽高的一半:</p>
<pre><code class="css">.ball {
width: 50vmin;
height: 50vmin;
font-size: 50vmin;
line-height: 1em;
}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbC1Jn" alt="02.png" title="02.png"></p>
<h3>二、绘制光环</h3>
<p>用 <code>::before</code> 伪元素绘制光环容器,比主元素宽20%,但高度只有主元素的30%,形状为一个矩形,边框为橙色:</p>
<pre><code class="css">.ball {
position: relative;
}
.ball::before {
content: '';
position: absolute;
width: 120%;
height: 30%;
left: -10%;
top: -20%;
border: 2vmin solid orange;
box-sizing: border-box;
}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbC1Jo" alt="03.png" title="03.png"></p>
<p>为边框加圆角,使光环变圆:</p>
<pre><code class="css">.ball::before {
border-radius: 50%;
}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbC1Jt" alt="04.png" title="04.png"></p>
<p>为光环加上光晕,光晕的颜色是半透明的黄色:</p>
<pre><code class="css">.ball::before {
box-shadow: 0 0 0.1em hsla(60, 100%, 50%, 0.5);
}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbC1Ju" alt="05.png" title="05.png"></p>
<h3>三、绘制光晕</h3>
<p>接下来用 <code>::after</code> 伪元素绘制阴影,阴影与主元素等宽,但高只有主元素的20%,定位到主元素的底部,背景色为半透明的黑色:</p>
<pre><code class="css">.ball::after {
content: '';
position: absolute;
width: 100%;
height: 20%;
left: 0;
bottom: 0;
background-color: hsla(0, 0%, 0%, 0.6);
}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbC1Jv" alt="06.png" title="06.png"></p>
<p>为阴影容器加圆角属性使阴影变圆,并将阴影置于篮球后面:</p>
<pre><code class="css">.ball::after {
border-radius: 50%;
z-index: -1;
}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbC1Ji" alt="07.png" title="07.png"></p>
<p>大功告成!</p>
<h2>参考</h2>
<ul>
<li>径向渐变背景,《CSS3 艺术》第4.3节</li>
<li>阴影,《CSS3 艺术》第5.1.1节</li>
<li>主元素与伪元素的重叠关系,《CSS3 艺术》第1.9.3节</li>
</ul>
<h2>关于我、我的专栏和我的书</h2>
<p>张偶,网络笔名 @comehope,20世纪末触网,被 Web 的无穷魅力所俘获,自此始终战斗在 Web 开发第一线。</p>
<p>《前端每日实战》专栏,是我近年来实践 PBL(project-based learning)方法的结果,以项目驱动学习,展现从灵感闪现到代码实现的完整过程,亦可作为前端开发的练手习题和开发参考。</p>
<p><img src="https://segmentfault.com/img/bVbCcXO" alt="" title=""></p>
<p>拙作《CSS3 艺术》一书于2020年1月由人民邮电出版社出版,全彩印刷,使用100多个生动美观的实例,系统地剖析了 CSS 与视觉效果相关的重要语法,并含有近10小时的视频演示。京东/天猫/当当有售。配套资源请访问人民邮电出版社公众号:<a href="https://link.segmentfault.com/?enc=IDIj71i5%2FHvph37RsYK%2Ftg%3D%3D.ObDq7n7nW4eqPSMaAoCNwcGXiicwh%2BQwgzN%2FBRgnHbpzjoNkGWe%2F9fhWFBpd8Iuxy1Cksyczus5lJn9r%2BuJ40w%3D%3D" rel="nofollow">https://mp.weixin.qq.com/s/6X42QkZ5N8JNji8aQ2FFcQ</a></p>
《前端每日实战》第172号作品:向平凡而又伟大的医护人员致敬!
https://segmentfault.com/a/1190000021650752
2020-01-27T11:56:21+08:00
2020-01-27T11:56:21+08:00
comehope
https://segmentfault.com/u/comehope
8
<p><img src="/img/bVbC0vC" alt="172.gif" title="172.gif"></p>
<p>这是一场没有硝烟的战斗,我们普通人能做的就是守在家里,静待疫情结束。向那些冒着生命危险救助患者的医护人员们,致敬!</p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/gObyOMQ">https://codepen.io/comehope/pen/gObyOMQ</a></p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=PuY8WRYSR0uhIHGj1sMrzg%3D%3D.0Mka43rVWf2c9VL4tFv0Kvy5v2Jn%2BU%2BbHvcvdF%2BS%2FUs1DX2IUX8SnXFxwsqQ%2Fuwa1HIUejb0i9cp1fgvVCFawQ%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<h3>一、定义 DOM 结构</h3>
<p>dom 结构是一个名为 <code>.words</code> 的容器,内含 5 个 <code><p></code> 标签,每个 <code><p></code> 标签中有一句话:</p>
<pre><code class="html"><div class="words">
<p>你们</p>
<p>在没有硝烟的</p>
<p>战场上</p>
<p>把生命之光</p>
<p>再次点燃</p>
</div></code></pre>
<p>令容器在页面居中:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbC0vD" alt="01.png" title="01.png"></p>
<h3>二、布局文字</h3>
<p>用 flex 布局,设置容器的宽度为 <code>5em</code>,因为有 5 个 <code><p></code> 标签,所以每句话占用 <code>1em</code>,形成文字竖排效果。文字的字号用 <code>vmin</code> 单位,即随页面大小自动调整字号的大小。</p>
<pre><code class="css">.words {
display: flex;
font-size: 15vmin;
width: 5em;
}
.words p {
line-height: 1em;
margin: 0;
}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbC0vE" alt="02.png" title="02.png"></p>
<p>文字加粗,并加上阴影:</p>
<pre><code class="css">.words {
font-family: sans-serif;
font-weight: bold;
}
.words p {
text-shadow: 0.05em 0.05em 0.3em hsla(0, 0%, 0%, 0.4);
}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbC0vF" alt="03.png" title="03.png"></p>
<p>设置页面的颜色为 <code>dodgerblue</code>,即宝石蓝色。因为 <code>color</code> 属性会被子元素继承,所以文字的颜色也变成宝石蓝色融入了背景中,实际上现在看到的仅仅是文字的阴影而已:</p>
<pre><code class="css">body {
color: dodgerblue;
background-color: currentColor;
}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbC0vG" alt="04.png" title="04.png"></p>
<h3>三、动画效果</h3>
<p>创建一个名为 <code>appear</code> 的关键帧,其中只有 1 帧,即在动画周期的 <code>50%</code> 为文字增加阴影,其实这行代码正是从 <code>.words p</code> 移过来的。动画效果是时长 6 秒的无限循环动画:</p>
<pre><code class="css">.words p {
/*text-shadow: 0.05em 0.05em 0.3em hsla(0, 0%, 0%, 0.4);*/
animation: appear 6s ease-in-out infinite;
}
@keyframes appear {
50% {
text-shadow: 0.05em 0.05em 0.3em hsla(0, 0%, 0%, 0.4);
}
}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbC0vH" alt="05.gif" title="05.gif"></p>
<p>接下来令每一句话逐个浮现,实现方法是为每个 <code><p></code> 标签设置一个 <code>--n</code> 变量,然后为动画设置 <code>animation-delay</code> 属性,用表达式为所有 <code><p></code> 标签设置从 1 秒到 5 秒的延迟时长,令动画看起来是一句一句地依次浮现:</p>
<pre><code class="css">.words p {
animation-delay: calc(var(--n) * 1s);
}
.words p:nth-child(1) {--n: 1;}
.words p:nth-child(2) {--n: 2;}
.words p:nth-child(3) {--n: 3;}
.words p:nth-child(4) {--n: 4;}
.words p:nth-child(5) {--n: 5;}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbC0vI" alt="06.gif" title="06.gif"></p>
<p>接下来,定义一个名为 <code>move</code> 的关键帧,令 <code>p</code> 元素从它原始位置稍偏左向稍偏右移动,使动画的动感更强烈。因为一共有 <code>appear</code> 和 <code>move</code> 2 组关键帧,所以需增加一个 <code>animation-name</code> 属性,专门用于定义多组关键帧:</p>
<pre><code class="css">.words p {
/*animation: appear 6s ease-in-out infinite;*/
animation: 6s ease-in-out infinite;
animation-name: appear, move;
}
@keyframes move {
from {transform: translateX(-30%);}
to {transform: translateX(30%);}
}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbC0vJ" alt="07.gif" title="07.gif"></p>
<p>大功告成!</p>
<h2>参考</h2>
<ul>
<li>flex 布局,《CSS3 艺术》第1.8.1节</li>
<li>阴影,《CSS3 艺术》第5.1.1节</li>
<li>动画 animation,《CSS3 艺术》第10章</li>
</ul>
<h2>关于我的书《CSS3 艺术》</h2>
<p><img src="https://segmentfault.com/img/bVbCcXO" alt="" title=""></p>
<p>拙作《CSS3 艺术》全彩印刷,使用100多个生动美观的实例,系统地剖析了 CSS 与视觉效果相关的重要语法,并含有近10小时的视频演示。2020年1月由人民邮电出版社出版,京东/天猫/当当有售,配套资源请访问人民邮电出版社公众号:<a href="https://link.segmentfault.com/?enc=QKez8byj6LAkF1VXC2%2BdZw%3D%3D.GtbTfV9jsnMUVoEiUWw9Zy3NOYCKh5nxLH9WjROl0mKyYvRF7Btkvga2UnL1FCVVe5RlmLLCkRgaQD7aeDjWWg%3D%3D" rel="nofollow">https://mp.weixin.qq.com/s/6X42QkZ5N8JNji8aQ2FFcQ</a></p>
前端每日实战:苦练 CSS 基本功——图解辅助线的原理和画法
https://segmentfault.com/a/1190000021507641
2020-01-07T07:42:23+08:00
2020-01-07T07:42:23+08:00
comehope
https://segmentfault.com/u/comehope
68
<p><img src="/img/bVbCphm" alt="头图.png" title="头图.png"></p>
<p>在用 CSS 进行绘图和布局时,除了借助浏览器开发工具之外,还经常需要绘制一些辅助线,以便定位参考。今天就以<a href="https://segmentfault.com/a/1190000021460319">第 170 号作品</a>中使用的网格线为例,详细讲解一下辅助线的原理和画法。</p>
<p>为了使辅助线明显可见,把线的颜色设置为和背景对比强烈的白色,并且线也粗一些,在实际使用时,你应该降低辅助线与背景的对比并且使用细线。</p>
<h2>分步图解</h2>
<h3>1、定义容器</h3>
<pre><code class="css">div {
font-size: 50px;
width: 6em;
height: 4em;
background-color: teal;
}</code></pre>
<p>假设你有一个 <code><div></code> 容器,容器里是否有元素都可以,当前演示为了突显辅助线,暂时让容器里空空如也:</p>
<p><img src="/img/bVbCphn" alt="01.png" title="01.png"></p>
<h3>2、一条基本的横线</h3>
<pre><code class="css">div {
background-image: linear-gradient(to bottom, transparent 95%, white 95%);
}</code></pre>
<p>网格线是一条一条线条组成的,所以要先画出一条线,它的95%都是透明的,只有5%是白色的:</p>
<p><img src="/img/bVbCpho" alt="02.png" title="02.png"></p>
<h3>3、一条有尺寸的横线</h3>
<pre><code class="css">div {
background-size: 1em 1em;
background-repeat: no-repeat;
}</code></pre>
<p>请把绘制网格线想象成是铺地砖,首先要定义地砖的尺寸,这里用 <code>1em 1em</code> 定义了一块方砖,同时让砖块不重复,所以只显示出了孤单的一块砖:</p>
<p><img src="/img/bVbCphp" alt="03.png" title="03.png"></p>
<h3>4、横向平铺地砖</h3>
<pre><code class="css">div {
background-repeat: repeat-x;
}</code></pre>
<p>如果把地砖横向平铺,就能组合成一条水平线:</p>
<p><img src="/img/bVbCphq" alt="04.png" title="04.png"></p>
<h3>5、纵向平铺地砖</h3>
<pre><code class="css">div {
background-repeat: repeat-y;
}</code></pre>
<p>如果把地砖纵向平铺,就能组合成一条垂直线。<br>错!<br>纵向平铺是像阶梯一样的效果:</p>
<p><img src="/img/bVbCphr" alt="05.png" title="05.png"></p>
<h3>6、横向和纵向同时平铺地砖</h3>
<pre><code class="css">div {
background-repeat: repeat;
}</code></pre>
<p>横向和纵向同时平铺,就是像作业本一样的多条横线效果。注意,这时最顶端是没有线的:</p>
<p><img src="/img/bVbCphs" alt="06.png" title="06.png"></p>
<h3>7、竖线平铺效果</h3>
<pre><code class="css">div {
background-image: linear-gradient(to right, transparent 95%, white 95%);
background-size: 1em 1em;
background-repeat: repeat;
}</code></pre>
<p>假如把地砖换成向右的竖线,即只把 <code>to bottom</code> 改为 <code>to right</code> ,其他不变,绘制出的就是一排一排的竖线。同样注意,这时最左边是没有线的:</p>
<p><img src="/img/bVbCpht" alt="07.png" title="07.png"></p>
<h3>8、不完美的网格线</h3>
<pre><code class="css">div {
background-image:
linear-gradient(to bottom, transparent 95%, white 95%),
linear-gradient(to right, transparent 95%, white 95%);
background-size: 1em 1em;
background-repeat: repeat;
}</code></pre>
<p>把第6步和第7步合并起来,网格线基本成型了,不过顶端和左端还缺少线条:</p>
<p><img src="/img/bVbCphu" alt="08.png" title="08.png"></p>
<h3>9、一段顶端线</h3>
<pre><code class="css">div {
background-image:
linear-gradient(to top, transparent 95%, white 95%);
background-size: 1em 1em;
background-repeat: no-repeat;
}</code></pre>
<p>来解决顶端线的问题,先画出一段顶端线。这段代码和第3步极相似,仅仅是 <code>to bottom</code> 改成了 <code>to top</code>:</p>
<p><img src="/img/bVbCphv" alt="09.png" title="09.png"></p>
<h3>10、一条顶端线</h3>
<pre><code class="css">div {
background-repeat: repeat-x;
}</code></pre>
<p>把这一段顶端线水平平铺,就是一条定位在顶部的水平线:</p>
<p><img src="/img/bVbCphw" alt="10.png" title="10.png"></p>
<h3>11、一段左端线</h3>
<pre><code class="css">div {
background-image:
linear-gradient(to left, transparent 95%, white 95%);
background-size: 1em 1em;
background-repeat: no-repeat;
}</code></pre>
<p>用类似的办法解决左端线问题,先定义一段左端线,注意 <code>linear-gradient</code> 函数的第 1 个参数改成 <code>to left</code> 了:</p>
<p><img src="/img/bVbCphx" alt="11.png" title="11.png"></p>
<h3>12、一条左端线</h3>
<pre><code class="css">div {
background-repeat: repeat-y;
}</code></pre>
<p>平铺这段左端线,就得到了一条紧挨容器左侧的竖线:</p>
<p><img src="/img/bVbCphy" alt="12.png" title="12.png"></p>
<h3>13、All in one 的完美效果</h3>
<pre><code class="css">div:nth-child(13) {
background-image:
linear-gradient(to bottom, transparent 95%, white 95%),
linear-gradient(to right, transparent 95%, white 95%),
linear-gradient(to top, transparent 95%, w hite 95%),
linear-gradient(to left, transparent 95%, white 95%);
background-size: 1em 1em;
background-repeat: repeat, repeat, repeat-x, repeat-y;</code></pre>
<p>好了,我们把第8步不完美的网格线、顶端线、左端线都合起来,就是完美的网格线了,注意 <code>background-repeart</code> 的写法,它有 4 个参数,分别对应 <code>background-image</code> 里的 4 条线:</p>
<p><img src="/img/bVbCphj" alt="13.png" title="13.png"></p>
<p>干得漂亮!收工。</p>
<h2>参考</h2>
<ul>
<li>《CSS3 艺术》第4.1.2节,背景图片 background-image</li>
<li>《CSS3 艺术》第4.1.4节,背景尺寸 background-size</li>
<li>《CSS3 艺术》第4.1.5节,背景平铺 background-repeat</li>
<li>《CSS3 艺术》第4.2节,线性渐变 linear-gradient()</li>
</ul>
《前端每日实战》第171号作品:用纯 CSS 绘制一朵美丽的雪花
https://segmentfault.com/a/1190000021497721
2020-01-06T09:57:54+08:00
2020-01-06T09:57:54+08:00
comehope
https://segmentfault.com/u/comehope
22
<p><img src="/img/bVbCmGh" alt="171.gif" title="171.gif"></p>
<p>昨夜北京下了大雪,让我们用 CSS 绘制一朵雪花,迎接这洁白美好的世界吧!</p>
<h2>一、效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/LYEeRBb">https://codepen.io/comehope/pen/LYEeRBb</a></p>
<h2>二、源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=ke%2FEfu61M1C9gWmCbr2big%3D%3D.8fsAEC4%2FJnWo9RzPfmNMARlyzKeN1cKBphY3rOtD6h8fSvodsOfEr2rxMVa37yx30Ixfgh1OSydiDzU1dIlpig%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>三、代码解读</h2>
<h3>定义 DOM 结构、页面背景和容器尺寸</h3>
<p>最外层容器是一个名为 <code>.snowflake</code> 的 <code><figure></code> 元素,内含 6 个 <code><div></code> 元素,分别代表雪花的6个花瓣,每个 <code><div></code> 中又包含 5 个 <code><span></code> 元素,每个 <code><span></code> 代表雪花上的冰凌。</p>
<pre><code class="html"><figure class="snowflake">
<div>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
<div>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
<div>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
<div>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
<div>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
<div>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
</figure></code></pre>
<p>页面背景取黑色,雪花取白色,并为容器画出黄色的轮廓作为辅助线,雪花图案将绘制在这个黄色虚线框内:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: black;
overflow: hidden;
}
.snowflake {
font-size: 100px;
color: snow;
width: 4em;
height: 4em;
outline: 1px dashed yellow;
}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbCmGi" alt="01.png" title="01.png"></p>
<h3>绘制出6个花瓣</h3>
<p>先绘制出1个花瓣中间的竖线:</p>
<pre><code class="css">div {
width: 0.1em;
height: 2em;
background-color: currentColor;
border-radius: 0.05em;
}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbCmGj" alt="02.png" title="02.png"></p>
<p>发现6个花瓣的竖线重叠在一起了,把它们合并到一起,看起来就像只有1条竖线:</p>
<p>div {</p>
<pre><code>position: absolute;</code></pre>
<p>}</p>
<p>效果如下图:</p>
<p><img src="/img/bVbCmGk" alt="03.png" title="03.png"></p>
<p>分别旋转每个花瓣,一共6个花瓣,所以各花瓣的旋转角度均相差60度:</p>
<pre><code class="css">div {
transform-origin: bottom;
transform: rotate(calc((var(--n) - 1)* 60deg));
}
div:nth-child(1) {--n: 1;}
div:nth-child(2) {--n: 2;}
div:nth-child(3) {--n: 3;}
div:nth-child(4) {--n: 4;}
div:nth-child(5) {--n: 5;}
div:nth-child(6) {--n: 6;}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbCmGl" alt="04.png" title="04.png"></p>
<h3>绘制花瓣上的冰凌</h3>
<p>接下来修饰花瓣,绘制花瓣上的冰凌。</p>
<p>先来出顶端的圆点,用 <code><div></code> 里的第1个 <code><span></code> 元素实现:</p>
<pre><code class="css">div {
display: flex;
flex-direction: column;
align-items: center;
}
div span:nth-child(1) {
width: 0.2em;
height: 0.2em;
background-color: currentColor;
border-radius: 50%;
}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbCmGm" alt="05.png" title="05.png"></p>
<p>然后增加离圆点最近的折线,用第 2 个 <code><span></code> 元素画出,这是用一个正方形4条边框中的2条实现的:</p>
<pre><code class="css">div span:nth-child(2) {
width: 0.5em;
height: 0.5em;
border: 0.1em solid;
border-width: 0.1em;
border-style: none solid solid none;
border-radius: 0.05em;
}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbCmGu" alt="06.png" title="06.png"></p>
<p>把折线旋转45度,让它的尖部和竖线重合:</p>
<pre><code class="css">div span:nth-child(2) {
transform: rotate(45deg);
}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbCmGB" alt="07.png" title="07.png"></p>
<p>增加第2条折线,和上面的代码类似,只是正方形的边长从 <code>0.5em</code> 缩短到 <code>0.4em</code> 了:</p>
<pre><code class="css">div span:nth-child(3) {
width: 0.4em;
height: 0.4em;
border: 0.1em solid;
border-width: 0.1em;
border-style: none solid solid none;
border-radius: 0.05em;
transform: rotate(45deg);
}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbCmGC" alt="08.png" title="08.png"></p>
<p>再增加第3条折线:</p>
<pre><code class="css">div span:nth-child(4) {
width: 0.3em;
height: 0.3em;
border: 0.1em solid;
border-width: 0.1em;
border-style: none solid solid none;
border-radius: 0.05em;
transform: rotate(45deg);
}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbCmGM" alt="09.png" title="09.png"></p>
<p>再增加第4条折线:</p>
<pre><code class="css">div span:nth-child(4) {
width: 0.3em;
height: 0.3em;
border: 0.1em solid;
border-width: 0.1em;
border-style: none solid solid none;
border-radius: 0.05em;
transform: rotate(45deg);
}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbCmGQ" alt="10.png" title="10.png"></p>
<p>你已经发现上面 4 条折线的代码有很多重复的,坚决不能忍,来重构吧,把这 4 段代码合并起来:</p>
<pre><code class="css">div span:nth-child(2),
div span:nth-child(3),
div span:nth-child(4),
div span:nth-child(5) {
width: var(--side-length);
height: var(--side-length);
border: 0.1em solid;
border-width: 0.1em;
border-style: none solid solid none;
border-radius: 0.05em;
transform: rotate(45deg);
}
div span:nth-child(2) {--side-length: 0.5em;}
div span:nth-child(3) {--side-length: 0.4em;}
div span:nth-child(4) {--side-length: 0.3em;}
div span:nth-child(5) {--side-length: 0.3em;}</code></pre>
<p>最后,让第1条折线离中心稍远点,这样还能让雪花中心更加漂亮:</p>
<pre><code class="css">div span:nth-child(2) {
margin-top: -0.2em;
}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbCmHl" alt="11.png" title="11.png"></p>
<h3>增加动画效果</h3>
<p>动画效果很简单,就是转啊转地,让这片雪花用10秒时间转一圈:</p>
<pre><code class="css">.snowflake {
animation: round 10s linear infinite;
}
@keyframes round {
to {
transform: rotate(1turn);
}
}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbCmHn" alt="12.gif" title="12.gif"></p>
<p>最后,删除掉辅助助线:</p>
<pre><code class="css">.snowflake {
/* outline: 1px dashed yellow; */
}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbCmHo" alt="13.gif" title="13.gif"></p>
<p>大功告成!</p>
<h2>四、参考</h2>
<ul>
<li>flex 布局,《CSS3 艺术》第1.8.1节</li>
<li>边框属性 border,《CSS3 艺术》第3.1节</li>
<li>变量 var() 和 表达式 calc(),《CSS3 艺术》第7.1节</li>
<li>变换旋转函数 rotate(),《CSS3 艺术》第8.1.2节</li>
<li>变换原点 transform-origin,《CSS3 艺术》第8.2节</li>
<li>动画 animation,《CSS3 艺术》第10章</li>
</ul>
前端每日实战:170# 二十一世纪二〇年代的第一天,一切刚刚开始………
https://segmentfault.com/a/1190000021460319
2020-01-01T21:58:48+08:00
2020-01-01T21:58:48+08:00
comehope
https://segmentfault.com/u/comehope
13
<p><img src="/img/bVbCcXN" alt="170.gif" title="170.gif"></p>
<p>大家好,有一段时间没有发表《前端每日实战》作品了,今天,2020年的第一天,终于又重新启程,我将继续实践 PBL(Project-based Learning)的学习方法,并把学习笔记分享出来,和大家交流探讨、共同进步。</p>
<p>这段没有发表作品的日子里,我在人民邮电出版社的支持和帮助下,写出了一本书,命名为《CSS3 艺术》,它脱胎于《前端每日实战》的一百多个作品,把伪元素、边框、背景、阴影、剪切、滤镜、色彩混合、变量、计数器、变换、缓动、动画这些概念进行了梳理和总结,希望能为大家学习 CSS 提供一些帮助。此书已于近日上市,京东、天猫、当当均可购买。</p>
<p><img src="/img/bVbCcXO" alt="IMG_9685.JPG" title="IMG_9685.JPG"></p>
<p>接下来进入正题,今天的项目是用纯 CSS 制作一个 2020 造型图案,并为它增加一点动画效果。</p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/jOEGzZx">https://codepen.io/comehope/pen/jOEGzZx</a></p>
<h2>代码解读</h2>
<h3>一、基本的 dom 结构和页面背景</h3>
<p>dom 结构的最外层用 <code><figure></code> 元素,表示这是一个图片:</p>
<pre><code class="html"><figure></figure></code></pre>
<p>页面用深红色背景,并采用 grid 布局:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: darkred;
}
figure {
display: grid;
grid-template-columns: repeat(8, 1em);
grid-template-rows: repeat(3, 1em);
font-size: 60px;
color: whitesmoke;
}</code></pre>
<p>grid 布局比定位布局的语义化更好,同样的布局效果,grid 布局的代码量比定位布局的代码量明显减少。此项目创建了一个 3 行 8 列的网格,为了能明显看出网格线,我们增加一些辅助线,这些辅助线会在作品完成后被删除掉:</p>
<pre><code class="css">figure {
background-image:
linear-gradient(to bottom, transparent 0%, transparent 99%, pink 100%),
linear-gradient(to right, transparent 0%, transparent 99%, pink 100%),
linear-gradient(to top, transparent 0%, transparent 99%, pink 100%),
linear-gradient(to left, transparent 0%, transparent 99%, pink 100%);
background-size: 1em 1em;
background-repeat: repeat, repeat, repeat-x, repeat-y;
}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbCcXU" alt="01.png" title="01.png"></p>
<h3>二、绘制数字 2</h3>
<p>在 dom 中增加有关数字 2 的元素,数字 2 被分成了 4 部分:</p>
<pre><code class="html"><figure>
<span class="two part1"></span>
<span class="two part2"></span>
<span class="two part3"></span>
<span class="two part4"></span>
</figure></code></pre>
<p>把这 4 部分分别放置在网格的相应位置上,<code>grid-area</code> 的属性 x/y 分别表示顶部网格线编号和左侧网格线编号,网格线是从 1 开始编号的,以 <code>.part4</code> 为例,它的顶部网格线是从上数的第3条,左侧网格是从左数的第2条,所以它的属性值是 <code>3/2</code>:</p>
<pre><code class="css">.two {
background-color: currentColor;
}
.two.part1 {grid-area: 1/1;}
.two.part2 {grid-area: 2/2;}
.two.part3 {grid-area: 3/1;}
.two.part4 {grid-area: 3/2;}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbCcXY" alt="02.png" title="02.png"></p>
<p>接下来把各部分的形状都改为扇形,<code>border-radius</code> 属性有 4 个值,分别代表左上、右上、右下、左下的圆角值,以 <code>.part4</code> 为例,它是左下角为圆角的扇形,所以它的属性值是 <code>0 0 0 100%</code>:</p>
<pre><code class="css">.two.part1 {grid-area: 1/1; border-radius: 100% 0 0 0;}
.two.part2 {grid-area: 2/2; border-radius: 0 0 100% 0;}
.two.part3 {grid-area: 3/1; border-radius: 100% 0 0 0;}
.two.part4 {grid-area: 3/2; border-radius: 0 0 0 100%;}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbCcXZ" alt="03.png" title="03.png"></p>
<p>这时一个数字 2 已经绘制出来了,另一个数字 2 不需要增加 dom 元素,只要把第 1 个元素复制一下就可以了,这里使用的是 <code>drop-shadow()</code> 函数,它的 2 个参数分别代表复制后的偏移量,此处的参数值为 <code>4em 0</code>,即水平方向向右平移 4em,垂直方向不变:</p>
<pre><code class="css">.two {
filter: drop-shadow(4em 0);
}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbCcX0" alt="04.png" title="04.png"></p>
<p>至此,2个数字 2 就都画出来了。</p>
<h3>三、绘制数字 0</h3>
<p>在 dom 中增加有关数字 0 的元素,和数字 2 不同,每一个数字 0 只需要 1 个 dom 元素,所以 2 个数字 0 需要 2 个 dom 元素:</p>
<pre><code class="html"><figure>
<span class="two part1"></span>
<span class="two part2"></span>
<span class="two part3"></span>
<span class="two part4"></span>
<span class="zero copy-1"></span>
<span class="zero copy-2"></span>
</figure></code></pre>
<p>在网格中分别定位 2 个数字 0,仍使用 <code>grid-area</code> 参数,但它们的属性值为 4 个数字,后 2 个数字分别代表底部网格线编号和右侧网格线编号,可知每个 0 占据 2 * 2 的网格区域:</p>
<pre><code class="css">.zero.copy-1 {grid-area: 2/3/4/5;}
.zero.copy-2 {grid-area: 2/7/4/9;}</code></pre>
<p>画出数字 0 的大致轮廓,这里是利用边框属性绘制的,元素本身宽高为 0,但是有 1em 的边框,其中上、下边框是白色,左、右边框是透明色,注意,在 CSS 中边框并不一定以线条的形式存在,在此处每条边框都是三角形:</p>
<pre><code class="css">.zero {
width: 0;
height: 0;
border: 1em solid;
border-color: currentColor transparent;
}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbCcX1" alt="05.png" title="05.png"></p>
<p>增加圆角效果:</p>
<pre><code class="css">.zero {
border-radius: 50%;
}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbCcX2" alt="06.png" title="06.png"></p>
<p>再倾斜 45 度:</p>
<pre><code class="css">.zero {
transform: rotate(-45deg);
}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbCcX3" alt="07.png" title="07.png"></p>
<p>至此,2 个数字 0 也都画出来了。</p>
<h3>四、增加 Happy New Year 文本</h3>
<p>在 dom 中增加有关文本的元素,一共 3 个单词,分别用 3 个元素表示:</p>
<pre><code class="html"><figure>
<span class="two part1"></span>
<span class="two part2"></span>
<span class="two part3"></span>
<span class="two part4"></span>
<span class="zero copy-1"></span>
<span class="zero copy-2"></span>
<span class="text happy">happy</span>
<span class="text new">new</span>
<span class="text year">year</span>
</figure></code></pre>
<p>效果如下图,可以看到这 3 个单词都重叠在第1行的第2个网格中,这是因为在 grid 布局下会自动把未指定 <code>grid-area</code> 属性的元素排放在未被占用的网格中:</p>
<p><img src="/img/bVbCcX4" alt="08.png" title="08.png"></p>
<p>接下来为 3 个文本元素设置它们的网格位置:</p>
<pre><code class="css">.text.happy {grid-area: 1/2;}
.text.new {grid-area: 2/4;}
.text.year {grid-area: 1/6;}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbCcX5" alt="09.png" title="09.png"></p>
<p>接下来设置文字的样式,把文字都改为大写字母,加粗,字体用花式字体,为避免与左侧的图案靠得太紧,再把文字左侧增加一点内边距:</p>
<pre><code class="css">.text {
text-transform: uppercase;
font-size: 0.66em;
line-height: 1.5em;
font-weight: bold;
font-family: cursive;
padding-left: 0.25em;
}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbCcX7" alt="10.png" title="10.png"></p>
<p>至此,文字绘制完成。</p>
<h3>五、增加动画效果</h3>
<p>因为数字 0 造型太抽象,所以我们让数字 0 转动起来,动画很简单,就是以 4 秒每圈的速度不断地转啊转,因为数字 0 此前设置了旋转 45 度,所以动画的 <code>to</code> 关键帧要加上 45 度,另外旋转的度数是负值,表示逆时针旋转:</p>
<pre><code class="css">.zero {
animation: round 4s linear infinite;
}
@keyframes round {
to {
transform: rotate(calc(-45deg + -1turn));
}
}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbCcYb" alt="11.gif" title="11.gif"></p>
<p>不过我们看到左侧的数字 0 在转动时遮挡住了文字“new”,为了避免遮挡,我们用色彩混合模式来解决,这样当数字 0 和文字“new”重叠时,重叠的部分会变为黑色:</p>
<pre><code class="css">.text {
mix-blend-mode: difference;
}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbCcYg" alt="12.gif" title="12.gif"></p>
<p>至此,整个作品全部完成了,最后把辅助线删除掉:</p>
<pre><code class="css">figure {
/*background-image:
linear-gradient(to bottom, transparent 0%, transparent 99%, pink 100%),
linear-gradient(to right, transparent 0%, transparent 99%, pink 100%),
linear-gradient(to top, transparent 0%, transparent 99%, pink 100%),
linear-gradient(to left, transparent 0%, transparent 99%, pink 100%);
background-size: 1em 1em;
background-repeat: repeat, repeat, repeat-x, repeat-y;*/
}</code></pre>
<p>效果如下图:</p>
<p><img src="/img/bVbCcYh" alt="13.gif" title="13.gif"></p>
<p>完整的 CSS 代码如下:</p>
<pre><code class="css">body {
margin: 0;![13.gif](/img/bVbCcYh)
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: darkred;
}
figure {
display: grid;
grid-template-columns: repeat(8, 1em);
grid-template-rows: repeat(3, 1em);
font-size: 60px;
color: whitesmoke;
}
.two {
background-color: currentColor;
filter: drop-shadow(4em 0);
}
.two.part1 {grid-area: 1/1; border-radius: 100% 0 0 0;}
.two.part2 {grid-area: 2/2; border-radius: 0 0 100% 0;}
.two.part3 {grid-area: 3/1; border-radius: 100% 0 0 0;}
.two.part4 {grid-area: 3/2; border-radius: 0 0 0 100%;}
.zero.copy-1 {grid-area: 2/3/4/5;}
.zero.copy-2 {grid-area: 2/7/4/9;}
.zero {
width: 0;
height: 0;
border: 1em solid;
border-color: currentColor transparent;
border-radius: 50%;
transform: rotate(-45deg);
animation: round 4s linear infinite;
}
@keyframes round {
to {
transform: rotate(calc(-45deg + -1turn));
}
}
.text.happy {grid-area: 1/2;}
.text.new {grid-area: 2/4;}
.text.year {grid-area: 1/6;}
.text {
text-transform: uppercase;
font-size: 0.66em;
line-height: 1.5em;
font-weight: bold;
font-family: cursive;
padding-left: 0.25em;
mix-blend-mode: difference;
}</code></pre>
<h2>参考</h2>
<ul>
<li>grid 布局,《CSS3 艺术》第1.8.2节;</li>
<li>drop-shadow() 函数,《CSS3 艺术》第5.2节;</li>
<li>border-radius 扇形造型,《CSS3 艺术》第3.3.3节;</li>
<li>border-radius 花型造型,《CSS3 艺术》第3.3.5节;</li>
<li>animation 动画,《CSS3 艺术》第10章;</li>
<li>mix-blend-mode 色彩混合模式,《CSS3 艺术》第6.3.3节。</li>
</ul>
前端每日实战 2018年10月至2019年6月项目汇总(共 20 个项目)
https://segmentfault.com/a/1190000019844617
2019-07-23T11:35:48+08:00
2019-07-23T11:35:48+08:00
comehope
https://segmentfault.com/u/comehope
45
<h2>过往项目</h2>
<p><a href="https://segmentfault.com/a/1190000016604394">2018 年 9 月份项目汇总(共 26 个项目)</a></p>
<p><a href="https://segmentfault.com/a/1190000016237865">2018 年 8 月份项目汇总(共 29 个项目)</a></p>
<p><a href="https://segmentfault.com/a/1190000015958405">2018 年 7 月份项目汇总(共 29 个项目)</a></p>
<p><a href="https://segmentfault.com/a/1190000015439611">2018 年 6 月份项目汇总(共 27 个项目)</a></p>
<p><a href="https://segmentfault.com/a/1190000015440135">2018 年 5 月份项目汇总(共 30 个项目)</a></p>
<p><a href="https://segmentfault.com/a/1190000014675969">2018 年 4 月份项目汇总(共 8 个项目)</a></p>
<h2>2018 年 10 月至 2019 年 6 月发布的项目</h2>
<p>《前端每日实战》专栏每天分解一个前端项目,用视频记录编码过程,再配合详细的代码解读,是学习前端开发的活的参考书!</p>
<h3><a href="https://segmentfault.com/a/1190000016582341">150# 视频演示如何用 CSS 和 D3 创作一个集体舞动画</a></h3>
<p><img src="https://segmentfault.com/img/bVbhJZg?w=400&h=301" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000016596370">151# 视频演示如何用纯 CSS 创作超能陆战队的大白</a></h3>
<p><img src="https://segmentfault.com/img/bVbhNDx?w=400&h=300" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000016600016">152# 视频演示如何用纯 CSS 创作一个圆点错觉效果</a></h3>
<p><img src="https://segmentfault.com/img/bVbhOAl?w=400&h=300" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000016651727">153# 视频演示如何用 CSS 和原生 JS 创作一组 tooltip 提示框</a></h3>
<p><img src="https://segmentfault.com/img/bVbh12C?w=400&h=303" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000016657527">154# 视频演示如何创作一个眼冒金星的动画效果</a></h3>
<p><img src="https://segmentfault.com/img/bVbh3xW?w=400&h=304" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000016668903">155# 视频演示如何用纯 CSS 创作一只热气球</a></h3>
<p><img src="https://segmentfault.com/img/bVbh6vq?w=400&h=300" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000016688955">156# 视频演示如何用纯 CSS 创作一个飞机舷窗风格的 toggle 控件</a></h3>
<p><img src="https://segmentfault.com/img/bVbibIK?w=400&h=301" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000016707813">157# 视频演示如何用纯 CSS 创作一个棋盘错觉动画</a></h3>
<p><img src="https://segmentfault.com/img/bVbigC0?w=400&h=299" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000016724029">158# 视频演示如何用纯 CSS 创作一个雨伞 toggle 控件</a></h3>
<p><img src="https://segmentfault.com/img/bVbikQw?w=400&h=301" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000016750591">159# 视频演示如何用 CSS 和原生 JS 创作一个展示苹果设备的交互动画</a></h3>
<p><img src="https://segmentfault.com/img/bVbirKY?w=400&h=307" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000016774570">160# 视频演示如何用纯 CSS 创作一个打开内容弹窗的交互动画</a></h3>
<p><img src="https://segmentfault.com/img/bVbixZI?w=400&h=302" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000016791409">161# 视频演示如何用纯 CSS 创作一张纪念卓别林的卡片</a></h3>
<p><img src="https://segmentfault.com/img/bVbiCnk?w=400&h=301" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000017064196">162# 视频演示如何用原生 JS 创作一个查询 github 用户的应用(内含 2 个视频)</a></h3>
<p><img src="https://segmentfault.com/img/bVbjLk3?w=400&h=301" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000017212162">163# 视频演示如何用原生 JS 创作一个多选一场景的交互游戏(内含 3 个视频)</a></h3>
<p><img src="https://segmentfault.com/img/bVbknOW?w=400&h=302" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000017311527">164# 视频演示如何用原生 JS 创作一个数独训练小游戏(内含 4 个视频)</a></h3>
<p><img src="https://segmentfault.com/img/bVbkNGa?w=400&h=300" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000017539885">165# 视频演示如何用 Vue 创作一个算术训练程序(内含 3 个视频)</a></h3>
<p><img src="https://segmentfault.com/img/bVblK5u?w=400&h=301" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000019216340">166# 视频演示如何用 CSS 创作一个 Safari LOGO</a></h3>
<p><img src="https://segmentfault.com/img/bVbsTZD?w=400&h=399" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000019238347">167# 视频演示如何用 1 个 dom 元素创作两颗爱心</a></h3>
<p><img src="https://segmentfault.com/img/bVbsSVm?w=400&h=348" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000019274437">168# 视频演示如何利用 Web Animation API 制作一个切换英语单词的交互动画</a></h3>
<p><img src="https://segmentfault.com/img/bVbs2j1?w=400&h=401" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000019462745">169# 视频演示如何制作“数略词”交互动画(内含2个视频)</a></h3>
<p><img src="https://segmentfault.com/img/bVbtPjm?w=400&h=401" alt="" title=""></p>
前端每日实战 169# 视频演示如何制作“数略词”交互动画(内含2个视频)
https://segmentfault.com/a/1190000019462745
2019-06-13T07:43:10+08:00
2019-06-13T07:43:10+08:00
comehope
https://segmentfault.com/u/comehope
10
<p><img src="/img/bVbtPjm?w=400&h=401" alt="图片描述" title="图片描述"></p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/byvRxB">https://codepen.io/comehope/pen/byvRxB</a></p>
<h2>可交互视频</h2>
<p>此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。</p>
<p>请用 chrome, safari, edge 打开观看。</p>
<p>视频1: <a href="https://link.segmentfault.com/?enc=0QF5AEnlCrrG%2FkEhnn2O6w%3D%3D.XF4Wnjbi0JIekWWPHCiMwlWNlJH6QGGtVdzDwK1YiuVz1RW5L%2Fsp3df%2Bn0ufLJ6%2F" rel="nofollow">https://scrimba.com/p/pEgDAM/cR4gpGsa</a><br>视频2: <a href="https://link.segmentfault.com/?enc=QQS9T1Han1oEvTDNLFH2Ww%3D%3D.aUpiXIDm9r8XaF62Tl%2FTKkV7p02%2BUexcm%2FuQaxUYCFAMfG0u4oeNRSce1vYPwnRe" rel="nofollow">https://scrimba.com/p/pEgDAM/czNp3MUZ</a></p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=Pz3ogOVXp7%2B7DPSVkIzmFw%3D%3D.z5Yd54Kjf6VnWuBf%2FcVxW2CEL8t8OBUV%2F8wZ0VgCiECeH1WQ6HjmKegpZ9j8Vm3sPOmETOrNAze%2FAQ2TOHltbw%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<p>大家是否见过 “i18n”、“a11y” 这样的英文单词?它们其实是一些单词的缩写,“i18n” 代表的是 “internationalization”(国际化),“a11y” 代表的是 “accessibility”(可访问性),因为包含的字母太多了,所以缩写时只保留头尾的字母,再把余下的字符个数写在中间,这种写法称为“Numeronym”,我把它翻译成“数略词”。据说最长的单词是 “pneumonoultramicroscopicsilicovolcanoconiosis”,由 45 个字母组成,意思是一种肺部疾病。</p>
<p>本项目将制作一个交互动画效果,令其在单词原词和“数略词”之间切换。整个项目分成二个步骤开发,第一步先实现一个固定单词的交互动画,第二步改写为能自动处理任意的单词,然后扩展应用到多个单词上。</p>
<h3>一、一个固定单词的交互动画</h3>
<p>dom 结构如下,最外侧的容器名为 <code>.container</code>,其中包含一个名为 <code>.word</code> 的 <code><div></code> 元素,它代表一个单词,它的子元素是 4 个 <code><p></code> 元素,分别代表单词的第1个字母、中间字符的个数(<code>.middle.short</code>)、中间的若干字符(<code>.middle.long</code>)、单词的最后1个字母。因为动画时将交替显示“中间字符的个数”和“中间的若干字符”,所以为它们设置了特殊类名,以便在随后的 css 代码中引用它们,当要同时选择它们时,就用它们共同的类名 <code>middle</code>,当要分别选择时,就指定它们各自的类名 <code>short</code> 和 <code>long</code>:</p>
<pre><code class="html"><div class="container">
<div class="word">
<p>i</p>
<p class="middle short">
<span>18</span>
</p>
<p class="middle long">
<span>nternationalizatio</span>
</p>
<p>n</p>
</div>
</div></code></pre>
<p>令容器居于页面正中:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-image: linear-gradient(bisque, lightcyan);
}</code></pre>
<p>让 4 个 <code><p></code> 标签包含的文字横向排列在容器中部:</p>
<pre><code class="css">.container {
width: 100%;
}
.word {
font-size: 35px;
font-family: monospace;
display: flex;
justify-content: center;
}</code></pre>
<p>把 2 个中间的 <code><p></code> 元素的文字上色,突出显示它们:</p>
<pre><code class="css">.middle {
color: tomato;
}</code></pre>
<p>接下来制作交互动画效果。</p>
<p>先把中间的若干字符隐藏起来,只显示中间字符的个数,在 html 代码中找到 <code>.middle.long</code> 元素,为它增加一个 <code>hide</code> 样式类:</p>
<pre><code class="html"><p class="middle long hide"></code></pre>
<p>在 css 中将 <code>.middle.hide</code> 元素的宽度设置为 <code>0</code>,并且不显示超出容器的部分:</p>
<pre><code class="css">.middle {
overflow: hidden;
}
.middle.hide {
width: 0;
}</code></pre>
<p>令鼠标悬停在单词上时,鼠标指针变成一只手,提示用户此时可以点击:</p>
<pre><code class="css">.word:hover {
cursor: pointer;
}</code></pre>
<p>为 <code>.word</code> 元素增加鼠标点击事件,当单词被点击时,2 个中间元素分别切换 <code>hide</code> 类,交替显示两者中的一个元素,这些代码写在一个名为 <code>initWordElement()</code> 的方法中。在页面载入时将执行 <code>init()</code> 方法,再在其中调用 <code>initWordElement()</code> 方法。没有让 <code>window.onload</code> 直接执行 <code>initWordElement()</code> 方法,而是通过 <code>init()</code> 来调用,是因为在页面初始化阶段还会要做一些其他操作,后面还会逐渐充实 <code>init()</code> 方法:</p>
<pre><code class="js">window.onload = init
function init() {
let el = document.querySelector('.word')
initWordElement(el)
}
function initWordElement(el) {
let middles = el.querySelectorAll('.middle')
el.onclick = () => middles.forEach(m => m.classList.toggle('hide'))
}</code></pre>
<p>现在,在页面上多次点击单词,能看到单词的中间部分不断切换了,不过这时还没有动画效果,接下来为切换增加缓动效果。</p>
<p>先为中间的 2 个元素设置宽度,这 2 个值是手工测量得到的,这不是最终的写法,后面我们会改成用脚本自动测量得到元素的宽度,不过因为现在我们要解决的是动画效果,所以先临时硬编码一下:</p>
<p>加缓动:</p>
<pre><code class="css">.middle {
transition: 1s;
}
.middle.short {width: 42px;}
.middle.long {width: 378px;}</code></pre>
<p>设置缓时长为 1 秒:</p>
<pre><code class="css">.middle {
transition: 1s;
}</code></pre>
<p>现在,点击单词时的切换效果,已经有了动画过程,接下来细化动画效果。</p>
<p>切换可以理解由 2 个动作组成:一个中间元素消失,另一个中间元素出现,通过增加缓动延时来实现这个效果:</p>
<pre><code class="css">.middle {
transition: 1s;
transition-delay: 1s;
}
.middle.hide {
transition: 1s;
}</code></pre>
<p>现在,当改变元素宽度时,是以元素的左侧为起点改变宽度的,不够漂亮,我们把它改成以中间为中点改变宽度,这样当元素变宽时,就向两侧延伸,当元素变窄时,就向中间收缩:</p>
<pre><code class="css">.middle {
position: relative;
}
.middle span {
position: absolute;
transform: translateX(0);
transition: 1s;
transition-delay: 1s;
}
.middle.hide span {
transform: translateX(-50%);
transition: 1s;
}</code></pre>
<p>接下来修改缓动时长,由 <code>1s</code> 缩短为 <code>0.5s</code>,也就是令动画速度加快一倍。为了能方便调试和维护,我们把时长的值定义为变量 <code>--t</code>:</p>
<pre><code class="css">.word {
--t: 0.5s;
}
.middle {
transition: var(--t);
transition-delay: var(--t);
}
.middle span {
transition: var(--t);
transition-delay: var(--t);
}
.middle.hide {
transition: var(--t);
}
.middle.hide span {
transition: var(--t);
}</code></pre>
<p>至此,动画效果制作完成。</p>
<h3>二、扩展应用到多个单词</h3>
<p>“数略词”有很多,为了能够一次展示多个单词,我们将对现有的程序进行扩展。</p>
<p>先引入 lodash 库,我们将利用它提供的一个模板函数来处理 html 模板:</p>
<pre><code class="html"><script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script></code></pre>
<p>扩展 dom 结构,<code>.container</code> 容器中将包含不止一个 <code>.word</code> 元素,而是多个 <code>.word</code> 元素了。</p>
<p>创建一个 html 模板,它的内容是 <code>.word</code> 元素的代码,其中的第一个字母、中间字符个数、中间的若干字符、最后一个字母,这些内容在模板中分别用变量 <code>first</code>、<code>middleLength</code>、<code>middle</code>、<code>last</code> 表示:</p>
<pre><code class="html"><script type="text/x-templ" id="template">
<p><%= first %></p>
<p class="middle short">
<span><%= middleLength %></span>
</p>
<p class="middle long hide">
<span><%= middle %></span>
</p>
<p><%= last %></p>
</script></code></pre>
<p>而原 <code>.container</code> 元素中的内容都要删除掉,以便动态填充:</p>
<pre><code class="html"><div class="container"></div></code></pre>
<p>写一个名为 <code>getWordObject()</code> 的获取单词对象的函数,输入是一个单词,如“internationalization”,输出是一个对象,这个对象的属性与 html 模板中的变量相对应:</p>
<pre><code class="js">function getWordObject(w) {
return {
first: w.slice(0, 1),
last: w.slice(-1),
middle: w.slice(1, -1),
middleLength: w.slice(1, -1).length,
}
}</code></pre>
<p>接下来写一个名为 <code>createWordElement()</code> 的方法,用于创建一个 <code>.word</code> 元素,在这个方法中使用了 lodash 的 <code>_.template()</code> 模板函数。该方法的输入是一个单词,将传递给 <code>getWordObject()</code> 函数:</p>
<pre><code class="js">function createWordElement(word) {
const TEMPLATE = document.querySelector('#template').innerHTML
let el = document.createElement('div')
el.className = 'word'
el.innerHTML = _.template(TEMPLATE)(getWordObject(word))
return el
}</code></pre>
<p>在负责页面初始化的 <code>init()</code> 方法中调用 <code>createWordElement()</code> 方法,整个流程改为先创建一个元素,然后把该元素添加到 <code>.container</code> 容器中,再初始化这个元素:</p>
<pre><code class="js">function init() {
let word = 'internationalization'
let el = createWordElement(word)
document.querySelector('.container').appendChild(el)
initWordElement(el)
}</code></pre>
<p>现在,运行一下页面,虽然运行效果没有任何变化,但是 css 的属性、页面元素都已经变成动态生成的了。如果把 <code>init()</code> 方法中的 <code>word</code> 变量值改为其他单词,如 “accessibility”,页面中就会显示 “a11y”<br>了。</p>
<p>不过,在单词变为 “a11y” 之后,中间元素占据的宽度就不正确了,这是因为此前中间元素的宽度是硬编码的,需要把它们改为用脚本赋值。先删除掉 css 中的这 2 行代码:</p>
<pre><code class="css">/* .middle.short {width: 42px;}
.middle.long {width: 378px;} */</code></pre>
<p>然后为 <code>.middle</code> 元素设置宽度属性,属性值是名为 <code>--w</code> 的变量:</p>
<pre><code class="css">.middle {
width: var(--w);
}</code></pre>
<p>然后在 <code>initWordElement()</code> 方法中增加一行,为变量 <code>--w</code> 赋值:</p>
<pre><code class="js">function initWordElement(el) {
let middles = el.querySelectorAll('.middle')
middles.forEach(m =>
m.style.setProperty('--w',
window.getComputedStyle(m.querySelector('span')).width))
el.onclick = () => middles.forEach(m => m.classList.toggle('hide'))
}</code></pre>
<p>好了,现在不论把单词换成什么,都能合适地展现了,至此,单个单词的动态改造就完成了。</p>
<p>接下来请孙大圣拔下几根毫毛,帮我们把一个单词变成多个单词吧。</p>
<p>修改 init() 方法,删除掉 <code>word</code> 变量,定义一个名为 <code>WORDS</code> 的数组,遍历这个数组,为数组中的每个单词创建一个 <code>.word</code> 元素:</p>
<pre><code class="js">function init() {
const WORDS = [
'localization',
'accessibility',
'internationalization',
'supercalifragilisticexpialidocious',
'pneumonoultramicroscopicsilicovolcanoconiosis'
]
WORDS.forEach(word => {
let el = createWordElement(word)
document.querySelector('.container').appendChild(el)
initWordElement(el)
})
}</code></pre>
<p>现在,页面上已经有 5 个单词了,点击那个 “p43s” 看看世界上最长的单词吧。</p>
<p>最后,因为 <code><p></code> 元素的外边距较大,把它调整得小一点,让纵向的几个单词排列得紧凑一点:</p>
<pre><code class="css">.word p {
margin: 0.3em 0;
}</code></pre>
<p>大功告成!</p>
前端每日实战 168# 视频演示如何利用 Web Animation API 制作一个切换英语单词的交互动画
https://segmentfault.com/a/1190000019274437
2019-05-23T17:08:36+08:00
2019-05-23T17:08:36+08:00
comehope
https://segmentfault.com/u/comehope
21
<p><img src="/img/bVbs2j1?w=400&h=401" alt="图片描述" title="图片描述"></p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/byabeG">https://codepen.io/comehope/pen/byabeG</a></p>
<h2>可交互视频</h2>
<p>此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。</p>
<p>请用 chrome, safari, edge 打开观看。</p>
<p><a href="https://link.segmentfault.com/?enc=Qrm5LRjp4bRVH7LfKLsgpg%3D%3D.7X6v2XEGnv4HcjzLKxSLz6j3xjoHYHT%2FaxxWExu341BLsS0bYWLje8qIKIQuWHNe" rel="nofollow">https://scrimba.com/p/pEgDAM/cevPbkfB</a></p>
<p>(因为 scrimba 不支持 web animation api,所以动画效果在视频播放过程中看不到,不过你可以随时暂停视频,手工刷新预览窗口查看动画效果)</p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=WUefUg9WwpdU8llrGPD9rw%3D%3D.JXUfQzUAlZ2DqplFz5n1qo9GW5qxrjhVhT5l%2FmvgkiJDB9gjGp8bre2gUpBYmZE8WhvcJL2EupvK34XlUYpIdQ%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<p>本作品用于展示若干包含字母组合 <code>OO</code> 的单词,每点击一下,<code>OO</code> 就眨眨眼,同时更换一个单词。</p>
<p>整体开发过程分成 4 步,第 1 步用 CSS 实现页面的静态布局,后面 3 步用 JS 实现动画和业务逻辑。第 2 步实现单词中间字母 <code>OO</code> 的眨眼效果,第 3 步实现随机取单词的逻辑,第 4 步实现字符的切换动画。</p>
<p>眨眼动画和字符切换动画都是用 Web Animation API 实现的。虽然用 JS 写动画比用 CSS 要麻烦一些,但 API 提供了一些事件 handler,在字符切换动画中就是利用事件机制来精确控制动画和在动画过程中加入业务逻辑的。</p>
<p>下面开始编码。</p>
<h3>一、静态布局:dom,css</h3>
<p>dom 结构很简单,一个名为 <code>.word</code> 的 <code><p></code> 元素中包含了 4 个 <code><span></code> 子元素,每个子元素容纳一个字符:</p>
<pre><code class="html"><p class="word">
<span>b</span>
<span>o</span>
<span>o</span>
<span>k</span>
</p></code></pre>
<p>令页面中的元素居中,设置页面背景色为青蓝色:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: steelblue;
}</code></pre>
<p>设置单词的样式,麻布色,大字号,大写:</p>
<pre><code class="css">.word {
font-size: 100px;
color: linen;
font-family: monospace;
font-weight: bold;
display: flex;
text-transform: uppercase;
cursor: pointer;
user-select: none;
}</code></pre>
<p>让单词两端的 2 个字符变为粉色:</p>
<pre><code class="css">.word span:first-child,
.word span:last-child {
color: pink;
}</code></pre>
<p>用径向渐变给单词中间的 <code>OO</code> 加上眼珠:</p>
<pre><code class="css">.word span:not(:first-child):not(:last-child) {
background-image: radial-gradient(
circle at center,
linen 0.05em,
transparent 0.05em
);
}</code></pre>
<p>至此,静态布局完成。</p>
<h3>二、眨眼动画</h3>
<p>为 <code>.word</code> 元素创建一个单击事件函数,每当点击发生时,就先让中间的 <code>OO</code> 眨眼,然后获得下一个要显示的单词,再把当前的单词换成新的单词:</p>
<pre><code class="js">document.querySelector('.word').onclick = function() {
//第1步:眨眼动画
//第2步:获得下一个单词
//第3步:字符切换动画
}</code></pre>
<p>先来实现第1步-眨眼动画。在此之前了解一下 Web Animation API 的语法,下面是一个简单的示例:</p>
<pre><code class="js">let keyframes = [
{transform: 'scaleY(1)'},
{transform: 'scaleY(0.1)'},
]
let options = {
duration: 200,
iterations: 2,
}
element.animate(keyframes, options)</code></pre>
<p><code>animate()</code> 方法接收 2 个参数,第 1 个参数是一个数组,用于定义关键帧;第 2 个参数是一个对象,用于定义动画属性,它们分别对应着 CSS 中的 <code>@keyframes</code> 语句和 <code>animation</code> 属性。上面的 JS 代码等价于以下 CSS 代码:</p>
<pre><code class="css">@keyframes anim {
from {
transform: scaleY(1);
}
to {
transform: scaleY(0);
}
}
.element {
animation-name: anim;
animation-duration: 200ms;
animation-iteration-count: 2;
}</code></pre>
<p>好了,我们来正式写眨眼动画:</p>
<pre><code class="js">function blinkEyes() {
let eyes = document.querySelectorAll('.word span:not(:first-child):not(:last-child)')
let keyframes = [
{transform: 'scaleY(1)', offset: 0},
{transform: 'scaleY(0.1)', offset: 0.25},
{transform: 'scaleY(1)', offset: 0.5},
{transform: 'scaleY(1)', offset: 1},
]
let options = {
duration: 200,
iterations: 2,
}
eyes.forEach(eye => eye.animate(keyframes, options))
}</code></pre>
<p>上面代码中的 <code>offset</code> 是 <code>@keyframes</code> 中为每一帧指定的百分比值。这段动画的意思是每次动画眨眼 2 次,每次眨眼用时 200ms,这 200ms 的前 50% 时间(即前 100ms)做眨眼动作,后 50% 时间等待,这样设计的目的是在 2 次眨眼之间插入 100ms 的间隔。</p>
<p>然后,在点击事件里调用上面的方法:</p>
<pre><code class="js">document.querySelector('.word').onclick = function() {
//第1步:眨眼动画
blinkEyes()
//第2步:获得下一个单词
//第3步:字符切换动画
}</code></pre>
<p>至此,当用鼠标点击文字时,<code>OO</code> 就会眨动。</p>
<h3>三、获得下一个单词</h3>
<p>接下来写一点业务逻辑,用于随机取出一个单词。</p>
<p>引入 lodash 库:</p>
<pre><code class="html"><script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script></code></pre>
<p>定义一个名为 <code>Word</code> 的类:</p>
<pre><code class="js">function Word() {
const WORDS = ['book', 'boot', 'cook', 'cool', 'door', 'food', 'fool', 'foot', 'good', 'look', 'loop', 'moon', 'noon', 'pool', 'poor', 'room', 'roof','root', 'soon', 'tool', 'wood', 'zoom',]
let current = 'book'
this.getNext = () => {return current = _(WORDS).without(current).sample()}
}</code></pre>
<p><code>Word</code> 类有一个名为 <code>getNext()</code> 的方法,用于从预设的数组中随机取出一个单词,可以用下面的代码测试一下效果,会输出类似 <code>food</code> 这样的单词:</p>
<pre><code class="js">let word = new Word()
console.log(word.getNext())</code></pre>
<p>因为接下来的动画只涉及单词左右两侧的字母,所以在 <code>getNext()</code> 方法中再把两端的字符拆出来,返回一个对象:</p>
<pre><code class="js">function Word() {
const WORDS = ['book', 'boot', 'cook', 'cool', 'door', 'food', 'fool', 'foot', 'good', 'look', 'loop', 'moon', 'noon', 'pool', 'poor', 'room', 'roof','root', 'soon', 'tool', 'wood', 'zoom',]
let current = 'book'
this.getNext = () => {
current = _(WORDS).without(current).sample()
return {
first: current.slice(0, 1),
last: current.slice(-1)
}
}
}</code></pre>
<p>再测试一下效果,输出结果会变为类似 <code>{first: "f", last: "d"}</code> 的对象。</p>
<p>在点击事件中调用上面的函数,把结果存入一个名为 <code>chars</code> 的变量中:</p>
<pre><code class="js">let word = new Word()
document.querySelector('.word').onclick = function() {
//step 1: eyes blink animation
blinkEyes()
//第2步:获得下一个单词
let chars = word.getNext()
//第3步:字符切换动画
}</code></pre>
<h3>四、字符切换动画</h3>
<p>该制作字符切换动画了。</p>
<p>函数的声明如下,函数名为 <code>switchChar</code>,它接收 2 个参数,第 1 个参数表示对哪个字符执行动画,值为 <code>first</code> 或 <code>last</code>,第 2 个参数是将被替换成的新字符:</p>
<pre><code class="js">function switchChar(which, char) {}</code></pre>
<p>这样来调用:</p>
<pre><code class="js">switchChar('first', 'f')</code></pre>
<p>先实现更换逻辑,不包含动画效果:</p>
<pre><code class="js">function switchChar(which, char) {
let letter = {
first: {
dom: document.querySelector('.word span:first-child'),
},
last: {
dom: document.querySelector('.word span:last-child'),
}
}[which]
letter.dom.textContent = char
}</code></pre>
<p>在点击事件中调用 <code>switchChar</code> 函数:</p>
<pre><code class="js">document.querySelector('.word').onclick = function() {
//step 1: eyes blink animation
blinkEyes()
//第2步:获得下一个单词
let chars = word.getNext()
//第3步:字符切换动画
Object.keys(chars).forEach(key => switchChar(key, chars[key]))
}</code></pre>
<p>现在运行程序的话,在每次点击之后,单词两侧的字符都会更新。</p>
<p>接下来写动画效果,方法和写眨眼动画类似。这里有两点要说明,一是因为有 <code>first</code>、<code>last</code> 2 个字符、又有入场、出场 2 个动画,所以实际上一共实现了 4 个动画效果;二是动画的流程是先让旧字符出场,再让新字符入场,而更换字符的操作放置在这 2 个动画中间,这是用动画 API 的 <code>onfinish</code> 事件实现的:</p>
<pre><code class="js">function switchChar(which, char) {
let letter = {
first: {
dom: document.querySelector('.word span:first-child'),
to: '-0.5em',
from: '0.8em',
},
last: {
dom: document.querySelector('.word span:last-child'),
to: '0.5em',
from: '-0.8em',
}
}[which]
let keyframes = {
out: [
{transform: `translateX(0)`, filter: 'opacity(1)'},
{transform: `translateX(${letter.to})`, filter: 'opacity(0)'},
],
in: [
{transform: `translateX(${letter.from})`, filter: 'opacity(0)'},
{transform: `translateX(0)`, filter: 'opacity(1)'},
]
}
let options = {
duration: 500,
fill: 'forwards',
easing: 'cubic-bezier(0.5, 1.5, 0.5, 1.5)'
}
letter.dom
.animate(keyframes.out, options)
.onfinish = function() {
letter.dom.animate(keyframes.in, options)
letter.dom.textContent = char
}
}</code></pre>
<p>至此,全部编码完成。解读 JS 代码和解读 CSS 代码不一样,因为不是每一行代码都有视觉效果,很难用语言描述。如果你有不理解的地方,一定是我没有讲清楚,那么请你多看几遍视频,仔细体会。</p>
<p>在前端每日实战的<a href="https://segmentfault.com/a/1190000017064196">第 162 号作品</a>中也曾使用过 Web Animation API,但那个作品的业务逻辑比这个要复杂,你在理解了这个作品之后若还想再挑战一下,可以再去参考它。</p>
<p>大功告成!</p>
前端每日实战 167# 视频演示如何用 1 个 dom 元素创作两颗爱心
https://segmentfault.com/a/1190000019238347
2019-05-20T17:23:41+08:00
2019-05-20T17:23:41+08:00
comehope
https://segmentfault.com/u/comehope
21
<p><img src="/img/bVbsSVm?w=400&h=348" alt="图片描述" title="图片描述"></p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/KLvENb">https://codepen.io/comehope/pen/KLvENb</a></p>
<h2>可交互视频</h2>
<p>此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。</p>
<p>请用 chrome, safari, edge 打开观看。</p>
<p><a href="https://link.segmentfault.com/?enc=onJhJUo7V04bFccvST6kdw%3D%3D.DXtUre3FtbsWPNa2PA61xtZojrQdLHLz6YMF7k658NKGukqGM4xyDg%2BxzRAGxd9%2F" rel="nofollow">https://scrimba.com/p/pEgDAM/cJ8vrMt2</a></p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=Nr4kJDhs1msQdMdF72wJAg%3D%3D.7EH287gj5VV19O%2FukSs%2Fbp65ukd%2BYkOc9bBsRFo7Fg0qRjPnCzdoi78z%2FhFdp52UukfaFNQJB4QSxgebpr2mxA%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<h3>一、绘制一个圆点</h3>
<p>定义 dom 结构,只有一个 dom 元素,名为 <code>.heart</code>:</p>
<pre><code class="html"><figure class="heart"></figure></code></pre>
<p>让元素居中显示,设置页面背景色为浅粉红渐变色:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(lightpink, white);
}</code></pre>
<p>画出一个红色圆点:</p>
<pre><code class="css">.heart {
font-size: 30px;
width: 1em;
height: 1em;
background-color: red;
border-radius: 50%;</code></pre>
<h3>二、制作一个点阵图形和它的投影</h3>
<p>我们要制作一个点阵图形,这个手法在<a href="https://segmentfault.com/a/1190000015444368">第 67 号作品</a>中曾有应用。原理是利用 box-shadow 的特性,如果你已经了解了这个手法,可以直接滚屏到下一节,如果还不了解,做下面这几个小实验就明白了。</p>
<p>给 <code>.heart</code> 设置下面的阴影,则在红色圆点的右侧会出现一个等大的黑点:</p>
<pre><code class="css">.heart {
box-shadow:
1.1em 0;
}</code></pre>
<p>继续在红点左侧画一个黑点:</p>
<pre><code class="css">.heart {
box-shadow:
1.1em 0,
-1.1em 0;
}</code></pre>
<p>再继续,在红点的下方画一个黑点:</p>
<pre><code class="css">.heart {
box-shadow:
1.1em 0,
-1.1em 0,
0 1.1em;
}</code></pre>
<p>再在红点的上方画一个黑点:</p>
<pre><code class="css">.heart {
box-shadow:
1.1em 0,
-1.1em 0,
0 1.1em,
0 -1.1em;
}</code></pre>
<p>现在你可以看到,4 个黑点组成一个十字形,而红点在这个十字形交叉点的位置。这个技巧就是把屏幕当作一个以红点为原点的平面坐标系,水平方向右侧为正,垂直方向下方为正,与浏览器的坐标体系一致。每一个阴影属性值就可以绘制出一个圆点,因为 <code>box-shadow</code> 可以接收多个属性性,所以就可以用多个圆点来画点阵图了。</p>
<p>为了让代码更直观,我们可以把代码的布局安排得和图案的形状一样,下面 <code>box-shadow</code> 的 4 个属性值用来画出一个十字形:</p>
<pre><code class="css">.heart {
box-shadow:
0 -1.1em,
-1.1em 0, 1.1em 0,
0 1.1em;
}</code></pre>
<p>因为没有给阴影指定颜色,所以它默认是黑色,要把它改成红色,并不需要在 <code>box-shadow</code> 赋值,基于 CSS 的继承原理,直接给元素指定一个颜色,阴影就采用相同的颜色了:</p>
<pre><code class="css">.heart {
color: red;
background-color: currentColor;
}</code></pre>
<p>大红太艳了,让它的颜色谈一点:</p>
<pre><code class="css">.heart {
color: hsla(0, 100%, 50%, 0.6);
}</code></pre>
<p>接下来再用一个技巧,用 <code>drop-shadow()</code> 把整个图案再复制出一份:</p>
<pre><code class="css">.heart {
filter: drop-shadow(3.3em 2.2em dodgerblue);
}</code></pre>
<h3>三、绘制心形图案</h3>
<p>有了上面的知识储备,绘制心形就只是一个工夫活了,花一点时间在纸上画一画,数一数,就可以创作点阵图形了。这里用到的心形是 9 * 8 点阵的:</p>
<pre><code class="css">.heart {
--heart-shape:
-3.3em -3.3em, -2.2em -3.3em, 2.2em -3.3em, 3.3em -3.3em,
-4.4em -2.2em, -3.3em -2.2em, -2.2em -2.2em, -1.1em -2.2em, 1.1em -2.2em, 2.2em -2.2em, 3.3em -2.2em, 4.4em -2.2em,
-4.4em -1.1em, -3.3em -1.1em, -2.2em -1.1em, -1.1em -1.1em, 0em -1.1em, 1.1em -1.1em, 2.2em -1.1em, 3.3em -1.1em, 4.4em -1.1em,
-4.4em 0em, -3.3em 0em, -2.2em 0em, -1.1em 0em, 0em 0em, 1.1em 0em, 2.2em 0em, 3.3em 0em, 4.4em 0em,
-3.3em 1.1em, -2.2em 1.1em, -1.1em 1.1em, 0em 1.1em, 1.1em 1.1em, 2.2em 1.1em, 3.3em 1.1em,
-2.2em 2.2em, -1.1em 2.2em, 0em 2.2em, 1.1em 2.2em, 2.2em 2.2em,
-1.1em 3.3em, 0em 3.3em, 1.1em 3.3em,
0em 4.4em;
box-shadow: var(--heart-shape);
}</code></pre>
<p>哈哈,这一大片代码真是让人头晕啊,但是如果是在编辑器里看,它就是一个心形呢,像下面这样,这就是传说中的所见即所得吧:</p>
<p><img src="/img/bVbsSVz?w=2076&h=1694" alt="图片描述" title="图片描述"></p>
<p>至此,我们得到了一颗红心和一颗蓝心。</p>
<h3>四、设计动画效果</h3>
<p>定义 2 组关键帧,分别用于红心和蓝心,红心的帧效果就是 <code>box-shadow</code> 属性的代码,蓝心的帧效果则是 <code>drop-shadow()</code> 的代码:</p>
<pre><code class="css">.heart {
/* box-shadow: var(--heart-shape); */
/* color: hsla(0, 100%, 50%, 0.6); */
/* filter: drop-shadow(3.3em 2.2em dodgerblue); */
}
@keyframes heart-one {
to {
box-shadow: var(--heart-shape);
color: hsla(0, 100%, 50%, 0.6);
}
}
@keyframes heart-two {
to {
filter: drop-shadow(3.3em 2.2em dodgerblue);
}
}</code></pre>
<p>最后,定义动画属性,注意蓝心有 <code>0.3s</code> 的延迟:</p>
<pre><code class="css">.heart {
animation:
heart-one 1s infinite alternate cubic-bezier(0.5, 1.7, 0.5, 1.5),
heart-two 1s 0.3s infinite alternate cubic-bezier(0.5, 1.7, 0.25, 1);
}</code></pre>
<p>大功告成!</p>
前端每日实战:166# 视频演示如何用 CSS 创作一个 Safari LOGO
https://segmentfault.com/a/1190000019216340
2019-05-17T18:02:59+08:00
2019-05-17T18:02:59+08:00
comehope
https://segmentfault.com/u/comehope
15
<p><img src="/img/bVbsTZD?w=400&h=399" alt="图片描述" title="图片描述"></p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/rgmPLR">https://codepen.io/comehope/pen/rgmPLR</a></p>
<h2>可交互视频</h2>
<p>此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。</p>
<p>请用 chrome, safari, edge 打开观看。</p>
<p><a href="https://link.segmentfault.com/?enc=nNB2MWjwd9TYC2rvEGsjzA%3D%3D.VIrhMqLtYTiXbO4aeOWkNWzChLrVeg4e9hkldrMDktjBqsgOIxOhCLZDJg9BsOBy" rel="nofollow">https://scrimba.com/p/pEgDAM/c2LBPPtg</a></p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=8kAepF1lkiJgh47iRTXJ9w%3D%3D.o56XqsOTGYQxm69ebInYA9CidPxOtJxzeRlhmOBru%2BnnFMYGRuHmLYgF7QmEoBi9iGPj6Cnzv57fM%2BkG6HkGNA%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<h3>容器基本属性</h3>
<p>Safari 浏览器的 LOGO 是一个指南针的形状,它的主要元素有 2 个,一个是围绕在表盘周围的刻度线,一个是中间的指针。所以我们定义 dom 结构如下,其中 <code>.marks</code> 代表刻度线,<code>.pointer</code> 代表指针。<code>.marks</code> 中有 4 个 <code><span></code> 元素,它们代表刻度线,实际的刻度线有几十条,这里只定义 4 条,目的是便于书写样式,等样式写好后,接下来会用 JavaScript 批量生成刻度线:</p>
<pre><code class="html"><figure class="safari">
<div class="marks">
<span></span>
<span></span>
<span></span>
<span></span>
</div>
<div class="pointer"></div>
</figure></code></pre>
<p>让作品显示在页面正中,页面背景为黑色:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: black;
}</code></pre>
<p>LOGO 容器是一个白色的圆角正方形。作品将使用 <code>em</code> 作为长度单位,如果想修改 LOGO 的尺寸,只要修改这里的 <code>font-size</code> 属性就可以了。用 <code>flex</code> 布局令其中的子元素 <code>.marks</code> 和 <code>.pointer</code> 都居中显示:</p>
<pre><code class="css">.safari {
font-size: 10px;
width: 15em;
height: 15em;
background-color: snow;
border-radius: 25%;
padding: 1em;
display: flex;
align-items: center;
justify-content: center;
}</code></pre>
<h3>绘制刻度线</h3>
<p>先绘制出刻度线所在的表盘,用线性渐变填充上蓝色渐变色:</p>
<pre><code class="css">.marks {
width: inherit;
height: inherit;
background-image: linear-gradient(
hsl(191, 98%, 55%),
hsl(220, 88%, 53%)
);
border-radius: 50%;
}</code></pre>
<p>再绘制出刻度线。围绕着一个圆周绘图的技巧是先令一组元素逐个旋转较小的角度(用 <code>rotate()</code> 函数实现),再让这些元素向旋转的方向移动(用 <code>translate()</code> 函数实现)。这里用变量 <code>--rotate-deg</code> 存储旋转的角度:</p>
<pre><code class="css">.marks {
display: flex;
align-items: center;
justify-content: center;
}
.marks span {
position: absolute;
width: 0.1em;
height: 0.9em;
background-color: snow;
transform: rotate(var(--rotate-deg)) translateY(6em);
}
.marks span:nth-child(1) {--rotate-deg: 0deg;}
.marks span:nth-child(2) {--rotate-deg: 90deg;}
.marks span:nth-child(3) {--rotate-deg: 180deg;}
.marks span:nth-child(4) {--rotate-deg: 270deg;}</code></pre>
<p>现在可以看到 4 条刻度线分别定位到表盘的上、下、左、右的边缘位置了。</p>
<h3>用 Javascript 批量生成刻度线</h3>
<p>因为刻度线有很多条,为了减少代码量,我们用 JavaScript 来批量创建刻度线。</p>
<p>在此之前,先删除掉 html 中的声明 <code><span></code> 元素的 4 行代码:</p>
<pre><code class="html"><figure class="safari">
<div class="marks">
<!-- <span></span>
<span></span>
<span></span>
<span></span> -->
</div>
<div class="pointer"></div>
</figure></code></pre>
<p>再删除 css 中设置刻度线角度的代码:</p>
<pre><code class="css">/* .marks span:nth-child(1) {--rotate-deg: 0deg;}
.marks span:nth-child(2) {--rotate-deg: 90deg;}
.marks span:nth-child(3) {--rotate-deg: 180deg;}
.marks span:nth-child(4) {--rotate-deg: 270deg;} */</code></pre>
<p>然后用 js 来批量创建 60 条刻度线:</p>
<pre><code class="js">const MARKS_COUNT = 60
Array(MARKS_COUNT).fill('').forEach((x, i) => {
let span = document.createElement('span')
span.style.setProperty('--rotate-deg', i * 360 / MARKS_COUNT + 'deg')
document.querySelector('.marks').appendChild(span)
})</code></pre>
<p>这里稍复杂的是表达式 <code>i * 360 / MARKS_COUNT + 'deg'</code>,其中 <code>360 / MARKS_COUNT </code> 是把一个圆周的 360 度分成若干份(也就是刻度线数量那么多的份数)之后每一份的角度,再用每一份的下标值 <code>i</code> 去乘它,就得到每条刻度线应旋转的角度了。</p>
<p>接下来设置刻度线的细节,令刻度线长短交错。代表刻度线长度的变量是 <code>--h</code>,长线长 <code>0.9em</code>,短线长 <code>0.5em</code>,为了让刻度线对齐,再用变量 <code>--y</code> 存储偏移量,令长线偏移 <code>6em</code>,短线偏移 <code>6.2em</code>。同时修改 <code>height</code> 属性和 <code>translateY()</code> 函数,让它们引用这 2 个变量的值。因为刻度线长短交错,所以用 <code>:nth-child(odd)</code> 和 <code>:nth-child(even)</code> 来设置 2 组不同的参数值:</p>
<pre><code class="css">.marks span {
height: var(--h);
transform: rotate(var(--rotate-deg)) translateY(var(--y));
}
.marks span:nth-child(odd) {--h: 0.9em; --y: 6em;}
.marks span:nth-child(even) {--h: 0.5em; --y: 6.2em;}</code></pre>
<p>至此,刻度线绘制完成。</p>
<h3>绘制指针</h3>
<p>指针是由 2 个三角形组成的,对于这种成对的元素,通常都用伪元素绘制。先确定一下指针的尺寸,用 <code>flex</code> 令它的子元素(也就是 2 个伪元素)纵向排列:</p>
<pre><code class="css">.pointer {
position: absolute;
width: 1.4em;
height: 12em;
display: flex;
flex-direction: column;
}</code></pre>
<p>绘制三角形的技巧是令容器的尺寸为 <code>0</code> 宽 <code>0</code> 高,然后用 3 条边框构成三角形,要是看不懂这段代码的话,动手试试就明白了。这里也定义了一个变量 <code>--c</code>,用于存储 2 个三角形的颜色,分别是红色和白色:</p>
<pre><code class="css">.pointer::before,
.pointer::after {
content: '';
border-bottom: 6em solid var(--c);
border-left: 0.7em solid transparent;
border-right: 0.7em solid transparent;
}
.pointer::after {
transform: rotate(180deg);
}
.pointer::before {--c: crimson;}
.pointer::after {--c: snow;}</code></pre>
<p>到这里,指针绘制完成,整个 LOGO 的形状也已经完成了。</p>
<p>最后,加一点动画效果,让指针像指南针那样转动。原理很简单,就是让指针在 <code>30</code> 度到 <code>50</code> 度之间来回摆动:</p>
<pre><code class="css">.pointer {
transform: rotate(30deg);
animation: rotate 1s ease-in-out infinite alternate;
}
@keyframes rotate {
to {
transform: rotate(50deg);
}
}</code></pre>
<p>大功告成!</p>
前端每日实战:165# 视频演示如何用 Vue 创作一个算术训练程序(内含 3 个视频)
https://segmentfault.com/a/1190000017539885
2018-12-27T17:07:05+08:00
2018-12-27T17:07:05+08:00
comehope
https://segmentfault.com/u/comehope
21
<p><img src="/img/bVblK5u?w=400&h=301" alt="图片描述" title="图片描述"></p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/dwzRyQ">https://codepen.io/comehope/pen/dwzRyQ</a></p>
<h2>可交互视频</h2>
<p>此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。</p>
<p>请用 chrome, safari, edge 打开观看。</p>
<p>第 1 部分:<br><a href="https://link.segmentfault.com/?enc=RfouSTsQpFgtz%2Fv%2BI5uRiA%3D%3D.pLaWUGf4vw%2Baa5Sv1xPvhiqIVJnxuxXo0slQWdOtA64GI%2BbZBFzPSBdgMrJQ0xoa" rel="nofollow">https://scrimba.com/p/pEgDAM/ca6wWSk</a></p>
<p>第 2 部分:<br><a href="https://link.segmentfault.com/?enc=hWXu%2Bv2%2FZs%2BibM1m0M3LIA%3D%3D.desMUw3dmurilYdQCOsbOISgUC0ZV6gZzhi5dOSbkttu4cemXl9bQxLdvVS06m%2BK" rel="nofollow">https://scrimba.com/p/pEgDAM/c7Zy2AZ</a></p>
<p>第 3 部分:<br><a href="https://link.segmentfault.com/?enc=vbOCtbVndzKJR7RtDKZfLw%3D%3D.15pAwfUSriP7j6FRGZyo64aFPdcHE5fuozhwOvwuKsah2EmoMXxzxGV%2FBwuH9e4i" rel="nofollow">https://scrimba.com/p/pEgDAM/c9R2Gsy</a></p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=7TcbWNdls73s5F%2Fb%2BZE5Kw%3D%3D.vjjS4x9kvQ%2FTwRvnbdheqgBNqiYsDZ20T6HrrUY9cJ9Yu3QQS9utbSAeujWfiGX0duJc86KmrcFxrH6SUNHMAw%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<p>本项目可以训练加、减、乘、除四则运算。比如训练加法时,界面给出 2 个数值表示 2 个加数,小朋友心算出结果后大声说出,然后点击“?”按钮查看结果,根据对照的结果,如果计算正确(或错误),就点击绿勾(或红叉),然后再开始下一道测验。界面中还会显示已经做过几道题,正确率是多少。为了增强趣味性,加入了音效,答对时会响起小猫甜美的叫声,答错时响起的是小猫失望的叫声。</p>
<p>页面用纯 css 布局,程序逻辑用 vue 框架编写,用 howler.js 库播放音效。整个应用分成 4 个步骤实现:静态页面布局、加法的程序逻辑、四则运算的程序逻辑、音效处理。</p>
<h3>一、页面布局</h3>
<p>先创建 dom 结构,整个文档分成 4 部分,<code>.choose-type</code> 是一组多选一按钮,用于选择四则运算的类型,<code>.score</code> 是成绩统计数据,<code>.expression</code> 是一个算式,它也是游戏的主体部分,<code>.judgment</code> 用于判断答题是否正确:</p>
<pre><code class="html"><div id="app">
<div class="choose-type"></div>
<div class="score"></div>
<div class="expression"></div>
<div class="judgment"></div>
</div></code></pre>
<p><code>.choose-type</code> 一共包含 4 个 <code>input[type=radio]</code> 控件,命名为 <code>arithmetic-type</code>,加、减、乘、除 4 种运算类型的值分别为 1、2、3、4,每个控件后跟随一个对应的<code>label</code>,最终我们将把 <code>input</code> 控件隐藏起来,而让用户操作 <code>label</code>。</p>
<pre><code class="html"><div id="app">
<div class="choose-type">
<div class="choose-type">
<input type="radio" id="addition" name="arithmetic-type" value="1">
<label for="addition">addition</label>
<input type="radio" id="subtraction" name="arithmetic-type" value="2">
<label for="subtraction">subtraction</label>
<input type="radio" id="multiplication" name="arithmetic-type" value="3">
<label for="multiplication">multiplication</label>
<input type="radio" id="division" name="arithmetic-type" value="4">
<label for="division">division</label>
</div>
<!-- 略 -->
</div></code></pre>
<p><code>.score</code> 包含 2 个数据,一个是已经做过的题目数,一个是正确率:</p>
<pre><code class="html"><div id="app">
<!-- 略 -->
<div class="score">
<span>ROUND 15</span>
<span>SCORE 88%</span>
</div>
<!-- 略 -->
</div></code></pre>
<p><code>.expression</code> 把一个表达式的各部分拆开,以便能修饰表达式各部分的样式。<code>.number</code> 表示等式左边的 2 个运算数,<code>.operation</code> 表示运算符和等号,<code>.show</code> 是一个问号,同时它也是一个按钮,当心算出结果后,点击它,就显示出 <code>.result</code> 元素,展示运算结果:</p>
<pre><code class="html"><div id="app">
<!-- 略 -->
<div class="expression">
<span class="number">10</span>
<span class="operation">+</span>
<span class="number">20</span>
<span class="operation">=</span>
<span class="button show">?</span>
<span class="result">30</span>
</div>
<!-- 略 -->
</div></code></pre>
<p><code>.judgment</code> 包含 2 个按钮,分别是表示正确的绿勾和表示错误的红叉,显示在结果的下方:</p>
<pre><code class="html"><div id="app">
<!-- 略 -->
<div class="judgment">
<span class="button right">✔</span>
<span class="button wrong">✘</span>
</div>
</div></code></pre>
<p>至此,完整的 dom 结构如下:</p>
<pre><code class="html"><div id="app">
<div class="choose-type">
<input type="radio" id="addition" name="arithmetic-type" value="1">
<label for="addition">addition</label>
<input type="radio" id="subtraction" name="arithmetic-type" value="2">
<label for="subtraction">subtraction</label>
<input type="radio" id="multiplication" name="arithmetic-type" value="3">
<label for="multiplication">multiplication</label>
<input type="radio" id="division" name="arithmetic-type" value="4">
<label for="division">division</label>
</div>
<div class="score">
<span>ROUND 15</span>
<span>SCORE 88%</span>
</div>
<div class="expression">
<span class="number">10</span>
<span class="operation">+</span>
<span class="number">20</span>
<span class="operation">=</span>
<span class="button show">?</span>
<span class="result">30</span>
</div>
<div class="judgment">
<span class="button right">✔</span>
<span class="button wrong">✘</span>
</div>
</div></code></pre>
<p>接下来用 css 布局。<br>居中显示:</p>
<pre><code class="css">body{
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(lightyellow, tan);
}</code></pre>
<p>设置应用的容器样式,黑色渐变背景,子元素纵向排列,尺寸用相对单位 <code>vw</code> 和 <code>em</code>,以便在窗口缩放后能自适应新窗口尺寸:</p>
<pre><code class="css">#app {
width: 66vmin;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 1em 4em rgba(0, 0, 0, 0.5);
border-radius: 2em;
padding: 8em 5em;
background: linear-gradient(black, dimgray, black);
font-family: sans-serif;
font-size: 1vw;
user-select: none;
}</code></pre>
<p>布局 <code>.choose-type</code> 区域。隐藏 <code>input</code> 控件,设置 <code>label</code> 为天蓝色:</p>
<pre><code class="css">.choose-type input[name=arithmetic-type] {
position: absolute;
visibility: hidden;
}
.choose-type label {
font-size: 2.5em;
color: skyblue;
margin: 0.3em;
letter-spacing: 0.02em;
}</code></pre>
<p>在 <code>label</code> 之间加入分隔线:</p>
<pre><code class="css">.choose-type label {
position: relative;
}
.choose-type label:not(:first-of-type)::before {
content: '|';
position: absolute;
color: skyblue;
left: -0.5em;
filter: opacity(0.6);
}</code></pre>
<p>设置 <code>label</code> 在鼠标悬停时变色,当 <code>input</code> 控件被选中时对应的 <code>label</code> 会变色、首字母变大写并显示下划线,为了使视觉效果切换平滑,设置了缓动时间。这里没有使用 <code>text-decoration: underline</code> 设置下划线,是因为用 <code>border</code> 才有缓动效果:</p>
<pre><code class="css">.choose-type label {
transition: 0.3s;
}
.choose-type label:hover {
color: deepskyblue;
cursor: pointer;
}
.choose-type input[name=arithmetic-type]:checked + label {
text-transform: capitalize;
color: deepskyblue;
border-style: solid;
border-width: 0 0 0.1em 0;
}</code></pre>
<p><code>.score</code> 区域用银色字,2 组数据之间留出一些间隔:</p>
<pre><code class="css">.score{
font-size: 2em;
color: silver;
margin: 1em 0 2em 0;
width: 45%;
display: flex;
justify-content: space-between;
}</code></pre>
<p><code>.expression</code> 区域用大字号,各元素用不同的颜色区分:</p>
<pre><code class="css">.expression {
font-size: 12em;
display: flex;
align-items: center;
}
.expression span {
margin: 0 0.05em;
}
.expression .number{
color: orange;
}
.expression .operation{
color: skyblue;
}
.expression .result{
color: gold;
}</code></pre>
<p><code>.show</code> 是等号右边的问号,它同时也是一个按钮,在这里把按钮的样式 <code>.button</code> 独立出来,因为后面还会用到 <code>.button</code> 样式:</p>
<pre><code class="css">.expression .show {
color: skyblue;
font-size: 0.8em;
line-height: 1em;
width: 1.5em;
text-align: center;
}
.button {
background-color: #222;
border: 1px solid #555;
padding: 0.1em;
}
.button:hover {
background-color: #333;
cursor: pointer;
}
.button:active {
background-color: #222;
}</code></pre>
<p>设置 <code>.judgment</code> 区域 2 个按钮的样式,它们还共享了 <code>.button</code> 样式:</p>
<pre><code class="css">.judgment {
font-size: 8em;
align-self: flex-end;
}
.judgment .wrong {
color: orangered;
}
.judgment .right {
color: lightgreen;
}</code></pre>
<p>至此,静态页面布局完成,完整的 css 代码如下:</p>
<pre><code class="css">body{
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(lightyellow, tan);
}
#app {
width: 66vw;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 1em 4em rgba(0, 0, 0, 0.5);
border-radius: 2em;
padding: 8em 5em;
background: linear-gradient(black, dimgray, black);
font-family: sans-serif;
font-size: 1vw;
user-select: none;
}
.choose-type input[name=arithmetic-type] {
position: absolute;
visibility: hidden;
}
.choose-type label {
font-size: 2.5em;
color: skyblue;
margin: 0.3em;
letter-spacing: 0.02em;
position: relative;
transition: 0.3s;
}
.choose-type label:not(:first-of-type)::before {
content: '|';
position: absolute;
color: skyblue;
left: -0.5em;
filter: opacity(0.6);
}
.choose-type label:hover {
color: deepskyblue;
cursor: pointer;
}
.choose-type input[name=arithmetic-type]:checked + label {
text-transform: capitalize;
color: deepskyblue;
border-style: solid;
border-width: 0 0 0.1em 0;
}
.score{
font-size: 2em;
color: silver;
margin: 1em 0 2em 0;
width: 45%;
display: flex;
justify-content: space-between;
}
.expression {
font-size: 12em;
display: flex;
align-items: center;
}
.expression span {
margin: 0 0.05em;
}
.expression .number{
color: orange;
}
.expression .operation{
color: skyblue;
}
.expression .result{
color: gold;
}
.expression .show {
color: skyblue;
font-size: 0.8em;
line-height: 1em;
width: 1.5em;
text-align: center;
}
.judgment {
font-size: 8em;
align-self: flex-end;
}
.judgment .wrong {
color: orangered;
}
.judgment .right {
color: lightgreen;
}
.button {
background-color: #222;
border: 1px solid #555;
padding: 0.1em;
}
.button:hover {
background-color: #333;
cursor: pointer;
}
.button:active {
background-color: #222;
}</code></pre>
<h3>二、加法的程序逻辑</h3>
<p>我们先用加法把流程跑通,再把加法扩展为四则运算。</p>
<p>引入 vue 框架:</p>
<pre><code class="html"><script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.21/vue.min.js"></script></code></pre>
<p>创建一个 Vue 对象:</p>
<pre><code class="javascript">let vm = new Vue({
el: '#app',
})</code></pre>
<p>定义数据,<code>round</code> 存储题目数,<code>round.all</code> 表示总共答过了多少道题,<code>round.right</code> 表示答对了多少道题;<code>numbers</code> 数组包含 2 个元素,用于存储等式左边的 2 个运算数,用数组是为了便于后面使用解构语法:</p>
<pre><code class="javascript">let vm = new Vue({
///...略
data: {
round: {all: 0, right: 0},
numbers: [0, 0],
}
///...略
})</code></pre>
<p>定义计算属性,<code>operation</code> 是操作符,目前是加号,<code>result</code> 是计算结果,等于 2 个运算数相加,<code>score</code> 是正确率,开始做第一题时正确率显示为 100%,后续根据实际答对的题数计算正确率:</p>
<pre><code class="javascript">let vm = new Vue({
///...略
computed: {
operation: function() {
return '+'
},
result: function() {
return this.numbers[0] + this.numbers[1]
},
score: function() {
return this.round.all == 1
? 100
: Math.round(this.round.right / (this.round.all - 1) * 100)
}
},
///...略
})</code></pre>
<p>把数据绑定到 html 模板中:</p>
<pre><code class="html"><div id="app">
<!-- 略 -->
<div class="score">
<span>ROUND {{round.all - 1}}</span>
<span>SCORE {{score}}%</span>
</div>
<div class="expression">
<span class="number">{{numbers[0]}}</span>
<span class="operation">{{operation}}</span>
<span class="number">{{numbers[1]}}</span>
<span class="operation">=</span>
<span class="button show">?</span>
<span class="result">{{result}}</span>
</div>
<!-- 略 -->
</div></code></pre>
<p>至此,页面中的数据都是动态获取的了。</p>
<p>等式右边的问号和结果不应同时显示出来,在用户思考时应显示问号,思考结束后应隐藏问号显示结果。为此,增加一个 <code>isThinking</code> 变量,用于标志用户所处的状态,默认为 <code>true</code>,即进入游戏时,用户开始思考第 1 道题目:</p>
<pre><code class="javascript">let vm = new Vue({
///...略
data: {
round: {all: 0, right: 0},
numbers: [0, 0],
isThinking: true,
},
///...略
})</code></pre>
<p>把 <code>isThinking</code> 绑定到 html 模板中,用户思考时只显示问号 <code>.show</code>,否则显示结果 <code>.result</code> 和判断结果正确与否的按钮 <code>.judgment</code>,此处请注意,对于占据同一个视觉位置的元素,用 <code>v-show=false</code>,即 <code>display: none</code> 隐藏,对于占据独立视觉位置的元素,用 <code>visibility: hidden</code> 隐藏:</p>
<pre><code class="html"><div id="app">
<!-- 略 -->
<div class="expression">
<!-- 略 -->
<span class="button show" v-show="isThinking">?</span>
<span class="result" v-show="!isThinking">{{result}}</span>
</div>
<div class="judgment" :style="{visibility: isThinking ? 'hidden' : 'visible'}">
<!-- 略 -->
</div>
</div></code></pre>
<p>接下来生成随机运算数。创建一个 <code>next()</code> 方法用于开始下一个题目,那么在页面载入后就应执行这个方法初始化第 1 道题目:</p>
<pre><code class="javascript">let vm = new Vue({
///...略
methods: {
next: function() {
},
},
})
window.onload = vm.next</code></pre>
<p><code>next()</code> 方法一方面要负责初始化运算数,还要把答过的题目数加1,这里独立出来一个 <code>newRound()</code> 方法是为了方便后面复用它:</p>
<pre><code class="javascript">let vm = new Vue({
///...略
methods: {
newRound: function() {
this.numbers = this.getNumbers()
this.isThinking = true
},
next: function() {
this.newRound()
this.round.all++
},
},
})</code></pre>
<p><code>getNumbers()</code> 方法用于生成 2 个随机数,它调用 <code>getRandomNumber()</code> 方法来生成一个随机数,其中 <code>level</code> 参数表示随机数的取值范围,<code>level</code> 为 1 时,生成的随机数介于 1 ~ 9 之间,<code>level</code> 为 2 时,生成的随机数介于 10 ~ 99 之间。为了增加一点加法的难度,我们把 <code>level</code> 设置为 2:</p>
<pre><code class="javascript">let vm = new Vue({
///...略
methods: {
getRandomNumber: function(level) {
let min = Math.pow(10, level - 1)
let max = Math.pow(10, level)
return min + Math.floor(Math.random() * (max - min))
},
getNumbers: function() {
let level = 2
let a = this.getRandomNumber(level)
let b = this.getRandomNumber(level)
return [a, b]
},
newRound: function() {
this.numbers = this.getNumbers()
this.isThinking = true
},
next: function() {
this.newRound()
this.round.all++
},
},
})</code></pre>
<p>此时,每刷新一次页面,运算数就会跟着刷新,因为每次页面加载都会运行 <code>vm.next()</code> 方法生成新的随机数。<br>接下来我们来处理按钮事件,页面中一共有 3 个按钮:问号按钮 <code>.show</code> 被点击后应显示结果;绿勾按钮 <code>.right</code> 被点击后应给答对题的数目加 1,然后进入下一道题;红叉按钮 <code>.wrong</code> 被点击后直接进入下一道题,所以我们在程序中增加 3 个方法,<code>getResult()</code>、<code>answerRight()</code>、<code>answerWrong</code> 分别对应上面的 3 个点击事件:</p>
<pre><code class="javascript">let vm = new Vue({
///...略
methods: {
///...略
getResult: function() {
this.isThinking = false
},
answerRight: function() {
this.round.right++
this.next()
},
answerWrong: function() {
this.next()
},
},
})</code></pre>
<p>把事件绑定到 html 模板:</p>
<pre><code class="html"><div id="app">
<!-- 略 -->
<div class="expression">
<!-- 略 -->
<span class="button show" v-show="isThinking" @click="getResult">?</span>
<!-- 略 -->
</div>
<div class="judgment" :style="{visibility: isThinking ? 'hidden' : 'visible'}">
<span class="button right" @click="answerRight">✔</span>
<span class="button wrong" @click="answerWrong">✘</span>
</div>
</div></code></pre>
<p>至此,加法程序就全部完成了,可以一道又一道题一直做下去。<br>此时的 html 代码如下:</p>
<pre><code class="html"><div id="app">
<div class="choose-type">
<!-- 没有改变 -->
</div>
<div class="score">
<span>ROUND {{round.all - 1}}</span>
<span>SCORE {{score}}%</span>
</div>
<div class="expression">
<span class="number">{{numbers[0]}}</span>
<span class="operation">{{operation}}</span>
<span class="number">{{numbers[1]}}</span>
<span class="operation">=</span>
<span class="button show" v-show="isThinking" @click="getResult">?</span>
<span class="result" v-show="!isThinking">{{result}}</span>
</div>
<div class="judgment" :style="{visibility: isThinking ? 'hidden' : 'visible'}">
<span class="button right" @click="answerRight">✔</span>
<span class="button wrong" @click="answerWrong">✘</span>
</div>
</div></code></pre>
<p>此时的 javascript 代码如下:</p>
<pre><code class="javascript">let vm = new Vue({
el: '#app',
data: {
round: {all: 0, right: 0},
numbers: [0, 0],
isThinking: true,
},
computed: {
operation: function() {
return '+'
},
result: function() {
return this.numbers[0] + this.numbers[1]
},
score: function() {
return this.round.all == 1
? 100
: Math.round(this.round.right / (this.round.all - 1) * 100)
}
},
methods: {
getRandomNumber: function(level) {
let min = Math.pow(10, level - 1)
let max = Math.pow(10, level)
return min + Math.floor(Math.random() * (max - min))
},
getNumbers: function() {
let level = 2
let a = this.getRandomNumber(level)
let b = this.getRandomNumber(level)
return [a, b]
},
newRound: function() {
this.numbers = this.getNumbers()
this.isThinking = true
},
next: function() {
this.newRound()
this.round.all++
},
getResult: function() {
this.isThinking = false
},
answerRight: function() {
this.round.right++
this.next()
},
answerWrong: function() {
this.next()
},
},
})
window.onload = vm.next</code></pre>
<h3>三、四则运算的程序逻辑</h3>
<p>我们先来评估一下四种运算在这个程序里会在哪些方面有差异。首先,运算符不同,加、减、乘、除的运算符分别是“+”、“-”、“×”、“÷”;第二是运算函数不同,这个不用多说。根据这 2 点,我们定义一个枚举对象 <code>ARITHMETIC_TYPE</code>,用它存储四种运算的差异,每个枚举对象有 2 个属性,<code>operation</code> 代表操作符,<code>f()</code> 函数是运算逻辑。另外,我们再声明一个变量 <code>arithmeticType</code>,用于存储用户当前选择的运算类型:</p>
<pre><code class="javascript">let vm = new Vue({
///...略
data: {
///...略
ARITHMETIC_TYPE: {
ADDITION: 1,
SUBTRACTION: 2,
MULTIPLICATION: 3,
DIVISION: 4,
properties: {
1: {operation: '+', f: ([x, y]) => x + y},
2: {operation: '-', f: ([x, y]) => x - y},
3: {operation: '×', f: ([x, y]) => x * y},
4: {operation: '÷', f: ([x, y]) => x / y}
}
},
arithmeticType: 1,
},
})</code></pre>
<p>改造计算属性中关于运算符和计算结果的函数:</p>
<pre><code class="javascript">let vm = new Vue({
///...略
computed: {
///...略
operation: function() {
// return '+'
return this.ARITHMETIC_TYPE.properties[this.arithmeticType].operation
},
result: function() {
// return this.numbers[0] + this.numbers[1]
return this.ARITHMETIC_TYPE.properties[this.arithmeticType].f(this.numbers)
},
///...略
},
})</code></pre>
<p>因为上面 2 个计算属性都用到了 <code>arithmeticType</code> 变量,所以当用户选择运算类型时,这 2 个计算属性的值会自动更新。另外,为了让 ui 逻辑更严密,我们令 <code>arithmeticType</code> 的值改变时,开始一个新题目:</p>
<pre><code class="javascript">let vm = new Vue({
///...略
watch: {
arithmeticType: function() {
this.newRound()
}
}
})</code></pre>
<p>然后,把 <code>arithmeticType</code> 变量绑定到 html 模板中的 <code>input</code> 控件上:</p>
<pre><code class="html"><div id="app">
<div class="choose-type">
<input type="radio" id="addition" name="arithmetic-type" value="1" v-model="arithmeticType">
<label for="addition">addition</label>
<input type="radio" id="subtraction" name="arithmetic-type" value="2" v-model="arithmeticType">
<label for="subtraction">subtraction</label>
<input type="radio" id="multiplication" name="arithmetic-type" value="3" v-model="arithmeticType">
<label for="multiplication">multiplication</label>
<input type="radio" id="division" name="arithmetic-type" value="4" v-model="arithmeticType">
<label for="division">division</label>
</div>
<!-- 略 -->
</div></code></pre>
<p>至此,当选择不同的运算类型时,表达式的运算符和计算结果都会自动更新为匹配的值,比如选择乘法时,运算符就变为乘号,运算结果为 2 个运算数的乘积。<br>不过,此时的最明显的问题是,除法的运算数因为是随机生成的,商经常是无限小数,为了更合理,我们规定这里的除法只做整除运算。再延伸一下,对于减法,为了避免差为负数,也规定被减数不小于减数。<br>解决这个问题的办法是在 <code>ARITHMETIC_TYPE</code> 枚举中添加一个 <code>gen()</code> 函数,用于存储生成运算数的逻辑,<code>gen()</code> 函数接收一个包含 2 个随机数的数组作为参数,对于加法和乘法,直接返回数组本身,减法的 <code>gen()</code> 函数为 <code>gen: ([a, b]) => a >= b ? [a, b] : [b, a]</code>,除法的 <code>gen()</code> 函数为 <code>gen: ([a, b]) => [a * b, b]</code>,经过如此处理的运算数,就可以实现上面规定的逻辑了。改造后的 <code>ARITHMETIC_TYPE</code> 如下:</p>
<pre><code class="javascript">let vm = new Vue({
///...略
data: {
///...略
ARITHMETIC_TYPE: {
ADDITION: 1,
SUBTRACTION: 2,
MULTIPLICATION: 3,
DIVISION: 4,
pproperties: {
1: {operation: '+', f: (arr) => arr, gen: ([a, b]) => [a, b]},
2: {operation: '-', f: ([x, y]) => x - y, gen: ([a, b]) => a >= b ? [a, b] : [b, a]},
3: {operation: '×', f: (arr) => arr, gen: ([a, b]) => [a, b]},
4: {operation: '÷', f: ([x, y]) => x / y, gen: ([a, b]) => [a * b, b]}
}
},
///...略
},
///...略
})</code></pre>
<p>然后,在 <code>getNumbers()</code> 中调用 <code>gen()</code> 方法:</p>
<pre><code class="javascript">let vm = new Vue({
///...略
methods: {
///...略
getNumbers: function() {
let level = 2
let a = this.getRandomNumber(2)
let b = this.getRandomNumber(2)
// return [a, b]
return this.ARITHMETIC_TYPE.properties[this.arithmeticType].gen([a, b])
},
///...略
},
///...略
})</code></pre>
<p>至此,减法可以保证差不为负数,除法也可以保证商是整数了。<br>接下来,我们来配置训练难度。对大多数人来说,2 个二位数的加减法不是很难,但是 2 个二位数的乘除法的难度就大多了。在生成随机数时,因为定义了 <code>level=2</code>,所以取值范围固定是 11 ~ 99,我们希望能够灵活配置每个运算数的取值范围,为此,我们需要再为 <code>ARITHMETIC_TYPE</code> 枚举中增加一个 <code>level</code> 属性,用于表示随机数的取值范围,它是一个包含 2 个元素的数组,分别表示 2 个运算数的取值范围,改造后的 <code>ARITHMETIC_TYPE</code> 如下:</p>
<pre><code class="javascript">let vm = new Vue({
///...略
data: {
///...略
ARITHMETIC_TYPE: {
ADDITION: 1,
SUBTRACTION: 2,
MULTIPLICATION: 3,
DIVISION: 4,
properties: {
1: {operation: '+', f: ([x, y]) => x + y, gen: (arr) => arr, level: [3, 2]},
2: {operation: '-', f: ([x, y]) => x - y, gen: ([a, b]) => a >= b ? [a, b] : [b, a], level: [3, 2]},
3: {operation: '×', f: ([x, y]) => x * y, gen: (arr) => arr, level: [2, 1]},
4: {operation: '÷', f: ([x, y]) => x / y, gen: ([a, b]) => [a * b, b], level: [2, 1]}
}
},
///...略
},
///...略
})</code></pre>
<p>然后,把 <code>getNumbers()</code> 函数的 <code>level</code> 变量的值改为从枚举 <code>ARITHMETIC_TYPE</code> 中取值:</p>
<pre><code class="javascript">let vm = new Vue({
///...略
methods: {
getNumbers: function() {
let level = this.ARITHMETIC_TYPE.properties[this.arithmeticType].level
let a = this.getRandomNumber(level[0])
let b = this.getRandomNumber(level[1])
return this.ARITHMETIC_TYPE.properties[this.arithmeticType].gen([a, b])
},
///...略
},
///...略
})</code></pre>
<p>现在运行程序可以看到,加减法的 2 个运算数分别是 3 位数和 2 位数,而乘除法的 2 个运算数则分别是 2 位数和 1 位数,你也可以根据自己的需要来调整训练难度。<br>至此,四则运算的程序逻辑全部完成,此时的 javascript 代码如下:</p>
<pre><code class="javascript">let vm = new Vue({
el: '#app',
data: {
round: {all: 0, right: 0},
numbers: [0, 0],
isThinking: true,
ARITHMETIC_TYPE: {
ADDITION: 1,
SUBTRACTION: 2,
MULTIPLICATION: 3,
DIVISION: 4,
properties: {
1: {operation: '+', f: ([x, y]) => x + y, gen: (arr) => arr, level: 2},
2: {operation: '-', f: ([x, y]) => x - y, gen: ([a, b]) => a >= b ? [a, b] : [b, a], level: 2},
3: {operation: '×', f: ([x, y]) => x * y, gen: (arr) => arr, level: 1},
4: {operation: '÷', f: ([x, y]) => x / y, gen: ([a, b]) => [a * b, b], level: 1}
}
},
arithmeticType: 1,
},
computed: {
operation: function() {
return this.ARITHMETIC_TYPE.properties[this.arithmeticType].operation
},
result: function() {
return this.ARITHMETIC_TYPE.properties[this.arithmeticType].f(this.numbers)
},
score: function() {
return this.round.all == 1
? 100
: Math.round(this.round.right / (this.round.all - 1) * 100)
}
},
methods: {
getRandomNumber: function(level) {
let min = Math.pow(10, level - 1)
let max = Math.pow(10, level)
return min + Math.floor(Math.random() * (max - min))
},
getNumbers: function() {
let level = this.ARITHMETIC_TYPE.properties[this.arithmeticType].level
let a = this.getRandomNumber(level[0])
let b = this.getRandomNumber(level[1])
return this.ARITHMETIC_TYPE.properties[this.arithmeticType].gen([a, b])
},
newRound: function() {
this.numbers = this.getNumbers()
this.isThinking = true
},
next: function() {
this.newRound()
this.round.all++
},
getResult: function() {
this.isThinking = false
},
answerRight: function() {
this.round.right++
this.next()
},
answerWrong: function() {
this.next()
},
},
watch: {
arithmeticType: function() {
this.newRound()
}
}
})
window.onload = vm.next</code></pre>
<h3>四、音效处理</h3>
<p>引入 howler 库:</p>
<pre><code class="html"><script src="https://cdnjs.cloudflare.com/ajax/libs/howler/2.1.1/howler.min.js"></script></code></pre>
<p>声明变量 <code>sound</code>,它有 2 个属性 <code>right</code> 和 <code>wrong</code>,分别代表回答正确和错误时的音效,属性值是一个 <code>Howl</code> 对象,在构造函数中指定音频文件的 url:</p>
<pre><code class="javascript">let vm = new Vue({
///...略
data: {
///...略
sound: {
right: new Howl({src: ['https://freesound.org/data/previews/203/203121_777645-lq.mp3']}),
wrong: new Howl({src: ['https://freesound.org/data/previews/415/415209_5121236-lq.mp3']})
},
},
///...略
})</code></pre>
<p>在 <code>answerRight()</code> 方法和 <code>answerWrong()</code> 方法中分别调用播放声音的 <code>play()</code> 方法即可:</p>
<pre><code class="javascript">let vm = new Vue({
///...略
methods: {
///...略
answerRight: function() {
this.round.right++
this.sound.right.play()
this.next()
},
answerWrong: function() {
this.sound.wrong.play()
this.next()
},
///...略
})</code></pre>
<p>现在,当点击绿勾时,就会响起小猫甜美的叫声;当点击红叉时,响起的是小猫失望的叫声。<br>至此,程序全部开发完成,最终的 javascript 代码如下:</p>
<pre><code class="javascript">let vm = new Vue({
el: '#app',
data: {
round: {all: 0, right: 0},
numbers: [0, 0],
isThinking: true,
ARITHMETIC_TYPE: {
ADDITION: 1,
SUBTRACTION: 2,
MULTIPLICATION: 3,
DIVISION: 4,
properties: {
1: {operation: '+', f: ([x, y]) => x + y, gen: (arr) => arr, level: [3, 2]},
2: {operation: '-', f: ([x, y]) => x - y, gen: ([a, b]) => a >= b ? [a, b] : [b, a], level: [3, 2]},
3: {operation: '×', f: ([x, y]) => x * y, gen: (arr) => arr, level: [2, 1]},
4: {operation: '÷', f: ([x, y]) => x / y, gen: ([a, b]) => [a * b, b], level: [2, 1]}
}
},
arithmeticType: 1,
sound: {
right: new Howl({src: ['https://freesound.org/data/previews/203/203121_777645-lq.mp3']}),
wrong: new Howl({src: ['https://freesound.org/data/previews/415/415209_5121236-lq.mp3']})
},
},
computed: {
operation: function() {
return this.ARITHMETIC_TYPE.properties[this.arithmeticType].operation
},
result: function() {
return this.ARITHMETIC_TYPE.properties[this.arithmeticType].f(this.numbers)
},
score: function() {
return this.round.all == 1
? 100
: Math.round(this.round.right / (this.round.all - 1) * 100)
}
},
methods: {
getRandomNumber: function(level) {
let min = Math.pow(10, level - 1)
let max = Math.pow(10, level)
return min + Math.floor(Math.random() * (max - min))
},
getNumbers: function() {
let level = this.ARITHMETIC_TYPE.properties[this.arithmeticType].level
let a = this.getRandomNumber(level[0])
let b = this.getRandomNumber(level[1])
return this.ARITHMETIC_TYPE.properties[this.arithmeticType].gen([a, b])
},
newRound: function() {
this.numbers = this.getNumbers()
this.isThinking = true
},
next: function() {
this.newRound()
this.round.all++
},
getResult: function() {
this.isThinking = false
},
answerRight: function() {
this.round.right++
this.sound.right.play()
this.next()
},
answerWrong: function() {
this.sound.wrong.play()
this.next()
},
},
watch: {
arithmeticType: function() {
this.newRound()
}
}
})
window.onload = vm.next</code></pre>
<p>大功告成!</p>
前端每日实战:164# 视频演示如何用原生 JS 创作一个数独训练小游戏(内含 4 个视频)
https://segmentfault.com/a/1190000017311527
2018-12-09T12:49:31+08:00
2018-12-09T12:49:31+08:00
comehope
https://segmentfault.com/u/comehope
27
<p><img src="/img/bVbkNGa?w=400&h=300" alt="图片描述" title="图片描述"></p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/mQYobz">https://codepen.io/comehope/pen/mQYobz</a></p>
<h2>可交互视频</h2>
<p>此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。</p>
<p>请用 chrome, safari, edge 打开观看。</p>
<p>第 1 部分:<br><a href="https://link.segmentfault.com/?enc=IXQFtMfY%2BH%2Fp5lS8BMWrsA%3D%3D.fVcCV2osjScGpfwtikVgbp0T7IZqTlGG3ebhZVOoCQ05EnGk6%2BU%2Ftk3AYx1tN6R5" rel="nofollow">https://scrimba.com/p/pEgDAM/c7Q86ug</a></p>
<p>第 2 部分:<br><a href="https://link.segmentfault.com/?enc=c4FT8clyRl%2BFKYMCeywLUQ%3D%3D.sPD1uw5QqfE8VTKxNGNYE%2FJv%2B4xd5CDeEbSwTsqYhZi08aR7YMbTl1ByjQ4s4PSJ" rel="nofollow">https://scrimba.com/p/pEgDAM/ckgBNAD</a></p>
<p>第 3 部分:<br><a href="https://link.segmentfault.com/?enc=YKuZEYZDEjljutVO%2BlF6Lg%3D%3D.5dO8wE7djpZXKvNyuIlbs5PhM%2BaIITqCJ6pijCPmpP3NVsK4VZpzB8Kv4MzBXKn8" rel="nofollow">https://scrimba.com/p/pEgDAM/cG7bWc8</a></p>
<p>第 4 部分:<br><a href="https://link.segmentfault.com/?enc=5BX9Dqnvg83i%2FDBpBxmK1w%3D%3D.%2ByOIpIkWD0uuu4GcGobjBKPr22YrWI51zG1OkB3igWdadqwUn9LZkFymhsUNka16" rel="nofollow">https://scrimba.com/p/pEgDAM/cez34fp</a></p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=JXV7oYOD7f%2FGNbHrUPhylg%3D%3D.SYAafoemA3j8lNS1pz4A%2ByqpDkAnKoGY7cRvvEv4ZJJnIVnQfhgj0m9KzRSFrouhdQufLLpj9%2FsjpnbSkLUkhQ%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<p>解数独的一项基本功是能迅速判断一行、一列或一个九宫格中缺少哪几个数字,本项目就是一个训练判断九宫格中缺少哪个数字的小游戏。游戏的流程是:先选择游戏难度,有 Easy、Normal、Hard 三档,分别对应着九宫格中缺少 1 个、2 个、3 个数字。开始游戏后,用键盘输入九宫格中缺少的数字,如果全答出来了,就会进入下一局,一共 5 局,5 局结束之后这一次游戏就结束了。在游戏过程中,九宫格的左上角会计时,右上角会计分。</p>
<p>整个游戏分成 4 个步骤开发:静态页面布局、程序逻辑、计分计时和动画效果。</p>
<h3>一、页面布局</h3>
<p>定义 dom 结构,<code>.app</code> 是整个应用的容器,<code>h1</code> 是游戏标题,<code>.game</code> 是游戏的主界面。<code>.game</code> 中的子元素包括 <code>.message</code> 和 <code>.digits</code>,<code>.message</code> 用来提示游戏时间 <code>.time</code>、游戏的局数 <code>.round</code>、得分 <code>.score</code>,<code>.digits</code> 里是 9 个数字:</p>
<pre><code class="html"><div class="app">
<h1>Sudoku Training</h1>
<div class="game">
<div class="message">
<p>
Time:
<span class="time">00:00</span>
</p>
<p class="round">1/5</p>
<p>
Score:
<span class="score">100</span>
</p>
</div>
<div class="digits">
<span>1</span>
<span>2</span>
<span>3</span>
<span>4</span>
<span>5</span>
<span>6</span>
<span>7</span>
<span>8</span>
<span>9</span>
</div>
</div>
</div></code></pre>
<p>居中显示:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: silver;
overflow: hidden;
}</code></pre>
<p>定义应用的宽度,子元素纵向布局:</p>
<pre><code class="css">.app {
width: 300px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
user-select: none;
}</code></pre>
<p>标题为棕色字:</p>
<pre><code class="css">h1 {
margin: 0;
color: sienna;
}</code></pre>
<p>提示信息是横向布局,重点内容加粗:</p>
<pre><code class="css">.game .message {
width: inherit;
display: flex;
justify-content: space-between;
font-size: 1.2em;
font-family: sans-serif;
}
.game .message span {
font-weight: bold;
}</code></pre>
<p>九宫格用 grid 布局,外框棕色,格子用杏白色背景:</p>
<pre><code class="css">.game .digits {
box-sizing: border-box;
width: 300px;
height: 300px;
padding: 10px;
border: 10px solid sienna;
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 10px;
}
.game .digits span {
width: 80px;
height: 80px;
background-color: blanchedalmond;
font-size: 30px;
font-family: sans-serif;
text-align: center;
line-height: 2.5em;
color: sienna;
position: relative;
}</code></pre>
<p>至此,游戏区域布局完成,接下来布局选择游戏难度的界面。<br>在 html 文件中增加 <code>.select-level</code> dom 结构,它包含一个难度列表 <code>levels</code> 和一个开始游戏的按钮 <code>.play</code>,游戏难度分为 <code>.easy</code>、<code>.normal</code> 和 <code>.hard</code> 三个级别:</p>
<pre><code class="html"><div class="app">
<h1>Sudoku Training</h1>
<div class="game">
<!-- 略 -->
</div>
<div class="select-level">
<div class="levels">
<input type="radio" name="level" id="easy" value="easy" checked="checked">
<label for="easy">Easy</label>
<input type="radio" name="level" id="normal" value="normal">
<label for="normal">Normal</label>
<input type="radio" name="level" id="hard" value="hard">
<label for="hard">Hard</label>
</div>
<div class="play">Play</div>
</div>
</div></code></pre>
<p>为选择游戏难度容器画一个圆形的外框,子元素纵向布局:</p>
<pre><code class="css">.select-level {
z-index: 2;
box-sizing: border-box;
width: 240px;
height: 240px;
border: 10px solid rgba(160, 82, 45, 0.8);
border-radius: 50%;
box-shadow:
0 0 0 0.3em rgba(255, 235, 205, 0.8),
0 0 1em 0.5em rgba(160, 82, 45, 0.8);
display: flex;
flex-direction: column;
align-items: center;
font-family: sans-serif;
}</code></pre>
<p>布局 3 个难度选项,横向排列:</p>
<pre><code class="css">.select-level .levels {
margin-top: 60px;
width: 190px;
display: flex;
justify-content: space-between;
}</code></pre>
<p>把 <code>input</code> 控件隐藏起来,只显示它们对应的 <code>label</code>:</p>
<pre><code class="css">.select-level .levels {
position: relative;
}
.select-level input[type=radio] {
visibility: hidden;
position: absolute;
left: 0;
}</code></pre>
<p>设置 <code>label</code> 的样式,为圆形按钮:</p>
<pre><code class="css">.select-level label {
width: 56px;
height: 56px;
background-color: rgba(160, 82, 45, 0.8);
border-radius: 50%;
text-align: center;
line-height: 56px;
color: blanchedalmond;
cursor: pointer;
}</code></pre>
<p>当某个 <code>label</code> 对应的 <code>input</code> 被选中时,令 <code>label</code> 背景色加深,以示区别:</p>
<pre><code class="css">.select-level input[type=radio]:checked + label {
background-color: sienna;
}</code></pre>
<p>设置开始游戏按钮 <code>.play</code> 的样式,以及交互效果:</p>
<pre><code class="css">.select-level .play {
width: 120px;
height: 30px;
background-color: sienna;
color: blanchedalmond;
text-align: center;
line-height: 30px;
border-radius: 30px;
text-transform: uppercase;
cursor: pointer;
margin-top: 30px;
font-size: 20px;
letter-spacing: 2px;
}
.select-level .play:hover {
background-color: saddlebrown;
}
.select-level .play:active {
transform: translate(2px, 2px);
}</code></pre>
<p>至此,选择游戏难度的界面布局完成,接下来布局游戏结束界面。<br>游戏结束区 <code>.game-over</code> 包含一个 <code>h2</code> 标题,二行显示最终结果的段落 <code>p</code> 和一个再玩一次的按钮 <code>.again</code>。最终结果包括最终耗时 <code>.final-time</code> 和最终得分 <code>.final-score</code>:</p>
<pre><code class="html"><div class="app">
<h1>Sudoku Training</h1>
<div class="game">
<!-- 略 -->
</div>
<div class="select-level">
<!-- 略 -->
</div>
<div class="game-over">
<h2>Game Over</h2>
<p>
Time:
<span class="final-time">00:00</span>
</p>
<p>
Score:
<span class="final-score">3000</span>
</p>
<div class="again">Play Again</div>
</div>
</div></code></pre>
<p>因为游戏结束界面和选择游戏难度界面的布局相似,所以借用 <code>.select-level</code> 的代码:</p>
<pre><code class="css">.select-level,
.game-over {
z-index: 2;
box-sizing: border-box;
width: 240px;
height: 240px;
border: 10px solid rgba(160, 82, 45, 0.8);
border-radius: 50%;
box-shadow:
0 0 0 0.3em rgba(255, 235, 205, 0.8),
0 0 1em 0.5em rgba(160, 82, 45, 0.8);
display: flex;
flex-direction: column;
align-items: center;
font-family: sans-serif;
}</code></pre>
<p>标题和最终结果都用棕色字:</p>
<pre><code class="css">.game-over h2 {
margin-top: 40px;
color: sienna;
}
.game-over p {
margin: 3px;
font-size: 20px;
color: sienna;
}</code></pre>
<p>“再玩一次”按钮 <code>.again</code> 的样式与开始游戏 <code>.play</code> 的样式相似,所以也借用 <code>.play</code> 的代码:</p>
<pre><code class="css">.select-level .play,
.game-over .again {
width: 120px;
height: 30px;
background-color: sienna;
color: blanchedalmond;
text-align: center;
line-height: 30px;
border-radius: 30px;
text-transform: uppercase;
cursor: pointer;
}
.select-level .play {
margin-top: 30px;
font-size: 20px;
letter-spacing: 2px;
}
.select-level .play:hover,
.game-over .again:hover {
background-color: saddlebrown;
}
.select-level .play:active,
.game-over .again:active {
transform: translate(2px, 2px);
}
.game-over .again {
margin-top: 10px;
}</code></pre>
<p>把选择游戏难度界面 <code>.select-level</code> 和游戏结束界面 <code>.game-over</code> 定位到游戏容器的中间位置:</p>
<pre><code class="css">.app {
position: relative;
}
.select-level,
.game-over {
position: absolute;
bottom: 40px;
}</code></pre>
<p>至此,游戏界面 <code>.game</code>、选择游戏难度界面 <code>.select-level</code> 和游戏结束界面 <code>.game-over</code> 均已布局完成。接下来为动态程序做些准备工作。<br>把选择游戏难度界面 <code>.select-level</code> 和游戏结束界面 <code>.game-over</code> 隐藏起来,当需要它们呈现时,会在脚本中设置它们的 <code>visibility</code> 属性:</p>
<pre><code class="css">.select-level,
.game-over {
visibility: hidden;
}</code></pre>
<p>游戏中,当选择游戏难度界面 <code>.select-level</code> 和游戏结束界面 <code>.game-over</code> 出现时,应该令游戏界面 <code>.game</code> 变模糊,并且加一个缓动时间,<code>.game.stop</code> 会在脚本中调用:</p>
<pre><code class="css">.game {
transition: 0.3s;
}
.game.stop {
filter: blur(10px);
}</code></pre>
<p>游戏中,当填错了数字时,要把错误的数字描一个红边;当填对了数字时,把数字的背景色改为巧克力色。<code>.game .digits span.wrong</code> 和 <code>.game .digits span.correct</code> 会在脚本中调用:</p>
<pre><code class="css">.game .digits span.wrong {
border: 2px solid crimson;
}
.game .digits span.correct {
background-color: chocolate;
color: gold;
}</code></pre>
<p>至此,完成全部布局和样式设计。</p>
<h3>二、程序逻辑</h3>
<p>引入 lodash 工具库,后面会用到 lodash 提供的一些数组函数:</p>
<pre><code class="html"><script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script></code></pre>
<p>在写程序逻辑之前,先定义几个存储业务数据的常量。<code>ALL_DIGITS</code> 存储了全部备选的数字,也就是从 1 到 9;<code>ANSWER_COUNT</code> 存储的是不同难度要回答的数字个数,easy 难度要回答 1 个数字,normal 难度要回答 2 个数字,hard 难度要回答 3 个数字;<code>ROUND_COUNT</code> 存储的是每次游戏的局数,默认是 5 局;<code>SCORE_RULE</code> 存储的是答对和答错时分数的变化,答对加 100 分,答错扣 10 分。定义这些常量的好处是避免在程序中出现魔法数字,提高程序可读性:</p>
<pre><code class="javascript">const ALL_DIGITS = ['1','2','3','4','5','6','7','8','9']
const ANSWER_COUNT = {EASY: 1, NORMAL: 2, HARD: 3}
const ROUND_COUNT = 5
const SCORE_RULE = {CORRECT: 100, WRONG: -10}</code></pre>
<p>再定义一个 dom 对象,用于引用 dom 元素,它的每个属性是一个 dom 元素,key 值与 class 类名保持一致。其中大部分 dom 元素是一个 element 对象,只有 <code>dom.digits</code> 和 <code>dom.levels</code> 是包含多个 element 对象的数组;另外 <code>dom.level</code> 用于获取被选中的难度,因为它的值随用户选择而变化,所以用函数来返回实时结果:</p>
<pre><code class="javascript">const $ = (selector) => document.querySelectorAll(selector)
const dom = {
game: $('.game')[0],
digits: Array.from($('.game .digits span')),
time: $('.game .time')[0],
round: $('.game .round')[0],
score: $('.game .score')[0],
selectLevel: $('.select-level')[0],
level: () => {return $('input[type=radio]:checked')[0]},
play: $('.select-level .play')[0],
gameOver: $('.game-over')[0],
again: $('.game-over .again')[0],
finalTime: $('.game-over .final-time')[0],
finalScore: $('.game-over .final-score')[0],
}</code></pre>
<p>在游戏过程中需要根据游戏进展随时修改 dom 元素的内容,这些修改过程我们也把它们先定义在 <code>render</code> 对象中,这样程序主逻辑就不用关心具体的 dom 操作了。<code>render</code> 对象的每个属性是一个 dom 操作,结构如下:</p>
<pre><code class="javascript">const render = {
initDigits: () => {},
updateDigitStatus: () => {},
updateTime: () => {},
updateScore: () => {},
updateRound: () => {},
updateFinal: () => {},
}</code></pre>
<p>下面我们把这些 dom 操作逐个写下来。<br><code>render.initDigits</code> 用来初始化九宫格。它接收一个文本数组,根据不同的难度级别,数组的长度可能是 8 个(easy 难度)、7 个(normal 难度)或 6 个(hard 难度),先把它补全为长度为 9 个数组,数量不足的元素补空字符,然后把它们随机分配到九宫格中:</p>
<pre><code class="javascript">const render = {
initDigits: (texts) => {
allTexts = texts.concat(_.fill(Array(ALL_DIGITS.length - texts.length), ''))
_.shuffle(dom.digits).forEach((digit, i) => {
digit.innerText = allTexts[i]
digit.className = ''
})
},
//...
}</code></pre>
<p><code>render.updateDigitStatus</code> 用来更新九宫格中单个格子的状态。它接收 2 个参数,<code>text</code><br> 是格子里的数字,<code>isAnswer</code> 指明这个数字是不是答案。格子的默认样式是浅色背景深色文字,如果传入的数字不是答案,也就是答错了,会为格子加上 <code>wrong</code> 样式,格子被描红边;如果传入的数字是答案,也就是答对了,会在一个空格子里展示这个数字,并为格子加上 <code>correct</code> 样式,格子的样式会改为深色背景浅色文字:</p>
<pre><code class="javascript">const render = {
//...
updateDigitStatus: (text, isAnswer) => {
if (isAnswer) {
let digit = _.find(dom.digits, x => (x.innerText == ''))
digit.innerText = text
digit.className = 'correct'
}
else {
_.find(dom.digits, x => (x.innerText == text)).className = 'wrong'
}
},
//...
}</code></pre>
<p><code>render.updateTime</code> 用来更新时间,<code>render.updateScore</code> 用来更新得分:</p>
<pre><code class="javascript">const render = {
//...
updateTime: (value) => {
dom.time.innerText = value.toString()
},
updateScore: (value) => {
dom.score.innerText = value.toString()
},
//...
}</code></pre>
<p><code>render.updateRound</code> 用来更新当前局数,显示为 “n/m” 的格式:</p>
<pre><code class="javascript">const render = {
//...
updateRound: (currentRound) => {
dom.round.innerText = [
currentRound.toString(),
'/',
ROUND_COUNT.toString(),
].join('')
},
//...
}</code></pre>
<p><code>render.updateFinal</code> 用来更新游戏结束界面里的最终成绩:</p>
<pre><code class="javascript">const render = {
//...
updateFinal: () => {
dom.finalTime.innerText = dom.time.innerText
dom.finalScore.innerText = dom.score.innerText
},
}</code></pre>
<p>接下来定义程序整体的逻辑结构。当页面加载完成之后执行 <code>init()</code> 函数,<code>init()</code> 函数会对整个游戏做些初始化的工作 ———— 令开始游戏按钮 <code>dom.play</code> 被点击时调用 <code>startGame()</code> 函数,令再玩一次按钮 <code>dom.again</code> 被点击时调用 <code>playAgain()</code> 函数,令按下键盘时触发事件处理程序 <code>pressKey()</code> ———— 最后调用 <code>newGame()</code> 函数开始新游戏:</p>
<pre><code class="javascript">window.onload = init
function init() {
dom.play.addEventListener('click', startGame)
dom.again.addEventListener('click', playAgain)
window.addEventListener('keyup', pressKey)
newGame()
}
function newGame() {
//...
}
function startGame() {
//...
}
function playAgain() {
//...
}
function pressKey() {
//...
}</code></pre>
<p>当游戏开始时,令游戏界面变模糊,呼出选择游戏难度的界面:</p>
<pre><code class="javascript">function newGame() {
dom.game.classList.add('stop')
dom.selectLevel.style.visibility = 'visible'
}</code></pre>
<p>当选择了游戏难度,点击开始游戏按钮 <code>dom.play</code> 时,隐藏掉选择游戏难度的界面,游戏界面恢复正常,然后把根据用户选择的游戏难度计算出的答案数字个数存储到全局变量 <code>answerCount</code> 中,调用 <code>newRound()</code> 开始一局游戏:</p>
<pre><code class="javascript">let answerCount
function startGame() {
dom.game.classList.remove('stop')
dom.selectLevel.style.visibility = 'hidden'
answerCount = ANSWER_COUNT[dom.level().value.toUpperCase()]
newRound()
}</code></pre>
<p>当一局游戏开始时,打乱所有候选数字,生成一个全局数组变量 <code>digits</code>,<code>digits</code> 的每个元素包含 3 个属性,<code>text</code> 属性表示数字文本,<code>isAnswer</code> 属性表示该数字是否为答案,<code>isPressed</code> 表示该数字是否被按下过,<code>isPressed</code> 的初始值均为 <code>false</code>,紧接着把 <code>digits</code> 渲染到九宫格中:</p>
<pre><code class="javascript">let digits
function newRound() {
digits = _.shuffle(ALL_DIGITS).map((x, i) => {
return {
text: x,
isAnwser: (i < answerCount),
isPressed: false
}
})
render.initDigits(_.filter(digits, x => !x.isAnwser).map(x => x.text))
}</code></pre>
<p>当用户按下键盘时,若按的键不是候选文本,就忽略这次按键事件。通过按键的文本在 <code>digits</code> 数组中找到对应的元素 <code>digit</code>,判断该键是否被按过,若被按过,也退出事件处理。接下来,就是针对没按过的键,在对应的 <code>digit</code> 对象上标明该键已按过,并且更新这个键的显示状态,如果用户按下的不是答案数字,就把该数字所在的格子描红,如果用户按下的是答案数字,就突出显示这个数字:</p>
<pre><code class="javascript">function pressKey(e) {
if (!ALL_DIGITS.includes(e.key)) return;
let digit = _.find(digits, x => (x.text == e.key))
if (digit.isPressed) return;
digit.isPressed = true
render.updateDigitStatus(digit.text, digit.isAnwser)
}</code></pre>
<p>当用户已经按下了所有的答案数字,这一局就结束了,开始新一局:</p>
<pre><code class="javascript">function pressKey(e) {
if (!ALL_DIGITS.includes(e.key)) return;
let digit = _.find(digits, x => (x.text == e.key))
if (digit.isPressed) return;
digit.isPressed = true
render.updateDigitStatus(digit.text, digit.isAnwser)
//判断用户是否已经按下所有的答案数字
let hasPressedAllAnswerDigits = (_.filter(digits, (x) => (x.isAnwser && x.isPressed)).length == answerCount)
if (!hasPressedAllAnswerDigits) return;
newRound()
}</code></pre>
<p>增加一个记录当前局数的全局变量 <code>round</code>,在游戏开始时它的初始值为 0,每局游戏开始时,它的值就加1,并更新游戏界面中的局数 <code>dom.round</code>:</p>
<pre><code class="javascript">let round
function newGame() {
round = 0 //初始化局数
dom.game.classList.add('stop')
dom.selectLevel.style.visibility = 'visible'
}
function startGame() {
render.updateRound(1) //初始化页面中的局数
dom.game.classList.remove('stop')
dom.selectLevel.style.visibility = 'hidden'
answerCount = ANSWER_COUNT[dom.level().value.toUpperCase()]
newRound()
}
function newRound() {
digits = _.shuffle(ALL_DIGITS).map((x, i) => {
return {
text: x,
isAnwser: (i < answerCount),
isPressed: false
}
})
render.initDigits(_.filter(digits, x => !x.isAnwser).map(x => x.text))
//每局开始时为局数加 1
round++
render.updateRound(round)
}</code></pre>
<p>当前局数 <code>round</code> 增加到常量 <code>ROUND_COUNT</code> 定义的游戏总局数,本次游戏结束,调用 <code>gameOver()</code> 函数,否则调用 <code>newRound()</code> 函数开始新一局:</p>
<pre><code class="javascript">function pressKey(e) {
if (!ALL_DIGITS.includes(e.key)) return;
let digit = _.find(digits, x => (x.text == e.key))
if (digit.isPressed) return;
digit.isPressed = true
render.updateDigitStatus(digit.text, digit.isAnwser)
let hasPressedAllAnswerDigits = (_.filter(digits, (x) => (x.isAnwser && x.isPressed)).length == answerCount)
if (!hasPressedAllAnswerDigits) return;
//判断是否玩够了总局数
let hasPlayedAllRounds = (round == ROUND_COUNT)
if (hasPlayedAllRounds) {
gameOver()
} else {
newRound()
}
}</code></pre>
<p>游戏结束时,令游戏界面变模糊,调出游戏结束界面,显示最终成绩:</p>
<pre><code class="javascript">function gameOver() {
render.updateFinal()
dom.game.classList.add('stop')
dom.gameOver.style.visibility = 'visible'
}</code></pre>
<p>在游戏结束界面,用户可以点击再玩一次按钮 <code>dom.again</code>,若点击了此按钮,就把游戏结束界面隐藏起来,开始一局新游戏,这就回到 <code>newGame()</code> 的流程了:</p>
<pre><code class="javascript">function playAgain() {
dom.game.classList.remove('stop')
dom.gameOver.style.visibility = 'hidden'
newGame()
}</code></pre>
<p>至此,整个游戏的流程已经跑通了,此时的脚本如下:</p>
<pre><code class="javascript">const ALL_DIGITS = ['1','2','3','4','5','6','7','8','9']
const ANSWER_COUNT = {EASY: 1, NORMAL: 2, HARD: 3}
const ROUND_COUNT = 3
const SCORE_RULE = {CORRECT: 100, WRONG: -10}
const $ = (selector) => document.querySelectorAll(selector)
const dom = {
game: $('.game')[0],
digits: Array.from($('.game .digits span')),
time: $('.game .time')[0],
round: $('.game .round')[0],
score: $('.game .score')[0],
selectLevel: $('.select-level')[0],
level: () => {return $('input[type=radio]:checked')[0]},
play: $('.select-level .play')[0],
gameOver: $('.game-over')[0],
again: $('.game-over .again')[0],
finalTime: $('.game-over .final-time')[0],
finalScore: $('.game-over .final-score')[0],
}
const render = {
initDigits: (texts) => {
allTexts = texts.concat(_.fill(Array(ALL_DIGITS.length - texts.length), ''))
_.shuffle(dom.digits).forEach((digit, i) => {
digit.innerText = allTexts[i]
digit.className = ''
})
},
updateDigitStatus: (text, isAnswer) => {
if (isAnswer) {
let digit = _.find(dom.digits, x => (x.innerText == ''))
digit.innerText = text
digit.className = 'correct'
}
else {
_.find(dom.digits, x => (x.innerText == text)).className = 'wrong'
}
},
updateTime: (value) => {
dom.time.innerText = value.toString()
},
updateScore: (value) => {
dom.score.innerText = value.toString()
},
updateRound: (currentRound) => {
dom.round.innerText = [
currentRound.toString(),
'/',
ROUND_COUNT.toString(),
].join('')
},
updateFinal: () => {
dom.finalTime.innerText = dom.time.innerText
dom.finalScore.innerText = dom.score.innerText
},
}
let answerCount, digits, round
window.onload = init
function init() {
dom.play.addEventListener('click', startGame)
dom.again.addEventListener('click', playAgain)
window.addEventListener('keyup', pressKey)
newGame()
}
function newGame() {
round = 0
dom.game.classList.add('stop')
dom.selectLevel.style.visibility = 'visible'
}
function startGame() {
render.updateRound(1)
dom.game.classList.remove('stop')
dom.selectLevel.style.visibility = 'hidden'
answerCount = ANSWER_COUNT[dom.level().value.toUpperCase()]
newRound()
}
function newRound() {
digits = _.shuffle(ALL_DIGITS).map((x, i) => {
return {
text: x,
isAnwser: (i < answerCount),
isPressed: false
}
})
render.initDigits(_.filter(digits, x => !x.isAnwser).map(x => x.text))
round++
render.updateRound(round)
}
function gameOver() {
render.updateFinal()
dom.game.classList.add('stop')
dom.gameOver.style.visibility = 'visible'
}
function playAgain() {
dom.game.classList.remove('stop')
dom.gameOver.style.visibility = 'hidden'
newGame()
}
function pressKey(e) {
if (!ALL_DIGITS.includes(e.key)) return;
let digit = _.find(digits, x => (x.text == e.key))
if (digit.isPressed) return;
digit.isPressed = true
render.updateDigitStatus(digit.text, digit.isAnwser)
let hasPressedAllAnswerDigits = (_.filter(digits, (x) => (x.isAnwser && x.isPressed)).length == answerCount)
if (!hasPressedAllAnswerDigits) return;
let hasPlayedAllRounds = (round == ROUND_COUNT)
if (hasPlayedAllRounds) {
gameOver()
} else {
newRound()
}
}</code></pre>
<h3>三、计分和计时</h3>
<p>接下来处理得分和时间,先处理得分。<br>首先声明一个用于存储得分的全局变量 <code>score</code>,在新游戏开始之前设置它的初始值为 <code>0</code>,在游戏开始时初始化页面中的得分:</p>
<pre><code class="javascript">let score
function newGame() {
round = 0
score = 0 //初始化得分
dom.game.classList.add('stop')
dom.selectLevel.style.visibility = 'visible'
}
function startGame() {
render.updateRound(1)
render.updateScore(0) //初始化页面中的得分
dom.game.classList.remove('stop')
dom.selectLevel.style.visibility = 'hidden'
answerCount = ANSWER_COUNT[dom.level().value.toUpperCase()]
newRound()
}</code></pre>
<p>在用户按键事件中根据按下的键是否为答案记录不同的分值:</p>
<pre><code class="javascript">function pressKey(e) {
if (!ALL_DIGITS.includes(e.key)) return;
let digit = _.find(digits, x => (x.text == e.key))
if (digit.isPressed) return;
digit.isPressed = true
render.updateDigitStatus(digit.text, digit.isAnwser)
//累积得分
score += digit.isAnwser ? SCORE_RULE.CORRECT : SCORE_RULE.WRONG
render.updateScore(score)
let hasPressedAllAnswerDigits = (_.filter(digits, (x) => (x.isAnwser && x.isPressed)).length == answerCount)
if (!hasPressedAllAnswerDigits) return;
let hasPlayedAllRounds = (round == ROUND_COUNT)
if (hasPlayedAllRounds) {
gameOver()
} else {
newRound()
}
}</code></pre>
<p>接下来处理时间。先创建一个计时器类 <code>Timer</code>,它的参数是一个用于把时间渲染到页面上的函数,另外 <code>Timer</code> 有 <code>start()</code> 和 <code>stop()</code> 2 个方法用于开启和停止计时器,计时器每秒会执行一次 <code>tickTock()</code> 函数:</p>
<pre><code class="javascript">function Timer(render) {
this.render = render
this.t = {},
this.start = () => {
this.t = setInterval(this.tickTock, 1000);
}
this.stop = () => {
clearInterval(this.t)
}
}</code></pre>
<p>定义一个记录时间的变量 <code>time</code>,它的初始值为 <code>0</code> 分 <code>0</code> 秒,在 <code>tickTock()</code> 函数中把秒数加1,并调用渲染函数把当前时间写到页面中:</p>
<pre><code class="javascript">function Timer(render) {
this.render = render
this.t = {}
this.time = {
minute: 0,
second: 0,
}
this.tickTock = () => {
this.time.second ++;
if (this.time.second == 60) {
this.time.minute ++
this.time.second = 0
}
render([
this.time.minute.toString().padStart(2, '0'),
':',
this.time.second.toString().padStart(2, '0'),
].join(''))
}
this.start = () => {
this.t = setInterval(this.tickTock, 1000)
}
this.stop = () => {
clearInterval(this.t)
}
}</code></pre>
<p>在开始游戏时初始化页面中的时间:</p>
<pre><code class="javascript">function startGame() {
render.updateRound(1)
render.updateScore(0)
render.updateTime('00:00') //初始化页面中的时间
dom.game.classList.remove('stop')
dom.selectLevel.style.visibility = 'hidden'
answerCount = ANSWER_COUNT[dom.level().value.toUpperCase()]
newRound()
}</code></pre>
<p>定义一个存储定时器的全局变量 <code>timer</code>,在创建游戏时初始化定时器,在游戏开始时启动计时器,在游戏结束时停止计时器:</p>
<pre><code class="javascript">let timer
function newGame() {
round = 0
score = 0
timer = new Timer(render.updateTime) //创建定时器
dom.game.classList.add('stop')
dom.selectLevel.style.visibility = 'visible'
}
function startGame() {
render.updateRound(1)
render.updateScore(0)
render.updateTime('00:00')
dom.game.classList.remove('stop')
dom.selectLevel.style.visibility = 'hidden'
answerCount = ANSWER_COUNT[dom.level().value.toUpperCase()]
newRound()
timer.start() //开始计时
}
function gameOver() {
timer.stop() //停止计时
render.updateFinal()
dom.game.classList.add('stop')
dom.gameOver.style.visibility = 'visible'
}</code></pre>
<p>至此,时钟已经可以运行了,在游戏开始时从 0 分 0 秒开始计时,在游戏结束时停止计时。<br>最后一个环节,当游戏结束之后,不应再响应用户的按键事件。为此,我们定义一个标明是否可按键的变量 <code>canPress</code>,在创建新游戏时它的状态是不可按,游戏开始之后变为可按,游戏结束之后再变为不可按:</p>
<pre><code class="javascript">let canPress
function newGame() {
round = 0
score = 0
time = {
minute: 0,
second: 0
}
timer = new Timer()
canPress = false //初始化是否可按键的标志
dom.game.classList.add('stop')
dom.selectLevel.style.visibility = 'visible'
}
function startGame() {
render.updateRound(1)
render.updateScore(0)
render.updateTime(0, 0)
dom.game.classList.remove('stop')
dom.selectLevel.style.visibility = 'hidden'
answerCount = ANSWER_COUNT[dom.level().value.toUpperCase()]
newRound()
timer.start(tickTock)
canPress = true //游戏开始后,可以按键
}
function gameOver() {
canPress = false //游戏结束后,不可以再按键
timer.stop()
render.updateFinal()
dom.game.classList.add('stop')
dom.gameOver.style.visibility = 'visible'
}</code></pre>
<p>在按键事件处理程序中,首先判断是否允许按键,若不允许,就退出事件处理程序:</p>
<pre><code class="javascript">function pressKey(e) {
if (!canPress) return; //判断是否允许按键
if (!ALL_DIGITS.includes(e.key)) return;
let digit = _.find(digits, x => (x.text == e.key))
if (digit.isPressed) return;
digit.isPressed = true
render.updateDigitStatus(digit.text, digit.isAnwser)
score += digit.isAnwser ? SCORE_RULE.CORRECT : SCORE_RULE.WRONG
render.updateScore(score)
let hasPressedAllAnswerDigits = (_.filter(digits, (x) => (x.isAnwser && x.isPressed)).length == answerCount)
if (hasPressedAllAnswerDigits) {
newRound()
}
}</code></pre>
<p>至此,计分计时设计完毕,此时的脚本如下:</p>
<pre><code class="javascript">const ALL_DIGITS = ['1','2','3','4','5','6','7','8','9']
const ANSWER_COUNT = {EASY: 1, NORMAL: 2, HARD: 3}
const ROUND_COUNT = 3
const SCORE_RULE = {CORRECT: 100, WRONG: -10}
const $ = (selector) => document.querySelectorAll(selector)
const dom = {
//略,与此前代码相同
}
const render = {
//略,与此前代码相同
}
let answerCount, digits, round, score, timer, canPress
window.onload = init
function init() {
//略,与此前代码相同
}
function newGame() {
round = 0
score = 0
timer = new Timer(render.updateTime)
canPress = false
dom.game.classList.add('stop')
dom.selectLevel.style.visibility = 'visible'
}
function startGame() {
render.updateRound(1)
render.updateScore(0)
render.updateTime(0, 0)
dom.game.classList.remove('stop')
dom.selectLevel.style.visibility = 'hidden'
answerCount = ANSWER_COUNT[dom.level().value.toUpperCase()]
newRound()
timer.start()
canPress = true
}
function newRound() {
//略,与此前代码相同
}
function gameOver() {
canPress = false
timer.stop()
render.updateFinal()
dom.game.classList.add('stop')
dom.gameOver.style.visibility = 'visible'
}
function playAgain() {
//略,与此前代码相同
}
function pressKey(e) {
if (!canPress) return;
if (!ALL_DIGITS.includes(e.key)) return;
let digit = _.find(digits, x => (x.text == e.key))
if (digit.isPressed) return;
digit.isPressed = true
render.updateDigitStatus(digit.text, digit.isAnwser)
score += digit.isAnwser ? SCORE_RULE.CORRECT : SCORE_RULE.WRONG
render.updateScore(score)
let hasPressedAllAnswerDigits = (_.filter(digits, (x) => (x.isAnwser && x.isPressed)).length == answerCount)
if (!hasPressedAllAnswerDigits) return;
let hasPlayedAllRounds = (round == ROUND_COUNT)
if (hasPlayedAllRounds) {
gameOver()
} else {
newRound()
}
}</code></pre>
<h3>四、动画效果</h3>
<p>引入 gsap 动画库:</p>
<pre><code class="html"><script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/2.0.2/TweenMax.min.js"></script></code></pre>
<p>游戏中一共有 6 个动画效果,分别是九宫格的出场与入场、选择游戏难度界面的显示与隐藏、游戏结束界面的显示与隐藏。为了集中管理动画效果,我们定义一个全局常量 <code>animation</code>,它的每个属性是一个函数,实现一个动画效果,结构如下,注意因为选择游戏难度界面和游戏结束界面的样式相似,所以它们共享了相同的动画效果,在调用函数时要传入一个参数 <code>element</code> 指定动画的 dom 对象:</p>
<pre><code class="javascript">const animation = {
digitsFrameOut: () => {
//九宫格出场
},
digitsFrameIn: () => {
//九宫格入场
},
showUI: (element) => {
//显示选择游戏难度界面和游戏结束界面
},
frameOut: (element) => {
//隐藏选择游戏难度界面和游戏结束界面
},
}</code></pre>
<p>确定下这几个动画的时机:</p>
<pre><code class="javascript">function newGame() {
round = 0
score = 0
timer = new Timer(render.updateTime)
canPress = false
//选择游戏难度界面 - 显示
dom.game.classList.add('stop')
dom.selectLevel.style.visibility = 'visible'
}
function startGame() {
render.updateRound(1)
render.updateScore(0)
render.updateTime('00:00')
//选择游戏难度界面 - 隐藏
dom.game.classList.remove('stop')
dom.selectLevel.style.visibility = 'hidden'
answerCount = ANSWER_COUNT[dom.level().value.toUpperCase()]
newRound()
timer.start()
canPress = true
}
function newRound() {
//九宫格 - 出场
digits = _.shuffle(ALL_DIGITS).map((x, i) => {
return {
text: x,
isAnwser: (i < answerCount),
isPressed: false
}
})
render.initDigits(_.filter(digits, x => !x.isAnwser).map(x => x.text))
//九宫格 - 入场
round++
render.updateRound(round)
}
function gameOver() {
canPress = false
timer.stop()
render.updateFinal()
//游戏结束界面 - 显示
dom.game.classList.add('stop')
dom.gameOver.style.visibility = 'visible'
}
function playAgain() {
//游戏结束界面 - 隐藏
dom.game.classList.remove('stop')
dom.gameOver.style.visibility = 'hidden'
newGame()
}</code></pre>
<p>把目前动画时机所在位置的代码移到 <code>animation</code> 对象中,九宫格出场和入场的动画目前是空的:</p>
<pre><code class="javascript">const animation = {
digitsFrameOut: () => {
//九宫格出场
},
digitsFrameIn: () => {
//九宫格入场
},
showUI: (element) => {
//显示选择游戏难度界面和游戏结束界面
dom.game.classList.add('stop')
element.style.visibility = 'visible'
},
hideUI: (element) => {
//隐藏选择游戏难度界面和游戏结束界面
dom.game.classList.remove('stop')
element.style.visibility = 'hidden'
},
}</code></pre>
<p>在动画时机的位置调用 <code>animation</code> 对应的动画函数,因为动画是有执行时长的,下一个动画要等到上一个动画结束之后再开始,所以我们采用了 async/await 的语法,让相邻的动画顺序执行:</p>
<pre><code class="javascript">async function newGame() {
round = 0
score = 0
timer = new Timer(render.updateTime)
canPress = false
// 选择游戏难度界面 - 显示
await animation.showUI(dom.selectLevel)
}
async function startGame() {
render.updateRound(1)
render.updateScore(0)
render.updateTime('00:00')
// 选择游戏难度界面 - 隐藏
await animation.hideUI(dom.selectLevel)
answerCount = ANSWER_COUNT[dom.level().value.toUpperCase()]
newRound()
timer.start()
canPress = true
}
async function newRound() {
//九宫格 - 出场
await animation.digitsFrameOut()
digits = _.shuffle(ALL_DIGITS).map((x, i) => {
return {
text: x,
isAnwser: (i < answerCount),
isPressed: false
}
})
render.initDigits(_.filter(digits, x => !x.isAnwser).map(x => x.text))
//九宫格 - 入场
await animation.digitsFrameIn()
round++
render.updateRound(round)
}
async function gameOver() {
canPress = false
timer.stop()
render.updateFinal()
// 游戏结束界面 - 显示
await animation.showUI(dom.gameOver)
}
async function playAgain() {
// 游戏结束界面 - 隐藏
await animation.hideUI(dom.gameOver)
newGame()
}</code></pre>
<p>接下来就开始设计动画效果。<br><code>animation.digitsFrameOut</code> 是九宫格的出场动画,各格子分别旋转着消失。注意,为了与 async/await 语法配合,我们让函数返回了一个 Promise 对象:</p>
<pre><code class="javascript">const animation = {
digitsFrameOut: () => {
return new Promise(resolve => {
new TimelineMax()
.staggerTo(dom.digits, 0, {rotation: 0})
.staggerTo(dom.digits, 1, {rotation: 360, scale: 0, delay: 0.5})
.timeScale(2)
.eventCallback('onComplete', resolve)
})
},
//...
}</code></pre>
<p><code>animation.digitsFrameIn</code> 是九宫格的入场动画,它的动画效果是各格子旋转着出现,而且各格子的出现时间稍有延迟:</p>
<pre><code class="javascript">const animation = {
//...
digitsFrameIn: () => {
return new Promise(resolve => {
new TimelineMax()
.staggerTo(dom.digits, 0, {rotation: 0})
.staggerTo(dom.digits, 1, {rotation: 360, scale: 1}, 0.1)
.timeScale(2)
.eventCallback('onComplete', resolve)
})
},
//...
}</code></pre>
<p><code>animation.showUI</code> 是显示择游戏难度界面和游戏结束界面的动画,它的效果是从高处落下,并在底部小幅反弹,模拟物体跌落的效果:</p>
<pre><code class="javascript">const animation = {
//...
showUI: (element) => {
dom.game.classList.add('stop')
return new Promise(resolve => {
new TimelineMax()
.to(element, 0, {visibility: 'visible', x: 0})
.from(element, 1, {y: '-300px', ease: Elastic.easeOut.config(1, 0.3)})
.timeScale(1)
.eventCallback('onComplete', resolve)
})
},
//...
}</code></pre>
<p><code>animation.hideUI</code> 是隐藏选择游戏难度界面和游戏结束界面的动画,它从正常位置向右移出画面:</p>
<pre><code class="javascript">const animation = {
//...
hideUI: (element) => {
dom.game.classList.remove('stop')
return new Promise(resolve => {
new TimelineMax()
.to(element, 1, {x: '300px', ease: Power4.easeIn})
.to(element, 0, {visibility: 'hidden'})
.timeScale(2)
.eventCallback('onComplete', resolve)
})
},
}</code></pre>
<p>至此,整个游戏的动画效果就完成了,全部代码如下:</p>
<pre><code class="javascript">const ALL_DIGITS = ['1','2','3','4','5','6','7','8','9']
const ANSWER_COUNT = {EASY: 1, NORMAL: 2, HARD: 3}
const ROUND_COUNT = 3
const SCORE_RULE = {CORRECT: 100, WRONG: -10}
const $ = (selector) => document.querySelectorAll(selector)
const dom = {
//略,与增加动画前相同
}
const render = {
//略,与增加动画前相同
}
const animation = {
digitsFrameOut: () => {
return new Promise(resolve => {
new TimelineMax()
.staggerTo(dom.digits, 0, {rotation: 0})
.staggerTo(dom.digits, 1, {rotation: 360, scale: 0, delay: 0.5})
.timeScale(2)
.eventCallback('onComplete', resolve)
})
},
digitsFrameIn: () => {
return new Promise(resolve => {
new TimelineMax()
.staggerTo(dom.digits, 0, {rotation: 0})
.staggerTo(dom.digits, 1, {rotation: 360, scale: 1}, 0.1)
.timeScale(2)
.eventCallback('onComplete', resolve)
})
},
showUI: (element) => {
dom.game.classList.add('stop')
return new Promise(resolve => {
new TimelineMax()
.to(element, 0, {visibility: 'visible', x: 0})
.from(element, 1, {y: '-300px', ease: Elastic.easeOut.config(1, 0.3)})
.timeScale(1)
.eventCallback('onComplete', resolve)
})
},
hideUI: (element) => {
dom.game.classList.remove('stop')
return new Promise(resolve => {
new TimelineMax()
.to(element, 1, {x: '300px', ease: Power4.easeIn})
.to(element, 0, {visibility: 'hidden'})
.timeScale(2)
.eventCallback('onComplete', resolve)
})
},
}
let answerCount, digits, round, score, timer, canPress
window.onload = init
function init() {
//略,与增加动画前相同
}
async function newGame() {
round = 0
score = 0
timer = new Timer(render.updateTime)
canPress = false
await animation.showUI(dom.selectLevel)
}
async function startGame() {
render.updateRound(1)
render.updateScore(0)
render.updateTime('00:00')
await animation.hideUI(dom.selectLevel)
answerCount = ANSWER_COUNT[dom.level().value.toUpperCase()]
newRound()
timer.start()
canPress = true
}
async function newRound() {
await animation.digitsFrameOut()
digits = _.shuffle(ALL_DIGITS).map((x, i) => {
return {
text: x,
isAnwser: (i < answerCount),
isPressed: false
}
})
render.initDigits(_.filter(digits, x => !x.isAnwser).map(x => x.text))
await animation.digitsFrameIn()
round++
render.updateRound(round)
}
async function gameOver() {
canPress = false
timer.stop()
render.updateFinal()
await animation.showUI(dom.gameOver)
}
async function playAgain() {
await animation.hideUI(dom.gameOver)
newGame()
}
function pressKey(e) {
//略,与增加动画前相同
}
function tickTock() {
//略,与增加动画前相同
}</code></pre>
<p>大功告成!</p>
<p>最后,附上交互流程图,方便大家理解。其中蓝色条带表示动画,粉色椭圆表示用户操作,绿色矩形和菱形表示主要的程序逻辑:</p>
<p><img src="/img/bVbkNGh?w=1470&h=1168" alt="图片描述" title="图片描述"></p>
前端每日实战:163# 视频演示如何用原生 JS 创作一个多选一场景的交互游戏(内含 3 个视频)
https://segmentfault.com/a/1190000017212162
2018-11-30T19:15:47+08:00
2018-11-30T19:15:47+08:00
comehope
https://segmentfault.com/u/comehope
25
<p><img src="/img/bVbknOW?w=400&h=302" alt="图片描述" title="图片描述"></p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/LXMzRX">https://codepen.io/comehope/pen/LXMzRX</a></p>
<h2>可交互视频</h2>
<p>此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。</p>
<p>请用 chrome, safari, edge 打开观看。</p>
<p>第 1 部分:<br><a href="https://link.segmentfault.com/?enc=lTZp1L7EHA0eakgtfNVANw%3D%3D.qkZk6BaszkM8GlolsJEKgr6HBAM63J0HN%2FCVD1gNIl02oPpbTNyrJNhXue5mbqoe" rel="nofollow">https://scrimba.com/p/pEgDAM/cQK3bSp</a></p>
<p>第 2 部分:<br><a href="https://link.segmentfault.com/?enc=3cb1ptxX2pW3twsEBwl6ag%3D%3D.%2BL4Brzl18I%2Fqh%2BsABOINEB9iG4xLUXOUmqeR8TH2kURCpZXBHwQo3l%2FuGIMJH7dc" rel="nofollow">https://scrimba.com/p/pEgDAM/cNJWncR</a></p>
<p>第 3 部分:<br><a href="https://link.segmentfault.com/?enc=c3uNADEOV70YVVrmcMBmqg%3D%3D.3BN5lJ0FZYiG5t35TwiU6h6J8l311b%2B5kUDZjKN7yNNylmNF8FYLh4ij9tNhy%2Fm0" rel="nofollow">https://scrimba.com/p/pEgDAM/cvgP8td</a></p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=E44AztN6YncVgQMBv8oUKw%3D%3D.dFkxWL9CS4Y8AOCSIGf%2FDc1KZ93%2FTRM%2BPgvm2V43dnI7x9ykMVHYdJNJWEIbHP4N8WeUessudsLKYQiXSqKswQ%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<p>多选一的场景是很常见的,浏览器自带的 <code><input type="radio"></code> 控件就适用于这样的场景。本项目将设计一个多选一的交互场景,用 css 进行页面布局、用 gsap 制作动画效果、用原生 js 编写程序逻辑。</p>
<p>这个游戏的逻辑很简单,在页面上部展示出一个动物的全身像,请用户在下面的小图片中选择这个动物对应的头像,如果选对了,就可以再玩一次。</p>
<p>整个游戏分成 3 个步骤开发:静态的页面布局、程序逻辑和动画效果。</p>
<h3>一、页面布局</h3>
<p>定义 dom 结构,容器中包含标题 <code>h1</code>、全身像 <code>.whole-body</code>、当选择正确时的提示语 <code>.bingo</code>、“再玩一次”按钮 <code>.again</code>、一组选择按钮 <code>.selector</code>。<code>.selector</code> 中包含 5 个展示头像的 <code>.face</code> 和 1 个标明当前被选中头像的 <code>.slider</code>。全身像和头像没有使用图片,都用 unicode 字符代替:</p>
<pre><code class="html"><div class="app">
<h1>Which face is the animal's?</h1>
<div class="whole-body">🐄</div>
<div class="bingo">
Bingo!
<span class="again">Play Again</span>
</div>
<div class="selector">
<span class="slider"></span>
<span class="face">🐭</span>
<span class="face">🐶</span>
<span class="face">🐷</span>
<span class="face">🐮</span>
<span class="face">🐯</span>
</div>
</div></code></pre>
<p>居中显示:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(darkblue, black);
}</code></pre>
<p>定义容器中子元素的按纵向布局,水平居中:</p>
<pre><code class="css">.app {
height: 420px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
}</code></pre>
<p>标题是白色文字:</p>
<pre><code class="css">h1 {
margin: 0;
color: white;
}</code></pre>
<p>全身像为大尺寸的圆形,利用阴影画一个半透明的粗边框:</p>
<pre><code class="css">.whole-body {
width: 200px;
height: 200px;
background-color: rgb(180, 220, 255);
border-radius: 50%;
font-size: 140px;
text-align: center;
line-height: 210px;
margin-top: 20px;
box-shadow: 0 0 0 15px rgba(180, 220, 255, 0.2);
user-select: none;
}</code></pre>
<p>选择正确时的提示语为白色:</p>
<pre><code class="css">.bingo {
color: white;
font-size: 30px;
font-family: sans-serif;
margin-top: 20px;
}</code></pre>
<p>“再玩一次”按钮的字体稍小,在鼠标悬停和点击时有交互效果:</p>
<pre><code class="css">.again {
display: inline-block;
font-size: 20px;
background-color: white;
color: darkblue;
padding: 5px;
border-radius: 5px;
box-shadow: 5px 5px 2px rgba(0, 0, 0, 0.4);
user-select: none;
}
.again:hover {
background-color: rgba(255, 255, 255, 0.8);
cursor: pointer;
}
.again:active {
transform: translate(2px, 2px);
box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.4);
}</code></pre>
<p>5 个头像为小尺寸的圆形,横向排列,半透明背景:</p>
<pre><code class="css">.selector {
display: flex;
}
.face {
width: 60px;
height: 60px;
background-color: rgba(255, 255, 255, 0.2);
border-radius: 50%;
font-size: 40px;
text-align: center;
line-height: 70px;
cursor: pointer;
user-select: none;
}
.face:not(:last-child) {
margin-right: 25px;
}</code></pre>
<p>在被选中的头像下面叠加一个同尺寸的浅蓝色色块:</p>
<pre><code class="css">.selector {
position: relative;
}
.slider {
position: absolute;
width: 60px;
height: 60px;
background-color: rgba(180, 220, 255, 0.6);
border-radius: 50%;
z-index: -1;
}</code></pre>
<p>至此,页面布局完成。</p>
<h3>二、程序逻辑</h3>
<p>引入 lodash 工具库,后面会用到 lodash 提供的一些数组函数:</p>
<pre><code class="html"><script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script></code></pre>
<p>在写程序逻辑之前,我们先定义 2 个常量。<br>第一个常量是存储动物头像和全身像的数据对象 <code>animals</code>,它的每个属性是 1 种动物,key 是头像,value 是全身像:</p>
<pre><code class="javascript">const animals = {
'🐭': '🐁',
'🐶': '🐕',
'🐷': '🐖',
'🐮': '🐄',
'🐯': '🐅',
'🐔': '🐓',
'🐵': '🐒',
'🐲': '🐉',
'🐴': '🐎',
'🐰': '🐇',
}</code></pre>
<p>第二个常量是存储 dom 元素引用的数据对象 <code>dom</code>,它的每个属性是一个 dom 元素,key 值与 class 类名保持一致,分别是代表全身像的 <code>dom.wholeBody</code>、代表选择正确时的提示信息 <code>dom.bingo</code>、代表“再玩一次”按钮的 <code>dom.bingo</code>、代表头像列表的 <code>dom.faces</code>、代表头像下面的滑块 <code>dom.slider</code>:</p>
<pre><code class="javascript">const dom = {
wholeBody: document.querySelector('.whole-body'),
bingo: document.querySelector('.bingo'),
again: document.querySelector('.again'),
faces: Array.from(document.querySelectorAll('.face')),
slider: document.querySelector('.slider'),
}</code></pre>
<p>接下来定义整体的逻辑结构,当页面加载完成之后执行 <code>init()</code> 函数,<code>init()</code> 函数会对整个游戏做些初始化的工作 ———— 令头像 <code>dom.faces</code> 被点击时调用 <code>select()</code> 函数,令“再玩一次”按钮 <code>dom.again</code> 被点击时调用 <code>newGame()</code> 函数 ———— 最后调用 <code>newGame()</code> 函数开始一局新游戏:</p>
<pre><code class="javascript">function newGame() {
//...
}
function select() {
//...
}
function init() {
dom.faces.forEach(face => {
face.addEventListener('click', select)
})
dom.again.addEventListener('click', newGame)
newGame()
}
window.onload = init</code></pre>
<p>在 <code>newGame()</code> 函数中调用 <code>shuffle()</code> 函数。<code>shuffle()</code> 函数的作用是随机地从 <code>animals</code> 数组中选出 5 个动物,把它们的头像显示在 <code>dom.faces</code> 中,再从中选出 1 个动物,把它的全身像显示在 <code>dom.wholeBody</code> 中。变量 <code>options</code> 代表被选出的 5 个动物,变量 <code>answer</code> 代表显示全身像的动物,因为后面还会用到 <code>options</code> 和 <code>answer</code>,所以把它们定义为全局变量。经过 <code>_.entries()</code> 函数的处理,<code>options</code> 数组的元素和 <code>answer</code> 的数据结构变为包含 2 个元素的数组 <code>[key, value]</code> 形式,其中第 <code>[0]</code> 个元素是头像,第 <code>[1]</code> 个元素是全身像:</p>
<pre><code class="javascript">let options = []
let answer = {}
function newGame() {
shuffle()
}
function shuffle() {
options = _.slice(_.shuffle(_.entries(animals)), -5)
answer = _.sample(_.slice(options, -4))
dom.faces.forEach((face, i) => {
face.innerText = options[i][0]
})
dom.wholeBody.innerText = answer[1]
}</code></pre>
<p>现在,每点击一次 <code>Play Again</code> 按钮,就会洗牌、更新图片。<br>接下来处理滑块。在 <code>select()</code> 函数中,首先把滑块 <code>dom.slider</code> 移动到被点击的头像位置:</p>
<pre><code class="javascript">function select(e) {
let position = _.findIndex(options, (o) => o[0] == e.target.innerText)
dom.slider.style.left = (25 + 60) * position + 'px'
}</code></pre>
<p>然后判断当前头像对应的全身像和页面上方全身像是否一致,若一致,就显示提示语 <code>dom.bingo</code>。在此之前,要把提示语隐藏掉:</p>
<pre><code class="javascript">function newGame() {
dom.bingo.style.visibility = 'hidden'
shuffle()
}
function select(e) {
let position = _.findIndex(options, (o) => o[0] == e.target.innerText)
dom.slider.style.left = (25 + 60) * position + 'px'
if (animals[e.target.innerText] == answer[1]) {
dom.bingo.style.visibility = 'visible'
}
}</code></pre>
<p>现在,游戏开局时是没有提示语的,只有选对了头像,才会出提示语。<br>不过出现了一个bug,就是当重开新局时,滑块还停留在上一局的位置,我们要改成开局时把滑块 <code>dom.slider</code> 移到头像列表的最左侧:</p>
<pre><code class="javascript">function newGame() {
dom.bingo.style.visibility = 'hidden'
shuffle()
dom.slider.style.left = '0px'
}</code></pre>
<p>现在,整个程序流程已经可以跑通了:页面加载后即开始一局游戏,任意选择头像,在选择了正确的头像时出现 <code>Bingo!</code> 字样,点击 <code>Play Again</code> 按钮可以开始下一局游戏。<br>不过,在逻辑上还有一点小瑕疵。当用户已经选择了正确的头像,显示出提示语之后,不应该还能点选其他头像。为此,我们引入一个全局变量 <code>canSelect</code>,它是一个布尔值,表示当前是否可以选择头像,初始值是 <code>false</code>,在 <code>newGame()</code> 函数的最后一步,它的值被设置为 <code>true</code>,在 <code>select()</code> 函数中首先判断 <code>canSelect</code> 的值,只有当值为 <code>true</code> 时,才能继续执行事件处理的后续程序,当用户选择了正确的头像时,<code>canSelect</code> 被设置为 <code>false</code>,表示这一局游戏结束了。</p>
<pre><code class="javascript">let canSelect = false
function newGame() {
dom.bingo.style.visibility = 'hidden'
shuffle()
dom.slider.style.left = '0px'
canSelect = true
}
function select(e) {
if (!canSelect) return;
let position = _.findIndex(options, x => x[0] == e.target.innerText)
dom.slider.style.left = (25 + 60) * position + 'px'
if (animals[e.target.innerText] == answer[1]) {
canSelect = false
dom.bingo.style.visibility = 'visible'
}
}</code></pre>
<p>至此的全部脚本如下:</p>
<pre><code class="javascript">const animals = {
'🐭': '🐁',
'🐶': '🐕',
'🐷': '🐖',
'🐮': '🐄',
'🐯': '🐅',
'🐔': '🐓',
'🐵': '🐒',
'🐲': '🐉',
'🐴': '🐎',
'🐰': '🐇',
}
const dom = {
wholeBody: document.querySelector('.whole-body'),
bingo: document.querySelector('.bingo'),
again: document.querySelector('.again'),
faces: Array.from(document.querySelectorAll('.face')),
slider: document.querySelector('.slider'),
}
let options = []
let answer = {}
let canSelect = false
function newGame() {
dom.bingo.style.visibility = 'hidden'
shuffle()
dom.slider.style.left = '0px'
canSelect = true
}
function shuffle() {
options = _.slice(_.shuffle(_.entries(animals)), -5)
answer = _.sample(_.slice(options, -4))
dom.faces.forEach((face, i) => {
face.innerText = options[i][0]
})
dom.wholeBody.innerText = answer[1]
}
function select(e) {
if (!canSelect) return;
let position = _.findIndex(options, x => x[0] == e.target.innerText)
dom.slider.style.left = (25 + 60) * position + 'px'
if (animals[e.target.innerText] == answer[1]) {
canSelect = false
dom.bingo.style.visibility = 'visible'
}
}
function init() {
dom.faces.forEach(face => {
face.addEventListener('click', select)
})
dom.again.addEventListener('click', newGame)
newGame()
}
window.onload = init</code></pre>
<h3>三、动画效果</h3>
<p>游戏中共有 4 个动画效果,分别是移动滑块 <code>dom.slider</code>、显示提示语 <code>dom.bingo</code>、动物(包括头像列表和全身像)出场、动物入场。为了集中管理动画效果,我们定义一个全局常量 <code>animation</code>,它有 4 个属性,每个属性是一个函数,实现一个动画效果,结构如下:</p>
<pre><code class="javascript">const animation = {
moveSlider: () => {
//移动滑块...
},
showBingo: () => {
//显示提示语...
},
frameOut: () => {
//动物出场...
},
frameIn: () => {
//动物入场...
},
}</code></pre>
<p>其实这 4 个动画的运行时机已经体现在 <code>newGame()</code> 函数和 <code>select()</code> 函数中了:</p>
<pre><code class="javascript">function newGame() {
dom.bingo.style.visibility = 'hidden' //此处改为 动物出场 动画
shuffle()
dom.slider.style.left = '0px' //此处改为 动物入场 动画
canSelect = true
}
function select(e) {
if (!canSelect) return;
let position = _.findIndex(options, (o) => o[0] == e.target.innerText)
dom.slider.style.left = (25 + 60) * position + 'px' //此处改为 移动滑块 动画
if (animals[e.target.innerText] == answer[1]) {
canSelect = false
dom.bingo.style.visibility = 'visible' //此处改为 显示提示语 动画
}
}</code></pre>
<p>所以,我们就可以把这 4 行代码转移到 <code>animation</code> 中,其中 <code>moveSlider()</code> 还增加了一个指明要移动到什么位置的 <code>position</code> 参数:</p>
<pre><code class="javascript">const animation = {
moveSlider: (position) => {
dom.slider.style.left = (25 + 60) * position + 'px'
},
showBingo: () => {
dom.bingo.style.visibility = 'visible'
},
frameOut: () => {
dom.bingo.style.visibility = 'hidden'
},
frameIn: () => {
dom.slider.style.left = '0px'
},
}</code></pre>
<p>同时,<code>newGame()</code> 函数和 <code>select()</code> 函数改为调用 <code>animation</code>:</p>
<pre><code class="javascript">function newGame() {
animation.frameOut()
shuffle()
animation.frameIn()
canSelect = true
}
function select(e) {
if (!canSelect) return;
let position = _.findIndex(options, (o) => o[0] == e.target.innerText)
animation.moveSlider(position)
if (animals[e.target.innerText] == answer[1]) {
canSelect = false
animation.showBingo()
}
}</code></pre>
<p>经过上面的整理,接下来的动画代码就可以集中写在 <code>animation</code> 对象里了。<br>本项目的动画效果用 <a href="https://link.segmentfault.com/?enc=DGhMPdIsLlecfoeSfgiZoQ%3D%3D.PgqBia70DnFHyEJ9xC8cp8lTpQ60dxo2AdQFqfHwkGc%3D" rel="nofollow">gsap</a> 实现,gsap 动画在以前的 <a href="https://segmentfault.com/a/1190000016362691">133#项目</a>、<a href="https://segmentfault.com/a/1190000016377676">134#项目</a>、<a href="https://segmentfault.com/a/1190000016521212">143#项目</a> 都用到了,大家可参考这些项目了解 gsap 的使用方法。</p>
<p>引入 gsap 动画库:</p>
<pre><code class="html"><script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/2.0.2/TweenMax.min.js"></script></code></pre>
<p>先编写移动滑块的动画 <code>moveSlider</code>,让滑块先缩小,然后移动到目的地,再放大:</p>
<pre><code class="javascript">const animation = {
moveSlider: () => {
new TimelineMax()
.to(dom.slider, 1, {scale: 0.3})
.to(dom.slider, 1, {left: (25 + 60) * position + 'px'})
.to(dom.slider, 1, {scale: 1})
.timeScale(5)
},
//...
}</code></pre>
<p>再编写显示提示语的动画 <code>showBingo</code>,显示出 <code>dom.bingo</code> 之后,让它左右晃动一下:</p>
<pre><code class="javascript">const animation = {
//...
showBingo: () => {
new TimelineMax()
.to(dom.bingo, 0, {visibility: 'visible'})
.to(dom.bingo, 1, {rotation: -5})
.to(dom.bingo, 1, {rotation: 5})
.to(dom.bingo, 1, {rotation: 0})
.timeScale(8)
},
//...
}</code></pre>
<p>再编写动物出场的动画,隐藏提示语 <code>dom.bingo</code> 之后,再同时把滑块 <code>dom.slider</code>、头像列表 <code>dom.faces</code>、全身像 <code>dom.wholeBody</code> 同时缩小到消失:</p>
<pre><code class="javascript">const animation = {
//...
frameOut: () => {
new TimelineMax()
.to(dom.bingo, 0, {visibility: 'hidden'})
.to(dom.slider, 1, {scale: 0}, 't1')
.staggerTo(dom.faces, 1, {scale: 0}, 0.25, 't1')
.to(dom.wholeBody, 1, {scale: 0}, 't1')
.timeScale(5)
},
//...
}</code></pre>
<p>再编写动物入场的动画,把滑块移到头像列表最左侧之后,再把刚才出场动画缩小到消失的那些元素放大到正常尺寸:</p>
<pre><code class="javascript">const animation = {
//...
frameIn: () => {
new TimelineMax()
.to(dom.slider, 0, {left: '0px'})
.to(dom.wholeBody, 2, {scale: 1, delay: 1})
.staggerTo(dom.faces, 1, {scale: 1}, 0.25)
.to(dom.slider, 1, {scale: 1})
.timeScale(5)
},
}</code></pre>
<p>现在运行一下程序,已经有动画效果了,但是会觉得有些不协调,那是因为动画有一定的运行时长,多个动画连续运行时应该有先后顺序,比如应该先出场再入场、先移动滑块再显示提示语,但现在它们都是同时运行的。为了让它们能顺序执行,我们用 async/await 来改造,先让动画函数返回 promise 对象,以 <code>moveSlider</code> 为例,它被改成这样:</p>
<pre><code class="javascript">const animation = {
moveSlider: () => {
return new Promise(resolve => {
new TimelineMax()
.to(dom.slider, 1, {scale: 0.3})
.to(dom.slider, 1, {left: (25 + 60) * position + 'px'})
.to(dom.slider, 1, {scale: 1})
.timeScale(5)
.eventCallback('onComplete', resolve)
})
},
//...
}</code></pre>
<p>然后把 <code>select()</code> 函数改造成 async 函数,并在调用动画之前加入 <code>await</code> 关键字:</p>
<pre><code class="javascript">async function select(e) {
if (!canSelect) return;
let position = _.findIndex(options, (o) => o[0] == e.target.innerText)
await animation.moveSlider(position)
if (animals[e.target.innerText] == answer[1]) {
canSelect = false
animation.showBingo()
}
}</code></pre>
<p>现在点击头像时,若选择正确,要等到滑块动画结束之后才会显示提示语。再用相同的方法,改造其他几个动画和 <code>select()</code> 函数。到这里,整个游戏的动画效果就全部完成了。至此的全部脚本如下:</p>
<pre><code class="javascript">const animals = {
//略,与增加动画前相同
}
const dom = {
//略,与增加动画前相同
}
const animation = {
frameOut: () => {
return new Promise(resolve => {
new TimelineMax()
.to(dom.bingo, 0, {visibility: 'hidden'})
.to(dom.slider, 1, {scale: 0}, 't1')
.staggerTo(dom.faces, 1, {scale: 0}, 0.25, 't1')
.to(dom.wholeBody, 1, {scale: 0}, 't1')
.timeScale(5)
.eventCallback('onComplete', resolve)
})
},
frameIn: () => {
return new Promise(resolve => {
new TimelineMax()
.to(dom.slider, 0, {left: '0px'})
.to(dom.wholeBody, 2, {scale: 1, delay: 1})
.staggerTo(dom.faces, 1, {scale: 1}, 0.25)
.to(dom.slider, 1, {scale: 1})
.timeScale(5)
.eventCallback('onComplete', resolve)
})
},
moveSlider: (position) => {
return new Promise(resolve => {
new TimelineMax()
.to(dom.slider, 1, {scale: 0.3})
.to(dom.slider, 1, {left: (25 + 60) * position + 'px'})
.to(dom.slider, 1, {scale: 1})
.timeScale(5)
.eventCallback('onComplete', resolve)
})
},
showBingo: () => {
return new Promise(resolve => {
new TimelineMax()
.to(dom.bingo, 0, {visibility: 'visible'})
.to(dom.bingo, 1, {rotation: -5})
.to(dom.bingo, 1, {rotation: 5})
.to(dom.bingo, 1, {rotation: 0})
.timeScale(8)
.eventCallback('onComplete', resolve)
})
},
}
let options = []
let answer = {}
let canSelect = false
async function newGame() {
await animation.frameOut()
shuffle()
await animation.frameIn()
canSelect = true
}
function shuffle() {
//略,与增加动画前相同
}
async function select(e) {
if (!canSelect) return;
let position = _.findIndex(options, (o) => o[0] == e.target.innerText)
await animation.moveSlider(position)
if (animals[e.target.innerText] == answer[1]) {
canSelect = false
await animation.showBingo()
}
}
function init() {
//略,与增加动画前相同
}
window.onload = init</code></pre>
<p>大功告成!</p>
<h3>四、程序流程图</h3>
<p>最后,附上程序流程图,方便大家理解。其中蓝色条带表示动画,粉色椭圆表示用户操作,绿色矩形和菱形表示主要的程序逻辑,橙色平等四边形表示 canSelect 变量。</p>
<p><img src="/img/bVbknOX?w=1316&h=1138" alt="图片描述" title="图片描述"></p>
前端每日实战:162# 视频演示如何用原生 JS 创作一个查询 github 用户的应用(内含 2 个视频)
https://segmentfault.com/a/1190000017064196
2018-11-19T16:31:32+08:00
2018-11-19T16:31:32+08:00
comehope
https://segmentfault.com/u/comehope
27
<p><img src="/img/bVbjLk3?w=400&h=301" alt="图片描述" title="图片描述"></p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/oQGqaG">https://codepen.io/comehope/pen/oQGqaG</a></p>
<h2>可交互视频</h2>
<p>此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。</p>
<p>请用 chrome, safari, edge 打开观看。</p>
<p>第 1 部分:<br><a href="https://link.segmentfault.com/?enc=mYdEoolWzoHPa3x0wogJXg%3D%3D.tHFjCsucO3xONBK3qfzvT%2Fzl3J0EX21kHtfcjXN3zM7rabAB4rJmhfNMiZazuogq" rel="nofollow">https://scrimba.com/p/pEgDAM/cEPkVUg</a></p>
<p>第 2 部分:<br><a href="https://link.segmentfault.com/?enc=G3sqrgltmkC22Cwv9ctlag%3D%3D.dOS2zPOZoVfHdelQAoWmbiLImsC%2BxfRvx1vTHSMh1h9wErCWSgY%2BXiC4P00MfGsc" rel="nofollow">https://scrimba.com/p/pEgDAM/crp63TR</a></p>
<p>(因为 scrimba 不支持 web animation api,第 2 部分末尾的动画效果在视频中看不到,请参考 codepen)</p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=dcs4t6B%2FkFOMekuKTeSF1g%3D%3D.1ZeewbALTBOpoRMcYEkpsbmnvUfrFQOfQfnLsBX5zUgvKtgjp1YjC4mpNDZdQxIGuz4%2FzeU0e66pZleGUOpWpw%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<p>这是一个读取 github 用户的应用,在搜索框内输入用户名,点击搜索后,就会通过 github 的 open api 取得用户信息,填写到下方的卡片中。</p>
<p>整个应用分成 3 个步骤开发:静态的页面布局、从 open api 读取数据并绑定到页面中、增加动画效果。</p>
<h3>一、页面布局</h3>
<p>定义 dom,整体结构分成上部的表单和下部的用户卡片:</p>
<pre><code class="html"><div class="app">
<form>
<!-- 暂略 -->
</form>
<div class="profile">
<!-- 暂略 -->
</div>
</div></code></pre>
<p>居中显示:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: #383838;
}</code></pre>
<p>定义应用容器的尺寸:</p>
<pre><code class="css">.app {
width: 320px;
height: 630px;
font-family: sans-serif;
position: relative;
}</code></pre>
<p>这是表单的 dom 结构,2 个表单控件分别是文本输入框 <code>#username</code> 和搜索按钮 <code>#search</code>,因为后面的脚本要引用这 2 个控件,所以为它们定义了 id 属性,接下来在 css 中也使用 id 选择器:</p>
<pre><code class="html"><form>
<input placeholder="Who are you looking for?" id="username">
<input type="button" value="Search" id="search">
</form></code></pre>
<p>令 2 个表单控件横向排列:</p>
<pre><code class="css">form {
height: 50px;
background-color: rgba(255, 255, 255, 0.2);
border-radius: 2px;
box-sizing: border-box;
padding: 8px;
display: flex;
}</code></pre>
<p>分别设置 2 个表单控件的样式:</p>
<pre><code class="css">input {
border: none;
font-size: 14px;
outline: none;
border-radius: inherit;
padding: 0 8px;
}
#username {
flex-grow: 1;
background-color: rgba(255, 255, 255, 0.9);
color: #42454e;
}
#search {
background-color: rgba(0, 97, 145, 0.75);
color: rgba(255, 255, 255, 0.8);
font-weight: bold;
margin-left: 8px;
cursor: pointer;
}</code></pre>
<p>为按钮增加悬停和点击的交互效果:</p>
<pre><code class="css">#search:hover {
background-color: rgba(0, 97, 145, 0.45);
}
#search:active {
transform: scale(0.98);
background-color: rgba(0, 97, 145, 0.75);
}</code></pre>
<p>至此,表单布局完成,接下来做用户卡片布局。<br>用户卡片的 dom 结构如下,卡片分成上半部分 <code>.header</code> 和下半部分 <code>.footer</code>,上半部分包括头像 <code>.avatar</code>、名字 <code>.name</code> 和位置 <code>.location</code>,下半部分包括一组详细的数据 <code>.details</code> 和一个跳到 github 的链接 <code>.to-github</code>:</p>
<pre><code class="html"><div class="profile">
<div class="header">
<div class="avatar"></div>
<h2 class="name">Octocat</h2>
<h3 class="location">San Francisco</h3>
</div>
<div class="footer">
<ul class="details">
<li>Repositories<span>111</span></li>
<li>Followers<br><span>222</span></li>
<li>Following<br><span>333</span></li>
</ul>
<a href="#" class="to-github">go to github</a>
</div>
</div></code></pre>
<p>令卡片的上半部分和下半部分竖向排列,并分别设置两部分的高度,大约是上半部分占卡片高度的三分之二,下半部分占卡片高度的三分之一,此时可以看出卡片的轮廓了:</p>
<pre><code class="css">.profile {
width: 320px;
position: absolute;
margin: 20px 0 0 0;
display: flex;
flex-direction: column;
border-radius: 5px;
}
.header {
height: 380px;
background-color: rgba(0, 97, 145, 0.45);
}
.footer {
height: 180px;
background-color: rgba(0, 97, 145, 0.75);
}</code></pre>
<p>令卡片上半部分的子元素竖向排列:</p>
<pre><code class="css">.header {
display: flex;
flex-direction: column;
align-items: center;
}</code></pre>
<p>设置头像图片,样式为描边的圆形,因为头像图片在后面还会用到,所以把它存储到变量 <code>--avatar</code> 中:</p>
<pre><code class="css">.profile {
--avatar: url('https://avatars3.githubusercontent.com/u/583231?v=4');
}
.avatar {
width: 140px;
height: 140px;
background-image: var(--avatar);
margin: 70px 0 0 0;
background-position: center;
background-size: cover;
border-radius: 50%;
box-shadow:
0 0 0 0.8em rgba(0, 0, 0, 0.2),
0 0 0 1em rgba(161, 220, 255, 0.35);
}</code></pre>
<p>设置名字和位置信息的样式,文字为白色:</p>
<pre><code class="css">.name {
margin: 50px 0 0 0;
color: white;
font-size: 28px;
font-weight: normal;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
}
.location {
margin: 5px 0 0 0;
color: rgba(255, 255, 255, 0.75);
font-weight: normal;
}</code></pre>
<p>至此,上半部分的布局完成,接下来布局下半部分。<br>令下半部分的子元素竖向排列:</p>
<pre><code class="css">.footer {
display: flex;
flex-direction: column;
align-items: center;
}</code></pre>
<p>横向排列三组数据,每项之间加入细分隔线:</p>
<pre><code class="css">.details {
list-style-type: none;
padding: 0;
display: flex;
margin: 40px 0 0 0;
}
.details li {
color: rgba(255, 255, 255, 0.6);
text-align: center;
padding: 0 6px;
}
.details li span {
display: block;
color: rgba(255, 255, 255, 0.8);
}
.details li:not(:first-child) {
border-left: 2px solid rgba(255, 255, 255, 0.15);
}</code></pre>
<p>设置跳转到 github 的链接样式和悬停效果:</p>
<pre><code class="css">.to-github {
width: 200px;
height: 40px;
background-color: rgba(255, 255, 255, 0.5);
text-align: center;
line-height: 40px;
color: rgba(0, 0, 0, 0.75);
text-decoration: none;
text-transform: uppercase;
border-radius: 20px;
transition: 0.3s;
}
.to-github:hover {
background-color: rgba(255, 255, 255, 0.8);
}</code></pre>
<p>至此,下半部分布局完成。<br>接下来用伪元素把头像图片作为整体背景:</p>
<pre><code class="css">.profile {
position: relative;
overflow: hidden;
}
.profile::before {
content: '';
position: absolute;
width: calc(100% + 20px * 2);
height: calc(100% + 20px * 2);
background-image: var(--avatar);
background-size: cover;
z-index: -1;
margin: -20px;
filter: blur(10px);
}</code></pre>
<p>到这里,整体的静态布局就完成了。</p>
<h3>二、绑定数据</h3>
<p>为了绑定数据,我们引入一个羽量级的模板库:</p>
<pre><code class="html"><script src="https://blueimp.github.io/JavaScript-Templates/js/tmpl.min.js"></script></code></pre>
<p>把卡片 <code>.profile</code> 包含的 dom 结构改写为 html 模板 <code>#template</code>,其中的 <code>o</code> 代表绑定的数据数据对象:</p>
<pre><code class="html"><script type="text/x-tmpl" id="template">
<div class="header">
<div class="avatar"></div>
<h2 class="name">{%= o.name %}</h2>
<h3 class="location">{%= o.location %}</h3>
</div>
<div class="footer">
<ul class="details">
<li>Repositories<span>{%= o.public_repos %}</span></li>
<li>Followers<br><span>{%= o.followers %}</span></li>
<li>Following<br><span>{%= o.following %}</span></li>
</ul>
<a href="{%= o.html_url %}" class="to-github">go to github</a>
</div>
</script></code></pre>
<p>声明一个假数据对象 <code>mockData</code>,它的数据结构与 github open api 的数据结构是一致的:</p>
<pre><code class="javascript">let mockData = {
"avatar_url": "https://avatars3.githubusercontent.com/u/583231?v=4",
"name": "The Octocat",
"location": "San Francisco",
"public_repos": 111,
"followers": 222,
"following": 333,
"html_url": "https://github.com/octocat",
}</code></pre>
<p>定义一个把数据绑定到 html 模板的函数 <code>render(container, data)</code>,第 1 个参数 <code>container</code> 表示 dom 容器,模板内容将填充在此容器中;第 2 个参数是数据对象。在页面载入时调用 <code>render()</code> 方法,把 <code>mockData</code> 作为参数传入,此时看到的效果和纯静态的效果一致,但用户卡片已经改为动态创建了:</p>
<pre><code class="javascript">window.onload = render(document.getElementsByClassName('profile')[0], mockData)
function render(container, data) {
container.innerHTML = tmpl('template', data)
container.style.setProperty('--avatar', `url(${data.avatar_url})`)
}</code></pre>
<p>定义一个从 github open api 读取用户信息的方法 <code>getData(username)</code>,然后调用 <code>render()</code> 方法把用户信息绑定到 html 模板。同时,把 <code>window.onload</code> 绑定的事件改为调用 <code>getData()</code> 方法,此时看到的效果仍和纯静态的效果一致,但数据已经变成动态读取了:</p>
<pre><code class="javascript">window.onload = getData('octocat')
function getData(username) {
let apiUrl = `https://api.github.com/users/${username}`
fetch(apiUrl)
.then((response) => response.json())
.then((data) => render(document.getElementsByClassName('profile')[0], data))
}</code></pre>
<p>为表单的 <code>search</code> 按钮绑定点击事件,实现搜索功能。可以查一下自己的 github 帐号试试看:</p>
<pre><code class="javascript">document.getElementById('search').addEventListener('click', () => {
let username = document.getElementById('username').value.replace(/[ ]/g, '')
if (username == '') {
return
}
getData(username)
})</code></pre>
<h3>三、增加动画效果</h3>
<p>为了能让用户感受到每次搜索后数据的变化过程,我们增加一点动画效果。创建一个 <code>update(data)</code> 函数来处理动画和渲染逻辑,同时把 <code>getData()</code> 函数的最后一步改为调用 <code>update()</code> 函数:</p>
<pre><code class="javascript">function getData(username) {
let apiUrl = `https://api.github.com/users/${username}`
fetch(apiUrl)
.then((response) => response.json())
// .then((data) => render(document.getElementsByClassName('profile')[0], data))
.then(update)
}
function update(data) {
let current = document.getElementsByClassName('profile')[0]
render(current, data)
}</code></pre>
<p>当页面首次载入时,不需要动画,直接渲染默认的用户信息即可。变量 <code>isInitial</code> 表示本次调用是否是在初始化页面时调用的,若是,就直接渲染。若不是,下面会执行动画效果。</p>
<pre><code class="javascript">function update(data) {
let current = document.getElementsByClassName('profile')[0]
let isInitial = (current.innerHTML == '')
if (isInitial) {
render(current, data)
return
}
}</code></pre>
<p>动画的过程是:创建一张新卡片,把数据绑定到新卡片上,然后把当前卡片移出视图,再把新卡片移入视图。下面的变量 <code>next</code> 代表新创建的卡片,把它定位到当前卡片的右侧:</p>
<pre><code class="javascript">function update(data) {
let current = document.getElementsByClassName('profile')[0]
let isInitial = (current.innerHTML == '')
if (isInitial) {
render(current, data)
return
}
let next = document.createElement('div')
next.className = 'profile'
next.style.left = '100%'
render(next, data)
current.after(next)
}</code></pre>
<p>因为动画分成 2 个动作——当前卡片移出和新卡片移入,所以我们定义 2 个动画效果,变量 <code>animationOut</code> 代表移出动画的参数,变量 <code>animationIn</code> 代表移入动画的参数。其中,<code>keyframes</code> 属性值相当于写 css 动画时用 <code>@keyframes</code> 定义的关键帧,<code>options</code> 属性值相当于写 css 动画时 <code>animation</code> 语句后面的参数,新卡片移入动画有半秒钟的延时。</p>
<pre><code class="javascript">function update(data) {
let current = document.getElementsByClassName('profile')[0]
let isInitial = (current.innerHTML == '')
if (isInitial) {
render(current, data)
return
}
let next = document.createElement('div')
next.className = 'profile'
next.style.left = '100%'
render(next, data)
current.after(next)
let animationOut = {
keyframes: [
{left: '0', opacity: 1, offset: 0},
{left: '-100%', opacity: 0, offset: 1}
],
options: {
duration: 500,
fill: 'forwards'
}
}
let animationIn = {
keyframes: [
{left: '100%', opacity: 0, offset: 0},
{left: '0', opacity: 1, offset: 1}
],
options: {
duration: 500,
fill: 'forwards',
delay: 500
}
}
}</code></pre>
<p>因为动画需异步执行,即在当前卡片移出的动画结束后再执行新卡片移入的动画,所以我们令当前卡片移出的动画结束后触发 <code>onfinish</code> 事件,然后再执行新卡片移入的动画,同时把旧卡片删除掉:</p>
<pre><code class="javascript">function update(data) {
let current = document.getElementsByClassName('profile')[0]
let isInitial = (current.innerHTML == '')
if (isInitial) {
render(current, data)
return
}
let next = document.createElement('div')
next.className = 'profile'
next.style.left = '100%'
render(next, data)
current.after(next)
let animationOut = {
keyframes: [
{left: '0', opacity: 1, offset: 0},
{left: '-100%', opacity: 0, offset: 1}
],
options: {
duration: 500,
fill: 'forwards'
}
}
let animationIn = {
keyframes: [
{left: '100%', opacity: 0, offset: 0},
{left: '0', opacity: 1, offset: 1}
],
options: {
duration: 500,
fill: 'forwards',
delay: 500
}
}
let animate = current.animate(animationOut.keyframes, animationOut.options)
animate.onfinish = function() {
current.remove()
next.animate(animationIn.keyframes, animationIn.options)
}
}</code></pre>
<p>最后,限定动画效果仅在 <code>.app</code> 容器中展现:</p>
<pre><code class="css">.app {
overflow: hidden;
}</code></pre>
<p>大功告成!</p>
前端每日实战:161# 视频演示如何用纯 CSS 创作一张纪念卓别林的卡片
https://segmentfault.com/a/1190000016791409
2018-10-24T17:43:17+08:00
2018-10-24T17:43:17+08:00
comehope
https://segmentfault.com/u/comehope
33
<p><img src="/img/bVbiCnk?w=400&h=301" alt="图片描述" title="图片描述"></p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/WaaBNV">https://codepen.io/comehope/pen/WaaBNV</a></p>
<h2>可交互视频</h2>
<p>此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。</p>
<p>请用 chrome, safari, edge 打开观看。</p>
<p><a href="https://link.segmentfault.com/?enc=WWmtxMV6SS9PDv0CtzqULg%3D%3D.LJ5IuWr0UqxwQHLP%2BPjwNEyJhuxW%2F8NvwrqTXEDbxmC1cwsS36gkYbDP%2FXyFbI1s" rel="nofollow">https://scrimba.com/p/pEgDAM/c3DQeC7</a></p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=dDsjjAEvUWmN6NkPL9%2BFow%3D%3D.mPDapeGBQGJTy11V05idRCPG4mxqjxsgwcxWTQGPh8mx3Ekea%2BxskuF3ITfK9XnSWXHCIT0sju9w2HHfK9iqEA%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<p>定义 dom,容器中包含的 3 个元素分别代表帽子、胡须和手杖:</p>
<pre><code class="html"><figure class="chaplin">
<span class="hat"></span>
<span class="beard"></span>
<span class="stick"></span>
</figure></code></pre>
<p>居中显示:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}</code></pre>
<p>定义容器尺寸,并设置子元素水平居中:</p>
<pre><code class="css">.chaplin {
width: 40em;
height: 30em;
font-size: 10px;
background-color: #eee;
box-shadow: 0 0 3em rgba(0, 0, 0, 0.2);
display: flex;
flex-direction: column;
align-items: center;
}</code></pre>
<p>定义默认颜色,后面用 <code>currentColor</code> 引用此颜色:</p>
<pre><code class="css">.chaplin {
color: #555;
}</code></pre>
<p>画出帽子的轮廓:</p>
<pre><code class="css">.chaplin {
position: relative;
}
.hat {
position: absolute;
width: 6.4em;
height: 4.6em;
background-color: currentColor;
border-radius: 2.3em 2.3em 0 0;
top: 1.4em;
}</code></pre>
<p>用伪元素画出帽沿:</p>
<pre><code class="css">.hat::before {
content: '';
position: absolute;
width: 10em;
height: 0.8em;
background-color: currentColor;
border-radius: 0.4em;
top: calc(100% + 0.4em);
left: calc((100% - 10em) / 2);
}</code></pre>
<p>画出胡子:</p>
<pre><code class="css">.beard {
position: absolute;
width: 1.5em;
height: 0;
top: 11.6em;
border: solid transparent;
border-width: 0 0.4em 1em 0.4em;
border-bottom-color: currentColor;
}</code></pre>
<p>画出手杖的杖杆:</p>
<pre><code class="css">.stick {
position: absolute;
width: 0.8em;
height: 10.5em;
background-color: currentColor;
bottom: 0;
}</code></pre>
<p>用 <code>::before</code> 伪元素画出手杖的握柄:</p>
<pre><code class="css">.stick::before {
content: '';
position: absolute;
box-sizing: border-box;
width: 5.6em;
height: 3em;
border: 0.8em solid;
border-radius: 5.6em 5.6em 0 0;
border-bottom: none;
top: -3em;
}</code></pre>
<p>用 <code>::after</code> 伪元素修饰握柄的端点,使其圆润自然:</p>
<pre><code class="css">.stick::after {
content: '';
position: absolute;
width: 0.8em;
height: 0.8em;
background-color: currentColor;
border-radius: 50%;
left: calc(5.6em - 0.8em);
top: -0.4em;
}</code></pre>
<p>使手杖水平居中:</p>
<pre><code class="css">.stick {
left: calc((100% - (5.6em - 0.8em)) / 2);
}</code></pre>
<p>至此,抽象的卓别林形象完成,接下来排版一句他的名言。<br>在 dom 中增加一个 <code>.quote</code> 元素,并把一句话分为 3 段:</p>
<pre><code class="html"><figure class="chaplin">
<span class="hat"></span>
<span class="beard"></span>
<span class="stick"></span>
<p class="quote">
<span>a day without</span>
<span>laughter</span>
<span>is a day wasted</span>
</p>
</figure></code></pre>
<p>定位文字,并竖排 3 段文字:</p>
<pre><code class="css">.quote {
position: absolute;
left: 50%;
bottom: 2.5em;
font-family: sans-serif;
text-transform: uppercase;
font-weight: bold;
display: flex;
flex-direction: column;
}</code></pre>
<p>调整字号和字间距,使 3 段文字对齐:</p>
<pre><code class="css">.quote span:nth-child(1) {
letter-spacing: 0.05em;
}
.quote span:nth-child(2) {
font-size: 1.6em;
}</code></pre>
<p>大功告成!</p>
前端每日实战:160# 视频演示如何用纯 CSS 创作一个打开内容弹窗的交互动画
https://segmentfault.com/a/1190000016774570
2018-10-23T14:57:01+08:00
2018-10-23T14:57:01+08:00
comehope
https://segmentfault.com/u/comehope
34
<p><img src="/img/bVbixZI?w=400&h=302" alt="图片描述" title="图片描述"></p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/GYXvez">https://codepen.io/comehope/pen/GYXvez</a></p>
<h2>可交互视频</h2>
<p>此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。</p>
<p>请用 chrome, safari, edge 打开观看。</p>
<p><a href="https://link.segmentfault.com/?enc=z%2FyYY9CZfcwBLxHM59Mz2A%3D%3D.NLcrQKR25%2Fhoe4vQAegwv99SBZl2Qja%2BgGh5J4A8l9vlkoWF8FkpxuDKc%2FhQPWuY" rel="nofollow">https://scrimba.com/p/pEgDAM/cNzVnAL</a></p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=E08DWC2s%2BInOJS3RO4zZcg%3D%3D.i7A4u3PxX4opKRLy3VW3c%2F1WPCJ3LAXC3RXOWaaPnJS8sENWjAiY0ZdshsT3YKZkZEBPOG3nPcOh8cUV1RPDdw%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<p>定义 dom,一个名为 <code>.main</code> 的容器中包含 1 个链接:</p>
<pre><code class="html"><div class="main">
<a href="#" class="open-popup">open popup</a>
</div></code></pre>
<p>设置页面的基本属性:无边距、全高、忽略溢出:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
overflow: hidden;
}</code></pre>
<p>设置主界面的背景和其中按钮的布局方式:</p>
<pre><code class="css">.main {
height: inherit;
background: linear-gradient(dodgerblue, darkblue);
display: flex;
align-items: center;
justify-content: center;
}</code></pre>
<p>设置按钮样式:</p>
<pre><code class="css">.open-popup {
box-sizing: border-box;
color: white;
font-size: 16px;
font-family: sans-serif;
width: 10em;
height: 4em;
border: 1px solid;
text-align: center;
line-height: 4em;
text-decoration: none;
text-transform: capitalize;
}</code></pre>
<p>设置按钮悬停效果:</p>
<pre><code class="css">.open-popup:hover {
border-width: 2px;
}</code></pre>
<p>至此,主界面完成,接下来制作弹窗。<br>在 dom 中增加的 <code>.popup</code> 小节表示弹窗内容,其中的 <code><a></code> 是返回按钮,<code><p></code> 是具体内容,这里我们把内容简化为一些陆生动物的 unicode 字符,为了能够触发这个弹窗,设置 <code>.popup</code> 的 <code>id</code> 为 <code>terrestrial</code>,并在 <code>.main</code> 的 <code><a></code> 链接中指向它:</p>
<pre><code class="html"><div class="main">
<a href="#terrestrial" class="open-popup">terrestrial animals</a>
</div>
<section id="terrestrial" class="popup">
<a href="#" class="back">&lt; back</a>
<p>??????????</p>
</section></code></pre>
<p>设置弹窗的尺寸,它将覆盖刚才的 <code>.main</code> 的内容:</p>
<pre><code class="css">.popup {
position: absolute;
top: 0;
width: 100%;
height: inherit;
display: flex;
flex-direction: column;
justify-content: start;
}</code></pre>
<p>设置返回按钮的样式:</p>
<pre><code class="css">.popup .back {
font-size: 20px;
font-family: sans-serif;
text-align: center;
height: 2em;
line-height: 2em;
background-color: #ddd;
color: black;
text-decoration: none;
}
.popup .back:visited {
color: black;
}
.popup .back:hover {
background-color: #eee;
}</code></pre>
<p>设置内容的样式:</p>
<pre><code class="css">.popup p {
font-size: 100px;
text-align: center;
margin: 0.1em 0.05em;
}</code></pre>
<p>设置弹窗内容默认是不显示的,只有点击主界面的链接时才显示:</p>
<pre><code class="css">.popup {
display: none;
}
.popup:target {
display: flex;
}</code></pre>
<p>至此,弹窗完成,但弹窗中的内容是重叠在主界面上面的,接下来制作从主界面到弹窗的动画效果。<br>动画效果包含 3 个步骤:页面中间的一条直线从左端横穿到右端,页面中间大幕向上下两端拉开,最后内容淡入,下面的制作过程依次是第 3 个步骤、第 2 个步骤、第 1 个步骤。<br>先让弹窗内容淡入:</p>
<pre><code class="css">.popup > * {
filter: opacity(0);
animation: fade 0.5s ease-in forwards;
}
@keyframes fade{
to {
filter: opacity(1);
}
}</code></pre>
<p>用伪元素 <code>::before</code> 制作一个白色背景,从页面中间向上下两端展开:</p>
<pre><code class="css">.popup::before {
content: '';
position: absolute;
box-sizing: border-box;
width: 100%;
height: 0;
top: 50%;
background-color: white;
animation: open-animate 0.5s cubic-bezier(0.8, 0.2, 0, 1.2) forwards;
}
@keyframes open-animate {
to {
height: 100vh;
top: 0;
}
}</code></pre>
<p>设置弹窗淡入动画的延时时长,形成先大幕拉开再显示内容的效果:</p>
<pre><code class="css">.popup > * {
animation-delay: 0.5s;
}</code></pre>
<p>用伪元素 <code>::after</code> 制作一条横线,从页面左端横穿到右端:</p>
<pre><code class="css">.popup::after {
content: '';
position: absolute;
width: 0;
height: 2px;
background-color: white;
top: calc((100% - 2px) / 2);
left: 0;
animation: line-animate 0.5s cubic-bezier(0.8, 0.2, 0, 1.2);
}
@keyframes line-animate {
50%, 100% {
width: 100%;
}
}</code></pre>
<p>再设置弹窗淡入动画和大幕拉开动画的延时时长,让动画效果依次执行:</p>
<pre><code class="css">.popup > * {
animation-delay: 1s;
}
.popup::before {
animation-delay: 0.5s;
}</code></pre>
<p>至此,整个动画效果完成。<br>在 dom 再中增加一组水生动物的内容,以及打开它的链接:</p>
<pre><code class="html"><div class="main">
<a href="#terrestrial" class="open-popup">terrestrial animals</a>
<a href="#aquatic" class="open-popup">aquatic animals</a>
</div>
<section id="terrestrial" class="popup">
<a href="#" class="back">&lt; back</a>
<p>??????????</p>
</section>
<section id="aquatic" class="popup">
<a href="#" class="back">&lt; back</a>
<p>??????????</p>
</section></code></pre>
<p>最后,设置一下主界面上按钮的间距:</p>
<pre><code class="css">.open-popup {
margin: 1em;
}</code></pre>
<p>大功告成!</p>
前端每日实战:159# 视频演示如何用 CSS 和原生 JS 创作一个展示苹果设备的交互动画
https://segmentfault.com/a/1190000016750591
2018-10-21T05:07:07+08:00
2018-10-21T05:07:07+08:00
comehope
https://segmentfault.com/u/comehope
26
<p><img src="/img/bVbirKY?w=400&h=307" alt="图片描述" title="图片描述"></p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/gBKWdW">https://codepen.io/comehope/pen/gBKWdW</a></p>
<h2>可交互视频</h2>
<p>此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。</p>
<p>请用 chrome, safari, edge 打开观看。</p>
<p>第 1 部分:<br><a href="https://link.segmentfault.com/?enc=Uf2OEOKlwEWdWGMKuo3F0w%3D%3D.2OXgf4Aq%2BeVhBmlnUzUKTw3LcR490tZ57b%2BHqs%2BYfQCULxIiq91%2FA1ogOZd8z1Xl" rel="nofollow">https://scrimba.com/p/pEgDAM/cazRgcL</a></p>
<p>第 2 部分:<br><a href="https://link.segmentfault.com/?enc=x0QBdFdSbPYGGeUyawsHwQ%3D%3D.sRMNVXF73bQWNgLAv13kBhfRUoasvxIp3HZvFOGPvacNlXA7hw6eeL1DkKNTn5%2B1" rel="nofollow">https://scrimba.com/p/pEgDAM/ceDK7cB</a></p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=nkvzR4eTrlxlLYSDKW37%2Bg%3D%3D.IDjSyDyvrPqUnjkH%2BeFb0jDXSKYETXUk%2FQkdLEP9JwLuwnrHdtW8kyci4gR9sPwzSkrHuusgf0F5SOsZtxhgkA%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<p>定义 dom,包含 5 个子元素,分别代表 iphone, mini, ipad, macbook, imac 这 5 种设备:</p>
<pre><code class="html"><div class="container">
<div class="device iphone"></div>
<div class="device mini"></div>
<div class="device ipad"></div>
<div class="device macbook"></div>
<div class="device imac"></div>
</div></code></pre>
<p>居中显示:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: #aaa;
}</code></pre>
<p>设置容器中子元素的布局方式:</p>
<pre><code class="css">.container {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
}</code></pre>
<p>设置设备的共有属性,线性渐变图案将作为屏幕的背景:</p>
<pre><code class="css">.device {
box-sizing: border-box;
position: relative;
display: flex;
justify-content: center;
background: linear-gradient(120deg, #ddd 30%, #ccc 30%);
}
.device::before,
.device::after {
content: '';
position: absolute;
}</code></pre>
<p>iphone, mini, ipad 的造型相似,都有顶部摄像头、传感器开口和底部按钮,所以这些共有属性可以一起设置,用 <code>::before</code> 伪元素画出顶部细节,<code>::after</code> 伪元素画出底部按钮:</p>
<pre><code class="css">.iphone::before,
.mini::before,
.ipad::before {
width: 2px;
height: 2px;
border-style: solid;
border-color: #a5adbe;
border-width: 0 12px 0 2px;
}
.iphone::after,
.mini::after,
.ipad::after {
width: 8px;
height: 8px;
background-color: white;
border-radius: 50%;
}</code></pre>
<p>接下来逐个画出设备。先画出 iphone 的轮廓:</p>
<pre><code class="css">.iphone {
width: 59px;
height: 124px;
border: #484f5e solid;
border-width: 18px 4px;
border-radius: 6px;
}</code></pre>
<p>定位 iphone 的顶部和底部细节:</p>
<pre><code class="css">.iphone::before {
top: -10px;
}
.iphone::after {
bottom: -13px;
}</code></pre>
<p>类似地,画出 mini:</p>
<pre><code class="css">.mini {
width: 93px;
height: 138px;
border: #484f5e solid;
border-width: 14px 5px;
border-radius: 10px;
}
.mini::before {
top: -8px;
}
.mini::after {
bottom: -11px;
}</code></pre>
<p>再画出 ipad:</p>
<pre><code class="css">.ipad {
width: 134px;
height: 176px;
border: #484f5e solid;
border-width: 18px 13px;
border-radius: 12px;
}
.ipad::before {
top: -10px;
}
.ipad::after {
bottom: -13px;
}</code></pre>
<p>接下来画 macbook,先画屏幕:</p>
<pre><code class="css">.macbook {
width: 234px;
height: 155px;
border: 8px solid #484f5e;
border-radius: 7px 7px 0 0;
}</code></pre>
<p>用 <code>::before</code> 伪元素画出摄像头:</p>
<pre><code class="css">.macbook::before {
width: 294px;
height: 14px;
background-color: #e8ebf0;
top: calc(100% + 8px);
border-radius: 0 0 14px 14px;
}</code></pre>
<p>用 <code>::after</code> 伪元素画出主机:</p>
<pre><code class="css">.macbook::after {
width: 3px;
height: 3px;
background-color: #a5adbe;
top: -6px;
border-radius: 50%;
}</code></pre>
<p>接下来画 imac,先画屏幕,屏幕的左、上、右的黑色边框没有用 <code>border</code> 属性画,是因为 <code>border</code> 会在端点处遗留一个斜角,所以改用 <code>box-shadow</code> 实现:</p>
<pre><code class="css">.imac {
width: 360px;
height: 215px;
border-radius: 10px;
box-shadow:
inset 0 14px #484f5e,
inset 14px 0 #484f5e,
inset -14px 0 #484f5e;
border-bottom: 33px solid #e8ebf1;
transform: translateY(14px);
}</code></pre>
<p>用 <code>::before</code> 伪元素画出梯形的底座:</p>
<pre><code class="css">.imac::before {
width: 90px;
height: 0;
top: calc(100% + 33px);
border: solid transparent;
border-bottom-color: #e2e4e8;
border-width: 0 10px 47px 10px;
}</code></pre>
<p>用 <code>::after</code> 伪元素画出顶部的摄像头和屏幕底部的按钮,注意按钮是用 <code>box-shadow</code> 实现的:</p>
<pre><code class="css">.imac::after {
width: 4px;
height: 4px;
background-color: #a5adbe;
top: 5px;
border-radius: 50%;
box-shadow: 0 191px 0 4px #464e5d;
}</code></pre>
<p>至此,设备全部绘制完成。<br>删除除 iphone 之外的其他设备的 dom 元素,只保留 1 个 dom 元素,后面的动画效果都在这个 dom 元素上变化:</p>
<pre><code class="html"><div class="container">
<div class="device iphone"></div>
<!-- <div class="device mini"></div>
<div class="device ipad"></div>
<div class="device macbook"></div>
<div class="device imac"></div> -->
</div></code></pre>
<p>设置容器尺寸,子元素垂直居中,设备的高度占容器高度的 75%:</p>
<pre><code class="css">.container {
width: 360px;
height: 350px;
justify-content: center;
}
.device {
transform: translateY(-25%);
}</code></pre>
<p>在 dom 中增加 2 个按钮元素,分别用 <code>.left</code> 和 <code>.right</code> 表示:</p>
<pre><code class="html"><div class="container">
<div class="device iphone"></div>
<div class="buttons">
<span class="left"></span>
<span class="right"></span>
</div>
</div></code></pre>
<p>定位按钮的位置:</p>
<pre><code class="css">.buttons {
position: absolute;
width: inherit;
font-size: 30px;
height: 2em;
bottom: 0;
display: flex;
justify-content: space-around;
}
.buttons > * {
position: relative;
width: 4em;
}</code></pre>
<p>按钮为向左和向右的箭头:</p>
<pre><code class="css">.buttons > *::before {
position: absolute;
}
.buttons .left::before {
content: '←';
right: 0;
}
.buttons .right::before {
content: '→';
}</code></pre>
<p>设置按钮样式为圆形:</p>
<pre><code class="css">.buttons > *::before {
position: absolute;
width: 2em;
height: 2em;
background-color: #484f5e;
color: silver;
text-align: center;
line-height: 2em;
border-radius: 1em;
cursor: pointer;
}</code></pre>
<p>增加鼠标悬停效果:</p>
<pre><code class="css">.buttons > *::before {
transition: 0.2s;
}
.buttons .left:hover::before {
width: 4em;
content: '⟵';
}
.buttons .right:hover::before {
width: 4em;
content: '⟶';
}</code></pre>
<p>增加按钮点击效果:</p>
<pre><code class="css">.buttons > *:active {
transform: scale(0.9);
filter: brightness(0.8);
}</code></pre>
<p>至此,按钮制作完毕,接下来创建交互脚本。</p>
<p>定义一个获取元素的函数 <code>$</code>:</p>
<pre><code class="javascript">const $ = (className) => document.getElementsByClassName(className)[0]</code></pre>
<p>定义一个存放设备名称的数组:</p>
<pre><code class="javascript">let devices = ['iphone', 'mini', 'ipad', 'macbook', 'imac']</code></pre>
<p>定义点击行为对数据的加工方法,当点击左侧按钮时,把数组最左边的 1 个元素移到最右边,相反地,当点击右侧按钮时,把数组最右边的 1 个元素移到最左边,这样就可以从 2 个方向循环遍历数组了:</p>
<pre><code class="javascript">let loop = {
'left': () => devices.unshift(devices.pop()),
'right': () => devices.push(devices.shift())
}</code></pre>
<p>定义点击事件,根据数组的变化切换设备:</p>
<pre><code class="javascript">Array.from($('buttons').children).forEach(element =>
element.addEventListener('click', function(e) {
loop[e.target.className]()
$('device').className = 'device ' + devices[0]
})
)</code></pre>
<p>最后,设置设备切换的缓动效果:</p>
<pre><code class="css">.device,
.device::before,
.device::after {
transition: 0.4s cubic-bezier(0.5, 1.7, 0.5, 1.2);
}</code></pre>
<p>大功告成!</p>
前端每日实战:158# 视频演示如何用纯 CSS 创作一个雨伞 toggle 控件
https://segmentfault.com/a/1190000016724029
2018-10-18T11:27:45+08:00
2018-10-18T11:27:45+08:00
comehope
https://segmentfault.com/u/comehope
33
<p><img src="/img/bVbikQw?w=400&h=301" alt="图片描述" title="图片描述"></p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/pxLbjv">https://codepen.io/comehope/pen/pxLbjv</a></p>
<h2>可交互视频</h2>
<p>此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。</p>
<p>请用 chrome, safari, edge 打开观看。</p>
<p><a href="https://link.segmentfault.com/?enc=QpLjUun6FLb8uP5KTfXWzg%3D%3D.VAINiGKW7HH2OWibzY%2Bmg3gOtQEwueYNh4Xcvxk9CbcT0vwJKMkLKrXmVdM2a2m5" rel="nofollow">https://scrimba.com/p/pEgDAM/cMV8euJ</a></p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=hL%2F9nPQQ2Jq2ans9hx8BsQ%3D%3D.EyP93z4SXwP1s%2BCLlSuswwkKtIXzu1a3bbsko1b78B3fl30TFu5LEVSd15yQbB7VaXQagF6tjsQpvMW8IPlt%2BQ%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<p>定义 dom,容器 <code>.umbralla</code> 中包含 2 个元素,<code>.canopy</code> 代表伞盖,<code>.shaft</code> 伞柄:</p>
<pre><code class="html"><figure class="umbralla">
<div class="canopy"></div>
<div class="shaft"></div>
</figure></code></pre>
<p>居中显示:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(skyblue, lightblue);
}</code></pre>
<p>设置伪元素的共有属性:</p>
<pre><code class="css">.umbrella *::before,
.umbrella *::after {
content: '';
position: absolute;
}</code></pre>
<p>先画出雨伞打开的样子。<br>设置容器尺寸,其中 <code>font-size</code> 的属性值后面还要用到,所以定义了一个变量:</p>
<pre><code class="css">:root {
--font-size: 10px;
}
.umbrella {
position: relative;
width: 25em;
height: 26em;
font-size: var(--font-size);
}</code></pre>
<p>定义伞盖的尺寸:</p>
<pre><code class="css">.umbrella .canopy {
position: absolute;
width: inherit;
height: 5.5em;
top: 2.5em;
}</code></pre>
<p>用 <code>::before</code> 伪元素画出伞盖的上半部分,方法先画一个半圆,然后在垂直方向上压缩它:</p>
<pre><code class="css">.umbrella .canopy::before {
width: inherit;
height: 12.5em;
background: rgb(100, 100, 100);
border-radius: 12.5em 12.5em 0 0;
transform: scaleY(0.4);
top: -4em;
}</code></pre>
<p>用 <code>::after</code> 伪元素画出伞盖的下半部分:</p>
<pre><code class="css">.umbrella .canopy::after {
width: inherit;
height: 1.5em;
background-color: #333;
top: 4em;
border-radius: 50%;
}</code></pre>
<p>画出伞柄的长杆:</p>
<pre><code class="css">.umbrella .shaft {
position: absolute;
width: 0.8em;
height: 18em;
background-color: rgba(100, 100, 100, 0.7);
top: 5.5em;
left: calc((100% - 0.8em) / 2);
}</code></pre>
<p>用伪元素画出伞杆顶部露出伞盖的尖头,方法和画伞盖上半部分类似,先画出半圆,然后在水平方向上压缩它:</p>
<pre><code class="css">.umbrella .shaft::before {
width: 6em;
height: 3em;
background-color: rgba(100, 100, 100, 0.7);
left: calc((100% - 6em) / 2);
top: -5.5em;
border-radius: 6em 6em 0 0;
transform: scaleX(0.1);
}</code></pre>
<p>画出雨伞的钩形把手:</p>
<pre><code class="css">.umbrella .shaft::after {
box-sizing: border-box;
width: 4em;
height: 2.5em;
border: 1em solid #333;
top: 100%;
left: calc(50% - 4em + 1em / 2);
border-radius: 0 0 2.5em 2.5em;
border-top: none;
}</code></pre>
<p>至此,完成了雨伞打开的样子,接下来通过变形画出雨伞合上时的样子。<br>先把伞盖的合上,方法是在水平方向上压缩,在垂直方向上拉伸:</p>
<pre><code class="css">.umbrella .canopy {
transform-origin: top;
transform: scale(0.08, 4);
}</code></pre>
<p>隐藏伞盖的下半部分:</p>
<pre><code class="css">.umbrella .canopy::after {
transform: scaleY(0);
}</code></pre>
<p>让伞倾斜,因为竖着的伞有点呆板,所以增加一点变化:</p>
<pre><code class="css">.umbrella {
transform: rotate(-30deg);
}</code></pre>
<p>至此,雨伞合上时的样子也完成了,接下来要把它变为 toggle 控件了。<br>在 dom 中增加一个 <code>checkbox</code> 控件:</p>
<pre><code class="html"><input type="checkbox" class="toggle">
<figure class="umbrella">
<!-- 略 -->
</figure></code></pre>
<p>设置控件与雨伞一样大,并置于雨伞图层的上层:</p>
<pre><code class="css">.toggle {
position: absolute;
filter: opacity(0);
width: 25em;
height: 26em;
font-size: var(--font-size);
cursor: pointer;
z-index: 2;
}</code></pre>
<p><code>checkbox</code> 控件的未选中状态对应雨伞合上时的样子,也就是目前雨伞的样子,所以只要指定控件被选中状态对应的雨伞打开时的样子就可以了。因为合上雨伞是对几个元素进行变形得到的,所以转换到雨伞打开状态就是取消变形。<br>先让伞正过来:</p>
<pre><code class="css">.toggle:checked ~ .umbrella {
transform: rotate(0deg);
}</code></pre>
<p>然后把伞盖打开:</p>
<pre><code class="css">.toggle:checked ~ .umbrella .canopy {
transform: scale(1, 1);
}</code></pre>
<p>再显示出伞盖的下半部分:</p>
<pre><code class="css">.toggle:checked ~ .umbrella .canopy::after {
transform: scaleY(1);
}</code></pre>
<p>最后,设置以上几个元素的缓动效果:</p>
<pre><code class="css">.umbrella,
.umbrella .canopy,
.umbrella .canopy::after {
transition: 0.3s cubic-bezier(0.5, -0.25, 0.5, 1.25);
}</code></pre>
<p>大功告成!</p>
前端每日实战:157# 视频演示如何用纯 CSS 创作一个棋盘错觉动画
https://segmentfault.com/a/1190000016707813
2018-10-17T08:00:56+08:00
2018-10-17T08:00:56+08:00
comehope
https://segmentfault.com/u/comehope
14
<p><img src="/img/bVbigC0?w=400&h=299" alt="图片描述" title="图片描述"></p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/VEyoGj">https://codepen.io/comehope/pen/VEyoGj</a></p>
<h2>可交互视频</h2>
<p>此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。</p>
<p>请用 chrome, safari, edge 打开观看。</p>
<p><a href="https://link.segmentfault.com/?enc=9btmIuoPD7TndShMhX0zPA%3D%3D.NLHs4d%2BqZtHVkjX1XUXwwvQyp9cRr5MXEXqcazVJH3S6C0i1FlfUYRIMoVs0oiON" rel="nofollow">https://scrimba.com/p/pEgDAM/cppKmsd</a></p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=KSEK9YceNfIDGHce5aDBDw%3D%3D.noMv2FjJddUabVW3qbmDj3DRpp8PVZzfpzw3sdy7xpxRZvLDKRIPDBXWG1tRY1p%2F3BvZw5%2FRZQyyFTs%2FoMMnhQ%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<p>定义 dom,容器中包含 10 个子元素,每个子元素表示一行:</p>
<pre><code class="html"><div class="container">
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div></code></pre>
<p>居中显示:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}</code></pre>
<p>定义容器尺寸,用 <code>vmin</code> 单位,并让子元素竖向排列:</p>
<pre><code class="css">.container {
width: 100vmin;
height: 100vmin;
display: flex;
flex-direction: column;
}</code></pre>
<p>设置子元素的背景图案为间隔的黑白色块,顶部有一条细线:</p>
<pre><code class="css">.container span {
width: inherit;
height: 10vmin;
background:
linear-gradient(
gray, gray 0.5vmin,
transparent 0.5vmin, transparent
),
repeating-linear-gradient(
to right,
black, black 10vmin,
transparent 10vmin, transparent 20vmin
)
}</code></pre>
<p>在容器底部补一条细线:</p>
<pre><code class="css">.container {
border-bottom: 0.5vmin solid gray;
}</code></pre>
<p>增加动画效果,让奇数行的背景向右移动半个色块的位置,移动之后看起来好像奇数行右宽左窄,偶数行左宽右窄,这是一种错觉:</p>
<pre><code class="css">.container span:nth-child(odd) {
animation: move 5s linear infinite;
}
@keyframes move {
0%, 55%, 100% {
background-position: 0 0;
}
5%, 50% {
background-position: 5vmin 0;
}
}</code></pre>
<p>让偶数行的背景也移动起来,产生相反方向的错觉:</p>
<pre><code class="css">.container span:nth-child(even) {
animation: move 5s linear infinite reverse;
}</code></pre>
<p>大功告成!</p>
前端每日实战:156# 视频演示如何用纯 CSS 创作一个飞机舷窗风格的 toggle 控件
https://segmentfault.com/a/1190000016688955
2018-10-15T16:15:41+08:00
2018-10-15T16:15:41+08:00
comehope
https://segmentfault.com/u/comehope
29
<p><img src="/img/bVbibIK?w=400&h=301" alt="图片描述" title="图片描述"></p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/jeaOrw">https://codepen.io/comehope/pen/jeaOrw</a></p>
<h2>可交互视频</h2>
<p>此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。</p>
<p>请用 chrome, safari, edge 打开观看。</p>
<p><a href="https://link.segmentfault.com/?enc=VAlBHxjBWOry2M5vBOXqew%3D%3D.kVBD4yXKEkZxsG4AqRXFEOBz16MZuB8FtL088kGq4LXhTRfQRze%2B3tP4OxPVXLFa" rel="nofollow">https://scrimba.com/p/pEgDAM/cdZVGSD</a></p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=xT3OH0GBdyC4GohmduCeEA%3D%3D.qPwq7e%2Bft2mqksQo0JKdPx9ePg5n25%2B6WR5gB9yNCpyB0lrTp%2FfhMNWmg9KTMWk1zjgqRRZ0D2FscZE1UWZuKg%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<p>定义 dom,<code>.windows</code> 容器表示舷窗,它的子元素 <code>.curtain</code> 表示窗帘:</p>
<pre><code class="html"><figure class="window">
<div class="curtain"></div>
</figure></code></pre>
<p>居中显示:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: skyblue;
}</code></pre>
<p>设置舷窗的尺寸,因为后面还会用到字号,所以字号用变量定义:</p>
<pre><code class="css">:root {
--font-size: 10px;
}
.window {
position: relative;
box-sizing: border-box;
width: 25em;
height: 35em;
font-size: var(--font-size);
background-color: #d9d9d9;
}</code></pre>
<p>用阴影画出厚窗框:</p>
<pre><code class="css">.window {
border-radius: 5em;
box-shadow:
inset 0 0 8em rgba(0, 0, 0, 0.2),
0 0 0 0.4em #808080,
0 0 0 4em whitesmoke,
0 0 0 4.4em #808080,
0 2em 4em 4em rgba(0, 0, 0, 0.1);
}</code></pre>
<p>设置窗帘样式,和窗口尺寸一样,但不拉到底:</p>
<pre><code class="css">.window .curtain {
position: absolute;
width: inherit;
height: inherit;
border-radius: 5em;
box-shadow:
0 0 0 0.5em #808080,
0 0 3em rgba(0, 0, 0, 0.4);
background-color: whitesmoke;
left: 0;
top: -5%;
}</code></pre>
<p>用伪元素在窗帘上画出指示灯,当窗帘关闭时亮红色光:</p>
<pre><code class="css">.window .curtain::before {
content: '';
position: absolute;
width: 40%;
height: 0.8em;
background-color: #808080;
left: 30%;
bottom: 1.6em;
border-radius: 0.4em;
}
.window .curtain::after {
content: '';
position: absolute;
width: 1.6em;
height: 0.8em;
background-image: radial-gradient(orange, orangered);
bottom: 1.6em;
border-radius: 0.4em;
left: calc((100% - 1.6em) / 2);
}</code></pre>
<p>以上是舷窗关闭时的样子,接下来绘制舷窗打开时的效果。<br>先在 dom 中添加一个 <code>checkbox</code>,当它被 <code>checked</code> 时即表示舷窗被打开:</p>
<pre><code class="html"><input type="checkbox" class="toggle">
<figure class="window">
<div class="handle"></div>
</figure></code></pre>
<p>隐藏 <code>checkbox</code>,用 <code>opacity(0)</code> 可以使元素在不可见的状态下仍可交互,把它的尺寸设置得到舷窗一样大,并且图层在舷窗之上,得到的效果就是点击舷窗时实际是点击了 <code>checkbox</code>:</p>
<pre><code class="css">.toggle {
position: absolute;
filter: opacity(0);
width: 25em;
height: 35em;
font-size: var(--font-size);
cursor: pointer;
z-index: 2;
}</code></pre>
<p>当舷窗打开时,<code>.curtain</code> 要向上移动,并且指示灯亮绿色光:</p>
<pre><code class="css">.window .curtain {
transition: 0.5s ease-in-out;
}
.toggle:checked ~ .window .curtain {
top: -90%;
}
.toggle:checked ~ .window .curtain::after {
background-image: radial-gradient(lightgreen, limegreen);
}</code></pre>
<p>隐藏超出窗户的部分:</p>
<pre><code class="css">.window {
overflow: hidden;
}</code></pre>
<p>接下来绘制舷窗外的风景。<br>在 dom 中增加表示云朵的 <code>.clouds</code> 元素,其中的 5 个 <code><span></code> 子元素分别表示 1 朵白云:</p>
<pre><code class="html"><input type="checkbox" class="toggle">
<figure class="window">
<div class="curtain"></div>
<div class="clouds">
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
</figure></code></pre>
<p>用云朵容器画出窗外的蓝天:</p>
<pre><code class="css">.window .clouds {
position: relative;
width: 20em;
height: 30em;
background-color: deepskyblue;
box-shadow: 0 0 0 0.4em #808080;
left: calc((100% - 20em) / 2);
top: calc((100% - 30em) / 2);
border-radius: 7em;
}</code></pre>
<p>每朵云由 3 部分组成,先画面积最大的部分:</p>
<pre><code class="css">.clouds span {
position: absolute;
width: 10em;
height: 4em;
background-color: white;
top: 20%;
border-radius: 4em;
}</code></pre>
<p>再用伪元素画 2 个突起的圆弧:</p>
<pre><code class="css">.clouds span::before,
.clouds span::after {
content: '';
position: absolute;
width: 4em;
height: 4em;
background-color: white;
border-radius: 50%;
}
.clouds span::before {
top: -2em;
left: 2em;
}
.clouds span::after {
top: -1em;
right: 1em;
}</code></pre>
<p>增加云朵飘动的动画效果:</p>
<pre><code class="css">.clouds span {
animation: move 4s linear infinite;
}
@keyframes move {
from {
left: -150%;
}
to {
left: 150%;
}
}</code></pre>
<p>使每朵云的大小、位置有一些变化:</p>
<pre><code class="css">.clouds span:nth-child(2) {
top: 40%;
animation-delay: -1s;
}
.clouds span:nth-child(3) {
top: 60%;
animation-delay: -0.5s;
}
.clouds span:nth-child(4) {
top: 20%;
transform: scale(2);
animation-delay: -1.5s;
}
.clouds span:nth-child(5) {
top: 70%;
transform: scale(1.5);
animation-delay: -3s;
}</code></pre>
<p>最后,隐藏容器外的内容:</p>
<pre><code class="css">.window .clouds {
overflow: hidden;
}</code></pre>
<p>大功告成!</p>
前端每日实战:155# 视频演示如何用纯 CSS 创作一只热气球
https://segmentfault.com/a/1190000016668903
2018-10-13T06:19:39+08:00
2018-10-13T06:19:39+08:00
comehope
https://segmentfault.com/u/comehope
18
<p><img src="/img/bVbh6vq?w=400&h=300" alt="图片描述" title="图片描述"></p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/KGveaN">https://codepen.io/comehope/pen/KGveaN</a></p>
<h2>可交互视频</h2>
<p>此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。</p>
<p>请用 chrome, safari, edge 打开观看。</p>
<p><a href="https://link.segmentfault.com/?enc=orpuxYHVSO1s%2B5SaOa3VDg%3D%3D.IZTUtUBEJmJ3w65ONhJhKOc7s%2B%2FDAd%2F3i%2BiYqOJ3pNU%2BmgVZJzgehneJ%2BLbUBjTS" rel="nofollow">https://scrimba.com/p/pEgDAM/cgdaPsr</a></p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=%2FVx5gjt6FMxJsiaDc8eAJw%3D%3D.K3aiRdTvB4OuExAEuu%2Bf2KM2sJUVdKppTsHQq5YMZzo5Wlp8dUGD4yVH%2B1yx7TubkKwIDNA33wkkhMtqmlSvBg%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<p>定义 dom,容器中有 2 个子元素,<code>.envelope</code> 代表伞盖,<code>.basket</code> 代表吊篮:</p>
<pre><code class="html"><figure class="balloon">
<div class="envelope">
<span></span>
<span></span>
</div>
<div class="basket">
<span></span>
<span></span>
<span></span>
<span></span>
</div>
</figure></code></pre>
<p>居中显示:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(deepskyblue, skyblue, lightblue 20%);
}</code></pre>
<p>定义容器的尺寸,子元素 <code>.envelope</code> 和 <code>.basket</code> 纵向居中布局:</p>
<pre><code class="css">.balloon {
width: 12em;
height: 19em;
font-size: 16px;
display: flex;
flex-direction: column;
align-items: center;
}</code></pre>
<p>先画伞盖。<br>定义伞盖的尺寸:</p>
<pre><code class="css">.envelope {
position: relative;
width: inherit;
height: 16em;
}</code></pre>
<p>伞盖的形状是上端为球形,下端为圆锥形,在二维平面中,圆锥在平面的投影为等腰三角形,所以我们先在上部画一个圆,再在下部画一个三角形。<br>先画上部的圆:</p>
<pre><code class="css">.envelope span {
position: absolute;
width: inherit;
height: 12em;
border-radius: 50%;
color: orange;
background-color: currentColor;
}</code></pre>
<p>再用伪元素画出下部的等腰三角形:</p>
<pre><code class="css">.envelope span::before {
content: '';
position: absolute;
width: 0;
height: 0;
border-width: 10em 5.5em 0 5.5em;
border-style: solid;
border-color: currentColor transparent transparent transparent;
left: calc(50% - 5.5em);
top: 8.45em;
}</code></pre>
<p><code>.envelope</code> 下共有 2 个 <code><span></code> 元素,让第 2 个 <code><span></code> 变形、变色,使伞盖形成竖条纹的花纹:</p>
<pre><code class="css">.envelope span:nth-child(2) {
transform: scaleX(0.4);
filter: brightness(0.85) contrast(1.4);
}</code></pre>
<p>隐藏 <code>.envelope</code> 容器外的部分,削掉三角形最下面的尖角:</p>
<pre><code class="css">.envelope {
overflow: hidden;
}</code></pre>
<p>至此,伞盖完成,接下来画吊篮。<br>定义吊篮的尺寸:</p>
<pre><code class="css">.basket {
position: relative;
width: 2em;
height: 3em;
}</code></pre>
<p>用 <code>::before</code> 伪元素画出篮子:</p>
<pre><code class="css">.basket::before {
content: '';
position: absolute;
width: inherit;
height: 1.6em;
background-color: peru;
bottom: 0;
border-radius: 0 0 0.5em 0.5em;
}</code></pre>
<p>用 <code>::after</code> 伪元素画出篮子的顶边:</p>
<pre><code class="css">.basket::after {
content: '';
position: absolute;
width: 105%;
height: 0.3em;
background-color: saddlebrown;
left: calc((100% - 105%) / 2);
top: 1.3em;
border-radius: 0.3em;
}</code></pre>
<p><code>.basket</code> 下共有 4 个 <code><span></code> 元素,代表 4 根缆绳,设置它们的样式为竖细线:</p>
<pre><code class="css">.basket span {
position: absolute;
width: 0.1em;
height: 1.5em;
background-color: burlywood;
}</code></pre>
<p>定位缆绳,并倾斜不同的角度:</p>
<pre><code class="css">.basket span {
left: calc((var(--n) - 1) * 0.6em);
transform-origin: bottom;
transform: rotate(calc(var(--r) * 7deg));
}
.basket span:nth-child(1) { --n: 1; --r: -2; }
.basket span:nth-child(2) { --n: 2; --r: -1; }
.basket span:nth-child(3) { --n: 3; --r: 1; }
.basket span:nth-child(4) { --n: 4; --r: 2; }</code></pre>
<p>最后,增加热气球微微浮动的动画效果:</p>
<pre><code class="css">.balloon {
animation: drift 2s infinite alternate;
}
@keyframes drift {
to {
transform: translateY(-5%);
}
}</code></pre>
<p>大功告成!</p>
前端每日实战:154# 视频演示如何创作一个眼冒金星的动画效果
https://segmentfault.com/a/1190000016657527
2018-10-12T08:22:57+08:00
2018-10-12T08:22:57+08:00
comehope
https://segmentfault.com/u/comehope
10
<p><img src="/img/bVbh3xW?w=400&h=304" alt="图片描述" title="图片描述"></p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/OBgBJJ">https://codepen.io/comehope/pen/OBgBJJ</a></p>
<h2>可交互视频</h2>
<p>此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。</p>
<p>请用 chrome, safari, edge 打开观看。</p>
<p><a href="https://link.segmentfault.com/?enc=wA5MoiyWIdryfaRmkjVt8w%3D%3D.d%2F863QdNZJpCrnezAa%2BT9CKYIcHxK0tkcfnWBkUojCnwwsi2QYaEfsjJYc9sSP%2F%2B" rel="nofollow">https://scrimba.com/p/pEgDAM/c83BKt3</a></p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=6a73wNET65MAYz3zqoIF2w%3D%3D.sVuWcRTnMqUCwWiE6PQ3cX0vSnL6jIrPp12VXfl2BcjJ6B9DBjC09WZL6lDV%2FtCMi3ZzzDQPS4lJCsZr%2BuMAdg%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<p>定义 dom,容器中包含 9 个子元素:</p>
<pre><code class="html"><div class='container'>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div></code></pre>
<p>居中显示:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: black;
}</code></pre>
<p>设置容器中子元素的布局方式,形成一个 3 * 3 的网格,其中 <code>--columns</code> 是网格每一边上的子元素数量:</p>
<pre><code class="css">.container {
display: grid;
--columns: 3;
grid-template-columns: repeat(var(--columns), 1fr);
}</code></pre>
<p>定义子元素样式:</p>
<pre><code class="css">.container span {
width: 25px;
height: 25px;
color: lime;
background-color: currentColor;
}</code></pre>
<p>增加子元素的动画效果,总动画时长是 5 秒,其中第 1 秒(0% ~ 20%)有动画,其余 4 秒(20% ~ 100%)静止:</p>
<pre><code class="css">.container span {
transform: scale(0);
animation: spin 5s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg) scale(1);
}
5%, 15% {
transform: rotate(90deg) scale(0);
background: white;
}
17.5% {
transform: rotate(180deg) scale(1);
background-color: currentColor;
}
20%, 100% {
transform: rotate(90deg) scale(0);
}
}</code></pre>
<p>设置动画延时,使各子元素的动画随机延时 4 秒之内的任意时间:</p>
<pre><code class="css">.container span {
animation-delay: calc(var(--delay) * 1s);
}
.container span:nth-child(1) { --delay: 0.8 }
.container span:nth-child(2) { --delay: 0.2 }
.container span:nth-child(3) { --delay: 1.9 }
.container span:nth-child(4) { --delay: 3.9 }
.container span:nth-child(5) { --delay: 2.8 }
.container span:nth-child(6) { --delay: 3.5 }
.container span:nth-child(7) { --delay: 1.5 }
.container span:nth-child(8) { --delay: 2.3 }
.container span:nth-child(9) { --delay: 1.7 }</code></pre>
<p>至此,静态效果完成,接下来批量处理 dom 元素。<br>引入 d3 库:</p>
<pre><code class="html"><script src="https://d3js.org/d3.v5.min.js"></script></code></pre>
<p>删除掉 css 文件中的 <code>--columns</code> 变量声明,用 d3 为变量赋值:</p>
<pre><code class="javascript">const COLUMNS = 3;
d3.select('.container')
.style('--columns', COLUMNS);</code></pre>
<p>删除掉 html 文件中的 <code><span></code> 子元素,用 d3 动态生成:</p>
<pre><code class="javascript">d3.select('.container')
.style('--columns', COLUMNS)
.selectAll('span')
.data(d3.range(COLUMNS * COLUMNS))
.enter()
.append('span');</code></pre>
<p>删除掉 css 文件中的 <code>--delay</code> 变量声明,用 d3 为变量生成随机数:</p>
<pre><code class="javascript">d3.select('.container')
.style('--columns', COLUMNS)
.selectAll('span')
.data(d3.range(COLUMNS * COLUMNS))
.enter()
.append('span')
.style('--delay', () => Math.random() * 4);</code></pre>
<p>最后,把边长改为 15,生成更多的子元素,加强视觉效果:</p>
<pre><code class="javascript">const COLUMNS = 15;</code></pre>
<p>大功告成!</p>
前端每日实战:153# 视频演示如何用 CSS 和原生 JS 创作一组 tooltip 提示框
https://segmentfault.com/a/1190000016651727
2018-10-11T16:37:13+08:00
2018-10-11T16:37:13+08:00
comehope
https://segmentfault.com/u/comehope
20
<p><img src="/img/bVbh12C?w=400&h=303" alt="图片描述" title="图片描述"></p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/rqyoYY">https://codepen.io/comehope/pen/rqyoYY</a></p>
<h2>可交互视频</h2>
<p>此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。</p>
<p>请用 chrome, safari, edge 打开观看。</p>
<p><a href="https://link.segmentfault.com/?enc=NQmcplcmOFFWWmKLdWlXnQ%3D%3D.c23uVtlwwO6%2FtKk1EhZkgRgb2vxcBV%2BvPYZ3W4seqjE8FFcvDywcVB5%2B6zZs6sck" rel="nofollow">https://scrimba.com/p/pEgDAM/c6p2Es2</a></p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=ZrKLcSRV4bXuDfNw%2BLIN3Q%3D%3D.rY0ZyuNa6P790pto1oMrfSRUTNOf%2F70Ilt%2BZAV9QemF7c%2BwWqb6SMRD7nXQW4GTwKuAvY%2BDggbbV5e0DXopaAw%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<p>定义 dom,容器中包含一个名为 <code>.emoji</code> 的子容器,代表一个头像,它的子元素 <code>eye left</code>、<code>eye right</code>、<code>mouth</code> 分别代表左眼、右眼和嘴巴:</p>
<pre><code class="html"><section class="container">
<div class="emoji">
<span class="eye left"></span>
<span class="eye right"></span>
<span class="mouth"></span>
</div>
</section></code></pre>
<p>居中显示:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: lightyellow;
}</code></pre>
<p>定义容器尺寸和子元素对齐方式:</p>
<pre><code class="css">.container {
position: relative;
width: 20em;
height: 20em;
font-size: 10px;
display: flex;
align-items: center;
justify-content: center;
}</code></pre>
<p>定义头像的轮廓:</p>
<pre><code class="css">.emoji {
position: relative;
box-sizing: border-box;
width: 10em;
height: 10em;
background-color: pink;
border-radius: 50% 50% 75% 50%;
}</code></pre>
<p>定义头像眼睛的轮廓:</p>
<pre><code class="css">.emoji .eye {
position: absolute;
box-sizing: border-box;
width: 3em;
height: 3em;
border: 0.1em solid gray;
border-radius: 50%;
top: 3em;
}
.emoji .eye.left {
left: 1em;
}
.emoji .eye.right {
right: 1em;
}</code></pre>
<p>画出眼珠:</p>
<pre><code class="css">.emoji .eye.left::before,
.emoji .eye.right::before {
content: '';
position: absolute;
width: 1em;
height: 1em;
background-color: #222;
border-radius: 50%;
top: 1em;
left: calc((100% - 1em) / 2);
}</code></pre>
<p>画出微笑的嘴:</p>
<pre><code class="css">.emoji .mouth {
position: absolute;
width: 2em;
height: 2em;
border: 0.1em solid;
bottom: 1em;
left: 40%;
border-radius: 50%;
border-color: transparent gray gray transparent;
transform: rotate(20deg);
}</code></pre>
<p>接下来制作眼珠转向 4 个方向的效果。<br>用 2 个变量分别表示眼珠的定位位置:</p>
<pre><code class="css">.emoji .eye {
--top: 1em;
--left: calc((100% - 1em) / 2);
}
.emoji .eye.left::before,
.emoji .eye.right::before {
top: var(--top);
left: var(--left);
}</code></pre>
<p>设置眼珠在 4 个方向的定位位置:</p>
<pre><code class="css">.emoji.top .eye {
--top: 0;
}
.emoji.bottom .eye {
--top: 1.8em;
}
.emoji.left .eye {
--left: 0;
}
.emoji.right .eye {
--left: 1.8em;
}</code></pre>
<p>此时,如果为 dom 元素 <code>.emoji</code> 增加 <code>top</code>、<code>bottom</code>、<code>left</code>、<code>right</code> 4 个样式中的任何一个样式,眼珠就会转向特定的方向。</p>
<p>在 dom 中增加 4 个元素,每个元素的内容是一个 @ 字符:</p>
<pre><code class="html"><section class="container">
<div class="emoji">
<!-- 略 -->
</div>
<span class="tip top">@</span>
<span class="tip left">@</span>
<span class="tip right">@</span>
<span class="tip bottom">@</span>
</section></code></pre>
<p>把 4 个元素布局在头像周围:</p>
<pre><code class="css">.tip {
position: absolute;
cursor: pointer;
font-size: 4.5em;
color: silver;
font-family: sans-serif;
font-weight: 100;
}
.tip.top {
top: -15%;
}
.tip.bottom {
bottom: -15%;
}
.tip.left {
left: -15%;
}
.tip.right {
right: -15%;
}</code></pre>
<p>写一段脚本,增加一点交互效果。当鼠标悬停在 4 个方向的 @ 上时,使眼珠朝相应的方向转去。这里的 <code>DIRECTION</code> 常量存储了 4 个方向,<code>EVENTS</code> 常量存储了 2 个鼠标事件,<code>$</code> 常量包装了根据类名获取 dom 元素的操作:</p>
<pre><code class="javascript">const DIRECTIONS = ['top', 'bottom', 'left', 'right']
const EVENTS = ['mouseover', 'mouseout']
const $ = (className) => document.getElementsByClassName(className)[0]
DIRECTIONS.forEach(direction =>
EVENTS.forEach((e) =>
$(`tip ${direction}`).addEventListener(e, () =>
$('emoji').classList.toggle(direction)
)
)
)</code></pre>
<p>为眼珠设置缓动时间,使动画平滑:</p>
<pre><code class="css">.emoji .eye.left::before,
.emoji .eye.right::before {
transition: 0.3s;
}</code></pre>
<p>接下来制作 tooltip 提示框。<br>为 4 个 @ 符号的 dom 增加 <code>data-tip</code> 属性,其内容就是 tooltip 信息:</p>
<pre><code class="html"><section class="container">
<div class="emoji">
<!-- 略 -->
</div>
<span class="tip top" data-tip="look up">@</span>
<span class="tip bottom" data-tip="look down">@</span>
<span class="tip left" data-tip="look to the left">@</span>
<span class="tip right" data-tip="look to the right">@</span>
</section></code></pre>
<p>用 <code>::before</code> 伪元素展示提示信息,样式为黑底白字:</p>
<pre><code class="css">.tip::before {
content: attr(data-tip);
position: absolute;
font-size: 0.3em;
font-family: sans-serif;
width: 10em;
text-align: center;
background-color: #222;
color: white;
padding: 0.5em;
border-radius: 0.2em;
box-shadow: 0 0.1em 0.3em rgba(0, 0, 0, 0.3);
}</code></pre>
<p>把顶部的提示框定位到顶部 @ 符号的上方正中:</p>
<pre><code class="css">.tip.top::before {
top: 0;
left: 50%;
transform: translate(-50%, calc(-100% - 0.6em));
}</code></pre>
<p>类似地,把其他 3 个提示框也定位到 @ 符号的旁边:</p>
<pre><code class="css">.tip.bottom::before {
bottom: 0;
left: 50%;
transform: translate(-50%, calc(100% + 0.6em));
}
.tip.left::before {
left: 0;
top: 50%;
transform: translate(calc(-100% - 0.6em), -50%);
}
.tip.right::before {
right: 0;
top: 50%;
transform: translate(calc(100% + 0.6em), -50%);
}</code></pre>
<p>用 <code>::after</code> 伪元素在顶部提示框下面画出一个倒三角形:</p>
<pre><code class="css">.tip::after {
content: '';
position: absolute;
font-size: 0.3em;
width: 0;
height: 0;
color: #222;
border: 0.6em solid transparent;
}
.tip.top::after {
border-bottom-width: 0;
border-top-color: currentColor;
top: -0.6em;
left: 50%;
transform: translate(-50%, 0);
}</code></pre>
<p>类似地,在其他 3 个提示框旁边画出三角形:</p>
<pre><code class="css">.tip.bottom::after {
border-top-width: 0;
border-bottom-color: currentColor;
bottom: -0.6em;
left: 50%;
transform: translate(-50%, 0);
}
.tip.left::after {
border-right-width: 0;
border-left-color: currentColor;
left: -0.6em;
top: 50%;
transform: translate(0, -50%);
}
.tip.right::after {
border-left-width: 0;
border-right-color: currentColor;
right: -0.6em;
top: 50%;
transform: translate(0, -50%);
}</code></pre>
<p>最后,隐藏提示框,使提示框只在鼠标悬停时出现:</p>
<pre><code class="css">.tip::before,
.tip::after {
visibility: hidden;
filter: opacity(0);
transition: 0.3s;
}
.tip:hover::before,
.tip:hover::after {
visibility: visible;
filter: opacity(1);
}</code></pre>
<p>大功告成!</p>
前端每日实战 2018 年 9 月份项目汇总(共 26 个项目)
https://segmentfault.com/a/1190000016604394
2018-10-08T07:13:47+08:00
2018-10-08T07:13:47+08:00
comehope
https://segmentfault.com/u/comehope
65
<h2>过往项目</h2>
<p><a href="https://segmentfault.com/a/1190000016237865">2018 年 8 月份项目汇总(共 29 个项目)</a></p>
<p><a href="https://segmentfault.com/a/1190000015958405">2018 年 7 月份项目汇总(共 29 个项目)</a></p>
<p><a href="https://segmentfault.com/a/1190000015439611">2018 年 6 月份项目汇总(共 27 个项目)</a></p>
<p><a href="https://segmentfault.com/a/1190000015440135">2018 年 5 月份项目汇总(共 30 个项目)</a></p>
<p><a href="https://segmentfault.com/a/1190000014675969">2018 年 4 月份项目汇总(共 8 个项目)</a></p>
<h2>2018 年 9 月份发布的项目</h2>
<p>《前端每日实战》专栏每天分解一个前端项目,用视频记录编码过程,再配合详细的代码解读,是学习前端开发的活的参考书!</p>
<h3><a href="https://segmentfault.com/a/1190000016231897">124# 视频演示如何用纯 CSS 创作一只纸鹤</a></h3>
<p><img src="https://segmentfault.com/img/bVbggOW?w=400&h=295" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000016243834">125# 视频演示如何用纯 CSS 创作一个失落的人独自行走的动画</a></h3>
<p><img src="https://segmentfault.com/img/bVbgjVt?w=400&h=301" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000016257190">126# 视频演示如何用纯 CSS 创作小球变矩形背景的按钮悬停效果</a></h3>
<p><img src="https://segmentfault.com/img/bVbgnoQ?w=400&h=300" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000016271648">127# 视频演示如何用纯 CSS 创作一个圆环旋转错觉动画</a></h3>
<p><img src="https://segmentfault.com/img/bVbgq95?w=400&h=302" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000016287188">128# 视频演示如何用纯 CSS 创作一个“女神来了,快让路”的动画</a></h3>
<p><img src="https://segmentfault.com/img/bVbgvcJ?w=400&h=301" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000016303635">129# 视频演示如何用纯 CSS 创作一个条纹错觉动画</a></h3>
<p><img src="https://segmentfault.com/img/bVbgztX?w=400&h=293" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000016321619">130# 视频演示如何用 CSS 在线字体和 D3 创作一个 Google & googol 信息图</a></h3>
<p><img src="https://segmentfault.com/img/bVbgD94?w=400&h=300" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000016331561">131# 视频演示如何用纯 CSS 创作一把剪刀</a></h3>
<p><img src="https://segmentfault.com/img/bVbgGKo?w=400&h=299" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000016345813">132# 视频演示如何用纯 CSS 创作一只思考的手</a></h3>
<p><img src="https://segmentfault.com/img/bVbgKsi?w=400&h=299" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000016362691">133# 视频演示如何用 CSS 和 GSAP 创作有多个关键帧的连续动画</a></h3>
<p><img src="https://segmentfault.com/img/bVbgOQt?w=400&h=302" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000016377676">134# 视频演示如何用 CSS 和 GSAP 创作一个树枝发芽的 loader</a></h3>
<p><img src="https://segmentfault.com/img/bVbgSKa?w=400&h=302" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000016390037">135# 视频演示如何用纯 CSS 创作一个悬停时右移的按钮特效</a></h3>
<p><img src="https://segmentfault.com/img/bVbgVXz?w=400&h=302" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000016406581">136# 视频演示如何用 D3 和 GSAP 创作一个横条 loader</a></h3>
<p><img src="https://segmentfault.com/img/bVbg0gq?w=400&h=305" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000016419507">137# 视频演示如何用纯 CSS 创作一个抽象的水波荡漾的动画</a></h3>
<p><img src="https://segmentfault.com/img/bVbg3CU?w=400&h=301" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000016456282">138# 视频演示如何用纯 CSS 创作一张 iPhone 价格信息图</a></h3>
<p><img src="https://segmentfault.com/img/bVbhdbh?w=400&h=300" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000016462519">139# 视频演示如何用 CSS 和 D3 创作光斑粒子交相辉映的动画</a></h3>
<p><img src="https://segmentfault.com/img/bVbheOE?w=400&h=305" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000016478152">140# 视频演示如何用纯 CSS 创作文本的淡入动画效果</a></h3>
<p><img src="https://segmentfault.com/img/bVbhiSN?w=400&h=301" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000016506733">141# 视频演示如何用 CSS 的 Grid 布局创作一枚小狗邮票</a></h3>
<p><img src="https://segmentfault.com/img/bVbhqjK?w=400&h=300" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000016508267">142# 视频演示如何用 CSS 的 Grid 布局创作一枚小鸡邮票</a></h3>
<p><img src="https://segmentfault.com/img/bVbhqIw?w=400&h=300" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000016510482">143# 视频演示如何用 CSS 的 Grid 布局创作一枚小松鼠邮票</a></h3>
<p><img src="https://segmentfault.com/img/bVbhrie?w=400&h=300" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000016521212">144# 视频演示如何用 D3 和 GSAP 创作一个集体舞动画</a></h3>
<p><img src="https://segmentfault.com/img/bVbht5j?w=400&h=301" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000016530200">145# 视频演示如何用纯 CSS 创作一个电源开关控件</a></h3>
<p><img src="https://segmentfault.com/img/bVbhwqh?w=400&h=301" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000016543472">146# 视频演示如何用纯 CSS 创作一个脉动 loader</a></h3>
<p><img src="https://segmentfault.com/img/bVbhzSl?w=400&h=300" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000016556930">147# 视频演示如何用纯 CSS 创作透视按钮的悬停特效</a></h3>
<p><img src="https://segmentfault.com/img/bVbhDnp?w=400&h=302" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000016561226">148# 视频演示如何用纯 CSS 创作从按钮两侧滑入装饰元素的悬停特效</a></h3>
<p><img src="https://segmentfault.com/img/bVbhEuH?w=400&h=300" alt="" title=""></p>
<h3><a href="https://segmentfault.com/a/1190000016577586">149# 视频演示如何用纯 CSS 创作一个宝路薄荷糖的动画</a></h3>
<p><img src="https://segmentfault.com/img/bVbhIKv?w=400&h=300" alt="" title=""></p>
前端每日实战:152# 视频演示如何用纯 CSS 创作一个圆点错觉效果
https://segmentfault.com/a/1190000016600016
2018-10-07T02:40:13+08:00
2018-10-07T02:40:13+08:00
comehope
https://segmentfault.com/u/comehope
10
<p><img src="/img/bVbhOAl?w=400&h=300" alt="图片描述" title="图片描述"></p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/gBwzKR">https://codepen.io/comehope/pen/gBwzKR</a></p>
<h2>可交互视频</h2>
<p>此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。</p>
<p>请用 chrome, safari, edge 打开观看。</p>
<p><a href="https://link.segmentfault.com/?enc=eVzY8NcTy%2FE9X%2FoJLyEprg%3D%3D.io5F2H0QGYBUMJVHNsK3tABJiDAxCSN8hGX%2FNyWRCDdX7VHpzpZTJMQrfIzLBy%2BS" rel="nofollow">https://scrimba.com/p/pEgDAM/ca82VAM</a></p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=OTe5gFn1vwX%2F%2FkH%2F2fNXow%3D%3D.XRjcCHfDokxMJtxVuBPLMLc5KEUEvc9yvkXoVis75lDt8DaQU5gMSJ8icCF%2FXKzx87nIqwYufaTc%2BjTBmGTElw%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<p>此项目无用户自定义的 dom 元素,利用系统默认的 <code><body></code> 元素作为容器。</p>
<p>定义页面尺寸,背景设置为黑色:</p>
<pre><code class="css">body {
margin: 0;
width: 100vw;
height: 100vh;
background-color: black;
}</code></pre>
<p>用线性渐变画出一横一竖二条灰色的细线:</p>
<pre><code class="css">body {
margin: 0;
width: 100vw;
height: 100vh;
background-color: black;
background-image:
linear-gradient(
to bottom,
#555 2vmin,
transparent 2vmin
),
linear-gradient(
to right,
#555 2vmin,
transparent 2vmin
);
}</code></pre>
<p>用径向渐变在左上角画一个白色的圆点:</p>
<pre><code class="css">body {
margin: 0;
width: 100vw;
height: 100vh;
background-color: black;
background-image:
radial-gradient(
circle at 1vmin 1vmin,
white 1vmin,
transparent 1vmin
),
linear-gradient(
to bottom,
#555 2vmin,
transparent 2vmin
),
linear-gradient(
to right,
#555 2vmin,
transparent 2vmin
);
}</code></pre>
<p>平铺背景:</p>
<pre><code class="css">body {
margin: 0;
width: 100vw;
height: 100vh;
background-color: black;
background-image:
radial-gradient(
circle at 1vmin 1vmin,
white 1vmin,
transparent 1vmin
),
linear-gradient(
to bottom,
#555 2vmin,
transparent 2vmin
),
linear-gradient(
to right,
#555 2vmin,
transparent 2vmin
);
background-size: 10vmin 10vmin;
}</code></pre>
<p>为避免圆点紧贴在左侧和顶部,为背景增加一点偏移量:</p>
<pre><code class="css">body {
margin: 0;
width: 100vw;
height: 100vh;
background-color: black;
background-image:
radial-gradient(
circle at 1vmin 1vmin,
white 1vmin,
transparent 1vmin
),
linear-gradient(
to bottom,
#555 2vmin,
transparent 2vmin
),
linear-gradient(
to right,
#555 2vmin,
transparent 2vmin
);
background-size: 10vmin 10vmin;
background-position: 5vmin 5vmin;
}</code></pre>
<p>现在,如果视线在页面中移动,就会看到黑色小圆点,这实际上是错觉。</p>
<p>大功告成!</p>
前端每日实战:151# 视频演示如何用纯 CSS 创作超能陆战队的大白
https://segmentfault.com/a/1190000016596370
2018-10-06T07:26:20+08:00
2018-10-06T07:26:20+08:00
comehope
https://segmentfault.com/u/comehope
17
<p><img src="/img/bVbhNDx?w=400&h=300" alt="图片描述" title="图片描述"></p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/ReGRaO">https://codepen.io/comehope/pen/ReGRaO</a></p>
<h2>可交互视频</h2>
<p>此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。</p>
<p>请用 chrome, safari, edge 打开观看。</p>
<p><a href="https://link.segmentfault.com/?enc=%2BkiFnQIb6j3BAXP21EwMAw%3D%3D.Z7sSR0KKcNSDyeP3WaUVDhKPTpm5TAWgYbJ8YqZc35tChmMlyDT9pfcBothThB8K" rel="nofollow">https://scrimba.com/p/pEgDAM/cEJDKSg</a></p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=%2BecIu8VAsmFBaoZsffLrHA%3D%3D.Vnkh89fcUKgbaV9u0xCMqc7%2BysI2AxnBkmH24m8VPLnReTSj%2BNK9cUUBUzEAup%2FYd6pDhVCQFmmPS%2BryPZ9I9w%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<p>整个人物分为 3 部分:头、身体、腿,下面按照这个顺序分别画出,先画头部。<br>定义 dom,容器 <code>.baymax</code> 表示大白,<code>head</code> 表示头部:</p>
<pre><code class="html"><div class="baymax">
<div class="head">
<div class="eyes"></div>
</div>
</div></code></pre>
<p>居中显示:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(176, 0, 0, 0.75);
}</code></pre>
<p>定义容器尺寸和子元素对齐方式:</p>
<pre><code class="css">.baymax {
width: 30em;
height: 41em;
font-size: 10px;
display: flex;
justify-content: center;
position: relative;
}</code></pre>
<p>画出头部轮廓:</p>
<pre><code class="css">.head {
position: absolute;
width: 9em;
height: 6em;
background-color: white;
border-radius: 50%;
box-shadow:
inset 0 -1.5em 3em rgba(0, 0, 0, 0.2),
0 0.5em 1.5em rgba(0, 0, 0, 0.2);
}</code></pre>
<p>画出双眼中间的线条:</p>
<pre><code class="css">.head .eyes {
position: absolute;
width: 4.8em;
height: 0.1em;
background-color: #222;
top: 2.3em;
left: calc((9em - 4.8em) / 2);
}</code></pre>
<p>画出双眼:</p>
<pre><code class="css">.head .eyes::before,
.head .eyes::after {
content: '';
position: absolute;
width: 0.8em;
height: 0.9em;
background-color: #222;
border-radius: 50%;
top: -0.3em;
}
.head .eyes::after {
right: 0;
}</code></pre>
<p>接下来画身体。<br>html 文件中增加身体的 dom 元素:</p>
<pre><code class="html"><div class="baymax">
<div class="head">
<!-- 略 -->
</div>
<div class="body">
<div class="chest">
<span class="button"></span>
</div>
<div class="belly"></div>
<div class="left arm">
<div class="fingers"></div>
</div>
<div class="right arm">
<div class="fingers"></div>
</div>
</div>
</div></code></pre>
<p>定义身体的宽度:</p>
<pre><code class="css">.body {
position: absolute;
width: inherit;
}</code></pre>
<p>画出胸部:</p>
<pre><code class="css">.body .chest {
position: absolute;
width: 19em;
height: 26em;
background-color: white;
top: 4em;
left: calc((100% - 19em) / 2);
border-radius: 50%;
z-index: -1;
}</code></pre>
<p>画出胸前的按钮:</p>
<pre><code class="css">.body .chest .button {
position: absolute;
width: 2em;
height: 2em;
background-color: white;
border-radius: 50%;
top: 4em;
right: 4em;
box-shadow:
inset 0 -0.5em 0.8em rgba(0, 0, 0, 0.15),
0.2em 0.3em 0.2em rgba(0, 0, 0, 0.05);
filter: opacity(0.75);
}</code></pre>
<p>画出肚皮:</p>
<pre><code class="css">.body .belly {
position: absolute;
width: 24em;
height: 31em;
background-color: white;
top: 5.5em;
left: calc((100% - 24em) / 2);
border-radius: 50%;
z-index: -2;
box-shadow:
inset 0 -2.5em 4em rgba(0, 0, 0, 0.15),
0 0.5em 1.5em rgba(0, 0, 0, 0.25);
}</code></pre>
<p>定义胳膊的高度起点:</p>
<pre><code class="css">.body .arm {
position: absolute;
top: 7.5em;
}</code></pre>
<p>胳膊分为肘以上的部分和肘以下的部分。<br>先设计这两段的共有属性:</p>
<pre><code class="css">.body .arm::before,
.body .arm::after {
content: '';
position: absolute;
background-color: white;
border-radius: 50%;
transform-origin: top;
z-index: -3;
}</code></pre>
<p>再用伪元素分别画出这两部分:</p>
<pre><code class="css">.body .arm::before {
width: 9em;
height: 20em;
left: 7em;
transform: rotate(30deg);
}
.body .arm::after {
width: 8em;
height: 15em;
left: -0.8em;
top: 9.5em;
transform: rotate(-5deg);
box-shadow: inset 0.4em -1em 1em rgba(0, 0, 0, 0.2);
}</code></pre>
<p>定义两根手指的共有属性:</p>
<pre><code class="css">.body .arm .fingers::before,
.body .arm .fingers::after {
content: '';
position: absolute;
width: 1.8em;
height: 4em;
background-color: white;
border-radius: 50%;
transform-origin: top;
}</code></pre>
<p>用伪元素分别画出两根手指:</p>
<pre><code class="css">.body .arm .fingers::before {
top: 22em;
left: 2em;
transform: rotate(-25deg);
box-shadow: inset 0.2em -0.4em 0.4em rgba(0, 0, 0, 0.4);
}
.body .arm .fingers::after {
top: 21.5em;
left: 4.8em;
transform: rotate(-5deg);
box-shadow: inset -0.2em -0.4em 0.8em rgba(0, 0, 0, 0.3);
z-index: -3;
}</code></pre>
<p>至此,完成了右胳膊。把右胳膊复制并水平翻转,即可得到左胳膊:</p>
<pre><code class="css">.body .arm.left {
transform: scaleX(-1);
right: 0;
z-index: -3;
}</code></pre>
<p>接下来画腿部。<br>在 html 文件中增加腿的 dom 元素:</p>
<pre><code class="html"><div class="baymax">
<div class="head">
<!-- 略 -->
</div>
<div class="body">
<!-- 略 -->
</div>
<div class="left leg"></div>
<div class="right leg"></div>
</div></code></pre>
<p>画出腿的内侧:</p>
<pre><code class="css">.leg {
position: absolute;
width: 5em;
height: 16em;
bottom: 0;
background-color: white;
border-bottom-right-radius: 1.5em;
left: 10em;
box-shadow: inset -0.7em -0.6em 0.7em rgba(0, 0, 0, 0.1);
z-index: -3;
}</code></pre>
<p>画出腿的外侧:</p>
<pre><code class="css">.leg::before {
content: '';
position: absolute;
width: 2.5em;
height: inherit;
background-color: white;
border-bottom-left-radius: 100%;
left: -2.5em;
box-shadow: inset 0.7em 1.5em 0.7em rgba(0, 0, 0, 0.4);
}</code></pre>
<p>至此,完成了右腿。把右腿复制并水平翻转,即可得到左腿:</p>
<pre><code class="css">.leg.left {
transform-origin: right;
transform: scaleX(-1);
}</code></pre>
<p>大功告成!</p>
前端每日实战:150# 视频演示如何用 CSS 和 D3 创作一个集体舞动画
https://segmentfault.com/a/1190000016582341
2018-10-02T10:26:18+08:00
2018-10-02T10:26:18+08:00
comehope
https://segmentfault.com/u/comehope
10
<p><img src="/img/bVbhJZg?w=400&h=301" alt="图片描述" title="图片描述"></p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/yRYOwq">https://codepen.io/comehope/pen/yRYOwq</a></p>
<h2>可交互视频</h2>
<p>此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。</p>
<p>请用 chrome, safari, edge 打开观看。</p>
<p><a href="https://link.segmentfault.com/?enc=TvPKn%2BB68WS%2FKpiCqq4%2B7A%3D%3D.kGUUhW7wZEEuhaHStll8qtsJVfD%2BCFCwnzp%2BNFkIYnxnDQtXtPlaJuhwueAU1H6a" rel="nofollow">https://scrimba.com/p/pEgDAM/cBZ3Nt6</a></p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=0nwZM61z4EGBu5L4SCYV1Q%3D%3D.1u5y5D2xK2UPbMH31BUxbBPxQdr4i7Td%2B6ModqxbDw%2Bu2hYE9EP13fKCR87ZmylfUwtzrUPuhyrK%2Bgxsnpe%2FCA%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<p>定义 dom,容器中包含 1 个 <code>.square</code> 子容器,子容器中包含 4 个 <code><span></code>,每个 <code><span></code> 代表一个对角扇形:</p>
<pre><code class="html"><figure class="container">
<div class="square">
<span></span>
<span></span>
<span></span>
<span></span>
</div>
</figure></code></pre>
<p>居中显示:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: #222;
}</code></pre>
<p>设置容器的尺寸单位,<code>1em</code> 等于 <code>8px</code>:</p>
<pre><code class="css">.container {
font-size: 8px;
}</code></pre>
<p>子容器中的 4 个 <code><span></code> 不设宽高,只设边框,其中第 1 个和第 4 个 <code><span></code> 只取左右边框,第 2 个和第 3 个 <code><span></code> 只取上下边框:</p>
<pre><code class="css">.square span {
display: block;
border: 2.5em solid transparent;
color: #ddd;
}
.square span:nth-child(1),
.square span:nth-child(4) {
border-left-color: currentColor;
border-right-color: currentColor;
}
.square span:nth-child(2),
.square span:nth-child(3) {
border-top-color: currentColor;
border-bottom-color: currentColor;
}</code></pre>
<p>把边框改为圆弧:</p>
<pre><code class="css">.square span {
border-radius: 50%;
}</code></pre>
<p>在子容器中用 grid 布局把 4 个 <code><span></code> 设置为 2 * 2 的网格:</p>
<pre><code class="css">.square {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-gap: 0.2em;
padding: 0.1em;
}</code></pre>
<p>旋转 4 个 <code><span></code>,使它们围合成一个正方形,看起来像一个花朵,算式的结果是 45 度,写成这样是为了和接下来的动画的算式的形式保持一致:</p>
<pre><code class="css">.square span {
transform: rotate(calc(45deg + 90deg * 0));
}</code></pre>
<p>增加让 <code><span></code> 旋转的动画,整个动画过程旋转 4 次,每次旋转 90 度,4 次旋转之后即返回原位:</p>
<pre><code class="css">.square span {
animation: rotation 2s ease-in-out infinite;
}
@keyframes rotation {
0% { transform: rotate(calc(45deg + 90deg * 0)); }
25% { transform: rotate(calc(45deg + 90deg * 1)); }
50% { transform: rotate(calc(45deg + 90deg * 2)); }
75% { transform: rotate(calc(45deg + 90deg * 3)); }
100% { transform: rotate(calc(45deg + 90deg * 4)); }
}</code></pre>
<p>使其中 2 个 <code><span></code> 朝相反的方向运动:</p>
<pre><code class="css">.square span:nth-child(2),
.square span:nth-child(3) {
animation-direction: reverse;
}</code></pre>
<p>至此,一个 <code>.square</code> 子容器的动画已经完成,接下来制作 4 个 <code>.square</code> 的动画。<br>在 dom 中再增加 3 组 <code>.square</code> 子容器:</p>
<pre><code class="html"><figure class="container">
<div class="square">
<span></span>
<span></span>
<span></span>
<span></span>
</div>
<div class="square">
<span></span>
<span></span>
<span></span>
<span></span>
</div>
<div class="square">
<span></span>
<span></span>
<span></span>
<span></span>
</div>
<div class="square">
<span></span>
<span></span>
<span></span>
<span></span>
</div>
</figure></code></pre>
<p>用 grid 布局把 4 个 <code>.square</code> 布局成网格状,变量 <code>--columns</code> 是网格的边长,即每边有 2 个 <code>.square</code> 子容器:</p>
<pre><code class="css">.container {
display: grid;
--columns: 2;
grid-template-columns: repeat(var(--columns), 1fr);
}</code></pre>
<p>现在看起来好像是有几个黑色的小方块在不停地移动,当 dom 元素越多时,动画效果看起来就越壮观,就像集体舞一样,人越多越有气势。接下来用 d3 批量增加 dom 的元素。<br>引入 d3 库:</p>
<pre><code class="html"><script src="https://d3js.org/d3.v5.min.js"></script></code></pre>
<p>声明一个 <code>COLUMNS</code> 常量,表示网格的边长:</p>
<pre><code class="javascript">const COLUMNS = 2;</code></pre>
<p>删除掉 html 文件中的 <code>.square</code> 子元素,改为用 d3 动态创建:</p>
<pre><code class="javascript">d3.select('.container')
.selectAll('div')
.data(d3.range(COLUMNS * COLUMNS))
.enter()
.append('div')
.attr('class', 'square');</code></pre>
<p>继续用连缀语法增加 <code><span></code> 子元素:</p>
<pre><code class="javascript">d3.select('.container')
.selectAll('div')
.data(d3.range(COLUMNS * COLUMNS))
.enter()
.append('div')
.attr('class', 'square')
.selectAll('span')
.data(d3.range(4))
.enter()
.append('span');</code></pre>
<p>删除掉 css 文件中的 <code>--columns</code> 变量声明,改为用 d3 动态声明:</p>
<pre><code class="javascript">d3.select('.container')
.style('--columns', COLUMNS)
/*略*/</code></pre>
<p>最后,把边长改为 4,即让 16 个 <code>.square</code> 一起动画:</p>
<pre><code class="javascript">const COLUMNS = 4;</code></pre>
<p>大功告成!</p>
前端每日实战:149# 视频演示如何用纯 CSS 创作一个宝路薄荷糖的动画
https://segmentfault.com/a/1190000016577586
2018-09-30T22:19:38+08:00
2018-09-30T22:19:38+08:00
comehope
https://segmentfault.com/u/comehope
15
<p><img src="/img/bVbhIKv?w=400&h=300" alt="图片描述" title="图片描述"></p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/oagrvz">https://codepen.io/comehope/pen/oagrvz</a></p>
<h2>可交互视频</h2>
<p>此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。</p>
<p>请用 chrome, safari, edge 打开观看。</p>
<p><a href="https://link.segmentfault.com/?enc=aQZZhTP4eLsxvTbxj1wU9g%3D%3D.K%2F83m6DBnKgPbNuAJxqbhJnEELU3wgAuYJsIQC7jArmVbUNjpziCmQZI7%2BJ3AN9A" rel="nofollow">https://scrimba.com/p/pEgDAM/cRbqJcD</a></p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=dx0y3WyZG6RFESRyftN7ew%3D%3D.K%2Bv9twHaPy5sYXRwvZyesB%2B3xSkoUNFnpjpL0KTynPB%2BDmkgHz5gVAAPLpOOUcq2E%2FF%2BTaMX9MeyrJ1kBBGy3g%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<p>定义 dom,只有 1 个元素:</p>
<pre><code class="html"><div class="spinner"></div></code></pre>
<p>居中显示:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: silver;
}</code></pre>
<p>定义容器尺寸:</p>
<pre><code class="css">.spinner {
width: 50vmin;
height: 50vmin;
position: relative;
}</code></pre>
<p>用 <code>before</code> 伪元素画出 1 个像宝路薄荷糖状的黑色圆环:</p>
<pre><code class="css">.spinner::before {
content: '';
position: absolute;
box-sizing: border-box;
width: inherit;
height: inherit;
border: 12.5vmin solid;
border-radius: 50%;
}</code></pre>
<p>接下来制作动画效果。<br>设置透视景深:</p>
<pre><code class="css">body {
perspective: 400px;
}</code></pre>
<p>让圆环在 z 轴上运动:</p>
<pre><code class="css">.spinner::before {
animation: spin 1.5s ease-in-out infinite both reverse;
}
@keyframes spin {
0%, 100% {
transform: translateZ(10vmin);
}
60% {
transform: translateZ(-10vmin);
}
}</code></pre>
<p>让圆环在 z 轴距离较大时稍稍倾斜:</p>
<pre><code class="css">@keyframes spin {
0%, 100% {
transform: translateZ(10vmin) rotateX(25deg);
}
60% {
transform: translateZ(-10vmin);
}
}</code></pre>
<p>增加缩放效果:</p>
<pre><code class="css">@keyframes spin {
0%, 100% {
transform: translateZ(10vmin) rotateX(25deg);
}
33% {
transform: translateZ(-10vmin) scale(0.4);
}
60% {
transform: translateZ(-10vmin);
}
}</code></pre>
<p>用 <code>after</code> 伪元素再画出一个白色的圆环,并且让它的动画延迟动画总长的一半时间:</p>
<pre><code class="css">.spinner::before,
.spinner::after {
/*略*/
animation: spin 1.5s ease-in-out infinite both reverse;
}
.spinner::after {
border-color: white;
animation-delay: -0.75s;
}</code></pre>
<p>接下来制作容器的动画效果,为了不受子元素动画的影响,先暂时屏蔽伪元素的动画效果。</p>
<pre><code class="css">.spinner::before,
.spinner::after {
/* animation: spin 1.5s ease-in-out infinite both reverse; */
}</code></pre>
<p>增加容器沿 x 轴旋转的动画效果,动画时间为子元素动画时间的2倍:</p>
<pre><code class="css">.spinner {
animation: wobble 3s ease-in-out infinite;
}
@keyframes wobble {
0%, 100% {
transform: rotateX(15deg);
}
50% {
transform: rotateX(60deg);
}
}</code></pre>
<p>增加容器沿 y 轴旋转的动画效果:</p>
<pre><code class="css">@keyframes wobble {
0%, 100% {
transform: rotateX(15deg) rotateY(60deg);
}
50% {
transform: rotateX(60deg) rotateY(-60deg);
}
}</code></pre>
<p>增加容器整体旋转的动画效果:</p>
<pre><code class="css">@keyframes wobble {
0%, 100% {
transform: rotateX(15deg) rotateY(60deg);
}
50% {
transform: rotateX(60deg) rotateY(-60deg) rotate(180deg);
}
}</code></pre>
<p>打开子元素的动画效果,使子元素的动画效果和容器的动画效果叠加:</p>
<pre><code class="css">.spinner::before,
.spinner::after {
animation: spin 1.5s ease-in-out infinite both reverse;
}</code></pre>
<p>最后,使子元素在 3d 空间上运动:</p>
<pre><code class="css">.spinner {
transform-style: preserve-3d;
}</code></pre>
<p>大功告成!</p>
前端每日实战:148# 视频演示如何用纯 CSS 创作从按钮两侧滑入装饰元素的悬停特效
https://segmentfault.com/a/1190000016561226
2018-09-29T12:40:32+08:00
2018-09-29T12:40:32+08:00
comehope
https://segmentfault.com/u/comehope
21
<p><img src="/img/bVbhEuH?w=400&h=300" alt="图片描述" title="图片描述"></p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/yRyOZr">https://codepen.io/comehope/pen/yRyOZr</a></p>
<h2>可交互视频</h2>
<p>此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。</p>
<p>请用 chrome, safari, edge 打开观看。</p>
<p><a href="https://link.segmentfault.com/?enc=8YuXjdF179R8m%2BAuiPODYQ%3D%3D.UvS%2FVWolEKMvc6WjUBL6f7FrAHbIUMvejqMNsr2hSS0HFh%2BH20fU%2BSXJrUuvRzKq" rel="nofollow">https://scrimba.com/p/pEgDAM/cmWMQtz</a></p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=KERtXSf%2FWkMQ2OcOkxmyNQ%3D%3D.QMD0IjzOLsUQ8nWiAgAn88mtB8q5sOlN6pJiFnCcldqepLqBLgZIkQThGOd16ixHM%2BQLyyIceU1%2B3MkwG8kwjQ%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<p>定义 dom,容器是一个无序列表,列表项代表按钮:</p>
<pre><code class="html"><ul>
<li>home</li>
</ul></code></pre>
<p>居中显示:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(deepskyblue, navy);
}</code></pre>
<p>去掉列表项前面的符号:</p>
<pre><code class="css">ul {
padding: 0;
list-style-type: none;
}</code></pre>
<p>设置按钮的文字样式:</p>
<pre><code class="css">ul li {
color: #ddd;
font-size: 25px;
font-family: sans-serif;
text-transform: uppercase;
}</code></pre>
<p>用伪元素在按钮的左侧增加一个方块:</p>
<pre><code class="css">ul li {
position: relative;
}
ul li::before {
content: '';
position: absolute;
width: 100%;
height: 100%;
background: tomato;
left: -100%;
}</code></pre>
<p>用伪元素在按钮的右侧增加一条下划线:</p>
<pre><code class="css">ul li::after {
content: '';
position: absolute;
width: 100%;
height: 0.2em;
background: tomato;
bottom: 0;
left: 100%;
}</code></pre>
<p>接下来设置鼠标悬停效果。<br>当鼠标悬停时,左侧的方块移到文字所在位置:</p>
<pre><code class="css">ul li::before {
transition: 0.4s ease-out;
}
ul li:hover::before {
left: 100%;
}</code></pre>
<p>右侧的下划线移到文字所在位置,它的动画时间延迟到方块的动画快结束时再开始:</p>
<pre><code class="css">ul li::after {
transition: 0.3s 0.3s ease-out;
}
ul li:hover::after {
left: 0%;
}</code></pre>
<p>同时,提高文字的亮度:</p>
<pre><code class="css">ul li {
transition: 0.3s;
cursor: pointer;
}
ul li:hover {
color: #fff;
}</code></pre>
<p>隐藏掉按钮外的部分,使方块和下划线在默认状态下都不可见,只有鼠标悬停时它们才从两侧入场:</p>
<pre><code class="css">ul li {
overflow: hidden;
}</code></pre>
<p>最后,在 dom 中再增加几个按钮:</p>
<pre><code class="html"><ul>
<li>home</li>
<li>products</li>
<li>services</li>
<li>contact</li>
</ul></code></pre>
<p>布局多个按钮:</p>
<pre><code class="css">ul {
display: flex;
flex-direction: column;
align-items: center;
}
ul li {
margin: 0.5em;
}</code></pre>
<p>大功告成!</p>
前端每日实战:147# 视频演示如何用纯 CSS 创作透视按钮的悬停特效
https://segmentfault.com/a/1190000016556930
2018-09-29T07:07:59+08:00
2018-09-29T07:07:59+08:00
comehope
https://segmentfault.com/u/comehope
29
<p><img src="/img/bVbhDnp?w=400&h=302" alt="图片描述" title="图片描述"></p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/qJEdKb">https://codepen.io/comehope/pen/qJEdKb</a></p>
<h2>可交互视频</h2>
<p>此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。</p>
<p>请用 chrome, safari, edge 打开观看。</p>
<p><a href="https://link.segmentfault.com/?enc=vpv85mFeRL%2BkfFHU6uYzhg%3D%3D.YmbkAKdbfmBrWGQ9xQQB%2BNqwYOx%2BZ5yXTMv3Cfnfs1yV%2FSeCyHY%2BmMFoBf%2F8Qht0" rel="nofollow">https://scrimba.com/p/pEgDAM/cEJRKud</a></p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=q%2FQmbIHkMP7pYcppsKGPCw%3D%3D.oTOltSiMNEUBZ6YUm7MZvt4zqoZHkbRfzCGhBnwFZRS1u752dV7b%2B9WUeAgqSGpJYZxbtBchOSXEW7WTLfekXw%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<p>定义 dom,容器是一个无序列表,包含 4 个元素,代表 4 个按钮:</p>
<pre><code class="html"><ul>
<li>home</li>
<li>products</li>
<li>services</li>
<li>contact</li>
</ul></code></pre>
<p>居中显示:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: cornsilk;
}</code></pre>
<p>去掉列表项前面的符号:</p>
<pre><code class="css">ul {
padding: 0;
list-style-type: none;
}</code></pre>
<p>设置按钮的边框和背景的样式,背景采用渐变色,但渐变的方向依次交替:</p>
<pre><code class="css">ul li {
box-sizing: border-box;
width: 15em;
height: 3em;
font-size: 20px;
border-radius: 0.5em;
margin: 0.5em;
box-shadow: 0 0 1em rgba(0,0,0,0.2);
}
ul li:nth-child(odd) {
background: linear-gradient(to right, orange, tomato);
}
ul li:nth-child(even) {
background: linear-gradient(to left, orange, tomato);
}</code></pre>
<p>设置按钮上文字的样式,依次交替居左或居右:</p>
<pre><code class="css">ul li {
color: white;
font-family: sans-serif;
text-transform: capitalize;
line-height: 3em;
}
ul li:nth-child(odd) {
text-align: left;
padding-left: 10%;
}
ul li:nth-child(even) {
text-align: right;
padding-right: 10%;
}</code></pre>
<p>设置按钮的透视效果,依次交替向左旋转和向右旋转,此时透视的距离是 <code>500px</code>,注意 perspective() 函数和 rotateY() 函数的顺序不能写反:</p>
<pre><code class="css">ul li:nth-child(odd) {
transform: perspective(500px) rotateY(45deg);
}
ul li:nth-child(even) {
transform: perspective(500px) rotateY(-45deg);
}</code></pre>
<p>为按钮增加悬停效果,使悬停时的透视距离变短为 <code>200px</code>,透视距离越短,旋转的幅度看起来就越大:</p>
<pre><code class="css">ul li:nth-child(odd):hover {
transform: perspective(200px) rotateY(45deg);
padding-left: 5%;
}
ul li:nth-child(even):hover {
transform: perspective(200px) rotateY(-45deg);
padding-right: 5%;
}</code></pre>
<p>最后,设置一个缓动时间,使效果转换变得平滑:</p>
<pre><code class="css">ul li {
transition: 0.3s;
cursor: pointer;
}</code></pre>
<p>大功告成!</p>
前端每日实战:146# 视频演示如何用纯 CSS 创作一个脉动 loader
https://segmentfault.com/a/1190000016543472
2018-09-28T07:30:58+08:00
2018-09-28T07:30:58+08:00
comehope
https://segmentfault.com/u/comehope
35
<p><img src="/img/bVbhzSl?w=400&h=300" alt="图片描述" title="图片描述"></p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/wYvGwr">https://codepen.io/comehope/pen/wYvGwr</a></p>
<h2>可交互视频</h2>
<p>此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。</p>
<p>请用 chrome, safari, edge 打开观看。</p>
<p><a href="https://link.segmentfault.com/?enc=0Nn64J4qgJ0quMvXmAzoGQ%3D%3D.vKx%2BNRe7AgwYryZcL7T672CAB4vJVHeYr6ap0%2FqMOx3voUm8C4R9mklgcbhwwpFH" rel="nofollow">https://scrimba.com/p/pEgDAM/cnMgQTr</a></p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=uR0asaOQDi9RSyCvE5OrDQ%3D%3D.mIwjKXsEiaqzbLM3zhAhkPZFFGtq0jJun3KQkeBYRqbaXPGirvolQiS1i4yk8l41O3H9EMVPPHPaGrIkKUDcIw%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<p>定义 dom,容器中包含 10 个子元素:</p>
<pre><code class="html"><div class="loader">
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div></code></pre>
<p>居中显示:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(#eee 70%, pink);
}</code></pre>
<p>设置容器的样式,是粉色背景并描边的一个圆:</p>
<pre><code class="css">.loader {
width: 6em;
height: 6em;
padding: 3em;
font-size: 10px;
background-color: pink;
border-radius: 50%;
border: 0.8em solid hotpink;
}</code></pre>
<p>设置子元素的布局方式为横向平铺:</p>
<pre><code class="css">.loader {
display: flex;
align-items: center;
justify-content: space-between;
}</code></pre>
<p>设置子元素的样式:</p>
<pre><code class="css">.loader > span {
width: 0.5em;
height: 50%;
background-color: deeppink;
}</code></pre>
<p>增加子元素的动画效果:</p>
<pre><code class="css">.loader > span {
transform: scaleY(0.05) translateX(-0.5em);
animation: span-animate 1.5s infinite ease-in-out;
}
@keyframes span-animate {
0%, 100% {
transform: scaleY(0.05) translateX(-0.5em);
}
15% {
transform: scaleY(1.2) translateX(1em);
}
90%, 100% {
background-color: hotpink;
}
}</code></pre>
<p>设置子元素下标,让子元素依次播放动画:</p>
<pre><code class="css">.loader > span {
animation-delay: calc(var(--n) * 0.05s);
}
.loader > span:nth-child(1) { --n: 1; }
.loader > span:nth-child(2) { --n: 2; }
.loader > span:nth-child(3) { --n: 3; }
.loader > span:nth-child(4) { --n: 4; }
.loader > span:nth-child(5) { --n: 5; }
.loader > span:nth-child(6) { --n: 6; }
.loader > span:nth-child(7) { --n: 7; }
.loader > span:nth-child(8) { --n: 8; }
.loader > span:nth-child(9) { --n: 9; }
.loader > span:nth-child(10) { --n: 10; }</code></pre>
<p>增加容器动画,加强脉动的效果:</p>
<pre><code class="css">.loader {
animation: loader-animate 1.5s infinite ease-in-out;
}
@keyframes loader-animate {
45%, 55% {
transform: scale(1.05);
}
}</code></pre>
<p>大功告成!</p>
前端每日实战:145# 视频演示如何用纯 CSS 创作一个电源开关控件
https://segmentfault.com/a/1190000016530200
2018-09-27T05:42:39+08:00
2018-09-27T05:42:39+08:00
comehope
https://segmentfault.com/u/comehope
23
<p><img src="/img/bVbhwqh?w=400&h=301" alt="图片描述" title="图片描述"></p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/PdMyJd">https://codepen.io/comehope/pen/PdMyJd</a></p>
<h2>可交互视频</h2>
<p>此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。</p>
<p>请用 chrome, safari, edge 打开观看。</p>
<p><a href="https://link.segmentfault.com/?enc=5PLlPv4Bj0WXFOp92FxW7Q%3D%3D.MX6CJYPBJqGswo9Z9Yvk%2F20BIM1uCJF%2FXZE0qlHng9AFRLSGAnjgXVc2Z0qxNHHN" rel="nofollow">https://scrimba.com/p/pEgDAM/c648Nu7</a></p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=War5qTMdSK2jLUYKJzgINQ%3D%3D.sKKURfbQvZh0HY%2Bz0igJL59a%2FIjpUqVrzW5J3nOEWexCnm6dmxNu7EIZOTxdka2aDSt1OtRdAgTLWXlAYNfqgw%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<p>定义 dom,包含 3 个元素,分别代表 <code>input</code> 控件、开关和灯光:</p>
<pre><code class="html"><input type="checkbox" class="toggle">
<div class="switch"></div>
<div class="light"></div></code></pre>
<p>居中显示:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: #eee;
}</code></pre>
<p>定义控件的样式,把控件的设置为透明,使其不可见,但仍可与用户交互。其中字号大小是变量,因为 <code>input</code> 控件的字号与正文字号不同,所以需要单独设置:</p>
<pre><code class="css">body {
font-size: var(--font-size);
}
:root {
--font-size: 16px;
}
.toggle {
position: absolute;
font-size: var(--font-size);
width: 5em;
height: 8em;
margin: 0;
filter: opacity(0);
cursor: pointer;
z-index: 2;
}</code></pre>
<p>设置开关的轮廓:</p>
<pre><code class="css">.switch {
position: absolute;
width: 5em;
height: 8em;
border-radius: 1.2em;
background: linear-gradient(#d2d4d6, white);
}</code></pre>
<p>用阴影使开关变得立体:</p>
<pre><code class="css">.switch {
box-shadow:
inset 0 -0.2em 0.4em rgba(212, 212, 212, 0.75),
inset 0 -0.8em 0 0.1em rgba(156, 156, 156, 0.85),
0 0 0 0.1em rgba(116, 116, 116, 0.8),
0 0.6em 0.6em rgba(41, 41, 41, 0.3),
0 0 0 0.4em #d4d7d8;
}</code></pre>
<p>用伪元素设置 <code>on</code> 和 <code>off</code> 文本,目前是处于 <code>off</code> 状态:</p>
<pre><code class="css">.toggle ~ .switch::before,
.toggle ~ .switch::after {
position: absolute;
width: 100%;
text-align: center;
font-size: 1.4em;
font-family: sans-serif;
text-transform: uppercase;
}
.toggle ~ .switch::before {
content: '|';
bottom: 1em;
color: rgba(0, 0, 0, 0.5);
text-shadow: 0 0.1em 0 rgba(255, 255, 255, 0.8);
}
.toggle ~ .switch::after {
content: 'O';
top: 0.6em;
color: rgba(0, 0, 0, 0.45);
text-shadow: 0 0.1em 0 rgba(255, 255, 255, 0.4);
}</code></pre>
<p>把 <code>input</code> 控件设置为 <code>checked</code>状态,以便设置处于 <code>on</code> 状态的样式:</p>
<pre><code class="html"><input type="checkbox" checked="checked" class="toggle"></code></pre>
<p>设置处于 <code>on</code> 状态的开关样式:</p>
<pre><code class="css">.toggle:checked ~ .switch {
background: linear-gradient(#a1a2a3, #f0f0f0);
box-shadow:
inset 0 0.2em 0.4em rgba(212, 205, 199, 0.75),
inset 0 0.8em 0 0.1em rgba(255, 255, 255, 0.77),
0 0 0 0.1em rgba(116, 116, 118, 0.8),
0 -0.2em 0.2em rgba(41, 41, 41, 0.18),
0 0 0 0.4em #d4d7d8;
}</code></pre>
<p>设置处于 <code>on</code> 状态的文本样式:</p>
<pre><code class="css">.toggle:checked ~ .switch::before {
bottom: 0.3em;
text-shadow: 0 0.1em 0 rgba(255, 255, 255, 0.5);
}
.toggle:checked ~ .switch::after {
top: 1.2em;
text-shadow: 0 0.1em 0 rgba(255, 255, 255, 0.15);
}</code></pre>
<p>接下来设置灯光效果。<br>先设置处于 <code>off</code> 状态的黑暗效果:</p>
<pre><code class="css">.toggle ~ .light {
width: 100vw;
height: 100vh;
background-color: #0a0a16;
z-index: 1;
filter: opacity(0.7);
}</code></pre>
<p>再设置处于 <code>on</code> 状态的明亮效果:</p>
<pre><code class="css">.toggle:checked ~ .light {
filter: opacity(0);
}</code></pre>
<p>大功告成!</p>
前端每日实战:144# 视频演示如何用 D3 和 GSAP 创作一个集体舞动画
https://segmentfault.com/a/1190000016521212
2018-09-26T12:13:07+08:00
2018-09-26T12:13:07+08:00
comehope
https://segmentfault.com/u/comehope
12
<p><img src="/img/bVbht5j?w=400&h=301" alt="图片描述" title="图片描述"></p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/gdVObN">https://codepen.io/comehope/pen/gdVObN</a></p>
<h2>可交互视频</h2>
<p>此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。</p>
<p>请用 chrome, safari, edge 打开观看。</p>
<p><a href="https://link.segmentfault.com/?enc=%2BMS3algJM1sV30mh8XhoTw%3D%3D.71jpl%2FH7kq8NnIckmfGA%2FCuuZD1QGVh4fUvIf26QmrHlmCyX9pZMP6lOaOB%2BWgNy" rel="nofollow">https://scrimba.com/p/pEgDAM/caRLack</a></p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=%2FhsbjoflA6MWFvC7bulyTA%3D%3D.KqCRk3GDTIy7zj%2F6VPSqvAcXpxfUaLRiIWA4BCRbl4HAd3FGKWpssK%2BDo%2FdNEw9AMo0L4t2LSPUBHmxEvqwfTQ%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<p>定义 dom,容器中包含 2 个子容器,<code>.horizontal</code> 代表水平的线段,<code>.vertical</code> 代表垂直的线段,每个子容器中包含 4 个子元素:</p>
<pre><code class="html"><div class="container">
<div class="horizontal">
<span></span>
<span></span>
<span></span>
<span></span>
</div>
<div class="vertical">
<span></span>
<span></span>
<span></span>
<span></span>
</div>
</div></code></pre>
<p>居中显示:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: skyblue;
}</code></pre>
<p>设置容器尺寸,其中 <code>--side-length</code> 是方阵的每一边的元素数量:</p>
<pre><code class="css">.container {
--side-length: 2;
position: relative;
width: calc(40px * calc(var(--side-length)));
height: calc(40px * calc(var(--side-length)));
}</code></pre>
<p>用 grid 布局排列子元素,4 个元素排列成 2 * 2 的方阵:</p>
<pre><code class="css">.container .horizontal,
.container .vertical {
position: absolute;
top: 0;
left: 0;
display: grid;
grid-template-columns: repeat(var(--side-length), 1fr);
}</code></pre>
<p>设置子元素的样式,<code>.horizontal</code> 内的子元素是横条,<code>.vertical</code> 内的子元素是竖条:</p>
<pre><code class="css">.container .horizontal span {
width: 40px;
height: 10px;
background: #fff;
margin: 15px 0;
}
.container .vertical span {
width: 10px;
height: 40px;
background: #fff;
margin: 0 15px;
}</code></pre>
<p>至此,静态布局完成,接下来用 d3 批量处理子元素。<br>引入 d3 库:</p>
<pre><code class="html"><script src="https://d3js.org/d3.v5.min.js"></script></code></pre>
<p>删除掉 html 文件中的子元素 dom 节点,删除掉 css 文件中声明的 css 变量。<br>定义方阵每一边的元素数量,并把这个数值赋给 css 变量:</p>
<pre><code class="javascript">const SIDE_LENGTH = 2;
let container = d3.select('.container')
.style('--side-length', SIDE_LENGTH);</code></pre>
<p>定义一个添加 <code>span</code> 子元素的函数,分别添加横向和竖向的子元素:</p>
<pre><code class="javascript">function appendSpan(selector) {
container.select(selector)
.selectAll('span')
.data(d3.range(SIDE_LENGTH * SIDE_LENGTH))
.enter()
.append('span');
}
appendSpan('.horizontal');
appendSpan('.vertical');</code></pre>
<p>此时,布局已改为动态的,可以通过修改 <code>SIDE_LENGTH</code> 的值来创建不同边长的方阵,比如以下语句将创建 5 * 5 的方阵:</p>
<pre><code class="javascript">const SIDE_LENGTH = 5;</code></pre>
<p>接下来用 GSAP 创建动画。(注:因 scrimba 在使用 gsap 时会崩溃,所以视频演示采用 css 动画,但 codepen 和 github 均采用 gsap 动画)<br>引入 GSAP 库:</p>
<pre><code class="html"><script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/2.0.2/TweenMax.min.js"></script></code></pre>
<p>声明动画变量 <code>animation</code>,声明代表 dom 元素的变量 <code>$horizontalSpan</code> 和 <code>$verticalSpan</code>:</p>
<pre><code class="javascript">let animation = new TimelineMax({repeat: -1});
let $horizontalSpan = '.container .horizontal span';
let $verticalSpan = '.container .vertical span';</code></pre>
<p>先创建横条的动画,共分成 4 步,每个 <code>to</code> 语句的最后一个参数是步骤的名称:</p>
<pre><code class="javascript">animation.to($horizontalSpan, 1, {rotation: 45}, 'step1')
.to($horizontalSpan, 1, {x: '-10px', y: '-10px'}, 'step2')
.to($horizontalSpan, 1, {rotation: 0, x: '0', y: '0', scaleY: 2, scaleX: 0.5}, 'step3')
.to($horizontalSpan, 1, {rotation: 90, scaleY: 1, scaleX: 1}, 'step4')</code></pre>
<p>再创建竖条的动画,<code>to</code> 语句的步骤名称与横条的步骤名称相同,以便与横条保持动画同步:</p>
<pre><code class="javascript">animation.to($verticalSpan, 1, {rotation: 45}, 'step1')
.to($verticalSpan, 1, {x: '10px', y: '10px'}, 'step2')
.to($verticalSpan, 1, {x: '0', y: '0', scaleX: 2, scaleY: 0.5}, 'step3')
.to($verticalSpan, 1, {rotation: 90, scaleX: 1, scaleY: 1}, 'step4');</code></pre>
<p>在动画的末尾用时间尺度缩放函数让动画播放速度加快一倍:</p>
<pre><code class="javascript">animation.timeScale(2);</code></pre>
<p>最后,把方阵的边长改为 10,方阵越大就越有气势:</p>
<pre><code class="javascript">const SIDE_LENGTH = 10;</code></pre>
<p>大功告成!</p>
前端每日实战:143# 视频演示如何用 CSS 的 Grid 布局创作一枚小松鼠邮票
https://segmentfault.com/a/1190000016510482
2018-09-25T15:37:02+08:00
2018-09-25T15:37:02+08:00
comehope
https://segmentfault.com/u/comehope
19
<p><img src="/img/bVbhrie?w=400&h=300" alt="图片描述" title="图片描述"></p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/YOoXpv">https://codepen.io/comehope/pen/YOoXpv</a></p>
<h2>可交互视频</h2>
<p>此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。</p>
<p>请用 chrome, safari, edge 打开观看。</p>
<p><a href="https://link.segmentfault.com/?enc=ROKyYwYggXx18TXyW3CCfw%3D%3D.EPryRZvSVZp%2FjOIRt6PQXlNX1yiRsRKr4FVIS13i0KRwfeIn4UAOWyPNZBJxV%2Fs9" rel="nofollow">https://scrimba.com/p/pEgDAM/c7KdMt8</a></p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=ZdSqGRSEhFvtwUD3MJmcJA%3D%3D.82y5zHALFUtITdaMFa0l67kKrll4DwQ%2B2%2B8b6oO%2FD6pv3SKuYnTlgKkFLFvxSR1RXbaJB0h6lXmSmxgynf%2Bhyw%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<p>定义 dom,容器表示邮票:</p>
<pre><code class="html"><div class="stamp">
</div></code></pre>
<p>居中显示:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: teal;
}</code></pre>
<p>设置容器尺寸:</p>
<pre><code class="css">.stamp {
position: relative;
width: 45em;
height: 63em;
font-size: 6px;
padding: 5em;
background-color: white;
}</code></pre>
<p>用重复背景绘制出邮票的齿孔:</p>
<pre><code class="css">.stamp {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.stamp::after,
.stamp::before {
content: '';
width: 100%;
height: 100%;
position: absolute;
background:
radial-gradient(circle, teal 50%, transparent 50%),
radial-gradient(circle, teal 50%, transparent 50%);
background-size: 3.5em 3.5em;
}
.stamp::before {
top: 1.5em;
background-repeat: repeat-y;
background-position: -4% 0, 104% 0;
}
.stamp::after {
left: 1.5em;
background-repeat: repeat-x;
background-position: 0 -3%, 0 103%;
}</code></pre>
<p>在 html 文件中增加小鸡的 dom 元素,子元素分别表示耳朵、头部、身体、尾巴下部、尾巴上部、腿、爪子:</p>
<pre><code class="html"><div class="stamp">
<div class="squirrel">
<div class="ear"></div>
<div class="head"></div>
<div class="body"></div>
<div class="tail-start"></div>
<div class="tail-end"></div>
<div class="leg"></div>
<div class="foot"></div>
</div>
</div></code></pre>
<p>设置 grid 布局的行列尺寸:</p>
<pre><code class="css">.squirrel {
display: grid;
grid-template-columns: 11.5em 7em 15.5em 10.5em;
grid-template-rows: 13em 5em 11.5em 22.5em;
background-color: silver;
padding: 2em;
margin-top: -2em;
}</code></pre>
<p>画出扇形的头部:</p>
<pre><code class="css">.head {
grid-column: 1;
grid-row: 3;
background-color: chocolate;
border-bottom-left-radius: 100%;
}</code></pre>
<p>用径向渐变画出眼睛:</p>
<pre><code class="css">.head {
background-image: radial-gradient(
circle at 58% 22%,
black 1.4em,
transparent 1.4em
);
}</code></pre>
<p>画出扇形的耳朵:</p>
<pre><code class="css">.ear {
grid-column: 2;
grid-row: 2;
width: 5em;
background-color: bisque;
border-bottom-right-radius: 100%;
}</code></pre>
<p>画出扇形的身体:</p>
<pre><code class="css">.body {
grid-column: 2 / 4;
grid-row: 4;
background-color: chocolate;
border-top-right-radius: 100%;
position: relative;
overflow: hidden;
}</code></pre>
<p>用伪元素,通过阴影画出蜷曲的腿:</p>
<pre><code class="css">.body::before {
content: '';
position: absolute;
width: 100%;
height: 50%;
box-shadow: 2em -2em 4em rgba(0, 0, 0, 0.3);
bottom: 0;
--w: calc((7em + 15.5em) / 2);
border-top-left-radius: var(--w);
border-top-right-radius: var(--w);
}</code></pre>
<p>画出半圆形的小爪子:</p>
<pre><code class="css">.foot {
grid-column: 1;
grid-row: 4;
height: 4em;
width: 8em;
background-color: saddlebrown;
justify-self: end;
align-self: end;
border-radius: 4em 4em 0 0;
filter: brightness(0.8);
}</code></pre>
<p>画出半圆形的尾巴下部:</p>
<pre><code class="css">.tail-start {
grid-column: 4;
grid-row: 4;
--h: calc(22.5em - 1.5em);
height: var(--h);
background-color: bisque;
align-self: end;
border-radius: 0 var(--h) var(--h) 0;
}</code></pre>
<p>画出半圆形的尾巴上部:</p>
<pre><code class="css">.tail-end {
grid-column: 3;
grid-row: 1 / 5;
--h: calc(13em + 5em + 11.5em + 1.5em);
height: var(--h);
background-color: chocolate;
border-radius: var(--h) 0 0 var(--h);
}</code></pre>
<p>在 dom 中再增加一些文本,包括标题、作者和面值:</p>
<pre><code class="html"><div class="stamp">
<div class="puppy">
<!-- 略 -->
</div>
<p class="text">
<span class="title">Squirrel</span>
<span class="author">comehope</span>
<span class="face-value">200</span>
</p>
</div></code></pre>
<p>设置标题的文字样式:</p>
<pre><code class="css">.text {
position: relative;
width: calc(100% + 2em * 2);
height: 6em;
font-family: sans-serif;
}
.text .title {
position: absolute;
font-size: 6em;
font-weight: bold;
color: darkslategray;
}</code></pre>
<p>设置作者的文字样式:</p>
<pre><code class="css">.text .author {
position: absolute;
font-size: 3em;
bottom: -1.2em;
color: dimgray;
}</code></pre>
<p>设置面值的文字样式:</p>
<pre><code class="css">.text .face-value {
position: absolute;
font-size: 14em;
right: 0;
line-height: 0.9em;
color: darkcyan;
}</code></pre>
<p>大功告成!</p>
前端每日实战:142# 视频演示如何用 CSS 的 Grid 布局创作一枚小鸡邮票
https://segmentfault.com/a/1190000016508267
2018-09-25T12:49:09+08:00
2018-09-25T12:49:09+08:00
comehope
https://segmentfault.com/u/comehope
11
<p><img src="/img/bVbhqIw?w=400&h=300" alt="图片描述" title="图片描述"></p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/mGZbmQ">https://codepen.io/comehope/pen/mGZbmQ</a></p>
<h2>可交互视频</h2>
<p>此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。</p>
<p>请用 chrome, safari, edge 打开观看。</p>
<p><a href="https://link.segmentfault.com/?enc=VkE42sCTZTUgZ8UOvCOLSg%3D%3D.ADYPN%2BxxvwHzNEYrXDd%2FjOTaDyx4Bsb6yjZ5cb%2B%2FvbpPOExS%2Bx8Eak%2FhDlM3Olfv" rel="nofollow">https://scrimba.com/p/pEgDAM/cWMPaha</a></p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=7Mw%2BmJRjD2rF85xfbJHrIg%3D%3D.%2B4GrX0idr%2B6ircqxpXLnYtMpq5bG3PXlfgDPMOdvZEKqxHLBlsTB2M1lQ97j0sknt3n5D49CL99vcysVVLdPyQ%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<p>定义 dom,容器表示邮票:</p>
<pre><code class="html"><div class="stamp">
</div></code></pre>
<p>居中显示:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: teal;
}</code></pre>
<p>设置容器尺寸:</p>
<pre><code class="css">.stamp {
position: relative;
width: 57em;
height: 71em;
font-size: 5px;
padding: 5em;
background-color: white;
}</code></pre>
<p>用重复背景绘制出邮票的齿孔:</p>
<pre><code class="css">.stamp {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.stamp::after,
.stamp::before {
content: '';
width: 100%;
height: 100%;
position: absolute;
background:
radial-gradient(circle, teal 50%, transparent 50%),
radial-gradient(circle, teal 50%, transparent 50%);
background-size: 3.5em 3.5em;
}
.stamp::before {
top: 1.5em;
background-repeat: repeat-y;
background-position: -3% 0, 103% 0;
}
.stamp::after {
left: 1.5em;
background-repeat: repeat-x;
background-position: 0 -2.5%, 0 102.5%;
}</code></pre>
<p>在 html 文件中增加小鸡的 dom 元素,子元素分别表示头部、喙、身体、尾巴、腿、爪子、太阳、桔子:</p>
<pre><code class="html"><div class="stamp">
<div class="rooster">
<span class="head"></span>
<span class="beak"></span>
<span class="body"></span>
<span class="tail"></span>
<span class="leg"></span>
<span class="foot"></span>
<span class="sun"></span>
<span class="orange-stuff"></span>
</div>
</div></code></pre>
<p>设置 grid 布局的行列尺寸:</p>
<pre><code class="css">.rooster {
display: grid;
grid-template-columns: 22.5em 13em 1.75em 14.5em 4.5em;
grid-template-rows: 12.5em 14.5em 15em 8em 5.5em;
background-color: wheat;
padding: 2em;
margin-top: -2em;
}</code></pre>
<p>画出扇形的头部:</p>
<pre><code class="css">.head {
grid-column: 4;
grid-row: 2;
background-color: burlywood;
border-top-left-radius: 100%;
}</code></pre>
<p>画出小鸡的眼睛和脸上的红晕:</p>
<pre><code class="css">.head {
position: relative;
}
.head::after {
content: '';
position: absolute;
width: 2.8em;
height: 2.8em;
border-radius: 50%;
background-color: black;
right: 30%;
box-shadow: 2em 4em 4em rgba(255, 100, 0, 0.5);
}</code></pre>
<p>画出扇形的喙:</p>
<pre><code class="css">.beak {
grid-column: 5;
grid-row: 2;
height: 4.5em;
background-color: darkorange;
border-bottom-right-radius: 100%;
}</code></pre>
<p>画出半圆形的身体:</p>
<pre><code class="css">.body {
grid-column: 2 / 5;
grid-row: 3;
width: 30em;
background-color: saddlebrown;
border-radius: 0 0 15em 15em;
}</code></pre>
<p>用伪元素,通过阴影画出翅膀:</p>
<pre><code class="css">.body {
position: relative;
overflow: hidden;
}
.body::after {
content: '';
position: absolute;
width: 20em;
height: 10em;
border-radius: inherit;
box-shadow: 4em 2em 4em rgba(0, 0, 0, 0.3);
left: calc((30em - 20em) / 2);
}</code></pre>
<p>画出扇形的尾巴:</p>
<pre><code class="css">.tail {
grid-column: 1;
grid-row: 1 / 3;
height: 22.5em;
background-color: burlywood;
align-self: end;
border-top-left-radius: 100%;
}</code></pre>
<p>画出扇形的腿:</p>
<pre><code class="css">.leg {
grid-column: 4;
grid-row: 4;
width: 8em;
background-color: burlywood;
border-bottom-right-radius: 100%;
}</code></pre>
<p>画出扇形的小爪子:</p>
<pre><code class="css">.foot {
grid-column: 4;
grid-row: 5;
width: 5.5em;
background-color: darkorange;
border-top-right-radius: 100%;
}</code></pre>
<p>画出半圆形的太阳:</p>
<pre><code class="css">.sun {
grid-column: 3 / 5;
grid-row: 1;
width: 17em;
--h: calc(17em / 2);
height: var(--h);
background-color: darkorange;
border-radius: 0 0 var(--h) var(--h);
}</code></pre>
<p>画出圆形的桔子和半圆形的叶片,注意此处叶片的画法与前面画半圆形的画法不同:</p>
<pre><code class="css">.orange-stuff {
grid-column: 1;
grid-row: 3 / 6;
width: 16em;
height: 16em;
background-color: darkorange;
align-self: end;
justify-self: end;
border-radius: 50%;
position: relative;
}
.orange-stuff::before {
content: '';
position: absolute;
width: 8em;
height: 8em;
background: linear-gradient(45deg, transparent 50%, saddlebrown 50%);
border-radius: 50%;
top: -6.8em;
left: 10%;
}</code></pre>
<p>在 dom 中再增加一些文本,包括标题、作者和面值:</p>
<pre><code class="html"><div class="stamp">
<div class="puppy">
<!-- 略 -->
</div>
<p class="text">
<span class="title">Rooster</span>
<span class="author">comehope</span>
<span class="face-value">120</span>
</p>
</div></code></pre>
<p>设置标题的文字样式:</p>
<pre><code class="css">.text {
position: relative;
width: calc(100% + 2em * 2);
height: 6em;
font-family: sans-serif;
}
.text .title {
position: absolute;
font-size: 6em;
font-weight: bold;
color: brown;
}</code></pre>
<p>设置作者的文字样式:</p>
<pre><code class="css">.text .author {
position: absolute;
font-size: 3em;
bottom: -1.2em;
color: dimgray;
}</code></pre>
<p>设置面值的文字样式:</p>
<pre><code class="css">.text .face-value {
position: absolute;
font-size: 14em;
right: 0;
line-height: 0.9em;
color: darkcyan;
}</code></pre>
<p>大功告成!</p>
前端每日实战:141# 视频演示如何用 CSS 的 Grid 布局创作一枚小狗邮票
https://segmentfault.com/a/1190000016506733
2018-09-25T10:56:31+08:00
2018-09-25T10:56:31+08:00
comehope
https://segmentfault.com/u/comehope
15
<p><img src="/img/bVbhqjK?w=400&h=300" alt="图片描述" title="图片描述"></p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/BOeEYV">https://codepen.io/comehope/pen/BOeEYV</a></p>
<h2>可交互视频</h2>
<p>此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。</p>
<p>请用 chrome, safari, edge 打开观看。</p>
<p><a href="https://link.segmentfault.com/?enc=8sMO%2FzSVWZOTx2ys0q%2FHCQ%3D%3D.bPqABpZk2ldark3%2FyheAOT2ZJqWjq54eVP%2FWsBSBjkNf2%2Fn9D3qvflab89zEGk9X" rel="nofollow">https://scrimba.com/p/pEgDAM/cPQ3vcq</a></p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=cemdqGSCM5%2FefO32ciezng%3D%3D.jrst2Aok8GzxlHTG7Zw3JTr74gfz%2F6dBk%2FByrPVAeeSqMJKrWPQ6OhjnMXXd6gF6ovtW1C%2FKb0wjtm3DlDJrpg%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<p>定义 dom,容器表示邮票:</p>
<pre><code class="html"><div class="stamp">
</div></code></pre>
<p>居中显示:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: teal;
}</code></pre>
<p>设置容器尺寸:</p>
<pre><code class="css">.stamp {
position: relative;
width: 40.5em;
height: 71em;
font-size: 6px;
padding: 5em;
background-color: white;
}</code></pre>
<p>用重复背景绘制出邮票的齿孔:</p>
<pre><code class="css">.stamp {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.stamp::after,
.stamp::before {
content: '';
width: 100%;
height: 100%;
position: absolute;
background: radial-gradient(circle, teal 50%, transparent 50%),
radial-gradient(circle, teal 50%, transparent 50%);
background-size: 3.5em 3.5em;
}
.stamp::before {
top: 1.5em;
background-repeat: repeat-y;
background-position: -4.5% 0, 104.5% 0;
}
.stamp::after {
left: 1.5em;
background-repeat: repeat-x;
background-position: 0 -2.5%, 0 102.5%;
}</code></pre>
<p>在 html 文件中增加小狗的 dom 元素,子元素分别表示耳朵、头部、眼睛、舌头、身体、尾巴和爪子:</p>
<pre><code class="html"><div class="stamp">
<div class="puppy">
<span class="ear"></span>
<span class="head"></span>
<span class="eyes"></span>
<span class="tongue"></span>
<span class="body"></span>
<span class="tail"></span>
<span class="foot"></span>
</div>
</div></code></pre>
<p>设置 grid 布局的行列尺寸:</p>
<pre><code class="css">.puppy {
display: grid;
grid-template-columns: 10em 22.5em 8em;
grid-template-rows: 21em 12.5em 3.75em 22.5em;
background-color: tan;
padding: 2em;
margin-top: -1em;
}</code></pre>
<p>画出小狗的头部,跨第1列和第2列、第2行和第3行,是一个半圆形:</p>
<pre><code class="css">.head {
grid-column: 1 / 3;
grid-row: 2 / 4;
border-bottom-left-radius: calc(12.5em + 3.75em);
border-bottom-right-radius: calc(12.5em + 3.75em);
background-color: bisque;
}</code></pre>
<p>用伪元素画出鼻子,是一个扇形,多余的部分被隐藏了:</p>
<pre><code class="css">.head {
position: relative;
overflow: hidden;
}
.head::before {
content: '';
position: absolute;
width: 7em;
height: 7em;
border-bottom-right-radius: 100%;
background-color: sienna;
}</code></pre>
<p>画出半圆形的眼晕:</p>
<pre><code class="css">.eyes {
grid-column: 2;
grid-row: 2;
justify-self: end;
position: relative;
height: 10.5em;
width: 21em;
border-radius: 0 0 10.5em 10.5em;
background-color: sienna;
}</code></pre>
<p>用径向渐变画出眼珠:</p>
<pre><code class="css">.eyes {
background-image: radial-gradient(
circle at 37% 33%,
black 1.4em,
transparent 1.4em
);
}</code></pre>
<p>画出半圆形的耳朵:</p>
<pre><code class="css">.ear {
grid-column: 2;
grid-row: 1;
justify-self: end;
width: 10.5em;
border-radius: 21em 0 0 21em;
background-color: sienna;
}</code></pre>
<p>画出扇形的舌头:</p>
<pre><code class="css">.tongue {
grid-column: 1;
grid-row: 3;
width: 5.5em;
height: 5.5em;
background-color: indianred;
border-bottom-left-radius: 100%;
}</code></pre>
<p>画出扇形的身体:</p>
<pre><code class="css">.body {
grid-column: 2;
grid-row: 4;
background-color: sienna;
border-top-left-radius: 100%;
}</code></pre>
<p>用伪元素,通过阴影画出中蹲着的腿:</p>
<pre><code class="css">.body {
position: relative;
overflow: hidden;
}
.body::after {
content: '';
position: absolute;
height: 50%;
width: 100%;
border-radius: 11.25em 11.25em 0 0;
box-shadow: 2em 0 4em rgba(0, 0, 0, 0.3);
bottom: 0;
}</code></pre>
<p>画出半圆形的尾巴:</p>
<pre><code class="css">.tail {
grid-column: 1;
grid-row: 4;
justify-self: end;
align-self: end;
height: 17.5em;
width: 8.75em;
background-color: bisque;
border-radius: 17.5em 0 0 17.5em;
}</code></pre>
<p>画出半圆形的小爪子:</p>
<pre><code class="css">.foot {
grid-column: 3;
grid-row: 4;
align-self: end;
height: 4em;
background-color: bisque;
border-radius: 4em 4em 0 0;
}</code></pre>
<p>在 dom 中再增加一些文本,包括标题、作者和面值:</p>
<pre><code class="html"><div class="stamp">
<div class="puppy">
<!-- 略 -->
</div>
<p class="text">
<span class="title">Puppy</span>
<span class="author">comehope</span>
<span class="face-value">80</span>
</p>
</div></code></pre>
<p>设置标题的文字样式:</p>
<pre><code class="css">.text {
position: relative;
width: calc(100% + 2em * 2);
height: 6em;
font-family: sans-serif;
}
.text .title {
position: absolute;
font-size: 6em;
font-weight: bold;
color: sienna;
}</code></pre>
<p>设置作者的文字样式:</p>
<pre><code class="css">.text .author {
position: absolute;
font-size: 3em;
bottom: -1.2em;
color: dimgray;
}</code></pre>
<p>设置面值的文字样式:</p>
<pre><code class="css">.text .face-value {
position: absolute;
font-size: 14em;
right: 0;
line-height: 0.9em;
color: darkcyan;
}</code></pre>
<p>大功告成!</p>
前端每日实战:140# 视频演示如何用纯 CSS 创作文本的淡入动画效果
https://segmentfault.com/a/1190000016478152
2018-09-21T08:40:36+08:00
2018-09-21T08:40:36+08:00
comehope
https://segmentfault.com/u/comehope
19
<p><img src="/img/bVbhiSN?w=400&h=301" alt="图片描述" title="图片描述"></p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/ZMwgqK">https://codepen.io/comehope/pen/ZMwgqK</a></p>
<h2>可交互视频</h2>
<p>此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。</p>
<p>请用 chrome, safari, edge 打开观看。</p>
<p><a href="https://link.segmentfault.com/?enc=a3Oft7iHPuXEFbsoFBxLNQ%3D%3D.Id%2BPV70XjMeWQkjAF167iQ%2F2mRzskrEu%2FoRgFRHXHKXZb9N9amRjRmmOqIMsq0x3" rel="nofollow">https://scrimba.com/p/pEgDAM/cJB3rAN</a></p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=2sEq%2BXbvvvBidnervae7sw%3D%3D.q5yB7A%2FTR1sIQItCiAwiEacO0E5giT46P50sQ5U6x3fTpHDqzBxAaHRxqOIKfw%2Fc2Z3Z%2BCxME9ltWZpDS%2FaYTQ%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<p>定义 dom,容器中包含若干子元素,每个子元素是 1 个字母:</p>
<pre><code class="html"><div class="container">
<span>h</span>
<span>a</span>
<span>p</span>
<span>p</span>
<span>y</span>
<span>&nbsp;</span>
<span>h</span>
<span>o</span>
<span>l</span>
<span>i</span>
<span>d</span>
<span>a</span>
<span>y</span>
<span>s</span>
</div></code></pre>
<p>居中显示:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(pink, white, pink);
}</code></pre>
<p>设置字体样式:</p>
<pre><code class="css">.container span {
display: inline-block;
color: purple;
font-weight: bold;
text-transform: uppercase;
font-size: 40px;
}</code></pre>
<p>定义文字从左到右的移动效果:</p>
<pre><code class="css">.container span {
animation: sideSlide 4s forwards infinite;
transform: translateX(-100vw);
}
@keyframes sideSlide {
15%, 20% {
transform: translateX(0.5em);
}
24% {
transform: translateX(0);
}
25%, 75% {
transform: translateX(0);
}
90%, 100% {
transform: translateX(100vw);
}
}</code></pre>
<p>增加文字缩放的动画效果:</p>
<pre><code class="css">.container span {
transform: translateX(-100vw) scale(0);
}
@keyframes sideSlide {
15%, 20% {
transform: translateX(0.5em) scale(1);
}
24% {
transform: translateX(0) scale(1.2);
}
25%, 75% {
transform: translateX(0) scale(1);
}
90%, 100% {
transform: translateX(100vw) scale(0);
}
}</code></pre>
<p>增加文字入场和出场时的淡入淡出效果:</p>
<pre><code class="css">.container span {
filter: opacity(0);
}
@keyframes sideSlide {
15%, 20% {
transform: translateX(0.5em) scale(1);
}
24% {
transform: translateX(0) scale(1.2);
}
25%, 75% {
transform: translateX(0) scale(1);
filter: opacity(1);
}
90%, 100% {
transform: translateX(100vw) scale(0);
filter: opacity(0);
}
}</code></pre>
<p>增加文字变色的效果:</p>
<pre><code class="css">@keyframes sideSlide {
15%, 20% {
transform: translateX(0.5em) scale(1);
color: purple;
}
24% {
transform: translateX(0) scale(1.2);
color: cyan;
}
25%, 75% {
transform: translateX(0) scale(1);
filter: opacity(1);
color: purple;
}
90%, 100% {
transform: translateX(100vw) scale(0);
filter: opacity(0);
}
}</code></pre>
<p>设置子元素的下标变量:</p>
<pre><code class="css">.container span:nth-child(1) { --n: 1; }
.container span:nth-child(2) { --n: 2; }
.container span:nth-child(3) { --n: 3; }
.container span:nth-child(4) { --n: 4; }
.container span:nth-child(5) { --n: 5; }
.container span:nth-child(6) { --n: 6; }
.container span:nth-child(7) { --n: 7; }
.container span:nth-child(8) { --n: 8; }
.container span:nth-child(9) { --n: 9; }
.container span:nth-child(10) { --n: 10; }
.container span:nth-child(11) { --n: 11; }
.container span:nth-child(12) { --n: 12; }
.container span:nth-child(13) { --n: 13; }
.container span:nth-child(14) { --n: 14; }</code></pre>
<p>设置子元素的动画延时:</p>
<pre><code class="css">.container span {
animation-delay: calc((var(--n) - 1) * 0.05s);
}</code></pre>
<p>大功告成!</p>
前端每日实战:139# 视频演示如何用 CSS 和 D3 创作光斑粒子交相辉映的动画
https://segmentfault.com/a/1190000016462519
2018-09-20T07:17:12+08:00
2018-09-20T07:17:12+08:00
comehope
https://segmentfault.com/u/comehope
21
<p><img src="/img/bVbheOE?w=400&h=305" alt="图片描述" title="图片描述"></p>
<h3>效果预览</h3>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/zJybdq">https://codepen.io/comehope/pen/zJybdq</a></p>
<h3>可交互视频</h3>
<p>此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。</p>
<p>请用 chrome, safari, edge 打开观看。</p>
<p><a href="https://link.segmentfault.com/?enc=YdmAuw8aLldnAk4LsAqqBg%3D%3D.kL5rTGazE%2Be0OFPkPovGoWnIsFSeoh2JB0cc%2BRMfjYJzElGVHbEhK6Tep%2BjBcNy8" rel="nofollow">https://scrimba.com/p/pEgDAM/cGV7phy</a></p>
<h3>源代码下载</h3>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=pzlwSxkz%2FHO8NhXc4OzwPw%3D%3D.aF8WRqTQpjFDtV3ofuGiX5PP3Kbq8qeDtKbF9UuqQQ5KDKgxy7jSeLYeqg9qZWUmyU%2FLMx9QuyIkiQp%2BwzSpYA%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<p>定义 dom,容器中包含 3 个子元素:</p>
<pre><code class="html"><div class='container'>
<span></span>
<span></span>
<span></span>
</div></code></pre>
<p>设置页面背景:</p>
<pre><code class="css">body {
margin: 0;
width: 100vw;
height: 100vh;
background: radial-gradient(circle at center, #222, black 20%);
}</code></pre>
<p>定义容器尺寸:</p>
<pre><code class="css">.container {
width: 100%;
height: 100%;
}</code></pre>
<p>设置光斑的样式,其中定义了偏亮和偏暗的 2 个颜色变量:</p>
<pre><code class="css">.container {
position: relative;
}
.container span {
--bright-color: #d4ff00;
--dark-color: #e1ff4d;
position: absolute;
width: 30px;
height: 30px;
margin-left: -15px;
margin-top: -15px;
background: radial-gradient(var(--bright-color), var(--dark-color));
border-radius: 50%;
box-shadow: 0 0 25px 3px var(--dark-color);
}</code></pre>
<p>把光斑定位到页面中心:</p>
<pre><code class="css">.container span {
transform: translateX(50vw) translateY(50vh);
}</code></pre>
<p>增加光斑从中心向四周扩散和收缩的动画效果:</p>
<pre><code class="css">.container span {
animation: animate 1.5s infinite alternate;
animation-delay: calc(var(--n) * 0.015s);
}
@keyframes animate {
80% {
filter: opacity(1);
}
100% {
transform: translateX(calc(var(--x) * 1vw)) translateY(calc(var(--y) * 1vh));
filter: opacity(0);
}
}</code></pre>
<p>定义动画中用到的变量 <code>--x</code>、<code>--y</code> 和 <code>--n</code>:</p>
<pre><code class="css">.container span:nth-child(1) {
--x: 20;
--y: 30;
--n: 1;
}
.container span:nth-child(2) {
--x: 60;
--y: 80;
--n: 2;
}
.container span:nth-child(3) {
--x: 10;
--y: 90;
--n: 3;
}</code></pre>
<p>设置容器的景深,使光斑的运动有从远到近的感觉:</p>
<pre><code class="css">.container {
perspective: 500px;
}
.container span {
transform: translateX(50vw) translateY(50vh) translateZ(-1000px);
}</code></pre>
<p>至此,少量元素的动画效果完成,接下来用 d3 批量创建 dom 元素和 css 变量。<br>引入 d3 库,同时删除 html 文件中的子元素和 css 文件中的子元素变量:</p>
<pre><code class="html"><script src="https://d3js.org/d3.v5.min.js"></script></code></pre>
<p>定义光斑粒子数量:</p>
<pre><code class="javascript">const COUNT = 3;</code></pre>
<p>批量创建 dom 元素:</p>
<pre><code class="javascript">d3.select('.container')
.selectAll('span')
.data(d3.range(COUNT))
.enter()
.append('span');</code></pre>
<p>为 dom 元素设置 <code>--x</code>、<code>--y</code> 和 <code>--n</code> 的值,其中 <code>--x</code> 和 <code>--y</code> 是 1 到 99 的随机数:</p>
<pre><code class="javascript">d3.select('.container')
/* 略 */
.style('--x', () => d3.randomUniform(1, 99)())
.style('--y', () => d3.randomUniform(1, 99)())
.style('--n', d => d);</code></pre>
<p>再为 dom 元素设置 <code>--bright-color</code> 和 <code>--dark-color</code> 的值:</p>
<pre><code class="javascript">d3.select('.container')
/* 略 */
.style('--dark-color', (d) => d3.color(`hsl(${70 + d * 0.1}, 100%, 50%)`))
.style('--bright-color', (d) => d3.color(`hsl(${70 + d * 0.1}, 100%, 50%)`).brighter(0.15));</code></pre>
<p>最后,把光斑粒子数量设置为 200 个:</p>
<pre><code class="css">const COUNT = 200;</code></pre>
<p>大功告成!</p>
前端每日实战:138# 视频演示如何用纯 CSS 创作一张 iPhone 价格信息图
https://segmentfault.com/a/1190000016456282
2018-09-19T16:03:45+08:00
2018-09-19T16:03:45+08:00
comehope
https://segmentfault.com/u/comehope
10
<p><img src="/img/bVbhdbh?w=400&h=300" alt="图片描述" title="图片描述"></p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/OorLGZ">https://codepen.io/comehope/pen/OorLGZ</a></p>
<h2>可交互视频</h2>
<p>此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。</p>
<p>请用 chrome, safari, edge 打开观看。</p>
<p><a href="https://link.segmentfault.com/?enc=apgduDRhNQ5CAyaZ7dVy0g%3D%3D.OgWnI7XMkyYEJTXd5Il6lf41Fn4uLero13muFAMIGd8RAJcsF%2F%2FuVb2aqiEHbiXf" rel="nofollow">https://scrimba.com/p/pEgDAM/cRB22cV</a></p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=J%2Bn9gORIre0Djb0w4sAw9Q%3D%3D.URAQ5hwpS03wUy0dsomg4PA3X47FRV%2BJ576N%2FtHag6dFbq9zFkjQrNZ7lp6D6U99ngkUB4cX7rlbM1C42CW8wg%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<p>定义 dom,容器中包含 3 个元素,<code>h1</code> 是图表标题,<code>.back</code> 表示背景墙,<code>.side</code> 表示侧边墙,<code>.back</code> 和 <code>.side</code> 中都包含一个无序列表,背景墙展示价格,侧边墙展示名称:</p>
<pre><code class="html"><div class="wall">
<h1>iPhone Price Comparison</h1>
<div class="back">
<ul>
<li class="xs-max"><span>$1099 ~ $1449</span></li>
<li class="xs"><span>$999 ~ $1349</span></li>
<li class="xr"><span>$749 ~ $899</span></li>
<li class="x"><span>$999 ~ $1149</span></li>
</ul>
</div>
<div class="side">
<ul>
<li class="xs-max">iPhone XS Max</li>
<li class="xs">iPhone XS</li>
<li class="xr">iPhone XR</li>
<li class="x">iPhone X</li>
</ul>
</div>
</div></code></pre>
<p>居中显示:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(lightblue, skyblue);
}</code></pre>
<p>定义容器尺寸:</p>
<pre><code class="css">.wall {
width: 60em;
height: 40em;
border: 1em solid rgba(255, 255, 255, 0.5);
border-radius: 2em;
font-size: 10px;
}</code></pre>
<p>用 grid 布局,把容器分成 2 部分,左侧80%为背景墙,右侧20%为侧边墙:</p>
<pre><code class="css">.wall {
display: grid;
grid-template-columns: 0 4fr 1fr;
}</code></pre>
<p>分别设置背景墙和侧边墙的背景色:</p>
<pre><code class="css">.back {
background: linear-gradient(
to right,
#555,
#ddd
);
}
.side {
background:
radial-gradient(
at 0% 50%,
/* tomato 25%,
yellow 90% */
rgba(0, 0, 0, 0.2) 25%,
rgba(0, 0, 0, 0) 90%
),
linear-gradient(
to right,
#ddd,
#ccc
)
}</code></pre>
<p>用 flex 布局设置对齐方式,列表垂直居中:</p>
<pre><code class="css">.back,
.side {
display: flex;
align-items: center;
}
.back {
justify-content: flex-end;
}
ul {
list-style-type: none;
padding: 0;
}</code></pre>
<p>设置标题样式:</p>
<pre><code class="css">h1 {
position: relative;
width: 20em;
margin: 1em;
color: white;
font-family: sans-serif;
}</code></pre>
<p>设置列表项的高度和颜色:</p>
<pre><code class="css">.back ul {
width: 75%;
}
.side ul {
width: 100%;
}
ul li {
height: 5em;
background-color: var(--c);
}
ul li:nth-child(1) {
--c: tomato;
}
ul li:nth-child(2) {
--c: coral;
}
ul li:nth-child(3) {
--c: lightsalmon;
}
ul li:nth-child(4) {
--c: deepskyblue;
}</code></pre>
<p>至此,整体布局完成。接下来设置左侧背景墙的横条样式。<br>横条的宽度根据与商品的上限售价 <code>--high-price</code> 成正比,以最贵的售价 <code>--max-price</code> 作为全长,其他横条的宽度为上限售价与最高售价的百分比:</p>
<pre><code class="css">ul {
display: flex;
flex-direction: column;
}
.back ul {
align-items: flex-end;
}
ul {
--max-price: 1449;
}
ul li.xs-max {
--high-price: 1449;
}
ul li.xs {
--high-price: 1349;
}
ul li.xr {
--high-price: 899;
}
ul li.x {
--high-price: 1149;
}
.back ul li {
width: calc(var(--high-price) / var(--max-price) * 100%);
}</code></pre>
<p>在横条中区分起售价 <code>--low-price</code> 的位置,比起售价高的区域填充更深的颜色:</p>
<pre><code class="css">ul li.xs-max {
--low-price: 1099;
--c2: orangered;
}
ul li.xs {
--low-price: 999;
--c2: tomato;
}
ul li.xr {
--low-price: 749;
--c2: coral;
}
ul li.x {
--low-price: 999;
--c2: dodgerblue;
}
.back ul li {
--percent: calc(var(--low-price) / var(--high-price) * 100%);
background: linear-gradient(
to left,
var(--c) var(--percent),
var(--c2) var(--percent)
);
}</code></pre>
<p>在横线的顶端画出一个向左的三角形:</p>
<pre><code class="css">.back ul li {
position: relative;
}
.back ul li::before {
content: '';
position: absolute;
width: 0;
height: 0;
transform: translateX(-3em);
border-right: 3em solid var(--c2);
border-top: 2.5em solid transparent;
border-bottom: 2.5em solid transparent;
}</code></pre>
<p>设置价格文字样式:</p>
<pre><code class="css">.back ul li span {
position: absolute;
width: 95%;
text-align: right;
color: white;
font-size: 1.25em;
line-height: 4em;
font-family: sans-serif;
}</code></pre>
<p>为各横条增加阴影,增强立体感:</p>
<pre><code class="css">ul li.xs-max {
z-index: 5;
}
ul li.xs {
z-index: 4;
}
ul li.xr {
z-index: 3;
}
ul li.x {
z-index: 2;
}
.back ul li {
filter: drop-shadow(0 1em 1em rgba(0, 0, 0, 0.3));
}</code></pre>
<p>至此,背景墙的横条完成。接下来设置侧边墙的样式。<br>为了制造立体效果,需要设置侧边墙的景深,并使列表倾斜:</p>
<pre><code class="css">.side {
perspective: 1000px;
}
.side ul {
transform-origin: left;
transform: rotateY(-75deg) scaleX(4);
}</code></pre>
<p>设置侧边墙的文字样式:</p>
<pre><code class="css">.wall {
overflow: hidden;
}
.side ul li {
padding-right: 30%;
text-align: right;
color: white;
font-family: sans-serif;
line-height: 5em;
}</code></pre>
<p>至此,静态视觉效果完成。最后增加入场动画效果:</p>
<pre><code class="css">ul li {
animation: show 1s linear forwards;
transform-origin: right;
transform: scaleX(0);
}
@keyframes show {
to {
transform: scaleX(1);
}
}
.back ul li {
animation-delay: 1s;
}</code></pre>
<p>大功告成!</p>
前端每日实战:137# 视频演示如何用纯 CSS 创作一个抽象的水波荡漾的动画
https://segmentfault.com/a/1190000016419507
2018-09-17T08:25:09+08:00
2018-09-17T08:25:09+08:00
comehope
https://segmentfault.com/u/comehope
19
<p><img src="/img/bVbg3CU?w=400&h=301" alt="图片描述" title="图片描述"></p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/MqqqwG">https://codepen.io/comehope/pen/MqqqwG</a></p>
<h2>可交互视频</h2>
<p>此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。</p>
<p>请用 chrome, safari, edge 打开观看。</p>
<p><a href="https://link.segmentfault.com/?enc=SPJCKgojpGzD43L7JN7dBg%3D%3D.XMw0lZPDUOcbMPGGUvLFMA1vOsGDZl0g441QxmZfHOIJ6I6CKKq9AOiRkIJKtg5I" rel="nofollow">https://scrimba.com/p/pEgDAM/cJBwwHn</a></p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=xJpr9p1rlyobWny2Boilww%3D%3D.xVNvcCjLKSObvpzyX9MQdEL83PfA6gzXvXBTmhgMW0Wn9NjlvxmRHKVf6Ze7cj85qPJN3z59U2d%2F0OY1WiVEaA%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<p>定义 dom,容器中包含 9 个元素:</p>
<pre><code class="html"><div class="container">
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div></code></pre>
<p>居中显示:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: black;
}</code></pre>
<p>定义容器尺寸:</p>
<pre><code class="css">.container {
width: 30em;
height: 30em;
font-size: 10px;
}</code></pre>
<p>用 grid 布局把 9 个子元素排列成 3 * 3 的网格状:</p>
<pre><code class="css">.container {
display: grid;
grid-template-columns: repeat(3, 1fr);
}</code></pre>
<p>设置容器内子元素的样式,是通过伪元素来设置的:</p>
<pre><code class="css">.container span {
position: relative;
}
.container span::before,
.container span::after
{
content: '';
position: absolute;
box-sizing: border-box;
border-style: none solid solid none;
border-width: 1em;
border-color: gold;
width: 100%;
height: 100%;
}</code></pre>
<p>旋转容器,让它的尖端朝上:</p>
<pre><code class="css">.container {
transform: rotate(-135deg);
}</code></pre>
<p>增加子元素尺寸由小到大变化的动画:</p>
<pre><code class="css">.container span::before,
.container span::after
{
animation:
animate-scale 1.6s linear infinite;
}
@keyframes animate-scale {
from {
width: 1%;
height: 1%;
}
to {
width: 100%;
height: 100%;
}
}</code></pre>
<p>增加子元素边框色变化的动画:</p>
<pre><code class="css">.container span::before,
.container span::after
{
animation:
animate-border-color 1.6s linear infinite,
animate-scale 1.6s linear infinite;
}
@keyframes animate-border-color {
0%, 25% {
border-color: tomato;
}
50%, 75% {
border-color: gold;
}
100% {
border-color: black;
}
}</code></pre>
<p>增加子元素边框宽度变化的动画:</p>
<pre><code class="css">.container span::before,
.container span::after
{
animation:
animate-border-width 1.6s linear infinite,
animate-border-color 1.6s linear infinite,
animate-scale 1.6s linear infinite;
}</code></pre>
<p>最后,让 <code>::after</code> 伪元素的动画时间慢半拍:</p>
<pre><code class="css">.container span::after {
animation-delay: -0.8s;
}
@keyframes animate-border-width {
0%, 100%{
border-width: 0.1em;
}
25% {
border-width: 1.5em;
}
}</code></pre>
<p>大功告成!</p>
前端每日实战:136# 视频演示如何用 D3 和 GSAP 创作一个横条 loader
https://segmentfault.com/a/1190000016406581
2018-09-15T07:05:30+08:00
2018-09-15T07:05:30+08:00
comehope
https://segmentfault.com/u/comehope
7
<p><img src="/img/bVbg0gq?w=400&h=305" alt="图片描述" title="图片描述"></p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/pOZKWJ">https://codepen.io/comehope/pen/pOZKWJ</a></p>
<h2>可交互视频</h2>
<p>此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。</p>
<p>请用 chrome, safari, edge 打开观看。</p>
<p><a href="https://link.segmentfault.com/?enc=CgvWfaeltoSnV1iaTr6Ajg%3D%3D.FelJhuI3bFdRja6XuxC1CWfuta6neyO5KqPPzrZToTnvuLHr4Hw8ZjDVAITCwlnG" rel="nofollow">https://scrimba.com/p/pEgDAM/cVB48Ur</a></p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=KqYKEjEQnm0rBoMhz%2B3oRg%3D%3D.1e%2FD9tGTm1%2FHS2J7t1UHWDk%2Faqp9mPdmyQP2R%2BNwCzqVfUCvJMNsPyAhKlSL8bCQgc1YzPMtlPIyApo6J1%2F5iA%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<p>定义 dom,容器中包含 1 个 <code>span</code> 元素:</p>
<pre><code class="html"><div class="loader">
<span></span>
</div></code></pre>
<p>居中显示:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: black;
}</code></pre>
<p>定义容器尺寸:</p>
<pre><code class="css">.loader {
width: 40em;
height: 1em;
font-size: 10px;
}</code></pre>
<p>设置容器中 <code>span</code> 的样式,是一个彩色细长条:</p>
<pre><code class="css">.loader {
position: relative;
}
.loader span {
position: absolute;
width: inherit;
height: inherit;
background-color: hsl(24, 100%, 65%);
}</code></pre>
<p>引入 d3.js:</p>
<pre><code class="html"><script src="https://d3js.org/d3.v5.min.js"></script></code></pre>
<p>删除掉 dom 中的 <code>span</code> 元素,改用 d3 创建,其中常量 <code>COUNT</code> 是细长条的数量,因为 <code>d3.range()</code> 生成的数据是从 0 开始的数列,所以把数据修正为以日常习惯的从 1 开始的数列:</p>
<pre><code class="javascript">const COUNT = 1;
d3.select('.loader')
.selectAll('span')
.data(d3.range(COUNT).map(d => d + 1))
.enter()
.append('span')</code></pre>
<p>删除掉 css 中设置 <code>span</code> 元素 <code>background-color</code> 属性的代码,改用 d3 设置:</p>
<pre><code class="javascript">d3.select('.loader')
.selectAll('span')
.data(d3.range(COUNT).map(d => d + 1))
.enter()
.append('span')
.style('background-color', `hsl(24, 100%, 65%)`)</code></pre>
<p>把细长条的数量改为 2 个,颜色改为动态生成:</p>
<pre><code class="javascript">const HUE_DEG = 12;
const COUNT = 2;
d3.select('.loader')
/* ...略 */
.style('background-color', (d) => `hsl(${d * HUE_DEG}, 100%, 65%)`)</code></pre>
<p>再进一步,把颜色改为彩色条和黑色条间隔出现,请注意虽然在表达式中 hue 的值是按 12 递增,但实际看到的的效果是彩色长条间的 hue 相差 24,因为其中夹杂着黑色长条:</p>
<pre><code class="javascript">d3.select('.loader')
/* ...略 */
.style('background-color', (d) => d % 2 !== 0
? `hsl(${d * HUE_DEG}, 100%, 65%)`
: 'black');</code></pre>
<p>此时,动态生成的 dom 结构为:</p>
<pre><code class="html"><div class="container">
<span style="background-color: rgb(255, 77, 77);"></span>
<span style="background-color: black;">
</div></code></pre>
<p>引入 gsap 库:</p>
<pre><code class="html"><script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/2.0.2/TweenMax.min.js"></script></code></pre>
<p>增加长条由中心向两侧伸展的动画效果:</p>
<pre><code class="javascript">let animation = new TimelineMax({repeat: -1});
animation.staggerFrom('.loader span', 0.5, {scaleX: 0}, 0.4)</code></pre>
<p>最后,把长条的数量改为 30 个,它是用整圈色相环的 360 度除以 hue 间隔得到的:</p>
<pre><code class="javascript">const COUNT = 360 / HUE_DEG;</code></pre>
<p>大功告成!</p>
前端每日实战:135# 视频演示如何用纯 CSS 创作一个悬停时右移的按钮特效
https://segmentfault.com/a/1190000016390037
2018-09-13T20:39:49+08:00
2018-09-13T20:39:49+08:00
comehope
https://segmentfault.com/u/comehope
34
<p><img src="/img/bVbgVXz?w=400&h=302" alt="图片描述" title="图片描述"></p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/PdaNXw">https://codepen.io/comehope/pen/PdaNXw</a></p>
<h2>可交互视频</h2>
<p>此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。</p>
<p>请用 chrome, safari, edge 打开观看。</p>
<p><a href="https://link.segmentfault.com/?enc=aio3ar6Uj%2BkhS7JhkrUrOQ%3D%3D.Gb0RaBKuK9KhSHjAxKvVynYmi8O3FP8KIjSs2lGVBUrjgmDbVTfichXmvhzBisWu" rel="nofollow">https://scrimba.com/p/pEgDAM/c3MV9Sa</a></p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=A6gc5KZDVPrkfCT3Pzwhow%3D%3D.01Eny%2FH9PPZrtCl%2FQ0B6wBqWrCob8Kiytby1kMCRiSIMWi8%2FuywFv2%2FwntnplkzCHLSx9dFiqrz0Jeo3QASMnA%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<p>定义 dom,导航中包含一个无序列表,列表项中内嵌一个 <code>span</code>,文字写在 <code>span</code> 中:</p>
<pre><code class="html"><nav>
<ul>
<li><span>home</span></li>
</ul>
</nav></code></pre>
<p>居中显示:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: #333;
}</code></pre>
<p>隐藏列表项前端的引导符号:</p>
<pre><code class="css">nav ul {
padding: 0;
list-style-type: none;
}</code></pre>
<p>设置按钮的尺寸和颜色:</p>
<pre><code class="css">nav li {
width: 8em;
height: 2em;
font-size: 25px;
color: orange;
}</code></pre>
<p>设置文字样式,注意高度是 <code>120%</code>,<code>span</code> 比它父级的 <code>li</code> 要高一些:</p>
<pre><code class="css">nav li span {
position: relative;
box-sizing: border-box;
width: inherit;
height: 120%;
top: -10%;
background-color: #333;
border: 2px solid;
font-family: sans-serif;
text-transform: capitalize;
display: flex;
align-items: center;
justify-content: center;
}</code></pre>
<p>将 <code>span</code> 元素稍向右移:</p>
<pre><code class="css">nav li span {
transform: translateX(4px);
}</code></pre>
<p>用列表项 <code>li</code> 的左边框画出 1 条竖线:</p>
<pre><code class="css">nav li {
box-sizing: border-box;
border-left: 2px solid;
}</code></pre>
<p>用列表项的伪元素再画出 2 条竖线,它们的高度依次降低,至此,按钮左侧一共有 3 条竖线:</p>
<pre><code class="css">nav li {
position: relative;
}
nav li::before,
nav li::after
{
content: '';
position: absolute;
width: inherit;
border-left: 2px solid;
z-index: -1;
}
nav li::before {
height: 80%;
top: 10%;
left: -8px;
}
nav li::after {
height: 60%;
top: 20%;
left: -14px;
}</code></pre>
<p>将伪元素的 2 条竖线的颜色逐渐变暗,增加一点层次感:</p>
<pre><code class="css">nav li::before {
filter: brightness(0.8);
}
nav li::after {
filter: brightness(0.6);
}</code></pre>
<p>增加鼠标悬停效果,默认状态是按钮遮住 3 条竖线,当鼠标悬停时,按钮右移,露出 3 条竖线:</p>
<pre><code class="css">nav li:hover span {
transform: translateX(4px);
}
nav li span {
/* transform: translateX(4px); */
transform: translateX(-16px);
transition: 0.3s;
}</code></pre>
<p>因为按钮默认状态的位置是偏左的,为了抵销这个偏移量,让列表项稍向右移:</p>
<pre><code class="css">nav ul {
transform: translateX(16px);
}</code></pre>
<p>在 dom 中再增加几个按钮:</p>
<pre><code class="html"><nav>
<ul>
<li><span>home</span></li>
<li><span>products</span></li>
<li><span>services</span></li>
<li><span>contact</span></li>
</ul>
</nav></code></pre>
<p>设置一下按钮的间距:</p>
<pre><code class="css">nav li {
margin-top: 0.8em;
}</code></pre>
<p>大功告成!</p>
前端每日实战:134# 视频演示如何用 CSS 和 GSAP 创作一个树枝发芽的 loader
https://segmentfault.com/a/1190000016377676
2018-09-13T06:10:40+08:00
2018-09-13T06:10:40+08:00
comehope
https://segmentfault.com/u/comehope
13
<p><img src="/img/bVbgSKa?w=400&h=302" alt="图片描述" title="图片描述"></p>
<h2>效果预览</h2>
<p>按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。</p>
<p><a href="https://codepen.io/comehope/pen/LJmpXZ">https://codepen.io/comehope/pen/LJmpXZ</a></p>
<h2>可交互视频</h2>
<p>此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。</p>
<p>请用 chrome, safari, edge 打开观看。</p>
<p><a href="https://link.segmentfault.com/?enc=F8Vg%2F9ucS5ppIyCrDgH4TQ%3D%3D.oVfj%2B%2BqLsBMCjlp%2B%2BTBYT3LwhXyLcJZbyY3CvRsexj6hVJd%2B4b0Enycpx%2BguaHH1" rel="nofollow">https://scrimba.com/p/pEgDAM/cdD8WHV</a></p>
<h2>源代码下载</h2>
<p>每日前端实战系列的全部源代码请从 github 下载:</p>
<p><a href="https://link.segmentfault.com/?enc=TV2W6GAYHElyk0LPFS%2FGOw%3D%3D.Ja%2BWCQ3HdkbeRIDmgraqp%2B7GsskLqe2vk2POob0NXtiBL6r7oi6dudzgwwNdp9nwjHZ9r42GIkAB2fMixqqGPg%3D%3D" rel="nofollow">https://github.com/comehope/front-end-daily-challenges</a></p>
<h2>代码解读</h2>
<p>定义 dom,容器包含 2 个元素,<code>branch</code> 代表枝,<code>leaves</code> 代表叶,叶有 6 个子元素,代表 6 个叶片:</p>
<pre><code class="html"><figure class="sapling">
<div class="branch"></div>
<div class="leaves">
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
</figure></code></pre>
<p>居中显示:</p>
<pre><code class="css">body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: black;
}</code></pre>
<p>定义容器尺寸,并设置子元素水平居中:</p>
<pre><code class="css">.sapling {
position: relative;
width: 5em;
height: 17.5em;
font-size: 10px;
display: flex;
justify-content: center;
}</code></pre>
<p>画出树枝:</p>
<pre><code class="css">.branch {
position: absolute;
width: 0.2em;
height: inherit;
border-radius: 25%;
background: burlywood;
}</code></pre>
<p>定义树叶容器,设置为叶片在垂直方向均匀分布,并且从下到上排列:</p>
<pre><code class="css">.leaves {
position: absolute;
width: inherit;
height: 15em;
top: 1em;
display: flex;
flex-direction: column-reverse;
}</code></pre>
<p>设置叶片的尺寸和和背景颜色:</p>
<pre><code class="css">.leaves span {
width: 2.5em;
height: 2.5em;
background-color: limegreen;
}</code></pre>
<p>设置左右叶片的各自样式:</p>
<pre><code class="css">.leaves span:nth-child(odd) {
border-bottom-left-radius: 3em;
border-top-right-radius: 3em;
transform-origin: right bottom;
align-self: flex-start;
}
.leaves span:nth-child(even) {
border-bottom-right-radius: 3em;
border-top-left-radius: 3em;
transform-origin: left bottom;
align-self: flex-end;
}</code></pre>
<p>至此,静态效果绘制完成,接下来开始写动画脚本。<br>引入 GSAP 库:</p>
<pre><code class="html"><script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/2.0.2/TweenMax.min.js"></script></code></pre>
<p>声明一个时间线对象:</p>
<pre><code class="javascript">let animation = new TimelineMax();</code></pre>
<p>增加树枝的入场动画效果,并为这个动画设置一个标签 <code>branch</code>:</p>
<pre><code class="javascript">animation.from('.branch', 4, {scaleY: 0, ease: Power1.easeOut}, 'branch');</code></pre>
<p>增加树叶的入场动画效果,它的参数中有 3 个 <code>0.5</code>,从左到右的含义分别是动画时长、多个叶片动画的间隔时长、相对 <code>branch</code> 标签动画的延迟时间:</p>
<pre><code class="javascript">animation.from('.branch', 4, {scaleY: 0, ease: Power1.easeOut}, 'branch')
.staggerFrom('.leaves span', 0.5, {scale: 0, ease: Power1.easeOut}, 0.5, 0.5, 'branch');</code></pre>
<p>增加叶片变黄的动画效果:</p>
<pre><code class="javascript">animation.from('.branch', 4, {scaleY: 0, ease: Power1.easeOut}, 'branch')
.staggerFrom('.leaves span', 0.5, {scale: 0, ease: Power1.easeOut}, 0.5, 0.5, 'branch')
.to(['.branch', '.leaves span'], 3, {backgroundColor: 'yellow'});</code></pre>
<p>增加淡出效果:</p>
<pre><code class="javascript">animation.from('.branch', 4, {scaleY: 0, ease: Power1.easeOut}, 'branch')
.staggerFrom('.leaves span', 0.5, {scale: 0, ease: Power1.easeOut}, 0.5, 0.5, 'branch')
.to(['.branch', '.leaves span'], 3, {backgroundColor: 'yellow'})
.to(['.branch', '.leaves span'], 1, {autoAlpha: 0});</code></pre>
<p>修改声明时间线的代码,使动画重复播放:</p>
<pre><code class="javascript">let animation = new TimelineMax({repeat: -1, repeatDelay: 0.5});</code></pre>
<p>大功告成!</p>