Skip to content

Instantly share code, notes, and snippets.

@leochocolat
Created November 22, 2020 16:30
Show Gist options
  • Save leochocolat/1cd77e120ed621a3922f9eb163e6c932 to your computer and use it in GitHub Desktop.
Save leochocolat/1cd77e120ed621a3922f9eb163e6c932 to your computer and use it in GitHub Desktop.
// Vendors
import gsap from 'gsap';
import InertiaPlugin from '@/vendor/gsap/InertiaPlugin';
import SplitText from '@/vendor/gsap/SplitText';
// Utils
import lerp from '@/utils/math/lerp';
import device from '@/utils/device';
import WindowResizeObserver from '@/utils/WindowResizeObserver';
// Components
import Section from '@/components/Section';
import Arrow from '@/assets/images/icons/arrow-right.svg?inline';
const MAX_OVERDRAG_DISTANCE = 300;
const MAX_OVERDRAG_DISTANCETOUCH = 100;
const CLICK_TRESHOLD = 3;
export default {
extends: Section,
props: ['state'],
data() {
return {
allowHover: true,
};
},
components: {
Arrow,
},
mounted() {
this.positionX = {
current: 0,
target: 0,
};
this.dragMousePositionX = 0;
this.deltaX = 0;
this.activeIndex = null;
this.touchStartMousePosition = { x: 0, y: 0 };
// Flags
this.allowDrag = true;
// Language
this.direction = this.$i18n.locale === 'en' ? 1 : -1;
if (this.$root.webglApp) {
this.scene = this.$root.webglApp.getScene('portfolio');
}
this.titles = this.splitTitles();
this.setActiveItem(0);
this.setupEventListeners();
this.resize();
this.track();
this.setupSlides();
this.checkIfDragAllowed();
this.checkIfSlideAllowed();
},
beforeDestroy() {
this.removeEventListeners();
// this.scene.destroySlides();
},
methods: {
/**
* Public
*/
hide() {
this.disableDrag();
this.disableHover();
},
show() {
this.updateItems();
this.checkIfDragAllowed();
this.checkIfSlideAllowed();
this.enableHover();
},
hideItems() {
this.disableDrag();
this.disableHover();
return new Promise((resolve) => {
const tl = new gsap.timeline({ onComplete: resolve });
tl.timeScale(1.2);
tl.to(this.$refs.list, 1.3, { x: -(WindowResizeObserver.width / 3) * this.direction, ease: 'power3.in' });
tl.to(this.$refs.item, 1, { autoAlpha: 0, ease: 'power3.in' }, 0.3);
tl.add(this.scene.hideCarouselSlides(), 0.2);
tl.add(this.$root.webglApp.hideScene(), 0.7);
});
},
showItems() {
return new Promise((resolve) => {
const tl = new gsap.timeline({
onComplete: () => {
resolve();
this.checkIfDragAllowed();
this.checkIfSlideAllowed();
this.enableHover();
},
});
tl.timeScale(1.2);
tl.to(this.$refs.item, 0.5, { autoAlpha: 1, ease: 'power3.inOut' });
tl.fromTo(this.$refs.list, 1.5, { x: (WindowResizeObserver.width / 3) * this.direction }, { x: 0, ease: 'power3.out' }, 0);
tl.add(this.$root.webglApp.showScene('portfolio'), 0);
tl.add(this.scene.showCarouselSlides(), 0.2);
});
},
updateItems() {
this.resetSliderPosition();
this.$nextTick(this.setupSlides);
},
enableDrag() {
this.allowDrag = true;
},
disableDrag() {
this.allowDrag = false;
},
enableHover() {
this.allowHover = true;
},
disableHover() {
this.allowHover = false;
this.mouseleaveHandler();
},
enableLeftArrow() {
if (this.disableLeftTween) this.disableLeftTween.kill();
this.$refs.buttonLeft.style.pointerEvent = 'all';
this.$refs.buttonLeft.disabled = false;
this.enableLeftTween = gsap.to(this.$refs.buttonLeft, 0.5, { autoAlpha: 1, x: 0, ease: 'power3.out' });
},
disableLeftArrow() {
if (this.enableLeftTween) this.enableLeftTween.kill();
this.$refs.buttonLeft.style.pointerEvent = 'none';
this.$refs.buttonLeft.disabled = true;
this.disableLeftTween = gsap.to(this.$refs.buttonLeft, 0.5, { autoAlpha: 0, x: 20, ease: 'power3.in' });
},
enableRightArrow() {
if (this.disableRightTween) this.disableRightTween.kill();
this.$refs.buttonRight.style.pointerEvent = 'all';
this.$refs.buttonRight.disabled = false;
this.enableRightTween = gsap.to(this.$refs.buttonRight, 0.5, { autoAlpha: 1, x: 0, ease: 'power3.out' });
},
disableRightArrow() {
if (this.enableRightTween) this.enableRightTween.kill();
this.$refs.buttonRight.style.pointerEvent = 'none';
this.$refs.buttonRight.disabled = true;
this.disableRightTween = gsap.to(this.$refs.buttonRight, 0.5, { autoAlpha: 0, x: -20, ease: 'power3.in' });
},
/**
* Private
*/
checkIfDragAllowed() {
if (this.data.length < 2) {
this.disableDrag();
} else {
this.enableDrag();
}
},
checkIfSlideAllowed() {
if (this.activeIndex === 0) {
this.disablePrevious();
} else {
this.enablePrevious();
}
if (this.activeIndex === this.data.length - 1) {
this.disableNext();
} else {
this.enableNext();
}
},
disableNext() {
if (this.direction === 1) {
this.disableRightArrow();
} else {
this.disableLeftArrow();
}
},
enableNext() {
if (this.direction === 1) {
this.enableRightArrow();
} else {
this.enableLeftArrow();
}
},
disablePrevious() {
if (this.direction === 1) {
this.disableLeftArrow();
} else {
this.disableRightArrow();
}
},
enablePrevious() {
if (this.direction === 1) {
this.enableLeftArrow();
} else {
this.enableRightArrow();
}
},
setupSlides() {
this.scene.destroySlides();
this.scene.setupSlides(this.data, this.direction);
},
resize(e) {
this.width = WindowResizeObserver.width;
this.height = WindowResizeObserver.height;
this.getBounds();
this.resetSliderPosition();
},
track() {
const track1 = InertiaPlugin.track(this.positionX, 'target')[0];
const track2 = InertiaPlugin.track(this.positionX, 'current')[0];
},
getBounds() {
this.listRect = this.$refs.list.getBoundingClientRect();
},
getSnapPoint(endValue) {
const interval = -this.listRect.width * this.direction;
const snapPoint = Math.round(endValue / interval) * interval;
const activeIndex = Math.min(Math.max(snapPoint / interval, 0), this.$refs.item.length - 1);
this.setActiveItem(activeIndex);
return snapPoint;
},
getLimits() {
const interval = -this.listRect.width;
let min = interval * (this.$refs.item.length - 1);
let max = 0;
if (this.direction === -1) {
max = interval * (this.$refs.item.length - 1) * -1;
min = 0;
}
return {
max,
min,
};
},
resetSliderPosition() {
this.positionX.target = 0;
this.positionX.current = 0;
this.setActiveItem(0);
this.scene.resetCarouselPosition(0);
},
updateTargetPosition() {
const maxOverdragDistance = device.isTouch() ? MAX_OVERDRAG_DISTANCETOUCH : MAX_OVERDRAG_DISTANCE;
const maxDragDistance = this.listRect.width * (this.$refs.item.length - 1);
let distance = 0;
if (this.direction === 1) {
if (this.positionX.target > 0 && this.deltaX <= 0) {
distance = this.positionX.target;
} else if (this.positionX.target < -maxDragDistance && this.deltaX >= 0) {
distance = Math.abs(this.positionX.target - -maxDragDistance);
}
const ease = Math.sin(Math.min(distance / maxOverdragDistance, 1) * Math.PI);
this.positionX.target -= this.deltaX * (1 - ease);
} else {
this.positionX.target -= this.deltaX;
}
},
updatePosition() {
this.positionX.current = lerp(this.positionX.current, this.positionX.target, 0.2);
const normalizedValue = this.positionX.current / this.listRect.width;
this.transform(this.$refs.item, this.positionX.current);
this.updateSliderPosition(normalizedValue);
},
updateSliderPosition(x) {
if (!this.scene) return;
this.scene.updateCarouselPosition(x);
},
transform(elements, x) {
const transform = `matrix3d(1,0,0.00,0,0.00,1,0.00,0,0,0,1,0,${x},0,0,1)`;
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
element.style.transform = transform;
element.style.webkitTransform = transform;
element.style.mozTransform = transform;
}
},
throw() {
this.tween = gsap.to(this.positionX, {
inertia: {
duration: { max: 1 },
resistance: 0,
current: {
min: this.getLimits().min,
max: this.getLimits().max,
end: this.getSnapPoint,
},
},
onUpdate: () => {
this.positionX.target = this.positionX.current;
},
});
},
setActiveItem(index) {
if (index === null || index === undefined || isNaN(index)) return;
if (this.activeIndex === index) return;
if (this.activeIndex !== null) {
const chars = this.titles[this.activeIndex].chars;
if (this._setInactiveItemTimeline) this._setInactiveItemTimeline.kill();
this._setInactiveItemTimeline = gsap.timeline();
this._setInactiveItemTimeline.to(this.$refs.title[this.activeIndex].$el, 0.3, { alpha: 0.3, ease: 'sine.inOut' }, 0);
this._setInactiveItemTimeline.to(chars, 0.6, { webkitTextStrokeColor: 'rgba(245, 175, 87, 1)' }, 0);
this._setInactiveItemTimeline.to(
chars,
0.3,
{
color: 'rgba(245, 175, 87, 0)',
ease: 'sine.inOut',
},
0
);
}
this.activeIndex = index;
this.checkIfSlideAllowed();
// for (let i = 0; i < this.$refs.item.length; i++) {
// const item = this.$refs.item[i];
// item.classList.remove('is-active');
// }
// this.$refs.item[this.activeIndex].classList.add('is-active');
const chars = this.titles[this.activeIndex].chars;
if (this._setActiveItemTimeline) this._setActiveItemTimeline.kill();
this._setActiveItemTimeline = gsap.timeline();
this._setActiveItemTimeline.to(this.$refs.title[this.activeIndex].$el, 0.3, { alpha: 1, ease: 'sine.inOut' }, 0);
this._setActiveItemTimeline.to(chars, 0.6, { webkitTextStrokeColor: 'rgba(245, 175, 87, 0)' }, 0.4);
this._setActiveItemTimeline.to(
chars,
0.3,
{
color: () => {
const alpha = 0.3 + 0.7 * Math.random();
return `rgba(245, 175, 87, ${alpha})`;
},
ease: 'sine.inOut',
stagger: {
each: 0.01,
from: 'random',
},
},
0
);
this.scene.setActive(this.activeIndex);
},
goToIndex(index) {
if (this.tween) this.tween.kill();
const interval = -this.listRect.width * this.direction;
const destination = index * interval;
gsap.to(this.positionX, 0.5, { target: destination });
this.setActiveItem(index);
},
setupEventListeners() {
gsap.ticker.add(this.tickHandler);
window.addEventListener('resize', this.resizeHandler);
if (device.isTouch()) {
this.$el.addEventListener('touchstart', this.touchstartHandler);
this.$el.addEventListener('touchmove', this.touchmoveHandler);
window.addEventListener('touchend', this.touchendHandler);
} else {
this.$el.addEventListener('mousedown', this.touchstartHandler);
this.$el.addEventListener('mousemove', this.mousemoveHandler);
window.addEventListener('mouseup', this.touchendHandler);
}
},
removeEventListeners() {
gsap.ticker.remove(this.tickHandler);
window.removeEventListener('resize', this.resizeHandler);
if (device.isTouch()) {
this.$el.removeEventListener('touchstart', this.touchstartHandler);
this.$el.removeEventListener('touchmove', this.touchmoveHandler);
window.removeEventListener('touchend', this.touchendHandler);
} else {
this.$el.removeEventListener('mousedown', this.touchstartHandler);
this.$el.removeEventListener('mousemove', this.mousemoveHandler);
window.removeEventListener('mouseup', this.touchendHandler);
}
},
splitTitles() {
const titles = [];
let item;
let split;
for (let i = 0, len = this.$refs.title.length; i < len; i++) {
item = this.$refs.title[i].$el;
split = new SplitText(item, { type: 'chars', charsClass: 'item-chars' });
titles.push(split);
}
return titles;
},
/**
* Handlers
*/
mousemoveHandler(event) {
this.touchmoveHandler(event);
},
touchstartHandler(event) {
if (this.tween) this.tween.kill();
this.disableHover();
this.isDraging = true;
// this.$el.style.cursor = 'grabbing';
const x = event.clientX || event.touches[0].clientX;
const y = event.clientY || event.touches[0].clientY;
this.dragMousePositionX = x;
this.lastMousemovePositionX = null;
this.touchStartMousePosition = { x, y };
},
touchmoveHandler(event) {
if (!this.allowDrag) return;
if (!this.isDraging) return;
const x = event.clientX || event.touches[0].clientX;
this.deltaX = this.dragMousePositionX - x;
this.dragMousePositionX = x;
this.lastMousemovePositionX = x;
this.scene.dragHandler(this.deltaX);
this.updateTargetPosition();
},
touchendHandler(e) {
if (!this.isDraging) return;
const x = e.clientX || this.lastMousemovePositionX;
this.isDraging = false;
// this.$el.style.cursor = 'grab';
this.scene.dragEndHandler(0);
this.enableHover();
if (e.clientX && this.touchStartMousePosition.x === x) return;
if (!e.clientX && !this.lastMousemovePositionX) return;
this.throw();
},
projectClickHandler(e) {
const currentTarget = e.currentTarget;
const index = parseInt(currentTarget.dataset.index);
const currentPos = { x: e.clientX, y: e.clientY };
const distanceX = Math.abs(this.touchStartMousePosition.x - currentPos.x);
const distanceY = Math.abs(this.touchStartMousePosition.y - currentPos.y);
if (distanceX <= CLICK_TRESHOLD && distanceY <= CLICK_TRESHOLD) {
const url = e.currentTarget.dataset.link;
if (index === this.activeIndex) {
this.$router.push(url);
} else {
this.goToIndex(index);
setTimeout(() => {
this.$router.push(url);
}, 500);
}
}
},
mouseenterHandler(e) {
if (!this.allowHover) return;
const element = e.currentTarget;
const index = parseInt(element.dataset.index);
if (index !== this.activeIndex) return;
this.isHovering = true;
if (this.mouseenterTimeline) this.mouseenterTimeline.kill();
if (this.mouseleaveTimeline) this.mouseleaveTimeline.kill();
this.mouseenterTimeline = new gsap.timeline();
for (let i = 0; i < this.$refs.hoverContainer.length; i++) {
const item = this.$refs.hoverContainer[i];
if (item === element) continue;
this.mouseenterTimeline.to(item, 0.4, { alpha: 0.5, ease: 'sine.inOut' }, 0);
}
this.scene.hoverAnimationIn();
},
mouseleaveHandler(e) {
if (!this.isHovering) return;
this.isHovering = false;
if (this.mouseenterTimeline) this.mouseenterTimeline.kill();
if (this.mouseleaveTimeline) this.mouseleaveTimeline.kill();
this.mouseleaveTimeline = new gsap.timeline();
this.mouseleaveTimeline.to(this.$refs.hoverContainer, 0.4, { alpha: 1, ease: 'sine.inOut' }, 0);
this.scene.hoverAnimationOut();
},
tickHandler() {
this.updatePosition();
},
resizeHandler() {
this.resize();
},
clickNextHandler() {
const index = this.activeIndex + this.direction;
if (index > this.$refs.item.length - 1) return;
this.goToIndex(index);
},
clickPreviousHandler() {
const index = this.activeIndex - this.direction;
if (index < 0) return;
this.goToIndex(index);
},
},
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment