@prosopo/puzzle-assets 0.1.3
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/CHANGELOG.md +39 -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 +131 -0
- package/dist/background.js.map +1 -0
- package/dist/browser.d.ts +15 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +38 -0
- package/dist/browser.js.map +1 -0
- package/dist/cjs/_virtual/_rolldown/runtime.cjs +23 -0
- package/dist/cjs/background.cjs +131 -0
- package/dist/cjs/compose.cjs +126 -0
- package/dist/cjs/decoys.cjs +105 -0
- package/dist/cjs/encode.cjs +51 -0
- package/dist/cjs/index.cjs +74 -0
- package/dist/cjs/notch.cjs +122 -0
- package/dist/cjs/palette.cjs +94 -0
- package/dist/cjs/prng.cjs +44 -0
- package/dist/compose.d.ts +9 -0
- package/dist/compose.d.ts.map +1 -0
- package/dist/compose.js +126 -0
- package/dist/compose.js.map +1 -0
- package/dist/decoys.d.ts +5 -0
- package/dist/decoys.d.ts.map +1 -0
- package/dist/decoys.js +104 -0
- package/dist/decoys.js.map +1 -0
- package/dist/encode.d.ts +6 -0
- package/dist/encode.d.ts.map +1 -0
- package/dist/encode.js +46 -0
- package/dist/encode.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +1 -0
- package/dist/notch.d.ts +7 -0
- package/dist/notch.d.ts.map +1 -0
- package/dist/notch.js +121 -0
- package/dist/notch.js.map +1 -0
- package/dist/palette.d.ts +9 -0
- package/dist/palette.d.ts.map +1 -0
- package/dist/palette.js +93 -0
- package/dist/palette.js.map +1 -0
- package/dist/prng.d.ts +10 -0
- package/dist/prng.d.ts.map +1 -0
- package/dist/prng.js +42 -0
- package/dist/prng.js.map +1 -0
- package/dist/tests/puzzleAssets.unit.test.d.ts +2 -0
- package/dist/tests/puzzleAssets.unit.test.d.ts.map +1 -0
- package/dist/tests/puzzleAssets.unit.test.js +257 -0
- package/dist/tests/puzzleAssets.unit.test.js.map +1 -0
- package/dist/types.d.ts +28 -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 +61 -0
- package/scripts/preview.ts +197 -0
- package/vite.cjs.config.ts +23 -0
- package/vite.esm.config.ts +27 -0
- package/vite.test.config.ts +32 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const require_runtime = require("./_virtual/_rolldown/runtime.cjs");
|
|
2
|
+
let sharp = require("sharp");
|
|
3
|
+
sharp = require_runtime.__toESM(sharp, 1);
|
|
4
|
+
//#region src/encode.ts
|
|
5
|
+
/**
|
|
6
|
+
* Background quality is deliberately high. WebP's chroma subsampling and
|
|
7
|
+
* ringing around the cut are exactly the kind of artefact an attacker could
|
|
8
|
+
* key on, *and* at q<95 the ringing becomes visually obvious around the
|
|
9
|
+
* decoy piece silhouettes we paint on top of the background — the mix of
|
|
10
|
+
* bright edge-rim and dark drop-shadow at the decoy edge is precisely the
|
|
11
|
+
* high-contrast local signal WebP butchers hardest. At 300x200 the extra few
|
|
12
|
+
* kilobytes of a higher quality setting are trivial.
|
|
13
|
+
*/
|
|
14
|
+
var BACKGROUND_QUALITY = 95;
|
|
15
|
+
var encodeBackground = (image) => (0, sharp.default)(image.data, { raw: {
|
|
16
|
+
width: image.width,
|
|
17
|
+
height: image.height,
|
|
18
|
+
channels: 4
|
|
19
|
+
} }).webp({
|
|
20
|
+
quality: BACKGROUND_QUALITY,
|
|
21
|
+
alphaQuality: 100,
|
|
22
|
+
effort: 4
|
|
23
|
+
}).toBuffer();
|
|
24
|
+
/**
|
|
25
|
+
* Lossless. The piece is a 44x44 sprite on transparency, and lossy WebP's
|
|
26
|
+
* ringing around the alpha edge is the dominant visual artefact — a subtle
|
|
27
|
+
* ripple that reads as wavy edges even at q=92. Lossless costs ~4 KB per
|
|
28
|
+
* piece instead of ~2 KB, which is nothing over the wire, and eliminates
|
|
29
|
+
* the ringing entirely.
|
|
30
|
+
*/
|
|
31
|
+
var encodePiece = (image) => (0, sharp.default)(image.data, { raw: {
|
|
32
|
+
width: image.width,
|
|
33
|
+
height: image.height,
|
|
34
|
+
channels: 4
|
|
35
|
+
} }).webp({
|
|
36
|
+
lossless: true,
|
|
37
|
+
alphaQuality: 100,
|
|
38
|
+
effort: 4
|
|
39
|
+
}).toBuffer();
|
|
40
|
+
/** PNG, for the preview script only — lossless so artefacts are visible. */
|
|
41
|
+
var encodePng = (image) => (0, sharp.default)(image.data, { raw: {
|
|
42
|
+
width: image.width,
|
|
43
|
+
height: image.height,
|
|
44
|
+
channels: 4
|
|
45
|
+
} }).png().toBuffer();
|
|
46
|
+
var toDataUri = (webp) => `data:image/webp;base64,${webp.toString("base64")}`;
|
|
47
|
+
//#endregion
|
|
48
|
+
exports.encodeBackground = encodeBackground;
|
|
49
|
+
exports.encodePiece = encodePiece;
|
|
50
|
+
exports.encodePng = encodePng;
|
|
51
|
+
exports.toDataUri = toDataUri;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_background = require("./background.cjs");
|
|
3
|
+
const require_notch = require("./notch.cjs");
|
|
4
|
+
const require_compose = require("./compose.cjs");
|
|
5
|
+
const require_decoys = require("./decoys.cjs");
|
|
6
|
+
const require_encode = require("./encode.cjs");
|
|
7
|
+
const require_prng = require("./prng.cjs");
|
|
8
|
+
//#region src/index.ts
|
|
9
|
+
/**
|
|
10
|
+
* Matches the widget's container. Kept here so the generator and the provider
|
|
11
|
+
* agree on geometry without the provider having to restate it. Geometry is
|
|
12
|
+
* widget-coupled and not exposed to operators.
|
|
13
|
+
*/
|
|
14
|
+
var DEFAULT_GEOMETRY = {
|
|
15
|
+
width: 300,
|
|
16
|
+
height: 200,
|
|
17
|
+
notchSize: 44
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Defaults for the per-render tunables. Operators can override any of these
|
|
21
|
+
* per-client (via `ClientSettingsSchema.puzzle`) or per-traffic-category
|
|
22
|
+
* (via `TrafficCategoryPolicySchema.puzzle`); the provider resolves an
|
|
23
|
+
* effective value and passes it to `renderPuzzle`.
|
|
24
|
+
*/
|
|
25
|
+
var DEFAULT_RENDER_SETTINGS = {
|
|
26
|
+
decoyCount: 5,
|
|
27
|
+
decoyEdgeDarkness: 20,
|
|
28
|
+
decoyBodyBrightness: 4,
|
|
29
|
+
holeDarken: .55,
|
|
30
|
+
decoyHoleDarken: .7
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Generate a fresh background. Each one is single-use by contract: serving the
|
|
34
|
+
* same background twice with two different notch positions would let an
|
|
35
|
+
* attacker diff the two composites and read both targets straight off. The
|
|
36
|
+
* caller's buffer is responsible for enforcing consume-once.
|
|
37
|
+
*/
|
|
38
|
+
var createBackground = (geometry = DEFAULT_GEOMETRY) => require_background.generateBackground(require_prng.createPrng(require_prng.createSeed()), geometry.width, geometry.height);
|
|
39
|
+
/**
|
|
40
|
+
* Cut a notch at `placement` and encode both halves.
|
|
41
|
+
*
|
|
42
|
+
* The background is consumed (mutated) by this call.
|
|
43
|
+
*/
|
|
44
|
+
var renderPuzzle = async (background, placement, geometry = DEFAULT_GEOMETRY, settings = DEFAULT_RENDER_SETTINGS) => {
|
|
45
|
+
const prng = require_prng.createPrng(require_prng.createSeed());
|
|
46
|
+
const pieceSize = geometry.pieceSize ?? geometry.notchSize;
|
|
47
|
+
require_decoys.paintDecoys(background, prng, settings.decoyCount, pieceSize, placement, settings.decoyEdgeDarkness, settings.decoyBodyBrightness, settings.decoyHoleDarken);
|
|
48
|
+
const shape = require_notch.createNotchShape(prng, pieceSize);
|
|
49
|
+
const { background: cut, piece } = require_compose.cutNotch(prng, background, shape, pieceSize, placement, settings.holeDarken);
|
|
50
|
+
const [backgroundWebp, pieceWebp] = await Promise.all([require_encode.encodeBackground(cut), require_encode.encodePiece(piece)]);
|
|
51
|
+
return {
|
|
52
|
+
background: backgroundWebp,
|
|
53
|
+
piece: pieceWebp,
|
|
54
|
+
pieceSize
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
/** Convenience: generate, cut and encode in one call. */
|
|
58
|
+
var createPuzzle = async (placement, geometry = DEFAULT_GEOMETRY, settings = DEFAULT_RENDER_SETTINGS) => renderPuzzle(createBackground(geometry), placement, geometry, settings);
|
|
59
|
+
//#endregion
|
|
60
|
+
exports.DEFAULT_GEOMETRY = DEFAULT_GEOMETRY;
|
|
61
|
+
exports.DEFAULT_RENDER_SETTINGS = DEFAULT_RENDER_SETTINGS;
|
|
62
|
+
exports.SEED_BYTES = require_prng.SEED_BYTES;
|
|
63
|
+
exports.createBackground = createBackground;
|
|
64
|
+
exports.createPrng = require_prng.createPrng;
|
|
65
|
+
exports.createPuzzle = createPuzzle;
|
|
66
|
+
exports.createSeed = require_prng.createSeed;
|
|
67
|
+
exports.encodeBackground = require_encode.encodeBackground;
|
|
68
|
+
exports.encodePiece = require_encode.encodePiece;
|
|
69
|
+
exports.encodePng = require_encode.encodePng;
|
|
70
|
+
exports.generateBackground = require_background.generateBackground;
|
|
71
|
+
exports.paintDecoyPiece = require_decoys.paintDecoyPiece;
|
|
72
|
+
exports.paintDecoys = require_decoys.paintDecoys;
|
|
73
|
+
exports.renderPuzzle = renderPuzzle;
|
|
74
|
+
exports.toDataUri = require_encode.toDataUri;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
//#region src/notch.ts
|
|
2
|
+
var sideOffset = (side, half, distance) => {
|
|
3
|
+
switch (side) {
|
|
4
|
+
case 0: return [0, -half - distance];
|
|
5
|
+
case 1: return [half + distance, 0];
|
|
6
|
+
case 2: return [0, half + distance];
|
|
7
|
+
default: return [-half - distance, 0];
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
var roundedBoxDistance = (px, py, half, radius) => {
|
|
11
|
+
const qx = Math.abs(px) - half + radius;
|
|
12
|
+
const qy = Math.abs(py) - half + radius;
|
|
13
|
+
return Math.hypot(Math.max(qx, 0), Math.max(qy, 0)) + Math.min(Math.max(qx, qy), 0) - radius;
|
|
14
|
+
};
|
|
15
|
+
var createJigsawShape = (prng, size) => {
|
|
16
|
+
const knobRadius = size * prng.range(.15, .19);
|
|
17
|
+
const bodyHalf = size / 2 - knobRadius * .9;
|
|
18
|
+
const cornerRadius = bodyHalf * prng.range(.28, .42);
|
|
19
|
+
const knobSide = prng.int(0, 3);
|
|
20
|
+
const [knobX, knobY] = sideOffset(knobSide, bodyHalf, -knobRadius * .45);
|
|
21
|
+
const hasBite = prng.next() < .55;
|
|
22
|
+
const biteSide = (knobSide + prng.int(1, 3)) % 4;
|
|
23
|
+
const biteRadius = size * prng.range(.13, .17);
|
|
24
|
+
const [biteX, biteY] = sideOffset(biteSide, bodyHalf, -biteRadius * .55);
|
|
25
|
+
return { distance(lx, ly) {
|
|
26
|
+
const px = lx - size / 2;
|
|
27
|
+
const py = ly - size / 2;
|
|
28
|
+
const body = roundedBoxDistance(px, py, bodyHalf, cornerRadius);
|
|
29
|
+
const knob = Math.hypot(px - knobX, py - knobY) - knobRadius;
|
|
30
|
+
let d = Math.min(body, knob);
|
|
31
|
+
if (hasBite) {
|
|
32
|
+
const bite = Math.hypot(px - biteX, py - biteY) - biteRadius;
|
|
33
|
+
d = Math.max(d, -bite);
|
|
34
|
+
}
|
|
35
|
+
return d;
|
|
36
|
+
} };
|
|
37
|
+
};
|
|
38
|
+
var createCircleShape = (prng, size) => {
|
|
39
|
+
const radius = size / 2 * prng.range(.85, .98);
|
|
40
|
+
const cx = size / 2;
|
|
41
|
+
const cy = size / 2;
|
|
42
|
+
return { distance(lx, ly) {
|
|
43
|
+
return Math.hypot(lx - cx, ly - cy) - radius;
|
|
44
|
+
} };
|
|
45
|
+
};
|
|
46
|
+
var createRoundedRectShape = (prng, size) => {
|
|
47
|
+
const aspect = prng.range(.5, 1);
|
|
48
|
+
const halfMax = size / 2 * prng.range(.82, .98);
|
|
49
|
+
const [halfW, halfH] = prng.next() < .5 ? [halfMax, halfMax * aspect] : [halfMax * aspect, halfMax];
|
|
50
|
+
const cornerRadius = Math.min(halfW, halfH) * prng.range(.08, .5);
|
|
51
|
+
const cx = size / 2;
|
|
52
|
+
const cy = size / 2;
|
|
53
|
+
return { distance(lx, ly) {
|
|
54
|
+
const px = lx - cx;
|
|
55
|
+
const py = ly - cy;
|
|
56
|
+
const qx = Math.abs(px) - halfW + cornerRadius;
|
|
57
|
+
const qy = Math.abs(py) - halfH + cornerRadius;
|
|
58
|
+
return Math.hypot(Math.max(qx, 0), Math.max(qy, 0)) + Math.min(Math.max(qx, qy), 0) - cornerRadius;
|
|
59
|
+
} };
|
|
60
|
+
};
|
|
61
|
+
var createPolygonShape = (prng, size) => {
|
|
62
|
+
const n = prng.int(3, 8);
|
|
63
|
+
const inradius = size / 2 * prng.range(.85, .98) * Math.cos(Math.PI / n);
|
|
64
|
+
const rotation = prng.next() * Math.PI * 2;
|
|
65
|
+
const normals = [];
|
|
66
|
+
for (let i = 0; i < n; i++) {
|
|
67
|
+
const angle = 2 * Math.PI * i / n - Math.PI / 2 + rotation;
|
|
68
|
+
normals.push([Math.cos(angle), Math.sin(angle)]);
|
|
69
|
+
}
|
|
70
|
+
const cx = size / 2;
|
|
71
|
+
const cy = size / 2;
|
|
72
|
+
return { distance(lx, ly) {
|
|
73
|
+
const px = lx - cx;
|
|
74
|
+
const py = ly - cy;
|
|
75
|
+
let maxD = Number.NEGATIVE_INFINITY;
|
|
76
|
+
for (const [nx, ny] of normals) {
|
|
77
|
+
const d = px * nx + py * ny - inradius;
|
|
78
|
+
if (d > maxD) maxD = d;
|
|
79
|
+
}
|
|
80
|
+
return maxD;
|
|
81
|
+
} };
|
|
82
|
+
};
|
|
83
|
+
var createFlowerShape = (prng, size) => {
|
|
84
|
+
const petals = prng.int(4, 8);
|
|
85
|
+
const rAvg = size / 2 * prng.range(.72, .88);
|
|
86
|
+
const rAmp = rAvg * prng.range(.1, .22);
|
|
87
|
+
const phase = prng.next() * Math.PI * 2;
|
|
88
|
+
const cx = size / 2;
|
|
89
|
+
const cy = size / 2;
|
|
90
|
+
return { distance(lx, ly) {
|
|
91
|
+
const px = lx - cx;
|
|
92
|
+
const py = ly - cy;
|
|
93
|
+
return Math.hypot(px, py) - (rAvg + rAmp * Math.cos(petals * Math.atan2(py, px) + phase));
|
|
94
|
+
} };
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Pick a random shape family per call. The real piece and its hole are cut
|
|
98
|
+
* from the same call so they match; decoys each call independently so they
|
|
99
|
+
* form a mixed field of silhouettes on the frame.
|
|
100
|
+
*/
|
|
101
|
+
var createNotchShape = (prng, size) => {
|
|
102
|
+
switch (prng.int(0, 4)) {
|
|
103
|
+
case 0: return createJigsawShape(prng, size);
|
|
104
|
+
case 1: return createCircleShape(prng, size);
|
|
105
|
+
case 2: return createRoundedRectShape(prng, size);
|
|
106
|
+
case 3: return createPolygonShape(prng, size);
|
|
107
|
+
default: return createFlowerShape(prng, size);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* Anti-aliased coverage in [0, 1] for a signed distance, using a one-pixel
|
|
112
|
+
* transition band centred on the boundary.
|
|
113
|
+
*/
|
|
114
|
+
var coverageFromDistance = (distance) => {
|
|
115
|
+
const t = .5 - distance;
|
|
116
|
+
if (t <= 0) return 0;
|
|
117
|
+
if (t >= 1) return 1;
|
|
118
|
+
return t * t * (3 - 2 * t);
|
|
119
|
+
};
|
|
120
|
+
//#endregion
|
|
121
|
+
exports.coverageFromDistance = coverageFromDistance;
|
|
122
|
+
exports.createNotchShape = createNotchShape;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
//#region src/palette.ts
|
|
2
|
+
var PALETTES = [
|
|
3
|
+
{
|
|
4
|
+
hue: 268,
|
|
5
|
+
spread: 46,
|
|
6
|
+
saturation: [.55, .78],
|
|
7
|
+
lightness: [.4, .7]
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
hue: 212,
|
|
11
|
+
spread: 42,
|
|
12
|
+
saturation: [.58, .8],
|
|
13
|
+
lightness: [.4, .7]
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
hue: 178,
|
|
17
|
+
spread: 44,
|
|
18
|
+
saturation: [.5, .72],
|
|
19
|
+
lightness: [.38, .68]
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
hue: 32,
|
|
23
|
+
spread: 38,
|
|
24
|
+
saturation: [.58, .8],
|
|
25
|
+
lightness: [.45, .72]
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
hue: 338,
|
|
29
|
+
spread: 40,
|
|
30
|
+
saturation: [.52, .74],
|
|
31
|
+
lightness: [.45, .72]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
hue: 138,
|
|
35
|
+
spread: 42,
|
|
36
|
+
saturation: [.46, .66],
|
|
37
|
+
lightness: [.38, .66]
|
|
38
|
+
}
|
|
39
|
+
];
|
|
40
|
+
var hueToChannel = (p, q, tRaw) => {
|
|
41
|
+
let t = tRaw;
|
|
42
|
+
if (t < 0) t += 1;
|
|
43
|
+
if (t > 1) t -= 1;
|
|
44
|
+
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
45
|
+
if (t < 1 / 2) return q;
|
|
46
|
+
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
47
|
+
return p;
|
|
48
|
+
};
|
|
49
|
+
/** h in degrees, s and l in [0, 1]. */
|
|
50
|
+
var hslToRgb = (h, s, l) => {
|
|
51
|
+
const hNorm = (h % 360 + 360) % 360 / 360;
|
|
52
|
+
if (s === 0) {
|
|
53
|
+
const v = Math.round(l * 255);
|
|
54
|
+
return {
|
|
55
|
+
r: v,
|
|
56
|
+
g: v,
|
|
57
|
+
b: v
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const q = l < .5 ? l * (1 + s) : l + s - l * s;
|
|
61
|
+
const p = 2 * l - q;
|
|
62
|
+
return {
|
|
63
|
+
r: Math.round(hueToChannel(p, q, hNorm + 1 / 3) * 255),
|
|
64
|
+
g: Math.round(hueToChannel(p, q, hNorm) * 255),
|
|
65
|
+
b: Math.round(hueToChannel(p, q, hNorm - 1 / 3) * 255)
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Draw `count` harmonious colours. The base hue is jittered per background so
|
|
70
|
+
* two draws from the same palette are not the same picture.
|
|
71
|
+
*
|
|
72
|
+
* Hues are spread evenly across the band rather than sampled uniformly at
|
|
73
|
+
* random, and lightness alternates between the ends of the range. Drawing both
|
|
74
|
+
* at random clusters them: neighbouring control points come out nearly the same
|
|
75
|
+
* colour and the mesh reads as one flat field with a slight gradient, which is
|
|
76
|
+
* the difference between "designed" and "beige".
|
|
77
|
+
*/
|
|
78
|
+
var drawPalette = (prng, count) => {
|
|
79
|
+
const spec = prng.pick(PALETTES);
|
|
80
|
+
const baseHue = spec.hue + prng.range(-20, 20);
|
|
81
|
+
const direction = prng.next() < .5 ? -1 : 1;
|
|
82
|
+
const colours = [];
|
|
83
|
+
for (let i = 0; i < count; i++) {
|
|
84
|
+
const hue = baseHue + direction * ((count === 1 ? .5 : i / (count - 1)) - .5) * 2 * spec.spread + prng.range(-6, 6);
|
|
85
|
+
const saturation = prng.range(spec.saturation[0], spec.saturation[1]);
|
|
86
|
+
const pole = i % 2 === 0 ? 0 : 1;
|
|
87
|
+
const lightness = spec.lightness[pole] + prng.range(-.04, .04) * (pole === 0 ? 1 : -1);
|
|
88
|
+
colours.push(hslToRgb(hue, saturation, lightness));
|
|
89
|
+
}
|
|
90
|
+
return colours;
|
|
91
|
+
};
|
|
92
|
+
//#endregion
|
|
93
|
+
exports.drawPalette = drawPalette;
|
|
94
|
+
exports.hslToRgb = hslToRgb;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
let node_crypto = require("node:crypto");
|
|
2
|
+
//#region src/prng.ts
|
|
3
|
+
var SEED_BYTES = 16;
|
|
4
|
+
/** Fresh 128-bit seed from the system CSPRNG. */
|
|
5
|
+
var createSeed = () => (0, node_crypto.randomBytes)(16);
|
|
6
|
+
/**
|
|
7
|
+
* xoshiro128** — small, fast, and good enough for imagery. Not a CSPRNG: the
|
|
8
|
+
* secrecy of the output rests on the seed, which is.
|
|
9
|
+
*/
|
|
10
|
+
var createPrng = (seed) => {
|
|
11
|
+
if (seed.length < 16) throw new Error(`puzzle-assets: seed must be at least 16 bytes, got ${seed.length}`);
|
|
12
|
+
let s0 = seed.readUInt32LE(0);
|
|
13
|
+
let s1 = seed.readUInt32LE(4);
|
|
14
|
+
let s2 = seed.readUInt32LE(8);
|
|
15
|
+
let s3 = seed.readUInt32LE(12);
|
|
16
|
+
if ((s0 | s1 | s2 | s3) === 0) s0 = 1;
|
|
17
|
+
const rotl = (x, k) => (x << k | x >>> 32 - k) >>> 0;
|
|
18
|
+
const nextUint32 = () => {
|
|
19
|
+
const result = Math.imul(rotl(Math.imul(s1, 5) >>> 0, 7), 9) >>> 0 >>> 0;
|
|
20
|
+
const t = s1 << 9 >>> 0;
|
|
21
|
+
s2 = (s2 ^ s0) >>> 0;
|
|
22
|
+
s3 = (s3 ^ s1) >>> 0;
|
|
23
|
+
s1 = (s1 ^ s2) >>> 0;
|
|
24
|
+
s0 = (s0 ^ s3) >>> 0;
|
|
25
|
+
s2 = (s2 ^ t) >>> 0;
|
|
26
|
+
s3 = rotl(s3, 11);
|
|
27
|
+
return result;
|
|
28
|
+
};
|
|
29
|
+
const next = () => nextUint32() / 4294967296;
|
|
30
|
+
return {
|
|
31
|
+
next,
|
|
32
|
+
int: (min, max) => min + Math.floor(next() * (max - min + 1)),
|
|
33
|
+
range: (min, max) => min + next() * (max - min),
|
|
34
|
+
pick: (items) => {
|
|
35
|
+
const item = items[Math.floor(next() * items.length)];
|
|
36
|
+
if (item === void 0) throw new Error("puzzle-assets: cannot pick from an empty list");
|
|
37
|
+
return item;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
//#endregion
|
|
42
|
+
exports.SEED_BYTES = SEED_BYTES;
|
|
43
|
+
exports.createPrng = createPrng;
|
|
44
|
+
exports.createSeed = createSeed;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type NotchShape } from "./notch.js";
|
|
2
|
+
import type { Prng } from "./prng.js";
|
|
3
|
+
import type { NotchPlacement, RgbaImage } from "./types.js";
|
|
4
|
+
export interface CutResult {
|
|
5
|
+
background: RgbaImage;
|
|
6
|
+
piece: RgbaImage;
|
|
7
|
+
}
|
|
8
|
+
export declare const cutNotch: (prng: Prng, background: RgbaImage, shape: NotchShape, size: number, placement: NotchPlacement, holeDarken: number) => CutResult;
|
|
9
|
+
//# 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,UAAU,EAAwB,MAAM,YAAY,CAAC;AACnE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAiE5D,MAAM,WAAW,SAAS;IAEzB,UAAU,EAAE,SAAS,CAAC;IAEtB,KAAK,EAAE,SAAS,CAAC;CACjB;AAQD,eAAO,MAAM,QAAQ,SACd,IAAI,cACE,SAAS,SACd,UAAU,QACX,MAAM,aACD,cAAc,cACb,MAAM,KAChB,SA4GF,CAAC"}
|
package/dist/compose.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { coverageFromDistance } from "./notch.js";
|
|
2
|
+
//#region src/compose.ts
|
|
3
|
+
/**
|
|
4
|
+
* Per-pixel noise added to the piece, in 0-255 units.
|
|
5
|
+
*
|
|
6
|
+
* This is not decoration. The piece is cut from the background, so a
|
|
7
|
+
* pixel-exact copy would let an attacker slide the piece over the background
|
|
8
|
+
* with normalised cross-correlation and land on the target in one pass —
|
|
9
|
+
* which would make the whole server-rendering exercise worthless. The piece is
|
|
10
|
+
* therefore brightness-shifted and re-noised so it correlates well with a
|
|
11
|
+
* region of the background but never perfectly, and the background gets fresh
|
|
12
|
+
* noise around the cut so the seam is not a clean template either.
|
|
13
|
+
*/
|
|
14
|
+
var PIECE_NOISE = 7;
|
|
15
|
+
var PIECE_BRIGHTNESS_RANGE = [-8, 8];
|
|
16
|
+
/**
|
|
17
|
+
* Disturbance ring outside the cut, in px. The ring exists so an edge detector
|
|
18
|
+
* cannot lock onto a clean gradient discontinuity along the seam. The noise
|
|
19
|
+
* tapers linearly to zero at the outer edge so it fades into the ambient
|
|
20
|
+
* background grain instead of terminating in a visible border.
|
|
21
|
+
*/
|
|
22
|
+
var SEAM_NOISE_RADIUS = 3;
|
|
23
|
+
/**
|
|
24
|
+
* Peak amplitude at the seam itself, in 0-255 units. Kept close to the
|
|
25
|
+
* background grain amplitude so the ring reads as continuation of the grain
|
|
26
|
+
* rather than as a halo around the hole.
|
|
27
|
+
*/
|
|
28
|
+
var SEAM_NOISE = 4;
|
|
29
|
+
/**
|
|
30
|
+
* Depth cue on the hole. HOLE_INNER_SHADOW is a fixed within-primitive
|
|
31
|
+
* ratio, but HOLE_DARKEN is caller-provided so operators can tune how
|
|
32
|
+
* obvious the target is versus the decoys.
|
|
33
|
+
*/
|
|
34
|
+
var HOLE_INNER_SHADOW = .45;
|
|
35
|
+
/**
|
|
36
|
+
* Depth cue on the piece itself. A subtle brightness rise just inside the
|
|
37
|
+
* silhouette edge, so the piece reads as raised rather than as a flat sticker.
|
|
38
|
+
* Kept low deliberately — a stronger rim starts to look like a drawn border
|
|
39
|
+
* against the darkened cutout it drops into.
|
|
40
|
+
*/
|
|
41
|
+
var PIECE_EDGE_LIGHT = 8;
|
|
42
|
+
var clamp255 = (v) => v < 0 ? 0 : v > 255 ? 255 : v;
|
|
43
|
+
/**
|
|
44
|
+
* Sample the background at `(x, y)`. Returns `null` when the coordinate
|
|
45
|
+
* sits outside the frame — the piece is clipped at the boundary rather
|
|
46
|
+
* than filled from a clamped edge pixel, so a piece whose placement
|
|
47
|
+
* overhangs the frame simply loses those pixels.
|
|
48
|
+
*/
|
|
49
|
+
var sampleBackground = (image, x, y) => {
|
|
50
|
+
if (x < 0 || y < 0 || x >= image.width || y >= image.height) return null;
|
|
51
|
+
const i = (y * image.width + x) * 4;
|
|
52
|
+
return [
|
|
53
|
+
image.data[i] ?? 0,
|
|
54
|
+
image.data[i + 1] ?? 0,
|
|
55
|
+
image.data[i + 2] ?? 0
|
|
56
|
+
];
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Cut the notch out of `background` and produce the matching piece.
|
|
60
|
+
*
|
|
61
|
+
* `background` is mutated in place — it is a freshly generated, single-use
|
|
62
|
+
* image, so there is nothing to preserve.
|
|
63
|
+
*/
|
|
64
|
+
var cutNotch = (prng, background, shape, size, placement, holeDarken) => {
|
|
65
|
+
const half = size / 2;
|
|
66
|
+
const left = Math.round(placement.targetX - half);
|
|
67
|
+
const top = Math.round(placement.targetY - half);
|
|
68
|
+
const pieceData = Buffer.alloc(size * size * 4);
|
|
69
|
+
const brightness = prng.range(PIECE_BRIGHTNESS_RANGE[0], PIECE_BRIGHTNESS_RANGE[1]);
|
|
70
|
+
for (let ly = 0; ly < size; ly++) for (let lx = 0; lx < size; lx++) {
|
|
71
|
+
const d = shape.distance(lx, ly);
|
|
72
|
+
const coverage = coverageFromDistance(d);
|
|
73
|
+
const pi = (ly * size + lx) * 4;
|
|
74
|
+
if (coverage <= 0) {
|
|
75
|
+
pieceData[pi + 3] = 0;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const sample = sampleBackground(background, left + lx, top + ly);
|
|
79
|
+
if (sample === null) {
|
|
80
|
+
pieceData[pi + 3] = 0;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const [r, g, b] = sample;
|
|
84
|
+
const edgeLight = d > -2 ? PIECE_EDGE_LIGHT * (1 + d / 2) : 0;
|
|
85
|
+
const noise = (prng.next() - .5) * 2 * PIECE_NOISE;
|
|
86
|
+
const delta = brightness + edgeLight + noise;
|
|
87
|
+
pieceData[pi] = clamp255(Math.round(r + delta));
|
|
88
|
+
pieceData[pi + 1] = clamp255(Math.round(g + delta));
|
|
89
|
+
pieceData[pi + 2] = clamp255(Math.round(b + delta));
|
|
90
|
+
pieceData[pi + 3] = Math.round(coverage * 255);
|
|
91
|
+
}
|
|
92
|
+
for (let ly = -SEAM_NOISE_RADIUS; ly < size + SEAM_NOISE_RADIUS; ly++) for (let lx = -SEAM_NOISE_RADIUS; lx < size + SEAM_NOISE_RADIUS; lx++) {
|
|
93
|
+
const x = left + lx;
|
|
94
|
+
const y = top + ly;
|
|
95
|
+
if (x < 0 || y < 0 || x >= background.width || y >= background.height) continue;
|
|
96
|
+
const bi = (y * background.width + x) * 4;
|
|
97
|
+
const d = shape.distance(lx, ly);
|
|
98
|
+
const coverage = coverageFromDistance(d);
|
|
99
|
+
if (coverage > 0) {
|
|
100
|
+
const r = background.data[bi] ?? 0;
|
|
101
|
+
const g = background.data[bi + 1] ?? 0;
|
|
102
|
+
const b = background.data[bi + 2] ?? 0;
|
|
103
|
+
const factor = holeDarken * (1 - (d > -3 ? HOLE_INNER_SHADOW * (1 + d / 3) : 0));
|
|
104
|
+
const mix = coverage;
|
|
105
|
+
background.data[bi] = clamp255(Math.round(r * (1 - mix) + r * factor * mix));
|
|
106
|
+
background.data[bi + 1] = clamp255(Math.round(g * (1 - mix) + g * factor * mix));
|
|
107
|
+
background.data[bi + 2] = clamp255(Math.round(b * (1 - mix) + b * factor * mix));
|
|
108
|
+
} else if (d < SEAM_NOISE_RADIUS) {
|
|
109
|
+
const falloff = 1 - d / SEAM_NOISE_RADIUS;
|
|
110
|
+
const noise = (prng.next() - .5) * 2 * SEAM_NOISE * falloff;
|
|
111
|
+
background.data[bi] = clamp255(Math.round((background.data[bi] ?? 0) + noise));
|
|
112
|
+
background.data[bi + 1] = clamp255(Math.round((background.data[bi + 1] ?? 0) + noise));
|
|
113
|
+
background.data[bi + 2] = clamp255(Math.round((background.data[bi + 2] ?? 0) + noise));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
background,
|
|
118
|
+
piece: {
|
|
119
|
+
data: pieceData,
|
|
120
|
+
width: size,
|
|
121
|
+
height: size
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
//#endregion
|
|
126
|
+
export { cutNotch };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compose.js","sourceRoot":"","sources":["../src/compose.ts"],"names":[],"mappings":"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"}
|
package/dist/decoys.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Prng } from "./prng.js";
|
|
2
|
+
import type { NotchPlacement, RgbaImage } from "./types.js";
|
|
3
|
+
export declare const paintDecoyPiece: (background: RgbaImage, prng: Prng, size: number, cx: number, cy: number, edgeDarkness: number, bodyBrightness: number, holeDarken: number) => void;
|
|
4
|
+
export declare const paintDecoys: (background: RgbaImage, prng: Prng, targetCount: number, notchSize: number, notch: NotchPlacement, edgeDarkness: number, bodyBrightness: number, holeDarken: number) => void;
|
|
5
|
+
//# sourceMappingURL=decoys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decoys.d.ts","sourceRoot":"","sources":["../src/decoys.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AA+C5D,eAAO,MAAM,eAAe,eACf,SAAS,QACf,IAAI,QACJ,MAAM,MACR,MAAM,MACN,MAAM,gBACI,MAAM,kBACJ,MAAM,cACV,MAAM,KAChB,IAiDF,CAAC;AAkBF,eAAO,MAAM,WAAW,eACX,SAAS,QACf,IAAI,eACG,MAAM,aACR,MAAM,SACV,cAAc,gBACP,MAAM,kBACJ,MAAM,cACV,MAAM,KAChB,IAyCF,CAAC"}
|
package/dist/decoys.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { coverageFromDistance, createNotchShape } from "./notch.js";
|
|
2
|
+
//#region src/decoys.ts
|
|
3
|
+
/**
|
|
4
|
+
* Decoys are rendered as shallow indented jigsaw silhouettes: a darker
|
|
5
|
+
* edge with a barely-lifted interior. Edge shading direction (darker at
|
|
6
|
+
* the rim) mirrors the real cutout's inner shadow, so at a glance decoys
|
|
7
|
+
* read as similar shapes and it's not immediately obvious which one is
|
|
8
|
+
* the real target. Edge/body amplitudes come from PuzzleRenderSettings
|
|
9
|
+
* so operators can tune the confusion level per-client from the portal.
|
|
10
|
+
*
|
|
11
|
+
* No drop shadow — a shadow ring picked up WebP artefacts and read as a
|
|
12
|
+
* visible halo around each decoy.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Minimum centre-to-centre distance from the real notch, in notch-sizes.
|
|
16
|
+
* Zero: decoys may sit right up to (and over) the target region. The cut
|
|
17
|
+
* itself runs after decoy paint, so the darkened hole still wins visually.
|
|
18
|
+
*/
|
|
19
|
+
var NOTCH_CLEARANCE = 0;
|
|
20
|
+
/** Placement retries per decoy before giving up on that slot. */
|
|
21
|
+
var PLACEMENT_ATTEMPTS = 8;
|
|
22
|
+
var clamp255 = (v) => v < 0 ? 0 : v > 255 ? 255 : v;
|
|
23
|
+
var distanceSquared = (cx, cy, other) => {
|
|
24
|
+
const dx = cx - other.cx;
|
|
25
|
+
const dy = cy - other.cy;
|
|
26
|
+
return dx * dx + dy * dy;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Depth cue on decoys, mirroring `HOLE_INNER_SHADOW` in `compose.ts`.
|
|
30
|
+
* Smaller so decoys read as shallower cuts than the real hole.
|
|
31
|
+
*/
|
|
32
|
+
var DECOY_INNER_SHADOW = .35;
|
|
33
|
+
/**
|
|
34
|
+
* Composite a jigsaw-shaped decoration onto `background`, centred on
|
|
35
|
+
* `(cx, cy)`. Mutates `background`. Runs the same signed-distance sampling
|
|
36
|
+
* as the real cut, so the silhouette reads as the same family of shapes.
|
|
37
|
+
*/
|
|
38
|
+
var paintDecoyPiece = (background, prng, size, cx, cy, edgeDarkness, bodyBrightness, holeDarken) => {
|
|
39
|
+
const shape = createNotchShape(prng, size);
|
|
40
|
+
const half = size / 2;
|
|
41
|
+
const left = Math.round(cx - half);
|
|
42
|
+
const top = Math.round(cy - half);
|
|
43
|
+
for (let ly = 0; ly < size; ly++) for (let lx = 0; lx < size; lx++) {
|
|
44
|
+
const x = left + lx;
|
|
45
|
+
const y = top + ly;
|
|
46
|
+
if (x < 0 || y < 0 || x >= background.width || y >= background.height) continue;
|
|
47
|
+
const d = shape.distance(lx, ly);
|
|
48
|
+
const coverage = coverageFromDistance(d);
|
|
49
|
+
if (coverage <= 0) continue;
|
|
50
|
+
const bi = (y * background.width + x) * 4;
|
|
51
|
+
const r = background.data[bi] ?? 0;
|
|
52
|
+
const g = background.data[bi + 1] ?? 0;
|
|
53
|
+
const b = background.data[bi + 2] ?? 0;
|
|
54
|
+
const factor = holeDarken * (1 - (d > -3 ? DECOY_INNER_SHADOW * (1 + d / 3) : 0));
|
|
55
|
+
const dr = r * factor;
|
|
56
|
+
const dg = g * factor;
|
|
57
|
+
const db = b * factor;
|
|
58
|
+
const delta = bodyBrightness - (d > -3 ? edgeDarkness * (1 + d / 3) : 0);
|
|
59
|
+
const mixR = r * (1 - coverage) + dr * coverage;
|
|
60
|
+
const mixG = g * (1 - coverage) + dg * coverage;
|
|
61
|
+
const mixB = b * (1 - coverage) + db * coverage;
|
|
62
|
+
background.data[bi] = clamp255(Math.round(mixR + delta * coverage));
|
|
63
|
+
background.data[bi + 1] = clamp255(Math.round(mixG + delta * coverage));
|
|
64
|
+
background.data[bi + 2] = clamp255(Math.round(mixB + delta * coverage));
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Fractional jitter around the configured decoy count. ±30% means a
|
|
69
|
+
* configured 5 draws anywhere in [4, 6]; a configured 100 draws in [70,
|
|
70
|
+
* 130]. Randomising the count prevents a solver from keying on "exactly N
|
|
71
|
+
* jigsaw regions on the board" as a stable feature.
|
|
72
|
+
*/
|
|
73
|
+
var COUNT_JITTER = .3;
|
|
74
|
+
/**
|
|
75
|
+
* Paint decoys onto `background`. The actual number placed varies per-
|
|
76
|
+
* challenge within ±COUNT_JITTER of `targetCount`, so a solver can't
|
|
77
|
+
* hard-code the expected count. Decoys avoid only the real notch
|
|
78
|
+
* placement (they may freely overlap each other). If a slot cannot be
|
|
79
|
+
* placed after `PLACEMENT_ATTEMPTS`, it is silently dropped — decoys are
|
|
80
|
+
* cosmetic, so an occasional miss is fine.
|
|
81
|
+
*/
|
|
82
|
+
var paintDecoys = (background, prng, targetCount, notchSize, notch, edgeDarkness, bodyBrightness, holeDarken) => {
|
|
83
|
+
if (targetCount <= 0) return;
|
|
84
|
+
const jitter = 1 + prng.range(-COUNT_JITTER, COUNT_JITTER);
|
|
85
|
+
const count = Math.max(0, Math.round(targetCount * jitter));
|
|
86
|
+
if (count === 0) return;
|
|
87
|
+
const notchClearanceSq = (notchSize * NOTCH_CLEARANCE) ** 2;
|
|
88
|
+
const minX = 0;
|
|
89
|
+
const maxX = background.width;
|
|
90
|
+
const minY = 0;
|
|
91
|
+
const maxY = background.height;
|
|
92
|
+
for (let i = 0; i < count; i++) for (let attempt = 0; attempt < PLACEMENT_ATTEMPTS; attempt++) {
|
|
93
|
+
const cx = prng.range(minX, maxX);
|
|
94
|
+
const cy = prng.range(minY, maxY);
|
|
95
|
+
if (distanceSquared(cx, cy, {
|
|
96
|
+
cx: notch.targetX,
|
|
97
|
+
cy: notch.targetY
|
|
98
|
+
}) < notchClearanceSq) continue;
|
|
99
|
+
paintDecoyPiece(background, prng, notchSize, cx, cy, edgeDarkness, bodyBrightness, holeDarken);
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
//#endregion
|
|
104
|
+
export { paintDecoyPiece, paintDecoys };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decoys.js","sourceRoot":"","sources":["../src/decoys.ts"],"names":[],"mappings":"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"}
|
package/dist/encode.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { RgbaImage } from "./types.js";
|
|
2
|
+
export declare const encodeBackground: (image: RgbaImage) => Promise<Buffer>;
|
|
3
|
+
export declare const encodePiece: (image: RgbaImage) => Promise<Buffer>;
|
|
4
|
+
export declare const encodePng: (image: RgbaImage) => Promise<Buffer>;
|
|
5
|
+
export declare const toDataUri: (webp: Buffer) => string;
|
|
6
|
+
//# sourceMappingURL=encode.d.ts.map
|