support context.clip(...) and context.fill(...) Path2D and fill-rule options

This commit is contained in:
Ales Menzel 2023-05-02 17:27:45 +02:00
parent 3c0c6c8a3e
commit 95048b5086
7 changed files with 114 additions and 16 deletions

View File

@ -20,8 +20,8 @@
"test": "npm run test:browser && npm run test:node",
"test:build": "parcel build ./test/unit-tests.html --dist-dir ./test-build --target none --public-url ./ --no-source-maps",
"test:browser": "npm run test:build && mocha-headless-chrome -f ./test-build/unit-tests.html -a disable-web-security",
"test:node": "ts-mocha -p ./test/tsconfig.json test/unit/**/*.ts --exit && npm run test:import",
"test:watch": "rm -rf ./parcel-cache && parcel serve ./test/unit-tests.html ./test/manual-tests.html ./test/sandbox.html ./test/text-paths.html",
"test:node": "ts-mocha -r ./test/node-global-setup.mjs -p ./test/tsconfig.json test/unit/**/*.ts --exit && npm run test:import",
"test:watch": "rm -rf ./.parcel-cache && parcel serve ./test/unit-tests.html ./test/manual-tests.html ./test/sandbox.html ./test/text-paths.html",
"tsc": "tsc --removeComments",
"rollup": "rollup -c --bundleConfigAsCjs",
"clean": "rm -rf ./lib && rm -rf ./types && rm -rf ./cmj && rm -rf ./test-build",

View File

@ -7,9 +7,10 @@ import { Shape } from './Shape';
import { HitCanvas, SceneCanvas } from './Canvas';
import { SceneContext } from './Context';
export type ClipFuncOutput = void | [Path2D | CanvasFillRule] | [Path2D, CanvasFillRule]
export interface ContainerConfig extends NodeConfig {
clearBeforeDraw?: boolean;
clipFunc?: (ctx: SceneContext) => void;
clipFunc?: (ctx: SceneContext) => ClipFuncOutput;
clipX?: number;
clipY?: number;
clipWidth?: number;
@ -396,14 +397,15 @@ export abstract class Container<
var m = transform.getMatrix();
context.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
context.beginPath();
let clipArgs;
if (clipFunc) {
clipFunc.call(this, context, this);
clipArgs = clipFunc.call(this, context, this);
} else {
var clipX = this.clipX();
var clipY = this.clipY();
context.rect(clipX, clipY, clipWidth, clipHeight);
}
context.clip();
context.clip.apply(context, clipArgs);
m = transform.copy().invert().getMatrix();
context.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
}
@ -519,7 +521,7 @@ export abstract class Container<
// there was "this" instead of "Container<ChildType>",
// but it breaks react-konva types: https://github.com/konvajs/react-konva/issues/390
clipFunc: GetSet<
(ctx: CanvasRenderingContext2D, shape: Container<ChildType>) => void,
(ctx: CanvasRenderingContext2D, shape: Container<ChildType>) => ClipFuncOutput,
this
>;
}
@ -638,5 +640,8 @@ Factory.addGetterSetter(Container, 'clipFunc');
* // set clip height
* container.clipFunc(function(ctx) {
* ctx.rect(0, 0, 100, 100);
*
* // optionally return a clip Path2D and clip-rule or just the clip-rule
* return [new Path2D('M0 0v50h50Z'), 'evenodd']
* });
*/

View File

