Spaces:
Running
Running
gemini-stars: 场景动画引擎与 9 个章节(横/竖屏自适应)(step 3)
Browse files- gemini-stars/README.md +1 -1
- gemini-stars/scenes.js +956 -0
- gemini-stars/vendor/mp4-muxer.LICENSE +21 -0
- gemini-stars/vendor/mp4-muxer.mjs +1879 -0
gemini-stars/README.md
CHANGED
|
@@ -60,7 +60,7 @@ gemini-stars/
|
|
| 60 |
|
| 61 |
- [x] Step 1:撰写本计划(本文件)并推送
|
| 62 |
- [x] Step 2:撰写旁白文案,生成 TTS 音频与 `timeline.js`,推送
|
| 63 |
-
- [
|
| 64 |
- [ ] Step 4:配乐与音效合成、混音、播放器,推送
|
| 65 |
- [ ] Step 5:MP4 本地导出(24/30/60fps,横/竖屏),推送
|
| 66 |
- [ ] Step 6:多平台封面 + 文案复制面板,推送
|
|
|
|
| 60 |
|
| 61 |
- [x] Step 1:撰写本计划(本文件)并推送
|
| 62 |
- [x] Step 2:撰写旁白文案,生成 TTS 音频与 `timeline.js`,推送
|
| 63 |
+
- [x] Step 3:场景动画引擎与全部场景(横/竖屏自适应),推送
|
| 64 |
- [ ] Step 4:配乐与音效合成、混音、播放器,推送
|
| 65 |
- [ ] Step 5:MP4 本地导出(24/30/60fps,横/竖屏),推送
|
| 66 |
- [ ] Step 6:多平台封面 + 文案复制面板,推送
|
gemini-stars/scenes.js
ADDED
|
@@ -0,0 +1,956 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// 双子座 · 场景渲染引擎(全新编写)
|
| 2 |
+
// 所有画面只由时间 t 决定(确定性),播放与导出共用。
|
| 3 |
+
import { TIMELINE } from './timeline.js';
|
| 4 |
+
export { TIMELINE };
|
| 5 |
+
|
| 6 |
+
export const SERIF = '"Noto Serif SC","Source Han Serif SC","Songti SC","STSong","SimSun",serif';
|
| 7 |
+
export const SANS = '"Noto Sans SC","PingFang SC","Microsoft YaHei","Hiragino Sans GB","Source Han Sans SC",sans-serif';
|
| 8 |
+
|
| 9 |
+
/* ---------------- 数学工具 ---------------- */
|
| 10 |
+
const TAU = Math.PI * 2;
|
| 11 |
+
export const clamp = (x, a = 0, b = 1) => Math.max(a, Math.min(b, x));
|
| 12 |
+
export const lerp = (a, b, k) => a + (b - a) * k;
|
| 13 |
+
export const sstep = (a, b, x) => { const k = clamp((x - a) / (b - a)); return k * k * (3 - 2 * k); };
|
| 14 |
+
const easeIO = k => (k < .5 ? 4 * k * k * k : 1 - Math.pow(-2 * k + 2, 3) / 2);
|
| 15 |
+
const easeOut = k => 1 - Math.pow(1 - clamp(k), 3);
|
| 16 |
+
const backOut = k => { k = clamp(k); const c = 1.7; return 1 + (c + 1) * Math.pow(k - 1, 3) + c * Math.pow(k - 1, 2); };
|
| 17 |
+
export function rng(seed) {
|
| 18 |
+
let a = seed >>> 0;
|
| 19 |
+
return () => { a |= 0; a = (a + 0x6D2B79F5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };
|
| 20 |
+
}
|
| 21 |
+
const fadeWin = (t, a, b, f = .6) => Math.min(sstep(a, a + f, t), 1 - sstep(b - f, b, t));
|
| 22 |
+
|
| 23 |
+
/* ---------------- 星表(RA 小时, Dec 度, 星等, 颜色) ---------------- */
|
| 24 |
+
const STARS = {
|
| 25 |
+
castor: [7.577, 31.89, 1.58, '#e3ecff', '北河二', 'Castor'],
|
| 26 |
+
pollux: [7.755, 28.03, 1.14, '#ffc985', '北河三', 'Pollux'],
|
| 27 |
+
alhena: [6.629, 16.40, 1.92, '#eef3ff', '井宿三', 'Alhena'],
|
| 28 |
+
mebsuta: [6.732, 25.13, 2.98, '#ffe2a8'],
|
| 29 |
+
tejat: [6.383, 22.51, 2.87, '#ffbf8a'],
|
| 30 |
+
propus: [6.248, 22.51, 3.30, '#ffb48a'],
|
| 31 |
+
mekbuda: [7.068, 20.57, 3.90, '#fff0c4'],
|
| 32 |
+
alzirr: [6.755, 12.90, 3.35, '#f4f6ff'],
|
| 33 |
+
wasat: [7.335, 21.98, 3.53, '#f7f7ff'],
|
| 34 |
+
kappa: [7.741, 24.40, 3.57, '#ffe6b0'],
|
| 35 |
+
lambda: [7.301, 16.54, 3.58, '#f4f7ff'],
|
| 36 |
+
theta: [6.879, 33.96, 3.60, '#f4f7ff'],
|
| 37 |
+
iota: [7.429, 27.80, 3.79, '#ffe1a6'],
|
| 38 |
+
upsilon: [7.599, 26.90, 4.06, '#ffc690'],
|
| 39 |
+
nu: [6.483, 20.21, 4.15, '#dfe8ff'],
|
| 40 |
+
tau: [7.186, 30.25, 4.41, '#ffd8a0'],
|
| 41 |
+
// 猎户与邻居
|
| 42 |
+
betel: [5.919, 7.41, 0.50, '#ffa266', '参宿四', 'Betelgeuse'],
|
| 43 |
+
rigel: [5.242, -8.20, 0.13, '#bcd2ff', '参宿七', 'Rigel'],
|
| 44 |
+
bella: [5.418, 6.35, 1.64, '#d6e2ff'],
|
| 45 |
+
saiph: [5.796, -9.67, 2.09, '#cfdcff'],
|
| 46 |
+
alnitak: [5.679, -1.94, 1.77, '#d8e4ff'],
|
| 47 |
+
alnilam: [5.604, -1.20, 1.69, '#d8e4ff'],
|
| 48 |
+
mintaka: [5.533, -0.30, 2.23, '#d8e4ff'],
|
| 49 |
+
meissa: [5.585, 9.93, 3.40, '#dde6ff'],
|
| 50 |
+
procyon: [7.655, 5.22, 0.34, '#fff6e0', '南河三', 'Procyon'],
|
| 51 |
+
};
|
| 52 |
+
const GEM_LINES = [
|
| 53 |
+
['castor', 'tau'], ['tau', 'theta'], ['tau', 'iota'], ['iota', 'upsilon'], ['upsilon', 'pollux'],
|
| 54 |
+
['tau', 'mebsuta'], ['mebsuta', 'nu'], ['mebsuta', 'tejat'], ['tejat', 'propus'],
|
| 55 |
+
['upsilon', 'kappa'], ['upsilon', 'wasat'], ['wasat', 'lambda'], ['wasat', 'mekbuda'], ['mekbuda', 'alhena'], ['lambda', 'alzirr'],
|
| 56 |
+
];
|
| 57 |
+
const ORI_LINES = [['betel', 'bella'], ['betel', 'alnitak'], ['bella', 'mintaka'], ['alnitak', 'alnilam'], ['alnilam', 'mintaka'], ['alnitak', 'saiph'], ['mintaka', 'rigel'], ['betel', 'meissa'], ['bella', 'meissa']];
|
| 58 |
+
const GEM_KEYS = Object.keys(STARS).slice(0, 16);
|
| 59 |
+
const COSD = Math.cos(22 * Math.PI / 180);
|
| 60 |
+
function sky(k) { const s = STARS[k]; return [-(s[0] - 7.0) * 15 * COSD, -(s[1] - 22)]; }
|
| 61 |
+
|
| 62 |
+
/* ---------------- 精灵缓存 ---------------- */
|
| 63 |
+
const spriteCache = new Map();
|
| 64 |
+
function glowSprite(color) {
|
| 65 |
+
if (spriteCache.has(color)) return spriteCache.get(color);
|
| 66 |
+
const c = mkCanvas(128, 128), g = c.getContext('2d');
|
| 67 |
+
const gr = g.createRadialGradient(64, 64, 0, 64, 64, 64);
|
| 68 |
+
gr.addColorStop(0, '#ffffff'); gr.addColorStop(.12, color); gr.addColorStop(.35, hexA(color, .35)); gr.addColorStop(1, hexA(color, 0));
|
| 69 |
+
g.fillStyle = gr; g.fillRect(0, 0, 128, 128);
|
| 70 |
+
spriteCache.set(color, c); return c;
|
| 71 |
+
}
|
| 72 |
+
let mkCanvasImpl = (w, h) => { if (typeof OffscreenCanvas !== 'undefined') return new OffscreenCanvas(w, h); const c = document.createElement('canvas'); c.width = w; c.height = h; return c; };
|
| 73 |
+
export function setCanvasFactory(f) { mkCanvasImpl = f; }
|
| 74 |
+
function mkCanvas(w, h) { return mkCanvasImpl(w, h); }
|
| 75 |
+
function hexA(hex, a) { const n = parseInt(hex.slice(1), 16); return `rgba(${n >> 16},${(n >> 8) & 255},${n & 255},${a})`; }
|
| 76 |
+
|
| 77 |
+
export function drawStar(ctx, x, y, r, color, alpha = 1, spikes = 0) {
|
| 78 |
+
if (alpha <= 0.002) return;
|
| 79 |
+
ctx.save(); ctx.globalAlpha = alpha; ctx.globalCompositeOperation = 'lighter';
|
| 80 |
+
const s = glowSprite(color); ctx.drawImage(s, x - r * 4, y - r * 4, r * 8, r * 8);
|
| 81 |
+
if (spikes > 0) {
|
| 82 |
+
ctx.globalAlpha = alpha * .55 * spikes;
|
| 83 |
+
const L = r * 9;
|
| 84 |
+
const g = ctx.createLinearGradient(x - L, y, x + L, y);
|
| 85 |
+
g.addColorStop(0, hexA(color, 0)); g.addColorStop(.5, hexA(color, 1)); g.addColorStop(1, hexA(color, 0));
|
| 86 |
+
ctx.fillStyle = g; ctx.fillRect(x - L, y - r * .12, L * 2, r * .24);
|
| 87 |
+
const g2 = ctx.createLinearGradient(x, y - L, x, y + L);
|
| 88 |
+
g2.addColorStop(0, hexA(color, 0)); g2.addColorStop(.5, hexA(color, 1)); g2.addColorStop(1, hexA(color, 0));
|
| 89 |
+
ctx.fillStyle = g2; ctx.fillRect(x - r * .12, y - L, r * .24, L * 2);
|
| 90 |
+
}
|
| 91 |
+
ctx.restore();
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
/* ---------------- 背景:星空 + 银河 ---------------- */
|
| 95 |
+
const BG_STARS = (() => { const r = rng(20260521); const a = []; for (let i = 0; i < 1100; i++) a.push({ x: r(), y: r(), m: Math.pow(r(), 3.2), d: .15 + r() * .85, ph: r() * TAU, f: .6 + r() * 2.4, c: r() < .2 ? '#ffd9a8' : r() < .5 ? '#cfe0ff' : '#ffffff' }); return a; })();
|
| 96 |
+
const mwCache = new Map();
|
| 97 |
+
function milkyWay(W, H) {
|
| 98 |
+
const key = W + 'x' + H; if (mwCache.has(key)) return mwCache.get(key);
|
| 99 |
+
const c = mkCanvas(Math.round(W * 1.4), Math.round(H * 1.3)), g = c.getContext('2d'), r = rng(77);
|
| 100 |
+
const ang = W > H ? -0.42 : -1.05, cx = W * .7, cy = H * .65, L = Math.hypot(W, H) * 1.3;
|
| 101 |
+
g.translate(cx, cy); g.rotate(ang);
|
| 102 |
+
const blobs = [['#3b2a7a', .22], ['#1d4f8f', .18], ['#7a2f6b', .12], ['#c58a4a', .06]];
|
| 103 |
+
for (let i = 0; i < 70; i++) {
|
| 104 |
+
const [col, a] = blobs[i % 4]; const x = (r() - .5) * L * 1.1, y = (r() - .5) * H * .22 * (1 + Math.sin(x / L * 5) * .4);
|
| 105 |
+
const rad = (80 + r() * 260) * Math.max(W, H) / 1920;
|
| 106 |
+
const gr = g.createRadialGradient(x, y, 0, x, y, rad); gr.addColorStop(0, hexA(col, a)); gr.addColorStop(1, hexA(col, 0));
|
| 107 |
+
g.fillStyle = gr; g.fillRect(x - rad, y - rad, rad * 2, rad * 2);
|
| 108 |
+
}
|
| 109 |
+
for (let i = 0; i < 5000; i++) {
|
| 110 |
+
const x = (r() - .5) * L * 1.1, y = (r() + r() + r() - 1.5) * H * .14;
|
| 111 |
+
g.fillStyle = `rgba(230,235,255,${.15 + r() * .5})`; const s = r() * 1.3 + .3; g.fillRect(x, y, s, s);
|
| 112 |
+
}
|
| 113 |
+
// 暗尘带
|
| 114 |
+
for (let i = 0; i < 40; i++) {
|
| 115 |
+
const x = (r() - .5) * L, y = (r() - .5) * H * .05, rad = (40 + r() * 120) * Math.max(W, H) / 1920;
|
| 116 |
+
const gr = g.createRadialGradient(x, y, 0, x, y, rad); gr.addColorStop(0, 'rgba(4,4,14,.35)'); gr.addColorStop(1, 'rgba(4,4,14,0)');
|
| 117 |
+
g.fillStyle = gr; g.fillRect(x - rad, y - rad, rad * 2, rad * 2);
|
| 118 |
+
}
|
| 119 |
+
mwCache.set(key, c); return c;
|
| 120 |
+
}
|
| 121 |
+
function drawBackground(ctx, t, W, H, cam = { x: 0, y: 0 }, tint = 0) {
|
| 122 |
+
const g = ctx.createLinearGradient(0, 0, 0, H);
|
| 123 |
+
g.addColorStop(0, '#03040d'); g.addColorStop(.55, '#070a1f'); g.addColorStop(1, tint ? '#120a24' : '#0b1030');
|
| 124 |
+
ctx.fillStyle = g; ctx.fillRect(0, 0, W, H);
|
| 125 |
+
const mw = milkyWay(W, H);
|
| 126 |
+
ctx.save(); ctx.globalAlpha = .85; ctx.globalCompositeOperation = 'lighter';
|
| 127 |
+
const ox = W * .2 + Math.sin(cam.x / W * .9) * W * .17, oy = H * .15 + Math.sin(cam.y / H) * H * .12;
|
| 128 |
+
ctx.drawImage(mw, -ox, -oy);
|
| 129 |
+
ctx.restore();
|
| 130 |
+
const u = Math.min(W, H) / 1080;
|
| 131 |
+
ctx.save(); ctx.globalCompositeOperation = 'lighter';
|
| 132 |
+
for (const s of BG_STARS) {
|
| 133 |
+
let x = (s.x * W - cam.x * s.d) % W; if (x < 0) x += W;
|
| 134 |
+
let y = (s.y * H - cam.y * s.d) % H; if (y < 0) y += H;
|
| 135 |
+
const tw = .55 + .45 * Math.sin(t * s.f + s.ph);
|
| 136 |
+
const r = (.5 + s.m * 2.2) * u;
|
| 137 |
+
ctx.globalAlpha = (.25 + s.m * .75) * tw;
|
| 138 |
+
ctx.fillStyle = s.c;
|
| 139 |
+
if (s.m > .55) { drawStar(ctx, x, y, r * .9, s.c, .5 * tw); } else { ctx.fillRect(x - r / 2, y - r / 2, r, r); }
|
| 140 |
+
}
|
| 141 |
+
ctx.restore();
|
| 142 |
+
}
|
| 143 |
+
function vignette(ctx, W, H, k = .65) {
|
| 144 |
+
const g = ctx.createRadialGradient(W / 2, H / 2, Math.min(W, H) * .3, W / 2, H / 2, Math.hypot(W, H) * .62);
|
| 145 |
+
g.addColorStop(0, 'rgba(0,0,0,0)'); g.addColorStop(1, `rgba(0,0,0,${k})`);
|
| 146 |
+
ctx.fillStyle = g; ctx.fillRect(0, 0, W, H);
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
/* ---------------- 文字工具 ---------------- */
|
| 150 |
+
function text(ctx, s, x, y, size, color = '#fff', align = 'center', alpha = 1, font = SANS, weight = 500, glow = 0) {
|
| 151 |
+
if (alpha <= .003) return;
|
| 152 |
+
ctx.save(); ctx.globalAlpha = alpha; ctx.font = `${weight} ${size}px ${font}`; ctx.textAlign = align; ctx.textBaseline = 'middle';
|
| 153 |
+
if (glow) { ctx.shadowColor = typeof glow === 'string' ? glow : color; ctx.shadowBlur = size * .6; }
|
| 154 |
+
ctx.fillStyle = color; ctx.fillText(s, x, y); ctx.restore();
|
| 155 |
+
}
|
| 156 |
+
function goldGrad(ctx, x0, y0, x1, y1) {
|
| 157 |
+
const g = ctx.createLinearGradient(x0, y0, x1, y1);
|
| 158 |
+
g.addColorStop(0, '#fff3cf'); g.addColorStop(.45, '#f5c66a'); g.addColorStop(.7, '#ffe6a6'); g.addColorStop(1, '#d99a3e'); return g;
|
| 159 |
+
}
|
| 160 |
+
function chip(ctx, s, x, y, size, alpha, fg = '#ffe6b0', bg = 'rgba(20,18,40,.72)', stroke = 'rgba(255,214,140,.55)') {
|
| 161 |
+
if (alpha <= .003) return;
|
| 162 |
+
ctx.save(); ctx.globalAlpha = alpha; ctx.font = `600 ${size}px ${SANS}`;
|
| 163 |
+
const w = ctx.measureText(s).width + size * 1.4, h = size * 1.9;
|
| 164 |
+
rrect(ctx, x - w / 2, y - h / 2, w, h, h / 2); ctx.fillStyle = bg; ctx.fill(); ctx.strokeStyle = stroke; ctx.lineWidth = Math.max(1, size * .06); ctx.stroke();
|
| 165 |
+
ctx.fillStyle = fg; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(s, x, y + size * .04); ctx.restore();
|
| 166 |
+
}
|
| 167 |
+
function rrect(ctx, x, y, w, h, r) { ctx.beginPath(); ctx.moveTo(x + r, y); ctx.arcTo(x + w, y, x + w, y + h, r); ctx.arcTo(x + w, y + h, x, y + h, r); ctx.arcTo(x, y + h, x, y, r); ctx.arcTo(x, y, x + w, y, r); ctx.closePath(); }
|
| 168 |
+
function wrap(ctx, s, maxW) {
|
| 169 |
+
const out = []; let line = '';
|
| 170 |
+
for (const ch of Array.from(s)) {
|
| 171 |
+
const test = line + ch;
|
| 172 |
+
if (ctx.measureText(test).width > maxW && line) {
|
| 173 |
+
if (',。、;:!?”)—'.includes(ch)) { line = test; continue; }
|
| 174 |
+
out.push(line); line = ch;
|
| 175 |
+
} else line = test;
|
| 176 |
+
}
|
| 177 |
+
if (line) out.push(line);
|
| 178 |
+
return out;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
/* ---------------- 星座视图 ---------------- */
|
| 182 |
+
// view: {cx, cy (度), s (像素/度), ox, oy (屏幕中心)}
|
| 183 |
+
function P(view, k) { const [x, y] = sky(k); return [view.ox + (x - view.cx) * view.s, view.oy + (y - view.cy) * view.s]; }
|
| 184 |
+
function drawLines(ctx, view, lines, prog, color, width, alpha) {
|
| 185 |
+
if (alpha <= 0) return;
|
| 186 |
+
const N = lines.length;
|
| 187 |
+
ctx.save(); ctx.globalAlpha = alpha; ctx.strokeStyle = color; ctx.lineWidth = width; ctx.lineCap = 'round';
|
| 188 |
+
ctx.shadowColor = color; ctx.shadowBlur = width * 4;
|
| 189 |
+
lines.forEach(([a, b], i) => {
|
| 190 |
+
const p = clamp(prog * N - i); if (p <= 0) return;
|
| 191 |
+
const [x0, y0] = P(view, a), [x1, y1] = P(view, b);
|
| 192 |
+
ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(lerp(x0, x1, p), lerp(y0, y1, p)); ctx.stroke();
|
| 193 |
+
});
|
| 194 |
+
ctx.restore();
|
| 195 |
+
}
|
| 196 |
+
function drawStarSet(ctx, view, keys, t, u, alpha = 1, boost = {}) {
|
| 197 |
+
for (const k of keys) {
|
| 198 |
+
const s = STARS[k]; const [x, y] = P(view, k);
|
| 199 |
+
const r = Math.max(1.2, (4.6 - s[2]) * 2.1) * u * (view.s / 22) ** .35 * (boost[k] || 1);
|
| 200 |
+
const tw = .85 + .15 * Math.sin(t * 2.3 + x * .01);
|
| 201 |
+
drawStar(ctx, x, y, r, s[3], alpha * tw, s[2] < 2 ? .9 : 0);
|
| 202 |
+
}
|
| 203 |
+
}
|
| 204 |
+
function starLabel(ctx, view, k, u, alpha, dx = 0, dy = -1, size = 30) {
|
| 205 |
+
const s = STARS[k]; const [x, y] = P(view, k);
|
| 206 |
+
text(ctx, s[4], x + dx * u, y + dy * 50 * u, size * u, '#fff4dc', 'center', alpha, SERIF, 700, 'rgba(255,200,120,.8)');
|
| 207 |
+
if (s[5]) text(ctx, s[5], x + dx * u, y + dy * 50 * u + (dy < 0 ? -size * .95 : size * .95) * u, size * .55 * u, '#c9d6ff', 'center', alpha * .85, SANS, 400);
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
/* ---------------- 人物线稿(神话) ---------------- */
|
| 211 |
+
function figure(ctx, x, y, h, o) {
|
| 212 |
+
const { color = '#ffd88a', alpha = 1, lean = 0, kneel = 0, fall = 0, arm = 0, glow = 1, fill = 'rgba(255,210,140,.10)' } = o;
|
| 213 |
+
if (alpha <= .003) return;
|
| 214 |
+
ctx.save(); ctx.globalAlpha = alpha; ctx.translate(x, y); ctx.rotate(fall * 1.35 + lean * .12);
|
| 215 |
+
ctx.strokeStyle = color; ctx.fillStyle = fill; ctx.lineWidth = h * .028; ctx.lineCap = 'round'; ctx.lineJoin = 'round';
|
| 216 |
+
ctx.shadowColor = color; ctx.shadowBlur = h * .06 * glow;
|
| 217 |
+
const kn = kneel;
|
| 218 |
+
const hip = [0, -h * (.42 - kn * .14)], neck = [0, -h * (.76 - kn * .14)], head = [0, -h * (.87 - kn * .14)];
|
| 219 |
+
// 披风(随时间轻摆,用 x 坐标做相位)
|
| 220 |
+
const sway = Math.sin((x + y) * .01 + (o.time || 0) * 1.6) * h * .03;
|
| 221 |
+
ctx.save(); ctx.globalAlpha *= .55; ctx.beginPath(); ctx.moveTo(neck[0] - h * .07, neck[1] + h * .03);
|
| 222 |
+
ctx.quadraticCurveTo(-h * .22 + sway, hip[1], -h * .2 + sway * 1.6, hip[1] + h * .26);
|
| 223 |
+
ctx.lineTo(-h * .06, hip[1] + h * .14); ctx.closePath(); ctx.fillStyle = hexA(color.length === 7 ? color : '#ffd88a', .22); ctx.fill(); ctx.stroke(); ctx.restore();
|
| 224 |
+
// 长袍
|
| 225 |
+
ctx.beginPath(); ctx.moveTo(neck[0] - h * .07, neck[1] + h * .02); ctx.lineTo(neck[0] + h * .07, neck[1] + h * .02);
|
| 226 |
+
ctx.lineTo(hip[0] + h * .13, hip[1] + h * .12); ctx.lineTo(hip[0] - h * .13, hip[1] + h * .12); ctx.closePath(); ctx.fill(); ctx.stroke();
|
| 227 |
+
// 腿
|
| 228 |
+
ctx.beginPath();
|
| 229 |
+
if (kn > .01) {
|
| 230 |
+
ctx.moveTo(hip[0] - h * .05, hip[1] + h * .1); ctx.lineTo(-h * .16, -h * .02 * (1 - kn)); ctx.lineTo(-h * .02, 0);
|
| 231 |
+
ctx.moveTo(hip[0] + h * .05, hip[1] + h * .1); ctx.lineTo(h * .14, -h * .18 * kn); ctx.lineTo(h * .16, 0);
|
| 232 |
+
} else {
|
| 233 |
+
ctx.moveTo(hip[0] - h * .05, hip[1] + h * .1); ctx.lineTo(-h * .1, 0);
|
| 234 |
+
ctx.moveTo(hip[0] + h * .05, hip[1] + h * .1); ctx.lineTo(h * .1, 0);
|
| 235 |
+
}
|
| 236 |
+
ctx.stroke();
|
| 237 |
+
// 手臂 arm: 0 垂下, 1 上举
|
| 238 |
+
ctx.beginPath();
|
| 239 |
+
const sh = [neck[0], neck[1] + h * .04];
|
| 240 |
+
const a1 = lerp(.35, -1.2, arm), a2 = lerp(Math.PI - .35, Math.PI + 1.2, arm * .2);
|
| 241 |
+
ctx.moveTo(sh[0] + h * .06, sh[1]); ctx.lineTo(sh[0] + h * .06 + Math.cos(a1) * h * .3, sh[1] + Math.sin(a1) * h * .3 + h * .12 * (1 - arm));
|
| 242 |
+
ctx.moveTo(sh[0] - h * .06, sh[1]); ctx.lineTo(sh[0] - h * .06 + Math.cos(a2) * h * .3, sh[1] + Math.sin(a2) * h * .3 + h * .12);
|
| 243 |
+
ctx.stroke();
|
| 244 |
+
// 头
|
| 245 |
+
ctx.beginPath(); ctx.arc(head[0], head[1], h * .075, 0, TAU); ctx.fill(); ctx.stroke();
|
| 246 |
+
ctx.restore();
|
| 247 |
+
}
|
| 248 |
+
function seal(ctx, ch, x, y, r, alpha, col = '#e0533f') {
|
| 249 |
+
if (alpha <= .003) return;
|
| 250 |
+
ctx.save(); ctx.globalAlpha = alpha; ctx.translate(x, y); ctx.rotate(-.08);
|
| 251 |
+
rrect(ctx, -r, -r, r * 2, r * 2, r * .25); ctx.fillStyle = hexA(col, .9); ctx.fill();
|
| 252 |
+
ctx.strokeStyle = 'rgba(255,240,220,.85)'; ctx.lineWidth = r * .07; rrect(ctx, -r * .82, -r * .82, r * 1.64, r * 1.64, r * .18); ctx.stroke();
|
| 253 |
+
ctx.fillStyle = '#fff6ea'; ctx.font = `700 ${r * 1.15}px ${SERIF}`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(ch, 0, r * .06);
|
| 254 |
+
ctx.restore();
|
| 255 |
+
}
|
| 256 |
+
function bolt(ctx, x, y, s, alpha) {
|
| 257 |
+
if (alpha <= .003) return;
|
| 258 |
+
ctx.save(); ctx.globalAlpha = alpha; ctx.translate(x, y); ctx.scale(s, s);
|
| 259 |
+
ctx.beginPath(); ctx.moveTo(10, -60); ctx.lineTo(-22, 6); ctx.lineTo(0, 6); ctx.lineTo(-12, 60); ctx.lineTo(26, -10); ctx.lineTo(4, -10); ctx.lineTo(20, -60); ctx.closePath();
|
| 260 |
+
ctx.fillStyle = '#fff2b0'; ctx.shadowColor = '#ffd75e'; ctx.shadowBlur = 40; ctx.fill(); ctx.restore();
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
/* ---------------- 布局 ---------------- */
|
| 264 |
+
function layout(W, H) { const u = Math.min(W, H) / 1080; const port = H > W; return { u, port }; }
|
| 265 |
+
|
| 266 |
+
/* ================= 场景 ================= */
|
| 267 |
+
const SC = {};
|
| 268 |
+
|
| 269 |
+
// 1. 序章
|
| 270 |
+
SC.intro = (ctx, lt, D, W, H, L, t) => {
|
| 271 |
+
const { u, port } = layout(W, H);
|
| 272 |
+
const view = { cx: 0, cy: -4, s: lerp(16, 22, easeIO(clamp(lt / D))) * u * (port ? .95 : 1), ox: W / 2, oy: H * (port ? .40 : .46) };
|
| 273 |
+
// 两颗星先亮
|
| 274 |
+
const a0 = sstep(.6, 3.2, lt);
|
| 275 |
+
const others = GEM_KEYS.filter(k => k !== 'castor' && k !== 'pollux');
|
| 276 |
+
drawStarSet(ctx, view, others, t, u, sstep(L[1] - 1, L[1] + 1.5, lt) * .9);
|
| 277 |
+
drawLines(ctx, view, GEM_LINES, sstep(L[1] - .5, L[1] + 4, lt), 'rgba(160,200,255,.55)', 2 * u, .8);
|
| 278 |
+
drawStarSet(ctx, view, ['castor', 'pollux'], t, u, a0, { castor: 1.6 + .2 * Math.sin(lt * 2), pollux: 1.7 + .2 * Math.sin(lt * 2 + 1) });
|
| 279 |
+
// 连结光弧
|
| 280 |
+
const [cx1, cy1] = P(view, 'castor'), [cx2, cy2] = P(view, 'pollux');
|
| 281 |
+
const arcA = sstep(2.5, 5, lt) * (1 - sstep(L[1] + 1, L[1] + 3, lt));
|
| 282 |
+
if (arcA > 0) {
|
| 283 |
+
ctx.save(); ctx.globalAlpha = arcA; ctx.strokeStyle = 'rgba(255,220,160,.8)'; ctx.lineWidth = 2 * u; ctx.setLineDash([4 * u, 10 * u]); ctx.lineDashOffset = -lt * 30;
|
| 284 |
+
ctx.beginPath(); ctx.moveTo(cx1, cy1); ctx.quadraticCurveTo((cx1 + cx2) / 2, (cy1 + cy2) / 2 - 80 * u, cx2, cy2); ctx.stroke(); ctx.restore();
|
| 285 |
+
}
|
| 286 |
+
// 雪原地平线
|
| 287 |
+
const hy = H * (port ? .72 : .80);
|
| 288 |
+
ctx.save(); const hg = ctx.createLinearGradient(0, hy - 60 * u, 0, H); hg.addColorStop(0, 'rgba(20,26,60,0)'); hg.addColorStop(.3, '#0d1230'); hg.addColorStop(1, '#05060f');
|
| 289 |
+
ctx.fillStyle = hg; ctx.beginPath(); ctx.moveTo(0, H);
|
| 290 |
+
for (let x = 0; x <= W; x += 20 * u) ctx.lineTo(x, hy + Math.sin(x / W * 5.2 + 1) * 26 * u + Math.sin(x / W * 13) * 10 * u);
|
| 291 |
+
ctx.lineTo(W, H); ctx.fill();
|
| 292 |
+
ctx.fillStyle = '#04050c'; ctx.beginPath(); ctx.moveTo(0, H);
|
| 293 |
+
for (let x = 0; x <= W; x += 16 * u) ctx.lineTo(x, hy + 40 * u + Math.sin(x / W * 3 + 2) * 30 * u + Math.sin(x / W * 17) * 6 * u);
|
| 294 |
+
ctx.lineTo(W, H); ctx.fill();
|
| 295 |
+
// 小人观星
|
| 296 |
+
figure(ctx, W * (port ? .74 : .78), hy + 52 * u, 70 * u, { color: '#9fb4ff', alpha: .9, arm: .6, glow: .4, fill: 'rgba(20,24,50,1)' });
|
| 297 |
+
ctx.restore();
|
| 298 |
+
// 标题
|
| 299 |
+
const ta = sstep(L[1] + .6, L[1] + 2.2, lt);
|
| 300 |
+
if (ta > 0) {
|
| 301 |
+
const ty = H * (port ? .17 : .17), size = (port ? 150 : 140) * u;
|
| 302 |
+
ctx.save(); ctx.globalAlpha = ta; ctx.font = `900 ${size}px ${SERIF}`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
| 303 |
+
ctx.shadowColor = 'rgba(255,190,90,.7)'; ctx.shadowBlur = 40 * u;
|
| 304 |
+
ctx.fillStyle = goldGrad(ctx, W / 2 - size * 1.5, ty - size / 2, W / 2 + size * 1.5, ty + size / 2);
|
| 305 |
+
const sp = lerp(.6, .18, easeOut(ta));
|
| 306 |
+
const chars = ['双', '子', '座']; chars.forEach((c, i) => ctx.fillText(c, W / 2 + (i - 1) * size * (1 + sp), ty));
|
| 307 |
+
ctx.restore();
|
| 308 |
+
text(ctx, 'G E M I N I · 两 颗 星 的 约 定', W / 2, ty + size * .85, 30 * u, '#cdd8ff', 'center', ta * .9, SANS, 400);
|
| 309 |
+
}
|
| 310 |
+
};
|
| 311 |
+
|
| 312 |
+
// 2. 怎样找到它
|
| 313 |
+
SC.find = (ctx, lt, D, W, H, L, t) => {
|
| 314 |
+
const { u, port } = layout(W, H);
|
| 315 |
+
const k1 = easeIO(sstep(L[0] + 1, L[1] + 1.5, lt)), k2 = easeIO(sstep(L[2] - 1.5, L[2] + 1.2, lt));
|
| 316 |
+
const s0 = 28 * u, s1 = (port ? 24 : 19) * u, s2 = (port ? 34 : 28) * u;
|
| 317 |
+
const view = {
|
| 318 |
+
cx: lerp(lerp(19.5, 7, k1), 0, k2), cy: lerp(lerp(21, 9, k1), -1.5, k2),
|
| 319 |
+
s: lerp(lerp(s0, s1, k1), s2, k2), ox: W / 2, oy: H * (port ? .40 : .47),
|
| 320 |
+
};
|
| 321 |
+
const ori = ['betel', 'rigel', 'bella', 'saiph', 'alnitak', 'alnilam', 'mintaka', 'meissa'];
|
| 322 |
+
drawLines(ctx, view, ORI_LINES, sstep(.2, 3, lt), 'rgba(150,180,255,.5)', 2 * u, .7 * (1 - k2 * .7));
|
| 323 |
+
drawStarSet(ctx, view, ori, t, u, 1 - k2 * .4);
|
| 324 |
+
drawStarSet(ctx, view, ['procyon'], t, u, .9);
|
| 325 |
+
// 腰带高光
|
| 326 |
+
const belt = sstep(.8, 2, lt) * (1 - sstep(L[1] + 1, L[1] + 3, lt));
|
| 327 |
+
['alnitak', 'alnilam', 'mintaka'].forEach((k, i) => { const [x, y] = P(view, k); const pulse = .5 + .5 * Math.sin(lt * 5 - i); drawStar(ctx, x, y, 14 * u * (1 + pulse * .3), '#9fc2ff', belt * .7); });
|
| 328 |
+
if (belt > 0) { const [x, y] = P(view, 'alnilam'); chip(ctx, '猎户座腰带', x, y + 60 * u, 26 * u, belt); }
|
| 329 |
+
text(ctx, '猎户座', ...P(view, 'saiph').map((v, i) => v + (i ? 70 * u : 0)), 30 * u, '#b8c8ff', 'center', sstep(.5, 2, lt) * (1 - k2), SERIF, 600);
|
| 330 |
+
// 指向箭头:参宿七 → 参宿四 → 双子
|
| 331 |
+
const arr = sstep(L[1], L[1] + 3, lt) * (1 - sstep(L[3], L[3] + 1.5, lt));
|
| 332 |
+
if (arr > 0) {
|
| 333 |
+
const [x0, y0] = P(view, 'rigel'), [xm, ym] = P(view, 'betel'), [x1, y1] = P(view, 'pollux');
|
| 334 |
+
const pr = easeOut(sstep(L[1], L[1] + 3, lt));
|
| 335 |
+
ctx.save(); ctx.globalAlpha = arr; ctx.strokeStyle = 'rgba(255,214,140,.9)'; ctx.lineWidth = 3 * u; ctx.setLineDash([12 * u, 10 * u]); ctx.lineDashOffset = -lt * 40;
|
| 336 |
+
ctx.shadowColor = '#ffcf7a'; ctx.shadowBlur = 12 * u;
|
| 337 |
+
const ex = lerp(x0, x1, pr), ey = lerp(y0, y1, pr);
|
| 338 |
+
ctx.beginPath(); ctx.moveTo(x0, y0); ctx.quadraticCurveTo(xm, ym, ex, ey); ctx.stroke(); ctx.setLineDash([]);
|
| 339 |
+
const ang = Math.atan2(y1 - ym, x1 - xm);
|
| 340 |
+
ctx.translate(ex, ey); ctx.rotate(ang); ctx.fillStyle = '#ffd98e'; ctx.beginPath(); ctx.moveTo(14 * u, 0); ctx.lineTo(-10 * u, -9 * u); ctx.lineTo(-10 * u, 9 * u); ctx.fill();
|
| 341 |
+
ctx.restore();
|
| 342 |
+
chip(ctx, '向东北方 ↖', (xm + x1) / 2, (ym + y1) / 2 + 50 * u, 26 * u, arr * sstep(L[1] + 1.5, L[1] + 2.5, lt));
|
| 343 |
+
}
|
| 344 |
+
// 双子座
|
| 345 |
+
const gA = sstep(L[1] + 1, L[2], lt);
|
| 346 |
+
drawStarSet(ctx, view, GEM_KEYS, t, u, gA, { castor: 1.25, pollux: 1.25 });
|
| 347 |
+
starLabel(ctx, view, 'castor', u, sstep(L[2] + .8, L[2] + 2, lt), 0, -1.1);
|
| 348 |
+
starLabel(ctx, view, 'pollux', u, sstep(L[2] + 1.4, L[2] + 2.6, lt), -110, 0);
|
| 349 |
+
// 头部光环
|
| 350 |
+
const halo = sstep(L[2] + .5, L[2] + 2, lt);
|
| 351 |
+
['castor', 'pollux'].forEach((k, i) => { const [x, y] = P(view, k); ctx.save(); ctx.globalAlpha = halo * (.5 + .3 * Math.sin(lt * 3 + i)); ctx.strokeStyle = i ? '#ffc985' : '#cfe0ff'; ctx.lineWidth = 2 * u; ctx.beginPath(); ctx.arc(x, y, (30 + 6 * Math.sin(lt * 2 + i)) * u, 0, TAU); ctx.stroke(); ctx.restore(); });
|
| 352 |
+
// 连线描绘 + 身形光晕
|
| 353 |
+
const lp = sstep(L[3] - .3, L[3] + 4.5, lt);
|
| 354 |
+
drawLines(ctx, view, GEM_LINES, lp, 'rgba(255,224,170,.95)', 3 * u, 1);
|
| 355 |
+
const fig = sstep(L[3] + 3, L[3] + 5, lt);
|
| 356 |
+
if (fig > 0) {
|
| 357 |
+
drawLines(ctx, view, GEM_LINES, 1, 'rgba(255,190,110,.25)', 26 * u, fig * .55);
|
| 358 |
+
const [ax, ay] = P(view, 'iota'), [bx, by] = P(view, 'upsilon');
|
| 359 |
+
drawStar(ctx, (ax + bx) / 2, (ay + by) / 2, 18 * u, '#ffd98e', fig * (.6 + .4 * Math.sin(lt * 4)));
|
| 360 |
+
chip(ctx, '手拉着手', (ax + bx) / 2 + 20 * u, (ay + by) / 2 + 60 * u, 24 * u, fig);
|
| 361 |
+
}
|
| 362 |
+
// 银河岸
|
| 363 |
+
text(ctx, '银 河', W * (port ? .82 : .86), H * (port ? .14 : .2), 26 * u, '#aab7ff', 'center', sstep(L[3], L[3] + 2, lt) * .7, SERIF, 400);
|
| 364 |
+
};
|
| 365 |
+
|
| 366 |
+
// 3. 神话
|
| 367 |
+
SC.myth = (ctx, lt, D, W, H, L, t) => {
|
| 368 |
+
const { u, port } = layout(W, H);
|
| 369 |
+
// 暖色神殿光
|
| 370 |
+
const warm = sstep(0, 1.5, lt) * (1 - sstep(L[4] + 2, L[4] + 5, lt));
|
| 371 |
+
ctx.save(); ctx.globalAlpha = warm * .8; const rg = ctx.createRadialGradient(W / 2, H * .55, 0, W / 2, H * .55, Math.max(W, H) * .6);
|
| 372 |
+
rg.addColorStop(0, 'rgba(120,70,40,.55)'); rg.addColorStop(1, 'rgba(10,6,20,0)'); ctx.fillStyle = rg; ctx.fillRect(0, 0, W, H); ctx.restore();
|
| 373 |
+
// 柱廊
|
| 374 |
+
const colA = warm * .55, base = H * (port ? .62 : .64);
|
| 375 |
+
for (let i = 0; i < 6; i++) {
|
| 376 |
+
const x = W * (i + .5) / 6, w = 34 * u;
|
| 377 |
+
ctx.save(); ctx.globalAlpha = colA * (i === 0 || i === 5 ? .9 : .4); const cg = ctx.createLinearGradient(x - w, 0, x + w, 0);
|
| 378 |
+
cg.addColorStop(0, 'rgba(60,40,30,.0)'); cg.addColorStop(.5, 'rgba(210,170,120,.35)'); cg.addColorStop(1, 'rgba(60,40,30,0)');
|
| 379 |
+
ctx.fillStyle = cg; ctx.fillRect(x - w, base - H * .5, w * 2, H * .5); ctx.restore();
|
| 380 |
+
}
|
| 381 |
+
ctx.save(); ctx.globalAlpha = warm * .6; ctx.fillStyle = 'rgba(200,160,110,.35)'; ctx.fillRect(0, base, W, 3 * u); ctx.restore();
|
| 382 |
+
|
| 383 |
+
const h = (port ? 380 : 360) * u, xa = W * (port ? .3 : .37), xb = W * (port ? .7 : .63);
|
| 384 |
+
const appear = sstep(.3, 1.8, lt);
|
| 385 |
+
const die = sstep(L[2] + .4, L[2] + 1.6, lt);
|
| 386 |
+
const plea = sstep(L[3], L[3] + 1, lt);
|
| 387 |
+
const rise = sstep(L[4] + 4.5, L[4] + 8.5, lt);
|
| 388 |
+
const castorCol = die > .5 && rise < .1 ? '#8c9bc0' : '#cfe0ff';
|
| 389 |
+
// 不朽光晕(弟弟)
|
| 390 |
+
const aura = sstep(L[1] + .5, L[1] + 2, lt) * (1 - rise);
|
| 391 |
+
if (aura > 0) drawStar(ctx, xb, base - h * .5, 70 * u, '#ffcf7a', aura * (.55 + .2 * Math.sin(lt * 3)));
|
| 392 |
+
// 心跳线(哥哥 · 凡人)
|
| 393 |
+
const mortal = sstep(L[1] + 1, L[1] + 2.4, lt) * (1 - sstep(L[2], L[2] + .6, lt));
|
| 394 |
+
if (mortal > 0) {
|
| 395 |
+
ctx.save(); ctx.globalAlpha = mortal; ctx.strokeStyle = '#8fd0ff'; ctx.lineWidth = 3 * u; ctx.shadowColor = '#8fd0ff'; ctx.shadowBlur = 10 * u; ctx.beginPath();
|
| 396 |
+
const y0 = base - h * 1.18; for (let i = 0; i <= 60; i++) { const x = xa - 120 * u + i * 4 * u; const ph = (i / 60 * 3 - lt * 1.2) % 1; const y = y0 - (Math.abs(ph - .5) < .04 ? 40 * u * Math.sin((ph - .46) * 80) : 0); i ? ctx.lineTo(x, y) : ctx.moveTo(x, y); }
|
| 397 |
+
ctx.stroke(); ctx.restore();
|
| 398 |
+
text(ctx, '凡人', xa, base - h * 1.32, 30 * u, '#bfe2ff', 'center', mortal, SERIF, 700);
|
| 399 |
+
}
|
| 400 |
+
text(ctx, '不朽 · 宙斯之子', xb, base - h * 1.32, 30 * u, '#ffe1a0', 'center', aura * sstep(L[1] + 1.5, L[1] + 2.5, lt) * (1 - sstep(L[2] + 2, L[2] + 3, lt)), SERIF, 700, 'rgba(255,190,90,.7)');
|
| 401 |
+
// 人物
|
| 402 |
+
const ry = -rise * H * .35;
|
| 403 |
+
figure(ctx, xa + die * 40 * u, base + ry, h, { time: t, color: castorCol, alpha: appear * (1 - rise), fall: die * (1 - plea * .0), glow: 1 - die * .6, fill: 'rgba(170,200,255,.10)' });
|
| 404 |
+
figure(ctx, xb - plea * 30 * u, base + ry, h, { time: t, color: '#ffd88a', alpha: appear * (1 - rise), kneel: plea * (1 - sstep(L[4], L[4] + 1.5, lt)), arm: plea * .9 });
|
| 405 |
+
// 名字与印章
|
| 406 |
+
const nameA = appear * (1 - sstep(L[2], L[2] + .8, lt));
|
| 407 |
+
text(ctx, '卡斯托尔', xa, base + 56 * u, 40 * u, '#e3ecff', 'center', nameA, SERIF, 800);
|
| 408 |
+
text(ctx, '波吕克斯', xb, base + 56 * u, 40 * u, '#ffe1a0', 'center', nameA, SERIF, 800);
|
| 409 |
+
seal(ctx, '马', xa - h * .42, base - h * .7, 34 * u, sstep(L[0] + 2, L[0] + 3, lt) * nameA);
|
| 410 |
+
seal(ctx, '拳', xb + h * .42, base - h * .7, 34 * u, sstep(L[0] + 4, L[0] + 5, lt) * nameA, '#c9852b');
|
| 411 |
+
text(ctx, '哥哥 · 驯马师', xa, base + 104 * u, 26 * u, '#aebfe6', 'center', nameA * sstep(L[0] + 2, L[0] + 3, lt), SANS, 400);
|
| 412 |
+
text(ctx, '弟弟 · 拳击手', xb, base + 104 * u, 26 * u, '#e9cf9c', 'center', nameA * sstep(L[0] + 4, L[0] + 5, lt), SANS, 400);
|
| 413 |
+
// 箭矢
|
| 414 |
+
const arrowK = sstep(L[2] - .2, L[2] + .45, lt);
|
| 415 |
+
if (arrowK > 0 && arrowK < 1) {
|
| 416 |
+
const sx = -50 * u, sy = base - h * .9, ex = xa, ey = base - h * .55, x = lerp(sx, ex, arrowK), y = lerp(sy, ey, arrowK);
|
| 417 |
+
ctx.save(); ctx.strokeStyle = '#ffeed0'; ctx.lineWidth = 4 * u; ctx.shadowColor = '#ff8d6a'; ctx.shadowBlur = 20 * u; ctx.beginPath(); ctx.moveTo(x - 90 * u, y - 25 * u); ctx.lineTo(x, y); ctx.stroke(); ctx.restore();
|
| 418 |
+
}
|
| 419 |
+
const flash = sstep(L[2] + .4, L[2] + .5, lt) * (1 - sstep(L[2] + .5, L[2] + 1.6, lt));
|
| 420 |
+
if (flash > 0) { ctx.save(); ctx.globalAlpha = flash * .45; ctx.fillStyle = '#b3242c'; ctx.fillRect(0, 0, W, H); ctx.restore(); }
|
| 421 |
+
// 宙斯闪电
|
| 422 |
+
const zA = sstep(L[3] + 2, L[3] + 3, lt) * (1 - sstep(L[4] + .8, L[4] + 1.8, lt));
|
| 423 |
+
const zf = zA * (.7 + .3 * Math.sin(lt * 14) * Math.sin(lt * 5));
|
| 424 |
+
bolt(ctx, W / 2, H * (port ? .12 : .14), 1.4 * u, zf);
|
| 425 |
+
text(ctx, '宙 斯', W / 2, H * (port ? .12 : .14) + 110 * u, 30 * u, '#fff2b0', 'center', zA, SERIF, 700);
|
| 426 |
+
// 不朽之光流向哥哥
|
| 427 |
+
const flow = sstep(L[3] + 2.5, L[3] + 3.5, lt) * (1 - sstep(L[4] + 4, L[4] + 5, lt));
|
| 428 |
+
if (flow > 0) {
|
| 429 |
+
const r = rng(9);
|
| 430 |
+
for (let i = 0; i < 60; i++) {
|
| 431 |
+
const ph = (r() + lt * (.25 + r() * .2)) % 1; const x = lerp(xb, xa + 40 * u, ph), y = lerp(base - h * .55, base - h * .2, ph) - Math.sin(ph * Math.PI) * 120 * u + (r() - .5) * 30 * u;
|
| 432 |
+
drawStar(ctx, x, y, (2 + r() * 3) * u, '#ffd98e', flow * Math.sin(ph * Math.PI));
|
| 433 |
+
}
|
| 434 |
+
text(ctx, '把我的不朽,分一半给他', W / 2, base - h * 1.15, 34 * u, '#fff0c8', 'center', sstep(L[3] + 2.6, L[3] + 3.6, lt) * (1 - sstep(L[4], L[4] + 1, lt)), SERIF, 700, 'rgba(255,190,90,.7)');
|
| 435 |
+
}
|
| 436 |
+
// 天上 / 冥界 交替
|
| 437 |
+
const alt = sstep(L[4] + 1.5, L[4] + 2.5, lt) * (1 - rise);
|
| 438 |
+
if (alt > 0) {
|
| 439 |
+
const ph = (lt - L[4] - 1.5) * .9;
|
| 440 |
+
const cx = W / 2, cy = H * (port ? .3 : .33), R = 120 * u;
|
| 441 |
+
ctx.save(); ctx.globalAlpha = alt;
|
| 442 |
+
ctx.beginPath(); ctx.arc(cx, cy, R, Math.PI, 0); ctx.fillStyle = 'rgba(255,220,150,.18)'; ctx.fill();
|
| 443 |
+
ctx.beginPath(); ctx.arc(cx, cy, R, 0, Math.PI); ctx.fillStyle = 'rgba(90,70,160,.25)'; ctx.fill();
|
| 444 |
+
ctx.strokeStyle = 'rgba(255,230,190,.5)'; ctx.lineWidth = 2 * u; ctx.beginPath(); ctx.arc(cx, cy, R, 0, TAU); ctx.stroke();
|
| 445 |
+
ctx.restore();
|
| 446 |
+
for (let i = 0; i < 2; i++) { const a = ph * Math.PI + i * Math.PI; drawStar(ctx, cx + Math.cos(a) * R, cy + Math.sin(a) * R, 10 * u, i ? '#ffc985' : '#e3ecff', alt); }
|
| 447 |
+
text(ctx, '天上', cx, cy - R - 26 * u, 24 * u, '#ffe6b0', 'center', alt, SERIF, 600);
|
| 448 |
+
text(ctx, '冥界', cx, cy + R + 26 * u, 24 * u, '#c3b5ff', 'center', alt, SERIF, 600);
|
| 449 |
+
}
|
| 450 |
+
// 升空化星
|
| 451 |
+
if (rise > 0) {
|
| 452 |
+
const vx = W / 2, vy = H * (port ? .36 : .42);
|
| 453 |
+
const view = { cx: 0, cy: -1.5, s: (port ? 24 : 22) * u, ox: vx, oy: vy };
|
| 454 |
+
const lineK = sstep(L[4] + 7.5, L[4] + 10.5, lt);
|
| 455 |
+
drawLines(ctx, view, GEM_LINES, lineK, 'rgba(255,224,170,.85)', 2.5 * u, rise);
|
| 456 |
+
drawStarSet(ctx, view, GEM_KEYS.filter(k => k !== 'castor' && k !== 'pollux'), t, u, lineK);
|
| 457 |
+
const [cx1, cy1] = P(view, 'castor'), [cx2, cy2] = P(view, 'pollux');
|
| 458 |
+
const k = easeIO(rise);
|
| 459 |
+
drawStar(ctx, lerp(xa, cx1, k), lerp(base - h * .6, cy1, k), lerp(30, 9, k) * u, '#e3ecff', 1, .8);
|
| 460 |
+
drawStar(ctx, lerp(xb, cx2, k), lerp(base - h * .6, cy2, k), lerp(30, 10, k) * u, '#ffc985', 1, .8);
|
| 461 |
+
}
|
| 462 |
+
};
|
| 463 |
+
|
| 464 |
+
// 4. 北河二:六星系统
|
| 465 |
+
SC.castor = (ctx, lt, D, W, H, L, t) => {
|
| 466 |
+
const { u, port } = layout(W, H);
|
| 467 |
+
const cx = W / 2, cy = H * (port ? .38 : .46);
|
| 468 |
+
// 开场:从星座推近
|
| 469 |
+
const zoom = easeIO(sstep(0, L[1] + .5, lt));
|
| 470 |
+
if (zoom < 1) {
|
| 471 |
+
const view = { cx: lerp(1, sky('castor')[0], zoom), cy: lerp(0, sky('castor')[1], zoom), s: lerp(16, 160, zoom * zoom) * u, ox: cx, oy: cy };
|
| 472 |
+
const a = 1 - zoom;
|
| 473 |
+
drawLines(ctx, view, GEM_LINES, 1, 'rgba(255,224,170,.7)', 2.5 * u, a);
|
| 474 |
+
drawStarSet(ctx, view, GEM_KEYS.filter(k => k !== 'castor'), t, u, a);
|
| 475 |
+
text(ctx, '神话 → 科学', cx, H * .12, 40 * u, '#cfe0ff', 'center', sstep(.3, 1.2, lt) * (1 - zoom), SERIF, 700);
|
| 476 |
+
}
|
| 477 |
+
// 望远镜视场
|
| 478 |
+
const scope = sstep(L[1], L[1] + 1.2, lt);
|
| 479 |
+
const R = (port ? 400 : 380) * u;
|
| 480 |
+
if (scope > 0) {
|
| 481 |
+
ctx.save(); ctx.globalAlpha = scope;
|
| 482 |
+
ctx.strokeStyle = 'rgba(160,200,255,.55)'; ctx.lineWidth = 2 * u; ctx.beginPath(); ctx.arc(cx, cy, R * lerp(1.4, 1, scope), 0, TAU); ctx.stroke();
|
| 483 |
+
ctx.setLineDash([6 * u, 8 * u]); ctx.beginPath(); ctx.moveTo(cx - R, cy); ctx.lineTo(cx - R * .75, cy); ctx.moveTo(cx + R * .75, cy); ctx.lineTo(cx + R, cy); ctx.moveTo(cx, cy - R); ctx.lineTo(cx, cy - R * .75); ctx.moveTo(cx, cy + R * .75); ctx.lineTo(cx, cy + R); ctx.stroke();
|
| 484 |
+
ctx.restore();
|
| 485 |
+
text(ctx, '望远镜视野', cx, cy - R - 30 * u, 26 * u, '#a9c2ff', 'center', scope * (1 - sstep(L[2], L[2] + 1, lt)), SANS, 400);
|
| 486 |
+
}
|
| 487 |
+
// 分裂:1 → 2 → 6
|
| 488 |
+
const split1 = easeOut(sstep(L[1] + 1.6, L[1] + 3.2, lt)), split2 = easeOut(sstep(L[2] + .3, L[2] + 2.5, lt));
|
| 489 |
+
const om = lt * .55;
|
| 490 |
+
const centers = [
|
| 491 |
+
[cx - R * .38 * split1, cy - R * .08 * split1], // A
|
| 492 |
+
[cx + R * .38 * split1, cy + R * .08 * split1], // B
|
| 493 |
+
[cx + R * .25 * split2, cy + R * .62 * split2], // C
|
| 494 |
+
];
|
| 495 |
+
// 整体轻转
|
| 496 |
+
const rot = split2 * lt * .08;
|
| 497 |
+
const rp = ([x, y]) => { const dx = x - cx, dy = y - cy; return [cx + dx * Math.cos(rot) - dy * Math.sin(rot), cy + dx * Math.sin(rot) + dy * Math.cos(rot)]; };
|
| 498 |
+
const pairs = [['A', '#e9f0ff', 1, 1.0], ['B', '#f0f4ff', .85, 1.4], ['C', '#ff9a6a', .5, 2.4]];
|
| 499 |
+
if (zoom > .4) {
|
| 500 |
+
pairs.forEach(([nm, col, sz, sp], i) => {
|
| 501 |
+
const vis = i < 2 ? 1 : split2;
|
| 502 |
+
if (vis <= 0) return;
|
| 503 |
+
const [px, py] = rp(centers[i]);
|
| 504 |
+
const sep = split2 * (i === 2 ? 26 : 22) * u;
|
| 505 |
+
const a = om * sp + i;
|
| 506 |
+
const x1 = px + Math.cos(a) * sep, y1 = py + Math.sin(a) * sep * .55, x2 = px - Math.cos(a) * sep, y2 = py - Math.sin(a) * sep * .55;
|
| 507 |
+
if (split2 > 0) { ctx.save(); ctx.globalAlpha = split2 * .4; ctx.strokeStyle = col; ctx.lineWidth = 1.3 * u; ctx.beginPath(); ctx.ellipse(px, py, sep, sep * .55, 0, 0, TAU); ctx.stroke(); ctx.restore(); }
|
| 508 |
+
drawStar(ctx, x1, y1, (14 + 6 * (1 - split2)) * sz * u * (i === 2 ? 1 : lerp(1.6, 1, split1)), col, vis, i < 2 ? .6 : 0);
|
| 509 |
+
if (split2 > .05) drawStar(ctx, x2, y2, 9 * sz * u, col, vis * split2);
|
| 510 |
+
const la = sstep(L[2] + 2, L[2] + 3, lt);
|
| 511 |
+
text(ctx, `北河二 ${nm}`, px, py - 84 * u, 28 * u, '#fff', 'center', la, SANS, 700);
|
| 512 |
+
text(ctx, i === 2 ? '一对红矮星' : '一对双星', px, py - 52 * u, 22 * u, col, 'center', la * .9, SANS, 400);
|
| 513 |
+
});
|
| 514 |
+
// A-B 公转轨迹 / C 远轨
|
| 515 |
+
if (split2 > 0) {
|
| 516 |
+
ctx.save(); ctx.globalAlpha = split2 * .3; ctx.strokeStyle = '#cfe0ff'; ctx.setLineDash([3 * u, 9 * u]); ctx.lineWidth = 1.5 * u;
|
| 517 |
+
ctx.beginPath(); ctx.ellipse(cx, cy, R * .4, R * .14, rot, 0, TAU); ctx.stroke();
|
| 518 |
+
ctx.beginPath(); ctx.ellipse(cx, cy, R * .7, R * .66, rot, 0, TAU); ctx.stroke(); ctx.restore();
|
| 519 |
+
}
|
| 520 |
+
}
|
| 521 |
+
// 计数器
|
| 522 |
+
const cnt = sstep(L[2] + 2.2, L[2] + 3.2, lt);
|
| 523 |
+
const cntY = port ? H * .7 : H * .14;
|
| 524 |
+
chip(ctx, '6 颗恒星 = 3 对双星', port ? W / 2 : W * .2, cntY, 34 * u, cnt);
|
| 525 |
+
// 距离尺
|
| 526 |
+
const dist = sstep(L[2] + 5, L[2] + 6.5, lt);
|
| 527 |
+
if (dist > 0) {
|
| 528 |
+
const y = port ? H * .75 + 30 * u : H * .14, x0 = port ? W * .14 : W * .6, x1 = port ? W * .86 : W * .92;
|
| 529 |
+
const yy = port ? y + 40 * u : y;
|
| 530 |
+
const xx1 = lerp(x0, x1, easeOut(dist));
|
| 531 |
+
ctx.save(); ctx.globalAlpha = dist; ctx.strokeStyle = 'rgba(200,220,255,.8)'; ctx.lineWidth = 2 * u; ctx.beginPath(); ctx.moveTo(x0, yy); ctx.lineTo(xx1, yy); ctx.stroke(); ctx.restore();
|
| 532 |
+
drawStar(ctx, x0, yy, 7 * u, '#6fb7ff', dist); text(ctx, '地球', x0, yy + 32 * u, 22 * u, '#a9d3ff', 'center', dist);
|
| 533 |
+
drawStar(ctx, xx1, yy, 9 * u, '#e9f0ff', dist); text(ctx, '北河二', x1, yy + 32 * u, 22 * u, '#e9f0ff', 'center', dist);
|
| 534 |
+
text(ctx, '≈ 51 光年', (x0 + x1) / 2, yy - 30 * u, 32 * u, '#ffe6b0', 'center', dist, SERIF, 800);
|
| 535 |
+
}
|
| 536 |
+
};
|
| 537 |
+
|
| 538 |
+
// 5. 北河三:橙色巨星
|
| 539 |
+
SC.pollux = (ctx, lt, D, W, H, L, t) => {
|
| 540 |
+
const { u, port } = layout(W, H);
|
| 541 |
+
const k = easeOut(sstep(0, 2.2, lt));
|
| 542 |
+
const gx = port ? W * .5 : W * .36, gy = port ? H * .33 : H * .47, R = (port ? 250 : 270) * u * lerp(.2, 1, k);
|
| 543 |
+
const bright = sstep(L[1], L[1] + 1, lt);
|
| 544 |
+
const planet = sstep(L[2], L[2] + 1, lt);
|
| 545 |
+
// 日冕光晕
|
| 546 |
+
drawStar(ctx, gx, gy, R * .6, '#ff9d45', .9 * k);
|
| 547 |
+
// 球体
|
| 548 |
+
ctx.save();
|
| 549 |
+
const g = ctx.createRadialGradient(gx - R * .3, gy - R * .3, R * .1, gx, gy, R);
|
| 550 |
+
g.addColorStop(0, '#fff1c4'); g.addColorStop(.35, '#ffbe63'); g.addColorStop(.8, '#e9772c'); g.addColorStop(1, '#a8441a');
|
| 551 |
+
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(gx, gy, R, 0, TAU); ctx.fill();
|
| 552 |
+
ctx.clip();
|
| 553 |
+
// 米粒组织
|
| 554 |
+
const r = rng(31); ctx.globalCompositeOperation = 'overlay';
|
| 555 |
+
for (let i = 0; i < 90; i++) {
|
| 556 |
+
const a = r() * TAU + lt * .05 * (r() > .5 ? 1 : .8), d = Math.sqrt(r()) * R, x = gx + Math.cos(a) * d, y = gy + Math.sin(a) * d * .98, s = (14 + r() * 40) * u * k;
|
| 557 |
+
const cg = ctx.createRadialGradient(x, y, 0, x, y, s); const on = .25 + .2 * Math.sin(lt * (1 + r() * 2) + i);
|
| 558 |
+
cg.addColorStop(0, `rgba(255,240,200,${on})`); cg.addColorStop(1, 'rgba(120,40,0,0)'); ctx.fillStyle = cg; ctx.fillRect(x - s, y - s, s * 2, s * 2);
|
| 559 |
+
}
|
| 560 |
+
ctx.globalCompositeOperation = 'source-over';
|
| 561 |
+
const limb = ctx.createRadialGradient(gx, gy, R * .7, gx, gy, R); limb.addColorStop(0, 'rgba(60,10,0,0)'); limb.addColorStop(1, 'rgba(60,10,0,.55)');
|
| 562 |
+
ctx.fillStyle = limb; ctx.fillRect(gx - R, gy - R, R * 2, R * 2);
|
| 563 |
+
ctx.restore();
|
| 564 |
+
// 行星轨道
|
| 565 |
+
if (planet > 0) {
|
| 566 |
+
const a = R * 1.55, b = R * .42;
|
| 567 |
+
ctx.save(); ctx.globalAlpha = planet * .6; ctx.strokeStyle = '#ffd7a0'; ctx.lineWidth = 1.5 * u; ctx.setLineDash([5 * u, 8 * u]);
|
| 568 |
+
ctx.beginPath(); ctx.ellipse(gx, gy, a, b, -.18, 0, TAU); ctx.stroke(); ctx.restore();
|
| 569 |
+
const ang = (lt - L[2]) * .7 + 2.2;
|
| 570 |
+
const px = gx + Math.cos(ang) * a * Math.cos(-.18) - Math.sin(ang) * b * Math.sin(-.18), py = gy + Math.cos(ang) * a * Math.sin(-.18) + Math.sin(ang) * b * Math.cos(-.18);
|
| 571 |
+
const behind = Math.sin(ang) < 0 && Math.hypot(px - gx, py - gy) < R;
|
| 572 |
+
if (!behind) {
|
| 573 |
+
ctx.save(); ctx.globalAlpha = planet; const pg = ctx.createRadialGradient(px - 6 * u, py - 6 * u, 1, px, py, 22 * u);
|
| 574 |
+
pg.addColorStop(0, '#f6e2c3'); pg.addColorStop(.6, '#b78a5c'); pg.addColorStop(1, '#4c3322'); ctx.fillStyle = pg; ctx.beginPath(); ctx.arc(px, py, 20 * u, 0, TAU); ctx.fill(); ctx.restore();
|
| 575 |
+
text(ctx, '北河三 b', px, py - 44 * u, 26 * u, '#ffe6c0', 'center', planet, SANS, 700);
|
| 576 |
+
}
|
| 577 |
+
}
|
| 578 |
+
// 信息卡
|
| 579 |
+
const ix = port ? W * .5 : W * .74, iy = port ? H * .62 : H * .3, al = port ? 'center' : 'center';
|
| 580 |
+
const s1 = sstep(L[0] + 1, L[0] + 2, lt);
|
| 581 |
+
text(ctx, '北河三 · Pollux', ix, iy, 46 * u, '#ffd28c', al, s1, SERIF, 900, 'rgba(255,150,60,.6)');
|
| 582 |
+
text(ctx, '橙色巨星 · 距地球约 34 光年', ix, iy + 52 * u, 28 * u, '#ffe9cc', al, sstep(L[0] + 2, L[0] + 3, lt) * (port ? 1 - bright : 1), SANS, 500);
|
| 583 |
+
// 太阳大小对比
|
| 584 |
+
const sunA = sstep(L[0] + 3.5, L[0] + 4.5, lt) * (1 - bright);
|
| 585 |
+
if (sunA > 0) {
|
| 586 |
+
const sx = port ? W * .5 : gx + R + 80 * u, sy = port ? gy + R + 60 * u : gy + R * .7, sr = R / 8.8;
|
| 587 |
+
drawStar(ctx, sx, sy, sr * .7, '#fff3c0', sunA);
|
| 588 |
+
ctx.save(); ctx.globalAlpha = sunA; ctx.fillStyle = '#ffe98f'; ctx.beginPath(); ctx.arc(sx, sy, sr, 0, TAU); ctx.fill(); ctx.restore();
|
| 589 |
+
text(ctx, '太阳', sx + (port ? 70 * u : 0), sy + (port ? 0 : 50 * u), 24 * u, '#fff3c0', 'center', sunA);
|
| 590 |
+
text(ctx, '半径约为太阳的 9 倍', ix, iy + 100 * u, 24 * u, '#d9c4a6', al, sunA * (port ? 0 : 1));
|
| 591 |
+
}
|
| 592 |
+
// 亮度对比
|
| 593 |
+
if (bright > 0) {
|
| 594 |
+
const by = port ? H * .69 : iy + 150 * u, bx = ix;
|
| 595 |
+
const rows = [['北河二', 1.58, '#cfe0ff'], ['北河三', 1.14, '#ffc985']];
|
| 596 |
+
const fade = bright * (1 - planet * .0);
|
| 597 |
+
rows.forEach(([nm, m, col], i) => {
|
| 598 |
+
const y = by + i * 64 * u, w = (port ? 360 : 300) * u * (3 - m) / 2.2 * easeOut(sstep(L[1] + .3 + i * .5, L[1] + 1.6 + i * .5, lt));
|
| 599 |
+
text(ctx, nm, bx - 190 * u, y, 28 * u, col, 'right', fade, SANS, 700);
|
| 600 |
+
ctx.save(); ctx.globalAlpha = fade; const bg = ctx.createLinearGradient(bx - 170 * u, 0, bx - 170 * u + w, 0); bg.addColorStop(0, hexA(col, .2)); bg.addColorStop(1, col);
|
| 601 |
+
ctx.fillStyle = bg; rrect(ctx, bx - 170 * u, y - 14 * u, Math.max(w, 1), 28 * u, 14 * u); ctx.fill(); ctx.restore();
|
| 602 |
+
text(ctx, `${m} 等`, bx - 160 * u + w + 10 * u, y, 26 * u, '#fff', 'left', fade, SANS, 600);
|
| 603 |
+
});
|
| 604 |
+
chip(ctx, '星等数字越小 = 越亮', bx, by + 128 * u, 22 * u, sstep(L[1] + 2, L[1] + 3, lt));
|
| 605 |
+
const crown = sstep(L[1] + 3, L[1] + 4, lt);
|
| 606 |
+
text(ctx, '★ 双子座最亮星', gx, gy - R - 40 * u, 32 * u, '#ffe08a', 'center', crown, SERIF, 800, 'rgba(255,190,80,.8)');
|
| 607 |
+
}
|
| 608 |
+
if (planet > 0) chip(ctx, '质量 ≥ 2 倍木星 · 公转约 590 天', port ? W * .5 : ix, port ? H * .14 : iy + 350 * u, 26 * u, sstep(L[2] + 1.5, L[2] + 2.5, lt));
|
| 609 |
+
};
|
| 610 |
+
|
| 611 |
+
// 6. 双子座流星雨
|
| 612 |
+
export const METEORS = (() => {
|
| 613 |
+
const sc = TIMELINE.scenes.find(s => s.id === 'meteor'); const r = rng(1213); const out = [];
|
| 614 |
+
const L = sc.lines.map(l => l.start - sc.start), D = sc.end - sc.start;
|
| 615 |
+
let tt = 1.0;
|
| 616 |
+
while (tt < D - 1) {
|
| 617 |
+
const peak = tt > L[1] - .5 && tt < L[2] + 1.5;
|
| 618 |
+
const end = L[2] + 2.5;
|
| 619 |
+
if (tt > end && tt < D - 2) { tt += 1.2; continue; }
|
| 620 |
+
out.push({ t: tt, a: r() * TAU, len: .5 + r() * .5, sp: .9 + r() * .6, br: r(), d0: .05 + r() * .2 });
|
| 621 |
+
tt += peak ? .05 + r() * .14 : .35 + r() * .7;
|
| 622 |
+
}
|
| 623 |
+
return { list: out, start: sc.start };
|
| 624 |
+
})();
|
| 625 |
+
SC.meteor = (ctx, lt, D, W, H, L, t) => {
|
| 626 |
+
const { u, port } = layout(W, H);
|
| 627 |
+
const orbit = sstep(L[2] + 1.2, L[2] + 3, lt);
|
| 628 |
+
const land = 1 - orbit;
|
| 629 |
+
const rx = W * (port ? .45 : .42), ry = H * (port ? .2 : .24);
|
| 630 |
+
if (land > 0) {
|
| 631 |
+
ctx.save(); ctx.globalAlpha = land;
|
| 632 |
+
// 辐射点
|
| 633 |
+
drawStar(ctx, rx, ry, 22 * u, '#ffd9a0', .6 + .2 * Math.sin(lt * 3));
|
| 634 |
+
text(ctx, '辐射点 · 北河二附近', rx, ry - 46 * u, 24 * u, '#ffe6b0', 'center', sstep(1, 2.5, lt), SANS, 500);
|
| 635 |
+
// 流星
|
| 636 |
+
const diag = Math.hypot(W, H);
|
| 637 |
+
for (const m of METEORS.list) {
|
| 638 |
+
const age = lt - m.t; if (age < 0 || age > 1.1) continue;
|
| 639 |
+
const k = age / 1.1, d0 = m.d0 * diag, d = d0 + m.sp * diag * .35 * easeOut(k);
|
| 640 |
+
const x = rx + Math.cos(m.a) * d, y = ry + Math.sin(m.a) * d;
|
| 641 |
+
const tail = diag * .14 * m.len * Math.sin(Math.min(1, k * 2.2) * Math.PI * .5);
|
| 642 |
+
const x0 = x - Math.cos(m.a) * tail, y0 = y - Math.sin(m.a) * tail;
|
| 643 |
+
const a = Math.sin(k * Math.PI) * (.5 + m.br * .5);
|
| 644 |
+
const gr = ctx.createLinearGradient(x0, y0, x, y); gr.addColorStop(0, 'rgba(255,255,255,0)'); gr.addColorStop(.7, `rgba(190,220,255,${a * .6})`); gr.addColorStop(1, `rgba(255,255,255,${a})`);
|
| 645 |
+
ctx.strokeStyle = gr; ctx.lineWidth = (1.5 + m.br * 2.5) * u; ctx.lineCap = 'round';
|
| 646 |
+
ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(x, y); ctx.stroke();
|
| 647 |
+
drawStar(ctx, x, y, (3 + m.br * 4) * u, '#e7f1ff', a);
|
| 648 |
+
}
|
| 649 |
+
// 山野剪影 + 观星者
|
| 650 |
+
const hy = H * (port ? .7 : .8);
|
| 651 |
+
ctx.fillStyle = '#05060e'; ctx.beginPath(); ctx.moveTo(0, H);
|
| 652 |
+
for (let x = 0; x <= W; x += 12 * u) ctx.lineTo(x, hy + Math.sin(x / W * 4 + .5) * 40 * u + Math.sin(x / W * 19) * 8 * u + (x > W * .55 && x < W * .75 ? -Math.sin((x - W * .55) / (W * .2) * Math.PI) * 50 * u : 0));
|
| 653 |
+
ctx.lineTo(W, H); ctx.fill();
|
| 654 |
+
// 松树
|
| 655 |
+
const tr = rng(5);
|
| 656 |
+
for (let i = 0; i < 14; i++) {
|
| 657 |
+
const x = tr() * W, s = (30 + tr() * 50) * u, y = hy + Math.sin(x / W * 4 + .5) * 40 * u + 8 * u;
|
| 658 |
+
ctx.beginPath(); ctx.moveTo(x, y - s * 2); ctx.lineTo(x - s * .5, y); ctx.lineTo(x + s * .5, y); ctx.fill();
|
| 659 |
+
}
|
| 660 |
+
figure(ctx, W * .66, hy - 40 * u, 60 * u, { color: '#30395e', alpha: 1, arm: .8, glow: 0, fill: '#05060e' });
|
| 661 |
+
figure(ctx, W * .69, hy - 38 * u, 52 * u, { color: '#30395e', alpha: 1, arm: .3, glow: 0, fill: '#05060e' });
|
| 662 |
+
ctx.restore();
|
| 663 |
+
// 信息
|
| 664 |
+
const ia = sstep(L[0] + 1.5, L[0] + 2.5, lt);
|
| 665 |
+
chip(ctx, '每年 12 月中旬 · 12/14 前后极大', W / 2, H * (port ? .09 : .08), 28 * u, ia * land);
|
| 666 |
+
const peak = sstep(L[1], L[1] + 1, lt) * land;
|
| 667 |
+
const n = Math.round(lerp(0, 150, easeOut(sstep(L[1], L[1] + 3, lt))));
|
| 668 |
+
text(ctx, `${n}`, W * (port ? .5 : .8), H * (port ? .42 : .4), 120 * u, '#fff', 'center', peak, SERIF, 900, 'rgba(160,200,255,.9)');
|
| 669 |
+
text(ctx, '颗 / 小时(理想暗夜峰值 ZHR)', W * (port ? .5 : .8), H * (port ? .42 : .4) + 84 * u, 26 * u, '#cfe0ff', 'center', peak);
|
| 670 |
+
}
|
| 671 |
+
if (orbit > 0) {
|
| 672 |
+
// 轨道图:太阳 / 地球 / 法厄同
|
| 673 |
+
const ox = W / 2, oy = H * (port ? .38 : .47), AU = (port ? 230 : 210) * u;
|
| 674 |
+
ctx.save(); ctx.globalAlpha = orbit;
|
| 675 |
+
drawStar(ctx, ox, oy, 20 * u, '#ffe08a', 1); ctx.fillStyle = '#fff1b0'; ctx.beginPath(); ctx.arc(ox, oy, 12 * u, 0, TAU); ctx.fill();
|
| 676 |
+
ctx.strokeStyle = 'rgba(120,180,255,.6)'; ctx.lineWidth = 2 * u; ctx.beginPath(); ctx.arc(ox, oy, AU, 0, TAU); ctx.stroke();
|
| 677 |
+
// 法厄同椭圆:a=1.27, e=0.89
|
| 678 |
+
const a = 1.27 * AU, e = .89, b = a * Math.sqrt(1 - e * e), c = a * e, rot = -2.2;
|
| 679 |
+
const ex = ox + Math.cos(rot) * c, ey = oy + Math.sin(rot) * c;
|
| 680 |
+
const draw = easeOut(sstep(L[2] + 1.5, L[2] + 4, lt));
|
| 681 |
+
ctx.strokeStyle = 'rgba(255,170,110,.85)'; ctx.lineWidth = 2.2 * u; ctx.beginPath(); ctx.ellipse(ex, ey, a, b, rot, 0, TAU * draw); ctx.stroke();
|
| 682 |
+
// 碎屑带
|
| 683 |
+
const r = rng(42);
|
| 684 |
+
for (let i = 0; i < 260; i++) {
|
| 685 |
+
const th = r() * TAU, jit = (r() - .5) * 14 * u;
|
| 686 |
+
const lx = Math.cos(th) * a, ly = Math.sin(th) * b;
|
| 687 |
+
const x = ex + lx * Math.cos(rot) - ly * Math.sin(rot) + jit, y = ey + lx * Math.sin(rot) + ly * Math.cos(rot) + (r() - .5) * 14 * u;
|
| 688 |
+
ctx.fillStyle = `rgba(255,200,150,${.25 + r() * .5})`; ctx.fillRect(x, y, 2 * u, 2 * u);
|
| 689 |
+
}
|
| 690 |
+
// 小行星
|
| 691 |
+
const pa = lt * .25;
|
| 692 |
+
const lx = Math.cos(pa) * a, ly = Math.sin(pa) * b;
|
| 693 |
+
const px = ex + lx * Math.cos(rot) - ly * Math.sin(rot), py = ey + lx * Math.sin(rot) + ly * Math.cos(rot);
|
| 694 |
+
ctx.fillStyle = '#c9a58a'; ctx.beginPath(); ctx.ellipse(px, py, 9 * u, 7 * u, pa, 0, TAU); ctx.fill();
|
| 695 |
+
// 地球
|
| 696 |
+
const ea = -1.2 + (lt - L[2]) * .12;
|
| 697 |
+
const ex2 = ox + Math.cos(ea) * AU, ey2 = oy + Math.sin(ea) * AU;
|
| 698 |
+
ctx.restore();
|
| 699 |
+
drawStar(ctx, ex2, ey2, 8 * u, '#7fc3ff', orbit);
|
| 700 |
+
text(ctx, '地球', ex2 + 40 * u, ey2, 24 * u, '#a9d3ff', 'left', orbit);
|
| 701 |
+
text(ctx, '太阳', ox, oy + 40 * u, 22 * u, '#ffe9a6', 'center', orbit);
|
| 702 |
+
text(ctx, '3200 法厄同(小行星)', px, py - 36 * u, 26 * u, '#ffc89a', 'center', orbit * sstep(L[2] + 2, L[2] + 3, lt), SANS, 700);
|
| 703 |
+
chip(ctx, '母体是小行星,而不是彗星', W / 2, H * (port ? .1 : .08), 28 * u, orbit * sstep(L[2] + 2.5, L[2] + 3.5, lt));
|
| 704 |
+
chip(ctx, '每年 12 月,地球穿过它留下的碎屑带', W / 2, H * (port ? .68 : .88) - (port ? 0 : 0), 24 * u, orbit * sstep(L[2] + 4, L[2] + 5, lt) * (port ? 1 : 0));
|
| 705 |
+
}
|
| 706 |
+
};
|
| 707 |
+
|
| 708 |
+
// 7. 历史发现
|
| 709 |
+
SC.discover = (ctx, lt, D, W, H, L, t) => {
|
| 710 |
+
const { u, port } = layout(W, H);
|
| 711 |
+
// 顶部小星座 + 位置标记
|
| 712 |
+
const view = { cx: 1, cy: -1.5, s: (port ? 11 : 10) * u, ox: W / 2, oy: H * (port ? .14 : .2) };
|
| 713 |
+
const top = sstep(0, 1.2, lt);
|
| 714 |
+
drawLines(ctx, view, GEM_LINES, 1, 'rgba(255,224,170,.6)', 2 * u, top);
|
| 715 |
+
drawStarSet(ctx, view, GEM_KEYS, t, u, top);
|
| 716 |
+
const mk = (k, label, col, a) => { const [x, y] = P(view, k); ctx.save(); ctx.globalAlpha = a; ctx.strokeStyle = col; ctx.lineWidth = 2.5 * u; ctx.beginPath(); ctx.arc(x, y, (20 + 4 * Math.sin(lt * 4)) * u, 0, TAU); ctx.stroke(); ctx.restore(); text(ctx, label, x + (k === 'propus' ? 40 : -40) * u, y + 34 * u, 22 * u, col, 'center', a, SANS, 700); };
|
| 717 |
+
const pA = sstep(L[1], L[1] + 1, lt), pB = sstep(L[2], L[2] + 1, lt);
|
| 718 |
+
mk('propus', '天王星 1781', '#8ef0ff', pA);
|
| 719 |
+
mk('wasat', '冥王星 1930', '#ffb3d0', pB);
|
| 720 |
+
// 两块面板
|
| 721 |
+
const pw = port ? W * .82 : W * .4, ph = port ? H * .22 : H * .44;
|
| 722 |
+
const p1 = port ? [W * .09, H * .27] : [W * .07, H * .38], p2 = port ? [W * .09, H * .51] : [W * .53, H * .38];
|
| 723 |
+
const panel = (x, y, a, title, sub) => {
|
| 724 |
+
if (a <= 0) return;
|
| 725 |
+
ctx.save(); ctx.globalAlpha = a; rrect(ctx, x, y, pw, ph, 24 * u); ctx.fillStyle = 'rgba(14,16,40,.78)'; ctx.fill(); ctx.strokeStyle = 'rgba(255,220,160,.4)'; ctx.lineWidth = 1.5 * u; ctx.stroke(); ctx.restore();
|
| 726 |
+
text(ctx, title, x + 30 * u, y + 44 * u, 40 * u, '#ffe6b0', 'left', a, SERIF, 900);
|
| 727 |
+
text(ctx, sub, x + 30 * u, y + 90 * u, 24 * u, '#c9d6ff', 'left', a, SANS, 400);
|
| 728 |
+
};
|
| 729 |
+
const a1 = sstep(L[1] - .3, L[1] + .8, lt), a2 = sstep(L[2] - .3, L[2] + .8, lt);
|
| 730 |
+
panel(p1[0], p1[1] + (1 - a1) * 40 * u, a1, '1781', '威廉·赫歇尔 · 自制望远镜');
|
| 731 |
+
panel(p2[0], p2[1] + (1 - a2) * 40 * u, a2, '1930', '克莱德·汤博 · 比对照相底片');
|
| 732 |
+
// 天王星 + 望远镜
|
| 733 |
+
if (a1 > 0) {
|
| 734 |
+
const x = p1[0] + pw * .72, y = p1[1] + ph * .55, r = Math.min(pw, ph) * .2 * backOut(sstep(L[1] + .6, L[1] + 1.8, lt));
|
| 735 |
+
ctx.save(); ctx.globalAlpha = a1;
|
| 736 |
+
const g = ctx.createRadialGradient(x - r * .3, y - r * .3, r * .1, x, y, r); g.addColorStop(0, '#e6ffff'); g.addColorStop(.6, '#7fd7e6'); g.addColorStop(1, '#2c7c93');
|
| 737 |
+
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(x, y, Math.max(r, .1), 0, TAU); ctx.fill();
|
| 738 |
+
ctx.strokeStyle = 'rgba(200,250,255,.6)'; ctx.lineWidth = 2 * u; ctx.beginPath(); ctx.ellipse(x, y, Math.max(r * .35, .1), Math.max(r * 1.6, .1), .1, 0, TAU); ctx.stroke();
|
| 739 |
+
// 望远镜线稿
|
| 740 |
+
const tx = p1[0] + pw * .24, ty = p1[1] + ph * .78;
|
| 741 |
+
ctx.strokeStyle = '#ffe6b0'; ctx.lineWidth = 4 * u; ctx.lineCap = 'round';
|
| 742 |
+
ctx.beginPath(); ctx.moveTo(tx - 50 * u, ty); ctx.lineTo(tx, ty - 60 * u); ctx.lineTo(tx + 50 * u, ty); ctx.moveTo(tx, ty - 60 * u); ctx.lineTo(tx, ty); ctx.stroke();
|
| 743 |
+
ctx.translate(tx, ty - 60 * u); ctx.rotate(-.55); ctx.lineWidth = 16 * u; ctx.beginPath(); ctx.moveTo(-60 * u, 0); ctx.lineTo(90 * u, 0); ctx.stroke();
|
| 744 |
+
ctx.restore();
|
| 745 |
+
text(ctx, '天王星', x, y + r + 34 * u, 26 * u, '#aef4ff', 'center', a1 * sstep(L[1] + 1, L[1] + 2, lt), SANS, 700);
|
| 746 |
+
}
|
| 747 |
+
// 冥王星:闪烁比对
|
| 748 |
+
if (a2 > 0) {
|
| 749 |
+
const bw = Math.min(pw * .26, ph * .55), by = p2[1] + ph * .62 - bw / 2;
|
| 750 |
+
const blink = Math.floor((lt - L[2]) * 2.2) % 2;
|
| 751 |
+
[0, 1].forEach(i => {
|
| 752 |
+
const bx = p2[0] + 30 * u + i * (bw + 20 * u);
|
| 753 |
+
ctx.save(); ctx.globalAlpha = a2; ctx.fillStyle = '#0a0b14'; ctx.fillRect(bx, by, bw, bw); ctx.strokeStyle = 'rgba(255,255,255,.35)'; ctx.strokeRect(bx, by, bw, bw);
|
| 754 |
+
const r = rng(88); ctx.fillStyle = '#eef'; for (let j = 0; j < 26; j++) { ctx.fillRect(bx + r() * bw, by + r() * bw, 2 * u, 2 * u); }
|
| 755 |
+
const on = i === blink ? 1 : .15;
|
| 756 |
+
ctx.globalAlpha = a2 * on; ctx.fillStyle = '#ffd0e4'; const dx = bx + bw * (.45 + i * .1), dy = by + bw * .5;
|
| 757 |
+
ctx.beginPath(); ctx.arc(dx, dy, 3.5 * u, 0, TAU); ctx.fill();
|
| 758 |
+
ctx.strokeStyle = '#ff8fbf'; ctx.lineWidth = 2 * u; ctx.beginPath(); ctx.arc(dx, dy, 16 * u, 0, TAU); ctx.stroke(); ctx.restore();
|
| 759 |
+
});
|
| 760 |
+
const x = p2[0] + pw * .82, y = p2[1] + ph * .58, r = Math.min(pw, ph) * .13 * backOut(sstep(L[2] + 1.2, L[2] + 2.4, lt));
|
| 761 |
+
ctx.save(); ctx.globalAlpha = a2; const g = ctx.createRadialGradient(x - r * .3, y - r * .3, r * .1, x, y, Math.max(r, .1)); g.addColorStop(0, '#fff1e6'); g.addColorStop(.6, '#d8b29a'); g.addColorStop(1, '#6b4d45');
|
| 762 |
+
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(x, y, Math.max(r, .1), 0, TAU); ctx.fill(); ctx.restore();
|
| 763 |
+
text(ctx, '冥王星', x, y + r + 30 * u, 26 * u, '#ffc6dc', 'center', a2 * sstep(L[2] + 1.5, L[2] + 2.5, lt), SANS, 700);
|
| 764 |
+
}
|
| 765 |
+
};
|
| 766 |
+
|
| 767 |
+
// 8. 黄道与文化
|
| 768 |
+
const ZOD = ['白羊', '金牛', '双子', '巨蟹', '狮子', '室女', '天秤', '天蝎', '人马', '摩羯', '宝瓶', '双鱼'];
|
| 769 |
+
SC.zodiac = (ctx, lt, D, W, H, L, t) => {
|
| 770 |
+
const { u, port } = layout(W, H);
|
| 771 |
+
const culture = sstep(L[1] - .4, L[1] + 1, lt);
|
| 772 |
+
const cx = W / 2, cy = H * (port ? .38 : .46), R = (port ? 330 : 300) * u;
|
| 773 |
+
const ring = sstep(0, 1.5, lt) * (1 - culture);
|
| 774 |
+
if (ring > 0) {
|
| 775 |
+
ctx.save(); ctx.globalAlpha = ring; ctx.translate(cx, cy);
|
| 776 |
+
const spin = -lt * .02;
|
| 777 |
+
ctx.strokeStyle = 'rgba(255,220,160,.35)'; ctx.lineWidth = 2 * u; ctx.beginPath(); ctx.arc(0, 0, R, 0, TAU); ctx.stroke(); ctx.beginPath(); ctx.arc(0, 0, R * .78, 0, TAU); ctx.stroke();
|
| 778 |
+
ZOD.forEach((z, i) => {
|
| 779 |
+
const a0 = spin + (i / 12) * TAU - Math.PI / 2, a1 = a0 + TAU / 12;
|
| 780 |
+
const isG = i === 2;
|
| 781 |
+
if (isG) { const hl = .45 + .25 * Math.sin(lt * 3); ctx.fillStyle = `rgba(255,200,110,${hl * .5})`; ctx.beginPath(); ctx.arc(0, 0, R, a0, a1); ctx.arc(0, 0, R * .78, a1, a0, true); ctx.fill(); }
|
| 782 |
+
ctx.strokeStyle = 'rgba(255,220,160,.3)'; ctx.beginPath(); ctx.moveTo(Math.cos(a0) * R * .78, Math.sin(a0) * R * .78); ctx.lineTo(Math.cos(a0) * R, Math.sin(a0) * R); ctx.stroke();
|
| 783 |
+
const am = (a0 + a1) / 2; ctx.save(); ctx.translate(Math.cos(am) * R * .89, Math.sin(am) * R * .89); ctx.rotate(am + Math.PI / 2);
|
| 784 |
+
ctx.fillStyle = isG ? '#fff2c8' : '#b9c3e8'; ctx.font = `${isG ? 800 : 500} ${(isG ? 30 : 24) * u}px ${SERIF}`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(z, 0, 0); ctx.restore();
|
| 785 |
+
});
|
| 786 |
+
ctx.restore();
|
| 787 |
+
// 太阳沿黄道
|
| 788 |
+
const g0 = -Math.PI / 2 + (2 / 12) * TAU - lt * .02;
|
| 789 |
+
const sa = g0 - .6 + (lt / Math.max(L[1], 1)) * (TAU / 12 + 1.0);
|
| 790 |
+
const sx = cx + Math.cos(sa) * R * .66, sy = cy + Math.sin(sa) * R * .66;
|
| 791 |
+
drawStar(ctx, sx, sy, 16 * u, '#ffe08a', ring);
|
| 792 |
+
ctx.save(); ctx.globalAlpha = ring; ctx.fillStyle = '#fff2b0'; ctx.beginPath(); ctx.arc(sx, sy, 10 * u, 0, TAU); ctx.fill(); ctx.restore();
|
| 793 |
+
drawStar(ctx, cx, cy, 10 * u, '#7fc3ff', ring); text(ctx, '地球', cx, cy + 34 * u, 22 * u, '#a9d3ff', 'center', ring);
|
| 794 |
+
chip(ctx, '太阳在双子座天区:约 6/21 – 7/20', cx, port ? H * .72 : H * .1, 28 * u, ring * sstep(2, 3, lt));
|
| 795 |
+
}
|
| 796 |
+
if (culture > 0) {
|
| 797 |
+
// 大 ♊ 符号
|
| 798 |
+
const gx = port ? W / 2 : W * .32, gy = port ? H * .3 : H * .46, s = (port ? 300 : 340) * u;
|
| 799 |
+
ctx.save(); ctx.globalAlpha = culture; ctx.font = `700 ${s}px ${SERIF}`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
| 800 |
+
ctx.shadowColor = 'rgba(255,190,90,.8)'; ctx.shadowBlur = 40 * u; ctx.fillStyle = goldGrad(ctx, gx - s / 2, gy - s / 2, gx + s / 2, gy + s / 2);
|
| 801 |
+
// 用线条画 ♊(避免字体缺字)
|
| 802 |
+
ctx.strokeStyle = ctx.fillStyle; ctx.lineWidth = s * .07; ctx.lineCap = 'round';
|
| 803 |
+
const w = s * .5, h2 = s * .55;
|
| 804 |
+
ctx.beginPath(); ctx.moveTo(gx - w * .62, gy - h2 * .55); ctx.quadraticCurveTo(gx, gy - h2 * .35, gx + w * .62, gy - h2 * .55);
|
| 805 |
+
ctx.moveTo(gx - w * .62, gy + h2 * .55); ctx.quadraticCurveTo(gx, gy + h2 * .35, gx + w * .62, gy + h2 * .55);
|
| 806 |
+
ctx.moveTo(gx - w * .25, gy - h2 * .45); ctx.lineTo(gx - w * .25, gy + h2 * .45);
|
| 807 |
+
ctx.moveTo(gx + w * .25, gy - h2 * .45); ctx.lineTo(gx + w * .25, gy + h2 * .45); ctx.stroke();
|
| 808 |
+
ctx.restore();
|
| 809 |
+
text(ctx, '5.21 – 6.21', gx, gy + s * .48, 40 * u, '#ffe6b0', 'center', culture, SERIF, 800);
|
| 810 |
+
const tx = port ? W / 2 : W * .68, ty = port ? H * .56 : H * .32;
|
| 811 |
+
text(ctx, '占星文化里的双子座', tx, ty, 34 * u, '#fff', 'center', culture, SERIF, 700);
|
| 812 |
+
text(ctx, '风象星座 · 守护星:水星', tx, ty + 50 * u, 24 * u, '#c9d6ff', 'center', culture * sstep(L[1] + 1.5, L[1] + 2.5, lt));
|
| 813 |
+
const words = [['好奇', '#9fe0ff'], ['健谈', '#ffd08a'], ['灵动', '#ffb3e0']];
|
| 814 |
+
words.forEach(([w, col], i) => {
|
| 815 |
+
const a = sstep(L[2] + .8 + i * .7, L[2] + 1.6 + i * .7, lt);
|
| 816 |
+
const x = tx + (i - 1) * (port ? 200 : 180) * u, y = ty + (port ? 140 : 160) * u + Math.sin(lt * 1.5 + i * 2) * 10 * u;
|
| 817 |
+
ctx.save(); ctx.globalAlpha = a; ctx.translate(x, y); ctx.scale(backOut(a), backOut(a));
|
| 818 |
+
ctx.beginPath(); ctx.arc(0, 0, 70 * u, 0, TAU); ctx.fillStyle = hexA(col, .15); ctx.fill(); ctx.strokeStyle = col; ctx.lineWidth = 2 * u; ctx.stroke(); ctx.restore();
|
| 819 |
+
text(ctx, w, x, y, 38 * u, col, 'center', a, SERIF, 800);
|
| 820 |
+
});
|
| 821 |
+
chip(ctx, '※ 文化趣味,不是科学结论', tx, ty + (port ? 270 : 300) * u, 22 * u, sstep(L[2] + 4, L[2] + 5, lt), '#d6dcff', 'rgba(20,20,40,.7)', 'rgba(180,190,255,.4)');
|
| 822 |
+
}
|
| 823 |
+
};
|
| 824 |
+
|
| 825 |
+
// 9. 尾声
|
| 826 |
+
SC.outro = (ctx, lt, D, W, H, L, t) => {
|
| 827 |
+
const { u, port } = layout(W, H);
|
| 828 |
+
const view = { cx: 1, cy: -1.5, s: lerp(22, 27, easeIO(clamp(lt / D))) * u * (port ? 1.15 : 1), ox: W / 2, oy: H * (port ? .34 : .40) };
|
| 829 |
+
const a = sstep(0, 1.5, lt);
|
| 830 |
+
drawLines(ctx, view, GEM_LINES, 1, 'rgba(255,190,110,.22)', 24 * u, a * .6);
|
| 831 |
+
drawLines(ctx, view, GEM_LINES, sstep(0, 3, lt), 'rgba(255,230,180,.95)', 2.6 * u, a);
|
| 832 |
+
drawStarSet(ctx, view, GEM_KEYS, t, u, a, { castor: 1.4, pollux: 1.5 });
|
| 833 |
+
// 回顾图标
|
| 834 |
+
const icons = [['兄弟', '#ffe1a0'], ['流星雨', '#cfe0ff'], ['六星家族', '#ffd0a0']];
|
| 835 |
+
icons.forEach(([s, col], i) => chip(ctx, s, W / 2 + (i - 1) * (port ? 250 : 260) * u, H * (port ? .62 : .74), 28 * u, sstep(.8 + i * .9, 1.8 + i * .9, lt) * (1 - sstep(L[1] - .5, L[1] + .5, lt)), col));
|
| 836 |
+
// 流星
|
| 837 |
+
const r = rng(3);
|
| 838 |
+
for (let i = 0; i < 6; i++) {
|
| 839 |
+
const t0 = 2 + i * 3.1 + r(), age = lt - t0; if (age < 0 || age > .8) continue;
|
| 840 |
+
const x = W * (.2 + r() * .6), y = H * (.08 + r() * .2), k = age / .8, len = 220 * u;
|
| 841 |
+
const x1 = x + k * 300 * u, y1 = y + k * 150 * u;
|
| 842 |
+
const g = ctx.createLinearGradient(x1 - len, y1 - len / 2, x1, y1); g.addColorStop(0, 'rgba(255,255,255,0)'); g.addColorStop(1, `rgba(255,255,255,${Math.sin(k * Math.PI)})`);
|
| 843 |
+
ctx.strokeStyle = g; ctx.lineWidth = 2.5 * u; ctx.beginPath(); ctx.moveTo(x1 - len * .9, y1 - len * .45); ctx.lineTo(x1, y1); ctx.stroke();
|
| 844 |
+
}
|
| 845 |
+
// 结尾标题
|
| 846 |
+
const e = sstep(L[2] - .5, L[2] + 1.5, lt);
|
| 847 |
+
if (e > 0) {
|
| 848 |
+
const ty = H * (port ? .66 : .78), size = (port ? 110 : 100) * u;
|
| 849 |
+
ctx.save(); ctx.globalAlpha = e; ctx.font = `900 ${size}px ${SERIF}`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
| 850 |
+
ctx.shadowColor = 'rgba(255,190,90,.7)'; ctx.shadowBlur = 30 * u; ctx.fillStyle = goldGrad(ctx, W / 2 - size * 2, ty, W / 2 + size * 2, ty);
|
| 851 |
+
ctx.fillText('抬头,找到它们', W / 2, ty); ctx.restore();
|
| 852 |
+
text(ctx, '双子座 · GEMINI', W / 2, ty + size * .85, 30 * u, '#cdd8ff', 'center', e, SANS, 400);
|
| 853 |
+
}
|
| 854 |
+
// 最后淡出
|
| 855 |
+
const out = sstep(D - 2.2, D, lt);
|
| 856 |
+
if (out > 0) { ctx.fillStyle = `rgba(0,0,0,${out})`; ctx.fillRect(0, 0, W, H); }
|
| 857 |
+
};
|
| 858 |
+
|
| 859 |
+
const CHAPTERS = { intro: '序章', find: '怎样找到它', myth: '神话 · 孪生兄弟', castor: '北河二 · 六星家族', pollux: '北河三 · 橙色巨星', meteor: '双子座流星雨', discover: '星空里的发现', zodiac: '黄道与星座文化', outro: '尾声' };
|
| 860 |
+
const CAMS = { intro: [0, 0], find: [1, .4], myth: [2, .8], castor: [3, 1.1], pollux: [4, 1.5], meteor: [5, 2.1], discover: [6, 2.5], zodiac: [7, 2.9], outro: [8, 3.3] };
|
| 861 |
+
|
| 862 |
+
/* ---------------- 主渲染 ---------------- */
|
| 863 |
+
export function sceneAt(t) { const S = TIMELINE.scenes; for (let i = S.length - 1; i >= 0; i--) if (t >= S[i].start) return S[i]; return S[0]; }
|
| 864 |
+
export function renderFrame(ctx, t, W, H, opt = {}) {
|
| 865 |
+
t = clamp(t, 0, TIMELINE.duration);
|
| 866 |
+
const sc = sceneAt(t), lt = t - sc.start, D = sc.end - sc.start, L = sc.lines.map(l => l.start - sc.start);
|
| 867 |
+
const { u, port } = layout(W, H);
|
| 868 |
+
// 背景相机:随章节缓慢漂移
|
| 869 |
+
const idx = TIMELINE.scenes.indexOf(sc), nxt = TIMELINE.scenes[idx + 1];
|
| 870 |
+
const base = CAMS[sc.id][1], to = nxt ? CAMS[nxt.id][1] : base + .3;
|
| 871 |
+
const cp = lerp(base, to, easeIO(clamp(lt / D)));
|
| 872 |
+
drawBackground(ctx, t, W, H, { x: cp * W * .5 + t * 6 * u, y: Math.sin(cp) * H * .2 }, sc.id === 'zodiac' || sc.id === 'myth');
|
| 873 |
+
// 场景(淡入淡出)
|
| 874 |
+
const fa = Math.min(sstep(0, .9, lt), 1 - sstep(D - .8, D, lt));
|
| 875 |
+
ctx.save(); ctx.globalAlpha = 1;
|
| 876 |
+
if (sc.id === 'intro') { SC[sc.id](ctx, lt, D, W, H, L, t); }
|
| 877 |
+
else {
|
| 878 |
+
// 用离屏层实现整体淡化
|
| 879 |
+
const layer = getLayer(W, H, opt.layerScale || 1); const lc = layer.getContext('2d');
|
| 880 |
+
lc.setTransform(1, 0, 0, 1, 0, 0); lc.clearRect(0, 0, layer.width, layer.height); lc.setTransform(layer.width / W, 0, 0, layer.height / H, 0, 0);
|
| 881 |
+
SC[sc.id](lc, lt, D, W, H, L, t);
|
| 882 |
+
ctx.globalAlpha = sc.id === 'outro' ? Math.max(fa, sstep(D - .8, D, lt)) : fa; ctx.drawImage(layer, 0, 0, W, H);
|
| 883 |
+
}
|
| 884 |
+
ctx.restore();
|
| 885 |
+
// 转场光带
|
| 886 |
+
const tr = 1 - Math.min(1, Math.abs(lt) / .7);
|
| 887 |
+
if (idx > 0 && tr > 0) {
|
| 888 |
+
ctx.save(); ctx.globalCompositeOperation = 'lighter'; ctx.globalAlpha = tr * .5;
|
| 889 |
+
const x = lerp(-W * .3, W * 1.3, clamp(lt / .7 * .5 + .5)); const g = ctx.createLinearGradient(x - W * .2, 0, x + W * .2, 0);
|
| 890 |
+
g.addColorStop(0, 'rgba(255,200,120,0)'); g.addColorStop(.5, 'rgba(255,220,170,.5)'); g.addColorStop(1, 'rgba(255,200,120,0)');
|
| 891 |
+
ctx.fillStyle = g; ctx.fillRect(0, 0, W, H); ctx.restore();
|
| 892 |
+
}
|
| 893 |
+
vignette(ctx, W, H);
|
| 894 |
+
// 章节角标
|
| 895 |
+
if (sc.id !== 'intro' && sc.id !== 'outro') {
|
| 896 |
+
const ca = sstep(.4, 1.4, lt) * (1 - sstep(D - 1, D - .2, lt));
|
| 897 |
+
const x = port ? 50 * u : 70 * u, y = port ? 80 * u : 64 * u;
|
| 898 |
+
ctx.save(); ctx.globalAlpha = ca; ctx.fillStyle = 'rgba(255,214,140,.9)'; ctx.fillRect(x, y - 18 * u, 4 * u, 36 * u); ctx.restore();
|
| 899 |
+
text(ctx, String(idx).padStart(2, '0'), x + 22 * u, y, 30 * u, '#ffd68c', 'left', ca, SERIF, 900);
|
| 900 |
+
text(ctx, CHAPTERS[sc.id], x + 74 * u, y, 28 * u, '#f1f3ff', 'left', ca, SANS, 600);
|
| 901 |
+
}
|
| 902 |
+
// 角标水印
|
| 903 |
+
text(ctx, 'GEMINI · 星空课', W - (port ? 50 : 70) * u, port ? 80 * u : 64 * u, 22 * u, '#cdd6ff', 'right', .55, SANS, 500);
|
| 904 |
+
// 字幕
|
| 905 |
+
if (opt.subtitles !== false) drawSubtitle(ctx, t, sc, W, H, u, port);
|
| 906 |
+
}
|
| 907 |
+
const layers = new Map();
|
| 908 |
+
function getLayer(W, H, s) { const k = W + 'x' + H + '@' + s; if (!layers.has(k)) layers.set(k, mkCanvas(Math.round(W * s), Math.round(H * s))); return layers.get(k); }
|
| 909 |
+
|
| 910 |
+
function drawSubtitle(ctx, t, sc, W, H, u, port) {
|
| 911 |
+
const ln = sc.lines.find(l => t >= l.start - .15 && t <= l.end + .3); if (!ln) return;
|
| 912 |
+
const a = Math.min(sstep(ln.start - .15, ln.start + .15, t), 1 - sstep(ln.end + .05, ln.end + .3, t));
|
| 913 |
+
const size = (port ? 50 : 44) * u;
|
| 914 |
+
ctx.save(); ctx.font = `600 ${size}px ${SANS}`;
|
| 915 |
+
const lines = wrap(ctx, ln.text, W * (port ? .84 : .74));
|
| 916 |
+
const lh = size * 1.42, h = lines.length * lh + size * .8;
|
| 917 |
+
const maxw = Math.max(...lines.map(s => ctx.measureText(s).width)) + size * 1.4;
|
| 918 |
+
const cy = port ? H * .835 : H - 70 * u - h / 2;
|
| 919 |
+
ctx.globalAlpha = a * .78; rrect(ctx, W / 2 - maxw / 2, cy - h / 2, maxw, h, size * .5);
|
| 920 |
+
const bg = ctx.createLinearGradient(0, cy - h / 2, 0, cy + h / 2); bg.addColorStop(0, 'rgba(10,12,30,.75)'); bg.addColorStop(1, 'rgba(18,14,40,.82)');
|
| 921 |
+
ctx.fillStyle = bg; ctx.fill(); ctx.strokeStyle = 'rgba(255,214,140,.28)'; ctx.lineWidth = 1.5 * u; ctx.stroke();
|
| 922 |
+
ctx.globalAlpha = a; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = '#fff7e6'; ctx.shadowColor = 'rgba(0,0,0,.8)'; ctx.shadowBlur = 6 * u;
|
| 923 |
+
lines.forEach((s, i) => ctx.fillText(s, W / 2, cy - h / 2 + size * .4 + lh * (i + .5)));
|
| 924 |
+
ctx.restore();
|
| 925 |
+
}
|
| 926 |
+
|
| 927 |
+
/* ---------------- 封面 ---------------- */
|
| 928 |
+
export function drawCover(ctx, W, H, cfg) {
|
| 929 |
+
const u = Math.min(W, H) / 1080, port = H > W;
|
| 930 |
+
drawBackground(ctx, 3.3, W, H, { x: W * .3, y: 0 });
|
| 931 |
+
const wide = W / H > 1.5;
|
| 932 |
+
const view = { cx: 0, cy: -3, s: (wide ? 19 : port ? 17 : 16) * u, ox: wide ? W * .68 : W / 2, oy: port ? H * .58 : wide ? H * .5 : H * .6 };
|
| 933 |
+
drawLines(ctx, view, GEM_LINES, 1, 'rgba(255,190,110,.25)', 26 * u, .7);
|
| 934 |
+
drawLines(ctx, view, GEM_LINES, 1, 'rgba(255,230,180,.95)', 3 * u, 1);
|
| 935 |
+
drawStarSet(ctx, view, GEM_KEYS, 1, u, 1, { castor: 2.2, pollux: 2.4 });
|
| 936 |
+
starLabel(ctx, view, 'castor', u, 1, 0, -1.1, 34);
|
| 937 |
+
starLabel(ctx, view, 'pollux', u, 1, -120, 0, 34);
|
| 938 |
+
// 流星
|
| 939 |
+
[[.15, .18], [.3, .1], [.8, .14]].forEach(([fx, fy], i) => {
|
| 940 |
+
const x = W * fx, y = H * fy, len = (260 - i * 50) * u; const g = ctx.createLinearGradient(x, y, x + len, y + len * .5);
|
| 941 |
+
g.addColorStop(0, 'rgba(255,255,255,0)'); g.addColorStop(1, 'rgba(255,255,255,.9)'); ctx.strokeStyle = g; ctx.lineWidth = 3 * u; ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + len, y + len * .5); ctx.stroke();
|
| 942 |
+
drawStar(ctx, x + len, y + len * .5, 5 * u, '#fff', 1);
|
| 943 |
+
});
|
| 944 |
+
vignette(ctx, W, H, .55);
|
| 945 |
+
// 标题
|
| 946 |
+
const tx = wide ? W * .3 : W / 2, ty = port ? H * .16 : wide ? H * .4 : H * .2;
|
| 947 |
+
const size = (port ? 190 : wide ? 170 : 160) * u;
|
| 948 |
+
ctx.save(); ctx.font = `900 ${size}px ${SERIF}`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.shadowColor = 'rgba(255,170,70,.85)'; ctx.shadowBlur = 40 * u;
|
| 949 |
+
ctx.fillStyle = goldGrad(ctx, tx - size * 1.6, ty - size / 2, tx + size * 1.6, ty + size / 2); ctx.fillText('双子座', tx, ty); ctx.restore();
|
| 950 |
+
text(ctx, cfg.hook || '天上最浪漫的一对兄弟', tx, ty + size * .78, (port ? 54 : 48) * u, '#fff4dc', 'center', 1, SERIF, 800, 'rgba(0,0,0,.9)');
|
| 951 |
+
// 卖点标签
|
| 952 |
+
const tags = cfg.tags || ['神话', '六星家族', '流星雨'];
|
| 953 |
+
const ty2 = port ? H * .9 : wide ? H * .4 + size * 1.55 : H * .9;
|
| 954 |
+
tags.forEach((s, i) => chip(ctx, s, tx + (i - (tags.length - 1) / 2) * (port ? 260 : 230) * u, ty2, (port ? 36 : 32) * u, 1));
|
| 955 |
+
if (cfg.badge) chip(ctx, cfg.badge, W - 140 * u, 70 * u, 26 * u, 1, '#1a1030', 'rgba(255,214,140,.95)', 'rgba(255,255,255,.6)');
|
| 956 |
+
}
|
gemini-stars/vendor/mp4-muxer.LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2023 Vanilagy
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
gemini-stars/vendor/mp4-muxer.mjs
ADDED
|
@@ -0,0 +1,1879 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
var __accessCheck = (obj, member, msg) => {
|
| 2 |
+
if (!member.has(obj))
|
| 3 |
+
throw TypeError("Cannot " + msg);
|
| 4 |
+
};
|
| 5 |
+
var __privateGet = (obj, member, getter) => {
|
| 6 |
+
__accessCheck(obj, member, "read from private field");
|
| 7 |
+
return getter ? getter.call(obj) : member.get(obj);
|
| 8 |
+
};
|
| 9 |
+
var __privateAdd = (obj, member, value) => {
|
| 10 |
+
if (member.has(obj))
|
| 11 |
+
throw TypeError("Cannot add the same private member more than once");
|
| 12 |
+
member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
| 13 |
+
};
|
| 14 |
+
var __privateSet = (obj, member, value, setter) => {
|
| 15 |
+
__accessCheck(obj, member, "write to private field");
|
| 16 |
+
setter ? setter.call(obj, value) : member.set(obj, value);
|
| 17 |
+
return value;
|
| 18 |
+
};
|
| 19 |
+
var __privateWrapper = (obj, member, setter, getter) => ({
|
| 20 |
+
set _(value) {
|
| 21 |
+
__privateSet(obj, member, value, setter);
|
| 22 |
+
},
|
| 23 |
+
get _() {
|
| 24 |
+
return __privateGet(obj, member, getter);
|
| 25 |
+
}
|
| 26 |
+
});
|
| 27 |
+
var __privateMethod = (obj, member, method) => {
|
| 28 |
+
__accessCheck(obj, member, "access private method");
|
| 29 |
+
return method;
|
| 30 |
+
};
|
| 31 |
+
|
| 32 |
+
// src/misc.ts
|
| 33 |
+
var bytes = new Uint8Array(8);
|
| 34 |
+
var view = new DataView(bytes.buffer);
|
| 35 |
+
var u8 = (value) => {
|
| 36 |
+
return [(value % 256 + 256) % 256];
|
| 37 |
+
};
|
| 38 |
+
var u16 = (value) => {
|
| 39 |
+
view.setUint16(0, value, false);
|
| 40 |
+
return [bytes[0], bytes[1]];
|
| 41 |
+
};
|
| 42 |
+
var i16 = (value) => {
|
| 43 |
+
view.setInt16(0, value, false);
|
| 44 |
+
return [bytes[0], bytes[1]];
|
| 45 |
+
};
|
| 46 |
+
var u24 = (value) => {
|
| 47 |
+
view.setUint32(0, value, false);
|
| 48 |
+
return [bytes[1], bytes[2], bytes[3]];
|
| 49 |
+
};
|
| 50 |
+
var u32 = (value) => {
|
| 51 |
+
view.setUint32(0, value, false);
|
| 52 |
+
return [bytes[0], bytes[1], bytes[2], bytes[3]];
|
| 53 |
+
};
|
| 54 |
+
var i32 = (value) => {
|
| 55 |
+
view.setInt32(0, value, false);
|
| 56 |
+
return [bytes[0], bytes[1], bytes[2], bytes[3]];
|
| 57 |
+
};
|
| 58 |
+
var u64 = (value) => {
|
| 59 |
+
view.setUint32(0, Math.floor(value / 2 ** 32), false);
|
| 60 |
+
view.setUint32(4, value, false);
|
| 61 |
+
return [bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7]];
|
| 62 |
+
};
|
| 63 |
+
var fixed_8_8 = (value) => {
|
| 64 |
+
view.setInt16(0, 2 ** 8 * value, false);
|
| 65 |
+
return [bytes[0], bytes[1]];
|
| 66 |
+
};
|
| 67 |
+
var fixed_16_16 = (value) => {
|
| 68 |
+
view.setInt32(0, 2 ** 16 * value, false);
|
| 69 |
+
return [bytes[0], bytes[1], bytes[2], bytes[3]];
|
| 70 |
+
};
|
| 71 |
+
var fixed_2_30 = (value) => {
|
| 72 |
+
view.setInt32(0, 2 ** 30 * value, false);
|
| 73 |
+
return [bytes[0], bytes[1], bytes[2], bytes[3]];
|
| 74 |
+
};
|
| 75 |
+
var ascii = (text, nullTerminated = false) => {
|
| 76 |
+
let bytes2 = Array(text.length).fill(null).map((_, i) => text.charCodeAt(i));
|
| 77 |
+
if (nullTerminated)
|
| 78 |
+
bytes2.push(0);
|
| 79 |
+
return bytes2;
|
| 80 |
+
};
|
| 81 |
+
var last = (arr) => {
|
| 82 |
+
return arr && arr[arr.length - 1];
|
| 83 |
+
};
|
| 84 |
+
var lastPresentedSample = (samples) => {
|
| 85 |
+
let result = void 0;
|
| 86 |
+
for (let sample of samples) {
|
| 87 |
+
if (!result || sample.presentationTimestamp > result.presentationTimestamp) {
|
| 88 |
+
result = sample;
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
return result;
|
| 92 |
+
};
|
| 93 |
+
var intoTimescale = (timeInSeconds, timescale, round = true) => {
|
| 94 |
+
let value = timeInSeconds * timescale;
|
| 95 |
+
return round ? Math.round(value) : value;
|
| 96 |
+
};
|
| 97 |
+
var rotationMatrix = (rotationInDegrees) => {
|
| 98 |
+
let theta = rotationInDegrees * (Math.PI / 180);
|
| 99 |
+
let cosTheta = Math.cos(theta);
|
| 100 |
+
let sinTheta = Math.sin(theta);
|
| 101 |
+
return [
|
| 102 |
+
cosTheta,
|
| 103 |
+
sinTheta,
|
| 104 |
+
0,
|
| 105 |
+
-sinTheta,
|
| 106 |
+
cosTheta,
|
| 107 |
+
0,
|
| 108 |
+
0,
|
| 109 |
+
0,
|
| 110 |
+
1
|
| 111 |
+
];
|
| 112 |
+
};
|
| 113 |
+
var IDENTITY_MATRIX = rotationMatrix(0);
|
| 114 |
+
var matrixToBytes = (matrix) => {
|
| 115 |
+
return [
|
| 116 |
+
fixed_16_16(matrix[0]),
|
| 117 |
+
fixed_16_16(matrix[1]),
|
| 118 |
+
fixed_2_30(matrix[2]),
|
| 119 |
+
fixed_16_16(matrix[3]),
|
| 120 |
+
fixed_16_16(matrix[4]),
|
| 121 |
+
fixed_2_30(matrix[5]),
|
| 122 |
+
fixed_16_16(matrix[6]),
|
| 123 |
+
fixed_16_16(matrix[7]),
|
| 124 |
+
fixed_2_30(matrix[8])
|
| 125 |
+
];
|
| 126 |
+
};
|
| 127 |
+
var deepClone = (x) => {
|
| 128 |
+
if (!x)
|
| 129 |
+
return x;
|
| 130 |
+
if (typeof x !== "object")
|
| 131 |
+
return x;
|
| 132 |
+
if (Array.isArray(x))
|
| 133 |
+
return x.map(deepClone);
|
| 134 |
+
return Object.fromEntries(Object.entries(x).map(([key, value]) => [key, deepClone(value)]));
|
| 135 |
+
};
|
| 136 |
+
var isU32 = (value) => {
|
| 137 |
+
return value >= 0 && value < 2 ** 32;
|
| 138 |
+
};
|
| 139 |
+
|
| 140 |
+
// src/box.ts
|
| 141 |
+
var box = (type, contents, children) => ({
|
| 142 |
+
type,
|
| 143 |
+
contents: contents && new Uint8Array(contents.flat(10)),
|
| 144 |
+
children
|
| 145 |
+
});
|
| 146 |
+
var fullBox = (type, version, flags, contents, children) => box(
|
| 147 |
+
type,
|
| 148 |
+
[u8(version), u24(flags), contents ?? []],
|
| 149 |
+
children
|
| 150 |
+
);
|
| 151 |
+
var ftyp = (details) => {
|
| 152 |
+
let minorVersion = 512;
|
| 153 |
+
if (details.fragmented)
|
| 154 |
+
return box("ftyp", [
|
| 155 |
+
ascii("iso5"),
|
| 156 |
+
// Major brand
|
| 157 |
+
u32(minorVersion),
|
| 158 |
+
// Minor version
|
| 159 |
+
// Compatible brands
|
| 160 |
+
ascii("iso5"),
|
| 161 |
+
ascii("iso6"),
|
| 162 |
+
ascii("mp41")
|
| 163 |
+
]);
|
| 164 |
+
return box("ftyp", [
|
| 165 |
+
ascii("isom"),
|
| 166 |
+
// Major brand
|
| 167 |
+
u32(minorVersion),
|
| 168 |
+
// Minor version
|
| 169 |
+
// Compatible brands
|
| 170 |
+
ascii("isom"),
|
| 171 |
+
details.holdsAvc ? ascii("avc1") : [],
|
| 172 |
+
ascii("mp41")
|
| 173 |
+
]);
|
| 174 |
+
};
|
| 175 |
+
var mdat = (reserveLargeSize) => ({ type: "mdat", largeSize: reserveLargeSize });
|
| 176 |
+
var free = (size) => ({ type: "free", size });
|
| 177 |
+
var moov = (tracks, creationTime, fragmented = false) => box("moov", null, [
|
| 178 |
+
mvhd(creationTime, tracks),
|
| 179 |
+
...tracks.map((x) => trak(x, creationTime)),
|
| 180 |
+
fragmented ? mvex(tracks) : null
|
| 181 |
+
]);
|
| 182 |
+
var mvhd = (creationTime, tracks) => {
|
| 183 |
+
let duration = intoTimescale(Math.max(
|
| 184 |
+
0,
|
| 185 |
+
...tracks.filter((x) => x.samples.length > 0).map((x) => {
|
| 186 |
+
const lastSample = lastPresentedSample(x.samples);
|
| 187 |
+
return lastSample.presentationTimestamp + lastSample.duration;
|
| 188 |
+
})
|
| 189 |
+
), GLOBAL_TIMESCALE);
|
| 190 |
+
let nextTrackId = Math.max(...tracks.map((x) => x.id)) + 1;
|
| 191 |
+
let needsU64 = !isU32(creationTime) || !isU32(duration);
|
| 192 |
+
let u32OrU64 = needsU64 ? u64 : u32;
|
| 193 |
+
return fullBox("mvhd", +needsU64, 0, [
|
| 194 |
+
u32OrU64(creationTime),
|
| 195 |
+
// Creation time
|
| 196 |
+
u32OrU64(creationTime),
|
| 197 |
+
// Modification time
|
| 198 |
+
u32(GLOBAL_TIMESCALE),
|
| 199 |
+
// Timescale
|
| 200 |
+
u32OrU64(duration),
|
| 201 |
+
// Duration
|
| 202 |
+
fixed_16_16(1),
|
| 203 |
+
// Preferred rate
|
| 204 |
+
fixed_8_8(1),
|
| 205 |
+
// Preferred volume
|
| 206 |
+
Array(10).fill(0),
|
| 207 |
+
// Reserved
|
| 208 |
+
matrixToBytes(IDENTITY_MATRIX),
|
| 209 |
+
// Matrix
|
| 210 |
+
Array(24).fill(0),
|
| 211 |
+
// Pre-defined
|
| 212 |
+
u32(nextTrackId)
|
| 213 |
+
// Next track ID
|
| 214 |
+
]);
|
| 215 |
+
};
|
| 216 |
+
var trak = (track, creationTime) => box("trak", null, [
|
| 217 |
+
tkhd(track, creationTime),
|
| 218 |
+
mdia(track, creationTime)
|
| 219 |
+
]);
|
| 220 |
+
var tkhd = (track, creationTime) => {
|
| 221 |
+
let lastSample = lastPresentedSample(track.samples);
|
| 222 |
+
let durationInGlobalTimescale = intoTimescale(
|
| 223 |
+
lastSample ? lastSample.presentationTimestamp + lastSample.duration : 0,
|
| 224 |
+
GLOBAL_TIMESCALE
|
| 225 |
+
);
|
| 226 |
+
let needsU64 = !isU32(creationTime) || !isU32(durationInGlobalTimescale);
|
| 227 |
+
let u32OrU64 = needsU64 ? u64 : u32;
|
| 228 |
+
let matrix;
|
| 229 |
+
if (track.info.type === "video") {
|
| 230 |
+
matrix = typeof track.info.rotation === "number" ? rotationMatrix(track.info.rotation) : track.info.rotation;
|
| 231 |
+
} else {
|
| 232 |
+
matrix = IDENTITY_MATRIX;
|
| 233 |
+
}
|
| 234 |
+
return fullBox("tkhd", +needsU64, 3, [
|
| 235 |
+
u32OrU64(creationTime),
|
| 236 |
+
// Creation time
|
| 237 |
+
u32OrU64(creationTime),
|
| 238 |
+
// Modification time
|
| 239 |
+
u32(track.id),
|
| 240 |
+
// Track ID
|
| 241 |
+
u32(0),
|
| 242 |
+
// Reserved
|
| 243 |
+
u32OrU64(durationInGlobalTimescale),
|
| 244 |
+
// Duration
|
| 245 |
+
Array(8).fill(0),
|
| 246 |
+
// Reserved
|
| 247 |
+
u16(0),
|
| 248 |
+
// Layer
|
| 249 |
+
u16(0),
|
| 250 |
+
// Alternate group
|
| 251 |
+
fixed_8_8(track.info.type === "audio" ? 1 : 0),
|
| 252 |
+
// Volume
|
| 253 |
+
u16(0),
|
| 254 |
+
// Reserved
|
| 255 |
+
matrixToBytes(matrix),
|
| 256 |
+
// Matrix
|
| 257 |
+
fixed_16_16(track.info.type === "video" ? track.info.width : 0),
|
| 258 |
+
// Track width
|
| 259 |
+
fixed_16_16(track.info.type === "video" ? track.info.height : 0)
|
| 260 |
+
// Track height
|
| 261 |
+
]);
|
| 262 |
+
};
|
| 263 |
+
var mdia = (track, creationTime) => box("mdia", null, [
|
| 264 |
+
mdhd(track, creationTime),
|
| 265 |
+
hdlr(track.info.type === "video" ? "vide" : "soun"),
|
| 266 |
+
minf(track)
|
| 267 |
+
]);
|
| 268 |
+
var mdhd = (track, creationTime) => {
|
| 269 |
+
let lastSample = lastPresentedSample(track.samples);
|
| 270 |
+
let localDuration = intoTimescale(
|
| 271 |
+
lastSample ? lastSample.presentationTimestamp + lastSample.duration : 0,
|
| 272 |
+
track.timescale
|
| 273 |
+
);
|
| 274 |
+
let needsU64 = !isU32(creationTime) || !isU32(localDuration);
|
| 275 |
+
let u32OrU64 = needsU64 ? u64 : u32;
|
| 276 |
+
return fullBox("mdhd", +needsU64, 0, [
|
| 277 |
+
u32OrU64(creationTime),
|
| 278 |
+
// Creation time
|
| 279 |
+
u32OrU64(creationTime),
|
| 280 |
+
// Modification time
|
| 281 |
+
u32(track.timescale),
|
| 282 |
+
// Timescale
|
| 283 |
+
u32OrU64(localDuration),
|
| 284 |
+
// Duration
|
| 285 |
+
u16(21956),
|
| 286 |
+
// Language ("und", undetermined)
|
| 287 |
+
u16(0)
|
| 288 |
+
// Quality
|
| 289 |
+
]);
|
| 290 |
+
};
|
| 291 |
+
var hdlr = (componentSubtype) => fullBox("hdlr", 0, 0, [
|
| 292 |
+
ascii("mhlr"),
|
| 293 |
+
// Component type
|
| 294 |
+
ascii(componentSubtype),
|
| 295 |
+
// Component subtype
|
| 296 |
+
u32(0),
|
| 297 |
+
// Component manufacturer
|
| 298 |
+
u32(0),
|
| 299 |
+
// Component flags
|
| 300 |
+
u32(0),
|
| 301 |
+
// Component flags mask
|
| 302 |
+
ascii("mp4-muxer-hdlr", true)
|
| 303 |
+
// Component name
|
| 304 |
+
]);
|
| 305 |
+
var minf = (track) => box("minf", null, [
|
| 306 |
+
track.info.type === "video" ? vmhd() : smhd(),
|
| 307 |
+
dinf(),
|
| 308 |
+
stbl(track)
|
| 309 |
+
]);
|
| 310 |
+
var vmhd = () => fullBox("vmhd", 0, 1, [
|
| 311 |
+
u16(0),
|
| 312 |
+
// Graphics mode
|
| 313 |
+
u16(0),
|
| 314 |
+
// Opcolor R
|
| 315 |
+
u16(0),
|
| 316 |
+
// Opcolor G
|
| 317 |
+
u16(0)
|
| 318 |
+
// Opcolor B
|
| 319 |
+
]);
|
| 320 |
+
var smhd = () => fullBox("smhd", 0, 0, [
|
| 321 |
+
u16(0),
|
| 322 |
+
// Balance
|
| 323 |
+
u16(0)
|
| 324 |
+
// Reserved
|
| 325 |
+
]);
|
| 326 |
+
var dinf = () => box("dinf", null, [
|
| 327 |
+
dref()
|
| 328 |
+
]);
|
| 329 |
+
var dref = () => fullBox("dref", 0, 0, [
|
| 330 |
+
u32(1)
|
| 331 |
+
// Entry count
|
| 332 |
+
], [
|
| 333 |
+
url()
|
| 334 |
+
]);
|
| 335 |
+
var url = () => fullBox("url ", 0, 1);
|
| 336 |
+
var stbl = (track) => {
|
| 337 |
+
const needsCtts = track.compositionTimeOffsetTable.length > 1 || track.compositionTimeOffsetTable.some((x) => x.sampleCompositionTimeOffset !== 0);
|
| 338 |
+
return box("stbl", null, [
|
| 339 |
+
stsd(track),
|
| 340 |
+
stts(track),
|
| 341 |
+
stss(track),
|
| 342 |
+
stsc(track),
|
| 343 |
+
stsz(track),
|
| 344 |
+
stco(track),
|
| 345 |
+
needsCtts ? ctts(track) : null
|
| 346 |
+
]);
|
| 347 |
+
};
|
| 348 |
+
var stsd = (track) => fullBox("stsd", 0, 0, [
|
| 349 |
+
u32(1)
|
| 350 |
+
// Entry count
|
| 351 |
+
], [
|
| 352 |
+
track.info.type === "video" ? videoSampleDescription(
|
| 353 |
+
VIDEO_CODEC_TO_BOX_NAME[track.info.codec],
|
| 354 |
+
track
|
| 355 |
+
) : soundSampleDescription(
|
| 356 |
+
AUDIO_CODEC_TO_BOX_NAME[track.info.codec],
|
| 357 |
+
track
|
| 358 |
+
)
|
| 359 |
+
]);
|
| 360 |
+
var videoSampleDescription = (compressionType, track) => box(compressionType, [
|
| 361 |
+
Array(6).fill(0),
|
| 362 |
+
// Reserved
|
| 363 |
+
u16(1),
|
| 364 |
+
// Data reference index
|
| 365 |
+
u16(0),
|
| 366 |
+
// Pre-defined
|
| 367 |
+
u16(0),
|
| 368 |
+
// Reserved
|
| 369 |
+
Array(12).fill(0),
|
| 370 |
+
// Pre-defined
|
| 371 |
+
u16(track.info.width),
|
| 372 |
+
// Width
|
| 373 |
+
u16(track.info.height),
|
| 374 |
+
// Height
|
| 375 |
+
u32(4718592),
|
| 376 |
+
// Horizontal resolution
|
| 377 |
+
u32(4718592),
|
| 378 |
+
// Vertical resolution
|
| 379 |
+
u32(0),
|
| 380 |
+
// Reserved
|
| 381 |
+
u16(1),
|
| 382 |
+
// Frame count
|
| 383 |
+
Array(32).fill(0),
|
| 384 |
+
// Compressor name
|
| 385 |
+
u16(24),
|
| 386 |
+
// Depth
|
| 387 |
+
i16(65535)
|
| 388 |
+
// Pre-defined
|
| 389 |
+
], [
|
| 390 |
+
VIDEO_CODEC_TO_CONFIGURATION_BOX[track.info.codec](track),
|
| 391 |
+
track.info.decoderConfig.colorSpace ? colr(track) : null
|
| 392 |
+
]);
|
| 393 |
+
var COLOR_PRIMARIES_MAP = {
|
| 394 |
+
"bt709": 1,
|
| 395 |
+
// ITU-R BT.709
|
| 396 |
+
"bt470bg": 5,
|
| 397 |
+
// ITU-R BT.470BG
|
| 398 |
+
"smpte170m": 6
|
| 399 |
+
// ITU-R BT.601 525 - SMPTE 170M
|
| 400 |
+
};
|
| 401 |
+
var TRANSFER_CHARACTERISTICS_MAP = {
|
| 402 |
+
"bt709": 1,
|
| 403 |
+
// ITU-R BT.709
|
| 404 |
+
"smpte170m": 6,
|
| 405 |
+
// SMPTE 170M
|
| 406 |
+
"iec61966-2-1": 13
|
| 407 |
+
// IEC 61966-2-1
|
| 408 |
+
};
|
| 409 |
+
var MATRIX_COEFFICIENTS_MAP = {
|
| 410 |
+
"rgb": 0,
|
| 411 |
+
// Identity
|
| 412 |
+
"bt709": 1,
|
| 413 |
+
// ITU-R BT.709
|
| 414 |
+
"bt470bg": 5,
|
| 415 |
+
// ITU-R BT.470BG
|
| 416 |
+
"smpte170m": 6
|
| 417 |
+
// SMPTE 170M
|
| 418 |
+
};
|
| 419 |
+
var colr = (track) => box("colr", [
|
| 420 |
+
ascii("nclx"),
|
| 421 |
+
// Colour type
|
| 422 |
+
u16(COLOR_PRIMARIES_MAP[track.info.decoderConfig.colorSpace.primaries]),
|
| 423 |
+
// Colour primaries
|
| 424 |
+
u16(TRANSFER_CHARACTERISTICS_MAP[track.info.decoderConfig.colorSpace.transfer]),
|
| 425 |
+
// Transfer characteristics
|
| 426 |
+
u16(MATRIX_COEFFICIENTS_MAP[track.info.decoderConfig.colorSpace.matrix]),
|
| 427 |
+
// Matrix coefficients
|
| 428 |
+
u8((track.info.decoderConfig.colorSpace.fullRange ? 1 : 0) << 7)
|
| 429 |
+
// Full range flag
|
| 430 |
+
]);
|
| 431 |
+
var avcC = (track) => track.info.decoderConfig && box("avcC", [
|
| 432 |
+
// For AVC, description is an AVCDecoderConfigurationRecord, so nothing else to do here
|
| 433 |
+
...new Uint8Array(track.info.decoderConfig.description)
|
| 434 |
+
]);
|
| 435 |
+
var hvcC = (track) => track.info.decoderConfig && box("hvcC", [
|
| 436 |
+
// For HEVC, description is a HEVCDecoderConfigurationRecord, so nothing else to do here
|
| 437 |
+
...new Uint8Array(track.info.decoderConfig.description)
|
| 438 |
+
]);
|
| 439 |
+
var vpcC = (track) => {
|
| 440 |
+
if (!track.info.decoderConfig) {
|
| 441 |
+
return null;
|
| 442 |
+
}
|
| 443 |
+
let decoderConfig = track.info.decoderConfig;
|
| 444 |
+
if (!decoderConfig.colorSpace) {
|
| 445 |
+
throw new Error(`'colorSpace' is required in the decoder config for VP9.`);
|
| 446 |
+
}
|
| 447 |
+
let parts = decoderConfig.codec.split(".");
|
| 448 |
+
let profile = Number(parts[1]);
|
| 449 |
+
let level = Number(parts[2]);
|
| 450 |
+
let bitDepth = Number(parts[3]);
|
| 451 |
+
let chromaSubsampling = 0;
|
| 452 |
+
let thirdByte = (bitDepth << 4) + (chromaSubsampling << 1) + Number(decoderConfig.colorSpace.fullRange);
|
| 453 |
+
let colourPrimaries = 2;
|
| 454 |
+
let transferCharacteristics = 2;
|
| 455 |
+
let matrixCoefficients = 2;
|
| 456 |
+
return fullBox("vpcC", 1, 0, [
|
| 457 |
+
u8(profile),
|
| 458 |
+
// Profile
|
| 459 |
+
u8(level),
|
| 460 |
+
// Level
|
| 461 |
+
u8(thirdByte),
|
| 462 |
+
// Bit depth, chroma subsampling, full range
|
| 463 |
+
u8(colourPrimaries),
|
| 464 |
+
// Colour primaries
|
| 465 |
+
u8(transferCharacteristics),
|
| 466 |
+
// Transfer characteristics
|
| 467 |
+
u8(matrixCoefficients),
|
| 468 |
+
// Matrix coefficients
|
| 469 |
+
u16(0)
|
| 470 |
+
// Codec initialization data size
|
| 471 |
+
]);
|
| 472 |
+
};
|
| 473 |
+
var av1C = () => {
|
| 474 |
+
let marker = 1;
|
| 475 |
+
let version = 1;
|
| 476 |
+
let firstByte = (marker << 7) + version;
|
| 477 |
+
return box("av1C", [
|
| 478 |
+
firstByte,
|
| 479 |
+
0,
|
| 480 |
+
0,
|
| 481 |
+
0
|
| 482 |
+
]);
|
| 483 |
+
};
|
| 484 |
+
var soundSampleDescription = (compressionType, track) => box(compressionType, [
|
| 485 |
+
Array(6).fill(0),
|
| 486 |
+
// Reserved
|
| 487 |
+
u16(1),
|
| 488 |
+
// Data reference index
|
| 489 |
+
u16(0),
|
| 490 |
+
// Version
|
| 491 |
+
u16(0),
|
| 492 |
+
// Revision level
|
| 493 |
+
u32(0),
|
| 494 |
+
// Vendor
|
| 495 |
+
u16(track.info.numberOfChannels),
|
| 496 |
+
// Number of channels
|
| 497 |
+
u16(16),
|
| 498 |
+
// Sample size (bits)
|
| 499 |
+
u16(0),
|
| 500 |
+
// Compression ID
|
| 501 |
+
u16(0),
|
| 502 |
+
// Packet size
|
| 503 |
+
fixed_16_16(track.info.sampleRate)
|
| 504 |
+
// Sample rate
|
| 505 |
+
], [
|
| 506 |
+
AUDIO_CODEC_TO_CONFIGURATION_BOX[track.info.codec](track)
|
| 507 |
+
]);
|
| 508 |
+
var esds = (track) => {
|
| 509 |
+
let description = new Uint8Array(track.info.decoderConfig.description);
|
| 510 |
+
return fullBox("esds", 0, 0, [
|
| 511 |
+
// https://stackoverflow.com/a/54803118
|
| 512 |
+
u32(58753152),
|
| 513 |
+
// TAG(3) = Object Descriptor ([2])
|
| 514 |
+
u8(32 + description.byteLength),
|
| 515 |
+
// length of this OD (which includes the next 2 tags)
|
| 516 |
+
u16(1),
|
| 517 |
+
// ES_ID = 1
|
| 518 |
+
u8(0),
|
| 519 |
+
// flags etc = 0
|
| 520 |
+
u32(75530368),
|
| 521 |
+
// TAG(4) = ES Descriptor ([2]) embedded in above OD
|
| 522 |
+
u8(18 + description.byteLength),
|
| 523 |
+
// length of this ESD
|
| 524 |
+
u8(64),
|
| 525 |
+
// MPEG-4 Audio
|
| 526 |
+
u8(21),
|
| 527 |
+
// stream type(6bits)=5 audio, flags(2bits)=1
|
| 528 |
+
u24(0),
|
| 529 |
+
// 24bit buffer size
|
| 530 |
+
u32(130071),
|
| 531 |
+
// max bitrate
|
| 532 |
+
u32(130071),
|
| 533 |
+
// avg bitrate
|
| 534 |
+
u32(92307584),
|
| 535 |
+
// TAG(5) = ASC ([2],[3]) embedded in above OD
|
| 536 |
+
u8(description.byteLength),
|
| 537 |
+
// length
|
| 538 |
+
...description,
|
| 539 |
+
u32(109084800),
|
| 540 |
+
// TAG(6)
|
| 541 |
+
u8(1),
|
| 542 |
+
// length
|
| 543 |
+
u8(2)
|
| 544 |
+
// data
|
| 545 |
+
]);
|
| 546 |
+
};
|
| 547 |
+
var dOps = (track) => {
|
| 548 |
+
let preskip = 3840;
|
| 549 |
+
let gain = 0;
|
| 550 |
+
const description = track.info.decoderConfig?.description;
|
| 551 |
+
if (description) {
|
| 552 |
+
if (description.byteLength < 18) {
|
| 553 |
+
throw new TypeError("Invalid decoder description provided for Opus; must be at least 18 bytes long.");
|
| 554 |
+
}
|
| 555 |
+
const view2 = ArrayBuffer.isView(description) ? new DataView(description.buffer, description.byteOffset, description.byteLength) : new DataView(description);
|
| 556 |
+
preskip = view2.getUint16(10, true);
|
| 557 |
+
gain = view2.getInt16(14, true);
|
| 558 |
+
}
|
| 559 |
+
return box("dOps", [
|
| 560 |
+
u8(0),
|
| 561 |
+
// Version
|
| 562 |
+
u8(track.info.numberOfChannels),
|
| 563 |
+
// OutputChannelCount
|
| 564 |
+
u16(preskip),
|
| 565 |
+
u32(track.info.sampleRate),
|
| 566 |
+
// InputSampleRate
|
| 567 |
+
fixed_8_8(gain),
|
| 568 |
+
// OutputGain
|
| 569 |
+
u8(0)
|
| 570 |
+
// ChannelMappingFamily
|
| 571 |
+
]);
|
| 572 |
+
};
|
| 573 |
+
var stts = (track) => {
|
| 574 |
+
return fullBox("stts", 0, 0, [
|
| 575 |
+
u32(track.timeToSampleTable.length),
|
| 576 |
+
// Number of entries
|
| 577 |
+
track.timeToSampleTable.map((x) => [
|
| 578 |
+
// Time-to-sample table
|
| 579 |
+
u32(x.sampleCount),
|
| 580 |
+
// Sample count
|
| 581 |
+
u32(x.sampleDelta)
|
| 582 |
+
// Sample duration
|
| 583 |
+
])
|
| 584 |
+
]);
|
| 585 |
+
};
|
| 586 |
+
var stss = (track) => {
|
| 587 |
+
if (track.samples.every((x) => x.type === "key"))
|
| 588 |
+
return null;
|
| 589 |
+
let keySamples = [...track.samples.entries()].filter(([, sample]) => sample.type === "key");
|
| 590 |
+
return fullBox("stss", 0, 0, [
|
| 591 |
+
u32(keySamples.length),
|
| 592 |
+
// Number of entries
|
| 593 |
+
keySamples.map(([index]) => u32(index + 1))
|
| 594 |
+
// Sync sample table
|
| 595 |
+
]);
|
| 596 |
+
};
|
| 597 |
+
var stsc = (track) => {
|
| 598 |
+
return fullBox("stsc", 0, 0, [
|
| 599 |
+
u32(track.compactlyCodedChunkTable.length),
|
| 600 |
+
// Number of entries
|
| 601 |
+
track.compactlyCodedChunkTable.map((x) => [
|
| 602 |
+
// Sample-to-chunk table
|
| 603 |
+
u32(x.firstChunk),
|
| 604 |
+
// First chunk
|
| 605 |
+
u32(x.samplesPerChunk),
|
| 606 |
+
// Samples per chunk
|
| 607 |
+
u32(1)
|
| 608 |
+
// Sample description index
|
| 609 |
+
])
|
| 610 |
+
]);
|
| 611 |
+
};
|
| 612 |
+
var stsz = (track) => fullBox("stsz", 0, 0, [
|
| 613 |
+
u32(0),
|
| 614 |
+
// Sample size (0 means non-constant size)
|
| 615 |
+
u32(track.samples.length),
|
| 616 |
+
// Number of entries
|
| 617 |
+
track.samples.map((x) => u32(x.size))
|
| 618 |
+
// Sample size table
|
| 619 |
+
]);
|
| 620 |
+
var stco = (track) => {
|
| 621 |
+
if (track.finalizedChunks.length > 0 && last(track.finalizedChunks).offset >= 2 ** 32) {
|
| 622 |
+
return fullBox("co64", 0, 0, [
|
| 623 |
+
u32(track.finalizedChunks.length),
|
| 624 |
+
// Number of entries
|
| 625 |
+
track.finalizedChunks.map((x) => u64(x.offset))
|
| 626 |
+
// Chunk offset table
|
| 627 |
+
]);
|
| 628 |
+
}
|
| 629 |
+
return fullBox("stco", 0, 0, [
|
| 630 |
+
u32(track.finalizedChunks.length),
|
| 631 |
+
// Number of entries
|
| 632 |
+
track.finalizedChunks.map((x) => u32(x.offset))
|
| 633 |
+
// Chunk offset table
|
| 634 |
+
]);
|
| 635 |
+
};
|
| 636 |
+
var ctts = (track) => {
|
| 637 |
+
return fullBox("ctts", 0, 0, [
|
| 638 |
+
u32(track.compositionTimeOffsetTable.length),
|
| 639 |
+
// Number of entries
|
| 640 |
+
track.compositionTimeOffsetTable.map((x) => [
|
| 641 |
+
// Time-to-sample table
|
| 642 |
+
u32(x.sampleCount),
|
| 643 |
+
// Sample count
|
| 644 |
+
u32(x.sampleCompositionTimeOffset)
|
| 645 |
+
// Sample offset
|
| 646 |
+
])
|
| 647 |
+
]);
|
| 648 |
+
};
|
| 649 |
+
var mvex = (tracks) => {
|
| 650 |
+
return box("mvex", null, tracks.map(trex));
|
| 651 |
+
};
|
| 652 |
+
var trex = (track) => {
|
| 653 |
+
return fullBox("trex", 0, 0, [
|
| 654 |
+
u32(track.id),
|
| 655 |
+
// Track ID
|
| 656 |
+
u32(1),
|
| 657 |
+
// Default sample description index
|
| 658 |
+
u32(0),
|
| 659 |
+
// Default sample duration
|
| 660 |
+
u32(0),
|
| 661 |
+
// Default sample size
|
| 662 |
+
u32(0)
|
| 663 |
+
// Default sample flags
|
| 664 |
+
]);
|
| 665 |
+
};
|
| 666 |
+
var moof = (sequenceNumber, tracks) => {
|
| 667 |
+
return box("moof", null, [
|
| 668 |
+
mfhd(sequenceNumber),
|
| 669 |
+
...tracks.map(traf)
|
| 670 |
+
]);
|
| 671 |
+
};
|
| 672 |
+
var mfhd = (sequenceNumber) => {
|
| 673 |
+
return fullBox("mfhd", 0, 0, [
|
| 674 |
+
u32(sequenceNumber)
|
| 675 |
+
// Sequence number
|
| 676 |
+
]);
|
| 677 |
+
};
|
| 678 |
+
var fragmentSampleFlags = (sample) => {
|
| 679 |
+
let byte1 = 0;
|
| 680 |
+
let byte2 = 0;
|
| 681 |
+
let byte3 = 0;
|
| 682 |
+
let byte4 = 0;
|
| 683 |
+
let sampleIsDifferenceSample = sample.type === "delta";
|
| 684 |
+
byte2 |= +sampleIsDifferenceSample;
|
| 685 |
+
if (sampleIsDifferenceSample) {
|
| 686 |
+
byte1 |= 1;
|
| 687 |
+
} else {
|
| 688 |
+
byte1 |= 2;
|
| 689 |
+
}
|
| 690 |
+
return byte1 << 24 | byte2 << 16 | byte3 << 8 | byte4;
|
| 691 |
+
};
|
| 692 |
+
var traf = (track) => {
|
| 693 |
+
return box("traf", null, [
|
| 694 |
+
tfhd(track),
|
| 695 |
+
tfdt(track),
|
| 696 |
+
trun(track)
|
| 697 |
+
]);
|
| 698 |
+
};
|
| 699 |
+
var tfhd = (track) => {
|
| 700 |
+
let tfFlags = 0;
|
| 701 |
+
tfFlags |= 8;
|
| 702 |
+
tfFlags |= 16;
|
| 703 |
+
tfFlags |= 32;
|
| 704 |
+
tfFlags |= 131072;
|
| 705 |
+
let referenceSample = track.currentChunk.samples[1] ?? track.currentChunk.samples[0];
|
| 706 |
+
let referenceSampleInfo = {
|
| 707 |
+
duration: referenceSample.timescaleUnitsToNextSample,
|
| 708 |
+
size: referenceSample.size,
|
| 709 |
+
flags: fragmentSampleFlags(referenceSample)
|
| 710 |
+
};
|
| 711 |
+
return fullBox("tfhd", 0, tfFlags, [
|
| 712 |
+
u32(track.id),
|
| 713 |
+
// Track ID
|
| 714 |
+
u32(referenceSampleInfo.duration),
|
| 715 |
+
// Default sample duration
|
| 716 |
+
u32(referenceSampleInfo.size),
|
| 717 |
+
// Default sample size
|
| 718 |
+
u32(referenceSampleInfo.flags)
|
| 719 |
+
// Default sample flags
|
| 720 |
+
]);
|
| 721 |
+
};
|
| 722 |
+
var tfdt = (track) => {
|
| 723 |
+
return fullBox("tfdt", 1, 0, [
|
| 724 |
+
u64(intoTimescale(track.currentChunk.startTimestamp, track.timescale))
|
| 725 |
+
// Base Media Decode Time
|
| 726 |
+
]);
|
| 727 |
+
};
|
| 728 |
+
var trun = (track) => {
|
| 729 |
+
let allSampleDurations = track.currentChunk.samples.map((x) => x.timescaleUnitsToNextSample);
|
| 730 |
+
let allSampleSizes = track.currentChunk.samples.map((x) => x.size);
|
| 731 |
+
let allSampleFlags = track.currentChunk.samples.map(fragmentSampleFlags);
|
| 732 |
+
let allSampleCompositionTimeOffsets = track.currentChunk.samples.map((x) => intoTimescale(x.presentationTimestamp - x.decodeTimestamp, track.timescale));
|
| 733 |
+
let uniqueSampleDurations = new Set(allSampleDurations);
|
| 734 |
+
let uniqueSampleSizes = new Set(allSampleSizes);
|
| 735 |
+
let uniqueSampleFlags = new Set(allSampleFlags);
|
| 736 |
+
let uniqueSampleCompositionTimeOffsets = new Set(allSampleCompositionTimeOffsets);
|
| 737 |
+
let firstSampleFlagsPresent = uniqueSampleFlags.size === 2 && allSampleFlags[0] !== allSampleFlags[1];
|
| 738 |
+
let sampleDurationPresent = uniqueSampleDurations.size > 1;
|
| 739 |
+
let sampleSizePresent = uniqueSampleSizes.size > 1;
|
| 740 |
+
let sampleFlagsPresent = !firstSampleFlagsPresent && uniqueSampleFlags.size > 1;
|
| 741 |
+
let sampleCompositionTimeOffsetsPresent = uniqueSampleCompositionTimeOffsets.size > 1 || [...uniqueSampleCompositionTimeOffsets].some((x) => x !== 0);
|
| 742 |
+
let flags = 0;
|
| 743 |
+
flags |= 1;
|
| 744 |
+
flags |= 4 * +firstSampleFlagsPresent;
|
| 745 |
+
flags |= 256 * +sampleDurationPresent;
|
| 746 |
+
flags |= 512 * +sampleSizePresent;
|
| 747 |
+
flags |= 1024 * +sampleFlagsPresent;
|
| 748 |
+
flags |= 2048 * +sampleCompositionTimeOffsetsPresent;
|
| 749 |
+
return fullBox("trun", 1, flags, [
|
| 750 |
+
u32(track.currentChunk.samples.length),
|
| 751 |
+
// Sample count
|
| 752 |
+
u32(track.currentChunk.offset - track.currentChunk.moofOffset || 0),
|
| 753 |
+
// Data offset
|
| 754 |
+
firstSampleFlagsPresent ? u32(allSampleFlags[0]) : [],
|
| 755 |
+
track.currentChunk.samples.map((_, i) => [
|
| 756 |
+
sampleDurationPresent ? u32(allSampleDurations[i]) : [],
|
| 757 |
+
// Sample duration
|
| 758 |
+
sampleSizePresent ? u32(allSampleSizes[i]) : [],
|
| 759 |
+
// Sample size
|
| 760 |
+
sampleFlagsPresent ? u32(allSampleFlags[i]) : [],
|
| 761 |
+
// Sample flags
|
| 762 |
+
// Sample composition time offsets
|
| 763 |
+
sampleCompositionTimeOffsetsPresent ? i32(allSampleCompositionTimeOffsets[i]) : []
|
| 764 |
+
])
|
| 765 |
+
]);
|
| 766 |
+
};
|
| 767 |
+
var mfra = (tracks) => {
|
| 768 |
+
return box("mfra", null, [
|
| 769 |
+
...tracks.map(tfra),
|
| 770 |
+
mfro()
|
| 771 |
+
]);
|
| 772 |
+
};
|
| 773 |
+
var tfra = (track, trackIndex) => {
|
| 774 |
+
let version = 1;
|
| 775 |
+
return fullBox("tfra", version, 0, [
|
| 776 |
+
u32(track.id),
|
| 777 |
+
// Track ID
|
| 778 |
+
u32(63),
|
| 779 |
+
// This specifies that traf number, trun number and sample number are 32-bit ints
|
| 780 |
+
u32(track.finalizedChunks.length),
|
| 781 |
+
// Number of entries
|
| 782 |
+
track.finalizedChunks.map((chunk) => [
|
| 783 |
+
u64(intoTimescale(chunk.startTimestamp, track.timescale)),
|
| 784 |
+
// Time
|
| 785 |
+
u64(chunk.moofOffset),
|
| 786 |
+
// moof offset
|
| 787 |
+
u32(trackIndex + 1),
|
| 788 |
+
// traf number
|
| 789 |
+
u32(1),
|
| 790 |
+
// trun number
|
| 791 |
+
u32(1)
|
| 792 |
+
// Sample number
|
| 793 |
+
])
|
| 794 |
+
]);
|
| 795 |
+
};
|
| 796 |
+
var mfro = () => {
|
| 797 |
+
return fullBox("mfro", 0, 0, [
|
| 798 |
+
// This value needs to be overwritten manually from the outside, where the actual size of the enclosing mfra box
|
| 799 |
+
// is known
|
| 800 |
+
u32(0)
|
| 801 |
+
// Size
|
| 802 |
+
]);
|
| 803 |
+
};
|
| 804 |
+
var VIDEO_CODEC_TO_BOX_NAME = {
|
| 805 |
+
"avc": "avc1",
|
| 806 |
+
"hevc": "hvc1",
|
| 807 |
+
"vp9": "vp09",
|
| 808 |
+
"av1": "av01"
|
| 809 |
+
};
|
| 810 |
+
var VIDEO_CODEC_TO_CONFIGURATION_BOX = {
|
| 811 |
+
"avc": avcC,
|
| 812 |
+
"hevc": hvcC,
|
| 813 |
+
"vp9": vpcC,
|
| 814 |
+
"av1": av1C
|
| 815 |
+
};
|
| 816 |
+
var AUDIO_CODEC_TO_BOX_NAME = {
|
| 817 |
+
"aac": "mp4a",
|
| 818 |
+
"opus": "Opus"
|
| 819 |
+
};
|
| 820 |
+
var AUDIO_CODEC_TO_CONFIGURATION_BOX = {
|
| 821 |
+
"aac": esds,
|
| 822 |
+
"opus": dOps
|
| 823 |
+
};
|
| 824 |
+
|
| 825 |
+
// src/target.ts
|
| 826 |
+
var isTarget = Symbol("isTarget");
|
| 827 |
+
var Target = class {
|
| 828 |
+
};
|
| 829 |
+
isTarget;
|
| 830 |
+
var ArrayBufferTarget = class extends Target {
|
| 831 |
+
constructor() {
|
| 832 |
+
super(...arguments);
|
| 833 |
+
this.buffer = null;
|
| 834 |
+
}
|
| 835 |
+
};
|
| 836 |
+
var StreamTarget = class extends Target {
|
| 837 |
+
constructor(options) {
|
| 838 |
+
super();
|
| 839 |
+
this.options = options;
|
| 840 |
+
if (typeof options !== "object") {
|
| 841 |
+
throw new TypeError("StreamTarget requires an options object to be passed to its constructor.");
|
| 842 |
+
}
|
| 843 |
+
if (options.onData) {
|
| 844 |
+
if (typeof options.onData !== "function") {
|
| 845 |
+
throw new TypeError("options.onData, when provided, must be a function.");
|
| 846 |
+
}
|
| 847 |
+
if (options.onData.length < 2) {
|
| 848 |
+
throw new TypeError(
|
| 849 |
+
"options.onData, when provided, must be a function that takes in at least two arguments (data and position). Ignoring the position argument, which specifies the byte offset at which the data is to be written, can lead to broken outputs."
|
| 850 |
+
);
|
| 851 |
+
}
|
| 852 |
+
}
|
| 853 |
+
if (options.chunked !== void 0 && typeof options.chunked !== "boolean") {
|
| 854 |
+
throw new TypeError("options.chunked, when provided, must be a boolean.");
|
| 855 |
+
}
|
| 856 |
+
if (options.chunkSize !== void 0 && (!Number.isInteger(options.chunkSize) || options.chunkSize < 1024)) {
|
| 857 |
+
throw new TypeError("options.chunkSize, when provided, must be an integer and not smaller than 1024.");
|
| 858 |
+
}
|
| 859 |
+
}
|
| 860 |
+
};
|
| 861 |
+
var FileSystemWritableFileStreamTarget = class extends Target {
|
| 862 |
+
constructor(stream, options) {
|
| 863 |
+
super();
|
| 864 |
+
this.stream = stream;
|
| 865 |
+
this.options = options;
|
| 866 |
+
if (!(stream instanceof FileSystemWritableFileStream)) {
|
| 867 |
+
throw new TypeError("FileSystemWritableFileStreamTarget requires a FileSystemWritableFileStream instance.");
|
| 868 |
+
}
|
| 869 |
+
if (options !== void 0 && typeof options !== "object") {
|
| 870 |
+
throw new TypeError("FileSystemWritableFileStreamTarget's options, when provided, must be an object.");
|
| 871 |
+
}
|
| 872 |
+
if (options) {
|
| 873 |
+
if (options.chunkSize !== void 0 && (!Number.isInteger(options.chunkSize) || options.chunkSize <= 0)) {
|
| 874 |
+
throw new TypeError("options.chunkSize, when provided, must be a positive integer");
|
| 875 |
+
}
|
| 876 |
+
}
|
| 877 |
+
}
|
| 878 |
+
};
|
| 879 |
+
|
| 880 |
+
// src/writer.ts
|
| 881 |
+
var _helper, _helperView;
|
| 882 |
+
var Writer = class {
|
| 883 |
+
constructor() {
|
| 884 |
+
this.pos = 0;
|
| 885 |
+
__privateAdd(this, _helper, new Uint8Array(8));
|
| 886 |
+
__privateAdd(this, _helperView, new DataView(__privateGet(this, _helper).buffer));
|
| 887 |
+
/**
|
| 888 |
+
* Stores the position from the start of the file to where boxes elements have been written. This is used to
|
| 889 |
+
* rewrite/edit elements that were already added before, and to measure sizes of things.
|
| 890 |
+
*/
|
| 891 |
+
this.offsets = /* @__PURE__ */ new WeakMap();
|
| 892 |
+
}
|
| 893 |
+
/** Sets the current position for future writes to a new one. */
|
| 894 |
+
seek(newPos) {
|
| 895 |
+
this.pos = newPos;
|
| 896 |
+
}
|
| 897 |
+
writeU32(value) {
|
| 898 |
+
__privateGet(this, _helperView).setUint32(0, value, false);
|
| 899 |
+
this.write(__privateGet(this, _helper).subarray(0, 4));
|
| 900 |
+
}
|
| 901 |
+
writeU64(value) {
|
| 902 |
+
__privateGet(this, _helperView).setUint32(0, Math.floor(value / 2 ** 32), false);
|
| 903 |
+
__privateGet(this, _helperView).setUint32(4, value, false);
|
| 904 |
+
this.write(__privateGet(this, _helper).subarray(0, 8));
|
| 905 |
+
}
|
| 906 |
+
writeAscii(text) {
|
| 907 |
+
for (let i = 0; i < text.length; i++) {
|
| 908 |
+
__privateGet(this, _helperView).setUint8(i % 8, text.charCodeAt(i));
|
| 909 |
+
if (i % 8 === 7)
|
| 910 |
+
this.write(__privateGet(this, _helper));
|
| 911 |
+
}
|
| 912 |
+
if (text.length % 8 !== 0) {
|
| 913 |
+
this.write(__privateGet(this, _helper).subarray(0, text.length % 8));
|
| 914 |
+
}
|
| 915 |
+
}
|
| 916 |
+
writeBox(box2) {
|
| 917 |
+
this.offsets.set(box2, this.pos);
|
| 918 |
+
if (box2.contents && !box2.children) {
|
| 919 |
+
this.writeBoxHeader(box2, box2.size ?? box2.contents.byteLength + 8);
|
| 920 |
+
this.write(box2.contents);
|
| 921 |
+
} else {
|
| 922 |
+
let startPos = this.pos;
|
| 923 |
+
this.writeBoxHeader(box2, 0);
|
| 924 |
+
if (box2.contents)
|
| 925 |
+
this.write(box2.contents);
|
| 926 |
+
if (box2.children) {
|
| 927 |
+
for (let child of box2.children)
|
| 928 |
+
if (child)
|
| 929 |
+
this.writeBox(child);
|
| 930 |
+
}
|
| 931 |
+
let endPos = this.pos;
|
| 932 |
+
let size = box2.size ?? endPos - startPos;
|
| 933 |
+
this.seek(startPos);
|
| 934 |
+
this.writeBoxHeader(box2, size);
|
| 935 |
+
this.seek(endPos);
|
| 936 |
+
}
|
| 937 |
+
}
|
| 938 |
+
writeBoxHeader(box2, size) {
|
| 939 |
+
this.writeU32(box2.largeSize ? 1 : size);
|
| 940 |
+
this.writeAscii(box2.type);
|
| 941 |
+
if (box2.largeSize)
|
| 942 |
+
this.writeU64(size);
|
| 943 |
+
}
|
| 944 |
+
measureBoxHeader(box2) {
|
| 945 |
+
return 8 + (box2.largeSize ? 8 : 0);
|
| 946 |
+
}
|
| 947 |
+
patchBox(box2) {
|
| 948 |
+
let endPos = this.pos;
|
| 949 |
+
this.seek(this.offsets.get(box2));
|
| 950 |
+
this.writeBox(box2);
|
| 951 |
+
this.seek(endPos);
|
| 952 |
+
}
|
| 953 |
+
measureBox(box2) {
|
| 954 |
+
if (box2.contents && !box2.children) {
|
| 955 |
+
let headerSize = this.measureBoxHeader(box2);
|
| 956 |
+
return headerSize + box2.contents.byteLength;
|
| 957 |
+
} else {
|
| 958 |
+
let result = this.measureBoxHeader(box2);
|
| 959 |
+
if (box2.contents)
|
| 960 |
+
result += box2.contents.byteLength;
|
| 961 |
+
if (box2.children) {
|
| 962 |
+
for (let child of box2.children)
|
| 963 |
+
if (child)
|
| 964 |
+
result += this.measureBox(child);
|
| 965 |
+
}
|
| 966 |
+
return result;
|
| 967 |
+
}
|
| 968 |
+
}
|
| 969 |
+
};
|
| 970 |
+
_helper = new WeakMap();
|
| 971 |
+
_helperView = new WeakMap();
|
| 972 |
+
var _target, _buffer, _bytes, _maxPos, _ensureSize, ensureSize_fn;
|
| 973 |
+
var ArrayBufferTargetWriter = class extends Writer {
|
| 974 |
+
constructor(target) {
|
| 975 |
+
super();
|
| 976 |
+
__privateAdd(this, _ensureSize);
|
| 977 |
+
__privateAdd(this, _target, void 0);
|
| 978 |
+
__privateAdd(this, _buffer, new ArrayBuffer(2 ** 16));
|
| 979 |
+
__privateAdd(this, _bytes, new Uint8Array(__privateGet(this, _buffer)));
|
| 980 |
+
__privateAdd(this, _maxPos, 0);
|
| 981 |
+
__privateSet(this, _target, target);
|
| 982 |
+
}
|
| 983 |
+
write(data) {
|
| 984 |
+
__privateMethod(this, _ensureSize, ensureSize_fn).call(this, this.pos + data.byteLength);
|
| 985 |
+
__privateGet(this, _bytes).set(data, this.pos);
|
| 986 |
+
this.pos += data.byteLength;
|
| 987 |
+
__privateSet(this, _maxPos, Math.max(__privateGet(this, _maxPos), this.pos));
|
| 988 |
+
}
|
| 989 |
+
finalize() {
|
| 990 |
+
__privateMethod(this, _ensureSize, ensureSize_fn).call(this, this.pos);
|
| 991 |
+
__privateGet(this, _target).buffer = __privateGet(this, _buffer).slice(0, Math.max(__privateGet(this, _maxPos), this.pos));
|
| 992 |
+
}
|
| 993 |
+
};
|
| 994 |
+
_target = new WeakMap();
|
| 995 |
+
_buffer = new WeakMap();
|
| 996 |
+
_bytes = new WeakMap();
|
| 997 |
+
_maxPos = new WeakMap();
|
| 998 |
+
_ensureSize = new WeakSet();
|
| 999 |
+
ensureSize_fn = function(size) {
|
| 1000 |
+
let newLength = __privateGet(this, _buffer).byteLength;
|
| 1001 |
+
while (newLength < size)
|
| 1002 |
+
newLength *= 2;
|
| 1003 |
+
if (newLength === __privateGet(this, _buffer).byteLength)
|
| 1004 |
+
return;
|
| 1005 |
+
let newBuffer = new ArrayBuffer(newLength);
|
| 1006 |
+
let newBytes = new Uint8Array(newBuffer);
|
| 1007 |
+
newBytes.set(__privateGet(this, _bytes), 0);
|
| 1008 |
+
__privateSet(this, _buffer, newBuffer);
|
| 1009 |
+
__privateSet(this, _bytes, newBytes);
|
| 1010 |
+
};
|
| 1011 |
+
var DEFAULT_CHUNK_SIZE = 2 ** 24;
|
| 1012 |
+
var MAX_CHUNKS_AT_ONCE = 2;
|
| 1013 |
+
var _target2, _sections, _chunked, _chunkSize, _chunks, _writeDataIntoChunks, writeDataIntoChunks_fn, _insertSectionIntoChunk, insertSectionIntoChunk_fn, _createChunk, createChunk_fn, _flushChunks, flushChunks_fn;
|
| 1014 |
+
var StreamTargetWriter = class extends Writer {
|
| 1015 |
+
constructor(target) {
|
| 1016 |
+
super();
|
| 1017 |
+
__privateAdd(this, _writeDataIntoChunks);
|
| 1018 |
+
__privateAdd(this, _insertSectionIntoChunk);
|
| 1019 |
+
__privateAdd(this, _createChunk);
|
| 1020 |
+
__privateAdd(this, _flushChunks);
|
| 1021 |
+
__privateAdd(this, _target2, void 0);
|
| 1022 |
+
__privateAdd(this, _sections, []);
|
| 1023 |
+
__privateAdd(this, _chunked, void 0);
|
| 1024 |
+
__privateAdd(this, _chunkSize, void 0);
|
| 1025 |
+
/**
|
| 1026 |
+
* The data is divided up into fixed-size chunks, whose contents are first filled in RAM and then flushed out.
|
| 1027 |
+
* A chunk is flushed if all of its contents have been written.
|
| 1028 |
+
*/
|
| 1029 |
+
__privateAdd(this, _chunks, []);
|
| 1030 |
+
__privateSet(this, _target2, target);
|
| 1031 |
+
__privateSet(this, _chunked, target.options?.chunked ?? false);
|
| 1032 |
+
__privateSet(this, _chunkSize, target.options?.chunkSize ?? DEFAULT_CHUNK_SIZE);
|
| 1033 |
+
}
|
| 1034 |
+
write(data) {
|
| 1035 |
+
__privateGet(this, _sections).push({
|
| 1036 |
+
data: data.slice(),
|
| 1037 |
+
start: this.pos
|
| 1038 |
+
});
|
| 1039 |
+
this.pos += data.byteLength;
|
| 1040 |
+
}
|
| 1041 |
+
flush() {
|
| 1042 |
+
if (__privateGet(this, _sections).length === 0)
|
| 1043 |
+
return;
|
| 1044 |
+
let chunks = [];
|
| 1045 |
+
let sorted = [...__privateGet(this, _sections)].sort((a, b) => a.start - b.start);
|
| 1046 |
+
chunks.push({
|
| 1047 |
+
start: sorted[0].start,
|
| 1048 |
+
size: sorted[0].data.byteLength
|
| 1049 |
+
});
|
| 1050 |
+
for (let i = 1; i < sorted.length; i++) {
|
| 1051 |
+
let lastChunk = chunks[chunks.length - 1];
|
| 1052 |
+
let section = sorted[i];
|
| 1053 |
+
if (section.start <= lastChunk.start + lastChunk.size) {
|
| 1054 |
+
lastChunk.size = Math.max(lastChunk.size, section.start + section.data.byteLength - lastChunk.start);
|
| 1055 |
+
} else {
|
| 1056 |
+
chunks.push({
|
| 1057 |
+
start: section.start,
|
| 1058 |
+
size: section.data.byteLength
|
| 1059 |
+
});
|
| 1060 |
+
}
|
| 1061 |
+
}
|
| 1062 |
+
for (let chunk of chunks) {
|
| 1063 |
+
chunk.data = new Uint8Array(chunk.size);
|
| 1064 |
+
for (let section of __privateGet(this, _sections)) {
|
| 1065 |
+
if (chunk.start <= section.start && section.start < chunk.start + chunk.size) {
|
| 1066 |
+
chunk.data.set(section.data, section.start - chunk.start);
|
| 1067 |
+
}
|
| 1068 |
+
}
|
| 1069 |
+
if (__privateGet(this, _chunked)) {
|
| 1070 |
+
__privateMethod(this, _writeDataIntoChunks, writeDataIntoChunks_fn).call(this, chunk.data, chunk.start);
|
| 1071 |
+
__privateMethod(this, _flushChunks, flushChunks_fn).call(this);
|
| 1072 |
+
} else {
|
| 1073 |
+
__privateGet(this, _target2).options.onData?.(chunk.data, chunk.start);
|
| 1074 |
+
}
|
| 1075 |
+
}
|
| 1076 |
+
__privateGet(this, _sections).length = 0;
|
| 1077 |
+
}
|
| 1078 |
+
finalize() {
|
| 1079 |
+
if (__privateGet(this, _chunked)) {
|
| 1080 |
+
__privateMethod(this, _flushChunks, flushChunks_fn).call(this, true);
|
| 1081 |
+
}
|
| 1082 |
+
}
|
| 1083 |
+
};
|
| 1084 |
+
_target2 = new WeakMap();
|
| 1085 |
+
_sections = new WeakMap();
|
| 1086 |
+
_chunked = new WeakMap();
|
| 1087 |
+
_chunkSize = new WeakMap();
|
| 1088 |
+
_chunks = new WeakMap();
|
| 1089 |
+
_writeDataIntoChunks = new WeakSet();
|
| 1090 |
+
writeDataIntoChunks_fn = function(data, position) {
|
| 1091 |
+
let chunkIndex = __privateGet(this, _chunks).findIndex((x) => x.start <= position && position < x.start + __privateGet(this, _chunkSize));
|
| 1092 |
+
if (chunkIndex === -1)
|
| 1093 |
+
chunkIndex = __privateMethod(this, _createChunk, createChunk_fn).call(this, position);
|
| 1094 |
+
let chunk = __privateGet(this, _chunks)[chunkIndex];
|
| 1095 |
+
let relativePosition = position - chunk.start;
|
| 1096 |
+
let toWrite = data.subarray(0, Math.min(__privateGet(this, _chunkSize) - relativePosition, data.byteLength));
|
| 1097 |
+
chunk.data.set(toWrite, relativePosition);
|
| 1098 |
+
let section = {
|
| 1099 |
+
start: relativePosition,
|
| 1100 |
+
end: relativePosition + toWrite.byteLength
|
| 1101 |
+
};
|
| 1102 |
+
__privateMethod(this, _insertSectionIntoChunk, insertSectionIntoChunk_fn).call(this, chunk, section);
|
| 1103 |
+
if (chunk.written[0].start === 0 && chunk.written[0].end === __privateGet(this, _chunkSize)) {
|
| 1104 |
+
chunk.shouldFlush = true;
|
| 1105 |
+
}
|
| 1106 |
+
if (__privateGet(this, _chunks).length > MAX_CHUNKS_AT_ONCE) {
|
| 1107 |
+
for (let i = 0; i < __privateGet(this, _chunks).length - 1; i++) {
|
| 1108 |
+
__privateGet(this, _chunks)[i].shouldFlush = true;
|
| 1109 |
+
}
|
| 1110 |
+
__privateMethod(this, _flushChunks, flushChunks_fn).call(this);
|
| 1111 |
+
}
|
| 1112 |
+
if (toWrite.byteLength < data.byteLength) {
|
| 1113 |
+
__privateMethod(this, _writeDataIntoChunks, writeDataIntoChunks_fn).call(this, data.subarray(toWrite.byteLength), position + toWrite.byteLength);
|
| 1114 |
+
}
|
| 1115 |
+
};
|
| 1116 |
+
_insertSectionIntoChunk = new WeakSet();
|
| 1117 |
+
insertSectionIntoChunk_fn = function(chunk, section) {
|
| 1118 |
+
let low = 0;
|
| 1119 |
+
let high = chunk.written.length - 1;
|
| 1120 |
+
let index = -1;
|
| 1121 |
+
while (low <= high) {
|
| 1122 |
+
let mid = Math.floor(low + (high - low + 1) / 2);
|
| 1123 |
+
if (chunk.written[mid].start <= section.start) {
|
| 1124 |
+
low = mid + 1;
|
| 1125 |
+
index = mid;
|
| 1126 |
+
} else {
|
| 1127 |
+
high = mid - 1;
|
| 1128 |
+
}
|
| 1129 |
+
}
|
| 1130 |
+
chunk.written.splice(index + 1, 0, section);
|
| 1131 |
+
if (index === -1 || chunk.written[index].end < section.start)
|
| 1132 |
+
index++;
|
| 1133 |
+
while (index < chunk.written.length - 1 && chunk.written[index].end >= chunk.written[index + 1].start) {
|
| 1134 |
+
chunk.written[index].end = Math.max(chunk.written[index].end, chunk.written[index + 1].end);
|
| 1135 |
+
chunk.written.splice(index + 1, 1);
|
| 1136 |
+
}
|
| 1137 |
+
};
|
| 1138 |
+
_createChunk = new WeakSet();
|
| 1139 |
+
createChunk_fn = function(includesPosition) {
|
| 1140 |
+
let start = Math.floor(includesPosition / __privateGet(this, _chunkSize)) * __privateGet(this, _chunkSize);
|
| 1141 |
+
let chunk = {
|
| 1142 |
+
start,
|
| 1143 |
+
data: new Uint8Array(__privateGet(this, _chunkSize)),
|
| 1144 |
+
written: [],
|
| 1145 |
+
shouldFlush: false
|
| 1146 |
+
};
|
| 1147 |
+
__privateGet(this, _chunks).push(chunk);
|
| 1148 |
+
__privateGet(this, _chunks).sort((a, b) => a.start - b.start);
|
| 1149 |
+
return __privateGet(this, _chunks).indexOf(chunk);
|
| 1150 |
+
};
|
| 1151 |
+
_flushChunks = new WeakSet();
|
| 1152 |
+
flushChunks_fn = function(force = false) {
|
| 1153 |
+
for (let i = 0; i < __privateGet(this, _chunks).length; i++) {
|
| 1154 |
+
let chunk = __privateGet(this, _chunks)[i];
|
| 1155 |
+
if (!chunk.shouldFlush && !force)
|
| 1156 |
+
continue;
|
| 1157 |
+
for (let section of chunk.written) {
|
| 1158 |
+
__privateGet(this, _target2).options.onData?.(
|
| 1159 |
+
chunk.data.subarray(section.start, section.end),
|
| 1160 |
+
chunk.start + section.start
|
| 1161 |
+
);
|
| 1162 |
+
}
|
| 1163 |
+
__privateGet(this, _chunks).splice(i--, 1);
|
| 1164 |
+
}
|
| 1165 |
+
};
|
| 1166 |
+
var FileSystemWritableFileStreamTargetWriter = class extends StreamTargetWriter {
|
| 1167 |
+
constructor(target) {
|
| 1168 |
+
super(new StreamTarget({
|
| 1169 |
+
onData: (data, position) => target.stream.write({
|
| 1170 |
+
type: "write",
|
| 1171 |
+
data,
|
| 1172 |
+
position
|
| 1173 |
+
}),
|
| 1174 |
+
chunked: true,
|
| 1175 |
+
chunkSize: target.options?.chunkSize
|
| 1176 |
+
}));
|
| 1177 |
+
}
|
| 1178 |
+
};
|
| 1179 |
+
|
| 1180 |
+
// src/muxer.ts
|
| 1181 |
+
var GLOBAL_TIMESCALE = 1e3;
|
| 1182 |
+
var SUPPORTED_VIDEO_CODECS = ["avc", "hevc", "vp9", "av1"];
|
| 1183 |
+
var SUPPORTED_AUDIO_CODECS = ["aac", "opus"];
|
| 1184 |
+
var TIMESTAMP_OFFSET = 2082844800;
|
| 1185 |
+
var FIRST_TIMESTAMP_BEHAVIORS = ["strict", "offset", "cross-track-offset"];
|
| 1186 |
+
var _options, _writer, _ftypSize, _mdat, _videoTrack, _audioTrack, _creationTime, _finalizedChunks, _nextFragmentNumber, _videoSampleQueue, _audioSampleQueue, _finalized, _validateOptions, validateOptions_fn, _writeHeader, writeHeader_fn, _computeMoovSizeUpperBound, computeMoovSizeUpperBound_fn, _prepareTracks, prepareTracks_fn, _generateMpeg4AudioSpecificConfig, generateMpeg4AudioSpecificConfig_fn, _createSampleForTrack, createSampleForTrack_fn, _addSampleToTrack, addSampleToTrack_fn, _validateTimestamp, validateTimestamp_fn, _finalizeCurrentChunk, finalizeCurrentChunk_fn, _finalizeFragment, finalizeFragment_fn, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn, _ensureNotFinalized, ensureNotFinalized_fn;
|
| 1187 |
+
var Muxer = class {
|
| 1188 |
+
constructor(options) {
|
| 1189 |
+
__privateAdd(this, _validateOptions);
|
| 1190 |
+
__privateAdd(this, _writeHeader);
|
| 1191 |
+
__privateAdd(this, _computeMoovSizeUpperBound);
|
| 1192 |
+
__privateAdd(this, _prepareTracks);
|
| 1193 |
+
// https://wiki.multimedia.cx/index.php/MPEG-4_Audio
|
| 1194 |
+
__privateAdd(this, _generateMpeg4AudioSpecificConfig);
|
| 1195 |
+
__privateAdd(this, _createSampleForTrack);
|
| 1196 |
+
__privateAdd(this, _addSampleToTrack);
|
| 1197 |
+
__privateAdd(this, _validateTimestamp);
|
| 1198 |
+
__privateAdd(this, _finalizeCurrentChunk);
|
| 1199 |
+
__privateAdd(this, _finalizeFragment);
|
| 1200 |
+
__privateAdd(this, _maybeFlushStreamingTargetWriter);
|
| 1201 |
+
__privateAdd(this, _ensureNotFinalized);
|
| 1202 |
+
__privateAdd(this, _options, void 0);
|
| 1203 |
+
__privateAdd(this, _writer, void 0);
|
| 1204 |
+
__privateAdd(this, _ftypSize, void 0);
|
| 1205 |
+
__privateAdd(this, _mdat, void 0);
|
| 1206 |
+
__privateAdd(this, _videoTrack, null);
|
| 1207 |
+
__privateAdd(this, _audioTrack, null);
|
| 1208 |
+
__privateAdd(this, _creationTime, Math.floor(Date.now() / 1e3) + TIMESTAMP_OFFSET);
|
| 1209 |
+
__privateAdd(this, _finalizedChunks, []);
|
| 1210 |
+
// Fields for fragmented MP4:
|
| 1211 |
+
__privateAdd(this, _nextFragmentNumber, 1);
|
| 1212 |
+
__privateAdd(this, _videoSampleQueue, []);
|
| 1213 |
+
__privateAdd(this, _audioSampleQueue, []);
|
| 1214 |
+
__privateAdd(this, _finalized, false);
|
| 1215 |
+
__privateMethod(this, _validateOptions, validateOptions_fn).call(this, options);
|
| 1216 |
+
options.video = deepClone(options.video);
|
| 1217 |
+
options.audio = deepClone(options.audio);
|
| 1218 |
+
options.fastStart = deepClone(options.fastStart);
|
| 1219 |
+
this.target = options.target;
|
| 1220 |
+
__privateSet(this, _options, {
|
| 1221 |
+
firstTimestampBehavior: "strict",
|
| 1222 |
+
...options
|
| 1223 |
+
});
|
| 1224 |
+
if (options.target instanceof ArrayBufferTarget) {
|
| 1225 |
+
__privateSet(this, _writer, new ArrayBufferTargetWriter(options.target));
|
| 1226 |
+
} else if (options.target instanceof StreamTarget) {
|
| 1227 |
+
__privateSet(this, _writer, new StreamTargetWriter(options.target));
|
| 1228 |
+
} else if (options.target instanceof FileSystemWritableFileStreamTarget) {
|
| 1229 |
+
__privateSet(this, _writer, new FileSystemWritableFileStreamTargetWriter(options.target));
|
| 1230 |
+
} else {
|
| 1231 |
+
throw new Error(`Invalid target: ${options.target}`);
|
| 1232 |
+
}
|
| 1233 |
+
__privateMethod(this, _prepareTracks, prepareTracks_fn).call(this);
|
| 1234 |
+
__privateMethod(this, _writeHeader, writeHeader_fn).call(this);
|
| 1235 |
+
}
|
| 1236 |
+
addVideoChunk(sample, meta, timestamp, compositionTimeOffset) {
|
| 1237 |
+
if (!(sample instanceof EncodedVideoChunk)) {
|
| 1238 |
+
throw new TypeError("addVideoChunk's first argument (sample) must be of type EncodedVideoChunk.");
|
| 1239 |
+
}
|
| 1240 |
+
if (meta && typeof meta !== "object") {
|
| 1241 |
+
throw new TypeError("addVideoChunk's second argument (meta), when provided, must be an object.");
|
| 1242 |
+
}
|
| 1243 |
+
if (timestamp !== void 0 && (!Number.isFinite(timestamp) || timestamp < 0)) {
|
| 1244 |
+
throw new TypeError(
|
| 1245 |
+
"addVideoChunk's third argument (timestamp), when provided, must be a non-negative real number."
|
| 1246 |
+
);
|
| 1247 |
+
}
|
| 1248 |
+
if (compositionTimeOffset !== void 0 && !Number.isFinite(compositionTimeOffset)) {
|
| 1249 |
+
throw new TypeError(
|
| 1250 |
+
"addVideoChunk's fourth argument (compositionTimeOffset), when provided, must be a real number."
|
| 1251 |
+
);
|
| 1252 |
+
}
|
| 1253 |
+
let data = new Uint8Array(sample.byteLength);
|
| 1254 |
+
sample.copyTo(data);
|
| 1255 |
+
this.addVideoChunkRaw(
|
| 1256 |
+
data,
|
| 1257 |
+
sample.type,
|
| 1258 |
+
timestamp ?? sample.timestamp,
|
| 1259 |
+
sample.duration,
|
| 1260 |
+
meta,
|
| 1261 |
+
compositionTimeOffset
|
| 1262 |
+
);
|
| 1263 |
+
}
|
| 1264 |
+
addVideoChunkRaw(data, type, timestamp, duration, meta, compositionTimeOffset) {
|
| 1265 |
+
if (!(data instanceof Uint8Array)) {
|
| 1266 |
+
throw new TypeError("addVideoChunkRaw's first argument (data) must be an instance of Uint8Array.");
|
| 1267 |
+
}
|
| 1268 |
+
if (type !== "key" && type !== "delta") {
|
| 1269 |
+
throw new TypeError("addVideoChunkRaw's second argument (type) must be either 'key' or 'delta'.");
|
| 1270 |
+
}
|
| 1271 |
+
if (!Number.isFinite(timestamp) || timestamp < 0) {
|
| 1272 |
+
throw new TypeError("addVideoChunkRaw's third argument (timestamp) must be a non-negative real number.");
|
| 1273 |
+
}
|
| 1274 |
+
if (!Number.isFinite(duration) || duration < 0) {
|
| 1275 |
+
throw new TypeError("addVideoChunkRaw's fourth argument (duration) must be a non-negative real number.");
|
| 1276 |
+
}
|
| 1277 |
+
if (meta && typeof meta !== "object") {
|
| 1278 |
+
throw new TypeError("addVideoChunkRaw's fifth argument (meta), when provided, must be an object.");
|
| 1279 |
+
}
|
| 1280 |
+
if (compositionTimeOffset !== void 0 && !Number.isFinite(compositionTimeOffset)) {
|
| 1281 |
+
throw new TypeError(
|
| 1282 |
+
"addVideoChunkRaw's sixth argument (compositionTimeOffset), when provided, must be a real number."
|
| 1283 |
+
);
|
| 1284 |
+
}
|
| 1285 |
+
__privateMethod(this, _ensureNotFinalized, ensureNotFinalized_fn).call(this);
|
| 1286 |
+
if (!__privateGet(this, _options).video)
|
| 1287 |
+
throw new Error("No video track declared.");
|
| 1288 |
+
if (typeof __privateGet(this, _options).fastStart === "object" && __privateGet(this, _videoTrack).samples.length === __privateGet(this, _options).fastStart.expectedVideoChunks) {
|
| 1289 |
+
throw new Error(`Cannot add more video chunks than specified in 'fastStart' (${__privateGet(this, _options).fastStart.expectedVideoChunks}).`);
|
| 1290 |
+
}
|
| 1291 |
+
let videoSample = __privateMethod(this, _createSampleForTrack, createSampleForTrack_fn).call(this, __privateGet(this, _videoTrack), data, type, timestamp, duration, meta, compositionTimeOffset);
|
| 1292 |
+
if (__privateGet(this, _options).fastStart === "fragmented" && __privateGet(this, _audioTrack)) {
|
| 1293 |
+
while (__privateGet(this, _audioSampleQueue).length > 0 && __privateGet(this, _audioSampleQueue)[0].decodeTimestamp <= videoSample.decodeTimestamp) {
|
| 1294 |
+
let audioSample = __privateGet(this, _audioSampleQueue).shift();
|
| 1295 |
+
__privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _audioTrack), audioSample);
|
| 1296 |
+
}
|
| 1297 |
+
if (videoSample.decodeTimestamp <= __privateGet(this, _audioTrack).lastDecodeTimestamp) {
|
| 1298 |
+
__privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _videoTrack), videoSample);
|
| 1299 |
+
} else {
|
| 1300 |
+
__privateGet(this, _videoSampleQueue).push(videoSample);
|
| 1301 |
+
}
|
| 1302 |
+
} else {
|
| 1303 |
+
__privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _videoTrack), videoSample);
|
| 1304 |
+
}
|
| 1305 |
+
}
|
| 1306 |
+
addAudioChunk(sample, meta, timestamp) {
|
| 1307 |
+
if (!(sample instanceof EncodedAudioChunk)) {
|
| 1308 |
+
throw new TypeError("addAudioChunk's first argument (sample) must be of type EncodedAudioChunk.");
|
| 1309 |
+
}
|
| 1310 |
+
if (meta && typeof meta !== "object") {
|
| 1311 |
+
throw new TypeError("addAudioChunk's second argument (meta), when provided, must be an object.");
|
| 1312 |
+
}
|
| 1313 |
+
if (timestamp !== void 0 && (!Number.isFinite(timestamp) || timestamp < 0)) {
|
| 1314 |
+
throw new TypeError(
|
| 1315 |
+
"addAudioChunk's third argument (timestamp), when provided, must be a non-negative real number."
|
| 1316 |
+
);
|
| 1317 |
+
}
|
| 1318 |
+
let data = new Uint8Array(sample.byteLength);
|
| 1319 |
+
sample.copyTo(data);
|
| 1320 |
+
this.addAudioChunkRaw(data, sample.type, timestamp ?? sample.timestamp, sample.duration, meta);
|
| 1321 |
+
}
|
| 1322 |
+
addAudioChunkRaw(data, type, timestamp, duration, meta) {
|
| 1323 |
+
if (!(data instanceof Uint8Array)) {
|
| 1324 |
+
throw new TypeError("addAudioChunkRaw's first argument (data) must be an instance of Uint8Array.");
|
| 1325 |
+
}
|
| 1326 |
+
if (type !== "key" && type !== "delta") {
|
| 1327 |
+
throw new TypeError("addAudioChunkRaw's second argument (type) must be either 'key' or 'delta'.");
|
| 1328 |
+
}
|
| 1329 |
+
if (!Number.isFinite(timestamp) || timestamp < 0) {
|
| 1330 |
+
throw new TypeError("addAudioChunkRaw's third argument (timestamp) must be a non-negative real number.");
|
| 1331 |
+
}
|
| 1332 |
+
if (!Number.isFinite(duration) || duration < 0) {
|
| 1333 |
+
throw new TypeError("addAudioChunkRaw's fourth argument (duration) must be a non-negative real number.");
|
| 1334 |
+
}
|
| 1335 |
+
if (meta && typeof meta !== "object") {
|
| 1336 |
+
throw new TypeError("addAudioChunkRaw's fifth argument (meta), when provided, must be an object.");
|
| 1337 |
+
}
|
| 1338 |
+
__privateMethod(this, _ensureNotFinalized, ensureNotFinalized_fn).call(this);
|
| 1339 |
+
if (!__privateGet(this, _options).audio)
|
| 1340 |
+
throw new Error("No audio track declared.");
|
| 1341 |
+
if (typeof __privateGet(this, _options).fastStart === "object" && __privateGet(this, _audioTrack).samples.length === __privateGet(this, _options).fastStart.expectedAudioChunks) {
|
| 1342 |
+
throw new Error(`Cannot add more audio chunks than specified in 'fastStart' (${__privateGet(this, _options).fastStart.expectedAudioChunks}).`);
|
| 1343 |
+
}
|
| 1344 |
+
let audioSample = __privateMethod(this, _createSampleForTrack, createSampleForTrack_fn).call(this, __privateGet(this, _audioTrack), data, type, timestamp, duration, meta);
|
| 1345 |
+
if (__privateGet(this, _options).fastStart === "fragmented" && __privateGet(this, _videoTrack)) {
|
| 1346 |
+
while (__privateGet(this, _videoSampleQueue).length > 0 && __privateGet(this, _videoSampleQueue)[0].decodeTimestamp <= audioSample.decodeTimestamp) {
|
| 1347 |
+
let videoSample = __privateGet(this, _videoSampleQueue).shift();
|
| 1348 |
+
__privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _videoTrack), videoSample);
|
| 1349 |
+
}
|
| 1350 |
+
if (audioSample.decodeTimestamp <= __privateGet(this, _videoTrack).lastDecodeTimestamp) {
|
| 1351 |
+
__privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _audioTrack), audioSample);
|
| 1352 |
+
} else {
|
| 1353 |
+
__privateGet(this, _audioSampleQueue).push(audioSample);
|
| 1354 |
+
}
|
| 1355 |
+
} else {
|
| 1356 |
+
__privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _audioTrack), audioSample);
|
| 1357 |
+
}
|
| 1358 |
+
}
|
| 1359 |
+
/** Finalizes the file, making it ready for use. Must be called after all video and audio chunks have been added. */
|
| 1360 |
+
finalize() {
|
| 1361 |
+
if (__privateGet(this, _finalized)) {
|
| 1362 |
+
throw new Error("Cannot finalize a muxer more than once.");
|
| 1363 |
+
}
|
| 1364 |
+
if (__privateGet(this, _options).fastStart === "fragmented") {
|
| 1365 |
+
for (let videoSample of __privateGet(this, _videoSampleQueue))
|
| 1366 |
+
__privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _videoTrack), videoSample);
|
| 1367 |
+
for (let audioSample of __privateGet(this, _audioSampleQueue))
|
| 1368 |
+
__privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _audioTrack), audioSample);
|
| 1369 |
+
__privateMethod(this, _finalizeFragment, finalizeFragment_fn).call(this, false);
|
| 1370 |
+
} else {
|
| 1371 |
+
if (__privateGet(this, _videoTrack))
|
| 1372 |
+
__privateMethod(this, _finalizeCurrentChunk, finalizeCurrentChunk_fn).call(this, __privateGet(this, _videoTrack));
|
| 1373 |
+
if (__privateGet(this, _audioTrack))
|
| 1374 |
+
__privateMethod(this, _finalizeCurrentChunk, finalizeCurrentChunk_fn).call(this, __privateGet(this, _audioTrack));
|
| 1375 |
+
}
|
| 1376 |
+
let tracks = [__privateGet(this, _videoTrack), __privateGet(this, _audioTrack)].filter(Boolean);
|
| 1377 |
+
if (__privateGet(this, _options).fastStart === "in-memory") {
|
| 1378 |
+
let mdatSize;
|
| 1379 |
+
for (let i = 0; i < 2; i++) {
|
| 1380 |
+
let movieBox2 = moov(tracks, __privateGet(this, _creationTime));
|
| 1381 |
+
let movieBoxSize = __privateGet(this, _writer).measureBox(movieBox2);
|
| 1382 |
+
mdatSize = __privateGet(this, _writer).measureBox(__privateGet(this, _mdat));
|
| 1383 |
+
let currentChunkPos = __privateGet(this, _writer).pos + movieBoxSize + mdatSize;
|
| 1384 |
+
for (let chunk of __privateGet(this, _finalizedChunks)) {
|
| 1385 |
+
chunk.offset = currentChunkPos;
|
| 1386 |
+
for (let { data } of chunk.samples) {
|
| 1387 |
+
currentChunkPos += data.byteLength;
|
| 1388 |
+
mdatSize += data.byteLength;
|
| 1389 |
+
}
|
| 1390 |
+
}
|
| 1391 |
+
if (currentChunkPos < 2 ** 32)
|
| 1392 |
+
break;
|
| 1393 |
+
if (mdatSize >= 2 ** 32)
|
| 1394 |
+
__privateGet(this, _mdat).largeSize = true;
|
| 1395 |
+
}
|
| 1396 |
+
let movieBox = moov(tracks, __privateGet(this, _creationTime));
|
| 1397 |
+
__privateGet(this, _writer).writeBox(movieBox);
|
| 1398 |
+
__privateGet(this, _mdat).size = mdatSize;
|
| 1399 |
+
__privateGet(this, _writer).writeBox(__privateGet(this, _mdat));
|
| 1400 |
+
for (let chunk of __privateGet(this, _finalizedChunks)) {
|
| 1401 |
+
for (let sample of chunk.samples) {
|
| 1402 |
+
__privateGet(this, _writer).write(sample.data);
|
| 1403 |
+
sample.data = null;
|
| 1404 |
+
}
|
| 1405 |
+
}
|
| 1406 |
+
} else if (__privateGet(this, _options).fastStart === "fragmented") {
|
| 1407 |
+
let startPos = __privateGet(this, _writer).pos;
|
| 1408 |
+
let mfraBox = mfra(tracks);
|
| 1409 |
+
__privateGet(this, _writer).writeBox(mfraBox);
|
| 1410 |
+
let mfraBoxSize = __privateGet(this, _writer).pos - startPos;
|
| 1411 |
+
__privateGet(this, _writer).seek(__privateGet(this, _writer).pos - 4);
|
| 1412 |
+
__privateGet(this, _writer).writeU32(mfraBoxSize);
|
| 1413 |
+
} else {
|
| 1414 |
+
let mdatPos = __privateGet(this, _writer).offsets.get(__privateGet(this, _mdat));
|
| 1415 |
+
let mdatSize = __privateGet(this, _writer).pos - mdatPos;
|
| 1416 |
+
__privateGet(this, _mdat).size = mdatSize;
|
| 1417 |
+
__privateGet(this, _mdat).largeSize = mdatSize >= 2 ** 32;
|
| 1418 |
+
__privateGet(this, _writer).patchBox(__privateGet(this, _mdat));
|
| 1419 |
+
let movieBox = moov(tracks, __privateGet(this, _creationTime));
|
| 1420 |
+
if (typeof __privateGet(this, _options).fastStart === "object") {
|
| 1421 |
+
__privateGet(this, _writer).seek(__privateGet(this, _ftypSize));
|
| 1422 |
+
__privateGet(this, _writer).writeBox(movieBox);
|
| 1423 |
+
let remainingBytes = mdatPos - __privateGet(this, _writer).pos;
|
| 1424 |
+
__privateGet(this, _writer).writeBox(free(remainingBytes));
|
| 1425 |
+
} else {
|
| 1426 |
+
__privateGet(this, _writer).writeBox(movieBox);
|
| 1427 |
+
}
|
| 1428 |
+
}
|
| 1429 |
+
__privateMethod(this, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn).call(this);
|
| 1430 |
+
__privateGet(this, _writer).finalize();
|
| 1431 |
+
__privateSet(this, _finalized, true);
|
| 1432 |
+
}
|
| 1433 |
+
};
|
| 1434 |
+
_options = new WeakMap();
|
| 1435 |
+
_writer = new WeakMap();
|
| 1436 |
+
_ftypSize = new WeakMap();
|
| 1437 |
+
_mdat = new WeakMap();
|
| 1438 |
+
_videoTrack = new WeakMap();
|
| 1439 |
+
_audioTrack = new WeakMap();
|
| 1440 |
+
_creationTime = new WeakMap();
|
| 1441 |
+
_finalizedChunks = new WeakMap();
|
| 1442 |
+
_nextFragmentNumber = new WeakMap();
|
| 1443 |
+
_videoSampleQueue = new WeakMap();
|
| 1444 |
+
_audioSampleQueue = new WeakMap();
|
| 1445 |
+
_finalized = new WeakMap();
|
| 1446 |
+
_validateOptions = new WeakSet();
|
| 1447 |
+
validateOptions_fn = function(options) {
|
| 1448 |
+
if (typeof options !== "object") {
|
| 1449 |
+
throw new TypeError("The muxer requires an options object to be passed to its constructor.");
|
| 1450 |
+
}
|
| 1451 |
+
if (!(options.target instanceof Target)) {
|
| 1452 |
+
throw new TypeError("The target must be provided and an instance of Target.");
|
| 1453 |
+
}
|
| 1454 |
+
if (options.video) {
|
| 1455 |
+
if (!SUPPORTED_VIDEO_CODECS.includes(options.video.codec)) {
|
| 1456 |
+
throw new TypeError(`Unsupported video codec: ${options.video.codec}`);
|
| 1457 |
+
}
|
| 1458 |
+
if (!Number.isInteger(options.video.width) || options.video.width <= 0) {
|
| 1459 |
+
throw new TypeError(`Invalid video width: ${options.video.width}. Must be a positive integer.`);
|
| 1460 |
+
}
|
| 1461 |
+
if (!Number.isInteger(options.video.height) || options.video.height <= 0) {
|
| 1462 |
+
throw new TypeError(`Invalid video height: ${options.video.height}. Must be a positive integer.`);
|
| 1463 |
+
}
|
| 1464 |
+
const videoRotation = options.video.rotation;
|
| 1465 |
+
if (typeof videoRotation === "number" && ![0, 90, 180, 270].includes(videoRotation)) {
|
| 1466 |
+
throw new TypeError(`Invalid video rotation: ${videoRotation}. Has to be 0, 90, 180 or 270.`);
|
| 1467 |
+
} else if (Array.isArray(videoRotation) && (videoRotation.length !== 9 || videoRotation.some((value) => typeof value !== "number"))) {
|
| 1468 |
+
throw new TypeError(`Invalid video transformation matrix: ${videoRotation.join()}`);
|
| 1469 |
+
}
|
| 1470 |
+
if (options.video.frameRate !== void 0 && (!Number.isInteger(options.video.frameRate) || options.video.frameRate <= 0)) {
|
| 1471 |
+
throw new TypeError(
|
| 1472 |
+
`Invalid video frame rate: ${options.video.frameRate}. Must be a positive integer.`
|
| 1473 |
+
);
|
| 1474 |
+
}
|
| 1475 |
+
}
|
| 1476 |
+
if (options.audio) {
|
| 1477 |
+
if (!SUPPORTED_AUDIO_CODECS.includes(options.audio.codec)) {
|
| 1478 |
+
throw new TypeError(`Unsupported audio codec: ${options.audio.codec}`);
|
| 1479 |
+
}
|
| 1480 |
+
if (!Number.isInteger(options.audio.numberOfChannels) || options.audio.numberOfChannels <= 0) {
|
| 1481 |
+
throw new TypeError(
|
| 1482 |
+
`Invalid number of audio channels: ${options.audio.numberOfChannels}. Must be a positive integer.`
|
| 1483 |
+
);
|
| 1484 |
+
}
|
| 1485 |
+
if (!Number.isInteger(options.audio.sampleRate) || options.audio.sampleRate <= 0) {
|
| 1486 |
+
throw new TypeError(
|
| 1487 |
+
`Invalid audio sample rate: ${options.audio.sampleRate}. Must be a positive integer.`
|
| 1488 |
+
);
|
| 1489 |
+
}
|
| 1490 |
+
}
|
| 1491 |
+
if (options.firstTimestampBehavior && !FIRST_TIMESTAMP_BEHAVIORS.includes(options.firstTimestampBehavior)) {
|
| 1492 |
+
throw new TypeError(`Invalid first timestamp behavior: ${options.firstTimestampBehavior}`);
|
| 1493 |
+
}
|
| 1494 |
+
if (typeof options.fastStart === "object") {
|
| 1495 |
+
if (options.video) {
|
| 1496 |
+
if (options.fastStart.expectedVideoChunks === void 0) {
|
| 1497 |
+
throw new TypeError(`'fastStart' is an object but is missing property 'expectedVideoChunks'.`);
|
| 1498 |
+
} else if (!Number.isInteger(options.fastStart.expectedVideoChunks) || options.fastStart.expectedVideoChunks < 0) {
|
| 1499 |
+
throw new TypeError(`'expectedVideoChunks' must be a non-negative integer.`);
|
| 1500 |
+
}
|
| 1501 |
+
}
|
| 1502 |
+
if (options.audio) {
|
| 1503 |
+
if (options.fastStart.expectedAudioChunks === void 0) {
|
| 1504 |
+
throw new TypeError(`'fastStart' is an object but is missing property 'expectedAudioChunks'.`);
|
| 1505 |
+
} else if (!Number.isInteger(options.fastStart.expectedAudioChunks) || options.fastStart.expectedAudioChunks < 0) {
|
| 1506 |
+
throw new TypeError(`'expectedAudioChunks' must be a non-negative integer.`);
|
| 1507 |
+
}
|
| 1508 |
+
}
|
| 1509 |
+
} else if (![false, "in-memory", "fragmented"].includes(options.fastStart)) {
|
| 1510 |
+
throw new TypeError(`'fastStart' option must be false, 'in-memory', 'fragmented' or an object.`);
|
| 1511 |
+
}
|
| 1512 |
+
if (options.minFragmentDuration !== void 0 && (!Number.isFinite(options.minFragmentDuration) || options.minFragmentDuration < 0)) {
|
| 1513 |
+
throw new TypeError(`'minFragmentDuration' must be a non-negative number.`);
|
| 1514 |
+
}
|
| 1515 |
+
};
|
| 1516 |
+
_writeHeader = new WeakSet();
|
| 1517 |
+
writeHeader_fn = function() {
|
| 1518 |
+
__privateGet(this, _writer).writeBox(ftyp({
|
| 1519 |
+
holdsAvc: __privateGet(this, _options).video?.codec === "avc",
|
| 1520 |
+
fragmented: __privateGet(this, _options).fastStart === "fragmented"
|
| 1521 |
+
}));
|
| 1522 |
+
__privateSet(this, _ftypSize, __privateGet(this, _writer).pos);
|
| 1523 |
+
if (__privateGet(this, _options).fastStart === "in-memory") {
|
| 1524 |
+
__privateSet(this, _mdat, mdat(false));
|
| 1525 |
+
} else if (__privateGet(this, _options).fastStart === "fragmented") {
|
| 1526 |
+
} else {
|
| 1527 |
+
if (typeof __privateGet(this, _options).fastStart === "object") {
|
| 1528 |
+
let moovSizeUpperBound = __privateMethod(this, _computeMoovSizeUpperBound, computeMoovSizeUpperBound_fn).call(this);
|
| 1529 |
+
__privateGet(this, _writer).seek(__privateGet(this, _writer).pos + moovSizeUpperBound);
|
| 1530 |
+
}
|
| 1531 |
+
__privateSet(this, _mdat, mdat(true));
|
| 1532 |
+
__privateGet(this, _writer).writeBox(__privateGet(this, _mdat));
|
| 1533 |
+
}
|
| 1534 |
+
__privateMethod(this, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn).call(this);
|
| 1535 |
+
};
|
| 1536 |
+
_computeMoovSizeUpperBound = new WeakSet();
|
| 1537 |
+
computeMoovSizeUpperBound_fn = function() {
|
| 1538 |
+
if (typeof __privateGet(this, _options).fastStart !== "object")
|
| 1539 |
+
return;
|
| 1540 |
+
let upperBound = 0;
|
| 1541 |
+
let sampleCounts = [
|
| 1542 |
+
__privateGet(this, _options).fastStart.expectedVideoChunks,
|
| 1543 |
+
__privateGet(this, _options).fastStart.expectedAudioChunks
|
| 1544 |
+
];
|
| 1545 |
+
for (let n of sampleCounts) {
|
| 1546 |
+
if (!n)
|
| 1547 |
+
continue;
|
| 1548 |
+
upperBound += (4 + 4) * Math.ceil(2 / 3 * n);
|
| 1549 |
+
upperBound += 4 * n;
|
| 1550 |
+
upperBound += (4 + 4 + 4) * Math.ceil(2 / 3 * n);
|
| 1551 |
+
upperBound += 4 * n;
|
| 1552 |
+
upperBound += 8 * n;
|
| 1553 |
+
}
|
| 1554 |
+
upperBound += 4096;
|
| 1555 |
+
return upperBound;
|
| 1556 |
+
};
|
| 1557 |
+
_prepareTracks = new WeakSet();
|
| 1558 |
+
prepareTracks_fn = function() {
|
| 1559 |
+
if (__privateGet(this, _options).video) {
|
| 1560 |
+
__privateSet(this, _videoTrack, {
|
| 1561 |
+
id: 1,
|
| 1562 |
+
info: {
|
| 1563 |
+
type: "video",
|
| 1564 |
+
codec: __privateGet(this, _options).video.codec,
|
| 1565 |
+
width: __privateGet(this, _options).video.width,
|
| 1566 |
+
height: __privateGet(this, _options).video.height,
|
| 1567 |
+
rotation: __privateGet(this, _options).video.rotation ?? 0,
|
| 1568 |
+
decoderConfig: null
|
| 1569 |
+
},
|
| 1570 |
+
// The fallback contains many common frame rates as factors
|
| 1571 |
+
timescale: __privateGet(this, _options).video.frameRate ?? 57600,
|
| 1572 |
+
samples: [],
|
| 1573 |
+
finalizedChunks: [],
|
| 1574 |
+
currentChunk: null,
|
| 1575 |
+
firstDecodeTimestamp: void 0,
|
| 1576 |
+
lastDecodeTimestamp: -1,
|
| 1577 |
+
timeToSampleTable: [],
|
| 1578 |
+
compositionTimeOffsetTable: [],
|
| 1579 |
+
lastTimescaleUnits: null,
|
| 1580 |
+
lastSample: null,
|
| 1581 |
+
compactlyCodedChunkTable: []
|
| 1582 |
+
});
|
| 1583 |
+
}
|
| 1584 |
+
if (__privateGet(this, _options).audio) {
|
| 1585 |
+
__privateSet(this, _audioTrack, {
|
| 1586 |
+
id: __privateGet(this, _options).video ? 2 : 1,
|
| 1587 |
+
info: {
|
| 1588 |
+
type: "audio",
|
| 1589 |
+
codec: __privateGet(this, _options).audio.codec,
|
| 1590 |
+
numberOfChannels: __privateGet(this, _options).audio.numberOfChannels,
|
| 1591 |
+
sampleRate: __privateGet(this, _options).audio.sampleRate,
|
| 1592 |
+
decoderConfig: null
|
| 1593 |
+
},
|
| 1594 |
+
timescale: __privateGet(this, _options).audio.sampleRate,
|
| 1595 |
+
samples: [],
|
| 1596 |
+
finalizedChunks: [],
|
| 1597 |
+
currentChunk: null,
|
| 1598 |
+
firstDecodeTimestamp: void 0,
|
| 1599 |
+
lastDecodeTimestamp: -1,
|
| 1600 |
+
timeToSampleTable: [],
|
| 1601 |
+
compositionTimeOffsetTable: [],
|
| 1602 |
+
lastTimescaleUnits: null,
|
| 1603 |
+
lastSample: null,
|
| 1604 |
+
compactlyCodedChunkTable: []
|
| 1605 |
+
});
|
| 1606 |
+
if (__privateGet(this, _options).audio.codec === "aac") {
|
| 1607 |
+
let guessedCodecPrivate = __privateMethod(this, _generateMpeg4AudioSpecificConfig, generateMpeg4AudioSpecificConfig_fn).call(
|
| 1608 |
+
this,
|
| 1609 |
+
2,
|
| 1610 |
+
// Object type for AAC-LC, since it's the most common
|
| 1611 |
+
__privateGet(this, _options).audio.sampleRate,
|
| 1612 |
+
__privateGet(this, _options).audio.numberOfChannels
|
| 1613 |
+
);
|
| 1614 |
+
__privateGet(this, _audioTrack).info.decoderConfig = {
|
| 1615 |
+
codec: __privateGet(this, _options).audio.codec,
|
| 1616 |
+
description: guessedCodecPrivate,
|
| 1617 |
+
numberOfChannels: __privateGet(this, _options).audio.numberOfChannels,
|
| 1618 |
+
sampleRate: __privateGet(this, _options).audio.sampleRate
|
| 1619 |
+
};
|
| 1620 |
+
}
|
| 1621 |
+
}
|
| 1622 |
+
};
|
| 1623 |
+
_generateMpeg4AudioSpecificConfig = new WeakSet();
|
| 1624 |
+
generateMpeg4AudioSpecificConfig_fn = function(objectType, sampleRate, numberOfChannels) {
|
| 1625 |
+
let frequencyIndices = [96e3, 88200, 64e3, 48e3, 44100, 32e3, 24e3, 22050, 16e3, 12e3, 11025, 8e3, 7350];
|
| 1626 |
+
let frequencyIndex = frequencyIndices.indexOf(sampleRate);
|
| 1627 |
+
let channelConfig = numberOfChannels;
|
| 1628 |
+
let configBits = "";
|
| 1629 |
+
configBits += objectType.toString(2).padStart(5, "0");
|
| 1630 |
+
configBits += frequencyIndex.toString(2).padStart(4, "0");
|
| 1631 |
+
if (frequencyIndex === 15)
|
| 1632 |
+
configBits += sampleRate.toString(2).padStart(24, "0");
|
| 1633 |
+
configBits += channelConfig.toString(2).padStart(4, "0");
|
| 1634 |
+
let paddingLength = Math.ceil(configBits.length / 8) * 8;
|
| 1635 |
+
configBits = configBits.padEnd(paddingLength, "0");
|
| 1636 |
+
let configBytes = new Uint8Array(configBits.length / 8);
|
| 1637 |
+
for (let i = 0; i < configBits.length; i += 8) {
|
| 1638 |
+
configBytes[i / 8] = parseInt(configBits.slice(i, i + 8), 2);
|
| 1639 |
+
}
|
| 1640 |
+
return configBytes;
|
| 1641 |
+
};
|
| 1642 |
+
_createSampleForTrack = new WeakSet();
|
| 1643 |
+
createSampleForTrack_fn = function(track, data, type, timestamp, duration, meta, compositionTimeOffset) {
|
| 1644 |
+
let presentationTimestampInSeconds = timestamp / 1e6;
|
| 1645 |
+
let decodeTimestampInSeconds = (timestamp - (compositionTimeOffset ?? 0)) / 1e6;
|
| 1646 |
+
let durationInSeconds = duration / 1e6;
|
| 1647 |
+
let adjusted = __privateMethod(this, _validateTimestamp, validateTimestamp_fn).call(this, presentationTimestampInSeconds, decodeTimestampInSeconds, track);
|
| 1648 |
+
presentationTimestampInSeconds = adjusted.presentationTimestamp;
|
| 1649 |
+
decodeTimestampInSeconds = adjusted.decodeTimestamp;
|
| 1650 |
+
if (meta?.decoderConfig) {
|
| 1651 |
+
if (track.info.decoderConfig === null) {
|
| 1652 |
+
track.info.decoderConfig = meta.decoderConfig;
|
| 1653 |
+
} else {
|
| 1654 |
+
Object.assign(track.info.decoderConfig, meta.decoderConfig);
|
| 1655 |
+
}
|
| 1656 |
+
}
|
| 1657 |
+
let sample = {
|
| 1658 |
+
presentationTimestamp: presentationTimestampInSeconds,
|
| 1659 |
+
decodeTimestamp: decodeTimestampInSeconds,
|
| 1660 |
+
duration: durationInSeconds,
|
| 1661 |
+
data,
|
| 1662 |
+
size: data.byteLength,
|
| 1663 |
+
type,
|
| 1664 |
+
// Will be refined once the next sample comes in
|
| 1665 |
+
timescaleUnitsToNextSample: intoTimescale(durationInSeconds, track.timescale)
|
| 1666 |
+
};
|
| 1667 |
+
return sample;
|
| 1668 |
+
};
|
| 1669 |
+
_addSampleToTrack = new WeakSet();
|
| 1670 |
+
addSampleToTrack_fn = function(track, sample) {
|
| 1671 |
+
if (__privateGet(this, _options).fastStart !== "fragmented") {
|
| 1672 |
+
track.samples.push(sample);
|
| 1673 |
+
}
|
| 1674 |
+
const sampleCompositionTimeOffset = intoTimescale(sample.presentationTimestamp - sample.decodeTimestamp, track.timescale);
|
| 1675 |
+
if (track.lastTimescaleUnits !== null) {
|
| 1676 |
+
let timescaleUnits = intoTimescale(sample.decodeTimestamp, track.timescale, false);
|
| 1677 |
+
let delta = Math.round(timescaleUnits - track.lastTimescaleUnits);
|
| 1678 |
+
track.lastTimescaleUnits += delta;
|
| 1679 |
+
track.lastSample.timescaleUnitsToNextSample = delta;
|
| 1680 |
+
if (__privateGet(this, _options).fastStart !== "fragmented") {
|
| 1681 |
+
let lastTableEntry = last(track.timeToSampleTable);
|
| 1682 |
+
if (lastTableEntry.sampleCount === 1) {
|
| 1683 |
+
lastTableEntry.sampleDelta = delta;
|
| 1684 |
+
lastTableEntry.sampleCount++;
|
| 1685 |
+
} else if (lastTableEntry.sampleDelta === delta) {
|
| 1686 |
+
lastTableEntry.sampleCount++;
|
| 1687 |
+
} else {
|
| 1688 |
+
lastTableEntry.sampleCount--;
|
| 1689 |
+
track.timeToSampleTable.push({
|
| 1690 |
+
sampleCount: 2,
|
| 1691 |
+
sampleDelta: delta
|
| 1692 |
+
});
|
| 1693 |
+
}
|
| 1694 |
+
const lastCompositionTimeOffsetTableEntry = last(track.compositionTimeOffsetTable);
|
| 1695 |
+
if (lastCompositionTimeOffsetTableEntry.sampleCompositionTimeOffset === sampleCompositionTimeOffset) {
|
| 1696 |
+
lastCompositionTimeOffsetTableEntry.sampleCount++;
|
| 1697 |
+
} else {
|
| 1698 |
+
track.compositionTimeOffsetTable.push({
|
| 1699 |
+
sampleCount: 1,
|
| 1700 |
+
sampleCompositionTimeOffset
|
| 1701 |
+
});
|
| 1702 |
+
}
|
| 1703 |
+
}
|
| 1704 |
+
} else {
|
| 1705 |
+
track.lastTimescaleUnits = 0;
|
| 1706 |
+
if (__privateGet(this, _options).fastStart !== "fragmented") {
|
| 1707 |
+
track.timeToSampleTable.push({
|
| 1708 |
+
sampleCount: 1,
|
| 1709 |
+
sampleDelta: intoTimescale(sample.duration, track.timescale)
|
| 1710 |
+
});
|
| 1711 |
+
track.compositionTimeOffsetTable.push({
|
| 1712 |
+
sampleCount: 1,
|
| 1713 |
+
sampleCompositionTimeOffset
|
| 1714 |
+
});
|
| 1715 |
+
}
|
| 1716 |
+
}
|
| 1717 |
+
track.lastSample = sample;
|
| 1718 |
+
let beginNewChunk = false;
|
| 1719 |
+
if (!track.currentChunk) {
|
| 1720 |
+
beginNewChunk = true;
|
| 1721 |
+
} else {
|
| 1722 |
+
let currentChunkDuration = sample.presentationTimestamp - track.currentChunk.startTimestamp;
|
| 1723 |
+
if (__privateGet(this, _options).fastStart === "fragmented") {
|
| 1724 |
+
let mostImportantTrack = __privateGet(this, _videoTrack) ?? __privateGet(this, _audioTrack);
|
| 1725 |
+
const chunkDuration = __privateGet(this, _options).minFragmentDuration ?? 1;
|
| 1726 |
+
if (track === mostImportantTrack && sample.type === "key" && currentChunkDuration >= chunkDuration) {
|
| 1727 |
+
beginNewChunk = true;
|
| 1728 |
+
__privateMethod(this, _finalizeFragment, finalizeFragment_fn).call(this);
|
| 1729 |
+
}
|
| 1730 |
+
} else {
|
| 1731 |
+
beginNewChunk = currentChunkDuration >= 0.5;
|
| 1732 |
+
}
|
| 1733 |
+
}
|
| 1734 |
+
if (beginNewChunk) {
|
| 1735 |
+
if (track.currentChunk) {
|
| 1736 |
+
__privateMethod(this, _finalizeCurrentChunk, finalizeCurrentChunk_fn).call(this, track);
|
| 1737 |
+
}
|
| 1738 |
+
track.currentChunk = {
|
| 1739 |
+
startTimestamp: sample.presentationTimestamp,
|
| 1740 |
+
samples: []
|
| 1741 |
+
};
|
| 1742 |
+
}
|
| 1743 |
+
track.currentChunk.samples.push(sample);
|
| 1744 |
+
};
|
| 1745 |
+
_validateTimestamp = new WeakSet();
|
| 1746 |
+
validateTimestamp_fn = function(presentationTimestamp, decodeTimestamp, track) {
|
| 1747 |
+
const strictTimestampBehavior = __privateGet(this, _options).firstTimestampBehavior === "strict";
|
| 1748 |
+
const noLastDecodeTimestamp = track.lastDecodeTimestamp === -1;
|
| 1749 |
+
const timestampNonZero = decodeTimestamp !== 0;
|
| 1750 |
+
if (strictTimestampBehavior && noLastDecodeTimestamp && timestampNonZero) {
|
| 1751 |
+
throw new Error(
|
| 1752 |
+
`The first chunk for your media track must have a timestamp of 0 (received DTS=${decodeTimestamp}).Non-zero first timestamps are often caused by directly piping frames or audio data from a MediaStreamTrack into the encoder. Their timestamps are typically relative to the age of thedocument, which is probably what you want.
|
| 1753 |
+
|
| 1754 |
+
If you want to offset all timestamps of a track such that the first one is zero, set firstTimestampBehavior: 'offset' in the options.
|
| 1755 |
+
`
|
| 1756 |
+
);
|
| 1757 |
+
} else if (__privateGet(this, _options).firstTimestampBehavior === "offset" || __privateGet(this, _options).firstTimestampBehavior === "cross-track-offset") {
|
| 1758 |
+
if (track.firstDecodeTimestamp === void 0) {
|
| 1759 |
+
track.firstDecodeTimestamp = decodeTimestamp;
|
| 1760 |
+
}
|
| 1761 |
+
let baseDecodeTimestamp;
|
| 1762 |
+
if (__privateGet(this, _options).firstTimestampBehavior === "offset") {
|
| 1763 |
+
baseDecodeTimestamp = track.firstDecodeTimestamp;
|
| 1764 |
+
} else {
|
| 1765 |
+
baseDecodeTimestamp = Math.min(
|
| 1766 |
+
__privateGet(this, _videoTrack)?.firstDecodeTimestamp ?? Infinity,
|
| 1767 |
+
__privateGet(this, _audioTrack)?.firstDecodeTimestamp ?? Infinity
|
| 1768 |
+
);
|
| 1769 |
+
}
|
| 1770 |
+
decodeTimestamp -= baseDecodeTimestamp;
|
| 1771 |
+
presentationTimestamp -= baseDecodeTimestamp;
|
| 1772 |
+
}
|
| 1773 |
+
if (decodeTimestamp < track.lastDecodeTimestamp) {
|
| 1774 |
+
throw new Error(
|
| 1775 |
+
`Timestamps must be monotonically increasing (DTS went from ${track.lastDecodeTimestamp * 1e6} to ${decodeTimestamp * 1e6}).`
|
| 1776 |
+
);
|
| 1777 |
+
}
|
| 1778 |
+
track.lastDecodeTimestamp = decodeTimestamp;
|
| 1779 |
+
return { presentationTimestamp, decodeTimestamp };
|
| 1780 |
+
};
|
| 1781 |
+
_finalizeCurrentChunk = new WeakSet();
|
| 1782 |
+
finalizeCurrentChunk_fn = function(track) {
|
| 1783 |
+
if (__privateGet(this, _options).fastStart === "fragmented") {
|
| 1784 |
+
throw new Error("Can't finalize individual chunks if 'fastStart' is set to 'fragmented'.");
|
| 1785 |
+
}
|
| 1786 |
+
if (!track.currentChunk)
|
| 1787 |
+
return;
|
| 1788 |
+
track.finalizedChunks.push(track.currentChunk);
|
| 1789 |
+
__privateGet(this, _finalizedChunks).push(track.currentChunk);
|
| 1790 |
+
if (track.compactlyCodedChunkTable.length === 0 || last(track.compactlyCodedChunkTable).samplesPerChunk !== track.currentChunk.samples.length) {
|
| 1791 |
+
track.compactlyCodedChunkTable.push({
|
| 1792 |
+
firstChunk: track.finalizedChunks.length,
|
| 1793 |
+
// 1-indexed
|
| 1794 |
+
samplesPerChunk: track.currentChunk.samples.length
|
| 1795 |
+
});
|
| 1796 |
+
}
|
| 1797 |
+
if (__privateGet(this, _options).fastStart === "in-memory") {
|
| 1798 |
+
track.currentChunk.offset = 0;
|
| 1799 |
+
return;
|
| 1800 |
+
}
|
| 1801 |
+
track.currentChunk.offset = __privateGet(this, _writer).pos;
|
| 1802 |
+
for (let sample of track.currentChunk.samples) {
|
| 1803 |
+
__privateGet(this, _writer).write(sample.data);
|
| 1804 |
+
sample.data = null;
|
| 1805 |
+
}
|
| 1806 |
+
__privateMethod(this, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn).call(this);
|
| 1807 |
+
};
|
| 1808 |
+
_finalizeFragment = new WeakSet();
|
| 1809 |
+
finalizeFragment_fn = function(flushStreamingWriter = true) {
|
| 1810 |
+
if (__privateGet(this, _options).fastStart !== "fragmented") {
|
| 1811 |
+
throw new Error("Can't finalize a fragment unless 'fastStart' is set to 'fragmented'.");
|
| 1812 |
+
}
|
| 1813 |
+
let tracks = [__privateGet(this, _videoTrack), __privateGet(this, _audioTrack)].filter((track) => track && track.currentChunk);
|
| 1814 |
+
if (tracks.length === 0)
|
| 1815 |
+
return;
|
| 1816 |
+
let fragmentNumber = __privateWrapper(this, _nextFragmentNumber)._++;
|
| 1817 |
+
if (fragmentNumber === 1) {
|
| 1818 |
+
let movieBox = moov(tracks, __privateGet(this, _creationTime), true);
|
| 1819 |
+
__privateGet(this, _writer).writeBox(movieBox);
|
| 1820 |
+
}
|
| 1821 |
+
let moofOffset = __privateGet(this, _writer).pos;
|
| 1822 |
+
let moofBox = moof(fragmentNumber, tracks);
|
| 1823 |
+
__privateGet(this, _writer).writeBox(moofBox);
|
| 1824 |
+
{
|
| 1825 |
+
let mdatBox = mdat(false);
|
| 1826 |
+
let totalTrackSampleSize = 0;
|
| 1827 |
+
for (let track of tracks) {
|
| 1828 |
+
for (let sample of track.currentChunk.samples) {
|
| 1829 |
+
totalTrackSampleSize += sample.size;
|
| 1830 |
+
}
|
| 1831 |
+
}
|
| 1832 |
+
let mdatSize = __privateGet(this, _writer).measureBox(mdatBox) + totalTrackSampleSize;
|
| 1833 |
+
if (mdatSize >= 2 ** 32) {
|
| 1834 |
+
mdatBox.largeSize = true;
|
| 1835 |
+
mdatSize = __privateGet(this, _writer).measureBox(mdatBox) + totalTrackSampleSize;
|
| 1836 |
+
}
|
| 1837 |
+
mdatBox.size = mdatSize;
|
| 1838 |
+
__privateGet(this, _writer).writeBox(mdatBox);
|
| 1839 |
+
}
|
| 1840 |
+
for (let track of tracks) {
|
| 1841 |
+
track.currentChunk.offset = __privateGet(this, _writer).pos;
|
| 1842 |
+
track.currentChunk.moofOffset = moofOffset;
|
| 1843 |
+
for (let sample of track.currentChunk.samples) {
|
| 1844 |
+
__privateGet(this, _writer).write(sample.data);
|
| 1845 |
+
sample.data = null;
|
| 1846 |
+
}
|
| 1847 |
+
}
|
| 1848 |
+
let endPos = __privateGet(this, _writer).pos;
|
| 1849 |
+
__privateGet(this, _writer).seek(__privateGet(this, _writer).offsets.get(moofBox));
|
| 1850 |
+
let newMoofBox = moof(fragmentNumber, tracks);
|
| 1851 |
+
__privateGet(this, _writer).writeBox(newMoofBox);
|
| 1852 |
+
__privateGet(this, _writer).seek(endPos);
|
| 1853 |
+
for (let track of tracks) {
|
| 1854 |
+
track.finalizedChunks.push(track.currentChunk);
|
| 1855 |
+
__privateGet(this, _finalizedChunks).push(track.currentChunk);
|
| 1856 |
+
track.currentChunk = null;
|
| 1857 |
+
}
|
| 1858 |
+
if (flushStreamingWriter) {
|
| 1859 |
+
__privateMethod(this, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn).call(this);
|
| 1860 |
+
}
|
| 1861 |
+
};
|
| 1862 |
+
_maybeFlushStreamingTargetWriter = new WeakSet();
|
| 1863 |
+
maybeFlushStreamingTargetWriter_fn = function() {
|
| 1864 |
+
if (__privateGet(this, _writer) instanceof StreamTargetWriter) {
|
| 1865 |
+
__privateGet(this, _writer).flush();
|
| 1866 |
+
}
|
| 1867 |
+
};
|
| 1868 |
+
_ensureNotFinalized = new WeakSet();
|
| 1869 |
+
ensureNotFinalized_fn = function() {
|
| 1870 |
+
if (__privateGet(this, _finalized)) {
|
| 1871 |
+
throw new Error("Cannot add new video or audio chunks after the file has been finalized.");
|
| 1872 |
+
}
|
| 1873 |
+
};
|
| 1874 |
+
export {
|
| 1875 |
+
ArrayBufferTarget,
|
| 1876 |
+
FileSystemWritableFileStreamTarget,
|
| 1877 |
+
Muxer,
|
| 1878 |
+
StreamTarget
|
| 1879 |
+
};
|