|
|
|
@@ -1,118 +1,84 @@
|
|
|
|
import { useEffect, useRef } from 'react'
|
|
|
|
import { useEffect, useRef } from 'react'
|
|
|
|
|
|
|
|
|
|
|
|
// PetalFall is the cozy ambient layer: a full-window WebGL canvas that drifts a
|
|
|
|
// PetalFall is the cozy ambient layer: a fixed, non-interactive 2D canvas that
|
|
|
|
// handful of soft flower petals down the page. It sits behind everything,
|
|
|
|
// drifts soft cherry-blossom petals down the page. Each petal is a real
|
|
|
|
// ignores pointer events, and stays sparse so it reads as "occasional petals on
|
|
|
|
// petal-shaped sprite (notched sakura silhouette with a gentle gradient and
|
|
|
|
// a breeze" rather than confetti. Honors prefers-reduced-motion by rendering
|
|
|
|
// soft alpha), pre-rendered once per color and then blitted many times with
|
|
|
|
// nothing. No external libraries — a single tiny shader program animates every
|
|
|
|
// rotation + sway. It sits behind the chrome, ignores pointer events, pauses
|
|
|
|
// petal entirely on the GPU, so the CPU just spins a clock.
|
|
|
|
// when the tab is hidden, and renders nothing for prefers-reduced-motion.
|
|
|
|
|
|
|
|
|
|
|
|
// Each petal is a quad whose corners we expand in the vertex shader. We pack one
|
|
|
|
// Saturated-but-soft blossom colors. Each petal fades from a light, near-white
|
|
|
|
// quad as 6 vertices; per-vertex we send the corner offset (which corner) plus
|
|
|
|
// tip to its color at the base, so it reads as a real petal rather than a blob.
|
|
|
|
// the petal's static randomness (lane, speed, size, sway, tint, spin).
|
|
|
|
const PALETTE: { r: number; g: number; b: number }[] = [
|
|
|
|
const PETAL_COUNT = 26
|
|
|
|
{ r: 255, g: 150, b: 190 }, // rose pink
|
|
|
|
|
|
|
|
{ r: 255, g: 184, b: 205 }, // pale blossom
|
|
|
|
// Soft Petal-palette tints (rose / blush / lavender), as RGB 0..1.
|
|
|
|
{ r: 255, g: 158, b: 130 }, // warm peach
|
|
|
|
const TINTS: [number, number, number][] = [
|
|
|
|
{ r: 209, g: 160, b: 255 }, // lavender
|
|
|
|
[0.91, 0.63, 0.75], // accent rose #E8A0BF
|
|
|
|
{ r: 255, g: 122, b: 172 }, // deep rose
|
|
|
|
[1.0, 0.94, 0.96], // near-white blush
|
|
|
|
|
|
|
|
[0.96, 0.72, 0.63], // peach #F4B8A0
|
|
|
|
|
|
|
|
[0.77, 0.71, 0.91], // lavender #C5B4E8
|
|
|
|
|
|
|
|
[0.85, 0.55, 0.69], // deeper rose
|
|
|
|
|
|
|
|
]
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
const VERT = `
|
|
|
|
const SPRITE_PX = 160 // off-screen render resolution per petal sprite
|
|
|
|
precision mediump float;
|
|
|
|
|
|
|
|
attribute vec2 a_corner; // quad corner in [-0.5,0.5]
|
|
|
|
|
|
|
|
attribute vec4 a_rand; // x: lane(0..1), y: speed, z: size, w: phase
|
|
|
|
|
|
|
|
attribute vec4 a_rand2; // x: swayAmp, y: swayFreq, z: spin, w: tintIdx
|
|
|
|
|
|
|
|
uniform float u_time;
|
|
|
|
|
|
|
|
uniform vec2 u_res;
|
|
|
|
|
|
|
|
varying vec2 v_uv;
|
|
|
|
|
|
|
|
varying float v_tint;
|
|
|
|
|
|
|
|
varying float v_alpha;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
function rgba(c: { r: number; g: number; b: number }, a: number): string {
|
|
|
|
v_uv = a_corner + 0.5;
|
|
|
|
return `rgba(${Math.round(c.r)}, ${Math.round(c.g)}, ${Math.round(c.b)}, ${a})`
|
|
|
|
v_tint = a_rand2.w;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
float lane = a_rand.x;
|
|
|
|
|
|
|
|
float speed = a_rand.y;
|
|
|
|
|
|
|
|
float size = a_rand.z;
|
|
|
|
|
|
|
|
float phase = a_rand.w;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Vertical fall: loop 0..1 over time, offset per petal so they don't sync.
|
|
|
|
|
|
|
|
float fall = fract(u_time * speed + phase);
|
|
|
|
|
|
|
|
// Map to clip space: start above the top (1.15) fall to below bottom (-1.15).
|
|
|
|
|
|
|
|
float y = 1.15 - fall * 2.3;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Horizontal: a base lane plus a gentle sway as it descends.
|
|
|
|
|
|
|
|
float sway = a_rand2.x * sin(u_time * a_rand2.y + phase * 6.2831);
|
|
|
|
|
|
|
|
float x = (lane * 2.0 - 1.0) + sway;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Spin the quad over time.
|
|
|
|
|
|
|
|
float ang = u_time * a_rand2.z + phase * 6.2831;
|
|
|
|
|
|
|
|
float c = cos(ang), s = sin(ang);
|
|
|
|
|
|
|
|
vec2 corner = a_corner * size;
|
|
|
|
|
|
|
|
// Keep petals visually square despite aspect ratio.
|
|
|
|
|
|
|
|
corner.x *= u_res.y / u_res.x;
|
|
|
|
|
|
|
|
vec2 rot = vec2(corner.x * c - corner.y * s, corner.x * s + corner.y * c);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Fade in near the top and out near the bottom so they don't pop.
|
|
|
|
|
|
|
|
v_alpha = smoothstep(0.0, 0.12, fall) * (1.0 - smoothstep(0.86, 1.0, fall));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
gl_Position = vec4(x + rot.x, y + rot.y, 0.0, 1.0);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`
|
|
|
|
function mixWhite(c: { r: number; g: number; b: number }, t: number) {
|
|
|
|
|
|
|
|
return { r: c.r + (255 - c.r) * t, g: c.g + (255 - c.g) * t, b: c.b + (255 - c.b) * t }
|
|
|
|
const FRAG = `
|
|
|
|
}
|
|
|
|
precision mediump float;
|
|
|
|
function scale(c: { r: number; g: number; b: number }, t: number) {
|
|
|
|
varying vec2 v_uv;
|
|
|
|
return { r: c.r * t, g: c.g * t, b: c.b * t }
|
|
|
|
varying float v_tint;
|
|
|
|
|
|
|
|
varying float v_alpha;
|
|
|
|
|
|
|
|
uniform vec3 u_tints[5];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// A soft petal/teardrop alpha mask centered in the quad.
|
|
|
|
|
|
|
|
float petalMask(vec2 uv) {
|
|
|
|
|
|
|
|
vec2 p = uv - vec2(0.5);
|
|
|
|
|
|
|
|
// Teardrop: pinch the top, round the bottom.
|
|
|
|
|
|
|
|
float r = length(vec2(p.x * 1.65, p.y * 1.15 + 0.12));
|
|
|
|
|
|
|
|
float body = smoothstep(0.5, 0.16, r);
|
|
|
|
|
|
|
|
// A subtle crease down the middle for a petal feel.
|
|
|
|
|
|
|
|
float crease = 1.0 - 0.18 * exp(-pow(p.x * 7.0, 2.0));
|
|
|
|
|
|
|
|
return body * crease;
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
// Draw one petal sprite: a notched sakura silhouette filled with a tip→base
|
|
|
|
float m = petalMask(v_uv);
|
|
|
|
// gradient and a faint central highlight, on a transparent canvas.
|
|
|
|
if (m < 0.01) discard;
|
|
|
|
function makeSprite(color: { r: number; g: number; b: number }): HTMLCanvasElement {
|
|
|
|
int idx = int(v_tint + 0.5);
|
|
|
|
const s = SPRITE_PX
|
|
|
|
vec3 col = u_tints[0];
|
|
|
|
const cv = document.createElement('canvas')
|
|
|
|
if (idx == 1) col = u_tints[1];
|
|
|
|
cv.width = s
|
|
|
|
else if (idx == 2) col = u_tints[2];
|
|
|
|
cv.height = s
|
|
|
|
else if (idx == 3) col = u_tints[3];
|
|
|
|
const g = cv.getContext('2d')!
|
|
|
|
else if (idx == 4) col = u_tints[4];
|
|
|
|
g.translate(s / 2, s / 2)
|
|
|
|
gl_FragColor = vec4(col, m * v_alpha * 0.55);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function compile(gl: WebGLRenderingContext, type: number, src: string): WebGLShader | null {
|
|
|
|
const hw = s * 0.3 // half width
|
|
|
|
const sh = gl.createShader(type)
|
|
|
|
const hh = s * 0.44 // half height
|
|
|
|
if (!sh) return null
|
|
|
|
|
|
|
|
gl.shaderSource(sh, src)
|
|
|
|
g.beginPath()
|
|
|
|
gl.compileShader(sh)
|
|
|
|
g.moveTo(0, hh) // pointed base
|
|
|
|
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
|
|
|
|
g.bezierCurveTo(hw, hh * 0.42, hw * 0.92, -hh * 0.52, hw * 0.2, -hh * 0.92) // right side → near tip
|
|
|
|
console.error('petal shader compile failed', gl.getShaderInfoLog(sh))
|
|
|
|
g.quadraticCurveTo(0, -hh * 0.72, -hw * 0.2, -hh * 0.92) // the sakura notch
|
|
|
|
gl.deleteShader(sh)
|
|
|
|
g.bezierCurveTo(-hw * 0.92, -hh * 0.52, -hw, hh * 0.42, 0, hh) // left side → base
|
|
|
|
return null
|
|
|
|
g.closePath()
|
|
|
|
}
|
|
|
|
|
|
|
|
return sh
|
|
|
|
const grad = g.createLinearGradient(0, -hh, 0, hh)
|
|
|
|
|
|
|
|
grad.addColorStop(0, rgba(mixWhite(color, 0.55), 0.78)) // light, soft tip
|
|
|
|
|
|
|
|
grad.addColorStop(0.45, rgba(color, 0.88))
|
|
|
|
|
|
|
|
grad.addColorStop(1, rgba(scale(color, 0.8), 0.95)) // deeper base
|
|
|
|
|
|
|
|
g.fillStyle = grad
|
|
|
|
|
|
|
|
g.fill()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// A soft white sheen down the centre for a delicate, glossy feel.
|
|
|
|
|
|
|
|
const sheen = g.createLinearGradient(-hw * 0.3, 0, hw * 0.3, 0)
|
|
|
|
|
|
|
|
sheen.addColorStop(0, 'rgba(255,255,255,0)')
|
|
|
|
|
|
|
|
sheen.addColorStop(0.5, 'rgba(255,255,255,0.28)')
|
|
|
|
|
|
|
|
sheen.addColorStop(1, 'rgba(255,255,255,0)')
|
|
|
|
|
|
|
|
g.fillStyle = sheen
|
|
|
|
|
|
|
|
g.fill()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return cv
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Deterministic-ish pseudo random (we can't use Math.random at module scope for
|
|
|
|
interface Petal {
|
|
|
|
// SSR safety, but here in an effect it's fine; kept simple).
|
|
|
|
baseX: number
|
|
|
|
function rand(): number {
|
|
|
|
y: number
|
|
|
|
return Math.random()
|
|
|
|
size: number
|
|
|
|
|
|
|
|
vy: number // fall speed (px/s)
|
|
|
|
|
|
|
|
angle: number
|
|
|
|
|
|
|
|
spin: number // rad/s
|
|
|
|
|
|
|
|
swayAmp: number
|
|
|
|
|
|
|
|
swayFreq: number
|
|
|
|
|
|
|
|
swayPhase: number
|
|
|
|
|
|
|
|
alpha: number
|
|
|
|
|
|
|
|
sprite: number
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function PetalFall() {
|
|
|
|
export function PetalFall() {
|
|
|
|
@@ -124,120 +90,90 @@ export function PetalFall() {
|
|
|
|
window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
|
|
|
|
window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
|
|
|
|
if (reduce) return // respect users who don't want ambient motion
|
|
|
|
if (reduce) return // respect users who don't want ambient motion
|
|
|
|
|
|
|
|
|
|
|
|
const canvas = canvasRef.current
|
|
|
|
const el = canvasRef.current
|
|
|
|
if (!canvas) return
|
|
|
|
if (!el) return
|
|
|
|
const gl = canvas.getContext('webgl', { alpha: true, premultipliedAlpha: false, antialias: true })
|
|
|
|
const c2d = el.getContext('2d')
|
|
|
|
if (!gl) return // no WebGL — silently skip the effect
|
|
|
|
if (!c2d) return
|
|
|
|
|
|
|
|
|
|
|
|
const vs = compile(gl, gl.VERTEX_SHADER, VERT)
|
|
|
|
|
|
|
|
const fs = compile(gl, gl.FRAGMENT_SHADER, FRAG)
|
|
|
|
|
|
|
|
if (!vs || !fs) return
|
|
|
|
|
|
|
|
const prog = gl.createProgram()!
|
|
|
|
|
|
|
|
gl.attachShader(prog, vs)
|
|
|
|
|
|
|
|
gl.attachShader(prog, fs)
|
|
|
|
|
|
|
|
gl.linkProgram(prog)
|
|
|
|
|
|
|
|
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
|
|
|
|
|
|
|
|
console.error('petal program link failed', gl.getProgramInfoLog(prog))
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
gl.useProgram(prog)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Build interleaved vertex data: 6 verts per petal, each with corner(2),
|
|
|
|
|
|
|
|
// rand(4), rand2(4) = 10 floats.
|
|
|
|
|
|
|
|
const FLOATS_PER_VERT = 10
|
|
|
|
|
|
|
|
const corners = [
|
|
|
|
|
|
|
|
[-0.5, -0.5],
|
|
|
|
|
|
|
|
[0.5, -0.5],
|
|
|
|
|
|
|
|
[-0.5, 0.5],
|
|
|
|
|
|
|
|
[-0.5, 0.5],
|
|
|
|
|
|
|
|
[0.5, -0.5],
|
|
|
|
|
|
|
|
[0.5, 0.5],
|
|
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
const data = new Float32Array(PETAL_COUNT * 6 * FLOATS_PER_VERT)
|
|
|
|
|
|
|
|
let o = 0
|
|
|
|
|
|
|
|
for (let i = 0; i < PETAL_COUNT; i++) {
|
|
|
|
|
|
|
|
const lane = rand()
|
|
|
|
|
|
|
|
const speed = 0.018 + rand() * 0.03 // slow, gentle fall
|
|
|
|
|
|
|
|
const size = 0.05 + rand() * 0.06
|
|
|
|
|
|
|
|
const phase = rand()
|
|
|
|
|
|
|
|
const swayAmp = 0.04 + rand() * 0.09
|
|
|
|
|
|
|
|
const swayFreq = 0.4 + rand() * 0.7
|
|
|
|
|
|
|
|
const spin = (rand() - 0.5) * 1.2
|
|
|
|
|
|
|
|
const tint = Math.floor(rand() * TINTS.length)
|
|
|
|
|
|
|
|
for (let c = 0; c < 6; c++) {
|
|
|
|
|
|
|
|
data[o++] = corners[c][0]
|
|
|
|
|
|
|
|
data[o++] = corners[c][1]
|
|
|
|
|
|
|
|
data[o++] = lane
|
|
|
|
|
|
|
|
data[o++] = speed
|
|
|
|
|
|
|
|
data[o++] = size
|
|
|
|
|
|
|
|
data[o++] = phase
|
|
|
|
|
|
|
|
data[o++] = swayAmp
|
|
|
|
|
|
|
|
data[o++] = swayFreq
|
|
|
|
|
|
|
|
data[o++] = spin
|
|
|
|
|
|
|
|
data[o++] = tint
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const buf = gl.createBuffer()
|
|
|
|
|
|
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, buf)
|
|
|
|
|
|
|
|
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const stride = FLOATS_PER_VERT * 4
|
|
|
|
|
|
|
|
const aCorner = gl.getAttribLocation(prog, 'a_corner')
|
|
|
|
|
|
|
|
const aRand = gl.getAttribLocation(prog, 'a_rand')
|
|
|
|
|
|
|
|
const aRand2 = gl.getAttribLocation(prog, 'a_rand2')
|
|
|
|
|
|
|
|
gl.enableVertexAttribArray(aCorner)
|
|
|
|
|
|
|
|
gl.vertexAttribPointer(aCorner, 2, gl.FLOAT, false, stride, 0)
|
|
|
|
|
|
|
|
gl.enableVertexAttribArray(aRand)
|
|
|
|
|
|
|
|
gl.vertexAttribPointer(aRand, 4, gl.FLOAT, false, stride, 2 * 4)
|
|
|
|
|
|
|
|
gl.enableVertexAttribArray(aRand2)
|
|
|
|
|
|
|
|
gl.vertexAttribPointer(aRand2, 4, gl.FLOAT, false, stride, 6 * 4)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const uTime = gl.getUniformLocation(prog, 'u_time')
|
|
|
|
|
|
|
|
const uRes = gl.getUniformLocation(prog, 'u_res')
|
|
|
|
|
|
|
|
const uTints = gl.getUniformLocation(prog, 'u_tints')
|
|
|
|
|
|
|
|
gl.uniform3fv(uTints, TINTS.flat())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
gl.enable(gl.BLEND)
|
|
|
|
|
|
|
|
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
|
|
|
|
|
|
|
|
gl.disable(gl.DEPTH_TEST)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Non-null aliases so the render closures keep the narrowed types.
|
|
|
|
// Non-null aliases so the render closures keep the narrowed types.
|
|
|
|
const view = canvas
|
|
|
|
const canvas = el
|
|
|
|
const glc = gl
|
|
|
|
const ctx = c2d
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const sprites = PALETTE.map(makeSprite)
|
|
|
|
const dpr = Math.min(window.devicePixelRatio || 1, 2)
|
|
|
|
const dpr = Math.min(window.devicePixelRatio || 1, 2)
|
|
|
|
|
|
|
|
let W = window.innerWidth
|
|
|
|
|
|
|
|
let H = window.innerHeight
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Scale petal count to the viewport so it stays sparse and gentle.
|
|
|
|
|
|
|
|
const count = Math.round(Math.min(46, Math.max(16, (W * H) / 46000)))
|
|
|
|
|
|
|
|
const rnd = (a: number, b: number) => a + Math.random() * (b - a)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const spawn = (initial: boolean): Petal => ({
|
|
|
|
|
|
|
|
baseX: rnd(0, W),
|
|
|
|
|
|
|
|
y: initial ? rnd(-H, H) : rnd(-60, -20),
|
|
|
|
|
|
|
|
size: rnd(16, 38),
|
|
|
|
|
|
|
|
vy: rnd(22, 52),
|
|
|
|
|
|
|
|
angle: rnd(0, Math.PI * 2),
|
|
|
|
|
|
|
|
spin: rnd(-0.9, 0.9),
|
|
|
|
|
|
|
|
swayAmp: rnd(14, 42),
|
|
|
|
|
|
|
|
swayFreq: rnd(0.3, 0.85),
|
|
|
|
|
|
|
|
swayPhase: rnd(0, Math.PI * 2),
|
|
|
|
|
|
|
|
alpha: rnd(0.5, 0.82),
|
|
|
|
|
|
|
|
sprite: Math.floor(Math.random() * sprites.length),
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let petals = Array.from({ length: count }, () => spawn(true))
|
|
|
|
|
|
|
|
|
|
|
|
function resize() {
|
|
|
|
function resize() {
|
|
|
|
const w = Math.floor(window.innerWidth * dpr)
|
|
|
|
W = window.innerWidth
|
|
|
|
const h = Math.floor(window.innerHeight * dpr)
|
|
|
|
H = window.innerHeight
|
|
|
|
if (view.width !== w || view.height !== h) {
|
|
|
|
canvas.width = Math.floor(W * dpr)
|
|
|
|
view.width = w
|
|
|
|
canvas.height = Math.floor(H * dpr)
|
|
|
|
view.height = h
|
|
|
|
canvas.style.width = `${W}px`
|
|
|
|
}
|
|
|
|
canvas.style.height = `${H}px`
|
|
|
|
glc.viewport(0, 0, view.width, view.height)
|
|
|
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
|
|
|
glc.uniform2f(uRes, view.width, view.height)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
resize()
|
|
|
|
resize()
|
|
|
|
window.addEventListener('resize', resize)
|
|
|
|
window.addEventListener('resize', resize)
|
|
|
|
|
|
|
|
|
|
|
|
let raf = 0
|
|
|
|
let raf = 0
|
|
|
|
let running = true
|
|
|
|
let running = true
|
|
|
|
const start = performance.now()
|
|
|
|
let last = performance.now()
|
|
|
|
function frame() {
|
|
|
|
|
|
|
|
|
|
|
|
function frame(now: number) {
|
|
|
|
if (!running) return
|
|
|
|
if (!running) return
|
|
|
|
const t = (performance.now() - start) / 1000
|
|
|
|
const dt = Math.min(0.05, (now - last) / 1000) // clamp big gaps (tab wake)
|
|
|
|
glc.clearColor(0, 0, 0, 0)
|
|
|
|
last = now
|
|
|
|
glc.clear(glc.COLOR_BUFFER_BIT)
|
|
|
|
const t = now / 1000
|
|
|
|
glc.uniform1f(uTime, t)
|
|
|
|
ctx.clearRect(0, 0, W, H)
|
|
|
|
glc.drawArrays(glc.TRIANGLES, 0, PETAL_COUNT * 6)
|
|
|
|
|
|
|
|
|
|
|
|
for (const p of petals) {
|
|
|
|
|
|
|
|
p.y += p.vy * dt
|
|
|
|
|
|
|
|
p.angle += p.spin * dt
|
|
|
|
|
|
|
|
const x = p.baseX + p.swayAmp * Math.sin(t * p.swayFreq + p.swayPhase)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Recycle once fully past the bottom.
|
|
|
|
|
|
|
|
if (p.y - p.size > H) {
|
|
|
|
|
|
|
|
Object.assign(p, spawn(false))
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ctx.save()
|
|
|
|
|
|
|
|
ctx.translate(x, p.y)
|
|
|
|
|
|
|
|
ctx.rotate(p.angle)
|
|
|
|
|
|
|
|
ctx.globalAlpha = p.alpha
|
|
|
|
|
|
|
|
ctx.drawImage(sprites[p.sprite], -p.size / 2, -p.size / 2, p.size, p.size)
|
|
|
|
|
|
|
|
ctx.restore()
|
|
|
|
|
|
|
|
}
|
|
|
|
raf = requestAnimationFrame(frame)
|
|
|
|
raf = requestAnimationFrame(frame)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Pause when the tab is hidden to save the battery.
|
|
|
|
|
|
|
|
const onVisibility = () => {
|
|
|
|
const onVisibility = () => {
|
|
|
|
if (document.hidden) {
|
|
|
|
if (document.hidden) {
|
|
|
|
running = false
|
|
|
|
running = false
|
|
|
|
cancelAnimationFrame(raf)
|
|
|
|
cancelAnimationFrame(raf)
|
|
|
|
} else if (!running) {
|
|
|
|
} else if (!running) {
|
|
|
|
running = true
|
|
|
|
running = true
|
|
|
|
|
|
|
|
last = performance.now()
|
|
|
|
raf = requestAnimationFrame(frame)
|
|
|
|
raf = requestAnimationFrame(frame)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -249,10 +185,7 @@ export function PetalFall() {
|
|
|
|
cancelAnimationFrame(raf)
|
|
|
|
cancelAnimationFrame(raf)
|
|
|
|
window.removeEventListener('resize', resize)
|
|
|
|
window.removeEventListener('resize', resize)
|
|
|
|
document.removeEventListener('visibilitychange', onVisibility)
|
|
|
|
document.removeEventListener('visibilitychange', onVisibility)
|
|
|
|
gl.deleteBuffer(buf)
|
|
|
|
petals = []
|
|
|
|
gl.deleteProgram(prog)
|
|
|
|
|
|
|
|
gl.deleteShader(vs)
|
|
|
|
|
|
|
|
gl.deleteShader(fs)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, [])
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
|
|
|