GSAP pause animation in a function - gsap

I have a set of buttons that when clicked show a sort of pop up and some simple animations. Each pop up contain the same animations for most of the content. Each pop up also has its own sets of animations so I have done the following to get this to work correctly.
$gridTitles.click(function() {
const tl = new TimelineMax();
const $pop = $(this).next('.grid__pop');
const $chars = $pop.find(".grid__pop-title span");
const $items = $pop.find(".grid__pop-list li");
const func = $(this).data("graphic-function");
tl.set($pop, {
autoAlpha: 0,
display: 'block',
scale: .5
})
.to($pop, 1, {
autoAlpha: 1,
scale: 1,
ease: Power2.easeInOut
})
.staggerFrom($chars, 0.01, {
autoAlpha: 0,
ease: Power2.easeIn
}, 0.1)
.add(graphicAnimation[func])
.staggerFrom($items, 0.8, {
autoAlpha: 0,
rotationX: 90,
ease: Power2.easeOut
}, .8);
return tl;
});
This runs my pop up code and also using the .add function I call another function that runs the specific animation for the pop up based on a data attribute matching the name of a function in an object.
const graphicAnimation = {
graphicServer: function() {
const tl = new TimelineMax();
const $server = $(".graphic-server__server");
const $one = $(".graphic-server__one");
const $two = $(".graphic-server__two");
const $three = $(".graphic-server__three");
return tl.to($server, 1, {
autoAlpha: 1,
xPercent: "0",
ease: Power2.easeInOut
})
.to($one, 1, {
autoAlpha: 1,
xPercent: "0",
ease: Power2.easeInOut
})
.to($two, 1, {
autoAlpha: 1,
xPercent: "0",
ease: Power2.easeInOut
})
.to($three, 1, {
autoAlpha: 1,
xPercent: "0",
ease: Power2.easeInOut
})
},
// more functions
}
This works great depending on which button is clicked the correct function is called in the object and my animations run. The problem now is that some of these animations are looping and when I close the popup I can't pause them.
Using something like the following I tried to accomplish this
$gridCloses.click(function() {
const tl = new TimelineMax();
const $pop = $(this).parents(".grid__pop");
const func = $(this).parents('.grid__pop').siblings('.grid__title').data("graphic-function");
graphicAnimation[func].pause();
return tl.to($pop, 1, {
autoAlpha: 0,
scale: .5,
display: 'none'
});
});
But calling graphicAnimation[func].pause(); isn't going to work as pause() is a function on the returned timeline from that function. How can I access the current running function and pause / kill it.

So I thought this out and just need to store the timelines in their own object as well
const graphicTimeLines = {
graphicServer : new TimelineMax(),
graphicType: new TimelineMax()
}
so with that in my closing action I can do something like the following to completely reset my loops and have the timeline be available to be called again.
graphicTimeLines[func].pause().progress(0).kill();
graphicTimeLines[func] = new TimelineMax();

Related

How can I get onHover to work for deck.gl MVTLayer?

The deck.gl MVTLayer inherits from Layer that has onHover enabled which together with pickable should give interactivity. I am trying to get interactivity to work so I can do a popup with the data I hover. But in below code, I can get the onClick event to fire, but not the onHover event. what am I doing wrong
Thanks :)
import React from "react";
import DeckGL from "#deck.gl/react";
import { MVTLayer } from "#deck.gl/geo-layers";
const INITIAL_VIEW_STATE = {
longitude: -122.41669,
latitude: 37.7853,
zoom: 13,
pitch: 0,
bearing: 0
};
const layer = new MVTLayer({
id: "MVTLayer",
data: [
"https://tiles-a.basemaps.cartocdn.com/vectortiles/carto.streets/v1/{z}/{x}/{y}.mvt"
],
stroked: false,
getLineColor: [255, 0, 0],
getFillColor: (f) => {
switch (f.properties.layerName) {
case "poi":
return [0, 0, 0];
case "water":
return [120, 150, 180];
case "building":
return [255, 0, 0];
default:
return [240, 240, 240];
}
},
getPointRadius: 2,
pointRadiusUnits: "pixels",
getLineWidth: (f) => {
switch (f.properties.class) {
case "street":
return 6;
case "motorway":
return 10;
default:
return 1;
}
},
maxZoom: 14,
minZoom: 0,
onHover: (info) => console.log("Hover", info.object),
onClick: (info) => console.log("Click", info.object),
pickable: true
});
export default function App() {
return (
<DeckGL
initialViewState={INITIAL_VIEW_STATE}
controller={true}
layers={[layer]}
></DeckGL>
);
}
It works .... I think I had a dodgy chrome instance running

