import { Util } from './Util'; import { Konva } from './Global'; import { Canvas } from './Canvas'; import { Shape } from './Shape'; import { IRect } from './types'; import type { Node } from './Node'; function simplifyArray(arr: Array) { var retArr: Array = [], len = arr.length, util = Util, n, val; for (n = 0; n < len; n++) { val = arr[n]; if (util._isNumber(val)) { val = Math.round(val * 1000) / 1000; } else if (!util._isString(val)) { val = val + ''; } retArr.push(val); } return retArr; } var COMMA = ',', OPEN_PAREN = '(', CLOSE_PAREN = ')', OPEN_PAREN_BRACKET = '([', CLOSE_BRACKET_PAREN = '])', SEMICOLON = ';', DOUBLE_PAREN = '()', // EMPTY_STRING = '', EQUALS = '=', // SET = 'set', CONTEXT_METHODS = [ 'arc', 'arcTo', 'beginPath', 'bezierCurveTo', 'clearRect', 'clip', 'closePath', 'createLinearGradient', 'createPattern', 'createRadialGradient', 'drawImage', 'ellipse', 'fill', 'fillText', 'getImageData', 'createImageData', 'lineTo', 'moveTo', 'putImageData', 'quadraticCurveTo', 'rect', 'roundRect', 'restore', 'rotate', 'save', 'scale', 'setLineDash', 'setTransform', 'stroke', 'strokeText', 'transform', 'translate', ]; var CONTEXT_PROPERTIES = [ 'fillStyle', 'strokeStyle', 'shadowColor', 'shadowBlur', 'shadowOffsetX', 'shadowOffsetY', 'letterSpacing', 'lineCap', 'lineDashOffset', 'lineJoin', 'lineWidth', 'miterLimit', 'direction', 'font', 'textAlign', 'textBaseline', 'globalAlpha', 'globalCompositeOperation', 'imageSmoothingEnabled', ] as const; const traceArrMax = 100; interface ExtendedCanvasRenderingContext2D extends CanvasRenderingContext2D { letterSpacing: string; } /** * Konva wrapper around native 2d canvas context. It has almost the same API of 2d context with some additional functions. * With core Konva shapes you don't need to use this object. But you will use it if you want to create * a [custom shape](/docs/react/Custom_Shape.html) or a [custom hit regions](/docs/events/Custom_Hit_Region.html). * For full information about each 2d context API use [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D) * @constructor * @memberof Konva * @example * const rect = new Konva.Shape({ * fill: 'red', * width: 100, * height: 100, * sceneFunc: (ctx, shape) => { * // ctx - is context wrapper * // shape - is instance of Konva.Shape, so it equals to "rect" variable * ctx.rect(0, 0, shape.getAttr('width'), shape.getAttr('height')); * * // automatically fill shape from props and draw hit region * ctx.fillStrokeShape(shape); * } * }) */ export class Context { canvas: Canvas; _context: CanvasRenderingContext2D; traceArr: Array; constructor(canvas: Canvas) { this.canvas = canvas; if (Konva.enableTrace) { this.traceArr = []; this._enableTrace(); } } /** * fill shape * @method * @name Konva.Context#fillShape * @param {Konva.Shape} shape */ fillShape(shape: Shape) { if (shape.fillEnabled()) { this._fill(shape); } } _fill(shape: Shape) { // abstract } /** * stroke shape * @method * @name Konva.Context#strokeShape * @param {Konva.Shape} shape */ strokeShape(shape: Shape) { if (shape.hasStroke()) { this._stroke(shape); } } _stroke(shape: Shape) { // abstract } /** * fill then stroke * @method * @name Konva.Context#fillStrokeShape * @param {Konva.Shape} shape */ fillStrokeShape(shape: Shape) { if (shape.attrs.fillAfterStrokeEnabled) { this.strokeShape(shape); this.fillShape(shape); } else { this.fillShape(shape); this.strokeShape(shape); } } getTrace(relaxed?: boolean, rounded?: boolean) { var traceArr = this.traceArr, len = traceArr.length, str = '', n, trace, method, args; for (n = 0; n < len; n++) { trace = traceArr[n]; method = trace.method; // methods if (method) { args = trace.args; str += method; if (relaxed) { str += DOUBLE_PAREN; } else { if (Util._isArray(args[0])) { str += OPEN_PAREN_BRACKET + args.join(COMMA) + CLOSE_BRACKET_PAREN; } else { if (rounded) { args = args.map((a) => typeof a === 'number' ? Math.floor(a) : a ); } str += OPEN_PAREN + args.join(COMMA) + CLOSE_PAREN; } } } else { // properties str += trace.property; if (!relaxed) { str += EQUALS + trace.val; } } str += SEMICOLON; } return str; } clearTrace() { this.traceArr = []; } _trace(str) { var traceArr = this.traceArr, len; traceArr.push(str); len = traceArr.length; if (len >= traceArrMax) { traceArr.shift(); } } /** * reset canvas context transform * @method * @name Konva.Context#reset */ reset() { var pixelRatio = this.getCanvas().getPixelRatio(); this.setTransform(1 * pixelRatio, 0, 0, 1 * pixelRatio, 0, 0); } /** * get canvas wrapper * @method * @name Konva.Context#getCanvas * @returns {Konva.Canvas} */ getCanvas() { return this.canvas; } /** * clear canvas * @method * @name Konva.Context#clear * @param {Object} [bounds] * @param {Number} [bounds.x] * @param {Number} [bounds.y] * @param {Number} [bounds.width] * @param {Number} [bounds.height] */ clear(bounds?: IRect) { var canvas = this.getCanvas(); if (bounds) { this.clearRect( bounds.x || 0, bounds.y || 0, bounds.width || 0, bounds.height || 0 ); } else { this.clearRect( 0, 0, canvas.getWidth() / canvas.pixelRatio, canvas.getHeight() / canvas.pixelRatio ); } } _applyLineCap(shape: Shape) { const lineCap = shape.attrs.lineCap; if (lineCap) { this.setAttr('lineCap', lineCap); } } _applyOpacity(shape: Node) { var absOpacity = shape.getAbsoluteOpacity(); if (absOpacity !== 1) { this.setAttr('globalAlpha', absOpacity); } } _applyLineJoin(shape: Shape) { const lineJoin = shape.attrs.lineJoin; if (lineJoin) { this.setAttr('lineJoin', lineJoin); } } setAttr(attr: string, val) { this._context[attr] = val; } /** * arc function. * @method * @name Konva.Context#arc */ arc( x: number, y: number, radius: number, startAngle: number, endAngle: number, counterClockwise?: boolean ) { this._context.arc(x, y, radius, startAngle, endAngle, counterClockwise); } /** * arcTo function. * @method * @name Konva.Context#arcTo * */ arcTo(x1: number, y1: number, x2: number, y2: number, radius: number) { this._context.arcTo(x1, y1, x2, y2, radius); } /** * beginPath function. * @method * @name Konva.Context#beginPath */ beginPath() { this._context.beginPath(); } /** * bezierCurveTo function. * @method * @name Konva.Context#bezierCurveTo */ bezierCurveTo( cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number ) { this._context.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y); } /** * clearRect function. * @method * @name Konva.Context#clearRect */ clearRect(x: number, y: number, width: number, height: number) { this._context.clearRect(x, y, width, height); } /** * clip function. * @method * @name Konva.Context#clip */ clip(fillRule?: CanvasFillRule): void; clip(path: Path2D, fillRule?: CanvasFillRule): void; clip(...args: any[]) { this._context.clip.apply(this._context, args as any); } /** * closePath function. * @method * @name Konva.Context#closePath */ closePath() { this._context.closePath(); } /** * createImageData function. * @method * @name Konva.Context#createImageData */ createImageData(width, height) { var a = arguments; if (a.length === 2) { return this._context.createImageData(width, height); } else if (a.length === 1) { return this._context.createImageData(width); } } /** * createLinearGradient function. * @method * @name Konva.Context#createLinearGradient */ createLinearGradient(x0: number, y0: number, x1: number, y1: number) { return this._context.createLinearGradient(x0, y0, x1, y1); } /** * createPattern function. * @method * @name Konva.Context#createPattern */ createPattern(image: CanvasImageSource, repetition: string | null) { return this._context.createPattern(image, repetition); } /** * createRadialGradient function. * @method * @name Konva.Context#createRadialGradient */ createRadialGradient( x0: number, y0: number, r0: number, x1: number, y1: number, r1: number ) { return this._context.createRadialGradient(x0, y0, r0, x1, y1, r1); } /** * drawImage function. * @method * @name Konva.Context#drawImage */ drawImage( image: CanvasImageSource, sx: number, sy: number, sWidth?: number, sHeight?: number, dx?: number, dy?: number, dWidth?: number, dHeight?: number ) { // this._context.drawImage(...arguments); var a = arguments, _context = this._context; if (a.length === 3) { _context.drawImage(image, sx, sy); } else if (a.length === 5) { _context.drawImage(image, sx, sy, sWidth as number, sHeight as number); } else if (a.length === 9) { _context.drawImage( image, sx, sy, sWidth as number, sHeight as number, dx as number, dy as number, dWidth as number, dHeight as number ); } } /** * ellipse function. * @method * @name Konva.Context#ellipse */ ellipse( x: number, y: number, radiusX: number, radiusY: number, rotation: number, startAngle: number, endAngle: number, counterclockwise?: boolean ) { this._context.ellipse( x, y, radiusX, radiusY, rotation, startAngle, endAngle, counterclockwise ); } /** * isPointInPath function. * @method * @name Konva.Context#isPointInPath */ isPointInPath( x: number, y: number, path?: Path2D, fillRule?: CanvasFillRule ) { if (path) { return this._context.isPointInPath(path, x, y, fillRule); } return this._context.isPointInPath(x, y, fillRule); } /** * fill function. * @method * @name Konva.Context#fill */ fill(fillRule?: CanvasFillRule): void; fill(path: Path2D, fillRule?: CanvasFillRule): void; fill(...args: any[]) { // this._context.fill(); this._context.fill.apply(this._context, args as any); } /** * fillRect function. * @method * @name Konva.Context#fillRect */ fillRect(x: number, y: number, width: number, height: number) { this._context.fillRect(x, y, width, height); } /** * strokeRect function. * @method * @name Konva.Context#strokeRect */ strokeRect(x: number, y: number, width: number, height: number) { this._context.strokeRect(x, y, width, height); } /** * fillText function. * @method * @name Konva.Context#fillText */ fillText(text: string, x: number, y: number, maxWidth?: number) { if (maxWidth) { this._context.fillText(text, x, y, maxWidth); } else { this._context.fillText(text, x, y); } } /** * measureText function. * @method * @name Konva.Context#measureText */ measureText(text: string) { return this._context.measureText(text); } /** * getImageData function. * @method * @name Konva.Context#getImageData */ getImageData(sx: number, sy: number, sw: number, sh: number) { return this._context.getImageData(sx, sy, sw, sh); } /** * lineTo function. * @method * @name Konva.Context#lineTo */ lineTo(x: number, y: number) { this._context.lineTo(x, y); } /** * moveTo function. * @method * @name Konva.Context#moveTo */ moveTo(x: number, y: number) { this._context.moveTo(x, y); } /** * rect function. * @method * @name Konva.Context#rect */ rect(x: number, y: number, width: number, height: number) { this._context.rect(x, y, width, height); } /** * roundRect function. * @method * @name Konva.Context#roundRect */ roundRect(x: number, y: number, width: number, height: number, radii: number) { if (this._context.roundRect) { this._context.roundRect(x, y, width, height, radii); } else { this._context.rect(x, y, width, height); } } /** * putImageData function. * @method * @name Konva.Context#putImageData */ putImageData(imageData: ImageData, dx: number, dy: number) { this._context.putImageData(imageData, dx, dy); } /** * quadraticCurveTo function. * @method * @name Konva.Context#quadraticCurveTo */ quadraticCurveTo(cpx: number, cpy: number, x: number, y: number) { this._context.quadraticCurveTo(cpx, cpy, x, y); } /** * restore function. * @method * @name Konva.Context#restore */ restore() { this._context.restore(); } /** * rotate function. * @method * @name Konva.Context#rotate */ rotate(angle: number) { this._context.rotate(angle); } /** * save function. * @method * @name Konva.Context#save */ save() { this._context.save(); } /** * scale function. * @method * @name Konva.Context#scale */ scale(x: number, y: number) { this._context.scale(x, y); } /** * setLineDash function. * @method * @name Konva.Context#setLineDash */ setLineDash(segments: number[]) { // works for Chrome and IE11 if (this._context.setLineDash) { this._context.setLineDash(segments); } else if ('mozDash' in this._context) { // verified that this works in firefox (this._context['mozDash']) = segments; } else if ('webkitLineDash' in this._context) { // does not currently work for Safari (this._context['webkitLineDash']) = segments; } // no support for IE9 and IE10 } /** * getLineDash function. * @method * @name Konva.Context#getLineDash */ getLineDash() { return this._context.getLineDash(); } /** * setTransform function. * @method * @name Konva.Context#setTransform */ setTransform( a: number, b: number, c: number, d: number, e: number, f: number ) { this._context.setTransform(a, b, c, d, e, f); } /** * stroke function. * @method * @name Konva.Context#stroke */ stroke(path2d?: Path2D) { if (path2d) { this._context.stroke(path2d); } else { this._context.stroke(); } } /** * strokeText function. * @method * @name Konva.Context#strokeText */ strokeText(text: string, x: number, y: number, maxWidth?: number) { this._context.strokeText(text, x, y, maxWidth); } /** * transform function. * @method * @name Konva.Context#transform */ transform(a: number, b: number, c: number, d: number, e: number, f: number) { this._context.transform(a, b, c, d, e, f); } /** * translate function. * @method * @name Konva.Context#translate */ translate(x: number, y: number) { this._context.translate(x, y); } _enableTrace() { var that = this, len = CONTEXT_METHODS.length, origSetter = this.setAttr, n, args; // to prevent creating scope function at each loop var func = function (methodName) { var origMethod = that[methodName], ret; that[methodName] = function () { args = simplifyArray(Array.prototype.slice.call(arguments, 0)); ret = origMethod.apply(that, arguments); that._trace({ method: methodName, args: args, }); return ret; }; }; // methods for (n = 0; n < len; n++) { func(CONTEXT_METHODS[n]); } // attrs that.setAttr = function () { origSetter.apply(that, arguments as any); var prop = arguments[0]; var val = arguments[1]; if ( prop === 'shadowOffsetX' || prop === 'shadowOffsetY' || prop === 'shadowBlur' ) { val = val / this.canvas.getPixelRatio(); } that._trace({ property: prop, val: val, }); }; } _applyGlobalCompositeOperation(node) { const op = node.attrs.globalCompositeOperation; var def = !op || op === 'source-over'; if (!def) { this.setAttr('globalCompositeOperation', op); } } } // supported context properties type CanvasContextProps = Pick< ExtendedCanvasRenderingContext2D, (typeof CONTEXT_PROPERTIES)[number] >; export interface Context extends CanvasContextProps {} CONTEXT_PROPERTIES.forEach(function (prop) { Object.defineProperty(Context.prototype, prop, { get() { return this._context[prop]; }, set(val) { this._context[prop] = val; }, }); }); export class SceneContext extends Context { constructor(canvas: Canvas, { willReadFrequently = false } = {}) { super(canvas); this._context = canvas._canvas.getContext('2d', { willReadFrequently, }) as CanvasRenderingContext2D; } _fillColor(shape: Shape) { var fill = shape.fill(); this.setAttr('fillStyle', fill); shape._fillFunc(this); } _fillPattern(shape: Shape) { this.setAttr('fillStyle', shape._getFillPattern()); shape._fillFunc(this); } _fillLinearGradient(shape: Shape) { var grd = shape._getLinearGradient(); if (grd) { this.setAttr('fillStyle', grd); shape._fillFunc(this); } } _fillRadialGradient(shape: Shape) { const grd = shape._getRadialGradient(); if (grd) { this.setAttr('fillStyle', grd); shape._fillFunc(this); } } _fill(shape) { const hasColor = shape.fill(), fillPriority = shape.getFillPriority(); // priority fills if (hasColor && fillPriority === 'color') { this._fillColor(shape); return; } const hasPattern = shape.getFillPatternImage(); if (hasPattern && fillPriority === 'pattern') { this._fillPattern(shape); return; } const hasLinearGradient = shape.getFillLinearGradientColorStops(); if (hasLinearGradient && fillPriority === 'linear-gradient') { this._fillLinearGradient(shape); return; } const hasRadialGradient = shape.getFillRadialGradientColorStops(); if (hasRadialGradient && fillPriority === 'radial-gradient') { this._fillRadialGradient(shape); return; } // now just try and fill with whatever is available if (hasColor) { this._fillColor(shape); } else if (hasPattern) { this._fillPattern(shape); } else if (hasLinearGradient) { this._fillLinearGradient(shape); } else if (hasRadialGradient) { this._fillRadialGradient(shape); } } _strokeLinearGradient(shape) { const start = shape.getStrokeLinearGradientStartPoint(), end = shape.getStrokeLinearGradientEndPoint(), colorStops = shape.getStrokeLinearGradientColorStops(), grd = this.createLinearGradient(start.x, start.y, end.x, end.y); if (colorStops) { // build color stops for (var n = 0; n < colorStops.length; n += 2) { grd.addColorStop(colorStops[n] as number, colorStops[n + 1] as string); } this.setAttr('strokeStyle', grd); } } _stroke(shape) { var dash = shape.dash(), // ignore strokeScaleEnabled for Text strokeScaleEnabled = shape.getStrokeScaleEnabled(); if (shape.hasStroke()) { if (!strokeScaleEnabled) { this.save(); var pixelRatio = this.getCanvas().getPixelRatio(); this.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); } this._applyLineCap(shape); if (dash && shape.dashEnabled()) { this.setLineDash(dash); this.setAttr('lineDashOffset', shape.dashOffset()); } this.setAttr('lineWidth', shape.strokeWidth()); if (!shape.getShadowForStrokeEnabled()) { this.setAttr('shadowColor', 'rgba(0,0,0,0)'); } var hasLinearGradient = shape.getStrokeLinearGradientColorStops(); if (hasLinearGradient) { this._strokeLinearGradient(shape); } else { this.setAttr('strokeStyle', shape.stroke()); } shape._strokeFunc(this); if (!strokeScaleEnabled) { this.restore(); } } } _applyShadow(shape) { var color = shape.getShadowRGBA() ?? 'black', blur = shape.getShadowBlur() ?? 5, offset = shape.getShadowOffset() ?? { x: 0, y: 0, }, scale = shape.getAbsoluteScale(), ratio = this.canvas.getPixelRatio(), scaleX = scale.x * ratio, scaleY = scale.y * ratio; this.setAttr('shadowColor', color); this.setAttr( 'shadowBlur', blur * Math.min(Math.abs(scaleX), Math.abs(scaleY)) ); this.setAttr('shadowOffsetX', offset.x * scaleX); this.setAttr('shadowOffsetY', offset.y * scaleY); } } export class HitContext extends Context { constructor(canvas: Canvas) { super(canvas); this._context = canvas._canvas.getContext('2d', { willReadFrequently: true, }) as CanvasRenderingContext2D; } _fill(shape: Shape) { this.save(); this.setAttr('fillStyle', shape.colorKey); shape._fillFuncHit(this); this.restore(); } strokeShape(shape: Shape) { if (shape.hasHitStroke()) { this._stroke(shape); } } _stroke(shape) { if (shape.hasHitStroke()) { // ignore strokeScaleEnabled for Text const strokeScaleEnabled = shape.getStrokeScaleEnabled(); if (!strokeScaleEnabled) { this.save(); var pixelRatio = this.getCanvas().getPixelRatio(); this.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); } this._applyLineCap(shape); var hitStrokeWidth = shape.hitStrokeWidth(); var strokeWidth = hitStrokeWidth === 'auto' ? shape.strokeWidth() : hitStrokeWidth; this.setAttr('lineWidth', strokeWidth); this.setAttr('strokeStyle', shape.colorKey); shape._strokeFuncHit(this); if (!strokeScaleEnabled) { this.restore(); } } } }