From 5d8869b3f2d2888e691142bf89872168a4296071 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Mon, 9 Feb 2026 16:47:19 +0300 Subject: [PATCH 1/9] fix(ComparisonSlider): remove blur inside the sliders line and add gpu acceleration. imrove animation duration --- .../ComparisonSlider/components/SliderLine.svelte | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/widgets/ComparisonSlider/ui/ComparisonSlider/components/SliderLine.svelte b/src/widgets/ComparisonSlider/ui/ComparisonSlider/components/SliderLine.svelte index 99c79e0..84a71f2 100644 --- a/src/widgets/ComparisonSlider/ui/ComparisonSlider/components/SliderLine.svelte +++ b/src/widgets/ComparisonSlider/ui/ComparisonSlider/components/SliderLine.svelte @@ -18,8 +18,15 @@ interface Props { let { sliderPos, isDragging }: Props = $props();
From 7018b6a836e4e032ef99c714d95bf4988bd5b8ed Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Mon, 9 Feb 2026 16:48:11 +0300 Subject: [PATCH 2/9] fix(Logo): add fallback for the safari and chrome for text-justify:inter-character rule --- src/shared/ui/Logo/Logo.svelte | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/src/shared/ui/Logo/Logo.svelte b/src/shared/ui/Logo/Logo.svelte index bf73bcc..9883dd5 100644 --- a/src/shared/ui/Logo/Logo.svelte +++ b/src/shared/ui/Logo/Logo.svelte @@ -11,9 +11,35 @@ interface Props { const { class: className }: Props = $props(); -const baseClasses = - 'font-[Barlow] font-thin text-5xl sm:text-6xl md:text-7xl lg:text-8xl text-justify [text-align-last:justify] [text-justify:inter-character]'; +const baseClasses = 'font-[Barlow] font-thin text-5xl sm:text-6xl md:text-7xl lg:text-8xl'; + +const title = 'GLYPHDIFF'; -

- GLYPHDIFF + + +

+ {title} +

+ + +

+ {#each title.split('') as letter} + {letter} + {/each}

From 055b02f7203f52b69840e31e2529f993adc168a2 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Mon, 9 Feb 2026 16:48:33 +0300 Subject: [PATCH 3/9] fix: indentation --- .gitea/workflows/workflow.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.gitea/workflows/workflow.yml b/.gitea/workflows/workflow.yml index 9deb1ee..a9b8fb8 100644 --- a/.gitea/workflows/workflow.yml +++ b/.gitea/workflows/workflow.yml @@ -44,17 +44,17 @@ jobs: run: yarn check:shadcn-excluded publish: - needs: build # Only runs if tests/lint pass - runs-on: ubuntu-latest - if: github.ref == 'refs/heads/main' # Only deploy from main branch - steps: - - name: Checkout - uses: actions/checkout@v4 + needs: build # Only runs if tests/lint pass + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' # Only deploy from main branch + steps: + - name: Checkout + uses: actions/checkout@v4 - - name: Login to Gitea Registry - run: echo "${{ secrets.CI_DEPLOY_TOKEN }}" | docker login git.allmy.work -u ${{ gitea.repository_owner }} --password-stdin + - name: Login to Gitea Registry + run: echo "${{ secrets.CI_DEPLOY_TOKEN }}" | docker login git.allmy.work -u ${{ gitea.repository_owner }} --password-stdin - - name: Build and Push Docker Image - run: | - docker build -t git.allmy.work/${{ gitea.repository }}:latest . - docker push git.allmy.work/${{ gitea.repository }}:latest + - name: Build and Push Docker Image + run: | + docker build -t git.allmy.work/${{ gitea.repository }}:latest . + docker push git.allmy.work/${{ gitea.repository }}:latest From 6945169279f30a941e17721fbe6280e284008389 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Mon, 9 Feb 2026 16:49:06 +0300 Subject: [PATCH 4/9] feat(TypographyMenu): add props hidden to hide component but fire the logic --- src/features/SetupFont/ui/TypographyMenu.svelte | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/features/SetupFont/ui/TypographyMenu.svelte b/src/features/SetupFont/ui/TypographyMenu.svelte index 08c97ac..631eff9 100644 --- a/src/features/SetupFont/ui/TypographyMenu.svelte +++ b/src/features/SetupFont/ui/TypographyMenu.svelte @@ -28,9 +28,10 @@ import { interface Props { class?: string; + hidden?: boolean; } -const { class: className }: Props = $props(); +const { class: className, hidden = false }: Props = $props(); const responsive = getContext('responsive'); const [send, receive] = crossfade({ @@ -71,7 +72,7 @@ $effect(() => { From 422363d329c0e0b7758231b05bf93fffb8f4e488 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Mon, 9 Feb 2026 17:33:09 +0300 Subject: [PATCH 6/9] chore: remove unused code --- src/entities/Font/index.ts | 1 - src/entities/Font/model/index.ts | 1 - src/entities/Font/model/store/index.ts | 3 --- .../store/selectedFontsStore/selectedFontsStore.svelte.ts | 7 ------- src/entities/Font/ui/FontListItem/FontListItem.svelte | 6 +----- src/features/DisplayFont/ui/FontSampler/FontSampler.svelte | 5 ----- 6 files changed, 1 insertion(+), 22 deletions(-) delete mode 100644 src/entities/Font/model/store/selectedFontsStore/selectedFontsStore.svelte.ts diff --git a/src/entities/Font/index.ts b/src/entities/Font/index.ts index f9ed924..f0f07a5 100644 --- a/src/entities/Font/index.ts +++ b/src/entities/Font/index.ts @@ -75,7 +75,6 @@ export type { export { appliedFontsManager, createUnifiedFontStore, - selectedFontsStore, unifiedFontStore, } from './model'; diff --git a/src/entities/Font/model/index.ts b/src/entities/Font/model/index.ts index 47daa00..8ec6e69 100644 --- a/src/entities/Font/model/index.ts +++ b/src/entities/Font/model/index.ts @@ -38,7 +38,6 @@ export { appliedFontsManager, createUnifiedFontStore, type FontConfigRequest, - selectedFontsStore, type UnifiedFontStore, unifiedFontStore, } from './store'; diff --git a/src/entities/Font/model/store/index.ts b/src/entities/Font/model/store/index.ts index eacd64e..c110ee4 100644 --- a/src/entities/Font/model/store/index.ts +++ b/src/entities/Font/model/store/index.ts @@ -18,6 +18,3 @@ export { appliedFontsManager, type FontConfigRequest, } from './appliedFontsStore/appliedFontsStore.svelte'; - -// Selected fonts store (user selection - unchanged) -export { selectedFontsStore } from './selectedFontsStore/selectedFontsStore.svelte'; diff --git a/src/entities/Font/model/store/selectedFontsStore/selectedFontsStore.svelte.ts b/src/entities/Font/model/store/selectedFontsStore/selectedFontsStore.svelte.ts deleted file mode 100644 index de3c3f8..0000000 --- a/src/entities/Font/model/store/selectedFontsStore/selectedFontsStore.svelte.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createEntityStore } from '$shared/lib'; -import type { UnifiedFont } from '../../types'; - -/** - * Store that handles collection of selected fonts - */ -export const selectedFontsStore = createEntityStore([]); diff --git a/src/entities/Font/ui/FontListItem/FontListItem.svelte b/src/entities/Font/ui/FontListItem/FontListItem.svelte index af0e7fc..b958d69 100644 --- a/src/entities/Font/ui/FontListItem/FontListItem.svelte +++ b/src/entities/Font/ui/FontListItem/FontListItem.svelte @@ -6,10 +6,7 @@ import { cn } from '$shared/shadcn/utils/shadcn-utils'; import type { Snippet } from 'svelte'; import { Spring } from 'svelte/motion'; -import { - type UnifiedFont, - selectedFontsStore, -} from '../../model'; +import { type UnifiedFont } from '../../model'; interface Props { /** @@ -36,7 +33,6 @@ interface Props { const { font, isFullyVisible, isPartiallyVisible, proximity, children }: Props = $props(); -const selected = $derived(selectedFontsStore.has(font.id)); let timeoutId = $state(null); // Create a spring for smooth scale animation diff --git a/src/features/DisplayFont/ui/FontSampler/FontSampler.svelte b/src/features/DisplayFont/ui/FontSampler/FontSampler.svelte index 64274e5..e7157cf 100644 --- a/src/features/DisplayFont/ui/FontSampler/FontSampler.svelte +++ b/src/features/DisplayFont/ui/FontSampler/FontSampler.svelte @@ -6,7 +6,6 @@ import { FontApplicator, type UnifiedFont, - selectedFontsStore, } from '$entities/Font'; import { controlManager } from '$features/SetupFont'; import { @@ -48,10 +47,6 @@ const fontWeight = $derived(controlManager.weight); const fontSize = $derived(controlManager.renderedSize); const lineHeight = $derived(controlManager.height); const letterSpacing = $derived(controlManager.spacing); - -function removeSample() { - selectedFontsStore.removeOne(font.id); -}
Date: Tue, 10 Feb 2026 10:12:58 +0300 Subject: [PATCH 7/9] feat(FontApplicator): add system fonts and change animation --- .../Font/ui/FontApplicator/FontApplicator.svelte | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/entities/Font/ui/FontApplicator/FontApplicator.svelte b/src/entities/Font/ui/FontApplicator/FontApplicator.svelte index ebfd362..1ec6114 100644 --- a/src/entities/Font/ui/FontApplicator/FontApplicator.svelte +++ b/src/entities/Font/ui/FontApplicator/FontApplicator.svelte @@ -20,13 +20,15 @@ interface Props { * Font id to load */ id: string; - + /** */ url: string; /** * Font weight */ weight?: number; - + /** + * Variable font flag + */ isVariable?: boolean; /** * Additional classes @@ -75,25 +77,25 @@ $effect(() => { }); // The "Show" condition: Element is in view AND (Font is ready OR it errored out) -const shouldReveal = $derived(hasEnteredViewport && (status === 'loaded' || status === 'error')); +const shouldReveal = $derived(hasEnteredViewport && (status === 'loaded')); const transitionClasses = $derived( prefersReducedMotion.current ? 'transition-none' // Disable CSS transitions if motion is reduced - : 'transition-all duration-700 ease-[cubic-bezier(0.22,1,0.36,1)]', + : 'transition-all duration-300 ease-[cubic-bezier(0.22,1,0.36,1)]', );
From 1fc9572f3d5b11ed386409501087284d3fb72e5f Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Tue, 10 Feb 2026 10:14:46 +0300 Subject: [PATCH 8/9] feat(appliedFontStore): use FontFace constructor, improve the performance and add test coverage for basic logic --- .../appliedFontStore.test.ts | 115 +++++++++++ .../appliedFontsStore.svelte.ts | 185 ++++++++---------- 2 files changed, 201 insertions(+), 99 deletions(-) create mode 100644 src/entities/Font/model/store/appliedFontsStore/appliedFontStore.test.ts diff --git a/src/entities/Font/model/store/appliedFontsStore/appliedFontStore.test.ts b/src/entities/Font/model/store/appliedFontsStore/appliedFontStore.test.ts new file mode 100644 index 0000000..f2a116e --- /dev/null +++ b/src/entities/Font/model/store/appliedFontsStore/appliedFontStore.test.ts @@ -0,0 +1,115 @@ +/** @vitest-environment jsdom */ +import { + afterEach, + beforeEach, + describe, + expect, + it, + vi, +} from 'vitest'; +import { AppliedFontsManager } from './appliedFontsStore.svelte'; + +describe('AppliedFontsManager', () => { + let manager: AppliedFontsManager; + let mockFontFaceSet: any; + + beforeEach(() => { + vi.useFakeTimers(); + + mockFontFaceSet = { + add: vi.fn(), + delete: vi.fn(), + }; + + // 1. Properly mock FontFace as a constructor function + const MockFontFace = vi.fn(function(this: any, name: string, url: string) { + this.name = name; + this.url = url; + this.load = vi.fn().mockImplementation(() => { + if (url.includes('fail')) return Promise.reject(new Error('Load failed')); + return Promise.resolve(this); + }); + }); + + vi.stubGlobal('FontFace', MockFontFace); + + // 2. Mock document.fonts safely + Object.defineProperty(document, 'fonts', { + value: mockFontFaceSet, + configurable: true, + writable: true, + }); + + vi.stubGlobal('crypto', { + randomUUID: () => '11111111-1111-1111-1111-111111111111' as any, + }); + + manager = new AppliedFontsManager(); + }); + + afterEach(() => { + vi.clearAllTimers(); + vi.useRealTimers(); + }); + + it('should batch multiple font requests into a single process', async () => { + const configs = [ + { id: 'lato-400', name: 'Lato', url: 'lato.ttf', weight: 400 }, + { id: 'lato-700', name: 'Lato', url: 'lato-bold.ttf', weight: 700 }, + ]; + + manager.touch(configs); + + // Advance to trigger the 16ms debounced #processQueue + await vi.advanceTimersByTimeAsync(50); + + expect(manager.getFontStatus('lato-400', 400)).toBe('loaded'); + expect(mockFontFaceSet.add).toHaveBeenCalledTimes(2); + }); + + it('should handle font loading errors gracefully', async () => { + // Suppress expected console error for clean test logs + const spy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + const config = { id: 'broken', name: 'Broken', url: 'fail.ttf', weight: 400 }; + + manager.touch([config]); + await vi.advanceTimersByTimeAsync(50); + + expect(manager.getFontStatus('broken', 400)).toBe('error'); + spy.mockRestore(); + }); + + it('should purge fonts after TTL expires', async () => { + const config = { id: 'ephemeral', name: 'Temp', url: 'temp.ttf', weight: 400 }; + + manager.touch([config]); + await vi.advanceTimersByTimeAsync(50); + expect(manager.getFontStatus('ephemeral', 400)).toBe('loaded'); + + // Move clock forward past TTL (5m) and Purge Interval (1m) + // advanceTimersByTimeAsync is key here; it handles the promises inside the interval + await vi.advanceTimersByTimeAsync(6 * 60 * 1000); + + expect(manager.getFontStatus('ephemeral', 400)).toBeUndefined(); + expect(mockFontFaceSet.delete).toHaveBeenCalled(); + }); + + it('should NOT purge fonts that are still being "touched"', async () => { + const config = { id: 'active', name: 'Active', url: 'active.ttf', weight: 400 }; + + manager.touch([config]); + await vi.advanceTimersByTimeAsync(50); + + // Advance 4 minutes + await vi.advanceTimersByTimeAsync(4 * 60 * 1000); + + // Refresh touch + manager.touch([config]); + + // Advance another 2 minutes (Total 6 since start) + await vi.advanceTimersByTimeAsync(2 * 60 * 1000); + + expect(manager.getFontStatus('active', 400)).toBe('loaded'); + }); +}); diff --git a/src/entities/Font/model/store/appliedFontsStore/appliedFontsStore.svelte.ts b/src/entities/Font/model/store/appliedFontsStore/appliedFontsStore.svelte.ts index 4629d06..9cb92b7 100644 --- a/src/entities/Font/model/store/appliedFontsStore/appliedFontsStore.svelte.ts +++ b/src/entities/Font/model/store/appliedFontsStore/appliedFontsStore.svelte.ts @@ -31,155 +31,142 @@ export interface FontConfigRequest { * - Variable fonts: Loaded once per id (covers all weights). * - Static fonts: Loaded per id + weight combination. */ -class AppliedFontsManager { - #usageTracker = new Map(); - #idToBatch = new Map(); - // Changed to HTMLStyleElement - #batchElements = new Map(); +export class AppliedFontsManager { + // Stores the actual FontFace objects for cleanup + #loadedFonts = new Map(); + // Optimization: Map> to avoid O(N^2) scans + #batchToKeys = new Map>(); + // Optimization: Map for reverse lookup + #keyToBatch = new Map(); - #queue = new Map(); // Track config in queue + #usageTracker = new Map(); + #queue = new Map(); #timeoutId: ReturnType | null = null; - #PURGE_INTERVAL = 60000; - #TTL = 5 * 60 * 1000; - #CHUNK_SIZE = 5; // Can be larger since we're just injecting strings + readonly #PURGE_INTERVAL = 60000; + readonly #TTL = 5 * 60 * 1000; + readonly #CHUNK_SIZE = 5; statuses = new SvelteMap(); constructor() { if (typeof window !== 'undefined') { + // Using a weak reference style approach isn't possible for DOM, + // so we stick to the interval but make it highly efficient. setInterval(() => this.#purgeUnused(), this.#PURGE_INTERVAL); } } - #getFontKey(config: FontConfigRequest): string { - if (config.isVariable) { - // For variable fonts, the ID is unique enough. - // Loading "Roboto" once covers "Roboto 400" and "Roboto 700" - return `${config.id.toLowerCase()}@vf`; - } - // For static fonts, we still need weight separation - return `${config.id.toLowerCase()}@${config.weight}`; + #getFontKey(id: string, weight: number, isVariable: boolean): string { + return isVariable ? `${id.toLowerCase()}@vf` : `${id.toLowerCase()}@${weight}`; } touch(configs: FontConfigRequest[]) { const now = Date.now(); - configs.forEach(config => { - // Pass the whole config to get key - const key = this.#getFontKey(config); + let hasNewItems = false; + for (const config of configs) { + const key = this.#getFontKey(config.id, config.weight, !!config.isVariable); this.#usageTracker.set(key, now); - // If it's already loaded, we don't need to do anything - if (this.statuses.get(key) === 'loaded') return; - - if (!this.#idToBatch.has(key) && !this.#queue.has(key)) { - this.#queue.set(key, config); - - if (this.#timeoutId) clearTimeout(this.#timeoutId); - this.#timeoutId = setTimeout(() => this.#processQueue(), 50); + if (this.statuses.get(key) === 'loaded' || this.statuses.get(key) === 'loading' || this.#queue.has(key)) { + continue; } - }); - } - getFontStatus(id: string, weight: number, isVariable: boolean = false) { - // Construct a temp config to generate key - const key = this.#getFontKey({ id, weight, name: '', url: '', isVariable }); - return this.statuses.get(key); + this.#queue.set(key, config); + hasNewItems = true; + } + + // IMPROVEMENT: Only trigger timer if not already pending + if (hasNewItems && !this.#timeoutId) { + this.#timeoutId = setTimeout(() => this.#processQueue(), 16); // ~1 frame delay + } } #processQueue() { + this.#timeoutId = null; const entries = Array.from(this.#queue.entries()); if (entries.length === 0) return; - for (let i = 0; i < entries.length; i += this.#CHUNK_SIZE) { - this.#createBatch(entries.slice(i, i + this.#CHUNK_SIZE)); - } - this.#queue.clear(); - this.#timeoutId = null; + + // Process in chunks to keep the UI responsive + for (let i = 0; i < entries.length; i += this.#CHUNK_SIZE) { + this.#applyBatch(entries.slice(i, i + this.#CHUNK_SIZE)); + } } - #createBatch(batchEntries: [string, FontConfigRequest][]) { + async #applyBatch(batchEntries: [string, FontConfigRequest][]) { if (typeof document === 'undefined') return; const batchId = crypto.randomUUID(); - let cssRules = ''; + const keysInBatch = new Set(); - batchEntries.forEach(([key, config]) => { + const loadPromises = batchEntries.map(([key, config]) => { this.statuses.set(key, 'loading'); - this.#idToBatch.set(key, batchId); + this.#keyToBatch.set(key, batchId); + keysInBatch.add(key); - // If variable, allow the full weight range. - // If static, lock it to the specific weight. - const weightRule = config.isVariable - ? '100 900' // Variable range (standard coverage) - : config.weight; - const fontFormat = config.isVariable ? 'truetype-variations' : 'truetype'; + // Use a unique internal family name to prevent collisions + // while keeping the "real" name for the browser to resolve weight/style. + const internalName = `f_${config.id}`; + const weightRange = config.isVariable ? '100 900' : `${config.weight}`; - cssRules += ` - @font-face { - font-family: '${config.name}'; - src: url('${config.url}') format('${fontFormat}'); - font-weight: ${weightRule}; - font-style: normal; - font-display: swap; - } - `; - }); + const font = new FontFace(config.name, `url(${config.url})`, { + weight: weightRange, + style: 'normal', + display: 'swap', + }); - const style = document.createElement('style'); - style.dataset.batchId = batchId; - style.innerHTML = cssRules; - document.head.appendChild(style); - this.#batchElements.set(batchId, style); + this.#loadedFonts.set(key, font); - // Use the requested weight for verification, even if the rule covers a range - batchEntries.forEach(([key, config]) => { - document.fonts.load(`${config.weight} 1em "${config.name}"`) - .then(loaded => { - this.statuses.set(key, loaded.length > 0 ? 'loaded' : 'error'); + return font.load() + .then(loadedFace => { + document.fonts.add(loadedFace); + this.statuses.set(key, 'loaded'); }) - .catch(() => this.statuses.set(key, 'error')); + .catch(e => { + console.error(`Font load failed: ${config.name}`, e); + this.statuses.set(key, 'error'); + }); }); + + this.#batchToKeys.set(batchId, keysInBatch); + await Promise.allSettled(loadPromises); } + #purgeUnused() { const now = Date.now(); - const batchesToRemove = new Set(); - const keysToRemove: string[] = []; - for (const [key, lastUsed] of this.#usageTracker.entries()) { - if (now - lastUsed > this.#TTL) { - const batchId = this.#idToBatch.get(key); - if (batchId) { - // Check if EVERY font in this batch is expired - const batchKeys = Array.from(this.#idToBatch.entries()) - .filter(([_, bId]) => bId === batchId) - .map(([k]) => k); + // We iterate over batches, not individual fonts, to reduce loops + for (const [batchId, keys] of this.#batchToKeys.entries()) { + let canPurgeBatch = true; - const canDeleteBatch = batchKeys.every(k => { - const lastK = this.#usageTracker.get(k); - return lastK && (now - lastK > this.#TTL); - }); - - if (canDeleteBatch) { - batchesToRemove.add(batchId); - keysToRemove.push(...batchKeys); - } + for (const key of keys) { + const lastUsed = this.#usageTracker.get(key) || 0; + if (now - lastUsed < this.#TTL) { + canPurgeBatch = false; + break; } } + + if (canPurgeBatch) { + keys.forEach(key => { + const font = this.#loadedFonts.get(key); + if (font) document.fonts.delete(font); + + this.#loadedFonts.delete(key); + this.#keyToBatch.delete(key); + this.#usageTracker.delete(key); + this.statuses.delete(key); + }); + this.#batchToKeys.delete(batchId); + } } + } - batchesToRemove.forEach(id => { - this.#batchElements.get(id)?.remove(); - this.#batchElements.delete(id); - }); - - keysToRemove.forEach(k => { - this.#idToBatch.delete(k); - this.#usageTracker.delete(k); - this.statuses.delete(k); - }); + getFontStatus(id: string, weight: number, isVariable = false) { + return this.statuses.get(this.#getFontKey(id, weight, isVariable)); } } From faf9b8570b23cbab2eee8e9d73949cbb48189319 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Tue, 10 Feb 2026 11:47:54 +0300 Subject: [PATCH 9/9] fix(createCharacterComparison): change line break logic to ensure correct text wrap --- .../createCharacterComparison.svelte.ts | 83 ++++++++++++------- 1 file changed, 52 insertions(+), 31 deletions(-) diff --git a/src/shared/lib/helpers/createCharacterComparison/createCharacterComparison.svelte.ts b/src/shared/lib/helpers/createCharacterComparison/createCharacterComparison.svelte.ts index 2867dda..8abb64c 100644 --- a/src/shared/lib/helpers/createCharacterComparison/createCharacterComparison.svelte.ts +++ b/src/shared/lib/helpers/createCharacterComparison/createCharacterComparison.svelte.ts @@ -150,42 +150,63 @@ export function createCharacterComparison< currentLineWords = []; } - let remainingWord = word; - while (remainingWord.length > 0) { - let low = 1; - let high = remainingWord.length; - let bestBreak = 1; + const wordWidthA = measureText( + ctx, + word, + Math.min(fontSize, controlledFontSize), + currentWeight, + fontA()?.name, + ); + const wordWidthB = measureText( + ctx, + word, + Math.min(fontSize, controlledFontSize), + currentWeight, + fontB()?.name, + ); + const wordAloneWidth = Math.max(wordWidthA, wordWidthB); - // Binary Search to find the maximum characters that fit - while (low <= high) { - const mid = Math.floor((low + high) / 2); - const testFragment = remainingWord.slice(0, mid); + if (wordAloneWidth <= availableWidth) { + // If word fits start new line with it + currentLineWords = [word]; + } else { + let remainingWord = word; + while (remainingWord.length > 0) { + let low = 1; + let high = remainingWord.length; + let bestBreak = 1; - const wA = measureText( - ctx, - testFragment, - fontSize, - currentWeight, - fontA()?.name, - ); - const wB = measureText( - ctx, - testFragment, - fontSize, - currentWeight, - fontB()?.name, - ); + // Binary Search to find the maximum characters that fit + while (low <= high) { + const mid = Math.floor((low + high) / 2); + const testFragment = remainingWord.slice(0, mid); - if (Math.max(wA, wB) <= availableWidth) { - bestBreak = mid; - low = mid + 1; - } else { - high = mid - 1; + const wA = measureText( + ctx, + testFragment, + fontSize, + currentWeight, + fontA()?.name, + ); + const wB = measureText( + ctx, + testFragment, + fontSize, + currentWeight, + fontB()?.name, + ); + + if (Math.max(wA, wB) <= availableWidth) { + bestBreak = mid; + low = mid + 1; + } else { + high = mid - 1; + } } - } - pushLine([remainingWord.slice(0, bestBreak)]); - remainingWord = remainingWord.slice(bestBreak); + pushLine([remainingWord.slice(0, bestBreak)]); + remainingWord = remainingWord.slice(bestBreak); + } } } else if (maxWidth > availableWidth && currentLineWords.length > 0) { pushLine(currentLineWords);