@ -362,8 +362,10 @@ export class Context {
* @method
* @name Konva.Context#clip
*/
clip() {
this._context.clip();
clip(fillRule?: CanvasFillRule): void
clip(path: Path2D, fillRule?: CanvasFillRule): void
clip(...args: any[]) {
this._context.clip.apply(this._context, args);
}
/**
* closePath function.
@ -482,12 +484,10 @@ export class Context {
* @method
* @name Konva.Context#fill
*/
fill(path2d?: Path2D) {
if (path2d) {
this._context.fill(path2d);
} else {
this._context.fill();
}
fill(fillRule?: CanvasFillRule): void
fill(path: Path2D, fillRule?: CanvasFillRule): void
fill(...args: any[]) {
this._context.fill.apply(this._context, args);
}
/**
* fillRect function.

View File

@ -56,6 +56,7 @@ export interface ShapeConfig extends NodeConfig {
fillRadialGradientColorStops?: Array<number | string>;
fillEnabled?: boolean;
fillPriority?: string;
fillRule?: CanvasFillRule
stroke?: string | CanvasGradient;
strokeWidth?: number;
fillAfterStrokeEnabled?: boolean;
@ -88,6 +89,8 @@ export interface ShapeGetClientRectConfig {
relativeTo?: Node;
}
export type FillFuncOutput = void | [Path2D | CanvasFillRule] | [Path2D, CanvasFillRule]
var HAS_SHADOW = 'hasShadow';
var SHADOW_RGBA = 'shadowRGBA';
var patternImage = 'patternImage';
@ -112,7 +115,12 @@ export const shapes: { [key: string]: Shape } = {};
// what color to use for hit test?
function _fillFunc(context) {
context.fill();
const fillRule = this.fillRule()
if (fillRule) {
context.fill(fillRule);
} else {
context.fill();
}
}
function _strokeFunc(context) {
context.stroke();
@ -176,7 +184,7 @@ export class Shape<
_centroid: boolean;
colorKey: string;
_fillFunc: (ctx: Context) => void;
_fillFunc: (ctx: Context) => FillFuncOutput;
_strokeFunc: (ctx: Context) => void;
_fillFuncHit: (ctx: Context) => void;
_strokeFuncHit: (ctx: Context) => void;
@ -1987,6 +1995,22 @@ Factory.addGetterSetter(Shape, 'fillPatternRotation', 0);
* shape.fillPatternRotation(20);
*/
Factory.addGetterSetter(Shape, 'fillRule', undefined, getStringValidator());
/**
* get/set fill rule
* @name Konva.Shape#fillRule
* @method
* @param {CanvasFillRule} rotation
* @returns {Konva.Shape}
* @example
* // get fill rule
* var fillRule = shape.fillRule();
*
* // set fill rule
* shape.fillRule('evenodd);
*/
Factory.backCompat(Shape, {
dashArray: 'dash',
getDashArray: 'getDash',

View File

@ -0,0 +1,11 @@
export function mochaGlobalSetup() {
globalThis.Path2D ??= class Path2D {
constructor(path) {
this.path = path
}
get [Symbol.toStringTag]() {
return `Path2D`;
}
}
}

View File

@ -1,4 +1,5 @@
import { addStage, cloneAndCompareLayer, Konva } from './test-utils';
import { assert } from 'chai';
describe('Group', function () {
// ======================================================
@ -45,4 +46,42 @@ describe('Group', function () {
cloneAndCompareLayer(layer, 200);
});
it('clip group with a Path2D', function () {
var stage = addStage();
var layer = new Konva.Layer();
var path = new Konva.Group({
width: 100,
height: 100,
clipFunc: () => [new Path2D('M0 0v50h50Z')]
});
layer.add(path);
stage.add(layer);
const trace = layer.getContext().getTrace()
assert.equal(trace, 'clearRect(0,0,578,200);save();transform(1,0,0,1,0,0);beginPath();clip([object Path2D]);transform(1,0,0,1,0,0);restore();');
});
it('clip group with a Path2D and clipRule', function () {
var stage = addStage();
var layer = new Konva.Layer();
var path = new Konva.Group({
width: 100,
height: 100,
clipFunc: () => [new Path2D('M0 0v50h50Z'), 'evenodd'],
});
layer.add(path);
stage.add(layer);
const trace = layer.getContext().getTrace()
assert.equal(trace, 'clearRect(0,0,578,200);save();transform(1,0,0,1,0,0);beginPath();clip([object Path2D],evenodd);transform(1,0,0,1,0,0);restore();');
});
});

View File

@ -1603,4 +1603,23 @@ describe('Path', function () {
assert.equal(trace1, trace2);
});
it('draw path with fillRule', function () {
var stage = addStage();
var layer = new Konva.Layer();
var path = new Konva.Path({
data: 'M200,100h100v50z',
fill: '#ccc',
fillRule: 'evenodd',
});
layer.add(path);
stage.add(layer);
const trace = layer.getContext().getTrace()
assert.equal(trace, 'clearRect(0,0,578,200);save();transform(1,0,0,1,0,0);beginPath();moveTo(200,100);lineTo(300,100);lineTo(300,150);closePath();fillStyle=#ccc;fill(evenodd);restore();');
});
});