@gtanczyk/diorama 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (302) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +157 -2
  3. package/app/bin/diorama.mjs +13 -0
  4. package/app/src/actions/budget.ts +160 -0
  5. package/app/src/actions/build.ts +158 -0
  6. package/app/src/actions/cast.ts +267 -0
  7. package/app/src/actions/depth.ts +95 -0
  8. package/app/src/actions/extend.ts +36 -0
  9. package/app/src/actions/faces.ts +160 -0
  10. package/app/src/actions/feedback.ts +87 -0
  11. package/app/src/actions/fix.ts +382 -0
  12. package/app/src/actions/history.ts +93 -0
  13. package/app/src/actions/idea-preview.ts +65 -0
  14. package/app/src/actions/idea-review.ts +102 -0
  15. package/app/src/actions/idea-sketch.ts +172 -0
  16. package/app/src/actions/index.ts +32 -0
  17. package/app/src/actions/items.ts +94 -0
  18. package/app/src/actions/kids.ts +215 -0
  19. package/app/src/actions/language.ts +52 -0
  20. package/app/src/actions/models.ts +64 -0
  21. package/app/src/actions/motion.ts +119 -0
  22. package/app/src/actions/pictures.ts +49 -0
  23. package/app/src/actions/reactions.ts +36 -0
  24. package/app/src/actions/registry.ts +101 -0
  25. package/app/src/actions/share.ts +32 -0
  26. package/app/src/actions/sound.ts +73 -0
  27. package/app/src/actions/start.ts +154 -0
  28. package/app/src/actions/step-work.ts +150 -0
  29. package/app/src/actions/story.ts +101 -0
  30. package/app/src/actions/studio.ts +133 -0
  31. package/app/src/actions/style.ts +72 -0
  32. package/app/src/actions/video.ts +46 -0
  33. package/app/src/actions/view.ts +109 -0
  34. package/app/src/actions/weather.ts +38 -0
  35. package/app/src/actions/world.ts +290 -0
  36. package/app/src/commands/action.ts +39 -0
  37. package/app/src/commands/backdrop.ts +188 -0
  38. package/app/src/commands/browsers.ts +21 -0
  39. package/app/src/commands/build.ts +503 -0
  40. package/app/src/commands/cast.ts +161 -0
  41. package/app/src/commands/clip.ts +431 -0
  42. package/app/src/commands/derive.ts +167 -0
  43. package/app/src/commands/doctor.ts +74 -0
  44. package/app/src/commands/explore.ts +123 -0
  45. package/app/src/commands/export.ts +158 -0
  46. package/app/src/commands/extend.ts +75 -0
  47. package/app/src/commands/faces.ts +80 -0
  48. package/app/src/commands/icons.ts +69 -0
  49. package/app/src/commands/index.ts +37 -0
  50. package/app/src/commands/language.ts +87 -0
  51. package/app/src/commands/layout.ts +31 -0
  52. package/app/src/commands/masks.ts +117 -0
  53. package/app/src/commands/mcp.ts +52 -0
  54. package/app/src/commands/model.ts +284 -0
  55. package/app/src/commands/paint.ts +182 -0
  56. package/app/src/commands/plates.ts +97 -0
  57. package/app/src/commands/play.ts +195 -0
  58. package/app/src/commands/playthrough.ts +111 -0
  59. package/app/src/commands/publish.ts +78 -0
  60. package/app/src/commands/reactions.ts +40 -0
  61. package/app/src/commands/reel.ts +46 -0
  62. package/app/src/commands/setup.ts +85 -0
  63. package/app/src/commands/sound.ts +100 -0
  64. package/app/src/commands/sprites.ts +232 -0
  65. package/app/src/commands/story.ts +37 -0
  66. package/app/src/commands/studio.ts +65 -0
  67. package/app/src/commands/style.ts +87 -0
  68. package/app/src/commands/verify.ts +45 -0
  69. package/app/src/commands/voice-retry.ts +14 -0
  70. package/app/src/commands/voices.ts +145 -0
  71. package/app/src/commands/weather.ts +52 -0
  72. package/app/src/commands/world.ts +104 -0
  73. package/app/src/film/clip.ts +386 -0
  74. package/app/src/film/reel.ts +126 -0
  75. package/app/src/film/screencast.ts +123 -0
  76. package/app/src/image/mask.ts +29 -0
  77. package/app/src/image/overlay.ts +38 -0
  78. package/app/src/image/raster.ts +224 -0
  79. package/app/src/main.ts +28 -0
  80. package/app/src/meta/choice.ts +27 -0
  81. package/app/src/meta/http.ts +99 -0
  82. package/app/src/meta/memo.ts +105 -0
  83. package/app/src/meta/muse.ts +70 -0
  84. package/app/src/meta/openrouter.ts +93 -0
  85. package/app/src/meta/prices.ts +25 -0
  86. package/app/src/meta/sam.ts +83 -0
  87. package/app/src/meta/schema.ts +63 -0
  88. package/app/src/meta/spark.ts +220 -0
  89. package/app/src/meta/step.ts +14 -0
  90. package/app/src/meta/voice-file.ts +78 -0
  91. package/app/src/meta/voice.ts +103 -0
  92. package/app/src/model/animation.ts +38 -0
  93. package/app/src/model/blender/relax.py +284 -0
  94. package/app/src/model/blender.ts +115 -0
  95. package/app/src/model/build.ts +50 -0
  96. package/app/src/model/face.ts +21 -0
  97. package/app/src/model/fit.ts +181 -0
  98. package/app/src/model/glb.ts +241 -0
  99. package/app/src/model/plan.ts +151 -0
  100. package/app/src/model/quality.ts +48 -0
  101. package/app/src/model/space.ts +168 -0
  102. package/app/src/model/sprites.ts +293 -0
  103. package/app/src/model/views.ts +163 -0
  104. package/app/src/play/creature.ts +43 -0
  105. package/app/src/play/depth-occlusion.ts +150 -0
  106. package/app/src/play/exit-outline.ts +48 -0
  107. package/app/src/play/face3d.ts +34 -0
  108. package/app/src/play/focus.ts +78 -0
  109. package/app/src/play/geometry.ts +272 -0
  110. package/app/src/play/gestures.ts +23 -0
  111. package/app/src/play/glow.ts +187 -0
  112. package/app/src/play/lipsync.ts +69 -0
  113. package/app/src/play/living.ts +83 -0
  114. package/app/src/play/outline-path.ts +130 -0
  115. package/app/src/play/parallax.ts +200 -0
  116. package/app/src/play/path.ts +262 -0
  117. package/app/src/play/preview.ts +71 -0
  118. package/app/src/play/reactions.ts +92 -0
  119. package/app/src/play/rig.ts +172 -0
  120. package/app/src/play/shadow.ts +253 -0
  121. package/app/src/play/sound.ts +51 -0
  122. package/app/src/play/weather.ts +58 -0
  123. package/app/src/qa/explore.ts +136 -0
  124. package/app/src/qa/probe.ts +139 -0
  125. package/app/src/qa/route.ts +118 -0
  126. package/app/src/registry.ts +23 -0
  127. package/app/src/scene/animation-source.ts +36 -0
  128. package/app/src/scene/assets.ts +37 -0
  129. package/app/src/scene/autofix.ts +133 -0
  130. package/app/src/scene/backdrop.ts +326 -0
  131. package/app/src/scene/blockin.ts +189 -0
  132. package/app/src/scene/brief.ts +74 -0
  133. package/app/src/scene/carry.ts +177 -0
  134. package/app/src/scene/cast-fixes.ts +33 -0
  135. package/app/src/scene/cast.ts +199 -0
  136. package/app/src/scene/corrections.ts +164 -0
  137. package/app/src/scene/critic.ts +169 -0
  138. package/app/src/scene/depthmap.ts +149 -0
  139. package/app/src/scene/derive.ts +475 -0
  140. package/app/src/scene/diagnose.ts +132 -0
  141. package/app/src/scene/face-calibration.ts +65 -0
  142. package/app/src/scene/face-registration.ts +21 -0
  143. package/app/src/scene/face-work.ts +71 -0
  144. package/app/src/scene/faces-data.ts +48 -0
  145. package/app/src/scene/faces-settings.ts +20 -0
  146. package/app/src/scene/faces.ts +435 -0
  147. package/app/src/scene/glows.ts +153 -0
  148. package/app/src/scene/held.ts +109 -0
  149. package/app/src/scene/icons.ts +185 -0
  150. package/app/src/scene/layers.ts +822 -0
  151. package/app/src/scene/layout.ts +216 -0
  152. package/app/src/scene/locate.ts +118 -0
  153. package/app/src/scene/maskcheck.ts +43 -0
  154. package/app/src/scene/masks.ts +392 -0
  155. package/app/src/scene/occluders.ts +123 -0
  156. package/app/src/scene/occlusion-check.ts +138 -0
  157. package/app/src/scene/paint-layout.ts +135 -0
  158. package/app/src/scene/paint.ts +81 -0
  159. package/app/src/scene/photo.ts +123 -0
  160. package/app/src/scene/plates.ts +256 -0
  161. package/app/src/scene/playbook.ts +121 -0
  162. package/app/src/scene/preview.ts +23 -0
  163. package/app/src/scene/reactions.ts +118 -0
  164. package/app/src/scene/restyle.ts +53 -0
  165. package/app/src/scene/rig-check.ts +68 -0
  166. package/app/src/scene/rig.ts +379 -0
  167. package/app/src/scene/scale.ts +148 -0
  168. package/app/src/scene/shadows.ts +215 -0
  169. package/app/src/scene/stairs.ts +183 -0
  170. package/app/src/scene/status.ts +32 -0
  171. package/app/src/scene/verify.ts +156 -0
  172. package/app/src/scene/walk.ts +326 -0
  173. package/app/src/scene/warp.ts +419 -0
  174. package/app/src/scene/weather.ts +118 -0
  175. package/app/src/scene/words.ts +65 -0
  176. package/app/src/secrets.ts +219 -0
  177. package/app/src/setup/checks.ts +110 -0
  178. package/app/src/sound/cost.ts +74 -0
  179. package/app/src/sound/data.ts +89 -0
  180. package/app/src/sound/fixes.ts +23 -0
  181. package/app/src/sound/issues.ts +21 -0
  182. package/app/src/sound/language.ts +11 -0
  183. package/app/src/sound/lipsync-make.ts +98 -0
  184. package/app/src/sound/lipsync.ts +24 -0
  185. package/app/src/sound/make.ts +229 -0
  186. package/app/src/sound/musicgen.py +20 -0
  187. package/app/src/sound/pcm.ts +280 -0
  188. package/app/src/sound/plan.ts +154 -0
  189. package/app/src/sound/retry.ts +43 -0
  190. package/app/src/sound/settings.ts +28 -0
  191. package/app/src/sound/sources.ts +81 -0
  192. package/app/src/sound/spoken.ts +70 -0
  193. package/app/src/sound/voices.ts +579 -0
  194. package/app/src/speech/transcribe.ts +38 -0
  195. package/app/src/speech/types.ts +18 -0
  196. package/app/src/story/check.ts +230 -0
  197. package/app/src/story/design.ts +108 -0
  198. package/app/src/story/engine.ts +233 -0
  199. package/app/src/story/fixes.ts +54 -0
  200. package/app/src/story/npc-guidance.ts +7 -0
  201. package/app/src/story/phrases.ts +4 -0
  202. package/app/src/story/schema.ts +92 -0
  203. package/app/src/studio/agent.ts +555 -0
  204. package/app/src/studio/discovery.ts +28 -0
  205. package/app/src/studio/events.ts +48 -0
  206. package/app/src/studio/history.ts +124 -0
  207. package/app/src/studio/jobs.ts +291 -0
  208. package/app/src/studio/locks.ts +33 -0
  209. package/app/src/studio/mcp.ts +86 -0
  210. package/app/src/studio/operation.ts +18 -0
  211. package/app/src/studio/repair.ts +31 -0
  212. package/app/src/studio/server.ts +423 -0
  213. package/app/src/studio/trouble.ts +48 -0
  214. package/app/src/world/design.ts +72 -0
  215. package/app/src/world/drawings.ts +198 -0
  216. package/app/src/world/extend-stage.ts +109 -0
  217. package/app/src/world/extend.ts +123 -0
  218. package/app/src/world/extension-progress.ts +10 -0
  219. package/app/src/world/files.ts +62 -0
  220. package/app/src/world/idea-review.ts +32 -0
  221. package/app/src/world/language.ts +116 -0
  222. package/app/src/world/photo-limits.ts +2 -0
  223. package/app/src/world/pool.ts +41 -0
  224. package/app/src/world/schedule.ts +64 -0
  225. package/app/src/world/schema.ts +74 -0
  226. package/app/src/world/sketch.ts +81 -0
  227. package/app/src/world/style-plan.ts +90 -0
  228. package/app/src/world/style-reference.ts +16 -0
  229. package/app/src/world/styled.ts +51 -0
  230. package/app/src/world/told.ts +105 -0
  231. package/app/src/world/video-limits.ts +3 -0
  232. package/app/src/world/video.ts +98 -0
  233. package/app/studio/activity.tsx +124 -0
  234. package/app/studio/agent.tsx +384 -0
  235. package/app/studio/api.ts +111 -0
  236. package/app/studio/atelier.tsx +234 -0
  237. package/app/studio/audio-missing.tsx +25 -0
  238. package/app/studio/audio-setup.tsx +33 -0
  239. package/app/studio/blender-setup.tsx +30 -0
  240. package/app/studio/build.tsx +679 -0
  241. package/app/studio/camera.tsx +59 -0
  242. package/app/studio/cast-work.tsx +207 -0
  243. package/app/studio/cast.tsx +178 -0
  244. package/app/studio/concept-edit.tsx +47 -0
  245. package/app/studio/creator-idea.tsx +46 -0
  246. package/app/studio/custom-look.tsx +36 -0
  247. package/app/studio/faces-setup.tsx +28 -0
  248. package/app/studio/favicon.svg +11 -0
  249. package/app/studio/figure.tsx +314 -0
  250. package/app/studio/fonts/OFL.txt +93 -0
  251. package/app/studio/fonts/fonts.css +7 -0
  252. package/app/studio/fonts/fraunces-italic-latin-ext.woff2 +0 -0
  253. package/app/studio/fonts/fraunces-italic-latin.woff2 +0 -0
  254. package/app/studio/fonts/fraunces-normal-latin-ext.woff2 +0 -0
  255. package/app/studio/fonts/fraunces-normal-latin.woff2 +0 -0
  256. package/app/studio/fonts/instrument-sans-normal-latin-ext.woff2 +0 -0
  257. package/app/studio/fonts/instrument-sans-normal-latin.woff2 +0 -0
  258. package/app/studio/icons.tsx +42 -0
  259. package/app/studio/idea-card.tsx +42 -0
  260. package/app/studio/idea-review.tsx +181 -0
  261. package/app/studio/idea-storyboard.tsx +134 -0
  262. package/app/studio/index.html +15 -0
  263. package/app/studio/job-words.ts +33 -0
  264. package/app/studio/lightbox.tsx +49 -0
  265. package/app/studio/main.tsx +1329 -0
  266. package/app/studio/nav.ts +14 -0
  267. package/app/studio/place.tsx +264 -0
  268. package/app/studio/review-sketch.tsx +47 -0
  269. package/app/studio/say-it.tsx +49 -0
  270. package/app/studio/step-work.tsx +134 -0
  271. package/app/studio/story.tsx +394 -0
  272. package/app/studio/style-menu.tsx +81 -0
  273. package/app/studio/style.css +1390 -0
  274. package/app/studio/sure.tsx +54 -0
  275. package/app/studio/tip.tsx +74 -0
  276. package/app/studio/tsconfig.json +5 -0
  277. package/app/studio/video.tsx +73 -0
  278. package/app/studio/world.tsx +188 -0
  279. package/app/tsconfig.json +13 -0
  280. package/app/web/audio.ts +315 -0
  281. package/app/web/face.ts +131 -0
  282. package/app/web/face3d.ts +35 -0
  283. package/app/web/glows.ts +387 -0
  284. package/app/web/hero3d.ts +728 -0
  285. package/app/web/index.html +118 -0
  286. package/app/web/living.ts +404 -0
  287. package/app/web/main.ts +3769 -0
  288. package/app/web/pad.ts +174 -0
  289. package/app/web/reactions.ts +79 -0
  290. package/app/web/sheet.ts +36 -0
  291. package/app/web/synth.ts +140 -0
  292. package/app/web/tsconfig.json +5 -0
  293. package/app/web/ui.ts +263 -0
  294. package/app/web/weather.ts +306 -0
  295. package/app/web/words.ts +12 -0
  296. package/docs/characters.md +58 -0
  297. package/docs/npc-motion.md +69 -0
  298. package/docs/scenes.md +75 -0
  299. package/docs/setup.md +60 -0
  300. package/docs/sound.md +23 -0
  301. package/docs/studio.md +62 -0
  302. package/package.json +70 -4
