Showing posts with label parallax. Show all posts
Showing posts with label parallax. Show all posts

Sunday, November 26, 2017

Changing CSS transform on scroll: jerky movement vs. smooth movement

Leave a Comment

I'm dissatisfied with existing parallax libraries, so I'm trying to write my own. My current one consists of three main classes:

  • ScrollDetector tracks an element's scroll position relative to the screen; it has functions to return a float representing its current position:
    • 0 represents the top edge of the element being at the bottom edge of the viewport
    • 1 represents the bottom edge of the element being at the top edge of the viewport
    • All other positions are interpolated/extrapolated linearly.
  • ScrollAnimation uses a ScrollDetector instance to interpolate arbitrary CSS values on another element, based on the ScrollDetector element.
  • ParallaxativeAnimation extends ScrollAnimation for the special case of a background image that should scroll at a precise factor of the window scroll speed.

My current situation is this:

  • ScrollAnimations using transform: translateY(x) work smoothly.
  • ParallaxativeAnimations using translateY(x) work, but animate jerkily.
  • ParallaxativeAnimations using translate3d(0, x, 0) are jerky, but not as badly.
  • The Rellax library's animations, which use translate3d(0, x, 0), work perfectly smoothly.

You can see the comparison on this pen. (The jerkiness shows up best in Firefox.) My library is on Bitbucket.

I don't know where the problem in my library lies and I don't know how to figure it out. Here is an abridged paste of where the heavy lifting is done while scrolling in the ScrollAnimation class that works smoothly:

getCSSValue(set, scrollPosition) {     return set.valueFormat.replace(set.substitutionString, ((set.endValue - set.startValue) * scrollPosition + set.startValue).toString() + set.unit) }  updateCSS() {     var cssValues = [];      var scrollPosition = this.scrollDetector.clampedRelativeScrollPosition();      var length = this.valueSets.length;     for(var i = 0; i < length; i++) {         cssValues.push(getCSSValue(valueSets[i], scrollPosition) );     }      this.setCSS(cssValues);     this.ticking = false; }  requestUpdate() {     if(!this.ticking) {         requestAnimationFrame(() => { this.updateCSS(); });     }      this.ticking = true; } 

And here's the equivalent in the ParallaxativeAnimation class that is jerky:

updateCSS() {     var scrollPosition = this.scrollDetector.clampedRelativeScrollPosition();     var cssValues = [];      var length = this.valueSets.length;     for(var i = 0; i < length; i++) {         var scrollTranslate = -((this.scrollTargetSize - this.valueSets[i].parallaxSize) * scrollPosition);          cssValues.push(             this.valueSets[i].valueFormat.replace(this.valueSets[i].substitutionString, scrollTranslate.toString() + 'px')         );     }      this.setCSS(cssValues);     this.ticking = false; }  requestUpdate() {     if(!this.ticking) {         requestAnimationFrame(() => { this.updateCSS(); });     }      this.ticking = true; } 

The math doesn't seem any more complicated, so I can't figure how that's affecting animation performance. I thought the difference might have been my styling on the parallax image, but in the pen above, the Rellax version has the exact same CSS on it, but animates perfectly smoothly. Rellax seems to maybe be doing more complicated math on each frame:

var updatePosition = function(percentage, speed) {   var value = (speed * (100 * (1 - percentage)));   return self.options.round ? Math.round(value) : Math.round(value * 100) / 100; };   // var update = function() {   if (setPosition() && pause === false) {     animate();   }    // loop again   loop(update); };  // Transform3d on parallax element var animate = function() {   for (var i = 0; i < self.elems.length; i++){     var percentage = ((posY - blocks[i].top + screenY) / (blocks[i].height + screenY));      // Subtracting initialize value, so element stays in same spot as HTML     var position = updatePosition(percentage, blocks[i].speed) - blocks[i].base;      var zindex = blocks[i].zindex;      // Move that element     // (Set the new translation and append initial inline transforms.)     var translate = 'translate3d(0,' + position + 'px,' + zindex + 'px) ' + blocks[i].transform;     self.elems[i].style[transformProp] = translate;   }   self.options.callback(position); }; 

