webspace/src/lib/components/tiles/Tile.svelte

279 lines
6.2 KiB
Svelte
Raw Normal View History

2025-11-30 22:44:47 +05:00
<!--
@component
Metro-like tile. Must be in a group to display correctly.
-->
<script lang="ts">
import randomNumber from "$lib/utils/random-number";
2025-11-30 22:44:47 +05:00
import { cva } from "class-variance-authority";
import { onDestroy, onMount } from "svelte";
2025-11-30 22:44:47 +05:00
/**
* Max angle value.
*/
const MAX_ANGLE = 10;
/**
* Base size of a icon in pixels.
**/
const ICON_BASE_SIZE = 45;
/**
* Icon sizes.
*/
const ICON_SIZES = {
small: ICON_BASE_SIZE,
medium: ICON_BASE_SIZE * 2,
wide: ICON_BASE_SIZE * 2,
large: ICON_BASE_SIZE * 4,
};
/**
* Base size of a page in pixels.
*/
const TILE_BASE_SIZE = 75;
/**
* Page heights.
*/
const PAGE_HEIGHTS = {
small: TILE_BASE_SIZE,
medium: TILE_BASE_SIZE * 2,
wide: TILE_BASE_SIZE * 2,
large: TILE_BASE_SIZE * 4,
};
/**
* Max scroll interval delay in milliseconds.
*/
const SCROLL_INTERVAL_MAX = 7.5 * 1000;
/**
* Min scroll interval delay in milliseconds.
*/
const SCROLL_INTERVAL_MIN = SCROLL_INTERVAL_MAX / 2;
/**
* Tile pages.
*/
let pages: HTMLElement;
/**
* Properties.
*/
2025-11-30 22:44:47 +05:00
let {
size,
row,
icon,
2025-11-30 22:44:47 +05:00
column,
children,
2025-11-30 22:44:47 +05:00
}: {
/**
* Size.
*/
size: "small" | "medium" | "wide" | "large";
/**
* Row index. Must be in a small tiles.
*/
row: number;
/**
* Column index. Must be in a small tiles.
*/
column: number;
/**
* Icon. Must be a valid URL.
*/
icon: string;
2025-11-30 22:44:47 +05:00
/**
* Pages.
2025-11-30 22:44:47 +05:00
*/
children: () => any;
2025-11-30 22:44:47 +05:00
} = $props();
/**
* Tile style.
**/
const style = cva(
[
"relative",
"bg-(--tile-color)",
"perspective-distant",
"active:scale-95",
"active:transform-gpu",
"active:transform-3d",
"active:rotate-x-(--tile-rotate-x)",
"active:rotate-y-(--tile-rotate-y)",
"duration-75",
"ease-in-out",
],
{
variants: {
size: {
small: ["row-span-1", "col-span-1"],
medium: ["row-span-2", "col-span-2"],
wide: ["row-span-2", "col-span-4"],
large: ["row-span-4", "col-span-4"],
},
2025-11-30 22:44:47 +05:00
},
},
);
/**
* Icon size.
*/
const iconSize = ICON_SIZES[size];
/**
* Page height.
*/
const pageHeight = PAGE_HEIGHTS[size];
/**
* Mouse position. Used by border (on mouse over).
*/
let mousePosition = $state({ x: 0, y: 0 });
/**
* Rotation values. Used by transform (on click).
*/
let rotationValues = $state({ x: 0, y: 0 });
/**
* Updates the mouse position-based values.
* @param event `mouseover` event.
*/
function updateDynamicMouseValues(event: MouseEvent) {
const element = event.target as HTMLElement;
const bounds = element.getBoundingClientRect();
mousePosition = {
x: event.clientX - bounds.left,
y: event.clientY - bounds.top,
};
const tileCenterX = bounds.width / 2;
const tileCenterY = bounds.height / 2;
rotationValues.x =
-((mousePosition.y - tileCenterY) / tileCenterY) * MAX_ANGLE;
rotationValues.y =
-((tileCenterX - mousePosition.x) / tileCenterX) * MAX_ANGLE;
}
/**
* Current page number.
*/
let currentPage = 1;
/**
* Scroll orientation.
*/
let scrollOrientation: "up" | "down" = "down";
/**
* Page count.
*/
let pageCount: number;
/**
* Scroll between pages. Used by scroll interval.
*/
function scrollPages() {
// NOTE: Handle the current page and multipler.
let scrollMultipler: number;
if (scrollOrientation === "down") {
scrollMultipler = 1;
currentPage++;
} else {
scrollMultipler = -1;
currentPage--;
}
// NOTE: Scroll the pages.
pages.scrollBy({ top: pageHeight * scrollMultipler });
// NOTE: Change the orientation after scroll.
if (currentPage === pageCount) {
scrollOrientation = "up";
} else if (currentPage === 1) {
scrollOrientation = "down";
}
}
/**
* Scroll interval.
*/
let scrollInterval: NodeJS.Timeout;
onMount(() => {
pageCount = Math.floor(pages.scrollHeight / pageHeight);
if (size === "small" || pageCount === 1) {
return;
}
const scrollDelay = randomNumber(
SCROLL_INTERVAL_MIN,
SCROLL_INTERVAL_MAX,
);
scrollInterval = setInterval(scrollPages, scrollDelay);
});
onDestroy(() => {
if (!scrollInterval) {
return;
}
clearInterval(scrollInterval);
});
2025-11-30 22:44:47 +05:00
</script>
<a
2025-11-30 22:44:47 +05:00
class={style({ size })}
style="
--tile-icon: url('{icon}');
--tile-icon-size: {iconSize}px;
--tile-page-height: {pageHeight}px;
--tile-name-display: {size !== 'small' ? 'inline' : 'none'};
--tile-rotate-x: {rotationValues.x}deg;
--tile-rotate-y: {rotationValues.y}deg;
2025-11-30 22:44:47 +05:00
grid-row-start: {row};
grid-column-start: {column};
2025-11-30 22:44:47 +05:00
"
onmousemove={updateDynamicMouseValues}
href="#h"
>
<!-- Tile pages -->
<div
class="w-full h-full overflow-y-hidden scroll-smooth duration-150"
bind:this={pages}
>
{@render children()}
</div>
<!-- Tile border -->
<div
class="
absolute top-0
w-full h-full
border-2 border-white
opacity-0 hover:opacity-50 hover:mask-(--mask) active:opacity-100
duration-75 ease-in-out
"
style="
--mask: radial-gradient(
{TILE_BASE_SIZE}px
at {mousePosition.x}px {mousePosition.y}px,
black 45%,
transparent
);
"
></div>
</a>