Angular animation on scroll - css

I build an webpage with angular, each module is an component it has an animation in it but it run's only when the page opens but i need to perform the animation while the component is visibile on the screen. i just tried below like hide and show the component by checking the scrollY of the page. is there any better way to do it?
#HostListener('window:scroll', ['$event']) onWindowScroll(e: any) {
if (window.pageYOffset < 180) {
this.heroShown = 0;
} else {
this.heroShown = 1;
}
console.log(e.target['scrollingElement'].scrollTop);
console.log(document.body.scrollTop);
console.log(window.pageYOffset);
}
`

for that you can use a Intersection Observer.
The observer fires an event when the element is visible.
So when the event fires you can start your animation.
private createObserver() {
const options = {
rootMargin: '0px',
threshold: this.threshold,
};
const isIntersecting = (entry: IntersectionObserverEntry) =>
entry.isIntersecting || entry.intersectionRatio > 0;
this.observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (isIntersecting(entry)) {
this.subject$.next({ entry, observer });
}
});
}, options);
}
a other way to archive this is using a framwork like gsap
There you can use something like a scrolltrigger.
Check the docs here.

Related

Toggling multiple tools on Paper.js

Can someone show me a working example (JSFiddle or otherwise) of how to have two tools on Paper.js that a user can click on to draw different shapes, say one for circles and one for squares?
Thanks!
You've got at least a couple of options here,
1. Activate Tool from paper.tools
Paper.js allows you to activate a Tool by calling tool.activate(), which causes only that particular Tool to receive Tool events, such as mousedown, mousedrag etc ...
When you create a new Tool via new paper.Tool(), that Tool is added in paper.tools so you can lookup for the Tool within that Array and call tool.activate() on it.
An example:
window.onload = () => {
// Setup Paper
paper.setup(document.querySelector('canvas'))
// Find a Tool in `paper.tools` and activate it
const activateTool = name => {
const tool = paper.tools.find(tool => tool.name === name)
tool.activate()
}
// Tool Path, draws paths on mouse-drag.
// Note: Wrap each Tool in an IIFE to avoid polluting the
// global scope with variables related with that Tool.
;(() => {
const tool = new paper.Tool()
tool.name = 'toolPath'
let path
tool.onMouseDown = function(event) {
path = new paper.Path()
path.strokeColor = '#424242'
path.strokeWidth = 4
path.add(event.point)
}
tool.onMouseDrag = function(event) {
path.add(event.point)
}
})()
// Tool Circle, draws a 30px circle on mousedown
;(() => {
const tool = new paper.Tool()
tool.name = 'toolCircle'
let path
tool.onMouseDown = function(event) {
path = new paper.Path.Circle({
center: event.point,
radius: 30,
fillColor: '#9C27B0'
})
}
})()
// Attach click handlers for Tool activation on all
// DOM buttons with class '.tool-button'
document.querySelectorAll('.tool-button').forEach(toolBtn => {
toolBtn.addEventListener('click', e => {
activateTool(e.target.getAttribute('data-tool-name'))
})
})
}
html,
body,
canvas {
width: 100%;
height: 100%;
margin: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/paper.js/0.11.5/paper-core.js"></script>
<button
class="tool-button"
data-tool-name="toolPath">
Draw Paths
</button>
<button
class="tool-button"
data-tool-name="toolCircle">
Stamp Circles
</button>
<canvas resize></canvas>
2. Create a ToolStack Class
However, I find it far more practical to create a ToolStack Class since it allows me to add additional methods later on, i.e isToolActive(), onToolSelect() (for adding is-active classes to DOM tool buttons) etc..
The ToolStack should then implement various methods for handling your Tools, the first and foremost being an activateTool method, that will lookup for a Tool by name and call it's tool.activate() method.
An example:
window.onload = () => {
// Setup Paper
paper.setup(document.querySelector('canvas'))
// Toolstack
class ToolStack {
constructor(tools) {
this.tools = tools.map(tool => tool())
}
activateTool(name) {
const tool = this.tools.find(tool => tool.name === name)
tool.activate()
}
// add more methods here as you see fit ...
}
// Tool Path, draws paths on mouse-drag
const toolPath = () => {
const tool = new paper.Tool()
tool.name = 'toolPath'
let path
tool.onMouseDown = function(event) {
path = new paper.Path()
path.strokeColor = '#424242'
path.strokeWidth = 4
path.add(event.point)
}
tool.onMouseDrag = function(event) {
path.add(event.point)
}
return tool
}
// Tool Circle, draws a 30px circle on mousedown
const toolCircle = () => {
const tool = new paper.Tool()
tool.name = 'toolCircle'
let path
tool.onMouseDown = function(event) {
path = new paper.Path.Circle({
center: event.point,
radius: 30,
fillColor: '#9C27B0'
})
}
return tool
}
// Construct a Toolstack, passing your Tools
const toolStack = new ToolStack([toolPath, toolCircle])
// Activate a certain Tool
toolStack.activateTool('toolPath')
// Attach click handlers for Tool activation on all
// DOM buttons with class '.tool-button'
document.querySelectorAll('.tool-button').forEach(toolBtn => {
toolBtn.addEventListener('click', e => {
toolStack.activateTool(e.target.getAttribute('data-tool-name'))
})
})
}
html,
body,
canvas {
width: 100%;
height: 100%;
margin: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/paper.js/0.11.5/paper-core.js"></script>
<button
class="tool-button"
data-tool-name="toolPath">
Draw Paths
</button>
<button
class="tool-button"
data-tool-name="toolCircle">
Stamp Circles
</button>
<canvas resize></canvas>

How to persist scrolls with turbolinks?

Sometimes it is desirable to persist scroll positions between page visits.
Turbolinks resets scroll position after loading the data.
How can I disable it for particular elements?
My solution in ES6:
const turbolinksPersistScroll = () => {
const persistScrollDataAttribute = 'turbolinks-persist-scroll'
let scrollPosition = null
let enabled = false
document.addEventListener('turbolinks:before-visit', (event) => {
if (enabled)
scrollPosition = window.scrollY
else
scrollPosition = null
enabled = false
})
document.addEventListener('turbolinks:load', (event) => {
const elements = document.querySelectorAll(`[data-${persistScrollDataAttribute}="true"]`)
for (let i = 0; i < elements.length; i++) {
elements[i].addEventListener('click', () => {
enabled = true
})
}
if (scrollPosition)
window.scrollTo(0, scrollPosition)
})
}
turbolinksPersistScroll()
And add on your links data-turbolinks-persist-scroll=true on links you want persist scrollbar position.
<a href="..." data-turbolinks-persist-scroll=true>Link</a>
This works for me, also with link_to remote: true.
Use the following javascript to persist scrolls. I have created a selector that matches all elements with class turbolinks-disable-scroll. Before loading,the script saves the scroll position and after loading it loads the saved positions.
// persist scrolls
// pirated from https://github.com/turbolinks/turbolinks-classic/issues/205
var elementsWithPersistentScrolls, persistentScrollsPositions;
elementsWithPersistentScrolls = ['.turbolinks-disable-scroll'];
persistentScrollsPositions = {};
$(document).on('turbolinks:before-visit', function() {
var i, len, results, selector;
persistentScrollsPositions = {};
results = [];
for (i = 0, len = elementsWithPersistentScrolls.length; i < len; i++) {
selector = elementsWithPersistentScrolls[i];
results.push(persistentScrollsPositions[selector] = $(selector).scrollTop());
}
return results;
});
$(document).on('turbolinks:load', function() {
var results, scrollTop, selector;
results = [];
for (selector in persistentScrollsPositions) {
scrollTop = persistentScrollsPositions[selector];
results.push($(selector).scrollTop(scrollTop));
}
return results;
});
It seems like there are two approaches to this problem.
Preserve flagged elements (#vedant1811's answer)
Preserve body scroll for flagged links
The second approach is the one that I've been looking for and couldn't find anywhere, so I'll provide my answer to that here.
The solution here is very similar to that of the first approach, but perhaps a little simpler. The idea is to grab the current scroll position of the body when an element is clicked, and then scroll to that position after the page is loaded:
Javascript
Turbolinks.scroll = {}
$(document).on('click', '[data-turbolinks-scroll=false]', function(e){
Turbolinks.scroll['top'] = $('body').scrollTop();
})
$(document).on('page:load', function() {
if (Turbolinks.scroll['top']) {
$('body').scrollTop(Turbolinks.scroll['top']);
}
Turbolinks.scroll = {};
});
Markup
<a href='/' data-turbolinks-scroll='false'>Scroll preserving link</a>
I use a scroll attribute on the Turbolinks object to store my scroll position when a [data-turbolinks-scroll=false] link is clicked, then after I scroll the page I clear this attribute.
It is important that you clear the attribute (Turbolinks.scroll = {}) otherwise, subsequent clicks on non-flagged anchor links will continue to scroll you to the same position.
Note: depending on the specific styling of html and body you may need to use the scroll offset from both. An example of how this might be accomplished is:
Turbolinks.scroll = {};
$(document).on('click', '[data-turbolinks-scroll=false]', function (e) {
Turbolinks.scroll['top'] = {
html: $("html").scrollTop(),
body: $("body").scrollTop()
}
});
$(document).on('turbolinks:load', function() {
if (Turbolinks.scroll['top']) {
$('html').scrollTop(Turbolinks.scroll['top']['html']);
$('body').scrollTop(Turbolinks.scroll['top']['body']);
}
Turbolinks.scroll = {};
});
I noticed that sometimes scroll is going up and then only down. This version prevents such behaviour:
const persistScrollDataAttribute = 'turbolinks-persist-scroll';
let scrollPosition = null;
const turbolinksPersistScroll = () => {
if (scrollPosition) {
window.scrollTo(0, scrollPosition);
scrollPosition = null;
}
const elements = document.querySelectorAll(`[data-${persistScrollDataAttribute}="true"]`)
for (let i = 0; i < elements.length; i++) {
elements[i].addEventListener('click', () => {
document.addEventListener("turbolinks:before-render", () => {
scrollPosition = window.scrollY;
}, {once: true})
})
}
}
document.addEventListener('turbolinks:load', turbolinksPersistScroll);
document.addEventListener('turbolinks:render', turbolinksPersistScroll);

Rendering from two cameras at the same time in A-Frame

the recent v0.3.0 blog post mentions WebVR 1.0 support allowing "us to have different content on the desktop display than the headset, opening the door for asynchronous gameplay and spectator modes." This is precisely what I'm trying to get working. I'm looking to have one camera in the scene represent the viewpoint of the HMD and a secondary camera represent a spectator of the same scene and render that view to a canvas on the same webpage. 0.3.0 removes the ability to render a-scene to a specific canvas in favor of embedded component. Any thoughts on how to accomplish two cameras rendering a single scene simultaneously?
My intention is to have a the desktop display show what a user is doing from a different perspective. My end goal is to be able to build a mixed reality green screen component.
While there may be a better or cleaner way to do this in the future, I was able to get a second camera rendering by looking at examples of how this is done in the THREE.js world.
I add a component to a non-active camera called spectator. in the init function I set up a new renderer and attach to div outside the scene to create a new canvas. I then call the render method inside the tick() part of the lifecycle.
I have not worked out how to isolate the movement of this camera yet. The default look controls of the 0.3.0 aframe scene still control both camera
Source code:
https://gist.github.com/derickson/334a48eb1f53f6891c59a2c137c180fa
I've created a set of components that can help with this. https://github.com/diarmidmackenzie/aframe-multi-camera
Here's an example showing usage with A-Frame 1.2.0 to display the main camera on the left half of the screen, and a secondary camera on the right half.
<!DOCTYPE html>
<html>
<head>
<script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/diarmidmackenzie/aframe-multi-camera#latest/src/multi-camera.min.js"></script>
</head>
<body>
<div>
<a-scene>
<a-entity camera look-controls wasd-controls position="0 1.6 0">
<!-- first secondary camera is a child of the main camera, so that it always has the same position / rotation -->
<!-- replace main camera (since main camera is rendered across the whole screen, which we don't want) -->
<a-entity
id="camera1"
secondary-camera="outputElement:#viewport1;sequence: replace"
>
</a-entity>
</a-entity>
<!-- PUT YOUR SCENE CONTENT HERE-->
<!-- position of 2nd secondary camera-->
<a-entity
id="camera2"
secondary-camera="outputElement:#viewport2"
position="8 1.6 -6"
rotation="0 90 0"
>
</a-entity>
</a-scene>
</div>
<!-- standard HTML to contrl layout of the two viewports-->
<div style="width: 100%; height:100%; display: flex">
<div id="viewport1" style="width: 50%; height:100%"></div>
<div id="viewport2" style="width: 50%; height:100%"></div>
</div>
</body>
</html>
Also here as a glitch: https://glitch.com/edit/#!/recondite-polar-hyssop
It's also been suggested that I post the entire source code for the multi-camera component here.
Here it is...
/* System that supports capture of the the main A-Frame render() call
by add-render-call */
AFRAME.registerSystem('add-render-call', {
init() {
this.render = this.render.bind(this);
this.originalRender = this.el.sceneEl.renderer.render;
this.el.sceneEl.renderer.render = this.render;
this.el.sceneEl.renderer.autoClear = false;
this.preRenderCalls = [];
this.postRenderCalls = [];
this.suppresssDefaultRenderCount = 0;
},
addPreRenderCall(render) {
this.preRenderCalls.push(render)
},
removePreRenderCall(render) {
const index = this.preRenderCalls.indexOf(render);
if (index > -1) {
this.preRenderCalls.splice(index, 1);
}
},
addPostRenderCall(render) {
this.postRenderCalls.push(render)
},
removePostRenderCall(render) {
const index = this.postRenderCalls.indexOf(render);
if (index > -1) {
this.postRenderCalls.splice(index, 1);
}
else {
console.warn("Unexpected failure to remove render call")
}
},
suppressOriginalRender() {
this.suppresssDefaultRenderCount++;
},
unsuppressOriginalRender() {
this.suppresssDefaultRenderCount--;
if (this.suppresssDefaultRenderCount < 0) {
console.warn("Unexpected unsuppression of original render")
this.suppresssDefaultRenderCount = 0;
}
},
render(scene, camera) {
renderer = this.el.sceneEl.renderer
// set up THREE.js stats to correctly count across all render calls.
renderer.info.autoReset = false;
renderer.info.reset();
this.preRenderCalls.forEach((f) => f());
if (this.suppresssDefaultRenderCount <= 0) {
this.originalRender.call(renderer, scene, camera)
}
this.postRenderCalls.forEach((f) => f());
}
});
/* Component that captures the main A-Frame render() call
and adds an additional render call.
Must specify an entity and component that expose a function call render(). */
AFRAME.registerComponent('add-render-call', {
multiple: true,
schema: {
entity: {type: 'selector'},
componentName: {type: 'string'},
sequence: {type: 'string', oneOf: ['before', 'after', 'replace'], default: 'after'}
},
init() {
this.invokeRender = this.invokeRender.bind(this);
},
update(oldData) {
// first clean up any old settings.
this.removeSettings(oldData)
// now add new settings.
if (this.data.sequence === "before") {
this.system.addPreRenderCall(this.invokeRender)
}
if (this.data.sequence === "replace") {
this.system.suppressOriginalRender()
}
if (this.data.sequence === "after" ||
this.data.sequence === "replace")
{
this.system.addPostRenderCall(this.invokeRender)
}
},
remove() {
this.removeSettings(this.data)
},
removeSettings(data) {
if (data.sequence === "before") {
this.system.removePreRenderCall(this.invokeRender)
}
if (data.sequence === "replace") {
this.system.unsuppressOriginalRender()
}
if (data.sequence === "after" ||
data.sequence === "replace")
{
this.system.removePostRenderCall(this.invokeRender)
}
},
invokeRender()
{
const componentName = this.data.componentName;
if ((this.data.entity) &&
(this.data.entity.components[componentName])) {
this.data.entity.components[componentName].render(this.el.sceneEl.renderer, this.system.originalRender);
}
}
});
/* Component to set layers via HTML attribute. */
AFRAME.registerComponent('layers', {
schema : {type: 'number', default: 0},
init: function() {
setObjectLayer = function(object, layer) {
if (!object.el ||
!object.el.hasAttribute('keep-default-layer')) {
object.layers.set(layer);
}
object.children.forEach(o => setObjectLayer(o, layer));
}
this.el.addEventListener("loaded", () => {
setObjectLayer(this.el.object3D, this.data);
});
if (this.el.hasAttribute('text')) {
this.el.addEventListener("textfontset", () => {
setObjectLayer(this.el.object3D, this.data);
});
}
}
});
/* This component has code in common with viewpoint-selector-renderer
However it's a completely generic stripped-down version, which
just delivers the 2nd camera function.
i.e. it is missing:
- The positioning of the viewpoint-selector entity.
- The cursor / raycaster elements.
*/
AFRAME.registerComponent('secondary-camera', {
schema: {
output: {type: 'string', oneOf: ['screen', 'plane'], default: 'screen'},
outputElement: {type: 'selector'},
cameraType: {type: 'string', oneOf: ['perspective, orthographic'], default: 'perspective'},
sequence: {type: 'string', oneOf: ['before', 'after', 'replace'], default: 'after'},
quality: {type: 'string', oneOf: ['high, low'], default: 'high'}
},
init() {
if (!this.el.id) {
console.error("No id specified on entity. secondary-camera only works on entities with an id")
}
this.savedViewport = new THREE.Vector4();
this.sceneInfo = this.prepareScene();
this.activeRenderTarget = 0;
// add the render call to the scene
this.el.sceneEl.setAttribute(`add-render-call__${this.el.id}`,
{entity: `#${this.el.id}`,
componentName: "secondary-camera",
sequence: this.data.sequence});
// if there is a cursor on this entity, set it up to read this camera.
if (this.el.hasAttribute('cursor')) {
this.el.setAttribute("cursor", "canvas: user; camera: user");
this.el.addEventListener('loaded', () => {
this.el.components['raycaster'].raycaster.layers.mask = this.el.object3D.layers.mask;
const cursor = this.el.components['cursor'];
cursor.removeEventListeners();
cursor.camera = this.camera;
cursor.canvas = this.data.outputElement;
cursor.canvasBounds = cursor.canvas.getBoundingClientRect();
cursor.addEventListeners();
cursor.updateMouseEventListeners();
});
}
if (this.data.output === 'plane') {
if (!this.data.outputElement.hasLoaded) {
this.data.outputElement.addEventListener("loaded", () => {
this.configureCameraToPlane()
});
} else {
this.configureCameraToPlane()
}
}
},
configureCameraToPlane() {
const object = this.data.outputElement.getObject3D('mesh');
function nearestPowerOf2(n) {
return 1 << 31 - Math.clz32(n);
}
// 2 * nearest power of 2 gives a nice look, but at a perf cost.
const factor = (this.data.quality === 'high') ? 2 : 1;
const width = factor * nearestPowerOf2(window.innerWidth * window.devicePixelRatio);
const height = factor * nearestPowerOf2(window.innerHeight * window.devicePixelRatio);
function newRenderTarget() {
const target = new THREE.WebGLRenderTarget(width,
height,
{
minFilter: THREE.LinearFilter,
magFilter: THREE.LinearFilter,
stencilBuffer: false,
generateMipmaps: false
});
return target;
}
// We use 2 render targets, and alternate each frame, so that we are
// never rendering to a target that is actually in front of the camera.
this.renderTargets = [newRenderTarget(),
newRenderTarget()]
this.camera.aspect = object.geometry.parameters.width /
object.geometry.parameters.height;
},
remove() {
this.el.sceneEl.removeAttribute(`add-render-call__${this.el.id}`);
if (this.renderTargets) {
this.renderTargets[0].dispose();
this.renderTargets[1].dispose();
}
// "Remove" code does not tidy up adjustments made to cursor component.
// rarely necessary as cursor is typically put in place at the same time
// as the secondary camera, and so will be disposed of at the same time.
},
prepareScene() {
this.scene = this.el.sceneEl.object3D;
const width = 2;
const height = 2;
if (this.data.cameraType === "orthographic") {
this.camera = new THREE.OrthographicCamera( width / - 2, width / 2, height / 2, height / - 2, 1, 1000 );
}
else {
this.camera = new THREE.PerspectiveCamera( 45, width / height, 1, 1000);
}
this.scene.add(this.camera);
return;
},
render(renderer, renderFunction) {
// don't bother rendering to screen in VR mode.
if (this.data.output === "screen" && this.el.sceneEl.is('vr-mode')) return;
var elemRect;
if (this.data.output === "screen") {
const elem = this.data.outputElement;
// get the viewport relative position of this element
elemRect = elem.getBoundingClientRect();
this.camera.aspect = elemRect.width / elemRect.height;
}
// Camera position & layers match this entity.
this.el.object3D.getWorldPosition(this.camera.position);
this.el.object3D.getWorldQuaternion(this.camera.quaternion);
this.camera.layers.mask = this.el.object3D.layers.mask;
this.camera.updateProjectionMatrix();
if (this.data.output === "screen") {
// "bottom" position is relative to the whole viewport, not just the canvas.
// We need to turn this into a distance from the bottom of the canvas.
// We need to consider the header bar above the canvas, and the size of the canvas.
const mainRect = renderer.domElement.getBoundingClientRect();
renderer.getViewport(this.savedViewport);
renderer.setViewport(elemRect.left - mainRect.left,
mainRect.bottom - elemRect.bottom,
elemRect.width,
elemRect.height);
renderFunction.call(renderer, this.scene, this.camera);
renderer.setViewport(this.savedViewport);
}
else {
// target === "plane"
// store off current renderer properties so that they can be restored.
const currentRenderTarget = renderer.getRenderTarget();
const currentXrEnabled = renderer.xr.enabled;
const currentShadowAutoUpdate = renderer.shadowMap.autoUpdate;
// temporarily override renderer proeperties for rendering to a texture.
renderer.xr.enabled = false; // Avoid camera modification
renderer.shadowMap.autoUpdate = false; // Avoid re-computing shadows
const renderTarget = this.renderTargets[this.activeRenderTarget];
renderTarget.texture.encoding = renderer.outputEncoding;
renderer.setRenderTarget(renderTarget);
renderer.state.buffers.depth.setMask( true ); // make sure the depth buffer is writable so it can be properly cleared, see #18897
renderer.clear();
renderFunction.call(renderer, this.scene, this.camera);
this.data.outputElement.getObject3D('mesh').material.map = renderTarget.texture;
// restore original renderer settings.
renderer.setRenderTarget(currentRenderTarget);
renderer.xr.enabled = currentXrEnabled;
renderer.shadowMap.autoUpdate = currentShadowAutoUpdate;
this.activeRenderTarget = 1 - this.activeRenderTarget;
}
}
});

