UNPKG

fabric

Version:

Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.

1 lines 12.5 kB
{"version":3,"file":"PencilBrush.mjs","names":[],"sources":["../../../src/brushes/PencilBrush.ts"],"sourcesContent":["import type { ModifierKey, TEvent } from '../EventTypeDefs';\nimport type { Point } from '../Point';\nimport { Shadow } from '../Shadow';\nimport { Path } from '../shapes/Path';\nimport { getSmoothPathFromPoints, joinPath } from '../util/path';\nimport type { Canvas } from '../canvas/Canvas';\nimport { BaseBrush } from './BaseBrush';\nimport type { TSimplePathData } from '../util/path/typedefs';\n\n/**\n * @private\n * @param {TSimplePathData} pathData SVG path commands\n * @returns {boolean}\n */\nfunction isEmptySVGPath(pathData: TSimplePathData): boolean {\n return joinPath(pathData) === 'M 0 0 Q 0 0 0 0 L 0 0';\n}\n\nexport class PencilBrush extends BaseBrush {\n /**\n * Discard points that are less than `decimate` pixel distant from each other\n * @type Number\n * @default 0.4\n */\n decimate = 0.4;\n\n /**\n * Draws a straight line between last recorded point to current pointer\n * Used for `shift` functionality\n *\n * @type boolean\n * @default false\n */\n drawStraightLine = false;\n\n /**\n * The event modifier key that makes the brush draw a straight line.\n * If `null` or 'none' or any other string that is not a modifier key the feature is disabled.\n * @type {ModifierKey | undefined | null}\n */\n straightLineKey: ModifierKey | undefined | null = 'shiftKey';\n\n declare protected _points: Point[];\n declare protected _hasStraightLine: boolean;\n declare protected oldEnd?: Point;\n\n constructor(canvas: Canvas) {\n super(canvas);\n this._points = [];\n this._hasStraightLine = false;\n }\n\n needsFullRender() {\n return super.needsFullRender() || this._hasStraightLine;\n }\n\n static drawSegment(ctx: CanvasRenderingContext2D, p1: Point, p2: Point) {\n const midPoint = p1.midPointFrom(p2);\n ctx.quadraticCurveTo(p1.x, p1.y, midPoint.x, midPoint.y);\n return midPoint;\n }\n\n /**\n * Invoked on mouse down\n * @param {Point} pointer\n */\n onMouseDown(pointer: Point, { e }: TEvent) {\n if (!this.canvas._isMainEvent(e)) {\n return;\n }\n this.drawStraightLine = !!this.straightLineKey && e[this.straightLineKey];\n this._prepareForDrawing(pointer);\n // capture coordinates immediately\n // this allows to draw dots (when movement never occurs)\n this._addPoint(pointer);\n this._render();\n }\n\n /**\n * Invoked on mouse move\n * @param {Point} pointer\n */\n onMouseMove(pointer: Point, { e }: TEvent) {\n if (!this.canvas._isMainEvent(e)) {\n return;\n }\n this.drawStraightLine = !!this.straightLineKey && e[this.straightLineKey];\n if (this.limitedToCanvasSize === true && this._isOutSideCanvas(pointer)) {\n return;\n }\n if (this._addPoint(pointer) && this._points.length > 1) {\n if (this.needsFullRender()) {\n // redraw curve\n // clear top canvas\n this.canvas.clearContext(this.canvas.contextTop);\n this._render();\n } else {\n const points = this._points,\n length = points.length,\n ctx = this.canvas.contextTop;\n // draw the curve update\n this._saveAndTransform(ctx);\n if (this.oldEnd) {\n ctx.beginPath();\n ctx.moveTo(this.oldEnd.x, this.oldEnd.y);\n }\n this.oldEnd = PencilBrush.drawSegment(\n ctx,\n points[length - 2],\n points[length - 1],\n );\n ctx.stroke();\n ctx.restore();\n }\n }\n }\n\n /**\n * Invoked on mouse up\n */\n onMouseUp({ e }: TEvent) {\n if (!this.canvas._isMainEvent(e)) {\n return true;\n }\n this.drawStraightLine = false;\n this.oldEnd = undefined;\n this._finalizeAndAddPath();\n\n return false;\n }\n\n /**\n * @private\n * @param {Point} pointer Actual mouse position related to the canvas.\n */\n _prepareForDrawing(pointer: Point) {\n this._reset();\n this._addPoint(pointer);\n this.canvas.contextTop.moveTo(pointer.x, pointer.y);\n }\n\n /**\n * @private\n * @param {Point} point Point to be added to points array\n */\n _addPoint(point: Point) {\n if (\n this._points.length > 1 &&\n point.eq(this._points[this._points.length - 1])\n ) {\n return false;\n }\n if (this.drawStraightLine && this._points.length > 1) {\n this._hasStraightLine = true;\n this._points.pop();\n }\n this._points.push(point);\n return true;\n }\n\n /**\n * Clear points array and set contextTop canvas style.\n * @private\n */\n _reset() {\n this._points = [];\n this._setBrushStyles(this.canvas.contextTop);\n this._setShadow();\n this._hasStraightLine = false;\n }\n\n /**\n * Draw a smooth path on the topCanvas using quadraticCurveTo\n * @private\n * @param {CanvasRenderingContext2D} [ctx]\n */\n _render(ctx: CanvasRenderingContext2D = this.canvas.contextTop) {\n let p1 = this._points[0],\n p2 = this._points[1];\n this._saveAndTransform(ctx);\n ctx.beginPath();\n //if we only have 2 points in the path and they are the same\n //it means that the user only clicked the canvas without moving the mouse\n //then we should be drawing a dot. A path isn't drawn between two identical dots\n //that's why we set them apart a bit\n if (this._points.length === 2 && p1.x === p2.x && p1.y === p2.y) {\n const width = this.width / 1000;\n p1.x -= width;\n p2.x += width;\n }\n ctx.moveTo(p1.x, p1.y);\n\n for (let i = 1; i < this._points.length; i++) {\n // we pick the point between pi + 1 & pi + 2 as the\n // end point and p1 as our control point.\n PencilBrush.drawSegment(ctx, p1, p2);\n p1 = this._points[i];\n p2 = this._points[i + 1];\n }\n // Draw last line as a straight line while\n // we wait for the next point to be able to calculate\n // the bezier control point\n ctx.lineTo(p1.x, p1.y);\n ctx.stroke();\n ctx.restore();\n }\n\n /**\n * Converts points to SVG path\n * @param {Point[]} points Array of points\n * @return {TSimplePathData} SVG path commands\n */\n convertPointsToSVGPath(points: Point[]): TSimplePathData {\n const correction = this.width / 1000;\n return getSmoothPathFromPoints(points, correction);\n }\n\n /**\n * Creates a Path object to add on canvas\n * @param {TSimplePathData} pathData Path data\n * @return {Path} Path to add on canvas\n */\n createPath(pathData: TSimplePathData): Path {\n const path = new Path(pathData, {\n fill: null,\n stroke: this.color,\n strokeWidth: this.width,\n strokeLineCap: this.strokeLineCap,\n strokeMiterLimit: this.strokeMiterLimit,\n strokeLineJoin: this.strokeLineJoin,\n strokeDashArray: this.strokeDashArray,\n });\n if (this.shadow) {\n this.shadow.affectStroke = true;\n path.shadow = new Shadow(this.shadow);\n }\n\n return path;\n }\n\n /**\n * Decimate points array with the decimate value\n */\n decimatePoints(points: Point[], distance: number) {\n if (points.length <= 2) {\n return points;\n }\n let lastPoint = points[0],\n cDistance;\n const zoom = this.canvas.getZoom(),\n adjustedDistance = Math.pow(distance / zoom, 2),\n l = points.length - 1,\n newPoints = [lastPoint];\n // TODO investigate why this is not i < l\n for (let i = 1; i < l - 1; i++) {\n cDistance =\n Math.pow(lastPoint.x - points[i].x, 2) +\n Math.pow(lastPoint.y - points[i].y, 2);\n if (cDistance >= adjustedDistance) {\n lastPoint = points[i];\n newPoints.push(lastPoint);\n }\n }\n // Add the last point from the original line to the end of the array.\n // This ensures decimate doesn't delete the last point on the line, and ensures the line is > 1 point.\n newPoints.push(points[l]);\n return newPoints;\n }\n\n /**\n * On mouseup after drawing the path on contextTop canvas\n * we use the points captured to create an new Path object\n * and add it to the canvas.\n */\n _finalizeAndAddPath() {\n const ctx = this.canvas.contextTop;\n ctx.closePath();\n if (this.decimate) {\n this._points = this.decimatePoints(this._points, this.decimate);\n }\n const pathData = this.convertPointsToSVGPath(this._points);\n if (isEmptySVGPath(pathData)) {\n // do not create 0 width/height paths, as they are\n // rendered inconsistently across browsers\n // Firefox 4, for example, renders a dot,\n // whereas Chrome 10 renders nothing\n this.canvas.requestRenderAll();\n return;\n }\n\n const path = this.createPath(pathData);\n this.canvas.clearContext(this.canvas.contextTop);\n this.canvas.fire('before:path:created', { path: path });\n this.canvas.add(path);\n this.canvas.requestRenderAll();\n path.setCoords();\n this._resetShadow();\n\n // fire event 'path' created\n this.canvas.fire('path:created', { path: path });\n }\n}\n"],"mappings":"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"}