← back to Model Arena

data/artifacts/047c3b8392fa/qwen3-14b.html

366 lines

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Audiovisual Instrument</title>
  <style>
    :root {
      --bg-color: #f9f9f9;
      --text-color: #333;
      --accent-color: #ff6b6b;
      --border-color: #ddd;
      --particle-color: #ff6b6b;
      --safe-height: 100vh;
    }

    body {
      margin: 0;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
      background-color: var(--bg-color);
      color: var(--text-color);
      display: flex;
      flex-direction: column;
      min-height: var(--safe-height);
      overflow: hidden;
      user-select: none;
      -webkit-tap-highlight-color: transparent;
    }

    header {
      padding: 1rem;
      display: flex;
      justify-content: space-between;
      align-items: center;
      border-bottom: 1px solid var(--border-color);
    }

    h1 {
      margin: 0;
      font-size: 1.5rem;
    }

    .controls {
      display: flex;
      flex-wrap: wrap;
      gap: 1rem;
      padding: 1rem;
      background-color: #fff;
      border: 1px solid var(--border-color);
      border-radius: 8px;
      flex: 1;
      min-height: 200px;
    }

    .control-group {
      display: flex;
      flex-direction: column;
      gap: 0.5rem;
      flex: 1 1 200px;
    }

    .control-group label {
      font-size: 0.9rem;
      margin-bottom: 0.25rem;
      display: block;
    }

    .control-group input[type="range"],
    .control-group input[type="checkbox"] {
      width: 100%;
      padding: 0.25rem;
      border: 1px solid var(--border-color);
      border-radius: 4px;
      background-color: #fff;
      -webkit-appearance: none;
      appearance: none;
      outline: none;
    }

    .control-group input[type="range"]::-webkit-slider-thumb {
      -webkit-appearance: none;
      appearance: none;
      width: 16px;
      height: 16px;
      background-color: var(--accent-color);
      border-radius: 50%;
      cursor: pointer;
      box-shadow: 0 2px 4px rgba(0,0,0,0.2);
    }

    .control-group input[type="range"]::-moz-range-thumb {
      width: 16px;
      height: 16px;
      background-color: var(--accent-color);
      border-radius: 50%;
      cursor: pointer;
    }

    .particle-canvas {
      flex: 1;
      position: relative;
      overflow: hidden;
      background-color: #fff;
      border: 1px solid var(--border-color);
      border-radius: 8px;
      min-height: 400px;
    }

    .status {
      padding: 1rem;
      text-align: center;
      font-size: 1rem;
      color: #555;
    }

    .error {
      color: #d32f2f;
    }

    .complete {
      color: #2e7d32;
    }

    @media (max-width: 600px) {
      .controls {
        flex-direction: column;
      }

      .control-group {
        flex: 1 1 100%;
      }
    }

    /* Reduced motion */
    @media (prefers-reduced-motion: no-preference) {
      .particle-canvas {
        animation: pulse 5s infinite ease-in-out;
      }
    }

    @keyframes pulse {
      0%, 100% {
        opacity: 1;
      }
      50% {
        opacity: 0.8;
      }
    }
  </style>
