scrollMagic - How to scroll to the right place? - javascript

There is such a thing.
https://jsfiddle.net/j6u6wp7x/1/
var scene;
var controller;
$(document).ready(function() {
parallaxAuto();
$('.viewer__nav div').click(function(event) {
var num = $(this).attr('data-num');
if (num == 'sticky') {
controller.scrollTo(scene);
}
var scrollPos = controller.info("scrollPos");
});
});
function hideShow(num, block) {
block.find("div.active").removeClass("active").animate({ opacity: 0,},300);
block.find("div.slide"+num).addClass("active").animate({ opacity: 1,},300);
}
// init variables
function parallaxAuto() {
var viewer = document.querySelector('.viewer.active'),
frame_count = 5,
offset_value = 500;
// init controller
controller = new ScrollMagic.Controller({
globalSceneOptions: {
triggerHook: 0,
reverse: true
}
});
// build pinned scene
scene = new ScrollMagic.Scene({
triggerElement: '#sticky',
duration: (frame_count * offset_value) + 'px',
reverse: true
})
.setPin('#sticky')
//.addIndicators()
.addTo(controller);
// build step frame scene
for (var i = 1, l = frame_count; i <= l; i++) {
new ScrollMagic.Scene({
triggerElement: '#sticky',
offset: i * offset_value
})
.setClassToggle(viewer, 'frame' + i)
//.addIndicators()
.addTo(controller);
}
}
Below there are 3 smaller images that create Navigation. I made it to jump to the top, but I cannot figure out how to jump to 2nd or 3rd.
var scrollPos = controller.info ( "scrollPos"); shows the current position, but I cannot imagine how to use it correctly.

I didn't go through the tit-bits of your code, but in similar situations we used the scrollIntoView() function of JavaScript(not jQuery).
var element = document.getElementById('id of your image');
element.scrollIntoView(false);
This much code should do the trick.
Hope it helps.
EDIT :1
Hi, I updated your fiddle, I think we need to store the scenes in a array and then refer it later. I guess you were looking for something like that.

Related

How to Make the vis-timeline pan-x smooth and eased?