The only thing I can really tell from Chrome Developer Tools is that the framerate isn't dipping too far below 60 fps, so maybe it's not that I'm doing too much work each frame, but that I'm doing something mathematically incorrect when I calculate the position?

So I don't know. I'm clearly in way over my head here. I'm sorry to throw a whole library at StackOverflow and say "FIX IT", but if anyone can tell what I'm doing wrong, or tell me how to use Developer Tools to maybe figure out what I'm doing wrong, I'd appreciate it very much.


EDIT

Okay, I've figured out that the most important factor in the jitteriness of the scrolling is the height of the element being translated. I had a miscalculation in my library that was causing the background images to be much taller than they needed to be when my scrollPixelsPerParallaxPixel property was high. I'm in the process of trying to correct that now.

2 Answers

Answers 1

Aside from the calculations, you could try running it asynchroneously by using Promise:

await Promise.all([   loop(update); ]); 

just to see if it has a positive impact on the performance.

I'd comment, but I don't have enough reputation yet.

Answers 2

Anything touching the DOM will be slow. CSS animations are fine, but if you update the CSS you are touching the DOM and it will be slow. Consider using a canvas element instead!

Read More

Monday, September 4, 2017

Horizontal CSS only parallax effect with layers greater than 100vw

Leave a Comment

How to bootstrap a site with horizontal CSS only parallax effect?

Requirements

  • CSS only parallax
  • parent layer must have width/height == 100vw/100vh
  • some child layers must have width/height > 100vw/100vh
  • after srcolling to the right bottom corner all child layers must end at the same right and bottom position which must be at the right bottom corner of the browsers window
  • all child layers (except the first) must have a top offset relative to its parent
  • on all phases of scrolling there must be no gap between all layers and and the right-, bottom-, and left edge of the browser window
  • results must base on calculations to have maximum flexibility
  • must be cross browser solid (at least newest version of majors)

enter image description here


What I have done so far

Actually this question is a follow-up question.
Here's a PEN with my current mockup state in SASS or CSS.

Working Simulated Example (jQuery)

In javascript its quite simple to achieve what I'm looking for. So here is a PEN that simulates the effect I'd like to do with CSS.

Already known Issues

The issue I'm most concerned about by now is the fact, that browser seem to render this scenario differently. See screenshot of browser window (chrome vs ff) scrolled to the right bottom corner below. But I hope this could be avoided.

enter image description here


There are so many parallax tuts out there - why is this different?

Actually I researched really a lot but didn't find not even one description how to implement horizontal parallax (means the child layers have a width > 100vw). Of course there are horizontal parallax scroll tuts out there. But they all have one in common: the child layer widths are always <= 100vw - and thats actually the difference.


html,  body {    height: 100%;    overflow: hidden;    width: 100%;  }    body {    -webkit-transform: translateZ(0);    transform: translateZ(0);  }    #projection {    -webkit-perspective: 1px;    perspective: 1px;    -webkit-perspective-origin: 0 0;    perspective-origin: 0 0;    height: 100%;    overflow: auto;    width: 100%;  }    .pro {    -webkit-transform: scale(1) translate(0px, 0px) translateZ(0px);    transform: scale(1) translate(0px, 0px) translateZ(0px);    height: 100%;    position: absolute;    -webkit-transform-origin: 0 0;    transform-origin: 0 0;    -webkit-transform-style: preserve-3d;    transform-style: preserve-3d;    width: 100%;  }    .pro--1 {    -webkit-transform: scale(4) translate(0px, 0px) translateZ(-3px);    transform: scale(4) translate(0px, 0px) translateZ(-3px);    width: 110%;  }    .pro--2 {    -webkit-transform: scale(3) translate(0px, 1em) translateZ(-2px);    transform: scale(3) translate(0px, 1em) translateZ(-2px);    width: 110%;  }    .pro--3 {    -webkit-transform: scale(2) translate(0px, 2em) translateZ(-1px);    transform: scale(2) translate(0px, 2em) translateZ(-1px);    width: 110%;  }    .pro {    background: rgba(0, 0, 0, 0.33);    box-shadow: inset 0 0 0 5px orange;    color: orange;    font-size: 4em;    line-height: 1em;    text-align: center;  }    .pro--2 {    box-shadow: inset 0 0 0 5px green;    color: green;  }    .pro--3 {    box-shadow: inset 0 0 0 5px blue;    color: blue;  }
<div id="projection">    <div class="pro pro--1">pro--1</div>    <div class="pro pro--2">pro--2</div>    <div class="pro pro--3">pro--3</div>  </div>

