送給前端工程師的生日禮物
?前言:哈嘍,我是樹醬,今天跟大家分享一篇來自寒草的沙雕文章 并祝他生日快樂。 關(guān)于寒草??的自我介紹:一只草系碼猿??。間歇性熱血??,持續(xù)性沙雕??。
?
前奏 ??
文章概述
本篇文章,大家可以:
與我一同設(shè)計一款生日禮物 ?? 與我攜手一同完成生日禮物 ?? 的開發(fā) 本人生日 ?? 將至的有感而發(fā)
希望大家閱讀本篇文章,可以體驗(yàn)到:
前端的樂趣 制作本禮物用到的css基礎(chǔ)知識 從我的有感而發(fā)中獲得一些經(jīng)驗(yàn)或者感悟
當(dāng)然我畢竟生日 ?? 將至,所以本篇文章十分期待大家的「生日祝福」,哈哈哈,求伙伴們給我點(diǎn)生日的儀式感~
起因:我需要一個生日驚喜!
如果大家了解我的話,我是一個喜歡用技術(shù)制作某些驚喜的程序員。
「8 月 7 日」是我的陽歷生日,沒有女朋友的我也是想要生日驚喜的呀 ??,也是想要一點(diǎn)儀式感的呀,于是我便有了用前端技術(shù)給自己制作一個生日禮物 ?? 的計劃,「正所謂,沒有人給你驚喜,你也要學(xué)會給自己制造驚喜」。
當(dāng)然這個生日禮物 ?? 我已經(jīng)開發(fā)完了,于是這篇文章也應(yīng)運(yùn)而生。
?我曾經(jīng)表示過,我做前端是因?yàn)閷W(xué)習(xí)前端的過程中,前端曾經(jīng)給過我很純粹的快樂與成就感,雖然工作之后前端更加邊成了吃飯的家伙 ??。我相信有很多人和我抱著一樣的想法走入前端的職業(yè)生涯(至少我有幾個同學(xué)和我具有一樣的出發(fā)點(diǎn)),所以我希望我可以用我的努力讓更多人發(fā)現(xiàn)前端是一個有趣的東西??, 我們可以用這個技術(shù)去實(shí)現(xiàn)一些自己的idea~
?
前奏:我該如何設(shè)計一款生日禮物?
開始的我也是很迷茫,我的生日禮物要有哪些要素呢,于是我就要去審問我自己,我喜歡哪些東西,思考良久。關(guān)于我的生日禮物需要哪些要素,我有了答案:
煙花 星空 "生日快樂"這幾個字 仰望星空的我
煙花??
我想大多數(shù)人都會喜歡煙花,大家想象一下無論是動漫還是電影作品,「男孩女孩手拉手走到江畔,或者海邊,吹著夏日的風(fēng),隨著一聲炸裂的聲響,煙花在天空中綻放,女孩望著煙花入了神,男孩望著女孩出了神,五彩斑斕的光映射在女生的眼眸,像極了他們絢爛的未來,多么浪漫」。哦,我沒有女朋友,我秀我自己干嘛呢。。。算了算了,嗚嗚嗚,大家理解這個意境就好。

星空?
其實(shí)我對于星空一直有一個情結(jié),小時候聽大人講,在他們年少的時候,經(jīng)常可以看到漫天的繁星,而我卻沒有見過那樣的場面,抬頭仰望,基本就是寥寥幾顆。我也曾想過夜攀高峰,或駐扎山野,抬頭望一望漫天星河。
大家想象一下這樣的畫面,「男孩挽著女孩的手漫步于星空下,仰首是星光燦爛,俯首是挽著的雙手與鄉(xiāng)間小路,駐足遠(yuǎn)眺,天空與蒼茫大地連接,星河一路延伸直至盡頭,美的失了神,男孩輕聲細(xì)語:“愿你陪我看盡萬年雪飄,路經(jīng)永世繁華,棲身滿也星空?!薄?/strong>,哦,我還是沒有女朋友,我又秀我自己干嘛呢。。。算了算了,嗚嗚嗚,大家理解這個意境就好。

我的簡筆畫:一場煙花??盛宴,上演于星空??之下
煙花,星空,生日快樂,仰望星空的我,四大要素齊了,于是我便開始了我的設(shè)計之旅,我先在我的筆記本上畫了這樣一幅簡筆畫:

