@netforge/engine 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/GAME-DESIGN.md +195 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +30 -0
  4. package/README.md +147 -2
  5. package/dist/api/appearance.d.ts +37 -0
  6. package/dist/api/appearance.js +41 -0
  7. package/dist/api/cards.d.ts +105 -0
  8. package/dist/api/cards.js +133 -0
  9. package/dist/api/collector.d.ts +114 -0
  10. package/dist/api/collector.js +204 -0
  11. package/dist/api/dungeon.d.ts +58 -0
  12. package/dist/api/dungeon.js +216 -0
  13. package/dist/api/harness.d.ts +149 -0
  14. package/dist/api/harness.js +549 -0
  15. package/dist/api/idle.d.ts +71 -0
  16. package/dist/api/idle.js +104 -0
  17. package/dist/api/race-harness.d.ts +3 -0
  18. package/dist/api/race-harness.js +148 -0
  19. package/dist/api/registry.d.ts +241 -0
  20. package/dist/api/registry.js +394 -0
  21. package/dist/api/scheduler.d.ts +56 -0
  22. package/dist/api/scheduler.js +160 -0
  23. package/dist/api/schemas.d.ts +958 -0
  24. package/dist/api/schemas.js +859 -0
  25. package/dist/api/tower.d.ts +127 -0
  26. package/dist/api/tower.js +183 -0
  27. package/dist/game/adapter.d.ts +69 -0
  28. package/dist/game/adapter.js +341 -0
  29. package/dist/game/arena-bot.d.ts +66 -0
  30. package/dist/game/arena-bot.js +188 -0
  31. package/dist/game/bot.d.ts +95 -0
  32. package/dist/game/bot.js +285 -0
  33. package/dist/game/compile.d.ts +97 -0
  34. package/dist/game/compile.js +476 -0
  35. package/dist/game/director.d.ts +42 -0
  36. package/dist/game/director.js +179 -0
  37. package/dist/game/earn-source.d.ts +122 -0
  38. package/dist/game/earn-source.js +386 -0
  39. package/dist/game/earn.d.ts +49 -0
  40. package/dist/game/earn.js +55 -0
  41. package/dist/game/index.d.ts +37 -0
  42. package/dist/game/index.js +25 -0
  43. package/dist/game/platformer.d.ts +156 -0
  44. package/dist/game/platformer.js +619 -0
  45. package/dist/game/profile.d.ts +97 -0
  46. package/dist/game/profile.js +140 -0
  47. package/dist/game/race.d.ts +177 -0
  48. package/dist/game/race.js +386 -0
  49. package/dist/game/racer.d.ts +98 -0
  50. package/dist/game/racer.js +379 -0
  51. package/dist/game/runner.d.ts +286 -0
  52. package/dist/game/runner.js +824 -0
  53. package/dist/game/threats.d.ts +23 -0
  54. package/dist/game/threats.js +49 -0
  55. package/dist/game/view.d.ts +47 -0
  56. package/dist/game/view.js +123 -0
  57. package/dist/index.d.ts +33 -0
  58. package/dist/index.js +24 -0
  59. package/dist/net/bits.d.ts +33 -0
  60. package/dist/net/bits.js +135 -0
  61. package/dist/net/client.d.ts +75 -0
  62. package/dist/net/client.js +151 -0
  63. package/dist/net/index.d.ts +23 -0
  64. package/dist/net/index.js +23 -0
  65. package/dist/net/input.d.ts +49 -0
  66. package/dist/net/input.js +112 -0
  67. package/dist/net/interpolation.d.ts +37 -0
  68. package/dist/net/interpolation.js +97 -0
  69. package/dist/net/netsim.d.ts +22 -0
  70. package/dist/net/netsim.js +52 -0
  71. package/dist/net/prediction.d.ts +31 -0
  72. package/dist/net/prediction.js +101 -0
  73. package/dist/net/profile-client.d.ts +75 -0
  74. package/dist/net/profile-client.js +173 -0
  75. package/dist/net/protocol.d.ts +208 -0
  76. package/dist/net/protocol.js +8 -0
  77. package/dist/net/room-client.d.ts +57 -0
  78. package/dist/net/room-client.js +144 -0
  79. package/dist/net/schema.d.ts +78 -0
  80. package/dist/net/schema.js +121 -0
  81. package/dist/net/sim-adapter.d.ts +134 -0
  82. package/dist/net/sim-adapter.js +22 -0
  83. package/dist/net/snapshot.d.ts +61 -0
  84. package/dist/net/snapshot.js +301 -0
  85. package/dist/rules/adapter.d.ts +53 -0
  86. package/dist/rules/adapter.js +261 -0
  87. package/dist/rules/cards.d.ts +115 -0
  88. package/dist/rules/cards.js +547 -0
  89. package/dist/rules/collector.d.ts +161 -0
  90. package/dist/rules/collector.js +732 -0
  91. package/dist/rules/genres.d.ts +13 -0
  92. package/dist/rules/genres.js +38 -0
  93. package/dist/rules/harness.d.ts +16 -0
  94. package/dist/rules/harness.js +127 -0
  95. package/dist/rules/idle.d.ts +115 -0
  96. package/dist/rules/idle.js +370 -0
  97. package/dist/rules/index.d.ts +16 -0
  98. package/dist/rules/index.js +14 -0
  99. package/dist/rules/kit.d.ts +171 -0
  100. package/dist/rules/kit.js +88 -0
  101. package/dist/rules/tower.d.ts +214 -0
  102. package/dist/rules/tower.js +655 -0
  103. package/dist/runtime/arena.d.ts +86 -0
  104. package/dist/runtime/arena.js +269 -0
  105. package/dist/runtime/assets.d.ts +26 -0
  106. package/dist/runtime/assets.js +35 -0
  107. package/dist/runtime/camera.d.ts +113 -0
  108. package/dist/runtime/camera.js +247 -0
  109. package/dist/runtime/crowd.d.ts +18 -0
  110. package/dist/runtime/crowd.js +62 -0
  111. package/dist/runtime/feel/audio.d.ts +150 -0
  112. package/dist/runtime/feel/audio.js +327 -0
  113. package/dist/runtime/feel/clock.d.ts +52 -0
  114. package/dist/runtime/feel/clock.js +80 -0
  115. package/dist/runtime/feel/flash.d.ts +28 -0
  116. package/dist/runtime/feel/flash.js +60 -0
  117. package/dist/runtime/feel/index.d.ts +27 -0
  118. package/dist/runtime/feel/index.js +16 -0
  119. package/dist/runtime/feel/kit.d.ts +159 -0
  120. package/dist/runtime/feel/kit.js +226 -0
  121. package/dist/runtime/feel/knockback.d.ts +41 -0
  122. package/dist/runtime/feel/knockback.js +70 -0
  123. package/dist/runtime/feel/noise.d.ts +6 -0
  124. package/dist/runtime/feel/noise.js +30 -0
  125. package/dist/runtime/feel/numbers.d.ts +91 -0
  126. package/dist/runtime/feel/numbers.js +172 -0
  127. package/dist/runtime/feel/rumble.d.ts +40 -0
  128. package/dist/runtime/feel/rumble.js +58 -0
  129. package/dist/runtime/feel/sound.d.ts +96 -0
  130. package/dist/runtime/feel/sound.js +176 -0
  131. package/dist/runtime/feel/spring.d.ts +42 -0
  132. package/dist/runtime/feel/spring.js +95 -0
  133. package/dist/runtime/feel/trauma.d.ts +48 -0
  134. package/dist/runtime/feel/trauma.js +76 -0
  135. package/dist/runtime/feel/tween.d.ts +66 -0
  136. package/dist/runtime/feel/tween.js +180 -0
  137. package/dist/runtime/feel/types.d.ts +30 -0
  138. package/dist/runtime/feel/types.js +5 -0
  139. package/dist/runtime/index.d.ts +28 -0
  140. package/dist/runtime/index.js +26 -0
  141. package/dist/runtime/input.d.ts +38 -0
  142. package/dist/runtime/input.js +159 -0
  143. package/dist/runtime/interpolate.d.ts +6 -0
  144. package/dist/runtime/interpolate.js +13 -0
  145. package/dist/runtime/render/animation.d.ts +63 -0
  146. package/dist/runtime/render/animation.js +128 -0
  147. package/dist/runtime/render/atmosphere.d.ts +32 -0
  148. package/dist/runtime/render/atmosphere.js +59 -0
  149. package/dist/runtime/render/crowd-motion.d.ts +68 -0
  150. package/dist/runtime/render/crowd-motion.js +163 -0
  151. package/dist/runtime/render/decals.d.ts +64 -0
  152. package/dist/runtime/render/decals.js +222 -0
  153. package/dist/runtime/render/highlight.d.ts +29 -0
  154. package/dist/runtime/render/highlight.js +110 -0
  155. package/dist/runtime/render/index.d.ts +35 -0
  156. package/dist/runtime/render/index.js +22 -0
  157. package/dist/runtime/render/lighting.d.ts +50 -0
  158. package/dist/runtime/render/lighting.js +142 -0
  159. package/dist/runtime/render/particles.d.ts +117 -0
  160. package/dist/runtime/render/particles.js +289 -0
  161. package/dist/runtime/render/post.d.ts +92 -0
  162. package/dist/runtime/render/post.js +191 -0
  163. package/dist/runtime/render/presets.d.ts +57 -0
  164. package/dist/runtime/render/presets.js +116 -0
  165. package/dist/runtime/render/quality.d.ts +113 -0
  166. package/dist/runtime/render/quality.js +180 -0
  167. package/dist/runtime/render/rig.d.ts +29 -0
  168. package/dist/runtime/render/rig.js +107 -0
  169. package/dist/runtime/render/sprite.d.ts +34 -0
  170. package/dist/runtime/render/sprite.js +58 -0
  171. package/dist/runtime/render/stack.d.ts +74 -0
  172. package/dist/runtime/render/stack.js +132 -0
  173. package/dist/runtime/render/vat.d.ts +78 -0
  174. package/dist/runtime/render/vat.js +219 -0
  175. package/dist/runtime/renderer.d.ts +19 -0
  176. package/dist/runtime/renderer.js +18 -0
  177. package/dist/runtime/stats.d.ts +11 -0
  178. package/dist/runtime/stats.js +29 -0
  179. package/dist/runtime/styles/handdrawn/ground.d.ts +11 -0
  180. package/dist/runtime/styles/handdrawn/ground.js +282 -0
  181. package/dist/runtime/styles/handdrawn/index.d.ts +4 -0
  182. package/dist/runtime/styles/handdrawn/index.js +94 -0
  183. package/dist/runtime/styles/handdrawn/ink.d.ts +31 -0
  184. package/dist/runtime/styles/handdrawn/ink.js +68 -0
  185. package/dist/runtime/styles/handdrawn/looks.d.ts +18 -0
  186. package/dist/runtime/styles/handdrawn/looks.js +42 -0
  187. package/dist/runtime/styles/handdrawn/paint.d.ts +59 -0
  188. package/dist/runtime/styles/handdrawn/paint.js +129 -0
  189. package/dist/runtime/styles/handdrawn/palette.d.ts +49 -0
  190. package/dist/runtime/styles/handdrawn/palette.js +70 -0
  191. package/dist/runtime/styles/handdrawn/textures.d.ts +27 -0
  192. package/dist/runtime/styles/handdrawn/textures.js +150 -0
  193. package/dist/runtime/styles/handdrawn/vfx.d.ts +14 -0
  194. package/dist/runtime/styles/handdrawn/vfx.js +73 -0
  195. package/dist/runtime/styles/index.d.ts +9 -0
  196. package/dist/runtime/styles/index.js +32 -0
  197. package/dist/runtime/styles/kit.d.ts +193 -0
  198. package/dist/runtime/styles/kit.js +1 -0
  199. package/dist/runtime/styles/pixel/index.d.ts +46 -0
  200. package/dist/runtime/styles/pixel/index.js +167 -0
  201. package/dist/runtime/styles/ps1/index.d.ts +29 -0
  202. package/dist/runtime/styles/ps1/index.js +41 -0
  203. package/dist/runtime/styles/ps1/lights.d.ts +47 -0
  204. package/dist/runtime/styles/ps1/lights.js +146 -0
  205. package/dist/runtime/styles/ps1/materials.d.ts +62 -0
  206. package/dist/runtime/styles/ps1/materials.js +285 -0
  207. package/dist/runtime/styles/ps1/pipeline.d.ts +52 -0
  208. package/dist/runtime/styles/ps1/pipeline.js +111 -0
  209. package/dist/runtime/styles/ps1/surfaces.d.ts +44 -0
  210. package/dist/runtime/styles/ps1/surfaces.js +228 -0
  211. package/dist/runtime/styles/toon/index.d.ts +5 -0
  212. package/dist/runtime/styles/toon/index.js +178 -0
  213. package/dist/runtime/styles/toon/ink.d.ts +39 -0
  214. package/dist/runtime/styles/toon/ink.js +73 -0
  215. package/dist/runtime/styles/toon/look.d.ts +10 -0
  216. package/dist/runtime/styles/toon/look.js +112 -0
  217. package/dist/runtime/styles/toon/material.d.ts +74 -0
  218. package/dist/runtime/styles/toon/material.js +208 -0
  219. package/dist/runtime/styles/toon/normals.d.ts +26 -0
  220. package/dist/runtime/styles/toon/normals.js +127 -0
  221. package/dist/runtime/styles/toon/shadows.d.ts +24 -0
  222. package/dist/runtime/styles/toon/shadows.js +42 -0
  223. package/dist/runtime/styles/toon/vfx.d.ts +15 -0
  224. package/dist/runtime/styles/toon/vfx.js +47 -0
  225. package/dist/runtime/styles/view.d.ts +35 -0
  226. package/dist/runtime/styles/view.js +34 -0
  227. package/dist/runtime/styles/voxel/core.d.ts +114 -0
  228. package/dist/runtime/styles/voxel/core.js +577 -0
  229. package/dist/runtime/styles/voxel/geometry.d.ts +27 -0
  230. package/dist/runtime/styles/voxel/geometry.js +40 -0
  231. package/dist/runtime/styles/voxel/index.d.ts +21 -0
  232. package/dist/runtime/styles/voxel/index.js +23 -0
  233. package/dist/runtime/styles/voxel/kit.d.ts +6 -0
  234. package/dist/runtime/styles/voxel/kit.js +11 -0
  235. package/dist/runtime/styles/voxel/look.d.ts +10 -0
  236. package/dist/runtime/styles/voxel/look.js +16 -0
  237. package/dist/runtime/styles/voxel/props.d.ts +9 -0
  238. package/dist/runtime/styles/voxel/props.js +52 -0
  239. package/dist/runtime/styles/voxel/shapes.d.ts +41 -0
  240. package/dist/runtime/styles/voxel/shapes.js +83 -0
  241. package/dist/runtime/styles/voxel/world.d.ts +49 -0
  242. package/dist/runtime/styles/voxel/world.js +422 -0
  243. package/llms.txt +183 -0
  244. package/package.json +82 -3