i'm using the vis-timeline library. In the documentations there is no property for easing the touch pan-x when navigating the timeline, resulting in a stiff and rigid navigation.
I tried this:
timeline.on('rangechange', function(properties) {
var windowStart = timeline.getWindow().start;
var windowEnd = timeline.getWindow().end;
var distance = properties.event.deltaX * properties.event.velocityX;
var newWindowStart = windowStart.valueOf() - distance;
var newWindowEnd = windowEnd.valueOf() - distance;
timeline.setWindow(newWindowStart, newWindowEnd, {
animation: {
easingFunction: 'easeInOutQuad'
}
});
});
How can i implement an eased panX navigation?
Ok, i find a solution.
Here's my code:
timeline.on("rangechanged", (properties) => {
if (properties.event) {
let distance =
properties.event.deltaX *
Math.abs(properties.event.velocityX);
let newWindowStart = new Date(
properties.start.valueOf() - distance * 100000000
);
let newWindowEnd = new Date(
properties.end.valueOf() - distance * 100000000
);
timeline.setWindow(
newWindowStart,
newWindowEnd,
{
animation: {
duration: 500,
easingFunction: "easeOutQuad",
},
}
);
}

ScrollMagic loop - scene duration the same as the video duration that's part of the scene

I have a loop of scenes with videos that play on scroll (loop because it's integrated with WordPress ACF Flexible fields). I'm using ScrollMagic for that (and GSAP but that doesn't apply here).
What I'm trying to do is to make the duration of the whole scene same as duration of the video of the scene (on the front-end there'll be a presentation with different videos). I tried simply video.duration, but it beahvaes in a weird way: sometimes it works, sometimes not. Is there a solution for this?
Here is my code:
const startVideoFullScreenOnScrollFunction = () => {
const startVideoFullScreenOnScroll = document.querySelectorAll('.video-fullscreen-text-on-scroll');
for (let i = 0; i < startVideoFullScreenOnScroll.length; i += 1) {
if (typeof (startVideoFullScreenOnScroll[i]) !== 'undefined' && startVideoFullScreenOnScroll[i] != null) {
const controllerVideoFullScreenOnScroll = new ScrollMagic.Controller();
const videoFullScreen = startVideoFullScreenOnScroll[i].querySelector('video');
const textsOnVideo = startVideoFullScreenOnScroll[i].querySelectorAll('.video-fullscreen-text-on-scroll__copy');
const timelineVideoFullScreenOnScroll = new TimelineMax();
// fade in & fade out text
for (let j = 0; j < textsOnVideo.length; j += 1) {
timelineVideoFullScreenOnScroll
.to(textsOnVideo[j], {
opacity: 1,
duration: 3,
})
.to(textsOnVideo[j], {
opacity: 0,
duration: 3,
});
}
// ScrollMagic scene
let sceneVideoFullScreenOnScroll = new ScrollMagic.Scene({
duration: ???,
triggerElement: startVideoFullScreenOnScroll[i],
triggerHook: 0
})
.setTween(timelineVideoFullScreenOnScroll)
.setPin(startVideoFullScreenOnScroll[i])
.addTo(controllerVideoFullScreenOnScroll);
let scrollpos = 0;
let startpos = 0;
sceneVideoFullScreenOnScroll.on('update', e => {
startpos = e.startPos / 1000;
scrollpos = e.scrollPos / 1000;
if (scrollpos >= startpos) {
videoFullScreen.currentTime = scrollpos - startpos;
};
});
}
}
}

Run a function when animation completes using Curtains.js

I'm looking to call a function right at the end of the wobble effect.
That is, at the end of the damping effect (when the wobble stops), I'd like to execute a GSAP timeline function. I'd assume this type of "onComplete" function would need to be called inside the onReady() of Curtains and perhaps by tracking the damping effect. I'm only familiar with GSAP's onComplete function, but don't know how I would implement it here. Maybe something that checks if deltas.applied is less than 0.001, then the function is called?
Below is the code snippet (without the fragment and vertex shaders). Full working code here:
CodePen
class Img {
constructor() {
const curtain = new Curtains({
container: "canvas",
watchScroll: false,
});
const params = {
vertexShader,
fragmentShader,
uniforms: {
time: {
name: "uTime",
type: "1f",
value: 0,
},
prog: {
name: "uProg",
type: "1f",
value: 0,
}
}
}
const planeElements = document.getElementsByClassName("plane")[0];
this.plane = curtain.addPlane(planeElements, params);
if (this.plane) {
this.plane
.onReady(() => {
this.introAnim();
})
.onRender(() => {
this.plane.uniforms.time.value++;
deltas.applied += (deltas.max - deltas.applied) * 0.05;
deltas.max += (0 - deltas.max) * 0.07;
this.plane.uniforms.prog.value = deltas.applied
})
}
// error handling
curtain.onError(function() {
document.body.classList.add("no-curtains");
});
}
introAnim() {
deltas.max = 6;
//console.log("complete") <-- need an onComplete type function~!
}
}
window.onload = function() {
const img = new Img();
}
What you could use is some algebra :)
First off, you should simplify your deltas.max function like so:
deltas.max += (0 - deltas.max) * 0.07;
// Simplifies to
deltas.max -= deltas.max * 0.07;
// Rewrite to
deltas.max = deltas.max - deltas.max * 0.07;
// Rewrite to
deltas.max = deltas.max * (1 - 0.07);
// Simplifies to
deltas.max *= 0.93; // Much nicer :)
That is actually pretty important to do because it makes our work of calculating the end value of our time variable and the duration of our animation significantly Easier:
// Given deltas.max *= 0.93, need to calculate end time value
// endVal = startVal * reductionFactor^n
// Rewrite as
// n = ln(endVal / startVal) / ln(reductionFactor) // for more see https://www.purplemath.com/modules/solvexpo2.htm
// n = ln(0.001 / 8) / ln(0.93)
const n = 123.84;
// Assuming 60fps normally: n / 60
const dur = 2.064;
Once we have those values all we have to do is create a linear tween animating our time to that value with that duration and update the max and prog values in the onUpdate:
gsap.to(this.plane.uniforms.time, {
value: n,
duration: dur,
ease: "none",
onUpdate: () => {
this.deltas.applied += (this.deltas.max - this.deltas.applied) * 0.05;
this.deltas.max *= 0.93;
this.plane.uniforms.prog.value = this.deltas.applied;
},
onComplete: () => console.log("complete!")
});
Then you get "complete!" when the animation finishes!
To make sure that your Curtains animations run at the proper rate even with monitors with high refresh rates (even the ones not directly animated with GSAP) it's also a good idea to turn off Curtain's autoRendering and use GSAP's ticker instead:
const curtains = new Curtains({ container: "canvas", autoRender: false });
// Use a single rAF for both GSAP and Curtains
function renderScene() {
curtains.render();
}
gsap.ticker.add(renderScene);
Altogether you get this demo.
This won't be the best answer possible but you can take some ideas and insights from it.
Open the console and see that when the animation gets completed it gets fired only once.
//Fire an onComplete event and listen for that
const event = new Event('onComplete');
class Img {
constructor() {
// Added a instance variable for encapsulation
this.animComplete = {anim1: false}
//Changed code above
const curtain = new Curtains({
container: "canvas",
watchScroll: false,
});
const params = {
vertexShader,
fragmentShader,
uniforms: {
time: {
name: "uTime",
type: "1f",
value: 0,
},
prog: {
name: "uProg",
type: "1f",
value: 0,
}
}
}
const planeElements = document.getElementsByClassName("plane")[0];
this.plane = curtain.addPlane(planeElements, params);
if (this.plane) {
this.plane
.onReady(() => {
this.introAnim();
document.addEventListener('onComplete', ()=>{
//Do damping effects here
console.log('complete')
})
})
.onRender(() => {
this.plane.uniforms.time.value++;
deltas.applied += (deltas.max - deltas.applied) * 0.05;
deltas.max += (0 - deltas.max) * 0.07;
this.plane.uniforms.prog.value = deltas.applied
if(deltas.applied<0.001 && !this.animComplete.anim1){
document.dispatchEvent(event)
this.animComplete.anim1 = true
}
})
}
// error handling
curtain.onError(function() {
document.body.classList.add("no-curtains");
});
}
introAnim() {
deltas.max = 6;
}
}
window.onload = function() {
const img = new Img();
}
I've found a solution to call a function at the end of the damping (wobble) effect, that doesn't use GSAP, but uses the Curtains onRender method. Because the uTime value goes up infinitely and the uProg value approaches 0, By tracking both the uTime and uProg values inside the Curtains onRender method we can find a point (2 thresholds) at which the damping effect has essentially completed. Not sure if this is the most efficient way, but it seems to work.
.onRender(() => {
if (this.plane.uniforms.prog.value < 0.008 && this.plane.uniforms.time.value > 50) { console.log("complete")}
})
Thanks to the Curtains docs re asynchronous-textures, I was able to better control the timing of the wobble effect with the desired result every time. That is, on computers with lower FPS, the entire damping effect takes place smoothly, with an onComplete function called at the end, as well as on comps with higher frame rates.
Although, as mentioned there is less control over the length of the effect, as we are not using GSAP to control the Utime values. Thanks #Zach! However, using a "threshold check" inside the curtains onRender this way, means the damping wobble effect is never compromised, if we were to disable the drawing at the on complete call.
By enabling the drawing at the same time the image is loaded we avoid any erratic behaviour. The following works now with hard refresh as well.
export default class Img {
constructor() {
this.deltas = {
max: 0,
applied: 0,
};
this.curtain = new Curtains({
container: "canvas",
watchScroll: false,
pixelRatio: Math.min(1.5, window.devicePixelRatio),
});
this.params = {
vertexShader,
fragmentShader,
uniforms: {
time: {
name: "uTime",
type: "1f",
value: 0,
},
prog: {
name: "uProg",
type: "1f",
value: 0,
},
},
};
this.planeElements = document.getElementsByClassName("plane")[0];
this.curtain.onError(() => document.body.classList.add("no-curtains"));
this.curtain.disableDrawing(); // disable drawing to begin with to prevent erratic timing issues
this.init();
}
init() {
this.plane = new Plane(this.curtain, this.planeElements, this.params);
this.playWobble();
}
loaded() {
return new Promise((resolve) => {
// load image and enable drawing as soon as it's ready
const asyncImgElements = document
.getElementById("async-textures-wrapper")
.getElementsByTagName("img");
// track image loading
let imagesLoaded = 0;
const imagesToLoad = asyncImgElements.length;
// load the images
this.plane.loadImages(asyncImgElements, {
// textures options
// improve texture rendering on small screens with LINEAR_MIPMAP_NEAREST minFilter
minFilter: this.curtain.gl.LINEAR_MIPMAP_NEAREST,
});
this.plane.onLoading(() => {
imagesLoaded++;
if (imagesLoaded === imagesToLoad) {
console.log("loaded");
// everything is ready, we need to render at least one frame
this.curtain.needRender();
// if window has been resized between plane creation and image loading, we need to trigger a resize
this.plane.resize();
// show our plane now
this.plane.visible = true;
this.curtain.enableDrawing();
resolve();
}
});
});
}
playWobble() {
if (this.plane) {
this.plane
.onReady(() => {
this.deltas.max = 7; // 7
})
.onRender(() => {
this.plane.uniforms.time.value++;
this.deltas.applied += (this.deltas.max - this.deltas.applied) * 0.05;
this.deltas.max += (0 - this.deltas.max) * 0.07;
this.plane.uniforms.prog.value = this.deltas.applied;
console.log(this.plane.uniforms.prog.value);
// ---- "on complete" working!! ( even on hard refresh) -----//
if (
this.plane.uniforms.prog.value < 0.001 &&
this.plane.uniforms.time.value > 50
) {
console.log("complete");
this.curtain.disableDrawing();
}
});
}
}
destroy() {
if (this.plane) {
this.curtain.disableDrawing();
this.curtain.dispose();
this.plane.remove();
}
}
}
const img = new Img();
Promise.all([img.loaded()]).then(() => {
console.log("animation started");
});

How to hide items in the correct sequence? , Using waypoint, TweenMax, jquery

I'm collapsando the header when I scroll the browser window, I'm using waypoints to trigger a function when passing a certain limit.
My problem is how to do that, for example, when I scroll down first disappear content (form inputs) and then change the height of the header, and then when I scroll up first increase height and then display the contents (form inputs) .
How do you could do?
I have a example here: fiddle
JS:
$(document).ready(init);
function init(){
var header, pageContainer, pageContent, brandImage;
header = $('header');
pageContainer = $('#page-container');
pageContent = $('#page-content');
brandImage = $('.brand img');
//functions
collapseHaeder();
function collapseHaeder(){
if(pageContainer.hasClass('collapse-header')){
var waypoint = new Waypoint({
element: document.getElementById('page-content'),
handler: function(direction) {
var elementsToResize = $('header, .brand-holder, .header-content');
var hideElms = $('.hide-element');
if(direction == 'up'){
hideElements(hideElms);
resizeHeader(elementsToResize);
}else {
resizeHeader(elementsToResize);
hideElements(hideElms);
}
}
});
}
}
function resizeHeader(elemts){
var newHeight = 45;
var brandHeight = newHeight - 10;
var easingEffect = 'Quart.easeInOut';
if(!elemts.hasClass('resized')){
elemts.addClass('resized');
}else {
elemts.removeClass('resized');
newHeight = 140;
brandHeight = newHeight / 2;
}
//header elements containers
TweenMax.to(elemts, 1, {height:newHeight, ease: easingEffect});
//page container padding
TweenMax.to(pageContainer, 1, {paddingTop:newHeight, ease: easingEffect});
//brand image
TweenMax.to(brandImage, 1, {height:brandHeight, ease: easingEffect});
}
function hideElements(hiddenElement){
var classHidded = 'has-hided';
if(!hiddenElement.hasClass(classHidded)){
hiddenElement.addClass(classHidded);
hiddenElement.fadeOut(800);
}else {
hiddenElement.fadeIn(500);
hiddenElement.removeClass(classHidded);
}
}
}
There is no need to use jQuery's fadeIn and fadeOut when you are already using GSAP. Take a look at this jsFiddle that I have created, code of which is as follows:
/*global TweenMax,TimelineMax*/
$(document).ready(init);
function init() {
var header, pageContainer, pageContent, brandImage, brandHolder, headerContent, hideElement;
var duration = .8,
ease = 'Power4.easeInOut',
stagger = duration * .2;
var minHeight = 45;
var brandHeight = minHeight - 10;
var classNameResized = 'resized';
var classNameHidden = 'has-hided';
var fadeTween = null,
heightTween = null,
paddingTween = null,
imageTween = null;
header = $('header');
pageContainer = $('#page-container');
pageContent = $('#page-content');
brandImage = $('.brand img');
brandHolder = $('.brand-holder');
headerContent = $('.header-content');
hideElement = $('.hide-element');
//functions
initTweens();
collapseHaeder();
function initTweens() {
fadeTween = TweenMax.to(hideElement, duration, {
autoAlpha: 0,
display: 'none',
ease: ease,
paused: true
});
heightTween = TweenMax.to([header, brandHolder, headerContent], duration, {
height: minHeight,
ease: ease,
paused: true,
delay: stagger
});
paddingTween = TweenMax.to(pageContainer, duration, {
paddingTop: minHeight,
ease: ease,
paused: true,
delay: stagger
});
imageTween = TweenMax.to(brandImage, duration, {
height: brandHeight,
ease: ease,
paused: true,
delay: stagger
});
}
function addClasses() {
if (!header.hasClass(classNameResized)) {
header.addClass(classNameResized);
brandHolder.addClass(classNameResized);
headerContent.addClass(classNameResized);
}
if (!hideElement.hasClass(classNameHidden)) {
hideElement.addClass(classNameHidden);
}
}
function removeClasses() {
if (header.hasClass(classNameResized)) {
header.removeClass(classNameResized);
brandHolder.removeClass(classNameResized);
headerContent.removeClass(classNameResized);
}
if (hideElement.hasClass(classNameHidden)) {
hideElement.removeClass(classNameHidden);
}
}
function collapseHaeder() {
if (pageContainer.hasClass('collapse-header')) {
var waypoint = new Waypoint({
element: pageContent,
handler: function (direction) {
if (direction == 'up') {
fadeTween.reverse();
heightTween.reverse();
paddingTween.reverse();
imageTween.reverse();
removeClasses();
} else {
fadeTween.play();
heightTween.play();
paddingTween.play();
imageTween.play();
addClasses();
}
}
});
}
}
}
There are many things that can be improved here in terms of code structure, animation style etc e.g. animating translateY instead of height for a more smooth animation (Link & Link) but I have tried to keep the structure / approach of the code untouched.
Hope it helps.
P.S. I would strongly recommend to take a look at TimelineMax as well for all your sequencing needs in animations. I have also forked another version using TimelineMax.
T

Javascript module pattern - what am I doing wrong?

A working version of this is here: http://est.pagodabox.com/client/svedka
I have the following function which I'm trying to convert into a module pattern, but when I try to use one of the function that I return at the bottom, for example:
est_project.closeContent($html);
I get an error that it's not a function. Is there something i'm doing wrong here?
Thanks!
var est_project = (function(){
// Setup functions
var flexDestroy,
cloneCurrent,
clonePosition,
switchSlide,
projectLayout,
contentHeight,
slidePos,
slideClick,
infoToggle,
closeContent;
// Destroy flexslider
flexDestroy = function($slider,$cleanSlider, $projBg) {
// Insert the clone of the un-initialized slide element, and remove the current flexslider
// Effectively "destroys" the current slider
var $curSlide = $slider.find('.flex-active-slide'),
// Get the zero based index of current slide
curSlideIndex = $curSlide.index() - 1,
curBg = $curSlide.find('img').attr('src'),
slideCount = $cleanSlider.data('count'),
i = 0,
$rearrange = $('');
// When you switch projects, the current slide should stay put
if(curSlideIndex !== 0 && slideCount > 1) {
// Cut from the current slide to the end, paste at the beginning
for(i = 0 ; i < slideCount; i += 1) {
if(curSlideIndex > i) {continue;}
$rearrange = $rearrange.add( $cleanSlider.find('li:eq(' + i + ')') );
}
$rearrange.remove();
$cleanSlider.find('li:first-child').before($rearrange)
$cleanSlider.css({'background-image' : 'url(' + curBg + ')'});
}
$slider.after($cleanSlider).remove();
clonePosition(slideheight);
};
return {
// Clone current
cloneCurrent: function($el) {
var $clean,
slideCount = $el.find('li').length;
$clean = $el.clone();
$clean.removeClass('project-current').find('div').removeClass('img-loading');
$clean.data('count',slideCount);
return $clean;
},
// Set the clone position, for when we add it to the DOM or resize the window
clonePosition: function(slideheight) {
var n = $cleanSlider.index(),
$myBg = $cleanSlider.find('div'),
myPosition = n * slideheight;
// Set the position of the inserted clone
$cleanSlider
.css({height: slideheight, top: myPosition, position : 'absolute'});
$myBg
.css({height: slideheight});
},
switchSlide: function($me, $slider) {
$('.project-current').removeClass('project-current');
$me.addClass('project-current');
// Get rid of current flexslider
flexDestroy($slider,$cleanSlider);
// Clone the unitialized slider so we can add it back in later when it gets destroyed
$cleanSlider = cloneCurrent($me);
$me.addClass('flexslider').flexslider({
animation: "slide",
animationSpeed: 500,
slideshow: false,
manualControls: '.dot-nav li a'
});
// After the flexslider initializes, slide the content
setTimeout(function(){
slidePos($me, $slidewrap, slideheight, $win);
},100);
},
// Custom "masonry" function, absolutely positions each project div according to the slide height
projectLayout: function(slideheight,$proj,$projBg) {
var n = 0;
$proj.each(function(){
var $me = $(this),
myPosition = n * slideheight;
// Set all the heights
$me
.css({top: myPosition, position : 'absolute'})
.add($projBg)
.css({height: slideheight});
n++;
});
},
// Set slide wrapper height to window height
contentHeight: function($win, $slidewrap) {
var winHeight = $win.height();
$slidewrap.css({height: winHeight});
},
// Set slide wrapper position to slide to the clicked slide, and set content position
slidePos: function($me, $slidewrap, slideheight, $win) {
var $contentText = $('.project-content .text'),
projNavHeight = Math.round( $win.height() * .1 ),
curIndex = $me.index(),
curTop = 0 - (curIndex * slideheight) + projNavHeight;
$slidewrap.css({transform: 'translate(0,' + curTop.toString() + 'px)'});
$('.corner-btn').add($contentText).css({'padding-top' : projNavHeight});
setTimeout(function(){
$slidewrap.removeClass('tr-none movin').addClass('tr-all');
$('.project').css({opacity: .4})
}, 100);
},
// Click a project, slide to it
slideClick: function($proj) {
$('.project').live('click',function(){
var $me = $(this),
myHref = $me.data('href'),
myTitle = $me.data('title'),
$slider = $('.flexslider'),
indexMy = $me.index(),
indexCur = $('.project-current').index(),
projDir;
$me.css({opacity: 1});
// Stop here if we click on the current project
if($me.hasClass('project-current')) {
return false;
}
History.pushState(null,myTitle,myHref);
});
},
// Hide and show content
infoToggle: function() {
// Open content
$('#corner-btn-info').on('click',function(){
$html.addClass('show-content');
if($('.project-content .text').height() <= $win.height()) {
$html.addClass('no-overflow');
}
$('.project-content-wrap').css({'z-index': 10});
});
// Close content
$('#corner-btn-close').live('click',function(){
closeContent($html);
});
},
closeContent: function($html) {
$html.removeClass('show-content');
setTimeout(function(){
$('.project-content-wrap').css({'z-index': -1});
$html.removeClass('no-overflow');
$('#classy').animate({scrollTop: 0})
},300);
}
};
});
The problem is that you're not executing the anonymous function, your code is the equivalent of:
var est_project = function() {};
You need to execute the function if you want it to return the functions defined in it.
Just replace the last line:
});
By:
}());
Or you can keep your code and call the closeContent function like this:
est_project().closeContent();
But I guess that's not what you want :-) You'd instantiate a new object everytime you call the est_project function.
At the start and end of your file just attach the object to window with the executed function and wrap whole function inside a self executing function. like this
(function(global) {
//your code goes here
global.est_project = est_project();
})(this)

Categories

Resources