vod frog, frog with the vods
5
fork

Configure Feed

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

add WavyCircle component, link creators to profile pages, increase header bottom margin to 50px

+145 -13
+1 -1
src/lib/FrogHeader.svelte
··· 22 22 23 23 <style> 24 24 .frog-header { 25 - padding: 30px 20px 30px; 25 + padding: 30px 20px 50px; 26 26 position: relative; 27 27 } 28 28
+7 -1
src/lib/VideoCard.svelte
··· 185 185 </div> 186 186 <div class="info"> 187 187 <h3 class="title">{video.value.title}</h3> 188 - <p class="creator">{creatorHandle}</p> 188 + <a href="/profile/{creatorHandle.replace('@', '')}" class="creator" onclick={(e: MouseEvent) => e.stopPropagation()}>{creatorHandle}</a> 189 189 <p class="date">{formatDate(video.value.createdAt)}</p> 190 190 </div> 191 191 </WavyBorder> ··· 327 327 } 328 328 329 329 .creator { 330 + display: block; 330 331 margin: 4px 0 0; 331 332 font-family: 'Fang', system-ui, sans-serif; 332 333 font-size: 0.8rem; ··· 334 335 opacity: 0.8; 335 336 text-decoration: underline; 336 337 text-decoration-color: #FF3992; 338 + transition: color 0.15s; 339 + } 340 + 341 + .creator:hover { 342 + color: #FF3992; 337 343 } 338 344 339 345 .date {
+121
src/lib/WavyCircle.svelte
··· 1 + <script lang="ts"> 2 + import { seededRandom } from './theme'; 3 + 4 + let { seed, fill = '#39FF44', strokeColor = '#0A182B', strokeWidth = 2, size = 48, children }: { 5 + seed: string; 6 + fill?: string; 7 + strokeColor?: string; 8 + strokeWidth?: number; 9 + size?: number; 10 + children: any; 11 + } = $props(); 12 + 13 + const { svgPath, clipPolygon } = generateWavyCircle(seed); 14 + 15 + function generateWavyCircle(s: string): { svgPath: string; clipPolygon: string } { 16 + const cx = 50, cy = 50; 17 + const baseRadius = 44; 18 + const amp = 4; 19 + const points = 64; // smooth circle 20 + 21 + // Layered sine waves for radial wobble 22 + const phase1 = seededRandom(s, 1) * Math.PI * 2; 23 + const phase2 = seededRandom(s, 2) * Math.PI * 2; 24 + const freq1 = 4 + seededRandom(s, 3) * 4; // 4-8 bumps around circle 25 + const freq2 = 8 + seededRandom(s, 4) * 6; // 8-14 detail bumps 26 + const ampScale = 0.7 + seededRandom(s, 5) * 0.6; 27 + 28 + const pts: [number, number][] = []; 29 + 30 + for (let i = 0; i < points; i++) { 31 + const angle = (i / points) * Math.PI * 2; 32 + const w1 = Math.sin(angle * freq1 + phase1) * amp * 0.7; 33 + const w2 = Math.sin(angle * freq2 + phase2) * amp * 0.3; 34 + const r = baseRadius + (w1 + w2) * ampScale; 35 + pts.push([ 36 + cx + Math.cos(angle) * r, 37 + cy + Math.sin(angle) * r 38 + ]); 39 + } 40 + 41 + // SVG path with smooth Catmull-Rom splines 42 + let d = `M ${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`; 43 + for (let i = 0; i < pts.length; i++) { 44 + const p0 = pts[(i - 1 + pts.length) % pts.length]; 45 + const p1 = pts[i]; 46 + const p2 = pts[(i + 1) % pts.length]; 47 + const p3 = pts[(i + 2) % pts.length]; 48 + const tension = 0.25; 49 + const cp1x = p1[0] + (p2[0] - p0[0]) * tension; 50 + const cp1y = p1[1] + (p2[1] - p0[1]) * tension; 51 + const cp2x = p2[0] - (p3[0] - p1[0]) * tension; 52 + const cp2y = p2[1] - (p3[1] - p1[1]) * tension; 53 + d += ` C ${cp1x.toFixed(1)} ${cp1y.toFixed(1)}, ${cp2x.toFixed(1)} ${cp2y.toFixed(1)}, ${p2[0].toFixed(1)} ${p2[1].toFixed(1)}`; 54 + } 55 + d += ' Z'; 56 + 57 + // CSS polygon for clip-path (sample more densely for smooth clip) 58 + const clipPts = pts.map(([x, y]) => `${x.toFixed(2)}% ${y.toFixed(2)}%`).join(', '); 59 + 60 + return { svgPath: d, clipPolygon: `polygon(${clipPts})` }; 61 + } 62 + </script> 63 + 64 + <div class="wavy-circle" style="width: {size}px; height: {size}px;"> 65 + <div class="circle-clipped" style="clip-path: {clipPolygon};"> 66 + <div class="circle-fill" style="background: {fill};"></div> 67 + <div class="circle-content"> 68 + {@render children()} 69 + </div> 70 + </div> 71 + <svg class="circle-stroke" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" overflow="visible"> 72 + <path 73 + d={svgPath} 74 + fill="none" 75 + stroke={strokeColor} 76 + stroke-width={strokeWidth} 77 + stroke-linejoin="round" 78 + /> 79 + </svg> 80 + </div> 81 + 82 + <style> 83 + .wavy-circle { 84 + position: relative; 85 + flex-shrink: 0; 86 + } 87 + 88 + .circle-clipped { 89 + position: relative; 90 + width: 100%; 91 + height: 100%; 92 + } 93 + 94 + .circle-fill { 95 + position: absolute; 96 + inset: 0; 97 + } 98 + 99 + .circle-content { 100 + position: relative; 101 + width: 100%; 102 + height: 100%; 103 + } 104 + 105 + .circle-content :global(img) { 106 + width: 100%; 107 + height: 100%; 108 + object-fit: cover; 109 + display: block; 110 + } 111 + 112 + .circle-stroke { 113 + position: absolute; 114 + inset: 0; 115 + width: 100%; 116 + height: 100%; 117 + z-index: 2; 118 + pointer-events: none; 119 + overflow: visible; 120 + } 121 + </style>
+2 -2
src/lib/theme.ts
··· 44 44 const r3 = seededRandom(seed, 2); 45 45 46 46 return { 47 - rotation: (r1 - 0.5) * 6, // -3 to +3 degrees 48 - translateX: (r2 - 0.5) * 12, // -6 to +6 px 47 + rotation: (r1 - 0.5) * 10, // -5 to +5 degrees 48 + translateX: (r2 - 0.5) * 60, // -30 to +30 px 49 49 translateY: (r3 - 0.5) * 60, // -30 to +30 px 50 50 }; 51 51 }
+14 -9
src/routes/+page.svelte
··· 13 13 import VideoCard from "$lib/VideoCard.svelte"; 14 14 import FrogHeader from "$lib/FrogHeader.svelte"; 15 15 import WavyBorder from "$lib/WavyBorder.svelte"; 16 + import WavyCircle from "$lib/WavyCircle.svelte"; 16 17 17 18 const PAGE_SIZE = 9; 18 19 ··· 130 131 <h2 class="player-title">{selectedVideo.value.title}</h2> 131 132 <div class="player-meta-row"> 132 133 {#if selectedAvatar} 133 - <img src={selectedAvatar} alt="" class="creator-avatar" /> 134 + <a href="/profile/{selectedHandle?.replace('@', '') || selectedVideo.value.creator}" class="creator-avatar-link"> 135 + <WavyCircle seed={selectedVideo.value.creator} fill="#FFDEED" strokeColor="#0A182B" strokeWidth={1.5} size={40}> 136 + <img src={selectedAvatar} alt="" /> 137 + </WavyCircle> 138 + </a> 134 139 {/if} 135 140 <p class="player-meta"> 136 - <span class="creator-tag" 141 + <a href="/profile/{selectedHandle?.replace('@', '') || selectedVideo.value.creator}" class="creator-tag" 137 142 >{selectedHandle || 138 - selectedVideo.value.creator}</span 143 + selectedVideo.value.creator}</a 139 144 > 140 145 <span class="dot">·</span> 141 146 {formatDate(selectedVideo.value.createdAt)} ··· 197 202 margin-top: 6px; 198 203 } 199 204 200 - .creator-avatar { 201 - width: 36px; 202 - height: 36px; 203 - border-radius: 50%; 204 - object-fit: cover; 205 + .creator-avatar-link { 205 206 flex-shrink: 0; 206 - border: 2px solid #0A182B; 207 + transition: transform 0.15s; 208 + } 209 + 210 + .creator-avatar-link:hover { 211 + transform: scale(1.1); 207 212 } 208 213 209 214 .player-title {