website/src/components/GoogleyEye.svelte

179 lines
4.0 KiB
Svelte
Raw Normal View History

2024-03-27 01:51:42 +01:00
<script context="module" lang="ts">
let mouse = { x: 0, y: 0 };
let visible = writable(false);
const maxDistance = globalThis.innerWidth / 3;
let frameId: number;
function update() {
frameId = requestAnimationFrame(update);
for (const eye of eyes) {
const dx = eye.x - mouse.x;
const dy = eye.y - mouse.y;
const angle = Math.atan2(dy, dx);
const distance = Math.hypot(dx, dy);
if (distance < maxDistance) {
eye.rotation = angle * (180 / Math.PI) - 90;
eye.distance = -Math.min(17, distance);
eye.el.style.setProperty("--distance", `${eye.distance}px`);
eye.el.style.setProperty(
"--rotation",
`${Math.floor(eye.rotation)}deg`,
);
eye.el.classList.add("active");
} else {
eye.el.classList.remove("active");
}
}
}
const eyes: {
x: number;
y: number;
rotation: number;
distance: number;
el: HTMLDivElement;
pupil: HTMLDivElement;
}[] = [];
function register(eye: HTMLDivElement) {
const { x, y, width, height } = eye.getBoundingClientRect();
eyes.push({
x: x + width / 2,
y: y + height / 2,
rotation: 0,
distance: 0,
el: eye,
pupil: eye.children[0] as HTMLDivElement,
});
if (frameId) cancelAnimationFrame(frameId);
update();
}
function unregister(eye: HTMLDivElement) {
const index = eyes.findIndex((e) => e.el === eye);
eyes.splice(index, 1);
if (eyes.length === 0) cancelAnimationFrame(frameId);
}
function handleMouseMove(ev: MouseEvent) {
mouse.x = ev.clientX;
mouse.y = ev.clientY;
}
</script>
<script lang="ts">
import { onMount } from "svelte";
import { writable } from "svelte/store";
import { scale } from "svelte/transition";
let eye: HTMLDivElement;
let _old_eye: HTMLDivElement;
$: if (eye !== _old_eye) {
if (_old_eye) {
unregister(_old_eye);
}
if (eye) {
register(eye);
}
_old_eye = eye;
}
onMount(() => {
return () => {
unregister(eye);
};
});
</script>
<svelte:window on:mousemove={handleMouseMove} />
<div
class="googley-eyes"
on:click={() => ($visible = !$visible)}
role="button"
tabindex="0"
aria-label="Toggle Googley Eyes"
aria-hidden="true"
on:keydown={(ev) => (ev.key === "Enter" ? ($visible = !$visible) : "")}
>
{#if $visible}
<div class="eye" bind:this={eye} transition:scale>
<div class="pupil"></div>
</div>
{/if}
</div>
<style>
.googley-eyes {
position: relative;
width: 100%;
height: 100%;
display: flex;
width: 50px;
height: 50px;
justify-content: center;
align-items: center;
}
.eye {
position: relative;
overflow: hidden;
width: 50px;
height: 50px;
border-radius: 50%;
background-color: white;
display: flex;
justify-content: center;
align-items: center;
transform: rotate(var(--rotation));
box-shadow: 0px 0px 30px #00000094;
outline: solid 1px #737373;
}
.eye::before {
content: "";
position: absolute;
width: 100%;
height: 100%;
border-radius: 50%;
background-color: rgba(200, 200, 200, 0.01);
transform: rotate(calc(var(--rotation) * -1));
box-shadow:
5px 5px 10px #ffffff70 inset,
1px 1px 4px #ffffff96 inset,
-2px -2px 10px black inset,
2px 2px 5px #00000078;
}
.googley-eyes > :global(.active > .pupil) {
transform: translateY(var(--distance)) scale(0.7);
}
.pupil {
position: absolute;
width: 50%;
height: 50%;
border-radius: 50%;
transition: transform cubic-bezier(0.25, 0.46, 0.45, 0.94) 0.2s;
}
.pupil::after {
content: "";
position: absolute;
width: 100%;
height: 100%;
border-radius: 50%;
background-color: black;
box-shadow:
-5px -5px 10px #ffffff70 inset,
-1px -1px 4px #ffffff96 inset,
2px 2px 2px black inset,
-2px -2px 5px #00000078;
transform: translate(-50%, -50%) rotate(calc(var(--rotation) * -1 + 180deg));
top: 50%;
left: 50%;
}
</style>