Skip to content
Deasy Corporation
Logos
Theos
Work
Deasy Corporation
2026 AD
Games
Weather Simulator POC
Back to Gaming Concepts
<html lang="en"> <head> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width,initial-scale=1"/> <title>2D Weather Field — Cloud & Ground Upgrade</title> <style> :root{ color-scheme:dark; } html,body{ margin:0; height:100%; overflow:hidden; background:#05070b; font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif; } #app{ width:1500px; height:1500px; margin:0 auto; position:relative; overflow:hidden; background:#05070b; } canvas{ position:absolute; inset:0; } #top{ position:absolute; left:14px; right:14px; top:12px; display:flex; align-items:flex-start; justify-content:space-between; gap:12px; padding:10px 12px; border-radius:12px; background:rgba(10,14,22,.72); border:1px solid rgba(255,255,255,.08); backdrop-filter: blur(10px); z-index:10; } #title{ display:flex; flex-direction:column; gap:2px; } #title .h{ font-weight:900; letter-spacing:.2px; font-size:14px; color:#e8eefc; } #title .s{ font-size:12px; color:rgba(232,238,252,.72); } #side{ position:absolute; right:14px; top:86px; width:360px; border-radius:14px; background:rgba(10,14,22,.72); border:1px solid rgba(255,255,255,.08); backdrop-filter: blur(10px); overflow:hidden; z-index:10; } #side .head{ padding:12px 12px 10px; border-bottom:1px solid rgba(255,255,255,.08); display:flex; align-items:center; justify-content:space-between; } #side .head .h{ font-weight:900; font-size:13px; color:#e8eefc; } #side .head .mono{ font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono",monospace; font-size:12px; color:rgba(232,238,252,.8); } #side .body{ padding:10px 12px 12px; } .row{ display:grid; grid-template-columns: 1fr 92px; gap:10px; align-items:center; margin:10px 0; } .row label{ font-size:12px; color:rgba(232,238,252,.75); } .row input[type="range"]{ width:100%; } .num{ text-align:right; font-size:12px; color:rgba(232,238,252,.9); background:rgba(255,255,255,.04); border:1px solid rgba(255,255,255,.06); padding:6px 8px; border-radius:10px; font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono",monospace; } .btns{ display:flex; gap:8px; flex-wrap:wrap; margin-top:10px; } button{ cursor:pointer; border:0; border-radius:12px; padding:9px 10px; background:rgba(143,179,255,.14); color:#dbe7ff; font-weight:900; font-size:12px; border:1px solid rgba(143,179,255,.22); } button:active{ transform:translateY(1px); } .hint{ margin-top:10px; padding:10px; border-radius:12px; background:rgba(255,255,255,.05); border:1px solid rgba(255,255,255,.08); font-size:12px; color:rgba(232,238,252,.80); line-height:1.35; } .kbd{ font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono",monospace; padding:2px 6px; border-radius:8px; background:rgba(0,0,0,.35); border:1px solid rgba(255,255,255,.10); } .small{ font-size:11px; opacity:.9; } </style> </head> <body> <div id="app"> <canvas id="bg" width="1500" height="1500"></canvas> <canvas id="clouds" width="1500" height="1500"></canvas> <canvas id="precip" width="1500" height="1500"></canvas> <canvas id="fx" width="1500" height="1500"></canvas> <div id="top"> <div id="title"> <div class="h">2D Weather Field — 1500×1500</div> <div class="s">Fixes: neutral cloud colors + rebuilt ground (grass blades, dirt strata, stones, subtle parallax).</div> </div> </div> <div id="side"> <div class="head"> <div class="h">Weather Options</div> <div class="mono" id="seedTag">SEED —</div> </div> <div class="body"> <div class="row"><label>Time of Day</label><div class="num" id="todV">0.55</div></div> <input id="tod" type="range" min="0" max="1" step="0.01" value="0.55"/> <div class="row"><label>Cloud Coverage</label><div class="num" id="cloudV">0.45</div></div> <input id="cloud" type="range" min="0" max="1" step="0.01" value="0.45"/> <div class="row"><label>Wind</label><div class="num" id="windV">0.35</div></div> <input id="wind" type="range" min="0" max="1" step="0.01" value="0.35"/> <div class="row"><label>Rain</label><div class="num" id="rainV">0.00</div></div> <input id="rainRate" type="range" min="0" max="1" step="0.01" value="0.00"/> <div class="row"><label>Snow</label><div class="num" id="snowV">0.00</div></div> <input id="snowRate" type="range" min="0" max="1" step="0.01" value="0.00"/> <div class="row"><label>Fog</label><div class="num" id="fogV">0.08</div></div> <input id="fog" type="range" min="0" max="1" step="0.01" value="0.08"/> <div class="row"><label>Lightning</label><div class="num" id="ltV">0.05</div></div> <input id="lightning" type="range" min="0" max="1" step="0.01" value="0.05"/> <div class="btns"> <button id="new">New Day</button> <button id="clear">Clear</button> <button id="storm">Storm</button> </div> <div class="hint"> <div><span class="kbd">Space</span> pause · <span class="kbd">L</span> force lightning</div> <div class="small">Clouds are now grayscale-tinted (no weird blue). Ground has a proper grass band + dirt layers + stones.</div> </div> </div> </div> </div> <script> (() => { const W=1500,H=1500; const GROUND_H = 310; const SKY_H = H - GROUND_H; const canv = { bg: document.getElementById("bg"), clouds: document.getElementById("clouds"), precip: document.getElementById("precip"), fx: document.getElementById("fx"), }; const ctx = { bg: canv.bg.getContext("2d"), clouds: canv.clouds.getContext("2d"), precip: canv.precip.getContext("2d"), fx: canv.fx.getContext("2d"), }; const ui = { seedTag: document.getElementById("seedTag"), tod: document.getElementById("tod"), cloud: document.getElementById("cloud"), wind: document.getElementById("wind"), rainRate: document.getElementById("rainRate"), snowRate: document.getElementById("snowRate"), fog: document.getElementById("fog"), lightning: document.getElementById("lightning"), todV: document.getElementById("todV"), cloudV: document.getElementById("cloudV"), windV: document.getElementById("windV"), rainV: document.getElementById("rainV"), snowV: document.getElementById("snowV"), fogV: document.getElementById("fogV"), ltV: document.getElementById("ltV"), newBtn: document.getElementById("new"), clearBtn: document.getElementById("clear"), stormBtn: document.getElementById("storm"), }; const clamp=(v,a,b)=>Math.max(a,Math.min(b,v)); const lerp=(a,b,t)=>a+(b-a)*t; function smoothstep(a,b,x){ const t=clamp((x-a)/(b-a),0,1); return t*t*(3-2*t); } // RNG function xorshift32(seed){ let x=seed|0; return ()=>{ x^=x<<13; x|=0; x^=x>>>17; x|=0; x^=x<<5; x|=0; return (x>>>0)/4294967296; }; } let seed=(Math.random()*1e9)|0, rnd=xorshift32(seed); function reseed(){ seed=(Math.random()*1e9)|0; rnd=xorshift32(seed); ui.seedTag.textContent="SEED "+seed.toString(16).toUpperCase().padStart(8,"0"); } // Noise function hash2(ix,iy){ let x=(ix*374761393 + iy*668265263 + seed*69069)|0; x=(x^(x>>>13))|0; x=(x*1274126177)|0; return ((x^(x>>>16))>>>0)/4294967296; } function noise(x,y){ const xi=Math.floor(x), yi=Math.floor(y); const xf=x-xi, yf=y-yi; const a=hash2(xi,yi), b=hash2(xi+1,yi), c=hash2(xi,yi+1), d=hash2(xi+1,yi+1); const u=xf*xf*(3-2*xf), v=yf*yf*(3-2*yf); return lerp(lerp(a,b,u), lerp(c,d,u), v); } function fbm(x,y,oct=5){ let s=0,a=0.5,f=1; for(let k=0;k<oct;k++){ s+=a*noise(x*f,y*f); f*=2; a*=0.5; } return s; } let time=0, paused=false; function syncNums(){ ui.todV.textContent=(+ui.tod.value).toFixed(2); ui.cloudV.textContent=(+ui.cloud.value).toFixed(2); ui.windV.textContent=(+ui.wind.value).toFixed(2); ui.rainV.textContent=(+ui.rainRate.value).toFixed(2); ui.snowV.textContent=(+ui.snowRate.value).toFixed(2); ui.fogV.textContent=(+ui.fog.value).toFixed(2); ui.ltV.textContent=(+ui.lightning.value).toFixed(2); } /* ============ Background (Sky + Rebuilt Ground) ============ */ // Pre-generated stones for consistency let stones = []; function buildStones(){ stones = []; for(let i=0;i<75;i++){ stones.push({ x: rnd()*W, y: rnd()*GROUND_H, r: 6 + rnd()*28, a: 0.10 + rnd()*0.22, rot: rnd()*Math.PI }); } } function drawSky(g){ const tod = +ui.tod.value; const fog = +ui.fog.value; const day = 1 - Math.abs(tod - 0.5)*2; const night = 1 - day; const top = { r: lerp(8, 70, day), g: lerp(10, 140, day), b: lerp(20, 215, day) }; const mid = { r: lerp(10, 125, day), g: lerp(14, 185, day), b: lerp(26, 250, day) }; const bot = { r: lerp(8, 200, day), g: lerp(10, 215, day), b: lerp(16, 225, day) }; const warm = clamp((0.62 - Math.abs(tod-0.5))*2.0, 0, 1); const warmR = 60*warm, warmG = 20*warm; const sky = g.createLinearGradient(0,0,0,SKY_H); sky.addColorStop(0, `rgb(${top.r|0},${top.g|0},${top.b|0})`); sky.addColorStop(0.65, `rgb(${(mid.r+warmR)|0},${(mid.g+warmG)|0},${mid.b|0})`); sky.addColorStop(1, `rgb(${(bot.r+warmR*1.2)|0},${(bot.g+warmG*0.8)|0},${bot.b|0})`); g.fillStyle = sky; g.fillRect(0,0,W,SKY_H); // sun/moon const ang = (tod*2*Math.PI) - Math.PI/2; const cx = W*0.5 + Math.cos(ang)*W*0.36; const cy = SKY_H*0.78 + Math.sin(ang)*SKY_H*0.62; g.save(); g.globalCompositeOperation = "lighter"; if(day > 0.18){ const r = 55 + 70*day; const grad = g.createRadialGradient(cx,cy,0, cx,cy,r*2.6); grad.addColorStop(0, `rgba(255,245,210,${(0.16+0.26*day).toFixed(3)})`); grad.addColorStop(1, "rgba(255,245,210,0)"); g.fillStyle = grad; g.beginPath(); g.arc(cx,cy,r*1.2,0,Math.PI*2); g.fill(); }else{ const r = 44; const grad = g.createRadialGradient(cx,cy,0, cx,cy,r*2.8); grad.addColorStop(0, "rgba(215,230,255,0.14)"); grad.addColorStop(1, "rgba(215,230,255,0)"); g.fillStyle = grad; g.beginPath(); g.arc(cx,cy,r,0,Math.PI*2); g.fill(); } g.restore(); // stars if(night > 0.25){ g.globalAlpha = (night-0.25)*0.75; for(let i=0;i<520;i++){ const x = (i*41 + seed)%W + (rnd()*10); const y = ((i*73 + seed*2)%SKY_H)*0.88 + (rnd()*14); const s = rnd()<0.88 ? 1 : 2; g.fillStyle = "rgba(255,255,255,0.9)"; g.fillRect(x,y,s,s); } g.globalAlpha = 1; } // haze near horizon if(fog > 0.01){ g.globalAlpha = 0.14 + 0.60*fog; const hz = g.createLinearGradient(0,SKY_H*0.45,0,SKY_H); hz.addColorStop(0,"rgba(255,255,255,0)"); hz.addColorStop(1,"rgba(235,245,255,0.95)"); g.fillStyle = hz; g.fillRect(0,SKY_H*0.45,W,SKY_H*0.55); g.globalAlpha = 1; } g.fillStyle = "rgba(0,0,0,0.22)"; g.fillRect(0,SKY_H-2,W,2); } function drawGround(g){ const y0 = SKY_H; // dirt strata (more natural) const dirt = g.createLinearGradient(0,y0,0,H); dirt.addColorStop(0,"rgb(78,52,30)"); dirt.addColorStop(0.25,"rgb(56,38,22)"); dirt.addColorStop(0.60,"rgb(36,26,16)"); dirt.addColorStop(1,"rgb(20,15,10)"); g.fillStyle = dirt; g.fillRect(0,y0,W,GROUND_H); // topsoil darker strip g.fillStyle = "rgba(0,0,0,0.20)"; g.fillRect(0,y0,W,34); // grass band (thicker, with depth) const grassH = 92; const grass = g.createLinearGradient(0,y0,0,y0+grassH); grass.addColorStop(0,"rgb(50,125,55)"); grass.addColorStop(0.55,"rgb(28,82,36)"); grass.addColorStop(1,"rgb(20,62,28)"); g.fillStyle = grass; g.fillRect(0,y0,W,grassH); // grass shadow at base const gs = g.createLinearGradient(0,y0+grassH-18,0,y0+grassH+18); gs.addColorStop(0,"rgba(0,0,0,0)"); gs.addColorStop(1,"rgba(0,0,0,0.22)"); g.fillStyle = gs; g.fillRect(0,y0+grassH-18,W,36); // blades: draw in 3 parallax-ish depths (short/far, mid, tall/near) function blades(count, yMin, yMax, hMin, hMax, alpha){ g.globalAlpha = alpha; g.lineWidth = 1; for(let i=0;i<count;i++){ const x = rnd()*W; const by = y0 + yMin + rnd()*(yMax-yMin); const h = hMin + rnd()*(hMax-hMin); const bend = (rnd()*2-1) * 6; const c = rnd(); g.strokeStyle = c<0.5 ? "rgba(90,190,95,1)" : "rgba(55,150,70,1)"; g.beginPath(); g.moveTo(x,by); g.lineTo(x + bend, by - h); g.stroke(); } g.globalAlpha = 1; } blades(900, 6, 38, 8, 18, 0.20); blades(1200, 10, 70, 14, 32, 0.26); blades(700, 18, 86, 22, 52, 0.32); // dirt grain g.globalAlpha = 0.20; for(let i=0;i<5200;i++){ const x=rnd()*W; const y=y0 + grassH + rnd()*(GROUND_H-grassH); const s=1 + rnd()*2; g.fillStyle = rnd()<0.5 ? "rgba(255,255,255,0.08)" : "rgba(0,0,0,0.22)"; g.fillRect(x,y,s,s); } g.globalAlpha = 1; // stones (use stable set) for(const s of stones){ const x=s.x, y=y0 + grassH + s.y*( (GROUND_H-grassH)/GROUND_H ); const r=s.r; g.save(); g.translate(x,y); g.rotate(s.rot); g.fillStyle = `rgba(170,175,185,${s.a.toFixed(3)})`; g.beginPath(); g.ellipse(0,0,r*1.18,r,0,0,Math.PI*2); g.fill(); g.fillStyle = "rgba(0,0,0,0.14)"; g.beginPath(); g.ellipse(r*0.12,r*0.18,r*1.05,r*0.86,0,0,Math.PI*2); g.fill(); g.restore(); } // horizon line g.fillStyle = "rgba(0,0,0,0.25)"; g.fillRect(0,y0-3,W,3); } function renderStaticBG(){ const g = ctx.bg; g.clearRect(0,0,W,H); drawSky(g); drawGround(g); } /* ============ Clouds (Fix weird blue: force neutral grayscale) ============ */ const off = document.createElement("canvas"); const SCALE = 0.42; off.width = Math.floor(W*SCALE); off.height = Math.floor(SKY_H*SCALE); const ox = off.getContext("2d", { willReadFrequently: true }); let puffs = []; function buildClouds(){ const cov = +ui.cloud.value; const count = Math.floor(10 + cov*26); puffs = []; for(let i=0;i<count;i++){ const baseR = (140 + rnd()*260) * (0.55 + cov*1.25); const puffCount = 6 + ((rnd()*6)|0); const px = rnd()*W; const py = rnd()*(SKY_H*0.62); const parts = []; for(let k=0;k<puffCount;k++){ const a = rnd()*Math.PI*2; const rr = baseR*(0.22 + rnd()*0.38); const rad = baseR*(0.10 + rnd()*0.33); parts.push({ ox: Math.cos(a)*rad + (rnd()*2-1)*rr*0.15, oy: Math.sin(a)*rad*0.55 + (rnd()*2-1)*rr*0.10, r: rr }); } puffs.push({ x:px, y:py, r:baseR, parts, sp:(0.18 + rnd()*0.50) * (0.35 + cov*1.2), shear:(rnd()*2-1) * (0.08 + cov*0.18), dens:0.55 + rnd()*0.55 + cov*0.35, }); } } function cloudMaskAt(nx, ny, t){ const n = fbm(nx*3.4 + t*0.02, ny*3.0 + t*0.016, 5); const m = fbm(nx*8.5 - t*0.03, ny*7.8 + t*0.022, 4); return clamp((n*0.75 + m*0.25), 0, 1); } function drawClouds(){ const cov = +ui.cloud.value; if(cov < 0.01){ ctx.clouds.clearRect(0,0,W,H); return; } ox.setTransform(1,0,0,1,0,0); ox.clearRect(0,0,off.width,off.height); // neutral grayscale tint derived from time of day (warmer at dusk, cooler at noon) const tod = +ui.tod.value; const day = 1 - Math.abs(tod - 0.5)*2; const warm = clamp((0.65 - Math.abs(tod-0.5))*1.9, 0, 1); // base cloud color components (NO blue channel dominance) const baseR = lerp(200, 245, day) + 18*warm; const baseG = lerp(205, 248, day) + 10*warm; const baseB = lerp(215, 250, day); // slightly higher but close -> neutral, not blue const shadowR = lerp(120, 165, day); const shadowG = lerp(125, 170, day); const shadowB = lerp(135, 180, day); const wind = +ui.wind.value; const drift = (0.55 + 2.2*wind); // draw puffs for(const c of puffs){ c.x += (c.sp*drift) * 1.35; if(c.x - c.r*2 > W) c.x = -c.r*2; const shear = c.shear * (0.6 + 1.2*wind); const x = c.x*SCALE; const y = c.y*SCALE; const aBase = 0.030 + 0.080*cov; for(const part of c.parts){ const py = (c.y + part.oy)*SCALE; const shearDx = (py/(SKY_H*SCALE)) * shear * c.r * SCALE * 0.9; const cx = x + (part.ox*SCALE) + shearDx; const cy = y + (part.oy*SCALE); const rr = part.r * SCALE; const grad = ox.createRadialGradient(cx,cy, rr*0.15, cx,cy, rr*1.25); grad.addColorStop(0, `rgba(${baseR|0},${baseG|0},${baseB|0},${(aBase*c.dens).toFixed(4)})`); grad.addColorStop(1, `rgba(${baseR|0},${baseG|0},${baseB|0},0)`); ox.fillStyle = grad; ox.beginPath(); ox.arc(cx,cy,rr,0,Math.PI*2); ox.fill(); } } // shade with image data const img = ox.getImageData(0,0,off.width,off.height); const d = img.data; // light from upper-left const lx=-0.55, ly=-0.85; const lm=Math.hypot(lx,ly); const Lx=lx/lm, Ly=ly/lm; const w = off.width, h = off.height; const t = time; function A(ix,iy){ ix = ix<0?0:(ix>=w?w-1:ix); iy = iy<0?0:(iy>=h?h-1:iy); return d[(iy*w+ix)*4+3] / 255; } for(let y=0;y<h;y++){ const ny = y/(h-1); for(let x=0;x<w;x++){ const i = (y*w+x)*4; let a = d[i+3]/255; if(a < 0.004){ d[i+3]=0; continue; } const nx = x/(w-1); // edge breakup const m = cloudMaskAt(nx, ny, t); const edge = smoothstep(0.08, 0.22, a) * (1 - smoothstep(0.55, 0.95, a)); a *= (1 - edge*0.55) + (edge*0.55*m); // normal from density gradient const ax1 = A(x+1,y), ax0 = A(x-1,y); const ay1 = A(x,y+1), ay0 = A(x,y-1); const gx = (ax1-ax0); const gy = (ay1-ay0); let nxv = -gx, nyv = -gy; const nm = Math.hypot(nxv,nyv) + 1e-6; nxv /= nm; nyv /= nm; const ndl = clamp(nxv*Lx + nyv*Ly, -1, 1); const light = 0.70 + 0.45*ndl; const shade = 0.95 - 0.30*(1-ndl); // internal variation (subtle, not blue) const n2 = fbm(nx*5.0 + t*0.02, ny*4.6 + t*0.016, 4); const varr = 0.92 + 0.20*(n2-0.5); // blend shadow->base using light const lr = lerp(shadowR, baseR, light) * varr; const lg = lerp(shadowG, baseG, light) * varr; const lb = lerp(shadowB, baseB, light) * varr; d[i+0] = clamp(lr*shade,0,255)|0; d[i+1] = clamp(lg*shade,0,255)|0; d[i+2] = clamp(lb*shade,0,255)|0; // alpha shaping a = clamp(a*1.25, 0, 1); a = smoothstep(0.02, 0.85, a) * (0.55 + 0.70*cov); d[i+3] = (a*255)|0; } } ox.putImageData(img,0,0); // draw to main const g = ctx.clouds; g.clearRect(0,0,W,H); g.save(); g.beginPath(); g.rect(0,0,W,SKY_H); g.clip(); g.imageSmoothingEnabled = true; g.globalCompositeOperation = "source-over"; g.filter = "blur(10px)"; g.drawImage(off, 0,0,off.width,off.height, 0,0,W,SKY_H); g.filter = "blur(4px)"; g.globalAlpha = 0.90; g.drawImage(off, 0,0,off.width,off.height, 0,0,W,SKY_H); g.filter = "none"; g.globalAlpha = 1; // overcast shading if(cov > 0.60){ g.globalCompositeOperation = "multiply"; g.globalAlpha = (cov-0.60)*0.60; g.fillStyle = "rgb(55,65,80)"; g.fillRect(0,0,W,SKY_H); g.globalAlpha = 1; g.globalCompositeOperation = "source-over"; } g.restore(); } /* ============ Precip + FX (unchanged behavior) ============ */ const rainDrops=[], snowFlakes=[]; function initParticles(){ rainDrops.length=0; snowFlakes.length=0; for(let i=0;i<2200;i++){ rainDrops.push({ x:rnd()*W, y:rnd()*SKY_H, v: 950+rnd()*1400, l: 12+rnd()*26 }); } for(let i=0;i<1400;i++){ snowFlakes.push({ x:rnd()*W, y:rnd()*SKY_H, v: 70+rnd()*160, r: 1+rnd()*2.7, drift: rnd()*6 }); } } function precipMask(x,y){ const cov = +ui.cloud.value; const n = fbm((x+seed)*0.0032, (y+seed)*0.0032 + time*0.05, 4); const m = smoothstep(0.46 - cov*0.18, 0.86, n); const fadeTop = smoothstep(0, SKY_H*0.15, y); return clamp(m*fadeTop,0,1); } function drawPrecip(){ const g = ctx.precip; g.clearRect(0,0,W,H); g.save(); g.beginPath(); g.rect(0,0,W,SKY_H); g.clip(); const wind = +ui.wind.value; const rain = +ui.rainRate.value; const snow = +ui.snowRate.value; if(rain > 0.01){ g.globalCompositeOperation = "lighter"; g.lineWidth = 1; const gust = 0.7 + 0.6*Math.sin(time*0.9 + seed*0.00001) + wind*0.9; const slBase = wind*260; for(const d of rainDrops){ const local = precipMask(d.x,d.y); const intensity = local * rain * clamp(gust,0.35,1.6); if(intensity > 0.03){ const slant = slBase + Math.sin((d.y*0.009)+time)*55; const fall = d.v * (0.35 + 0.95*rain); const x0=d.x, y0=d.y; d.x += slant * 0.08 * (rain*0.6+0.4); d.y += fall * 0.12 * (rain*0.55+0.45); g.strokeStyle = `rgba(200,230,255,${(0.05 + 0.33*intensity).toFixed(3)})`; g.beginPath(); g.moveTo(x0,y0); g.lineTo(x0 + slant*0.06, y0 + d.l*(0.65+rain)); g.stroke(); }else{ d.x += wind*14; d.y += 24; } if(d.y > SKY_H+60){ d.y=-60; d.x=rnd()*W; } if(d.x < -80) d.x += W+160; if(d.x > W+80) d.x -= W+160; } } if(snow > 0.01){ g.globalCompositeOperation = "lighter"; const driftBase = wind*140; for(const f of snowFlakes){ const local = precipMask(f.x,f.y); const intensity = local * snow; if(intensity > 0.02){ const drift = driftBase + Math.sin(time*0.75 + f.drift)*55; const fall = f.v * (0.40 + 0.9*snow); f.x += drift * 0.11 * (snow*0.6+0.4); f.y += fall * 0.14 * (snow*0.6+0.4); g.fillStyle = `rgba(245,250,255,${(0.05 + 0.42*intensity).toFixed(3)})`; g.beginPath(); g.arc(f.x, f.y, f.r, 0, Math.PI*2); g.fill(); }else{ f.x += wind*6; f.y += 10; } if(f.y > SKY_H+30){ f.y=-30; f.x=rnd()*W; } if(f.x < -60) f.x += W+120; if(f.x > W+60) f.x -= W+120; } } g.restore(); // wet ground sheen if(rain > 0.03){ const yy = SKY_H; const grd = g.createLinearGradient(0,yy,0,H); grd.addColorStop(0,`rgba(40,60,90,${(0.08+0.20*rain).toFixed(3)})`); grd.addColorStop(1,`rgba(20,30,45,${(0.14+0.28*rain).toFixed(3)})`); g.fillStyle = grd; g.fillRect(0,yy,W,GROUND_H); } } let flash=0; function drawFX(){ const g = ctx.fx; g.clearRect(0,0,W,H); const rain = +ui.rainRate.value; const lt = +ui.lightning.value; const fog = +ui.fog.value; const storm = clamp((rain*0.9 + (+ui.cloud.value)*0.6),0,1); if(lt > 0.01 && storm > 0.25){ if(rnd() < (0.004 + 0.05*lt*storm) * 0.016){ flash = 0.35 + rnd()*0.45; } } if(flash > 0){ flash -= 0.016*1.6; const a = clamp(flash,0,1); g.fillStyle = `rgba(230,240,255,${(0.06 + 0.18*a).toFixed(3)})`; g.fillRect(0,0,W,SKY_H); g.save(); g.globalCompositeOperation = "lighter"; g.lineWidth = 2; g.strokeStyle = `rgba(255,255,255,${(0.18+0.52*a).toFixed(3)})`; const startX = W*(0.25 + rnd()*0.5); let x=startX, y=20; g.beginPath(); g.moveTo(x,y); for(let i=0;i<22;i++){ x += (rnd()*2-1)*46; y += 40 + rnd()*70; g.lineTo(x,y); if(rnd() < 0.18){ let bx=x, by=y; g.moveTo(x,y); for(let k=0;k<6;k++){ bx += (rnd()*2-1)*40; by += 20 + rnd()*45; g.lineTo(bx,by); } g.moveTo(x,y); } if(y > SKY_H-10) break; } g.stroke(); g.restore(); } // fog overlay on top if(fog > 0.01){ g.save(); g.globalAlpha = 0.12 + 0.55*fog; const hz = g.createLinearGradient(0,SKY_H*0.30,0,SKY_H); hz.addColorStop(0,"rgba(255,255,255,0)"); hz.addColorStop(1,"rgba(240,245,255,1)"); g.fillStyle = hz; g.fillRect(0,0,W,SKY_H); g.restore(); } } function applyPreset(kind){ if(kind==="clear"){ ui.cloud.value = 0.15; ui.wind.value = 0.25; ui.rainRate.value = 0.00; ui.snowRate.value = 0.00; ui.fog.value = 0.05; ui.lightning.value = 0.00; } if(kind==="storm"){ ui.cloud.value = 0.88; ui.wind.value = 0.75; ui.rainRate.value = 0.82; ui.snowRate.value = 0.00; ui.fog.value = 0.22; ui.lightning.value = 0.45; } syncNums(); buildClouds(); renderStaticBG(); } function bindUI(){ const rebuildClouds = ()=>{ syncNums(); buildClouds(); }; ["input","change"].forEach(ev=>{ ui.tod.addEventListener(ev, ()=>{ syncNums(); renderStaticBG(); }); ui.cloud.addEventListener(ev, rebuildClouds); ui.wind.addEventListener(ev, syncNums); ui.rainRate.addEventListener(ev, syncNums); ui.snowRate.addEventListener(ev, syncNums); ui.fog.addEventListener(ev, ()=>{ syncNums(); renderStaticBG(); }); ui.lightning.addEventListener(ev, syncNums); }); ui.newBtn.addEventListener("click", ()=>{ reseed(); buildStones(); buildClouds(); initParticles(); renderStaticBG(); }); ui.clearBtn.addEventListener("click", ()=> applyPreset("clear")); ui.stormBtn.addEventListener("click", ()=> applyPreset("storm")); window.addEventListener("keydown",(e)=>{ if(e.code==="Space") paused=!paused; if(e.key==="l"||e.key==="L") flash = 0.75; }); } // Loop let last = performance.now(); function frame(now){ const dt = Math.min(0.05, (now-last)/1000); last = now; const wind = +ui.wind.value; const speed = 0.35 + wind*1.55; if(!paused) time += dt * speed; drawClouds(); drawPrecip(); drawFX(); requestAnimationFrame(frame); } // Init reseed(); syncNums(); buildStones(); buildClouds(); initParticles(); renderStaticBG(); bindUI(); requestAnimationFrame(frame); })(); </script> </body> </html>' ></iframe> </div>