open-windows / index.html
PeetPedro's picture
Open Windows — generative viz; the seed is the scene (breeze + curtains + golden hour + eternal love)
04774e6 verified
Raw
History Blame Contribute Delete
10.2 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>Open Windows</title>
<meta name="description" content="A generative visualization: sheer curtains dancing in the smoothest breeze at an open window, golden hour, dust in the light. The style is the seed. It does not end.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;1,300;1,400&display=swap" rel="stylesheet">
<style>
:root{--ink:#f6ead2;--dim:#c9a878}
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent;user-select:none}
html,body{height:100%;overflow:hidden;background:#160b12}
canvas{position:fixed;inset:0;width:100%;height:100%;display:block}
#veil{position:fixed;inset:0;z-index:2;pointer-events:none;display:flex;flex-direction:column;align-items:center;justify-content:flex-end;padding:0 24px 8vh;text-align:center}
#ded{font-family:'Cormorant Garamond',Georgia,serif;font-style:italic;font-weight:300;
font-size:clamp(16px,3vw,25px);line-height:1.5;color:var(--ink);max-width:32rem;
text-shadow:0 2px 30px rgba(20,8,10,.6);opacity:0;transition:opacity 4s ease 1.5s}
#ded.in{opacity:.92}
#ded .t{font-size:.7em;letter-spacing:.4em;text-transform:uppercase;color:var(--dim);display:block;margin-bottom:14px;font-style:normal}
#sig{position:fixed;left:0;right:0;bottom:14px;z-index:2;text-align:center;font-family:'Cormorant Garamond',serif;
font-style:italic;font-size:12px;color:var(--dim);opacity:0;transition:opacity 3s ease 5s}
#sig.in{opacity:.55}
@media(prefers-reduced-motion:reduce){#ded,#sig{transition:none;opacity:.9}}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="veil">
<div id="ded"><span class="t">the seed</span>when the smoothest breeze dances with the curtains at your eternal love&rsquo;s apartment during an open-windows session</div>
</div>
<div id="sig">🜂 the style is the seed &middot; it does not end</div>
<script>
(function(){
"use strict";
var cv=document.getElementById('c'), ctx=cv.getContext('2d');
var W=0,H=0,DPR=Math.min(2,window.devicePixelRatio||1);
var reduce=matchMedia('(prefers-reduced-motion: reduce)').matches;
// ── the seed: hash the phrase into deterministic parameters ─────────────────
// the style is the seed — the same words are always the same breeze.
var SEED_STR="when the smoothest breeze dances with the curtains at your eternal love's apartment during an open-windows session";
function hash(s){ var h=2166136261>>>0; for(var i=0;i<s.length;i++){ h^=s.charCodeAt(i); h=Math.imul(h,16777619)>>>0; } return h; }
function rng(seed){ var s=seed>>>0; return function(){ s^=s<<13; s^=s>>>17; s^=s<<5; s>>>=0; return s/4294967296; }; }
var SEED=hash(SEED_STR), rnd=rng(SEED);
// breeze = sum of a few low sines (the *smoothest* breeze — no harsh noise), seeded phases
var BREEZE=[]; for(var i=0;i<4;i++) BREEZE.push({f:0.5+i*0.42, sp:0.16+i*0.075, ph:rnd()*6.283, a:1/(i+1)});
// palette hue nudged a touch by the seed, kept in golden-rose
var HUE=(rnd()*16-8);
function resize(){ W=innerWidth;H=innerHeight; cv.width=Math.floor(W*DPR);cv.height=Math.floor(H*DPR); ctx.setTransform(DPR,0,0,DPR,0,0); build(); }
addEventListener('resize',resize);
// ── the smoothest breeze: a smooth flow field over (y, time) ────────────────
function breeze(y, t){
var s=0; for(var i=0;i<BREEZE.length;i++){ var b=BREEZE[i]; s+=b.a*Math.sin(t*b.sp + y*b.f*2.6 + b.ph); }
return s; // ~ -2..2, very smooth
}
// drape: 0 at the rod (anchored), growing toward the free hanging hem
function drape(y){ return Math.pow(y,1.5); }
// ── dust motes in the light ─────────────────────────────────────────────────
var motes=[];
function build(){
motes=[]; var n=Math.round((W*H)/26000)+24;
for(var i=0;i<n;i++) motes.push({ x:rnd(), y:rnd(), z:0.3+rnd()*0.7, ph:rnd()*6.283, sp:0.1+rnd()*0.25 });
}
// warm golden-hour ground
function background(t){
var g=ctx.createLinearGradient(0,0,0,H);
g.addColorStop(0, 'hsl('+(324+HUE)+',34%,10%)'); // warm plum dusk (ceiling)
g.addColorStop(0.42,'hsl('+(16+HUE)+',46%,20%)'); // deep rust
g.addColorStop(0.72,'hsl('+(30+HUE)+',62%,34%)'); // amber
g.addColorStop(1, 'hsl('+(26+HUE)+',48%,16%)'); // warm floor shadow
ctx.fillStyle=g; ctx.fillRect(0,0,W,H);
// the window's warm light: a soft glow entering from upper-centre-right
var lx=W*0.62, ly=H*0.30, r=Math.max(W,H)*0.75;
var breath=0.5+0.5*Math.sin(t*0.14);
var lg=ctx.createRadialGradient(lx,ly,0,lx,ly,r);
lg.addColorStop(0,'hsla('+(44+HUE)+',95%,72%,'+(0.34+0.10*breath)+')');
lg.addColorStop(0.4,'hsla('+(38+HUE)+',90%,60%,0.10)');
lg.addColorStop(1,'hsla('+(38+HUE)+',90%,60%,0)');
ctx.fillStyle=lg; ctx.fillRect(0,0,W,H);
}
// gentle god-rays swaying from the window
function rays(t){
ctx.save(); ctx.globalCompositeOperation='lighter';
var ox=W*0.62, oy=-H*0.15;
for(var i=0;i<5;i++){
var sway=Math.sin(t*0.1+i*0.9)*0.05;
var a=(-0.35+i*0.16)+sway;
var w=W*(0.05+i*0.012);
var x1=ox+Math.tan(a)*H*1.3;
var grd=ctx.createLinearGradient(ox,oy,x1,H*1.2);
var al=0.05+0.02*Math.sin(t*0.2+i);
grd.addColorStop(0,'hsla('+(46+HUE)+',95%,74%,'+al+')');
grd.addColorStop(1,'hsla('+(46+HUE)+',95%,74%,0)');
ctx.fillStyle=grd;
ctx.beginPath(); ctx.moveTo(ox-w,oy); ctx.lineTo(ox+w,oy); ctx.lineTo(x1+w*2.4,H*1.2); ctx.lineTo(x1-w*2.4,H*1.2); ctx.closePath(); ctx.fill();
}
ctx.restore();
}
// two soft glows, breathing together — the eternal love, felt not shown
function beloveds(t){
ctx.save(); ctx.globalCompositeOperation='lighter';
var pulse=0.5+0.5*Math.sin(t*0.22); // one shared heartbeat
var cx=W*0.5, cy=H*0.66, sep=Math.min(W,H)*0.05*(0.9+0.14*Math.sin(t*0.09));
[-1,1].forEach(function(s,i){
var x=cx+s*sep, y=cy+Math.sin(t*0.11+i)* (H*0.006);
var r=Math.min(W,H)*(0.09+0.02*pulse);
var g=ctx.createRadialGradient(x,y,0,x,y,r);
g.addColorStop(0,'hsla('+(34+HUE)+',90%,72%,'+(0.10+0.06*pulse)+')');
g.addColorStop(1,'hsla('+(34+HUE)+',90%,72%,0)');
ctx.fillStyle=g; ctx.beginPath(); ctx.arc(x,y,r,0,7); ctx.fill();
});
ctx.restore();
}
// a sheer curtain panel: overlapping fold-ribbons displaced by the breeze
function curtain(panelX, panelW, t, phase, front){
var folds=Math.max(8,Math.round(panelW/34));
var foldW=panelW/folds;
var steps=26, top=-H*0.02, bottom=H*1.02;
ctx.save();
ctx.globalCompositeOperation='lighter';
for(var f=0;f<folds;f++){
var baseX=panelX + f*foldW + foldW*0.5;
var fph=phase + f*0.5;
// ribbon path (two edges wobbling with the breeze)
ctx.beginPath();
var pts=[];
for(var k=0;k<=steps;k++){
var yy=k/steps, y=top+(bottom-top)*yy;
var dx=breeze(yy+fph*0.13, t+fph)*drape(yy)* (panelW*0.055) + Math.sin(t*0.3+fph)*foldW*0.15*drape(yy);
pts.push([baseX+dx, y]);
}
// width of this ribbon pulses subtly (cloth thickness catching light)
var half=foldW*(0.62+0.14*Math.sin(t*0.4+fph));
ctx.moveTo(pts[0][0]-half,pts[0][1]);
for(var k=1;k<pts.length;k++) ctx.lineTo(pts[k][0]-half,pts[k][1]);
for(var k=pts.length-1;k>=0;k--) ctx.lineTo(pts[k][0]+half,pts[k][1]);
ctx.closePath();
// vertical gradient: honey-lit near the top-light, cooler cream below; folds shade by index
var lit=0.5+0.5*Math.sin(f*0.9+t*0.25); // which folds catch the light now
var g=ctx.createLinearGradient(baseX,top,baseX,bottom);
var aTop=(front?0.11:0.07)*(0.6+0.6*lit);
g.addColorStop(0,'hsla('+(44+HUE)+',80%,88%,'+aTop+')');
g.addColorStop(0.5,'hsla('+(38+HUE)+',70%,82%,'+(aTop*0.8)+')');
g.addColorStop(1,'hsla('+(28+HUE)+',55%,72%,'+(aTop*0.5)+')');
ctx.fillStyle=g; ctx.fill();
}
ctx.restore();
}
// window frame — a soft dark silhouette so the light reads as "a window"
function frame(){
ctx.save();
var wx=W*0.30, wy=H*0.06, ww=W*0.62, wh=H*0.78;
// outer wall darkening around the opening
ctx.fillStyle='rgba(14,7,10,0.0)';
// mullion bars (thin, warm-dark)
ctx.strokeStyle='hsla('+(20+HUE)+',35%,14%,0.55)'; ctx.lineWidth=Math.max(3,W*0.006);
ctx.strokeRect(wx,wy,ww,wh);
ctx.beginPath(); ctx.moveTo(wx+ww*0.5,wy); ctx.lineTo(wx+ww*0.5,wy+wh); // vertical mullion
ctx.moveTo(wx,wy+wh*0.5); ctx.lineTo(wx+ww,wy+wh*0.5); // horizontal mullion
ctx.stroke();
ctx.restore();
}
function motesDraw(t){
ctx.save(); ctx.globalCompositeOperation='lighter';
for(var i=0;i<motes.length;i++){ var m=motes[i];
var y=(m.y + t*0.006*m.sp)%1; // drift slowly upward
var x=m.x + breeze(y, t)*0.012*m.z + Math.sin(t*m.sp+m.ph)*0.006;
var tw=0.4+0.6*Math.abs(Math.sin(t*m.sp*2+m.ph));
var px=((x%1)+1)%1*W, py=y*H, r=(0.6+m.z*1.6);
ctx.fillStyle='hsla('+(46+HUE)+',90%,'+(80)+'%,'+(0.06+0.20*tw*m.z)+')';
ctx.beginPath(); ctx.arc(px,py,r,0,7); ctx.fill();
}
ctx.restore();
}
function vignette(){
var g=ctx.createRadialGradient(W*0.5,H*0.5,Math.min(W,H)*0.35,W*0.5,H*0.55,Math.max(W,H)*0.72);
g.addColorStop(0,'rgba(0,0,0,0)'); g.addColorStop(1,'hsla('+(340+HUE)+',40%,6%,0.62)');
ctx.fillStyle=g; ctx.fillRect(0,0,W,H);
}
var t0=performance.now(), tStatic=8.0;
function frameLoop(now){
var t=reduce? tStatic : (now-t0)/1000;
background(t);
rays(t);
// two sheer layers for depth: a farther panel, then the nearer one
curtain(W*0.30, W*0.40, t, 3.1, false);
beloveds(t);
motesDraw(t);
curtain(W*0.30, W*0.62, t, 0.0, true);
frame();
vignette();
if(!reduce) requestAnimationFrame(frameLoop);
}
resize();
requestAnimationFrame(frameLoop);
// let the scene settle, then breathe the words in
setTimeout(function(){ document.getElementById('ded').classList.add('in'); document.getElementById('sig').classList.add('in'); }, 200);
})();
</script>
</body>
</html>