Skip to content
SlashClone

Seeded image crops

@slashclone/seeded-image-crops

Give every card, profile, and page header its own cover from just a few hero images: each item gets a stable crop (a hand-picked framing, nudged per item), the same every time it loads. One small file shared by every card, so a page of cards downloads the images once. Includes a checker that previews how evenly your items spread across the crops.

SkillContentv0.1.0 (latest)Tested
Activity
New: be the first to try it
  1. 1#!/usr/bin/env node
  2. 2// Checks a list of crops: how many distinct covers a set of seeds gets, how evenly they
  3. 3// spread over the framings, and that the same seed always gets the same crop. Node
  4. 4// built-ins only; the picker matches examples/seeded-crop.ts.
  5. 5// Usage: node scripts/check-crops.mjs --crops examples/crops.json [--seeds 200]
  6. 6// [--out out/crop-report.json]
  7. 7import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
  8. 8import { dirname } from "node:path";
  9. 9import { parseArgs } from "node:util";
  10. 11const { values } = parseArgs({
  11. 12 options: {
  12. 13 crops: { type: "string" },
  13. 14 seeds: { type: "string", default: "200" },
  14. 15 out: { type: "string" },
  15. 16 },
  16. 17});
  17. 18if (!values.crops) {
  18. 19 console.error("Missing --crops: a JSON array of { x, y, zoom }.");
  19. 20 process.exit(2);
  20. 22const crops = JSON.parse(readFileSync(values.crops, "utf8"));
  21. 23const valid = (c) => c.x >= 0 && c.x <= 100 && c.y >= 0 && c.y <= 100 && c.zoom >= 1;
  22. 24if (!Array.isArray(crops) || crops.length === 0 || !crops.every(valid)) {
  23. 25 console.error("Each crop needs x and y from 0 to 100 and a zoom of at least 1.");
  24. 26 process.exit(1);
  25. 29function hash(seed) {
  26. 30 let h = 2166136261;
  27. 31 for (let i = 0; i < seed.length; i++) h = Math.imul(h ^ seed.charCodeAt(i), 16777619);
  28. 32 h = Math.imul(h ^ (h >>> 16), 0x45d9f3b);
  29. 33 h = Math.imul(h ^ (h >>> 16), 0x45d9f3b);
  30. 34 return (h ^ (h >>> 16)) >>> 0;
  31. 36function cropFor(seed) {
  32. 37 const h = hash(seed);
  33. 38 const base = crops[h % crops.length];
  34. 39 const nudge = (bits) => ((h >>> bits) & 255) / 255 - 0.5;
  35. 40 const clamp = (n) => Math.min(100, Math.max(0, Math.round(n)));
  36. 41 return {
  37. 42 framing: h % crops.length,
  38. 43 x: clamp(base.x + nudge(8) * 10),
  39. 44 y: clamp(base.y + nudge(16) * 10),
  40. 45 zoom: Math.max(1, Math.round(base.zoom * (1 + nudge(24) * 0.25) * 100) / 100),
  41. 46 };
  42. 49const n = Math.max(1, Number(values.seeds));
  43. 50const seeds = Array.from({ length: n }, (_, i) => `item-${String(i)}`);
  44. 51const picked = seeds.map(cropFor);
  45. 52const stable = seeds.every((s, i) => JSON.stringify(cropFor(s)) === JSON.stringify(picked[i]));
  46. 53const distinct = new Set(picked.map((c) => `${c.x},${c.y},${c.zoom}`)).size;
  47. 54const perFraming = crops.map((_, i) => picked.filter((c) => c.framing === i).length);
  48. 55const report = { framings: crops.length, seeds: n, distinct, stable, perFraming };
  49. 57console.log(
  50. 58 `${String(n)} seeds -> ${String(distinct)} distinct covers over ${String(crops.length)} framings; per framing: ${perFraming.join(", ")}; stable: ${String(stable)}`,
  51. 59);
  52. 60if (values.out) {
  53. 61 mkdirSync(dirname(values.out), { recursive: true });
  54. 62 writeFileSync(values.out, JSON.stringify(report, null, 2) + "\n");