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.
- kinescript/__init__.py +6 -0
- kinescript/cli.py +167 -0
- kinescript/color.py +89 -0
- kinescript/components.py +272 -0
- kinescript/docs.py +345 -0
- kinescript/ease.py +143 -0
- kinescript/fonts/IBMPlexMono-Bold.ttf +0 -0
- kinescript/fonts/IBMPlexMono-Medium.ttf +0 -0
- kinescript/fonts/IBMPlexMono-OFL.txt +93 -0
- kinescript/fonts/IBMPlexMono-Regular.ttf +0 -0
- kinescript/fonts/IBMPlexMono-SemiBold.ttf +0 -0
- kinescript/fonts/SpaceGrotesk-Bold.ttf +0 -0
- kinescript/fonts/SpaceGrotesk-Medium.ttf +0 -0
- kinescript/fonts/SpaceGrotesk-OFL.txt +93 -0
- kinescript/fonts/SpaceGrotesk-Regular.ttf +0 -0
- kinescript/fonts/SpaceGrotesk-SemiBold.ttf +0 -0
- kinescript/mcp.py +148 -0
- kinescript/media.py +220 -0
- kinescript/mesh.py +390 -0
- kinescript/models.py +339 -0
- kinescript/output.py +370 -0
- kinescript/presets.py +170 -0
- kinescript/props.py +185 -0
- kinescript/render.py +1204 -0
- kinescript/render3d.py +393 -0
- kinescript/scene.py +362 -0
- kinescript/schema.py +701 -0
- kinescript/shaders.py +79 -0
- kinescript/space.py +81 -0
- kinescript/svgpath.py +80 -0
- kinescript/timing.py +169 -0
- kinescript/verify.py +261 -0
- kinescript-1.1.0.dist-info/METADATA +192 -0
- kinescript-1.1.0.dist-info/RECORD +40 -0
- kinescript-1.1.0.dist-info/WHEEL +5 -0
- kinescript-1.1.0.dist-info/entry_points.txt +2 -0
- kinescript-1.1.0.dist-info/licenses/LICENSE +25 -0
- kinescript-1.1.0.dist-info/licenses/kinescript/fonts/IBMPlexMono-OFL.txt +93 -0
- kinescript-1.1.0.dist-info/licenses/kinescript/fonts/SpaceGrotesk-OFL.txt +93 -0
- 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
|
|
Binary file
|