console
const canvas = document.getElementById("snow");
const ctx = canvas.getContext("2d");
const startText = document.getElementById("startText");
let W, H;
let particles = [];
let snows = [];
let hearts = [];
let ripples = [];
let glow = 0;
let isClicking = false;
// 初始数量
let snowCount = 80;
let heartCount = 8;
// ✅一共5条(新增2条,第5条是最终句:这个冬天,我陪你)
const blessingList = [
"冬至,岁岁安暖",
"冬至,幸得有你",
"凛冬至,共朝夕",
"长夜逢冬至,有幸遇见你",
"这个冬天,我陪你"
];
let stage = -1; // -1=冬至,0,1,2,3,4
function resize() {
const dpr = window.devicePixelRatio || 1;
W = window.innerWidth;
H = window.innerHeight;
canvas.width = W * dpr;
canvas.height = H * dpr;
canvas.style.width = W + "px";
canvas.style.height = H + "px";
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
resize();
window.addEventListener("resize", resize);
class Snow{
constructor(){
this.reset();
}
reset(){
this.x = Math.random()*W;
this.y = -10;
this.r = Math.random()*2 + 0.6;
this.sy = Math.random()*1.4 + 0.6;
this.sx = (Math.random()-0.5)*0.7;
this.alpha = Math.random()*0.6 + 0.3;
}
update(){
this.y += this.sy;
this.x += this.sx + Math.sin(this.y*0.01)*0.2;
if(this.y>H) this.reset();
}
draw(){
ctx.save();
ctx.globalAlpha = this.alpha;
ctx.fillStyle="#fff";
ctx.beginPath();
ctx.arc(this.x,this.y,this.r,0,Math.PI*2);
ctx.fill();
ctx.restore();
}
}
class Heart{
constructor(){
this.reset();
}
reset(){
this.x = Math.random()*W;
this.y = -10;
this.size = Math.random()*10 + 6;
this.sy = Math.random()*0.8 + 0.4;
this.sx = (Math.random()-0.5)*0.4;
this.alpha = Math.random()*0.5 + 0.4;
}
update(){
this.y += this.sy;
this.x += this.sx + Math.sin(this.y*0.01)*0.15;
if(this.y>H) this.reset();
}
draw(){
ctx.save();
ctx.globalAlpha = this.alpha;
ctx.translate(this.x,this.y);
ctx.scale(this.size/16,this.size/16);
ctx.fillStyle="#ff6b9d";
ctx.beginPath();
ctx.moveTo(0,-4);
ctx.bezierCurveTo(6,-12,12,-4,0,8);
ctx.bezierCurveTo(-12,-4,-6,-12,0,-4);
ctx.fill();
ctx.restore();
}
}
class Ripple{
constructor(x,y){
this.x=x;this.y=y;
this.r=0;
this.maxR = Math.min(W,H)*0.25;
this.alpha=0.7;
}
update(){
this.r +=4;
this.alpha -=0.025;
}
draw(){
ctx.save();
ctx.globalAlpha = Math.max(0,this.alpha);
ctx.strokeStyle="rgba(255,255,255,0.8)";
ctx.lineWidth=2;
ctx.beginPath();
ctx.arc(this.x,this.y,this.r,0,Math.PI*2);
ctx.stroke();
ctx.restore();
}
}
class Particle{
constructor(tx,ty,char){
this.x = Math.random()*W;
this.y = -H;
this.tx = tx;
this.ty = ty;
this.char = char;
this.size = 18;
this.formed = false;
}
update(){
if(!this.formed){
const dx = this.tx - this.x;
const dy = this.ty - this.y;
this.x += dx*0.1;
this.y += dy*0.1;
if(Math.abs(dx)<6 && Math.abs(dy)<6){
this.formed = true;
}
}else{
this.x += Math.sin(Date.now()*0.001 + this.tx)*0.15;
this.y += Math.cos(Date.now()*0.0013 + this.ty)*0.15;
}
}
draw(){
ctx.save();
if(this.formed){
ctx.shadowBlur = 12*glow;
ctx.shadowColor = "rgba(255,255,255,0.9)";
}
ctx.fillStyle="#fff";
ctx.font = `bold ${this.size}px "Microsoft YaHei"`;
ctx.fillText(this.char,this.x,this.y);
ctx.restore();
}
}
function addSnow(num){
for(let i=0;i<num;i++){
snows.push(new Snow());
}
}
function addHeart(num){
for(let i=0;i<num;i++){
hearts.push(new Heart());
}
}
function initTextParticle(text){
particles = [];
ctx.save();
ctx.font="bold 48px Microsoft YaHei";
const m = ctx.measureText(text);
const startX = W/2 - m.width/2;
const baseY = H/2;
for(let i=0;i<text.length;i++){
const char = text[i];
const w = ctx.measureText(char).width;
particles.push(new Particle(startX + w*i, baseY, char));
}
ctx.restore();
}
function tapHandle(x,y){
if(isClicking) return;
isClicking = true;
ripples.push(new Ripple(x,y));
particles = [];
glow = 0;
stage++;
// ✅点击一次增加雪花爱心
addSnow(25);
addHeart(3);
if(stage < blessingList.length){
initTextParticle(blessingList[stage]);
startText.style.opacity = "0";
startText.style.display = "none";
}else{
// 全部走完回到冬至
stage = -1;
startText.style.display = "block";
startText.style.opacity = "1";
}
setTimeout(()=>{
isClicking = false;
},250);
}
canvas.addEventListener("click",e=>tapHandle(e.clientX,e.clientY));
canvas.addEventListener("touchstart",e=>{
e.preventDefault();
for(let t of e.touches) tapHandle(t.clientX,t.clientY);
},{passive:false})
function loop(){
ctx.clearRect(0,0,W,H);
snows.forEach(s=>{s.update();s.draw()});
hearts.forEach(h=>{h.update();h.draw()});
ripples = ripples.filter(r=>r.alpha>0);
ripples.forEach(r=>{r.update();r.draw()});
if(particles.length>0){
particles.forEach(p=>{p.update();p.draw()});
glow = Math.min(1, glow+0.02);
}
requestAnimationFrame(loop);
}
//初始化
for(let i=0;i<snowCount;i++) snows.push(new Snow());
for(let i=0;i<heartCount;i++) hearts.push(new Heart());
loop();
<div id="startText">冬至</div>
<canvas id="snow"></canvas>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
background: #0f172a;
font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
#startText {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #ffffff;
font-size: clamp(48px, 11vw, 96px);
letter-spacing: clamp(10px, 3vw, 24px);
z-index: 5;
pointer-events: none;
opacity: 1;
transition: opacity 0.7s ease;
white-space: nowrap;
}
#snow {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 2;
}