Download index.html from Aluode/GhostYou: direct link, hf CLI and curl.
- Browser
- Download file 8.01 kB
-
https://huggingface.co/spaces/Aluode/GhostYou/resolve/main/index.html
- Command line
-
hf download hf://spaces/Aluode/GhostYou/index.html
-
curl -L -o index.html https://huggingface.co/spaces/Aluode/GhostYou/resolve/main/index.html
8.01 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <title>Ghost You</title> | |
| <style> | |
| :root { --bg:#07060b; --fg:#d9e4ff; --dim:#7d86a8; --glow:#9fd8ff; --accent:#ff8a3d; } | |
| * { box-sizing:border-box; } | |
| html,body { margin:0; height:100%; background:var(--bg); color:var(--fg); | |
| font:15px/1.45 "Georgia", serif; overflow:hidden; } | |
| #stage { position:fixed; inset:0; display:flex; align-items:center; justify-content:center; } | |
| canvas { width:min(100vw, 133.33vh); height:min(75vw, 100vh); image-rendering:auto; | |
| filter:contrast(1.08) saturate(.85); } | |
| #veil { position:fixed; inset:0; pointer-events:none; | |
| background:radial-gradient(ellipse at center, transparent 45%, rgba(4,2,10,.85) 100%); } | |
| #intro { position:fixed; inset:0; display:flex; flex-direction:column; align-items:center; | |
| justify-content:center; gap:18px; text-align:center; padding:16px; | |
| background:radial-gradient(ellipse at center, #15112a 0%, var(--bg) 70%); z-index:5; } | |
| h1 { font-size:clamp(40px, 9vw, 92px); margin:0; letter-spacing:.04em; font-weight:normal; | |
| color:var(--fg); text-shadow:0 0 18px var(--glow), 0 0 42px #4a6cff; animation:float 4s ease-in-out infinite; } | |
| @keyframes float { 50% { transform:translateY(-10px); opacity:.85; } } | |
| p { max-width:34em; color:var(--dim); margin:0; } | |
| button { font:inherit; color:var(--bg); background:var(--accent); border:0; border-radius:999px; | |
| padding:12px 26px; cursor:pointer; box-shadow:0 0 24px rgba(255,138,61,.45); } | |
| button.ghost { background:transparent; color:var(--fg); border:1px solid #3a3f5c; box-shadow:none; padding:8px 16px; } | |
| #bar { position:fixed; left:0; right:0; bottom:0; display:flex; flex-wrap:wrap; gap:8px; align-items:center; | |
| justify-content:center; padding:10px 16px; z-index:4; font-size:13px; color:var(--dim); | |
| background:linear-gradient(transparent, rgba(7,6,11,.9)); } | |
| #bar label { display:flex; gap:6px; align-items:center; } | |
| input[type=range] { width:110px; accent-color:var(--glow); } | |
| #err { color:var(--accent); } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="stage"><canvas id="c" width="320" height="240"></canvas></div> | |
| <div id="veil"></div> | |
| <div id="intro"> | |
| <h1>Ghost You</h1> | |
| <p>Sit still and you are solid. Move, and you fade until the room shows through you. | |
| Your motion plays a theremin: higher on the screen is a higher note, more motion is louder.</p> | |
| <p>For the best haunting, step out of view for a few seconds so the room can learn what is behind you.</p> | |
| <button id="go">Summon the ghost</button> | |
| <span id="err"></span> | |
| </div> | |
| <div id="bar" hidden> | |
| <button class="ghost" id="learn">Learn empty room (3 s)</button> | |
| <label>ghostliness <input id="ghostly" type="range" min="0" max="100" value="85"></label> | |
| <label>linger <input id="linger" type="range" min="0" max="98" value="88"></label> | |
| <label>volume <input id="vol" type="range" min="0" max="100" value="55"></label> | |
| <button class="ghost" id="mute">mute</button> | |
| </div> | |
| <video id="v" playsinline muted hidden></video> | |
| <script> | |
| const W = 320, H = 240, N = W * H; | |
| const cv = document.getElementById('c'), ctx = cv.getContext('2d', { willReadFrequently:true }); | |
| const video = document.getElementById('v'); | |
| const bg = new Float32Array(N * 3); // the room's memory: slow average of what is behind you | |
| const trail = new Float32Array(N); // lingering ghost mask | |
| let haveBg = false, learnUntil = 0, audio = null, muted = false; | |
| const $ = id => document.getElementById(id); | |
| $('go').onclick = async () => { | |
| try { | |
| const stream = await navigator.mediaDevices.getUserMedia({ video:{ width:640, height:480, facingMode:'user' }, audio:false }); | |
| video.srcObject = stream; await video.play(); | |
| audio = makeTheremin(); | |
| $('intro').remove(); $('bar').hidden = false; | |
| requestAnimationFrame(loop); | |
| } catch (e) { $('err').textContent = 'The camera would not open: ' + (e.message || e); } | |
| }; | |
| $('learn').onclick = () => { learnUntil = performance.now() + 3000; }; | |
| $('mute').onclick = () => { muted = !muted; $('mute').textContent = muted ? 'unmute' : 'mute'; }; | |
| function makeTheremin() { | |
| const ac = new (window.AudioContext || window.webkitAudioContext)(); | |
| const out = ac.createGain(); out.gain.value = 0; | |
| const osc = ac.createOscillator(); osc.type = 'sine'; osc.frequency.value = 440; | |
| const osc2 = ac.createOscillator(); osc2.type = 'sine'; osc2.frequency.value = 441.5; | |
| const g2 = ac.createGain(); g2.gain.value = .35; | |
| const lfo = ac.createOscillator(); lfo.frequency.value = 5.6; | |
| const lfoAmt = ac.createGain(); lfoAmt.gain.value = 6; | |
| lfo.connect(lfoAmt); lfoAmt.connect(osc.frequency); lfoAmt.connect(osc2.frequency); | |
| // a little haunted-hallway echo | |
| const delay = ac.createDelay(1); delay.delayTime.value = .33; | |
| const fb = ac.createGain(); fb.gain.value = .38; | |
| const lp = ac.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.value = 1800; | |
| osc.connect(out); osc2.connect(g2); g2.connect(out); | |
| out.connect(ac.destination); out.connect(delay); delay.connect(lp); lp.connect(fb); fb.connect(delay); lp.connect(ac.destination); | |
| osc.start(); osc2.start(); lfo.start(); | |
| return { ac, out, osc, osc2, lfoAmt }; | |
| } | |
| let energy = 0, cy = .5, cx = .5; | |
| function loop(t) { | |
| // mirror the camera so it behaves like a mirror | |
| ctx.save(); ctx.translate(W, 0); ctx.scale(-1, 1); ctx.drawImage(video, 0, 0, W, H); ctx.restore(); | |
| const img = ctx.getImageData(0, 0, W, H), d = img.data; | |
| const learning = t < learnUntil; | |
| if (!haveBg) { for (let i = 0; i < N; i++) { bg[3*i] = d[4*i]; bg[3*i+1] = d[4*i+1]; bg[3*i+2] = d[4*i+2]; } haveBg = true; } | |
| const ghostly = $('ghostly').value / 100, linger = $('linger').value / 100; | |
| let mSum = 0, ySum = 0, xSum = 0; | |
| for (let i = 0; i < N; i++) { | |
| const r = d[4*i], g = d[4*i+1], b = d[4*i+2]; | |
| const br = bg[3*i], bgG = bg[3*i+1], bb = bg[3*i+2]; | |
| const diff = (Math.abs(r - br) + Math.abs(g - bgG) + Math.abs(b - bb)) / 3; | |
| // motion against the remembered room, softened to 0..1 | |
| let m = Math.min(1, Math.max(0, (diff - 18) / 40)); | |
| trail[i] = Math.max(m, trail[i] * linger); | |
| const a = trail[i] * ghostly; | |
| // the room learns fast while you're away or during "learn", barely at all where something moves | |
| const rate = learning ? .25 : (m < .05 ? .03 : .0015); | |
| bg[3*i] += rate * (r - br); bg[3*i+1] += rate * (g - bgG); bg[3*i+2] += rate * (b - bb); | |
| // see-through: show the room where you moved, with a cold spectral edge glow | |
| const lum = (r + g + b) / 3; | |
| const edge = a * (1 - a) * 4; // strongest at the half-faded rim | |
| const flick = 1 + (Math.random() - .5) * .08 * a; | |
| d[4*i] = ((1 - a) * r + a * (br * .82 + lum * .18 * .7)) * flick + edge * 40; | |
| d[4*i+1] = ((1 - a) * g + a * (bgG * .85 + lum * .18 * .85)) * flick + edge * 70; | |
| d[4*i+2] = ((1 - a) * b + a * (bb * .9 + lum * .18)) * flick + edge * 110; | |
| if (m > .2) { mSum += m; ySum += m * ((i / W) | 0); xSum += m * (i % W); } | |
| } | |
| ctx.putImageData(img, 0, 0); | |
| // theremin: height of your motion is pitch, amount of motion is volume, left/right is wobble | |
| const frac = mSum / N; | |
| energy += (Math.min(1, frac * 12) - energy) * .12; | |
| if (mSum > 30) { cy += (ySum / mSum / H - cy) * .15; cx += (xSum / mSum / W - cx) * .15; } | |
| if (audio) { | |
| const now = audio.ac.currentTime; | |
| const f = 140 * Math.pow(2, (1 - cy) * 3.2); // about 140 Hz to 1300 Hz | |
| audio.osc.frequency.setTargetAtTime(f, now, .06); | |
| audio.osc2.frequency.setTargetAtTime(f * 1.0035, now, .06); | |
| audio.lfoAmt.gain.setTargetAtTime(2 + cx * 14, now, .2); | |
| const v = muted ? 0 : energy * $('vol').value / 100 * .5; | |
| audio.out.gain.setTargetAtTime(v, now, .08); | |
| } | |
| if (learning) { ctx.fillStyle = 'rgba(217,228,255,.85)'; ctx.font = '14px Georgia'; | |
| ctx.fillText('learning the empty room… ' + Math.ceil((learnUntil - t) / 1000), 10, 20); } | |
| requestAnimationFrame(loop); | |
| } | |
| </script> | |
| </body> | |
| </html> | |