feat(components/tiles): mouse-based effects

This commit is contained in:
mikhail "synzr" 2025-12-01 15:15:55 +05:00
parent 7205033ee5
commit 073676b662
4 changed files with 97 additions and 17 deletions

View file

@ -9,6 +9,11 @@ Metro-like tile. Must be in a group to display correctly.
import { cva } from "class-variance-authority";
import { onDestroy, onMount } from "svelte";
/**
* Max angle value.
*/
const MAX_ANGLE = 10;
/**
* Base size of a icon in pixels.
**/
@ -27,22 +32,22 @@ Metro-like tile. Must be in a group to display correctly.
/**
* Base size of a page in pixels.
*/
const PAGE_BASE_SIZE = 75;
const TILE_BASE_SIZE = 75;
/**
* Page heights.
*/
const PAGE_HEIGHTS = {
small: PAGE_BASE_SIZE,
medium: PAGE_BASE_SIZE * 2,
wide: PAGE_BASE_SIZE * 2,
large: PAGE_BASE_SIZE * 4,
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 = 10 * 1000;
const SCROLL_INTERVAL_MAX = 7.5 * 1000;
/**
* Min scroll interval delay in milliseconds.
@ -50,9 +55,9 @@ Metro-like tile. Must be in a group to display correctly.
const SCROLL_INTERVAL_MIN = SCROLL_INTERVAL_MAX / 2;
/**
* Tile element.
* Tile pages.
*/
let element: HTMLElement;
let pages: HTMLElement;
/**
* Properties.
@ -94,7 +99,18 @@ Metro-like tile. Must be in a group to display correctly.
* Tile style.
**/
const style = cva(
["bg-(--tile-color)", "overflow-y-hidden", "scroll-smooth"],
[
"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: {
@ -117,6 +133,38 @@ Metro-like tile. Must be in a group to display correctly.
*/
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.
*/
@ -147,7 +195,7 @@ Metro-like tile. Must be in a group to display correctly.
}
// NOTE: Scroll the pages.
element.scrollBy({ top: pageHeight * scrollMultipler });
pages.scrollBy({ top: pageHeight * scrollMultipler });
// NOTE: Change the orientation after scroll.
if (currentPage === pageCount) {
@ -163,7 +211,7 @@ Metro-like tile. Must be in a group to display correctly.
let scrollInterval: NodeJS.Timeout;
onMount(() => {
pageCount = Math.floor(element.scrollHeight / pageHeight);
pageCount = Math.floor(pages.scrollHeight / pageHeight);
if (size === "small" || pageCount === 1) {
return;
@ -186,17 +234,45 @@ Metro-like tile. Must be in a group to display correctly.
});
</script>
<div
<a
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;
grid-row-start: {row};
grid-column-start: {column};
"
bind:this={element}
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>

View file

@ -5,6 +5,9 @@ A group of tiles. Places them correctly in a grid.
-->
<script lang="ts">
/**
* Properties.
*/
let {
title,
rows,
@ -43,7 +46,7 @@ A group of tiles. Places them correctly in a grid.
rows *= 2;
</script>
<section>
<section role="group">
<!-- Group title -->
{#if title}
<h1 class="py-2 select-none">{title}</h1>

View file

@ -20,7 +20,7 @@ Image page for a tile. Must be in a tile to display correctly.
style="height: var(--tile-page-height); background-image: url('{image}');"
>
<div
class="w-[20px] mt-auto aspect-square bg-contain bg-center bg-no-repeat self-end m-2"
class="w-5 mt-auto aspect-square bg-contain bg-center bg-no-repeat self-end m-2"
style="background-image: var(--tile-icon);"
></div>
</div>

View file

@ -14,6 +14,7 @@
<TileGroup title="a lot of mikhails" rows={3} columns={3}>
<Tile size="small" row={5} column={2} icon={iconSynzr}>
<TileIconPage name="mikhail" />
<TileTextPage title="Title" subtitle="Subtitle" text="Text" />
</Tile>
<Tile size="medium" row={3} column={5} icon={iconSynzr}>