@@ -0,0 +1,387 @@
1
+ // The painted lights alive (src/play/glow.ts): a soft halo added over each light that breathes, flickers or
2
+ // twinkles, and a lighthouse lamp's beam sweeping round, flaring when it turns towards us. Drawn with 'lighter'
3
+ // over the painting and its cards, so it only ever adds light, never covers what is painted. The one exception
4
+ // is a lighthouse's own painted light paper (a burst of golden rays, a tissue beam): it is lifted off the painting
5
+ // as a card, its place filled from the sky round it, and turned behind the cards (drawBehind), so the painted
6
+ // light itself moves and no second beam is laid over a frozen one.
7
+ import { beamAngle, beamPaper, fillUnder, glowLevel, paperPose, type Glow } from '../src/play/glow.ts';
8
+ import { hashSeed } from '../src/play/weather.ts';
9
+
10
+ type Shift = (y: number) => [number, number];
11
+ type Painting = CanvasImageSource & { width: number; height: number };
12
+ /** A lighthouse's light paper as a card: its paper, the fill under it, where it may show, and a canvas to turn it on. */
13
+ type Paper = { card: HTMLCanvasElement; patch: HTMLCanvasElement; px: number; py: number; keep: HTMLCanvasElement; turned: HTMLCanvasElement; box: [number, number, number, number]; burst: boolean };
14
+ /** Light thrown on the rain falling through it: a pool round a point, by brightness 0..1. */
15
+ export type Pool = { x: number; y: number; r: number; c: string; a: number };
16
+
17
+ /** #rrggbb as "r,g,b" for rgba() strings. */
18
+ const rgb = (hex: string) => {
19
+ const n = parseInt(hex.replace('#', '').slice(0, 6).padEnd(6, 'f'), 16);
20
+ return `${(n >> 16) & 255},${(n >> 8) & 255},${n & 255}`;
21
+ };
22
+
23
+ export class GlowFx {
24
+ private time = 0;
25
+ /** Far lights twinkle as a few sparks spread round their point, fixed per light. */
26
+ private readonly sparks: Array<Array<[number, number, number]>>;
27
+
28
+ /** Each lit light's own bright paint (a lantern's glass, a lit window), cut from the painting: drawn over itself
29
+ * in time with the light, so the painted lamp itself flickers and flares, not only a halo laid over it. */
30
+ private readonly lit: Array<{ canvas: HTMLCanvasElement; x: number; y: number } | undefined>;
31
+
32
+ private readonly glows: Glow[];
33
+ /** Each planned beam's lifted light paper, by glow index. */
34
+ private readonly papers: Array<Paper | undefined>;
35
+ /** Where the lights shine on the falling rain this frame (drawAir reads it). */
36
+ pools: Pool[] = [];
37
+
38
+ constructor(planned: Glow[], private readonly H: number, painting?: Painting, wet?: Uint8Array, cards: number[][] = []) {
39
+ const frame = painting && planned.some((g) => g.kind === 'beam') ? pixelsOf(painting) : undefined;
40
+ this.papers = planned.map((g) => (frame && g.kind === 'beam' ? liftPaper(painting!, frame, g, planned.filter((o) => o.kind !== 'beam' && Math.hypot(o.x - g.x, o.y - g.y) > 120), wet) : undefined));
41
+ const onPaper = (x: number, y: number) => this.papers.some((p) => p && x >= p.box[0] && y >= p.box[1] && x < p.box[2] && y < p.box[3] && p.keep.getContext('2d')!.getImageData(Math.floor(x - p.box[0]), Math.floor(y - p.box[1]), 1, 1).data[0] > 200);
42
+ // The planned lights, then every other light Muse painted (a market's strings of lanterns, candle boats, lit
43
+ // windows), found in the painting itself: each flickers on its own phase too. Not on a lighthouse's rays.
44
+ this.glows = painting ? [...planned, ...paintedLights(painting, planned, wet, cards).filter((g) => !onPaper(g.x, g.y))] : planned;
45
+ const glows = this.glows;
46
+ this.lit = glows.map((g) => (painting && g.kind !== 'twinkle' && g.kind !== 'beam' ? brightPaint(painting, g, H) : undefined));
47
+ this.sparks = glows.map((g) => {
48
+ let h = hashSeed(g.key);
49
+ const r = () => ((h = Math.imul(h ^ (h >>> 15), 2246822507) >>> 0) % 1000) / 1000;
50
+ return g.kind === 'twinkle' ? Array.from({ length: 7 }, () => [(r() - 0.5) * 90, (r() - 0.5) * 22, r() * 6.28] as [number, number, number]) : [];
51
+ });
52
+ }
53
+
54
+ step(dt: number) {
55
+ this.time += dt;
56
+ }
57
+
58
+ /** The lighthouse's light paper, turned: over the place's far paint, under its cards (the tower stands in front). */
59
+ drawBehind(ctx: CanvasRenderingContext2D, shift: Shift) {
60
+ this.papers.forEach((p, i) => {
61
+ if (!p?.burst) return;
62
+ const g = this.glows[i];
63
+ const pose = paperPose(g, p.burst, this.time);
64
+ const [sx, sy] = shift(g.y);
65
+ ctx.drawImage(p.patch, p.px + sx, p.py + sy);
66
+ const c = p.turned.getContext('2d')!;
67
+ const [lx, ly] = [g.x - p.box[0], g.y - p.box[1]];
68
+ c.clearRect(0, 0, p.turned.width, p.turned.height);
69
+ c.save();
70
+ c.globalAlpha = pose.alpha;
71
+ c.translate(lx, ly);
72
+ c.rotate(pose.turn);
73
+ c.scale(pose.scale, pose.scale);
74
+ c.translate(-lx, -ly);
75
+ c.drawImage(p.card, 0, 0);
76
+ c.restore();
77
+ // Only over the sky, the sea and where the paper was: never over a boat's sail or the tower.
78
+ c.globalCompositeOperation = 'destination-in';
79
+ c.drawImage(p.keep, 0, 0);
80
+ c.globalCompositeOperation = 'source-over';
81
+ ctx.drawImage(p.turned, p.box[0] + sx, p.box[1] + sy);
82
+ });
83
+ }
84
+
85
+ draw(ctx: CanvasRenderingContext2D, shift: Shift) {
86
+ const t = this.time;
87
+ const pools: Pool[] = [];
88
+ ctx.save();
89
+ ctx.globalCompositeOperation = 'lighter';
90
+ this.glows.forEach((g, i) => {
91
+ const [sx, sy] = shift(g.y);
92
+ const x = g.x + sx;
93
+ const y = g.y + sy;
94
+ // Nearer lights (lower in the picture) are bigger.
95
+ const near = Math.max(0, Math.min(1, g.y / this.H));
96
+ const level = glowLevel(g, t);
97
+ const c = rgb(g.colour);
98
+ if (g.kind === 'twinkle') {
99
+ for (const [dx, dy, ph] of this.sparks[i]) {
100
+ const a = Math.max(0, Math.sin(t * 2.6 + ph * 3) * 0.5 + Math.sin(t * 0.9 + ph) * 0.5);
101
+ halo(ctx, x + dx, y + dy, 3 + 4 * a, c, 0.55 * a);
102
+ }
103
+ return;
104
+ }
105
+ const paper = this.papers[i];
106
+ if (g.kind === 'beam' && paper) {
107
+ // The painted paper is the beam (drawBehind); here only its glow: the paper lit up again, brighter as it
108
+ // breathes or turns to us, and the lamp flaring.
109
+ const pose = paperPose(g, paper.burst, t);
110
+ const [sx2, sy2] = shift(g.y);
111
+ if (paper.burst) {
112
+ ctx.globalAlpha = Math.min(1, 0.22 + 0.35 * pose.flare + 0.2 * (level - 0.5));
113
+ ctx.drawImage(paper.turned, paper.box[0] + sx2, paper.box[1] + sy2);
114
+ ctx.globalAlpha = 1;
115
+ pools.push({ x, y, r: 260, c, a: 0.35 + 0.3 * pose.flare });
116
+ } else {
117
+ // The painted beam lit by a wave running out of the lamp along it: its own paper, brightest at the wave.
118
+ const dir = (paper.box[0] + paper.box[2]) / 2 < g.x ? -1 : 1;
119
+ const far = dir < 0 ? g.x - paper.box[0] : paper.box[2] - g.x;
120
+ const tc = paper.turned.getContext('2d')!;
121
+ const lx = g.x - paper.box[0];
122
+ tc.clearRect(0, 0, paper.turned.width, paper.turned.height);
123
+ tc.globalCompositeOperation = 'source-over';
124
+ tc.drawImage(paper.card, 0, 0);
125
+ const band = tc.createLinearGradient(lx, 0, lx + dir * far, 0);
126
+ const at = (u: number) => Math.max(0, Math.min(1, u));
127
+ band.addColorStop(0, 'rgba(255,255,255,0.35)');
128
+ band.addColorStop(at(pose.wave - 0.16), 'rgba(255,255,255,0.3)');
129
+ band.addColorStop(at(pose.wave), 'rgba(255,255,255,1)');
130
+ band.addColorStop(at(pose.wave + 0.12), 'rgba(255,255,255,0.3)');
131
+ band.addColorStop(1, 'rgba(255,255,255,0.25)');
132
+ tc.globalCompositeOperation = 'destination-in';
133
+ tc.fillStyle = band;
134
+ tc.fillRect(0, 0, paper.turned.width, paper.turned.height);
135
+ tc.globalCompositeOperation = 'source-over';
136
+ ctx.globalAlpha = 0.6 + 0.3 * level;
137
+ ctx.drawImage(paper.turned, paper.box[0] + sx2, paper.box[1] + sy2);
138
+ ctx.globalAlpha = 1;
139
+ if (pose.wave <= 1) pools.push({ x: x + dir * far * pose.wave, y, r: 200, c, a: 0.7 });
140
+ for (let k = 1; k <= 4; k++) pools.push({ x: x + (dir * far * k) / 5, y, r: 120, c, a: 0.25 });
141
+ }
142
+ halo(ctx, x, y, (60 + 60 * near) * (1 + 1.6 * pose.flare), c, 0.35 + 0.25 * level + 0.5 * pose.flare);
143
+ halo(ctx, x, y, (14 + 10 * near) * (1 + pose.flare), '255,250,235', 0.5 + 0.4 * pose.flare);
144
+ pools.push({ x, y, r: 160 * (1 + pose.flare), c, a: 0.3 + 0.6 * pose.flare });
145
+ return;
146
+ }
147
+ if (g.kind === 'beam') {
148
+ const a = beamAngle(g, t);
149
+ // Seen from the side, the turning beam swings left and right; pointing at us it flares, away it is gone.
150
+ const across = Math.cos(a);
151
+ const towards = Math.sin(a);
152
+ // Even a far lighthouse throws its beam across the whole sky: it is the brightest thing in a night storm.
153
+ const reach = (900 + 600 * near) * Math.abs(across);
154
+ if (reach > 8) {
155
+ const end = x + Math.sign(across) * reach;
156
+ const spread = 26 + 150 * Math.abs(across) * (0.5 + near);
157
+ const strength = 0.6 + 0.4 * towards * towards;
158
+ // A wide soft cone, and a narrow white-hot core inside it, both blurred: light, not a cut-out wedge.
159
+ // Soft as light without a blur filter (a canvas blur every frame halves the frame rate): each cone is
160
+ // laid three times, a little wider and fainter each time, so its edge fades out.
161
+ for (const [w, a0, a1, colour] of [[spread, 0.85, 0.34, c], [spread * 0.3, 0.8, 0.18, '255,250,235']] as const) {
162
+ for (const [grow, share] of [[1.35, 0.25], [1.15, 0.3], [0.9, 0.45]] as const) {
163
+ const grad = ctx.createLinearGradient(x, y, end, y);
164
+ grad.addColorStop(0, `rgba(${colour},${a0 * strength * share})`);
165
+ grad.addColorStop(0.45, `rgba(${colour},${a1 * strength * share})`);
166
+ grad.addColorStop(1, `rgba(${colour},0)`);
167
+ ctx.fillStyle = grad;
168
+ ctx.beginPath();
169
+ ctx.moveTo(x, y - 5 * grow);
170
+ ctx.lineTo(end, y - w * grow);
171
+ ctx.lineTo(end, y + w * grow);
172
+ ctx.lineTo(x, y + 5 * grow);
173
+ ctx.closePath();
174
+ ctx.fill();
175
+ }
176
+ }
177
+ for (let k = 1; k <= 6; k++) pools.push({ x: x + ((end - x) * k) / 6, y, r: 60 + (spread * k) / 4, c, a: 0.5 * (1 - k / 8) });
178
+ }
179
+ const flare = towards > 0 ? Math.pow(Math.max(0, 1 - Math.abs(across) * 2.5), 2) : 0;
180
+ halo(ctx, x, y, (30 + 40 * near) * (1 + 1.4 * flare), c, 0.28 + 0.2 * level + 0.5 * flare);
181
+ pools.push({ x, y, r: 120 * (1 + flare), c, a: 0.3 + 0.5 * flare });
182
+ return;
183
+ }
184
+ const r = g.kind === 'window' ? 16 + 34 * near : g.kind === 'flame' ? 26 + 44 * near : 22 + 40 * near;
185
+ const a = g.kind === 'flame' ? 0.42 : g.kind === 'window' ? 0.34 : 0.3;
186
+ // The light thrown round: a wide faint spill on the walls and the ground, rising and falling with the light.
187
+ const found = g.key.startsWith('painted-');
188
+ const lamp = !found && g.kind !== 'window';
189
+ if (!found) halo(ctx, x, y, r * (2.6 + 0.5 * level), c, (g.kind === 'window' ? 0.07 : 0.11) * (0.3 + level));
190
+ // A lamp or a flame blooms: a wide soft light breathing round it, as if the wet air glowed.
191
+ if (lamp) halo(ctx, x, y, r * (3.4 + 1.4 * level), c, 0.03 + 0.32 * level);
192
+ const lit = this.lit[i];
193
+ if (lit) {
194
+ // The lamp's own paint over itself (its glass, a scribbled halo round it): brighter on a flare, gone back
195
+ // to the painting on a dip, and a lamp's halo swelling and turning a little as it breathes, so its painted
196
+ // rays shimmer. A flame's tongue leans a little with its flicker.
197
+ const lean = g.kind === 'flame' ? (level - 0.5) * 3 * (0.5 + near) : 0;
198
+ ctx.globalAlpha = Math.max(0, Math.min(1, (g.kind === 'window' ? 0.45 : found ? 0.5 : 1.1) * (level - 0.15)));
199
+ if (lamp) {
200
+ const ph = (hashSeed(g.key) % 628) / 100;
201
+ ctx.save();
202
+ ctx.translate(x, y);
203
+ ctx.rotate(0.035 * Math.sin(t * 0.9 + ph));
204
+ ctx.scale(1 + 0.09 * (level - 0.5), 1 + 0.09 * (level - 0.5));
205
+ ctx.translate(-x, -y);
206
+ ctx.drawImage(lit.canvas, lit.x + sx + lean, lit.y + sy);
207
+ ctx.restore();
208
+ } else ctx.drawImage(lit.canvas, lit.x + sx + lean, lit.y + sy);
209
+ ctx.globalAlpha = 1;
210
+ }
211
+ halo(ctx, x, y, r * (found ? 0.45 : 1) * (0.85 + 0.3 * level), c, a * (found ? 0.6 : 1) * (0.4 + level));
212
+ // A white-hot heart for a lamp, and its light on the rain round it.
213
+ if (lamp) halo(ctx, x, y, r * 0.35 * (0.8 + 0.4 * level), '255,248,225', 0.2 + 0.35 * level);
214
+ if (!found && g.kind !== 'window') pools.push({ x, y, r: r * (4.5 + 1.5 * level), c, a: 0.3 + 0.8 * level });
215
+ });
216
+ ctx.restore();
217
+ this.pools = pools;
218
+ }
219
+ }
220
+
221
+ /** The painting's pixels at full size. */
222
+ function pixelsOf(painting: Painting): ImageData {
223
+ const c = Object.assign(document.createElement('canvas'), { width: painting.width, height: painting.height }).getContext('2d', { willReadFrequently: true })!;
224
+ c.drawImage(painting, 0, 0);
225
+ return c.getImageData(0, 0, painting.width, painting.height);
226
+ }
227
+
228
+ /**
229
+ * A beam's light paper lifted off the painting (beamPaper): the paper as a card, and what goes under it
230
+ * (fillUnder), so turned paper never leaves its painted twin behind.
231
+ */
232
+ function liftPaper(painting: Painting, frame: ImageData, g: Glow, others: Glow[], wet?: Uint8Array): Paper | undefined {
233
+ const found = beamPaper(frame.data, frame.width, frame.height, g, others);
234
+ if (!found) return undefined;
235
+ const [x0, y0, x1, y1] = found.box;
236
+ const [w, h, W] = [x1 - x0, y1 - y0, frame.width];
237
+ const canvas = (cw: number, ch: number) => Object.assign(document.createElement('canvas'), { width: cw, height: ch });
238
+ const card = canvas(w, h);
239
+ const keep = canvas(w, h);
240
+ const cardImg = new ImageData(w, h);
241
+ const keepImg = new ImageData(w, h);
242
+ for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) {
243
+ const [o, i] = [(y * w + x) * 4, ((y + y0) * W + x + x0) * 4];
244
+ // Only the light itself turns: warm paint, never the sea, foam or sky it was painted over.
245
+ const [r, gr, b] = [frame.data[i], frame.data[i + 1], frame.data[i + 2]];
246
+ const warm = Math.max(0, Math.min(1, (r - b - 45) / 45)) * Math.max(0, Math.min(1, (r + gr - 260) / 60));
247
+ const al = found.alpha[y * w + x] * warm;
248
+ cardImg.data.set([r, gr, b, Math.round(255 * al)], o);
249
+ // Where turned paper may show: where paper was, and the blue of the sky and the sea.
250
+ const open = al > 0.05 || frame.data[i + 2] > frame.data[i] + 8;
251
+ keepImg.data.set([255, 255, 255, open ? 255 : 0], o);
252
+ }
253
+ card.getContext('2d')!.putImageData(cardImg, 0, 0);
254
+ keep.getContext('2d', { willReadFrequently: true })!.putImageData(keepImg, 0, 0);
255
+ // The fill under it: the paint round it, carried in across the paper (fillUnder). Only a burst is lifted.
256
+ const patch = canvas(w, h);
257
+ if (found.burst) {
258
+ // The fill goes only where the light was painted, and not on water: the moving sea is drawn there already.
259
+ const fill = fillUnder(frame.data, W, g, found) as Uint8ClampedArray<ArrayBuffer>;
260
+ for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) {
261
+ const o = (y * w + x) * 4;
262
+ if (cardImg.data[o + 3] < 8 || (wet && wet[(y + y0) * W + x + x0] > 100)) fill[o + 3] = 0;
263
+ }
264
+ patch.getContext('2d')!.putImageData(new ImageData(fill, w, h), 0, 0);
265
+ }
266
+ return { card, patch, px: x0, py: y0, keep, turned: canvas(w, h), box: found.box, burst: found.burst };
267
+ }
268
+
269
+ /**
270
+ * The painting's bright warm pixels round a light (a lantern's glass and flame, a window's panes), soft-edged:
271
+ * what lights up when the light flares. Brighter than most of the picture and warmer than it is blue.
272
+ */
273
+ function brightPaint(painting: CanvasImageSource & { width: number; height: number }, g: Glow, H: number) {
274
+ const near = Math.max(0, Math.min(1, g.y / H));
275
+ // A lamp's reach takes in the halo painted round it (a crayon scribble, painted rays), so that breathes too.
276
+ const R = Math.round(g.key.startsWith('painted-') ? 12 + 22 * near : g.kind === 'window' ? 22 + 40 * near : g.kind === 'lamp' ? 60 + 100 * near : 30 + 50 * near);
277
+ const x0 = Math.max(0, Math.round(g.x - R));
278
+ const y0 = Math.max(0, Math.round(g.y - R));
279
+ const w = Math.min(painting.width, Math.round(g.x + R)) - x0;
280
+ const h = Math.min(painting.height, Math.round(g.y + R)) - y0;
281
+ if (w < 4 || h < 4) return undefined;
282
+ const canvas = Object.assign(document.createElement('canvas'), { width: w, height: h });
283
+ const c = canvas.getContext('2d', { willReadFrequently: true })!;
284
+ c.drawImage(painting, x0, y0, w, h, 0, 0, w, h);
285
+ const img = c.getImageData(0, 0, w, h);
286
+ const px = img.data;
287
+ let any = 0;
288
+ for (let at = 0; at < px.length; at += 4) {
289
+ const [r, gr, b] = [px[at], px[at + 1], px[at + 2]];
290
+ const luma = 0.3 * r + 0.59 * gr + 0.11 * b;
291
+ const warm = r - b;
292
+ // Soft in and out: from luma 160 to 220, and only where red outweighs blue (lamplight, not a pale sky).
293
+ const k = Math.max(0, Math.min(1, (luma - 160) / 60)) * Math.max(0, Math.min(1, (warm - 20) / 40));
294
+ // And fading towards the patch's rim, so no square edge shows.
295
+ const dx = (at / 4) % w + x0 - g.x;
296
+ const dy = Math.floor(at / 4 / w) + y0 - g.y;
297
+ const fall = Math.max(0, 1 - Math.hypot(dx, dy) / R) ** 0.7;
298
+ px[at + 3] = Math.round(255 * k * fall);
299
+ if (px[at + 3] > 40) any++;
300
+ }
301
+ if (any < 6) return undefined;
302
+ c.putImageData(img, 0, 0);
303
+ return { canvas, x: x0, y: y0 };
304
+ }
305
+
306
+ /** At most this many lights found in the painting come alive, the brightest first. */
307
+ const MAX_PAINTED = 48;
308
+
309
+ /**
310
+ * The lights painted in the picture beyond the planned ones: warm, bright blobs (a lantern's paper, a candle, a
311
+ * lit pane), found on a quarter-size copy. Each becomes a flame, or on water (a reflection) a flame too but drawn
312
+ * over the shimmer; far from any planned light, so none is lit twice. No model call.
313
+ */
314
+ function paintedLights(painting: CanvasImageSource & { width: number; height: number }, planned: Glow[], wet?: Uint8Array, cards: number[][] = []): Glow[] {
315
+ const S = 4;
316
+ const [w, h] = [Math.floor(painting.width / S), Math.floor(painting.height / S)];
317
+ if (w < 8 || h < 8) return [];
318
+ const c = Object.assign(document.createElement('canvas'), { width: w, height: h }).getContext('2d', { willReadFrequently: true })!;
319
+ c.drawImage(painting, 0, 0, w, h);
320
+ const px = c.getImageData(0, 0, w, h).data;
321
+ const hot = new Uint8Array(w * h);
322
+ for (let i = 0; i < w * h; i++) {
323
+ const [r, g, b] = [px[i * 4], px[i * 4 + 1], px[i * 4 + 2]];
324
+ if (0.3 * r + 0.59 * g + 0.11 * b > 190 && r - b > 70 && r > 230) hot[i] = 1;
325
+ }
326
+ const seen = new Uint8Array(w * h);
327
+ const found: Array<Glow & { n: number }> = [];
328
+ for (let start = 0; start < w * h; start++) {
329
+ if (!hot[start] || seen[start]) continue;
330
+ const stack = [start];
331
+ seen[start] = 1;
332
+ let [n, sx, sy, sr, sg, sb] = [0, 0, 0, 0, 0, 0];
333
+ let [x0, y0, x1, y1] = [w, h, 0, 0];
334
+ while (stack.length) {
335
+ const i = stack.pop()!;
336
+ const [x, y] = [i % w, Math.floor(i / w)];
337
+ n++; sx += x; sy += y; sr += px[i * 4]; sg += px[i * 4 + 1]; sb += px[i * 4 + 2];
338
+ [x0, y0, x1, y1] = [Math.min(x0, x), Math.min(y0, y), Math.max(x1, x + 1), Math.max(y1, y + 1)];
339
+ for (const j of [x > 0 ? i - 1 : -1, x < w - 1 ? i + 1 : -1, y > 0 ? i - w : -1, y < h - 1 ? i + w : -1]) {
340
+ if (j >= 0 && hot[j] && !seen[j]) { seen[j] = 1; stack.push(j); }
341
+ }
342
+ }
343
+ // A lantern or a pane, not a speck and not a whole sunset.
344
+ if (n < 2 || n > 150) continue;
345
+ // Nor a thin painted ray or streak: a light is round-ish, not a line.
346
+ if (Math.max(x1 - x0, y1 - y0) > 4 && n / ((x1 - x0) * (y1 - y0)) < 0.3) continue;
347
+ const [x, y] = [(sx / n + 0.5) * S, (sy / n + 0.5) * S];
348
+ // A light shines out of darker paint round it; a sunlit yellow wall does not.
349
+ const r0 = Math.ceil(Math.sqrt(n)) + 3;
350
+ let [ring, rn] = [0, 0];
351
+ for (let a = 0; a < 16; a++) {
352
+ const [qx, qy] = [Math.round(sx / n + r0 * Math.cos((a * Math.PI) / 8)), Math.round(sy / n + r0 * Math.sin((a * Math.PI) / 8))];
353
+ if (qx < 0 || qy < 0 || qx >= w || qy >= h) continue;
354
+ const q = (qy * w + qx) * 4;
355
+ ring += 0.3 * px[q] + 0.59 * px[q + 1] + 0.11 * px[q + 2];
356
+ rn++;
357
+ }
358
+ const inner = (0.3 * sr + 0.59 * sg + 0.11 * sb) / n;
359
+ if (!rn || inner - ring / rn < 70) continue;
360
+ if (planned.some((g) => Math.hypot(g.x - x, g.y - y) < 40)) continue;
361
+ // A light on a card slides with it; a glow laid at its painted place would part from it.
362
+ if (cards.some((b) => x >= b[0] - 10 && x <= b[2] + 10 && y >= b[1] - 10 && y <= b[3] + 10)) continue;
363
+ const hex = (v: number) => Math.round(Math.min(255, v / n)).toString(16).padStart(2, '0');
364
+ const onWater = !!wet && wet[Math.round(y) * painting.width + Math.round(x)] > 100;
365
+ found.push({ key: `painted-${onWater ? 'reflection' : 'lantern'}-${Math.round(x)}-${Math.round(y)}`, x, y, colour: `#${hex(sr)}${hex(sg)}${hex(sb)}`, kind: 'flame', n });
366
+ }
367
+ // Brightest (biggest) first, and not two on top of each other.
368
+ found.sort((a, b) => b.n - a.n);
369
+ const out: Glow[] = [];
370
+ for (const f of found) {
371
+ if (out.length >= MAX_PAINTED) break;
372
+ if (out.some((o) => Math.hypot(o.x - f.x, o.y - f.y) < 18)) continue;
373
+ out.push({ key: f.key, x: f.x, y: f.y, colour: f.colour, kind: f.kind });
374
+ }
375
+ return out;
376
+ }
377
+
378
+ /** A soft round glow: bright at its heart, fading to nothing at r. */
379
+ function halo(ctx: CanvasRenderingContext2D, x: number, y: number, r: number, c: string, a: number) {
380
+ if (a <= 0.01 || r <= 0.5) return;
381
+ const grad = ctx.createRadialGradient(x, y, 0, x, y, r);
382
+ grad.addColorStop(0, `rgba(${c},${Math.min(1, a)})`);
383
+ grad.addColorStop(0.35, `rgba(${c},${Math.min(1, a) * 0.45})`);
384
+ grad.addColorStop(1, `rgba(${c},0)`);
385
+ ctx.fillStyle = grad;
386
+ ctx.fillRect(x - r, y - r, 2 * r, 2 * r);
387
+ }