@nasser-sw/fabric 7.0.1-beta1 → 7.0.1-beta3

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.
Files changed (66) hide show
  1. package/dist/index.js +504 -102
  2. package/dist/index.js.map +1 -1
  3. package/dist/index.min.js +1 -1
  4. package/dist/index.min.js.map +1 -1
  5. package/dist/index.min.mjs +1 -1
  6. package/dist/index.min.mjs.map +1 -1
  7. package/dist/index.mjs +504 -102
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/index.node.cjs +504 -102
  10. package/dist/index.node.cjs.map +1 -1
  11. package/dist/index.node.mjs +504 -102
  12. package/dist/index.node.mjs.map +1 -1
  13. package/dist/package.json.min.mjs +1 -1
  14. package/dist/package.json.mjs +1 -1
  15. package/dist/src/shapes/Polyline.d.ts +7 -0
  16. package/dist/src/shapes/Polyline.d.ts.map +1 -1
  17. package/dist/src/shapes/Polyline.min.mjs +1 -1
  18. package/dist/src/shapes/Polyline.min.mjs.map +1 -1
  19. package/dist/src/shapes/Polyline.mjs +48 -16
  20. package/dist/src/shapes/Polyline.mjs.map +1 -1
  21. package/dist/src/shapes/Text/Text.d.ts.map +1 -1
  22. package/dist/src/shapes/Text/Text.min.mjs +1 -1
  23. package/dist/src/shapes/Text/Text.min.mjs.map +1 -1
  24. package/dist/src/shapes/Text/Text.mjs +64 -12
  25. package/dist/src/shapes/Text/Text.mjs.map +1 -1
  26. package/dist/src/shapes/Textbox.d.ts.map +1 -1
  27. package/dist/src/shapes/Textbox.min.mjs +1 -1
  28. package/dist/src/shapes/Textbox.min.mjs.map +1 -1
  29. package/dist/src/shapes/Textbox.mjs +2 -52
  30. package/dist/src/shapes/Textbox.mjs.map +1 -1
  31. package/dist/src/shapes/Triangle.d.ts +27 -2
  32. package/dist/src/shapes/Triangle.d.ts.map +1 -1
  33. package/dist/src/shapes/Triangle.min.mjs +1 -1
  34. package/dist/src/shapes/Triangle.min.mjs.map +1 -1
  35. package/dist/src/shapes/Triangle.mjs +72 -12
  36. package/dist/src/shapes/Triangle.mjs.map +1 -1
  37. package/dist/src/text/overlayEditor.d.ts.map +1 -1
  38. package/dist/src/text/overlayEditor.min.mjs +1 -1
  39. package/dist/src/text/overlayEditor.min.mjs.map +1 -1
  40. package/dist/src/text/overlayEditor.mjs +143 -9
  41. package/dist/src/text/overlayEditor.mjs.map +1 -1
  42. package/dist/src/util/misc/cornerRadius.d.ts +70 -0
  43. package/dist/src/util/misc/cornerRadius.d.ts.map +1 -0
  44. package/dist/src/util/misc/cornerRadius.min.mjs +2 -0
  45. package/dist/src/util/misc/cornerRadius.min.mjs.map +1 -0
  46. package/dist/src/util/misc/cornerRadius.mjs +181 -0
  47. package/dist/src/util/misc/cornerRadius.mjs.map +1 -0
  48. package/dist-extensions/src/shapes/Polyline.d.ts +7 -0
  49. package/dist-extensions/src/shapes/Polyline.d.ts.map +1 -1
  50. package/dist-extensions/src/shapes/Text/Text.d.ts.map +1 -1
  51. package/dist-extensions/src/shapes/Textbox.d.ts.map +1 -1
  52. package/dist-extensions/src/shapes/Triangle.d.ts +27 -2
  53. package/dist-extensions/src/shapes/Triangle.d.ts.map +1 -1
  54. package/dist-extensions/src/text/overlayEditor.d.ts.map +1 -1
  55. package/dist-extensions/src/util/misc/cornerRadius.d.ts +70 -0
  56. package/dist-extensions/src/util/misc/cornerRadius.d.ts.map +1 -0
  57. package/fabric-test-editor.html +1048 -0
  58. package/package.json +164 -164
  59. package/src/shapes/Polyline.ts +70 -29
  60. package/src/shapes/Text/Text.ts +79 -14
  61. package/src/shapes/Textbox.ts +1 -1
  62. package/src/shapes/Triangle.spec.ts +76 -0
  63. package/src/shapes/Triangle.ts +85 -15
  64. package/src/text/overlayEditor.ts +152 -12
  65. package/src/util/misc/cornerRadius.spec.ts +141 -0
  66. package/src/util/misc/cornerRadius.ts +269 -0
