先上效果图:
代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--text-color: #f6f1e6;
--drops-color: #231d1c;
}
body {
background: var(--text-color);
font-family: Georgia, serif;
}
.canvas {
position: relative;
height: 100vh;
overflow: hidden;
}
.canvas--animated .puddle__letter {
-webkit-animation-play-state: running;
animation-play-state: running;
}
.puddle {
position: absolute;
display: flex;
transform: rotate(var(--r, 0deg)) translate(calc(-50% - var(--x, 0)), calc(-50% - var(--y, 0)));
top: 50%;
left: 50%;
color: var(--text-color);
font-size: 6vmin;
white-space: pre;
}
.puddle__letter {
transform: translateY(800px);
padding: 0.1em 0.2em;
margin: -0.1em -0.2em;
will-change: transform;
-webkit-animation: drop 800ms var(--delay) ease-out forwards paused;
animation: drop 800ms var(--delay) ease-out forwards paused;
}
.combined-puddles {
position: relative;
height: 100%;
z-index: -1;
filter: url(#drops-filter);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.combined-puddles .puddle__letter {
border-radius: 50%;
color: var(--drops-color);
background-color: currentColor;
}
.combined-puddles .puddle__letter--t-1 {
box-shadow: 0.0259077911em 0.0158195613em 0 0.156531918em currentColor;
}
.combined-puddles .puddle__letter--t-2 {
box-shadow: 0.0027610231em 0.0292517118em 0 0.0017377822em currentColor;
}
.combined-puddles .puddle__letter--t-3 {
box-shadow: 0.0014897121em 0.0208965418em 0 0.2029532073em currentColor;
}
.combined-puddles .puddle__letter--t-4 {
box-shadow: 0.0181212165em 0.0001267036em 0 0.2696130855em currentColor;
}
.combined-puddles .puddle__letter--t-5 {
box-shadow: 0.0299544544em 0.0155872295em 0 0.3927980665em currentColor;
}
.combined-puddles .puddle__letter--t-6 {
box-shadow: 0.0223628085em 0.013623193em 0 0.3951977831em currentColor;
}
.combined-puddles .puddle__letter--t-7 {
box-shadow: 0.017073126em 0.0166402911em 0 0.1251815461em currentColor;
}
.combined-puddles .puddle__letter--t-8 {
box-shadow: 0.0160218528em 0.0103797109em 0 0.2355980051em currentColor;
}
.combined-puddles .puddle__letter--t-9 {
box-shadow: 0.0061457537em 0.0272304962em 0 0.0502537243em currentColor;
}
.combined-puddles .puddle__letter--t-10 {
box-shadow: 0.0010890892em 0.0108737853em 0 0.2990892848em currentColor;
}
@-webkit-keyframes drop {
0% {
transform: translate(0, 100vh);
}
80% {
transform: translate(0, -5px);
}
100% {
transform: translate(0, 0);
}
}
@keyframes drop {
0% {
transform: translate(0, 100vh);
}
80% {
transform: translate(0, -5px);
}
100% {
transform: translate(0, 0);
}
}
</style>
</head>
<body>
<div class="canvas">
<div class="puddle" style="--x: 1.55em; --y: 3.73em">Roses are red</div>
<div class="puddle" style="--x: 1.5em; --y: 1.8em">Violets are blue</div>
<div class="puddle" style="--x: -1.5em; --y: -1.8em">Unexpected ";"</div>
<div class="puddle" style="--x: -1.3em; --y: -3.7em">On line 32</div>
</div>
</body>
<script>
class Droppy {
DEFAULT_OPTIONS = {
canvasSelector: ".canvas",
textSelector: ".puddle",
letterClassName: "puddle__letter",
dropsClassName: "combined-puddles",
delayBetweenDrops: 95,
dropTypes: 10,
wordAngleRange: [-3, 3]
};
constructor(opts) {
this.opts = { ...this.DEFAULT_OPTIONS, ...opts };
this.$textSelector = document.querySelectorAll(this.opts.textSelector);
this.$canvas = document.querySelector(this.opts.canvasSelector);
this.init();
}
init() {
this.injectSVGFilter();
this.wrapLetters();
this.addDelayToEachLetter();
this.createDrops();
this.startAnimation();
}
getRandomInt = (min, max) => {
return Math.floor(Math.random() * (max - min + 1)) + min;
};
startAnimation() {
this.$canvas.classList.add("canvas--animated");
}
wrapLetters() {
this.$textSelector.forEach(($word) => {
const letters = Array.from($word.innerText).map((letter) => {
const dropType = this.getRandomInt(1, this.opts.dropTypes);
const className = `${this.opts.letterClassName} ${this.opts.letterClassName}--t-${dropType}`;
return `<div class="${className}">${letter}</div>`;
});
const angle = this.getRandomInt(
this.opts.wordAngleRange[0],
this.opts.wordAngleRange[1]
);
$word.style.cssText += `--r:${angle}deg`;
$word.innerHTML = letters.join("");
});
}
addDelayToEachLetter() {
const letters = document.querySelectorAll(`.${this.opts.letterClassName}`);
Array.from(letters, ($letter, index) => {
const delay = index * this.opts.delayBetweenDrops;
$letter.style.cssText += `--delay:${delay}ms`;
});
}
createDrops() {
const $drops = document.createElement("div");
$drops.className = this.opts.dropsClassName;
Array.from(this.$textSelector, ($word) =>
$drops.appendChild($word.cloneNode(true))
);
this.$canvas.appendChild($drops);
}
injectSVGFilter() {
const filter =
'<svg style="display:none;"><filter id="drops-filter" x="-50%" width="200%" y="-50%" height="200%" color-interpolation-filters="sRGB"><feGaussianBlur in="SourceGraphic" stdDeviation="8" result="blur" /><feColorMatrix in="blur" mode="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 21 -7" result="cm" /></filter></svg>';
this.$canvas.insertAdjacentHTML("beforeend", filter);
}
}
new Droppy();
</script>
</html>