data endpoint for entity 90008 (aka. a website)

feat: add eyes

ptr.pet c7cc4aa7 890da5b2

verified
+92
src/components/eye.svelte
···
+
<script lang="ts">
+
import { renderDate } from '$lib/dateFmt';
+
import { genDollcode } from '$lib/dollcode';
+
import Tooltip from './tooltip.svelte';
+
+
interface Props {
+
top: number;
+
left: number;
+
kind?: string;
+
visits: number[];
+
id: string;
+
}
+
+
let { top, left, kind = 'normal', visits, id }: Props = $props();
+
+
const reverse = Math.random() > 0.35;
+
let rotation = $state((Math.random() - 0.5) * 0.4);
+
const opacity = Math.min(Math.random() * 0.3 + 0.4, 0.7);
+
+
let closed = $state(false);
+
let look = $state('forward');
+
const looks = ['left', 'forward', 'right'];
+
const pickLook = $derived(() => {
+
const pickable = looks.filter((l) => {
+
return l !== look;
+
});
+
return pickable.at(Math.floor(Math.random() * pickable.length)) ?? 'forward';
+
});
+
const randomizeLook = () => {
+
look = pickLook();
+
rotation = (Math.random() - 0.5) * 0.4;
+
setTimeout(randomizeLook, 2000 + Math.random() * 6000);
+
};
+
+
let src = $derived(closed ? `/eyes/closed.webp` : `/eyes/${kind}_${look}.webp`);
+
+
randomizeLook();
+
</script>
+
+
<!-- svelte-ignore a11y_mouse_events_have_key_events -->
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
+
<Tooltip
+
style="
+
position: fixed;
+
top: {top}vh;
+
left: {left}%;
+
"
+
y="translate-y-none"
+
targetY="group-hover:translate-y-none"
+
x="-translate-x-[20%]"
+
targetX="group-hover:-translate-x-[20%]"
+
>
+
{#snippet tooltipContent()}
+
<p class="font-monospace" style="min-width: {id.length + 15}ch;">
+
//observant/id={id}<br />
+
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;/date="{renderDate(
+
visits[0]
+
)}"<br />
+
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;/count={visits.length}/
+
</p>
+
{/snippet}
+
<div
+
class="group flex gap-4 items-center scale-[0.75]"
+
style="
+
opacity: {opacity};
+
flex-direction: {reverse ? 'column-reverse' : 'column'};
+
"
+
onmouseover={() => {
+
closed = true;
+
}}
+
onmouseleave={() => {
+
closed = false;
+
}}
+
>
+
<span class="eye-text !text-base">{genDollcode(visits.length)}</span>
+
<!-- svelte-ignore a11y_missing_attribute -->
+
<img class="w-24 eye-image" style="transform: rotate({rotation}rad);" {src} />
+
<span class="eye-text">{genDollcode(visits[0])}</span>
+
</div>
+
</Tooltip>
+
+
<style lang="postcss">
+
.eye-text {
+
@apply text-xs [font-family:Doll_Mono] opacity-30 group-hover:opacity-80;
+
}
+
.eye-image {
+
@apply opacity-50 group-hover:opacity-100;
+
image-rendering: pixelated !important;
+
filter: drop-shadow(4px 4px 0 theme(colors.ralsei.green.light))
+
drop-shadow(-4px -4px 0 theme(colors.ralsei.pink.neon));
+
}
+
</style>
+29 -25
src/components/tooltip.svelte
···
<script lang="ts">
-
import Window from "./window.svelte";
+
import Window from './window.svelte';
-
interface Props {
-
x?: string;
-
y?: string;
-
targetY?: string;
-
targetX?: string;
-
tooltipContent?: import('svelte').Snippet;
-
children?: import('svelte').Snippet;
-
}
+
interface Props {
+
x?: string;
+
y?: string;
+
targetY?: string;
+
targetX?: string;
+
tooltipContent?: import('svelte').Snippet;
+
children?: import('svelte').Snippet;
+
style?: string;
+
}
-
let {
-
x = "translate-x-none",
-
y = "translate-y-full",
-
targetY = "group-hover:-translate-y-[105%]",
-
targetX = "group-hover:-translate-x-2/3",
-
tooltipContent,
-
children
-
}: Props = $props();
+
let {
+
x = 'translate-x-none',
+
y = 'translate-y-full',
+
targetY = 'group-hover:-translate-y-[105%]',
+
targetX = 'group-hover:-translate-x-2/3',
+
tooltipContent,
+
children,
+
style = ''
+
}: Props = $props();
</script>
-
<div class="group">
-
<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}">
-
<Window tooltip>
-
{#if tooltipContent}{@render tooltipContent()}{:else}Hello world!{/if}
-
</Window>
-
</div>
-
{@render children?.()}
-
</div>
+
<div class="group" {style}>
+
<div
+
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}"
+
>
+
<Window tooltip>
+
{#if tooltipContent}{@render tooltipContent()}{:else}Hello world!{/if}
+
</Window>
+
</div>
+
{@render children?.()}
+
</div>
+23
src/lib/dollcode.ts
···
+
// https://noe.sh/dollcode/
+
const charmap = ['▌', '▖', '▘'];
+
export const genDollcode = (number: number) => {
+
const output = [];
+
let window = number;
+
let loopProtection = 1000;
+
+
while (loopProtection > 0 && window > 0) {
+
const mod = window % 3;
+
+
if (mod == 0) {
+
window = (window - 3) / 3;
+
} else {
+
window = (window - mod) / 3;
+
}
+
+
output.unshift(charmap[mod]);
+
+
loopProtection--;
+
}
+
+
return output.join('');
+
};
+24 -24
src/lib/index.ts
···
-
import type { Cookies } from '@sveltejs/kit'
-
import { hash } from 'crypto'
+
import type { Cookies } from '@sveltejs/kit';
+
import { hash } from 'crypto';
export const scopeCookies = (cookies: Cookies, path: string) => {
-
return {
-
get: (key: string) => {
-
return cookies.get(key)
-
},
-
set: (key: string, value: string, props: import('cookie').CookieSerializeOptions = {}) => {
-
cookies.set(key, value, { ...props, path })
-
},
-
delete: (key: string, props: import('cookie').CookieSerializeOptions = {}) => {
-
cookies.delete(key, { ...props, path })
-
}
-
}
-
}
+
return {
+
get: (key: string) => {
+
return cookies.get(key);
+
},
+
set: (key: string, value: string, props: import('cookie').CookieSerializeOptions = {}) => {
+
cookies.set(key, value, { ...props, path });
+
},
+
delete: (key: string, props: import('cookie').CookieSerializeOptions = {}) => {
+
cookies.delete(key, { ...props, path });
+
}
+
};
+
};
-
const cipherChars = ['#', '%', '+', '=', '//']
+
const cipherChars = ['#', '%', '+', '=', '//'];
export const fancyText = (input: string) => {
-
const hashed = hash("sha256", input, "hex")
-
let result = ""
-
let idx = 0
-
while (idx < hashed.length) {
-
result += cipherChars[hashed.charCodeAt(idx) % cipherChars.length]
-
idx += 1
-
}
-
return result
-
}
+
const hashed = hash('sha256', input, 'hex');
+
let result = '';
+
let idx = 0;
+
while (idx < hashed.length) {
+
result += cipherChars[hashed.charCodeAt(idx) % cipherChars.length];
+
idx += 1;
+
}
+
return result;
+
};
+2 -5
src/lib/visits.ts
···
parseInt(existsSync(visitCountFile) ? readFileSync(visitCountFile).toString() : '0')
);
-
type Visitor = { visits: number[] };
+
export type Visitor = { visits: number[] };
export const lastVisitors = writable<Map<string, Visitor>>(new Map());
const VISITOR_EXPIRY_SECONDS = 60 * 60; // an hour seems reasonable
···
})
.then(async (resp) => {
if (resp !== null) {
-
const msg = await resp.json();
const host = `(${request.headers.get('host')}|${request.headers.get('x-real-ip')}|${request.headers.get('user-agent')})`;
-
console.log(
-
`sent visitor analytic to dark visitors: ${resp.statusText}; ${msg.message ?? ''}${host}`
-
);
+
console.log(`sent visitor analytic to dark visitors: ${resp.statusText}; ${host}`);
}
});
};
+41 -2
src/routes/+layout.server.ts
···
import { bounceCount, distanceTravelled } from '$lib/metrics.js';
import { lastVisitors, visitCount } from '$lib/visits.js';
-
import { localBounces, localDistanceTravelled } from '../components/pet.svelte';
import { get } from 'svelte/store';
export const csr = true;
···
recentVisitCount += visitor.visits.length;
}
+
const eyePositions = [];
+
const usedPositions = [];
+
for (let i = 0; i < Math.min(visitors.size, 10); i++) {
+
let maxMinDistance = 0;
+
let bestPosition = null;
+
+
// Try multiple positions and keep the one with largest minimum distance to existing points
+
for (let attempt = 0; attempt < 50; attempt++) {
+
const sidePreference = Math.random() < 0.5;
+
const testLeft = sidePreference
+
? Math.random() * 30 // Left side
+
: 60 + Math.random() * 30; // Right side
+
const testTop = Math.random() * 80;
+
+
let currentMinDistance = Infinity;
+
+
// Calculate minimum distance to all existing points
+
for (const pos of usedPositions) {
+
const distance = Math.sqrt(
+
Math.pow(testLeft - pos.left, 2) + Math.pow(testTop - pos.top, 2)
+
);
+
currentMinDistance = Math.min(currentMinDistance, distance);
+
}
+
+
// If this position has a larger minimum distance, keep it
+
if (currentMinDistance > maxMinDistance) {
+
maxMinDistance = currentMinDistance;
+
bestPosition = { left: testLeft, top: testTop };
+
}
+
}
+
+
// Use the best position found
+
const left = bestPosition ? bestPosition.left : Math.random() * 90;
+
const top = bestPosition ? bestPosition.top : Math.random() * 80;
+
+
usedPositions.push({ left, top });
+
eyePositions.push([top, left]);
+
}
+
return {
route: url.pathname,
petTotalBounce: bounceCount.get(),
petTotalDistance: distanceTravelled.get(),
visitCount: get(visitCount),
lastVisitors: visitors,
-
recentVisitCount
+
recentVisitCount,
+
eyePositions
};
}
+15 -1
src/routes/+layout.svelte
···
<script lang="ts">
import { browser } from '$app/environment';
import getTitle from '$lib/getTitle';
+
import Eye from '../components/eye.svelte';
import NavButton from '../components/navButton.svelte';
import Pet, { localBounces, localDistanceTravelled } from '../components/pet.svelte';
import Tooltip from '../components/tooltip.svelte';
···
let title = $derived(getTitle(data.route));
const svgSquiggles = [[2], [3], [2], [3], [1]];
+
+
// svelte-ignore non_reactive_update
+
let eyePositions = null;
+
if (eyePositions === null) {
+
eyePositions = data.eyePositions;
+
}
</script>
<svelte:head>
···
</defs>
</svg>
+
{#each data.lastVisitors as [id, visitor], index}
+
{@const pos = eyePositions.at(index)}
+
{#if pos !== undefined}
+
<Eye visits={visitor.visits} {id} top={pos[0]} left={pos[1]} />
+
{/if}
+
{/each}
+
<div
class="md:h-[96vh] pb-[8vh] lg:px-[1vw] 2xl:px-[2vw] lg:pb-[3vh] lg:pt-[1vh] overflow-x-hidden [scrollbar-gutter:stable]"
>
{@render children?.()}
</div>
-
<Pet></Pet>
+
<Pet />
<nav class="w-full min-h-[5vh] max-h-[5vh] fixed bottom-0 z-[999] bg-ralsei-black overflow-visible">
<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.