@@ -0,0 +1,94 @@
1
+ /**
2
+ * HAND-DRAWN: the world as an ink-and-watercolour illustration.
3
+ *
4
+ * The library's 3D models are painted, not swapped for flat cards: every
5
+ * lit surface is repainted in watercolour washes from one pigment box
6
+ * (paint.ts), pen lines are drawn from the depth of the scene with a
7
+ * wandering, pressure-varied line that is redrawn eight times a second
8
+ * (ink.ts), the frame is printed on paper, effects are painted flecks,
9
+ * blots and brush rings (vfx.ts), and template ground is a painted field
10
+ * or a ruled yard (ground.ts). A game opts in with `game({ style:
11
+ * "handdrawn" })`; nothing in the game changes.
12
+ */
13
+ import { Vector2, Vector4 } from "three/webgpu";
14
+ import { uniform } from "three/tsl";
15
+ import { paintField, paintGlow, paintPaving } from "./ground.js";
16
+ import { inkDisplay, inkScene } from "./ink.js";
17
+ import { handdrawnLook } from "./looks.js";
18
+ import { paintNode, PaintWatcher } from "./paint.js";
19
+ import { noiseTexture, paperTexture, seeded } from "./textures.js";
20
+ import { decalLook, particleLook, ringLook } from "./vfx.js";
21
+ /** Lines, hatching and wash borders are redrawn this many times a second (the boil). */
22
+ export const BOIL_FPS = 8;
23
+ /** Seconds between scans for newly loaded materials. */
24
+ const SCAN_EVERY = 0.25;
25
+ /** Metres one painted ground tile covers, and one paved floor tile (four slabs). */
26
+ const GROUND_TILE = 14;
27
+ const FLOOR_TILE = 8;
28
+ export const handdrawnKit = {
29
+ id: "handdrawn",
30
+ needsPost: true,
31
+ bloom: true,
32
+ look: (preset) => handdrawnLook(preset),
33
+ attach({ renderer, scene, preset }) {
34
+ const noise = noiseTexture();
35
+ const boil = uniform(new Vector2(0, 0));
36
+ const dpr = uniform(1);
37
+ const first = handdrawnLook(preset);
38
+ const paint = { boil, dpr, noise, snap: uniform(0.62), hatch: uniform(1), exposure: uniform(first.paint), values: uniform(new Vector4(...first.values)), glow: uniform(first.glow) };
39
+ const ink = { boil, dpr, noise, paperTex: paperTexture(), lineWidth: uniform(1.9), lineStrength: uniform(0.92), wobble: uniform(1.1), paper: uniform(1) };
40
+ const watcher = new PaintWatcher(scene, () => paintNode(paint));
41
+ const rnd = seeded(97);
42
+ let tick = -1;
43
+ let lastScan = -Infinity;
44
+ const surfaces = new Map();
45
+ return {
46
+ post: {
47
+ scene: (input) => inkScene(input, ink),
48
+ display: (color, input) => inkDisplay(color, input, ink),
49
+ },
50
+ frame(seconds) {
51
+ const t = Math.floor(seconds * BOIL_FPS);
52
+ if (t !== tick) {
53
+ tick = t;
54
+ boil.value.set(rnd(), rnd());
55
+ }
56
+ dpr.value = renderer.getPixelRatio();
57
+ if (seconds - lastScan >= SCAN_EVERY || seconds < lastScan) {
58
+ lastScan = seconds;
59
+ watcher.scan();
60
+ }
61
+ },
62
+ setPreset(p) {
63
+ const l = handdrawnLook(p);
64
+ paint.exposure.value = l.paint;
65
+ paint.values.value.set(...l.values);
66
+ paint.glow.value = l.glow;
67
+ },
68
+ particles: particleLook(noise),
69
+ decals: decalLook(noise),
70
+ rings: ringLook(noise),
71
+ surface(kind) {
72
+ if (kind !== "ground" && kind !== "floor" && kind !== "glow")
73
+ return null;
74
+ let t = surfaces.get(kind);
75
+ if (t === undefined) {
76
+ t = kind === "ground" ? paintField() : kind === "floor" ? paintPaving() : paintGlow();
77
+ surfaces.set(kind, t);
78
+ }
79
+ if (!t)
80
+ return null;
81
+ if (kind === "glow")
82
+ return t;
83
+ const c = t.clone();
84
+ // Metres one tile covers: the pen marks then print at about one screen pixel at the templates' camera distance.
85
+ c.userData = { tile: kind === "ground" ? GROUND_TILE : FLOOR_TILE };
86
+ return c;
87
+ },
88
+ dispose() {
89
+ for (const t of surfaces.values())
90
+ t?.dispose();
91
+ },
92
+ };
93
+ },
94
+ };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The hand-drawn post stages.
3
+ *
4
+ * scene (HDR): the painted frame is read through a slow wobble, so wash
5
+ * borders are never ruler-straight, and pigment pools a little darker
6
+ * where one wash meets another (the wet edge of real watercolour).
7
+ * display (sRGB): pen lines from the depth buffer (silhouettes heavy,
8
+ * creases light) with varying pressure, small breaks and their own
9
+ * wobble, redrawn a few times a second; then the frame is printed on
10
+ * paper: tooth, fibres, granulation in the darker washes.
11
+ */
12
+ import type { Node, Texture, UniformNode, Vector2 } from "three/webgpu";
13
+ import type { StylePostInput } from "../kit.js";
14
+ export interface InkUniforms {
15
+ readonly boil: UniformNode<"vec2", Vector2>;
16
+ readonly dpr: UniformNode<"float", number>;
17
+ /** Pen width in CSS px. */
18
+ readonly lineWidth: UniformNode<"float", number>;
19
+ /** 0..1 line opacity. */
20
+ readonly lineStrength: UniformNode<"float", number>;
21
+ /** Wobble of the washes, CSS px. */
22
+ readonly wobble: UniformNode<"float", number>;
23
+ /** 0..1 how much the paper shows. */
24
+ readonly paper: UniformNode<"float", number>;
25
+ readonly noise: Texture;
26
+ readonly paperTex: Texture;
27
+ }
28
+ /** HDR stage: wobbled read of the painted frame, darker wet edges. */
29
+ export declare function inkScene(input: StylePostInput, u: InkUniforms): Node<"vec4">;
30
+ /** Display stage: pen lines and paper over the graded, tone-mapped frame. */
31
+ export declare function inkDisplay(color: Node<"vec4">, input: StylePostInput, u: InkUniforms): Node<"vec4">;
@@ -0,0 +1,68 @@
1
+ import { abs, float, luminance, max, mix, perspectiveDepthToViewZ, pow, screenUV, smoothstep, texture, vec2, vec3, vec4 } from "three/tsl";
2
+ import { PIGMENTS } from "./palette.js";
3
+ const hexVec = (hex) => vec3(((hex >> 16) & 255) / 255, ((hex >> 8) & 255) / 255, (hex & 255) / 255);
4
+ /** HDR stage: wobbled read of the painted frame, darker wet edges. */
5
+ export function inkScene(input, u) {
6
+ const px = input.texel;
7
+ // CSS pixel coordinates of this fragment.
8
+ const css = screenUV.div(px).div(u.dpr);
9
+ const n = texture(u.noise, css.div(170).add(u.boil.mul(0.37))).rg.sub(0.5).mul(2);
10
+ const uv = screenUV.add(n.mul(u.wobble).mul(u.dpr).mul(px));
11
+ const c = input.color.sample(uv);
12
+ // Wet edge: how much the colour changes across a few pixels.
13
+ const r = px.mul(u.dpr).mul(1.6);
14
+ const l = luminance(c.rgb);
15
+ const lx = luminance(input.color.sample(uv.add(vec2(r.x, 0))).rgb);
16
+ const ly = luminance(input.color.sample(uv.add(vec2(0, r.y))).rgb);
17
+ const lx2 = luminance(input.color.sample(uv.sub(vec2(r.x, 0))).rgb);
18
+ const ly2 = luminance(input.color.sample(uv.sub(vec2(0, r.y))).rgb);
19
+ const grad = abs(lx.sub(l)).add(abs(ly.sub(l))).add(abs(lx2.sub(l))).add(abs(ly2.sub(l))).div(max(l, float(0.06)));
20
+ // The darker side of a border collects pigment.
21
+ const darker = smoothstep(0, 0.02, l.mul(4).sub(lx).sub(ly).sub(lx2).sub(ly2).negate());
22
+ const pool = smoothstep(0.12, 0.6, grad).mul(darker).mul(0.18);
23
+ return vec4(c.rgb.mul(float(1).sub(pool)), c.a);
24
+ }
25
+ /** Display stage: pen lines and paper over the graded, tone-mapped frame. */
26
+ export function inkDisplay(color, input, u) {
27
+ const px = input.texel;
28
+ const css = screenUV.div(px).div(u.dpr);
29
+ // The pen wanders on its own (lines sit a little off the washes, as when inked by hand).
30
+ const nw = texture(u.noise, css.div(90).add(u.boil)).rg.sub(0.5).mul(2);
31
+ const uv = screenUV.add(nw.mul(1.1).mul(u.dpr).mul(px));
32
+ // Pressure: thicker and thinner along a stroke, with an occasional lift.
33
+ const press = texture(u.noise, css.div(55).add(u.boil.yx)).g;
34
+ const width = u.lineWidth.mul(press.mul(0.8).add(0.6));
35
+ const lift = smoothstep(0.1, 0.24, press);
36
+ const o = px.mul(u.dpr).mul(width);
37
+ const dist = (p) => perspectiveDepthToViewZ(input.depth.sample(p).x, input.near, input.far).negate();
38
+ const dc = dist(uv);
39
+ const d1 = dist(uv.add(vec2(o.x, 0)));
40
+ const d2 = dist(uv.sub(vec2(o.x, 0)));
41
+ const d3 = dist(uv.add(vec2(0, o.y)));
42
+ const d4 = dist(uv.sub(vec2(0, o.y)));
43
+ // Silhouettes: a neighbour much further away (the line sits on the near side).
44
+ const far = max(max(d1.sub(dc), d2.sub(dc)), max(d3.sub(dc), d4.sub(dc)));
45
+ const sil = smoothstep(0.006, 0.02, far.div(dc));
46
+ // Creases: the surface bends (1/z is flat across a plane, so its Laplacian finds folds).
47
+ const wc = float(1).div(dc);
48
+ const lap = abs(float(1).div(d1).add(float(1).div(d2)).add(float(1).div(d3)).add(float(1).div(d4)).sub(wc.mul(4))).div(wc);
49
+ const crease = smoothstep(0.0035, 0.012, lap).mul(0.55);
50
+ // Colour edges: where one painted thing meets another at the same depth (a crowd shoulder to shoulder).
51
+ const lumAt = (p) => luminance(input.color.sample(p).rgb);
52
+ const l0 = lumAt(uv);
53
+ const lx = abs(lumAt(uv.add(vec2(o.x, 0))).sub(lumAt(uv.sub(vec2(o.x, 0)))));
54
+ const ly = abs(lumAt(uv.add(vec2(0, o.y))).sub(lumAt(uv.sub(vec2(0, o.y)))));
55
+ const edge = smoothstep(0.55, 1.1, max(lx, ly).div(max(l0, float(0.04)))).mul(0.6);
56
+ const line = max(max(sil, crease), edge).mul(lift).mul(u.lineStrength);
57
+ const ink = hexVec(PIGMENTS.ink);
58
+ let col = mix(color.rgb, ink, line);
59
+ // Paper: tooth and fibres everywhere, granulation where the wash is dark, a slow tone across the sheet.
60
+ const pp = texture(u.paperTex, css.div(512));
61
+ const sheet = texture(u.paperTex, css.div(2048)).g;
62
+ const lum = luminance(col);
63
+ const tooth = pow(pp.r, float(1.4));
64
+ const gran = mix(float(1), pp.b.mul(0.22).add(0.78), float(1).sub(lum).mul(0.9));
65
+ const paper = mix(float(1), tooth.mul(gran).mul(sheet.mul(0.06).add(0.97)), u.paper);
66
+ col = col.mul(paper);
67
+ return vec4(col, color.a);
68
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The look presets as an illustrator would paint them. Light is soft and
3
+ * coloured (the washes keep its hue), fog is the paper showing through,
4
+ * tone mapping is neutral so pigments print as mixed, and bloom is a
5
+ * whisper (watercolour does not glow; only lanterns and fire get a halo).
6
+ * `paint` is the light level the washes are cut at for that preset.
7
+ */
8
+ import type { Look, LookPreset } from "../../render/presets.js";
9
+ export interface HanddrawnLook extends Look {
10
+ /** Multiplier on the light level before the washes are cut. */
11
+ readonly paint: number;
12
+ /** Paint value of each wash: deep shadow, shadow, mid, light. */
13
+ readonly values: readonly [number, number, number, number];
14
+ /** Paint value where the light is hot (lanterns, fire). */
15
+ readonly glow: number;
16
+ }
17
+ export declare const HANDDRAWN_LOOKS: Record<LookPreset, HanddrawnLook>;
18
+ export declare function handdrawnLook(preset: LookPreset): HanddrawnLook;
@@ -0,0 +1,42 @@
1
+ const neutralGrade = {
2
+ exposure: 1,
3
+ contrast: 1,
4
+ saturation: 1,
5
+ shadowTint: [1, 1, 1],
6
+ highlightTint: [1, 1, 1],
7
+ toneMapping: "neutral",
8
+ };
9
+ export const HANDDRAWN_LOOKS = {
10
+ // A nocturne: an indigo wash over the sheet, the moon cool on the tops of things, lanterns left as warm paper.
11
+ night: {
12
+ paint: 0.9,
13
+ values: [0.34, 0.52, 0.74, 0.94], glow: 1.3,
14
+ lighting: { sky: 0x8ea4e6, ground: 0x3e3c5c, hemi: 1.7, sun: 0xc4d2ff, sunIntensity: 1.6, sunDir: [-0.45, 0.85, 0.3], ambient: 0.35, ambientColor: 0x8090c8, shadowStrength: 0.55 },
15
+ atmosphere: { fog: 0x2e3450, skyTop: 0x1c2036, mode: "linear", near: 40, far: 120, density: 0.01, heightFog: 0, heightFogTop: 0 },
16
+ grade: { ...neutralGrade, saturation: 0.96, vignette: 0.34, bloomStrength: 0.28, bloomRadius: 0.35, bloomThreshold: 0.92 },
17
+ },
18
+ dusk: {
19
+ paint: 1,
20
+ values: [0.32, 0.5, 0.74, 0.95], glow: 1.2,
21
+ lighting: { sky: 0xffcfae, ground: 0x5a4658, hemi: 1.5, sun: 0xffb27a, sunIntensity: 2.2, sunDir: [0.7, 0.45, 0.35], ambient: 0.25, ambientColor: 0xffe0c8, shadowStrength: 0.6 },
22
+ atmosphere: { fog: 0x8a6a74, skyTop: 0x3a3050, mode: "linear", near: 40, far: 130, density: 0.008, heightFog: 0, heightFogTop: 0 },
23
+ grade: { ...neutralGrade, vignette: 0.3, bloomStrength: 0.22, bloomRadius: 0.35, bloomThreshold: 0.92 },
24
+ },
25
+ day: {
26
+ paint: 1,
27
+ values: [0.4, 0.6, 0.84, 1], glow: 1.12,
28
+ lighting: { sky: 0xe4ecf6, ground: 0x8a7a5a, hemi: 1.6, sun: 0xfff2dc, sunIntensity: 2.5, sunDir: [0.4, 0.9, 0.25], ambient: 0.25, ambientColor: 0xffffff, shadowStrength: 0.5 },
29
+ atmosphere: { fog: 0xece3cc, skyTop: 0xd8dde0, mode: "linear", near: 50, far: 170, density: 0.006, heightFog: 0, heightFogTop: 0 },
30
+ grade: { ...neutralGrade, vignette: 0.22, bloomStrength: 0.12, bloomRadius: 0.3, bloomThreshold: 0.96 },
31
+ },
32
+ studio: {
33
+ paint: 1,
34
+ values: [0.42, 0.62, 0.86, 1], glow: 1.1,
35
+ lighting: { sky: 0xffffff, ground: 0x6a6258, hemi: 1.6, sun: 0xffffff, sunIntensity: 2.2, sunDir: [0.5, 1, 0.6], ambient: 0.3, ambientColor: 0xffffff, shadowStrength: 0.45 },
36
+ atmosphere: { fog: 0xece3cc, skyTop: 0xe6dcc4, mode: "linear", near: 60, far: 200, density: 0.005, heightFog: 0, heightFogTop: 0 },
37
+ grade: { ...neutralGrade, vignette: 0.18, bloomStrength: 0.1, bloomRadius: 0.3, bloomThreshold: 0.96 },
38
+ },
39
+ };
40
+ export function handdrawnLook(preset) {
41
+ return HANDDRAWN_LOOKS[preset] ?? HANDDRAWN_LOOKS.studio;
42
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Watercolour painting for lit materials. Every lit surface keeps its own
3
+ * geometry, maps, skinning and vertex animation; only its final colour is
4
+ * repainted (the material's `outputNode`):
5
+ *
6
+ * - its colour is pulled toward the nearest pigment in the palette, so a
7
+ * pack of toy-bright models reads as one painter's box;
8
+ * - the light it receives is split into a few washes (deep shadow,
9
+ * shadow, mid, light, glow) with ragged, slightly wet borders, keeping
10
+ * the light's hue (moonlight blue, lantern amber);
11
+ * - shadows go cool, and the deepest are hatched with pen strokes that
12
+ * are redrawn a few times a second.
13
+ *
14
+ * Light is read as the ratio of the lit colour to the albedo, so every
15
+ * light the scene has (sun, sky, lanterns, emissive glow) is honoured
16
+ * without touching the lighting code.
17
+ */
18
+ import type { Material, Node, Object3D, Texture, UniformNode, Vector2, Vector4 } from "three/webgpu";
19
+ export interface PaintUniforms {
20
+ /** Random offset re-rolled a few times a second (the boil). */
21
+ readonly boil: UniformNode<"vec2", Vector2>;
22
+ /** Render pixels per CSS pixel. */
23
+ readonly dpr: UniformNode<"float", number>;
24
+ /** 0..1 pull toward the palette. */
25
+ readonly snap: UniformNode<"float", number>;
26
+ /** Strength of the shadow hatching, 0..1. */
27
+ readonly hatch: UniformNode<"float", number>;
28
+ /** Light level multiplier before the washes are cut (per look preset). */
29
+ readonly exposure: UniformNode<"float", number>;
30
+ /** Paint value of each wash: deep shadow, shadow, mid, light (the glow is `glow`). */
31
+ readonly values: UniformNode<"vec4", Vector4>;
32
+ readonly glow: UniformNode<"float", number>;
33
+ readonly noise: Texture;
34
+ }
35
+ /** The repainted output for a lit material (assign to `material.outputNode`). */
36
+ export declare function paintNode(u: PaintUniforms): Node<"vec4">;
37
+ type PaintableMaterial = Material & {
38
+ outputNode?: Node | null;
39
+ metalness?: number;
40
+ roughness?: number;
41
+ flatShading?: boolean;
42
+ };
43
+ /** Lit materials get painted; unlit ones (flames, glows, outlines, effects) keep their colour. */
44
+ export declare function paintable(m: Material): m is PaintableMaterial;
45
+ /**
46
+ * Finds lit materials as they arrive in the scene (models load late) and
47
+ * paints each once. A scan walks the scene graph (a few hundred nodes; the
48
+ * crowds are one node per type), so it runs every few frames, not every one.
49
+ */
50
+ export declare class PaintWatcher {
51
+ private readonly root;
52
+ private readonly make;
53
+ private readonly done;
54
+ painted: number;
55
+ constructor(root: Object3D, make: () => Node<"vec4">);
56
+ private paint;
57
+ scan(): void;
58
+ }
59
+ export {};
@@ -0,0 +1,129 @@
1
+ import { abs, diffuseColor, dot, exp, float, Fn, fract, luminance, max, min, mix, output, screenCoordinate, smoothstep, sqrt, texture, vec3, vec4, } from "three/tsl";
2
+ import { HANDDRAWN_PALETTE, linearRgb, PIGMENTS } from "./palette.js";
3
+ const PIGMENT_LINEAR = HANDDRAWN_PALETTE.map(linearRgb);
4
+ /**
5
+ * The palette's pull on a linear colour: a soft blend of the pigments
6
+ * nearest to it (compared gamma-encoded with eye weights). Soft, not a hard
7
+ * nearest pick, so a gradient in a texture stays a gradient instead of
8
+ * breaking into patches.
9
+ */
10
+ const pigmentOf = Fn(([c]) => {
11
+ const g = sqrt(max(c, vec3(0)));
12
+ const sum = vec3(0).toVar();
13
+ const wsum = float(1e-6).toVar();
14
+ for (const p of PIGMENT_LINEAR) {
15
+ const pg = vec3(Math.sqrt(p[0]), Math.sqrt(p[1]), Math.sqrt(p[2]));
16
+ const d = g.sub(pg);
17
+ const w = exp(dot(d.mul(d), vec3(0.3, 0.59, 0.11)).mul(-260));
18
+ sum.addAssign(vec3(p[0], p[1], p[2]).mul(w));
19
+ wsum.addAssign(w);
20
+ }
21
+ return sum.div(wsum);
22
+ });
23
+ /** A pen stroke field: 1 on lines `spacing` CSS px apart along `u`, about 1 px wide. */
24
+ function strokes(u, spacing) {
25
+ const d = abs(fract(u).sub(0.5)).mul(spacing);
26
+ return float(1).sub(smoothstep(0.35, 1.15, d));
27
+ }
28
+ /** The repainted output for a lit material (assign to `material.outputNode`). */
29
+ export function paintNode(u) {
30
+ const albedo = diffuseColor.rgb;
31
+ const lit = output.rgb;
32
+ const pig = pigmentOf(albedo);
33
+ const snapped = mix(albedo, pig, u.snap);
34
+ // A wash is never solid black: very dark albedos (night stone, dark walls) are lifted toward a grey wash, and the
35
+ // hatching and the pen carry their darkness instead, as an illustrator would draw a dark rock.
36
+ const darkness = float(1).sub(smoothstep(0.015, 0.12, luminance(snapped)));
37
+ const paint = mix(snapped, snapped.mul(0.4).add(vec3(...linearRgb(PIGMENTS.slate)).mul(0.6)), darkness.mul(0.65));
38
+ // Light received, per channel; its luminance picks the wash and its hue tints it.
39
+ const ratio = lit.div(max(albedo, vec3(0.045)));
40
+ const level = luminance(ratio).mul(u.exposure);
41
+ const hue = min(ratio.div(max(luminance(ratio), float(1e-3))), vec3(2.2));
42
+ // Screen position in CSS pixels, and the noise that makes wash borders ragged.
43
+ const sc = screenCoordinate.xy.div(u.dpr);
44
+ const n = texture(u.noise, sc.div(150).add(u.boil)).r;
45
+ const nf = texture(u.noise, sc.div(37).add(u.boil.yx)).b;
46
+ const lv = level.mul(n.mul(0.22).add(0.89));
47
+ // Washes: deep shadow, shadow, mid, light, glow (each preset sets their values).
48
+ const v = u.values;
49
+ const s1 = smoothstep(0.1, 0.16, lv);
50
+ const s2 = smoothstep(0.3, 0.38, lv);
51
+ const s3 = smoothstep(0.62, 0.74, lv);
52
+ const s4 = smoothstep(1.45, 1.8, lv);
53
+ const value = v.x.add(v.y.sub(v.x).mul(s1)).add(v.z.sub(v.y).mul(s2)).add(v.w.sub(v.z).mul(s3)).add(u.glow.sub(v.w).mul(s4));
54
+ // Shadows go cool, light keeps its hue.
55
+ const cool = mix(vec3(0.8, 0.86, 1.1), vec3(1, 1, 1), s2);
56
+ let col = paint.mul(value).mul(mix(vec3(1, 1, 1), hue, 0.6)).mul(cool);
57
+ // Hatching: the shadow wash gets one direction, the deep shadow a cross.
58
+ const jitter = nf.mul(0.5);
59
+ const h1 = strokes(sc.x.mul(0.62).add(sc.y).div(7).add(jitter).add(u.boil.x.mul(3)), 7);
60
+ const h2 = strokes(sc.x.sub(sc.y.mul(0.62)).div(7.5).add(jitter).add(u.boil.y.mul(3)), 7.5);
61
+ const dash = smoothstep(0.26, 0.4, texture(u.noise, sc.div(19).add(u.boil)).g);
62
+ const shade = float(1).sub(s2);
63
+ const deep = max(float(1).sub(s1), darkness.mul(0.8));
64
+ // Light single hatching in the shadow wash, a firmer cross only in the deep shadow (a lit field never turns to cloth).
65
+ const hatch = max(h1.mul(shade).mul(0.45), max(h1, h2).mul(deep).mul(0.9)).mul(dash).mul(u.hatch);
66
+ const ink = vec3(...linearRgb(PIGMENTS.sepia));
67
+ col = mix(col, col.mul(0.3).add(ink.mul(0.3)), hatch);
68
+ return vec4(col, output.a);
69
+ }
70
+ const LIT_TYPES = new Set([
71
+ "MeshStandardMaterial",
72
+ "MeshStandardNodeMaterial",
73
+ "MeshPhysicalMaterial",
74
+ "MeshPhysicalNodeMaterial",
75
+ "MeshLambertMaterial",
76
+ "MeshLambertNodeMaterial",
77
+ "MeshPhongMaterial",
78
+ "MeshPhongNodeMaterial",
79
+ "MeshToonMaterial",
80
+ "MeshToonNodeMaterial",
81
+ ]);
82
+ /** Lit materials get painted; unlit ones (flames, glows, outlines, effects) keep their colour. */
83
+ export function paintable(m) {
84
+ const pm = m;
85
+ return LIT_TYPES.has(m.type) && (pm.outputNode === undefined || pm.outputNode === null);
86
+ }
87
+ /**
88
+ * Finds lit materials as they arrive in the scene (models load late) and
89
+ * paints each once. A scan walks the scene graph (a few hundred nodes; the
90
+ * crowds are one node per type), so it runs every few frames, not every one.
91
+ */
92
+ export class PaintWatcher {
93
+ root;
94
+ make;
95
+ done = new WeakSet();
96
+ painted = 0;
97
+ constructor(root, make) {
98
+ this.root = root;
99
+ this.make = make;
100
+ }
101
+ paint(m) {
102
+ if (this.done.has(m))
103
+ return;
104
+ this.done.add(m);
105
+ if (!paintable(m))
106
+ return;
107
+ // Paint is matte: no metal, no gloss (watercolour has no specular).
108
+ if (m.metalness !== undefined)
109
+ m.metalness = 0;
110
+ if (m.roughness !== undefined)
111
+ m.roughness = 1;
112
+ m.outputNode = this.make();
113
+ m.needsUpdate = true;
114
+ this.painted++;
115
+ }
116
+ scan() {
117
+ this.root.traverse((o) => {
118
+ const mesh = o;
119
+ if (!mesh.isMesh)
120
+ return;
121
+ const mat = mesh.material;
122
+ if (Array.isArray(mat))
123
+ for (const m of mat)
124
+ this.paint(m);
125
+ else if (mat)
126
+ this.paint(mat);
127
+ });
128
+ }
129
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The hand-drawn palette: a watercolour box of pigments. Every surface the
3
+ * style paints is pulled toward its nearest pigment, which is what makes
4
+ * models from different packs read as one illustrator's work. The asset
5
+ * library's handdrawn manifest carries the same list (its `palette`), so
6
+ * generated art is locked to it too.
7
+ */
8
+ /** Pigments as sRGB hex, light to dark within each family. */
9
+ export declare const PIGMENTS: {
10
+ readonly paper: 15985366;
11
+ readonly naples: 15521178;
12
+ readonly ochre: 14263624;
13
+ readonly sienna: 12088378;
14
+ readonly umber: 7227955;
15
+ readonly sepia: 3811872;
16
+ readonly peach: 15450015;
17
+ readonly vermilion: 14047804;
18
+ readonly crimson: 11023690;
19
+ readonly rose: 14192538;
20
+ readonly violet: 8146814;
21
+ readonly indigo: 3882859;
22
+ readonly ultramarine: 4941480;
23
+ readonly cerulean: 7579339;
24
+ readonly teal: 4097923;
25
+ readonly viridian: 4159062;
26
+ readonly sap: 7771455;
27
+ readonly leaf: 11125372;
28
+ readonly olive: 9078344;
29
+ readonly slate: 9213344;
30
+ readonly paynes: 4147800;
31
+ readonly stone: 11971221;
32
+ readonly ink: 2365972;
33
+ readonly white: 16513006;
34
+ };
35
+ export type Pigment = keyof typeof PIGMENTS;
36
+ /** The palette in a fixed order (the manifest's `palette`). */
37
+ export declare const HANDDRAWN_PALETTE: readonly number[];
38
+ /** "#rrggbb" strings, for manifests and docs. */
39
+ export declare const HANDDRAWN_PALETTE_HEX: readonly string[];
40
+ /** sRGB 0..255 channel to linear 0..1. */
41
+ export declare function srgbToLinear(c: number): number;
42
+ /** A hex colour as linear rgb. */
43
+ export declare function linearRgb(hex: number): [number, number, number];
44
+ /**
45
+ * The pigment nearest to a colour (sRGB hex), measured in a perceptual-ish
46
+ * space (gamma-encoded, weighted toward green like the eye). Pure, so the
47
+ * shader's choice can be checked in node.
48
+ */
49
+ export declare function nearestPigment(hex: number): number;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The hand-drawn palette: a watercolour box of pigments. Every surface the
3
+ * style paints is pulled toward its nearest pigment, which is what makes
4
+ * models from different packs read as one illustrator's work. The asset
5
+ * library's handdrawn manifest carries the same list (its `palette`), so
6
+ * generated art is locked to it too.
7
+ */
8
+ /** Pigments as sRGB hex, light to dark within each family. */
9
+ export const PIGMENTS = {
10
+ paper: 0xf3ead6,
11
+ naples: 0xecd59a,
12
+ ochre: 0xd9a548,
13
+ sienna: 0xb8743a,
14
+ umber: 0x6e4a33,
15
+ sepia: 0x3a2a20,
16
+ peach: 0xebbf9f,
17
+ vermilion: 0xd65a3c,
18
+ crimson: 0xa8354a,
19
+ rose: 0xd88f9a,
20
+ violet: 0x7c4f7e,
21
+ indigo: 0x3b3f6b,
22
+ ultramarine: 0x4b66a8,
23
+ cerulean: 0x73a6cb,
24
+ teal: 0x3e8783,
25
+ viridian: 0x3f7656,
26
+ sap: 0x76953f,
27
+ leaf: 0xa9c27c,
28
+ olive: 0x8a8648,
29
+ slate: 0x8c95a0,
30
+ paynes: 0x3f4a58,
31
+ stone: 0xb6aa95,
32
+ ink: 0x241a14,
33
+ white: 0xfbf7ee,
34
+ };
35
+ /** The palette in a fixed order (the manifest's `palette`). */
36
+ export const HANDDRAWN_PALETTE = Object.values(PIGMENTS);
37
+ /** "#rrggbb" strings, for manifests and docs. */
38
+ export const HANDDRAWN_PALETTE_HEX = HANDDRAWN_PALETTE.map((c) => `#${c.toString(16).padStart(6, "0")}`);
39
+ /** sRGB 0..255 channel to linear 0..1. */
40
+ export function srgbToLinear(c) {
41
+ const v = c / 255;
42
+ return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
43
+ }
44
+ /** A hex colour as linear rgb. */
45
+ export function linearRgb(hex) {
46
+ return [srgbToLinear((hex >> 16) & 255), srgbToLinear((hex >> 8) & 255), srgbToLinear(hex & 255)];
47
+ }
48
+ /**
49
+ * The pigment nearest to a colour (sRGB hex), measured in a perceptual-ish
50
+ * space (gamma-encoded, weighted toward green like the eye). Pure, so the
51
+ * shader's choice can be checked in node.
52
+ */
53
+ export function nearestPigment(hex) {
54
+ const r = (hex >> 16) & 255;
55
+ const g = (hex >> 8) & 255;
56
+ const b = hex & 255;
57
+ let best = HANDDRAWN_PALETTE[0] ?? 0;
58
+ let bd = Infinity;
59
+ for (const p of HANDDRAWN_PALETTE) {
60
+ const dr = r - ((p >> 16) & 255);
61
+ const dg = g - ((p >> 8) & 255);
62
+ const db = b - (p & 255);
63
+ const d = 0.3 * dr * dr + 0.59 * dg * dg + 0.11 * db * db;
64
+ if (d < bd) {
65
+ bd = d;
66
+ best = p;
67
+ }
68
+ }
69
+ return best;
70
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Procedural textures for the hand-drawn style, generated in JS (no DOM,
3
+ * no files, deterministic from a seed): a tileable noise field the line
4
+ * wobble, wet edges and hatching read, and a sheet of paper with grain and
5
+ * fibres the whole frame is printed on. Original work, so no licence
6
+ * question.
7
+ */
8
+ import { DataTexture } from "three/webgpu";
9
+ /** A small seeded generator (mulberry32). */
10
+ export declare function seeded(seed: number): () => number;
11
+ /** Tileable value noise on a `size` grid with `cells` lattice cells per side, fractal over `octaves`. Values 0..1. */
12
+ export declare function tileNoise(size: number, cells: number, octaves: number, rnd: () => number): Float32Array;
13
+ /**
14
+ * Noise for the style's shaders, 256 px and tileable: R low (wobble, wet
15
+ * edges), G mid (line pressure and breaks), B fine (stroke jitter), A white.
16
+ */
17
+ export declare function noiseData(seed?: number, size?: number): Uint8Array;
18
+ /**
19
+ * Cold-pressed paper, 512 px and tileable: R is the surface (1 = the top of
20
+ * the tooth, lower = valleys and fibres), G a slower mottle for the sheet's
21
+ * tone, B the tooth alone (where pigment granulates).
22
+ */
23
+ export declare function paperData(seed?: number, size?: number): Uint8Array;
24
+ /** The shared noise texture (built once per page). */
25
+ export declare function noiseTexture(): DataTexture;
26
+ /** The shared paper texture (built once per page). */
27
+ export declare function paperTexture(): DataTexture;