|
1 | 1 | (() => { |
2 | | - const patternCharacters = "0123456789ABCDEF"; |
| 2 | + const PATTERN_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; |
| 3 | + const PATTERN_LENGTH = 900; |
| 4 | + const SCRAMBLE_MS = 140; |
3 | 5 |
|
4 | 6 | const loadRecipes = (url) => |
5 | 7 | fetch(url).then((response) => { |
6 | 8 | if (!response.ok) throw new Error(`HTTP ${response.status}`); |
7 | 9 | return response.json(); |
8 | 10 | }); |
9 | 11 |
|
10 | | - const initPatterns = (root) => { |
11 | | - root.querySelectorAll("[data-cookbook-pattern]").forEach((pattern, patternIndex) => { |
12 | | - if (pattern.dataset.patternReady) return; |
13 | | - pattern.dataset.patternReady = "true"; |
14 | | - let value = ""; |
15 | | - for (let index = 0; index < 1800; index += 1) { |
16 | | - const characterIndex = (index * 7 + patternIndex * 11) % patternCharacters.length; |
17 | | - value += patternCharacters.charAt(characterIndex); |
18 | | - if (index % 4 === 3 && index % 32 !== 31) value += " "; |
19 | | - if (index % 32 === 31) value += "\n"; |
20 | | - } |
21 | | - pattern.textContent = value; |
| 12 | + const generatePattern = (length) => { |
| 13 | + const chars = new Array(length); |
| 14 | + for (let index = 0; index < length; index += 1) { |
| 15 | + chars[index] = PATTERN_CHARS.charAt((Math.random() * PATTERN_CHARS.length) | 0); |
| 16 | + } |
| 17 | + return chars.join(""); |
| 18 | + }; |
| 19 | + |
| 20 | + const motionQuery = () => |
| 21 | + window.matchMedia("(hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference)"); |
| 22 | + |
| 23 | + const initEvervault = (root) => { |
| 24 | + root.querySelectorAll("[data-evervault]").forEach((visual) => { |
| 25 | + if (visual.dataset.evervaultReady) return; |
| 26 | + visual.dataset.evervaultReady = "true"; |
| 27 | + const noise = visual.querySelector("[data-cookbook-pattern]"); |
| 28 | + if (!noise) return; |
| 29 | + |
| 30 | + const query = motionQuery(); |
| 31 | + if (!query.matches) return; |
| 32 | + |
| 33 | + let rect = null; |
| 34 | + let pointerX = 0; |
| 35 | + let pointerY = 0; |
| 36 | + let frame = 0; |
| 37 | + let lastScramble = 0; |
| 38 | + let patternReady = false; |
| 39 | + |
| 40 | + const paint = (scramble) => { |
| 41 | + visual.style.setProperty("--mouse-x", `${pointerX}px`); |
| 42 | + visual.style.setProperty("--mouse-y", `${pointerY}px`); |
| 43 | + if (scramble) noise.textContent = generatePattern(PATTERN_LENGTH); |
| 44 | + frame = 0; |
| 45 | + }; |
| 46 | + |
| 47 | + const onEnter = () => { |
| 48 | + rect = visual.getBoundingClientRect(); |
| 49 | + if (!patternReady) { |
| 50 | + noise.textContent = generatePattern(PATTERN_LENGTH); |
| 51 | + patternReady = true; |
| 52 | + } |
| 53 | + }; |
| 54 | + const onMove = (event) => { |
| 55 | + if (!rect) rect = visual.getBoundingClientRect(); |
| 56 | + pointerX = event.clientX - rect.left; |
| 57 | + pointerY = event.clientY - rect.top; |
| 58 | + if (frame) return; |
| 59 | + frame = window.requestAnimationFrame(() => { |
| 60 | + const now = performance.now(); |
| 61 | + const scramble = now - lastScramble >= SCRAMBLE_MS; |
| 62 | + if (scramble) lastScramble = now; |
| 63 | + paint(scramble); |
| 64 | + }); |
| 65 | + }; |
| 66 | + const onLeave = () => { |
| 67 | + if (frame) window.cancelAnimationFrame(frame); |
| 68 | + frame = 0; |
| 69 | + rect = null; |
| 70 | + pointerX = 0; |
| 71 | + pointerY = 0; |
| 72 | + visual.style.setProperty("--mouse-x", "50%"); |
| 73 | + visual.style.setProperty("--mouse-y", "50%"); |
| 74 | + }; |
| 75 | + visual.addEventListener("mouseenter", onEnter); |
| 76 | + visual.addEventListener("mousemove", onMove); |
| 77 | + visual.addEventListener("mouseleave", onLeave); |
| 78 | + }); |
| 79 | + }; |
| 80 | + |
| 81 | + let familyPopstate = null; |
| 82 | + const bindFamilyPopstate = () => { |
| 83 | + if (bindFamilyPopstate.bound) return; |
| 84 | + bindFamilyPopstate.bound = true; |
| 85 | + window.addEventListener("popstate", () => { |
| 86 | + if (typeof familyPopstate === "function") familyPopstate(); |
22 | 87 | }); |
23 | 88 | }; |
24 | 89 |
|
|
137 | 202 |
|
138 | 203 | compactLifecycle(root); |
139 | 204 |
|
140 | | - const builderHeading = root.querySelector(".cookbook-builder__intro h2"); |
141 | | - const builderIntro = root.querySelector(".cookbook-builder__intro > p"); |
142 | | - if (builderHeading) builderHeading.textContent = "Pick a recipe and runtime"; |
143 | | - if (builderIntro) { |
144 | | - builderIntro.textContent = |
145 | | - "Start with the result you want, then choose one of the runtimes FastVideo actually maintains for it."; |
146 | | - } |
147 | | - |
148 | | - const selectionLabels = root.querySelectorAll(".cookbook-selection-row__label"); |
149 | | - if (selectionLabels[0]) { |
150 | | - selectionLabels[0].querySelector("strong").textContent = "Recipe"; |
151 | | - selectionLabels[0].querySelector("span").textContent = "Task and checkpoint"; |
152 | | - } |
153 | | - if (selectionLabels[1]) { |
154 | | - selectionLabels[1].querySelector("strong").textContent = "Runtime"; |
155 | | - selectionLabels[1].querySelector("span").textContent = "Maintained paths only"; |
156 | | - } |
157 | | - |
158 | 205 | const modelOptions = root.querySelector("[data-cookbook-model-options]"); |
159 | 206 | const hardwareOptions = root.querySelector("[data-cookbook-hardware-options]"); |
160 | 207 | const description = root.querySelector("[data-cookbook-description]"); |
161 | | - const hardwareNote = root.querySelector(".cookbook-hardware-note"); |
162 | 208 | const label = root.querySelector("[data-cookbook-label]"); |
163 | 209 | const model = root.querySelector("[data-cookbook-model]"); |
164 | 210 | const task = root.querySelector("[data-cookbook-task]"); |
|
178 | 224 | modelOptions.setAttribute("aria-label", "Recipe"); |
179 | 225 | hardwareOptions.setAttribute("aria-label", "Runtime"); |
180 | 226 |
|
181 | | - if (hardwareNote) { |
182 | | - hardwareNote.textContent = "Exact device and memory details appear only when a recorded run supports them."; |
183 | | - } |
184 | | - const runtimeFactLabel = hardwareValue?.closest("div")?.querySelector("dt"); |
185 | | - if (runtimeFactLabel) runtimeFactLabel.textContent = "Hardware"; |
186 | | - |
187 | 227 | let recipes; |
188 | 228 | try { |
189 | 229 | ({ recipes } = await loadRecipes(root.dataset.recipes)); |
|
192 | 232 | console.error("Failed to load FastVideo cookbook recipes", error); |
193 | 233 | return; |
194 | 234 | } |
| 235 | + if (!root.isConnected) return; |
195 | 236 |
|
196 | 237 | const familyRecipes = recipes.filter((recipe) => recipe.family === family); |
197 | 238 | if (!familyRecipes.length) return; |
|
225 | 266 | const defaultRecipeId = familyRecipes[0].id; |
226 | 267 | let selectedRecipeId = requestedRecipe && byId.has(requestedRecipe) ? requestedRecipe : defaultRecipeId; |
227 | 268 | let selectedGroupId = groupIdFor(byId.get(selectedRecipeId)); |
| 269 | + let renderedRuntimeGroup = null; |
228 | 270 |
|
229 | 271 | const renderRuntimeOptions = () => { |
230 | 272 | const groupRecipes = groups.get(selectedGroupId) || []; |
|
255 | 297 | } |
256 | 298 |
|
257 | 299 | const render = ({ groupChanged = false, historyMode = "replace" } = {}) => { |
| 300 | + if (!root.isConnected) return; |
258 | 301 | if (!byId.has(selectedRecipeId)) selectedRecipeId = defaultRecipeId; |
259 | 302 | let recipe = byId.get(selectedRecipeId); |
260 | 303 | if (groupChanged || groupIdFor(recipe) !== selectedGroupId) { |
|
265 | 308 | } |
266 | 309 |
|
267 | 310 | selectedGroupId = groupIdFor(recipe); |
268 | | - renderRuntimeOptions(); |
| 311 | + if (renderedRuntimeGroup !== selectedGroupId) { |
| 312 | + renderRuntimeOptions(); |
| 313 | + renderedRuntimeGroup = selectedGroupId; |
| 314 | + } |
269 | 315 | const runtime = runtimeFor(recipe); |
270 | 316 |
|
271 | 317 | modelOptions.querySelectorAll("button").forEach((option) => { |
|
337 | 383 |
|
338 | 384 | render(); |
339 | 385 |
|
340 | | - window.addEventListener("popstate", () => { |
| 386 | + familyPopstate = () => { |
| 387 | + if (!root.isConnected) return; |
341 | 388 | const nextQuery = new URLSearchParams(window.location.search); |
342 | 389 | const nextRecipe = nextQuery.get("recipe"); |
343 | 390 | selectedRecipeId = nextRecipe && byId.has(nextRecipe) ? nextRecipe : defaultRecipeId; |
344 | 391 | selectedGroupId = groupIdFor(byId.get(selectedRecipeId)); |
345 | 392 | render({ historyMode: "none" }); |
346 | | - }); |
| 393 | + }; |
| 394 | + bindFamilyPopstate(); |
347 | 395 | }; |
348 | 396 |
|
349 | 397 | const init = () => { |
350 | | - initPatterns(document); |
| 398 | + initEvervault(document); |
351 | 399 | document.querySelectorAll("[data-cookbook][data-family]").forEach((root) => { |
352 | 400 | if (root.dataset.initialized) return; |
353 | 401 | root.dataset.initialized = "true"; |
|
0 commit comments