@lmliheng/lesson-video 0.0.0-stage → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,632 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Kaggle 房价预测 · 竖屏课件</title>
7
+ <style>
8
+ /* 1080x1920 竖屏课件:所有样式都从时间 t 计算,不含 CSS 过渡,保证逐帧可复现 */
9
+ :root {
10
+ --bg: #000;
11
+ --surface: #0d0d0d;
12
+ --border: #1f1f1f;
13
+ --border-2: #303030;
14
+ --fg: #f3f4f6;
15
+ --fg-muted: #9ca3af;
16
+ --fg-faint: #6b7280;
17
+ --brand: #8ab4f8;
18
+ --brand-deep: #4285f4;
19
+ --brand-tint: #1a2b45;
20
+ --amber: #f0b429;
21
+ --ease: cubic-bezier(0.2, 0.7, 0.3, 1);
22
+ }
23
+ * {
24
+ box-sizing: border-box;
25
+ -webkit-font-smoothing: antialiased;
26
+ }
27
+ html,
28
+ body {
29
+ margin: 0;
30
+ padding: 0;
31
+ width: 1080px;
32
+ height: 1920px;
33
+ overflow: hidden;
34
+ background: var(--bg);
35
+ color: var(--fg);
36
+ font-family:
37
+ "Microsoft YaHei", "PingFang SC", ui-sans-serif, system-ui, "Segoe UI", Roboto, sans-serif;
38
+ }
39
+ code,
40
+ pre,
41
+ .mono {
42
+ font-family: Consolas, "Cascadia Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
43
+ }
44
+
45
+ /* 常驻背景:点阵网格,随整条片子极缓慢平移 —— 页面切换时的“连续性”来源 */
46
+ #bg {
47
+ position: absolute;
48
+ inset: 0;
49
+ background-image: radial-gradient(rgba(138, 180, 248, 0.1) 1.6px, transparent 1.6px);
50
+ background-size: 72px 72px;
51
+ }
52
+
53
+ #bar {
54
+ position: absolute;
55
+ left: 0;
56
+ top: 0;
57
+ width: 1080px;
58
+ height: 8px;
59
+ background: #141414;
60
+ z-index: 40;
61
+ }
62
+ #barfill {
63
+ height: 100%;
64
+ width: 0;
65
+ background: var(--brand-deep);
66
+ }
67
+
68
+ #brand {
69
+ position: absolute;
70
+ left: 150px;
71
+ top: 172px;
72
+ z-index: 35;
73
+ display: flex;
74
+ align-items: center;
75
+ gap: 18px;
76
+ font-size: 30px;
77
+ color: var(--fg-faint);
78
+ letter-spacing: 0.02em;
79
+ }
80
+ #brand .mark {
81
+ width: 38px;
82
+ height: 38px;
83
+ border-radius: 10px 10px 10px 4px;
84
+ background: var(--brand-deep);
85
+ display: flex;
86
+ align-items: center;
87
+ justify-content: center;
88
+ font-size: 24px;
89
+ font-weight: 700;
90
+ color: #08111f;
91
+ }
92
+ /* 常驻水印:右上角,与正文右边界对齐 */
93
+ #wm {
94
+ position: absolute;
95
+ right: 178px;
96
+ top: 180px;
97
+ z-index: 35;
98
+ font-size: 26px;
99
+ letter-spacing: 0.02em;
100
+ color: var(--fg-faint);
101
+ opacity: 0.62;
102
+ font-family: Consolas, "Cascadia Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
103
+ }
104
+
105
+ .scene {
106
+ position: absolute;
107
+ left: 0;
108
+ top: 0;
109
+ width: 1080px;
110
+ height: 1920px;
111
+ padding: 300px 185px 450px 150px;
112
+ display: none;
113
+ flex-direction: column;
114
+ justify-content: flex-start;
115
+ will-change: transform, opacity;
116
+ }
117
+ .scene.on {
118
+ display: flex;
119
+ }
120
+ .scene.center {
121
+ justify-content: center;
122
+ padding-bottom: 620px;
123
+ }
124
+
125
+ .kicker {
126
+ font-size: 30px;
127
+ letter-spacing: 0.2em;
128
+ text-transform: uppercase;
129
+ color: var(--brand);
130
+ margin-bottom: 30px;
131
+ }
132
+ /* text-wrap:balance —— 标题折行时左右均衡,避免最后一行只剩一两个字(安全区变窄后尤其明显) */
133
+ .stitle {
134
+ font-size: 68px;
135
+ font-weight: 600;
136
+ letter-spacing: -0.025em;
137
+ line-height: 1.24;
138
+ margin: 0 0 62px;
139
+ text-wrap: balance;
140
+ }
141
+
142
+ /* 开场 */
143
+ .hero {
144
+ font-size: 132px;
145
+ font-weight: 600;
146
+ letter-spacing: -0.045em;
147
+ line-height: 1.06;
148
+ margin: 0 0 30px;
149
+ text-wrap: balance;
150
+ }
151
+ .hero-sub {
152
+ font-size: 48px;
153
+ color: var(--fg-muted);
154
+ line-height: 1.5;
155
+ margin: 0 0 54px;
156
+ }
157
+ .rule {
158
+ width: 220px;
159
+ height: 5px;
160
+ background: var(--brand-deep);
161
+ border-radius: 3px;
162
+ margin-bottom: 48px;
163
+ }
164
+ .tags {
165
+ display: flex;
166
+ flex-wrap: wrap;
167
+ gap: 16px;
168
+ }
169
+ .tag {
170
+ border: 1px solid var(--border-2);
171
+ border-radius: 9999px;
172
+ padding: 12px 26px;
173
+ font-size: 32px;
174
+ color: var(--fg-muted);
175
+ }
176
+
177
+ /* 要点 */
178
+ .item {
179
+ display: flex;
180
+ gap: 26px;
181
+ align-items: flex-start;
182
+ border: 1px solid var(--border);
183
+ border-radius: 14px;
184
+ background: var(--surface);
185
+ padding: 34px 38px;
186
+ margin-bottom: 30px;
187
+ }
188
+ .item .dot {
189
+ flex: 0 0 auto;
190
+ width: 16px;
191
+ height: 16px;
192
+ border-radius: 9999px;
193
+ background: var(--brand-deep);
194
+ margin-top: 20px;
195
+ }
196
+ .item .tx {
197
+ font-size: 42px;
198
+ line-height: 1.5;
199
+ }
200
+ .item .tx em {
201
+ font-style: normal;
202
+ color: var(--brand);
203
+ }
204
+
205
+ /* 数学 */
206
+ .formula-card {
207
+ border: 1px solid var(--border);
208
+ border-radius: 14px;
209
+ background: var(--surface);
210
+ padding: 38px 40px;
211
+ margin-bottom: 32px;
212
+ border-left: 6px solid var(--border-2);
213
+ }
214
+ .formula-card.hi {
215
+ border-left-color: var(--brand-deep);
216
+ background: var(--brand-tint);
217
+ }
218
+ .fc-label {
219
+ font-size: 30px;
220
+ color: var(--fg-faint);
221
+ margin-bottom: 22px;
222
+ }
223
+ .fc-body {
224
+ font-size: 50px;
225
+ line-height: 1.6;
226
+ color: var(--fg);
227
+ }
228
+ .fc-body sup,
229
+ .fc-body sub {
230
+ font-size: 0.62em;
231
+ }
232
+ .frac {
233
+ display: inline-flex;
234
+ flex-direction: column;
235
+ vertical-align: middle;
236
+ text-align: center;
237
+ margin: 0 6px;
238
+ font-size: 0.78em;
239
+ line-height: 1.15;
240
+ }
241
+ .frac b {
242
+ font-weight: 600;
243
+ border-bottom: 2px solid currentColor;
244
+ padding: 0 8px;
245
+ }
246
+ .frac i {
247
+ font-style: normal;
248
+ padding: 0 8px;
249
+ }
250
+
251
+ /* 密集型:公式卡 ≥4 张时收紧,保证不越过 y=1470 的字幕安全线 */
252
+ .scene.dense .stitle {
253
+ font-size: 56px;
254
+ margin-bottom: 36px;
255
+ }
256
+ .scene.dense .formula-card {
257
+ padding: 22px 32px;
258
+ margin-bottom: 18px;
259
+ }
260
+ .scene.dense .fc-label {
261
+ font-size: 24px;
262
+ margin-bottom: 10px;
263
+ }
264
+ .scene.dense .fc-body {
265
+ font-size: 38px;
266
+ line-height: 1.5;
267
+ }
268
+
269
+ /* 代码 */
270
+ .card {
271
+ border: 1px solid var(--border);
272
+ border-radius: 14px;
273
+ background: var(--surface);
274
+ overflow: hidden;
275
+ }
276
+ .card-hd {
277
+ display: flex;
278
+ align-items: center;
279
+ gap: 12px;
280
+ padding: 20px 26px;
281
+ border-bottom: 1px solid var(--border);
282
+ font-size: 26px;
283
+ color: var(--fg-faint);
284
+ }
285
+ .card-hd .dots {
286
+ display: flex;
287
+ gap: 9px;
288
+ }
289
+ .card-hd .dots i {
290
+ width: 14px;
291
+ height: 14px;
292
+ border-radius: 9999px;
293
+ background: var(--border-2);
294
+ display: block;
295
+ }
296
+ .card-bd {
297
+ padding: 22px 24px 26px;
298
+ font-size: 23px;
299
+ line-height: 1.7;
300
+ white-space: pre;
301
+ overflow: hidden;
302
+ }
303
+ .ln {
304
+ display: block;
305
+ }
306
+ .k {
307
+ color: #ff7b72;
308
+ } /* keyword */
309
+ .s {
310
+ color: #a5d6ff;
311
+ } /* string */
312
+ .c {
313
+ color: #6b7280;
314
+ } /* comment */
315
+ .n {
316
+ color: #79c0ff;
317
+ } /* number */
318
+ .f {
319
+ color: #d2a8ff;
320
+ } /* call */
321
+ .m {
322
+ color: var(--fg-muted);
323
+ }
324
+
325
+ .notes {
326
+ margin-top: 30px;
327
+ }
328
+ .note {
329
+ display: flex;
330
+ gap: 20px;
331
+ font-size: 32px;
332
+ color: var(--fg-muted);
333
+ line-height: 1.55;
334
+ margin-bottom: 18px;
335
+ }
336
+ .note .n-dot {
337
+ flex: 0 0 auto;
338
+ width: 12px;
339
+ height: 12px;
340
+ border-radius: 9999px;
341
+ background: var(--brand);
342
+ margin-top: 16px;
343
+ }
344
+
345
+ /* 结尾 */
346
+ .outro {
347
+ font-size: 104px;
348
+ font-weight: 600;
349
+ letter-spacing: -0.04em;
350
+ line-height: 1.12;
351
+ margin: 0 0 28px;
352
+ text-wrap: balance;
353
+ }
354
+ .outro-sub {
355
+ font-size: 44px;
356
+ color: var(--fg-muted);
357
+ margin: 0 0 64px;
358
+ }
359
+ .cta {
360
+ display: inline-block;
361
+ background: var(--fg);
362
+ color: #111827;
363
+ font-size: 40px;
364
+ font-weight: 600;
365
+ border-radius: 10px;
366
+ padding: 22px 46px;
367
+ }
368
+
369
+ /* 字幕 */
370
+ #subs {
371
+ position: absolute;
372
+ left: 0;
373
+ top: 1490px;
374
+ width: 1080px;
375
+ height: 230px;
376
+ z-index: 30;
377
+ text-align: center;
378
+ }
379
+ .sub {
380
+ position: absolute;
381
+ left: 110px;
382
+ width: 860px;
383
+ text-align: center;
384
+ font-size: 46px;
385
+ line-height: 1.34;
386
+ font-weight: 600;
387
+ text-shadow: 0 2px 18px rgba(0, 0, 0, 0.95);
388
+ }
389
+ .sub.prev {
390
+ top: 0;
391
+ font-size: 36px;
392
+ font-weight: 400;
393
+ color: var(--fg-faint);
394
+ }
395
+ .sub.cur {
396
+ top: 66px;
397
+ }
398
+ </style>
399
+ </head>
400
+ <body>
401
+ <div id="bg"></div>
402
+ <div id="brand"><span class="mark" id="brand-mark"></span><span id="brand-text"></span></div>
403
+ <div id="wm"></div>
404
+ <div id="scenes"></div>
405
+ <div id="subs">
406
+ <div class="sub prev" id="sub-prev"></div>
407
+ <div class="sub cur" id="sub-cur"></div>
408
+ </div>
409
+ <div id="bar"><div id="barfill"></div></div>
410
+
411
+ <script>
412
+ "use strict";
413
+ const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
414
+
415
+ /* ---------- 极简 Python 语法高亮(离线、确定性) ---------- */
416
+ const PY_MASTER =
417
+ /(#[^\n]*)|('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*")|\b(\d+\.?\d*)\b|\b(from|import|def|class|return|if|elif|else|for|in|while|with|as|not|and|or|None|True|False|lambda|self|print)\b|([A-Za-z_][A-Za-z_0-9]*)(?=\()/g;
418
+ function highlight(src) {
419
+ let out = "",
420
+ last = 0,
421
+ m;
422
+ PY_MASTER.lastIndex = 0;
423
+ while ((m = PY_MASTER.exec(src)) !== null) {
424
+ out += esc(src.slice(last, m.index));
425
+ const [full, comment, str, num, kw, fn] = m;
426
+ const cls = comment ? "c" : str ? "s" : num ? "n" : kw ? "k" : "f";
427
+ out += `<span class="${cls}">${esc(full)}</span>`;
428
+ last = m.index + full.length;
429
+ }
430
+ out += esc(src.slice(last));
431
+ return out;
432
+ }
433
+ /* 代码行逐行出现:整段在 reveal 秒内走完,长幕短幕都合适 */
434
+ function codeLines(code, dur, reveal) {
435
+ const lines = code.split("\n");
436
+ const n = lines.length;
437
+ const secs = Math.min(reveal || 4.5, (dur || 10) * 0.55);
438
+ const start = 0.04 * (dur || 10);
439
+ const step = n > 1 ? (secs - start) / (n - 1) : 0;
440
+ return lines
441
+ .map(
442
+ (l, i) =>
443
+ `<span class="ln" data-at="${Math.max(0, start + i * step).toFixed(2)}">${l ? highlight(l) : " "}</span>`,
444
+ )
445
+ .join("");
446
+ }
447
+
448
+ /* ---------- 各幕模板 ---------- */
449
+ const T = {
450
+ title: (s) => `
451
+ <div class="kicker" data-r="0.02">${esc(s.kicker)}</div>
452
+ <h1 class="hero" data-r="0.06">${esc(s.title)}</h1>
453
+ <div class="rule" data-r="0.12"></div>
454
+ <p class="hero-sub" data-r="0.16">${esc(s.sub)}</p>
455
+ <div class="tags">${(s.tags || []).map((t, i) => `<span class="tag" data-r="${(0.24 + i * 0.07).toFixed(2)}">${esc(t)}</span>`).join("")}</div>`,
456
+
457
+ points: (s) => `
458
+ <div class="kicker" data-r="0.02">${esc(s.kicker)}</div>
459
+ <h2 class="stitle" data-r="0.06">${esc(s.title)}</h2>
460
+ ${(s.items || [])
461
+ .map(
462
+ (t, i) => `<div class="item" data-r="${(0.16 + i * 0.17).toFixed(2)}">
463
+ <span class="dot"></span><div class="tx">${t}</div></div>`,
464
+ )
465
+ .join("")}`,
466
+
467
+ math: (s) => `
468
+ <div class="kicker" data-r="0.02">${esc(s.kicker)}</div>
469
+ <h2 class="stitle" data-r="0.06">${esc(s.title)}</h2>
470
+ ${(s.steps || [])
471
+ .map(
472
+ (st, i) => `<div class="formula-card${st.hi ? " hi" : ""}" ${at(st, 0.16 + i * 0.24)}>
473
+ <div class="fc-label">${esc(st.label)}</div>
474
+ <div class="fc-body">${st.formula}</div></div>`,
475
+ )
476
+ .join("")}`,
477
+
478
+ code: (s) => `
479
+ <div class="kicker" data-r="0.02">${esc(s.kicker)}</div>
480
+ <h2 class="stitle" data-r="0.06">${esc(s.title)}</h2>
481
+ <div class="card" data-r="0.10">
482
+ <div class="card-hd"><span class="dots"><i></i><i></i><i></i></span>${esc(s.file)}</div>
483
+ <div class="card-bd">${codeLines(s.code, s.dur, s.code_reveal)}</div>
484
+ </div>`,
485
+
486
+ source: (s) => `
487
+ <div class="kicker" data-r="0.02">${esc(s.kicker)}</div>
488
+ <h2 class="stitle" data-r="0.06">${esc(s.title)}</h2>
489
+ <div class="card" data-r="0.10">
490
+ <div class="card-hd"><span class="dots"><i></i><i></i><i></i></span>${esc(s.file)}</div>
491
+ <div class="card-bd">${codeLines(s.code, s.dur, s.code_reveal)}</div>
492
+ </div>
493
+ <div class="notes">${(s.notes || [])
494
+ .map(
495
+ (t, i) =>
496
+ `<div class="note" ${at(t, 0.56 + i * 0.12)}><span class="n-dot"></span><span>${esc(typeof t === "string" ? t : t.text)}</span></div>`,
497
+ )
498
+ .join("")}</div>`,
499
+
500
+ outro: (s) => `
501
+ <div class="kicker" data-r="0.02">${esc(s.kicker)}</div>
502
+ <h2 class="outro" data-r="0.08">${esc(s.title)}</h2>
503
+ <p class="outro-sub" data-r="0.16">${esc(s.sub)}</p>
504
+ <div ${at({ at: s.cta_at }, 0.3)}><span class="cta">${esc(s.cta)}</span></div>`,
505
+ };
506
+
507
+ /* 出现时机:优先用脚本里写死的秒数 at(贴着讲解走),否则按场景进度比例 r */
508
+ function at(item, r) {
509
+ return item && item.at !== undefined ? `data-at="${item.at}"` : `data-r="${r.toFixed(2)}"`;
510
+ }
511
+
512
+ /* ---------- 缓动:与设计规范同一条 cubic-bezier(.2,.7,.3,1) ---------- */
513
+ function cubicBezier(x1, y1, x2, y2) {
514
+ const A = (a, b) => 1 - 3 * b + 3 * a,
515
+ B = (a, b) => 3 * b - 6 * a,
516
+ C = (a) => 3 * a;
517
+ const calc = (t, a, b) => ((A(a, b) * t + B(a, b)) * t + C(a)) * t;
518
+ const slope = (t, a, b) => 3 * A(a, b) * t * t + 2 * B(a, b) * t + C(a);
519
+ return (x) => {
520
+ if (x <= 0) return 0;
521
+ if (x >= 1) return 1;
522
+ let t = x;
523
+ for (let i = 0; i < 8; i++) {
524
+ const d = slope(t, x1, x2);
525
+ if (Math.abs(d) < 1e-6) break;
526
+ t -= (calc(t, x1, x2) - x) / d;
527
+ }
528
+ return calc(t, y1, y2);
529
+ };
530
+ }
531
+ const ease = cubicBezier(0.2, 0.7, 0.3, 1);
532
+ const clamp01 = (v) => (v < 0 ? 0 : v > 1 ? 1 : v);
533
+
534
+ /* ---------- 全局状态 ---------- */
535
+ let L = null,
536
+ scenes = [];
537
+ const els = [];
538
+
539
+ function __init(lesson) {
540
+ L = lesson;
541
+ const meta = lesson.meta;
542
+ const brand = meta.brand || meta.series || "";
543
+ document.getElementById("brand-text").textContent = brand;
544
+ // 台标字母与右上角水印都来自数据:meta.mark / meta.wm,没给就自动取首字 / 整块隐藏
545
+ document.getElementById("brand-mark").textContent =
546
+ meta.mark || brand.trim().slice(0, 1) || "▶";
547
+ const wm = document.getElementById("wm");
548
+ wm.textContent = meta.wm || "";
549
+ wm.style.display = meta.wm ? "" : "none";
550
+ const host = document.getElementById("scenes");
551
+ scenes = lesson.scenes;
552
+ scenes.forEach((s) => {
553
+ const el = document.createElement("div");
554
+ el.className = "scene";
555
+ if (s.type === "title" || s.type === "outro") el.classList.add("center");
556
+ if (s.type === "math" && (s.steps || []).length >= 4) el.classList.add("dense");
557
+ el.innerHTML = T[s.type](s);
558
+ host.appendChild(el);
559
+ els.push({
560
+ def: s,
561
+ el,
562
+ rev: [...el.querySelectorAll("[data-r],[data-at]")].map((n) => ({
563
+ n,
564
+ // data-at 是相对本幕的秒数;data-r 是场景进度比例,统一折成秒
565
+ at:
566
+ n.dataset.at !== undefined
567
+ ? parseFloat(n.dataset.at)
568
+ : parseFloat(n.dataset.r) * s.dur,
569
+ })),
570
+ });
571
+ });
572
+ }
573
+
574
+ function seek(t) {
575
+ const total = L.duration;
576
+ document.getElementById("barfill").style.width = clamp01(t / total) * 100 + "%";
577
+ document.getElementById("bg").style.backgroundPosition =
578
+ `${(-t * 11).toFixed(1)}px ${(-t * 7).toFixed(1)}px`;
579
+
580
+ // 字幕:当前句 + 上一句
581
+ const subs = L.subtitles;
582
+ let idx = -1;
583
+ for (let i = 0; i < subs.length; i++) if (t >= subs[i].start) idx = i;
584
+ const cur = subs[idx],
585
+ prev = idx > 0 ? subs[idx - 1] : null;
586
+ const elCur = document.getElementById("sub-cur"),
587
+ elPrev = document.getElementById("sub-prev");
588
+ if (cur && t <= cur.end + 0.5) {
589
+ elCur.textContent = cur.text;
590
+ elCur.style.opacity = Math.min(
591
+ clamp01((t - cur.start) / 0.14),
592
+ clamp01((cur.end + 0.28 - t) / 0.2),
593
+ );
594
+ if (prev && t < cur.start + 0.9) {
595
+ elPrev.textContent = prev.text;
596
+ elPrev.style.opacity = clamp01((cur.start + 0.9 - t) / 0.5) * 0.75;
597
+ } else {
598
+ elPrev.textContent = "";
599
+ elPrev.style.opacity = 0;
600
+ }
601
+ } else {
602
+ elCur.style.opacity = 0;
603
+ elPrev.style.opacity = 0;
604
+ }
605
+
606
+ els.forEach(({ def, el, rev }, i) => {
607
+ const local = t - def.start;
608
+ if (local < 0 || local > def.dur) {
609
+ el.classList.remove("on");
610
+ return;
611
+ }
612
+ el.classList.add("on");
613
+ const p = clamp01(local / def.dur);
614
+ const enter = ease(clamp01(local / 0.42)); // 进场 0.42s
615
+ const leave = ease(clamp01((def.dur - local) / 0.3)); // 出场 0.30s
616
+ const a = Math.min(enter, leave);
617
+ el.style.opacity = a;
618
+ el.style.transform = `translateY(${((1 - enter) * 46 - (1 - leave) * 34).toFixed(2)}px)`;
619
+ rev.forEach(({ n, at }) => {
620
+ // 逐帧算,不用 CSS 过渡 —— 进场固定 0.38s,与幕长无关
621
+ const q = ease(clamp01((local - at) / 0.38));
622
+ n.style.opacity = q;
623
+ n.style.transform = `translateY(${((1 - q) * 24).toFixed(2)}px)`;
624
+ });
625
+ });
626
+ }
627
+
628
+ window.__init = __init;
629
+ window.seek = seek;
630
+ </script>
631
+ </body>
632
+ </html>