@prosopo/icon-order-assets 0.2.5 → 0.2.6

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 (59) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +6 -5
  2. package/.turbo/turbo-build$colon$esm.log +7 -5
  3. package/.turbo/turbo-build$colon$tsc.log +5 -5
  4. package/CHANGELOG.md +13 -0
  5. package/dist/cjs/background.cjs +2 -2
  6. package/dist/cjs/compose.cjs +5 -95
  7. package/dist/cjs/index.cjs +6 -6
  8. package/dist/cjs/markup.cjs +97 -0
  9. package/dist/esm/background.js +1 -1
  10. package/dist/esm/browser.js +24 -0
  11. package/dist/esm/compose.js +5 -93
  12. package/dist/esm/index.js +5 -5
  13. package/dist/esm/markup.js +94 -0
  14. package/dist/tsc/background.d.ts +1 -1
  15. package/dist/tsc/background.d.ts.map +1 -1
  16. package/dist/tsc/background.js +1 -1
  17. package/dist/tsc/background.js.map +1 -1
  18. package/dist/tsc/browser.d.ts +14 -0
  19. package/dist/tsc/browser.d.ts.map +1 -0
  20. package/dist/tsc/browser.js +18 -0
  21. package/dist/tsc/browser.js.map +1 -0
  22. package/dist/tsc/compose.d.ts +1 -1
  23. package/dist/tsc/compose.d.ts.map +1 -1
  24. package/dist/tsc/compose.js +5 -77
  25. package/dist/tsc/compose.js.map +1 -1
  26. package/dist/tsc/index.d.ts +2 -2
  27. package/dist/tsc/index.d.ts.map +1 -1
  28. package/dist/tsc/index.js +5 -3
  29. package/dist/tsc/index.js.map +1 -1
  30. package/dist/tsc/markup.d.ts +7 -0
  31. package/dist/tsc/markup.d.ts.map +1 -0
  32. package/dist/tsc/markup.js +75 -0
  33. package/dist/tsc/markup.js.map +1 -0
  34. package/dist/tsc/place.d.ts +1 -1
  35. package/dist/tsc/place.d.ts.map +1 -1
  36. package/dist/tsc/shuffle.d.ts +1 -1
  37. package/dist/tsc/shuffle.d.ts.map +1 -1
  38. package/dist/tsc/tests/iconOrderAssets.unit.test.js +29 -0
  39. package/dist/tsc/tests/iconOrderAssets.unit.test.js.map +1 -1
  40. package/dist/tsc/types.d.ts +1 -0
  41. package/dist/tsc/types.d.ts.map +1 -1
  42. package/dist/types/background.d.ts +1 -1
  43. package/dist/types/background.d.ts.map +1 -1
  44. package/dist/types/browser.d.ts +14 -0
  45. package/dist/types/browser.d.ts.map +1 -0
  46. package/dist/types/compose.d.ts +1 -1
  47. package/dist/types/compose.d.ts.map +1 -1
  48. package/dist/types/index.d.ts +2 -2
  49. package/dist/types/index.d.ts.map +1 -1
  50. package/dist/types/markup.d.ts +7 -0
  51. package/dist/types/markup.d.ts.map +1 -0
  52. package/dist/types/place.d.ts +1 -1
  53. package/dist/types/place.d.ts.map +1 -1
  54. package/dist/types/shuffle.d.ts +1 -1
  55. package/dist/types/shuffle.d.ts.map +1 -1
  56. package/dist/types/types.d.ts +1 -0
  57. package/dist/types/types.d.ts.map +1 -1
  58. package/package.json +8 -3
  59. package/vite.esm.config.ts +1 -0
@@ -10,15 +10,16 @@ ViteCommonJSConfig: .
10
10
  vite v8.1.6 building ssr environment for production...
11
11
  Bundle build started
12
12
  
13
- ✓ 7 modules transformed.
13
+ ✓ 8 modules transformed.
14
14
  rendering chunks...
15
15
  computing gzip size...
16
16
  dist/cjs/shuffle.cjs 0.44 kB │ gzip: 0.29 kB
17
+ dist/cjs/compose.cjs 0.83 kB │ gzip: 0.41 kB
17
18
  dist/cjs/_virtual/_rolldown/runtime.cjs 1.00 kB │ gzip: 0.50 kB
18
19
  dist/cjs/glyphs.cjs 1.56 kB │ gzip: 0.77 kB
19
20
  dist/cjs/place.cjs 2.46 kB │ gzip: 1.12 kB
