@prosopo/icon-order-assets 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +24 -0
  2. package/.turbo/turbo-build$colon$tsc.log +14 -0
  3. package/.turbo/turbo-build.log +25 -0
  4. package/CHANGELOG.md +21 -0
  5. package/LICENSE +201 -0
  6. package/dist/_virtual/_rolldown/runtime.js +3 -0
  7. package/dist/background.d.ts +4 -0
  8. package/dist/background.d.ts.map +1 -0
  9. package/dist/background.js +87 -0
  10. package/dist/background.js.map +1 -0
  11. package/dist/cjs/_virtual/_rolldown/runtime.cjs +23 -0
  12. package/dist/cjs/background.cjs +87 -0
  13. package/dist/cjs/compose.cjs +108 -0
  14. package/dist/cjs/glyphs.cjs +41 -0
  15. package/dist/cjs/index.cjs +64 -0
  16. package/dist/cjs/place.cjs +68 -0
  17. package/dist/cjs/shuffle.cjs +16 -0
  18. package/dist/compose.d.ts +5 -0
  19. package/dist/compose.d.ts.map +1 -0
  20. package/dist/compose.js +103 -0
  21. package/dist/compose.js.map +1 -0
  22. package/dist/glyphs.d.ts +15 -0
  23. package/dist/glyphs.d.ts.map +1 -0
  24. package/dist/glyphs.js +39 -0
  25. package/dist/glyphs.js.map +1 -0
  26. package/dist/index.d.ts +20 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +54 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/place.d.ts +8 -0
  31. package/dist/place.d.ts.map +1 -0
  32. package/dist/place.js +68 -0
  33. package/dist/place.js.map +1 -0
  34. package/dist/shuffle.d.ts +3 -0
  35. package/dist/shuffle.d.ts.map +1 -0
  36. package/dist/shuffle.js +16 -0
  37. package/dist/shuffle.js.map +1 -0
  38. package/dist/tests/iconOrderAssets.unit.test.d.ts +2 -0
  39. package/dist/tests/iconOrderAssets.unit.test.d.ts.map +1 -0
  40. package/dist/tests/iconOrderAssets.unit.test.js +202 -0
  41. package/dist/tests/iconOrderAssets.unit.test.js.map +1 -0
  42. package/dist/types.d.ts +29 -0
  43. package/dist/types.d.ts.map +1 -0
  44. package/dist/types.js +2 -0
  45. package/dist/types.js.map +1 -0
  46. package/package.json +53 -3
  47. package/scripts/preview.ts +73 -0
  48. package/vite.cjs.config.ts +23 -0
  49. package/vite.esm.config.ts +22 -0
  50. package/vite.test.config.ts +32 -0
  51. package/README.md +0 -3
