W3C home > Mailing lists > Public > public-fx@w3.org > July to September 2016

Re: [css-transforms] CSS3D breaks with opacity flattening

From: /#!/JoePea <trusktr@gmail.com>
Date: Sun, 18 Sep 2016 17:51:14 -0700
Message-ID: <CAKU1PAXUEyF30huUvccwQ6PF7Vujiz8Eqsi8ioxCWS4V6VOqcg@mail.gmail.com>
To: "Tab Atkins Jr." <jackalmage@gmail.com>, trchen@chromium.org
Cc: Rik Cabanier <cabanier@gmail.com>, Chris Harrelson <chrishtr@google.com>, Simon Fraser <simon.fraser@apple.com>, "public-fx@w3.org" <public-fx@w3.org>
Hey all,

Sorry if my previous replies were rash, but I've collected myself and
prepared the following response which can hopefully paint a picture of the
problem as well as propose a solution.

Let's step outside of the web box for a second, and just imagine: we have a
3D engine, and with that engine we have game characters or objects that are
made of arbitrary number of descendant children (subtrees) in an overall
scene graph. Imagine these objects are in a game where things fade in and
out of view and therefore involve transparency.

I believe we all want the API for doing this to be as easy as possible.
And, since this is the case, then it makes sense that applying opacity less
than one to the root node of an object in a scene graph will make the whole
object transparent without flattening it.

Take for example, this basic car I just made:

https://jsfiddle.net/trusktr/ymonmo70/1

=== Challenge ===

Here's a challenge for you all: make the whole car transparent without
modifying the markup (it is important not to modify the markup because that
is what CSS is about). But, if you want to modify the markup, I am open to
seeing that solution as well (and I already know the non-nested solution
will work, but will be tedious to migrate to). Keep in mind, this is very
very easy with "legacy" behavior, and we can make the whole car transparent
by simply adding the following CSS into the CSS text box:

```css
.car {
  opacity: 0.5;
}
```


which results in the following (works in Safari 9 and Firefox 47, broken in
Chrome 53):

https://jsfiddle.net/trusktr/ymonmo70/2

As far as I know, I don't think any of you will have a solution that is as
simple as simply applying an opacity to the root node of the car. The
simplest solution I can imagine is applying opacity to all the leaf nodes,
and that will involve much more work than the addition of a single opacity
property to a single selector or element.

With the new behavior, it isn't so simple any more -- *the web's 3D API is
now **more difficult to use*, which isn't what we want as API developers.

We should aim to make 3D programming more easy, not more difficult! Opacity
flattening makes 3D programming more difficult because the 3D library
programmer now has to go and keep track of opacities virtually, for every
node in the scene (DOM nodes in our case) and then has to traverse the
scene graph (DOM tree) manually and multiply all opacities manually in
order to finally apply actual opacities to the leaf-most items (elements)
in the scene graph.

Note, even with "legacy" behavior or the new flattening behavior, there is
currently no way to make rendered non-leaf nodes transparent. In the
following example, try and make *only* the red box transparent but not
anything else (i.e. not it's children):

https://jsfiddle.net/trusktr/ymonmo70/4

With the new behavior, we are limited to making only leaf-nodes of an
existing scene transparent, and keep in mind we'd like to apply CSS *without
modifying markup*.

One of the great things about preserve-3d and nested elements is that
transformation matrices can be cached, and don't need to be re-calculated
all the time on the JavaScript side because the HTML engine has them cached
in the DOM hierarchy. This prevents a TON of string manipulation in
JavaScript due to converting numbers into strings to pass to the CSS engine
via style tags (that will be fixed with Typed CSSOM, but we're not sure
when that's making it into the wild).

In legacy behavior, the same applies to opacity: the HTML/CSS engine caches
values and does multiplication automatically, which makes the end API
easier to use for 3D programming, and *more performant.*

In order to solve the problem that the new opacity behavior entails, the
best solution is (unfortunately) to abandon preserve-3d and nested DOM (*losing
the performance benefits*), then keeping track of the transformation and
opacity hierarchy manually, multiplying down the tree manually, and finally
applying *all the values via number-to-string conversion in the style
attributes, which may involve string parsing, splitting, joining, etc,
before being actually passed to the style attribute (yikes!)*.

Reverting to the non-nested approach means that we are forfeiting the
performance advantages of the nested DOM approach just to achieve what we
want.

Note, using the non-nested approach, virtual hierarchies, and manual
handling of the math allows us to easily make a parent in the hierarchy
transparent but not its children (which is impossible in the nested
approach using the `opacity` property).

=== Impact ===

One argument for the go-ahead to implement this change is that "not many
people are impacted".

I'd like to argue that 3D in the web (as far as DOM goes) is already fairly
difficult compared to code-based APIs (i.e. imperative APIs rather than
declarative APIs, and not just in the web), and relatively few people
actually use CSS3D. The vast majority of websites still use only the
decades-old 2-dimensional technology of the web, and the 3D aspects of
HTML/CSS are relatively new and difficult to mix with the 2-dimensional
features. It may be easy to think that many people aren't affected by this,
but let's take into consideration the ratio of the number of people who use
opacity in 3D scenes to the number of people who program CSS3D scenes. If
you take these numbers into consideration, I am willing to bet that the
number will be a lot higher than the 0.006% mentioned in the blink-dev
thread
<https://groups.google.com/a/chromium.org/d/msg/blink-dev/eBIp90_il1o/9q3M0ww2BgAJ>
.

Famous was making a an open-source library that depended on the legacy
behavior, and this change breaks that library. They've gone proprietary,
and their proprietary stuff probably still depends on the legacy behavior.
They will now face the chore of re-writing parts of their library to
convert from nested dom to non-nested dom if they wish for opacity to work
the same as before.

I'd like to show you how these changes impact my own library at
http://infamous.io (which is being renamed and moved to a new domain soon).

My library gives us both a JavaScript API (imperative) and a
Custom-Element-based HTML API (declarative) for defining 3D scenes. When
using the JavaScript API, it generates the same elements as when using the
HTML API. For our purposes, I will show only the HTML API.

Here is the same car example as above (or, at least similar), made with my
custom elements:

https://jsfiddle.net/trusktr/ymonmo70/5

What is great about this example is that those custom elements are *the
elements themselves* in the final rendering (look in the element inspector
and notice that they have `transform: matrix3d()` applied to them). This
means we can place anything into those elements. For example, let's place
some text onto the surfaces of the car just for fun:

https://jsfiddle.net/trusktr/ymonmo70/6

Now, if we apply opacity, the whole car will be flattened in Chrome 53
(compare it to Safari 9) because we've applied opacity onto the `.car`
element element:

https://jsfiddle.net/trusktr/ymonmo70/7

I believe I have something really nice in the works, and plan to add WebGL
soon, but now it is fundamentally broken: in order to fix the rendering,
but keep my same nested markup, I will have to render new elements into a
flat structure that sits next to my <motor-scene> elements. This means that
for each motor-node element I will create a reciprocal element in the
actual rendering context, while my custom elements will not actually be
rendered (they will be display:none). In other words, although I'd like for
my API to be nested and using preserve-3d, I will have to render *completely
separate elements* next to my custom elements in order to solve the opacity
problem that is on my hands now.

But, there are some problems with this! Remember that we were able to place
content like text inside the custom elements, and it worked fine because
the custom elements are part of the actual rendered scene?

Well, now that won't be the case any more. Now I have the following
problems to deal with:

   1. I have to convert my code so it renders non-nested elements next to
   my custom elements (or maybe inside a ShadowDOM root).
   2. I will have to implement transformation caching just for DOM
   rendering, and can no longer take advantage of the HTML engine's transform
   caching.
   3. Same for opacity caching down the scene graph, I will need to
   implement it rather than let the HTML engine handle it.
   4. Lastly, but most problematically, I have to find a way to clone or
   copy the user's content from inside my custom elements into the new
   non-nested elements. This will have detrimental effects on the user's
   ability to target and select those elements for styling, as they will no
   longer appear in their original places. Users will need to use IDs all over
   the place when they probably shouldn't, and will likely mess up a bunch of
   times before discovering the ID workaround. This may also have an impact on
   SEO, but I haven't looked into that much yet.

The first three points aren't as bad as the fourth, as I'll have to
implement the first three points when I add WebGL anyways. But, the fourth
point is really bad! The new behavior is an extreme pain because of point
number four.

I hope you can see how, from my perspective as a library author writing on
top of HTML/CSS3D, this is a very unwelcoming change if opacity is to have
any meaning in my 3D scenes.

There aren't many people doing what I'm doing. There are only three
libraries out there that are doing what I am doing (that I know of):

   - Three.js <http://threejs.org> - Three.js uses the non-nested approach,
   so does not face the opacity problem. People who use Three.js'
   CSS3DRenderer know what they are working with from the start, and they
   won't possibly get confused in targeting their elements for styling,
   because their elements will not be transplanted like with my library. But
   note, Three.js will not be able to update to the nested-approach due to
   this problem, and therefore won't be able to have the performance benefits
   of the nested approach.
   - Famo.us <http://deprecated.famous.org> (0.3 and below) - Like
   Three.js, uses non-nested approach, and is worry-free like Three.js.
   - Famo.us Engine <http://github.com/famous/engine> (0.5 and above) -
   Uses the nested approach, and is now broken just like my library.
   - Samsara <http://samsarajs.org> - Uses the non-nested approach, so no
   problems there.
   - Infamous <http://infamous.io> - My library, which uses the nested
   approach. Problem!

In sincerity, opacity flattening is a breaking change that shouldn't have
been implemented by any browsers without a full solution for "legacy" code
with 3D in mind.

=== What We Need ===

What we need are possibly at least three forms of opacity:

   1. 3-dimensional, like the legacy behavior where things don't get
   flattened and opacities are multiplied down the hierarchy.
   2. Parent-only (opacity on a single element), where nothing is
   flattened, and opacity is applied only to the target element and not its
   children. Note, there is currently no way to do this in the nested
   approach, only in the non-nested approach with virtual hierarchies. In
   "legacy" behavior, we can only apply opacity to an entire subtree of a 3D
   context, not just to a parent element, so that's either all or nothing.
   3. Flattening, which is the new behavior, though I fail to see why
   anyone applying opacity to a 3D object desires for the object to be
   suddenly flat, as that doesn't make much sense and is unintuitive, and
   means the API is more difficult to use that what we'd like.

Note, with "legacy" behavior, *it is already possible to create a new 3D
context and apply opacity to it*, so this new behavior isn't actually
needed (from a functional perspective), because we can currently achieve
the same behavior with the "legacy" implementation. The new behavior does
two things:

   1. it eliminates the desired 3D behavior in nested scenes that use
   preserve-3d
   2. it adds a second method of opacifying an entire 3D scene which *we
   can already do*.

Here is an example of just that using "legacy" techniques, where I simply
add the style `motor-scene {opacity: 0.5}` to the CSS, which opacifies the
3D context:

https://jsfiddle.net/trusktr/ymonmo70/8

We all agree that the spec (whatever it says) should be well defined so
that it is clear what browsers should implement. I also believe that the
legacy behavior is much more desirable for 3D programmers than is the
flattening of objects (legacy-like behavior just needs to be clearly
spec'd).

=== Possible Solution ===

Maybe we can improve the spec (before making breaking changes, and browser
developers, you all roll back to "legacy" implementation ASAP) so that the
spec can appease all three opacity styles.

This could be achieved with something like a new CSS property called
`opacity-style`, f.e.

   - `opacity-style: 3d` which is similar to the legacy behavior and is
   great for 3D scenes, and does not flatten anything. Opacity is multiplied
   all the way down the tree with other same-style opacities and stops at
   elements that have a different opacity-style (`flat` or `single`).
   - `opacity-style: single` which does not flatten anything and only
   applies opacity to the target element but not its children
   - `opacity-style: flat` which is like the new behavior that Chrome 53
   just introduced, things are flattened and a new 3D context is made. I still
   fail to see how this is desirable. *Assuming we can apply an opacity to
   a 3D object to make it transparent and then having it become flat like
   paper is simply not intuitive.*

The default value can be `opacity-style: single`, which I think is the
least impacting of the three. We can then see what 3D programmers end up
using more often (I'd like to bet that `opacity-style: flat` will be the
least used). Or, perhaps the default for a 3D object can be `single`, and
`flat` for non-3D objects.

What are your thoughts on something like this, `opacity-style`? Because, as
it stands, opacity in the 3D web isn't very workable now in nested scenes
(it is much more workable with the legacy behavior).

*/#!/*JoePea

On Thu, Sep 15, 2016 at 11:16 AM, Tab Atkins Jr. <jackalmage@gmail.com>
wrote:

> On Thu, Sep 15, 2016 at 10:02 AM, Rik Cabanier <cabanier@gmail.com> wrote:
> > On Wed, Sep 14, 2016 at 8:44 PM, /#!/JoePea <trusktr@gmail.com> wrote:
> >> Here's an example using Famous Engine (http://github.com/famous/engine
> ):
> >>
> >> First, with opacity at the default value of 1.0, the "Famous Code" logo
> >> moves back and forth and rotates:
> >> http://jsfiddle.net/trusktr/spauv8fs/5
> >>
> >> With opacity reduced, it breaks in Chrome 53:
> >> http://jsfiddle.net/trusktr/spauv8fs/6
> >>
> >> Famous Engine has the ability to mix WebGL with DOM, and this includes
> >> opacity. This new behavior causes the DOM elements not to move in 3D
> space
> >> the WebGL meshes.
> >>
> >> First, here's a demo, with opacity at 1.0:
> >> http://jsfiddle.net/trusktr/spauv8fs/7
> >>
> >> The, with opacity at 0.7:
> >> http://jsfiddle.net/trusktr/spauv8fs/8
> >>
> >> What you are supposed to see is a DOM element and a WebGL Mesh that both
> >> seem to intersect with the pink DOM-based background (the
> implementation is
> >> not perfect yet...). In the second fiddle (spauv8fs/8) the "Famous Code"
> >> logo appears not to move any more while the WebGL mesh continues to
> move.
> >>
> >> There is a bug in the WebGL renderer which I believe may be due to
> changes
> >> in WebGL, but the WebGL part of those examples is supposed to be
> transparent
> >> as well.
> >>
> >> I myself am working on a new implementation of DOM + WebGL, and it
> allows
> >> application of opacity. This change in Chrome 53 completely breaks how
> that
> >> is supposed to work. I don't have an online demo yet...
> >>
> >> I would say let's definitely consider undoing the changes to the spec so
> >> that flattening does not happen when applying an opacity to 3D DOM
> elements.
> >> The reasoning is not just to prevent breaking apps, but because the new
> >> behavior simply doesn't make sense as far as 3D goes.
> >
> >
> > I don't understand how you come to that conclusion.
> > The new behavior seems more logical since it applies the opacity to
> element
> > that has the property applied. The old implementation distributed the
> value
> > to its children which is counter to any other CSS value.
> > Are you proposing that we also do this for filters, blending, backdrop
> > blurring and other effects, or is opacity special in some way,
> >
> > As Simon stated, if you want the old behavior, just add a selector to
> your
> > opacity parameter so it's applied to the children.
>
> Yes. This is not a spec bug, it's a natural and unavoidable
> consequence of doing a "group effect", which opacity, filters, and a
> few other effects are.  These types of effects require the group to be
> rendered as a unit, then have the effect applied; in 3d space, this
> has the effect of flattening them. (If you didn't flatten, then other
> items could get between the individual pieces in 3d order, and there's
> no consistent or sensible way to render that. This is identical to how
> z-index is "flattened" by opacity and other group effects, so you
> can't sandwich elements elsewhere in the page between the elements in
> the group.)
>
> If you want to sandwich things, you need to push the effect further
> down into the leaves, so it doesn't group as many things together.
> This lets you do more re-ordering, but has a different visual effect -
> if one item occludes another in a group, when they're made transparent
> they still occlude; if they're made individually transparent, you'll
> be able to see the second item behind the first.  Similar differences
> exist for other group effects - if you're doing a gaussian blur,
> blurring two boxes as a group can look quite different than blurring
> them individually.
>
> As Simon said, Chrome 52 and earlier Safari are simply buggy and not
> "grouping" things correctly; they're instead automatically pushing the
> opacity down further toward the leaves when 3d is involved.  If you
> want the same effect, just do so manually, as Rik recommends.
>
> (We had an almost identical request in the SVG Working Group a few
> days ago. I posted
> https://github.com/w3c/svgwg/issues/264#issuecomment-246750601 as an
> extended explanation of what's happening.)
>
> ~TJ
>
Received on Monday, 19 September 2016 00:52:26 UTC

This archive was generated by hypermail 2.4.0 : Friday, 17 January 2020 19:49:57 UTC