Giter VIP home page Giter VIP logo

flubber's Introduction

Build Status

flubber

Some best-guess methods for smoothly interpolating between 2-D shapes.

Flubber in action

Why?

Let's say you want to animate between two SVG paths or canvas shapes in a visualization. If you plug in their coordinates or their path strings to something like d3.transition(), it might work if the shapes correspond to each other really well - for example, turning a triangle into a different triangle. But once your shapes don't really correspond, you'll get unpredictable results with weird inversions and sudden jumps.

The goal of this library is to provide a best-guess interpolation for any two arbitrary shapes (or collections of shapes) that results in a reasonably smooth animation, without overthinking it.

Installation

In a browser (exposes the flubber global):

<script src="https://unpkg.com/[email protected]"></script>

With NPM:

npm install flubber

And then import/require it:

var flubber = require("flubber"); // Node classic
import { interpolate } from "flubber" // ES6

How to use

Flubber expects a shape input to be either an SVG path string or an array of [x, y] points (a "ring"):

"M100,100 L200,100 L150,200Z" // A triangle as a path string
[[100, 100], [200, 100], [150, 200]] // A triangle as a ring

Flubber methods return interpolators, functions that you can call later with a value from 0 to 1 to get back the corresponding shape, where 0 is the beginning of the animation and 1 is the end.

Using D3, usage could look something like:

var triangle = [[1, 0], [2, 2], [0, 2]],
    pentagon = [[0, 0], [2, 0], [2, 1], [1, 2], [0, 1]];

var interpolator = flubber.interpolate(triangle, pentagon);

d3.select("path")
    .transition()
    .attrTween("d", function(){ return interpolator; });

Without D3, usage might look something like this:

// Mixing and matching input types is OK
var triangle = "M1,0 L2,2 L0,2 Z",
    pentagon = [[0, 0], [2, 0], [2, 1], [1, 2], [0, 1]];

var interpolator = flubber.interpolate(triangle, pentagon);

requestAnimationFrame(draw);

function draw(time) {
    var t = howFarAlongTheAnimationIsOnAScaleOfZeroToOne(time);
    myPathElement.setAttribute("d", interpolator(t));
    if (t < 1) {
        requestAnimationFrame(draw);
    }
}

Note: it doesn't matter whether your ring has a closing point identical to the first point.

API

flubber.interpolate(fromShape, toShape [, options])

fromShape and toShape should each be a ring or an SVG path string. If your path string includes holes or multiple shapes in a single string, everything but the first outer shape will be ignored.

This returns a function that takes a value t from 0 to 1 and returns the in-between shape:

var interpolator = flubber.interpolate(triangle, octagon);

interpolator(0); // returns an SVG triangle path string
interpolator(0.5); // returns something halfway between the triangle and the octagon
interpolator(1); // returns an SVG octagon path string

options can include the following keys:

string: whether to output results as an SVG path string or an array of points. (default: true)
maxSegmentLength: the lower this number is, the smoother the resulting animation will be, at the expense of performance. Represents a number in pixels (if no transforms are involved). Set it to false or Infinity for no smoothing. (default: 10)

.interpolate() in action with SVG paths as input

.interpolate() in action with GeoJSON coordinates as input

flubber.toCircle(fromShape, x, y, r[, options])

Like interpolate(), but for the specific case of transforming the shape to a circle centered at [x, y] with radius r.

var interpolator = flubber.toCircle(triangle, 100, 100, 10);

interpolator(0); // returns an SVG triangle path string
interpolator(0.5); // returns something halfway between the triangle and the circle
interpolator(1); // returns a circle path string centered at 100, 100 with a radius of 10

.toCircle() in action

flubber.toRect(fromShape, x, y, width, height[, options])

Like interpolate(), but for the specific case of transforming the shape to a rectangle with the upper-left corner [x, y] and the dimensions width x height.

var interpolator = flubber.toRect(triangle, 10, 50, 100, 200);

interpolator(0); // returns an SVG triangle path string
interpolator(0.5); // returns something halfway between the triangle and the rectangle
interpolator(1); // returns a rectangle path string from [10, 50] in the upper left to [110, 250] in the lower right

.toRect() in action

flubber.fromCircle(x, y, r, toShape[, options])

Like toCircle() but reversed.

