@double_geste/agent-standup 1.22.0 → 1.23.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/bin/agent-standup.js +6 -3
- package/kokoro-worker.js +25 -0
- package/kokoro.js +82 -0
- package/package.json +3 -1
- package/public/index.html +94 -30
- package/public/locales/de.json +12 -1
- package/public/locales/en.json +11 -0
- package/public/locales/es.json +12 -1
- package/public/locales/ja.json +12 -1
- package/public/locales/pt-BR.json +17 -2
- package/public/locales/zh-CN.json +12 -1
- package/public/locales/zh-TW.json +17 -6
- package/public/meme.css +60 -0
- package/public/meme.js +235 -0
- package/public/sfx/oh-my-god.mp3 +0 -0
- package/public/sfx/scratch-1.mp3 +0 -0
- package/public/sfx/scratch-2.mp3 +0 -0
- package/public/sfx/scratch-3.mp3 +0 -0
- package/server.js +61 -33
package/public/locales/es.json
CHANGED
|
@@ -165,5 +165,16 @@
|
|
|
165
165
|
"Open folder": "Abrir carpeta",
|
|
166
166
|
"custom character(s) loaded": "personaje(s) propio(s) cargado(s)",
|
|
167
167
|
"Drop your pixel art (PNG) in this folder to replace the characters:": "Suelta tu pixel art (PNG) en esta carpeta para reemplazar los personajes:",
|
|
168
|
-
"lead.png = the lead · you.png = you · others = the sub-agents · name-talk.png = open mouth": "lead.png = el agente principal · you.png = tú · el resto = los subagentes · name-talk.png = boca abierta"
|
|
168
|
+
"lead.png = the lead · you.png = you · others = the sub-agents · name-talk.png = open mouth": "lead.png = el agente principal · you.png = tú · el resto = los subagentes · name-talk.png = boca abierta",
|
|
169
|
+
"🔥 Meme mode (unhinged)": "🔥 Modo meme (desmadre total)",
|
|
170
|
+
"Downloading, you can carry on: system voices are used meanwhile.": "Descargando, puedes seguir: mientras tanto se usan las voces del sistema.",
|
|
171
|
+
"Effects volume": "Volumen de los efectos",
|
|
172
|
+
"Good-quality local voices, mostly English (one French voice). Free, nothing leaves your machine.": "Voces locales de buena calidad, sobre todo en inglés (una sola voz en francés). Gratis, nada sale de tu equipo.",
|
|
173
|
+
"Installing…": "Instalando…",
|
|
174
|
+
"Intensity": "Intensidad",
|
|
175
|
+
"Loading the model (downloaded on first use, about 155 MB)…": "Cargando el modelo (se descarga la primera vez, unos 155 MB)…",
|
|
176
|
+
"npm not found: install Node.js to enable this option.": "No se encontró npm: instala Node.js para activar esta opción.",
|
|
177
|
+
"The engine stopped: system voices are used instead.": "El motor se detuvo: se usan las voces del sistema.",
|
|
178
|
+
"Very natural voices (installs on demand)": "Voces muy naturales (se instalan bajo demanda)",
|
|
179
|
+
"Voices ready.": "Voces listas."
|
|
169
180
|
}
|
package/public/locales/ja.json
CHANGED
|
@@ -165,5 +165,16 @@
|
|
|
165
165
|
"Open folder": "フォルダを開く",
|
|
166
166
|
"custom character(s) loaded": "件のカスタムキャラクターを読み込みました",
|
|
167
167
|
"Drop your pixel art (PNG) in this folder to replace the characters:": "このフォルダにドット絵(PNG)を入れると、キャラクターを差し替えられます:",
|
|
168
|
-
"lead.png = the lead · you.png = you · others = the sub-agents · name-talk.png = open mouth": "lead.png = メインエージェント · you.png = あなた · それ以外 = サブエージェント · name-talk.png = 口を開けた状態"
|
|
168
|
+
"lead.png = the lead · you.png = you · others = the sub-agents · name-talk.png = open mouth": "lead.png = メインエージェント · you.png = あなた · それ以外 = サブエージェント · name-talk.png = 口を開けた状態",
|
|
169
|
+
"🔥 Meme mode (unhinged)": "🔥 ミームモード(やりすぎ)",
|
|
170
|
+
"Downloading, you can carry on: system voices are used meanwhile.": "ダウンロード中です。そのまま作業を続けられます。それまではシステム音声を使います。",
|
|
171
|
+
"Effects volume": "効果音の音量",
|
|
172
|
+
"Good-quality local voices, mostly English (one French voice). Free, nothing leaves your machine.": "品質の良いローカル音声(主に英語、フランス語は1種類のみ)。無料で、データがPCの外に出ることはありません。",
|
|
173
|
+
"Installing…": "インストール中…",
|
|
174
|
+
"Intensity": "強度",
|
|
175
|
+
"Loading the model (downloaded on first use, about 155 MB)…": "モデルを読み込み中(初回使用時にダウンロード、約155 MB)…",
|
|
176
|
+
"npm not found: install Node.js to enable this option.": "npm が見つかりません。このオプションを使うには Node.js をインストールしてください。",
|
|
177
|
+
"The engine stopped: system voices are used instead.": "エンジンが停止しました。代わりにシステム音声を使います。",
|
|
178
|
+
"Very natural voices (installs on demand)": "とても自然な音声(必要時にインストール)",
|
|
179
|
+
"Voices ready.": "音声の準備ができました。"
|
|
169
180
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_meta": {
|
|
2
|
+
"_meta": {
|
|
3
|
+
"language": "pt-BR",
|
|
4
|
+
"reviewed": false,
|
|
5
|
+
"note": "AI draft, native review welcome"
|
|
6
|
+
},
|
|
3
7
|
"Characters": "Personagens",
|
|
4
8
|
"Reload": "Recarregar",
|
|
5
9
|
"Open folder": "Abrir pasta",
|
|
@@ -161,5 +165,16 @@
|
|
|
161
165
|
"Writing ": "Escrevendo ",
|
|
162
166
|
"You": "Você",
|
|
163
167
|
"Your agents are done 🎉": "Seus agentes terminaram 🎉",
|
|
164
|
-
"your turn": "sua vez"
|
|
168
|
+
"your turn": "sua vez",
|
|
169
|
+
"🔥 Meme mode (unhinged)": "🔥 Modo meme (sem noção)",
|
|
170
|
+
"Downloading, you can carry on: system voices are used meanwhile.": "Baixando, pode continuar: enquanto isso, as vozes do sistema são usadas.",
|
|
171
|
+
"Effects volume": "Volume dos efeitos",
|
|
172
|
+
"Good-quality local voices, mostly English (one French voice). Free, nothing leaves your machine.": "Vozes locais de boa qualidade, principalmente em inglês (só uma voz em francês). Grátis, nada sai do seu computador.",
|
|
173
|
+
"Installing…": "Instalando…",
|
|
174
|
+
"Intensity": "Intensidade",
|
|
175
|
+
"Loading the model (downloaded on first use, about 155 MB)…": "Carregando o modelo (baixado no primeiro uso, cerca de 155 MB)…",
|
|
176
|
+
"npm not found: install Node.js to enable this option.": "npm não encontrado: instale o Node.js para ativar esta opção.",
|
|
177
|
+
"The engine stopped: system voices are used instead.": "O mecanismo parou: as vozes do sistema são usadas no lugar.",
|
|
178
|
+
"Very natural voices (installs on demand)": "Vozes muito naturais (instaladas sob demanda)",
|
|
179
|
+
"Voices ready.": "Vozes prontas."
|
|
165
180
|
}
|
|
@@ -165,5 +165,16 @@
|
|
|
165
165
|
"Open folder": "打开文件夹",
|
|
166
166
|
"custom character(s) loaded": "个自定义角色已加载",
|
|
167
167
|
"Drop your pixel art (PNG) in this folder to replace the characters:": "把你的像素画(PNG)放进此文件夹即可替换角色:",
|
|
168
|
-
"lead.png = the lead · you.png = you · others = the sub-agents · name-talk.png = open mouth": "lead.png = 主代理 · you.png = 你 · 其他 = 子代理 · name-talk.png = 张嘴"
|
|
168
|
+
"lead.png = the lead · you.png = you · others = the sub-agents · name-talk.png = open mouth": "lead.png = 主代理 · you.png = 你 · 其他 = 子代理 · name-talk.png = 张嘴",
|
|
169
|
+
"🔥 Meme mode (unhinged)": "🔥 梗图模式(彻底放飞)",
|
|
170
|
+
"Downloading, you can carry on: system voices are used meanwhile.": "正在下载,你可以继续使用:期间会使用系统语音。",
|
|
171
|
+
"Effects volume": "音效音量",
|
|
172
|
+
"Good-quality local voices, mostly English (one French voice). Free, nothing leaves your machine.": "高质量的本地语音,以英语为主(法语只有一种声音)。免费,数据不会离开你的电脑。",
|
|
173
|
+
"Installing…": "正在安装…",
|
|
174
|
+
"Intensity": "强度",
|
|
175
|
+
"Loading the model (downloaded on first use, about 155 MB)…": "正在加载模型(首次使用时下载,约 155 MB)…",
|
|
176
|
+
"npm not found: install Node.js to enable this option.": "未找到 npm:请安装 Node.js 以启用此选项。",
|
|
177
|
+
"The engine stopped: system voices are used instead.": "引擎已停止:改用系统语音。",
|
|
178
|
+
"Very natural voices (installs on demand)": "超自然语音(按需安装)",
|
|
179
|
+
"Voices ready.": "语音已就绪。"
|
|
169
180
|
}
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
"Agents list (left)": "代理清單(左側)",
|
|
19
19
|
"All projects (most recent)": "所有專案(最近優先)",
|
|
20
20
|
"Ambience volume": "環境音量",
|
|
21
|
-
"An agent may be waiting for you ✋": "
|
|
22
|
-
"Animal Crossing (babble)": "
|
|
21
|
+
"An agent may be waiting for you ✋": "可能有代理在等你 ✋",
|
|
22
|
+
"Animal Crossing (babble)": "動物森友會說話聲",
|
|
23
23
|
"Asking a question": "正在提問",
|
|
24
24
|
"brief": "簡述",
|
|
25
25
|
"Busiest agents": "最忙碌的代理",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"Little lines & animations": "小對白與動畫",
|
|
80
80
|
"longest wait ": "最長等待 ",
|
|
81
81
|
"Looking for a tool": "正在找工具",
|
|
82
|
-
"looping?": "
|
|
82
|
+
"looping?": "鬼打牆?",
|
|
83
83
|
"Music volume": "音樂音量",
|
|
84
84
|
"Natural voices": "自然語音",
|
|
85
85
|
"need you": "需要你",
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
"Project to follow": "要追蹤的專案",
|
|
102
102
|
"Prompt from": "提示來自",
|
|
103
103
|
"Radio unreachable: check your connection or pick another station.": "無法連上電台:請檢查網路連線或換一個電台。",
|
|
104
|
-
"Reactions": "
|
|
104
|
+
"Reactions": "表情回應",
|
|
105
105
|
"Reactions & jokes": "回應與玩笑",
|
|
106
106
|
"Reactions bar (👍 👏 ☕)": "回應列(👍 👏 ☕)",
|
|
107
107
|
"read-only": "唯讀",
|
|
@@ -124,7 +124,7 @@
|
|
|
124
124
|
"session": "工作階段",
|
|
125
125
|
"Sessions": "工作階段",
|
|
126
126
|
"Settings": "設定",
|
|
127
|
-
"Simlish (babble)": "
|
|
127
|
+
"Simlish (babble)": "模擬市民說話聲",
|
|
128
128
|
"Sound": "聲音",
|
|
129
129
|
"Sound (M key)": "聲音(M 鍵)",
|
|
130
130
|
"Sound is playing in another Agent Standup window. Click 🔈 to take it over here.": "聲音正在另一個 Agent Standup 視窗播放。點擊 🔈 即可改在此處播放。",
|
|
@@ -165,5 +165,16 @@
|
|
|
165
165
|
"Writing ": "撰寫 ",
|
|
166
166
|
"You": "你",
|
|
167
167
|
"Your agents are done 🎉": "你的代理都完成了 🎉",
|
|
168
|
-
"your turn": "換你了"
|
|
168
|
+
"your turn": "換你了",
|
|
169
|
+
"🔥 Meme mode (unhinged)": "🔥 迷因模式(完全失控)",
|
|
170
|
+
"Downloading, you can carry on: system voices are used meanwhile.": "下載中,你可以繼續使用:這段時間會先用系統語音。",
|
|
171
|
+
"Effects volume": "音效音量",
|
|
172
|
+
"Good-quality local voices, mostly English (one French voice). Free, nothing leaves your machine.": "品質不錯的本機語音,以英文為主(法文只有一種聲音)。免費,資料不會離開你的電腦。",
|
|
173
|
+
"Installing…": "安裝中…",
|
|
174
|
+
"Intensity": "強度",
|
|
175
|
+
"Loading the model (downloaded on first use, about 155 MB)…": "正在載入模型(第一次使用時下載,約 155 MB)…",
|
|
176
|
+
"npm not found: install Node.js to enable this option.": "找不到 npm:請安裝 Node.js 才能啟用此選項。",
|
|
177
|
+
"The engine stopped: system voices are used instead.": "引擎已停止:改用系統語音。",
|
|
178
|
+
"Very natural voices (installs on demand)": "超自然語音(需要時才安裝)",
|
|
179
|
+
"Voices ready.": "語音已就緒。"
|
|
169
180
|
}
|
package/public/meme.css
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/* Meme mode (public/meme.js). Everything hangs off body.meme or lives in #memeLayer, so this file is inert while the mode is off. */
|
|
2
|
+
#memeLayer { position:absolute; inset:0; z-index:12; pointer-events:none; overflow:hidden; display:none }
|
|
3
|
+
body.meme #memeLayer { display:block }
|
|
4
|
+
.mm { position:absolute; font-family:Impact,"Arial Black",Anton,"Helvetica Neue",sans-serif; will-change:transform,opacity }
|
|
5
|
+
|
|
6
|
+
/* Impact text slamming in, then drifting off */
|
|
7
|
+
.mm.slam { transform:translate(-50%,-50%); text-transform:uppercase; font-weight:900; letter-spacing:.5px; line-height:1; white-space:nowrap; max-width:92%; overflow:hidden; text-overflow:ellipsis; -webkit-text-stroke:2px #000; paint-order:stroke fill; text-shadow:4px 4px 0 #000, -2px -2px 0 #000, 0 0 18px rgba(0,0,0,.6); animation:mmSlam 1.4s cubic-bezier(.2,1.6,.4,1) forwards }
|
|
8
|
+
@keyframes mmSlam { 0% { transform:translate(-50%,-50%) rotate(var(--rot,0deg)) scale(4); opacity:0; filter:blur(6px) } 18% { transform:translate(-50%,-50%) rotate(var(--rot,0deg)) scale(1); opacity:1; filter:none } 80% { transform:translate(-50%,-50%) rotate(var(--rot,0deg)) scale(1.08); opacity:1 } 100% { transform:translate(-50%,-50%) rotate(var(--rot,0deg)) scale(1.6); opacity:0 } }
|
|
9
|
+
|
|
10
|
+
/* top text / bottom text */
|
|
11
|
+
.mm.tb { inset:0; display:flex; flex-direction:column; justify-content:space-between; align-items:center; padding:3% 4%; text-align:center; animation:mmFade 2.2s forwards }
|
|
12
|
+
.mm.tb div { font-size:clamp(28px,6vw,72px); line-height:1; color:#fff; text-transform:uppercase; -webkit-text-stroke:2px #000; paint-order:stroke fill; text-shadow:3px 3px 0 #000; animation:mmPunch .5s cubic-bezier(.2,1.6,.4,1) backwards }
|
|
13
|
+
.mm.tb .bot { animation-delay:.25s }
|
|
14
|
+
@keyframes mmPunch { from { transform:scale(3); opacity:0 } }
|
|
15
|
+
@keyframes mmFade { 0%, 80% { opacity:1 } 100% { opacity:0 } }
|
|
16
|
+
|
|
17
|
+
/* lens flare: a spinning rainbow disc, screen-blended */
|
|
18
|
+
.mm.flare { transform:translate(-50%,-50%); border-radius:50%; mix-blend-mode:screen; opacity:.9; background:conic-gradient(from 0deg,#ff0040,#ff8a00,#ffe600,#3bff6e,#52d8ff,#8a5cff,#ff0040); -webkit-mask:radial-gradient(circle,#000 0,rgba(0,0,0,.6) 25%,transparent 70%); mask:radial-gradient(circle,#000 0,rgba(0,0,0,.6) 25%,transparent 70%); animation:mmFlare 1.6s ease-out forwards }
|
|
19
|
+
@keyframes mmFlare { 0% { transform:translate(-50%,-50%) rotate(0) scale(.2); opacity:0 } 25% { opacity:.95 } 100% { transform:translate(-50%,-50%) rotate(540deg) scale(1.4); opacity:0 } }
|
|
20
|
+
|
|
21
|
+
/* hitmarker */
|
|
22
|
+
.mm.hit { transform:translate(-50%,-50%); color:#fff; font-size:42px; line-height:1; text-shadow:0 0 4px #000, 2px 2px 0 #000; animation:mmHit .7s ease-out forwards }
|
|
23
|
+
@keyframes mmHit { 0% { transform:translate(-50%,-50%) scale(2.2); opacity:1 } 40% { transform:translate(-50%,-50%) scale(1) } 100% { transform:translate(-50%,-50%) scale(.9); opacity:0 } }
|
|
24
|
+
|
|
25
|
+
/* emoji rain, colour flash, star wipe */
|
|
26
|
+
.mm.rain { top:-50px; line-height:1; filter:drop-shadow(0 3px 4px rgba(0,0,0,.5)); animation:mmRain 2.4s ease-in forwards }
|
|
27
|
+
@keyframes mmRain { to { transform:translateY(120vh) rotate(720deg) } }
|
|
28
|
+
.mm.flash { inset:0; animation:mmFlash .11s ease-out forwards }
|
|
29
|
+
@keyframes mmFlash { from { opacity:1 } to { opacity:0 } }
|
|
30
|
+
.mm.wipe { inset:-20%; background:#fff }
|
|
31
|
+
.mm.wipe.star { clip-path:polygon(50% 0%,61% 35%,98% 35%,68% 57%,79% 91%,50% 70%,21% 91%,32% 57%,2% 35%,39% 35%); background:conic-gradient(#ff0040,#ffe600,#3bff6e,#52d8ff,#ff5cf0,#ff0040); animation:mmStar 1.3s ease-in-out forwards }
|
|
32
|
+
@keyframes mmStar { 0% { transform:scale(0) rotate(0) } 45% { transform:scale(3) rotate(180deg) } 55% { transform:scale(3) rotate(200deg) } 100% { transform:scale(0) rotate(360deg) } }
|
|
33
|
+
|
|
34
|
+
/* deal with it, Illuminati, to be continued */
|
|
35
|
+
.mm.shades { transform:translate(-50%,0); line-height:1; filter:drop-shadow(0 4px 6px rgba(0,0,0,.6)); animation:mmShades .6s cubic-bezier(.3,1.5,.5,1) forwards }
|
|
36
|
+
@keyframes mmShades { from { transform:translate(-50%,-120px); opacity:0 } to { transform:translate(-50%,var(--fall,40px)); opacity:1 } }
|
|
37
|
+
.mm.tri { left:50%; top:45%; width:220px; height:190px; margin:-95px 0 0 -110px; animation:mmTri 2.6s ease-in-out forwards }
|
|
38
|
+
.mm.tri i { display:block; width:0; height:0; border-left:110px solid transparent; border-right:110px solid transparent; border-bottom:190px solid #3bff6e; filter:drop-shadow(0 0 24px #3bff6e) }
|
|
39
|
+
.mm.tri span { position:absolute; left:50%; top:110px; transform:translate(-50%,-50%); font-size:60px; line-height:1 }
|
|
40
|
+
@keyframes mmTri { 0% { transform:scale(0) rotate(-180deg); opacity:0 } 30% { transform:scale(1) rotate(0); opacity:1 } 85% { transform:scale(1.1) rotate(20deg); opacity:1 } 100% { transform:scale(3) rotate(40deg); opacity:0 } }
|
|
41
|
+
#center.mm-freeze #stage, #center.mm-freeze #present, #center.mm-freeze #roster { filter:sepia(1) contrast(1.2) saturate(.6) }
|
|
42
|
+
.mm.tbc { left:4%; bottom:9%; font-size:clamp(26px,4vw,48px); line-height:1; font-style:italic; color:#fff; -webkit-text-stroke:1px #000; paint-order:stroke fill; text-shadow:3px 3px 0 #000; animation:mmTbc .5s ease-out }
|
|
43
|
+
@keyframes mmTbc { from { transform:translateX(-120%) } }
|
|
44
|
+
|
|
45
|
+
/* the tiles: punch-in zoom with chromatic aberration, deep-fried pulses */
|
|
46
|
+
body.meme .tile.mm-zoom { animation:mmZoom .5s cubic-bezier(.2,1.5,.4,1); z-index:3 }
|
|
47
|
+
@keyframes mmZoom { 0% { transform:scale(1) } 30% { transform:scale(1.18) rotate(-1.5deg); filter:drop-shadow(-4px 0 0 rgba(255,0,0,.7)) drop-shadow(4px 0 0 rgba(0,180,255,.7)) } 100% { transform:none } }
|
|
48
|
+
body.meme .tile.mm-fry { animation:mmFry .4s steps(2) infinite }
|
|
49
|
+
@keyframes mmFry { 0%, 100% { filter:contrast(1.7) saturate(2.6) brightness(1.1) } 50% { filter:contrast(2.2) saturate(4) brightness(1.2) hue-rotate(15deg) } }
|
|
50
|
+
|
|
51
|
+
/* the whole call in costume: Impact subtitles, a spinning logo, a rainbow glow round the stage */
|
|
52
|
+
body.meme .bubble, body.meme #pcap { font-family:Impact,"Arial Black",Anton,sans-serif; text-transform:uppercase; letter-spacing:.5px; -webkit-text-stroke:1px #000; paint-order:stroke fill; text-shadow:2px 2px 0 #000; font-size:15px }
|
|
53
|
+
body.meme .tile.focus .bubble { font-size:20px }
|
|
54
|
+
body.meme .brand .logo { animation:mmSpin 2s linear infinite }
|
|
55
|
+
@keyframes mmSpin { to { transform:rotate(360deg) } }
|
|
56
|
+
body.meme #center::after { content:""; position:absolute; inset:0; pointer-events:none; z-index:11; animation:mmGlow 4s linear infinite }
|
|
57
|
+
@keyframes mmGlow { 0% { box-shadow:inset 0 0 50px rgba(255,0,64,.45) } 25% { box-shadow:inset 0 0 50px rgba(255,230,0,.45) } 50% { box-shadow:inset 0 0 50px rgba(59,255,110,.45) } 75% { box-shadow:inset 0 0 50px rgba(82,216,255,.45) } 100% { box-shadow:inset 0 0 50px rgba(255,0,64,.45) } }
|
|
58
|
+
|
|
59
|
+
/* The switch is an explicit ask for motion, so reduced motion only removes the full-screen flashes and wipes (strobing). */
|
|
60
|
+
@media (prefers-reduced-motion:reduce) { .mm.flash, .mm.wipe { display:none } }
|
package/public/meme.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// Meme mode: an optional costume over the call.
|
|
2
|
+
// Airhorns, hitmarkers, lens flares, Impact captions, vine booms, sad trombones, star wipes, emoji rain. It never changes
|
|
3
|
+
// what is shown or said, only how loudly. Off by default (⚙ → 🔥 Meme mode), intensity 1–11.
|
|
4
|
+
//
|
|
5
|
+
// Loaded before the page's own script. It reads that script's globals (prefs, S, AUD) at call time only: classic scripts
|
|
6
|
+
// share one global lexical scope, and nothing here runs before the page has started playing. The pure parts (CORE) have no DOM
|
|
7
|
+
// and also run under `node --test` (test/meme.test.js).
|
|
8
|
+
(function (root) {
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
// ---- pure parts: word picking, intensity curve, flash guard, deterministic choice
|
|
12
|
+
const CORE = {
|
|
13
|
+
hash(s) { let h = 2166136261; for (const c of String(s)) h = Math.imul(h ^ c.charCodeAt(0), 16777619); return h >>> 0; },
|
|
14
|
+
pick(list, seed) { return list[(seed >>> 0) % list.length]; },
|
|
15
|
+
// The loud words of a line, for the kinetic captions: words already in capitals first, then the longest; no duplicates.
|
|
16
|
+
words(text, max = 3) {
|
|
17
|
+
const seen = new Set(), out = [];
|
|
18
|
+
const ws = String(text || '').split(/[^\p{L}\p{N}'’]+/u).map((w) => w.replace(/^['’]+|['’]+$/g, '')).filter((w) => w.length >= 3 && /\p{L}/u.test(w));
|
|
19
|
+
const score = (w) => (w === w.toUpperCase() ? 100 : 0) + w.length;
|
|
20
|
+
for (const w of ws.sort((a, b) => score(b) - score(a))) { const k = w.toLowerCase(); if (seen.has(k) || out.length >= max) continue; seen.add(k); out.push(w.toUpperCase()); }
|
|
21
|
+
return out;
|
|
22
|
+
},
|
|
23
|
+
// Intensity 1–11 → how often the optional extras fire, how long each effect rests, how hard the screen shakes, how much rains.
|
|
24
|
+
scale(level) {
|
|
25
|
+
const n = Math.round(+level), l = Number.isFinite(n) ? Math.max(1, Math.min(11, n)) : 11, t = (l - 1) / 10;
|
|
26
|
+
return { level: l, chance: +(0.2 + 0.8 * t).toFixed(3), cooldown: Math.round(6000 - 5400 * t), shake: +(2 + 16 * t).toFixed(1), particles: Math.round(6 + 34 * t) };
|
|
27
|
+
},
|
|
28
|
+
// Full-screen flashes are capped whatever the intensity: at most `max` in any `win` ms.
|
|
29
|
+
FlashGuard: class {
|
|
30
|
+
constructor(max = 3, win = 1000, now = () => Date.now()) { this.max = max; this.win = win; this.now = now; this.times = []; }
|
|
31
|
+
ok() { const t = this.now(); this.times = this.times.filter((x) => t - x < this.win); if (this.times.length >= this.max) return false; this.times.push(t); return true; }
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// ---- the mode itself: sounds (WebAudio synth + a few bundled clips) and visuals (#memeLayer over the stage)
|
|
36
|
+
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
|
37
|
+
const T = (fr, en) => (typeof prefs !== 'undefined' && prefs.lang === 'fr' ? fr : en);
|
|
38
|
+
const nameOf = (id) => (((typeof S !== 'undefined' && S.agents) || {})[id] || {}).name || id;
|
|
39
|
+
|
|
40
|
+
const MEME = {
|
|
41
|
+
CORE, on: false, bus: null, nb: null, clips: new Map(), guard: new CORE.FlashGuard(), last: {}, timer: 0, rm: false,
|
|
42
|
+
CLIPS: { omg: '/sfx/oh-my-god.mp3', scratch1: '/sfx/scratch-1.mp3', scratch2: '/sfx/scratch-2.mp3', scratch3: '/sfx/scratch-3.mp3' },
|
|
43
|
+
PAL: ['#fff', '#ffe600', '#ff3b3b', '#3bff6e', '#52d8ff', '#ff5cf0'],
|
|
44
|
+
WINS: () => ['GG', 'W', 'EZ', 'POGGERS', 'NO SCOPE', 'OH BABY', 'BASED', 'SHEEEESH', 'ABSOLUTE UNIT', 'LET HIM COOK'].concat(T(['BIEN OUEJ', 'C’EST PAS FAUX', 'OKLM', 'MAIS OUI C’EST CLAIR'], [])),
|
|
45
|
+
FAILS: () => ['BRUH', 'TASK FAILED SUCCESSFULLY', 'OH NO', 'EMOTIONAL DAMAGE', 'SKILL ISSUE', 'IT’S OVER', 'RIP', 'F IN THE CHAT', 'WHO DID THIS', 'WORKS ON MY MACHINE'].concat(T(['OH LE BOULET', 'C’EST LA CATA', 'MAIS NON'], [])),
|
|
46
|
+
STALLS: () => ['👁👄👁', T('tout va bien 🔥🐶', 'this is fine 🔥🐶'), 'hello?? 📞', T('⏳ ça vient ça vient', '⏳ any day now'), T('il est en train de CUISINER 🧑🍳', 'bro is COOKING 🧑🍳'), 'AFK?', T('*grillons*', '*crickets*')],
|
|
47
|
+
COMBOS: ['WOMBO COMBO', 'OH BABY A TRIPLE', 'TRIPLE KILL', 'HOLY MOLY', 'MULTI-TRACK DRIFTING'],
|
|
48
|
+
|
|
49
|
+
// ---------- lifecycle ----------
|
|
50
|
+
active() { return !!(this.on && typeof S !== 'undefined' && S.playing && !S.recomputing && !document.hidden); },
|
|
51
|
+
sound() { return !!(this.on && this.bus && typeof AUD !== 'undefined' && AUD.ctx && !AUD.off()); },
|
|
52
|
+
cfg() { return CORE.scale(typeof prefs !== 'undefined' ? prefs.memeLevel : 11); },
|
|
53
|
+
roll(k = 1) { return Math.random() < this.cfg().chance * k; },
|
|
54
|
+
rest(key, mul = 1) { const now = performance.now(); if (now < (this.last[key] || 0)) return false; this.last[key] = now + this.cfg().cooldown * mul; return true; },
|
|
55
|
+
apply() { // called when the switch, the mute state or the audio ownership changes, and once at start-up
|
|
56
|
+
this.on = !!(typeof prefs !== 'undefined' && prefs.meme);
|
|
57
|
+
document.body.classList.toggle('meme', this.on);
|
|
58
|
+
this.rm = !!(root.matchMedia && root.matchMedia('(prefers-reduced-motion: reduce)').matches); // the switch asks for motion; reduced motion only turns off flashes and wipes
|
|
59
|
+
this.ensureBus(); if (this.bus) this.bus.gain.value = this.sound() ? (prefs.memeVol ?? 0.8) : 0;
|
|
60
|
+
if (this.on && !this.timer) this.timer = setInterval(() => this.tick(), 4000);
|
|
61
|
+
if (!this.on) { clearInterval(this.timer); this.timer = 0; const l = document.getElementById('memeLayer'); if (l) l.innerHTML = ''; document.querySelectorAll('.mm-fry, .mm-zoom').forEach((t) => t.classList.remove('mm-fry', 'mm-zoom')); }
|
|
62
|
+
},
|
|
63
|
+
ensureBus() { if (this.bus || typeof AUD === 'undefined' || !AUD.ctx) return; const c = AUD.ctx; this.bus = c.createGain(); this.bus.connect(c.destination); if (AUD.rec) this.bus.connect(AUD.rec); }, // into the export mix too
|
|
64
|
+
fire(kind, info = {}) { if (!this.active()) return; try { this.ensureBus(); const h = this.ON[kind]; if (h) h.call(this, info); } catch {} },
|
|
65
|
+
tick() { // stalled agents (the tiles the page already marks .slow / .quiet) get crickets
|
|
66
|
+
if (!this.active()) return; const stuck = [...document.querySelectorAll('#stage .tile.quiet, #stage .tile.slow')].filter((t) => t.style.display !== 'none');
|
|
67
|
+
if (!stuck.length || !this.roll(0.5) || !this.rest('stall', 4)) return;
|
|
68
|
+
const t = stuck[Math.floor(Math.random() * stuck.length)], id = t.id.slice(2), r = this.rect(id); this.crickets();
|
|
69
|
+
this.slam(CORE.pick(this.STALLS(), CORE.hash(id + ((Date.now() / 10000) | 0))), { size: 30, left: r && r.cx + 'px', top: r && r.y + 'px', ms: 2500 });
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
// ---------- what each event does ----------
|
|
73
|
+
ON: {
|
|
74
|
+
talk(i) { if (i.id === 'user') return this.ON.user.call(this, i); if (!this.rest('talk', 0.5)) return; this.zoom(i.id); if (this.roll()) this.kinetic(i.text); if (this.cfg().level >= 9 && this.roll(0.6)) this.fry(i.id); },
|
|
75
|
+
interrupt() { this.scratched = performance.now(); this.clip('scratch' + (1 + Math.floor(Math.random() * 3)), () => this.scratch()); }, // the human cut a line being read: a real record scratch
|
|
76
|
+
user() { if (!this.rest('user', 2)) return; if (performance.now() - (this.scratched || 0) > 2500) this.scratch(); this.topBottom(T('L’HUMAIN EST', 'THE HUMAN HAS'), T('ENTRÉ DANS LE CHAT', 'ENTERED THE CHAT')); this.flash('rgba(255,255,255,.5)'); if (this.roll()) this.rain(['👀', '😳', '🫡'], Math.round(this.cfg().particles / 2)); },
|
|
77
|
+
order(i) {
|
|
78
|
+
if (!this.rest('order')) return; this.airhorn(1); const r = this.rect(i.to); if (r) this.flare(r.cx, r.cy, Math.max(200, r.w * 1.2));
|
|
79
|
+
this.slam(T(`${nameOf(i.to)} DÉPLOYÉ`, `DEPLOYING ${nameOf(i.to)}`), { size: 56, color: '#ffe600' }); if (this.roll(0.5)) this.flash('rgba(255,230,0,.35)');
|
|
80
|
+
},
|
|
81
|
+
report(i) {
|
|
82
|
+
if (!this.rest('report')) return; this.hitmarker(); this.sparkle(); const r = this.rect(i.from);
|
|
83
|
+
if (r) for (let k = 0; k < 3; k++) setTimeout(() => this.cross(r.x + Math.random() * r.w, r.y + Math.random() * r.h), k * 90);
|
|
84
|
+
const xp = 100 * (1 + Math.floor(Math.random() * 9)); this.slam(`+${xp} XP`, { size: 40, color: '#3bff6e', left: r && r.cx + 'px', top: r && r.y + 'px' });
|
|
85
|
+
if (this.roll(0.7)) setTimeout(() => this.active() && this.slam(CORE.pick(this.WINS(), CORE.hash(i.from + xp)), { size: 64 }), 250);
|
|
86
|
+
if (this.roll(0.45)) { this.airhorn(3); this.flash('rgba(255,255,255,.35)'); }
|
|
87
|
+
if (this.roll(0.5)) setTimeout(() => this.active() && this.slam('STONKS 📈', { size: 44, color: '#52d8ff', top: '8%' }), 600);
|
|
88
|
+
},
|
|
89
|
+
tool(i) {
|
|
90
|
+
if (!this.roll(0.08) || !this.rest('tool', 0.3)) return; this.hitmarker(); const r = this.rect(i.id); if (!r) return;
|
|
91
|
+
this.cross(r.cx + (Math.random() - 0.5) * r.w * 0.6, r.cy + (Math.random() - 0.5) * r.h * 0.6); this.slam('+10', { size: 22, left: r.cx + 'px', top: r.cy + 'px', ms: 800 });
|
|
92
|
+
},
|
|
93
|
+
fail(i) {
|
|
94
|
+
if (!this.rest('fail', 1.5)) return; this.shake(1.4); this.flash('rgba(255,0,0,.45)');
|
|
95
|
+
if (Math.random() < 0.4) this.clip('omg', () => this.boom()); else { this.boom(); setTimeout(() => this.active() && (Math.random() < 0.5 ? this.trombone() : this.bruh()), 450); }
|
|
96
|
+
this.slam(CORE.pick(this.FAILS(), CORE.hash(i.id + Date.now())), { size: 70, color: '#ff3b3b' });
|
|
97
|
+
if (this.roll()) this.rain(['💀', '🤡', '🔥', '😭'], Math.round(this.cfg().particles / 2)); const r = this.rect(i.id); if (r && this.roll(0.5)) this.slam('💀', { size: 60, left: r.cx + 'px', top: r.cy + 'px' });
|
|
98
|
+
},
|
|
99
|
+
leave(i) {
|
|
100
|
+
if (!this.rest('leave')) return;
|
|
101
|
+
if (Math.random() < 0.5) { this.yeet(); this.fling(i.id); const r = this.rect(i.id); this.slam('YEET', { size: 64, color: '#ffe600', left: r && r.cx + 'px', top: r && r.cy + 'px' }); } else { this.sting(); this.freeze(); }
|
|
102
|
+
},
|
|
103
|
+
win() {
|
|
104
|
+
if (!this.rest('win', 4)) return; this.airhorn(3); setTimeout(() => this.active() && this.badumtss(), 1300); setTimeout(() => this.active() && this.sparkle(), 1800);
|
|
105
|
+
this.rain(['🌮', '🥤', '🔥', '💯', '😂', '🏆']); const l = this.layer().getBoundingClientRect(); for (let k = 0; k < 4; k++) setTimeout(() => this.flare(Math.random() * l.width, Math.random() * l.height, 220 + Math.random() * 220), k * 250);
|
|
106
|
+
this.triangle(); this.slam(T('MAMAN VIENS VOIR', 'MOM GET THE CAMERA'), { size: 64, color: '#ffe600', top: '20%' }); setTimeout(() => this.shades('main'), 900); this.flash('rgba(255,255,255,.5)'); if (this.roll(0.6)) this.spin();
|
|
107
|
+
},
|
|
108
|
+
card() { if (!this.rest('card', 2)) return; this.sting(); this.wipe('star'); },
|
|
109
|
+
combo(i) {
|
|
110
|
+
if (!this.rest('combo', 3)) return; this.wub(); setTimeout(() => this.active() && this.ohh(), 300); this.shake(0.8);
|
|
111
|
+
this.slam(CORE.pick(this.COMBOS, CORE.hash(String(Date.now()))), { size: 80, color: '#ff5cf0', top: '30%' });
|
|
112
|
+
(i.ids || []).forEach((id, k) => { const r = this.rect(id); if (r) setTimeout(() => this.flare(r.cx, r.cy, r.w * 1.3), k * 150); }); if (this.roll(0.6)) this.spin();
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
// ---------- sounds: synth patches on the meme bus, plus bundled clips ----------
|
|
117
|
+
node(type, f) { const o = AUD.ctx.createOscillator(); o.type = type; o.frequency.value = f; return o; },
|
|
118
|
+
env(t, a, h, r, v) { const g = AUD.ctx.createGain(); g.gain.setValueAtTime(0.0001, t); g.gain.exponentialRampToValueAtTime(v, t + a); g.gain.setValueAtTime(v, t + a + h); g.gain.exponentialRampToValueAtTime(0.0001, t + a + h + r); return g; },
|
|
119
|
+
dist(k = 40) { const ws = AUD.ctx.createWaveShaper(), n = 256, curve = new Float32Array(n); for (let i = 0; i < n; i++) { const x = (i / (n - 1)) * 2 - 1; curve[i] = ((3 + k) * x * 20 * (Math.PI / 180)) / (Math.PI + k * Math.abs(x)); } ws.curve = curve; return ws; },
|
|
120
|
+
noise() { const c = AUD.ctx; if (!this.nb) { this.nb = c.createBuffer(1, c.sampleRate * 2, c.sampleRate); const d = this.nb.getChannelData(0); for (let i = 0; i < d.length; i++) d[i] = Math.random() * 2 - 1; } const s = c.createBufferSource(); s.buffer = this.nb; s.loop = true; return s; },
|
|
121
|
+
filt(type, f, q = 1) { const b = AUD.ctx.createBiquadFilter(); b.type = type; b.frequency.value = f; b.Q.value = q; return b; },
|
|
122
|
+
airhorn(n = 1) { // three detuned saws and a sub square, clipped hard, pitch sliding up into the note; the last of three holds
|
|
123
|
+
if (!this.sound()) return; const c = AUD.ctx;
|
|
124
|
+
for (let k = 0; k < n; k++) {
|
|
125
|
+
const t = c.currentTime + k * 0.32, len = n > 1 && k === n - 1 ? 0.85 : 0.28, g = this.env(t, 0.015, len, 0.15, 0.35), d = this.dist(50), lp = this.filt('lowpass', 2600, 2);
|
|
126
|
+
[[466, 'sawtooth'], [468.5, 'sawtooth'], [463.5, 'sawtooth'], [233, 'square']].forEach(([f, ty]) => { const o = this.node(ty, f * 0.88); o.frequency.exponentialRampToValueAtTime(f, t + 0.06); o.connect(d); o.start(t); o.stop(t + len + 0.2); });
|
|
127
|
+
d.connect(lp).connect(g).connect(this.bus);
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
boom() { // vine boom: a sine dropping from 120 to 32 Hz with a thud of low noise
|
|
131
|
+
if (!this.sound()) return; const c = AUD.ctx, t = c.currentTime, o = this.node('sine', 120), g = this.env(t, 0.005, 0.05, 1.3, 0.9); o.frequency.exponentialRampToValueAtTime(32, t + 0.5); o.connect(this.dist(8)).connect(g).connect(this.bus); o.start(t); o.stop(t + 1.5);
|
|
132
|
+
const n = this.noise(), ng = this.env(t, 0.003, 0.03, 0.25, 0.5); n.connect(this.filt('lowpass', 220)).connect(ng).connect(this.bus); n.start(t); n.stop(t + 0.4);
|
|
133
|
+
},
|
|
134
|
+
hitmarker() { // two clicks of bright noise and a tink
|
|
135
|
+
if (!this.sound()) return; const c = AUD.ctx, t = c.currentTime;
|
|
136
|
+
for (let k = 0; k < 2; k++) { const at = t + k * 0.045, n = this.noise(), g = this.env(at, 0.002, 0.01, 0.05, 0.35); n.connect(this.filt('highpass', 3800)).connect(g).connect(this.bus); n.start(at); n.stop(at + 0.08); }
|
|
137
|
+
const o = this.node('sine', 2650), g = this.env(t, 0.002, 0.02, 0.07, 0.18); o.connect(g).connect(this.bus); o.start(t); o.stop(t + 0.12);
|
|
138
|
+
},
|
|
139
|
+
trombone() { // sad trombone: four notes, each sliding down with a closing wah
|
|
140
|
+
if (!this.sound()) return; const c = AUD.ctx; let t = c.currentTime;
|
|
141
|
+
[[233, 0.38], [220, 0.38], [208, 0.38], [196, 1.0]].forEach(([f, d]) => {
|
|
142
|
+
const o = this.node('sawtooth', f), bp = this.filt('bandpass', 900, 2.5); bp.frequency.exponentialRampToValueAtTime(380, t + d); o.frequency.setValueAtTime(f, t); o.frequency.exponentialRampToValueAtTime(f * 0.94, t + d);
|
|
143
|
+
const g = this.env(t, 0.04, d - 0.12, 0.1, 0.3); o.connect(bp).connect(g).connect(this.bus); o.start(t); o.stop(t + d + 0.1); t += d + 0.05;
|
|
144
|
+
});
|
|
145
|
+
},
|
|
146
|
+
bruh() { // one low vowel: two saws through a formant sweep, pitch sagging
|
|
147
|
+
if (!this.sound()) return; const c = AUD.ctx, t = c.currentTime, bp = this.filt('bandpass', 350, 3); bp.frequency.linearRampToValueAtTime(950, t + 0.12); bp.frequency.linearRampToValueAtTime(260, t + 0.5);
|
|
148
|
+
[115, 116.5].forEach((f) => { const o = this.node('sawtooth', f); o.frequency.linearRampToValueAtTime(f * 0.74, t + 0.55); o.connect(bp); o.start(t); o.stop(t + 0.65); });
|
|
149
|
+
bp.connect(this.env(t, 0.03, 0.35, 0.2, 0.45)).connect(this.bus);
|
|
150
|
+
},
|
|
151
|
+
sting() { // dramatic "dun dun dunnn": three minor stabs, the last one held
|
|
152
|
+
if (!this.sound()) return; const c = AUD.ctx, t0 = c.currentTime;
|
|
153
|
+
[[0, 0.32, [196, 233, 294]], [0.42, 0.32, [185, 220, 277]], [0.84, 1.6, [147, 175, 220]]].forEach(([at, d, fs]) => {
|
|
154
|
+
const t = t0 + at, lp = this.filt('lowpass', 1200), g = this.env(t, 0.02, d, 0.3, 0.22);
|
|
155
|
+
fs.forEach((f) => [this.node('sawtooth', f), this.node('square', f / 2)].forEach((o) => { o.connect(lp); o.start(t); o.stop(t + d + 0.4); })); lp.connect(g).connect(this.bus);
|
|
156
|
+
});
|
|
157
|
+
},
|
|
158
|
+
scratch() { // record scratch: resonant noise swept down then back up (stands in when the clip cannot be loaded)
|
|
159
|
+
if (!this.sound()) return; const c = AUD.ctx, t = c.currentTime, n = this.noise(), bp = this.filt('bandpass', 2400, 6); bp.frequency.exponentialRampToValueAtTime(300, t + 0.18); bp.frequency.exponentialRampToValueAtTime(1800, t + 0.36);
|
|
160
|
+
n.connect(bp).connect(this.env(t, 0.01, 0.3, 0.08, 0.5)).connect(this.bus); n.start(t); n.stop(t + 0.45);
|
|
161
|
+
},
|
|
162
|
+
badumtss() { // kick, snare, crash
|
|
163
|
+
if (!this.sound()) return; const c = AUD.ctx, t = c.currentTime;
|
|
164
|
+
const kick = (at) => { const o = this.node('sine', 160), g = this.env(at, 0.003, 0.03, 0.22, 0.8); o.frequency.exponentialRampToValueAtTime(48, at + 0.18); o.connect(g).connect(this.bus); o.start(at); o.stop(at + 0.3); };
|
|
165
|
+
const snare = (at) => { const n = this.noise(), g = this.env(at, 0.002, 0.03, 0.16, 0.5); n.connect(this.filt('highpass', 1100)).connect(g).connect(this.bus); n.start(at); n.stop(at + 0.25); const o = this.node('triangle', 190), og = this.env(at, 0.002, 0.02, 0.1, 0.4); o.connect(og).connect(this.bus); o.start(at); o.stop(at + 0.15); };
|
|
166
|
+
const crash = (at) => { const n = this.noise(), g = this.env(at, 0.003, 0.05, 1.4, 0.35); n.connect(this.filt('highpass', 5000)).connect(g).connect(this.bus); n.start(at); n.stop(at + 1.6); };
|
|
167
|
+
kick(t); snare(t + 0.17); crash(t + 0.36);
|
|
168
|
+
},
|
|
169
|
+
sparkle() { if (!this.sound()) return; const t = AUD.ctx.currentTime; [880, 1109, 1319, 1760, 2217].forEach((f, i) => { const at = t + i * 0.06, o = this.node('sine', f), g = this.env(at, 0.005, 0.04, 0.5, 0.14); o.connect(g).connect(this.bus); o.start(at); o.stop(at + 0.6); }); },
|
|
170
|
+
crickets() { // three chirps: a high sine chopped at 38 Hz
|
|
171
|
+
if (!this.sound()) return; const c = AUD.ctx, t = c.currentTime;
|
|
172
|
+
for (let k = 0; k < 3; k++) { const at = t + k * 0.5, o = this.node('sine', 4300), g = this.env(at, 0.01, 0.12, 0.05, 0.08), lfo = this.node('square', 38), lg = c.createGain(); lg.gain.value = 0.08; lfo.connect(lg).connect(g.gain); o.connect(g).connect(this.bus); o.start(at); lfo.start(at); o.stop(at + 0.2); lfo.stop(at + 0.2); }
|
|
173
|
+
},
|
|
174
|
+
ohh() { // a crowd going "ohhh": seven detuned saws sagging through a closing formant
|
|
175
|
+
if (!this.sound()) return; const c = AUD.ctx, t = c.currentTime, bp = this.filt('bandpass', 700, 1.2); bp.frequency.exponentialRampToValueAtTime(380, t + 1.1);
|
|
176
|
+
for (let k = 0; k < 7; k++) { const f = 200 + k * 23 + Math.random() * 10, o = this.node('sawtooth', f); o.frequency.linearRampToValueAtTime(f * 0.8, t + 1.1); o.connect(bp); o.start(t); o.stop(t + 1.3); }
|
|
177
|
+
bp.connect(this.env(t, 0.18, 0.4, 0.6, 0.25)).connect(this.bus);
|
|
178
|
+
},
|
|
179
|
+
wub() { // dubstep: a sub saw under a resonant low-pass that an accelerating LFO opens and closes
|
|
180
|
+
if (!this.sound()) return; const c = AUD.ctx, t = c.currentTime, lp = this.filt('lowpass', 400, 12), lfo = this.node('sine', 4), lg = c.createGain(); lg.gain.value = 350; lfo.frequency.linearRampToValueAtTime(12, t + 1.2); lfo.connect(lg).connect(lp.frequency);
|
|
181
|
+
[this.node('sawtooth', 55), this.node('square', 55.5)].forEach((o) => { o.connect(lp); o.start(t); o.stop(t + 1.5); }); lp.connect(this.dist(15)).connect(this.env(t, 0.03, 1.1, 0.25, 0.4)).connect(this.bus); lfo.start(t); lfo.stop(t + 1.5);
|
|
182
|
+
},
|
|
183
|
+
yeet() { // a rising whoosh
|
|
184
|
+
if (!this.sound()) return; const c = AUD.ctx, t = c.currentTime, n = this.noise(), bp = this.filt('bandpass', 400, 1); bp.frequency.exponentialRampToValueAtTime(6500, t + 0.35); n.connect(bp).connect(this.env(t, 0.02, 0.2, 0.15, 0.35)).connect(this.bus); n.start(t); n.stop(t + 0.45);
|
|
185
|
+
const o = this.node('sine', 300); o.frequency.exponentialRampToValueAtTime(1400, t + 0.35); o.connect(this.env(t, 0.02, 0.2, 0.1, 0.12)).connect(this.bus); o.start(t); o.stop(t + 0.4);
|
|
186
|
+
},
|
|
187
|
+
async clip(name, fallback) { // a bundled clip, decoded once, played on the meme bus; the synth stands in if it cannot be loaded
|
|
188
|
+
if (!this.sound()) return;
|
|
189
|
+
try {
|
|
190
|
+
if (!this.clips.has(name)) this.clips.set(name, fetch(this.CLIPS[name]).then((r) => { if (!r.ok) throw new Error(String(r.status)); return r.arrayBuffer(); }).then((ab) => AUD.ctx.decodeAudioData(ab)));
|
|
191
|
+
const buf = await this.clips.get(name); if (!this.sound()) return; const s = AUD.ctx.createBufferSource(); s.buffer = buf; s.connect(this.bus); s.start();
|
|
192
|
+
} catch { this.clips.delete(name); if (fallback) fallback(); }
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
// ---------- visuals: everything lands in #memeLayer, nothing there is clickable ----------
|
|
196
|
+
layer() { let l = document.getElementById('memeLayer'); if (!l) { l = document.createElement('div'); l.id = 'memeLayer'; (document.getElementById('center') || document.body).appendChild(l); } return l; },
|
|
197
|
+
el(cls, html, style) { const d = document.createElement('div'); d.className = 'mm ' + cls; d.innerHTML = html; for (const k in style || {}) if (style[k] != null && style[k] !== false) d.style.setProperty(k.startsWith('--') ? k : k.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase()), String(style[k])); this.layer().appendChild(d); return d; },
|
|
198
|
+
gone(d, ms) { setTimeout(() => d.remove(), ms); },
|
|
199
|
+
rect(id) { // a tile's box in layer coordinates, or null when it is not on screen
|
|
200
|
+
const t = document.getElementById('t-' + id); if (!t || t.style.display === 'none' || document.body.classList.contains('tree') || document.body.classList.contains('board')) return null;
|
|
201
|
+
const r = t.getBoundingClientRect(), l = this.layer().getBoundingClientRect(); if (!r.width) return null;
|
|
202
|
+
return { x: r.left - l.left, y: r.top - l.top, w: r.width, h: r.height, cx: r.left - l.left + r.width / 2, cy: r.top - l.top + r.height / 2 };
|
|
203
|
+
},
|
|
204
|
+
slam(text, o = {}) { const d = this.el('slam', esc(text), { left: o.left || 15 + Math.random() * 70 + '%', top: o.top || 15 + Math.random() * 60 + '%', fontSize: (o.size || 48) + 'px', color: o.color || '#fff', '--rot': (o.rot ?? Math.random() * 24 - 12) + 'deg' }); this.gone(d, o.ms || 1400); return d; },
|
|
205
|
+
kinetic(text) { const n = this.cfg().level >= 8 ? 3 : this.cfg().level >= 4 ? 2 : 1; CORE.words(text, n).forEach((w, i) => setTimeout(() => this.active() && this.slam(w, { size: 34 + Math.random() * 40, color: this.PAL[i % this.PAL.length] }), 350 + i * 420)); },
|
|
206
|
+
topBottom(top, bottom, ms = 2200) { this.gone(this.el('tb', `<div class="top">${esc(top)}</div><div class="bot">${esc(bottom || '')}</div>`), ms); },
|
|
207
|
+
flare(x, y, size = 260, ms = 1600) { this.gone(this.el('flare', '', { left: x + 'px', top: y + 'px', width: size + 'px', height: size + 'px', animationDuration: ms + 'ms' }), ms); },
|
|
208
|
+
cross(x, y) { this.gone(this.el('hit', '✕', { left: x + 'px', top: y + 'px' }), 700); },
|
|
209
|
+
rain(emojis, n) { n = n || this.cfg().particles; for (let i = 0; i < n; i++) this.gone(this.el('rain', emojis[i % emojis.length], { left: Math.random() * 100 + '%', animationDelay: Math.random() * 0.8 + 's', animationDuration: 1.6 + Math.random() * 1.6 + 's', fontSize: 18 + Math.random() * 26 + 'px' }), 4200); },
|
|
210
|
+
shake(mul = 1) {
|
|
211
|
+
const amp = this.cfg().shake * mul, c = document.getElementById('center'); if (!c || !c.animate) return;
|
|
212
|
+
const kf = []; for (let i = 0; i < 10; i++) kf.push({ transform: `translate(${(Math.random() * 2 - 1) * amp}px,${(Math.random() * 2 - 1) * amp}px) rotate(${((Math.random() * 2 - 1) * amp) / 6}deg)` }); kf.push({ transform: 'none' });
|
|
213
|
+
c.animate(kf, { duration: 450, easing: 'ease-out' });
|
|
214
|
+
},
|
|
215
|
+
flash(color = '#fff', ms = 110) { if (this.rm || !this.guard.ok()) return; this.gone(this.el('flash', '', { background: color, animationDuration: ms + 'ms' }), ms + 20); }, // never under reduced motion, and at most 3 a second
|
|
216
|
+
zoom(id) { const t = document.getElementById('t-' + id); if (!t) return; t.classList.remove('mm-zoom'); void t.offsetWidth; t.classList.add('mm-zoom'); setTimeout(() => t.classList.remove('mm-zoom'), 520); },
|
|
217
|
+
fry(id, ms = 1800) { const t = document.getElementById('t-' + id); if (!t) return; t.classList.add('mm-fry'); setTimeout(() => t.classList.remove('mm-fry'), ms); },
|
|
218
|
+
wipe(kind = 'star') { if (this.rm) return; this.gone(this.el('wipe ' + kind, ''), 1300); },
|
|
219
|
+
spin() { const s = document.getElementById('stage'); if (!s || !s.animate) return; s.animate([{ transform: 'perspective(900px) rotateY(0)' }, { transform: 'perspective(900px) rotateY(360deg)' }], { duration: 1100, easing: 'cubic-bezier(.6,0,.3,1)' }); },
|
|
220
|
+
shades(id, ms = 3500) { const r = this.rect(id); if (!r) return; this.gone(this.el('shades', '🕶️', { left: r.cx + 'px', top: r.y + 'px', fontSize: Math.max(30, r.w * 0.28) + 'px', '--fall': r.h * 0.3 + 'px' }), ms); },
|
|
221
|
+
triangle(ms = 2600) { this.gone(this.el('tri', '<i></i><span>👁️</span>'), ms); },
|
|
222
|
+
freeze(ms = 2600) { // "To be continued": sepia over the stage and the arrow banner
|
|
223
|
+
const c = document.getElementById('center'); if (!c) return; c.classList.add('mm-freeze'); const d = this.el('tbc', `<span>${esc(T('À suivre', 'To Be Continued'))}</span> ➡︎`);
|
|
224
|
+
setTimeout(() => { c.classList.remove('mm-freeze'); d.remove(); }, ms);
|
|
225
|
+
},
|
|
226
|
+
fling(id) { // the leaving tile is thrown off-screen, then let go
|
|
227
|
+
const t = document.getElementById('t-' + id); if (!t || !t.animate) return; const dir = Math.random() < 0.5 ? -1 : 1;
|
|
228
|
+
const a = t.animate([{ transform: 'none', opacity: 1 }, { transform: `translate(${dir * 120}vw,-40vh) rotate(${dir * 540}deg) scale(.4)`, opacity: 0.6 }], { duration: 900, easing: 'cubic-bezier(.2,.6,.5,1)', fill: 'forwards' });
|
|
229
|
+
setTimeout(() => a.cancel(), 1500);
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = CORE;
|
|
234
|
+
if (typeof root.document !== 'undefined') root.MEME = MEME;
|
|
235
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|