UNPKG

svg.pathmorphing2.js

Version:

Another plugin for the svg.js library to enable path morphing / animation

170 lines (144 loc) 7.36 kB
var SVG = require('svg.js') , CSP = require('./cubicsuperpath') module.exports = SVG // This method will be removed when the version 2.3.7 of svg.js is released since it will be built-in SVG.extend(SVG.PathArray, { // Test if the passed path array use the same path data commands as this path array equalCommands: function(pathArray) { var i, il, equalCommands pathArray = new SVG.PathArray(pathArray) equalCommands = this.value.length === pathArray.value.length for(i = 0, il = this.value.length; equalCommands && i < il; i++) { equalCommands = this.value[i][0] === pathArray.value[i][0] } return equalCommands } }) // Take two path array that don't have the same commands (which mean that they // cannot be morphed in one another) and return 2 equivalent path array (meaning // that they produce the same shape as the passed path array) that have the // same commands (moveto and curveto) // // Algorithm used: // First, convert every path segment of the two passed paths into equivalent cubic Bezier curves. // Then, calculate the positions relative to the total length of the path of the endpoint of all those cubic Bezier curves. // After that, split the Bezier curves of the source at the positions that the destination have that are not common to the source and vice versa. // Finally, make the source and destination have the same number of subpaths. SVG.utils.makePathsMorphable = function (sourcePathArray, destinationPathArray) { var source, sourcePositions, sourcePositionsToSplitAt , destination, destinationPositions, destinationPositionsToSplitAt , i, il, j, jl , s, d , sourceSubpath, destinationSubpath, lastSegPt // Convert every path segments into equivalent cubic Bezier curves source = CSP.cubicSuperPath(sourcePathArray) destination = CSP.cubicSuperPath(destinationPathArray) // The positions relative to the total length of the path is calculated for the endpoint of all those cubic bezier curves sourcePositions = CSP.positions(source) destinationPositions = CSP.positions(destination) // Find the positions that the destination have that are not in the source and vice versa sourcePositionsToSplitAt = [] destinationPositionsToSplitAt = [] i = 0, il = sourcePositions.length j = 0, jl = destinationPositions.length while(i < il && j < jl) { // Test if the two values are equal taking into account the imprecision of floating point number if (Math.abs(sourcePositions[i] - destinationPositions[j]) < 0.000001) { i++ j++ } else if(sourcePositions[i] > destinationPositions[j]){ sourcePositionsToSplitAt.push(destinationPositions[j++]) } else { destinationPositionsToSplitAt.push(sourcePositions[i++]) } } // If there are still some destination positions left, they all are not in the source and vice versa sourcePositionsToSplitAt = sourcePositionsToSplitAt.concat(destinationPositions.slice(j)) destinationPositionsToSplitAt = destinationPositionsToSplitAt.concat(sourcePositions.slice(i)) // Split the source and the destination at the positions they don't have in common CSP.splitAtPositions(source, sourcePositions, sourcePositionsToSplitAt) CSP.splitAtPositions(destination, destinationPositions, destinationPositionsToSplitAt) // Break paths so that corresponding subpaths have an equal number of segments s = source, source = [], sourceSubpath = s[i = 0] d = destination, destination = [], destinationSubpath = d[j = 0] while (sourceSubpath && destinationSubpath) { // Push REFERENCES to the current subpath arrays in their respective array source.push(sourceSubpath) destination.push(destinationSubpath) il = sourceSubpath.length jl = destinationSubpath.length // If the current subpath of the source and the current subpath of the destination don't // have the same length, that mean that the biggest of the two must be split in two if(il > jl) { lastSegPt = sourceSubpath[jl-1] // Perform the split using splice that change the content of the array by removing elements and returning them in an array sourceSubpath = sourceSubpath.splice(jl) sourceSubpath.unshift(lastSegPt) // The last segment point is duplicated since these two segments must be joined together destinationSubpath = d[++j] // This subpath has been accounted for, past to the next } else if(il < jl) { lastSegPt = destinationSubpath[il-1] destinationSubpath = destinationSubpath.splice(il) destinationSubpath.unshift(lastSegPt) sourceSubpath = s[++i] } else { sourceSubpath = s[++i] destinationSubpath = d[++j] } } // Convert in path array and return return [CSP.uncubicSuperPath(source), CSP.uncubicSuperPath(destination)] } SVG.extend(SVG.PathArray, { // Make path array morphable morph: function(pathArray) { var pathsMorphable this.destination = new SVG.PathArray(pathArray) if(this.equalCommands(this.destination)) { this.sourceMorphable = this this.destinationMorphable = this.destination } else { pathsMorphable = SVG.utils.makePathsMorphable(this.value, this.destination) this.sourceMorphable = pathsMorphable[0] this.destinationMorphable = pathsMorphable[1] } return this } // Get morphed path array at given position , at: function(pos) { // Make sure a destination, a morphable source and a morphable destination are defined // Also, when pos is 0, we don't return sourceMorphable since the "real" path (this) may have // closepath commands which differs in behavior from "manually" closing a path (what sourceMorphable does) // For more details, see: https://www.w3.org/TR/SVG11/paths.html#PathDataClosePathCommand if (pos === 0 || !(this.destination && this.sourceMorphable && this.destinationMorphable)) { return this } else if(pos === 1) { // destination is used here for the same reason stated above return this.destination } else { var sourceArray = this.sourceMorphable.value , destinationArray = this.destinationMorphable.value , array = [], pathArray = new SVG.PathArray() , i, il, j, jl // Animate has specified in the SVG spec // See: https://www.w3.org/TR/SVG11/paths.html#PathElement for (i = 0, il = sourceArray.length; i < il; i++) { array[i] = [sourceArray[i][0]] for(j=1, jl = sourceArray[i].length; j < jl; j++) { array[i][j] = sourceArray[i][j] + (destinationArray[i][j] - sourceArray[i][j]) * pos } // For the two flags of the elliptical arc command, the SVG spec say: // Flags and booleans are interpolated as fractions between zero and one, with any non-zero value considered to be a value of one/true // Elliptical arc command as an array followed by corresponding indexes: // ['A', rx, ry, x-axis-rotation, large-arc-flag, sweep-flag, x, y] // 0 1 2 3 4 5 6 7 if(array[i][0] === 'A') { array[i][4] = +(array[i][4] != 0) array[i][5] = +(array[i][5] != 0) } } // Directly modify the value of a path array, this is done this way for performance pathArray.value = array return pathArray } } })