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

Configure Feed

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

feat: add eyes

dusk c7cc4aa7 890da5b2

+226 -57
+92
src/components/eye.svelte
··· 1 + <script lang="ts"> 2 + import { renderDate } from '$lib/dateFmt'; 3 + import { genDollcode } from '$lib/dollcode'; 4 + import Tooltip from './tooltip.svelte'; 5 + 6 + interface Props { 7 + top: number; 8 + left: number; 9 + kind?: string; 10 + visits: number[]; 11 + id: string; 12 + } 13 + 14 + let { top, left, kind = 'normal', visits, id }: Props = $props(); 15 + 16 + const reverse = Math.random() > 0.35; 17 + let rotation = $state((Math.random() - 0.5) * 0.4); 18 + const opacity = Math.min(Math.random() * 0.3 + 0.4, 0.7); 19 + 20 + let closed = $state(false); 21 + let look = $state('forward'); 22 + const looks = ['left', 'forward', 'right']; 23 + const pickLook = $derived(() => { 24 + const pickable = looks.filter((l) => { 25 + return l !== look; 26 + }); 27 + return pickable.at(Math.floor(Math.random() * pickable.length)) ?? 'forward'; 28 + }); 29 + const randomizeLook = () => { 30 + look = pickLook(); 31 + rotation = (Math.random() - 0.5) * 0.4; 32 + setTimeout(randomizeLook, 2000 + Math.random() * 6000); 33 + }; 34 + 35 + let src = $derived(closed ? `/eyes/closed.webp` : `/eyes/${kind}_${look}.webp`); 36 + 37 + randomizeLook(); 38 + </script> 39 + 40 + <!-- svelte-ignore a11y_mouse_events_have_key_events --> 41 + <!-- svelte-ignore a11y_no_static_element_interactions --> 42 + <Tooltip 43 + style=" 44 + position: fixed; 45 + top: {top}vh; 46 + left: {left}%; 47 + " 48 + y="translate-y-none" 49 + targetY="group-hover:translate-y-none" 50 + x="-translate-x-[20%]" 51 + targetX="group-hover:-translate-x-[20%]" 52 + > 53 + {#snippet tooltipContent()} 54 + <p class="font-monospace" style="min-width: {id.length + 15}ch;"> 55 + //observant/id={id}<br /> 56 + &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;/date="{renderDate( 57 + visits[0] 58 + )}"<br /> 59 + &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;/count={visits.length}/ 60 + </p> 61 + {/snippet} 62 + <div 63 + class="group flex gap-4 items-center scale-[0.75]" 64 + style=" 65 + opacity: {opacity}; 66 + flex-direction: {reverse ? 'column-reverse' : 'column'}; 67 + " 68 + onmouseover={() => { 69 + closed = true; 70 + }} 71 + onmouseleave={() => { 72 + closed = false; 73 + }} 74 + > 75 + <span class="eye-text !text-base">{genDollcode(visits.length)}</span> 76 + <!-- svelte-ignore a11y_missing_attribute --> 77 + <img class="w-24 eye-image" style="transform: rotate({rotation}rad);" {src} /> 78 + <span class="eye-text">{genDollcode(visits[0])}</span> 79 + </div> 80 + </Tooltip> 81 + 82 + <style lang="postcss"> 83 + .eye-text { 84 + @apply text-xs [font-family:Doll_Mono] opacity-30 group-hover:opacity-80; 85 + } 86 + .eye-image { 87 + @apply opacity-50 group-hover:opacity-100; 88 + image-rendering: pixelated !important; 89 + filter: drop-shadow(4px 4px 0 theme(colors.ralsei.green.light)) 90 + drop-shadow(-4px -4px 0 theme(colors.ralsei.pink.neon)); 91 + } 92 + </style>
+29 -25
src/components/tooltip.svelte
··· 1 1 <script lang="ts"> 2 - import Window from "./window.svelte"; 2 + import Window from './window.svelte'; 3 3 4 - interface Props { 5 - x?: string; 6 - y?: string; 7 - targetY?: string; 8 - targetX?: string; 9 - tooltipContent?: import('svelte').Snippet; 10 - children?: import('svelte').Snippet; 11 - } 4 + interface Props { 5 + x?: string; 6 + y?: string; 7 + targetY?: string; 8 + targetX?: string; 9 + tooltipContent?: import('svelte').Snippet; 10 + children?: import('svelte').Snippet; 11 + style?: string; 12 + } 12 13 13 - let { 14 - x = "translate-x-none", 15 - y = "translate-y-full", 16 - targetY = "group-hover:-translate-y-[105%]", 17 - targetX = "group-hover:-translate-x-2/3", 18 - tooltipContent, 19 - children 20 - }: Props = $props(); 14 + let { 15 + x = 'translate-x-none', 16 + y = 'translate-y-full', 17 + targetY = 'group-hover:-translate-y-[105%]', 18 + targetX = 'group-hover:-translate-x-2/3', 19 + tooltipContent, 20 + children, 21 + style = '' 22 + }: Props = $props(); 21 23 </script> 22 24 23 - <div class="group"> 24 - <div class="absolute scale-0 transition-all [transition-timing-function:cubic-bezier(0.4,0,0.2,1.6)] [transition-duration:300ms] opacity-0 group-hover:scale-100 group-hover:opacity-100 {y} {x} {targetY} {targetX}"> 25 - <Window tooltip> 26 - {#if tooltipContent}{@render tooltipContent()}{:else}Hello world!{/if} 27 - </Window> 28 - </div> 29 - {@render children?.()} 30 - </div> 25 + <div class="group" {style}> 26 + <div 27 + class="z-10 absolute scale-0 transition-all [transition-timing-function:cubic-bezier(0.4,0,0.2,1.6)] [transition-duration:300ms] opacity-0 group-hover:scale-100 group-hover:opacity-100 {y} {x} {targetY} {targetX}" 28 + > 29 + <Window tooltip> 30 + {#if tooltipContent}{@render tooltipContent()}{:else}Hello world!{/if} 31 + </Window> 32 + </div> 33 + {@render children?.()} 34 + </div>
+23
src/lib/dollcode.ts
··· 1 + // https://noe.sh/dollcode/ 2 + const charmap = ['▌', '▖', '▘']; 3 + export const genDollcode = (number: number) => { 4 + const output = []; 5 + let window = number; 6 + let loopProtection = 1000; 7 + 8 + while (loopProtection > 0 && window > 0) { 9 + const mod = window % 3; 10 + 11 + if (mod == 0) { 12 + window = (window - 3) / 3; 13 + } else { 14 + window = (window - mod) / 3; 15 + } 16 + 17 + output.unshift(charmap[mod]); 18 + 19 + loopProtection--; 20 + } 21 + 22 + return output.join(''); 23 + };
+24 -24
src/lib/index.ts
··· 1 - import type { Cookies } from '@sveltejs/kit' 2 - import { hash } from 'crypto' 1 + import type { Cookies } from '@sveltejs/kit'; 2 + import { hash } from 'crypto'; 3 3 4 4 export const scopeCookies = (cookies: Cookies, path: string) => { 5 - return { 6 - get: (key: string) => { 7 - return cookies.get(key) 8 - }, 9 - set: (key: string, value: string, props: import('cookie').CookieSerializeOptions = {}) => { 10 - cookies.set(key, value, { ...props, path }) 11 - }, 12 - delete: (key: string, props: import('cookie').CookieSerializeOptions = {}) => { 13 - cookies.delete(key, { ...props, path }) 14 - } 15 - } 16 - } 5 + return { 6 + get: (key: string) => { 7 + return cookies.get(key); 8 + }, 9 + set: (key: string, value: string, props: import('cookie').CookieSerializeOptions = {}) => { 10 + cookies.set(key, value, { ...props, path }); 11 + }, 12 + delete: (key: string, props: import('cookie').CookieSerializeOptions = {}) => { 13 + cookies.delete(key, { ...props, path }); 14 + } 15 + }; 16 + }; 17 17 18 - const cipherChars = ['#', '%', '+', '=', '//'] 18 + const cipherChars = ['#', '%', '+', '=', '//']; 19 19 export const fancyText = (input: string) => { 20 - const hashed = hash("sha256", input, "hex") 21 - let result = "" 22 - let idx = 0 23 - while (idx < hashed.length) { 24 - result += cipherChars[hashed.charCodeAt(idx) % cipherChars.length] 25 - idx += 1 26 - } 27 - return result 28 - } 20 + const hashed = hash('sha256', input, 'hex'); 21 + let result = ''; 22 + let idx = 0; 23 + while (idx < hashed.length) { 24 + result += cipherChars[hashed.charCodeAt(idx) % cipherChars.length]; 25 + idx += 1; 26 + } 27 + return result; 28 + };
+2 -5
src/lib/visits.ts
··· 10 10 parseInt(existsSync(visitCountFile) ? readFileSync(visitCountFile).toString() : '0') 11 11 ); 12 12 13 - type Visitor = { visits: number[] }; 13 + export type Visitor = { visits: number[] }; 14 14 export const lastVisitors = writable<Map<string, Visitor>>(new Map()); 15 15 const VISITOR_EXPIRY_SECONDS = 60 * 60; // an hour seems reasonable 16 16 ··· 129 129 }) 130 130 .then(async (resp) => { 131 131 if (resp !== null) { 132 - const msg = await resp.json(); 133 132 const host = `(${request.headers.get('host')}|${request.headers.get('x-real-ip')}|${request.headers.get('user-agent')})`; 134 - console.log( 135 - `sent visitor analytic to dark visitors: ${resp.statusText}; ${msg.message ?? ''}${host}` 136 - ); 133 + console.log(`sent visitor analytic to dark visitors: ${resp.statusText}; ${host}`); 137 134 } 138 135 }); 139 136 };
+41 -2
src/routes/+layout.server.ts
··· 1 1 import { bounceCount, distanceTravelled } from '$lib/metrics.js'; 2 2 import { lastVisitors, visitCount } from '$lib/visits.js'; 3 - import { localBounces, localDistanceTravelled } from '../components/pet.svelte'; 4 3 import { get } from 'svelte/store'; 5 4 6 5 export const csr = true; ··· 15 14 recentVisitCount += visitor.visits.length; 16 15 } 17 16 17 + const eyePositions = []; 18 + const usedPositions = []; 19 + for (let i = 0; i < Math.min(visitors.size, 10); i++) { 20 + let maxMinDistance = 0; 21 + let bestPosition = null; 22 + 23 + // Try multiple positions and keep the one with largest minimum distance to existing points 24 + for (let attempt = 0; attempt < 50; attempt++) { 25 + const sidePreference = Math.random() < 0.5; 26 + const testLeft = sidePreference 27 + ? Math.random() * 30 // Left side 28 + : 60 + Math.random() * 30; // Right side 29 + const testTop = Math.random() * 80; 30 + 31 + let currentMinDistance = Infinity; 32 + 33 + // Calculate minimum distance to all existing points 34 + for (const pos of usedPositions) { 35 + const distance = Math.sqrt( 36 + Math.pow(testLeft - pos.left, 2) + Math.pow(testTop - pos.top, 2) 37 + ); 38 + currentMinDistance = Math.min(currentMinDistance, distance); 39 + } 40 + 41 + // If this position has a larger minimum distance, keep it 42 + if (currentMinDistance > maxMinDistance) { 43 + maxMinDistance = currentMinDistance; 44 + bestPosition = { left: testLeft, top: testTop }; 45 + } 46 + } 47 + 48 + // Use the best position found 49 + const left = bestPosition ? bestPosition.left : Math.random() * 90; 50 + const top = bestPosition ? bestPosition.top : Math.random() * 80; 51 + 52 + usedPositions.push({ left, top }); 53 + eyePositions.push([top, left]); 54 + } 55 + 18 56 return { 19 57 route: url.pathname, 20 58 petTotalBounce: bounceCount.get(), 21 59 petTotalDistance: distanceTravelled.get(), 22 60 visitCount: get(visitCount), 23 61 lastVisitors: visitors, 24 - recentVisitCount 62 + recentVisitCount, 63 + eyePositions 25 64 }; 26 65 }
+15 -1
src/routes/+layout.svelte
··· 1 1 <script lang="ts"> 2 2 import { browser } from '$app/environment'; 3 3 import getTitle from '$lib/getTitle'; 4 + import Eye from '../components/eye.svelte'; 4 5 import NavButton from '../components/navButton.svelte'; 5 6 import Pet, { localBounces, localDistanceTravelled } from '../components/pet.svelte'; 6 7 import Tooltip from '../components/tooltip.svelte'; ··· 43 44 let title = $derived(getTitle(data.route)); 44 45 45 46 const svgSquiggles = [[2], [3], [2], [3], [1]]; 47 + 48 + // svelte-ignore non_reactive_update 49 + let eyePositions = null; 50 + if (eyePositions === null) { 51 + eyePositions = data.eyePositions; 52 + } 46 53 </script> 47 54 48 55 <svelte:head> ··· 137 144 </defs> 138 145 </svg> 139 146 147 + {#each data.lastVisitors as [id, visitor], index} 148 + {@const pos = eyePositions.at(index)} 149 + {#if pos !== undefined} 150 + <Eye visits={visitor.visits} {id} top={pos[0]} left={pos[1]} /> 151 + {/if} 152 + {/each} 153 + 140 154 <div 141 155 class="md:h-[96vh] pb-[8vh] lg:px-[1vw] 2xl:px-[2vw] lg:pb-[3vh] lg:pt-[1vh] overflow-x-hidden [scrollbar-gutter:stable]" 142 156 > 143 157 {@render children?.()} 144 158 </div> 145 159 146 - <Pet></Pet> 160 + <Pet /> 147 161 148 162 <nav class="w-full min-h-[5vh] max-h-[5vh] fixed bottom-0 z-[999] bg-ralsei-black overflow-visible"> 149 163 <div
static/eyes/closed.webp

This is a binary file and will not be displayed.

static/eyes/normal_forward.webp

This is a binary file and will not be displayed.

static/eyes/normal_left.webp

This is a binary file and will not be displayed.

static/eyes/normal_right.webp

This is a binary file and will not be displayed.