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,maximum-scale=1,user-scalable=no,viewport-fit=cover">
<meta name="theme-color" content="#0a1622">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>鲸鱼娘 · 手机桌宠</title>
<style>
  *{box-sizing:border-box;-webkit-tap-highlight-color:transparent;}
  html,body{
    margin:0;height:100%;overflow:hidden;overscroll-behavior:none;
    background:radial-gradient(circle at 50% 38%, #1b3a57 0%, #0a1622 72%);
    touch-action:none;
    user-select:none;-webkit-user-select:none;
    font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;
  }
  canvas{display:block;position:fixed;inset:0;touch-action:none;}

  /* 台词气泡 */
  #bubble{
    position:fixed;left:0;top:0;
    transform:translate(-50%,-100%) scale(.7);
    background:rgba(255,255,255,.95);color:#12324a;
    padding:8px 15px;border-radius:15px;
    font-size:15px;white-space:nowrap;
    opacity:0;pointer-events:none;
    box-shadow:0 8px 22px rgba(0,0,0,.4);
    transition:transform .22s cubic-bezier(.3,1.6,.6,1),opacity .22s;
    will-change:transform,opacity;
  }
  #bubble.show{opacity:1;transform:translate(-50%,-100%) scale(1);}
  #bubble::after{
    content:"";position:absolute;left:50%;bottom:-7px;
    transform:translateX(-50%);
    border:8px solid transparent;border-top-color:rgba(255,255,255,.95);border-bottom:0;
  }

  /* 底部提示 */
  #hint{
    position:fixed;left:50%;bottom:calc(24px + env(safe-area-inset-bottom));
    transform:translateX(-50%);
    color:rgba(190,232,255,.75);font-size:13px;letter-spacing:.5px;
    white-space:nowrap;pointer-events:none;
    transition:opacity .8s;text-shadow:0 2px 10px rgba(0,0,0,.5);
  }
  #hint.hide{opacity:0;}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="bubble"></div>
<div id="hint">轻点鲸鱼娘 · 拖动让她看你</div>

