Vertical video from landscape sources
After this recipe you can turn 16:9 footage into a 9:16 video for phones, either by showing the whole picture over a blurred copy of itself or by cropping it to fill the frame and panning across it.
Set the canvas
Section titled “Set the canvas”The output size is the project’s settings. For 1080×1920 at 30 fps:
"settings": { "width": 1080, "height": 1920, "frame_rate": { "num": 30, "den": 1 }, "sample_rate": 48000}Nothing else in the project names pixels: positions are fractions of the canvas, and lengths are fractions of its shorter side. The same document renders at 720×1280 if you change only width and height.
How a source fits the canvas
Section titled “How a source fits the canvas”A clip with the default visual (scale 1) shows its source contained in the canvas: as large as it can be while the whole source stays visible, centred. A 1920×1080 source on a 1080×1920 canvas is drawn 1080 pixels wide and 607.5 tall, with empty space above and below.
To cover the canvas instead, scale it by the source’s aspect ratio divided by the canvas’s (for a source wider than the canvas). For a 16:9 source on a 9:16 canvas that is (16/9) ÷ (9/16) = (16/9)² ≈ 3.1605. The picture is then 1920 pixels tall and about 3413 wide, and the sides are cut off.
| Source | Canvas | Scale to cover |
|---|---|---|
| 16:9 | 9:16 | 3.1605 |
| 16:9 | 1:1 | 1.7778 |
| 16:9 | 4:5 | 2.2222 |
Blurred fill behind the whole picture
Section titled “Blurred fill behind the whole picture”Two tracks show the same source. The lower one covers the canvas, blurred and darkened by a composition. The upper one shows the whole picture, contained.
{ "schema_version": 1, "id": "vertical", "name": "Vertical from landscape", "settings": { "width": 1080, "height": 1920, "frame_rate": { "num": 30, "den": 1 }, "sample_rate": 48000 }, "sources": [ { "id": "coast", "locator": "asset://coast", "kind": "image" } ], "compositions": [ { "id": "backdrop", "output": "darker", "nodes": [ { "id": "clip", "op": { "type": "clip" } }, { "id": "blur", "op": { "type": "effect", "input": "clip", "effect": { "id": "alpha2.effect.gaussian-blur", "version": 1, "params": { "radius": { "mode": "constant", "value": { "type": "float", "value": 0.03 } } } } } }, { "id": "darker", "op": { "type": "effect", "input": "blur", "effect": { "id": "alpha2.effect.color-controls", "version": 1, "params": { "exposure": { "mode": "constant", "value": { "type": "float", "value": -1 } } } } } } ] } ], "timeline": { "tracks": [ { "id": "back", "name": "Blurred fill", "kind": "video", "clips": [ { "id": "fill", "source_id": "coast", "timeline_range": { "start": 0, "end": 480000 }, "source_range": { "start": 0, "end": 480000 }, "visual": { "scale": { "mode": "constant", "value": { "x": 3.1605, "y": 3.1605 } } }, "composition": { "id": "backdrop" } } ], "transitions": [] }, { "id": "front", "name": "Picture", "kind": "video", "clips": [ { "id": "picture", "source_id": "coast", "timeline_range": { "start": 0, "end": 480000 }, "source_range": { "start": 0, "end": 480000 } } ], "transitions": [] } ] }}The backdrop composition runs the clip through alpha2.effect.gaussian-blur and then alpha2.effect.color-controls with exposure −1. The composition builds the clip’s picture, and the clip’s visual.scale then enlarges the result to cover the canvas. Both clips read the same source, so with a video both show the same frame.
Crop to fill and pan
Section titled “Crop to fill and pan”To fill the frame with the picture, keep only the covering clip and move it. position is where the source’s centre (its default anchor) lands on the canvas. At scale 3.1605 the picture is about 3.16 canvas widths wide, so its centre can move between about −0.58 and 1.58 before an edge shows. Keying position.x from 1.2 to −0.2 pans from the left part of the picture to the right part over four seconds.
{ "schema_version": 1, "id": "vertical-pan", "name": "Vertical crop and pan", "settings": { "width": 1080, "height": 1920, "frame_rate": { "num": 30, "den": 1 }, "sample_rate": 48000 }, "sources": [ { "id": "coast", "locator": "asset://coast", "kind": "image" } ], "timeline": { "tracks": [ { "id": "picture", "name": "Picture", "kind": "video", "clips": [ { "id": "picture", "source_id": "coast", "timeline_range": { "start": 0, "end": 480000 }, "source_range": { "start": 0, "end": 480000 }, "visual": { "scale": { "mode": "constant", "value": { "x": 3.1605, "y": 3.1605 } }, "position": { "mode": "keyframes", "keyframes": [ { "time": 0, "value": { "x": 1.2, "y": 0.5 }, "interpolation": { "type": "cubic_bezier", "x1": 0.65, "y1": 0, "x2": 0.35, "y2": 1 } }, { "time": 480000, "value": { "x": -0.2, "y": 0.5 }, "interpolation": { "type": "hold" } } ] } } } ], "transitions": [] } ] }}Export it
Section titled “Export it”alpha2-host export --project vertical.json \ --asset coast=coast.jpg \ --out vertical.mp4import { readFile } from "node:fs/promises";import { exportProject } from "@alpha2-sh/node";
const project = JSON.parse(await readFile("vertical.json", "utf8"));await exportProject({ project, assets: { coast: "coast.jpg" }, startTicks: 0, endTicks: 480_000, // 4 s frameRate: { num: 30, den: 1 }, output: "vertical.mp4",});alpha2.compose has both layouts as nodes: the shorts canvas preset is 9:16, Reframe fits a video over a blurred, cropped copy of itself, and Media(..., fit="cover") crops to fill.
import alpha2from alpha2 import compose as c
video = c.Video(c.Reframe("talk.mp4"), canvas="shorts")alpha2.export(video.compile(), "talk-vertical.mp4")See also
Section titled “See also”- Project model: settings, sources, tracks and clips.
- Picture in picture:
scale,anchorandpositionin detail. - Effects and compositions: effect chains on a clip.
- Document schema: every field of
settings.
