Smoother fixed float - javascript

I have created a spirit level for my new site.
I used the fixed float idea but tried to limit it to the realm of the tube. It's not perfect but it's aided by the shortness of the page.
How could I make it smoother (particularly when scrolling slowly)?
Also, when I scroll with my iPhone, it works but only after I finish scrolling and not as I'm scrolling. Is this just a limitation of the iPhone's scrolling mechanism or is there a way around that?
The HTML:
<div id="spirit_level">
<div id="shimmery"></div> <!-- just for y gradient -->
<div id="shimmerx"></div> <!-- just for x gradient -->
<div id="bumps"></div> <!-- just for another overlay -->
<div id="tube">
<div id="bubble"></div>
<div id="overside"></div> <!-- glass + line markings -->
</div>
</div>
<div id="spirit_shadow"></div>
The CSS:
The spirit level is placed using fixed positioning, and everything inside is positioned absolutely (relative to the spirit level).
The Javascript:
/* START: init spirit_level/bubble */
var bubble_h = 53, tube_h = 242,
doc_h = parseInt($(document).height()),
viewport_h = parseInt($(window).height()),
scrollDepth = $(document).scrollTop(),
orig_scrollDepth = scrollDepth,
tube_top = viewport_h/2 - tube_h/2,
center = 0;
/*center the tube and bubble:
$('#tube').css('top', tube_top+'px')
.addClass('bubble_prep');
placeBubble(1);
/* END: init spirit_level/bubble */
$(window).unbind("scroll").scroll(function () {
placeBubble(0);
})
The placeBubble() function:
function placeBubble(first_time)
{
scrollDepth = $(document).scrollTop();
if(first_time)
{
$('#bubble').css('top', center + 'px');
}
temp = ((center - (scrollDepth - orig_scrollDepth))/viewport_h)*100;
$('#bubble').css('top', (temp<-50?-50:(temp>50?50:temp)) +'%')
.animate(
{top: (center/viewport_h)*100+'%'},
{duration:800,queue:false},
{
step: function(now, fx) {
//this is never called, don't know why
alert(now);
}
}, function(){
/*
Should I do the following?
orig_scrollDepth = $(document).scrollTop();*/
});
/*Without the next line, the bubble slides down from the top of the
tube on any length scroll in any direction*/
orig_scrollDepth = $(document).scrollTop();
}
}
Edit:
Wow, I just checked with a Samsung Galaxy S1 (standard web browser). The z-indexing and absolute positioning of the measuring tape are disastrously failing. Why is this?

