Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions crates/rustmotion-core/src/schema/scenario.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1082,6 +1082,81 @@ pub enum PostEffect {
#[serde(default = "default_flash_duration")]
duration: f32,
},
/// A tape-tear glitch: the frame breaks into horizontal bands slid
/// sideways, with optional noise, scanlines and a travelling tracking
/// line. Bounded by `at` and `duration` like [`PostEffect::Flash`],
/// because it reads as a beat rather than as a state — a reference reel
/// holds it for well under a second on a rewind.
Vhs {
/// When the tear starts, on the scene's own timeline.
at: TimePoint,
/// How long it lasts, in seconds. Default: 0.5.
#[serde(default = "default_vhs_duration")]
duration: f32,
/// Number of horizontal strips the frame breaks into. Default: 12.
#[serde(default = "default_vhs_bands")]
bands: u32,
/// Largest sideways shift a band can take, in pixels. Default: 40.
#[serde(default = "default_vhs_offset")]
offset: f32,
/// Strength of the added noise, 0..1. Default: 0.25.
#[serde(default = "default_vhs_noise")]
noise: f32,
/// Strength of the darkened scanlines, 0..1. Default: 0.2.
#[serde(default = "default_vhs_scanlines")]
scanlines: f32,
/// A thin coloured line travelling down the frame, as tape tracking
/// error looks. Absent means none.
#[serde(default, skip_serializing_if = "Option::is_none")]
tracking_line: Option<VhsTrackingLine>,
/// Seed for the band layout and the noise. The same seed gives the
/// same tear every render.
#[serde(default = "default_vhs_seed")]
seed: u64,
},
}

/// The travelling line of a [`PostEffect::Vhs`] tear.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct VhsTrackingLine {
/// Line colour as a hex string. Default: `"#3DA5FF"`.
#[serde(default = "default_vhs_tracking_color")]
pub color: String,
/// How many times it crosses the frame per second. Default: 1.5.
#[serde(default = "default_vhs_tracking_speed")]
pub speed: f32,
/// Line thickness in pixels. Default: 3.0.
#[serde(default = "default_vhs_tracking_thickness")]
pub thickness: f32,
}

fn default_vhs_duration() -> f32 {
0.5
}
fn default_vhs_bands() -> u32 {
12
}
fn default_vhs_offset() -> f32 {
40.0
}
fn default_vhs_noise() -> f32 {
0.25
}
fn default_vhs_scanlines() -> f32 {
0.2
}
fn default_vhs_seed() -> u64 {
1
}
fn default_vhs_tracking_color() -> String {
"#3DA5FF".to_string()
}
fn default_vhs_tracking_speed() -> f32 {
1.5
}
fn default_vhs_tracking_thickness() -> f32 {
3.0
}

fn default_grain_intensity() -> f32 {
Expand Down
1 change: 1 addition & 0 deletions crates/rustmotion/skills/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ Read individual rule files for detailed explanations, GOOD/BAD examples, and con
- [rules/whip-transition.md](rules/whip-transition.md) - The whip cut: a directional slide that streaks **both** frames along its axis, unlike `zoom_blur` which streaks only the outgoing one
- [rules/draw-progress-stroke.md](rules/draw-progress-stroke.md) - `draw_progress` at 0 paints nothing, and an `svg` draw-on matches the finished mark's stroke width, cap and join
- [rules/svg-text-fonts.md](rules/svg-text-fonts.md) - Why `<text>` inside an `svg` needs a resolvable font face, and what happens when the host has none
- [rules/vhs-tear.md](rules/vhs-tear.md) - The `vhs` scene effect: bands slid sideways, noise, scanlines and a travelling tracking line — bounded by `at`/`duration` because it is a beat, not a filter
- [rules/geometry-safety.md](rules/geometry-safety.md) - Keep all content inside the viewport: `white-space`, `auto_scroll`, `overflow` semantics + violation kinds
- [rules/clip-path.md](rules/clip-path.md) - Non-rectangular masking: the six `clip-path` shapes, how their percentages resolve, and why `node-path` is not one of them yet
- [rules/overlapping-scenes.md](rules/overlapping-scenes.md) - Make an element outlive a cut: overlapping `at` windows composite instead of replacing, who supplies the background, and why `snap` never creates an overlap
Expand Down
51 changes: 51 additions & 0 deletions crates/rustmotion/skills/rules/vhs-tear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# `vhs` : la déchirure de bande

Un effet de scène (`scene.effects`) qui casse l'image en bandes horizontales
décalées latéralement, avec bruit, lignes de balayage et une ligne de suivi qui
descend. C'est le vocabulaire glitch d'un « rewind ».

```json
"effects": [
{ "type": "vhs", "at": 0.4, "duration": 0.8,
"bands": 12, "offset": 60, "noise": 0.3, "scanlines": 0.2,
"tracking_line": { "color": "#3DA5FF", "speed": 1.5, "thickness": 3 },
"seed": 3 }
]
```

| Champ | Défaut | Rôle |
|---|---|---|
| `at` | — | début, sur la timeline **de la scène** (comme `flash`) |
| `duration` | `0.5` | durée en secondes |
| `bands` | `12` | nombre de bandes horizontales |
| `offset` | `40` | décalage latéral maximal d'une bande, en pixels |
| `noise` | `0.25` | intensité du bruit, 0..1 |
| `scanlines` | `0.2` | assombrissement une ligne sur deux, 0..1 |
| `tracking_line` | absent | la ligne colorée qui descend |
| `seed` | `1` | même graine, même déchirure |

## C'est un beat, pas un état

`at` et `duration` le bornent comme `flash`, et pour la même raison : la
référence le tient **moins d'une seconde**. Hors de sa fenêtre, la frame n'est pas
touchée du tout — pas « à peine », pas du tout. Un `vhs` qui couvre toute une
scène ne lit plus comme un accident de lecture, il lit comme un filtre.

## Deux choses à savoir

**Le redécoupage est temporel.** Les bandes sont retirées au sort douze fois par
seconde, pas à chaque frame : une déchirure qui change à 30 ou 60 images par
seconde scintille au lieu de s'accrocher. La graine et l'instant décident
ensemble, donc deux rendus du même fichier donnent les mêmes octets.

**Une bande décalée laisse du vide.** Le bord libéré est rempli en noir, pas
répété ni étiré — c'est ce qui fait lire le décalage comme une déchirure et non
comme un flou. Sur un fond clair ça se voit beaucoup ; baisse `offset` plutôt que
d'espérer que ça se fonde.

## Ce que ça ne remplace pas

`chromatic_wipe` sépare les canaux à une **coupe**, `chromatic_aberration` sur un
**nœud**. `vhs` est un état de la frame entière. Les combiner sur le même beat est
possible et c'est souvent ce que fait la référence — le `vhs` porte la géométrie,
l'aberration porte la couleur.
Loading
Loading