Is it possible to display a UI transition to reflect changes in a collection with meteorjs?

I would like to display a pulse transition when my collection change.
In my html file, I have that:
<template name="menuItemTag">
<div class="app-menu-item-tag ui label">{{ count }}</div>
</template>
In my js file, I expose the count variable for my template like that:
Template.menuItemTag.count = function() {
return MyCollection.find().count();
};
With that the count change in the ui when the collection is updated.
Now, I would like to display a pulse transition on my div label each time the count value change.
I tried to use the cursor.observe
Template.menuItemTag.rendered = function() {
MyCollection.find().observe({
added: function (id, user) {
$('.app-menu-item-tag:nth(0)').transition('pulse');
},
removed: function () {
$('.app-menu-item-tag:nth(0)').transition('pulse');
}
});
};
Unfortunately, it is call too many times when the template is rendered the first time.
If initialy I have 40 items in my collection, the transition is played 40 times...
Is there a clean way for playing a ui transition on changes and avoid the collection initialisation?
Try this:
Template.menuItemTag.count = function() {
return Session.get('count');
};
Template.menuItemTag.rendered = function() {
this.countComputation = Deps.autorun(function() {
Session.set('count', MyCollection.find().count());
$('.app-menu-item-tag:nth(0)').transition('pulse');
});
};
Template.menuItemTag.destroyed = function() {
this.countComputation.stop();
};
Thanks sbking for your answer, I still have a problem on initialization of the collection.
I propose below to defer the first animation util the collection will be completely filled:
Template.menuItemTag.count = function() {
return Session.get('count');
};
Template.menuItemTag.rendered = function() {
var that = this;
this.countComputation = Deps.autorun(function() {
Session.set('count', MyCollection.find().count());
// Cancel playing UI transition. The collection is not completely initialized
if (that.handleTimeout) {
Meteor.clearTimeout(that.handleTimeout);
}
// Play the UI transition without the timeout if the collection is initialized
if (that.noNeedTimeoutAnymore) {
$('.app-menu-item-tag:nth(0)').transition('pulse');
}
// Tentative to play the UI transition during the collection feeding
else {
that.handleTimeout = Meteor.setTimeout(function() {
$('.app-menu-item-tag:nth(0)').transition('pulse');
// At this point we know that the collection is totaly initialized
// then we can remove the timeout on animation for the future update
that.noNeedTimeoutAnymore = true;
}, 1500); // You can adjust the delay if needed
}
});
};
Template.menuItemTag.destroyed = function() {
this.countComputation.stop();
};