20
- dist/cjs/index.cjs 2.69 kB │ gzip: 1.12 kB
21
- dist/cjs/background.cjs 4.09 kB │ gzip: 1.42 kB
22
- dist/cjs/compose.cjs 4.69 kB │ gzip: 2.02 kB
21
+ dist/cjs/index.cjs 2.70 kB │ gzip: 1.11 kB
22
+ dist/cjs/background.cjs 4.11 kB │ gzip: 1.43 kB
23
+ dist/cjs/markup.cjs 4.32 kB │ gzip: 1.89 kB
23
24
 
24
- ✓ built in 54ms
25
+ ✓ built in 44ms
@@ -10,15 +10,17 @@ ViteEsmConfig: .
10
10
  vite v8.1.6 building ssr environment for production...
11
11
  Bundle build started
12
12
  
13
- ✓ 7 modules transformed.
13
+ ✓ 9 modules transformed.
14
14
  rendering chunks...
15
15
  computing gzip size...
16
16
  dist/esm/_virtual/_rolldown/runtime.js 0.04 kB │ gzip: 0.06 kB
17
17
  dist/esm/shuffle.js 0.43 kB │ gzip: 0.29 kB
18
+ dist/esm/compose.js 0.64 kB │ gzip: 0.35 kB
19
+ dist/esm/browser.js 1.02 kB │ gzip: 0.54 kB
18
20
  dist/esm/glyphs.js 1.51 kB │ gzip: 0.76 kB
19
- dist/esm/index.js 2.21 kB │ gzip: 1.03 kB
21
+ dist/esm/index.js 2.20 kB │ gzip: 1.02 kB
20
22
  dist/esm/place.js 2.38 kB │ gzip: 1.11 kB
21
- dist/esm/background.js 4.03 kB │ gzip: 1.40 kB
22
- dist/esm/compose.js 4.35 kB │ gzip: 1.92 kB
23
+ dist/esm/background.js 4.04 kB │ gzip: 1.41 kB
24
+ dist/esm/markup.js 4.11 kB │ gzip: 1.86 kB
23
25
 
24
- ✓ built in 46ms
26
+ ✓ built in 224ms
@@ -1,14 +1,14 @@
1
1
  $ tsc --build --verbose
2
- 10:55:01 AM - Projects in this build:
2
+ 10:05:54 AM - Projects in this build:
3
3
  * ../../dev/config/tsconfig.json
4
4
  * ../puzzle-assets/tsconfig.json
5
5
  * tsconfig.json
6
6
 
7
- 10:55:02 AM - Project '../../dev/config/tsconfig.json' is up to date because newest input '../../dev/config/src/vite/modifyExportsFiles.test.ts' is older than output '../../dev/config/dist/tsc/tsconfig.tsbuildinfo'
7
+ 10:05:54 AM - Project '../../dev/config/tsconfig.json' is up to date because newest input '../../dev/config/src/webpack/webpack.config.ts' is older than output '../../dev/config/dist/tsc/tsconfig.tsbuildinfo'
8
8
 
9
- 10:55:02 AM - Project '../puzzle-assets/tsconfig.json' is up to date because newest input '../puzzle-assets/src/types.ts' is older than output '../puzzle-assets/dist/tsc/tsconfig.tsbuildinfo'
9
+ 10:05:54 AM - Project '../puzzle-assets/tsconfig.json' is up to date because newest input '../puzzle-assets/src/types.ts' is older than output '../puzzle-assets/dist/tsc/tsconfig.tsbuildinfo'
10
10
 
11
- 10:55:02 AM - Project 'tsconfig.json' is out of date because output file 'dist/tsc/tsconfig.tsbuildinfo' does not exist
11
+ 10:05:54 AM - Project 'tsconfig.json' is out of date because output file 'dist/tsc/tsconfig.tsbuildinfo' does not exist
12
12
 
13
- 10:55:02 AM - Building project '/home/runner/work/captcha/captcha/packages/icon-order-assets/tsconfig.json'...
13
+ 10:05:54 AM - Building project '/home/runner/work/captcha/captcha/packages/icon-order-assets/tsconfig.json'...
14
14
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # @prosopo/icon-order-assets
2
2
 
3
+ ## 0.2.6
4
+ ### Patch Changes
5
+
6
+ - e1cb7f4: Icon order challenges can now be redrawn exactly, the way puzzle challenges already can.
7
+
8
+ - The provider saves what it needs to redraw each icon order challenge on its record: the render seed, the frame geometry and the render settings (`render`).
9
+ - `@prosopo/icon-order-assets/browser` is a new entry that rebuilds a challenge from those inputs without `sharp`, so the portal can show the image a visitor was served. It returns the frame and the target strip as SVG plus the grain to lay over the frame. Rasterised the same way, the frame matches the served one byte for byte.
10
+ - `@prosopo/puzzle-assets/browser` also exports `hslToRgb` and the `Prng` type, which the new entry uses.
11
+
12
+ The seed regenerates the answer too, so like the puzzle seeds it is never sent to a client.
13
+ - Updated dependencies [e1cb7f4]
14
+ - @prosopo/puzzle-assets@0.2.7
15
+
3
16
  ## 0.2.5
