Jump to content
GreenSock

Search In
  • More options...
Find results that contain...
Find results in...
Karliky

How to do soft bezier with MotionPath

Recommended Posts

Hi all,

 

I'm using GSAP 2 and I want to migrate to GSAP 3.

 

In order to do it, I need to use the type "soft" point control from GSAP 2, like this:

      bezier: {
        values: keyframes,
        curviness: 0,
        type: 'soft',
        timeResolution: 0,
      },

I see no way to do that kind of animation with GSAP 3, I found the "soft" implementation here: 

https://github.com/greensock/GSAP/blob/2.1.3/src/esm/BezierPlugin.js#L220-L260

 

Is there any way to perform a Bezier animation like the described above with GSAP 2 on the version 3?. I see this like a regression in the GSAP Api and I can't migrate to the version 3.

Link to comment
Share on other sites

Not really a regression. v3 is major release which allows for breaking changes.

 

Type soft and quadratic were removed from v3, but those can easily be replaced by creating an SVG path data string.

 

Here's how you can do a soft path. 

 

function softBezier(target, values) {
    
  var points = [{
    x: gsap.getProperty(target, "x"),
    y: gsap.getProperty(target, "y"),
  }, ...values];
  
  var size = points.length;
  var mids = [];
  var p1, p2, i;
  
  for (i = 0; i < size - 1; i++) {
    
    p1 = points[i];
    p2 = points[i+1];
    
    mids.push({
      x: (p1.x + p2.x) / 2,
      y: (p1.y + p2.y) / 2
    });
  }
  
  var path = `M${points[0].x} ${points[0].y} Q`
  
  for (i = 1; i < size - 2; i++) {
    
    p1 = points[i];
    p2 = mids[i];
    
    path += ` ${p1.x} ${p1.y} ${p2.x} ${p2.y}`;
  }
  
  p1 = points[i];
  p2 = points[i+1];
  
  path += ` ${p1.x} ${p1.y} ${p2.x} ${p2.y}`;
  
  return path;
}

 

This demo compares v2 soft to a v3 path. v2 seems to drift a little, but I'm not sure why. @GreenSock might be able to answer that one, and maybe why resolution only works for arrays in v3.

 

See the Pen 7b3d8b77a17de21df95e899db5da0148 by osublake (@osublake) on CodePen

 

 

  • Like 4
Link to comment
Share on other sites

Hey @OSUblake, thanks for your help.

 

So the problem with that approach is that a SVG path assumes to have a point vector X/Y, in my case I have a 3D vector X, Y, Z among other properties like Yaw/Pitch/Roll.

 

The question is, how to animate all of those properties, who are not 2D vectors.

 

This is how my JS object looks like in GSAP 2:

 

	const cinematicValues = {
		"x": 1282.3004150390625,
		"y": -4295.484375,
		"z": 38.27876281738281,
		"yawCos": -0.9832111580884312,
		"yawSin": 0.18247141861235708,
		"pitch": 0.18669402599334717,
		"roll": 0
	};

Each keyframe is an object with those same values moved to some point:

keyframes = [
  {
		"x": 1319.0740966796875,
		"y": -4324.2041015625,
		"z": 51.754337310791016,
		"yawCos": -0.3853230395894568,
		"yawSin": -0.9227817483899114,
		"pitch": -0.49398455023765564,
		"roll": 0
	}, {
		"x": 1245.7869873046875,
		"y": -4298.46142578125,
		"z": 38.76139450073242,
		"yawCos": -0.9444551739729924,
		"yawSin": -0.32864026587690787,
		"pitch": -0.1405552178621292,
		"roll": 0
	},
      ...]

 

That softBezier function can't handle that input, also I'm not sure of how to properly adapt it as the Path uses the Q command to create a quadratic curve.

 

thanks in advance. 

 

Link to comment
Share on other sites

Hm... soft is just a bunch of quadratic beziers, but we can convert those to cubic, which is what gsap really wants. So maybe try this.

 

It could probably be re-written with less loops.

 

