hlassiege/darylpublic Fork
⑂ 42bca16
Commits
⬇ Clone ▾
git clone https://git.rickub.com/hlassiege/daryl.git
git clone ssh://git@rickub.com/hlassiege/daryl.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

feat(web): keep the playhead centered when navigating the timeline

The wheel alone now scrolls the page, to reach the lower tracks.
Ctrl+wheel moves the playhead with the timeline following it in the
middle of the screen, and Alt+wheel zooms on the playhead, which stays
centered, as in Filmora. A jump off-screen while paused recenters it.
hlassiege committed 2026-10-12T09:22:57+02:00 Browse files
42bca16 parent: fdce48a
modified web/src/i18n/locales/en/editing.ts +2 -2
@@ -15,14 +15,14 @@ export default {
1515 zoomOut: "Zoom out (−)",
1616 fit: "Fit",
1717 fitTitle: "Fit to window (Shift+Z)",
18- zoomIn: "Zoom in (+) · Ctrl+scroll",
18+ zoomIn: "Zoom in (+) · Alt+scroll",
1919 busy: "Daryl is working on a copy: the changes land at the end of the turn",
2020 undo: "Undo",
2121 export: "Export for Resolve",
2222 exportTitle: "Writes the .fcpxml to the project folder",
2323 revealTitle: "Open the edit folder",
2424 },
25- keys: "Ctrl+scroll zoom · Shift+Z fit · Space play · J K L · ←→ frame · ↑↓ cuts · A/B select/blade · Ctrl+B cut · Del · M beat · I/O range · Ctrl+Z undo · drag on empty space = range",
25+ keys: "Ctrl+scroll move playhead · Alt+scroll zoom · Shift+Z fit · Space play · J K L · ←→ frame · ↑↓ cuts · A/B select/blade · Ctrl+B cut · Del · M beat · I/O range · Ctrl+Z undo · drag on empty space = range",
2626 tracks: {
2727 aroll: "Video",
2828 zooms: "Zoom",
@@ -15,14 +15,14 @@ export default {
15 zoomOut: "Zoom out (−)",15 zoomOut: "Zoom out (−)",
16 fit: "Fit",16 fit: "Fit",
17 fitTitle: "Fit to window (Shift+Z)",17 fitTitle: "Fit to window (Shift+Z)",
18- zoomIn: "Zoom in (+) · Ctrl+scroll",18+ zoomIn: "Zoom in (+) · Alt+scroll",
19 busy: "Daryl is working on a copy: the changes land at the end of the turn",19 busy: "Daryl is working on a copy: the changes land at the end of the turn",
20 undo: "Undo",20 undo: "Undo",
21 export: "Export for Resolve",21 export: "Export for Resolve",
22 exportTitle: "Writes the .fcpxml to the project folder",22 exportTitle: "Writes the .fcpxml to the project folder",
23 revealTitle: "Open the edit folder",23 revealTitle: "Open the edit folder",
24 },24 },
25- keys: "Ctrl+scroll zoom · Shift+Z fit · Space play · J K L · ←→ frame · ↑↓ cuts · A/B select/blade · Ctrl+B cut · Del · M beat · I/O range · Ctrl+Z undo · drag on empty space = range",25+ keys: "Ctrl+scroll move playhead · Alt+scroll zoom · Shift+Z fit · Space play · J K L · ←→ frame · ↑↓ cuts · A/B select/blade · Ctrl+B cut · Del · M beat · I/O range · Ctrl+Z undo · drag on empty space = range",
26 tracks: {26 tracks: {
27 aroll: "Video",27 aroll: "Video",
28 zooms: "Zoom",28 zooms: "Zoom",
modified web/src/i18n/locales/fr/editing.ts +2 -2
@@ -15,14 +15,14 @@ export default {
1515 zoomOut: "Dézoomer (−)",
1616 fit: "Ajuster",
1717 fitTitle: "Ajuster à la fenêtre (Maj+Z)",
18- zoomIn: "Zoomer (+) · Ctrl+molette",
18+ zoomIn: "Zoomer (+) · Alt+molette",
1919 busy: "Daryl travaille sur sa copie : ses changements arrivent à la fin de son tour",
2020 undo: "Annuler",
2121 export: "Exporter pour Resolve",
2222 exportTitle: "Écrit le .fcpxml dans le dossier du projet",
2323 revealTitle: "Ouvrir le dossier du montage",
2424 },
25- keys: "Ctrl+molette zoom · Maj+Z ajuster · Espace lecture · J K L · ←→ image · ↑↓ coupes · A/B sélection/lame · Ctrl+B couper · Suppr · M beat · I/O plage · Ctrl+Z annuler · glisser sur une zone vide = plage",
25+ keys: "Ctrl+molette déplacer la tête · Alt+molette zoom · Maj+Z ajuster · Espace lecture · J K L · ←→ image · ↑↓ coupes · A/B sélection/lame · Ctrl+B couper · Suppr · M beat · I/O plage · Ctrl+Z annuler · glisser sur une zone vide = plage",
2626 tracks: {
2727 aroll: "Vidéo",
2828 zooms: "Zoom",
@@ -15,14 +15,14 @@ export default {
15 zoomOut: "Dézoomer (−)",15 zoomOut: "Dézoomer (−)",
16 fit: "Ajuster",16 fit: "Ajuster",
17 fitTitle: "Ajuster à la fenêtre (Maj+Z)",17 fitTitle: "Ajuster à la fenêtre (Maj+Z)",
18- zoomIn: "Zoomer (+) · Ctrl+molette",18+ zoomIn: "Zoomer (+) · Alt+molette",
19 busy: "Daryl travaille sur sa copie : ses changements arrivent à la fin de son tour",19 busy: "Daryl travaille sur sa copie : ses changements arrivent à la fin de son tour",
20 undo: "Annuler",20 undo: "Annuler",
21 export: "Exporter pour Resolve",21 export: "Exporter pour Resolve",
22 exportTitle: "Écrit le .fcpxml dans le dossier du projet",22 exportTitle: "Écrit le .fcpxml dans le dossier du projet",
23 revealTitle: "Ouvrir le dossier du montage",23 revealTitle: "Ouvrir le dossier du montage",
24 },24 },
25- keys: "Ctrl+molette zoom · Maj+Z ajuster · Espace lecture · J K L · ←→ image · ↑↓ coupes · A/B sélection/lame · Ctrl+B couper · Suppr · M beat · I/O plage · Ctrl+Z annuler · glisser sur une zone vide = plage",25+ keys: "Ctrl+molette déplacer la tête · Alt+molette zoom · Maj+Z ajuster · Espace lecture · J K L · ←→ image · ↑↓ coupes · A/B sélection/lame · Ctrl+B couper · Suppr · M beat · I/O plage · Ctrl+Z annuler · glisser sur une zone vide = plage",
26 tracks: {26 tracks: {
27 aroll: "Vidéo",27 aroll: "Vidéo",
28 zooms: "Zoom",28 zooms: "Zoom",
modified web/src/project/steps/MontageStep.vue +1 -1
@@ -7,7 +7,7 @@
77 *
88 * Raccourcis (conventions de Resolve) : Espace lecture, J/K/L, ←/→ image par image (Maj : 1 s),
99 * ↑/↓ coupe précédente / suivante, A sélection, B lame, Ctrl+B couper à la tête, Suppr supprimer,
10- * M beat (posé à la tête de lecture, à tirer sur son passage), I/O plage, Alt+X effacer la plage, Ctrl+molette ou +/− zoom, Maj+Z ajuster, Ctrl+Z annuler.
10+ * M beat (posé à la tête de lecture, à tirer sur son passage), I/O plage, Alt+X effacer la plage, Alt+molette ou +/− zoom, Ctrl+molette déplacer la tête, Maj+Z ajuster, Ctrl+Z annuler.
1111 */
1212 import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
1313 import { useI18n } from "vue-i18n";
@@ -7,7 +7,7 @@
7 *7 *
8 * Raccourcis (conventions de Resolve) : Espace lecture, J/K/L, ←/→ image par image (Maj : 1 s),8 * Raccourcis (conventions de Resolve) : Espace lecture, J/K/L, ←/→ image par image (Maj : 1 s),
9 * ↑/↓ coupe précédente / suivante, A sélection, B lame, Ctrl+B couper à la tête, Suppr supprimer,9 * ↑/↓ coupe précédente / suivante, A sélection, B lame, Ctrl+B couper à la tête, Suppr supprimer,
10- * M beat (posé à la tête de lecture, à tirer sur son passage), I/O plage, Alt+X effacer la plage, Ctrl+molette ou +/− zoom, Maj+Z ajuster, Ctrl+Z annuler.10+ * M beat (posé à la tête de lecture, à tirer sur son passage), I/O plage, Alt+X effacer la plage, Alt+molette ou +/− zoom, Ctrl+molette déplacer la tête, Maj+Z ajuster, Ctrl+Z annuler.
11 */11 */
12 import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue";12 import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
13 import { useI18n } from "vue-i18n";13 import { useI18n } from "vue-i18n";
modified web/src/project/steps/montage/Timeline.vue +19 -12
@@ -1,8 +1,9 @@
11 <script setup lang="ts">
22 /**
3- * La timeline du montage, façon DaVinci Resolve : Ctrl + molette zoome autour de la tête de lecture, la molette
4- * fait défiler, Maj + Z ajuste à la fenêtre. On glisse les éléments, on tire leurs bords pour les
5- * rogner (avec aimantation), la lame coupe ; glisser sur une zone vide sélectionne une plage.
3+ * La timeline du montage, façon DaVinci Resolve : Alt + molette zoome sur la tête de lecture, Ctrl + molette la
4+ * déplace (elle reste au milieu de l'écran, comme dans Filmora ; la molette seule fait défiler la page),
5+ * Maj + Z ajuste à la fenêtre. On glisse les éléments, on tire leurs bords pour les rogner (avec aimantation),
6+ * la lame coupe ; glisser sur une zone vide sélectionne une plage.
67 * Seule la partie visible est dessinée : vignettes et forme d'onde suivent le zoom.
78 * Un média glissé depuis l'inspecteur s'insère dans l'A-roll s'il est lâché sur sa piste, sinon il se pose au-dessus.
89 * Les beats du storyboard pas encore faits, en pointillés sur leur piste à eux (au-dessus du motion : un beat peut
@@ -95,10 +96,10 @@ function setZoom(next: number, anchor: number, anchorPx: number) {
9596 if (scroller.value) scroller.value.scrollLeft = Math.max(0, anchor * pps.value - anchorPx);
9697 });
9798 }
98-// la tête de lecture garde sa place à l'écran ; hors champ, on la ramène au centre
99+// comme dans Filmora : on zoome sur la tête de lecture, qui reste au milieu de l'écran (près d'un bout de la
100+// timeline, elle s'en écarte le temps qu'il faut)
99101 function zoomBy(factor: number, anchor = props.t) {
100- const px = x(anchor) - scrollX.value;
101- setZoom(pps.value * factor, anchor, px >= 0 && px <= viewW.value ? px : viewW.value / 2);
102+ setZoom(pps.value * factor, anchor, viewW.value / 2);
102103 }
103104 function fit() {
104105 setZoom(minPps.value, 0, 0);
@@ -106,12 +107,16 @@ function fit() {
106107 defineExpose({ zoomBy, fit });
107108
108109 function onWheel(e: WheelEvent) {
109- if (e.ctrlKey || e.metaKey) {
110+ // la molette seule fait défiler la page (pour atteindre les pistes du bas)
111+ if (e.altKey) {
110112 e.preventDefault();
111113 zoomBy(Math.pow(1.0015, -e.deltaY));
112- } else if (!e.shiftKey && Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
114+ } else if ((e.ctrlKey || e.metaKey) && Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
115+ // on se déplace avec la tête de lecture, qui reste au milieu de l'écran
113116 e.preventDefault();
114- scroller.value!.scrollLeft += e.deltaY;
117+ const t = Math.max(0, Math.min(props.t + e.deltaY / pps.value, props.total));
118+ emit("seek", t);
119+ scroller.value!.scrollLeft = Math.max(0, x(t) - viewW.value / 2);
115120 }
116121 }
117122
@@ -124,12 +129,14 @@ onMounted(() => {
124129 onUnmounted(() => ro.disconnect());
125130 watch(() => props.total, (n, old) => { if (!old && n) nextTick(fit); else if (pps.value < minPps.value) pps.value = minPps.value; });
126131
127-// la lecture reste visible : on tourne la page quand la tête sort de l'écran
132+// la tête reste visible : en lecture on tourne la page quand elle sort de l'écran ; un saut (flèches, coupe
133+// suivante, ±5 s…) hors de l'écran la ramène au centre
128134 watch(() => props.t, (t) => {
129135 const el = scroller.value;
130- if (!el || !props.playing) return;
136+ if (!el || scrub) return;
131137 const px = x(t);
132- if (px > scrollX.value + viewW.value - 30 || px < scrollX.value) el.scrollLeft = Math.max(0, px - 60);
138+ if (px <= scrollX.value + viewW.value - 30 && px >= scrollX.value) return;
139+ el.scrollLeft = Math.max(0, props.playing ? px - 60 : px - viewW.value / 2);
133140 });
134141
135142 // --- règle
@@ -1,8 +1,9 @@
1 <script setup lang="ts">1 <script setup lang="ts">
2 /**2 /**
3- * La timeline du montage, façon DaVinci Resolve : Ctrl + molette zoome autour de la tête de lecture, la molette3+ * La timeline du montage, façon DaVinci Resolve : Alt + molette zoome sur la tête de lecture, Ctrl + molette la
4- * fait défiler, Maj + Z ajuste à la fenêtre. On glisse les éléments, on tire leurs bords pour les4+ * déplace (elle reste au milieu de l'écran, comme dans Filmora ; la molette seule fait défiler la page),
5- * rogner (avec aimantation), la lame coupe ; glisser sur une zone vide sélectionne une plage.5+ * Maj + Z ajuste à la fenêtre. On glisse les éléments, on tire leurs bords pour les rogner (avec aimantation),
6+ * la lame coupe ; glisser sur une zone vide sélectionne une plage.
6 * Seule la partie visible est dessinée : vignettes et forme d'onde suivent le zoom.7 * Seule la partie visible est dessinée : vignettes et forme d'onde suivent le zoom.
7 * Un média glissé depuis l'inspecteur s'insère dans l'A-roll s'il est lâché sur sa piste, sinon il se pose au-dessus.8 * Un média glissé depuis l'inspecteur s'insère dans l'A-roll s'il est lâché sur sa piste, sinon il se pose au-dessus.
8 * Les beats du storyboard pas encore faits, en pointillés sur leur piste à eux (au-dessus du motion : un beat peut9 * Les beats du storyboard pas encore faits, en pointillés sur leur piste à eux (au-dessus du motion : un beat peut
@@ -95,10 +96,10 @@ function setZoom(next: number, anchor: number, anchorPx: number) {
95 if (scroller.value) scroller.value.scrollLeft = Math.max(0, anchor * pps.value - anchorPx);96 if (scroller.value) scroller.value.scrollLeft = Math.max(0, anchor * pps.value - anchorPx);
96 });97 });
97 }98 }
98-// la tête de lecture garde sa place à l'écran ; hors champ, on la ramène au centre99+// comme dans Filmora : on zoome sur la tête de lecture, qui reste au milieu de l'écran (près d'un bout de la
100+// timeline, elle s'en écarte le temps qu'il faut)
99 function zoomBy(factor: number, anchor = props.t) {101 function zoomBy(factor: number, anchor = props.t) {
100- const px = x(anchor) - scrollX.value;102+ setZoom(pps.value * factor, anchor, viewW.value / 2);
101- setZoom(pps.value * factor, anchor, px >= 0 && px <= viewW.value ? px : viewW.value / 2);
102 }103 }
103 function fit() {104 function fit() {
104 setZoom(minPps.value, 0, 0);105 setZoom(minPps.value, 0, 0);
@@ -106,12 +107,16 @@ function fit() {
106 defineExpose({ zoomBy, fit });107 defineExpose({ zoomBy, fit });
107 108
108 function onWheel(e: WheelEvent) {109 function onWheel(e: WheelEvent) {
109- if (e.ctrlKey || e.metaKey) {110+ // la molette seule fait défiler la page (pour atteindre les pistes du bas)
111+ if (e.altKey) {
110 e.preventDefault();112 e.preventDefault();
111 zoomBy(Math.pow(1.0015, -e.deltaY));113 zoomBy(Math.pow(1.0015, -e.deltaY));
112- } else if (!e.shiftKey && Math.abs(e.deltaY) > Math.abs(e.deltaX)) {114+ } else if ((e.ctrlKey || e.metaKey) && Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
115+ // on se déplace avec la tête de lecture, qui reste au milieu de l'écran
113 e.preventDefault();116 e.preventDefault();
114- scroller.value!.scrollLeft += e.deltaY;117+ const t = Math.max(0, Math.min(props.t + e.deltaY / pps.value, props.total));
118+ emit("seek", t);
119+ scroller.value!.scrollLeft = Math.max(0, x(t) - viewW.value / 2);
115 }120 }
116 }121 }
117 122
@@ -124,12 +129,14 @@ onMounted(() => {
124 onUnmounted(() => ro.disconnect());129 onUnmounted(() => ro.disconnect());
125 watch(() => props.total, (n, old) => { if (!old && n) nextTick(fit); else if (pps.value < minPps.value) pps.value = minPps.value; });130 watch(() => props.total, (n, old) => { if (!old && n) nextTick(fit); else if (pps.value < minPps.value) pps.value = minPps.value; });
126 131
127-// la lecture reste visible : on tourne la page quand la tête sort de l'écran132+// la tête reste visible : en lecture on tourne la page quand elle sort de l'écran ; un saut (flèches, coupe
133+// suivante, ±5 s…) hors de l'écran la ramène au centre
128 watch(() => props.t, (t) => {134 watch(() => props.t, (t) => {
129 const el = scroller.value;135 const el = scroller.value;
130- if (!el || !props.playing) return;136+ if (!el || scrub) return;
131 const px = x(t);137 const px = x(t);
132- if (px > scrollX.value + viewW.value - 30 || px < scrollX.value) el.scrollLeft = Math.max(0, px - 60);138+ if (px <= scrollX.value + viewW.value - 30 && px >= scrollX.value) return;
139+ el.scrollLeft = Math.max(0, props.playing ? px - 60 : px - viewW.value / 2);
133 });140 });
134 141
135 // --- règle142 // --- règle