Custom Code Experiment •
Music: Iwà Oru Eléṣè
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>Iwà Oru Eléṣè</title>
<style>
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; background: #000; overflow: hidden; }
canvas#main { display: block; position: fixed; inset: 0; width: 100%; height: 100%; }
#overlay {
position: fixed; inset: 0;
display: flex; flex-direction: column; justify-content: space-between;
pointer-events: none;
font-family: 'Space Mono', 'Courier New', monospace;
}
#header { padding: 28px 36px; }
#track-title {
font-family: Georgia, 'Times New Roman', serif;
font-style: italic; font-size: clamp(15px, 2.2vw, 21px);
color: rgba(255,255,255,0.85); letter-spacing: 0.02em;
}
#track-sub { font-size: 10px; color: rgba(255,255,255,0.3); letter-spacing: 0.18em; text-transform: uppercase; margin-top: 5px; }
#controls { padding: 20px 36px 30px; pointer-events: all; display: flex; flex-direction: column; gap: 12px; }
#wv-canvas { width: 100%; height: 28px; opacity: 0.45; display: block; }
#transport { display: flex; align-items: center; gap: 14px; }
#play-btn {
width: 42px; height: 42px; border-radius: 50%;
border: 1px solid rgba(255,255,255,0.35); background: rgba(255,255,255,0.07);
color: #fff; font-size: 13px; cursor: pointer;
display: flex; align-items: center; justify-content: center;
transition: background 0.2s, border-color 0.2s; flex-shrink: 0;
}
#play-btn:hover { background: rgba(255,255,255,0.18); border-color: rgba(255,255,255,0.6); }
#play-btn:disabled { opacity: 0.35; cursor: default; }
#progress-track {
flex: 1; height: 20px; cursor: pointer; position: relative;
}
#progress-track::before {
content: ''; position: absolute;
left: 0; right: 0; top: 50%; height: 2px;
transform: translateY(-50%);
background: rgba(255,255,255,0.15); border-radius: 1px;
}
#progress-fill {
position: absolute; left: 0; top: 50%; height: 2px;
transform: translateY(-50%);
background: rgba(255,255,255,0.75); border-radius: 1px;
width: 0%; pointer-events: none;
}
#progress-thumb {
position: absolute; top: 50%; width: 11px; height: 11px;
border-radius: 50%; background: #fff;
box-shadow: 0 0 6px rgba(255,200,80,0.7), 0 0 0 2px rgba(255,200,80,0.35);
transform: translate(-50%, -50%);
pointer-events: none; left: 0%;
transition: box-shadow 0.15s;
}
#progress-track.dragging #progress-thumb {
box-shadow: 0 0 10px rgba(255,200,80,1), 0 0 0 3px rgba(255,200,80,0.6);
}
#time-lbl { font-size: 10px; color: rgba(255,255,255,0.4); letter-spacing: 0.08em; min-width: 90px; text-align: right; }
#start-screen {
position: fixed; inset: 0; display: flex; flex-direction: column;
align-items: center; justify-content: center; gap: 18px;
cursor: pointer; z-index: 10; transition: opacity 0.5s;
background: rgba(0,0,0,0.55);
font-family: 'Space Mono', 'Courier New', monospace;
}
#start-screen.hidden { opacity: 0; pointer-events: none; }
#play-circle {
width: 78px; height: 78px; border-radius: 50%;
border: 1px solid rgba(255,200,80,0.5); background: rgba(255,200,80,0.07);
display: flex; align-items: center; justify-content: center; position: relative;
}
.pulse-ring {
position: absolute; width: 78px; height: 78px;
border: 1px solid rgba(255,200,80,0.35); border-radius: 50%;
animation: pulse-out 2.4s ease-out infinite;
}
.pulse-ring:nth-child(2) { animation-delay: 0.8s; }
.pulse-ring:nth-child(3) { animation-delay: 1.6s; }
@keyframes pulse-out { 0%{transform:scale(1);opacity:.7} 100%{transform:scale(3.8);opacity:0} }
#play-tri { width:0;height:0;border-top:11px solid transparent;border-bottom:11px solid transparent;border-left:18px solid rgba(255,200,80,.9);margin-left:4px; }
#start-label { font-size: 10px; letter-spacing: 0.25em; text-transform: uppercase; color: rgba(255,255,255,0.35); }
#status-msg { font-size: 10px; letter-spacing: 0.12em; color: rgba(255,200,80,0.6); text-transform: uppercase; min-height: 14px; }
</style>
</head>
<body>
<canvas id="main"></canvas>
<div id="start-screen">
<div id="play-circle">
<div class="pulse-ring"></div>
<div class="pulse-ring"></div>
<div class="pulse-ring"></div>
<div id="play-tri"></div>
</div>
<div id="start-label">click to begin</div>
<div id="status-msg"></div>
</div>
<div id="overlay">
<div id="header">
<div id="track-title">Iwà Oru Eléṣè</div>
<div id="track-sub">01 · audio visualiser</div>
</div>
<div id="controls">
<canvas id="wv-canvas" height="28"></canvas>
<div id="transport">
<button id="play-btn" disabled>▶</button>
<div id="progress-track"><div id="progress-fill"></div><div id="progress-thumb"></div></div>
<div id="time-lbl">–:–– / –:––</div>
</div>
</div>
</div>
<script>
const AUDIO_URL =
"https://cdn.jsdelivr.net/gh/alaric-visi/music@main/" +
"%20iw%C3%A0-oru-el%E1%BA%B9%CC%81p%E1%BA%B9%CC%80/" +
"01-%20iw%C3%A0-oru-el%E1%BA%B9%CC%81p%E1%BA%B9%CC%80.mp3";
const mainCvs = document.getElementById('main');
const wvCvs = document.getElementById('wv-canvas');
const ctx = mainCvs.getContext('2d');
const wvCtx = wvCvs.getContext('2d');
const ss = document.getElementById('start-screen');
const playBtn = document.getElementById('play-btn');
const progFill = document.getElementById('progress-fill');
const progThumb = document.getElementById('progress-thumb');
const progTrack = document.getElementById('progress-track');
const timeLbl = document.getElementById('time-lbl');
const statusMsg = document.getElementById('status-msg');
let W, H, cx, cy, dpr;
function resize() {
dpr = devicePixelRatio || 1;
W = mainCvs.width = innerWidth * dpr;
H = mainCvs.height = innerHeight * dpr;
cx = W / 2; cy = H / 2;
wvCvs.width = wvCvs.offsetWidth * dpr;
wvCvs.height = wvCvs.offsetHeight * dpr;
}
addEventListener('resize', resize); resize();
let audioCtx, analyser, audioBuffer, bufSrc;
let fftData, wvData, bufLen;
let loadState = 'idle';
let playing = false;
let pausedAt = 0;
let startedAt = 0;
let duration = 0;
let time = 0, beatCooldown = 0, prevBass = 0;
function fmt(s) {
if (!isFinite(s) || s < 0) return '–:––';
return Math.floor(s / 60) + ':' + Math.floor(s % 60).toString().padStart(2, '0');
}
function currentTime() {
if (!playing) return pausedAt;
return audioCtx.currentTime - startedAt;
}
function setProgress(fraction) {
const pct = (fraction * 100).toFixed(3) + '%';
progFill.style.width = pct;
progThumb.style.left = pct;
timeLbl.textContent = fmt(fraction * duration) + ' / ' + fmt(duration);
}
function updateUI() {
if (!duration || isDragging) return;
setProgress(Math.min(Math.max(currentTime(), 0), duration) / duration);
}
function stopSource(intentional) {
if (!bufSrc) return;
bufSrc.onended = null;
try { bufSrc.stop(); } catch (_) {}
bufSrc.disconnect();
bufSrc = null;
if (!intentional) {
playing = false;
pausedAt = 0;
playBtn.textContent = '▶';
progFill.style.width = '0%';
progThumb.style.left = '0%';
timeLbl.textContent = fmt(0) + ' / ' + fmt(duration);
}
}
function createAnalyserOnce() {
if (analyser) return;
analyser = audioCtx.createAnalyser();
analyser.fftSize = 2048;
analyser.smoothingTimeConstant = 0.82;
bufLen = analyser.frequencyBinCount;
fftData = new Uint8Array(bufLen);
wvData = new Uint8Array(bufLen);
analyser.connect(audioCtx.destination);
}
function playFrom(offset) {
stopSource(true);
createAnalyserOnce();
bufSrc = audioCtx.createBufferSource();
bufSrc.buffer = audioBuffer;
bufSrc.connect(analyser);
const clampedOffset = Math.max(0, Math.min(offset, duration));
bufSrc.start(0, clampedOffset);
startedAt = audioCtx.currentTime - clampedOffset;
bufSrc.onended = () => {
stopSource(false);
};
}
async function loadAudio() {
if (loadState !== 'idle') return loadState === 'ready';
loadState = 'loading';
statusMsg.textContent = 'fetching audio…';
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
let ab;
try {
const res = await fetch(AUDIO_URL);
if (!res.ok) throw new Error('HTTP ' + res.status);
statusMsg.textContent = 'downloading…';
ab = await res.arrayBuffer();
} catch (e) {
loadState = 'error';
statusMsg.textContent = 'load failed — see console';
console.error('Audio fetch failed:', e);
return false;
}
try {
statusMsg.textContent = 'decoding…';
audioBuffer = await audioCtx.decodeAudioData(ab);
duration = audioBuffer.duration;
loadState = 'ready';
statusMsg.textContent = '';
playBtn.disabled = false;
return true;
} catch (e) {
loadState = 'error';
statusMsg.textContent = 'decode error — see console';
console.error('Decode error:', e);
return false;
}
}
let toggling = false;
async function togglePlay() {
if (toggling) return;
toggling = true;
try {
if (loadState === 'idle') {
ss.classList.add('hidden');
const ok = await loadAudio();
if (!ok) {
ss.classList.remove('hidden');
return;
}
}
if (loadState === 'loading') return;
if (loadState === 'error') return;
if (audioCtx.state === 'suspended') await audioCtx.resume();
if (playing) {
pausedAt = currentTime();
stopSource(true);
playing = false;
playBtn.textContent = '▶';
} else {
playFrom(pausedAt);
playing = true;
playBtn.textContent = '❚❚';
}
} finally {
toggling = false;
}
}
function seekTo(seconds) {
if (loadState !== 'ready') return;
const wasPlaying = playing;
pausedAt = Math.max(0, Math.min(seconds, duration));
if (wasPlaying) {
stopSource(true);
playing = false;
const doPlay = () => { playFrom(pausedAt); playing = true; playBtn.textContent = '❚❚'; };
if (audioCtx.state === 'suspended') { audioCtx.resume().then(doPlay); } else { doPlay(); }
}
setProgress(pausedAt / duration);
}
ss.addEventListener('click', () => { if (!playing && loadState !== 'loading') togglePlay(); });
playBtn.addEventListener('click', e => { e.stopPropagation(); togglePlay(); });
let isDragging = false;
function trackFraction(clientX) {
const r = progTrack.getBoundingClientRect();
return Math.max(0, Math.min(1, (clientX - r.left) / r.width));
}
function startDrag(clientX) {
if (!duration) return;
isDragging = true;
progTrack.classList.add('dragging');
setProgress(trackFraction(clientX));
}
function moveDrag(clientX) {
if (!isDragging || !duration) return;
setProgress(trackFraction(clientX));
}
function endDrag(clientX) {
if (!isDragging) return;
isDragging = false;
progTrack.classList.remove('dragging');
if (!duration) return;
const fraction = trackFraction(clientX);
setProgress(fraction);
seekTo(fraction * duration);
}
progTrack.addEventListener('mousedown', e => { startDrag(e.clientX); e.preventDefault(); });
progTrack.addEventListener('touchstart', e => { startDrag(e.touches[0].clientX); }, { passive: true });
document.addEventListener('mousemove', e => { moveDrag(e.clientX); });
document.addEventListener('touchmove', e => { if (isDragging) moveDrag(e.touches[0].clientX); }, { passive: true });
document.addEventListener('mouseup', e => { endDrag(e.clientX); });
document.addEventListener('touchend', e => { endDrag(e.changedTouches[0].clientX); });
const PAL = [
[255,200,50],[255,140,30],[255,80,110],[200,80,255],
[80,200,255],[80,255,180],[255,240,100],[255,100,60]
];
class Particle {
constructor(x, y, angle, speed, color, life) {
this.x = x; this.y = y;
this.vx = Math.cos(angle) * speed; this.vy = Math.sin(angle) * speed;
this.color = color; this.life = life; this.maxLife = life;
this.sz = (Math.random() * 2.5 + 1) * dpr; this.trail = [];
}
update() {
this.trail.push([this.x, this.y]);
if (this.trail.length > 10) this.trail.shift();
this.x += this.vx; this.y += this.vy;
this.vx *= 0.963; this.vy *= 0.963; this.life--;
}
draw() {
const a = this.life / this.maxLife, [r, g, b] = this.color;
for (let i = 0; i < this.trail.length; i++) {
ctx.beginPath(); ctx.arc(this.trail[i][0], this.trail[i][1], this.sz * .5, 0, Math.PI * 2);
ctx.fillStyle = `rgba(${r},${g},${b},${(i / this.trail.length) * a * .35})`; ctx.fill();
}
ctx.beginPath(); ctx.arc(this.x, this.y, this.sz, 0, Math.PI * 2);
ctx.fillStyle = `rgba(${r},${g},${b},${a})`; ctx.fill();
ctx.beginPath(); ctx.arc(this.x, this.y, this.sz * .38, 0, Math.PI * 2);
ctx.fillStyle = `rgba(255,255,255,${a * .55})`; ctx.fill();
}
}
class Ring {
constructor(x, y, color, maxR) {
this.x = x; this.y = y; this.r = 0; this.maxR = maxR;
this.color = color; this.life = 1;
this.thick = (0.8 + Math.random() * 1.2) * dpr;
}
update() { this.r += (this.maxR - this.r) * .07 + 2; this.life -= .022; }
draw() {
const [r, g, b] = this.color;
ctx.beginPath(); ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
ctx.strokeStyle = `rgba(${r},${g},${b},${this.life * .55})`;
ctx.lineWidth = this.thick; ctx.stroke();
}
}
class Ambient {
constructor() { this.reset(true); }
reset(init) {
this.x = Math.random() * W;
this.y = init ? Math.random() * H : (Math.random() < .5 ? -20 : H + 20);
this.vx = (Math.random() - .5) * .4 * dpr; this.vy = (Math.random() - .5) * .4 * dpr;
this.r = (1 + Math.random() * 1.5) * dpr;
this.color = PAL[Math.floor(Math.random() * PAL.length)];
this.alpha = Math.random() * .28 + .04;
this.life = .5 + Math.random() * .5; this.decay = .003 + Math.random() * .003;
}
update() { this.x += this.vx; this.y += this.vy; this.life -= this.decay; if (this.life <= 0) this.reset(false); }
draw() {
const [r, g, b] = this.color;
ctx.beginPath(); ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
ctx.fillStyle = `rgba(${r},${g},${b},${this.alpha * this.life})`; ctx.fill();
}
}
class Starburst {
constructor(x, y, bands) {
this.parts = [];
const c1 = PAL[Math.floor(Math.random() * PAL.length)];
const c2 = PAL[Math.floor(Math.random() * PAL.length)];
const nRays = 8 + Math.floor(bands.energy * 22);
const base = Math.random() * Math.PI * 2;
const spd = (5 + bands.bass * 16) * dpr;
for (let i = 0; i < nRays; i++) {
const ang = base + (i / nRays) * Math.PI * 2 + (Math.random() - .5) * .5;
for (let k = 0; k < 4; k++) {
const s = spd * (.5 + Math.random() * .9);
const life = 45 + Math.floor(Math.random() * 40);
this.parts.push(new Particle(x, y, ang + (Math.random() - .5) * .12, s,
Math.random() < .4 ? c2 : c1, life + k * 6));
}
}
rings.push(new Ring(x, y, c1, (60 + bands.energy * 180) * dpr));
rings.push(new Ring(x, y, c2, (30 + bands.bass * 80) * dpr));
this.alive = true;
}
update() {
this.parts.forEach(p => p.update());
this.parts = this.parts.filter(p => p.life > 0);
if (!this.parts.length) this.alive = false;
}
draw() { this.parts.forEach(p => p.draw()); }
}
let starbursts = [], rings = [], ambients = [];
for (let i = 0; i < 60; i++) ambients.push(new Ambient());
function getBands() {
if (!analyser) return { bass: 0, mid: 0, high: 0, energy: 0 };
analyser.getByteFrequencyData(fftData);
analyser.getByteTimeDomainData(wvData);
const bE = Math.floor(bufLen * .05), mE = Math.floor(bufLen * .25);
let bass = 0, mid = 0, high = 0;
for (let i = 0; i < bE; i++) bass += fftData[i];
for (let i = bE; i < mE; i++) mid += fftData[i];
for (let i = mE; i < bufLen; i++) high += fftData[i];
bass /= bE * 255; mid /= (mE - bE) * 255; high /= (bufLen - mE) * 255;
return { bass, mid, high, energy: bass * .6 + mid * .3 + high * .1 };
}
function drawSpectrum() {
const n = 180, step = Math.floor(bufLen / n), maxBH = H * .32, bW = W / n;
for (let i = 0; i < n; i++) {
let s = 0;
for (let j = 0; j < step; j++) s += fftData[i * step + j];
const v = s / (step * 255), bh = v * maxBH;
const [r, g, b] = PAL[Math.floor(i / n * PAL.length) % PAL.length];
ctx.fillStyle = `rgba(${r},${g},${b},${.35 + v * .55})`;
ctx.fillRect(i * bW, H - bh, bW - 1, bh);
ctx.fillStyle = `rgba(${r},${g},${b},${(.35 + v * .55) * .28})`;
ctx.fillRect(i * bW, 0, bW - 1, bh * .2);
}
}
function drawWaveform() {
if (!wvData) return;
const wd = wvCvs.width, wh = wvCvs.height;
wvCtx.clearRect(0, 0, wd, wh);
wvCtx.beginPath();
const sw = wd / bufLen; let x = 0;
for (let i = 0; i < bufLen; i++) {
const v = (wvData[i] - 128) / 128, y = wh / 2 + v * wh * .44;
i === 0 ? wvCtx.moveTo(x, y) : wvCtx.lineTo(x, y);
x += sw;
}
wvCtx.strokeStyle = 'rgba(255,200,80,.55)';
wvCtx.lineWidth = dpr; wvCtx.stroke();
}
function drawCenter(bands) {
const base = Math.min(W, H) * .09, pulse = base + bands.bass * base * 2;
for (let i = 0; i < 120; i++) {
const ang = (i / 120) * Math.PI * 2;
const fv = fftData ? fftData[Math.floor((i / 120) * bufLen * .5)] / 255 : 0;
const r = pulse + fv * base * 3;
const px = cx + Math.cos(ang) * r, py = cy + Math.sin(ang) * r;
const [pr, pg, pb] = PAL[(Math.floor((i / 120) * PAL.length) + Math.floor(time * .01)) % PAL.length];
ctx.beginPath(); ctx.arc(px, py, (1.5 + fv * 4) * dpr, 0, Math.PI * 2);
ctx.fillStyle = `rgba(${pr},${pg},${pb},${.4 + fv * .6})`; ctx.fill();
}
const [cr, cg, cb] = PAL[Math.floor(time * .015) % PAL.length];
const g2 = ctx.createRadialGradient(cx, cy, 0, cx, cy, pulse * .9);
g2.addColorStop(0, `rgba(${cr},${cg},${cb},${.12 + bands.bass * .22})`);
g2.addColorStop(.5, `rgba(${cr},${cg},${cb},${.04 + bands.bass * .08})`);
g2.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = g2;
ctx.beginPath(); ctx.arc(cx, cy, pulse, 0, Math.PI * 2); ctx.fill();
}
function drawIdle() {
for (let k = 0; k < 3; k++) {
const ph = time * .025 + k * (Math.PI * 2 / 3);
const r = Math.min(W, H) * .07 * (1 + .3 * Math.sin(ph));
const [pr, pg, pb] = PAL[(k + Math.floor(time * .01)) % PAL.length];
ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.strokeStyle = `rgba(${pr},${pg},${pb},.18)`;
ctx.lineWidth = dpr; ctx.stroke();
}
}
function loop() {
requestAnimationFrame(loop);
time++;
const bands = getBands();
ctx.fillStyle = playing ? 'rgba(0,0,0,.16)' : 'rgba(0,0,0,.28)';
ctx.fillRect(0, 0, W, H);
ambients.forEach(a => { a.update(); a.draw(); });
if (playing) {
drawSpectrum();
updateUI();
beatCooldown--;
if (beatCooldown <= 0 &&
(bands.bass - prevBass > .07 || (bands.energy > .28 && Math.random() < .04))) {
const x = cx + (Math.random() - .5) * W * .55;
const y = cy + (Math.random() - .5) * H * .45;
starbursts.push(new Starburst(x, y, bands));
beatCooldown = 10 + Math.floor(Math.random() * 12);
}
if (Math.random() < bands.mid * .25) {
rings.push(new Ring(
cx + (Math.random() - .5) * W * .7,
cy + (Math.random() - .5) * H * .6,
PAL[Math.floor(Math.random() * PAL.length)],
(25 + bands.mid * 100) * dpr
));
}
prevBass = bands.bass;
} else {
drawIdle();
}
drawWaveform();
rings.forEach(r => { r.update(); r.draw(); }); rings = rings.filter(r => r.life > 0);
starbursts.forEach(s => { s.update(); s.draw(); }); starbursts = starbursts.filter(s => s.alive);
if (analyser) drawCenter(bands);
}
loop();
</script>
</body>
</html>