function softBezier(target, values) {
  
  var props = Object.keys(values[0]);
  var getProp = gsap.getProperty(target);
    
  var start = {};
  props.forEach(prop => start[prop] = getProp(prop));
  
  var points = [start, ...values];
  var path = [start];
  var last = start;
  
  var size = points.length;
  var mids = [];
  var p0, p1, p2, i;
    
  for (i = 0; i < size - 1; i++) {
    
    p1 = points[i];
    p2 = points[i+1];
    
    var mid = {};    
    props.forEach(prop => mid[prop] = (p1[prop] + p2[prop]) / 2);
    mids.push(mid);
  }
    
  for (i = 1; i < size - 2; i++) {
    
    p0 = last;
    p1 = points[i];
    p2 = mids[i];
    
    var cp1 = {};
    var cp2 = {};
    
    props.forEach(prop => {      
      cp1[prop] = (p0[prop] + 2 * p1[prop]) / 3;
      cp2[prop] = (p2[prop] + 2 * p1[prop]) / 3;
    });
    
    path.push(cp1, cp2, p2);
    
    last = p2;
  }
  
  p0 = last;
  p1 = points[i];
  p2 = points[i+1];

  var cp1 = {};
  var cp2 = {};

  props.forEach(prop => {
    cp1[prop] = (p0[prop] + 2 * p1[prop]) / 3;
    cp2[prop] = (p2[prop] + 2 * p1[prop]) / 3;
  });
    
  path.push(cp1, cp2, p2);
    
  return path;
}

 

You will need to add type: "cubic" to your motionPath vars.

 

gsap.to(target, {
  motionPath: {
    path: softBezier(target, points),
    type: "cubic"
  }
});

 

Note that last time I tried to animate a z property with cubic beziers, it didn't seem to work correctly. I don't know if @GreenSock is aware of that or if it has been addressed.

 

 

 

 

 

  • Like 3
Link to comment
Share on other sites

That defenitely works for X and Y, and what I see is that the movement is smooth which is what I was looking for. But as you said, Z is not being animated, nor any of the other properties like yaw/pitch/roll.

 

GSAP 2 was able to animate any property, let's see what @GreenSocksais about this issue. It looks like when setting type:cubic all the values are ignored but x/y.

 

thank you! :) 

Link to comment
Share on other sites

Sorry, I didn't test that code with other properties. It looks like gsap can only tween x and y properties when using type: "cubic". I thought it could handle other properties, but it looks like that will only work when using type: "thru". 

 

But yeah, I'll wait and see what greensock has to say about this.

 

 

 

Link to comment
Share on other sites

I have read both source code of 2.1.3 BezierPlugin.js and 3.2.6 MotionPathPlugin.js. To me it's pretty clear that MotionPathPlugin.js can't handle any other property but x/y, the code is highly coupled to that concept with vars like this.

 

var _xProps = ["x", "translateX", "left", "marginLeft"], _yProps = ["y", "translateY", "top", "marginTop"],

So I don't think there is a way ti make MotionPathPlugin work with any other property, BezierPlugin was agnostic to the input altought MotionPathPlugin is not.

 

The solutions that my mind come up are complicated and difficult to mantain:

- Migrate BezierPlugin to GSAP 3.x

- Write my own implementation of a Cubic Bezier whose agnostic to the properties passed.

- Pass properties to gsap.to that are decoupled into X/Y by using Math.cos/Math.asin, then convert them back to a proper value inside onUpdate by calling Math.atan2, this will make it work for GSAP 3, however it involves creating. complicated code base which is outside the scope of my project.

Link to comment
Share on other sites

I'd just wait for greensock to respond. If he is taking a long time, that usually means he is working on a solution. 😉

 

  • Like 1
  • Haha 2
Link to comment
Share on other sites

Sorry, please give me some more time to look into it - I've had a crazy last few days and am catching up. I'll respond within the next 24 hours for sure. 

  • Like 2
Link to comment
Share on other sites

Of course, just take the time you need. 😊

Link to comment
Share on other sites

Alright, here's a function that'll convert points into the cubic bezier equivalent "soft" version: 

function softPointsToCubic(points, target, curviness) {
  let result, getter, p1, p2, v1, v2, p, i, a, b, c, d, l;
  if (target) {
    a = {};
    getter = gsap.getProperty(target);
    for (p in points[0]) {
      a[p] = getter(p);
    }
    points = points.slice(0);
    points.unshift(a);
  }
  !curviness && curviness !== 0 && (curviness = 1);
  curviness *= 2 / 3; // how strongly it pulls toward the control points. Cubic Bezier control points are 2/3rds the distance to the quadratic point.
  d = points[0];
  result = [d];
  l = points.length - 1;
  for (i = 1; i < l; i++) {
    a = d;
    b = {};
    c = {};
    d = {};
    p1 = points[i];
    p2 = points[i+1];
    for (p in a) {
      v1 = a[p];
      v2 = p1[p];
      b[p] = v1 + (v2 - v1) * curviness;
      d[p] = v1 = (i === l - 1) ? p2[p] : (v2 + p2[p]) / 2;
      c[p] = v1 + (v2 - v1) * curviness;
    }
    result.push(b, c, d);
  }
  return result;
}

