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.
Files changed (317) hide show
  1. package/BREAKING-CHANGES.md +18 -0
  2. package/README.md +112 -20
  3. package/dev/audio-builder.ts +4 -2
  4. package/dev/canvas-recorder.css +59 -0
  5. package/dev/canvas-recorder.ts +679 -51
  6. package/dev/delay-files.ts +1 -1
  7. package/dev/font-utils.ts +8 -1
  8. package/dev/format-editor.ts +734 -0
  9. package/dev/format-string.ts +110 -0
  10. package/dev/local-file-row.ts +107 -0
  11. package/dev/multi-text-dialog.ts +360 -0
  12. package/dev/multi-text-panel.ts +11 -0
  13. package/dev/player-body.ts +86 -6
  14. package/dev/rect-snap.ts +9 -9
  15. package/dev/slide-panel.ts +1 -1
  16. package/dev/split-dialog.ts +13 -2
  17. package/dev/static-image-panel.ts +35 -5
  18. package/dev/timeline-display.ts +50 -5
  19. package/dev/transcripts.ts +5 -3
  20. package/dev/video-clip-panel.ts +201 -15
  21. package/dist/dev/audio-builder.d.ts +1 -1
  22. package/dist/dev/audio-builder.d.ts.map +1 -1
  23. package/dist/dev/audio-builder.js +3 -2
  24. package/dist/dev/audio-builder.js.map +1 -1
  25. package/dist/dev/canvas-recorder.css +59 -0
  26. package/dist/dev/canvas-recorder.js +612 -50
  27. package/dist/dev/canvas-recorder.js.map +1 -1
  28. package/dist/dev/delay-files.js +1 -1
  29. package/dist/dev/delay-files.js.map +1 -1
  30. package/dist/dev/font-utils.d.ts.map +1 -1
  31. package/dist/dev/font-utils.js +9 -1
  32. package/dist/dev/font-utils.js.map +1 -1
  33. package/dist/dev/format-editor.d.ts +75 -0
  34. package/dist/dev/format-editor.d.ts.map +1 -0
  35. package/dist/dev/format-editor.js +642 -0
  36. package/dist/dev/format-editor.js.map +1 -0
  37. package/dist/dev/format-string.d.ts +62 -0
  38. package/dist/dev/format-string.d.ts.map +1 -0
  39. package/dist/dev/format-string.js +86 -0
  40. package/dist/dev/format-string.js.map +1 -0
  41. package/dist/dev/local-file-row.d.ts +22 -0
  42. package/dist/dev/local-file-row.d.ts.map +1 -0
  43. package/dist/dev/local-file-row.js +84 -0
  44. package/dist/dev/local-file-row.js.map +1 -0
  45. package/dist/dev/multi-text-dialog.d.ts +30 -0
  46. package/dist/dev/multi-text-dialog.d.ts.map +1 -0
  47. package/dist/dev/multi-text-dialog.js +297 -0
  48. package/dist/dev/multi-text-dialog.js.map +1 -0
  49. package/dist/dev/multi-text-panel.d.ts +2 -0
  50. package/dist/dev/multi-text-panel.d.ts.map +1 -1
  51. package/dist/dev/multi-text-panel.js +4 -0
  52. package/dist/dev/multi-text-panel.js.map +1 -1
  53. package/dist/dev/player-body.d.ts +1 -1
  54. package/dist/dev/player-body.d.ts.map +1 -1
  55. package/dist/dev/player-body.js +86 -6
  56. package/dist/dev/player-body.js.map +1 -1
  57. package/dist/dev/rect-snap.d.ts +1 -1
  58. package/dist/dev/rect-snap.js +7 -7
  59. package/dist/dev/rect-snap.js.map +1 -1
  60. package/dist/dev/slide-panel.d.ts +1 -1
  61. package/dist/dev/slide-panel.d.ts.map +1 -1
  62. package/dist/dev/slide-panel.js +1 -1
  63. package/dist/dev/slide-panel.js.map +1 -1
  64. package/dist/dev/split-dialog.d.ts.map +1 -1
  65. package/dist/dev/split-dialog.js +14 -2
  66. package/dist/dev/split-dialog.js.map +1 -1
  67. package/dist/dev/static-image-panel.d.ts +2 -0
  68. package/dist/dev/static-image-panel.d.ts.map +1 -1
  69. package/dist/dev/static-image-panel.js +29 -5
  70. package/dist/dev/static-image-panel.js.map +1 -1
  71. package/dist/dev/timeline-display.d.ts +23 -2
  72. package/dist/dev/timeline-display.d.ts.map +1 -1
  73. package/dist/dev/timeline-display.js +47 -5
  74. package/dist/dev/timeline-display.js.map +1 -1
  75. package/dist/dev/transcripts.d.ts.map +1 -1
  76. package/dist/dev/transcripts.js +5 -3
  77. package/dist/dev/transcripts.js.map +1 -1
  78. package/dist/dev/video-clip-panel.d.ts +8 -0
  79. package/dist/dev/video-clip-panel.d.ts.map +1 -1
  80. package/dist/dev/video-clip-panel.js +175 -9
  81. package/dist/dev/video-clip-panel.js.map +1 -1
  82. package/dist/lib/index.d.ts +3 -0
  83. package/dist/lib/index.d.ts.map +1 -1
  84. package/dist/lib/index.js +3 -0
  85. package/dist/lib/index.js.map +1 -1
  86. package/dist/lib/main-control.d.ts +50 -4
  87. package/dist/lib/main-control.d.ts.map +1 -1
  88. package/dist/lib/main-control.js +94 -12
  89. package/dist/lib/main-control.js.map +1 -1
  90. package/dist/src/binary-search.js +1 -1
  91. package/dist/src/binary-search.js.map +1 -1
  92. package/dist/src/corner-rounder.d.ts +17 -0
  93. package/dist/src/corner-rounder.d.ts.map +1 -0
  94. package/dist/src/corner-rounder.js +89 -0
  95. package/dist/src/corner-rounder.js.map +1 -0
  96. package/dist/src/cross-fade.d.ts +1 -1
  97. package/dist/src/cross-fade.d.ts.map +1 -1
  98. package/dist/src/cross-fade.js +3 -3
  99. package/dist/src/cross-fade.js.map +1 -1
  100. package/dist/src/fancy-text.d.ts +116 -0
  101. package/dist/src/fancy-text.d.ts.map +1 -0
  102. package/dist/src/fancy-text.js +211 -0
  103. package/dist/src/fancy-text.js.map +1 -0
  104. package/dist/src/glib/letters-base.d.ts +1 -1
  105. package/dist/src/glib/letters-base.d.ts.map +1 -1
  106. package/dist/src/glib/letters-base.js +4 -3
  107. package/dist/src/glib/letters-base.js.map +1 -1
  108. package/dist/src/glib/line-font.d.ts +2 -2
  109. package/dist/src/glib/line-font.d.ts.map +1 -1
  110. package/dist/src/glib/line-font.js +2 -2
  111. package/dist/src/glib/line-font.js.map +1 -1
  112. package/dist/src/glib/paragraph-layout.d.ts +2 -2
  113. package/dist/src/glib/paragraph-layout.d.ts.map +1 -1
  114. package/dist/src/glib/paragraph-layout.js +1 -1
  115. package/dist/src/glib/paragraph-layout.js.map +1 -1
  116. package/dist/src/glib/path-shape-splitter.d.ts +1 -1
  117. package/dist/src/glib/path-shape-splitter.d.ts.map +1 -1
  118. package/dist/src/glib/path-shape-splitter.js +1 -1
  119. package/dist/src/glib/path-shape-splitter.js.map +1 -1
  120. package/dist/src/glib/path-shape.js +2 -2
  121. package/dist/src/glib/path-shape.js.map +1 -1
  122. package/dist/src/interpolate.d.ts +2 -2
  123. package/dist/src/interpolate.d.ts.map +1 -1
  124. package/dist/src/interpolate.js +1 -1
  125. package/dist/src/interpolate.js.map +1 -1
  126. package/dist/src/lattice.d.ts +1 -1
  127. package/dist/src/lattice.d.ts.map +1 -1
  128. package/dist/src/local-files.d.ts +129 -0
  129. package/dist/src/local-files.d.ts.map +1 -0
  130. package/dist/src/local-files.js +327 -0
  131. package/dist/src/local-files.js.map +1 -0
  132. package/dist/src/morph-animation.d.ts +1 -1
  133. package/dist/src/morph-animation.d.ts.map +1 -1
  134. package/dist/src/morph-animation.js +2 -2
  135. package/dist/src/morph-animation.js.map +1 -1
  136. package/dist/src/peano-fourier/fourier-shared.d.ts +1 -1
  137. package/dist/src/peano-fourier/fourier-shared.d.ts.map +1 -1
  138. package/dist/src/peano-fourier/fourier-shared.js +17 -4
  139. package/dist/src/peano-fourier/fourier-shared.js.map +1 -1
  140. package/dist/src/schedule-helper.d.ts +4 -4
  141. package/dist/src/schedule-helper.d.ts.map +1 -1
  142. package/dist/src/schedule-helper.js +2 -2
  143. package/dist/src/schedule-helper.js.map +1 -1
  144. package/dist/src/showable.d.ts +36 -4
  145. package/dist/src/showable.d.ts.map +1 -1
  146. package/dist/src/showable.js +37 -1
  147. package/dist/src/showable.js.map +1 -1
  148. package/dist/src/slide-components/arrow.d.ts +4 -4
  149. package/dist/src/slide-components/arrow.d.ts.map +1 -1
  150. package/dist/src/slide-components/arrow.js +2 -2
  151. package/dist/src/slide-components/arrow.js.map +1 -1
  152. package/dist/src/slide-components/cross-fade-transition.d.ts +3 -3
  153. package/dist/src/slide-components/cross-fade-transition.d.ts.map +1 -1
  154. package/dist/src/slide-components/cross-fade-transition.js +3 -3
  155. package/dist/src/slide-components/cross-fade-transition.js.map +1 -1
  156. package/dist/src/slide-components/duration-agnostic.d.ts +3 -3
  157. package/dist/src/slide-components/duration-agnostic.d.ts.map +1 -1
  158. package/dist/src/slide-components/duration-agnostic.js +1 -1
  159. package/dist/src/slide-components/duration-agnostic.js.map +1 -1
  160. package/dist/src/slide-components/fixed-duration.d.ts +1 -1
  161. package/dist/src/slide-components/fixed-duration.d.ts.map +1 -1
  162. package/dist/src/slide-components/fixed-duration.js +1 -1
  163. package/dist/src/slide-components/fixed-duration.js.map +1 -1
  164. package/dist/src/slide-components/fourier.d.ts +6 -6
  165. package/dist/src/slide-components/fourier.d.ts.map +1 -1
  166. package/dist/src/slide-components/fourier.js +6 -6
  167. package/dist/src/slide-components/fourier.js.map +1 -1
  168. package/dist/src/slide-components/frame-counter.d.ts +2 -2
  169. package/dist/src/slide-components/frame-counter.d.ts.map +1 -1
  170. package/dist/src/slide-components/frame-counter.js +1 -1
  171. package/dist/src/slide-components/frame-counter.js.map +1 -1
  172. package/dist/src/slide-components/function-graph.d.ts +2 -2
  173. package/dist/src/slide-components/function-graph.d.ts.map +1 -1
  174. package/dist/src/slide-components/function-graph.js +3 -3
  175. package/dist/src/slide-components/function-graph.js.map +1 -1
  176. package/dist/src/slide-components/halftone-shadow.d.ts +4 -4
  177. package/dist/src/slide-components/halftone-shadow.d.ts.map +1 -1
  178. package/dist/src/slide-components/halftone-shadow.js +2 -2
  179. package/dist/src/slide-components/halftone-shadow.js.map +1 -1
  180. package/dist/src/slide-components/handwriting-effect.d.ts +5 -5
  181. package/dist/src/slide-components/handwriting-effect.d.ts.map +1 -1
  182. package/dist/src/slide-components/handwriting-effect.js +5 -5
  183. package/dist/src/slide-components/handwriting-effect.js.map +1 -1
  184. package/dist/src/slide-components/hold.d.ts +3 -3
  185. package/dist/src/slide-components/hold.d.ts.map +1 -1
  186. package/dist/src/slide-components/hold.js +3 -3
  187. package/dist/src/slide-components/hold.js.map +1 -1
  188. package/dist/src/slide-components/image.d.ts +3 -3
  189. package/dist/src/slide-components/image.d.ts.map +1 -1
  190. package/dist/src/slide-components/image.js +3 -3
  191. package/dist/src/slide-components/image.js.map +1 -1
  192. package/dist/src/slide-components/in-parallel.d.ts +3 -3
  193. package/dist/src/slide-components/in-parallel.d.ts.map +1 -1
  194. package/dist/src/slide-components/in-parallel.js +2 -2
  195. package/dist/src/slide-components/in-parallel.js.map +1 -1
  196. package/dist/src/slide-components/in-series.d.ts +4 -4
  197. package/dist/src/slide-components/in-series.d.ts.map +1 -1
  198. package/dist/src/slide-components/in-series.js +5 -1
  199. package/dist/src/slide-components/in-series.js.map +1 -1
  200. package/dist/src/slide-components/live-duration.d.ts +1 -1
  201. package/dist/src/slide-components/live-duration.d.ts.map +1 -1
  202. package/dist/src/slide-components/live-duration.js +1 -1
  203. package/dist/src/slide-components/live-duration.js.map +1 -1
  204. package/dist/src/slide-components/multi-text.d.ts +76 -15
  205. package/dist/src/slide-components/multi-text.d.ts.map +1 -1
  206. package/dist/src/slide-components/multi-text.js +170 -25
  207. package/dist/src/slide-components/multi-text.js.map +1 -1
  208. package/dist/src/slide-components/rectangle.d.ts +3 -3
  209. package/dist/src/slide-components/rectangle.d.ts.map +1 -1
  210. package/dist/src/slide-components/rectangle.js +1 -1
  211. package/dist/src/slide-components/rectangle.js.map +1 -1
  212. package/dist/src/slide-components/registry.d.ts +1 -1
  213. package/dist/src/slide-components/registry.d.ts.map +1 -1
  214. package/dist/src/slide-components/registry.js +18 -18
  215. package/dist/src/slide-components/registry.js.map +1 -1
  216. package/dist/src/slide-components/serialize.d.ts +1 -1
  217. package/dist/src/slide-components/serialize.d.ts.map +1 -1
  218. package/dist/src/slide-components/serialize.js +2 -2
  219. package/dist/src/slide-components/serialize.js.map +1 -1
  220. package/dist/src/slide-components/show-error.js +2 -2
  221. package/dist/src/slide-components/show-error.js.map +1 -1
  222. package/dist/src/slide-components/simple-text.d.ts +4 -4
  223. package/dist/src/slide-components/simple-text.d.ts.map +1 -1
  224. package/dist/src/slide-components/simple-text.js +4 -4
  225. package/dist/src/slide-components/simple-text.js.map +1 -1
  226. package/dist/src/slide-components/slide-component.d.ts +5 -5
  227. package/dist/src/slide-components/slide-component.d.ts.map +1 -1
  228. package/dist/src/slide-components/slide-component.js +4 -4
  229. package/dist/src/slide-components/slide-component.js.map +1 -1
  230. package/dist/src/slide-components/slide-left.d.ts +3 -3
  231. package/dist/src/slide-components/slide-left.d.ts.map +1 -1
  232. package/dist/src/slide-components/slide-left.js +3 -3
  233. package/dist/src/slide-components/slide-left.js.map +1 -1
  234. package/dist/src/slide-components/split-video-clip.d.ts +2 -2
  235. package/dist/src/slide-components/split-video-clip.d.ts.map +1 -1
  236. package/dist/src/slide-components/split-video-clip.js +3 -3
  237. package/dist/src/slide-components/split-video-clip.js.map +1 -1
  238. package/dist/src/slide-components/text-effect.d.ts +2 -2
  239. package/dist/src/slide-components/text-effect.d.ts.map +1 -1
  240. package/dist/src/slide-components/traditional-text.d.ts +4 -4
  241. package/dist/src/slide-components/traditional-text.d.ts.map +1 -1
  242. package/dist/src/slide-components/traditional-text.js +3 -3
  243. package/dist/src/slide-components/traditional-text.js.map +1 -1
  244. package/dist/src/slide-components/video-clip.d.ts +17 -5
  245. package/dist/src/slide-components/video-clip.d.ts.map +1 -1
  246. package/dist/src/slide-components/video-clip.js +52 -11
  247. package/dist/src/slide-components/video-clip.js.map +1 -1
  248. package/dist/src/slide-components/video-info.d.ts +11 -11
  249. package/dist/src/slide-components/video-info.d.ts.map +1 -1
  250. package/dist/src/slide-components/video-info.js +32 -21
  251. package/dist/src/slide-components/video-info.js.map +1 -1
  252. package/dist/src/slow-image-sources.d.ts +7 -1
  253. package/dist/src/slow-image-sources.d.ts.map +1 -1
  254. package/dist/src/slow-image-sources.js +36 -5
  255. package/dist/src/slow-image-sources.js.map +1 -1
  256. package/dist/src/snapshot.d.ts +2 -2
  257. package/dist/src/snapshot.d.ts.map +1 -1
  258. package/dist/src/snapshot.js +4 -4
  259. package/dist/src/snapshot.js.map +1 -1
  260. package/dist/src/stroke-colors.d.ts +1 -1
  261. package/dist/src/stroke-colors.d.ts.map +1 -1
  262. package/dist/src/stroke-colors.js +4 -4
  263. package/dist/src/stroke-colors.js.map +1 -1
  264. package/dist/src/utility.d.ts +3 -4
  265. package/dist/src/utility.d.ts.map +1 -1
  266. package/dist/src/utility.js +4 -4
  267. package/dist/src/utility.js.map +1 -1
  268. package/lib/index.ts +3 -0
  269. package/lib/main-control.ts +119 -12
  270. package/package.json +7 -5
  271. package/src/binary-search.ts +2 -2
  272. package/src/corner-rounder.ts +128 -0
  273. package/src/cross-fade.ts +3 -3
  274. package/src/fancy-text.ts +255 -0
  275. package/src/glib/letters-base.ts +4 -3
  276. package/src/glib/line-font.ts +2 -2
  277. package/src/glib/paragraph-layout.ts +2 -2
  278. package/src/glib/path-shape-splitter.ts +1 -1
  279. package/src/glib/path-shape.ts +2 -2
  280. package/src/interpolate.ts +3 -3
  281. package/src/lattice.ts +1 -1
  282. package/src/local-files.ts +389 -0
  283. package/src/morph-animation.ts +2 -2
  284. package/src/peano-fourier/fourier-shared.ts +17 -4
  285. package/src/schedule-helper.ts +4 -4
  286. package/src/showable.ts +54 -4
  287. package/src/slide-components/arrow.ts +4 -4
  288. package/src/slide-components/cross-fade-transition.ts +4 -4
  289. package/src/slide-components/duration-agnostic.ts +3 -3
  290. package/src/slide-components/fixed-duration.ts +1 -1
  291. package/src/slide-components/fourier.ts +9 -9
  292. package/src/slide-components/frame-counter.ts +2 -2
  293. package/src/slide-components/function-graph.ts +4 -4
  294. package/src/slide-components/halftone-shadow.ts +4 -4
  295. package/src/slide-components/handwriting-effect.ts +7 -7
  296. package/src/slide-components/hold.ts +4 -4
  297. package/src/slide-components/image.ts +5 -5
  298. package/src/slide-components/in-parallel.ts +4 -4
  299. package/src/slide-components/in-series.ts +7 -4
  300. package/src/slide-components/live-duration.ts +1 -1
  301. package/src/slide-components/multi-text.ts +195 -30
  302. package/src/slide-components/rectangle.ts +3 -3
  303. package/src/slide-components/registry.ts +19 -19
  304. package/src/slide-components/serialize.ts +2 -2
  305. package/src/slide-components/show-error.ts +2 -2
  306. package/src/slide-components/simple-text.ts +7 -7
  307. package/src/slide-components/slide-component.ts +7 -7
  308. package/src/slide-components/slide-left.ts +4 -4
  309. package/src/slide-components/split-video-clip.ts +4 -4
  310. package/src/slide-components/text-effect.ts +2 -2
  311. package/src/slide-components/traditional-text.ts +5 -5
  312. package/src/slide-components/video-clip.ts +64 -13
  313. package/src/slide-components/video-info.ts +33 -21
  314. package/src/slow-image-sources.ts +38 -6
  315. package/src/snapshot.ts +4 -4
  316. package/src/stroke-colors.ts +4 -4
  317. package/src/utility.ts +5 -5
@@ -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
- PIXELS_PER_UNIT,
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 * PIXELS_PER_UNIT) / info.video.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 wholeFileBtn = button(
177
- timingRow,
178
- "Whole file",
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 16×9 canvas. Centered.",
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 ${PIXELS_PER_UNIT} px/unit — pixel perfect. Keeps the rectangle's center.`,
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 set";
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.length === 1 &&
408
- sameSoundClip(imported[0], expectedSoundClip(clip, audio))
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
- setStatus("Audio imported, but out of date", "#a06000");
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
- * Local files are forbidden.
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":"AAAA;;;;;;;;;;;;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
+ {"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 fetch(url);
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
- * Local files are forbidden.
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":"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"}
1
+ {"version":3,"file":"audio-builder.js","sourceRoot":"","sources":["../../dev/audio-builder.ts"],"names":[],"mappings":"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"}
@@ -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
+ }