← Mike Higgins Home
Custom Code Experiment •

Music: As The Sting 2

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>Ast The Sting 2</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@2.44.0/tabler-icons.min.css">
<style>
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
  html, body {
    width: 100%; height: 100%;
    background: #05060f;
    display: flex; flex-direction: column;
    align-items: center; justify-content: center;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    overflow: hidden;
  }
  #three-canvas {
    width: 100%; height: calc(100vh - 120px);
    display: block;
  }
  #controls {
    width: 88%; max-width: 600px;
    display: flex; flex-direction: column;
    align-items: center; gap: 12px;
    padding: 10px 0 16px;
    flex-shrink: 0;
  }
  #track-info { text-align: center; }
  #track-title { font-size: 15px; font-weight: 500; color: rgba(255,255,255,0.9); letter-spacing: 0.04em; }
  #track-artist { font-size: 12px; color: rgba(255,255,255,0.4); margin-top: 3px; }
  #progress-wrap { width: 100%; display: flex; align-items: center; gap: 10px; }
  #time-cur, #time-dur {
    font-size: 11px; color: rgba(255,255,255,0.4);
    min-width: 34px; font-variant-numeric: tabular-nums;
  }
  #time-dur { text-align: right; }
  #progress-bar {
    flex: 1; height: 4px; background: rgba(255,255,255,0.12);
    border-radius: 2px; position: relative; cursor: pointer; overflow: visible;
  }
  #progress-fill { height: 100%; width: 0%; border-radius: 2px; background: rgba(255,255,255,0.75); pointer-events: none; transition: width 0.1s linear; }
  #progress-thumb {
    position: absolute; right: -6px; top: 50%; transform: translateY(-50%);
    width: 12px; height: 12px; border-radius: 50%; background: #fff;
    pointer-events: none; opacity: 0; transition: opacity 0.2s;
  }
  #progress-bar:hover #progress-thumb { opacity: 1; }
  #play-btn {
    width: 54px; height: 54px; border-radius: 50%;
    border: 1.5px solid rgba(255,255,255,0.25);
    background: rgba(255,255,255,0.06);
    cursor: pointer; display: flex; align-items: center; justify-content: center;
    font-size: 22px; color: rgba(255,255,255,0.9);
    transition: transform 0.12s, background 0.15s; flex-shrink: 0;
  }
  #play-btn:hover { background: rgba(255,255,255,0.12); transform: scale(1.06); }
  #play-btn:active { transform: scale(0.95); }
  #btn-row { display: flex; align-items: center; gap: 18px; }
  #vol-wrap { display: flex; align-items: center; gap: 8px; }
  #vol-icon { font-size: 16px; color: rgba(255,255,255,0.4); }
  #vol-slider {
    -webkit-appearance: none; appearance: none;
    width: 80px; height: 4px; background: rgba(255,255,255,0.15);
    border-radius: 2px; outline: none; cursor: pointer;
  }
  #vol-slider::-webkit-slider-thumb {
    -webkit-appearance: none; width: 12px; height: 12px;
    border-radius: 50%; background: rgba(255,255,255,0.85); cursor: pointer;
  }
  #vol-slider::-moz-range-thumb {
    width: 12px; height: 12px; border-radius: 50%;
    background: rgba(255,255,255,0.85); cursor: pointer; border: none;
  }
  #loading-msg {
    position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
    font-size: 13px; color: rgba(255,255,255,0.35); pointer-events: none; letter-spacing: 0.04em;
    z-index: 10;
  }
</style>
</head>
<body>
<div id="loading-msg">Click play to load…</div>
<canvas id="three-canvas"></canvas>