flubber.fromRect(x, y, width, height, toShape[, options])

Like toRect() but reversed.

flubber.separate(fromShape, toShapeList[, options])

If you're trying to interpolate between a single shape and multiple shapes (for example, a group of three circles turning into a single big circle), this method will break your shapes into pieces so you can animate between the two sets. This isn't terribly performant and has some quirks but it tends to get the job done.

fromShape should be a ring or SVG path string, and toShapeList should be an array of them.

The options are the same as for interpolate(), with the additional option of single, which defaults to false.

If single is false, this returns an array of n interpolator functions, where n is the length of toShapeList. If single is set to true this returns one interpolator that combines things into one giant path string or one big array of rings.

// returns an array of two interpolator functions
var interpolators = flubber.separate(triangle, [square, otherSquare]);

d3.selectAll("path")
    .data(interpolators)
    .transition()
    .attrTween("d", function(interpolator) { return interpolator; });

.separate() in action

// returns a single interpolator function
var combinedInterpolator = flubber.separate(triangle, [square, otherSquare], { single: true });

// This one path element will be two squares at the end
d3.select("path")
    .transition()
    .attrTween("d", function() { return combinedInterpolator; });

.separate({ single: true }) in action

flubber.combine(fromShapeList, toShape[, options])

Like separate() but reversed.

flubber.interpolateAll(fromShapeList, toShapeList[, options])

Like separate() or combine() but instead expects two arrays of shapes the same length (e.g. an array of three triangles turning into an array of three squares). The shapes will be matched up in the order of the arrays (the first fromShapeList item will turn into the first toShapeList item, and so on).

.interpolateAll() in action

.interpolateAll({ single: true }) in action

flubber.toPathString(ring)

A helper function for converting an array of points to an SVG path string.

flubber.toPathString([[1, 1], [2, 1], [1.5, 2]]);
// Returns "M1,1L2,1L1.5,2Z"

flubber.splitPathString(pathString)

A helper function for splitting an SVG path string that might contain multiple shapes into an array of one-shape path strings.

flubber.splitPathString("M1,1 L2,1 L1.5,2Z M3,3 L4,3 L3.5,4 Z");
// Returns ["M1,1 L2,1 L1.5,2Z", "M3,3 L4,3 L3.5,4 Z"]

Examples

Note: most of these demos use D3 to keep the code concise, but this can be used with any library, or with no library at all.

Morphing SVG paths

Morphing GeoJSON coordinates

Morphing to and from circles

Morphing to and from rectangles

Morphing between one shape and multiple shapes (one element)

Morphing between one shape and multiple shapes (multiple elements)

Morphing between two sets of multiple shapes

Vanilla JS + Canvas

Medley of different methods

To do

  • Maintain original vertices when polygonizing a path string with curves
  • Add force: true option to collapse small additional polygons onto the perimeter of the largest
  • Support unclosed lines
  • Use curves between points for fromCircle() and toCircle()
  • Deal with holes?
  • Accept SVG elements as arguments instead of just path strings?
  • Add pre-simplification as an option
  • Simulated annealing or random swapping for multishape matching?

Video

OpenVisConf 2017 talk about shape interpolation

Alternatives

react-svg-morph - utility for morphing between two SVGs in React

GreenSock MorphSVG plugin - GSAP shape morphing utility (costs money, not open source)

d3.geo2rect - a plugin for morphing between GeoJSON and a rectangular SVG grid

d3-interpolate-path - a D3 interpolator to interpolate between two unclosed lines, for things like line chart transitions with mismatched data

Wilderness - an SVG manipulation and animation library

Cirque - JS utility for morphing between circles and polygons

Credits

Many thanks to:

License

MIT License

Copyright (c) 2017 Noah Veltman

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.

flubber's People

Contributors

alexjlockwood avatar veltman avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

flubber's Issues

Jest test fails: r.getTotalLength is not a function

I'm using flubber in a react component using jest to test. I initialize the interpolator statically:

static readonly interpolator = interpolate(fromShape, toShape, {
        maxSegmentLength: 0.1,
});

And call it using react-spring

<Spring
    native={true}
    from={{ t: 0 }}
    to={{
        t:
            this.state.which
                ? 1
                : 0,
    }}
>
    {({ t }) => (
        <animated.path
            d={t.interpolate(
                Class.interpolator,
            )}
        />
    )}
