vod jam and earl vod.atverkackt.de
4
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

goose.art de3d35eb 9f367b5f

+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 {