frameyard 0.4.0 → 0.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.
- package/CHANGELOG.md +46 -0
- package/GLOSSARY.md +4 -0
- package/README.md +16 -9
- package/dist/cli.js +7 -11
- package/dist/cli.js.map +1 -1
- package/dist/composition/adapter.d.ts +30 -0
- package/dist/composition/adapter.d.ts.map +1 -0
- package/dist/composition/adapter.js +2 -0
- package/dist/composition/adapter.js.map +1 -0
- package/dist/composition/animation.d.ts +19 -0
- package/dist/composition/animation.d.ts.map +1 -0
- package/dist/composition/animation.js +55 -0
- package/dist/composition/animation.js.map +1 -0
- package/dist/composition/attributes.d.ts +7 -0
- package/dist/composition/attributes.d.ts.map +1 -0
- package/dist/composition/attributes.js +24 -0
- package/dist/composition/attributes.js.map +1 -0
- package/dist/composition/descriptor.d.ts +30 -0
- package/dist/composition/descriptor.d.ts.map +1 -0
- package/dist/composition/descriptor.js +188 -0
- package/dist/composition/descriptor.js.map +1 -0
- package/dist/composition/diagnostics.d.ts +12 -0
- package/dist/composition/diagnostics.d.ts.map +1 -0
- package/dist/composition/diagnostics.js +38 -0
- package/dist/composition/diagnostics.js.map +1 -0
- package/dist/composition/index.d.ts +12 -0
- package/dist/composition/index.d.ts.map +1 -0
- package/dist/composition/index.js +7 -0
- package/dist/composition/index.js.map +1 -0
- package/dist/composition/media.d.ts +27 -0
- package/dist/composition/media.d.ts.map +1 -0
- package/dist/composition/media.js +162 -0
- package/dist/composition/media.js.map +1 -0
- package/dist/composition/paths.d.ts +5 -0
- package/dist/composition/paths.d.ts.map +1 -0
- package/dist/composition/paths.js +16 -0
- package/dist/composition/paths.js.map +1 -0
- package/dist/composition/transcripts.d.ts +19 -0
- package/dist/composition/transcripts.d.ts.map +1 -0
- package/dist/composition/transcripts.js +57 -0
- package/dist/composition/transcripts.js.map +1 -0
- package/dist/composition/walk.d.ts +10 -0
- package/dist/composition/walk.d.ts.map +1 -0
- package/dist/composition/walk.js +32 -0
- package/dist/composition/walk.js.map +1 -0
- package/dist/contracts.d.ts +2 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js.map +1 -1
- package/dist/html/document-adapter.d.ts +5 -0
- package/dist/html/document-adapter.d.ts.map +1 -0
- package/dist/html/document-adapter.js +17 -0
- package/dist/html/document-adapter.js.map +1 -0
- package/dist/html/parse.d.ts +2 -6
- package/dist/html/parse.d.ts.map +1 -1
- package/dist/html/parse.js.map +1 -1
- package/dist/html/validate.d.ts.map +1 -1
- package/dist/html/validate.js +92 -375
- package/dist/html/validate.js.map +1 -1
- package/dist/preview/main.d.ts.map +1 -1
- package/dist/preview/main.js +4 -2
- package/dist/preview/main.js.map +1 -1
- package/dist/renderer/browser.d.ts.map +1 -1
- package/dist/renderer/browser.js +32 -5
- package/dist/renderer/browser.js.map +1 -1
- package/dist/renderer/index.d.ts.map +1 -1
- package/dist/renderer/index.js +18 -7
- package/dist/renderer/index.js.map +1 -1
- package/dist/runtime/document-adapter.d.ts +4 -0
- package/dist/runtime/document-adapter.d.ts.map +1 -0
- package/dist/runtime/document-adapter.js +19 -0
- package/dist/runtime/document-adapter.js.map +1 -0
- package/dist/runtime/host.d.ts +2 -0
- package/dist/runtime/host.d.ts.map +1 -1
- package/dist/runtime/host.js +78 -183
- package/dist/runtime/host.js.map +1 -1
- package/dist/runtime/media.d.ts.map +1 -1
- package/dist/runtime/media.js +19 -50
- package/dist/runtime/media.js.map +1 -1
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +10 -5
- package/dist/scaffold.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/adr/0006-seek-embedded-media-from-the-frame-clock.md +4 -0
- package/docs/adr/0008-share-the-composition-document-grammar.md +18 -0
- package/docs/adr/0009-composite-production-frames-in-software.md +37 -0
- package/docs/v0.1-design.md +5 -4
- package/examples/demo/README.md +1 -1
- package/examples/video/README.md +61 -0
- package/examples/video/assets/clip.mp4 +0 -0
- package/examples/video/assets/clip.webm +0 -0
- package/examples/video/compositions/video-mp4.html +362 -0
- package/examples/video/compositions/video.html +362 -0
- package/examples/video/props/video.json +8 -0
- package/examples/video/scripts/make-clip.sh +109 -0
- package/examples/video/video.config.js +6 -0
- package/examples/webgl/README.md +30 -0
- package/examples/webgl/compositions/webgl.controller.js +200 -0
- package/examples/webgl/compositions/webgl.html +215 -0
- package/examples/webgl/props/webgl.json +6 -0
- package/examples/webgl/video.config.js +5 -0
- package/package.json +4 -2
- package/skills/frameyard/SKILL.md +3 -2
- package/skills/frameyard/references/json-results.md +2 -2
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Regenerate examples/video/assets/clip.webm and clip.mp4.
|
|
4
|
+
#
|
|
5
|
+
# The visual is a procedural gradient field warped by two sinusoidal
|
|
6
|
+
# displacement maps. The audio is an additive sine pad with pink-noise air.
|
|
7
|
+
# Fixed seeds keep the generated grain stable across runs on the same FFmpeg.
|
|
8
|
+
#
|
|
9
|
+
# Requires FFmpeg with libvpx-vp9, libx264, Opus, and AAC support.
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# examples/video/scripts/make-clip.sh
|
|
13
|
+
# examples/video/scripts/make-clip.sh --out-dir /tmp/frameyard-clip
|
|
14
|
+
|
|
15
|
+
set -euo pipefail
|
|
16
|
+
|
|
17
|
+
usage() {
|
|
18
|
+
cat <<'EOF'
|
|
19
|
+
Usage: make-clip.sh [--out-dir <directory>]
|
|
20
|
+
|
|
21
|
+
Writes clip.webm (VP9 + Opus) and clip.mp4 (H.264 + AAC) to
|
|
22
|
+
examples/video/assets by default.
|
|
23
|
+
EOF
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
27
|
+
out_dir="$(cd -- "$script_dir/.." && pwd)/assets"
|
|
28
|
+
|
|
29
|
+
while [ "$#" -gt 0 ]; do
|
|
30
|
+
case "$1" in
|
|
31
|
+
--out-dir)
|
|
32
|
+
if [ "$#" -lt 2 ]; then
|
|
33
|
+
echo "make-clip: --out-dir requires a directory." >&2
|
|
34
|
+
usage >&2
|
|
35
|
+
exit 2
|
|
36
|
+
fi
|
|
37
|
+
out_dir="$2"
|
|
38
|
+
shift 2
|
|
39
|
+
;;
|
|
40
|
+
-h | --help)
|
|
41
|
+
usage
|
|
42
|
+
exit 0
|
|
43
|
+
;;
|
|
44
|
+
*)
|
|
45
|
+
echo "make-clip: unknown argument: $1" >&2
|
|
46
|
+
usage >&2
|
|
47
|
+
exit 2
|
|
48
|
+
;;
|
|
49
|
+
esac
|
|
50
|
+
done
|
|
51
|
+
|
|
52
|
+
for tool in ffmpeg ffprobe; do
|
|
53
|
+
if ! command -v "$tool" >/dev/null 2>&1; then
|
|
54
|
+
echo "make-clip: $tool is required on PATH." >&2
|
|
55
|
+
exit 1
|
|
56
|
+
fi
|
|
57
|
+
done
|
|
58
|
+
|
|
59
|
+
mkdir -p "$out_dir"
|
|
60
|
+
out_dir="$(cd -- "$out_dir" && pwd)"
|
|
61
|
+
work_dir="$(mktemp -d "$out_dir/.make-clip.XXXXXX")"
|
|
62
|
+
trap 'rm -rf "$work_dir"' EXIT
|
|
63
|
+
|
|
64
|
+
# 12 seconds of a slow circular gradient, warped by two radial sine/cosine
|
|
65
|
+
# displacement maps. Scaling up and cropping hides the displacement edges.
|
|
66
|
+
ffmpeg -hide_banner -loglevel error -y \
|
|
67
|
+
-f lavfi -i "gradients=s=1280x720:r=30:d=12:c0=0x071827:c1=0x0d3a3a:c2=0x2f8a7e:c3=0xffd7a0:c4=0x123a58:c5=0x8fe6c8:n=6:type=circular:speed=0.02:seed=9" \
|
|
68
|
+
-f lavfi -i "nullsrc=s=1280x720:r=30:d=12,geq=lum='128+72*sin(hypot(X-640,Y-360)/42-T*0.55)':cb=128:cr=128" \
|
|
69
|
+
-f lavfi -i "nullsrc=s=1280x720:r=30:d=12,geq=lum='128+68*cos(hypot(X-640,Y-360)/55+T*0.47)':cb=128:cr=128" \
|
|
70
|
+
-filter_complex "[0:v][1:v][2:v]displace=edge=smear,scale=1440:810,crop=1280:720,gblur=sigma=6,eq=contrast=1.26:brightness=0.015:saturation=1.06,noise=alls=4:allf=t+u:all_seed=7,vignette=PI/3.6,format=yuv420p[v]" \
|
|
71
|
+
-map "[v]" -t 12 -an \
|
|
72
|
+
-c:v libvpx-vp9 -crf 31 -b:v 0 -row-mt 1 -tile-columns 2 -cpu-used 2 -g 60 \
|
|
73
|
+
"$work_dir/clip-video.webm"
|
|
74
|
+
|
|
75
|
+
# A slow A-minor-ish pad with movement from chorus/echo and a filtered
|
|
76
|
+
# pink-noise bed. The clip fades in and out so the timeline edit is soft.
|
|
77
|
+
ffmpeg -hide_banner -loglevel error -y \
|
|
78
|
+
-f lavfi -i "aevalsrc=exprs='0.24*sin(2*PI*110*t)+0.12*sin(2*PI*164.81*t)+0.10*sin(2*PI*220*t)*(0.8+0.2*sin(2*PI*0.07*t))+0.06*sin(2*PI*261.63*t)+0.04*sin(2*PI*329.63*t)+0.018*sin(2*PI*440*t)':s=48000:d=12" \
|
|
79
|
+
-f lavfi -i "anoisesrc=color=pink:sample_rate=48000:duration=12:amplitude=0.08:seed=7" \
|
|
80
|
+
-filter_complex "[0:a]chorus=0.6:0.9:50|60:0.4|0.32:0.25|0.3:2|1.3,aecho=0.8:0.6:280|520:0.25|0.16,lowpass=f=4200,volume=0.9[pad];[1:a]lowpass=f=700,volume=0.075[air];[pad][air]amix=inputs=2:duration=longest:normalize=0,afade=t=in:st=0:d=2,afade=t=out:st=9.4:d=2.6,alimiter=limit=0.9,pan=stereo|c0=c0|c1=c0[a]" \
|
|
81
|
+
-map "[a]" -t 12 -c:a libopus -b:a 96k \
|
|
82
|
+
"$work_dir/clip-audio.webm"
|
|
83
|
+
|
|
84
|
+
ffmpeg -hide_banner -loglevel error -y \
|
|
85
|
+
-i "$work_dir/clip-video.webm" \
|
|
86
|
+
-i "$work_dir/clip-audio.webm" \
|
|
87
|
+
-map 0:v:0 -map 1:a:0 -c copy \
|
|
88
|
+
"$work_dir/clip.webm"
|
|
89
|
+
|
|
90
|
+
# The MP4 variant is a transcode of the WebM source with the same timeline.
|
|
91
|
+
ffmpeg -hide_banner -loglevel error -y \
|
|
92
|
+
-i "$work_dir/clip.webm" \
|
|
93
|
+
-c:v libx264 -preset medium -crf 20 -pix_fmt yuv420p -g 60 \
|
|
94
|
+
-c:a aac -b:a 128k -movflags +faststart \
|
|
95
|
+
"$work_dir/clip.mp4"
|
|
96
|
+
|
|
97
|
+
for file in "$work_dir/clip.webm" "$work_dir/clip.mp4"; do
|
|
98
|
+
ffprobe -v error -select_streams v:0 \
|
|
99
|
+
-show_entries stream=codec_name,width,height,r_frame_rate \
|
|
100
|
+
-of default=noprint_wrappers=1 "$file" >/dev/null
|
|
101
|
+
ffprobe -v error -select_streams a:0 -show_entries stream=codec_name \
|
|
102
|
+
-of default=noprint_wrappers=1 "$file" >/dev/null
|
|
103
|
+
done
|
|
104
|
+
|
|
105
|
+
mv -f "$work_dir/clip.webm" "$out_dir/clip.webm"
|
|
106
|
+
mv -f "$work_dir/clip.mp4" "$out_dir/clip.mp4"
|
|
107
|
+
|
|
108
|
+
echo "Wrote $out_dir/clip.webm (VP9 + Opus)"
|
|
109
|
+
echo "Wrote $out_dir/clip.mp4 (H.264 + AAC)"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Frameyard WebGL example
|
|
2
|
+
|
|
3
|
+
A 12-second, 1920×1080 composition that renders a procedural light field with a
|
|
4
|
+
WebGL fragment shader and layers HTML/CSS typography over it:
|
|
5
|
+
|
|
6
|
+
- a WebGL 1 fullscreen triangle with one cached program and cached uniforms
|
|
7
|
+
- shader motion derived only from the composition frame
|
|
8
|
+
- props for the kicker, title, subtitle, and accent color
|
|
9
|
+
- frame-scheduled CSS typography and a frame-state progress bar
|
|
10
|
+
- controller-updated frame and time readouts
|
|
11
|
+
|
|
12
|
+
The composition lives in `compositions/webgl.html`; the external controller is
|
|
13
|
+
`compositions/webgl.controller.js`. Draw every frame from `update()` and do not
|
|
14
|
+
rely on the previous drawing buffer. Production sessions pin software WebGL so
|
|
15
|
+
the captured pixels do not depend on the host GPU.
|
|
16
|
+
|
|
17
|
+
From the repository root:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
pnpm build
|
|
21
|
+
node dist/cli.js check webgl --project examples/webgl
|
|
22
|
+
node dist/cli.js still webgl --frame 210 --project examples/webgl --out webgl-still.png
|
|
23
|
+
node dist/cli.js render webgl --project examples/webgl --props examples/webgl/props/webgl.json
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Start the interactive preview with:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
node dist/cli.js dev --project examples/webgl
|
|
30
|
+
```
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
const VERTEX_SHADER = `
|
|
2
|
+
attribute vec2 a_position;
|
|
3
|
+
|
|
4
|
+
void main() {
|
|
5
|
+
gl_Position = vec4(a_position, 0.0, 1.0);
|
|
6
|
+
}
|
|
7
|
+
`;
|
|
8
|
+
|
|
9
|
+
const FRAGMENT_SHADER = `
|
|
10
|
+
precision highp float;
|
|
11
|
+
|
|
12
|
+
uniform vec2 u_resolution;
|
|
13
|
+
uniform float u_frame;
|
|
14
|
+
uniform float u_duration;
|
|
15
|
+
uniform vec3 u_accent;
|
|
16
|
+
|
|
17
|
+
const float TAU = 6.28318530718;
|
|
18
|
+
|
|
19
|
+
float hash(vec2 p) {
|
|
20
|
+
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
void main() {
|
|
24
|
+
vec2 uv = gl_FragCoord.xy / u_resolution;
|
|
25
|
+
vec2 p = (gl_FragCoord.xy * 2.0 - u_resolution) / u_resolution.y;
|
|
26
|
+
float phase = (u_frame / max(u_duration, 1.0)) * TAU;
|
|
27
|
+
|
|
28
|
+
// A slow domain warp turns the contour field into a liquid surface.
|
|
29
|
+
vec2 q = p;
|
|
30
|
+
q.x += 0.045 * sin(p.y * 3.4 - phase * 0.8);
|
|
31
|
+
q.y += 0.10 * sin(p.x * 2.2 + phase * 0.55) + 0.035 * sin(p.x * 5.7 - phase * 0.9);
|
|
32
|
+
|
|
33
|
+
float terrain = q.y + 0.07 * sin(q.x * 3.6 + phase * 0.7) + 0.025 * sin(q.x * 8.4 - phase * 1.1);
|
|
34
|
+
float contour = abs(fract(terrain * 14.0 + phase * 0.5) - 0.5);
|
|
35
|
+
float line = smoothstep(0.455, 0.5, contour);
|
|
36
|
+
float glow = smoothstep(0.26, 0.5, contour);
|
|
37
|
+
|
|
38
|
+
vec3 ink = mix(vec3(0.016, 0.026, 0.038), vec3(0.035, 0.078, 0.085), uv.y);
|
|
39
|
+
vec3 color = ink;
|
|
40
|
+
|
|
41
|
+
float right = smoothstep(0.14, 0.66, uv.x);
|
|
42
|
+
color += u_accent * line * (0.10 + 0.38 * right);
|
|
43
|
+
color += u_accent * glow * 0.045 * right;
|
|
44
|
+
|
|
45
|
+
// A soft moving light and one thin orbit ring anchor the right side.
|
|
46
|
+
vec2 light = vec2(0.34 + 0.05 * cos(phase * 0.6), 0.06 + 0.04 * sin(phase * 0.45));
|
|
47
|
+
float radius = length(p - light);
|
|
48
|
+
float halo = 0.035 / (radius + 0.035);
|
|
49
|
+
vec3 white = mix(u_accent, vec3(0.92, 1.0, 0.98), 0.7);
|
|
50
|
+
color += white * halo * 0.42;
|
|
51
|
+
|
|
52
|
+
float ring = 1.0 - smoothstep(0.0, 0.007, abs(radius - 0.27 - 0.012 * sin(phase * 1.4)));
|
|
53
|
+
color += white * ring * 0.22;
|
|
54
|
+
|
|
55
|
+
// Static grain keeps the gradient from banding without adding temporal noise.
|
|
56
|
+
color += (hash(gl_FragCoord.xy) - 0.5) * 0.02;
|
|
57
|
+
float vignette = 1.0 - smoothstep(0.62, 1.28, length(p * vec2(0.86, 1.0)));
|
|
58
|
+
color *= 0.58 + 0.42 * vignette;
|
|
59
|
+
|
|
60
|
+
gl_FragColor = vec4(color, 1.0);
|
|
61
|
+
}
|
|
62
|
+
`;
|
|
63
|
+
|
|
64
|
+
let gl;
|
|
65
|
+
let program;
|
|
66
|
+
let buffer;
|
|
67
|
+
let uniforms;
|
|
68
|
+
let labels;
|
|
69
|
+
let durationFrames = 360;
|
|
70
|
+
let fps = 30;
|
|
71
|
+
|
|
72
|
+
function compileShader(type, source) {
|
|
73
|
+
const shader = gl.createShader(type);
|
|
74
|
+
gl.shaderSource(shader, source);
|
|
75
|
+
gl.compileShader(shader);
|
|
76
|
+
if (gl.getShaderParameter(shader, gl.COMPILE_STATUS) !== true) {
|
|
77
|
+
const log = gl.getShaderInfoLog(shader);
|
|
78
|
+
gl.deleteShader(shader);
|
|
79
|
+
throw new Error(log ?? "Could not compile the WebGL shader.");
|
|
80
|
+
}
|
|
81
|
+
return shader;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function linkProgram() {
|
|
85
|
+
const vertex = compileShader(gl.VERTEX_SHADER, VERTEX_SHADER);
|
|
86
|
+
const fragment = compileShader(gl.FRAGMENT_SHADER, FRAGMENT_SHADER);
|
|
87
|
+
const next = gl.createProgram();
|
|
88
|
+
gl.attachShader(next, vertex);
|
|
89
|
+
gl.attachShader(next, fragment);
|
|
90
|
+
gl.linkProgram(next);
|
|
91
|
+
gl.detachShader(next, vertex);
|
|
92
|
+
gl.detachShader(next, fragment);
|
|
93
|
+
gl.deleteShader(vertex);
|
|
94
|
+
gl.deleteShader(fragment);
|
|
95
|
+
if (gl.getProgramParameter(next, gl.LINK_STATUS) !== true) {
|
|
96
|
+
const log = gl.getProgramInfoLog(next);
|
|
97
|
+
gl.deleteProgram(next);
|
|
98
|
+
throw new Error(log ?? "Could not link the WebGL program.");
|
|
99
|
+
}
|
|
100
|
+
return next;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function hexToRgb(value) {
|
|
104
|
+
const match = /^#([0-9a-f]{6})$/i.exec(typeof value === "string" ? value : "");
|
|
105
|
+
if (!match) {
|
|
106
|
+
return [0.47, 0.94, 0.78];
|
|
107
|
+
}
|
|
108
|
+
const number = Number.parseInt(match[1], 16);
|
|
109
|
+
return [((number >> 16) & 0xff) / 255, ((number >> 8) & 0xff) / 255, (number & 0xff) / 255];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export default {
|
|
113
|
+
validateProps(props) {
|
|
114
|
+
const errors = [];
|
|
115
|
+
for (const key of ["kicker", "title", "subtitle"]) {
|
|
116
|
+
if (typeof props[key] !== "string") {
|
|
117
|
+
errors.push(key + " must be a string");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (typeof props.accent !== "string" || !/^#[0-9a-f]{6}$/i.test(props.accent)) {
|
|
121
|
+
errors.push("accent must be a #rrggbb color");
|
|
122
|
+
}
|
|
123
|
+
return errors;
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
mount({ root, props }) {
|
|
127
|
+
const canvas = root.querySelector("#field");
|
|
128
|
+
fps = Number(root.getAttribute("fps") ?? 30);
|
|
129
|
+
durationFrames = Number(root.getAttribute("duration-in-frames") ?? 360);
|
|
130
|
+
|
|
131
|
+
gl = canvas.getContext("webgl", {
|
|
132
|
+
alpha: false,
|
|
133
|
+
antialias: false,
|
|
134
|
+
depth: false,
|
|
135
|
+
stencil: false,
|
|
136
|
+
preserveDrawingBuffer: true,
|
|
137
|
+
});
|
|
138
|
+
if (!gl) {
|
|
139
|
+
throw new Error("WebGL is unavailable. The production renderer pins SwiftShader.");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
program = linkProgram();
|
|
143
|
+
gl.useProgram(program);
|
|
144
|
+
|
|
145
|
+
buffer = gl.createBuffer();
|
|
146
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
147
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
148
|
+
const position = gl.getAttribLocation(program, "a_position");
|
|
149
|
+
gl.enableVertexAttribArray(position);
|
|
150
|
+
gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);
|
|
151
|
+
|
|
152
|
+
uniforms = {
|
|
153
|
+
frame: gl.getUniformLocation(program, "u_frame"),
|
|
154
|
+
resolution: gl.getUniformLocation(program, "u_resolution"),
|
|
155
|
+
duration: gl.getUniformLocation(program, "u_duration"),
|
|
156
|
+
accent: gl.getUniformLocation(program, "u_accent"),
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const accent = hexToRgb(props.accent);
|
|
160
|
+
gl.viewport(0, 0, canvas.width, canvas.height);
|
|
161
|
+
gl.uniform2f(uniforms.resolution, canvas.width, canvas.height);
|
|
162
|
+
gl.uniform1f(uniforms.duration, durationFrames);
|
|
163
|
+
gl.uniform3f(uniforms.accent, accent[0], accent[1], accent[2]);
|
|
164
|
+
gl.clearColor(0.016, 0.026, 0.038, 1);
|
|
165
|
+
|
|
166
|
+
labels = {
|
|
167
|
+
frame: root.querySelector("#frame"),
|
|
168
|
+
total: root.querySelector("#total"),
|
|
169
|
+
clock: root.querySelector("#clock"),
|
|
170
|
+
};
|
|
171
|
+
labels.total.textContent = String(durationFrames - 1).padStart(3, "0");
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
update({ frame }) {
|
|
175
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
176
|
+
gl.uniform1f(uniforms.frame, frame);
|
|
177
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
178
|
+
|
|
179
|
+
labels.frame.textContent = String(frame).padStart(3, "0");
|
|
180
|
+
labels.clock.textContent = (frame / fps).toFixed(2) + " S";
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
dispose() {
|
|
184
|
+
if (gl) {
|
|
185
|
+
if (buffer) {
|
|
186
|
+
gl.deleteBuffer(buffer);
|
|
187
|
+
}
|
|
188
|
+
if (program) {
|
|
189
|
+
gl.deleteProgram(program);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
gl = undefined;
|
|
193
|
+
program = undefined;
|
|
194
|
+
buffer = undefined;
|
|
195
|
+
uniforms = undefined;
|
|
196
|
+
labels = undefined;
|
|
197
|
+
durationFrames = 360;
|
|
198
|
+
fps = 30;
|
|
199
|
+
},
|
|
200
|
+
};
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<script type="application/json" data-fy="props">
|
|
6
|
+
{
|
|
7
|
+
"kicker": "Shader Study / 001",
|
|
8
|
+
"title": "Liquid Light",
|
|
9
|
+
"subtitle": "A frame-driven WebGL field, resolved in software so every captured pixel stays reproducible.",
|
|
10
|
+
"accent": "#78f0c8"
|
|
11
|
+
}
|
|
12
|
+
</script>
|
|
13
|
+
<style>
|
|
14
|
+
@keyframes rise {
|
|
15
|
+
from {
|
|
16
|
+
opacity: 0;
|
|
17
|
+
transform: translateY(44px);
|
|
18
|
+
}
|
|
19
|
+
to {
|
|
20
|
+
opacity: 1;
|
|
21
|
+
transform: translateY(0);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
@keyframes fade {
|
|
25
|
+
from {
|
|
26
|
+
opacity: 0;
|
|
27
|
+
}
|
|
28
|
+
to {
|
|
29
|
+
opacity: 1;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
@keyframes draw {
|
|
33
|
+
from {
|
|
34
|
+
opacity: 0;
|
|
35
|
+
transform: scaleX(0);
|
|
36
|
+
}
|
|
37
|
+
to {
|
|
38
|
+
opacity: 1;
|
|
39
|
+
transform: scaleX(1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
html,
|
|
43
|
+
body,
|
|
44
|
+
fy-composition {
|
|
45
|
+
background: #060a0e;
|
|
46
|
+
}
|
|
47
|
+
#scene {
|
|
48
|
+
position: relative;
|
|
49
|
+
width: 100%;
|
|
50
|
+
height: 100%;
|
|
51
|
+
overflow: hidden;
|
|
52
|
+
background: #060a0e;
|
|
53
|
+
color: #edf6f3;
|
|
54
|
+
font-family: Inter, ui-sans-serif, system-ui, sans-serif;
|
|
55
|
+
}
|
|
56
|
+
#field {
|
|
57
|
+
position: absolute;
|
|
58
|
+
inset: 0;
|
|
59
|
+
display: block;
|
|
60
|
+
width: 100%;
|
|
61
|
+
height: 100%;
|
|
62
|
+
}
|
|
63
|
+
#scrim {
|
|
64
|
+
position: absolute;
|
|
65
|
+
inset: 0;
|
|
66
|
+
background: linear-gradient(
|
|
67
|
+
90deg,
|
|
68
|
+
rgba(4, 8, 11, 0.94) 0%,
|
|
69
|
+
rgba(4, 8, 11, 0.78) 34%,
|
|
70
|
+
rgba(4, 8, 11, 0.18) 66%,
|
|
71
|
+
rgba(4, 8, 11, 0) 100%
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
#vignette {
|
|
75
|
+
position: absolute;
|
|
76
|
+
inset: 0;
|
|
77
|
+
background: radial-gradient(ellipse at 60% 45%, transparent 34%, rgba(0, 0, 0, 0.46) 100%);
|
|
78
|
+
pointer-events: none;
|
|
79
|
+
}
|
|
80
|
+
#kicker {
|
|
81
|
+
position: absolute;
|
|
82
|
+
top: 72px;
|
|
83
|
+
left: 148px;
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
gap: 16px;
|
|
87
|
+
font:
|
|
88
|
+
600 14px/1 ui-monospace,
|
|
89
|
+
monospace;
|
|
90
|
+
letter-spacing: 0.26em;
|
|
91
|
+
text-transform: uppercase;
|
|
92
|
+
color: var(--fy-prop-accent);
|
|
93
|
+
animation: fade 1s linear both;
|
|
94
|
+
}
|
|
95
|
+
#kicker::before {
|
|
96
|
+
content: "";
|
|
97
|
+
width: 8px;
|
|
98
|
+
height: 8px;
|
|
99
|
+
background: var(--fy-prop-accent);
|
|
100
|
+
box-shadow: 0 0 18px var(--fy-prop-accent);
|
|
101
|
+
}
|
|
102
|
+
#meta {
|
|
103
|
+
position: absolute;
|
|
104
|
+
top: 72px;
|
|
105
|
+
right: 148px;
|
|
106
|
+
text-align: right;
|
|
107
|
+
font:
|
|
108
|
+
500 13px/1.9 ui-monospace,
|
|
109
|
+
monospace;
|
|
110
|
+
letter-spacing: 0.12em;
|
|
111
|
+
color: #80908e;
|
|
112
|
+
animation: fade 1s linear both;
|
|
113
|
+
}
|
|
114
|
+
#meta strong {
|
|
115
|
+
color: #dce9e6;
|
|
116
|
+
font-weight: 500;
|
|
117
|
+
}
|
|
118
|
+
#title {
|
|
119
|
+
position: absolute;
|
|
120
|
+
top: 292px;
|
|
121
|
+
left: 148px;
|
|
122
|
+
width: 780px;
|
|
123
|
+
margin: 0;
|
|
124
|
+
font-size: 168px;
|
|
125
|
+
line-height: 0.88;
|
|
126
|
+
letter-spacing: -0.062em;
|
|
127
|
+
font-weight: 660;
|
|
128
|
+
animation: rise 1s cubic-bezier(0.215, 0.61, 0.355, 1) both;
|
|
129
|
+
}
|
|
130
|
+
#subtitle {
|
|
131
|
+
position: absolute;
|
|
132
|
+
top: 604px;
|
|
133
|
+
left: 152px;
|
|
134
|
+
width: 720px;
|
|
135
|
+
margin: 0;
|
|
136
|
+
font-size: 30px;
|
|
137
|
+
line-height: 1.45;
|
|
138
|
+
font-weight: 420;
|
|
139
|
+
color: #96a7a3;
|
|
140
|
+
animation: rise 1s cubic-bezier(0.215, 0.61, 0.355, 1) both;
|
|
141
|
+
}
|
|
142
|
+
#footer {
|
|
143
|
+
position: absolute;
|
|
144
|
+
left: 148px;
|
|
145
|
+
right: 148px;
|
|
146
|
+
bottom: 64px;
|
|
147
|
+
animation: fade 1s linear both;
|
|
148
|
+
}
|
|
149
|
+
#rule {
|
|
150
|
+
position: relative;
|
|
151
|
+
height: 1px;
|
|
152
|
+
background: rgba(237, 246, 243, 0.14);
|
|
153
|
+
transform-origin: left center;
|
|
154
|
+
animation: draw 1s cubic-bezier(0.215, 0.61, 0.355, 1) both;
|
|
155
|
+
}
|
|
156
|
+
#progress {
|
|
157
|
+
position: absolute;
|
|
158
|
+
inset: 0 auto 0 0;
|
|
159
|
+
width: calc(var(--fy-progress) * 100%);
|
|
160
|
+
background: var(--fy-prop-accent);
|
|
161
|
+
box-shadow: 0 0 16px var(--fy-prop-accent);
|
|
162
|
+
}
|
|
163
|
+
#footer-row {
|
|
164
|
+
display: flex;
|
|
165
|
+
justify-content: space-between;
|
|
166
|
+
align-items: baseline;
|
|
167
|
+
gap: 24px;
|
|
168
|
+
margin-top: 18px;
|
|
169
|
+
font:
|
|
170
|
+
500 13px/1 ui-monospace,
|
|
171
|
+
monospace;
|
|
172
|
+
letter-spacing: 0.14em;
|
|
173
|
+
color: #758581;
|
|
174
|
+
animation: fade 1s linear both;
|
|
175
|
+
}
|
|
176
|
+
#footer-row strong {
|
|
177
|
+
color: var(--fy-prop-accent);
|
|
178
|
+
font-weight: 600;
|
|
179
|
+
}
|
|
180
|
+
#footer-meta {
|
|
181
|
+
display: flex;
|
|
182
|
+
gap: 28px;
|
|
183
|
+
}
|
|
184
|
+
</style>
|
|
185
|
+
</head>
|
|
186
|
+
<body>
|
|
187
|
+
<fy-composition width="1920" height="1080" fps="30" duration-in-frames="360">
|
|
188
|
+
<div id="scene">
|
|
189
|
+
<canvas id="field" width="1920" height="1080"></canvas>
|
|
190
|
+
<div id="scrim"></div>
|
|
191
|
+
<div id="vignette"></div>
|
|
192
|
+
<div id="kicker" data-fy-text="kicker" data-fy-anim-start="0" data-fy-anim-duration="28"></div>
|
|
193
|
+
<div id="meta" data-fy-anim-start="18" data-fy-anim-duration="34">
|
|
194
|
+
<div><strong>WEBGL 1.0</strong> / SWIFTSHADER</div>
|
|
195
|
+
<div>1920 × 1080 · 30 FPS</div>
|
|
196
|
+
<div>12.00 S · 360 FRAMES</div>
|
|
197
|
+
</div>
|
|
198
|
+
<h1 id="title" data-fy-text="title" data-fy-anim-start="6" data-fy-anim-duration="56"></h1>
|
|
199
|
+
<p id="subtitle" data-fy-text="subtitle" data-fy-anim-start="34" data-fy-anim-duration="52"></p>
|
|
200
|
+
<div id="footer" data-fy-anim-start="28" data-fy-anim-duration="30">
|
|
201
|
+
<div id="rule" data-fy-anim-start="38" data-fy-anim-duration="60">
|
|
202
|
+
<div id="progress"></div>
|
|
203
|
+
</div>
|
|
204
|
+
<div id="footer-row" data-fy-anim-start="44" data-fy-anim-duration="30">
|
|
205
|
+
<span>FRAME <strong id="frame">000</strong> / <span id="total">359</span></span>
|
|
206
|
+
<span id="clock">0.00 S</span>
|
|
207
|
+
<span id="footer-meta"><span>FRAME-SEEKED</span><span>NO RAF</span></span>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
</fy-composition>
|
|
212
|
+
|
|
213
|
+
<script type="text/fy-controller" src="./webgl.controller.js"></script>
|
|
214
|
+
</body>
|
|
215
|
+
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frameyard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Local-first, deterministic video composition framework for HTML, CSS, and JavaScript.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -66,11 +66,13 @@
|
|
|
66
66
|
"build": "tsc -p tsconfig.json",
|
|
67
67
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
68
68
|
"typecheck:test": "tsc -p tsconfig.test.json",
|
|
69
|
+
"typecheck:browser": "tsc -p tsconfig.browser.json",
|
|
69
70
|
"check:scripts": "node scripts/check-scripts.mjs",
|
|
70
71
|
"lint": "oxlint",
|
|
71
72
|
"format": "oxfmt",
|
|
72
73
|
"format:check": "oxfmt --check",
|
|
73
|
-
"check:static": "pnpm check:scripts && pnpm lint && pnpm format:check && pnpm typecheck && pnpm typecheck:test",
|
|
74
|
+
"check:static": "pnpm check:scripts && pnpm lint && pnpm format:check && pnpm typecheck && pnpm typecheck:test && pnpm typecheck:browser",
|
|
75
|
+
"compare-stills": "node scripts/compare-stills.mjs",
|
|
74
76
|
"check": "pnpm check:static && pnpm test",
|
|
75
77
|
"test": "pnpm build && vitest run",
|
|
76
78
|
"test:unit": "vitest run test/contracts.test.ts test/docs.test.ts test/random.test.ts test/release.test.ts test/scaffold.test.ts test/transcript.test.ts",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: frameyard
|
|
3
3
|
description: Create, validate, inspect, and render deterministic Frameyard videos authored with HTML, CSS, and an optional JavaScript controller. Use when a user asks to build or operate a Frameyard project, produce parameterized video, add a local TTS or transcription asset through Frameyard's wrappers, or diagnose a Frameyard render. Do not use for general-purpose media generation outside a Frameyard project.
|
|
4
|
-
frameyard-api-version: 0.
|
|
4
|
+
frameyard-api-version: 0.5.0
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Frameyard
|
|
@@ -22,7 +22,7 @@ Use Frameyard's public project format and CLI. Do not introduce an agent-only ru
|
|
|
22
22
|
3. **Produce optional assets** with `npx frameyard tts` and `npx frameyard transcribe`, never with the upstream executables. Read [audio assets](references/audio-assets.md). Do not overwrite or regenerate existing generated assets without permission.
|
|
23
23
|
4. **Validate** with `npx frameyard check <id> --json`. Static validation runs before Chromium; then every frame is evaluated. Read the structured `diagnostics` array. Read [diagnostics](references/diagnostics.md) and [common mistakes](references/common-mistakes.md).
|
|
24
24
|
5. **Inspect** representative frames with `npx frameyard still <id> --frame <n> --out <file.png> --json`. View the PNGs when the host supports image viewing. If visual inspection is impossible, say so explicitly.
|
|
25
|
-
6. **Render** with `npx frameyard render <id> --out <file.mp4> [--props <file>] [--concurrency <n>] --json`.
|
|
25
|
+
6. **Render** with `npx frameyard render <id> [--out <file.mp4>] [--props <file>] [--concurrency <n>] --json`. The default output is `out/<id>.mp4` in the project root.
|
|
26
26
|
7. **Report** the project path, composition entry, controller, props, generated assets, provenance manifest, validation results, diagnostics, still paths and inspection status, final video path, and any failures.
|
|
27
27
|
|
|
28
28
|
Never claim success when `check` fails, a still is missing, a render exits nonzero, or visual inspection was skipped. A partial or unpublished render is not a successful result.
|
|
@@ -32,6 +32,7 @@ Never claim success when `check` fails, a still is missing, a render exits nonze
|
|
|
32
32
|
- Use `--json` for every non-interactive command. Parse stdout as the structured result; treat stderr as progress and diagnostics.
|
|
33
33
|
- Keep the current output files unless the user explicitly approves replacement. Use `--force` only with that approval.
|
|
34
34
|
- Use `--project <dir>` when not running from the project root.
|
|
35
|
+
- `render` writes to `out/<id>.mp4` in the project root unless `--out` is passed. Explicit `--out` paths are relative to the current working directory.
|
|
35
36
|
- Use `--concurrency <n>` only when faster local rendering is useful; the default is `1` and the maximum is `16`. It changes capture scheduling only, not composition behavior or output frame order.
|
|
36
37
|
- Use `npx frameyard doctor --json` before reporting an environment problem. It inspects without repairing or downloading.
|
|
37
38
|
- Read [JSON results](references/json-results.md) for result and error shapes, and [failure recovery](references/failure-recovery.md) for actionable error codes.
|
|
@@ -118,7 +118,7 @@ Static validation diagnostics include source locations. Runtime diagnostics may
|
|
|
118
118
|
"composition": "hello",
|
|
119
119
|
"entry": "compositions/hello.html",
|
|
120
120
|
"controller": { "kind": "inline" },
|
|
121
|
-
"path": "/absolute/path/hello.mp4",
|
|
121
|
+
"path": "/absolute/path/out/hello.mp4",
|
|
122
122
|
"width": 1920,
|
|
123
123
|
"height": 1080,
|
|
124
124
|
"fps": 30,
|
|
@@ -134,7 +134,7 @@ Static validation diagnostics include source locations. Runtime diagnostics may
|
|
|
134
134
|
}
|
|
135
135
|
```
|
|
136
136
|
|
|
137
|
-
A successful render means the final path exists and was published atomically. A nonzero exit means no new partial video was published. `concurrency` is the number of browser sessions actually used; it is capped by `durationInFrames` and by the CLI maximum of 16.
|
|
137
|
+
A successful render means the final path exists and was published atomically. A nonzero exit means no new partial video was published. Without `--out`, the final path is `out/<composition-id>.mp4` inside the project root. `concurrency` is the number of browser sessions actually used; it is capped by `durationInFrames` and by the CLI maximum of 16.
|
|
138
138
|
|
|
139
139
|
## tts
|
|
140
140
|
|