</head>
<body>
  <header>
    <h1>Audiovisual Instrument</h1>
    <button id="toggleMute" aria-label="Toggle mute">🔊</button>
  </header>
  <div class="controls">
    <div class="control-group">
      <label for="tone">Tone</label>
      <input type="range" id="tone" min="20" max="1000" value="440" step="1" />
    </div>
    <div class="control-group">
      <label for="volume">Volume</label>
      <input type="range" id="volume" min="0" max="1" step="0.01" value="0.5" />
    </div>
    <div class="control-group">
      <label for="particleCount">Particles</label>
      <input type="range" id="particleCount" min="0" max="100" value="30" step="1" />
    </div>
    <div class="control-group">
      <label for="speed">Speed</label>
      <input type="range" id="speed" min="0.1" max="5" value="1" step="0.1" />
    </div>
    <div class="control-group">
      <label for="mute">Mute</label>
      <input type="checkbox" id="mute" />
    </div>
  </div>
  <div class="particle-canvas" id="canvas"></div>
  <div class="status" id="status">Start playing with the tone slider to see reactive particles.</div>

  <script>
    const canvas = document.getElementById('canvas');
    const ctx = canvas.getContext('2d');
    const status = document.getElementById('status');
    const toggleMuteBtn = document.getElementById('toggleMute');
    const muteCheckbox = document.getElementById('mute');
    const toneSlider = document.getElementById('tone');
    const volumeSlider = document.getElementById('volume');
    const particleCountSlider = document.getElementById('particleCount');
    const speedSlider = document.getElementById('speed');

    let isMuted = muteCheckbox.checked;

    // Resize canvas
    function resizeCanvas() {
      canvas.width = canvas.clientWidth;
      canvas.height = canvas.clientHeight;
    }

    window.addEventListener('resize', resizeCanvas);
    resizeCanvas();

    // Audio setup
    const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    let oscillator = audioCtx.createOscillator();
    let gainNode = audioCtx.createGain();

    oscillator.type = 'sine';
    oscillator.frequency.value = parseFloat(toneSlider.value);
    gainNode.gain.value = parseFloat(volumeSlider.value);

    oscillator.connect(gainNode);
    gainNode.connect(audioCtx.destination);

    oscillator.start();
    oscillator.stop(audioCtx.currentTime + 1);

    function updateAudio() {
      oscillator.frequency.value = parseFloat(toneSlider.value);
      gainNode.gain.value = parseFloat(volumeSlider.value);
    }

    toneSlider.addEventListener('input', updateAudio);
    volumeSlider.addEventListener('input', updateAudio);

    muteCheckbox.addEventListener('change', () => {
      isMuted = muteCheckbox.checked;
      updateAudio();
      toggleMuteBtn.textContent = isMuted ? '🔊' : '🔇';
    });

    toggleMuteBtn.addEventListener('click', () => {
      muteCheckbox.checked = !isMuted;
      isMuted = !isMuted;
      toggleMuteBtn.textContent = isMuted ? '🔊' : '🔇';
      updateAudio();
    });

    // Particle system
    class Particle {
      constructor(x, y, radius, color, speed) {
        this.x = x;
        this.y = y;
        this.radius = radius;
        this.color = color;
        this.speed = speed;
        this.angle = Math.random() * Math.PI * 2;
        this.vx = Math.cos(this.angle) * this.speed;
        this.vy = Math.sin(this.angle) * this.speed;
      }

      update() {
        this.x += this.vx;
        this.y += this.vy;
        if (this.x < 0 || this.x > canvas.width || this.y < 0 || this.y > canvas.height) {
          this.reset();
        }
      }

      reset() {
        this.x = canvas.width / 2;
        this.y = canvas.height / 2;
        this.angle = Math.random() * Math.PI * 2;
        this.vx = Math.cos(this.angle) * this.speed;
        this.vy = Math.sin(this.angle) * this.speed;
      }

      draw(ctx) {
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
        ctx.fillStyle = this.color;
        ctx.fill();
      }
    }

    const particles = [];
    const particleCount = parseInt(particleCountSlider.value);
    const particleSpeed = parseFloat(speedSlider.value);

    for (let i = 0; i < particleCount; i++) {
      const radius = 3 + Math.random() * 3;
      const color = `hsl(${Math.random() * 360}, 100%, 60%)`;
      particles.push(new Particle(
        canvas.width / 2,
        canvas.height / 2,
        radius,
        color,
        particleSpeed
      ));
    }

    function drawParticles() {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      for (const particle of particles) {
        particle.update();
        particle.draw(ctx);
      }
    }

    function animate() {
      drawParticles();
      requestAnimationFrame(animate);
    }

    animate();

    // Handle user interaction
    let isPlaying = false;

    function startAudio() {
      if (isPlaying) return;
      oscillator.stop();
      oscillator = audioCtx.createOscillator();
      oscillator.type = 'sine';
      oscillator.frequency.value = parseFloat(toneSlider.value);
      gainNode.gain.value = parseFloat(volumeSlider.value);
      oscillator.connect(gainNode);
      oscillator.start();
      isPlaying = true;
    }

    function stopAudio() {
      oscillator.stop();
      isPlaying = false;
    }

    toneSlider.addEventListener('input', () => {
      if (isPlaying) return;
      startAudio();
    });

    particleCountSlider.addEventListener('input', () => {
      const count = parseInt(particleCountSlider.value);
      particles.length = 0;
      for (let i = 0; i < count; i++) {
        const radius = 3 + Math.random() * 3;
        const color = `hsl(${Math.random() * 360}, 100%, 60%)`;
        particles.push(new Particle(
          canvas.width / 2,
          canvas.height / 2,
          radius,
          color,
          particleSpeed
        ));
      }
    });

    speedSlider.addEventListener('input', () => {
      const speed = parseFloat(speedSlider.value);
      for (const particle of particles) {
        particle.speed = speed;
      }
    });

    // Status messages
    function setStatus(message, type = 'normal') {
      status.textContent = message;
      status.className = 'status';
      if (type === 'error') status.classList.add('error');
      else if (type === 'complete') status.classList.add('complete');
    }

    setStatus("Start playing with the tone slider to see reactive particles.");
  </script>
</body>
</html>