SOURCE

console 命令行工具 X clear

                    
>
console
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>圣诞树</title>
<style>
*{margin:0;box-sizing:border-box}
html,body{height:100%}
canvas{display:block;width:100%;height:100%;}
body{overflow:hidden;background:#020308;}
</style>
</head>
<body>
<canvas id="scene"></canvas>
<script>
const canvas = document.getElementById('scene');
const ctx = canvas.getContext('2d');

const BLUE = ['#8efbff','#8b8dff','#5a86ff'];
const PINK = ['#ffc3ba','#ffadff','#ff60ff'];
const WHITE = ['#ffffff','#fffdad','#efffcf'];
const pick = arr => arr[Math.floor(Math.random() * arr.length)];

let W, DPR, cx, flooY, topY, baseR, treeH;
let particles = [], ornaments = [], bgStars = [], floorDots = [], floaters = [];

const RIBBONS = [
  {color:'#8efbff',turns:2.7,offset:0,width:1.7,sample:210},
  {color:'#ffb0e2',turns:2.7,offset:Math.PI,width:1.7,sample:210},
  {color:'#ffadff',turns:2.7,offset:Math.PI/2,width:1.2,sample:210},
];

function build(){
  particles = []; ornaments = []; bgStars = []; floorDots = []; floaters = [];
  const nP = Math.max(1000,Math.min(1800,Math.floor(W * H / 750)));
  for(let i=0;i<nP;i++){
    particles.push({
      t:Math.random(),
      theta:Math.random()*Math.PI*2,
      rho:Math.random(),
      size:0.6 + Math.random()*1.3,
      color:Math.random()<0.42 ? pick(BLUE) : Math.random()<0.82 ? PINK : WHITE,
      phase:Math.random()*Math.PI*2,
      speed:1.5 + Math.random()*2.5
    });
  }
  for(let i=0;i<36;i++){
    ornaments.push({
      t:0.12+Math.random()*0.88,
      theta:Math.random()*Math.PI*2,
      rho:0.2 + Math.random()*0.42,
      size:1.8 + Math.random()*2.6,
      color:Math.random()<0.5 ? pick(BLUE) : pick(PINK),
      phase:Math.random()*Math.PI*2,
      speed:1 + Math.random()*2
    });
  }
  const nB = Math.max(70,Math.floor(W * H / 14000));
  for(let i=0;i<nB;i++){
    bgStars.push({
      nx:Math.random(), ny:Math.random()*0.85,
      size:0.5 + Math.random()*1.6,
      phase:Math.random()*Math.PI*2,
      speed:0.8 + Math.random()*1.8,
      big:Math.random()<0.12
    });
  }
  for(let i=0;i<70;i++){
    floorDots.push({
      ang:Math.random()*Math.PI*2,
      dist:0.15 + Math.random()*1.15,
      yJit:(Math.random()-0.5)*0.12,
      size:1.2 + Math.random()*3.2,
      color:Math.random()<0.45 ? pick(BLUE):Math.random()<0.9 ? pick(PINK):pick(WHITE),
      phase:Math.random()*Math.PI*2,
      speed:0.6 + Math.random()*1.4
    });
  }
  for(let i=0;i<42;i++){
    floaters.push({
      ang:Math.random()*Math.PI*2,
      dist:0.3 + Math.random()*1.2,
      t:Math.random(),
      size:0.5 + Math.random()*1.1,
      color:Math.random()<0.5 ? pick(BLUE) : pick(PINK),
      phase:Math.random()*Math.PI*2,
      speed:0.04 + Math.random()*0.08
    });
  }
}

function resize(){
  DPR = Math.min(window.devicePixelRatio || 1,2);
  W = window.innerWidth;
  H = window.innerHeight;
  canvas.width = Math.round(W * DPR);
  canvas.height = Math.round(H * DPR);
  canvas.style.width = W + 'px';
  canvas.style.height = H + 'px';
  ctx.setTransform(DPR,0,0,DPR,0,0);
  const scale = Math.min(W,H);
  treeH = H * 0.66;
  baseR = Math.min(scale * 0.30, W * 0.36);
  floorY = H * 0.86;
  topY = floorY - treeH;
  cx = W / 2;
  build();
}

function conePos(t, theta, rho, rot){
  const r = Math.max(baseR * t,2);
  const th = theta + rot;
  const x3 = Math.cos(th) * r * rho;
  const z3 = Math.sin(th) * r;
  const depth = Math.max(0,Math.min(1,(z3 / r + 1)/2));
  return { x: cx + x3, y: topY + t * treeH, depth: depth }
}

function paintBg(time){
  const g = ctx.createLinearGradient(0,0,0,H);
  g.addColorStop(0,'#060312');
  g.addColorStop(0.55,'#04020c');
  g.addColorStop(1,'#010007');
  ctx.fillStyle = g;
  ctx.fillRect(0,0,W,H);
  ctx.globalCompositeOperation = 'lighter';
  let h1 = ctx.createRadialGradient(cx - baseR * 0.4, topY + treeH * 0.45,0, cx - baseR * 0.4, topY + treeH * 0.45, treeH * 0.6);
  h1.addColorStop(0,'rgba(90,140,255,0.10)');
  ctx.fillStyle = h1;
  ctx.fillRect(0,0,W,H);
  let h2 = ctx.createRadialGradient(cx + baseR * 0.4, topY + treeH * 0.6,0, cx + baseR * 0.4, topY + treeH * 0.6, treeH * 0.6);
  h2.addColorStop(1,'rgba(255,140,210,0.09)');
  ctx.fillStyle = h2;
  ctx.fillRect(0,0,W,H);
  ctx.globalCompositeOperation = 'source-over';
}

function drawBgStars(time){
  ctx.globalCompositeOperation = 'lighter';
  for(const s of bgStars){
    const a = 0.25 + 0.75 * (0.5 + 0.5 * Math.sin(time * s.speed + s.phase));
    const x = s.nx * W, y = s.ny * H;
    ctx.globalAlpha = a * 0.8;
    ctx.fillStyle = '#fdfeff';
    ctx.beginPath();
    ctx.arc(x,y,s.size,0,Math.PI*2);
    ctx.fill();
    if(s.big){
      ctx.globalAlpha = a * 0.5;
      ctx.strokeStyle = '#ffffff';
      ctx.lineWidth = 0.6;
      const l = s.size * 4;
      ctx.beginPath();
      ctx.moveTo(x - l, y); ctx.lineTo(x + l, y);
      ctx.moveTo(x, y - l); ctx.lineTo(x, y + l);
      ctx.stroke();
    }
  }
  ctx.globalAlpha = 1;
  ctx.globalCompositeOperation = 'source-over';
}

function drawFloor(time){
  ctx.globalCompositeOperation = 'lighter';
  ctx.save();
  ctx.translate(cx, floorY + baseR * 0.05);
  ctx.scale(1,0.26);
  const g = ctx.createRadialGradient(0,0,0,0,0,baseR * 1.35);
  g.addColorStop(0,'rgba(160,190,255,0.20)');
  g.addColorStop(0.5,'rgba(220,150,230,0.08)');
  g.addColorStop(1,'rgba(160,190,255,0)');
  ctx.fillStyle = g;
  ctx.beginPath();
  ctx.arc(0,0,baseR*1.35,0,Math.PI*2);
  ctx.fill();
  ctx.restore();
  for(const d of floorDots){
    const sway = Math.sin(time * 0.3 + d.phase) * 0.05;
    const x = cx + Math.cos(d.ang) * d.dist * baseR * (1 + sway);
    const y = floorY + d.yJit * baseR + Math.sin(d.ang * 2) * baseR * 0.05;
    const a = 0.15 + 0.6 * (0.5 + 0.5 * Math.sin(time * d.speed + d.phase));
    ctx.globalAlpha = a;
    ctx.fillStyle = d.color;
    ctx.beginPath();
    ctx.arc(x,y,d.size,0,Math.PI*2);
    ctx.fill();
  }
  ctx.globalAlpha = 1;
  ctx.globalCompositeOperation = 'source-over';
}

function drawConeGlow(){
  ctx.globalCompositeOperation = 'lighter';
  const g = ctx.createLinearGradient(0,topY,0,floorY);
  g.addColorStop(0,'rgba(180,160,255,0.05)');
  g.addColorStop(1,'rgba(255,170,225,0.10)');
  ctx.fillStyle = g;
  ctx.beginPath();
  ctx.moveTo(cx, topY);
  ctx.lineTo(cx - baseR * 1.02, floorY);
  ctx.lineTo(cx + baseR * 1.02, floorY);
  ctx.closePath();
  ctx.fill();
  ctx.globalCompositeOperation = 'source-over';
}

function drawTree(time, rot){
  ctx.globalCompositeOperation = 'lighter';
  for(const p of particles){
    const q = conePos(p.t, p.theta, p.rho, rot);
    const tw = 0.55 + 0.45 * (0.5 + 0.5 * Math.sin(time * p.speed + p.phase));
    const depth = 0.45 + 0.55 * q.depth;
    ctx.globalAlpha = tw * depth * 0.9;
    ctx.fillStyle = p.color;
    const s = p.size * (0.7 + 0.5 * q.depth);
    ctx.beginPath();
    ctx.arc(q.x,q.y,s,0,Math.PI*2);
    ctx.fill();
  }
  ctx.globalAlpha = 1;
  ctx.globalCompositeOperation = 'source-over';
}

function drawRibbons(time, rot){
  ctx.globalCompositeOperation = 'lighter';
  for(const rb of RIBBONS){
    for(let i=0;i<rb.sample;i++){
      const s = i / (rb.sample - 1);
      const t = 0.05 + s * 0.93;
      const r = baseR * t * 1.02;
      const th = s * rb.turns * Math.PI * 2 + rb.offset + rot;
      const x = cx + Math.cos(th) * r;
      const y = topY + t * treeH;
      const z3 = Math.sin(th) * r;
      const depth = Math.max(0,(z3 / r + 1)/2);
      const tw = 0.6 + 0.4 * (0.5 + 0.5 * Math.sin(time * 2 + s * 6 + rb.offset));
      const alpha = tw * (0.35 + 0.65 * depth);
      ctx.fillStyle = rb.color;
      ctx.globalAlpha = alpha * 0.32;
      ctx.beginPath();
      ctx.arc(x,y,rb.width * 2.2,0,Math.PI*2);
      ctx.fill();
      ctx.globalAlpha = alpha;
      ctx.beginPath();
      ctx.arc(x,y,rb.width * (0.8 + 0.4 * depth),0,Math.PI*2);
      ctx.fill();
    }
  }
  ctx.globalAlpha = 1;
  ctx.globalCompositeOperation = 'source-over';
}

function drawOrnaments(time, rot){
  ctx.globalCompositeOperation = 'lighter';
  for(const o of ornaments){
    const q = conePos(o.t, o.theta, o.rho, rot);
    const tw = 0.5 + 0.5 * (0.5 + 0.5 * Math.sin(time * o.speed + o.phase));
    ctx.globalAlpha = 0.35 + 0.65 * q.depth;
    ctx.shadowBlur = 12 * q.depth;
    ctx.shadowColor = o.color;
    ctx.fillStyle = o.color;
    ctx.beginPath();
    ctx.arc(q.x,q.y,o.size*(0.8+0.4*q.depth),0,Math.PI*2);
    ctx.fill();
    ctx.shadowBlur = 0;
    ctx.globalAlpha = tw * 0.9;
    ctx.fillStyle = '#ffffff';
    ctx.beginPath();
    ctx.arc(q.x - o.size*0.25, q.y - o.size*0.25, o.size*0.3,0,Math.PI*2);
    ctx.fill();
  }
  ctx.globalAlpha = 1;
  ctx.globalCompositeOperation = 'source-over';
}

function drawFloaters(time){
  ctx.globalCompositeOperation = 'lighter';
  for(const f of floaters){
    f.t -= f.speed * 0.016;
    if(f.t < 0){ f.t = 1; f.ang = Math.random()*Math.PI*2; }
    const y = topY + f.t * treeH;
    const x = cx + Math.cos(f.ang + time * 0.2) * f.dist * baseR * (1.15 - f.t * 0.4);
    const a = 0.25 + 0.5 * (1 - f.t);
    ctx.globalAlpha = a * (0.5 + 0.5 * Math.sin(time * 2 + f.phase));
    ctx.fillStyle = f.color;
    ctx.beginPath();
    ctx.arc(x,y,f.size,0,Math.PI*2);
    ctx.fill();
  }
  ctx.globalAlpha = 1;
  ctx.globalCompositeOperation = 'source-over';
}

function starPath(x, y, spikes, outer, inner, rot){
  ctx.beginPath();
  let ang = rot;
  const step = Math.PI / spikes;
  for(let i=0;i<spikes*2;i++){
    const r = i%2 ===0 ? outer : inner;
    const px = x + Math.cos(ang)*r;
    const py = y + Math.sin(ang)*r;
    if(i===0) ctx.moveTo(px,py); else ctx.lineTo(px,py);
    ang += step;
  }
  ctx.closePath();
}

function drawTopStar(time){
  const pulse = 1 + 0.12 * Math.sin(time * 2.2);
  const x = cx, y = topY - treeH * 0.02;
  ctx.globalCompositeOperation = 'lighter';
  const g = ctx.createRadialGradient(x,y,0,x,y,baseR*0.5*pulse);
  g.addColorStop(0,'rgba(255,235,250,0.5)');
  g.addColorStop(0.4,'rgba(255,190,230,0.18)');
  g.addColorStop(1,'rgba(255,190,230,0)');
  ctx.fillStyle = g;
  ctx.beginPath();
  ctx.arc(x,y,baseR*0.5*pulse,0,Math.PI*2);
  ctx.fill();
  ctx.shadowColor = '#ffe9fb';
  ctx.shadowBlur = 22;
  ctx.fillStyle = '#fff6fd';
  starPath(x,y,5,baseR*0.14*pulse,baseR*0.06*pulse,-Math.PI/2);
  ctx.fill();
  ctx.shadowBlur = 0;
  ctx.globalCompositeOperation = 'source-over';
}

function drawGreeting(){
  ctx.globalCompositeOperation = 'source-over';
  const text = 'Merry Christmas';
  const size = Math.max(14,Math.round(H*0.018));
  ctx.font = '300 ' + size + 'px "Trebuchet MS", Georgia, serif';
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  const g = ctx.createLinearGradient(cx-120,0,cx+120,0);
  g.addColorStop(0,'#9ecbff');
  g.addColorStop(0.5,'#ffd0ee');
  g.addColorStop(1,'#b7d3ff');
  ctx.fillStyle = g;
  ctx.globalAlpha = 0.75;
  ctx.fillText(text,cx,H*0.955);
  ctx.globalAlpha = 1;
}

function render(time, rot){
  paintBg(time);
  drawBgStars(time);
  drawFloor(time);
  drawConeGlow();
  drawTree(time, rot);
  drawRibbons(time, rot);
  drawOrnaments(time, rot);
  drawFloaters(time);
  drawTopStar(time);
  drawGreeting();
}

window.addEventListener('resize', resize);
resize();
render(0.6,0.2);
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if(reduceMotion){
  window.setTimeout(()=>render(1.2,0.3),120);
}else{
  const t0 = performance.now();
  function frame(now){
    const time = (now - t0)/1000;
    render(time, time * 0.18);
    requestAnimationFrame(frame);
  }
  requestAnimationFrame(frame);
}
</script>
</body>
</html>