kinescript 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. kinescript/__init__.py +6 -0
  2. kinescript/cli.py +167 -0
  3. kinescript/color.py +89 -0
  4. kinescript/components.py +272 -0
  5. kinescript/docs.py +345 -0
  6. kinescript/ease.py +143 -0
  7. kinescript/fonts/IBMPlexMono-Bold.ttf +0 -0
  8. kinescript/fonts/IBMPlexMono-Medium.ttf +0 -0
  9. kinescript/fonts/IBMPlexMono-OFL.txt +93 -0
  10. kinescript/fonts/IBMPlexMono-Regular.ttf +0 -0
  11. kinescript/fonts/IBMPlexMono-SemiBold.ttf +0 -0
  12. kinescript/fonts/SpaceGrotesk-Bold.ttf +0 -0
  13. kinescript/fonts/SpaceGrotesk-Medium.ttf +0 -0
  14. kinescript/fonts/SpaceGrotesk-OFL.txt +93 -0
  15. kinescript/fonts/SpaceGrotesk-Regular.ttf +0 -0
  16. kinescript/fonts/SpaceGrotesk-SemiBold.ttf +0 -0
  17. kinescript/mcp.py +148 -0
  18. kinescript/media.py +220 -0
  19. kinescript/mesh.py +390 -0
  20. kinescript/models.py +339 -0
  21. kinescript/output.py +370 -0
  22. kinescript/presets.py +170 -0
  23. kinescript/props.py +185 -0
  24. kinescript/render.py +1204 -0
  25. kinescript/render3d.py +393 -0
  26. kinescript/scene.py +362 -0
  27. kinescript/schema.py +701 -0
  28. kinescript/shaders.py +79 -0
  29. kinescript/space.py +81 -0
  30. kinescript/svgpath.py +80 -0
  31. kinescript/timing.py +169 -0
  32. kinescript/verify.py +261 -0
  33. kinescript-1.1.0.dist-info/METADATA +192 -0
  34. kinescript-1.1.0.dist-info/RECORD +40 -0
  35. kinescript-1.1.0.dist-info/WHEEL +5 -0
  36. kinescript-1.1.0.dist-info/entry_points.txt +2 -0
  37. kinescript-1.1.0.dist-info/licenses/LICENSE +25 -0
  38. kinescript-1.1.0.dist-info/licenses/kinescript/fonts/IBMPlexMono-OFL.txt +93 -0
  39. kinescript-1.1.0.dist-info/licenses/kinescript/fonts/SpaceGrotesk-OFL.txt +93 -0
  40. kinescript-1.1.0.dist-info/top_level.txt +1 -0
