UNPKG

fabric

Version:

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

1 lines 15.6 kB
{"version":3,"file":"StrokeLineJoinProjections.min.mjs","names":[],"sources":["../../../../../src/util/misc/projectStroke/StrokeLineJoinProjections.ts"],"sourcesContent":["import type { XY } from '../../../Point';\nimport { Point } from '../../../Point';\nimport { halfPI, twoMathPi } from '../../../constants';\nimport type { TRadian } from '../../../typedefs';\nimport { degreesToRadians } from '../radiansDegreesConversion';\nimport {\n calcAngleBetweenVectors,\n calcVectorRotation,\n crossProduct,\n getOrthonormalVector,\n getUnitVector,\n isBetweenVectors,\n magnitude,\n rotateVector,\n} from '../vectors';\nimport { StrokeProjectionsBase } from './StrokeProjectionsBase';\nimport type { TProjection, TProjectStrokeOnPointsOptions } from './types';\n\nconst zeroVector = new Point();\n\n/**\n * class in charge of finding projections for each type of line join\n * @see {@link [Closed path projections at #8344](https://github.com/fabricjs/fabric.js/pull/8344#2-closed-path)}\n *\n * - MDN:\n * - https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin\n * - https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-linejoin\n * - Spec: https://svgwg.org/svg2-draft/painting.html#StrokeLinejoinProperty\n * - Playground to understand how the line joins works: https://hypertolosana.github.io/efficient-webgl-stroking/index.html\n * - View the calculated projections for each of the control points: https://codesandbox.io/s/project-stroke-points-with-context-to-trace-b8jc4j?file=/src/index.js\n *\n */\nexport class StrokeLineJoinProjections extends StrokeProjectionsBase {\n /**\n * The point being projected (the angle ∠BAC)\n */\n declare A: Point;\n /**\n * The point before A\n */\n declare B: Point;\n /**\n * The point after A\n */\n declare C: Point;\n /**\n * The AB vector\n */\n AB: Point;\n /**\n * The AC vector\n */\n AC: Point;\n /**\n * The angle of A (∠BAC)\n */\n alpha: TRadian;\n /**\n * The bisector of A (∠BAC)\n */\n bisector: Point;\n\n static getOrthogonalRotationFactor(vector1: Point, vector2?: Point) {\n const angle = vector2\n ? calcAngleBetweenVectors(vector1, vector2)\n : calcVectorRotation(vector1);\n return Math.abs(angle) < halfPI ? -1 : 1;\n }\n\n constructor(A: XY, B: XY, C: XY, options: TProjectStrokeOnPointsOptions) {\n super(options);\n this.A = new Point(A);\n this.B = new Point(B);\n this.C = new Point(C);\n this.AB = this.createSideVector(this.A, this.B);\n this.AC = this.createSideVector(this.A, this.C);\n this.alpha = calcAngleBetweenVectors(this.AB, this.AC);\n this.bisector = getUnitVector(\n // if AC is also the zero vector nothing will be projected\n // in that case the next point will handle the projection\n rotateVector(this.AB.eq(zeroVector) ? this.AC : this.AB, this.alpha / 2),\n );\n }\n\n calcOrthogonalProjection(\n from: Point,\n to: Point,\n magnitude: number = this.strokeProjectionMagnitude,\n ) {\n const vector = this.createSideVector(from, to);\n const orthogonalProjection = getOrthonormalVector(vector);\n const correctSide = StrokeLineJoinProjections.getOrthogonalRotationFactor(\n orthogonalProjection,\n this.bisector,\n );\n return this.scaleUnitVector(orthogonalProjection, magnitude * correctSide);\n }\n\n /**\n * BEVEL\n * Calculation: the projection points are formed by the vector orthogonal to the vertex.\n *\n * @see https://github.com/fabricjs/fabric.js/pull/8344#2-2-bevel\n */\n projectBevel() {\n const projections: Point[] = [];\n // if `alpha` equals 0 or 2*PI, the projections are the same for `B` and `C`\n (this.alpha % twoMathPi === 0 ? [this.B] : [this.B, this.C]).forEach(\n (to) => {\n projections.push(this.projectOrthogonally(this.A, to));\n projections.push(\n this.projectOrthogonally(this.A, to, -this.strokeProjectionMagnitude),\n );\n },\n );\n return projections;\n }\n\n /**\n * MITER\n * Calculation: the corner is formed by extending the outer edges of the stroke\n * at the tangents of the path segments until they intersect.\n *\n * @see https://github.com/fabricjs/fabric.js/pull/8344#2-1-miter\n */\n projectMiter() {\n const projections: Point[] = [],\n alpha = Math.abs(this.alpha),\n hypotUnitScalar = 1 / Math.sin(alpha / 2),\n miterVector = this.scaleUnitVector(\n this.bisector,\n -this.strokeProjectionMagnitude * hypotUnitScalar,\n );\n\n // When two line segments meet at a sharp angle, it is possible for the join to extend,\n // far beyond the thickness of the line stroking the path. The stroke-miterlimit imposes\n // a limit on the extent of the line join.\n // MDN: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-miterlimit\n // When the stroke is uniform, scaling changes the arrangement of points, this changes the miter-limit\n const strokeMiterLimit = this.options.strokeUniform\n ? magnitude(\n this.scaleUnitVector(this.bisector, this.options.strokeMiterLimit),\n )\n : this.options.strokeMiterLimit;\n\n if (\n magnitude(miterVector) / this.strokeProjectionMagnitude <=\n strokeMiterLimit\n ) {\n projections.push(this.applySkew(this.A.add(miterVector)));\n }\n /* when the miter-limit is reached, the stroke line join becomes of type bevel.\n We always need two orthogonal projections which are basically bevel-type projections,\n so regardless of whether the miter-limit was reached or not, we include these projections.\n */\n projections.push(...this.projectBevel());\n\n return projections;\n }\n\n /**\n * ROUND (without skew)\n * Calculation: the projections are the two vectors parallel to X and Y axes\n *\n * @see https://github.com/fabricjs/fabric.js/pull/8344#2-3-1-round-without-skew\n */\n private projectRoundNoSkew(startCircle: Point, endCircle: Point) {\n const projections: Point[] = [],\n // correctSide is used to only consider projecting for the outer side\n correctSide = new Point(\n StrokeLineJoinProjections.getOrthogonalRotationFactor(this.bisector),\n StrokeLineJoinProjections.getOrthogonalRotationFactor(\n new Point(this.bisector.y, this.bisector.x),\n ),\n ),\n radiusOnAxisX = new Point(1, 0)\n .scalarMultiply(this.strokeProjectionMagnitude)\n .multiply(this.strokeUniformScalar)\n .multiply(correctSide),\n radiusOnAxisY = new Point(0, 1)\n .scalarMultiply(this.strokeProjectionMagnitude)\n .multiply(this.strokeUniformScalar)\n .multiply(correctSide);\n\n [radiusOnAxisX, radiusOnAxisY].forEach((vector) => {\n if (isBetweenVectors(vector, startCircle, endCircle)) {\n projections.push(this.A.add(vector));\n }\n });\n return projections;\n }\n\n /**\n * ROUND (with skew)\n * Calculation: the projections are the points furthest from the vertex in\n * the direction of the X and Y axes after distortion.\n *\n * @see https://github.com/fabricjs/fabric.js/pull/8344#2-3-2-round-skew\n */\n private projectRoundWithSkew(startCircle: Point, endCircle: Point) {\n const projections: Point[] = [];\n\n const { skewX, skewY, scaleX, scaleY, strokeUniform } = this.options,\n shearing = new Point(\n Math.tan(degreesToRadians(skewX)),\n Math.tan(degreesToRadians(skewY)),\n );\n // The points furthest from the vertex in the direction of the X and Y axes after distortion\n const circleRadius = this.strokeProjectionMagnitude,\n newY = strokeUniform\n ? circleRadius /\n scaleY /\n Math.sqrt(1 / scaleY ** 2 + (1 / scaleX ** 2) * shearing.y ** 2)\n : circleRadius / Math.sqrt(1 + shearing.y ** 2),\n furthestY = new Point(\n // Safe guard due to floating point precision. In some situations the square root\n // was returning NaN because of a negative number close to zero.\n Math.sqrt(Math.max(circleRadius ** 2 - newY ** 2, 0)),\n newY,\n ),\n newX = strokeUniform\n ? circleRadius /\n Math.sqrt(\n 1 +\n (shearing.x ** 2 * (1 / scaleY) ** 2) /\n (1 / scaleX + (1 / scaleX) * shearing.x * shearing.y) ** 2,\n )\n : circleRadius /\n Math.sqrt(1 + shearing.x ** 2 / (1 + shearing.x * shearing.y) ** 2),\n furthestX = new Point(\n newX,\n Math.sqrt(Math.max(circleRadius ** 2 - newX ** 2, 0)),\n );\n\n [\n furthestX,\n furthestX.scalarMultiply(-1),\n furthestY,\n furthestY.scalarMultiply(-1),\n ]\n // We need to skew the vector here as this information is used to check if\n // it is between the start and end of the circle segment\n .map((vector) =>\n this.applySkew(\n strokeUniform ? vector.multiply(this.strokeUniformScalar) : vector,\n ),\n )\n .forEach((vector) => {\n if (isBetweenVectors(vector, startCircle, endCircle)) {\n projections.push(this.applySkew(this.A).add(vector));\n }\n });\n\n return projections;\n }\n\n projectRound() {\n const projections: Point[] = [];\n /* Include the start and end points of the circle segment, so that only\n the projections contained within it are included */\n // add the orthogonal projections (start and end points of circle segment)\n projections.push(...this.projectBevel());\n // let's determines which one of the orthogonal projection is the beginning and end of the circle segment.\n // when `alpha` equals 0 or 2*PI, we have a straight line, so the way to find the start/end is different.\n const isStraightLine = this.alpha % twoMathPi === 0,\n // change the origin of the projections to point A\n // so that the cross product calculation is correct\n newOrigin = this.applySkew(this.A),\n proj0 = projections[isStraightLine ? 0 : 2].subtract(newOrigin),\n proj1 = projections[isStraightLine ? 1 : 0].subtract(newOrigin),\n // when `isStraightLine` === true, we compare with the vector opposite AB, otherwise we compare with the bisector.\n comparisonVector = isStraightLine\n ? this.applySkew(this.AB.scalarMultiply(-1))\n : this.applySkew(\n this.bisector.multiply(this.strokeUniformScalar).scalarMultiply(-1),\n ),\n // the beginning of the circle segment is always to the right of the comparison vector (cross product > 0)\n isProj0Start = crossProduct(proj0, comparisonVector) > 0,\n startCircle = isProj0Start ? proj0 : proj1,\n endCircle = isProj0Start ? proj1 : proj0;\n if (!this.isSkewed()) {\n projections.push(...this.projectRoundNoSkew(startCircle, endCircle));\n } else {\n projections.push(...this.projectRoundWithSkew(startCircle, endCircle));\n }\n return projections;\n }\n\n /**\n * Project stroke width on points returning projections for each point as follows:\n * - `miter`: 1 point corresponding to the outer boundary. If the miter limit is exceeded, it will be 2 points (becomes bevel)\n * - `bevel`: 2 points corresponding to the bevel possible boundaries, orthogonal to the stroke.\n * - `round`: same as `bevel` when it has no skew, with skew are 4 points.\n */\n protected projectPoints() {\n switch (this.options.strokeLineJoin) {\n case 'miter':\n return this.projectMiter();\n case 'round':\n return this.projectRound();\n default:\n return this.projectBevel();\n }\n }\n\n public project(): TProjection[] {\n return this.projectPoints().map((point) => ({\n originPoint: this.A,\n projectedPoint: point,\n angle: this.alpha,\n bisector: this.bisector,\n }));\n }\n}\n"],"mappings":"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"}