@olegkoval/agent-skills 1.60.0 → 1.62.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 (223) hide show
  1. package/.github/copilot-instructions.md +2 -0
  2. package/.github/prompts/carousel-maker.prompt.md +102 -0
  3. package/.github/prompts/shorts-animator.prompt.md +103 -0
  4. package/.kiro/steering/carousel-maker.md +101 -0
  5. package/.kiro/steering/shorts-animator.md +102 -0
  6. package/.windsurf/rules/carousel-maker.md +100 -0
  7. package/.windsurf/rules/shorts-animator.md +101 -0
  8. package/README.md +4 -2
  9. package/adapters/claude/olko-creative/skills/carousel-maker/SKILL.md +112 -0
  10. package/adapters/claude/olko-creative/skills/carousel-maker/scripts/cm.ts +220 -0
  11. package/adapters/claude/olko-creative/skills/carousel-maker/scripts/domain/check.ts +68 -0
  12. package/adapters/claude/olko-creative/skills/carousel-maker/scripts/domain/layout.ts +75 -0
  13. package/adapters/claude/olko-creative/skills/carousel-maker/scripts/domain/schema.ts +54 -0
  14. package/adapters/claude/olko-creative/skills/carousel-maker/scripts/domain/types.ts +33 -0
  15. package/adapters/claude/olko-creative/skills/carousel-maker/scripts/edge/io.ts +87 -0
  16. package/adapters/claude/olko-creative/skills/carousel-maker/scripts/package.json +4 -0
  17. package/adapters/claude/olko-creative/skills/carousel-maker/scripts/test/domain.test.ts +99 -0
  18. package/adapters/claude/olko-creative/skills/carousel-maker/scripts/tsconfig.json +16 -0
  19. package/adapters/claude/olko-creative/skills/shorts-animator/SKILL.md +113 -0
  20. package/adapters/claude/olko-creative/skills/shorts-animator/references/kit.md +59 -0
  21. package/adapters/claude/olko-creative/skills/shorts-animator/references/looks.md +32 -0
  22. package/adapters/claude/olko-creative/skills/shorts-animator/references/writing.md +9 -0
  23. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/audio/compose.ts +290 -0
  24. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/audio/dsp.ts +193 -0
  25. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/audio/instruments.ts +185 -0
  26. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/audio/master.ts +55 -0
  27. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/audio/soundtrack.ts +119 -0
  28. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/domain/captions.ts +35 -0
  29. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/domain/check.ts +96 -0
  30. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/domain/look.ts +26 -0
  31. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/domain/patch.ts +38 -0
  32. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/domain/schema.ts +81 -0
  33. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/domain/timeline.ts +75 -0
  34. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/domain/types.ts +91 -0
  35. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/edge/chrome.ts +169 -0
  36. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/edge/doctor.ts +52 -0
  37. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/edge/media.ts +105 -0
  38. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/edge/page.ts +53 -0
  39. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/edge/probe.ts +75 -0
  40. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/edge/sound.ts +46 -0
  41. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/edge/voice.ts +183 -0
  42. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/looks/gouache.json +7 -0
  43. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/looks/ink.json +6 -0
  44. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/looks/oil.json +7 -0
  45. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/looks/pastel.json +7 -0
  46. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/looks/watercolor.json +6 -0
  47. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/package.json +4 -0
  48. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/runtime/globals.d.ts +199 -0
  49. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/runtime/kit.ts +856 -0
  50. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/runtime/paint.ts +268 -0
  51. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/runtime/stage.ts +181 -0
  52. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/runtime/tsconfig.json +12 -0
  53. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/sa.ts +262 -0
  54. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/test/domain.test.ts +107 -0
  55. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/tsconfig.json +16 -0
  56. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/tts.py +20 -0
  57. package/adapters/claude/olko-github-pr/skills/lekker-review/SKILL.md +30 -0
  58. package/adapters/claude/olko-github-pr/skills/lekker-review/references/context-gathering.md +3 -0
  59. package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/finding-state.mjs +207 -0
  60. package/adapters/codex/olko-creative/README.md +2 -0
  61. package/adapters/cursor/olko-creative/skills/carousel-maker/SKILL.md +112 -0
  62. package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/cm.ts +220 -0
  63. package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/domain/check.ts +68 -0
  64. package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/domain/layout.ts +75 -0
  65. package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/domain/schema.ts +54 -0
  66. package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/domain/types.ts +33 -0
  67. package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/edge/io.ts +87 -0
  68. package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/package.json +4 -0
  69. package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/test/domain.test.ts +99 -0
  70. package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/tsconfig.json +16 -0
  71. package/adapters/cursor/olko-creative/skills/shorts-animator/SKILL.md +113 -0
  72. package/adapters/cursor/olko-creative/skills/shorts-animator/references/kit.md +59 -0
  73. package/adapters/cursor/olko-creative/skills/shorts-animator/references/looks.md +32 -0
  74. package/adapters/cursor/olko-creative/skills/shorts-animator/references/writing.md +9 -0
  75. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/audio/compose.ts +290 -0
  76. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/audio/dsp.ts +193 -0
  77. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/audio/instruments.ts +185 -0
  78. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/audio/master.ts +55 -0
  79. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/audio/soundtrack.ts +119 -0
  80. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/domain/captions.ts +35 -0
  81. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/domain/check.ts +96 -0
  82. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/domain/look.ts +26 -0
  83. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/domain/patch.ts +38 -0
  84. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/domain/schema.ts +81 -0
  85. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/domain/timeline.ts +75 -0
  86. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/domain/types.ts +91 -0
  87. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/edge/chrome.ts +169 -0
  88. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/edge/doctor.ts +52 -0
  89. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/edge/media.ts +105 -0
  90. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/edge/page.ts +53 -0
  91. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/edge/probe.ts +75 -0
  92. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/edge/sound.ts +46 -0
  93. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/edge/voice.ts +183 -0
  94. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/looks/gouache.json +7 -0
  95. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/looks/ink.json +6 -0
  96. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/looks/oil.json +7 -0
  97. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/looks/pastel.json +7 -0
  98. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/looks/watercolor.json +6 -0
  99. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/package.json +4 -0
  100. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/runtime/globals.d.ts +199 -0
  101. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/runtime/kit.ts +856 -0
  102. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/runtime/paint.ts +268 -0
  103. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/runtime/stage.ts +181 -0
  104. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/runtime/tsconfig.json +12 -0
  105. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/sa.ts +262 -0
  106. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/test/domain.test.ts +107 -0
  107. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/tsconfig.json +16 -0
  108. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/tts.py +20 -0
  109. package/adapters/grok/olko-creative/skills/carousel-maker/SKILL.md +112 -0
  110. package/adapters/grok/olko-creative/skills/carousel-maker/scripts/cm.ts +220 -0
  111. package/adapters/grok/olko-creative/skills/carousel-maker/scripts/domain/check.ts +68 -0
  112. package/adapters/grok/olko-creative/skills/carousel-maker/scripts/domain/layout.ts +75 -0
  113. package/adapters/grok/olko-creative/skills/carousel-maker/scripts/domain/schema.ts +54 -0
  114. package/adapters/grok/olko-creative/skills/carousel-maker/scripts/domain/types.ts +33 -0
  115. package/adapters/grok/olko-creative/skills/carousel-maker/scripts/edge/io.ts +87 -0
  116. package/adapters/grok/olko-creative/skills/carousel-maker/scripts/package.json +4 -0
  117. package/adapters/grok/olko-creative/skills/carousel-maker/scripts/test/domain.test.ts +99 -0
  118. package/adapters/grok/olko-creative/skills/carousel-maker/scripts/tsconfig.json +16 -0
  119. package/adapters/grok/olko-creative/skills/shorts-animator/SKILL.md +113 -0
  120. package/adapters/grok/olko-creative/skills/shorts-animator/references/kit.md +59 -0
  121. package/adapters/grok/olko-creative/skills/shorts-animator/references/looks.md +32 -0
  122. package/adapters/grok/olko-creative/skills/shorts-animator/references/writing.md +9 -0
  123. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/audio/compose.ts +290 -0
  124. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/audio/dsp.ts +193 -0
  125. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/audio/instruments.ts +185 -0
  126. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/audio/master.ts +55 -0
  127. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/audio/soundtrack.ts +119 -0
  128. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/domain/captions.ts +35 -0
  129. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/domain/check.ts +96 -0
  130. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/domain/look.ts +26 -0
  131. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/domain/patch.ts +38 -0
  132. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/domain/schema.ts +81 -0
  133. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/domain/timeline.ts +75 -0
  134. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/domain/types.ts +91 -0
  135. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/edge/chrome.ts +169 -0
  136. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/edge/doctor.ts +52 -0
  137. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/edge/media.ts +105 -0
  138. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/edge/page.ts +53 -0
  139. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/edge/probe.ts +75 -0
  140. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/edge/sound.ts +46 -0
  141. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/edge/voice.ts +183 -0
  142. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/looks/gouache.json +7 -0
  143. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/looks/ink.json +6 -0
  144. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/looks/oil.json +7 -0
  145. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/looks/pastel.json +7 -0
  146. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/looks/watercolor.json +6 -0
  147. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/package.json +4 -0
  148. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/runtime/globals.d.ts +199 -0
  149. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/runtime/kit.ts +856 -0
  150. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/runtime/paint.ts +268 -0
  151. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/runtime/stage.ts +181 -0
  152. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/runtime/tsconfig.json +12 -0
  153. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/sa.ts +262 -0
  154. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/test/domain.test.ts +107 -0
  155. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/tsconfig.json +16 -0
  156. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/tts.py +20 -0
  157. package/adapters/hermes/olko-creative/README.md +2 -0
  158. package/catalog/skills.json +46 -0
  159. package/package.json +1 -1
  160. package/plugins/olko-apple-kit/.claude-plugin/plugin.json +1 -1
  161. package/plugins/olko-creative/.claude-plugin/plugin.json +1 -1
  162. package/plugins/olko-creative/skills/carousel-maker/SKILL.md +111 -0
  163. package/plugins/olko-creative/skills/carousel-maker/scripts/cm.ts +220 -0
  164. package/plugins/olko-creative/skills/carousel-maker/scripts/domain/check.ts +68 -0
  165. package/plugins/olko-creative/skills/carousel-maker/scripts/domain/layout.ts +75 -0
  166. package/plugins/olko-creative/skills/carousel-maker/scripts/domain/schema.ts +54 -0
  167. package/plugins/olko-creative/skills/carousel-maker/scripts/domain/types.ts +33 -0
  168. package/plugins/olko-creative/skills/carousel-maker/scripts/edge/io.ts +87 -0
  169. package/plugins/olko-creative/skills/carousel-maker/scripts/package.json +4 -0
  170. package/plugins/olko-creative/skills/carousel-maker/scripts/test/domain.test.ts +99 -0
  171. package/plugins/olko-creative/skills/carousel-maker/scripts/tsconfig.json +16 -0
  172. package/plugins/olko-creative/skills/shorts-animator/SKILL.md +112 -0
  173. package/plugins/olko-creative/skills/shorts-animator/references/kit.md +59 -0
  174. package/plugins/olko-creative/skills/shorts-animator/references/looks.md +32 -0
  175. package/plugins/olko-creative/skills/shorts-animator/references/writing.md +9 -0
  176. package/plugins/olko-creative/skills/shorts-animator/scripts/audio/compose.ts +290 -0
  177. package/plugins/olko-creative/skills/shorts-animator/scripts/audio/dsp.ts +193 -0
  178. package/plugins/olko-creative/skills/shorts-animator/scripts/audio/instruments.ts +185 -0
  179. package/plugins/olko-creative/skills/shorts-animator/scripts/audio/master.ts +55 -0
  180. package/plugins/olko-creative/skills/shorts-animator/scripts/audio/soundtrack.ts +119 -0
  181. package/plugins/olko-creative/skills/shorts-animator/scripts/domain/captions.ts +35 -0
  182. package/plugins/olko-creative/skills/shorts-animator/scripts/domain/check.ts +96 -0
  183. package/plugins/olko-creative/skills/shorts-animator/scripts/domain/look.ts +26 -0
  184. package/plugins/olko-creative/skills/shorts-animator/scripts/domain/patch.ts +38 -0
  185. package/plugins/olko-creative/skills/shorts-animator/scripts/domain/schema.ts +81 -0
  186. package/plugins/olko-creative/skills/shorts-animator/scripts/domain/timeline.ts +75 -0
  187. package/plugins/olko-creative/skills/shorts-animator/scripts/domain/types.ts +91 -0
  188. package/plugins/olko-creative/skills/shorts-animator/scripts/edge/chrome.ts +169 -0
  189. package/plugins/olko-creative/skills/shorts-animator/scripts/edge/doctor.ts +52 -0
  190. package/plugins/olko-creative/skills/shorts-animator/scripts/edge/media.ts +105 -0
  191. package/plugins/olko-creative/skills/shorts-animator/scripts/edge/page.ts +53 -0
  192. package/plugins/olko-creative/skills/shorts-animator/scripts/edge/probe.ts +75 -0
  193. package/plugins/olko-creative/skills/shorts-animator/scripts/edge/sound.ts +46 -0
  194. package/plugins/olko-creative/skills/shorts-animator/scripts/edge/voice.ts +183 -0
  195. package/plugins/olko-creative/skills/shorts-animator/scripts/looks/gouache.json +7 -0
  196. package/plugins/olko-creative/skills/shorts-animator/scripts/looks/ink.json +6 -0
  197. package/plugins/olko-creative/skills/shorts-animator/scripts/looks/oil.json +7 -0
  198. package/plugins/olko-creative/skills/shorts-animator/scripts/looks/pastel.json +7 -0
  199. package/plugins/olko-creative/skills/shorts-animator/scripts/looks/watercolor.json +6 -0
  200. package/plugins/olko-creative/skills/shorts-animator/scripts/package.json +4 -0
  201. package/plugins/olko-creative/skills/shorts-animator/scripts/runtime/globals.d.ts +199 -0
  202. package/plugins/olko-creative/skills/shorts-animator/scripts/runtime/kit.ts +856 -0
  203. package/plugins/olko-creative/skills/shorts-animator/scripts/runtime/paint.ts +268 -0
  204. package/plugins/olko-creative/skills/shorts-animator/scripts/runtime/stage.ts +181 -0
  205. package/plugins/olko-creative/skills/shorts-animator/scripts/runtime/tsconfig.json +12 -0
  206. package/plugins/olko-creative/skills/shorts-animator/scripts/sa.ts +262 -0
  207. package/plugins/olko-creative/skills/shorts-animator/scripts/test/domain.test.ts +107 -0
  208. package/plugins/olko-creative/skills/shorts-animator/scripts/tsconfig.json +16 -0
  209. package/plugins/olko-creative/skills/shorts-animator/scripts/tts.py +20 -0
  210. package/plugins/olko-garmin-kit/.claude-plugin/plugin.json +1 -1
  211. package/plugins/olko-git-tools/.claude-plugin/plugin.json +1 -1
  212. package/plugins/olko-github-pr/.claude-plugin/plugin.json +1 -1
  213. package/plugins/olko-github-pr/skills/lekker-review/SKILL.md +30 -0
  214. package/plugins/olko-github-pr/skills/lekker-review/references/context-gathering.md +3 -0
  215. package/plugins/olko-github-pr/skills/lekker-review/scripts/finding-state.mjs +207 -0
  216. package/plugins/olko-github-pr/skills/lekker-review/workflow.js +46 -3
  217. package/plugins/olko-obsidian/.claude-plugin/plugin.json +1 -1
  218. package/plugins/olko-product/.claude-plugin/plugin.json +1 -1
  219. package/plugins/olko-reflection/.claude-plugin/plugin.json +1 -1
  220. package/plugins/olko-release/.claude-plugin/plugin.json +1 -1
  221. package/plugins/olko-skill-meta/.claude-plugin/plugin.json +1 -1
  222. package/plugins/olko-web-ops/.claude-plugin/plugin.json +1 -1
  223. package/scripts/lib/catalog.mjs +2 -1
