my eye

author Angelo Gladding

name Color

published 2025-06-26T19:10:32.042281-07:00

type entry

updated 2025-06-26T20:36:54.916430-07:00

url /color, /2025/06/27/7a

visibility public

Content

<style>
  #controls {
    margin-bottom: 10px;
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    align-items: center;
  }

  button, label {
    font-size: 14px;
  }

  #preview {
    width: 400px;
    height: 300px;
    background-color: hsl(0, 100%, 50%);
    cursor: crosshair;
    transition: background-color 0.2s ease;
    position: relative;
    margin-top: 10px;
  }

  .fullscreen {
    position: fixed !important;
    top: 0; left: 0;
    width: 100vw !important;
    height: 100vh !important;
    z-index: 9999;
  }

  .gradient-overlay {
    position: absolute;
    top: 0; left: 0;
    width: 100%;
    height: 100%;
    background: linear-gradient(to right, red, yellow, lime, cyan, blue, magenta, red),
                linear-gradient(to top, black, transparent 50%, white);
    mix-blend-mode: lighten;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.2s;
  }

  .show-gradient .gradient-overlay {
    opacity: 1;
  }

  #swatch {
    width: 30px;
    height: 30px;
    border: 1px solid #444;
    display: inline-block;
    vertical-align: middle;
    margin-left: 10px;
  }

  #readout {
    font-family: monospace;
    margin-left: 10px;
  }

  input[type=range] {
    vertical-align: middle;
  }
</style>

<div id="controls">
  <button onclick="setMode('picker')">Color Picker</button>
  <button onclick="setMode('redPulse')">Red Pulse</button>
  <button onclick="setMode('audioPulse')">Audio Pulse Mode</button>
  <button onclick="setMode('micPulse')">Mic Pulse Mode</button>
  <button onclick="enterFullscreen()">Fullscreen</button>

  <label>Sat:
    <input type="range" id="satSlider" min="0" max="100" value="100">
  </label>

  <label>Alpha:
    <input type="range" id="alphaSlider" min="0" max="100" value="100">
  </label>

  <div id="swatch"></div>
  <div id="readout">hsl(0,100%,50%) / #ff0000</div>
</div>

<div id="preview">
  <div class="gradient-overlay"></div>
</div>

<script>
  const $$preview = document.getElementById('preview')
  const $$gradientOverlay = $$preview.querySelector('.gradient-overlay')
  const $$satSlider = document.getElementById('satSlider')
  const $$alphaSlider = document.getElementById('alphaSlider')
  const $$swatch = document.getElementById('swatch')
  const $$readout = document.getElementById('readout')

  let mode = 'picker'
  let ctrlHeld = false
  let animationFrame
  let selectedHue = 0
  let selectedLightness = 50
  let selectedSaturation = 100
  let selectedAlpha = 1
  let angle = 0

  function hslToHex(h, s, l) {
    s /= 100
    l /= 100
    let c = (1 - Math.abs(2 * l - 1)) * s
    let x = c * (1 - Math.abs((h / 60) % 2 - 1))
    let m = l - c / 2
    let r = 0, g = 0, b = 0
    if (h < 60) { r = c; g = x }
    else if (h < 120) { r = x; g = c }
    else if (h < 180) { g = c; b = x }
    else if (h < 240) { g = x; b = c }
    else if (h < 300) { r = x; b = c }
    else { r = c; g = 0; b = x }
    r = Math.round((r + m) * 255)
    g = Math.round((g + m) * 255)
    b = Math.round((b + m) * 255)
    return "#" + [r, g, b].map(x => x.toString(16).padStart(2, '0')).join('')
  }

  function updateDisplayColor(h, s, l, a = 1) {
    const color = `hsla($${h}, $${s}%, $${l}%, $${a})`
    $$preview.style.backgroundColor = color
    $$swatch.style.backgroundColor = color
    $$readout.textContent = `hsl($${h},$${s}%,$${l}%) / $${hslToHex(h, s, l)}`
  }

function setupAudioVisualizer() {
  const $$audio = document.getElementById('audio')
  const audioCtx = new (window.AudioContext || window.webkitAudioContext)()
  const source = audioCtx.createMediaElementSource($$audio)
  const analyser = audioCtx.createAnalyser()
  analyser.fftSize = 256

  const dataArray = new Uint8Array(analyser.frequencyBinCount)
  source.connect(analyser)
  analyser.connect(audioCtx.destination)

  let hue = selectedHue
  let sat = selectedSaturation

  function animateAudioReactive() {
    analyser.getByteFrequencyData(dataArray)
    const avg = dataArray.reduce((a, b) => a + b) / dataArray.length
    const norm = avg / 255  // range: 0.0–1.0

    const l = 20 + norm * 30  // lightness: 20%–50%
    updateDisplayColor(hue, sat, l, selectedAlpha)

    animationFrame = requestAnimationFrame(animateAudioReactive)
  }

  $$audio.play()
  animateAudioReactive()
}

