GSAP Scrolltrigger not working with multiple triggers - javascript

I have multiple triggers each does the following:
Show/Removes a canvas
Changes the background colour of a section
Plays/Pauses video when in view
Plays/Pauses CSS animation
The first two work when without the other 2 triggers and I get this error on the play pause video: Uncaught TypeError: Cannot read properties of null (reading 'play')
However, I have followed the GSAP tutorial on this so I can only assume there is a conflict with the previous triggers.
const hideWebGl = gsap.timeline({
scrollTrigger: {
trigger: ".wriggle-cursor",
scrub: true,
start: 'top 100%',
end: 'top -125%',
onEnter: () => $('.home #canvas').css('display','block'),
onLeave: () => $('.home #canvas').css('display','none'),
onEnterBack: () => $('.home #canvas').css('display','block'),
onLeaveBack: () => $('.home #canvas').css('display','none'),
},
});
const projectsBackground = gsap.timeline({
scrollTrigger: {
trigger: ".projects-section-next .whatwedo",
scrub: true,
start: 'top 60%',
end: 'top -300%',
onEnter: () => $('.projects-section-next .whatwedo').css('background-color','white'),
onLeave: () => $('.projects-section-next .whatwedo').css('background-color','transparent'),
onEnterBack: () => $('.projects-section-next .whatwedo').css('background-color','white'),
onLeaveBack: () => $('.projects-section-next .whatwedo').css('background-color','transparent'),
},
});
let allVideoDivs = gsap.utils.toArray('.vid-gsap');
allVideoDivs.forEach((videoDiv, i) => {
let videoElem = videoDiv.querySelector('video')
ScrollTrigger.create({
trigger: videoElem,
start: 'top 80%',
end: 'top 20%',
markers: true,
onEnter: () => videoElem.play(),
onEnterBack: () => videoElem.play(),
onLeave: () => videoElem.pause(),
onLeaveBack: () => videoElem.pause(),
});
});
let allCssAnimation = gsap.utils.toArray('.ticker-marquee');
allCssAnimation.forEach((cssAnimation, i) => {
let cssElem = cssAnimation.querySelector('video')
ScrollTrigger.create({
trigger:cssElem,
start: 'top 80%',
end: 'top 20%',
markers: true,
onEnter: () => cssElem.css('-webkit-animation-play-state','running'),
onLeave: () => cssElem.css('-webkit-animation-play-state','paused'),
onEnterBack: () => cssElem.css('-webkit-animation-play-state','running'),
onLeaveBack: () => cssElem.css('-webkit-animation-play-state','paused'),
});
});

I changed the video play pause section to this, and it fixed it.
const videos = gsap.utils.toArray('video')
videos.forEach(function(video, i) {
ScrollTrigger.create({
trigger: video,
start: 'top center',
end: 'bottom center',
onEnter: () => video.play(),
onEnterBack: () => video.play(),
onLeave: () => video.pause(),
onLeaveBack: () => video.pause(),
});
})

Related

Call Alpine js from javascript

I have Alpinajs and Splidejs on page. I want to change aplinajs variable from splide event
document.addEventListener('alpine:init', () => {
Alpine.data('titles', () => ({
slidetitle_1: true,
toggle(n) {
this["slidetitle_" + n] = true
}
}))
})
then when in slider event
var splide = new Splide('.splide').mount();
splide.on( 'active', function (t) {
// not working
Alpine.data('titles', () => {
this["slidetitle_" + 1] = true;
});
// not working
Alpine.$data.titles.toggle(1);
});
but none of this working

How could I optimize this js animation?

