feat(font): compute split index and 3-region slice

This commit is contained in:
Ilia Mashkov
2026-05-30 21:50:02 +03:00
parent efbc464b14
commit ccf51c645e
2 changed files with 108 additions and 2 deletions
@@ -42,4 +42,46 @@ describe('computeLineRenderModel', () => {
expect(model.windowChars).toEqual([]);
expect(model.rightText).toBe('');
});
it('places entire line in rightText when slider is at 0', () => {
const line = makeLine([
{ char: 'A', widthA: 10, widthB: 10 },
{ char: 'B', widthA: 10, widthB: 10 },
{ char: 'C', widthA: 10, widthB: 10 },
]);
const model = computeLineRenderModel(line, 0, 500, 0);
expect(model.leftText).toBe('');
expect(model.windowChars).toEqual([]);
expect(model.rightText).toBe('ABC');
});
it('places entire line in leftText when slider is at 100', () => {
const line = makeLine([
{ char: 'A', widthA: 10, widthB: 10 },
{ char: 'B', widthA: 10, widthB: 10 },
{ char: 'C', widthA: 10, widthB: 10 },
]);
const model = computeLineRenderModel(line, 100, 500, 0);
expect(model.leftText).toBe('ABC');
expect(model.windowChars).toEqual([]);
expect(model.rightText).toBe('');
});
it('splits line correctly with slider mid-line (window=0)', () => {
// Equal widths → line is centered. Container=300, total=30 → xOffset=135.
// Char thresholds (per the threshold formula in the design):
// threshold[i] = xOffset + prefA[i] + widthA[i]/2
// i=0: 135 + 0 + 5 = 140 → 140/300 = 46.67%
// i=1: 135 + 10 + 5 = 150 → 150/300 = 50.00%
// i=2: 135 + 20 + 5 = 160 → 160/300 = 53.33%
const line = makeLine([
{ char: 'A', widthA: 10, widthB: 10 },
{ char: 'B', widthA: 10, widthB: 10 },
{ char: 'C', widthA: 10, widthB: 10 },
]);
// Slider just past B's threshold (50%) but not C's (53.33%).
const model = computeLineRenderModel(line, 51, 300, 0);
expect(model.leftText).toBe('AB');
expect(model.rightText).toBe('C');
});
});
@@ -41,8 +41,72 @@ export function computeLineRenderModel(
containerWidth: number,
windowSize: number,
): LineRenderModel {
if (line.chars.length === 0) {
const chars = line.chars;
const n = chars.length;
if (n === 0) {
return { leftText: '', windowChars: [], rightText: '' };
}
throw new Error('not implemented');
const split = findSplitIndex(chars, sliderPos, containerWidth);
const halfWindow = Math.floor(Math.max(0, windowSize) / 2);
let windowStart = clamp(split - halfWindow, 0, n);
let windowEnd = clamp(windowStart + Math.max(0, windowSize), 0, n);
windowStart = Math.max(0, windowEnd - Math.max(0, windowSize));
const leftText = chars.slice(0, windowStart).map(c => c.char).join('');
const rightText = chars.slice(windowEnd).map(c => c.char).join('');
const windowChars = chars.slice(windowStart, windowEnd).map((c, idx) => ({
key: `${windowStart + idx}-${c.char}`,
char: c.char,
isPast: (windowStart + idx) < split,
}));
return { leftText, windowChars, rightText };
}
/**
* Walks chars left-to-right computing the per-char threshold using prefix sums
* of widthA (past chars, fontA) and suffix sums of widthB (future chars, fontB).
* Returns the count of chars whose threshold the slider has passed.
*/
function findSplitIndex(
chars: ComparisonChar[],
sliderPos: number,
containerWidth: number,
): number {
const n = chars.length;
const sliderX = (sliderPos / 100) * containerWidth;
const prefA = new Float64Array(n + 1);
for (let i = 0; i < n; i++) {
prefA[i + 1] = prefA[i] + chars[i].widthA;
}
const sufB = new Float64Array(n + 1);
for (let i = n - 1; i >= 0; i--) {
sufB[i] = sufB[i + 1] + chars[i].widthB;
}
let split = 0;
for (let i = 0; i < n; i++) {
const totalWidth = prefA[i] + chars[i].widthA + sufB[i + 1];
const xOffset = (containerWidth - totalWidth) / 2;
const threshold = xOffset + prefA[i] + chars[i].widthA / 2;
if (sliderX > threshold) {
split = i + 1;
} else {
break;
}
}
return split;
}
function clamp(value: number, lo: number, hi: number): number {
if (value < lo) {
return lo;
}
if (value > hi) {
return hi;
}
return value;
}