jsPlumb + Panzoom infinite droppable canvas

I have created a codepen that uses jquery ui droppable(for drag/drop), jsPlumb (for flowcharting) and Panzoom (panning and zooming) to create a flowchart builder. You could drag the list items from the draggable container (1st column) to the flowchart (2nd column) and then connect the items using the dots to create a flowchart. The #flowchart is a Panzoom target with both pan and zoom enabled. This all works fine.
However, I would like to have the #flowchart div always span the whole area of the flowchart-wrapper i.e. the #flowchart should be an infinite canvas that supports panning, zooming and is a droppable container.
It should have the same effect as flowchart-builder-demo. The canvas there is infinite where you can drag and drop items (Questions, Actions, Outputs) from the right column.
Any pointers on how to achieve this (like the relevant events or multiple panzoom elements and/or css changes) would be greatly appreciated.
const BG_SRC_TGT = "#2C7BE5";
const HEX_SRC_ENDPOINT = BG_SRC_TGT;
const HEX_TGT_ENDPOINT = BG_SRC_TGT;
const HEX_ENDPOINT_HOVER = "#fd7e14";
const HEX_CONNECTOR = "#39afd1";
const HEX_CONNECTOR_HOVER = "#fd7e14";
const connectorPaintStyle = {
strokeWidth: 2,
stroke: HEX_CONNECTOR,
joinstyle: "round",
outlineStroke: "white",
outlineWidth: 1
},
connectorHoverStyle = {
strokeWidth: 3,
stroke: HEX_CONNECTOR_HOVER,
outlineWidth: 2,
outlineStroke: "white"
},
endpointHoverStyle = {
fill: HEX_ENDPOINT_HOVER,
stroke: HEX_ENDPOINT_HOVER
},
sourceEndpoint = {
endpoint: "Dot",
paintStyle: {
stroke: HEX_SRC_ENDPOINT,
fill: "transparent",
radius: 4,
strokeWidth: 3
},
isSource: true,
connector: ["Flowchart", { stub: [40, 60], gap: 8, cornerRadius: 5, alwaysRespectStubs: true }],
connectorStyle: connectorPaintStyle,
hoverPaintStyle: endpointHoverStyle,
connectorHoverStyle: connectorHoverStyle,
dragOptions: {},
overlays: [
["Label", {
location: [0.5, 1.5],
label: "Drag",
cssClass: "endpointSourceLabel",
visible: false
}]
]
},
targetEndpoint = {
endpoint: "Dot",
paintStyle: {
fill: HEX_TGT_ENDPOINT,
radius: 5
},
hoverPaintStyle: endpointHoverStyle,
maxConnections: -1,
dropOptions: { hoverClass: "hover", activeClass: "active" },
isTarget: true,
overlays: [
["Label", { location: [0.5, -0.5], label: "Drop", cssClass: "endpointTargetLabel", visible: false }]
]
};
const getUniqueId = () => Math.random().toString(36).substring(2, 8);
// Setup jquery ui draggable, droppable
$("li.list-group-item").draggable({
helper: "clone",
zIndex: 100,
scroll: false,
start: function (event, ui) {
var width = event.target.getBoundingClientRect().width;
$(ui.helper).css({
'width': Math.ceil(width)
});
}
});
$('#flowchart').droppable({
hoverClass: "drop-hover",
tolerance: "pointer",
drop: function (event, ui) {
var helper = $(ui.helper);
var fieldId = getUniqueId();
var offset = $(this).offset(),
x = event.pageX - offset.left,
y = event.pageY - offset.top;
helper.find('div.field').clone(false)
.animate({ 'min-height': '40px', width: '180px' })
.css({ position: 'absolute', left: x, top: y })
.attr('id', fieldId)
.appendTo($(this)).fadeIn('fast', function () {
var field = $("#" + fieldId);
jsPlumbInstance.draggable(field, {
containment: "parent",
scroll: true,
grid: [5, 5],
stop: function (event, ui) {
}
});
field.addClass('panzoom-exclude');
var bottomEndpoints = ["BottomCenter"];
var topEndPoints = ["TopCenter"];
addEndpoints(fieldId, bottomEndpoints, topEndPoints);
jsPlumbInstance.revalidate(fieldId);
});
}
});
const addEndpoints = (toId, sourceAnchors, targetAnchors) => {
for (var i = 0; i < sourceAnchors.length; i++) {
var sourceUUID = toId + sourceAnchors[i];
jsPlumbInstance.addEndpoint(toId, sourceEndpoint, { anchor: sourceAnchors[i], uuid: sourceUUID });
}
for (var j = 0; j < targetAnchors.length; j++) {
var targetUUID = toId + targetAnchors[j];
jsPlumbInstance.addEndpoint(toId, targetEndpoint, { anchor: targetAnchors[j], uuid: targetUUID });
}
$('.jtk-endpoint').addClass('panzoom-exclude');
}
// Setup jsPlumbInstance
var jsPlumbInstance = jsPlumb.getInstance({
DragOptions: { cursor: 'pointer', zIndex: 12000 },
ConnectionOverlays: [
["Arrow", { location: 1 }],
["Label", {
location: 0.1,
id: "label",
cssClass: "aLabel"
}]
],
Container: 'flowchart'
});
// Setup Panzoom
const elem = document.getElementById('flowchart');
const panzoom = Panzoom(elem, {
excludeClass: 'panzoom-exclude',
canvas: true
});
const parent = elem.parentElement;
parent.addEventListener('wheel', panzoom.zoomWithWheel);
I've just been working on the exact same issue and came across this as the only answer
Implementing pan and zoom in jsPlumb
The PanZoom used looks to be quite old - but the idea was the same, use the JQuery Draggable plugin for the movable elements, instead of the in-built JsPlumb one. This allows the elements to move out of bounds.
The below draggable function fixed it for me using the PanZoom library.
var that = this;
var currentScale = 1;
var element = $('.element');
element.draggable({
start: function (e) {
//we need current scale factor to adjust coordinates of dragging element
currentScale = that.panzoom.getScale();
$(this).css("cursor", "move");
that.panzoom.setOptions({ disablePan: true });
},
drag: function (e, ui) {
ui.position.left = ui.position.left / currentScale;
ui.position.top = ui.position.top / currentScale;
if ($(this).hasClass("jtk-connected")) {
that.jsPlumbInstance.repaintEverything();
}
},
stop: function (e, ui) {
var nodeId = $(this).attr('id');
that.jsPlumbInstance.repaintEverything();
$(this).css("cursor", "");
that.panzoom.setOptions({ disablePan: false });
}
});
I'm not sure if redrawing everything on drag is that efficient - so maybe just redraw both the connecting elements.