I've been trying to build this animation with three palm leaves gently swaying as a background for a website, but it needs to run as smoothly as possible without messing with the page loading time too much. I'm pretty new to js so the code I've got at the moment is probably not optimal,
Therefore I am asking if I could get some suggestions on how I could optimize the code to make it run better and faster. What do you think?
this is the code right now
const background = document.querySelector(".leaf-background");
const leafs1 = document.createElement("div");
const leafs2 = document.createElement("div");
const leafs3 = document.createElement("div");
leafs1.classList.add("leaf-1");
leafs2.classList.add("leaf-2");
leafs3.classList.add("leaf-3");
background.appendChild(leafs1);
background.appendChild(leafs2);
background.appendChild(leafs3);
let animateLeafs1 = () => {
anime({
targets: ".leaf-1",
rotateZ: () => {
return anime.random(-5, 5);
},
rotateX: () => {
return anime.random(-30, 30);
},
easing: "linear",
duration: 3000,
complete: animateLeafs1
});
};
animateLeafs1();
let animateLeafs2 = () => {
anime({
targets: ".leaf-2",
//rotateZ: () => {
// return anime.random(-5, 5);
//},
rotateX: () => {
return anime.random(40, -40);
},
easing: "linear",
duration: 4000,
complete: animateLeafs2
});
};
animateLeafs2();
let animateLeafs3 = () => {
anime({
targets: ".leaf-3",
rotateZ: () => {
return anime.random(-2, 2);
},
rotateX: () => {
return anime.random(30, -30);
},
easing: "linear",
duration: 4000,
complete: animateLeafs3
});
};
animateLeafs3();
does this work? i cant even test it
const background = document.querySelector(".leaf-background");
const leafArray = []
// array starts from 0
for(var i=0; i<3;i++){
leafArray[i]= document.createElement("div");
// remember that classes start from 0 ---> leaf-0, leaf-1 , leaf-2
leafArray[i].classList.add("leaf-" + i);
// add a common class for selector
leafArray[i].classList.add("generalLeafs")
background.appendChild(leafArray[i]);
}
console.log(leafArray);
let animateLeafs = () => {
anime({
targets: ".generalLeafs",
rotateZ: () => {
return anime.random(-5, 5);
},
rotateX: () => {
return anime.random(-30, 30);
},
easing: "linear",
duration: 3000,
complete: animateLeafs
});
};
animateLeafs();
start you class names from 0

Js file doesn't work when imported in Angular project