</Spring>

When I run tests the test for this component now fails with this error:

TypeError: r.getTotalLength is not a function
      at R (node_modules/flubber/build/flubber.min.js:1:7808)
      at Q (node_modules/flubber/build/flubber.min.js:1:7390)
      at J (node_modules/flubber/build/flubber.min.js:1:8565)
      at Fn (node_modules/flubber/build/flubber.min.js:2:17798)
      ...

Can be get code from this project?

Hello @veltman, amazing job, seems works fine in most case.

I've made svg-morphing and other tools 2-year ago which based on snapsvg and d3.js (path tween demo), then amazing @colinmeinke made the wilderness.js, i replaced code with wilderness.js and second demo still d3.js and now seem i can change second way with this way, my project was closed-source due of more peoples told like i stole code from gsap morphsvg, and removed from github. name has been unim.js, superanimation.js, sajs, etc...

I will credit project in comments of js, whenever i am use. I have all of code including moveIndex (wilderness.js), just using add and normalise for get better result. Thanks for work.

I'd like to simplify the code as has much unnecessary dependecies that can be with simply code or other small deps, your morphing looks nice and best because there has pieceOrder function that does the job nice and orders everything in right and fast.

Flubber performs differently when loaded via webpack/babel.

I'll preface this with the admission it could be a bug in webpack/transpilation process but I'm not sure.

I was attempting to use flubber in an app created with create-react-app. I noticed some of my animations had ugly transitions when using flubber. I attempted to reproduce using a bare minimum example but was unable. After a bit of banging my head, it seemed flubber didn't work correctly for me when loaded via an es6 import (alongside webpack/babel) but did work when loaded via a normal script tag.

Here's a link to my demo: https://nthitz.github.io/flubbertest/index.html the top (broken) animation is using flubber loaded through es6 imports. The bottom animation looks more what I desire but I'm having to load flubber via a script tag.

I copied the basic demo from here https://veltman.github.io/flubber/demos/basic-svg.html and added my own paths that were causing problems (Rings). My HTML is here https://github.com/nthitz/flubbertest/blob/master/public/index.html and it has two identical SVGs with different #ids.

My JS is here https://github.com/nthitz/flubbertest/blob/master/src/index.js basically I just call the same flubber demo once using the flubber loaded via ES6 and once with the flubber loaded via the script tag. End up with vastly different behavior. This demo doesn't actually use React, just using CRA for the quick webpack/babel setup.

Any idea if this is a flubber issue or something happening with the trasnspilation? Thanks!

Add flubber.align()?

By default, flubber.interpolate() will return the original start and end shapes at t=0 and t=1. It might be worth having a method that returns two corresponding path strings / coordinate rings, in case someone wants to use them to smoothly animate in some other context (like a CSS animation).

// fromStr and toStr are mismatched paths
let [from, to] = flubber.align(fromStr, toStr);

// from and to are now path strings with a matching number of points with good rotation for animating

Also not sure whether align() is the best method name for this.

output precision for path strings need not use 16 digits

Right now Flubber returns massive path strings with lots of tiny straight-line segments whose vertices are specified to full double-precision floating point, converted to decimal for printing, which results in 16-digit decimals. This is unnecessarily precise, since (a) conforming SVG renderers only need to support single precision (no more than 8 digits) and (b) anything more than a few digits is visually indistinguishable.

I'd recommend using no finer than maybe 2 digits beyond the specified maxSegmentLength.

Perf Testing Compared to Default D3 Interpolation

I think it would be helpful if the documentation gave folks some sense of the performance of the interpolations with the segment options so they could plan accordingly. Right now I feel like I'm evaluating on a case-by-case basis which probably doesn't make sense for everyone.

Broken in latest Chromium

It looks like this library is now broken in the latest version of Chromium. Apparently they've conformed to the spec. To repro the issue, simply check out this example in the latest Chromium:

https://veltman.github.io/flubber/demos/basic-svg.html

Uncaught DOMException: Failed to execute 'getPointAtLength' on 'SVGGeometryElement': The element is in an inactive document.

Looks like the source of the problem is that we can no longer call

let p = m.getPointAtLength((len * i) / numPoints);
from the output of here:

flubber/src/svg.js

Lines 94 to 108 in 0cadadf

