@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.
- package/.github/copilot-instructions.md +2 -0
- package/.github/prompts/carousel-maker.prompt.md +102 -0
- package/.github/prompts/shorts-animator.prompt.md +103 -0
- package/.kiro/steering/carousel-maker.md +101 -0
- package/.kiro/steering/shorts-animator.md +102 -0
- package/.windsurf/rules/carousel-maker.md +100 -0
- package/.windsurf/rules/shorts-animator.md +101 -0
- package/README.md +4 -2
- package/adapters/claude/olko-creative/skills/carousel-maker/SKILL.md +112 -0
- package/adapters/claude/olko-creative/skills/carousel-maker/scripts/cm.ts +220 -0
- package/adapters/claude/olko-creative/skills/carousel-maker/scripts/domain/check.ts +68 -0
- package/adapters/claude/olko-creative/skills/carousel-maker/scripts/domain/layout.ts +75 -0
- package/adapters/claude/olko-creative/skills/carousel-maker/scripts/domain/schema.ts +54 -0
- package/adapters/claude/olko-creative/skills/carousel-maker/scripts/domain/types.ts +33 -0
- package/adapters/claude/olko-creative/skills/carousel-maker/scripts/edge/io.ts +87 -0
- package/adapters/claude/olko-creative/skills/carousel-maker/scripts/package.json +4 -0
- package/adapters/claude/olko-creative/skills/carousel-maker/scripts/test/domain.test.ts +99 -0
- package/adapters/claude/olko-creative/skills/carousel-maker/scripts/tsconfig.json +16 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/SKILL.md +113 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/references/kit.md +59 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/references/looks.md +32 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/references/writing.md +9 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/audio/compose.ts +290 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/audio/dsp.ts +193 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/audio/instruments.ts +185 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/audio/master.ts +55 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/audio/soundtrack.ts +119 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/domain/captions.ts +35 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/domain/check.ts +96 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/domain/look.ts +26 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/domain/patch.ts +38 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/domain/schema.ts +81 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/domain/timeline.ts +75 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/domain/types.ts +91 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/edge/chrome.ts +169 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/edge/doctor.ts +52 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/edge/media.ts +105 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/edge/page.ts +53 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/edge/probe.ts +75 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/edge/sound.ts +46 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/edge/voice.ts +183 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/looks/gouache.json +7 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/looks/ink.json +6 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/looks/oil.json +7 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/looks/pastel.json +7 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/looks/watercolor.json +6 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/package.json +4 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/runtime/globals.d.ts +199 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/runtime/kit.ts +856 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/runtime/paint.ts +268 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/runtime/stage.ts +181 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/runtime/tsconfig.json +12 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/sa.ts +262 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/test/domain.test.ts +107 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/tsconfig.json +16 -0
- package/adapters/claude/olko-creative/skills/shorts-animator/scripts/tts.py +20 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/SKILL.md +30 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/context-gathering.md +3 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/finding-state.mjs +207 -0
- package/adapters/codex/olko-creative/README.md +2 -0
- package/adapters/cursor/olko-creative/skills/carousel-maker/SKILL.md +112 -0
- package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/cm.ts +220 -0
- package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/domain/check.ts +68 -0
- package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/domain/layout.ts +75 -0
- package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/domain/schema.ts +54 -0
- package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/domain/types.ts +33 -0
- package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/edge/io.ts +87 -0
- package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/package.json +4 -0
- package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/test/domain.test.ts +99 -0
- package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/tsconfig.json +16 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/SKILL.md +113 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/references/kit.md +59 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/references/looks.md +32 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/references/writing.md +9 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/audio/compose.ts +290 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/audio/dsp.ts +193 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/audio/instruments.ts +185 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/audio/master.ts +55 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/audio/soundtrack.ts +119 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/domain/captions.ts +35 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/domain/check.ts +96 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/domain/look.ts +26 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/domain/patch.ts +38 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/domain/schema.ts +81 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/domain/timeline.ts +75 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/domain/types.ts +91 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/edge/chrome.ts +169 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/edge/doctor.ts +52 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/edge/media.ts +105 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/edge/page.ts +53 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/edge/probe.ts +75 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/edge/sound.ts +46 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/edge/voice.ts +183 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/looks/gouache.json +7 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/looks/ink.json +6 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/looks/oil.json +7 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/looks/pastel.json +7 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/looks/watercolor.json +6 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/package.json +4 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/runtime/globals.d.ts +199 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/runtime/kit.ts +856 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/runtime/paint.ts +268 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/runtime/stage.ts +181 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/runtime/tsconfig.json +12 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/sa.ts +262 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/test/domain.test.ts +107 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/tsconfig.json +16 -0
- package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/tts.py +20 -0
- package/adapters/grok/olko-creative/skills/carousel-maker/SKILL.md +112 -0
- package/adapters/grok/olko-creative/skills/carousel-maker/scripts/cm.ts +220 -0
- package/adapters/grok/olko-creative/skills/carousel-maker/scripts/domain/check.ts +68 -0
- package/adapters/grok/olko-creative/skills/carousel-maker/scripts/domain/layout.ts +75 -0
- package/adapters/grok/olko-creative/skills/carousel-maker/scripts/domain/schema.ts +54 -0
- package/adapters/grok/olko-creative/skills/carousel-maker/scripts/domain/types.ts +33 -0
- package/adapters/grok/olko-creative/skills/carousel-maker/scripts/edge/io.ts +87 -0
- package/adapters/grok/olko-creative/skills/carousel-maker/scripts/package.json +4 -0
- package/adapters/grok/olko-creative/skills/carousel-maker/scripts/test/domain.test.ts +99 -0
- package/adapters/grok/olko-creative/skills/carousel-maker/scripts/tsconfig.json +16 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/SKILL.md +113 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/references/kit.md +59 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/references/looks.md +32 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/references/writing.md +9 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/audio/compose.ts +290 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/audio/dsp.ts +193 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/audio/instruments.ts +185 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/audio/master.ts +55 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/audio/soundtrack.ts +119 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/domain/captions.ts +35 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/domain/check.ts +96 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/domain/look.ts +26 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/domain/patch.ts +38 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/domain/schema.ts +81 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/domain/timeline.ts +75 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/domain/types.ts +91 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/edge/chrome.ts +169 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/edge/doctor.ts +52 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/edge/media.ts +105 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/edge/page.ts +53 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/edge/probe.ts +75 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/edge/sound.ts +46 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/edge/voice.ts +183 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/looks/gouache.json +7 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/looks/ink.json +6 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/looks/oil.json +7 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/looks/pastel.json +7 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/looks/watercolor.json +6 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/package.json +4 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/runtime/globals.d.ts +199 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/runtime/kit.ts +856 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/runtime/paint.ts +268 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/runtime/stage.ts +181 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/runtime/tsconfig.json +12 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/sa.ts +262 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/test/domain.test.ts +107 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/tsconfig.json +16 -0
- package/adapters/grok/olko-creative/skills/shorts-animator/scripts/tts.py +20 -0
- package/adapters/hermes/olko-creative/README.md +2 -0
- package/catalog/skills.json +46 -0
- package/package.json +1 -1
- package/plugins/olko-apple-kit/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-creative/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-creative/skills/carousel-maker/SKILL.md +111 -0
- package/plugins/olko-creative/skills/carousel-maker/scripts/cm.ts +220 -0
- package/plugins/olko-creative/skills/carousel-maker/scripts/domain/check.ts +68 -0
- package/plugins/olko-creative/skills/carousel-maker/scripts/domain/layout.ts +75 -0
- package/plugins/olko-creative/skills/carousel-maker/scripts/domain/schema.ts +54 -0
- package/plugins/olko-creative/skills/carousel-maker/scripts/domain/types.ts +33 -0
- package/plugins/olko-creative/skills/carousel-maker/scripts/edge/io.ts +87 -0
- package/plugins/olko-creative/skills/carousel-maker/scripts/package.json +4 -0
- package/plugins/olko-creative/skills/carousel-maker/scripts/test/domain.test.ts +99 -0
- package/plugins/olko-creative/skills/carousel-maker/scripts/tsconfig.json +16 -0
- package/plugins/olko-creative/skills/shorts-animator/SKILL.md +112 -0
- package/plugins/olko-creative/skills/shorts-animator/references/kit.md +59 -0
- package/plugins/olko-creative/skills/shorts-animator/references/looks.md +32 -0
- package/plugins/olko-creative/skills/shorts-animator/references/writing.md +9 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/audio/compose.ts +290 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/audio/dsp.ts +193 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/audio/instruments.ts +185 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/audio/master.ts +55 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/audio/soundtrack.ts +119 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/domain/captions.ts +35 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/domain/check.ts +96 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/domain/look.ts +26 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/domain/patch.ts +38 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/domain/schema.ts +81 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/domain/timeline.ts +75 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/domain/types.ts +91 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/edge/chrome.ts +169 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/edge/doctor.ts +52 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/edge/media.ts +105 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/edge/page.ts +53 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/edge/probe.ts +75 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/edge/sound.ts +46 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/edge/voice.ts +183 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/looks/gouache.json +7 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/looks/ink.json +6 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/looks/oil.json +7 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/looks/pastel.json +7 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/looks/watercolor.json +6 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/package.json +4 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/runtime/globals.d.ts +199 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/runtime/kit.ts +856 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/runtime/paint.ts +268 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/runtime/stage.ts +181 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/runtime/tsconfig.json +12 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/sa.ts +262 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/test/domain.test.ts +107 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/tsconfig.json +16 -0
- package/plugins/olko-creative/skills/shorts-animator/scripts/tts.py +20 -0
- package/plugins/olko-garmin-kit/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-git-tools/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-github-pr/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-github-pr/skills/lekker-review/SKILL.md +30 -0
- package/plugins/olko-github-pr/skills/lekker-review/references/context-gathering.md +3 -0
- package/plugins/olko-github-pr/skills/lekker-review/scripts/finding-state.mjs +207 -0
- package/plugins/olko-github-pr/skills/lekker-review/workflow.js +46 -3
- package/plugins/olko-obsidian/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-product/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-reflection/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-release/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-skill-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-web-ops/.claude-plugin/plugin.json +1 -1
- 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
|
+
})();
|