我畫的不好,抱歉讓各位看客受驚了。但是通過這個畫大家可以看到,我這個生日禮物 ?? 要素已經(jīng)齊全了,天空中有“生日快樂”幾個大字,閃爍的星星以及絢爛的煙花,地面上有仰望星空的我~
?于是我想到了本作品的主題:「一場煙花的盛宴,將在星空下上演」
?
間奏 ??
現(xiàn)在的構(gòu)想和設(shè)計已經(jīng)比較清晰,那么用本章來將我們的設(shè)計實(shí)現(xiàn)出來吧~ 文章下面的幾個章節(jié)分別對應(yīng)上文設(shè)計中的:
"生日快樂"這幾個字 仰望星空的我 星空 煙花
生日快樂,筆走龍蛇 ??
我想天空飄來四個字,“生日快樂”,感覺也不是很炫酷,所以就有了一個設(shè)想,就是一筆一劃的寫出生日快樂四個字,就像有人在天空中寫下了生日祝福一樣,豈不是很炫酷,很有儀式感。
那么問題來了,我如何去實(shí)現(xiàn)這樣的效果呢,我的第一反應(yīng)是canvas去繪制這幾個字,但是點(diǎn)的坐標(biāo)很難去找,我又很難去弄出好看的字體,于是在我的糾結(jié)之下,我放棄了這個想法。
之后我就去咨詢了大帥老猿


之后大帥老師給我推薦了一個這樣的庫:hanzi-writer, 大家可以去看一看這個文檔docs,完美契合了我的需求,于是說干就干。
「html」
<div class="happy-birthday__container">
<div id="sheng"></div>
<div id="ri"></div>
<div id="kuai"></div>
<div id="le"></div>
<div id="ya"></div>
<div id="site"></div>
<div id="xiao"></div>
<div id="han"></div>
<div id="cao"></div>
</div>
「js」
// 文字效果
const BASE_CONFIG = {
width: 100,
height: 100,
padding: 5,
delayBetweenStrokes: 0,
strokeAnimationSpeed: 1.2,
showCharacter: false,
showOutline: false,
}
const WRITER_CONFIG = {
...BASE_CONFIG,
strokeColor: '#e09037'
};
const NAME_CONFIG = {
...BASE_CONFIG,
strokeColor: '#87db92'
};
const getWriterList = () => {
let writerList = [];
writerList.push(HanziWriter.create('sheng', '生', WRITER_CONFIG));
writerList.push(HanziWriter.create('ri', '日', WRITER_CONFIG));
writerList.push(HanziWriter.create('kuai', '快', WRITER_CONFIG));
writerList.push(HanziWriter.create('le', '樂', WRITER_CONFIG));
writerList.push(HanziWriter.create('ya', '吖', WRITER_CONFIG));
writerList.push(HanziWriter.create('xiao', '小', NAME_CONFIG));
writerList.push(HanziWriter.create('han', '寒', NAME_CONFIG));
writerList.push(HanziWriter.create('cao', '草', NAME_CONFIG));
return writerList;
}
const generateAnimateWriter = async (writerList) => {
const writerCount = writerList.length;
for (const writer of writerList) {
await writer.animateCharacter();
}
// 文字全都顯示完全后消失掉
document.getElementsByClassName('happy-birthday__container')[0].style.opacity = 0;
}
其實(shí)就是官網(wǎng)的用法,我用了七個字,“「生日快樂吖,小寒草」”,生日快樂和小寒草做了分割,樣式我就不帶著大家一起寫了,就是簡單的定位。
「效果如下」:

寒草佇立,仰望星空 ??
下面我們就要去弄出來一個寒草了,大家看我簡筆畫的姿勢并不是我最后想要呈現(xiàn)出來的姿勢,因?yàn)槲疫@個人是特別喜歡海賊王的,海賊王是有一個經(jīng)典的姿勢的。

大家看過海賊王一定很熟悉這個動作,但是我最后實(shí)現(xiàn)出來的不是這樣,我把抬起的手臂變成了右手(一定不是因?yàn)槲姨脹]看記錯了,一定不是?。。。?,最后我呈現(xiàn)出來的效果是這樣的:

注意,看上去簡單,但是其實(shí)是有很多細(xì)節(jié)的:
地面的弧度 天空,大地,人物顏色的漸變 人物形狀組成 人的影子(沒錯,在這個寂寥的夜晚,我有影子相伴) 下面我逐一敘述這幾個細(xì)節(jié)的實(shí)現(xiàn)方式。
「地面的弧度」:
dom:
<div class="land"></div>
css
.container {
background: linear-gradient(#07112c, #2355d6);
width: 100%;
height: 100vh;
overflow: hidden;
}
.land {
position: relative;
z-index: 20;
height: 60vh;
margin-top: 75vh;
width: 300%;
background: linear-gradient(#072945 0%, #000 30%);
border-radius: 50%;
margin-left: -100vw;
}
很好理解,我給地面三倍的寬度,并通過border-radius 50%使其變成一個橢圓,并通過定位使其居中,最后給container一個overflow:hidden
「顏色漸變」:
顏色漸變這個很常見了,用的其實(shí)就是:background: linear-gradient(#07112c, #2355d6);
背景顏色的線性漸變。
「人物的組成及其陰影」:
遇事不決,先看細(xì)節(jié):
先看人物構(gòu)成,其實(shí)就是一個圓(腦袋瓜兒),一個長方形(身體的軀干),三個橢圓(兩個胳膊 + 上半身下面的弧度),兩個三角形(兩只腿)之后通過定位和在一起的的,代碼就不在此處展示了。
其實(shí)注意的是我的人物其實(shí)不是黑色的,然而根據(jù)生活經(jīng)驗(yàn),我們其實(shí)知道影子是黑色的(「話說我開始搞了彩色的影子被朋友指出來,我才想起來,哦對,影子是黑色的!??!」),所以方法也很簡單,用兩個“人”,一個黑人,一個彩人,黑人用來做倒影,再用彩色的人把黑人蓋住,ok,我真的是小機(jī)靈鬼~
這里做倒影用的是 css3 里面的 「box-reflect」 屬性:
.person-reflect {
-webkit-box-reflect: below 0px -webkit-gradient(linear, left top, left bottom, from(transparent), to(rgba(0, 0, 0, 0.6)));
}
星光閃爍,墜入山河 ?
看效果圖我們可以發(fā)現(xiàn),星星?的效果其實(shí)由兩部分組成:
忽大忽小的閃爍效果 下墜的效果(說白了就是彈幕嘛)
「閃爍效果」:
閃爍效果其實(shí)就是用的 「keyframes」 動畫
@keyframes star-scale {
from {
transform: scale(1, 1);
}
25% {
transform: scale(0.1, 0.1);
}
50% {
transform: scale(1, 1);
}
25% {
transform: scale(2, 2);
}
to {
transform: scale(1, 1);
}
}
.star {
height: 3px;
width: 3px;
background-color: #faf89d;
border-radius: 50%;
position: absolute;
animation: star-scale 2s;
animation-iteration-count: infinite;
}
「下墜效果」:
下墜也是比較常見的了
// 星星效果
function Star(type) {
this.speed = 1;
this.star = document.createElement('div');
this.star.className = type === 'star' ? 'star' : 'moon';
this.star.style.top = '0px';
this.star.style.left = Math.random() * window.innerWidth + 1 + 'px';
document.body.appendChild(this.star);
}
Star.prototype.down = function () {
var that = this;
function move() {
that.star.style.top = that.star.offsetTop + that.speed + 'px';
if (that.star.offsetTop > window.innerHeight) {
clearInterval(timer);
document.body.removeChild(that.star);
}
}
var timer = setInterval(move, 25);
}
let starTimer = setInterval(() => {
new Star('star').down();
}, 300)
哈哈哈,其實(shí)大家看代碼可以發(fā)現(xiàn)我其實(shí)原本打算加入月亮的,后來沒有什么覺得特別好的效果就取消了~
煙火絢爛,火樹銀花 ??

煙花其實(shí)就很簡單了(ps:看完別人的文章搞出來之后再炫耀式的來一句真簡單是不是很找打,哈哈哈哈),這里我是照著大帥老猿的文章學(xué)習(xí)的,大家感興趣可以去看看:快過年了,用JS讓你的網(wǎng)頁放??煙花吧
我在動態(tài)文字消失后在隨機(jī)位置綻放煙花:
function fireRandom() {
const x = (canvas.width * 0.7) * Math.random() + canvas.width * 0.2;
const y = (canvas.height * 0.6) * Math.random() + canvas.height * 0.2;
fire(x, y);
}
雖然是隨機(jī),但是也不能太隨機(jī),要不煙花位置太偏了顯示效果不好,所以我就圈定了canvas的部分范圍~
完整代碼
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HAPPY BIRTHDAY</title>
<style>
* {
margin: 0;
padding: 0;
}
body {
overflow: hidden;
}
.container {
background: linear-gradient(#07112c, #2355d6);
width: 100%;
height: 100vh;
overflow: hidden;
}
.land {
position: relative;
z-index: 20;
height: 60vh;
margin-top: 75vh;
width: 300%;
background: linear-gradient(#072945 0%, #000 30%);
border-radius: 50%;
margin-left: -100vw;
}
.happy-birthday__container {
position: absolute;
top: 130px;
display: flex;
justify-content: center;
width: 100%;
opacity: .6;
transition: all 2s;
}
.happy-birthday__container>div {
height: 100px;
width: 100px;
margin: 0 10px;
flex: 0 1 auto;
}
.person {
position: absolute;
z-index: 99;
top: calc(75vh - 100px);
left: calc(50vw - 30px);
height: 100px;
width: 60px;
}
.person-reflect {
-webkit-box-reflect: below 0px -webkit-gradient(linear, left top, left bottom, from(transparent), to(rgba(0, 0, 0, 0.6)));
}
.head {
position: relative;
height: 40px;
width: 40px;
background-color: #000;
border-radius: 50%;
margin-left: 10px;
z-index: 99;
}
.body-container {
margin-left: 15px;
margin-top: -10px;
width: 30px;
position: relative;
z-index: 20;
}
.body-content {
width: 100%;
height: 35px;
background-color: #000;
}
.body-radius {
width: 100%;
height: 10px;
border-radius: 50%;
background-color: #000;
margin-top: -5px;
}
.legs {
margin-left: 15px;
margin-top: -6px;
height: 30px;
width: 36px;
}
.left {
display: inline-block;
border-right: 7px solid transparent;
border-left: 5px solid transparent;
border-top: 35px solid #000;
}
.right {
display: inline-block;
border-right: 6px solid transparent;
border-left: 7px solid transparent;
border-top: 35px solid #000;
}
.left-hand {
position: absolute;
height: 35px;
width: 10px;
background-color: #000;
border-radius: 50%;
left: 10px;
top: 35px;
z-index: 1;
}
.right-hand {
position: absolute;
width: 9px;
height: 50px;
background-color: #000;
left: 45px;
top: 7px;
border-radius: 50%;
transform: rotate(20deg);
z-index: 1;
}
.person-show>.head {
background: linear-gradient(#072253, rgb(20, 19, 65));
}
.person-show>.body-container>div {
background-color: rgb(20, 19, 65);
}
.person-show>.left-hand,
.right-hand {
background-color: rgb(20, 19, 65);
}
.person-show .legs>div {
border-top-color: #151618;
}
@keyframes star-scale {
from {
transform: scale(1, 1);
}
25% {
transform: scale(0.1, 0.1);
}
50% {
transform: scale(1, 1);
}
25% {
transform: scale(2, 2);
}
to {
transform: scale(1, 1);
}
}
.star {
height: 3px;
width: 3px;
background-color: #faf89d;
border-radius: 50%;
position: absolute;
animation: star-scale 2s;
animation-iteration-count: infinite;
}
#my-canvas {
position: absolute;
width: 100%;
height: 80vh;
}
</style>
</head>
<body>
<div class="container">
<div class="happy-birthday__container">
<div id="sheng"></div>
<div id="ri"></div>
<div id="kuai"></div>
<div id="le"></div>
<div id="ya"></div>
<div id="site"></div>
<div id="xiao"></div>
<div id="han"></div>
<div id="cao"></div>
</div>
<canvas id="my-canvas"></canvas>
<div class="person person-reflect">
<div class="head"></div>
<div class="left-hand"></div>
<div class="right-hand"></div>
<div class="body-container">
<div class="body-content"></div>
<div class="body-radius"></div>
</div>
<div class="legs">
<div class="left"></div>
<div class="right"></div>
</div>
</div>
<div class="person person-show">
<div class="head"></div>
<div class="left-hand"></div>
<div class="right-hand"></div>
<div class="body-container">
<div class="body-content"></div>
<div class="body-radius"></div>
</div>
<div class="legs">
<div class="left"></div>
<div class="right"></div>
</div>
</div>
<div class="land"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/hanzi-writer.min.js"></script>
<script>
// 煙花效果
var canvas = document.getElementById('my-canvas');
var context = canvas.getContext('2d');
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight * 0.8;
}
function clearCanvas() {
context.clearRect(0, 0, canvas.width, canvas.height);
}
window.addEventListener('resize', resizeCanvas, false);
resizeCanvas();
function mouseDownHandler(e) {
var x = e.clientX;
var y = e.clientY;
fire(x, y);
}
var rid;
function fire(x, y) {
createFireworks(x, y);
function tick() {
context.globalCompositeOperation = 'destination-out';
context.fillStyle = 'rgba(0, 0, 0,' + 20 / 100 + ')';
context.fillRect(0, 0, canvas.width, canvas.height);
context.globalCompositeOperation = 'lighter';
drawFireworks();
rid = requestAnimationFrame(tick);
}
cancelAnimationFrame(rid);
tick();
}
var particles = [];
function createFireworks(sx, sy) {
clearCanvas();
particles = [];
var hue = Math.floor(Math.random() * 51) + 150;
var hueVariance = 30;
var count = 365;
for (var i = 0; i < count; i++) {
var p = {};
var angle = Math.floor(Math.random() * 360);
p.radians = angle * Math.PI / 180;
p.x = sx;
p.y = sy;
p.speed = (Math.random() * 5) + .4;
p.radius = p.speed;
p.size = Math.floor(Math.random() * 3) + 1;
p.hue = Math.floor(Math.random() * ((hue + hueVariance) - (hue - hueVariance))) + (hue - hueVariance);
p.brightness = Math.floor(Math.random() * 31) + 50;
p.alpha = (Math.floor(Math.random() * 61) + 40) / 100;
particles.push(p);
}
}
function drawFireworks() {
for (var i = 0; i < particles.length; i++) {
var p = particles[i];
var vx = Math.cos(p.radians) * p.radius;
var vy = Math.sin(p.radians) * p.radius + 1.2;
p.x += vx;
p.y += vy;
p.radius *= 1 - p.speed / 300;
p.alpha -= 0.005;
context.beginPath();
context.arc(p.x, p.y, p.size, 0, Math.PI * 2, false);
context.closePath();
context.fillStyle = 'hsla('+p.hue+100+', 100%, '+p.brightness+'%, '+p.alpha+')';
context.fill();
}
}
function fireRandom() {
const x = (canvas.width * 0.7) * Math.random() + canvas.width * 0.2;
const y = (canvas.height * 0.6) * Math.random() + canvas.height * 0.2;
fire(x, y);
}
document.addEventListener('mousedown', mouseDownHandler, false);
// 星星效果
function Star(type) {
this.speed = 1;
this.star = document.createElement('div');
this.star.className = type === 'star' ? 'star' : 'moon';
this.star.style.top = '0px';
this.star.style.left = Math.random() * window.innerWidth + 1 + 'px';
document.body.appendChild(this.star);
}
Star.prototype.down = function () {
var that = this;
function move() {
that.star.style.top = that.star.offsetTop + that.speed + 'px';
if (that.star.offsetTop > window.innerHeight) {
clearInterval(timer);
document.body.removeChild(that.star);
}
}
var timer = setInterval(move, 25);
}
let starTimer = setInterval(() => {
new Star('star').down();
}, 300)
// 文字效果
const BASE_CONFIG = {
width: 100,
height: 100,
padding: 5,
delayBetweenStrokes: 0,
strokeAnimationSpeed: 1.2,
showCharacter: false,
showOutline: false,
}
const WRITER_CONFIG = {
...BASE_CONFIG,
strokeColor: '#e09037'
};
const NAME_CONFIG = {
...BASE_CONFIG,
strokeColor: '#87db92'
};
const getWriterList = () => {
let writerList = [];
writerList.push(HanziWriter.create('sheng', '生', WRITER_CONFIG));
writerList.push(HanziWriter.create('ri', '日', WRITER_CONFIG));
writerList.push(HanziWriter.create('kuai', '快', WRITER_CONFIG));
writerList.push(HanziWriter.create('le', '樂', WRITER_CONFIG));
writerList.push(HanziWriter.create('ya', '吖', WRITER_CONFIG));
writerList.push(HanziWriter.create('xiao', '小', NAME_CONFIG));
writerList.push(HanziWriter.create('han', '寒', NAME_CONFIG));
writerList.push(HanziWriter.create('cao', '草', NAME_CONFIG));
return writerList;
}
const generateAnimateWriter = async (writerList) => {
const writerCount = writerList.length;
for (const writer of writerList) {
await writer.animateCharacter();
}
document.getElementsByClassName('happy-birthday__container')[0].style.opacity = 0;
fireRandom();
setInterval(() => {
fireRandom();
} , 2000);
}
const writerList = getWriterList();
generateAnimateWriter(writerList);
</script>
</body>
</html>
尾奏 ??
沖向未來的極樂迪斯科

在這關(guān)鍵的一天,我當(dāng)然要去說一說未來的打算,我以我眼中比較浪漫主義的口吻來書寫這篇文章,當(dāng)然在這個環(huán)節(jié)也是希望把我的未來盡可能說的更加充滿浪漫色彩,所以我打算分成幾部分來講。
開源
我們有一個組織:CodingCommunism
最近想做個 「vue3」 的基礎(chǔ)ui組件: commi-ui
有很多朋友也參與進(jìn)來了,感謝大家的陪伴,我們一定可以一起努力,把這個組件庫弄好,當(dāng)然如果大家想圍觀我們的開發(fā)過程,也可以加我的微信 hancao97,我拉你進(jìn)群。
當(dāng)然不僅如此,我們還有很多別的東西:
fe-file-rename automated-interface-testing(這個是我的深坑,我會填的!) ...
生活

曾經(jīng)說過工作后要去很多很多地方,要去看很多很多風(fēng)景,今年上半年也算是去了很多地方了,但是最近因?yàn)楣ぷ骱蛯懽鞯脑驍R置了一些原本的想法,但是我這閑不住的心還是在的。
想去西藏看純凈的山純凈的水 想去重慶吃火鍋看魔幻的山城 總會出發(fā)走上旅程的~
我一直在路上~
難忘今宵~啊~難忘今宵~
又到了今年的生日??,我又長大了一歲,前幾天已經(jīng)完成了工作一周年的成就,現(xiàn)在又要完成24周歲的成就,其實(shí)有很多感慨,但是我的文筆又難以去很好的表達(dá)。
寫文半年,工作一年,無論是在我們公司,還是在掘金寫作的過程中,都認(rèn)識了很多很棒的伙伴,獲得了很多成長,從曾經(jīng)磕磕絆絆的萌新到現(xiàn)在也變成了一個可以獨(dú)立負(fù)責(zé)一個大模塊,完整負(fù)責(zé)一個定制項(xiàng)目的前端工程師。我想這應(yīng)該只是開始,一個中二的少年走上沙場,征戰(zhàn)四方的開始,我相信我會有更好的未來??,大家也是~
今夜我與大家一同佇立于夜空下,望漫天繁星??,賞煙花絢爛??,耳邊回響起每年春節(jié)聯(lián)歡晚會臨近午夜時奏響的難忘今宵。
?難忘今宵,啊,難忘今宵~
?
我們還會見面的,伙伴們,祝你們的生活變得更加有幸福感~
如果大家喜歡本篇文章,請留下你們的生日祝福??吧~
祝大家生活更加精彩~
請你喝杯?? 記得三連哦~
1.閱讀完記得給?? 醬點(diǎn)個贊哦,有?? 有動力
2.關(guān)注公眾號前端那些趣事,陪你聊聊前端的趣事
3.文章收錄在Github frontendThings 感謝Star?
