← Mike Higgins Home
Custom Code Experiment •

Dragon 1: Canvas

Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dragon 1: Canvas</title>
<style>
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

  body {
    background: #0a0000;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
    overflow: hidden;
  }

  .scene {
    position: relative;
    width: 100vw;
    height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    background: radial-gradient(ellipse at 50% 55%, #2a0000 0%, #0a0000 70%);
  }

  #fire-canvas {
    position: fixed;
    inset: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
    z-index: 10;
  }

  .flag-panel {
    position: relative;
    width: 540px;
    height: 340px;
    background: linear-gradient(180deg, #c8000e 0%, #c8000e 50%, #eee8d0 50%, #eee8d0 100%);
    border: 2px solid rgba(80,40,0,0.5);
    box-shadow:
      0 0 0 5px #0a0000,
      0 0 80px 30px rgba(200,0,10,0.25);
    border-radius: 3px;
    overflow: visible;
  }

  .flag-panel::after {
    content: '';
    position: absolute;
    inset: 0;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4'%3E%3Crect width='1' height='4' fill='rgba(0,0,0,0.04)'/%3E%3C/svg%3E");
    pointer-events: none;
    border-radius: 3px;
  }

  .dragon-wrap {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 380px;
    height: 300px;
    animation: breathe 3.2s ease-in-out infinite;
    filter: drop-shadow(0 6px 20px rgba(0,0,0,0.7));
    overflow: visible;
  }

  .tail-group {
    animation: tail-swish 3s ease-in-out infinite;
    transform-origin: 265px 188px;
  }

  .wing-upper {
    animation: wing-beat 3.2s ease-in-out infinite;
    transform-origin: 185px 112px;
  }

  .dragon-eye {
    animation: eye-pulse 1.8s ease-in-out infinite alternate;
  }
  .eye-glow {
    animation: eye-glow-anim 1.8s ease-in-out infinite alternate;
  }

  @keyframes breathe {
    0%,100% { transform: translate(-50%,-50%) scale(1) rotate(0deg); }
    40%      { transform: translate(-50%,-51.5%) scale(1.012) rotate(0.3deg); }
    70%      { transform: translate(-50%,-50.5%) scale(1.008) rotate(-0.2deg); }
  }

  @keyframes tail-swish {
    0%   { transform: rotate(0deg); }
    25%  { transform: rotate(5deg); }
    75%  { transform: rotate(-6deg); }
    100% { transform: rotate(0deg); }
  }

  @keyframes wing-beat {
    0%,100% { transform: rotate(0deg) scaleY(1); }
    30%     { transform: rotate(-4deg) scaleY(1.05); }
    65%     { transform: rotate(3deg) scaleY(0.96); }
  }

  @keyframes eye-pulse {
    from { fill: #ff5500; }
    to   { fill: #ffdd00; }
  }

  @keyframes eye-glow-anim {
    from { opacity: 0.4; r: 7; }
    to   { opacity: 0.9; r: 11; }
  }

  .scene { transition: background 0.08s; }
</style>
</head>
<body>
<div class="scene" id="scene">
  <canvas id="fire-canvas"></canvas>

  <div class="flag-panel">
    <div class="dragon-wrap">
      <svg id="dragon-svg" viewBox="0 0 380 300" xmlns="http://www.w3.org/2000/svg" overflow="visible">
        <defs>
          <radialGradient id="body-grad" cx="50%" cy="35%" r="65%">
            <stop offset="0%" stop-color="#ee001c"/>
            <stop offset="100%" stop-color="#7a0000"/>
          </radialGradient>
          <radialGradient id="wing-grad" cx="25%" cy="25%" r="75%">
            <stop offset="0%" stop-color="#cc001a" stop-opacity="0.92"/>
            <stop offset="100%" stop-color="#4a0000" stop-opacity="0.72"/>
          </radialGradient>
          <radialGradient id="eye-glow-grad" cx="50%" cy="50%" r="50%">
            <stop offset="0%" stop-color="#ffdd00" stop-opacity="1"/>
            <stop offset="100%" stop-color="#ff4400" stop-opacity="0"/>
          </radialGradient>
          <filter id="blur-soft">
            <feGaussianBlur stdDeviation="1.5"/>
          </filter>
        </defs>

        <g class="tail-group">
          <path d="M 255 188 Q 302 204 334 178 Q 358 158 348 132 Q 340 114 324 122 Q 310 132 320 150 Q 328 164 316 172 Q 296 184 272 177 Z"
                fill="url(#body-grad)" stroke="#600000" stroke-width="1.5"/>
          <polygon points="332,172 343,154 325,167" fill="#bb001a" stroke="#600000" stroke-width="1"/>
          <polygon points="346,149 355,132 338,146" fill="#bb001a" stroke="#600000" stroke-width="1"/>
          <polygon points="317,172 307,157 312,174" fill="#bb001a" stroke="#600000" stroke-width="1"/>
        </g>

        <g class="wing-upper">
          <path d="M 192 118 Q 232 62 294 46 Q 314 40 312 62 Q 293 77 283 92 Q 313 72 328 88 Q 333 104 312 112 Q 333 98 341 115 Q 341 132 318 133 Q 293 135 268 143 Q 232 151 202 151 Z"
                fill="url(#wing-grad)" stroke="#600000" stroke-width="1.5" stroke-linejoin="round"/>
          <path d="M 202 151 Q 242 112 294 46"  fill="none" stroke="#880010" stroke-width="0.8" opacity="0.5"/>
          <path d="M 212 148 Q 258 118 312 112" fill="none" stroke="#880010" stroke-width="0.8" opacity="0.5"/>
          <path d="M 222 145 Q 264 123 318 133" fill="none" stroke="#880010" stroke-width="0.8" opacity="0.4"/>
          <line x1="192" y1="118" x2="294" y2="46"  stroke="#600000" stroke-width="1.8"/>
          <line x1="197" y1="123" x2="328" y2="88"  stroke="#600000" stroke-width="1.8"/>
          <line x1="202" y1="134" x2="341" y2="115" stroke="#600000" stroke-width="1.8"/>
          <line x1="202" y1="144" x2="318" y2="133" stroke="#600000" stroke-width="1.8"/>
        </g>

        <path d="M 197 168 Q 244 194 268 214 Q 248 224 217 213 Q 196 203 192 182 Z"
              fill="url(#wing-grad)" stroke="#600000" stroke-width="1" opacity="0.75"/>

        <path d="M 238 198 Q 258 218 251 242 Q 247 254 241 256 L 238 256 Q 241 244 243 235 Q 239 220 230 212 Z"
              fill="url(#body-grad)" stroke="#600000" stroke-width="1.5"/>
        <path d="M 238 256 Q 232 265 228 262 Q 230 255 238 256" fill="#4a0000"/>
        <path d="M 241 256 Q 240 266 236 265 Q 236 257 241 256" fill="#4a0000"/>
        <path d="M 244 254 Q 246 264 242 264 Q 241 256 244 254" fill="#4a0000"/>

        <path d="M 168 178 Q 147 204 140 232 Q 136 246 132 250 L 129 250 Q 132 238 134 230 Q 130 208 147 188 Z"
              fill="url(#body-grad)" stroke="#600000" stroke-width="1.5"/>
        <path d="M 130 250 Q 122 258 119 256 Q 122 249 130 250" fill="#4a0000"/>
        <path d="M 132 250 Q 128 260 124 259 Q 125 251 132 250" fill="#4a0000"/>
        <path d="M 135 248 Q 134 258 130 258 Q 130 250 135 248" fill="#4a0000"/>

        <ellipse cx="202" cy="182" rx="74" ry="53" fill="url(#body-grad)" stroke="#600000" stroke-width="2"/>
        <path d="M 157 167 Q 178 157 202 162 Q 226 157 247 167" fill="none" stroke="#990010" stroke-width="1" opacity="0.45"/>
        <path d="M 150 182 Q 174 172 202 177 Q 230 172 254 182" fill="none" stroke="#990010" stroke-width="1" opacity="0.45"/>
        <path d="M 154 197 Q 177 187 202 192 Q 227 187 250 197" fill="none" stroke="#990010" stroke-width="1" opacity="0.4"/>

        <path d="M 158 152 Q 150 130 147 110 Q 145 92 152 82 Q 160 72 170 77 Q 182 82 180 102 Q 177 122 177 152 Z"
              fill="url(#body-grad)" stroke="#600000" stroke-width="1.5"/>
        <path d="M 155 142 Q 166 134 178 142" fill="none" stroke="#990010" stroke-width="1" opacity="0.45"/>
        <path d="M 152 126 Q 164 118 177 126" fill="none" stroke="#990010" stroke-width="1" opacity="0.45"/>
        <path d="M 152 110 Q 163 103 175 110" fill="none" stroke="#990010" stroke-width="1" opacity="0.4"/>
        <polygon points="154,140 148,126 157,138" fill="#bb001a" stroke="#600000" stroke-width="1"/>
        <polygon points="151,124 145,110 154,122" fill="#bb001a" stroke="#600000" stroke-width="1"/>
        <polygon points="152,108 146,94 155,106" fill="#bb001a" stroke="#600000" stroke-width="1"/>

        <path d="M 112 94 Q 120 70 140 62 Q 157 56 167 64 Q 180 74 177 90 Q 174 106 160 112 Q 142 118 124 112 Q 112 106 112 94 Z"
              fill="url(#body-grad)" stroke="#600000" stroke-width="2"/>
        <path d="M 112 94 Q 97 96 82 108 Q 74 118 78 128 Q 84 135 97 131 Q 110 124 114 112 Z"
              fill="url(#body-grad)" stroke="#600000" stroke-width="1.5"/>
        <path d="M 82 114 Q 97 108 114 110" fill="none" stroke="#600000" stroke-width="1.5"/>
        <ellipse cx="85" cy="110" rx="3" ry="2" fill="#3a0000"/>
        <ellipse cx="92" cy="108" rx="3" ry="2" fill="#3a0000"/>

        <polygon points="157,60 150,38 162,56" fill="#bb001a" stroke="#600000" stroke-width="1.5"/>
        <polygon points="147,58 138,38 150,54" fill="#bb001a" stroke="#600000" stroke-width="1.5"/>
        <polygon points="167,66 162,45 170,64" fill="#bb001a" stroke="#600000" stroke-width="1.5"/>
        <path d="M 162 68 Q 174 54 180 64 Q 174 70 162 68" fill="#990016" stroke="#600000" stroke-width="1"/>

        <circle class="eye-glow" cx="150" cy="82" r="9" fill="url(#eye-glow-grad)" filter="url(#blur-soft)"/>
        <ellipse cx="150" cy="82" rx="8" ry="7" fill="#110000"/>
        <ellipse class="dragon-eye" cx="150" cy="82" rx="5.5" ry="5.5" fill="#ff5500"/>
        <ellipse cx="150" cy="82" rx="2" ry="5" fill="#050000"/>
        <ellipse cx="148" cy="79" rx="1.5" ry="1.5" fill="rgba(255,248,180,0.75)"/>

        <polygon points="97,114 94,125 99,114"  fill="#f0eada" stroke="#b89500" stroke-width="0.5"/>
        <polygon points="101,112 99,122 104,112" fill="#f0eada" stroke="#b89500" stroke-width="0.5"/>
        <polygon points="105,111 104,120 108,111" fill="#f0eada" stroke="#b89500" stroke-width="0.5"/>

        <path d="M 162 178 Q 187 191 197 213 Q 182 218 167 208 Q 158 198 160 182 Z"
              fill="#bb001a" stroke="#800000" stroke-width="1" opacity="0.55"/>

      </svg>
    </div>
  </div>
</div>

<script>
const canvas = document.getElementById('fire-canvas');
const ctx    = canvas.getContext('2d');

function resize() {
  canvas.width  = window.innerWidth;
  canvas.height = window.innerHeight;
}
resize();
window.addEventListener('resize', resize);

function getMouthPos() {
  const svg  = document.getElementById('dragon-svg');
  const rect = svg.getBoundingClientRect();
  const sx = rect.width  / 380;
  const sy = rect.height / 300;
  return {
    x: rect.left + 80 * sx,
    y: rect.top  + 118 * sy
  };
}

const POOL_SIZE = 420;
const particles = [];

function resetParticle(p) {
  const m = getMouthPos();

  const speed  = 3.5 + Math.random() * 5.5;
  const angle  = Math.PI + (Math.random() - 0.5) * 0.55;
  const jitter = (Math.random() - 0.5) * 1.2;

  p.x    = m.x + (Math.random() - 0.5) * 6;
  p.y    = m.y + (Math.random() - 0.5) * 8;
  p.vx   = Math.cos(angle) * speed;
  p.vy   = Math.sin(angle) * speed + jitter;
  p.life = 0;
  p.maxLife = 0.55 + Math.random() * 0.55;
  p.size  = 6 + Math.random() * 22;
  p.type  = Math.random() < 0.18 ? 'spark' : 'flame';
  p.wobble = (Math.random() - 0.5) * 0.015;
  p.wobbleSpeed = 0.08 + Math.random() * 0.12;
  p.wobblePhase = Math.random() * Math.PI * 2;
}

for (let i = 0; i < POOL_SIZE; i++) {
  const p = {};
  resetParticle(p);
  p.life = Math.random() * p.maxLife;
  particles.push(p);
}

function flameColor(t, alpha) {
  let r, g, b;
  if (t < 0.15) {
    r = 255; g = 255; b = Math.round(200 * (1 - t / 0.15));
  } else if (t < 0.4) {
    const s = (t - 0.15) / 0.25;
    r = 255; g = Math.round(255 - s * 135); b = 0;
  } else if (t < 0.7) {
    const s = (t - 0.4) / 0.3;
    r = 255; g = Math.round(120 - s * 120); b = 0;
  } else {
    const s = (t - 0.7) / 0.3;
    r = Math.round(255 - s * 180); g = 0; b = 0;
  }
  return `rgba(${r},${g},${b},${alpha.toFixed(3)})`;
}

function sparkColor(t) {
  if (t < 0.3)  return `rgba(255,255,180,${(1-t*2).toFixed(3)})`;
  if (t < 0.6)  return `rgba(255,160,0,${(1-t).toFixed(3)})`;
  return `rgba(180,20,0,${((1-t)*0.6).toFixed(3)})`;
}

let lastTime = 0;
let frameCount = 0;

function draw(ts) {
  const dt = Math.min((ts - lastTime) / 1000, 0.05);
  lastTime = ts;
  frameCount++;

  ctx.clearRect(0, 0, canvas.width, canvas.height);

  const m = getMouthPos();

  const glow = ctx.createRadialGradient(m.x, m.y, 0, m.x, m.y, 120);
  glow.addColorStop(0,   'rgba(255,140,0,0.18)');
  glow.addColorStop(0.4, 'rgba(255,60,0,0.10)');
  glow.addColorStop(1,   'rgba(255,0,0,0)');
  ctx.fillStyle = glow;
  ctx.beginPath();
  ctx.ellipse(m.x - 40, m.y, 160, 80, 0, 0, Math.PI * 2);
  ctx.fill();

  particles.sort((a, b) => (a.type === 'spark' ? 1 : 0) - (b.type === 'spark' ? 1 : 0));

  for (const p of particles) {
    p.life += dt;
    if (p.life >= p.maxLife) { resetParticle(p); continue; }

    const t = p.life / p.maxLife;

    p.wobblePhase += p.wobbleSpeed;
    const perp = Math.sin(p.wobblePhase) * p.wobble * canvas.height;

    if (p.type === 'flame') {
      p.x += p.vx * (1 - t * 0.5) * dt * 60;
      p.y += (p.vy + perp) * (1 - t * 0.4) * dt * 60;
      p.vy -= 0.06 * dt * 60;
      p.vx *= 1 - 0.012 * dt * 60;
    } else {
      p.x += p.vx * dt * 60;
      p.y += p.vy * dt * 60;
      p.vy -= 0.12 * dt * 60;
      p.vx *= 1 - 0.008 * dt * 60;
    }

    let alpha;
    if (t < 0.08)      alpha = t / 0.08;
    else if (t < 0.65) alpha = 1;
    else               alpha = 1 - (t - 0.65) / 0.35;
    alpha = Math.max(0, alpha);

    const size = p.size * (1 + t * 0.6);

    if (p.type === 'flame') {
      const grad = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, size);
      grad.addColorStop(0,   flameColor(t, alpha * 0.9));
      grad.addColorStop(0.4, flameColor(t, alpha * 0.55));
      grad.addColorStop(1,   flameColor(t, 0));
      ctx.globalCompositeOperation = 'lighter';
      ctx.fillStyle = grad;
      ctx.beginPath();
      ctx.save();
      ctx.translate(p.x, p.y);
      ctx.rotate(Math.atan2(p.vy, p.vx));
      ctx.scale(1.6, 1);
      ctx.arc(0, 0, size, 0, Math.PI * 2);
      ctx.restore();
      ctx.fill();
      ctx.globalCompositeOperation = 'source-over';
    } else {
      ctx.globalCompositeOperation = 'lighter';
      ctx.strokeStyle = sparkColor(t);
      ctx.lineWidth   = Math.max(0.8, 2 * (1 - t));
      ctx.shadowColor = '#ffcc00';
      ctx.shadowBlur  = 6;
      ctx.beginPath();
      ctx.moveTo(p.x - p.vx * 3, p.y - p.vy * 3);
      ctx.lineTo(p.x, p.y);
      ctx.stroke();
      ctx.shadowBlur = 0;
      ctx.globalCompositeOperation = 'source-over';
    }
  }

  if (frameCount % 4 === 0) {
    const intensity = 0.12 + Math.random() * 0.14;
    document.getElementById('scene').style.background =
      `radial-gradient(ellipse at 50% 55%, rgba(${Math.round(40+intensity*80)},0,0,1) 0%, #0a0000 70%)`;
  }

  requestAnimationFrame(draw);
}

requestAnimationFrame(draw);
</script>
</body>
</html>