Background image not showing on canvas in a strange way - image

I'm coding a greeting card generator to train in VueJS 3. Everything is working correctly, apart from one thing, look at my code:
<template>
<div>
<h1>greeting card generator</h1>
<div class="board">
<canvas id='myCanvas' :width="size.w" :height="size.h" tabindex='0'
style="border:1px solid #000000;"
></canvas>
</div>
<textarea
:style="'width:' + size.w + 'px; resize:none;'"
v-model="texte"
placeholder="Write your text here">
</textarea>
</div>
</template>
<script>
import {
defineComponent, onMounted, ref, reactive, watch,
} from 'vue';
export default defineComponent({
setup() {
const myCanvas = ref(null);
const texte = ref('');
const rapport = ref(0);
const size = reactive({
w: window.innerWidth * 0.8,
h: (window.innerWidth * 0.8) / 1.8083832335329342,
});
function drawText() {
const fontSize = 0.05 * window.innerWidth - 10;
myCanvas.value.font = `${fontSize}px Adrip`;
myCanvas.value.textAlign = 'center';
const x = size.w / 2;
const lineHeight = fontSize;
const lines = texte.value.split('\n');
for (let i = 0; i < lines.length; i += 1) {
myCanvas.value.fillText(
lines[lines.length - i - 1],
x,
(size.h * 0.98) - (i * lineHeight),
);
}
}
function initCarte() {
const background = new Image();
background.src = '/img/fond.jpeg';
background.onload = function () {
rapport.value = background.naturalWidth / background.naturalHeight;
size.h = size.w / rapport.value;
try {
myCanvas.value.drawImage(background, 0, 0, size.w, size.h);
} catch (e) {
console.log(`ERREUR DE CHARGEMENT D'IMAGE: ${e}`);
}
drawText();
};
}
function handleResize() {
size.w = window.innerWidth * 0.8;
size.h = size.w / rapport.value;
initCarte();
}
window.addEventListener('resize', handleResize);
onMounted(() => {
const c = document.getElementById('myCanvas');
const ctx = c.getContext('2d');
myCanvas.value = ctx;
initCarte();
});
watch(texte, () => {
initCarte();
});
return {
myCanvas,
size,
texte,
};
},
});
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
#font-face {
font-family: 'Adrip';
src: local('Adrip'), url('/fonts/adrip1.ttf') format('truetype');
}
#myCanvas {
border: 1px solid grey;
}
</style>
Look at this line:
h: (window.innerWidth * 0.8) / 1.8083832335329342,
If I don't hardcode this and only put the canonical value window.innerWidth * 0.8, the image doesn't display, although the size.h = size.w / rapport.value; line executes correctly.
I really don't understand this behaviour, could somebody explain it to me?
Also, if anybody has a clue on how it would be possible to load the image once and for all so that I don't have to load it at every refresh, it would be better :)
Thanks in advance!

Your problem is that you change the size of the canvas after drawing the image, due to how templating magic works. If you put debugger behind drawText(); in the background onload function, you will see that it actually draws the image. However, in this same function, you set size.h. size is reactive, and is thus marked as "dirty". size is also used in the template, so the template is marked dirty. After the onload function is executed, Vue will rerender your template... and erase your image.
I think your best bet here is to use nextTick. You need to use it sparingly, but I think this is one of the instances where you have no choice but to wait for the DOM to settle. To do this, import nextTick from vue:
import { nextTick } from 'vue';
Then surround your drawImage try-catch block with that.
background.onload = function () {
rapport.value = background.naturalWidth / background.naturalHeight;
size.h = size.w / rapport.value;
nextTick(() => {
try {
myCanvas.value.drawImage(background, 0, 0, size.w, size.h);
} catch (e) {
console.log(`ERREUR DE CHARGEMENT D'IMAGE: ${e}`);
}
drawText();
});
};
As for your last question how to load the image once... the short answer is... you can't. Whenever the canvas changes, you need to redraw it. At least the image should be cached by the browser, so it just draws it from cache rather than doing another http request.

Related

How do you correctly include three.js and gltf.js