Get correct mouseover interaction in a ThreeJS VueJS app changing the window

I'm quite proud of what I've done: I have a menu which comprises 4 shapes. When you hover a shape, it's chaging color, growing and pushing the other shapes on top, while the rotation gets slower.
I read the ThreeJS docs and follow the advices of StackOverflow members.
I'm struggling with mouse interactions and window resizing: when I first open the browser, the mouseover doesn't seem to be called exactly when the mouse is over.
And when I resize the window, it's clearly messed up.
If anybody has a clue on what I'm doing wrong, thanks in advance :)
Here is my component:
<template>
<v-container>
<div #click="onClick" #mousemove="onMouseMove" id="menu3D" style="background-color: transparent; position: fixed; left: 20px; width:15%; height:100%;"></div>
<v-row class="text-center">
<v-col
class="mb-5"
cols="12"
>
<h2 class="headline font-weight-bold mb-3">
Accueil
</h2>
<v-row justify="center">
<p>
THIS IS ONLY A TEST
</p>
</v-row>
</v-col>
</v-row>
</v-container>
</template>
<script>
import * as Three from 'three'
export default {
name: 'Home',
mounted() {
this.init();
},
methods: {
init: function() {
this.createScene();
this.createCamera();
this.userData.formes.forEach(x=>this.createShape(x))
this.addSpotlight(16777215/*'#fdffab'*/);
this.addAmbientLight();
this.animate();
window.addEventListener('resize', this.onResize())
},
onResize: function() {
let container = document.getElementById('menu3D');
this.renderer.setSize(container.clientWidth, container.clientHeight);
this.camera.aspect = container.clientWidth / container.clientHeight;
this.camera.updateProjectionMatrix();
},
createScene: function() {
this.renderer = new Three.WebGLRenderer({
antialias: true,
alpha: true
});
let container = document.getElementById('menu3D');
this.renderer.setSize(container.clientWidth, container.clientHeight);
this.renderer.setPixelRatio(window.devicePixelRatio);
this.renderer.setClearColor(0xffffff,0);
container.appendChild(this.renderer.domElement);
},
createCamera: function() {
//let container = document.getElementById('container');
this.camera = new Three.PerspectiveCamera(50, 1.686275 /*container.clientWidth/container.clientHeight*/, 0.01, 1000);
this.camera.position.set(0, 5, 20);
this.camera.zoom = 1;
},
createShape: function(shape) {
let material = new Three.MeshStandardMaterial({
"color": '#0000ff'/*16777215*/,
"roughness": 1,
"metalness": 0.5,
"emissive": 0,
"depthFunc": 3,
"depthTest": true,
"depthWrite": true,
"stencilWrite": false,
"stencilWriteMask": 255,
"stencilFunc": 519,
"stencilRef": 0,
"stencilFuncMask": 255,
"stencilFail": 7680,
"stencilZFail": 7680,
"stencilZPass": 7680
})
switch (shape.nom) {
case "Box": {
this.geometry = new Three.BoxBufferGeometry(1.8,1.8,1.8)
break;
}
case "Sphere": {
this.geometry = new Three.SphereBufferGeometry(1,8,6,0,6.283185,0, 3.141593)
break;
}
case "Dodecahedron": {
this.geometry = new Three.DodecahedronBufferGeometry(1.2,0)
break;
}
case "Icosahedron": {
this.geometry = new Three.IcosahedronBufferGeometry(1.5,0)
break;
}
}
this.mesh = new Three.Mesh(this.geometry, material)
this.mesh.name = shape.nom
this.mesh.userData = shape.userData
this.mesh.receiveShadow = true
this.mesh.castShadow = true
this.mesh.position.set(0, shape.userData.position.y, 0)
this.scene.add(this.mesh)
},
addSpotlight: function(color) {
const light = new Three.SpotLight(color, 2, 1000)
light.position.set(0, 0, 30)
this.scene.add(light)
},
addAmbientLight: function() {
const light = new Three.AmbientLight('#fff', 0.5)
this.scene.add(light)
},
verifForme: function(e) {
let t = this
let elt = t.scene.getObjectByName(e);
t.intersects = t.raycaster.intersectObject(elt);
if (t.intersects.length !== 0) {
// if it's not in the array, we put it at the beginning
if (t.userData.souris.indexOf(e)<0) {
t.userData.souris.unshift(e);
console.log(t.userData.souris[0] + " survolé!");
}
if (t.userData.souris[0] == e) {
let obj = t.intersects[0].object;
obj.material.color.set('#'+elt.userData.couleurs[1]);
obj.scale.set(obj.scale.x<1.4?obj.scale.x+t.VITESSE_ZOOM:obj.scale.x,obj.scale.y<1.4?obj.scale.y+t.VITESSE_ZOOM:obj.scale.y,obj.scale.z<1.4?obj.scale.z+t.VITESSE_ZOOM:obj.scale.z);
obj.rotation.y += t.VITESSE_ROTATION/t.RALENTISSEMENT
t.replacer(obj,obj.userData.position.y+obj.userData.decalage)
}
else {
t.retrecir(e,elt);
}
}
else {
if (t.userData.souris.indexOf(e)>=0) {
t.userData.souris = t.userData.souris.filter(forme => forme != e);
}
t.retrecir(e,elt);
}
},
onClick: function ( event ) {
event.preventDefault();
if (this.userData.souris.length >0 ) { console.log(this.userData.souris[0] + " clicked!"); }
else {
console.log("click outside!")
}
},
onMouseMove: function(event){
let container = document.getElementById('menu3D');
this.mouse.x = ( event.clientX / container.clientWidth ) * 2 - 1;
this.mouse.y = - ( event.clientY / container.clientHeight ) * 2 + 1;
//console.log(JSON.stringify(this.mouse))
},
replacer: function(e,py) {
// next line to prevent shaking
if (Math.abs(e.position.y - py) < 0.05) { return true }
let rhesus = 10*this.VITESSE_ZOOM
if (this.userData.souris[0] != e.name) { rhesus *= 3 }
//console.log(e.name+': '+this.userData.souris[0]+' - '+rhesus)
if (e.position.y > py) { rhesus = -1 }
e.position.set(0,Math.trunc(10*e.position.y+rhesus)/10,0)
},
retrecir: function (n,e) {
// checking if the clicked element is on top
let dec = 0
let elt = this
if ((elt.userData.souris.length > 0) && (elt.userData.formes.map(x=>x.nom).indexOf(n)<elt.userData.formes.map(x=>x.nom).indexOf(elt.userData.souris[0]))) {
dec = Math.trunc(10*e.parent.getObjectByName(elt.userData.souris[0]).userData.decalage*2.1)/10;
}
e.material.color.set('#'+e.userData.couleurs[0]);
e.rotation.y += elt.VITESSE_ROTATION
e.scale.set(e.scale.x>1?e.scale.x-elt.VITESSE_ZOOM:e.scale.x,e.scale.y>1?e.scale.y-elt.VITESSE_ZOOM:e.scale.y,e.scale.z>1?e.scale.z-elt.VITESSE_ZOOM:e.scale.z);
let newY = e.userData.position.y+dec
if (e.position.y != newY) {
elt.replacer(e,newY)
}
},
animate: function() {
let elt = this
requestAnimationFrame(this.animate);
this.raycaster.setFromCamera(this.mouse, this.camera);
this.userData.formes.map(x=>x.nom).forEach(x=>elt.verifForme(x))
if (this.userData.souris.length >0 ) { document.body.style.cursor = "pointer"; }
else { document.body.style.cursor = "default"; }
this.camera.updateProjectionMatrix();
this.renderer.render(this.scene, this.camera);
}
},
data: () => ({
scene: new Three.Scene(),
camera: null,
renderer: Three.WebGLRenderer,
mesh: new Three.Mesh,
factor:0,
mouse : new Three.Vector2(1, 1),
raycaster : new Three.Raycaster(),
intersects : [],
VITESSE_ROTATION: 0.05,
VITESSE_ZOOM: 0.1,
RALENTISSEMENT: 3,
userData: {
"souris": [],
"formes": [
{
"nom": "Box",
"userData": {
"position": {
"x": 0,
"y": 7.8,
"z": 0
},
"couleurs": [
"aaaaaa",
"095256"
],
"decalage": 0.5
}
},
{
"nom": "Icosahedron",
"userData": {
"position": {
"x": 0,
"y": 5.5,
"z": 0
},
"couleurs": [
"aaaaaa",
"087F8C"
],
"decalage": 0.5
}
},
{
"nom": "Dodecahedron",
"userData": {
"position": {
"x": 0,
"y": 3.1,
"z": 0
},
"couleurs": [
"aaaaaa",
"5AAA95"
],
"decalage": 0.4
}
},
{
"nom": "Sphere",
"userData": {
"position": {
"x": 0,
"y": 1,
"z": 0
},
"couleurs": [
"aaaaaa",
"86A873"
],
"decalage": 0.2
}
}
]
}
}),
}
</script>
I investigated the problem using the code you provided and fixed it locally so I'm hoping it also works for you. The issues are the following:
A tiny hard to find typo in the attachment of the resize event: instead of window.addEventListener('resize', this.onResize()) you need to use window.addEventListener('resize', this.onResize); removing the () because you don't want to call the function at the time of the attachment, you want it called each time the event is triggered.
Due to the first issue, as the resize function wasn't getting called when you expected, I guess this is what led you to use a hard-coded value (1.686275) in the camera instantiation instead of the recommended formula container.clientWidth / container.clientHeight so you need to change that back to
createCamera: function () {
let container = document.getElementById('menu3D');
this.camera = new Three.PerspectiveCamera(50, container.clientWidth / container.clientHeight, 0.01, 1000);
...
Also as the 3D container div is not in the root level of the HTML body due to VueJS requirements, in onMouseMove() you need to consume the offset coordinates instead of the client ones as follows:
onMouseMove: function (event) {
let container = document.getElementById('menu3D');
this.mouse.x = (event.offsetX / container.clientWidth) * 2 - 1;
this.mouse.y = - (event.offsetY / container.clientHeight) * 2 + 1;
...
I'm afraid the problem originates from the creation of your camera:
this.camera = new Three.PerspectiveCamera(50, 1.686275 /*container.clientWidth/container.clientHeight*/, 0.01, 1000);
Why are you using the magical aspect number 1.686275 instead of the actual width/height ratio like you do on resize? This is giving you a different behavior before and after resizing.
This is my best guess at first glance, although I presume there are other instances of hard coded “magic numbers” in your app that need to be re-calculated based on the screen’s width and height. I couldn't possibly read through the 300 lines of code you posted. You should consider isolating the problem to create a minimal working example and add it to your question via a code snippet so we can see your code in action.

ScrollMagic - set a variable scene

I have a page with multiple similar sections with the class ".imgPanel" that I would like to apply the same animation to each section every time the page scrolls into view.
I don't want to create a new scene for each animation as they are all the same. I know there is a way to create a variable scene (i hope that is the correct terminology), and I am hoping someone can help.
Does anyone know how I would adjust the code below to make that happen. I am sure if I get one example of it with my code below I will be able to understand and use this technique again.
Thanks in advance. Adam.
function scrollMagic() {
if (!Modernizr.touch) {
var controller = new ScrollMagic.Controller({
duration: "200%",
});
var panelAnim = new TimelineMax();
panelAnim
.from($('.imgPanel'), 1.4, {
y: '-50px',
autoAlpha: 0,
ease: Power1.easeOut
}, '-=0.2')
var introScene = new ScrollMagic.Scene({
duration: "100%",
})
.setTween(panelAnim)
.addTo(controller);
}
}
I figured it out after watching Ihatetomatoes video
function scrollMagic() {
if (!Modernizr.touch) {
var controller = new ScrollMagic.Controller({
duration: "100%",
});
$('.imgPanel').each(function(){
var tween = TweenMax.from($(this), 1.4, { y: '-50px', autoAlpha: 0,
ease: Power1.easeOut }, '-=0.2');
var scene = new ScrollMagic.Scene ({
duration: "100%",
offset: -300, // offset trigger position by 100px
triggerElement: this
})
.setTween(tween)
.addTo(controller);
});
}
}

Vue removeListener within directive on destroy

I got an directive that i use for animating elements within the DOM, the directive looks like this:
Vue.directive('animate', {
bind(el, binding) {
let start = (binding.value)?binding.value.start:{y: 50, autoAlpha: 0};
TweenMax.set(el, start);
},
inserted: function(el, binding) {
let end = (binding.value)?binding.value.end:{y: 0, autoAlpha: 1};
end.ease = Quart.easeOut;
end.onComplete = () => {
el.removeAttribute("style");
el.classList.add("animation-done");
};
const f = function() {
console.log("scrolling");
if (InViewPort(el)) {
window.removeEventListener('scroll', f);
TweenMax.to(el, 1.5, end);
}
};
window.addEventListener('scroll', f);
f();
}
});
The directive is called within the component like this:
v-animate="{start: {y: 50, autoAlpha: 0}, end: {y: 0, autoAlpha: 1, delay: index * 0.5}}"
This gives me the flexibility i would like to have, only one problem. The event listener is within the inserted scope, and not available on the unbind hook.
Maybe directive isn't the way to go in Vue, does anybody have some advice how to handle this?