@plotdb/lotion 0.3.0 → 0.5.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/CHANGELOG.md +46 -0
- package/README.md +70 -4
- package/cli.js +56 -13
- package/index.css +46 -0
- package/index.js +226 -12
- package/index.min.css +1 -1
- package/index.min.js +1 -1
- package/package.json +1 -1
- package/vector.js +1345 -46
- package/vector.min.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,49 @@
|
|
|
1
|
+
## v0.5.0
|
|
2
|
+
|
|
3
|
+
- player: captions. `captions: [[t0, t1, text, meta?], ...]` or `{lang: [...]}`, drawn outside the stage in a
|
|
4
|
+
bottom safe area, styled by css variables; readers turn them on / off with the `CC` button, the `c` key or
|
|
5
|
+
`showCaptions(on)`; `captionsBurned: true` starts them off for scenes that already show the narration.
|
|
6
|
+
Not drawn in `?render` unless `burnCaptions` ( or `?render&captions` ).
|
|
7
|
+
- player: events `on` / `off` ( `time`, `chapter`, `caption`, `captions` ) and state `chapter`, `caption`,
|
|
8
|
+
`captionsOn`, `captionLang`
|
|
9
|
+
- chapters may carry a third element, `meta` ( such as `{description, thumb}` ), passed through in `player.chapter`
|
|
10
|
+
- `lotion.vtt.parse` / `stringify`: WebVTT to and from the caption format
|
|
11
|
+
- cli: `lotion video` writes captions as `.vtt` next to the mp4 ( shifted by `--from` ); `lotion captions` exports
|
|
12
|
+
them alone. render protocol: `window.CAPTIONS`
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
## v0.4.2
|
|
16
|
+
|
|
17
|
+
- animate, size: `clip-path: inset()` ( reveals ) becomes one svg clipPath whose rect moves by smil, instead of one
|
|
18
|
+
appearance per inset ( lotitor's sumi-ink trial: html variants 68 to 35 )
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
## v0.4.1
|
|
22
|
+
|
|
23
|
+
- bug fix:
|
|
24
|
+
- vector / animate: box-shadow and text-shadow are reordered to "offsets blur spread color", since computed style
|
|
25
|
+
puts the color first and satori then drew a broken path, so shadows were missing
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
## v0.4.0
|
|
29
|
+
|
|
30
|
+
- features:
|
|
31
|
+
- `lotion.animate(opt)` / `player.animate(opt)` ( experimental, in `vector.js` ): convert a whole animation into one
|
|
32
|
+
animated svg. each element becomes a `<g>` nested as in the dom; its motion ( position and transform as a matrix,
|
|
33
|
+
opacity, blur ) is sampled per frame and written as css keyframes, simplified to the points linear interpolation
|
|
34
|
+
needs; each distinct appearance is drawn once by satori and switched by visibility. inline svg is walked
|
|
35
|
+
natively: groups animate, leaves keep their markup with numeric attributes as smil `<animate>`, definitions are
|
|
36
|
+
written once, and text uses fonts embedded once. `mix-blend-mode` now works at any depth.
|
|
37
|
+
- demo: the `/vector/` page converts the whole animation and plays the animated svg
|
|
38
|
+
- animate, size: numeric tracks are written once per element instead of once per appearance ( positions of svg
|
|
39
|
+
leaves join the group matrix, inheritable values such as `stroke-dashoffset` animate on the group );
|
|
40
|
+
css gradient masks moved by `mask-position` / `mask-size` ( wipes ) become one svg mask with a moving rect, so
|
|
41
|
+
the content is drawn once; paths drawn progressively ( each `d` a prefix of a longer one ) become the full path
|
|
42
|
+
revealed by a dash animation. the code-only sumi-ink trial drops from 3.2 MB to 1.5 MB.
|
|
43
|
+
- tweaks:
|
|
44
|
+
- vector: inline svg keeps a content-based placeholder `src` until fonts are ready
|
|
45
|
+
|
|
46
|
+
|
|
1
47
|
## v0.3.0
|
|
2
48
|
|
|
3
49
|
- features:
|
package/README.md
CHANGED
|
@@ -48,7 +48,14 @@ player options:
|
|
|
48
48
|
- `width`, `height`: design size of the stage. default `1920` x `1080`. The stage scales to fit the container.
|
|
49
49
|
- `duration`: total length in seconds.
|
|
50
50
|
- `seek(t)`: render the frame at time `t`. must depend on `t` only.
|
|
51
|
-
- `chapters`: optional `[[t, name], ...]`, shown as ticks on the timeline and used by `←` / `→`.
|
|
51
|
+
- `chapters`: optional `[[t, name, meta?], ...]`, shown as ticks on the timeline and used by `←` / `→`. `meta` is
|
|
52
|
+
an optional object ( such as `{description, thumb}` ) the player passes through in `player.chapter`.
|
|
53
|
+
- `captions`: optional `[[t0, t1, text, meta?], ...]` ( `meta` such as `{speaker}` ), or `{zh: [...], en: [...]}`
|
|
54
|
+
for several languages. See Captions.
|
|
55
|
+
- `captionsBurned`: `true` when the scene already shows the narration as text: captions start off. default
|
|
56
|
+
`false` ( captions start on ). Readers can always turn them on or off.
|
|
57
|
+
- `captionLang`: initial caption language. default the first one.
|
|
58
|
+
- `burnCaptions`: draw captions in `?render` mode ( frame export ). default `false`; `?render&captions` also works.
|
|
52
59
|
- `start`: initial time. `?t=<sec>` in the url takes precedence.
|
|
53
60
|
- `autoplay`: default `false`.
|
|
54
61
|
- `cues`: optional function returning sound cues `[{t, ...}]`, exported by `lotion cues`.
|
|
@@ -56,16 +63,45 @@ player options:
|
|
|
56
63
|
`false` shows nothing.
|
|
57
64
|
|
|
58
65
|
player methods: `start()`, `seek(t)`, `play(go = true)`, `pause()`, `toggle()`, `fullscreen()`, `encode(opt)`
|
|
59
|
-
( see Online Export )
|
|
66
|
+
( see Online Export ), `showCaptions(on = true)`, `setCaptionLang(lang)`, `on(name, cb)`, `off(name, cb)`.
|
|
60
67
|
`player.ready` is a promise resolved by `start()`.
|
|
61
68
|
|
|
69
|
+
player state: `t`, `playing`, `chapter` ( `{index, t, name, meta}` or `null` ), `caption`
|
|
70
|
+
( `{t0, t1, text, meta, lang}` or `null` ), `captionsOn`, `captionLang`, `captionLangs`.
|
|
71
|
+
|
|
72
|
+
events ( `player.on(name, cb)` ):
|
|
73
|
+
|
|
74
|
+
- `time`: every seek, with `t`.
|
|
75
|
+
- `chapter`: the current chapter changed, with `player.chapter`.
|
|
76
|
+
- `caption`: the shown caption changed ( including to `null` ), with `player.caption`. Fired whether captions
|
|
77
|
+
are on or off, so a page can read them anyway ( search, transcripts ).
|
|
78
|
+
- `captions`: captions turned on / off or switched language, with `{on, lang}`.
|
|
79
|
+
|
|
62
80
|
Until `start()` is called, the stage is hidden behind the loading screen, the control bar is disabled, and
|
|
63
81
|
`seek` / `play` do nothing. Scenes often need asynchronous preparation before the first frame is right
|
|
64
82
|
( web fonts, measuring the layout to place things ), and a half-built stage should not be seen or scrubbed.
|
|
65
83
|
So build the scene, finish whatever it waits for, then call `start()`:
|
|
66
84
|
|
|
67
85
|
Promise.all([document.fonts.ready, prepare!]).then -> p.start!
|
|
68
|
-
Keys ( when the player is focused ): `space` play / pause, `←` / `→` previous / next chapter, `f` fullscreen
|
|
86
|
+
Keys ( when the player is focused ): `space` play / pause, `←` / `→` previous / next chapter, `f` fullscreen,
|
|
87
|
+
`c` captions on / off.
|
|
88
|
+
|
|
89
|
+
### Captions
|
|
90
|
+
|
|
91
|
+
Captions are drawn by the player, outside the stage: they keep their size when the stage scales, sit in a safe
|
|
92
|
+
area at the bottom, and are not part of `encode()` frames. Style them with css variables on `.lotion`:
|
|
93
|
+
`--lotion-caption-size`, `--lotion-caption-bg`, `--lotion-caption-color`, `--lotion-caption-font`.
|
|
94
|
+
The control bar shows a `CC` button when there are captions.
|
|
95
|
+
|
|
96
|
+
`lotion.vtt.parse(text)` reads WebVTT into the caption format ( a `<v name>` voice becomes `{speaker}` ),
|
|
97
|
+
`lotion.vtt.stringify(captions)` writes it back. `lotion video` writes a `.vtt` next to the mp4, and
|
|
98
|
+
`lotion captions` exports it alone.
|
|
99
|
+
|
|
100
|
+
Guidelines for splitting ( common subtitle practice ): Traditional Chinese up to about 16 characters a line, at most
|
|
101
|
+
2 lines, no faster than about 9 characters a second; English up to about 42 characters a line, 17 - 20 a second.
|
|
102
|
+
Keep each caption on screen 0.83 - 7 s, with at least 2 frames between captions. Break at punctuation or pauses,
|
|
103
|
+
not inside a phrase; merge captions that are too short. With word timings from a speech generator, cut at sentence
|
|
104
|
+
ends and at pauses longer than about 0.3 s, then split or merge by those limits.
|
|
69
105
|
|
|
70
106
|
helpers:
|
|
71
107
|
|
|
@@ -147,6 +183,33 @@ dependency after lotion ( `{name: 'lotion', path: 'vector.min.js'}` ); rescope g
|
|
|
147
183
|
web font ( system fonts are not available ), mask or clip-path under a transform other than a translation
|
|
148
184
|
( satori does not move masks with transforms ).
|
|
149
185
|
|
|
186
|
+
### Animated svg
|
|
187
|
+
|
|
188
|
+
`lotion.animate({el, seek, duration, fps})` ( or `player.animate({fps})` ) converts the whole animation into one svg
|
|
189
|
+
that plays by itself, resolving to `{svg, warnings, stats}`:
|
|
190
|
+
|
|
191
|
+
- every element becomes a `<g>`, nested as in the dom. Its motion, the position and transform combined into a
|
|
192
|
+
matrix, opacity and a `blur()` filter, is sampled at `fps` and written as css keyframes, keeping only the samples
|
|
193
|
+
linear interpolation needs.
|
|
194
|
+
- everything else is its appearance. Each distinct appearance is drawn once ( by satori ) and shown by visibility
|
|
195
|
+
keyframes, so an element that only moves costs one drawing, while one whose content changes ( typing, a growing
|
|
196
|
+
width ) costs one per state. Elements with a css mask, clip-path or other filters, `<img>` and `<canvas>`, are
|
|
197
|
+
drawn with their children as one appearance.
|
|
198
|
+
- inline svg is kept as svg: groups animate like elements, leaves keep their markup with numeric attributes
|
|
199
|
+
( coordinates, `stroke-dashoffset`, opacities ... ) written as smil `<animate>`, definitions are written once,
|
|
200
|
+
and text uses fonts embedded once. nested `<svg>` becomes a group, since smil inside a nested svg does not follow
|
|
201
|
+
the outer svg when paused or seeked.
|
|
202
|
+
- tracks are written once per element: positions of svg leaves join the group matrix, and inheritable values
|
|
203
|
+
( `stroke-dashoffset`, `stroke-width`, fill / stroke opacity ) animate on the group.
|
|
204
|
+
- a css mask of one `linear-gradient` ( horizontal or vertical, `no-repeat` ) becomes an svg mask whose rect follows
|
|
205
|
+
`mask-position` / `mask-size`, so a wipe draws its content once. Other masks and clip-paths are drawn per state.
|
|
206
|
+
- a path whose `d` grows ( each one a prefix of a longer one, stroked without fill, dashes or markers ) becomes the
|
|
207
|
+
longest path revealed by a dash animation.
|
|
208
|
+
- options: `fps` ( default 30 ), `from`, `to`, `loop` ( default true ), `progress`, `signal`.
|
|
209
|
+
|
|
210
|
+
The demo converts in under a second into 230 KB ( 35 KB with brotli ); the code-only sumi-ink trial ( 44 s at 10 fps )
|
|
211
|
+
in about 2 seconds into 1.5 MB ( 230 KB with brotli ). Serve it compressed, or save it as `.svgz`. Frames of the animated svg match screenshots as closely as single frames do.
|
|
212
|
+
|
|
150
213
|
On the demo and the sumi-ink trial of lotitor, frames rasterized from the svg match screenshots at 29 - 39 dB PSNR;
|
|
151
214
|
the remaining difference is mostly text antialiasing and baselines in fixed line-heights. A frame takes 50 - 300 ms.
|
|
152
215
|
|
|
@@ -158,6 +221,8 @@ With `?render` in the url, a page should cover the viewport with its stage and e
|
|
|
158
221
|
- `window.seek(t)`: render the frame at `t`.
|
|
159
222
|
- `window.DURATION`: total length in seconds.
|
|
160
223
|
- `window.cues()`: optional, list of sound cues.
|
|
224
|
+
- `window.CAPTIONS`: optional, captions as `{lang: [[t0, t1, text, meta?], ...]}` ( `lotion video` writes them as
|
|
225
|
+
`.vtt` ).
|
|
161
226
|
|
|
162
227
|
`lotion.player` does this automatically, and only when `start()` is called: the cli waits for `window.seek` to
|
|
163
228
|
exist before rendering, so it never captures a scene that is still being prepared. Any other page can follow
|
|
@@ -193,8 +258,9 @@ targets under the root ( or use `:scope` ). A block sample is in `web/src/pug/bl
|
|
|
193
258
|
|
|
194
259
|
lotion frames <src> <outdir> <t...> one png per time
|
|
195
260
|
lotion sheet <src> <out.png> <t...> the same, tiled into one contact sheet
|
|
196
|
-
lotion video <src> <out.mp4> render to mp4
|
|
261
|
+
lotion video <src> <out.mp4> render to mp4, plus <out>.vtt when the page has captions
|
|
197
262
|
lotion cues <src> <out.json> dump window.cues()
|
|
263
|
+
lotion captions <src> <out.vtt> export captions as WebVTT ( out.<lang>.vtt for several languages )
|
|
198
264
|
lotion bundle <base-url> <block> <out> pack a block and its dependencies into one file
|
|
199
265
|
lotion bgm | sfx | mix | beats removed, see Audio
|
|
200
266
|
|
package/cli.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var fs, path, http, child_process, usage, parse, types, serve, playwright, open, shot, frames, sheet, WARM, encode, video, cues, bundle, moved, argv, ref$, pos, opt, cmd, src, out, rest, times, p, slice$ = [].slice;
|
|
2
|
+
var fs, path, http, child_process, usage, parse, types, serve, playwright, open, shot, frames, sheet, WARM, encode, video, writeVtt, cues, bundle, moved, argv, ref$, pos, opt, cmd, src, out, rest, times, p, slice$ = [].slice;
|
|
3
3
|
fs = require('fs');
|
|
4
4
|
path = require('path');
|
|
5
5
|
http = require('http');
|
|
6
6
|
child_process = require('child_process');
|
|
7
|
-
usage = 'usage:\n lotion <frames|sheet|video|cues> <src> <out> [t...] [options]\n lotion bundle <base-url> <block-name> <out> [options]\n audio generators ( bgm | sfx | mix | beats ) were removed, see README > Audio\nsee README for options.';
|
|
7
|
+
usage = 'usage:\n lotion <frames|sheet|video|cues|captions> <src> <out> [t...] [options]\n lotion bundle <base-url> <block-name> <out> [options]\n audio generators ( bgm | sfx | mix | beats ) were removed, see README > Audio\nsee README for options.';
|
|
8
8
|
parse = function(argv){
|
|
9
9
|
var ref$, pos, opt, i, a;
|
|
10
10
|
ref$ = [[], {}], pos = ref$[0], opt = ref$[1];
|
|
@@ -269,7 +269,7 @@ video = function(src, out, opt){
|
|
|
269
269
|
workers = Math.max(1, Math.floor(+(opt.workers || 1)));
|
|
270
270
|
t0 = Date.now();
|
|
271
271
|
return open(src, opt).then(function(first){
|
|
272
|
-
var n, done, tick, finish, k, tmp, parts;
|
|
272
|
+
var n, done, tick, finish, vtt, k, tmp, parts;
|
|
273
273
|
o.from = +(opt.from || 0);
|
|
274
274
|
n = Math.round((+(opt.to || first.duration) - o.from) * o.fps);
|
|
275
275
|
done = 0;
|
|
@@ -282,8 +282,11 @@ video = function(src, out, opt){
|
|
|
282
282
|
finish = function(){
|
|
283
283
|
return console.log(out + " ( " + n + " frames, " + ((Date.now() - t0) / 1000).toFixed(0) + "s )");
|
|
284
284
|
};
|
|
285
|
+
vtt = writeVtt(first.page, out, o.from, +(opt.to || first.duration));
|
|
285
286
|
if (workers === 1) {
|
|
286
|
-
return
|
|
287
|
+
return vtt.then(function(){
|
|
288
|
+
return encode(first.page, 0, n, out, o, tick);
|
|
289
|
+
}).then(function(){
|
|
287
290
|
return first.close();
|
|
288
291
|
}).then(finish);
|
|
289
292
|
}
|
|
@@ -302,15 +305,17 @@ video = function(src, out, opt){
|
|
|
302
305
|
out: path.join(tmp, "part-" + w + ".mp4")
|
|
303
306
|
};
|
|
304
307
|
});
|
|
305
|
-
return
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
308
|
+
return vtt.then(function(){
|
|
309
|
+
return Promise.all([Promise.resolve(first)].concat((function(){
|
|
310
|
+
var i$, to$, results$ = [];
|
|
311
|
+
for (i$ = 1, to$ = k; i$ < to$; ++i$) {
|
|
312
|
+
results$.push(i$);
|
|
313
|
+
}
|
|
314
|
+
return results$;
|
|
315
|
+
}()).map(function(){
|
|
316
|
+
return open(src, opt);
|
|
317
|
+
})));
|
|
318
|
+
}).then(function(pages){
|
|
314
319
|
return Promise.all(parts.map(function(p, w){
|
|
315
320
|
return encode(pages[w].page, p.i0, p.i1, p.out, o, tick);
|
|
316
321
|
}))['finally'](function(){
|
|
@@ -345,6 +350,36 @@ video = function(src, out, opt){
|
|
|
345
350
|
}).then(finish);
|
|
346
351
|
});
|
|
347
352
|
};
|
|
353
|
+
writeVtt = function(page, out, t0, t1){
|
|
354
|
+
var vtt;
|
|
355
|
+
vtt = require('./index').vtt;
|
|
356
|
+
return page.evaluate(function(){
|
|
357
|
+
return window.CAPTIONS || null;
|
|
358
|
+
}).then(function(tracks){
|
|
359
|
+
var langs, base, i$, len$, lang, list, f, results$ = [];
|
|
360
|
+
if (!tracks) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
langs = Object.keys(tracks);
|
|
364
|
+
base = out.replace(/\.(vtt|mp4|webm|mov)$/i, '');
|
|
365
|
+
for (i$ = 0, len$ = langs.length; i$ < len$; ++i$) {
|
|
366
|
+
lang = langs[i$];
|
|
367
|
+
list = tracks[lang].filter(fn$).map(fn1$);
|
|
368
|
+
f = langs.length > 1 && lang
|
|
369
|
+
? base + "." + lang + ".vtt"
|
|
370
|
+
: base + ".vtt";
|
|
371
|
+
fs.writeFileSync(f, vtt.stringify(list));
|
|
372
|
+
results$.push(console.log(f));
|
|
373
|
+
}
|
|
374
|
+
return results$;
|
|
375
|
+
function fn$(c){
|
|
376
|
+
return c[1] > t0 && c[0] < t1;
|
|
377
|
+
}
|
|
378
|
+
function fn1$(c){
|
|
379
|
+
return [Math.max(0, c[0] - t0), Math.min(t1, c[1]) - t0].concat(c.slice(2));
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
};
|
|
348
383
|
cues = function(src, out, opt){
|
|
349
384
|
return open(src, opt).then(function(arg$){
|
|
350
385
|
var page, close;
|
|
@@ -449,6 +484,14 @@ if ((ref$ = argv[0]) === 'bgm' || ref$ === 'sfx' || ref$ === 'mix' || ref$ === '
|
|
|
449
484
|
return video(src, out, opt);
|
|
450
485
|
case 'cues':
|
|
451
486
|
return cues(src, out, opt);
|
|
487
|
+
case 'captions':
|
|
488
|
+
return open(src, opt).then(function(arg$){
|
|
489
|
+
var page, close;
|
|
490
|
+
page = arg$.page, close = arg$.close;
|
|
491
|
+
return writeVtt(page, out, 0, Infinity).then(function(){
|
|
492
|
+
return close();
|
|
493
|
+
});
|
|
494
|
+
});
|
|
452
495
|
case 'bundle':
|
|
453
496
|
if (rest[0]) {
|
|
454
497
|
return bundle(src, out, rest[0], opt);
|
package/index.css
CHANGED
|
@@ -66,6 +66,52 @@
|
|
|
66
66
|
color: var(--lotion-ink);
|
|
67
67
|
border: 1px solid var(--lotion-line);
|
|
68
68
|
}
|
|
69
|
+
.lotion .lotion-cc {
|
|
70
|
+
width: auto;
|
|
71
|
+
height: 28px;
|
|
72
|
+
padding: 0 8px;
|
|
73
|
+
border-radius: 6px;
|
|
74
|
+
font: 600 13px/1 sans-serif;
|
|
75
|
+
letter-spacing: 0.04em;
|
|
76
|
+
background: transparent;
|
|
77
|
+
color: var(--lotion-ink);
|
|
78
|
+
border: 1px solid var(--lotion-line);
|
|
79
|
+
}
|
|
80
|
+
.lotion.lotion-captions-on .lotion-cc {
|
|
81
|
+
background: var(--lotion-ink);
|
|
82
|
+
border-color: var(--lotion-ink);
|
|
83
|
+
color: #fff;
|
|
84
|
+
}
|
|
85
|
+
.lotion .lotion-viewport {
|
|
86
|
+
container-type: inline-size;
|
|
87
|
+
}
|
|
88
|
+
.lotion .lotion-captions {
|
|
89
|
+
--lotion-caption-size: clamp(14px, 2.6cqw, 44px);
|
|
90
|
+
position: absolute;
|
|
91
|
+
left: 10%;
|
|
92
|
+
right: 10%;
|
|
93
|
+
bottom: 6%;
|
|
94
|
+
display: none;
|
|
95
|
+
flex-direction: column;
|
|
96
|
+
align-items: center;
|
|
97
|
+
gap: 0.2em;
|
|
98
|
+
pointer-events: none;
|
|
99
|
+
text-align: center;
|
|
100
|
+
font-size: var(--lotion-caption-size);
|
|
101
|
+
line-height: 1.35;
|
|
102
|
+
}
|
|
103
|
+
.lotion.lotion-captions-on .lotion-captions {
|
|
104
|
+
display: flex;
|
|
105
|
+
}
|
|
106
|
+
.lotion .lotion-caption {
|
|
107
|
+
max-width: 100%;
|
|
108
|
+
padding: 0.12em 0.5em;
|
|
109
|
+
border-radius: 0.2em;
|
|
110
|
+
background: var(--lotion-caption-bg, rgba(0,0,0,0.72));
|
|
111
|
+
color: var(--lotion-caption-color, #fff);
|
|
112
|
+
font-family: var(--lotion-caption-font, sans-serif);
|
|
113
|
+
white-space: pre-wrap;
|
|
114
|
+
}
|
|
69
115
|
.lotion .lotion-time {
|
|
70
116
|
font: 15px/1 sans-serif;
|
|
71
117
|
color: var(--lotion-mute);
|
package/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
(function(){
|
|
2
|
-
var PI, sin, cos, exp, sqrt, round, clamp, lerp, ss, spring, presets, track, vis, bump, typing, hex, mixc, rgb, mk, put, txt, NS, svg, player, ref$, libs, libCache, lib, encode, lotion;
|
|
2
|
+
var PI, sin, cos, exp, sqrt, round, clamp, lerp, ss, spring, presets, track, vis, bump, typing, hex, mixc, rgb, mk, put, txt, NS, svg, player, ref$, libs, libCache, lib, encode, vttTime, vttParseTime, vtt, lotion;
|
|
3
3
|
PI = Math.PI, sin = Math.sin, cos = Math.cos, exp = Math.exp, sqrt = Math.sqrt, round = Math.round;
|
|
4
4
|
clamp = function(v, a, b){
|
|
5
5
|
a == null && (a = 0);
|
|
@@ -179,7 +179,7 @@
|
|
|
179
179
|
return e;
|
|
180
180
|
};
|
|
181
181
|
player = function(opt){
|
|
182
|
-
var this$ = this;
|
|
182
|
+
var c, tracks, k, v, ref$, this$ = this;
|
|
183
183
|
opt == null && (opt = {});
|
|
184
184
|
this.opt = opt;
|
|
185
185
|
this.root = typeof opt.root === 'string'
|
|
@@ -190,6 +190,9 @@
|
|
|
190
190
|
this.duration = opt.duration || 0;
|
|
191
191
|
this.chapters = opt.chapters || [];
|
|
192
192
|
this.t = 0;
|
|
193
|
+
this.chapter = null;
|
|
194
|
+
this.caption = null;
|
|
195
|
+
this.handlers = {};
|
|
193
196
|
this.playing = false;
|
|
194
197
|
this.last = null;
|
|
195
198
|
this.started = false;
|
|
@@ -197,8 +200,29 @@
|
|
|
197
200
|
return this$.readyRes = res;
|
|
198
201
|
});
|
|
199
202
|
this.renderMode = /[?&]render\b/.test(location.search);
|
|
203
|
+
c = opt.captions;
|
|
204
|
+
tracks = !c
|
|
205
|
+
? {}
|
|
206
|
+
: Array.isArray(c) ? {
|
|
207
|
+
'': c
|
|
208
|
+
} : c;
|
|
209
|
+
this.captionTracks = {};
|
|
210
|
+
for (k in tracks) {
|
|
211
|
+
v = tracks[k];
|
|
212
|
+
this.captionTracks[k] = v.slice().sort(fn$);
|
|
213
|
+
}
|
|
214
|
+
this.captionLangs = Object.keys(this.captionTracks);
|
|
215
|
+
this.captionLang = in$(opt.captionLang, this.captionLangs)
|
|
216
|
+
? opt.captionLang
|
|
217
|
+
: (ref$ = this.captionLangs[0]) != null ? ref$ : null;
|
|
218
|
+
this.captionsOn = this.renderMode
|
|
219
|
+
? !!(opt.burnCaptions || /[?&]captions\b/.test(location.search))
|
|
220
|
+
: !opt.captionsBurned;
|
|
200
221
|
this.init();
|
|
201
222
|
return this;
|
|
223
|
+
function fn$(a, b){
|
|
224
|
+
return a[0] - b[0];
|
|
225
|
+
}
|
|
202
226
|
};
|
|
203
227
|
player.prototype = (ref$ = Object.create(Object.prototype), ref$.constructor = player, ref$.init = function(){
|
|
204
228
|
var r, ref$, html, q, this$ = this;
|
|
@@ -218,7 +242,7 @@
|
|
|
218
242
|
html = typeof this.opt.loading === 'string' ? this.opt.loading : '<div class="lotion-spinner"></div>';
|
|
219
243
|
this.loading = mk('lotion-loading-screen', html, this.viewport);
|
|
220
244
|
}
|
|
221
|
-
this.bar = mk('lotion-bar', '<div class="lotion-btn lotion-play"></div>\n<div class="lotion-track"><div class="lotion-rail"></div><div class="lotion-fill"></div>\n<div class="lotion-chapters"></div><div class="lotion-knob"></div></div>\n<div class="lotion-time"></div>\n<div class="lotion-btn lotion-fs" title="全螢幕"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"\nstroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M2 6V2h4M10 2h4v4M14 10v4h-4M6 14H2v-4"/></svg></div>', r);
|
|
245
|
+
this.bar = mk('lotion-bar', '<div class="lotion-btn lotion-play"></div>\n<div class="lotion-track"><div class="lotion-rail"></div><div class="lotion-fill"></div>\n<div class="lotion-chapters"></div><div class="lotion-knob"></div></div>\n<div class="lotion-time"></div>\n<div class="lotion-btn lotion-cc" title="字幕 ( c )">CC</div>\n<div class="lotion-btn lotion-fs" title="全螢幕"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"\nstroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M2 6V2h4M10 2h4v4M14 10v4h-4M6 14H2v-4"/></svg></div>', r);
|
|
222
246
|
q = function(n){
|
|
223
247
|
return this$.bar.querySelector(".lotion-" + n);
|
|
224
248
|
};
|
|
@@ -229,15 +253,21 @@
|
|
|
229
253
|
time: q('time'),
|
|
230
254
|
track: q('track'),
|
|
231
255
|
chapters: q('chapters'),
|
|
232
|
-
fs: q('fs')
|
|
256
|
+
fs: q('fs'),
|
|
257
|
+
cc: q('cc')
|
|
233
258
|
};
|
|
234
259
|
this.chapters.map(function(arg$){
|
|
235
|
-
var t, name, e;
|
|
236
|
-
t = arg$[0], name = arg$[1];
|
|
260
|
+
var t, name, meta, e;
|
|
261
|
+
t = arg$[0], name = arg$[1], meta = arg$[2];
|
|
237
262
|
e = mk('lotion-ch', '', this$.el.chapters);
|
|
238
|
-
e.title = name;
|
|
263
|
+
e.title = name + (meta && meta.description ? "\n" + meta.description : '');
|
|
239
264
|
return e.style.left = 100 * t / this$.duration + "%";
|
|
240
265
|
});
|
|
266
|
+
this.el.captions = mk('lotion-captions', '', this.viewport);
|
|
267
|
+
if (!this.captionLangs.length) {
|
|
268
|
+
this.el.cc.remove();
|
|
269
|
+
}
|
|
270
|
+
this.root.classList.toggle('lotion-captions-on', this.captionsOn);
|
|
241
271
|
this.bind();
|
|
242
272
|
if (this.renderMode) {
|
|
243
273
|
r.classList.add('lotion-render');
|
|
@@ -267,6 +297,9 @@
|
|
|
267
297
|
if (this.opt.cues) {
|
|
268
298
|
window.cues = this.opt.cues;
|
|
269
299
|
}
|
|
300
|
+
if (this.captionLangs.length) {
|
|
301
|
+
window.CAPTIONS = this.captionTracks;
|
|
302
|
+
}
|
|
270
303
|
}
|
|
271
304
|
this.readyRes(this);
|
|
272
305
|
m = /[?&]t=([\d.]+)/.exec(location.search);
|
|
@@ -291,6 +324,10 @@
|
|
|
291
324
|
e.stopPropagation();
|
|
292
325
|
return this$.fullscreen();
|
|
293
326
|
});
|
|
327
|
+
this.el.cc.addEventListener('click', function(e){
|
|
328
|
+
e.stopPropagation();
|
|
329
|
+
return this$.showCaptions(!this$.captionsOn);
|
|
330
|
+
});
|
|
294
331
|
this.viewport.addEventListener('click', function(){
|
|
295
332
|
this$.root.focus();
|
|
296
333
|
return this$.toggle();
|
|
@@ -324,6 +361,8 @@
|
|
|
324
361
|
return this$.toggle();
|
|
325
362
|
} else if (e.key === 'f') {
|
|
326
363
|
return this$.fullscreen();
|
|
364
|
+
} else if (e.key === 'c' && this$.captionLangs.length) {
|
|
365
|
+
return this$.showCaptions(!this$.captionsOn);
|
|
327
366
|
} else if (e.key === 'ArrowRight') {
|
|
328
367
|
return this$.seek((ref$ = starts.find(function(s){
|
|
329
368
|
return s > this$.t + 0.05;
|
|
@@ -351,7 +390,7 @@
|
|
|
351
390
|
}, ref$.fmt = function(t){
|
|
352
391
|
return Math.floor(t / 60) + ":" + ("0" + Math.floor(t % 60)).slice(-2);
|
|
353
392
|
}, ref$.seek = function(t){
|
|
354
|
-
var p, ch;
|
|
393
|
+
var p, ci, i$, ref$, len$, i, c, ch;
|
|
355
394
|
if (!this.started || this.encoding) {
|
|
356
395
|
return;
|
|
357
396
|
}
|
|
@@ -360,11 +399,109 @@
|
|
|
360
399
|
p = 100 * t / this.duration + "%";
|
|
361
400
|
this.el.fill.style.width = p;
|
|
362
401
|
this.el.knob.style.left = p;
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
402
|
+
ci = -1;
|
|
403
|
+
for (i$ = 0, len$ = (ref$ = this.chapters).length; i$ < len$; ++i$) {
|
|
404
|
+
i = i$;
|
|
405
|
+
c = ref$[i$];
|
|
406
|
+
if (c[0] <= t) {
|
|
407
|
+
ci = i;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
ch = this.chapters[ci];
|
|
366
411
|
this.el.time.textContent = (ch ? ch[1] + " · " : '') + (this.fmt(t) + " / " + this.fmt(this.duration));
|
|
367
|
-
|
|
412
|
+
this.root.classList.toggle('lotion-playing', this.playing);
|
|
413
|
+
if ((ch
|
|
414
|
+
? ci
|
|
415
|
+
: -1) !== (this.chapter
|
|
416
|
+
? this.chapter.index
|
|
417
|
+
: -1)) {
|
|
418
|
+
this.chapter = ch ? {
|
|
419
|
+
index: ci,
|
|
420
|
+
t: ch[0],
|
|
421
|
+
name: ch[1],
|
|
422
|
+
meta: ch[2] || null
|
|
423
|
+
} : null;
|
|
424
|
+
this.fire('chapter', this.chapter);
|
|
425
|
+
}
|
|
426
|
+
this.updateCaption();
|
|
427
|
+
return this.fire('time', t);
|
|
428
|
+
}, ref$.updateCaption = function(){
|
|
429
|
+
var t, list, act, cap, key, i$, ref$, len$, line;
|
|
430
|
+
t = this.t;
|
|
431
|
+
list = (this.captionLang != null && this.captionTracks[this.captionLang]) || [];
|
|
432
|
+
act = list.filter(function(c){
|
|
433
|
+
return c[0] <= t && t < c[1];
|
|
434
|
+
});
|
|
435
|
+
cap = !act.length
|
|
436
|
+
? null
|
|
437
|
+
: {
|
|
438
|
+
t0: act[0][0],
|
|
439
|
+
t1: act[act.length - 1][1],
|
|
440
|
+
text: act.map(function(c){
|
|
441
|
+
return c[2];
|
|
442
|
+
}).join('\n'),
|
|
443
|
+
meta: act[0][3] || null,
|
|
444
|
+
lang: this.captionLang
|
|
445
|
+
};
|
|
446
|
+
key = function(c){
|
|
447
|
+
if (c) {
|
|
448
|
+
return c.lang + "|" + c.t0 + "|" + c.text;
|
|
449
|
+
} else {
|
|
450
|
+
return '';
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
if (key(cap) === key(this.caption)) {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
this.caption = cap;
|
|
457
|
+
this.el.captions.innerHTML = '';
|
|
458
|
+
if (cap) {
|
|
459
|
+
for (i$ = 0, len$ = (ref$ = cap.text.split('\n')).length; i$ < len$; ++i$) {
|
|
460
|
+
line = ref$[i$];
|
|
461
|
+
mk('lotion-caption', '', this.el.captions).textContent = line;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return this.fire('caption', cap);
|
|
465
|
+
}, ref$.showCaptions = function(v){
|
|
466
|
+
v == null && (v = true);
|
|
467
|
+
this.captionsOn = !!v;
|
|
468
|
+
this.root.classList.toggle('lotion-captions-on', this.captionsOn);
|
|
469
|
+
this.fire('captions', {
|
|
470
|
+
on: this.captionsOn,
|
|
471
|
+
lang: this.captionLang
|
|
472
|
+
});
|
|
473
|
+
return this;
|
|
474
|
+
}, ref$.setCaptionLang = function(lang){
|
|
475
|
+
if (!in$(lang, this.captionLangs)) {
|
|
476
|
+
return this;
|
|
477
|
+
}
|
|
478
|
+
this.captionLang = lang;
|
|
479
|
+
this.updateCaption();
|
|
480
|
+
this.fire('captions', {
|
|
481
|
+
on: this.captionsOn,
|
|
482
|
+
lang: lang
|
|
483
|
+
});
|
|
484
|
+
return this;
|
|
485
|
+
}, ref$['on'] = function(name, cb){
|
|
486
|
+
var ref$, ref1$;
|
|
487
|
+
((ref1$ = (ref$ = this.handlers)[name]) != null
|
|
488
|
+
? ref1$
|
|
489
|
+
: ref$[name] = []).push(cb);
|
|
490
|
+
return this;
|
|
491
|
+
}, ref$['off'] = function(name, cb){
|
|
492
|
+
if (this.handlers[name]) {
|
|
493
|
+
this.handlers[name] = this.handlers[name].filter(function(h){
|
|
494
|
+
return h !== cb;
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
return this;
|
|
498
|
+
}, ref$.fire = function(name, v){
|
|
499
|
+
var i$, ref$, len$, h, results$ = [];
|
|
500
|
+
for (i$ = 0, len$ = (ref$ = (this.handlers[name] || []).slice()).length; i$ < len$; ++i$) {
|
|
501
|
+
h = ref$[i$];
|
|
502
|
+
results$.push(h(v));
|
|
503
|
+
}
|
|
504
|
+
return results$;
|
|
368
505
|
}, ref$.tick = function(now){
|
|
369
506
|
var this$ = this;
|
|
370
507
|
if (!this.playing) {
|
|
@@ -583,7 +720,79 @@
|
|
|
583
720
|
throw e;
|
|
584
721
|
});
|
|
585
722
|
};
|
|
723
|
+
vttTime = function(t){
|
|
724
|
+
var ms, h, m, s, pad;
|
|
725
|
+
ms = Math.round(t * 1000);
|
|
726
|
+
h = Math.floor(ms / 3600000);
|
|
727
|
+
m = Math.floor(ms / 60000) % 60;
|
|
728
|
+
s = Math.floor(ms / 1000) % 60;
|
|
729
|
+
pad = function(v, n){
|
|
730
|
+
n == null && (n = 2);
|
|
731
|
+
return (v + "").padStart(n, '0');
|
|
732
|
+
};
|
|
733
|
+
return pad(h) + ":" + pad(m) + ":" + pad(s) + "." + pad(ms % 1000, 3);
|
|
734
|
+
};
|
|
735
|
+
vttParseTime = function(s){
|
|
736
|
+
var p;
|
|
737
|
+
p = s.trim().split(':');
|
|
738
|
+
return p.reduce(function(a, v){
|
|
739
|
+
return a * 60 + parseFloat(v);
|
|
740
|
+
}, 0);
|
|
741
|
+
};
|
|
742
|
+
vtt = {
|
|
743
|
+
parse: function(text){
|
|
744
|
+
var ret, blocks, i$, len$, b, lines, i, ref$, a, z, body, meta, m, cue;
|
|
745
|
+
ret = [];
|
|
746
|
+
blocks = text.replace(/\r\n?/g, '\n').split(/\n{2,}/);
|
|
747
|
+
for (i$ = 0, len$ = blocks.length; i$ < len$; ++i$) {
|
|
748
|
+
b = blocks[i$];
|
|
749
|
+
lines = b.split('\n').filter(fn$);
|
|
750
|
+
i = lines.findIndex(fn1$);
|
|
751
|
+
if (i < 0) {
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
ref$ = lines[i].split('-->'), a = ref$[0], z = ref$[1];
|
|
755
|
+
body = lines.slice(i + 1).join('\n');
|
|
756
|
+
meta = null;
|
|
757
|
+
if (m = /^<v(?:\.[^\s>]*)?\s+([^>]+)>/.exec(body)) {
|
|
758
|
+
meta = {
|
|
759
|
+
speaker: m[1].trim()
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
body = body.replace(/<\/?[^>]+>/g, '').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&');
|
|
763
|
+
cue = [vttParseTime(a), vttParseTime(z.trim().split(/\s+/)[0]), body];
|
|
764
|
+
if (meta) {
|
|
765
|
+
cue.push(meta);
|
|
766
|
+
}
|
|
767
|
+
ret.push(cue);
|
|
768
|
+
}
|
|
769
|
+
return ret;
|
|
770
|
+
function fn$(l){
|
|
771
|
+
return l.trim();
|
|
772
|
+
}
|
|
773
|
+
function fn1$(l){
|
|
774
|
+
return /-->/.test(l);
|
|
775
|
+
}
|
|
776
|
+
},
|
|
777
|
+
stringify: function(captions){
|
|
778
|
+
var esc, cues;
|
|
779
|
+
esc = function(s){
|
|
780
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
781
|
+
};
|
|
782
|
+
cues = captions.map(function(arg$){
|
|
783
|
+
var t0, t1, text, meta, body;
|
|
784
|
+
t0 = arg$[0], t1 = arg$[1], text = arg$[2], meta = arg$[3];
|
|
785
|
+
body = esc(text);
|
|
786
|
+
if (meta && meta.speaker) {
|
|
787
|
+
body = "<v " + esc(meta.speaker) + ">" + body;
|
|
788
|
+
}
|
|
789
|
+
return vttTime(t0) + " --> " + vttTime(t1) + "\n" + body;
|
|
790
|
+
});
|
|
791
|
+
return "WEBVTT\n\n" + cues.join('\n\n') + '\n';
|
|
792
|
+
}
|
|
793
|
+
};
|
|
586
794
|
lotion = {
|
|
795
|
+
vtt: vtt,
|
|
587
796
|
clamp: clamp,
|
|
588
797
|
lerp: lerp,
|
|
589
798
|
ss: ss,
|
|
@@ -610,6 +819,11 @@
|
|
|
610
819
|
} else if (typeof window != 'undefined' && window !== null) {
|
|
611
820
|
window.lotion = lotion;
|
|
612
821
|
}
|
|
822
|
+
function in$(x, xs){
|
|
823
|
+
var i = -1, l = xs.length >>> 0;
|
|
824
|
+
while (++i < l) if (x === xs[i]) return true;
|
|
825
|
+
return false;
|
|
826
|
+
}
|
|
613
827
|
function import$(obj, src){
|
|
614
828
|
var own = {}.hasOwnProperty;
|
|
615
829
|
for (var key in src) if (own.call(src, key)) obj[key] = src[key];
|