IntersectionObserver and transform transition - javascript

my first post on Stackoverflow
I'm trying to build a simple scroll effect with intersectionObserver. The idea is when an item is out of the viewport, it is scale to 0, and scales to 1 when is fully visible. I really don't know if it is the good way to build this type of effect... but seems to work correctly, except:
I have some issues with "jumping" effects on the element when scrolling. I guess requestAnimationsFrame should solve it, but I'm unable to implement it and I would need some help to anderstand how it works.
A pen is visible here
Note: beginner JS,
Thank you.
const sectionScaleOption = {
root: null,
rootMargin: '0px',
threshold: [
0.0,
0.05,
0.1,
0.15,
0.2,
0.25,
0.3,
0.35,
0.4,
0.45,
0.5,
0.55,
0.6,
0.65,
0.7,
0.75,
0.8,
0.85,
0.9,
0.95,
1.0,
],
};
observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
const ratio = entry.intersectionRatio;
const section = entry.target;
if (ratio > 0) {
section.classList.add('forTest');
console.log(ratio);
section.style.transform = `scale(${ratio})`;
} else {
section.classList.remove('forTest');
console.log('out');
}
});
}, sectionScaleOption);
const sectionToScale = document.querySelector('.section');
observer.observe(sectionToScale);

For a better result a finally used:
https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
function buildThresholdList() {
let thresholds = [];
let numSteps = 100;
for (let i=1.0; i<=numSteps; i++) {
let ratio = i/numSteps;
thresholds.push(ratio);
}
thresholds.push(0);
return thresholds;
}
const obsOptnScrollOpct = {
root: null,
rootMargin: '0px',
threshold: buildThresholdList()
};
It is not perfect, and I would be very happy for any other suggestions/advices.

Related

Matter.js — How to prevent mouseConstraint to capture scroll events?

I am trying to enable scrolling into a page that contains a Matter.js canvas. I looked everywhere and the recommendation is to add removeEventListener so the mouse constraint won't hijack the scrolling event. Unfortunately, I am having no success. How can I enable a scroll?
Thank you in advance!
CodeSandbox
Code
import "./styles.css";
import Matter from "matter-js";
//Fetch our canvas
var canvas = document.getElementById("world");
//Setup Matter JS
var engine = Matter.Engine.create();
var world = engine.world;
var render = Matter.Render.create({
canvas: canvas,
engine: engine,
options: {
width: 500,
height: 500,
background: "transparent",
wireframes: false,
showAngleIndicator: false
}
});
//Add a ball
var ball = Matter.Bodies.circle(250, 250, 50, {
density: 0.04,
friction: 0.01,
frictionAir: 0.00001,
restitution: 0.8,
render: {
fillStyle: "#F35e66",
strokeStyle: "black",
lineWidth: 1
}
});
Matter.World.add(world, ball);
//Add a floor
var floor = Matter.Bodies.rectangle(250, 520, 500, 40, {
isStatic: true, //An immovable object
render: {
visible: false
}
});
Matter.World.add(world, floor);
//Make interactive
var mouseConstraint = Matter.MouseConstraint.create(engine, {
//Create Constraint
element: canvas,
constraint: {
render: {
visible: false
},
stiffness: 0.8
}
});
Matter.World.add(world, mouseConstraint);
// Why is this not working?
mouseConstraint.mouse.element.removeEventListener(
"mousewheel",
mouseConstraint.mouse.mousewheel
);
mouseConstraint.mouse.element.removeEventListener(
"DOMMouseScroll",
mouseConstraint.mouse.mousewheel
);
//Start the engine
Matter.Engine.run(engine);
Matter.Render.run(render);
It's a HTML issue. in index.html set the overflow to scroll

Matter.js — How to get the dimension of an image to set Bodies sizes?

