impress / impress.js

It's a presentation framework based on the power of CSS3 transforms and transitions in modern browsers and inspired by the idea behind prezi.com.
http://impress.js.org
MIT License
37.62k stars 6.67k forks source link

CSS3 scale property produces bad quality on lower-than-config-resolution windows #609

Open fin-ger opened 7 years ago

fin-ger commented 7 years ago

There are several other issues in this repo that deal with sizing/scaling a slide/step to fit into the browser window. This issue deals with visual artifacts produced by the CSS scale property.

As far as I understood the current implementation the scale value will be <1 if the browser windows content area is too small to fit a given resolution inside the browser window. The resolution (in px) is given via defaults or the data-width and data-height attributes on the impress root element. This will NOT "perfectly" fit a presentation into windows that provide higher resolution than the configured one (common on high-dpi displays). Also this will create aliasing artifacts on windows that are smaller than the configured resolution (the scale value is <1). I think (!) the aliasing artifacts are created by the browser when rendering the "scene" in its native resolution (e.g. 1920x1080) and downscaling this scene to e.g. 1024x576 (common beamer resolution). Hinting and antialiasing done by the vector graphics rasterizer of the browser are destroyed (correct me if I'm wrong).

This is a magnified screenshot of native resolution font rasterization: native-resolution

This is a magnified screenshot of the scaled font rasterization: aliasing

You can see that the scaled scene has more aliasing artifacts than the native resolution image.

I tried to accomplish an always-fitting-slide without using the CSS scale property with vw and vh properties at a slide aspect ratio of 16:9. This approach can set the font-size to e.g. 4vh to scale all the content with the slide. When using em to size all the content it should work pretty good. I did not use impress.js for this example, so it remains to be seen if such a change can be integrated into impress.js.

Example without CSS scale (jsfiddle) Update: Example without CSS scale (jsfiddle) - correct font scale

This approach would make a presentation fully independent to the users browser window resolution. The translate calls that are done by impress.js for moving the slide/step canvas has to be adopted to not use px but use vw/vh respectively. I played around with the units of the translate property and vw/vh seem to work for translations.

Please add comments for further investigation of this issue.

henrikingo commented 7 years ago

Hi @fin-ger

This is an interesting study you've made. And indeed it seems vw and vh would be very useful units for creating slide shows, since the idea with impress.js is to scale contents of a slide to match screen size.

I think one question I have from reading your above entry: It's possible in impress.js, that you will see content from other slides in the background of the current slide. Example: http://impress.github.io/impress.js/#/its-in-3d They may have a different scale (so different size). How would content on such a slide scale, if it was using vw and vh?

fin-ger commented 7 years ago

@henrikingo

I thought about your input and came up with another proof-of-concept implementation that shows how to implement a vh/vw-based presentation framework.

Proof-of-concept with 3D

I noticed 2 things while implementing this:

  1. The em-based sizing of text leads to jittering while a transition is performed.
  2. As I am using a container which enables % sizing in the slides, a reflow gets triggered on all slides when the size of the container changes. This typically happens when a zoom or slide transition is performed. The result is a bad rendering performance (frame time) when running this approach on a weaker PC.

I know that there are tricks to optimize reflows but I don't know how to apply them (if even possible).

Also the new design forbids the use of px (or the result is highly dependent on the size of the browser window :smile:).

Feel free to play around with the constants - it's quite funny to see how everything automagically adopts (or explodes) :rofl:

fin-ger commented 7 years ago

I added some transparency to the slides and the rendering performance is really bad...

3D vw/vh presentation concept

I think a combination of the transform: scale(...) and the vw/vh solution will give the best results.

During the transition of slides, the old transform: scale(...) should be used for the following reasons:

  1. When using transform: scale(...) to size a slide, only the framebuffer texture of the transformed div gets resized -> no reflow
  2. As the texture of the slide is scaled, the jitter produced by the interpolated font-size when doing a transition with vw/vh, is avoided.

We have to be careful on which DOM element we apply the transformation. What we don't want is one big framebuffer texture that scales all slides as this will consume huge amounts of VRAM which may not be applicable on all PCs. Instead we want multiple smaller framebuffer textures that get updated independently (e.g. per slide).

After the transition of slides the above shown vw/vh approach should be favored instead of transform: scale(...) for the following reasons:

  1. The text rendering result is better as hinting and antialiasing information get preserved.
  2. The slides adapt to the window size.

The challenge is to integrate transform: scale(...) into the vw/vh approach with equal sizing to prevent jumps when the transition ends.

An alternative solution would be to implement the container in a way that no reflow gets triggered e.g. not using top/bottom/left/right/font-size css properties to create a zoom effect which effectively implements the data-scale attribute. I can't think of any solution but maybe someone else has an idea...

EDIT: I forgot to mention that the performance drain (the reflow) only occurs when the data-scale attribute of a slide differs to the previous slide.

henrikingo commented 7 years ago

Thanks for looking into this more. The limitations of scaling are annoying users regularly. (As you can see from above referenced issue about scaling SVG.) It would be great if there was a solution. I think your results are encouraging, although it is not clear to me if a solution along these lines will be feasible to implement in impress.js in a backward compatible way? (I'm really a backend developer - what you're doing is definitively stretching the limits of my knowledge.)

We have to be careful on which DOM element we apply the transformation. What we don't want is one big framebuffer texture that scales all slides as this will consume huge amounts of VRAM which may not be applicable on all PCs. Instead we want multiple smaller framebuffer textures that get updated independently (e.g. per slide).

Isn't this what impress.js currently does? And if we want all slides to be potentially visible during a presentation, isn't it logically necessary to re-scale all of them all the time?

This is the reason bartaz has been hesitant to officially support it on phones, because it could easily run out of RAM. One patch proposed a solution (for mobile devices) to only show the previous, current and next slides to limit RAM consumption. For more advanced presentations, that want other slides to be visible - such as in the background of the current slide - this is quite limiting of course.

fin-ger commented 7 years ago

Isn't this what impress.js currently does?

Yes I think this is what impress.js currently does. The thing that I wanted to point out is that we have to be careful to not remove this feature.

And if we want all slides to be potentially visible during a presentation, isn't it logically necessary to re-scale all of them all the time?

I thought of something like this for a transition from slide A to slide B:

  1. User triggers transition from slide A to slide B
  2. Content reflow to fit dimension of slide B (no transition for top/bottom/left/right/font-size)
  3. Use transform: scale(...) to fit dimension of slide A
  4. Enable transform transitions
  5. Transition to transform: scale(1)

This avoids a reflow for every transition frame but only reflows at the beginning of each transition. During the transition itself the hinting/antialising artifacts will still appear but I think this will barely be noticed by any user.

fin-ger commented 6 years ago

I implemented the above approach

Single reflow per slide transition

Problems that occured while implementing this were:

  1. When changing the global scale on the container element the static perspective leads to a "jump" at the start of the transition. I am now also transitioning the perspective property of the impress element additionally to the scale property on the container. As the perspective does not translate 1:1 to a scale, the transition seems a bit bouncy when using the same transition function on both properties. Currently I cannot think of a solution for eliminating the bouncy perspective transition.
  2. When a small slide (e.g. 10% the size of the largest slide) is shown, other slides that are of "normal" size allocate a huge framebuffer (e.g. 9180x5164 pixels for the first slide when viewing the smallest in my example - ~180MiB VRAM). This is expected behavior as we want the magnified slides to be rendered in native resolution to avoid artifacts. However most slides will eventually be outside the camera frustum and therefore waste tremendous amounts of VRAM. Additionally one could conclude that rendering artifacts on non-active slides are not relevant for most user.

To solve the above problems:

  1. Set display: none to all slides that are completely outside of the camera frustum. This will additionally improve performance when reflowing the content at the beginning of a transition as less slides need a reflow (hidden elements are not reflown). Ideas are welcome for how to detect slides outside of the camera frustum. The simplest (and dumbest) approach would be to do a plane-to-frustum collision on the CPU (js) which will drain the performance on weaker devices even more.
  2. Set a scale property on non-active slides to reduce the framebuffer size. This could be e.g. set to match half of the window resolution.
henrikingo commented 6 years ago

HI @fin-ger

Cool, thanks for sharing. I will try your solution at a later point, as I'm very interested in solving this issue (one way or another).

Related to solution requiring lots of RAM, kind of related is the mobile plugin, which allows you to display: none all but the current, previous and next slides. Of course, that's not what you want here, because you'd probably want some background slide to always be visible. But you could then use additional CSS (div.background) to make that so, and hide the ones you don't need.

Note, it could also be useful to know that impress.js sets a class on body element, which has the name of the active slide. For example, when active slide is "step-1": body.impress-on-step-1.

We're working on merging my stuff into impress.js repo, so expect the mobile plugin to show up some weeks from now.

fin-ger commented 6 years ago

Reduced VRAM

I tried to fix the 2nd problem and ran in the following issues:

When implementing solution 2 I noticed that the mix of height, width, font-size and scale leads to "broken" slides as seen when viewing slide 2 and inspecting the rendering layers in chrome. It seems like the vertices of the plane get distorted... Possibly I'm stretching the capabilities of the rendering engine.

When implementing solution 1 I used the top,left,bottom,right boundaries of the slides (in screen coordinates) to "collide" with the visible area. This only handles the x and y coordinates and is not capable of hiding slides behind the "camera" as z is not taken into account. I could not find a way to get the z boundaries of a DOM element :grin: Additionally I can only evaluate which slide is visible and which not when the transition is finished. This leads to timing issues (transition is done in css, collision in js) and slides will appear in the background when the transition is finished. To prevent the next slide to be hidden when doing a transition (it might not be visible from the previous one) I always show the next slide on transition start.

I think my current implementation will work well for presentations that use small amounts of 3D (e.g. not my example :rofl: ) where slides are not covered by other slides.

Issues that remain:

  1. Timing issues for show/hide of out-of-frustum slides
  2. Browser rendering issues when using complex 3D
  3. Handle z boundaries of slides
fin-ger commented 6 years ago

fyi: I added an SVG to prove that it's fully scalable on any resolution:

SVG example added on slide 3

Edit: I removed the automatic show/hide of slides as this solves 1 and 3. I solved 2 by adjusting the arrangement of the 2nd slide a bit (removed complexity).

SVG without slide show/hide

henrikingo commented 6 years ago

Hi @fin-ger

Thanks for your work on investigating this issue. Partly thanks to following you, I've found out a way to get non-blurry background images also with current impress.js. Here's a demo I did to explain how to make a non-blurred background and the pro's and con's of both ways: http://openlife.cc/blogs/2017/october/impressjs-howto-slides-over-background-image

As you've discovered, having a large background image that's also sharp, will use up HUGE amounts of RAM. In fact, images often failed to load for me. So I'm not sure that we could switch to such a solution in impress.js in general, rather it is better that the user use their own CSS to achieve this if they want to. This way they can also manage when they want to consume that much RAM and when not.

janishutz commented 8 months ago

I do agree with you, so we can close this issue?