feat(ComparisonSlider): Improve Comparison slider's readability, incapsulate some code into separate components and snippets
This commit is contained in:
@@ -29,4 +29,5 @@ export {
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
createCharacterComparison,
|
createCharacterComparison,
|
||||||
|
type LineData,
|
||||||
} from './createCharacterComparison/createCharacterComparison.svelte';
|
} from './createCharacterComparison/createCharacterComparison.svelte';
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export {
|
|||||||
type EntityStore,
|
type EntityStore,
|
||||||
type Filter,
|
type Filter,
|
||||||
type FilterModel,
|
type FilterModel,
|
||||||
|
type LineData,
|
||||||
type Property,
|
type Property,
|
||||||
type TypographyControl,
|
type TypographyControl,
|
||||||
type VirtualItem,
|
type VirtualItem,
|
||||||
|
|||||||
@@ -11,7 +11,9 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts" generics="T extends { name: string; id: string }">
|
<script lang="ts" generics="T extends { name: string; id: string }">
|
||||||
import { createCharacterComparison } from '$shared/lib';
|
import { createCharacterComparison } from '$shared/lib';
|
||||||
|
import type { LineData } from '$shared/lib';
|
||||||
import { Spring } from 'svelte/motion';
|
import { Spring } from 'svelte/motion';
|
||||||
|
import CharacterSlot from './components/CharacterSlot.svelte';
|
||||||
import Labels from './components/Labels.svelte';
|
import Labels from './components/Labels.svelte';
|
||||||
import SliderLine from './components/SliderLine.svelte';
|
import SliderLine from './components/SliderLine.svelte';
|
||||||
|
|
||||||
@@ -22,12 +24,15 @@ interface Props<T extends { name: string; id: string }> {
|
|||||||
fontB: T;
|
fontB: T;
|
||||||
/** Text to display and compare */
|
/** Text to display and compare */
|
||||||
text?: string;
|
text?: string;
|
||||||
|
|
||||||
|
weight?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
fontA,
|
fontA,
|
||||||
fontB,
|
fontB,
|
||||||
text = 'The quick brown fox jumps over the lazy dog',
|
text = 'The quick brown fox jumps over the lazy dog',
|
||||||
|
weight = 400,
|
||||||
}: Props<T> = $props();
|
}: Props<T> = $props();
|
||||||
|
|
||||||
let container: HTMLElement | undefined = $state();
|
let container: HTMLElement | undefined = $state();
|
||||||
@@ -42,6 +47,7 @@ const charComparison = createCharacterComparison(
|
|||||||
() => text,
|
() => text,
|
||||||
() => fontA,
|
() => fontA,
|
||||||
() => fontB,
|
() => fontB,
|
||||||
|
() => weight,
|
||||||
);
|
);
|
||||||
|
|
||||||
/** Physics-based spring for smooth handle movement */
|
/** Physics-based spring for smooth handle movement */
|
||||||
@@ -80,7 +86,7 @@ $effect(() => {
|
|||||||
|
|
||||||
// Re-run line breaking when container resizes or dependencies change
|
// Re-run line breaking when container resizes or dependencies change
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (container && measureCanvas) {
|
if (container && measureCanvas && weight && fontA && fontB) {
|
||||||
// Using rAF to ensure DOM is ready/stabilized
|
// Using rAF to ensure DOM is ready/stabilized
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
charComparison.breakIntoLines(container, measureCanvas);
|
charComparison.breakIntoLines(container, measureCanvas);
|
||||||
@@ -91,7 +97,7 @@ $effect(() => {
|
|||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
if (container && measureCanvas) {
|
if (container && measureCanvas && weight) {
|
||||||
charComparison.breakIntoLines(container, measureCanvas);
|
charComparison.breakIntoLines(container, measureCanvas);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -100,6 +106,30 @@ $effect(() => {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{#snippet renderLine(line: LineData, lineIndex: number)}
|
||||||
|
<div
|
||||||
|
class="relative flex w-full justify-center items-center whitespace-nowrap"
|
||||||
|
style:height="1.2em"
|
||||||
|
>
|
||||||
|
{#each line.text.split('') as char, charIndex}
|
||||||
|
{@const { proximity, isPast } = charComparison.getCharState(lineIndex, charIndex, line, sliderPos)}
|
||||||
|
<!--
|
||||||
|
Single Character Span
|
||||||
|
- Font Family switches based on `isPast`
|
||||||
|
- Transitions/Transforms provide the "morph" feel
|
||||||
|
-->
|
||||||
|
<CharacterSlot
|
||||||
|
{char}
|
||||||
|
{proximity}
|
||||||
|
{isPast}
|
||||||
|
{weight}
|
||||||
|
fontAName={fontA.name}
|
||||||
|
fontBName={fontB.name}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
<!-- Hidden canvas used for text measurement by the helper -->
|
<!-- Hidden canvas used for text measurement by the helper -->
|
||||||
<canvas bind:this={measureCanvas} class="hidden" width="1" height="1"></canvas>
|
<canvas bind:this={measureCanvas} class="hidden" width="1" height="1"></canvas>
|
||||||
|
|
||||||
@@ -110,7 +140,11 @@ $effect(() => {
|
|||||||
aria-valuenow={Math.round(sliderPos)}
|
aria-valuenow={Math.round(sliderPos)}
|
||||||
aria-label="Font comparison slider"
|
aria-label="Font comparison slider"
|
||||||
onpointerdown={startDragging}
|
onpointerdown={startDragging}
|
||||||
class="group relative w-full py-16 px-6 sm:py-24 sm:px-12 overflow-hidden bg-white rounded-[2.5rem] border border-slate-100 shadow-2xl select-none touch-none cursor-ew-resize min-h-100 flex flex-col justify-center"
|
class="
|
||||||
|
group relative w-full py-16 px-6 sm:py-24 sm:px-12 overflow-hidden
|
||||||
|
bg-white rounded-[2.5rem] border border-slate-100 shadow-2xl
|
||||||
|
select-none touch-none cursor-ew-resize min-h-100 flex flex-col justify-center
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<!-- Background Gradient Accent -->
|
<!-- Background Gradient Accent -->
|
||||||
<div
|
<div
|
||||||
@@ -124,59 +158,27 @@ $effect(() => {
|
|||||||
|
|
||||||
<!-- Text Rendering Container -->
|
<!-- Text Rendering Container -->
|
||||||
<div
|
<div
|
||||||
class="relative flex flex-col items-center gap-4 text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold leading-[1.15] z-10 pointer-events-none text-center"
|
class="
|
||||||
|
relative flex flex-col items-center gap-4
|
||||||
|
text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold leading-[1.15]
|
||||||
|
z-10 pointer-events-none text-center
|
||||||
|
"
|
||||||
style:perspective="1000px"
|
style:perspective="1000px"
|
||||||
>
|
>
|
||||||
{#each charComparison.lines as line, lineIndex}
|
{#each charComparison.lines as line, lineIndex}
|
||||||
<div class="relative w-full whitespace-nowrap">
|
<div
|
||||||
{#each line.text.split('') as char, charIndex}
|
class="relative w-full whitespace-nowrap"
|
||||||
{@const { proximity, isPast } = charComparison.getCharState(lineIndex, charIndex, line, sliderPos)}
|
style:height="1.2em"
|
||||||
<!--
|
style:display="flex"
|
||||||
Single Character Span
|
style:align-items="center"
|
||||||
- Font Family switches based on `isPast`
|
style:justify-content="center"
|
||||||
- Transitions/Transforms provide the "morph" feel
|
>
|
||||||
-->
|
{@render renderLine(line, lineIndex)}
|
||||||
<span
|
|
||||||
class="inline-block transition-all duration-300 ease-out will-change-transform"
|
|
||||||
style:font-family={isPast ? fontB.name : fontA.name}
|
|
||||||
style:color={isPast
|
|
||||||
? 'rgb(79, 70, 229)'
|
|
||||||
: 'rgb(15, 23, 42)'}
|
|
||||||
style:transform="
|
|
||||||
scale({1 + proximity * 0.2}) translateY({-proximity *
|
|
||||||
12}px) rotateY({proximity *
|
|
||||||
25 *
|
|
||||||
(isPast ? -1 : 1)}deg)
|
|
||||||
"
|
|
||||||
style:will-change={proximity > 0
|
|
||||||
? 'transform, font-family, color'
|
|
||||||
: 'auto'}
|
|
||||||
>
|
|
||||||
{char === ' ' ? '\u00A0' : char}
|
|
||||||
</span>
|
|
||||||
{/each}
|
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Visual Components -->
|
<!-- Visual Components -->
|
||||||
<SliderLine {sliderPos} />
|
<SliderLine {sliderPos} isDragging={isDragging} />
|
||||||
<Labels {fontA} {fontB} {sliderPos} />
|
<Labels {fontA} {fontB} {sliderPos} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
|
||||||
span {
|
|
||||||
/*
|
|
||||||
Optimize for performance and smooth transitions.
|
|
||||||
step-end logic is effectively handled by binary font switching in JS.
|
|
||||||
*/
|
|
||||||
transition:
|
|
||||||
font-family 0.15s ease-out,
|
|
||||||
color 0.2s ease-out,
|
|
||||||
transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
||||||
transform-style: preserve-3d;
|
|
||||||
backface-visibility: hidden;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<!--
|
||||||
|
Component: CharacterSlot
|
||||||
|
Renders a character with particular styling based on proximity, isPast, weight, fontAName, and fontBName.
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
char: string;
|
||||||
|
proximity: number;
|
||||||
|
isPast: boolean;
|
||||||
|
weight: number;
|
||||||
|
fontAName: string;
|
||||||
|
fontBName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { char, proximity, isPast, weight, fontAName, fontBName }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class={cn(
|
||||||
|
'inline-block transition-all duration-300 ease-out will-change-transform',
|
||||||
|
isPast ? 'text-indigo-500' : 'text-neutral-950',
|
||||||
|
)}
|
||||||
|
style:font-family={isPast ? fontBName : fontAName}
|
||||||
|
style:font-weight={weight}
|
||||||
|
style:transform="
|
||||||
|
scale({1 + proximity * 0.2})
|
||||||
|
translateY({-proximity * 12}px)
|
||||||
|
rotateY({proximity * 25 * (isPast ? -1 : 1)}deg)
|
||||||
|
"
|
||||||
|
style:filter="brightness({1 + proximity * 0.2}) contrast({1 + proximity * 0.1})"
|
||||||
|
style:text-shadow={proximity > 0.5 ? '0 0 15px rgba(99,102,241,0.3)' : 'none'}
|
||||||
|
style:will-change={proximity > 0 ? 'transform, font-family, color' : 'auto'}
|
||||||
|
>
|
||||||
|
{char === ' ' ? '\u00A0' : char}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
span {
|
||||||
|
/*
|
||||||
|
Optimize for performance and smooth transitions.
|
||||||
|
step-end logic is effectively handled by binary font switching in JS.
|
||||||
|
*/
|
||||||
|
transition:
|
||||||
|
font-family 0.15s ease-out,
|
||||||
|
color 0.2s ease-out,
|
||||||
|
transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user