7 Commits

Author SHA1 Message Date
mikkeli e3c843d65c feat(274): name the モトツ pair with a slash in the transect subtitles
モトツ橋にモトツシェルター -> モトツ橋 / モトツシェルター, matching the two nodes the
visualiser lights side by side on the road. The rest of the diff is Aegisub's
saved cursor position.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PLt25nCVMsoroKALYUBu6u
2026-09-30 01:29:15 +09:00
mikkeli 530238a328 feat(274): render at 60 fps, with all timing in seconds
The composition is now 60 fps (37,157 frames). Everything was already written
against time in seconds except the spectrum's release, which counted frames and
so would have shortened from 0.2 s to 0.1 s at 60 fps.

- spectrum.ts: RELEASE_FRAMES -> RELEASE_WINDOW_S (0.2 s) sampled every
  RELEASE_STEP_S (1/30 s), RELEASE_DECAY per step. At 30, 60 and 120 fps this
  reads the same instants (0, 33.3, 66.7, 100, 133.3, 166.7 ms back) with the
  same weights, so the tail keeps its length and the work per frame stays
  constant instead of growing with the frame rate.
- time.ts: secondsToFrames / framesToSeconds, the one place the two meet —
  only Remotion's API counts frames.
- camera.ts: the smoothing window's hardcoded 1/30 is now FOLLOW_SAMPLE_S,
  a sampling interval in seconds rather than a frame count.
- Composition.tsx: FPS 30 -> 60; the duration still follows the audio length.

Changing FPS is now the only edit needed to try another frame rate.

Verified: eslint + tsc clean; the 60 fps export reports 60/1 and 37,157 frames,
with audio 0.00 ms off at 1:45 and 9:00.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PLt25nCVMsoroKALYUBu6u
2026-09-30 01:29:15 +09:00
mikkeli d7b3824e1e fix(274): end the fade-out where the black hold starts
FADE_OUT_POST holds full black over the last stretch of the video, but the ramp
still aimed to finish at lastFrameS - FADE_OUT_S, so it completed inside the
hold and the two overlapped. The ramp now runs from
lastFrameS - FADE_OUT_POST - FADE_OUT_S to lastFrameS - FADE_OUT_POST, giving
FADE_OUT_S of fade followed by FADE_OUT_POST of black.

Verified: with 0.4 / 0.3 / 0.2 / 0.4 the picture is clear from 0.70 s to
618.67 s, and the rendered file is pure black for the first and last 13 frames
at 30 fps (25 at 60).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PLt25nCVMsoroKALYUBu6u
2026-09-30 01:29:13 +09:00
mikkeli 2dfd52e33d style(274): retune tunnel, shrine and other colours
Dark palette, chosen while previewing in the Studio:
- tunnel #b6c5c1 -> #a6ccc2
- shrine #a05050 -> #b94141
- other #a7b7d6 -> #8b9ab8

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PLt25nCVMsoroKALYUBu6u
2026-09-18 02:00:29 +09:00
mikkeli fde2aee5d1 feat(274): subtitles for the transect visualiser
- 274-transect.ass: Aegisub subtitles for baking into the final transect video
  (274_transect.mp4) — addresses (274-addr), places (274-place) and the
  closing credits (274-credits), all in M PLUS 1 Code.
- BOM.md: the four static M PLUS 1 Code weights it needs (Regular, Medium,
  SemiBold, Bold; SIL OFL 1.1) from coz-m/MPLUS_FONTS at 2a968b8. Static per-
  weight files rather than Google Fonts' variable TTF, so libass matches the
  styles' full names exactly.

Verified: a libass test bake resolves every style to the real files — Medium
for 274-addr, SemiBold for 274-place, Regular and Bold (for {\b1}) for
274-credits — with no fallback.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PLt25nCVMsoroKALYUBu6u
2026-09-18 02:00:29 +09:00
mikkeli e23772af17 feat(274): fade the transect from and to black
The visualiser now starts and ends on a completely black screen: a black layer
over everything whose opacity comes from fade.ts.

- FADE_IN_PRE (0.4 s): hold on full black at the very start.
- FADE_IN_S (0.3 s): then fade from black to the picture.
- FADE_OUT_S (0.2 s): fade to black at the end, reaching full black on the last
  frame ((durationInFrames - 1) / fps), not a frame after it.