And here's a fork of Blake's demo that shows it in action: 

See the Pen 34a7b9cbd60bb84cd808ed1ec03536a8?editors=0010 by GreenSock (@GreenSock) on CodePen

 

Note that I needed to tweak something in MotionPathPlugin to allow properties that aren't related to x/y, so that'll be in the next release but I already pushed it here as a preview: https://s3-us-west-2.amazonaws.com/s.cdpn.io/16327/MotionPathPlugin.min.js

 

Does that help? 

 

I didn't want to bake all that into the plugin because frankly I don't think it's very common at all for folks to need the "soft" option, and it'd be more efficient to let folks use an external helper function. But of course if there's a lot of demand in the coming months, I'd certainly be willing to reconsider. 

  • Like 4
Link to comment
Share on other sites

4 hours ago, GreenSock said:

Alright, here's a function that'll convert points into the cubic bezier equivalent "soft" version: 

 

Nice code. Much simpler and compact than what I came up with. 😉

 

I'm still wondering about resolution.

 

Quote
resolution Number - Only applies when path is an Array. Due to the nature of bezier curves, plotting the progression of an object on the path over time can make it appear to speed up or slow down based on the placement of the control points and the length of each successive segment on the path. So MotionPathPlugin implements a technique that reduces or eliminates that variance, but it involves breaking the segments down into a certain number of pieces which is what resolution controls. The greater the number, the more accurate the time remapping. But there is a processing price to pay for greater precision. The default value of 12 is typically perfect, but if you notice slight pace changes on the path you can increase the resolution value. Or, if you want to prioritize speed you could reduce the number.

 

Does that only apply to the thru type? It looks like it doesn't work with cubic. However, it looks like cubic has a pretty uniform speed, so you must be doing something internally to normalize it, right?

 

I'm more wondering what's going on under the hood, and getting clarification.

 

  • Like 1
Link to comment
Share on other sites

The demo looks that is working as expected compared with GSAP 2, but in my tests the values are "bumping" from time to time on GSAP 3. I'm writin a Three.js demo so you can both take a look but it will take me some time to do it so please wait until I finish it. thank you!

Link to comment
Share on other sites

13 hours ago, OSUblake said:

I'm still wondering about resolution. Does that only apply to the thru type? It looks like it doesn't work with cubic. However, it looks like cubic has a pretty uniform speed, so you must be doing something internally to normalize it, right? I'm more wondering what's going on under the hood, and getting clarification.