function measure(d) {
// Use native browser measurement if running in browser
if (typeof window !== "undefined" && window && window.document) {
try {
let path = window.document.createElementNS(
"http://www.w3.org/2000/svg",
"path"
);
path.setAttributeNS(null, "d", d);
return path;
} catch (e) {}
}
// Fall back to svg-path-properties
return svgPathProperties(d);
}

There's a related Chromium issue - https://bugs.chromium.org/p/chromium/issues/detail?id=1108966

Interpolate fills the shape

Hey guys,

i am having the problem, that two shapes that i interpolate will have a phase inbetween where the shape is filled:

Before:
Screenshot 2019-10-13 15 25 09
Inbetween:
Screenshot 2019-10-13 15 25 38
After:
Screenshot 2019-10-13 15 25 15

That's the code:

const paths = {
  circleOutline:
    'M77,5A72,72,0,1,1,5,77,72.08,72.08,0,0,1,77,5m0-5a77,77,0,1,0,77,77A77,77,0,0,0,77,0Z',
  rectangleOutline:
'M102.73,5A46.33,46.33,0,0,1,149,51.27v51.46A46.33,46.33,0,0,1,102.73,149H51.27A46.33,46.33,0,0,1,5,102.73V51.27A46.33,46.33,0,0,1,51.27,5h51.46m0-5H51.27A51.26,51.26,0,0,0,0,51.27v51.46A51.26,51.26,0,0,0,51.27,154h51.46A51.26,51.26,0,0,0,154,102.73V51.27A51.26,51.26,0,0,0,102.73,0Z',
 };

    const interpolator = interpolate(paths.circleOutline, paths.rectangleOutline);
<path d={interpolator(0.7)} />

What am i doing wrong?

Typescript typings

It'd be great to have typings for this library, so it can be used w/ Typescript.

I can have a go at building the typings myself, but no promises on their quality :)

Expose an API that outputs the compatible to/from path strings

LMK if this already exists and I just missed it.

A lot of the time a developer will want to morph two path strings but will find it difficult because the path strings may not be compatible with each other. Flubber solves this problem by returning an interpolator that outputs the path string to use at a given time t, with the original from/to path string being returned at t=0 and t=1.

This library could potentially be even more useful if there was a way to obtain the compatible/morphable path strings to use at t=0 and t=1. The raw path strings could then be used using CSS, for example: https://css-tricks.com/many-tools-shape-morphing/#article-header-id-4

Improving performance: aka To-Do for Performance

Here i wanna to make this project as fast as possible, for creating plug-in for my project.

To-Do's:

  • 1. Converting Curves to Lines, solution here
  • 2. Point-A-Long replacement to direct interpolation (improves by 5-15% performance)
  • 3. distance replacement to direct Math.sqrt (improves 5%)
  • 4. Implementing the change of index
  • 5. Implementing the config for disabling topology/split into parts, as this costs at perf

Total performance improvements: 1 (5-50%) + 2 (5-15%) + 3 (5%) = 15%-70% improvements

And some others, that may i change in future

Using in Gatsby with IE11

Hey! Just wondered what the support out the box for this library is? I'm having some issues on IE11 where the entire site isn't rendering with the following console error

Unexpected call to method or property access

It doesn't give me much more to go off other than mentioning Symbol a few times, and the error disappears if i remove flubber.

Any help would be hugely appreciated.

Thanks!

Question: how does flubber approach morphing paths with different #s of shapes?

This isn't a bug report... more just a question. Hope you don't mind. :)

I'm the author of ShapeShifter, an SVG icon animation app. You can see an example video showing how to animate a play icon into a pause icon here: https://www.youtube.com/watch?v=2aq3ljlnQdI

Shape Shifter provides a feature called "auto-fix" which gives "best guess estimates" for path morphing animations, similar to your library. It uses an application of the Needleman-Wunsch algorithm. Right now I only have it working well with single-shape SVGs though.

Flubber looks like it handles multi-shape path morphs pretty well, so I am kind of curious how you approached creating "best guess" animations for paths with different #s of shapes? Thanks in advance if you have any tips. :)

Porting to other languages

Hi! Great library. I've been using it for years.
Some time ago I needed the same logic in other non-js projects, so I ended up porting it to Python and dart.

