vod frog, frog with the vods
5
fork

Configure Feed

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

add fly spawner: flies buzz around the page, click to splat them

goose.art 22b2ce2c 24d63523

+178
spec/fly.png

This is a binary file and will not be displayed.

spec/splat.png

This is a binary file and will not be displayed.

+176
src/lib/FlySpawner.svelte
··· 1 + <!-- 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. 5 + --> 6 + <script lang="ts"> 7 + import { onMount, onDestroy } from 'svelte'; 8 + 9 + const MAX_FLIES = 5; 10 + const MIN_SPAWN_MS = 0; 11 + const MAX_SPAWN_MS = 30000; 12 + 13 + interface Fly { 14 + 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 + splatted: boolean; // has it been clicked? 21 + fading: boolean; // is it fading out after splat? 22 + } 23 + 24 + let flies: Fly[] = $state([]); 25 + let nextId = 0; 26 + let spawnTimer: ReturnType<typeof setTimeout> | null = null; 27 + let moveInterval: ReturnType<typeof setInterval> | null = null; 28 + 29 + function randomPercent() { 30 + return Math.random() * 80 + 10; // 10–90% to stay away from edges 31 + } 32 + 33 + function spawnFly() { 34 + if (flies.length >= MAX_FLIES) { 35 + scheduleNextSpawn(); 36 + return; 37 + } 38 + 39 + const fly: Fly = { 40 + id: nextId++, 41 + x: randomPercent(), 42 + y: randomPercent(), 43 + targetX: randomPercent(), 44 + targetY: randomPercent(), 45 + rotation: Math.random() * 360, 46 + splatted: false, 47 + fading: false, 48 + }; 49 + 50 + flies = [...flies, fly]; 51 + scheduleNextSpawn(); 52 + } 53 + 54 + function scheduleNextSpawn() { 55 + const delay = MIN_SPAWN_MS + Math.random() * (MAX_SPAWN_MS - MIN_SPAWN_MS); 56 + spawnTimer = setTimeout(spawnFly, delay); 57 + } 58 + 59 + function splatFly(id: number) { 60 + flies = flies.map(f => { 61 + if (f.id === id && !f.splatted) { 62 + return { ...f, splatted: true }; 63 + } 64 + return f; 65 + }); 66 + 67 + // Start fading after a beat, then remove 68 + setTimeout(() => { 69 + flies = flies.map(f => f.id === id ? { ...f, fading: true } : f); 70 + }, 200); 71 + 72 + setTimeout(() => { 73 + flies = flies.filter(f => f.id !== id); 74 + }, 1500); 75 + } 76 + 77 + /** Move all live flies toward their targets, pick new targets when close */ 78 + function moveFliesTowardTargets() { 79 + flies = flies.map(f => { 80 + if (f.splatted) return f; 81 + 82 + const dx = f.targetX - f.x; 83 + const dy = f.targetY - f.y; 84 + const dist = Math.sqrt(dx * dx + dy * dy); 85 + 86 + // Pick a new target when close 87 + if (dist < 2) { 88 + return { 89 + ...f, 90 + targetX: randomPercent(), 91 + targetY: randomPercent(), 92 + }; 93 + } 94 + 95 + // Move toward target with some wobble 96 + const speed = 0.4 + Math.random() * 0.3; 97 + const wobbleX = (Math.random() - 0.5) * 1.5; 98 + const wobbleY = (Math.random() - 0.5) * 1.5; 99 + const angle = Math.atan2(dy, dx) * (180 / Math.PI); 100 + 101 + return { 102 + ...f, 103 + x: f.x + (dx / dist) * speed + wobbleX, 104 + y: f.y + (dy / dist) * speed + wobbleY, 105 + rotation: angle + (Math.random() - 0.5) * 30, 106 + }; 107 + }); 108 + } 109 + 110 + onMount(() => { 111 + scheduleNextSpawn(); 112 + moveInterval = setInterval(moveFliesTowardTargets, 50); 113 + }); 114 + 115 + onDestroy(() => { 116 + if (spawnTimer) clearTimeout(spawnTimer); 117 + if (moveInterval) clearInterval(moveInterval); 118 + }); 119 + </script> 120 + 121 + <div class="fly-layer"> 122 + {#each flies as fly (fly.id)} 123 + <!-- svelte-ignore a11y_no_static_element_interactions --> 124 + <div 125 + class="fly" 126 + class:splatted={fly.splatted} 127 + class:fading={fly.fading} 128 + style="left: {fly.x}%; top: {fly.y}%; transform: rotate({fly.rotation}deg);" 129 + onmousedown={() => splatFly(fly.id)} 130 + ontouchstart={() => splatFly(fly.id)} 131 + > 132 + <img 133 + src={fly.splatted ? '/splat.png' : '/fly.png'} 134 + alt="fly" 135 + draggable="false" 136 + /> 137 + </div> 138 + {/each} 139 + </div> 140 + 141 + <style> 142 + .fly-layer { 143 + position: fixed; 144 + inset: 0; 145 + z-index: 200; 146 + pointer-events: none; 147 + overflow: hidden; 148 + } 149 + 150 + .fly { 151 + position: absolute; 152 + width: 36px; 153 + height: 36px; 154 + pointer-events: auto; 155 + cursor: url('/frogcursor-small.png') 8 4, pointer; 156 + transition: transform 0.05s linear; 157 + will-change: left, top, transform; 158 + } 159 + 160 + .fly img { 161 + width: 100%; 162 + height: 100%; 163 + object-fit: contain; 164 + filter: drop-shadow(1px 1px 2px rgba(0, 0, 0, 0.3)); 165 + } 166 + 167 + .fly.splatted { 168 + pointer-events: none; 169 + transform: rotate(0deg) !important; 170 + } 171 + 172 + .fly.fading { 173 + opacity: 0; 174 + transition: opacity 1.2s ease-out; 175 + } 176 + </style>
+2
src/routes/+layout.svelte
··· 1 1 <script lang="ts"> 2 2 import MeshBackground from '$lib/MeshBackground.svelte'; 3 3 import PlantOverlay from '$lib/PlantOverlay.svelte'; 4 + import FlySpawner from '$lib/FlySpawner.svelte'; 4 5 5 6 let { children } = $props(); 6 7 </script> ··· 41 42 42 43 <MeshBackground /> 43 44 <PlantOverlay /> 45 + <FlySpawner /> 44 46 45 47 {@render children()} 46 48
static/fly.png

This is a binary file and will not be displayed.

static/splat.png

This is a binary file and will not be displayed.