ÿþ<meta charset="utf-8"> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel='preconnect' href='https://js.1ink.us'> <link rel='preconnect' href='https://glsl.1ink.us'> <link rel='preconnect' href='https://storage.noahcohn.com'> <link rel="preload" as="image" href="./image/panel-bg.jpg" fetchpriority="high"> <link rel="preload" as="image" href="./image/shroud.jpg"> <link rel="preload" as="script" href="./projectm-v.035-thread.1ijs"> <title>B3HD " ProjectM</title> <title>1ink.us " ProjectM</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { background-color: black; overflow: hidden; font-family: 'Inter', system-ui, sans-serif; } html, body { width: 100%; height: 100%; } #wrap { position: absolute; top: 0; left: 0; height: 100vh; width: 100vw; padding: 0; pointer-events: none; } #contain1 { position: absolute; padding: 0; background-color: rgba(255,255,255,0.0); } canvas.emscripten { display: block; margin: 0; padding: 0; image-rendering: crisp-edges; } #circle { pointer-events: none; border: 0; position: absolute; overflow: hidden; z-index: 3200; background: transparent; } /* Logos */ .logo { pointer-events: none; position: absolute; z-index: 3300; } #logo-right { top: 2vh; right: 2vh; height: 15vh; } #logo-left { top: 2vh; left: 2vh; height: 15vh; } /* %% CIRCULAR CONTROL PANEL %% */ .panel-container { position: fixed; bottom: 2vh; left: 2vh; width: 409px; height: 409px; filter: drop-shadow(0 0 18px hsla(220, 55%, 50%, 0.4)); transition: filter 0.6s ease; z-index: 3300; } .panel-container::before { content: ''; position: absolute; inset: 0; border-radius: 9999px; border: 9px solid #0d1117; box-shadow: inset 0 0 0 2px #1e293b; pointer-events: none; z-index: 9; } .panel-container::after { content: ''; position: absolute; inset: 0; border-radius: 9999px; pointer-events: none; z-index: 9; background: conic-gradient(rgba(255,255,255,0.22) 0deg 48deg, transparent 48deg 185deg, rgba(255,255,255,0.09) 185deg 233deg, transparent 233deg 360deg); mask: radial-gradient(circle at 50%, transparent 91%, black 93%, black 100%); -webkit-mask: radial-gradient(circle at 50%, transparent 91%, black 93%, black 100%); } @keyframes bezel-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .panel-bg { width: 100%; height: 100%; border-radius: 9999px; overflow: hidden; box-shadow: 0 25px 60px -15px rgb(0 0 0 / 0.7); position: relative; } .panel-bg img { width: 100%; height: 100%; object-fit: cover; } .panel-bg::before { content: ''; position: absolute; inset: 0; border-radius: inherit; background: radial-gradient(circle at 50% 50%, transparent 52%, rgba(0,0,0,0.48) 100%); pointer-events: none; z-index: 1; } .clickable-button { position: absolute; background: transparent; border: none; cursor: pointer; z-index: 10; border-radius: 6px; transition: transform 0.1s ease, filter 0.1s ease; } .clickable-button::before { content: ''; position: absolute; inset: -5px; border-radius: 9px; background: radial-gradient(ellipse at 50% 50%, rgba(255,220,80,0.35), transparent 68%); opacity: 0; transition: opacity 0.12s ease; pointer-events: none; } .clickable-button:hover::before { opacity: 1; } .clickable-button:active { transform: scale(0.95); filter: brightness(1.1); } .clickable-button.engaged::before { background: radial-gradient(ellipse at 50% 50%, rgba(255,155,0,0.58), transparent 68%); opacity: 1; box-shadow: 0 0 14px rgba(255,155,0,0.35); } .knob { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 106px; height: 106px; background: radial-gradient(circle at 40% 30%, #1f2937, #111827 60%, #000); border-radius: 9999px; box-shadow: 0 0 0 8px #1f2937, inset 0 20px 40px rgba(255,255,255,0.15), 0 10px 30px rgba(0,0,0,0.8); z-index: 20; cursor: grab; display: flex; align-items: center; justify-content: center; } .knob:active { cursor: grabbing; } .knob::before { content: ''; position: absolute; inset: 0; border-radius: 50%; background: conic-gradient(from 0deg, rgba(255,255,255,0.13) 0deg 38deg, transparent 38deg 360deg); pointer-events: none; } .knob::after { content: ''; width: 12px; height: 12px; background: #ef4444; border-radius: 9999px; box-shadow: 0 0 10px #ef4444; } .led-ring { position: absolute; inset: 0; pointer-events: none; z-index: 5; } .led { position: absolute; left: calc(50% - 5px); top: calc(50% - 175px); width: 10px; height: 10px; border-radius: 50%; transform-origin: 5px 175px; } .led--power { background: #22c55e; box-shadow: 0 0 6px 2px rgba(34,197,94,0.8); transform: rotate(0deg); } .led--audio { background: #4ade80; box-shadow: 0 0 6px 2px rgba(74,222,128,0.8); transform: rotate(72deg); } .led--lock { background: #4b5563; box-shadow: 0 0 3px 1px rgba(75,85,99,0.5); transform: rotate(144deg); } body.has-lock .led--lock { background: #f59e0b; box-shadow: 0 0 7px 2px rgba(245,158,11,0.85); } .led--random { background: #60a5fa; box-shadow: 0 0 6px 2px rgba(96,165,250,0.8); transform: rotate(216deg); } .led--beat { background: #374151; box-shadow: none; transform: rotate(288deg); transition: background 0.06s ease, box-shadow 0.06s ease; } .led--beat.on { background: #f87171; box-shadow: 0 0 8px 3px rgba(248,113,113,0.85); } @keyframes led-heartbeat { 0%,100%{opacity:1} 50%{opacity:0.2} } @keyframes led-double-blink { 0%,17%,34%,100%{opacity:1} 8.5%,25.5%{opacity:0.08} } #preset-name { position: fixed; bottom: 2vh; left: 450px; z-index: 3299; font-family: "Lucida Console", "Courier New", monospace; font-size: 14px; color: rgba(200,200,200,0.75); pointer-events: none; white-space: nowrap; text-shadow: 1px 1px 2px rgba(0,0,0,0.85); } #audio-player-status { position: fixed; bottom: 5vh; left: 450px; z-index: 3299; font-family: "Lucida Console", "Courier New", monospace; font-size: 13px; color: rgba(180, 220, 255, 0.85); pointer-events: none; white-space: nowrap; text-shadow: 1px 1px 2px rgba(0,0,0,0.85); } .ext-player-header { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; color: #ccc; font-family: 'Lucida Console', monospace; font-size: 12px; padding: 0.5rem 0.75rem; background: #111; } .ext-player-hide { pointer-events: auto; cursor: pointer; border: 1px solid #555; border-radius: 4px; background: #222; color: #ddd; font-family: inherit; font-size: 11px; padding: 0.2rem 0.55rem; } .ext-player-hide:hover { background: #333; border-color: #888; } #gl { pointer-events: none; } #show-btn { display: none; position: fixed; bottom: 2vh; left: 2vh; z-index: 3301; padding: 12px 20px; background: #2a3f5f; color: white; border: 2px solid #555; border-radius: 8px; font-size: 14px; cursor: pointer; } #show-btn:not(.hidden) { display: block; } @media (prefers-reduced-motion: no-preference) { .panel-container::after { animation: bezel-spin 18s linear infinite; } .led--audio { animation: led-heartbeat 1.2s ease-in-out infinite; } .led--random { animation: led-double-blink 1.5s ease-in-out infinite; } } </style> </head> <body> <!-- Splash --> <img id="splash1" src="./image/shroud.jpg" style="position:absolute;height:100vh;width:100vw;z-index:3590;display:block;"/> <img id="splash2" src="./image/spinner.gif" style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);height:20vh;width:20vh;z-index:3591;display:block;"/> <!-- Visualization --> <div id="wrap"> <div id="contain1" style="position:absolute;"> <canvas id="circle" style="position:absolute;z-index:3200;pointer-events:none;"></canvas> <canvas class="emscripten" id="mcanvas" style="pointer-events:auto;display:block;position:absolute;z-index:3001;background:rgba(0,0,0,0);top:0;left:0;"></canvas> <canvas class="emscripten" id="scanvas" style="pointer-events:auto;display:block;position:absolute;z-index:3000;background:rgba(0,0,0,1);top:0;left:0;transform:scaleY(-1);"></canvas> </div> </div> <!-- Logos --> <img src="./image/scale4x.png" class="logo" id="logo-right"/> <img src="./image/1inkusa.png" class="logo" id="logo-left"/> <!-- Circular Control Panel --> <div class="panel-container" id="panel-container"> <div class="panel-bg"> <img src="./image/panel-bg.jpg" alt="Control Panel"/> </div> <div class="led-ring"> <span class="led led--power" title="Power"></span> <span class="led led--audio" title="Audio"></span> <span class="led led--lock" title="Preset Locked"></span> <span class="led led--random" title="Random"></span> <span class="led led--beat" title="Beat"></span> </div> <!-- Clickable areas --> <!-- Calibrated against panel-bg.jpg (circular ring of 8 buttons around centered knob) --> <button id="lockPresetBtn" onclick="lockPreset()" class="clickable-button" style="top:14%;left:34%;width:32%;height:12%;"></button> <button onclick="hidePanel()" class="clickable-button" style="top:22%;left:14%;width:22%;height:16%;"></button> <button id="audioPlayerBtn" onclick="cycleAudioPlayer()" class="clickable-button" title="Audio Player  click to cycle FLAC, MOD, or hide" style="top:22%;right:14%;width:22%;height:16%;"></button> <button id="musicBtn" onclick="startChangeSong()" class="clickable-button" style="top:38%;left:6%;width:22%;height:18%;"></button> <button id="randomPresetBtn" onclick="randomPreset()" class="clickable-button" style="top:38%;right:6%;width:22%;height:18%;"></button> <button id="toggleBezelBtn" onclick="fullWindow()" class="clickable-button" style="top:56%;left:14%;width:22%;height:16%;"></button> <button id="customMilkBtn" onclick="randomCustom()" class="clickable-button" style="top:56%;right:14%;width:22%;height:16%;"></button> <button onclick="toggleFullscreen()" class="clickable-button" style="top:64%;left:34%;width:32%;height:13%;"></button> <div id="knob" class="knob"></div> </div> <button onclick="showPanel()" id="show-btn" class="hidden">Show Controls</button> <div id="audio-player-status">Audio Player</div> <div id="preset-name">Preset: --</div> <!-- Hidden data --> <div hidden id="shut">2</div> <div hidden id="lns">1</div> <div hidden id="presetDir">default</div> <div hidden id="milkPath">https://glsl.1ink.us/milk/flexi.milk</div> <div hidden id="birdsongPath">https://projectm.1ink.us/birdsongs/26HouseFinchSong.mp3</div> <audio crossorigin id="track" preload="none" style="pointer-events:none;"></audio> <!-- External audio players (PCM via postMessage) --> <section id="flacPlayerSection" class="ext-player-section" style="display:none;position:fixed;z-index:3400;top:12vh;left:8vw;width:84vw;height:76vh;background:#000;border:2px solid #333;border-radius:8px;box-shadow:0 0 30px rgba(0,0,0,0.9);overflow:hidden;"> <div class="ext-player-header"> <span>FLAC Player</span> <button type="button" class="ext-player-hide" onclick="closeAudioPlayer()">Hide</button> </div> <iframe id="flacFrame" src="https://projectm.1ink.us/flac-player" style="width:100%;height:calc(100% - 32px);border:none;background:#000;" allow="autoplay; fullscreen"></iframe> </section> <section id="modPlayerSection" class="ext-player-section" style="display:none;position:fixed;z-index:3400;top:12vh;left:8vw;width:84vw;height:76vh;background:#000;border:2px solid #333;border-radius:8px;box-shadow:0 0 30px rgba(0,0,0,0.9);overflow:hidden;"> <div class="ext-player-header"> <span>MOD Player</span> <button type="button" class="ext-player-hide" onclick="closeAudioPlayer()">Hide</button> </div> <iframe id="modFrame" src="https://mod.1ink.us" style="width:100%;height:calc(100% - 32px);border:none;background:#000;" allow="autoplay; fullscreen"></iframe> </section> <div hidden id="flacPlayerUrl">https://projectm.1ink.us/flac-player</div> <div hidden id="modPlayerUrl">https://mod.1ink.us</div> <!-- Bezel assets --> <img src='https://projectm.1ink.us/unglass.png' id="gl" alt="Glass" style="display:none;" crossorigin="anonymous"> <img src='https://projectm.1ink.us/bezel.jpg' id="frm" hidden alt="Frame" crossorigin="anonymous"/> <canvas id="tcan" hidden></canvas> <script> // ==================== BEZEL / SHUTTER DRAWING ==================== let rec, ccs, mw, tc, iww, img, imgb, hi, sht, lns, glas, rr, mdd, gtop, nw, nh, ndiff, bctx, ttx; function clo(sz){ bctx.globalCompositeOperation='source-over'; bctx.fillStyle='rgba(0,0,0,0.3)'; shtr(rr-2); bctx.fillStyle='rgba(0,0,0,0.1)'; shtr(rr-1); bctx.fillStyle='rgba(0,0,0,0.3)'; shtr(rr); bctx.fillStyle='rgba(0,0,0,0.4)'; shtr(rr+1); shtr(rr); bctx.fillStyle='rgba(0,0,0,0.4)'; shtr(rr+2); } function shtr(sim){ if(ccs.getContext){ bctx.beginPath(); bctx.arc(mw, rr, sim, 0, 2*Math.PI, false); bctx.fill(); } } function op(sz){ bctx.globalCompositeOperation='source-over'; bctx.fillStyle='rgba(168,168,168,1)'; shtr(rr); bctx.globalCompositeOperation='destination-out'; shtr(sz); } function drawBezel(){ img = document.querySelector('#frm'); imgb = document.querySelector('#gl'); // Defer until both bezel images are loaded if (!img.complete || !img.naturalWidth || !imgb.complete || !imgb.naturalWidth) { const onLoad = () => { img.removeEventListener('load', onLoad); imgb.removeEventListener('load', onLoad); drawBezel(); }; img.addEventListener('load', onLoad); imgb.addEventListener('load', onLoad); return; } hi = window.innerHeight; rec = Math.round(window.innerWidth); document.querySelector('#circle').width = rec; document.querySelector('#circle').height = hi; glas = Math.round(hi * 1.133); imgb.height = glas; imgb.width = glas; rr = Math.round(hi / 2); mw = Math.round(((rec - hi) / 2) + rr); tc = document.querySelector('#tcan'); tc.height = hi; mdd = Math.round((rec - glas) / 2); gtop = Math.round((imgb.height - hi) / 2); imgb.style.cssText = `display:block;position:absolute;left:${mdd}px;right:${mdd}px;z-index:555;top:-${gtop}px;`; nw = img.naturalWidth; nh = img.naturalHeight; iww = Math.round((hi / nh) * nw); ndiff = Math.round(((iww - rec) / 2) * (nh / hi)); tc.width = rec; sht = document.querySelector('#shut').innerHTML; lns = document.querySelector('#lns').innerHTML; if (ccs.getContext) { ccs = document.querySelector('#circle'); bctx = ccs.getContext('2d', { alpha: true, desynchronized: false }); ttx = tc.getContext('2d', { alpha: true }); ttx.drawImage(img, ndiff, 0, nw - Math.round(nw*0.02), nh - Math.round(nh*0.02), 0, 0, iww, hi); bctx.fillStyle = 'black'; bctx.fillRect(0, 0, rec, hi); bctx.fillStyle = bctx.createPattern(tc, 'no-repeat'); bctx.fillRect(0, 0, rec, hi); bctx.globalCompositeOperation = 'destination-out'; shtr(rr); bctx.globalCompositeOperation = 'source-over'; bctx.fillStyle = 'rgba(168,168,168,1)'; shtr(rr); } } ccs = document.querySelector('#circle'); // Draw once bezel frame assets finish loading (initial page load). (function preloadBezelAssets() { const frm = document.querySelector('#frm'); const gl = document.querySelector('#gl'); if (!frm || !gl) return; const redraw = () => { if (typeof drawBezel === 'function') drawBezel(); }; if (frm.complete && gl.complete && frm.naturalWidth && gl.naturalWidth) { redraw(); } else { frm.addEventListener('load', redraw, { once: true }); gl.addEventListener('load', redraw, { once: true }); } })(); </script> <script type="module"> const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; let Module; let isBezelMode = true; let isPresetLocked = false; const AUDIO_CHANNEL_NAME = 'projectm-audio'; let externalAudioChannel = null; let pcmTransferPtr = 0; const PCM_TRANSFER_CAP = 2048; const pendingExternalPCM = []; const MAX_PENDING_EXTERNAL_PCM = 24; const DEFAULT_EXTERNAL_PCM_ORIGINS = new Set([ window.location.origin, 'https://test.1ink.us', 'https://projectm.1ink.us', 'https://flac.1ink.us', 'https://mod.1ink.us' ].filter(Boolean)); // External PCM bridge receiver architecture: // - Sender side (popup/iframe) owns timing/clocking (RAF vs audio clock) and posts chunks. // - Receiver side (this page) accepts postMessage/BroadcastChannel payloads and forwards PCM to projectM. // - Internal audio/worklet pcmData path remains separate; this only handles external bridge traffic. // Contract expected from child iframe/popup: postMessage({ type: 'pcm', buffer: Float32Array, channels: 1|2, sampleRate? }, targetOrigin) function moduleCanAcceptExternalPCM() { return !!(Module && Module.HEAPF32 && Module._projectm_pcm_add_float_wrapper && Module._malloc); } function ensurePcmTransferBuffer() { if (pcmTransferPtr) return pcmTransferPtr; if (!moduleCanAcceptExternalPCM()) return 0; pcmTransferPtr = Module._malloc(PCM_TRANSFER_CAP * 4); return pcmTransferPtr; } function queueExternalPCM(buffer, channels, sampleRate) { if (pendingExternalPCM.length >= MAX_PENDING_EXTERNAL_PCM) { console.warn('[projectM external PCM] queue full, dropping oldest chunk'); pendingExternalPCM.shift(); } pendingExternalPCM.push({ buffer, channels, sampleRate }); } function feedPCMToModule(buffer, channels = 2, sampleRate) { if (!(buffer instanceof Float32Array)) { console.debug('[projectM external PCM] ignored non-Float32Array payload'); return; } const ch = channels === 1 ? 1 : 2; if (channels !== 1 && channels !== 2) { console.debug('[projectM external PCM] invalid channels value; defaulting to stereo:', channels); } if (ch === 2 && (buffer.length % 2) !== 0) { console.debug('[projectM external PCM] rejected odd-length stereo payload:', buffer.length); return; } const samplesPerChannel = ch === 1 ? buffer.length : (buffer.length / 2); if (samplesPerChannel <= 0) return; if (!moduleCanAcceptExternalPCM()) { queueExternalPCM(buffer, ch, sampleRate); return; } let ptr = 0; let usedPrealloc = false; if (buffer.length <= PCM_TRANSFER_CAP) { ptr = ensurePcmTransferBuffer(); usedPrealloc = !!ptr; } if (!ptr) { ptr = Module._malloc(buffer.length * 4); if (!ptr) return; } try { // Convert byte pointer to Float32 index (divide by 4). Module.HEAPF32.set(buffer, ptr >> 2); Module._projectm_pcm_add_float_wrapper(0, ptr, samplesPerChannel, ch); console.debug('[projectM external PCM] fed chunk', { channels: ch, samplesPerChannel, totalSamples: buffer.length, sampleRate }); } finally { if (!usedPrealloc && ptr && Module._free) { Module._free(ptr); } } } function flushQueuedExternalPCM() { if (!moduleCanAcceptExternalPCM() || pendingExternalPCM.length === 0) return; while (pendingExternalPCM.length) { const next = pendingExternalPCM.shift(); feedPCMToModule(next.buffer, next.channels, next.sampleRate); } } function isTrustedExternalPcmOrigin(origin) { try { const raw = localStorage.getItem('externalPcmAllowedOrigins') || ''; const allowedOrigins = raw.split(',').map((s) => s.trim()).filter(Boolean); if (allowedOrigins.length > 0) { return allowedOrigins.includes(origin); } return DEFAULT_EXTERNAL_PCM_ORIGINS.has(origin); } catch (_) { console.debug('[projectM external PCM] localStorage unavailable; using default origin allowlist'); return DEFAULT_EXTERNAL_PCM_ORIGINS.has(origin); } } function setupExternalAudioReceiver() { window.addEventListener('message', (event) => { if (!isTrustedExternalPcmOrigin(event.origin)) { console.debug('[projectM external PCM] ignored untrusted origin:', event.origin); return; } const d = event.data; if (d && d.type === 'pcm' && d.buffer instanceof Float32Array) { feedPCMToModule(d.buffer, d.channels, d.sampleRate); } }); if (!externalAudioChannel) { try { externalAudioChannel = new BroadcastChannel(AUDIO_CHANNEL_NAME); externalAudioChannel.onmessage = (e) => { const d = e.data; if (d && d.type === 'pcm' && d.buffer instanceof Float32Array) { feedPCMToModule(d.buffer, d.channels, d.sampleRate); } }; } catch (e) { console.debug('[projectM external PCM] BroadcastChannel unavailable:', e); } } } setupExternalAudioReceiver(); const externalPcmFlushInterval = setInterval(flushQueuedExternalPCM, 100); window.addEventListener('beforeunload', () => { clearInterval(externalPcmFlushInterval); if (externalAudioChannel) { externalAudioChannel.close(); externalAudioChannel = null; } if (pcmTransferPtr && Module && Module._free) { Module._free(pcmTransferPtr); pcmTransferPtr = 0; } }); // ===== DRAGGABLE KNOB ===== const knob = document.getElementById('knob'); const panelContainer = document.getElementById('panel-container'); let rot = 0, dragging = false, startAng = 0; function angle(e) { const r = knob.getBoundingClientRect(); const cx = r.left + r.width/2; const cy = r.top + r.height/2; return Math.atan2(e.clientY - cy, e.clientX - cx) * 180 / Math.PI; } function applyKnobRotation(deg) { knob.style.transform = `translate(-50%,-50%) rotate(${deg}deg)`; const hue = ((deg % 360) + 360) % 360; panelContainer.style.filter = `drop-shadow(0 0 18px hsla(${hue},55%,50%,0.42))`; } knob.addEventListener('mousedown', e => { dragging = true; startAng = angle(e) - rot; }); document.addEventListener('mousemove', e => { if (dragging) { rot = angle(e) - startAng; applyKnobRotation(rot); } }); document.addEventListener('mouseup', () => dragging = false); knob.addEventListener('touchstart', e => { dragging = true; startAng = angle(e.touches[0]) - rot; }); document.addEventListener('touchmove', e => { if (dragging) { rot = angle(e.touches[0]) - startAng; applyKnobRotation(rot); } }); document.addEventListener('touchend', () => dragging = false); // ===== BUTTON PULSE ===== function pulseButton(btn) { if (prefersReducedMotion) return; btn.classList.remove('pulse'); void btn.offsetWidth; btn.classList.add('pulse'); setTimeout(() => btn.classList.remove('pulse'), 420); } document.querySelectorAll('.clickable-button').forEach(btn => { btn.addEventListener('click', e => pulseButton(e.currentTarget)); }); // ===== PANEL SHOW/HIDE ===== function hidePanel() { panelContainer.style.transition = 'all 0.3s ease'; panelContainer.style.opacity = '0'; setTimeout(() => { panelContainer.style.display = 'none'; document.getElementById('show-btn').classList.remove('hidden'); document.getElementById('logo-left').style.opacity='0'; document.getElementById('logo-right').style.opacity='0'; document.getElementById('preset-name').style.opacity='0'; document.getElementById('audio-player-status').style.opacity='0'; }, 280); } function showPanel() { panelContainer.style.display = 'block'; panelContainer.style.opacity = '0'; setTimeout(() => { panelContainer.style.transition = 'all 0.3s ease'; panelContainer.style.opacity = '1'; document.getElementById('logo-left').style.opacity='1'; document.getElementById('logo-right').style.opacity='1'; document.getElementById('preset-name').style.opacity='1'; document.getElementById('audio-player-status').style.opacity='1'; }, 10); document.getElementById('show-btn').classList.add('hidden'); } window.hidePanel = hidePanel; window.showPanel = showPanel; // ===== BEAT LED ===== const beatLed = document.querySelector('.led--beat'); if (!prefersReducedMotion) setInterval(() => beatLed.classList.toggle('on'), 500); // ===== IDLE KNOB WOBBLE ===== if (!prefersReducedMotion) { setTimeout(() => { knob.style.transition = 'transform 1.5s cubic-bezier(0.34,1.56,0.64,1)'; knob.style.transform = `translate(-50%,-50%) rotate(${Math.random()*25-12}deg)`; }, 1200); } // ===== PRESET DISPLAY ===== window.updatePresetDisplay = function(name) { if (!name) return; const basename = name.split('/').pop(); const el = document.querySelector('#preset-name'); if (el) { el.textContent = 'Preset: ' + basename; window.currentPresetName = basename; } }; // ===== API PRESET HELPERS ===== function getPresetDir() { const el = document.getElementById('presetDir'); return (el && el.innerHTML.trim() !== 'default') ? el.innerHTML.trim() : 'any'; } const PRESET_API_BASES = [ 'https://storage.noahcohn.com', 'https://storage.1ink.us' ]; function getPresetApiBases(preferred) { const fromStorage = localStorage.getItem('apiBase'); const ordered = [preferred, fromStorage, ...PRESET_API_BASES].filter(Boolean); return [...new Set(ordered)]; } async function fetchApiPreset(apiBase) { const dir = getPresetDir(); const bases = getPresetApiBases(apiBase); let lastError = null; for (const base of bases) { try { const res = await fetch(`${base}/api/presets/random?dir=${encodeURIComponent(dir)}`); if (!res.ok) throw new Error(`API error ${res.status} from ${base}`); const data = await res.json(); if (!data.url || !data.filename) throw new Error(`Invalid API response from ${base}`); const milkRes = await fetch(data.url); if (!milkRes.ok) throw new Error(`Failed to fetch preset milk from ${data.url}`); const buffer = await milkRes.arrayBuffer(); const bytes = new Uint8ClampedArray(buffer); const safeName = data.filename.replace(/[^a-zA-Z0-9._-]/g, '_'); const vfsPath = `/presets/api_${data.dir || 'any'}_${safeName}`; if (Module.FS) Module.FS.writeFile(vfsPath, bytes); return { vfsPath, filename: data.filename, dir: data.dir }; } catch (error) { lastError = error; console.warn('[ProjectM] preset API attempt failed:', base, error); } } throw lastError || new Error('No preset API base available'); } async function loadStartupApiPresets(count = 5) { const apiBase = localStorage.getItem('apiBase') || PRESET_API_BASES[0]; const results = []; for (let i = 0; i < count; i++) { try { const r = await fetchApiPreset(apiBase); results.push(r); if (i === 0) window.updatePresetDisplay(r.filename); } catch(e){} } return results; } async function loadRandomApiPreset() { if (!Module || !Module.ccall) return; try { const result = await fetchApiPreset(); Module.ccall('load_preset_file', null, ['string'], [result.vfsPath]); window.updatePresetDisplay(result.filename); } catch(e) { console.error(e); } } // ===== BUTTON HANDLERS ===== function startChangeSong() { console.log('[B3HD] Start/Change Song'); } function randomPreset() { loadRandomApiPreset(); } function randomCustom() { loadRandomApiPreset(); } const AUDIO_PLAYER_SOURCES = [ { id: 'none', label: 'Audio Player' }, { id: 'flac', label: 'FLAC Player', sectionId: 'flacPlayerSection' }, { id: 'mod', label: 'MOD Player', sectionId: 'modPlayerSection' } ]; let activeAudioSourceIndex = 0; function updateAudioPlayerUi(source) { const status = document.getElementById('audio-player-status'); const btn = document.getElementById('audioPlayerBtn'); if (status) { status.textContent = source.id === 'none' ? 'Audio Player' : `Audio: ${source.label}`; } if (btn) { btn.classList.toggle('engaged', source.id !== 'none'); btn.title = source.id === 'none' ? 'Audio Player  click to open FLAC, MOD, or hide' : `${source.label} active  click to switch or hide`; } } function hideAllAudioPlayers() { document.querySelectorAll('.ext-player-section').forEach((section) => { section.style.display = 'none'; }); } function showAudioPlayer(sourceId) { const source = AUDIO_PLAYER_SOURCES.find((entry) => entry.id === sourceId) || AUDIO_PLAYER_SOURCES[0]; activeAudioSourceIndex = AUDIO_PLAYER_SOURCES.findIndex((entry) => entry.id === source.id); hideAllAudioPlayers(); if (source.sectionId) { const section = document.getElementById(source.sectionId); if (section) section.style.display = 'block'; console.info('[projectM external PCM] Child iframe/popup should postMessage({ type: "pcm", buffer: Float32Array, channels: 1|2, sampleRate? }, "*") to this page'); } updateAudioPlayerUi(source); } function cycleAudioPlayer() { activeAudioSourceIndex = (activeAudioSourceIndex + 1) % AUDIO_PLAYER_SOURCES.length; showAudioPlayer(AUDIO_PLAYER_SOURCES[activeAudioSourceIndex].id); } function closeAudioPlayer() { activeAudioSourceIndex = 0; showAudioPlayer('none'); } function fullWindow() { isBezelMode = !isBezelMode; const bezelBtn = document.getElementById('toggleBezelBtn'); if (bezelBtn) bezelBtn.classList.toggle('engaged', isBezelMode); if (!isBezelMode && document.fullscreenElement) { document.exitFullscreen().catch(()=>{}); } updateLayout(); syncModuleSize(); if (isBezelMode) drawBezel(); } function lockPreset() { isPresetLocked = !isPresetLocked; const btn = document.getElementById('lockPresetBtn'); document.body.classList.toggle('has-lock', isPresetLocked); btn.classList.toggle('engaged', isPresetLocked); if (Module && Module.ccall) Module.ccall('set_preset_locked', null, ['number'], [isPresetLocked ? 1 : 0]); } function toggleFullscreen() { isBezelMode = false; updateLayout(); syncModuleSize(); if (!document.fullscreenElement) { document.documentElement.requestFullscreen().catch(()=>{}); } else { document.exitFullscreen().catch(()=>{}); } } window.lockPreset = lockPreset; window.startChangeSong = startChangeSong; window.randomPreset = randomPreset; window.randomCustom = randomCustom; window.cycleAudioPlayer = cycleAudioPlayer; window.closeAudioPlayer = closeAudioPlayer; window.flacPlayer = () => showAudioPlayer('flac'); window.modPlayer = () => showAudioPlayer('mod'); window.fullWindow = fullWindow; window.toggleFullscreen = toggleFullscreen; // ===== LAYOUT ===== function updateLayout() { const mcanvas = document.querySelector('#mcanvas'); const scanvas = document.querySelector('#scanvas'); const contain1 = document.querySelector('#contain1'); const circle = document.querySelector('#circle'); const gl = document.querySelector('#gl'); const bezelBtn = document.getElementById('toggleBezelBtn'); if (bezelBtn) bezelBtn.classList.toggle('engaged', isBezelMode); if (isBezelMode) { const sz = window.innerHeight; if (contain1) { contain1.style.width = sz + 'px'; contain1.style.height = sz + 'px'; contain1.style.left = `calc(50vw - ${sz/2}px)`; contain1.style.top = `calc(50vh - ${sz/2}px)`; contain1.style.position = 'absolute'; } if (circle) circle.style.display = 'block'; if (gl) gl.style.display = 'block'; drawBezel(); } else { if (gl) gl.style.display = 'none'; if (contain1) { contain1.style.width = '100vw'; contain1.style.height = '100vh'; contain1.style.left = '0'; contain1.style.top = '0'; contain1.style.position = 'fixed'; } if (circle) circle.style.display = 'none'; } } function syncModuleSize() { const contain1 = document.querySelector('#contain1'); const mcanvas = document.querySelector('#mcanvas'); const scanvas = document.querySelector('#scanvas'); if (!contain1 || !mcanvas || !scanvas || !Module) return; const rect = contain1.getBoundingClientRect(); const dpr = window.devicePixelRatio || 1; const w = Math.max(1, Math.round(rect.width * dpr)); const h = Math.max(1, Math.round(rect.height * dpr)); mcanvas.width = w; mcanvas.height = h; scanvas.width = w; scanvas.height = h; mcanvas.style.width = rect.width + 'px'; mcanvas.style.height = rect.height + 'px'; scanvas.style.width = rect.width + 'px'; scanvas.style.height = rect.height + 'px'; if (Module._set_window_size) Module._set_window_size(w, h); if (Module._set_aspect_correction) Module._set_aspect_correction(isBezelMode); } const resizeObserver = new ResizeObserver(() => { updateLayout(); syncModuleSize(); }); const contain1El = document.querySelector('#contain1'); if (contain1El) resizeObserver.observe(contain1El); window.addEventListener('resize', () => { updateLayout(); syncModuleSize(); }); // ===== KEYBOARD ===== document.addEventListener('keydown', e => { if (e.key === ' ' || e.keyCode === 32) { e.preventDefault(); document.getElementById('randomPresetBtn').click(); } if (e.key.toLowerCase() === 'l') document.getElementById('lockPresetBtn').click(); if (e.key.toLowerCase() === 'h') hidePanel(); if (e.key.toLowerCase() === 'a') cycleAudioPlayer(); if (e.key === 'Escape') closeAudioPlayer(); }); // ===== INITIALIZATION ===== updateLayout(); function loadScript(src) { return new Promise((resolve, reject) => { const scr = document.createElement('script'); scr.src = src; scr.async = true; scr.charset = 'utf-8'; scr.onload = () => resolve(); scr.onerror = () => reject(new Error(`Failed to load ${src}`)); document.body.appendChild(scr); }); } setTimeout(() => { document.getElementById('splash2').style.display = 'none'; }, 5200); setTimeout(() => { document.getElementById('splash1').style.display = 'none'; }, 5500); setTimeout(() => { const scr = document.createElement("script"); scr.async = true; scr.charset = 'utf-8'; scr.type = 'text/javascript'; scr.defer = true; scr.src = "./projectm-v.035-thread.1ijs"; document.body.appendChild(scr); async function initProjectM() { try { await loadScript('./projectm-v.035-thread.1ijs'); Module = await createModule(); flushQueuedExternalPCM(); Module._init(); updateLayout(); syncModuleSize(); document.getElementById('splash2').style.display = 'none'; requestAnimationFrame(() => { document.getElementById('splash1').style.display = 'none'; }); const mcanvas = document.querySelector('#mcanvas'); Module._start_render(mcanvas.width, mcanvas.height); document.getElementById('musicBtn').click(); const startup = await loadStartupApiPresets(5); if (startup.length > 0) { Module.ccall('load_preset_file', null, ['string'], [startup[0].vfsPath]); window.updatePresetDisplay(startup[0].filename); for (let i = 1; i < startup.length; i++) { Module.ccall('add_preset_file', null, ['string'], [startup[i].vfsPath]); } } } catch (err) { console.error('projectM init failed:', err); } } initProjectM(); }, 750); </script> </body>