data endpoint for entity 90008 (aka. a website)
0
fork

Configure Feed

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

at svelte 311 lines 8.7 kB view raw
1<script module lang="ts"> 2 import { get, writable } from 'svelte/store'; 3 4 export const localDistanceTravelled = writable(0.0); 5 export const localBounces = writable(0); 6</script> 7 8<script lang="ts"> 9 import { draggable } from '@neodrag/svelte'; 10 import { browser } from '$app/environment'; 11 12 interface Props { 13 apiToken: string; 14 } 15 16 let { apiToken }: Props = $props(); 17 18 let lastDragged = 0; 19 let mouseX = 0; 20 let mouseY = 0; 21 22 let position = $state({ x: 0, y: 0 }); 23 let rotation = $state(0.0); 24 let sprite = $state('/pet/idle.webp'); 25 let flip = $state(false); 26 let dragged = $state(false); 27 28 let targetX = 120; 29 let speed = 10.0; 30 let tickRate = 20; 31 let delta = 1.0 / tickRate; 32 33 let strideRadius = 4.0; 34 let strideAngle = 0; 35 36 const turnStrideWheel = (by: number) => { 37 strideAngle += by / strideRadius; 38 if (strideAngle > Math.PI * 2) { 39 strideAngle -= Math.PI * 2; 40 } else if (strideAngle < 0) { 41 strideAngle += Math.PI * 2; 42 } 43 }; 44 45 let targetRotation = 0.0; 46 let rotationVelocity = 0.0; 47 let springStiffness = 20.0; // How quickly rotation returns to target 48 let springDamping = 0.2; // Damping factor to prevent oscillation 49 50 const updateRotationSpring = () => { 51 // Spring physics: calculate force based on distance from target 52 const springForce = (targetRotation - rotation) * springStiffness; 53 54 // Apply damping to velocity 55 rotationVelocity = rotationVelocity * (1 - springDamping) + springForce * delta; 56 57 // Update rotation based on velocity 58 rotation += rotationVelocity * delta; 59 60 // If we're very close to target and barely moving, just snap to target 61 if (Math.abs(rotation - targetRotation) < 0.01 && Math.abs(rotationVelocity) < 0.01) { 62 rotation = targetRotation; 63 rotationVelocity = 0; 64 } 65 }; 66 67 // Add spring update to the move function 68 if (browser) setInterval(updateRotationSpring, tickRate); 69 70 const moveTowards = (from: number, to: number, by: number) => { 71 let d = (to - from) * 1.0; 72 let l = Math.abs(d); 73 let s = Math.sign(d); 74 let moveBy = s * Math.min(l, by) * delta; 75 return moveBy; 76 }; 77 78 // Physics constants 79 let velocityX = 0; 80 let velocityY = 0; 81 let gravity = 200.0; // Gravity strength (positive because -Y is up) 82 let friction = 0.96; // Air friction 83 let groundFriction = 0.9; // Ground friction 84 let bounciness = 0.8; // How much energy is preserved on bounce 85 86 const sendBounceMetrics = () => { 87 fetch(`/_api/pet/bounce?_token=${apiToken}`); 88 localBounces.set(get(localBounces) + 1); 89 }; 90 91 let deltaTravelled = 0.0; 92 let deltaTravelledTotal = 0.0; 93 const updateDistanceTravelled = () => { 94 if (deltaTravelled > 0.1 || deltaTravelled < -0.1) { 95 localDistanceTravelled.update((n) => { 96 n += deltaTravelled; 97 return n; 98 }); 99 deltaTravelledTotal += deltaTravelled; 100 } 101 deltaTravelled = 0.0; 102 }; 103 104 const sendTotalDistance = () => { 105 fetch(`/_api/pet/distance?_token=${apiToken}`, { 106 method: 'POST', 107 body: deltaTravelledTotal.toString() 108 }); 109 deltaTravelledTotal = 0.0; 110 }; 111 112 // sending every 5 seconds is probably reliable enough 113 if (browser) setInterval(sendTotalDistance, 1000 * 5); 114 115 const move = () => { 116 if (dragged) return; 117 118 // Apply physics when pet is in motion 119 if (velocityX !== 0 || velocityY !== 0 || position.y !== 0) { 120 // Apply gravity (remember negative Y is upward) 121 velocityY += gravity * delta; 122 123 // Apply friction 124 const fric = position.y === 0 ? groundFriction : friction; 125 velocityX *= fric; 126 velocityY *= fric; 127 128 // Update position 129 const moveX = velocityX * delta; 130 const moveY = velocityY * delta; 131 position.x += moveX; 132 position.y += moveY; 133 134 deltaTravelled += Math.sqrt(moveX ** 2 + moveY ** 2); 135 updateDistanceTravelled(); 136 137 // Handle window boundaries 138 const viewportWidth = window.innerWidth; 139 140 // Bounce off sides 141 if (position.x < 0) { 142 position.x = 0; 143 velocityX = -velocityX * bounciness; 144 sendBounceMetrics(); 145 } else if (position.x > viewportWidth) { 146 position.x = viewportWidth; 147 velocityX = -velocityX * bounciness; 148 sendBounceMetrics(); 149 } 150 151 // Bounce off bottom (floor) 152 if (position.y > 0) { 153 position.y = 0; 154 velocityY = -velocityY * bounciness; 155 // Only bounce if velocity is significant 156 if (Math.abs(velocityY) < 80) { 157 velocityY = 0; 158 position.y = 0; 159 } else { 160 sendBounceMetrics(); 161 } 162 } 163 164 // reset velocity 165 if (Math.abs(velocityX) < 5 && Math.abs(velocityY) < 5) { 166 velocityX = 0; 167 velocityY = 0; 168 } 169 170 // Update flip based on velocity 171 if (Math.abs(velocityX) > 0.5) { 172 flip = velocityX < 0; 173 } 174 175 targetRotation = velocityX * 0.02 + velocityY * 0.01; 176 177 return; 178 } 179 180 // Normal movement when not physics-based 181 let moveByX = moveTowards( 182 position.x, 183 targetX, 184 speed * ((self.innerWidth ?? 1600.0) / 1600.0) 185 ); 186 position.x += moveByX; 187 188 turnStrideWheel(moveByX); 189 190 flip = moveByX < 0.0; 191 if (moveByX > 0.1 || moveByX < -0.1) { 192 sprite = strideAngle % Math.PI < Math.PI * 0.5 ? '/pet/walk1.webp' : '/pet/walk2.webp'; 193 } else { 194 sprite = '/pet/idle.webp'; 195 } 196 197 deltaTravelled += Math.abs(moveByX); 198 updateDistanceTravelled(); 199 }; 200 201 if (browser) setInterval(move, tickRate); 202 203 const shake = (event: DeviceMotionEvent) => { 204 const accel = event.acceleration ?? event.accelerationIncludingGravity; 205 if (accel === null || accel.x === null || accel.y === null) return; 206 if (Math.abs(accel.x) + Math.abs(accel.y) < 40.0) return; 207 // make it so that it amplifies motion proportionally to the window size 208 const windowRatio = (window.innerWidth * 1.0) / (window.innerHeight * 1.0); 209 velocityX += accel.x * windowRatio * 5.0; 210 velocityY += accel.y * (1.0 / windowRatio) * 5.0; 211 sprite = '/pet/pick.webp'; 212 }; 213 214 if (browser) self.ondevicemotion = shake; 215 216 // this is for ios 217 const askForShakePermission = () => { 218 if ( 219 typeof DeviceMotionEvent !== 'undefined' && 220 // eslint-disable-next-line @typescript-eslint/no-explicit-any 221 typeof (DeviceMotionEvent as any).requestPermission === 'function' 222 ) { 223 // eslint-disable-next-line @typescript-eslint/no-explicit-any 224 (DeviceMotionEvent as any) 225 .requestPermission() 226 .then((permissionState: string) => { 227 if (permissionState === 'granted') { 228 self.ondevicemotion = shake; 229 } 230 }) 231 .catch(console.error); 232 } 233 }; 234 235 const pickNewTargetX = () => { 236 const viewportWidth = self.innerWidth || null; 237 if (viewportWidth !== null && Math.abs(position.x - targetX) < 5) { 238 targetX = Math.max( 239 Math.min( 240 targetX + (Math.random() - 0.5) * (viewportWidth * 0.5), 241 viewportWidth * 0.9 242 ), 243 viewportWidth * 0.1 244 ); 245 } 246 // Set a random interval for the next target update (between 4-10 seconds) 247 const randomDelay = Math.floor(Math.random() * 6000) + 4000; 248 setTimeout(pickNewTargetX, randomDelay); 249 }; 250 251 // Start the process 252 if (browser) setTimeout(pickNewTargetX, 1000); 253</script> 254 255<!-- svelte-ignore a11y_missing_attribute --> 256<div 257 use:draggable={{ 258 position, 259 applyUserSelectHack: true, 260 handle: 'img', 261 bounds: { 262 bottom: (window.innerHeight / 100) * 5 263 }, 264 onDragStart: () => { 265 sprite = '/pet/pick.webp'; 266 dragged = true; 267 }, 268 onDrag: ({ offsetX, offsetY, event }) => { 269 position.x = offsetX; 270 position.y = offsetY; 271 const mouseXD = event.movementX * delta; 272 const mouseYD = event.movementY * delta; 273 deltaTravelled += Math.sqrt(mouseXD ** 2 + mouseYD ** 2); 274 // reset mouse movement if it's not moving in the same direction so it doesnt accumulate its weird!@!@ 275 mouseX = Math.sign(mouseXD) != Math.sign(mouseX) ? mouseXD : mouseX + mouseXD; 276 mouseY = Math.sign(mouseYD) != Math.sign(mouseY) ? mouseYD : mouseY + mouseYD; 277 rotationVelocity += mouseXD + mouseYD; 278 lastDragged = Date.now(); 279 }, 280 onDragEnd: () => { 281 // reset mouse movement if we stopped for longer than some time 282 if (Date.now() - lastDragged > 50) { 283 mouseX = 0.0; 284 mouseY = 0.0; 285 } 286 // apply velocity based on rotation since we already keep track of that 287 velocityX = mouseX * 70.0; 288 velocityY = mouseY * 50.0; 289 updateDistanceTravelled(); 290 // reset mouse movement we dont want it to accumulate 291 mouseX = 0.0; 292 mouseY = 0.0; 293 dragged = false; 294 } 295 }} 296 class="fixed bottom-[4.6vh] z-[1000] hover:animate-squiggle" 297 style="cursor: url('/icons/gaze.webp'), pointer;" 298> 299 <!-- svelte-ignore a11y_no_noninteractive_element_interactions --> 300 <!-- svelte-ignore a11y_click_events_have_key_events --> 301 <img 302 draggable="false" 303 onclick={askForShakePermission} 304 style=" 305 image-rendering: pixelated !important; 306 transform: rotate({rotation}rad) scaleX({flip ? -1 : 1}); 307 filter: invert(100%) drop-shadow(2px 2px 0 black) drop-shadow(-2px -2px 0 black); 308 " 309 src={sprite} 310 /> 311</div>