Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- **Slide** (timeline toolbar) — with a video/audio clip selected and the
playhead over it, slips the source `in`/`out` window so the frame under the
playhead becomes the new in-point; timeline start and duration stay fixed.
Linked AV partners stay in sync. Toasts when there isn’t enough media tail.
- Preview playback speed — a monitor toolbar toggle plus **J**/**K**/**L** shortcuts cycle the preview player through 1×, 1.5×, 2×, and 4× (L faster, J slower, K play/pause and reset to 1×). It rides on top of each clip's own speed and is forced back to 1× during export, so renders always come out at real time.

## [1.6.0] - 2026-07-14
Expand Down
11 changes: 11 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,10 @@ glitch (RGB split + jitter) · pop (overshoot scale — stickers/captions).
`transitionIn: {type:"fade"}`.
- A cut/split is just two clips: first with `duration: t`, second with
`start: +t, in: +t×speed, duration: rest`.
- A **slide** (UI toolbar) keeps `start`/`duration` and sets `in` to the media
time under the playhead (source out moves by the same delta). Requires
`in + sourceWindow ≤ media.duration` afterward; linked AV partners share the
new `in` via the usual timing sync.
- `bgRemove` and `chromaKey` can combine with all filters; heavy pixel work is
automatic (only runs when those props are set).

Expand Down Expand Up @@ -425,6 +429,13 @@ of previous durations.

**Music bed**: audio file on A1, `props.volume: 0.3`, trim with `in`/`duration`.

**Slide (slip) source**: with the playhead over a video/audio clip, raise `in`
so the frame under the playhead becomes the clip in-point; leave `start` and
`duration` unchanged. The UI **Slide** button does this and toasts if there is
no unused source tail — i.e. if the new `in` plus the integrated source window
(`mediaTimeAt(clipEnd) − in`, which covers static speed and speed ramps) would
exceed `media.duration`.

**Cinematic grade**: `props.filterPreset: "teal-orange"` (tweak with
`temperature`/`vignette` on top).

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ same time.

**Editing**