package/package.json CHANGED
@@ -1,164 +1,164 @@
1
- {
2
- "name": "@nasser-sw/fabric",
3
- "description": "Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.",
4
- "homepage": "http://fabricjs.com/",
5
- "version": "7.0.1-beta1",
6
- "author": "Juriy Zaytsev <kangax@gmail.com>",
7
- "contributors": [
8
- {
9
- "name": "Andrea Bogazzi",
10
- "email": "andreabogazzi79@gmail.com",
11
- "url": "https://github.com/asturur"
12
- },
13
- {
14
- "name": "Shachar Nencel",
15
- "email": "shacharnen@gmail.com",
16
- "url": "https://github.com/ShaMan123"
17
- },
18
- {
19
- "name": "Steve Eberhardt",
20
- "email": "melchiar2@gmail.com",
21
- "url": "https://github.com/melchiar"
22
- }
23
- ],
24
- "keywords": [
25
- "canvas",
26
- "graphic",
27
- "graphics",
28
- "SVG",
29
- "node-canvas",
30
- "parser",
31
- "HTML5",
32
- "object model"
33
- ],
34
- "publishConfig": {
35
- "access": "public"
36
- },
37
- "repository": {
38
- "type": "git",
39
- "url": "https://github.com/fabricjs/fabric.js"
40
- },
41
- "bugs": {
42
- "url": "https://github.com/fabricjs/fabric.js/issues"
43
- },
44
- "license": "MIT",
45
- "scripts": {
46
- "docs": "typedoc",
47
- "cli": "node ./scripts/index.mjs",
48
- "sandboxscript": "node ./scripts/sandbox.mjs",
49
- "build": "npm run cli -- build",
50
- "build:fast": "npm run build -- -f",
51
- "dev": "npm run cli -- dev",
52
- "start": "npm run sandboxscript -- start",
53
- "export": "npm run cli -- website export",
54
- "test:vitest": "vitest --run --project unit-node",
55
- "test:vitest:chromium": "vitest --run --project unit-chromium",
56
- "test:vitest:firefox": "vitest --run --project unit-firefox",
57
- "test:vitest:all": "vitest --run",
58
- "test:vitest:coverage": "vitest --run --coverage --project unit-node",
59
- "test:vitest:coverage:watch": "npm run test:vitest --coverage=true",
60
- "coverage:merge": "nyc merge coveragefiles .nyc_output/merged-coverage.json",
61
- "coverage:report": "nyc report --skip-full=true --reporter=lcov --reporter=text --reporter=text-summary",
62
- "coverage:report:ci": "nyc report --reporter=text-summary",
63
- "test:e2e": "npm run playwright:typecheck && playwright test",
64
- "playwright:typecheck": "tsc -p ./e2e/tsconfig.json --noEmit",
65
- "sandbox": "npm run sandboxscript -- sandbox",
66
- "local-server": "serve ./ -l tcp://localhost:8080",
67
- "lint": "eslint src extensions",
68
- "prettier:check": "prettier --check .",
69
- "prettier:write": "prettier --write ."
70
- },
71
- "devDependencies": {
72
- "@babel/cli": "^7.28.3",
73
- "@babel/core": "^7.28.3",
74
- "@babel/preset-env": "^7.28.3",
75
- "@babel/preset-typescript": "^7.27.1",
76
- "@eslint/js": "^9.31.0",
77
- "@playwright/test": "^1.55.0",
78
- "@rollup/plugin-babel": "^6.0.4",
79
- "@rollup/plugin-json": "^6.1.0",
80
- "@rollup/plugin-terser": "^0.4.4",
81
- "@rollup/plugin-typescript": "^12.1.4",
82
- "@types/fs-extra": "^11.0.4",
83
- "@types/jsdom": "^21.1.7",
84
- "@types/micromatch": "^4.0.9",
85
- "@types/node": "^24.3.0",
86
- "@vitest/browser": "^3.2.4",
87
- "@vitest/coverage-v8": "^3.2.4",
88
- "@vitest/ui": "^3.2.4",
89
- "babel-plugin-transform-imports": "git+https://git@github.com/fabricjs/babel-plugin-transform-imports.git",
90
- "chalk": "^5.6.0",
91
- "commander": "^14.0.0",
92
- "es-toolkit": "1.39.7",
93
- "eslint-config-prettier": "^10.1.8",
94
- "fs-extra": "^11.3.1",
95
- "inquirer": "^12.9.4",
96
- "micromatch": "^4.0.8",
97
- "moment": "^2.30.1",
98
- "nyc": "^17.1.0",
99
- "prettier": "^3.6.2",
100
- "ps-list": "^8.1.1",
101
- "rollup": "^4.48.0",
102
- "semver": "^7.7.2",
103
- "serve": "^14.2.4",
104
- "tslib": "^2.8.1",
105
- "typescript": "^5.9.2",
106
- "typescript-eslint": "^8.40.0",
107
- "v8-to-istanbul": "^9.3.0",
108
- "vite": "^6.3.5",
109
- "vitest": "^3.2.4"
110
- },
111
- "engines": {
112
- "node": ">=18.0.0"
113
- },
114
- "overrides": {
115
- "canvas": {
116
- "canvas": "3.2.0"
117
- }
118
- },
119
- "module": "./dist/index.mjs",
120
- "types": "./dist/index.d.ts",
121
- "typesVersions": {
122
- ">=4.2": {
123
- "*": [
124
- "dist/index.d.ts"
125
- ],
126
- "node": [
127
- "dist/index.node.d.ts"
128
- ]
129
- }
130
- },
131
- "sideEffects": false,
132
- "exports": {
133
- ".": {
134
- "types": "./dist/index.d.ts",
135
- "import": "./dist/index.min.mjs",
136
- "require": "./dist/index.min.js",
137
- "default": "./dist/index.min.js"
138
- },
139
- "./es": {
140
- "types": "./dist/index.d.ts",
141
- "import": "./dist/fabric.min.mjs",
142
- "require": null,
143
- "default": null
144
- },
145
- "./node": {
146
- "node": "./dist/index.node.cjs",
147
- "types": "./dist/index.node.d.ts",
148
- "import": "./dist/index.node.mjs",
149
- "require": "./dist/index.node.cjs",
150
- "default": "./dist/index.node.cjs"
151
- },
152
- "./extensions": {
153
- "node": "./dist-extensions/index.mjs",
154
- "types": "./dist-extensions/extensions/index.d.ts",
155
- "import": "./dist-extensions/index.mjs",
156
- "require": null,
157
- "default": "./dist-extensions/fabric-extensions.min.js"
158
- }
159
- },
160
- "optionalDependencies": {
161
- "canvas": "^3.2.0",
162
- "jsdom": "^26.1.0"
163
- }
164
- }
1
+ {
2
+ "name": "@nasser-sw/fabric",
3
+ "description": "Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.",
4
+ "homepage": "http://fabricjs.com/",
5
+ "version": "7.0.1-beta3",
6
+ "author": "Juriy Zaytsev <kangax@gmail.com>",
7
+ "contributors": [
8
+ {
9
+ "name": "Andrea Bogazzi",
10
+ "email": "andreabogazzi79@gmail.com",
11
+ "url": "https://github.com/asturur"
12
+ },
13
+ {
14
+ "name": "Shachar Nencel",
15
+ "email": "shacharnen@gmail.com",
16
+ "url": "https://github.com/ShaMan123"
17
+ },
18
+ {
19
+ "name": "Steve Eberhardt",
20
+ "email": "melchiar2@gmail.com",
21
+ "url": "https://github.com/melchiar"
22
+ }
23
+ ],
24
+ "keywords": [
25
+ "canvas",
26
+ "graphic",
27
+ "graphics",
28
+ "SVG",
29
+ "node-canvas",
30
+ "parser",
31
+ "HTML5",
32
+ "object model"
33
+ ],
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/fabricjs/fabric.js"
40
+ },
41
+ "bugs": {
42
+ "url": "https://github.com/fabricjs/fabric.js/issues"
43
+ },
44
+ "license": "MIT",
45
+ "scripts": {
46
+ "docs": "typedoc",
47
+ "cli": "node ./scripts/index.mjs",
48
+ "sandboxscript": "node ./scripts/sandbox.mjs",
49
+ "build": "npm run cli -- build",
50
+ "build:fast": "npm run build -- -f",
51
+ "dev": "npm run cli -- dev",
52
+ "start": "npm run sandboxscript -- start",
53
+ "export": "npm run cli -- website export",
54
+ "test:vitest": "vitest --run --project unit-node",
55
+ "test:vitest:chromium": "vitest --run --project unit-chromium",
56
+ "test:vitest:firefox": "vitest --run --project unit-firefox",
57
+ "test:vitest:all": "vitest --run",
58
+ "test:vitest:coverage": "vitest --run --coverage --project unit-node",
59
+ "test:vitest:coverage:watch": "npm run test:vitest --coverage=true",
60
+ "coverage:merge": "nyc merge coveragefiles .nyc_output/merged-coverage.json",
61
+ "coverage:report": "nyc report --skip-full=true --reporter=lcov --reporter=text --reporter=text-summary",
62
+ "coverage:report:ci": "nyc report --reporter=text-summary",
63
+ "test:e2e": "npm run playwright:typecheck && playwright test",
64
+ "playwright:typecheck": "tsc -p ./e2e/tsconfig.json --noEmit",
65
+ "sandbox": "npm run sandboxscript -- sandbox",
66
+ "local-server": "serve ./ -l tcp://localhost:8080",
67
+ "lint": "eslint src extensions",
68
+ "prettier:check": "prettier --check .",
69
+ "prettier:write": "prettier --write ."
70
+ },
71
+ "devDependencies": {
72
+ "@babel/cli": "^7.28.3",
73
+ "@babel/core": "^7.28.3",
74
+ "@babel/preset-env": "^7.28.3",
75
+ "@babel/preset-typescript": "^7.27.1",
76
+ "@eslint/js": "^9.31.0",
77
+ "@playwright/test": "^1.55.0",
78
+ "@rollup/plugin-babel": "^6.0.4",
79
+ "@rollup/plugin-json": "^6.1.0",
80
+ "@rollup/plugin-terser": "^0.4.4",
81
+ "@rollup/plugin-typescript": "^12.1.4",
82
+ "@types/fs-extra": "^11.0.4",
83
+ "@types/jsdom": "^21.1.7",
84
+ "@types/micromatch": "^4.0.9",
85
+ "@types/node": "^24.3.0",
86
+ "@vitest/browser": "^3.2.4",
87
+ "@vitest/coverage-v8": "^3.2.4",
88
+ "@vitest/ui": "^3.2.4",
89
+ "babel-plugin-transform-imports": "git+https://git@github.com/fabricjs/babel-plugin-transform-imports.git",
90
+ "chalk": "^5.6.0",
91
+ "commander": "^14.0.0",
92
+ "es-toolkit": "1.39.7",
93
+ "eslint-config-prettier": "^10.1.8",
94
+ "fs-extra": "^11.3.1",
95
+ "inquirer": "^12.9.4",
96
+ "micromatch": "^4.0.8",
97
+ "moment": "^2.30.1",
98
+ "nyc": "^17.1.0",
99
+ "prettier": "^3.6.2",
100
+ "ps-list": "^8.1.1",
101
+ "rollup": "^4.48.0",
102
+ "semver": "^7.7.2",
103
+ "serve": "^14.2.4",
104
+ "tslib": "^2.8.1",
105
+ "typescript": "^5.9.2",
106
+ "typescript-eslint": "^8.40.0",
107
+ "v8-to-istanbul": "^9.3.0",
108
+ "vite": "^6.3.5",
109
+ "vitest": "^3.2.4"
110
+ },
111
+ "engines": {
112
+ "node": ">=18.0.0"
113
+ },
114
+ "overrides": {
115
+ "canvas": {
116
+ "canvas": "3.2.0"
117
+ }
118
+ },
119
+ "module": "./dist/index.mjs",
120
+ "types": "./dist/index.d.ts",
121
+ "typesVersions": {
122
+ ">=4.2": {
123
+ "*": [
124
+ "dist/index.d.ts"
125
+ ],
126
+ "node": [
127
+ "dist/index.node.d.ts"
128
+ ]
129
+ }
130
+ },
131
+ "sideEffects": false,
132
+ "exports": {
133
+ ".": {
134
+ "types": "./dist/index.d.ts",
135
+ "import": "./dist/index.min.mjs",
136
+ "require": "./dist/index.min.js",
137
+ "default": "./dist/index.min.js"
138
+ },
139
+ "./es": {
140
+ "types": "./dist/index.d.ts",
141
+ "import": "./dist/fabric.min.mjs",
142
+ "require": null,
143
+ "default": null
144
+ },
145
+ "./node": {
146
+ "node": "./dist/index.node.cjs",
147
+ "types": "./dist/index.node.d.ts",
148
+ "import": "./dist/index.node.mjs",
149
+ "require": "./dist/index.node.cjs",
150
+ "default": "./dist/index.node.cjs"
151
+ },
152
+ "./extensions": {
153
+ "node": "./dist-extensions/index.mjs",
154
+ "types": "./dist-extensions/extensions/index.d.ts",
155
+ "import": "./dist-extensions/index.mjs",
156
+ "require": null,
157
+ "default": "./dist-extensions/fabric-extensions.min.js"
158
+ }
159
+ },
160
+ "optionalDependencies": {
161
+ "canvas": "^3.2.0",
162
+ "jsdom": "^26.1.0"
163
+ }
164
+ }
@@ -25,16 +25,23 @@ import {
25
25
  TOP,
26
26
  } from '../constants';