4
17
  ### Patch Changes
5
18
 
@@ -1,4 +1,4 @@
1
- let _prosopo_puzzle_assets = require("@prosopo/puzzle-assets");
1
+ let _prosopo_puzzle_assets_browser = require("@prosopo/puzzle-assets/browser");
2
2
  //#region src/background.ts
3
3
  /**
4
4
  * A hard-edged collage rather than a smooth gradient: on a gradient every icon
@@ -7,7 +7,7 @@ let _prosopo_puzzle_assets = require("@prosopo/puzzle-assets");
7
7
  */
8
8
  var round = (value) => Math.round(value * 100) / 100;
9
9
  var rgb = (hue, saturation, lightness) => {
10
- const { r, g, b } = (0, _prosopo_puzzle_assets.hslToRgb)(hue, saturation, lightness);
10
+ const { r, g, b } = (0, _prosopo_puzzle_assets_browser.hslToRgb)(hue, saturation, lightness);
11
11
  return `rgb(${r},${g},${b})`;
12
12
  };
13
13
  var panels = (prng, geometry, hues, count) => {
@@ -1,108 +1,18 @@
1
1
  const require_runtime = require("./_virtual/_rolldown/runtime.cjs");
2
- const require_background = require("./background.cjs");
3
- const require_glyphs = require("./glyphs.cjs");
4
- const require_shuffle = require("./shuffle.cjs");
5
- let _prosopo_puzzle_assets = require("@prosopo/puzzle-assets");
2
+ const require_markup = require("./markup.cjs");
6
3
  let sharp = require("sharp");
7
4
  sharp = require_runtime.__toESM(sharp, 1);
8
5
  //#region src/compose.ts
9
- /** Glyph path data is authored in this box; see glyphs.ts. */
10
- var GLYPH_BOX = 100;
11
- /**
12
- * Width of the dark outline stroke relative to the bright one. Narrower and
13
- * icons vanish over light regions; wider and the halo becomes the strongest
14
- * edge in the frame, which is what a solver looks for.
15
- */
16
- var HALO_WIDTH_MULTIPLE = 2.4;
17
- var HALO_COLOUR = "rgb(12,14,22)";
18
- var LEGEND_CHIP_COLOUR = "rgb(24,27,38)";
19
- var LEGEND_GAP = 6;
20
- var LEGEND_GLYPH_INSET = .62;
21
- var round = (value) => Math.round(value * 100) / 100;
22
- var strokeColour = (hue) => {
23
- const { r, g, b } = (0, _prosopo_puzzle_assets.hslToRgb)(hue, .86, .64);
24
- return `rgb(${r},${g},${b})`;
25
- };
26
- /**
27
- * Converts a frame-pixel stroke width into glyph-box units, so line weight
28
- * does not vary with (and so leak) the icon's size.
29
- */
30
- var strokeUnits = (widthPx, size) => widthPx * GLYPH_BOX / size;
31
- var glyphMarkup = (placement, settings, rotationOverride) => {
32
- const scale = placement.size / GLYPH_BOX;
33
- const rotation = rotationOverride ?? placement.rotation;
34
- const inner = strokeUnits(settings.strokeWidth, placement.size);
35
- const halo = inner * HALO_WIDTH_MULTIPLE;
36
- const path = require_glyphs.glyphPath(placement.kind);
37
- return [
38
- `<g transform="${[
39
- `translate(${round(placement.x)} ${round(placement.y)})`,
40
- `rotate(${round(rotation)})`,
41
- `scale(${round(scale)})`,
42
- `translate(${-GLYPH_BOX / 2} ${-GLYPH_BOX / 2})`
43
- ].join(" ")}" fill="none" stroke-linecap="round" stroke-linejoin="round">`,
44
- `<path d="${path}" stroke="${HALO_COLOUR}" stroke-opacity="${round(settings.haloOpacity)}" stroke-width="${round(halo)}"/>`,
45
- `<path d="${path}" stroke="${strokeColour(placement.hue)}" stroke-opacity="${round(settings.iconOpacity)}" stroke-width="${round(inner)}"/>`,
46
- "</g>"
47
- ].join("");
48
- };
49
- 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>`);
50
- /**
51
- * Draw order is shuffled here because callers hold targets and decoys as
52
- * separate lists, and concatenating them would paint every target on top
53
- * wherever icons overlap — a tell that needs no shape recognition.
54
- */
55
- var compositeIcons = async (prng, icons, geometry, settings) => {
56
- const body = require_shuffle.shuffle(prng, icons).map((placement) => glyphMarkup(placement, settings)).join("");
57
- const document = svgDocument(geometry.width, geometry.height, require_background.collageMarkup(prng, geometry, settings.backgroundClutter) + body);
58
- const { data, info } = await (0, sharp.default)(document).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
59
- return addGrain(prng, {
60
- data,
61
- width: info.width,
62
- height: info.height
63
- });
64
- };
65
- /**
66
- * Noise over the flat vector fills, so region boundaries are not perfectly
67
- * clean input for an edge detector.
68
- */
69
- var GRAIN_AMPLITUDE = 6;
70
- var addGrain = (prng, image) => {
71
- const { data } = image;
72
- for (let i = 0; i < data.length; i += 4) {
73
- const noise = Math.round((prng.next() - .5) * 2 * GRAIN_AMPLITUDE);
74
- for (let channel = 0; channel < 3; channel++) {
75
- const value = (data[i + channel] ?? 0) + noise;
76
- data[i + channel] = value < 0 ? 0 : value > 255 ? 255 : value;
77
- }
78
- }
79
- return image;
80
- };
81
- /**
82
- * Legend glyphs are drawn upright whatever their rotation on the frame, so a
83
- * template-matching solver is not handed a rotation-aligned crib. The hue is
84
- * kept: it helps a human and a solver already has it from the pixels.
85
- */
86
- var renderLegend = async (targets, legendIconSize, settings) => {
87
- const step = legendIconSize + LEGEND_GAP;
88
- const width = Math.max(legendIconSize, targets.length * step - (targets.length > 0 ? LEGEND_GAP : 0));
89
- const radius = legendIconSize / 2;
90
- const body = targets.map((target, index) => {
91
- const cx = index * step + radius;
92
- return `<circle cx="${round(cx)}" cy="${round(radius)}" r="${round(radius)}" fill="${LEGEND_CHIP_COLOUR}"/>` + glyphMarkup({
93
- ...target,
94
- x: cx,
95
- y: radius,
96
- size: legendIconSize * LEGEND_GLYPH_INSET
97
- }, settings, 0);
98
- }).join("");
99
- const { data, info } = await (0, sharp.default)(svgDocument(width, legendIconSize, body)).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
6
+ var rasterise = async (svg) => {
7
+ const { data, info } = await (0, sharp.default)(Buffer.from(svg)).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
100
8
  return {
101
9
  data,
102
10
  width: info.width,
103
11
  height: info.height
104
12
  };
105
13
  };
14
+ var compositeIcons = async (prng, icons, geometry, settings) => require_markup.addGrain(prng, await rasterise(require_markup.frameMarkup(prng, icons, geometry, settings)));
15
+ var renderLegend = (targets, legendIconSize, settings) => rasterise(require_markup.legendMarkup(targets, legendIconSize, settings));
106
16
  //#endregion
107
17
  exports.compositeIcons = compositeIcons;
108
18
  exports.renderLegend = renderLegend;
@@ -1,5 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_glyphs = require("./glyphs.cjs");
3
+ const require_markup = require("./markup.cjs");
3
4
  const require_compose = require("./compose.cjs");
4
5
  const require_place = require("./place.cjs");
5
6
  let _prosopo_puzzle_assets = require("@prosopo/puzzle-assets");
@@ -19,15 +20,13 @@ var DEFAULT_RENDER_SETTINGS = {
19
20
  haloOpacity: .7,
20
21
  backgroundClutter: 8
21
22
  };
22
- /** Fixed because the widget lays the legend out against its own header. */
23
- var LEGEND_ICON_SIZE = 26;
24
23
  /**
25
24
  * The background is drawn fresh per call and never reused: two layouts on the
26
25
  * same background could be diffed to read both answers. The returned
27
26
  * `targets` are the answer and must never be serialised into a response.
28
27
  */
29
- var renderIconOrder = async (geometry = DEFAULT_GEOMETRY, settings = DEFAULT_RENDER_SETTINGS) => {
30
- const prng = (0, _prosopo_puzzle_assets.createPrng)((0, _prosopo_puzzle_assets.createSeed)());
28
+ var renderIconOrder = async (geometry = DEFAULT_GEOMETRY, settings = DEFAULT_RENDER_SETTINGS, seed = (0, _prosopo_puzzle_assets.createSeed)()) => {
29
+ const prng = (0, _prosopo_puzzle_assets.createPrng)(seed);
31
30
  const { targets, decoys } = require_place.placeIcons(prng, geometry, settings.targetCount, settings.decoyCount);
32
31
  const composited = await require_compose.compositeIcons(prng, [...targets, ...decoys], geometry, settings);
33
32
  const legend = await require_compose.renderLegend(targets, 26, settings);
@@ -36,7 +35,8 @@ var renderIconOrder = async (geometry = DEFAULT_GEOMETRY, settings = DEFAULT_REN
36
35
  background: backgroundWebp,
37
36
  legend: legendWebp,
38
37
  legendIconSize: 26,
39
- targets
38
+ targets,
39
+ seed: seed.toString("hex")
40
40
  };
41
41
  };
42
42
  /**
@@ -55,7 +55,7 @@ exports.DEFAULT_GEOMETRY = DEFAULT_GEOMETRY;
55
55
  exports.DEFAULT_RENDER_SETTINGS = DEFAULT_RENDER_SETTINGS;
56
56
  exports.GLYPH_KINDS = require_glyphs.GLYPH_KINDS;
57
57
  exports.GlyphKind = require_glyphs.GlyphKind;
58
- exports.LEGEND_ICON_SIZE = LEGEND_ICON_SIZE;
58
+ exports.LEGEND_ICON_SIZE = require_markup.LEGEND_ICON_SIZE;
59
59
  exports.compositeIcons = require_compose.compositeIcons;
60
60
  exports.glyphPath = require_glyphs.glyphPath;
61
61
  exports.gradeClicks = gradeClicks;
@@ -0,0 +1,97 @@
1
+ const require_background = require("./background.cjs");
2
+ const require_glyphs = require("./glyphs.cjs");
3
+ const require_shuffle = require("./shuffle.cjs");
4
+ let _prosopo_puzzle_assets_browser = require("@prosopo/puzzle-assets/browser");
5
+ //#region src/markup.ts
6
+ /** Glyph path data is authored in this box; see glyphs.ts. */
7
+ var GLYPH_BOX = 100;
8
+ /**
9
+ * Width of the dark outline stroke relative to the bright one. Narrower and
10
+ * icons vanish over light regions; wider and the halo becomes the strongest
11
+ * edge in the frame, which is what a solver looks for.
12
+ */
13
+ var HALO_WIDTH_MULTIPLE = 2.4;
14
+ var HALO_COLOUR = "rgb(12,14,22)";
15
+ /** Fixed because the widget lays the legend out against its own header. */
16
+ var LEGEND_ICON_SIZE = 26;
17
+ var LEGEND_CHIP_COLOUR = "rgb(24,27,38)";
18
+ var LEGEND_GAP = 6;
19
+ var LEGEND_GLYPH_INSET = .62;
20
+ var round = (value) => Math.round(value * 100) / 100;
21
+ var strokeColour = (hue) => {
22
+ const { r, g, b } = (0, _prosopo_puzzle_assets_browser.hslToRgb)(hue, .86, .64);
23
+ return `rgb(${r},${g},${b})`;
24
+ };
25
+ /**
26
+ * Converts a frame-pixel stroke width into glyph-box units, so line weight
27
+ * does not vary with (and so leak) the icon's size.
28
+ */
29
+ var strokeUnits = (widthPx, size) => widthPx * GLYPH_BOX / size;
30
+ var glyphMarkup = (placement, settings, rotationOverride) => {
31
+ const scale = placement.size / GLYPH_BOX;
32
+ const rotation = rotationOverride ?? placement.rotation;
33
+ const inner = strokeUnits(settings.strokeWidth, placement.size);
34
+ const halo = inner * HALO_WIDTH_MULTIPLE;
35
+ const path = require_glyphs.glyphPath(placement.kind);
36
+ return [
37
+ `<g transform="${[
38
+ `translate(${round(placement.x)} ${round(placement.y)})`,
39
+ `rotate(${round(rotation)})`,
40
+ `scale(${round(scale)})`,
41
+ `translate(${-GLYPH_BOX / 2} ${-GLYPH_BOX / 2})`
42
+ ].join(" ")}" fill="none" stroke-linecap="round" stroke-linejoin="round">`,
43
+ `<path d="${path}" stroke="${HALO_COLOUR}" stroke-opacity="${round(settings.haloOpacity)}" stroke-width="${round(halo)}"/>`,
44
+ `<path d="${path}" stroke="${strokeColour(placement.hue)}" stroke-opacity="${round(settings.iconOpacity)}" stroke-width="${round(inner)}"/>`,
45
+ "</g>"
46
+ ].join("");
47
+ };
48
+ var svgDocument = (width, height, body) => `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">${body}</svg>`;
49
+ /**
50
+ * Draw order is shuffled here because callers hold targets and decoys as
51
+ * separate lists, and concatenating them would paint every target on top
52
+ * wherever icons overlap — a tell that needs no shape recognition.
53
+ */
54
+ var frameMarkup = (prng, icons, geometry, settings) => {
55
+ const body = require_shuffle.shuffle(prng, icons).map((placement) => glyphMarkup(placement, settings)).join("");
56
+ return svgDocument(geometry.width, geometry.height, require_background.collageMarkup(prng, geometry, settings.backgroundClutter) + body);
57
+ };
58
+ /**
59
+ * Noise over the flat vector fills, so region boundaries are not perfectly
60
+ * clean input for an edge detector.
61
+ */
62
+ var GRAIN_AMPLITUDE = 6;
63
+ var addGrain = (prng, image) => {
64
+ const { data } = image;
65
+ for (let i = 0; i < data.length; i += 4) {
66
+ const noise = Math.round((prng.next() - .5) * 2 * GRAIN_AMPLITUDE);
67
+ for (let channel = 0; channel < 3; channel++) {
68
+ const value = (data[i + channel] ?? 0) + noise;
69
+ data[i + channel] = value < 0 ? 0 : value > 255 ? 255 : value;
70
+ }
71
+ }
72
+ return image;
73
+ };
74
+ /**
75
+ * Legend glyphs are drawn upright whatever their rotation on the frame, so a
76
+ * template-matching solver is not handed a rotation-aligned crib. The hue is
77
+ * kept: it helps a human and a solver already has it from the pixels.
78
+ */
79
+ var legendMarkup = (targets, legendIconSize, settings) => {
80
+ const step = legendIconSize + LEGEND_GAP;
81
+ const width = Math.max(legendIconSize, targets.length * step - (targets.length > 0 ? LEGEND_GAP : 0));
82
+ const radius = legendIconSize / 2;
83
+ return svgDocument(width, legendIconSize, targets.map((target, index) => {
84
+ const cx = index * step + radius;
85
+ return `<circle cx="${round(cx)}" cy="${round(radius)}" r="${round(radius)}" fill="${LEGEND_CHIP_COLOUR}"/>` + glyphMarkup({
86
+ ...target,
87
+ x: cx,
88
+ y: radius,
89
+ size: legendIconSize * LEGEND_GLYPH_INSET
90
+ }, settings, 0);
91
+ }).join(""));
92
+ };
93
+ //#endregion
94
+ exports.LEGEND_ICON_SIZE = LEGEND_ICON_SIZE;
95
+ exports.addGrain = addGrain;
96
+ exports.frameMarkup = frameMarkup;
97
+ exports.legendMarkup = legendMarkup;
@@ -1,4 +1,4 @@
1
- import { hslToRgb } from "@prosopo/puzzle-assets";
1
+ import { hslToRgb } from "@prosopo/puzzle-assets/browser";
2
2
  //#region src/background.ts
