← quik.run
NP@naman-parashar-s-team / sand-cardPublic

sand-card

A JS snippet on QuikRun, callable at quik.run/r/sand-card.

JS· node:20· 20 runs· 0 forks· updated 24m ago
Endpointquik.run/r/sand-cardRunFork
sand-card.js
// sand-card — a premium "buried metal card" reveal.
// WebGL2: shader sand (FBM dunes + golden glint), a persistent dig mask so the
// reveal STAYS (real excavation, no spring-back), a GPU particle spray at the dig
// front, and a bloom post-pass. The whole experience is the served HTML below.
//
// The bound import only selects ES-module mode in the sandbox (needed for the
// default export); the page itself pulls in no libraries.
import htm from "htm";
void htm;
 
const HTML = `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Buried premium metal card</title>
<style>
*{box-sizing:border-box}
html,body{margin:0;height:100%}
body{background:radial-gradient(120% 90% at 50% 20%, #0f0c07 0%, #030303 72%);
color:#b9ad92;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:22px;overflow:hidden}
.wrap{position:relative;width:600px;height:600px;border-radius:24px}
canvas{display:block;width:600px;height:600px;border-radius:24px;cursor:crosshair;touch-action:none}
.hint{font-size:11px;letter-spacing:.34em;text-transform:uppercase;color:#6a6048;opacity:.9}
.hint b{color:#c9a24b;font-weight:600}
</style>
</head>
<body>
<div class="wrap"><canvas id="gl"></canvas></div>
<div class="hint">drag to unearth &middot; <b>double-click</b> to rebury</div>
<script>
(function(){
"use strict";
var CSS = 600, DPR = Math.min(window.devicePixelRatio || 1, 2);
var W = Math.round(CSS * DPR), H = Math.round(CSS * DPR);
var canvas = document.getElementById("gl");
canvas.width = W; canvas.height = H;
var gl = canvas.getContext("webgl2", { alpha:false, antialias:false, premultipliedAlpha:false });
if(!gl){ document.body.innerHTML = "<p style='color:#c9a24b;font-family:monospace'>WebGL2 unavailable</p>"; return; }
var extF = gl.getExtension("EXT_color_buffer_float");
 
/* ---------- premium metal credit card, drawn to a 2D texture ---------- */
function makeCardTexture(){
var s = 2, cw = CSS*s, ch = CSS*s;
var cv = document.createElement("canvas"); cv.width = cw; cv.height = ch;
var c = cv.getContext("2d");
// scene backdrop (so bloom + edges sit on something)
var bg = c.createRadialGradient(cw*0.5, ch*0.32, 0, cw*0.5, ch*0.5, cw*0.72);
bg.addColorStop(0, "#0c0905"); bg.addColorStop(1, "#040302");
c.fillStyle = bg; c.fillRect(0,0,cw,ch);
 
// card geometry (landscape, ISO ~1.586:1)
var cardW = 404*s, cardH = 255*s, x = (cw-cardW)/2, y = (ch-cardH)/2, r = 22*s;
function rr(x,y,w,h,rad){ c.beginPath(); c.moveTo(x+rad,y); c.arcTo(x+w,y,x+w,y+h,rad); c.arcTo(x+w,y+h,x,y+h,rad); c.arcTo(x,y+h,x,y,rad); c.arcTo(x,y,x+w,y,rad); c.closePath(); }
 
// drop shadow (card sits proud of the sand -> casts down)
c.save(); c.shadowColor="rgba(0,0,0,0.8)"; c.shadowBlur=54*s; c.shadowOffsetY=30*s;
rr(x,y,cardW,cardH,r); c.fillStyle="#000"; c.fill(); c.restore();
 
// card thickness: a metal slab offset down-right peeks out as the 3D edge
var eg=c.createLinearGradient(x,y+cardH,x+cardW,y+cardH+16*s);
eg.addColorStop(0,"#3a342a"); eg.addColorStop(0.5,"#14110c"); eg.addColorStop(1,"#050505");
rr(x+3*s, y+9*s, cardW, cardH, r); c.fillStyle=eg; c.fill();
 
c.save(); rr(x,y,cardW,cardH,r); c.clip();
 
// base obsidian metal
var g = c.createLinearGradient(x,y,x+cardW,y+cardH);
g.addColorStop(0,"#1a1712"); g.addColorStop(0.45,"#0c0b09"); g.addColorStop(0.55,"#0a0a0c"); g.addColorStop(1,"#161310");
c.fillStyle=g; c.fillRect(x,y,cardW,cardH);
// warm top sheen
var sh = c.createRadialGradient(x+cardW*0.3,y,0,x+cardW*0.3,y, cardH*1.4);
sh.addColorStop(0,"rgba(201,162,75,0.10)"); sh.addColorStop(1,"rgba(0,0,0,0)");
c.fillStyle=sh; c.fillRect(x,y,cardW,cardH);
 
// brushed-metal microlines
c.save(); c.globalAlpha=0.05;
for(var i=0;i<420;i++){ c.strokeStyle = i%2? "#c9a24b":"#000"; c.lineWidth=0.6*s;
var yy=y+Math.random()*cardH; c.beginPath(); c.moveTo(x,yy); c.lineTo(x+cardW,yy+ (Math.random()-0.5)*2*s); c.stroke(); }
c.restore();
 
// guilloché engine-turned rosette (the premium security signature)
c.save(); c.translate(x+cardW*0.5, y+cardH*0.5); c.globalAlpha=0.16; c.lineWidth=0.5*s;
c.strokeStyle="#c9a24b";
for(var ring=0; ring<5; ring++){
var baseR = (58 + ring*17)*s, amp=(9+ring*1.4)*s, k=7+ring;
c.beginPath();
for(var a=0; a<=6.2832; a+=0.02){
var rad = baseR + amp*Math.cos(k*a) + amp*0.4*Math.sin((k+3)*a);
var px = Math.cos(a)*rad*1.5, py=Math.sin(a)*rad;
if(a===0) c.moveTo(px,py); else c.lineTo(px,py);
}
c.closePath(); c.stroke();
}
c.restore();
 
// EMV chip
var chx=x+40*s, chy=y+96*s, chw=58*s, chh=44*s;
var cg=c.createLinearGradient(chx,chy,chx+chw,chy+chh);
cg.addColorStop(0,"#f4e2a8"); cg.addColorStop(0.5,"#c9a24b"); cg.addColorStop(1,"#8a6a2a");
rr(chx,chy,chw,chh,7*s); c.fillStyle=cg; c.fill();
c.strokeStyle="rgba(60,40,10,0.6)"; c.lineWidth=1*s;
// chip contacts
c.beginPath();
c.moveTo(chx+chw*0.33,chy); c.lineTo(chx+chw*0.33,chy+chh);
c.moveTo(chx+chw*0.66,chy); c.lineTo(chx+chw*0.66,chy+chh);
c.moveTo(chx,chy+chh*0.5); c.lineTo(chx+chw,chy+chh*0.5);
c.moveTo(chx+chw*0.15,chy+chh*0.28); c.lineTo(chx+chw*0.85,chy+chh*0.28);
c.moveTo(chx+chw*0.15,chy+chh*0.72); c.lineTo(chx+chw*0.85,chy+chh*0.72);
c.stroke();
 
// contactless glyph
c.save(); c.translate(chx+chw+26*s, chy+chh*0.5); c.strokeStyle="#c9a24b"; c.lineWidth=2.4*s; c.lineCap="round";
for(var w=0; w<3; w++){ c.beginPath(); c.arc(0,0,(8+w*7)*s, -0.7, 0.7); c.stroke(); }
c.restore();
 
// holographic patch
var hx=x+cardW-92*s, hy=y+92*s, hw=52*s, hh=40*s;
var holo=c.createLinearGradient(hx,hy,hx+hw,hy+hh);
holo.addColorStop(0,"rgba(120,220,200,0.5)"); holo.addColorStop(0.4,"rgba(220,180,120,0.5)");
holo.addColorStop(0.7,"rgba(180,140,220,0.5)"); holo.addColorStop(1,"rgba(120,200,220,0.5)");
rr(hx,hy,hw,hh,8*s); c.fillStyle=holo; c.fill();
c.globalAlpha=0.5; c.strokeStyle="#efe3c0"; c.lineWidth=1*s; rr(hx,hy,hw,hh,8*s); c.stroke(); c.globalAlpha=1;
 
// brand wordmark
c.textBaseline="alphabetic";
c.fillStyle="#e7d6a6"; c.font= (26*s)+"px Georgia, 'Times New Roman', serif";
c.save(); c.setTransform(1,0,0,1,0,0);
c.fillStyle="#e7d6a6"; c.font= "700 "+(24*s)+"px Georgia, serif";
c.fillText("AUREUS", x+40*s, y+56*s);
c.font= (10*s)+"px ui-monospace, monospace"; c.fillStyle="#9c8a5e";
c.fillText("METAL · PRIVATE", x+40*s, y+72*s);
c.restore();
 
// embossed helper
function emboss(txt, ex, ey, font, spacing){
c.font=font; c.textBaseline="alphabetic";
var cx=ex;
for(var i2=0;i2<txt.length;i2++){
var ch2=txt[i2];
c.fillStyle="rgba(0,0,0,0.55)"; c.fillText(ch2, cx+1.5*s, ey+1.8*s);
var lg=c.createLinearGradient(cx,ey-18*s,cx,ey+4*s);
lg.addColorStop(0,"#f6e7b6"); lg.addColorStop(1,"#b89451");
c.fillStyle=lg; c.fillText(ch2, cx, ey);
cx += c.measureText(ch2).width + spacing;
}
}
// PAN
emboss("5412 7508 9021 1808", x+40*s, y+cardH-70*s, "600 "+(21.5*s)+"px ui-monospace, Menlo, monospace", 0.3*s);
// labels
c.fillStyle="#7d7150"; c.font=(8.5*s)+"px ui-monospace, monospace";
c.fillText("CARD HOLDER", x+40*s, y+cardH-44*s);
c.fillText("VALID THRU", x+cardW-150*s, y+cardH-44*s);
// name + date embossed
emboss("R. MALHOTRA", x+40*s, y+cardH-24*s, "600 "+(15*s)+"px ui-monospace, monospace", 0.4*s);
emboss("08/29", x+cardW-150*s, y+cardH-24*s, "600 "+(15*s)+"px ui-monospace, monospace", 0.4*s);
 
// network mark (generic gold dual-disc)
var nx=x+cardW-58*s, ny=y+cardH-40*s;
c.globalAlpha=0.95;
c.fillStyle="#d9a24b"; c.beginPath(); c.arc(nx,ny,16*s,0,6.2832); c.fill();
c.fillStyle="#f0d488"; c.beginPath(); c.arc(nx+16*s,ny,16*s,0,6.2832); c.fill();
c.globalCompositeOperation="overlay"; c.fillStyle="#8a6a2a";
c.beginPath(); c.arc(nx+8*s,ny,16*s,0,6.2832); c.fill(); c.globalCompositeOperation="source-over"; c.globalAlpha=1;
 
// beveled rim: light on top-left, dark on bottom-right => rounded metal edge
c.lineWidth=1.8*s;
c.strokeStyle="rgba(246,231,182,0.55)"; rr(x+1.4*s,y+1.4*s,cardW-2.8*s,cardH-2.8*s,r); c.stroke();
var bevg=c.createLinearGradient(x,y,x+cardW,y+cardH);
bevg.addColorStop(0,"rgba(255,244,210,0.0)"); bevg.addColorStop(0.55,"rgba(0,0,0,0.0)"); bevg.addColorStop(1,"rgba(0,0,0,0.55)");
c.lineWidth=2.6*s; c.strokeStyle=bevg; rr(x+2.6*s,y+2.6*s,cardW-5.2*s,cardH-5.2*s,r); c.stroke();
c.restore();
 
var tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,gl.RGBA,gl.UNSIGNED_BYTE,cv);
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.LINEAR);
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.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
return tex;
}
 
/* ---------- GL helpers ---------- */
function sh(type, src){ var s=gl.createShader(type); gl.shaderSource(s,src); gl.compileShader(s);
if(!gl.getShaderParameter(s,gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s)+"\\n"+src); return s; }
function prog(vs,fs){ var p=gl.createProgram(); gl.attachShader(p,sh(gl.VERTEX_SHADER,vs)); gl.attachShader(p,sh(gl.FRAGMENT_SHADER,fs));
gl.linkProgram(p); if(!gl.getProgramParameter(p,gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(p)); return p; }
function fbo(w,h,float){ var t=gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D,t);
var it = (float&&extF)? gl.RGBA16F: gl.RGBA, tp=(float&&extF)? gl.HALF_FLOAT: gl.UNSIGNED_BYTE;
gl.texImage2D(gl.TEXTURE_2D,0,it,w,h,0,gl.RGBA,tp,null);
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.LINEAR);
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);
var f=gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER,f);
gl.framebufferTexture2D(gl.FRAMEBUFFER,gl.COLOR_ATTACHMENT0,gl.TEXTURE_2D,t,0);
gl.bindFramebuffer(gl.FRAMEBUFFER,null); return {f:f,t:t,w:w,h:h}; }
 
var QVS = "#version 300 es\\nin vec2 p; out vec2 vUv; void main(){ vUv=p*0.5+0.5; gl_Position=vec4(p,0.,1.); }";
var quad = gl.createVertexArray(); gl.bindVertexArray(quad);
var qb=gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER,qb);
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,-1, 3,-1, -1,3]),gl.STATIC_DRAW);
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0,2,gl.FLOAT,false,0,0); gl.bindVertexArray(null);
function drawQuad(){ gl.bindVertexArray(quad); gl.drawArrays(gl.TRIANGLES,0,3); gl.bindVertexArray(null); }
 
/* ---------- noise (shared GLSL) ---------- */
var NOISE = [
"vec2 hash2(vec2 p){ p=vec2(dot(p,vec2(127.1,311.7)),dot(p,vec2(269.5,183.3))); return fract(sin(p)*43758.5453); }",
"float hash1(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); vec2 u=f*f*(3.-2.*f);",
" float a=hash1(i),b=hash1(i+vec2(1,0)),c=hash1(i+vec2(0,1)),d=hash1(i+vec2(1,1));",
" return mix(mix(a,b,u.x),mix(c,d,u.x),u.y); }",
"float fbm(vec2 p){ float v=0.,a=.5; for(int i=0;i<5;i++){ v+=a*vnoise(p); p*=2.03; a*=.5; } return v; }"
].join("\\n");
 
/* ---------- programs ---------- */
var digProg = prog(QVS, "#version 300 es\\nprecision highp float;\\nin vec2 vUv; out vec4 o;\\n"+
"uniform sampler2D uPrev; uniform vec2 uCur; uniform float uR; uniform float uStr; uniform float uAsp;\\n"+
"void main(){ float prev=texture(uPrev,vUv).r;\\n"+
" vec2 d=(vUv-uCur)*vec2(uAsp,1.0); float dist=length(d);\\n"+
" float brush=smoothstep(uR,0.0,dist)*uStr;\\n"+
" o=vec4(clamp(prev+brush,0.0,1.3),0.,0.,1.); }");
 
var sceneProg = prog(QVS, "#version 300 es\\nprecision highp float;\\nin vec2 vUv; out vec4 o;\\n"+
"uniform sampler2D uCard; uniform sampler2D uMask; uniform float uT; uniform float uAsp; uniform vec2 uPx; uniform vec2 uMouse;\\n"+ NOISE + "\\n"+
"const vec3 GOLD_HOT=vec3(1.0,0.86,0.56);\\n"+
// sand height: broad dunes + wind ripples + fine grain — this gives it FORM
"float dune(vec2 uv){ vec2 sp=uv*vec2(uAsp,1.0);\\n"+
" float d=fbm(sp*2.6);\\n"+
" float rip=0.5+0.5*sin(sp.x*28.0 + sp.y*6.0 + fbm(sp*2.6)*5.0);\\n"+
" float fine=vnoise(sp*115.0);\\n"+
" return d*0.68 + rip*0.075 + fine*0.045; }\\n"+
// effective surface = sand minus how much has been dug away (persistent)
"float field(vec2 uv){ return dune(uv)*0.5 - texture(uMask,uv).r*0.95; }\\n"+
"void main(){ vec2 uv=vUv;\\n"+
// surface normal from the height field -> lights dunes, ripples AND the dig slopes
" float hL=field(uv-vec2(uPx.x,0.0)), hR=field(uv+vec2(uPx.x,0.0));\\n"+
" float hD=field(uv-vec2(0.0,uPx.y)), hU=field(uv+vec2(0.0,uPx.y));\\n"+
" vec3 N=normalize(vec3((hL-hR)*185.0,(hD-hU)*185.0, 1.0));\\n"+
" vec3 L=normalize(vec3(-0.40,0.52,0.66));\\n"+
" float diff=clamp(dot(N,L),0.0,1.0);\\n"+
" float dug=texture(uMask,uv).r;\\n"+
" float remain=dune(uv)*0.5 - dug*0.95 + 0.30;\\n"+ // >0 sand present, <0 card
" float sandMask=smoothstep(0.0,0.05,remain);\\n"+
" vec3 shadow=vec3(0.14,0.100,0.038), lit=vec3(0.92,0.70,0.30);\\n"+
" vec3 sand=mix(shadow,lit, 0.36 + diff*0.80);\\n"+
" sand+=(vnoise(uv*940.0)-0.5)*0.04;\\n"+ // per-grain speckle
// sparse specular sparkle only where a grain faces the light
" vec3 Hh=normalize(L+vec3(0.0,0.0,1.0)); float spec=pow(clamp(dot(N,Hh),0.0,1.0),48.0);\\n"+
" float fleck=step(0.93, hash1(floor(uv*vec2(660.0,660.0))));\\n"+
" float tw=0.6+0.4*sin(uT*2.3 + floor(uv.x*660.0)*1.7);\\n"+
" sand+=GOLD_HOT*spec*fleck*tw*1.1;\\n"+
// card region (rounded-rect SDF) — used for parallax + metal sheen so it reads 3D
" vec2 cc=uv-vec2(0.5); vec2 hf=vec2(0.338,0.213); float crn=0.05;\\n"+
" vec2 qd=abs(cc)-hf+crn; float sd=length(max(qd,vec2(0.0)))+min(max(qd.x,qd.y),0.0)-crn;\\n"+
" float cardArea=smoothstep(0.006,-0.006,sd);\\n"+
// parallax: surface shifts with the cursor as if the slab tilts
" vec2 cuv=uv + (uMouse-0.5)*0.013*cardArea;\\n"+
" vec3 card=texture(uCard,cuv).rgb;\\n"+
" float ao=smoothstep(0.13,0.0,remain)*smoothstep(-0.13,0.0,remain);\\n"+ // contact shadow at the sand wall
" card*=mix(1.0,0.5,ao);\\n"+
" float rev=1.0-sandMask;\\n"+
// mouse-tilt specular sheen — a bright band sweeps the metal as you move
" vec2 md=uMouse-0.5;\\n"+
" float axis=dot(uv-0.5, normalize(vec2(0.82,-0.58)));\\n"+
" float sheen=exp(-pow((axis + md.x*0.6 - md.y*0.34)*6.5,2.0));\\n"+
" float brushed=0.72+0.28*sin(uv.y*520.0);\\n"+
" card+=vec3(1.0,0.94,0.74)*sheen*brushed*cardArea*rev*0.6;\\n"+
" vec3 col=mix(card,sand,sandMask);\\n"+
" o=vec4(col,1.0); }");
 
var brightProg = prog(QVS, "#version 300 es\\nprecision highp float;\\nin vec2 vUv; out vec4 o;\\n"+
"uniform sampler2D uT0; uniform float uThresh;\\n"+
"void main(){ vec3 c=texture(uT0,vUv).rgb; float l=dot(c,vec3(0.299,0.587,0.114));\\n"+
" float k=max(l-uThresh,0.0)/max(l,0.0001); o=vec4(c*k*smoothstep(0.0,0.3,l),1.); }");
 
var blurProg = prog(QVS, "#version 300 es\\nprecision highp float;\\nin vec2 vUv; out vec4 o;\\n"+
"uniform sampler2D uT0; uniform vec2 uDir;\\n"+
"void main(){ vec3 s=vec3(0.0); float w[5]=float[](0.2270,0.1946,0.1216,0.0540,0.0162);\\n"+
" s+=texture(uT0,vUv).rgb*w[0];\\n"+
" for(int i=1;i<5;i++){ s+=texture(uT0,vUv+uDir*float(i)).rgb*w[i]; s+=texture(uT0,vUv-uDir*float(i)).rgb*w[i]; }\\n"+
" o=vec4(s,1.); }");
 
var compProg = prog(QVS, "#version 300 es\\nprecision highp float;\\nin vec2 vUv; out vec4 o;\\n"+
"uniform sampler2D uScene; uniform sampler2D uBloom; uniform float uBloomI;\\n"+ NOISE + "\\n"+
"void main(){ vec3 c=texture(uScene,vUv).rgb; vec3 b=texture(uBloom,vUv).rgb;\\n"+
" c+=b*uBloomI;\\n"+
" float v=smoothstep(1.15,0.35,length(vUv-0.5)); c*=mix(0.72,1.0,v);\\n"+ // vignette
" c+= (hash1(vUv*vec2(1920.0,1080.0))-0.5)*0.02;\\n"+ // dither
" c=pow(c, vec3(0.95));\\n"+
" o=vec4(c,1.); }");
 
// particles (soft additive gold points)
var PMAX=2400;
var pPos=new Float32Array(PMAX*2), pVel=new Float32Array(PMAX*2), pLife=new Float32Array(PMAX);
var pData=new Float32Array(PMAX*3); // x,y,life for buffer
var pHead=0;
var pBuf=gl.createBuffer();
var pVao=gl.createVertexArray(); gl.bindVertexArray(pVao);
gl.bindBuffer(gl.ARRAY_BUFFER,pBuf); gl.bufferData(gl.ARRAY_BUFFER,pData.byteLength,gl.DYNAMIC_DRAW);
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0,3,gl.FLOAT,false,0,0); gl.bindVertexArray(null);
var partProg = prog(
"#version 300 es\\nin vec3 p; out float vL; uniform float uDpr;\\n"+
"void main(){ vL=p.z; gl_Position=vec4(p.xy*2.0-1.0,0.,1.); gl_PointSize=mix(1.0,3.6,p.z)*uDpr; }",
"#version 300 es\\nprecision highp float; in float vL; out vec4 o;\\n"+
"void main(){ vec2 d=gl_PointCoord-0.5; float m=smoothstep(0.5,0.0,length(d));\\n"+
" vec3 col=mix(vec3(0.42,0.30,0.13),vec3(0.95,0.78,0.45),vL);\\n"+
" o=vec4(col*m*vL*0.7, m*vL*0.8); }");
 
function spawn(ux,uy,vx,vy){
for(var n=0;n<3;n++){ var i=pHead; pHead=(pHead+1)%PMAX;
pPos[i*2]=ux; pPos[i*2+1]=uy;
var ang=Math.random()*6.283, spd=0.0015+Math.random()*0.004;
pVel[i*2]=Math.cos(ang)*spd + vx*0.35; pVel[i*2+1]=Math.sin(ang)*spd + vy*0.35 + 0.001;
pLife[i]=0.7+Math.random()*0.3;
}
}
function stepParticles(dt){
for(var i=0;i<PMAX;i++){ if(pLife[i]<=0){ pData[i*3+2]=0; continue; }
pVel[i*2+1]-=0.00006; // gravity (uv space, y up)
pVel[i*2]*=0.96; pVel[i*2+1]*=0.96;
pPos[i*2]+=pVel[i*2]; pPos[i*2+1]+=pVel[i*2+1];
pLife[i]-=dt*1.1;
pData[i*3]=pPos[i*2]; pData[i*3+1]=pPos[i*2+1]; pData[i*3+2]=Math.max(pLife[i],0);
}
gl.bindBuffer(gl.ARRAY_BUFFER,pBuf); gl.bufferSubData(gl.ARRAY_BUFFER,0,pData);
}
 
/* ---------- targets ---------- */
var cardTex = makeCardTexture();
var maskA=fbo(W,H,false), maskB=fbo(W,H,false);
var scene=fbo(W,H,true);
var bw=Math.round(W/2), bh=Math.round(H/2);
var bloomA=fbo(bw,bh,true), bloomB=fbo(bw,bh,true);
// clear masks to 0
[maskA,maskB].forEach(function(m){ gl.bindFramebuffer(gl.FRAMEBUFFER,m.f); gl.clearColor(0,0,0,1); gl.clear(gl.COLOR_BUFFER_BIT); });
gl.bindFramebuffer(gl.FRAMEBUFFER,null);
 
var ASP = W/H;
var mouse={x:0.5,y:0.5,px:0.5,py:0.5,on:false,mv:0};
function setM(e){ var b=canvas.getBoundingClientRect(); var t=e.touches?e.touches[0]:e;
var nx=(t.clientX-b.left)/b.width, ny=1.0-(t.clientY-b.top)/b.height;
mouse.px=mouse.x; mouse.py=mouse.y; mouse.x=nx; mouse.y=ny; mouse.on=true;
mouse.mv=Math.min(1.0, Math.hypot(nx-mouse.px,ny-mouse.py)*22.0); }
canvas.addEventListener("mousemove",setM);
canvas.addEventListener("touchmove",function(e){setM(e);e.preventDefault();},{passive:false});
canvas.addEventListener("mouseleave",function(){mouse.on=false;});
canvas.addEventListener("dblclick",function(){ [maskA,maskB].forEach(function(m){ gl.bindFramebuffer(gl.FRAMEBUFFER,m.f); gl.clearColor(0,0,0,1); gl.clear(gl.COLOR_BUFFER_BIT); }); gl.bindFramebuffer(gl.FRAMEBUFFER,null); });
 
function u(p,n){ return gl.getUniformLocation(p,n); }
var last=performance.now();
function frame(now){
var dt=Math.min((now-last)/1000,0.05); last=now;
 
// 1) dig into mask (persistent) — only when moving
var str = mouse.on ? (0.10 + mouse.mv*0.65) : 0.0;
if(str>0.001){
gl.bindFramebuffer(gl.FRAMEBUFFER,maskB.f); gl.viewport(0,0,W,H); gl.useProgram(digProg);
gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D,maskA.t); gl.uniform1i(u(digProg,"uPrev"),0);
gl.uniform2f(u(digProg,"uCur"),mouse.x,mouse.y); gl.uniform1f(u(digProg,"uR"),0.075);
gl.uniform1f(u(digProg,"uStr"),str*0.5); gl.uniform1f(u(digProg,"uAsp"),ASP);
drawQuad();
var tmp=maskA; maskA=maskB; maskB=tmp;
// spawn particles along the dig
spawn(mouse.x,mouse.y,(mouse.x-mouse.px),(mouse.y-mouse.py));
}
mouse.mv*=0.6;
 
stepParticles(dt);
 
// 2) scene
gl.bindFramebuffer(gl.FRAMEBUFFER,scene.f); gl.viewport(0,0,W,H); gl.useProgram(sceneProg);
gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D,cardTex); gl.uniform1i(u(sceneProg,"uCard"),0);
gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D,maskA.t); gl.uniform1i(u(sceneProg,"uMask"),1);
gl.uniform1f(u(sceneProg,"uT"),now/1000); gl.uniform1f(u(sceneProg,"uAsp"),ASP);
gl.uniform2f(u(sceneProg,"uPx"),2.2/W,2.2/H);
gl.uniform2f(u(sceneProg,"uMouse"),mouse.x,mouse.y);
drawQuad();
 
// 2b) particles additively into scene
gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA,gl.ONE);
gl.useProgram(partProg); gl.uniform1f(u(partProg,"uDpr"),DPR);
gl.bindVertexArray(pVao); gl.drawArrays(gl.POINTS,0,PMAX); gl.bindVertexArray(null);
gl.disable(gl.BLEND);
 
// 3) bloom bright-pass -> half res
gl.bindFramebuffer(gl.FRAMEBUFFER,bloomA.f); gl.viewport(0,0,bw,bh); gl.useProgram(brightProg);
gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D,scene.t); gl.uniform1i(u(brightProg,"uT0"),0);
gl.uniform1f(u(brightProg,"uThresh"),0.82); drawQuad();
// blur iterations
var src=bloomA,dst=bloomB;
for(var it=0; it<3; it++){
gl.bindFramebuffer(gl.FRAMEBUFFER,dst.f); gl.viewport(0,0,bw,bh); gl.useProgram(blurProg);
gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D,src.t); gl.uniform1i(u(blurProg,"uT0"),0);
gl.uniform2f(u(blurProg,"uDir"),1.4/bw,0.0); drawQuad();
var t1=src;src=dst;dst=t1;
gl.bindFramebuffer(gl.FRAMEBUFFER,dst.f); gl.viewport(0,0,bw,bh); gl.useProgram(blurProg);
gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D,src.t); gl.uniform1i(u(blurProg,"uT0"),0);
gl.uniform2f(u(blurProg,"uDir"),0.0,1.4/bh); drawQuad();
var t2=src;src=dst;dst=t2;
}
 
// 4) composite to screen
gl.bindFramebuffer(gl.FRAMEBUFFER,null); gl.viewport(0,0,W,H); gl.useProgram(compProg);
gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D,scene.t); gl.uniform1i(u(compProg,"uScene"),0);
gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D,src.t); gl.uniform1i(u(compProg,"uBloom"),1);
gl.uniform1f(u(compProg,"uBloomI"),0.5); drawQuad();
 
window.__ready=true;
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
`;
 
export default async function handler() {
return new Response(HTML, {
status: 200,
headers: { "content-type": "text/html; charset=utf-8" },
});
}
 
Fork this and make it yours

Describe a change in plain English and QuikRun deploys it to your own URL.

Fork snippet

Built on QuikRun by @naman-parashar-s-team · plain English in, live URL out