I'm rewriting my question because stackoverflow thought my post was spam (because I included 6000+ lines of code). I'm trying to make a web app that tracks the user's face and puts a 3D object over the face like a "filter". The thing is, I don't want this app to have any external dependencies except 1 (at least for scripts/packages/modules/whatever). Therefore, I copied three.js minified (from https://cdn.jsdelivr.net/npm/three#0.120.1/build/three.min.js) inbetween in the HTML file, as well as the GLTFLoader.js script from the GitHub repository in examples/jsm/loaders/.
I started with Jeeliz's face filter repository, and I'm trying to implement GLTFLoader.js, but when I use
const loader = new GLTFLoader();
it gives me a GLTFLoader is not defined error, as well as console messages that ES6 module was not imported.
When I use
const loader = new THREE.GLTFLoader();
it says it's not a constructor, so I lean towards the former being the correct way to construct the loader.
I appreciate any help in advance! I mostly code in Python or C++ and I'm still a beginner, but I've tinkered with JavaScript a few times so I thought I could handle this. I posted this question once but stackoverflow crashed after saying the post is spam, so I'm including a link to the html file:
https://www.dropbox.com/s/p86xchlldr7j2vf/index-mine.html?dl=0
Code:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta http-equiv="content-language" content="en-EN" />
<title>Filter Web App</title>
<!-- INCLUDE FACEFILTER SCRIPT -->
<script src=‘https://raw.githubusercontent.com/jeeliz/jeelizFaceFilter/master/dist/jeelizFaceFilter.js’>
</script>
<!-- INCLUDE THREE.JS -->
<script src=‘https://cdn.jsdelivr.net/npm/three#0.120.1/build/three.min.js’>
</script>
<!-- INCLUDE RESIZER -->
<script src=‘https://raw.githubusercontent.com/jeeliz/jeelizFaceFilter/master/helpers/JeelizResizer.js’>
</script>
<!-- INCLUDE GLTFLOADER.JS -->
<script src=‘https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/jsm/loaders/GLTFLoader.js’>
</script>
<!-- INCLUDE JEELIZ THREE.JS HELPER -->
<script>
/*
Helper for Three.js
*/
const punkThreeHelper = (function(){
// internal settings:
const _settings = {
rotationOffsetX: 0.0, // negative -> look upper. in radians
pivotOffsetYZ: [0.2, 0.6],// YZ of the distance between the center of the cube and the pivot
detectionThreshold: 0.8, // sensibility, between 0 and 1. Less -> more sensitive
detectionHysteresis: 0.02,
//tweakMoveYRotateX: 0,//0.5, // tweak value: move detection window along Y axis when rotate the face around X (look up <-> down)
cameraMinVideoDimFov: 35 // Field of View for the smallest dimension of the video in degrees
};
// private vars:
let _threeRenderer = null,
_threeScene = null,
_threeVideoMesh = null,
_threeVideoTexture = null,
_threeTranslation = null;
let _maxFaces = -1,
_isMultiFaces = false,
_detectCallback = null,
_isVideoTextureReady = false,
_isSeparateThreeCanvas = false,
_faceFilterCv = null,
_videoElement = null,
_isDetected = false,
_scaleW = 1,
_canvasAspectRatio = -1;
const _threeCompositeObjects = [];
let _gl = null,
_glVideoTexture = null,
_glShpCopyCut = null,
_glShpCopyCutVideoMatUniformPointer = null;
let _videoTransformMat2 = null;
// private funcs:
function destroy(){
_isVideoTextureReady = false;
_threeCompositeObjects.splice(0);
if (_threeVideoTexture){
_threeVideoTexture.dispose();
_threeVideoTexture = null;
}
}
function create_threeCompositeObjects(){
for (let i=0; i<_maxFaces; ++i){
// COMPOSITE OBJECT WHICH WILL TRACK A DETECTED FACE
const threeCompositeObject = new THREE.Object3D();
threeCompositeObject.frustumCulled = false;
threeCompositeObject.visible = false;
_threeCompositeObjects.push(threeCompositeObject);
_threeScene.add(threeCompositeObject);
}
}
function create_videoScreen(){
const videoScreenVertexShaderSource = "attribute vec2 position;\n\
uniform mat2 videoTransformMat2;\n\
varying vec2 vUV;\n\
void main(void){\n\
gl_Position = vec4(position, 0., 1.);\n\
vUV = 0.5 + videoTransformMat2 * position;\n\
}";
const videoScreenFragmentShaderSource = "precision lowp float;\n\
uniform sampler2D samplerVideo;\n\
varying vec2 vUV;\n\
void main(void){\n\
gl_FragColor = texture2D(samplerVideo, vUV);\n\
}";
if (_isSeparateThreeCanvas){
const compile_shader = function(source, type, typeString) {
const glShader = _gl.createShader(type);
_gl.shaderSource(glShader, source);
_gl.compileShader(glShader);
if (!_gl.getShaderParameter(glShader, _gl.COMPILE_STATUS)) {
alert("ERROR IN " + typeString + " SHADER: " + _gl.getShaderInfoLog(glShader));
return null;
}
return glShader;
};
const glShaderVertex = compile_shader(videoScreenVertexShaderSource, _gl.VERTEX_SHADER, 'VERTEX');
const glShaderFragment = compile_shader(videoScreenFragmentShaderSource, _gl.FRAGMENT_SHADER, 'FRAGMENT');
_glShpCopyCut = _gl.createProgram();
_gl.attachShader(_glShpCopyCut, glShaderVertex);
_gl.attachShader(_glShpCopyCut, glShaderFragment);
_gl.linkProgram(_glShpCopyCut);
const samplerVideo = _gl.getUniformLocation(_glShpCopyCut, 'samplerVideo');
_glShpCopyCutVideoMatUniformPointer = _gl.getUniformLocation(_glShpCopyCut, 'videoTransformMat2');
return;
}
// init video texture with red:
_threeVideoTexture = new THREE.DataTexture( new Uint8Array([255,0,0]), 1, 1, THREE.RGBFormat);
_threeVideoTexture.needsUpdate = true;
// CREATE THE VIDEO BACKGROUND:
const videoMaterial = new THREE.RawShaderMaterial({
depthWrite: false,
depthTest: false,
vertexShader: videoScreenVertexShaderSource,
fragmentShader: videoScreenFragmentShaderSource,
uniforms:{
samplerVideo: {value: _threeVideoTexture},
videoTransformMat2: {
value: _videoTransformMat2
}
}
});
const videoGeometry = new THREE.BufferGeometry()
const videoScreenCorners = new Float32Array([-1,-1, 1,-1, 1,1, -1,1]);
// handle both new and old THREE.js versions:
const setVideoGeomAttribute = (videoGeometry.setAttribute || videoGeometry.addAttribute).bind(videoGeometry);
setVideoGeomAttribute( 'position', new THREE.BufferAttribute( videoScreenCorners, 2 ) );
videoGeometry.setIndex(new THREE.BufferAttribute(new Uint16Array([0,1,2, 0,2,3]), 1));
_threeVideoMesh = new THREE.Mesh(videoGeometry, videoMaterial);
that.apply_videoTexture(_threeVideoMesh);
_threeVideoMesh.renderOrder = -1000; // render first
_threeVideoMesh.frustumCulled = false;
_threeScene.add(_threeVideoMesh);
} //end create_videoScreen()
function detect(detectState){
_threeCompositeObjects.forEach(function(threeCompositeObject, i){
_isDetected = threeCompositeObject.visible;
const ds = detectState[i];
if (_isDetected && ds.detected < _settings.detectionThreshold-_settings.detectionHysteresis){
// DETECTION LOST
if (_detectCallback) _detectCallback(i, false);
threeCompositeObject.visible = false;
} else if (!_isDetected && ds.detected > _settings.detectionThreshold+_settings.detectionHysteresis){
// FACE DETECTED
if (_detectCallback) _detectCallback(i, true);
threeCompositeObject.visible = true;
}
}); //end loop on all detection slots
}
function update_poses(ds, threeCamera){
// tan( <horizontal FoV> / 2 ):
const halfTanFOVX = Math.tan(threeCamera.aspect * threeCamera.fov * Math.PI/360); //tan(<horizontal FoV>/2), in radians (threeCamera.fov is vertical FoV)
_threeCompositeObjects.forEach(function(threeCompositeObject, i){
if (!threeCompositeObject.visible) return;
const detectState = ds[i];
// tweak Y position depending on rx:
//const tweak = _settings.tweakMoveYRotateX * Math.tan(detectState.rx);
const cz = Math.cos(detectState.rz), sz = Math.sin(detectState.rz);
// relative width of the detection window (1-> whole width of the detection window):
const W = detectState.s * _scaleW;
// distance between the front face of the cube and the camera:
const DFront = 1 / ( 2 * W * halfTanFOVX );
// D is the distance between the center of the unit cube and the camera:
const D = DFront + 0.5;
// coords in 2D of the center of the detection window in the viewport:
const xv = detectState.x * _scaleW;
const yv = detectState.y * _scaleW;
// coords in 3D of the center of the cube (in the view coordinates system):
const z = -D; // minus because view coordinate system Z goes backward
const x = xv * D * halfTanFOVX;
const y = yv * D * halfTanFOVX / _canvasAspectRatio;
// set position before pivot:
threeCompositeObject.position.set(-sz*_settings.pivotOffsetYZ[0], -cz*_settings.pivotOffsetYZ[0], -_settings.pivotOffsetYZ[1]);
// set rotation and apply it to position:
threeCompositeObject.rotation.set(detectState.rx+_settings.rotationOffsetX, detectState.ry, detectState.rz, "ZYX");
threeCompositeObject.position.applyEuler(threeCompositeObject.rotation);
// add translation part:
_threeTranslation.set(x, y+_settings.pivotOffsetYZ[0], z+_settings.pivotOffsetYZ[1]);
threeCompositeObject.position.add(_threeTranslation);
}); //end loop on composite objects
}
//public methods:
const that = {
// launched with the same spec object than callbackReady. set spec.threeCanvasId to the ID of the threeCanvas to be in 2 canvas mode:
init: function(spec, detectCallback){
destroy();
_maxFaces = spec.maxFacesDetected;
_glVideoTexture = spec.videoTexture;
_videoTransformMat2 = spec.videoTransformMat2;
_gl = spec.GL;
_faceFilterCv = spec.canvasElement;
_isMultiFaces = (_maxFaces>1);
_videoElement = spec.videoElement;
// enable 2 canvas mode if necessary:
let threeCanvas = null;
if (spec.threeCanvasId){
_isSeparateThreeCanvas = true;
// adjust the threejs canvas size to the threejs canvas:
threeCanvas = document.getElementById(spec.threeCanvasId);
threeCanvas.setAttribute('width', _faceFilterCv.width);
threeCanvas.setAttribute('height', _faceFilterCv.height);
} else {
threeCanvas = _faceFilterCv;
}
if (typeof(detectCallback) !== 'undefined'){
_detectCallback = detectCallback;
}
// init THREE.JS context:
_threeRenderer = new THREE.WebGLRenderer({
context: (_isSeparateThreeCanvas) ? null : _gl,
canvas: threeCanvas,
alpha: (_isSeparateThreeCanvas || spec.alpha) ? true : false,
preserveDrawingBuffer: true // to make image capture possible
});
_threeScene = new THREE.Scene();
_threeTranslation = new THREE.Vector3();
create_threeCompositeObjects();
create_videoScreen();
// handle device orientation change:
window.addEventListener('orientationchange', function(){
setTimeout(punkfacefilter.resize, 1000);
}, false);
const returnedDict = {
videoMesh: _threeVideoMesh,
renderer: _threeRenderer,
scene: _threeScene
};
if (_isMultiFaces){
returnedDict.faceObjects = _threeCompositeObjects
} else {
returnedDict.faceObject = _threeCompositeObjects[0];
}
return returnedDict;
}, //end that.init()
detect: function(detectState){
const ds = (_isMultiFaces) ? detectState : [detectState];
// update detection states:
detect(ds);
},
get_isDetected: function() {
return _isDetected;
},
render: function(detectState, threeCamera){
const ds = (_isMultiFaces) ? detectState : [detectState];
// update detection states then poses:
detect(ds);
update_poses(ds, threeCamera);
if (_isSeparateThreeCanvas){
// render the video texture on the faceFilter canvas:
_gl.viewport(0, 0, _faceFilterCv.width, _faceFilterCv.height);
_gl.useProgram(_glShpCopyCut);
_gl.uniformMatrix2fv(_glShpCopyCutVideoMatUniformPointer, false, _videoTransformMat2);
_gl.activeTexture(_gl.TEXTURE0);
_gl.bindTexture(_gl.TEXTURE_2D, _glVideoTexture);
_gl.drawElements(_gl.TRIANGLES, 3, _gl.UNSIGNED_SHORT, 0);
} else {
// reinitialize the state of THREE.JS because JEEFACEFILTER have changed stuffs:
// -> can be VERY costly !
_threeRenderer.state.reset();
}
// trigger the render of the THREE.JS SCENE:
_threeRenderer.render(_threeScene, threeCamera);
},
sortFaces: function(bufferGeometry, axis, isInv){ // sort faces long an axis
// Useful when a bufferGeometry has alpha: we should render the last faces first
const axisOffset = {X:0, Y:1, Z:2}[axis.toUpperCase()];
const sortWay = (isInv) ? -1 : 1;
// fill the faces array:
const nFaces = bufferGeometry.index.count/3;
const faces = new Array(nFaces);
for (let i=0; i<nFaces; ++i){
faces[i] = [bufferGeometry.index.array[3*i], bufferGeometry.index.array[3*i+1], bufferGeometry.index.array[3*i+2]];
}
// compute centroids:
const aPos = bufferGeometry.attributes.position.array;
const centroids = faces.map(function(face, faceIndex){
return [
(aPos[3*face[0]]+aPos[3*face[1]]+aPos[3*face[2]])/3, // X
(aPos[3*face[0]+1]+aPos[3*face[1]+1]+aPos[3*face[2]+1])/3, // Y
(aPos[3*face[0]+2]+aPos[3*face[1]+2]+aPos[3*face[2]+2])/3, // Z
face
];
});
// sort centroids:
centroids.sort(function(ca, cb){
return (ca[axisOffset]-cb[axisOffset]) * sortWay;
});
// reorder bufferGeometry faces:
centroids.forEach(function(centroid, centroidIndex){
const face = centroid[3];
bufferGeometry.index.array[3*centroidIndex] = face[0];
bufferGeometry.index.array[3*centroidIndex+1] = face[1];
bufferGeometry.index.array[3*centroidIndex+2] = face[2];
});
}, //end sortFaces
get_threeVideoTexture: function(){
return _threeVideoTexture;
},
apply_videoTexture: function(threeMesh){
if (_isVideoTextureReady){
return;
}
threeMesh.onAfterRender = function(){
// Replace _threeVideoTexture.__webglTexture by the real video texture:
try {
_threeRenderer.properties.update(_threeVideoTexture, '__webglTexture', _glVideoTexture);
_threeVideoTexture.magFilter = THREE.LinearFilter;
_threeVideoTexture.minFilter = THREE.LinearFilter;
_isVideoTextureReady = true;
} catch(e){
console.log('WARNING in punkThreeHelper: the glVideoTexture is not fully initialized');
}
delete(threeMesh.onAfterRender);
};
},
// create an occluder, IE a transparent object which writes on the depth buffer:
create_threejsOccluder: function(occluderURL, callback){
const occluderMesh = new THREE.Mesh();
new THREE.BufferGeometryLoader().load(occluderURL, function(occluderGeometry){
const mat = new THREE.ShaderMaterial({
vertexShader: THREE.ShaderLib.basic.vertexShader,
fragmentShader: "precision lowp float;\n void main(void){\n gl_FragColor=vec4(1.,0.,0.,1.);\n }",
uniforms: THREE.ShaderLib.basic.uniforms,
colorWrite: false
});
occluderMesh.renderOrder = -1; //render first
occluderMesh.material = mat;
occluderMesh.geometry = occluderGeometry;
if (typeof(callback)!=='undefined' && callback) callback(occluderMesh);
});
return occluderMesh;
},
set_pivotOffsetYZ: function(pivotOffset) {
_settings.pivotOffsetYZ = pivotOffset;
},
create_camera: function(zNear, zFar){
const threeCamera = new THREE.PerspectiveCamera(1, 1, (zNear) ? zNear : 0.1, (zFar) ? zFar : 100);
that.update_camera(threeCamera);
return threeCamera;
},
update_camera: function(threeCamera){
// compute aspectRatio:
const canvasElement = _threeRenderer.domElement;
const cvw = canvasElement.width;
const cvh = canvasElement.height;
_canvasAspectRatio = cvw / cvh;
// compute vertical field of view:
const vw = _videoElement.videoWidth;
const vh = _videoElement.videoHeight;
const videoAspectRatio = vw / vh;
const fovFactor = (vh > vw) ? (1.0 / videoAspectRatio) : 1.0;
const fov = _settings.cameraMinVideoDimFov * fovFactor;
console.log('INFO in punkThreeHelper - update_camera(): Estimated vertical video FoV is', fov);
// compute X and Y offsets in pixels:
let scale = 1.0;
if (_canvasAspectRatio > videoAspectRatio) {
// the canvas is more in landscape format than the video, so we crop top and bottom margins:
scale = cvw / vw;
} else {
// the canvas is more in portrait format than the video, so we crop right and left margins:
scale = cvh / vh;
}
const cvws = vw * scale, cvhs = vh * scale;
const offsetX = (cvws - cvw) / 2.0;
const offsetY = (cvhs - cvh) / 2.0;
_scaleW = cvw / cvws;
// apply parameters:
threeCamera.aspect = _canvasAspectRatio;
threeCamera.fov = fov;
console.log('INFO in punkThreeHelper.update_camera(): camera vertical estimated FoV is', fov, 'deg');
threeCamera.setViewOffset(cvws, cvhs, offsetX, offsetY, cvw, cvh);
threeCamera.updateProjectionMatrix();
// update drawing area:
_threeRenderer.setSize(cvw, cvh, false);
_threeRenderer.setViewport(0, 0, cvw, cvh);
}, //end update_camera()
resize: function(w, h, threeCamera){
_threeRenderer.domElement.width = w;
_threeRenderer.domElement.height = h;
punkfacefilter.resize();
if (threeCamera){
that.update_camera(threeCamera);
}
}
}
return that;
})();
// Export ES6 module:
try {
module.exports = punkThreeHelper;
} catch(e){
console.log('punkThreeHelper ES6 Module not exported');
window.punkThreeHelper = punkThreeHelper;
}
</script>
<!-- INCLUDE DEMO SCRIPT -->
<script>
let THREECAMERA = null;
// callback: launched if a face is detected or lost.
function detect_callback(faceIndex, isDetected) {
if (isDetected) {
console.log('INFO in detect_callback(): DETECTED');
} else {
console.log('INFO in detect_callback(): LOST');
}
}
// build the 3D. called once when punk Face Filter is OK
function init_threeScene(spec) {
const threeStuffs = punkThreeHelper.init(spec, detect_callback);
// CREATE A CUBE
const loader = new GLTFLoader();
loader.load( '/your-glb-file2.glb', function ( gltf ) {
threeStuffs.faceObject.add( gltf.scene );
} );
//CREATE THE CAMERA
THREECAMERA = punkThreeHelper.create_camera();
}
// entry point:
function main(){
punkResizer.size_canvas({
canvasId: 'jeeFaceFilterCanvas',
callback: function(isError, bestVideoSettings){
init_faceFilter(bestVideoSettings);
}
})
}
function init_faceFilter(videoSettings){
punkfacefilter.init({
followZRot: true,
canvasId: 'jeeFaceFilterCanvas',
NNCPath: '/', // root of NN_DEFAULT.json file
maxFacesDetected: 1,
callbackReady: function(errCode, spec){
if (errCode){
console.log('AN ERROR HAPPENS. ERR =', errCode);
return;
}
console.log('INFO: punkfacefilter IS READY');
init_threeScene(spec);
},
// called at each render iteration (drawing loop):
callbackTrack: function(detectState){
punkThreeHelper.render(detectState, THREECAMERA);
}
}); //end punkfacefilter.init call
}
window.addEventListener('load', main);
</script>
<style>
a {color: #eee; text-decoration: none}
a:hover {color: blue;}
body {overflow: auto; overflow-y: auto;
background-color: white;
background-attachment: fixed;
background-position: center;
background-size: contain;
margin: 0px;}
#jeeFaceFilterCanvas {
z-index: 10;
position: absolute;
max-height: 100%;
max-width: 100%;
left: 50%;
top: 50%;
width: 100vmin;
transform: translate(-50%, -50%) rotateY(180deg);
}
#threeCanvas{
z-index: 11;
position: absolute;
max-height: 100%;
max-width: 100%;
left: 50%;
top: 50%;
width: 100vmin;
transform: translate(-50%, -50%) rotateY(180deg);
}
#media (max-width: 787px) {
#jeeFaceFilterCanvas {
right: 0px;
top: 60px;
transform: rotateY(180deg);
}
}
</style>
</head>
<body>
<canvas width="600" height="600" id='jeeFaceFilterCanvas'></canvas>
</body>
</html>
/your-glb-file2.glb is a correct 3D file in the same directory as this. I made this code from the html file I linked, but src’d the script from URLs, unlike the real html file I linked.
The
// CREATE A CUBE
const loader = new GLTFLoader();
loader.load( '/your-glb-file2.glb', function ( gltf ) {
threeStuffs.faceObject.add( gltf.scene );
} );
near the end is the problem. The app correctly puts a cube over my face like I want it to when it’s
// CREATE A CUBE
const cubeGeometry = new THREE.BoxGeometry(1,1,1);
const cubeMaterial = new THREE.MeshNormalMaterial();
const threeCube = new THREE.Mesh(cubeGeometry, cubeMaterial);
threeCube.frustumCulled = false;
threeStuffs.faceObject.add(threeCube);
I tried constructing the loader as a "sub"function of three.js and not as one, but it isn't working either way, when it worked with a cube over my face and the cube 3D object loaded with three.js' native functions.