Verified: eslint + tsc clean; stills show the first and last frames are pure
RGB (0, 0, 0) and brightness ramps as timed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PLt25nCVMsoroKALYUBu6u
2026-09-18 02:00:29 +09:00
mikkeli 2731d9d332 Merge pull request '274: transect composition, spectrum outro, and lyric fixes it exposed' (#1) from song274/transect into song274/master
Reviewed-on: #1
2026-09-17 01:36:53 +00:00
9 changed files with 250 additions and 14 deletions
+168
View File
@@ -0,0 +1,168 @@
[Script Info]
; Script generated by Aegisub 3.2.2
; http://www.aegisub.org/
Title: Default Aegisub file
ScriptType: v4.00+
WrapStyle: 0
ScaledBorderAndShadow: yes
YCbCr Matrix: PC.601
PlayResX: 1920
PlayResY: 1080
[Aegisub Project Garbage]
Audio File: 274_transect.mp4
Video File: 274_transect.mp4
Video AR Mode: 4
Video AR Value: 1.777778
Video Zoom Percent: 0.500000
Scroll Position: 74
Active Line: 82
Video Position: 8587
[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,1
Style: rightoffset,Source Han Serif SC SemiBold,40,&H006C574D,&H000000FF,&H006E6E6E,&H00B4CBD7,-1,0,0,0,100,100,0,0,1,0,0,3,10,60,60,1
Style: main heavy,源ノ明朝 SemiBold,45,&H006C574D,&H000000FF,&H003D2D22,&HE56C574D,-1,0,0,0,100,100,5,0,1,0,5,2,10,10,125,1
Style: happy1551_base,Inziu Iosevka Slab SC,75,&H0000D2FF,&H000000FF,&H000000C8,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,10,10,70,1
Style: happy1551_zx,Inziu Iosevka Slab SC,75,&H000000FF,&H000000FF,&H0000006E,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,10,10,70,1
Style: happy1551_60,Inziu Iosevka Slab SC,75,&H00E0DDCE,&H000000FF,&H00353537,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,10,10,70,1
Style: happy1551_houye,Inziu Iosevka Slab SC,75,&H00DA7400,&H000000FF,&H00DFD1A8,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,10,10,70,1
Style: happy1551_s20,Inziu Iosevka Slab SC,75,&H00B38766,&H000000FF,&H00F1F1F1,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,10,10,70,1
Style: leftoffset,Source Han Serif SC Medium,65,&H00FFFFFF,&H000000FF,&H00343434,&H00000000,0,0,0,0,100,100,0,0,1,2,0,1,40,10,20,1
Style: main heavy - left,源ノ明朝 SemiBold,90,&H003F6CCD,&H000000FF,&H00221716,&H85AAB2B7,0,0,0,0,100,100,0,0,1,3,0,1,10,250,100,1
Style: credits,Source Han Serif SC Medium,35,&H00C2B3A9,&H000000FF,&H000D0C0E,&H00000000,0,0,0,0,100,100,0,0,1,2,0,9,0,45,30,1
Style: transparent_timesig,Noto Serif JP Medium,60,&H00000000,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,1,150,0,200,1
Style: 274-place,M PLUS 1 Code SemiBold,70,&H001AB4F9,&H000000FF,&H003C3228,&H00000000,0,0,0,0,100,100,2,0,1,4,0,1,128,0,180,1
Style: 274-credits,M PLUS 1 Code,50,&H001AB4F9,&H000000FF,&H003C3228,&H00000000,0,0,0,0,100,100,1,0,1,2,0,3,0,150,100,1
Style: 274-addr,M PLUS 1 Code Medium,50,&H001AB4F9,&H000000FF,&H003C3228,&H00000000,0,0,0,0,100,100,4,0,1,2,0,1,128,0,262,1
Style: main heavy right,源ノ明朝 SemiBold,45,&H006C574D,&H000000FF,&H003D2D22,&HE56C574D,-1,0,0,0,100,100,5,0,1,0,5,3,10,125,125,1
Style: credits_sponsor,源ノ角ゴシック Heavy,188,&H00FFFFFF,&H000000FF,&H000D0C0E,&H00000000,-1,0,0,0,100,100,0,0,1,4.5,4.5,5,0,0,0,1
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:14.54,0:00:19.67,274-addr,,0,0,0,,夕張郡由仁町川端
Dialogue: 0,0:00:17.89,0:00:19.67,274-place,,0,0,0,,川端橋
Dialogue: 0,0:00:19.67,0:00:25.04,274-place,,0,0,0,,
Dialogue: 0,0:00:25.04,0:00:42.01,274-addr,,0,0,0,,夕張郡栗山町滝下
Dialogue: 0,0:00:28.41,0:00:32.06,274-place,,0,0,0,,川端発電所
Dialogue: 0,0:00:32.06,0:00:35.44,274-place,,0,0,0,,滝下八幡神社
Dialogue: 0,0:00:35.44,0:00:39.84,274-place,,0,0,0,,滝ノ下信号場
Dialogue: 0,0:00:39.84,0:00:42.01,274-place,,0,0,0,,雨霧橋
Dialogue: 0,0:00:42.01,0:00:42.45,274-place,,0,0,0,,
Dialogue: 0,0:00:42.45,0:00:46.69,274-addr,,0,0,0,,夕張郡由仁町川端
Dialogue: 0,0:00:45.15,0:00:46.69,274-place,,0,0,0,,竜仙橋
Dialogue: 0,0:00:46.69,0:00:47.63,274-place,,0,0,0,,
Dialogue: 0,0:00:47.63,0:00:54.73,274-addr,,0,0,0,,夕張郡栗山町滝下
Dialogue: 0,0:00:50.08,0:00:52.88,274-place,,0,0,0,,草木橋
Dialogue: 0,0:00:52.88,0:00:54.73,274-place,,0,0,0,,滝の上こ線橋
Dialogue: 0,0:00:54.73,0:00:56.44,274-place,,0,0,0,,
Dialogue: 0,0:00:56.44,0:01:08.78,274-addr,,0,0,0,,夕張市滝ノ上
Dialogue: 0,0:00:59.95,0:01:03.47,274-place,,0,0,0,,滝の上発電所
Dialogue: 0,0:01:03.47,0:01:06.94,274-place,,0,0,0,,滝ノ上信号場
Dialogue: 0,0:01:06.94,0:01:08.78,274-place,,0,0,0,,滝ノ上神社
Dialogue: 0,0:01:08.78,0:01:10.50,274-place,,0,0,0,,
Dialogue: 0,0:01:10.50,0:01:45.03,274-addr,,0,0,0,,夕張市紅葉山
Dialogue: 0,0:01:12.05,0:01:13.86,274-place,,0,0,0,,十三里信号場
Dialogue: 0,0:01:13.86,0:01:16.44,274-place,,0,0,0,,十三里橋
Dialogue: 0,0:01:16.44,0:01:18.78,274-place,,0,0,0,,紅葉山駐車帯
Dialogue: 0,0:01:18.78,0:01:21.06,274-place,,0,0,0,,瑞穂橋
Dialogue: 0,0:01:21.06,0:01:24.44,274-place,,0,0,0,,セブンイレブン夕張紅葉山店
Dialogue: 0,0:01:24.44,0:01:27.06,274-place,,0,0,0,,新夕張駅
Dialogue: 0,0:01:27.06,0:01:30.02,274-place,,0,0,0,,紅葉山郵便局
Dialogue: 0,0:01:30.02,0:01:32.30,274-place,,0,0,0,,紅葉山神社
Dialogue: 0,0:01:32.30,0:01:35.24,274-place,,0,0,0,,セイコーマートもみじやま店
Dialogue: 0,0:01:35.24,0:01:38.48,274-place,,0,0,0,,紅楓橋
Dialogue: 0,0:01:38.48,0:01:40.25,274-place,,0,0,0,,ポン久留喜橋
Dialogue: 0,0:01:40.25,0:01:42.44,274-place,,0,0,0,,久留喜橋
Dialogue: 0,0:01:42.44,0:01:43.79,274-place,,0,0,0,,楓橋
Dialogue: 0,0:01:43.79,0:01:45.03,274-place,,0,0,0,,桂橋
Dialogue: 0,0:01:45.03,0:01:45.77,274-place,,0,0,0,,
Dialogue: 0,0:01:45.77,0:02:00.74,274-addr,,0,0,0,,夕張市楓
Dialogue: 0,0:01:49.12,0:01:50.76,274-place,,0,0,0,,千紫橋
Dialogue: 0,0:01:50.76,0:01:52.51,274-place,,0,0,0,,楓駐車帯
Dialogue: 0,0:01:52.51,0:01:53.80,274-place,,0,0,0,,万紅橋
Dialogue: 0,0:01:53.80,0:01:55.54,274-place,,0,0,0,,登川神社
Dialogue: 0,0:01:55.54,0:01:57.32,274-place,,0,0,0,,楓信号場
Dialogue: 0,0:01:57.32,0:01:59.00,274-place,,0,0,0,,登川橋
Dialogue: 0,0:01:59.00,0:02:00.74,274-place,,0,0,0,,登川郵便局
Dialogue: 0,0:02:00.74,0:02:01.26,274-place,,0,0,0,,
Dialogue: 0,0:02:01.26,0:02:06.62,274-addr,,0,0,0,,夕張市登川
Dialogue: 0,0:02:02.93,0:02:05.23,274-place,,0,0,0,,花園橋
Dialogue: 0,0:02:05.23,0:02:06.62,274-place,,0,0,0,,登川トンネル
Dialogue: 0,0:02:06.62,0:03:30.80,274-place,,0,0,0,,
Dialogue: 0,0:03:30.80,0:03:39.41,274-addr,,0,0,0,,勇払郡むかわ町穂別長和
Dialogue: 0,0:03:34.55,0:03:36.77,274-place,,0,0,0,,オマナイ橋
Dialogue: 0,0:03:36.77,0:03:39.41,274-place,,0,0,0,,桂木橋
Dialogue: 0,0:03:39.41,0:03:41.13,274-place,,0,0,0,,
Dialogue: 0,0:03:41.13,0:04:05.59,274-addr,,0,0,0,,勇払郡むかわ町穂別稲里
Dialogue: 0,0:03:44.62,0:03:46.67,274-place,,0,0,0,,大和橋
Dialogue: 0,0:03:46.67,0:03:48.38,274-place,,0,0,0,,豊進橋
Dialogue: 0,0:03:48.38,0:03:51.61,274-place,,0,0,0,,豊進神社
Dialogue: 0,0:03:51.61,0:03:53.14,274-place,,0,0,0,,豊里橋
Dialogue: 0,0:03:53.14,0:03:55.52,274-place,,0,0,0,,稲豊橋
Dialogue: 0,0:03:55.52,0:03:57.78,274-place,,0,0,0,,稲里橋
Dialogue: 0,0:03:57.78,0:04:01.08,274-place,,0,0,0,,稲里簡易郵便局
Dialogue: 0,0:04:01.08,0:04:03.50,274-place,,0,0,0,,佐主橋
Dialogue: 0,0:04:03.50,0:04:05.59,274-place,,0,0,0,,穂別ダム神社
Dialogue: 0,0:04:05.59,0:04:05.96,274-place,,0,0,0,,
Dialogue: 0,0:04:05.96,0:04:25.27,274-addr,,0,0,0,,勇払郡むかわ町穂別長和
Dialogue: 0,0:04:10.08,0:04:13.02,274-place,,0,0,0,,長和トンネル
Dialogue: 0,0:04:13.02,0:04:15.39,274-place,,0,0,0,,穂別大橋
Dialogue: 0,0:04:15.39,0:04:18.81,274-place,,0,0,0,,山水橋
Dialogue: 0,0:04:18.81,0:04:23.20,274-place,,0,0,0,,沼の沢橋
Dialogue: 0,0:04:23.20,0:04:25.27,274-place,,0,0,0,,稲里トンネル
Dialogue: 0,0:04:25.27,0:04:26.00,274-place,,0,0,0,,
Dialogue: 0,0:04:26.00,0:04:55.52,274-addr,,0,0,0,,勇払郡むかわ町穂別福山
Dialogue: 0,0:04:29.74,0:04:31.50,274-place,,0,0,0,,ペンケ橋
Dialogue: 0,0:04:31.50,0:04:33.62,274-place,,0,0,0,,下福山橋
Dialogue: 0,0:04:33.62,0:04:35.02,274-place,,0,0,0,,オロロップ橋
Dialogue: 0,0:04:35.02,0:04:37.07,274-place,,0,0,0,,福山橋
Dialogue: 0,0:04:37.07,0:04:38.70,274-place,,0,0,0,,福山神社
Dialogue: 0,0:04:38.70,0:04:41.02,274-place,,0,0,0,,福山大橋
Dialogue: 0,0:04:41.02,0:04:43.29,274-place,,0,0,0,,千坂橋
Dialogue: 0,0:04:43.29,0:04:45.12,274-place,,0,0,0,,オコタン橋
Dialogue: 0,0:04:45.12,0:04:46.22,274-place,,0,0,0,,モトツトンネル
Dialogue: 0,0:04:46.22,0:04:48.98,274-place,,0,0,0,,モトツ橋 / モトツシェルター
Dialogue: 0,0:04:48.98,0:04:51.39,274-place,,0,0,0,,福山トンネル
Dialogue: 0,0:04:51.39,0:04:53.61,274-place,,0,0,0,,福穂橋
Dialogue: 0,0:04:53.61,0:04:55.52,274-place,,0,0,0,,穂高トンネル
Dialogue: 0,0:04:55.52,0:05:22.60,274-place,,0,0,0,,
Dialogue: 0,0:05:22.60,0:05:50.73,274-addr,,0,0,0,,勇払郡占冠村中央
Dialogue: 0,0:05:25.76,0:05:28.88,274-place,,0,0,0,,穂高橋
Dialogue: 0,0:05:28.88,0:05:31.52,274-place,,0,0,0,,高翠橋
Dialogue: 0,0:05:31.52,0:05:34.95,274-place,,0,0,0,,占翠橋
Dialogue: 0,0:05:34.95,0:05:38.08,274-place,,0,0,0,,名石駐車場
Dialogue: 0,0:05:38.08,0:05:40.99,274-place,,0,0,0,,名石橋
Dialogue: 0,0:05:40.99,0:05:43.96,274-place,,0,0,0,,赤岩大橋
Dialogue: 0,0:05:43.96,0:05:48.62,274-place,,0,0,0,,渓流大橋
Dialogue: 0,0:05:48.62,0:05:50.73,274-place,,0,0,0,,日高トンネル
Dialogue: 0,0:05:50.73,0:07:11.39,274-place,,0,0,0,,
Dialogue: 0,0:07:11.39,0:07:16.38,274-addr,,0,0,0,,沙流郡日高町日高
Dialogue: 0,0:07:14.14,0:07:16.38,274-place,,0,0,0,,日夕橋
Dialogue: 0,0:07:16.38,0:07:28.85,274-place,,0,0,0,,
Dialogue: 0,0:07:28.85,0:07:34.09,274-addr,,0,0,0,,沙流郡日高町宮下町
Dialogue: 0,0:07:31.68,0:07:34.09,274-place,,0,0,0,,日高神社
Dialogue: 0,0:07:34.09,0:07:42.90,274-place,,0,0,0,,
Dialogue: 0,0:07:42.90,0:07:55.11,274-addr,,0,0,0,,沙流郡日高町本町東
Dialogue: 0,0:07:45.51,0:07:49.88,274-place,,0,0,0,,新川橋
Dialogue: 0,0:07:49.88,0:07:52.76,274-place,,0,0,0,,セイコーマート日高道の駅店
Dialogue: 0,0:07:52.76,0:07:55.11,274-place,,0,0,0,,日高郵便局
Dialogue: 0,0:07:55.11,0:07:56.69,274-place,,0,0,0,,
Dialogue: 0,0:07:56.69,0:08:02.15,274-addr,,0,0,0,,沙流郡日高町千栄
Dialogue: 0,0:07:59.36,0:08:02.15,274-place,,0,0,0,,北電日高発電所
Dialogue: 0,0:08:02.15,0:08:03.90,274-place,,0,0,0,,
Dialogue: 0,0:08:03.90,0:08:09.44,274-addr,,0,0,0,,沙流郡日高町日高
Dialogue: 0,0:08:06.38,0:08:09.44,274-place,,0,0,0,,右左府発電所
Dialogue: 0,0:08:09.44,0:08:10.93,274-place,,0,0,0,,
Dialogue: 0,0:08:10.93,0:08:12.66,274-credits,,0,0,0,,{\b1}東に続く{\b0}
Dialogue: 0,0:08:12.66,0:08:17.66,274-place,,0,0,0,,
Dialogue: 0,0:08:24.92,0:08:31.98,274-credits,,0,0,0,,{\b1}'274'{\b0}\Na song about (one stretch of) national route 274
Dialogue: 0,0:08:31.98,0:08:38.97,274-credits,,0,0,0,,music by Tomaš Orika / data aggregation by Mikkeli Matlock\Nvisualiser by Joroz Tosci / powered by Claude Opus 5
Dialogue: 0,0:08:38.97,0:08:46.00,274-credits,,0,0,0,,voiced by Yuzuki Yukari Shizuku (A.I.VOICE 2)
Dialogue: 0,0:08:46.00,0:08:52.98,274-credits,,0,0,0,,{\b1}thanks to:{\b0}\Nroute01.com\Nhgf03030.a.la9.jp
Dialogue: 0,0:08:52.98,0:09:00.00,274-credits,,0,0,0,,{\b1}thanks to:{\b0}\N全国道路施設点検データベース\N国土交通省北海道開発局 道路施設の老朽化対策\N国土数値情報 河川データセット
Dialogue: 0,0:09:00.00,0:09:06.98,274-credits,,0,0,0,,{\b1}thanks to:{\b0}\N日本郵便 郵便番号データ\N国土地理院 逆ジオコーディング API
Dialogue: 0,0:09:06.98,0:09:14.00,274-credits,,0,0,0,,{\b1}thanks to:{\b0}\NOpenStreetMap contributors\NGoogle Maps\NWikipedia
Dialogue: 0,0:09:14.00,0:09:20.96,274-credits,,0,0,0,,2026 northwich committee of magic researches
Dialogue: 0,0:09:20.96,0:09:22.98,274-credits,,0,0,0,,see you in Yuubari
+5 -1
View File
@@ -1,6 +1,6 @@
# Bill of materials — 274 # Bill of materials — 274
5 file(s), 465.6 MiB. Generated by `scripts/bom_gen.py`. 9 file(s), 472.1 MiB. Generated by `scripts/bom_gen.py`.
## audio ## audio
@@ -19,5 +19,9 @@ file size hash description
file size hash description file size hash description
-------------------------------- ------- -------------------------- ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -------------------------------- ------- -------------------------- ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
`fonts/MPLUS1Code-Bold.ttf` 1.6 MiB `2bb7c37e098719928fc47383` M PLUS 1 Code Bold (static TTF, SIL OFL 1.1) from coz-m/MPLUS_FONTS at 2a968b8 (fonts/MPLUS1Code/ttf). Used by 274-transect.ass for {\b1} runs in 274-credits.
`fonts/MPLUS1Code-Medium.ttf` 1.6 MiB `0c9342dd910f7daaea1d40ac` M PLUS 1 Code Medium (static TTF, SIL OFL 1.1) from coz-m/MPLUS_FONTS at 2a968b8 (fonts/MPLUS1Code/ttf). Used by 274-transect.ass style 274-addr.
`fonts/MPLUS1Code-Regular.ttf` 1.6 MiB `2f8308951632e89da5e3bcee` M PLUS 1 Code Regular (static TTF, SIL OFL 1.1) from coz-m/MPLUS_FONTS at 2a968b8 (fonts/MPLUS1Code/ttf). Used by 274-transect.ass style 274-credits.
`fonts/MPLUS1Code-SemiBold.ttf` 1.6 MiB `8d5d9c9d3e427a99b2b4e25e` M PLUS 1 Code SemiBold (static TTF, SIL OFL 1.1) from coz-m/MPLUS_FONTS at 2a968b8 (fonts/MPLUS1Code/ttf). Used by 274-transect.ass style 274-place.
`fonts/NotoSerifJP-Medium.otf` 6.0 MiB `b3aa1bf0e4aeb96623aeaab1` Noto Serif JP Medium (SubsetOTF/JP, v2.003, SIL OFL 1.1) from notofonts/noto-cjk release Serif2.003 (12_NotoSerifJP.zip). Used by 274.ass styles transparent_lyrics_en, transparent_lyrics_jp. `fonts/NotoSerifJP-Medium.otf` 6.0 MiB `b3aa1bf0e4aeb96623aeaab1` Noto Serif JP Medium (SubsetOTF/JP, v2.003, SIL OFL 1.1) from notofonts/noto-cjk release Serif2.003 (12_NotoSerifJP.zip). Used by 274.ass styles transparent_lyrics_en, transparent_lyrics_jp.
`fonts/NotoSerifJP-SemiBold.otf` 6.0 MiB `81863c254d5833df489433c4` Noto Serif JP SemiBold (SubsetOTF/JP, v2.003, SIL OFL 1.1) from notofonts/noto-cjk release Serif2.003 (12_NotoSerifJP.zip). Used by 274.ass style transparent_title. `fonts/NotoSerifJP-SemiBold.otf` 6.0 MiB `81863c254d5833df489433c4` Noto Serif JP SemiBold (SubsetOTF/JP, v2.003, SIL OFL 1.1) from notofonts/noto-cjk release Serif2.003 (12_NotoSerifJP.zip). Used by 274.ass style transparent_title.
+1 -1
View File
@@ -4,7 +4,7 @@ import { loadTransectData } from "./data";
import { darkPalette } from "./palette"; import { darkPalette } from "./palette";
import { Transect, type TransectProps } from "./Transect"; import { Transect, type TransectProps } from "./Transect";
const FPS = 30; const FPS = 60;
/** Length of song-274/audio/274_release2.wav (see BOM.md). */ /** Length of song-274/audio/274_release2.wav (see BOM.md). */
const AUDIO_DURATION_S = 619.270083; const AUDIO_DURATION_S = 619.270083;
+7 -1
View File
@@ -18,6 +18,7 @@ import {
type Frame, type Frame,
} from "./camera"; } from "./camera";
import type { Cue, RosterItem, TransectData } from "./data"; import type { Cue, RosterItem, TransectData } from "./data";
import { blackOpacity } from "./fade";
import { import {
extendBounds, extendBounds,
GROUND, GROUND,
@@ -82,7 +83,7 @@ const currentCue = (list: Cue[] | undefined, t: number): Cue | undefined => {
export const Transect: React.FC<TransectProps> = ({ palette, audio, data }) => { export const Transect: React.FC<TransectProps> = ({ palette, audio, data }) => {
const frame = useCurrentFrame(); const frame = useCurrentFrame();
const { fps, width, height } = useVideoConfig(); const { fps, width, height, durationInFrames } = useVideoConfig();
const projection = useMemo(() => (data ? projectGround(data.route) : null), [data]); const projection = useMemo(() => (data ? projectGround(data.route) : null), [data]);
// The main road plus the east continuation. The paint runs into the latter // The main road plus the east continuation. The paint runs into the latter
// after 東に続く; the camera and overview stay on the main road, so that run // after 東に続く; the camera and overview stay on the main road, so that run
@@ -129,6 +130,7 @@ export const Transect: React.FC<TransectProps> = ({ palette, audio, data }) => {
} }
const t = frame / fps; const t = frame / fps;
const fadeToBlack = blackOpacity(t, (durationInFrames - 1) / fps);
const headAt = (time: number) => { const headAt = (time: number) => {
const km = kmAt(time); const km = kmAt(time);
const pos = routeAtKm(data.route, km); const pos = routeAtKm(data.route, km);
@@ -221,6 +223,10 @@ export const Transect: React.FC<TransectProps> = ({ palette, audio, data }) => {
{nodes.filter((n) => isAwake(n.look)).map(draw)} {nodes.filter((n) => isAwake(n.look)).map(draw)}
</g> </g>
</svg> </svg>
{/* Fade from and to black at the two ends (fade.ts). */}
{fadeToBlack > 0 ? (
<AbsoluteFill style={{ backgroundColor: "#000000", opacity: fadeToBlack }} />
) : null}
</AbsoluteFill> </AbsoluteFill>
); );
}; };
+6 -1
View File
@@ -27,6 +27,11 @@ export const ZOOM_IN_S = 3;
* This is the standard deviation, in seconds; the window spans ±3 of these. * This is the standard deviation, in seconds; the window spans ±3 of these.
*/ */
export const FOLLOW_SMOOTH_S = 0.6; export const FOLLOW_SMOOTH_S = 0.6;
/**
* How often that window is sampled, in seconds. Independent of fps: finer
* sampling costs more per frame and changes the average only slightly.
*/
export const FOLLOW_SAMPLE_S = 1 / 30;
/** /**
* An overview frames the covered road plus some road ahead of the head, so the * An overview frames the covered road plus some road ahead of the head, so the
* head does not sit against the frame edge: this fraction of the distance * head does not sit against the frame edge: this fraction of the distance
@@ -95,7 +100,7 @@ export const overviewWeight = (parts: Span[], t: number): number => {
/** The head's position averaged over a Gaussian window around `t`. */ /** The head's position averaged over a Gaussian window around `t`. */
export const smoothedFocus = (focusAt: (t: number) => Point, t: number): Point => { export const smoothedFocus = (focusAt: (t: number) => Point, t: number): Point => {
const step = 1 / 30; const step = FOLLOW_SAMPLE_S;
const reach = 3 * FOLLOW_SMOOTH_S; const reach = 3 * FOLLOW_SMOOTH_S;
let sumX = 0; let sumX = 0;
let sumY = 0; let sumY = 0;
+28
View File
@@ -0,0 +1,28 @@
/**
* Fade from and to a completely black screen at the two ends of the video: a
* black layer over everything, fully opaque on the first and last frames.
*/
/** Seconds of complete black at start */
export const FADE_IN_PRE = 0.4;
/** Seconds of complete black at end */
export const FADE_OUT_POST = 0.4;
/** Seconds from black to the picture at the start. 0 disables the fade-in. */
export const FADE_IN_S = 0.3;
/** Seconds from the picture to black at the end. 0 disables the fade-out. */
export const FADE_OUT_S = 0.2;
const clamp01 = (v: number) => Math.min(1, Math.max(0, v));
/**
* Opacity of the black layer at time `t`.
* @param lastFrameS time of the video's last frame, (durationInFrames - 1) / fps,
* so that frame is fully black rather than nearly so
*/
export const blackOpacity = (t: number, lastFrameS: number): number => {
const fadeIn = (t < FADE_IN_PRE) ? 1 : (FADE_IN_S > 0 ? 1 - clamp01((t - FADE_IN_PRE) / FADE_IN_S) : 0);
// Black from here to the last frame; the fade-out ramp ends exactly here.
const blackFromS = lastFrameS - FADE_OUT_POST;
const fadeOut = (t >= blackFromS) ? 1 : (FADE_OUT_S > 0 ? clamp01((t - (blackFromS - FADE_OUT_S)) / FADE_OUT_S) : 0);
return Math.max(fadeIn, fadeOut);
};
+3 -3
View File
@@ -42,13 +42,13 @@ export const darkPalette: TransectPalette = {
roadAhead: "#49565f", // --rule roadAhead: "#49565f", // --rule
roadCovered: "#fdb021", // --road roadCovered: "#fdb021", // --road
bridge: "#9be9de", // --bridge bridge: "#9be9de", // --bridge
tunnel: "#b6c5c1", // --tunnel tunnel: "#a6ccc2", // --tunnel
shed: "#eba9eb", // --shed shed: "#eba9eb", // --shed
watercourse: "#5FB6D6", // --water watercourse: "#5FB6D6", // --water
place: "#68ad7f", // --place place: "#68ad7f", // --place
railway: "#68ad7f", // --railway railway: "#68ad7f", // --railway
shrine: "#a05050", // --shrine shrine: "#b94141", // --shrine
other: "#a7b7d6", // --util other: "#8b9ab8", // --util
}; };
export const lightPalette: TransectPalette = { export const lightPalette: TransectPalette = {
+18 -7
View File
@@ -1,5 +1,6 @@
import { visualizeAudio, type MediaUtilsAudioData } from "@remotion/media-utils"; import { visualizeAudio, type MediaUtilsAudioData } from "@remotion/media-utils";
import { random } from "remotion"; import { random } from "remotion";
import { secondsToFrames } from "./time";
/** /**
* The outro spectrum: from SPECTRUM_START_S the nodes stop following the lyric * The outro spectrum: from SPECTRUM_START_S the nodes stop following the lyric
@@ -43,11 +44,16 @@ export const TREBLE_TILT = 0.5;
export const CURVE = 0.6; export const CURVE = 0.6;
/** /**
* Release: a node's level is the loudest of its last RELEASE_FRAMES frames, each * Release: a node's level is the loudest reading over the RELEASE_WINDOW_S
* frame back multiplied by RELEASE_DECAY once more. Hits jump up at once and * before it, sampled every RELEASE_STEP_S and multiplied by RELEASE_DECAY once
* fall back smoothly, without any state carried between frames. * more per step back. Hits jump up at once and fall back smoothly, without any
* state carried between frames.
*
* Sampling by time rather than by frame keeps the tail the same length, and the
* work per frame the same, whatever the fps.
*/ */
export const RELEASE_FRAMES = 6; export const RELEASE_WINDOW_S = 0.2;
export const RELEASE_STEP_S = 1 / 30;
export const RELEASE_DECAY = 0.75; export const RELEASE_DECAY = 0.75;
/** FFT size for visualizeAudio: bins are (sample rate / 2) / FFT_SAMPLES wide (23.4 Hz). */ /** FFT size for visualizeAudio: bins are (sample rate / 2) / FFT_SAMPLES wide (23.4 Hz). */
@@ -69,7 +75,7 @@ export const spectrumWeight = (t: number): number =>
* otherwise the spectrum's start, so nothing is fetched for the rest of the song. * otherwise the spectrum's start, so nothing is fetched for the rest of the song.
*/ */
export const audioWindowFrame = (frame: number, fps: number): number => export const audioWindowFrame = (frame: number, fps: number): number =>
Math.max(frame, Math.floor((SPECTRUM_START_S - SPECTRUM_FADE_S) * fps)); Math.max(frame, secondsToFrames(SPECTRUM_START_S - SPECTRUM_FADE_S, fps));
/** `count` log-spaced band centres from FREQ_LOW_HZ to FREQ_HIGH_HZ. */ /** `count` log-spaced band centres from FREQ_LOW_HZ to FREQ_HIGH_HZ. */
export const bandFrequencies = (count: number): number[] => export const bandFrequencies = (count: number): number[] =>
@@ -131,7 +137,12 @@ export const bandLevels = (args: {
frequencies.length > 1 ? Math.pow(FREQ_HIGH_HZ / FREQ_LOW_HZ, 1 / (frequencies.length - 1)) : 2; frequencies.length > 1 ? Math.pow(FREQ_HIGH_HZ / FREQ_LOW_HZ, 1 / (frequencies.length - 1)) : 2;
const levels = frequencies.map(() => 0); const levels = frequencies.map(() => 0);
for (let back = 0; back < RELEASE_FRAMES && frame - back >= 0; back++) { const steps = Math.max(1, Math.round(RELEASE_WINDOW_S / RELEASE_STEP_S));
for (let step = 0; step < steps; step++) {
const back = secondsToFrames(step * RELEASE_STEP_S, fps);
if (frame - back < 0) {
break;
}
const spectrum = visualizeAudio({ const spectrum = visualizeAudio({
audioData, audioData,
frame: frame - back, frame: frame - back,
@@ -140,7 +151,7 @@ export const bandLevels = (args: {
dataOffsetInSeconds, dataOffsetInSeconds,
optimizeFor: "accuracy", optimizeFor: "accuracy",
}); });
const decay = Math.pow(RELEASE_DECAY, back); const decay = Math.pow(RELEASE_DECAY, step);
frequencies.forEach((hz, i) => { frequencies.forEach((hz, i) => {
const raw = bandValue(spectrum, hz, ratio, binHz); const raw = bandValue(spectrum, hz, ratio, binHz);
const level = const level =
+14
View File
@@ -0,0 +1,14 @@
/**
* Frame-rate conversions.
*
* Every timing constant in this composition is in seconds, so the animation is
* identical at any fps. Only Remotion's own API counts frames, so convert here
* rather than writing frame counts into the constants.
*/
/** Seconds to a whole number of frames, rounded to the nearest. */
export const secondsToFrames = (seconds: number, fps: number): number =>
Math.round(seconds * fps);
/** A frame number back to its time in seconds. */
export const framesToSeconds = (frames: number, fps: number): number => frames / fps;