PageSourceSearch

https://n8k99.com/static/townsquare/widget/birds.mjs

js n8k99.com collected 2026-10-06 18:22:45 UTC 6,825 bytes, 254 lines download raw bytes

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.