I am trying to programmatically set the width and heights of the chained bodies in matter.js. Unfortunately, I am only getting 0 as values and I am unsure why. My guess is that the images are not being loaded fast enough to provide those values. How can I load those dimensions before the images are loaded?
Pseudo-code
Several bodies from Array
Get the width and height of each image in the Array
Use this value to set the Bodies dimensions
Code
var playA = Composites.stack(
percentX(25) - assetSize / 2,
percentY(25),
1,
6,
5,
5,
function (x, y) {
iA++;
var imgWidth;
var imgHeight;
var img = new Image();
img.src = String(design[iA]);
var imgWidth = 0;
var imgHeight = 0;
img.onload = function a() {
imgWidth = img.naturalWidth;
imgHeight = img.naturalHeight;
console.log(String(design[iA]), imgWidth, imgHeight);
};
console.log(String(design[iA]), imgHeight, imgWidth); // I can't access the values here.
return Bodies.rectangle(x, y, imgWidth, imgHeight, {
// collisionFilter: { group: group },
friction: 1,
render: {
sprite: {
texture: design[iA],
xScale: (assetSize / 100) * 0.46,
yScale: (assetSize / 100) * 0.46
}
}
});
}
);
Composites.chain(playA, 0.3, 0, -0.5, 0, {
stiffness: 1,
length: 10,
render: { type: "line", visible: false }
});
If you know the dimensions and can populate an array beforehand, the solution is potentially straightforward since Matter.js loads images given a URL string, with the caveat that the engine doesn't wait for the loads before running.
Here's a minimal example of iterating over width/height pairs in an array and passing these properties into the rectangle calls which I'll use as a stepping stone to the example that matches your use case.
const engine = Matter.Engine.create();
const render = Matter.Render.create({
element: document.body,
engine: engine,
options: {
width: 450,
height: 250,
wireframes: false, // required for images
}
});
Matter.Render.run(render);
const runner = Matter.Runner.create();
Matter.Runner.run(runner, engine);
const imgSizes = [[56, 48], [45, 50], [35, 50], [60, 63]];
const stack = Matter.Composites.stack(
// xx, yy, columns, rows, columnGap, rowGap, cb
150, 50, 4, 1, 0, 0,
(x, y, i) => {
const [w, h] = imgSizes[i];
return Matter.Bodies.rectangle(x, y, w, h, {
render: {
sprite: {
texture: `http://placekitten.com/${w}/${h}`
}
}
});
}
);
Matter.Composites.chain(stack, 0.5, 0, -0.5, 0, {
stiffness: 0.75,
length: 10,
render: {type: "line", visible: true}
});
Matter.Composite.add(engine.world, [
stack,
Matter.Bodies.rectangle(225, 0, 450, 25, {
isStatic: true
}),
Matter.Bodies.rectangle(450, 150, 25, 300, {
isStatic: true
}),
Matter.Bodies.rectangle(0, 150, 25, 300, {
isStatic: true
}),
Matter.Bodies.rectangle(225, 250, 450, 25, {
isStatic: true
})
]);
const mouse = Matter.Mouse.create(render.canvas);
const mouseConstraint = Matter.MouseConstraint.create(engine, {
mouse: mouse,
constraint: {
stiffness: 0.2,
render: {visible: true}
}
});
Matter.Composite.add(engine.world, mouseConstraint);
render.mouse = mouse;
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.18.0/matter.min.js"></script>
Now, if you need to load images using onload and use their dimensions, you'll need to use promises or put all code dependent on these images into the sequence of onload callback(s) as described in the canonical How do I return the response from an asynchronous call?.
The failing pattern is:
const getSomethingAsync = cb => setTimeout(() => cb("something"), 0);
let data = null;
getSomethingAsync(result => {
data = result;
console.log("this runs last");
});
console.log(data); // guaranteed to be null, not "something"
// more logic that is supposed to depend on data
The fix is:
const getSomethingAsync = cb => setTimeout(() => cb("something"), 0);
getSomethingAsync(data => {
console.log(data);
// logic that depends on the data from `getSomethingAsync`
});
console.log("this will run first");
// logic that doesn't depend on data from `getSomethingAsync`
Since you're juggling multiple onloads, you can promisify the onloads to make them easier to work with. I have a couple examples of doing this here and here agnostic of matter.js.
Here's an example of using promises to load images applied to your general problem. Again, I'll use my own code so that it's runnable and reproducible, but the pattern should be easy to extrapolate to your project.
The idea is to first load the images using a series of promises which are resolved when onload handlers fire, then use Promise.all to chain a then which runs the MJS initializer callback only when all images are loaded. The widths and heights are then accessible to your matter.js code within the callback.
As a side benefit, this ensures images are loaded by the time MJS runs.
const initializeMJS = images => {
const engine = Matter.Engine.create();
const render = Matter.Render.create({
element: document.body,
engine: engine,
options: {
width: 450,
height: 250,
wireframes: false, // required for images
}
});
Matter.Render.run(render);
const runner = Matter.Runner.create();
Matter.Runner.run(runner, engine);
const stack = Matter.Composites.stack(
// xx, yy, columns, rows, columnGap, rowGap, cb
150, 50, 4, 1, 0, 0,
(x, y, i) => {
const {width: w, height: h} = images[i];
return Matter.Bodies.rectangle(x, y, w, h, {
render: {
sprite: {
texture: images[i].src
}
}
});
}
);
Matter.Composites.chain(stack, 0.5, 0, -0.5, 0, {
stiffness: 0.75,
length: 10,
render: {type: "line", visible: true}
});
Matter.Composite.add(engine.world, [
stack,
Matter.Bodies.rectangle(225, 0, 450, 25, {
isStatic: true
}),
Matter.Bodies.rectangle(450, 150, 25, 300, {
isStatic: true
}),
Matter.Bodies.rectangle(0, 150, 25, 300, {
isStatic: true
}),
Matter.Bodies.rectangle(225, 250, 450, 25, {
isStatic: true
})
]);
const mouse = Matter.Mouse.create(render.canvas);
const mouseConstraint = Matter.MouseConstraint.create(engine, {
mouse: mouse,
constraint: {
stiffness: 0.2,
render: {visible: true}
}
});
Matter.Composite.add(engine.world, mouseConstraint);
render.mouse = mouse;
};
const imageSizes = [[56, 48], [45, 50], [35, 50], [60, 63]];
const imageURLs = imageSizes.map(([w, h]) =>
`http://placekitten.com/${w}/${h}`
);
Promise.all(imageURLs.map(e =>
new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = e;
})
))
.then(initializeMJS)
;
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.18.0/matter.min.js"></script>

