@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.
- package/.turbo/turbo-build$colon$cjs.log +24 -0
- package/.turbo/turbo-build$colon$tsc.log +14 -0
- package/.turbo/turbo-build.log +25 -0
- package/CHANGELOG.md +21 -0
- package/LICENSE +201 -0
- package/dist/_virtual/_rolldown/runtime.js +3 -0
- package/dist/background.d.ts +4 -0
- package/dist/background.d.ts.map +1 -0
- package/dist/background.js +87 -0
- package/dist/background.js.map +1 -0
- package/dist/cjs/_virtual/_rolldown/runtime.cjs +23 -0
- package/dist/cjs/background.cjs +87 -0
- package/dist/cjs/compose.cjs +108 -0
- package/dist/cjs/glyphs.cjs +41 -0
- package/dist/cjs/index.cjs +64 -0
- package/dist/cjs/place.cjs +68 -0
- package/dist/cjs/shuffle.cjs +16 -0
- package/dist/compose.d.ts +5 -0
- package/dist/compose.d.ts.map +1 -0
- package/dist/compose.js +103 -0
- package/dist/compose.js.map +1 -0
- package/dist/glyphs.d.ts +15 -0
- package/dist/glyphs.d.ts.map +1 -0
- package/dist/glyphs.js +39 -0
- package/dist/glyphs.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +54 -0
- package/dist/index.js.map +1 -0
- package/dist/place.d.ts +8 -0
- package/dist/place.d.ts.map +1 -0
- package/dist/place.js +68 -0
- package/dist/place.js.map +1 -0
- package/dist/shuffle.d.ts +3 -0
- package/dist/shuffle.d.ts.map +1 -0
- package/dist/shuffle.js +16 -0
- package/dist/shuffle.js.map +1 -0
- package/dist/tests/iconOrderAssets.unit.test.d.ts +2 -0
- package/dist/tests/iconOrderAssets.unit.test.d.ts.map +1 -0
- package/dist/tests/iconOrderAssets.unit.test.js +202 -0
- package/dist/tests/iconOrderAssets.unit.test.js.map +1 -0
- package/dist/types.d.ts +29 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +53 -3
- package/scripts/preview.ts +73 -0
- package/vite.cjs.config.ts +23 -0
- package/vite.esm.config.ts +22 -0
- package/vite.test.config.ts +32 -0
- 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"}
|
package/dist/compose.js
ADDED
|
@@ -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":"AAcA,OAAO,EAA6B,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAC7E,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAQvC,MAAM,SAAS,GAAG,GAAG,CAAC;AAOtB,MAAM,mBAAmB,GAAG,GAAG,CAAC;AAEhC,MAAM,WAAW,GAAG,eAAe,CAAC;AAEpC,MAAM,kBAAkB,GAAG,eAAe,CAAC;AAC3C,MAAM,UAAU,GAAG,CAAC,CAAC;AACrB,MAAM,kBAAkB,GAAG,IAAI,CAAC;AAEhC,MAAM,KAAK,GAAG,CAAC,KAAa,EAAU,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC;AAEvE,MAAM,YAAY,GAAG,CAAC,GAAW,EAAU,EAAE;IAC5C,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,QAAQ,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IAC9C,OAAO,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;AAC9B,CAAC,CAAC;AAMF,MAAM,WAAW,GAAG,CAAC,OAAe,EAAE,IAAY,EAAU,EAAE,CAC7D,CAAC,OAAO,GAAG,SAAS,CAAC,GAAG,IAAI,CAAC;AAE9B,MAAM,WAAW,GAAG,CACnB,SAAwB,EACxB,QAAiC,EACjC,gBAAyB,EAChB,EAAE;IACX,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,GAAG,SAAS,CAAC;IACzC,MAAM,QAAQ,GAAG,gBAAgB,IAAI,SAAS,CAAC,QAAQ,CAAC;IACxD,MAAM,KAAK,GAAG,WAAW,CAAC,QAAQ,CAAC,WAAW,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;IAChE,MAAM,IAAI,GAAG,KAAK,GAAG,mBAAmB,CAAC;IACzC,MAAM,IAAI,GAAG,SAAS,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IACvC,MAAM,SAAS,GAAG;QACjB,aAAa,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG;QACxD,UAAU,KAAK,CAAC,QAAQ,CAAC,GAAG;QAC5B,SAAS,KAAK,CAAC,KAAK,CAAC,GAAG;QACxB,aAAa,CAAC,SAAS,GAAG,CAAC,IAAI,CAAC,SAAS,GAAG,CAAC,GAAG;KAChD,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,OAAO;QACN,iBAAiB,SAAS,+DAA+D;QACzF,YAAY,IAAI,aAAa,WAAW,qBAAqB,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC,mBAAmB,KAAK,CAAC,IAAI,CAAC,KAAK;QAC3H,YAAY,IAAI,aAAa,YAAY,CAAC,SAAS,CAAC,GAAG,CAAC,qBAAqB,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC,mBAAmB,KAAK,CAAC,KAAK,CAAC,KAAK;QAC5I,MAAM;KACN,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACZ,CAAC,CAAC;AAEF,MAAM,WAAW,GAAG,CAAC,KAAa,EAAE,MAAc,EAAE,IAAY,EAAU,EAAE,CAC3E,MAAM,CAAC,IAAI,CACV,kDAAkD,KAAK,aAAa,MAAM,kBAAkB,KAAK,IAAI,MAAM,KAAK,IAAI,QAAQ,CAC5H,CAAC;AAOH,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,EAClC,IAAU,EACV,KAA+B,EAC/B,QAA2B,EAC3B,QAAiC,EACZ,EAAE;IACvB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC;SAC/B,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,WAAW,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;SACpD,IAAI,CAAC,EAAE,CAAC,CAAC;IAEX,MAAM,QAAQ,GAAG,WAAW,CAC3B,QAAQ,CAAC,KAAK,EACd,QAAQ,CAAC,MAAM,EACf,aAAa,CAAC,IAAI,EAAE,QAAQ,EAAE,QAAQ,CAAC,iBAAiB,CAAC,GAAG,IAAI,CAChE,CAAC;IACF,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,MAAM,KAAK,CAAC,QAAQ,CAAC;SAC1C,WAAW,EAAE;SACb,GAAG,EAAE;SACL,QAAQ,CAAC,EAAE,iBAAiB,EAAE,IAAI,EAAE,CAAC,CAAC;IAExC,OAAO,QAAQ,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;AACzE,CAAC,CAAC;AAMF,MAAM,eAAe,GAAG,CAAC,CAAC;AAE1B,MAAM,QAAQ,GAAG,CAAC,IAAU,EAAE,KAAgB,EAAa,EAAE;IAC5D,MAAM,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC;IACvB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACzC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,GAAG,CAAC,GAAG,CAAC,GAAG,eAAe,CAAC,CAAC;QACpE,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,CAAC,EAAE,OAAO,EAAE,EAAE,CAAC;YAC9C,MAAM,KAAK,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,KAAK,CAAC;YAC/C,IAAI,CAAC,CAAC,GAAG,OAAO,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC;QAC/D,CAAC;IACF,CAAC;IACD,OAAO,KAAK,CAAC;AACd,CAAC,CAAC;AAOF,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,EAChC,OAAiC,EACjC,cAAsB,EACtB,QAAiC,EACZ,EAAE;IACvB,MAAM,IAAI,GAAG,cAAc,GAAG,UAAU,CAAC;IACzC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CACrB,cAAc,EACd,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAC7D,CAAC;IACF,MAAM,MAAM,GAAG,cAAc,GAAG,CAAC,CAAC;IAClC,MAAM,IAAI,GAAG,OAAO;SAClB,GAAG,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE;QACtB,MAAM,EAAE,GAAG,KAAK,GAAG,IAAI,GAAG,MAAM,CAAC;QACjC,MAAM,IAAI,GAAG,eAAe,KAAK,CAAC,EAAE,CAAC,SAAS,KAAK,CAAC,MAAM,CAAC,QAAQ,KAAK,CAAC,MAAM,CAAC,WAAW,kBAAkB,KAAK,CAAC;QACnH,MAAM,KAAK,GAAG,WAAW,CACxB;YACC,GAAG,MAAM;YACT,CAAC,EAAE,EAAE;YACL,CAAC,EAAE,MAAM;YACT,IAAI,EAAE,cAAc,GAAG,kBAAkB;SACzC,EACD,QAAQ,EACR,CAAC,CACD,CAAC;QACF,OAAO,IAAI,GAAG,KAAK,CAAC;IACrB,CAAC,CAAC;SACD,IAAI,CAAC,EAAE,CAAC,CAAC;IAEX,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,KAAK,EAAE,cAAc,EAAE,IAAI,CAAC,CAAC;SAC1E,WAAW,EAAE;SACb,GAAG,EAAE;SACL,QAAQ,CAAC,EAAE,iBAAiB,EAAE,IAAI,EAAE,CAAC,CAAC;IAExC,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;AACzD,CAAC,CAAC"}
|
package/dist/glyphs.d.ts
ADDED
|
@@ -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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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"}
|
package/dist/place.d.ts
ADDED
|
@@ -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 @@
|
|
|
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"}
|
package/dist/shuffle.js
ADDED
|
@@ -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 @@
|
|
|
1
|
+
{"version":3,"file":"iconOrderAssets.unit.test.d.ts","sourceRoot":"","sources":["../../src/tests/iconOrderAssets.unit.test.ts"],"names":[],"mappings":""}
|