27
27
  import type { CSSRules } from '../parser/typedefs';
28
+ import {
29
+ applyCornerRadiusToPolygon,
30
+ renderRoundedPolygon,
31
+ generateRoundedPolygonPath,
32
+ } from '../util/misc/cornerRadius';
28
33
 
29
34
  export const polylineDefaultValues: Partial<TClassProperties<Polyline>> = {
30
35
  /**
31
36
  * @deprecated transient option soon to be removed in favor of a different design
32
37
  */
33
38
  exactBoundingBox: false,
39
+ cornerRadius: 0,
34
40
  };
35
41
 
36
42
  export interface SerializedPolylineProps extends SerializedObjectProps {
37
43
  points: XY[];
44
+ cornerRadius: number;
38
45
  }
39
46
 
40
47
  export class Polyline<
@@ -59,6 +66,13 @@ export class Polyline<
59
66
  */
60
67
  declare exactBoundingBox: boolean;
61
68
 
69
+ /**
70
+ * Corner radius for rounded corners
71
+ * @type Number
72
+ * @default 0
73
+ */
74
+ declare cornerRadius: number;
75
+
62
76
  declare private initialized: true | undefined;
63
77
 
64
78
  static ownDefaults = polylineDefaultValues;
@@ -91,7 +105,7 @@ export class Polyline<
91
105
 