"resolution" basically controls how many segments each cubic bezier piece (from one anchor to the next) gets busted up into for measuring purposes which affects how evenly the time gets distributed. The default is normally fine, but if you notice things accelerating/decelerating unexpectedly at spots, you can increase the resolution. It just costs more processing time (shouldn't be terrible). 

 

Does that clear things up? 

 

8 hours ago, Karliky said:

The demo looks that is working as expected compared with GSAP 2, but in my tests the values are "bumping" from time to time on GSAP 3. I'm writin a Three.js demo so you can both take a look but it will take me some time to do it so please wait until I finish it. thank you!

I'm not quite sure what you mean by "bumping". Can you explain? 

 

The only thing I can think of is that when measuring distances, the "x" and "y" values are typically linked so that distances for time normalization are Math.sqrt(x * x + y * y) but z isn't factored in that way. It's treated independently. It's not a simple task to force z into that calculation (it'd be costly kb-wise and I can't imagine even 0.0001% of our users would ever actually tap into that so I feel bad making everyone pay the kb price).

 

Maybe it'll be more clear when you explain/show more about the "bumping" thing. 

Link to comment
Share on other sites

27 minutes ago, GreenSock said:

Does that clear things up? 

 

I understand how it works, but it doesn't seem to have any effect. With a resolution of 0, I would expect it to go fast on long straight segments, and slow down on short curvy segments. 

Link to comment
Share on other sites

32 minutes ago, GreenSock said:

It's not a simple task to force z into that calculation (it'd be costly kb-wise and I can't imagine even 0.0001% of our users would ever actually tap into that so I feel bad making everyone pay the kb price).

 

I dunno. Who's more like likely to use the MotionPathPlugin, your typical user who is doing UI animations, or someone who is using a WebGl library like three.js? Seems like it's excluding a large segment.

 

  • Like 1
Link to comment
Share on other sites

29 minutes ago, OSUblake said:

 

I understand how it works, but it doesn't seem to have any effect. With a resolution of 0, I would expect it to go fast on long straight segments, and slow down on short curvy segments. 

Oh, sorry about the confusion there - it doesn't allow 0. I figured nobody would ever want that, and the internal code is like vars.resolution || 12, so since 0 is "falsy" it jumps to the default. Try setting it to 1 and you'll see the speeding up and slowing down a lot more. 

Link to comment
Share on other sites

32 minutes ago, OSUblake said:

I dunno. Who's more like likely to use the MotionPathPlugin, your typical user who is doing UI animations, or someone who is using a WebGl library like three.js? Seems like it's excluding a large segment.

 

Oh, I think it's not even close - I'd bet less than 0.01% need 3D motion paths. I'm not saying it wouldn't be useful to anyone though. Maybe if there's enough demand, we create a MotionPath3DPlugin or something. Putting that functionality into the existing plugin means significant kb being added to anything that leverages paths/beziers, including MorphSVGPlugin, CustomEase, and the PathEditor that's used in MotionPathHelper. I think this is the first time this kind of request has come up since GSAP 3 was launched which is an indicator of how pervasive the need is. Agreed? 

Link to comment
Share on other sites

37 minutes ago, GreenSock said:

Oh, sorry about the confusion there - it doesn't allow 0. I figured nobody would ever want that, and the internal code is like vars.resolution || 12, so since 0 is "falsy" it jumps to the default. Try setting it to 1 and you'll see the speeding up and slowing down a lot more. 

 

I've used 0 in the past. I think I was doing a thru type with a curviness of 0 to do some linear animations.

 

24 minutes ago, GreenSock said:

I think this is the first time this kind of request has come up since GSAP 3 was launched which is an indicator of how pervasive the need is.

 

It was brought up recently. 

 

 

 

 

  • Like 1
Link to comment
Share on other sites

Hey @OSUblake and @GreenSock!

 

I pledge you all to read and watch this carefully.

 

Thanks for putting all of that effort on this.

 

Here are my results using GSAP 2 and GSAP 3.

 

First of all, please note that, this is not that I want support for Z or 3D support on MotionPathPlugin. It's more that I expect MotionPathPlugin to treat any property the same way BezierPlugin does for the same input.

 

Watch this video, animated with GSAP2:

https://youtu.be/RgrhY0oMrF8

Now watch this video, animated with GSAP3:

https://youtu.be/IyIZE276vmI

Here is the same video with side by side comparison:

https://youtu.be/PzYNoRFi9gU

You can clearly see that GSAP 2 follows the path in a smooth way, and GSAP 3 does bump/jump everytime it gets to a point. My project is an in-game cinematic tool that is based on this GSAP 2 type soft animation. If I migrate to GSAP 3, no one could create a cinematic as no one wants its camera to bump/jump every x seconds.

 

Please note that both videos used the same input.

 

Here are 2 codepens, one using GSAP 2 and other one using GSAP 3.

 

GSAP 2 / Three.js (BezierPlugin type soft):

See the Pen zYvJjyr by karliky (@karliky) on CodePen

 

GSAP 3 / Three.js (softPointsToCubic type cubic)

See the Pen QWjVrzo by karliky (@karliky) on CodePen

 

If you take a look at the GSAP 3 example. You can clearly see that bump/jump/whatever it's called from time to time.

 

To me, it looks that even though BezierPlugin and MotionPathPlugin can solve the same problem, they are essentially different.

 

I hope this makes it clear the problem I'm having with GSAP 3 and why I can't migrate to GSAP 3. :)

 

Thanks in advance!

 

 

 

  • Like 3
Link to comment
Share on other sites

I have updated both codepen examples in the post above as I think they now show the problem clearly.

 

As you can see, GSAP 2 moves the camera smoothly around the whole path.

GSAP 3 movement "twerks/bumps/jumps" and doesn't even follow the same path even though they have the same input.

 

The config for each example:

GSAP 2:

bezier: {
  values: keyframes,
  curviness: 0.1,
  type: 'soft',
  timeResolution: 0,
},

GSAP 3

motionPath: {
  path: softPointsToCubic(keyframes, cinematicValues, 0.1),
  type: 'cubic',
  resolution: 0
},

 

Thanks :)

  • Like 2
Link to comment
Share on other sites

I'll need some time to look into this but thanks for the demos. 👍

  • Like 1
Link to comment
Share on other sites

  • 2 weeks later...

This is a friendly reminder to let you know that some of us are waiting for this @GreenSock :) thanks for your support

  • Thanks 1
Link to comment
Share on other sites

Yep, I haven't forgotten but this is a very complex issue and we've been swamped with the ScrollTrigger launch. I'll circle back as soon as I can. I've already sunk many hours into this, so I'm not ignoring it. :) 

  • Like 1
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×