feature/comparison-slider #19
@@ -204,37 +204,41 @@ export function createCharacterComparison<
|
|||||||
/**
|
/**
|
||||||
* precise calculation of character state based on global slider position.
|
* precise calculation of character state based on global slider position.
|
||||||
*
|
*
|
||||||
* @param lineIndex - Index of the line
|
|
||||||
* @param charIndex - Index of the character in the line
|
* @param charIndex - Index of the character in the line
|
||||||
* @param lineData - The line data object
|
|
||||||
* @param sliderPos - Current slider position (0-100)
|
* @param sliderPos - Current slider position (0-100)
|
||||||
|
* @param lineElement - The line element
|
||||||
|
* @param container - The container element
|
||||||
* @returns Object containing proximity (0-1) and isPast (boolean)
|
* @returns Object containing proximity (0-1) and isPast (boolean)
|
||||||
*/
|
*/
|
||||||
function getCharState(
|
function getCharState(
|
||||||
lineIndex: number,
|
|
||||||
charIndex: number,
|
charIndex: number,
|
||||||
lineData: LineData,
|
|
||||||
sliderPos: number,
|
sliderPos: number,
|
||||||
|
lineElement?: HTMLElement,
|
||||||
|
container?: HTMLElement,
|
||||||
) {
|
) {
|
||||||
if (!containerWidth) return { proximity: 0, isPast: false };
|
if (!containerWidth || !container) {
|
||||||
|
return {
|
||||||
|
proximity: 0,
|
||||||
|
isPast: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const charElement = lineElement?.children[charIndex] as HTMLElement;
|
||||||
|
|
||||||
// Calculate the pixel position of the character relative to the CONTAINER
|
if (!charElement) {
|
||||||
// 1. Find the left edge of the centered line
|
return { proximity: 0, isPast: false };
|
||||||
const lineStartOffset = (containerWidth - lineData.width) / 2;
|
}
|
||||||
|
|
||||||
// 2. Find the character's center relative to the line
|
// Get the actual bounding box of the character
|
||||||
const charRelativePercent = (charIndex + 0.5) / lineData.text.length;
|
const charRect = charElement.getBoundingClientRect();
|
||||||
const charPixelPos = lineStartOffset + (charRelativePercent * lineData.width);
|
const containerRect = container.getBoundingClientRect();
|
||||||
|
|
||||||
// 3. Convert back to global percentage (0-100)
|
// Calculate character center relative to container
|
||||||
const charGlobalPercent = (charPixelPos / containerWidth) * 100;
|
const charCenter = charRect.left + (charRect.width / 2) - containerRect.left;
|
||||||
|
const charGlobalPercent = (charCenter / containerWidth) * 100;
|
||||||
|
|
||||||
const distance = Math.abs(sliderPos - charGlobalPercent);
|
const distance = Math.abs(sliderPos - charGlobalPercent);
|
||||||
|
const range = 5;
|
||||||
// Proximity range: +/- 15% around the slider
|
|
||||||
const range = 15;
|
|
||||||
const proximity = Math.max(0, 1 - distance / range);
|
const proximity = Math.max(0, 1 - distance / range);
|
||||||
|
|
||||||
const isPast = sliderPos > charGlobalPercent;
|
const isPast = sliderPos > charGlobalPercent;
|
||||||
|
|
||||||
return { proximity, isPast };
|
return { proximity, isPast };
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ const charComparison = createCharacterComparison(
|
|||||||
() => sizeControl.value,
|
() => sizeControl.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let lineElements = $state<(HTMLElement | undefined)[]>([]);
|
||||||
|
|
||||||
/** Physics-based spring for smooth handle movement */
|
/** Physics-based spring for smooth handle movement */
|
||||||
const sliderSpring = new Spring(50, {
|
const sliderSpring = new Spring(50, {
|
||||||
stiffness: 0.2, // Balanced for responsiveness
|
stiffness: 0.2, // Balanced for responsiveness
|
||||||
@@ -138,12 +140,18 @@ $effect(() => {
|
|||||||
|
|
||||||
{#snippet renderLine(line: LineData, lineIndex: number)}
|
{#snippet renderLine(line: LineData, lineIndex: number)}
|
||||||
<div
|
<div
|
||||||
|
bind:this={lineElements[lineIndex]}
|
||||||
class="relative flex w-full justify-center items-center whitespace-nowrap"
|
class="relative flex w-full justify-center items-center whitespace-nowrap"
|
||||||
style:height={`${heightControl.value}em`}
|
style:height={`${heightControl.value}em`}
|
||||||
style:line-height={`${heightControl.value}em`}
|
style:line-height={`${heightControl.value}em`}
|
||||||
>
|
>
|
||||||
{#each line.text.split('') as char, charIndex}
|
{#each line.text.split('') as char, charIndex}
|
||||||
{@const { proximity, isPast } = charComparison.getCharState(lineIndex, charIndex, line, sliderPos)}
|
{@const { proximity, isPast } = charComparison.getCharState(
|
||||||
|
charIndex,
|
||||||
|
sliderPos,
|
||||||
|
lineElements[lineIndex],
|
||||||
|
container,
|
||||||
|
),}
|
||||||
<!--
|
<!--
|
||||||
Single Character Span
|
Single Character Span
|
||||||
- Font Family switches based on `isPast`
|
- Font Family switches based on `isPast`
|
||||||
@@ -177,29 +185,25 @@ $effect(() => {
|
|||||||
aria-label="Font comparison slider"
|
aria-label="Font comparison slider"
|
||||||
onpointerdown={startDragging}
|
onpointerdown={startDragging}
|
||||||
class="
|
class="
|
||||||
group relative w-full py-16 px-6 sm:py-24 sm:px-12 overflow-hidden
|
group relative w-full py-16 px-0 sm:py-24 sm:px-0 overflow-hidden
|
||||||
bg-indigo-50 rounded-[2.5rem] border border-slate-100 shadow-2xl
|
rounded-[2.5rem]
|
||||||
select-none touch-none cursor-ew-resize min-h-100 flex flex-col justify-center
|
select-none touch-none cursor-ew-resize min-h-100 flex flex-col justify-center
|
||||||
|
backdrop-blur-lg bg-gradient-to-br from-gray-100/70 via-white/50 to-gray-100/60
|
||||||
|
border border-gray-300/40
|
||||||
|
shadow-[inset_0_4px_12px_0_rgba(0,0,0,0.12),inset_0_2px_4px_0_rgba(0,0,0,0.08),0_1px_2px_0_rgba(255,255,255,0.8)]
|
||||||
|
before:absolute before:inset-0 before:rounded-[2.5rem] before:p-[1px]
|
||||||
|
before:bg-gradient-to-br before:from-black/5 before:via-black/2 before:to-transparent
|
||||||
|
before:-z-10 before:blur-sm
|
||||||
"
|
"
|
||||||
class:box-shadow={'-20px 20px 60px #bebebe, 20px -20px 60px #ffffff;'}
|
|
||||||
in:fly={{ y: 0, x: -50, duration: 300, easing: cubicOut, opacity: 0.2 }}
|
in:fly={{ y: 0, x: -50, duration: 300, easing: cubicOut, opacity: 0.2 }}
|
||||||
>
|
>
|
||||||
<!-- Background Gradient Accent -->
|
|
||||||
<div
|
|
||||||
class="
|
|
||||||
absolute inset-0 bg-linear-to-br
|
|
||||||
from-slate-50/50 via-white to-slate-100/50
|
|
||||||
opacity-50 pointer-events-none
|
|
||||||
"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Text Rendering Container -->
|
<!-- Text Rendering Container -->
|
||||||
<div
|
<div
|
||||||
class="
|
class="
|
||||||
relative flex flex-col items-center gap-4
|
relative flex flex-col items-center gap-4
|
||||||
text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold leading-[1.15]
|
text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold leading-[1.15]
|
||||||
z-10 pointer-events-none text-center
|
z-10 pointer-events-none text-center
|
||||||
|
drop-shadow-[0_3px_6px_rgba(255,255,255,0.9)]
|
||||||
"
|
"
|
||||||
style:perspective="1000px"
|
style:perspective="1000px"
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user