<script>
(() => {
/* ---------- CSS ---------- */
const style = document.createElement("style");
style.textContent = `
.ascii-wrap{
position:relative;
width:100%;
max-width:1000px;
margin:0 auto;
background:black;
overflow:hidden;
aspect-ratio:16/9;
touch-action:none;
cursor:pointer;
}
.ascii-video{
position:absolute; inset:0;
width:100%; height:100%;
object-fit:contain;
background:black;
opacity:0;
pointer-events:none;
}
.flash-canvas,.ascii-canvas{
position:absolute; inset:0;
width:100%; height:100%;
pointer-events:none;
background:transparent;
}
.flash-canvas{ z-index:1; }
.ascii-canvas{ z-index:2; }
.ascii-link-canvas{
position:fixed; inset:0;
width:100vw; height:100vh;
pointer-events:none;
z-index:2147483647;
background:transparent;
}
`;
document.head.appendChild(style);
/* ---------- GLOBALS ---------- */
const instances = [];
let linkCanvas, lctx;
let currentPairs = [];
let linkUntil = 0;
const MATCH_DIST = 170;
const MATCH_HOLD_MS = 1200;
const SAMPLE_GRID = 24;
// +0.5s spacing between match events
const COOLDOWN_MS = 500;
let cooldownUntil = 0;
// cycle: off 2s, on 4s, off 1s
const CYCLE = [
{ dur: 2000, on: false },
{ dur: 4000, on: true },
{ dur: 1000, on: false }
];
let cycleStart = performance.now();
let cycleIndex = 0;
function cycleState(now){
let t = now - cycleStart;
while (t > CYCLE[cycleIndex].dur){
t -= CYCLE[cycleIndex].dur;
cycleIndex = (cycleIndex + 1) % CYCLE.length;
cycleStart = now - t;
}
return CYCLE[cycleIndex].on;
}
// cursor / touch tracking (screen coords)
let cursor = { x: 0, y: 0, active: false };
function setCursor(x,y){
cursor.x = x;
cursor.y = y;
cursor.active = true;
}
window.addEventListener("pointermove", e => setCursor(e.clientX, e.clientY), { passive:true });
window.addEventListener("pointerdown", e => setCursor(e.clientX, e.clientY), { passive:true });
window.addEventListener("pointerleave", () => cursor.active=false, { passive:true });
window.addEventListener("touchstart", e=>{
if(e.touches && e.touches[0]) setCursor(e.touches[0].clientX, e.touches[0].clientY);
}, { passive:true });
window.addEventListener("touchmove", e=>{
if(e.touches && e.touches[0]) setCursor(e.touches[0].clientX, e.touches[0].clientY);
}, { passive:true });
window.addEventListener("touchend", ()=>{ cursor.active=false; }, { passive:true });
/* ---------- LINK CANVAS ---------- */
function ensureLinkCanvas(){
if(linkCanvas || !document.body) return;
linkCanvas = document.createElement("canvas");
linkCanvas.className = "ascii-link-canvas";
document.body.appendChild(linkCanvas);
lctx = linkCanvas.getContext("2d");
function resize(){
linkCanvas.width = window.innerWidth;
linkCanvas.height = window.innerHeight;
lctx.setLineDash([9,9]);
lctx.lineWidth = 1.5;
lctx.shadowColor = "rgba(255,255,255,0.45)";
lctx.shadowBlur = 4;
lctx.font = "11px monospace";
lctx.textBaseline = "top";
}
resize();
window.addEventListener("resize", resize);
}
/* ---------- INIT ONE BLOCK ---------- */
function initOne(wrap){
if(wrap.dataset.asciiInit==="1") return;
wrap.dataset.asciiInit="1";
const video = wrap.querySelector(".ascii-video");
const flashCanvas = wrap.querySelector(".flash-canvas");
const asciiCanvas = wrap.querySelector(".ascii-canvas");
if(!video || !flashCanvas || !asciiCanvas) return;
const chars = " .:-=+*#%@";
const sampleCanvas = document.createElement("canvas");
const sctx = sampleCanvas.getContext("2d",{willReadFrequently:true});
const fctx = flashCanvas.getContext("2d");
const actx = asciiCanvas.getContext("2d");
const BLACK_CUTOFF = 38;
let vis={w:0,h:0,left:0,top:0};
let visReady=false;
const colsBase=75;
let cols=colsBase;
const fps=22;
let lastFrame=0;
let baseGlint=0.03;
let glintStrength=baseGlint;
let isPointerDown=false;
let halo={active:false,x:0,y:0,r:120,strength:1};
let flash={alpha:0,blocks:[],drift:0,vx:0,jitter:0};
function setAspectAndFit(){
if(!video.videoWidth||!video.videoHeight) return;
wrap.style.aspectRatio=`${video.videoWidth}/${video.videoHeight}`;
const r=wrap.getBoundingClientRect();
const vAR=video.videoWidth/video.videoHeight;
const wAR=r.width/r.height;
if(wAR>vAR){
vis.h=r.height;
vis.w=r.height*vAR;
vis.left=(r.width-vis.w)/2;
vis.top=0;
}else{
vis.w=r.width;
vis.h=r.width/vAR;
vis.left=0;
vis.top=(r.height-vis.h)/2;
}
[flashCanvas,asciiCanvas].forEach(c=>{
c.style.left=vis.left+"px";
c.style.top=vis.top+"px";
c.style.width=vis.w+"px";
c.style.height=vis.h+"px";
c.width=Math.floor(vis.w);
c.height=Math.floor(vis.h);
});
actx.imageSmoothingEnabled=false;
fctx.imageSmoothingEnabled=false;
sctx.imageSmoothingEnabled=false;
halo.r = Math.max(85, vis.w * 0.16);
visReady=vis.w>10 && vis.h>10;
}
video.addEventListener("loadedmetadata", setAspectAndFit);
video.addEventListener("canplay", setAspectAndFit);
window.addEventListener("resize", setAspectAndFit);
function computeScrollBoost(){
const rect=wrap.getBoundingClientRect();
const vh=window.innerHeight||800;
const topNorm=Math.min(Math.max(1-rect.top/vh,0),1);
return topNorm*topNorm*0.12;
}
function render(t){
if(t-lastFrame<1000/fps){ requestAnimationFrame(render); return; }
lastFrame=t;
if(video.readyState<2 || !visReady){
requestAnimationFrame(render); return;
}
/* ----- FLASHES (under ASCII) ----- */
fctx.clearRect(0,0,flashCanvas.width,flashCanvas.height);
if(flash.alpha>0 && flash.blocks.length){
fctx.save(); fctx.globalAlpha=flash.alpha;
flash.drift+=flash.vx;
if(Math.random()<0.06) flash.vx*=-1;
if(Math.random()<0.04) flash.jitter=(Math.random()-0.5)*0.08;
flash.jitter*=0.85;
for(const b of flash.blocks){
const wob=b.wobble*Math.sin(t*0.01+b.phase);
let driftX=flash.drift+wob+flash.jitter;
driftX=Math.max(-b.left,Math.min(1-(b.left+b.w),driftX));
const sx=(b.left+driftX)*video.videoWidth;
const sy=b.top*video.videoHeight;
const sw=b.w*video.videoWidth;
const sh=b.h*video.videoHeight;
const dx=(b.left+driftX)*vis.w;
const dy=b.top*vis.h;
const dw=b.w*vis.w;
const dh=b.h*vis.h;
fctx.drawImage(video,sx,sy,sw,sh,dx,dy,dw,dh);
}
fctx.restore();
flash.alpha*=0.97;
if(flash.alpha<0.03) flash.alpha=0;
}
/* ----- ASCII ----- */
glintStrength=baseGlint+computeScrollBoost();
const cellW=vis.w/cols;
const cellH=cellW*2.0;
const rows=Math.floor(vis.h/cellH);
sampleCanvas.width=cols;
sampleCanvas.height=rows;
sctx.drawImage(video,0,0,cols,rows);
let data;
try{ data=sctx.getImageData(0,0,cols,rows).data; }
catch{
actx.clearRect(0,0,asciiCanvas.width,asciiCanvas.height);
actx.fillStyle="white";
actx.fillText("CORS blocked. Allow your domain in Cloudinary.",10,30);
requestAnimationFrame(render); return;
}
actx.clearRect(0,0,asciiCanvas.width,asciiCanvas.height);
actx.save(); actx.globalAlpha=0.30; actx.fillStyle="black";
actx.fillRect(0,0,asciiCanvas.width,asciiCanvas.height);
actx.restore();
actx.font=`${cellH}px monospace`;
actx.textBaseline="top";
let p=0;
for(let y=0;y<rows;y++){
for(let x=0;x<cols;x++){
const r=data[p], g=data[p+1], b=data[p+2];
const bright=(r+g+b)/3;
const idx=Math.floor((bright/255)*(chars.length-1));
let ch=chars[idx];
const isBlack=bright<BLACK_CUTOFF;
const maxc=Math.max(r,g,b), minc=Math.min(r,g,b);
const sat=maxc===0?0:(maxc-minc)/maxc;
const cx=x*cellW+cellW*0.5;
const cy=y*cellH+cellH*0.5;
let haloStrength=0;
if(!isBlack && halo.active){
const dxh=cx-halo.x, dyh=cy-halo.y;
const d=Math.sqrt(dxh*dxh+dyh*dyh);
if(d<halo.r) haloStrength=(1-d/halo.r)*halo.strength;
}
if(haloStrength>0 && Math.random()<haloStrength*0.85){
ch=chars[Math.floor(Math.random()*chars.length)];
}
const jitterX = haloStrength>0
? (Math.sin(t*0.024+cy*0.05)*haloStrength*cellW*1.1
+ (Math.random()-0.5)*haloStrength*cellW*0.9)
: 0;
const glintProb=(isBlack?0:glintStrength)+haloStrength*0.55;
const isGlint=!isBlack && sat>0.25 && bright>55 && Math.random()<glintProb;
let gray=Math.floor(195+(bright/255)*60);
let rr=gray, gg=gray, bb=gray;
if(isGlint){
const boost=1.35+haloStrength*2.2;
rr=Math.min(255,r*boost);
gg=Math.min(255,g*boost);
bb=Math.min(255,b*boost);
const punch=18+haloStrength*55;
rr=Math.min(255,rr+punch);
gg=Math.min(255,gg+punch);
bb=Math.min(255,bb+punch);
}
actx.fillStyle=`rgb(${rr|0},${gg|0},${bb|0})`;
actx.fillText(ch,x*cellW+jitterX,y*cellH);
p+=4;
}
}
requestAnimationFrame(render);
}
requestAnimationFrame(render);
/* ----- HARDENED AUTOPLAY ----- */
video.muted = true;
video.setAttribute("muted", "");
video.setAttribute("playsinline", "");
video.setAttribute("autoplay", "");
video.setAttribute("loop", "");
const tryPlay = () => {
const p = video.play();
if (p && typeof p.catch === "function") p.catch(()=>{});
};
tryPlay();
setTimeout(tryPlay, 600);
setTimeout(tryPlay, 1600);
const unlock = () => { tryPlay(); };
window.addEventListener("pointerdown", unlock, { once:true });
window.addEventListener("touchstart", unlock, { once:true });
window.addEventListener("keydown", unlock, { once:true });
document.addEventListener("visibilitychange", () => {
if (!document.hidden) tryPlay();
});
/* ----- TEARS avg ~38.5% ----- */
function buildIrregularTears(){
const targetArea=0.385;
const blocks=[];
let areaLeft=targetArea;
const bands=4+Math.floor(Math.random()*6);
for(let i=0;i<bands;i++){
const hBand=0.018+Math.random()*0.075;
const segs=2+Math.floor(Math.random()*5);
const top=Math.random()*(1-hBand);
for(let s=0;s<segs;s++){
if(areaLeft<=0) break;
let wSeg=0.10+Math.random()*0.40;
const desired=areaLeft/hBand;
wSeg=Math.min(wSeg,desired*(0.55+Math.random()*0.9));
wSeg=Math.min(Math.max(wSeg,0.07),0.95);
const left=Math.random()*(1-wSeg);
blocks.push({
left, top, w:wSeg, h:hBand,
wobble:0.01+Math.random()*0.03,
phase:Math.random()*Math.PI*2
});
areaLeft-=wSeg*hBand;
}
}
const chips=3+Math.floor(Math.random()*5);
for(let c=0;c<chips;c++){
const w=0.035+Math.random()*0.10;
const h=0.012+Math.random()*0.05;
blocks.push({
left:Math.random()*(1-w),
top:Math.random()*(1-h),
w,h,
wobble:0.006+Math.random()*0.025,
phase:Math.random()*Math.PI*2
});
}
return blocks;
}
function doFlashBurst(dramatic=false){
const hits=dramatic?3:(1+Math.floor(Math.random()*2));
let i=0;
function one(){
flash.blocks=buildIrregularTears();
flash.alpha=dramatic?1.0:0.95;
flash.drift=0;
flash.vx=(Math.random()-0.5)*(dramatic?0.05:0.02);
const onDur=dramatic?220:(70+Math.random()*220);
setTimeout(()=>{
i++;
if(i<hits){
const gap=dramatic?120:(120+Math.random()*320);
setTimeout(one,gap);
}
},onDur);
}
one();
}
/* ----- FULL-FRAME FLASH OF RAW VIDEO ----- */
function doFullFrameFlash(){
// one block covering entire frame
flash.blocks = [{
left: 0,
top: 0,
w: 1,
h: 1,
wobble: 0,
phase: 0
}];
flash.alpha = 0.95; // strong but still under ASCII
flash.drift = 0;
flash.vx = 0;
flash.jitter = 0;
// now ~0.25–0.45s
setTimeout(()=>{
flash.alpha = 0;
flash.blocks = [];
}, 250 + Math.random()*200);
}
(function flashLoop(){
const wait=4000+Math.random()*9000;
setTimeout(()=>{ doFlashBurst(false); flashLoop(); },wait);
})();
/* ----- LOOP: TRIGGER FULL-FRAME FLASH EVERY 4–8s ----- */
(function fullFlashLoop(){
const wait = 4000 + Math.random()*4000; // 4–8s
setTimeout(()=>{
doFullFrameFlash();
fullFlashLoop();
}, wait);
})();
/* ----- INTERACTION ----- */
function toHaloCoords(clientX,clientY){
const rect=wrap.getBoundingClientRect();
const x=clientX-rect.left-vis.left;
const y=clientY-rect.top-vis.top;
halo.x=Math.min(Math.max(x,0),vis.w);
halo.y=Math.min(Math.max(y,0),vis.h);
}
wrap.addEventListener("pointermove",(e)=>{
toHaloCoords(e.clientX,e.clientY);
halo.active=true;
halo.strength=isPointerDown?1.3:1.0;
});
wrap.addEventListener("pointerleave",()=>halo.active=false);
wrap.addEventListener("click",(e)=>{
e.preventDefault(); doFlashBurst(true);
});
wrap.addEventListener("pointerdown",(e)=>{
e.preventDefault();
isPointerDown=true;
try{ wrap.setPointerCapture(e.pointerId); }catch{}
toHaloCoords(e.clientX,e.clientY);
const rect=wrap.getBoundingClientRect();
const nx=Math.min(Math.max((e.clientX-rect.left)/rect.width,0),1);
cols=Math.round(40+nx*90);
});
wrap.addEventListener("pointerup",(e)=>{
isPointerDown=false;
try{ wrap.releasePointerCapture(e.pointerId); }catch{}
cols=colsBase;
halo.strength=1.0;
});
instances.push({
wrap, video,
dominantRGB:null,
dominantPos:null,
isReady:()=>{
const r=wrap.getBoundingClientRect();
return r.width>10 && r.height>10;
},
mapNormToScreen:(xNorm, yNorm)=>{
const r=wrap.getBoundingClientRect();
const x = r.left + vis.left + xNorm * vis.w;
const y = r.top + vis.top + yNorm * vis.h;
return {x,y};
}
});
}
/* ---------- RGB dominant sampling WITH POSITION ---------- */
const tinyC = document.createElement("canvas");
const tinyCtx = tinyC.getContext("2d", { willReadFrequently:true });
function sampleDominantRGBWithPos(inst){
const v = inst.video;
if (v.readyState < 2) return null;
tinyC.width = SAMPLE_GRID;
tinyC.height = SAMPLE_GRID;
tinyCtx.drawImage(v, 0, 0, SAMPLE_GRID, SAMPLE_GRID);
let d;
try { d = tinyCtx.getImageData(0,0,SAMPLE_GRID,SAMPLE_GRID).data; }
catch { return null; }
let rs=0, gs=0, bs=0, n=0;
for (let i=0; i<d.length; i+=4){
const r=d[i], g=d[i+1], b=d[i+2];
const bright=(r+g+b)/3;
if (bright < 30) continue;
rs+=r; gs+=g; bs+=b; n++;
}
if (!n) return null;
const mean = { r: rs/n, g: gs/n, b: bs/n };
let bestDist = 1e9, bestPx=SAMPLE_GRID/2, bestPy=SAMPLE_GRID/2;
for (let py=0; py<SAMPLE_GRID; py++){
for (let px=0; px<SAMPLE_GRID; px++){
const i = (py*SAMPLE_GRID + px)*4;
const r=d[i], g=d[i+1], b=d[i+2];
const bright=(r+g+b)/3;
if (bright < 30) continue;
const dr=r-mean.r, dg=g-mean.g, db=b-mean.b;
const dist = dr*dr + dg*dg + db*db;
if (dist < bestDist){
bestDist = dist;
bestPx = px; bestPy = py;
}
}
}
return {
mean,
pos:{
xNorm: bestPx/(SAMPLE_GRID-1),
yNorm: bestPy/(SAMPLE_GRID-1)
}
};
}
function colorDist(A,B){
const dr=A.r-B.r, dg=A.g-B.g, db=A.b-B.b;
return Math.sqrt(dr*dr + dg*dg + db*db);
}
function checkMatches(){
const now = performance.now();
if (now < cooldownUntil) return;
const ready = instances.filter(i => i.isReady());
if (ready.length < 2) return;
ready.forEach(inst=>{
const out = sampleDominantRGBWithPos(inst);
if (out){
inst.dominantRGB = out.mean;
inst.dominantPos = out.pos;
}
});
const matches = [];
for (let i=0; i<ready.length; i++){
for (let j=i+1; j<ready.length; j++){
const A = ready[i].dominantRGB;
const B = ready[j].dominantRGB;
const Ap = ready[i].dominantPos;
const Bp = ready[j].dominantPos;
if (!A || !B || !Ap || !Bp) continue;
if (colorDist(A,B) < MATCH_DIST){
const aPoint = ready[i].mapNormToScreen(Ap.xNorm, Ap.yNorm);
const bPoint = ready[j].mapNormToScreen(Bp.xNorm, Bp.yNorm);
const arcSign = Math.random() < 0.5 ? -1 : 1;
const arcMag = 0.12 + Math.random()*0.28;
const arcBase = Math.random()*Math.PI*2;
matches.push({
ax:aPoint.x, ay:aPoint.y,
bx:bPoint.x, by:bPoint.y,
arcSign, arcMag, arcBase
});
}
}
}
if (matches.length){
linkUntil = now + MATCH_HOLD_MS;
currentPairs = matches;
cooldownUntil = now + MATCH_HOLD_MS + COOLDOWN_MS;
} else if (now > linkUntil){
currentPairs = [];
linkUntil = 0;
}
}
/* ---------- helpers for tags ---------- */
function drawTag(text, x, y){
const padX = 4, padY = 2;
const w = lctx.measureText(text).width;
const h = 12;
lctx.fillStyle = "rgba(255,0,0,0.95)";
lctx.fillRect(x, y, w + padX*2, h + padY*2);
lctx.fillStyle = "white";
lctx.fillText(text, x + padX, y + padY);
}
function drawLinks(){
if(!lctx) return;
const now=performance.now();
lctx.clearRect(0,0,linkCanvas.width,linkCanvas.height);
const active = cycleState(now);
if(!active || now>linkUntil || !currentPairs.length) return;
const pulse = 0.7 + 0.3*Math.sin(now*0.012);
lctx.save();
lctx.strokeStyle=`rgba(255,255,255,${pulse})`;
lctx.setLineDash([9,9]);
lctx.lineWidth = 1.5;
lctx.font = "11px monospace";
// draw main lines + endpoints + endpoint tags
currentPairs.forEach(p=>{
const {ax,ay,bx,by} = p;
if(!isFinite(ax+ay+bx+by)) return;
const dx = bx-ax, dy = by-ay;
const len = Math.sqrt(dx*dx + dy*dy) || 1;
const nx = -dy/len, ny = dx/len;
const wobble = Math.sin(now*0.004 + p.arcBase) * 0.6;
const bend = len * p.arcMag * p.arcSign * (1 + wobble);
const cx = (ax+bx)/2 + nx*bend;
const cy = (ay+by)/2 + ny*bend;
lctx.beginPath();
lctx.moveTo(ax,ay);
lctx.quadraticCurveTo(cx,cy,bx,by);
lctx.stroke();
// red endpoint circles
const r0 = 4.5;
lctx.setLineDash([]);
lctx.fillStyle = "rgba(255,0,0,0.95)";
lctx.beginPath(); lctx.arc(ax,ay,r0,0,Math.PI*2); lctx.fill();
lctx.beginPath(); lctx.arc(bx,by,r0,0,Math.PI*2); lctx.fill();
// endpoint tags
const offset = 7;
drawTag(`(${ax.toFixed(0)},${ay.toFixed(0)})`, ax + offset, ay + offset);
drawTag(`(${bx.toFixed(0)},${by.toFixed(0)})`, bx + offset, by + offset);
lctx.setLineDash([9,9]); // restore dashes
});
// extra line to cursor + cursor tag
if(cursor.active){
// use the nearest endpoint among ALL active lines
let best = null;
let bestD = Infinity;
currentPairs.forEach(p=>{
const dA = Math.hypot(cursor.x - p.ax, cursor.y - p.ay);
const dB = Math.hypot(cursor.x - p.bx, cursor.y - p.by);
if(dA < bestD){ bestD = dA; best = {x:p.ax,y:p.ay}; }
if(dB < bestD){ bestD = dB; best = {x:p.bx,y:p.by}; }
});
if(best){
// thin dashed line from nearest dot to cursor
lctx.save();
lctx.strokeStyle=`rgba(255,255,255,${pulse})`;
lctx.setLineDash([6,7]);
lctx.lineWidth = 1.2;
lctx.beginPath();
lctx.moveTo(best.x, best.y);
lctx.lineTo(cursor.x, cursor.y);
lctx.stroke();
lctx.restore();
// cursor tag
drawTag(`(${cursor.x.toFixed(0)},${cursor.y.toFixed(0)})`, cursor.x + 8, cursor.y + 8);
}
}
lctx.restore();
}
function bootAll(){
ensureLinkCanvas();
document.querySelectorAll(".ascii-wrap").forEach(initOne);
}
const bootTimer=setInterval(()=>{
if(document.body && (document.readyState==="interactive"||document.readyState==="complete")){
clearInterval(bootTimer);
bootAll();
setTimeout(()=>{
setInterval(checkMatches, 120);
(function linkLoop(){ drawLinks(); requestAnimationFrame(linkLoop); })();
}, 1600);
const obs=new MutationObserver(()=>bootAll());
obs.observe(document.body,{childList:true,subtree:true});
}
},200);
})();
</script>