vod frog, frog with the vods
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix clip/stroke alignment: match viewBox, increase polygon samples to 10/segment

goose.art 2780da5a e34cf53a

+360 -337
+1 -1
.svelte-kit/generated/server/internal.js
··· 25 25 app: ({ head, body, assets, nonce, env }) => "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<link rel=\"icon\" href=\"/frogcursor.png\" />\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n\t\t<title>vod frog 🐸</title>\n\t\t" + head + "\n\t</head>\n\t<body>\n\t\t<div style=\"display: contents\">" + body + "</div>\n\t</body>\n</html>\n", 26 26 error: ({ status, message }) => "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<title>" + message + "</title>\n\n\t\t<style>\n\t\t\tbody {\n\t\t\t\t--bg: white;\n\t\t\t\t--fg: #222;\n\t\t\t\t--divider: #ccc;\n\t\t\t\tbackground: var(--bg);\n\t\t\t\tcolor: var(--fg);\n\t\t\t\tfont-family:\n\t\t\t\t\tsystem-ui,\n\t\t\t\t\t-apple-system,\n\t\t\t\t\tBlinkMacSystemFont,\n\t\t\t\t\t'Segoe UI',\n\t\t\t\t\tRoboto,\n\t\t\t\t\tOxygen,\n\t\t\t\t\tUbuntu,\n\t\t\t\t\tCantarell,\n\t\t\t\t\t'Open Sans',\n\t\t\t\t\t'Helvetica Neue',\n\t\t\t\t\tsans-serif;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tjustify-content: center;\n\t\t\t\theight: 100vh;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t.error {\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tmax-width: 32rem;\n\t\t\t\tmargin: 0 1rem;\n\t\t\t}\n\n\t\t\t.status {\n\t\t\t\tfont-weight: 200;\n\t\t\t\tfont-size: 3rem;\n\t\t\t\tline-height: 1;\n\t\t\t\tposition: relative;\n\t\t\t\ttop: -0.05rem;\n\t\t\t}\n\n\t\t\t.message {\n\t\t\t\tborder-left: 1px solid var(--divider);\n\t\t\t\tpadding: 0 0 0 1rem;\n\t\t\t\tmargin: 0 0 0 1rem;\n\t\t\t\tmin-height: 2.5rem;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t}\n\n\t\t\t.message h1 {\n\t\t\t\tfont-weight: 400;\n\t\t\t\tfont-size: 1em;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t@media (prefers-color-scheme: dark) {\n\t\t\t\tbody {\n\t\t\t\t\t--bg: #222;\n\t\t\t\t\t--fg: #ddd;\n\t\t\t\t\t--divider: #666;\n\t\t\t\t}\n\t\t\t}\n\t\t</style>\n\t</head>\n\t<body>\n\t\t<div class=\"error\">\n\t\t\t<span class=\"status\">" + status + "</span>\n\t\t\t<div class=\"message\">\n\t\t\t\t<h1>" + message + "</h1>\n\t\t\t</div>\n\t\t</div>\n\t</body>\n</html>\n" 27 27 }, 28 - version_hash: "rtydas" 28 + version_hash: "tbmlr0" 29 29 }; 30 30 31 31 export async function get_hooks() {
+1 -1
.svelte-kit/non-ambient.d.ts
··· 38 38 }; 39 39 Pathname(): "/"; 40 40 ResolvedPathname(): `${"" | `/${string}`}${ReturnType<AppTypes['Pathname']>}`; 41 - Asset(): "/favicon.png" | "/favicon.svg" | "/frogcursor-small.png" | "/frogcursor.png" | "/robots.txt" | string & {}; 41 + Asset(): "/favicon.png" | "/favicon.svg" | "/frogcursor-small.png" | "/frogcursor.png" | "/frogeye.png" | "/froggie.png" | "/froggiejump.png" | "/froggiestand.png" | "/robots.txt" | string & {}; 42 42 } 43 43 }
+353 -330
src/lib/VideoPlayer.svelte
··· 1 1 <script lang="ts"> 2 - import { onMount } from 'svelte'; 3 - import Hls from 'hls.js'; 4 - import WavyBorder from './WavyBorder.svelte'; 2 + import { onMount } from "svelte"; 3 + import Hls from "hls.js"; 4 + import WavyBorder from "./WavyBorder.svelte"; 5 5 6 - let { src }: { src: string } = $props(); 6 + let { src }: { src: string } = $props(); 7 7 8 - let videoEl: HTMLVideoElement | undefined = $state(); 9 - let hls: Hls | null = null; 10 - let errorMsg = $state(''); 8 + let videoEl: HTMLVideoElement | undefined = $state(); 9 + let hls: Hls | null = null; 10 + let errorMsg = $state(""); 11 11 12 - // Playback state 13 - let playing = $state(false); 14 - let currentTime = $state(0); 15 - let duration = $state(0); 12 + // Playback state 13 + let playing = $state(false); 14 + let currentTime = $state(0); 15 + let duration = $state(0); 16 16 17 - // Frog scrub state 18 - let scrubBarEl: HTMLDivElement | undefined = $state(); 19 - let isScrubbing = $state(false); 20 - let scrubProgress = $state(0); 21 - let frogFrame = $state(0); 22 - let frogFlipped = $state(false); 23 - let lastFrogProgress = 0; 24 - let showControls = $state(true); 25 - let hideTimeout: ReturnType<typeof setTimeout> | null = null; 17 + // Frog scrub state 18 + let scrubBarEl: HTMLDivElement | undefined = $state(); 19 + let isScrubbing = $state(false); 20 + let scrubProgress = $state(0); 21 + let frogFrame = $state(0); 22 + let frogFlipped = $state(false); 23 + let lastFrogProgress = 0; 24 + let showControls = $state(true); 25 + let hideTimeout: ReturnType<typeof setTimeout> | null = null; 26 26 27 - // Fullscreen 28 - let isFullscreen = $state(false); 27 + // Fullscreen 28 + let isFullscreen = $state(false); 29 29 30 - function destroy() { 31 - if (hls) { hls.destroy(); hls = null; } 32 - } 30 + function destroy() { 31 + if (hls) { 32 + hls.destroy(); 33 + hls = null; 34 + } 35 + } 33 36 34 - function setup() { 35 - if (!videoEl || !src) return; 36 - destroy(); 37 - errorMsg = ''; 37 + function setup() { 38 + if (!videoEl || !src) return; 39 + destroy(); 40 + errorMsg = ""; 38 41 39 - if (Hls.isSupported()) { 40 - hls = new Hls({ enableWorker: true, lowLatencyMode: false }); 41 - hls.loadSource(src); 42 - hls.attachMedia(videoEl); 43 - hls.on(Hls.Events.MANIFEST_PARSED, () => { 44 - videoEl?.play().catch(() => {}); 45 - }); 46 - hls.on(Hls.Events.ERROR, (_event, data) => { 47 - console.error('HLS error:', data); 48 - if (data.fatal) { 49 - switch (data.type) { 50 - case Hls.ErrorTypes.NETWORK_ERROR: 51 - errorMsg = `Network error: ${data.details}`; 52 - hls?.startLoad(); 53 - break; 54 - case Hls.ErrorTypes.MEDIA_ERROR: 55 - errorMsg = `Media error: ${data.details}`; 56 - hls?.recoverMediaError(); 57 - break; 58 - default: 59 - errorMsg = `Fatal error: ${data.details}`; 60 - destroy(); 61 - break; 62 - } 63 - } 64 - }); 65 - } else if (videoEl.canPlayType('application/vnd.apple.mpegurl')) { 66 - videoEl.src = src; 67 - videoEl.addEventListener('loadedmetadata', () => { 68 - videoEl?.play().catch(() => {}); 69 - }); 70 - videoEl.addEventListener('error', () => { 71 - errorMsg = 'Audio may not work — these streams use Opus. Try Chrome or Firefox.'; 72 - }); 73 - } else { 74 - errorMsg = 'HLS playback is not supported in this browser.'; 75 - } 76 - } 42 + if (Hls.isSupported()) { 43 + hls = new Hls({ enableWorker: true, lowLatencyMode: false }); 44 + hls.loadSource(src); 45 + hls.attachMedia(videoEl); 46 + hls.on(Hls.Events.MANIFEST_PARSED, () => { 47 + videoEl?.play().catch(() => {}); 48 + }); 49 + hls.on(Hls.Events.ERROR, (_event, data) => { 50 + console.error("HLS error:", data); 51 + if (data.fatal) { 52 + switch (data.type) { 53 + case Hls.ErrorTypes.NETWORK_ERROR: 54 + errorMsg = `Network error: ${data.details}`; 55 + hls?.startLoad(); 56 + break; 57 + case Hls.ErrorTypes.MEDIA_ERROR: 58 + errorMsg = `Media error: ${data.details}`; 59 + hls?.recoverMediaError(); 60 + break; 61 + default: 62 + errorMsg = `Fatal error: ${data.details}`; 63 + destroy(); 64 + break; 65 + } 66 + } 67 + }); 68 + } else if (videoEl.canPlayType("application/vnd.apple.mpegurl")) { 69 + videoEl.src = src; 70 + videoEl.addEventListener("loadedmetadata", () => { 71 + videoEl?.play().catch(() => {}); 72 + }); 73 + videoEl.addEventListener("error", () => { 74 + errorMsg = 75 + "Audio may not work — these streams use Opus. Try Chrome or Firefox."; 76 + }); 77 + } else { 78 + errorMsg = "HLS playback is not supported in this browser."; 79 + } 80 + } 77 81 78 - $effect(() => { 79 - src; 80 - if (videoEl) setup(); 81 - return destroy; 82 - }); 82 + $effect(() => { 83 + src; 84 + if (videoEl) setup(); 85 + return destroy; 86 + }); 83 87 84 - $effect(() => { 85 - document.addEventListener('fullscreenchange', onFullscreenChange); 86 - return () => document.removeEventListener('fullscreenchange', onFullscreenChange); 87 - }); 88 + $effect(() => { 89 + document.addEventListener("fullscreenchange", onFullscreenChange); 90 + return () => 91 + document.removeEventListener( 92 + "fullscreenchange", 93 + onFullscreenChange, 94 + ); 95 + }); 88 96 89 - let lastHopProgress = 0; 97 + let lastHopProgress = 0; 90 98 91 - function onTimeUpdate() { 92 - if (!videoEl || isScrubbing) return; 93 - currentTime = videoEl.currentTime; 94 - duration = videoEl.duration || 0; 95 - const newProgress = duration > 0 ? currentTime / duration : 0; 99 + function onTimeUpdate() { 100 + if (!videoEl || isScrubbing) return; 101 + currentTime = videoEl.currentTime; 102 + duration = videoEl.duration || 0; 103 + const newProgress = duration > 0 ? currentTime / duration : 0; 96 104 97 - // Hop the frog as playback advances 98 - const delta = newProgress - lastHopProgress; 99 - if (Math.abs(delta) > 0.015) { 100 - frogFrame = frogFrame === 0 ? 1 : 0; 101 - frogFlipped = delta < 0; 102 - lastHopProgress = newProgress; 103 - } 105 + // Hop the frog as playback advances 106 + const delta = newProgress - lastHopProgress; 107 + if (Math.abs(delta) > 0.015) { 108 + frogFrame = frogFrame === 0 ? 1 : 0; 109 + frogFlipped = delta < 0; 110 + lastHopProgress = newProgress; 111 + } 104 112 105 - scrubProgress = newProgress; 106 - } 113 + scrubProgress = newProgress; 114 + } 107 115 108 - function onPlay() { playing = true; } 109 - function onPause() { playing = false; } 116 + function onPlay() { 117 + playing = true; 118 + } 119 + function onPause() { 120 + playing = false; 121 + } 110 122 111 - function togglePlay() { 112 - if (!videoEl) return; 113 - if (videoEl.paused) videoEl.play().catch(() => {}); 114 - else videoEl.pause(); 115 - } 123 + function togglePlay() { 124 + if (!videoEl) return; 125 + if (videoEl.paused) videoEl.play().catch(() => {}); 126 + else videoEl.pause(); 127 + } 116 128 117 - function toggleFullscreen() { 118 - const wrapper = videoEl?.closest('.player-wrapper'); 119 - if (!wrapper) return; 120 - if (!document.fullscreenElement) { 121 - wrapper.requestFullscreen?.().catch(() => {}); 122 - } else { 123 - document.exitFullscreen?.().catch(() => {}); 124 - } 125 - } 129 + function toggleFullscreen() { 130 + const wrapper = videoEl?.closest(".player-wrapper"); 131 + if (!wrapper) return; 132 + if (!document.fullscreenElement) { 133 + wrapper.requestFullscreen?.().catch(() => {}); 134 + } else { 135 + document.exitFullscreen?.().catch(() => {}); 136 + } 137 + } 126 138 127 - function onFullscreenChange() { 128 - isFullscreen = !!document.fullscreenElement; 129 - } 139 + function onFullscreenChange() { 140 + isFullscreen = !!document.fullscreenElement; 141 + } 130 142 131 - // Scrub bar — clicking the track or dragging the frog 132 - function scrubFromEvent(e: MouseEvent) { 133 - if (!scrubBarEl) return; 134 - const rect = scrubBarEl.getBoundingClientRect(); 135 - const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); 136 - const delta = pct - lastFrogProgress; 137 - if (Math.abs(delta) > 0.02) { 138 - frogFrame = frogFrame === 0 ? 1 : 0; 139 - frogFlipped = delta < 0; 140 - lastFrogProgress = pct; 141 - } 142 - scrubProgress = pct; 143 - if (videoEl && duration > 0) { 144 - videoEl.currentTime = pct * duration; 145 - currentTime = pct * duration; 146 - } 147 - } 143 + // Scrub bar — clicking the track or dragging the frog 144 + function scrubFromEvent(e: MouseEvent) { 145 + if (!scrubBarEl) return; 146 + const rect = scrubBarEl.getBoundingClientRect(); 147 + const pct = Math.max( 148 + 0, 149 + Math.min(1, (e.clientX - rect.left) / rect.width), 150 + ); 151 + const delta = pct - lastFrogProgress; 152 + if (Math.abs(delta) > 0.02) { 153 + frogFrame = frogFrame === 0 ? 1 : 0; 154 + frogFlipped = delta < 0; 155 + lastFrogProgress = pct; 156 + } 157 + scrubProgress = pct; 158 + if (videoEl && duration > 0) { 159 + videoEl.currentTime = pct * duration; 160 + currentTime = pct * duration; 161 + } 162 + } 148 163 149 - function onScrubDown(e: MouseEvent) { 150 - e.preventDefault(); 151 - isScrubbing = true; 152 - scrubFromEvent(e); 153 - window.addEventListener('mousemove', onScrubMove); 154 - window.addEventListener('mouseup', onScrubUp); 155 - } 164 + function onScrubDown(e: MouseEvent) { 165 + e.preventDefault(); 166 + isScrubbing = true; 167 + scrubFromEvent(e); 168 + window.addEventListener("mousemove", onScrubMove); 169 + window.addEventListener("mouseup", onScrubUp); 170 + } 156 171 157 - function onFrogDown(e: MouseEvent) { 158 - e.preventDefault(); 159 - e.stopPropagation(); 160 - isScrubbing = true; 161 - window.addEventListener('mousemove', onScrubMove); 162 - window.addEventListener('mouseup', onScrubUp); 163 - } 172 + function onFrogDown(e: MouseEvent) { 173 + e.preventDefault(); 174 + e.stopPropagation(); 175 + isScrubbing = true; 176 + window.addEventListener("mousemove", onScrubMove); 177 + window.addEventListener("mouseup", onScrubUp); 178 + } 164 179 165 - function onScrubMove(e: MouseEvent) { 166 - if (!isScrubbing) return; 167 - scrubFromEvent(e); 168 - } 180 + function onScrubMove(e: MouseEvent) { 181 + if (!isScrubbing) return; 182 + scrubFromEvent(e); 183 + } 169 184 170 - function onScrubUp() { 171 - isScrubbing = false; 172 - window.removeEventListener('mousemove', onScrubMove); 173 - window.removeEventListener('mouseup', onScrubUp); 174 - } 185 + function onScrubUp() { 186 + isScrubbing = false; 187 + window.removeEventListener("mousemove", onScrubMove); 188 + window.removeEventListener("mouseup", onScrubUp); 189 + } 175 190 176 - function onMouseActivity() { 177 - showControls = true; 178 - if (hideTimeout) clearTimeout(hideTimeout); 179 - hideTimeout = setTimeout(() => { 180 - if (playing) showControls = false; 181 - }, 2500); 182 - } 191 + function onMouseActivity() { 192 + showControls = true; 193 + if (hideTimeout) clearTimeout(hideTimeout); 194 + hideTimeout = setTimeout(() => { 195 + if (playing) showControls = false; 196 + }, 2500); 197 + } 183 198 </script> 184 199 185 200 <WavyBorder seed="player-main"> 186 - <div 187 - class="player-wrapper" 188 - onmousemove={onMouseActivity} 189 - onmouseenter={onMouseActivity} 190 - onmouseleave={() => { if (playing) showControls = false; }} 191 - role="region" 192 - > 193 - <video 194 - bind:this={videoEl} 195 - playsinline 196 - ontimeupdate={onTimeUpdate} 197 - onplay={onPlay} 198 - onpause={onPause} 199 - onclick={togglePlay} 200 - > 201 - <track kind="captions" /> 202 - </video> 201 + <div 202 + class="player-wrapper" 203 + onmousemove={onMouseActivity} 204 + onmouseenter={onMouseActivity} 205 + onmouseleave={() => { 206 + if (playing) showControls = false; 207 + }} 208 + role="region" 209 + > 210 + <video 211 + bind:this={videoEl} 212 + playsinline 213 + ontimeupdate={onTimeUpdate} 214 + onplay={onPlay} 215 + onpause={onPause} 216 + onclick={togglePlay} 217 + > 218 + <track kind="captions" /> 219 + </video> 203 220 204 - <!-- Minimal controls: frog scrub bar + frogeye fullscreen --> 205 - <div class="controls" class:visible={showControls || !playing}> 206 - <!-- Frog scrub area (no visible bar — frog position IS the progress) --> 207 - <!-- svelte-ignore a11y_no_static_element_interactions --> 208 - <div 209 - class="scrub-bar" 210 - bind:this={scrubBarEl} 211 - onmousedown={onScrubDown} 212 - role="slider" 213 - aria-valuenow={currentTime} 214 - aria-valuemin={0} 215 - aria-valuemax={duration} 216 - tabindex={0} 217 - > 218 - <!-- svelte-ignore a11y_no_static_element_interactions --> 219 - <div 220 - class="scrub-frog" 221 - style="left: {scrubProgress * 100}%;" 222 - class:flipped={frogFlipped} 223 - onmousedown={onFrogDown} 224 - > 225 - <img 226 - src={frogFrame === 0 ? '/froggiestand.png' : '/froggiejump.png'} 227 - alt="scrub" 228 - class="frog-sprite" 229 - draggable="false" 230 - /> 231 - </div> 232 - </div> 233 - </div> 221 + <!-- Minimal controls: frog scrub bar + frogeye fullscreen --> 222 + <div class="controls" class:visible={showControls || !playing}> 223 + <!-- Frog scrub area (no visible bar — frog position IS the progress) --> 224 + <!-- svelte-ignore a11y_no_static_element_interactions --> 225 + <div 226 + class="scrub-bar" 227 + bind:this={scrubBarEl} 228 + onmousedown={onScrubDown} 229 + role="slider" 230 + aria-valuenow={currentTime} 231 + aria-valuemin={0} 232 + aria-valuemax={duration} 233 + tabindex={0} 234 + > 235 + <!-- svelte-ignore a11y_no_static_element_interactions --> 236 + <div 237 + class="scrub-frog" 238 + style="left: {scrubProgress * 100}%;" 239 + class:flipped={frogFlipped} 240 + onmousedown={onFrogDown} 241 + > 242 + <img 243 + src={frogFrame === 0 244 + ? "/froggiestand.png" 245 + : "/froggiejump.png"} 246 + alt="scrub" 247 + class="frog-sprite" 248 + draggable="false" 249 + /> 250 + </div> 251 + </div> 252 + </div> 234 253 235 - <!-- Frogeye fullscreen toggle — bottom right --> 236 - <button 237 - class="fullscreen-btn" 238 - class:visible={showControls || !playing} 239 - onclick={toggleFullscreen} 240 - title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'} 241 - > 242 - <img src="/frogeye.png" alt="fullscreen" class="frogeye" /> 243 - </button> 254 + <!-- Frogeye fullscreen toggle — bottom right --> 255 + <button 256 + class="fullscreen-btn" 257 + class:visible={showControls || !playing} 258 + onclick={toggleFullscreen} 259 + title={isFullscreen ? "Exit fullscreen" : "Fullscreen"} 260 + > 261 + <img src="/frogeye.png" alt="fullscreen" class="frogeye" /> 262 + </button> 244 263 245 - <!-- Play/pause overlay on center when paused --> 246 - {#if !playing} 247 - <button class="play-overlay" onclick={togglePlay}> 248 - <svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z" fill="currentColor"/></svg> 249 - </button> 250 - {/if} 264 + <!-- Play/pause overlay on center when paused --> 265 + {#if !playing} 266 + <button class="play-overlay" onclick={togglePlay}> 267 + <svg viewBox="0 0 24 24" 268 + ><path d="M8 5v14l11-7z" fill="currentColor" /></svg 269 + > 270 + </button> 271 + {/if} 251 272 252 - {#if errorMsg} 253 - <div class="error-overlay">{errorMsg}</div> 254 - {/if} 255 - </div> 273 + {#if errorMsg} 274 + <div class="error-overlay">{errorMsg}</div> 275 + {/if} 276 + </div> 256 277 </WavyBorder> 257 278 258 279 <style> 259 - .player-wrapper { 260 - position: relative; 261 - width: 100%; 262 - background: #0A182B; 263 - overflow: hidden; 264 - } 280 + .player-wrapper { 281 + position: relative; 282 + width: 100%; 283 + background: #0a182b; 284 + overflow: hidden; 285 + } 265 286 266 - video { 267 - width: 100%; 268 - display: block; 269 - max-height: 70vh; 270 - cursor: pointer; 271 - } 287 + video { 288 + width: 100%; 289 + display: block; 290 + max-height: 70vh; 291 + cursor: pointer; 292 + } 272 293 273 - /* Frog scrub area — no visible bar, frog position is the progress */ 274 - .controls { 275 - position: absolute; 276 - bottom: 2%; 277 - left: 10%; 278 - right: 15%; 279 - opacity: 0; 280 - transition: opacity 0.25s ease; 281 - pointer-events: none; 282 - } 294 + /* Frog scrub area — no visible bar, frog position is the progress */ 295 + .controls { 296 + position: absolute; 297 + bottom: 5%; 298 + left: 10%; 299 + right: 15%; 300 + opacity: 0; 301 + transition: opacity 0.25s ease; 302 + pointer-events: none; 303 + } 283 304 284 - .controls.visible { 285 - opacity: 1; 286 - pointer-events: auto; 287 - } 305 + .controls.visible { 306 + opacity: 1; 307 + pointer-events: auto; 308 + } 288 309 289 - .scrub-bar { 290 - position: relative; 291 - height: 48px; 292 - cursor: pointer; 293 - } 310 + .scrub-bar { 311 + position: relative; 312 + height: 48px; 313 + cursor: pointer; 314 + } 294 315 295 - .scrub-frog { 296 - position: absolute; 297 - bottom: 0; 298 - transform: translateX(-50%); 299 - cursor: grab; 300 - transition: left 0.05s linear; 301 - z-index: 2; 302 - padding: 6px; 303 - } 316 + .scrub-frog { 317 + position: absolute; 318 + bottom: 0; 319 + transform: translateX(-50%); 320 + cursor: grab; 321 + transition: left 0.05s linear; 322 + z-index: 2; 323 + padding: 6px; 324 + } 304 325 305 - .scrub-frog:active { 306 - cursor: grabbing; 307 - } 326 + .scrub-frog:active { 327 + cursor: grabbing; 328 + } 308 329 309 - .scrub-frog.flipped { 310 - transform: translateX(-50%) scaleX(-1); 311 - } 330 + .scrub-frog.flipped { 331 + transform: translateX(-50%) scaleX(-1); 332 + } 312 333 313 - .frog-sprite { 314 - width: 48px; 315 - height: auto; 316 - filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.5)); 317 - pointer-events: none; 318 - } 334 + .frog-sprite { 335 + width: 48px; 336 + height: auto; 337 + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.5)); 338 + pointer-events: none; 339 + } 319 340 320 - /* Frogeye fullscreen button */ 321 - .fullscreen-btn { 322 - all: unset; 323 - position: absolute; 324 - bottom: 2%; 325 - right: 8%; 326 - cursor: pointer; 327 - opacity: 0; 328 - transition: opacity 0.25s ease, transform 0.2s ease; 329 - z-index: 5; 330 - } 341 + /* Frogeye fullscreen button */ 342 + .fullscreen-btn { 343 + all: unset; 344 + position: absolute; 345 + bottom: 2%; 346 + right: 8%; 347 + cursor: pointer; 348 + opacity: 0; 349 + transition: 350 + opacity 0.25s ease, 351 + transform 0.2s ease; 352 + z-index: 5; 353 + } 331 354 332 - .fullscreen-btn.visible { 333 - opacity: 1; 334 - } 355 + .fullscreen-btn.visible { 356 + opacity: 1; 357 + } 335 358 336 - .fullscreen-btn:hover { 337 - transform: scale(1.15); 338 - } 359 + .fullscreen-btn:hover { 360 + transform: scale(1.15); 361 + } 339 362 340 - .frogeye { 341 - width: 44px; 342 - height: 44px; 343 - filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.4)); 344 - } 363 + .frogeye { 364 + width: 44px; 365 + height: 44px; 366 + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.4)); 367 + } 345 368 346 - /* Center play button when paused */ 347 - .play-overlay { 348 - all: unset; 349 - position: absolute; 350 - inset: 0; 351 - display: flex; 352 - align-items: center; 353 - justify-content: center; 354 - cursor: pointer; 355 - } 369 + /* Center play button when paused */ 370 + .play-overlay { 371 + all: unset; 372 + position: absolute; 373 + inset: 0; 374 + display: flex; 375 + align-items: center; 376 + justify-content: center; 377 + cursor: pointer; 378 + } 356 379 357 - .play-overlay svg { 358 - width: 64px; 359 - height: 64px; 360 - color: #39FF44; 361 - filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.5)); 362 - transition: transform 0.15s ease; 363 - } 380 + .play-overlay svg { 381 + width: 64px; 382 + height: 64px; 383 + color: #39ff44; 384 + filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.5)); 385 + transition: transform 0.15s ease; 386 + } 364 387 365 - .play-overlay:hover svg { 366 - transform: scale(1.1); 367 - color: #FF3992; 368 - } 388 + .play-overlay:hover svg { 389 + transform: scale(1.1); 390 + color: #ff3992; 391 + } 369 392 370 - .error-overlay { 371 - position: absolute; 372 - bottom: 15%; 373 - left: 10%; 374 - right: 10%; 375 - background: rgba(255, 57, 146, 0.85); 376 - color: #FFDEED; 377 - padding: 8px 12px; 378 - border-radius: 6px; 379 - font-family: 'Fang', system-ui, sans-serif; 380 - font-size: 0.8rem; 381 - } 393 + .error-overlay { 394 + position: absolute; 395 + bottom: 15%; 396 + left: 10%; 397 + right: 10%; 398 + background: rgba(255, 57, 146, 0.85); 399 + color: #ffdeed; 400 + padding: 8px 12px; 401 + border-radius: 6px; 402 + font-family: "Fang", system-ui, sans-serif; 403 + font-size: 0.8rem; 404 + } 382 405 </style>
+5 -5
src/lib/WavyBorder.svelte
··· 75 75 // Sample many points along the spline for the CSS polygon 76 76 const sampledPts: [number, number][] = []; 77 77 const totalPts = points.length; 78 - const samplesPerSegment = 4; 78 + const samplesPerSegment = 10; 79 79 for (let i = 0; i < totalPts; i++) { 80 80 const p0 = points[(i - 1 + totalPts) % totalPts]; 81 81 const p1 = points[i]; ··· 111 111 </div> 112 112 113 113 <!-- Stroke outline rendered via SVG, same path so it aligns --> 114 - <svg class="wavy-stroke" viewBox="-4 -4 108 108" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg" overflow="visible"> 114 + <svg class="wavy-stroke" viewBox="0 0 100 100" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg" overflow="visible"> 115 115 <path 116 116 d={svgPath} 117 117 fill="none" ··· 147 147 148 148 .wavy-stroke { 149 149 position: absolute; 150 - inset: -4px; 151 - width: calc(100% + 8px); 152 - height: calc(100% + 8px); 150 + inset: 0; 151 + width: 100%; 152 + height: 100%; 153 153 z-index: 3; 154 154 pointer-events: none; 155 155 overflow: visible;