3
3
  /**
4
4
  * A hard-edged collage rather than a smooth gradient: on a gradient every icon
@@ -0,0 +1,24 @@
1
+ import "./_virtual/_rolldown/runtime.js";
2
+ import { GLYPH_KINDS, GlyphKind, glyphPath } from "./glyphs.js";
3
+ import { LEGEND_ICON_SIZE, addGrain, frameMarkup, legendMarkup } from "./markup.js";
4
+ import { placeIcons } from "./place.js";
5
+ import { createPrng } from "@prosopo/puzzle-assets/browser";
6
+ //#region src/browser.ts
7
+ /**
8
+ * Rebuilds what the provider drew for `seed`. A browser rasterises SVG a
9
+ * little differently from sharp, so the result matches the served image
10
+ * closely rather than pixel for pixel.
11
+ */
12
+ var redrawIconOrder = (seed, geometry, settings) => {
13
+ const prng = createPrng(Buffer.from(seed, "hex"));
14
+ const { targets, decoys } = placeIcons(prng, geometry, settings.targetCount, settings.decoyCount);
15
+ return {
16
+ frame: frameMarkup(prng, [...targets, ...decoys], geometry, settings),
17
+ legend: legendMarkup(targets, 26, settings),
18
+ legendIconSize: 26,
19
+ targets,
20
+ addGrain: (image) => addGrain(prng, image)
21
+ };
22
+ };
23
+ //#endregion
24
+ export { GLYPH_KINDS, GlyphKind, LEGEND_ICON_SIZE, glyphPath, redrawIconOrder };
@@ -1,103 +1,15 @@
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";
1
+ import { addGrain, frameMarkup, legendMarkup } from "./markup.js";
5
2
  import sharp from "sharp";
