glytch/index.html
type-two 1a769e6cbd Baseline: GLYTCH prototype, FKTRY lore codex, scaffold + 5-lane orchestration docs
- index.html: WIMVEE GLYTCH match-the-glitch prototype (playable)
- docs/FKTRY_LORE.md: world bible / MODELBEAST asset codex
- fktry/: vite+three+TS scaffold, contracts, main loop, seed data
- fktry/MASTERPLAN.md + CONTRACTS.md + lanes/LANE-{SIM,RENDER,UI,SCREEN,DATA}.md:
  round protocol and Round 1 orders for parallel Opus 4.8 executors

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 15:51:12 +10:00

330 lines
12 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>WIMVEE GLYTCH</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
:root{
--bg:#050208; --fg:#d8ffd8; --dim:#4a6b4a; --hot:#ff3fd4; --cy:#3fffe0;
}
*{box-sizing:border-box; margin:0; padding:0}
body{
background:var(--bg); color:var(--fg);
font-family:"Courier New", ui-monospace, monospace;
display:flex; flex-direction:column; align-items:center;
min-height:100vh; padding:16px; gap:10px;
text-transform:uppercase; letter-spacing:.08em;
}
h1{font-size:18px; color:var(--hot); text-shadow:2px 0 var(--cy), -2px 0 #ff2222; }
#screens{display:flex; gap:14px; flex-wrap:wrap; justify-content:center}
.panel{display:flex; flex-direction:column; gap:4px; align-items:center}
.panel .label{font-size:11px; color:var(--dim)}
canvas.screen{
width:380px; height:285px; image-rendering:pixelated;
border:1px solid #222; background:#000;
}
#hud{display:flex; gap:24px; align-items:center; font-size:13px}
#sync{font-size:22px; min-width:140px; text-align:center}
#sync.hot{color:var(--cy)}
#params{list-style:none; font-size:12px; width:772px; max-width:95vw;
display:grid; grid-template-columns:repeat(4,1fr); gap:3px 14px}
#params li{display:flex; gap:6px; align-items:center; color:var(--dim); padding:2px 4px}
#params li.sel{color:var(--fg); background:#131020; outline:1px solid #2a2440}
#params li .bar{flex:1; height:8px; background:#181425; position:relative; overflow:hidden}
#params li .bar i{position:absolute; left:0; top:0; bottom:0; background:var(--hot)}
#params li.sel .bar i{background:var(--cy)}
#help{font-size:10px; color:var(--dim); max-width:772px; text-align:center; line-height:1.7}
#round{color:var(--hot)}
#msg{font-size:14px; color:var(--cy); height:18px}
</style>
</head>
<body>
<h1>WIMVEE GLYTCH</h1>
<div id="hud">
<span>ROUND <span id="round">1</span></span>
<span id="sync">SYNC 00.0%</span>
<span id="msg"></span>
</div>
<div id="screens">
<div class="panel"><div class="label">TARGET SIGNAL</div><canvas id="cvT" class="screen" width="512" height="384"></canvas></div>
<div class="panel"><div class="label">YOUR SIGNAL</div><canvas id="cvP" class="screen" width="512" height="384"></canvas></div>
</div>
<ul id="params"></ul>
<div id="help">
←/→ or L1/R1: select property · ↑/↓ or d-pad: adjust · left stick Y: fine adjust ·
hold shift / R2: slow · N or START: new round · match the target signal to ≥96% sync
</div>
<script>
"use strict";
// ---- glitch parameter definitions -------------------------------------
const PARAMS = [
{ key:"mosh", name:"MOSH" }, // macroblock displacement
{ key:"melt", name:"MELT" }, // vertical pixel drip
{ key:"chroma", name:"CHROMA" }, // RGB channel split
{ key:"tear", name:"TEAR" }, // scanline horizontal tears
{ key:"roll", name:"V-HOLD" }, // vertical hold roll
{ key:"burn", name:"BURN" }, // film burn blotches
{ key:"noise", name:"STATIC" }, // analog snow
{ key:"freeze", name:"FREEZE" }, // time quantize + dropout
];
const N = PARAMS.length;
let target = new Float32Array(N);
let player = new Float32Array(N);
let sel = 0, round = 1, winTimer = 0, locked = false;
// ---- base "broadcast" texture: SMPTE bars + camcorder overlay ---------
function makeBaseTexture(){
const c = document.createElement("canvas");
c.width = 512; c.height = 384;
const g = c.getContext("2d");
const bars = ["#c0c0c0","#c0c000","#00c0c0","#00c000","#c000c0","#c00000","#0000c0"];
const bw = c.width / bars.length;
bars.forEach((col,i)=>{ g.fillStyle=col; g.fillRect(i*bw,0,bw+1,c.height*0.62); });
// castellations strip
const casts = ["#0000c0","#131313","#c000c0","#131313","#00c0c0","#131313","#c0c0c0"];
casts.forEach((col,i)=>{ g.fillStyle=col; g.fillRect(i*bw,c.height*0.62,bw+1,c.height*0.10); });
// pluge / ramp bottom
g.fillStyle="#101010"; g.fillRect(0,c.height*0.72,c.width,c.height*0.28);
const ramp = g.createLinearGradient(0,0,c.width,0);
ramp.addColorStop(0,"#000"); ramp.addColorStop(1,"#fff");
g.fillStyle=ramp; g.fillRect(c.width*0.55,c.height*0.80,c.width*0.4,20);
// camcorder overlay text
g.fillStyle="#eee"; g.font="bold 26px 'Courier New', monospace";
g.fillText("PLAY ▶", 24, c.height*0.86);
g.fillText("SP 0:12:34", 24, c.height*0.945);
g.font="bold 20px 'Courier New', monospace";
g.fillStyle="#f4f4f4";
g.fillText("WIMVEE GLYTCH", c.width*0.55, c.height*0.945);
g.fillStyle="#ff4444"; g.beginPath();
g.arc(c.width-30, c.height*0.83, 8, 0, Math.PI*2); g.fill();
return c;
}
// ---- webgl -------------------------------------------------------------
const VS = `
attribute vec2 p;
varying vec2 vUv;
void main(){ vUv = p*0.5+0.5; vUv.y = 1.0-vUv.y; gl_Position = vec4(p,0.,1.); }`;
const FS = `
precision mediump float;
varying vec2 vUv;
uniform sampler2D tex;
uniform float t;
uniform float u_mosh, u_melt, u_chroma, u_tear, u_roll, u_burn, u_noise, u_freeze;
float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1,311.7)))*43758.5453); }
float vnoise(vec2 p){
vec2 i=floor(p), f=fract(p); f=f*f*(3.-2.*f);
return mix(mix(hash(i),hash(i+vec2(1,0)),f.x),
mix(hash(i+vec2(0,1)),hash(i+vec2(1,1)),f.x),f.y);
}
float fbm(vec2 p){ return 0.5*vnoise(p)+0.25*vnoise(p*2.3)+0.125*vnoise(p*5.1); }
void main(){
// freeze quantizes the timebase everything else animates on
float tt = mix(t, floor(t*2.5)/2.5, clamp(u_freeze*1.4,0.,1.));
vec2 uv = vUv;
// v-hold roll
uv.y = fract(uv.y + u_roll * tt * 0.35);
// scanline tears: bands shove sideways
float band = floor(uv.y * 36.0);
float tr = hash(vec2(band, floor(tt*7.0)));
if(tr > 1.0 - u_tear*0.85){
uv.x += (hash(vec2(band, floor(tt*7.0)+9.0)) - 0.5) * u_tear * 0.5;
}
// datamosh: macroblocks pick up stale offsets
vec2 blk = floor(uv * vec2(16.0, 12.0));
float mb = hash(blk + floor(tt*3.0));
if(mb > 1.0 - u_mosh*0.9){
vec2 off = vec2(hash(blk+1.7), hash(blk+4.2)) - 0.5;
uv += off * u_mosh * 0.35;
}
// melt: columns drip downward
float drip = pow(vnoise(vec2(uv.x*30.0, floor(tt*1.5))), 3.0);
uv.y -= u_melt * drip * uv.y * 0.9;
uv = fract(uv);
// chroma split
float ca = u_chroma * 0.03;
vec3 col;
col.r = texture2D(tex, uv + vec2( ca, 0.)).r;
col.g = texture2D(tex, uv).g;
col.b = texture2D(tex, uv - vec2( ca, ca*0.6)).b;
// static
float sn = hash(uv*vec2(431.0,917.0) + fract(tt)*13.0);
col = mix(col, vec3(sn), u_noise * 0.7);
// freeze dropout: grey flash lines
float dropo = step(0.985 - u_freeze*0.12, hash(vec2(floor(uv.y*90.0), floor(tt*5.0))));
col = mix(col, vec3(0.45), dropo * u_freeze);
// film burn: fbm blotch eats the frame, hot rim then black
float b = fbm(uv*3.0 + vec2(0., -tt*0.4));
float edge = smoothstep(1.0 - u_burn*1.1, 1.0 - u_burn*1.1 + 0.12, b);
vec3 rim = vec3(1.0, 0.55, 0.15);
col = mix(col, rim, edge * (1.0 - smoothstep(0.05, 0.35, edge)) * 2.0 * u_burn);
col = mix(col, vec3(0.02), smoothstep(0.3, 1.0, edge) * step(0.01, u_burn));
// faint scanlines for vibe (constant, not scored)
col *= 0.92 + 0.08 * sin(vUv.y * 384.0 * 3.1415);
gl_FragColor = vec4(col, 1.0);
}`;
function makeGL(canvas){
const gl = canvas.getContext("webgl", {antialias:false});
const compile = (type, src)=>{
const s = gl.createShader(type);
gl.shaderSource(s, src); gl.compileShader(s);
if(!gl.getShaderParameter(s, gl.COMPILE_STATUS))
throw new Error(gl.getShaderInfoLog(s));
return s;
};
const prog = gl.createProgram();
gl.attachShader(prog, compile(gl.VERTEX_SHADER, VS));
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, FS));
gl.linkProgram(prog);
if(!gl.getProgramParameter(prog, gl.LINK_STATUS))
throw new Error(gl.getProgramInfoLog(prog));
gl.useProgram(prog);
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 3,-1, -1,3]), gl.STATIC_DRAW);
const loc = gl.getAttribLocation(prog, "p");
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, makeBaseTexture());
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
const uni = { t: gl.getUniformLocation(prog, "t") };
for(const p of PARAMS) uni[p.key] = gl.getUniformLocation(prog, "u_"+p.key);
return { gl, uni };
}
const T = makeGL(document.getElementById("cvT"));
const P = makeGL(document.getElementById("cvP"));
function draw(ctx, vals, time){
const { gl, uni } = ctx;
gl.uniform1f(uni.t, time);
PARAMS.forEach((p,i)=> gl.uniform1f(uni[p.key], vals[i]));
gl.drawArrays(gl.TRIANGLES, 0, 3);
}
// ---- rounds & scoring ---------------------------------------------------
function newRound(){
target.fill(0); player.fill(0);
const active = Math.min(2 + Math.floor((round-1)/1.5), N);
const idx = [...Array(N).keys()].sort(()=>Math.random()-0.5).slice(0, active);
for(const i of idx) target[i] = 0.15 + Math.random()*0.8;
winTimer = 0; locked = false;
document.getElementById("round").textContent = round;
document.getElementById("msg").textContent = "";
}
function syncScore(){
let d = 0;
for(let i=0;i<N;i++) d += Math.abs(target[i]-player[i]);
return Math.max(0, 1 - d/ (N*0.45)) * 100;
}
// ---- ui -----------------------------------------------------------------
const list = document.getElementById("params");
PARAMS.forEach((p,i)=>{
const li = document.createElement("li");
li.innerHTML = `<span style="width:64px">${p.name}</span><span class="bar"><i></i></span>`;
list.appendChild(li);
});
function renderUI(score){
[...list.children].forEach((li,i)=>{
li.classList.toggle("sel", i===sel);
li.querySelector("i").style.width = (player[i]*100).toFixed(1)+"%";
});
const el = document.getElementById("sync");
el.textContent = "SYNC " + score.toFixed(1).padStart(4,"0") + "%";
el.classList.toggle("hot", score >= 96);
}
// ---- input ----------------------------------------------------------------
const keys = {};
addEventListener("keydown", e=>{
keys[e.key] = true;
const step = e.shiftKey ? 0.01 : 0.05;
if(e.key==="ArrowLeft"){ sel = (sel+N-1)%N; e.preventDefault(); }
if(e.key==="ArrowRight"){ sel = (sel+1)%N; e.preventDefault(); }
if(e.key==="ArrowUp" && !locked){ player[sel] = Math.min(1, player[sel] + step); e.preventDefault(); }
if(e.key==="ArrowDown" && !locked){ player[sel] = Math.max(0, player[sel] - step); e.preventDefault(); }
if(e.key==="n"||e.key==="N"){ round=1; newRound(); }
});
addEventListener("keyup", e=> keys[e.key]=false);
let prevPad = {};
function pollInput(dt){
if(locked) return;
const pad = navigator.getGamepads && navigator.getGamepads()[0];
if(pad){
const pressed = i => pad.buttons[i] && pad.buttons[i].pressed;
const was = i => prevPad[i];
if(pressed(4) && !was(4)) sel = (sel+N-1)%N; // L1
if(pressed(5) && !was(5)) sel = (sel+1)%N; // R1
if(pressed(9) && !was(9)){ round=1; newRound(); } // start
const slow = pad.buttons[7] && pad.buttons[7].value > 0.3 ? 0.25 : 1.0; // R2
if(pressed(12)) player[sel] = Math.min(1, player[sel] + 0.6*dt*slow); // dpad up
if(pressed(13)) player[sel] = Math.max(0, player[sel] - 0.6*dt*slow); // dpad down
if(pressed(14) && !was(14)) sel = (sel+N-1)%N;
if(pressed(15) && !was(15)) sel = (sel+1)%N;
const ly = pad.axes[1] || 0;
if(Math.abs(ly) > 0.15) player[sel] = Math.min(1, Math.max(0, player[sel] - ly*0.4*dt));
prevPad = {}; pad.buttons.forEach((b,i)=> prevPad[i]=b.pressed);
}
}
// ---- main loop --------------------------------------------------------------
let last = performance.now();
function frame(now){
const dt = Math.min(0.05, (now-last)/1000); last = now;
const t = now/1000;
pollInput(dt);
const score = syncScore();
if(!locked){
if(score >= 96){
winTimer += dt;
document.getElementById("msg").textContent = "LOCKING " + (2-winTimer).toFixed(1);
if(winTimer >= 2){
locked = true;
document.getElementById("msg").textContent = "SIGNAL LOCKED";
setTimeout(()=>{ round++; newRound(); }, 1200);
}
} else { winTimer = 0; if(!locked) document.getElementById("msg").textContent = ""; }
}
draw(T, target, t);
draw(P, player, t);
renderUI(score);
requestAnimationFrame(frame);
}
newRound();
requestAnimationFrame(frame);
</script>
</body>
</html>