I'm trying to receive fog effect in React. The main idea is that I have two components: first component handles with updating of coordinates of clouds and their velocity, the second component is responsible for one cloud. I have problem with moving of clouds, if I don't clear canvas I can see track of every cloud, if I apply canvas.clear I can't see anything. Do you have any tip, where I should place clear canvas.clear or do you have other ideas?
The first component:
import React from 'react';
import styled from 'styled-components';
import FogPiece from './Fog-Piece.jsx';
const CanvasContext = React.createContext();
const FogDiv = styled.div`
position: absolute;
width:100vw;
height:100vh;
`
class Fog extends React.Component{
constructor(props){
super(props);
this.canvas = React.createRef();
this.state = {
ctx: null,
parameters:[],
screenWidth : 0,
screenHeight: 0,
}
}
componentDidMount = () => {
Promise.all(this.newCoordinates()).then((paramArray) =>{
this.setState({
ctx: this.canvas.current.getContext('2d'),
screenWidth: this.canvas.current.parentNode.getBoundingClientRect().width,
screenHeight: this.canvas.current.parentNode.getBoundingClientRect().height,
parameters: paramArray
});
window.requestAnimationFrame(this.update)
})
}
newCoordinates = () => {
return(Array.from(Array(this.props.density).keys()).map(elem =>{
return new Promise (resolve => {
const params = {
x: this.random(0,this.state.screenWidth),
y: this.random(0,this.state.screenHeight),
velocityX: this.random(-this.props.maxVelocity, this.props.maxVelocity),
velocityY: this.random(-this.props.maxVelocity, this.props.maxVelocity)
}
resolve(params)
})
}))
}
updateCoordinates = () => {
return(this.state.parameters.map(elem =>{
return new Promise (resolve => {
elem = this.ifCross(elem.x, elem.y, elem.velocityX, elem.velocityY);
const params = {
x: elem.x + elem.velocityX,
y: elem.y + elem.velocityY,
velocityX: elem.velocityX,
velocityY: elem.velocityY
}
resolve(params)
})
}))
}
random = (min,max) => {
return Math.random()*(max - min) + min
}
ifCross = (x,y, velocityX, velocityY) => {
if (x > this.state.screenWidth){
x = this.state.screenWidth
velocityX = - velocityX
}
if (x < 0){
x = 0
velocityX = - velocityX
}
if (y > this.state.screenHeight){
y = this.state.screenHeight
velocityY = - velocityY
}
if (y < 0){
y = 0
velocityY = - velocityY
}
return {x:x, y:y, velocityX:velocityX, velocityY:velocityY }
}
update = () => {
Promise.all(this.updateCoordinates()).then((paramArray) =>{
//here is the problem
// this.state.ctx.clearRect(0,0,this.state.screenWidth, this.state.screenHeight)
this.setState({
parameters: paramArray,
});
window.requestAnimationFrame(this.update)
})
}
render(){
return(
<FogDiv>
<canvas width={this.state.screenWidth} height={this.state.screenHeight} ref = {this.canvas} >
{this.state.ctx && (
<CanvasContext.Provider value = {this.state.ctx}>
{this.state.parameters.map(param =>(
<FogPiece
x = {param.x}
y = {param.y}
/>
))}
</CanvasContext.Provider>
)}
</canvas>
</FogDiv>
)
}
}
export default Fog;
export {
CanvasContext
}
the second one:
import React from 'react';
import styled from 'styled-components';
import {CanvasContext} from './Fog.jsx';
class FogPiece extends React.Component{
constructor(props){
super(props);
this.state = {
image:'https://media.istockphoto.com/vectors/sample-red-square-grunge-textured-isolated-stamp-vector-id471401412',
}
}
random(min,max){
return Math.random()*(max - min) + min
}
render(){
return(
<CanvasContext.Consumer>
{ctx => {
console.log("x", "y", this.props)
const img = new Image();
img.src = this.state.image;
img.onload = () => {
ctx.drawImage(img,
this.props.x,
this.props.y,
40,
40)
}
}}
</CanvasContext.Consumer>
)
}
}
export default FogPiece;
Related
I'm trying to rewrite the App component in this CodePen as a Functional component using Typescript.
However, I am getting error like this when trying to run it:
ERROR in src/App.tsx:13:14
TS2339: Property 'forceUpdateHandler' does not exist on type 'MutableRefObject<Spinner | null>'.
11 |
12 | const handleClick = () => {
> 13 | _child1?.forceUpdateHandler();
| ^^^^^^^^^^^^^^^^^^
14 | _child2?.forceUpdateHandler();
15 | _child3?.forceUpdateHandler();
16 | };
What is the correct way to handle Spinner.forceUpdateHandler?
Here's my attempt:
App.tsx
Rewriting the class component as a functional component
This has been simplified from the original to focus on the problematic area
import React, { useRef } from "react";
import "./App.css";
import Spinner from "./Spinner.js";
const App = () => {
const [matches, setMatches] = React.useState<number[]>([]);
const _child1 = useRef<Spinner | null>(null);
const _child2 = useRef<Spinner | null>(null);
const _child3 = useRef<Spinner | null>(null);
const handleClick = () => {
_child1?.forceUpdateHandler();
_child2?.forceUpdateHandler();
_child3?.forceUpdateHandler();
};
const finishHandler = (value: number) => {
setMatches([...matches, value]);
if (matches.length === 3) {
console.log("Done");
emptyArray();
}
};
const emptyArray = () => {
setMatches([]);
};
return (
<div>
<div className={`spinner-container`}>
<Spinner onFinish={finishHandler} ref={_child1} timer="1000" />
<Spinner onFinish={finishHandler} ref={_child2} timer="1400" />
<Spinner onFinish={finishHandler} ref={_child3} timer="2200" />
<div className="gradient-fade"></div>
</div>
<button onClick={handleClick}>SPIN!!!</button>
</div>
);
};
export default App;
Spinner.js
Same as in the above CodePen, with imports and exports added
import React from "react";
class Spinner extends React.Component {
constructor(props) {
super(props);
this.forceUpdateHandler = this.forceUpdateHandler.bind(this);
}
forceUpdateHandler() {
this.reset();
}
reset() {
if (this.timer) {
clearInterval(this.timer);
}
this.start = this.setStartPosition();
this.setState({
position: this.start,
timeRemaining: this.props.timer,
});
this.timer = setInterval(() => {
this.tick();
}, 100);
}
state = {
position: 0,
lastPosition: null,
};
static iconHeight = 188;
multiplier = Math.floor(Math.random() * (4 - 1) + 1);
start = this.setStartPosition();
speed = Spinner.iconHeight * this.multiplier;
setStartPosition() {
return Math.floor(Math.random() * 9) * Spinner.iconHeight * -1;
}
moveBackground() {
this.setState({
position: this.state.position - this.speed,
timeRemaining: this.state.timeRemaining - 100,
});
}
getSymbolFromPosition() {
let { position } = this.state;
const totalSymbols = 9;
const maxPosition = Spinner.iconHeight * (totalSymbols - 1) * -1;
let moved = (this.props.timer / 100) * this.multiplier;
let startPosition = this.start;
let currentPosition = startPosition;
for (let i = 0; i < moved; i++) {
currentPosition -= Spinner.iconHeight;
if (currentPosition < maxPosition) {
currentPosition = 0;
}
}
this.props.onFinish(currentPosition);
}
tick() {
if (this.state.timeRemaining <= 0) {
clearInterval(this.timer);
this.getSymbolFromPosition();
} else {
this.moveBackground();
}
}
componentDidMount() {
clearInterval(this.timer);
this.setState({
position: this.start,
timeRemaining: this.props.timer,
});
this.timer = setInterval(() => {
this.tick();
}, 100);
}
render() {
let { position, current } = this.state;
return (
<div
style={{ backgroundPosition: "0px " + position + "px" }}
className={`icons`}
/>
);
}
}
export default Spinner;
Ref's hold the actual reference in the current property, so it should actually be:
const handleClick = () => {
_child1?.current?.forceUpdateHandler();
_child2?.current?.forceUpdateHandler();
_child3?.current?.forceUpdateHandler();
};
You can read more about it here
just an idea may be you should try redefining the Spinner type in your functional component
type SpinnerProps ={
forceUpdateHandler: () => void,
startPosition: () => number,
.... // TODO all the other props need to be defined
}
const _child1 = useRef<SpinnerProps | null>(null);
I'm trying to modify a Spinner component by adding a new method Spinner.moveToPosition so that it will spin to a specific symbol, as opposed to Spinner.forceUpdateHandler which spins it to a random symbol.
Here's my CodePen: https://codepen.io/gameveloster/pen/qBxJPVK
Original CodePen: https://codepen.io/antibland/pen/ypagZd
However, when I added a "SPIN!" button where clicking the button will get them to spin to positions 0, 0 and 1 so that the left two spinners will display the same image
const handleClick = () => {
// The first and second symbols should be the same after spinning, but they are not
_child1?.current?.moveToPosition(0);
_child2?.current?.moveToPosition(0);
_child3?.current?.moveToPosition(1);
};
they end up not doing that:
What might be the issue here? Thank you!
Spinner.js
import React from "react";
class Spinner extends React.Component {
constructor(props) {
super(props);
this.forceUpdateHandler = this.forceUpdateHandler.bind(this);
}
moveToPosition(position) {
if (this.timer) {
clearInterval(this.timer);
}
this.setState({
position: position * Spinner.iconHeight,
timeRemaining: this.props.timer,
});
this.timer = setInterval(() => {
this.tick();
}, 100);
}
forceUpdateHandler() {
this.reset();
}
reset() {
if (this.timer) {
clearInterval(this.timer);
}
this.start = this.setStartPosition();
this.setState({
position: this.start,
timeRemaining: this.props.timer,
});
this.timer = setInterval(() => {
this.tick();
}, 100);
}
state = {
position: 0,
lastPosition: null,
};
static iconHeight = 188;
multiplier = Math.floor(Math.random() * (4 - 1) + 1);
start = this.setStartPosition();
speed = Spinner.iconHeight * this.multiplier;
setStartPosition() {
return Math.floor(Math.random() * 9) * Spinner.iconHeight * -1;
}
moveBackground() {
this.setState({
position: this.state.position - this.speed,
timeRemaining: this.state.timeRemaining - 100,
});
}
getSymbolFromPosition() {
let { position } = this.state;
const totalSymbols = 9;
const maxPosition = Spinner.iconHeight * (totalSymbols - 1) * -1;
let moved = (this.props.timer / 100) * this.multiplier;
let startPosition = this.start;
let currentPosition = startPosition;
for (let i = 0; i < moved; i++) {
currentPosition -= Spinner.iconHeight;
if (currentPosition < maxPosition) {
currentPosition = 0;
}
}
this.props.onFinish(currentPosition);
}
tick() {
if (this.state.timeRemaining <= 0) {
clearInterval(this.timer);
this.getSymbolFromPosition();
} else {
this.moveBackground();
}
}
componentDidMount() {
clearInterval(this.timer);
this.setState({
position: this.start,
timeRemaining: this.props.timer,
});
this.timer = setInterval(() => {
this.tick();
}, 100);
}
render() {
let { position, current } = this.state;
return (
<div
style={{ backgroundPosition: "0px " + position + "px" }}
className={`icons`}
/>
);
}
}
export default Spinner;
App.tsx
import React, { useRef } from "react";
import "./App.css";
import Spinner from "./Spinner.js";
const App = () => {
const [matches, setMatches] = React.useState<number[]>([]);
const _child1 = useRef<Spinner | null>(null);
const _child2 = useRef<Spinner | null>(null);
const _child3 = useRef<Spinner | null>(null);
const handleClick = () => {
// The first and second symbols should be the same after spinning, but they are not
_child1?.current?.moveToPosition(0);
_child2?.current?.moveToPosition(0);
_child3?.current?.moveToPosition(1);
};
const finishHandler = (value: number) => {
setMatches([...matches, value]);
if (matches.length === 3) {
console.log("Done");
emptyArray();
}
};
const emptyArray = () => {
setMatches([]);
};
return (
<div>
<div className={`spinner-container`}>
<Spinner onFinish={finishHandler} ref={_child1} timer="1000" />
<Spinner onFinish={finishHandler} ref={_child2} timer="1400" />
<Spinner onFinish={finishHandler} ref={_child3} timer="2200" />
<div className="gradient-fade"></div>
</div>
<button onClick={handleClick}>SPIN!!!</button>
</div>
);
};
export default App;
We recently worked on an auto-scrolling while freely swipeable component using React.js. The implementation idea is inspired by this article
And we've made something like this in React:
import React, { Component } from "react";
import PropTypes from "prop-types";
import "./AutoScroller.css";
const NUM_OF_CLONES = 10;
const AUTO_SCROLL_OFFSET = 1; // min offset of scrollTo is 1
const AUTO_SCROLL_INTERVAL = 32; // 1000 ms / 30 fps
export default class AutoScroller extends Component {
static propTypes = {
contents: PropTypes.array.isRequired,
itemWidth: PropTypes.number.isRequired,
numsOfItemsPerScreen: PropTypes.number.isRequired
};
constructor(props) {
super(props);
this.autoScrollerRef = React.createRef();
this.currentPosition = 0;
this.autoScrollTimer = null;
this.scrollingTimer = null;
/* boolean status */
this.isTouch = false;
this.isScrolling = false;
}
componentDidMount() {
this.startAutoScroll();
this.autoScrollerRef.current.addEventListener(
"touchstart",
this.touchStartHandler
);
this.autoScrollerRef.current.addEventListener(
"touchend",
this.touchEndHandler
);
this.autoScrollerRef.current.addEventListener("scroll", this.scrollHandler);
this.autoScrollerRef.current.addEventListener(
"contextmenu",
this.contextMenuHandler
);
}
componentWillUnmount() {
this.clearAutoScroll();
this.clearScrollingTimer();
this.autoScrollerRef.current.removeEventListener(
"touchstart",
this.touchStartHandler
);
this.autoScrollerRef.current.removeEventListener(
"touchend",
this.touchEndHandler
);
this.autoScrollerRef.current.removeEventListener(
"scroll",
this.scrollHandler
);
this.autoScrollerRef.current.removeEventListener(
"contextmenu",
this.contextMenuHandler
);
}
touchStartHandler = () => {
this.isTouch = true;
this.clearAutoScroll();
};
touchEndHandler = () => {
this.isTouch = false;
if (!this.isScrolling) {
this.currentPosition = this.autoScrollerRef.current.scrollLeft;
this.startAutoScroll();
}
};
scrollHandler = () => {
const {
contents: { length },
itemWidth
} = this.props;
this.isScrolling = true;
this.currentPosition = this.autoScrollerRef.current.scrollLeft;
const maxOffset = length * itemWidth;
if (this.currentPosition > maxOffset) {
const offset = this.currentPosition - maxOffset;
this.autoScrollerRef.current.scrollTo(offset, 0);
this.currentPosition = offset;
} else if (this.currentPosition <= 0) {
const offset = this.currentPosition + maxOffset;
this.autoScrollerRef.current.scrollTo(offset, 0);
this.currentPosition = offset;
}
/***
* note: there will be only one timer, and the timer is only created by the very last scroll
* only when the scroll event is not triggered anymore, the timer starts to get executed.
*/
if (this.scrollingTimer) {
clearTimeout(this.scrollingTimer);
}
this.scrollingTimer = setTimeout(() => {
this.isScrolling = false;
/***
* note: resume auto-scroll when the momentum scroll (after finger leaves) stalls the scroll
*/
if (!this.isTouch) {
this.startAutoScroll();
}
}, 300);
};
contextMenuHandler = (event) => {
event.preventDefault();
};
startAutoScroll = () => {
if (!this.autoScrollTimer) {
this.autoScrollTimer = setInterval(this.autoScroll, AUTO_SCROLL_INTERVAL);
}
};
clearAutoScroll = () => {
if (this.autoScrollTimer) {
clearInterval(this.autoScrollTimer);
this.autoScrollTimer = null;
}
};
clearScrollingTimer = () => {
if (this.scrollingTimer) {
clearTimeout(this.scrollingTimer);
this.scrollingTimer = null;
}
};
autoScroll = () => {
const {
contents: { length },
itemWidth,
numsOfItemsPerScreen
} = this.props;
if (this.currentPosition < 0) {
this.currentPosition = 0;
}
if (length > numsOfItemsPerScreen) {
const position = this.currentPosition + AUTO_SCROLL_OFFSET;
this.autoScrollerRef.current.scrollTo(position, 0);
const maxOffset = length * itemWidth;
if (this.currentPosition > maxOffset) {
const offset = this.currentPosition - maxOffset;
this.autoScrollerRef.current.scrollTo(offset, 0);
this.currentPosition = offset;
} else {
this.currentPosition = position;
}
}
};
getWrappedData = () => {
const { contents } = this.props;
const { length } = contents;
const numberOfClones = length < NUM_OF_CLONES ? length : NUM_OF_CLONES;
return [...contents, ...contents.slice(0, numberOfClones)];
};
render() {
const { itemGap, lineHeight } = this.props;
return (
<div className="auto-scroller" ref={this.autoScrollerRef}>
<ul>
{this.getWrappedData().map((content, index) => (
<Item
key={`auto-scroller-item-${index}`}
content={content}
itemGap={itemGap}
lineHeight={lineHeight}
/>
))}
</ul>
</div>
);
}
}
class Item extends Component {
static propTypes = {
content: PropTypes.object.isRequired,
itemGap: PropTypes.number,
lineHeight: PropTypes.number
};
render() {
const { content, itemGap = 10 } = this.props;
return (
<li
className="auto-scroller__item"
style={{ paddingRight: `${itemGap}px` }}
>
<div className="auto-scroller__item__content">
<img draggable={false} src={content.imgUrl} />
<div className="auto-scroller__item__content__title">
{content.title}
</div>
</div>
</li>
);
}
}
You can test with the demo from PlayCode (source code).
Just open the link with Safari on the iPhone.
What I observed was every time when it was on the boundary cases, the image started to flicker.
Further, if you swipe it with your finger forth and back on that point, the whole UI started to flicker. (see this screen recording) However, we didn't spot this glitch on Android devices.
Any possible solutions are welcome. Does anyone encounter something like this before?
removing overflow-y: hidden; and overflow-x: auto; from autoscroller.css
solved it on my end.
another solution would be to add z-index: 1; and scroll-behavior: smooth; to .auto-scroller
let me know if it worked!
I would like to create a canvas where the user can draw an Arrow by using his mouse.
What I'm trying to accomplish is exactly this: https://jsfiddle.net/w33e9fpa/
But I don't understand how to convert that to React code, and my implementation currently doesn't work. When I run this code it seems that an arrow is drawn on the top left of the canvas, but nothing happens if I click on it.
Here's my code:
class DrawArrow extends Component {
state = {
isDrawing: false,
mode: "brush"
};
componentDidMount() {
const canvas = document.createElement("canvas");
canvas.width = 300;
canvas.height = 300;
const context = canvas.getContext("2d");
this.setState({ canvas, context });
}
handleMouseDown = () => {
this.setState({ isDrawing: true });
// TODO: improve
const stage = this.arrow.parent.parent;
this.lastPointerPosition = stage.getPointerPosition();
this.setState({
posX: this.lastPointerPosition.x,
poxY: this.lastPointerPosition.y
})
}
handleMouseUp = () => {
this.setState({ isDrawing: false });
};
handleMouseMove = () => {
if (this.state.drawing === true) {
const stage = this.arrow.parent.parent;
this.lastPointerPosition = stage.getPointerPosition();
var pos = stage.getPointerPosition();
var oldPoints = this.arrow.points();
this.arrow.points([oldPoints[0], oldPoints[1], pos.x, pos.y])
this.arrow.getLayer().draw();
}
}
render() {
return (
<Arrow
points= {[this.state.posX,this.state.posY, this.state.posX, this.state.posY]}
pointerLength= {20}
pointerWidth= {20}
fill= 'black'
stroke= 'black'
strokeWidth= {4}
onMouseDown={this.handleMouseDown}
onMouseUp={this.handleMouseUp}
onMouseMove={this.handleMouseMove}
/>
);
}
}
class NewWhite extends Component {
render() {
return (
<Stage width={900} height={700}>
<Layer>
<DrawArrow />
</Layer>
</Stage>
);
}
}
Thanks for you help !
Here you go:
import React, { Component } from "react";
import { Stage, Layer, Arrow, Circle, Line } from "react-konva";
import ReactDOM from "react-dom";
import "./styles.css";
class Drawable {
constructor(startx, starty) {
this.startx = startx;
this.starty = starty;
}
}
class ArrowDrawable extends Drawable {
constructor(startx, starty) {
super(startx, starty);
this.x = startx;
this.y = starty;
}
registerMovement(x, y) {
this.x = x;
this.y = y;
}
render() {
const points = [this.startx, this.starty, this.x, this.y];
return <Arrow points={points} fill="black" stroke="black" />;
}
}
class CircleDrawable extends ArrowDrawable {
constructor(startx, starty) {
super(startx, starty);
this.x = startx;
this.y = starty;
}
render() {
const dx = this.startx - this.x;
const dy = this.starty - this.y;
const radius = Math.sqrt(dx * dx + dy * dy);
return (
<Circle radius={radius} x={this.startx} y={this.starty} stroke="black" />
);
}
}
class FreePathDrawable extends Drawable {
constructor(startx, starty) {
super(startx, starty);
this.points = [startx, starty];
}
registerMovement(x, y) {
this.points = [...this.points, x, y];
}
render() {
return <Line points={this.points} fill="black" stroke="black" />;
}
}
class SceneWithDrawables extends Component {
constructor(props) {
super(props);
this.state = {
drawables: [],
newDrawable: [],
newDrawableType: "FreePathDrawable"
};
}
getNewDrawableBasedOnType = (x, y, type) => {
const drawableClasses = {
FreePathDrawable,
ArrowDrawable,
CircleDrawable
};
return new drawableClasses[type](x, y);
};
handleMouseDown = e => {
const { newDrawable } = this.state;
if (newDrawable.length === 0) {
const { x, y } = e.target.getStage().getPointerPosition();
const newDrawable = this.getNewDrawableBasedOnType(
x,
y,
this.state.newDrawableType
);
this.setState({
newDrawable: [newDrawable]
});
}
};
handleMouseUp = e => {
const { newDrawable, drawables } = this.state;
if (newDrawable.length === 1) {
const { x, y } = e.target.getStage().getPointerPosition();
const drawableToAdd = newDrawable[0];
drawableToAdd.registerMovement(x, y);
drawables.push(drawableToAdd);
this.setState({
newDrawable: [],
drawables
});
}
};
handleMouseMove = e => {
const { newDrawable } = this.state;
if (newDrawable.length === 1) {
const { x, y } = e.target.getStage().getPointerPosition();
const updatedNewDrawable = newDrawable[0];
updatedNewDrawable.registerMovement(x, y);
this.setState({
newDrawable: [updatedNewDrawable]
});
}
};
render() {
const drawables = [...this.state.drawables, ...this.state.newDrawable];
return (
<div>
<button
onClick={e => {
this.setState({ newDrawableType: "ArrowDrawable" });
}}
>
Draw Arrows
</button>
<button
onClick={e => {
this.setState({ newDrawableType: "CircleDrawable" });
}}
>
Draw Circles
</button>
<button
onClick={e => {
this.setState({ newDrawableType: "FreePathDrawable" });
}}
>
Draw FreeHand!
</button>
<Stage
onMouseDown={this.handleMouseDown}
onMouseUp={this.handleMouseUp}
onMouseMove={this.handleMouseMove}
width={900}
height={700}
>
<Layer>
{drawables.map(drawable => {
return drawable.render();
})}
</Layer>
</Stage>
</div>
);
}
}
function App() {
return <SceneWithDrawables />;
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Working example to play with:
https://codesandbox.io/s/w12qznzx5
We are building a React-Redux web app that will display multiple Three JS scenes. These scenes come in pairs, and each pair will have synchronized zooming. To facilitate that, we're storing camera data in the Redux store.
Here is our React class (take a deep breath, it's a little long for a SO question), which uses react-three-renderer to produce Three JS objects:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Vector3 } from 'three';
import React3 from 'react-three-renderer';
import ReferenceGrid from './ReferenceGridVisual';
import ResourceGroup from './resourceGroups/ResourceGroup';
import { initializeRayCastScene } from './viewportMath/RayCastScene';
import zoomCamera from './viewportMath/CameraZoom';
import { registerCamera, zoom } from './actions/cameraActions';
import { InitThreeJsDomEvents, UpdateDomCamera } from './domUtility/ThreeJSDom';
class ThreeJsScene extends Component {
constructor(props) {
super(props);
this.ZoomAmount = 150;
this.ZoomMaxCap = 1000;
this.ZoomMinCap = 6;
this.zoomPadding = 10;
this.minimumZoom = 45;
}
componentWillMount() {
initializeRayCastScene();
this.props.registerCamera(this.props.sceneName);
}
componentDidMount() {
// eslint-disable-next-line no-underscore-dangle
InitThreeJsDomEvents(this.camera, this.canvas._canvas);
}
onWheel = (event) => {
// eslint-disable-next-line
this.zoom(event.clientX, event.clientY, event.deltaY);
}
setCameraRef = (camera) => {
UpdateDomCamera(camera);
this.camera = camera;
}
zoom(screenPosX, screenPosY, zoomAmount) {
const size = {
width: this.props.width,
height: this.props.height,
};
const result = zoomCamera(screenPosX, screenPosY, zoomAmount, this.camera.position,
size, this.props.distance, this.camera, this.props.cameraType, this.ZoomMaxCap,
this.ZoomMinCap);
this.ZoomAmount = (result.ZoomAmount) ? result.ZoomAmount : this.ZoomAmount;
this.props.zoom(this.props.sceneName, result.distanceChangeFactor, result.newCameraPosition);
}
render() {
let position;
if (this.props.cameraPosition != null) {
position = new Vector3(
this.props.cameraPosition.x,
this.props.cameraPosition.y,
this.props.cameraPosition.z
);
} else {
position = new Vector3();
}
const left = -this.props.width / 2;
const right = this.props.width / 2;
const top = this.props.height / 2;
const bottom = -this.props.height / 2;
return (
<div
style={{ lineHeight: '0' }}
onWheel={this.onWheel}
>
<React3
width={this.props.width}
height={this.props.height}
mainCamera="camera"
antialias
pixelRatio={1}
ref={(canvas) => { this.canvas = canvas; }}
>
<scene ref={(scene) => { this.scene = scene; }}>
<orthographicCamera
name="camera"
left={left}
right={right}
top={top}
bottom={bottom}
near={0.01}
far={1400}
position={position}
ref={this.setCameraRef}
/>
<ambientLight
color={0xaaaaaa}
/>
<directionalLight
color={0xaaaaaa}
intensity={1.1}
position={new Vector3(3, 4, 10)}
lookAt={new Vector3(0, 0, 0)}
/>
<ReferenceGrid xActive yActive zActive={false} store={this.props.store} />
<ResourceGroup store={this.props.store}>
{this.props.children}
</ResourceGroup>
</scene>
</React3>
</div>
);
}
}
const mapStateToProps = (state, ownProps) => {
const ownCamera = state.cameras.get(ownProps.sceneName);
if (ownCamera == null) {
console.log('own camera null');
return { cameraAvailable: false };
}
console.log('has own camera');
const cameraPosition = ownCamera.position;
const cameraType = ownCamera.type;
const distance = ownCamera.distance;
return {
cameraAvailable: true,
cameraPosition,
cameraType,
distance,
};
};
const mapDispatchToProps = dispatch => ({
registerCamera: (cameraName) => {
dispatch(registerCamera(cameraName));
},
zoom: (cameraName, factor, newCameraPosition) => {
dispatch(zoom(cameraName, factor, newCameraPosition));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(ThreeJsScene);
Additionally, for reference, here are the action creators:
export const registerCamera = cameraName => (dispatch) => {
dispatch({ type: 'REGISTER_CAMERA', newCameraName: cameraName });
};
export const zoom = (cameraName, factor, newCameraPosition) => (dispatch, getState) => {
const state = getState();
const zoomFactor = state.cameras.get(cameraName).distance * (1 - factor);
dispatch({ type: 'CAMERA_ZOOM', cameraName, factor: zoomFactor, newCameraPosition });
};
And the reducer:
import { Map } from 'immutable';
const defaultCameraProperties = {
distance: 150,
type: 'orthogonal',
position: { x: 0, y: 10, z: 50 },
rotation: { x: 0, y: 0, z: 0, w: 1 },
};
const initialState = Map();
export default (state = initialState, action) => {
switch (action.type) {
case 'REGISTER_CAMERA': {
const newCamera = {
...defaultCameraProperties,
...action.newCameraProperties,
};
return state.set(action.newCameraName, newCamera);
}
case 'CAMERA_ZOOM': {
const updatedDistance = action.factor;
const updatedCameraPosition = {
...state.get(action.cameraName).position,
...action.newCameraPosition,
};
const updatedCamera = {
...state.get(action.cameraName),
position: updatedCameraPosition,
distance: updatedDistance,
};
return state.set(action.cameraName, updatedCamera);
}
default: {
return state;
}
}
};
The challenge is in the zoom function in the React class, the React props are not what I would expect, and therefore zooming is failing. Here is a summary of the sequence of relevant events as I understand them:
componentWillMount is called, which dispatches the REGISTER_CAMERA method. (We do this rather than having camera data by default in the store because these pairs of scenes are generated dynamically - there is not a static number of them.)
The React render method is called.
The React render method is called again since the REGISTER_CAMERA action has now modified the store and we have new props - the camera related props are now available.
I trigger zoom with my mouse wheel. The onWheel handler calls the zoom function, but breakpointing in that method reveals that the camera related props - like this.props.cameraType - are undefined. The React props appear as they do in 2. (zoomCamera does some calculations. Since these properties are unavailable, zooming fails.)
I can't figure out why this is. My suspicion is I'm misunderstanding something about what this context is bound to the zoom method.
In short my question is why are my props not up to date and how can I make the updated version available to the zoom function?
Turns out it was an error with hot module reloading. Running our build cold does not exhibit the issue.