6
3
  //#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 });
4
+ var rasterise = async (svg) => {
5
+ const { data, info } = await sharp(Buffer.from(svg)).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
96
6
  return {
97
7
  data,
98
8
  width: info.width,
99
9
  height: info.height
100
10
  };
101
11
  };
12
+ var compositeIcons = async (prng, icons, geometry, settings) => addGrain(prng, await rasterise(frameMarkup(prng, icons, geometry, settings)));
13
+ var renderLegend = (targets, legendIconSize, settings) => rasterise(legendMarkup(targets, legendIconSize, settings));
102
14
  //#endregion
103
15
  export { compositeIcons, renderLegend };
package/dist/esm/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import "./_virtual/_rolldown/runtime.js";
2
2
  import { GLYPH_KINDS, GlyphKind, glyphPath } from "./glyphs.js";
3
+ import { LEGEND_ICON_SIZE } from "./markup.js";
3
4
  import { compositeIcons, renderLegend } from "./compose.js";
4
5
  import { placeIcons } from "./place.js";
5
6
  import { createPrng, createSeed, encodeBackground, encodePiece } from "@prosopo/puzzle-assets";
@@ -19,15 +20,13 @@ var DEFAULT_RENDER_SETTINGS = {
19
20
  haloOpacity: .7,
20
21
  backgroundClutter: 8
21
22
  };