new Image() in Next.js?

Can you use new Image() inside the Next.js? I am getting error, saying it is not defined.
I know I can simply use inside JSX, but in this case, I am not sure if it will work, as I must reference the image outisde JSX, as shown below.
If there is no way to make new Image() work with Next.js, please show a different way to make this work. This new Image() is for making a GSAP scroll animation work.
Here is my code:
import Link from 'next/link'
import Head from 'next/head'
import Image from 'next/image'
import {useEffect, useRef, useState} from 'react'
import styles from '../styles/Home.module.css'
import {gsap} from 'gsap'
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
const Home = () => {
const viewer = useRef(null)
const image = new Image();
if (typeof window !== "undefined") {
gsap.registerPlugin(ScrollTrigger);
}
useEffect(()=>{
const rows = 5
const columns = 10
const frame_count = rows * columns - 1
// const imageWidth = 6336
// const imageHeight = 1782
const imageWidth = 4049
const imageHeight = 3000
const horizDiff = imageWidth / columns
const vertDiff = imageHeight / rows
const ctx = viewer.current.getContext("2d");
viewer.current.width = horizDiff;
viewer.current.height = vertDiff;
const image = new Image()
image.src = "./spriteDesk.jpg";
// image.src = "./spriteMobile.jpg";
image.onload = () => onUpdate();
const setPos = gsap.quickSetter(viewer.current, "background-position");
const obj = {num: 0};
gsap.to(obj, {
num: frame_count,
ease: "steps(" + frame_count + ")",
scrollTrigger: {
trigger: ".section-one",
start: "top top",
end: "+=" + imageHeight,
pin: true,
anticipatePin: 1,
scrub: 1
},
onUpdate
});
function onUpdate() {
ctx.clearRect(0, 0, horizDiff, vertDiff);
const x = Math.round((obj.num % columns) * horizDiff);
const y = Math.round(Math.floor(obj.num / columns) * vertDiff);
ctx.drawImage(image, x, y, horizDiff, vertDiff, 0, 0, horizDiff, vertDiff);
}
},[])
return (
<>
<Head>
<title>TalkingSkunk | Home</title>
<meta name='keywords' content='talkingskunk' />
<link rel="icon" href="/favicon.ico" />
{/* <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.6.1/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.6.1/ScrollTrigger.min.js"></script> */}
</Head>
{/* <div className={styles.bgWrap}>
<Image
src="/spriteDesk.png"
className='cityscape'
data-speed="0.2"
layout="fill"
objectFit="cover"
quality={100}
/> */}
{/* <Image src ='/spriteDesk.jpg' alt="spriteDesk" width ={4049} height = {3000} /> */}
{/* <p className={styles.bgText}>
Discover
</p> */}
<section className="styles.scene styles.section section-one">
<canvas ref={viewer} className="styles.viewer"></canvas>
</section>
</>
)
}
export default Home;
If anyone still needs this it can be accomplished by following the next steps:
Replace import Image from 'next/image' with import NextImage from 'next/image' (or anything similar),
Replace all <Image ... tags with <NextImage ... (or, if you don't have any, than just remove the import)
Use The Image() constructor to create a new HTMLImageElement instance described on mdn page.
In case you use Next.js version 11+ (or earlier) use one of the solutions described on this question to disable no-img-element lint in order to prevent the error.

In nextTick component is not redrawn

Please, look at my code, I assign a new value to the variable in data and var width have value 100. After that, when animation end, i try return value to var width 100, and start animation again, but Vue does not assign new value 100 and stay 0. But if i will do this with setTimeout it's work perfect. Why is this not happening in nextTick?
link to jsfiddle
new Vue({
el: '#app',
data: {
width: 100,
time: 0
},
mounted() {
setTimeout(() => {
this.time = 5000;
this.width = 0;
setTimeout(() => {
this.rerenderBar();
}, 5100)
}, 1000)
},
methods: {
rerenderBar() {
this.time = 0;
this.width = 100;
/* this.$nextTick(() => {
this.time = 5000;
this.width = 0;
}) */
setTimeout(() => {
this.time = 5000;
this.width = 0;
}, 1500)
}
}
})
<div id="app">
<div class="progress-bar-wrap">
<div class="progress-bar" :style="{
'width': width + '%',
'transition-duration': `${time}ms`
}"></div>
</div>
</div>
My guess is that because $nextTick runs after Vue's DOM update cycle and your animations are powered by css transitions directly on the element (not handled by Vue), the $nextTick happens immediately after calling renderBar It does not wait for your animation to complete.
If you need to wait for the animation to finish, you can look into using Vue Transitions and use Javascript Hooks to reset the width of the bar when the animation finishes.

