I have trouble finding the reason for making the component re-render when scrolling down.
I've tried to build scenes with particles using shader and FBO techniques.
I used useRef and useMemo to prevent re-rendering instead of useState. I tried to load 3d models in this component using useGLTF, but not worked.
Re-render happens when scrolling down.
What seems to be the problem?
Here is my code.
let opacity;
const size = 80;
const FboParticles = ({ models }) => {
const { viewport } = useThree();
const scroll = useScroll();
/**
* Particles options
*/
// This reference gives us direct access to our points
const points = useRef();
const simulationMaterialRef = useRef();
// Create a camera and a scene for our FBO
// Create a simple square geometry with custom uv and positions attributes
const [scene, camera, positions, uvs] = useMemo(() => {
return [
new Scene(),
new OrthographicCamera(-1, 1, 1, -1, 1 / Math.pow(2, 53), 1),
new Float32Array([
-1, -1, 0, 1, -1, 0, 1, 1, 0, -1, -1, 0, 1, 1, 0, -1, 1, 0,
]),
new Float32Array([0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0]),
];
}, []);
// Create our FBO render target
const renderTarget = useFBO(size, size, {
minFilter: NearestFilter,
magFilter: NearestFilter,
format: RGBAFormat,
stencilBuffer: false,
type: FloatType,
});
// Generate a "buffer" of vertex of size "size" with normalized coordinates
const particlesPosition = useMemo(() => {
const length = size * size;
const particles = new Float32Array(length * 3);
for (let i = 0; i < length; i++) {
let i3 = i * 3;
particles[i3 + 0] = (i % size) / size;
particles[i3 + 1] = i / size / size;
}
return particles;
}, []);
const uniforms = useMemo(
() => ({
uPositions: {
value: null,
},
uMobile: {
value: 1,
},
uPixelRatio: {
value: Math.min(window.devicePixelRatio, 2),
},
vColor: {
value: 0,
},
defaultTime: {
value: 0,
},
uOpacity: {
value: 0,
},
uColorTrigger: {
value: 0,
},
}),
[]
);
useLayoutEffect(() => {
if (viewport.width < 4.8) {
points.current.material.uniforms.uMobile.value = 0.1;
}
}, []);
// FBO useFrame
useFrame(({ gl, clock }) => {
gl.setRenderTarget(renderTarget);
gl.clear();
gl.render(scene, camera);
gl.setRenderTarget(null);
points.current.material.uniforms.uPositions.value = renderTarget.texture;
points.current.material.uniforms.defaultTime.value = clock.elapsedTime;
simulationMaterialRef.current.uniforms.uTime.value = clock.elapsedTime;
});
const [scales, colors] = useMemo(() => {
const length = size * size * 3;
const color = new Color();
const sca = [];
const cols = [];
let q = ["white", "white", 0x2675ad, 0x0b5394, 0x0b9490];
const range = viewport.width < 4.8 ? 20 : 40;
for (let i = 0; i < length; i++) {
const i3 = i * 3;
// color
color.set(q[randInt(0, 4)]);
cols[i3 + 0] = color.r;
cols[i3 + 1] = color.g;
cols[i3 + 2] = color.b;
// particles scale
sca[i] = Math.random() * range;
}
return [new Float32Array(sca), new Float32Array(cols)];
}, []);
/**
* mouse event
*/
const hoveredRef = useRef();
const planeGeo = useMemo(() => {
return new PlaneGeometry(viewport.width, viewport.height, 1, 1);
}, []);
/**
* scroll
*/
// scroll animation
useFrame(({ mouse }) => {
const x = (mouse.x * viewport.width) / 2;
const y = (mouse.y * viewport.height) / 2;
if (viewport.width > 6.7) {
if (hoveredRef.current) {
simulationMaterialRef.current.uniforms.uMouse.value = new Vector2(x, y);
simulationMaterialRef.current.uniforms.uMouseTrigger.value = 1;
} else simulationMaterialRef.current.uniforms.uMouseTrigger.value = 0;
}
const aRange = scroll.range(0.0, 1 / 12);
const bRange = scroll.range(0.7 / 12, 1 / 12);
simulationMaterialRef.current.uniforms.scrollTriggerA.value = aRange;
const cRange = scroll.range(1.7 / 12, 1 / 12);
const dRange = scroll.range(3.6 / 12, 1 / 12);
const c = scroll.visible(1.7 / 12, 1 / 12);
const d = scroll.visible(2.7 / 12, 1.9 / 12);
simulationMaterialRef.current.uniforms.scrollTriggerB.value = cRange;
const e = scroll.visible(4.8 / 12, 2 / 12);
const eRange = scroll.range(4.8 / 12, 1 / 12);
simulationMaterialRef.current.uniforms.scrollTriggerC.value = eRange;
const f = scroll.visible(6.8 / 12, 1 / 12);
const g = scroll.visible(7.6 / 12, 1 / 12);
const fRange = scroll.range(6.8 / 12, 1 / 12);
const gRange = scroll.range(7.6 / 12, 1 / 12);
simulationMaterialRef.current.uniforms.scrollTriggerD.value = fRange;
simulationMaterialRef.current.uniforms.scrollTriggerE.value = gRange;
const h = scroll.visible(9.6 / 12, 2.4 / 12);
const hRange = scroll.range(9.6 / 12, 1 / 12);
simulationMaterialRef.current.uniforms.scrollTriggerF.value = hRange;
points.current.material.uniforms.uColorTrigger.value = hRange;
const iRange = scroll.range(10.6 / 12, 1.4 / 12);
simulationMaterialRef.current.uniforms.scrollTriggerG.value = iRange;
// opacity
opacity = 1 - bRange;
c && (opacity = cRange);
d && (opacity = 1 - dRange);
e && (opacity = eRange);
f && (opacity = 1 - fRange);
g && (opacity = fRange);
h && (opacity = hRange);
points.current.material.uniforms.uOpacity.value = opacity;
});
return (
<>
<mesh
position={[0, 0, 0]}
onPointerOver={(e) => (
e.stopPropagation(), (hoveredRef.current = true)
)}
onPointerOut={(e) => (hoveredRef.current = false)}
visible={false}
geometry={planeGeo}
/>
{/* Render off-screen our simulation material and square geometry */}
{createPortal(
<mesh>
<simulationMaterial
ref={simulationMaterialRef}
args={[size, viewport, models]}
/>
<bufferGeometry>
<bufferAttribute
attach="attributes-position"
count={positions.length / 3}
array={positions}
itemSize={3}
/>
<bufferAttribute
attach="attributes-uv"
count={uvs.length / 2}
array={uvs}
itemSize={2}
/>
</bufferGeometry>
</mesh>,
scene
)}
<points ref={points}>
<bufferGeometry>
<bufferAttribute
attach="attributes-position"
count={particlesPosition.length / 3}
array={particlesPosition}
itemSize={3}
/>
<bufferAttribute
attach="attributes-color"
count={colors.length / 3}
array={colors}
itemSize={3}
/>
<bufferAttribute
attach="attributes-aScale"
count={scales.length}
array={scales}
itemSize={1}
/>
</bufferGeometry>
<shaderMaterial
blending={AdditiveBlending}
depthWrite={false}
// transparent={true}
fragmentShader={fragmentShader}
vertexShader={vertexShader}
uniforms={uniforms}
/>
</points>
</>
);
};
export default FboParticles;
You just need use states, if you want re-render your component use states like this:
const App = () => {
const [,forceUpdate] = useState({update: true});
return (
<button onClick{() => forceUpdate({update:true})}>re render this component</button>
)
}
use this forceUpdate anywhere, but focus it should take object if you want to write int on that you should set different value so yes use random int on that
forceUpdate(Math.random())
Related
I am using THREE.js in react.
I want to fill 500 number of 1X1X1 box geometry in a 200x200x200 area randomly.
The following code shows how to get the position.xyz with the range from -100 to 100.
However, how to get the position that all 1X1X1 box geometry could not touch each other (no overlapping)?
I have no idea.
function Geometry() {
const geo_num = 500;
const box_length = 200;
const geo_position = useMemo(() => {
const array = []
for (let i = 0; i < geo_num; i++) {
const xdirection = Math.round(Math.random()) * 2 - 1;
const ydirection = Math.round(Math.random()) * 2 - 1;
const zdirection = Math.round(Math.random()) * 2 - 1;
const xpos = Math.random() * box_length * xdirection * 0.5;
const ypos = Math.random() * box_length * ydirection * 0.5;
const zpos = Math.random() * box_length * zdirection * 0.5;
array.push(new THREE.Vector3(xpos, ypos, zpos))
}
return array
})
return (
<>
{
geo_position.map((element, index) => (
<mesh position={element} key={index} >
<boxGeometry args={[1, 1, 1]} />
</mesh>
))
}
</>
)
}
A very rough concept of how you can do it, using an array and its .includes() method:
body{
overflow: hidden;
margin: 0;
}
<script type="module">
import * as THREE from "https://cdn.skypack.dev/three#0.136.0";
import {OrbitControls} from "https://cdn.skypack.dev/three#0.136.0/examples/jsm/controls/OrbitControls";
let scene = new THREE.Scene();
let camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 1, 1000);
camera.position.set(50, 50, 250);
let renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
window.addEventListener("resize", event => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
let contols = new OrbitControls(camera, renderer.domElement);
let boxCount = 500;
let range = [-100, 100];
let v3Array = [];
let counter = 0;
let v3 = new THREE.Vector3();
while(counter < boxCount){
let v3 = [
THREE.MathUtils.randInt(range[0], range[1]).toFixed(0),
THREE.MathUtils.randInt(range[0], range[1]).toFixed(0),
THREE.MathUtils.randInt(range[0], range[1]).toFixed(0),
].join("|");
if (!v3Array.includes(v3)){
v3Array.push(v3);
counter++
}
}
v3Array.map( p => {
let o = new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshBasicMaterial({color: Math.random() * 0xffffff, wireframe: true}));
let pos = p.split("|").map(c => {return parseInt(c)});
o.position.set(pos[0], pos[1], pos[2]);
scene.add(o);
});
let boxHelper = new THREE.Box3Helper(new THREE.Box3(new THREE.Vector3().setScalar(range[0] - 0.5), new THREE.Vector3().setScalar(range[1] + 0.5)), "yellow");
scene.add(boxHelper);
renderer.setAnimationLoop( () => {
renderer.render(scene, camera);
})
</script>
I have a component that draws a sequence that it gets from my redux store:
const sequence = useSelector(state => state.sequence)
My useEffect call that does the canvas draw is being triggered whenever ‘sequence’ updates:
useEffect(() => {
store.subscribe(() => {
console.log(`sequence-canvas: store state is now ${JSON.stringify(store.getState())}`)
drawSequenceCanvas(sequence)
});
}, [sequence])
But for some reason the ‘sequence’ populated by useSelector is behind my store state. If I draw sequence instead of the store state it draws the correct state:
useEffect(() => {
store.subscribe(() => {
console.log(`sequence-canvas: store state is now ${JSON.stringify(store.getState())}`)
drawSequenceCanvas(store.getState().sequence)
});
}, [sequence])
I don't get why I need to use the store state here instead of the selector. What am I not understanding?
Here is the full module:
import React, { useEffect, useRef } from 'react';
import store from '../app/store'
import { useSelector } from "react-redux";
import * as PIXI from 'pixi.js'
import SongPlayer from "../player/song-player";
Math.minmax = (value, min, max) => Math.min(Math.max(value, min), max);
var pixiapp;
var container;
var parentContainer;
var renderer;
var height;
var width;
var mouseIsDown = false;
var isDraggingVertical = false
var isDraggingHorizontal = false
const dragThresholdVertical = 10
const dragThresholdHorizontal = 10
const grey = 0x404040
const darkgrey = 0x707070
const green = 0x10a010
const brightRed = 0xdb5660
const brightGreen = 0x56db60
const blue = 0x0000ff
const borderColor = 0x1e1e1e
const envelopeLineColor = 0xff0000
const backgroundColor = 0x28282b
const darkgreen = 0x107010
const red = 0xff0000
const white = 0xffffff;
const colorVelocityBar = 0x5e5f68;
const middleNote = 60;
const minNote = middleNote - 24;
const maxNote = middleNote + 24;
const borderWidth = 10;
const bottomBorderHeight = 20;
const topBorderHeight = 20;
const gapWidth = 4;
const barWidth = 50;
const numSteps = 16;
const lineThickness = 0.005;
const DragTargets = {
None: 0,
NoteBox: 1,
OctaveBox: 2,
VelocityBox: 3,
}
const DragDirections = {
None: 0,
Vertical: 1,
Horizontal: 2,
}
var dragTarget
var dragDirection
var dragStepNum = -1
var dragStartPos = { x: -1, y: -1, }
const noteBarTextStyle = new PIXI.TextStyle({
align: "center",
fill: "#000000",
fontFamily: "Helvetica",
fontSize: 12
});
const turnAKnobTextStyle = new PIXI.TextStyle({
align: "center",
fill: "#000000",
fontFamily: "Helvetica",
fontSize: 36,
});
const noteName = [
"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
]
function getControllerValueTop(value, minValue, maxValue) {
// return bottomBorderHeight + (value - minvalue) / (maxValue - minvalue) * (height - bottomBorderHeight - topBorderHeight) / 127
return height - bottomBorderHeight - (value - minValue) / (maxValue - minValue) * (height - bottomBorderHeight - topBorderHeight);
}
function getTimeX(sequence, time) {
const maxTime = sequence.steps.length * sequence.tempo / 60 / sequence.division
// console.log(`maxTime ${maxTime} sequence.steps.length ${sequence.steps.length} sequence.tempo ${sequence.tempo} sequence.division ${sequence.division}`)
const timex = borderWidth + time / maxTime * (width - borderWidth - borderWidth)
// console.log(`time ${time} timex ${timex} borderWidth ${borderWidth} maxTime ${maxTime} width ${width}`)
return timex
}
// function getXTime(sequence, x) {
// const maxTime = sequence.steps * sequence.tempo / 60 / sequence.division
// return borderWidth + x / (width - borderWidth - borderWidth) * maxTime
// }
function getNoteTop(note) {
return height - bottomBorderHeight - (note + 12 - minNote) / (maxNote - minNote + 12) * (height - bottomBorderHeight - topBorderHeight);
}
function getNoteBottom(note) {
return getNoteTop(note - 12) + lineThickness;
// return bottomBorderHeight - lineThickness + (note - minNote) / (maxNote - minNote + 12) * (1 - bottomBorderHeight - topBorderHeight);
}
function getNoteHeight() {
return (height - bottomBorderHeight - topBorderHeight) * 12 / (maxNote - minNote + 12);
}
function getStepLeft(sequence, stepNum) {
var stride = getStepStride(sequence)
// console.log(`getStepLeft: borderWidth ${borderWidth} num steps ${sequence.steps.length} stride ${stride}`)
return borderWidth + stepNum * stride;
}
function getStepRight(sequence, stepNum) {
var stride = (width - (borderWidth * 2)) / sequence.steps.length;
var step = sequence.steps[stepNum];
return getStepLeft(sequence, stepNum) + stride * step.gateLength - lineThickness;
}
function getStepStride(sequence) {
var stride = (width - (borderWidth * 2)) / sequence.steps.length;
// console.log(`getStepStride: ${sequence.numSteps} -> ${stride}`)
return stride
}
function getStepWidth(sequence, stepNum) {
var stride = getStepStride(sequence);
var step = sequence.steps[stepNum];
return stride * step.gateLength;
}
function drawSequenceCanvas(sequence) {
const sequencePlayer = SongPlayer.getSequencePlayer(sequence._id)
// console.log(`draw: sequence player is ${JSON.stringify(sequencePlayer)}`)
const stepnum = sequencePlayer ? sequencePlayer.stepNum : -1;
// console.log(`stepnum ${stepnum}`)
if (container) {
container.destroy({ children: true })
}
container = new PIXI.Container();
parentContainer.addChild(container);
// Background
var bar = new PIXI.Graphics();
// bar.lineStyle(2, 0x00FFFF, 1);
bar.beginFill(backgroundColor, 1);
bar.drawRect(0, 0, width, height);
bar.endFill();
container.addChild(bar);
// Top border
bar = new PIXI.Graphics();
bar.beginFill(borderColor, 1);
bar.drawRect(0, 0, width, topBorderHeight, 16);
bar.endFill();
container.addChild(bar);
// Bottom border
bar = new PIXI.Graphics();
bar.beginFill(borderColor, 1);
bar.drawRect(0, height-bottomBorderHeight, width, bottomBorderHeight);
bar.endFill();
container.addChild(bar);
// Sides
bar = new PIXI.Graphics();
bar.beginFill(borderColor, 1);
bar.drawRect(0, topBorderHeight, borderWidth, height - bottomBorderHeight - topBorderHeight);
bar.drawRect(width - borderWidth, topBorderHeight, borderWidth, height - bottomBorderHeight - topBorderHeight);
bar.endFill();
container.addChild(bar);
// Velocity bars
var stepNum = 0;
var barColor = colorVelocityBar;
sequence.steps.forEach(step => {
const bar = new PIXI.Graphics();
// bar.lineStyle(2, 0xFF00FF, 1);
bar.beginFill(barColor, 1);
var barHeight = height - bottomBorderHeight - getControllerValueTop(step.velocity, 0, 127);
bar.drawRect(getStepLeft(sequence, stepNum), height - bottomBorderHeight - barHeight, getStepWidth(sequence, stepNum), barHeight);
bar.endFill();
container.addChild(bar);
++stepNum;
})
// Horizontal grid lines
const lineColor = borderColor;
var lines = new PIXI.Graphics();
// console.log(`minnote ${minNote} ${getNoteTop(minNote)} ${getNoteTop(minNote - 12)}`)
lines.position.set(borderWidth, getNoteTop(minNote - 12));
lines.lineStyle(2, lineColor, 1)
.moveTo(borderWidth, getNoteTop(minNote - 12))
.lineTo(width - borderWidth, getNoteTop(minNote - 12))
.moveTo(borderWidth, getNoteTop(minNote))
.lineTo(width - borderWidth, getNoteTop(minNote))
.moveTo(borderWidth, getNoteTop(minNote + 12))
.lineTo(width - borderWidth, getNoteTop(minNote + 12))
.moveTo(borderWidth, getNoteTop(minNote + 24))
.lineTo(width - borderWidth, getNoteTop(minNote + 24))
.moveTo(borderWidth, getNoteTop(minNote + 36))
.lineTo(width - borderWidth, getNoteTop(minNote + 36))
.moveTo(borderWidth, getNoteTop(minNote + 48))
.lineTo(width - borderWidth, getNoteTop(minNote + 48))
container.addChild(lines);
// Note bars
stepNum = 0;
console.log(`SequenceCanvas.draw - note bars - ${JSON.stringify(sequence.steps)}`);
sequence.steps.forEach(step => {
const bar = new PIXI.Graphics();
// bar.lineStyle(2, 0xFF00FF, 1);
barColor = sequencePlayer && stepNum === sequencePlayer.stepNum ? brightRed : brightGreen;
bar.beginFill(barColor, 1);
var barHeight = height - bottomBorderHeight - getControllerValueTop(step.velocity, 0, 127);
bar.drawRoundedRect(getStepLeft(sequence, stepNum), getNoteTop(step.note), getStepWidth(sequence, stepNum), getNoteHeight(), 8);
bar.endFill();
container.addChild(bar);
const noteText = new PIXI.Text(noteName[step.note % 12], noteBarTextStyle)
noteText.anchor.set(0.5);
// noteText.width = getStepStride(sequence)
noteText.x = getStepLeft(sequence, stepNum) + getStepWidth(sequence, stepNum) / 2
noteText.y = getNoteTop(step.note) + getNoteHeight() / 2
container.addChild(noteText)
++stepNum;
})
// Bottom bars
stepNum = 0;
barColor = green;
// console.log(`SequenceCanvas.Init ${JSON.stringify(sequence.steps)}`);
sequence.steps.forEach(step => {
const bar = new PIXI.Graphics();
// bar.lineStyle(2, 0xFF00FF, 1);
bar.beginFill(0x707481, 1);
bar.drawRoundedRect(getStepLeft(sequence, stepNum), height - bottomBorderHeight, getStepWidth(sequence, stepNum), bottomBorderHeight, 4);
bar.endFill();
container.addChild(bar);
const noteText = new PIXI.Text((step.note % 12), noteBarTextStyle)
noteText.anchor.set(0.5);
// noteText.width = getStepStride(sequence)
noteText.x = getStepLeft(sequence, stepNum) + getStepStride(sequence) / 2
noteText.y = height - bottomBorderHeight + 10
container.addChild(noteText)
++stepNum;
})
if (sequence.currentEnvelopeId != 'notes') {
DrawEnvelopes(container, sequence)
}
// // Random box for testing
// bar = new PIXI.Graphics();
// bar.lineStyle(2, 0x00FFFF, 1);
// bar.beginFill(0x650A5A, 0.25);
// bar.drawRoundedRect(height, getNoteTop(minNote), 50, getNoteHeight(minNote), 16);
// bar.endFill();
// container.addChild(bar);
}
function DrawEnvelopes(container, sequence) {
if (sequence.currentEnvelopeId) {
console.log(`DrawEnvelopes: ${sequence.currentEnvelopeId}`)
if (sequence.currentEnvelopeId == "notes") {
// don't draw envelopes
} else if (sequence.currentEnvelopeId == "new" || sequence.currentEnvelopeId == "learn") {
console.log(`draw warning`)
const bar = new PIXI.Graphics();
bar.beginFill(white, 0.5);
bar.drawRect(0, 0, width, height);
bar.endFill();
container.addChild(bar);
const turnAKnobText = new PIXI.Text("Turn a knob to map", turnAKnobTextStyle)
turnAKnobText.anchor.set(0.5);
turnAKnobText.width = width / 2
turnAKnobText.x = width / 2
turnAKnobText.y = height / 2
container.addChild(turnAKnobText)
} else {
DrawEnvelope(container, sequence, sequence.currentEnvelopeId)
}
}
}
function DrawEnvelope(container, sequence, envelopeId) {
console.log(`DrawEnvelope: ${envelopeId} ${JSON.stringify(sequence)}`)
const envelope = sequence.envelopes[envelopeId]
if (!envelope) {
return
}
console.log(`DrawEnvelope: ${envelopeId} - envelope.controller ${JSON.stringify(envelope.controller)}`)
if (envelope.controller !== "notes") {
console.log(`DrawEnvelope: ${envelope.controller}`)
if (envelope.controller == null) {
} else {
const points = envelope.points
// console.log(`DrawEnvelope: We have ${envelope.controllers.length} controllers with points ${JSON.stringify(points)}`)
// console.log(`DrawEnvelope: point 0 ${JSON.stringify(points[0])}`)
// Lines
const lineColor = envelopeLineColor;
var lines = new PIXI.Graphics();
lines.position.set(0, 0);
lines.lineStyle(1, lineColor, 1)
var y = getControllerValueTop(points[0].value, 0, 127)
lines.moveTo(0, y)
for (const [pointTime, point] of Object.entries(points)) {
var x = getTimeX(sequence, point.time)
y = getControllerValueTop(point.value, 0, 127)
// console.log(`point ${JSON.stringify(point)} draw line to time ${point.time} value ${point.value} ${x},${y}`)
lines.lineTo(x, y)
}
lines.lineTo(width, y)
// Dots
lines.fillStyle = { color: lineColor, alpha: 0.5, visible: true }
lines.beginFill(lineColor)
for (const [pointTime, point] of Object.entries(points)) {
var x = getTimeX(sequence, point.time)
y = getControllerValueTop(point.value, 0, 127)
// console.log(`point ${JSON.stringify(point)} draw line to time ${point.time} value ${point.value} ${x},${y}`)
lines.drawCircle(x, y, 4)
}
container.addChild(lines);
}
}
}
const handlePointerDown = e => {
const sequence = store.getState().sequence
console.log(`handlePointerDown ${mouseIsDown} ${JSON.stringify(e.data)}`)
mouseIsDown = true;
var x = e.data.global.x
var y = e.data.global.y
dragStartPos = { x: x, y: y, }
dragStepNum = -1
for (var stepNum = 0; stepNum < sequence.steps.length; stepNum++) {
var stepleft = getStepLeft(sequence, stepNum);
if (x > stepleft && x < getStepRight(sequence, stepNum)) {
dragStepNum = stepNum
console.log(`tapped step ${dragStepNum}`)
break
}
}
console.log(`handlePointerDown: ${x},${y} dragStepNum ${dragStepNum} target ${dragTarget}`)
console.log(`handlePointerDown: step ${JSON.stringify(sequence.steps[stepNum])}`)
if (dragStartPos.y > height - bottomBorderHeight) {
dragTarget = DragTargets.NoteBox
} else if (dragStartPos.y < getNoteBottom(sequence.steps[stepNum].note) && dragStartPos.y > getNoteTop(sequence.steps[stepNum].note)) {
dragTarget = DragTargets.OctaveBox
} else if (dragStartPos.y > bottomBorderHeight && dragStartPos.y < height - topBorderHeight) {
console.log(`drag velocity box y = ${dragStartPos.y}`)
dragTarget = DragTargets.VelocityBox
}
console.log(`handlePointerDown: step ${dragStepNum} target ${dragTarget}`)
}
const handlePointerUp = e => {
const sequence = store.getState().sequence
console.log(`handlePointerUp ${mouseIsDown}`)
mouseIsDown = false;
isDraggingVertical = false;
isDraggingHorizontal = false;
}
const handlePointerMove = e => {
const sequence = store.getState().sequence
if (mouseIsDown) {
var x = e.data.global.x
var y = e.data.global.y
if (x !== null && y !== null && x !== Infinity && y !== Infinity) {
// console.log(`drag ${JSON.stringify(e.data)}`)
if (dragTarget === DragTargets.OctaveBox) {
// Detect direction before we start editing
if (!isDraggingVertical && !isDraggingHorizontal) {
if (Math.abs(y - dragStartPos.y) > dragThresholdVertical) {
isDraggingVertical = true;
}
else if (Math.abs(x - dragStartPos.x) > dragThresholdHorizontal) {
isDraggingHorizontal = true;
}
else {
return
}
}
if (isDraggingVertical) {
console.log(`drag note: ${sequence.steps[dragStepNum].note} ${x},${y} `)
const notenum = sequence.steps[dragStepNum].note
if (y < getNoteTop(notenum)) {
console.log(`drag y ${y} < ${getNoteTop(notenum)} getNoteTop note for note ${notenum}, dragStepNum ${dragStepNum}`)
store.dispatch({type: "sequence/stepNote", payload: {stepNum: dragStepNum, note: notenum + 12}})
} else if (y > getNoteBottom(notenum)) {
console.log(`drag y ${y} > ${getNoteBottom(notenum)} getNoteBottom for note ${notenum}, dragStepNum ${dragStepNum}`)
store.dispatch({type: "sequence/stepNote", payload: {stepNum: dragStepNum, note: notenum - 12}})
}
}
else {
var stride = getStepStride(sequence);
// note.gateLength
// x = Math.minmix(x, getStepLeft(sequence, dragStepNum), getStepRight(sequence, dragStepNum))
var newGateLength = (x - getStepLeft(sequence, dragStepNum)) / getStepStride(sequence)
if (x < getStepLeft(sequence, dragStepNum)) {
newGateLength = 0;
} else if (x > getStepLeft(sequence, dragStepNum) + getStepStride(sequence)) {
newGateLength = 1.0
}
store.dispatch({
type: "sequence/stepGateLength",
payload: {stepNum: dragStepNum, gateLength: newGateLength}
})
}
}
else if (dragTarget === DragTargets.NoteBox) {
const notenum = sequence.steps[dragStepNum].note
if (y - dragStartPos.y > bottomBorderHeight) {
store.dispatch({ type: "sequence/stepNote", payload: { stepNum: dragStepNum, note: notenum - 1 } })
dragStartPos.y = y
} else if (y - dragStartPos.y < -bottomBorderHeight) {
store.dispatch({ type: "sequence/stepNote", payload: { stepNum: dragStepNum, note: notenum + 1 } })
dragStartPos.y = y
}
}
else if (dragTarget === DragTargets.VelocityBox) {
const velocity = sequence.steps[dragStepNum].velocity
y = Math.minmax(y, topBorderHeight, height - bottomBorderHeight)
const newVelocity = 127 - (127 * (y - topBorderHeight) / (height - topBorderHeight - bottomBorderHeight))
console.log(`drag velocitybox ${y} => ${newVelocity}`)
store.dispatch({ type: "sequence/stepVelocity", payload: { stepNum: dragStepNum, velocity: newVelocity } })
}
}
}
}
function SequenceCanvas(props) {
console.log(`hi from SequenceCanvas ${JSON.stringify(props)}`)
// The React way to get a dom element is by giving it a ref prop
const canvasRef = useRef(null)
const sequence = useSelector(state => state.sequence)
useEffect( () => {
// console.log(`sequence-canvas: store state is now ${JSON.stringify(store.getState())}`)
width = props.width;
height = props.height;
console.log("new PIXI.Application")
pixiapp = new PIXI.Application({
view: canvasRef.current,
powerPreference: 'high-performance',
backgroundAlpha: false,
resolution: window.devicePixelRatio || 1,
autoDensity: true,
width: width,
height: height
});
renderer = PIXI.autoDetectRenderer();
parentContainer = new PIXI.Container();
parentContainer.interactive = true
parentContainer.on('pointerdown', (e) => { handlePointerDown(e) })
parentContainer.on('pointerup', (e) => { handlePointerUp(e) })
parentContainer.on('mouseupoutside', (e) => { handlePointerUp(e) })
parentContainer.on('pointermove', (e) => { handlePointerMove(e) })
pixiapp.stage.addChild(parentContainer);
drawSequenceCanvas(sequence)
return () => {
parentContainer.removeAllListeners()
}
}, [])
useEffect(() => {
store.subscribe(() => {
console.log(`sequence-canvas: store state is now ${JSON.stringify(store.getState())}`)
drawSequenceCanvas(store.getState().sequence)
});
}, [sequence])
const timerIdRef = useRef(0)
function handleInterval() {
console.log(`hi from sequenceCanvas.IntervalTimer`)
drawSequenceCanvas(sequence)
}
const startIntervalTimer = () => {
console.log(`SequenceCanvas.startIntervalTimer - timerIdRef.current === ${timerIdRef.current}`)
if (timerIdRef.current == 0) {
timerIdRef.current = setInterval(() => {
handleInterval()
})
}
}
const stopIntervalTimer = () => {
console.log(`SequenceCanvas.stopIntervalTimer`)
clearInterval(timerIdRef.current)
timerIdRef.current = 0
}
useEffect(() => {
console.log(`startIntervalTime from useEffect`)
// startIntervalTimer();
return () => clearInterval(timerIdRef.current);
}, []);
return (
<div>
<div>
<canvas {...props} ref={canvasRef}>
Your browser does not support the HTML canvas tag.
</canvas>
</div>
</div>
);
}
// export {SequenceCanvas, drawSequenceCanvas};
export {SequenceCanvas};
Every time that sequence is changed - you create new store subscribe on useEffect hook - more and more. You can make it work like this:
const sequence = useSelector(state => state.sequence);
const sequenceRef = useRef(sequence);
sequenceRef.current = sequence;
useEffect(() => {
return store.subscribe(() => { // <--- return will unsubscribe from store
drawSequenceCanvas(sequenceRef.current); // <---actual link
});
}, []);
or you can do it like this
const sequence = useSelector(state => state.sequence);
useEffect(() => {
return store.subscribe(() => { // <--- return will unsubscribe from store
drawSequenceCanvas(sequence);
});
}, [sequence]);
BUT - useSelector hook is already uses store subscribe - so you can do just like this:
const sequence = useSelector(state => state.sequence);
useEffect(() => {
drawSequenceCanvas(sequence);
}, [sequence]);
// OR with usePrevious hook - https://usehooks.com/usePrevious/
const sequence = useSelector(state => state.sequence);
const prevSequence = usePrevious(sequence);
if (sequence !== prevSequence) {
drawSequenceCanvas(sequence);
}
** Looking for matched item to different color**
I'm currently trying to get color changes of mesh if condition match else keep it default color. but somehow it keeps same color for all mesh object.
I did try to add simple logic with Boolean to toggle for color but that also doesn't work. any advice will be accepted.
function generateMatrix(
mesh: MutableRefObject<InstancedMesh>,
getItemForGenerate: GetItemForGenerateData,
userDefineditems: string[],
xRotationValue: number,
yRotationValue: number
) {
if (getItemForGenerate?.items) {
const tempObject = new THREE.Object3D();
getItemForGenerate.items.forEach((items) => {
items.entities.forEach((item, index) => {
tempObject.position.set(
item.xMapPosition
item.yMapPosition
item.zMapPosition
);
//change color of the object if userdefined item matches the any of the items provided
if (userDefineditems?.includes(item.itemName)) {
tempObject.scale.set(item.length, item.depth , item.height );
// how to change color here if user defined lcoations matches any item on the Matrix
}
tempObject.scale.set(item.length , item.depth , item.height );
tempObject.updateMatrix();
mesh.current.setMatrixAt(index, tempObject.matrix);
});
});
mesh.current.rotation.x = xRotationValue;
mesh.current.rotation.y = yRotationValue;
mesh.current.instanceMatrix.needsUpdate = true;
return mesh;
}
}
const itemMatrix = (items: itemMatrixProps) => {
let mesh = useRef();
const { xRotationValue, yRotationValue, getItemForGenerate, userDefineditems } = items;
const [positionDetailsData, setPositionDetailsData] = useState<PositionDetails>(null);
useEffect(() => {
generateMatrix(mesh, getItemForGenerate, userDefineditems, xRotationValue, yRotationValue);
}, [getItemForGenerate, userDefineditems, xRotationValue, yRotationValue]);
useEffect(() => {
setPositionDetailsData(getPostion());
}, [getItemForGenerate]);
const getPostion = () => {
let xDivisor = 1;
let yDivisor = 1;
let zDivisor = 1;
const positionDetails: PositionDetails = {
xPosition: 0,
yPosition: 0,
zPosition: 0
};
let meshCount = getItemForGenerate?.count;
({ xDivisor, yDivisor, zDivisor } = generateMaxDivisors(getItemForGenerate, xDivisor, yDivisor, zDivisor));
const = xDivisor > yDivisor ? xDivisor : yDivisor;
const yPosition = yDivisor / (yDivisor + yDivisor * 0.3);
const xPosition = / (xDivisor + yDivisor);
positionDetails.xPosition = xPosition;
positionDetails.yPosition = yPosition;
positionDetails.zPosition = 0;
return positionDetails;
};
return (
<instancedMesh
ref={mesh}
args={[null, null, getItemForGenerate?.count]}
position={[-positionDetailsData?.xPosition, -positionDetailsData?.yPosition, positionDetailsData?.zPosition]}
>
<boxGeometry attach="geometry" args={[0.9, 0.9, 0.9]} />
<meshNormalMaterial />
<meshBasicMaterial color={'#047ABC'} />
</instancedMesh>
);
}
I try with THREE.Color but didn't work.
I faced an issue with react-p5-wrapper that it is running in background although I have switch to another route in my react app.
For example, I am currently in /game, and the console is logging "running draw", but when I switch to /about-us, it still logging, meaning it is still running the draw function
Here is my code in sandbox
App.js
import "./styles.css";
import { Route, Switch, BrowserRouter as Router, Link } from "react-router-dom";
export default function App() {
return (
<div className="App">
<Router>
<Link to="/game">Game</Link> | <Link to="/about-us">About Us</Link>
<Switch>
<Route path="/about-us" component={require("./abtus").default} />
<Route path="/game" component={require("./game").default} />
</Switch>
</Router>
</div>
);
}
game.js
import { useEffect } from "react";
import { ReactP5Wrapper, P5Instance } from "react-p5-wrapper";
// Sound
let pointSound, endSound;
let playEndSound = false;
/**
* #param {P5Instance} p
*/
const sketch = (p) => {
const MAX_SPEED = 15;
const pickDirections = () => {
return ((Math.floor(Math.random() * 3) % 2 === 0 ? 1 : -1) * (Math.floor(Math.random() * 2) + 1));
};
const randomXPos = () => {
return p.random(30, p.width - 30);
};
const ramdomImgIndex = () => {
return Math.floor(Math.random() * imgs.length);
};
const reset = () => {
score = 0;
speed = 2;
falls = [
{
y: -70,
x: randomXPos(),
rotation: 0,
direction: pickDirections(),
imgIndex: ramdomImgIndex()
}
];
};
const rotate_n_draw_image = (image,img_x,img_y,img_width,img_height,img_angle) => {
p.imageMode(p.CENTER);
p.translate(img_x + img_width / 2, img_y + img_width / 2);
p.rotate((Math.PI / 180) * img_angle);
p.image(image, 0, 0, img_width, img_height);
p.rotate((-Math.PI / 180) * img_angle);
p.translate(-(img_x + img_width / 2), -(img_y + img_width / 2));
p.imageMode(p.CORNER);
};
// Images
let imgs = [],
basket = { img: null, width: 150, height: 150 },
imgSize = 50;
let screen = 0,
falls = [
{
y: -70,
x: randomXPos(),
rotation: 0,
direction: pickDirections(),
imgIndex: ramdomImgIndex()
}
],
score = 0,
speed = 2;
const startScreen = () => {
p.background(205, 165, 142);
p.fill(255);
p.textAlign(p.CENTER);
p.text("WELCOME TO MY CATCHING GAME", p.width / 2, p.height / 2);
p.text("click to start", p.width / 2, p.height / 2 + 20);
reset();
};
const gameOn = () => {
p.background(219, 178, 157);
p.textAlign(p.LEFT);
p.text("score = " + score, 30, 20);
falls = falls.map(({ x, y, rotation, direction, imgIndex }) => {
// rotate while dropping
rotation += direction;
// dropping
y += speed;
return { x, y, rotation, direction, imgIndex };
});
falls.forEach(({ x, y, rotation, imgIndex }, i) => {
// when is lower than the border line
if (y > p.height) {
screen = 2;
playEndSound = true;
}
// when reaching the border line and is within the range
if (
y > p.height - 50 &&
x > p.mouseX - basket.width / 2 &&
x < p.mouseX + basket.width / 2
) {
// Play Sound
pointSound.currentTime = 0;
pointSound.play();
// Increase Score
score += 10;
// Increase Speed
if (speed < MAX_SPEED) {
speed += 0.1;
speed = parseFloat(speed.toFixed(2));
}
// Whether add new item into array or not
if (i === falls.length - 1 && falls.length < 3) {
falls.push({
x: randomXPos(),
y: -70 - p.height / 3,
rotation: 0,
direction: pickDirections(),
imgIndex: ramdomImgIndex()
});
}
falls[i].y = -70;
falls[i].x = randomXPos();
falls[i].imgIndex = ramdomImgIndex();
}
rotate_n_draw_image(imgs[imgIndex], x, y, imgSize, imgSize, rotation);
});
p.imageMode(p.CENTER);
p.image(
basket.img,
p.mouseX,
p.height - basket.height / 2,
basket.width,
basket.height
);
};
const endScreen = () => {
if (playEndSound) {
endSound.play();
playEndSound = false;
}
p.background(205, 165, 142);
p.textAlign(p.CENTER);
p.text("GAME OVER", p.width / 2, p.height / 2);
p.text("SCORE = " + score, p.width / 2, p.height / 2 + 20);
p.text("click to play again", p.width / 2, p.height / 2 + 60);
};
p.preload = () => {
// Load Images
imgs[0] = p.loadImage("https://dummyimage.com/400x400");
imgs[1] = p.loadImage("https://dummyimage.com/400x400");
imgs[2] = p.loadImage("https://dummyimage.com/401x401");
basket.img = p.loadImage("https://dummyimage.com/500x500");
};
p.setup = () => {
p.createCanvas(
window.innerWidth > 400 ? 400 : window.innerWidth,
window.innerHeight > 500 ? 500 : window.innerHeight
);
};
p.draw = () => {
console.log("running draw");
switch (screen) {
case 0:
startScreen();
break;
case 1:
gameOn();
break;
case 2:
endScreen();
break;
default:
}
};
p.mousePressed = () => {
if (screen === 0) {
screen = 1;
} else if (screen === 2) {
screen = 0;
}
};
};
const CatchingGmae = () => {
useEffect(() => {
// eslint-disable-next-line
pointSound = new Audio("/game/points.wav");
// eslint-disable-next-line
endSound = new Audio("/game/end.wav");
pointSound.volume = 0.3;
return () => {
pointSound.muted = true;
endSound.muted = true;
};
});
return (
<div className="mx-auto flex justify-center items-center">
<ReactP5Wrapper sketch={sketch} />
</div>
);
};
export default CatchingGame;
Is there anyway to stop it from running in background when user switches route?
Given your setup, I can see two ways of telling the sketch to stop when route is switched and the Game react component is not rendered anymore.
Alt 1. You can make something similar to react-p5-wrapper
documentation, reacting to props:
In CatchingGmae component:
const [lastRender, setLastRender] = useState(Date.now());
useEffect(() => {
const interval = setInterval(() => setLastRender(Date.now()), 100);
return () => {
clearInterval(interval);
};
}, []);
return (
<>
<div className="mx-auto flex justify-center items-center">
<ReactP5Wrapper sketch={sketch} lastRender={lastRender} />
In sketch:
let lastRender = 0;
p.updateWithProps = (props) => {
lastRender = props.lastRender;
};
p.draw = () => {
if (!(Date.now() > lastRender + 100)) {
console.log("running draw");
☝ The problem with the Alt 1 is that react will do calculations and re-render frequently for no reason.
Alt 2. Use a state outside of React, a very simple side-effect
for the component, for the sketch to poll on.
Add to CatchingGmae component:
useEffect(() => {
window.noLoop = false;
return () => {
window.noLoop = true;
};
}, []);
Inside p.draw:
if (window.noLoop) return p.noLoop();
☝ This works without calculations, but you might want to scope the global within your own namespace or using other state manager.
I have simple Points mesh with custom shader and buffer geometry.
The geometry has position, size and color attributes.
On pointer hover, the hovered vertex turns into red color.
So far so good.
Now I would like to animate the change of color of the hovered vertex.
Here is the code snippet for the Points mesh:
const Points = (
props = { hoveredIndex: null, initialCameraZ: 0, array: [], sizes: [] }
) => {
const [_, setHover] = useState(false);
const vertices = new Float32Array(props.array);
const sizes = new Float32Array(props.sizes);
const _colors = genColors(props.sizes.length); // returns [] of numbers.
const uniforms = useMemo(() => {
return {
time: { type: "f", value: 0.0 },
cameraZ: { type: "f", value: props.initialCameraZ }
};
}, [props.initialCameraZ]);
// trying to use react-spring here
const [animProps, setAnimProps] = useSpring(() => ({
colors: _colors
}));
const geometry = useUpdate(
(geo) => {
if (props.hoveredIndex !== null) {
const i = props.hoveredIndex * 3;
const cols = [..._colors];
cols[i] = 1.0;
cols[i + 1] = 0.0;
cols[i + 2] = 0.0;
geo.setAttribute(
"color",
new THREE.BufferAttribute(new Float32Array(cols), 3)
);
setAnimProps({ colors: cols });
} else {
geo.setAttribute(
"color",
new THREE.BufferAttribute(new Float32Array(_colors), 3)
);
setAnimProps({ colors: _colors });
}
},
[props.hoveredIndex]
);
return (
<a.points
onPointerOver={(e) => setHover(true)}
onPointerOut={(e) => setHover(false)}
>
<a.bufferGeometry attach="geometry" ref={geometry}>
<bufferAttribute
attachObject={["attributes", "position"]}
count={vertices.length / 3}
array={vertices}
itemSize={3}
/>
<bufferAttribute
attachObject={["attributes", "size"]}
count={sizes.length}
array={sizes}
itemSize={1}
/>
<a.bufferAttribute
attachObject={["attributes", "color"]}
count={_colors.length}
array={new Float32Array(_colors)}
// array={animProps.colors} // this does not work
itemSize={3}
/>
</a.bufferGeometry>
<shaderMaterial
attach="material"
uniforms={uniforms}
vertexShader={PointsShader.vertexShader}
fragmentShader={PointsShader.fragmentShader}
vertexColors={true}
/>
</a.points>
);
};
Full code and example is available on codesandbox
When I try to use animProps.colors for color in bufferAttribute it fails to change the color.
What am i doing wrong? How to make it right?
I know I could create start and target color attributes, pass them to the shader and interpolate there but that would beat the purpose of using react-three-fiber.
Is there a way animating buffer attributes in react-three-fiber?
This likely isn't working as expected because changes made to buffer attributes need to be explicitly flagged for sending to the GPU. I suppose that react-spring does not do this out of the box.
Here's a vanilla example, note the usage of needsUpdate:
import { useFrame } from '#react-three/fiber'
import React, { useRef } from 'react'
import { BufferAttribute, DoubleSide } from 'three'
const positions = new Float32Array([
1, 0, 0,
0, 1, 0,
-1, 0, 0,
0, -1, 0
])
const indices = new Uint16Array([
0, 1, 3,
2, 3, 1,
])
const Comp = () => {
const positionsRef = useRef<BufferAttribute>(null)
useFrame(() => {
const x = 1 + Math.sin(performance.now() * 0.01) * 0.5
positionsRef.current.array[0] = x
positionsRef.current.needsUpdate = true
})
return <mesh>
<bufferGeometry>
<bufferAttribute
ref={positionsRef}
attach='attributes-position'
array={positions}
count={positions.length / 3}
itemSize={3}
/>
<bufferAttribute
attach="index"
array={indices}
count={indices.length}
itemSize={1}
/>
</bufferGeometry>
<meshBasicMaterial
color={[0, 1, 1]}
side={DoubleSide}
/>
</mesh>
}
For further reading check out this tutorial.