detect if element is being transitioned

How to detect via js if any sort of transition is being applied to the element right now?
Short story of my problem:
I have a situation where I'm firing a function on the `transitionend` event, but sometimes the element doesn't have any *transition* being applied (because in Firefox, for example, the user might click some element rapidly which makes the transition goes crazy and stop working) so I want to know when it doesn't work and just fire the function myself, skipping the `transitionend`. I am trying to avoid ugly solutions..
You can use the Web Animation API for that, notably the Element#getAnimations method, which will return a list of Animation objects applied on the Element. These will include Web Animations (from .animate()), CSS #keyframes animations, and CSS transitions.
document.querySelectorAll("a").forEach((el) => {
el.onmouseenter = (evt) => {
const animations = el.getAnimations(); // we could also ask to look in the subtree
// we're only interested in CSS transitions
const transitions = animations.filter((anim) => anim instanceof CSSTransition);
console.log(transitions.length
? transitions.map((anim) => anim.transitionProperty )
: "no transition"
);
};
});
a:hover {
color: red;
opacity: 0.5;
}
.color-transition {
transition: color 1s;
}
.color-and-opacity-transition {
transition: color 1s, opacity 5s;
}
/*SO-only*/.as-console-wrapper { max-height: 110px !important }
<b>Hover these anchors to log their applied transitions.</b><br>
<a class="color-transition">color transition</a><br>
<a class="color-and-opacity-transition">color & opacity transition</a><br>
<a>no transition</a>
You can listen to transitionstart, transitionend, and, transitioncancel events. To tell if some element is under transition. However, you cannot know if some element will start a transition (even if it has transition-delay: 0s) using this code:
/** #type {Map<HTMLElement, number>} */
const transitionCounter = new Map();
/** #type {(() => void)[]} */
const waitingTransition = [];
const incReference = (counter, target) => {
if (counter.has(target)) {
counter.set(target, counter.get(target) + 1);
} else {
counter.set(target, 1);
}
};
const desReference = (counter, target) => {
if (!counter.has(target)) {
return;
} else if (counter.get(target) === 1) {
counter.delete(target);
} else {
counter.set(target, counter.get(target) - 1);
}
};
document.addEventListener('transitionstart', event => {
const { target } = event;
incReference(transitionCounter, target);
const onFinish = event => {
if (event.target !== target) return;
desReference(transitionCounter, target);
target.removeEventListener('transitioncancel', onFinish);
target.removeEventListener('transitionend', onFinish);
[...waitingTransition].forEach(listener => { listener(); });
};
target.addEventListener('transitioncancel', onFinish);
target.addEventListener('transitionend', onFinish);
});
/**
* #param {HTMLElement} element
* #returns {boolean}
*/
const isUnderTransition = function (element) {
const parents = [];
for (let i = element; i; i = i.offsetParent) parents.push(i);
return Array.from(transitionCounter.keys()).some(running => parents.includes(running));
};
/**
* #param {HTMLElement} element
* #returns {Promise<void>}
*/
const waitTransitionEnd = async function (element) {
if (!isUnderTransition(element)) return Promise.resolve();
return new Promise(resolve => {
waitingTransition.push(function listener() {
if (isUnderTransition(element)) return;
waitingTransition.splice(waitingTransition.indexOf(listener), 1);
resolve();
});
});
};
As specified by W3C Editor's Draft - CSS Transition
The ‘transitionend’ event occurs at the completion of the transition. In the case where a transition is removed before completion, such as if the transition-property is removed, then the event will not fire.
So, I think there's not a valid simple way to solve this problem. The solution is left to the implementation (the browser) which decide if it does or doesn't render the transition at all.
Maybe, a solution could be to attach a listener to the element that fires the transition and after a specific elapsed time it checks if the transitioned element has the required CSS attributes set, and if those attributes aren't set as expected you can run your function by yourself.

Resources