Jump to content
Search Community

scale3d() not supported ?

cesare.polonara test
Moderator Tag

Go to solution Solved by GreenSock,

Recommended Posts

I think you're the first person I've ever seen (in 11 years) express a desire to apply a scaleZ. To optimize performance, that isn't something GSAP supports directly, but you can easily use a proxy and the complex string interpolation capabilities of GSAP to do it: 

See the Pen poOygrq?editors=1010 by GreenSock (@GreenSock) on CodePen

 

Is that what you're looking for? 

  • Like 3
Link to comment
Share on other sites

12 minutes ago, GreenSock said:

I think you're the first person I've ever seen (in 11 years) express a desire to apply a scaleZ. To optimize performance, that isn't something GSAP supports directly, but you can easily use a proxy and the complex string interpolation capabilities of GSAP to do it: 

 

 

 

Is that what you're looking for? 

Ahah I know it's not a common use case since I'd as well usually use canvas 3D to achieve this sort of stuff, but this is a project using a sort of cube in CSS only that I use to perform weird animations, and your solution works pretty well!

Could I ask why it works animating the proxy object and it doesn't when trying to animate directly the style object of the element ?
I naively was thinking it would have used that complex string interpolation automatically.

Thanks BTW, always a great support!

  • Like 1
Link to comment
Share on other sites

6 minutes ago, cesare.polonara said:

Could I ask why it works animating the proxy object and it doesn't when trying to animate directly the style object of the element ?
I naively was thinking it would have used that complex string interpolation automatically.

No, it can't do the complex string stuff unless the starting and ending values are formatted exactly the same (so that only the numbers are different). For example: 

// tweenable as a "complex string"
start: "rotateX(0deg) rotateY(0deg) scale3d(1, 1, 1)"
end: "rotateX(110deg) rotateY(-80deg) scale3d(2, 2, 2)"

// NOT tweenable as a "complex string"
start: "rotateX(0deg) rotateY(0deg) scale3d(1, 1, 1)"
end: "translate(-50%, -50%) skewX(20deg)"

See how the start and end of the 2nd one are completely different? The strings don't match up at all. 

 

Transforms are GSAP's specialty in fact. A ton of effort has gone into smoothing out handling transforms. Under the hood, GSAP has to parse the transforms string (the browser reports it as either a matrix() or matrix3d()), splitting it into all the different components like x, y, z, scaleX, scaleY, rotation, rotationX, rotationY, etc. so that you can control each one independently. That's impossible with CSS. For example, try animating translateX() over the course of 2 seconds with a particular ease, and then halfway through that, start animating translateY() with a different ease over the course of 2 seconds. Nope. 

 

So all that parsing is complicated but it opens up a whole world of flexibility and fun. And GSAP also ensures that things are applied in a consistent order-of-operation. Since almost nobody has ever needed scaleZ support, we saved some kb and complexity (CPU cycles) by omitting that. 

 

Does that help clear things up? 

  • Like 2
Link to comment
Share on other sites

1 hour ago, GreenSock said:

No, it can't do the complex string stuff unless the starting and ending values are formatted exactly the same (so that only the numbers are different). For example: 

// tweenable as a "complex string"
start: "rotateX(0deg) rotateY(0deg) scale3d(1, 1, 1)"
end: "rotateX(110deg) rotateY(-80deg) scale3d(2, 2, 2)"

// NOT tweenable as a "complex string"
start: "rotateX(0deg) rotateY(0deg) scale3d(1, 1, 1)"
end: "translate(-50%, -50%) skewX(20deg)"

See how the start and end of the 2nd one are completely different? The strings don't match up at all. 

 

Transforms are GSAP's specialty in fact. A ton of effort has gone into smoothing out handling transforms. Under the hood, GSAP has to parse the transforms string (the browser reports it as either a matrix() or matrix3d()), splitting it into all the different components like x, y, z, scaleX, scaleY, rotation, rotationX, rotationY, etc. so that you can control each one independently. That's impossible with CSS. For example, try animating translateX() over the course of 2 seconds with a particular ease, and then halfway through that, start animating translateY() with a different ease over the course of 2 seconds. Nope. 

 

So all that parsing is complicated but it opens up a whole world of flexibility and fun. And GSAP also ensures that things are applied in a consistent order-of-operation. Since almost nobody has ever needed scaleZ support, we saved some kb and complexity (CPU cycles) by omitting that. 

 

Does that help clear things up? 

Yeah I got the requirement to perform string interpolation, in fact I was doing this:
 

