@hraness/slopcamera 3.3.6 → 3.4.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/README.md +6 -4
- package/apps/desktop/application/operation.ts +1 -1
- package/apps/desktop/cli/capability-manifest.ts +13 -0
- package/apps/desktop/cli/commands.ts +1 -1
- package/apps/desktop/cli/help.ts +12 -1
- package/apps/desktop/cli/html-overlay-renderer.ts +16 -0
- package/apps/desktop/cli/portable-surface.ts +2 -0
- package/apps/desktop/code/public.ts +1 -0
- package/apps/desktop/core/visual-style-look.ts +34 -0
- package/apps/desktop/dist/cli/main.js +132 -108
- package/apps/desktop/html-overlay/runtime.ts +15 -0
- package/dist/cli.js +6 -2
- package/dist/{index-10bz03js.js → index-abnk4h9c.js} +3 -2
- package/dist/index.js +1 -1
- package/docs/README.md +1 -0
- package/examples/style-portfolio/ANIMATION-STUDIES.md +34 -0
- package/examples/style-portfolio/FILM-PLATE.md +19 -0
- package/examples/style-portfolio/README.md +137 -0
- package/examples/style-portfolio/animation-studies.html +597 -0
- package/examples/style-portfolio/assets/instrument-serif-OFL.txt +93 -0
- package/examples/style-portfolio/assets/instrument-serif-latin-400.woff2 +0 -0
- package/examples/style-portfolio/build-gallery.ts +403 -0
- package/examples/style-portfolio/film-plate.html +76 -0
- package/examples/style-portfolio/finish-film.ts +152 -0
- package/examples/style-portfolio/market-street/README.md +43 -0
- package/examples/style-portfolio/market-street/mesh.py +145 -0
- package/examples/style-portfolio/market-street/scene.py +760 -0
- package/examples/style-portfolio/market-street/source.json +1 -0
- package/examples/style-portfolio/market-street/street_data.py +72 -0
- package/examples/style-portfolio/render-film-plate.ts +128 -0
- package/examples/style-portfolio/render.ts +89 -0
- package/examples/style-portfolio/verify-animation-source.mjs +94 -0
- package/package.json +11 -2
- package/skills/slopcamera/SKILL.md +9 -0
- package/skills/slopcamera/references/gateway-media.md +5 -0
- package/skills/slopcamera/references/install.md +1 -1
- package/skills/slopcamera/references/music-video.md +5 -0
- package/skills/slopcamera/references/native-studio.md +4 -0
- package/skills/slopcamera/references/visual-style-direction.md +143 -0
- package/src/cli.ts +28 -0
- package/src/index.ts +1 -0
- package/src/version.ts +1 -1
- package/src/visual-style.ts +375 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"engine":"blender","entrypoint":{"kind":"python","path":"scene.py"},"files":["mesh.py","scene.py","street_data.py"]}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Evidence for the historical interpretation, separate from art direction.
|
|
2
|
+
|
|
3
|
+
The scene is a compressed invented block inspired by the 1906 film. Dimensions,
|
|
4
|
+
shop occupants, figure performances, and most facades are authored estimates.
|
|
5
|
+
No building footprint is claimed to have been digitized from a Sanborn plate.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
SOURCES = [
|
|
9
|
+
{
|
|
10
|
+
'id': 'loc-film',
|
|
11
|
+
'url': 'https://www.loc.gov/item/00694408/',
|
|
12
|
+
'title': 'A trip down Market Street before the fire',
|
|
13
|
+
'institution': 'Library of Congress',
|
|
14
|
+
'supports': ['1906 setting', 'cable-car viewpoint toward Ferry Building',
|
|
15
|
+
'black-and-white 35 mm source', 'mixed street traffic',
|
|
16
|
+
'damp roadway visible in the film'],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
'id': 'sfmta-conversion',
|
|
20
|
+
'url': 'https://www.sfmta.com/blog/how-1906-earthquake-transformed-our-public-transit-system',
|
|
21
|
+
'title': 'How the 1906 Earthquake Transformed Our Public Transit System',
|
|
22
|
+
'institution': 'San Francisco Municipal Transportation Agency',
|
|
23
|
+
'supports': ['electric conversion of Market Street followed the earthquake'],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
'id': 'sfplanning-lamps',
|
|
27
|
+
'url': 'https://commissions.sfplanning.org/hpcpackets/2014.794A.pdf',
|
|
28
|
+
'title': 'Path of Gold Light Standards landmark case report',
|
|
29
|
+
'institution': 'San Francisco Planning Department',
|
|
30
|
+
'supports': ['Path of Gold bases date to 1908, globe tops to 1916'],
|
|
31
|
+
},
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
INTERPRETATION = {
|
|
35
|
+
'depictedPeriod': 'San Francisco, before the April 1906 earthquake',
|
|
36
|
+
'kind': 'procedural historical interpretation',
|
|
37
|
+
'historicalReconstruction': False,
|
|
38
|
+
'authoredEstimates': [
|
|
39
|
+
'All metric geometry, street compression, camera lens and height',
|
|
40
|
+
'Facades, shop names, costumes, individual vehicles and performances',
|
|
41
|
+
'Monochrome material response, weather, lighting and capture cadence',
|
|
42
|
+
],
|
|
43
|
+
'excludedAnachronisms': ['Path of Gold globes', 'Market Street traction catenary'],
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# +Y runs inland. The camera travels toward -Y. The shot faces the Ferry,
|
|
47
|
+
# where screen-left is +X and screen-right is -X. Parcel coordinates are
|
|
48
|
+
# composition coordinates, not surveyed geographic coordinates.
|
|
49
|
+
ROAD_WIDTH = 26.0
|
|
50
|
+
SIDEWALK_WIDTH = 5.3
|
|
51
|
+
FERRY_Y = -12.0
|
|
52
|
+
CAPTURE_FPS = 16
|
|
53
|
+
DURATION_SECONDS = 6.0
|
|
54
|
+
|
|
55
|
+
# Art-directed frontage parcels with separate setbacks at side-street cuts.
|
|
56
|
+
# Values are (center Y, frontage, height, bays, material, roof treatment).
|
|
57
|
+
PARCELS = {
|
|
58
|
+
-1: [(12, 22, 18, 6, 'stone', 'flat'),
|
|
59
|
+
(40, 23, 25, 6, 'brick', 'pediment'),
|
|
60
|
+
(70, 25, 20, 7, 'plaster', 'flat'),
|
|
61
|
+
(105, 30, 31, 8, 'brick_light', 'mansard'),
|
|
62
|
+
(146, 35, 24, 9, 'stone', 'pediment'),
|
|
63
|
+
(187, 31, 36, 8, 'brick', 'flat'),
|
|
64
|
+
(224, 30, 23, 8, 'plaster', 'flat')],
|
|
65
|
+
1: [(14, 24, 21, 7, 'brick_light', 'flat'),
|
|
66
|
+
(46, 25, 18, 7, 'stone', 'flat'),
|
|
67
|
+
(77, 27, 30, 8, 'brick', 'pediment'),
|
|
68
|
+
(111, 24, 22, 6, 'stone', 'flat'),
|
|
69
|
+
(148, 35, 33, 9, 'brick_light', 'mansard'),
|
|
70
|
+
(190, 34, 25, 9, 'plaster', 'pediment'),
|
|
71
|
+
(230, 33, 31, 9, 'brick', 'flat')],
|
|
72
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/** Retain one local photographic plate and render a bounded single-image transfer study. */
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { mkdir, open, realpath, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import sharp from "sharp";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { HtmlSceneInputSchema } from "../../apps/desktop/html-overlay/scene";
|
|
9
|
+
|
|
10
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
11
|
+
const maxBytes = 32 * 1024 * 1024;
|
|
12
|
+
const maxPixels = 33_000_000;
|
|
13
|
+
const args = process.argv.slice(2);
|
|
14
|
+
const options = new Map<string, string>();
|
|
15
|
+
let dryRun = false;
|
|
16
|
+
for (let index = 0; index < args.length; index++) {
|
|
17
|
+
const arg = args[index]!;
|
|
18
|
+
if (arg === "--dry-run" && !dryRun) dryRun = true;
|
|
19
|
+
else if (["--image", "--run", "--width", "--height"].includes(arg) && args[index + 1] !== undefined) {
|
|
20
|
+
if (options.has(arg)) throw new Error(`Duplicate option ${arg}.`);
|
|
21
|
+
options.set(arg, args[++index]!);
|
|
22
|
+
} else throw new Error(`Unknown, duplicate, or incomplete option ${arg}.`);
|
|
23
|
+
}
|
|
24
|
+
const imageArg = options.get("--image");
|
|
25
|
+
const run = options.get("--run");
|
|
26
|
+
if (!imageArg || !run) throw new Error("Usage: bun examples/style-portfolio/render-film-plate.ts --image <local.png|local.jpg> --run <new-id> [--width <maximum>] [--height <maximum>] [--dry-run]");
|
|
27
|
+
if (imageArg.length > 4096 || /[\0\r\n]/u.test(imageArg) || /^[a-z][a-z0-9+.-]*:\/\//iu.test(imageArg)) throw new Error("--image must be an explicit bounded local path, not a URL.");
|
|
28
|
+
if (!/^[a-z0-9][a-z0-9-]{0,63}$/u.test(run)) throw new Error("--run must be a new short lowercase filename token.");
|
|
29
|
+
function bound(name: string, fallback: number) {
|
|
30
|
+
const value = options.get(name);
|
|
31
|
+
if (value === undefined) return fallback;
|
|
32
|
+
if (!/^[0-9]{1,4}$/u.test(value)) throw new Error(`${name} must be an integer in [16,8192].`);
|
|
33
|
+
const parsed = Number(value);
|
|
34
|
+
if (parsed < 16 || parsed > 8192) throw new Error(`${name} must be an integer in [16,8192].`);
|
|
35
|
+
return parsed;
|
|
36
|
+
}
|
|
37
|
+
const hasSize = options.has("--width") || options.has("--height");
|
|
38
|
+
const maxWidth = bound("--width", hasSize ? 8192 : 1440);
|
|
39
|
+
const maxHeight = bound("--height", hasSize ? 8192 : 1080);
|
|
40
|
+
const input = await realpath(resolve(root, imageArg));
|
|
41
|
+
// Read from one handle with a fixed byte ceiling, then bind the retained copy.
|
|
42
|
+
const handle = await open(input, "r");
|
|
43
|
+
let bytes: Buffer;
|
|
44
|
+
try {
|
|
45
|
+
const before = await handle.stat();
|
|
46
|
+
if (!before.isFile() || !Number.isSafeInteger(before.size) || before.size < 1 || before.size > maxBytes) throw new Error("Use a regular PNG/JPEG file no larger than 32 MiB.");
|
|
47
|
+
bytes = Buffer.alloc(before.size);
|
|
48
|
+
let position = 0;
|
|
49
|
+
while (position < bytes.length) {
|
|
50
|
+
const result = await handle.read(bytes, position, bytes.length - position, position);
|
|
51
|
+
if (result.bytesRead === 0) throw new Error("The plate changed while it was read.");
|
|
52
|
+
position += result.bytesRead;
|
|
53
|
+
}
|
|
54
|
+
const extra = await handle.read(Buffer.alloc(1), 0, 1, position);
|
|
55
|
+
const after = await handle.stat();
|
|
56
|
+
if (extra.bytesRead !== 0 || after.size !== before.size || after.mtimeMs !== before.mtimeMs || after.ino !== before.ino || after.dev !== before.dev) throw new Error("The plate changed while it was read.");
|
|
57
|
+
} finally { await handle.close(); }
|
|
58
|
+
const metadata = await sharp(bytes, { limitInputPixels: maxPixels, failOn: "error", animated: true }).metadata();
|
|
59
|
+
if (!["png", "jpeg"].includes(metadata.format ?? "") || (metadata.pages ?? 1) !== 1) throw new Error("Use one static PNG or JPEG photograph.");
|
|
60
|
+
const sourceWidth = metadata.autoOrient.width;
|
|
61
|
+
const sourceHeight = metadata.autoOrient.height;
|
|
62
|
+
if (!Number.isInteger(sourceWidth) || !Number.isInteger(sourceHeight) || sourceWidth < 16 || sourceHeight < 16 || sourceWidth * sourceHeight > maxPixels) throw new Error("The decoded plate must be at least 16 pixels per side and at most 33 megapixels.");
|
|
63
|
+
const scale = Math.min(1, maxWidth / sourceWidth, maxHeight / sourceHeight);
|
|
64
|
+
const width = Math.floor(sourceWidth * scale / 2) * 2;
|
|
65
|
+
const height = Math.floor(sourceHeight * scale / 2) * 2;
|
|
66
|
+
if (width < 2 || height < 2 || width * height > maxPixels) throw new Error("The requested aspect ratio does not fit a bounded even raster.");
|
|
67
|
+
const sourceSha256 = createHash("sha256").update(bytes).digest("hex");
|
|
68
|
+
const extension = metadata.format === "png" ? "png" : "jpg";
|
|
69
|
+
const outputRoot = join(root, "artifacts/style-portfolio/film-plate");
|
|
70
|
+
await mkdir(outputRoot, { recursive: true });
|
|
71
|
+
if (await realpath(outputRoot) !== outputRoot) throw new Error("The output directory and its parents must not be symlinks.");
|
|
72
|
+
const output = join(outputRoot, run);
|
|
73
|
+
// The run directory is the exclusive claim. A previous or ambiguous run is
|
|
74
|
+
// never overwritten or automatically resumed, even when only planning failed.
|
|
75
|
+
try { await mkdir(output); }
|
|
76
|
+
catch (error) {
|
|
77
|
+
if ((error as NodeJS.ErrnoException).code === "EEXIST") throw new Error(`Run ${run} already exists. Inspect its intent, log, and host receipts; no overwrite or retry was made.`);
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
const retainedPlate = join(output, `plate.${extension}`);
|
|
81
|
+
await writeFile(retainedPlate, bytes, { flag: "wx", mode: 0o600 });
|
|
82
|
+
const retainedRelative = relative(root, retainedPlate).split(sep).join("/");
|
|
83
|
+
if (isAbsolute(retainedRelative) || retainedRelative.startsWith("../")) throw new Error("The retained plate must stay inside the repository artifacts directory.");
|
|
84
|
+
const source = HtmlSceneInputSchema.parse({
|
|
85
|
+
kind: "slopcamera.html-scene", schemaVersion: 1,
|
|
86
|
+
name: "Single-image film-transfer study",
|
|
87
|
+
document: { path: "examples/style-portfolio/film-plate.html" },
|
|
88
|
+
canvas: { width, height, deviceScaleFactor: 1 },
|
|
89
|
+
timing: { durationUs: 6_000_000, fps: 16 },
|
|
90
|
+
seed: 19060414, background: "#000000", libraries: [],
|
|
91
|
+
parameters: { kind: "single-image-film-transfer", sourceWidth, sourceHeight, sourceSha256 },
|
|
92
|
+
resources: [{ name: "plate", path: retainedRelative, urlPath: `assets/plate.${extension}`, mediaType: metadata.format === "png" ? "image/png" : "image/jpeg" }],
|
|
93
|
+
});
|
|
94
|
+
const requestPath = join(output, "scene.json");
|
|
95
|
+
const intentPath = join(output, "intent.json");
|
|
96
|
+
await writeFile(requestPath, JSON.stringify(source, null, 2) + "\n", { flag: "wx" });
|
|
97
|
+
const intent = {
|
|
98
|
+
kind: "slopcamera.single-image-film-transfer-intent", schemaVersion: 1,
|
|
99
|
+
mode: dryRun ? "plan" : "render", input, sourceSha256, sourceBytes: bytes.length,
|
|
100
|
+
sourceDimensions: { width: sourceWidth, height: sourceHeight }, outputDimensions: { width, height },
|
|
101
|
+
requestedMaximum: { width: maxWidth, height: maxHeight }, retainedPlate: retainedRelative,
|
|
102
|
+
requestPath, timing: source.timing,
|
|
103
|
+
limitations: ["One photographic image with optical movement; no subject animation.", "Delivery never exceeds input dimensions. A small optical crop is resampled within that raster.", "Grain and diffusion are a separate explicit finishing operation."],
|
|
104
|
+
};
|
|
105
|
+
await writeFile(intentPath, JSON.stringify(intent, null, 2) + "\n", { flag: "wx" });
|
|
106
|
+
const argv = [process.execPath, join(root, "apps/desktop/dist/cli/main.js"), "html", "render", "--input", requestPath, "--json"];
|
|
107
|
+
if (dryRun) argv.push("--dry-run");
|
|
108
|
+
const logPath = join(output, "render.log");
|
|
109
|
+
const log = await open(logPath, "wx", 0o600);
|
|
110
|
+
let code: number;
|
|
111
|
+
let stdout: string;
|
|
112
|
+
try {
|
|
113
|
+
const child = Bun.spawn(argv, { cwd: root, env: process.env, stdout: "pipe", stderr: log.fd });
|
|
114
|
+
const result = await Promise.all([child.exited, new Response(child.stdout).text()]);
|
|
115
|
+
[code, stdout] = result;
|
|
116
|
+
} finally { await log.close(); }
|
|
117
|
+
if (code !== 0) {
|
|
118
|
+
await writeFile(join(output, "failed.json"), JSON.stringify({ code, stdout }, null, 2) + "\n", { flag: "wx" });
|
|
119
|
+
throw new Error(`Slopcamera ${dryRun ? "plan" : "render"} failed (${code}); inspect ${logPath} and retained host receipts. No retry was made.`);
|
|
120
|
+
}
|
|
121
|
+
const result: unknown = JSON.parse(stdout);
|
|
122
|
+
const resultPath = join(output, dryRun ? "plan.json" : "result.json");
|
|
123
|
+
await writeFile(resultPath, JSON.stringify(result, null, 2) + "\n", { flag: "wx" });
|
|
124
|
+
if (!dryRun) {
|
|
125
|
+
const completion = z.object({ kind: z.literal("slopcamera.html-scene-export"), verification: z.object({ width: z.number(), height: z.number(), frameCount: z.number(), durationUs: z.number() }) }).parse(result);
|
|
126
|
+
if (completion.verification.width !== width || completion.verification.height !== height || completion.verification.frameCount !== 96 || completion.verification.durationUs !== 6_000_000) throw new Error("Rendered dimensions or timing differ from the admitted request; retain this result for review.");
|
|
127
|
+
}
|
|
128
|
+
console.log(JSON.stringify({ state: dryRun ? "planned" : "rendered", interpretation: "single-image-film-transfer", sourceDimensions: intent.sourceDimensions, dimensions: { width, height }, fps: 16, durationSeconds: 6, sourceSha256, resultPath, intentPath }));
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/** Render original style studies through the canonical Slopcamera host. */
|
|
2
|
+
import { mkdir, open, realpath, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { getVisualStyleProfile } from "../../src/visual-style";
|
|
6
|
+
|
|
7
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
8
|
+
const studies = ["theatrical-cel", "watercolor-storybook", "pixel-art", "math-explainer",
|
|
9
|
+
"midcentury-limited", "rubber-hose", "cut-paper", "stopmotion-clay", "engraving",
|
|
10
|
+
"ink-sketch", "rotoscope", "clean-motion"] as const;
|
|
11
|
+
export async function renderStudies(args: readonly string[], workspaceRoot = root, log: (message: string) => void = console.log): Promise<void> {
|
|
12
|
+
const flagNames = new Set(["--style", "--width", "--offset", "--run"]);
|
|
13
|
+
const options = new Map<string, string>();
|
|
14
|
+
const flags = new Set<string>();
|
|
15
|
+
for (let i = 0; i < args.length; i++) {
|
|
16
|
+
const arg = args[i]!;
|
|
17
|
+
if (arg === "--all" || arg === "--still" || arg === "--dry-run") {
|
|
18
|
+
if (flags.has(arg)) throw new Error(`Duplicate flag ${arg}`);
|
|
19
|
+
flags.add(arg);
|
|
20
|
+
} else if (flagNames.has(arg) && args[i + 1] !== undefined) {
|
|
21
|
+
if (options.has(arg)) throw new Error(`Duplicate option ${arg}`);
|
|
22
|
+
options.set(arg, args[++i]!);
|
|
23
|
+
} else throw new Error(`Unknown or incomplete option ${arg}`);
|
|
24
|
+
}
|
|
25
|
+
const still = flags.has("--still");
|
|
26
|
+
const width = Number(options.get("--width") ?? 3840);
|
|
27
|
+
const offset = Number(options.get("--offset") ?? (still ? 3 : 0));
|
|
28
|
+
const run = options.get("--run") ?? (still ? "stills" : "films");
|
|
29
|
+
if (!/^[a-z0-9][a-z0-9-]{0,63}$/u.test(run)) throw new Error("--run must be a short lowercase filename token.");
|
|
30
|
+
if (![1280, 1920, 2560, 3840].includes(width)) throw new Error("--width must be 1280, 1920, 2560, or 3840.");
|
|
31
|
+
if (!Number.isFinite(offset) || offset < 0 || offset >= 6 || (!still && offset !== 0)) throw new Error("--offset must be in [0,6), and requires --still.");
|
|
32
|
+
const requested = options.get("--style");
|
|
33
|
+
if ((requested === undefined) === !flags.has("--all")) throw new Error("Choose exactly one of --style <id> or --all.");
|
|
34
|
+
const selected = requested === undefined ? studies : studies.filter(id => id === requested);
|
|
35
|
+
if (selected.length === 0) throw new Error(`Study not found. Choose: ${studies.join(", ")}`);
|
|
36
|
+
const canonicalRoot = await realpath(workspaceRoot);
|
|
37
|
+
const output = join(canonicalRoot, "artifacts", "style-portfolio", run);
|
|
38
|
+
await mkdir(output, { recursive: true });
|
|
39
|
+
if (await realpath(output) !== output) throw new Error("Output directories must not be symlinks.");
|
|
40
|
+
for (const style of selected) {
|
|
41
|
+
// Plans have their own immutable attempt, so inspecting a new width never
|
|
42
|
+
// replaces an execution's scene, log, result, or native receipt references.
|
|
43
|
+
const prefix = `${style}${flags.has("--dry-run") ? ".plan" : ""}`;
|
|
44
|
+
const resultPath = join(output, `${prefix}.result.json`);
|
|
45
|
+
const requestPath = join(output, `${prefix}.scene.json`);
|
|
46
|
+
const intentPath = join(output, `${prefix}.intent.json`);
|
|
47
|
+
// A retained intent without a result is ambiguous. Inspect its log and host
|
|
48
|
+
// receipts before selecting an explicitly new --run, never auto-resubmit.
|
|
49
|
+
try {
|
|
50
|
+
await writeFile(intentPath, JSON.stringify({ kind: "slopcamera.style-study-intent", style, requestPath, width, still,
|
|
51
|
+
mode: flags.has("--dry-run") ? "plan" : "render" }) + "\n", { flag: "wx", mode: 0o600 });
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
54
|
+
log(JSON.stringify({ style, state: "already-attempted", intentPath, resultPath }));
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const profile = getVisualStyleProfile(style);
|
|
58
|
+
const scene = {
|
|
59
|
+
kind: "slopcamera.html-scene", schemaVersion: 1,
|
|
60
|
+
name: `${profile.name} / ${still ? "master still" : "motion study"}`,
|
|
61
|
+
document: { path: "examples/style-portfolio/animation-studies.html" },
|
|
62
|
+
canvas: { width, height: width * 9 / 16, deviceScaleFactor: 1 },
|
|
63
|
+
timing: { durationUs: still ? 1 : 6_000_000, fps: 24 },
|
|
64
|
+
seed: 20260923, libraries: [], resources: [],
|
|
65
|
+
parameters: { style, direction: profile, timeOffsetSeconds: offset },
|
|
66
|
+
background: profile.palette.background,
|
|
67
|
+
};
|
|
68
|
+
await writeFile(requestPath, JSON.stringify(scene, null, 2) + "\n", { flag: "wx", mode: 0o600 });
|
|
69
|
+
const childArgs = [process.execPath, join(canonicalRoot, "apps/desktop/dist/cli/main.js"), "html", "render", "--input", requestPath, "--json"];
|
|
70
|
+
if (flags.has("--dry-run")) childArgs.push("--dry-run");
|
|
71
|
+
const logPath = join(output, `${prefix}.log`);
|
|
72
|
+
const logFile = await open(logPath, "wx", 0o600);
|
|
73
|
+
let code: number;
|
|
74
|
+
let stdout: string;
|
|
75
|
+
try {
|
|
76
|
+
const child = Bun.spawn(childArgs, { cwd: canonicalRoot, env: process.env, stdout: "pipe", stderr: logFile.fd });
|
|
77
|
+
[code, stdout] = await Promise.all([child.exited, new Response(child.stdout).text()]);
|
|
78
|
+
} finally { await logFile.close(); }
|
|
79
|
+
if (code !== 0) {
|
|
80
|
+
await writeFile(join(output, `${prefix}.failed.json`), JSON.stringify({ code, stdout }) + "\n", { flag: "wx" });
|
|
81
|
+
throw new Error(`${style} render failed (${code}); inspect ${logPath}. No automatic retry was made.`);
|
|
82
|
+
}
|
|
83
|
+
const result: unknown = JSON.parse(stdout);
|
|
84
|
+
await writeFile(resultPath, JSON.stringify(result, null, 2) + "\n", { flag: "wx" });
|
|
85
|
+
log(JSON.stringify({ style, state: flags.has("--dry-run") ? "planned" : "rendered", resultPath }));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (import.meta.main) await renderStudies(process.argv.slice(2));
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/** Source checks only. This does not replace rendered-frame and motion review. */
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { runInNewContext } from "node:vm";
|
|
6
|
+
import { getVisualStyleProfile } from "../../src/visual-style.ts";
|
|
7
|
+
|
|
8
|
+
const html = await readFile(new URL("./animation-studies.html", import.meta.url), "utf8");
|
|
9
|
+
const scripts = [...html.matchAll(/<script\s*>([\s\S]*?)<\/script\s*>/giu)];
|
|
10
|
+
assert.equal((html.match(/<script\b/giu) ?? []).length, 1, "The self-contained scene has one script element.");
|
|
11
|
+
assert.equal(scripts.length, 1, "The scene script is inline and has no attributes.");
|
|
12
|
+
const script = scripts[0]?.[1];
|
|
13
|
+
assert.ok(script, "The inline scene script is nonempty.");
|
|
14
|
+
new Function(script);
|
|
15
|
+
let digest = createHash("sha256");
|
|
16
|
+
const contexts = [];
|
|
17
|
+
const record = (name, values) => digest.update(JSON.stringify([name, values]));
|
|
18
|
+
function context(name) {
|
|
19
|
+
let depth = 0;
|
|
20
|
+
const methods = {
|
|
21
|
+
save() { depth++; record(`${name}.save`, []); },
|
|
22
|
+
restore() { assert.ok(depth > 0, "Canvas restore has an owned save."); depth--; record(`${name}.restore`, []); },
|
|
23
|
+
createLinearGradient(...args) { return gradient("linear", args); },
|
|
24
|
+
createRadialGradient(...args) { return gradient("radial", args); },
|
|
25
|
+
};
|
|
26
|
+
const gradient = (kind, args) => {
|
|
27
|
+
args.forEach(value => assert.ok(Number.isFinite(value), "Gradient coordinates are finite."));
|
|
28
|
+
const result = { kind, args, stops: [], addColorStop(position, color) {
|
|
29
|
+
assert.ok(position >= 0 && position <= 1, "Gradient stop lies in [0, 1].");
|
|
30
|
+
result.stops.push([position, color]);
|
|
31
|
+
} };
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
const proxy = new Proxy(methods, {
|
|
35
|
+
get(target, property) {
|
|
36
|
+
if (property in target) return target[property];
|
|
37
|
+
return (...args) => {
|
|
38
|
+
for (const value of args) if (typeof value === "number") assert.ok(Number.isFinite(value), `${String(property)} receives finite coordinates.`);
|
|
39
|
+
if (property === "arc") assert.ok(args[2] >= 0, "Arc radius is nonnegative.");
|
|
40
|
+
if (property === "ellipse") assert.ok(args[2] >= 0 && args[3] >= 0, "Ellipse radii are nonnegative.");
|
|
41
|
+
record(`${name}.${String(property)}`, args);
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
set(target, property, value) { target[property] = value; record(`${name}.${String(property)}=`, [value]); return true; },
|
|
45
|
+
});
|
|
46
|
+
contexts.push(() => assert.equal(depth, 0, "Scene leaves a balanced Canvas save stack."));
|
|
47
|
+
return proxy;
|
|
48
|
+
}
|
|
49
|
+
const main = { width: 1920, height: 1080, getContext: () => context("main") };
|
|
50
|
+
const pixel = { width: 320, height: 180, getContext: () => context("pixel") };
|
|
51
|
+
let frame;
|
|
52
|
+
const host = { parameters: { style: "theatrical-cel" }, onFrame(callback) { frame = callback; } };
|
|
53
|
+
const window = { innerWidth: 1920, innerHeight: 1080, devicePixelRatio: 1, SlopcameraOverlay: host };
|
|
54
|
+
runInNewContext(script, {
|
|
55
|
+
window, SlopcameraOverlay: host,
|
|
56
|
+
document: { getElementById: () => main, createElement: tag => { assert.equal(tag, "canvas"); return pixel; } },
|
|
57
|
+
Path2D: class { constructor(data) { assert.equal(typeof data, "string"); assert.ok(!/NaN|Infinity/u.test(data), "Path coordinates are finite."); this.data = data; } },
|
|
58
|
+
}, { timeout: 10_000 });
|
|
59
|
+
const drawing = window.PortfolioDrawing;
|
|
60
|
+
assert.equal(drawing.styles.length, 12);
|
|
61
|
+
const fingerprint = (time, style, direction) => {
|
|
62
|
+
digest = createHash("sha256");
|
|
63
|
+
drawing.render(time, style, direction);
|
|
64
|
+
contexts.forEach(check => check());
|
|
65
|
+
return digest.digest("hex");
|
|
66
|
+
};
|
|
67
|
+
let samples = 0;
|
|
68
|
+
for (const style of drawing.styles) {
|
|
69
|
+
const profile = getVisualStyleProfile(style);
|
|
70
|
+
drawing.validateDirection(profile, style);
|
|
71
|
+
for (const time of [0, .25, .84, 1.68, 2.65, 3, 5.999, 6]) {
|
|
72
|
+
const first = fingerprint(time, style, profile);
|
|
73
|
+
fingerprint((time + 1.3) % 6, "clean-motion", getVisualStyleProfile("clean-motion"));
|
|
74
|
+
assert.equal(fingerprint(time, style, profile), first, `${style} at ${time}s is independent of seek order.`);
|
|
75
|
+
samples++;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
assert.throws(() => drawing.render(1, "unknown"), /Unsupported style/u);
|
|
79
|
+
assert.throws(() => drawing.render(NaN, "theatrical-cel"), /finite number/u);
|
|
80
|
+
assert.throws(() => drawing.render(-1, "theatrical-cel"), /finite number/u);
|
|
81
|
+
assert.throws(() => drawing.render(6.1, "theatrical-cel"), /finite number/u);
|
|
82
|
+
assert.throws(() => drawing.validateDirection({}, "theatrical-cel"), /required/u);
|
|
83
|
+
assert.throws(() => drawing.validateDirection({ ...getVisualStyleProfile("theatrical-cel"), id: "pixel-art" }, "theatrical-cel"), /must match/u);
|
|
84
|
+
assert.throws(() => drawing.validateDirection({ ...getVisualStyleProfile("theatrical-cel"), unsupported: 1 }, "theatrical-cel"), /unsupported/u);
|
|
85
|
+
assert.throws(() => drawing.validateDirection({ ...getVisualStyleProfile("theatrical-cel"), cadence: { ...getVisualStyleProfile("theatrical-cel").cadence, fps: 0 } }, "theatrical-cel"), /integer/u);
|
|
86
|
+
window.innerWidth = Infinity;
|
|
87
|
+
assert.throws(() => drawing.render(0, "theatrical-cel"), /canvas CSS width/u);
|
|
88
|
+
window.innerWidth = 1920;
|
|
89
|
+
window.devicePixelRatio = 9;
|
|
90
|
+
assert.throws(() => drawing.render(0, "theatrical-cel"), /devicePixelRatio/u);
|
|
91
|
+
window.devicePixelRatio = 1;
|
|
92
|
+
assert.throws(() => frame({ timeMs: -1 }), /timeMs/u);
|
|
93
|
+
assert.throws(() => frame({ timeMs: 6001 }), /timeMs/u);
|
|
94
|
+
console.log(`Passed: 12 canonical profiles, ${samples} deterministic drawing-command samples, balanced Canvas stacks, finite geometry, and invalid-input checks. Pixel and motion quality still require real renders.`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hraness/slopcamera",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0",
|
|
4
4
|
"description": "A local visual studio for coding agents: author scenes, combine generated and recorded media, and export images, diagrams, animation, and video from retained sources.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -162,6 +162,15 @@
|
|
|
162
162
|
"examples/studio/hybrid-scene.ts",
|
|
163
163
|
"examples/html/*.html",
|
|
164
164
|
"examples/html/*.json",
|
|
165
|
+
"examples/style-portfolio/**/*.html",
|
|
166
|
+
"examples/style-portfolio/**/*.ts",
|
|
167
|
+
"examples/style-portfolio/**/*.mjs",
|
|
168
|
+
"!examples/style-portfolio/**/*.test.ts",
|
|
169
|
+
"examples/style-portfolio/**/*.py",
|
|
170
|
+
"examples/style-portfolio/**/*.json",
|
|
171
|
+
"examples/style-portfolio/**/*.md",
|
|
172
|
+
"examples/style-portfolio/**/*.woff2",
|
|
173
|
+
"examples/style-portfolio/**/*.txt",
|
|
165
174
|
"examples/studio/vgpu/heat-field.mjs",
|
|
166
175
|
"apps/desktop/studio/assets/README.md",
|
|
167
176
|
"!apps/desktop/cli/**/*.test.ts",
|
|
@@ -227,7 +236,7 @@
|
|
|
227
236
|
"benchmark:code-concurrency": "bun run ./apps/desktop/code/concurrency-benchmark.ts",
|
|
228
237
|
"check:release-workflows": "bun test ./scripts/release-workflow.test.ts ./scripts/github-release.test.ts",
|
|
229
238
|
"check:schema": "bun run ./scripts/validate-schema.ts",
|
|
230
|
-
"check:skill": "bun run ./scripts/validate-skill.ts && bun test ./scripts/social-collage-compositor.test.ts",
|
|
239
|
+
"check:skill": "bun run ./scripts/validate-skill.ts && bun test ./scripts/social-collage-compositor.test.ts && bun examples/style-portfolio/verify-animation-source.mjs",
|
|
231
240
|
"check:standalone": "bun test ./scripts/standalone-support-fixture.test.ts && bun run ./scripts/check-standalone.ts",
|
|
232
241
|
"test:vectorize:official": "bun run ./scripts/official-vectorizer-smoke.ts",
|
|
233
242
|
"test:package": "bun run ./scripts/package-smoke.ts",
|
|
@@ -7,6 +7,14 @@ description: Create, inspect, edit and render visual media with the Slopcamera C
|
|
|
7
7
|
|
|
8
8
|
Start from the requested result and existing source. Use the installed `slopcamera` CLI, inspect its real capabilities and retain the authored source beside its derived media. Read only the references needed by the task.
|
|
9
9
|
|
|
10
|
+
For creative film and animation, choose coherent art direction before rendering.
|
|
11
|
+
Use [visual style direction](references/visual-style-direction.md) for historical
|
|
12
|
+
footage, animation families, resolution, and visual review. Default to native 4K
|
|
13
|
+
for an unspecified creative master when the renderer and source support it;
|
|
14
|
+
preserve deliberate pixel grids, period aspect ratios, source limits, and the
|
|
15
|
+
user's explicit settings. Treat quality as staging,
|
|
16
|
+
materials, movement, and finishing together.
|
|
17
|
+
|
|
10
18
|
## Establish the host
|
|
11
19
|
|
|
12
20
|
Run `slopcamera --help` and use `slopcamera doctor --json` for the selected local tools. If installation or a version mismatch blocks the task, read [installation and readiness](references/install.md). A skill installation does not install the CLI or native engines.
|
|
@@ -17,6 +25,7 @@ Slopcamera installs from its verified release archive or from source. Historical
|
|
|
17
25
|
|
|
18
26
|
| Requested result | Read |
|
|
19
27
|
| --- | --- |
|
|
28
|
+
| Historical film, cel animation, pixel art, classic animation styles, or a demo portfolio | [Visual style direction](references/visual-style-direction.md) |
|
|
20
29
|
| Editable diagram, flowchart or visual explanation | [Diagrams](references/diagrams.md) and [visual communication](references/visual-communication.md) |
|
|
21
30
|
| Patent-style drawing sheets, physical margins, monochrome SVGs or a drawing PDF | [Drawing sheets](references/patent-drawings.md); requires current source |
|
|
22
31
|
| Custom diagram themes, fonts or icon packages | [Customization](references/customization.md) |
|
|
@@ -6,6 +6,11 @@ credential. It has no Slopcamera account, credential store, or hosted project.
|
|
|
6
6
|
|
|
7
7
|
## Start with the intended role
|
|
8
8
|
|
|
9
|
+
For a creative film or animation brief, use
|
|
10
|
+
[visual style direction](visual-style-direction.md) to translate the intended
|
|
11
|
+
look into concrete source, camera, movement, and finishing decisions. Keep a
|
|
12
|
+
literal prompt literal when that is what the user requested.
|
|
13
|
+
|
|
9
14
|
Clarify what the generated media needs to do in the final project:
|
|
10
15
|
|
|
11
16
|
- the subject or message;
|
|
@@ -14,7 +14,7 @@ command -v slopcamera
|
|
|
14
14
|
|
|
15
15
|
A restricted shell can omit package-manager paths. Check known host installation paths before declaring a tool unavailable. If Bun is genuinely absent, follow its official [installation guide](https://bun.sh/docs/installation) within the user’s authorized setup scope. Do not switch package managers or pipe an unreviewed installer into a shell.
|
|
16
16
|
|
|
17
|
-
Slopcamera installs from its verified release archive: `bun add --global https://github.com/hraness/slopcamera/releases/download/v3.3.
|
|
17
|
+
Slopcamera installs from its verified release archive: `bun add --global https://github.com/hraness/slopcamera/releases/download/v3.3.6/hraness-slopcamera-3.3.6.tgz`, then `slopcamera doctor --json`. Building from source is the contributor path. Historical Atet archives do not install the renamed CLI; never substitute `slopcamera` into an old archive URL.
|
|
18
18
|
|
|
19
19
|
Use an existing compatible source build when available. Otherwise follow the [complete source-install guide](https://github.com/hraness/slopcamera/blob/main/docs/how-to/use-current-source.md): clone into a new directory, record its exact commit, install locked dependencies without lifecycle scripts, build the SDK and source CLI, then define the shell command against that checkout. A clone or skill installation alone does not install the executable. Do not alter an existing active checkout to satisfy this path.
|
|
20
20
|
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Render a music video from an authored scene
|
|
2
2
|
|
|
3
|
+
Choose the intended film or animation language with
|
|
4
|
+
[visual style direction](visual-style-direction.md) before authoring the scene.
|
|
5
|
+
The example dimensions below are an iteration setting; select native delivery
|
|
6
|
+
dimensions explicitly for the finished movie.
|
|
7
|
+
|
|
3
8
|
Use this workflow for a complete HTML or Three.js music video with a local track,
|
|
4
9
|
including dancing characters, changing landscapes, and musical light accents.
|
|
5
10
|
The export retains the source and creates an ordinary editable project with
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Direct a native film studio
|
|
2
2
|
|
|
3
|
+
For historical footage, physical materials, or a distinctive animation family,
|
|
4
|
+
establish [visual style direction](visual-style-direction.md) before choosing
|
|
5
|
+
lighting, camera, frame cadence, and finishing.
|
|
6
|
+
|
|
3
7
|
These commands require current source; check [installation](install.md) and `slopcamera help studio`.
|
|
4
8
|
|
|
5
9
|
Use `slopcamera studio` when the deliverable needs native Blender, CadQuery or Manim authoring. Keep the native source, caches and exact settings alongside review frames and final clips. This complements the existing Three/Spark scene and ordinary video-project workflows.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Direct a visual style
|
|
2
|
+
|
|
3
|
+
Use this reference when a scene needs a recognizable film or animation language,
|
|
4
|
+
or when producing a portfolio of distinct styles. The current-source `style`
|
|
5
|
+
catalog records reusable production decisions:
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
slopcamera style list --json
|
|
9
|
+
slopcamera style show silent-actuality --json
|
|
10
|
+
slopcamera style show theatrical-cel --json
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
These profiles are art direction. They do not automatically transform an image,
|
|
14
|
+
replace a scene's materials, synthesize character animation, or certify historical
|
|
15
|
+
accuracy. Apply them through an appropriate renderer. The seven `html catalog`
|
|
16
|
+
profiles describe rendering tools; visual styles are a separate choice.
|
|
17
|
+
|
|
18
|
+
## Establish the picture before the finish
|
|
19
|
+
|
|
20
|
+
Write a short direction for the specific subject: setting and period, dominant
|
|
21
|
+
silhouette, foreground and distance planes, light source, camera position, one
|
|
22
|
+
readable action, and intended emotional rhythm. Keep the user's literal facts and
|
|
23
|
+
selected identity. Where invention is authorized, give the subject a concrete
|
|
24
|
+
setting and action instead of filling the prompt with quality adjectives.
|
|
25
|
+
|
|
26
|
+
Select the renderer that can produce the needed evidence:
|
|
27
|
+
|
|
28
|
+
| Picture requirement | Useful route |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| Volumetric light, physical material, contact, perspective, architecture | Blender studio or Three scene |
|
|
31
|
+
| Drawn contours, held drawings, flat shadow shapes, graphic composition | Canvas/p5/Two HTML scene |
|
|
32
|
+
| Correct mathematical construction with deliberate teaching beats | Manim or checked deterministic Canvas geometry |
|
|
33
|
+
| Photographic or painted source assets that procedural geometry cannot supply | Authorized Gateway image generation, then reviewed composition |
|
|
34
|
+
| Photochemical finishing of an already convincing source | Existing ordered video-look effects, with restrained diffusion, grain, and vignette |
|
|
35
|
+
|
|
36
|
+
A photo-textured background does not make moving geometry photorealistic. A
|
|
37
|
+
Canvas material study does not demonstrate a physical clay simulation. Name the
|
|
38
|
+
actual method in a portfolio caption.
|
|
39
|
+
|
|
40
|
+
For creative delivery without specified dimensions, default to a native
|
|
41
|
+
3840×2160 master or an equivalent frame in the intended aspect ratio when the
|
|
42
|
+
renderer and source support it. Use explicit 1920×1080 previews for iteration.
|
|
43
|
+
Respect source-image dimensions and a provider's actual output limits; state a
|
|
44
|
+
lower native delivery clearly. Do not upscale a draft and call it native
|
|
45
|
+
high-resolution detail. A 4:3 silent-film composition and a low-resolution pixel
|
|
46
|
+
grid are deliberate exceptions to widescreen layout, not lower quality targets.
|
|
47
|
+
|
|
48
|
+
## Historical film
|
|
49
|
+
|
|
50
|
+
Choose a period and acquisition process before choosing damage. Early silent
|
|
51
|
+
actuality, a 1940s studio negative, postwar 16mm documentary, and a 1970s home movie
|
|
52
|
+
have different framing, motion, lighting, tonal latitude, and surface texture.
|
|
53
|
+
Use a dated primary reference for specific history and label uncertain geometry
|
|
54
|
+
or staging as interpretation. A source link beside an invented measurement does
|
|
55
|
+
not verify that measurement.
|
|
56
|
+
|
|
57
|
+
For an early street actuality:
|
|
58
|
+
|
|
59
|
+
- Research street furniture, transport, overhead utilities, paving, clothing,
|
|
60
|
+
signage, and the sightline. Keep source-supported facts separate from estimated
|
|
61
|
+
dimensions and compressed geography. Inspect generated images for contradictions
|
|
62
|
+
even when the prompt explicitly forbids them. Do not add a familiar landmark or lamp
|
|
63
|
+
merely because it exists there today.
|
|
64
|
+
- Stage a useful street depth: irregular storefront bays, window recesses,
|
|
65
|
+
cornices, varied reflectance, believable kerbs, contact shadows, and atmospheric
|
|
66
|
+
distance. Keep buildings from reading as identical extruded blocks.
|
|
67
|
+
- Put the camera on the actual support, such as a cable car at a plausible human
|
|
68
|
+
height and speed. Keep distant geography coherent as it moves. Avoid a drone
|
|
69
|
+
orbit, wide modern lens, skyline reveal, or floating gimbal unless the reference
|
|
70
|
+
supports it.
|
|
71
|
+
- Set capture cadence and playback cadence separately. A selected 16 fps
|
|
72
|
+
reconstruction is an artistic timing choice, not proof of the source camera's
|
|
73
|
+
exact crank speed. Hold drawings or photographed exposures deliberately; do
|
|
74
|
+
not interpolate every style to 60 fps.
|
|
75
|
+
- Plan the monochrome values in the source. Early film spectral response cannot
|
|
76
|
+
be recovered by desaturating arbitrary modern colors. Make roofs, skin,
|
|
77
|
+
clothing, sky, and street separate in luminance; tune this against the reference.
|
|
78
|
+
- Apply gentle highlight compression, optical softness, and restrained vignette
|
|
79
|
+
after the source works. Keep frame-varying grain fine enough to preserve facade
|
|
80
|
+
detail. Inspect wet surfaces, grain and stroke widths at final pixel size. Separate
|
|
81
|
+
visible sky brightness from ambient fill when a bright sky washes out contact
|
|
82
|
+
shadows; inspect material mapping on every visible face orientation.
|
|
83
|
+
Gate weave belongs to the entire image, not separate jittering objects.
|
|
84
|
+
Dirt and scratches are optional print wear, not the definition of old film.
|
|
85
|
+
|
|
86
|
+
Distinguish capture, print, restoration, and scan. A clean high-resolution scan
|
|
87
|
+
can retain fine texture and stable framing. Do not add heavy dust, crushed blacks,
|
|
88
|
+
sepia, random flicker, letterbox bars, or a fake timecode by habit. Keep invented
|
|
89
|
+
archival-looking footage labeled as an authored interpretation. A photographic
|
|
90
|
+
plate with optical movement is a still-image study, not evidence of independently
|
|
91
|
+
animated people, vehicles, or a historical event.
|
|
92
|
+
|
|
93
|
+
The Market Street example includes a source manifest and an explicitly compressed
|
|
94
|
+
street plan. Consult the Library of Congress film and SFMTA history linked in the
|
|
95
|
+
example before changing its dates or street equipment.
|
|
96
|
+
|
|
97
|
+
## Animation families
|
|
98
|
+
|
|
99
|
+
An influence such as *Akira* or 3Blue1Brown is useful reference language. Translate
|
|
100
|
+
it into mechanisms and create original subjects and compositions. The catalog
|
|
101
|
+
uses generic technique names so the direction is reusable beyond one reference.
|
|
102
|
+
|
|
103
|
+
| Family | Construct the image and movement | Reject at review |
|
|
104
|
+
| --- | --- | --- |
|
|
105
|
+
| Theatrical cel | Painted depth planes; precise perspective; selective contour weight; solid shadow shapes; articulated motion; held poses and short accents; camera motion sampled independently | Glossy toy materials, uniform outline filters, idle motion everywhere |
|
|
106
|
+
| Watercolor storybook | Broad pigment washes, warm paper, lost edges, a focal patch of crisp ink, gentle overlapping action | A blur over vector art, evenly noisy outlines, identical texture on every object |
|
|
107
|
+
| Ink sketch | Confident primary contours, sparse searching lines, directional hatching, selected line boil tied to held drawings | Unstable silhouettes, crosshatching through highlights, whole-scene wobble |
|
|
108
|
+
| Rubber hose | Strong readable silhouette, curved limbs, arcs, anticipation, squash and stretch with volume awareness, clear contact | Random sine waves on every joint, sliding feet, impossible eye direction |
|
|
109
|
+
| Midcentury limited | Asymmetric geometry, restrained spot palette, texture fields, selective held animation, strong negative space | Full constant motion, muddy colors, ornamental texture obscuring the subject |
|
|
110
|
+
| Cut paper | Distinct cut contours, shallow layer separation, directional edge shadows, articulated pivots and reveal order | Universal blur, deep plastic bevels, disconnected shadows |
|
|
111
|
+
| Stop-motion clay | Fingerprint-scale surface, soft contact, slightly imperfect construction, intentionally stepped poses and weighted settling | A smooth wobbling sphere described as physical stop motion, glossy uniform skin |
|
|
112
|
+
| Pixel art | Explicit logical grid, limited palette, clustered shading, authored sprite poses, integer-aligned travel, nearest-neighbor integer scale | Blurred scaling, subpixel edges, unrestricted gradients, every object at a different pixel size |
|
|
113
|
+
| Rotoscope | Gesture and weight from coherent anatomy, restrained contour drift, distinct held silhouettes, consistent volume | Unmotivated jitter, bendy joints, inconsistent limb lengths |
|
|
114
|
+
| Engraving | Hatching follows form; line density describes value; leave highlights open; compose large black and white masses | Uniform screen-space stripe overlays or crushed unreadable detail |
|
|
115
|
+
| Mathematical explanation | State one claim, assign stable colors, construct the relation, pause at its implication, keep notation readable | Decorative formulas, mismatched axes, moving labels, false equality, a waveform inconsistent with its generators |
|
|
116
|
+
| Isometric design | Stable projection, consistent edge families, hierarchy through material and spacing, staged exploded movement | Conflicting vanishing points, changing scale, unrelated decorative labels |
|
|
117
|
+
| Clean motion | A small set of shapes, optical alignment, purposeful easing, pauses, consistent transformation rules | Continuous bouncing, gratuitous overshoot, generic orbiting particles |
|
|
118
|
+
|
|
119
|
+
Build the action from anticipation, main movement, follow-through, and a readable
|
|
120
|
+
hold when those suit the style. Offset secondary action; avoid identical phase on
|
|
121
|
+
every leaf or limb. Derive state from absolute frame time, with seeded variation
|
|
122
|
+
keyed to the object and exposure index. Keep stable construction randomness
|
|
123
|
+
separate from intentionally changing drawing or grain randomness. Seeking frames
|
|
124
|
+
in a different order must reproduce the same image.
|
|
125
|
+
|
|
126
|
+
## Review and retain
|
|
127
|
+
|
|
128
|
+
Review representative frames before a full render: establishing view, strongest
|
|
129
|
+
action, transition, and ending. Then inspect the encoded movie for timing,
|
|
130
|
+
flicker, texture shimmer, crop, strobing, text, and compression. Pixel-level
|
|
131
|
+
sharpness does not establish coherent art direction. A large output dimension
|
|
132
|
+
does not establish source detail.
|
|
133
|
+
|
|
134
|
+
For a portfolio, select different subjects and compositions that reveal each
|
|
135
|
+
technique. Include the editable sources, profile IDs, actual output dimensions,
|
|
136
|
+
render receipts, and honest renderer labels. Separate reusable direction from
|
|
137
|
+
scene-specific drawing code. A contact sheet supports visual comparison; it does
|
|
138
|
+
not replace checking motion. Preserve an old result when it explains a change.
|
|
139
|
+
|
|
140
|
+
Keep bounded render jobs and their original sources. Dense grain can make a
|
|
141
|
+
lossless intermediate exceed the existing media bound, especially at 4K. Shorten
|
|
142
|
+
the shot or split it into retained takes without weakening that boundary. Do not
|
|
143
|
+
retry an ambiguous native or paid generation before inspecting its retained state.
|