PageSourceSearch

https://piratesbargain.com/assets/sprite_showcase.js?m=1786229593.0

js piratesbargain.com collected 2026-09-29 08:54:07 UTC 11,981 bytes, 353 lines download raw bytes

1// Sprite showcase — a self-contained animated sprite that walks around a
2// bordered box on a /product/<slug> page for a SPRITE_PACK listing.
3//
4// The page is WS-served, so the showcase box appears in the DOM *after* the
5// renderer has loaded its scripts. This module follows the same self-init
6// pattern as assets/cart_persist.js and the FlexLayout map observer in
7// assets/js/tileset_clientside.js: a global `_initialized` guard plus a
8// MutationObserver that scans for `.sprite-showcase[data-frames]` containers
9// added to the tree, initializing each box exactly once (a per-element dataset
10// flag).
11//
12// The server renders:
13//   <div class="sprite-showcase"
14//        data-frames='{"walking/south/000":"/sprites/<slug>/walking/south/000.png", ...}'
15//        data-base="/sprites/<slug>">
16// where data-frames is the EXACT frames_for() map — so the JS only ever paints
17// frames that EXIST (no 404 probing).
18//
19// ANTI-COPY: the sprite is a CSS background-image on a <div> (never an <img>),
20// the box disables the context menu, drag, and selection, and the random walk
21// keeps the sprite moving — so a right-click "Save image" or a drag grabs
22// nothing useful and a clean screenshot is hard to land.
23//
24// prefers-reduced-motion: collapse to a single centered static idle frame; no
25// movement, no animation loop.
26(function () {
27  if (window._spriteShowcaseInitialized) return;
28  window._spriteShowcaseInitialized = true;
29
30  var REDUCED_MOTION =
31    window.matchMedia &&
32    window.matchMedia("(prefers-reduced-motion: reduce)").matches;
33
34  // Canonical compass directions, in clockwise order starting at north, with
35  // their unit movement vectors (y grows downward in screen space).
36  var DIRS = [
37    { name: "north", dx: 0, dy: -1 },
38    { name: "northeast", dx: 1, dy: -1 },
39    { name: "east", dx: 1, dy: 0 },
40    { name: "southeast", dx: 1, dy: 1 },
41    { name: "south", dx: 0, dy: 1 },
42    { name: "southwest", dx: -1, dy: 1 },
43    { name: "west", dx: -1, dy: 0 },
44    { name: "northwest", dx: -1, dy: -1 },
45  ];
46
47  var SPRITE_PX = 64; // on-screen sprite footprint
48  var FPS = 8; // animation cadence
49  var SPEED = 0.7; // px per ms — gentle stroll
50
51  // Parse the frames_for map into { anim: { dir: [url, url, ...] } }, with
52  // each direction's frames sorted by zero-padded index.
53  function parseFrames(raw) {
54    var map = {};
55    var data;
56    try {
57      data = JSON.parse(raw);
58    } catch (e) {
59      return map;
60    }
61    var indexByKey = {};
62    Object.keys(data || {}).forEach(function (key) {
63      // key shape: "{anim}/{dir}/{idx:03d}"
64      var parts = key.split("/");
65      if (parts.length !== 3) return;
66      var anim = parts[0];
67      var dir = parts[1];
68      var idx = parseInt(parts[2], 10);
69      if (isNaN(idx)) return;
70      map[anim] = map[anim] || {};
71      map[anim][dir] = map[anim][dir] || [];
72      var entry = { idx: idx, url: data[key] };
73      map[anim][dir].push(entry);
74      indexByKey[anim + "/" + dir] = true;
75    });
76    // Sort each direction's frames by index and flatten to a url list.
77    Object.keys(map).forEach(function (anim) {
78      Object.keys(map[anim]).forEach(function (dir) {
79        map[anim][dir].sort(function (a, b) {
80          return a.idx - b.idx;
81        });
82        map[anim][dir] = map[anim][dir].map(function (e) {
83          return e.url;
84        });
85      });
86    });
87    return map;
88  }
89
90  // Pick the animation to show off: prefer a movement anim, else idle, else
91  // the first anim the pack actually has.
92  function pickAnim(frames) {
93    var anims = Object.keys(frames);
94    if (!anims.length) return null;
95    var prefer = ["walking", "walk", "running", "run"];
96    for (var i = 0; i < prefer.length; i++) {
97      if (frames[prefer[i]]) return prefer[i];
98    }
99    if (frames.idle) return "idle";
100    // Any non-idle anim reads as motion-capable; else fall to the first.
101    var nonIdle = anims.filter(function (a) {
102      return a !== "idle";
103    });
104    return nonIdle.length ? nonIdle[0] : anims[0];
105  }
106
107  // Directions this animation actually carries, intersected with the compass
108  // order so the facing-vector lookup only ever picks a direction that exists.
109  function availableDirs(animFrames) {
110    return DIRS.filter(function (d) {
111      return animFrames[d.name] && animFrames[d.name].length;
112    });
113  }
114
115  // Nearest available direction to a movement vector (dot product over the
116  // unit vectors). Falls back to the first available when the vector is zero.
117  function dirForVector(dirs, dx, dy) {
118    if (dx === 0 && dy === 0) return dirs[0];
119    var best = dirs[0];
120    var bestDot = -Infinity;
121    for (var i = 0; i < dirs.length; i++) {
122      var d = dirs[i];
123      var dot = d.dx * dx + d.dy * dy;
124      // normalize the candidate vector so diagonals don't out-vote cardinals
125      var mag = Math.sqrt(d.dx * d.dx + d.dy * d.dy) || 1;
126      dot = dot / mag;
127      if (dot > bestDot) {
128        bestDot = dot;
129        best = d;
130      }
131    }
132    return best;
133  }
134
135  function staticFrame(box, frames) {
136    // A single centered, non-moving frame (reduced-motion, idle-only, or a
137    // single-direction pack). Prefer idle/first-direction frame 0.
138    var anim = frames.idle ? "idle" : pickAnim(frames);
139    if (!anim) return;
140    var dirs = availableDirs(frames[anim]);
141    var dir = dirs.length ? dirs[0].name : Object.keys(frames[anim])[0];
142    var url = frames[anim][dir] && frames[anim][dir][0];
143    if (!url) return;
144    var sprite = makeSpriteEl(url);
145    var rect = box.getBoundingClientRect();
146    var bw = rect.width || box.offsetWidth || 320;
147    var bh = rect.height || box.offsetHeight || 320;
148    sprite.style.left = Math.round((bw - SPRITE_PX) / 2) + "px";
149    sprite.style.top = Math.round((bh - SPRITE_PX) / 2) + "px";
150    box.appendChild(sprite);
151  }
152
153  function makeSpriteEl(url) {
154    var sprite = document.createElement("div");
155    sprite.className = "sprite-showcase__sprite";
156    sprite.setAttribute("draggable", "false");
157    sprite.style.position = "absolute";
158    sprite.style.width = SPRITE_PX + "px";
159    sprite.style.height = SPRITE_PX + "px";
160    sprite.style.backgroundImage = "url('" + url + "')";
161    sprite.style.backgroundSize = "contain";
162    sprite.style.backgroundRepeat = "no-repeat";
163    sprite.style.backgroundPosition = "center";
164    sprite.style.imageRendering = "pixelated";
165    sprite.style.pointerEvents = "none";
166    sprite.style.userSelect = "none";
167    sprite.style.webkitUserDrag = "none";
168    sprite.style.willChange = "left, top";
169    return sprite;
170  }
171
172  function startWander(box, frames) {
173    var anim = pickAnim(frames);
174    if (!anim) return;
175    var dirs = availableDirs(frames[anim]);
176
177    // Single-direction (or no usable direction) packs don't move — they'd
178    // turn-clamp against an empty direction set. Play the one direction static.
179    if (dirs.length <= 1) {
180      staticFrame(box, frames);
181      return;
182    }
183
184    var rect = box.getBoundingClientRect();
185    var bw = rect.width || box.offsetWidth || 320;
186    var bh = rect.height || box.offsetHeight || 320;
187    var maxX = Math.max(0, bw - SPRITE_PX);
188    var maxY = Math.max(0, bh - SPRITE_PX);
189
190    var sprite = makeSpriteEl("");
191    box.appendChild(sprite);
192
193    var pos = { x: maxX / 2, y: maxY / 2 };
194    var heading = dirs[Math.floor(Math.random() * dirs.length)];
195    var facing = heading;
196    var frameIdx = 0;
197    var lastFrameAt = 0;
198    var lastMoveAt = 0;
199    var pauseUntil = 0;
200
201    function pickNewHeading() {
202      // Choose a fresh available direction (allow repeats rarely); occasional
203      // brief pause to read as "showing itself off".
204      heading = dirs[Math.floor(Math.random() * dirs.length)];
205      facing = heading;
206      if (Math.random() < 0.25) {
207        pauseUntil = performance.now() + 400 + Math.random() * 900;
208      }
209    }
210
211    function paint() {
212      var seq = frames[anim][facing.name] || [];
213      if (!seq.length) {
214        // Facing a direction this anim lacks — snap to the nearest it has.
215        facing = dirForVector(dirs, facing.dx, facing.dy);
216        seq = frames[anim][facing.name] || [];
217      }
218      if (seq.length) {
219        var url = seq[frameIdx % seq.length];
220        sprite.style.backgroundImage = "url('" + url + "')";
221      }
222    }
223
224    function tick(now) {
225      if (!box.isConnected) return; // box left the DOM — stop the loop
226      var dtMove = lastMoveAt ? now - lastMoveAt : 0;
227      lastMoveAt = now;
228
229      var moving = now >= pauseUntil;
230      if (moving) {
231        pos.x += heading.dx * SPEED * dtMove;
232        pos.y += heading.dy * SPEED * dtMove;
233
234        // Clamp to the box and turn on contact with an edge.
235        var bounced = false;
236        if (pos.x <= 0) {
237          pos.x = 0;
238          bounced = true;
239        } else if (pos.x >= maxX) {
240          pos.x = maxX;
241          bounced = true;
242        }
243        if (pos.y <= 0) {
244          pos.y = 0;
245          bounced = true;
246        } else if (pos.y >= maxY) {
247          pos.y = maxY;
248          bounced = true;
249        }
250        if (bounced || Math.random() < 0.004) {
251          pickNewHeading();
252        }
253        facing = dirForVector(dirs, heading.dx, heading.dy);
254      }
255
256      sprite.style.left = Math.round(pos.x) + "px";
257      sprite.style.top = Math.round(pos.y) + "px";
258
259      // Advance the walk-cycle frame at FPS (only while moving; a pause holds
260      // the current frame so it reads as standing still).
261      if (moving && now - lastFrameAt >= 1000 / FPS) {
262        frameIdx += 1;
263        lastFrameAt = now;
264        paint();
265      }
266      requestAnimationFrame(tick);
267    }
268
269    paint();
270    requestAnimationFrame(tick);
271  }
272
273  function initBox(box) {
274    if (box.dataset.showcaseInit === "1") return;
275    box.dataset.showcaseInit = "1";
276
277    // Anti-copy hardening on the container.
278    box.oncontextmenu = function () {
279      return false;
280    };
281    box.addEventListener("dragstart", function (e) {
282      e.preventDefault();
283    });
284    box.style.userSelect = "none";
285    box.style.webkitUserSelect = "none";
286    box.style.position = box.style.position || "relative";
287    box.style.overflow = "hidden";
288
289    var frames = parseFrames(box.getAttribute("data-frames") || "{}");
290    if (!Object.keys(frames).length) return;
291
292    // Defer one frame so the box has measurable dimensions (the Photos tab is
293    // default-active + keepMounted, so it's visible on first paint).
294    var run = function () {
295      if (REDUCED_MOTION) {
296        staticFrame(box, frames);
297      } else {
298        startWander(box, frames);
299      }
300    };
301    if (box.offsetWidth > 0) {
302      requestAnimationFrame(run);
303    } else {
304      // Not yet measured — poll briefly until it has a size, then run once.
305      var tries = 0;
306      var wait = setInterval(function () {
307        tries += 1;
308        if (box.offsetWidth > 0 || tries > 40) {
309          clearInterval(wait);
310          run();
311        }
312      }, 50);
313    }
314  }
315
316  function scan(root) {
317    var nodes = (root || document).querySelectorAll(
318      ".sprite-showcase[data-frames]"
319    );
320    for (var i = 0; i < nodes.length; i++) {
321      initBox(nodes[i]);
322    }
323  }
324
325  function startObserver() {
326    scan(document);
327    var observer = new MutationObserver(function (mutations) {
328      for (var i = 0; i < mutations.length; i++) {
329        var added = mutations[i].addedNodes;
330        for (var j = 0; j < added.length; j++) {
331          var node = added[j];
332          if (node.nodeType !== 1) continue; // element nodes only
333          if (
334            node.classList &&
335            node.classList.contains("sprite-showcase") &&
336            node.hasAttribute("data-frames")
337          ) {
338            initBox(node);
339          } else if (node.querySelectorAll) {
340            scan(node);
341          }
342        }
343      }
344    });
345    observer.observe(document.body, { childList: true, subtree: true });
346  }
347
348  if (document.readyState === "loading") {
349    document.addEventListener("DOMContentLoaded", startObserver);
350  } else {
351    startObserver();
352  }
353})();

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.