UNPKG

svg-kit

Version:

A SVG toolkit, with its own Vector Graphics structure, multiple renderers (svg text, DOM svg, canvas), and featuring Flowing Text.

1,210 lines (1,000 loc) 33.6 kB
/* SVG Kit Copyright (c) 2017 - 2024 Cédric Ronvel The MIT License (MIT) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ "use strict" ; const Move = require( './Move.js' ) ; const Line = require( './Line.js' ) ; const CubicBezier = require( './CubicBezier.js' ) ; const QuadraticBezier = require( './QuadraticBezier.js' ) ; const Arc = require( './Arc.js' ) ; const BoundingBox = require( '../BoundingBox.js' ) ; const degToRad = deg => deg * Math.PI / 180 ; const radToDeg = rad => rad * 180 / Math.PI ; const radToRoundedDeg = rad => Math.round( rad * 180 / Math.PI ) ; function Path( commands , invertY = false ) { this.commands = [] ; this.invertY = invertY ; // Computed this.computeCurvesBuild = null ; this.curves = [] ; this.totalLength = 0 ; this.polygonHull = null ; this.polylineHull = null ; this.boundingBox = new BoundingBox( null ) ; if ( commands ) { this.set( commands ) ; } } module.exports = Path ; Path.prototype.__prototypeUID__ = 'svg-kit/Path' ; Path.prototype.__prototypeVersion__ = require( '../../package.json' ).version ; // Circular... const Polygon = require( '../Polygon.js' ) ; const Polyline = require( '../Polyline.js' ) ; Path.prototype.set = function( commands ) { if ( ! Array.isArray( commands ) ) { return ; } this.boundingBox.nullify() ; this.polygonHull = this.polylineHull = null ; for ( let command of commands ) { if ( Path.commands[ command.type ].add ) { Path.commands[ command.type ].add( this.commands , command ) ; } } } ; Path.prototype.export = function( data = {} ) { if ( this.commands.length ) { data.commands = this.commands.map( command => Object.assign( {} , command ) ) ; } return data ; } ; // Changing invertY parameter can change commands based on chirality (turtle-based commands) Path.prototype.setInvertY = function( invertY ) { invertY = !! invertY ; if ( this.invertY === invertY ) { return ; } this.invertY = invertY ; this.clearComputed() ; this.computeCurves() ; } ; Path.prototype.clearComputed = function() { this.computeCurvesBuild = null ; this.curves.length = 0 ; this.totalLength = 0 ; this.polygonHull = this.polylineHull = null ; this.boundingBox.nullify() ; } ; /* This method is used to build the SVG 'd' attribute from the SVG Kit's command format. */ Path.prototype.toD = function() { return Path.commandsToD( this.commands , this.invertY ) ; } ; // TODO - for an existing parser, see here: // https://github.com/fontello/svgpath/blob/master/lib/path_parse.js Path.parse = function() {} ; Path.commandsToD = ( commands , invertY = false ) => { var build = { invertY , d: '' , pu: false , // Pen Up, when true, turtle-like commands move without tracing anything cx: 0 , // cursor position x cy: 0 , // cursor position y ca: invertY ? - Math.PI / 2 : Math.PI / 2 // cursor angle, default to positive Y-axis } ; commands.forEach( ( command , index ) => { if ( ! Path.commands[ command.type ].toD ) { return ; } if ( index ) { build.d += ' ' ; } Path.commands[ command.type ].toD( command , build ) ; } ) ; return build.d ; } ; Path.prototype.computeCurves = function() { if ( this.computeCurvesBuild && this.computeCurvesBuild.next >= this.commands.length ) { return ; } if ( ! this.computeCurvesBuild ) { this.computeCurvesBuild = { next: 0 , invertY: this.invertY , lastCurve: null , openPoint: { x: 0 , y: 0 } , // The point the .close() command close to pu: false , // Pen Up, when true, turtle-like commands move without tracing anything ca: this.invertY ? - Math.PI / 2 : Math.PI / 2 // cursor angle, default to positive Y-axis } ; } for ( ; this.computeCurvesBuild.next < this.commands.length ; this.computeCurvesBuild.next ++ ) { let command = this.commands[ this.computeCurvesBuild.next ] ; if ( Path.commands[ command.type ].toCurve ) { let curve = Path.commands[ command.type ].toCurve( command , this.computeCurvesBuild ) ; if ( curve ) { if ( curve instanceof Move ) { this.computeCurvesBuild.openPoint = { x: curve.endPoint.x , y: curve.endPoint.y } ; } else { this.totalLength += curve.length ; this.boundingBox.merge( curve.boundingBox ) ; } this.computeCurvesBuild.lastCurve = curve ; this.curves.push( curve ) ; } } } } ; Path.prototype.getLength = function() { this.computeCurves() ; return this.totalLength ; } ; Path.prototype.getPointAtLength = function( length , extraData = false ) { this.computeCurves() ; if ( length < 0 ) { length = 0 ; } else if ( length > this.totalLength ) { length = this.totalLength ; } var lastCurve = null , remainingLength = length ; for ( let curve of this.curves ) { if ( curve ) { if ( ! ( curve instanceof Move ) ) { if ( remainingLength <= curve.length ) { return extraData ? curve.getPropertiesAtLength( remainingLength ) : curve.getPointAtLength( remainingLength ) ; } remainingLength -= curve.length ; } lastCurve = curve ; } } // Nothing found? Return the endPoint of the last curve if ( ! lastCurve ) { return extraData ? { x: 0 , y: 0 , dx: 0 , dy: 0 } : { x: 0 , y: 0 } ; // eslint-disable-line object-curly-newline } if ( lastCurve instanceof Move ) { return extraData ? { x: lastCurve.endPoint.x , y: lastCurve.endPoint.y , dx: 0 , dy: 0 } : { x: lastCurve.endPoint.x , y: lastCurve.endPoint.y } ; // eslint-disable-line object-curly-newline } // Due to floating point error, it is possible that length = totalLength overflow // This will prevent this return extraData ? lastCurve.getPropertiesAtLength( lastCurve.length ) : lastCurve.getPointAtLength( lastCurve.length ) ; } ; const isPointEqual = ( point1 , point2 ) => point1.x === point2.x && point1.y === point2.y ; // Split on Move curve, grouping by shapes (closed or not) // noOpenShape: remove group that DOES NOT close to itself (that are not polygons) // noClosedShape: remove group that DOES close to itself (that are not polylines) Path.prototype.groupCurvesByShape = function( noOpenShape = false , noClosedShape = false ) { var group = [] , groupList = [] , closed ; for ( let curve of this.curves ) { if ( curve instanceof Move ) { if ( group.length ) { closed = isPointEqual( group[ 0 ].startPoint , group[ group.length - 1 ].endPoint ) ; if ( closed ? ! noClosedShape : ! noOpenShape ) { groupList.push( group ) ; } group = [] ; } } else { group.push( curve ) ; } } closed = isPointEqual( group[ 0 ].startPoint , group[ group.length - 1 ].endPoint ) ; if ( group.length && ( closed ? ! noClosedShape : ! noOpenShape ) ) { groupList.push( group ) ; } return groupList ; } ; /* Options: step: get a point every step value length forceKeyPoints: boolean (default: false), if true: always add points at the begining and at the end of a path's part (aka 'cusp') angleThreshold: if set, only add the point if its tangent angle has moved more than this threshold (in radian) angleThresholdDeg: the same than angleThreshold, but angle is in degree groupShape: boolean (default: false), if true: points are grouped by shape, if false: the produced array is flatten noOpenShape: internal, used by .toPolygon(), remove points of open shape noClosedShape: internal, used by .toPolyline(), remove points of closed shaped */ Path.prototype.getPoints = function( options = {} ) { var step = options.step || 1 , forceKeyPoints = !! options.forceKeyPoints , angleThreshold = options.angleThresholdDeg !== undefined ? degToRad( + options.angleThresholdDeg || 0 ) : + options.angleThreshold || 0 , simplifyLines = !! angleThreshold , pointsGroups = [] ; this.computeCurves() ; var length = 0 , lengthRemainder = 0 ; for ( let group of this.groupCurvesByShape( options.noOpenShape , options.noClosedShape ) ) { let data = this.getPointsOfShape( group , step , forceKeyPoints , length , lengthRemainder , simplifyLines ) ; pointsGroups.push( data.points ) ; length = data.endingLength ; lengthRemainder = data.lengthRemainder ; } if ( angleThreshold ) { for ( let points of pointsGroups ) { this.simplifyShape( points , angleThreshold ) ; } } console.warn( "getPoints():" , pointsGroups.flat() ) ; return options.groupShape ? pointsGroups : pointsGroups.flat() ; } ; Path.prototype.getPointsOfShape = function( curveList , step , forceKeyPoints , startingLength , lengthRemainder , simplifyLines ) { var pointList = [] , lastPoint = null , lengthUpToLastCurve = startingLength , lastCurveRemainder = lengthRemainder ; for ( let curve of curveList ) { let startPoint = curve.getPropertiesAtLength( 0 ) ; if ( ! lastPoint ) { startPoint.atLength = lengthUpToLastCurve ; startPoint.startShape = true ; if ( forceKeyPoints ) { startPoint.keyPoint = true ; } pointList.push( startPoint ) ; lastPoint = startPoint ; } else { lastPoint.nextAngle = startPoint.angle ; } if ( forceKeyPoints ) { let point , lengthInCurve = step , subdivision = Math.round( curve.length / step ) || 1 , everyCurveLength = curve.length / subdivision ; if ( ! simplifyLines || ! ( curve instanceof Line ) ) { for ( let i = 1 ; i < subdivision ; i ++ , lengthInCurve += everyCurveLength ) { point = curve.getPropertiesAtLength( lengthInCurve ) ; point.atLength = lengthUpToLastCurve + lengthInCurve ; pointList.push( point ) ; lastPoint = point ; } } // Special case for the end of the curve, we want to avoid floating point errors point = curve.getPropertiesAtLength( curve.length ) ; point.atLength = lengthUpToLastCurve + curve.length ; point.keyPoint = true ; pointList.push( point ) ; lastPoint = point ; lengthUpToLastCurve += curve.length ; } else { let lastLengthInCurve , lengthInCurve = step - lastCurveRemainder ; for ( ; lengthInCurve <= curve.length ; lengthInCurve += step ) { let point = curve.getPropertiesAtLength( lengthInCurve ) ; point.atLength = lengthUpToLastCurve + lengthInCurve ; lastLengthInCurve = lengthInCurve ; pointList.push( point ) ; lastPoint = point ; } lengthUpToLastCurve += curve.length ; lastCurveRemainder = curve.length - lastLengthInCurve ; } } lastPoint.endShape = true ; return { points: pointList , endingLength: lengthUpToLastCurve , lengthRemainder: lastCurveRemainder } ; } ; function threePointsAngle( pointList , previousIndex , index , nextIndex ) { let angle , previousPoint = pointList[ previousIndex ] , point = pointList[ index ] , nextPoint = pointList[ nextIndex ] , previousDirection = Math.atan2( point.x - previousPoint.x , point.y - previousPoint.y ) , nextDirection = Math.atan2( nextPoint.x - point.x , nextPoint.y - point.y ) ; // Absolute value of the angle angle = Math.abs( ( previousDirection - nextDirection ) % ( 2 * Math.PI ) ) ; angle = angle <= Math.PI ? angle : 2 * Math.PI - angle ; return angle ; } Path.prototype.simplifyShape = function( pointList , angleThreshold ) { var simplified = true ; // We need a multi-pass approch here, doing it in one-shot will produce multiple consecutive points stripped // and 2 consecutive points non-stripped, and so on... The previous lines eat all the “angle capital”. // The removal is badly distributed. // That's why a single pass should not remove consecutive points. while ( simplified ) { simplified = false ; for ( let index = 1 ; index <= pointList.length - 2 ; index ++ ) { // Do not remove key-points if ( ! pointList[ index ].keyPoint ) { let angle = threePointsAngle( pointList , index - 1 , index , index + 1 ) ; //console.warn( "Angle:" , radToRoundedDeg( angle ) ) ; if ( angle < angleThreshold ) { // Now we will remove this point ONLY if the angle before and after are not let angleBefore = index >= 2 ? threePointsAngle( pointList , index - 2 , index - 1 , index + 1 ) : 0 ; let angleAfter = index <= pointList.length - 3 ? threePointsAngle( pointList , index - 1 , index + 1 , index + 2 ) : 0 ; //console.log( "Angle before/after:" , radToRoundedDeg( angleBefore ) , radToRoundedDeg( angleAfter ) ) ; if ( angleBefore <= angleThreshold && angleAfter <= angleThreshold ) { //console.log( "==> remove it!" ) ; pointList.splice( index , 1 ) ; simplified = true ; //index -- ; // Can't do that, we need a mutli-pass approch } } } } } } ; const DEFAULT_ANGLE_THRESHOLD = degToRad( 15 ) ; // Simplify the path, get a list of Polygon (only close-shapes are returned) Path.prototype.toPolygon = function( options = {} ) { var groups = this.getPoints( { step: options.step || 10 , forceKeyPoints: true , angleThreshold: ( options.angleThresholdDeg ? degToRad( + options.angleThresholdDeg || 0 ) : + options.angleThreshold || 0 ) || DEFAULT_ANGLE_THRESHOLD , noOpenShape: true , groupShape: true } ) ; return groups.map( points => new Polygon( { points } ) ) ; } ; // Simplify the path, get a list of Polyline (only open-shapes are returned) Path.prototype.toPolyline = function( options = {} ) { var groups = this.getPoints( { step: options.step || 10 , forceKeyPoints: true , angleThreshold: ( options.angleThresholdDeg ? degToRad( + options.angleThresholdDeg || 0 ) : + options.angleThreshold || 0 ) || DEFAULT_ANGLE_THRESHOLD , noClosedShape: true , groupShape: true } ) ; return groups.map( points => new Polyline( { points } ) ) ; } ; Path.prototype.computeHull = function() { if ( ! this.polygonHull || ! this.polylineHull ) { this.polygonHull = this.toPolygon() ; this.polylineHull = this.toPolyline() ; } /* this.boundingBox.nullify() ; this.polygonHull.forEach( polygon => this.boundingBox.merge( polygon.boundingBox ) ) ; this.polylineHull.forEach( polyline => this.boundingBox.merge( polyline.boundingBox ) ) ; */ } ; Path.prototype.isInside = function( coords ) { // First, check the BBox, maybe we don't have perform the more expensive polygon hull creation and test (lot of line-segments)... if ( ! this.boundingBox.isInside( coords ) ) { return false ; } if ( ! this.polygonHull ) { this.computeHull() ; } return this.polygonHull.some( polygon => polygon.isInside( coords ) ) ; } ; /* Now add path commands. First, true SVG path commands. */ const ORIGIN = { x: 0 , y: 0 } ; //const radToDeg = rad => rad * 180 / Math.PI ; Path.commands = {} ; Path.commands.close = { add: ( commands ) => commands.push( { type: 'close' } ) , toD: ( command , build ) => build.d += 'z' , toCurve: ( command , build ) => { let lastPoint = build.lastCurve ? build.lastCurve.endPoint : ORIGIN ; return new Line( { x: lastPoint.x , y: lastPoint.y } , { x: build.openPoint.x , y: build.openPoint.y } ) ; } } ; Path.commands.move = { add: ( commands , data ) => commands.push( { type: 'move' , rel: true , x: data.x || 0 , y: data.y || 0 } ) , toD: ( command , build ) => { let y = build.invertY ? - command.y : command.y ; if ( command.rel ) { build.d += 'm ' + command.x + ' ' + y ; build.cx += command.x ; build.cy += y ; } else { build.d += 'M ' + command.x + ' ' + y ; build.cx = command.x ; build.cy = y ; } } , toCurve: ( command , build ) => { let lastPoint = build.lastCurve ? build.lastCurve.endPoint : ORIGIN ; let ox = 0 , oy = 0 ; if ( command.rel ) { ox = lastPoint.x ; oy = lastPoint.y ; } return new Move( { x: ox + command.x , y: oy + command.y } ) ; } } ; // Converted to 'move' (rel: false) Path.commands.moveTo = { add: ( commands , data ) => commands.push( { type: 'move' , x: data.x || 0 , y: data.y || 0 } ) } ; Path.commands.line = { add: ( commands , data ) => commands.push( { type: 'line' , rel: true , x: data.x || 0 , y: data.y || 0 } ) , toD: ( command , build ) => { let y = build.invertY ? - command.y : command.y ; if ( command.rel ) { build.d += 'l ' + command.x + ' ' + y ; build.cx += command.x ; build.cy += y ; } else { build.d += 'L ' + command.x + ' ' + y ; build.cx = command.x ; build.cy = y ; } } , toCurve: ( command , build ) => { let lastPoint = build.lastCurve ? build.lastCurve.endPoint : ORIGIN ; let ox = 0 , oy = 0 ; if ( command.rel ) { ox = lastPoint.x ; oy = lastPoint.y ; } return new Line( { x: lastPoint.x , y: lastPoint.y } , { x: ox + command.x , y: oy + command.y } ) ; } } ; // Converted to 'line' (rel: false) Path.commands.lineTo = { add: ( commands , data ) => commands.push( { type: 'line' , x: data.x || 0 , y: data.y || 0 } ) } ; Path.commands.hLine = { add: ( commands , data ) => commands.push( { type: 'hLine' , rel: true , x: typeof data === 'number' ? data || 0 : data.x || 0 } ) , toD: ( command , build ) => { if ( command.rel ) { build.d += 'h ' + command.x ; build.cx += command.x ; } else { build.d += 'H ' + command.x ; build.cx = command.x ; } } , toCurve: ( command , build ) => { let lastPoint = build.lastCurve ? build.lastCurve.endPoint : ORIGIN ; let ox = 0 ; if ( command.rel ) { ox = lastPoint.x ; } return new Line( { x: lastPoint.x , y: lastPoint.y } , { x: ox + command.x , y: lastPoint.y } ) ; } } ; // Converted to 'hLine' (rel: false) Path.commands.hLineTo = { add: ( commands , data ) => commands.push( { type: 'hLine' , x: typeof data === 'number' ? data || 0 : data.x || 0 } ) } ; Path.commands.vLine = { add: ( commands , data ) => commands.push( { type: 'vLine' , rel: true , y: typeof data === 'number' ? data || 0 : data.y || 0 } ) , toD: ( command , build ) => { let y = build.invertY ? - command.y : command.y ; if ( command.rel ) { build.d += 'v ' + y ; build.cy += y ; } else { build.d += 'V ' + y ; build.cy = y ; } } , toCurve: ( command , build ) => { let lastPoint = build.lastCurve ? build.lastCurve.endPoint : ORIGIN ; let oy = 0 ; if ( command.rel ) { oy = lastPoint.y ; } return new Line( { x: lastPoint.x , y: lastPoint.y } , { x: lastPoint.x , y: oy + command.y } ) ; } } ; // Converted to 'vLine' (rel: false) Path.commands.vLineTo = { add: ( commands , data ) => commands.push( { type: 'vLine' , y: typeof data === 'number' ? data || 0 : data.y || 0 } ) } ; Path.commands.curve = { add: ( commands , data ) => commands.push( { type: 'curve' , rel: true , cx1: data.cx1 || 0 , cy1: data.cy1 || 0 , cx2: data.cx2 || 0 , cy2: data.cy2 || 0 , x: data.x || 0 , y: data.y || 0 } ) , toD: ( command , build ) => { let cy1 = build.invertY ? - command.cy1 : command.cy1 , cy2 = build.invertY ? - command.cy2 : command.cy2 , y = build.invertY ? - command.y : command.y ; if ( command.rel ) { build.d += 'c ' + command.cx1 + ' ' + cy1 + ' ' + command.cx2 + ' ' + cy2 + ' ' + command.x + ' ' + y ; build.cx += command.x ; build.cy += y ; } else { build.d += 'C ' + command.cx1 + ' ' + cy1 + ' ' + command.cx2 + ' ' + cy2 + ' ' + command.x + ' ' + y ; build.cx = command.x ; build.cy = y ; } } , toCurve: ( command , build ) => { let lastPoint = build.lastCurve ? build.lastCurve.endPoint : ORIGIN ; let ox = 0 , oy = 0 ; if ( command.rel ) { ox = lastPoint.x ; oy = lastPoint.y ; } return new CubicBezier( { x: lastPoint.x , y: lastPoint.y } , { x: ox + command.cx1 , y: oy + command.cy1 } , { x: ox + command.cx2 , y: oy + command.cy2 } , { x: ox + command.x , y: oy + command.y } ) ; } } ; // Converted to 'curve' Path.commands.curveTo = { add: ( commands , data ) => commands.push( { type: 'curve' , cx1: data.cx1 || 0 , cy1: data.cy1 || 0 , cx2: data.cx2 || 0 , cy2: data.cy2 || 0 , x: data.x || 0 , y: data.y || 0 } ) } ; Path.commands.smoothCurve = { add: ( commands , data ) => commands.push( { type: 'smoothCurve' , rel: true , cx: data.cx || data.cx2 || 0 , cy: data.cy || data.cy2 || 0 , x: data.x || 0 , y: data.y || 0 } ) , toD: ( command , build ) => { let cy = build.invertY ? - command.cy : command.cy , y = build.invertY ? - command.y : command.y ; if ( command.rel ) { build.d += 's ' + command.cx + ' ' + cy + ' ' + command.x + ' ' + y ; build.cx += command.x ; build.cy += y ; } else { build.d += 'S ' + command.cx + ' ' + cy + ' ' + command.x + ' ' + y ; build.cx = command.x ; build.cy = y ; } } , toCurve: ( command , build ) => { let lastPoint = build.lastCurve ? build.lastCurve.endPoint : ORIGIN ; let lastControl = build.lastCurve && ( build.lastCurve instanceof CubicBezier ) ? build.lastCurve.endControl : lastPoint ; let ox = 0 , oy = 0 ; if ( command.rel ) { ox = lastPoint.x ; oy = lastPoint.y ; } return new CubicBezier( { x: lastPoint.x , y: lastPoint.y } , { x: 2 * lastPoint.x - lastControl.x , y: 2 * lastPoint.y - lastControl.y } , { x: ox + command.cx , y: oy + command.cy } , { x: ox + command.x , y: oy + command.y } ) ; } } ; // Converted to 'smoothCurve' Path.commands.smoothCurveTo = { add: ( commands , data ) => commands.push( { type: 'smoothCurve' , cx: data.cx || data.cx2 || 0 , cy: data.cy || data.cy2 || 0 , x: data.x || 0 , y: data.y || 0 } ) } ; // q-curve = Quadratic curve, it uses just one controle point instead of two Path.commands.qCurve = { add: ( commands , data ) => commands.push( { type: 'qCurve' , rel: true , cx: data.cx || data.cx1 || 0 , cy: data.cy || data.cy1 || 0 , x: data.x || 0 , y: data.y || 0 } ) , toD: ( command , build ) => { let cy = build.invertY ? - command.cy : command.cy , y = build.invertY ? - command.y : command.y ; if ( command.rel ) { build.d += 'q ' + command.cx + ' ' + cy + ' ' + command.x + ' ' + y ; build.cx += command.x ; build.cy += y ; } else { build.d += 'Q ' + command.cx + ' ' + cy + ' ' + command.x + ' ' + y ; build.cx = command.x ; build.cy = y ; } } , toCurve: ( command , build ) => { let lastPoint = build.lastCurve ? build.lastCurve.endPoint : ORIGIN ; let ox = 0 , oy = 0 ; if ( command.rel ) { ox = lastPoint.x ; oy = lastPoint.y ; } return new QuadraticBezier( { x: lastPoint.x , y: lastPoint.y } , { x: ox + command.cx , y: oy + command.cy } , { x: ox + command.x , y: oy + command.y } ) ; } } ; // Converted to 'qCurve' Path.commands.qCurveTo = { add: ( commands , data ) => commands.push( { type: 'qCurve' , cx: data.cx || data.cx1 || 0 , cy: data.cy || data.cy1 || 0 , x: data.x || 0 , y: data.y || 0 } ) } ; Path.commands.smoothQCurve = { add: ( commands , data ) => commands.push( { type: 'smoothQCurve' , rel: true , x: data.x || 0 , y: data.y || 0 } ) , toD: ( command , build ) => { let y = build.invertY ? - command.y : command.y ; if ( command.rel ) { build.d += 't ' + command.x + ' ' + y ; build.cx += command.x ; build.cy += y ; } else { build.d += 'T ' + command.x + ' ' + y ; build.cx = command.x ; build.cy = y ; } } , toCurve: ( command , build ) => { let lastPoint = build.lastCurve ? build.lastCurve.endPoint : ORIGIN ; let lastControl = build.lastCurve && ( build.lastCurve instanceof QuadraticBezier ) ? build.lastCurve.control : lastPoint ; let ox = 0 , oy = 0 ; if ( command.rel ) { ox = lastPoint.x ; oy = lastPoint.y ; } return new QuadraticBezier( { x: lastPoint.x , y: lastPoint.y } , { x: 2 * lastPoint.x - lastControl.x , y: 2 * lastPoint.y - lastControl.y } , { x: ox + command.x , y: oy + command.y } ) ; } } ; // Converted to 'smoothQCurve' Path.commands.smoothQCurveTo = { add: ( commands , data ) => commands.push( { type: 'smoothQCurve' , x: data.x || 0 , y: data.y || 0 } ) } ; Path.commands.arc = { add: ( commands , data ) => commands.push( { type: 'arc' , rel: true , rx: data.rx || 0 , ry: data.ry || 0 , ra: data.ra || data.a || 0 , // x-axis rotation la: data.largeArc !== undefined ? !! data.largeArc : data.longArc !== undefined ? !! data.longArc : data.la !== undefined ? !! data.la : false , pr: data.positiveRotation !== undefined ? !! data.positiveRotation : data.sweep !== undefined ? !! data.sweep : // <- this is the SVG term data.pr !== undefined ? !! data.pr : true , x: data.x || 0 , y: data.y || 0 } ) , toD: ( command , build ) => { let ra = build.invertY ? - command.ra : command.ra , pr = build.invertY ? ! command.pr : command.pr , y = build.invertY ? - command.y : command.y ; if ( command.rel ) { build.d += 'a ' + command.rx + ' ' + command.ry + ' ' + ra + ' ' + ( + command.la ) + ' ' + ( + pr ) + ' ' + command.x + ' ' + y ; build.cx += command.x ; build.cy += y ; } else { build.d += 'A ' + command.rx + ' ' + command.ry + ' ' + ra + ' ' + ( + command.la ) + ' ' + ( + pr ) + ' ' + command.x + ' ' + y ; build.cx = command.x ; build.cy = y ; } } , toCurve: ( command , build ) => { let lastPoint = build.lastCurve ? build.lastCurve.endPoint : ORIGIN ; let ox = 0 , oy = 0 ; if ( command.rel ) { ox = lastPoint.x ; oy = lastPoint.y ; } return new Arc( { x: lastPoint.x , y: lastPoint.y } , { x: command.rx , y: command.ry } , command.ra , command.la , command.pr , { x: ox + command.x , y: oy + command.y } ) ; } } ; // Converted to 'arc' Path.commands.arcTo = { add: ( commands , data ) => commands.push( { type: 'arc' , rx: data.rx || 0 , ry: data.ry || 0 , ra: data.ra || data.a || 0 , // x-axis rotation la: data.largeArc !== undefined ? !! data.largeArc : data.longArc !== undefined ? !! data.longArc : data.la !== undefined ? !! data.la : false , pr: data.positiveRotation !== undefined ? !! data.positiveRotation : data.sweep !== undefined ? !! data.sweep : // <- this is the SVG term data.pr !== undefined ? !! data.pr : true , x: data.x || 0 , y: data.y || 0 } ) } ; // Converted to 'arc' // All angles use positive as Y-axis to X-axis (Spellcast usage) Path.commands.negativeArc = { add: ( commands , data ) => commands.push( { type: 'arc' , rel: true , rx: data.rx || 0 , ry: data.ry || 0 , ra: - ( data.ra || data.a || 0 ) , // x-axis rotation la: data.largeArc !== undefined ? !! data.largeArc : data.longArc !== undefined ? !! data.longArc : data.la !== undefined ? !! data.la : false , pr: ! ( data.positiveRotation !== undefined ? !! data.positiveRotation : data.sweep !== undefined ? !! data.sweep : // <- this is the SVG term data.pr !== undefined ? !! data.pr : true ) , x: data.x || 0 , y: data.y || 0 } ) } ; // Converted to 'arc' // All angles use positive as Y-axis to X-axis (Spellcast usage) Path.commands.negativeArcTo = { add: ( commands , data ) => commands.push( { type: 'arc' , rx: data.rx || 0 , ry: data.ry || 0 , ra: - ( data.ra || data.a || 0 ) , // x-axis rotation la: data.largeArc !== undefined ? !! data.largeArc : data.longArc !== undefined ? !! data.longArc : data.la !== undefined ? !! data.la : false , pr: ! ( data.positiveRotation !== undefined ? !! data.positiveRotation : data.sweep !== undefined ? !! data.sweep : // <- this is the SVG term data.pr !== undefined ? !! data.pr : true ) , x: data.x || 0 , y: data.y || 0 } ) } ; /* Turtle-like commands. */ // Not a user command, user should use penUp or penDown Path.commands.pen = { add: ( commands , data ) => commands.push( { type: 'pen' , u: !! data.up } ) , toD: ( command , build ) => { build.pu = command.u ; } , toCurve: ( command , build ) => { build.pu = command.u ; } } ; // Converted to 'pen' Path.commands.penUp = { add: ( commands ) => commands.push( { type: 'pen' , u: true } ) } ; // Converted to 'pen' Path.commands.penDown = { add: ( commands ) => commands.push( { type: 'pen' , u: false } ) } ; Path.commands.forward = { add: ( commands , data ) => commands.push( { type: 'forward' , l: typeof data === 'number' ? data : data.length || data.l || 0 } ) , toD: ( command , build ) => { let dx = command.l * Math.cos( build.ca ) , dy = command.l * Math.sin( build.ca ) ; if ( build.pu ) { build.d += 'm ' + dx + ' ' + dy ; } else { build.d += 'l ' + dx + ' ' + dy ; } build.cx += dx ; build.cy += dy ; } , toCurve: ( command , build ) => { let lastPoint = build.lastCurve ? build.lastCurve.endPoint : ORIGIN ; let dx = command.l * Math.cos( build.ca ) , dy = command.l * Math.sin( build.ca ) ; if ( build.pu ) { return new Move( { x: lastPoint.x + dx , y: lastPoint.y + dy } ) ; } return new Line( { x: lastPoint.x , y: lastPoint.y } , { x: lastPoint.x + dx , y: lastPoint.y + dy } ) ; } } ; // Converted to 'forward' Path.commands.backward = { add: ( commands , data ) => commands.push( { type: 'forward' , l: - ( typeof data === 'number' ? data : data.length || data.l || 0 ) } ) } ; // Turn using positive as X-axis to Y-axis Path.commands.turn = { add: ( commands , data ) => commands.push( { type: 'turn' , rel: true , a: typeof data === 'number' ? data : data.angle || data.a || 0 } ) , toD: ( command , build ) => { let a = build.invertY ? - command.a : command.a ; if ( command.rel ) { build.ca += degToRad( a ) ; } else { build.ca = degToRad( a ) ; } } , toCurve: ( command , build ) => { let a = build.invertY ? - command.a : command.a ; if ( command.rel ) { build.ca += degToRad( a ) ; } else { build.ca = degToRad( a ) ; } } } ; // Converted to 'turn' // Turn from X-axis to Y-axis Path.commands.turnTo = { add: ( commands , data ) => commands.push( { type: 'turn' , a: typeof data === 'number' ? data : data.angle || data.a || 0 } ) } ; // Converted to 'turn' // Turn using positive as Y-axis to X-axis (Spellcast usage) Path.commands.negativeTurn = { add: ( commands , data ) => commands.push( { type: 'turn' , rel: true , a: - ( typeof data === 'number' ? data : data.angle || data.a || 0 ) } ) } ; // Converted to 'turn' // Turn from Y-axis to X-axis (clockwise when Y point upward, the invert of the standard 2D computer graphics) (Spellcast usage) Path.commands.negativeTurnTo = { add: ( commands , data ) => commands.push( { type: 'turn' , a: 90 - ( typeof data === 'number' ? data : data.angle || data.a || 0 ) } ) } ; // A turtle-like way of doing a curve: combine a forward and turn, moving along a circle Path.commands.forwardTurn = { add: ( commands , data ) => commands.push( { type: 'forwardTurn' , l: data.length || data.l || 0 , a: data.angle || data.a || 0 } ) , toD: ( command , build ) => { let a = build.invertY ? - command.a : command.a ; /* We will first transpose to a circle of center 0,0 and we are starting at x=radius,y=0 and moving positively */ let angleRad = degToRad( a ) , angleSign = angleRad >= 0 ? 1 : - 1 , alpha = Math.abs( angleRad ) , radius = command.l / alpha , trX = radius * Math.cos( alpha ) , trY = radius * Math.sin( alpha ) , dist = Math.sqrt( ( radius - trX ) ** 2 + trY ** 2 ) , beta = Math.atan2( radius - trX , trY ) ; // beta is the deviation let dx = dist * Math.cos( build.ca + angleSign * beta ) , dy = dist * Math.sin( build.ca + angleSign * beta ) ; if ( build.pu ) { build.d += 'm ' + dx + ' ' + dy ; } else { build.d += 'a ' + radius + ' ' + radius + ' 0 ' + ( alpha > Math.PI ? 1 : 0 ) + ' ' + ( angleRad >= 0 ? 1 : 0 ) + ' ' + dx + ' ' + dy ; } build.cx += dx ; build.cy += dy ; build.ca += angleRad ; } } ; // Converted to 'forwardTurn' // Turn using positive as Y-axis to X-axis (clockwise when Y point upward, the invert of the standard 2D computer graphics) (Spellcast usage) Path.commands.forwardNegativeTurn = { add: ( commands , data ) => commands.push( { type: 'forwardTurn' , l: data.length || data.l || 0 , a: - ( data.angle || data.a || 0 ) } ) } ; // Create API methods for ( let type in Path.commands ) { if ( Path.commands[ type ].add ) { Path.prototype[ type ] = function( data ) { Path.commands[ type ].add( this.commands , data ) ; this.computeCurves() ; return this ; } ; } } // Utilities // Create lines from an array of points (object with .x and .y) Path.linePointsToD = ( points , invertY = false ) => { let yMul = invertY ? - 1 : 1 , str = 'M' ; points.forEach( point => { str += ' ' + point.x + ',' + ( point.y * yMul ) ; } ) ; return str ; } ; Path.polygonPointsToD = ( points , invertY = false ) => Path.linePointsToD( points , invertY ) + ' z' ;