Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(FabricObject): Render clipPath as sharp as the object #9774

Merged
merged 44 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
cde81b4
updated PR
asturur Mar 28, 2024
b175b32
update CHANGELOG.md
github-actions[bot] Mar 28, 2024
3977939
update seaClipPath
asturur Mar 28, 2024
1a6ea9e
of corse break because i removed a part of the launcher
asturur Mar 28, 2024
995e127
Merge branch 'sharper-clippath' of github.com:fabricjs/fabric.js into…
asturur Mar 28, 2024
1071f8f
renamed method, is not a cache
asturur Mar 29, 2024
6e8b60a
diffs
asturur Mar 29, 2024
833a940
diffs
asturur Mar 29, 2024
7d94674
diffs
asturur Mar 29, 2024
6beedb0
support nested clippaths
asturur Mar 29, 2024
819ec30
no full string
asturur Mar 29, 2024
7fcedf0
renamed golden for different tests
asturur Mar 29, 2024
c4e850a
separated tests
asturur Mar 29, 2024
212d22c
updated to master
asturur Apr 2, 2024
c7e13e6
meh
asturur Apr 2, 2024
d9d6adc
ok
asturur Apr 2, 2024
5b006b0
ok
asturur Apr 2, 2024
e7f1c61
run tests
asturur Apr 2, 2024
2152a58
added svg example
asturur Apr 4, 2024
f8d1648
notes
asturur Apr 4, 2024
fabbd38
save this mess of a code for reference
asturur Apr 4, 2024
a940c6c
Merge branch 'master' into sharper-clippath
asturur Aug 11, 2024
926b84a
Merge branch 'master' into sharper-clippath
asturur Sep 30, 2024
6607d3f
consolidate canvas creations
asturur Sep 30, 2024
23ae925
testing
asturur Sep 30, 2024
33ed955
redone tests
asturur Sep 30, 2024
9a1e1aa
sea-svg
asturur Sep 30, 2024
b4a0a9e
modified
asturur Sep 30, 2024
5fc172b
passing tests
asturur Sep 30, 2024
af6210e
improve code
asturur Oct 3, 2024
4ceef1b
ok
asturur Oct 3, 2024
f6713f3
new test
asturur Oct 3, 2024
253a6d0
try first
asturur Oct 3, 2024
284c29b
add xml preamble
asturur Oct 3, 2024
f1a1408
added golden
asturur Oct 3, 2024
0bae683
no dist changes
asturur Oct 3, 2024
7d58474
fixes
asturur Oct 3, 2024
277f0d9
restore this
asturur Oct 3, 2024
988e1e4
restore this
asturur Oct 3, 2024
81a03ae
test this
asturur Oct 3, 2024
f33abe8
Merge branch 'master' into sharper-clippath
asturur Oct 3, 2024
a2d8c85
Merge branch 'master' into sharper-clippath
asturur Oct 3, 2024
49f2a17
merged master
asturur Oct 3, 2024
802db59
Update CHANGELOG.md
asturur Oct 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .codesandbox/templates/vanilla/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as fabric from 'fabric';
import './styles.css';
import { testCase } from './testcases/responsive';
import { testCase } from './testcases/loadingSvgs';

const el = document.getElementById('canvas');
const canvas = (window.canvas = new fabric.Canvas(el));
Expand Down
24 changes: 24 additions & 0 deletions .codesandbox/templates/vanilla/src/testcases/loadingSvgs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as fabric from 'fabric';

