diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a3656a5464..48beeedf322 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [next] +- fix(): gradient transform + rm workarounds [#9359](https://github.com/fabricjs/fabric.js/pull/9359) + **BREAKING**: + - `toLive(ctx)` => `toLive(ctx, target)` + - rm(): `_applyPatternGradientTransformText`, `_applyPatternGradientTransform` - fix(IText): cursor width under group [#9341](https://github.com/fabricjs/fabric.js/pull/9341) - TS(Canvas): constructor optional el [#9348](https://github.com/fabricjs/fabric.js/pull/9348) diff --git a/e2e/setup/index.ts b/e2e/setup/index.ts index ad4334b852c..dd85398bc73 100644 --- a/e2e/setup/index.ts +++ b/e2e/setup/index.ts @@ -2,11 +2,14 @@ import setupApp from './setupApp'; import setupCoverage from './setupCoverage'; import setupSelectors from './setupSelectors'; -export default () => { +/** + * @param {Function} [testConfig] pass data/config from the test to the browser + */ +export default (testConfig?: () => any) => { // call first setupSelectors(); // call before using fabric setupCoverage(); // call at the end - navigates the page - setupApp(); + setupApp(testConfig); }; diff --git a/e2e/setup/setupApp.ts b/e2e/setup/setupApp.ts index 95c362619b6..d5225e9f39e 100644 --- a/e2e/setup/setupApp.ts +++ b/e2e/setup/setupApp.ts @@ -4,8 +4,12 @@ import path from 'path'; import imports from '../imports'; import { JSDOM } from 'jsdom'; -export default () => { +/** + * @param {Function} [testConfig] pass data/config from the test to the browser + */ +export default (testConfig: () => any = () => {}) => { test.beforeEach(async ({ page }, { file }) => { + await page.exposeFunction('testConfig', testConfig); await page.goto('/e2e/site'); // expose imports for consumption page.addScriptTag({ diff --git a/e2e/tests/gradient-transform/common.ts b/e2e/tests/gradient-transform/common.ts new file mode 100644 index 00000000000..eb3f13955dd --- /dev/null +++ b/e2e/tests/gradient-transform/common.ts @@ -0,0 +1,61 @@ +/** + * Runs from both the browser and node + */ + +export function render( + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + fabric: typeof import('fabric'), + { type, el }: { type: 'text' | 'rect'; el?: HTMLCanvasElement } +) { + const targets = new Array(8).fill(0).map((_, index) => { + const angle = index * 45; + const gradient = new fabric.Gradient({ + coords: { + x1: 0, + y1: 0, + x2: 1, + y2: 1, + }, + gradientUnits: 'percentage', + // offsetX: 150, + gradientTransform: fabric.util.createRotateMatrix({ angle }), + colorStops: [ + { + offset: 0, + color: 'red', + }, + { + offset: 1, + color: 'blue', + }, + ], + }); + + return type === 'text' + ? new fabric.Text(`Gradient\n${angle}°`, { + fontSize: 50, + fontWeight: 'bold', + fill: gradient, + left: (index % 4) * 250, + top: Math.floor(index / 4) * 250, + }) + : new fabric.Rect({ + width: 150, + height: 150, + fill: gradient, + left: (index % 4) * 250, + top: Math.floor(index / 4) * 250, + }); + }); + + const canvas = new fabric.StaticCanvas(el, { + width: 1000, + height: 400, + backgroundColor: 'white', + enableRetinaScaling: false, + }); + canvas.add(...targets); + canvas.renderAll(); + + return { canvas }; +} diff --git a/e2e/tests/gradient-transform/index.spec.ts b/e2e/tests/gradient-transform/index.spec.ts new file mode 100644 index 00000000000..a00e35b877f --- /dev/null +++ b/e2e/tests/gradient-transform/index.spec.ts @@ -0,0 +1,36 @@ +import { expect, test } from '@playwright/test'; +import type { StaticCanvas } from 'fabric/node'; +import setup from '../../setup'; +import { CanvasUtil } from '../../utils/CanvasUtil'; +import { render } from './common'; + +for (const type of ['rect', 'text'] as const) { + test.describe(`Gradient transform on ${type}`, () => { + setup(() => ({ type })); + test('Gradient transform', async ({ page }, config) => { + await test.step('browser', async () => { + expect( + await new CanvasUtil(page).screenshot({ element: 'main' }), + 'browser snapshot' + ).toMatchSnapshot({ + name: `${type}.png`, + maxDiffPixelRatio: 0.05, + }); + }); + + await test.step('node', async () => { + // we want the browser snapshot of a test to be committed and not the node snapshot + config.config.updateSnapshots = 'none'; + expect( + ( + (await render(await import('../../..'), { type })) + .canvas as StaticCanvas + ) + .getNodeCanvas() + .toBuffer(), + 'node snapshot should match browser snapshot' + ).toMatchSnapshot({ name: `${type}.png`, maxDiffPixelRatio: 0.05 }); + }); + }); + }); +} diff --git a/e2e/tests/gradient-transform/index.spec.ts-snapshots/rect.png b/e2e/tests/gradient-transform/index.spec.ts-snapshots/rect.png new file mode 100644 index 00000000000..568e95f6fd0 Binary files /dev/null and b/e2e/tests/gradient-transform/index.spec.ts-snapshots/rect.png differ diff --git a/e2e/tests/gradient-transform/index.spec.ts-snapshots/text.png b/e2e/tests/gradient-transform/index.spec.ts-snapshots/text.png new file mode 100644 index 00000000000..f25b4d07895 Binary files /dev/null and b/e2e/tests/gradient-transform/index.spec.ts-snapshots/text.png differ diff --git a/e2e/tests/gradient-transform/index.ts b/e2e/tests/gradient-transform/index.ts new file mode 100644 index 00000000000..f5e657eb8ee --- /dev/null +++ b/e2e/tests/gradient-transform/index.ts @@ -0,0 +1,11 @@ +/** + * Runs in the **BROWSER** + */ + +import * as fabric from 'fabric'; +import { before } from '../test'; +import { render } from './common'; + +before('#canvas', async (el) => + render(fabric, { ...(await testConfig()), el }) +); diff --git a/e2e/tests/template-with-node/common.ts b/e2e/tests/template-with-node/common.ts index e00e20cf30a..bb0a57c0227 100644 --- a/e2e/tests/template-with-node/common.ts +++ b/e2e/tests/template-with-node/common.ts @@ -2,11 +2,17 @@ * Runs from both the browser and node */ -import type { StaticCanvas } from 'fabric'; - -// eslint-disable-next-line @typescript-eslint/consistent-type-imports -export function render(canvas: StaticCanvas, fabric: typeof import('fabric')) { - canvas.setDimensions({ width: 200, height: 70 }); +export function render( + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + fabric: typeof import('fabric'), + { el }: { el?: HTMLCanvasElement } = {} +) { + const canvas = new fabric.StaticCanvas(el, { + width: 200, + height: 70, + backgroundColor: 'white', + enableRetinaScaling: false, + }); const textbox = new fabric.Textbox('fabric.js test', { width: 200, top: 20, @@ -14,4 +20,6 @@ export function render(canvas: StaticCanvas, fabric: typeof import('fabric')) { canvas.add(textbox); canvas.centerObjectH(textbox); canvas.renderAll(); + + return { canvas, objects: { textbox } }; } diff --git a/e2e/tests/template-with-node/index.spec.ts b/e2e/tests/template-with-node/index.spec.ts index 9a4426ca1fc..b45315c0e46 100644 --- a/e2e/tests/template-with-node/index.spec.ts +++ b/e2e/tests/template-with-node/index.spec.ts @@ -1,7 +1,7 @@ import { expect, test } from '@playwright/test'; +import type { StaticCanvas } from 'fabric/node'; import setup from '../../setup'; import { CanvasUtil } from '../../utils/CanvasUtil'; -import { createNodeSnapshot } from '../../utils/createNodeSnapshot'; import { render } from './common'; setup(); @@ -9,7 +9,7 @@ setup(); test('TEST NAME', async ({ page }, config) => { await test.step('browser', async () => { expect( - await new CanvasUtil(page).screenshot(), + await new CanvasUtil(page).screenshot({ element: 'main' }), 'browser snapshot' ).toMatchSnapshot({ name: 'textbox.png', @@ -21,7 +21,9 @@ test('TEST NAME', async ({ page }, config) => { // we want the browser snapshot of a test to be committed and not the node snapshot config.config.updateSnapshots = 'none'; expect( - await createNodeSnapshot(render), + ((await render(await import('../../..'))).canvas as StaticCanvas) + .getNodeCanvas() + .toBuffer(), 'node snapshot should match browser snapshot' ).toMatchSnapshot({ name: 'textbox.png', maxDiffPixelRatio: 0.05 }); }); diff --git a/e2e/tests/template-with-node/index.ts b/e2e/tests/template-with-node/index.ts index 8b22350eec2..51b4dd0e7c1 100644 --- a/e2e/tests/template-with-node/index.ts +++ b/e2e/tests/template-with-node/index.ts @@ -3,7 +3,7 @@ */ import * as fabric from 'fabric'; -import { beforeAll } from '../test'; +import { before } from '../test'; import { render } from './common'; -beforeAll((canvas) => render(canvas, fabric), { enableRetinaScaling: false }); +before('#canvas', (el) => render(fabric, { el })); diff --git a/e2e/tests/test.ts b/e2e/tests/test.ts index 91af2e38331..3a9ef78b6f2 100644 --- a/e2e/tests/test.ts +++ b/e2e/tests/test.ts @@ -3,10 +3,13 @@ */ import type { Object as FabricObject } from 'fabric'; -import { Canvas } from 'fabric'; +import { Canvas, type StaticCanvas } from 'fabric'; import * as fabric from 'fabric'; -const canvasMap = (window.canvasMap = new Map()); +const canvasMap = (window.canvasMap = new Map< + HTMLCanvasElement, + Canvas | StaticCanvas +>()); const objectMap = (window.objectMap = new Map()); type AsyncReturnValue = T | Promise; @@ -33,7 +36,7 @@ export function before( * @returns a map of objects for playwright to access during tests */ cb: (canvas: HTMLCanvasElement) => AsyncReturnValue<{ - canvas: Canvas; + canvas: Canvas | StaticCanvas; objects?: Record; }> ) { @@ -72,7 +75,7 @@ export function beforeAll( export function after( selector: string, - cb: (canvas: Canvas) => AsyncReturnValue + cb: (canvas: Canvas | StaticCanvas) => AsyncReturnValue ) { teardownTasks.push(() => { const el = document.querySelector(selector); diff --git a/e2e/utils/CanvasUtil.ts b/e2e/utils/CanvasUtil.ts index d5a67672d05..c6755a4267f 100644 --- a/e2e/utils/CanvasUtil.ts +++ b/e2e/utils/CanvasUtil.ts @@ -25,9 +25,17 @@ export class CanvasUtil { return this.page.keyboard.press(`${modifier}+KeyV`); } - screenshot(options: LocatorScreenshotOptions = {}) { + screenshot({ + element = 'wrapper', + ...options + }: LocatorScreenshotOptions & { element?: 'wrapper' | 'top' | 'main' } = {}) { + const selector = { + wrapper: `canvas_wrapper=${this.selector}`, + top: `canvas_top=${this.selector}`, + main: this.selector, + }[element]; return this.page - .locator(`canvas_wrapper=${this.selector}`) + .locator(selector) .screenshot({ omitBackground: true, ...options }); } diff --git a/index.node.ts b/index.node.ts index 0be09bd7bc2..0ccde639494 100644 --- a/index.node.ts +++ b/index.node.ts @@ -1,6 +1,7 @@ // first we set the env variable by importing the node env file import { getNodeCanvas } from './src/env/node'; +import { DOMMatrix } from 'canvas'; import type { JpegConfig, PngConfig } from 'canvas'; import { Canvas as CanvasBase, @@ -10,6 +11,9 @@ import { FabricObject } from './src/shapes/Object/Object'; FabricObject.ownDefaults.objectCaching = false; +// @ts-expect-error global polyfill +global.DOMMatrix = DOMMatrix; + export * from './fabric'; export class StaticCanvas extends StaticCanvasBase { diff --git a/src/Pattern/Pattern.ts b/src/Pattern/Pattern.ts index bbfeefb2e00..1c94f11b59e 100644 --- a/src/Pattern/Pattern.ts +++ b/src/Pattern/Pattern.ts @@ -11,6 +11,9 @@ import type { PatternOptions, SerializedPatternOptions, } from './types'; +import { createTranslateMatrix, multiplyTransformMatrixArray } from '../util'; +import type { StaticCanvas } from '../canvas/StaticCanvas'; +import { FabricObject } from '../shapes/Object/Object'; /** * @see {@link http://fabricjs.com/patterns demo} @@ -125,7 +128,10 @@ export class Pattern { * @param {CanvasRenderingContext2D} ctx Context to create pattern * @return {CanvasPattern} */ - toLive(ctx: CanvasRenderingContext2D): CanvasPattern | null { + toLive( + ctx: CanvasRenderingContext2D, + target: StaticCanvas | FabricObject + ): CanvasPattern | '' { if ( // if the image failed to load, return, and allow rest to continue loading !this.source || @@ -135,10 +141,27 @@ export class Pattern { this.source.naturalWidth === 0 || this.source.naturalHeight === 0)) ) { - return null; + return ''; } - return ctx.createPattern(this.source, this.repeat)!; + const pattern = ctx.createPattern(this.source, this.repeat)!; + const { patternTransform, offsetX = 0, offsetY = 0 } = this; + const { x, y } = + // correct rendering position from object rendering origin (center) to tl + target instanceof FabricObject + ? { x: -target.width / 2, y: -target.height / 2 } + : { x: 0, y: 0 }; + if (patternTransform || offsetX || offsetY || x || y) { + pattern.setTransform( + new DOMMatrix( + multiplyTransformMatrixArray([ + patternTransform, + createTranslateMatrix(offsetX + x, offsetY + y), + ]) + ) + ); + } + return pattern; } /** diff --git a/src/canvas/StaticCanvas.ts b/src/canvas/StaticCanvas.ts index a8c399e70a9..f20c717cfc1 100644 --- a/src/canvas/StaticCanvas.ts +++ b/src/canvas/StaticCanvas.ts @@ -1,7 +1,6 @@ import { config } from '../config'; import { CENTER, VERSION } from '../constants'; import type { CanvasEvents, StaticCanvasEvents } from '../EventTypeDefs'; -import type { Gradient } from '../gradient/Gradient'; import { createCollectionMixin } from '../Collection'; import { CommonMethods } from '../CommonMethods'; import type { Pattern } from '../Pattern'; @@ -641,7 +640,6 @@ export class StaticCanvas< if (!fill && !object) { return; } - const isAFiller = isFiller(fill); if (fill) { ctx.save(); ctx.beginPath(); @@ -650,16 +648,10 @@ export class StaticCanvas< ctx.lineTo(this.width, this.height); ctx.lineTo(0, this.height); ctx.closePath(); - ctx.fillStyle = isAFiller ? fill.toLive(ctx /* this */)! : fill; + ctx.fillStyle = isFiller(fill) ? fill.toLive(ctx, this) : fill; if (needsVpt) { ctx.transform(...v); } - if (isAFiller) { - ctx.transform(1, 0, 0, 1, fill.offsetX || 0, fill.offsetY || 0); - const m = ((fill as Gradient<'linear'>).gradientTransform || - (fill as Pattern).patternTransform) as TMat2D; - m && ctx.transform(...m); - } ctx.fill(); ctx.restore(); } diff --git a/src/gradient/Gradient.ts b/src/gradient/Gradient.ts index 37d7349507f..9c6daa41500 100644 --- a/src/gradient/Gradient.ts +++ b/src/gradient/Gradient.ts @@ -1,7 +1,7 @@ import { Color } from '../color/Color'; import { iMatrix } from '../constants'; import { parseTransformAttribute } from '../parser/parseTransformAttribute'; -import type { FabricObject } from '../shapes/Object/FabricObject'; +import { FabricObject } from '../shapes/Object/Object'; import type { TMat2D } from '../typedefs'; import { uid } from '../util/internals/uid'; import { pick } from '../util/misc/pick'; @@ -20,6 +20,14 @@ import type { } from './typedefs'; import { classRegistry } from '../ClassRegistry'; import { isPath } from '../util/typeAssertions'; +import type { StaticCanvas } from '../canvas/StaticCanvas'; +import { + createScaleMatrix, + createTranslateMatrix, + magnitude, + multiplyTransformMatrixArray, +} from '../util'; +import { Point } from '../Point'; /** * Gradient class @@ -295,19 +303,41 @@ export class Gradient< * @param {CanvasRenderingContext2D} ctx Context to render on * @return {CanvasGradient} */ - toLive(ctx: CanvasRenderingContext2D): CanvasGradient { - const coords = this.coords as GradientCoords<'radial'>; - const gradient = - this.type === 'linear' - ? ctx.createLinearGradient(coords.x1, coords.y1, coords.x2, coords.y2) - : ctx.createRadialGradient( - coords.x1, - coords.y1, - coords.r1, - coords.x2, - coords.y2, - coords.r2 - ); + toLive( + ctx: CanvasRenderingContext2D, + target: StaticCanvas | FabricObject + ): CanvasGradient { + const { offsetX = 0, offsetY = 0 } = this; + const { x, y } = + // correct rendering position from object rendering origin (center) to tl + target instanceof FabricObject + ? { x: -target.width / 2, y: -target.height / 2 } + : { x: 0, y: 0 }; + const transform = multiplyTransformMatrixArray([ + this.gradientTransform, + createTranslateMatrix(offsetX + x, offsetY + y), + this.gradientUnits === 'percentage' && + createScaleMatrix(target.width, target.height), + ]); + const { x1, y1, x2, y2 } = this.coords; + const p1 = new Point(x1, y1).transform(transform); + const p2 = new Point(x2, y2).transform(transform); + + let gradient: CanvasGradient; + if (this.type === 'linear') { + gradient = ctx.createLinearGradient(p1.x, p1.y, p2.x, p2.y); + } else { + const { r1, r2 } = this.coords as GradientCoords<'radial'>; + const hypot = magnitude(new Point(1, 0).transform(transform, true)); + gradient = ctx.createRadialGradient( + p1.x, + p1.y, + r1 * hypot, + p2.x, + p2.y, + r2 * hypot + ); + } this.colorStops.forEach(({ color, opacity, offset }) => { gradient.addColorStop( diff --git a/src/shapes/Line.ts b/src/shapes/Line.ts index 953a3af8340..d1135b040dd 100644 --- a/src/shapes/Line.ts +++ b/src/shapes/Line.ts @@ -131,11 +131,9 @@ export class Line< // make sure setting "fill" changes color of a line // (by copying fillStyle to strokeStyle, since line is stroked, not filled) const origStrokeStyle = ctx.strokeStyle; - if (isFiller(this.stroke)) { - ctx.strokeStyle = this.stroke.toLive(ctx)!; - } else { - ctx.strokeStyle = this.stroke ?? ctx.fillStyle; - } + ctx.strokeStyle = isFiller(this.stroke) + ? this.stroke.toLive(ctx, this) + : this.stroke ?? ctx.fillStyle; this.stroke && this._renderStroke(ctx); ctx.strokeStyle = origStrokeStyle; } diff --git a/src/shapes/Object/Object.ts b/src/shapes/Object/Object.ts index 8571463e00e..6c060f87e71 100644 --- a/src/shapes/Object/Object.ts +++ b/src/shapes/Object/Object.ts @@ -47,8 +47,6 @@ import { fabricObjectDefaultValues, stateProperties, } from './defaultValues'; -import type { Gradient } from '../../gradient/Gradient'; -import type { Pattern } from '../../Pattern'; import type { Canvas } from '../../canvas/Canvas'; import type { SerializedObjectProps } from './types/SerializedObjectProps'; import type { ObjectProps } from './types/ObjectProps'; @@ -1030,7 +1028,7 @@ export class FabricObject< _setStrokeStyles( ctx: CanvasRenderingContext2D, decl: Pick< - this, + ObjectProps, | 'stroke' | 'strokeWidth' | 'strokeLineCap' @@ -1046,37 +1044,16 @@ export class FabricObject< ctx.lineDashOffset = decl.strokeDashOffset; ctx.lineJoin = decl.strokeLineJoin; ctx.miterLimit = decl.strokeMiterLimit; - if (isFiller(stroke)) { - if ( - (stroke as Gradient<'linear'>).gradientUnits === 'percentage' || - (stroke as Gradient<'linear'>).gradientTransform || - (stroke as Pattern).patternTransform - ) { - // need to transform gradient in a pattern. - // this is a slow process. If you are hitting this codepath, and the object - // is not using caching, you should consider switching it on. - // we need a canvas as big as the current object caching canvas. - this._applyPatternForTransformedGradient(ctx, stroke); - } else { - // is a simple gradient or pattern - ctx.strokeStyle = stroke.toLive(ctx)!; - this._applyPatternGradientTransform(ctx, stroke); - } - } else { - // is a color - ctx.strokeStyle = decl.stroke as string; - } + ctx.strokeStyle = isFiller(stroke) ? stroke.toLive(ctx, this) : stroke; } } - _setFillStyles(ctx: CanvasRenderingContext2D, { fill }: Pick) { + _setFillStyles( + ctx: CanvasRenderingContext2D, + { fill }: Pick + ) { if (fill) { - if (isFiller(fill)) { - ctx.fillStyle = fill.toLive(ctx)!; - this._applyPatternGradientTransform(ctx, fill); - } else { - ctx.fillStyle = fill; - } + ctx.fillStyle = isFiller(fill) ? fill.toLive(ctx, this) : fill; } } @@ -1143,35 +1120,6 @@ export class FabricObject< ctx.shadowBlur = ctx.shadowOffsetX = ctx.shadowOffsetY = 0; } - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {TFiller} filler {@link Pattern} or {@link Gradient} - */ - _applyPatternGradientTransform( - ctx: CanvasRenderingContext2D, - filler: TFiller - ) { - if (!isFiller(filler)) { - return { offsetX: 0, offsetY: 0 }; - } - const t = - (filler as Gradient<'linear'>).gradientTransform || - (filler as Pattern).patternTransform; - const offsetX = -this.width / 2 + filler.offsetX || 0, - offsetY = -this.height / 2 + filler.offsetY || 0; - - if ((filler as Gradient<'linear'>).gradientUnits === 'percentage') { - ctx.transform(this.width, 0, 0, this.height, offsetX, offsetY); - } else { - ctx.transform(1, 0, 0, 1, offsetX, offsetY); - } - if (t) { - ctx.transform(t[0], t[1], t[2], t[3], t[4], t[5]); - } - return { offsetX: offsetX, offsetY: offsetY }; - } - /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on @@ -1240,59 +1188,6 @@ export class FabricObject< ctx.restore(); } - /** - * This function try to patch the missing gradientTransform on canvas gradients. - * transforming a context to transform the gradient, is going to transform the stroke too. - * we want to transform the gradient but not the stroke operation, so we create - * a transformed gradient on a pattern and then we use the pattern instead of the gradient. - * this method has drawbacks: is slow, is in low resolution, needs a patch for when the size - * is limited. - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Gradient} filler - */ - _applyPatternForTransformedGradient( - ctx: CanvasRenderingContext2D, - filler: TFiller - ) { - const dims = this._limitCacheSize(this._getCacheCanvasDimensions()), - pCanvas = createCanvasElement(), - retinaScaling = this.getCanvasRetinaScaling(), - width = dims.x / this.scaleX / retinaScaling, - height = dims.y / this.scaleY / retinaScaling; - // in case width and height are less than 1px, we have to round up. - // since the pattern is no-repeat, this is fine - pCanvas.width = Math.ceil(width); - pCanvas.height = Math.ceil(height); - const pCtx = pCanvas.getContext('2d'); - if (!pCtx) { - return; - } - pCtx.beginPath(); - pCtx.moveTo(0, 0); - pCtx.lineTo(width, 0); - pCtx.lineTo(width, height); - pCtx.lineTo(0, height); - pCtx.closePath(); - pCtx.translate(width / 2, height / 2); - pCtx.scale( - dims.zoomX / this.scaleX / retinaScaling, - dims.zoomY / this.scaleY / retinaScaling - ); - this._applyPatternGradientTransform(pCtx, filler); - pCtx.fillStyle = filler.toLive(ctx)!; - pCtx.fill(); - ctx.translate( - -this.width / 2 - this.strokeWidth / 2, - -this.height / 2 - this.strokeWidth / 2 - ); - ctx.scale( - (retinaScaling * this.scaleX) / dims.zoomX, - (retinaScaling * this.scaleY) / dims.zoomY - ); - ctx.strokeStyle = pCtx.createPattern(pCanvas, 'no-repeat') ?? ''; - } - /** * This function is an helper for svg import. it returns the center of the object in the svg * untransformed coordinates diff --git a/src/shapes/Text/Text.ts b/src/shapes/Text/Text.ts index 0bd28cbefb7..fcee5c9f40a 100644 --- a/src/shapes/Text/Text.ts +++ b/src/shapes/Text/Text.ts @@ -13,7 +13,6 @@ import type { Abortable, TCacheCanvasDimensions, TClassProperties, - TFiller, TOptions, } from '../../typedefs'; import { classRegistry } from '../../ClassRegistry'; @@ -42,9 +41,6 @@ import { JUSTIFY_RIGHT, } from './constants'; import { CENTER, LEFT, RIGHT, TOP, BOTTOM } from '../../constants'; -import { isFiller } from '../../util/typeAssertions'; -import type { Gradient } from '../../gradient/Gradient'; -import type { Pattern } from '../../Pattern'; import type { CSSRules } from '../../parser/typedefs'; let measuringContext: CanvasRenderingContext2D | null; @@ -1231,104 +1227,6 @@ export class Text< ctx.restore(); } - /** - * This function try to patch the missing gradientTransform on canvas gradients. - * transforming a context to transform the gradient, is going to transform the stroke too. - * we want to transform the gradient but not the stroke operation, so we create - * a transformed gradient on a pattern and then we use the pattern instead of the gradient. - * this method has drawbacks: is slow, is in low resolution, needs a patch for when the size - * is limited. - * @private - * @param {TFiller} filler a fabric gradient instance - * @return {CanvasPattern} a pattern to use as fill/stroke style - */ - _applyPatternGradientTransformText(filler: TFiller) { - const pCanvas = createCanvasElement(), - // TODO: verify compatibility with strokeUniform - width = this.width + this.strokeWidth, - height = this.height + this.strokeWidth, - pCtx = pCanvas.getContext('2d')!; - pCanvas.width = width; - pCanvas.height = height; - pCtx.beginPath(); - pCtx.moveTo(0, 0); - pCtx.lineTo(width, 0); - pCtx.lineTo(width, height); - pCtx.lineTo(0, height); - pCtx.closePath(); - pCtx.translate(width / 2, height / 2); - pCtx.fillStyle = filler.toLive(pCtx)!; - this._applyPatternGradientTransform(pCtx, filler); - pCtx.fill(); - return pCtx.createPattern(pCanvas, 'no-repeat')!; - } - - handleFiller( - ctx: CanvasRenderingContext2D, - property: `${T}Style`, - filler: TFiller | string - ): { offsetX: number; offsetY: number } { - let offsetX: number, offsetY: number; - if (isFiller(filler)) { - if ( - (filler as Gradient<'linear'>).gradientUnits === 'percentage' || - (filler as Gradient<'linear'>).gradientTransform || - (filler as Pattern).patternTransform - ) { - // need to transform gradient in a pattern. - // this is a slow process. If you are hitting this codepath, and the object - // is not using caching, you should consider switching it on. - // we need a canvas as big as the current object caching canvas. - offsetX = -this.width / 2; - offsetY = -this.height / 2; - ctx.translate(offsetX, offsetY); - ctx[property] = this._applyPatternGradientTransformText(filler); - return { offsetX, offsetY }; - } else { - // is a simple gradient or pattern - ctx[property] = filler.toLive(ctx)!; - return this._applyPatternGradientTransform(ctx, filler); - } - } else { - // is a color - ctx[property] = filler; - } - return { offsetX: 0, offsetY: 0 }; - } - - /** - * This function prepare the canvas for a stroke style, and stroke and strokeWidth - * need to be sent in as defined - * @param {CanvasRenderingContext2D} ctx - * @param {CompleteTextStyleDeclaration} style with stroke and strokeWidth defined - * @returns - */ - _setStrokeStyles( - ctx: CanvasRenderingContext2D, - { - stroke, - strokeWidth, - }: Pick - ) { - ctx.lineWidth = strokeWidth; - ctx.lineCap = this.strokeLineCap; - ctx.lineDashOffset = this.strokeDashOffset; - ctx.lineJoin = this.strokeLineJoin; - ctx.miterLimit = this.strokeMiterLimit; - return this.handleFiller(ctx, 'strokeStyle', stroke!); - } - - /** - * This function prepare the canvas for a ill style, and fill - * need to be sent in as defined - * @param {CanvasRenderingContext2D} ctx - * @param {CompleteTextStyleDeclaration} style with ill defined - * @returns - */ - _setFillStyles(ctx: CanvasRenderingContext2D, { fill }: Pick) { - return this.handleFiller(ctx, 'fillStyle', fill!); - } - /** * @private * @param {String} method @@ -1370,21 +1268,19 @@ export class Text< } if (shouldFill) { - const fillOffsets = this._setFillStyles(ctx, fullDecl); - ctx.fillText( - _char, - left - fillOffsets.offsetX, - top - fillOffsets.offsetY - ); + this._setFillStyles(ctx, fullDecl); + ctx.fillText(_char, left, top); } if (shouldStroke) { - const strokeOffsets = this._setStrokeStyles(ctx, fullDecl); - ctx.strokeText( - _char, - left - strokeOffsets.offsetX, - top - strokeOffsets.offsetY - ); + this._setStrokeStyles(ctx, { + strokeLineCap: this.strokeLineCap, + strokeDashOffset: this.strokeDashOffset, + strokeLineJoin: this.strokeLineJoin, + strokeMiterLimit: this.strokeMiterLimit, + ...fullDecl, + }); + ctx.strokeText(_char, left, top); } ctx.restore(); diff --git a/test/visual/golden/text6.png b/test/visual/golden/text6.png index 6f6691e61be..ec47fb81dac 100644 Binary files a/test/visual/golden/text6.png and b/test/visual/golden/text6.png differ