@nasser-sw/fabric 7.0.1-beta1 → 7.0.1-beta10
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/0 +0 -0
- package/debug/{konva → konva-master}/CHANGELOG.md +2 -1
- package/debug/{konva → konva-master}/README.md +7 -3
- package/debug/{konva → konva-master}/package.json +1 -1
- package/debug/{konva → konva-master}/release.sh +1 -4
- package/debug/{konva → konva-master}/src/Canvas.ts +37 -0
- package/debug/{konva → konva-master}/src/shapes/Text.ts +2 -2
- package/dist/index.js +1853 -288
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/index.min.mjs +1 -1
- package/dist/index.min.mjs.map +1 -1
- package/dist/index.mjs +1853 -288
- package/dist/index.mjs.map +1 -1
- package/dist/index.node.cjs +1853 -288
- package/dist/index.node.cjs.map +1 -1
- package/dist/index.node.mjs +1853 -288
- package/dist/index.node.mjs.map +1 -1
- package/dist/package.json.min.mjs +1 -1
- package/dist/package.json.mjs +1 -1
- package/dist/src/shapes/Line.d.ts +33 -86
- package/dist/src/shapes/Line.d.ts.map +1 -1
- package/dist/src/shapes/Line.min.mjs +1 -1
- package/dist/src/shapes/Line.min.mjs.map +1 -1
- package/dist/src/shapes/Line.mjs +405 -159
- package/dist/src/shapes/Line.mjs.map +1 -1
- package/dist/src/shapes/Polyline.d.ts +7 -0
- package/dist/src/shapes/Polyline.d.ts.map +1 -1
- package/dist/src/shapes/Polyline.min.mjs +1 -1
- package/dist/src/shapes/Polyline.min.mjs.map +1 -1
- package/dist/src/shapes/Polyline.mjs +48 -16
- package/dist/src/shapes/Polyline.mjs.map +1 -1
- package/dist/src/shapes/Text/Text.d.ts +19 -0
- package/dist/src/shapes/Text/Text.d.ts.map +1 -1
- package/dist/src/shapes/Text/Text.min.mjs +1 -1
- package/dist/src/shapes/Text/Text.min.mjs.map +1 -1
- package/dist/src/shapes/Text/Text.mjs +302 -16
- package/dist/src/shapes/Text/Text.mjs.map +1 -1
- package/dist/src/shapes/Textbox.d.ts +43 -1
- package/dist/src/shapes/Textbox.d.ts.map +1 -1
- package/dist/src/shapes/Textbox.min.mjs +1 -1
- package/dist/src/shapes/Textbox.min.mjs.map +1 -1
- package/dist/src/shapes/Textbox.mjs +521 -67
- package/dist/src/shapes/Textbox.mjs.map +1 -1
- package/dist/src/shapes/Triangle.d.ts +27 -2
- package/dist/src/shapes/Triangle.d.ts.map +1 -1
- package/dist/src/shapes/Triangle.min.mjs +1 -1
- package/dist/src/shapes/Triangle.min.mjs.map +1 -1
- package/dist/src/shapes/Triangle.mjs +72 -12
- package/dist/src/shapes/Triangle.mjs.map +1 -1
- package/dist/src/text/examples/arabicTextExample.d.ts +60 -0
- package/dist/src/text/examples/arabicTextExample.d.ts.map +1 -0
- package/dist/src/text/measure.d.ts +9 -0
- package/dist/src/text/measure.d.ts.map +1 -1
- package/dist/src/text/measure.min.mjs +1 -1
- package/dist/src/text/measure.min.mjs.map +1 -1
- package/dist/src/text/measure.mjs +175 -4
- package/dist/src/text/measure.mjs.map +1 -1
- package/dist/src/text/overlayEditor.d.ts.map +1 -1
- package/dist/src/text/overlayEditor.min.mjs +1 -1
- package/dist/src/text/overlayEditor.min.mjs.map +1 -1
- package/dist/src/text/overlayEditor.mjs +155 -9
- package/dist/src/text/overlayEditor.mjs.map +1 -1
- package/dist/src/text/scriptUtils.d.ts +142 -0
- package/dist/src/text/scriptUtils.d.ts.map +1 -0
- package/dist/src/text/scriptUtils.min.mjs +2 -0
- package/dist/src/text/scriptUtils.min.mjs.map +1 -0
- package/dist/src/text/scriptUtils.mjs +212 -0
- package/dist/src/text/scriptUtils.mjs.map +1 -0
- package/dist/src/util/misc/cornerRadius.d.ts +70 -0
- package/dist/src/util/misc/cornerRadius.d.ts.map +1 -0
- package/dist/src/util/misc/cornerRadius.min.mjs +2 -0
- package/dist/src/util/misc/cornerRadius.min.mjs.map +1 -0
- package/dist/src/util/misc/cornerRadius.mjs +181 -0
- package/dist/src/util/misc/cornerRadius.mjs.map +1 -0
- package/dist-extensions/src/shapes/CustomLine.d.ts +10 -0
- package/dist-extensions/src/shapes/CustomLine.d.ts.map +1 -0
- package/dist-extensions/src/shapes/Line.d.ts +33 -86
- package/dist-extensions/src/shapes/Line.d.ts.map +1 -1
- package/dist-extensions/src/shapes/Polyline.d.ts +7 -0
- package/dist-extensions/src/shapes/Polyline.d.ts.map +1 -1
- package/dist-extensions/src/shapes/Text/Text.d.ts +19 -0
- package/dist-extensions/src/shapes/Text/Text.d.ts.map +1 -1
- package/dist-extensions/src/shapes/Textbox.d.ts +43 -1
- package/dist-extensions/src/shapes/Textbox.d.ts.map +1 -1
- package/dist-extensions/src/shapes/Triangle.d.ts +27 -2
- package/dist-extensions/src/shapes/Triangle.d.ts.map +1 -1
- package/dist-extensions/src/text/measure.d.ts +9 -0
- package/dist-extensions/src/text/measure.d.ts.map +1 -1
- package/dist-extensions/src/text/overlayEditor.d.ts.map +1 -1
- package/dist-extensions/src/text/scriptUtils.d.ts +142 -0
- package/dist-extensions/src/text/scriptUtils.d.ts.map +1 -0
- package/dist-extensions/src/util/misc/cornerRadius.d.ts +70 -0
- package/dist-extensions/src/util/misc/cornerRadius.d.ts.map +1 -0
- package/fabric-test-editor.html +3552 -0
- package/fabric-test2.html +647 -0
- package/fabric.ts +182 -182
- package/fonts/STV Bold.ttf +0 -0
- package/fonts/STV Light.ttf +0 -0
- package/fonts/STV Regular.ttf +0 -0
- package/package.json +164 -164
- package/src/shapes/Line.ts +484 -157
- package/src/shapes/Polyline.ts +70 -29
- package/src/shapes/Text/Text.ts +317 -19
- package/src/shapes/Textbox.ts +544 -12
- package/src/shapes/Triangle.spec.ts +76 -0
- package/src/shapes/Triangle.ts +85 -15
- package/src/text/measure.ts +200 -50
- package/src/text/overlayEditor.ts +164 -12
- package/src/util/misc/cornerRadius.spec.ts +141 -0
- package/src/util/misc/cornerRadius.ts +269 -0
- /package/debug/{konva → konva-master}/LICENSE +0 -0
- /package/debug/{konva → konva-master}/gulpfile.mjs +0 -0
- /package/debug/{konva → konva-master}/resources/doc-includes/ContainerParams.txt +0 -0
- /package/debug/{konva → konva-master}/resources/doc-includes/NodeParams.txt +0 -0
- /package/debug/{konva → konva-master}/resources/doc-includes/ShapeParams.txt +0 -0
- /package/debug/{konva → konva-master}/resources/jsdoc.conf.json +0 -0
- /package/debug/{konva → konva-master}/rollup.config.mjs +0 -0
- /package/debug/{konva → konva-master}/src/Animation.ts +0 -0
- /package/debug/{konva → konva-master}/src/BezierFunctions.ts +0 -0
- /package/debug/{konva → konva-master}/src/Container.ts +0 -0
- /package/debug/{konva → konva-master}/src/Context.ts +0 -0
- /package/debug/{konva → konva-master}/src/Core.ts +0 -0
- /package/debug/{konva → konva-master}/src/DragAndDrop.ts +0 -0
- /package/debug/{konva → konva-master}/src/Factory.ts +0 -0
- /package/debug/{konva → konva-master}/src/FastLayer.ts +0 -0
- /package/debug/{konva → konva-master}/src/Global.ts +0 -0
- /package/debug/{konva → konva-master}/src/Group.ts +0 -0
- /package/debug/{konva → konva-master}/src/Layer.ts +0 -0
- /package/debug/{konva → konva-master}/src/Node.ts +0 -0
- /package/debug/{konva → konva-master}/src/PointerEvents.ts +0 -0
- /package/debug/{konva → konva-master}/src/Shape.ts +0 -0
- /package/debug/{konva → konva-master}/src/Stage.ts +0 -0
- /package/debug/{konva → konva-master}/src/Tween.ts +0 -0
- /package/debug/{konva → konva-master}/src/Util.ts +0 -0
- /package/debug/{konva → konva-master}/src/Validators.ts +0 -0
- /package/debug/{konva → konva-master}/src/_CoreInternals.ts +0 -0
- /package/debug/{konva → konva-master}/src/_FullInternals.ts +0 -0
- /package/debug/{konva → konva-master}/src/canvas-backend.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Blur.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Brighten.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Brightness.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Contrast.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Emboss.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Enhance.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Grayscale.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/HSL.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/HSV.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Invert.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Kaleidoscope.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Mask.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Noise.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Pixelate.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Posterize.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/RGB.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/RGBA.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Sepia.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Solarize.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Threshold.ts +0 -0
- /package/debug/{konva → konva-master}/src/index.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Arc.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Arrow.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Circle.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Ellipse.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Image.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Label.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Line.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Path.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Rect.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/RegularPolygon.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Ring.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Sprite.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Star.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/TextPath.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Transformer.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Wedge.ts +0 -0
- /package/debug/{konva → konva-master}/src/skia-backend.ts +0 -0
- /package/debug/{konva → konva-master}/src/types.ts +0 -0
- /package/debug/{konva → konva-master}/tsconfig.json +0 -0
- /package/debug/{konva → konva-master}/tsconfig.test.json +0 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Triangle } from './Triangle';
|
|
2
|
+
|
|
3
|
+
describe('Triangle with Corner Radius', () => {
|
|
4
|
+
describe('constructor', () => {
|
|
5
|
+
it('should create triangle with default cornerRadius of 0', () => {
|
|
6
|
+
const triangle = new Triangle();
|
|
7
|
+
expect(triangle.cornerRadius).toBe(0);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('should create triangle with specified cornerRadius', () => {
|
|
11
|
+
const triangle = new Triangle({ cornerRadius: 10 });
|
|
12
|
+
expect(triangle.cornerRadius).toBe(10);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('rendering', () => {
|
|
17
|
+
let canvas: HTMLCanvasElement;
|
|
18
|
+
let ctx: CanvasRenderingContext2D;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
canvas = document.createElement('canvas');
|
|
22
|
+
canvas.width = 200;
|
|
23
|
+
canvas.height = 200;
|
|
24
|
+
ctx = canvas.getContext('2d')!;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should render sharp triangle when cornerRadius is 0', () => {
|
|
28
|
+
const triangle = new Triangle({ width: 100, height: 100, cornerRadius: 0 });
|
|
29
|
+
const spy = jest.spyOn(ctx, 'moveTo');
|
|
30
|
+
const lineSpy = jest.spyOn(ctx, 'lineTo');
|
|
31
|
+
const bezierSpy = jest.spyOn(ctx, 'bezierCurveTo');
|
|
32
|
+
|
|
33
|
+
triangle._render(ctx);
|
|
34
|
+
|
|
35
|
+
expect(spy).toHaveBeenCalled();
|
|
36
|
+
expect(lineSpy).toHaveBeenCalled();
|
|
37
|
+
expect(bezierSpy).not.toHaveBeenCalled(); // No curves for sharp corners
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should render rounded triangle when cornerRadius > 0', () => {
|
|
41
|
+
const triangle = new Triangle({ width: 100, height: 100, cornerRadius: 10 });
|
|
42
|
+
const bezierSpy = jest.spyOn(ctx, 'bezierCurveTo');
|
|
43
|
+
|
|
44
|
+
triangle._render(ctx);
|
|
45
|
+
|
|
46
|
+
expect(bezierSpy).toHaveBeenCalled(); // Should use curves for rounded corners
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('toObject', () => {
|
|
51
|
+
it('should include cornerRadius in serialized object', () => {
|
|
52
|
+
const triangle = new Triangle({ cornerRadius: 15 });
|
|
53
|
+
const obj = triangle.toObject();
|
|
54
|
+
|
|
55
|
+
expect(obj.cornerRadius).toBe(15);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('_toSVG', () => {
|
|
60
|
+
it('should generate polygon SVG for sharp triangle', () => {
|
|
61
|
+
const triangle = new Triangle({ width: 100, height: 100, cornerRadius: 0 });
|
|
62
|
+
const svg = triangle._toSVG();
|
|
63
|
+
|
|
64
|
+
expect(svg.join('')).toContain('<polygon');
|
|
65
|
+
expect(svg.join('')).toContain('points=');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should generate path SVG for rounded triangle', () => {
|
|
69
|
+
const triangle = new Triangle({ width: 100, height: 100, cornerRadius: 10 });
|
|
70
|
+
const svg = triangle._toSVG();
|
|
71
|
+
|
|
72
|
+
expect(svg.join('')).toContain('<path');
|
|
73
|
+
expect(svg.join('')).toContain('d=');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
});
|
package/src/shapes/Triangle.ts
CHANGED
|
@@ -1,24 +1,50 @@
|
|
|
1
1
|
import { classRegistry } from '../ClassRegistry';
|
|
2
|
-
import { FabricObject } from './Object/FabricObject';
|
|
2
|
+
import { FabricObject, cacheProperties } from './Object/FabricObject';
|
|
3
3
|
import type { FabricObjectProps, SerializedObjectProps } from './Object/types';
|
|
4
4
|
import type { TClassProperties, TOptions } from '../typedefs';
|
|
5
5
|
import type { ObjectEvents } from '../EventTypeDefs';
|
|
6
|
+
import {
|
|
7
|
+
applyCornerRadiusToPolygon,
|
|
8
|
+
renderRoundedPolygon,
|
|
9
|
+
generateRoundedPolygonPath,
|
|
10
|
+
} from '../util/misc/cornerRadius';
|
|
6
11
|
|
|
7
12
|
export const triangleDefaultValues: Partial<TClassProperties<Triangle>> = {
|
|
8
13
|
width: 100,
|
|
9
14
|
height: 100,
|
|
15
|
+
cornerRadius: 0,
|
|
10
16
|
};
|
|
11
17
|
|
|
18
|
+
interface UniqueTriangleProps {
|
|
19
|
+
cornerRadius: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SerializedTriangleProps
|
|
23
|
+
extends SerializedObjectProps,
|
|
24
|
+
UniqueTriangleProps {}
|
|
25
|
+
|
|
26
|
+
export interface TriangleProps extends FabricObjectProps, UniqueTriangleProps {}
|
|
27
|
+
|
|
28
|
+
const TRIANGLE_PROPS = ['cornerRadius'] as const;
|
|
29
|
+
|
|
12
30
|
export class Triangle<
|
|
13
|
-
Props extends TOptions<
|
|
14
|
-
SProps extends
|
|
31
|
+
Props extends TOptions<TriangleProps> = Partial<TriangleProps>,
|
|
32
|
+
SProps extends SerializedTriangleProps = SerializedTriangleProps,
|
|
15
33
|
EventSpec extends ObjectEvents = ObjectEvents,
|
|
16
34
|
>
|
|
17
35
|
extends FabricObject<Props, SProps, EventSpec>
|
|
18
|
-
implements
|
|
36
|
+
implements TriangleProps
|
|
19
37
|
{
|
|
38
|
+
/**
|
|
39
|
+
* Corner radius for rounded triangle corners
|
|
40
|
+
* @type Number
|
|
41
|
+
*/
|
|
42
|
+
declare cornerRadius: number;
|
|
43
|
+
|
|
20
44
|
static type = 'Triangle';
|
|
21
45
|
|
|
46
|
+
static cacheProperties = [...cacheProperties, ...TRIANGLE_PROPS];
|
|
47
|
+
|
|
22
48
|
static ownDefaults = triangleDefaultValues;
|
|
23
49
|
|
|
24
50
|
static getDefaults(): Record<string, any> {
|
|
@@ -35,33 +61,77 @@ export class Triangle<
|
|
|
35
61
|
this.setOptions(options);
|
|
36
62
|
}
|
|
37
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Get triangle points as an array of XY coordinates
|
|
66
|
+
* @private
|
|
67
|
+
*/
|
|
68
|
+
private _getTrianglePoints() {
|
|
69
|
+
const widthBy2 = this.width / 2;
|
|
70
|
+
const heightBy2 = this.height / 2;
|
|
71
|
+
|
|
72
|
+
return [
|
|
73
|
+
{ x: -widthBy2, y: heightBy2 }, // bottom left
|
|
74
|
+
{ x: 0, y: -heightBy2 }, // top center
|
|
75
|
+
{ x: widthBy2, y: heightBy2 }, // bottom right
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
|
|
38
79
|
/**
|
|
39
80
|
* @private
|
|
40
81
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
41
82
|
*/
|
|
42
83
|
_render(ctx: CanvasRenderingContext2D) {
|
|
43
|
-
|
|
44
|
-
|
|
84
|
+
if (this.cornerRadius > 0) {
|
|
85
|
+
// Render rounded triangle
|
|
86
|
+
const points = this._getTrianglePoints();
|
|
87
|
+
const roundedCorners = applyCornerRadiusToPolygon(points, this.cornerRadius);
|
|
88
|
+
renderRoundedPolygon(ctx, roundedCorners, true);
|
|
89
|
+
} else {
|
|
90
|
+
// Render sharp triangle (original implementation)
|
|
91
|
+
const widthBy2 = this.width / 2;
|
|
92
|
+
const heightBy2 = this.height / 2;
|
|
45
93
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
94
|
+
ctx.beginPath();
|
|
95
|
+
ctx.moveTo(-widthBy2, heightBy2);
|
|
96
|
+
ctx.lineTo(0, -heightBy2);
|
|
97
|
+
ctx.lineTo(widthBy2, heightBy2);
|
|
98
|
+
ctx.closePath();
|
|
99
|
+
}
|
|
51
100
|
|
|
52
101
|
this._renderPaintInOrder(ctx);
|
|
53
102
|
}
|
|
54
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Returns object representation of an instance
|
|
106
|
+
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
|
|
107
|
+
* @return {Object} object representation of an instance
|
|
108
|
+
*/
|
|
109
|
+
toObject<
|
|
110
|
+
T extends Omit<Props & TClassProperties<this>, keyof SProps>,
|
|
111
|
+
K extends keyof T = never,
|
|
112
|
+
>(propertiesToInclude: K[] = []): Pick<T, K> & SProps {
|
|
113
|
+
return super.toObject([...TRIANGLE_PROPS, ...propertiesToInclude]);
|
|
114
|
+
}
|
|
115
|
+
|
|
55
116
|
/**
|
|
56
117
|
* Returns svg representation of an instance
|
|
57
118
|
* @return {Array} an array of strings with the specific svg representation
|
|
58
119
|
* of the instance
|
|
59
120
|
*/
|
|
60
121
|
_toSVG() {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
points =
|
|
64
|
-
|
|
122
|
+
if (this.cornerRadius > 0) {
|
|
123
|
+
// Generate rounded triangle as path
|
|
124
|
+
const points = this._getTrianglePoints();
|
|
125
|
+
const roundedCorners = applyCornerRadiusToPolygon(points, this.cornerRadius);
|
|
126
|
+
const pathData = generateRoundedPolygonPath(roundedCorners, true);
|
|
127
|
+
return ['<path ', 'COMMON_PARTS', `d="${pathData}" />`];
|
|
128
|
+
} else {
|
|
129
|
+
// Original sharp triangle implementation
|
|
130
|
+
const widthBy2 = this.width / 2;
|
|
131
|
+
const heightBy2 = this.height / 2;
|
|
132
|
+
const points = `${-widthBy2} ${heightBy2},0 ${-heightBy2},${widthBy2} ${heightBy2}`;
|
|
133
|
+
return ['<polygon ', 'COMMON_PARTS', 'points="', points, '" />'];
|
|
134
|
+
}
|
|
65
135
|
}
|
|
66
136
|
}
|
|
67
137
|
|
package/src/text/measure.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Advanced Text Measurement System
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Provides precise text measurement with caching, font metrics,
|
|
5
5
|
* and DPI awareness for optimal performance and accuracy.
|
|
6
6
|
*/
|
|
@@ -62,7 +62,7 @@ function getMeasurementContext(): CanvasRenderingContext2D {
|
|
|
62
62
|
export function measureGrapheme(
|
|
63
63
|
grapheme: string,
|
|
64
64
|
options: MeasurementOptions,
|
|
65
|
-
ctx?: CanvasRenderingContext2D
|
|
65
|
+
ctx?: CanvasRenderingContext2D,
|
|
66
66
|
): GraphemeMeasurement {
|
|
67
67
|
// Check cache first
|
|
68
68
|
const cached = measurementCache.get(grapheme, options);
|
|
@@ -72,14 +72,14 @@ export function measureGrapheme(
|
|
|
72
72
|
|
|
73
73
|
// Use provided context or get global one
|
|
74
74
|
const context = ctx || getMeasurementContext();
|
|
75
|
-
|
|
75
|
+
|
|
76
76
|
// Set font properties
|
|
77
77
|
applyFontStyle(context, options);
|
|
78
|
-
|
|
78
|
+
|
|
79
79
|
// Measure the grapheme
|
|
80
80
|
const metrics = context.measureText(grapheme);
|
|
81
81
|
const fontMetrics = getFontMetrics(options);
|
|
82
|
-
|
|
82
|
+
|
|
83
83
|
// Calculate comprehensive measurements
|
|
84
84
|
const measurement: GraphemeMeasurement = {
|
|
85
85
|
width: metrics.width,
|
|
@@ -88,10 +88,10 @@ export function measureGrapheme(
|
|
|
88
88
|
descent: fontMetrics.descent,
|
|
89
89
|
baseline: fontMetrics.ascent,
|
|
90
90
|
};
|
|
91
|
-
|
|
91
|
+
|
|
92
92
|
// Cache the result
|
|
93
93
|
measurementCache.set(grapheme, options, measurement);
|
|
94
|
-
|
|
94
|
+
|
|
95
95
|
return measurement;
|
|
96
96
|
}
|
|
97
97
|
|
|
@@ -102,11 +102,11 @@ export function measureGraphemeWithKerning(
|
|
|
102
102
|
grapheme: string,
|
|
103
103
|
previousGrapheme: string | undefined,
|
|
104
104
|
options: MeasurementOptions,
|
|
105
|
-
ctx?: CanvasRenderingContext2D
|
|
105
|
+
ctx?: CanvasRenderingContext2D,
|
|
106
106
|
): KerningMeasurement {
|
|
107
107
|
// Get individual measurement
|
|
108
108
|
const individual = measureGrapheme(grapheme, options, ctx);
|
|
109
|
-
|
|
109
|
+
|
|
110
110
|
// If no previous character, kerning width equals regular width
|
|
111
111
|
if (!previousGrapheme) {
|
|
112
112
|
return {
|
|
@@ -114,7 +114,7 @@ export function measureGraphemeWithKerning(
|
|
|
114
114
|
kernedWidth: individual.width,
|
|
115
115
|
};
|
|
116
116
|
}
|
|
117
|
-
|
|
117
|
+
|
|
118
118
|
// Check kerning cache
|
|
119
119
|
const kerningPair = `${previousGrapheme}${grapheme}`;
|
|
120
120
|
const cachedKerning = kerningCache.get(kerningPair, options);
|
|
@@ -124,25 +124,83 @@ export function measureGraphemeWithKerning(
|
|
|
124
124
|
kernedWidth: cachedKerning,
|
|
125
125
|
};
|
|
126
126
|
}
|
|
127
|
-
|
|
127
|
+
|
|
128
128
|
// Use provided context or get global one
|
|
129
129
|
const context = ctx || getMeasurementContext();
|
|
130
130
|
applyFontStyle(context, options);
|
|
131
|
-
|
|
131
|
+
|
|
132
132
|
// Measure the pair
|
|
133
133
|
const pairWidth = context.measureText(previousGrapheme + grapheme).width;
|
|
134
|
-
const previousWidth = measureGrapheme(
|
|
134
|
+
const previousWidth = measureGrapheme(
|
|
135
|
+
previousGrapheme,
|
|
136
|
+
options,
|
|
137
|
+
context,
|
|
138
|
+
).width;
|
|
135
139
|
const kernedWidth = pairWidth - previousWidth;
|
|
136
|
-
|
|
140
|
+
|
|
137
141
|
// Cache kerning result
|
|
138
142
|
kerningCache.set(kerningPair, options, kernedWidth);
|
|
139
|
-
|
|
143
|
+
|
|
140
144
|
return {
|
|
141
145
|
...individual,
|
|
142
146
|
kernedWidth,
|
|
143
147
|
};
|
|
144
148
|
}
|
|
145
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Get a representative character for font metrics measurement
|
|
152
|
+
* Uses canvas to test which scripts the font actually supports
|
|
153
|
+
*/
|
|
154
|
+
function getRepresentativeCharacter(fontFamily: string): string {
|
|
155
|
+
const context = getMeasurementContext();
|
|
156
|
+
|
|
157
|
+
// Wait for font to be ready if possible
|
|
158
|
+
if (typeof document !== 'undefined' && 'fonts' in document) {
|
|
159
|
+
try {
|
|
160
|
+
// Check if font is ready, if not, use fallback immediately
|
|
161
|
+
if (!document.fonts.check(`16px ${fontFamily}`)) {
|
|
162
|
+
return 'M'; // Use safe fallback while font loads
|
|
163
|
+
}
|
|
164
|
+
} catch (e) {
|
|
165
|
+
// Font check failed, use fallback
|
|
166
|
+
return 'M';
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Test characters for different scripts
|
|
171
|
+
const testChars = [
|
|
172
|
+
{ char: 'م', script: 'Arabic' }, // Arabic
|
|
173
|
+
{ char: 'א', script: 'Hebrew' }, // Hebrew
|
|
174
|
+
{ char: 'अ', script: 'Devanagari' }, // Hindi/Sanskrit
|
|
175
|
+
{ char: 'ا', script: 'Urdu' }, // Urdu
|
|
176
|
+
{ char: 'ک', script: 'Persian' }, // Persian
|
|
177
|
+
{ char: 'த', script: 'Tamil' }, // Tamil
|
|
178
|
+
{ char: 'ก', script: 'Thai' }, // Thai
|
|
179
|
+
{ char: 'М', script: 'Cyrillic' }, // Cyrillic
|
|
180
|
+
{ char: 'Ω', script: 'Greek' }, // Greek
|
|
181
|
+
{ char: 'M', script: 'Latin' } // Latin (fallback)
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
// Set the font
|
|
185
|
+
context.font = `16px ${fontFamily}`;
|
|
186
|
+
|
|
187
|
+
// Test each character to see which ones render properly
|
|
188
|
+
// Use a more robust width check to avoid false positives
|
|
189
|
+
const fallbackWidth = context.measureText('M').width;
|
|
190
|
+
|
|
191
|
+
for (const test of testChars) {
|
|
192
|
+
const metrics = context.measureText(test.char);
|
|
193
|
+
|
|
194
|
+
// Character is valid if it has width and isn't just a fallback glyph
|
|
195
|
+
if (metrics.width > 0 && Math.abs(metrics.width - fallbackWidth) > 0.1) {
|
|
196
|
+
return test.char;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Fallback to Latin 'M'
|
|
201
|
+
return 'M';
|
|
202
|
+
}
|
|
203
|
+
|
|
146
204
|
/**
|
|
147
205
|
* Get font metrics for layout calculations
|
|
148
206
|
*/
|
|
@@ -152,20 +210,24 @@ export function getFontMetrics(options: MeasurementOptions): FontMetrics {
|
|
|
152
210
|
if (cached) {
|
|
153
211
|
return cached;
|
|
154
212
|
}
|
|
155
|
-
|
|
213
|
+
|
|
156
214
|
const context = getMeasurementContext();
|
|
157
215
|
applyFontStyle(context, options);
|
|
158
|
-
|
|
159
|
-
// Use
|
|
160
|
-
const
|
|
216
|
+
|
|
217
|
+
// Use representative character based on font's primary script
|
|
218
|
+
const sample = getRepresentativeCharacter(options.fontFamily);
|
|
219
|
+
const metrics = context.measureText(sample);
|
|
161
220
|
const fontSize = options.fontSize;
|
|
162
|
-
|
|
221
|
+
|
|
163
222
|
// Calculate metrics with fallbacks
|
|
164
|
-
const fontBoundingBoxAscent =
|
|
165
|
-
|
|
166
|
-
const
|
|
223
|
+
const fontBoundingBoxAscent =
|
|
224
|
+
metrics.fontBoundingBoxAscent ?? fontSize * 0.91;
|
|
225
|
+
const fontBoundingBoxDescent =
|
|
226
|
+
metrics.fontBoundingBoxDescent ?? fontSize * 0.21;
|
|
227
|
+
const actualBoundingBoxAscent =
|
|
228
|
+
metrics.actualBoundingBoxAscent ?? fontSize * 0.716;
|
|
167
229
|
const actualBoundingBoxDescent = metrics.actualBoundingBoxDescent ?? 0;
|
|
168
|
-
|
|
230
|
+
|
|
169
231
|
const result: FontMetrics = {
|
|
170
232
|
ascent: fontBoundingBoxAscent,
|
|
171
233
|
descent: fontBoundingBoxDescent,
|
|
@@ -176,7 +238,7 @@ export function getFontMetrics(options: MeasurementOptions): FontMetrics {
|
|
|
176
238
|
actualBoundingBoxAscent,
|
|
177
239
|
actualBoundingBoxDescent,
|
|
178
240
|
};
|
|
179
|
-
|
|
241
|
+
|
|
180
242
|
fontMetricsCache.set(cacheKey, result);
|
|
181
243
|
return result;
|
|
182
244
|
}
|
|
@@ -184,21 +246,24 @@ export function getFontMetrics(options: MeasurementOptions): FontMetrics {
|
|
|
184
246
|
/**
|
|
185
247
|
* Apply font styling to canvas context
|
|
186
248
|
*/
|
|
187
|
-
function applyFontStyle(
|
|
249
|
+
function applyFontStyle(
|
|
250
|
+
ctx: CanvasRenderingContext2D,
|
|
251
|
+
options: MeasurementOptions,
|
|
252
|
+
): void {
|
|
188
253
|
const fontDeclaration = getFontDeclaration(options);
|
|
189
254
|
ctx.font = fontDeclaration;
|
|
190
|
-
|
|
255
|
+
|
|
191
256
|
if (options.letterSpacing) {
|
|
192
257
|
// Modern browsers support letterSpacing
|
|
193
258
|
if ('letterSpacing' in ctx) {
|
|
194
259
|
(ctx as any).letterSpacing = `${options.letterSpacing}px`;
|
|
195
260
|
}
|
|
196
261
|
}
|
|
197
|
-
|
|
262
|
+
|
|
198
263
|
if (options.direction) {
|
|
199
264
|
ctx.direction = options.direction;
|
|
200
265
|
}
|
|
201
|
-
|
|
266
|
+
|
|
202
267
|
ctx.textBaseline = 'alphabetic';
|
|
203
268
|
}
|
|
204
269
|
|
|
@@ -207,14 +272,18 @@ function applyFontStyle(ctx: CanvasRenderingContext2D, options: MeasurementOptio
|
|
|
207
272
|
*/
|
|
208
273
|
function getFontDeclaration(options: MeasurementOptions): string {
|
|
209
274
|
const { fontStyle, fontWeight, fontSize, fontFamily } = options;
|
|
210
|
-
|
|
275
|
+
|
|
211
276
|
// Normalize font family (add quotes if needed)
|
|
212
|
-
|
|
213
|
-
|
|
277
|
+
let normalizedFamily =
|
|
278
|
+
fontFamily.includes(' ') &&
|
|
279
|
+
!fontFamily.includes('"') &&
|
|
214
280
|
!fontFamily.includes("'")
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
281
|
+
? `"${fontFamily}"`
|
|
282
|
+
: fontFamily;
|
|
283
|
+
|
|
284
|
+
// Note: Font fallbacks are handled in the rendering phase only
|
|
285
|
+
// to avoid affecting measurement calculations for text wrapping
|
|
286
|
+
|
|
218
287
|
return `${fontStyle} ${fontWeight} ${fontSize}px ${normalizedFamily}`;
|
|
219
288
|
}
|
|
220
289
|
|
|
@@ -301,12 +370,19 @@ export class MeasurementCache {
|
|
|
301
370
|
return `${fontDecl}|${grapheme}|${letterSpacing}`;
|
|
302
371
|
}
|
|
303
372
|
|
|
304
|
-
get(
|
|
373
|
+
get(
|
|
374
|
+
grapheme: string,
|
|
375
|
+
options: MeasurementOptions,
|
|
376
|
+
): GraphemeMeasurement | undefined {
|
|
305
377
|
const key = this.getCacheKey(grapheme, options);
|
|
306
378
|
return this.cache.get(key);
|
|
307
379
|
}
|
|
308
380
|
|
|
309
|
-
set(
|
|
381
|
+
set(
|
|
382
|
+
grapheme: string,
|
|
383
|
+
options: MeasurementOptions,
|
|
384
|
+
measurement: GraphemeMeasurement,
|
|
385
|
+
): void {
|
|
310
386
|
const key = this.getCacheKey(grapheme, options);
|
|
311
387
|
this.cache.set(key, measurement);
|
|
312
388
|
}
|
|
@@ -380,6 +456,14 @@ export const measurementCache = new MeasurementCache();
|
|
|
380
456
|
export const kerningCache = new KerningCache();
|
|
381
457
|
export const fontMetricsCache = new FontMetricsCache();
|
|
382
458
|
|
|
459
|
+
// Set up font loading listener to clear caches when fonts change
|
|
460
|
+
if (typeof document !== 'undefined' && 'fonts' in document) {
|
|
461
|
+
document.fonts.addEventListener('loadingdone', () => {
|
|
462
|
+
// Clear all caches when fonts finish loading
|
|
463
|
+
clearAllCaches();
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
383
467
|
/**
|
|
384
468
|
* Clear all measurement caches
|
|
385
469
|
*/
|
|
@@ -406,15 +490,15 @@ export function getCacheStats() {
|
|
|
406
490
|
export function batchMeasureGraphemes(
|
|
407
491
|
graphemes: string[],
|
|
408
492
|
options: MeasurementOptions,
|
|
409
|
-
ctx?: CanvasRenderingContext2D
|
|
493
|
+
ctx?: CanvasRenderingContext2D,
|
|
410
494
|
): GraphemeMeasurement[] {
|
|
411
495
|
const context = ctx || getMeasurementContext();
|
|
412
496
|
applyFontStyle(context, options);
|
|
413
|
-
|
|
497
|
+
|
|
414
498
|
// Separate cached and uncached measurements
|
|
415
499
|
const results: GraphemeMeasurement[] = new Array(graphemes.length);
|
|
416
500
|
const uncachedIndices: number[] = [];
|
|
417
|
-
|
|
501
|
+
|
|
418
502
|
// Check cache for all graphemes
|
|
419
503
|
graphemes.forEach((grapheme, index) => {
|
|
420
504
|
const cached = measurementCache.get(grapheme, options);
|
|
@@ -424,13 +508,13 @@ export function batchMeasureGraphemes(
|
|
|
424
508
|
uncachedIndices.push(index);
|
|
425
509
|
}
|
|
426
510
|
});
|
|
427
|
-
|
|
511
|
+
|
|
428
512
|
// Measure uncached graphemes
|
|
429
513
|
const fontMetrics = getFontMetrics(options);
|
|
430
|
-
uncachedIndices.forEach(index => {
|
|
514
|
+
uncachedIndices.forEach((index) => {
|
|
431
515
|
const grapheme = graphemes[index];
|
|
432
516
|
const metrics = context.measureText(grapheme);
|
|
433
|
-
|
|
517
|
+
|
|
434
518
|
const measurement: GraphemeMeasurement = {
|
|
435
519
|
width: metrics.width,
|
|
436
520
|
height: fontMetrics.lineHeight,
|
|
@@ -438,11 +522,11 @@ export function batchMeasureGraphemes(
|
|
|
438
522
|
descent: fontMetrics.descent,
|
|
439
523
|
baseline: fontMetrics.ascent,
|
|
440
524
|
};
|
|
441
|
-
|
|
525
|
+
|
|
442
526
|
measurementCache.set(grapheme, options, measurement);
|
|
443
527
|
results[index] = measurement;
|
|
444
528
|
});
|
|
445
|
-
|
|
529
|
+
|
|
446
530
|
return results;
|
|
447
531
|
}
|
|
448
532
|
|
|
@@ -451,13 +535,13 @@ export function batchMeasureGraphemes(
|
|
|
451
535
|
*/
|
|
452
536
|
export function estimateTextWidth(
|
|
453
537
|
text: string,
|
|
454
|
-
options: MeasurementOptions
|
|
538
|
+
options: MeasurementOptions,
|
|
455
539
|
): number {
|
|
456
540
|
// Use average character width for estimation
|
|
457
541
|
const avgChar = 'n'; // Representative character
|
|
458
542
|
const avgMeasurement = measureGrapheme(avgChar, options);
|
|
459
543
|
const letterSpacing = options.letterSpacing || 0;
|
|
460
|
-
|
|
544
|
+
|
|
461
545
|
return text.length * (avgMeasurement.width + letterSpacing);
|
|
462
546
|
}
|
|
463
547
|
|
|
@@ -466,11 +550,77 @@ export function estimateTextWidth(
|
|
|
466
550
|
*/
|
|
467
551
|
export function isFontReady(fontFamily: string): boolean {
|
|
468
552
|
if (typeof document === 'undefined') return true;
|
|
469
|
-
|
|
553
|
+
|
|
470
554
|
if ('fonts' in document) {
|
|
471
555
|
return document.fonts.check(`16px ${fontFamily}`);
|
|
472
556
|
}
|
|
473
|
-
|
|
557
|
+
|
|
474
558
|
// Fallback - assume font is ready
|
|
475
559
|
return true;
|
|
476
|
-
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Detect if a font lacks English glyph support
|
|
564
|
+
* These fonts should use browser-native measurement instead of Fabric's character-by-character measurement
|
|
565
|
+
*/
|
|
566
|
+
export function fontLacksEnglishGlyphs(fontFamily: string): boolean {
|
|
567
|
+
if (typeof document === 'undefined') return false;
|
|
568
|
+
|
|
569
|
+
// Known fonts that lack English glyphs
|
|
570
|
+
const knownNonEnglishFonts = [
|
|
571
|
+
'stv', 'arabic', 'naskh', 'thuluth', 'kufi', 'diwani',
|
|
572
|
+
'nastaliq', 'kufic', 'hijazi', 'madinah', 'makkah'
|
|
573
|
+
];
|
|
574
|
+
|
|
575
|
+
const lowerFontFamily = fontFamily.toLowerCase();
|
|
576
|
+
|
|
577
|
+
// Check known list first
|
|
578
|
+
if (knownNonEnglishFonts.some(font => lowerFontFamily.includes(font))) {
|
|
579
|
+
return true;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Dynamic glyph support detection
|
|
583
|
+
const context = getMeasurementContext();
|
|
584
|
+
context.font = `16px ${fontFamily}`;
|
|
585
|
+
|
|
586
|
+
// Test English characters
|
|
587
|
+
const englishChars = ['A', 'B', 'C', 'a', 'b', 'c', 'M', 'W'];
|
|
588
|
+
const fallbackFont = 'Arial, sans-serif';
|
|
589
|
+
|
|
590
|
+
// Measure with target font
|
|
591
|
+
const targetWidths = englishChars.map(char => context.measureText(char).width);
|
|
592
|
+
|
|
593
|
+
// Measure with fallback font
|
|
594
|
+
context.font = `16px ${fallbackFont}`;
|
|
595
|
+
const fallbackWidths = englishChars.map(char => context.measureText(char).width);
|
|
596
|
+
|
|
597
|
+
// If most measurements are identical, the font likely doesn't have English glyphs
|
|
598
|
+
let identicalCount = 0;
|
|
599
|
+
for (let i = 0; i < englishChars.length; i++) {
|
|
600
|
+
if (Math.abs(targetWidths[i] - fallbackWidths[i]) < 0.5) {
|
|
601
|
+
identicalCount++;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const lacksSupportThreshold = englishChars.length * 0.7; // 70% identical = lacks support
|
|
606
|
+
const lacksSupport = identicalCount >= lacksSupportThreshold;
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
return lacksSupport;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Cache for font glyph detection results
|
|
613
|
+
const fontGlyphCache = new Map<string, boolean>();
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Cached version of font glyph detection
|
|
617
|
+
*/
|
|
618
|
+
export function fontLacksEnglishGlyphsCached(fontFamily: string): boolean {
|
|
619
|
+
if (fontGlyphCache.has(fontFamily)) {
|
|
620
|
+
return fontGlyphCache.get(fontFamily)!;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const result = fontLacksEnglishGlyphs(fontFamily);
|
|
624
|
+
fontGlyphCache.set(fontFamily, result);
|
|
625
|
+
return result;
|
|
626
|
+
}
|