@bycrux/editor 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/engine/__tests__/scheduler.test.ts +35 -0
- package/src/engine/scheduler.ts +10 -2
- package/src/index.ts +33 -0
- package/src/lib/__tests__/font-faces.test.ts +244 -0
- package/src/lib/__tests__/font-loader-parity.test.tsx +236 -0
- package/src/lib/__tests__/google-fonts.test.ts +319 -2
- package/src/lib/font-families.ts +286 -0
- package/src/lib/google-fonts.ts +132 -10
- package/src/text/FontPicker.tsx +82 -11
- package/src/text/__tests__/FontPicker.baseUrl.test.tsx +112 -0
- package/src/types.ts +68 -0
- package/src/video/CaptionRegenModal.tsx +34 -8
- package/src/video/VersionPanel.tsx +1 -1
- package/src/video/VideoEditor.tsx +32 -4
- package/src/video/__tests__/CaptionRegenModal.test.tsx +161 -2
- package/src/video/__tests__/VideoEditor.captionSeam.test.tsx +102 -0
- package/src/video/__tests__/captionProfileDefaults.test.ts +121 -0
- package/src/video/captionProfileDefaults.ts +86 -0
- package/src/video/panels/LeftPanelTabs.tsx +20 -2
- package/src/video/preview/PreviewPlayer.tsx +52 -2
- package/src/video/preview/__tests__/useVideoPlayback.muted.test.ts +239 -0
- package/src/video/preview/useEnginePlayback.ts +58 -8
- package/src/video/preview/useVideoPlayback.ts +48 -16
- package/src/video/timeline/canvas/TimelineCanvas.tsx +10 -8
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.edgeScroll.test.tsx +38 -7
- package/src/video/timeline/canvas/draw.ts +78 -1
- package/src/video/timeline/timeline-model.ts +19 -0
|
@@ -603,6 +603,17 @@ export const LABEL_FONT = '10px ui-sans-serif, system-ui, sans-serif'
|
|
|
603
603
|
export const LABEL_PAD_PX = 6
|
|
604
604
|
/** Below this width a label is more smear than information, so it is skipped. */
|
|
605
605
|
export const MIN_LABEL_WIDTH_PX = 28
|
|
606
|
+
/** Muted-speaker glyph drawn at the head of a muted audio bar. Mute used to be
|
|
607
|
+
* carried by fill alone (`audioMutedFill` plus a dropped border), which reads
|
|
608
|
+
* as "a slightly different colour" rather than "this is off" — operators asked
|
|
609
|
+
* why a lane was silent while looking straight at the thing telling them. */
|
|
610
|
+
export const MUTED_ICON_SIZE_PX = 9
|
|
611
|
+
/** Gap between the glyph and the label that follows it. */
|
|
612
|
+
export const MUTED_ICON_GAP_PX = 4
|
|
613
|
+
/** Below this bar width even the glyph is dropped. Lower than
|
|
614
|
+
* `MIN_LABEL_WIDTH_PX` on purpose: on a bar too narrow to name, "is it muted"
|
|
615
|
+
* is the more valuable of the two answers, so the glyph outlives the label. */
|
|
616
|
+
export const MIN_MUTED_ICON_WIDTH_PX = 14
|
|
606
617
|
/** Middle-baseline offset from a clip's top edge — half the font's 10px plus a
|
|
607
618
|
* 4px margin, so the label rides just inside the clip's top border. */
|
|
608
619
|
export const LABEL_TOP_OFFSET_PX = 9
|
|
@@ -1375,6 +1386,57 @@ export interface AudioItemDrawArgs {
|
|
|
1375
1386
|
hoveredFadeSide?: 'in' | 'out' | null
|
|
1376
1387
|
}
|
|
1377
1388
|
|
|
1389
|
+
/**
|
|
1390
|
+
* A speaker with a cross beside it, drawn in a `size`×`size` box anchored at
|
|
1391
|
+
* (x, y). Filled for the speaker body and stroked for the cross, so it holds
|
|
1392
|
+
* up both against the washed-out muted fill and over whatever waveform the
|
|
1393
|
+
* content layer painted underneath.
|
|
1394
|
+
*
|
|
1395
|
+
* Deliberately geometry rather than a font glyph or an inline SVG: this runs
|
|
1396
|
+
* inside the timeline's canvas painter, where the only other text is drawn with
|
|
1397
|
+
* `LABEL_FONT`, and a missing icon font would fail as a blank box with no
|
|
1398
|
+
* fallback path to notice it.
|
|
1399
|
+
*/
|
|
1400
|
+
function drawMutedGlyph(
|
|
1401
|
+
ctx: DrawContext,
|
|
1402
|
+
x: number,
|
|
1403
|
+
y: number,
|
|
1404
|
+
size: number,
|
|
1405
|
+
color: string,
|
|
1406
|
+
): void {
|
|
1407
|
+
const s = size
|
|
1408
|
+
ctx.save()
|
|
1409
|
+
ctx.strokeStyle = color
|
|
1410
|
+
ctx.fillStyle = color
|
|
1411
|
+
// `DrawContext` is the painter's narrowed surface and carries no
|
|
1412
|
+
// `lineJoin`/`lineCap`; at 9px neither is visible anyway.
|
|
1413
|
+
ctx.lineWidth = 1
|
|
1414
|
+
|
|
1415
|
+
// Speaker: a small back plate, then the cone flaring out of it.
|
|
1416
|
+
const bodyW = s * 0.26
|
|
1417
|
+
const bodyH = s * 0.34
|
|
1418
|
+
const bodyX = x + s * 0.04
|
|
1419
|
+
const bodyY = y + (s - bodyH) / 2
|
|
1420
|
+
ctx.beginPath()
|
|
1421
|
+
ctx.moveTo(bodyX, bodyY)
|
|
1422
|
+
ctx.lineTo(bodyX + bodyW, bodyY)
|
|
1423
|
+
ctx.lineTo(x + s * 0.48, y + s * 0.14)
|
|
1424
|
+
ctx.lineTo(x + s * 0.48, y + s * 0.86)
|
|
1425
|
+
ctx.lineTo(bodyX + bodyW, bodyY + bodyH)
|
|
1426
|
+
ctx.lineTo(bodyX, bodyY + bodyH)
|
|
1427
|
+
ctx.closePath()
|
|
1428
|
+
ctx.fill()
|
|
1429
|
+
|
|
1430
|
+
// The cross — what actually says "off" at a glance.
|
|
1431
|
+
ctx.beginPath()
|
|
1432
|
+
ctx.moveTo(x + s * 0.62, y + s * 0.3)
|
|
1433
|
+
ctx.lineTo(x + s * 0.96, y + s * 0.7)
|
|
1434
|
+
ctx.moveTo(x + s * 0.96, y + s * 0.3)
|
|
1435
|
+
ctx.lineTo(x + s * 0.62, y + s * 0.7)
|
|
1436
|
+
ctx.stroke()
|
|
1437
|
+
ctx.restore()
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1378
1440
|
export function drawAudioItem(
|
|
1379
1441
|
ctx: DrawContext,
|
|
1380
1442
|
args: AudioItemDrawArgs,
|
|
@@ -1412,11 +1474,26 @@ export function drawAudioItem(
|
|
|
1412
1474
|
if (fadeInPx > 0) drawFadeEnvelope(ctx, rect, 'in', fadeInPx, fadeSpanX, fadeSpanWidth, fadeInCurve, palette)
|
|
1413
1475
|
if (fadeOutPx > 0) drawFadeEnvelope(ctx, rect, 'out', fadeOutPx, fadeSpanX, fadeSpanWidth, fadeOutCurve, palette)
|
|
1414
1476
|
|
|
1477
|
+
// Mute glyph leads, and the label starts after it, so the two can never
|
|
1478
|
+
// overlap on a narrow bar.
|
|
1479
|
+
const headX = rect.x + LABEL_PAD_PX + handleWidth
|
|
1480
|
+
let labelX = headX
|
|
1481
|
+
if (muted && rect.width >= MIN_MUTED_ICON_WIDTH_PX) {
|
|
1482
|
+
drawMutedGlyph(
|
|
1483
|
+
ctx,
|
|
1484
|
+
headX,
|
|
1485
|
+
rect.y + (rect.height - MUTED_ICON_SIZE_PX) / 2,
|
|
1486
|
+
MUTED_ICON_SIZE_PX,
|
|
1487
|
+
palette.colors.audioText,
|
|
1488
|
+
)
|
|
1489
|
+
labelX = headX + MUTED_ICON_SIZE_PX + MUTED_ICON_GAP_PX
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1415
1492
|
if (rect.width >= MIN_LABEL_WIDTH_PX) {
|
|
1416
1493
|
ctx.fillStyle = palette.colors.audioText
|
|
1417
1494
|
ctx.font = LABEL_FONT
|
|
1418
1495
|
ctx.textBaseline = 'middle'
|
|
1419
|
-
ctx.fillText(label,
|
|
1496
|
+
ctx.fillText(label, labelX, rect.y + rect.height / 2)
|
|
1420
1497
|
}
|
|
1421
1498
|
ctx.restore()
|
|
1422
1499
|
|
|
@@ -127,6 +127,25 @@ export function resolveAudioWindow(
|
|
|
127
127
|
return { start, end: Math.max(horizon, start + AUDIO_FALLBACK_SPAN_SECONDS) }
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Furthest end across the project's unmuted audio tracks, resolving each
|
|
132
|
+
* track's window through `resolveAudioWindow` — so a track carrying no explicit
|
|
133
|
+
* `end` reports its natural length instead of collapsing to 0.
|
|
134
|
+
*
|
|
135
|
+
* Deliberately called with NO horizon: this feeds the value `contentDuration`
|
|
136
|
+
* is derived from, so passing one back in would be circular.
|
|
137
|
+
*
|
|
138
|
+
* Callers use this only as a LAST-RESORT transport ceiling, for a project with
|
|
139
|
+
* nothing visual in it. Audio stays out of the ceiling whenever there IS visual
|
|
140
|
+
* content — the canvas/video divergence over the audio tail is intentional and
|
|
141
|
+
* documented in timeline-core's `durations.js`.
|
|
142
|
+
*/
|
|
143
|
+
export function audioEnd(project: Project): number {
|
|
144
|
+
return (project.audio?.tracks ?? [])
|
|
145
|
+
.filter(t => !t.muted)
|
|
146
|
+
.reduce((m, t) => Math.max(m, resolveAudioWindow(t).end), 0)
|
|
147
|
+
}
|
|
148
|
+
|
|
130
149
|
/**
|
|
131
150
|
* Group audio tracks into rendered lanes, in ascending lane order. Tracks
|
|
132
151
|
* carrying an explicit `lane` keep it; the rest are auto-assigned lanes above
|