canvas-recorder-lib 0.5.0 → 0.6.0
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/BREAKING-CHANGES.md +18 -0
- package/README.md +112 -20
- package/dev/audio-builder.ts +4 -2
- package/dev/canvas-recorder.css +59 -0
- package/dev/canvas-recorder.ts +679 -51
- package/dev/delay-files.ts +1 -1
- package/dev/font-utils.ts +8 -1
- package/dev/format-editor.ts +734 -0
- package/dev/format-string.ts +110 -0
- package/dev/local-file-row.ts +107 -0
- package/dev/multi-text-dialog.ts +360 -0
- package/dev/multi-text-panel.ts +11 -0
- package/dev/player-body.ts +86 -6
- package/dev/rect-snap.ts +9 -9
- package/dev/slide-panel.ts +1 -1
- package/dev/split-dialog.ts +13 -2
- package/dev/static-image-panel.ts +35 -5
- package/dev/timeline-display.ts +50 -5
- package/dev/transcripts.ts +5 -3
- package/dev/video-clip-panel.ts +201 -15
- package/dist/dev/audio-builder.d.ts +1 -1
- package/dist/dev/audio-builder.d.ts.map +1 -1
- package/dist/dev/audio-builder.js +3 -2
- package/dist/dev/audio-builder.js.map +1 -1
- package/dist/dev/canvas-recorder.css +59 -0
- package/dist/dev/canvas-recorder.js +612 -50
- package/dist/dev/canvas-recorder.js.map +1 -1
- package/dist/dev/delay-files.js +1 -1
- package/dist/dev/delay-files.js.map +1 -1
- package/dist/dev/font-utils.d.ts.map +1 -1
- package/dist/dev/font-utils.js +9 -1
- package/dist/dev/font-utils.js.map +1 -1
- package/dist/dev/format-editor.d.ts +75 -0
- package/dist/dev/format-editor.d.ts.map +1 -0
- package/dist/dev/format-editor.js +642 -0
- package/dist/dev/format-editor.js.map +1 -0
- package/dist/dev/format-string.d.ts +62 -0
- package/dist/dev/format-string.d.ts.map +1 -0
- package/dist/dev/format-string.js +86 -0
- package/dist/dev/format-string.js.map +1 -0
- package/dist/dev/local-file-row.d.ts +22 -0
- package/dist/dev/local-file-row.d.ts.map +1 -0
- package/dist/dev/local-file-row.js +84 -0
- package/dist/dev/local-file-row.js.map +1 -0
- package/dist/dev/multi-text-dialog.d.ts +30 -0
- package/dist/dev/multi-text-dialog.d.ts.map +1 -0
- package/dist/dev/multi-text-dialog.js +297 -0
- package/dist/dev/multi-text-dialog.js.map +1 -0
- package/dist/dev/multi-text-panel.d.ts +2 -0
- package/dist/dev/multi-text-panel.d.ts.map +1 -1
- package/dist/dev/multi-text-panel.js +4 -0
- package/dist/dev/multi-text-panel.js.map +1 -1
- package/dist/dev/player-body.d.ts +1 -1
- package/dist/dev/player-body.d.ts.map +1 -1
- package/dist/dev/player-body.js +86 -6
- package/dist/dev/player-body.js.map +1 -1
- package/dist/dev/rect-snap.d.ts +1 -1
- package/dist/dev/rect-snap.js +7 -7
- package/dist/dev/rect-snap.js.map +1 -1
- package/dist/dev/slide-panel.d.ts +1 -1
- package/dist/dev/slide-panel.d.ts.map +1 -1
- package/dist/dev/slide-panel.js +1 -1
- package/dist/dev/slide-panel.js.map +1 -1
- package/dist/dev/split-dialog.d.ts.map +1 -1
- package/dist/dev/split-dialog.js +14 -2
- package/dist/dev/split-dialog.js.map +1 -1
- package/dist/dev/static-image-panel.d.ts +2 -0
- package/dist/dev/static-image-panel.d.ts.map +1 -1
- package/dist/dev/static-image-panel.js +29 -5
- package/dist/dev/static-image-panel.js.map +1 -1
- package/dist/dev/timeline-display.d.ts +23 -2
- package/dist/dev/timeline-display.d.ts.map +1 -1
- package/dist/dev/timeline-display.js +47 -5
- package/dist/dev/timeline-display.js.map +1 -1
- package/dist/dev/transcripts.d.ts.map +1 -1
- package/dist/dev/transcripts.js +5 -3
- package/dist/dev/transcripts.js.map +1 -1
- package/dist/dev/video-clip-panel.d.ts +8 -0
- package/dist/dev/video-clip-panel.d.ts.map +1 -1
- package/dist/dev/video-clip-panel.js +175 -9
- package/dist/dev/video-clip-panel.js.map +1 -1
- package/dist/lib/index.d.ts +3 -0
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +3 -0
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/main-control.d.ts +50 -4
- package/dist/lib/main-control.d.ts.map +1 -1
- package/dist/lib/main-control.js +94 -12
- package/dist/lib/main-control.js.map +1 -1
- package/dist/src/binary-search.js +1 -1
- package/dist/src/binary-search.js.map +1 -1
- package/dist/src/corner-rounder.d.ts +17 -0
- package/dist/src/corner-rounder.d.ts.map +1 -0
- package/dist/src/corner-rounder.js +89 -0
- package/dist/src/corner-rounder.js.map +1 -0
- package/dist/src/cross-fade.d.ts +1 -1
- package/dist/src/cross-fade.d.ts.map +1 -1
- package/dist/src/cross-fade.js +3 -3
- package/dist/src/cross-fade.js.map +1 -1
- package/dist/src/fancy-text.d.ts +116 -0
- package/dist/src/fancy-text.d.ts.map +1 -0
- package/dist/src/fancy-text.js +211 -0
- package/dist/src/fancy-text.js.map +1 -0
- package/dist/src/glib/letters-base.d.ts +1 -1
- package/dist/src/glib/letters-base.d.ts.map +1 -1
- package/dist/src/glib/letters-base.js +4 -3
- package/dist/src/glib/letters-base.js.map +1 -1
- package/dist/src/glib/line-font.d.ts +2 -2
- package/dist/src/glib/line-font.d.ts.map +1 -1
- package/dist/src/glib/line-font.js +2 -2
- package/dist/src/glib/line-font.js.map +1 -1
- package/dist/src/glib/paragraph-layout.d.ts +2 -2
- package/dist/src/glib/paragraph-layout.d.ts.map +1 -1
- package/dist/src/glib/paragraph-layout.js +1 -1
- package/dist/src/glib/paragraph-layout.js.map +1 -1
- package/dist/src/glib/path-shape-splitter.d.ts +1 -1
- package/dist/src/glib/path-shape-splitter.d.ts.map +1 -1
- package/dist/src/glib/path-shape-splitter.js +1 -1
- package/dist/src/glib/path-shape-splitter.js.map +1 -1
- package/dist/src/glib/path-shape.js +2 -2
- package/dist/src/glib/path-shape.js.map +1 -1
- package/dist/src/interpolate.d.ts +2 -2
- package/dist/src/interpolate.d.ts.map +1 -1
- package/dist/src/interpolate.js +1 -1
- package/dist/src/interpolate.js.map +1 -1
- package/dist/src/lattice.d.ts +1 -1
- package/dist/src/lattice.d.ts.map +1 -1
- package/dist/src/local-files.d.ts +129 -0
- package/dist/src/local-files.d.ts.map +1 -0
- package/dist/src/local-files.js +327 -0
- package/dist/src/local-files.js.map +1 -0
- package/dist/src/morph-animation.d.ts +1 -1
- package/dist/src/morph-animation.d.ts.map +1 -1
- package/dist/src/morph-animation.js +2 -2
- package/dist/src/morph-animation.js.map +1 -1
- package/dist/src/peano-fourier/fourier-shared.d.ts +1 -1
- package/dist/src/peano-fourier/fourier-shared.d.ts.map +1 -1
- package/dist/src/peano-fourier/fourier-shared.js +17 -4
- package/dist/src/peano-fourier/fourier-shared.js.map +1 -1
- package/dist/src/schedule-helper.d.ts +4 -4
- package/dist/src/schedule-helper.d.ts.map +1 -1
- package/dist/src/schedule-helper.js +2 -2
- package/dist/src/schedule-helper.js.map +1 -1
- package/dist/src/showable.d.ts +36 -4
- package/dist/src/showable.d.ts.map +1 -1
- package/dist/src/showable.js +37 -1
- package/dist/src/showable.js.map +1 -1
- package/dist/src/slide-components/arrow.d.ts +4 -4
- package/dist/src/slide-components/arrow.d.ts.map +1 -1
- package/dist/src/slide-components/arrow.js +2 -2
- package/dist/src/slide-components/arrow.js.map +1 -1
- package/dist/src/slide-components/cross-fade-transition.d.ts +3 -3
- package/dist/src/slide-components/cross-fade-transition.d.ts.map +1 -1
- package/dist/src/slide-components/cross-fade-transition.js +3 -3
- package/dist/src/slide-components/cross-fade-transition.js.map +1 -1
- package/dist/src/slide-components/duration-agnostic.d.ts +3 -3
- package/dist/src/slide-components/duration-agnostic.d.ts.map +1 -1
- package/dist/src/slide-components/duration-agnostic.js +1 -1
- package/dist/src/slide-components/duration-agnostic.js.map +1 -1
- package/dist/src/slide-components/fixed-duration.d.ts +1 -1
- package/dist/src/slide-components/fixed-duration.d.ts.map +1 -1
- package/dist/src/slide-components/fixed-duration.js +1 -1
- package/dist/src/slide-components/fixed-duration.js.map +1 -1
- package/dist/src/slide-components/fourier.d.ts +6 -6
- package/dist/src/slide-components/fourier.d.ts.map +1 -1
- package/dist/src/slide-components/fourier.js +6 -6
- package/dist/src/slide-components/fourier.js.map +1 -1
- package/dist/src/slide-components/frame-counter.d.ts +2 -2
- package/dist/src/slide-components/frame-counter.d.ts.map +1 -1
- package/dist/src/slide-components/frame-counter.js +1 -1
- package/dist/src/slide-components/frame-counter.js.map +1 -1
- package/dist/src/slide-components/function-graph.d.ts +2 -2
- package/dist/src/slide-components/function-graph.d.ts.map +1 -1
- package/dist/src/slide-components/function-graph.js +3 -3
- package/dist/src/slide-components/function-graph.js.map +1 -1
- package/dist/src/slide-components/halftone-shadow.d.ts +4 -4
- package/dist/src/slide-components/halftone-shadow.d.ts.map +1 -1
- package/dist/src/slide-components/halftone-shadow.js +2 -2
- package/dist/src/slide-components/halftone-shadow.js.map +1 -1
- package/dist/src/slide-components/handwriting-effect.d.ts +5 -5
- package/dist/src/slide-components/handwriting-effect.d.ts.map +1 -1
- package/dist/src/slide-components/handwriting-effect.js +5 -5
- package/dist/src/slide-components/handwriting-effect.js.map +1 -1
- package/dist/src/slide-components/hold.d.ts +3 -3
- package/dist/src/slide-components/hold.d.ts.map +1 -1
- package/dist/src/slide-components/hold.js +3 -3
- package/dist/src/slide-components/hold.js.map +1 -1
- package/dist/src/slide-components/image.d.ts +3 -3
- package/dist/src/slide-components/image.d.ts.map +1 -1
- package/dist/src/slide-components/image.js +3 -3
- package/dist/src/slide-components/image.js.map +1 -1
- package/dist/src/slide-components/in-parallel.d.ts +3 -3
- package/dist/src/slide-components/in-parallel.d.ts.map +1 -1
- package/dist/src/slide-components/in-parallel.js +2 -2
- package/dist/src/slide-components/in-parallel.js.map +1 -1
- package/dist/src/slide-components/in-series.d.ts +4 -4
- package/dist/src/slide-components/in-series.d.ts.map +1 -1
- package/dist/src/slide-components/in-series.js +5 -1
- package/dist/src/slide-components/in-series.js.map +1 -1
- package/dist/src/slide-components/live-duration.d.ts +1 -1
- package/dist/src/slide-components/live-duration.d.ts.map +1 -1
- package/dist/src/slide-components/live-duration.js +1 -1
- package/dist/src/slide-components/live-duration.js.map +1 -1
- package/dist/src/slide-components/multi-text.d.ts +76 -15
- package/dist/src/slide-components/multi-text.d.ts.map +1 -1
- package/dist/src/slide-components/multi-text.js +170 -25
- package/dist/src/slide-components/multi-text.js.map +1 -1
- package/dist/src/slide-components/rectangle.d.ts +3 -3
- package/dist/src/slide-components/rectangle.d.ts.map +1 -1
- package/dist/src/slide-components/rectangle.js +1 -1
- package/dist/src/slide-components/rectangle.js.map +1 -1
- package/dist/src/slide-components/registry.d.ts +1 -1
- package/dist/src/slide-components/registry.d.ts.map +1 -1
- package/dist/src/slide-components/registry.js +18 -18
- package/dist/src/slide-components/registry.js.map +1 -1
- package/dist/src/slide-components/serialize.d.ts +1 -1
- package/dist/src/slide-components/serialize.d.ts.map +1 -1
- package/dist/src/slide-components/serialize.js +2 -2
- package/dist/src/slide-components/serialize.js.map +1 -1
- package/dist/src/slide-components/show-error.js +2 -2
- package/dist/src/slide-components/show-error.js.map +1 -1
- package/dist/src/slide-components/simple-text.d.ts +4 -4
- package/dist/src/slide-components/simple-text.d.ts.map +1 -1
- package/dist/src/slide-components/simple-text.js +4 -4
- package/dist/src/slide-components/simple-text.js.map +1 -1
- package/dist/src/slide-components/slide-component.d.ts +5 -5
- package/dist/src/slide-components/slide-component.d.ts.map +1 -1
- package/dist/src/slide-components/slide-component.js +4 -4
- package/dist/src/slide-components/slide-component.js.map +1 -1
- package/dist/src/slide-components/slide-left.d.ts +3 -3
- package/dist/src/slide-components/slide-left.d.ts.map +1 -1
- package/dist/src/slide-components/slide-left.js +3 -3
- package/dist/src/slide-components/slide-left.js.map +1 -1
- package/dist/src/slide-components/split-video-clip.d.ts +2 -2
- package/dist/src/slide-components/split-video-clip.d.ts.map +1 -1
- package/dist/src/slide-components/split-video-clip.js +3 -3
- package/dist/src/slide-components/split-video-clip.js.map +1 -1
- package/dist/src/slide-components/text-effect.d.ts +2 -2
- package/dist/src/slide-components/text-effect.d.ts.map +1 -1
- package/dist/src/slide-components/traditional-text.d.ts +4 -4
- package/dist/src/slide-components/traditional-text.d.ts.map +1 -1
- package/dist/src/slide-components/traditional-text.js +3 -3
- package/dist/src/slide-components/traditional-text.js.map +1 -1
- package/dist/src/slide-components/video-clip.d.ts +17 -5
- package/dist/src/slide-components/video-clip.d.ts.map +1 -1
- package/dist/src/slide-components/video-clip.js +52 -11
- package/dist/src/slide-components/video-clip.js.map +1 -1
- package/dist/src/slide-components/video-info.d.ts +11 -11
- package/dist/src/slide-components/video-info.d.ts.map +1 -1
- package/dist/src/slide-components/video-info.js +32 -21
- package/dist/src/slide-components/video-info.js.map +1 -1
- package/dist/src/slow-image-sources.d.ts +7 -1
- package/dist/src/slow-image-sources.d.ts.map +1 -1
- package/dist/src/slow-image-sources.js +36 -5
- package/dist/src/slow-image-sources.js.map +1 -1
- package/dist/src/snapshot.d.ts +2 -2
- package/dist/src/snapshot.d.ts.map +1 -1
- package/dist/src/snapshot.js +4 -4
- package/dist/src/snapshot.js.map +1 -1
- package/dist/src/stroke-colors.d.ts +1 -1
- package/dist/src/stroke-colors.d.ts.map +1 -1
- package/dist/src/stroke-colors.js +4 -4
- package/dist/src/stroke-colors.js.map +1 -1
- package/dist/src/utility.d.ts +3 -4
- package/dist/src/utility.d.ts.map +1 -1
- package/dist/src/utility.js +4 -4
- package/dist/src/utility.js.map +1 -1
- package/lib/index.ts +3 -0
- package/lib/main-control.ts +119 -12
- package/package.json +7 -5
- package/src/binary-search.ts +2 -2
- package/src/corner-rounder.ts +128 -0
- package/src/cross-fade.ts +3 -3
- package/src/fancy-text.ts +255 -0
- package/src/glib/letters-base.ts +4 -3
- package/src/glib/line-font.ts +2 -2
- package/src/glib/paragraph-layout.ts +2 -2
- package/src/glib/path-shape-splitter.ts +1 -1
- package/src/glib/path-shape.ts +2 -2
- package/src/interpolate.ts +3 -3
- package/src/lattice.ts +1 -1
- package/src/local-files.ts +389 -0
- package/src/morph-animation.ts +2 -2
- package/src/peano-fourier/fourier-shared.ts +17 -4
- package/src/schedule-helper.ts +4 -4
- package/src/showable.ts +54 -4
- package/src/slide-components/arrow.ts +4 -4
- package/src/slide-components/cross-fade-transition.ts +4 -4
- package/src/slide-components/duration-agnostic.ts +3 -3
- package/src/slide-components/fixed-duration.ts +1 -1
- package/src/slide-components/fourier.ts +9 -9
- package/src/slide-components/frame-counter.ts +2 -2
- package/src/slide-components/function-graph.ts +4 -4
- package/src/slide-components/halftone-shadow.ts +4 -4
- package/src/slide-components/handwriting-effect.ts +7 -7
- package/src/slide-components/hold.ts +4 -4
- package/src/slide-components/image.ts +5 -5
- package/src/slide-components/in-parallel.ts +4 -4
- package/src/slide-components/in-series.ts +7 -4
- package/src/slide-components/live-duration.ts +1 -1
- package/src/slide-components/multi-text.ts +195 -30
- package/src/slide-components/rectangle.ts +3 -3
- package/src/slide-components/registry.ts +19 -19
- package/src/slide-components/serialize.ts +2 -2
- package/src/slide-components/show-error.ts +2 -2
- package/src/slide-components/simple-text.ts +7 -7
- package/src/slide-components/slide-component.ts +7 -7
- package/src/slide-components/slide-left.ts +4 -4
- package/src/slide-components/split-video-clip.ts +4 -4
- package/src/slide-components/text-effect.ts +2 -2
- package/src/slide-components/traditional-text.ts +5 -5
- package/src/slide-components/video-clip.ts +64 -13
- package/src/slide-components/video-info.ts +33 -21
- package/src/slow-image-sources.ts +38 -6
- package/src/snapshot.ts +4 -4
- package/src/stroke-colors.ts +4 -4
- package/src/utility.ts +5 -5
package/dev/video-clip-panel.ts
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
import { ReadOnlyRect } from "phil-lib/misc";
|
|
2
2
|
import { Keyframe } from "../src/interpolate.ts";
|
|
3
3
|
import { SoundClip } from "../src/showable.ts";
|
|
4
|
+
import { canvasRecorder, type VideoSize } from "../lib/main-control.ts";
|
|
4
5
|
import { VideoClipComponent } from "../src/slide-components/video-clip.ts";
|
|
5
6
|
import {
|
|
6
7
|
centeredOnCanvas,
|
|
7
8
|
maxFit,
|
|
8
9
|
minCover,
|
|
9
|
-
|
|
10
|
+
pixelsPerUnit,
|
|
10
11
|
preservePixels,
|
|
11
12
|
sameCenter,
|
|
12
13
|
shrinkToAspect,
|
|
13
14
|
VideoInfo,
|
|
14
15
|
} from "../src/slide-components/video-info.ts";
|
|
15
16
|
import { ALIGN_HORIZONTALLY, ALIGN_VERTICALLY, buildSnapRow } from "./rect-snap.ts";
|
|
17
|
+
import { buildLocalFileRow } from "./local-file-row.ts";
|
|
18
|
+
import {
|
|
19
|
+
LocalFileError,
|
|
20
|
+
onLocalFilesChange,
|
|
21
|
+
VIDEO_FILE_TYPES,
|
|
22
|
+
} from "../src/local-files.ts";
|
|
16
23
|
|
|
17
24
|
/**
|
|
18
25
|
* The `notes` on a sound clip that this panel created. This is how it
|
|
@@ -23,6 +30,13 @@ const IMPORTED_AUDIO_NOTE = "imported from video";
|
|
|
23
30
|
|
|
24
31
|
/** What the panel needs from the Visual Editor. It never reaches into canvas-recorder.ts directly. */
|
|
25
32
|
export type VideoClipPanelHooks = {
|
|
33
|
+
/**
|
|
34
|
+
* Change the whole video's size, and remember it, as if the user had typed it in the size fields.
|
|
35
|
+
* @returns False if the size is locked, or not usable.
|
|
36
|
+
*/
|
|
37
|
+
readonly setVideoSize: (size: VideoSize) => boolean;
|
|
38
|
+
/** The URL was changed by a button here. Save, and rebuild the schedule editor so the URL field shows it. */
|
|
39
|
+
readonly urlChanged: () => void;
|
|
26
40
|
/**
|
|
27
41
|
* Start / End were changed by a button. Save, and rebuild the schedule
|
|
28
42
|
* editor so the number fields show the new values.
|
|
@@ -68,6 +82,8 @@ function formatSpeed(speed: number): string {
|
|
|
68
82
|
* every keystroke names a file that doesn't exist, and that is not a problem.
|
|
69
83
|
*/
|
|
70
84
|
function quietError(error: unknown): string {
|
|
85
|
+
// These are written for the user already, and say what to do.
|
|
86
|
+
if (error instanceof LocalFileError) return error.message;
|
|
71
87
|
const text = String(error);
|
|
72
88
|
if (/404|not found|Failed to fetch/i.test(text)) return "Not found";
|
|
73
89
|
if (/unsupported|unrecognized|format/i.test(text)) return "Not a video file this can read";
|
|
@@ -89,13 +105,21 @@ function playbackSpeed(clip: VideoClipComponent): number {
|
|
|
89
105
|
|
|
90
106
|
const isRealTime = (speed: number) => Math.abs(speed - 1) < 0.0005;
|
|
91
107
|
|
|
108
|
+
/**
|
|
109
|
+
* The largest even number of pixels that fits in `pixels`.
|
|
110
|
+
* HEVC stores color at half resolution (4:2:0), so the saved file's sides must be even.
|
|
111
|
+
*/
|
|
112
|
+
function evenSize(pixels: number): number {
|
|
113
|
+
return Math.max(2, Math.floor(pixels / 2) * 2);
|
|
114
|
+
}
|
|
115
|
+
|
|
92
116
|
/**
|
|
93
117
|
* Output pixels per source pixel, for one Dest Rect keyframe. 1 is native
|
|
94
118
|
* resolution. Above 1 the video is being upscaled and will look soft.
|
|
95
119
|
*/
|
|
96
120
|
function nativeRatio(rect: ReadOnlyRect, info: VideoInfo): number {
|
|
97
121
|
const drawn = shrinkToAspect(rect, info.video.width / info.video.height);
|
|
98
|
-
return (drawn.width *
|
|
122
|
+
return (drawn.width * pixelsPerUnit()) / info.video.width;
|
|
99
123
|
}
|
|
100
124
|
|
|
101
125
|
/** The sound clip that would line this video's audio up with its picture, right now. */
|
|
@@ -122,6 +146,18 @@ function expectedSoundClip(
|
|
|
122
146
|
};
|
|
123
147
|
}
|
|
124
148
|
|
|
149
|
+
/**
|
|
150
|
+
* How far `sound` has been slid away from the picture, compared with a fresh import.
|
|
151
|
+
* Trimming either end doesn't change this; only moving the sound does.
|
|
152
|
+
* @returns Milliseconds. Negative means the sound plays later than the matching picture.
|
|
153
|
+
*/
|
|
154
|
+
function syncOffsetMs(sound: SoundClip, expected: SoundClip): number {
|
|
155
|
+
// Which moment of the file plays at the clip's own time 0, for each.
|
|
156
|
+
const fileAtZero = (clip: SoundClip) =>
|
|
157
|
+
(clip.startMsIntoClip ?? 0) - clip.startMsIntoScene;
|
|
158
|
+
return fileAtZero(sound) - fileAtZero(expected);
|
|
159
|
+
}
|
|
160
|
+
|
|
125
161
|
function sameSoundClip(a: SoundClip, b: SoundClip): boolean {
|
|
126
162
|
const close = (x: number | undefined, y: number | undefined) =>
|
|
127
163
|
x === y || (x !== undefined && y !== undefined && Math.abs(x - y) < 0.5);
|
|
@@ -152,6 +188,45 @@ export function buildVideoClipPanel(
|
|
|
152
188
|
legend.textContent = "Video File";
|
|
153
189
|
panel.append(legend);
|
|
154
190
|
|
|
191
|
+
panel.append(
|
|
192
|
+
buildLocalFileRow({
|
|
193
|
+
types: VIDEO_FILE_TYPES,
|
|
194
|
+
pickerId: "video-clips",
|
|
195
|
+
startIn: "videos",
|
|
196
|
+
currentUrl: () => clip.urlScalar.value,
|
|
197
|
+
choose(url) {
|
|
198
|
+
// Start and End still at a brand new clip's 0 and 1000 means nobody has chosen what to show.
|
|
199
|
+
// Then show the whole file at 1×, and import its audio, so it plays like the file itself.
|
|
200
|
+
const untouched =
|
|
201
|
+
clip.startMsIntoClipScalar.value === 0 &&
|
|
202
|
+
clip.endMsIntoClipScalar.value === 1000;
|
|
203
|
+
clip.urlScalar.value = url;
|
|
204
|
+
hooks.urlChanged();
|
|
205
|
+
if (!untouched) return;
|
|
206
|
+
clip.videoInfo()?.then(
|
|
207
|
+
(info) => {
|
|
208
|
+
// The user may have picked another file while this one opened.
|
|
209
|
+
if (clip.urlScalar.value !== url) return;
|
|
210
|
+
clip.endMsIntoClipScalar.value = info.durationMs;
|
|
211
|
+
// Bubbles up through scheduleHasChanged(), like the Whole file button.
|
|
212
|
+
clip.setDuration(info.durationMs);
|
|
213
|
+
if (info.audio) {
|
|
214
|
+
// The same as Import audio, now that the clip plays at 1×.
|
|
215
|
+
const kept = clip.soundClips.filter(
|
|
216
|
+
(sc) => sc.notes !== IMPORTED_AUDIO_NOTE,
|
|
217
|
+
);
|
|
218
|
+
clip.soundClips = [...kept, expectedSoundClip(clip, info.audio)];
|
|
219
|
+
clip.scheduleHasChanged();
|
|
220
|
+
}
|
|
221
|
+
hooks.scalarsChanged();
|
|
222
|
+
},
|
|
223
|
+
// The panel shows why the file didn't open.
|
|
224
|
+
() => {},
|
|
225
|
+
);
|
|
226
|
+
},
|
|
227
|
+
}),
|
|
228
|
+
);
|
|
229
|
+
|
|
155
230
|
const line = (parent: HTMLElement, cssText = "") => {
|
|
156
231
|
const div = document.createElement("div");
|
|
157
232
|
div.style.cssText = cssText;
|
|
@@ -173,11 +248,9 @@ export function buildVideoClipPanel(
|
|
|
173
248
|
const derivedEl = line(panel, "font-size:0.9em;line-height:1.4;margin-top:0.3em");
|
|
174
249
|
|
|
175
250
|
const timingRow = line(panel, "margin-top:0.5em");
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
"Start = 0, End = the end of the file, and Duration to match, so it plays in real time.",
|
|
180
|
-
);
|
|
251
|
+
const WHOLE_FILE_TITLE =
|
|
252
|
+
"Start = 0, End = the end of the file, and Duration to match, so it plays in real time.";
|
|
253
|
+
const wholeFileBtn = button(timingRow, "Whole file", WHOLE_FILE_TITLE);
|
|
181
254
|
const realTimeBtn = button(
|
|
182
255
|
timingRow,
|
|
183
256
|
"Play at 1×",
|
|
@@ -190,10 +263,14 @@ export function buildVideoClipPanel(
|
|
|
190
263
|
splitBtn.addEventListener("click", () => hooks.split());
|
|
191
264
|
|
|
192
265
|
const rectRow = line(panel, "margin-top:0.4em");
|
|
266
|
+
const SIZE_VIDEO_TITLE =
|
|
267
|
+
"Make the whole video exactly this clip's size, in pixels, and fill it with this clip, " +
|
|
268
|
+
"one video pixel per output pixel. The units stay as wide as they are now.";
|
|
269
|
+
const sizeVideoBtn = button(rectRow, "Make video this size", SIZE_VIDEO_TITLE);
|
|
193
270
|
const rectButtons = [
|
|
194
271
|
{
|
|
195
272
|
text: "Fit canvas",
|
|
196
|
-
title: "As large as possible while the whole video stays on the
|
|
273
|
+
title: "As large as possible while the whole video stays on the canvas. Centered.",
|
|
197
274
|
make: (info: VideoInfo) =>
|
|
198
275
|
centeredOnCanvas(maxFit(info.video.width, info.video.height)),
|
|
199
276
|
},
|
|
@@ -205,7 +282,7 @@ export function buildVideoClipPanel(
|
|
|
205
282
|
},
|
|
206
283
|
{
|
|
207
284
|
text: "Native pixels",
|
|
208
|
-
title: `One video pixel per output pixel at ${
|
|
285
|
+
title: `One video pixel per output pixel at ${pixelsPerUnit()} px/unit — pixel perfect. Keeps the rectangle's center.`,
|
|
209
286
|
make: (info: VideoInfo, rect: ReadOnlyRect) =>
|
|
210
287
|
sameCenter(preservePixels(info.video.width, info.video.height), rect),
|
|
211
288
|
},
|
|
@@ -260,7 +337,7 @@ export function buildVideoClipPanel(
|
|
|
260
337
|
function draw(): void {
|
|
261
338
|
switch (state) {
|
|
262
339
|
case "empty":
|
|
263
|
-
statusEl.textContent = "No URL
|
|
340
|
+
statusEl.textContent = "No file yet: open one, or type a URL below.";
|
|
264
341
|
statusEl.style.color = "#888";
|
|
265
342
|
break;
|
|
266
343
|
case "loading":
|
|
@@ -361,6 +438,24 @@ export function buildVideoClipPanel(
|
|
|
361
438
|
realTimeBtn.hidden = Number.isNaN(speed) || isRealTime(speed);
|
|
362
439
|
realTimeBtn.disabled = !(end > start);
|
|
363
440
|
for (const { btn } of rectButtons) btn.disabled = !ready;
|
|
441
|
+
const alreadyWhole =
|
|
442
|
+
ready &&
|
|
443
|
+
start === 0 &&
|
|
444
|
+
Math.abs(end - info!.durationMs) < 0.5 &&
|
|
445
|
+
Math.abs(clip.duration - info!.durationMs) < 0.5;
|
|
446
|
+
wholeFileBtn.disabled = !ready || alreadyWhole;
|
|
447
|
+
wholeFileBtn.title = alreadyWhole
|
|
448
|
+
? `${WHOLE_FILE_TITLE}\n\nIt already is.`
|
|
449
|
+
: WHOLE_FILE_TITLE;
|
|
450
|
+
const alreadySized = ready && videoMatchesClip(info!);
|
|
451
|
+
sizeVideoBtn.disabled = !ready || canvasRecorder.sizeLocked || alreadySized;
|
|
452
|
+
sizeVideoBtn.title = canvasRecorder.sizeLocked
|
|
453
|
+
? `${SIZE_VIDEO_TITLE}\n\nThis program sets the video's size, so it can't be changed here.`
|
|
454
|
+
: alreadySized
|
|
455
|
+
? `${SIZE_VIDEO_TITLE}\n\nIt already is.`
|
|
456
|
+
: ready
|
|
457
|
+
? `${SIZE_VIDEO_TITLE}\n\n${evenSize(info!.video.width)} × ${evenSize(info!.video.height)} pixels.`
|
|
458
|
+
: SIZE_VIDEO_TITLE;
|
|
364
459
|
alignRow.draw();
|
|
365
460
|
drawAudio(speed);
|
|
366
461
|
}
|
|
@@ -401,15 +496,27 @@ export function buildVideoClipPanel(
|
|
|
401
496
|
importBtn.title =
|
|
402
497
|
"Add this file's audio as a sound clip on this Video Clip, lined up with the picture. " +
|
|
403
498
|
"It lives on the clip, so it moves with the clip on the timeline.";
|
|
499
|
+
const expected = expectedSoundClip(clip, audio);
|
|
404
500
|
if (imported.length === 0) {
|
|
405
501
|
setStatus("");
|
|
406
|
-
} else if (
|
|
407
|
-
imported
|
|
408
|
-
|
|
409
|
-
) {
|
|
502
|
+
} else if (imported.length > 1) {
|
|
503
|
+
setStatus("Audio imported, then cut into pieces");
|
|
504
|
+
importBtn.title += "\n\nRe-import replaces the pieces with one sound clip, lined up with the picture.";
|
|
505
|
+
} else if (sameSoundClip(imported[0], expected)) {
|
|
410
506
|
setStatus("Audio imported ✓", "#070");
|
|
411
507
|
} else {
|
|
412
|
-
|
|
508
|
+
const offsetMs = syncOffsetMs(imported[0], expected);
|
|
509
|
+
if (Math.abs(offsetMs) < 0.5) {
|
|
510
|
+
setStatus("Audio trimmed, still in sync ✓", "#070");
|
|
511
|
+
} else {
|
|
512
|
+
const seconds = (Math.abs(offsetMs) / 1000).toFixed(2);
|
|
513
|
+
setStatus(
|
|
514
|
+
`Audio moved ${seconds} s ${offsetMs < 0 ? "later" : "earlier"} than the picture`,
|
|
515
|
+
"#555",
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
importBtn.title +=
|
|
519
|
+
"\n\nRe-import undoes your changes to this sound clip: its start, its end, and any move.";
|
|
413
520
|
}
|
|
414
521
|
}
|
|
415
522
|
|
|
@@ -469,6 +576,67 @@ export function buildVideoClipPanel(
|
|
|
469
576
|
return "Dest Rect has several keyframes. Click ✎ on the one you want to change.";
|
|
470
577
|
}
|
|
471
578
|
|
|
579
|
+
/**
|
|
580
|
+
* What "Make video this size" does: the video's new size, and the Dest Rect that fills it.
|
|
581
|
+
*
|
|
582
|
+
* The pixels are the clip's, rounded down to even numbers. The units stay as wide as they
|
|
583
|
+
* are now, and get as tall as the clip's shape needs. The rectangle puts one clip pixel on
|
|
584
|
+
* one output pixel, from the top left corner. If a side was odd, its last column or row is
|
|
585
|
+
* off the edge, rather than resampling the whole picture.
|
|
586
|
+
*/
|
|
587
|
+
function clipSize(info: VideoInfo): { size: VideoSize; rect: ReadOnlyRect } {
|
|
588
|
+
const { width: videoWidth, height: videoHeight } = info.video;
|
|
589
|
+
const pixelWidth = evenSize(videoWidth);
|
|
590
|
+
const pixelHeight = evenSize(videoHeight);
|
|
591
|
+
const width = canvasRecorder.width;
|
|
592
|
+
const height = (width * pixelHeight) / pixelWidth;
|
|
593
|
+
const unitsPerPixel = width / pixelWidth;
|
|
594
|
+
return {
|
|
595
|
+
size: { pixelWidth, pixelHeight, width, height },
|
|
596
|
+
rect: { x: 0, y: 0, width: videoWidth * unitsPerPixel, height: videoHeight * unitsPerPixel },
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/** True when "Make video this size" would change nothing. */
|
|
601
|
+
function videoMatchesClip(info: VideoInfo): boolean {
|
|
602
|
+
const target = targetKeyframe();
|
|
603
|
+
// Several keyframes and none chosen: clicking explains that, so keep the button enabled.
|
|
604
|
+
if (typeof target === "string") return false;
|
|
605
|
+
const { size, rect } = clipSize(info);
|
|
606
|
+
const close = (a: number, b: number) => Math.abs(a - b) <= 1e-6 * Math.max(1, Math.abs(b));
|
|
607
|
+
const current = canvasRecorder.size;
|
|
608
|
+
return (
|
|
609
|
+
current.pixelWidth === size.pixelWidth &&
|
|
610
|
+
current.pixelHeight === size.pixelHeight &&
|
|
611
|
+
close(current.height, size.height) &&
|
|
612
|
+
close(target.value.x, rect.x) &&
|
|
613
|
+
close(target.value.y, rect.y) &&
|
|
614
|
+
close(target.value.width, rect.width) &&
|
|
615
|
+
close(target.value.height, rect.height)
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
sizeVideoBtn.addEventListener("click", () => {
|
|
620
|
+
if (!info) return;
|
|
621
|
+
const target = targetKeyframe();
|
|
622
|
+
if (typeof target === "string") {
|
|
623
|
+
rectHint.textContent = target;
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
const { width: videoWidth, height: videoHeight } = info.video;
|
|
627
|
+
const { size, rect } = clipSize(info);
|
|
628
|
+
const { pixelWidth, pixelHeight } = size;
|
|
629
|
+
if (!hooks.setVideoSize(size)) return;
|
|
630
|
+
target.value = rect;
|
|
631
|
+
hooks.rectKeyframeChanged(target);
|
|
632
|
+
rectHint.textContent =
|
|
633
|
+
pixelWidth === videoWidth && pixelHeight === videoHeight
|
|
634
|
+
? ""
|
|
635
|
+
: `The clip is ${videoWidth} × ${videoHeight}. The video is ${pixelWidth} × ${pixelHeight}, ` +
|
|
636
|
+
"because HEVC needs even sizes, so the clip's last column or row is cut off.";
|
|
637
|
+
drawDerived();
|
|
638
|
+
});
|
|
639
|
+
|
|
472
640
|
for (const { btn, make } of rectButtons) {
|
|
473
641
|
btn.addEventListener("click", () => {
|
|
474
642
|
if (!info) return;
|
|
@@ -505,6 +673,24 @@ export function buildVideoClipPanel(
|
|
|
505
673
|
hooks.soundClipsChanged();
|
|
506
674
|
});
|
|
507
675
|
|
|
676
|
+
// The size fields changed, so "Make video this size" may have something to do now, or not.
|
|
677
|
+
const stopWatchingSize = canvasRecorder.onSizeChange(() => {
|
|
678
|
+
if (!panel.isConnected) {
|
|
679
|
+
stopWatchingSize();
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
drawDerived();
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
// Permission granted, or a file picked again: a file that failed may open now.
|
|
686
|
+
const stopListening = onLocalFilesChange(() => {
|
|
687
|
+
if (!panel.isConnected) {
|
|
688
|
+
stopListening();
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
if (state === "error") refresh();
|
|
692
|
+
});
|
|
693
|
+
|
|
508
694
|
refresh();
|
|
509
695
|
return {
|
|
510
696
|
element: panel,
|
|
@@ -42,7 +42,7 @@ export declare class AudioBuilder {
|
|
|
42
42
|
* And the url will start with "./".
|
|
43
43
|
*
|
|
44
44
|
* CORS can be an issue depending on the server.
|
|
45
|
-
*
|
|
45
|
+
* A `local:` URL reads a file on the user's computer. See src/local-files.ts.
|
|
46
46
|
* @param startMsInDestination 0 to play the new clip at the the beginning of the video.
|
|
47
47
|
* 1000 to start this clip 1 second after the video starts.
|
|
48
48
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audio-builder.d.ts","sourceRoot":"","sources":["../../dev/audio-builder.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"audio-builder.d.ts","sourceRoot":"","sources":["../../dev/audio-builder.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AACH,qBAAa,YAAY;;IACvB;;;OAGG;IACH,OAAO,CAAC,YAAY,CAAsB;IAC1C;;OAEG;IACH,OAAO,CAAC,MAAM,CAAc;IAE5B,cAAc,IAAI,WAAW;gBAIjB,eAAe,EAAE,MAAM;IAwCnC,UAAU;IAKV,SAAS,CAAC,GAAG,EAAE,MAAM;IAIrB,kFAAkF;IAClF,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI;IAajD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAoDG;IACG,GAAG,CACP,GAAG,EAAE,MAAM,EACX,oBAAoB,EAAE,MAAM,EAC5B,eAAe,GAAE,MAAU,EAC3B,MAAM,GAAE,MAAiB,GACxB,OAAO,CAAC,IAAI,CAAC;IAgEV,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAKvB,oBAAoB,CAAC,YAAY,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAQzE,OAAO,CAAC,gBAAgB;CA4CzB"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fetchMedia } from "../src/local-files.js";
|
|
1
2
|
/**
|
|
2
3
|
* This creates a complete audio clip by joining multiple clips.
|
|
3
4
|
* It starts with all silence and you can add the clips wherever you need them.
|
|
@@ -38,7 +39,7 @@ export class AudioBuilder {
|
|
|
38
39
|
}
|
|
39
40
|
async #createNewAudioBuffer(url) {
|
|
40
41
|
const time1 = performance.now();
|
|
41
|
-
const response = await
|
|
42
|
+
const response = await fetchMedia(url);
|
|
42
43
|
const time2 = performance.now();
|
|
43
44
|
const encodedSource = await response.arrayBuffer();
|
|
44
45
|
const time3 = performance.now();
|
|
@@ -86,7 +87,7 @@ export class AudioBuilder {
|
|
|
86
87
|
* And the url will start with "./".
|
|
87
88
|
*
|
|
88
89
|
* CORS can be an issue depending on the server.
|
|
89
|
-
*
|
|
90
|
+
* A `local:` URL reads a file on the user's computer. See src/local-files.ts.
|
|
90
91
|
* @param startMsInDestination 0 to play the new clip at the the beginning of the video.
|
|
91
92
|
* 1000 to start this clip 1 second after the video starts.
|
|
92
93
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audio-builder.js","sourceRoot":"","sources":["../../dev/audio-builder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,OAAO,YAAY;IACvB;;;OAGG;IACK,YAAY,CAAsB;IAC1C;;OAEG;IACK,MAAM,CAAc;IAE5B,cAAc;QACZ,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAED,YAAY,eAAuB;QACjC,MAAM,UAAU,GAAG,KAAK,CAAC;QACzB,yEAAyE;QACzE,wEAAwE;QACxE,mEAAmE;QACnE,IAAI,CAAC,YAAY,GAAG,IAAI,mBAAmB,CAAC;YAC1C,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;YAC3C,UAAU;SACX,CAAC,CAAC;QACH,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAC3B,CAAC,EACD,IAAI,CAAC,IAAI,CAAC,CAAC,eAAe,GAAG,IAAI,CAAC,GAAG,UAAU,CAAC,CACjD,CAAC;QAEF,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC,EAAE,YAAY,EAAE,UAAU,CAAC,CAAC;IAC5E,CAAC;IAED,KAAK,CAAC,qBAAqB,CAAC,GAAW;QACrC,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;QAChC,MAAM,QAAQ,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"audio-builder.js","sourceRoot":"","sources":["../../dev/audio-builder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAEnD;;;;;;;;;;;;GAYG;AACH,MAAM,OAAO,YAAY;IACvB;;;OAGG;IACK,YAAY,CAAsB;IAC1C;;OAEG;IACK,MAAM,CAAc;IAE5B,cAAc;QACZ,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAED,YAAY,eAAuB;QACjC,MAAM,UAAU,GAAG,KAAK,CAAC;QACzB,yEAAyE;QACzE,wEAAwE;QACxE,mEAAmE;QACnE,IAAI,CAAC,YAAY,GAAG,IAAI,mBAAmB,CAAC;YAC1C,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;YAC3C,UAAU;SACX,CAAC,CAAC;QACH,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAC3B,CAAC,EACD,IAAI,CAAC,IAAI,CAAC,CAAC,eAAe,GAAG,IAAI,CAAC,GAAG,UAAU,CAAC,CACjD,CAAC;QAEF,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC,EAAE,YAAY,EAAE,UAAU,CAAC,CAAC;IAC5E,CAAC;IAED,KAAK,CAAC,qBAAqB,CAAC,GAAW;QACrC,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;QAChC,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,GAAG,CAAC,CAAC;QACvC,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;QAChC,MAAM,aAAa,GAAG,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC;QACnD,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;QAChC,+FAA+F;QAC/F,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,eAAe,CAAC,aAAa,CAAC,CAAC;QAC5E,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;QAChC,MAAM,UAAU,GAAG,aAAa,CAAC,UAAU,GAAG,IAAI,CAAC;QACnD,KAAK;YACH,OAAO,CAAC,GAAG,CACT,kBAAkB,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO;gBAC9E,GAAG,CAAC,CAAC,KAAK,GAAG,KAAK,CAAC,GAAG,UAAU,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,YAAY;gBACxD,gBAAgB,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ;gBAClD,WAAW,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAC7C,CAAC;QACJ,OAAO,YAAY,CAAC;IACtB,CAAC;IAEQ,MAAM,GAAG,IAAI,GAAG,EAAgC,CAAC;IACjD,SAAS,GAAG,IAAI,GAAG,EAAuB,CAAC;IAEpD,UAAU;QACR,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QACpB,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC;IACzB,CAAC;IAED,SAAS,CAAC,GAAW;QACnB,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC;IAC7B,CAAC;IAED,kFAAkF;IAClF,gBAAgB,CAAC,GAAW;QAC1B,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC;IACzC,CAAC;IAED,gBAAgB,CAAC,GAAW;QAC1B,OAAO,IAAI,CAAC,MAAM,CAAC,mBAAmB,CAAC,GAAG,EAAE,CAAC,UAAU,EAAE,EAAE,CACzD,IAAI,CAAC,qBAAqB,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE;YAClD,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;YACpC,OAAO,GAAG,CAAC;QACb,CAAC,CAAC,CACH,CAAC;IACJ,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAoDG;IACH,KAAK,CAAC,GAAG,CACP,GAAW,EACX,oBAA4B,EAC5B,kBAA0B,CAAC,EAC3B,SAAiB,QAAQ;QAEzB,IAAI,oBAAoB,GAAG,CAAC,EAAE,CAAC;YAC7B,yDAAyD;YACzD,eAAe,IAAI,oBAAoB,CAAC;YACxC,MAAM,IAAI,oBAAoB,CAAC;YAC/B,oBAAoB,GAAG,CAAC,CAAC;QAC3B,CAAC;QACD,IAAI,eAAe,GAAG,CAAC,EAAE,CAAC;YACxB,kEAAkE;YAClE,oBAAoB,IAAI,eAAe,CAAC;YACxC,MAAM,IAAI,eAAe,CAAC;YAC1B,eAAe,GAAG,CAAC,CAAC;QACtB,CAAC;QAED,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC;QAEtD,MAAM,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC,UAAU,GAAG,IAAI,CAAC;QACnD,MAAM,gBAAgB,GAAG,IAAI,CAAC,KAAK,CAAC,oBAAoB,GAAG,YAAY,CAAC,CAAC;QACzE,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,eAAe,GAAG,YAAY,CAAC,CAAC;QAC/D,0EAA0E;QAC1E,uEAAuE;QACvE,iDAAiD;QACjD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CACpB,CAAC,EACD,IAAI,CAAC,GAAG,CACN,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,YAAY,CAAC,EACjC,YAAY,CAAC,MAAM,GAAG,WAAW,EACjC,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,gBAAgB,CACtC,CACF,CAAC;QACF,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC;YAChB,OAAO;QACT,CAAC;QACD,MAAM,WAAW,GAAG,IAAI,CAAC,MAAM;aAC5B,cAAc,CAAC,CAAC,CAAC;aACjB,QAAQ,CAAC,gBAAgB,EAAE,gBAAgB,GAAG,KAAK,CAAC,CAAC;QACxD,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CACzB,EAAE,MAAM,EAAE,YAAY,CAAC,gBAAgB,EAAE,EACzC,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,CACb,YAAY;aACT,cAAc,CAAC,OAAO,CAAC;aACvB,QAAQ,CAAC,WAAW,EAAE,WAAW,GAAG,KAAK,CAAC,CAChD,CAAC;QACF,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC1B,mBAAmB;YACnB,MAAM,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;YAC3B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;gBAC/B,WAAW,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC;YAC9B,CAAC;QACH,CAAC;aAAM,CAAC;YACN,uEAAuE;YACvE,uEAAuE;YACvE,sBAAsB;YACtB,MAAM,KAAK,GAAG,CAAC,GAAG,QAAQ,CAAC,MAAM,CAAC;YAClC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;gBAC/B,IAAI,GAAG,GAAG,CAAC,CAAC;gBACZ,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;oBAC/B,GAAG,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC;gBACpB,CAAC;gBACD,WAAW,CAAC,CAAC,CAAC,IAAI,GAAG,GAAG,KAAK,CAAC;YAChC,CAAC;QACH,CAAC;IACH,CAAC;IAED,KAAK,CAAC,MAAM;QACV,MAAM,cAAc,GAAG,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC1D,OAAO,IAAI,IAAI,CAAC,CAAC,cAAc,CAAC,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC;IAC3D,CAAC;IAED,KAAK,CAAC,oBAAoB,CAAC,YAA8B;QACvD,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;QACjC,YAAY,CAAC,GAAG,GAAG,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;QAC7C,iCAAiC;IACnC,CAAC;IAED,gCAAgC;IAChC,2DAA2D;IACnD,gBAAgB,CAAC,MAAmB;QAC1C,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;QAClC,MAAM,WAAW,GAAG,MAAM,CAAC,gBAAgB,CAAC;QAC5C,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC;QACrC,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,GAAG,WAAW,GAAG,CAAC,GAAG,EAAE,CAAC;QACpD,MAAM,WAAW,GAAG,IAAI,WAAW,CAAC,MAAM,CAAC,CAAC;QAC5C,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,WAAW,CAAC,CAAC;QAEvC,MAAM,WAAW,GAAG,CAAC,MAAc,EAAE,GAAW,EAAE,EAAE;YAClD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE;gBACjC,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;QACjD,CAAC,CAAC;QAEF,WAAW,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;QACvB,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,GAAG,MAAM,CAAC,MAAM,GAAG,WAAW,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC;QAC9D,WAAW,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;QACvB,WAAW,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;QACxB,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;QAC7B,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC;QAC5B,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,WAAW,EAAE,IAAI,CAAC,CAAC;QACtC,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,UAAU,EAAE,IAAI,CAAC,CAAC;QACrC,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,UAAU,GAAG,WAAW,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC;QACvD,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,WAAW,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC;QAC1C,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;QAC7B,WAAW,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;QACxB,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,MAAM,CAAC,MAAM,GAAG,WAAW,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC;QAE1D,IAAI,MAAM,GAAG,EAAE,CAAC;QAChB,KAAK,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,WAAW,EAAE,EAAE,EAAE,EAAE,CAAC;YACxC,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;YACzB,MAAM,OAAO,GAAG,MAAM,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC;YAC1C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;gBACvC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;gBACrD,IAAI,CAAC,QAAQ,CACX,MAAM,EACN,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,MAAM,GAAG,MAAM,EAC9C,IAAI,CACL,CAAC;gBACF,MAAM,IAAI,CAAC,CAAC;YACd,CAAC;QACH,CAAC;QAED,OAAO,WAAW,CAAC;IACrB,CAAC;CACF"}
|
|
@@ -367,3 +367,62 @@ body {
|
|
|
367
367
|
outline: 2px solid #4a90d9;
|
|
368
368
|
outline-offset: -2px;
|
|
369
369
|
}
|
|
370
|
+
|
|
371
|
+
/* ── Multi Text editor ───────────────────────────────────────────────── */
|
|
372
|
+
|
|
373
|
+
/* The FormatEditor in dev/format-editor.ts: plain text with a menu ("chip")
|
|
374
|
+
in front of each run of text, naming its Text Format. From
|
|
375
|
+
format-editor-prototype's style.css. */
|
|
376
|
+
.format-editor {
|
|
377
|
+
min-height: 6rem;
|
|
378
|
+
padding: 0.5rem 0.75rem;
|
|
379
|
+
border: 1px solid #c7c7cc;
|
|
380
|
+
border-radius: 6px;
|
|
381
|
+
background: white;
|
|
382
|
+
/* Newlines are stored as plain "\n" characters, not <br> or <div>. */
|
|
383
|
+
white-space: pre-wrap;
|
|
384
|
+
overflow-wrap: anywhere;
|
|
385
|
+
line-height: 1.9;
|
|
386
|
+
font-size: 1.05rem;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.format-editor:focus-within {
|
|
390
|
+
outline: 2px solid #0a66d8;
|
|
391
|
+
outline-offset: 1px;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.format-chip {
|
|
395
|
+
display: inline-block;
|
|
396
|
+
margin: 0 1px;
|
|
397
|
+
vertical-align: baseline;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.format-chip select {
|
|
401
|
+
font: inherit;
|
|
402
|
+
font-size: 0.8rem;
|
|
403
|
+
line-height: 1.2;
|
|
404
|
+
padding: 0 0.15rem;
|
|
405
|
+
color: #1d1d1f;
|
|
406
|
+
background: #e3edfb;
|
|
407
|
+
border: 1px solid #9dbbe6;
|
|
408
|
+
border-radius: 4px;
|
|
409
|
+
cursor: pointer;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/* A name that isn't one of the formats this text can see. */
|
|
413
|
+
.format-chip.invalid select {
|
|
414
|
+
color: #d70015;
|
|
415
|
+
background: #fde8ea;
|
|
416
|
+
border-color: #d70015;
|
|
417
|
+
font-weight: 600;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/* In the open menu, all options except the unknown one use the normal color. */
|
|
421
|
+
.format-chip.invalid select option {
|
|
422
|
+
color: #1d1d1f;
|
|
423
|
+
font-weight: normal;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.format-chip select option.invalid {
|
|
427
|
+
color: #d70015;
|
|
428
|
+
}
|