Not sure what is your policy regarding ports to other languages, e.g. some projects mention them in their readme. Just wanted to let you know that they exist 🙂

Reanimatedv2

Hello all,

Any chance we can get reanimated v2 support in this great library ?

Split triangulation differently to produce more regular triangles

I'm trying to do some transitions on the topojson/world-atlas 110m map and finding that some of the more complicated geographies break a lot of the time when calling flubber.combine.

I've got a reproducible block up here [gist].

If you add e.g. the United States back in (by no longer filtering out id=840 in line 46) it'll break, saying Uncaught RangeError: Can't collapse topology into 10 pieces.. Changing maxSegmentLength can make things better or worse, causing more or fewer countries to break (but I've yet to find a value that works for all of them).

Also, thanks for the awesome library! I've been eagerly watching your bl.ocks for a while and wanting to give these transitions a try, and this makes it so easy to do.

Can we use flubber to animate from svg shape to path

Can we use flubber to animate from SVG shape (like rect, circle) to path?
If it is possible can someone share an example or code snippet rearging that.

Or is there some way to animate many circle to topojson map?

Thanks

Animation typical doesn't work over time

Hello, I send to you a message because It was hapened a strange thing when I used your plugin typical...
I follow all your tutorial, and the animation work great until 5 minutes, the animation become very fast and fast and I don't understand why , I try with several navigator and I have the same problem...
I send to you a video to show you : https://youtu.be/zsOzF6gpqsc

And I show you my code :

import React from 'react';

// import scss
import './home.scss';
// import typical animation
import Typical from 'react-typical';

class Home extends React.Component {
  state = {
  }


  render() {

    return (
      <div id="home">
        <div id="home-presentation">
          <h1 id="home-title">Anaïs Nisimov</h1>
          <p id="home-paragraph">Je suis{' '}
            <Typical
              id="home-profil"
              steps={['artiste sonore et numérique', 2000, 'développeuse web Junior', 2000]}
              loop={Infinity}
              wrapper="b"
            />
          </p>
        </div>
        <div id="home-ContainerImage">
          <img id="home-image" src="src/assets/images/logoecoute1_copie.png" alt="logohome" size="small" />
        </div>
      </div>
    );
  }
}


export default Home;

If you see something I mised, please let me know

PS: thanks for the tutorial ^^

React Native TransformError

I get this error with React Native.

Building JavaScript bundle: error
TransformError: /Users/RONIN/ReactNativeProjects/react-native-examples/node_modules/flubber/build/flubber.min.js: Unknown plugin "add-module-exports" specified in "/Users/RONIN/ReactNativeProjects/react-native-examples/node_modules/flubber/package.json" at 0, attempted to resolve relative to "/Users/RONIN/ReactNativeProjects/react-native-examples/node_modules/flubber"

Error: 'svgPathProperties' is not exported

In your code

node_modules/flubber/src/svg.js (2:9)
1: import Path from "svgpath";
2: import { svgPathProperties } from "svg-path-properties";
            ^
3: import normalizeRing from "./normalize.js";
4: import { isFiniteNumber } from "./math.js";

svPathProperties is not an exported name , the correct import is

2: import svgPathProperties from "svg-path-properties";
            ^
3: import normalizeRing from "./normalize.js";
4: import { isFiniteNumber } from "./math.js";```


Support for open/unclosed polygons (polylines)

Thanks for this amazing library!

Flubber works great for polygons but we're also trying to use it to transition a polyline. This mostly works except two issues:

  • sometimes points are added between the start and end of the path
  • the location of the opening is sometimes "moved"

Is this behavior something that could be suppressed with a polyline/unclosed flag??

2019-01-24_14-38-06

Thanks!

Multiple instances of flubber.js

Hello. Firstly, thanks very much for the work you've done to create flubber.js. It works fantastically and is a huge help.

I'm wondering how to run multiple instances of flubber.js on one DOM. I apologize beforehand if this is very obvious, but I've attempted a few things and struggled a bit and haven't been able to identify a way to allow this to happen. It appears the code identifies anything that is a path and removes each one except the first one. I tried to assign the different paths a class and use the d3 selector (i.e. d3.selectAll("path").filter(".nameOfClass"); with no luck. If you could provide any advice or suggestions it would be greatly appreciated. Thanks for your time.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.