92
106
  declare strokeOffset: Point;
93
107
 
94
- static cacheProperties = [...cacheProperties, 'points'];
108
+ static cacheProperties = [...cacheProperties, 'points', 'cornerRadius'];
95
109
 
96
110
  strokeDiff: Point;
97
111
 
@@ -312,7 +326,7 @@ export class Polyline<
312
326
  K extends keyof T = never,
313
327
  >(propertiesToInclude: K[] = []): Pick<T, K> & SProps {
314
328
  return {
315
- ...super.toObject(propertiesToInclude),
329
+ ...super.toObject(['cornerRadius', ...propertiesToInclude]),
316
330
  points: this.points.map(({ x, y }) => ({ x, y })),
317
331
  };
318
332
  }
@@ -323,28 +337,42 @@ export class Polyline<
323
337
  * of the instance
324
338
  */
325
339
  _toSVG() {
326
- const points = [],
327
- diffX = this.pathOffset.x,
328
- diffY = this.pathOffset.y,
329
- NUM_FRACTION_DIGITS = config.NUM_FRACTION_DIGITS;
330
-
331
- for (let i = 0, len = this.points.length; i < len; i++) {
332
- points.push(
333
- toFixed(this.points[i].x - diffX, NUM_FRACTION_DIGITS),
334
- ',',
335
- toFixed(this.points[i].y - diffY, NUM_FRACTION_DIGITS),
336
- ' ',
337
- );
340
+ if (this.cornerRadius > 0 && this.points.length >= 3) {
341
+ // Generate rounded polygon/polyline as path
342
+ const diffX = this.pathOffset.x;
343
+ const diffY = this.pathOffset.y;
344
+ const adjustedPoints = this.points.map(point => ({
345
+ x: point.x - diffX,
346
+ y: point.y - diffY,
347
+ }));
348
+ const roundedCorners = applyCornerRadiusToPolygon(adjustedPoints, this.cornerRadius);
349
+ const pathData = generateRoundedPolygonPath(roundedCorners, !this.isOpen());
350
+ return ['<path ', 'COMMON_PARTS', `d="${pathData}" />\n`];
351
+ } else {
352
+ // Original sharp corners implementation
353
+ const points = [];
354
+ const diffX = this.pathOffset.x;
355
+ const diffY = this.pathOffset.y;
356
+ const NUM_FRACTION_DIGITS = config.NUM_FRACTION_DIGITS;
357
+
358
+ for (let i = 0, len = this.points.length; i < len; i++) {
359
+ points.push(
360
+ toFixed(this.points[i].x - diffX, NUM_FRACTION_DIGITS),
361
+ ',',
362
+ toFixed(this.points[i].y - diffY, NUM_FRACTION_DIGITS),
363
+ ' ',
364
+ );
365
+ }
366
+ return [
367
+ `<${
368
+ (this.constructor as typeof Polyline).type.toLowerCase() as
369
+ | 'polyline'
370
+ | 'polygon'
371
+ } `,
372
+ 'COMMON_PARTS',
373
+ `points="${points.join('')}" />\n`,
374
+ ];
338
375
  }
339
- return [
340
- `<${
341
- (this.constructor as typeof Polyline).type.toLowerCase() as
342
- | 'polyline'
343
- | 'polygon'
344
- } `,
345
- 'COMMON_PARTS',
346
- `points="${points.join('')}" />\n`,
347
- ];
348
376
  }
349
377
 
350
378
  /**
@@ -361,13 +389,26 @@ export class Polyline<
361
389
  // NaN comes from parseFloat of a empty string in parser
362
390
  return;
363
391
  }
364
- ctx.beginPath();
365
- ctx.moveTo(this.points[0].x - x, this.points[0].y - y);
366
- for (let i = 0; i < len; i++) {
367
- const point = this.points[i];
368
- ctx.lineTo(point.x - x, point.y - y);
392
+
393
+ if (this.cornerRadius > 0 && len >= 3) {
394
+ // Render with rounded corners
395
+ const adjustedPoints = this.points.map(point => ({
396
+ x: point.x - x,
397
+ y: point.y - y,
398
+ }));
399
+ const roundedCorners = applyCornerRadiusToPolygon(adjustedPoints, this.cornerRadius);
400
+ renderRoundedPolygon(ctx, roundedCorners, !this.isOpen());
401
+ } else {
402
+ // Original sharp corners implementation
403
+ ctx.beginPath();
404
+ ctx.moveTo(this.points[0].x - x, this.points[0].y - y);
405
+ for (let i = 0; i < len; i++) {
406
+ const point = this.points[i];
407
+ ctx.lineTo(point.x - x, point.y - y);
408
+ }
409
+ !this.isOpen() && ctx.closePath();
369
410
  }
370
- !this.isOpen() && ctx.closePath();
411
+
371
412
  this._renderPaintInOrder(ctx);
372
413
  }
373
414
 
@@ -593,6 +593,8 @@ export class FabricText<
593
593
  line,
594
594
  charBound,
595
595
  spaces;
596
+ const isRtl = this.direction === 'rtl';
597
+
596
598
  for (let i = 0, len = this._textLines.length; i < len; i++) {
597
599
  if (
598
600
  this.textAlign !== JUSTIFY &&
@@ -609,15 +611,56 @@ export class FabricText<
609
611
  ) {
610
612
  numberOfSpaces = spaces.length;
611
613
  diffSpace = (this.width - currentLineWidth) / numberOfSpaces;
612
- for (let j = 0; j <= line.length; j++) {
613
- charBound = this.__charBounds[i][j];
614
- if (this._reSpaceAndTab.test(line[j])) {
615
- charBound.width += diffSpace;
616
- charBound.kernedWidth += diffSpace;
617
- charBound.left += accumulatedSpace;
618
- accumulatedSpace += diffSpace;
619
- } else {
620
- charBound.left += accumulatedSpace;
614
+
615
+ if (isRtl) {
616
+ // For RTL text, we need to redistribute spaces while maintaining correct visual order
617
+ // The key insight is that RTL text is stored in logical order but displayed in visual order
618
+ // We need to adjust bounds to account for the visual positioning
619
+
620
+ // First, collect all space positions
621
+ const spaceIndices = [];
622
+ for (let j = 0; j < line.length; j++) {
623
+ if (this._reSpaceAndTab.test(line[j])) {
624
+ spaceIndices.push(j);
625
+ }
626
+ }
627
+
628
+ // Calculate total expansion
629
+ const totalExpansion = diffSpace * numberOfSpaces;
630
+
631
+ // For RTL, we need to work backwards through the visual positions
632
+ // but still update logical positions correctly
633
+ let spaceCount = 0;
634
+ for (let j = 0; j <= line.length; j++) {
635
+ charBound = this.__charBounds[i][j];
636
+ if (charBound) {
637
+ if (this._reSpaceAndTab.test(line[j])) {
638
+ charBound.width += diffSpace;
639
+ charBound.kernedWidth += diffSpace;
640
+ spaceCount++;
641
+ }
642
+
643
+ // For RTL, shift all characters to the right by the total expansion
644
+ // minus the expansion that comes after this character
645
+ const remainingSpaces = numberOfSpaces - spaceCount;
646
+ const shiftAmount = remainingSpaces * diffSpace;
647
+ charBound.left += shiftAmount;
648
+ }
649
+ }
650
+ } else {
651
+ // LTR processing (original logic)
652
+ for (let j = 0; j <= line.length; j++) {
653
+ charBound = this.__charBounds[i][j];
654
+ if (charBound) {
655
+ if (this._reSpaceAndTab.test(line[j])) {
656
+ charBound.width += diffSpace;
657
+ charBound.kernedWidth += diffSpace;
658
+ charBound.left += accumulatedSpace;
659
+ accumulatedSpace += diffSpace;
660
+ } else {
661
+ charBound.left += accumulatedSpace;
662
+ }
663
+ }
621
664
  }
622
665
  }
623
666
  }
@@ -1376,7 +1419,15 @@ export class FabricText<
1376
1419
  if (currentDirection !== this.direction) {
1377
1420
  ctx.canvas.setAttribute('dir', isLtr ? 'ltr' : 'rtl');
1378
1421
  ctx.direction = isLtr ? 'ltr' : 'rtl';
1379
- ctx.textAlign = isLtr ? LEFT : RIGHT;
1422
+
1423
+ // For justify alignments, we need to set the correct canvas text alignment
1424
+ // This is crucial for RTL text to render in the correct order
1425
+ if (isJustify) {
1426
+ // Justify uses LEFT alignment as a base, letting the character positioning handle justification
1427
+ ctx.textAlign = LEFT;
1428
+ } else {
1429
+ ctx.textAlign = isLtr ? LEFT : RIGHT;
1430
+ }
1380
1431
  }
1381
1432
  top -= (lineHeight * this._fontSizeFraction) / this.lineHeight;
1382
1433
  if (shortCut) {
@@ -1665,14 +1716,26 @@ export class FabricText<
1665
1716
  direction = this.direction,
1666
1717
  isEndOfWrapping = this.isEndOfWrapping(lineIndex);
1667
1718
  let leftOffset = 0;
1668
- if (
1719
+
1720
+ // Handle justify alignments (excluding last lines and wrapped line ends)
1721
+ const isJustifyLine =
1669
1722
  textAlign === JUSTIFY ||
1670
1723
  (textAlign === JUSTIFY_CENTER && !isEndOfWrapping) ||
1671
1724
  (textAlign === JUSTIFY_RIGHT && !isEndOfWrapping) ||
1672
- (textAlign === JUSTIFY_LEFT && !isEndOfWrapping)
1673
- ) {
1674
- return 0;
1725
+ (textAlign === JUSTIFY_LEFT && !isEndOfWrapping);
1726
+
1727
+ if (isJustifyLine) {
1728
+ // Justify lines should start at the left edge for LTR and right edge for RTL
1729
+ // The space distribution is handled by enlargeSpaces()
1730
+ if (direction === 'rtl') {
1731
+ // For RTL justify, we need to account for the line being right-aligned
1732
+ return 0;
1733
+ } else {
1734
+ return 0;
1735
+ }
1675
1736
  }
1737
+
1738
+ // Handle non-justify alignments
1676
1739
  if (textAlign === CENTER) {
1677
1740
  leftOffset = lineDiff / 2;
1678
1741
  }
@@ -1685,6 +1748,8 @@ export class FabricText<
1685
1748
  if (textAlign === JUSTIFY_RIGHT) {
1686
1749
  leftOffset = lineDiff;
1687
1750
  }
1751
+
1752
+ // Apply RTL adjustments for non-justify alignments
1688
1753
  if (direction === 'rtl') {
1689
1754
  if (
1690
1755
  textAlign === RIGHT ||
@@ -2,7 +2,7 @@ import type { TClassProperties, TOptions } from '../typedefs';
2
2
  import { IText } from './IText/IText';
3
3
  import { classRegistry } from '../ClassRegistry';
4
4
  import { createTextboxDefaultControls } from '../controls/commonControls';
5
- import { JUSTIFY } from './Text/constants';
5
+ import { JUSTIFY, JUSTIFY_CENTER } from './Text/constants';
6
6
  import type { TextStyleDeclaration } from './Text/StyledText';
7
7
  import type { SerializedITextProps, ITextProps } from './IText/IText';
8
8
  import type { ITextEvents } from './IText/ITextBehavior';