fix(ComparisonView): fix character morphing thresholds and add tracking support
This commit is contained in:
+83
-45
@@ -95,11 +95,13 @@ export class CharacterComparisonEngine {
|
|||||||
#lastText = '';
|
#lastText = '';
|
||||||
#lastFontA = '';
|
#lastFontA = '';
|
||||||
#lastFontB = '';
|
#lastFontB = '';
|
||||||
|
#lastSpacing = 0;
|
||||||
|
#lastSize = 0;
|
||||||
|
|
||||||
// Cached layout results
|
// Cached layout results
|
||||||
#lastWidth = -1;
|
#lastWidth = -1;
|
||||||
#lastLineHeight = -1;
|
#lastLineHeight = -1;
|
||||||
#lastResult: ComparisonResult | null = null;
|
#lastResult = $state<ComparisonResult | null>(null);
|
||||||
|
|
||||||
constructor(locale?: string) {
|
constructor(locale?: string) {
|
||||||
this.#segmenter = new Intl.Segmenter(locale, { granularity: 'grapheme' });
|
this.#segmenter = new Intl.Segmenter(locale, { granularity: 'grapheme' });
|
||||||
@@ -116,6 +118,8 @@ export class CharacterComparisonEngine {
|
|||||||
* @param fontB CSS font string for the second font: `"weight sizepx \"family\""`.
|
* @param fontB CSS font string for the second font: `"weight sizepx \"family\""`.
|
||||||
* @param width Available line width in pixels.
|
* @param width Available line width in pixels.
|
||||||
* @param lineHeight Line height in pixels (passed directly to pretext).
|
* @param lineHeight Line height in pixels (passed directly to pretext).
|
||||||
|
* @param spacing Letter spacing in em (from typography settings).
|
||||||
|
* @param size Current font size in pixels (used to convert spacing em to px).
|
||||||
* @returns Per-line grapheme data for both fonts. Empty `lines` when `text` is empty.
|
* @returns Per-line grapheme data for both fonts. Empty `lines` when `text` is empty.
|
||||||
*/
|
*/
|
||||||
layout(
|
layout(
|
||||||
@@ -124,12 +128,21 @@ export class CharacterComparisonEngine {
|
|||||||
fontB: string,
|
fontB: string,
|
||||||
width: number,
|
width: number,
|
||||||
lineHeight: number,
|
lineHeight: number,
|
||||||
|
spacing: number = 0,
|
||||||
|
size: number = 16,
|
||||||
): ComparisonResult {
|
): ComparisonResult {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return { lines: [], totalHeight: 0 };
|
return { lines: [], totalHeight: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFontChange = text !== this.#lastText || fontA !== this.#lastFontA || fontB !== this.#lastFontB;
|
const spacingPx = spacing * size;
|
||||||
|
|
||||||
|
const isFontChange = text !== this.#lastText
|
||||||
|
|| fontA !== this.#lastFontA
|
||||||
|
|| fontB !== this.#lastFontB
|
||||||
|
|| spacing !== this.#lastSpacing
|
||||||
|
|| size !== this.#lastSize;
|
||||||
|
|
||||||
const isLayoutChange = width !== this.#lastWidth || lineHeight !== this.#lastLineHeight;
|
const isLayoutChange = width !== this.#lastWidth || lineHeight !== this.#lastLineHeight;
|
||||||
|
|
||||||
if (!isFontChange && !isLayoutChange && this.#lastResult) {
|
if (!isFontChange && !isLayoutChange && this.#lastResult) {
|
||||||
@@ -140,11 +153,13 @@ export class CharacterComparisonEngine {
|
|||||||
if (isFontChange) {
|
if (isFontChange) {
|
||||||
this.#preparedA = prepareWithSegments(text, fontA);
|
this.#preparedA = prepareWithSegments(text, fontA);
|
||||||
this.#preparedB = prepareWithSegments(text, fontB);
|
this.#preparedB = prepareWithSegments(text, fontB);
|
||||||
this.#unifiedPrepared = this.#createUnifiedPrepared(this.#preparedA, this.#preparedB);
|
this.#unifiedPrepared = this.#createUnifiedPrepared(this.#preparedA, this.#preparedB, spacingPx);
|
||||||
|
|
||||||
this.#lastText = text;
|
this.#lastText = text;
|
||||||
this.#lastFontA = fontA;
|
this.#lastFontA = fontA;
|
||||||
this.#lastFontB = fontB;
|
this.#lastFontB = fontB;
|
||||||
|
this.#lastSpacing = spacing;
|
||||||
|
this.#lastSize = size;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.#unifiedPrepared || !this.#preparedA || !this.#preparedB) {
|
if (!this.#unifiedPrepared || !this.#preparedA || !this.#preparedB) {
|
||||||
@@ -175,7 +190,6 @@ export class CharacterComparisonEngine {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// PERFORMANCE: Reuse segmenter results if possible, but for now just optimize the loop
|
|
||||||
const graphemes = Array.from(this.#segmenter.segment(segmentText), s => s.segment);
|
const graphemes = Array.from(this.#segmenter.segment(segmentText), s => s.segment);
|
||||||
|
|
||||||
const advA = intA.breakableFitAdvances[sIdx];
|
const advA = intA.breakableFitAdvances[sIdx];
|
||||||
@@ -186,8 +200,12 @@ export class CharacterComparisonEngine {
|
|||||||
|
|
||||||
for (let gIdx = gStart; gIdx < gEnd; gIdx++) {
|
for (let gIdx = gStart; gIdx < gEnd; gIdx++) {
|
||||||
const char = graphemes[gIdx];
|
const char = graphemes[gIdx];
|
||||||
const wA = advA != null ? advA[gIdx]! : intA.widths[sIdx]!;
|
let wA = advA != null ? advA[gIdx]! : intA.widths[sIdx]!;
|
||||||
const wB = advB != null ? advB[gIdx]! : intB.widths[sIdx]!;
|
let wB = advB != null ? advB[gIdx]! : intB.widths[sIdx]!;
|
||||||
|
|
||||||
|
// Apply letter spacing (tracking) to the width of each character
|
||||||
|
wA += spacingPx;
|
||||||
|
wB += spacingPx;
|
||||||
|
|
||||||
chars.push({
|
chars.push({
|
||||||
char,
|
char,
|
||||||
@@ -219,66 +237,86 @@ export class CharacterComparisonEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates character proximity and direction relative to a slider position.
|
* Calculates character states for an entire line in a single sequential pass.
|
||||||
*
|
*
|
||||||
* Uses the most recent `layout()` result — must be called after `layout()`.
|
* Walks characters left-to-right, accumulating the running x position using
|
||||||
* No DOM calls are made; all geometry is derived from cached layout data.
|
* each character's actual rendered width: `widthB` for already-morphed characters
|
||||||
|
* (isPast=true) and `widthA` for upcoming ones. This ensures thresholds stay
|
||||||
|
* aligned with the visual DOM layout even when the two fonts have different widths.
|
||||||
*
|
*
|
||||||
* @param lineIndex Zero-based index of the line within the last layout result.
|
* @param line A single laid-out line from the last layout result.
|
||||||
* @param charIndex Zero-based index of the character within that line's `chars` array.
|
|
||||||
* @param sliderPos Current slider position as a percentage (0–100) of `containerWidth`.
|
* @param sliderPos Current slider position as a percentage (0–100) of `containerWidth`.
|
||||||
* @param containerWidth Total container width in pixels, used to convert pixel offsets to %.
|
* @param containerWidth Total container width in pixels.
|
||||||
* @returns `proximity` in [0, 1] (1 = slider exactly over char center) and
|
* @returns Per-character `proximity` and `isPast` in the same order as `line.chars`.
|
||||||
* `isPast` (true when the slider has already passed the char center).
|
|
||||||
*/
|
*/
|
||||||
getCharState(
|
getLineCharStates(
|
||||||
lineIndex: number,
|
line: ComparisonLine,
|
||||||
charIndex: number,
|
|
||||||
sliderPos: number,
|
sliderPos: number,
|
||||||
containerWidth: number,
|
containerWidth: number,
|
||||||
): { proximity: number; isPast: boolean } {
|
): Array<{ proximity: number; isPast: boolean }> {
|
||||||
if (!this.#lastResult || !this.#lastResult.lines[lineIndex]) {
|
if (!line) {
|
||||||
return { proximity: 0, isPast: false };
|
return [];
|
||||||
}
|
}
|
||||||
|
const chars = line.chars;
|
||||||
const line = this.#lastResult.lines[lineIndex];
|
const n = chars.length;
|
||||||
const char = line.chars[charIndex];
|
const sliderX = (sliderPos / 100) * containerWidth;
|
||||||
|
|
||||||
if (!char) {
|
|
||||||
return { proximity: 0, isPast: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Center the comparison on the unified width
|
|
||||||
// In the UI, lines are centered. So we need to calculate the global X.
|
|
||||||
const lineXOffset = (containerWidth - line.width) / 2;
|
|
||||||
const charCenterX = lineXOffset + char.xA + (char.widthA / 2);
|
|
||||||
|
|
||||||
const charGlobalPercent = (charCenterX / containerWidth) * 100;
|
|
||||||
|
|
||||||
const distance = Math.abs(sliderPos - charGlobalPercent);
|
|
||||||
const range = 5;
|
const range = 5;
|
||||||
|
// Prefix sums of widthA (left chars will be past → use widthA).
|
||||||
|
// Suffix sums of widthB (right chars will not be past → use widthB).
|
||||||
|
// This lets us compute, for each char i, what the total line width and
|
||||||
|
// char center would be at the exact moment the slider crosses that char:
|
||||||
|
// left side (0..i-1) already past → font A widths
|
||||||
|
// right side (i+1..n-1) not yet past → font B widths
|
||||||
|
const prefA = new Float64Array(n + 1);
|
||||||
|
const sufB = new Float64Array(n + 1);
|
||||||
|
for (let i = 0; i < n; i++) { prefA[i + 1] = prefA[i] + chars[i].widthA; }
|
||||||
|
for (let i = n - 1; i >= 0; i--) { sufB[i] = sufB[i + 1] + chars[i].widthB; }
|
||||||
|
// Per-char threshold: slider x at which this char should toggle isPast.
|
||||||
|
const thresholds = new Float64Array(n);
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const totalWidth = prefA[i] + chars[i].widthA + sufB[i + 1];
|
||||||
|
const xOffset = (containerWidth - totalWidth) / 2;
|
||||||
|
thresholds[i] = xOffset + prefA[i] + chars[i].widthA / 2;
|
||||||
|
}
|
||||||
|
// Determine isPast for each char at the current slider position.
|
||||||
|
const isPastArr = new Uint8Array(n);
|
||||||
|
for (let i = 0; i < n; i++) { isPastArr[i] = sliderX > thresholds[i] ? 1 : 0; }
|
||||||
|
// Compute visual positions based on actual rendered widths (font A if past, B if not).
|
||||||
|
const totalRendered = chars.reduce((s, c, i) => s + (isPastArr[i] ? c.widthA : c.widthB), 0);
|
||||||
|
const xOffset = (containerWidth - totalRendered) / 2;
|
||||||
|
let currentX = xOffset;
|
||||||
|
return chars.map((char, i) => {
|
||||||
|
const isPast = isPastArr[i] === 1;
|
||||||
|
const charWidth = isPast ? char.widthA : char.widthB;
|
||||||
|
const visualCenter = currentX + charWidth / 2;
|
||||||
|
const charGlobalPercent = (visualCenter / containerWidth) * 100;
|
||||||
|
const distance = Math.abs(sliderPos - charGlobalPercent);
|
||||||
const proximity = Math.max(0, 1 - distance / range);
|
const proximity = Math.max(0, 1 - distance / range);
|
||||||
const isPast = sliderPos > charGlobalPercent;
|
currentX += charWidth;
|
||||||
|
|
||||||
return { proximity, isPast };
|
return { proximity, isPast };
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal helper to merge two prepared texts into a "worst-case" unified version
|
* Internal helper to merge two prepared texts into a "worst-case" unified version
|
||||||
*/
|
*/
|
||||||
#createUnifiedPrepared(a: PreparedTextWithSegments, b: PreparedTextWithSegments): PreparedTextWithSegments {
|
#createUnifiedPrepared(
|
||||||
|
a: PreparedTextWithSegments,
|
||||||
|
b: PreparedTextWithSegments,
|
||||||
|
spacingPx: number = 0,
|
||||||
|
): PreparedTextWithSegments {
|
||||||
// Cast to `any`: accessing internal numeric arrays not in the public type signature.
|
// Cast to `any`: accessing internal numeric arrays not in the public type signature.
|
||||||
const intA = a as any;
|
const intA = a as any;
|
||||||
const intB = b as any;
|
const intB = b as any;
|
||||||
|
|
||||||
const unified = { ...intA };
|
const unified = { ...intA };
|
||||||
|
|
||||||
unified.widths = intA.widths.map((w: number, i: number) => Math.max(w, intB.widths[i]));
|
unified.widths = intA.widths.map((w: number, i: number) => Math.max(w, intB.widths[i]) + spacingPx);
|
||||||
unified.lineEndFitAdvances = intA.lineEndFitAdvances.map((w: number, i: number) =>
|
unified.lineEndFitAdvances = intA.lineEndFitAdvances.map((w: number, i: number) =>
|
||||||
Math.max(w, intB.lineEndFitAdvances[i])
|
Math.max(w, intB.lineEndFitAdvances[i]) + spacingPx
|
||||||
);
|
);
|
||||||
unified.lineEndPaintAdvances = intA.lineEndPaintAdvances.map((w: number, i: number) =>
|
unified.lineEndPaintAdvances = intA.lineEndPaintAdvances.map((w: number, i: number) =>
|
||||||
Math.max(w, intB.lineEndPaintAdvances[i])
|
Math.max(w, intB.lineEndPaintAdvances[i]) + spacingPx
|
||||||
);
|
);
|
||||||
|
|
||||||
unified.breakableFitAdvances = intA.breakableFitAdvances.map((advA: number[] | null, i: number) => {
|
unified.breakableFitAdvances = intA.breakableFitAdvances.map((advA: number[] | null, i: number) => {
|
||||||
@@ -287,13 +325,13 @@ export class CharacterComparisonEngine {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (!advA) {
|
if (!advA) {
|
||||||
return advB;
|
return advB.map((w: number) => w + spacingPx);
|
||||||
}
|
}
|
||||||
if (!advB) {
|
if (!advB) {
|
||||||
return advA;
|
return advA.map((w: number) => w + spacingPx);
|
||||||
}
|
}
|
||||||
|
|
||||||
return advA.map((w: number, j: number) => Math.max(w, advB[j]));
|
return advA.map((w: number, j: number) => Math.max(w, advB[j]) + spacingPx);
|
||||||
});
|
});
|
||||||
|
|
||||||
return unified;
|
return unified;
|
||||||
|
|||||||
+35
-39
@@ -109,56 +109,52 @@ describe('CharacterComparisonEngine', () => {
|
|||||||
expect(r2).not.toBe(r1);
|
expect(r2).not.toBe(r1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getCharState returns proximity 1 when slider is exactly over char center', () => {
|
it('getLineCharStates returns proximity 1 when slider is exactly over char center', () => {
|
||||||
// 'A' only: FontA width=10. Container=500px. Line centered.
|
|
||||||
// lineXOffset = (500 - maxWidth) / 2. maxWidth = max(10, 15) = 15 (FontB is wider).
|
|
||||||
// charCenterX = lineXOffset + xA + widthA/2.
|
|
||||||
// Using xA=0, widthA=10: charCenterX = (500-15)/2 + 0 + 5 = 247.5 + 5 = 252.5
|
|
||||||
// charGlobalPercent = (252.5 / 500) * 100 = 50.5
|
|
||||||
// distance = |50.5 - 50.5| = 0 => proximity = 1
|
|
||||||
const containerWidth = 500;
|
const containerWidth = 500;
|
||||||
engine.layout('A', '400 16px "FontA"', '400 16px "FontB"', containerWidth, 20);
|
const result = engine.layout('A', '400 16px "FontA"', '400 16px "FontB"', containerWidth, 20);
|
||||||
// Recalculate expected percent manually:
|
// Single char: no neighbors → totalWidth = widthA, threshold = containerWidth/2.
|
||||||
const lineWidth = Math.max(FONT_A_WIDTH, FONT_B_WIDTH); // 15 (unified worst-case)
|
// When isPast=false, visual center = (containerWidth - widthB)/2 + widthB/2 = containerWidth/2.
|
||||||
const lineXOffset = (containerWidth - lineWidth) / 2;
|
// So proximity=1 at exactly 50%.
|
||||||
const charCenterX = lineXOffset + 0 + FONT_A_WIDTH / 2;
|
const charPercent = 50;
|
||||||
const charPercent = (charCenterX / containerWidth) * 100;
|
|
||||||
|
|
||||||
const state = engine.getCharState(0, 0, charPercent, containerWidth);
|
const states = engine.getLineCharStates(result.lines[0], charPercent, containerWidth);
|
||||||
expect(state.proximity).toBe(1);
|
expect(states[0]?.proximity).toBe(1);
|
||||||
expect(state.isPast).toBe(false);
|
expect(states[0]?.isPast).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getCharState returns proximity 0 when slider is far from char', () => {
|
it('getLineCharStates returns proximity 0 when slider is far from char', () => {
|
||||||
engine.layout('A', '400 16px "FontA"', '400 16px "FontB"', 500, 20);
|
const result = engine.layout('A', '400 16px "FontA"', '400 16px "FontB"', 500, 20);
|
||||||
// Slider at 0%, char is near 50% — distance > 5 range => proximity = 0
|
const states = engine.getLineCharStates(result.lines[0], 0, 500);
|
||||||
const state = engine.getCharState(0, 0, 0, 500);
|
expect(states[0]?.proximity).toBe(0);
|
||||||
expect(state.proximity).toBe(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getCharState isPast is true when slider has passed char center', () => {
|
it('getLineCharStates isPast is true when slider has passed char center', () => {
|
||||||
engine.layout('A', '400 16px "FontA"', '400 16px "FontB"', 500, 20);
|
const result = engine.layout('A', '400 16px "FontA"', '400 16px "FontB"', 500, 20);
|
||||||
const state = engine.getCharState(0, 0, 100, 500);
|
const states = engine.getLineCharStates(result.lines[0], 100, 500);
|
||||||
expect(state.isPast).toBe(true);
|
expect(states[0]?.isPast).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getCharState returns safe default for out-of-range lineIndex', () => {
|
it('getLineCharStates returns empty array for out-of-range lineIndex', () => {
|
||||||
engine.layout('A', '400 16px "FontA"', '400 16px "FontB"', 500, 20);
|
const result = engine.layout('A', '400 16px "FontA"', '400 16px "FontB"', 500, 20);
|
||||||
const state = engine.getCharState(99, 0, 50, 500);
|
// Passing an undefined object because the index doesn't exist.
|
||||||
expect(state.proximity).toBe(0);
|
const states = engine.getLineCharStates(result.lines[99], 50, 500);
|
||||||
expect(state.isPast).toBe(false);
|
expect(states).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getCharState returns safe default for out-of-range charIndex', () => {
|
it('getLineCharStates returns empty array before layout() has been called', () => {
|
||||||
engine.layout('A', '400 16px "FontA"', '400 16px "FontB"', 500, 20);
|
// Passing an undefined object because layout() hasn't been called.
|
||||||
const state = engine.getCharState(0, 99, 50, 500);
|
const states = engine.getLineCharStates(undefined as any, 50, 500);
|
||||||
expect(state.proximity).toBe(0);
|
expect(states).toEqual([]);
|
||||||
expect(state.isPast).toBe(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getCharState returns safe default before layout() has been called', () => {
|
it('getLineCharStates returns safe defaults for all chars', () => {
|
||||||
const state = engine.getCharState(0, 0, 50, 500);
|
const result = engine.layout('A', '400 16px "FontA"', '400 16px "FontB"', 500, 20);
|
||||||
expect(state.proximity).toBe(0);
|
const states = engine.getLineCharStates(result.lines[0], 50, 500);
|
||||||
expect(state.isPast).toBe(false);
|
expect(states.length).toBeGreaterThan(0);
|
||||||
|
for (const s of states) {
|
||||||
|
expect(s.proximity).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(s.proximity).toBeLessThanOrEqual(1);
|
||||||
|
expect(typeof s.isPast).toBe('boolean');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ $effect(() => {
|
|||||||
<span
|
<span
|
||||||
class="char-wrap"
|
class="char-wrap"
|
||||||
style:font-size="{typography.renderedSize}px"
|
style:font-size="{typography.renderedSize}px"
|
||||||
|
style:margin-right="{typography.spacing}em"
|
||||||
style:will-change={proximity > 0 ? 'transform' : 'auto'}
|
style:will-change={proximity > 0 ? 'transform' : 'auto'}
|
||||||
>
|
>
|
||||||
{#each [0, 1] as s (s)}
|
{#each [0, 1] as s (s)}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ const isMobile = $derived(responsive?.isMobile ?? false);
|
|||||||
|
|
||||||
let isDragging = $state(false);
|
let isDragging = $state(false);
|
||||||
let isTypographyMenuOpen = $state(false);
|
let isTypographyMenuOpen = $state(false);
|
||||||
|
let containerWidth = $state(0);
|
||||||
|
|
||||||
// New high-performance layout engine
|
// New high-performance layout engine
|
||||||
const comparisonEngine = new CharacterComparisonEngine();
|
const comparisonEngine = new CharacterComparisonEngine();
|
||||||
@@ -127,6 +128,7 @@ $effect(() => {
|
|||||||
const _weight = typography.weight;
|
const _weight = typography.weight;
|
||||||
const _size = typography.renderedSize;
|
const _size = typography.renderedSize;
|
||||||
const _height = typography.height;
|
const _height = typography.height;
|
||||||
|
const _spacing = typography.spacing;
|
||||||
|
|
||||||
if (container && fontA && fontB) {
|
if (container && fontA && fontB) {
|
||||||
// PRETEXT API strings: "weight sizepx family"
|
// PRETEXT API strings: "weight sizepx family"
|
||||||
@@ -137,14 +139,17 @@ $effect(() => {
|
|||||||
const width = container.offsetWidth;
|
const width = container.offsetWidth;
|
||||||
const padding = isMobile ? 48 : 96;
|
const padding = isMobile ? 48 : 96;
|
||||||
const availableWidth = width - padding;
|
const availableWidth = width - padding;
|
||||||
const lineHeight = _size * 1.2; // Approximate
|
const lineHeight = _size * _height;
|
||||||
|
|
||||||
|
containerWidth = width;
|
||||||
layoutResult = comparisonEngine.layout(
|
layoutResult = comparisonEngine.layout(
|
||||||
_text,
|
_text,
|
||||||
fontAStr,
|
fontAStr,
|
||||||
fontBStr,
|
fontBStr,
|
||||||
availableWidth,
|
availableWidth,
|
||||||
lineHeight,
|
lineHeight,
|
||||||
|
_spacing,
|
||||||
|
_size,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -157,12 +162,15 @@ $effect(() => {
|
|||||||
if (container && fontA && fontB) {
|
if (container && fontA && fontB) {
|
||||||
const width = container.offsetWidth;
|
const width = container.offsetWidth;
|
||||||
const padding = isMobile ? 48 : 96;
|
const padding = isMobile ? 48 : 96;
|
||||||
|
containerWidth = width;
|
||||||
layoutResult = comparisonEngine.layout(
|
layoutResult = comparisonEngine.layout(
|
||||||
comparisonStore.text,
|
comparisonStore.text,
|
||||||
`${typography.weight} ${typography.renderedSize}px "${fontA.name}"`,
|
`${typography.weight} ${typography.renderedSize}px "${fontA.name}"`,
|
||||||
`${typography.weight} ${typography.renderedSize}px "${fontB.name}"`,
|
`${typography.weight} ${typography.renderedSize}px "${fontB.name}"`,
|
||||||
width - padding,
|
width - padding,
|
||||||
typography.renderedSize * 1.2,
|
typography.renderedSize * typography.height,
|
||||||
|
typography.spacing,
|
||||||
|
typography.renderedSize,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -239,11 +247,15 @@ const scaleClass = $derived(
|
|||||||
my-auto
|
my-auto
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
{#each layoutResult.lines as line, lineIndex}
|
{#each layoutResult.lines as line}
|
||||||
|
{@const lineStates = comparisonEngine.getLineCharStates(line, sliderPos, containerWidth)}
|
||||||
<Line chars={line.chars}>
|
<Line chars={line.chars}>
|
||||||
{#snippet character({ char, index })}
|
{#snippet character({ char, index })}
|
||||||
{@const { proximity, isPast } = comparisonEngine.getCharState(lineIndex, index, sliderPos, container?.offsetWidth ?? 0)}
|
<Character
|
||||||
<Character {char} {proximity} {isPast} />
|
{char}
|
||||||
|
proximity={lineStates[index]?.proximity ?? 0}
|
||||||
|
isPast={lineStates[index]?.isPast ?? false}
|
||||||
|
/>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Line>
|
</Line>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
Reference in New Issue
Block a user