179 lines
4.0 KiB
Svelte
179 lines
4.0 KiB
Svelte
|
<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>
|