fix: iterate pre-computed chars array in Line.svelte to fix unicode grapheme splitting bug

This commit is contained in:
Ilia Mashkov
2026-04-11 16:26:41 +03:00
parent 5977e0a0dc
commit 99f662e2d5
2 changed files with 53 additions and 35 deletions

View File

@@ -9,11 +9,12 @@
-->
<script lang="ts">
import {
type CharacterComparison,
type ResponsiveManager,
createCharacterComparison,
debounce,
} from '$shared/lib';
import {
CharacterComparisonEngine,
} from '$shared/lib/helpers/CharacterComparisonEngine/CharacterComparisonEngine.svelte';
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import { Loader } from '$shared/ui';
import { getContext } from 'svelte';
@@ -44,22 +45,16 @@ const isLoading = $derived(comparisonStore.isLoading || !comparisonStore.isReady
const typography = $derived(comparisonStore.typography);
let container = $state<HTMLElement>();
let measureCanvas = $state<HTMLCanvasElement>();
const responsive = getContext<ResponsiveManager>('responsive');
const isMobile = $derived(responsive?.isMobile ?? false);
let isDragging = $state(false);
const charComparison: CharacterComparison = createCharacterComparison(
() => comparisonStore.text,
() => fontA,
() => fontB,
() => typography.weight,
() => typography.renderedSize,
);
// New high-performance layout engine
const comparisonEngine = new CharacterComparisonEngine();
let lineElements = $state<(HTMLElement | undefined)[]>([]);
let layoutResult = $state<ReturnType<typeof comparisonEngine.layout>>({ lines: [], totalHeight: 0 });
const sliderSpring = new Spring(50, {
stiffness: 0.2,
@@ -123,18 +118,41 @@ $effect(() => {
const _weight = typography.weight;
const _size = typography.renderedSize;
const _height = typography.height;
if (container && measureCanvas && fontA && fontB) {
requestAnimationFrame(() => {
charComparison.breakIntoLines(container, measureCanvas);
});
if (container && fontA && fontB) {
// PRETEXT API strings: "weight sizepx family"
const fontAStr = `${_weight} ${_size}px "${fontA.name}"`;
const fontBStr = `${_weight} ${_size}px "${fontB.name}"`;
// Use offsetWidth to avoid transform scaling issues
const width = container.offsetWidth;
const padding = isMobile ? 48 : 96;
const availableWidth = width - padding;
const lineHeight = _size * 1.2; // Approximate
layoutResult = comparisonEngine.layout(
_text,
fontAStr,
fontBStr,
availableWidth,
lineHeight,
);
}
});
$effect(() => {
if (typeof window === 'undefined') return;
const handleResize = () => {
if (container && measureCanvas) {
charComparison.breakIntoLines(container, measureCanvas);
if (container && fontA && fontB) {
const width = container.offsetWidth;
const padding = isMobile ? 48 : 96;
layoutResult = comparisonEngine.layout(
comparisonStore.text,
`${typography.weight} ${typography.renderedSize}px "${fontA.name}"`,
`${typography.weight} ${typography.renderedSize}px "${fontB.name}"`,
width - padding,
typography.renderedSize * 1.2,
);
}
};
window.addEventListener('resize', handleResize);
@@ -156,9 +174,6 @@ const scaleClass = $derived(
);
</script>
<!-- Hidden measurement canvas -->
<canvas bind:this={measureCanvas} class="hidden" width="1" height="1"></canvas>
<!--
Outer flex container — fills parent.
The paper div inside scales down when the sidebar opens on desktop.
@@ -218,10 +233,10 @@ const scaleClass = $derived(
my-auto
"
>
{#each charComparison.lines as line, lineIndex}
<Line bind:element={lineElements[lineIndex]} text={line.text}>
{#each layoutResult.lines as line, lineIndex}
<Line chars={line.chars}>
{#snippet character({ char, index })}
{@const { proximity, isPast } = charComparison.getCharState(index, sliderPos, lineElements[lineIndex], container)}
{@const { proximity, isPast } = comparisonEngine.getCharState(lineIndex, index, sliderPos, container?.offsetWidth ?? 0)}
<Character {char} {proximity} {isPast} />
{/snippet}
</Line>