gsap.fromTo(".cube", {
  transform: "rotateX(0deg) rotateY(0deg) scale3d(1,1,1)"
},{
    transform: "rotateX(120deg) rotateY(110deg) scale3d(2,2,2)",
}

But if I understood correctly the point is that when you animate the `transform` property of an object of type HTMLElement it's actually performing a custom parsing of the string you pass and not using string interpolation, while if you use a POJO like a proxy, it has no way to know  the `transform` property is a css property so it just tries to string interpolate it, I got that about right ?

Link to comment
Share on other sites

3 minutes ago, cesare.polonara said:

But if I understood correctly the point is that when you animate the `transform` property of an object of type HTMLElement it's actually performing a custom parsing of the string you pass and not using string interpolation, while if you use a POJO like a proxy, it has no way to know  the `transform` property is a css property so it just tries to string interpolate it, I got that about right ?

That's exactly correct. GSAP is smart enough to check if the target is a DOM element and if so, it handles the "transform" property in a very particular way (with parsing, isolating individual components, etc.). A generic object, however, skips all that.  👍

  • Like 2
Link to comment
Share on other sites

8 minutes ago, GreenSock said:

That's exactly correct. GSAP is smart enough to check if the target is a DOM element and if so, it handles the "transform" property in a very particular way (with parsing, isolating individual components, etc.). A generic object, however, skips all that.  👍

Great to know, thanks for your time !

Link to comment
Share on other sites

Totally! 

 

I'm sure it wouldn't be noticeable unless you're doing hundreds of simultaneous animations like that, but in my tests when you use CSS variables that then get plugged into transforms, it doesn't perform quite as well as applying them directly. But again, nobody would ever notice that in a real-world project unless there's a LOT going on. And maybe browsers have improved things since I did those tests. I just figured I'd mention it in case you wanna run your own tests first before deciding which option to choose. 

  • Like 1
Link to comment
Share on other sites

28 minutes ago, elegantseagulls said:

You could also use css custom properties to avoid having to match all the transform strings exactly.

css:

.cube {
  --scale3d: 1, 1, 1;
  
  transform: scale3d(var(--scale3d)) translate(50%);
}

js:

gsap.to('.cube', {'--scale3d': '2, 2, 2'})

 

 

Good to know, I ended up following Jack's suggestion yesterday btw, so I wrote a specific util to handle the transform string update and it's behaving averagely well leaving the timeline code clean enough, it just does not like very much if you do incremental string interpolation like "+=" but I can live without that:
 

const updateTransform = (selector, proxy) => {
	const el = document.querySelector(selector);
	const transformString = Object.entries(proxy).reduce(
		(acc, curr) =>
			curr[0] === '_gsap' ? acc + '' : acc + ` ${curr[0]}(${curr[1]})`,
		''
	);
	el.style.transform = transformString;
};

// Actual cube proxy props

const cubeProxy = {
	translateX: '-50%',
	translateY: '-50%',
	translateZ: '0vw',
	rotateX: '0deg',
	rotateY: '0deg',
	rotateZ: '0deg',
	rotate3d: '0,0,0,0deg',
	scale3d: '0,0,0',
};

// Then use it onUpdate:

const updateCube = () => updateTransform('#my-cube', cubeProxy);

 

  • Like 1
Link to comment
Share on other sites

Oh, I definitely would not do that because:

  1. You're tapping into a private property ("_gsap") which is not guaranteed to be supported in future versions. Whenever a property starts with "_", that basically means "don't touch this - it's for internal use only". I mean it will certainly work in this version, but I just hate the idea of your code breaking in the future if we ever changed _gsap to something else.
  2. Your technique is very slow/wasteful. I obsess about performance, sorry. It's probably fine for your needs, but I'd never recommend doing a loop like that, creating a new function on every onUpdate, running a querySelector(), building the entire transform string even with values that are the defaults, etc... on every single tick. You're putting extra load on the parser. It's probably much worse performance-wise than @elegantseagulls suggestion with CSS variables. 

Is your goal to just add scaleZ support? That's it? And you need to be able to accommodate various mixes of components (your original question had a simple string that was formatted the same for the start and end values...is that no longer the case?)? 

 

There are a lot of ways to accomplish this. I just want to be clear about your goal(s) before recommending anything. I thought the prior solution I provided was sufficient for your needs, but it sounds like maybe I misunderstood. 

Link to comment
Share on other sites

2 hours ago, GreenSock said:

Oh, I definitely would not do that because:

  1. You're tapping into a private property ("_gsap") which is not guaranteed to be supported in future versions. Whenever a property starts with "_", that basically means "don't touch this - it's for internal use only". I mean it will certainly work in this version, but I just hate the idea of your code breaking in the future if we ever changed _gsap to something else.
  2. Your technique is very slow/wasteful. I obsess about performance, sorry. It's probably fine for your needs, but I'd never recommend doing a loop like that, creating a new function on every onUpdate, running a querySelector(), building the entire transform string even with values that are the defaults, etc... on every single tick. You're putting extra load on the parser. It's probably much worse performance-wise than @elegantseagulls suggestion with CSS variables. 

Is your goal to just add scaleZ support? That's it? And you need to be able to accommodate various mixes of components (your original question had a simple string that was formatted the same for the start and end values...is that no longer the case?)? 

 

There are a lot of ways to accomplish this. I just want to be clear about your goal(s) before recommending anything. I thought the prior solution I provided was sufficient for your needs, but it sounds like maybe I misunderstood. 

Well the example I posted was just a minimum abstraction to understand how to animate scaleZ, in practice the animation is a complex scrollTrigger timeline, and on that cube there are applied a lot of different transforms, mainly translate, rotate, and scale on all the axis with different start/end times, and I'd like to keep the timeline code itself as cleanest as possible avoiding to write inline functions.

About performance you are right, that probably sucks, I just wrote that yesterday to make the things work and I'm biased to write functional code, but in this case it' s definitely bad performance wise.
Do you think it would perform better this way, with no loops at all inside the update function?
 

		const cube = document.querySelector('#my-cube');

		const cubeProxy = {
			translateX: '-50%',
			translateY: '-50%',
			translateZ: '0vw',
			rotateX: '0deg',
			rotateY: '0deg',
			rotateZ: '0deg',
			rotate3d: '0,0,0,0deg',
			scale3d: '0,0,0',
		};

		const updateCube = () => {
			const transformString = `translateX(${cubeProxy.translateX}) translateY(${cubeProxy.translateY}) translateZ(${cubeProxy.translateZ}) rotateX(${cubeProxy.rotateX}) rotateY(${cubeProxy.rotateY}) rotateZ(${cubeProxy.rotateZ}) scale3d(${cubeProxy.scale3d})`;
			cube.style.transform = transformString;
		};

 Do you have any more elegant solution to granularly change in the transformString only the property that actually changes?
I even tried with an actual JS Proxy and a custom setter to intercept the prop change to avoid the onUpdate function but it does not seem to work at all and that's not probably a good idea in any case...
I have no way to benchmark this now.

Thanks.

Link to comment
Share on other sites

  • Solution

Here's a plugin I whipped together for you that lets you animate scaleZ in GSAP 3: 

gsap.registerPlugin({
  name: "scaleZ", 
  priority: -1, // make it run last so that the other transforms are already applied
  init(target, value) {
    target._gsap || gsap.set(target, {x: "+=0"}); // force creation
    let cache = this.cache = target._gsap;
    if (!("scaleZ" in cache)) {
      cache.scaleZ = 1;
      let oldRender = cache.renderTransform;
      cache.renderTransform = (ratio, data) => {
        oldRender(ratio, data); // takes care of everything except scaleZ()
        target.style.transform += " scaleZ(" + data.scaleZ + ")";
      }
    }
    this.add(cache, "scaleZ", cache.scaleZ, value);
    this._props.push("scaleZ");
  },
  render(ratio, data) {
    let pt = data._pt;
		while (pt) {
			pt.r(ratio, pt.d);
			pt = pt._next;
		}
    data.cache.renderTransform(ratio, data.cache);
  }
});

And here it is in action: 

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

 

That way, you just load that plugin once and it works with all your tweens without needing to add any onUpdates or anything. 

 

Does that help? 

  • Like 4
Link to comment
Share on other sites

8 hours ago, GreenSock said:

Here's a plugin I whipped together for you that lets you animate scaleZ in GSAP 3: 

gsap.registerPlugin({
  name: "scaleZ", 
  priority: -1, // make it run last so that the other transforms are already applied
  init(target, value) {
    target._gsap || gsap.set(target, {x: "+=0"}); // force creation
    let cache = this.cache = target._gsap;
    if (!("scaleZ" in cache)) {
      cache.scaleZ = 1;
      let oldRender = cache.renderTransform;
      cache.renderTransform = (ratio, data) => {
        oldRender(ratio, data); // takes care of everything except scaleZ()
        target.style.transform += " scaleZ(" + data.scaleZ + ")";
      }
    }
    this.add(cache, "scaleZ", cache.scaleZ, value);
    this._props.push("scaleZ");
  },
  render(ratio, data) {
    let pt = data._pt;
		while (pt) {
			pt.r(ratio, pt.d);
			pt = pt._next;
		}
    data.cache.renderTransform(ratio, data.cache);
  }
});

And here it is in action: 

 

 

 

That way, you just load that plugin once and it works with all your tweens without needing to add any onUpdates or anything. 

 

Does that help? 

This looks amazing dude. This low level API looks pretty powerful, thanks for taking your time of showing this, the more I use GSAP the more I realize I need to learn how to use it properly...
Marked as definitely solved.
UPDATE:
Implemented it and it's noticeably smoother than the proxy version plus now rotations are pretty precise.

  • Like 5
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.
×
×
  • Create New...