1// this script is mostly written by ai
2
3const urls: Record<string, string> = {
4 "Kappacap": "https://www.youtube.com/channel/UClywJ-scBa8hcCywQ_7gWbg",
5 "atamai": "https://www.tumblr.com/atamai-m8",
6 "moxley": "https://www.youtube.com/channel/UCNJ6EbYQcnqpB6cQLGYXQow",
7 "TheJustinator": "https://www.youtube.com/channel/UCdy0IJXbdKyTWyKajyEvodw",
8 "chaosyumi": "https://chaosyumi.net",
9 "LunaPogi": "https://www.youtube.com/channel/UC7NHQWu3awWcC0rm8nB5CfQ",
10 "zeccet": "https://www.youtube.com/channel/UChec240BfC1UKX9DvBBIyLQ",
11 "TheRealTinyWorld": "https://www.youtube.com/channel/UCLfv-6_J0WLsA2jAsJxiLOg",
12 "Fred French VA": "https://www.castingcall.club/fredfrenchiii",
13 "rqfirqfo": "https://www.youtube.com/channel/UCqeAGMquQNtrSfd-quZX3KQ",
14 "krispykarim": "https://www.youtube.com/@krispykarim",
15 "SimplyDoodled Studios": "https://www.youtube.com/channel/UCkhbgaqiIkliOKyEORnkhDg",
16 "paper clover": "",
17 "Coffee": "https://www.youtube.com/channel/UCftnPCmG6dfQ50YOMRMtkjg",
18 "harjjw": "https://www.youtube.com/channel/UCPQv3ueB-E771gnwcgALzxw",
19 "yellowmarkers": "https://www.youtube.com/channel/UCcR5G761gkEKvWskjEqWFkQ",
20 "TheNothingGuy07": "https://www.youtube.com/channel/UC8SZGQ32RQxiQeTnQ5hgYAg",
21 "bill wurtz": "https://billwurtz.com",
22};
23
24const animators: [string, number][] = [
25 ["atamai", 0 * 60 + 3.1],
26 ["TheJustinator", 1 * 60 + 2],
27 ["chaosyumi", 1 * 60 + 33],
28 ["TheJustinator", 2 * 60 + 1],
29 ["zeccet", 2 * 60 + 22],
30 ["TheRealTinyWorld", 3 * 60 + 10],
31 ["LunaPogi", 3 * 60 + 35],
32 ["moxley", 3 * 60 + 50],
33 ["rqfirqfo", 4 * 60 + 3],
34 ["krispykarim", 4 * 60 + 35],
35 ["SimplyDoodled Studios", 5 * 60 + 4],
36 ["chaosyumi", 5 * 60 + 33],
37 ["paper clover", 6 * 60 + 4],
38 ["harjjw", 6 * 60 + 37],
39 ["yellowmarkers", 7 * 60 + 3],
40 ["TheNothingGuy07", 7 * 60 + 36],
41 ["LunaPogi", 8 * 60 + 0],
42 ["paper clover", 9 * 60 + 11],
43];
44
45const narrators: [string, number][] = [
46 ["moxley", 0 * 60 + 3.1],
47 ["Fred French VA", 3 * 60 + 10],
48 ["krispykarim", 4 * 60 + 35],
49 ["LunaPogi", 8 * 60 + 0],
50];
51
52const musicians: [string, number][] = [
53 ["moxley", 0 * 60 + 3.1],
54 ["TheJustinator", 3 * 60 + 10],
55 ["LunaPogi", 6 * 60 + 4],
56 ["bill wurtz", 9 * 60 + 11],
57];
58
59// random colors for each creator
60const creatorHues: Record<string, number> = {};
61for (const name of Object.keys(urls)) {
62 let hash = 0;
63 for (let i = 0; i < name.length; i++) {
64 hash = ((hash << 5) - hash) + name.charCodeAt(i);
65 hash |= 0;
66 }
67 creatorHues[name] = hash % 360;
68}
69
70function findCurrent(list: [string, number][], time: number): string | null {
71 let current: string | null = null;
72 for (const [name, start] of list) {
73 if (time >= start) {
74 current = name;
75 } else {
76 break;
77 }
78 }
79 return current;
80}
81
82function createCreditElement(name: string, role: string): string {
83 const hue = creatorHues[name] ?? 0;
84 const url = urls[name];
85 const style = `background: hsl(${hue}, 60%, 25%); color: hsl(${hue}, 80%, 85%);`;
86
87 if (url) {
88 return `<a class="custom" href="${url}" target="_blank" style="${style}">${name} <small>(${role})</small></a>`;
89 }
90 return `<a style="${style}">${name} <small>(${role})</small></a>`;
91}
92
93const creditsBox = document.getElementById("live-credits")!;
94
95let lastCreditsKey = "";
96
97function updateCredits(video) {
98 const time = video.currentTime;
99
100 const animator = time > 9 * 60 + 32 ? null : findCurrent(animators, time);
101 const narrator = time > 9 * 60 + 11 ? null : findCurrent(narrators, time);
102 const musician = time > 9 * 60 + 32 ? null : findCurrent(musicians, time);
103
104 // group roles by person
105 const rolesByPerson: Record<string, string[]> = {};
106 if (animator) (rolesByPerson[animator] ??= []).push("animation");
107 if (narrator) (rolesByPerson[narrator] ??= []).push("narration");
108 if (musician) (rolesByPerson[musician] ??= []).push("music");
109
110 const creditsKey = JSON.stringify(rolesByPerson);
111 if (creditsKey === lastCreditsKey) return;
112 lastCreditsKey = creditsKey;
113
114 const html = Object.entries(rolesByPerson)
115 .map(([name, roles]) => createCreditElement(name, roles.join(", ")))
116 .join(" ");
117
118 creditsBox.innerHTML = html
119 ? "<b style='flex:none;padding:0 8px'>credits: </b>" + html
120 : "";
121}
122
123globalThis.onCloverVideoInit = (id: string, video: HTMLVideoElement) => {
124 if (id === "main-player") {
125 updateCredits(video);
126 video.addEventListener("timeupdate", () => {
127 updateCredits(video);
128 });
129 video.addEventListener("seeked", () => {
130 updateCredits(video);
131 });
132 }
133};