@hraness/slopcamera 3.2.6 → 3.2.7
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 +5 -2
- package/apps/desktop/application/operation.ts +1 -1
- package/apps/desktop/cli/args.ts +8 -0
- package/apps/desktop/cli/command-host-resources.ts +3 -0
- package/apps/desktop/cli/commands.ts +10 -1
- package/apps/desktop/cli/help.ts +13 -3
- package/apps/desktop/cli/html-scene.ts +368 -0
- package/apps/desktop/cli/main.ts +6 -1
- package/apps/desktop/cli/media-ingest-model.ts +2 -0
- package/apps/desktop/cli/media-ingest-program.ts +9 -4
- package/apps/desktop/cli/portable-surface.ts +1 -0
- package/apps/desktop/cli/recording-controller.ts +1 -1
- package/apps/desktop/cli/root-help-intro.ts +14 -0
- package/apps/desktop/dist/cli/main.js +241 -222
- package/apps/desktop/html-overlay/contracts.ts +3 -0
- package/apps/desktop/html-overlay/index.ts +19 -0
- package/apps/desktop/html-overlay/music-clock.ts +126 -0
- package/apps/desktop/html-overlay/rigged-glb.ts +429 -0
- package/apps/desktop/html-overlay/runtime.ts +2 -0
- package/apps/desktop/html-overlay/scaffolds.ts +2 -1
- package/apps/desktop/html-overlay/scene.ts +47 -0
- package/dist/cli.js +1 -1
- package/dist/{index-jchqst8w.js → index-rvghzt7b.js} +1 -1
- package/dist/index.js +1 -1
- package/docs/README.md +1 -0
- package/examples/html/music-video.html +514 -0
- package/examples/html/music-video.json +16 -0
- package/package.json +3 -1
- package/skills/slopcamera/SKILL.md +2 -1
- package/skills/slopcamera/references/install.md +1 -1
- package/skills/slopcamera/references/music-video.md +149 -0
- package/skills/slopcamera/references/video-projects.md +6 -2
- package/skills/slopcamera/references/workflows-sdk.md +1 -1
- package/src/version.ts +1 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { RepositoryRelativePathSchema } from "../contracts";
|
|
3
|
+
import {
|
|
4
|
+
HtmlOverlayAuthoringInputSchema, HtmlOverlayCanvasSchema, HtmlOverlayDeclaredResourceSchema,
|
|
5
|
+
HtmlOverlayInlineDocumentSchema, HtmlOverlayParametersSchema, HtmlOverlayTimingSchema,
|
|
6
|
+
} from "./contracts";
|
|
7
|
+
import { HtmlOverlayExecutionProfileSchema } from "./execution-profile";
|
|
8
|
+
import { HtmlOverlayLibrarySelectionSchema } from "./libraries";
|
|
9
|
+
|
|
10
|
+
const ResourceSchema = HtmlOverlayDeclaredResourceSchema.omit({ bytes: true, sha256: true }).extend({
|
|
11
|
+
path: RepositoryRelativePathSchema,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
/** A local scene source. Audio is an explicit caller-selected import, never a browser resource. */
|
|
15
|
+
export const HtmlSceneInputSchema = z.strictObject({
|
|
16
|
+
kind: z.literal("slopcamera.html-scene"),
|
|
17
|
+
schemaVersion: z.literal(1),
|
|
18
|
+
name: z.string().trim().min(1).max(512),
|
|
19
|
+
document: z.union([HtmlOverlayInlineDocumentSchema, z.strictObject({ path: RepositoryRelativePathSchema })]),
|
|
20
|
+
canvas: HtmlOverlayCanvasSchema,
|
|
21
|
+
timing: HtmlOverlayTimingSchema,
|
|
22
|
+
libraries: HtmlOverlayLibrarySelectionSchema.default([]),
|
|
23
|
+
parameters: HtmlOverlayParametersSchema.default({}),
|
|
24
|
+
resources: z.array(ResourceSchema).max(64).default([]),
|
|
25
|
+
seed: z.number().int().safe().min(0).max(0xffff_ffff).default(0),
|
|
26
|
+
background: z.string().regex(/^#[0-9a-fA-F]{6}$/u).default("#080b16"),
|
|
27
|
+
executionProfile: HtmlOverlayExecutionProfileSchema.optional(),
|
|
28
|
+
audio: z.strictObject({
|
|
29
|
+
path: z.string().min(1).max(4096).refine(path => !/[\0\r\n]/u.test(path), "Audio path contains a control character."),
|
|
30
|
+
}).optional(),
|
|
31
|
+
}).superRefine((input, context) => {
|
|
32
|
+
const authoring = HtmlOverlayAuthoringInputSchema.safeParse({
|
|
33
|
+
kind: "slopcamera.html-overlay", schemaVersion: 1, canvas: input.canvas, timing: input.timing,
|
|
34
|
+
libraries: input.libraries, parameters: input.parameters, seed: input.seed,
|
|
35
|
+
html: "html" in input.document ? input.document.html : "<html></html>",
|
|
36
|
+
resources: input.resources.map(({ path: _path, ...resource }) => ({ ...resource, bytes: 0, sha256: "0".repeat(64) })),
|
|
37
|
+
});
|
|
38
|
+
// Feed only the authoring contract's own fields through its workload and identity checks.
|
|
39
|
+
if (!authoring.success) {
|
|
40
|
+
for (const issue of authoring.error.issues) context.addIssue({ code: "custom", message: issue.message });
|
|
41
|
+
}
|
|
42
|
+
// The existing renderer emits CSS-sized screenshots at every device scale.
|
|
43
|
+
if (input.canvas.width % 2 !== 0 || input.canvas.height % 2 !== 0) {
|
|
44
|
+
context.addIssue({ code: "custom", message: "MP4 export requires even canvas width and height." });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
export type HtmlSceneInput = Readonly<z.infer<typeof HtmlSceneInputSchema>>;
|
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
package/docs/README.md
CHANGED
|
@@ -14,6 +14,7 @@ These pages describe Slopcamera from current source. [Install that source](how-t
|
|
|
14
14
|
- [Run current-source commands](how-to/use-current-source.md): build an exact checkout of Slopcamera.
|
|
15
15
|
|
|
16
16
|
- [Edit and deliver video](how-to/edit-video.md): import footage, align related tracks, place overlays, and check a delivery.
|
|
17
|
+
- [Make a music video from an HTML scene](how-to/music-video.md): render authored visuals with a local track and retain separate sources in an editable project.
|
|
17
18
|
- [Generate images, video, or narration](how-to/generate-media.md): discover Gateway capabilities, acknowledge selected uploads, and retain the result.
|
|
18
19
|
- [Direct short generated clips](directing-video.md): budget, review takes, preserve endpoint continuity, and recover uncertain work.
|
|
19
20
|
- [Author a native film](studio.md): use Blender, CadQuery, or Manim; retain caches; share assets and calibrated cameras.
|
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Island Pulse — procedural music video</title>
|
|
7
|
+
<style>
|
|
8
|
+
html, body { margin: 0; width: 100%; height: 100%; overflow: hidden; background: transparent; }
|
|
9
|
+
canvas { display: block; width: 100%; height: 100%; }
|
|
10
|
+
</style>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<canvas aria-label="An articulated island robot dancing above a transforming tropical coast"></canvas>
|
|
14
|
+
<script type="module">
|
|
15
|
+
import * as THREE from "three";
|
|
16
|
+
|
|
17
|
+
// Original articulated mascot and procedural scenery; no external assets.
|
|
18
|
+
// Bind the "three" library. Recommended canvas: 1280 × 720.
|
|
19
|
+
// Supply parameters.music to override this explicit example timing.
|
|
20
|
+
const music = Object.freeze({
|
|
21
|
+
bpm: 88.88,
|
|
22
|
+
beatOffsetUs: 0,
|
|
23
|
+
beatsPerBar: 4,
|
|
24
|
+
...SlopcameraOverlay.parameters.music,
|
|
25
|
+
});
|
|
26
|
+
SlopcameraOverlay.musicClock(0, music);
|
|
27
|
+
const TAU = Math.PI * 2;
|
|
28
|
+
const mod = (value, divisor) => ((value % divisor) + divisor) % divisor;
|
|
29
|
+
const mix = THREE.MathUtils.lerp;
|
|
30
|
+
const smooth = (low, high, value) => THREE.MathUtils.smoothstep(value, low, high);
|
|
31
|
+
const random = (label) => SlopcameraOverlay.randomFor("island-pulse:" + label);
|
|
32
|
+
const resources = new Set();
|
|
33
|
+
const track = (resource) => { resources.add(resource); return resource; };
|
|
34
|
+
const canvas = document.querySelector("canvas");
|
|
35
|
+
const renderer = new THREE.WebGLRenderer({
|
|
36
|
+
canvas, alpha: true, antialias: true, premultipliedAlpha: false,
|
|
37
|
+
powerPreference: "high-performance",
|
|
38
|
+
});
|
|
39
|
+
renderer.setPixelRatio(devicePixelRatio);
|
|
40
|
+
renderer.setSize(SlopcameraOverlay.width, SlopcameraOverlay.height, false);
|
|
41
|
+
renderer.setClearColor(0x000000, 0);
|
|
42
|
+
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
|
43
|
+
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
44
|
+
renderer.toneMappingExposure = 1.05;
|
|
45
|
+
renderer.info.autoReset = false;
|
|
46
|
+
const scene = new THREE.Scene();
|
|
47
|
+
const camera = new THREE.PerspectiveCamera(
|
|
48
|
+
42, SlopcameraOverlay.width / SlopcameraOverlay.height, 0.1, 180,
|
|
49
|
+
);
|
|
50
|
+
const ambient = new THREE.HemisphereLight(0xb3f4ed, 0x233341, 2.4);
|
|
51
|
+
const key = new THREE.DirectionalLight(0xffe7c0, 3.4);
|
|
52
|
+
key.position.set(4, 7, 5);
|
|
53
|
+
const rim = new THREE.DirectionalLight(0x62fff0, 3.2);
|
|
54
|
+
rim.position.set(-5, 4, -5);
|
|
55
|
+
scene.add(ambient, key, rim);
|
|
56
|
+
|
|
57
|
+
const sphere = track(new THREE.SphereGeometry(1, 24, 16));
|
|
58
|
+
const box = track(new THREE.BoxGeometry(1, 1, 1));
|
|
59
|
+
const cylinder = track(new THREE.CylinderGeometry(1, 1, 1, 24));
|
|
60
|
+
const material = (color, extra = {}) => track(new THREE.MeshStandardMaterial({
|
|
61
|
+
color, roughness: 0.54, metalness: 0.08, ...extra,
|
|
62
|
+
}));
|
|
63
|
+
const ceramic = material(0xf6e6c6);
|
|
64
|
+
const teal = material(0x177f87, { metalness: 0.28 });
|
|
65
|
+
const coral = material(0xf47762);
|
|
66
|
+
const ink = material(0x101e35, { roughness: 0.32 });
|
|
67
|
+
const mint = material(0x7affdf, { emissive: 0x35dbbd, emissiveIntensity: 1.1 });
|
|
68
|
+
const gold = material(0xeab956, { metalness: 0.5, roughness: 0.3 });
|
|
69
|
+
const mesh = (parent, geometry, surface, position, scale) => {
|
|
70
|
+
const object = new THREE.Mesh(geometry, surface);
|
|
71
|
+
object.position.set(...position);
|
|
72
|
+
object.scale.set(...scale);
|
|
73
|
+
parent.add(object);
|
|
74
|
+
return object;
|
|
75
|
+
};
|
|
76
|
+
const joint = (parent, name, position) => {
|
|
77
|
+
const object = new THREE.Group();
|
|
78
|
+
object.name = name;
|
|
79
|
+
object.position.set(...position);
|
|
80
|
+
parent.add(object);
|
|
81
|
+
return object;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Named rigid joints make this mascot articulated, not a skinned asset.
|
|
85
|
+
const mascot = joint(scene, "mascot", [0, 0, 0]);
|
|
86
|
+
const hips = joint(mascot, "hips", [0, 1.15, 0]);
|
|
87
|
+
mesh(hips, sphere, teal, [0, 0, 0], [0.4, 0.25, 0.3]);
|
|
88
|
+
const torso = joint(hips, "torso", [0, 0.12, 0]);
|
|
89
|
+
mesh(torso, sphere, ceramic, [0, 0.32, 0], [0.56, 0.61, 0.36]);
|
|
90
|
+
mesh(torso, sphere, teal, [0, 0.29, 0.315], [0.28, 0.32, 0.095]);
|
|
91
|
+
const heart = mesh(torso, sphere, mint, [0, 0.35, 0.4], [0.09, 0.09, 0.035]);
|
|
92
|
+
const head = joint(torso, "head", [0, 0.96, 0]);
|
|
93
|
+
mesh(head, sphere, ceramic, [0, 0.13, 0], [0.58, 0.46, 0.43]);
|
|
94
|
+
mesh(head, sphere, ink, [0, 0.12, 0.342], [0.45, 0.255, 0.13]);
|
|
95
|
+
const eyes = [-1, 1].map(side => mesh(
|
|
96
|
+
head, sphere, mint, [side * 0.17, 0.16, 0.462], [0.055, 0.095, 0.023],
|
|
97
|
+
));
|
|
98
|
+
mesh(head, sphere, coral, [0, -0.035, 0.451], [0.085, 0.022, 0.02]);
|
|
99
|
+
const antenna = joint(head, "antenna", [0, 0.54, 0]);
|
|
100
|
+
mesh(antenna, cylinder, gold, [0, 0.095, 0], [0.035, 0.19, 0.035]);
|
|
101
|
+
mesh(antenna, sphere, coral, [0, 0.23, 0], [0.105, 0.105, 0.105]);
|
|
102
|
+
const arms = [-1, 1].map(side => {
|
|
103
|
+
const shoulder = joint(torso, side < 0 ? "right-shoulder" : "left-shoulder", [side * 0.53, 0.57, 0]);
|
|
104
|
+
mesh(shoulder, sphere, coral, [0, 0, 0], [0.2, 0.2, 0.2]);
|
|
105
|
+
mesh(shoulder, sphere, teal, [0, -0.22, 0], [0.145, 0.27, 0.145]);
|
|
106
|
+
const elbow = joint(shoulder, side < 0 ? "right-elbow" : "left-elbow", [0, -0.43, 0]);
|
|
107
|
+
mesh(elbow, sphere, gold, [0, 0, 0], [0.14, 0.14, 0.14]);
|
|
108
|
+
mesh(elbow, sphere, ceramic, [0, -0.18, 0], [0.16, 0.235, 0.16]);
|
|
109
|
+
mesh(elbow, sphere, coral, [0, -0.4, 0.02], [0.2, 0.22, 0.19]);
|
|
110
|
+
return { side, shoulder, elbow };
|
|
111
|
+
});
|
|
112
|
+
const legs = [-1, 1].map(side => {
|
|
113
|
+
const thigh = joint(hips, side < 0 ? "right-thigh" : "left-thigh", [side * 0.265, -0.055, 0]);
|
|
114
|
+
mesh(thigh, sphere, teal, [0, -0.215, 0], [0.175, 0.29, 0.175]);
|
|
115
|
+
const knee = joint(thigh, side < 0 ? "right-knee" : "left-knee", [0, -0.46, 0]);
|
|
116
|
+
mesh(knee, sphere, coral, [0, 0, 0], [0.17, 0.17, 0.17]);
|
|
117
|
+
mesh(knee, sphere, ceramic, [0, -0.2, 0], [0.16, 0.26, 0.16]);
|
|
118
|
+
const foot = mesh(knee, sphere, ink, [0, -0.425, 0.12], [0.235, 0.15, 0.35]);
|
|
119
|
+
return { side, thigh, knee, foot };
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const stage = mesh(scene, cylinder, material(0x163d49), [0, -0.08, 0], [2.12, 0.24, 2.12]);
|
|
123
|
+
stage.name = "floating-stage";
|
|
124
|
+
const ringGeometry = track(new THREE.TorusGeometry(1, 0.015, 8, 96));
|
|
125
|
+
const ringMaterial = material(0x54efd5, { emissive: 0x28cfae, emissiveIntensity: 1.1 });
|
|
126
|
+
const rings = [1.38, 1.72, 2.05].map(radius => {
|
|
127
|
+
const ring = mesh(scene, ringGeometry, ringMaterial, [0, 0.048, 0], [radius, radius, radius]);
|
|
128
|
+
ring.rotation.x = -Math.PI / 2;
|
|
129
|
+
return { ring, radius };
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const landscape = { time: { value: 0 }, morph: { value: 0 }, night: { value: 0 } };
|
|
133
|
+
const landscapeFunctions = `
|
|
134
|
+
float terrainHeight(vec2 p, float morph) {
|
|
135
|
+
float inland = smoothstep(2.0, 8.0, -p.y);
|
|
136
|
+
float rolling = 0.65 + 0.3 * sin(p.x * 0.34 + p.y * 0.21)
|
|
137
|
+
+ 0.24 * cos(p.x * 0.7 - p.y * 0.18);
|
|
138
|
+
float mountain = 0.7 + 2.9 * pow(0.5 + 0.5 * sin(p.x * 0.24 + p.y * 0.17), 3.0)
|
|
139
|
+
+ 1.45 * pow(0.5 + 0.5 * cos(p.x * 0.46 - p.y * 0.28), 4.0);
|
|
140
|
+
return -0.5 + inland * mix(rolling, mountain, morph);
|
|
141
|
+
}
|
|
142
|
+
`;
|
|
143
|
+
const terrainHeight = (x, z, morph) => {
|
|
144
|
+
const rolling = 0.65 + 0.3 * Math.sin(x * 0.34 + z * 0.21)
|
|
145
|
+
+ 0.24 * Math.cos(x * 0.7 - z * 0.18);
|
|
146
|
+
const mountain = 0.7 + 2.9 * (0.5 + 0.5 * Math.sin(x * 0.24 + z * 0.17)) ** 3
|
|
147
|
+
+ 1.45 * (0.5 + 0.5 * Math.cos(x * 0.46 - z * 0.28)) ** 4;
|
|
148
|
+
return -0.5 + smooth(2, 8, -z) * mix(rolling, mountain, morph);
|
|
149
|
+
};
|
|
150
|
+
const terrainGeometry = track(new THREE.PlaneGeometry(56, 42, 100, 80));
|
|
151
|
+
terrainGeometry.rotateX(-Math.PI / 2);
|
|
152
|
+
terrainGeometry.translate(0, 0, -23);
|
|
153
|
+
const terrainMaterial = track(new THREE.ShaderMaterial({
|
|
154
|
+
uniforms: landscape,
|
|
155
|
+
vertexShader: `
|
|
156
|
+
uniform float morph;
|
|
157
|
+
varying vec3 worldPosition;
|
|
158
|
+
varying vec3 terrainNormal;
|
|
159
|
+
${landscapeFunctions}
|
|
160
|
+
void main() {
|
|
161
|
+
vec3 p = position;
|
|
162
|
+
p.y = terrainHeight(p.xz, morph);
|
|
163
|
+
float dx = terrainHeight(p.xz + vec2(0.08, 0.0), morph) - p.y;
|
|
164
|
+
float dz = terrainHeight(p.xz + vec2(0.0, 0.08), morph) - p.y;
|
|
165
|
+
terrainNormal = normalize(vec3(-dx, 0.08, -dz));
|
|
166
|
+
worldPosition = p;
|
|
167
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);
|
|
168
|
+
}
|
|
169
|
+
`,
|
|
170
|
+
fragmentShader: `
|
|
171
|
+
uniform float morph;
|
|
172
|
+
uniform float night;
|
|
173
|
+
varying vec3 worldPosition;
|
|
174
|
+
varying vec3 terrainNormal;
|
|
175
|
+
void main() {
|
|
176
|
+
vec3 sand = vec3(0.48, 0.30, 0.15);
|
|
177
|
+
vec3 jungle = mix(vec3(0.065, 0.24, 0.12), vec3(0.018, 0.10, 0.10), night);
|
|
178
|
+
float coast = smoothstep(-0.25, 0.45, worldPosition.y);
|
|
179
|
+
vec3 color = mix(sand, jungle, coast);
|
|
180
|
+
color *= 0.62 + 0.58 * max(0.0, dot(terrainNormal, normalize(vec3(-0.4, 1.0, 0.5))));
|
|
181
|
+
color += 0.025 * sin(worldPosition.x * 1.7 + worldPosition.z * 2.1);
|
|
182
|
+
float mist = smoothstep(6.0, 48.0, -worldPosition.z) * 0.7;
|
|
183
|
+
color = mix(color, mix(vec3(0.24, 0.46, 0.43), vec3(0.025, 0.055, 0.12), night), mist);
|
|
184
|
+
gl_FragColor = vec4(color, 1.0);
|
|
185
|
+
}
|
|
186
|
+
`,
|
|
187
|
+
}));
|
|
188
|
+
scene.add(new THREE.Mesh(terrainGeometry, terrainMaterial));
|
|
189
|
+
|
|
190
|
+
const waterGeometry = track(new THREE.PlaneGeometry(160, 140, 72, 48));
|
|
191
|
+
waterGeometry.rotateX(-Math.PI / 2);
|
|
192
|
+
waterGeometry.translate(0, -0.5, -30);
|
|
193
|
+
const waterMaterial = track(new THREE.ShaderMaterial({
|
|
194
|
+
uniforms: landscape,
|
|
195
|
+
vertexShader: `
|
|
196
|
+
uniform float time;
|
|
197
|
+
varying vec3 waterPosition;
|
|
198
|
+
void main() {
|
|
199
|
+
vec3 p = position;
|
|
200
|
+
p.y += 0.045 * sin(p.x * 0.7 + time * 0.55)
|
|
201
|
+
+ 0.035 * sin(p.z * 1.1 - time * 0.7);
|
|
202
|
+
waterPosition = p;
|
|
203
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);
|
|
204
|
+
}
|
|
205
|
+
`,
|
|
206
|
+
fragmentShader: `
|
|
207
|
+
uniform float time;
|
|
208
|
+
uniform float night;
|
|
209
|
+
varying vec3 waterPosition;
|
|
210
|
+
void main() {
|
|
211
|
+
vec2 p = waterPosition.xz;
|
|
212
|
+
float line = 0.5 + 0.5 * sin(p.y * 3.0 + sin(p.x * 0.8 + time * 0.3) - time * 0.85);
|
|
213
|
+
float glint = pow(line, 20.0) * (0.4 + 0.6 * pow(0.5 + 0.5 * sin(p.x * 2.0), 6.0));
|
|
214
|
+
vec3 base = mix(vec3(0.025, 0.27, 0.29), vec3(0.003, 0.028, 0.075), night);
|
|
215
|
+
vec3 color = base + glint * mix(vec3(0.12, 0.24, 0.18), vec3(0.015, 0.32, 0.29), night);
|
|
216
|
+
color = mix(color, mix(vec3(0.26, 0.41, 0.39), vec3(0.025, 0.055, 0.12), night), smoothstep(15.0, 75.0, -p.y));
|
|
217
|
+
gl_FragColor = vec4(color, 1.0);
|
|
218
|
+
}
|
|
219
|
+
`,
|
|
220
|
+
}));
|
|
221
|
+
scene.add(new THREE.Mesh(waterGeometry, waterMaterial));
|
|
222
|
+
|
|
223
|
+
const skyMaterial = track(new THREE.ShaderMaterial({
|
|
224
|
+
uniforms: landscape,
|
|
225
|
+
side: THREE.BackSide,
|
|
226
|
+
depthWrite: false,
|
|
227
|
+
vertexShader: `
|
|
228
|
+
varying vec3 skyDirection;
|
|
229
|
+
void main() {
|
|
230
|
+
skyDirection = position;
|
|
231
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
232
|
+
}
|
|
233
|
+
`,
|
|
234
|
+
fragmentShader: `
|
|
235
|
+
uniform float night;
|
|
236
|
+
varying vec3 skyDirection;
|
|
237
|
+
void main() {
|
|
238
|
+
float height = smoothstep(-0.08, 0.62, normalize(skyDirection).y);
|
|
239
|
+
vec3 horizon = mix(vec3(0.80, 0.43, 0.29), vec3(0.065, 0.09, 0.19), night);
|
|
240
|
+
vec3 zenith = mix(vec3(0.07, 0.30, 0.39), vec3(0.003, 0.012, 0.047), night);
|
|
241
|
+
gl_FragColor = vec4(mix(horizon, zenith, height), 1.0);
|
|
242
|
+
}
|
|
243
|
+
`,
|
|
244
|
+
}));
|
|
245
|
+
mesh(scene, sphere, skyMaterial, [0, 0, 0], [90, 90, 90]);
|
|
246
|
+
const sun = mesh(scene, sphere, material(0xffd0a0, {
|
|
247
|
+
emissive: 0xffa169, emissiveIntensity: 0.65, roughness: 1,
|
|
248
|
+
}), [-8, 6.2, -22], [1.5, 1.5, 1.5]);
|
|
249
|
+
|
|
250
|
+
const dummy = new THREE.Object3D();
|
|
251
|
+
const palmCount = 18;
|
|
252
|
+
const trunkGeometry = track(new THREE.CylinderGeometry(0.06, 0.105, 1, 7));
|
|
253
|
+
const leafGeometry = track(new THREE.SphereGeometry(1, 8, 4));
|
|
254
|
+
const trunks = track(new THREE.InstancedMesh(trunkGeometry, material(0x755337), palmCount));
|
|
255
|
+
const leaves = track(new THREE.InstancedMesh(leafGeometry, material(0x289363), palmCount * 6));
|
|
256
|
+
trunks.frustumCulled = false;
|
|
257
|
+
leaves.frustumCulled = false;
|
|
258
|
+
scene.add(trunks, leaves);
|
|
259
|
+
const palms = Array.from({ length: palmCount }, (_, index) => {
|
|
260
|
+
const side = index % 2 === 0 ? -1 : 1;
|
|
261
|
+
return {
|
|
262
|
+
x: side * (3.3 + random("palm-x:" + index) * 9),
|
|
263
|
+
z: -4.8 - random("palm-z:" + index) * 17,
|
|
264
|
+
height: 1.6 + random("palm-height:" + index) * 1.5,
|
|
265
|
+
phase: random("palm-phase:" + index) * TAU,
|
|
266
|
+
};
|
|
267
|
+
});
|
|
268
|
+
const houseCount = 22;
|
|
269
|
+
const houses = track(new THREE.InstancedMesh(box, material(0xffffff), houseCount));
|
|
270
|
+
const roofs = track(new THREE.InstancedMesh(box, material(0xcc6a50), houseCount));
|
|
271
|
+
const windows = track(new THREE.InstancedMesh(box, material(0xffc979, {
|
|
272
|
+
emissive: 0xe99549, emissiveIntensity: 0.3,
|
|
273
|
+
}), houseCount * 2));
|
|
274
|
+
[houses, roofs, windows].forEach(object => { object.frustumCulled = false; scene.add(object); });
|
|
275
|
+
const houseColors = [0xd59778, 0x75b1a5, 0xe2c178, 0xc88499, 0xa0c5ce];
|
|
276
|
+
const town = Array.from({ length: houseCount }, (_, index) => {
|
|
277
|
+
houses.setColorAt(index, new THREE.Color(houseColors[index % houseColors.length]));
|
|
278
|
+
return {
|
|
279
|
+
x: (index - (houseCount - 1) / 2) * 0.87,
|
|
280
|
+
z: -8.7 - random("house-z:" + index) * 3.4,
|
|
281
|
+
height: 0.7 + random("house-height:" + index) * 1.0,
|
|
282
|
+
width: 0.6 + random("house-width:" + index) * 0.22,
|
|
283
|
+
};
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const sparkPositions = [];
|
|
287
|
+
const sparkPhases = [];
|
|
288
|
+
for (let index = 0; index < 160; index += 1) {
|
|
289
|
+
sparkPositions.push(
|
|
290
|
+
(random("spark-x:" + index) - 0.5) * 28,
|
|
291
|
+
0.3 + random("spark-y:" + index) * 10,
|
|
292
|
+
-2.5 - random("spark-z:" + index) * 30,
|
|
293
|
+
);
|
|
294
|
+
sparkPhases.push(random("spark-phase:" + index) * TAU);
|
|
295
|
+
}
|
|
296
|
+
const sparkGeometry = track(new THREE.BufferGeometry());
|
|
297
|
+
sparkGeometry.setAttribute("position", new THREE.Float32BufferAttribute(sparkPositions, 3));
|
|
298
|
+
sparkGeometry.setAttribute("phase", new THREE.Float32BufferAttribute(sparkPhases, 1));
|
|
299
|
+
const sparkMaterial = track(new THREE.ShaderMaterial({
|
|
300
|
+
uniforms: { ...landscape, pixelRatio: { value: devicePixelRatio } },
|
|
301
|
+
transparent: true, depthWrite: false, blending: THREE.AdditiveBlending,
|
|
302
|
+
vertexShader: `
|
|
303
|
+
attribute float phase;
|
|
304
|
+
uniform float time;
|
|
305
|
+
uniform float pixelRatio;
|
|
306
|
+
varying float glow;
|
|
307
|
+
void main() {
|
|
308
|
+
vec3 p = position;
|
|
309
|
+
p.x += 0.35 * sin(time * 0.2 + phase);
|
|
310
|
+
p.y += 0.22 * sin(time * 0.38 + phase);
|
|
311
|
+
vec4 view = modelViewMatrix * vec4(p, 1.0);
|
|
312
|
+
gl_Position = projectionMatrix * view;
|
|
313
|
+
gl_PointSize = clamp(34.0 / -view.z, 1.2, 4.0) * pixelRatio;
|
|
314
|
+
glow = 0.6 + 0.4 * sin(time * 0.45 + phase);
|
|
315
|
+
}
|
|
316
|
+
`,
|
|
317
|
+
fragmentShader: `
|
|
318
|
+
uniform float night;
|
|
319
|
+
varying float glow;
|
|
320
|
+
void main() {
|
|
321
|
+
float alpha = 1.0 - smoothstep(0.08, 0.5, length(gl_PointCoord - 0.5));
|
|
322
|
+
gl_FragColor = vec4(vec3(0.24, 1.2, 0.88), alpha * glow * (0.12 + 0.65 * night));
|
|
323
|
+
}
|
|
324
|
+
`,
|
|
325
|
+
}));
|
|
326
|
+
scene.add(new THREE.Points(sparkGeometry, sparkMaterial));
|
|
327
|
+
|
|
328
|
+
// MSAA belongs on the render target as well as the presentation canvas.
|
|
329
|
+
// Bloom is localized to bright fragments; exposure never pulses to the beat.
|
|
330
|
+
const targetWidth = Math.round(SlopcameraOverlay.width * devicePixelRatio);
|
|
331
|
+
const targetHeight = Math.round(SlopcameraOverlay.height * devicePixelRatio);
|
|
332
|
+
const target = track(new THREE.WebGLRenderTarget(targetWidth, targetHeight, {
|
|
333
|
+
type: THREE.HalfFloatType,
|
|
334
|
+
samples: Math.min(4, renderer.capabilities.maxSamples),
|
|
335
|
+
depthBuffer: true,
|
|
336
|
+
}));
|
|
337
|
+
const postScene = new THREE.Scene();
|
|
338
|
+
const postCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
|
339
|
+
const postMaterial = track(new THREE.ShaderMaterial({
|
|
340
|
+
depthTest: false, depthWrite: false,
|
|
341
|
+
uniforms: {
|
|
342
|
+
image: { value: target.texture },
|
|
343
|
+
texel: { value: new THREE.Vector2(1 / targetWidth, 1 / targetHeight) },
|
|
344
|
+
},
|
|
345
|
+
vertexShader: `
|
|
346
|
+
varying vec2 imageUv;
|
|
347
|
+
void main() { imageUv = uv; gl_Position = vec4(position.xy, 0.0, 1.0); }
|
|
348
|
+
`,
|
|
349
|
+
fragmentShader: `
|
|
350
|
+
uniform sampler2D image;
|
|
351
|
+
uniform vec2 texel;
|
|
352
|
+
varying vec2 imageUv;
|
|
353
|
+
void main() {
|
|
354
|
+
vec4 source = texture2D(image, imageUv);
|
|
355
|
+
vec3 bloom = vec3(0.0);
|
|
356
|
+
for (int index = 0; index < 8; index++) {
|
|
357
|
+
float angle = float(index) * 0.7853981634;
|
|
358
|
+
vec2 direction = vec2(cos(angle), sin(angle)) * texel;
|
|
359
|
+
bloom += max(texture2D(image, imageUv + direction * 3.0).rgb - 1.1, 0.0);
|
|
360
|
+
bloom += 0.5 * max(texture2D(image, imageUv + direction * 8.0).rgb - 1.1, 0.0);
|
|
361
|
+
}
|
|
362
|
+
gl_FragColor = vec4(source.rgb + bloom * 0.016, source.a);
|
|
363
|
+
#include <tonemapping_fragment>
|
|
364
|
+
#include <colorspace_fragment>
|
|
365
|
+
}
|
|
366
|
+
`,
|
|
367
|
+
}));
|
|
368
|
+
postScene.add(new THREE.Mesh(track(new THREE.PlaneGeometry(2, 2)), postMaterial));
|
|
369
|
+
|
|
370
|
+
function dancePose(style, beat) {
|
|
371
|
+
const step = Math.sin(Math.PI * beat);
|
|
372
|
+
const wave = Math.sin(Math.PI * beat * 0.5);
|
|
373
|
+
return [
|
|
374
|
+
[0.28 * step, 0.42, 0.42, 0.25, 0.25],
|
|
375
|
+
[0.18 * step, 1.9 + 0.3 * wave, 0.4, 0.7, 0.35],
|
|
376
|
+
[0.38 * wave, 1.15 + 0.25 * step, 1.15 - 0.25 * step, 0.9, 0.9],
|
|
377
|
+
[0.22 * step, 0.65, 1.9 - 0.3 * wave, 0.3, 0.7],
|
|
378
|
+
][mod(style, 4)];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
SlopcameraOverlay.onFrame(({ timeMs }) => {
|
|
382
|
+
const seconds = timeMs / 1000;
|
|
383
|
+
const clock = SlopcameraOverlay.musicClock(Math.round(timeMs * 1000), music);
|
|
384
|
+
const beat = clock.beatPosition;
|
|
385
|
+
const pulse = SlopcameraOverlay.musicPulse(clock.beatPhase, 0.7);
|
|
386
|
+
const phrase = Math.floor(beat / 16);
|
|
387
|
+
const phraseBlend = smooth(0.82, 1, mod(beat / 16, 1));
|
|
388
|
+
const poseA = dancePose(phrase, beat);
|
|
389
|
+
const poseB = dancePose(phrase + 1, beat);
|
|
390
|
+
const pose = poseA.map((value, index) => mix(value, poseB[index], phraseBlend));
|
|
391
|
+
const step = Math.sin(Math.PI * beat);
|
|
392
|
+
const lift = 0.07 * (1 - Math.cos(TAU * beat));
|
|
393
|
+
mascot.position.set(0.12 * step, 0, 0.035 * Math.sin(TAU * beat / 4));
|
|
394
|
+
mascot.rotation.set(0, 0.24 * Math.sin(TAU * beat / 8)
|
|
395
|
+
+ TAU * smooth(12, 16, mod(beat, 32)), 0);
|
|
396
|
+
hips.rotation.set(0.05 * Math.cos(TAU * beat), pose[0], 0.075 * step);
|
|
397
|
+
torso.rotation.set(0.035 * Math.sin(TAU * beat), -pose[0] * 0.42, -0.1 * step);
|
|
398
|
+
head.rotation.set(0.07 * Math.sin(TAU * beat), -0.12 * step, 0.035 * step);
|
|
399
|
+
antenna.rotation.set(0.08 * Math.sin(TAU * beat - 0.5), 0, 0.16 * step);
|
|
400
|
+
for (const { side, shoulder, elbow } of arms) {
|
|
401
|
+
shoulder.rotation.set(0.25 * step * side, 0, side * pose[side > 0 ? 1 : 2]);
|
|
402
|
+
elbow.rotation.set(-pose[side > 0 ? 3 : 4] - 0.22 * Math.cos(Math.PI * beat + side), 0, side * 0.15);
|
|
403
|
+
}
|
|
404
|
+
for (const { side, thigh, knee, foot } of legs) {
|
|
405
|
+
thigh.rotation.set(side * step * 0.38, 0, side * 0.045);
|
|
406
|
+
knee.rotation.set(0.13 + 0.26 * Math.max(0, -side * step), 0, 0);
|
|
407
|
+
foot.rotation.set(-0.1 - 0.08 * Math.cos(Math.PI * beat + side), 0, 0);
|
|
408
|
+
}
|
|
409
|
+
// Recompute support from this pose, keeping the lowest ellipsoid shoe
|
|
410
|
+
// above the stage while retaining the beat's small airborne bounce.
|
|
411
|
+
mascot.updateMatrixWorld(true);
|
|
412
|
+
const lowestFoot = Math.min(...legs.map(({ foot }) => {
|
|
413
|
+
const matrix = foot.matrixWorld.elements;
|
|
414
|
+
return matrix[13] - Math.hypot(matrix[1], matrix[5], matrix[9]);
|
|
415
|
+
}));
|
|
416
|
+
mascot.position.y = 0.052 + lift - lowestFoot;
|
|
417
|
+
const blink = 1 - 0.85 * (1 - smooth(0, 0.11, Math.abs(mod(beat + 2, 12) - 6)));
|
|
418
|
+
eyes.forEach(eye => eye.scale.set(0.055, 0.095 * blink, 0.023));
|
|
419
|
+
heart.scale.setScalar(0.09 * (1 + 0.12 * pulse));
|
|
420
|
+
heart.scale.z = 0.035;
|
|
421
|
+
mint.emissiveIntensity = 0.9 + 0.35 * pulse;
|
|
422
|
+
ringMaterial.emissiveIntensity = 0.75 + 0.65 * pulse;
|
|
423
|
+
rings.forEach(({ ring, radius }, index) => {
|
|
424
|
+
const accent = SlopcameraOverlay.musicPulse(mod(clock.beatPhase - index * 0.1, 1), 0.7);
|
|
425
|
+
ring.scale.setScalar(radius * (1 + 0.014 * accent));
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
const journey = THREE.MathUtils.clamp(timeMs / SlopcameraOverlay.durationMs, 0, 1);
|
|
429
|
+
const morph = smooth(0.1, 0.72, journey);
|
|
430
|
+
const night = smooth(0.28, 0.97, journey);
|
|
431
|
+
landscape.time.value = seconds;
|
|
432
|
+
landscape.morph.value = morph;
|
|
433
|
+
landscape.night.value = night;
|
|
434
|
+
ambient.intensity = mix(2.4, 1.35, night);
|
|
435
|
+
key.intensity = mix(3.4, 2.2, night);
|
|
436
|
+
sun.position.set(-8 + 1.2 * journey, 6.2 - 4.8 * journey, -22);
|
|
437
|
+
sun.scale.setScalar(1.5 - 0.12 * journey);
|
|
438
|
+
windows.material.emissiveIntensity = 0.3 + night * 1.8;
|
|
439
|
+
|
|
440
|
+
palms.forEach(({ x, z, height, phase }, index) => {
|
|
441
|
+
const y = terrainHeight(x, z, morph);
|
|
442
|
+
const sway = 0.07 * Math.sin(seconds * 0.45 + phase);
|
|
443
|
+
const growth = 0.78 + morph * 0.4;
|
|
444
|
+
dummy.position.set(x, y + height * growth * 0.5, z);
|
|
445
|
+
dummy.rotation.set(0, 0, sway);
|
|
446
|
+
dummy.scale.set(1, height * growth, 1);
|
|
447
|
+
dummy.updateMatrix();
|
|
448
|
+
trunks.setMatrixAt(index, dummy.matrix);
|
|
449
|
+
for (let leaf = 0; leaf < 6; leaf += 1) {
|
|
450
|
+
const angle = leaf * TAU / 6 + phase;
|
|
451
|
+
dummy.position.set(x - Math.sin(sway) * height * growth * 0.5
|
|
452
|
+
+ Math.sin(angle) * 0.36, y + height * growth, z + Math.cos(angle) * 0.36);
|
|
453
|
+
dummy.rotation.set(0.15 * Math.sin(angle), angle, -0.22 * Math.cos(angle) + sway);
|
|
454
|
+
dummy.scale.set(0.14, 0.065, 0.83 * growth);
|
|
455
|
+
dummy.updateMatrix();
|
|
456
|
+
leaves.setMatrixAt(index * 6 + leaf, dummy.matrix);
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
trunks.instanceMatrix.needsUpdate = true;
|
|
460
|
+
leaves.instanceMatrix.needsUpdate = true;
|
|
461
|
+
town.forEach(({ x, z, height, width }, index) => {
|
|
462
|
+
const y = terrainHeight(x, z, morph);
|
|
463
|
+
const h = height * (1 - morph * 0.28);
|
|
464
|
+
dummy.rotation.set(0, 0, 0);
|
|
465
|
+
dummy.position.set(x, y + h * 0.5, z);
|
|
466
|
+
dummy.scale.set(width, h, 0.75);
|
|
467
|
+
dummy.updateMatrix();
|
|
468
|
+
houses.setMatrixAt(index, dummy.matrix);
|
|
469
|
+
dummy.position.set(x, y + h + 0.06, z);
|
|
470
|
+
dummy.scale.set(width + 0.12, 0.12, 0.86);
|
|
471
|
+
dummy.updateMatrix();
|
|
472
|
+
roofs.setMatrixAt(index, dummy.matrix);
|
|
473
|
+
for (let window = 0; window < 2; window += 1) {
|
|
474
|
+
dummy.position.set(x + (window - 0.5) * width * 0.48, y + h * 0.58, z + 0.385);
|
|
475
|
+
dummy.scale.set(width * 0.17, Math.min(0.32, h * 0.3), 0.015);
|
|
476
|
+
dummy.updateMatrix();
|
|
477
|
+
windows.setMatrixAt(index * 2 + window, dummy.matrix);
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
[houses, roofs, windows].forEach(object => { object.instanceMatrix.needsUpdate = true; });
|
|
481
|
+
|
|
482
|
+
const barPosition = clock.barIndex + clock.barPhase;
|
|
483
|
+
camera.position.set(
|
|
484
|
+
0.7 + 1.15 * Math.sin(TAU * barPosition / 16),
|
|
485
|
+
2.9 + 0.12 * Math.sin(TAU * barPosition / 12),
|
|
486
|
+
8.1 + 0.22 * Math.cos(TAU * barPosition / 16),
|
|
487
|
+
);
|
|
488
|
+
camera.lookAt(0, 1.3, -0.6);
|
|
489
|
+
renderer.info.reset();
|
|
490
|
+
renderer.setRenderTarget(target);
|
|
491
|
+
renderer.render(scene, camera);
|
|
492
|
+
renderer.setRenderTarget(null);
|
|
493
|
+
renderer.render(postScene, postCamera);
|
|
494
|
+
if (renderer.info.render.calls > 96 || renderer.info.render.triangles > 150_000) {
|
|
495
|
+
throw new Error("The music-video example exceeded its draw-call or triangle budget.");
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
renderer.setRenderTarget(target);
|
|
500
|
+
renderer.compile(scene, camera);
|
|
501
|
+
renderer.setRenderTarget(null);
|
|
502
|
+
renderer.compile(postScene, postCamera);
|
|
503
|
+
canvas.addEventListener("webglcontextlost", event => {
|
|
504
|
+
event.preventDefault();
|
|
505
|
+
throw new Error("The music-video rendering context was lost.");
|
|
506
|
+
});
|
|
507
|
+
addEventListener("pagehide", () => {
|
|
508
|
+
for (const resource of resources) resource.dispose();
|
|
509
|
+
renderer.dispose();
|
|
510
|
+
renderer.forceContextLoss();
|
|
511
|
+
}, { once: true });
|
|
512
|
+
</script>
|
|
513
|
+
</body>
|
|
514
|
+
</html>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"kind": "slopcamera.html-scene",
|
|
3
|
+
"schemaVersion": 1,
|
|
4
|
+
"name": "Island robot",
|
|
5
|
+
"document": { "path": "examples/html/music-video.html" },
|
|
6
|
+
"canvas": { "width": 1280, "height": 720, "deviceScaleFactor": 1 },
|
|
7
|
+
"timing": { "durationUs": 43204320, "fps": 30 },
|
|
8
|
+
"libraries": ["three"],
|
|
9
|
+
"seed": 8888,
|
|
10
|
+
"background": "#080b16",
|
|
11
|
+
"executionProfile": "three-webgl2-hardware-v1",
|
|
12
|
+
"parameters": {
|
|
13
|
+
"music": { "bpm": 88.88, "beatOffsetUs": 0, "beatsPerBar": 4 }
|
|
14
|
+
},
|
|
15
|
+
"resources": []
|
|
16
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hraness/slopcamera",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.7",
|
|
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",
|
|
@@ -161,6 +161,8 @@
|
|
|
161
161
|
"docs/studio.md",
|
|
162
162
|
"examples/studio/native-workflow.ts",
|
|
163
163
|
"examples/studio/hybrid-scene.ts",
|
|
164
|
+
"examples/html/*.html",
|
|
165
|
+
"examples/html/*.json",
|
|
164
166
|
"examples/studio/vgpu/heat-field.mjs",
|
|
165
167
|
"apps/desktop/studio/assets/README.md",
|
|
166
168
|
"!apps/desktop/cli/**/*.test.ts",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: slopcamera
|
|
3
|
-
description: Create, inspect, edit and render visual media with the Slopcamera CLI and SDK. Use for diagrams, raster-to-SVG conversion, screen or camera recordings, video editing and captions, AI images/video/speech/transcription, Three.js scenes and world-space media, Blender rigs and simulation, CadQuery models, Manim educational films, HTML/WGSL motion graphics, and durable agent workflows.
|
|
3
|
+
description: Create, inspect, edit and render visual media with the Slopcamera CLI and SDK. Use for diagrams, raster-to-SVG conversion, screen or camera recordings, video editing and captions, music videos from authored scenes and local tracks, AI images/video/speech/transcription, Three.js scenes and world-space media, Blender rigs and simulation, CadQuery models, Manim educational films, HTML/WGSL motion graphics, and durable agent workflows.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Create visual media with Slopcamera
|
|
@@ -21,6 +21,7 @@ Slopcamera installs from its verified release archive or from source. Historical
|
|
|
21
21
|
| Custom diagram themes, fonts or icon packages | [Customization](references/customization.md) |
|
|
22
22
|
| Raster artwork converted to SVG | [Vectorization](references/vectorization.md) |
|
|
23
23
|
| Record, clean up, caption, frame or deliver video | [Video projects](references/video-projects.md) |
|
|
24
|
+
| Music video, dancing mascot or timed HTML scene with a local soundtrack | [Music videos](references/music-video.md) |
|
|
24
25
|
| Generate images, clips, narration or transcripts | [Gateway media](references/gateway-media.md) |
|
|
25
26
|
| Budgeted short clips, reviewed takes and continuity | [Directing video](references/directing-video.md) |
|
|
26
27
|
| Editable Three/Spark scene, saved world, named-part edits or shared camera | [Directed scenes](references/directed-scenes.md) |
|
|
@@ -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.2.
|
|
17
|
+
Slopcamera installs from its verified release archive: `bun add --global https://github.com/hraness/slopcamera/releases/download/v3.2.6/hraness-slopcamera-3.2.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
|
|