Skip to content

Commit 02f469f

Browse files
committed
Let the strip be dragged to any keystroke
The filmstrip was a row of buttons; now it is one timeline for the whole review. A press lands on a segment and a fraction inside it, and the drag carries across hunks, so any moment of any hunk is one gesture away. Playing and speaking wait for the pointer to lift, and the keystroke sound is marked heard so a forward drag does not fire every click between here and there.
1 parent 62aac53 commit 02f469f

2 files changed

Lines changed: 54 additions & 4 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ space pause , . a keystroke back or forward
150150
arrows previous or next hunk a autoplay through everything
151151
j / k scroll wheel, ctrl-wheel zoom, shift-wheel sideways
152152
drag orbit 0 refit the camera and follow the caret
153+
drag the filmstrip to scrub to any keystroke of any hunk
153154
c more or less context (refetched from git)
154155
l overlay g / G fade the front or back plane
155156
t / T next or previous theme f fullscreen

‎src/viewer.html‎

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
@keyframes slide { from { transform:translateX(-105%); } to { transform:translateX(480%); } }
5959
@keyframes blink { 0%,100% { opacity:1; } 50% { opacity:.45; } }
6060
#transport button:disabled:hover { color:var(--dim); background:var(--key); }
61-
#strip { display:flex; gap:3px; flex:1 1 220px; min-height:26px; }
61+
#strip { display:flex; gap:3px; flex:1 1 220px; min-height:26px; touch-action:none; }
6262
#strip .seg { position:relative; flex:1 1 0; min-width:6px; border-radius:4px;
6363
background:var(--key); cursor:pointer; overflow:hidden; transition:background .15s; }
6464
#strip .seg:hover { background:var(--keyOn); }
@@ -83,7 +83,7 @@
8383
<b>&rarr;</b>/<b>&larr;</b> hunk &middot; <b>x</b>/<b>X</b> speed &middot; <b>m</b> sound &middot; <b>v</b> voice &middot; <b>f</b> full &middot; <b>a</b> autoplay<br>
8484
<b>wheel</b> or <b>j</b>/<b>k</b> scroll &middot; <b>ctrl-wheel</b> or <b>+</b>/<b>&minus;</b> zoom &middot;
8585
<b>shift-wheel</b> sideways &middot; <b>0</b> refit and follow &middot; <b>c</b> context &middot; <b>l</b> overlay &middot; <b>g</b>/<b>G</b> front/back &middot;
86-
<b>s</b> side by side &middot;
86+
<b>s</b> side by side &middot; <b>drag the strip</b> to scrub &middot;
8787
<b>t</b> theme &middot; <b>drag</b> orbit &middot; <b>?</b> hide this<br>
8888
every change is typed out: <span class="del">removals erase</span>,
8989
<span class="add">additions type in</span>
@@ -1152,12 +1152,14 @@
11521152
});
11531153
}
11541154

1155-
const narrate = () =>
1155+
const narrate = () => {
1156+
if (scrub) return; // a drag crosses many hunks; the one it lands on speaks
11561157
say([
11571158
hunk.group && { text: hunk.group, tone: TONE.group },
11581159
{ text: whatChanged(), tone: TONE.what },
11591160
hunk.note && { text: hunk.note, tone: TONE.why },
11601161
]);
1162+
};
11611163

11621164
function syncVoiceBtn() {
11631165
button("voice").textContent = voiceOn ? "voice" : "silent";
@@ -1229,11 +1231,58 @@
12291231
const edge = document.createElement("i");
12301232
edge.className = entry.additions >= entry.deletions ? "add" : "del";
12311233
seg.append(bar, edge);
1232-
seg.onclick = () => goto(i);
12331234
strip.appendChild(seg);
12341235
});
12351236
}
12361237

1238+
/* ---------- scrubbing ----------
1239+
The strip is one timeline for the whole review: the pointer lands on a
1240+
segment and a fraction inside it, so any keystroke of any hunk is one gesture
1241+
away. Playing and speaking wait for the pointer to lift, and the keystroke
1242+
sound is marked as already heard, or a forward drag would fire every click
1243+
between here and there at once. */
1244+
let scrub = null;
1245+
1246+
const boxOf = (seg) => seg.getBoundingClientRect();
1247+
1248+
function pointAt(x) {
1249+
const segs = [...strip.children];
1250+
const found = segs.findIndex((seg) => x < boxOf(seg).right);
1251+
const index = found < 0 ? segs.length - 1 : found;
1252+
const box = boxOf(segs[index]);
1253+
return { index, fraction: Math.min(1, Math.max(0, (x - box.left) / box.width)) };
1254+
}
1255+
1256+
function scrubTo(x) {
1257+
const { index, fraction } = pointAt(x);
1258+
if (index !== at) goto(index);
1259+
clock = fraction * timeline.total;
1260+
heard = clock;
1261+
}
1262+
1263+
strip.addEventListener("pointerdown", (e) => {
1264+
strip.setPointerCapture(e.pointerId);
1265+
scrub = { playing: speed > 0 };
1266+
setSpeed(0);
1267+
hush();
1268+
scrubTo(e.clientX);
1269+
});
1270+
1271+
strip.addEventListener("pointermove", (e) => {
1272+
if (scrub) scrubTo(e.clientX);
1273+
});
1274+
1275+
const endScrub = () => {
1276+
if (!scrub) return;
1277+
const { playing } = scrub;
1278+
scrub = null;
1279+
setSpeed(playing ? 1 : 0);
1280+
narrate();
1281+
};
1282+
1283+
strip.addEventListener("pointerup", endScrub);
1284+
strip.addEventListener("pointercancel", endScrub);
1285+
12371286
function resize() {
12381287
camera.aspect = innerWidth / innerHeight;
12391288
camera.updateProjectionMatrix();

0 commit comments

Comments
 (0)