0 Answers

Read More

Tuesday, August 22, 2017

formular to calculate width/height (relative to parent) of container with translateZ inside of parent container with perspective

Leave a Comment

What is the formular to calculate the widths/heights of child elements with translateZ inside of parent container with set perspective (keyword: "parallax") relative to its parents width/height?

I'd like to create a site with parallax effect on both axis. I was able to figure out everything i need for my mockup except one thing. How to calculate the childrens widths/heights when its above 100%. Because of parents perspective and childrens translateZ the childrens widths/heights visually don't align with parents width/height anymore.

The formular to scale the child elements is: 1 + (translateZ * -1) / perspective. But i was not able to find a formular for width/height. BTW: When childrens widths/heights <= 100% everything works fine.
But see the result on the image below when width >= 100% (containers have top offset to make things visible).

enter image description here

To be correct the approach in my particular case is to let all child elements have visually the same widths/heights.


in SASS (preferred): PEN or SassMeister
in CSS: PEN


links from the specs that could help:
https://www.w3.org/TR/css-transforms-1/#recomposing-to-a-3d-matrix
https://www.w3.org/TR/css-transforms-1/#mathematical-description


"Googled" a lot but didn't find anything pointing me to the right direction. Thanks in advance...

html, body {    height: 100%;    overflow: hidden;    width: 100%;  }    #projection {    perspective: 1px;    perspective-origin: 0 0;    height: 100%;    overflow: auto;    width: 100%;  }    .pro {    transform: scale(1) translate(0px, 0px) translateZ(0px);    height: 100%;    position: absolute;    transform-origin: 0 0;    transform-style: preserve-3d;    width: 100%;  }    .pro--1 {    transform: scale(4) translate(0px, 0px) translateZ(-3px);    width: 110%;  }    .pro--2 {    transform: scale(3) translate(0px, 50%) translateZ(-2px);    width: 110%;  }    .pro--3 {    transform: scale(2) translate(0px, 100%) translateZ(-1px);    width: 110%;  }    .pro {    background: #333;    box-shadow: inset 0 0 0 5px orange;    color: orange;    font-size: 4em;    line-height: 1em;    text-align: center;  }    .pro--2 {    background: rgba(75, 75, 75, 0.5);    box-shadow: inset 0 0 0 5px green;    color: green;    line-height: 4em;  }    .pro--3 {    background: rgba(75, 75, 75, 0.5);    box-shadow: inset 0 0 0 5px white;    color: white;    line-height: 7em;  }
<div id="projection">    <div class="pro pro--1">pro--1</div>    <div class="pro pro--2">pro--2</div>    <div class="pro pro--3">pro--3</div>  </div>

SASS