Maybe your use of .css is making it jumpy? .animate could be a suitable replacement, with a very fast setting so that it smoothly glides to the temporary starting position, then slowly glides back to the center afterward.
$('#bubble').animate({'top': (temp<-50?-50:(temp>50?50:temp)) +'%'},{duration:200})
.animate( [...]

Related

How do I calculate how far down a div I'm scrolling based on the viewport and the div's position?

I'm building a blog with a system to see how deep in an article the visitor is currently in, the result is in percentage.
To simplify some elements, I'm tracking the bottom border of the browser and comparing it to the article's actual position. The page itself is composed of a banner with some items inside (title, metadata, etc), then the article itself is displayed under it, but the banner's height is 60vh. The article has no set height, and no overflow: scroll property.
This is how I'm currently doing it:
const getScrollPercent = () => {
const prose = document.querySelector<HTMLDivElement>('.prose')
if (prose) {
// closest way I got the actual div's top side)
const proseTop = prose.getBoundingClientRect().top + document.documentElement.scrollTop
const proseBottom = proseTop + prose.offsetHeight
const browserBottom = window.scrollY + window.innerHeight
if (browserBottom < proseTop) {
return 0
} else if (browserBottom > proseTop && browserBottom < proseBottom) {
return (browserBottom / proseBottom) * 100
} else {
return 100
}
} else {
return 0
}
}
However, the behavior is a bit off: As long as I don't have the .prose div in my viewport, it displays 0 percent, which is completely ok. However, at the first pixel, the percentage jumps to ~24%, and then it finishes smoothly at 100% once reaching the div's bottom.
I don't really know if it's my way of calculing the div's height that's not good, or how I calculate the progressing percentage.
Edit: Html structure
(It's a Nuxt/VueJS context)
<nav>
<!-- Contains some links-->
</nav>
<main>
<header>
<h1>Some article title with a banner in background</h1>
<div class="meta">
<!-- Some meta infos (tags, creation date, etc) -->
</div>
</header>
<div class="progress">
<span class="global">
{{ getScrollPercent }}%
</span>
</div>
<article>
<!-- lots of <p> and <h2> to <h6> tags, about 2200px high) !-->
</article>
</main>
Dividing by proseBottom is not the right way to do this. If proseTop is 5000, height is 200, proseBottom is going to be 5200. So even when browser bottom is at 5000, you are going to be at 5000/5200 percent, which isn't what you want (right?)
I think what you want is
(proseTop - browserBottom) / proseHeight * 100;
This is how you calculate the percentage of the prose field that is off screen below
You need to get the reference of your div node first.
in react most likely use useRef
const yourDivRef = useRef()
//then in your actual div
<div ref={yourDivRef}/>
then you could calculate ur position with code below
const node = yourDivRef.current
const offsetHeight = node.scrollHeight // this is about how height your ref div is
const innerHeight = node.clientHeight // this is how height your browser window is
const scrollTop = node.scrollTop // this is where you scroll position at
for example: if you want to get if you scroll reach the bottom you do
// check if scroll reach within 5px to the bottom
const hasReachedBottom = offsetHeight - (innerHeight + scrollTop) <= 5
so as your asked:
(innerHeight + scrollTop) / offsetHeight
should be the percentage of position where you at in your div
Also don't forget to init the scroll Event listener.
useEffect(() => {
// handleScroll should be the place you do all the math.
listenTarget.addEventListener('scroll', handleScroll)
return () => listenTarget.removeEventListener('scroll', handleScroll)
}, [node])
I hope you find this is answer your question.

Sticky sidebar from bottom during scrolling

I'm planning to recreate the Medium.com like sidebar. This is what I have now but it's far from over.
Open the JSFiddle below to understand better; I am looking to do the following:
When you scroll down, it suddenly sticks to the bottom. How do I make it stick gradually as you scroll?
Because it uses position: fixed, it moves towards the right side without respecting the layout. How do I fix this?
When you scroll up and there's less space, it overlaps with the header as shown in the screenshot. Again, most likely because position: fixed is used.
How do I fix this? I know there's sticky-kit that does the work but I can't use any plugin.
HTML:
<div class="container">
<div class="row">
<div class="col-xs-12">
Header and Menu is placed here.
<hr />
</div>
<div class="col-xs-8">
<p>This is the main body which the user would be scrolling infinitely as more data gets loaded on scroll.</p>
</div>
<div class="col-xs-4">
<div id="sidebar">
<p>
This is the sidebar which I want to stop when it reaches the bottom like the one shown in medium dot com
</p>
</div>
</div>
</div>
</div>
Javascript:
function scrollCheck() {
var $right = $('#sidebar'),
scrollTop = $(window).scrollTop(),
windowHeight = $(window).height(),
docHeight = $(document).height(),
rightHeight = $right.height(),
distanceToTop = rightHeight - windowHeight,
distanceToBottom = scrollTop + windowHeight - docHeight;
if (scrollTop > distanceToTop) {
$right.css({
'position': 'fixed',
'bottom': (scrollTop + windowHeight > docHeight ? distanceToBottom + 'px' : '0px')
});
}
else {
$right.css({
'position': 'relative',
'bottom': 'auto'
});
}
}
$(window).bind('scroll', scrollCheck);
JSFIDDLE
I'll answer what questions of yours I could. Here is the edited Fiddle first.
As for your questions:
The sudden sticking to the bottom is caused because the element isn't the full length of the page so sticking it to the bottom with a fixed position will cause it to jump there. Whereas with the change I made so it sticks to the top there won't have this jump, as the element is at the top of the screen when scrolling so it can be discreetly fixed there.
This was because the element didn't have a fixed width and setting position: fixed; means that the elements width is no longer set by the parent, but the view port. So it expands to fill all the width in the view port it can.
This was happening because the position: fixed; was never removed when scrolling back above the elements original position, the updated if statement in the Js now removes the class with position: fixed; when scrolling above its original location.
A more detailed look into what I changed.
I added a CSS class so .toggleClass could be used to make the function cleaner.
.docked-sidebar {
position: fixed;
top: 0;
}
I also changed the conditions for the if statement so that they worked. Using .offset().top; to get the distance between the sidebar and the top of the page, while removing most of the other variables as they weren't needed. Finally I created bool variable(isDocked) so that the same condition isn't triggered multiple times.
var $right = $('#sidebar'),
isDocked = false,
initOffsetTop = $right.offset().top;
function scrollCheck() {
var scrollTop = $(window).scrollTop(),
offsetTop = $right.offset().top;
if (!isDocked && ((offsetTop - scrollTop) < 0)) {
$right.toggleClass("docked-sidebar");
isDocked = true;
} else if (isDocked && scrollTop <= initOffsetTop) {
$right.toggleClass("docked-sidebar");
isDocked = false;
}
}
For sticking to the bottom and then to the top exactly like the example website I recommend checking this question.

Javascript generated margin-top messes up placement content

Case:
I have a slider that scrolls through some images, while, when you scroll down, the menu and content moves over it. Once the menu gets to the top, it sticks to it, as it's changed to a fixed position.
Problem:
Once the menu snaps to place, it resets it's original position (a margin-top) from a generated ammount of pixels to the 0 value. This cases the page to jump that generated ammount of pixels down, which shouldn't happen. It shouldn't jump down at all, but I think it has to do with the ammount of pixels that is generated before it is set to 0, which cases on my screen a gap of 955 pixels. It jumps thus 955 pixels down after it applies the fixed state.
So my questio now is, how can i fix this. I tried applying instead of a margin a padding (no go, white screen), applying instead of a margin-top: 0px a top:0 so i dont have to use the margins, but also a no go.
Case link:
http://test.thewebfanatics.com/jellyweb/home
Code
$(window).scroll(function () {
if ($('.resolutionwrap').length == 1) {
var documentScrollTop = $(document).scrollTop() + 100;
var fixedToggle = $('#slides').height();
if (documentScrollTop > fixedToggle) {
$('#hoofdmenu').addClass('fixed');
$('#hoofdmenu').css("margin-top", "0px");
} else {
$('#hoofdmenu').removeClass('fixed');
$('#hoofdmenu').css("margin-top", $('#slides').height() - 100);
}
}
});
Hope someone can help me on this matter.
Okay, as I was posting the remark on the fiddle, I realized that if the content moved, I could also just simply return it to it's position by javascript by making a counter balanced value of it.
Shortly said: I countered the margin, by creating a different margin-top that balanced that scale. It's maybe not the most beautifull solution, but it did the trick.
$(window).scroll(function () {
if ($('.resolutionwrap').length == 1) {
var documentScrollTop = $(document).scrollTop() + 100;
var fixedToggle = $('#slides').height();
// console.log($('#slides').height());
// console.log($('.resolutionwrap').height());
if (documentScrollTop > fixedToggle) {
$('#hoofdmenu').addClass('fixed');
$('#hoofdmenu').css("margin-top", "0px");
$('.content').css("margin-top", $('#slides').height());
} else {
$('#hoofdmenu').removeClass('fixed');
$('#hoofdmenu').css("margin-top", $('#slides').height() - 100);
$('.content').css("margin-top", "0px");
}
}
})

Fixing Jank on Movement locked to Scroll on Android

I am creating a header that acts like the Chrome for Android Address bar. The effect is that the header is a pseudo sticky header that scrolls out of view as you scroll down and then you you begin to scroll back up the header scrolls back into view.
Right now it works fine on the desktop (around 60fps) but on Chrome for Android (on Nexus 7 2013) it is full of jank.
Demo: jsFiddle
Both the header and content area are moved with transform translateY which are more performant than pos:top
I am also using requestAnimationFrame to debounce scrolling and only change properties when it is most convenient for the browser.
The header is position: fixed; top: 0; and then scrolled in and out of view with transform: translateY(...);. Also instead of using margin-top to get the content out from underneath the header, I am using transform: translateY(...);
The basic structure of my js looks like:
var latestScrollTop = 0;
var lastReactedScrollTop = 0;
var ticking = false;
function DoScroll()
{
var builtUpScrollTop = latestScrollTop - lastReactedScrollTop;
// Fold the top bar while we are scrolling (lock it to scrolling)
$('header.main-header').css('transform', 'translateY(' ... 'px)');
HeaderHeightChange();
lastReactedScrollTop = latestScrollTop;
ticking = false;
}
function HeaderHeightChange()
{
// We need to update the margin-top for the content so we don't overlap it
$('main.content-area').css('transform', 'translateY(' ... 'px)');
}
function requestTick() {
if(!ticking) {
requestAnimationFrame(function(){
DoScroll();
});
}
ticking = true;
}
$(window).on('scroll', function(e) {
latestScrollTop = $(window).scrollTop();
requestTick();
});
The effect is not complete as it needs to resolve the fold after you finish scrolling (and is coded) but I do not want to complicate the issue when just the scroll movement lock to header is causing jank. I see paint rectangles when scrolling up and down even though I am changing transform which I assume the gpu is handling and shouldn't be painting.
Edit: It seems when debugging with ADB that there is a a bunch of clear grey outlined time in each frame.
Turns out that even though I was using transform: translateY() that you still need to add translateZ(0) to see the benefit of layers and having it gpu accelerated.
But I did also update my code to use a object literal code style and got rid of the forced synchronous layout warning in the timeline by reading then writing. This is coupled along with requestAnimationFrame.
Demo: jsFiddle
var myUtils = {
clamp: function(min, max, value) {
return Math.min(Math.max(value, min), max);
},
getTranslateYFromTransform: function(rawTransform) {
return parseFloat(rawTransform.match(/^matrix\((([+-]?[0-9]*\.?[0-9]*),\s*?){5}([+-]?[0-9]*\.?[0-9]*)\)$/)[3])
}
};
var scrollHeader = {
latestScrollTop: 0,
lastReactedScrollTop: 0,
headerHeight: 0,
headerTransformTranslateY: 0,
ticking: false,
requestTick: function() {
if(!scrollHeader.ticking) {
requestAnimationFrame(function(){
scrollHeader.doHeaderFold();
});
}
scrollHeader.ticking = true;
},
doHeaderFold: function() {
var header = $('header.main-header');
var builtUpScrollTop = scrollHeader.latestScrollTop - scrollHeader.lastReactedScrollTop;
scrollHeader.headerHeight = header.outerHeight();
scrollHeader.headerTransformTranslateY = myUtils.clamp(-parseInt(scrollHeader.headerHeight), 0, (myUtils.getTranslateYFromTransform(header.css('transform')) - builtUpScrollTop));
// Fold the top bar while we are scrolling (lock it to scrolling)
header.css('transform', 'translateY(' + scrollHeader.headerTransformTranslateY + 'px) translateZ(0)');
scrollHeader.headerHeightChange();
scrollHeader.lastReactedScrollTop = scrollHeader.latestScrollTop;
scrollHeader.ticking = false;
},
headerHeightChange: function() {
// We need to update the margin-top for the content so we don't overlap it
$('main.content-area').css('transform', 'translateY(' + (scrollHeader.headerHeight + scrollHeader.headerTransformTranslateY) + 'px) translateZ(0)');
}
};
$(window).on('scroll', function(e) {
//console.log(e);
scrollHeader.latestScrollTop = $(window).scrollTop();
scrollHeader.requestTick();
});
This makes the timeline debugging on ADB (Nexus 7 2013) look like(very smooth):
Also to get rid of a small jump when first scrolling add transform: translateZ(0) to your element before animating it.

Infinite scrolling in both directions - Up and down

I am trying to create a page that is an endless scrolling loop both up and down.
At the moment I am using jquery to relocate content from the top of the page to the bottom. This creates a nice seemless loop when you scroll down but I would like it to work when the user scrolls up too.
The problem seems to be that even if content is positioned in negative vertical space on the page the scroll will not extend to that space. As far as I am aware there is no way to override this so I am looking for some type of work around.
I have thoughts of using javascript to disable the scrolling and using the scroll event to reposition the elements but there are already lots of absolute positioned elements and animation happening on the page so I'm concerned about performance taking that route.
Any other leads?
OK... I worked it out.
I adapted this script which instantly relocates the scroll location to the top of the page when you get to the bottom and to the bottom when you reach the top.
$(window).scroll(function() {
if ( $(window).scrollTop() >= 18830 ) {
$(window).scrollTop(201);
}
else if ( $(window).scrollTop() == 0 ) {
$(window).scrollTop(18629);
}
});
And then I made sure that the content at the bottom and the top of the page was identical. I thought that there would be a flash or something when this relocation happened but it's smooth!
The solution I like the best is this one (code), because it adds elements at the bottom before the bottom is reached, making sure that scrolling remains continuous (even with smooth scrolling on). However, it doesn't work that well on mobile phones where scrolling can happen pretty quickly. I recommend Marijn Haverbeke's wonderful article on fake scrollbars in CodeMirror where he deals with similar issues.
I leave you with some snippets.
First, some background. Why would we want to fake a scrollbar to begin with?
In order to remain responsive when huge documents are loaded in, CodeMirror does not render the whole document, but only the part of it that is currently scrolled into view. This means that the amount of DOM nodes it creates is limited by the size of the viewport, and the browser relayouts triggered by changes to the text are relatively cheap.
And further down...
Then, it listens to wheel events, but never calls preventDefault on them or does scrolling in response to them. Instead, it responds by setting a timeout to observe the amount of pixels that the wheel event did scroll the content, and uses that to tweak its delta-to-pixel rate at run-time.
Clone your HTML body two (or three) times (in javascript or otherwise). Start the page in the middle copy instead of the top, and then you can handle scrolling however you like.
Any other leads?
Seen these?
5 jQuery infinite Scrolling Demos
jsfiddle that I cannot find origin of. (I didn't write and don't know who did)
As many have suggested, if your page doesn't look exactly the same at the top and at the bottom you’ll need to clone your content to make it look like it does. I’ve made an example using this technique that works pretty smooth:
/*
Ininite looping scroll.
Tested and works well in latest Chrome, Safari and Firefox.
*/
(function (window) {
'use strict';
var doc = document,
body = doc.body,
html = doc.documentElement,
startElement = doc.getElementsByClassName('is-start')[0],
clones = doc.getElementsByClassName('is-clone'),
disableScroll = false,
docHeight,
scrollPos,
clonesHeight,
i;
function getScrollPos() {
return (window.pageYOffset || html.scrollTop) - (html.clientTop || 0);
}
function getDocHeight() {
return Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight);
}
function getClonesHeight() {
i = 0;
clonesHeight = 0;
for (i; i < clones.length; i += 1) {
clonesHeight = clonesHeight + clones[i].offsetHeight;
}
return clonesHeight;
}
docHeight = getDocHeight();
clonesHeight = getClonesHeight();
window.addEventListener('resize', function () {
scrollPos = getScrollPos();
docHeight = getDocHeight();
clonesHeight = getClonesHeight();
if (scrollPos <= 0) {
window.scroll(0, 1); // Scroll 1 pixel to allow upwards scrolling.
}
}, false);
window.addEventListener('scroll', function () {
if (disableScroll === false) {
scrollPos = getScrollPos();
if (clonesHeight + scrollPos >= docHeight) {
// Scroll to the top when you’ve reached the bottom
window.scroll(0, 1); // Scroll 1 pixel to allow upwards scrolling.
disableScroll = true;
} else if (scrollPos <= 0) {
// Scroll to the top of the clones when you reach the top.
window.scroll(0, docHeight - clonesHeight);
disableScroll = true;
}
if (disableScroll) {
// Disable scroll-repositioning for a while to avoid flickering.
window.setTimeout(function () {
disableScroll = false;
}, 100);
}
}
}, false);
// Needs a small delay in some browsers.
window.setTimeout(function () {
if (startElement) {
// Start at the middle of the starting block.
window.scroll(0, Math.round(startElement.getBoundingClientRect().top + document.body.scrollTop - (window.innerHeight - startElement.offsetHeight) / 2));
} else {
// Scroll 1 pixel to allow upwards scrolling.
window.scroll(0, 1);
}
});
}(this));
section {
position: relative;
text-align: center;
height: 80vh;
}
.red {
background: #FF4136;
}
.green {
background: #2ECC40;
}
.blue {
background: #0074D9;
}
.orange {
background: rebeccapurple;
}
h1 {
margin: 0;
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 100%;
font-size: 5vw;
color: #fff;
text-transform: uppercase;
}
body {
font-family: "Avenir Next", Montserrat, Helvetica, sans-serif;
font-weight: normal;
font-size: 100%;
}
::scrollbar {
display: none;
}
<section class="green">
<h1>One</h1>
</section>
<section class="red">
<h1>For</h1>
</section>
<section class="blue">
<h1>All</h1>
</section>
<section class="orange">
<h1>And</h1>
</section>
<section class="blue">
<h1>All</h1>
</section>
<section class="red">
<h1>For</h1>
</section>
<!--
These following blocks are the same as the first blocks to get that looping illusion going. You need to add clones to fill out a full viewport height.
-->
<section class="green is-clone is-start">
<h1>One</h1>
</section>
<section class="red is-clone">
<h1>For</h1>
</section>
Building up on Mahmoud's answer, I hacked up this in a few minutes.
It works somewhat (at least on Firefox) when scrolling either with keys or with mouse wheel, but it gets all glitchy when dragging the scrollbar. Depending on how the div heights relate to the viewport height, all kinds of fireworks can happen too.
Still, I hope this can help you get on the right direction.
function onScroll(){
var SAFETY_MARGIN = 50,
scrollPos = $(this).scrollTop(),
docHeight = $(document.body).height(),
winHeight = $(window).height(),
firstDiv = $('body>div:first-child')[0],
lastDiv = $('body>div:last-child')[0],
lowerLimit = SAFETY_MARGIN,
higherLimit = docHeight - SAFETY_MARGIN;
// Scrolling too high
if( scrollPos <= lowerLimit ){
// Move content to top;
$(lastDiv).prependTo(document.body);
// Adjust scroll position to compensate
// for the new content at the top
$(window).scrollTop(scrollPos + $(lastDiv).height());
}
// Scrolling too low
else if( scrollPos + winHeight >= higherLimit ){
// Move content to bottom
$(firstDiv).appendTo(document.body);
// Adjust scroll position to compensate
// for the missing content at the top
$(window).scrollTop(scrollPos - $(firstDiv).height());
}
}
$(window).scroll(onScroll);
$(window).load(function(){
var $body = $(document.body);
$(window).scrollTop($body.height() / 2);
});
</script>
</head>
<body>
<div style="height: 600px; background-color: red"> </div>
<div style="height: 600px; background-color: green"> </div>
<div style="height: 600px; background-color: blue"> </div>
</body>
</html>

Categories

Resources