Editing part sizes of a 3d model

I want to develop a web app to entering measurements of a man and displaying a 3d model with these measurements. I have chosen three.js to start it. And I downloaded a 3d model named standard-male-figure from clara.io. Here is my code to display human model.
import React, { Component } from "react";
import PropTypes from "prop-types";
import withStyles from "#material-ui/core/styles/withStyles";
import * as THREE from "three-full";
const styles = (/*theme*/) => ({
});
class ThreeDView extends Component {
constructor(props){
super(props);
this.start = this.start.bind(this);
this.stop = this.stop.bind(this);
this.renderScene - this.renderScene.bind(this);
this.animate = this.animate.bind(this);
}
componentDidMount() {
const width = this.mount.clientWidth;
const height = this.mount.clientHeight;
//ADD SCENE
this.scene = new THREE.Scene();
//ADD CAMERA
this.camera = new THREE.PerspectiveCamera(100,1);
this.camera.position.z = 12;
this.camera.position.y = 0;
this.camera.position.x = 0;
//ADD RENDERER
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setClearColor("#f0f0f0");
this.renderer.setSize(width, height);
this.mount.appendChild(this.renderer.domElement);
// MOUSE ROTATION
this.orbit = new THREE.OrbitControls(this.camera,this.renderer.domElement);
this.orbit.update();
//ADD LIGHTS
this.light = new THREE.PointLight(0xffffff,1.3);
this.light.position.z = 10;
this.light.position.y=20;
this.scene.add(this.light);
// ADD MAN FIGURE
const loader = new THREE.ColladaLoader();
loader.load("/models/standard-male-figure.dae",(manFigure)=>{
this.man = manFigure;
this.man.name = "man-figure";
this.man.scene.position.y = -10;
this.scene.add(this.man.scene);
},undefined,()=>alert("Loading failed"));
this.start();
}
componentWillUnmount() {
this.stop();
this.mount.removeChild(this.renderer.domElement);
}
start() {
if (!this.frameId) {
this.frameId = requestAnimationFrame(this.animate);
}
}
stop () {
cancelAnimationFrame(this.frameId);
}
animate () {
this.renderScene();
this.frameId = window.requestAnimationFrame(this.animate);
}
renderScene () {
this.orbit.update();
this.light.position.z = this.camera.position.z;
this.light.position.y=this.camera.position.y+20;
this.light.position.x=this.camera.position.x;
this.renderer.render(this.scene, this.camera);
}
render() {
return (
<div style={{ height: "640px" }} ref={(mount) => { this.mount = mount; }} >
</div>
);
}
}
ThreeDView.propTypes = {
values: PropTypes.object
};
/*
all values in inches
values = {
heightOfHand:10,
, etc..
}
*/
export default withStyles(styles)(ThreeDView);
values is measurements that user is entering. I have no idea about how to start updating 3d model with these measurements. Please give me a starting point or any advise to complete this. Thank You!.
Firstly you can get the current size and scale of your man
const box = new THREE.Box3().setFromObject(this.man)
const currentObjectSize = box.getSize()
const currentObjectScale = this.man.scale
and when an user update the size value (newSize), you can calculate a new scale for you man
const newScale = new THREE.Vector3(
currentObjectScale.x * newSize.x/currentObjectSize.x,
currentObjectScale.y * newSize.y/currentObjectSize.y,
currentObjectScale.z * newSize.z/currentObjectSize.z
)
and update your man with this scale
this.man.scale.set(newScale.x, newScale.y, newScale.z)

