vod frog, frog with the vods
5
fork

Configure Feed

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

fly spritesheet: CSS-animated wing flaps, remove individual frames

+48 -52
spec/fly2.png

This is a binary file and will not be displayed.

spec/fly3.png

This is a binary file and will not be displayed.

spec/fly4.png

This is a binary file and will not be displayed.

+48 -52
src/lib/FlySpawner.svelte
··· 1 1 <!-- 2 2 FlySpawner: Periodically spawns flies that buzz around the page. 3 - Click a fly to splat it — it turns into splat.png, fades out, then is removed. 4 - Max 5 flies on screen. New fly spawns every 0–30 seconds randomly. 3 + Click/tap a fly to splat it — it fades out and is removed. 4 + Uses a CSS sprite sheet for wing flap animation (no JS frame swapping). 5 + Max 5 flies, spawning every 1–5s from screen edges. 5 6 --> 6 7 <script lang="ts"> 7 8 import { onMount, onDestroy } from 'svelte'; 8 9 9 10 const MAX_FLIES = 5; 10 - const MIN_SPAWN_MS = 0; 11 - const MAX_SPAWN_MS = 30000; 11 + const MIN_SPAWN_MS = 1000; 12 + const MAX_SPAWN_MS = 5000; 12 13 13 14 interface Fly { 14 15 id: number; 15 - x: number; // current X position (%) 16 - y: number; // current Y position (%) 17 - targetX: number; // where it's heading (%) 18 - targetY: number; // where it's heading (%) 19 - rotation: number; // current rotation (deg) 20 - flipped: boolean; // true when moving right (sprite faces left by default) 21 - splatted: boolean; // has it been clicked? 22 - fading: boolean; // is it fading out after splat? 16 + x: number; 17 + y: number; 18 + targetX: number; 19 + targetY: number; 20 + rotation: number; 21 + flipped: boolean; 22 + splatted: boolean; 23 + fading: boolean; 23 24 } 24 25 25 26 let flies: Fly[] = $state([]); ··· 27 28 let spawnTimer: ReturnType<typeof setTimeout> | null = null; 28 29 let moveInterval: ReturnType<typeof setInterval> | null = null; 29 30 30 - /** Random position for fly targets — anywhere on screen */ 31 + /** Random position for fly targets */ 31 32 function randomPercent() { 32 - return Math.random() * 80 + 10; // 10–90% 33 + return Math.random() * 80 + 10; 33 34 } 34 35 35 36 /** Spawn position — always from a screen edge */ 36 37 function edgeSpawnPosition(): { x: number; y: number } { 37 38 const edge = Math.floor(Math.random() * 4); 38 39 switch (edge) { 39 - case 0: return { x: -5, y: Math.random() * 100 }; // left 40 - case 1: return { x: 105, y: Math.random() * 100 }; // right 41 - case 2: return { x: Math.random() * 100, y: -5 }; // top 42 - default: return { x: Math.random() * 100, y: 105 }; // bottom 40 + case 0: return { x: -5, y: Math.random() * 100 }; 41 + case 1: return { x: 105, y: Math.random() * 100 }; 42 + case 2: return { x: Math.random() * 100, y: -5 }; 43 + default: return { x: Math.random() * 100, y: 105 }; 43 44 } 44 45 } 45 46 ··· 72 73 } 73 74 74 75 function splatFly(id: number) { 75 - flies = flies.map(f => { 76 - if (f.id === id && !f.splatted) { 77 - return { ...f, splatted: true }; 78 - } 79 - return f; 80 - }); 76 + flies = flies.map(f => f.id === id && !f.splatted ? { ...f, splatted: true } : f); 81 77 82 - // Start fading after a beat, then remove 83 78 setTimeout(() => { 84 79 flies = flies.map(f => f.id === id ? { ...f, fading: true } : f); 85 80 }, 200); ··· 89 84 }, 1500); 90 85 } 91 86 92 - /** Move all live flies toward their targets, pick new targets when close */ 87 + /** Move all live flies toward their targets */ 93 88 function moveFliesTowardTargets() { 94 89 flies = flies.map(f => { 95 90 if (f.splatted) return f; ··· 98 93 const dy = f.targetY - f.y; 99 94 const dist = Math.sqrt(dx * dx + dy * dy); 100 95 101 - // Pick a new target when close 102 96 if (dist < 2) { 103 - return { 104 - ...f, 105 - targetX: randomPercent(), 106 - targetY: randomPercent(), 107 - }; 97 + return { ...f, targetX: randomPercent(), targetY: randomPercent() }; 108 98 } 109 99 110 - // Move toward target with some wobble 111 100 const speed = 0.4 + Math.random() * 0.3; 112 101 const wobbleX = (Math.random() - 0.5) * 1.5; 113 102 const wobbleY = (Math.random() - 0.5) * 1.5; 114 - 115 - // Sprite faces left by default, so 0° = moving left. 116 - // Only rotate ±30° from the current heading to keep it level. 117 103 const angle = Math.atan2(dy, dx) * (180 / Math.PI); 118 - // Clamp to a gentle wobble: -30° to +30° from horizontal 119 - const clampedAngle = Math.max(-30, Math.min(30, angle > 90 ? angle - 180 : angle < -90 ? angle + 180 : angle)); 120 - 121 - // Flip horizontally when moving right (sprite faces left by default) 122 - const movingRight = dx > 0; 104 + const clampedAngle = Math.max(-30, Math.min(30, 105 + angle > 90 ? angle - 180 : angle < -90 ? angle + 180 : angle 106 + )); 123 107 124 108 return { 125 109 ...f, 126 110 x: f.x + (dx / dist) * speed + wobbleX, 127 111 y: f.y + (dy / dist) * speed + wobbleY, 128 112 rotation: clampedAngle + (Math.random() - 0.5) * 10, 129 - flipped: movingRight, 113 + flipped: dx > 0, 130 114 }; 131 115 }); 132 116 } ··· 153 137 onmousedown={() => splatFly(fly.id)} 154 138 ontouchstart={() => splatFly(fly.id)} 155 139 > 156 - <img 157 - src={fly.splatted ? '/splat.png' : '/fly.png'} 158 - alt="fly" 159 - draggable="false" 160 - /> 140 + {#if fly.splatted} 141 + <img src="/splat.png" alt="splat" class="splat-img" draggable="false" /> 142 + {:else} 143 + <div class="fly-sprite"></div> 144 + {/if} 161 145 </div> 162 146 {/each} 163 147 </div> ··· 173 157 174 158 .fly { 175 159 position: absolute; 176 - width: 36px; 177 - height: 36px; 160 + width: 42px; 161 + height: 30px; 178 162 pointer-events: auto; 179 163 cursor: url('/frogcursor-small.png') 8 4, pointer; 180 - transition: transform 0.05s linear; 181 164 will-change: left, top, transform; 182 165 } 183 166 184 - .fly img { 167 + /* Sprite sheet: 5 frames, each 210x150, total 1050x150 */ 168 + .fly-sprite { 169 + width: 100%; 170 + height: 100%; 171 + background: url('/fly-sprite.png') 0 0 no-repeat; 172 + background-size: 500% 100%; /* 5 frames wide */ 173 + animation: flap 0.4s steps(5) infinite; 174 + } 175 + 176 + @keyframes flap { 177 + from { background-position: 0% 0; } 178 + to { background-position: 100% 0; } 179 + } 180 + 181 + .splat-img { 185 182 width: 100%; 186 183 height: 100%; 187 184 object-fit: contain; 188 - filter: drop-shadow(1px 1px 2px rgba(0, 0, 0, 0.3)); 189 185 } 190 186 191 187 .fly.splatted {
static/fly-sprite.png

This is a binary file and will not be displayed.

static/fly.png

This is a binary file and will not be displayed.