- 3 video tracks + 4 audio tracks, drag/trim/split/snap, undo/redo
- 3 video tracks + 4 audio tracks, drag/trim/split/slide/snap, undo/redo
- **Settings** (cog in the top bar) — optional prefs stored in this browser via
`localStorage`. Enable **Link timeline and Project bin selection** so picking a
timeline clip highlights its media in Project, and clicking a Project item
Expand Down
35 changes: 35 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -1866,6 +1866,40 @@ function trimToPlayhead(side) {
syncLinkedTiming(c);
scheduleSave(); renderInspector();
}
/* Slip the selected clip’s source window: keep timeline start/duration,
set `in` to the media time under the playhead (source out moves by the
same delta). Needs enough media after the current out (“tail”). */
function slideClipAtPlayhead() {
const c = getClip(state.selId);
if (!c) { toast("Select a clip first"); return; }
if (c.kind !== "video" && c.kind !== "audio") {
toast("Slide works on video or audio clips");
return;
}
const t = state.time;
if (t < c.start || t >= clipEnd(c)) { toast("Move playhead over the selected clip"); return; }
const media = getMedia(c.mediaId);
if (!media || !(Number(media.duration) > 0)) {
toast("Media duration unknown — wait for probe");
return;
}
// mediaTimeAt already honors static speed and speed-ramp integrals
const srcIn = mediaTimeAt(c, t);
const srcUsed = mediaTimeAt(c, clipEnd(c)) - c.in;
if (!(srcIn - c.in > 1e-4)) {
toast("Playhead is already at the clip in-point");
return;
}
if (srcIn + srcUsed > media.duration + 1e-4) {
toast("Not enough media tail to slide");
return;
}
pushUndo();
c.in = srcIn;
syncLinkedTiming(c);
scheduleSave(); renderInspector();
seekMediaWhilePaused();
}
/* Split at IN/OUT and discard clip heads before IN and tails after OUT.
Skips disabled tracks when track enable/disable is available. */
function trimToWorkArea() {
Expand Down Expand Up @@ -5441,6 +5475,7 @@ els.fileInput.addEventListener("change", () => { importFiles(els.fileInput.files
$("btnTitle").addEventListener("click", addTitle);
$("btnAdjust").addEventListener("click", addAdjust);
$("btnSplit").addEventListener("click", splitAtPlayhead);
$("btnSlide").addEventListener("click", slideClipAtPlayhead);
$("btnCloseGap").addEventListener("click", closeGapAtPlayhead);
$("btnNextGap").addEventListener("click", goToNextGap);
$("btnTrimIO").addEventListener("click", trimToWorkArea);
Expand Down
2 changes: 2 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@
<button class="btn tiny hidden" id="btnTrimIO" title="Trim clips to IN/OUT (⇧T)"><svg class="ico" viewBox="0 0 16 16" width="13" height="13" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-width="1.85" stroke-linecap="square" d="M1.25 4.5H12M4.5 1.25V12M4.5 12H14.75M12 4.5V14.75"/></svg> Trim</button>
<button class="btn tiny toggle hidden" id="btnWorkAreaPlay" title="Limit playback to IN/OUT (Home/End jump to markers; playhead outside = override)"><svg class="ico" viewBox="0 0 16 16" width="13" height="13" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-width="1.85" stroke-linecap="square" stroke-linejoin="miter" d="M1.5 2.5V13.5M14.5 2.5V13.5M4.25 8H11.75M9.35 5.6L12 8L9.35 10.4"/></svg> Limit</button>
<button class="btn tiny" id="btnSplit" title="Split at playhead (S)">✂ Split</button>
<button class="btn tiny" id="btnSlide" title="Slide source in/out so the frame under the playhead becomes the clip in-point (timeline length unchanged)">Slide</button>
<button class="btn tiny" id="btnCloseGap" title="Close gap at playhead (⇧G)"><svg class="ico" viewBox="0 0 16 16" width="14" height="14" style="vertical-align:-3px" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" stroke-linejoin="miter" d="M0.75 4.5H3.5V11.5H0.75M15.25 4.5H12.5V11.5H15.25"/><path fill="none" stroke="currentColor" stroke-width="1.45" stroke-linecap="round" d="M6.35 5.85L9.65 10.15M9.65 5.85L6.35 10.15"/></svg> Close gap</button>
<button class="btn tiny" id="btnNextGap" title="Find gap (G)"><svg class="ico" viewBox="0 0 16 16" width="14" height="14" style="vertical-align:-3px" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" stroke-linejoin="miter" d="M0.75 4.5H3.5V11.5H0.75M15.25 4.5H12.5V11.5H15.25"/><text x="8" y="11.1" text-anchor="middle" fill="currentColor" font-size="9.5" font-family="Segoe UI, system-ui, sans-serif" font-weight="700">?</text></svg> Find gap</button>
<button class="btn tiny" id="btnDelete" title="Delete selected (Del)"><svg class="ico" viewBox="0 0 16 16" width="13" height="13" aria-hidden="true"><path fill="currentColor" d="M5.5 1.5h5v1.25H14v1.5H2v-1.5h3.5V1.5zM3.75 5.5h8.5v8.25a1.5 1.5 0 0 1-1.5 1.5h-5.5a1.5 1.5 0 0 1-1.5-1.5V5.5zm2 2v5h1.25v-5H5.75zm3.25 0v5H10.25v-5H9z"/></svg> Delete</button>
Expand Down Expand Up @@ -207,6 +208,7 @@ <h2>Keyboard shortcuts</h2>
<tr><td><kbd>Space</kbd></td><td>Play / pause</td></tr>
<tr><td><kbd>J</kbd> <kbd>K</kbd> <kbd>L</kbd></td><td>Shuttle preview — <kbd>L</kbd> faster / <kbd>J</kbd> slower (1× · 1.5× · 2× · 4×); <kbd>K</kbd> play/pause + reset to 1×</td></tr>
<tr><td><kbd>S</kbd></td><td>Split clip(s) at playhead</td></tr>
<tr><td>Slide (toolbar)</td><td>Slip selected clip source so playhead frame becomes the in-point</td></tr>
<tr><td><kbd>G</kbd></td><td>Find next shared gap (wraps; respects IN/OUT)</td></tr>
<tr><td><kbd>⇧</kbd>+<kbd>G</kbd></td><td>Close gap at playhead (enabled tracks)</td></tr>
<tr><td><kbd>Alt</kbd>+<kbd>T</kbd></td><td>Add transition at playhead (in / out by half)</td></tr>
Expand Down
Loading