I've used an HTML template in my angular project. Everything seems fine until I realized that some js files doesn't work which affected the whole project. The most important part is even the main.js file didn't work. My lect said to ignore the js and recreate which part doesn't work but I'm tryna use this file because there are many things in there.
Here are the codes in the main.js file, is there something here that is not compatible with Angular?
This is my first time using StackOverflow so I'm sorry if there's anything wrong with the way I'm asking
(function () {
"use strict";
/**
* Easy selector helper function
*/
const select = (el, all = false) => {
el = el.trim()
if (all) {
return [...document.querySelectorAll(el)]
} else {
return document.querySelector(el)
}
}
/**
* Easy event listener function
*/
const on = (type, el, listener, all = false) => {
let selectEl = select(el, all)
if (selectEl) {
if (all) {
selectEl.forEach(e => e.addEventListener(type, listener))
} else {
selectEl.addEventListener(type, listener)
}
}
}
/**
* Easy on scroll event listener
*/
const onscroll = (el, listener) => {
el.addEventListener('scroll', listener)
}
/**
* Navbar links active state on scroll
*/
let navbarlinks = select('#navbar .scrollto', true)
const navbarlinksActive = () => {
let position = window.scrollY + 200
navbarlinks.forEach(navbarlink => {
if (!navbarlink.hash) return
let section = select(navbarlink.hash)
if (!section) return
if (position >= section.offsetTop && position <= (section.offsetTop + section.offsetHeight)) {
navbarlink.classList.add('active')
} else {
navbarlink.classList.remove('active')
}
})
}
window.addEventListener('load', navbarlinksActive)
onscroll(document, navbarlinksActive)
/**
* Scrolls to an element with header offset
*/
const scrollto = (el) => {
let header = select('#header')
let offset = header.offsetHeight
let elementPos = select(el).offsetTop
window.scrollTo({
top: elementPos - offset,
behavior: 'smooth'
})
}
/**
* Toggle .header-scrolled class to #header when page is scrolled
*/
let selectHeader = select('#header')
if (selectHeader) {
const headerScrolled = () => {
if (window.scrollY > 100) {
selectHeader.classList.add('header-scrolled')
} else {
selectHeader.classList.remove('header-scrolled')
}
}
window.addEventListener('load', headerScrolled)
onscroll(document, headerScrolled)
}
/**
* Back to top button
*/
let backtotop = select('.back-to-top')
if (backtotop) {
const toggleBacktotop = () => {
if (window.scrollY > 100) {
backtotop.classList.add('active')
} else {
backtotop.classList.remove('active')
}
}
window.addEventListener('load', toggleBacktotop)
onscroll(document, toggleBacktotop)
}
/**
* Mobile nav toggle
*/
on('click', '.mobile-nav-toggle', function(e) {
select('#navbar').classList.toggle('navbar-mobile')
this.classList.toggle('bi-list')
this.classList.toggle('bi-x')
})
/**
* Mobile nav dropdowns activate
*/
on('click', '.navbar .dropdown > a', function(e) {
if (select('#navbar').classList.contains('navbar-mobile')) {
e.preventDefault()
this.nextElementSibling.classList.toggle('dropdown-active')
}
}, true)
/**
* Scrool with ofset on links with a class name .scrollto
*/
on('click', '.scrollto', function(e) {
if (select(this.hash)) {
e.preventDefault()
let navbar = select('#navbar')
if (navbar.classList.contains('navbar-mobile')) {
navbar.classList.remove('navbar-mobile')
let navbarToggle = select('.mobile-nav-toggle')
navbarToggle.classList.toggle('bi-list')
navbarToggle.classList.toggle('bi-x')
}
scrollto(this.hash)
}
}, true)
/**
* Scroll with ofset on page load with hash links in the url
*/
window.addEventListener('load', () => {
if (window.location.hash) {
if (select(window.location.hash)) {
scrollto(window.location.hash)
}
}
});
/**
* Preloader
*/
let preloader = select('#preloader');
if (preloader) {
window.addEventListener('load', () => {
preloader.remove()
});
}
/**
* Clients Slider
*/
new Swiper('.clients-slider', {
speed: 400,
loop: true,
autoplay: {
delay: 5000,
disableOnInteraction: false
},
slidesPerView: 'auto',
pagination: {
el: '.swiper-pagination',
type: 'bullets',
clickable: true
},
breakpoints: {
320: {
slidesPerView: 2,
spaceBetween: 40
},
480: {
slidesPerView: 3,
spaceBetween: 60
},
640: {
slidesPerView: 4,
spaceBetween: 80
},
992: {
slidesPerView: 6,
spaceBetween: 120
}
}
});
/**
* Porfolio isotope and filter
*/
window.addEventListener('load', () => {
let portfolioContainer = select('.portfolio-container');
if (portfolioContainer) {
let portfolioIsotope = new Isotope(portfolioContainer, {
itemSelector: '.portfolio-item'
});
let portfolioFilters = select('#portfolio-flters li', true);
on('click', '#portfolio-flters li', function(e) {
e.preventDefault();
portfolioFilters.forEach(function(el) {
el.classList.remove('filter-active');
});
this.classList.add('filter-active');
portfolioIsotope.arrange({
filter: this.getAttribute('data-filter')
});
portfolioIsotope.on('arrangeComplete', function() {
AOS.refresh()
});
}, true);
}
});
/**
* Initiate portfolio lightbox
*/
const portfolioLightbox = GLightbox({
selector: '.portfolio-lightbox'
});
/**
* Portfolio details slider
*/
new Swiper('.portfolio-details-slider', {
speed: 400,
loop: true,
autoplay: {
delay: 5000,
disableOnInteraction: false
},
pagination: {
el: '.swiper-pagination',
type: 'bullets',
clickable: true
}
});
/**
* Testimonials slider
*/
new Swiper('.testimonials-slider', {
speed: 600,
loop: true,
autoplay: {
delay: 5000,
disableOnInteraction: false
},
slidesPerView: 'auto',
pagination: {
el: '.swiper-pagination',
type: 'bullets',
clickable: true
}
});
/**
* Animation on scroll
*/
window.addEventListener('load', () => {
AOS.init({
duration: 1000,
easing: "ease-in-out",
once: true,
mirror: false
});
});
})()```
You need to export this code logic, and import it in the files that you need it in.
Let's put all your code in a file called a.js.
Your file becomes
export default a = (function () {
"use strict";
...
let's create a file called b.js on the same level as a.js
And later you import it in b.js as follows
import a from "./a"
Note
Once you import a.js into any file, all the logic inside it will be automatically executed (you can see the function execution at the end) because the function self executes.

Access the outer items of an object in a function of that object

I've used Dock of PrimeReact.
<Dock model={options.buttons} position="right"/>
According to its document, For model we should pass its dockItems.
So I defined options like this:
I have an array of objects. Inside it I have buttons, I want to check if the xValue doesn't have value then I show an error message.
const options = [
{
xValue: '',
uperMessage: 'Select a start date',
lowerMessage: '',
setMessage: function (time) {
this.lowerMessage = `Selected start date: ${dateHelper.formatDate(time)}`;
},
buttons: [
{
label: 'Next',
icon: () => <Button label="Next" className="p-button-success" />,
command: function () {
if (!this.xValue) {
toast.current.show({ severity: 'error', summary: 'Error', detail: 'Select a start date!', life: 3000 });
} else {
setCurrentStep(2);
}
}
},
{
label: 'Cancel',
icon: () => <Button label="Cancel" className="p-button-secondary" />,
command: () => {
cancel()
}
}
]
},
{
xValue: '',
uperMessage: 'Select a end date',
lowerMessage: '',
setMessage: function (time) {
this.lowerMessage = `Selected end date: ${dateHelper.formatDate(time)}`;
},
buttons: [
{
label: 'Next',
icon: () => <Button label="Next" className="p-button-success" />,
command: function (xValue) {
if (!this.xValue) {
toast.current.show({ severity: 'error', summary: 'Error', detail: 'Select a Divergence end date!', life: 3000 });
} else {
setCurrentStep(3);
}
}
},
{
label: 'Cancel',
icon: () => <Button label="Cancel" className="p-button-secondary" />,
command: () => {
cancel()
}
}]
}];
How can I access the xValue in the command function?
If I use this, it gets the items of the current scope. How can I access the items of the parent?
Any help would be greatly appreciated.
You can change command function and map on the options items, also buttons items. Finally you can find the current item and get the xValue :
command: function () {
let $this = this;
options.map((option,oindex)=>{
option.buttons.map(button,binex)=>{
if(button == $this)
if (!option.xValue) {
toast.current.show({ severity: 'error', summary: 'Error', detail: 'Select a start date!', life: 3000 });
} else {
setCurrentStep(2);
}
})
})
you could use the array name "options" to access xValue like that
if(!options[item_index]['xValue']){
....
}
for example:- options[0]["xValue"] => // return ''

disabling a scrollmagic controller in an object literal es6

i am having an issue trying to reenable a scrollmagic controller if it has been disabled before.
i want to have the logo color change only triggered if its a narrow viewport (if the logo is in the colored area) and disabled if its wide..that works so far
but if i resize the window to narrow again it won't reenable the controller..i tried to destroy and reset the controller as well but somehow it won't reenable the controller...
codepen (gsap and scrollmagic used):
https://codepen.io/HendrikEng/pen/owyBYz?editors=0011
js:
const mobile = {
controller: new ScrollMagic.Controller(),
changeLogo: {
init: () => {
console.log("init tweens an scrollmagic");
const tweens = {
enterOuter: () => {
TweenMax.fromTo(
".c-logo__outer",
1,
{ fill: "#4dabfc" },
{ fill: "#fff" }
);
},
enterInner: () => {
TweenMax.fromTo(
".c-logo__inner",
1,
{ fill: "#fff" },
{ fill: "#4dabfc" }
);
},
leaveOuter: () => {
TweenMax.fromTo(
".c-logo__outer",
1,
{ fill: "#fff" },
{ fill: "#4dabfc" }
);
},
leaveInner: () => {
TweenMax.fromTo(
".c-logo__inner",
1,
{ fill: "#4dabfc" },
{ fill: "#fff" }
);
}
};
const trigger = document.querySelectorAll(".js-change-logo");
trigger.forEach(id => {
const scene = new ScrollMagic.Scene({
triggerElement: id,
reverse: true,
triggerHook: 0.065,
duration: id.clientHeight
})
.on("enter", () => {
tweens.enterOuter();
tweens.enterInner();
})
.on("leave", () => {
tweens.leaveOuter();
tweens.leaveInner();
})
.addIndicators()
.addTo(mobile.controller);
});
},
destroyTweens: () => {
console.log("kill tweens");
TweenMax.killTweensOf(".c-logo__outer");
TweenMax.killTweensOf(".c-logo__inner");
TweenMax.set(".c-logo__outer", { clearProps: "all" });
TweenMax.set(".c-logo__inner", { clearProps: "all" });
}
}
};
$(window).on("resize", function() {
var win = $(this); //this = window
if (win.width() <= 450) {
// reanble controller if disabledbed before - doesnt work
mobile.controller.enabled(true);
mobile.changeLogo.init();
} else {
// disable scrollmagic controller
mobile.controller.enabled(false);
// destroy tweens
mobile.changeLogo.destroyTweens();
}
}).resize();
#hendrikeng I hope you don't mind, but I changed your code quite a lot. I've found myself needing to do this exact thing numerous times recently, so I based a lot of it on my own work.
I think the largest issue was that you were running a lot of functions on every resize which is not very performant and also makes it difficult to keep track of what's initialised and what's not. Mine relies on an init_flag so that it is only setup once.
There are then methods to update (duration on resize if needed) and destroy.
https://codepen.io/motionimaging/pen/848366af015cdf3a90de5fb395193502/?editors=0100
const mobile = {
init_flag: false,
init: () => {
$(window).on('resize', function(){
const width = $(window).width();
if( width <= 450 ){
if(! mobile.init_flag ){
mobile.setup();
} else {
mobile.update();
}
} else {
if( mobile.init_flag ){
mobile.destroy();
}
}
});
},
setup: () => {
mobile.init_flag = true;
mobile.triggers = document.querySelectorAll('.js-change-logo');
const tweens = {
enterOuter: () => {
TweenMax.fromTo(
'.c-logo__outer',
1,
{ fill: '#4dabfc' },
{ fill: '#fff' }
);
},
enterInner: () => {
TweenMax.fromTo(
'.c-logo__inner',
1,
{ fill: '#fff' },
{ fill: '#4dabfc' }
);
},
leaveOuter: () => {
TweenMax.fromTo(
'.c-logo__outer',
1,
{ fill: '#fff' },
{ fill: '#4dabfc' }
);
},
leaveInner: () => {
TweenMax.fromTo(
'.c-logo__inner',
1,
{ fill: '#4dabfc' },
{ fill: '#fff' }
);
}
};
mobile.controller = new ScrollMagic.Controller();
mobile.triggers.forEach(el => {
el.scene = new ScrollMagic.Scene({
triggerElement: el,
reverse: true,
triggerHook: 0.065,
duration: el.clientHeight
})
.on('enter', () => {
tweens.enterOuter();
tweens.enterInner();
})
.on('leave', () => {
tweens.leaveOuter();
tweens.leaveInner();
})
.addIndicators()
.addTo(mobile.controller);
});
},
update: () => {
if( mobile.init_flag ){
mobile.triggers.forEach(el => {
el.scene.duration(el.clientHeight);
});
}
},
destroy: function(){
mobile.controller.destroy(true);
mobile.init_flag = false;
$('.c-logo > *').attr('style', '');
},
};
mobile.init();

Categories