@mixin  projection($translateZ: 0, $translateX: 0, $translateY: 0, $width: 0, $height: 0, $perspective: $perspective)    // strip and sanitize units for further calculations   // units must be "px" for both $translateZ and $perspective   $unit: unit( $translateZ )   @if '' != $unit     $translateZ: $translateZ / ($translateZ * 0 + 1)     @if 'px' != $unit       @warn '$translateZ must have "px" as unit!'    $unit: unit( $perspective )   @if '' != $unit     $perspective: $perspective / ($perspective * 0 + 1)     @if 'px' != $unit       @warn '$perspective must have "px" as unit!'    $unit: 0px // yeah - technically this is no unit    // calculate scaling factor   $scale: 1 + ($translateZ * -1) / $perspective    // sanitize units for translateX, translateY, translateZ   $translateZ: $translateZ + $unit   @if unitless( $translateX )     $translateX: $translateX + $unit   @if unitless( $translateY )     $translateY: $translateY + $unit    // render css "transform: scale() translate(x, y) translateZ()"   transform: scale( $scale ) translate($translateX, $translateY) translateZ( $translateZ + $unit )  $width: 110% // 100% works like a charme $translateZ--1: -3 // "px" will be added in mixin $translateZ--2: -2 $translateZ--3: -1 $perspective: 1  html, body   height: 100%   overflow: hidden   width: 100%  #projection   perspective: $perspective + 0px   perspective-origin: 0 0   height: 100%   overflow: auto   width: 100%  .pro   @include projection()   height: 100%   position: absolute   transform-origin: 0 0   transform-style: preserve-3d   width: 100%  .pro--1   @include projection( $translateZ--1 )   width: $width  .pro--2   @include projection( $translateZ--2, 0, 50% )   width: $width  .pro--3   @include projection( $translateZ--3, 0, 100% )   width: $width 

2 Answers

Answers 1

You've already solved your problem. Your code does exactly what you need it to do, it's just a CSS layout issue now.

https://codepen.io/anon/pen/xLWGzp?editors=0100

Because of the perspective changes, if you hang everything off the x-axis center everything will begin to line up properly:

(I'm just adding in the code changes here, I've left everything else the same)

#projection     perspective-origin: center top  .pro     transform-origin: center top 

Now everything's lining up better, but it's still a bit off - you can change the $width variable to anything other than 100% to see the problem (60% is a good one)

So the problem now is just due to the positioning of the elements, when you set position: absolute they're default positioned to the left, change the width and add scale and transform and you get this equal-width/not-equal-position, so center them by adding:

#projection     position: relative  .pro     left: 50%     margin-left: $width * -.5 

