feat(montage): give storyboard beats their own lane above motion
A beat no longer lands on the motion, zoom or B-roll lane: it gets a dedicated "Beats" lane, so a beat can sit over a finished motion (to ask for a change) without colliding with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6c30fa0 parent: dfde5ac modified
web/src/i18n/locales/en/editing.ts +1 -0 | @@ -33,6 +33,7 @@ export default { | ||
| 33 | 33 | markers: "Marker", |
| 34 | 34 | }, |
| 35 | 35 | lanes: { |
| 36 | + beats: "Beats", | |
| 36 | 37 | chapters: "Chapters", |
| 37 | 38 | texts: "Texts", |
| 38 | 39 | motion: "Motion", |
| @@ -33,6 +33,7 @@ export default { | |||
| 33 | markers: "Marker", | 33 | markers: "Marker", |
| 34 | }, | 34 | }, |
| 35 | lanes: { | 35 | lanes: { |
| 36 | + beats: "Beats", | ||
| 36 | chapters: "Chapters", | 37 | chapters: "Chapters", |
| 37 | texts: "Texts", | 38 | texts: "Texts", |
| 38 | motion: "Motion", | 39 | motion: "Motion", |
modified
web/src/i18n/locales/fr/editing.ts +1 -0 | @@ -33,6 +33,7 @@ export default { | ||
| 33 | 33 | markers: "Marqueur", |
| 34 | 34 | }, |
| 35 | 35 | lanes: { |
| 36 | + beats: "Beats", | |
| 36 | 37 | chapters: "Chapitres", |
| 37 | 38 | texts: "Textes", |
| 38 | 39 | motion: "Motion", |
| @@ -33,6 +33,7 @@ export default { | |||
| 33 | markers: "Marqueur", | 33 | markers: "Marqueur", |
| 34 | }, | 34 | }, |
| 35 | lanes: { | 35 | lanes: { |
| 36 | + beats: "Beats", | ||
| 36 | chapters: "Chapitres", | 37 | chapters: "Chapitres", |
| 37 | texts: "Textes", | 38 | texts: "Textes", |
| 38 | motion: "Motion", | 39 | motion: "Motion", |
modified
web/src/project/steps/MontageStep.vue +1 -2 | @@ -153,7 +153,7 @@ async function comment(text?: string) { | ||
| 153 | 153 | chat.value?.compose(text ?? ""); |
| 154 | 154 | } |
| 155 | 155 | |
| 156 | -// --- storyboard : les beats cochés pas encore faits, en pointillés sur leur piste et en slide sur l'aperçu ; | |
| 156 | +// --- storyboard : les beats cochés pas encore faits, en pointillés sur la piste des beats et en slide sur l'aperçu ; | |
| 157 | 157 | // on crée leur animation un par un (pour tester vite) ou tous d'un coup ; zooms et B-roll vidéo se posent tels quels. |
| 158 | 158 | // On en pose aussi à la main (M) : un motion à sa consigne vide, qu'on tire sur son passage et qu'on écrit sur sa slide |
| 159 | 159 | const SB = useStoryboard(project.value.id); |
| @@ -161,7 +161,6 @@ const pending = computed(() => (SB.view.value?.board?.beats ?? []) | ||
| 161 | 161 | .filter((b) => b.on && !SB.view.value?.composed[b.id])); |
| 162 | 162 | const ghosts = computed(() => pending.value.map((b) => ({ |
| 163 | 163 | id: b.id, at: b.at, dur: b.dur, on: b.on, label: b.show || tr(`editing.storyboard.types.${b.type}`), color: TYPE_COLOR[b.type], |
| 164 | - track: b.type === "zoom" ? "zooms" as const : b.type === "broll" && b.src && !isImage(b.src) ? "broll" as const : "motion" as const, | |
| 165 | 164 | }))); |
| 166 | 165 | /** le beat sélectionné sur la timeline (sa fiche remplace l'inspecteur) */ |
| 167 | 166 | const beatSel = ref<string | null>(null); |
| @@ -153,7 +153,7 @@ async function comment(text?: string) { | |||
| 153 | chat.value?.compose(text ?? ""); | 153 | chat.value?.compose(text ?? ""); |
| 154 | } | 154 | } |
| 155 | 155 | ||
| 156 | -// --- storyboard : les beats cochés pas encore faits, en pointillés sur leur piste et en slide sur l'aperçu ; | 156 | +// --- storyboard : les beats cochés pas encore faits, en pointillés sur la piste des beats et en slide sur l'aperçu ; |
| 157 | // on crée leur animation un par un (pour tester vite) ou tous d'un coup ; zooms et B-roll vidéo se posent tels quels. | 157 | // on crée leur animation un par un (pour tester vite) ou tous d'un coup ; zooms et B-roll vidéo se posent tels quels. |
| 158 | // On en pose aussi à la main (M) : un motion à sa consigne vide, qu'on tire sur son passage et qu'on écrit sur sa slide | 158 | // On en pose aussi à la main (M) : un motion à sa consigne vide, qu'on tire sur son passage et qu'on écrit sur sa slide |
| 159 | const SB = useStoryboard(project.value.id); | 159 | const SB = useStoryboard(project.value.id); |
| @@ -161,7 +161,6 @@ const pending = computed(() => (SB.view.value?.board?.beats ?? []) | |||
| 161 | .filter((b) => b.on && !SB.view.value?.composed[b.id])); | 161 | .filter((b) => b.on && !SB.view.value?.composed[b.id])); |
| 162 | const ghosts = computed(() => pending.value.map((b) => ({ | 162 | const ghosts = computed(() => pending.value.map((b) => ({ |
| 163 | id: b.id, at: b.at, dur: b.dur, on: b.on, label: b.show || tr(`editing.storyboard.types.${b.type}`), color: TYPE_COLOR[b.type], | 163 | id: b.id, at: b.at, dur: b.dur, on: b.on, label: b.show || tr(`editing.storyboard.types.${b.type}`), color: TYPE_COLOR[b.type], |
| 164 | - track: b.type === "zoom" ? "zooms" as const : b.type === "broll" && b.src && !isImage(b.src) ? "broll" as const : "motion" as const, | ||
| 165 | }))); | 164 | }))); |
| 166 | /** le beat sélectionné sur la timeline (sa fiche remplace l'inspecteur) */ | 165 | /** le beat sélectionné sur la timeline (sa fiche remplace l'inspecteur) */ |
| 167 | const beatSel = ref<string | null>(null); | 166 | const beatSel = ref<string | null>(null); |
modified
web/src/project/steps/montage/Timeline.vue +6 -4 | @@ -5,7 +5,8 @@ | ||
| 5 | 5 | * rogner (avec aimantation), la lame coupe ; glisser sur une zone vide sélectionne une plage. |
| 6 | 6 | * Seule la partie visible est dessinée : vignettes et forme d'onde suivent le zoom. |
| 7 | 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 | - * Les beats du storyboard pas encore faits (en pointillés sur leur piste) se glissent et se tirent aussi : | |
| 8 | + * Les beats du storyboard pas encore faits, en pointillés sur leur piste à eux (au-dessus du motion : un beat peut | |
| 9 | + * couvrir un plan déjà fait, pour le refaire), se glissent et se tirent aussi : | |
| 9 | 10 | * leurs bords s'aimantent aux mots (le début un peu avant un mot, la fin un peu après). |
| 10 | 11 | */ |
| 11 | 12 | import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue"; |
| @@ -21,8 +22,8 @@ const props = defineProps<{ | ||
| 21 | 22 | tl: Timeline; total: number; t: number; tool: "select" | "blade"; |
| 22 | 23 | sourceDuration: number; analysis: MontageView["analysis"]; peaks: number[] | null; |
| 23 | 24 | media: MontageView["media"]; motions: MontageView["motions"]; playing: boolean; |
| 24 | - /** les beats du storyboard, en pointillés sur leur piste (motion, zooms ou B-roll), à leur couleur */ | |
| 25 | - ghosts?: { id: string; at: number; dur: number; label: string; on: boolean; track: "motion" | "zooms" | "broll"; color: string }[]; | |
| 25 | + /** les beats du storyboard pas encore faits, en pointillés sur leur piste, à la couleur de leur type */ | |
| 26 | + ghosts?: { id: string; at: number; dur: number; label: string; on: boolean; color: string }[]; | |
| 26 | 27 | /** le beat sélectionné */ |
| 27 | 28 | beatSel?: string | null; |
| 28 | 29 | /** les mots de la timeline [début, fin], et l'avance / la traîne d'un beat sur ses mots */ |
| @@ -42,6 +43,7 @@ const LABEL_W = 92; | ||
| 42 | 43 | const ROWS = [ |
| 43 | 44 | { key: "chapters", h: 22 }, |
| 44 | 45 | { key: "texts", h: 24 }, |
| 46 | + { key: "beats", h: 26 }, | |
| 45 | 47 | { key: "motion", h: 30 }, |
| 46 | 48 | { key: "zooms", h: 22 }, |
| 47 | 49 | { key: "broll", h: 34 }, |
| @@ -582,7 +584,7 @@ function motionState(id: string) { | ||
| 582 | 584 | |
| 583 | 585 | <!-- storyboard : les beats proposés, sur leur piste --> |
| 584 | 586 | <div v-for="v in ghostsV" :key="`g-${v.it.id}`" class="ghost" :class="{ off: !v.it.on, on: beatSel === v.it.id }" |
| 585 | - :style="{ top: `${tops[v.it.track] + 2}px`, height: `${ROWS.find((r) => r.key === v.it.track)!.h - 4}px`, left: `${x(v.a)}px`, | |
| 587 | + :style="{ top: `${tops.beats + 2}px`, height: `${rowH.beats - 4}px`, left: `${x(v.a)}px`, | |
| 586 | 588 | width: `${Math.max(3, x(v.b - v.a))}px`, '--c': v.it.color }" |
| 587 | 589 | :title="v.it.label" @pointerdown="downGhost($event, v.it)"> |
| 588 | 590 | <i class="edge l" @pointerdown="downGhost($event, v.it, 'left')" /> |
| @@ -5,7 +5,8 @@ | |||
| 5 | * rogner (avec aimantation), la lame coupe ; glisser sur une zone vide sélectionne une plage. | 5 | * rogner (avec aimantation), 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. | 6 | * 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. | 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 | - * Les beats du storyboard pas encore faits (en pointillés sur leur piste) se glissent et se tirent aussi : | 8 | + * Les beats du storyboard pas encore faits, en pointillés sur leur piste à eux (au-dessus du motion : un beat peut |
| 9 | + * couvrir un plan déjà fait, pour le refaire), se glissent et se tirent aussi : | ||
| 9 | * leurs bords s'aimantent aux mots (le début un peu avant un mot, la fin un peu après). | 10 | * leurs bords s'aimantent aux mots (le début un peu avant un mot, la fin un peu après). |
| 10 | */ | 11 | */ |
| 11 | import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue"; | 12 | import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue"; |
| @@ -21,8 +22,8 @@ const props = defineProps<{ | |||
| 21 | tl: Timeline; total: number; t: number; tool: "select" | "blade"; | 22 | tl: Timeline; total: number; t: number; tool: "select" | "blade"; |
| 22 | sourceDuration: number; analysis: MontageView["analysis"]; peaks: number[] | null; | 23 | sourceDuration: number; analysis: MontageView["analysis"]; peaks: number[] | null; |
| 23 | media: MontageView["media"]; motions: MontageView["motions"]; playing: boolean; | 24 | media: MontageView["media"]; motions: MontageView["motions"]; playing: boolean; |
| 24 | - /** les beats du storyboard, en pointillés sur leur piste (motion, zooms ou B-roll), à leur couleur */ | 25 | + /** les beats du storyboard pas encore faits, en pointillés sur leur piste, à la couleur de leur type */ |
| 25 | - ghosts?: { id: string; at: number; dur: number; label: string; on: boolean; track: "motion" | "zooms" | "broll"; color: string }[]; | 26 | + ghosts?: { id: string; at: number; dur: number; label: string; on: boolean; color: string }[]; |
| 26 | /** le beat sélectionné */ | 27 | /** le beat sélectionné */ |
| 27 | beatSel?: string | null; | 28 | beatSel?: string | null; |
| 28 | /** les mots de la timeline [début, fin], et l'avance / la traîne d'un beat sur ses mots */ | 29 | /** les mots de la timeline [début, fin], et l'avance / la traîne d'un beat sur ses mots */ |
| @@ -42,6 +43,7 @@ const LABEL_W = 92; | |||
| 42 | const ROWS = [ | 43 | const ROWS = [ |
| 43 | { key: "chapters", h: 22 }, | 44 | { key: "chapters", h: 22 }, |
| 44 | { key: "texts", h: 24 }, | 45 | { key: "texts", h: 24 }, |
| 46 | + { key: "beats", h: 26 }, | ||
| 45 | { key: "motion", h: 30 }, | 47 | { key: "motion", h: 30 }, |
| 46 | { key: "zooms", h: 22 }, | 48 | { key: "zooms", h: 22 }, |
| 47 | { key: "broll", h: 34 }, | 49 | { key: "broll", h: 34 }, |
| @@ -582,7 +584,7 @@ function motionState(id: string) { | |||
| 582 | 584 | ||
| 583 | <!-- storyboard : les beats proposés, sur leur piste --> | 585 | <!-- storyboard : les beats proposés, sur leur piste --> |
| 584 | <div v-for="v in ghostsV" :key="`g-${v.it.id}`" class="ghost" :class="{ off: !v.it.on, on: beatSel === v.it.id }" | 586 | <div v-for="v in ghostsV" :key="`g-${v.it.id}`" class="ghost" :class="{ off: !v.it.on, on: beatSel === v.it.id }" |
| 585 | - :style="{ top: `${tops[v.it.track] + 2}px`, height: `${ROWS.find((r) => r.key === v.it.track)!.h - 4}px`, left: `${x(v.a)}px`, | 587 | + :style="{ top: `${tops.beats + 2}px`, height: `${rowH.beats - 4}px`, left: `${x(v.a)}px`, |
| 586 | width: `${Math.max(3, x(v.b - v.a))}px`, '--c': v.it.color }" | 588 | width: `${Math.max(3, x(v.b - v.a))}px`, '--c': v.it.color }" |
| 587 | :title="v.it.label" @pointerdown="downGhost($event, v.it)"> | 589 | :title="v.it.label" @pointerdown="downGhost($event, v.it)"> |
| 588 | <i class="edge l" @pointerdown="downGhost($event, v.it, 'left')" /> | 590 | <i class="edge l" @pointerdown="downGhost($event, v.it, 'left')" /> |