how to gsap about continuity play

I wanna work play method for continuity.
but I can't work only once.
If I use restart method, I can continuous action.
However It's not cool.
I took the gif animation.
Please click the link below.
https://gyazo.com/06622369ad530b9dc9a4cb5ee90b71dc
let flag = true;
document.body.addEventListener("click", function () {
if (flag) {
startGrap.play();
flag = false;
} else {
endGrap.play();
flag = true;
}
});
const startGrap = gsap
.timeline()
.addLabel("start")
.to(property, 0.5, {
direction: -8.0,
ease: "power2.out",
})
.to(property, 0.7, {
direction: 0.0,
ease: "back.out(3.0)",
})
.to(
property,
0.8,
{
offsetZ: 0.0,
ease: "power3.inOut",
},
"start"
)
.pause();
const endGrap = gsap
.timeline()
.addLabel("start")
.to(property, 0.4, {
direction: -8.0,
ease: "power2.out",
})
.to(property, 0.4, {
direction: 0.0,
ease: "back.out(4)",
})
.to(
property,
0.8,
{
offsetZ: -500.0,
ease: "power3.inOut",
},
"start"
)
.pause();
maybe I resolved how to way.
I make instance of timelinelite every time.
but I don't think this way is better.
https://gyazo.com/f9feaa276fa5e8a4b047d60adc39d610
document.body.addEventListener("click", function () {
if (flag) {
new TimelineLite()
.addLabel("start")
.to(property, 0.5, {
direction: -8.0,
ease: "power2.out",
})
.to(property, 0.7, {
direction: 0.0,
ease: "back.out(3.0)",
})
.to(
property,
0.8,
{
offsetZ: 0.0,
ease: "power3.inOut",
},
"start"
);
} else {
new TimelineLite()
.addLabel("start")
.to(property, 0.4, {
direction: -8.0,
ease: "power2.out",
})
.to(property, 0.4, {
direction: 0.0,
ease: "back.out(4)",
})
.to(
property,
0.8,
{
offsetZ: -500.0,
ease: "power3.inOut",
},
"start"
);
}
flag = !flag;
});