22
- /** Fixed because the widget lays the legend out against its own header. */
23
- var LEGEND_ICON_SIZE = 26;
24
23
  /**
25
24
  * The background is drawn fresh per call and never reused: two layouts on the
26
25
  * same background could be diffed to read both answers. The returned
27
26
  * `targets` are the answer and must never be serialised into a response.
28
27
  */
29
- var renderIconOrder = async (geometry = DEFAULT_GEOMETRY, settings = DEFAULT_RENDER_SETTINGS) => {
30
- const prng = createPrng(createSeed());
28
+ var renderIconOrder = async (geometry = DEFAULT_GEOMETRY, settings = DEFAULT_RENDER_SETTINGS, seed = createSeed()) => {
29
+ const prng = createPrng(seed);
31
30
  const { targets, decoys } = placeIcons(prng, geometry, settings.targetCount, settings.decoyCount);
32
31
  const composited = await compositeIcons(prng, [...targets, ...decoys], geometry, settings);
33
32
  const legend = await renderLegend(targets, 26, settings);
@@ -36,7 +35,8 @@ var renderIconOrder = async (geometry = DEFAULT_GEOMETRY, settings = DEFAULT_REN
36
35
  background: backgroundWebp,
37
36
  legend: legendWebp,
38
37
  legendIconSize: 26,
39
- targets
38
+ targets,
39
+ seed: seed.toString("hex")
40
40
  };
41
41
  };
42
42
  /**
@@ -0,0 +1,94 @@
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/browser";
5
+ //#region src/markup.ts
6
+ /** Glyph path data is authored in this box; see glyphs.ts. */
7
+ var GLYPH_BOX = 100;
8
+ /**
9
+ * Width of the dark outline stroke relative to the bright one. Narrower and
10
+ * icons vanish over light regions; wider and the halo becomes the strongest
11
+ * edge in the frame, which is what a solver looks for.
12
+ */
13
+ var HALO_WIDTH_MULTIPLE = 2.4;
14
+ var HALO_COLOUR = "rgb(12,14,22)";
15
+ /** Fixed because the widget lays the legend out against its own header. */
16
+ var LEGEND_ICON_SIZE = 26;
17
+ var LEGEND_CHIP_COLOUR = "rgb(24,27,38)";
18
+ var LEGEND_GAP = 6;
19
+ var LEGEND_GLYPH_INSET = .62;
20
+ var round = (value) => Math.round(value * 100) / 100;
21
+ var strokeColour = (hue) => {
22
+ const { r, g, b } = hslToRgb(hue, .86, .64);
23
+ return `rgb(${r},${g},${b})`;
24
+ };
25
+ /**
26
+ * Converts a frame-pixel stroke width into glyph-box units, so line weight
27
+ * does not vary with (and so leak) the icon's size.
28
+ */
29
+ var strokeUnits = (widthPx, size) => widthPx * GLYPH_BOX / size;
30
+ var glyphMarkup = (placement, settings, rotationOverride) => {
31
+ const scale = placement.size / GLYPH_BOX;
32
+ const rotation = rotationOverride ?? placement.rotation;
33
+ const inner = strokeUnits(settings.strokeWidth, placement.size);
34
+ const halo = inner * HALO_WIDTH_MULTIPLE;
35
+ const path = glyphPath(placement.kind);
36
+ return [
37
+ `<g transform="${[
38
+ `translate(${round(placement.x)} ${round(placement.y)})`,
39
+ `rotate(${round(rotation)})`,
40
+ `scale(${round(scale)})`,
41
+ `translate(${-GLYPH_BOX / 2} ${-GLYPH_BOX / 2})`
42
+ ].join(" ")}" fill="none" stroke-linecap="round" stroke-linejoin="round">`,
43
+ `<path d="${path}" stroke="${HALO_COLOUR}" stroke-opacity="${round(settings.haloOpacity)}" stroke-width="${round(halo)}"/>`,
44
+ `<path d="${path}" stroke="${strokeColour(placement.hue)}" stroke-opacity="${round(settings.iconOpacity)}" stroke-width="${round(inner)}"/>`,
45
+ "</g>"
46
+ ].join("");
47
+ };
48
+ var svgDocument = (width, height, body) => `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">${body}</svg>`;
49
+ /**
50
+ * Draw order is shuffled here because callers hold targets and decoys as
51
+ * separate lists, and concatenating them would paint every target on top
52
+ * wherever icons overlap — a tell that needs no shape recognition.
53
+ */
54
+ var frameMarkup = (prng, icons, geometry, settings) => {
55
+ const body = shuffle(prng, icons).map((placement) => glyphMarkup(placement, settings)).join("");
56
+ return svgDocument(geometry.width, geometry.height, collageMarkup(prng, geometry, settings.backgroundClutter) + body);
57
+ };
58
+ /**
59
+ * Noise over the flat vector fills, so region boundaries are not perfectly
60
+ * clean input for an edge detector.
61
+ */
62
+ var GRAIN_AMPLITUDE = 6;
63
+ var addGrain = (prng, image) => {
64
+ const { data } = image;
65
+ for (let i = 0; i < data.length; i += 4) {
66
+ const noise = Math.round((prng.next() - .5) * 2 * GRAIN_AMPLITUDE);
67
+ for (let channel = 0; channel < 3; channel++) {
68
+ const value = (data[i + channel] ?? 0) + noise;
69
+ data[i + channel] = value < 0 ? 0 : value > 255 ? 255 : value;
70
+ }
71
+ }
72
+ return image;
73
+ };
74
+ /**
75
+ * Legend glyphs are drawn upright whatever their rotation on the frame, so a
76
+ * template-matching solver is not handed a rotation-aligned crib. The hue is
77
+ * kept: it helps a human and a solver already has it from the pixels.
78
+ */
79
+ var legendMarkup = (targets, legendIconSize, settings) => {
80
+ const step = legendIconSize + LEGEND_GAP;
81
+ const width = Math.max(legendIconSize, targets.length * step - (targets.length > 0 ? LEGEND_GAP : 0));
82
+ const radius = legendIconSize / 2;
83
+ return svgDocument(width, legendIconSize, targets.map((target, index) => {
84
+ const cx = index * step + radius;
85
+ return `<circle cx="${round(cx)}" cy="${round(radius)}" r="${round(radius)}" fill="${LEGEND_CHIP_COLOUR}"/>` + glyphMarkup({
86
+ ...target,
87
+ x: cx,
88
+ y: radius,
89
+ size: legendIconSize * LEGEND_GLYPH_INSET
90
+ }, settings, 0);
91
+ }).join(""));
92
+ };
93
+ //#endregion
94
+ export { LEGEND_ICON_SIZE, addGrain, frameMarkup, legendMarkup };
@@ -1,4 +1,4 @@
1
- import { type Prng } from "@prosopo/puzzle-assets";
1
+ import { type Prng } from "@prosopo/puzzle-assets/browser";
2
2
  import type { IconOrderGeometry } from "./types.js";
3
3
  export declare const collageMarkup: (prng: Prng, geometry: IconOrderGeometry, clutter: number) => string;
4
4
  //# sourceMappingURL=background.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"background.d.ts","sourceRoot":"","sources":["../../src/background.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,KAAK,IAAI,EAAY,MAAM,wBAAwB,CAAC;AAC7D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AA6GpD,eAAO,MAAM,aAAa,SACnB,IAAI,YACA,iBAAiB,WAClB,MAAM,KACb,MA0BF,CAAC"}
1
+ {"version":3,"file":"background.d.ts","sourceRoot":"","sources":["../../src/background.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,KAAK,IAAI,EAAY,MAAM,gCAAgC,CAAC;AACrE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AA6GpD,eAAO,MAAM,aAAa,SACnB,IAAI,YACA,iBAAiB,WAClB,MAAM,KACb,MA0BF,CAAC"}
@@ -1,4 +1,4 @@
1
- import { hslToRgb } from "@prosopo/puzzle-assets";
1
+ import { hslToRgb } from "@prosopo/puzzle-assets/browser";
2
2
  const round = (value) => Math.round(value * 100) / 100;
3
3
  const rgb = (hue, saturation, lightness) => {
4
4
  const { r, g, b } = hslToRgb(hue, saturation, lightness);