@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
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# @prosopo/puzzle-assets
|
|
2
|
+
|
|
3
|
+
## 0.1.3
|
|
4
|
+
### Patch Changes
|
|
5
|
+
|
|
6
|
+
- a9c0406: Add vite export path
|
|
7
|
+
- 9bf4570: Export code for use in the portal
|
|
8
|
+
|
|
9
|
+
## 0.1.2
|
|
10
|
+
### Patch Changes
|
|
11
|
+
|
|
12
|
+
- 572f965: chore(puzzle-assets): mark private to unblock changeset publish
|
|
13
|
+
|
|
14
|
+
`@prosopo/puzzle-assets` was never published to npm (registry returns
|
|
15
|
+
404). `publish_release` calls `npx changeset publish` which walks every
|
|
16
|
+
non-private workspace; the trusted-publisher OIDC flow only works for
|
|
17
|
+
packages that already exist on the registry, so the first-time publish
|
|
18
|
+
hit `ENEEDAUTH` and aborted the whole job — that's why v3.7.15, v3.7.16
|
|
19
|
+
and v3.7.17 all failed at the same step and never published the docker
|
|
20
|
+
image tags either.
|
|
21
|
+
|
|
22
|
+
`puzzle-assets` is only consumed inside the workspace (provider bundles
|
|
23
|
+
it into the CLI docker image); it has no external consumers, so mark it
|
|
24
|
+
`private: true`. changeset skips private packages by design, so the
|
|
25
|
+
release pipeline goes green without needing a legacy npm token or a
|
|
26
|
+
manual bootstrap publish.
|
|
27
|
+
|
|
28
|
+
If we ever want to publish it externally, drop `private` and either
|
|
29
|
+
seed a first publish with a legacy `NPM_TOKEN` or configure the package
|
|
30
|
+
as a trusted publisher on npmjs.com first.
|
|
31
|
+
|
|
32
|
+
## 0.1.1
|
|
33
|
+
### Patch Changes
|
|
34
|
+
|
|
35
|
+
- 35f640f: Render puzzle captcha imagery on the provider instead of sending the answer to the client.
|
|
36
|
+
|
|
37
|
+
The challenge used to carry `targetX`/`targetY` and the widget drew the target box straight from them, so any HTTP client could echo the coordinates back as its solution and pass without a browser. The provider now synthesises a background procedurally, cuts the notch into the pixels, and returns the background and piece as data URIs; the target and the tolerance never leave the server.
|
|
38
|
+
|
|
39
|
+
Backgrounds come from the new `@prosopo/puzzle-assets` package and are single-use — reusing one across two challenges would let an attacker diff the composites and recover both notch positions.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"background.d.ts","sourceRoot":"","sources":["../src/background.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAqF5C,eAAO,MAAM,kBAAkB,SACxB,IAAI,SACH,MAAM,UACL,MAAM,KACZ,SAmFF,CAAC"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { drawPalette } from "./palette.js";
|
|
2
|
+
//#region src/background.ts
|
|
3
|
+
/**
|
|
4
|
+
* Grain amplitude, in 0-255 units, applied per channel.
|
|
5
|
+
*
|
|
6
|
+
* This is the one knob serving both goals at once. Visually it reads as film
|
|
7
|
+
* grain and stops the mesh gradient banding on 8-bit displays. Structurally it
|
|
8
|
+
* raises the noise floor an edge detector has to clear to find the notch
|
|
9
|
+
* boundary — a perfectly smooth gradient would hand the target over to a Canny
|
|
10
|
+
* filter. Too high and the image looks like TV static, so it stays subtle.
|
|
11
|
+
*/
|
|
12
|
+
var GRAIN_AMPLITUDE = 5;
|
|
13
|
+
var VIGNETTE_STRENGTH = .16;
|
|
14
|
+
var clamp255 = (v) => v < 0 ? 0 : v > 255 ? 255 : v;
|
|
15
|
+
/**
|
|
16
|
+
* Smooth multi-colour blend: every pixel is a Gaussian-weighted mix of the
|
|
17
|
+
* control points, giving the "mesh gradient" look — broad soft colour fields
|
|
18
|
+
* with no hard edges anywhere.
|
|
19
|
+
*
|
|
20
|
+
* The falloff has to be local. Inverse-distance weighting (1/d^2) looks right
|
|
21
|
+
* on paper but at 300x200 every control point is a comparable distance from
|
|
22
|
+
* every pixel, so the weights come out near-equal and the whole frame collapses
|
|
23
|
+
* to the palette's mean — a flat grey-ish wash. A Gaussian with sigma around a
|
|
24
|
+
* third of the frame keeps each point dominant near itself and still blends
|
|
25
|
+
* smoothly in between.
|
|
26
|
+
*/
|
|
27
|
+
var meshColourAt = (points, x, y) => {
|
|
28
|
+
let wSum = 0;
|
|
29
|
+
let r = 0;
|
|
30
|
+
let g = 0;
|
|
31
|
+
let b = 0;
|
|
32
|
+
for (const point of points) {
|
|
33
|
+
const dx = x - point.x;
|
|
34
|
+
const dy = y - point.y;
|
|
35
|
+
const w = Math.exp(-(dx * dx + dy * dy) / (2 * point.sigma * point.sigma));
|
|
36
|
+
wSum += w;
|
|
37
|
+
r += point.colour.r * w;
|
|
38
|
+
g += point.colour.g * w;
|
|
39
|
+
b += point.colour.b * w;
|
|
40
|
+
}
|
|
41
|
+
if (wSum < 1e-6) {
|
|
42
|
+
let best = points[0];
|
|
43
|
+
if (!best) throw new Error("puzzle-assets: no mesh points");
|
|
44
|
+
let bestD = Number.POSITIVE_INFINITY;
|
|
45
|
+
for (const point of points) {
|
|
46
|
+
const d = (x - point.x) ** 2 + (y - point.y) ** 2;
|
|
47
|
+
if (d < bestD) {
|
|
48
|
+
bestD = d;
|
|
49
|
+
best = point;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return best.colour;
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
r: r / wSum,
|
|
56
|
+
g: g / wSum,
|
|
57
|
+
b: b / wSum
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Synthesise a background. Deterministic in `prng`, so the same seed always
|
|
62
|
+
* yields the same picture — see the seed-secrecy note in prng.ts for why that
|
|
63
|
+
* determinism is safe only while the seed stays on the provider.
|
|
64
|
+
*/
|
|
65
|
+
var generateBackground = (prng, width, height) => {
|
|
66
|
+
const pointCount = prng.int(4, 6);
|
|
67
|
+
const palette = drawPalette(prng, pointCount);
|
|
68
|
+
const blobPalette = drawPalette(prng, 3);
|
|
69
|
+
const minDimension = Math.min(width, height);
|
|
70
|
+
const points = [];
|
|
71
|
+
for (let i = 0; i < pointCount; i++) {
|
|
72
|
+
const colour = palette[i];
|
|
73
|
+
if (!colour) throw new Error("puzzle-assets: palette underflow");
|
|
74
|
+
points.push({
|
|
75
|
+
x: prng.range(-.15, 1.15) * width,
|
|
76
|
+
y: prng.range(-.15, 1.15) * height,
|
|
77
|
+
colour,
|
|
78
|
+
sigma: prng.range(.26, .48) * minDimension
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const blobs = [];
|
|
82
|
+
const blobCount = prng.int(2, 4);
|
|
83
|
+
for (let i = 0; i < blobCount; i++) {
|
|
84
|
+
const colour = blobPalette[i % blobPalette.length];
|
|
85
|
+
if (!colour) throw new Error("puzzle-assets: palette underflow");
|
|
86
|
+
blobs.push({
|
|
87
|
+
x: prng.range(.1, .9) * width,
|
|
88
|
+
y: prng.range(.1, .9) * height,
|
|
89
|
+
sigma: prng.range(.18, .38) * minDimension,
|
|
90
|
+
colour,
|
|
91
|
+
alpha: prng.range(.16, .32)
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const data = Buffer.alloc(width * height * 4);
|
|
95
|
+
const cx = width / 2;
|
|
96
|
+
const cy = height / 2;
|
|
97
|
+
const maxRadius = Math.sqrt(cx * cx + cy * cy);
|
|
98
|
+
for (let y = 0; y < height; y++) for (let x = 0; x < width; x++) {
|
|
99
|
+
const base = meshColourAt(points, x, y);
|
|
100
|
+
let r = base.r;
|
|
101
|
+
let g = base.g;
|
|
102
|
+
let b = base.b;
|
|
103
|
+
for (const blob of blobs) {
|
|
104
|
+
const dx = x - blob.x;
|
|
105
|
+
const dy = y - blob.y;
|
|
106
|
+
const a = blob.alpha * Math.exp(-(dx * dx + dy * dy) / (2 * blob.sigma * blob.sigma));
|
|
107
|
+
r += (blob.colour.r - r) * a;
|
|
108
|
+
g += (blob.colour.g - g) * a;
|
|
109
|
+
b += (blob.colour.b - b) * a;
|
|
110
|
+
}
|
|
111
|
+
const dxc = x - cx;
|
|
112
|
+
const dyc = y - cy;
|
|
113
|
+
const vignette = 1 - VIGNETTE_STRENGTH * ((dxc * dxc + dyc * dyc) / (maxRadius * maxRadius));
|
|
114
|
+
r *= vignette;
|
|
115
|
+
g *= vignette;
|
|
116
|
+
b *= vignette;
|
|
117
|
+
const grain = (prng.next() - .5) * 2 * GRAIN_AMPLITUDE;
|
|
118
|
+
const i = (y * width + x) * 4;
|
|
119
|
+
data[i] = clamp255(Math.round(r + grain));
|
|
120
|
+
data[i + 1] = clamp255(Math.round(g + grain));
|
|
121
|
+
data[i + 2] = clamp255(Math.round(b + grain));
|
|
122
|
+
data[i + 3] = 255;
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
data,
|
|
126
|
+
width,
|
|
127
|
+
height
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
//#endregion
|
|
131
|
+
export { generateBackground };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"background.js","sourceRoot":"","sources":["../src/background.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { NotchPlacement, PuzzleGeometry, PuzzleRenderSettings, RgbaImage } from "./types.js";
|
|
2
|
+
export { createPrng, createSeed, SEED_BYTES } from "./prng.js";
|
|
3
|
+
export { generateBackground } from "./background.js";
|
|
4
|
+
export { paintDecoys, paintDecoyPiece } from "./decoys.js";
|
|
5
|
+
export type { NotchPlacement, PuzzleGeometry, PuzzleRenderSettings, RenderedPuzzle, RgbaImage, } from "./types.js";
|
|
6
|
+
export declare const DEFAULT_GEOMETRY: PuzzleGeometry;
|
|
7
|
+
export declare const DEFAULT_RENDER_SETTINGS: PuzzleRenderSettings;
|
|
8
|
+
export interface RenderedPuzzleRgba {
|
|
9
|
+
background: RgbaImage;
|
|
10
|
+
piece: RgbaImage;
|
|
11
|
+
pieceSize: number;
|
|
12
|
+
}
|
|
13
|
+
export declare const renderPuzzleToRgba: (background: RgbaImage, placement: NotchPlacement, geometry?: PuzzleGeometry, settings?: PuzzleRenderSettings) => RenderedPuzzleRgba;
|
|
14
|
+
export declare const createBackground: (geometry?: PuzzleGeometry) => RgbaImage;
|
|
15
|
+
//# sourceMappingURL=browser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EACX,cAAc,EACd,cAAc,EACd,oBAAoB,EACpB,SAAS,EACT,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC3D,YAAY,EACX,cAAc,EACd,cAAc,EACd,oBAAoB,EACpB,cAAc,EACd,SAAS,GACT,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,gBAAgB,EAAE,cAI9B,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAE,oBAMrC,CAAC;AAEF,MAAM,WAAW,kBAAkB;IAClC,UAAU,EAAE,SAAS,CAAC;IACtB,KAAK,EAAE,SAAS,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CAClB;AAOD,eAAO,MAAM,kBAAkB,eAClB,SAAS,aACV,cAAc,aACf,cAAc,aACd,oBAAoB,KAC5B,kBAuBF,CAAC;AAEF,eAAO,MAAM,gBAAgB,cAClB,cAAc,KACtB,SAC2E,CAAC"}
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import "./_virtual/_rolldown/runtime.js";
|
|
2
|
+
import { generateBackground } from "./background.js";
|
|
3
|
+
import { createNotchShape } from "./notch.js";
|
|
4
|
+
import { cutNotch } from "./compose.js";
|
|
5
|
+
import { paintDecoyPiece, paintDecoys } from "./decoys.js";
|
|
6
|
+
import { SEED_BYTES, createPrng, createSeed } from "./prng.js";
|
|
7
|
+
//#region src/browser.ts
|
|
8
|
+
var DEFAULT_GEOMETRY = {
|
|
9
|
+
width: 300,
|
|
10
|
+
height: 200,
|
|
11
|
+
notchSize: 44
|
|
12
|
+
};
|
|
13
|
+
var DEFAULT_RENDER_SETTINGS = {
|
|
14
|
+
decoyCount: 5,
|
|
15
|
+
decoyEdgeDarkness: 20,
|
|
16
|
+
decoyBodyBrightness: 4,
|
|
17
|
+
holeDarken: .55,
|
|
18
|
+
decoyHoleDarken: .7
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Same pipeline as `renderPuzzle` in index.ts but returns raw RGBA rather
|
|
22
|
+
* than WebP-encoded buffers, so browser consumers can `putImageData` onto
|
|
23
|
+
* a canvas without needing sharp.
|
|
24
|
+
*/
|
|
25
|
+
var renderPuzzleToRgba = (background, placement, geometry = DEFAULT_GEOMETRY, settings = DEFAULT_RENDER_SETTINGS) => {
|
|
26
|
+
const prng = createPrng(createSeed());
|
|
27
|
+
const pieceSize = geometry.pieceSize ?? geometry.notchSize;
|
|
28
|
+
paintDecoys(background, prng, settings.decoyCount, pieceSize, placement, settings.decoyEdgeDarkness, settings.decoyBodyBrightness, settings.decoyHoleDarken);
|
|
29
|
+
const { background: cut, piece } = cutNotch(prng, background, createNotchShape(prng, pieceSize), pieceSize, placement, settings.holeDarken);
|
|
30
|
+
return {
|
|
31
|
+
background: cut,
|
|
32
|
+
piece,
|
|
33
|
+
pieceSize
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
var createBackground = (geometry = DEFAULT_GEOMETRY) => generateBackground(createPrng(createSeed()), geometry.width, geometry.height);
|
|
37
|
+
//#endregion
|
|
38
|
+
export { DEFAULT_GEOMETRY, DEFAULT_RENDER_SETTINGS, SEED_BYTES, createBackground, createPrng, createSeed, generateBackground, paintDecoyPiece, paintDecoys, renderPuzzleToRgba };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.js","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAQnD,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAS3D,MAAM,CAAC,MAAM,gBAAgB,GAAmB;IAC/C,KAAK,EAAE,GAAG;IACV,MAAM,EAAE,GAAG;IACX,SAAS,EAAE,EAAE;CACb,CAAC;AAEF,MAAM,CAAC,MAAM,uBAAuB,GAAyB;IAC5D,UAAU,EAAE,CAAC;IACb,iBAAiB,EAAE,EAAE;IACrB,mBAAmB,EAAE,CAAC;IACtB,UAAU,EAAE,IAAI;IAChB,eAAe,EAAE,GAAG;CACpB,CAAC;AAaF,MAAM,CAAC,MAAM,kBAAkB,GAAG,CACjC,UAAqB,EACrB,SAAyB,EACzB,WAA2B,gBAAgB,EAC3C,WAAiC,uBAAuB,EACnC,EAAE;IACvB,MAAM,IAAI,GAAG,UAAU,CAAC,UAAU,EAAE,CAAC,CAAC;IACtC,MAAM,SAAS,GAAG,QAAQ,CAAC,SAAS,IAAI,QAAQ,CAAC,SAAS,CAAC;IAC3D,WAAW,CACV,UAAU,EACV,IAAI,EACJ,QAAQ,CAAC,UAAU,EACnB,SAAS,EACT,SAAS,EACT,QAAQ,CAAC,iBAAiB,EAC1B,QAAQ,CAAC,mBAAmB,EAC5B,QAAQ,CAAC,eAAe,CACxB,CAAC;IACF,MAAM,KAAK,GAAG,gBAAgB,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IAChD,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,QAAQ,CAC1C,IAAI,EACJ,UAAU,EACV,KAAK,EACL,SAAS,EACT,SAAS,EACT,QAAQ,CAAC,UAAU,CACnB,CAAC;IACF,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;AAC9C,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAC/B,WAA2B,gBAAgB,EAC/B,EAAE,CACd,kBAAkB,CAAC,UAAU,CAAC,UAAU,EAAE,CAAC,EAAE,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
+
key = keys[i];
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
+
get: ((k) => from[k]).bind(null, key),
|
|
13
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
+
value: mod,
|
|
20
|
+
enumerable: true
|
|
21
|
+
}) : target, mod));
|
|
22
|
+
//#endregion
|
|
23
|
+
exports.__toESM = __toESM;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
const require_palette = require("./palette.cjs");
|
|
2
|
+
//#region src/background.ts
|
|
3
|
+
/**
|
|
4
|
+
* Grain amplitude, in 0-255 units, applied per channel.
|
|
5
|
+
*
|
|
6
|
+
* This is the one knob serving both goals at once. Visually it reads as film
|
|
7
|
+
* grain and stops the mesh gradient banding on 8-bit displays. Structurally it
|
|
8
|
+
* raises the noise floor an edge detector has to clear to find the notch
|
|
9
|
+
* boundary — a perfectly smooth gradient would hand the target over to a Canny
|
|
10
|
+
* filter. Too high and the image looks like TV static, so it stays subtle.
|
|
11
|
+
*/
|
|
12
|
+
var GRAIN_AMPLITUDE = 5;
|
|
13
|
+
var VIGNETTE_STRENGTH = .16;
|
|
14
|
+
var clamp255 = (v) => v < 0 ? 0 : v > 255 ? 255 : v;
|
|
15
|
+
/**
|
|
16
|
+
* Smooth multi-colour blend: every pixel is a Gaussian-weighted mix of the
|
|
17
|
+
* control points, giving the "mesh gradient" look — broad soft colour fields
|
|
18
|
+
* with no hard edges anywhere.
|
|
19
|
+
*
|
|
20
|
+
* The falloff has to be local. Inverse-distance weighting (1/d^2) looks right
|
|
21
|
+
* on paper but at 300x200 every control point is a comparable distance from
|
|
22
|
+
* every pixel, so the weights come out near-equal and the whole frame collapses
|
|
23
|
+
* to the palette's mean — a flat grey-ish wash. A Gaussian with sigma around a
|
|
24
|
+
* third of the frame keeps each point dominant near itself and still blends
|
|
25
|
+
* smoothly in between.
|
|
26
|
+
*/
|
|
27
|
+
var meshColourAt = (points, x, y) => {
|
|
28
|
+
let wSum = 0;
|
|
29
|
+
let r = 0;
|
|
30
|
+
let g = 0;
|
|
31
|
+
let b = 0;
|
|
32
|
+
for (const point of points) {
|
|
33
|
+
const dx = x - point.x;
|
|
34
|
+
const dy = y - point.y;
|
|
35
|
+
const w = Math.exp(-(dx * dx + dy * dy) / (2 * point.sigma * point.sigma));
|
|
36
|
+
wSum += w;
|
|
37
|
+
r += point.colour.r * w;
|
|
38
|
+
g += point.colour.g * w;
|
|
39
|
+
b += point.colour.b * w;
|
|
40
|
+
}
|
|
41
|
+
if (wSum < 1e-6) {
|
|
42
|
+
let best = points[0];
|
|
43
|
+
if (!best) throw new Error("puzzle-assets: no mesh points");
|
|
44
|
+
let bestD = Number.POSITIVE_INFINITY;
|
|
45
|
+
for (const point of points) {
|
|
46
|
+
const d = (x - point.x) ** 2 + (y - point.y) ** 2;
|
|
47
|
+
if (d < bestD) {
|
|
48
|
+
bestD = d;
|
|
49
|
+
best = point;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return best.colour;
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
r: r / wSum,
|
|
56
|
+
g: g / wSum,
|
|
57
|
+
b: b / wSum
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Synthesise a background. Deterministic in `prng`, so the same seed always
|
|
62
|
+
* yields the same picture — see the seed-secrecy note in prng.ts for why that
|
|
63
|
+
* determinism is safe only while the seed stays on the provider.
|
|
64
|
+
*/
|
|
65
|
+
var generateBackground = (prng, width, height) => {
|
|
66
|
+
const pointCount = prng.int(4, 6);
|
|
67
|
+
const palette = require_palette.drawPalette(prng, pointCount);
|
|
68
|
+
const blobPalette = require_palette.drawPalette(prng, 3);
|
|
69
|
+
const minDimension = Math.min(width, height);
|
|
70
|
+
const points = [];
|
|
71
|
+
for (let i = 0; i < pointCount; i++) {
|
|
72
|
+
const colour = palette[i];
|
|
73
|
+
if (!colour) throw new Error("puzzle-assets: palette underflow");
|
|
74
|
+
points.push({
|
|
75
|
+
x: prng.range(-.15, 1.15) * width,
|
|
76
|
+
y: prng.range(-.15, 1.15) * height,
|
|
77
|
+
colour,
|
|
78
|
+
sigma: prng.range(.26, .48) * minDimension
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const blobs = [];
|
|
82
|
+
const blobCount = prng.int(2, 4);
|
|
83
|
+
for (let i = 0; i < blobCount; i++) {
|
|
84
|
+
const colour = blobPalette[i % blobPalette.length];
|
|
85
|
+
if (!colour) throw new Error("puzzle-assets: palette underflow");
|
|
86
|
+
blobs.push({
|
|
87
|
+
x: prng.range(.1, .9) * width,
|
|
88
|
+
y: prng.range(.1, .9) * height,
|
|
89
|
+
sigma: prng.range(.18, .38) * minDimension,
|
|
90
|
+
colour,
|
|
91
|
+
alpha: prng.range(.16, .32)
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const data = Buffer.alloc(width * height * 4);
|
|
95
|
+
const cx = width / 2;
|
|
96
|
+
const cy = height / 2;
|
|
97
|
+
const maxRadius = Math.sqrt(cx * cx + cy * cy);
|
|
98
|
+
for (let y = 0; y < height; y++) for (let x = 0; x < width; x++) {
|
|
99
|
+
const base = meshColourAt(points, x, y);
|
|
100
|
+
let r = base.r;
|
|
101
|
+
let g = base.g;
|
|
102
|
+
let b = base.b;
|
|
103
|
+
for (const blob of blobs) {
|
|
104
|
+
const dx = x - blob.x;
|
|
105
|
+
const dy = y - blob.y;
|
|
106
|
+
const a = blob.alpha * Math.exp(-(dx * dx + dy * dy) / (2 * blob.sigma * blob.sigma));
|
|
107
|
+
r += (blob.colour.r - r) * a;
|
|
108
|
+
g += (blob.colour.g - g) * a;
|
|
109
|
+
b += (blob.colour.b - b) * a;
|
|
110
|
+
}
|
|
111
|
+
const dxc = x - cx;
|
|
112
|
+
const dyc = y - cy;
|
|
113
|
+
const vignette = 1 - VIGNETTE_STRENGTH * ((dxc * dxc + dyc * dyc) / (maxRadius * maxRadius));
|
|
114
|
+
r *= vignette;
|
|
115
|
+
g *= vignette;
|
|
116
|
+
b *= vignette;
|
|
117
|
+
const grain = (prng.next() - .5) * 2 * GRAIN_AMPLITUDE;
|
|
118
|
+
const i = (y * width + x) * 4;
|
|
119
|
+
data[i] = clamp255(Math.round(r + grain));
|
|
120
|
+
data[i + 1] = clamp255(Math.round(g + grain));
|
|
121
|
+
data[i + 2] = clamp255(Math.round(b + grain));
|
|
122
|
+
data[i + 3] = 255;
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
data,
|
|
126
|
+
width,
|
|
127
|
+
height
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
//#endregion
|
|
131
|
+
exports.generateBackground = generateBackground;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const require_notch = require("./notch.cjs");
|
|
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 = require_notch.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 = require_notch.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
|
+
exports.cutNotch = cutNotch;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const require_notch = require("./notch.cjs");
|
|
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 = require_notch.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 = require_notch.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
|
+
exports.paintDecoyPiece = paintDecoyPiece;
|
|
105
|
+
exports.paintDecoys = paintDecoys;
|