function setupMicVisualizer() {
  const audioCtx = new (window.AudioContext || window.webkitAudioContext)()
  const analyser = audioCtx.createAnalyser()
  analyser.fftSize = 256
  const dataArray = new Uint8Array(analyser.frequencyBinCount)

  navigator.mediaDevices.getUserMedia({ audio: true })
    .then(stream => {
      const source = audioCtx.createMediaStreamSource(stream)
      source.connect(analyser)

      let hue = selectedHue
      let sat = selectedSaturation

      function animateMic() {
        analyser.getByteFrequencyData(dataArray)
        const avg = dataArray.reduce((a, b) => a + b) / dataArray.length
        const norm = avg / 255  // 0 to 1

        const amplified = Math.pow(norm, 1.5)  // Exponential boost for low sounds
        const l = 10 + amplified * 70  // Pulse between 10–80% lightness
        // XXX const l = 20 + norm * 30  // Pulse between 20–50% lightness
        updateDisplayColor(hue, sat, l, selectedAlpha)

        animationFrame = requestAnimationFrame(animateMic)
      }

      animateMic()
    })
    .catch(err => {
      alert('Microphone access denied or unavailable')
      console.error(err)
    })
}

  function setMode(newMode) {
    cancelAnimationFrame(animationFrame)
    mode = newMode

    if (mode === 'audioPulse') {
      setupAudioVisualizer()
    }

if (mode === 'micPulse') {
  setupMicVisualizer()
}

    if (mode === 'picker') {
      $$preview.classList.remove('fullscreen')
      updateDisplayColor(selectedHue, selectedSaturation, selectedLightness, selectedAlpha)

      $$preview.onmousemove = e => {
        if (!ctrlHeld) return
        const { h, l } = getColorFromMouse(e)
        updateDisplayColor(h, $$satSlider.value, l, $$alphaSlider.value / 100)
      }

      $$preview.onclick = e => {
        if (!ctrlHeld) return
        const { h, l } = getColorFromMouse(e)
        selectedHue = h
        selectedLightness = l
        selectedSaturation = parseInt($$satSlider.value)
        selectedAlpha = parseInt($$alphaSlider.value) / 100
        updateDisplayColor(h, selectedSaturation, l, selectedAlpha)
      }
    }

    if (mode === 'redPulse') {
      $$preview.onmousemove = null
      angle = 0
      function pulse() {
        const l = 20 + 10 * Math.sin(angle)
        updateDisplayColor(0, 80, l, 1)
        angle += 0.02
        animationFrame = requestAnimationFrame(pulse)
      }
      pulse()
    }
  }

  function getColorFromMouse(e) {
    const rect = $$preview.getBoundingClientRect()
    const x = e.clientX - rect.left
    const y = e.clientY - rect.top
    const h = Math.round((x / rect.width) * 360)
    const l = Math.round((1 - y / rect.height) * 100)
    return { h, l }
  }

  function enterFullscreen() {
    $$preview.classList.add('fullscreen')
    $$preview.requestFullscreen?.()
  }

  function exitFullscreen() {
    $$preview.classList.remove('fullscreen')
    document.exitFullscreen?.()
  }

  document.addEventListener('keydown', e => {
    if (e.key === 'Control' && !ctrlHeld) {
      ctrlHeld = true
      if (mode === 'picker') {
        $$preview.classList.add('show-gradient')
      }
    }
    if (e.key === 'Escape') {
      exitFullscreen()
    }
  })

  document.addEventListener('keyup', e => {
    if (e.key === 'Control') {
      ctrlHeld = false
      $$preview.classList.remove('show-gradient')
      if (mode === 'picker') {
        updateDisplayColor(selectedHue, selectedSaturation, selectedLightness, selectedAlpha)
      }
    }
  })

  document.addEventListener('fullscreenchange', () => {
    if (!document.fullscreenElement) {
      $$preview.classList.remove('fullscreen')
    }
  })

  $$satSlider.addEventListener('input', () => {
    selectedSaturation = parseInt($$satSlider.value)
    updateDisplayColor(selectedHue, selectedSaturation, selectedLightness, selectedAlpha)
  })

  $$alphaSlider.addEventListener('input', () => {
    selectedAlpha = parseInt($$alphaSlider.value) / 100
    updateDisplayColor(selectedHue, selectedSaturation, selectedLightness, selectedAlpha)
  })

  setMode('picker')
</script>