kaizen-agent 1.47.0 → 1.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -84
- package/cli/build-web.ts +4 -2
- package/cli/hermes.ts +65 -0
- package/cli/install.ts +24 -10
- package/cli/localsend.test.ts +84 -0
- package/cli/localsend.ts +131 -0
- package/cli/notes.test.ts +6 -4
- package/cli/settings.ts +1 -0
- package/cli/state.test.ts +37 -0
- package/cli/state.ts +35 -20
- package/cli/video.test.ts +479 -0
- package/cli/video.ts +507 -0
- package/cli/web.ts +127 -1
- package/cli/worktree.test.ts +3 -1
- package/package.json +1 -1
- package/skills/kaizen/config.default.yml +4 -0
- package/skills/media/kaizen-book/SKILL.md +107 -0
- package/skills/media/kaizen-book/assets/reader.html +440 -0
- package/skills/media/kaizen-book/assets/styles.json +45 -0
- package/skills/media/kaizen-book/examples/book.json +14 -0
- package/skills/media/kaizen-book/examples/images/reach.svg +1 -0
- package/skills/media/kaizen-book/examples/manuscript.md +127 -0
- package/skills/media/kaizen-book/references/interview.md +85 -0
- package/skills/media/kaizen-book/references/proofread.md +47 -0
- package/skills/media/kaizen-book/references/writing.md +53 -0
- package/skills/media/kaizen-book/scripts/build_book.py +279 -0
- package/skills/media/kaizen-book/scripts/check.sh +10 -0
- package/skills/media/kaizen-book/scripts/check_book.py +128 -0
- package/skills/media/kaizen-book/scripts/deploy.py +58 -0
- package/skills/media/kaizen-book/scripts/pdf.mjs +99 -0
- package/skills/media/kaizen-book/scripts/setup.sh +9 -0
- package/skills/media/kaizen-book/scripts/verify.mjs +95 -0
- package/skills/{story-deck → media/kaizen-deck}/SKILL.md +1 -1
- package/skills/{story-deck → media/kaizen-deck}/scripts/check.sh +1 -1
- package/skills/{story-deck → media/kaizen-deck}/scripts/install_fonts.py +2 -2
- package/skills/media/kaizen-video/SKILL.md +112 -0
- package/skills/media/kaizen-video/assets/CREDITS.txt +16 -0
- package/skills/media/kaizen-video/assets/film-fonts/Fraunces.ttf +0 -0
- package/skills/media/kaizen-video/assets/film-fonts/Inter.ttf +0 -0
- package/skills/media/kaizen-video/assets/film-fonts/PlayfairDisplay-Italic.ttf +0 -0
- package/skills/media/kaizen-video/assets/film-fonts/PlayfairDisplay.ttf +0 -0
- package/skills/media/kaizen-video/assets/fonts/Fredoka.ttf +0 -0
- package/skills/media/kaizen-video/assets/fonts/Montserrat.ttf +0 -0
- package/skills/media/kaizen-video/assets/fonts/SpecialElite.ttf +0 -0
- package/skills/media/kaizen-video/assets/music/investigations.mp3 +0 -0
- package/skills/media/kaizen-video/assets/music/lights.mp3 +0 -0
- package/skills/media/kaizen-video/assets/music/sunny.mp3 +0 -0
- package/skills/media/kaizen-video/examples/config.json +152 -0
- package/skills/media/kaizen-video/examples/sample-film/README.md +10 -0
- package/skills/media/kaizen-video/examples/sample-film/film.json +63 -0
- package/skills/media/kaizen-video/film.html +246 -0
- package/skills/media/kaizen-video/hyperframes.json +12 -0
- package/skills/media/kaizen-video/presets/educational-warning/CREDITS.txt +3 -0
- package/skills/media/kaizen-video/presets/educational-warning/README.md +26 -0
- package/skills/media/kaizen-video/presets/educational-warning/assets/sfx/ending.wav +0 -0
- package/skills/media/kaizen-video/presets/educational-warning/assets/sfx/reveal.wav +0 -0
- package/skills/media/kaizen-video/presets/educational-warning/assets/sfx/shutter.wav +0 -0
- package/skills/media/kaizen-video/presets/educational-warning/assets/sfx/stamp.wav +0 -0
- package/skills/media/kaizen-video/presets/educational-warning/index.html +236 -0
- package/skills/media/kaizen-video/presets/educational-warning/package.json +10 -0
- package/skills/media/kaizen-video/presets/film-cinematic-drama/CREDITS.txt +1 -0
- package/skills/media/kaizen-video/presets/film-cinematic-drama/README.md +2 -0
- package/skills/media/kaizen-video/presets/film-cinematic-drama/style.json +39 -0
- package/skills/media/kaizen-video/presets/film-fun-energetic/CREDITS.txt +1 -0
- package/skills/media/kaizen-video/presets/film-fun-energetic/README.md +2 -0
- package/skills/media/kaizen-video/presets/film-fun-energetic/style.json +39 -0
- package/skills/media/kaizen-video/presets/film-promo-booth/CREDITS.txt +1 -0
- package/skills/media/kaizen-video/presets/film-promo-booth/README.md +2 -0
- package/skills/media/kaizen-video/presets/film-promo-booth/style.json +39 -0
- package/skills/media/kaizen-video/presets/film-viral-social/CREDITS.txt +1 -0
- package/skills/media/kaizen-video/presets/film-viral-social/README.md +2 -0
- package/skills/media/kaizen-video/presets/film-viral-social/style.json +39 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/CREDITS.txt +3 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/README.md +35 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/boing.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/chime.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/ding.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/ending.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/pop.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/reveal.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/shutter.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/stamp.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/whoosh.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/index.html +260 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/package.json +10 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/CREDITS.txt +3 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/README.md +52 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/fonts/InstrumentSerif.ttf +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/fonts/SpaceMono.ttf +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/boing.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/ding.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/ending.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/pop.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/reveal.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/shutter.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/stamp.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/tick.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/whoosh.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/index.html +272 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/package.json +10 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/CREDITS.txt +12 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/README.md +51 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/clock.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/ding.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/ending.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/heart.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/projector.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/reveal.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/shutter.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/stamp.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/whoosh.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/index.html +260 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/package.json +10 -0
- package/skills/media/kaizen-video/presets/tiktok-scam-alert/CREDITS.txt +8 -0
- package/skills/media/kaizen-video/presets/tiktok-scam-alert/README.md +24 -0
- package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/clock.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/ding.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/ending.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/heart.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/reveal.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/shutter.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/stamp.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/whoosh.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-scam-alert/index.html +266 -0
- package/skills/media/kaizen-video/presets/tiktok-scam-alert/package.json +10 -0
- package/skills/media/kaizen-video/references/presets.md +76 -0
- package/skills/{story-video → media/kaizen-video}/references/production.md +2 -2
- package/skills/{story-video → media/kaizen-video}/references/revisions.md +9 -9
- package/skills/media/kaizen-video/scripts/__pycache__/intake.cpython-314.pyc +0 -0
- package/skills/media/kaizen-video/scripts/__pycache__/make_film.cpython-314.pyc +0 -0
- package/skills/media/kaizen-video/scripts/__pycache__/make_tiktok.cpython-314.pyc +0 -0
- package/skills/media/kaizen-video/scripts/__pycache__/render_cut.cpython-314.pyc +0 -0
- package/skills/media/kaizen-video/scripts/__pycache__/sfx.cpython-314.pyc +0 -0
- package/skills/media/kaizen-video/scripts/__pycache__/srt.cpython-314.pyc +0 -0
- package/skills/media/kaizen-video/scripts/__pycache__/transcribe.cpython-314.pyc +0 -0
- package/skills/media/kaizen-video/scripts/__pycache__/vo.cpython-314.pyc +0 -0
- package/skills/media/kaizen-video/scripts/check.sh +22 -0
- package/skills/{story-video → media/kaizen-video}/scripts/intake.py +2 -2
- package/skills/media/kaizen-video/scripts/make_film.py +263 -0
- package/skills/media/kaizen-video/scripts/make_tiktok.py +213 -0
- package/skills/{story-video → media/kaizen-video}/scripts/master.sh +12 -11
- package/skills/media/kaizen-video/scripts/render_cut.py +88 -0
- package/skills/media/kaizen-video/scripts/setup.sh +14 -0
- package/skills/{story-video → media/kaizen-video}/scripts/sfx.py +3 -3
- package/skills/media/kaizen-video/scripts/srt.py +9 -0
- package/skills/media/kaizen-video/scripts/still.sh +7 -0
- package/skills/media/kaizen-video/scripts/transcribe.py +45 -0
- package/skills/media/kaizen-video/tools/carve.mjs +560 -0
- package/skills/media/kaizen-video/tools/lib/main-module.mjs +13 -0
- package/web/board/views.ts +1 -1
- package/web/board.html +3 -1
- package/web/board.js +2 -2
- package/web/notebook.js +1 -1
- package/web/notes.html +2 -0
- package/web/term.js +38 -36
- package/web/term.ts +43 -10
- package/web/video.html +298 -0
- package/web/video.js +81 -0
- package/web/video.ts +610 -0
- package/skills/story-video/SKILL.md +0 -108
- package/skills/story-video/engine/package.json +0 -12
- package/skills/story-video/engine/src/Root.tsx +0 -24
- package/skills/story-video/engine/src/engine/Film.tsx +0 -405
- package/skills/story-video/engine/src/engine/presets.json +0 -58
- package/skills/story-video/engine/src/index.ts +0 -3
- package/skills/story-video/engine/tsconfig.json +0 -1
- package/skills/story-video/examples/sample-film/README.md +0 -12
- package/skills/story-video/examples/sample-film/film.json +0 -16
- package/skills/story-video/references/presets.md +0 -21
- package/skills/story-video/scripts/check.sh +0 -19
- package/skills/story-video/scripts/setup.sh +0 -25
- package/skills/story-video/scripts/srt.py +0 -30
- package/skills/story-video/scripts/still.sh +0 -10
- /package/skills/{story-deck → media/kaizen-deck}/examples/build-example.js +0 -0
- /package/skills/{story-deck → media/kaizen-deck}/references/rules.md +0 -0
- /package/skills/{story-deck → media/kaizen-deck}/references/story-arc.md +0 -0
- /package/skills/{story-deck → media/kaizen-deck}/scripts/kit.js +0 -0
- /package/skills/{story-deck → media/kaizen-deck}/scripts/prep_images.py +0 -0
- /package/skills/{story-deck → media/kaizen-deck}/scripts/qa.sh +0 -0
- /package/skills/{story-video → media/kaizen-video}/examples/sample-film/lines.json +0 -0
- /package/skills/{story-video → media/kaizen-video}/references/licensing.md +0 -0
- /package/skills/{story-video → media/kaizen-video}/references/script.md +0 -0
- /package/skills/{story-video → media/kaizen-video}/scripts/music.sh +0 -0
- /package/skills/{story-video → media/kaizen-video}/scripts/vo.py +0 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="id" data-resolution="portrait">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=1080, height=1920" />
|
|
6
|
+
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
|
7
|
+
<script src="captions.js"></script>
|
|
8
|
+
<style>
|
|
9
|
+
@font-face { font-family: "Montserrat"; src: url("assets/fonts/Montserrat.ttf"); font-weight: 100 900; }
|
|
10
|
+
@font-face { font-family: "Special Elite"; src: url("assets/fonts/SpecialElite.ttf"); }
|
|
11
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
12
|
+
html, body { width: 1080px; height: 1920px; overflow: hidden; background: #05080a; }
|
|
13
|
+
body { font-family: "Montserrat", sans-serif; color: #f2efe8; }
|
|
14
|
+
#root { position: relative; width: 100%; height: 100%; overflow: hidden; }
|
|
15
|
+
.fill { position: absolute; inset: 0; }
|
|
16
|
+
|
|
17
|
+
/* Camera: cold, desaturated mystery grade on the talking head. */
|
|
18
|
+
#cam { position: absolute; inset: 0; transform-origin: 50% 38%; filter: blur(0px) brightness(1); }
|
|
19
|
+
#a-roll { width: 100%; height: 100%; object-fit: cover;
|
|
20
|
+
filter: contrast(1.12) saturate(0.72) brightness(0.92); }
|
|
21
|
+
#tint { background: linear-gradient(180deg, rgba(20,70,80,.25), rgba(30,60,70,.08) 45%, rgba(2,6,10,.35)); mix-blend-mode: multiply; }
|
|
22
|
+
#vignette { background: radial-gradient(ellipse 90% 75% at 50% 42%, transparent 62%, rgba(0,0,0,.42) 100%); }
|
|
23
|
+
#grain { inset: -50%; opacity: .13; mix-blend-mode: overlay;
|
|
24
|
+
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>"); }
|
|
25
|
+
#flash { background: #fff; opacity: 0; }
|
|
26
|
+
#dim { background: rgba(0,0,0,.45); opacity: 0; }
|
|
27
|
+
/* Camera-capture transition: viewfinder brackets, REC tag, shutter blades. */
|
|
28
|
+
#vf { opacity: 0; }
|
|
29
|
+
#vf i { position: absolute; width: 120px; height: 120px; border: 0 solid rgba(242,239,232,.9); }
|
|
30
|
+
#vf i:nth-child(1) { left: 70px; top: 200px; border-width: 6px 0 0 6px; }
|
|
31
|
+
#vf i:nth-child(2) { right: 70px; top: 200px; border-width: 6px 6px 0 0; }
|
|
32
|
+
#vf i:nth-child(3) { left: 70px; bottom: 260px; border-width: 0 0 6px 6px; }
|
|
33
|
+
#vf i:nth-child(4) { right: 70px; bottom: 260px; border-width: 0 6px 6px 0; }
|
|
34
|
+
#vf .rec { position: absolute; left: 110px; top: 240px; font-weight: 800; font-size: 40px; letter-spacing: 3px; display: flex; align-items: center; gap: 14px; }
|
|
35
|
+
#vf .rec b { width: 26px; height: 26px; border-radius: 50%; background: #e3262f; }
|
|
36
|
+
#vf .focus { position: absolute; left: 390px; top: 760px; width: 300px; height: 300px; border: 4px solid rgba(242,239,232,.8); }
|
|
37
|
+
.blade { position: absolute; left: 0; right: 0; height: 50%; background: #000; }
|
|
38
|
+
#blade-t { top: 0; transform-origin: 50% 0; }
|
|
39
|
+
#blade-b { bottom: 0; transform-origin: 50% 100%; }
|
|
40
|
+
|
|
41
|
+
/* Captions: small caps chunks, active word in red. Equal side margins: centered, and mostly clear of TikTok's side buttons. */
|
|
42
|
+
#captions { position: absolute; left: 110px; right: 110px; top: 1270px; height: 220px; }
|
|
43
|
+
.chunk { position: absolute; inset: 0; display: flex; flex-wrap: wrap; justify-content: center; align-content: center;
|
|
44
|
+
gap: 6px 28px; opacity: 0; }
|
|
45
|
+
.w { position: relative; display: block; font-weight: 800; font-size: 64px; letter-spacing: 1px; text-transform: uppercase; color: #a7aeb2;
|
|
46
|
+
text-shadow: 0 4px 18px rgba(0,0,0,.9); }
|
|
47
|
+
/* Karaoke fill: a copy of the word revealed left to right while it is spoken. */
|
|
48
|
+
.w .fill { position: absolute; inset: 0; color: #e3262f; text-shadow: none; clip-path: inset(0 100% 0 0); }
|
|
49
|
+
|
|
50
|
+
/* Hook + keyword titles. */
|
|
51
|
+
.title { position: absolute; left: 0; right: 0; top: 640px; display: flex; flex-direction: column; align-items: center; opacity: 0; }
|
|
52
|
+
.title h1 { white-space: nowrap; font-weight: 900; font-size: 150px; line-height: .95; letter-spacing: 2px; text-align: center;
|
|
53
|
+
text-shadow: 0 8px 40px rgba(0,0,0,.9); }
|
|
54
|
+
.title p { margin-top: 26px; background: #f2efe8; color: #0a0d0f; font-weight: 700; font-size: 40px; padding: 6px 18px; }
|
|
55
|
+
#hook { top: 360px; }
|
|
56
|
+
#hook h1 { font-size: 132px; }
|
|
57
|
+
#hook .red { color: #e3262f; }
|
|
58
|
+
.title.end { top: 0; bottom: 0; justify-content: center; }
|
|
59
|
+
.end h1 { color: #e3262f; font-size: 230px; line-height: .9; text-shadow: 0 0 60px rgba(227,38,47,.45), 0 10px 50px #000; }
|
|
60
|
+
|
|
61
|
+
/* Case-file cards. */
|
|
62
|
+
.case { position: absolute; left: 125px; right: 125px; top: 520px; padding: 56px 54px 64px; opacity: 0;
|
|
63
|
+
background: #e9e0cc; color: #1b1712; font-family: "Special Elite", monospace; box-shadow: 0 30px 80px rgba(0,0,0,.8); }
|
|
64
|
+
.case .k { font-size: 34px; letter-spacing: 6px; color: #8a1c1c; border-bottom: 3px solid #1b1712; padding-bottom: 14px; }
|
|
65
|
+
.case h2 { font-size: 104px; margin-top: 30px; line-height: 1; }
|
|
66
|
+
.case .s { font-size: 40px; margin-top: 26px; line-height: 1.3; }
|
|
67
|
+
.case:has(.stamp) { padding-bottom: 170px; }
|
|
68
|
+
.stamp { position: absolute; right: 40px; bottom: 36px; border: 8px solid #c0161f; color: #c0161f; font-family: "Montserrat";
|
|
69
|
+
font-weight: 900; font-size: 76px; padding: 4px 22px; letter-spacing: 4px; opacity: 0; }
|
|
70
|
+
|
|
71
|
+
/* Photo cards (floating, like the reference's B-roll cards). */
|
|
72
|
+
.photo { position: absolute; left: 125px; right: 125px; top: 330px; opacity: 0; display: flex; flex-direction: column; align-items: center; }
|
|
73
|
+
.frame { width: 100%; height: 640px; border-radius: 28px; overflow: hidden; box-shadow: 0 30px 90px rgba(0,0,0,.85); border: 3px solid rgba(242,239,232,.15); }
|
|
74
|
+
.frame img { width: 100%; height: 100%; object-fit: cover; filter: saturate(.75) contrast(1.08) brightness(.9); }
|
|
75
|
+
.photo p { margin-top: 28px; background: #f2efe8; color: #0a0d0f; font-weight: 700; font-size: 38px; padding: 6px 18px; }
|
|
76
|
+
.title .frame { width: 820px; height: 560px; margin-bottom: 40px; }
|
|
77
|
+
.title.has-img { top: 300px; }
|
|
78
|
+
.case .frame { height: 420px; border-radius: 6px; margin-bottom: 26px; box-shadow: none; border: 0; }
|
|
79
|
+
.case .frame img { filter: grayscale(.6) sepia(.35) contrast(1.1); }
|
|
80
|
+
|
|
81
|
+
/* Plot-twist checklist. */
|
|
82
|
+
#list { position: absolute; left: 125px; right: 125px; top: 560px; display: flex; flex-direction: column; gap: 34px; opacity: 0; }
|
|
83
|
+
#list .head { font-family: "Special Elite", monospace; font-size: 58px; letter-spacing: 8px; color: #e3262f; opacity: 0;
|
|
84
|
+
border-bottom: 3px solid rgba(227,38,47,.6); padding-bottom: 12px; text-shadow: 0 0 18px rgba(227,38,47,.5); }
|
|
85
|
+
.item { display: flex; align-items: center; gap: 30px; font-family: "Special Elite", monospace; font-size: 64px; text-transform: uppercase; opacity: 0;
|
|
86
|
+
text-shadow: 0 6px 24px rgba(0,0,0,.9); }
|
|
87
|
+
.item span { clip-path: inset(0 100% 0 0); }
|
|
88
|
+
.item b { flex: none; width: 72px; height: 72px; border: 5px solid #e3262f; display: grid; place-items: center; color: #e3262f;
|
|
89
|
+
font-family: "Montserrat"; font-weight: 900; font-size: 54px; box-shadow: 0 0 26px rgba(227,38,47,.65), inset 0 0 14px rgba(227,38,47,.4); }
|
|
90
|
+
</style>
|
|
91
|
+
</head>
|
|
92
|
+
<body>
|
|
93
|
+
<div id="root" data-composition-id="main" data-start="0" data-duration="{{DURATION}}" data-width="1080" data-height="1920">
|
|
94
|
+
<div id="cam">
|
|
95
|
+
<video id="a-roll" class="clip" src="{{VIDEO}}" playsinline data-start="0" data-duration="{{VIDEO_DURATION}}" data-track-index="0" data-has-audio="true" data-fx-chain="{"version": 1, "nodes": [{"type": "highpass", "id": "n1", "label": "Remove Rumble", "params": {"frequency": 90, "q": 0.707, "poles": "2"}}, {"type": "compressor", "id": "n2", "label": "Even Out Loudness", "params": {"threshold": -26, "ratio": 3, "attack": 10, "release": 200, "knee": 2.83, "makeup": 6, "mix": 1}}, {"type": "gain", "id": "n3", "label": "Level Up", "params": {"gain": 5}}, {"type": "truepeak", "id": "n4", "label": "Peak Ceiling", "params": {"ceiling": -1.5, "lookahead": 3, "release": 80}}]}"></video>
|
|
96
|
+
</div>
|
|
97
|
+
<div id="tint" class="fill"></div>
|
|
98
|
+
<div id="vignette" class="fill"></div>
|
|
99
|
+
<div id="dim" class="fill"></div>
|
|
100
|
+
|
|
101
|
+
<div id="hook" class="title"><h1>{{HOOK_1}}</h1><h1 class="red">{{HOOK_2}}</h1><p>{{HOOK_SUB}}</p></div>
|
|
102
|
+
<div id="beats"></div>
|
|
103
|
+
<div id="list"></div>
|
|
104
|
+
<div id="captions"></div>
|
|
105
|
+
|
|
106
|
+
<div id="vf" class="fill"><i></i><i></i><i></i><i></i><div class="rec"><b></b>REC</div><div class="focus"></div></div>
|
|
107
|
+
<div id="blade-t" class="blade"></div>
|
|
108
|
+
<div id="blade-b" class="blade"></div>
|
|
109
|
+
<div id="grain" class="fill"></div>
|
|
110
|
+
<div id="flash" class="fill"></div>
|
|
111
|
+
|
|
112
|
+
<audio id="music" src="assets/music.wav" data-start="0" data-duration="{{DURATION}}" data-track-index="2" data-volume="{{MUSIC_VOLUME}}"></audio>
|
|
113
|
+
<!-- SFX:BEGIN --><!-- SFX:END -->
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<script>
|
|
117
|
+
const END = {{DURATION}}, HOOK = 3.0, RED = "#e3262f";
|
|
118
|
+
const tl = gsap.timeline({ paused: true });
|
|
119
|
+
const el = (tag, cls, html) => { const e = document.createElement(tag); if (cls) e.className = cls; if (html) e.innerHTML = html; return e; };
|
|
120
|
+
|
|
121
|
+
// Punch-in: alternate framing on every jump cut, slow push inside each segment.
|
|
122
|
+
SEG_STARTS.forEach((s, i) => {
|
|
123
|
+
const e = SEG_STARTS[i + 1] ?? END, base = i % 2 ? 1.14 : 1.0;
|
|
124
|
+
tl.fromTo("#cam", { scale: base }, { scale: base + 0.04, duration: e - s, ease: "none", immediateRender: i === 0 }, s);
|
|
125
|
+
});
|
|
126
|
+
// Grain jitter + a faint light flicker (fixed pattern, no Math.random).
|
|
127
|
+
const steps = Math.floor(END * 12);
|
|
128
|
+
for (let i = 0; i < steps; i++) tl.set("#grain", { x: ((i * 37) % 120) - 60, y: ((i * 53) % 120) - 60 }, i / 12);
|
|
129
|
+
const flicker = { ease: "rough({strength:2,points:8,randomize:false})", duration: 0.25 };
|
|
130
|
+
tl.fromTo("#a-roll", { opacity: 0.55 }, { opacity: 1, ...flicker }, 7.9);
|
|
131
|
+
[22.4, 41.1, 58.6, 70.2, 95.4, 110.7].forEach(t =>
|
|
132
|
+
tl.fromTo("#a-roll", { opacity: 0.55 }, { opacity: 1, immediateRender: false, ...flicker }, t));
|
|
133
|
+
|
|
134
|
+
// Flash + dim helper for every hard beat.
|
|
135
|
+
const hit = (t, dur, focus = true) => {
|
|
136
|
+
// Viewfinder frames the shot, focus box pulls in, shutter blades snap at t.
|
|
137
|
+
const v = Math.max(0, t - 0.3);
|
|
138
|
+
tl.set("#vf", { opacity: 1 }, v);
|
|
139
|
+
tl.fromTo("#vf .focus", { scale: 1.6, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.25, ease: "power2.out", immediateRender: false }, v);
|
|
140
|
+
tl.set("#vf", { opacity: 0 }, t + 0.12);
|
|
141
|
+
tl.fromTo(".blade", { scaleY: 0 }, { scaleY: 1, duration: 0.06, ease: "power2.in", immediateRender: false }, t);
|
|
142
|
+
tl.to(".blade", { scaleY: 0, duration: 0.12, ease: "power2.out" }, t + 0.07);
|
|
143
|
+
tl.set("#flash", { opacity: 0.25 }, t + 0.07);
|
|
144
|
+
tl.to("#flash", { opacity: 0, duration: 0.25, ease: "power2.out" }, t + 0.07);
|
|
145
|
+
if (!focus) return; // hook: keep the speaker clear
|
|
146
|
+
tl.to("#dim", { opacity: 1, duration: 0.25 }, t);
|
|
147
|
+
tl.to("#cam", { filter: "blur(12px) brightness(1)", duration: 0.25 }, t);
|
|
148
|
+
if (t + dur < END) {
|
|
149
|
+
tl.to("#dim", { opacity: 0, duration: 0.3 }, t + dur - 0.3);
|
|
150
|
+
tl.to("#cam", { filter: "blur(0px) brightness(1)", duration: 0.3 }, t + dur - 0.3);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
gsap.set(".blade", { scaleY: 0 });
|
|
155
|
+
|
|
156
|
+
// Shrink any title wider than the safe area (long words like PENGECUALIAN).
|
|
157
|
+
const fit = n => n.querySelectorAll("h1").forEach(h => { const w = h.scrollWidth; if (w > 840) h.style.fontSize = Math.floor(parseFloat(getComputedStyle(h).fontSize) * 840 / w) + "px"; });
|
|
158
|
+
|
|
159
|
+
// Hook.
|
|
160
|
+
fit(document.getElementById("hook"));
|
|
161
|
+
hit(0.3, HOOK - 0.3, false);
|
|
162
|
+
tl.fromTo("#hook", { opacity: 0, scale: 1.25 }, { opacity: 1, scale: 1, duration: 0.45, ease: "power3.out" }, 0.05);
|
|
163
|
+
tl.to("#hook", { opacity: 0, duration: 0.3 }, HOOK - 0.3);
|
|
164
|
+
|
|
165
|
+
// Beats.
|
|
166
|
+
const beats = document.getElementById("beats"), list = document.getElementById("list");
|
|
167
|
+
let listStart = null;
|
|
168
|
+
const pic = b => b.img ? `<div class="frame"><img src="assets/img/${b.img}" alt="" data-layout-allow-overflow></div>` : "";
|
|
169
|
+
const kbAt = (n, t) => { const im = n.querySelector(".frame img"); if (im) tl.fromTo(im, { scale: 1.14 }, { scale: 1, duration: 3.4, ease: "none" }, t); };
|
|
170
|
+
BEATS.forEach((b, i) => {
|
|
171
|
+
if (b.kind === "end") {
|
|
172
|
+
// Ending: one huge word per line, slammed in on the gong hit, camera shake, near-black.
|
|
173
|
+
const n = el("div", "title end", b.title.split(" ").map(w => `<h1>${w}</h1>`).join(""));
|
|
174
|
+
beats.append(n); fit(n);
|
|
175
|
+
tl.to("#dim", { opacity: 1, duration: 0.2 }, b.t);
|
|
176
|
+
tl.to("#cam", { filter: "blur(14px) brightness(0.5)", duration: 0.2 }, b.t);
|
|
177
|
+
tl.set(n, { opacity: 1 }, b.t);
|
|
178
|
+
n.querySelectorAll("h1").forEach((h, j) =>
|
|
179
|
+
tl.fromTo(h, { opacity: 0, scale: 2.6 }, { opacity: 1, scale: 1, duration: 0.22, ease: "power4.in" }, b.t + j * 0.16));
|
|
180
|
+
const land = b.t + (n.children.length - 1) * 0.16 + 0.22;
|
|
181
|
+
tl.set("#flash", { opacity: 0.35 }, land);
|
|
182
|
+
tl.to("#flash", { opacity: 0, duration: 0.4 }, land);
|
|
183
|
+
[14, -12, 9, -6, 3, 0].forEach((x, j) => tl.to(n, { x, duration: 0.05 }, land + j * 0.05));
|
|
184
|
+
tl.to(n, { scale: 1.06, duration: END - land, ease: "none" }, land);
|
|
185
|
+
} else if (b.kind === "title") {
|
|
186
|
+
const d = 1.8;
|
|
187
|
+
const n = el("div", "title" + (b.img ? " has-img" : ""), pic(b) + `<h1>${b.title}</h1>` + (b.sub ? `<p>${b.sub}</p>` : ""));
|
|
188
|
+
beats.append(n); fit(n); hit(b.t, d); kbAt(n, b.t);
|
|
189
|
+
tl.fromTo(n, { opacity: 0, scale: 1.3 }, { opacity: 1, scale: 1, duration: 0.4, ease: "power3.out" }, b.t + 0.08);
|
|
190
|
+
tl.to(n, { opacity: 0, duration: 0.25 }, b.t + d - 0.25);
|
|
191
|
+
} else if (b.kind === "case") {
|
|
192
|
+
const d = 3.2;
|
|
193
|
+
const n = el("div", "case", pic(b) + `<div class="k">${b.kicker}</div><h2>${b.title}</h2>` +
|
|
194
|
+
(b.sub ? `<div class="s">${b.sub}</div>` : "") + (b.stamp ? `<div class="stamp" data-layout-allow-overlap>${b.stamp}</div>` : ""));
|
|
195
|
+
beats.append(n); hit(b.t, d); kbAt(n, b.t);
|
|
196
|
+
tl.fromTo(n, { opacity: 0, y: 120, rotation: -4 }, { opacity: 1, y: 0, rotation: -1.5, duration: 0.5, ease: "back.out(1.4)" }, b.t + 0.08);
|
|
197
|
+
const st = n.querySelector(".stamp");
|
|
198
|
+
if (st) tl.fromTo(st, { opacity: 0, scale: 2.4, rotation: -18 }, { opacity: 1, scale: 1, rotation: -12, duration: 0.25, ease: "power4.in" }, b.t + 0.9);
|
|
199
|
+
tl.to(n, { opacity: 0, y: -60, duration: 0.3 }, b.t + d - 0.3);
|
|
200
|
+
} else if (b.kind === "photo") {
|
|
201
|
+
const d = 2.8;
|
|
202
|
+
const n = el("div", "photo", pic(b) + `<p>${b.label}</p>`);
|
|
203
|
+
beats.append(n); hit(b.t, d); kbAt(n, b.t);
|
|
204
|
+
tl.fromTo(n, { opacity: 0, y: 80, rotation: 3 }, { opacity: 1, y: 0, rotation: 1, duration: 0.45, ease: "power3.out" }, b.t + 0.08);
|
|
205
|
+
tl.to(n, { opacity: 0, scale: 0.92, duration: 0.3 }, b.t + d - 0.3);
|
|
206
|
+
} else {
|
|
207
|
+
if (listStart === null) listStart = b.t;
|
|
208
|
+
// Mystery reveal: the "?" box flickers on like a failing bulb, a glitch jolt, then the text types itself out.
|
|
209
|
+
const n = el("div", "item", `<b>?</b><span>${b.text}</span>`);
|
|
210
|
+
list.append(n);
|
|
211
|
+
const box = n.querySelector("b"), txt = n.querySelector("span");
|
|
212
|
+
tl.set(n, { opacity: 1 }, b.t);
|
|
213
|
+
[0, 1, 0.15, 1, 0.3, 0.85, 1].forEach((o, k) => tl.set(box, { opacity: o }, b.t + k * 0.045));
|
|
214
|
+
[[10, "4px 0 #e3262f, -4px 0 #31d0d6"], [-6, "-3px 0 #e3262f, 3px 0 #31d0d6"], [0, "0 6px 24px rgba(0,0,0,.9)"]]
|
|
215
|
+
.forEach(([x, sh], k) => tl.set(n, { x, textShadow: sh }, b.t + 0.32 + k * 0.05));
|
|
216
|
+
tl.fromTo(txt, { clipPath: "inset(0 100% 0 0)" }, { clipPath: "inset(0 0% 0 0)", duration: 0.85, ease: `steps(${b.text.length})` }, b.t + 0.05);
|
|
217
|
+
tl.fromTo(box, { scale: 1 }, { scale: 1.12, duration: 0.6, ease: "sine.inOut", yoyo: true, repeat: 5 }, b.t + 0.5);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
// The checklist leaves when the speaker moves on: the next jump cut after the last item (max 6 s).
|
|
221
|
+
const items = BEATS.filter(b => b.kind === "item"), lastItem = items.length ? items[items.length - 1].t : 0;
|
|
222
|
+
const listEnd = Math.min(SEG_STARTS.find(s => s > lastItem + 1) ?? END, lastItem + 6);
|
|
223
|
+
if (items.length) {
|
|
224
|
+
const head = el("div", "head"); head.textContent = "KEMUNGKINAN:"; list.prepend(head);
|
|
225
|
+
tl.fromTo(head, { clipPath: "inset(0 100% 0 0)", opacity: 1 }, { clipPath: "inset(0 0% 0 0)", duration: 0.5, ease: "steps(12)" }, listStart - 0.6);
|
|
226
|
+
tl.fromTo("#list", { opacity: 0 }, { opacity: 1, duration: 0.3 }, listStart - 0.6);
|
|
227
|
+
tl.to("#dim", { opacity: 1, duration: 0.3 }, listStart - 0.6);
|
|
228
|
+
tl.to("#cam", { filter: "blur(10px) brightness(0.7)", duration: 0.4 }, listStart - 0.6);
|
|
229
|
+
tl.to(["#list", "#dim"], { opacity: 0, duration: 0.4 }, listEnd - 0.4);
|
|
230
|
+
tl.to("#cam", { filter: "blur(0px) brightness(1)", duration: 0.4 }, listEnd - 0.4);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Captions (hidden under the hook and while a card or the list is on screen).
|
|
234
|
+
const busy = BEATS.filter(b => b.kind !== "item" && b.kind !== "photo").map(b => [b.t, b.t + (b.kind === "case" ? 3.2 : b.kind === "end" ? END : 1.8)])
|
|
235
|
+
.concat(items.length ? [[listStart - 0.6, listEnd]] : []); // captions stay on under the hook
|
|
236
|
+
const cap = document.getElementById("captions");
|
|
237
|
+
CAPTIONS.forEach(chunk => {
|
|
238
|
+
const n = el("div", "chunk"); cap.append(n);
|
|
239
|
+
const start = chunk[0].s, end = chunk[chunk.length - 1].hold;
|
|
240
|
+
if (busy.some(([a, b]) => start < b && end > a)) return;
|
|
241
|
+
tl.fromTo(n, { opacity: 0, y: 24 }, { opacity: 1, y: 0, duration: 0.12, ease: "power2.out" }, start);
|
|
242
|
+
tl.set(n, { opacity: 0 }, end);
|
|
243
|
+
chunk.forEach((w, j) => {
|
|
244
|
+
const txt = w.w.replace(/[,.?!]+$/, "");
|
|
245
|
+
const s = el("span", "w"); s.textContent = txt; n.append(s);
|
|
246
|
+
const f = el("span", "fill"); f.textContent = txt; s.append(f);
|
|
247
|
+
const until = chunk[j + 1] ? chunk[j + 1].s : end;
|
|
248
|
+
// Fill sweeps across the word over exactly its spoken duration, one frame early (reads as in sync).
|
|
249
|
+
const t0 = Math.max(start, w.s - 1 / 30), dur = Math.max(w.e - w.s, 0.1);
|
|
250
|
+
tl.fromTo(f, { clipPath: "inset(0 100% 0 0)" }, { clipPath: "inset(0 0% 0 0)", duration: dur, ease: "power1.out" }, t0);
|
|
251
|
+
tl.fromTo(s, { scale: 1.06 }, { scale: 1, duration: 0.18, ease: "power2.out" }, t0);
|
|
252
|
+
// Same rule for every word: grey upcoming, red while spoken, white once the next word starts.
|
|
253
|
+
tl.set(f, { color: "#f2efe8" }, until);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
window.__timelines["main"] = tl;
|
|
258
|
+
</script>
|
|
259
|
+
</body>
|
|
260
|
+
</html>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Sound effects:
|
|
2
|
+
- shutter.wav, stamp.wav, reveal.wav, ending.wav: synthesized with ffmpeg tone generators, CC0.
|
|
3
|
+
- ding.wav: "406243 stubb typewriter-ding-near-mono.wav" by stubb (Wikimedia Commons), CC0.
|
|
4
|
+
- heart.wav: "Heartbeat speedsup.wav" by Bob Smaude (Wikimedia Commons), CC BY 4.0.
|
|
5
|
+
- clock.wav: "Clock ticking.ogg" by natalie (Wikimedia Commons), public domain.
|
|
6
|
+
- whoosh.wav: synthesized with ffmpeg, CC0.
|
|
7
|
+
|
|
8
|
+
Fonts: "Montserrat" by Julieta Ulanovsky et al. (Google Fonts, SIL Open Font License 1.1).
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# TikTok scam-alert preset
|
|
2
|
+
|
|
3
|
+
A 1080×1920 HyperFrames style for public-warning explainers about fraud. It was copied from `tiktok-mystery` and keeps that preset's config keys and card types.
|
|
4
|
+
|
|
5
|
+
## Look and motion
|
|
6
|
+
|
|
7
|
+
- **Grade:** high contrast and desaturated, with a heavy vignette and light grain. There is no flicker.
|
|
8
|
+
- **Hook:** "line1" in big yellow, "line2" in white, and a red subtitle box, all between two strips of hazard tape.
|
|
9
|
+
- **Card entrance:** red brackets, an "AWAS" tag, shutter blades, and a red alarm border that pulses with the swoosh (`shutter.wav`).
|
|
10
|
+
- **Captions:** karaoke fill in yellow.
|
|
11
|
+
- **Card types:**
|
|
12
|
+
- `title`: hazard tape, a white headline, and a red subtitle box.
|
|
13
|
+
- `case`: a white report card with a red edge and an optional red stamp.
|
|
14
|
+
- `photo`: as in mystery, plus `"tall": true` for full-height screenshots (red frame, top-anchored) and `"dur"` in seconds, and `"pos"` (CSS object-position, e.g. `"50% 100%"` to show the bottom of a chat). Captions hide while a tall photo is up.
|
|
15
|
+
- `item`: the "TIPS AMAN:" checklist with green check marks that pop in.
|
|
16
|
+
- `end`: yellow words between hazard tape.
|
|
17
|
+
|
|
18
|
+
## Audio
|
|
19
|
+
|
|
20
|
+
- **Music:** `investigations.mp3` (Kevin MacLeod, CC BY 4.0).
|
|
21
|
+
- **Synthesized sound effects (ffmpeg, CC0):** `shutter` (fast swoosh into a low knock), `stamp` (heavy rubber-stamp thud), `reveal` (tip chime), `ending` (noise riser into a low hit at 1.2 s). Peaks are −7 dBFS, and the ending is −4 dBFS.
|
|
22
|
+
- **Kept from mystery:** `ding`, `whoosh`, `heart` and `clock` (usable as reactions for tension).
|
|
23
|
+
|
|
24
|
+
Usage is the same as `presets/tiktok-mystery/README.md`.
|
|
Binary file
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="id" data-resolution="portrait">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=1080, height=1920" />
|
|
6
|
+
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
|
7
|
+
<script src="captions.js"></script>
|
|
8
|
+
<style>
|
|
9
|
+
@font-face { font-family: "Montserrat"; src: url("assets/fonts/Montserrat.ttf"); font-weight: 100 900; }
|
|
10
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
11
|
+
html, body { width: 1080px; height: 1920px; overflow: hidden; background: #05080a; }
|
|
12
|
+
body { font-family: "Montserrat", sans-serif; color: #f2efe8; }
|
|
13
|
+
#root { position: relative; width: 100%; height: 100%; overflow: hidden; }
|
|
14
|
+
.fill { position: absolute; inset: 0; }
|
|
15
|
+
|
|
16
|
+
/* Camera: high-contrast, desaturated grade with a heavy vignette for a tense, dramatic feel. */
|
|
17
|
+
#cam { position: absolute; inset: 0; transform-origin: 50% 38%; filter: blur(0px) brightness(1); }
|
|
18
|
+
#a-roll { width: 100%; height: 100%; object-fit: cover;
|
|
19
|
+
filter: contrast(1.14) saturate(0.78) brightness(0.94); }
|
|
20
|
+
#tint { background: linear-gradient(180deg, rgba(0,0,0,.18), transparent 30%, transparent 55%, rgba(0,0,0,.45)); }
|
|
21
|
+
/* Hazard tape, used by the hook, titles and the ending. */
|
|
22
|
+
.tape { width: 840px; height: 30px; background: repeating-linear-gradient(-45deg, #ffd400 0 34px, #111 34px 68px); box-shadow: 0 6px 24px rgba(0,0,0,.6); }
|
|
23
|
+
#alarm { box-shadow: inset 0 0 0 14px #e3262f, inset 0 0 140px rgba(227,38,47,.55); opacity: 0; }
|
|
24
|
+
#vignette { background: radial-gradient(ellipse 90% 75% at 50% 42%, transparent 55%, rgba(0,0,0,.62) 100%); }
|
|
25
|
+
#grain { inset: -50%; opacity: .06; mix-blend-mode: overlay;
|
|
26
|
+
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>"); }
|
|
27
|
+
#flash { background: #fff; opacity: 0; }
|
|
28
|
+
#dim { background: rgba(0,0,0,.45); opacity: 0; }
|
|
29
|
+
/* Alert transition: red brackets, warning tag, blades snap. */
|
|
30
|
+
#vf { opacity: 0; }
|
|
31
|
+
#vf i { position: absolute; width: 120px; height: 120px; border: 0 solid #e3262f; }
|
|
32
|
+
#vf i:nth-child(1) { left: 70px; top: 200px; border-width: 6px 0 0 6px; }
|
|
33
|
+
#vf i:nth-child(2) { right: 70px; top: 200px; border-width: 6px 6px 0 0; }
|
|
34
|
+
#vf i:nth-child(3) { left: 70px; bottom: 260px; border-width: 0 0 6px 6px; }
|
|
35
|
+
#vf i:nth-child(4) { right: 70px; bottom: 260px; border-width: 0 6px 6px 0; }
|
|
36
|
+
#vf .rec { position: absolute; left: 110px; top: 240px; font-weight: 800; font-size: 40px; letter-spacing: 3px; display: flex; align-items: center; gap: 14px; }
|
|
37
|
+
#vf .rec { color: #ffd400; }
|
|
38
|
+
#vf .rec b { width: 26px; height: 26px; border-radius: 50%; background: #e3262f; }
|
|
39
|
+
#vf .focus { position: absolute; left: 390px; top: 760px; width: 300px; height: 300px; border: 4px solid #ffd400; }
|
|
40
|
+
.blade { position: absolute; left: 0; right: 0; height: 50%; background: #000; }
|
|
41
|
+
#blade-t { top: 0; transform-origin: 50% 0; }
|
|
42
|
+
#blade-b { bottom: 0; transform-origin: 50% 100%; }
|
|
43
|
+
|
|
44
|
+
/* Captions: small caps chunks, active word in red. Equal side margins: centered, and mostly clear of TikTok's side buttons. */
|
|
45
|
+
#captions { position: absolute; left: 110px; right: 110px; top: 1270px; height: 220px; }
|
|
46
|
+
.chunk { position: absolute; inset: 0; display: flex; flex-wrap: wrap; justify-content: center; align-content: center;
|
|
47
|
+
gap: 6px 28px; opacity: 0; }
|
|
48
|
+
.w { position: relative; display: block; font-weight: 800; font-size: 64px; letter-spacing: 1px; text-transform: uppercase; color: #a7aeb2;
|
|
49
|
+
text-shadow: 0 4px 18px rgba(0,0,0,.9); }
|
|
50
|
+
/* Karaoke fill: a copy of the word revealed left to right while it is spoken. */
|
|
51
|
+
.w .fill { position: absolute; inset: 0; color: #ffd400; text-shadow: none; clip-path: inset(0 100% 0 0); }
|
|
52
|
+
|
|
53
|
+
/* Hook + keyword titles. */
|
|
54
|
+
.title { position: absolute; left: 0; right: 0; top: 640px; display: flex; flex-direction: column; align-items: center; opacity: 0; }
|
|
55
|
+
.title h1 { white-space: nowrap; font-weight: 900; font-size: 150px; line-height: .95; letter-spacing: 2px; text-align: center;
|
|
56
|
+
text-shadow: 0 8px 40px rgba(0,0,0,.9); }
|
|
57
|
+
.title .tape { margin-bottom: 34px; }
|
|
58
|
+
.title p { margin-top: 26px; background: #e3262f; color: #fff; font-weight: 700; font-size: 40px; padding: 6px 18px; }
|
|
59
|
+
#hook { top: 360px; }
|
|
60
|
+
#hook h1 { font-size: 132px; }
|
|
61
|
+
#hook h1:first-of-type { color: #ffd400; font-size: 190px; }
|
|
62
|
+
#hook .tape { margin: 0 0 30px; }
|
|
63
|
+
#hook .tape:last-child { margin: 34px 0 0; }
|
|
64
|
+
.title.end { top: 0; bottom: 0; justify-content: center; }
|
|
65
|
+
.end .tape { margin: 30px 0; }
|
|
66
|
+
.end h1 { color: #ffd400; font-size: 230px; line-height: .9; text-shadow: 0 0 60px rgba(255,212,0,.35), 0 10px 50px #000; }
|
|
67
|
+
|
|
68
|
+
/* Evidence / report cards. */
|
|
69
|
+
.case { position: absolute; left: 125px; right: 125px; top: 520px; padding: 56px 54px 64px; opacity: 0;
|
|
70
|
+
background: #f6f6f4; color: #121212; border-left: 22px solid #e3262f; border-radius: 10px; box-shadow: 0 30px 80px rgba(0,0,0,.8); }
|
|
71
|
+
.case .k { font-weight: 800; font-size: 34px; letter-spacing: 6px; color: #e3262f; border-bottom: 3px solid #121212; padding-bottom: 14px; }
|
|
72
|
+
.case h2 { font-weight: 900; font-size: 92px; margin-top: 30px; line-height: 1; }
|
|
73
|
+
.case .s { font-weight: 600; font-size: 40px; margin-top: 26px; line-height: 1.3; }
|
|
74
|
+
.case:has(.stamp) { padding-bottom: 170px; }
|
|
75
|
+
.stamp { position: absolute; right: 40px; bottom: 36px; border: 8px solid #c0161f; color: #c0161f; font-family: "Montserrat";
|
|
76
|
+
font-weight: 900; font-size: 76px; padding: 4px 22px; letter-spacing: 4px; opacity: 0; }
|
|
77
|
+
|
|
78
|
+
/* Photo cards (floating, like the reference's B-roll cards). */
|
|
79
|
+
.photo { position: absolute; left: 125px; right: 125px; top: 330px; opacity: 0; display: flex; flex-direction: column; align-items: center; }
|
|
80
|
+
.frame { width: 100%; height: 640px; border-radius: 28px; overflow: hidden; box-shadow: 0 30px 90px rgba(0,0,0,.85); border: 3px solid rgba(242,239,232,.15); }
|
|
81
|
+
.frame img { width: 100%; height: 100%; object-fit: cover; }
|
|
82
|
+
.frame.tall { height: 1180px; border: 6px solid #e3262f; }
|
|
83
|
+
.frame.tall img { object-position: 50% 3%; }
|
|
84
|
+
.photo p { margin-top: 28px; background: #e3262f; color: #fff; font-weight: 700; font-size: 38px; padding: 6px 18px; }
|
|
85
|
+
.title .frame { width: 820px; height: 560px; margin-bottom: 40px; }
|
|
86
|
+
.title.has-img { top: 300px; }
|
|
87
|
+
.case .frame { height: 420px; border-radius: 6px; margin-bottom: 26px; box-shadow: none; border: 0; }
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
/* Safety-tips checklist. */
|
|
91
|
+
#list { position: absolute; left: 125px; right: 125px; top: 560px; display: flex; flex-direction: column; gap: 34px; opacity: 0; }
|
|
92
|
+
#list .head { font-weight: 900; font-size: 58px; letter-spacing: 8px; color: #2ee06f; opacity: 0;
|
|
93
|
+
border-bottom: 3px solid rgba(46,224,111,.6); padding-bottom: 12px; text-shadow: 0 4px 18px rgba(0,0,0,.8); }
|
|
94
|
+
.item { display: flex; align-items: center; gap: 30px; font-weight: 800; font-size: 52px; line-height: 1.15; text-transform: uppercase; opacity: 0;
|
|
95
|
+
text-shadow: 0 6px 24px rgba(0,0,0,.9); }
|
|
96
|
+
.item span { clip-path: inset(0 100% 0 0); }
|
|
97
|
+
.item b { flex: none; width: 72px; height: 72px; border-radius: 50%; background: #2ee06f; display: grid; place-items: center; color: #0b2a16;
|
|
98
|
+
font-family: "Montserrat"; font-weight: 900; font-size: 54px; box-shadow: 0 0 26px rgba(46,224,111,.6); }
|
|
99
|
+
</style>
|
|
100
|
+
</head>
|
|
101
|
+
<body>
|
|
102
|
+
<div id="root" data-composition-id="main" data-start="0" data-duration="{{DURATION}}" data-width="1080" data-height="1920">
|
|
103
|
+
<div id="cam">
|
|
104
|
+
<video id="a-roll" class="clip" src="{{VIDEO}}" playsinline data-start="0" data-duration="{{VIDEO_DURATION}}" data-track-index="0" data-has-audio="true" data-fx-chain="{"version": 1, "nodes": [{"type": "highpass", "id": "n1", "label": "Remove Rumble", "params": {"frequency": 90, "q": 0.707, "poles": "2"}}, {"type": "compressor", "id": "n2", "label": "Even Out Loudness", "params": {"threshold": -26, "ratio": 3, "attack": 10, "release": 200, "knee": 2.83, "makeup": 6, "mix": 1}}, {"type": "gain", "id": "n3", "label": "Level Up", "params": {"gain": 5}}, {"type": "truepeak", "id": "n4", "label": "Peak Ceiling", "params": {"ceiling": -1.5, "lookahead": 3, "release": 80}}]}"></video>
|
|
105
|
+
</div>
|
|
106
|
+
<div id="tint" class="fill"></div>
|
|
107
|
+
<div id="vignette" class="fill"></div>
|
|
108
|
+
<div id="dim" class="fill"></div>
|
|
109
|
+
|
|
110
|
+
<div id="hook" class="title"><div class="tape"></div><h1>{{HOOK_1}}</h1><h1>{{HOOK_2}}</h1><p>{{HOOK_SUB}}</p><div class="tape"></div></div>
|
|
111
|
+
<div id="beats"></div>
|
|
112
|
+
<div id="list"></div>
|
|
113
|
+
<div id="captions"></div>
|
|
114
|
+
|
|
115
|
+
<div id="vf" class="fill"><i></i><i></i><i></i><i></i><div class="rec"><b></b>AWAS</div><div class="focus"></div></div>
|
|
116
|
+
<div id="blade-t" class="blade"></div>
|
|
117
|
+
<div id="blade-b" class="blade"></div>
|
|
118
|
+
<div id="alarm" class="fill"></div>
|
|
119
|
+
<div id="grain" class="fill"></div>
|
|
120
|
+
<div id="flash" class="fill"></div>
|
|
121
|
+
|
|
122
|
+
<audio id="music" src="assets/music.wav" data-start="0" data-duration="{{DURATION}}" data-track-index="2" data-volume="{{MUSIC_VOLUME}}"></audio>
|
|
123
|
+
<!-- SFX:BEGIN --><!-- SFX:END -->
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<script>
|
|
127
|
+
const END = {{DURATION}}, HOOK = 3.0;
|
|
128
|
+
const tl = gsap.timeline({ paused: true });
|
|
129
|
+
const el = (tag, cls, html) => { const e = document.createElement(tag); if (cls) e.className = cls; if (html) e.innerHTML = html; return e; };
|
|
130
|
+
|
|
131
|
+
// Punch-in: alternate framing on every jump cut, slow push inside each segment.
|
|
132
|
+
SEG_STARTS.forEach((s, i) => {
|
|
133
|
+
const e = SEG_STARTS[i + 1] ?? END, base = i % 2 ? 1.14 : 1.0;
|
|
134
|
+
tl.fromTo("#cam", { scale: base }, { scale: base + 0.04, duration: e - s, ease: "none", immediateRender: i === 0 }, s);
|
|
135
|
+
});
|
|
136
|
+
// Grain jitter (fixed pattern, no Math.random).
|
|
137
|
+
const steps = Math.floor(END * 12);
|
|
138
|
+
for (let i = 0; i < steps; i++) tl.set("#grain", { x: ((i * 37) % 120) - 60, y: ((i * 53) % 120) - 60 }, i / 12);
|
|
139
|
+
|
|
140
|
+
// Flash + dim helper for every hard beat.
|
|
141
|
+
const hit = (t, dur, focus = true) => {
|
|
142
|
+
// Viewfinder frames the shot, focus box pulls in, shutter blades snap at t.
|
|
143
|
+
const v = Math.max(0, t - 0.3);
|
|
144
|
+
tl.set("#vf", { opacity: 1 }, v);
|
|
145
|
+
tl.fromTo("#vf .focus", { scale: 1.6, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.25, ease: "power2.out", immediateRender: false }, v);
|
|
146
|
+
tl.set("#vf", { opacity: 0 }, t + 0.12);
|
|
147
|
+
tl.fromTo(".blade", { scaleY: 0 }, { scaleY: 1, duration: 0.06, ease: "power2.in", immediateRender: false }, t);
|
|
148
|
+
tl.to(".blade", { scaleY: 0, duration: 0.12, ease: "power2.out" }, t + 0.07);
|
|
149
|
+
// Red alarm border pulses twice with the alert beep.
|
|
150
|
+
tl.fromTo("#alarm", { opacity: 0 }, { opacity: 1, duration: 0.12, yoyo: true, repeat: 3, ease: "power1.inOut", immediateRender: false }, t);
|
|
151
|
+
tl.set("#flash", { opacity: 0.25 }, t + 0.07);
|
|
152
|
+
tl.to("#flash", { opacity: 0, duration: 0.25, ease: "power2.out" }, t + 0.07);
|
|
153
|
+
if (!focus) return; // hook: keep the speaker clear
|
|
154
|
+
tl.to("#dim", { opacity: 1, duration: 0.25 }, t);
|
|
155
|
+
tl.to("#cam", { filter: "blur(12px) brightness(1)", duration: 0.25 }, t);
|
|
156
|
+
if (t + dur < END) {
|
|
157
|
+
tl.to("#dim", { opacity: 0, duration: 0.3 }, t + dur - 0.3);
|
|
158
|
+
tl.to("#cam", { filter: "blur(0px) brightness(1)", duration: 0.3 }, t + dur - 0.3);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
gsap.set(".blade", { scaleY: 0 });
|
|
163
|
+
|
|
164
|
+
// Shrink any title wider than the safe area (long words like PENGECUALIAN).
|
|
165
|
+
const fit = n => n.querySelectorAll("h1").forEach(h => { const w = h.scrollWidth; if (w > 840) h.style.fontSize = Math.floor(parseFloat(getComputedStyle(h).fontSize) * 840 / w) + "px"; });
|
|
166
|
+
|
|
167
|
+
// Hook.
|
|
168
|
+
fit(document.getElementById("hook"));
|
|
169
|
+
hit(0.3, HOOK - 0.3, false);
|
|
170
|
+
tl.fromTo("#hook", { opacity: 0, scale: 1.25 }, { opacity: 1, scale: 1, duration: 0.45, ease: "power3.out" }, 0.05);
|
|
171
|
+
tl.to("#hook", { opacity: 0, duration: 0.3 }, HOOK - 0.3);
|
|
172
|
+
|
|
173
|
+
// Beats.
|
|
174
|
+
const beats = document.getElementById("beats"), list = document.getElementById("list");
|
|
175
|
+
let listStart = null;
|
|
176
|
+
const pic = b => b.img ? `<div class="frame"><img src="assets/img/${b.img}" alt="" data-layout-allow-overflow></div>` : "";
|
|
177
|
+
const kbAt = (n, t) => { const im = n.querySelector(".frame img"); if (im) tl.fromTo(im, { scale: 1.14 }, { scale: 1, duration: 3.4, ease: "none" }, t); };
|
|
178
|
+
BEATS.forEach((b, i) => {
|
|
179
|
+
if (b.kind === "end") {
|
|
180
|
+
// Ending: one huge word per line, slammed in on the gong hit, camera shake, near-black.
|
|
181
|
+
const n = el("div", "title end", `<div class="tape"></div>` + b.title.split(" ").map(w => `<h1>${w}</h1>`).join("") + `<div class="tape"></div>`);
|
|
182
|
+
beats.append(n); fit(n);
|
|
183
|
+
tl.to("#dim", { opacity: 1, duration: 0.2 }, b.t);
|
|
184
|
+
tl.to("#cam", { filter: "blur(14px) brightness(0.5)", duration: 0.2 }, b.t);
|
|
185
|
+
tl.set(n, { opacity: 1 }, b.t);
|
|
186
|
+
n.querySelectorAll("h1").forEach((h, j) =>
|
|
187
|
+
tl.fromTo(h, { opacity: 0, scale: 2.6 }, { opacity: 1, scale: 1, duration: 0.22, ease: "power4.in" }, b.t + j * 0.16));
|
|
188
|
+
const land = b.t + (n.children.length - 1) * 0.16 + 0.22;
|
|
189
|
+
tl.set("#flash", { opacity: 0.35 }, land);
|
|
190
|
+
tl.to("#flash", { opacity: 0, duration: 0.4 }, land);
|
|
191
|
+
[14, -12, 9, -6, 3, 0].forEach((x, j) => tl.to(n, { x, duration: 0.05 }, land + j * 0.05));
|
|
192
|
+
tl.to(n, { scale: 1.06, duration: END - land, ease: "none" }, land);
|
|
193
|
+
} else if (b.kind === "title") {
|
|
194
|
+
const d = 1.8;
|
|
195
|
+
const n = el("div", "title" + (b.img ? " has-img" : ""), pic(b) + `<div class="tape"></div><h1>${b.title}</h1>` + (b.sub ? `<p>${b.sub}</p>` : ""));
|
|
196
|
+
beats.append(n); fit(n); hit(b.t, d); kbAt(n, b.t);
|
|
197
|
+
tl.fromTo(n, { opacity: 0, scale: 1.3 }, { opacity: 1, scale: 1, duration: 0.4, ease: "power3.out" }, b.t + 0.08);
|
|
198
|
+
tl.to(n, { opacity: 0, duration: 0.25 }, b.t + d - 0.25);
|
|
199
|
+
} else if (b.kind === "case") {
|
|
200
|
+
const d = 3.2;
|
|
201
|
+
const n = el("div", "case", pic(b) + `<div class="k">${b.kicker}</div><h2>${b.title}</h2>` +
|
|
202
|
+
(b.sub ? `<div class="s">${b.sub}</div>` : "") + (b.stamp ? `<div class="stamp" data-layout-allow-overlap>${b.stamp}</div>` : ""));
|
|
203
|
+
beats.append(n); hit(b.t, d); kbAt(n, b.t);
|
|
204
|
+
tl.fromTo(n, { opacity: 0, y: 120, rotation: -4 }, { opacity: 1, y: 0, rotation: -1.5, duration: 0.5, ease: "back.out(1.4)" }, b.t + 0.08);
|
|
205
|
+
const st = n.querySelector(".stamp");
|
|
206
|
+
if (st) tl.fromTo(st, { opacity: 0, scale: 2.4, rotation: -18 }, { opacity: 1, scale: 1, rotation: -12, duration: 0.25, ease: "power4.in" }, b.t + 0.9);
|
|
207
|
+
tl.to(n, { opacity: 0, y: -60, duration: 0.3 }, b.t + d - 0.3);
|
|
208
|
+
} else if (b.kind === "photo") {
|
|
209
|
+
const d = b.dur ?? 2.8; // a tall screenshot needs longer to read
|
|
210
|
+
const n = el("div", "photo", pic(b).replace('class="frame"', b.tall ? 'class="frame tall"' : 'class="frame"') + `<p>${b.label}</p>`);
|
|
211
|
+
if (b.pos) n.querySelector(".frame img").style.objectPosition = b.pos; // e.g. "50% 100%" to show a chat's last messages
|
|
212
|
+
beats.append(n); hit(b.t, d); kbAt(n, b.t);
|
|
213
|
+
tl.fromTo(n, { opacity: 0, y: 80, rotation: 3 }, { opacity: 1, y: 0, rotation: 1, duration: 0.45, ease: "power3.out" }, b.t + 0.08);
|
|
214
|
+
tl.to(n, { opacity: 0, scale: 0.92, duration: 0.3 }, b.t + d - 0.3);
|
|
215
|
+
} else {
|
|
216
|
+
if (listStart === null) listStart = b.t;
|
|
217
|
+
// Tip reveal: the check pops in, then the text types itself out.
|
|
218
|
+
const n = el("div", "item", `<b>✓</b><span>${b.text}</span>`);
|
|
219
|
+
list.append(n);
|
|
220
|
+
const box = n.querySelector("b"), txt = n.querySelector("span");
|
|
221
|
+
tl.set(n, { opacity: 1 }, b.t);
|
|
222
|
+
tl.fromTo(box, { scale: 0, rotation: -90 }, { scale: 1, rotation: 0, duration: 0.35, ease: "back.out(2.5)" }, b.t);
|
|
223
|
+
tl.fromTo(txt, { clipPath: "inset(0 100% 0 0)" }, { clipPath: "inset(0 0% 0 0)", duration: 0.85, ease: `steps(${b.text.length})` }, b.t + 0.05);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
// The checklist leaves when the speaker moves on: the next jump cut after the last item (max 6 s).
|
|
227
|
+
const items = BEATS.filter(b => b.kind === "item"), lastItem = items.length ? items[items.length - 1].t : 0;
|
|
228
|
+
const listEnd = Math.min(SEG_STARTS.find(s => s > lastItem + 1) ?? END, lastItem + 6);
|
|
229
|
+
if (items.length) {
|
|
230
|
+
const head = el("div", "head"); head.textContent = "TIPS AMAN:"; list.prepend(head);
|
|
231
|
+
tl.fromTo(head, { clipPath: "inset(0 100% 0 0)", opacity: 1 }, { clipPath: "inset(0 0% 0 0)", duration: 0.5, ease: "steps(12)" }, listStart - 0.6);
|
|
232
|
+
tl.fromTo("#list", { opacity: 0 }, { opacity: 1, duration: 0.3 }, listStart - 0.6);
|
|
233
|
+
tl.to("#dim", { opacity: 1, duration: 0.3 }, listStart - 0.6);
|
|
234
|
+
tl.to("#cam", { filter: "blur(10px) brightness(0.7)", duration: 0.4 }, listStart - 0.6);
|
|
235
|
+
tl.to(["#list", "#dim"], { opacity: 0, duration: 0.4 }, listEnd - 0.4);
|
|
236
|
+
tl.to("#cam", { filter: "blur(0px) brightness(1)", duration: 0.4 }, listEnd - 0.4);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Captions (hidden under the hook and while a card or the list is on screen).
|
|
240
|
+
const busy = BEATS.filter(b => b.kind !== "item" && (b.kind !== "photo" || b.tall)).map(b => [b.t, b.t + (b.kind === "case" ? 3.2 : b.kind === "end" ? END : b.kind === "photo" ? b.dur ?? 2.8 : 1.8)])
|
|
241
|
+
.concat(items.length ? [[listStart - 0.6, listEnd]] : []); // captions stay on under the hook
|
|
242
|
+
const cap = document.getElementById("captions");
|
|
243
|
+
CAPTIONS.forEach(chunk => {
|
|
244
|
+
const n = el("div", "chunk"); cap.append(n);
|
|
245
|
+
const start = chunk[0].s, end = chunk[chunk.length - 1].hold;
|
|
246
|
+
if (busy.some(([a, b]) => start < b && end > a)) return;
|
|
247
|
+
tl.fromTo(n, { opacity: 0, y: 24 }, { opacity: 1, y: 0, duration: 0.12, ease: "power2.out" }, start);
|
|
248
|
+
tl.set(n, { opacity: 0 }, end);
|
|
249
|
+
chunk.forEach((w, j) => {
|
|
250
|
+
const txt = w.w.replace(/[,.?!]+$/, "");
|
|
251
|
+
const s = el("span", "w"); s.textContent = txt; n.append(s);
|
|
252
|
+
const f = el("span", "fill"); f.textContent = txt; s.append(f);
|
|
253
|
+
const until = chunk[j + 1] ? chunk[j + 1].s : end;
|
|
254
|
+
// Fill sweeps across the word over exactly its spoken duration, one frame early (reads as in sync).
|
|
255
|
+
const t0 = Math.max(start, w.s - 1 / 30), dur = Math.max(w.e - w.s, 0.1);
|
|
256
|
+
tl.fromTo(f, { clipPath: "inset(0 100% 0 0)" }, { clipPath: "inset(0 0% 0 0)", duration: dur, ease: "power1.out" }, t0);
|
|
257
|
+
tl.fromTo(s, { scale: 1.06 }, { scale: 1, duration: 0.18, ease: "power2.out" }, t0);
|
|
258
|
+
// Same rule for every word: grey upcoming, red while spoken, white once the next word starts.
|
|
259
|
+
tl.set(f, { color: "#f2efe8" }, until);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
window.__timelines["main"] = tl;
|
|
264
|
+
</script>
|
|
265
|
+
</body>
|
|
266
|
+
</html>
|