1/** 2 * Ambient birds â client-side DOM layer driven by server spawn/flee events. 3 * 4 * Motion is CSS-only; this module never runs in the animation frame loop. 5 */ 6 7import { MAX_X, MIN_X } from "./constants.mjs"; 8 9const BIRD_FLYING_SVG = ` 10 <svg viewBox="0 0 12 8" preserveAspectRatio="xMidYMid meet" aria-hidden="true"> 11 <path class="bird-wing bird-wing--left" d="M6 2 L1 6"></path> 12 <path class="bird-wing bird-wing--right" d="M6 2 L11 6"></path> 13 </svg> 14`; 15 16const BIRD_PERCHED_SVG = ` 17 <svg viewBox="0 0 18 11" preserveAspectRatio="xMidYMax meet" aria-hidden="true"> 18 <path d="M2.2 5.6 L0.8 4.8 L1 6.5"></path> 19 <path d="M2.5 5.8 C2.2 3.8 4.5 2.6 7.2 2.8 C9.8 2.5 12 3.8 12.2 5.6 C12.3 7.2 10.8 8.4 8.4 8.4 C6.2 8.4 2.8 7.4 2.5 5.8"></path> 20 <path d="M4.8 4.2 Q7.2 5.4 6.2 7.2"></path> 21 <circle cx="11.8" cy="4.2" r="1.15"></circle> 22 <line x1="12.8" y1="4.1" x2="15.2" y2="3.8"></line> 23 <line x1="6.8" y1="8.4" x2="6.3" y2="11"></line> 24 <line x1="9.2" y1="8.4" x2="9.7" y2="11"></line> 25 </svg> 26`; 27 28/** 29 * @param {BirdView} bird 30 */ 31function setBirdFlyingArt(bird) { 32 bird.el.innerHTML = BIRD_FLYING_SVG; 33} 34 35/** 36 * @param {BirdView} bird 37 */ 38function setBirdPerchedArt(bird) { 39 bird.el.innerHTML = BIRD_PERCHED_SVG; 40} 41 42/** 43 * @typedef {Object} BirdView 44 * @property {HTMLElement} el 45 * @property {number} x 46 * @property {string} perchId 47 */ 48 49/** 50 * @typedef {import("./context.mjs").WidgetContext & { birds?: Map<number, BirdView>, birdLayer?: HTMLElement }} BirdsContext 51 */ 52 53/** 54 * @param {HTMLElement} stage 55 * @returns {HTMLElement} 56 */ 57export function mountBirdLayer(stage) { 58 const layer = document.createElement("div"); 59 layer.className = "townsquare__birds"; 60 layer.setAttribute("aria-hidden", "true"); 61 stage.appendChild(layer); 62 return layer; 63} 64 65/** 66 * @param {BirdsContext} ctx 67 */ 68export function initBirds(ctx) { 69 ctx.birds = new Map(); 70 ctx.birdLayer = mountBirdLayer(ctx.stage); 71} 72 73/** 74 * @param {BirdsContext} ctx 75 * @param {string} perchId 76 * @returns {{ x: number, liftPx: number } | null} 77 */ 78function perchLayout(ctx, perchId) { 79 const perch = ctx.birdPerchesById?.get(perchId); 80 if (!perch) return null; 81 return { x: perch.x, liftPx: perch.liftPx }; 82} 83 84/** 85 * @param {number} id 86 * @param {number} x 87 * @param {number} liftPx 88 * @returns {BirdView} 89 */ 90function createBirdElement(id, x, liftPx) { 91 const el = document.createElement("div"); 92 el.className = "bird"; 93 el.dataset.birdId = String(id); 94 el.style.setProperty("--bird-x", String(x)); 95 el.style.setProperty("--bird-lift", `${liftPx}px`); 96 setBirdFlyingArt({ el }); 97 return { el, x, perchId: "" }; 98} 99 100/** 101 * @param {BirdView} bird 102 * @param {string} perchId 103 */ 104function setBirdPerch(ctx, bird, perchId) { 105 bird.perchId = perchId; 106 const layout = perchLayout(ctx, perchId); 107 if (!layout) return; 108 bird.x = layout.x; 109 bird.el.style.setProperty("--bird-x", String(layout.x)); 110 bird.el.style.setProperty("--bird-lift", `${layout.liftPx}px`); 111} 112 113/** 114 * @param {BirdView} bird 115 */ 116function removeBirdElement(bird) { 117 bird.el.remove(); 118} 119 120/** 121 * @param {"left" | "right"} from 122 * @returns {number} 123 */ 124function entryX(from) { 125 return from === "left" ? MIN_X - 0.06 : MAX_X + 0.06; 126} 127 128/** 129 * @param {BirdsContext} ctx 130 * @param {number} id 131 * @param {string} perchId 132 * @param {number} x 133 * @param {"left" | "right"} from 134 */ 135function upsertArrivingBird(ctx, id, perchId, x, from) { 136 if (!ctx.birdLayer || !ctx.birds) return; 137 138 const layout = perchLayout(ctx, perchId); 139 if (!layout) return; 140 141 let bird = ctx.birds.get(id); 142 if (!bird) { 143 bird = createBirdElement(id, x, layout.liftPx); 144 ctx.birds.set(id, bird); 145 ctx.birdLayer.appendChild(bird.el); 146 } else { 147 setBirdPerch(ctx, bird, perchId); 148 } 149 150 bird.perchId = perchId; 151 setBirdFlyingArt(bird); 152 bird.el.className = "bird bird--arriving"; 153 bird.el.style.setProperty("--bird-from", String(entryX(from))); 154 bird.el.style.setProperty("--bird-to", String(x)); 155 bird.el.style.setProperty("--bird-facing", from === "left" ? "1" : "-1"); 156 157 const onArrive = (event) => { 158 if (event.animationName !== "bird-arrive") return; 159 bird.el.removeEventListener("animationend", onArrive); 160 setBirdPerchedArt(bird); 161 bird.el.className = "bird bird--perched"; 162 }; 163 bird.el.addEventListener("animationend", onArrive); 164} 165 166/** 167 * @param {BirdsContext} ctx 168 * @param {number} id 169 * @param {string} perchId 170 * @param {number} x 171 */ 172function upsertPerchedBird(ctx, id, perchId, x) { 173 if (!ctx.birdLayer || !ctx.birds) return; 174 175 const layout = perchLayout(ctx, perchId); 176 if (!layout) return; 177 178 let bird = ctx.birds.get(id); 179 if (!bird) { 180 bird = createBirdElement(id, x, layout.liftPx);
181 bird.perchId = perchId; 182 setBirdPerchedArt(bird); 183 bird.el.className = "bird bird--perched"; 184 ctx.birds.set(id, bird); 185 ctx.birdLayer.appendChild(bird.el); 186 return; 187 } 188 189 setBirdPerch(ctx, bird, perchId); 190 setBirdPerchedArt(bird); 191 bird.el.className = "bird bird--perched"; 192} 193 194/** 195 * @param {BirdsContext} ctx 196 * @param {Array<{ id: number, perchId: string, x: number, state?: string }>} birds 197 */ 198export function syncBirdsFromHello(ctx, birds) { 199 if (!ctx.birds || !ctx.birdLayer) return; 200 201 for (const bird of birds || []) { 202 if (typeof bird.id !== "number" || typeof bird.perchId !== "string") continue; 203 upsertPerchedBird(ctx, bird.id, bird.perchId, bird.x); 204 } 205} 206 207/** 208 * @param {BirdsContext} ctx 209 * @param {{ action: string, id: number, perchId: string, x: number, from?: "left" | "right" }} message 210 */ 211export function applyBirdSpawn(ctx, message) { 212 if (message.action !== "spawn") return; 213 upsertArrivingBird(ctx, message.id, message.perchId, message.x, message.from || "left"); 214} 215 216/** 217 * @param {BirdsContext} ctx 218 * @param {{ action: string, id: number, x: number, dir: number }} message 219 */ 220export function applyBirdFlee(ctx, message) { 221 if (message.action !== "flee" || !ctx.birds) return; 222 223 const bird = ctx.birds.get(message.id); 224 if (!bird) return; 225 226 bird.x = message.x; 227 bird.el.style.setProperty("--bird-x", String(message.x)); 228 bird.el.style.setProperty("--bird-dir", String(message.dir < 0 ? -1 : 1)); 229 bird.el.style.setProperty("--bird-facing", message.dir < 0 ? "-1" : "1"); 230 setBirdFlyingArt(bird); 231 bird.el.className = "bird bird--fleeing"; 232 233 const onFlee = (event) => { 234 if (event.animationName !== "bird-flee") return; 235 bird.el.removeEventListener("animationend", onFlee); 236 ctx.birds?.delete(message.id); 237 removeBirdElement(bird); 238 }; 239 bird.el.addEventListener("animationend", onFlee); 240} 241 242/** 243 * @param {BirdsContext} ctx 244 */ 245export function destroyBirds(ctx) { 246 if (ctx.birds) { 247 for (const bird of ctx.birds.values()) { 248 removeBirdElement(bird); 249 } 250 ctx.birds.clear(); 251 } 252 ctx.birdLayer?.remove(); 253 ctx.birdLayer = undefined; 254}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.