deck.gl + tripslayer without react.js

How can I enable animation of TripsLayer without react? TripsLayer example uses React and I really don't know, how to convert it to pure js. Please look at "animate" function in the code below. I tried to update state of layer but it doesn't work (there is no animation of TripsLayer). I don't know, where should I assign "time" variable.
Demo of TripsLayer: https://deck.gl/#/examples/custom-layers/trip-routes
React version my code: https://github.com/uber/deck.gl/blob/master/examples/website/trips/app.js
Docs for TripsLayer: https://github.com/uber/deck.gl/tree/master/modules/experimental-layers/src/trips-layer
My code:
import {MapboxLayer} from '#deck.gl/mapbox';
import {TripsLayer} from '#deck.gl/experimental-layers';
import mapboxgl from 'mapbox-gl';
class App
{
constructor()
{
this.stateInfo = { time: 600 };
}
get state()
{
return this.stateInfo;
}
animate() {
var loopLength = 18000; // unit corresponds to the timestamp in source data
var animationSpeed = 30; // unit time per second
const timestamp = Date.now() / 1000;
const loopTime = loopLength / animationSpeed;
var time = Math.round(((timestamp % loopTime) / loopTime) * loopLength);
// HOW SHOULD I USE time VARIABLE???
window.requestAnimationFrame(this.animate.bind(this));
}
}
var obido = new App();
var tripsLayerObido = new TripsLayer({
id: 'trips',
data: 'trips/tripsKR.json',
getPath: d => d.segments,
getColor: d => (d.vendor === 0 ? [253, 128, 93] : [23, 184, 190]),
opacity: 0.6,
strokeWidth: 30,
trailLength: 180,
currentTime: obido.state.time
});
const LIGHT_SETTINGS = {
lightsPosition: [-74.05, 40.7, 8000, -73.5, 41, 5000],
ambientRatio: 0.05,
diffuseRatio: 0.6,
specularRatio: 0.8,
lightsStrength: [2.0, 0.0, 0.0, 0.0],
numberOfLights: 2
};
export const INITIAL_VIEW_STATE = {
longitude: 19.93,
latitude: 50.03,
zoom: 12.8,
maxZoom: 19,
pitch: 60,
bearing: 0
};
mapboxgl.accessToken = "XXX";
const map = new mapboxgl.Map({
container: 'app',
style: 'mapbox://styles/elninopl/cjnlge6rl094w2so70l1cf8y5',
center: [INITIAL_VIEW_STATE.longitude, INITIAL_VIEW_STATE.latitude],
zoom: INITIAL_VIEW_STATE.zoom,
pitch: INITIAL_VIEW_STATE.pitch,
layers: [tripsLayerObido]
});
map.on('load', () => {
obido.animate(0);
});
Please try setProps, which makes my trips layer updates:
this.tripsLayer.setProps({ currentTime: this.currentTime });
yes,it works.
function animate(timestamp) {
var timestamp = Date.now() / 1000;
var loopTime = loopLength / animationSpeed;
curtime= ((timestamp % loopTime) / loopTime) * loopLength;
requestAnimationFrame(animate);
tripLayer.setProps({ currentTime: curtime });
}
I'm a bit late here, but here there is an example of using deck.gl TripsLayer with the Scripting API (without React).
<script src="https://unpkg.com/deck.gl#^8.4.0/dist.min.js"></script>
<script src="https://unpkg.com/#deck.gl/carto#^8.4.0/dist.min.js"></script>
<script src="https://libs.cartocdn.com/mapbox-gl/v1.13.0/mapbox-gl.js"></script>
<link href="https://libs.cartocdn.com/mapbox-gl/v1.13.0/mapbox-gl.css" rel="stylesheet" />
<div id="map" style="width: 100vw; height: 100vh"></div>
const ambientLight = new deck.AmbientLight({
color: [255, 255, 255],
intensity: 1.0
});
const pointLight = new deck.PointLight({
color: [255, 255, 255],
intensity: 2.0,
position: [-74.05, 40.7, 8000]
});
const lightingEffect = new deck.LightingEffect({ ambientLight, pointLight });
const material = {
ambient: 0.1,
diffuse: 0.6,
shininess: 32,
specularColor: [60, 64, 70]
};
const theme = {
buildingColor: [74, 80, 87],
trailColor0: [253, 128, 93],
trailColor1: [23, 184, 190],
material,
effects: [lightingEffect]
};
const LOOP_LENGTH = 1800;
const ANIMATION_SPEED = 0.4;
async function initialize() {
deck.carto.setDefaultCredentials({
username: 'public',
apiKey: 'default_public',
});
// Fetch Data from CARTO
// Notice that you can use any Deck.gl layer with CARTO datasets getting GeoJSON data from CARTO's API. This method is recommended for complex layers with datasets below 50Mb
const tripsUrl = 'https://public.carto.com/api/v2/sql?q=SELECT the_geom, vendor, timestamps FROM sf_trips_v7&format=geojson';
const geojsonTripsData = await fetch(tripsUrl).then(response => response.json());
// TripsLayer needs data in the following format
const layerData = geojsonTripsData.features.map(f => ({
vendor: f.properties.vendor,
timestamps: f.properties.timestamps,
path: f.geometry.coordinates[0]
}));
const staticLayers = [
// This is only needed when using shadow effects
new deck.PolygonLayer({
id: 'ground-layer',
data: [[[-74.0, 40.7], [-74.02, 40.7], [-74.02, 40.72], [-74.0, 40.72]]],
getPolygon: f => f,
stroked: false,
getFillColor: [0, 0, 0, 0]
}),
new deck.carto.CartoSQLLayer({
id: 'buildings-layer',
data: 'SELECT the_geom_webmercator, height FROM sf_buildings',
extruded: true,
wireframe: true,
opacity: 0.5,
getElevation: f => f.properties.height,
getFillColor: [74, 80, 87],
material: theme.material
})
];
// Create Deck.GL map
const deckgl = new deck.DeckGL({
container: 'map',
mapStyle: 'https://basemaps.cartocdn.com/gl/dark-matter-nolabels-gl-style/style.json',
initialViewState: {
longitude: -74,
latitude: 40.73,
zoom: 12,
pitch: 45,
bearing: 0
},
controller: true,
layers: staticLayers,
effects: theme.effects
});
let time = 0;
function animate() {
time = (time + ANIMATION_SPEED) % LOOP_LENGTH;
window.requestAnimationFrame(animate);
}
setInterval(() => {
deckgl.setProps({
layers: [
...staticLayers,
new deck.TripsLayer({
id: 'trips-layer',
data: layerData,
getPath: d => d.path,
getTimestamps: d => d.timestamps,
getColor: d => (d.vendor === 0 ? theme.trailColor0 : theme.trailColor1),
opacity: 0.3,
widthMinPixels: 2,
rounded: true,
trailLength: 180,
currentTime: time,
shadowEnabled: false
})
]
});
}, 50);
window.requestAnimationFrame(animate);
}
initialize();