const svgString = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" viewBox="5 3 10 10" version="1.1" width="400" height="400">
<g>
<g>
<clipPath id="a">
<circle transform="scale(1.2, 1.2) translate(-1, -1)" r="4" cx="8" cy="8" />
</clipPath>
<clipPath id="t" clip-path="url(#a)">
<circle r="6" transform="scale(1.3, 0.8) translate(1, 1)" cx="7" cy="7" />
</clipPath>
<clipPath id="c" clip-path="url(#t)" >
<circle transform="translate(12, 10) scale(14, 14)" r="0.5" cx="0.01" cy="0.01" />
</clipPath>
<path clip-path="url(#c)" d="M15.422,18.129l-5.264-2.768l-5.265,2.768l1.006-5.863L1.64,8.114l5.887-0.855
l2.632-5.334l2.633,5.334l5.885,0.855l-4.258,4.152L15.422,18.129z" fill="red"/>
</g>
</g>
</svg>`;

export async function testCase(canvas: fabric.Canvas) {
const svg = await fabric.loadSVGFromString(svgString);
canvas.add(...(svg.objects.filter((obj) => !!obj) as fabric.FabricObject[]));
}
3 changes: 2 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
uses: ./.github/actions/build-fabric-cached
- name: Run ${{ matrix.suite }} tests with coverage
if: matrix.suite == 'unit'
run: npm run test:coverage
run: npm run test:coverage && sleep 5
- name: Run ${{ matrix.suite }} tests with coverage
if: matrix.suite == 'visual'
run: npm run test:visual:coverage
Expand All @@ -49,6 +49,7 @@ jobs:
with:
name: coverage-${{ matrix.suite }}
path: .nyc_output/*.json

browser:
needs: [prime-build]
name: ${{ matrix.target }} ${{ matrix.suite }} tests
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
# Changelog

## [next]

- fix(FabricObject): Fix clipPath blurryness with scale [#9774](https://github.com/fabricjs/fabric.js/pull/9774)

## [6.4.3]

- fix(FabricObject): Render clipPath as sharp as the object [#9774](https://github.com/fabricjs/fabric.js/pull/9774)
- fix(Controls): changeWidth can change width with decimals [#10186](https://github.com/fabricjs/fabric.js/pull/10186)
- ci(): Add some prebuilt fabric in the dist folder [#10178](https://github.com/fabricjs/fabric.js/pull/10178)
- chore(): Add more generic font families to FabricText.genericFonts [#10167](https://github.com/fabricjs/fabric.js/pull/10167)
Expand Down
16 changes: 8 additions & 8 deletions src/canvas/StaticCanvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
} from '../util/animation/AnimationFrameProvider';
import { runningAnimations } from '../util/animation/AnimationRegistry';
import { uid } from '../util/internals/uid';
import { createCanvasElement, toDataURL } from '../util/misc/dom';
import { createCanvasElementFor, toDataURL } from '../util/misc/dom';
import { invertTransform, transformPoint } from '../util/misc/matrix';
import type { EnlivenObjectOptions } from '../util/misc/objectEnlive';
import {
Expand Down Expand Up @@ -585,9 +585,10 @@ export class StaticCanvas<
if (path) {
path._set('canvas', this);
// needed to setup a couple of variables
// todo migrate to the newer one
path.shouldCache();
path._transformDone = true;
path.renderCache({ forClipping: true });
(path as TCachedFabricObject).renderCache({ forClipping: true });
this.drawClipPathOnCanvas(ctx, path as TCachedFabricObject);
}
this._renderOverlay(ctx);
Expand Down Expand Up @@ -1334,9 +1335,7 @@ export class StaticCanvas<
* This essentially copies canvas dimensions since loadFromJSON does not affect canvas size.
*/
cloneWithoutData() {
const el = createCanvasElement();
el.width = this.width;
el.height = this.height;
const el = createCanvasElementFor(this);
return new (this.constructor as Constructor<this>)(el);
}

Expand Down Expand Up @@ -1425,12 +1424,13 @@ export class StaticCanvas<
translateY = (vp[5] - (top || 0)) * multiplier,
newVp = [newZoom, 0, 0, newZoom, translateX, translateY] as TMat2D,
originalRetina = this.enableRetinaScaling,
canvasEl = createCanvasElement(),
canvasEl = createCanvasElementFor({
width: scaledWidth,
height: scaledHeight,
}),
objectsToRender = filter
? this._objects.filter((obj) => filter(obj))
: this._objects;
canvasEl.width = scaledWidth;
canvasEl.height = scaledHeight;
this.enableRetinaScaling = false;
this.viewportTransform = newVp;
this.width = scaledWidth;
Expand Down
10 changes: 6 additions & 4 deletions src/filters/BaseFilter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { getEnv } from '../env';
import { createCanvasElement } from '../util/misc/dom';
import type {
T2DPipelineState,
TWebGLAttributeLocationMap,
Expand All @@ -15,6 +14,7 @@ import {
} from './shaders/baseFilter';
import type { Abortable } from '../typedefs';
import { FabricError } from '../util/internals/console';
import { createCanvasElementFor } from '../util/misc/dom';

const regex = new RegExp(highPsourceCode, 'g');

Expand Down Expand Up @@ -368,9 +368,11 @@ export class BaseFilter<
*/
createHelpLayer(options: T2DPipelineState) {
if (!options.helpLayer) {
const helpLayer = createCanvasElement();
helpLayer.width = options.sourceWidth;
helpLayer.height = options.sourceHeight;
const { sourceWidth, sourceHeight } = options;
const helpLayer = createCanvasElementFor({
width: sourceWidth,
height: sourceHeight,
});
options.helpLayer = helpLayer;
}
}
Expand Down
6 changes: 2 additions & 4 deletions src/filters/WebGLFilterBackend.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { config } from '../config';
import { createCanvasElement } from '../util/misc/dom';
import { createCanvasElementFor } from '../util/misc/dom';
import type {
TWebGLPipelineState,
TProgramCache,
Expand Down Expand Up @@ -72,9 +72,7 @@ export class WebGLFilterBackend {
* class properties to the GLFilterBackend class.
*/
createWebGLCanvas(width: number, height: number): void {
const canvas = createCanvasElement();
canvas.width = width;
canvas.height = height;
const canvas = createCanvasElementFor({ width, height });
const glOptions = {
alpha: true,
premultipliedAlpha: false,
Expand Down
6 changes: 2 additions & 4 deletions src/filters/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getFabricWindow } from '../env';
import { createCanvasElement } from '../util/misc/dom';
import { createCanvasElement, createCanvasElementFor } from '../util/misc/dom';
import { WebGLFilterBackend } from './WebGLFilterBackend';
import type { TWebGLPipelineState, T2DPipelineState } from './typedefs';

Expand All @@ -16,7 +16,7 @@ export const isWebGLPipelineState = (
* putImageData is faster than drawImage for that specific operation.
*/
export const isPutImageFaster = (width: number, height: number): boolean => {
const targetCanvas = createCanvasElement();
const targetCanvas = createCanvasElementFor({ width, height });
const sourceCanvas = createCanvasElement();
const gl = sourceCanvas.getContext('webgl')!;
// eslint-disable-next-line no-undef
Expand All @@ -31,8 +31,6 @@ export const isPutImageFaster = (width: number, height: number): boolean => {
targetCanvas: targetCanvas,
} as unknown as TWebGLPipelineState;
let startTime;
targetCanvas.width = width;
targetCanvas.height = height;

startTime = getFabricWindow().performance.now();
WebGLFilterBackend.prototype.copyGLTo2D.call(
Expand Down
16 changes: 14 additions & 2 deletions src/parser/elements_parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,11 @@ export class ElementsParser {

// TODO: resolveClipPath could be run once per clippath with minor work per object.
// is a refactor that i m not sure is worth on this code
async resolveClipPath(obj: NotParsedFabricObject, usingElement: Element) {
async resolveClipPath(
obj: NotParsedFabricObject,
usingElement: Element,
exactOwner?: Element,
) {
const clipPathElements = this.extractPropertyDefinition(
obj,
'clipPath',
Expand All @@ -146,6 +150,7 @@ export class ElementsParser {
const clipPathTag = clipPathElements[0].parentElement!;
let clipPathOwner = usingElement;
while (
!exactOwner &&
clipPathOwner.parentElement &&
clipPathOwner.getAttribute('clip-path') !== obj.clipPath
) {
Expand Down Expand Up @@ -188,7 +193,14 @@ export class ElementsParser {
clipPath.calcTransformMatrix(),
);
if (clipPath.clipPath) {
await this.resolveClipPath(clipPath, clipPathOwner);
await this.resolveClipPath(
clipPath,
clipPathOwner,
// this is tricky.
// it tries to differentiate from when clipPaths are inherited by outside groups
// or when are really clipPaths referencing other clipPaths
clipPathTag.getAttribute('clip-path') ? clipPathOwner : undefined,
);
}
const { scaleX, scaleY, angle, skewX, translateX, translateY } =
qrDecompose(gTransform);
Expand Down
1 change: 0 additions & 1 deletion src/parser/parseSVGDocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ export async function parseSVGDocument(
crossOrigin,
signal,
};

const elements = descendants.filter((el) => {
applyViewboxTransform(el);
return isValidSvgTag(el) && !hasInvalidAncestor(el); // http://www.w3.org/TR/SVG/struct.html#DefsElement
Expand Down
22 changes: 12 additions & 10 deletions src/shapes/Group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
} from '../LayoutManager/constants';
import type { SerializedLayoutManager } from '../LayoutManager/LayoutManager';
import type { FitContentLayout } from '../LayoutManager';
import type { DrawContext } from './Object/Object';

/**
* This class handles the specific case of creating a group using {@link Group#fromObject} and is not meant to be used in any other case.
Expand All @@ -42,7 +43,6 @@ import type { FitContentLayout } from '../LayoutManager';
* This layout manager doesn't do anything and therefore keeps the exact layout the group had when {@link Group#toObject} was called.
*/
class NoopLayoutManager extends LayoutManager {
// eslint-disable-next-line @typescript-eslint/no-empty-function
performLayout() {}
}

Expand Down Expand Up @@ -493,23 +493,25 @@ export class Group
* Execute the drawing operation for an object on a specified context
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
drawObject(ctx: CanvasRenderingContext2D) {
drawObject(
ctx: CanvasRenderingContext2D,
forClipping: boolean | undefined,
context: DrawContext,
) {
this._renderBackground(ctx);
for (let i = 0; i < this._objects.length; i++) {
const obj = this._objects[i];
// TODO: handle rendering edge case somehow
if (
this.canvas?.preserveObjectStacking &&
this._objects[i].group !== this
) {
if (this.canvas?.preserveObjectStacking && obj.group !== this) {
ctx.save();
ctx.transform(...invertTransform(this.calcTransformMatrix()));
this._objects[i].render(ctx);
obj.render(ctx);
ctx.restore();
} else if (this._objects[i].group === this) {
this._objects[i].render(ctx);
} else if (obj.group === this) {
obj.render(ctx);
}
}
this._drawClipPath(ctx, this.clipPath);
this._drawClipPath(ctx, this.clipPath, context);
}

/**
Expand Down
20 changes: 9 additions & 11 deletions src/shapes/Image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type {
TOptions,
} from '../typedefs';
import { uid } from '../util/internals/uid';
import { createCanvasElement } from '../util/misc/dom';
import { createCanvasElementFor } from '../util/misc/dom';
import { findScaleToCover, findScaleToFit } from '../util/misc/findScaleTo';
import type { LoadImageOptions } from '../util/misc/objectEnlive';
import {
Expand Down Expand Up @@ -497,19 +497,16 @@ export class FabricImage<
this._lastScaleY = scaleY;
return;
}
const canvasEl = createCanvasElement(),
sourceWidth = elementToFilter.width,
sourceHeight = elementToFilter.height;
canvasEl.width = sourceWidth;
canvasEl.height = sourceHeight;
const canvasEl = createCanvasElementFor(elementToFilter),
{ width, height } = elementToFilter;
this._element = canvasEl;
this._lastScaleX = filter.scaleX = scaleX;
this._lastScaleY = filter.scaleY = scaleY;
getFilterBackend().applyFilters(
[filter],
elementToFilter,
sourceWidth,
sourceHeight,
width,
height,
this._element,
);
this._filterScalingX = canvasEl.width / this._originalElement.width;
Expand Down Expand Up @@ -549,9 +546,10 @@ export class FabricImage<
if (this._element === this._originalElement) {
// if the _element a reference to _originalElement
// we need to create a new element to host the filtered pixels
const canvasEl = createCanvasElement();
canvasEl.width = sourceWidth;
canvasEl.height = sourceHeight;
const canvasEl = createCanvasElementFor({
width: sourceWidth,
height: sourceHeight,
});
this._element = canvasEl;
this._filteredEl = canvasEl;
} else if (this._filteredEl) {
Expand Down
Loading
Loading