@hraness/slopcamera 3.3.6 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +30 -16
  2. package/apps/desktop/application/operation.ts +1 -1
  3. package/apps/desktop/cli/args.ts +1 -1
  4. package/apps/desktop/cli/capability-manifest.ts +13 -0
  5. package/apps/desktop/cli/commands.ts +8 -1
  6. package/apps/desktop/cli/help.ts +45 -10
  7. package/apps/desktop/cli/html-overlay-renderer.ts +16 -0
  8. package/apps/desktop/cli/menubar.ts +91 -15
  9. package/apps/desktop/cli/portable-surface.ts +2 -0
  10. package/apps/desktop/code/public.ts +1 -0
  11. package/apps/desktop/core/visual-style-look.ts +34 -0
  12. package/apps/desktop/dist/cli/main.js +338 -282
  13. package/apps/desktop/html-overlay/catalog.ts +1 -0
  14. package/apps/desktop/html-overlay/runtime.ts +15 -0
  15. package/apps/desktop/html-overlay/spatial.ts +10 -6
  16. package/apps/desktop/workflows/index.ts +1 -0
  17. package/dist/cli.js +6 -2
  18. package/dist/{index-sy1n4zfy.js → index-24w75s6a.js} +1 -1
  19. package/dist/{index-qtn3s421.js → index-8bhp7cj2.js} +1 -1
  20. package/dist/{index-10bz03js.js → index-jab2mg0s.js} +4 -3
  21. package/dist/{index-qry58nj2.js → index-mnp0bftq.js} +2 -2
  22. package/dist/index.js +1 -1
  23. package/dist/operations.js +1 -1
  24. package/dist/vectorize/worker.js +1 -1
  25. package/dist/workflow.js +1 -1
  26. package/docs/README.md +4 -3
  27. package/examples/studio/vgpu/README.md +1 -1
  28. package/examples/style-portfolio/ANIMATION-STUDIES.md +34 -0
  29. package/examples/style-portfolio/FILM-PLATE.md +19 -0
  30. package/examples/style-portfolio/README.md +137 -0
  31. package/examples/style-portfolio/animation-studies.html +597 -0
  32. package/examples/style-portfolio/assets/instrument-serif-OFL.txt +93 -0
  33. package/examples/style-portfolio/assets/instrument-serif-latin-400.woff2 +0 -0
  34. package/examples/style-portfolio/build-gallery.ts +403 -0
  35. package/examples/style-portfolio/film-plate.html +76 -0
  36. package/examples/style-portfolio/finish-film.ts +152 -0
  37. package/examples/style-portfolio/market-street/README.md +43 -0
  38. package/examples/style-portfolio/market-street/mesh.py +145 -0
  39. package/examples/style-portfolio/market-street/scene.py +760 -0
  40. package/examples/style-portfolio/market-street/source.json +1 -0
  41. package/examples/style-portfolio/market-street/street_data.py +72 -0
  42. package/examples/style-portfolio/render-film-plate.ts +128 -0
  43. package/examples/style-portfolio/render.ts +89 -0
  44. package/examples/style-portfolio/verify-animation-source.mjs +94 -0
  45. package/package.json +14 -5
  46. package/skills/slopcamera/SKILL.md +13 -3
  47. package/skills/slopcamera/references/brand-illustrations.md +237 -0
  48. package/skills/slopcamera/references/gateway-media.md +6 -1
  49. package/skills/slopcamera/references/install.md +10 -1
  50. package/skills/slopcamera/references/music-video.md +5 -0
  51. package/skills/slopcamera/references/native-studio.md +4 -0
  52. package/skills/slopcamera/references/patent-drawings.md +4 -4
  53. package/skills/slopcamera/references/support.md +1 -1
  54. package/skills/slopcamera/references/vectorization.md +3 -1
  55. package/skills/slopcamera/references/visual-style-direction.md +143 -0
  56. package/src/cli.ts +28 -0
  57. package/src/code/public-operations.ts +9 -0
  58. package/src/index.ts +1 -0
  59. package/src/mcp/tools.ts +4 -0
  60. package/src/operations.ts +4 -0
  61. package/src/support.ts +1 -1
  62. package/src/vectorize/command.ts +6 -1
  63. package/src/vectorize/supervisor.ts +23 -3
  64. package/src/vectorize/tool.ts +8 -4
  65. package/src/version.ts +1 -1
  66. package/src/visual-style.ts +378 -0
@@ -2,7 +2,10 @@ import { fileURLToPath } from "node:url"
2
2
  import { mkdtemp, rm } from "node:fs/promises"
3
3
  import { tmpdir } from "node:os"
4
4
  import { dirname, join } from "node:path"
5
- import { runBoundedCommand } from "./command.js"
5
+ import {
6
+ TERMINATION_GRACE_MS,
7
+ runBoundedCommand,
8
+ } from "./command.js"
6
9
  import { resolveVectorizeLimits } from "./limits.js"
