hlassiege/darylpublic Fork
⑂ 629b099
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

fix(web): stack overlapping sound effects on sub-lanes in the timeline

Overlapping sfx clips were drawn on top of each other on a single lane,
making dense sound design unreadable and impossible to edit by hand.
Each clip now goes to the first free sub-lane and the sfx row grows.
hlassiege committed 2026-10-11T15:38:50+02:00 Browse files
629b099 parent: c637961
modified web/src/project/steps/montage/Timeline.vue +19 -4
@@ -57,7 +57,22 @@ const tops = (() => {
5757 let y = RULER;
5858 return Object.fromEntries(ROWS.map((r) => { const o = y; y += r.h + 2; return [r.key, o]; })) as Record<RowKey, number>;
5959 })();
60-const height = RULER + ROWS.reduce((n, r) => n + r.h + 2, 0);
60+// les sons qui se chevauchent passent sur des sous-pistes : la piste des sons (la dernière) grandit d'autant
61+const SFX_SUB = 20;
62+const sfxLanes = computed(() => {
63+ const lane = new Map<string, number>();
64+ const ends: number[] = [];
65+ for (const s of props.tl.audio.filter((s) => s.role === "sfx").sort((a, b) => a.at - b.at)) {
66+ let k = ends.findIndex((e) => e <= s.at);
67+ if (k < 0) k = ends.push(0) - 1;
68+ ends[k] = s.at + s.out - s.in;
69+ lane.set(s.id, k);
70+ }
71+ return { lane, count: Math.max(1, ends.length) };
72+});
73+const sfxH = computed(() => sfxLanes.value.count * SFX_SUB + 4);
74+const rowHeight = (key: RowKey) => (key === "sfx" ? sfxH.value : rowH[key]);
75+const height = computed(() => RULER + ROWS.reduce((n, r) => n + rowHeight(r.key) + 2, 0));
6176
6277 // --- zoom et défilement
6378 const scroller = ref<HTMLElement>();
@@ -533,7 +548,7 @@ function motionState(id: string) {
533548 <div class="tl" :class="{ blade: tool === 'blade' }">
534549 <div class="labels" :style="{ width: `${LABEL_W}px`, height: `${height}px` }">
535550 <div class="corner" :style="{ height: `${RULER}px` }" />
536- <div v-for="r in ROWS" :key="r.key" class="lab" :style="{ top: `${tops[r.key]}px`, height: `${r.h}px` }">{{ tr(`editing.lanes.${r.key}`) }}</div>
551+ <div v-for="r in ROWS" :key="r.key" class="lab" :style="{ top: `${tops[r.key]}px`, height: `${rowHeight(r.key)}px` }">{{ tr(`editing.lanes.${r.key}`) }}</div>
537552 </div>
538553 <div ref="scroller" class="scroller" :style="{ height: `${height + 12}px` }"
539554 @wheel="onWheel" @scroll="scrollX = ($event.target as HTMLElement).scrollLeft">
@@ -549,7 +564,7 @@ function motionState(id: string) {
549564 </div>
550565
551566 <!-- fonds des pistes -->
552- <div v-for="r in ROWS" :key="r.key" class="lane" :class="r.key" :style="{ top: `${tops[r.key]}px`, height: `${r.h}px`, width: `${x(total)}px` }" />
567+ <div v-for="r in ROWS" :key="r.key" class="lane" :class="r.key" :style="{ top: `${tops[r.key]}px`, height: `${rowHeight(r.key)}px`, width: `${x(total)}px` }" />
553568
554569 <!-- chapitres -->
555570 <div v-for="v in chaptersV" :key="v.m.id" class="chapter" :class="{ on: isSel('markers', v.m.id) }"
@@ -647,7 +662,7 @@ function motionState(id: string) {
647662 <i class="edge r" @pointerdown="down($event, 'audio', v.it.id, 'right')" />
648663 </div>
649664 <div v-for="v in sfxV" :key="v.it.id" class="item sfx" :class="{ on: isSel('audio', v.it.id) }"
650- :style="{ top: `${tops.sfx + 2}px`, height: `${rowH.sfx - 4}px`, left: `${x(v.a)}px`, width: `${Math.max(4, x(v.b - v.a))}px` }"
665+ :style="{ top: `${tops.sfx + 2 + (sfxLanes.lane.get(v.it.id) ?? 0) * SFX_SUB}px`, height: `${SFX_SUB - 2}px`, left: `${x(v.a)}px`, width: `${Math.max(4, x(v.b - v.a))}px` }"
651666 :title="`${nameOf(v.it.src)} · ${v.it.gain_db} dB`" @pointerdown="down($event, 'audio', v.it.id)" @dblclick="goTo(v.a)">
652667 <i class="edge l" @pointerdown="down($event, 'audio', v.it.id, 'left')" />
653668 <span>{{ nameOf(v.it.src) }}</span>
@@ -57,7 +57,22 @@ const tops = (() => {
57 let y = RULER;57 let y = RULER;
58 return Object.fromEntries(ROWS.map((r) => { const o = y; y += r.h + 2; return [r.key, o]; })) as Record<RowKey, number>;58 return Object.fromEntries(ROWS.map((r) => { const o = y; y += r.h + 2; return [r.key, o]; })) as Record<RowKey, number>;
59 })();59 })();
60-const height = RULER + ROWS.reduce((n, r) => n + r.h + 2, 0);60+// les sons qui se chevauchent passent sur des sous-pistes : la piste des sons (la dernière) grandit d'autant
61+const SFX_SUB = 20;
62+const sfxLanes = computed(() => {
63+ const lane = new Map<string, number>();
64+ const ends: number[] = [];
65+ for (const s of props.tl.audio.filter((s) => s.role === "sfx").sort((a, b) => a.at - b.at)) {
66+ let k = ends.findIndex((e) => e <= s.at);
67+ if (k < 0) k = ends.push(0) - 1;
68+ ends[k] = s.at + s.out - s.in;
69+ lane.set(s.id, k);
70+ }
71+ return { lane, count: Math.max(1, ends.length) };
72+});
73+const sfxH = computed(() => sfxLanes.value.count * SFX_SUB + 4);
74+const rowHeight = (key: RowKey) => (key === "sfx" ? sfxH.value : rowH[key]);
75+const height = computed(() => RULER + ROWS.reduce((n, r) => n + rowHeight(r.key) + 2, 0));
61 76
62 // --- zoom et défilement77 // --- zoom et défilement
63 const scroller = ref<HTMLElement>();78 const scroller = ref<HTMLElement>();
@@ -533,7 +548,7 @@ function motionState(id: string) {
533 <div class="tl" :class="{ blade: tool === 'blade' }">548 <div class="tl" :class="{ blade: tool === 'blade' }">
534 <div class="labels" :style="{ width: `${LABEL_W}px`, height: `${height}px` }">549 <div class="labels" :style="{ width: `${LABEL_W}px`, height: `${height}px` }">
535 <div class="corner" :style="{ height: `${RULER}px` }" />550 <div class="corner" :style="{ height: `${RULER}px` }" />
536- <div v-for="r in ROWS" :key="r.key" class="lab" :style="{ top: `${tops[r.key]}px`, height: `${r.h}px` }">{{ tr(`editing.lanes.${r.key}`) }}</div>551+ <div v-for="r in ROWS" :key="r.key" class="lab" :style="{ top: `${tops[r.key]}px`, height: `${rowHeight(r.key)}px` }">{{ tr(`editing.lanes.${r.key}`) }}</div>
537 </div>552 </div>
538 <div ref="scroller" class="scroller" :style="{ height: `${height + 12}px` }"553 <div ref="scroller" class="scroller" :style="{ height: `${height + 12}px` }"
539 @wheel="onWheel" @scroll="scrollX = ($event.target as HTMLElement).scrollLeft">554 @wheel="onWheel" @scroll="scrollX = ($event.target as HTMLElement).scrollLeft">
@@ -549,7 +564,7 @@ function motionState(id: string) {
549 </div>564 </div>
550 565
551 <!-- fonds des pistes -->566 <!-- fonds des pistes -->
552- <div v-for="r in ROWS" :key="r.key" class="lane" :class="r.key" :style="{ top: `${tops[r.key]}px`, height: `${r.h}px`, width: `${x(total)}px` }" />567+ <div v-for="r in ROWS" :key="r.key" class="lane" :class="r.key" :style="{ top: `${tops[r.key]}px`, height: `${rowHeight(r.key)}px`, width: `${x(total)}px` }" />
553 568
554 <!-- chapitres -->569 <!-- chapitres -->
555 <div v-for="v in chaptersV" :key="v.m.id" class="chapter" :class="{ on: isSel('markers', v.m.id) }"570 <div v-for="v in chaptersV" :key="v.m.id" class="chapter" :class="{ on: isSel('markers', v.m.id) }"
@@ -647,7 +662,7 @@ function motionState(id: string) {
647 <i class="edge r" @pointerdown="down($event, 'audio', v.it.id, 'right')" />662 <i class="edge r" @pointerdown="down($event, 'audio', v.it.id, 'right')" />
648 </div>663 </div>
649 <div v-for="v in sfxV" :key="v.it.id" class="item sfx" :class="{ on: isSel('audio', v.it.id) }"664 <div v-for="v in sfxV" :key="v.it.id" class="item sfx" :class="{ on: isSel('audio', v.it.id) }"
650- :style="{ top: `${tops.sfx + 2}px`, height: `${rowH.sfx - 4}px`, left: `${x(v.a)}px`, width: `${Math.max(4, x(v.b - v.a))}px` }"665+ :style="{ top: `${tops.sfx + 2 + (sfxLanes.lane.get(v.it.id) ?? 0) * SFX_SUB}px`, height: `${SFX_SUB - 2}px`, left: `${x(v.a)}px`, width: `${Math.max(4, x(v.b - v.a))}px` }"
651 :title="`${nameOf(v.it.src)} · ${v.it.gain_db} dB`" @pointerdown="down($event, 'audio', v.it.id)" @dblclick="goTo(v.a)">666 :title="`${nameOf(v.it.src)} · ${v.it.gain_db} dB`" @pointerdown="down($event, 'audio', v.it.id)" @dblclick="goTo(v.a)">
652 <i class="edge l" @pointerdown="down($event, 'audio', v.it.id, 'left')" />667 <i class="edge l" @pointerdown="down($event, 'audio', v.it.id, 'left')" />
653 <span>{{ nameOf(v.it.src) }}</span>668 <span>{{ nameOf(v.it.src) }}</span>