kinescript/docs.py ADDED
@@ -0,0 +1,345 @@
1
+ """The scene reference printed by `kine docs` and served to agents by the MCP server.
2
+
3
+ Written for small models as well as large ones: rules first, one complete example, every default
4
+ stated, then recipes to copy. Names of motions, eases, shaders and components are filled in from
5
+ the code, so they cannot drift; tests/test_docs.py renders every JSON example in this text.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from .components import COMPONENT_DOCS
10
+ from .ease import EASES
11
+ from .presets import PRESETS
12
+ from .shaders import PRESETS as SHADERS
13
+
14
+ REFERENCE = """\
15
+ KINESCRIPT SCENE REFERENCE
16
+ A scene is one JSON object. Kinescript turns it into a video. Frame n depends only on the scene
17
+ and n, so any frame can be rendered alone.
18
+
19
+ RULES
20
+ 1. Write strict JSON: double quotes, no comments, no trailing commas.
21
+ 2. Every layer has a "type" and a unique "id".
22
+ 3. Times are in seconds. Sizes are in pixels. Colours are "#rrggbb" or "#rrggbbaa".
23
+ 4. x and y say where the layer's anchor goes. They default to 0, the TOP-LEFT corner of the
24
+ canvas. For the centre write "x": "50%", "y": "50%". The anchor defaults to "center".
25
+ 5. Layers draw in list order: later layers are on top. Put backgrounds first.
26
+ 6. A layer without "dur" or "until" stays until the scene ends. Nothing fades out unless you
27
+ give it an "exit", e.g. "exit": "fade".
28
+ 7. Use only the keys listed here. `kine check` rejects unknown keys and names the right one.
29
+ 8. After writing a scene, run `kine check scene.json` and fix every error before rendering.
30
+
31
+ A COMPLETE SCENE (copy it and change it)
32
+ {
33
+ "canvas": {"w": 1920, "h": 1080, "fps": 30, "bg": "#0a0c10"},
34
+ "layers": [
35
+ {"id": "bg", "type": "shader", "preset": "score", "opacity": 0.6},
36
+ {"id": "title", "type": "text", "text": "Launch **Day**", "x": "50%", "y": "45%", "size": 140,
37
+ "weight": 800, "fill": "#f2f0e8", "emph": "#ff633f", "at": 0.3, "dur": 3,
38
+ "split": "chars", "enter": {"name": "rise", "dur": 0.5, "stagger": 0.03}, "exit": "fade"},
39
+ {"id": "sub", "type": "text", "text": "October 2026", "x": "50%", "y": "60%", "size": 48,
40
+ "fill": "#9299a8", "at": "title.in+0.8", "until": "title.out", "enter": "fade", "exit": "fade"},
41
+ {"id": "bar", "type": "rect", "x": "50%", "y": "68%", "w": 320, "h": 6, "fill": "#ff633f",
42
+ "at": 1.2, "dur": 2, "enter": "wipe", "exit": "fade"}
43
+ ]
44
+ }
45
+ It is 3.3 s long: with canvas duration "auto", the scene ends when the last timed layer ends.
46
+
47
+ SCENE (top level)
48
+ canvas {"w": 1920, "h": 1080, "fps": 30, "bg": "#0a0c10", "duration": "auto"}
49
+ w and h 16..16384, fps 1..240. duration: seconds, or "auto" (the default: the
50
+ latest end of any layer). Vertical video: "w": 1080, "h": 1920.
51
+ layers the list of layers.
52
+ theme {"accent": "#ff633f"}: then "$accent" anywhere means that value.
53
+ markers {"drop": 12.5}: named times, usable as "at": "drop".
54
+ grade whole-frame look: {"contrast": 1.05, "saturate": 1.1, "vignette": 0.35, "grain": 0.05}
55
+ motionBlur {"samples": 6, "shutter": 0.5} (slower: renders 6 sub-frames per frame)
56
+ camera moves and turns the view in depth (see DEPTH AND CAMERA).
57
+ light how model layers are lit (see 3D MODELS).
58
+ audio [{"src": "music.mp3", "at": 0, "gain": -8, "fadeIn": 1, "fadeOut": 2, "trim": [0, 30]}]
59
+ gain is in dB (0 = unchanged, -6 = about half as loud). fadeOut ends where the audio
60
+ ends: the scene end, unless trim makes the audio shorter.
61
+ components your own reusable layer groups (see COMPONENTS).
62
+
63
+ TIME: "at" (start), "dur" (length), "until" (end)
64
+ 1.5 seconds
65
+ "45f" frames
66
+ "0:02.5" minutes:seconds
67
+ "title.in" when layer "title" starts; "title.out" when it ends; "title.mid" its middle
68
+ "title.out+0.2" 0.2 s after title ends; "title.in-10f" 10 frames before title starts
69
+ "prev.out" when the layer listed just before this one ends
70
+ "drop" a marker from "markers"
71
+ "until": "end" runs to the scene end
72
+ "at" defaults to 0. Inside a group, times count from the group's start.
73
+ A video without dur lasts as long as its trimmed clip.
74
+
75
+ KEYS EVERY LAYER HAS
76
+ id, type, at, dur, until
77
+ x, y position of the anchor. A number (px), "50%" (of the canvas, or of the group's
78
+ w and h), "50%-120", "10vw", "5vh", or an expression.
79
+ anchor center (default) | topleft | top | topright | left | right | bottomleft | bottom |
80
+ bottomright. Video, shader, and image without x default to topleft.
81
+ opacity 0..1 (default 1)
82
+ scale, scaleX, scaleY 1 = normal size
83
+ rotate degrees, clockwise
84
+ z, rotateX, rotateY, backface, fixed depth: see DEPTH AND CAMERA
85
+ blur px
86
+ blend normal | multiply | screen | overlay | add | lighten | darken | softLight |
87
+ difference | color
88
+ shadow {"x": 0, "y": 6, "blur": 16, "color": "#00000099"} (those are the defaults)
89
+ fx {"brightness": 0, "contrast": 1, "saturate": 1, "hue": 0, "exposure": 0,
90
+ "temperature": 0, "tint": 0, "gray": 0, "invert": 0}
91
+ enter, exit a motion (see ENTER AND EXIT)
92
+ hidden true skips the layer
93
+ allowOffscreen true stops check warning when the layer leaves the canvas
94
+ note free text, ignored
95
+
96
+ LAYER TYPES (defaults in brackets)
97
+ rect w [200] h [120] radius [0] fill [#f2f0e8 unless stroke is set] stroke strokeWidth [4]
98
+ dimOutside (0..1: darkens everything outside the rect, to highlight part of a video)
99
+ circle r [50] fill stroke strokeWidth
100
+ ellipse w h fill stroke strokeWidth
101
+ polygon sides [6] r [80] inner (0..1 makes a star) fill stroke strokeWidth
102
+ line points [[x, y], [x, y], ...] (measured from the layer's x, y) stroke strokeWidth
103
+ smooth closed
104
+ path d (SVG path data, e.g. "M0 0 L100 0") or points; fill stroke strokeWidth
105
+ arrow x1 y1 x2 y2 bend [0] head [22] stroke strokeWidth
106
+ text text (centred on x, y: the anchor is center), size [64], weight [600] (100..900), font ["Space Grotesk"; also "IBM Plex Mono",
107
+ any installed family, or a .ttf path], italic, fill [#f2f0e8], emph (colour of
108
+ words written **like this**), align [center] (left|center|right), maxWidth (wraps
109
+ lines), lineHeight [1.18], letterSpacing [0], stroke, strokeWidth,
110
+ box {"fill": "#000000b0", "pad": [12, 20], "radius": 10} (a background behind the
111
+ text; pad is [vertical, horizontal]), case (upper|lower),
112
+ split (chars|words|lines: animate each unit, see ENTER AND EXIT),
113
+ caret (true shows a cursor while typing),
114
+ unitX unitY unitRotate unitScale unitOpacity (expressions per unit; i is its index)
115
+ counter value (animate it, e.g. {"from": 0, "to": 1250, "dur": 2}), format ["{:,.0f}"; also
116
+ "${:,.2f}M" or "{:.0f}%"], and every text key
117
+ image src (every src, for image, video, captions and audio, is relative to the scene
118
+ file), w, h (give one and the other keeps the
119
+ aspect), fit [cover] (cover|contain|stretch), zoom [1] (zooms inside the frame),
120
+ focus [[0.5, 0.5]] (the point zoom moves toward), radius, stroke, strokeWidth
121
+ video src, trim [start, end] (seconds in the source file), speed [1], volume [1] (0 mutes),
122
+ loop, w, h [canvas size], fit, zoom, focus, radius. Its sound is mixed in.
123
+ captions cues [[start, end, "text"], ...] (seconds from the layer start) or src (.srt/.vtt),
124
+ mode (karaoke|pop: highlight the current word), active (colour of that word), and
125
+ the text keys. Defaults: x "50%", y "86%", size 46, weight 700, a dark box behind.
126
+ shader preset ({shaders}) with uniforms {"name": number or colour},
127
+ mesh and score are subtle; aurora and grid are strong, so use opacity 0.2-0.4.
128
+ Or sksl: your own SkSL "half4 main(float2 p) { ... return half4(r, g, b, a); }".
129
+ p is the pixel position (0..w, 0..h); p / iResolution is 0..1. The colour is
130
+ premultiplied: for a see-through colour return half4(rgb * a, a). iTime (seconds),
131
+ iResolution, iProgress (0..1) and your uniforms are declared for you.
132
+ Fills the canvas unless you give w and h.
133
+ model a lit 3D solid, 3D text, an extruded SVG path or a model file: shape, src or parts
134
+ (see 3D MODELS).
135
+ group children [layers], w, h, clip (true cuts children off at w x h), radius, fill.
136
+ Children are placed from the group's x, y and start at the group's "at".
137
+ sequence clips [{"src": "a.mp4", "trim": [0, 4]}, ...] and transition {"type": "fade",
138
+ "dur": 0.4} (fade|wipe|slide|zoom|blur|cut). Becomes back-to-back video layers named
139
+ <id>-1, <id>-2, ... Each transition overlaps the clips it joins, so 3 s + 3 s with a
140
+ 0.5 s fade lasts 5.5 s.
141
+
142
+ DEPTH AND CAMERA (3D)
143
+ Any layer can move and turn in depth. x is right, y is down and z is toward the viewer, in px,
144
+ as in CSS. A scene that uses depth is seen through a camera; at z 0 everything keeps its size.
145
+ z px toward the viewer. Near layers grow; far ones (negative z) shrink and drift less
146
+ when the camera moves.
147
+ rotateX degrees: positive tips the top edge away.
148
+ rotateY degrees: positive turns the right edge away. Past 90 you see the back, mirrored.
149
+ backface visible (default) | hidden: hidden skips the layer while its back faces the camera.
150
+ fixed true: a top-level layer ignores the camera's moves (captions are fixed).
151
+ sort on a group: "depth" draws its children far to near each frame, not in list order.
152
+ camera top level. Every key animates, and all default to 0 except fov:
153
+ {"x": 0, "y": 0, "z": 0, "rotateX": 0, "rotateY": 0, "rotate": 0, "fov": 40}
154
+ x and y pan (px). z moves the camera back; negative moves it in. rotateY orbits it
155
+ right around the canvas centre, rotateX up over it (looking down), rotate rolls it.
156
+ fov (5..150) sets how strong perspective looks; it does not change size at z 0.
157
+ Motions: flip turns in on the vertical axis (deg [90]), flip-up on the horizontal axis, tumble
158
+ spins in from depth, push comes forward from depth (dist [600]). With "split" each unit flips.
159
+ Layers still draw in list order, so put far layers first, or use a group with "sort": "depth".
160
+ A card that turns over: rotateY on the group, and each side hides its back:
161
+ {"id": "card", "type": "group", "x": "50%", "y": "50%", "rotateY": [[0.5, 0], [1.5, 180]], "children": [
162
+ {"id": "card-front", "type": "group", "anchor": "center", "w": 560, "h": 150, "radius": 28,
163
+ "fill": "#f2f0e8", "backface": "hidden", "children": [
164
+ {"id": "card-front-text", "type": "text", "text": "front", "size": 64, "fill": "#0a0c10"}]},
165
+ {"id": "card-back", "type": "group", "anchor": "center", "w": 560, "h": 150, "radius": 28,
166
+ "fill": "#ff633f", "rotateY": 180, "backface": "hidden", "children": [
167
+ {"id": "card-back-text", "type": "text", "text": "back", "size": 64, "fill": "#0a0c10"}]}]}
168
+ A whole scene: the camera pushes in and swings round layers at three depths; the note stays put:
169
+ {"canvas": {"w": 1920, "h": 1080, "fps": 30, "duration": 4},
170
+ "camera": {"z": [[0, 300], ["end", -100]], "rotateY": [[0, -12], ["end", 10]]},
171
+ "layers": [
172
+ {"id": "far", "type": "rect", "x": "50%", "y": "50%", "z": -600, "w": 1600, "h": 900, "radius": 40,
173
+ "fill": "#151922"},
174
+ {"id": "title", "type": "text", "text": "Depth", "x": "50%", "y": "45%", "size": 140, "weight": 800,
175
+ "split": "chars", "enter": {"name": "flip", "stagger": 0.05}},
176
+ {"id": "near", "type": "circle", "x": "78%", "y": "72%", "z": 250, "r": 60, "fill": "#ff633f"},
177
+ {"id": "note", "type": "text", "text": "fixed to the screen", "x": "50%", "y": "88%", "size": 40,
178
+ "fixed": true}]}
179
+
180
+ 3D MODELS
181
+ A model layer is a lit solid in the space of DEPTH AND CAMERA, so z, rotateX, rotateY, rotate,
182
+ scale and the camera apply to it. x and y place its centre.
183
+ shape cube (w [200], h and depth [= w]) | sphere (r [100]) | cylinder (r [80], h [200]) |
184
+ cone (r [90], h [200], tip up) | torus (r [110], tube [r x 0.35], facing you) |
185
+ plane (w, h; both sides show) | text | path. Sizes are px and animate.
186
+ text shape "text" pushes letters out in depth: text ("\n" breaks lines), size [120],
187
+ weight [600], font, italic, letterSpacing, lineHeight, depth [size x 0.2], bevel [0]
188
+ (px of 45-degree edge). Centred on x, y. Each new text, size, depth or bevel value
189
+ rebuilds the solid, so grow it with scale.
190
+ path shape "path": d (SVG path data, filled nonzero), w and/or h (fits inside them, keeping
191
+ its shape), depth [a fifth of its larger side], bevel [0], strokeWidth (in d's units:
192
+ extrudes the stroke, with round ends, for line icons).
193
+ detail low | medium (default) | high: how many triangles. A medium sphere has about 2,200.
194
+ fill one colour [theme accent] shine 0..1 [0.4] metal 0..1 [0] rim 0..1 [0.5] (edge
195
+ light) bands [0] (2 or more: flat toon steps). These animate.
196
+ parts [{"shape": "sphere", "r": 30, "x": 0, "y": -120, "z": 0, "rotateX": 0, "rotateY": 0,
197
+ "rotate": 0, "scale": 1, "fill": "#b9f45d", "shine": 0.9}, ...] placed from the
198
+ model's centre, in plain numbers (parts do not animate; the model does). A part takes
199
+ the model's material keys it does not set. A model may have shape (or src), parts, or both.
200
+ src a model file: .glb or .gltf (glTF 2.0), or .obj with its .mtl. Colours, shine and metal
201
+ come from the file; a fill on the layer recolours all of it. It fits inside w and/or h
202
+ keeping its shape, else its longest side is 300 px. Node transforms apply; animation,
203
+ skins and textures do not (a texture gives its mean colour). Long triangles are cut to
204
+ 1/24 of its size for the depth sort. Draco- or meshopt-compressed files are not read.
205
+ ground true or {"opacity": 0.5, "blur": px, "y": px}: a soft shadow on a level floor under the
206
+ model, as if lit from above, fainter as the model rises. The floor sits at the model's
207
+ lowest point before it turns, or at y in the layer's parent space (canvas px at the top
208
+ level): give y when the model moves up and down.
209
+ explode 0..1: faces fly out along their normals.
210
+ draw 0..1, or "enter": "build": triangles appear from the bottom up.
211
+ light top level, or on one model: studio (default) | soft | dramatic | flat (no shading),
212
+ or {"preset": "studio", "key": 0.85, "fill": 0.25, "ambient": 0.22, "rim": 0.35,
213
+ "from": "topleft", "color": "#fff4e6"}. from: topleft | top | topright | left | right |
214
+ front | bottomleft | bottom | bottomright. Lights move with the camera.
215
+ Triangles are sorted far to near within one model, so things that touch or pass through each
216
+ other go in one model as parts. Where two surfaces cut through each other the seam is stepped,
217
+ so let parts touch rather than cross. Separate layers draw in list order. check warns past
218
+ 60,000 triangles in a frame.
219
+ A ring around a ball that builds in, turns, and flies apart:
220
+ {"id": "emblem", "type": "model", "x": "50%", "y": "45%", "detail": "high",
221
+ "rotateX": [[0, 64], ["end", 18]], "rotateY": [[0, -30], ["end", 70]],
222
+ "enter": {"name": "build", "dur": 1.6}, "explode": [[3, 0], ["end", 1]],
223
+ "parts": [{"shape": "torus", "r": 250, "tube": 70, "fill": "#ff633f", "metal": 0.45, "shine": 0.7},
224
+ {"shape": "sphere", "r": 100, "fill": "#b9f45d", "shine": 0.9}]}
225
+ A 3D title that builds in, turns, and settles:
226
+ {"id": "title3d", "type": "model", "shape": "text", "text": "LAUNCH", "x": "50%", "y": "50%",
227
+ "size": 240, "weight": 800, "depth": 70, "bevel": 5, "shine": 0.6,
228
+ "rotateY": [[0, -40], [2.4, 0, "outCubic"]], "rotateX": [[0, 18], [2.4, 0, "outCubic"]],
229
+ "enter": {"name": "build", "dur": 1.2}}
230
+ A product turntable from a model file, on a soft floor shadow:
231
+ {"id": "product", "type": "model", "src": "rocket.glb", "x": "50%", "y": "46%", "h": 600,
232
+ "rotateY": [[0, 0], ["end", 360]], "ground": true, "enter": {"name": "build", "dur": 1.6}}
233
+
234
+ ANIMATING A VALUE (any number or colour key)
235
+ constant "x": 960
236
+ tween "opacity": {"from": 0, "to": 1, "dur": 0.6, "delay": 0, "ease": "outCubic"}
237
+ (those are the defaults; delay counts from the layer start)
238
+ keyframes "x": [[0, 200], [0.8, 960, "outBack"], ["end", 1700]]
239
+ Each keyframe is [time, value] or [time, value, ease into it]. Time is seconds from
240
+ the layer start; a negative time counts back from its end; "40%" is a share of its
241
+ length; "end" is its end. Between keyframes the default ease is inOutCubic.
242
+ A flat list such as [0, 100] is NOT keyframes.
243
+ expression "y": "=540 + sin(t * 3) * 40" (a string starting with "=")
244
+ t = seconds since the layer started d = the layer's length p = t / d (0..1)
245
+ T = seconds since the scene started i = unit index (split text) W, H, fps
246
+ helpers: sin cos tan sqrt abs min max round floor ceil exp log atan2 hypot pi tau
247
+ clamp(v, lo, hi) lerp(a, b, p) noise(x, seed) wiggle(freq, amp, t, seed)
248
+ ease("outCubic", p)
249
+ Colours animate too: "fill": {"from": "#ff633f", "to": "#b9f45d", "dur": 1}.
250
+ Gradient fill: "fill": {"linear": [[0, 0], [1, 1]], "stops": ["#ff633f", "#7b5cff"]}
251
+ or {"radial": [0.5, 0.5, 0.6], "stops": [...]}; points are 0..1 of the layer box.
252
+
253
+ ENTER AND EXIT
254
+ "enter": "rise" or {"name": "rise", "dur": 0.5, "ease": "outCubic", "stagger": 0.03}
255
+ Defaults: dur 0.5, ease outCubic (pop and spin: outBack; type: linear; draw: inOutCubic;
256
+ snap: outQuart). An exit plays during the LAST dur seconds of the layer and ends with it.
257
+ Combine with +: "rise+blur". Options: dist (px moved by rise, drop, left, right; default 48),
258
+ from (start scale of pop [0.4] and zoom [1.25]), amount (blur px [24], track px [40]).
259
+ With "split" on text, each unit enters in turn: unit k starts k x stagger seconds after the
260
+ layer starts, so the whole reveal takes dur + (units - 1) x stagger.
261
+ motions: {motions}
262
+ rise moves up into place, drop moves down, left moves left, right moves right. type is a
263
+ typewriter. draw draws a stroke or path on, and builds a model (build is the same). wipe reveals left to right. snap is a hard cut in
264
+ with a small settle. flip, flip-up, tumble and push move in depth (see DEPTH AND CAMERA).
265
+ none is no motion.
266
+
267
+ EASES {eases}, spring, spring(stiffness, damping), bezier(x1, y1, x2, y2), steps(n)
268
+ in* starts slow, out* ends slow (the most natural for entrances), inOut* does both.
269
+
270
+ COMPONENTS {"use": "<name>", "id": "...", "at": ..., "dur": ..., ...parameters}
271
+ Built in (parameters):
272
+ {components}
273
+ x and y place the component as noted above. Components also accept enter, exit, opacity,
274
+ scale, rotate, hidden.
275
+ Your own: "components": {"badge": {"params": {"label": "NEW"}, "layers": [
276
+ {"type": "text", "text": "{{label}}", "size": 40}]}}
277
+ then a layer {"use": "badge", "label": "BETA", "x": 200, "y": 100}.
278
+
279
+ RECIPES (layers to paste into "layers")
280
+ Title that types on:
281
+ {"id": "hello", "type": "text", "text": "Hello world", "x": "50%", "y": "50%", "size": 120,
282
+ "enter": {"name": "type", "dur": 1.2}, "caret": true}
283
+ Words one after another, each replacing the last, centred at x, y:
284
+ {"use": "kinetic", "id": "words", "words": ["Write", "Check", "Render"], "every": 0.6}
285
+ Pill label:
286
+ {"id": "pill", "type": "text", "text": "NEW", "x": "50%", "y": "50%", "size": 40,
287
+ "fill": "#0a0c10", "box": {"fill": "#b9f45d", "pad": [10, 24], "radius": 999}}
288
+ Bar of a bar chart growing up from a baseline at y 900, with its value riding on top and its
289
+ label under the baseline. The value's y uses the same tween as the bar's height (same dur,
290
+ same default ease), so it stays 40 px above the bar. The label is anchored at its top edge,
291
+ 20 px below the baseline. Keep text off the bar: light text over the orange fails the
292
+ contrast check:
293
+ {"id": "bar1", "type": "rect", "x": 600, "y": 900, "anchor": "bottom", "w": 120,
294
+ "h": {"from": 0, "to": 400, "dur": 0.8}, "fill": "#ff633f", "at": 0.5}
295
+ {"id": "bar1-value", "type": "counter", "x": 600, "y": {"from": 860, "to": 460, "dur": 0.8},
296
+ "size": 48, "at": 0.5, "value": {"from": 0, "to": 80, "dur": 0.8}, "format": "{:.0f}"}
297
+ {"id": "bar1-label", "type": "text", "text": "Q1", "x": 600, "y": 920, "anchor": "top",
298
+ "size": 40, "at": 0.5}
299
+ Slow zoom on a photo (Ken Burns):
300
+ {"id": "photo", "type": "image", "src": "photo.jpg", "w": 1920, "h": 1080, "anchor": "topleft",
301
+ "zoom": [[0, 1], ["end", 1.15, "linear"]], "focus": [0.6, 0.4]}
302
+ Two clips with a crossfade:
303
+ {"id": "cut", "type": "sequence", "clips": [{"src": "a.mp4", "trim": [0, 4]},
304
+ {"src": "b.mp4", "trim": [2, 6]}], "transition": {"type": "fade", "dur": 0.5}}
305
+ Full-screen gradient background:
306
+ {"id": "bg", "type": "rect", "x": 0, "y": 0, "anchor": "topleft", "w": "100%", "h": "100%",
307
+ "fill": {"linear": [[0, 0], [1, 1]], "stops": ["#1e3a8a", "#6d28d9"]}}
308
+ Progress bar along the bottom for the whole video:
309
+ {"use": "progress", "id": "progress", "color": "#ff633f", "h": 8}
310
+ Music under everything: top-level "audio": [{"src": "music.mp3", "gain": -10, "fadeIn": 1, "fadeOut": 2}]
311
+
312
+ COMMANDS
313
+ kine new scene.json write a starter scene
314
+ kine check scene.json JSON list of problems. error: must fix. warning: should fix:
315
+ text off the canvas or inside the outer 5% (safe-area),
316
+ overlapping text, contrast under 3:1, or text shown for less
317
+ than about 0.4 s + 0.3 s per word (read-time).
318
+ info: a name it read for you, e.g. "'color' read as 'fill'".
319
+ kine look scene.json --boxes one PNG of 12 evenly spaced frames; --boxes outlines layers and
320
+ labels the top-level ones; -n 16 frames, --at 1 2.5 4 exact
321
+ times (use it to see a transition), --width 480, --cols 4, -o
322
+ kine still scene.json --at 2.5 one frame as PNG (-o to name it); --at -1 is 1 s before the
323
+ end; --at end is the last frame
324
+ kine probe scene.json --at 2 every layer's start and end, and screen boxes at 2 s
325
+ kine probe clip.mp4 a media file's duration, w, h, fps and whether it has audio
326
+ kine render scene.json -o out.mp4 -q draft|standard|high
327
+ standard is the default, draft the fastest, high the best.
328
+ Also .mov or .webm (with --transparent for no background),
329
+ .gif (longest side 720 px, 15 fps), or a folder/ for PNG frames.
330
+ --scale 0.5 halves the size; --from 2 --to 4 renders a part.
331
+ kine watch scene.json re-makes the contact sheet on every save
332
+ kine doctor checks ffmpeg, the GPU and fonts
333
+ kine mcp the same tools for MCP clients
334
+
335
+ WORKFLOW FOR AGENTS
336
+ 1. Write the scene. 2. kine check: fix every error, read the warnings, run it again.
337
+ 3. kine look --boxes, and look at the image. 4. kine render -q draft, then the final render.
338
+ """
339
+
340
+
341
+ def reference() -> str:
342
+ comps = "\n".join(f" {k:<11}{v}" for k, v in COMPONENT_DOCS.items())
343
+ eases = " ".join(sorted(k for k in EASES if k not in ("in", "out", "inOut")))
344
+ return (REFERENCE.replace("{motions}", " ".join(sorted(PRESETS)))
345
+ .replace("{shaders}", "|".join(SHADERS)).replace("{components}", comps).replace("{eases}", eases))
kinescript/ease.py ADDED
@@ -0,0 +1,143 @@
1
+ """Easing curves. Every curve maps [0,1] -> roughly [0,1] (back/elastic overshoot)."""
2
+ from __future__ import annotations
3
+
4
+ import math
5
+ import re
6
+ from functools import lru_cache
7
+ from typing import Callable
8
+
9
+ Ease = Callable[[float], float]
10
+
11
+
12
+ def _out(f: Ease) -> Ease:
13
+ return lambda t: 1.0 - f(1.0 - t)
14
+
15
+
16
+ def _inout(f: Ease) -> Ease:
17
+ return lambda t: f(2 * t) / 2 if t < 0.5 else 1 - f(2 - 2 * t) / 2
18
+
19
+
20
+ def _back(t: float, s: float = 1.70158) -> float:
21
+ return t * t * ((s + 1) * t - s)
22
+
23
+
24
+ def _elastic(t: float) -> float:
25
+ if t in (0.0, 1.0):
26
+ return t
27
+ return -(2 ** (10 * t - 10)) * math.sin((t * 10 - 10.75) * (2 * math.pi) / 3)
28
+
29
+
30
+ def _bounce_out(t: float) -> float:
31
+ n, d = 7.5625, 2.75
32
+ if t < 1 / d:
33
+ return n * t * t
34
+ if t < 2 / d:
35
+ t -= 1.5 / d
36
+ return n * t * t + 0.75
37
+ if t < 2.5 / d:
38
+ t -= 2.25 / d
39
+ return n * t * t + 0.9375
40
+ t -= 2.625 / d
41
+ return n * t * t + 0.984375
42
+
43
+
44
+ _BASE: dict[str, Ease] = {
45
+ "sine": lambda t: 1 - math.cos(t * math.pi / 2),
46
+ "quad": lambda t: t * t,
47
+ "cubic": lambda t: t ** 3,
48
+ "quart": lambda t: t ** 4,
49
+ "quint": lambda t: t ** 5,
50
+ "expo": lambda t: 0.0 if t == 0 else 2 ** (10 * t - 10),
51
+ "circ": lambda t: 1 - math.sqrt(max(0.0, 1 - t * t)),
52
+ "back": _back,
53
+ "elastic": _elastic,
54
+ "bounce": lambda t: 1 - _bounce_out(1 - t),
55
+ }
56
+
57
+ EASES: dict[str, Ease] = {"linear": lambda t: t, "hold": lambda t: 0.0 if t < 1 else 1.0}
58
+ for _n, _f in _BASE.items():
59
+ _c = _n.capitalize()
60
+ EASES["in" + _c] = _f
61
+ EASES["out" + _c] = _out(_f)
62
+ EASES["inOut" + _c] = _inout(_f)
63
+ # Short aliases agents reach for first.
64
+ EASES.update({"in": EASES["inCubic"], "out": EASES["outCubic"], "inOut": EASES["inOutCubic"],
65
+ "smooth": lambda t: t * t * (3 - 2 * t)})
66
+
67
+
68
+ def _bezier(x1: float, y1: float, x2: float, y2: float) -> Ease:
69
+ def sample(a1: float, a2: float, u: float) -> float:
70
+ return 3 * a1 * u * (1 - u) ** 2 + 3 * a2 * u * u * (1 - u) + u ** 3
71
+
72
+ def f(x: float) -> float:
73
+ lo, hi = 0.0, 1.0
74
+ for _ in range(24):
75
+ mid = (lo + hi) / 2
76
+ if sample(x1, x2, mid) < x:
77
+ lo = mid
78
+ else:
79
+ hi = mid
80
+ return sample(y1, y2, (lo + hi) / 2)
81
+ return f
82
+
83
+
84
+ def _spring(stiffness: float = 170, damping: float = 26, mass: float = 1) -> Ease:
85
+ """Critically/under-damped spring normalised so it settles at t=1."""
86
+ w0 = math.sqrt(stiffness / mass)
87
+ zeta = damping / (2 * math.sqrt(stiffness * mass))
88
+ span = max(0.3, -math.log(0.001) / max(zeta * w0, 1e-3))
89
+
90
+ def raw(t: float) -> float:
91
+ s = t * span
92
+ if zeta < 1:
93
+ wd = w0 * math.sqrt(1 - zeta * zeta)
94
+ return 1 - math.exp(-zeta * w0 * s) * (math.cos(wd * s) + zeta * w0 / wd * math.sin(wd * s))
95
+ return 1 - math.exp(-w0 * s) * (1 + w0 * s)
96
+
97
+ end = raw(1.0)
98
+ # Spread the residual so the curve lands exactly on 1 and the last frame does not jump.
99
+ return lambda t: 1.0 if t >= 1 else raw(t) + (1 - end) * t
100
+
101
+
102
+ @lru_cache(maxsize=256)
103
+ def get(name: str | None) -> Ease:
104
+ """Resolve an ease by name: 'outCubic', 'spring', 'spring(120,12)', 'bezier(.2,.8,.2,1)', 'steps(4)'."""
105
+ if not name:
106
+ return EASES["outCubic"]
107
+ if name in EASES:
108
+ return EASES[name]
109
+ m = re.fullmatch(r"(\w+)\(([^)]*)\)", name.replace(" ", ""))
110
+ if m:
111
+ kind, args = m.group(1), [float(a) for a in m.group(2).split(",") if a]
112
+ if kind in ("bezier", "cubicBezier") and len(args) == 4:
113
+ return _bezier(*args)
114
+ if kind == "spring":
115
+ return _spring(*args)
116
+ if kind == "steps" and args:
117
+ n = max(1, int(args[0]))
118
+ return lambda t: min(1.0, math.floor(t * n) / n) if t < 1 else 1.0
119
+ if name == "spring":
120
+ return _spring()
121
+ c = canonical(name)
122
+ if c:
123
+ return get(c)
124
+ from .schema import unknown
125
+ raise ValueError(unknown("ease", name, list(EASES) + ["spring"]) + ", spring(k,d), bezier(a,b,c,d), steps(n)")
126
+
127
+
128
+ CSS = {"ease": "inOutSine", "easein": "inCubic", "easeout": "outCubic", "easeinout": "inOutCubic",
129
+ "linear": "linear", "cubicbezier": "bezier"}
130
+
131
+
132
+ def canonical(name: str) -> str | None:
133
+ """'easeOutCubic', 'ease-out-cubic', 'EaseInOut', CSS 'ease-in' -> Kinescript names; None when nothing fits."""
134
+ s = re.sub(r"[-_ ]", "", str(name)).lower()
135
+ m = re.fullmatch(r"(\w+?)(\(.*\))", s)
136
+ if m and m.group(1) in ("cubicbezier", "bezier", "spring", "steps"):
137
+ return ("bezier" if "bezier" in m.group(1) else m.group(1)) + m.group(2)
138
+ if s in CSS:
139
+ return CSS[s]
140
+ if s.startswith("ease"):
141
+ s = s[4:]
142
+ low = {k.lower(): k for k in EASES}
143
+ return low.get(s)
Binary file
Binary file
@@ -0,0 +1,93 @@
1
+ Copyright © 2017 IBM Corp. with Reserved Font Name "Plex"
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+
5
+ This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file
Binary file
Binary file