7
10
  import {
8
11
  MAX_VECTORIZE_REQUEST_BYTES,
@@ -33,8 +36,25 @@ const vectorizeErrorCodes = new Set<VectorizeErrorCode>([
33
36
  "trace_failed",
34
37
  "unsafe_svg",
35
38
  ])
36
- const WORKER_SHUTDOWN_RESERVE_MS = 250
37
- const WORKER_RESPONSE_RESERVE_MS = 100
39
+ /**
40
+ * Wall-clock the supervisor keeps back from the worker's budget so the whole
41
+ * boundary (worker timeout, layered process-group termination, stream
42
+ * settling, temporary-root removal, response decoding) still rejects inside
43
+ * `limits.maxDurationMs`.
44
+ *
45
+ * When the worker overruns, two termination layers run back to back: the
46
+ * worker terminates its own VTracer group, and this supervisor terminates the
47
+ * worker group. Each layer waits `TERMINATION_GRACE_MS` before SIGKILL and then
48
+ * needs the scheduler to reap the process and settle its pipes. A loaded CI
49
+ * runner has been measured spending ~400 ms in that chain against the earlier
50
+ * 350 ms reserve (hraness/slopcamera#235), so the reserve now budgets each
51
+ * layer explicitly instead of guessing one round number.
52
+ */
53
+ const PROCESS_TEARDOWN_ALLOWANCE_MS = 250
54
+ const TERMINATION_LAYERS = 2
55
+ const WORKER_SHUTDOWN_RESERVE_MS =
56
+ TERMINATION_LAYERS * (TERMINATION_GRACE_MS + PROCESS_TEARDOWN_ALLOWANCE_MS)
57
+ const WORKER_RESPONSE_RESERVE_MS = 150
38
58
 
39
59
  export async function runVectorizeWorker(
40
60
  input: VectorizeInput,
@@ -383,11 +383,13 @@ async function hashRegularFile(
383
383
  deadline.assert("VTracer hash")
384
384
  handle = await open(resolvedPath, boundedReadFlags())
385
385
  const metadata = await handle.stat()
386
- if (!metadata.isFile() || metadata.size < 1 || metadata.size > maximumBytes) {
386
+ if (
387
+ !metadata?.isFile() || metadata.size < 1 || metadata.size > maximumBytes
388
+ ) {
387
389
  throw new VectorizeError(
388
390
  failureCode,
389
391
  "VTracer must be a non-empty regular file within its size limit.",
390
- { bytes: metadata.size, maximumBytes },
392
+ { bytes: metadata?.size ?? 0, maximumBytes },
391
393
  )
392
394
  }
393
395
  const hash = createHash("sha256")
@@ -470,11 +472,13 @@ function assertBoundedRegularTool(
470
472
  metadata: Awaited<ReturnType<FileHandle["stat"]>>,
471
473
  path: string,
472
474
  ): void {
473
- if (!metadata.isFile() || metadata.size < 1 || metadata.size > MAX_TOOL_BYTES) {
475
+ if (
476
+ !metadata?.isFile() || metadata.size < 1 || metadata.size > MAX_TOOL_BYTES
477
+ ) {
474
478
  throw new VectorizeError(
475
479
  "tool_version",
476
480
  `VTracer must be a non-empty regular file: ${path}`,
477
- { bytes: metadata.size, maximumBytes: MAX_TOOL_BYTES },
481
+ { bytes: metadata?.size ?? 0, maximumBytes: MAX_TOOL_BYTES },
478
482
  )
479
483
  }
480
484
  }
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const SLOPCAMERA_VERSION = "3.3.6" as const
1
+ export const SLOPCAMERA_VERSION = "3.5.0" as const
@@ -0,0 +1,378 @@
1
+ import { z } from "zod"
2
+
3
+ /**
4
+ * Art direction is independent of the renderer, its library locks, and model choice.
5
+ * Public copy cites this count; `scripts/check-copy.ts` fails on drift.
6
+ */
7
+ export const VISUAL_STYLE_IDS = Object.freeze([
8
+ "silent-actuality", "noir-35mm", "documentary-16mm", "super8-color",
9
+ "theatrical-cel", "watercolor-storybook", "ink-sketch", "rubber-hose",
10
+ "midcentury-limited", "cut-paper", "stopmotion-clay", "pixel-art",
11
+ "rotoscope", "engraving", "math-explainer", "isometric-design", "clean-motion",
12
+ ] as const)
13
+
14
+ export const VisualStyleIdSchema = z.enum(VISUAL_STYLE_IDS)
15
+ export type VisualStyleId = z.infer<typeof VisualStyleIdSchema>
16
+
17
+ const DirectionTextSchema = z.string().trim().min(1).max(512)
18
+ const DirectionListSchema = z.array(DirectionTextSchema).min(1).max(12).readonly()
19
+ const ColorSchema = z.string().regex(/^#[0-9a-f]{6}$/u)
20
+ const UnitSchema = z.number().finite().min(0).max(1)
21
+
22
+ export const VisualStyleProfileSchema = z.strictObject({
23
+ id: VisualStyleIdSchema,
24
+ name: z.string().trim().min(1).max(80),
25
+ family: z.enum(["historical-film", "drawn-animation", "tactile-animation", "digital-animation", "explanation"]),
26
+ summary: DirectionTextSchema,
27
+ palette: z.strictObject({
28
+ background: ColorSchema,
29
+ ink: ColorSchema,
30
+ paper: ColorSchema,
31
+ primary: ColorSchema,
32
+ secondary: ColorSchema,
33
+ accent: ColorSchema,
34
+ }).readonly(),
35
+ cadence: z.strictObject({
36
+ fps: z.number().int().min(1).max(120),
37
+ exposureFrames: z.number().int().min(1).max(12),
38
+ cameraExposureFrames: z.number().int().min(1).max(12),
39
+ shutterAngle: z.number().finite().min(0).max(360),
40
+ }).readonly(),
41
+ delivery: z.strictObject({
42
+ width: z.number().int().min(1080).max(8192).multipleOf(2),
43
+ height: z.number().int().min(1080).max(8192).multipleOf(2),
44
+ sampling: z.enum(["smooth", "nearest"]),
45
+ logicalGrid: z.strictObject({
46
+ width: z.number().int().min(16).max(1024),
47
+ height: z.number().int().min(16).max(1024),
48
+ }).readonly().optional(),
49
+ }).superRefine((delivery, context) => {
50
+ const grid = delivery.logicalGrid
51
+ if (delivery.sampling === "nearest") {
52
+ if (grid === undefined || delivery.width % grid.width !== 0
53
+ || delivery.height % grid.height !== 0
54
+ || delivery.width / grid.width !== delivery.height / grid.height) {
55
+ context.addIssue({ code: "custom", message: "Nearest sampling requires a logical grid with one exact integer scale on both axes." })
56
+ }
57
+ } else if (grid !== undefined) {
58
+ context.addIssue({ code: "custom", message: "A logical pixel grid requires nearest sampling." })
59
+ }
60
+ }).readonly(),
61
+ shape: DirectionListSchema,
62
+ material: DirectionListSchema,
63
+ camera: DirectionListSchema,
64
+ finishing: z.strictObject({
65
+ grain: UnitSchema,
66
+ grainSizePxAt1080: z.number().finite().min(0).max(8),
67
+ halation: UnitSchema,
68
+ gateWeavePxAt1080: z.number().finite().min(0).max(4),
69
+ vignette: UnitSchema,
70
+ saturation: z.number().finite().min(0).max(2),
71
+ contrast: z.number().finite().min(0.5).max(2),
72
+ }).readonly(),
73
+ avoid: DirectionListSchema,
74
+ acceptance: DirectionListSchema,
75
+ }).readonly()
76
+
77
+ export type VisualStyleProfile = z.infer<typeof VisualStyleProfileSchema>
78
+
79
+ const cleanFinish: VisualStyleProfile["finishing"] = Object.freeze({
80
+ grain: 0, grainSizePxAt1080: 0, halation: 0, gateWeavePxAt1080: 0,
81
+ vignette: 0, saturation: 1, contrast: 1,
82
+ })
83
+ const cinema = Object.freeze({ fps: 24, exposureFrames: 1, cameraExposureFrames: 1, shutterAngle: 180 })
84
+ const onTwos = Object.freeze({ fps: 24, exposureFrames: 2, cameraExposureFrames: 1, shutterAngle: 0 })
85
+ const uhd = Object.freeze({ width: 3840, height: 2160, sampling: "smooth" as const })
86
+ const academy = Object.freeze({ width: 2880, height: 2160, sampling: "smooth" as const })
87
+
88
+ function profile(value: z.input<typeof VisualStyleProfileSchema>): VisualStyleProfile {
89
+ return VisualStyleProfileSchema.parse(value)
90
+ }
91
+
92
+ /** Recommendations, not automatic effects or claims of historical authenticity. */
93
+ export const VISUAL_STYLE_PROFILES: readonly VisualStyleProfile[] = Object.freeze([
94
+ profile({
95
+ id: "silent-actuality", name: "Silent actuality", family: "historical-film",
96
+ summary: "An observed street or everyday action reconstructed with early monochrome photography, a restrained camera, and a coherent period world.",
97
+ palette: { background: "#171714", ink: "#080909", paper: "#e2dfce", primary: "#a7a797", secondary: "#666b63", accent: "#c6c3ad" },
98
+ cadence: { fps: 24, exposureFrames: 1, cameraExposureFrames: 1, shutterAngle: 180 }, delivery: academy,
99
+ shape: ["Stage a deep street with foreground, middle distance, and a readable vanishing point.", "Choose one place and decade; match silhouettes of buildings, signs, transport, and clothing to that brief."],
100
+ material: ["Use rough masonry, imperfect glazing, worn painted timber, and matte clothing; build surface detail before aging the image.", "Separate sky, pale facades, dark coats, and street into readable monochrome values."],
101
+ camera: ["Use an eye-level locked tripod or a very slow mechanically plausible pan; let the world supply movement.", "Keep the 4:3 gate visible through composition; introduce only low-amplitude frame-wise registration variation."],
102
+ finishing: { grain: 0.16, grainSizePxAt1080: 1.4, halation: 0.04, gateWeavePxAt1080: 0.65, vignette: 0.18, saturation: 0, contrast: 1.08 },
103
+ avoid: ["Do not equate historical film with sepia, a black border, or repeated scratch overlays.", "Avoid glossy CGI, contemporary props, drone paths, fake archive dates, and unqualified documentary claims."],
104
+ acceptance: ["With texture disabled, the world and framing still read as the selected period.", "Moving subjects remain legible; grain changes per exposed frame, while scratches and weave do not overpower the action.", "Review full-resolution surfaces and motion; label the result as an authored reconstruction, not found footage."],
105
+ }),
106
+ profile({
107
+ id: "noir-35mm", name: "35 mm noir", family: "historical-film",
108
+ summary: "A dramatic monochrome city or interior organized by motivated pools of light, negative space, and strong depth.",
109
+ palette: { background: "#101316", ink: "#040608", paper: "#f1efe5", primary: "#aeb5b7", secondary: "#475159", accent: "#d6d4c9" },
110
+ cadence: cinema, delivery: academy,
111
+ shape: ["Use one strong silhouette, diagonal blocking, and a deep foreground-to-background relationship.", "Let window mullions, stairs, awnings, and practical lights produce motivated graphic shapes."],
112
+ material: ["Give masonry, wet pavement, wool, skin, and glass distinct tonal responses.", "Protect luminous highlights while holding enough shadow detail to read the principal action."],
113
+ camera: ["Use a deliberate dolly, fixed composition, or motivated low angle; decide the point of attention before moving.", "Let occlusion and lighting reveal the subject instead of adding random camera shake."],
114
+ finishing: { grain: 0.08, grainSizePxAt1080: 0.9, halation: 0.08, gateWeavePxAt1080: 0.15, vignette: 0.16, saturation: 0, contrast: 1.2 },
115
+ avoid: ["Do not crush every shadow or turn wet streets into mirror-like plastic.", "Avoid neon cyberpunk color, arbitrary fog, or broad blur that erases the staged action."],
116
+ acceptance: ["The eye finds the subject in a thumbnail and still sees the intended shadow detail at full size.", "Light sources explain the shadows and reflections; the complete motion preserves this logic."],
117
+ }),
118
+ profile({
119
+ id: "documentary-16mm", name: "16 mm documentary", family: "historical-film",
120
+ summary: "A patient observational view with plausible handheld weight, tactile surfaces, restrained color, and the texture of a small film gauge.",
121
+ palette: { background: "#343731", ink: "#202824", paper: "#d8d2af", primary: "#a7996e", secondary: "#66807c", accent: "#b86644" },
122
+ cadence: cinema, delivery: academy,
123
+ shape: ["Stage a specific ordinary activity with incidental depth and asymmetry.", "Resolve period street furniture, signage, vehicles, and building details before styling."],
124
+ material: ["Use natural light, restrained saturation, rough plaster, faded paint, and uneven reflectance.", "Keep highlight roll-off soft and retain believable material scale in close and distant surfaces."],
125
+ camera: ["Use a human-height view, slow reframing, and very small smooth operator drift.", "Pick a plausible lens and viewpoint; foreground parallax must follow camera translation."],
126
+ finishing: { grain: 0.13, grainSizePxAt1080: 1.3, halation: 0.07, gateWeavePxAt1080: 0.3, vignette: 0.12, saturation: 0.72, contrast: 0.96 },
127
+ avoid: ["Avoid pristine game-engine materials, evenly spaced procedural city blocks, and modern billboard typography in a period scene.", "Do not simulate handheld photography with frantic random movement or apply grain that swims independently of frames."],
128
+ acceptance: ["The untreated scene has convincing period objects, human scale, lighting, and material variation.", "Camera drift feels operated, the principal activity stays readable, and small highlights do not clip broadly."],
129
+ }),
130
+ profile({
131
+ id: "super8-color", name: "Super 8 color memory", family: "historical-film",
132
+ summary: "An intimate small-gauge color scene with warm highlights, gentle exposure breathing, and grounded everyday details.",
133
+ palette: { background: "#302c28", ink: "#263333", paper: "#ead5a4", primary: "#c59a60", secondary: "#698c80", accent: "#bd6650" },
134
+ cadence: { fps: 18, exposureFrames: 1, cameraExposureFrames: 1, shutterAngle: 180 }, delivery: academy,
135
+ shape: ["Favor intimate framing, simple gestures, and irregular lived-in surroundings.", "Choose one dominant warm/cool relationship and reserve bright color for a small focal object."],
136
+ material: ["Use fabric, foliage, skin, glass, and paint with visible but gentle surface detail.", "Keep a soft tonal shoulder and restrained shadow color; emulate a chosen look without claiming an exact film-stock match."],
137
+ camera: ["Keep movement light and human, with short settled moments between reframes.", "Author a complete first and last composition; use exposure breathing sparingly and continuously."],
138
+ finishing: { grain: 0.18, grainSizePxAt1080: 1.8, halation: 0.12, gateWeavePxAt1080: 0.55, vignette: 0.17, saturation: 0.84, contrast: 0.93 },
139
+ avoid: ["Avoid looping light leaks, damaged-film overlays in every frame, and extreme orange skin.", "Do not confuse a 4K delivery raster with invented high-frequency detail inside the small-gauge image."],
140
+ acceptance: ["The frame reads as an intimate photograph before grain is added.", "The 18 fps delivery has intentional motion cadence, retained subject detail, and no sudden exposure flashes."],
141
+ }),
142
+ profile({
143
+ id: "theatrical-cel", name: "Theatrical cel animation", family: "drawn-animation",
144
+ summary: "Late-1980s theatrical anime craft: dimensional layouts, intricate painted environments, decisive ink contours, and controlled cel shadows.",
145
+ palette: { background: "#101e2c", ink: "#111626", paper: "#c1d3d0", primary: "#396275", secondary: "#9b745e", accent: "#ec593f" },
146
+ cadence: onTwos, delivery: uhd,
147
+ shape: ["Construct convincing perspective and a clear silhouette with designed mechanical and architectural details.", "Separate painted background planes from animated foreground cels; reserve high-detail edges for the focal region."],
148
+ material: ["Use varied ink weight, two or three purposeful shadow shapes, and opaque painted color.", "Give background paintings richer texture and atmospheric depth while keeping moving cels clean."],
149
+ camera: ["Use a composed layout, measured multiplane translation, and a clear action axis.", "Hold character drawings on twos by default; reserve ones, smears, or impact drawings for specifically authored actions."],
150
+ finishing: { grain: 0.025, grainSizePxAt1080: 0.6, halation: 0.045, gateWeavePxAt1080: 0, vignette: 0.04, saturation: 0.95, contrast: 1.06 },
151
+ avoid: ["Avoid generic glossy 3D with a black outline, arbitrary neon, and uniformly detailed noise.", "Do not copy a film's characters, logos, or specific shot; translate the production principles into an original scene."],
152
+ acceptance: ["Silhouette, perspective, ink, cel-shadow grouping, and painted depth remain convincing without grain.", "Camera motion stays smooth while exposed character poses hold intentionally; motion does not slide joints or contours."],
153
+ }),
154
+ profile({
155
+ id: "watercolor-storybook", name: "Watercolor storybook", family: "drawn-animation",
156
+ summary: "A luminous painted illustration brought to life through selective movement, pigment pooling, and breathing paper space.",
157
+ palette: { background: "#eee8d6", ink: "#575b57", paper: "#fff8e6", primary: "#709487", secondary: "#8ba9b6", accent: "#c67757" },
158
+ cadence: onTwos, delivery: uhd,
159
+ shape: ["Compose broad washed silhouettes, irregular contours, and generous unpainted paper.", "Design a limited set of focal edges; allow distant forms to dissolve into larger color masses."],
160
+ material: ["Create translucent overlapping washes with edge pooling and subtle fixed paper tooth.", "Bind pigment texture to the painted object rather than regenerating all texture on every frame."],
161
+ camera: ["Use a quiet tableau or shallow multiplane drift.", "Animate one narrative gesture at a time with soft anticipation and a settled pause."],
162
+ finishing: { ...cleanFinish, grain: 0.025, grainSizePxAt1080: 0.6, saturation: 0.75 },
163
+ avoid: ["Avoid wet-looking plastic, uniform Gaussian blur, and a noise filter that obscures the drawing.", "Do not let paper texture crawl or painted edges shimmer between held drawings."],
164
+ acceptance: ["Paper, transparent wash, pooled pigment, and focal edge hierarchy are individually visible.", "Texture follows objects and motion leaves the composition calm and readable."],
165
+ }),
166
+ profile({
167
+ id: "ink-sketch", name: "Living ink sketch", family: "drawn-animation",
168
+ summary: "An expressive drawing with intentional line economy, selective hatching, and restrained redraw variation.",
169
+ palette: { background: "#eee9dc", ink: "#282c2d", paper: "#fbf5e5", primary: "#566565", secondary: "#acaaa0", accent: "#b86742" },
170
+ cadence: { ...onTwos, exposureFrames: 3 }, delivery: uhd,
171
+ shape: ["Establish contour, gesture, and hierarchy before adding crosshatching.", "Let open contours and deliberate omissions carry light; vary line weight by depth and emphasis."],
172
+ material: ["Use a small set of pressure-sensitive strokes with directionally consistent hatching.", "Register each redraw to the same underlying shape so line boil does not deform the subject."],
173
+ camera: ["Keep a stable drawing surface or make one legible pan across the sheet.", "Use held poses, brief drawing reveals, and small secondary gestures."],
174
+ finishing: { ...cleanFinish, saturation: 0.25 },
175
+ avoid: ["Avoid vector-perfect uniform outlines, evenly applied scribble noise, and permanent jitter of every point.", "Do not add decorative hatching over labels or important negative space."],
176
+ acceptance: ["The gesture reads at thumbnail size and line hierarchy remains clear at full resolution.", "Redraw variation is subtle and stable within each exposure; no unintended frame-to-frame crawling."],
177
+ }),
178
+ profile({
179
+ id: "rubber-hose", name: "Rubber-hose cartoon", family: "drawn-animation",
180
+ summary: "Elastic early-cartoon movement with clean silhouettes, rhythmic arcs, anticipation, and appealing graphic staging.",
181
+ palette: { background: "#d5cdbb", ink: "#252523", paper: "#f4eddc", primary: "#5f655d", secondary: "#96998a", accent: "#b28355" },
182
+ cadence: onTwos, delivery: academy,
183
+ shape: ["Build readable bean-shaped torsos, curved hose limbs, and large simple hands and feet.", "Stage clear silhouettes with ample space for arcs, squash, stretch, and overlap."],
184
+ material: ["Use opaque flat ink and warm paper-like backgrounds with sparse decorative detail.", "Keep line weight and facial registration stable as the body deforms."],
185
+ camera: ["Favor a fixed theatrical view and whole-body acting.", "Author anticipation, action, overshoot, and settle; preserve foot contact during planted poses."],
186
+ finishing: { grain: 0.04, grainSizePxAt1080: 0.8, halation: 0, gateWeavePxAt1080: 0.08, vignette: 0.06, saturation: 0.15, contrast: 1.05 },
187
+ avoid: ["Avoid sliding feet, unexplained limb length changes, and constant sine-wave movement without acting beats.", "Use original characters and props rather than familiar cartoon mascots."],
188
+ acceptance: ["Anticipation, elastic action, follow-through, and a settled pose can be identified in the complete clip.", "Feet and hands make intentional contact, and facial features remain readable through deformation."],
189
+ }),
190
+ profile({
191
+ id: "midcentury-limited", name: "Mid-century limited animation", family: "drawn-animation",
192
+ summary: "Modernist graphic staging with asymmetric shapes, flat color, spare backgrounds, and selective expressive movement.",
193
+ palette: { background: "#e7dab6", ink: "#263739", paper: "#f6edd5", primary: "#467f82", secondary: "#c79744", accent: "#cf6049" },
194
+ cadence: { ...onTwos, exposureFrames: 3 }, delivery: uhd,
195
+ shape: ["Design large asymmetric color blocks and stylized angular silhouettes.", "Use negative space and economical drawn details to direct attention."],
196
+ material: ["Favor matte gouache-like flat fields with slight fixed paper texture.", "Keep a few controlled overlaps and color relationships rather than realistic shading."],
197
+ camera: ["Treat the frame as a designed poster; use purposeful pans or held compositions.", "Animate the expressive part while holding the body or background; time actions around clear pauses."],
198
+ finishing: { ...cleanFinish, saturation: 0.88 },
199
+ avoid: ["Avoid gradients on every shape, generic geometric confetti, and continual motion of every object.", "Limited animation still needs poses, timing, and staging; do not substitute random bobbing."],
200
+ acceptance: ["The still frame reads as a coherent graphic design with a dominant focal shape.", "Selective motion clarifies the action and the held regions feel intentional."],
201
+ }),
202
+ profile({
203
+ id: "cut-paper", name: "Layered cut paper", family: "tactile-animation",
204
+ summary: "A handmade collage with meaningful overlaps, irregular cut edges, restrained paper relief, and mechanical articulation.",
205
+ palette: { background: "#dad2b9", ink: "#374a43", paper: "#f6edce", primary: "#597d66", secondary: "#d2a45b", accent: "#bf5d45" },
206
+ cadence: onTwos, delivery: uhd,
207
+ shape: ["Build a small number of nested, readable paper silhouettes and visible articulation pivots.", "Let overlapping layers explain the spatial construction."],
208
+ material: ["Use fixed fiber texture, small edge irregularities, and soft contact shadows proportional to paper thickness.", "Keep lighting direction consistent across every layer."],
209
+ camera: ["Use a mostly frontal rostrum view with restrained depth changes.", "Move paper pieces around authored pivots and stepped poses; keep texture attached to each piece."],
210
+ finishing: { ...cleanFinish, saturation: 0.84 },
211
+ avoid: ["Avoid inflated bevels, thick plastic shadows, sliding textures, and smooth stretchy limbs.", "Do not make the whole scene look like a flat vector image with one global drop shadow."],
212
+ acceptance: ["Separate paper layers, their contact shadows, and edge character are readable.", "Articulation, occlusion, and paper texture remain consistent throughout motion."],
213
+ }),
214
+ profile({
215
+ id: "stopmotion-clay", name: "Clay stop motion", family: "tactile-animation",
216
+ summary: "A miniature tactile set with shaped clay, practical lighting, intentional pose increments, and believable physical contact.",
217
+ palette: { background: "#283f47", ink: "#22333a", paper: "#edd6a9", primary: "#719b92", secondary: "#d8a451", accent: "#c16b54" },
218
+ cadence: { ...onTwos, cameraExposureFrames: 2 }, delivery: uhd,
219
+ shape: ["Design a strong hand-shaped silhouette with slightly irregular proportions and tangible seams.", "Stage a small set whose miniature scale is supported by prop size and limited depth."],
220
+ material: ["Use soft diffuse clay with sparse tool marks and local roughness variation.", "Create contact shadows and controlled depth of field; avoid uniformly shiny surfaces."],
221
+ camera: ["Use a physically plausible fixed miniature camera or slow incremented move.", "Author weight shifts and stepped poses, preserving planted contacts and deliberate squash."],
222
+ finishing: { ...cleanFinish, grain: 0.018, grainSizePxAt1080: 0.6, vignette: 0.05, saturation: 0.86 },
223
+ avoid: ["Avoid frictionless interpolation, floating props, animated texture noise, and glossy toy materials.", "Do not call procedural wobble a physical simulation or claim a practical shoot."],
224
+ acceptance: ["Clay surface, contact, miniature lighting, and stepped posing all contribute to the treatment.", "No planted foot slides, prop intersections, or focus changes conceal the principal action."],
225
+ }),
226
+ profile({
227
+ id: "pixel-art", name: "Authored pixel art", family: "digital-animation",
228
+ summary: "A deliberately drawn low-resolution world with a limited palette, designed pixel clusters, discrete poses, and exact integer enlargement.",
229
+ palette: { background: "#182633", ink: "#111c2d", paper: "#e6dcc1", primary: "#5c9c83", secondary: "#d2aa63", accent: "#d96658" },
230
+ cadence: { ...onTwos, cameraExposureFrames: 2 },
231
+ delivery: { ...uhd, sampling: "nearest", logicalGrid: { width: 320, height: 180 } },
232
+ shape: ["Draw on the declared 320 by 180 logical grid with readable clusters and deliberate stair-step diagonals.", "Separate foreground sprites, midground tiles, and distant parallax planes with value and palette."],
233
+ material: ["Use a bounded palette with authored highlight and shadow ramps.", "Place dithering only where it explains a gradient or material; preserve crisp one-pixel accents."],
234
+ camera: ["Quantize sprite positions and camera translation to logical pixels.", "Use authored pose changes and integer parallax offsets; export with a single nearest-neighbor integer scale."],
235
+ finishing: cleanFinish,
236
+ avoid: ["Avoid anti-aliasing, postprocess blur, noninteger scaling, smooth subpixel translation, and a pixelation filter over an unrelated image.", "Do not add film grain or screen simulation by default; those are separate authored treatments."],
237
+ acceptance: ["Each logical pixel maps to an equal, crisp 12 by 12 output block in the recommended 3840 by 2160 delivery.", "Palette, sprite clusters, occlusion, and pose changes read clearly throughout the full clip."],
238
+ }),
239
+ profile({
240
+ id: "rotoscope", name: "Painterly rotoscope", family: "drawn-animation",
241
+ summary: "Naturalistic human movement translated into stable painted masses, expressive contour, and deliberately simplified light.",
242
+ palette: { background: "#243747", ink: "#172934", paper: "#e7d8bc", primary: "#798b82", secondary: "#b08a69", accent: "#dc8561" },
243
+ cadence: onTwos, delivery: uhd,
244
+ shape: ["Preserve anatomical landmarks, weight, and the readable gesture from an authorized reference or deliberately authored motion.", "Simplify the figure into consistent large painted masses instead of tracing every visible texture."],
245
+ material: ["Keep controlled contour variation and two or three light groups attached to the subject.", "Let brush texture follow body surfaces and maintain facial landmark identity."],
246
+ camera: ["Use a stable action axis and clear full-body or close gesture framing.", "Retain natural timing and planted contact; author redraw variation per exposure rather than per display refresh."],
247
+ finishing: { ...cleanFinish, grain: 0.012, grainSizePxAt1080: 0.5, saturation: 0.78 },
248
+ avoid: ["Avoid edge-detection filters, flickering facial features, crawling paint, and rubbery anatomical drift.", "Do not describe a scene as reference-traced unless an actual authorized reference was used."],
249
+ acceptance: ["Weight shifts and gestures remain plausible, with stable anatomy and consistent light grouping.", "Contours and texture hold coherently between exposures without accidental shimmer."],
250
+ }),
251
+ profile({
252
+ id: "engraving", name: "Engraved illustration", family: "drawn-animation",
253
+ summary: "A precise monochrome illustration whose curved hatching, contour hierarchy, and open paper explain form.",
254
+ palette: { background: "#e8dfc8", ink: "#25322f", paper: "#f6ecd2", primary: "#52645b", secondary: "#a5ab92", accent: "#9b653d" },
255
+ cadence: cinema, delivery: uhd,
256
+ shape: ["Use a clear structural outline and reserve fine marks for material and curvature.", "Organize hatching into form-following bands with intentional light gaps."],
257
+ material: ["Render crisp fine ink with sparse paper texture and deliberate crosshatch density.", "Keep line spacing wide enough to survive the actual delivery raster and video encoding."],
258
+ camera: ["Use a slow explanatory reveal, rotation, or camera move with stable line registration.", "If the underlying form moves, move its hatch coordinates with it rather than sampling new screen-space noise."],
259
+ finishing: { ...cleanFinish, saturation: 0.08 },
260
+ avoid: ["Avoid uniform screen-space hatch overlays, moire, crushed dense line fields, and illegible decorative labels.", "Do not present an artistic mechanical drawing as dimensionally verified engineering."],
261
+ acceptance: ["Hatch direction describes form and fine lines remain distinct in a full-resolution delivery frame.", "Motion does not create moire or crawl that overwhelms the subject."],
262
+ }),
263
+ profile({
264
+ id: "math-explainer", name: "Mathematical explanation", family: "explanation",
265
+ summary: "A lucid visual argument built from persistent objects, purposeful color, geometric construction, and time to understand each step.",
266
+ palette: { background: "#101c2a", ink: "#e0e8ed", paper: "#f4f1de", primary: "#59b6d5", secondary: "#e4bf63", accent: "#df7e82" },
267
+ cadence: { fps: 30, exposureFrames: 1, cameraExposureFrames: 1, shutterAngle: 0 }, delivery: uhd,
268
+ shape: ["Choose one mathematical claim; construct the relevant objects before transforming or comparing them.", "Use persistent object identities, meaningful axes, clear labels, and whitespace around the argument."],
269
+ material: ["Use clean vector strokes, restrained fills, and consistent notation with semantic colors.", "Keep line weight, label size, and contrast readable at the intended viewing size."],
270
+ camera: ["Hold a stable coordinate system unless a move directly explains the mathematics.", "Sequence setup, construction, transformation, comparison, and conclusion with deliberate pauses."],
271
+ finishing: cleanFinish,
272
+ avoid: ["Avoid decorative equations, unsupported numeric claims, arbitrary camera spins, and simultaneous unrelated reveals.", "Do not substitute a familiar presenter's mascot or identity for the clarity of the explanation."],
273
+ acceptance: ["The illustrated relationship can be checked from the authored geometry or data.", "Each color keeps its meaning, labels remain legible, and each logical step has a readable settled state."],
274
+ }),
275
+ profile({
276
+ id: "isometric-design", name: "Isometric design study", family: "digital-animation",
277
+ summary: "An orthographic object or system study with consistent construction, restrained materials, and diagram-like spatial clarity.",
278
+ palette: { background: "#e6e5db", ink: "#2c4147", paper: "#f7f2df", primary: "#7b9c93", secondary: "#d1ad66", accent: "#c9674d" },
279
+ cadence: cinema, delivery: uhd,
280
+ shape: ["Use one consistent orthographic projection and a clear assembled silhouette.", "Make repeated parts and physical relationships coherent; reserve detail for the function being explained."],
281
+ material: ["Assign a small palette of believable matte, metal, glass, and rubber responses where appropriate.", "Use soft contact shadows and explicit edges to separate adjacent parts."],
282
+ camera: ["Hold the projection and composition during assembly or an exploded view.", "Move components along meaningful axes and include a settled assembled and separated state."],
283
+ finishing: cleanFinish,
284
+ avoid: ["Avoid inconsistent vanishing points, floating unconnected parts, excessive bevels, and candy-like materials on every surface.", "Do not imply fabrication readiness without dimensional and engineering validation."],
285
+ acceptance: ["Part relationships remain legible during every movement, with no accidental intersection or cropped component.", "The final composition has coherent scale, contact, projection, and material distinction."],
286
+ }),
287
+ profile({
288
+ id: "clean-motion", name: "Editorial motion graphics", family: "digital-animation",
289
+ summary: "Precise typography, controlled hierarchy, and economical choreography organized around a clear editorial message.",
290
+ palette: { background: "#15232e", ink: "#f2eee1", paper: "#f8f4e9", primary: "#7cafb1", secondary: "#d4b87b", accent: "#dc795b" },
291
+ cadence: { fps: 30, exposureFrames: 1, cameraExposureFrames: 1, shutterAngle: 0 }, delivery: uhd,
292
+ shape: ["Establish one typographic hierarchy, a consistent grid, and a small set of geometric devices.", "Use exact supplied copy and arrange it to read in the intended viewing order."],
293
+ material: ["Use restrained flat color, carefully weighted rules, and crisp type at the final output size.", "Reserve texture and gradients for an authored editorial purpose."],
294
+ camera: ["Treat the screen as a composition; use position, scale, and opacity only where they clarify hierarchy.", "Separate entrance, readable hold, transition, and exit; stagger related objects rather than animating everything at once."],
295
+ finishing: cleanFinish,
296
+ avoid: ["Avoid decorative dashboard cards, indiscriminate bounce, tiny type, and transitions that obscure supplied text.", "Do not introduce slogans, data, or claims absent from the brief."],
297
+ acceptance: ["Exact copy reads comfortably in the delivery frame and remains visible long enough to understand.", "Every move has a purpose, spacing stays controlled, and the first and last frames are intentionally composed."],
298
+ }),
299
+ ])
300
+
301
+ const profilesById = new Map<VisualStyleId, VisualStyleProfile>(VISUAL_STYLE_PROFILES.map(item => [item.id, item]))
302
+
303
+ /** Returns immutable direction. It does not render, dispatch a model, or rewrite a source. */
304
+ export function getVisualStyleProfile(id: unknown): VisualStyleProfile {
305
+ return profilesById.get(VisualStyleIdSchema.parse(id))!
306
+ }
307
+
308
+ /** Explicit direction for an author or a model prompt; applying it remains a caller decision. */
309
+ export function createVisualStyleDirection(id: unknown): string {
310
+ const style = getVisualStyleProfile(id)
311
+ const grid = style.delivery.logicalGrid
312
+ return [
313
+ `${style.name}: ${style.summary}`,
314
+ `Palette: ${Object.entries(style.palette).map(([role, color]) => `${role} ${color}`).join(", ")}.`,
315
+ `Shape and staging: ${style.shape.join(" ")}`,
316
+ `Materials and light: ${style.material.join(" ")}`,
317
+ `Camera and motion: ${style.camera.join(" ")}`,
318
+ `Cadence: ${style.cadence.fps} fps; subject exposure ${style.cadence.exposureFrames} frame(s); camera exposure ${style.cadence.cameraExposureFrames} frame(s); shutter angle ${style.cadence.shutterAngle} degrees.`,
319
+ `Finishing targets: ${Object.entries(style.finishing).map(([name, value]) => `${name}=${value}`).join(", ")}. These are authoring controls, not a film-stock calibration.`,
320
+ `Recommended delivery: ${style.delivery.width} by ${style.delivery.height}, ${style.delivery.sampling} sampling.${grid === undefined ? "" : ` Author at ${grid.width} by ${grid.height} logical pixels and enlarge by exactly ${style.delivery.width / grid.width}.`}`,
321
+ `Avoid: ${style.avoid.join(" ")}`,
322
+ `Review actual frames and motion: ${style.acceptance.join(" ")}`,
323
+ ].join("\n")
324
+ }
325
+
326
+ const MAX_STYLE_TIME_US = 3_600_000_000
327
+
328
+ function checkInteger(value: number, name: string, maximum: number): void {
329
+ if (!Number.isSafeInteger(value) || value < 0 || value > maximum) {
330
+ throw new RangeError(`${name} must be an integer from 0 through ${maximum}.`)
331
+ }
332
+ }
333
+
334
+ export type VisualStyleExposure = Readonly<{
335
+ frame: number
336
+ exposureIndex: number
337
+ exposureTimeUs: number
338
+ cameraTimeUs: number
339
+ }>
340
+
341
+ /** Independent subject and camera holds on an absolute microsecond clock. No state is accumulated. */
342
+ export function sampleVisualStyleExposure(timeUs: number, id: unknown): VisualStyleExposure {
343
+ checkInteger(timeUs, "timeUs", MAX_STYLE_TIME_US)
344
+ const { fps, exposureFrames, cameraExposureFrames } = getVisualStyleProfile(id).cadence
345
+ const frame = Math.floor(timeUs * fps / 1_000_000) || 0
346
+ const exposureIndex = Math.floor(frame / exposureFrames)
347
+ const cameraFrame = Math.floor(frame / cameraExposureFrames) * cameraExposureFrames
348
+ return Object.freeze({
349
+ frame,
350
+ exposureIndex,
351
+ // Ceil selects the first integer microsecond inside the held frame. Flooring
352
+ // a nonintegral frame boundary would seek back into the previous exposure.
353
+ exposureTimeUs: Math.ceil(exposureIndex * exposureFrames * 1_000_000 / fps),
354
+ cameraTimeUs: Math.ceil(cameraFrame * 1_000_000 / fps),
355
+ })
356
+ }
357
+
358
+ /** Versioned keyed variation; use an exposure index for redraws, or a frame index for film grain. */
359
+ export const VISUAL_STYLE_VARIATION_ALGORITHM = "slopcamera.visual-style.fnv1a-avalanche-v1" as const
360
+
361
+ export function visualStyleFrameVariation(seed: number, frame: number, channel: string): number {
362
+ checkInteger(seed, "seed", 0xffff_ffff)
363
+ checkInteger(frame, "frame", 432_000)
364
+ if (typeof channel !== "string" || channel.length < 1 || channel.length > 128
365
+ || !/^[a-zA-Z0-9][a-zA-Z0-9._:/-]*$/u.test(channel)) {
366
+ throw new TypeError("channel must be 1–128 ASCII letters, digits, dots, underscores, colons, slashes, or hyphens, beginning with a letter or digit.")
367
+ }
368
+ let value = (2166136261 ^ seed) >>> 0
369
+ for (let index = 0; index < 4; index += 1) {
370
+ value = Math.imul(value ^ ((frame >>> (index * 8)) & 255), 16777619) >>> 0
371
+ }
372
+ for (let index = 0; index < channel.length; index += 1) {
373
+ value = Math.imul(value ^ channel.charCodeAt(index), 16777619) >>> 0
374
+ }
375
+ value = Math.imul(value ^ (value >>> 16), 0x85ebca6b) >>> 0
376
+ value = Math.imul(value ^ (value >>> 13), 0xc2b2ae35) >>> 0
377
+ return ((value ^ (value >>> 16)) >>> 0) / 0x1_0000_0000
378
+ }