一、创意效果介绍
运行代码会有烟花等特效,名字也可以随意修改
二、效果视频
生日快乐
三、设计代码
(1)HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>动态生日快乐</title>
<link rel="stylesheet" href="./style.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/prefixfree/1.0.7/prefixfree.min.js"></script>
</head>
<body>
<!-- partial:index.partial.html -->
<canvas id="c"></canvas>
<!-- partial -->
<script src="./script.js"></script>
</body>
</html>
(2)CSS
canvas {
position: absolute;
top: 0;
left: 0;
}
p {
margin: 0 0;
position: absolute;
font: 16px Verdana;
color: #eee;
height: 25px;
top: calc( 100vh - 30px );
text-shadow: 0 0 2px white;
}
p a {
text-decoration: none;
color: #aaa;
}
span {
font-size: 11px;
}
p > a:first-of-type {
font-size: 20px;
}
body {
overflow: hidden;
}
(3)JavaScript
var w = (c.width = window.innerWidth),
h = (c.height = window.innerHeight),
ctx = c.getContext("2d"),
hw = w / 2, // half-width
hh = h / 2,
sentenceIndex = 0,
opts = {
get strings() {
const sentences = [
["HAPPY", "BIRTHDAY!"],
["星染夜空", "生日快乐!"],
];
return sentences[sentenceIndex++ % sentences.length];
},
charSize: 30,
charSpacing: 35,
lineHeight: 40,
cx: w / 2,
cy: h / 2,
fireworkPrevPoints: 10,
fireworkBaseLineWidth: 5,
fireworkAddedLineWidth: 8,
fireworkSpawnTime: 200,
fireworkBaseReachTime: 30,
fireworkAddedReachTime: 30,
fireworkCircleBaseSize: 20,
fireworkCircleAddedSize: 10,
fireworkCircleBaseTime: 30,
fireworkCircleAddedTime: 30,
fireworkCircleFadeBaseTime: 10,
fireworkCircleFadeAddedTime: 5,
fireworkBaseShards: 5,
fireworkAddedShards: 5,
fireworkShardPrevPoints: 3,
fireworkShardBaseVel: 4,
fireworkShardAddedVel: 2,
fireworkShardBaseSize: 3,
fireworkShardAddedSize: 3,
gravity: 0.1,
upFlow: -0.1,
letterContemplatingWaitTime: 360,
balloonSpawnTime: 20,
balloonBaseInflateTime: 10,
balloonAddedInflateTime: 10,
balloonBaseSize: 20,
balloonAddedSize: 20,
balloonBaseVel: 0.4,
balloonAddedVel: 0.4,
balloonBaseRadian: -(Math.PI / 2 - 0.5),
balloonAddedRadian: -1,
},
currentSentence = opts.strings;
(calc = {
totalWidth:
opts.charSpacing *
Math.max(currentSentence[0].length, currentSentence[1].length),
}),
(Tau = Math.PI * 2),
(TauQuarter = Tau / 4),
(letters = []);
ctx.font = opts.charSize + "px Verdana";
function Letter(char, x, y) {
this.char = char;
this.x = x;
this.y = y;
this.dx = -ctx.measureText(char).width / 2;
this.dy = +opts.charSize / 2;
this.fireworkDy = this.y - hh;
var hue = (x / calc.totalWidth) * 360;
this.color = "hsl(hue,80%,50%)".replace("hue", hue);
this.lightAlphaColor = "hsla(hue,80%,light%,alp)".replace("hue", hue);
this.lightColor = "hsl(hue,80%,light%)".replace("hue", hue);
this.alphaColor = "hsla(hue,80%,50%,alp)".replace("hue", hue);
this.reset();
}
Letter.prototype.reset = function () {
this.phase = "firework";
this.tick = 0;
this.spawned = false;
this.spawningTime = (opts.fireworkSpawnTime * Math.random()) | 0;
this.reachTime =
(opts.fireworkBaseReachTime + opts.fireworkAddedReachTime * Math.random()) |
0;
this.lineWidth =
opts.fireworkBaseLineWidth + opts.fireworkAddedLineWidth * Math.random();
this.prevPoints = [[0, hh, 0]];
};
Letter.prototype.step = function () {
if (this.phase === "firework") {
if (!this.spawned) {
++this.tick;
if (this.tick >= this.spawningTime) {
this.tick = 0;
this.spawned = true;
}
} else {
++this.tick;
var linearProportion = this.tick / this.reachTime,
armonicProportion = Math.sin(linearProportion * TauQuarter),
x = linearProportion * this.x,
y = hh + armonicProportion * this.fireworkDy;
if (this.prevPoints.length > opts.fireworkPrevPoints)
this.prevPoints.shift();
this.prevPoints.push([x, y, linearProportion * this.lineWidth]);
var lineWidthProportion = 1 / (this.prevPoints.length - 1);
for (var i = 1; i < this.prevPoints.length; ++i) {
var point = this.prevPoints[i],
point2 = this.prevPoints[i - 1];
ctx.strokeStyle = this.alphaColor.replace(
"alp",
i / this.prevPoints.length
);
ctx.lineWidth = point[2] * lineWidthProportion * i;
ctx.beginPath();
ctx.moveTo(point[0], point[1]);
ctx.lineTo(point2[0], point2[1]);
ctx.stroke();
}
if (this.tick >= this.reachTime) {
this.phase = "contemplate";
this.circleFinalSize =
opts.fireworkCircleBaseSize +
opts.fireworkCircleAddedSize * Math.random();
this.circleCompleteTime =
(opts.fireworkCircleBaseTime +
opts.fireworkCircleAddedTime * Math.random()) |
0;
this.circleCreating = true;
this.circleFading = false;
this.circleFadeTime =
(opts.fireworkCircleFadeBaseTime +
opts.fireworkCircleFadeAddedTime * Math.random()) |
0;
this.tick = 0;
this.tick2 = 0;
this.shards = [];
var shardCount =
(opts.fireworkBaseShards +
opts.fireworkAddedShards * Math.random()) |
0,
angle = Tau / shardCount,
cos = Math.cos(angle),
sin = Math.sin(angle),
x = 1,
y = 0;
for (var i = 0; i < shardCount; ++i) {
var x1 = x;
x = x * cos - y * sin;
y = y * cos + x1 * sin;
this.shards.push(new Shard(this.x, this.y, x, y, this.alphaColor));
}
}
}
} else if (this.phase === "contemplate") {
++this.tick;
if (this.circleCreating) {
++this.tick2;
var proportion = this.tick2 / this.circleCompleteTime,
armonic = -Math.cos(proportion * Math.PI) / 2 + 0.5;
ctx.beginPath();
ctx.fillStyle = this.lightAlphaColor
.replace("light", 50 + 50 * proportion)
.replace("alp", proportion);
ctx.beginPath();
ctx.arc(this.x, this.y, armonic * this.circleFinalSize, 0, Tau);
ctx.fill();
if (this.tick2 > this.circleCompleteTime) {
this.tick2 = 0;
this.circleCreating = false;
this.circleFading = true;
}
} else if (this.circleFading) {
ctx.fillStyle = this.lightColor.replace("light", 70);
ctx.fillText(this.char, this.x + this.dx, this.y + this.dy);
++this.tick2;
var proportion = this.tick2 / this.circleFadeTime,
armonic = -Math.cos(proportion * Math.PI) / 2 + 0.5;
ctx.beginPath();
ctx.fillStyle = this.lightAlphaColor
.replace("light", 100)
.replace("alp", 1 - armonic);
ctx.arc(this.x, this.y, this.circleFinalSize, 0, Tau);
ctx.fill();
if (this.tick2 >= this.circleFadeTime) this.circleFading = false;
} else {
ctx.fillStyle = this.lightColor.replace("light", 70);
ctx.fillText(this.char, this.x + this.dx, this.y + this.dy);
}
for (var i = 0; i < this.shards.length; ++i) {
this.shards[i].step();
if (!this.shards[i].alive) {
this.shards.splice(i, 1);
--i;
}
}
if (this.tick > opts.letterContemplatingWaitTime) {
this.phase = "balloon";
this.tick = 0;
this.spawning = true;
this.spawnTime = (opts.balloonSpawnTime * Math.random()) | 0;
this.inflating = false;
this.inflateTime =
(opts.balloonBaseInflateTime +
opts.balloonAddedInflateTime * Math.random()) |
0;
this.size =
(opts.balloonBaseSize + opts.balloonAddedSize * Math.random()) | 0;
var rad =
opts.balloonBaseRadian + opts.balloonAddedRadian * Math.random(),
vel = opts.balloonBaseVel + opts.balloonAddedVel * Math.random();
this.vx = Math.cos(rad) * vel;
this.vy = Math.sin(rad) * vel;
}
} else if (this.phase === "balloon") {
ctx.strokeStyle = this.lightColor.replace("light", 80);
if (this.spawning) {
++this.tick;
ctx.fillStyle = this.lightColor.replace("light", 70);
ctx.fillText(this.char, this.x + this.dx, this.y + this.dy);
if (this.tick >= this.spawnTime) {
this.tick = 0;
this.spawning = false;
this.inflating = true;
}
} else if (this.inflating) {
++this.tick;
var proportion = this.tick / this.inflateTime,
x = (this.cx = this.x),
y = (this.cy = this.y - this.size * proportion);
ctx.fillStyle = this.alphaColor.replace("alp", proportion);
ctx.beginPath();
generateBalloonPath(x, y, this.size * proportion);
ctx.fill();
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x, this.y);
ctx.stroke();
ctx.fillStyle = this.lightColor.replace("light", 70);
ctx.fillText(this.char, this.x + this.dx, this.y + this.dy);
if (this.tick >= this.inflateTime) {
this.tick = 0;
this.inflating = false;
}
} else {
this.cx += this.vx;
this.cy += this.vy += opts.upFlow;
ctx.fillStyle = this.color;
ctx.beginPath();
generateBalloonPath(this.cx, this.cy, this.size);
ctx.fill();
ctx.beginPath();
ctx.moveTo(this.cx, this.cy);
ctx.lineTo(this.cx, this.cy + this.size);
ctx.stroke();
ctx.fillStyle = this.lightColor.replace("light", 70);
ctx.fillText(this.char, this.cx + this.dx, this.cy + this.dy + this.size);
if (this.cy + this.size < -hh || this.cx < -hw || this.cy > hw)
this.phase = "done";
}
}
};
function Shard(x, y, vx, vy, color) {
var vel =
opts.fireworkShardBaseVel + opts.fireworkShardAddedVel * Math.random();
this.vx = vx * vel;
this.vy = vy * vel;
this.x = x;
this.y = y;
this.prevPoints = [[x, y]];
this.color = color;
this.alive = true;
this.size =
opts.fireworkShardBaseSize + opts.fireworkShardAddedSize * Math.random();
}
Shard.prototype.step = function () {
this.x += this.vx;
this.y += this.vy += opts.gravity;
if (this.prevPoints.length > opts.fireworkShardPrevPoints)
this.prevPoints.shift();
this.prevPoints.push([this.x, this.y]);
var lineWidthProportion = this.size / this.prevPoints.length;
for (var k = 0; k < this.prevPoints.length - 1; ++k) {
var point = this.prevPoints[k],
point2 = this.prevPoints[k + 1];
ctx.strokeStyle = this.color.replace("alp", k / this.prevPoints.length);
ctx.lineWidth = k * lineWidthProportion;
ctx.beginPath();
ctx.moveTo(point[0], point[1]);
ctx.lineTo(point2[0], point2[1]);
ctx.stroke();
}
if (this.prevPoints[0][1] > hh) this.alive = false;
};
function generateBalloonPath(x, y, size) {
ctx.moveTo(x, y);
ctx.bezierCurveTo(
x - size / 2,
y - size / 2,
x - size / 4,
y - size,
x,
y - size
);
ctx.bezierCurveTo(x + size / 4, y - size, x + size / 2, y - size / 2, x, y);
}
function anim() {
window.requestAnimationFrame(anim);
ctx.fillStyle = "#111";
ctx.fillRect(0, 0, w, h);
ctx.translate(hw, hh);
var done = true;
for (var l = 0; l < letters.length; ++l) {
letters[l].step();
if (letters[l].phase !== "done") done = false;
}
ctx.translate(-hw, -hh);
if (done) {
for (var l = 0; l < letters.length; ++l) {
letters[l].reset();
}
currentSentence = opts.strings;
letters = [];
for (var i = 0; i < currentSentence.length; ++i) {
for (var j = 0; j < currentSentence[i].length; ++j) {
letters.push(
new Letter(
currentSentence[i][j],
j * opts.charSpacing +
opts.charSpacing / 2 -
(currentSentence[i].length * opts.charSize) / 2,
i * opts.lineHeight +
opts.lineHeight / 2 -
(currentSentence.length * opts.lineHeight) / 2
)
);
}
}
}
}
for (var i = 0; i < currentSentence.length; ++i) {
for (var j = 0; j < currentSentence[i].length; ++j) {
letters.push(
new Letter(
currentSentence[i][j],
j * opts.charSpacing +
opts.charSpacing / 2 -
(currentSentence[i].length * opts.charSize) / 2,
i * opts.lineHeight +
opts.lineHeight / 2 -
(currentSentence.length * opts.lineHeight) / 2
)
);
}
}
anim();
window.addEventListener("resize", function () {
w = c.width = window.innerWidth;
h = c.height = window.innerHeight;
hw = w / 2;
hh = h / 2;
ctx.font = opts.charSize + "px Verdana";
});