@olegkoval/agent-skills 1.59.3 → 1.61.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/self-critique.prompt.md +167 -43
- 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 +5 -3
- 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-reflection/skills/self-critique/SKILL.md +167 -43
- package/adapters/claude/olko-reflection/skills/self-critique/scripts/claim-ledger.mjs +159 -0
- package/adapters/claude/olko-reflection/skills/self-critique/scripts/rule-store.mjs +193 -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/cursor/olko-reflection/skills/self-critique/SKILL.md +167 -43
- package/adapters/cursor/olko-reflection/skills/self-critique/scripts/claim-ledger.mjs +159 -0
- package/adapters/cursor/olko-reflection/skills/self-critique/scripts/rule-store.mjs +193 -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/grok/olko-reflection/skills/self-critique/SKILL.md +167 -43
- package/adapters/grok/olko-reflection/skills/self-critique/scripts/claim-ledger.mjs +159 -0
- package/adapters/grok/olko-reflection/skills/self-critique/scripts/rule-store.mjs +193 -0
- package/adapters/hermes/olko-creative/README.md +2 -0
- package/catalog/skills.json +47 -1
- package/docs/superpowers/plans/2026-10-27-self-critique-v2.md +306 -0
- package/docs/superpowers/specs/2026-10-27-self-critique-v2-spec.md +104 -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-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-reflection/skills/self-critique/SKILL.md +167 -43
- package/plugins/olko-reflection/skills/self-critique/scripts/claim-ledger.mjs +159 -0
- package/plugins/olko-reflection/skills/self-critique/scripts/rule-store.mjs +193 -0
- 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,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2023",
|
|
4
|
+
"lib": ["es2023"],
|
|
5
|
+
"module": "nodenext",
|
|
6
|
+
"moduleResolution": "nodenext",
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"allowImportingTsExtensions": true,
|
|
11
|
+
"erasableSyntaxOnly": true,
|
|
12
|
+
"verbatimModuleSyntax": true,
|
|
13
|
+
"skipLibCheck": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["*.ts", "domain", "edge", "test"]
|
|
16
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: shorts-animator
|
|
3
|
+
description: Use when someone wants a narrated, painted or hand-drawn animated short (TikTok, Reels, YouTube Shorts) from a script or topic and a look described in words. Every frame is drawn in code and GPU-painted, voiced locally, captioned and scored into an MP4; no image models or stock.
|
|
4
|
+
license: MIT
|
|
5
|
+
allowed-tools: Bash, Read, Write, Edit
|
|
6
|
+
compatibility: Codex, Claude Code, Hermes Agent, Cursor, GitHub Copilot, Windsurf, Kiro, and other Agent Skills compatible tools. Needs Node 22.18+, ffmpeg and Chrome; the narrator installs Python and its voice model on first use.
|
|
7
|
+
metadata:
|
|
8
|
+
targets: [_source-only]
|
|
9
|
+
author: Oleg Koval
|
|
10
|
+
tags:
|
|
11
|
+
- video
|
|
12
|
+
- animation
|
|
13
|
+
- shorts
|
|
14
|
+
- narration
|
|
15
|
+
- canvas
|
|
16
|
+
---
|
|
17
|
+
<!-- Generated by scripts/build-adapters.sh. Do not edit directly. -->
|
|
18
|
+
|
|
19
|
+
# Shorts Animator
|
|
20
|
+
|
|
21
|
+
A script and a look in, an animated short out. You write the lines and draw every shot as canvas code; the
|
|
22
|
+
tool paints it on the GPU on twos, narrates it with a local Kokoro voice, captions it, scores it and
|
|
23
|
+
encodes the MP4. Nothing is filmed, generated by an image model, or licensed.
|
|
24
|
+
|
|
25
|
+
`SA` = `node <this skill's folder>/scripts/sa.ts`. Write the full command each time. One folder per short:
|
|
26
|
+
`WORK=./<slug>-short`. First run on a machine: `SA doctor` (checks Node, ffmpeg, Chrome, sets up the voice,
|
|
27
|
+
renders a 3-second test).
|
|
28
|
+
|
|
29
|
+
## 1. Decide, do not interrogate
|
|
30
|
+
|
|
31
|
+
Decide the lines, shots, look, voice and music yourself. Ask one question only if the subject is unclear.
|
|
32
|
+
A pasted script is used word for word. A topic: write it per `references/writing.md`.
|
|
33
|
+
|
|
34
|
+
| they say | look |
|
|
35
|
+
|---|---|
|
|
36
|
+
| oil, painterly, Van Gogh, swirly, "like the faith shorts" | `oil` |
|
|
37
|
+
| watercolour, soft, dreamy, children's book | `watercolor` |
|
|
38
|
+
| crayon, pastel, chalk, for kids | `pastel` |
|
|
39
|
+
| ink, comic, graphic novel, line art | `ink` |
|
|
40
|
+
| flat, storybook, gouache, poster, calm | `gouache` |
|
|
41
|
+
|
|
42
|
+
Custom look: `"look": { "base": "oil", "brush": 16, "palette": {...} }`. Read `references/looks.md` only
|
|
43
|
+
when overriding numbers. Voice `bm_george` (default, scripture and quotes), `am_michael`, `bf_emma`
|
|
44
|
+
(stories), `af_heart` (kids); `speed` 0.85 to 0.95 reads calm. Their recording: `"voice": { "file":
|
|
45
|
+
"assets/voice.m4a" }` with `say` lines matching it in order. `"voice": "none"` for music only. Music
|
|
46
|
+
`mood`: `tender`, `hopeful`, `night`, `wonder`, `playful`, `none`, or a track path. `SA looks` lists all.
|
|
47
|
+
|
|
48
|
+
## 2. Write the plan and the drawings
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
SA new "$WORK" --look oil # short.json + scenes.js + assets/
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`short.json` lines: `{ "say": "...", "scene": "meadow" | ["a", "b"], "move": "push", "hold": 0.8 }`.
|
|
55
|
+
- A list of scenes cuts on the caption chunks. Cut often: a new picture every 1.5 to 3 s.
|
|
56
|
+
- `move`: push, pull, pan-left, pan-right, rise, fall, drift, still. `transition`: cut, fade, dip (rarely).
|
|
57
|
+
- `caption` shows different words; `/` forces a break. `captions.style`: box, plain, none.
|
|
58
|
+
- `format`: 9:16 (default), 16:9, 1:1, 4:5. `twos`: 2 (hand-drawn), 1 (smooth).
|
|
59
|
+
|
|
60
|
+
`scenes.js`: `window.SCENES = { "meadow": (k) => { k.sky({...}); k.person({...}); } }`. Read
|
|
61
|
+
`references/kit.md` before writing scenes. Draw like a painter:
|
|
62
|
+
- Big simple shapes, strong value contrast, one subject. The paint pass adds texture; draw nothing fussy.
|
|
63
|
+
- Keep faces out of the caption band (about 77% down).
|
|
64
|
+
- Every shot different: place, scale, day and night, warm and cool. Match the words.
|
|
65
|
+
- Small motion: `pose: "walk"`, `dx`, rain, glints. `k.t` seconds, `k.p` progress 0 to 1.
|
|
66
|
+
- No helper for it (a boat, an animal)? Write a small function with `k.g`, flat shapes in the palette.
|
|
67
|
+
|
|
68
|
+
## 3. Check, then look (budgeted)
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
SA check "$WORK" # no browser, about a second: missing scenes, throwing scenes, unknown kit calls,
|
|
72
|
+
# figures under the captions, line and total length
|
|
73
|
+
SA frames "$WORK" # narrates (cached), one painted frame per shot -> frames/sheet.jpg
|
|
74
|
+
SA still "$WORK" 7 # one shot full size, only when the sheet shows a problem you cannot place
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Fix every `ERROR` from `check` before spending a browser run. Then read `frames/sheet.jpg` once. Fix muddy
|
|
78
|
+
or empty shots, a figure lost in its background (contrast, `rim`), look-alike shots, a red "no scene"
|
|
79
|
+
frame, and every `⚠ the page said` line. **Budget: at most two frames rounds before the first render.**
|
|
80
|
+
Images are the expensive part of this loop; do not reopen a sheet you have already judged.
|
|
81
|
+
|
|
82
|
+
## 4. Render and deliver
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
SA render "$WORK" # about a minute for 40 s -> <title>.mp4, renders/vN.mp4 (+ its short.json), qa/sheet.jpg
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Read `qa/sheet.jpg` only if the render printed a warning or they report a problem. Tell them in two
|
|
89
|
+
lines what it is (look, length, shots) and: "Tell me what to change, one sentence is enough."
|
|
90
|
+
|
|
91
|
+
## 5. One-sentence changes
|
|
92
|
+
|
|
93
|
+
Smallest change, `SA check`, render, show. `SA set "$WORK" voice.speed=0.85 music.mood=night` edits
|
|
94
|
+
short.json (JSON values; plain text otherwise) and refuses an invalid result.
|
|
95
|
+
|
|
96
|
+
| they say | change |
|
|
97
|
+
|---|---|
|
|
98
|
+
| more painterly, softer | `look` or its numbers (brush, swirl, wash) |
|
|
99
|
+
| slower, faster | `voice.speed`, `pause` |
|
|
100
|
+
| a woman's voice, American | `voice.id` |
|
|
101
|
+
| no music, more emotional | `music.mood` |
|
|
102
|
+
| bigger captions, no box | `captions.size` 52, `captions.style` plain |
|
|
103
|
+
| for YouTube | `format` 16:9, redraw shots whose layout breaks |
|
|
104
|
+
| change shot 4 | that scene function only |
|
|
105
|
+
|
|
106
|
+
## When something goes wrong
|
|
107
|
+
|
|
108
|
+
| symptom | fix |
|
|
109
|
+
|---|---|
|
|
110
|
+
| no Chrome | install Chrome, or `SA doctor` fetches a headless one |
|
|
111
|
+
| "painting on the CPU" | works, about 10x slower |
|
|
112
|
+
| narrator setup fails | needs Python 3.10+ or `uv`; or use `"voice": "none"` or their recording |
|
|
113
|
+
| voice timing off with their recording | `say` lines must match its words, in order |
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# The drawing kit
|
|
2
|
+
|
|
3
|
+
Every scene is `(k) => { ... }`. Positions are fractions of the frame (x 0→1 left→right, y 0→1 top→bottom);
|
|
4
|
+
sizes in px are for a 1920-tall frame and scale with it (`k.u`). Draw back to front: sky, far, middle, near.
|
|
5
|
+
|
|
6
|
+
| in `k` | what |
|
|
7
|
+
|---|---|
|
|
8
|
+
| `k.g` | the 2D canvas context (camera already applied): draw anything the helpers don't cover |
|
|
9
|
+
| `k.W`, `k.H`, `k.u` | frame size, and the scale (H/1920) |
|
|
10
|
+
| `k.X(x)`, `k.Y(y)` | fraction → pixels |
|
|
11
|
+
| `k.t`, `k.p` | seconds into the shot (on twos), progress 0→1 |
|
|
12
|
+
| `k.r()` | random, the same on every drawing of the shot |
|
|
13
|
+
| `k.rng(seed)`, `k.noise1(x)` | a seeded random source; smooth 1D noise |
|
|
14
|
+
| `k.pal`, `k.beat` | the short's palette; this shot's beat (scene, line, move) |
|
|
15
|
+
| `k.mix(a, b, t)`, `k.alpha(c, a)` | colour helpers |
|
|
16
|
+
| `IMAGES.name` | an image from `assets/` (a logo, a reference), for `k.g.drawImage` |
|
|
17
|
+
|
|
18
|
+
## Sky and light
|
|
19
|
+
- `k.sky({ stops: [top, …, bottom], y0, y1 })`: a gradient
|
|
20
|
+
- `k.clouds({ y, band, n, size, color, shade, drift, seed })`: small cumulus in a band, drifting
|
|
21
|
+
- `k.sun({ x, y, r, color, glow, glowSize, glowAlpha })`: sun, moon, a bright bulb
|
|
22
|
+
- `k.glow({ x, y, r, color, a })`: any soft light (windows, lanterns, a halo)
|
|
23
|
+
- `k.stars({ n, y1 })`, `k.motes({ n, y0, y1, color })` (fireflies, embers), `k.rain({ n, a, slant })`
|
|
24
|
+
- `k.rays({ x, y, angle, spread, n, a, width })`: god rays through trees or clouds
|
|
25
|
+
- `k.haze({ y, h, color, a })`: air between planes
|
|
26
|
+
|
|
27
|
+
## Land and water
|
|
28
|
+
- `k.ridge({ y, amp, freq, color, top, shape, seed })`: filled ridge: `soft` hills, `peaks` mountains,
|
|
29
|
+
`pines` a conifer line, `bush` a broadleaf line, `rocky`
|
|
30
|
+
- `k.ground({ y, color, far, stripes, stripe, flowers })`: the ground plane, mown stripes, flowers
|
|
31
|
+
- `k.path({ x, y, near, far, color, bend })`: a road to the horizon
|
|
32
|
+
- `k.water({ y0, y1, color, deep, glint, glints })`: lake or sea with moving glints
|
|
33
|
+
- `k.waterfall({ x, w, y0, y1 })`
|
|
34
|
+
- `k.shape({ pts: [[x,y],…], color, smooth })`: any shape: a cliff, a giant tree trunk, an island
|
|
35
|
+
|
|
36
|
+
## Plants
|
|
37
|
+
- `k.tree({ x, y, h, kind, crown, trunk, fruit })`: kind `round` `pine` `autumn` `bare` `palm` `column`
|
|
38
|
+
- `k.trees({ n, y, h, x0, x1, kind, crown })`: a row
|
|
39
|
+
- `k.foliage({ side, x, depth, y0, y1, color, light })`: leaves hanging into frame from a side
|
|
40
|
+
- `k.trunks({ n, color, light, width, x0, x1 })`: inside a forest
|
|
41
|
+
|
|
42
|
+
## Built things
|
|
43
|
+
- `k.house({ x, y, w, h, wall, roof, window, lit, floors, aFrame })`
|
|
44
|
+
- `k.city({ y, h, color, lit, litColor, density, n })`: skyline, windows lit at night
|
|
45
|
+
- `k.pagoda({ x, y, h, color })`, `k.pole({ x, y, h })`, `k.lamp({ x, y, h, light })`
|
|
46
|
+
- `k.block({ x0, y0, x1, y1, color, edge })`: walls, doorways, a lit edge
|
|
47
|
+
- `k.heart({ x, y, s, color })`
|
|
48
|
+
|
|
49
|
+
## People
|
|
50
|
+
`k.person({ x, y, h, pose, facing, hair, skin, hairColor, top, bottom, shoes, robe, rim, silhouette, walk, dx, flip, build })`
|
|
51
|
+
- feet at (x, y), `h` = height as a fraction of the frame (0.25 is a full figure in a landscape)
|
|
52
|
+
- `pose`: `stand` `walk` `cheer` `open` `wave` `point` `reach` `pray` `hug` `sit` `kneel` `hold`
|
|
53
|
+
- `facing`: `front` | `back` (faces are left blank on purpose: it's the style, and it reads at any size)
|
|
54
|
+
- `hair`: `short` `long` `curly` `afro` `bun` `hood` `hat` `cap` `bald`
|
|
55
|
+
- `robe: true`: a long garment (biblical, monks, a dress)
|
|
56
|
+
- `silhouette: "#111"`: one colour (backlit, sunset); `rim: "#9db6f0"`: a lit outline (night)
|
|
57
|
+
- `dx`: moves over the shot (0.05 = 5% of the width); `walk`: steps per second
|
|
58
|
+
- `k.people([{...}, {...}])`: several `person` calls in one
|
|
59
|
+
- Two people walking together read as a relationship; a tiny figure in a huge landscape reads as awe or loneliness.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Looks: from words to numbers
|
|
2
|
+
|
|
3
|
+
A look is a JSON object. Start from a shipped one (`"base": "oil"`) and override.
|
|
4
|
+
|
|
5
|
+
| key | what it does | range |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| `mode` | `strokes` (paint/pastel), `wash` (watercolour/ink), `flat` | |
|
|
8
|
+
| `brush` | stroke size in px at 1920 tall | 5 fine … 11 oil … 18 bold |
|
|
9
|
+
| `length` | stroke length ÷ width | 2 dabs … 4.6 hatching |
|
|
10
|
+
| `swirl` | strokes follow a swirl (1) or the shapes (0) | 0 … 1 |
|
|
11
|
+
| `curve` | strokes bend | 0 … 1 |
|
|
12
|
+
| `bristle` | streaks inside a stroke, broken dry ends | 0 … 1 |
|
|
13
|
+
| `impasto` | thick paint catching light | 0 … 1 |
|
|
14
|
+
| `hueJitter` `valueJitter` `satJitter` | colour variation stroke to stroke | 0 … 0.4 |
|
|
15
|
+
| `respect` | big strokes stop at edges (crisper subjects) | 0 … 1 |
|
|
16
|
+
| `boil` | share of strokes redrawn every drawing (the shimmer) | 0 still … 1 lively |
|
|
17
|
+
| `wash` `bleed` | watercolour pooling, blooms, granulation; how far colour runs | 0 … 1 |
|
|
18
|
+
| `outline` `ink` | hand-inked lines, their colour | 0 … 1 |
|
|
19
|
+
| `paper` `paperColor` `grain` | canvas/paper texture, grain | 0 … 1.6 |
|
|
20
|
+
| `posterize` | flat colour bands (screen print, retro) | 0 off, 4 … 8 |
|
|
21
|
+
| `saturation` `contrast` `warmth` | grade | ~0.8 … 1.3 |
|
|
22
|
+
| `vignette` `bloom` | dark corners, glow from lights | 0 … 0.6 |
|
|
23
|
+
| `captions` | `{ style, size, y, box, color, font, case }` | |
|
|
24
|
+
|
|
25
|
+
Words → moves:
|
|
26
|
+
- "thicker / bolder paint" → brush +4, impasto +0.2 · "finer" → brush −3
|
|
27
|
+
- "swirly / Van Gogh" → swirl 0.9, curve 1, hueJitter 0.25 · "calmer" → swirl 0.3, boil 0.2
|
|
28
|
+
- "dreamy / soft" → watercolor, bleed 1 · "crisp" → respect 1, boil 0.2
|
|
29
|
+
- "vintage / old film" → saturation 0.8, warmth 0.5, grain 1, vignette 0.5
|
|
30
|
+
- "comic / graphic novel" → ink, posterize 6 · "screen print / retro poster" → gouache, posterize 5
|
|
31
|
+
- "night is too dark" → contrast 0.95 and lighten the palette, not the look
|
|
32
|
+
- "for kids" → pastel or gouache, saturation 1.1, warm palette, playful music
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Writing the lines
|
|
2
|
+
|
|
3
|
+
- One idea per line, 4 to 12 words. Read aloud it should take 1.5 to 4 seconds.
|
|
4
|
+
- Open on a hook that makes the second line necessary ("You have heard that it was said,").
|
|
5
|
+
- A turn two-thirds in ("But…", "And yet…", "Nobody ever…").
|
|
6
|
+
- Land the last line; give it `hold: 0.8` and a final wide or calm shot.
|
|
7
|
+
- Scripture, quotes and poems: use a public-domain translation (KJV, WEB) or their own words; keep it exact.
|
|
8
|
+
- Never invent a quote and attribute it to a real person.
|
|
9
|
+
- 20 to 45 seconds is the sweet spot for Shorts, Reels and TikTok; 60 s at most.
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
// The music: presets, chord voicing and the arrangement. Pure given the two rngs; the only effect is
|
|
2
|
+
// writing into the buses it allocates itself.
|
|
3
|
+
import { SR, biquad, busAdd, busMixFrom, delay, human, makeBus, reverb } from "./dsp.ts";
|
|
4
|
+
import type { Bus, Rng, Voice } from "./dsp.ts";
|
|
5
|
+
import {
|
|
6
|
+
bass, brush, ep, epChord, hat, kick, outroImpact, outroRiser, pad, pluck, shaker, snare, softSynth, strum, vinyl,
|
|
7
|
+
} from "./instruments.ts";
|
|
8
|
+
|
|
9
|
+
// key = MIDI root; prog = chords as scale degrees in the key, one per bar; ext = chord size (4 = sevenths,
|
|
10
|
+
// 5 = ninths). Chords are voiced in the middle register with the root left to the bass.
|
|
11
|
+
export type Preset = {
|
|
12
|
+
readonly bpm: number; readonly root: number; readonly scale: readonly number[]; readonly prog: readonly number[];
|
|
13
|
+
readonly ext: number; readonly parts: readonly string[]; readonly swing: number; readonly verb: number;
|
|
14
|
+
readonly duck: number; readonly low?: number; readonly drums?: number;
|
|
15
|
+
};
|
|
16
|
+
const MAJ: readonly number[] = [0, 2, 4, 5, 7, 9, 11], MIN: readonly number[] = [0, 2, 3, 5, 7, 8, 10];
|
|
17
|
+
|
|
18
|
+
export const PRESETS: Readonly<Record<string, Preset>> = {
|
|
19
|
+
kinetic: { bpm: 118, root: 57, scale: MIN, prog: [0, 5, 2, 6], ext: 5, parts: ["kick4", "snare", "hatOff", "bassHouse", "epStab", "pad"], swing: 0, verb: 0.2, duck: 0.6 },
|
|
20
|
+
keynote: { bpm: 100, root: 60, scale: MAJ, prog: [0, 5, 3, 4], ext: 4, parts: ["kickHalf", "shaker16", "epPulse", "epHold", "pad", "bassLong"], swing: 0, verb: 0.3, duck: 0.85 },
|
|
21
|
+
aura: { bpm: 78, root: 63, scale: MAJ, prog: [0, 3, 5, 4], ext: 5, parts: ["pad", "epArp", "bassSwell"], swing: 0, verb: 0.45, duck: 1 },
|
|
22
|
+
paper: { bpm: 104, root: 65, scale: MAJ, prog: [0, 4, 5, 3], ext: 4, parts: ["kickSoft", "brush", "hat8", "pluck8", "bassBounce"], swing: 0.14, verb: 0.22, duck: 0.85 },
|
|
23
|
+
terminal: { bpm: 112, root: 62, scale: MIN, prog: [0, 5, 6, 0], ext: 4, parts: ["kick4", "hat16", "bass8", "synthArp", "pad"], swing: 0, verb: 0.2, duck: 0.65, low: 0.7, drums: 0.85 },
|
|
24
|
+
data: { bpm: 86, root: 67, scale: MAJ, prog: [1, 4, 0, 5], ext: 5, parts: ["kickBoom", "snareBoom", "hat8", "epHold", "bassBoom", "vinyl"], swing: 0.18, verb: 0.25, duck: 0.85 },
|
|
25
|
+
story: { bpm: 96, root: 62, scale: MAJ, prog: [0, 4, 5, 3], ext: 4, parts: ["kickSoft", "brush", "shaker8", "guitar8", "epHold", "bassLong"], swing: 0.08, verb: 0.3, duck: 0.9 },
|
|
26
|
+
// Shorts Animator's moods: beds for a voice to sit on, no drums in the quiet ones
|
|
27
|
+
tender: { bpm: 70, root: 60, scale: MAJ, prog: [0, 5, 3, 4], ext: 5, parts: ["pad", "epArp", "bassSwell"], swing: 0, verb: 0.5, duck: 1 },
|
|
28
|
+
hopeful: { bpm: 84, root: 62, scale: MAJ, prog: [0, 4, 5, 3], ext: 4, parts: ["pad", "guitar8", "bassLong"], swing: 0.06, verb: 0.38, duck: 1 },
|
|
29
|
+
night: { bpm: 64, root: 57, scale: MIN, prog: [0, 5, 3, 6], ext: 5, parts: ["pad", "epHold", "bassSwell"], swing: 0, verb: 0.55, duck: 1 },
|
|
30
|
+
wonder: { bpm: 76, root: 65, scale: MAJ, prog: [3, 4, 0, 5], ext: 5, parts: ["pad", "epArp", "bassSwell"], swing: 0, verb: 0.5, duck: 1 },
|
|
31
|
+
playful: { bpm: 100, root: 65, scale: MAJ, prog: [0, 4, 5, 3], ext: 4, parts: ["kickSoft", "brush", "pluck8", "bassBounce"], swing: 0.14, verb: 0.22, duck: 0.9 },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// ---------- harmony ----------
|
|
35
|
+
const chord = (root: number, scale: readonly number[], deg: number, size = 4): number[] =>
|
|
36
|
+
Array.from({ length: size }, (_, k) => { const s = deg + k * 2; return root + scale[s % 7] + 12 * Math.floor(s / 7); });
|
|
37
|
+
const bassNote = (root: number, scale: readonly number[], deg: number): number => {
|
|
38
|
+
let m = root + scale[deg % 7];
|
|
39
|
+
while (m > 45) m -= 12;
|
|
40
|
+
while (m < 33) m += 12;
|
|
41
|
+
return m;
|
|
42
|
+
};
|
|
43
|
+
function nextWithPc(from: number, pc: number): number {
|
|
44
|
+
let m = from + 1;
|
|
45
|
+
while (m % 12 !== pc) m++;
|
|
46
|
+
return m;
|
|
47
|
+
}
|
|
48
|
+
// rootless close voicing that tops out by B4, as near as it can to the last chord's lowest note
|
|
49
|
+
function voicing(root: number, scale: readonly number[], deg: number, size: number, prev: readonly number[] | null): number[] {
|
|
50
|
+
const tones = chord(root, scale, deg, size).map((m) => ((m % 12) + 12) % 12);
|
|
51
|
+
const pcs = size >= 4 ? tones.slice(1) : tones;
|
|
52
|
+
const want = prev ? (prev[0] + 56) / 2 : 56; // led by the last chord, pulled back toward the middle
|
|
53
|
+
const cands = Array.from({ length: 14 }, (_, i) => 51 + i).filter((lo) => lo % 12 === pcs[0]).map((lo) => {
|
|
54
|
+
const v = pcs.slice(1).reduce((acc, pc) => [...acc, nextWithPc(acc[acc.length - 1], pc)], [lo]);
|
|
55
|
+
return { v, score: Math.abs(lo - want) + (v[v.length - 1] > 71 ? 100 : 0) };
|
|
56
|
+
});
|
|
57
|
+
return cands.reduce((best, c) => (c.score < best.score ? c : best)).v;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------- the arrangement ----------
|
|
61
|
+
export type Timing = { readonly duration: number; readonly outroAt?: number };
|
|
62
|
+
type Ctx = {
|
|
63
|
+
readonly P: Preset; readonly r: Rng; readonly ham: Rng;
|
|
64
|
+
readonly spb: number; readonly bar: number; readonly s16: number; readonly end: number;
|
|
65
|
+
readonly drums: Bus; readonly inst: Bus; readonly low: Bus; readonly send: Bus;
|
|
66
|
+
readonly voices: readonly (readonly number[])[]; readonly roots: readonly number[];
|
|
67
|
+
};
|
|
68
|
+
type Bar = { readonly b: number; readonly t0: number; readonly ch: readonly number[]; readonly rt: number; readonly len: number };
|
|
69
|
+
|
|
70
|
+
const hasPart = (c: Ctx, p: string): boolean => c.P.parts.includes(p);
|
|
71
|
+
const intro = (c: Ctx, bar: Bar): boolean => bar.b === 0 && c.P.bpm < 90; // slow beds open on the harmony alone
|
|
72
|
+
const before = (c: Ctx, t: number): boolean => t < c.end - 0.02;
|
|
73
|
+
// every call draws one rng value for the humanising jitter, so the call order is part of the sound
|
|
74
|
+
function at(c: Ctx, bar: Bar, q: number): number {
|
|
75
|
+
const swing = q % 2 ? c.P.swing * c.s16 * 2 : 0;
|
|
76
|
+
return bar.t0 + q * c.s16 + swing + (c.r() - 0.5) * 0.01;
|
|
77
|
+
}
|
|
78
|
+
const steps = (step: number, count = 16 / step): number[] => Array.from({ length: count }, (_, i) => i * step);
|
|
79
|
+
|
|
80
|
+
// stops at the first step that falls past the end
|
|
81
|
+
function walk(c: Ctx, bar: Bar, qs: readonly number[], fn: (q: number, t: number) => void): void {
|
|
82
|
+
for (const q of qs) {
|
|
83
|
+
const t = Math.max(0, at(c, bar, q));
|
|
84
|
+
if (!before(c, t)) break;
|
|
85
|
+
fn(q, t);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// skips the steps that fall past the end, keeps going
|
|
89
|
+
function visit(c: Ctx, bar: Bar, qs: readonly number[], clampT: boolean, fn: (q: number, t: number) => void): void {
|
|
90
|
+
for (const q of qs) {
|
|
91
|
+
const raw = at(c, bar, q), t = clampT ? Math.max(0, raw) : raw;
|
|
92
|
+
if (before(c, t)) fn(q, t);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const instSend = (c: Ctx, v: Voice, t: number, ig: number, pan: number, sg: number): void => {
|
|
96
|
+
busAdd(c.inst, v, t, ig, pan);
|
|
97
|
+
busAdd(c.send, v, t, sg);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// ---------- drums: each step returns the kick times it played (the sidechain listens to them) ----------
|
|
101
|
+
type DrumStep = (c: Ctx, bar: Bar, q: number, t: number) => number[];
|
|
102
|
+
const kickAt = (c: Ctx, t: number, v: Float32Array, gain: number): number[] => { busAdd(c.drums, v, t, gain); return [t]; };
|
|
103
|
+
const snareAt = (c: Ctx, t: number, s: Float32Array, dg: number, sg: number): number[] => { busAdd(c.drums, s, t, dg); busAdd(c.send, s, t, sg); return []; };
|
|
104
|
+
const tick = (c: Ctx, v: Float32Array, t: number, gain: number, pan: number): number[] => { busAdd(c.drums, v, t, gain, pan); return []; };
|
|
105
|
+
|
|
106
|
+
// the order is the order the rng is drawn in
|
|
107
|
+
const DRUM_STEPS: readonly (readonly [string, DrumStep])[] = [
|
|
108
|
+
["kick4", (c, _b, q, t) => (q % 4 === 0 ? kickAt(c, t, kick(c.r), 0.75 * human(c.r, 0.08)) : [])],
|
|
109
|
+
["kickHalf", (c, _b, q, t) => (q === 0 || q === 8 ? kickAt(c, t, kick(c.r, { punch: 0.6, dec: 0.32 }), 0.45) : [])],
|
|
110
|
+
["kickSoft", (c, bar, q, t) => (q === 0 || q === 8 || (q === 6 && bar.b % 2 === 1) ? kickAt(c, t, kick(c.r, { punch: 0.7, dec: 0.3 }), 0.55) : [])],
|
|
111
|
+
["kickBoom", (c, _b, q, t) => (q === 0 || q === 7 || q === 10 ? kickAt(c, t, kick(c.r, { pitch: 48, dec: 0.4 }), q === 7 ? 0.4 : 0.65) : [])],
|
|
112
|
+
["snare", (c, _b, q, t) => (q === 4 || q === 12 ? snareAt(c, t, snare(c.r, { tone: 200, top: 8000 }), 0.42, 0.18) : [])],
|
|
113
|
+
["snareBoom", (c, _b, q, t) => (q === 4 || q === 12 ? snareAt(c, t, snare(c.r, { dec: 0.16, top: 6000 }), 0.5, 0.12) : [])],
|
|
114
|
+
["brush", (c, _b, q, t) => {
|
|
115
|
+
if (q !== 4 && q !== 12) return [];
|
|
116
|
+
const s = brush(c.r);
|
|
117
|
+
busAdd(c.drums, s, t, 0.5, 0.1); busAdd(c.send, s, t, 0.2);
|
|
118
|
+
return [];
|
|
119
|
+
}],
|
|
120
|
+
["hatOff", (c, _b, q, t) => (q % 4 === 2 ? tick(c, hat(c.r), t, 0.3 * human(c.r, 0.3), 0.25) : [])],
|
|
121
|
+
["hat8", (c, _b, q, t) => (q % 2 === 0 ? tick(c, hat(c.r), t, (q % 4 === 2 ? 0.2 : 0.11) * human(c.r, 0.4), 0.3) : [])],
|
|
122
|
+
["shaker16", (c, _b, q, t) => tick(c, shaker(c.r), t, (q % 4 === 2 ? 0.2 : q % 2 ? 0.07 : 0.12) * human(c.r, 0.4), 0.35)],
|
|
123
|
+
["shaker8", (c, _b, q, t) => (q % 2 === 0 ? tick(c, shaker(c.r), t, (q % 4 === 2 ? 0.2 : 0.1) * human(c.r, 0.4), 0.35) : [])],
|
|
124
|
+
["hat16", (c, _b, q, t) => tick(c, hat(c.r), t, (q % 4 === 2 ? 0.16 : 0.08) * human(c.r, 0.4), -0.2)],
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
// recursion (not a loop) so the kick times come back as a value; stops at the first step past the end
|
|
128
|
+
function drumWalk(c: Ctx, bar: Bar, q: number): number[] {
|
|
129
|
+
if (q >= 16) return [];
|
|
130
|
+
const t = Math.max(0, at(c, bar, q));
|
|
131
|
+
if (!before(c, t)) return [];
|
|
132
|
+
const here = DRUM_STEPS.flatMap(([name, fn]) => (hasPart(c, name) ? fn(c, bar, q, t) : []));
|
|
133
|
+
return [...here, ...drumWalk(c, bar, q + 1)];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---------- harmony and bass parts ----------
|
|
137
|
+
type Part = (c: Ctx, bar: Bar) => void;
|
|
138
|
+
|
|
139
|
+
const padPart: Part = (c, bar) => {
|
|
140
|
+
const p = pad(bar.ch, bar.len + 1.2, c.r, { cutoff: c.P.bpm < 90 ? 2800 : 2400, attack: intro(c, bar) ? 1.4 : 0.5 });
|
|
141
|
+
instSend(c, p, bar.t0, hasPart(c, "epArp") ? 0.5 : 0.28, 0, 0.4);
|
|
142
|
+
};
|
|
143
|
+
const epHoldPart: Part = (c, bar) => visit(c, bar, bar.b % 2 ? [0, 10] : [0], false, (q, t) => {
|
|
144
|
+
const v = epChord(bar.ch, q ? c.spb * 1.5 : bar.len + 0.3, 0.42, c.r, c.ham);
|
|
145
|
+
instSend(c, v, Math.max(0, t), q ? 0.3 : 0.45, 0, 0.3);
|
|
146
|
+
});
|
|
147
|
+
const epStabPart: Part = (c, bar) => {
|
|
148
|
+
if (intro(c, bar)) return;
|
|
149
|
+
visit(c, bar, [3, 6, 10, 14], false, (q, t) => instSend(c, epChord(bar.ch, c.spb * 0.55, 0.5, c.r, c.ham, { roll: 0.004 }), t, 0.5, q % 2 ? 0.15 : -0.15, 0.25));
|
|
150
|
+
};
|
|
151
|
+
const epPulsePart: Part = (c, bar) => walk(c, bar, steps(2), (q, t) => {
|
|
152
|
+
const m = q % 8 === 0 ? bar.ch[0] : q % 4 === 0 ? bar.ch[2] ?? bar.ch[1] : bar.ch[1];
|
|
153
|
+
const v = ep(m - 12 >= 48 ? m - 12 : m, c.spb * 0.9, 0.38 * human(c.r, 0.15), c.ham);
|
|
154
|
+
instSend(c, v, t, 0.38, q % 4 ? 0.2 : -0.2, 0.25);
|
|
155
|
+
});
|
|
156
|
+
const epArpPart: Part = (c, bar) => walk(c, bar, steps(4), (q, t) => {
|
|
157
|
+
const v = ep(bar.ch[(q / 4) % bar.ch.length], c.spb * 3, 0.3 * human(c.r, 0.2), c.ham);
|
|
158
|
+
instSend(c, v, t, 0.42, (q / 4 - 1.5) * 0.3, 0.5);
|
|
159
|
+
});
|
|
160
|
+
const PLUCK8_PAT = [0, 2, 1, 3, 2, 1, 0, 2];
|
|
161
|
+
const pluck8Part: Part = (c, bar) => {
|
|
162
|
+
if (intro(c, bar)) return;
|
|
163
|
+
// steps 3 and 7 are rests: they draw no jitter and cannot end the walk
|
|
164
|
+
walk(c, bar, [0, 1, 2, 4, 5, 6].map((k) => k * 2), (q, t) => {
|
|
165
|
+
const k = q / 2, s = pluck(bar.ch[PLUCK8_PAT[k] % bar.ch.length], 0.8, c.r, { bright: 0.28, decay: 0.993 });
|
|
166
|
+
instSend(c, s, t, human(c.r, 0.2), (k % 3 - 1) * 0.3, 0.3);
|
|
167
|
+
});
|
|
168
|
+
};
|
|
169
|
+
const GUITAR8_PAT = [0, 1, 2, 3, 2, 1, 2, 3];
|
|
170
|
+
const guitar8Part: Part = (c, bar) => walk(c, bar, steps(2), (q, t) => {
|
|
171
|
+
const k = q / 2, ch = bar.ch;
|
|
172
|
+
const m = k === 0 ? (ch[0] - 12 >= 45 ? ch[0] - 12 : ch[0]) : ch[GUITAR8_PAT[k] % ch.length];
|
|
173
|
+
const s = pluck(m, 1.2, c.r, { bright: 0.22, decay: 0.996 });
|
|
174
|
+
instSend(c, s, t, 0.9 * human(c.r, 0.2), k % 2 ? 0.25 : -0.25, 0.3);
|
|
175
|
+
});
|
|
176
|
+
const SYNTH_ARP_PAT = [0, 1, 2, 1, 3, 2];
|
|
177
|
+
const synthArpPart: Part = (c, bar) => {
|
|
178
|
+
if (intro(c, bar)) return;
|
|
179
|
+
walk(c, bar, steps(1), (q, t) => {
|
|
180
|
+
const m = bar.ch[SYNTH_ARP_PAT[q % 6] % bar.ch.length] - 12;
|
|
181
|
+
const s = softSynth(m < 45 ? m + 12 : m, c.s16 * 1.6, 1900 + 700 * Math.sin((bar.b * 16 + q) / 10));
|
|
182
|
+
instSend(c, s, t, 0.22, q % 2 ? 0.25 : -0.25, 0.22);
|
|
183
|
+
});
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const lowAt = (c: Ctx, v: Float32Array, t: number, gain: number): void => busAdd(c.low, v, t, gain);
|
|
187
|
+
const bassHousePart: Part = (c, bar) => {
|
|
188
|
+
if (intro(c, bar)) return;
|
|
189
|
+
visit(c, bar, [2, 6, 10, 14], false, (q, t) => lowAt(c, bass(q === 14 ? bar.rt + 7 : bar.rt, c.spb * 0.45), t, 0.55));
|
|
190
|
+
};
|
|
191
|
+
const bassLongPart: Part = (c, bar) => visit(c, bar, [0, 8], true, (_q, t) => lowAt(c, bass(bar.rt, c.spb * 1.9, { a: 0.02, r: 0.2 }), t, 0.45));
|
|
192
|
+
const bassSwellPart: Part = (c, bar) => lowAt(c, bass(bar.rt, bar.len + 0.4, { a: 0.6, r: 0.6, drive: 1 }), bar.t0, 0.4);
|
|
193
|
+
const BOUNCE: readonly (readonly [number, number])[] = [[0, 0], [6, 7], [8, 0], [14, 7]];
|
|
194
|
+
const bassBouncePart: Part = (c, bar) => {
|
|
195
|
+
if (intro(c, bar)) return;
|
|
196
|
+
BOUNCE.forEach(([q, d]) => visit(c, bar, [q], true, (_q, t) => lowAt(c, bass(bar.rt + d, c.spb * 0.45, { tone: 0.3 }), t, 0.5)));
|
|
197
|
+
};
|
|
198
|
+
const bass8Part: Part = (c, bar) => {
|
|
199
|
+
if (intro(c, bar)) return;
|
|
200
|
+
visit(c, bar, steps(2), true, (_q, t) => lowAt(c, bass(bar.rt, c.spb * 0.4), t, 0.34));
|
|
201
|
+
};
|
|
202
|
+
const bassBoomPart: Part = (c, bar) => visit(c, bar, [0, 10], true, (q, t) => lowAt(c, bass(bar.rt, c.spb * (q ? 1.2 : 1.5), { a: 0.01, r: 0.15 }), t, 0.55));
|
|
203
|
+
|
|
204
|
+
// the order is the order the rng is drawn in: harmony first, then bass
|
|
205
|
+
const PARTS: readonly (readonly [string, Part])[] = [
|
|
206
|
+
["pad", padPart], ["epHold", epHoldPart], ["epStab", epStabPart], ["epPulse", epPulsePart], ["epArp", epArpPart],
|
|
207
|
+
["pluck8", pluck8Part], ["guitar8", guitar8Part], ["synthArp", synthArpPart],
|
|
208
|
+
["bassHouse", bassHousePart], ["bassLong", bassLongPart], ["bassSwell", bassSwellPart],
|
|
209
|
+
["bassBounce", bassBouncePart], ["bass8", bass8Part], ["bassBoom", bassBoomPart],
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
function renderBar(c: Ctx, bar: Bar): number[] {
|
|
213
|
+
const kicks = intro(c, bar) ? [] : drumWalk(c, bar, 0);
|
|
214
|
+
PARTS.forEach(([name, part]) => { if (hasPart(c, name)) part(c, bar); });
|
|
215
|
+
return kicks;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// the end: a low swell, one deep hit, the home chord left ringing
|
|
219
|
+
function ending(c: Ctx, duration: number): void {
|
|
220
|
+
const { P, r, ham, spb, end } = c;
|
|
221
|
+
const home = voicing(P.root, P.scale, 0, P.ext || 4, c.voices[c.voices.length - 1]);
|
|
222
|
+
const riserSec = Math.min(1.6, spb * 3);
|
|
223
|
+
busAdd(c.inst, outroRiser(r, riserSec), end - riserSec, 0.14);
|
|
224
|
+
const hit = outroImpact(r, P.root);
|
|
225
|
+
busAdd(c.inst, hit, end, 0.3); busAdd(c.send, hit, end, 0.2);
|
|
226
|
+
const tail = Math.max(2.4, duration - end + 1.8);
|
|
227
|
+
if (hasPart(c, "guitar8") || hasPart(c, "pluck8")) {
|
|
228
|
+
const s = strum([bassNote(P.root, P.scale, 0) + 12, ...home], tail, r, { bright: 0.24, decay: 0.998 });
|
|
229
|
+
busAdd(c.inst, s, end, 1.1); busAdd(c.send, s, end, 0.4);
|
|
230
|
+
} else {
|
|
231
|
+
const ch = epChord(home, tail, 0.45, r, ham, { roll: 0.03 });
|
|
232
|
+
busAdd(c.inst, ch, end, 0.55); busAdd(c.send, ch, end, 0.5);
|
|
233
|
+
}
|
|
234
|
+
const p = pad(home, tail, r, { cutoff: 2200, attack: 0.05, release: 1.8 });
|
|
235
|
+
busAdd(c.inst, p, end, 0.22); busAdd(c.send, p, end, 0.4);
|
|
236
|
+
busAdd(c.low, bass(bassNote(P.root, P.scale, 0), Math.max(1.5, duration - end + 0.6), { a: 0.005, r: 1.2 }), end, 0.45);
|
|
237
|
+
if (P.parts.some((x) => x.startsWith("kick"))) busAdd(c.drums, kick(r, { dec: 0.5 }), end, 0.7);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ---------- mix ----------
|
|
241
|
+
function duckAround(duck: Float32Array, t: number, depth: number): void {
|
|
242
|
+
const i0 = Math.round(t * SR);
|
|
243
|
+
for (let i = 0; i < 0.22 * SR && i0 + i < duck.length; i++) duck[i0 + i] = Math.min(duck[i0 + i], 1 - depth + depth * Math.pow(i / (0.22 * SR), 0.6));
|
|
244
|
+
}
|
|
245
|
+
// sidechain: the harmony breathes under each kick (deep for the dance beds, barely for the gentle ones)
|
|
246
|
+
function sidechain(c: Ctx, kicks: readonly number[]): void {
|
|
247
|
+
const depth = 1 - c.P.duck;
|
|
248
|
+
if (depth <= 0.01) return;
|
|
249
|
+
const { inst, low } = c, duck = new Float32Array(inst.n).fill(1);
|
|
250
|
+
kicks.forEach((t) => duckAround(duck, t, depth));
|
|
251
|
+
for (let i = 0; i < inst.n; i++) { inst.L[i] *= duck[i]; inst.R[i] *= duck[i]; low.L[i] *= duck[i] * 0.7 + 0.3; low.R[i] *= duck[i] * 0.7 + 0.3; }
|
|
252
|
+
}
|
|
253
|
+
// a dark room: the reverb only hears the middle of the spectrum
|
|
254
|
+
function darken(send: Bus): void {
|
|
255
|
+
const sl = [biquad("lp", 6500), biquad("lp", 6500)], sh = [biquad("hp", 220), biquad("hp", 220)];
|
|
256
|
+
for (let i = 0; i < send.n; i++) { send.L[i] = sl[0](sh[0](send.L[i])); send.R[i] = sl[1](sh[1](send.R[i])); }
|
|
257
|
+
}
|
|
258
|
+
function mixDown(c: Ctx, total: number, duration: number): Bus {
|
|
259
|
+
const { P, send, spb } = c;
|
|
260
|
+
darken(send);
|
|
261
|
+
const wet = reverb(send, { room: P.verb > 0.4 ? 0.88 : 0.82, damp: 0.45 });
|
|
262
|
+
const echo = P.bpm >= 100 ? delay(send, spb * 0.75, 0.28, 2400) : null;
|
|
263
|
+
const music = makeBus(total);
|
|
264
|
+
busMixFrom(music, c.drums, P.drums ?? 1); busMixFrom(music, c.inst, 1); busMixFrom(music, c.low, P.low ?? 1); busMixFrom(music, wet, P.verb * 2.2);
|
|
265
|
+
if (echo) busMixFrom(music, echo, 0.1);
|
|
266
|
+
// fade in over half a beat, out after the ring
|
|
267
|
+
const fin = Math.round(spb * SR * 0.5), fout0 = Math.round((duration + 0.2) * SR), fout1 = Math.round((duration + 2.6) * SR);
|
|
268
|
+
for (let i = 0; i < music.n; i++) {
|
|
269
|
+
const g = (i < fin ? i / fin : 1) * (i > fout0 ? Math.max(0, 1 - (i - fout0) / (fout1 - fout0)) : 1);
|
|
270
|
+
music.L[i] *= g; music.R[i] *= g;
|
|
271
|
+
}
|
|
272
|
+
return music;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function composeMusic(P: Preset, timing: Timing, r: Rng, ham: Rng): Bus {
|
|
276
|
+
const { duration } = timing;
|
|
277
|
+
const spb = 60 / P.bpm, bar = spb * 4, total = duration + 3; // ring-out
|
|
278
|
+
const end = timing.outroAt ?? Math.max(0, duration - 3);
|
|
279
|
+
const nBars = Math.ceil(duration / bar) + 1;
|
|
280
|
+
const deg = (b: number): number => P.prog[b % P.prog.length];
|
|
281
|
+
const voices = Array.from({ length: nBars }).reduce<number[][]>((acc, _, b) => [...acc, voicing(P.root, P.scale, deg(b), P.ext || 4, acc[b - 1] ?? null)], []);
|
|
282
|
+
const roots = Array.from({ length: nBars }, (_, b) => bassNote(P.root, P.scale, deg(b)));
|
|
283
|
+
const c: Ctx = { P, r, ham, spb, bar, s16: spb / 4, end, drums: makeBus(total), inst: makeBus(total), low: makeBus(total), send: makeBus(total), voices, roots };
|
|
284
|
+
const bars = Array.from({ length: nBars }, (_, b) => b).filter((b) => b * bar < end - 0.01);
|
|
285
|
+
const kicks = bars.flatMap((b) => renderBar(c, { b, t0: b * bar, ch: voices[b], rt: roots[b], len: Math.min(bar, end - b * bar) }));
|
|
286
|
+
if (hasPart(c, "vinyl")) busAdd(c.inst, vinyl(duration + 1, r), 0, 0.5);
|
|
287
|
+
ending(c, duration);
|
|
288
|
+
sidechain(c, kicks);
|
|
289
|
+
return mixDown(c, total, duration);
|
|
290
|
+
}
|