@sc4rfurryx/proteusjs 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API.md +438 -0
- package/FEATURES.md +286 -0
- package/LICENSE +21 -0
- package/README.md +645 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/proteus.cjs.js +16014 -0
- package/dist/proteus.cjs.js.map +1 -0
- package/dist/proteus.d.ts +3018 -0
- package/dist/proteus.esm.js +16005 -0
- package/dist/proteus.esm.js.map +1 -0
- package/dist/proteus.esm.min.js +8 -0
- package/dist/proteus.esm.min.js.map +1 -0
- package/dist/proteus.js +16020 -0
- package/dist/proteus.js.map +1 -0
- package/dist/proteus.min.js +8 -0
- package/dist/proteus.min.js.map +1 -0
- package/package.json +98 -0
- package/src/__tests__/mvp-integration.test.ts +518 -0
- package/src/accessibility/AccessibilityEngine.ts +2106 -0
- package/src/accessibility/ScreenReaderSupport.ts +444 -0
- package/src/accessibility/__tests__/ScreenReaderSupport.test.ts +435 -0
- package/src/animations/FLIPAnimationSystem.ts +491 -0
- package/src/compatibility/BrowserCompatibility.ts +1076 -0
- package/src/containers/BreakpointSystem.ts +347 -0
- package/src/containers/ContainerBreakpoints.ts +726 -0
- package/src/containers/ContainerManager.ts +370 -0
- package/src/containers/ContainerUnits.ts +336 -0
- package/src/containers/ContextIsolation.ts +394 -0
- package/src/containers/ElementQueries.ts +411 -0
- package/src/containers/SmartContainer.ts +536 -0
- package/src/containers/SmartContainers.ts +376 -0
- package/src/containers/__tests__/ContainerBreakpoints.test.ts +411 -0
- package/src/containers/__tests__/SmartContainers.test.ts +281 -0
- package/src/content/ResponsiveImages.ts +570 -0
- package/src/core/EventSystem.ts +147 -0
- package/src/core/MemoryManager.ts +321 -0
- package/src/core/PerformanceMonitor.ts +238 -0
- package/src/core/PluginSystem.ts +275 -0
- package/src/core/ProteusJS.test.ts +164 -0
- package/src/core/ProteusJS.ts +962 -0
- package/src/developer/PerformanceProfiler.ts +567 -0
- package/src/developer/VisualDebuggingTools.ts +656 -0
- package/src/developer/ZeroConfigSystem.ts +593 -0
- package/src/index.ts +35 -0
- package/src/integration.test.ts +227 -0
- package/src/layout/AdaptiveGrid.ts +429 -0
- package/src/layout/ContentReordering.ts +532 -0
- package/src/layout/FlexboxEnhancer.ts +406 -0
- package/src/layout/FlowLayout.ts +545 -0
- package/src/layout/SpacingSystem.ts +512 -0
- package/src/observers/IntersectionObserverPolyfill.ts +289 -0
- package/src/observers/ObserverManager.ts +299 -0
- package/src/observers/ResizeObserverPolyfill.ts +179 -0
- package/src/performance/BatchDOMOperations.ts +519 -0
- package/src/performance/CSSOptimizationEngine.ts +646 -0
- package/src/performance/CacheOptimizationSystem.ts +601 -0
- package/src/performance/EfficientEventHandler.ts +740 -0
- package/src/performance/LazyEvaluationSystem.ts +532 -0
- package/src/performance/MemoryManagementSystem.ts +497 -0
- package/src/performance/PerformanceMonitor.ts +931 -0
- package/src/performance/__tests__/BatchDOMOperations.test.ts +309 -0
- package/src/performance/__tests__/EfficientEventHandler.test.ts +268 -0
- package/src/performance/__tests__/PerformanceMonitor.test.ts +422 -0
- package/src/polyfills/BrowserPolyfills.ts +586 -0
- package/src/polyfills/__tests__/BrowserPolyfills.test.ts +328 -0
- package/src/test/setup.ts +115 -0
- package/src/theming/SmartThemeSystem.ts +591 -0
- package/src/types/index.ts +134 -0
- package/src/typography/ClampScaling.ts +356 -0
- package/src/typography/FluidTypography.ts +759 -0
- package/src/typography/LineHeightOptimization.ts +430 -0
- package/src/typography/LineHeightOptimizer.ts +326 -0
- package/src/typography/TextFitting.ts +355 -0
- package/src/typography/TypographicScale.ts +428 -0
- package/src/typography/VerticalRhythm.ts +369 -0
- package/src/typography/__tests__/FluidTypography.test.ts +432 -0
- package/src/typography/__tests__/LineHeightOptimization.test.ts +436 -0
- package/src/utils/Logger.ts +173 -0
- package/src/utils/debounce.ts +259 -0
- package/src/utils/performance.ts +371 -0
- package/src/utils/support.ts +106 -0
- package/src/utils/version.ts +24 -0
@@ -0,0 +1,326 @@
|
|
1
|
+
/**
|
2
|
+
* Smart Line Height Optimization for ProteusJS
|
3
|
+
* Dynamic line-height optimization for optimal readability
|
4
|
+
*/
|
5
|
+
|
6
|
+
export interface LineHeightConfig {
|
7
|
+
baseFontSize: number;
|
8
|
+
baseLineHeight: number;
|
9
|
+
minLineHeight: number;
|
10
|
+
maxLineHeight: number;
|
11
|
+
language: string;
|
12
|
+
contentType: 'heading' | 'body' | 'caption' | 'code' | 'display';
|
13
|
+
accessibility: boolean;
|
14
|
+
density: 'compact' | 'comfortable' | 'spacious';
|
15
|
+
}
|
16
|
+
|
17
|
+
export interface OptimizationResult {
|
18
|
+
lineHeight: number;
|
19
|
+
ratio: number;
|
20
|
+
reasoning: string[];
|
21
|
+
accessibility: {
|
22
|
+
wcagCompliant: boolean;
|
23
|
+
readabilityScore: number;
|
24
|
+
};
|
25
|
+
}
|
26
|
+
|
27
|
+
export class LineHeightOptimizer {
|
28
|
+
private static readonly LANGUAGE_ADJUSTMENTS = {
|
29
|
+
'en': 1.0, // English baseline
|
30
|
+
'zh': 1.1, // Chinese - needs more space
|
31
|
+
'ja': 1.1, // Japanese - needs more space
|
32
|
+
'ko': 1.1, // Korean - needs more space
|
33
|
+
'ar': 1.05, // Arabic - slightly more space
|
34
|
+
'hi': 1.05, // Hindi - slightly more space
|
35
|
+
'th': 1.15, // Thai - needs significant space
|
36
|
+
'vi': 1.05, // Vietnamese - slightly more space
|
37
|
+
'de': 0.98, // German - can be slightly tighter
|
38
|
+
'fr': 1.0, // French - baseline
|
39
|
+
'es': 1.0, // Spanish - baseline
|
40
|
+
'it': 1.0, // Italian - baseline
|
41
|
+
'pt': 1.0, // Portuguese - baseline
|
42
|
+
'ru': 1.02, // Russian - slightly more space
|
43
|
+
'default': 1.0
|
44
|
+
};
|
45
|
+
|
46
|
+
private static readonly CONTENT_TYPE_RATIOS = {
|
47
|
+
'heading': { min: 1.0, optimal: 1.2, max: 1.4 },
|
48
|
+
'body': { min: 1.3, optimal: 1.5, max: 1.8 },
|
49
|
+
'caption': { min: 1.2, optimal: 1.4, max: 1.6 },
|
50
|
+
'code': { min: 1.2, optimal: 1.4, max: 1.6 },
|
51
|
+
'display': { min: 0.9, optimal: 1.1, max: 1.3 }
|
52
|
+
};
|
53
|
+
|
54
|
+
private static readonly DENSITY_MULTIPLIERS = {
|
55
|
+
'compact': 0.9,
|
56
|
+
'comfortable': 1.0,
|
57
|
+
'spacious': 1.1
|
58
|
+
};
|
59
|
+
|
60
|
+
/**
|
61
|
+
* Calculate optimal line height
|
62
|
+
*/
|
63
|
+
public calculateOptimalLineHeight(
|
64
|
+
fontSize: number,
|
65
|
+
lineLength: number,
|
66
|
+
containerWidth: number,
|
67
|
+
config: LineHeightConfig
|
68
|
+
): OptimizationResult {
|
69
|
+
const reasoning: string[] = [];
|
70
|
+
let lineHeight = config.baseLineHeight;
|
71
|
+
|
72
|
+
// 1. Base ratio for content type
|
73
|
+
const contentRatios = LineHeightOptimizer.CONTENT_TYPE_RATIOS[config.contentType];
|
74
|
+
lineHeight = contentRatios.optimal;
|
75
|
+
reasoning.push(`Base ratio for ${config.contentType}: ${lineHeight}`);
|
76
|
+
|
77
|
+
// 2. Font size adjustment
|
78
|
+
const sizeAdjustment = this.calculateSizeAdjustment(fontSize, config.baseFontSize);
|
79
|
+
lineHeight *= sizeAdjustment;
|
80
|
+
reasoning.push(`Font size adjustment (${fontSize}px): ×${sizeAdjustment.toFixed(3)}`);
|
81
|
+
|
82
|
+
// 3. Line length adjustment
|
83
|
+
const lengthAdjustment = this.calculateLineLengthAdjustment(lineLength, fontSize);
|
84
|
+
lineHeight *= lengthAdjustment;
|
85
|
+
reasoning.push(`Line length adjustment (${lineLength} chars): ×${lengthAdjustment.toFixed(3)}`);
|
86
|
+
|
87
|
+
// 4. Language adjustment
|
88
|
+
const langCode = config.language?.split('-')[0]?.toLowerCase() || 'en';
|
89
|
+
const languageMultiplier = LineHeightOptimizer.LANGUAGE_ADJUSTMENTS[langCode as keyof typeof LineHeightOptimizer.LANGUAGE_ADJUSTMENTS] || LineHeightOptimizer.LANGUAGE_ADJUSTMENTS.default;
|
90
|
+
lineHeight *= languageMultiplier;
|
91
|
+
if (languageMultiplier !== 1.0) {
|
92
|
+
reasoning.push(`Language adjustment (${langCode}): ×${languageMultiplier}`);
|
93
|
+
}
|
94
|
+
|
95
|
+
// 5. Density adjustment
|
96
|
+
const densityMultiplier = LineHeightOptimizer.DENSITY_MULTIPLIERS[config.density];
|
97
|
+
lineHeight *= densityMultiplier;
|
98
|
+
reasoning.push(`Density adjustment (${config.density}): ×${densityMultiplier}`);
|
99
|
+
|
100
|
+
// 6. Container width adjustment
|
101
|
+
const widthAdjustment = this.calculateWidthAdjustment(containerWidth, fontSize);
|
102
|
+
lineHeight *= widthAdjustment;
|
103
|
+
if (widthAdjustment !== 1.0) {
|
104
|
+
reasoning.push(`Container width adjustment: ×${widthAdjustment.toFixed(3)}`);
|
105
|
+
}
|
106
|
+
|
107
|
+
// 7. Accessibility adjustments
|
108
|
+
if (config.accessibility) {
|
109
|
+
const accessibilityAdjustment = this.calculateAccessibilityAdjustment(lineHeight, config);
|
110
|
+
lineHeight = accessibilityAdjustment.lineHeight;
|
111
|
+
if (accessibilityAdjustment.adjusted) {
|
112
|
+
reasoning.push(`Accessibility adjustment: ${accessibilityAdjustment.reason}`);
|
113
|
+
}
|
114
|
+
}
|
115
|
+
|
116
|
+
// 8. Clamp to bounds
|
117
|
+
const originalLineHeight = lineHeight;
|
118
|
+
lineHeight = Math.max(config.minLineHeight, Math.min(config.maxLineHeight, lineHeight));
|
119
|
+
if (lineHeight !== originalLineHeight) {
|
120
|
+
reasoning.push(`Clamped to bounds: ${config.minLineHeight}-${config.maxLineHeight}`);
|
121
|
+
}
|
122
|
+
|
123
|
+
// Calculate accessibility metrics
|
124
|
+
const accessibility = this.calculateAccessibilityMetrics(lineHeight, fontSize, config);
|
125
|
+
|
126
|
+
return {
|
127
|
+
lineHeight: Math.round(lineHeight * 1000) / 1000, // Round to 3 decimal places
|
128
|
+
ratio: lineHeight,
|
129
|
+
reasoning,
|
130
|
+
accessibility
|
131
|
+
};
|
132
|
+
}
|
133
|
+
|
134
|
+
/**
|
135
|
+
* Apply line height optimization to element
|
136
|
+
*/
|
137
|
+
public applyOptimization(
|
138
|
+
element: Element,
|
139
|
+
result: OptimizationResult,
|
140
|
+
config: LineHeightConfig
|
141
|
+
): void {
|
142
|
+
const htmlElement = element as HTMLElement;
|
143
|
+
htmlElement.style.lineHeight = result.lineHeight.toString();
|
144
|
+
|
145
|
+
// Add data attributes for debugging
|
146
|
+
if (config.accessibility) {
|
147
|
+
htmlElement.setAttribute('data-proteus-line-height', result.lineHeight.toString());
|
148
|
+
htmlElement.setAttribute('data-proteus-wcag-compliant', result.accessibility.wcagCompliant.toString());
|
149
|
+
}
|
150
|
+
}
|
151
|
+
|
152
|
+
/**
|
153
|
+
* Create responsive line height optimization
|
154
|
+
*/
|
155
|
+
public createResponsiveOptimization(
|
156
|
+
element: Element,
|
157
|
+
config: LineHeightConfig
|
158
|
+
): () => void {
|
159
|
+
const updateLineHeight = () => {
|
160
|
+
const computedStyle = getComputedStyle(element);
|
161
|
+
const fontSize = parseFloat(computedStyle.fontSize);
|
162
|
+
const containerWidth = element.getBoundingClientRect().width;
|
163
|
+
|
164
|
+
// Estimate line length based on average character width
|
165
|
+
const averageCharWidth = fontSize * 0.5; // Approximate
|
166
|
+
const lineLength = Math.floor(containerWidth / averageCharWidth);
|
167
|
+
|
168
|
+
const result = this.calculateOptimalLineHeight(
|
169
|
+
fontSize,
|
170
|
+
lineLength,
|
171
|
+
containerWidth,
|
172
|
+
config
|
173
|
+
);
|
174
|
+
|
175
|
+
this.applyOptimization(element, result, config);
|
176
|
+
};
|
177
|
+
|
178
|
+
// Initial optimization
|
179
|
+
updateLineHeight();
|
180
|
+
|
181
|
+
// Set up resize observer
|
182
|
+
const resizeObserver = new ResizeObserver(() => {
|
183
|
+
updateLineHeight();
|
184
|
+
});
|
185
|
+
|
186
|
+
resizeObserver.observe(element);
|
187
|
+
|
188
|
+
// Return cleanup function
|
189
|
+
return () => {
|
190
|
+
resizeObserver.disconnect();
|
191
|
+
};
|
192
|
+
}
|
193
|
+
|
194
|
+
/**
|
195
|
+
* Calculate font size adjustment factor
|
196
|
+
*/
|
197
|
+
private calculateSizeAdjustment(fontSize: number, baseFontSize: number): number {
|
198
|
+
const ratio = fontSize / baseFontSize;
|
199
|
+
|
200
|
+
// Smaller fonts need relatively larger line heights
|
201
|
+
// Larger fonts can have relatively smaller line heights
|
202
|
+
if (ratio < 1) {
|
203
|
+
return 1 + (1 - ratio) * 0.2; // Up to 20% increase for small fonts
|
204
|
+
} else if (ratio > 1) {
|
205
|
+
return 1 - Math.min((ratio - 1) * 0.1, 0.15); // Up to 15% decrease for large fonts
|
206
|
+
}
|
207
|
+
|
208
|
+
return 1;
|
209
|
+
}
|
210
|
+
|
211
|
+
/**
|
212
|
+
* Calculate line length adjustment factor
|
213
|
+
*/
|
214
|
+
private calculateLineLengthAdjustment(lineLength: number, fontSize: number): number {
|
215
|
+
// Optimal line length is 45-75 characters
|
216
|
+
const optimalMin = 45;
|
217
|
+
const optimalMax = 75;
|
218
|
+
|
219
|
+
if (lineLength < optimalMin) {
|
220
|
+
// Short lines can have tighter line height
|
221
|
+
return 0.95;
|
222
|
+
} else if (lineLength > optimalMax) {
|
223
|
+
// Long lines need more line height for readability
|
224
|
+
const excess = lineLength - optimalMax;
|
225
|
+
return 1 + Math.min(excess * 0.002, 0.2); // Up to 20% increase
|
226
|
+
}
|
227
|
+
|
228
|
+
return 1;
|
229
|
+
}
|
230
|
+
|
231
|
+
/**
|
232
|
+
* Calculate container width adjustment factor
|
233
|
+
*/
|
234
|
+
private calculateWidthAdjustment(containerWidth: number, fontSize: number): number {
|
235
|
+
// Very narrow containers need slightly more line height
|
236
|
+
const minComfortableWidth = fontSize * 20; // 20em
|
237
|
+
|
238
|
+
if (containerWidth < minComfortableWidth) {
|
239
|
+
const ratio = containerWidth / minComfortableWidth;
|
240
|
+
return 1 + (1 - ratio) * 0.1; // Up to 10% increase
|
241
|
+
}
|
242
|
+
|
243
|
+
return 1;
|
244
|
+
}
|
245
|
+
|
246
|
+
/**
|
247
|
+
* Calculate accessibility adjustments
|
248
|
+
*/
|
249
|
+
private calculateAccessibilityAdjustment(
|
250
|
+
lineHeight: number,
|
251
|
+
config: LineHeightConfig
|
252
|
+
): { lineHeight: number; adjusted: boolean; reason: string } {
|
253
|
+
// WCAG 2.1 AA requires line height to be at least 1.5 times the font size for body text
|
254
|
+
const wcagMinimum = config.contentType === 'body' ? 1.5 : 1.3;
|
255
|
+
|
256
|
+
if (lineHeight < wcagMinimum) {
|
257
|
+
return {
|
258
|
+
lineHeight: wcagMinimum,
|
259
|
+
adjusted: true,
|
260
|
+
reason: `WCAG compliance requires minimum ${wcagMinimum}`
|
261
|
+
};
|
262
|
+
}
|
263
|
+
|
264
|
+
return {
|
265
|
+
lineHeight,
|
266
|
+
adjusted: false,
|
267
|
+
reason: ''
|
268
|
+
};
|
269
|
+
}
|
270
|
+
|
271
|
+
/**
|
272
|
+
* Calculate accessibility metrics
|
273
|
+
*/
|
274
|
+
private calculateAccessibilityMetrics(
|
275
|
+
lineHeight: number,
|
276
|
+
fontSize: number,
|
277
|
+
config: LineHeightConfig
|
278
|
+
): { wcagCompliant: boolean; readabilityScore: number } {
|
279
|
+
const wcagMinimum = config.contentType === 'body' ? 1.5 : 1.3;
|
280
|
+
const wcagCompliant = lineHeight >= wcagMinimum;
|
281
|
+
|
282
|
+
// Calculate readability score (0-100)
|
283
|
+
let score = 50; // Base score
|
284
|
+
|
285
|
+
// Line height contribution (40 points max)
|
286
|
+
const optimalRatio = LineHeightOptimizer.CONTENT_TYPE_RATIOS[config.contentType].optimal;
|
287
|
+
const heightDiff = Math.abs(lineHeight - optimalRatio);
|
288
|
+
const heightScore = Math.max(0, 40 - (heightDiff * 100));
|
289
|
+
score += heightScore;
|
290
|
+
|
291
|
+
// WCAG compliance bonus (10 points)
|
292
|
+
if (wcagCompliant) {
|
293
|
+
score += 10;
|
294
|
+
}
|
295
|
+
|
296
|
+
// Clamp to 0-100
|
297
|
+
score = Math.max(0, Math.min(100, score));
|
298
|
+
|
299
|
+
return {
|
300
|
+
wcagCompliant,
|
301
|
+
readabilityScore: Math.round(score)
|
302
|
+
};
|
303
|
+
}
|
304
|
+
|
305
|
+
/**
|
306
|
+
* Get optimal configuration for language and content type
|
307
|
+
*/
|
308
|
+
public static getOptimalConfig(
|
309
|
+
language: string,
|
310
|
+
contentType: LineHeightConfig['contentType'],
|
311
|
+
accessibility: boolean = true
|
312
|
+
): Partial<LineHeightConfig> {
|
313
|
+
const contentRatios = this.CONTENT_TYPE_RATIOS[contentType];
|
314
|
+
|
315
|
+
return {
|
316
|
+
baseFontSize: 16,
|
317
|
+
baseLineHeight: contentRatios.optimal,
|
318
|
+
minLineHeight: contentRatios.min,
|
319
|
+
maxLineHeight: contentRatios.max,
|
320
|
+
language,
|
321
|
+
contentType,
|
322
|
+
accessibility,
|
323
|
+
density: 'comfortable'
|
324
|
+
};
|
325
|
+
}
|
326
|
+
}
|
@@ -0,0 +1,355 @@
|
|
1
|
+
/**
|
2
|
+
* Text Fitting Algorithms for ProteusJS
|
3
|
+
* Dynamic text sizing for optimal readability and layout
|
4
|
+
*/
|
5
|
+
|
6
|
+
export interface FittingConfig {
|
7
|
+
mode: 'single-line' | 'multi-line' | 'overflow-aware';
|
8
|
+
minSize: number;
|
9
|
+
maxSize: number;
|
10
|
+
unit: 'px' | 'rem' | 'em';
|
11
|
+
precision: number;
|
12
|
+
maxIterations: number;
|
13
|
+
lineHeight?: number;
|
14
|
+
wordBreak?: 'normal' | 'break-all' | 'keep-all';
|
15
|
+
overflow?: 'hidden' | 'ellipsis' | 'clip';
|
16
|
+
}
|
17
|
+
|
18
|
+
export interface FittingResult {
|
19
|
+
fontSize: number;
|
20
|
+
lineHeight: number;
|
21
|
+
actualLines: number;
|
22
|
+
overflow: boolean;
|
23
|
+
iterations: number;
|
24
|
+
success: boolean;
|
25
|
+
}
|
26
|
+
|
27
|
+
export class TextFitting {
|
28
|
+
private canvas: HTMLCanvasElement;
|
29
|
+
private context: CanvasRenderingContext2D;
|
30
|
+
|
31
|
+
constructor() {
|
32
|
+
this.canvas = document.createElement('canvas');
|
33
|
+
this.context = this.canvas.getContext('2d')!;
|
34
|
+
}
|
35
|
+
|
36
|
+
/**
|
37
|
+
* Fit text to container with optimal sizing
|
38
|
+
*/
|
39
|
+
public fitText(
|
40
|
+
element: Element,
|
41
|
+
text: string,
|
42
|
+
containerWidth: number,
|
43
|
+
containerHeight: number,
|
44
|
+
config: FittingConfig
|
45
|
+
): FittingResult {
|
46
|
+
const computedStyle = getComputedStyle(element);
|
47
|
+
|
48
|
+
switch (config.mode) {
|
49
|
+
case 'single-line':
|
50
|
+
return this.fitSingleLine(text, containerWidth, config, computedStyle);
|
51
|
+
case 'multi-line':
|
52
|
+
return this.fitMultiLine(text, containerWidth, containerHeight, config, computedStyle);
|
53
|
+
case 'overflow-aware':
|
54
|
+
return this.fitOverflowAware(text, containerWidth, containerHeight, config, computedStyle);
|
55
|
+
default:
|
56
|
+
throw new Error(`Unknown fitting mode: ${config.mode}`);
|
57
|
+
}
|
58
|
+
}
|
59
|
+
|
60
|
+
/**
|
61
|
+
* Apply fitting result to element
|
62
|
+
*/
|
63
|
+
public applyFitting(element: Element, result: FittingResult, config: FittingConfig): void {
|
64
|
+
const htmlElement = element as HTMLElement;
|
65
|
+
|
66
|
+
htmlElement.style.fontSize = `${result.fontSize}${config.unit}`;
|
67
|
+
htmlElement.style.lineHeight = result.lineHeight.toString();
|
68
|
+
|
69
|
+
if (config.mode === 'overflow-aware' && result.overflow) {
|
70
|
+
htmlElement.style.overflow = config.overflow || 'hidden';
|
71
|
+
if (config.overflow === 'ellipsis') {
|
72
|
+
htmlElement.style.textOverflow = 'ellipsis';
|
73
|
+
htmlElement.style.whiteSpace = 'nowrap';
|
74
|
+
}
|
75
|
+
}
|
76
|
+
}
|
77
|
+
|
78
|
+
/**
|
79
|
+
* Measure text dimensions
|
80
|
+
*/
|
81
|
+
public measureText(
|
82
|
+
text: string,
|
83
|
+
fontSize: number,
|
84
|
+
fontFamily: string,
|
85
|
+
fontWeight: string = 'normal'
|
86
|
+
): { width: number; height: number } {
|
87
|
+
this.context.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
|
88
|
+
const metrics = this.context.measureText(text);
|
89
|
+
|
90
|
+
return {
|
91
|
+
width: metrics.width,
|
92
|
+
height: fontSize * 1.2 // Approximate line height
|
93
|
+
};
|
94
|
+
}
|
95
|
+
|
96
|
+
/**
|
97
|
+
* Calculate optimal font size for single line
|
98
|
+
*/
|
99
|
+
private fitSingleLine(
|
100
|
+
text: string,
|
101
|
+
containerWidth: number,
|
102
|
+
config: FittingConfig,
|
103
|
+
computedStyle: CSSStyleDeclaration
|
104
|
+
): FittingResult {
|
105
|
+
const fontFamily = computedStyle.fontFamily;
|
106
|
+
const fontWeight = computedStyle.fontWeight;
|
107
|
+
|
108
|
+
let minSize = config.minSize;
|
109
|
+
let maxSize = config.maxSize;
|
110
|
+
let iterations = 0;
|
111
|
+
let bestSize = minSize;
|
112
|
+
|
113
|
+
// Binary search for optimal size
|
114
|
+
while (maxSize - minSize > config.precision && iterations < config.maxIterations) {
|
115
|
+
const testSize = (minSize + maxSize) / 2;
|
116
|
+
const measurement = this.measureText(text, testSize, fontFamily, fontWeight);
|
117
|
+
|
118
|
+
if (measurement.width <= containerWidth) {
|
119
|
+
bestSize = testSize;
|
120
|
+
minSize = testSize;
|
121
|
+
} else {
|
122
|
+
maxSize = testSize;
|
123
|
+
}
|
124
|
+
|
125
|
+
iterations++;
|
126
|
+
}
|
127
|
+
|
128
|
+
const finalMeasurement = this.measureText(text, bestSize, fontFamily, fontWeight);
|
129
|
+
|
130
|
+
return {
|
131
|
+
fontSize: bestSize,
|
132
|
+
lineHeight: config.lineHeight || 1.2,
|
133
|
+
actualLines: 1,
|
134
|
+
overflow: finalMeasurement.width > containerWidth,
|
135
|
+
iterations,
|
136
|
+
success: finalMeasurement.width <= containerWidth
|
137
|
+
};
|
138
|
+
}
|
139
|
+
|
140
|
+
/**
|
141
|
+
* Calculate optimal font size for multi-line text
|
142
|
+
*/
|
143
|
+
private fitMultiLine(
|
144
|
+
text: string,
|
145
|
+
containerWidth: number,
|
146
|
+
containerHeight: number,
|
147
|
+
config: FittingConfig,
|
148
|
+
computedStyle: CSSStyleDeclaration
|
149
|
+
): FittingResult {
|
150
|
+
const fontFamily = computedStyle.fontFamily;
|
151
|
+
const fontWeight = computedStyle.fontWeight;
|
152
|
+
const lineHeight = config.lineHeight || 1.4;
|
153
|
+
|
154
|
+
let minSize = config.minSize;
|
155
|
+
let maxSize = config.maxSize;
|
156
|
+
let iterations = 0;
|
157
|
+
let bestSize = minSize;
|
158
|
+
let bestLines = 0;
|
159
|
+
|
160
|
+
while (maxSize - minSize > config.precision && iterations < config.maxIterations) {
|
161
|
+
const testSize = (minSize + maxSize) / 2;
|
162
|
+
const lines = this.calculateLines(text, testSize, containerWidth, fontFamily, fontWeight);
|
163
|
+
const totalHeight = lines * testSize * lineHeight;
|
164
|
+
|
165
|
+
if (totalHeight <= containerHeight) {
|
166
|
+
bestSize = testSize;
|
167
|
+
bestLines = lines;
|
168
|
+
minSize = testSize;
|
169
|
+
} else {
|
170
|
+
maxSize = testSize;
|
171
|
+
}
|
172
|
+
|
173
|
+
iterations++;
|
174
|
+
}
|
175
|
+
|
176
|
+
const finalLines = this.calculateLines(text, bestSize, containerWidth, fontFamily, fontWeight);
|
177
|
+
const finalHeight = finalLines * bestSize * lineHeight;
|
178
|
+
|
179
|
+
return {
|
180
|
+
fontSize: bestSize,
|
181
|
+
lineHeight,
|
182
|
+
actualLines: finalLines,
|
183
|
+
overflow: finalHeight > containerHeight,
|
184
|
+
iterations,
|
185
|
+
success: finalHeight <= containerHeight
|
186
|
+
};
|
187
|
+
}
|
188
|
+
|
189
|
+
/**
|
190
|
+
* Calculate optimal font size with overflow awareness
|
191
|
+
*/
|
192
|
+
private fitOverflowAware(
|
193
|
+
text: string,
|
194
|
+
containerWidth: number,
|
195
|
+
containerHeight: number,
|
196
|
+
config: FittingConfig,
|
197
|
+
computedStyle: CSSStyleDeclaration
|
198
|
+
): FittingResult {
|
199
|
+
// First try multi-line fitting
|
200
|
+
const multiLineResult = this.fitMultiLine(text, containerWidth, containerHeight, config, computedStyle);
|
201
|
+
|
202
|
+
if (multiLineResult.success) {
|
203
|
+
return multiLineResult;
|
204
|
+
}
|
205
|
+
|
206
|
+
// If multi-line doesn't fit, try single-line with ellipsis
|
207
|
+
const singleLineResult = this.fitSingleLine(text, containerWidth, config, computedStyle);
|
208
|
+
|
209
|
+
// Calculate how many lines we can fit at this font size
|
210
|
+
const lineHeight = config.lineHeight || 1.4;
|
211
|
+
const maxLines = Math.floor(containerHeight / (singleLineResult.fontSize * lineHeight));
|
212
|
+
|
213
|
+
return {
|
214
|
+
fontSize: singleLineResult.fontSize,
|
215
|
+
lineHeight,
|
216
|
+
actualLines: Math.min(maxLines, 1),
|
217
|
+
overflow: true,
|
218
|
+
iterations: singleLineResult.iterations,
|
219
|
+
success: maxLines >= 1
|
220
|
+
};
|
221
|
+
}
|
222
|
+
|
223
|
+
/**
|
224
|
+
* Calculate number of lines for given text and font size
|
225
|
+
*/
|
226
|
+
private calculateLines(
|
227
|
+
text: string,
|
228
|
+
fontSize: number,
|
229
|
+
containerWidth: number,
|
230
|
+
fontFamily: string,
|
231
|
+
fontWeight: string
|
232
|
+
): number {
|
233
|
+
this.context.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
|
234
|
+
|
235
|
+
const words = text.split(/\s+/);
|
236
|
+
let lines = 1;
|
237
|
+
let currentLineWidth = 0;
|
238
|
+
|
239
|
+
for (const word of words) {
|
240
|
+
const wordWidth = this.context.measureText(`${word } `).width;
|
241
|
+
|
242
|
+
if (currentLineWidth + wordWidth > containerWidth) {
|
243
|
+
lines++;
|
244
|
+
currentLineWidth = wordWidth;
|
245
|
+
} else {
|
246
|
+
currentLineWidth += wordWidth;
|
247
|
+
}
|
248
|
+
}
|
249
|
+
|
250
|
+
return lines;
|
251
|
+
}
|
252
|
+
|
253
|
+
/**
|
254
|
+
* Get optimal configuration for content type
|
255
|
+
*/
|
256
|
+
public static getOptimalConfig(contentType: 'heading' | 'body' | 'caption' | 'button'): Partial<FittingConfig> {
|
257
|
+
switch (contentType) {
|
258
|
+
case 'heading':
|
259
|
+
return {
|
260
|
+
mode: 'single-line',
|
261
|
+
minSize: 18,
|
262
|
+
maxSize: 48,
|
263
|
+
precision: 0.5,
|
264
|
+
maxIterations: 20,
|
265
|
+
lineHeight: 1.2
|
266
|
+
};
|
267
|
+
case 'body':
|
268
|
+
return {
|
269
|
+
mode: 'multi-line',
|
270
|
+
minSize: 14,
|
271
|
+
maxSize: 20,
|
272
|
+
precision: 0.25,
|
273
|
+
maxIterations: 15,
|
274
|
+
lineHeight: 1.5
|
275
|
+
};
|
276
|
+
case 'caption':
|
277
|
+
return {
|
278
|
+
mode: 'overflow-aware',
|
279
|
+
minSize: 12,
|
280
|
+
maxSize: 16,
|
281
|
+
precision: 0.25,
|
282
|
+
maxIterations: 10,
|
283
|
+
lineHeight: 1.3,
|
284
|
+
overflow: 'ellipsis'
|
285
|
+
};
|
286
|
+
case 'button':
|
287
|
+
return {
|
288
|
+
mode: 'single-line',
|
289
|
+
minSize: 14,
|
290
|
+
maxSize: 18,
|
291
|
+
precision: 0.25,
|
292
|
+
maxIterations: 10,
|
293
|
+
lineHeight: 1,
|
294
|
+
overflow: 'ellipsis'
|
295
|
+
};
|
296
|
+
default:
|
297
|
+
return {};
|
298
|
+
}
|
299
|
+
}
|
300
|
+
|
301
|
+
/**
|
302
|
+
* Create responsive text fitting
|
303
|
+
*/
|
304
|
+
public createResponsiveFitting(
|
305
|
+
element: Element,
|
306
|
+
configs: Array<{
|
307
|
+
containerSize: number;
|
308
|
+
config: FittingConfig;
|
309
|
+
}>
|
310
|
+
): void {
|
311
|
+
const htmlElement = element as HTMLElement;
|
312
|
+
const text = element.textContent || '';
|
313
|
+
|
314
|
+
// Sort configs by container size
|
315
|
+
const sortedConfigs = configs.sort((a, b) => a.containerSize - b.containerSize);
|
316
|
+
|
317
|
+
// Create resize observer to update fitting
|
318
|
+
const resizeObserver = new ResizeObserver((entries) => {
|
319
|
+
for (const entry of entries) {
|
320
|
+
const containerWidth = entry.contentRect.width;
|
321
|
+
const containerHeight = entry.contentRect.height;
|
322
|
+
|
323
|
+
// Find appropriate config
|
324
|
+
let activeConfig = sortedConfigs[0]!.config;
|
325
|
+
for (const { containerSize, config } of sortedConfigs) {
|
326
|
+
if (containerWidth >= containerSize) {
|
327
|
+
activeConfig = config;
|
328
|
+
}
|
329
|
+
}
|
330
|
+
|
331
|
+
// Apply fitting
|
332
|
+
const result = this.fitText(element, text, containerWidth, containerHeight, activeConfig);
|
333
|
+
this.applyFitting(element, result, activeConfig);
|
334
|
+
}
|
335
|
+
});
|
336
|
+
|
337
|
+
resizeObserver.observe(element);
|
338
|
+
|
339
|
+
// Store observer for cleanup
|
340
|
+
(htmlElement as any)._proteusTextFittingObserver = resizeObserver;
|
341
|
+
}
|
342
|
+
|
343
|
+
/**
|
344
|
+
* Remove responsive text fitting
|
345
|
+
*/
|
346
|
+
public removeResponsiveFitting(element: Element): void {
|
347
|
+
const htmlElement = element as HTMLElement;
|
348
|
+
const observer = (htmlElement as any)._proteusTextFittingObserver;
|
349
|
+
|
350
|
+
if (observer) {
|
351
|
+
observer.disconnect();
|
352
|
+
delete (htmlElement as any)._proteusTextFittingObserver;
|
353
|
+
}
|
354
|
+
}
|
355
|
+
}
|