vod frog, frog with the vods
5
fork

Configure Feed

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

wobbly pagination buttons using WavyButton component

+122 -31
+119
src/lib/WavyButton.svelte
··· 1 + <!-- 2 + WavyButton: A wobbly oval button using sine-wave generated borders. 3 + Reusable for any text content — used for pagination, login, etc. 4 + --> 5 + <script lang="ts"> 6 + import { seededRandom } from './theme'; 7 + 8 + let { seed = 'btn', fill = '#0A182B', textColor = '#FFDEED', disabled = false, onclick, children }: { 9 + seed?: string; 10 + fill?: string; 11 + textColor?: string; 12 + disabled?: boolean; 13 + onclick?: (e: MouseEvent) => void; 14 + children: any; 15 + } = $props(); 16 + 17 + const { svgPath, clipPolygon } = $derived(generateWavyOval(seed)); 18 + 19 + function generateWavyOval(s: string): { svgPath: string; clipPolygon: string } { 20 + const cx = 50, cy = 50; 21 + const rx = 44, ry = 36; 22 + const amp = 3; 23 + const points = 48; 24 + 25 + const phase1 = seededRandom(s, 1) * Math.PI * 2; 26 + const phase2 = seededRandom(s, 2) * Math.PI * 2; 27 + const freq1 = 3 + seededRandom(s, 3) * 3; 28 + const freq2 = 6 + seededRandom(s, 4) * 4; 29 + const ampScale = 0.7 + seededRandom(s, 5) * 0.5; 30 + 31 + const pts: [number, number][] = []; 32 + for (let i = 0; i < points; i++) { 33 + const angle = (i / points) * Math.PI * 2; 34 + const w1 = Math.sin(angle * freq1 + phase1) * amp * 0.7; 35 + const w2 = Math.sin(angle * freq2 + phase2) * amp * 0.3; 36 + const wobble = (w1 + w2) * ampScale; 37 + pts.push([cx + Math.cos(angle) * (rx + wobble), cy + Math.sin(angle) * (ry + wobble)]); 38 + } 39 + 40 + let d = `M ${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`; 41 + for (let i = 0; i < pts.length; i++) { 42 + const p0 = pts[(i - 1 + pts.length) % pts.length]; 43 + const p1 = pts[i]; 44 + const p2 = pts[(i + 1) % pts.length]; 45 + const p3 = pts[(i + 2) % pts.length]; 46 + const tension = 0.25; 47 + d += ` C ${(p1[0] + (p2[0] - p0[0]) * tension).toFixed(1)} ${(p1[1] + (p2[1] - p0[1]) * tension).toFixed(1)}, ${(p2[0] - (p3[0] - p1[0]) * tension).toFixed(1)} ${(p2[1] - (p3[1] - p1[1]) * tension).toFixed(1)}, ${p2[0].toFixed(1)} ${p2[1].toFixed(1)}`; 48 + } 49 + d += ' Z'; 50 + 51 + const clipPts = pts.map(([x, y]) => `${x.toFixed(2)}% ${y.toFixed(2)}%`).join(', '); 52 + return { svgPath: d, clipPolygon: `polygon(${clipPts})` }; 53 + } 54 + </script> 55 + 56 + <button class="wavy-btn" class:disabled {disabled} onclick={onclick}> 57 + <div class="btn-clipped" style="clip-path: {clipPolygon};"> 58 + <div class="btn-fill" style="background: {fill};"></div> 59 + <span class="btn-text" style="color: {textColor};"> 60 + {@render children()} 61 + </span> 62 + </div> 63 + <svg class="btn-stroke" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" overflow="visible"> 64 + <path d={svgPath} fill="none" stroke="#0A182B" stroke-width="2" stroke-linejoin="round" /> 65 + </svg> 66 + </button> 67 + 68 + <style> 69 + .wavy-btn { 70 + all: unset; 71 + position: relative; 72 + width: clamp(100px, 12vw, 140px); 73 + height: clamp(44px, 5vw, 56px); 74 + cursor: pointer; 75 + transition: transform 0.15s ease; 76 + } 77 + 78 + .wavy-btn:hover:not(.disabled) { 79 + transform: scale(1.05); 80 + } 81 + 82 + .wavy-btn.disabled { 83 + opacity: 0.5; 84 + cursor: wait; 85 + } 86 + 87 + .btn-clipped { 88 + position: relative; 89 + width: 100%; 90 + height: 100%; 91 + display: flex; 92 + align-items: center; 93 + justify-content: center; 94 + } 95 + 96 + .btn-fill { 97 + position: absolute; 98 + inset: 0; 99 + } 100 + 101 + .btn-text { 102 + position: relative; 103 + z-index: 1; 104 + font-family: 'PicNic', cursive, system-ui; 105 + font-size: clamp(0.9rem, 1.3vw, 1.15rem); 106 + letter-spacing: 0.5px; 107 + white-space: nowrap; 108 + } 109 + 110 + .btn-stroke { 111 + position: absolute; 112 + inset: 0; 113 + width: 100%; 114 + height: 100%; 115 + z-index: 2; 116 + pointer-events: none; 117 + overflow: visible; 118 + } 119 + </style>
+3 -31
src/routes/+page.svelte
··· 15 15 import FrogHeader from "$lib/FrogHeader.svelte"; 16 16 import WavyBorder from "$lib/WavyBorder.svelte"; 17 17 import WavyCircle from "$lib/WavyCircle.svelte"; 18 + import WavyButton from "$lib/WavyButton.svelte"; 18 19 19 20 const PAGE_SIZE = 9; 20 21 ··· 168 169 169 170 <div class="pagination"> 170 171 {#if pageIndex > 0} 171 - <button class="page-btn" onclick={prevPage} disabled={loading}> 172 - ← previous 173 - </button> 172 + <WavyButton seed="prev-page" disabled={loading} onclick={prevPage}>← previous</WavyButton> 174 173 {/if} 175 174 <span class="page-num">page {pageIndex + 1}</span> 176 175 {#if hasMore} 177 - <button class="page-btn" onclick={nextPage} disabled={loading}> 178 - next → 179 - </button> 176 + <WavyButton seed="next-page" disabled={loading} onclick={nextPage}>next →</WavyButton> 180 177 {/if} 181 178 </div> 182 179 </div> ··· 257 254 padding: 30px; 258 255 } 259 256 260 - .page-btn { 261 - background: #0a182b; 262 - color: #39ff44; 263 - border: 3px solid #0a182b; 264 - padding: 12px 32px; 265 - border-radius: 40px; 266 - font-family: "PicNic", cursive, system-ui; 267 - font-size: 1.1rem; 268 - cursor: pointer; 269 - transition: all 0.2s ease; 270 - letter-spacing: 0.5px; 271 - } 272 257 273 - .page-btn:hover { 274 - background: #39ff44; 275 - color: #0a182b; 276 - border-color: #0a182b; 277 - } 278 - 279 - .page-btn:disabled { 280 - background: #1a8c22; 281 - color: #0a182b; 282 - border-color: #1a8c22; 283 - cursor: wait; 284 - opacity: 0.6; 285 - } 286 258 287 259 .page-num { 288 260 font-family: "PicNic", cursive, system-ui;