Skip to content

Commit

Permalink
Merge pull request #36 from pbeshai/interpolate-path-commands
Browse files Browse the repository at this point in the history
Add interpolatePathCommands
  • Loading branch information
pbeshai authored Jul 15, 2020
2 parents cb6fe38 + 8992b10 commit 98121b1
Show file tree
Hide file tree
Showing 7 changed files with 2,254 additions and 3,687 deletions.
61 changes: 58 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@

[![npm version](https://badge.fury.io/js/d3-interpolate-path.svg)](https://badge.fury.io/js/d3-interpolate-path)

d3-interpolate-path is a D3 plugin that adds an [interpolator](https://github.com/d3/d3-interpolate)
optimized for SVG <path> elements.
d3-interpolate-path is a zero-dependency D3 plugin that adds an [interpolator](https://github.com/d3/d3-interpolate)
optimized for SVG <path> elements. It can also work directly with object representations of path commands that can be later interpreted for use with canvas or WebGL.

Note this package no longer has a dependency on d3 or d3-interpolate.

Blog: [Improving D3 Path Animation](https://bocoup.com/weblog/improving-d3-path-animation)

Demo: https://peterbeshai.com/d3-interpolate-path/

![d3-interpolate-path demo](https://peterbeshai.com/d3-interpolate-path/d3-interpolate-path-demo.gif)



## Example Usage

```js
Expand Down Expand Up @@ -69,7 +73,7 @@ If you use NPM, `npm install d3-interpolate-path`. Otherwise, download the [late

<a href="#interpolatePath" name="interpolatePath">#</a> <b>interpolatePath</b>(*a*, *b*, *excludeSegment*)

Returns an interpolator between two path attribute `d` strings *a* and *b*. The interpolator extends *a* and *b* to have the same number of points before using [d3.interpolateString](https://github.com/d3/d3-interpolate#interpolateString) on them.
Returns an interpolator between two path attribute `d` strings *a* and *b*. The interpolator extends *a* and *b* to have the same number of points before applying linear interpolation on the values. It uses De Castlejau's algorithm for handling bezier curves.

```js
var pathInterpolator = interpolatePath('M0,0 L10,10', 'M10,10 L20,20 L30,30')
Expand All @@ -96,3 +100,54 @@ function excludeSegment(a, b) {
return a.x === b.x && a.x === 300; // here 300 is the max X
}
```



<a href="#interpolatePathCommands" name="interpolatePathCommands">#</a> <b>interpolatePathCommands</b>(*aCommands*, *bCommands*, *excludeSegment*)

Returns an interpolator between two paths defined as arrays of command objects *a* and *b*. The interpolator extends *a* and *b* to have the same number of points if they differ. This can be useful if you want to work with paths in other formats besides SVG (e.g. canvas or WebGL).

Command objects take the following form:

```ts
| { type: 'M', x: number, y: number },
| { type: 'L', x, y }
| { type: 'H', x }
| { type: 'V', y }
| { type: 'C', x1, y1, x2, y2, x, y }
| { type: 'S', x2, y2, x, y }
| { type: 'Q', x1, y1, x, y }
| { type: 'T', x, y }
| { type: 'A', rx, ry, xAxisRotation, largeArcFlag, sweepFlag, x, y }
| { type: 'Z' }
```

Example usage:

```js
const a = [
{ type: 'M', x: 0, y: 0 },
{ type: 'L', x: 10, y: 10 },
];
const b = [
{ type: 'M', x: 10, y: 10 },
{ type: 'L', x: 20, y: 20 },
{ type: 'L', x: 200, y: 200 },
];

const interpolator = interpolatePathCommands(a, b);

let result = interpolator(0);
/* => [
{ type: 'M', x: 0, y: 0 },
{ type: 'L', x: 5, y: 5 },
{ type: 'L', x: 10, y: 10 },
] */

result = interpolator(0.5);
/* => [
{ type: 'M', x: 5, y: 5 },
{ type: 'L', x: 12.5, y: 12.5 },
{ type: 'L', x: 105, y: 105 },
] */
```
204 changes: 160 additions & 44 deletions docs/d3-interpolate-path.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,80 @@ function _objectSpread2(target) {
return target;
}

function _unsupportedIterableToArray(o, minLen) {
if (!o) return;
if (typeof o === "string") return _arrayLikeToArray(o, minLen);
var n = Object.prototype.toString.call(o).slice(8, -1);
if (n === "Object" && o.constructor) n = o.constructor.name;
if (n === "Map" || n === "Set") return Array.from(o);
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
}

function _arrayLikeToArray(arr, len) {
if (len == null || len > arr.length) len = arr.length;

for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i];

return arr2;
}

function _createForOfIteratorHelper(o, allowArrayLike) {
var it;

if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) {
if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") {
if (it) o = it;
var i = 0;

var F = function () {};

return {
s: F,
n: function () {
if (i >= o.length) return {
done: true
};
return {
done: false,
value: o[i++]
};
},
e: function (e) {
throw e;
},
f: F
};
}

throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}

var normalCompletion = true,
didErr = false,
err;
return {
s: function () {
it = o[Symbol.iterator]();
},
n: function () {
var step = it.next();
normalCompletion = step.done;
return step;
},
e: function (e) {
didErr = true;
err = e;
},
f: function () {
try {
if (!normalCompletion && it.return != null) it.return();
} finally {
if (didErr) throw err;
}
}
};
}

/**
* de Casteljau's algorithm for drawing and splitting bezier curves.
* Inspired by https://pomax.github.io/bezierinfo/
Expand Down Expand Up @@ -482,24 +556,39 @@ function makeCommands(d) {
* the same number of points. This allows for a smooth transition when they
* have a different number of points.
*
* Ignores the `Z` character in paths unless both A and B end with it.
* Ignores the `Z` command in paths unless both A and B end with it.
*
* @param {String} a The `d` attribute for a path
* @param {String} b The `d` attribute for a path
* This function works directly with arrays of command objects instead of with
* path `d` strings (see interpolatePath for working with `d` strings).
*
* @param {Object[]} aCommandsInput Array of path commands
* @param {Object[]} bCommandsInput Array of path commands
* @param {Function} excludeSegment a function that takes a start command object and
* end command object and returns true if the segment should be excluded from splitting.
* @returns {Function} Interpolation function that maps t ([0, 1]) to a path `d` string.
* @returns {Function} Interpolation function that maps t ([0, 1]) to an array of path commands.
*/


function interpolatePath(a, b, excludeSegment) {
var aCommands = makeCommands(a);
var bCommands = makeCommands(b);
function interpolatePathCommands(aCommandsInput, bCommandsInput, excludeSegment) {
// make a copy so we don't mess with the input arrays
var aCommands = aCommandsInput == null ? [] : aCommandsInput.slice();
var bCommands = bCommandsInput == null ? [] : bCommandsInput.slice(); // both input sets are empty, so we don't interpolate

if (!aCommands.length && !bCommands.length) {
return function nullInterpolator() {
return '';
return [];
};
} // do we add Z during interpolation? yes if either one has it. (we'd expect both to have it or not)


var addZ = (aCommands.length === 0 || aCommands[aCommands.length - 1].type === 'Z') && (bCommands.length === 0 || bCommands[bCommands.length - 1].type === 'Z'); // we temporarily remove Z

if (aCommands.length > 0 && aCommands[aCommands.length - 1].type === 'Z') {
aCommands.pop();
}

if (bCommands.length > 0 && bCommands[bCommands.length - 1].type === 'Z') {
bCommands.pop();
} // if A is empty, treat it as if it used to contain just the first point
// of B. This makes it so the line extends out of from that first point.

Expand Down Expand Up @@ -532,26 +621,33 @@ function interpolatePath(a, b, excludeSegment) {
var interpolatedCommands = aCommands.map(function (aCommand) {
return _objectSpread2({}, aCommand);
});
var addZ = (a == null || a[a.length - 1] === 'Z') && (b == null || b[b.length - 1] === 'Z');
return function pathInterpolator(t) {

if (addZ) {
interpolatedCommands.push({
type: 'Z'
});
}

return function pathCommandInterpolator(t) {
// at 1 return the final value without the extensions used during interpolation
if (t === 1) {
return b == null ? '' : b;
return bCommandsInput == null ? [] : bCommandsInput;
} // interpolate the commands using the mutable interpolated command objs
// we can skip at t=0 since we copied aCommands to begin


if (t > 0) {
for (var i = 0; i < interpolatedCommands.length; ++i) {
if (interpolatedCommands[i].type === 'Z') continue;
var aCommand = aCommands[i];
var bCommand = bCommands[i];
var interpolatedCommand = interpolatedCommands[i];
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;

var _iterator = _createForOfIteratorHelper(typeMap[interpolatedCommand.type]),
_step;

try {
for (var _iterator = typeMap[interpolatedCommand.type][Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var arg = _step.value;
interpolatedCommand[arg] = (1 - t) * aCommand[arg] + t * bCommand[arg]; // do not use floats for flags (#27), round to integer

Expand All @@ -560,46 +656,65 @@ function interpolatePath(a, b, excludeSegment) {
}
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
_iterator.e(err);
} finally {
try {
if (!_iteratorNormalCompletion && _iterator["return"] != null) {
_iterator["return"]();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
_iterator.f();
}
}
} // convert to a string (fastest concat: https://jsperf.com/join-concat/150)
}

return interpolatedCommands;
};
}
/**
* Interpolate from A to B by extending A and B during interpolation to have
* the same number of points. This allows for a smooth transition when they
* have a different number of points.
*
* Ignores the `Z` character in paths unless both A and B end with it.
*
* @param {String} a The `d` attribute for a path
* @param {String} b The `d` attribute for a path
* @param {Function} excludeSegment a function that takes a start command object and
* end command object and returns true if the segment should be excluded from splitting.
* @returns {Function} Interpolation function that maps t ([0, 1]) to a path `d` string.
*/

function interpolatePath(a, b, excludeSegment) {
// note this removes Z
var aCommands = makeCommands(a);
var bCommands = makeCommands(b);

if (!aCommands.length && !bCommands.length) {
return function nullInterpolator() {
return '';
};
}

var addZ = (a == null || a[a.length - 1] === 'Z') && (b == null || b[b.length - 1] === 'Z');
var commandInterpolator = interpolatePathCommands(aCommands, bCommands, excludeSegment);
return function pathStringInterpolator(t) {
// at 1 return the final value without the extensions used during interpolation
if (t === 1) {
return b == null ? '' : b;
}

var interpolatedCommands = commandInterpolator(t); // convert to a string (fastest concat: https://jsperf.com/join-concat/150)

var interpolatedString = '';
var _iteratorNormalCompletion2 = true;
var _didIteratorError2 = false;
var _iteratorError2 = undefined;

var _iterator2 = _createForOfIteratorHelper(interpolatedCommands),
_step2;

try {
for (var _iterator2 = interpolatedCommands[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
var _interpolatedCommand = _step2.value;
interpolatedString += commandToString(_interpolatedCommand);
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
var interpolatedCommand = _step2.value;
interpolatedString += commandToString(interpolatedCommand);
}
} catch (err) {
_didIteratorError2 = true;
_iteratorError2 = err;
_iterator2.e(err);
} finally {
try {
if (!_iteratorNormalCompletion2 && _iterator2["return"] != null) {
_iterator2["return"]();
}
} finally {
if (_didIteratorError2) {
throw _iteratorError2;
}
}
_iterator2.f();
}

if (addZ) {
Expand All @@ -611,6 +726,7 @@ function interpolatePath(a, b, excludeSegment) {
}

exports.interpolatePath = interpolatePath;
exports.interpolatePathCommands = interpolatePathCommands;

Object.defineProperty(exports, '__esModule', { value: true });

Expand Down
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export {
default as interpolatePath,
interpolatePathCommands,
} from './src/interpolatePath';
Loading

0 comments on commit 98121b1

Please sign in to comment.