<script>
(function(){
  'use strict';

  const canvas = document.getElementById('c');
  const ctx    = canvas.getContext('2d');
  const bubble = document.getElementById('bubble');
  const hint   = document.getElementById('hint');

  let vw = 0, vh = 0, scale = 1, dpr = 1;

  function resize(){
    vw = window.innerWidth;
    vh = window.innerHeight;
    dpr = Math.min(window.devicePixelRatio || 1, 2.5);
    canvas.width  = Math.round(vw * dpr);
    canvas.height = Math.round(vh * dpr);
    canvas.style.width  = vw + 'px';
    canvas.style.height = vh + 'px';

    scale = Math.min(vw / 340, vh / 430);
    scale = Math.max(scale, 0.45);

    placeBubble();
  }
  window.addEventListener('resize', resize);
  window.addEventListener('orientationchange', () => setTimeout(resize, 220));

  function originY(){ return vh / 2 + 15 * scale; }
  function headTopY(){ return originY() - 158 * scale; }

  function placeBubble(){
    bubble.style.left = (vw / 2) + 'px';
    bubble.style.top  = headTopY() + 'px';
  }

  const t0 = performance.now();
  let last = t0;

  let blinkP = 0, blinkLeft = 0, nextBlink = 1.6;
  const look  = { x: 0, y: 0 };
  const lookT = { x: 0, y: 0 };
  let jumpY = 0, jumpV = 0;
  let spoutAcc = 0;
  const particles = [];
  let sayTimer = null, pressTimer = null, lookResetT = null;

  const LINES = [
    '噗噜噜~',
    '今天也要加油哦!',
    '摸头…有点害羞啦',
    '要一起游泳吗?',
    '咕噜咕噜…',
    '累了就歇一会儿嘛~',
    '我是鲸鱼娘小汐!',
    '呼——喷水!',
    '海水咸咸的~',
    '手指…在跟着你哦'
  ];

  const clamp = (v, a, b) => v < a ? a : v > b ? b : v;

  canvas.addEventListener('pointermove', e => {
    const dx = (e.clientX - vw / 2) / (150 * scale);
    const dy = (e.clientY - originY()) / (150 * scale);
    lookT.x = clamp(dx, -1, 1);
    lookT.y = clamp(dy, -1, 1);
  });

  canvas.addEventListener('pointerdown', e => {
    e.preventDefault();
    if (jumpY === 0 && jumpV === 0) jumpV = -330;
    burst(24, 1);
    say(LINES[(Math.random() * LINES.length) | 0]);
    hint.classList.add('hide');

    clearInterval(pressTimer);
    pressTimer = setInterval(() => burst(10, 0.75), 320); 
    clearTimeout(lookResetT);
  });

  function endPress(){
    clearInterval(pressTimer);
    pressTimer = null;
    clearTimeout(lookResetT);
    lookResetT = setTimeout(() => { lookT.x = 0; lookT.y = 0; }, 420);
  }
  window.addEventListener('pointerup', endPress);
  window.addEventListener('pointercancel', endPress);

  window.addEventListener('keydown', e => {
    if (e.code === 'Space'){ e.preventDefault(); burst(20, 1); say('噗——!'); }
  });

  setTimeout(() => hint.classList.add('hide'), 5000);

  function say(text){
    bubble.textContent = text;
    placeBubble();
    bubble.classList.add('show');
    clearTimeout(sayTimer);
    sayTimer = setTimeout(() => bubble.classList.remove('show'), 1900);
  }

  function burst(n, power){
    for (let i = 0; i < n; i++){
      const life = 1.0 + Math.random() * 0.7;
      particles.push({
        x: (Math.random() - 0.5) * 16,
        y: -156 + Math.random() * 8,
        vx: (Math.random() - 0.5) * 230 * power,
        vy: (-160 - Math.random() * 180) * power,
        life, max: life,
        size: 2 + Math.random() * 3.4,
        hue: 190 + Math.random() * 25
      });
    }
    if (particles.length > 220) particles.splice(0, particles.length - 220);
  }

  function frame(now){
    const dt = Math.min(0.05, (now - last) / 1000);
    last = now;
    const t = (now - t0) / 1000;

    nextBlink -= dt;
    if (nextBlink <= 0 && blinkLeft <= 0){
      blinkLeft = 0.16;
      nextBlink = 2 + Math.random() * 4;
    }
    if (blinkLeft > 0){
      blinkLeft -= dt;
      const p = Math.min(1, Math.max(0, 1 - blinkLeft / 0.16));
      blinkP = Math.sin(Math.PI * p);
    } else blinkP = 0;

    const k = 1 - Math.pow(0.001, dt);
    look.x += (lookT.x - look.x) * k;
    look.y += (lookT.y - look.y) * k;

    if (jumpY < 0 || jumpV !== 0){
      jumpV += 1400 * dt;
      jumpY += jumpV * dt;
      if (jumpY >= 0 && jumpV > 0){ jumpY = 0; jumpV = 0; }
    }

    spoutAcc += dt * 7;
    while (spoutAcc >= 1){
      spoutAcc -= 1;
      const life = 0.85 + Math.random() * 0.5;
      particles.push({
        x: (Math.random() - 0.5) * 14,
        y: -158 + Math.random() * 6,
        vx: (Math.random() - 0.5) * 55,
        vy: -135 - Math.random() * 75,
        life, max: life,
        size: 1.4 + Math.random() * 2.3,
        hue: 190 + Math.random() * 25
      });
    }
    if (particles.length > 220) particles.splice(0, particles.length - 220);

    for (let i = particles.length - 1; i >= 0; i--){
      const p = particles[i];
      p.vy += 430 * dt;
      p.x  += p.vx * dt;
      p.y  += p.vy * dt;
      p.life -= dt;
      if (p.life <= 0) particles.splice(i, 1);
    }

    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    ctx.clearRect(0, 0, vw, vh);

    ctx.save();
    ctx.translate(vw / 2, originY());
    ctx.scale(scale, scale);

    drawBackground(t);
    drawGirl(t, blinkP);
    drawParticles();

    ctx.restore();

    requestAnimationFrame(frame);
  }

  function drawBackground(t){
    ctx.save();
    ctx.strokeStyle = '#9FE8FF';
    ctx.lineWidth = 1.4;
    for (let i = 0; i < 14; i++){
      const seed = i * 2.399;
      const x = Math.sin(seed) * 165 + Math.sin(t * 0.6 + i) * 8;
      const y = 210 - ((t * 20 + i * 88) % 470);
      const r = 3 + (i % 4) * 2.6;
      ctx.globalAlpha = 0.10 + (i % 3) * 0.05;
      ctx.beginPath();
      ctx.arc(x, y, r, 0, Math.PI * 2);
      ctx.stroke();
    }
    ctx.restore();
  }

  const HY = -88;
  const FY = -80;

  function drawArm(s, t){
    ctx.save();
    ctx.translate(s * 40, 12);
    ctx.rotate(s * (0.25 + Math.sin(t * 2 + s) * 0.12));
    ctx.fillStyle = '#CDEEFC';
    ctx.beginPath();
    ctx.ellipse(0, 14, 10, 17, 0, 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();
  }

  function drawEye(cx, cy, blink, lx, ly){
    const open = 1 - blink;
    if (open < 0.12){
      ctx.beginPath();
      ctx.moveTo(cx - 11, cy);
      ctx.quadraticCurveTo(cx, cy + 7, cx + 11, cy);
      ctx.strokeStyle = '#2C5570';
      ctx.lineWidth = 3;
      ctx.lineCap = 'round';
      ctx.stroke();
      return;
    }
    const ry = 15 * open;

    ctx.beginPath();
    ctx.ellipse(cx, cy, 12, ry, 0, 0, Math.PI * 2);
    ctx.fillStyle = '#FFFFFF';
    ctx.fill();

    ctx.save();
    ctx.beginPath();
    ctx.ellipse(cx, cy, 12, ry, 0, 0, Math.PI * 2);
    ctx.clip();

    const g = ctx.createLinearGradient(0, cy - ry, 0, cy + ry);
    g.addColorStop(0,    '#3E9BD6');
    g.addColorStop(0.55, '#155C8F');
    g.addColorStop(1,    '#0A2B44');
    ctx.fillStyle = g;
    ctx.beginPath();
    ctx.ellipse(cx + lx * 4, cy + ly * 3, 9, ry * 0.92, 0, 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();

    ctx.fillStyle = 'rgba(255,255,255,0.95)';
    ctx.beginPath();
    ctx.arc(cx + lx * 4 - 3.2, cy + ly * 3 - ry * 0.4, 3.4, 0, Math.PI * 2);
    ctx.fill();

    ctx.fillStyle = 'rgba(255,255,255,0.65)';
    ctx.beginPath();
    ctx.arc(cx + lx * 4 + 3.5, cy + ly * 3 + ry * 0.35, 1.9, 0, Math.PI * 2);
    ctx.fill();
  }

  function drawGirl(t, blink){
    const lx = look.x, ly = look.y;

    ctx.beginPath();
    ctx.ellipse(0, 118, 42, 9, 0, 0, Math.PI * 2);
    ctx.fillStyle = 'rgba(0,0,0,0.20)';
    ctx.fill();

    ctx.save();
    ctx.translate(0, 40);
    ctx.rotate(Math.sin(t * 1.6) * 0.16);
    const tg = ctx.createLinearGradient(0, 0, 0, 80);
    tg.addColorStop(0, '#8FE3FF');
    tg.addColorStop(1, '#2AA0DC');
    ctx.fillStyle = tg;
    ctx.beginPath();
    ctx.moveTo(0, -12);
    ctx.bezierCurveTo(-8, 18, -28, 44, -52, 58);
    ctx.bezierCurveTo(-32, 64, -10, 52, 0, 36);
    ctx.bezierCurveTo(10, 52, 32, 64, 52, 58);
    ctx.bezierCurveTo(28, 44, 8, 18, 0, -12);
    ctx.closePath();
    ctx.fill();
    ctx.restore();

    const bodyG = ctx.createLinearGradient(0, -20, 0, 80);
    bodyG.addColorStop(0, '#F0FBFF');
    bodyG.addColorStop(1, '#A9DFF7');
    ctx.fillStyle = bodyG;
    ctx.beginPath();
    ctx.ellipse(0, 26, 44, 48, 0, 0, Math.PI * 2);
    ctx.fill();

    ctx.fillStyle = '#FFFFFF';
    ctx.beginPath();
    ctx.ellipse(0, 36, 30, 32, 0, 0, Math.PI * 2);
    ctx.fill();

    drawArm(-1, t);
    drawArm( 1, t);

    const hairG = ctx.createLinearGradient(0, HY - 72, 0, HY + 72);
    hairG.addColorStop(0, '#7FD6F7');
    hairG.addColorStop(1, '#2A9BD6');

    ctx.fillStyle = hairG;
    ctx.beginPath();
    ctx.ellipse(0, HY, 70, 70, 0, 0, Math.PI * 2);
    ctx.fill();

    [-1, 1].forEach(s => {
      ctx.save();
      ctx.translate(s * 64, HY + 14);
      ctx.rotate(s * (0.4 + Math.sin(t * 2 + s * 1.3) * 0.1));
      ctx.fillStyle = '#A5E7FF';
      ctx.beginPath();
      ctx.ellipse(s * 8, 0, 22, 13, 0, 0, Math.PI * 2);
      ctx.fill();
      ctx.restore();
    });

    [-1, 1].forEach(s => {
      ctx.fillStyle = hairG;
      ctx.beginPath();
      ctx.moveTo(s * 56, FY - 32);
      ctx.quadraticCurveTo(s * 76, FY + 18, s * 62, FY + 62);
      ctx.quadraticCurveTo(s * 50, FY + 28, s * 44, FY - 12);
      ctx.closePath();
      ctx.fill();
    });

    ctx.fillStyle = '#FFE9DF';
    ctx.beginPath();
    ctx.ellipse(0, FY, 58, 54, 0, 0, Math.PI * 2);
    ctx.fill();

    ctx.fillStyle = hairG;
    ctx.beginPath();
    ctx.arc(0, FY, 60, Math.PI, 0);
    ctx.quadraticCurveTo(58, FY - 4,  40, FY - 16);
    ctx.quadraticCurveTo(28, FY - 40, 16, FY - 12);
    ctx.quadraticCurveTo(4,  FY - 44,-10, FY - 12);
    ctx.quadraticCurveTo(-24, FY - 40,-36, FY - 16);
    ctx.quadraticCurveTo(-56, FY - 4, -60, FY);
    ctx.closePath();
    ctx.fill();

    drawEye(-26, FY + 20, blink, lx, ly);
    drawEye( 26, FY + 20, blink, lx, ly);

    [-1, 1].forEach(s => {
      ctx.beginPath();
      ctx.ellipse(s * 40, FY + 34, 11, 6.5, 0, 0, Math.PI * 2);
      ctx.fillStyle = 'rgba(255,145,170,0.42)';
      ctx.fill();
    });

    ctx.beginPath();
    ctx.moveTo(-8, FY + 40);
    ctx.quadraticCurveTo(0, FY + 49, 8, FY + 40);
    ctx.strokeStyle = '#D9737D';
    ctx.lineWidth = 2.6;
    ctx.lineCap = 'round';
    ctx.stroke();
  }

  function drawParticles(){
    for (const p of particles){
      const a = Math.max(0, p.life / p.max);
      ctx.globalAlpha = a;
      ctx.fillStyle = 'hsl(' + p.hue + ',100%,78%)';
      ctx.beginPath();
      ctx.arc(p.x, p.y, p.size * (0.4 + 0.6 * a), 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.globalAlpha = 1;
  }

  resize();
  requestAnimationFrame(frame);
})();
</script>
</body>
</html>