React - animate mount and unmount of a single component

Something this simple should be easily accomplished, yet I'm pulling my hair out over how complicated it is.
All I want to do is animate the mounting & unmounting of a React component, that's it. Here's what I've tried so far and why each solution won't work:
ReactCSSTransitionGroup - I'm not using CSS classes at all, it's all JS styles, so this won't work.
ReactTransitionGroup - This lower level API is great, but it requires you to use a callback when the animation is complete, so just using CSS transitions won't work here. There are always animation libraries, which leads to the next point:
GreenSock - The licensing is too restrictive for business use IMO.
React Motion - This seems great, but TransitionMotion is extremely confusing and overly complicated for what I need.
Of course I can just do trickery like Material UI does, where the elements are rendered but remain hidden (left: -10000px) but I'd rather not go that route. I consider it hacky, and I want my components to unmount so they clean up and are not cluttering up the DOM.
I want something that's easy to implement. On mount, animate a set of styles; on unmount, animate the same (or another) set of styles. Done. It also has to be high performance on multiple platforms.
I've hit a brick wall here. If I'm missing something and there's an easy way to do this, let me know.
This is a bit lengthy but I've used all the native events and methods to achieve this animation. No ReactCSSTransitionGroup, ReactTransitionGroup and etc.
Things I've used
React lifecycle methods
onTransitionEnd event
How this works
Mount the element based on the mount prop passed(mounted) and with default style(opacity: 0)
After mount or update, use componentDidMount (componentWillReceiveProps for further updates)to change the style (opacity: 1) with a timeout(to make it async).
During unmount, pass a prop to the component to identify unmount, change the style again(opacity: 0), onTransitionEnd, remove unmount the element from the DOM.
Continue the cycle.
Go through the code, you'll understand. If any clarification is needed, please leave a comment.
class App extends React.Component{
constructor(props) {
super(props)
this.transitionEnd = this.transitionEnd.bind(this)
this.mountStyle = this.mountStyle.bind(this)
this.unMountStyle = this.unMountStyle.bind(this)
this.state ={ //base css
show: true,
style :{
fontSize: 60,
opacity: 0,
transition: 'all 2s ease',
}
}
}
componentWillReceiveProps(newProps) { // check for the mounted props
if(!newProps.mounted)
return this.unMountStyle() // call outro animation when mounted prop is false
this.setState({ // remount the node when the mounted prop is true
show: true
})
setTimeout(this.mountStyle, 10) // call the into animation
}
unMountStyle() { // css for unmount animation
this.setState({
style: {
fontSize: 60,
opacity: 0,
transition: 'all 1s ease',
}
})
}
mountStyle() { // css for mount animation
this.setState({
style: {
fontSize: 60,
opacity: 1,
transition: 'all 1s ease',
}
})
}
componentDidMount(){
setTimeout(this.mountStyle, 10) // call the into animation
}
transitionEnd(){
if(!this.props.mounted){ // remove the node on transition end when the mounted prop is false
this.setState({
show: false
})
}
}
render() {
return this.state.show && <h1 style={this.state.style} onTransitionEnd={this.transitionEnd}>Hello</h1>
}
}
class Parent extends React.Component{
constructor(props){
super(props)
this.buttonClick = this.buttonClick.bind(this)
this.state = {
showChild: true,
}
}
buttonClick(){
this.setState({
showChild: !this.state.showChild
})
}
render(){
return <div>
<App onTransitionEnd={this.transitionEnd} mounted={this.state.showChild}/>
<button onClick={this.buttonClick}>{this.state.showChild ? 'Unmount': 'Mount'}</button>
</div>
}
}
ReactDOM.render(<Parent />, document.getElementById('app'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.3.2/react-with-addons.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app"></div>
Here is my solution using the new hooks API (with TypeScript), based on this post, for delaying the component's unmount phase:
function useDelayUnmount(isMounted: boolean, delayTime: number) {
const [ shouldRender, setShouldRender ] = useState(false);
useEffect(() => {
let timeoutId: number;
if (isMounted && !shouldRender) {
setShouldRender(true);
}
else if(!isMounted && shouldRender) {
timeoutId = setTimeout(
() => setShouldRender(false),
delayTime
);
}
return () => clearTimeout(timeoutId);
}, [isMounted, delayTime, shouldRender]);
return shouldRender;
}
Usage:
const Parent: React.FC = () => {
const [ isMounted, setIsMounted ] = useState(true);
const shouldRenderChild = useDelayUnmount(isMounted, 500);
const mountedStyle = {opacity: 1, transition: "opacity 500ms ease-in"};
const unmountedStyle = {opacity: 0, transition: "opacity 500ms ease-in"};
const handleToggleClicked = () => {
setIsMounted(!isMounted);
}
return (
<>
{shouldRenderChild &&
<Child style={isMounted ? mountedStyle : unmountedStyle} />}
<button onClick={handleToggleClicked}>Click me!</button>
</>
);
}
CodeSandbox link.
I countered this problem during my work, and simple as it seemed, it is really not in React. In a normal scenario where you render something like:
this.state.show ? {childen} : null;
as this.state.show changes the children are mounted/unmounted right away.
One approach I took is creating a wrapper component Animate and use it like
<Animate show={this.state.show}>
{childen}
</Animate>
now as this.state.show changes, we can perceive prop changes with getDerivedStateFromProps(componentWillReceiveProps) and create intermediate render stages to perform animations.
We start with Static Stage when the children is mounted or unmounted.
Once we detect the show flag changes, we enter Prep Stage where we calculate necessary properties like height and width from ReactDOM.findDOMNode.getBoundingClientRect().
Then entering Animate State we can use css transition to change height, width and opacity from 0 to the calculated values (or to 0 if unmounting).
At the end of transition, we use onTransitionEnd api to change back to
Static stage.
There are much more details to how the stages transfer smoothly but this could be overall idea:)
If anyone interested, I created a React library https://github.com/MingruiZhang/react-animate-mount to share my solution. Any feedback welcome:)
Using the knowledge gained from Pranesh's answer, I came up with an alternate solution that's configurable and reusable:
const AnimatedMount = ({ unmountedStyle, mountedStyle }) => {
return (Wrapped) => class extends Component {
constructor(props) {
super(props);
this.state = {
style: unmountedStyle,
};
}
componentWillEnter(callback) {
this.onTransitionEnd = callback;
setTimeout(() => {
this.setState({
style: mountedStyle,
});
}, 20);
}
componentWillLeave(callback) {
this.onTransitionEnd = callback;
this.setState({
style: unmountedStyle,
});
}
render() {
return <div
style={this.state.style}
onTransitionEnd={this.onTransitionEnd}
>
<Wrapped { ...this.props } />
</div>
}
}
};
Usage:
import React, { PureComponent } from 'react';
class Thing extends PureComponent {
render() {
return <div>
Test!
</div>
}
}
export default AnimatedMount({
unmountedStyle: {
opacity: 0,
transform: 'translate3d(-100px, 0, 0)',
transition: 'opacity 250ms ease-out, transform 250ms ease-out',
},
mountedStyle: {
opacity: 1,
transform: 'translate3d(0, 0, 0)',
transition: 'opacity 1.5s ease-out, transform 1.5s ease-out',
},
})(Thing);
And finally, in another component's render method:
return <div>
<ReactTransitionGroup>
<Thing />
</ReactTransitionGroup>
</div>
Framer motion
Install framer-motion from npm.
import { motion, AnimatePresence } from "framer-motion"
export const MyComponent = ({ isVisible }) => (
<AnimatePresence>
{isVisible && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
/>
)}
</AnimatePresence>
)
I think using Transition from react-transition-group is probably the easiest way to track mounting/unmounting. It is incredibly flexible. I'm using some classes to show how easy it is to use but you can definitely hook up your own JS animations utilizing addEndListener prop - which I've had a lot of luck using GSAP with as well.
Sandbox: https://codesandbox.io/s/k9xl9mkx2o
And here's my code.
import React, { useState } from "react";
import ReactDOM from "react-dom";
import { Transition } from "react-transition-group";
import styled from "styled-components";
const H1 = styled.h1`
transition: 0.2s;
/* Hidden init state */
opacity: 0;
transform: translateY(-10px);
&.enter,
&.entered {
/* Animate in state */
opacity: 1;
transform: translateY(0px);
}
&.exit,
&.exited {
/* Animate out state */
opacity: 0;
transform: translateY(-10px);
}
`;
const App = () => {
const [show, changeShow] = useState(false);
const onClick = () => {
changeShow(prev => {
return !prev;
});
};
return (
<div>
<button onClick={onClick}>{show ? "Hide" : "Show"}</button>
<Transition mountOnEnter unmountOnExit timeout={200} in={show}>
{state => {
let className = state;
return <H1 className={className}>Animate me</H1>;
}}
</Transition>
</div>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
For those considering react-motion, animating a single component when it mounts and unmounts can be overwhelming to set up.
There's a library called react-motion-ui-pack that makes this process a lot easier to start with. It's a wrapper around react-motion, which means you get all the benefits from the library (i.e. you are able to interrupt the animation, have multiple unmounts happen at the same time).
Usage:
import Transition from 'react-motion-ui-pack'
<Transition
enter={{ opacity: 1, translateX: 0 }}
leave={{ opacity: 0, translateX: -100 }}
component={false}
>
{ this.state.show &&
<div key="hello">
Hello
</div>
}
</Transition>
Enter defines what the end state of the component should be; leave is the style that is applied when the component is unmounted.
You might find that once you have used the UI pack a couple of times, the react-motion library might not be as daunting anymore.
You can do this with React Transition Group. It gives you CSS classes, so you can write your animation code in those CSS classes.
Follow this simple example
import {CSSTransition } from 'react-transition-group';//This should be imported
import './AnimatedText.css';
const AnimatedText = () => {
const [showText, setShowText] = useState(false); //By default text will be not shown
//Handler to switch states
const switchHandler = () =>{
setShowText(!showText);
};
return (
//in : pass your state here, it will used by library to toggle. It should be boolean
//timeout: your amination total time(it should be same as mentioned in css)
//classNames: give class name of your choice, library will prefix it with it's animation classes
//unmountOnExit: Component will be unmounted when your state changes to false
<CSSTransition in={showText} timeout={500} classNames='fade' unmountOnExit={true}>
<h1>Animated Text</h1>
</CSSTransition>
<button onClick={switchHandler}>Show Text</button>
);
};
export default AnimatedText;
Now, let's write the animation in CSS file(AnimatedText.css), Remember classNames property(in this case fade)
//fade class should be prefixed
/*****Fade In effect when component is mounted*****/
//This is when your animation starts
fade-enter {
opacity: 0;
}
//When your animation is active
.fade-enter.fade-enter-active {
opacity: 1;
transition: all 500ms ease-in;
}
/*****Fade In effect when component is mounted*****/
/*****Fade Out effect when component is unmounted*****/
.fade-exit {
opacity: 1;
}
.fade-exit-active {
opacity: 0;
transition: all 500ms ease-out;
}
/*****Fade Out effect when component is unmounted*****/
There's also a appear class, which can be used when your component loads for the first time. Check documentation for more details
Animating enter and exit transitions is much easier with react-move.
example on codesandbox
Here my 2cents:
thanks to #deckele for his solution. My solution is based on his, it's the stateful's component version, fully reusable.
here my sandbox: https://codesandbox.io/s/302mkm1m.
here my snippet.js:
import ReactDOM from "react-dom";
import React, { Component } from "react";
import style from "./styles.css";
class Tooltip extends Component {
state = {
shouldRender: false,
isMounted: true,
}
shouldComponentUpdate(nextProps, nextState) {
if (this.state.shouldRender !== nextState.shouldRender) {
return true
}
else if (this.state.isMounted !== nextState.isMounted) {
console.log("ismounted!")
return true
}
return false
}
displayTooltip = () => {
var timeoutId;
if (this.state.isMounted && !this.state.shouldRender) {
this.setState({ shouldRender: true });
} else if (!this.state.isMounted && this.state.shouldRender) {
timeoutId = setTimeout(() => this.setState({ shouldRender: false }), 500);
() => clearTimeout(timeoutId)
}
return;
}
mountedStyle = { animation: "inAnimation 500ms ease-in" };
unmountedStyle = { animation: "outAnimation 510ms ease-in" };
handleToggleClicked = () => {
console.log("in handleToggleClicked")
this.setState((currentState) => ({
isMounted: !currentState.isMounted
}), this.displayTooltip());
};
render() {
var { children } = this.props
return (
<main>
{this.state.shouldRender && (
<div className={style.tooltip_wrapper} >
<h1 style={!(this.state.isMounted) ? this.mountedStyle : this.unmountedStyle}>{children}</h1>
</div>
)}
<style>{`
#keyframes inAnimation {
0% {
transform: scale(0.1);
opacity: 0;
}
60% {
transform: scale(1.2);
opacity: 1;
}
100% {
transform: scale(1);
}
}
#keyframes outAnimation {
20% {
transform: scale(1.2);
}
100% {
transform: scale(0);
opacity: 0;
}
}
`}
</style>
</main>
);
}
}
class App extends Component{
render(){
return (
<div className="App">
<button onClick={() => this.refs.tooltipWrapper.handleToggleClicked()}>
click here </button>
<Tooltip
ref="tooltipWrapper"
>
Here a children
</Tooltip>
</div>
)};
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Here's how I solved this in 2019, while making a loading spinner. I'm using React functional components.
I have a parent App component that has a child Spinner component.
App has state for whether the app is loading or not. When the app is loading, Spinner is rendered normally. When the app is not loading (isLoading is false) Spinner is rendered with the prop shouldUnmount.
App.js:
import React, {useState} from 'react';
import Spinner from './Spinner';
const App = function() {
const [isLoading, setIsLoading] = useState(false);
return (
<div className='App'>
{isLoading ? <Spinner /> : <Spinner shouldUnmount />}
</div>
);
};
export default App;
Spinner has state for whether it's hidden or not. In the beginning, with default props and state, Spinner is rendered normally. The Spinner-fadeIn class animates it fading in. When Spinner receives the prop shouldUnmount it renders with the Spinner-fadeOut class instead, animating it fading out.
However I also wanted the component to unmount after fading out.
At this point I tried using the onAnimationEnd React synthetic event, similar to #pranesh-ravi's solution above, but it didn't work. Instead I used setTimeout to set the state to hidden with a delay the same length as the animation. Spinner will update after the delay with isHidden === true, and nothing will be rendered.
The key here is that the parent doesn't unmount the child, it tells the child when to unmount, and the child unmounts itself after it takes care of its unmounting business.
Spinner.js:
import React, {useState} from 'react';
import './Spinner.css';
const Spinner = function(props) {
const [isHidden, setIsHidden] = useState(false);
if(isHidden) {
return null
} else if(props.shouldUnmount) {
setTimeout(setIsHidden, 500, true);
return (
<div className='Spinner Spinner-fadeOut' />
);
} else {
return (
<div className='Spinner Spinner-fadeIn' />
);
}
};
export default Spinner;
Spinner.css:
.Spinner {
position: fixed;
display: block;
z-index: 999;
top: 50%;
left: 50%;
margin: -40px 0 0 -20px;
height: 40px;
width: 40px;
border: 5px solid #00000080;
border-left-color: #bbbbbbbb;
border-radius: 40px;
}
.Spinner-fadeIn {
animation:
rotate 1s linear infinite,
fadeIn .5s linear forwards;
}
.Spinner-fadeOut {
animation:
rotate 1s linear infinite,
fadeOut .5s linear forwards;
}
#keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
#keyframes fadeOut {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
#keyframes rotate {
100% {
transform: rotate(360deg);
}
}
I was also in dire need of single component Animation . I tired using React Motion but i was pulling my hairs for such a trivial issue.. (i thing). After some googling i came across this post on their git repo . Hope it helps someone..
Referenced From & also the credit.
This works for me as of now. My use case was a modal to animate and unmount in case of load and unload.
class Example extends React.Component {
constructor() {
super();
this.toggle = this.toggle.bind(this);
this.onRest = this.onRest.bind(this);
this.state = {
open: true,
animating: false,
};
}
toggle() {
this.setState({
open: !this.state.open,
animating: true,
});
}
onRest() {
this.setState({ animating: false });
}
render() {
const { open, animating } = this.state;
return (
<div>
<button onClick={this.toggle}>
Toggle
</button>
{(open || animating) && (
<Motion
defaultStyle={open ? { opacity: 0 } : { opacity: 1 }}
style={open ? { opacity: spring(1) } : { opacity: spring(0) }}
onRest={this.onRest}
>
{(style => (
<div className="box" style={style} />
))}
</Motion>
)}
</div>
);
}
}
This can be done easily using the CSSTransition component from react-transition-group, which is just like the libraries you mentioned. The trick is you need to wrap the CSSTransition component without a show/hide mechanism like you typically would.i.e. {show && <Child>}... Otherwise you are hiding the animation and it won't work. Example:
ParentComponent.js
import React from 'react';
import {CSSTransition} from 'react-transition-group';
function ParentComponent({show}) {
return (
<CSSTransition classes="parentComponent-child" in={show} timeout={700}>
<ChildComponent>
</CSSTransition>
)}
ParentComponent.css
// animate in
.parentComponent-child-enter {
opacity: 0;
}
.parentComponent-child-enter-active {
opacity: 1;
transition: opacity 700ms ease-in;
}
// animate out
.parentComponent-child-exit {
opacity: 1;
}
.parentComponent-child-exit-active {
opacity: 0;
transition: opacity 700ms ease-in;
}
I know there are a lot of answers here, but I still did not find one that suits my needs. I want:
Functional components
A solution that'll allow my components to easily fade in/out when they're mounted/unmounted.
After many hours of fiddling, I have a solution that works I'd say 90%. I've written the limitation in a comment block in the code below. I'd still love a better solution, but this is the best I've found, including the other solutions here.
const TIMEOUT_DURATION = 80 // Just looked like best balance of silky smooth and stop delaying me.
// Wrap this around any views and they'll fade in and out when mounting /
// unmounting. I tried using <ReactCSSTransitionGroup> and <Transition> but I
// could not get them to work. There is one major limitation to this approach:
// If a component that's mounted inside of <Fade> has direct prop changes,
// <Fade> will think that it's a new component and unmount/mount it. This
// means the inner component will fade out and fade in, and things like cursor
// position in forms will be reset. The solution to this is to abstract <Fade>
// into a wrapper component.
const Fade: React.FC<{}> = ({ children }) => {
const [ className, setClassName ] = useState('fade')
const [ newChildren, setNewChildren ] = useState(children)
const effectDependency = Array.isArray(children) ? children : [children]
useEffect(() => {
setClassName('fade')
const timerId = setTimeout(() => {
setClassName('fade show')
setNewChildren(children)
}, TIMEOUT_DURATION)
return () => {
clearTimeout(timerId)
}
}, effectDependency)
return <Container fluid className={className + ' p-0'}>{newChildren}</Container>
}
If you have a component you want to fade in/out, wrap it in <Fade> Ex. <Fade><MyComponent/><Fade>.
Note that this uses react-bootstrap for the class names and for <Container/>, but both could be easily replaced with custom CSS and a regular old <div>.
If I use Velocity or AnimeJS library to animate node directly (instead of css or setTimeout), then I found out I can design a hook to provide the animation status on and function onToggle to kick off the animation (ex. slidedown, fade).
Basically what the hook does is to toggle on and off the animation, and afterwards update the on accordingly. Therefore we can get the status of the animation accurately. Without doing so would reply on a ad-hoc duration.
/**
* A hook to provide animation status.
* #class useAnimate
* #param {object} _ props
* #param {async} _.animate Promise to perform animation
* #param {object} _.node Dom node to animate
* #param {bool} _.disabled Disable animation
* #returns {useAnimateObject} Animate status object
* #example
* const { on, onToggle } = useAnimate({
* animate: async () => { },
* node: node
* })
*/
import { useState, useCallback } from 'react'
const useAnimate = ({
animate, node, disabled,
}) => {
const [on, setOn] = useState(false)
const onToggle = useCallback(v => {
if (disabled) return
if (v) setOn(true)
animate({ node, on: v }).finally(() => {
if (!v) setOn(false)
})
}, [animate, node, disabled, effect])
return [on, onToggle]
}
export default useAnimate
The usage is the following,
const ref = useRef()
const [on, onToggle] = useAnimate({
animate: animateFunc,
node: ref.current,
disabled
})
const onClick = () => { onToggle(!on) }
return (
<div ref={ref}>
{on && <YOUROWNCOMPONENT onClick={onClick} /> }
</div>
)
and the animate implementation could be,
import anime from 'animejs'
const animateFunc = (params) => {
const { node, on } = params
const height = on ? 233 : 0
return new Promise(resolve => {
anime({
targets: node,
height,
complete: () => { resolve() }
}).play()
})
}
You can use React SyntheticEvent for that.
With events like onAnimationEnd or onTransitionEnd you can accomplish that.
React Docs: https://reactjs.org/docs/events.html#animation-events
Code Example: https://dev.to/michalczaplinski/super-easy-react-mount-unmount-animations-with-hooks-4foj
You could always use React lifecycle methods but react-transition-group is by far the most convenient library for animations I have come across, Whether you are using styled-components or plain css. It is especially useful when you want to track the mounting and unmounting of your component and render animations accordingly.
Use Transition with styled-components and CSSTransition when you are using plain css classnames.
If you are looking for simple hooks example:
import React, { useEffect, useReducer } from "react";
import ReactDOM from "react-dom";
const ANIMATION_TIME = 2 * 1000;
function Component() {
const [isMounted, toggleMounted] = useReducer((p) => !p, true);
const [isAnimateAnmount, toggleAnimateUnmount] = useReducer((p) => !p, false);
const [isVisible, toggleVisible] = useReducer((p) => (p ? 0 : 1), 0);
useEffect(() => {
if (isAnimateAnmount) {
toggleVisible();
toggleAnimateUnmount();
setTimeout(() => {
toggleMounted();
}, ANIMATION_TIME);
}
}, [isAnimateAnmount]);
useEffect(() => {
toggleVisible();
}, [isMounted]);
return (
<>
<button onClick={toggleAnimateUnmount}>toggle</button>
<div>{isMounted ? "Mounted" : "Unmounted"}</div>
{isMounted && (
<div
style={{
fontSize: 60,
opacity: isVisible,
transition: "all 2s ease"
}}
>
Example
</div>
)}
</>
);
}
I created a general purpose WrapperComponent called MountAnimation so that you can animate elements in and out without always having to write the same thing over and over. It uses CSSTransitions under the hood so you need to install that.
Install dependecies
npm install react-transition-group
Create component in one of your folders
import { CSSTransition } from "react-transition-group"
export const MountAnimation = ({
children,
timeout = 300, // MATCH YOUR DEFAULT ANIMATION DURATION
isVisible = false,
unmountOnExit = true,
classNames = "transition-translate-y", // ADD YOUR DEFAULT ANIMATION
...restProps
}) => {
return (
<CSSTransition
in={isVisible}
timeout={timeout}
classNames={classNames}
unmountOnExit={unmountOnExit}
{...restProps}
>
<div>{children}</div>
</CSSTransition>
)
}
Simply use it like this:
import { MountAnimation } from '../../path/to/component'
...
const [isElementVisible, setIsElementVisible] = useState(false)
return (
<MountAnimation isVisible={isElementVisible}>
// your content here
</MountAnimation>
)
(Go creative here) You need to declare your animation in your CSS file. Make sure you declare this in a globally available CSS file if you are code-splitting. In this example I am using the following animation:
.transition-translate-y-enter {
opacity: 0;
transform: translateY(-5px);
}
.transition-translate-y-enter-active {
opacity: 1;
transform: translateY(0px);
transition: opacity 300ms ease-in-out, transform 300ms ease-in-out;
}
.transition-translate-y-exit {
opacity: 1;
transform: translateY(0px);
}
.transition-translate-y-exit-active {
opacity: 0;
transform: translateY(-5px);
transition: opacity 300ms ease-in-out, transform 300ms ease-in-out;
}
Here is a live example of this implementation:
https://codesandbox.io/s/vibrant-elion-ngfzr?file=/src/App.js
rewrote of https://stackoverflow.com/a/54114180/8552163
please if you click like on this comment, like the parent too, thank you
clear js
import {useState, useEffect} from 'react';
const useDelayUnmount = (isMounted, msDelay = 500)=>{
const [shouldRender, setShouldRender] = useState(false);
useEffect(()=>{
let timeoutId;
if(isMounted && !shouldRender){
setShouldRender(true);
}else if(!isMounted && shouldRender){
timeoutId = setTimeout(()=>setShouldRender(false), msDelay);
}
return ()=>clearTimeout(timeoutId);
}, [isMounted, msDelay, shouldRender]);
return shouldRender;
};
export default useDelayUnmount;
The below code is somewhat similar to other answers but I wanted to attempt to scale the solution
to easily add different type of transitions e.g. fade, slide etc.
to use the same solution irrespective of conditionally rendering or just rendering without any condition.
In the below code, only these two code items are required to implement
AnimatableComponent - React component which internally uses TransitionComponent.
Note: One can also use TransitionComponent separately too to have transitions on a component which are not rendered conditionally.
And a set of CSS classes.
Rest is how to use them.
/**
* This is a simple component which applies your provided trasition on the component.
* Depending upon whether you want to show or hide the component, it adds In and Out classes on the component.
* For example, if your transition name is slide then it will add slideIn and slideOut classes to the component.
* At the end, it also provides onTransitionEnd event property to know when the transition ends.
*/
const TransitionComponent = ({show, transition, onTransitionEnd, children}) => {
const [transitionDirection, setTransitionDirection] = React.useState('out')
React.useEffect(() => {
const direction = show ? `${transition}In` : `${transition}Out`
setTransitionDirection(direction)
console.log(`${transition} ${direction}`)
}, [show])
return (
<div className={`${transition} ${transitionDirection}`} onTransitionEnd={onTransitionEnd}>
{children}
</div>
)
}
/**
* This can act as base/wrapper component for any custom component to animate
* React Components does not provide you a place to execute some code before a component is unmounted.
* For example, let's say a custom component is conditionally rendered based on a property/expression in the parent component (called as condition) and
* as soon as that condition evaluates to false then component is removed from the DOM.
* What to do when you want to show animations before it is removed? This component handles exactly that.
* This component handles conditional rendering within itself behind mount property and expects you to provide your condition in a show property
* show property is then mapped to the internal mount state at some desired levels like
* - When show is true, set mount to true. Thereby, adding the component in the DOM and running the animation.
* - When show is false, check if transition finishes and set mount to false. Thereby, removing the component from the DOM.
* conditionallyRender property? This component comes with another property so that you can enjoy the animations even if you do not want to conditionally render the component
* - When conditionallyRender is false, mount property is no more in effect and component rendered unconditionally i.e. is not conditionally rendered
* - When conditionallyRender is true (also default), mount property is in effect to conditionally render the component
*/
const AnimatableComponent = ({conditionallyRender = true, show, transition = 'fade', children}) => {
const [mount, setMount] = React.useState(show);
React.useEffect(() => {
if (conditionallyRender && show) {
setMount(true)
console.log('Mounted')
}
}, [show])
const handleTransitionEnd = () => {
if (conditionallyRender && !show) {
setMount(false)
console.log('Unmounted')
}
}
const getAnimatableChildren = () => {
return (
<TransitionComponent show={show} transition={transition} onTransitionEnd={handleTransitionEnd}>
{children}
</TransitionComponent>
)
}
return (
conditionallyRender
? ((show || mount) && getAnimatableChildren())
: getAnimatableChildren()
)
}
const BoxComponent = () => {
return (
<div className='box'>Box</div>
)
}
const App = () => {
const [mountedShow, setMountedShow] = React.useState(false)
const [displayShow, setDisplayShow] = React.useState(false)
const handleMountUnmountClick = () => {
setMountedShow(!mountedShow)
}
const handleShowHideClick = () => {
setDisplayShow(!displayShow)
}
return (
<React.Fragment>
<div style={{display: 'flex'}}>
<div style={{flex: 1}}>
<h1>Mount/Unmount Transitions</h1>
<AnimatableComponent conditionallyRender={true} show={mountedShow} transition='slide'>
<BoxComponent/>
</AnimatableComponent>
<button type="button" onClick={handleMountUnmountClick}>Mount/Unmount Box</button>
</div>
<div style={{flex: 1}}>
<h1>Display Transitions</h1>
<AnimatableComponent conditionallyRender={false} show={displayShow} transition='slide'>
<BoxComponent/>
</AnimatableComponent>
<button type="button" onClick={handleShowHideClick}>Show/Hide Box</button>
</div>
</div>
</React.Fragment>
)
}
ReactDOM.render(<App/>, document.getElementById('app-container'))
.box {
width: 100px;
height: 100px;
border: 1px solid #000;
background-color: #f00;
margin: 24px;
}
.fade {
visibility: hidden;
opacity: 0;
transition: opacity 550ms, visibility 350ms;
}
.fade.fadeIn {
visibility: visible;
opacity: 1;
}
.fade.fadeOut {
visibility: hidden;
opacity: 0;
}
.slide {
visibility: hidden;
opacity: 0;
transform: translateX(100vw);
transition: transform 350ms, opacity 350ms, visibility 350ms;
}
.slide.slideIn {
visibility: visible;
opacity: 1;
transform: translateX(0);
}
.slide.slideOut {
visibility: hidden;
opacity: 0;
transform: translateX(100vw);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
<div id="app-container">This is where React app will initialize</div>
Notes:
Here is a link to a good post that was referred to write this code.
Also referred this Code Sandbox for the condition of conditional rendering where both show and mount variables were used as in ((show || mount) && <Your Component.../>
Earlier I used only mount as in mount && <Your Component.../>. I do not understand the reason but it seems it has something to do with the DOM reflow and how CSS Transitions needs a reflow to properly work when they are applied on a conditionally rendered element e.g. an element added using appendChild or display: none in JS or && in React JSX.
Here is another codesandbox from one of the answer, which uses only one condition but this one used CSS Animations. CSS Animation doesn't seem to have this issue but CSS transitions do.
In Conclusion, it seems that if you use
CSS Transitions, then use logical OR of both the variables as in (show || mount) && <Your Component... />.
CSS Animations, then just the mount as in mount && <Your Component... />.
I found the simplest solution that works for me.
You can make animation using only css.
If we use animations, they are usually looped. But you can add a property that will allow the animation to fire only 1 time.
And when mounting the component, it turns out that we will start the animation, which will work 1 time.
(I use tailwind but the principle is the same)
How it looks in component (like simple class):
...
return (
<>
<div
id="scrim"
onClick={handleClose}
className="fixed inset-0 block flex h-full w-full items-end justify-center overflow-y-hidden bg-gray-800/50"
>
<div className="mt-[56px] flex animate-sheet 👈 flex-col items-center justify-center">
<div className=" z-10 flex w-full min-w-[220px] max-w-[640px] flex-col overflow-y-auto rounded-t-[16px] bg-blue-50 px-[12px] pb-[28px] scrollbar-hide dark:bg-gray-900">
...
Now change configuration of your tailwind file and add additional animation
...
animation: {
sheet: "sheet 0.5s cubic-bezier(.9,0,.2,1) 1", 👈 here you can change "1" to "infinity" and get looped animation or vice versa
},...
And specify keyframes:
...
keyframes: {
sheet: {
from: {
transform: "translateY(100%)",
},
to: {
transform: "translateY(0)",
},
},
...
The disadvantage of this method is that there is no effect when the component is unmounted. But in my case, I think that in terms of the amount of code, its simplicity and the effect achieved, this is an excellent compromise than dragging new dependencies or adding custom hooks.

Resources