Here is an open GitHub issue Github Issue
Here is a Expo Snack
For some reason, variables are not incrementing inside the canvas function while outside works just fine. Please have a look at my code:
function home ({ navigation }) {
const [counter, setCounter] = useState(330);
useEffect(() => {
const timeout = setTimeout(() => {
setCounter(counter + 1);
}, 1000);
return () => {
clearTimeout(timeout);
};
}, [counter]);
console.log('outside ', counter);
const _onGLContextCreate = (gl) => {
var ctx = new Expo2DContext(gl);
// setInterval(() => {
// console.log('set interval doesnt refresh too ', counter);
// }, 1000);
console.log('inside ', counter);
let circle = {
x: counter,
y: 100,
radius: 30,
color: 'black'
}
let circle2 = {
x: 400,
y: 100,
radius: 30,
color: 'blue'
}
function drawCircle() {
ctx.beginPath();
ctx.arc(circle.x, circle.y, circle.radius, 0, Math.PI * 2);
ctx.fillStyle = circle.color;
ctx.fill();
ctx.closePath();
}
function drawCircle2() {
ctx.beginPath();
ctx.arc(circle2.x, circle2.y, circle2.radius, 0, Math.PI * 2);
ctx.fillStyle = circle2.color;
ctx.fill();
}
function update() {
drawCircle();
drawCircle2();
}
function animate() {
ctx.clearRect(0, 0, ctx.width, ctx.height);
requestAnimationFrame(animate);
update();
ctx.flush();
}
animate();
ctx.stroke();
ctx.flush();
};
return (
<GLView style={{ flex: 1 }} onContextCreate={_onGLContextCreate} />
);
}
export { home };
Here is what logs show:
outside 330
inside 330
outside 331
outside 332
outside 333
outside 334
outside 335
outside 336
outside 337
Does anybody know why is being read once in canvas and what could be the solution to increment it as in ouside in canvas function?
I don't know exactly what is the cause, but I found issues in the architecture, and when I fixed them it worked.
TL;DR see result here https://snack.expo.dev/#dozsolti/expogl-ball
You don't need to rerender the component because you update only the canvas so the useState will be swapped with useRef. Also, you probably meant to update the counter every second so for that I changed useTimeout with useInterval. (The useTimeout worked only because it was in a useEffect with the counter dependency which was updated, sort of like calling himself. The correct way was to use a useEffect when the component was loaded and a setInterval)
After that you needed to swap counter with counter.current
Keep in mind that the _onGLContextCreate is running only once so the circle and circle2 objects aren't changing. That's we I changed changed the x value in the update
Besides those, everything looks fine, I guess you optimize the code a little bit, like create a single DrawCircle function that takes x,y as parameters, and so on.
I'm upgrading my app to VueJS 3. I read that you could keep the same components. But I have an error in the console now, although I didn't change anything. Here is my component:
<template>
<v-container>
<div
#click="onClick"
#mousemove="onMouseMove"
id="menu3D"
style="background-color: transparent; position: fixed; left: 20px; width:15%; height:100%;">
</div>
<v-row class="text-center">
<v-col
class="mb-5"
cols="12"
>
<h2 class="headline font-weight-bold mb-3">
Accueil
</h2>
<v-row justify="center">
<p>
Client: {{ JSON.stringify(client)}}
</p>
<p>
Mouse: {{ JSON.stringify(mouse)}}
</p>
<p>
Container: {{ JSON.stringify(container)}}
</p>
</v-row>
</v-col>
</v-row>
</v-container>
</template>
<script>
import * as Three from 'three';
export default {
name: 'Accueil',
mounted() {
this.init();
},
methods: {
init() {
this.createScene();
this.createCamera();
this.userData.formes.forEach((x) => this.createShape(x));
this.addSpotlight(16777215);
this.addAmbientLight();
this.animate();
window.addEventListener('resize', this.onResize);
},
onResize() {
const container = document.getElementById('menu3D');
this.renderer.setSize(container.clientWidth, container.clientHeight);
this.camera.aspect = container.clientWidth / container.clientHeight;
this.camera.updateProjectionMatrix();
},
createScene() {
this.renderer = new Three.WebGLRenderer({
antialias: true,
alpha: true,
});
const container = document.getElementById('menu3D');
this.renderer.setSize(container.clientWidth, container.clientHeight);
this.renderer.setPixelRatio(window.devicePixelRatio);
this.renderer.setClearColor(0xffffff, 0);
container.appendChild(this.renderer.domElement);
},
createCamera() {
const container = document.getElementById('menu3D');
this.camera = new Three.PerspectiveCamera(50,
container.clientWidth / container.clientHeight, 0.01, 1000);
this.camera.position.set(0, 5, 20);
this.camera.zoom = 1;
},
createShape(shape) {
const material = new Three.MeshStandardMaterial({
color: '#0000ff',
roughness: 1,
metalness: 0.5,
emissive: 0,
depthFunc: 3,
depthTest: true,
depthWrite: true,
stencilWrite: false,
stencilWriteMask: 255,
stencilFunc: 519,
stencilRef: 0,
stencilFuncMask: 255,
stencilFail: 7680,
stencilZFail: 7680,
stencilZPass: 7680,
});
switch (shape.nom) {
case 'Box': {
this.geometry = new Three.BoxBufferGeometry(1.8, 1.8, 1.8);
break;
}
case 'Sphere': {
this.geometry = new Three.SphereBufferGeometry(1, 8, 6, 0, 6.283185, 0, 3.141593);
break;
}
case 'Dodecahedron': {
this.geometry = new Three.DodecahedronBufferGeometry(1.2, 0);
break;
}
case 'Icosahedron': {
this.geometry = new Three.IcosahedronBufferGeometry(1.5, 0);
break;
}
default: {
return false;
}
}
this.mesh = new Three.Mesh(this.geometry, material);
this.mesh.name = shape.nom;
this.mesh.userData = shape.userData;
this.mesh.receiveShadow = true;
this.mesh.castShadow = true;
this.mesh.position.set(0, shape.userData.position.y, 0);
this.scene.add(this.mesh);
return true;
},
addSpotlight(color) {
const light = new Three.SpotLight(color, 2, 1000);
light.position.set(0, 0, 30);
this.scene.add(light);
},
addAmbientLight() {
const light = new Three.AmbientLight('#fff', 0.5);
this.scene.add(light);
},
verifForme(e) {
const t = this;
const elt = t.scene.getObjectByName(e);
t.intersects = t.raycaster.intersectObject(elt);
if (t.intersects.length !== 0) {
// s'il ne figure pas dans le tableau, on le met en premier
if (t.userData.souris.indexOf(e) < 0) {
t.userData.souris.unshift(e);
console.log(`${t.userData.souris[0]} survolé!`);
}
if (t.userData.souris[0] === e) {
const obj = t.intersects[0].object;
obj.material.color.set(`#${elt.userData.couleurs[1]}`);
obj.scale.set(obj.scale.x < 1.4
? obj.scale.x + t.VITESSE_ZOOM
: obj.scale.x, obj.scale.y < 1.4
? obj.scale.y + t.VITESSE_ZOOM
: obj.scale.y, obj.scale.z < 1.4
? obj.scale.z + t.VITESSE_ZOOM
: obj.scale.z);
obj.rotation.y += t.VITESSE_ROTATION / t.RALENTISSEMENT;
t.replacer(obj, obj.userData.position.y + obj.userData.decalage);
} else {
t.retrecir(e, elt);
}
} else {
if (t.userData.souris.indexOf(e) >= 0) {
t.userData.souris = t.userData.souris.filter((forme) => forme !== e);
}
t.retrecir(e, elt);
}
},
onClick(event) {
event.preventDefault();
if (this.userData.souris.length > 0) {
console.log(`${this.userData.souris[0]} cliqué!`);
} else {
console.log('clic dans le vide!');
}
},
onMouseMove(event) {
const container = document.getElementById('menu3D');
this.mouse.x = (event.offsetX / container.clientWidth) * 2 - 1;
this.mouse.y = -(event.offsetY / container.clientHeight) * 2 + 1;
this.client.clientX = event.clientX;
this.client.clientY = event.clientY;
this.container.width = container.clientWidth;
this.container.height = container.clientHeight;
// console.log(JSON.stringify(this.mouse))
},
replacer(e, py) {
// la ligne suivante est pour éviter les tremblements
if (Math.abs(e.position.y - py) < 0.05) { return true; }
let rhesus = 10 * this.VITESSE_DEPLACEMENT;
if (this.userData.souris[0] !== e.name) { rhesus *= 3; }
// console.log(e.name+': '+this.userData.souris[0]+' - '+rhesus)
if (e.position.y > py) { rhesus = -1; }
e.position.set(0, Math.trunc(10 * e.position.y + rhesus) / 10, 0);
return true;
},
retrecir(n, e) {
// on vérifie si le truc cliqué est dessus
let dec = 0;
const elt = this;
if ((elt.userData.souris.length > 0)
&& (elt.userData.formes.map((x) => x.nom).indexOf(n)
< elt.userData.formes.map((x) => x.nom).indexOf(elt.userData.souris[0]))) {
dec = Math.trunc(10
* e.parent.getObjectByName(elt.userData.souris[0]).userData.decalage
* 2.1) / 10;
}
e.material.color.set(`#${e.userData.couleurs[0]}`);
e.rotation.y += elt.VITESSE_ROTATION;
e.scale.set(e.scale.x > 1
? e.scale.x - elt.VITESSE_ZOOM : e.scale.x,
e.scale.y > 1
? e.scale.y - elt.VITESSE_ZOOM : e.scale.y,
e.scale.z > 1
? e.scale.z - elt.VITESSE_ZOOM : e.scale.z);
const newY = e.userData.position.y + dec;
if (e.position.y !== newY) {
elt.replacer(e, newY);
}
},
animate() {
const elt = this;
requestAnimationFrame(this.animate);
this.raycaster.setFromCamera(this.mouse, this.camera);
this.userData.formes.map((x) => x.nom).forEach((x) => elt.verifForme(x));
if (this.userData.souris.length > 0) {
document.body.style.cursor = 'pointer';
} else { document.body.style.cursor = 'default'; }
this.camera.updateProjectionMatrix();
this.renderer.render(this.scene, this.camera);
},
},
data: () => ({
container: { height: 0, width: 0 },
client: { clientX: 0, clientY: 0 },
scene: new Three.Scene(),
camera: null,
renderer: Three.WebGLRenderer,
mesh: new Three.Mesh(),
factor: 0,
mouse: new Three.Vector2(1, 1),
raycaster: new Three.Raycaster(),
intersects: [],
VITESSE_ROTATION: 0.05,
VITESSE_DEPLACEMENT: 0.1,
VITESSE_ZOOM: 0.05,
RALENTISSEMENT: 3,
userData: {
souris: [],
formes: [
{
nom: 'Box',
userData: {
position: {
x: 0,
y: 7.8,
z: 0,
},
couleurs: [
'aaaaaa',
'095256',
],
decalage: 0.5,
},
},
{
nom: 'Icosahedron',
userData: {
position: {
x: 0,
y: 5.5,
z: 0,
},
couleurs: [
'aaaaaa',
'087F8C',
],
decalage: 0.5,
},
},
{
nom: 'Dodecahedron',
userData: {
position: {
x: 0,
y: 3.1,
z: 0,
},
couleurs: [
'aaaaaa',
'5AAA95',
],
decalage: 0.4,
},
},
{
nom: 'Sphere',
userData: {
position: {
x: 0,
y: 1,
z: 0,
},
couleurs: [
'aaaaaa',
'86A873',
],
decalage: 0.2,
},
},
],
},
}),
};
</script>
And here is the error I have in the console with VueJS 3:
three.module.js?5a89:24471 Uncaught TypeError:
'get' on proxy: property 'modelViewMatrix' is a read-only and
non-configurable data property on the proxy target but the proxy did not
return its actual value (expected '#<Matrix4>' but got '[object Object]')
at renderObject (three.module.js?5a89:24471)
at renderObjects (three.module.js?5a89:24458)
at Proxy.WebGLRenderer.render (three.module.js?5a89:24258)
at animate (HelloWorld.vue?fdab:192)
If anyone has got a clue, thanks in advance...
It worked with Vue 2
Reason it worked fine with Vue 2 lies in the fact Vue 2 is using different reactivity system based on Object.defineProperty API.
The same API is used by THREE.js a lot to add some non-writable and non-configurable properties to it's data structures
When object with such property was passed to Vue (by declaring it inside data for example), Vue just skipped such property resulting in stored value/object being non-reactive (as Vue could not detect property access while rendering the component template)
Vue 3 proxies
Vue 3 is using new reactivity system base on ES6 proxies.
This is pretty new and even that a lot of effort has been put into developing and testing it, issues like this will arise as people start migrating (And I completely agree with #Serg - Vue 3 is still new and unless you have skill and time to "live on the edge" you should wait a bit before migrating from Vue 2)
This new reactivity system doesn't play well with non-writable non-configurable properties on objects - you can find minimal reproducible example in this sandbox
Imho it is a bug and is reported to Vue#next repo
sandbox uses composition API but that doesn't matter as using reactive() is the same as declaring your variables inside data() function (Vue just do it automatically for you)
Workarounds
As said before, problem is in reactivity system. I'm not an expert on THREE.js but from what I know it doesn't make much sense to put the THREE data structures into Vue reactivity system - all point of reactivity is to detect data changes and re-render template when needed. THREE has its own rendering system and is usually using single <canvas> HTML element so it makes no sense to trigger Vue re-render on THREE data structures change...
There are multiple ways to opt-out from Vue reactivity:
Use Object.freeze() on your objects. Not very useful in this case but good to know
Do not declare your variables in data() and assign the values in created()/mounted() hook (example bellow). You can assign them into component itself (this) if you need to access them in multiple methods or as a local variables (const/let) whenf you don't need it
When using Composition API, do not use reactive() on THREE data structures
NOTE: Even if they admit it is a bug, only way of fixing it is to leave such property and object it holds non-reactive (not putting Proxy around that object) so result will be same as opting-out of reactivity completely. But using this workaround also gives you faster and less memory hungry app as all the reactivity is not really that cheap
Example - creating non-reactive component properties
export default {
data() {
return {
};
},
mounted() {
this.init();
},
methods: {
init() {
this.scene = new THREE.Scene();
this.camera = new THREE.OrthographicCamera(...);
this.renderer = new THREE.WebGLRenderer({ ... })
this.geometry = new THREE.PlaneBufferGeometry( );
const material = new THREE.MeshBasicMaterial({ color: 0xff0000 });
this.plane = new THREE.Mesh(this.geometry, material);
this.scene.add(this.plane);
this.renderer.render(this.scene, this.camera);
},
}
toRaw(vue3) - At this time, you can feel his strength !
You can use this method, to solve a series of these problems
If mesh/xxx is a ref variable
scene.add(toRaw(mesh.value))
renderer.value.render(toRaw(scene.value), camera.value);
I am using threejs + vue3 + pinia. Pinia was wrapping objects in Proxy too, but I need to pass 3d object to it sometimes (inside if other model). So I had a model like:
class SomeModel {
otherProp: 'some value',
graphicObject: new THREE.Object3D(),
}
The way I fixed this issue is by changing graphicObject prop to a function, that return 3d object, that was saved in other variable. It looks like this:
class SomeModel {
otherProp: 'some value',
constructor(graphicObject) {
this.graphicObject = () => graphicObject,
}
}
new SomeModel(new THREE.Object3D());
This way 3d object is hidden from Vue at all time, if you dont pass this object directly to any reactive variable. And the way you access it in other methods is by just calling this function like in example:
<script setup>
import { ref } from 'vue';
// You may just call constructor inside new SomeModel() if you want.
const graphicObject = new THREE.Object3D();
const someModel = ref(new SomeModel(graphicObject));
function changePosition(x, y, z) {
// should not emit errors, because 3d object is not reactive -
// it's just a value, returned from function.
someModel.value.graphicObject().position.set(x, y, z);
}
</script>
I am using React-native-reanimated and react-native-gesture-handler,
I need to trigger a function after animation done, This is my my code:
let a = new Value(1);
let onStateChange = event([
{
nativeEvent: ({state}) =>
block([
cond(
eq(state, State.END),
set(a, runTiming(new Clock(), 1, 0)),
),
]),
},
]);
return <TapGestureHandler onHandlerStateChange={onStateChange}>
I need something like this :
onStateChange = event => {
if (event.nativeEvent.state === State.END) {
alert("I'm being pressed");
}
return block([
cond(
eq(event.nativeEvent.state, State.END),
set(a, runTiming(new Clock(), 1, 0)),
),
]);
},
But not works. :/
There is a call method to handle back from native section to JS.
You can use the call method once the animation is finished (state.END).
This example is for normal animation but the implementation for your case should be similar, notice that when you use call you can pass back parameters to the function you want to use.
function runTiming({ clock, value, dest, afterAnimationActions }) {
const state = {
finished: new Value(0),
position: value,
time: new Value(0),
frameTime: new Value(0),
};
const config = {
duration: ANIMATION_DURATION,
toValue: dest,
easing: Easing.inOut(Easing.ease),
};
return block([
cond(clockRunning(clock), 0, [
set(state.finished, 0),
set(state.time, 0),
set(state.position, value),
set(state.frameTime, 0),
set(config.toValue, dest),
startClock(clock),
]),
timing(clock, state, config),
cond(state.finished, [
stopClock(clock),
call([dest], (d) => afterAnimationActions(d)), // <-- Add this
]),
state.position,
]);
}
With that, you can call runTiming like this:
transY = runTiming({
clock: this.clock,
value: this.from,
dest: this.toValue,
afterAnimationActions: this.afterAnimationActions,
});
And the afterAnimationActions should look like this:
afterAnimationActions = ([dest]) => {
// `dest` is the param we passed using `call`
// The function logic here
};
I have a page with multiple similar sections with the class ".imgPanel" that I would like to apply the same animation to each section every time the page scrolls into view.
I don't want to create a new scene for each animation as they are all the same. I know there is a way to create a variable scene (i hope that is the correct terminology), and I am hoping someone can help.
Does anyone know how I would adjust the code below to make that happen. I am sure if I get one example of it with my code below I will be able to understand and use this technique again.
Thanks in advance. Adam.
function scrollMagic() {
if (!Modernizr.touch) {
var controller = new ScrollMagic.Controller({
duration: "200%",
});
var panelAnim = new TimelineMax();
panelAnim
.from($('.imgPanel'), 1.4, {
y: '-50px',
autoAlpha: 0,
ease: Power1.easeOut
}, '-=0.2')
var introScene = new ScrollMagic.Scene({
duration: "100%",
})
.setTween(panelAnim)
.addTo(controller);
}
}
I figured it out after watching Ihatetomatoes video
function scrollMagic() {
if (!Modernizr.touch) {
var controller = new ScrollMagic.Controller({
duration: "100%",
});
$('.imgPanel').each(function(){
var tween = TweenMax.from($(this), 1.4, { y: '-50px', autoAlpha: 0,
ease: Power1.easeOut }, '-=0.2');
var scene = new ScrollMagic.Scene ({
duration: "100%",
offset: -300, // offset trigger position by 100px
triggerElement: this
})
.setTween(tween)
.addTo(controller);
});
}
}
I have a set of buttons that when clicked show a sort of pop up and some simple animations. Each pop up contain the same animations for most of the content. Each pop up also has its own sets of animations so I have done the following to get this to work correctly.
$gridTitles.click(function() {
const tl = new TimelineMax();
const $pop = $(this).next('.grid__pop');
const $chars = $pop.find(".grid__pop-title span");
const $items = $pop.find(".grid__pop-list li");
const func = $(this).data("graphic-function");
tl.set($pop, {
autoAlpha: 0,
display: 'block',
scale: .5
})
.to($pop, 1, {
autoAlpha: 1,
scale: 1,
ease: Power2.easeInOut
})
.staggerFrom($chars, 0.01, {
autoAlpha: 0,
ease: Power2.easeIn
}, 0.1)
.add(graphicAnimation[func])
.staggerFrom($items, 0.8, {
autoAlpha: 0,
rotationX: 90,
ease: Power2.easeOut
}, .8);
return tl;
});
This runs my pop up code and also using the .add function I call another function that runs the specific animation for the pop up based on a data attribute matching the name of a function in an object.
const graphicAnimation = {
graphicServer: function() {
const tl = new TimelineMax();
const $server = $(".graphic-server__server");
const $one = $(".graphic-server__one");
const $two = $(".graphic-server__two");
const $three = $(".graphic-server__three");
return tl.to($server, 1, {
autoAlpha: 1,
xPercent: "0",
ease: Power2.easeInOut
})
.to($one, 1, {
autoAlpha: 1,
xPercent: "0",
ease: Power2.easeInOut
})
.to($two, 1, {
autoAlpha: 1,
xPercent: "0",
ease: Power2.easeInOut
})
.to($three, 1, {
autoAlpha: 1,
xPercent: "0",
ease: Power2.easeInOut
})
},
// more functions
}
This works great depending on which button is clicked the correct function is called in the object and my animations run. The problem now is that some of these animations are looping and when I close the popup I can't pause them.
Using something like the following I tried to accomplish this
$gridCloses.click(function() {
const tl = new TimelineMax();
const $pop = $(this).parents(".grid__pop");
const func = $(this).parents('.grid__pop').siblings('.grid__title').data("graphic-function");
graphicAnimation[func].pause();
return tl.to($pop, 1, {
autoAlpha: 0,
scale: .5,
display: 'none'
});
});
But calling graphicAnimation[func].pause(); isn't going to work as pause() is a function on the returned timeline from that function. How can I access the current running function and pause / kill it.
So I thought this out and just need to store the timelines in their own object as well
const graphicTimeLines = {
graphicServer : new TimelineMax(),
graphicType: new TimelineMax()
}
so with that in my closing action I can do something like the following to completely reset my loops and have the timeline be available to be called again.
graphicTimeLines[func].pause().progress(0).kill();
graphicTimeLines[func] = new TimelineMax();