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 { cva } from "class-variance-authority";
import { onDestroy, onMount } from "svelte"; import { onDestroy, onMount } from "svelte";
/**
* Max angle value.
*/
const MAX_ANGLE = 10;
/** /**
* Base size of a icon in pixels. * 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. * Base size of a page in pixels.
*/ */
const PAGE_BASE_SIZE = 75; const TILE_BASE_SIZE = 75;
/** /**
* Page heights. * Page heights.
*/ */
const PAGE_HEIGHTS = { const PAGE_HEIGHTS = {
small: PAGE_BASE_SIZE, small: TILE_BASE_SIZE,
medium: PAGE_BASE_SIZE * 2, medium: TILE_BASE_SIZE * 2,
wide: PAGE_BASE_SIZE * 2, wide: TILE_BASE_SIZE * 2,
large: PAGE_BASE_SIZE * 4, large: TILE_BASE_SIZE * 4,
}; };
/** /**
* Max scroll interval delay in milliseconds. * 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. * 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; const SCROLL_INTERVAL_MIN = SCROLL_INTERVAL_MAX / 2;
/** /**
* Tile element. * Tile pages.
*/ */
let element: HTMLElement; let pages: HTMLElement;
/** /**
* Properties. * Properties.
@ -94,7 +99,18 @@ Metro-like tile. Must be in a group to display correctly.
* Tile style. * Tile style.
**/ **/
const style = cva( 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: { variants: {
size: { size: {
@ -117,6 +133,38 @@ Metro-like tile. Must be in a group to display correctly.
*/ */
const pageHeight = PAGE_HEIGHTS[size]; 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. * Current page number.
*/ */
@ -147,7 +195,7 @@ Metro-like tile. Must be in a group to display correctly.
} }
// NOTE: Scroll the pages. // NOTE: Scroll the pages.
element.scrollBy({ top: pageHeight * scrollMultipler }); pages.scrollBy({ top: pageHeight * scrollMultipler });
// NOTE: Change the orientation after scroll. // NOTE: Change the orientation after scroll.
if (currentPage === pageCount) { if (currentPage === pageCount) {
@ -163,7 +211,7 @@ Metro-like tile. Must be in a group to display correctly.
let scrollInterval: NodeJS.Timeout; let scrollInterval: NodeJS.Timeout;
onMount(() => { onMount(() => {
pageCount = Math.floor(element.scrollHeight / pageHeight); pageCount = Math.floor(pages.scrollHeight / pageHeight);
if (size === "small" || pageCount === 1) { if (size === "small" || pageCount === 1) {
return; return;
@ -186,17 +234,45 @@ Metro-like tile. Must be in a group to display correctly.
}); });
</script> </script>
<div <a
class={style({ size })} class={style({ size })}
style=" style="
--tile-icon: url('{icon}'); --tile-icon: url('{icon}');
--tile-icon-size: {iconSize}px; --tile-icon-size: {iconSize}px;
--tile-page-height: {pageHeight}px; --tile-page-height: {pageHeight}px;
--tile-name-display: {size !== 'small' ? 'inline' : 'none'}; --tile-name-display: {size !== 'small' ? 'inline' : 'none'};
--tile-rotate-x: {rotationValues.x}deg;
--tile-rotate-y: {rotationValues.y}deg;
grid-row-start: {row}; grid-row-start: {row};
grid-column-start: {column}; grid-column-start: {column};
" "
bind:this={element} onmousemove={updateDynamicMouseValues}
href="#h"
> >
{@render children()} <!-- Tile pages -->
</div> <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"> <script lang="ts">
/**
* Properties.
*/
let { let {
title, title,
rows, rows,
@ -43,7 +46,7 @@ A group of tiles. Places them correctly in a grid.
rows *= 2; rows *= 2;
</script> </script>
<section> <section role="group">
<!-- Group title --> <!-- Group title -->
{#if title} {#if title}
<h1 class="py-2 select-none">{title}</h1> <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}');" style="height: var(--tile-page-height); background-image: url('{image}');"
> >
<div <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);" style="background-image: var(--tile-icon);"
></div> ></div>
</div> </div>

View file

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