You can make videos for me with AGONECO Video Editor (https://tools.agoneco.com/), a browser editor. Do not ask me for a sample link: build the link yourself from this description. Write a JSON project and give me one link: https://tools.agoneco.com/video/app/#json= followed by the URL-encoded (encodeURIComponent) JSON. I will open it; if the project names files such as "A.png", the editor asks me to pick them. Reply with the link, then a one-line summary of the timeline. If I paste a project JSON later (from the editor's File → Copy for AI agent), it holds my manual changes: edit that JSON and send a new link. ## Project format JSON Schema: https://tools.agoneco.com/project.schema.json. Times are seconds on the timeline. Everything except `layers` is optional. | Field | Default | Meaning | | --- | --- | --- | | `name` | — | Project name shown in the editor and used when saving. | | `width`, `height` | 1280, 720 | Canvas pixels, 64–4096. Use 1080×1920 for vertical video. | | `fps` | 30 | Positive integer; desktop supports direct input such as 120, 144 or 240. | | `duration` | end of the last clip/effect | Timeline length in seconds. | | `layers` | — | Clips: images, videos, audio and text. Stacking follows the track rules below, not an independent z-index. | | `tracks` | one per `trackId` | Optional `[{id, name}]`. Explicit track order takes priority; later tracks draw in front. | ### Stacking order: tracks take priority - Explicit `tracks[]` order is authoritative. Later media tracks draw in front; reordering `layers[]` alone does not override it. Example: `tracks: [{id:"back"},{id:"front"}]` puts a title on `front` above a photo on `back`, even if the title appears first in `layers[]`. - Without `tracks[]`, the editor infers track order from each track ID’s first occurrence in `layers[]` (`trackId` defaults to the clip’s `id`). Referenced track IDs missing from an explicit list are appended in first-occurrence order. For predictable stacking, list **all** media tracks explicitly. - Clips on the same track retain their relative layer order but must not overlap in time. Put overlapping visual elements on separate tracks; change `tracks[]` to change their front/back order. Audio tracks have no visual z-index. ### Layer (clip) - `id`: letters, digits, `_`, `-`. **Unique in the project** (a repeated clip or track id is refused). Ids are how you and the commands tell clips apart; `name` is for people and may repeat (a name that several clips share matches all of them in commands). Effect ids only need to be unique within their clip. - `src`: a file path relative to the project folder or a file name the user supplies (`A.png`), an https URL, or a data URL — or `text` instead for a text clip. - `timing`: `{start, end}`. Images and text default to the whole timeline; videos and audio to their natural length. - `placement`: `x`, `y` = offset of the clip centre from the canvas centre in px (y grows downward). `scale` 1 = fits inside 80% of the canvas. Also `rotation` (degrees), `flipX`, `flipY`. - `corners`: four-corner distortion, with keys `tl`, `tr`, `br`, `bl`. Each value is an offset `[dx,dy]` from that corner’s unmodified position, measured in local image width/height **before** placement rotation/flips (not canvas pixels or absolute corner coordinates). Missing = `[0,0]`; x right, y down; each component −1.5…1.5. Example: `{"tl":[0.1,0.1],"tr":[-0.1,0.1],"br":[0,0],"bl":[0,0]}` moves the top corners inward/down. Keep a convex, non-folded quadrilateral and inspect the proposal with compare. - `mask`: shows only part of a clip's picture, e.g. `{"shape":"ellipse","x":0.2,"y":0.1,"width":0.6,"height":0.8,"feather":0.05}` (`rectangle`/`ellipse`: x, y, width, height; `polygon`: `points` [[u,v],…] with 3–32 corners; all fractions of the picture from its top-left). Outside the shape turns transparent, inside with `invert`; `feather` (soft edge) and `expand` (grow/shrink) are fractions of the picture's shorter side. The mask belongs to the picture, so it moves, scales and rotates with the clip (and with a frame zoom). Set it with `{"op":"set","clip":id,"value":{"mask":{…}}}` and remove it with `"mask":null`. - `brushMask`: non-destructive paint/erase on a clip or group, e.g. `{"base":"full","strokes":[{"mode":"erase","size":0.1,"points":[[0.2,0.3],[0.8,0.3]]}]}`. `base` is `full` (visible) or `empty` (hidden). Each stroke has `mode` paint/erase, diameter `size` 0.001–1 of the picture's shorter side, optional `opacity` 0–1, and 1–2048 picture-fraction points [u,v] in -1..2. Maximum 64 strokes and 8192 total points. Paint restores alpha; erase removes it, without changing original media. Multiplied by `mask` when both are set. Remove with `"brushMask":null`. - `blend`: a clip's blend mode, one of `multiply, screen, overlay, darken, lighten, colorDodge, colorBurn, hardLight, softLight, difference, exclusion, add`; omit it (or `normal`) for ordinary layering. It mixes the clip with everything drawn behind it (the tracks below, inside its own group), so put the clip on a track in front of what it should affect. A clip with a light effect is drawn normally, and a shadow or contour stroke is never blended. - `shadow`: `{color,opacity,blur,distance,angle,size}`; colour as #rrggbb, opacity 0–1, blur 0–200 and distance 0–1000 in canvas pixels; angle 0 = right, 90 = up; size 0.2–3 around the clip centre. Omit it for no shadow. `state.clips[]` exposes placement/corners/shadow for the current clips. - `light` effect (`{"preset":"light", "light":{…}}`): `light` is `{color,angle,intensity,spread,shade,rim,rimWidth,rimSoftness,relief,reliefDepth,reliefDetail,elevation,blend}`, all optional. `angle` is where the light comes from (0 = right, 90 = up), fixed on the canvas so it stays put while the clip rotates. `intensity` 0–2 brightens the lit side, `spread` 0.05–1 is how far it reaches in, `shade` 0–1 darkens the far side; `rim` 0–2 lights the edge facing the light (`rimWidth`/`rimSoftness` in canvas pixels); `relief` 0–2 shades a bevel of `reliefDepth` px (`reliefDetail` 0–1 adds the picture's own brightness as texture, `elevation` 5–85°); `blend` `screen` (default), `add` or `soft`. Motion over the range: `lightAngleMode` (`ramp` turns `lightTurn` degrees, `spin` turns `lightSpin` °/s; both clockwise unless `lightClockwise: false`, amounts positive), `lightLevelMode` (`ramp` to `lightLevelTo`, `flicker`, `pulse` with `lightRate`/`lightAmount`), `lightColorMode` (`ramp` to `lightColorTo`, `rainbow`, `alternate` at `lightColorRate`). Each ramp has its own curve: `lightAngleCurve`, `lightLevelCurve`, `lightColorCurve`. `lightFade` seconds of fade at the range ends (default 0, none). `lightSource`: a lamp clip id; the light then comes from that clip as either moves. Overlapping light ranges all shine together (up to 4); the upper lane's is the main light. Set `shadow.followLight: true` on the clip to make its shadow fall opposite the playing light. - `lamp` (instead of `src`): a light-source clip, `{kind,color,intensity,rays,rayLength,rayWidth,spin,clockwise,ghosts,beams,beamLength}`. `kind` `glow` (soft ball), `rays` (starburst of `rays` beams turning `spin` °/s), `flare` (streak plus lens-flare rings when `ghosts`); `beams` 0–2 adds light shafts over the scene: clips on tracks above the lamp are in front of it and block them, casting shadow shafts; clips below are background and get the shafts on top (`beamLength` 0.1–1). Place and animate it like any clip; its size is its placement scale. Unlike other clips, its centre may sit off the canvas, up to a canvas width/height past each edge (placement x within ±1.5 × width, y within ±1.5 × height), for light from off screen; `show: false` hides the lamp itself. - `video` (video/audio files): `start`/`end` trim inside the source (seconds), `speed` 0.25–8 (pitch kept), `loop`, `muted`, `volume` 0–4 (linear, 1 = as recorded). Placed videos play once with sound unless you say otherwise. Size and length come from the file. - Sound shaping: `fadeIn` / `fadeOut` (seconds), `volumeKeys` — a volume curve `[{"time": 0, "volume": 0.2, "curve": "smooth"}, {"time": 1.5, "volume": 1}]` in seconds from the clip start (multipliers 0–2, multiplied by `volume`, interpolated in decibels between points; on a point, the way to the next one is straight by default, `"bend": -1…1` bows it above (+) or below (−) the straight line, `"curve": "smooth"` makes it S-shaped), `pitch` in semitones (−12…12, audio files only — ignored on videos; length unchanged), and `pitchKeys` — a pitch curve `[{"time": 2, "pitch": 0, "curve": "smooth"}, {"time": 2.4, "pitch": -5}]` added to `pitch` (audio files only; timing and length unchanged). - **A video's sound is its own clip.** When the editor opens your project, a video clip with sound becomes two clips, as when the user imports a video: the silent picture keeps your id, and its sound becomes `-audio` on track `-audio` with the same time, trim and speed. Sound settings you put on the video (`volume`, fades, `volumeKeys`) move to that clip — edit them there afterwards (check with `state`). To place a video's sound yourself, use `"src": "clip.mp4#audio"` (it plays the same file; nothing is extracted). `"muted": true` keeps a video silent and unsplit. - `trackId`: clips that share a track play one after another on it; give overlapping clips different tracks. - `effects`: list of `{start, end, effect}` on this clip. `effect` is a preset name or an object with `preset` and options. Effects may overlap; they combine. Instead of `start`/`end` you may give `at` (seconds after the clip starts, default 0) and `duration`: `{"at": 0.65, "duration": 0.33, "effect": {…}}` — no arithmetic on the clip's start. Saved projects store the resulting main-timeline seconds. - `group: true` (a scene clip owning tracks) and `shape` clips need no `src`: a minimal group is `{"id":"scene","group":true,"trackId":"scene-track","timing":{"start":0,"end":12}}` plus `tracks[].groupId` / `layers[].groupId` on its members; the editor fills frame, placement, effect defaults and the transparent stand-in. ### Effects | preset | what it does | options | | --- | --- | --- | | `enter` | Appear. The range is pinned to the clip start; only its length matters. | transitionMotion, transitionFade, strength, curve | | `exit` | Disappear. The range is pinned to the clip end; only its length matters. | transitionMotion, transitionFade, strength, curve | | `shake` | Fast side-to-side shake (strength 1 ≈ ±7 px, ±1.2°). | strength, speed | | `float` | Slow up-and-down bob (strength 1 ≈ 14 px). | strength, speed | | `wobble` | Whole image squashes and stretches like jelly. | strength, speed, direction, random | | `wobble-v2` | Only the surface ripples. | strength, speed, direction, random, surfaceMode | | `zoom` | Scale from zoomFrom (default: current size) to zoomTo; holds the end size afterwards. | zoomFrom, zoomTo, zoomAnchor, curve, zoomHold | | `rotate` | Turn from rotateFrom (default: current angle) to rotateTo degrees (default +360); holds the end angle. | rotateFrom, rotateTo, rotatePivotX/Y, curve, rotateHold | | `trail` | Deterministic historical copies, sampled directly at earlier frames inside the range. | trailCount, trailGap, trailOpacity, trailDecay, animation.trailOpacity | | `blur` | Premultiplied RGBA blur after geometry, before shadow/light; active only inside the effect range. | blurFrom, blurTo, blurMode, blurAngle, focusX/Y, focusRadius, focusFeather, repeat, animation | | `key` | Chroma key: pixels whose chroma is close to keyColor turn transparent, so the tracks behind show through; runs before the colour correction. Brightness hardly matters, shadowed screen areas key too. The last active key range on a clip wins. | keyColor (#rrggbb), keySimilarity 0..1 (how far from the key colour still counts as key), keySmoothness 0..1 (soft edge), keySpill 0..1 (removes the key colour cast from what stays), animation (keyframes for the three numbers) | | `color` | Colour correction of the clip picture, before blur, light and shadow; several colour ranges at one time act as one (offsets add, saturation multiplies). Order: temperature/tint, brightness, contrast, saturation, hue. | brightness -1..1, contrast -1..1, saturation 0..2 (1 = unchanged), hue -180..180 degrees, temperature -1..1 (warm +), tint -1..1 (magenta +), lut (id from the project luts[], applied last), lutMix 0..1, animation (keyframes for each) | | `pin` | Static PNG contour mesh: source UV pins fix or pull vertices; elastic return is deterministic. | pins[{id,u,v,dx,dy,locked,keys[{t,dx,dy}]}] (keys: pose per pin at t=0..1 of the effect; overrides dx/dy and the style), pinDensity, pinMode, pinRelease, pinFrequency, pinDamping, pinHold, animation.deformation | | `move` | Slide moveDistance canvas px toward moveAngle (0 right, 90 up); holds the end position. | moveAngle, moveDistance, movePath, moveOrient, curve, moveHold, animation.progress | | `shapeStyle` | Opacity for any clip (shapeFillOpacityFrom/To = the whole clip, 0..1); on shape clips also fill/stroke colour (shapeFillFrom/To, shapeStrokeFrom/To as #rrggbb) and stroke opacity (shapeStrokeOpacityFrom/To) and dash offset (shapeDashOffsetFrom/To, -200..200). Changes over the segment and holds the last look unless shapeHold is false. Keyframes: animation.shapeFillOpacity, shapeStrokeOpacity (numbers 0..1), shapeDashOffset (-200..200), shapeFillColor, shapeStrokeColor (#rrggbb), and gradientFromX/Y and gradientToX/Y (-2..3, relative to fill bounds). | shapeFillFrom, shapeFillTo, shapeStrokeFrom, shapeStrokeTo, shapeFillOpacityFrom, shapeFillOpacityTo, shapeStrokeOpacityFrom, shapeStrokeOpacityTo, shapeDashOffsetFrom, shapeDashOffsetTo, shapeHold, curve, animation.* | | `textStyle` | Text fill/stroke colour and opacity, sharing the shapeStyle channels. Opacity affects the text fill and outlines separately, leaving its box background and shadow intact. Keyframes, hold and repeat are supported. | shapeFillFrom, shapeFillTo, shapeStrokeFrom, shapeStrokeTo, shapeFillOpacityFrom, shapeFillOpacityTo, shapeStrokeOpacityFrom, shapeStrokeOpacityTo, shapeHold, curve, animation.* | | `sprite` | Plays a sprite sheet action on a clip that has sprite. The frame comes from time alone; the last frame holds after the range. | spriteAction, spriteFps, spriteMode (loop, pingpong, once) | | `frameZoom` | The clip frame stays; the picture inside zooms/pans. x/y name the picture point (0..1) shown at the frame centre; at zoom z keep them within 0.5/z of 0 and 1 (zoom 1.6: 0.3125–0.6875) or transparent edges show. A range goes from viewFrom* (default: the view the previous frameZoom range left, which is zoom 1 at 0.5,0.5 unless that range has viewHold:true) to viewTo*; without viewHold the view snaps back to zoom 1 when the range ends. A frameZoom range works the same stored on the clip or on any visible lane of its track (a "camera" lane is just a lane name); all of a clip's frameZoom ranges form one sequence in start order, and where two overlap the earlier start wins. | viewFromZoom, viewToZoom, viewFromX/Y, viewToX/Y, viewInstant, viewHold, viewControl, viewKeys[{t,zoom,x,y}] (view per moment, t=0..1 of the effect; used when viewControl is keys) | | `light` | Lights the clip shape from one side during the range (see `light` below). Overlapping light ranges: the upper lane wins. | light, lightAngleMode, lightClockwise, lightTurn, lightSpin, lightAngleCurve, lightLevelMode, lightLevelTo, lightLevelCurve, lightRate, lightAmount, lightColorMode, lightColorTo, lightColorCurve, lightColorRate, lightFade | - `transitionMotion`: `fade`, `pop`, `slide`, `slide-top`, `slide-left`, `slide-right`, `slide-top-left`, `slide-top-right`, `slide-bottom-left`, `slide-bottom-right` (`slide` = from/to below). - `curve`: `linear`, `smooth`, `easeIn`, `easeOut`. `zoomAnchor`: `top-left`, `top`, `top-right`, `left`, `center`, `right`, `bottom-left`, `bottom`, `bottom-right`. - `strength` 0–10 and `speed` 0.1–10 default to 1. ### Text `"text": {"content": "Hello", "fontSize": 64, "color": "#ffffff", "outlineColor": "#000000", "outlineWidth": 4}`. Options: `fontFamily` (`sans-serif`, `serif`, `monospace`, `pretendard`, `noto-sans-kr`, `nanum-myeongjo`, `gowun-dodum`, `black-han-sans`, `do-hyeon`, `jua`, `nanum-pen`), `bold`, `background` (`#rrggbb` or null), `backgroundOpacity`, `align`, `maxWidth` (fraction of canvas width), `role` (`subtitle` places it at the bottom), `reveal` (`words` reveals word by word, `chars` character by character), `count` (`{"from":0,"to":1250}` counts the first number in the text). The optional `label` object adds nameplate padding, rounded background and an image icon: `{"paddingX":24,"paddingY":12,"radius":12,"icon":"icon.png","iconSize":48,"gap":12,"side":"left"}`. Set `background` for the plate; omit `box` for content sizing. Icon media is packaged and relinked like image fills. Font size is in canvas pixels; give `placement.x/y` to move it. Word art keeps editable text. Fill: `colorTo`, `gradientAngle`, `gradientType` (linear/radial/angular), `gradientFrom` and `gradientTo` (both null or both {x,y} fractions from -2 to 3), `gradientStops` (up to 6 interior `{offset:0.01..0.99,color}` stops, strictly increasing), `fillOpacity`. Extra `outlines` (up to 4) are `{color,width:1..30,gap:0..20,opacity:0..1}`, ordered outward beyond the existing outline. Extra `shadows` (up to 4) are `{color,blur:0..60,offsetX:-120..120,offsetY:-120..120,opacity:0..1}`. Text and shapes share surface relief: text uses `bevelSize` (0..20, zero disables), `bevelStrength` (0..1), `bevelAngle` (0..360), `bevelMode` (bevel/emboss), and `innerShadow` (same shadow object, null, or an ordered array of 1..4 shadows). Inner shadows accept optional `spread` (-60..60, default 0) optional `blendMode` (same modes as fills), and `visible` set to false to retain it without rendering. Shapes put these fields in `shape.surface`. Animate text fill and outline colour/opacity with a `textStyle` effect; it uses the existing shapeStyle colour/opacity channel names. Native clip motion effects still apply. Text supports up to eight additional `fills`; shapes use `shape.paint.fills`. Each fill has type (solid/linear/radial/angular/image), color/colorTo (#rrggbb), angle (0..360), opacity (0..1), visible, and blendMode (the clip blend mode names). Gradient fills also accept stops: up to six {offset:0.01..0.99,color} entries in strictly increasing order, between color and colorTo. Optional from and to points {x,y} use fractions of the fill bounds (-2..3); supply both to replace automatic angle-based placement. To animate an additional gradient, give that fill a unique id and set gradientFillId on its shapeStyle or textStyle effect; removing the fill leaves the effect without a target. Image fills require src (a static PNG/JPEG/WebP media reference); mode is cover/fit/stretch/clip/repeat, scaleX/scaleY are 0.01..20, offsetX/offsetY are -32768..32768 canvas pixels, gapX/gapY are 0..4096 pixels, interlace is 0..1 of the repeat tile and interlaceAxis is x or y (default x). align is topLeft/top/topRight/left/center/right/bottomLeft/bottom/bottomRight for cover, fit and stretch (default center). Angle rotates the image freely. These media files are packaged, mapped and retained like ordinary assets, including hidden fills and history. Array order is bottom to top; base alpha and glyph/shape boundaries remain unchanged. Shapes support the same gradient/texture fields in `shape.paint`, plus up to four inward `shape.outlines` rings and solid/dash/dot/custom `shape.strokeStyle`; custom uses `strokeDashArray` (1..32 nonnegative lengths, at least one positive). Open paths accept `strokeCap` (butt/round/square) and `strokeJoin` (miter/round/bevel), defaulting to round for existing projects; `strokeMiterLimit` is 1..1000 (default 4). Open paths accept `startArrow` and `endArrow` as a built-in style name or {type,scale,rotation}; omission means none. Arrow decorations do not yet support mesh deformation. A corner pin whose projection horizon crosses the expanded arrow bounds is rejected. ### Example: "make an ending from A.png that shakes and then disappears" ```json { "name": "Ending", "width": 1280, "height": 720, "fps": 30, "duration": 4, "layers": [ { "id": "logo", "src": "A.png", "placement": { "x": 0, "y": -40, "scale": 0.7 }, "timing": { "start": 0, "end": 4 }, "effects": [ { "start": 0, "end": 0.6, "effect": { "preset": "enter", "transitionMotion": "pop", "curve": "easeOut" } }, { "start": 0.6, "end": 3, "effect": { "preset": "shake", "strength": 1.2, "speed": 1 } }, { "start": 3, "end": 4, "effect": { "preset": "exit", "transitionMotion": "fade", "curve": "easeIn" } } ] }, { "id": "thanks", "text": { "content": "Thanks for watching", "fontFamily": "black-han-sans", "fontSize": 64, "color": "#ffffff", "outlineColor": "#000000", "outlineWidth": 4 }, "placement": { "x": 0, "y": 250 }, "timing": { "start": 0.8, "end": 4 }, "effects": [ { "start": 0.8, "end": 1.4, "effect": { "preset": "enter", "transitionMotion": "slide" } }, { "start": 3, "end": 4, "effect": { "preset": "exit", "transitionMotion": "fade" } } ] } ] } ``` ### Limits - No fixed non-text media clip, track, or library asset count; up to 16 ranges per effect lane. Automatic audio clips, tracks, and assets are added as needed. Use validate for the actual totals. - Unknown presets, out-of-range numbers or bad colours stop the project from opening and the editor shows the reason. My request: