if (!shooterOccluded(px + z / 2, py + z / 2, z)) { ctx.fillStyle = css(hx(k > n / 2 ? C.brassHi : C.washi), (.3 + .6 * f) * A); ctx.fillRect(px, py, z, z); } const d2 = d - tl / n / 2, qx = Math.round(hxp - ca * d2), qy = Math.round(hyp - sa * d2); if (!shooterOccluded(qx + .5, qy + .5, 1)) { ctx.fillStyle = css(hx(C.brassHi), (.12 + .3 * f) * A); ctx.fillRect(qx, qy, 1, 1); } } const hx0 = Math.round(hxp / 2) * 2, hy0 = Math.round(hyp / 2) * 2; if (shooterOccluded(hx0 + 1, hy0 + 1, 5)) continue; ctx.fillStyle = css(hx(C.washi), .18 * A); ctx.fillRect(hx0 - 2, hy0 - 2, 6, 6); ctx.fillStyle = css(hx(head), .95 * A); ctx.fillRect(hx0 - 1, hy0 - 1, 3, 3); } } function drawBackground(now) { const nowS = now / 1000, S = WX.storm, U = WX.sunset; skyNowS = nowS; placeSun(nowS); const D = dayLift(), sK = clamp01(S / .8), si = SEASONS.indexOf(SEA); const inner = lerpC(lerpC(lerpC(lerpC(hx(SKY.nightIn), hx(SKY.sunIn), D), hx(SKY_SEA[si < 0 ? 0 : si]), .14), hx("#2a2838"), .3 * U), hx(SKY.stormIn), sK); const outer = lerpC(lerpC(hx(SKY.nightOut), hx(SKY.sunOut), D), hx(SKY.stormOut), sK); const outerB = lerpC(outer, hx(C.inkDeep), .22 * BOOST * (1 - .5 * D)); const g = ctx.createRadialGradient(CX, CY, 30, CX, CY, W * .78); g.addColorStop(0, css(inner)); g.addColorStop(.55, css(lerpC(inner, outerB, .6 + .1 * BOOST))); g.addColorStop(1, css(outerB)); ctx.fillStyle = g; ctx.fillRect(0, 0, W, H); BLOOMS.forEach(([x, y, r, c], i) => wash(x + Math.sin(nowS * .05 + i * 1.7) * 18, y + Math.cos(nowS * .04 + i) * 12, r, c, .2 * (1 - .6 * S))); wash(CX, CY + 30, 230, SEA.wash, SEA.washA * (1 + .3 * BOOST) * (1 - .6 * S)); if (SEA.frost) wash(CX, CY, 260, FROST, .05 * (1 - .5 * S)); if (U > .01) { wash(CX, CY, 250, C.amber, .2 * U); wash(SUN0.x, SUN0.y, 190, C.rose, .14 * U); } if (S > .01) wash(CX, CY - 150, 440, C.inkDeep, .5 * S); drawBackGrain(nowS, S); const [sdx, sdy] = starDrift(nowS), ddx = nowS * 1.1, ddy = Math.sin(nowS * .017 + 1) * 9; for (const s of stars) { ctx.globalAlpha = Math.max(0, (.16 + .12 * Math.sin(now * .0008 + s.p)) * (1 - S) * (1 - .5 * U) * SEA.stars * (1 - .7 * WX.cloud)); ctx.fillStyle = C.washi; ctx.fillRect(wrapX(s.x + sdx), s.y + sdy, s.r, s.r); } for (const d of DUST) { const x = wrapX(d.x + ddx), y = d.y + ddy, off = sstep(PLATE_R + 18, PLATE_R + 40, Math.hypot(x - CX, y - CY)); ctx.globalAlpha = Math.max(0, (.13 + .08 * Math.sin(now * .0006 + d.p)) * (1 - .55 * S) * (1 - .5 * U) * (1 - .5 * WX.cloud) * off); ctx.fillStyle = d.c; ctx.fillRect(x, y, d.z, d.z); } ctx.globalAlpha = 1; drawTwinkles(nowS); drawShooters(nowS); drawClouds(nowS); const ROT = nowS * Math.PI * 2 / 180, ROT2 = -nowS * Math.PI * 2 / 240, GE = SONG && AN ? at(AN.Es, songT()) : .2; const gA = .16 + .05 * BOOST + .24 * S + .08 * U + .06 * dayLift(); ctx.strokeStyle = css(lerpC(hx(C.brass), hx(C.brassHi), S), gA + .05 * GE); ctx.lineWidth = 1; [INNER_R, OUTER_R, OUTER_R + 30].forEach(r => { ctx.beginPath(); ctx.arc(CX, CY, r, 0, Math.PI * 2); ctx.stroke(); }); for (let i = 0; i < 60; i++) { const a = ROT + i * Math.PI / 30, r1 = OUTER_R + 26, r2 = r1 + (i % 5 ? 4 : 9); ctx.beginPath(); ctx.moveTo(CX + Math.cos(a) * r1, CY + Math.sin(a) * r1); ctx.lineTo(CX + Math.cos(a) * r2, CY + Math.sin(a) * r2); ctx.stroke(); } ctx.save(); ctx.lineWidth = 1.6; ctx.strokeStyle = css(hx(C.brassHi), (gA + .1) * (.8 + .25 * GE)); for (let k = 0; k < 3; k++) { const a0 = ROT + k * 2.0944; ctx.beginPath(); ctx.arc(CX, CY, OUTER_R + 30, a0, a0 + .42); ctx.stroke(); } ctx.lineWidth = 1; ctx.strokeStyle = css(hx(C.brass), gA + .06); ctx.fillStyle = css(hx(C.brassHi), gA + .2); for (let k = 0; k < 4; k++) { const a0 = ROT2 + k * 1.5708; ctx.beginPath(); ctx.arc(CX, CY, INNER_R, a0, a0 + .22); ctx.stroke(); ctx.fillRect(Math.round(CX + Math.cos(a0) * INNER_R) - 1, Math.round(CY + Math.sin(a0) * INNER_R) - 1, 2, 2); } for (let k = 0; k < 2; k++) { const a0 = ROT2 * .75 + .6 + k * Math.PI; ctx.fillRect(Math.round(CX + Math.cos(a0) * OUTER_R) - 1, Math.round(CY + Math.sin(a0) * OUTER_R) - 1, 3, 3); } ctx.restore(); ctx.save(); ctx.translate(W - 46, 26); ctx.fillStyle = css(lerpC(hx(C.seal), hx(C.sealHi), S), .62 + .16 * BOOST + .3 * S); roundRect(0, 0, 26, 26, 3); ctx.strokeStyle = "rgba(22,26,38,.9)"; ctx.lineWidth = 1.6; ctx.beginPath(); ctx.moveTo(6, 9); ctx.lineTo(20, 9); ctx.lineTo(17, 15); ctx.lineTo(9, 15); ctx.closePath(); ctx.stroke(); ctx.beginPath(); ctx.moveTo(13, 9); ctx.lineTo(13, 4); ctx.moveTo(6, 19); ctx.lineTo(20, 19); ctx.moveTo(8, 22); ctx.lineTo(18, 22); ctx.stroke(); ctx.restore(); } function roundRect(x, y, w, h, r) { ctx.beginPath(); ctx.moveTo(x + r, y); ctx.arcTo(x + w, y, x + w, y + h, r); ctx.arcTo(x + w, y + h, x, y + h, r); ctx.arcTo(x, y + h, x, y, r); ctx.arcTo(x, y, x + w, y, r); ctx.closePath(); ctx.fill(); } const SUN0 = { x: CX - 44, y: CY - 40 }; const SUN = { x: SUN0.x, y: SUN0.y, r: 36 }; let skyNowS = 0; function placeSun(t) { SUN.x = SUN0.x + 6.5 * Math.sin(t * .11) + 2.5 * Math.sin(t * .27 + 1.3); SUN.y = SUN0.y + (WX.kind === "sunset" ? 12 : WX.kind === "clear" ? -4 : 0) + 4 * Math.sin(t * .083 + .8) + 1.6 * Math.sin(t * .31); } const sunE = .35; const sunVis = () => WX.kind === "sun" || WX.kind === "sunset" ? 1 : 0; const dayLift = () => WX.kind === "storm" ? 0 : clamp01(WX.sun) * (1 - .5 * WX.cloud) * (1 - WX.storm); function drawSun() { const hide = 1 - sunVis(); if (hide < .98) drawSunDisc(); if (WX.kind === "storm") drawPlateStorm(1); } function drawSunDisc() { const U = WX.sunset, S = WX.storm, sp = Math.max(0, WX.sun), r = SUN.r + 5 * U + 3 * sunE + 5 * WX.sun + 1.2 * Math.sin(skyNowS * .45), x = SUN.x, y = SUN.y, a = (1 - .28 * S) * sunVis(), T = skyNowS; wash(x, y, r * (2.5 + .5 * sp), C.amber, (.22 + .06 * BOOST + .2 * U + .12 * sunE + .14 * sp) * a); wash(x, y, r * 1.8, "#f0a24c", (.3 + .14 * BOOST + .16 * U) * a); const g = ctx.createRadialGradient(x - r * .35, y - r * .4, r * .08, x, y, r); g.addColorStop(0, css(popH("#f8cd7a", PUNCH.sun), a)); g.addColorStop(.5 - .06 * BOOST, css(popH("#ec8f3c", PUNCH.sun, .02), a)); g.addColorStop(1, css(pop(lerpC(hx("#cf5a2e"), hx("#b9473a"), U), PUNCH.sun, .1), a)); ctx.fillStyle = g; ctx.beginPath(); ctx.arc(x, y, r, 0, 7); ctx.fill(); ctx.strokeStyle = css(hx("#93391f"), .68 * a); ctx.lineWidth = 2.4; ctx.beginPath(); ctx.arc(x, y, r - 1.2, 0, 7); ctx.stroke(); ctx.save(); ctx.beginPath(); ctx.arc(x, y, r, 0, 7); ctx.clip(); ctx.fillStyle = css(hx("#141927"), .5 + .2 * S); for (let i = 0; i < 4; i++) ctx.fillRect(x - r, Math.round(y + r * .2 + i * r * .2 + 1.5 * Math.sin(T * .35 + i * 1.1)), r * 2, 1.4 + i * .7); ctx.restore(); for (let k = 0; k < 32; k++) { const an = k / 32 * 6.2832 + T * .03, rr = r + 4.5; ctx.fillStyle = css(hx(k % 2 ? "#f8cd7a" : C.amber), (.75 + .15 * BOOST + .2 * U) * a); ctx.fillRect(Math.round(x + Math.cos(an) * rr) - 1, Math.round(y + Math.sin(an) * rr) - 1, 2, 2); } if (sp > .05) for (let k = 0; k < 16; k++) { const an = (k + .5) / 16 * 6.2832 - T * .06, rr = r + 10 + (k % 2) * 4 + Math.round(2 * Math.sin(T * .9 + k * .8)); ctx.fillStyle = css(hx(k % 2 ? C.gold : "#f0a24c"), .7 * sp * a); ctx.fillRect(Math.round(x + Math.cos(an) * rr) - 1, Math.round(y + Math.sin(an) * rr) - 1, 3, 3); } } function drawPlateStorm(hide) { const S = WX.storm * hide, F = WX.flash * hide, nowS = skyNowS; wash(SUN.x, SUN.y, 120, C.inkDeep, .42 * S); wash(SUN.x + 30, SUN.y - 6, 90, "#4a5266", .3 * S); const bars = [[-78, -30, 132], [-46, -18, 158], [-92, -6, 118], [-34, 6, 104]]; bars.forEach(([dx, dy, bw], k) => { const drift = 20 * Math.sin(nowS * (.05 + .02 * k) + k * 1.7); const bx = Math.round(SUN.x + dx + drift), by = SUN.y + dy, bh = 12; ctx.fillStyle = css(lerpC(hx("#59627a"), hx("#2a2f3e"), k / 3), .9 * S); roundRect(bx, by, bw, bh, bh / 2); ctx.fillStyle = css(hx("#07080d"), .45 * S); ctx.fillRect(bx + bh / 2, by + bh - 3, bw - bh, 3); ctx.fillStyle = css(hx(C.washi), .18 * S); ctx.fillRect(bx + bh / 2, by, bw - bh, 1); if (F > .01) { ctx.fillStyle = css(hx(C.washi), .65 * F); ctx.fillRect(bx + bh / 2, by + bh - 3, bw - bh, 3); ctx.fillStyle = css(hx(k % 2 ? C.brassHi : C.amber), .5 * F); ctx.fillRect(bx + bh / 2 + 6, by + bh, bw - bh - 12, 1); ctx.fillStyle = css(hx(C.brassHi), .5 * F); ctx.fillRect(bx + bh / 2, by, bw - bh, 1); } }); if (F > .01) wash(SUN.x - 10, SUN.y + 24, 110, C.washi, .2 * F); } const PSKY = { sun: { top: "#4a6f94", hor: "#7d93a6", a: .8, bl: "#d8cfb6", blA: .1 }, sunset: { top: "#2a2140", hor: "#7c4236", a: .85, bl: "#e0893a", blA: .12 }, clear: { top: "#0f1528", hor: "#243050", a: .85, bl: "#34436a", blA: .26 }, clouds: { top: "#465166", hor: "#5f6878", a: .82, bl: "#8a93a4", blA: .12 }, rain: { top: "#262e3e", hor: "#3a4456", a: .85, bl: "#4a5468", blA: .16 }, storm: { top: "#151821", hor: "#262b38", a: .55, bl: "#2a2f3e", blA: .2 } }; const PCLOUD = { sun: [[-78, [46, 30], 3.2, 0, 0], [-40, [36, 22], 4.4, 90, 1], [-6, [54, 34, 20], 2.6, 170, 0]], sunset: [[-56, [110, 70], 2.2, 30, 1], [-18, [80, 52], 3, 150, 0], [12, [64], 1.8, 60, 0]], clear: [[-30, [70], 1.4, 40, 0]], clouds: [[-92, [120, 84], 2.4, 0, 0], [-60, [96, 66, 40], 3.4, 120, 0], [-70, [70, 48], 5.2, 40, 1], [-30, [110, 70], 4.4, 200, 1], [4, [88, 58], 3, 80, 1]], rain: [[-96, [150, 110], 5.5, 0, 0], [-72, [120, 90, 54], 7, 140, 1], [-46, [100, 64], 8.5, 60, 1], [-22, [80], 6.5, 210, 0]], storm: [] }; function pixCloud(x, y, bars, body, a, lit, litA) { const bh = 9; bars.forEach((bw, k) => { const bx = Math.round(x + (bars[0] - bw) * (k % 2 ? .62 : .3)), by = Math.round(y + k * (bh - 2)); ctx.fillStyle = css(lerpC(hx(body), hx(C.inkDeep), k * .12), a); roundRect(bx, by, bw, bh, bh / 2); ctx.fillStyle = css(hx(C.inkDeep), .3 * a); ctx.fillRect(bx + bh / 2, by + bh - 2, bw - bh, 2); ctx.fillStyle = css(hx(lit || C.washi), (litA != null ? litA : .35) * a); ctx.fillRect(bx + bh / 2, k ? by + bh - 3 : by, bw - bh, 1); }); } function drawPlateSky(back) { const k = WX.kind, P = PSKY[k] || PSKY.clear, T = skyNowS, top = CY - PLATE_R, gy = GROUND + 8; if (back) { const g = ctx.createLinearGradient(0, top, 0, gy); g.addColorStop(0, css(lerpC(hx(P.top), hx(SEA.wash), .06), P.a)); g.addColorStop(1, css(lerpC(hx(P.hor), hx(SEA.wash), .1), P.a)); ctx.fillStyle = g; ctx.fillRect(CX - PLATE_R, top, PLATE_R * 2, gy - top); for (let i = 0; i < 2; i++) wash(CX + Math.sin(T * (.05 + .02 * i) + i * 2.4) * 70, CY - 50 + i * 40 + Math.cos(T * .04 + i) * 14, 70 + 20 * i, P.bl, P.blA); if (k === "sunset") { wash(SUN.x + 10, gy - 30, 150, C.amber, .16); wash(SUN.x - 30, gy - 12, 110, C.rose, .14); } if (k === "sun") wash(SUN.x, SUN.y, 90, C.washi, .1 + .03 * Math.sin(T * .5)); if (k === "clear") { const px = CX, py = GROUND + 60, rot = T * .0105; for (let i = 0; i < 110; i++) { const a0 = hsh(i * 7 + 1) * 6.2832 + rot, rr = 30 + hsh(i * 7 + 2) * 280, x = px + Math.cos(a0) * rr, y = py + Math.sin(a0) * rr; if (y > gy - 4 || Math.hypot(x - CX, y - CY) > PLATE_R - 4) continue; const tw = .5 + .5 * Math.sin(T * (.6 + hsh(i * 7 + 3) * 1.4) + i * 2.1), big = hsh(i * 7 + 4) < .22; const al = (.35 + .6 * tw * tw) * Math.min(1.25, SEA.stars), xx = Math.round(x), yy = Math.round(y); ctx.fillStyle = css(hx(i % 5 ? C.washi : "#aebbd6"), al); ctx.fillRect(xx, yy, big ? 2 : 1, big ? 2 : 1); if (big && tw > .82) { ctx.fillStyle = css(hx("#aebbd6"), .45 * (tw - .82) / .18); ctx.fillRect(xx - 2, yy, 2, 1); ctx.fillRect(xx + 2, yy + 1, 2, 1); ctx.fillRect(xx + 1, yy - 2, 1, 2); ctx.fillRect(xx, yy + 2, 1, 2); } } const mx = SUN.x + 6, my = SUN.y, mr = 17; wash(mx, my, mr * 3.4, C.washi, .1 + .02 * Math.sin(T * .4)); ctx.save(); ctx.beginPath(); ctx.arc(mx, my, mr, 0, 7); ctx.clip(); ctx.fillStyle = css(hx("#33405e"), .55); ctx.fillRect(mx - mr, my - mr, mr * 2, mr * 2); const mg = ctx.createRadialGradient(mx - mr * .4, my - mr * .4, 1, mx, my, mr); mg.addColorStop(0, "#f3ead2"); mg.addColorStop(1, "#c9bd9c"); ctx.fillStyle = mg; ctx.beginPath(); ctx.arc(mx, my, mr, 0, 7); ctx.arc(mx + mr * .5, my - mr * .22, mr * .86, 0, 7, true); ctx.fill("evenodd"); ctx.fillStyle = css(hx("#141927"), .35); ctx.fillRect(mx - mr, Math.round(my + mr * .35), mr * 2, 1); ctx.fillRect(mx - mr, Math.round(my + mr * .6), mr * 2, 1.4); ctx.restore(); for (let j = 0; j < 20; j++) { const an = j / 20 * 6.2832 + T * .04; if (Math.cos(an - 3.6) < -.2) continue; ctx.fillStyle = css(hx(C.washi), .4); ctx.fillRect(Math.round(mx + Math.cos(an) * (mr + 3)) - 1, Math.round(my + Math.sin(an) * (mr + 3)) - 1, 2, 2); } } if (k === "clouds") { wash(SUN.x, SUN.y, 70, C.washi, .16); ctx.fillStyle = css(hx("#d8cfb6"), .16); ctx.beginPath(); ctx.arc(SUN.x, SUN.y, 26, 0, 7); ctx.fill(); ctx.strokeStyle = css(hx(C.washi), .18); ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(SUN.x, SUN.y, 26, 0, 7); ctx.stroke(); } if (k === "rain" || k === "storm") { const N = k === "storm" ? 46 : 30, frost = SEA.frost; for (let i = 0; i < N; i++) { const spd = (frost ? 38 : 150) + hsh(i * 11 + 1) * (frost ? 20 : 70), span = gy - top + 20; const y = top - 10 + ((hsh(i * 11 + 2) * span + T * spd) % span), x = CX - PLATE_R - 20 + hsh(i * 11 + 3) * (PLATE_R * 2 + 40) - (y - top) * (frost ? .12 : .3); if (frost) { ctx.fillStyle = css(hx(FROST), .45); ctx.fillRect(Math.round(x), Math.round(y), 2, 2); continue; } ctx.fillStyle = css(hx("#9fb0c8"), .22 + .14 * hsh(i * 11 + 4)); ctx.fillRect(Math.round(x), Math.round(y), 1, 5); ctx.fillRect(Math.round(x) - 1, Math.round(y) + 5, 1, 3); } if (!frost) for (let i = 0; i < 6; i++) { const ph = (T * .9 + hsh(i * 13 + 5)) % 1, sx = Math.round(CX - PLATE_R + 14 + hsh(i * 13 + Math.floor(T * .9 + hsh(i * 13 + 5)) * 3) * (PLATE_R * 2 - 28)); ctx.fillStyle = css(hx("#aebbd6"), .4 * (1 - ph)); ctx.fillRect(sx - 1 - Math.round(3 * ph), gy - 1, 2, 1); ctx.fillRect(sx + 1 + Math.round(3 * ph), gy - 1, 2, 1); } } } const lanes = PCLOUD[k] || []; for (const [dy, bars, v, ph, front] of lanes) { if (!!front === back) continue; const wmax = bars[0], span = PLATE_R * 2 + wmax + 40, x = CX - PLATE_R - wmax - 20 + ((T * v + ph) % span), y = CY + dy + 2 * Math.sin(T * .2 + ph); if (k === "sun") pixCloud(x, y, bars, "#e6dcc2", front ? .62 : .5, "#fbf3dc", .7); else if (k === "sunset") pixCloud(x, y, bars, "#5a3c4c", .72, C.gold, .9); else if (k === "clear") pixCloud(x, y, bars, "#2a3654", .45, "#aebbd6", .4); else if (k === "clouds") pixCloud(x, y, bars, front ? "#9aa2b0" : "#6c7586", front ? .78 : .7, C.washi, .45); else if (k === "rain") pixCloud(x, y, bars, front ? "#4a5466" : "#353e50", .85, "#8a95a8", .4); } if (!back && (k === "sun" || k === "sunset")) { const sl = Math.floor(T / 14), e = (T - sl * 14) / 10; if (e < 1 && hsh(sl * 5 + 2) < .7) { const dir = hsh(sl * 5 + 3) < .5 ? 1 : -1, bx0 = CX + dir * (-PLATE_R - 10 + e * (PLATE_R * 2 + 20)), by0 = CY - 70 + hsh(sl * 5 + 4) * 30 + 3 * Math.sin(e * 9); ctx.fillStyle = css(hx(k === "sun" ? "#2a3048" : "#2a1e28"), .75); for (let b = 0; b < 2; b++) { const x = Math.round(bx0 - dir * b * 11), y = Math.round(by0 + b * 5), up = Math.floor(T * 3.2 + b) % 2; ctx.fillRect(x, y, 1, 1); ctx.fillRect(x - 2, y - up, 2, 1); ctx.fillRect(x + 1, y - up, 2, 1); ctx.fillRect(x - 3, y - 2 * up + (1 - up), 1, 1); ctx.fillRect(x + 3, y - 2 * up + (1 - up), 1, 1); } } } } function drawCenterPlate(alphaPlate, alphaModel, mA = 1) { const s = .9 + .1 * alphaPlate, S = WX.storm, U = WX.sunset; ctx.save(); ctx.globalAlpha = alphaPlate; ctx.translate(CX, CY); ctx.scale(s, s); ctx.translate(-CX, -CY); ctx.fillStyle = "rgba(0,0,0,.45)"; ctx.beginPath(); ctx.arc(CX, CY + 6, PLATE_R + 9, 0, 7); ctx.fill(); ctx.fillStyle = css(pop(lerpC(hx(C.brass), hx(C.brassHi), .6 * S + .3 * U), PUNCH.brass)); ctx.beginPath(); ctx.arc(CX, CY, PLATE_R + 6, 0, 7); ctx.fill(); ctx.fillStyle = "#4e3d25"; ctx.beginPath(); ctx.arc(CX, CY, PLATE_R + 2, 0, 7); ctx.fill(); const g = ctx.createRadialGradient(CX - 30, CY - 40, 10, CX, CY, PLATE_R); g.addColorStop(0, css(lerpC(hx("#2a3552"), hx("#3d2c3a"), U))); g.addColorStop(1, "#111724"); ctx.fillStyle = g; ctx.beginPath(); ctx.arc(CX, CY, PLATE_R, 0, 7); ctx.fill(); ctx.save(); ctx.beginPath(); ctx.arc(CX, CY, PLATE_R, 0, 7); ctx.clip(); drawPlateSky(true); bloom(CX + 40, CY + 20, 110, "#22304a", WX.kind === "sun" ? .28 : .45); wash(CX + 20, GROUND, 110, C.mossDeep, .3 + .08 * BOOST); wash(CX - 10, GROUND - 6, 120, SEA.mist, SEA.mistA * (1 + .35 * BOOST)); drawSun(); drawPlateSky(false); ctx.strokeStyle = css(hx(C.washi), .1 + .06 * U); ctx.lineWidth = 1; { const wo = 7 * Math.sin(skyNowS * .21); for (let i = 0; i < 4; i++) { ctx.beginPath(); ctx.moveTo(CX - PLATE_R, CY - 10 + i * 8); ctx.lineTo(CX - 60 + i * 12 + wo * (1 - i * .2), CY - 10 + i * 8); ctx.stroke(); } } ctx.fillStyle = css(lerpC(hx("#18201b"), hx(C.inkDeep), .3 * BOOST)); ctx.fillRect(CX - PLATE_R, GROUND + 8, PLATE_R * 2, 60); ctx.fillStyle = css(lerpC(hx(C.moss), hx(C.mossHi), .5 * BOOST), .22 + .14 * BOOST); ctx.fillRect(CX - PLATE_R, GROUND + 8, PLATE_R * 2, 1); if (SEA.frost) { ctx.fillStyle = css(hx(FROST), .45); ctx.fillRect(CX - PLATE_R, GROUND + 8, PLATE_R * 2, 2); for (let i = 0; i < 26; i++) ctx.fillRect(Math.round(CX - PLATE_R + 6 + hsh(i * 13 + 5) * (PLATE_R * 2 - 12)), Math.round(GROUND + 11 + hsh(i * 13 + 6) * 14), 2, 1); } else if (SEA.key === "autumn") for (let i = 0; i < 12; i++) { ctx.fillStyle = css(popH(SEA.leaf[i % 4], PUNCH.acc), .82); ctx.beginPath(); ctx.ellipse(CX - PLATE_R + 14 + hsh(i * 17 + 1) * (PLATE_R * 2 - 28), GROUND + 12 + hsh(i * 17 + 2) * 12, 3, 1.6, hsh(i * 17 + 3) * 3, 0, 7); ctx.fill(); } else if (SEA.key === "spring") for (let i = 0; i < 10; i++) { ctx.fillStyle = css(popH(i % 2 ? SEA.leaf[0] : SEA.leaf[2], PUNCH.acc), .68); ctx.fillRect(Math.round(CX - PLATE_R + 14 + hsh(i * 19 + 1) * (PLATE_R * 2 - 28)), Math.round(GROUND + 12 + hsh(i * 19 + 2) * 12), 2, 2); } ctx.fillStyle = "#3a2c22"; roundRect(CX - pot().w / 2 - 16, GROUND, pot().w + 32, 8, 2); ctx.fillStyle = "#241b15"; ctx.fillRect(CX - pot().w / 2 - 10, GROUND + 8, 6, 8); ctx.fillRect(CX + pot().w / 2 + 4, GROUND + 8, 6, 8); ctx.globalAlpha = alphaPlate * alphaModel; drawTree(false); drawPot(pot(), CX, GROUND); drawTree(true); ctx.globalAlpha = alphaPlate; drawMist(alphaPlate); ctx.strokeStyle = "rgba(5,6,10,.5)"; ctx.lineWidth = 7; ctx.beginPath(); ctx.arc(CX, CY, PLATE_R - 2, 0, 7); ctx.stroke(); ctx.restore(); ctx.strokeStyle = css(hx(C.gold), .25 + .45 * U); ctx.lineWidth = 1.6; ctx.beginPath(); ctx.arc(CX, CY, PLATE_R + 5, Math.PI * 1.05, Math.PI * 1.55); ctx.stroke(); ctx.restore(); } function potPath(p, cx, by) { const w = p.w, h = p.h, t = by - h; ctx.beginPath(); switch (p.shape) { case "drum": ctx.moveTo(cx - w / 2, t); ctx.lineTo(cx + w / 2, t); ctx.lineTo(cx + w / 2 - 4, by); ctx.lineTo(cx - w / 2 + 4, by); break; case "bowl": ctx.moveTo(cx - w / 2, t); ctx.lineTo(cx + w / 2, t); ctx.bezierCurveTo(cx + w / 2, t + h * .9, cx + w * .2, by, cx, by); ctx.bezierCurveTo(cx - w * .2, by, cx - w / 2, t + h * .9, cx - w / 2, t); break; case "tall": ctx.moveTo(cx - w / 2, t); ctx.lineTo(cx + w / 2, t); ctx.lineTo(cx + w / 2 - 3, by - 6); ctx.lineTo(cx + w / 2 - 8, by); ctx.lineTo(cx - w / 2 + 8, by); ctx.lineTo(cx - w / 2 + 3, by - 6); break; case "flared": ctx.moveTo(cx - w / 2, t); ctx.lineTo(cx + w / 2, t); ctx.lineTo(cx + w / 2 - 16, by - 5); ctx.lineTo(cx + w / 2 - 16, by); ctx.lineTo(cx + w / 2 - 26, by); ctx.lineTo(cx + w / 2 - 26, by - 4); ctx.lineTo(cx - w / 2 + 26, by - 4); ctx.lineTo(cx - w / 2 + 26, by); ctx.lineTo(cx - w / 2 + 16, by); ctx.lineTo(cx - w / 2 + 16, by - 5); break; case "footed": ctx.moveTo(cx - w / 2 + 4, t); ctx.lineTo(cx + w / 2 - 4, t); ctx.quadraticCurveTo(cx + w / 2 + 6, t + h * .55, cx + w / 2 - 8, by - 6); ctx.lineTo(cx + w / 2 - 12, by); ctx.lineTo(cx + w / 2 - 20, by); ctx.lineTo(cx + w / 2 - 20, by - 5); ctx.lineTo(cx - w / 2 + 20, by - 5); ctx.lineTo(cx - w / 2 + 20, by); ctx.lineTo(cx - w / 2 + 12, by); ctx.lineTo(cx - w / 2 + 8, by - 6); ctx.quadraticCurveTo(cx - w / 2 - 6, t + h * .55, cx - w / 2 + 4, t); break; case "oval": ctx.moveTo(cx - w / 2, t); ctx.lineTo(cx + w / 2, t); ctx.quadraticCurveTo(cx + w / 2, by, cx + w / 2 - 14, by); ctx.lineTo(cx - w / 2 + 14, by); ctx.quadraticCurveTo(cx - w / 2, by, cx - w / 2, t); break; case "lotus": ctx.moveTo(cx - w / 2, t); for (let i = 0; i < 6; i++) { const x0 = cx - w / 2 + i * w / 6; ctx.quadraticCurveTo(x0 + w / 12, t + 7, x0 + w / 6, t); } ctx.bezierCurveTo(cx + w / 2 - 2, t + h * .8, cx + w * .2, by, cx, by); ctx.bezierCurveTo(cx - w * .2, by, cx - w / 2 + 2, t + h * .8, cx - w / 2, t); break; } ctx.closePath(); } function drawPot(p, cx, by) { const t = by - p.h; const g = ctx.createLinearGradient(cx - p.w / 2, 0, cx + p.w / 2, 0); g.addColorStop(0, css(popH(p.g2, PUNCH.pot, .14))); g.addColorStop(.35, css(popH(p.g1, PUNCH.pot))); g.addColorStop(1, css(popH(p.g2, PUNCH.pot, .14))); potPath(p, cx, by); ctx.fillStyle = g; ctx.fill(); ctx.save(); potPath(p, cx, by); ctx.clip(); ctx.strokeStyle = "rgba(16,18,26,.45)"; ctx.fillStyle = "rgba(16,18,26,.25)"; ctx.lineWidth = 1; const ps = seed; seed = 4242; if (p.pat === "crackle") { ctx.strokeStyle = "rgba(240,232,210,.28)"; for (let i = 0; i < 16; i++) { let x = cx - p.w / 2 + rnd() * p.w, y = t + rnd() * p.h; ctx.beginPath(); ctx.moveTo(x, y); for (let k = 0; k < 3; k++) { x += (rnd() - .5) * 14; y += (rnd() - .5) * 10; ctx.lineTo(x, y); } ctx.stroke(); } } else if (p.pat === "drip") { ctx.fillStyle = "rgba(176,141,87,.35)"; for (let i = 0; i < 7; i++) { const x = cx - p.w / 2 + 8 + i * (p.w - 16) / 6, d = 6 + rnd() * 10; ctx.beginPath(); ctx.moveTo(x - 5, t); ctx.lineTo(x + 5, t); ctx.lineTo(x + 2, t + d); ctx.arc(x, t + d, 2, 0, Math.PI); ctx.closePath(); ctx.fill(); } } else if (p.pat === "band") { ctx.fillStyle = "rgba(176,141,87,.55)"; ctx.fillRect(cx - p.w, t + p.h * .3, p.w * 2, 3); ctx.fillRect(cx - p.w, t + p.h * .3 + 6, p.w * 2, 1); } else if (p.pat === "waves") { ctx.strokeStyle = "rgba(216,207,182,.22)"; for (let x = cx - p.w / 2; x < cx + p.w / 2; x += 12) { ctx.beginPath(); ctx.arc(x, t + p.h * .75, 6, Math.PI, 0); ctx.stroke(); ctx.beginPath(); ctx.arc(x, t + p.h * .75, 3, Math.PI, 0); ctx.stroke(); } } else if (p.pat === "dots") { ctx.fillStyle = "rgba(169,88,74,.45)"; for (let i = 0; i < 9; i++) { ctx.beginPath(); ctx.arc(cx - p.w / 2 + 10 + rnd() * (p.w - 20), t + 6 + rnd() * (p.h - 14), 1.6 + rnd() * 1.4, 0, 7); ctx.fill(); } } seed = ps; ctx.fillStyle = "rgba(255,255,255,.07)"; ctx.fillRect(cx - p.w / 2 + p.w * .18, t, p.w * .08, p.h); ctx.restore(); ctx.fillStyle = p.rim; roundRect(cx - p.w / 2 - 2, t - 3, p.w + 4, 5, 2); ctx.fillStyle = "#2a2a22"; ctx.fillRect(cx - p.w / 2 + 4, t - 4, p.w - 8, 2); ctx.fillStyle = C.mossDeep; ctx.beginPath(); ctx.ellipse(cx, t - 3, p.w / 2 - 6, 3, 0, Math.PI, 0); ctx.fill(); if (SEA.frost) { ctx.fillStyle = css(hx(FROST), .7); ctx.beginPath(); ctx.ellipse(cx - 4, t - 4, p.w / 2 - 14, 2.6, 0, Math.PI, 0); ctx.fill(); ctx.fillStyle = css(hx(C.washi), .5); ctx.fillRect(Math.round(cx - p.w * .22), t - 6, Math.round(p.w * .2), 1); for (let i = 0; i < 14; i++) { const u = hsh(i * 31 + 9), x = Math.round(cx - p.w / 2 + 1 + u * (p.w - 4)), wd = 2 + Math.round(hsh(i * 31 + 10) * 6); ctx.fillStyle = css(hx(i % 4 ? FROST : C.washi), .62 + .2 * hsh(i * 31 + 11)); ctx.fillRect(x, t - 4, wd, 2); if (wd > 5) ctx.fillRect(x + 1, t - 5, wd - 3, 1); } ctx.fillStyle = css(hx(FROST), .5); ctx.fillRect(cx - p.w / 2 - 1, t - 4, 3, 2); ctx.fillRect(cx + p.w / 2 - 2, t - 4, 3, 2); } } function drawTree(foliage) { const M = model(), S = WX.storm, U = WX.sunset, fall = SEA.key === "autumn", sp = Math.max(0, WX.sun) * (1 - S); const warm = fall ? .55 + .45 * U : .45 * sstep(.35, .95, U); const summer = SEA.key === "summer"; const rimC = lerpC(lerpC(hx(C.washi), hx(C.brassHi), summer ? .2 : .45), hx(C.gold), warm); const rimA = .12 + (summer ? .04 : .08) * sp + (fall ? .5 * U : .22 * sstep(.35, .95, U)); const glowC = fall ? "214,170,100" : "222,208,172"; if (!foliage) { ctx.lineCap = "round"; for (const n of nodes) { const tip = !n.kids.length && n.depth >= 2, ex = n.x + (tip ? (n.dx || 0) * .6 : 0), ey = n.y + (tip ? (n.dy || 0) * .6 : 0); ctx.strokeStyle = css(n.depth < 2 ? popH(C.bark, PUNCH.bark, .08) : popH(C.barkHi, PUNCH.bark)); ctx.lineWidth = Math.min(13, 1.2 + Math.sqrt(n.thick) * 1.9); ctx.beginPath(); ctx.moveTo(n.ox, n.oy); ctx.lineTo(ex, ey); ctx.stroke(); } if (SEA.frost) for (const n of nodes) { if (n.depth < 1 || hsh(n.x * 7 + n.y * 13) > .55) continue; const ex = Math.round(n.x + (!n.kids.length ? (n.dx || 0) * .6 : 0)), ey = Math.round(n.y + (!n.kids.length ? (n.dy || 0) * .6 : 0)), w = n.depth < 2 ? 4 : 3; ctx.fillStyle = css(hx(FROST), .8); ctx.fillRect(ex - w / 2 - 1, ey - 3, w + 1, 2); ctx.fillStyle = css(hx(C.washi), .55); ctx.fillRect(ex - w / 2, ey - 4, w - 1, 1); } return; } for (const n of nodes) { if (n.kids.length || n.depth < 2) continue; if (SEA.sparse && n.tint > 1 - SEA.sparse) continue; const r = (4 + Math.min(4, n.depth) * 1.3) * n.grow * SEA.scale, x = n.x + (n.dx || 0), y = n.y + (n.dy || 0), rot = n.rot || 0; if (n.glow > .03) { ctx.fillStyle = `rgba(${glowC},${(.32 * n.glow).toFixed(3)})`; ctx.beginPath(); ctx.arc(x, y, r * 2.2, 0, 7); ctx.fill(); } ctx.fillStyle = "rgba(5,6,10,.52)"; ctx.beginPath(); ctx.ellipse(x + .8, y + 1.8, r * 1.3, r * .85, rot, 0, 7); ctx.fill(); ctx.save(); ctx.translate(x, y); ctx.rotate(rot); ctx.fillStyle = css(pop(lerpC(hx(n.tint < .5 ? M.leaf[0] : M.leaf[1]), hx(SEA.leaf[Math.floor(n.tint * 4) % 4]), SEA.mix), PUNCH.leaf, n.tint < .25 ? .07 : 0)); if (M.leafShape === "pad") { ctx.beginPath(); ctx.ellipse(0, 0, r * 1.6, r * .95, 0, 0, 7); ctx.fill(); ctx.fillStyle = "rgba(226,218,196,.23)"; ctx.beginPath(); ctx.ellipse(0, -r * .3, r * 1.1, r * .35, 0, 0, 7); ctx.fill(); } else if (M.leafShape === "star") { for (let k = 0; k < 5; k++) { const a = k * 1.2566 - 1.57; ctx.beginPath(); ctx.ellipse(Math.cos(a) * r * .55, Math.sin(a) * r * .55, r * .55, r * .25, a, 0, 7); ctx.fill(); } } else if (M.leafShape === "blossom") { for (let k = 0; k < 5; k++) { const a = k * 1.2566; ctx.beginPath(); ctx.arc(Math.cos(a) * r * .45, Math.sin(a) * r * .45, r * .4, 0, 7); ctx.fill(); } ctx.fillStyle = "rgba(176,141,87,.85)"; ctx.beginPath(); ctx.arc(0, 0, 1.4, 0, 7); ctx.fill(); } else { for (let k = 0; k < 4; k++) { ctx.beginPath(); ctx.arc((k % 2 ? 1 : -1) * r * .5, (k > 1 ? 1 : -1) * r * .35, r * .5, 0, 7); ctx.fill(); } } ctx.strokeStyle = css(lerpC(rimC, hx(C.brassHi), S), rimA + .35 * S); ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(0, 0, r * .95, Math.PI * 1.05, Math.PI * 1.6); ctx.stroke(); if (SEA.frost && (n.tint > .45 || n.tint < .14)) { ctx.fillStyle = css(hx(FROST), .78); ctx.fillRect(-r * .7, -r * .75, r * 1.2, 1.4); ctx.fillRect(r * .2, -r * .4, 1.4, 1.4); if (n.tint > .52) { ctx.fillStyle = css(hx(C.washi), .6); ctx.fillRect(-r * .35, -r * .95, r * .6, 1.2); } } ctx.restore(); if (n.glow > .03) { ctx.fillStyle = `rgba(240,226,196,${(.45 * n.glow).toFixed(3)})`; ctx.beginPath(); ctx.arc(x, y, r * .5, 0, 7); ctx.fill(); } } } function drawMotif(p) { const { x, y } = p, r = p.r * .55; ctx.save(); ctx.translate(x, y); { const k = 1 + .1 * p.pulse + .05 * p.lv; ctx.rotate(.22 * Math.sin(p.ph * .8) * (.3 + p.lv)); ctx.scale(k, k); } ctx.strokeStyle = "rgba(232,226,208,.74)"; ctx.fillStyle = "rgba(232,226,208,.74)"; ctx.lineWidth = 1.2; ctx.lineCap = "round"; const L = (a, b, c, d) => { ctx.beginPath(); ctx.moveTo(a, b); ctx.lineTo(c, d); ctx.stroke(); }; const A = (cx, cy, rr, s, e) => { ctx.beginPath(); ctx.arc(cx, cy, rr, s, e); ctx.stroke(); }; switch (p.ring === 0 ? p.el : p.motif) { case "earth": for (let i = 0; i < 3; i++) { ctx.beginPath(); ctx.ellipse(0, r * .6 - i * r * .55, r * (1 - i * .25), r * .28, 0, 0, 7); ctx.stroke(); } break; case "water": for (let i = 1; i <= 3; i++) A(0, r * .7, r * i * .4, Math.PI, 0); break; case "fire": ctx.beginPath(); ctx.moveTo(-r * .8, r * .7); ctx.lineTo(0, -r); ctx.lineTo(r * .8, r * .7); ctx.closePath(); ctx.stroke(); ctx.beginPath(); ctx.moveTo(-r * .35, r * .7); ctx.lineTo(0, -r * .1); ctx.lineTo(r * .35, r * .7); ctx.stroke(); break; case "wind": ctx.beginPath(); for (let t = 0; t < 12; t += .2) { const rr = r * t / 12; ctx.lineTo(Math.cos(t) * rr, Math.sin(t) * rr); } ctx.stroke(); break; case "void": A(0, 0, r * .85, 0, 7); ctx.beginPath(); ctx.arc(0, 0, 1.6, 0, 7); ctx.fill(); break; case "stone": ctx.beginPath(); ctx.ellipse(0, r * .2, r * .9, r * .5, -.2, 0, 7); ctx.stroke(); break; case "rain": for (let i = -1; i <= 1; i++) L(i * r * .6 + r * .2, -r * .7, i * r * .6 - r * .2, r * .7); break; case "moon": ctx.beginPath(); ctx.arc(0, 0, r * .75, .6, 5.7); ctx.arc(r * .35, 0, r * .55, 5.4, .9, true); ctx.closePath(); ctx.fill(); break; case "cloud": A(-r * .4, r * .1, r * .4, Math.PI, 0); A(r * .2, -r * .05, r * .5, Math.PI, 0); L(-r * .8, r * .1, r * .7, r * .1); break; case "wave": A(-r * .4, r * .3, r * .45, Math.PI, 0); A(r * .45, r * .3, r * .45, Math.PI, 0); break; case "leaf": ctx.beginPath(); ctx.moveTo(-r * .8, r * .5); ctx.quadraticCurveTo(-r * .4, -r * .8, r * .8, -r * .5); ctx.quadraticCurveTo(r * .4, r * .8, -r * .8, r * .5); ctx.stroke(); L(-r * .8, r * .5, r * .5, -r * .3); break; case "crackle":L(-r * .8, -r * .2, -r * .1, r * .1); L(-r * .1, r * .1, r * .3, -r * .7); L(-r * .1, r * .1, r * .6, r * .6); L(r * .3, -r * .1, r * .8, 0); break; case "sand": for (let i = -1; i <= 1; i++) L(-r * .8, i * r * .45, r * .8, i * r * .45); break; case "mist": L(-r * .8, -r * .3, r * .2, -r * .3); L(-r * .3, 0, r * .8, 0); L(-r * .6, r * .3, r * .4, r * .3); break; case "pebbles":[[-.4, .2], [.3, .3], [0, -.35]].forEach(([a, b]) => { ctx.beginPath(); ctx.arc(a * r, b * r, r * .2, 0, 7); ctx.fill(); }); break; case "drip": ctx.beginPath(); ctx.moveTo(0, -r * .8); ctx.quadraticCurveTo(r * .7, r * .2, 0, r * .7); ctx.quadraticCurveTo(-r * .7, r * .2, 0, -r * .8); ctx.stroke(); break; case "seed": ctx.beginPath(); ctx.arc(0, r * .3, r * .3, 0, 7); ctx.fill(); L(0, 0, 0, -r * .7); L(0, -r * .4, r * .4, -r * .7); break; } ctx.restore(); } function drawPlateLife(p, R) { const lv = Math.min(1, p.lv * .8 + p.pulse * .7), warm = p.ring ? C.washi : C.gold; if (lv > .02) wash(p.x, p.y, R * (.7 + .3 * p.pulse), p.ring ? SEA.glaze : C.amber, .08 + .3 * lv); if (p.rip < 1) { const e = p.rip, rr = R * (.2 + .72 * e); ctx.strokeStyle = css(hx(warm), .7 * (1 - e)); ctx.lineWidth = 1.4; ctx.beginPath(); ctx.arc(p.x, p.y, rr, 0, 7); ctx.stroke(); if (e > .25) { const r2 = R * (.2 + .72 * (e - .25)); ctx.strokeStyle = css(hx(C.brassHi), .45 * (1 - e)); ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(p.x, p.y, r2, 0, 7); ctx.stroke(); } } ctx.strokeStyle = css(hx(C.washi), .22 + .5 * lv); ctx.lineWidth = 1.4; ctx.beginPath(); ctx.arc(p.x, p.y, R * .8, p.ph, p.ph + .5 + 1.3 * lv); ctx.stroke(); ctx.setLineDash([2, 3]); ctx.lineWidth = 1; ctx.strokeStyle = css(hx(C.brassHi), .1 + .3 * lv); ctx.beginPath(); ctx.arc(p.x, p.y, R * .62, -p.ph * .6, -p.ph * .6 + 6.2832); ctx.stroke(); ctx.setLineDash([]); const oa = p.ph * 1.6 + Math.PI, ox = Math.round(p.x + Math.cos(oa) * R * .8), oy = Math.round(p.y + Math.sin(oa) * R * .8), sz = p.pulse > .4 ? 3 : 2; ctx.fillStyle = css(hx(p.ring ? C.brassHi : C.gold), .5 + .5 * lv); ctx.fillRect(ox - 1, oy - 1, sz, sz); } const PLATE_HALO = { clear: { c: "#7398de", in: .38, w: 12, rc: "#b4cbf4", ra: .42, a: 1 }, storm: { c: "#6d92da", in: .30, w: 11, rc: "#b0c6ee", ra: .36, a: 1 }, rain: { c: "#7b9cd6", in: .34, w: 11, rc: "#b9cbea", ra: .38, a: 1 }, clouds: { c: "#94aacc", in: .18, w: 9, rc: "#c3cfe0", ra: .20, a: 1 }, sun: { c: "#e6bc7c", in: .28, w: 10, rc: "#f2d9a8", ra: .30, a: 1 }, sunset: { c: "#e39868", in: .28, w: 10, rc: "#f2c096", ra: .30, a: 1 } }; function plateHalo(p, R0, now, k) { const gw = PLATE_HALO[WX.kind]; if (!gw) return; const amt = gw.a * k; const br = .78 + .22 * Math.sin(now * .001 * (6.2832 / 6.5) + p.i * 1.7 + p.ring * 2.4) + .06 * Math.sin(now * .001 * 1.9 + p.i * 3.1); const RW = gw.w * (.92 + .1 * br), hg = ctx.createRadialGradient(p.x, p.y, R0 - 1, p.x, p.y, R0 + RW), ac = hx(gw.c); hg.addColorStop(0, css(ac, gw.in * amt * br)); hg.addColorStop(.38, css(ac, gw.in * .4 * amt * br)); hg.addColorStop(1, css(ac, 0)); ctx.fillStyle = hg; ctx.beginPath(); ctx.arc(p.x, p.y, R0 + RW, 0, 6.2832); ctx.arc(p.x, p.y, R0 - 1, 6.2832, 0, true); ctx.fill(); const sd = Math.atan2(p.y - CY, p.x - CX), sw = .25 * Math.sin(now * .001 * .37 + p.i); ctx.lineWidth = 1; ctx.strokeStyle = css(hx(gw.rc), gw.ra * amt * (.7 + .3 * br)); ctx.beginPath(); ctx.arc(p.x, p.y, R0 + .8, sd + .7 + sw, sd + 1.9 + sw); ctx.stroke(); ctx.beginPath(); ctx.arc(p.x, p.y, R0 + .8, sd - 1.9 + sw, sd - .7 + sw); ctx.stroke(); } function drawStormHalos(now) { for (const p of plates) { const arr = arrivalOf(p, now - arriveT0); if (arr <= 0 || p.alive < .05) continue; const r = p.r * (.6 + .4 * arr); ctx.save(); ctx.globalAlpha = arr; plateHalo(p, r * p.alive + 2.5, now, WX.storm * (1 - .6 * WX.flash)); ctx.restore(); } } const MIST = { lead: .5, clear: 1.0, pow: .85, a: .62, bank: .34, grain: .3, top: 178, drift: 5, cols: ["#d8cfb6", "#c7ccc9", "#a9b4c0", "#8e9bb0", "#cfd5d8"], }; let mistNow = { d: 0, rise: 0 }, mistT = 0; function mistBlob(x, y, rx, ry, hex, a) { if (a < .004) return; const c = hx(hex); ctx.save(); ctx.translate(x, y); ctx.scale(1, ry / rx); const g = ctx.createRadialGradient(0, 0, 0, 0, 0, rx); g.addColorStop(0, css(c, a)); g.addColorStop(.45, css(c, a * .62)); g.addColorStop(.78, css(c, a * .2)); g.addColorStop(1, css(c, 0)); ctx.fillStyle = g; ctx.fillRect(-rx, -rx, rx * 2, rx * 2); ctx.restore(); } function drawMist(alphaPlate) { const d = mistNow.d * alphaPlate; if (d < .01) return; const T = mistT, rise = mistNow.rise, base = GROUND + 6, top = base - MIST.top * rise, span = PLATE_R * 2 + 80; ctx.save(); mistBlob(CX, base, 150, 24 + 18 * rise, MIST.cols[1], MIST.bank * d); mistBlob(CX + 8 * Math.sin(T * .4), base - 10, 96, 20 + 16 * rise, MIST.cols[0], MIST.bank * .9 * d); for (let i = 0; i < 14; i++) { const h = k => hsh(i * 23 + k * 7 + 1901), f = (i + .5) / 14; const y = base - (base - top) * f + 6 * Math.sin(T * (.3 + .2 * h(1)) + i); const dir = h(2) < .5 ? -1 : 1, v = MIST.drift * (.6 + .8 * h(3)) * dir; const x = CX - span / 2 + ((h(4) * span + T * v) % span + span) % span; const ax = 1 - .55 * sstep(40, PLATE_R, Math.abs(x - CX)); const rx = 46 + 40 * h(5) + 18 * f, ry = 16 + 12 * h(6); mistBlob(x, y, rx, ry, MIST.cols[(i + (h(7) * 3 | 0)) % MIST.cols.length], MIST.a * d * ax * (.55 + .45 * h(8)) * (f <= rise + .08 ? 1 : .35)); } const pk = sstep(.45, 1, d); mistBlob(CX, (base + top) / 2 + 10, 92, Math.max(20, (base - top) * .55), MIST.cols[0], MIST.a * .75 * pk); mistBlob(CX - 6, GROUND - 20, 70, 36, MIST.cols[4], MIST.a * .5 * pk); for (let i = 0; i < 22; i++) { const h = k => hsh(i * 31 + k * 5 + 4219), life = 3 + 2 * h(1), ph = ((T / life + h(2)) % 1 + 1) % 1; const x = Math.round(CX - 95 + h(3) * 190 + 6 * Math.sin(T * .7 + i)), y = Math.round(base - ph * (base - top + 10)); const a = MIST.grain * d * Math.sin(Math.PI * ph) * (.5 + .5 * h(4)); if (a < .01) continue; ctx.fillStyle = css(hx(MIST.cols[i % 2 ? 0 : 2]), a); ctx.fillRect(x, y, h(5) < .3 ? 2 : 1, 1); } ctx.restore(); } const GLASS = { dur: 1150, core: "#d4e4fa", band: "#86aee8", fil: "#bcd5fb", glow: "#7aa2e4", per: { clear: 8, rain: 7.5, storm: 7, clouds: 9, sun: 11, sunset: 10 }, amt: { clear: 1, rain: .95, storm: 1, clouds: .85, sun: .62, sunset: .68 } }; let boardNow = 0; function glassStart(p, now) { if (p.zapT != null && now - p.zapT >= 0 && now - p.zapT < GLASS.dur) return { t0: p.zapT, seed: p.zapS }; const P = (GLASS.per[WX.kind] || 9) * 1000, id = p.ring * 31 + p.i, sl = Math.floor((now + hsh(id * 17 + 5) * P) / P); if (hsh(sl * 977 + id * 131 + 7) > .45) return null; const t0 = sl * P - hsh(id * 17 + 5) * P + hsh(sl * 389 + id * 53 + 2) * (P - GLASS.dur); return now >= t0 && now < t0 + GLASS.dur ? { t0, seed: sl * 7 + id } : null; } function glassKick(p, s) { if (s < .72 || p.alive < .9) return; const last = p.zapT == null ? -1e9 : p.zapT; if (boardNow - last < 3200) return; p.zapT = boardNow; p.zapS = (boardNow | 0) + p.i * 3; } function drawGlassSweeps(now) { const k0 = GLASS.amt[WX.kind] != null ? GLASS.amt[WX.kind] : .8; for (const p of plates) { if (p.alive < .9) continue; const arr = arrivalOf(p, now - arriveT0); if (arr < .9) continue; const g = glassStart(p, now); if (!g) continue; const u = (now - g.t0) / GLASS.dur, e = u * u * (3 - 2 * u), env = Math.sin(Math.PI * u), k = k0 * env * arr * (1 - .5 * WX.flash); if (k < .01) continue; const R = p.r * (.6 + .4 * arr) * p.alive, h = i => hsh(g.seed * 7919 + i * 104729); const th = -.75 + (h(1) - .5) * .9 + (h(2) < .5 ? 0 : Math.PI); const nx = Math.cos(th), ny = Math.sin(th), tx = -ny, ty = nx, d = -R * 1.35 + e * R * 2.7; const bx = p.x + nx * d, by = p.y + ny * d, BW = R * .55; ctx.save(); ctx.globalCompositeOperation = "screen"; const RO = R + 2.5, hg = ctx.createRadialGradient(p.x, p.y, RO - 1, p.x, p.y, RO + 8); hg.addColorStop(0, css(hx(GLASS.glow), .30 * k)); hg.addColorStop(1, css(hx(GLASS.glow), 0)); ctx.fillStyle = hg; ctx.beginPath(); ctx.arc(p.x, p.y, RO + 8, 0, 6.2832); ctx.arc(p.x, p.y, RO - 1, 6.2832, 0, true); ctx.fill(); ctx.lineWidth = 1; ctx.strokeStyle = css(hx(GLASS.fil), .42 * k); ctx.beginPath(); ctx.arc(p.x, p.y, R + 1.2, 0, 6.2832); ctx.stroke(); ctx.beginPath(); ctx.arc(p.x, p.y, R, 0, 6.2832); ctx.clip(); ctx.fillStyle = css(hx(GLASS.glow), .07 * k); ctx.fillRect(p.x - R, p.y - R, R * 2, R * 2); const lg = ctx.createLinearGradient(bx - nx * BW, by - ny * BW, bx + nx * BW * .35, by + ny * BW * .35); lg.addColorStop(0, css(hx(GLASS.band), 0)); lg.addColorStop(.62, css(hx(GLASS.band), .20 * k)); lg.addColorStop(.9, css(hx(GLASS.core), .34 * k)); lg.addColorStop(1, css(hx(GLASS.core), 0)); ctx.fillStyle = lg; ctx.fillRect(p.x - R - 2, p.y - R - 2, R * 2 + 4, R * 2 + 4); const ex = bx - nx * BW * 1.1, ey = by - ny * BW * 1.1, lg2 = ctx.createLinearGradient(ex - nx * 4, ey - ny * 4, ex + nx * 4, ey + ny * 4); lg2.addColorStop(0, css(hx(GLASS.core), 0)); lg2.addColorStop(.5, css(hx(GLASS.core), .14 * k)); lg2.addColorStop(1, css(hx(GLASS.core), 0)); ctx.fillStyle = lg2; ctx.fillRect(p.x - R - 2, p.y - R - 2, R * 2 + 4, R * 2 + 4); const fl = Math.floor(now / 70), hf = i => hsh(fl * 6151 + g.seed * 29 + i * 92821), N = 9; ctx.strokeStyle = css(hx(GLASS.fil), (.5 + .25 * hf(0)) * k); ctx.lineWidth = 1; ctx.beginPath(); for (let i = 0; i <= N; i++) { const s = -R * 1.05 + (i / N) * R * 2.1, j = (hf(i + 1) - .5) * 3.2; const x = bx + tx * s + nx * j, y = by + ty * s + ny * j; i ? ctx.lineTo(x, y) : ctx.moveTo(x, y); } ctx.stroke(); const cs = Math.sqrt(Math.max(0, R * R - d * d)); if (cs > 2) for (const sg of [-1, 1]) if (hf(20 + sg) < .7) { ctx.fillStyle = css(hx(GLASS.core), .7 * k); ctx.fillRect(Math.round(bx + tx * cs * sg * .96) - 1, Math.round(by + ty * cs * sg * .96) - 1, 2, 2); } ctx.restore(); } } function drawPlates(now) { for (const p of plates) { const arr = arrivalOf(p, now - arriveT0); if (arr <= 0) continue; ctx.save(); ctx.globalAlpha = arr; const r = p.r * (.6 + .4 * arr); ctx.fillStyle = "rgba(0,0,0,.3)"; ctx.beginPath(); ctx.arc(p.x, p.y + 3, r + 2, 0, 7); ctx.fill(); if (p.alive < .05) { ctx.strokeStyle = "rgba(176,141,87,.35)"; ctx.setLineDash([3, 4]); ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(p.x, p.y, r, 0, 7); ctx.stroke(); ctx.setLineDash([]); } else { const a = p.alive, S = WX.storm, U = WX.sunset; if (WX.kind !== "storm") plateHalo(p, r * a + 2.5, now, 1); ctx.fillStyle = css(pop(lerpC(hx(p.ring ? C.brassDim : C.brass), hx(C.brassHi), .55 * S + .2 * U), PUNCH.brass)); ctx.beginPath(); ctx.arc(p.x, p.y, r * a + 2.5, 0, 7); ctx.fill(); const g = ctx.createRadialGradient(p.x - r * .3, p.y - r * .3, 1, p.x, p.y, r); const pf = pop(lerpC(hx(p.fill), hx(SEA.glaze), SEA.gmix), PUNCH.glaze, .04); g.addColorStop(0, css(pop(lerpC(lerpC(hx(p.rim), hx(SEA.glaze), SEA.gmix * .6), hx(C.washi), .16 * BOOST), PUNCH.glaze))); g.addColorStop(.7, css(pf)); g.addColorStop(1, css(lerpC(pf, hx(C.inkDeep), .45 + .1 * BOOST))); ctx.fillStyle = g; ctx.beginPath(); ctx.arc(p.x, p.y, r * a, 0, 7); ctx.fill(); wash(p.x - r * .3, p.y - r * .35, r * .8, C.washi, (.12 + .07 * BOOST) * a); ctx.strokeStyle = "rgba(5,6,10,.52)"; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(p.x + .6, p.y + .6, Math.max(0, r * a - 1.4), 0, 7); ctx.stroke(); const toC = Math.atan2(SUN0.y - p.y, SUN0.x - p.x); ctx.strokeStyle = css(hx(C.gold), (.18 + .1 * BOOST + .55 * U) * a); ctx.lineWidth = 1.4; ctx.beginPath(); ctx.arc(p.x, p.y, r * a + 1.6, toC - .9, toC + .9); ctx.stroke(); if (a > .8) drawPlateLife(p, r * a); if (a > .8) drawMotif(p); if (p.glow > .03) { ctx.strokeStyle = `rgba(234,220,191,${.55 * p.glow})`; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(p.x, p.y, r + 4, 0, 7); ctx.stroke(); } } ctx.restore(); } } const BREEZE_VIS = false; function drawBreeze(now) { if (!BREEZE_VIS || !SONG || !AN || songT() <= 0) return; const e = env(songT()), x0 = breezeX; const gr = ctx.createLinearGradient(x0 - 80, 0, x0 + 4, 0); gr.addColorStop(0, "rgba(176,141,87,0)"); gr.addColorStop(1, `rgba(176,141,87,${(.05 + .12 * e).toFixed(3)})`); ctx.fillStyle = gr; ctx.fillRect(x0 - 80, 0, 84, H); ctx.strokeStyle = `rgba(201,170,120,${(.18 + .32 * e).toFixed(3)})`; ctx.lineWidth = 1.2; for (let i = 0; i < 6; i++) { const y = 60 + i * 96 + Math.sin(now * .001 + i) * 10, x = x0 - 10 - (i % 3) * 14; ctx.beginPath(); ctx.moveTo(x - 30, y); ctx.lineTo(x - 6, y); ctx.arc(x - 6, y - 6, 6, Math.PI / 2, -Math.PI * .9, true); ctx.stroke(); } } function drawFx(dt) { for (let i = falling.length - 1; i >= 0; i--) { const f = falling[i]; f.vy += .02 * dt / 16; f.x1 += f.vx; f.x2 += f.vx; f.y1 += f.vy; f.y2 += f.vy; f.a -= .008 * dt / 16; if (f.a <= 0) { falling.splice(i, 1); continue; } ctx.globalAlpha = f.a; ctx.strokeStyle = C.barkHi; ctx.lineWidth = Math.min(5, 1 + Math.sqrt(f.thick)); ctx.beginPath(); ctx.moveTo(f.x1, f.y1); ctx.lineTo(f.x2, f.y2); ctx.stroke(); if (f.leaf) { ctx.fillStyle = css(lerpC(hx(model().leaf[0]), hx(SEA.leaf[0]), SEA.mix)); ctx.beginPath(); ctx.ellipse(f.x2, f.y2, 5, 3, 0, 0, 7); ctx.fill(); } ctx.globalAlpha = 1; } for (let i = ripples.length - 1; i >= 0; i--) { const s = ripples[i]; s.r += .6 * dt / 16; s.a -= .016 * dt / 16; if (s.a <= 0) { ripples.splice(i, 1); continue; } ctx.strokeStyle = s.seal ? `rgba(169,88,74,${s.a})` : `rgba(201,170,120,${s.a})`; ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(s.x, s.y, s.r, 0, 7); ctx.stroke(); } if (drag && drag.moved > 8 && drag.trail.length > 1) { ctx.strokeStyle = drag.cut ? "rgba(169,88,74,.6)" : "rgba(169,189,136,.38)"; ctx.lineWidth = drag.cut ? 1.6 : 1.2; ctx.setLineDash(drag.cut ? [4, 5] : [2, 6]); ctx.beginPath(); drag.trail.forEach((p, i) => i ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y)); ctx.stroke(); ctx.setLineDash([]); } ctx.fillStyle = "rgba(216,207,182,.4)"; ctx.font = "italic 12px Georgia, serif"; ctx.textAlign = "right"; ctx.fillText(`${model().name} ยท ${pot().name}`, W - 14, H - 14); ctx.textAlign = "left"; } const SCAN_D = 3.4, SCAN_LEAD = .3, BW = 96, IDLE_P = 18, IDLE_0 = 8; const IDLE_W = ["sunset", "clouds", "sun", "rain", "clear", "storm", "sunset", "sun"]; const idleState = i => ({ s: [2, 3, 0, 1][Math.floor(i / 2) % 4], w: IDLE_W[i % IDLE_W.length] }); const SONG_P = 16, SONG_W = ["sun", "clouds", "sunset", "rain", "clear", "storm", "sun", "clear", "clouds", "sunset"]; function songScenes() { if (AN._sc) return AN._sc; const dur = (SONG && SONG.dur) || 1, n = Math.max(1, Math.ceil(dur / SONG_P)), out = []; let prev = null; for (let k = 0; k < n; k++) { const a = k * SONG_P, s = Math.min(3, Math.floor(a / dur * 4)); let w = a / dur > .82 ? "sunset" : SONG_W[k % SONG_W.length]; if (prev && prev.s === s && prev.w === w) w = SONG_W[(k + 1) % SONG_W.length]; const st = { s, w }; if (!prev) out.push({ t0: -1e9, st, id: 0 }); else if (prev.s !== s || prev.w !== w) out.push({ t0: Math.max(0, a - SCAN_LEAD), st, id: k }); prev = st; } return (AN._sc = out); } function sceneNow(clock) { if (SONG && AN) { const sc = songScenes(); let k = 0; for (let i = 1; i < sc.length; i++) { if (sc[i].t0 <= clock) k = i; else break; } const p = (clock - sc[k].t0) / SCAN_D; return k > 0 && p < 1 ? { st: sc[k].st, from: sc[k - 1].st, p, id: sc[k].id } : { st: sc[k].st }; } const i = clock < IDLE_0 ? 0 : 1 + Math.floor((clock - IDLE_0) / IDLE_P), p = (clock - IDLE_0 - (i - 1) * IDLE_P) / SCAN_D; return i > 0 && p < 1 ? { st: idleState(i), from: idleState(i - 1), p, id: -i } : { st: idleState(i) }; } function applyState(st, t) { const w = WEATHERS[st.w], live = !!(SONG && AN), m = live ? at(AN.St, t) : 0; SEA = SEASONS[st.s]; const storm = st.w === "storm";