svg-kit
Version:
A SVG toolkit, with its own Vector Graphics structure, multiple renderers (svg text, DOM svg, canvas), and featuring Flowing Text.
331 lines (259 loc) • 9.8 kB
JavaScript
/*
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" ;
/*
Mostly derived from svg-path-properties by RogerVecianaAbzu:
https://github.com/rveciana/svg-path-properties/blob/master/src/bezier.ts
*/
const BoundingBox = require( '../BoundingBox.js' ) ;
function Arc( startPoint , radius , xAxisRotate , largeArcFlag , sweepFlag , endPoint ) {
this.startPoint = startPoint ;
this.radius = radius ;
this.xAxisRotate = xAxisRotate ;
this.largeArcFlag = largeArcFlag ;
this.sweepFlag = sweepFlag ;
this.endPoint = endPoint ;
this.boundingBox = new BoundingBox( null ) ;
this.length = this.getLength( true ) ; // <-- this will update the BBox
}
module.exports = Arc ;
Arc.prototype.getLength = function( updateBBox ) {
if ( updateBBox ) { this.boundingBox.nullify() ; }
const lengthProperties = this.approximateArcLengthOfCurve(
300 ,
t => this.pointOnEllipticalArc(
this.startPoint ,
this.radius ,
this.xAxisRotate ,
this.largeArcFlag ,
this.sweepFlag ,
this.endPoint ,
t
) ,
updateBBox ? this.boundingBox : null
) ;
return lengthProperties.arcLength ;
} ;
Arc.prototype.getPointAtLength = function( length ) {
if ( length === 0 ) { return { x: this.startPoint.x , y: this.startPoint.y } ; }
if ( length === this.length ) { return { x: this.endPoint.x , y: this.endPoint.y } ; }
const position = this.pointOnEllipticalArc(
this.startPoint ,
this.radius ,
this.xAxisRotate ,
this.largeArcFlag ,
this.sweepFlag ,
this.endPoint ,
length / this.length
) ;
return { x: position.x , y: position.y } ;
} ;
Arc.prototype.getTangentAtLength = function( length ) {
const delta = 0.001 * ( Math.min( this.radius.x , this.radius.y ) || this.length ) ; // should manage degenerate case
const p1 = this.getPointAtLength( length - delta ) ;
const p2 = this.getPointAtLength( length + delta ) ;
const dx = p2.x - p1.x ;
const dy = p2.y - p1.y ;
const dist = Math.sqrt( dx * dx + dy * dy ) ;
return {
x: dx / dist ,
y: dy / dist ,
angle: Math.atan2( dy , dx )
} ;
} ;
// Position + Tangent + Angle (radians)
Arc.prototype.getPropertiesAtLength = function( length ) {
const point = this.getPointAtLength( length ) ;
const tangent = this.getTangentAtLength( length ) ;
return {
x: point.x ,
y: point.y ,
dx: tangent.x ,
dy: tangent.y ,
angle: tangent.angle
} ;
} ;
Arc.pointOnEllipticalArc =
Arc.prototype.pointOnEllipticalArc = function( p0 , radius , xAxisRotation , largeArcFlag , sweepFlag , p1 , t ) {
// In accordance to: http://www.w3.org/TR/SVG/implnote.html#ArcOutOfRangeParameters
let rx = Math.abs( radius.x ) ,
ry = Math.abs( radius.y ) ,
xAxisRotationRadians = toRadians( mod( xAxisRotation , 360 ) ) ;
// If the endpoints are identical, then this is equivalent to omitting the elliptical arc segment entirely.
if ( p0.x === p1.x && p0.y === p1.y ) {
return { x: p0.x , y: p0.y , ellipticalArcAngle: 0 } ; // Check if angle is correct
}
// If rx = 0 or ry = 0 then this arc is treated as a straight line segment joining the endpoints.
if ( rx === 0 || ry === 0 ) {
//return this.pointOnLine(p0, p1, t);
return { x: 0 , y: 0 , ellipticalArcAngle: 0 } ; // Check if angle is correct
}
// Following "Conversion from endpoint to center parameterization"
// http://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter
// Step #1: Compute transformedPoint
const dx = ( p0.x - p1.x ) / 2 ;
const dy = ( p0.y - p1.y ) / 2 ;
const transformedPoint = {
x: Math.cos( xAxisRotationRadians ) * dx + Math.sin( xAxisRotationRadians ) * dy ,
y: - Math.sin( xAxisRotationRadians ) * dx + Math.cos( xAxisRotationRadians ) * dy
} ;
// Ensure radii are large enough
const radiiCheck =
Math.pow( transformedPoint.x , 2 ) / Math.pow( rx , 2 ) +
Math.pow( transformedPoint.y , 2 ) / Math.pow( ry , 2 ) ;
if ( radiiCheck > 1 ) {
rx = Math.sqrt( radiiCheck ) * rx ;
ry = Math.sqrt( radiiCheck ) * ry ;
}
// Step #2: Compute transformedCenter
const cSquareNumerator =
Math.pow( rx , 2 ) * Math.pow( ry , 2 ) -
Math.pow( rx , 2 ) * Math.pow( transformedPoint.y , 2 ) -
Math.pow( ry , 2 ) * Math.pow( transformedPoint.x , 2 ) ;
const cSquareRootDenom =
Math.pow( rx , 2 ) * Math.pow( transformedPoint.y , 2 ) +
Math.pow( ry , 2 ) * Math.pow( transformedPoint.x , 2 ) ;
let cRadicand = cSquareNumerator / cSquareRootDenom ;
// Make sure this never drops below zero because of precision
cRadicand = cRadicand < 0 ? 0 : cRadicand ;
const cCoef = ( largeArcFlag !== sweepFlag ? 1 : - 1 ) * Math.sqrt( cRadicand ) ;
const transformedCenter = {
x: cCoef * ( ( rx * transformedPoint.y ) / ry ) ,
y: cCoef * ( - ( ry * transformedPoint.x ) / rx )
} ;
// Step #3: Compute center
const center = {
x:
Math.cos( xAxisRotationRadians ) * transformedCenter.x -
Math.sin( xAxisRotationRadians ) * transformedCenter.y +
( p0.x + p1.x ) / 2 ,
y:
Math.sin( xAxisRotationRadians ) * transformedCenter.x +
Math.cos( xAxisRotationRadians ) * transformedCenter.y +
( p0.y + p1.y ) / 2
} ;
// Step #4: Compute start/sweep angles
// Start angle of the elliptical arc prior to the stretch and rotate operations.
// Difference between the start and end angles
const startVector = {
x: ( transformedPoint.x - transformedCenter.x ) / rx ,
y: ( transformedPoint.y - transformedCenter.y ) / ry
} ;
const startAngle = angleBetween(
{
x: 1 ,
y: 0
} ,
startVector
) ;
const endVector = {
x: ( - transformedPoint.x - transformedCenter.x ) / rx ,
y: ( - transformedPoint.y - transformedCenter.y ) / ry
} ;
let sweepAngle = angleBetween( startVector , endVector ) ;
if ( ! sweepFlag && sweepAngle > 0 ) {
sweepAngle -= 2 * Math.PI ;
}
else if ( sweepFlag && sweepAngle < 0 ) {
sweepAngle += 2 * Math.PI ;
}
// We use % instead of `mod(..)` because we want it to be -360deg to 360deg(but actually in radians)
sweepAngle %= 2 * Math.PI ;
// From http://www.w3.org/TR/SVG/implnote.html#ArcParameterizationAlternatives
const angle = startAngle + sweepAngle * t ;
const ellipseComponentX = rx * Math.cos( angle ) ;
const ellipseComponentY = ry * Math.sin( angle ) ;
const point = {
x:
Math.cos( xAxisRotationRadians ) * ellipseComponentX -
Math.sin( xAxisRotationRadians ) * ellipseComponentY +
center.x ,
y:
Math.sin( xAxisRotationRadians ) * ellipseComponentX +
Math.cos( xAxisRotationRadians ) * ellipseComponentY +
center.y ,
ellipticalArcStartAngle: startAngle ,
ellipticalArcEndAngle: startAngle + sweepAngle ,
ellipticalArcAngle: angle ,
ellipticalArcCenter: center ,
resultantRx: rx ,
resultantRy: ry
} ;
return point ;
} ;
Arc.approximateArcLengthOfCurve =
Arc.prototype.approximateArcLengthOfCurve = function( resolution , pointOnCurveFunc , boundingBox = null ) {
// Resolution is the number of segments we use
resolution = resolution ? resolution : 500 ;
let resultantArcLength = 0 ;
const arcLengthMap = [] ;
const approximationLines = [] ;
let nextPoint ,
prevPoint = pointOnCurveFunc( 0 ) ;
if ( boundingBox ) { boundingBox.ensurePoint( prevPoint ) ; }
for ( let i = 0 ; i < resolution ; i ++ ) {
const t = clamp( i * ( 1 / resolution ) , 0 , 1 ) ;
nextPoint = pointOnCurveFunc( t ) ;
if ( boundingBox ) { boundingBox.ensurePoint( nextPoint ) ; }
resultantArcLength += distance( prevPoint , nextPoint ) ;
approximationLines.push( [ prevPoint , nextPoint ] ) ;
arcLengthMap.push( {
t: t ,
arcLength: resultantArcLength
} ) ;
prevPoint = nextPoint ;
}
// Last stretch to the endpoint
nextPoint = pointOnCurveFunc( 1 ) ;
if ( boundingBox ) { boundingBox.ensurePoint( nextPoint ) ; }
approximationLines.push( [ prevPoint , nextPoint ] ) ;
resultantArcLength += distance( prevPoint , nextPoint ) ;
arcLengthMap.push( {
t: 1 ,
arcLength: resultantArcLength
} ) ;
return {
arcLength: resultantArcLength ,
arcLengthMap: arcLengthMap ,
approximationLines: approximationLines
} ;
} ;
const mod = ( x , m ) => {
return ( ( x % m ) + m ) % m ;
} ;
const toRadians = ( angle ) => {
return angle * ( Math.PI / 180 ) ;
} ;
const distance = ( p0 , p1 ) => {
return Math.sqrt( Math.pow( p1.x - p0.x , 2 ) + Math.pow( p1.y - p0.y , 2 ) ) ;
} ;
const clamp = ( val , min , max ) => {
return Math.min( Math.max( val , min ) , max ) ;
} ;
const angleBetween = ( v0 , v1 ) => {
const p = v0.x * v1.x + v0.y * v1.y ;
const n = Math.sqrt(
( Math.pow( v0.x , 2 ) + Math.pow( v0.y , 2 ) ) * ( Math.pow( v1.x , 2 ) + Math.pow( v1.y , 2 ) )
) ;
const sign = v0.x * v1.y - v0.y * v1.x < 0 ? - 1 : 1 ;
const angle = sign * Math.acos( p / n ) ;
return angle ;
} ;