I created Parallax script for some elements (rect & circles) so when I scroll from top to bottom, elements should move to the top
Elements starting position is added in HTML directly using < style >
The problem is when I scroll back elements move down, but they are not in the same starting position when I reach the top of the page.
HTML:
<img style="top: 62%; left: 46.3%;" class="l-parallax_item" src="./path/to/img">
<img style="top: 74%; left: 42.7%;" class="l-parallax_item" src="./path/to/img">
CSS:
Parent has position relative, l-parallax_item is position absolute
JS:
var parallaxElements = document.getElementsByClassName("l-parallax_item");
var lastScrollTop = 0;
window.onscroll = function() {
var st = window.pageYOffset || document.documentElement.scrollTop;
if (st > lastScrollTop){
console.log("bottom");
for(i = 0; i < parallaxElements.length; i++) {
var position = parallaxElements[i].offsetTop;
var movePx = parallaxElements[i].getAttribute("data-px-per-scroll");
parallaxElements[i].style.top = (position - parseInt(movePx))+"px";
}
} else {
console.log("top");
for(i = 0; i < parallaxElements.length; i++) {
var position = parallaxElements[i].offsetTop;
var movePx = parallaxElements[i].getAttribute("data-px-per-scroll");
parallaxElements[i].style.top = (position + parseInt(movePx))+"px";
}
}
lastScrollTop = st <= 0 ? 0 : st; // For Mobile or negative scrolling
}
I noticed when I scroll from top to bottom I get more console.log("bottom") messages. * When I scroll top > bottom and vise versa.
So I guess that is the reason why the element is not in the same position too when I go bottom > top.
http://prntscr.com/ka9osq
How can I fix this?
EDIT: http://jsfiddle.net/r9751p8q/6/
Try to scroll to the bottom and then back you will see that some element disappear
Here is a possible solution of your problem.
HTML
<div class="section">
<div style="top: 100px; left: 46.3%;" data-px-per-scroll="0.5" data-initial-position="100" class="l-parallax_item"></div>
<div style="top: 300px; left: 37.7%;" data-px-per-scroll="0.9" data-initial-position="300" class="l-parallax_item"></div>
<div style="top: 80px; left: 56%;" data-px-per-scroll="0.3" data-initial-position="80" class="l-parallax_item"></div>
<div style="top: 230px; left: 75%;" data-px-per-scroll="0.8" data-initial-position="230" class="l-parallax_item"></div>
<div style="top: 60px;left: 7.1%;" data-px-per-scroll="0.5" data-initial-position="60" class="l-parallax_item"></div>
</div>
JS
function parallaxBlocks(){
let parallaxElements = document.getElementsByClassName("l-parallax_item");
let st = window.pageYOffset || document.documentElement.scrollTop;
let elementsLength = parallaxElements.length;
for(i = 0; i < elementsLength; i++) {
let movePx = 1 + parseFloat(parallaxElements[i].dataset["pxPerScroll"]);
let position = 1 + parseInt(parallaxElements[i].dataset["initialPosition"]);
parallaxElements[i].style.top = Math.floor(position - (st * movePx)) + 'px';
}
}
window.addEventListener('scroll', parallaxBlocks)
But I also changed your html. For working example check this fiddle .
And some explanation what I'm actually doing in the JS code:
In this code we don't need to check if we scroll up or down because the logic of the positioning of the elements is shifted form what was their current position (as in your code) to what was their initial position.
I'm using let to declare variables instead of var for scope reasons (and because I love new things). Still if you are going to support older browsers you might want to check caniuse for compatibilities.
For the top css property I'm using fixed values, and I store their original top offset in a new data attribute called initial-position. This initial position will be used later to calculate the new position of each element. If you need a % values then you can keep the top property with % value, but you will need also another loop to go through all the .l-parallax_item and check their offset from the top and record this value in their data-initial-position.
Note that I'm using dataset instead getAttribute. dataset is made for all data attributes. And see how the cebab-case became camelCase. More to read here
Also the px-per-scroll is no longer a fixed amount of pixels instead it is a ratio from the scroll offset from top. You can play with the fiddle to see how it works.
Bonus: why I added another variable for the elements length instead just using it in the for loop arguments.
I believe that there is another way to do this but hope that this one will help you.
The reason it's not working:
Scrolling is not precise and because you base your position on the total sum of scrolls the positioning will end up unpredictable. To show you what I mean:
Your console.log() fires every time you scroll. If you change both logs to console.log(parallaxElements[0].style.top) it will show you the top position of the first parallaxed element each time you make a scroll move. Now take your mouse and scroll one "tick" down, then one "tick" up and repeat many times. The position numbers will not be the same every time, and therein lies the problem.
Solution:
Base your parallax elements position on the actual pageYOffset. Since Ale already posted a working solution with this in mind one more code example is reduntant.
Related
So what I want to happen is that when viewing the Span the text is normal but as you scroll down it starts moving until it looks like such:
Before the effect:
While the effect occurs:
The header is represented by spans for each letter. In the initial state, the top pixel value for each is 0. But the idea as mentioned is that that changes alongside the scroll value.
I wanted to keep track of the scroll position through JS and jQuery and then change the pixel value as needed. But that's what I have been having trouble with. Also making it smooth has been another issue.
Use the mathematical functions sine and cosine, for characters at even and odd indices respectively, as the graphs of the functions move up and down like waves. This will create a smooth effect:
cos(x) == 1 - sin(x), so in a sense, each character will be the "opposite" of the next one to create that scattered look:
function makeContainerWiggleOnScroll(container, speed = 0.01, distance = 4) {
let wiggle = function() {
// y-axis scroll value
var y = window.pageYOffset || document.body.scrollTop;
// make div pseudo-(position:fixed), because setting the position to fixed makes the letters overlap
container.style.marginTop = y + 'px';
for (var i = 0; i < container.children.length; i++) {
var span = container.children[i];
// margin-top = { amplitude of the sine/cosine function (to make it always positive) } + { the sine/cosine function (to make it move up and down }
// cos(x) = 1 - sin(x)
var trigFunc = i % 2 ? Math.cos : Math.sin;
span.style.marginTop = distance + distance * trigFunc(speed * y)/2 + 'px';
}
};
window.addEventListener('scroll', wiggle);
wiggle(); // init
}
makeContainerWiggleOnScroll(document.querySelector('h2'));
body {
height: 500px;
margin-top: 0;
}
span {
display: inline-block;
vertical-align: top;
}
<h2>
<span>H</span><span>e</span><span>a</span><span>d</span><span>e</span><span>r</span>
</h2>
Important styling note: the spans' display must be set to inline-block, so that margin-top works.
Something like this will be the core of your JS functionality:
window.addEventListener('scroll', function(e) {
var scrl = window.scrollY
// Changing the position of elements that we want to go up
document.querySelectorAll('.up').forEach(function(el){
el.style.top = - scrl/30 +'px';
});
// Changing the position of elements that we want to go down
document.querySelectorAll('.down').forEach(function(el){
el.style.top = scrl/30 +'px';
});
});
We're basically listening in on the scroll event, checking how much has the user scrolled and then act upon it by offsetting our spans (which i've classed as up & down)
JSBin Example
Something you can improve on yourself would be making sure that the letters wont go off the page when the user scrolls a lot.
You can do this with simple math calculation, taking in consideration the window's total height and using the current scrollY as a multiplier.
- As RokoC has pointed out there is room for performance improvements.Implement some debouncing or other kinds of limiters
I'm looking for a way in jQuery or pure JS to get the amount of pixels scrolled, not from the top of the page, but from the bottom of a div.
In other words I need to turn the amount scrolled beyond a div's height + its pixel distance from the top of the page into a variable.
I want to append this parallax code below so instead of calculating from the top of the page, calculates from a target div's distance from the top + its height.
/* Parallax Once Threshold is Reached */
var triggerOne = $('#trigger-01').offset().top;
$(window).scroll(function(e){
if ($(window).scrollTop() >= triggerOne) {
function parallaxTriggerOne(){
var scrolled = $(window).scrollTop();
$('#test').css('top',+(scrolled*0.2)+'px');
}
parallaxTriggerOne();
} else {
$('#test').css('top','initial');
}
});
I realize I didn't phrase this quite clear enough, I'm looking to only get the value of the amount of pixels scrolled since passing a div, so for example if I had a 200px tall div at the very top of the page and I scrolled 20 pixels beyond it, that variable I need would equal 20, not 220.
You can get a div's position by using div.offsetTop,
adding div.offsetHeight into div's distance from top of page will give you bottom of div, then you can subtract from window's scroll to get your desired value.
Feel free to ask if you have any doubts.
var div = document.getElementById('foo');
let div_bottom = div.offsetTop + div.offsetHeight;
var doc = document.documentElement;
var left = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0);
var scroll_top, scroll_after_div;
setInterval(function(){
scroll_top = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0);
scroll_after_div = scroll_top - div_bottom;
console.log(scroll_after_div);
}, 1000);
body { margin: 0; }
<div id="foo" style="position:relative; top: 100px; height: 30px; width: 100%; background-color: #000;"></div>
<div id="bar" style="position:relative; top: 700px; height: 30px; width: 100%; background-color: #000;"></div>
In this snippet setInterval method is printing the scroll value each second, you can scroll and see the change in value.
To work out the distance from the top of the page to the bottom of an element, you can add an elements outerHeight() with its offset().top.
Example: https://jsfiddle.net/dw2jwLpw/
console.log(
$('.target').outerHeight() + $('.target').offset().top
);
In pure JS you can get the bottom of the div directly with document.getElementById("my-element").getBoundingClientRect().bottom.
In jQuery you can use $('#my-element').offset().top + $('#my-element').height()
I'm trying to highlight some text in a div, with the highlight being a fixed line in said text. So far I've got a very simple solution that uses two divs, one that houses the text, and the other acting as the highlight, and as you scroll the text, it will pass through the highlight div.
HTML is as follows:
<div id="test">
text...
</div>
<div id="highlight"></div>
CSS is:
#highlight {
position: fixed;
top: 50%;
width: 100%;
background-color: #ccff00;
height: 30px;
opacity: 0.6;
}
#test{
position: absolute;
font-size: 30px;
top: 50%;
}
A demo of it can be found here
I was wondering if anyone knows how to make it so that scrolling the text can be done in a way where as a user scrolls, the next line becomes highlighted. Currently it scrolls normally, so the highlight may miss a line, or not highlight a complete line. Additionally, I was wondering how it would be best to make the text scroll all the way to the bottom. Would adding a margin of the same size as the offset at the top work? Alternative solutions for any of this would be appreciated as well.
Try adding an event listener to the window on scroll. Then calculate the offset by taking the scrollY % line-height and set the highlight top margin to the negative of that value.
JavaScript below:
var highlight = document.querySelector("#highlight");
window.addEventListener('scroll', function(e){
var y = window.scrollY;
var offset = y % 30;
highlight.style.marginTop = - y % 30 + "px";
});
See Working Fiddle
Not sure if this
https://jsfiddle.net/ok0x3apo/6/ is what you're looking for
You can see that I'm remodifying the entered text, to get line by line highlight as page scrolls.
var el = document.getElementById("text"),
content = el.innerHTML.replace(/ |^\s+|\s+$/g,""),
lines = content.split(/\./);
var html = "";
for(var i in lines){
html+="<p class='clear_display' id='id_"+i+"'>"+lines[i]+".</p>";
};
document.getElementById("text").innerHTML=html;
You can make changes to the "clear_display" class on how you prefer to have the text block.
function calledEveryScroll() {
var scrollPosition = $(window).scrollTop();
for(var i in lines){
var currentSection = document.querySelector("#id_"+i+"");
var sectionTop = currentSection.offsetTop;
if (scrollPosition<=0){
$(".clear_display").removeClass('active');
document.querySelector("#id_0").className += " active";
}
if (scrollPosition >= sectionTop-50) {
$(".clear_display").removeClass('active');
if (!$(currentSection).hasClass('active')) {
$(currentSection).addClass('active');
if(previous){
if(currentSection.offsetTop==previous.offsetTop){
$(previous).addClass('active');
}
}
var previous = currentSection;
}
//return false;
}
}
}
function resizing(){
var offset =100;
var bottom = $(window).height()-offset;
$('#text').css('margin-bottom',bottom);
}
This function checks each line when page scrolls.For the scroll to reach the bottom I'm calculating the margin-bottom.Hope it helps.
I need a better way to calculate a scrollable div's viewport.
Under normal circumstances, I would use the following attributes: (scrollLeft, scrollTop, clientWidth, clientHeight)
Using these numbers I can accurately determine which part of a scrollable DOM element's viewport is currently visible, I use this information to asynchronously load things that are visible to the user on demand when scrolling to the content horizontally or vertically. When the content of the DIV is massive, this will avoid an embarassing browser crashing bug because of too many DOM elements being loaded.
My component has worked for a while now with no issues, this build we are introducing RTL support. Now everything is thrown off because of browser inconsistencies.
To demonstrate, I have created a simple example which will output the scrollLeft attribute of a scrollable element in a JSFiddle.
The behavior of the scrollLeft attribute on this simple scrollable element is not consistent from one browser to the next. The 3 major browsers I've tried all behaved differently.
FF-latest scrollLeft starts at 0 and goes negative when scrolling left
IE 9 scrollLeft starts at 0 and goes positive when scrolling left
Chrome-latest scrollLeft starts at a higher number and goes to 0 when scrolling left
I want to avoid having code like if(ie){...}else if(ff){...}else if (chrome){...} that would be horrible, and not maintainable in the long run in case browsers change behavior.
Is there a better way to figure out precisely which part of the DIV is currently visible?
Perhaps there is some other reliable DOM attribute other than scrollLeft?
Maybe there is a jQuery plugin that will do it for me, keeping in mind which browser version it is?
Maybe there is a technique I can use to figure out which of the cases it is at runtime without relying on some unreliable browser detection (i.e. userAgent)
Fiddle Example (code copied below)
HTML
<div id="box"><div id="content">scroll me</div></div>
<div id="output">Scroll Left: <span id="scrollLeft"></span></div>
CSS
#box {
width: 100px; height: 100px;
overflow: auto;
direction: rtl;
}
#content { width: 300px; height: 300px; }
JS
function updateScroll() {
$('#scrollLeft').text(box.scrollLeft());
}
var box = $('#box').scroll(updateScroll);
updateScroll();
Here's a jQuery plugin which does not use browser detection: https://github.com/othree/jquery.rtl-scroll-type
Using this plugin you could replace jQuery's scrollLeft function with your own predictable version, like this:
var origScrollLeft = jQuery.fn.scrollLeft;
jQuery.fn.scrollLeft = function(i) {
var value = origScrollLeft.apply(this, arguments);
if (i === undefined) {
switch(jQuery.support.rtlScrollType) {
case "negative":
return value + this[0].scrollWidth - this[0].clientWidth;
case "reverse":
return this[0].scrollWidth - value - this[0].clientWidth;
}
}
return value;
};
I didn't include the code for setting the scroll offset, but you get the idea.
Here's the fiddle: http://jsfiddle.net/scA63/
Also, this lib may be of interest too.
You can try this:-
var initialScrollLeft = $('#box').scrollLeft(), negativeToZero, startFromZero;
if(initialScrollLeft === 0){
startFromZero = true;
} else if(initialScrollLeft < 0){
negativeToZero = true;
}
var box = $('#box').scroll(function(){
if(startFromZero){
if(box.scrollLeft()>0){
$('#scrollLeft').text(- (box.scrollLeft()));
}else {
$('#scrollLeft').text(box.scrollLeft());
}
} else if(negativeToZero){
$('#scrollLeft').text(box.scrollLeft()+(box[0].scrollWidth - box[0].clientWidth));
} else{
$('#scrollLeft').text(box.scrollLeft()-(box[0].scrollWidth - box[0].clientWidth));
}
});
Problem: (Ex. Scroll Width = 100)
Chrome - Most Right: 100 Most Left: 0.
IE- Most Right: 0 Most Left: 100.
Firefox - Most Right: 0 Most Left: -100.
Solution #1
As mentioned by #Lucas Trzesniewski.
You could use this Jquery plugin:
https://github.com/othree/jquery.rtl-scroll-type
The plugin is used to detect which type is the browser are using.
Assign the result to jQuery's support object named 'rtlScrollType'.
You will need the scrollWidth of the element to transform between
these three types of value
Solution #2
Credits: jQuery.scrollLeft() when direction is rtl - different values in different browsers
I know you didn't want to include browser detection individually for each browser. With this example, only 2 extra lines of code are added for Safari and Chrome and it works like a charm!
Modified it to demonstrate it better for you.
$('div.Container').scroll(function () {
st = $("div.Container").scrollLeft() + ' ' + GetScrollLeft($("div.Container"));
$('#scrollLeft').html(st);
});
function GetScrollLeft(elem) {
var scrollLeft = elem.scrollLeft();
if ($("body").css("direction").toLowerCase() == "rtl") {
// Absolute value - gets IE and FF to return the same values
var scrollLeft = Math.abs(scrollLeft);
// Get Chrome and Safari to return the same value as well
if ($.browser.webkit) {
scrollLeft = elem[0].scrollWidth - elem[0].clientWidth - scrollLeft;
}
}
return scrollLeft;
}
JSFiddle:
http://jsfiddle.net/SSZRd/1/
The value on the left should be the same for all browser while the value on the right is the older value which is different on all browser. (Tested on Firefox, Safari, Chrome, IE9).
1. FF-latest scrollLeft starts at 0 and goes negative when scrolling left
2. IE 9 scrollLeft starts at 0 and goes positive when scrolling left
3. Chrome-latest scrollLeft starts at a higher number and goes to when scrolling left
I want to avoid having code like if(ie){...}else if(ff){...}else if(chrome){...}
that would be horrible, and not maintainable in the long run in case browsers change behavior
FYI:
Chrome 85 (final shipping Aug. 2020) fixed this bug and aligns behaviour with Firefox and Safari and the spec.
See https://www.chromestatus.com/feature/5759578031521792
Is there a feature detection available for this?
Yes, e.g. use one of two scrips (from Frédéric Wang) available here:
https://people.igalia.com/fwang/scrollable-elements-in-non-default-writing-modes/
either this
function scroll_coordinates_behavior_with_scrollIntoView() {
/* Append a RTL scrollable 1px square containing two 1px-wide descendants on
the same line, reveal each of them successively and compare their
scrollLeft coordinates. The scrollable square has 'position: fixed' so
that scrollIntoView() calls don't scroll the viewport. */
document.body.insertAdjacentHTML("beforeend", "<div style='direction: rtl;\
position: fixed; left: 0; top: 0; overflow: hidden; width: 1px; height: 1px;'>\
<div style='width: 2px; height: 1px;'><div style='display: inline-block;\
width: 1px;'></div><div style='display: inline-block; width: 1px;'></div>\
3</div></div>");
var scroller = document.body.lastElementChild;
scroller.firstElementChild.children[0].scrollIntoView();
var right = scroller.scrollLeft;
scroller.firstElementChild.children[1].scrollIntoView();
var left = scroller.scrollLeft;
/* Per the CSSOM specification, the standard behavior is:
- decreasing coordinates when scrolling leftward.
- nonpositive coordinates for scroller with leftward overflow. */
var result = { "decreasing": left < right, "nonpositive": left < 0 };
document.body.removeChild(scroller);
return result;
}
or that
function scroll_coordinates_behavior_by_setting_nonpositive_scrollLeft() {
/* Append a RTL scrollable 1px square containing a 2px-wide child and check
the initial scrollLeft and whether it's possible to set a negative one.*/
document.body.insertAdjacentHTML("beforeend", "<div style='direction: rtl;\
position: absolute; left: 0; top: 0; overflow: hidden; width: 1px;\
height: 1px;'><div style='width: 2px; height: 1px;'></div></div>");
var scroller = document.body.lastElementChild;
var initially_positive = scroller.scrollLeft > 0;
scroller.scrollLeft = -1;
var has_negative = scroller.scrollLeft < 0;
/* Per the CSSOM specification, the standard behavio999r is:
- decreasing coordinates when scrolling leftward.
- nonpositive coordinates for scroller with leftward overflow. */
var result = { "decreasing": has_negative ||
initially_positive, "nonpositive": has_negative };
document.body.removeChild(scroller);
return result;
}
I have something vaguely like the following:
<div id="body">
surrounding text
<div id="pane" style="overflow: auto; height: 500px; width: 500px;">
lots and lots of text here
<span id="some_bit">tooltip appears below-right of here</span>
</div>
more surrounding text (should be overlapped by tooltip)
</div>
and:
<div id="tooltip" style="width: 100px; height: 100px;">Whee</div>
What I want to do is insert the tooltip such that it is positioned above the pane it's in. If it's attached to an element that's next to the pane boundary (like above), then it should be visible above the pane, and above the text surrounding the pane.
It should NOT a) extend the pane, such that you have to scroll down to see the tooltip (like in http://saizai.com/css_overlap.png), or b) be cut off, so you can't see all of the tooltip.
I'm inserting this with JS, so I can add a wrapper position:relative div or the like if needed, calculate offsets and make it position:absolute, etc. I would prefer to not assume anything about the pane's position property - the tooltip should be insertable with minimal assumptions of possible page layout. (This is just one example case.)
It's for a prototype tooltip library I'm writing that will be open source.
ETA: http://jsfiddle.net/vCb2y/5/ behaves visually like I want (if you keep re-hovering the trigger text), but would require me to update the position of the tooltip on all DOM changes and scrolling behavior. I would rather the tooltip be positioned with pure CSS/HTML so that it has the same visual behavior (i.e. it overlaps all other elements) but stays in its position relative to the target under DOM changes, scrolling, etc.
ETA 2: http://tjkdesign.com/articles/z-index/teach_yourself_how_elements_stack.asp (keep defaults except set cyan div 'a' to position:relative; imagine 'A' is the pane and 'a' the tooltip) seems to more closely behave as I want, but I've not been able to get it to work elsewhere. Note that if you make 'A' overflow: auto, it breaks the overlapping behavior of 'a'.
I can't think of a pure HTML/CSS solution for this.
The overflow declaration is the issue here. If the tooltip is in #pane:
you establish a positioning context within #pane, then the tooltip shows next to #some_bit (regardless of scrolling, etc.) but it gets cut-off.
you do not establish a positioning context, then the tooltip is not clipped but it has no clue where #some_bit is on the page.
I'm afraid you'll need JS to monitor where #some_bit is on the page and position the tooltip accordingly. You'd also need to kill that tooltip as soon as #some_bit is outside of the viewing area (not an issue if the trigger is mouseover).
Actually, if the trigger is mouseover then you may want to use the cursor coordinates to position the tooltip (versus calculating the position of #some_bit).
I would just put the tooltip outside of the #pane div and position it absolutely using JavaScript since you're using JS anyway.
I don't use Prototype so I don't know how it's done in Prototype, but in jQuery, you'd use $(element).position() to get the element position. If you have to do it manually, it's a little more complicated.
And you'll probably want to add a little extra logic to prevent the tooltip from extending outside of the document.
Edit: CSS used
#tooltip {
z-index: 9999;
display: none;
position: absolute;
}
JS used
Note: in jQuery, but it should be easy to change it to Prototype syntax.
$('#some_bit').hover(function() {
var docViewTop = $(window).scrollTop();
var docViewBottom = docViewTop + $(window).height();
// hovered element
var offset = $(this).offset();
var top = offset.top + docViewTop;
var left = offset.left;
var width = $(this).width();
var height = $(this).height();
var right = left + width;
var bottom = top + height;
// pane
var poffset = $('#pane').offset();
var ptop = poffset.top + docViewTop;
var pleft = poffset.left;
var pwidth = $('#pane').width();
var pheight = $('#pane').height();
var pright = pleft + pwidth;
var pbottom = ptop + pheight;
// tooltip
var ttop = bottom;
var tleft = right;
var twidth = $('#tooltip').width();
var theight = $('#tooltip').height();
var tright = tleft + twidth;
var tbottom = ttop + theight;
if (tright > pright)
tleft = pright - twidth;
if (tbottom > pbottom)
ttop = pbottom - theight;
if (tbottom > docViewBottom)
ttop = docViewBottom - theight;
$('#tooltip').offset({top: ttop, left: tleft});
$('#tooltip').css('display', 'block');
}, function() {
$('#tooltip').hide();
});
Edit: See it here.