(info here as to why that works to center: https://css-tricks.com/quick-css-trick-how-to-center-an-object-exactly-in-the-center/)

So now jiggle $width around to double-check, I tested it from 20% up to 150% and it works fine.

Answers 2

I have changed the style slightly, to make things more visible.

The result seems ok for me. May be I am misunderstanding something ?

html, body {    height: 100%;    overflow: hidden;    width: 100%;  }    #projection {    perspective: 1px;    perspective-origin: 0 0;    height: 50%;    overflow: visible;    width: 50%;    margin-left: 50px;    background-color: grey;  }    .pro {    transform: scale(1) translate(0px, 0px) translateZ(0px);    height: 50%;    position: absolute;    transform-origin: 0 0;    transform-style: preserve-3d;    width: 100%;  }    .pro--1 {    transform: scale(4) translate(0px, 0px) translateZ(-3px);    width: 110%;  }    .pro--2 {    transform: scale(3) translate(0px, 120%) translateZ(-2px);    width: 110%;  }    .pro--3 {    transform: scale(2) translate(0px, 240%) translateZ(-1px);    width: 110%;  }    .pro--1 {    background: rgba(0, 0, 75, 0.5);    color: blue;    line-height: 1em;    text-align: center;  }    .pro--2 {    background: rgba(0, 75, 0, 0.5);    color: green;    line-height: 4em;  }    .pro--3 {    background: rgba(75, 0, 0, 0.5);    color: red;    line-height: 7em;  }
<div id="projection">    <div class="pro pro--1">pro--1</div>    <div class="pro pro--2">pro--2</div>    <div class="pro pro--3">pro--3</div>  </div>

Read More

Thursday, May 5, 2016

Parallax - Offset element(s), tied to scroll

Leave a Comment

Banging my head trying to sort out the correct logic for adding simple parallax behavior.

I would like to have a number of elements on a page which start out with their top offset a certain distance (e.g. 300px). Then as you scroll down the page, once the top of the element is revealed it will slowly shift upwards (tied to scroll) until the top of element reaches middle of viewport at which time it's top offset is 0 and it remains in place.

I tried using third party script (Scroll Magic, Stellar, etc), but when I couldn't get it right now I'm trying custom code:

https://jsfiddle.net/louiswalch/5bxz8fku/1/

var $Window = $(window); var offset_amount = 400; var window_height = $Window.height(); var window_half   = (window_height/2); var sections      = $('SECTION.reveal');  sections.each(function() {    var element = $(this);    // Make sure we always start with the right offset   element.css({top: offset_amount});    $Window.bind('scroll', function() {      var viewport_top    = $Window.scrollTop();     var viewport_middle = viewport_top + (window_height/2)     var viewport_bottom = viewport_top + window_height;     var element_top     = element.offset().top;      if (element_top > viewport_top &&  element_top <= viewport_bottom) {        var distance_to_middle  = (element_top - viewport_middle);       var amount_to_middle    = (distance_to_middle / window_half);        console.log(amount_to_middle);        if (amount_to_middle >= 0) {         element.css({top: (offset_amount * amount_to_middle)+ 'px'});       } else {         // ? Lock to end position ?       }      }    });  }); 

2 Answers

Answers 1

jsBin demo 1. (margin space effect on both enter and exit)
jsBin demo 2. (preserve 0 margin once touched)

Instead of targeting the section elements, (create and) target their first child elements,
otherwise you'll create a concurrency mess trying to get the top position but simultaneously modifying it.

Also, you cannot rely on fixed 300px margin (i.e: if window height is less than 500px, you're already missing 100px). That space can vary when the screen height is really small, so you also need to find the idealMarg value.

var $win = $(window),     $rev = $('.reveal'),     winH2 = 0,     winSt = 0;  function reveal() {    winSt = $win.scrollTop();   winH2 = $win.height()/2;    $rev.each(function(i, el){     var y = el.getBoundingClientRect().top,         toMiddleMax = Math.max(0, y-winH2),         idealMarg   = Math.min(300, toMiddleMax),         margMin     = Math.min(idealMarg, idealMarg * (toMiddleMax/winH2));     $(">div", this).css({transform: "translateY("+ margMin +"px)"});   });  }  $win.on({"load resize scroll" : reveal}); 
*{box-sizing:border-box; -webkit-box-sizing:border-box;} html, body{height:100%; margin:0;}  section > div{   padding: 40px;   min-height: 100vh; } 
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>  <section>   <div style="background-color:red">1</div> </section> <section class="reveal">   <div style="background-color: yellow">2</div> </section> <section class="reveal">   <div style="background-color: orange">3</div> </section> <section class="reveal">   <div style="background-color: pink">4</div> </section> 

I've used in HTML just a <div> logically, that has to be the one and only first child of a section parent.
You're welcome to tweak the above code to make it more performant.

Answers 2

Hey so here is my go at an awnser.

http://jsbin.com/wibiferili/edit?html,js,output

The jist of it is as follows.

JS

var $Window = $(window),     parallaxFactor = 2;  $('.parallaxblock').each(function(a,b){     var element = $(b);     element.css("top",element.data("pOffset") + "px");      $Window.bind('scroll', function() {         var pos =              // Base Offset             element.data("pOffset")             // parallaxFactor             - ($Window.scrollTop() / parallaxFactor);          pos = pos < 0 ? 0 : pos;          element.animate({"top": pos + "px"},10);          return;     });  }); 

Styles

body{   height: 4000px; }  .parallaxblock{   position:fixed;   background:#999;   opacity:.5; } 

Example Usage

<div class="parallaxblock" data-p-offset=100>Im A Block</div> <div class="parallaxblock" data-p-offset=200>Im Also Block</div> <div class="parallaxblock" data-p-offset=1500>Im Another Block</div> 

So by checking the offest its never lower then 0 we can lock it at the top of the screen once it reaches it.

I get the offset amount of the data tag on the div.

If you wanted to change the rate of scroll in different posistions you could change the parallax factor at a certain percentage of screen height.

Hope this helps.

Read More

Wednesday, April 27, 2016

Parallax effect on each item in a recycler view?

Leave a Comment

I'm trying to play with Parallax and getting some weird bugs, wondering if anyone can add some input to it. The only app I've seen implement parallax effectively is soundcloud. It's quite subtle, but each item has an image background and it has he parallax effect as you scroll.

I've created a custom RecyclerView to handle this, here is what I have so far:

public class ParallaxScrollListener extends RecyclerView.OnScrollListener {  private float scrollSpeed = 0.5f;  @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) {     super.onScrolled(recyclerView, dx, dy);     LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();      int firstVisible = layoutManager.findFirstVisibleItemPosition();     int visibleCount = Math.abs(firstVisible - layoutManager.findLastVisibleItemPosition());      Matrix imageMatrix;     float tempSpeed = -100;      if (dy > 0) {         tempSpeed = scrollSpeed;     } else if (dy < 0) {         tempSpeed = -scrollSpeed;     }      for (int i = firstVisible; i < (firstVisible + visibleCount); i++) {         ImageView imageView = ((MyClass.MyAdapter.MyViewHolder) recyclerView.getLayoutManager().findViewByPosition(i).getTag()).image;         if (imageView != null) {             imageMatrix = imageView.getImageMatrix();             imageMatrix.postTranslate(0, tempSpeed);             imageView.setImageMatrix(imageMatrix);             imageView.invalidate();         }     } } 

In my RecyclerView Adapter's onBindView I have the following as well:

 Matrix matrix = viewHolder.image.getImageMatrix();  matrix.postTranslate(0, 0);  viewHolder.image.setImageMatrix(matrix);  viewHolder.itemView.setTag(viewHolder); 

Finally inside the onViewRecycled method I have the following:

@Override     public void onViewRecycled(MyViewHolder viewHolder) {         super.onViewRecycled(viewHolder);         if (viewHolder.image != null) {             viewHolder.image.setScaleType(ImageView.ScaleType.MATRIX);             Matrix matrix = viewHolder.image.getImageMatrix();             // this is set manually to show to the center             matrix.reset();             viewHolder.image.setImageMatrix(matrix);         } } 

I been working with this code on Github to get the idea

So the parallax works, but but views in my RecyclerView move as well. I have a CardView beneath the image and it moves, creating big gaps between each item. Scrolling is what causes this, the more the scroll up and down the bigger the gaps get, and the images get smaller as the parallax moves them out of their bounds.

I've tried messing with the numbers like scrollSpeed in the OnScrollListener but while it reduces the bug it also reduces the parallax.

Has anyone got any ideas on how I can achieve a bug free parallax effect on each item in my RecyclerView? I feel like I'm getting somewhere with this but it's still very buggy and I don't know what the next step is.

P.s I've tried looking at 3rd party libraries but they all seem to only use header parallax like the CoordinatorLayout, I haven't found any that do it just on each item in a list.

I'm hoping this question gets a good discussion going even if I don't solve my problem because Parallax seems to be underused in Android and there's very little around about it.

Thanks for you time, appreciate any help.

2 Answers

Answers 1

I managed to get Parallax working with this library: https://github.com/yayaa/ParallaxRecyclerView

For anyone doing this themselves, it's still a good thing to play and see how it works.

Similar concept to my code but it actually works! haha.

Answers 2

You are on the right track. You have to use a ScrollListener. Furtermore, you have to access RecyclerViews LayoutManager and iterate over all items that are visible and set translateY according to the amount of pixels scrolled.

The things get a little bit more complicated, because you can't use recyclerView.getChildAt(pos) because LayoutManager is responsible to layout elements and they might be in different order in the LayoutManager than getChildAt(pos).

So the algorithm basically should look like this (pseudo code, assuming LinearLayoutManager is used):

for (int i = layoutManager.findFirstVisibleItemPosition(); i <= layoutmanager.findLastVisibleItemPosition; i++){     // i is the adapter position     ViewHolder vh = recyclerView.findViewHolderForAdapterPosition(i);    vh.imageView.setTranslationY( computedParalaxOffset ); // assuming ViewHolder has a imageView field on which you want to apply the parallax effect } 
Read More