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.
Related
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.
So I am making a dropdown that uses transition of max-height and opacity, for example if the dropdown was 500 pixels in height, I would use 500px for max-height when open, and 0 when closed. However, my dropdown's height is auto calculated. I need to set max-height to this auto calculated height, so that the transition has correct timin. Otherwise, if I used a larger max-height value, when being closed the dropdown would not move until max-height had transitioned below the true value, and then suddenly move faster for the remaining time. Any way to do this?
So I found a solution. I just set the maxHeight to the offsetHeight before triggering closing of my dropdown.
I also needed to use setTimeout of 0 milliseconds for triggering closing it though after running the below function.
export function retreatDropdown(e: HTMLElement) {
e.style.maxHeight = `${e.offsetHeight}px`;
}
I could do a similar trick for opening animation, but you won't know the correct maxHeight until it is opened, so my guess this is more tricky (but less important than closing animation IMO).
EDIT: Complete (TypeScript) solution for both closing/opening dropdown
const defaultTransitionTime = 200;
export function toggleDropdown(
e: HTMLElement,
value: boolean = null,
toggleAction: () => void = null // Function that toggles true/false for accordion open
) {
if (value) {
openDropdown(e, toggleAction);
} else {
retreatDropdown(e, toggleAction);
}
}
export function openDropdown(
e: HTMLElement,
openAction: () => void = null
) {
if (openAction) setTimeout(() => openAction(), 0);
e.style.display = "block";
setTimeout(() => {
const offsetHeight = e.offsetHeight;
e.style.maxHeight = "0px";
setTimeout(() => {
e.style.maxHeight = `${offsetHeight}px`;
}, 5);
}, 5);
setTimeout(() => {
e.style.maxHeight = "none";
}, defaultTransitionTime);
}
export function retreatDropdown(
e: HTMLElement,
closeAction: () => void = null
) {
e.style.maxHeight = `${e.offsetHeight}px`;
if (closeAction) setTimeout(() => closeAction(), 0);
}
Dropdown CSS class:
.dropdown {
transition: opacity 200ms linear, max-height 200ms linear;
will-change: opacity, max-height;
}
Angular ngStyle (applied on open/close of accordion):
elementStyle(e: HTMLElement) {
return this.isOpen(HTMLElement) // I use a map of booleans for open state here...
? { display: "block", "max-height": "none" }
: { "max-height": 0, opacity: 0 };
}
I wonder if it's possible to restrict keyframe animations to a scope based on classnames. The benefit would be to be able to use the same animation-name multiple times without getting issues. I couldn't find any infos about that..
In case it's not possible:
are there any best practices to handle naming conflicts?
I used to use something like SCSS to generate automatically created names for my keyframes. They might not be as descriptive, but they ensure uniqueness. Something like:
$animation-id-count: 0 !global;
#function animation-id {
$animation-id-count: $animation-id-count + 1;
#return animation-id-#{$animation-id-count};
}
After this, just use the function in your code like this:
.class {
$id: animation-id();
#keyframes #{$id}{
...keyframes
}
animation: $id 1s infinite;
}
That way if you insert it anywhere else in your SCSS or move it, it will still match the right animation, and it stops namespaces from overlapping in any way.
Here is a JSX approach (you will need object-hash for that).
The following example shows how to define different animations with respective unique ID based on transform: scale(n). For that purpose, define a function which returns the keyframes and its ID. The keyframes ID is a custom string suffixed with a hash of the function options (e.g. the scale factor).
(Be careful of CSS custom identifier, e.g. do not include a . in your ID. See MDN: < custom-ident >.)
import hash from "object-hash";
const keyFramesScale = (options = {}) => {
let { transforms, id, scale } = options;
transforms = transforms || "";
scale = scale || 1.25;
const keyFramesId = `scale${id ? "-" + id : ""}-${hash(options).substring(0, 6)}`;
const keyFrames = {
[`#keyframes ${keyFramesId}`]: {
"100%": {
transform: `scale(${scale}) ${transforms}`,
},
"0%": {
transform: `scale(1) ${transforms}`,
}
}
};
return [keyFramesId, keyFrames];
};
How to use it:
const [scaleUpId, keyFramesScaleUp] = keyFramesScale({ scale: 1.25, transforms: "rotate(-30deg)", id: "up" });
const [scaleDownId, keyFramesScaleDown] = keyFramesScale({ scale: 0.75, transforms: "rotate(-30deg)", id: "down" });
// scaleUpId = "scale-up-c61254"
// scaleDownId = "scale-down-6194d5"
// ...
<tag style={{
...keyFramesScaleUp,
...keyFramesScaleDown,
...(!hasTouchScreen && isActive && !isClicked && {
animation: `${scaleUpId} 0.5s infinite alternate linear`,
"&:hover": {
animation: "none",
},
}),
...(isClicked && {
animation: `${scaleDownId} .25s 1 linear`,
}),
}} />
Of course, you can write a more generic function that hashes the whole key frames and assign it an ID based on that.
EDIT
To concretize what has been said, here is the generic approach. We first define a generic function that takes an animation name (e.g. scale, pulse, etc.), its keyframes (which can be an object or a function), and optionally keyframes parameters and its default values.
import hash from "object-hash";
const createKeyFramesId = (id, keyFrames) => {
return `${id}-${hash(keyFrames).substring(0, 6)}`;
};
const genericKeyFrames = (name, keyFrames, defaults = {}, options = {}) => {
if (typeof keyFrames === "function") {
// The order of defaults & options is important: the latter overrides the former.
keyFrames = keyFrames({ ...defaults, ...options });
}
const keyFramesId = createKeyFramesId(name, keyFrames);
const keyFramesObject = {
[`#keyframes ${keyFramesId}`]: keyFrames
};
return [keyFramesId, keyFramesObject];
};
From now on, we can define all kind of animations. Their usage is the same as above.
export const keyFramesPulse = () =>
genericKeyFrames("pulse", {
"100%": {
opacity: "1",
},
"0%": {
opacity: "0.5",
},
});
export const keyFramesRotate = (options = {}) => {
const defaults = {
rotate: 360,
transforms: "",
};
const rotateKeyFrames = ({ rotate, transforms }) => {
return {
"100%": {
transform: `rotate(${rotate}deg) ${transforms}`,
}
}
};
return genericKeyFrames(`rotate`, rotateKeyFrames, defaults, options);
};
export const keyFramesScale = (options = {}) => {
const defaults = {
scale: 1.25,
transforms: ""
};
const scaleKeyFrames = ({ scale, transforms }) => {
return {
"100%": {
transform: `scale(${scale}) ${transforms}`,
},
"0%": {
transform: `scale(1) ${transforms}`,
}
}
};
return genericKeyFrames(`scale`, scaleKeyFrames, defaults, options);
};
What it looks like in DevTools:
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;
}
}
});
We want to display a looping slideshow of pictures that looks like a gif. The current result is visible at this url: https://figuredevices.com.
Our current approach is using opacity to show or hide slides:
class SlideShow extends React.Component {
constructor(props, context) {
super(props);
this.state = {
currentSlide: 0
};
this.interval = null;
}
componentDidMount() {
this.interval = setInterval(this.transitionToNextSlide.bind(this), 200);
}
componentWillUnmount(){
if(this.interval){
clearInterval(this.interval);
}
}
transitionToNextSlide() {
let nextSlide = this.state.currentSlide + 1;
if (nextSlide == this.props.slides.length) {
nextSlide = 0;
}
this.setState({currentSlide: nextSlide});
}
render () {
let slides = this.props.pictures.map((picture, idx) => {
let slideContainerStyle = {
opacity: this.state.currentSlide == idx ? 1 : 0
};
return(
<div style={slideContainerStyle} key={idx}>
<Slide picture={picture}/>
</div>
);
})
let containerStyle = {
width:'100%'
};
return (
<div style={containerStyle}>
{slides}
</div>
);
}
};
Pictures are loaded 5 by five into this.props.picture. The number of pictures is not bounded and I am worried about performance as this number grows. There are two things that don't feel right to me:
The map operation in the render method is traversing a whole array every 200ms only to change two css properties.
The DOM is growing a lot in size but most of nodes are hidden
Would you suggest a better approach, maybe using animation or react-motion ?
You should maintain all of your pictures as an array, and every 200ms, increment the array index. Then, instead of displaying all of the pictures every single time, just have it display the picture at your current index. This is a lot better because you're only returning one photo ever instead of a bunch of invisible ones :)
Note: I wasn't able to test this, so I'm not sure if everything is exactly syntactically correct, but this is the general idea. Every 200ms, increment the slideIndex. Then, always return a div with only the one picture you want to see.
class SlideShow extends React.Component {
constructor(props, context) {
super(props);
this.state = {
slideIndex;
};
this.interval = null;
}
componentDidMount() {
this.interval = setInterval(this.transitionToNextSlide.bind(this), 200);
}
componentWillUnmount(){
if(this.interval){
clearInterval(this.interval);
}
}
transitionToNextSlide() {
this.setState({this.state.slideIndex: (this.state.slideIndex + 1) % this.props.slides.length})
render () {
let containerStyle = {
width:'100%'
};
return (
<div style={containerStyle}>
<Slide picture={this.props.pictures[this.state.currentSlide]}/>
</div>
);
}
};