How can I better manage using GSAP animations in Nuxt.js?

I am building a portfolio page using nuxt.js and Green Sock(GSAP) animation for on-load, menu, and page transitions. It is more difficult to accommodate GSAP than using CSS based transitions but I feel the results are worth it.
I am not using webpack and nuxt correctly to make everything more modular and concise. I need some help to better export variables and functions, use Vue.js reactive properties better, and take advantage of what's already available instead of working against it.
My default.vue file has a method that manages the menu:
showMenu() {
if (process.browser) {
var tl = new TimelineMax();
var slide = document.querySelector("#slide-menu");
var pl = document.querySelector("body");
var nl = document.querySelectorAll(".nav-link");
if (this.$store.state.menuIsActive === false) {
tl
.to(slide, 0.5, {
y: "100%",
ease: this.$store.state.animParams.ease
})
.staggerTo(
nl,
this.$store.state.animParams.dur3,
{
y: "-10px",
autoAlpha: 1,
ease: this.$store.state.animParams.ease
},
this.$store.state.animParams.dur2
);
} else if (this.$store.state.menuIsActive === true) {
console.log("true");
tl
.staggerTo(
nl,
this.$store.state.animParams.dur3,
{
y: "10px",
autoAlpha: 0,
ease: this.$store.state.animParams.ease
},
this.$store.state.animParams.dur2
)
.to(slide, 0.5, {
y: "-100%",
ease: this.$store.state.animParams.ease
});
}
this.$store.commit("toggleMenuState");
}
}
I have a menu.js middleware that closes the menu when open. I would prefer to use the same variable but redeclare it instead:
import TweenMax from 'gsap'
export default function ({ store }) {
if (store.state.menuIsActive === !false) {
if (process.browser) {
var tl = new TimelineMax()
var slide = document.querySelector('#slide-menu')
var nl = document.querySelectorAll('.nav-link')
if (store.state.menuIsActive === true) {
console.log('initmenu')
tl
.staggerTo(
nl,
store.state.animParams.dur3,
{ y: '10px', autoAlpha: 0, ease: store.state.animParams.ease },
store.state.animParams.dur2
)
.to(slide, 0.5, { y: '-100%', ease: store.state.animParams.ease })
store.commit('setMenuState', false)
}
}
}
}
I duplicated the page transition because I couldn't get it working correctly in my nuxt.config.js. I would like to keep all of my transitions there or in a separate plugins file maybe but am not sure the best way to do so.
From my root index.vue page file:
import TweenMax from "gsap";
export default {
transition: {
name: "page",
mode: "out-in",
css: false,
enter: function(el, done) {
console.log("animenter contact");
if (process.browser) {
var tl = new TimelineMax();
var tg = document.querySelectorAll(".el");
var st = document.querySelector(".overlay-grid img");
var box = document.querySelectorAll(".box");
tl.fromTo(st, 0.3, { x: -100 }, { x: 0, autoAlpha: 1 });
tl.staggerTo(tg, 0.3, { autoAlpha: 1 }, 0.1);
tl.staggerTo(box, 0.3, { autoAlpha: 1, onComplete: done }, 0.1);
}
},
leave: function(el, done) {
console.log("leaving contact...");
if (process.browser) {
var tl = new TimelineMax();
var tg = document.querySelectorAll(".el");
var st = document.querySelector(".overlay-grid img");
var pl = document.querySelector("body");
var box = document.querySelectorAll(".box");
tl.fromTo(st, 0.3, { x: 0 }, { x: -100, autoAlpha: 0 });
tl.staggerTo(tg, 0.3, { autoAlpha: 0 }, 0.1);
tl.staggerTo(box, 0.3, { autoAlpha: 0, onComplete: done }, 0.1);
}
}
}
And finally, in nuxt.config.js:
router: {
middleware: 'menu'
},
build: {
vendor: [
'gsap',
....
] /*
}
I also have some variables set in the store to manage state, and some transition durations so I don't have to edit them in as many places to see how something looks. It is becoming a lot of work to preview ideas or small changes and I wasn't able to find an in-depth article that tackles this problem.
Any help with tips and tricks for Nuxt, Vue, GSAP, Webpack, and Javascript in general would be greatly appreciated.

Categories

Resources