@@ -0,0 +1,856 @@
1
+ /*
2
+ * THE DRAWING KIT: what a scene is drawn with, flat, before the paint pass.
3
+ *
4
+ * A scene is a function (k) => { ... } that draws one shot. It gets:
5
+ * k.g the 2D context, in frame pixels (k.W × k.H), camera applied
6
+ * k.W, k.H the frame size; k.u = k.H / 1920 (so sizes read the same at any size)
7
+ * k.t seconds into this shot (already on twos), k.p its progress 0→1
8
+ * k.r() a random number, the same on every drawing of this shot
9
+ * k.pal the short's palette (look.palette), k.beat the line being said
10
+ * and every helper below. Helpers take an options object; anything left out
11
+ * has a sensible default. Positions are fractions of the frame (x: 0.5 is the
12
+ * middle, y: 0.7 is 70% down) unless a name ends in Px.
13
+ *
14
+ * Shapes are kept simple and readable, with clear silhouettes and big
15
+ * value contrast: the paint pass adds the texture, so nothing here should.
16
+ *
17
+ * Every helper that draws randomly makes its own seeded rng from its seed, and
18
+ * the order of those rng() calls is part of the picture: never reorder them.
19
+ */
20
+ (function () {
21
+ function rng(seed: number): Rng {
22
+ let s = (seed >>> 0) || 1;
23
+ return () => ((s = (Math.imul(s, 1664525) + 1013904223) >>> 0) / 4294967296);
24
+ }
25
+ function hashStr(s: Seed): number {
26
+ let h = 2166136261;
27
+ for (const ch of String(s)) h = Math.imul(h ^ ch.charCodeAt(0), 16777619);
28
+ return h >>> 0;
29
+ }
30
+ // smooth 1D noise for ridgelines
31
+ function noise1(seed: number): (x: number) => number {
32
+ const r = rng(seed), v = Array.from({ length: 512 }, r);
33
+ return (x) => {
34
+ const i = Math.floor(x), f = x - i, u = f * f * (3 - 2 * f);
35
+ return v[i & 511] * (1 - u) + v[(i + 1) & 511] * u;
36
+ };
37
+ }
38
+ function rgb(c: string): number[] {
39
+ if (c.startsWith("rgb")) {
40
+ const parts = c.match(/[\d.]+/g);
41
+ if (!parts) throw new Error(`kit: cannot read colour "${c}"`);
42
+ return parts.slice(0, 3).map(Number);
43
+ }
44
+ let h = c.replace("#", "");
45
+ if (h.length === 3) h = [...h].map((x) => x + x).join("");
46
+ const n = parseInt(h, 16);
47
+ return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
48
+ }
49
+ function mix(a: string, b: string, t: number): string {
50
+ const A = rgb(a), B = rgb(b);
51
+ return `rgb(${A.map((x, i) => Math.round(x + (B[i] - x) * t)).join(",")})`;
52
+ }
53
+ function alpha(c: string, a: number): string {
54
+ return `rgba(${rgb(c).join(",")},${a})`;
55
+ }
56
+
57
+ /** What every helper needs: the context (frame pixels), the frame, and the clock. */
58
+ interface Ctx {
59
+ g: CanvasRenderingContext2D;
60
+ W: number; H: number; u: number; t: number; p: number;
61
+ X: (x: number) => number;
62
+ Y: (y: number) => number;
63
+ }
64
+
65
+ /* ---------- sky and light ---------- */
66
+
67
+ /** A vertical gradient: stops = ["#top", "#mid", "#bottom"] or [[0,"#a"],[1,"#b"]]. */
68
+ function sky({ g, W, H, Y }: Ctx, { stops = ["#8fb7e8", "#dfe9f2"], y0 = 0, y1 = 1 }: SkyOpts = {}): void {
69
+ const gr = g.createLinearGradient(0, Y(y0), 0, Y(y1));
70
+ stops.forEach((s, i) => {
71
+ if (Array.isArray(s)) gr.addColorStop(s[0], s[1]);
72
+ else gr.addColorStop(i / Math.max(1, stops.length - 1), s);
73
+ });
74
+ g.fillStyle = gr;
75
+ g.fillRect(-W, Y(y0) - H, W * 3, Y(y1 - y0) + 2 * H);
76
+ }
77
+
78
+ function cloudPuffs(rr: Rng, cx: number, cy: number, w: number): [number, number, number][] {
79
+ const puffs = 4 + Math.floor(rr() * 5);
80
+ const P: [number, number, number][] = [];
81
+ for (let j = 0; j < puffs; j++) P.push([cx + (rr() - 0.5) * w, cy + (rr() - 0.7) * w * 0.18, w * (0.12 + 0.14 * rr())]);
82
+ return P;
83
+ }
84
+
85
+ /** Soft cumulus: clusters of ellipses in a band, drifting with t. */
86
+ function clouds(
87
+ { g, u, t, X, Y }: Ctx,
88
+ { y = 0.04, band = 0.3, n = 26, color = "#ffffff", shade = "#c9d6e6", size = 1, drift = 6, seed: s = 1, opacity = 0.95, perspective = true }: CloudsOpts = {},
89
+ ): void {
90
+ const rr = rng(hashStr("clouds" + s));
91
+ for (let i = 0; i < n; i++) {
92
+ const f = rr();
93
+ // nearer the horizon (lower in the band) they are smaller and flatter
94
+ const depth = perspective ? 1 - f * 0.6 : 1;
95
+ const cx = X(-0.15 + 1.3 * rr()) + t * drift * u * (0.6 + rr()) * depth;
96
+ const cy = Y(y + band * f);
97
+ const w = (70 + 170 * rr()) * size * u * depth;
98
+ const P = cloudPuffs(rr, cx, cy, w);
99
+ if (shade) {
100
+ g.fillStyle = alpha(shade, opacity);
101
+ for (const [px, py, pr] of P) { g.beginPath(); g.ellipse(px + pr * 0.15, py + pr * 0.3, pr * 1.25, pr * 0.55, 0, 0, 7); g.fill(); }
102
+ }
103
+ g.fillStyle = alpha(color, opacity);
104
+ for (const [px, py, pr] of P) { g.beginPath(); g.ellipse(px, py, pr * 1.2, pr * 0.58, 0, 0, 7); g.fill(); }
105
+ }
106
+ }
107
+
108
+ /** A disc with a glow: the sun, the moon, a street lamp's bulb. */
109
+ function sun(
110
+ { g, u, X, Y }: Ctx,
111
+ { x = 0.7, y = 0.2, r: rad = 70, color = "#fff6d8", glow = "#ffe9a8", glowSize = 5, glowAlpha = 0.55 }: SunOpts = {},
112
+ ): void {
113
+ const R = rad * u;
114
+ const gr = g.createRadialGradient(X(x), Y(y), R * 0.5, X(x), Y(y), R * glowSize);
115
+ gr.addColorStop(0, alpha(glow, glowAlpha)); gr.addColorStop(1, alpha(glow, 0));
116
+ g.fillStyle = gr; g.fillRect(X(x) - R * glowSize, Y(y) - R * glowSize, R * glowSize * 2, R * glowSize * 2);
117
+ g.fillStyle = color; g.beginPath(); g.arc(X(x), Y(y), R, 0, 7); g.fill();
118
+ }
119
+
120
+ /** A soft light anywhere (lamps, windows, fireflies, a candle). */
121
+ function glow({ g, u, X, Y }: Ctx, { x, y, r: rad = 60, color = "#ffd27a", a = 0.8 }: GlowOpts): void {
122
+ const R = rad * u;
123
+ const gr = g.createRadialGradient(X(x), Y(y), 0, X(x), Y(y), R);
124
+ gr.addColorStop(0, alpha(color, a)); gr.addColorStop(1, alpha(color, 0));
125
+ g.fillStyle = gr; g.fillRect(X(x) - R, Y(y) - R, 2 * R, 2 * R);
126
+ }
127
+
128
+ /** Stars on a night sky, twinkling slowly. */
129
+ function stars(
130
+ { g, W, u, t, Y }: Ctx,
131
+ { n = 120, y1 = 0.5, color = "#fffbe8", size = 2.4, seed: s = 3 }: StarsOpts = {},
132
+ ): void {
133
+ const rr = rng(hashStr("stars" + s));
134
+ for (let i = 0; i < n; i++) {
135
+ const x = rr() * W, y = rr() * Y(y1), tw = 0.5 + 0.5 * Math.sin(t * (1 + rr() * 2) + rr() * 6);
136
+ g.fillStyle = alpha(color, 0.35 + 0.65 * tw * rr());
137
+ g.beginPath(); g.arc(x, y, size * u * (0.4 + rr()), 0, 7); g.fill();
138
+ }
139
+ }
140
+
141
+ /** Shafts of light, from a point, in a fan. */
142
+ function rays(
143
+ { g, H, u, X, Y }: Ctx,
144
+ { x = 0.8, y = -0.05, angle = 115, spread = 22, n = 4, length = 1.4, color = "#fff3c4", a = 0.28, width = 60 }: RaysOpts = {},
145
+ ): void {
146
+ const rr = rng(hashStr("rays" + x + y));
147
+ g.save();
148
+ g.globalCompositeOperation = "screen";
149
+ g.filter = `blur(${Math.round(18 * u)}px)`;
150
+ for (let i = 0; i < n; i++) {
151
+ const ang = ((angle + (i / Math.max(1, n - 1) - 0.5) * spread) * Math.PI) / 180;
152
+ const w = width * u * (0.5 + rr());
153
+ const L = length * H;
154
+ const ex = X(x) + Math.cos(ang) * L, ey = Y(y) + Math.sin(ang) * L;
155
+ const gr = g.createLinearGradient(X(x), Y(y), ex, ey);
156
+ gr.addColorStop(0, alpha(color, a)); gr.addColorStop(1, alpha(color, 0));
157
+ g.fillStyle = gr;
158
+ const nx = -Math.sin(ang), ny = Math.cos(ang);
159
+ g.beginPath();
160
+ g.moveTo(X(x) + nx * w * 0.2, Y(y) + ny * w * 0.2);
161
+ g.lineTo(ex + nx * w, ey + ny * w);
162
+ g.lineTo(ex - nx * w, ey - ny * w);
163
+ g.lineTo(X(x) - nx * w * 0.2, Y(y) - ny * w * 0.2);
164
+ g.fill();
165
+ }
166
+ g.restore();
167
+ }
168
+
169
+ /** Rain: slanted streaks that fall with t. */
170
+ function rain(
171
+ { g, W, H, u, t }: Ctx,
172
+ { n = 260, color = "#dfe8f2", a = 0.45, slant = 0.18, length = 46, speed = 1, seed: s = 12 }: RainOpts = {},
173
+ ): void {
174
+ const rr = rng(hashStr("rain" + s));
175
+ g.strokeStyle = alpha(color, a);
176
+ g.lineCap = "round";
177
+ for (let i = 0; i < n; i++) {
178
+ const L = length * u * (0.5 + rr()), sp = (1400 + rr() * 900) * u * speed;
179
+ const x0 = rr() * W * 1.3 - W * 0.15, y0 = ((rr() * H * 1.2 + t * sp) % (H * 1.2)) - H * 0.1;
180
+ g.lineWidth = (1.5 + rr() * 2) * u;
181
+ g.beginPath(); g.moveTo(x0 - y0 * slant, y0); g.lineTo(x0 - (y0 + L) * slant, y0 + L); g.stroke();
182
+ }
183
+ }
184
+
185
+ /** Small lights drifting: fireflies, embers, lanterns far off. */
186
+ function motes(
187
+ c: Ctx,
188
+ { n = 24, y0 = 0.4, y1 = 0.9, color = "#ffcf6a", r: rad = 5, seed: s = 13 }: MotesOpts = {},
189
+ ): void {
190
+ const { g, u, t, X, Y } = c;
191
+ const rr = rng(hashStr("motes" + s));
192
+ for (let i = 0; i < n; i++) {
193
+ const x = rr() + Math.sin(t * (0.3 + rr() * 0.4) + i) * 0.02, y = y0 + (y1 - y0) * rr() + Math.cos(t * 0.4 + i * 2) * 0.01;
194
+ glow(c, { x, y, r: rad * 6, color, a: 0.35 });
195
+ g.fillStyle = color; g.beginPath(); g.arc(X(x), Y(y), rad * u, 0, 7); g.fill();
196
+ }
197
+ }
198
+
199
+ /** A band of haze: lays air between planes, so distance reads. */
200
+ function haze({ g, W, Y }: Ctx, { y = 0.5, h = 0.12, color = "#ffffff", a = 0.4 }: HazeOpts = {}): void {
201
+ const gr = g.createLinearGradient(0, Y(y - h), 0, Y(y + h));
202
+ gr.addColorStop(0, alpha(color, 0)); gr.addColorStop(0.5, alpha(color, a)); gr.addColorStop(1, alpha(color, 0));
203
+ g.fillStyle = gr; g.fillRect(-W, Y(y - h), W * 3, Y(2 * h));
204
+ }
205
+
206
+ /* ---------- land ---------- */
207
+
208
+ type RidgeHeight = (n: (x: number) => number, fx: number, freq: number) => number;
209
+ const RIDGE_HEIGHT = new Map<string, RidgeHeight>([
210
+ ["peaks", (n, fx, freq) => Math.pow(n(fx * freq), 1.6) * 1.4 + 0.25 * n(fx * freq * 4 + 9)],
211
+ ["pines", (n, fx, freq) => {
212
+ const cw = 1 / (freq * 4), ci = Math.floor(fx / cw), cf = (fx / cw) - ci;
213
+ const ch = 0.55 + 0.45 * n(ci * 0.77 + 3);
214
+ return 0.35 * n(fx * freq) + ch * (1 - Math.abs(cf - 0.5) * 2) * 0.9;
215
+ }],
216
+ ["bush", (n, fx, freq) => 0.5 * n(fx * freq) + 0.35 * Math.abs(Math.sin(fx * freq * 18 + n(fx * 30) * 3)) + 0.15 * n(fx * freq * 9)],
217
+ ["rocky", (n, fx, freq) => n(fx * freq) * 0.8 + 0.3 * Math.abs(n(fx * freq * 6) - 0.5) * 2],
218
+ ]);
219
+ const ridgeSoft: RidgeHeight = (n, fx, freq) => n(fx * freq) * 0.85 + 0.15 * n(fx * freq * 3 + 4);
220
+
221
+ /**
222
+ * A ridge filled down to the bottom: hills, mountains, a far treeline.
223
+ * shape: "soft" rolling, "peaks" mountains, "pines" a conifer line,
224
+ * "bush" a broadleaf line, "rocky".
225
+ */
226
+ function ridge(
227
+ { g, X, Y }: Ctx,
228
+ { y = 0.55, amp = 0.06, freq = 3, color = "#3d6b3a", shape = "soft", seed: s = 1, to = 1.2, x0 = -0.2, x1 = 1.2, top = null }: RidgeOpts = {},
229
+ ): void {
230
+ const n = noise1(hashStr("ridge" + s + shape));
231
+ if (top) {
232
+ const gr = g.createLinearGradient(0, Y(y - amp), 0, Y(y + amp * 2));
233
+ gr.addColorStop(0, top); gr.addColorStop(1, color);
234
+ g.fillStyle = gr;
235
+ } else g.fillStyle = color;
236
+ const heightAt = RIDGE_HEIGHT.get(shape) ?? ridgeSoft;
237
+ g.beginPath();
238
+ g.moveTo(X(x0), Y(to));
239
+ const steps = 220;
240
+ for (let i = 0; i <= steps; i++) {
241
+ const fx = x0 + ((x1 - x0) * i) / steps;
242
+ g.lineTo(X(fx), Y(y - amp * heightAt(n, fx, freq)));
243
+ }
244
+ g.lineTo(X(x1), Y(to));
245
+ g.closePath();
246
+ g.fill();
247
+ }
248
+
249
+ /** The ground plane from y down, with an optional gradient, mown stripes and flowers. */
250
+ function ground(
251
+ { g, W, H, u, X, Y }: Ctx,
252
+ { y = 0.62, color = "#5d9a3b", far = null, stripes = 0, stripe = null, flowers = 0, flower = "#f7f3e6", seed: s = 2 }: GroundOpts = {},
253
+ ): void {
254
+ if (far) {
255
+ const gr = g.createLinearGradient(0, Y(y), 0, H);
256
+ gr.addColorStop(0, far); gr.addColorStop(1, color);
257
+ g.fillStyle = gr;
258
+ } else g.fillStyle = color;
259
+ g.fillRect(-W, Y(y), W * 3, H * 2);
260
+ if (stripes) {
261
+ const rr = rng(hashStr("stripes" + s));
262
+ for (let i = 0; i < stripes; i++) {
263
+ const f = i / stripes;
264
+ const yy = y + (1 - y) * Math.pow(f, 1.6);
265
+ const hh = (1 - y) * (Math.pow((i + 1) / stripes, 1.6) - Math.pow(f, 1.6)) * 0.5;
266
+ g.fillStyle = alpha(stripe || mix(color, "#000000", 0.12), 0.5 + 0.3 * rr());
267
+ g.fillRect(-W, Y(yy), W * 3, Y(hh));
268
+ }
269
+ }
270
+ if (flowers) {
271
+ const rr = rng(hashStr("flowers" + s));
272
+ for (let i = 0; i < flowers; i++) {
273
+ const fy = y + (1 - y) * Math.pow(rr(), 0.7), sz = (2 + 7 * (fy - y) / (1 - y)) * u;
274
+ g.fillStyle = alpha(flower, 0.85);
275
+ g.beginPath(); g.arc(rr() * W, Y(fy), sz, 0, 7); g.fill();
276
+ }
277
+ }
278
+ }
279
+
280
+ /** A path or road narrowing to a vanishing point. */
281
+ function path(
282
+ { g, X, Y }: Ctx,
283
+ { x = 0.5, y = 0.62, near = 0.5, far = 0.04, color = "#b59b72", to = 1.02, bend = 0 }: PathOpts = {},
284
+ ): void {
285
+ g.fillStyle = color;
286
+ g.beginPath();
287
+ g.moveTo(X(x - far / 2), Y(y));
288
+ g.quadraticCurveTo(X(x - near / 4 + bend), Y((y + to) / 2), X(x - near / 2), Y(to));
289
+ g.lineTo(X(x + near / 2), Y(to));
290
+ g.quadraticCurveTo(X(x + near / 4 + bend), Y((y + to) / 2), X(x + far / 2), Y(y));
291
+ g.closePath(); g.fill();
292
+ }
293
+
294
+ /* ---------- water ---------- */
295
+
296
+ /** A lake or sea band, with sparkling horizontal glints that move. */
297
+ function water(
298
+ { g, W, u, t, Y }: Ctx,
299
+ { y0 = 0.45, y1 = 0.62, color = "#2f8fc9", deep = "#1d5f97", glint = "#e9f6ff", glints = 90, seed: s = 4, speed = 1 }: WaterOpts = {},
300
+ ): void {
301
+ const gr = g.createLinearGradient(0, Y(y0), 0, Y(y1));
302
+ gr.addColorStop(0, color); gr.addColorStop(1, deep);
303
+ g.fillStyle = gr; g.fillRect(-W, Y(y0), W * 3, Y(y1 - y0));
304
+ const rr = rng(hashStr("glint" + s));
305
+ for (let i = 0; i < glints; i++) {
306
+ const fy = rr(), yy = Y(y0 + (y1 - y0) * fy);
307
+ const ph = rr() * 6.28, sp = (0.6 + rr()) * speed;
308
+ const vis = 0.5 + 0.5 * Math.sin(t * 2.2 * sp + ph);
309
+ const len = (14 + 60 * rr()) * u * (0.4 + fy);
310
+ const xx = rr() * W + Math.sin(t * sp + ph) * 10 * u;
311
+ g.fillStyle = alpha(glint, 0.25 + 0.6 * vis);
312
+ g.fillRect(xx, yy, len, (2 + 3 * fy) * u);
313
+ }
314
+ }
315
+
316
+ /** A falling column of water, with a churn of foam at its foot. */
317
+ function waterfall(
318
+ { g, H, u, t, X, Y }: Ctx,
319
+ { x = 0.5, w = 0.2, y0 = 0.1, y1 = 0.7, color = "#e8f4f6", shade = "#a9cfd8", speed = 1 }: WaterfallOpts = {},
320
+ ): void {
321
+ g.fillStyle = shade; g.fillRect(X(x - w / 2), Y(y0), X(w), Y(y1 - y0));
322
+ const rr = rng(hashStr("fall" + x));
323
+ for (let i = 0; i < 26; i++) {
324
+ const lx = X(x - w / 2) + rr() * X(w), lw = (4 + rr() * 16) * u;
325
+ const off = ((t * 900 * speed * (0.7 + rr() * 0.6) * u + rr() * H) % (Y(y1 - y0)));
326
+ g.fillStyle = alpha(color, 0.75);
327
+ g.fillRect(lx, Y(y0), lw, Y(y1 - y0));
328
+ g.fillStyle = alpha("#ffffff", 0.6);
329
+ g.fillRect(lx, Y(y0) + off, lw, 40 * u);
330
+ }
331
+ for (let i = 0; i < 14; i++) {
332
+ g.fillStyle = alpha("#ffffff", 0.7);
333
+ g.beginPath();
334
+ g.ellipse(X(x - w / 2 - 0.03) + rr() * X(w + 0.06), Y(y1) + (rr() - 0.3) * 30 * u, (30 + rr() * 50) * u, (10 + rr() * 16) * u, 0, 0, 7);
335
+ g.fill();
336
+ }
337
+ }
338
+
339
+ /* ---------- plants ---------- */
340
+
341
+ /** Everything a tree kind needs: where it stands, its colours and its own rng. */
342
+ interface TreeGeo {
343
+ g: CanvasRenderingContext2D; u: number; rr: Rng; kind: string;
344
+ cx: number; base: number; top: number; th: number; tw: number;
345
+ trunk: string; crown: string; fruit: string | null;
346
+ }
347
+
348
+ // pine and column (cypress) are one shape with different width and tier count
349
+ function drawConifer({ g, kind, cx, base, top, th, tw, crown }: TreeGeo): void {
350
+ g.fillRect(cx - tw / 2, base, tw, th * 0.15);
351
+ g.fillStyle = crown;
352
+ const wide = kind === "column" ? 0.12 : 0.32;
353
+ g.beginPath();
354
+ g.moveTo(cx, top);
355
+ const tiers = kind === "column" ? 1 : 5;
356
+ for (let i = 1; i <= tiers; i++) {
357
+ const yy = top - th * (i / tiers) * 0.92, ww = -th * wide * (i / tiers);
358
+ g.lineTo(cx + ww, yy); g.lineTo(cx + ww * 0.45, yy - th * 0.04);
359
+ }
360
+ for (let i = tiers; i >= 1; i--) {
361
+ const yy = top - th * (i / tiers) * 0.92, ww = -th * wide * (i / tiers);
362
+ g.lineTo(cx - ww * 0.45, yy - th * 0.04); g.lineTo(cx - ww, yy);
363
+ }
364
+ g.closePath(); g.fill();
365
+ }
366
+
367
+ function drawBareTree({ g, u, rr, cx, base, th, tw, trunk }: TreeGeo): void {
368
+ g.strokeStyle = trunk; g.lineCap = "round";
369
+ const branch = (x0: number, y0: number, ang: number, len: number, wd: number, d: number): void => {
370
+ if (d > 5 || len < 6 * u) return;
371
+ const x1 = x0 + Math.cos(ang) * len, y1 = y0 + Math.sin(ang) * len;
372
+ g.lineWidth = wd; g.beginPath(); g.moveTo(x0, y0); g.lineTo(x1, y1); g.stroke();
373
+ branch(x1, y1, ang - 0.4 - rr() * 0.3, len * 0.72, wd * 0.68, d + 1);
374
+ branch(x1, y1, ang + 0.4 + rr() * 0.3, len * 0.72, wd * 0.68, d + 1);
375
+ };
376
+ branch(cx, base, -Math.PI / 2, -th * 0.35, tw, 0);
377
+ }
378
+
379
+ function drawPalm({ g, u, cx, base, top, th, tw, trunk, crown }: TreeGeo): void {
380
+ g.strokeStyle = trunk; g.lineWidth = tw; g.lineCap = "round";
381
+ g.beginPath(); g.moveTo(cx, base); g.quadraticCurveTo(cx + th * 0.1, base + th * 0.5, cx - th * 0.05, top); g.stroke();
382
+ g.strokeStyle = crown; g.lineWidth = 10 * u;
383
+ for (let i = 0; i < 8; i++) {
384
+ const a = (i / 8) * Math.PI * 2;
385
+ g.beginPath(); g.moveTo(cx - th * 0.05, top);
386
+ g.quadraticCurveTo(cx - th * 0.05 + Math.cos(a) * -th * 0.2, top + Math.sin(a) * -th * 0.06 + th * 0.04, cx - th * 0.05 + Math.cos(a) * -th * 0.32, top - th * 0.1);
387
+ g.stroke();
388
+ }
389
+ }
390
+
391
+ // round / autumn: a cluster of blobs
392
+ function drawRoundTree({ g, u, rr, kind, cx, top, th, crown, fruit }: TreeGeo): void {
393
+ g.fillStyle = crown;
394
+ const R = -th * 0.32;
395
+ const ccy = top + R;
396
+ for (let i = 0; i < 9; i++) {
397
+ const a = rr() * 6.28, d = rr() * R * 0.6;
398
+ g.beginPath(); g.arc(cx + Math.cos(a) * d, ccy + Math.sin(a) * d * 0.8, R * (0.45 + 0.3 * rr()), 0, 7); g.fill();
399
+ }
400
+ if (fruit || kind === "autumn") {
401
+ g.fillStyle = fruit || "#e0862f";
402
+ for (let i = 0; i < 40; i++) {
403
+ const a = rr() * 6.28, d = rr() * R;
404
+ g.beginPath(); g.arc(cx + Math.cos(a) * d, ccy + Math.sin(a) * d * 0.8, (3 + rr() * 5) * u, 0, 7); g.fill();
405
+ }
406
+ }
407
+ }
408
+
409
+ const TREE_DRAW = new Map<string, (t: TreeGeo) => void>([
410
+ ["pine", drawConifer], ["column", drawConifer], ["bare", drawBareTree], ["palm", drawPalm],
411
+ ]);
412
+
413
+ /** One tree. kind: "round", "pine", "autumn", "bare", "palm", "column" (cypress). */
414
+ function tree(
415
+ { g, u, X, Y }: Ctx,
416
+ { x = 0.5, y = 0.7, h = 0.3, kind = "round", crown = "#3f7d34", trunk = "#5b3b25", fruit = null, seed: s = 5 }: TreeOpts = {},
417
+ ): void {
418
+ const rr = rng(hashStr("tree" + s + x));
419
+ const base = Y(y), top = Y(y - h), cx = X(x), th = (top - base);
420
+ g.fillStyle = trunk;
421
+ const tw = Math.max(6 * u, -th * 0.05);
422
+ if (kind !== "pine" && kind !== "column") g.fillRect(cx - tw / 2, base + th * 0.55, tw, -th * 0.55);
423
+ (TREE_DRAW.get(kind) ?? drawRoundTree)({ g, u, rr, kind, cx, base, top, th, tw, trunk, crown, fruit });
424
+ }
425
+
426
+ /** A row of trees along a line, sized by depth. */
427
+ function trees(
428
+ c: Ctx,
429
+ { n = 6, y = 0.62, h = 0.18, x0 = 0, x1 = 1, kind = "round", crown = "#3f7d34", trunk = "#4a3020", jitter = 0.4, seed: s = 7 }: TreesOpts = {},
430
+ ): void {
431
+ const rr = rng(hashStr("trees" + s));
432
+ for (let i = 0; i < n; i++) {
433
+ const fx = x0 + ((x1 - x0) * (i + 0.5 + (rr() - 0.5) * jitter)) / n;
434
+ tree(c, { x: fx, y: y + (rr() - 0.5) * 0.01, h: h * (0.75 + rr() * 0.5), kind, crown: mix(crown, "#000000", rr() * 0.18), trunk, seed: Number(s) * 31 + i });
435
+ }
436
+ }
437
+
438
+ /** Foliage hanging into the frame from a side: a ragged, leafy mass. */
439
+ function foliage(
440
+ { g, W, u, X, Y }: Ctx,
441
+ { side = "left", x = null, y0 = -0.05, y1 = 0.7, depth = 0.22, color = "#2f6a2c", light = "#5c9a3c", seed: s = 8 }: FoliageOpts = {},
442
+ ): void {
443
+ const rr = rng(hashStr("fol" + s + side));
444
+ const edge = x ?? (side === "left" ? 0 : 1);
445
+ const dir = side === "left" ? 1 : -1;
446
+ const dark = mix(color, "#000000", 0.35);
447
+ // the mass, then clumps of small leaves in three values, lit from the top
448
+ g.fillStyle = dark;
449
+ g.fillRect(side === "left" ? X(-0.3) : X(edge), Y(y0), X(0.3) + (side === "left" ? X(edge) : W), Y(y1 - y0));
450
+ const clumps: [number, number, number][] = [];
451
+ for (let i = 0; i < 46; i++) {
452
+ const fy = y0 + (y1 - y0) * rr();
453
+ const reach = depth * (0.35 + 0.65 * Math.sin(Math.PI * (fy - y0) / (y1 - y0))) * (0.55 + 0.6 * rr());
454
+ clumps.push([X(edge + dir * reach * (0.55 + 0.45 * rr())), Y(fy), (30 + 50 * rr()) * u]);
455
+ }
456
+ const tones: [string, number, number][] = [[dark, 0, 30], [color, -0.25, 26], [light, -0.55, 12]];
457
+ for (const [tone, shift, n] of tones) {
458
+ g.fillStyle = tone;
459
+ for (const [cx, cy, R] of clumps) {
460
+ for (let j = 0; j < n; j++) {
461
+ const a = rr() * 6.28, d = Math.sqrt(rr()) * R;
462
+ const lx = cx + Math.cos(a) * d - dir * R * 0.2 * -shift, ly = cy + Math.sin(a) * d * 0.8 + R * shift;
463
+ const lr = (5 + rr() * 9) * u;
464
+ g.beginPath(); g.ellipse(lx, ly, lr * 1.5, lr, rr() * 3.14, 0, 7); g.fill();
465
+ }
466
+ }
467
+ }
468
+ }
469
+
470
+ /** Tall trunks rising out of frame: a forest seen from inside. */
471
+ function trunks(
472
+ { g, H, X, Y }: Ctx,
473
+ { n = 5, color = "#7a5534", light = "#a9784a", y1 = 1, seed: s = 9, x0 = 0, x1 = 1, width = 0.08 }: TrunksOpts = {},
474
+ ): void {
475
+ const rr = rng(hashStr("trunks" + s));
476
+ for (let i = 0; i < n; i++) {
477
+ const fx = x0 + ((x1 - x0) * (i + 0.2 + rr() * 0.6)) / n, w = X(width) * (0.6 + rr() * 0.7);
478
+ g.fillStyle = color; g.fillRect(X(fx) - w / 2, -H, w, Y(y1) + H);
479
+ g.fillStyle = light; g.fillRect(X(fx) - w / 2, -H, w * 0.28, Y(y1) + H);
480
+ }
481
+ }
482
+
483
+ /* ---------- built things ---------- */
484
+
485
+ function houseBody({ g, X, Y }: Ctx, o: Required<Pick<HouseOpts, "x" | "y" | "h" | "wall" | "roof" | "aFrame">>, L: number, R: number, T: number, B: number): void {
486
+ const { x, y, h, wall, roof, aFrame } = o;
487
+ g.fillStyle = wall;
488
+ if (aFrame) {
489
+ g.beginPath(); g.moveTo(L, B); g.lineTo(X(x), Y(y - h * 2)); g.lineTo(R, B); g.closePath(); g.fill();
490
+ } else g.fillRect(L, T, R - L, B - T);
491
+ g.fillStyle = roof;
492
+ g.beginPath();
493
+ if (aFrame) { g.moveTo(L - X(0.03), B); g.lineTo(X(x), Y(y - h * 2.15)); g.lineTo(R + X(0.03), B); g.lineTo(R, B); g.lineTo(X(x), Y(y - h * 1.95)); g.lineTo(L, B); }
494
+ else { g.moveTo(L - X(0.02), T); g.lineTo(X(x), T - Y(h * 0.5)); g.lineTo(R + X(0.02), T); }
495
+ g.closePath(); g.fill();
496
+ }
497
+
498
+ /** A house: box, pitched roof, windows (lit at night). */
499
+ function house(
500
+ c: Ctx,
501
+ { x = 0.5, y = 0.7, w = 0.3, h = 0.16, wall = "#efe6d8", roof = "#3b3a45", window: windowColor = "#3c3b46", lit = false, litColor = "#ffcf6b", floors = 2, aFrame = false }: HouseOpts = {},
502
+ ): void {
503
+ const { g, W, H, X, Y } = c;
504
+ const L = X(x - w / 2), R = X(x + w / 2), B = Y(y), T = Y(y - h);
505
+ houseBody(c, { x, y, h, wall, roof, aFrame }, L, R, T, B);
506
+ const cols = Math.max(2, Math.round(w / 0.07));
507
+ const rows = aFrame ? 1 : floors;
508
+ for (let ry = 0; ry < rows; ry++)
509
+ for (let cx = 0; cx < cols; cx++) {
510
+ const ww = ((R - L) / cols) * 0.42, wh = ((B - T) / rows) * 0.42;
511
+ const wx = L + ((R - L) * (cx + 0.5)) / cols - ww / 2, wy = (aFrame ? B - (B - T) * 0.7 : T + ((B - T) * (ry + 0.5)) / rows - wh / 2);
512
+ g.fillStyle = lit ? litColor : windowColor;
513
+ g.fillRect(wx, wy, ww, wh);
514
+ if (lit) glow(c, { x: (wx + ww / 2) / W, y: (wy + wh / 2) / H, r: 70, color: litColor, a: 0.35 });
515
+ }
516
+ }
517
+
518
+ /** A skyline of towers; at night their windows are lit dots. */
519
+ function city(
520
+ { g, W, H, u, t, X, Y }: Ctx,
521
+ { y = 0.62, h = 0.22, color = "#1b2740", lit = true, litColor = "#ffd66b", density = 0.35, n = 16, seed: s = 11, x0 = -0.05, x1 = 1.05 }: CityOpts = {},
522
+ ): void {
523
+ const rr = rng(hashStr("city" + s));
524
+ const bw = (x1 - x0) / n;
525
+ for (let i = 0; i < n; i++) {
526
+ const bx = x0 + i * bw + (rr() - 0.5) * bw * 0.4, bh = h * (0.35 + rr() * 0.75), ww = bw * (0.7 + rr() * 0.6);
527
+ g.fillStyle = mix(color, "#000000", rr() * 0.25);
528
+ g.fillRect(X(bx), Y(y - bh), X(ww), Y(bh) + 2);
529
+ if (!lit) continue;
530
+ const cols = Math.max(2, Math.round(ww * W / (14 * u))), rows = Math.round(bh * H / (18 * u));
531
+ for (let ry = 1; ry < rows; ry++)
532
+ for (let cx = 0; cx < cols; cx++) {
533
+ if (rr() > density) continue;
534
+ const on = Math.sin(t * 0.6 + i * 3 + ry * 7 + cx) > -0.85;
535
+ if (!on) continue;
536
+ g.fillStyle = alpha(litColor, 0.6 + rr() * 0.4);
537
+ g.fillRect(X(bx) + (cx + 0.25) * (X(ww) / cols), Y(y - bh) + ry * (Y(bh) / rows), (X(ww) / cols) * 0.45, (Y(bh) / rows) * 0.4);
538
+ }
539
+ }
540
+ }
541
+
542
+ /** A pagoda or tower of stacked roofs. */
543
+ function pagoda({ g, u, X, Y }: Ctx, { x = 0.5, y = 0.6, h = 0.3, color = "#3b2a22", tiers = 5 }: PagodaOpts = {}): void {
544
+ g.fillStyle = color;
545
+ for (let i = 0; i < tiers; i++) {
546
+ const f = i / tiers, w = X(0.16 * (1 - f * 0.6)), yy = Y(y - h * f);
547
+ g.fillRect(X(x) - w * 0.32, yy - Y(h / tiers), w * 0.64, Y(h / tiers));
548
+ g.beginPath(); g.moveTo(X(x) - w * 0.62, yy - Y(h / tiers) * 0.45); g.lineTo(X(x), yy - Y(h / tiers) * 1.05); g.lineTo(X(x) + w * 0.62, yy - Y(h / tiers) * 0.45); g.closePath(); g.fill();
549
+ }
550
+ g.fillRect(X(x) - 3 * u, Y(y - h) - Y(h * 0.18), 6 * u, Y(h * 0.18));
551
+ }
552
+
553
+ /** A utility pole with sagging wires running out of frame. */
554
+ function pole({ g, u, X, Y }: Ctx, { x = 0.75, y = 0.75, h = 0.6, color = "#2a2626", wires = 3 }: PoleOpts = {}): void {
555
+ g.fillStyle = color; g.fillRect(X(x) - 5 * u, Y(y - h), 10 * u, Y(h));
556
+ g.fillRect(X(x) - 40 * u, Y(y - h) + 30 * u, 80 * u, 7 * u);
557
+ g.strokeStyle = color; g.lineWidth = 2.5 * u;
558
+ for (let i = 0; i < wires; i++) {
559
+ const yy = Y(y - h) + 34 * u;
560
+ const ex = -0.1, ey = yy + (i - 1) * 30 * u - 60 * u;
561
+ g.beginPath(); g.moveTo(X(x) + (i - 1) * 35 * u, yy); g.quadraticCurveTo((X(x) + X(ex)) / 2, yy + 80 * u, X(ex), ey); g.stroke();
562
+ }
563
+ }
564
+
565
+ /** A wall, a doorway or a block: any flat rectangle with a lit edge. */
566
+ function block({ g, u, X, Y }: Ctx, { x0, y0, x1, y1, color = "#283a5a", edge = null, edgeWidth = 4 }: BlockOpts): void {
567
+ g.fillStyle = color; g.fillRect(X(x0), Y(y0), X(x1 - x0), Y(y1 - y0));
568
+ if (edge) { g.fillStyle = edge; g.fillRect(X(x0), Y(y0), edgeWidth * u, Y(y1 - y0)); }
569
+ }
570
+
571
+ /** A lamp post with its pool of light. */
572
+ function lamp(c: Ctx, { x = 0.5, y = 0.8, h = 0.4, color = "#1d1d24", light = "#fff1c2", pool = true }: LampOpts = {}): void {
573
+ const { g, u, X, Y } = c;
574
+ g.fillStyle = color; g.fillRect(X(x) - 4 * u, Y(y - h), 8 * u, Y(h));
575
+ sun(c, { x, y: y - h, r: 16, color: light, glow: light, glowSize: 9, glowAlpha: 0.4 });
576
+ if (pool) {
577
+ g.fillStyle = alpha(light, 0.16);
578
+ g.beginPath(); g.ellipse(X(x), Y(y), 220 * u, 40 * u, 0, 0, 7); g.fill();
579
+ }
580
+ }
581
+
582
+ /** Any closed shape from points [[x,y],...] (fractions); smooth: true rounds it. */
583
+ function shape({ g, X, Y }: Ctx, { pts, color = "#000", smooth = false }: ShapeOpts): void {
584
+ g.fillStyle = color; g.beginPath();
585
+ if (!smooth) {
586
+ pts.forEach(([x, y], i) => {
587
+ if (i) g.lineTo(X(x), Y(y));
588
+ else g.moveTo(X(x), Y(y));
589
+ });
590
+ } else {
591
+ const P = pts.map(([x, y]) => [X(x), Y(y)]);
592
+ g.moveTo((P[0][0] + P[1][0]) / 2, (P[0][1] + P[1][1]) / 2);
593
+ for (let i = 1; i <= P.length; i++) {
594
+ const a = P[i % P.length], b = P[(i + 1) % P.length];
595
+ g.quadraticCurveTo(a[0], a[1], (a[0] + b[0]) / 2, (a[1] + b[1]) / 2);
596
+ }
597
+ }
598
+ g.closePath(); g.fill();
599
+ }
600
+
601
+ /** A heart (a pond, a sign, a balloon). */
602
+ function heart({ g, X, Y }: Ctx, { x = 0.5, y = 0.5, s: size = 0.2, color = "#7ec8ef" }: HeartOpts = {}): void {
603
+ const S = X(size), cx = X(x), cy = Y(y);
604
+ g.fillStyle = color; g.beginPath();
605
+ g.moveTo(cx, cy + S * 0.35);
606
+ g.bezierCurveTo(cx - S * 0.9, cy - S * 0.1, cx - S * 0.45, cy - S * 0.62, cx, cy - S * 0.25);
607
+ g.bezierCurveTo(cx + S * 0.45, cy - S * 0.62, cx + S * 0.9, cy - S * 0.1, cx, cy + S * 0.35);
608
+ g.fill();
609
+ }
610
+
611
+ /* ---------- people ---------- */
612
+
613
+ /** The figure being drawn: its context (maybe the rim layer), its unit U (a head) and its joints. */
614
+ interface Fig {
615
+ g: CanvasRenderingContext2D;
616
+ c: (col: string) => string; // silhouette overrides every colour
617
+ U: number; bw: number; t: number; ph: number;
618
+ pose: string; facing: string; hair: string; robe: boolean; silhouette: string | null;
619
+ skin: string; hairColor: string; top: string; bottom: string; shoes: string;
620
+ hipY: number; shY: number; neckY: number;
621
+ }
622
+
623
+ // a tapered limb through three points
624
+ function limb(g: CanvasRenderingContext2D, pts: Pt[], w0: number, w1: number, col: string): void {
625
+ g.strokeStyle = col;
626
+ for (let i = 0; i < pts.length - 1; i++) {
627
+ g.lineWidth = w0 + (w1 - w0) * (i / Math.max(1, pts.length - 2));
628
+ g.beginPath(); g.moveTo(...pts[i]); g.lineTo(...pts[i + 1]); g.stroke();
629
+ }
630
+ }
631
+
632
+ /** Hip, knee and foot of one leg for a pose; s is -1 (left) or 1 (right). */
633
+ type LegPose = (s: number, hx: number, hy: number, U: number, ph: number) => [Pt, Pt];
634
+ const THIGH = 1.75, SHIN = 1.75;
635
+ const walkAngles = (s: number, ph: number): Pt => {
636
+ const q = ph + (s > 0 ? Math.PI : 0);
637
+ return [Math.sin(q) * 0.36, Math.max(0, -Math.cos(q)) * 0.55];
638
+ };
639
+ const standLeg: LegPose = (s, hx, hy, U, ph) => {
640
+ const thigh = U * THIGH, shin = U * SHIN;
641
+ const [a, bend] = walkAngles(s, ph);
642
+ const kx = hx + Math.sin(a) * thigh, ky = hy + Math.cos(a) * thigh;
643
+ return [[kx, ky], [kx + Math.sin(a - bend) * shin, ky + Math.cos(a - bend) * shin]];
644
+ };
645
+ const idleLeg: LegPose = (_s, hx, hy, U) => {
646
+ const ky = hy + U * THIGH;
647
+ return [[hx, ky], [hx, ky + U * SHIN]];
648
+ };
649
+ const sitLeg: LegPose = (_s, hx, hy, U) => {
650
+ const kx = hx + U * 1.7, ky = hy + U * 0.1;
651
+ return [[kx, ky], [kx + U * 0.15, ky + U * SHIN]];
652
+ };
653
+ const kneelLeg: LegPose = (s, hx, hy, U) => {
654
+ if (s < 0) {
655
+ const kx = hx - U * 0.1, ky = hy + U * THIGH * 0.95;
656
+ return [[kx, ky], [kx - U * 1.7, ky + U * 0.05]];
657
+ }
658
+ const kx = hx + U * 1.2, ky = hy + U * 1.0;
659
+ return [[kx, ky], [kx, 0]];
660
+ };
661
+ const LEG_POSES = new Map<string, LegPose>([["sit", sitLeg], ["kneel", kneelLeg], ["walk", standLeg]]);
662
+
663
+ function legs(f: Fig): void {
664
+ const { g, c, U, bw, ph, pose, facing, robe, bottom, shoes, hipY } = f;
665
+ if (robe && pose !== "walk" && pose !== "sit" && pose !== "kneel") return;
666
+ const legPose = LEG_POSES.get(pose) ?? idleLeg;
667
+ for (const s of [-1, 1]) {
668
+ const hx = s * U * 0.42 * bw, hy = hipY + U * 0.2;
669
+ const [[kx, ky], [fx, fy]] = legPose(s, hx, hy, U, ph);
670
+ limb(g, [[hx, hy], [kx, ky], [fx, fy - U * 0.1]], U * 0.78 * bw, U * 0.56 * bw, c(bottom));
671
+ g.fillStyle = c(shoes);
672
+ g.beginPath(); g.ellipse(fx + (facing === "back" ? 0 : U * 0.08), fy - U * 0.1, U * 0.36, U * 0.2, 0, 0, 7); g.fill();
673
+ }
674
+ }
675
+
676
+ // torso: rounded shoulders, a waist, the hips; a robe falls to the ankles
677
+ function torso(f: Fig): void {
678
+ const { g, c, U, bw, robe, silhouette, top, bottom, hipY, shY, neckY } = f;
679
+ const sw = U * 1.05 * bw, ww = U * 0.78 * bw, hw = U * 0.86 * bw;
680
+ g.fillStyle = c(top);
681
+ g.beginPath();
682
+ g.moveTo(-U * 0.3, neckY + U * 0.1);
683
+ g.quadraticCurveTo(-sw, shY - U * 0.05, -sw, shY + U * 0.55);
684
+ if (robe) {
685
+ g.quadraticCurveTo(-ww, hipY, -hw * 1.35, -U * 0.15);
686
+ g.lineTo(hw * 1.35, -U * 0.15);
687
+ g.quadraticCurveTo(ww, hipY, sw, shY + U * 0.55);
688
+ } else {
689
+ g.quadraticCurveTo(-ww * 0.98, hipY - U * 0.9, -hw, hipY + U * 0.45);
690
+ g.lineTo(hw, hipY + U * 0.45);
691
+ g.quadraticCurveTo(ww * 0.98, hipY - U * 0.9, sw, shY + U * 0.55);
692
+ }
693
+ g.quadraticCurveTo(sw, shY - U * 0.05, U * 0.3, neckY + U * 0.1);
694
+ g.closePath(); g.fill();
695
+ if (!robe && !silhouette) { g.fillStyle = bottom; g.fillRect(-hw, hipY + U * 0.05, hw * 2, U * 0.45); }
696
+ }
697
+
698
+ // arms: [upper angle, forearm bend] in radians from hanging straight down, + = away from the body
699
+ type ArmSet = [Pt, Pt];
700
+ const ARM_POSES = new Map<string, ArmSet>([
701
+ ["stand", [[0.1, 0.05], [0.1, 0.05]]], ["cheer", [[2.75, 0.15], [2.75, 0.15]]], ["open", [[1.45, 0.08], [1.45, 0.08]]],
702
+ ["point", [[0.1, 0.05], [1.5, 0]]], ["reach", [[0.1, 0.05], [2.9, 0.05]]],
703
+ ["pray", [[0.25, -1.9], [0.25, -1.9]]], ["hug", [[0.7, -1.6], [0.7, -1.6]]], ["sit", [[0.25, -0.9], [0.25, -0.9]]],
704
+ ["kneel", [[0.15, -0.5], [0.15, -0.5]]], ["hold", [[0.12, -0.5], [0.12, -0.5]]],
705
+ ]);
706
+ const standArms: ArmSet = [[0.1, 0.05], [0.1, 0.05]];
707
+ const ARM_ANIMATED = new Map<string, (t: number, ph: number) => ArmSet>([
708
+ ["walk", (_t, ph) => { const sw = Math.sin(ph) * 0.38; return [[0.06 + sw, -0.15], [0.06 - sw, -0.15]]; }],
709
+ ["wave", (t) => [[0.1, 0.05], [2.4, 0.55 + Math.sin(t * 8) * 0.35]]],
710
+ ]);
711
+
712
+ function arms(f: Fig): void {
713
+ const { g, c, U, bw, t, ph, pose, skin, top, shY } = f;
714
+ const upper = U * 1.5, fore = U * 1.4;
715
+ const animated = ARM_ANIMATED.get(pose);
716
+ const set = animated ? animated(t, ph) : ARM_POSES.get(pose) ?? standArms;
717
+ [-1, 1].forEach((s, i) => {
718
+ const [a1, a2] = set[i];
719
+ const sx = s * U * 0.88 * bw, sy = shY + U * 0.45;
720
+ const ex = sx + s * Math.sin(a1) * upper, ey = sy + Math.cos(a1) * upper;
721
+ const a = a1 + a2;
722
+ const hx = ex + s * Math.sin(a) * fore, hy = ey + Math.cos(a) * fore;
723
+ limb(g, [[sx, sy], [ex, ey], [hx, hy]], U * 0.5 * bw, U * 0.4 * bw, c(top));
724
+ g.fillStyle = c(skin);
725
+ g.beginPath(); g.arc(hx, hy, U * 0.22, 0, 7); g.fill();
726
+ });
727
+ }
728
+
729
+ /** What a hair style needs: the head centre height and radius, and whether we see the back. */
730
+ interface HeadGeo { f: Fig; hy: number; hr: number; back: boolean }
731
+ type HairStyle = (h: HeadGeo) => void;
732
+ const PI = Math.PI;
733
+
734
+ const curlyHair = (big: number): HairStyle => ({ f: { g }, hy, hr }) => {
735
+ for (let i = 0; i < 11; i++) {
736
+ const a = PI * 0.9 + (i / 10) * PI * 1.2;
737
+ g.beginPath(); g.arc(Math.cos(a) * hr * 0.82 * big, hy - hr * 0.15 + Math.sin(a) * hr * 0.8 * big, hr * 0.36 * big, 0, 7); g.fill();
738
+ }
739
+ };
740
+ const HAIR_STYLES = new Map<string, HairStyle>([
741
+ ["short", ({ f: { g }, hy, hr }) => { g.beginPath(); g.ellipse(0, hy - hr * 0.25, hr * 0.92, hr * 0.8, 0, PI * 1.02, -0.02); g.fill(); }],
742
+ ["long", ({ f: { g }, hy, hr }) => { g.beginPath(); g.ellipse(0, hy - hr * 0.2, hr * 0.98, hr * 0.88, 0, PI, 0); g.fill(); }],
743
+ ["curly", curlyHair(1)],
744
+ ["afro", curlyHair(1.35)],
745
+ ["bun", ({ f: { g }, hy, hr }) => {
746
+ g.beginPath(); g.ellipse(0, hy - hr * 0.25, hr * 0.92, hr * 0.78, 0, PI, 0); g.fill();
747
+ g.beginPath(); g.arc(0, hy - hr * 1.05, hr * 0.4, 0, 7); g.fill();
748
+ }],
749
+ ["hood", ({ f: { g, c, top, skin }, hy, hr, back }) => {
750
+ g.fillStyle = c(top); g.beginPath(); g.ellipse(0, hy, hr * 1.1, hr * 1.15, 0, 0, 7); g.fill();
751
+ g.fillStyle = c(back ? top : skin); g.beginPath(); g.ellipse(0, hy + hr * 0.12, hr * 0.68, hr * 0.78, 0, 0, 7); g.fill();
752
+ }],
753
+ ["hat", ({ f: { g }, hy, hr }) => {
754
+ g.beginPath(); g.moveTo(-hr * 2.1, hy - hr * 0.3); g.lineTo(0, hy - hr * 1.55); g.lineTo(hr * 2.1, hy - hr * 0.3); g.closePath(); g.fill();
755
+ }],
756
+ ["cap", ({ f: { g }, hy, hr }) => {
757
+ g.beginPath(); g.ellipse(0, hy - hr * 0.3, hr * 0.95, hr * 0.75, 0, PI, 0); g.fill();
758
+ g.fillRect(-hr * 0.2, hy - hr * 0.35, hr * 1.4, hr * 0.18);
759
+ }],
760
+ ]);
761
+
762
+ function head(f: Fig): void {
763
+ const { g, c, U, facing, hair, skin, hairColor, neckY } = f;
764
+ g.fillStyle = c(skin);
765
+ g.fillRect(-U * 0.2, neckY - U * 0.15, U * 0.4, U * 0.4);
766
+ const hy = neckY - U * 0.62, hr = U * 0.58;
767
+ const back = facing === "back";
768
+ if (hair === "long") { g.fillStyle = c(hairColor); g.beginPath(); g.ellipse(0, hy + U * 0.55, hr * 1.02, U * 1.35, 0, 0, 7); g.fill(); }
769
+ g.fillStyle = c(back && hair !== "bald" ? hairColor : skin);
770
+ g.beginPath(); g.ellipse(0, hy, hr * 0.86, hr, 0, 0, 7); g.fill();
771
+ g.fillStyle = c(hairColor);
772
+ HAIR_STYLES.get(hair)?.({ f, hy, hr, back });
773
+ }
774
+
775
+ /** Where the figure's drawing goes: the frame itself, or a scratch layer when it has a rim light. */
776
+ function figureLayer(target: CanvasRenderingContext2D, layer: HTMLCanvasElement): CanvasRenderingContext2D {
777
+ layer.width = target.canvas.width; layer.height = target.canvas.height;
778
+ const lg = layer.getContext("2d");
779
+ if (!lg) throw new Error("kit: 2D canvas is not available for the rim layer");
780
+ lg.setTransform(target.getTransform());
781
+ return lg;
782
+ }
783
+
784
+ /** The rim: the figure's own outline, lit, then the figure over it. */
785
+ function drawRim(target: CanvasRenderingContext2D, layer: HTMLCanvasElement, rim: string, u: number): void {
786
+ target.save();
787
+ target.setTransform(1, 0, 0, 1, 0, 0);
788
+ target.shadowColor = rim; target.shadowBlur = 7 * u;
789
+ for (const [ox, oy] of [[-2.5, 0], [2.5, 0], [0, -2.5]]) { target.shadowOffsetX = ox * u; target.shadowOffsetY = oy * u; target.drawImage(layer, 0, 0); }
790
+ target.shadowColor = "transparent";
791
+ target.drawImage(layer, 0, 0);
792
+ target.restore();
793
+ }
794
+
795
+ /**
796
+ * A person, flat and readable, head to feet h tall (fraction of frame),
797
+ * feet at (x, y).
798
+ * pose: "stand" | "walk" | "cheer" (arms up) | "open" (arms out) | "sit" |
799
+ * "wave" | "point" | "kneel" | "pray" | "reach" (one arm up to the sky) | "hug"
800
+ * facing: "front" | "back" (no face drawn; the back of the head)
801
+ * hair: "short" | "long" | "curly" | "bun" | "afro" | "bald" | "hood" | "hat"
802
+ * skin, hairColor, top, bottom, shoes: colours
803
+ * robe: true draws a long garment to the ankles (in `top`)
804
+ * rim: a colour for a thin light around the figure (night scenes)
805
+ * silhouette: one colour for everything (backlit)
806
+ * walk: steps per second when pose is "walk"; dx moves the figure over the shot
807
+ */
808
+ function person(c: Ctx, scratch: { layer: HTMLCanvasElement | null }, o: PersonOpts = {}): void {
809
+ const {
810
+ x = 0.5, y = 0.85, h = 0.3, pose = "stand", facing = "front", hair = "short",
811
+ skin = "#b9825a", hairColor = "#2a1d16", top = "#c9a27e", bottom = "#2c2a35", shoes = "#1d1a1a",
812
+ robe = false, rim = null, silhouette = null, walk = 1.5, dx = 0, scale = 1, flip = false, build = 1,
813
+ } = o;
814
+ const { g: target, u, t, p, X, Y } = c;
815
+ const layer = rim ? (scratch.layer ?? (scratch.layer = document.createElement("canvas"))) : null;
816
+ const g = layer ? figureLayer(target, layer) : target;
817
+ const U = (Y(h) * scale) / 7.4; // ~7.4 heads tall, measured in heads
818
+ g.save();
819
+ g.translate(X(x + dx * p), Y(y));
820
+ if (flip) g.scale(-1, 1);
821
+ g.lineCap = "round"; g.lineJoin = "round";
822
+ const lift = pose === "kneel" ? U * 1.6 : pose === "sit" ? U * 1.9 : 0; // how far the hips drop
823
+ const f: Fig = {
824
+ g, c: (col) => silhouette || col, U, bw: build, t, ph: t * walk * Math.PI * 2,
825
+ pose, facing, hair, robe, silhouette, skin, hairColor, top, bottom, shoes,
826
+ hipY: -U * 3.55 + lift, shY: -U * 6.05 + lift, neckY: -U * 6.3 + lift,
827
+ };
828
+ // back-facing: arms behind the torso read wrong, so they go first only when facing away is false
829
+ legs(f);
830
+ if (facing === "back") { arms(f); torso(f); } else { torso(f); arms(f); }
831
+ head(f);
832
+ g.restore();
833
+ if (layer && rim) drawRim(target, layer, rim, u);
834
+ }
835
+
836
+ /** Builds the kit for one drawing of one shot. */
837
+ function make(g: CanvasRenderingContext2D, W: number, H: number, t: number, p: number, seed: number, pal: Palette | undefined, beat: Beat): Kit {
838
+ const u = H / 1920;
839
+ const X = (x: number) => x * W, Y = (y: number) => y * H;
840
+ const c: Ctx = { g, W, H, u, t, p, X, Y };
841
+ const scratch: { layer: HTMLCanvasElement | null } = { layer: null }; // the rim layer, made once per drawing
842
+ return {
843
+ g, W, H, u, t, p, r: rng(seed), pal: pal || {}, beat, mix, alpha, rng, noise1, X, Y,
844
+ sky: (o) => sky(c, o), clouds: (o) => clouds(c, o), sun: (o) => sun(c, o), glow: (o) => glow(c, o),
845
+ stars: (o) => stars(c, o), rays: (o) => rays(c, o), rain: (o) => rain(c, o), motes: (o) => motes(c, o),
846
+ haze: (o) => haze(c, o), ridge: (o) => ridge(c, o), ground: (o) => ground(c, o), path: (o) => path(c, o),
847
+ water: (o) => water(c, o), waterfall: (o) => waterfall(c, o), tree: (o) => tree(c, o), trees: (o) => trees(c, o),
848
+ foliage: (o) => foliage(c, o), trunks: (o) => trunks(c, o), house: (o) => house(c, o), city: (o) => city(c, o),
849
+ pagoda: (o) => pagoda(c, o), pole: (o) => pole(c, o), block: (o) => block(c, o), lamp: (o) => lamp(c, o),
850
+ shape: (o) => shape(c, o), heart: (o) => heart(c, o), person: (o) => person(c, scratch, o),
851
+ people: (list) => list.forEach((o) => person(c, scratch, o)),
852
+ };
853
+ }
854
+
855
+ window.ShortsKit = { make, rng, hashStr, mix, alpha };
856
+ })();