<div id="controls">
  <div id="track-info">
    <div id="track-title">As The Sting 2</div>
    <div id="track-artist">Mike H</div>
  </div>
  <div id="progress-wrap">
    <span id="time-cur">0:00</span>
    <div id="progress-bar">
      <div id="progress-fill"></div>
      <div id="progress-thumb"></div>
    </div>
    <span id="time-dur">0:00</span>
  </div>
  <div id="btn-row">
    <div id="vol-wrap">
      <i class="ti ti-volume" id="vol-icon" aria-hidden="true"></i>
      <input type="range" id="vol-slider" min="0" max="1" step="0.01" value="0.8">
    </div>
    <button id="play-btn" aria-label="Play">
      <i class="ti ti-player-play" id="play-icon"></i>
    </button>
    <div style="width:96px"></div>
  </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js"></script>
<script>
(function () {
  const AUDIO_URL = 'https://cdn.jsdelivr.net/gh/alaric-visi/music@main/as-the-sting/as-the-sting-2.mp3';

  // ---- Audio ----
  const playBtn = document.getElementById('play-btn');
  const playIcon = document.getElementById('play-icon');
  const progressBar = document.getElementById('progress-bar');
  const progressFill = document.getElementById('progress-fill');
  const timeCur = document.getElementById('time-cur');
  const timeDur = document.getElementById('time-dur');
  const loadingMsg = document.getElementById('loading-msg');
  const volSlider = document.getElementById('vol-slider');
  const volIcon = document.getElementById('vol-icon');

  let audioCtx, analyser, source, gainNode;
  let audioBuffer = null, isPlaying = false, startTime = 0, pauseOffset = 0, duration = 0;
  let freqData, waveData;
  let initialized = false;

  function fmt(t) {
    if (!isFinite(t) || t < 0) t = 0;
    return `${Math.floor(t / 60)}:${Math.floor(t % 60).toString().padStart(2, '0')}`;
  }

  async function initAudio() {
    if (initialized) return;
    initialized = true;
    audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    gainNode = audioCtx.createGain();
    gainNode.gain.value = parseFloat(volSlider.value);
    gainNode.connect(audioCtx.destination);
    analyser = audioCtx.createAnalyser();
    analyser.fftSize = 2048;
    analyser.smoothingTimeConstant = 0.82;
    analyser.connect(gainNode);
    freqData = new Uint8Array(analyser.frequencyBinCount);
    waveData = new Uint8Array(analyser.fftSize);
    loadingMsg.textContent = 'Loading audio…';
    try {
      const res = await fetch(AUDIO_URL);
      const ab = await res.arrayBuffer();
      audioBuffer = await audioCtx.decodeAudioData(ab);
      duration = audioBuffer.duration;
      timeDur.textContent = fmt(duration);
      loadingMsg.style.display = 'none';
      play();
    } catch (e) {
      loadingMsg.textContent = 'Could not load audio.';
      initialized = false;
    }
  }

  function play() {
    if (!audioBuffer) return;
    if (audioCtx.state === 'suspended') audioCtx.resume();
    source = audioCtx.createBufferSource();
    source.buffer = audioBuffer;
    source.connect(analyser);
    source.start(0, pauseOffset);
    startTime = audioCtx.currentTime - pauseOffset;
    isPlaying = true;
    playIcon.className = 'ti ti-player-pause';
    source.onended = () => { if (isPlaying) { pauseOffset = 0; isPlaying = false; playIcon.className = 'ti ti-player-play'; } };
  }

  function pause() {
    if (source) { source.onended = null; source.stop(); }
    pauseOffset = Math.max(0, audioCtx.currentTime - startTime);
    isPlaying = false;
    playIcon.className = 'ti ti-player-play';
  }

  playBtn.addEventListener('click', async () => {
    if (!initialized) { await initAudio(); return; }
    if (!audioBuffer) return;
    isPlaying ? pause() : play();
  });

  volSlider.addEventListener('input', () => {
    if (gainNode) gainNode.gain.value = parseFloat(volSlider.value);
    volIcon.className = parseFloat(volSlider.value) < 0.01 ? 'ti ti-volume-off' : 'ti ti-volume';
  });

  progressBar.addEventListener('click', (e) => {
    if (!audioBuffer) return;
    const rect = progressBar.getBoundingClientRect();
    const frac = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
    pauseOffset = frac * duration;
    if (isPlaying) { if (source) { source.onended = null; source.stop(); } play(); }
    else { timeCur.textContent = fmt(pauseOffset); progressFill.style.width = (frac * 100) + '%'; }
  });

  // ---- Three.js ----
  const canvas = document.getElementById('three-canvas');
  const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setClearColor(0x05060f, 1);

  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(50, 1, 0.1, 1000);
  camera.position.set(0, 1.5, 7);
  camera.lookAt(0, 0, 0);

  function resize() {
    const W = canvas.clientWidth, H = canvas.clientHeight;
    renderer.setSize(W, H, false);
    camera.aspect = W / H;
    camera.updateProjectionMatrix();
  }
  resize();
  window.addEventListener('resize', resize);

  // Wireframe material that we'll tint dynamically
  function wireMat(color) {
    return new THREE.MeshBasicMaterial({ color, wireframe: true, transparent: true, opacity: 0.85 });
  }

  const crabGroup = new THREE.Group();
  scene.add(crabGroup);

  // ---- Build crab geometry ----

  // Body (flattened ellipsoid)
  const bodyGeo = new THREE.SphereGeometry(1, 12, 8);
  bodyGeo.scale(1.4, 0.55, 1.0);
  const bodyMat = wireMat(0x00ffcc);
  const bodyMesh = new THREE.Mesh(bodyGeo, bodyMat);
  crabGroup.add(bodyMesh);

  // Carapace ridge lines (extra sphere slightly bigger)
  const ridgeGeo = new THREE.SphereGeometry(1.05, 8, 5);
  ridgeGeo.scale(1.38, 0.52, 0.98);
  const ridgeMesh = new THREE.Mesh(ridgeGeo, wireMat(0x00ccaa));
  crabGroup.add(ridgeMesh);

  // Eyes
  function makeEye(x) {
    const g = new THREE.SphereGeometry(0.12, 6, 6);
    const m = new THREE.Mesh(g, wireMat(0xff44aa));
    m.position.set(x, 0.25, 0.92);
    return m;
  }
  const eyeL = makeEye(-0.28), eyeR = makeEye(0.28);
  crabGroup.add(eyeL, eyeR);

  // Claws (big front claws)
  function makeClaw(side) {
    const g = new THREE.Group();

    // Upper arm
    const arm1 = new THREE.Mesh(new THREE.CylinderGeometry(0.07, 0.1, 0.9, 6), wireMat(0x00ffcc));
    arm1.rotation.z = side * -0.7;
    arm1.position.set(side * 0.7, 0, 0.3);
    g.add(arm1);

    // Lower arm
    const arm2 = new THREE.Mesh(new THREE.CylinderGeometry(0.06, 0.08, 0.85, 6), wireMat(0x00ffcc));
    arm2.rotation.z = side * -0.3;
    arm2.position.set(side * 1.45, 0.1, 0.55);
    g.add(arm2);

    // Claw pincer A
    const pincerA = new THREE.Mesh(new THREE.ConeGeometry(0.12, 0.55, 6), wireMat(0x44ffdd));
    pincerA.rotation.z = side * (Math.PI / 2 + 0.3);
    pincerA.position.set(side * 2.1, 0.25, 0.7);
    g.add(pincerA);

    // Claw pincer B
    const pincerB = new THREE.Mesh(new THREE.ConeGeometry(0.10, 0.5, 6), wireMat(0x44ffdd));
    pincerB.rotation.z = side * (Math.PI / 2 - 0.3);
    pincerB.position.set(side * 2.1, -0.15, 0.7);
    g.add(pincerB);

    // Claw knuckle
    const knuckle = new THREE.Mesh(new THREE.SphereGeometry(0.15, 6, 6), wireMat(0x00ffcc));
    knuckle.position.set(side * 2.05, 0.05, 0.7);
    g.add(knuckle);

    return g;
  }
  const clawL = makeClaw(-1), clawR = makeClaw(1);
  crabGroup.add(clawL, clawR);

  // Walking legs (3 per side)
  const legs = [];
  function makeLeg(side, index) {
    const g = new THREE.Group();
    const xOff = side * (0.9 + index * 0.15);
    const zOff = -0.2 - index * 0.35;
    const spreadAngle = side * (0.4 + index * 0.15);

    const seg1 = new THREE.Mesh(new THREE.CylinderGeometry(0.045, 0.055, 0.75, 5), wireMat(0x00ddbb));
    seg1.rotation.z = spreadAngle;
    seg1.position.set(xOff, -0.05, zOff);
    g.add(seg1);

    const seg2 = new THREE.Mesh(new THREE.CylinderGeometry(0.035, 0.045, 0.65, 5), wireMat(0x00ccaa));
    seg2.rotation.z = spreadAngle * 1.6;
    seg2.position.set(side * (Math.abs(xOff) + 0.35), -0.35, zOff);
    g.add(seg2);

    const tip = new THREE.Mesh(new THREE.ConeGeometry(0.03, 0.25, 4), wireMat(0x00ffcc));
    tip.rotation.z = spreadAngle * 2;
    tip.position.set(side * (Math.abs(xOff) + 0.65), -0.65, zOff);
    g.add(tip);

    legs.push({ group: g, seg1, seg2, tip, side, index, phase: index * 1.1 + (side < 0 ? 0.5 : 0) });
    return g;
  }

  for (let i = 0; i < 3; i++) {
    crabGroup.add(makeLeg(-1, i));
    crabGroup.add(makeLeg(1, i));
  }

  // Back swimmerets (small paddle legs)
  function makeSwimmeret(side) {
    const g = new THREE.Group();
    const s = new THREE.Mesh(new THREE.CylinderGeometry(0.03, 0.04, 0.5, 4), wireMat(0x0099aa));
    s.rotation.z = side * 0.6;
    s.position.set(side * 1.0, -0.2, -0.85);
    g.add(s);
    const pad = new THREE.Mesh(new THREE.SphereGeometry(0.07, 5, 4), wireMat(0x0099aa));
    pad.position.set(side * 1.3, -0.45, -0.9);
    g.add(pad);
    return g;
  }
  crabGroup.add(makeSwimmeret(-1), makeSwimmeret(1));

  // Antennae
  function makeAntenna(side) {
    const g = new THREE.Group();
    const pts = [];
    for (let i = 0; i <= 10; i++) {
      const t = i / 10;
      pts.push(new THREE.Vector3(side * (0.15 + t * 1.2), t * 0.9, 0.85 - t * 0.1));
    }
    const curve = new THREE.CatmullRomCurve3(pts);
    const geo = new THREE.TubeGeometry(curve, 10, 0.018, 4, false);
    g.add(new THREE.Mesh(geo, wireMat(0xff88cc)));
    return g;
  }
  const antennaL = makeAntenna(-1), antennaR = makeAntenna(1);
  crabGroup.add(antennaL, antennaR);

  // Tail/abdomen flap
  const abdomenGeo = new THREE.SphereGeometry(0.45, 8, 5);
  abdomenGeo.scale(1.0, 0.35, 0.9);
  const abdomen = new THREE.Mesh(abdomenGeo, wireMat(0x009988));
  abdomen.position.set(0, -0.08, -1.0);
  crabGroup.add(abdomen);

  // Background particles
  const bgParticles = [];
  const bgGeo = new THREE.BufferGeometry();
  const bgPositions = new Float32Array(600 * 3);
  for (let i = 0; i < 600; i++) {
    bgPositions[i * 3] = (Math.random() - 0.5) * 30;
    bgPositions[i * 3 + 1] = (Math.random() - 0.5) * 20;
    bgPositions[i * 3 + 2] = (Math.random() - 0.5) * 20 - 5;
  }
  bgGeo.setAttribute('position', new THREE.BufferAttribute(bgPositions, 3));
  const bgPoints = new THREE.Points(bgGeo, new THREE.PointsMaterial({ color: 0x004466, size: 0.04 }));
  scene.add(bgPoints);

  // Fog
  scene.fog = new THREE.FogExp2(0x05060f, 0.08);

  // Ambient ring (torus around the crab)
  const ringGeo = new THREE.TorusGeometry(2.2, 0.015, 4, 80);
  const ringMesh = new THREE.Mesh(ringGeo, wireMat(0x004455));
  ringMesh.rotation.x = Math.PI / 2;
  scene.add(ringMesh);

  // Second ring
  const ring2Geo = new THREE.TorusGeometry(3.0, 0.01, 4, 100);
  const ring2Mesh = new THREE.Mesh(ring2Geo, wireMat(0x002233));
  ring2Mesh.rotation.x = Math.PI / 2.2;
  scene.add(ring2Mesh);

  // ---- Animation ----
  let time = 0;
  const clock = new THREE.Clock();

  function lerpColor(c1, c2, t) {
    const r1 = (c1 >> 16) & 0xff, g1 = (c1 >> 8) & 0xff, b1 = c1 & 0xff;
    const r2 = (c2 >> 16) & 0xff, g2 = (c2 >> 8) & 0xff, b2 = c2 & 0xff;
    return ((Math.round(r1 + (r2 - r1) * t) << 16) | (Math.round(g1 + (g2 - g1) * t) << 8) | Math.round(b1 + (b2 - b1) * t));
  }

  function setWireColor(mesh, hex) {
    if (mesh.material) mesh.material.color.setHex(hex);
    if (mesh.children) mesh.children.forEach(c => setWireColor(c, hex));
  }

  function animate() {
    requestAnimationFrame(animate);
    const dt = clock.getDelta();
    time += dt;

    let energy = 0, bass = 0, mid = 0, high = 0, rawBass = 0;
    if (analyser && isPlaying) {
      analyser.getByteFrequencyData(freqData);
      analyser.getByteTimeDomainData(waveData);
      for (let i = 0; i < freqData.length; i++) energy += freqData[i];
      energy /= freqData.length * 255;
      for (let i = 0; i < 8; i++) bass += freqData[i]; rawBass = bass / 8; bass = rawBass / 255;
      for (let i = 8; i < 80; i++) mid += freqData[i]; mid /= 72 * 255;
      for (let i = 80; i < 200; i++) high += freqData[i]; high /= 120 * 255;
    } else {
      energy = (Math.sin(time * 0.7) * 0.5 + 0.5) * 0.05;
      bass = energy * 0.6; mid = energy * 0.3; high = energy * 0.15;
    }

    // Body breathe + bass pulse
    const bodyScale = 1 + bass * 0.35 + Math.sin(time * 1.2) * 0.02;
    bodyMesh.scale.set(bodyScale, 1 + bass * 0.2, bodyScale * 0.9);
    ridgeMesh.scale.set(bodyScale * 1.02, 1 + bass * 0.22, bodyScale * 0.92);

    // Abdomen flap
    abdomen.rotation.x = Math.sin(time * 2.1) * 0.15 * (1 + mid * 3);
    abdomen.scale.set(1, 1 + bass * 0.4, 1);

    // Claw gesticulate
    const clawPulse = bass * 0.8;
    clawL.rotation.y = Math.sin(time * 1.8) * 0.2 + clawPulse * 0.5;
    clawL.rotation.x = Math.sin(time * 1.3 + 0.5) * 0.15;
    clawR.rotation.y = -Math.sin(time * 1.8) * 0.2 - clawPulse * 0.5;
    clawR.rotation.x = Math.sin(time * 1.3) * 0.15;
    clawL.scale.set(1 + clawPulse * 0.4, 1 + clawPulse * 0.4, 1 + clawPulse * 0.4);
    clawR.scale.set(1 + clawPulse * 0.4, 1 + clawPulse * 0.4, 1 + clawPulse * 0.4);

    // Walking legs ripple
    legs.forEach(leg => {
      const wave = Math.sin(time * 4.5 + leg.phase) * (0.08 + mid * 0.5);
      leg.seg1.rotation.x = wave;
      leg.seg2.rotation.x = wave * 1.4;
      leg.tip.rotation.x = wave * 1.8;
    });

    // Antennae wiggle
    antennaL.rotation.y = Math.sin(time * 2.5) * (0.15 + high * 0.8);
    antennaL.rotation.z = Math.sin(time * 1.7) * 0.1;
    antennaR.rotation.y = Math.sin(time * 2.5 + 0.4) * (0.15 + high * 0.8);
    antennaR.rotation.z = -Math.sin(time * 1.7) * 0.1;

    // Eyes pulse
    const eyeScale = 1 + high * 1.2;
    eyeL.scale.setScalar(eyeScale);
    eyeR.scale.setScalar(eyeScale);

    // Full crab rotation + bob
    crabGroup.rotation.y = Math.sin(time * 0.4) * 0.35 + energy * Math.sin(time * 8) * 0.08;
    crabGroup.position.y = Math.sin(time * 1.1) * 0.12 + bass * 0.25;
    crabGroup.rotation.z = Math.sin(time * 0.6) * 0.04;

    // Color shifts with music
    const hue = (time * 20 + energy * 180) % 360;
    const bodyHex = lerpColor(0x00ffcc, 0xff44aa, bass * 2);
    setWireColor(bodyMesh, bodyHex);
    setWireColor(ridgeMesh, lerpColor(0x00ccaa, 0xff2288, bass));
    setWireColor(clawL, lerpColor(0x00ffcc, 0xffaa00, mid * 3));
    setWireColor(clawR, lerpColor(0x00ffcc, 0xffaa00, mid * 3));
    setWireColor(eyeL, lerpColor(0xff44aa, 0xffffff, high * 2));
    setWireColor(eyeR, lerpColor(0xff44aa, 0xffffff, high * 2));
    legs.forEach(leg => setWireColor(leg.group, lerpColor(0x00ddbb, 0xff6600, mid * 2)));
    setWireColor(antennaL, lerpColor(0xff88cc, 0x88ffff, high * 3));
    setWireColor(antennaR, lerpColor(0xff88cc, 0x88ffff, high * 3));

    // Rings pulse
    const ringScale = 1 + bass * 0.6 + Math.sin(time * 2) * 0.02;
    ringMesh.scale.set(ringScale, ringScale, 1);
    ringMesh.material.color.setHex(lerpColor(0x004455, 0x00ffcc, bass * 0.8));
    ringMesh.material.opacity = 0.3 + bass * 0.6;
    ring2Mesh.scale.set(1 + mid * 0.4, 1 + mid * 0.4, 1);
    ring2Mesh.rotation.z = time * 0.2;
    ring2Mesh.material.color.setHex(lerpColor(0x002233, 0xff44aa, mid * 1.5));
    ring2Mesh.material.opacity = 0.15 + mid * 0.5;

    // BG particles drift
    bgPoints.rotation.y = time * 0.03;
    bgPoints.material.color.setHex(lerpColor(0x004466, 0x00ffcc, energy * 2));
    bgPoints.material.size = 0.04 + energy * 0.12;

    // Camera subtle drift
    camera.position.x = Math.sin(time * 0.3) * 0.5;
    camera.position.y = 1.5 + Math.sin(time * 0.2) * 0.3 + bass * 0.3;
    camera.lookAt(0, 0.2 + bass * 0.3, 0);

    // Progress bar update
    if (isPlaying && audioCtx) {
      const cur = Math.min(audioCtx.currentTime - startTime, duration);
      progressFill.style.width = (duration > 0 ? (cur / duration) * 100 : 0) + '%';
      timeCur.textContent = fmt(Math.max(0, cur));
    }

    renderer.render(scene, camera);
  }

  animate();
})();
</script>
</body>
</html>