From f79e5621f83dbbd36e8baf7d8f9fe3ea1705fd1e Mon Sep 17 00:00:00 2001
From: Tomasz Plonka <4361591+PlkMarudny@users.noreply.github.com>
Date: Thu, 10 Sep 2026 15:37:27 +0300
Subject: [PATCH 1/2] feat: use transform() for playhead movement
---
app.js | 4 +++-
index.html | 2 +-
style.css | 16 +++++++++++++---
3 files changed, 17 insertions(+), 5 deletions(-)
diff --git a/app.js b/app.js
index 23237d1..d9236f2 100644
--- a/app.js
+++ b/app.js
@@ -6305,7 +6305,9 @@ function loop(ts) {
drawFrame();
}
if (state.dirtyTimeline) rebuildClips();
- els.playhead.style.left = state.time * state.pps + "px";
+ // device-pixel snap — CSS-pixel round still AA-pulses at 125%/150% DPR
+ const dpr = devicePixelRatio || 1;
+ els.playhead.style.transform = `translateX(${Math.round(state.time * state.pps * dpr) / dpr}px)`;
drawRuler();
updateSafeOverlay();
updateKfGraphs();
diff --git a/index.html b/index.html
index ba3ca98..69a54e2 100644
--- a/index.html
+++ b/index.html
@@ -162,7 +162,7 @@
-
+
diff --git a/style.css b/style.css
index bc33913..920e581 100644
--- a/style.css
+++ b/style.css
@@ -1868,20 +1868,30 @@ body.track-size-s .clip .clip-kf-n {
}
/* ── Playhead ── */
+/* 13px wrapper (cap box) so we don't compositor-scale a 1px layer. Line at x=6. */
.playhead {
position: absolute;
top: 0;
bottom: 0;
- width: 1px;
- background: #ff4d6a;
+ left: -6px;
+ width: 13px;
z-index: 4;
pointer-events: none;
}
+.playhead-line {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 6px;
+ width: 1px;
+ background: #ff4d6a;
+}
+
.playhead-cap {
position: absolute;
top: 0;
- left: -6px;
+ left: 0;
width: 13px;
height: 14px;
background: #ff4d6a;
From 5bf9e9d8e1fa8812b57511e4f7653e2cd6e11b10 Mon Sep 17 00:00:00 2001
From: Tomasz Plonka <4361591+PlkMarudny@users.noreply.github.com>
Date: Sun, 13 Sep 2026 11:18:46 +0300
Subject: [PATCH 2/2] fix: restructure playhead movement mechanics
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The layout shift from left was not the clips reflowing after all - the playhead expanding the scroller’s overflow was to blame. CLS shift 0.00 now.
---
app.js | 9 ++++++---
index.html | 2 +-
style.css | 21 ++++++++-------------
3 files changed, 15 insertions(+), 17 deletions(-)
diff --git a/app.js b/app.js
index d9236f2..d5f55db 100644
--- a/app.js
+++ b/app.js
@@ -6280,6 +6280,7 @@ function ensureFont(name) {
/* ── Main loop ── */
let lastTs = null;
+let playheadPx = -1;
function loop(ts) {
if (lastTs == null) lastTs = ts;
const dt = Math.min(0.1, (ts - lastTs) / 1000);
@@ -6305,9 +6306,11 @@ function loop(ts) {
drawFrame();
}
if (state.dirtyTimeline) rebuildClips();
- // device-pixel snap — CSS-pixel round still AA-pulses at 125%/150% DPR
- const dpr = devicePixelRatio || 1;
- els.playhead.style.transform = `translateX(${Math.round(state.time * state.pps * dpr) / dpr}px)`;
+ const phX = Math.round(state.time * state.pps);
+ if (phX !== playheadPx) {
+ playheadPx = phX;
+ els.playhead.style.left = phX + "px";
+ }
drawRuler();
updateSafeOverlay();
updateKfGraphs();
diff --git a/index.html b/index.html
index 69a54e2..ba3ca98 100644
--- a/index.html
+++ b/index.html
@@ -162,7 +162,7 @@
-
+
diff --git a/style.css b/style.css
index 920e581..cba587f 100644
--- a/style.css
+++ b/style.css
@@ -1542,6 +1542,7 @@ input[type=range] {
.timeline-scroll {
flex: 1;
+ min-height: 0;
overflow-x: auto;
overflow-y: auto;
position: relative;
@@ -1560,6 +1561,9 @@ input[type=range] {
.tracks-content {
position: relative;
+ /* clip so abspos playhead `left` cannot grow the scroller's overflow */
+ overflow: clip;
+ overflow-clip-margin: 8px;
}
.work-dim {
position: absolute;
@@ -1868,30 +1872,21 @@ body.track-size-s .clip .clip-kf-n {
}
/* ── Playhead ── */
-/* 13px wrapper (cap box) so we don't compositor-scale a 1px layer. Line at x=6. */
.playhead {
position: absolute;
top: 0;
bottom: 0;
- left: -6px;
- width: 13px;
- z-index: 4;
- pointer-events: none;
-}
-
-.playhead-line {
- position: absolute;
- top: 0;
- bottom: 0;
- left: 6px;
width: 1px;
background: #ff4d6a;
+ z-index: 4;
+ pointer-events: none;
+ contain: layout;
}
.playhead-cap {
position: absolute;
top: 0;
- left: 0;
+ left: -6px;
width: 13px;
height: 14px;
background: #ff4d6a;