@@ -0,0 +1,41 @@
1
+ //#region src/glyphs.ts
2
+ /**
3
+ * Shapes must stay distinguishable from each other under rotation, or the
4
+ * legend becomes ambiguous; hence no plain line and only one ring.
5
+ */
6
+ var GlyphKind = /* @__PURE__ */ function(GlyphKind) {
7
+ GlyphKind["ring"] = "ring";
8
+ GlyphKind["spiral"] = "spiral";
9
+ GlyphKind["chevron"] = "chevron";
10
+ GlyphKind["bolt"] = "bolt";
11
+ GlyphKind["cross"] = "cross";
12
+ GlyphKind["triangle"] = "triangle";
13
+ GlyphKind["star"] = "star";
14
+ GlyphKind["wave"] = "wave";
15
+ GlyphKind["square"] = "square";
16
+ GlyphKind["arc"] = "arc";
17
+ return GlyphKind;
18
+ }({});
19
+ /** Stroked path data in a 100x100 box centred on (50, 50). */
20
+ var GLYPH_PATHS = {
21
+ ["ring"]: "M 14 50 A 36 36 0 1 0 86 50 A 36 36 0 1 0 14 50",
22
+ ["spiral"]: "M 50 12 A 38 38 0 1 1 12 50 A 28 28 0 1 1 68 50 A 18 18 0 1 1 32 50",
23
+ ["chevron"]: "M 22 20 L 54 50 L 22 80 M 48 20 L 80 50 L 48 80",
24
+ ["bolt"]: "M 26 12 L 62 40 L 38 54 L 74 88",
25
+ ["cross"]: "M 20 20 L 80 80 M 80 20 L 20 80",
26
+ ["triangle"]: "M 50 14 L 86 82 L 14 82 Z",
27
+ ["star"]: "M 50 12 L 59.4 37.1 L 86.1 38.2 L 65.2 54.9 L 72.3 80.6 L 50 66 L 27.7 80.6 L 34.8 54.9 L 13.9 38.2 L 40.6 37.1 Z",
28
+ ["wave"]: "M 12 62 Q 31 22 50 62 T 88 62",
29
+ ["square"]: "M 20 20 H 80 V 80 H 20 Z",
30
+ ["arc"]: "M 14 72 A 40 40 0 0 1 86 72"
31
+ };
32
+ var GLYPH_KINDS = Object.values(GlyphKind);
33
+ var glyphPath = (kind) => {
34
+ const path = GLYPH_PATHS[kind];
35
+ if (!path) throw new Error(`icon-order-assets: no path for glyph "${kind}"`);
36
+ return path;
37
+ };
38
+ //#endregion
39
+ exports.GLYPH_KINDS = GLYPH_KINDS;
40
+ exports.GlyphKind = GlyphKind;
41
+ exports.glyphPath = glyphPath;
@@ -0,0 +1,64 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_glyphs = require("./glyphs.cjs");
3
+ const require_compose = require("./compose.cjs");
4
+ const require_place = require("./place.cjs");
5
+ let _prosopo_puzzle_assets = require("@prosopo/puzzle-assets");
6
+ //#region src/index.ts
7
+ /** Matches the widget's frame; stored targets and graded clicks use these pixels. */
8
+ var DEFAULT_GEOMETRY = {
9
+ width: 300,
10
+ height: 200,
11
+ iconSize: 38
12
+ };
13
+ /** `targetCount` and `decoyCount` must match the defaults in @prosopo/types. */
14
+ var DEFAULT_RENDER_SETTINGS = {
15
+ targetCount: 3,
16
+ decoyCount: 4,
17
+ strokeWidth: 3,
18
+ iconOpacity: .92,
19
+ haloOpacity: .7,
20
+ backgroundClutter: 8
21
+ };
22
+ /** Fixed because the widget lays the legend out against its own header. */
23
+ var LEGEND_ICON_SIZE = 26;
24
+ /**
25
+ * The background is drawn fresh per call and never reused: two layouts on the
26
+ * same background could be diffed to read both answers. The returned
27
+ * `targets` are the answer and must never be serialised into a response.
28
+ */
29
+ var renderIconOrder = async (geometry = DEFAULT_GEOMETRY, settings = DEFAULT_RENDER_SETTINGS) => {
30
+ const prng = (0, _prosopo_puzzle_assets.createPrng)((0, _prosopo_puzzle_assets.createSeed)());
31
+ const { targets, decoys } = require_place.placeIcons(prng, geometry, settings.targetCount, settings.decoyCount);
32
+ const composited = await require_compose.compositeIcons(prng, [...targets, ...decoys], geometry, settings);
33
+ const legend = await require_compose.renderLegend(targets, 26, settings);
34
+ const [backgroundWebp, legendWebp] = await Promise.all([(0, _prosopo_puzzle_assets.encodeBackground)(composited), (0, _prosopo_puzzle_assets.encodePiece)(legend)]);
35
+ return {
36
+ background: backgroundWebp,
37
+ legend: legendWebp,
38
+ legendIconSize: 26,
39
+ targets
40
+ };
41
+ };
42
+ /**
43
+ * Click i must land on target i, within `tolerance` times that icon's size.
44
+ */
45
+ var gradeClicks = (targets, clicks, tolerance) => {
46
+ if (clicks.length !== targets.length) return false;
47
+ return targets.every((target, index) => {
48
+ const click = clicks[index];
49
+ if (!click) return false;
50
+ return Math.hypot(click.x - target.x, click.y - target.y) <= tolerance * target.size;
51
+ });
52
+ };
53
+ //#endregion
54
+ exports.DEFAULT_GEOMETRY = DEFAULT_GEOMETRY;
55
+ exports.DEFAULT_RENDER_SETTINGS = DEFAULT_RENDER_SETTINGS;
56
+ exports.GLYPH_KINDS = require_glyphs.GLYPH_KINDS;
57
+ exports.GlyphKind = require_glyphs.GlyphKind;
58
+ exports.LEGEND_ICON_SIZE = LEGEND_ICON_SIZE;
59
+ exports.compositeIcons = require_compose.compositeIcons;
60
+ exports.glyphPath = require_glyphs.glyphPath;
61
+ exports.gradeClicks = gradeClicks;
62
+ exports.placeIcons = require_place.placeIcons;
63
+ exports.renderIconOrder = renderIconOrder;
64
+ exports.renderLegend = require_compose.renderLegend;
@@ -0,0 +1,68 @@
1
+ const require_glyphs = require("./glyphs.cjs");
2
+ const require_shuffle = require("./shuffle.cjs");
3
+ //#region src/place.ts
4
+ /**
5
+ * Minimum centre-to-centre distance, as a multiple of the larger icon's size,
6
+ * so a click cannot land on two icons at once.
7
+ */
8
+ var MIN_SEPARATION = 1.15;
9
+ /** Keep whole icons inside the frame; a clipped glyph is unidentifiable. */
10
+ var EDGE_MARGIN = .55;
11
+ var MAX_ATTEMPTS_PER_ICON = 60;
12
+ var SIZE_JITTER = [.85, 1.15];
13
+ var farEnough = (x, y, size, placed) => placed.every((other) => {
14
+ const need = MIN_SEPARATION * Math.max(size, other.size);
15
+ return Math.hypot(x - other.x, y - other.y) >= need;
16
+ });
17
+ /**
18
+ * The legend identifies a target by its shape, so every icon on the frame,
19
+ * target or decoy, must be a distinct glyph; hence one deck for both.
20
+ */
21
+ var drawKinds = (prng, count) => {
22
+ if (count > require_glyphs.GLYPH_KINDS.length) throw new Error(`icon-order-assets: asked for ${count} distinct glyphs but only ${require_glyphs.GLYPH_KINDS.length} exist`);
23
+ return require_shuffle.shuffle(prng, require_glyphs.GLYPH_KINDS).slice(0, count);
24
+ };
25
+ var tryPlace = (prng, geometry, kind, placed) => {
26
+ const size = geometry.iconSize * prng.range(SIZE_JITTER[0], SIZE_JITTER[1]);
27
+ const margin = size * EDGE_MARGIN;
28
+ for (let attempt = 0; attempt < MAX_ATTEMPTS_PER_ICON; attempt++) {
29
+ const x = prng.range(margin, geometry.width - margin);
30
+ const y = prng.range(margin, geometry.height - margin);
31
+ if (!farEnough(x, y, size, placed)) continue;
32
+ return {
33
+ x,
34
+ y,
35
+ kind,
36
+ rotation: prng.range(0, 360),
37
+ hue: prng.range(0, 360),
38
+ size
39
+ };
40
+ }
41
+ };
42
+ /**
43
+ * Targets are mandatory, since a frame missing one is unsolvable, so failing
44
+ * to place one throws. Decoys are best-effort and are dropped when the frame
45
+ * is too crowded.
46
+ */
47
+ var placeIcons = (prng, geometry, targetCount, decoyCount) => {
48
+ const kinds = drawKinds(prng, targetCount + decoyCount);
49
+ const placed = [];
50
+ const targets = [];
51
+ const decoys = [];
52
+ kinds.forEach((kind, index) => {
53
+ const placement = tryPlace(prng, geometry, kind, placed);
54
+ const isTarget = index < targetCount;
55
+ if (!placement) {
56
+ if (isTarget) throw new Error(`icon-order-assets: could not place target icon ${index + 1} of ${targetCount} in a ${geometry.width}x${geometry.height} frame at size ${geometry.iconSize}`);
57
+ return;
58
+ }
59
+ placed.push(placement);
60
+ (isTarget ? targets : decoys).push(placement);
61
+ });
62
+ return {
63
+ targets,
64
+ decoys
65
+ };
66
+ };
67
+ //#endregion
68
+ exports.placeIcons = placeIcons;
@@ -0,0 +1,16 @@
1
+ //#region src/shuffle.ts
2
+ /** Fisher-Yates, so every ordering is equally likely. */
3
+ var shuffle = (prng, items) => {
4
+ const out = [...items];
5
+ for (let i = out.length - 1; i > 0; i--) {
6
+ const j = prng.int(0, i);
7
+ const a = out[i];
8
+ const b = out[j];
9
+ if (a === void 0 || b === void 0) throw new Error("icon-order-assets: shuffle index out of range");
10
+ out[i] = b;
11
+ out[j] = a;
12
+ }
13
+ return out;
14
+ };
15
+ //#endregion
16
+ exports.shuffle = shuffle;
@@ -0,0 +1,5 @@
1
+ import { type Prng, type RgbaImage } from "@prosopo/puzzle-assets";
2
+ import type { IconOrderGeometry, IconOrderRenderSettings, IconPlacement } from "./types.js";
3
+ export declare const compositeIcons: (prng: Prng, icons: readonly IconPlacement[], geometry: IconOrderGeometry, settings: IconOrderRenderSettings) => Promise<RgbaImage>;
4
+ export declare const renderLegend: (targets: readonly IconPlacement[], legendIconSize: number, settings: IconOrderRenderSettings) => Promise<RgbaImage>;
5
+ //# sourceMappingURL=compose.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compose.d.ts","sourceRoot":"","sources":["../src/compose.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,KAAK,IAAI,EAAE,KAAK,SAAS,EAAY,MAAM,wBAAwB,CAAC;AAK7E,OAAO,KAAK,EACX,iBAAiB,EACjB,uBAAuB,EACvB,aAAa,EACb,MAAM,YAAY,CAAC;AAkEpB,eAAO,MAAM,cAAc,SACpB,IAAI,SACH,SAAS,aAAa,EAAE,YACrB,iBAAiB,YACjB,uBAAuB,KAC/B,OAAO,CAAC,SAAS,CAgBnB,CAAC;AAyBF,eAAO,MAAM,YAAY,YACf,SAAS,aAAa,EAAE,kBACjB,MAAM,YACZ,uBAAuB,KAC/B,OAAO,CAAC,SAAS,CA+BnB,CAAC"}
@@ -0,0 +1,103 @@
1
+ import { collageMarkup } from "./background.js";
2
+ import { glyphPath } from "./glyphs.js";
3
+ import { shuffle } from "./shuffle.js";
4
+ import { hslToRgb } from "@prosopo/puzzle-assets";
5
+ import sharp from "sharp";
6
+ //#region src/compose.ts
7
+ /** Glyph path data is authored in this box; see glyphs.ts. */
8
+ var GLYPH_BOX = 100;
9
+ /**
10
+ * Width of the dark outline stroke relative to the bright one. Narrower and
11
+ * icons vanish over light regions; wider and the halo becomes the strongest
12
+ * edge in the frame, which is what a solver looks for.
13
+ */
14
+ var HALO_WIDTH_MULTIPLE = 2.4;
15
+ var HALO_COLOUR = "rgb(12,14,22)";
16
+ var LEGEND_CHIP_COLOUR = "rgb(24,27,38)";
17
+ var LEGEND_GAP = 6;
18
+ var LEGEND_GLYPH_INSET = .62;
19
+ var round = (value) => Math.round(value * 100) / 100;
20
+ var strokeColour = (hue) => {
21
+ const { r, g, b } = hslToRgb(hue, .86, .64);
22
+ return `rgb(${r},${g},${b})`;
23
+ };
24
+ /**
25
+ * Converts a frame-pixel stroke width into glyph-box units, so line weight
26
+ * does not vary with (and so leak) the icon's size.
27
+ */
28
+ var strokeUnits = (widthPx, size) => widthPx * GLYPH_BOX / size;
29
+ var glyphMarkup = (placement, settings, rotationOverride) => {
30
+ const scale = placement.size / GLYPH_BOX;
31
+ const rotation = rotationOverride ?? placement.rotation;
32
+ const inner = strokeUnits(settings.strokeWidth, placement.size);
33
+ const halo = inner * HALO_WIDTH_MULTIPLE;
34
+ const path = glyphPath(placement.kind);
35
+ return [
36
+ `<g transform="${[
37
+ `translate(${round(placement.x)} ${round(placement.y)})`,
38
+ `rotate(${round(rotation)})`,
39
+ `scale(${round(scale)})`,
40
+ `translate(${-GLYPH_BOX / 2} ${-GLYPH_BOX / 2})`
41
+ ].join(" ")}" fill="none" stroke-linecap="round" stroke-linejoin="round">`,
42
+ `<path d="${path}" stroke="${HALO_COLOUR}" stroke-opacity="${round(settings.haloOpacity)}" stroke-width="${round(halo)}"/>`,
43
+ `<path d="${path}" stroke="${strokeColour(placement.hue)}" stroke-opacity="${round(settings.iconOpacity)}" stroke-width="${round(inner)}"/>`,
44
+ "</g>"
45
+ ].join("");
46
+ };
47
+ var svgDocument = (width, height, body) => Buffer.from(`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">${body}</svg>`);
48
+ /**
49
+ * Draw order is shuffled here because callers hold targets and decoys as
50
+ * separate lists, and concatenating them would paint every target on top
51
+ * wherever icons overlap — a tell that needs no shape recognition.
52
+ */
53
+ var compositeIcons = async (prng, icons, geometry, settings) => {
54
+ const body = shuffle(prng, icons).map((placement) => glyphMarkup(placement, settings)).join("");
55
+ const { data, info } = await sharp(svgDocument(geometry.width, geometry.height, collageMarkup(prng, geometry, settings.backgroundClutter) + body)).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
56
+ return addGrain(prng, {
57
+ data,
58
+ width: info.width,
59
+ height: info.height
60
+ });
61
+ };
62
+ /**
63
+ * Noise over the flat vector fills, so region boundaries are not perfectly
64
+ * clean input for an edge detector.
65
+ */
66
+ var GRAIN_AMPLITUDE = 6;
67
+ var addGrain = (prng, image) => {
68
+ const { data } = image;
69
+ for (let i = 0; i < data.length; i += 4) {
70
+ const noise = Math.round((prng.next() - .5) * 2 * GRAIN_AMPLITUDE);
71
+ for (let channel = 0; channel < 3; channel++) {
72
+ const value = (data[i + channel] ?? 0) + noise;
73
+ data[i + channel] = value < 0 ? 0 : value > 255 ? 255 : value;
74
+ }
75
+ }
76
+ return image;
77
+ };
78
+ /**
79
+ * Legend glyphs are drawn upright whatever their rotation on the frame, so a
80
+ * template-matching solver is not handed a rotation-aligned crib. The hue is
81
+ * kept: it helps a human and a solver already has it from the pixels.
82
+ */
83
+ var renderLegend = async (targets, legendIconSize, settings) => {
84
+ const step = legendIconSize + LEGEND_GAP;
85
+ const width = Math.max(legendIconSize, targets.length * step - (targets.length > 0 ? LEGEND_GAP : 0));
86
+ const radius = legendIconSize / 2;
87
+ const { data, info } = await sharp(svgDocument(width, legendIconSize, targets.map((target, index) => {
88
+ const cx = index * step + radius;
89
+ return `<circle cx="${round(cx)}" cy="${round(radius)}" r="${round(radius)}" fill="${LEGEND_CHIP_COLOUR}"/>` + glyphMarkup({
90
+ ...target,
91
+ x: cx,
92
+ y: radius,
93
+ size: legendIconSize * LEGEND_GLYPH_INSET
94
+ }, settings, 0);
95
+ }).join(""))).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
96
+ return {
97
+ data,
98
+ width: info.width,
99
+ height: info.height
100
+ };
101
+ };
102
+ //#endregion
103
+ export { compositeIcons, renderLegend };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compose.js","sourceRoot":"","sources":["../src/compose.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ export declare enum GlyphKind {
2
+ ring = "ring",
3
+ spiral = "spiral",
4
+ chevron = "chevron",
5
+ bolt = "bolt",
6
+ cross = "cross",
7
+ triangle = "triangle",
8
+ star = "star",
9
+ wave = "wave",
10
+ square = "square",
11
+ arc = "arc"
12
+ }
13
+ export declare const GLYPH_KINDS: readonly GlyphKind[];
14
+ export declare const glyphPath: (kind: GlyphKind) => string;
15
+ //# sourceMappingURL=glyphs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"glyphs.d.ts","sourceRoot":"","sources":["../src/glyphs.ts"],"names":[],"mappings":"AAkBA,oBAAY,SAAS;IACpB,IAAI,SAAS;IACb,MAAM,WAAW;IACjB,OAAO,YAAY;IACnB,IAAI,SAAS;IACb,KAAK,UAAU;IACf,QAAQ,aAAa;IACrB,IAAI,SAAS;IACb,IAAI,SAAS;IACb,MAAM,WAAW;IACjB,GAAG,QAAQ;CACX;AAkBD,eAAO,MAAM,WAAW,EAAE,SAAS,SAAS,EAA6B,CAAC;AAE1E,eAAO,MAAM,SAAS,SAAU,SAAS,KAAG,MAM3C,CAAC"}
package/dist/glyphs.js ADDED
@@ -0,0 +1,39 @@
1
+ //#region src/glyphs.ts
2
+ /**
3
+ * Shapes must stay distinguishable from each other under rotation, or the
4
+ * legend becomes ambiguous; hence no plain line and only one ring.
5
+ */
6
+ var GlyphKind = /* @__PURE__ */ function(GlyphKind) {
7
+ GlyphKind["ring"] = "ring";
8
+ GlyphKind["spiral"] = "spiral";
9
+ GlyphKind["chevron"] = "chevron";
10
+ GlyphKind["bolt"] = "bolt";
11
+ GlyphKind["cross"] = "cross";
12
+ GlyphKind["triangle"] = "triangle";
13
+ GlyphKind["star"] = "star";
14
+ GlyphKind["wave"] = "wave";
15
+ GlyphKind["square"] = "square";
16
+ GlyphKind["arc"] = "arc";
17
+ return GlyphKind;
18
+ }({});
19
+ /** Stroked path data in a 100x100 box centred on (50, 50). */
20
+ var GLYPH_PATHS = {
21
+ ["ring"]: "M 14 50 A 36 36 0 1 0 86 50 A 36 36 0 1 0 14 50",
22
+ ["spiral"]: "M 50 12 A 38 38 0 1 1 12 50 A 28 28 0 1 1 68 50 A 18 18 0 1 1 32 50",
23
+ ["chevron"]: "M 22 20 L 54 50 L 22 80 M 48 20 L 80 50 L 48 80",
24
+ ["bolt"]: "M 26 12 L 62 40 L 38 54 L 74 88",
25
+ ["cross"]: "M 20 20 L 80 80 M 80 20 L 20 80",
26
+ ["triangle"]: "M 50 14 L 86 82 L 14 82 Z",
27
+ ["star"]: "M 50 12 L 59.4 37.1 L 86.1 38.2 L 65.2 54.9 L 72.3 80.6 L 50 66 L 27.7 80.6 L 34.8 54.9 L 13.9 38.2 L 40.6 37.1 Z",
28
+ ["wave"]: "M 12 62 Q 31 22 50 62 T 88 62",
29
+ ["square"]: "M 20 20 H 80 V 80 H 20 Z",
30
+ ["arc"]: "M 14 72 A 40 40 0 0 1 86 72"
31
+ };
32
+ var GLYPH_KINDS = Object.values(GlyphKind);
33
+ var glyphPath = (kind) => {
34
+ const path = GLYPH_PATHS[kind];
35
+ if (!path) throw new Error(`icon-order-assets: no path for glyph "${kind}"`);
36
+ return path;
37
+ };
38
+ //#endregion
39
+ export { GLYPH_KINDS, GlyphKind, glyphPath };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"glyphs.js","sourceRoot":"","sources":["../src/glyphs.ts"],"names":[],"mappings":"AAkBA,MAAM,CAAN,IAAY,SAWX;AAXD,WAAY,SAAS;IACpB,0BAAa,CAAA;IACb,8BAAiB,CAAA;IACjB,gCAAmB,CAAA;IACnB,0BAAa,CAAA;IACb,4BAAe,CAAA;IACf,kCAAqB,CAAA;IACrB,0BAAa,CAAA;IACb,0BAAa,CAAA;IACb,8BAAiB,CAAA;IACjB,wBAAW,CAAA;AACZ,CAAC,EAXW,SAAS,KAAT,SAAS,QAWpB;AAGD,MAAM,WAAW,GAAwC;IACxD,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,iDAAiD;IACnE,CAAC,SAAS,CAAC,MAAM,CAAC,EACjB,qEAAqE;IACtE,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,iDAAiD;IACtE,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,iCAAiC;IACnD,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,iCAAiC;IACpD,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,2BAA2B;IACjD,CAAC,SAAS,CAAC,IAAI,CAAC,EACf,mHAAmH;IACpH,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,+BAA+B;IACjD,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,0BAA0B;IAC9C,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,6BAA6B;CAC9C,CAAC;AAEF,MAAM,CAAC,MAAM,WAAW,GAAyB,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;AAE1E,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,IAAe,EAAU,EAAE;IACpD,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;IAC/B,IAAI,CAAC,IAAI,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,yCAAyC,IAAI,GAAG,CAAC,CAAC;IACnE,CAAC;IACD,OAAO,IAAI,CAAC;AACb,CAAC,CAAC"}
@@ -0,0 +1,20 @@
1
+ import type { IconOrderGeometry, IconOrderRenderSettings, RenderedIconOrder } from "./types.js";
2
+ export { GLYPH_KINDS, GlyphKind, glyphPath } from "./glyphs.js";
3
+ export { placeIcons } from "./place.js";
4
+ export type { IconLayout } from "./place.js";
5
+ export { compositeIcons, renderLegend } from "./compose.js";
6
+ export type { IconOrderGeometry, IconOrderRenderSettings, IconPlacement, RenderedIconOrder, } from "./types.js";
7
+ export declare const DEFAULT_GEOMETRY: IconOrderGeometry;
8
+ export declare const DEFAULT_RENDER_SETTINGS: IconOrderRenderSettings;
9
+ export declare const LEGEND_ICON_SIZE = 26;
10
+ export declare const renderIconOrder: (geometry?: IconOrderGeometry, settings?: IconOrderRenderSettings) => Promise<RenderedIconOrder>;
11
+ export interface IconTargetGeometry {
12
+ x: number;
13
+ y: number;
14
+ size: number;
15
+ }
16
+ export declare const gradeClicks: (targets: readonly IconTargetGeometry[], clicks: readonly {
17
+ x: number;
18
+ y: number;
19
+ }[], tolerance: number) => boolean;
20
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EACX,iBAAiB,EACjB,uBAAuB,EACvB,iBAAiB,EACjB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAChE,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5D,YAAY,EACX,iBAAiB,EACjB,uBAAuB,EACvB,aAAa,EACb,iBAAiB,GACjB,MAAM,YAAY,CAAC;AAGpB,eAAO,MAAM,gBAAgB,EAAE,iBAI9B,CAAC;AAGF,eAAO,MAAM,uBAAuB,EAAE,uBAOrC,CAAC;AAGF,eAAO,MAAM,gBAAgB,KAAK,CAAC;AAOnC,eAAO,MAAM,eAAe,cACjB,iBAAiB,aACjB,uBAAuB,KAC/B,OAAO,CAAC,iBAAiB,CA2B3B,CAAC;AAGF,MAAM,WAAW,kBAAkB;IAClC,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,IAAI,EAAE,MAAM,CAAC;CACb;AAKD,eAAO,MAAM,WAAW,YACd,SAAS,kBAAkB,EAAE,UAC9B,SAAS;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,aAChC,MAAM,KACf,OAcF,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,54 @@
1
+ import "./_virtual/_rolldown/runtime.js";
2
+ import { GLYPH_KINDS, GlyphKind, glyphPath } from "./glyphs.js";
3
+ import { compositeIcons, renderLegend } from "./compose.js";
4
+ import { placeIcons } from "./place.js";
5
+ import { createPrng, createSeed, encodeBackground, encodePiece } from "@prosopo/puzzle-assets";
6
+ //#region src/index.ts
7
+ /** Matches the widget's frame; stored targets and graded clicks use these pixels. */
8
+ var DEFAULT_GEOMETRY = {
9
+ width: 300,
10
+ height: 200,
11
+ iconSize: 38
12
+ };
13
+ /** `targetCount` and `decoyCount` must match the defaults in @prosopo/types. */
14
+ var DEFAULT_RENDER_SETTINGS = {
15
+ targetCount: 3,
16
+ decoyCount: 4,
17
+ strokeWidth: 3,
18
+ iconOpacity: .92,
19
+ haloOpacity: .7,
20
+ backgroundClutter: 8
21
+ };
22
+ /** Fixed because the widget lays the legend out against its own header. */
23
+ var LEGEND_ICON_SIZE = 26;
24
+ /**
25
+ * The background is drawn fresh per call and never reused: two layouts on the
26
+ * same background could be diffed to read both answers. The returned
27
+ * `targets` are the answer and must never be serialised into a response.
28
+ */
29
+ var renderIconOrder = async (geometry = DEFAULT_GEOMETRY, settings = DEFAULT_RENDER_SETTINGS) => {
30
+ const prng = createPrng(createSeed());
31
+ const { targets, decoys } = placeIcons(prng, geometry, settings.targetCount, settings.decoyCount);
32
+ const composited = await compositeIcons(prng, [...targets, ...decoys], geometry, settings);
33
+ const legend = await renderLegend(targets, 26, settings);
34
+ const [backgroundWebp, legendWebp] = await Promise.all([encodeBackground(composited), encodePiece(legend)]);
35
+ return {
36
+ background: backgroundWebp,
37
+ legend: legendWebp,
38
+ legendIconSize: 26,
39
+ targets
40
+ };
41
+ };
42
+ /**
43
+ * Click i must land on target i, within `tolerance` times that icon's size.
44
+ */
45
+ var gradeClicks = (targets, clicks, tolerance) => {
46
+ if (clicks.length !== targets.length) return false;
47
+ return targets.every((target, index) => {
48
+ const click = clicks[index];
49
+ if (!click) return false;
50
+ return Math.hypot(click.x - target.x, click.y - target.y) <= tolerance * target.size;
51
+ });
52
+ };
53
+ //#endregion
54
+ export { DEFAULT_GEOMETRY, DEFAULT_RENDER_SETTINGS, GLYPH_KINDS, GlyphKind, LEGEND_ICON_SIZE, compositeIcons, glyphPath, gradeClicks, placeIcons, renderIconOrder, renderLegend };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAcA,OAAO,EACN,UAAU,EACV,UAAU,EACV,gBAAgB,EAChB,WAAW,GACX,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAOxC,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAChE,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExC,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAS5D,MAAM,CAAC,MAAM,gBAAgB,GAAsB;IAClD,KAAK,EAAE,GAAG;IACV,MAAM,EAAE,GAAG;IACX,QAAQ,EAAE,EAAE;CACZ,CAAC;AAGF,MAAM,CAAC,MAAM,uBAAuB,GAA4B;IAC/D,WAAW,EAAE,CAAC;IACd,UAAU,EAAE,CAAC;IACb,WAAW,EAAE,CAAC;IACd,WAAW,EAAE,IAAI;IACjB,WAAW,EAAE,GAAG;IAChB,iBAAiB,EAAE,CAAC;CACpB,CAAC;AAGF,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAE,CAAC;AAOnC,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,EACnC,WAA8B,gBAAgB,EAC9C,WAAoC,uBAAuB,EAC9B,EAAE;IAC/B,MAAM,IAAI,GAAG,UAAU,CAAC,UAAU,EAAE,CAAC,CAAC;IACtC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,UAAU,CACrC,IAAI,EACJ,QAAQ,EACR,QAAQ,CAAC,WAAW,EACpB,QAAQ,CAAC,UAAU,CACnB,CAAC;IACF,MAAM,UAAU,GAAG,MAAM,cAAc,CACtC,IAAI,EACJ,CAAC,GAAG,OAAO,EAAE,GAAG,MAAM,CAAC,EACvB,QAAQ,EACR,QAAQ,CACR,CAAC;IACF,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC,OAAO,EAAE,gBAAgB,EAAE,QAAQ,CAAC,CAAC;IAEvE,MAAM,CAAC,cAAc,EAAE,UAAU,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QACtD,gBAAgB,CAAC,UAAU,CAAC;QAC5B,WAAW,CAAC,MAAM,CAAC;KACnB,CAAC,CAAC;IAEH,OAAO;QACN,UAAU,EAAE,cAAc;QAC1B,MAAM,EAAE,UAAU;QAClB,cAAc,EAAE,gBAAgB;QAChC,OAAO;KACP,CAAC;AACH,CAAC,CAAC;AAYF,MAAM,CAAC,MAAM,WAAW,GAAG,CAC1B,OAAsC,EACtC,MAA2C,EAC3C,SAAiB,EACP,EAAE;IACZ,IAAI,MAAM,CAAC,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC;QACtC,OAAO,KAAK,CAAC;IACd,CAAC;IACD,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE;QACtC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;QAC5B,IAAI,CAAC,KAAK,EAAE,CAAC;YACZ,OAAO,KAAK,CAAC;QACd,CAAC;QACD,OAAO,CACN,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC;YAClD,SAAS,GAAG,MAAM,CAAC,IAAI,CACvB,CAAC;IACH,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { Prng } from "@prosopo/puzzle-assets";
2
+ import type { IconOrderGeometry, IconPlacement } from "./types.js";
3
+ export interface IconLayout {
4
+ targets: IconPlacement[];
5
+ decoys: IconPlacement[];
6
+ }
7
+ export declare const placeIcons: (prng: Prng, geometry: IconOrderGeometry, targetCount: number, decoyCount: number) => IconLayout;
8
+ //# sourceMappingURL=place.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"place.d.ts","sourceRoot":"","sources":["../src/place.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAGnD,OAAO,KAAK,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAenE,MAAM,WAAW,UAAU;IAE1B,OAAO,EAAE,aAAa,EAAE,CAAC;IAEzB,MAAM,EAAE,aAAa,EAAE,CAAC;CACxB;AAyDD,eAAO,MAAM,UAAU,SAChB,IAAI,YACA,iBAAiB,eACd,MAAM,cACP,MAAM,KAChB,UAsBF,CAAC"}
package/dist/place.js ADDED
@@ -0,0 +1,68 @@
1
+ import { GLYPH_KINDS } from "./glyphs.js";
2
+ import { shuffle } from "./shuffle.js";
3
+ //#region src/place.ts
4
+ /**
5
+ * Minimum centre-to-centre distance, as a multiple of the larger icon's size,
6
+ * so a click cannot land on two icons at once.
7
+ */
8
+ var MIN_SEPARATION = 1.15;
9
+ /** Keep whole icons inside the frame; a clipped glyph is unidentifiable. */
10
+ var EDGE_MARGIN = .55;
11
+ var MAX_ATTEMPTS_PER_ICON = 60;
12
+ var SIZE_JITTER = [.85, 1.15];
13
+ var farEnough = (x, y, size, placed) => placed.every((other) => {
14
+ const need = MIN_SEPARATION * Math.max(size, other.size);
15
+ return Math.hypot(x - other.x, y - other.y) >= need;
16
+ });
17
+ /**
18
+ * The legend identifies a target by its shape, so every icon on the frame,
19
+ * target or decoy, must be a distinct glyph; hence one deck for both.
20
+ */
21
+ var drawKinds = (prng, count) => {
22
+ if (count > GLYPH_KINDS.length) throw new Error(`icon-order-assets: asked for ${count} distinct glyphs but only ${GLYPH_KINDS.length} exist`);
23
+ return shuffle(prng, GLYPH_KINDS).slice(0, count);
24
+ };
25
+ var tryPlace = (prng, geometry, kind, placed) => {
26
+ const size = geometry.iconSize * prng.range(SIZE_JITTER[0], SIZE_JITTER[1]);
27
+ const margin = size * EDGE_MARGIN;
28
+ for (let attempt = 0; attempt < MAX_ATTEMPTS_PER_ICON; attempt++) {
29
+ const x = prng.range(margin, geometry.width - margin);
30
+ const y = prng.range(margin, geometry.height - margin);
31
+ if (!farEnough(x, y, size, placed)) continue;
32
+ return {
33
+ x,
34
+ y,
35
+ kind,
36
+ rotation: prng.range(0, 360),
37
+ hue: prng.range(0, 360),
38
+ size
39
+ };
40
+ }
41
+ };
42
+ /**
43
+ * Targets are mandatory, since a frame missing one is unsolvable, so failing
44
+ * to place one throws. Decoys are best-effort and are dropped when the frame
45
+ * is too crowded.
46
+ */
47
+ var placeIcons = (prng, geometry, targetCount, decoyCount) => {
48
+ const kinds = drawKinds(prng, targetCount + decoyCount);
49
+ const placed = [];
50
+ const targets = [];
51
+ const decoys = [];
52
+ kinds.forEach((kind, index) => {
53
+ const placement = tryPlace(prng, geometry, kind, placed);
54
+ const isTarget = index < targetCount;
55
+ if (!placement) {
56
+ if (isTarget) throw new Error(`icon-order-assets: could not place target icon ${index + 1} of ${targetCount} in a ${geometry.width}x${geometry.height} frame at size ${geometry.iconSize}`);
57
+ return;
58
+ }
59
+ placed.push(placement);
60
+ (isTarget ? targets : decoys).push(placement);
61
+ });
62
+ return {
63
+ targets,
64
+ decoys
65
+ };
66
+ };
67
+ //#endregion
68
+ export { placeIcons };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"place.js","sourceRoot":"","sources":["../src/place.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { Prng } from "@prosopo/puzzle-assets";
2
+ export declare const shuffle: <T>(prng: Prng, items: readonly T[]) => T[];
3
+ //# sourceMappingURL=shuffle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shuffle.d.ts","sourceRoot":"","sources":["../src/shuffle.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAGnD,eAAO,MAAM,OAAO,GAAI,CAAC,QAAQ,IAAI,SAAS,SAAS,CAAC,EAAE,KAAG,CAAC,EAa7D,CAAC"}
@@ -0,0 +1,16 @@
1
+ //#region src/shuffle.ts
2
+ /** Fisher-Yates, so every ordering is equally likely. */
3
+ var shuffle = (prng, items) => {
4
+ const out = [...items];
5
+ for (let i = out.length - 1; i > 0; i--) {
6
+ const j = prng.int(0, i);
7
+ const a = out[i];
8
+ const b = out[j];
9
+ if (a === void 0 || b === void 0) throw new Error("icon-order-assets: shuffle index out of range");
10
+ out[i] = b;
11
+ out[j] = a;
12
+ }
13
+ return out;
14
+ };
15
+ //#endregion
16
+ export { shuffle };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shuffle.js","sourceRoot":"","sources":["../src/shuffle.ts"],"names":[],"mappings":"AAiBA,MAAM,CAAC,MAAM,OAAO,GAAG,CAAI,IAAU,EAAE,KAAmB,EAAO,EAAE;IAClE,MAAM,GAAG,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC;IACvB,KAAK,IAAI,CAAC,GAAG,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QACzC,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACzB,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;QACjB,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;QACjB,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,SAAS,EAAE,CAAC;YACxC,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;QAClE,CAAC;QACD,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QACX,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;IACD,OAAO,GAAG,CAAC;AACZ,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=iconOrderAssets.unit.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"iconOrderAssets.unit.test.d.ts","sourceRoot":"","sources":["../../src/tests/iconOrderAssets.unit.test.ts"],"names":[],"mappings":""}