Cloning nodes and appending to separate Layers in React-Konva - reactjs

I'm trying to recreate an effect similar to the hover effect found on this site: http://tabotabo.com
Currently, what I'm doing is have the video playing on a layer with a second scaled up layer also playing the video with an additional Text object with a destination-in compositing operation. This is currently working well enough but I was curious if there would be a more efficient way to achieve this by either caching or cloning the first layer and sending that through to the second layer instead of having two separate video objects running in tandem.
Here is the relevant code, if it helps.
Main Render:
<Stage width={width} height={height} ref={ref => (this.stage = ref)}>
<Layer hitGraphEnabled={false}>
<CanvasVideo
src={this.state.background}
settings={{id: 'main', width: width, height: height }}
ref={(el) => this.main = el }
/>
</Layer>
<Layer hitGraphEnabled={false} scaleX={hoverScale} scaleY={hoverScale} x={scaleOffsetX} y={scaleOffsetY}>
<CanvasVideo
src={this.state.background}
settings={{id: 'main2', width: width, height: height }}
ref={(el) => this.main2 = el }
/>
<Text
id="hoverText"
text={this.state.menu.hoverText}
globalCompositeOperation='destination-in'
fontSize={200}
fill="white"
opacity={hoverOpacity}
height={height}
width={width}
align="center"
verticalAlign='middle'
/>
</Layer>
</Stage>
Video Container Class:
import React, { Component } from 'react';
import Konva from 'konva';
import { render } from 'react-dom';
import { Stage, Layer, Image } from 'react-konva';
class CanvasVideo extends Component {
constructor(props) {
super(props);
const video = document.createElement('video');
video.muted = true;
video.autoplay = false;
video.loop = true;
video.src = props.src;
this.state = {
video: video
};
video.addEventListener('canplay', () => {
video.play();
this.image.getLayer().batchDraw();
this.requestUpdate();
});
}
requestUpdate = () => {
this.image.getLayer().batchDraw();
requestAnimationFrame(this.requestUpdate);
}
render() {
let { settings } = this.props
return (
<Image
{...settings}
image={this.state.video}
ref={node => { this.image = node; }}
/>
);
}
}
CanvasVideo.defaultProps = {
settings: null,
};
export default CanvasVideo;
Any explanations or insights would be greatly appreciated.
Thank you!

Currently, there is no way to reuse Konva.Image or any other Konva.Node inside different parents. Konva.Node can have only one parent.
I see only one optimization here is to reuse <video> element you created in CanvasVideo component. It can be like this:
const cache = {};
function createVideo(url) {
if (cache[url]) {
return cache[url];
}
const video = document.createElement('video');
video.src = url;
cache[url] = video;
return video;
}
const video = createVideo(props.src);

Related

Change Material of a babylonJs model with react-babylonjs

I load a model coming from Blender (exported with babylon js exporter). The model comes with materials. and has 5 meshes (very simple test model).
I would like to change albedo (color under natural light) of some materials, but don't get how to do, as there is no component related to the material (because imported) and in react, there is usually a function to call to update internal values (then a refresh is triggered).
const onModelLoaded = model => {
model.meshes.forEach(mesh => {
console.log(`mesh... `, mesh.material.albedoColor);
// It shows well albedo of each material
});
};
export const SceneWithLoad = () => {
return (
<div>
<Engine antialias adaptToDeviceRatio canvasId="babylonJS">
<Scene>
<Suspense>
<Model
rootUrl="assets/firstLoco.babylon"
sceneFileName=""
onModelLoaded={onModelLoaded}
/>
</Suspense>
<hemisphericLight ... />
<arcRotateCamera ... />
</Scene>
</Engine>
</div>
);
};
When mesh is loaded, I can see albedo of each material with onModelLoaded (that's great), now I would like to update albedo on a regular basis (setInterval(() => {changeAlbedo()}, 1000)), but ref to Material objects change on refresh, and I need to call a function for react to know code updated the material albedo.
Cant find the trick here, Thanks for advices !
Following numerous tests
Here the ids of the materials are known. They are used to set albedo on them through the lodash filter function. In this case, it alternates red / white lights in front / back of a locomotive.
boolBal is a value set to true or false every sec. SwapMaterial doesn't display anything and is simply an entry point for the scene modification code.
What is not really "react way of working" is that scene are mutable (you can update a scene item without generating a new reference), react in principle is not (state change = new ref to the state object)
Any better suggestion please comment.
import _ from "lodash";
import { Engine, Scene, Model, useScene } from "react-babylonjs";
import { Vector3, Color3 } from "#babylonjs/core";
import "#babylonjs/loaders";
const colorWhite = new Color3(1, 1, 1);
const colorRed = new Color3(1, 0, 0);
const SwapMaterial = ({ boolVal }) => {
const scene = useScene();
_.filter(
scene.materials,
r => ["FrontLeft-mat", "FrontRight-mat"].indexOf(r.id) >= 0
).forEach(m => {
m.albedoColor = boolVal ? colorRed : colorWhite;
});
_.filter(
scene.materials,
r => ["RearLeft-mat", "RearRight-mat"].indexOf(r.id) >= 0
).forEach(m => {
m.albedoColor = !boolVal ? colorRed : colorWhite;
});
return null;
};
export const SceneWithLoad = () => {
const [boolVal, boolValSet] = useState(false);
const ref = useRef({});
ref.current.boolVal = boolVal;
ref.current.boolValSet = boolValSet;
useEffect(() => {
const intId = setInterval(() => {
ref.current.boolValSet(!ref.current.boolVal);
}, 1000);
return () => {
clearInterval(intId);
};
}, []);
return (
<div>
<Engine antialias adaptToDeviceRatio canvasId="babylonJS">
<Scene>
<Suspense>
<Model rootUrl="assets/firstLoco.babylon" sceneFileName="" />
</Suspense>
<hemisphericLight ... />
<hemisphericLight ... />
<arcRotateCamera ... />
<SwapMaterial {...{ boolVal }} />
</Scene>
</Engine>
</div>
);
};

React Leaflet v3 : Good way to create a control

I'm displaying a map with react leaflet v3 in my react app.
I just want to add a custom control, but I can't figure out what's the good way to do that.
Actually, i do it like that, but it seems to be not working.
function DptCtl(props) {
// Control
const map = useMap();
// List of dpts and provinces
const dpts = useSelector(dptsSelector);
L.Control.Dpts = L.Control.extend({
onAdd(map) {
const container = L.DomUtil.create('div');
const input = L.DomUtil.create('input', container);
return container;
},
onRemove(map) {}
})
L.Control.dpts = function (opts) {
return new L.Control.Dpts(opts);
}
useEffect(() => {
const control = L.Control.dpts({ position: props.position })
map.addControl(control)
return () => {
map.removeControl(control)
}
}, [dpts])
return null;
}
React-Leaflet v3 provides the createControlComponent Hook in the Core API that takes in an instance of a Leaflet control and returns a Leaflet element.
Here is an example using Leaflet's Zoom control:
import L from 'leaflet';
import { createControlComponent } from '#react-leaflet/core';
const createControlLayer = (props) => {
// Set up an instance of the control:
const controlInstance = new L.Control.Zoom(props);
return controlInstance;
};
// Pass the control instance to the React-Leaflet createControlComponent hook:
const customControl = createControlComponent(createControlLayer);
export default customControl;
Then, add the new custom control layer to the Map:
<MapContainer
center={[37.0902, -95.7129]}
zoom={3}
zoomControl={false}
style={{ height: '100vh', width: '100%', padding: 0 }}
whenCreated={(map) => setMap(map)}
>
<CustomControl />
<LayersControl position="topright">
<LayersControl.BaseLayer checked name="Map">
<TileLayer
attribution='© OpenStreetMap contributors'
url={maps.base}
/>
</LayersControl.BaseLayer>
</LayersControl>
</MapContainer>
DEMO
https://react-leaflet.js.org/docs/core-api/#createcontrolcomponent
https://javascript.plainenglish.io/how-to-create-a-react-leaflet-control-component-with-leaflet-routing-machine-8eef98259f20

Custom button on the leaflet map with React-leaflet version3

I'm a new leaflet learner with React typescript. Want to create a custom button on the map. On clicking the button a popup will appear. I saw many example but they are all based on older version and I also tried to create my own but no luck. The documentation also not providing much help. Even a functional custom control component is also very effective for my app. Any help on this will be much appreciated. Here is my code,
Custom button
import React, { Component } from "react";
import { useMap } from "react-leaflet";
import L, { LeafletMouseEvent, Map } from "leaflet";
class Description extends React.Component<{props: any}> {
createButtonControl() {
const MapHelp = L.Control.extend({
onAdd: (map : Map) => {
const helpDiv = L.DomUtil.create("button", ""); //how to pass here the button name and
//other property ?
//a bit clueless how to add a click event listener to this button and then
// open a popup div on the map
}
});
return new MapHelp({ position: "bottomright" });
}
componentDidMount() {
const { map } = this.props as any;
const control = this.createButtonControl();
control.addTo(map);
}
render() {
return null;
}
}
function withMap(Component : any) {
return function WrappedComponent(props : any) {
const map = useMap();
return <Component {...props} map={map} />;
};
}
export default withMap(Description);
The way I want to call it
<MapContainer
center={defaultPosition}
zoom={6}
zoomControl={false}
>
<Description />
<TileLayer
attribution="Map tiles by Carto, under CC BY 3.0. Data by OpenStreetMap, under ODbL."
url="https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png"
/>
<ZoomControl position={'topright'}/>
</MapContainer>
You're close. Sticking with the class component, you just need to continue creating your buttons instance. You can use a prop on Description to determine what your button will say and do:
<Description
title={"My Button Title"}
markerPosition={[20.27, -157]}
description="This is a custom description!"
/>
In your decsription's createButtonControl, you're almost there. You just need to fill it out a bit:
createButtonControl() {
const MapHelp = L.Control.extend({
onAdd: (map) => {
const helpDiv = L.DomUtil.create("button", "");
this.helpDiv = helpDiv;
// set the inner content from the props
helpDiv.innerHTML = this.props.title;
// add the event listener that will create a marker on the map
helpDiv.addEventListener("click", () => {
console.log(map.getCenter());
const marker = L.marker()
.setLatLng(this.props.markerPosition)
.bindPopup(this.props.description)
.addTo(map);
marker.openPopup();
});
// return the button div
return helpDiv;
}
});
return new MapHelp({ position: "bottomright" });
}
Working codesandbox
There's a million ways to vary this, but hopefully that will get you going.

Cannot crop image using react-image-crop

I trying to crop image using react-image-crop (https://www.npmjs.com/package/react-image-crop/v/6.0.7). Actually I cannot move cropped area or stretch it. I also use there React DragZone to upload image. Could you explain what I'm doing wrong and what could be the problem ? React-image-crop css is also imported. Can anybody help?
import React, {useCallback} from 'react'
import {useDropzone} from 'react-dropzone'
import ReactCrop from 'react-image-crop'
import ReactDOM from "react-dom";
import 'react-image-crop/dist/ReactCrop.css';
import "../Styles/ImageUpload.css"
function ImageUpload(files, addFile) {
const crop = {
disabled: false,
locked: false,
unit: 'px', // default, can be 'px' or '%'
x: 130,
y: 50,
width: 200,
height: 200
}
const onCrop = (image, pixelCrop, fileName) => {
}
const onImageLoaded = (image) => {
}
const onCropComplete = async crop => {
}
const onDrop = useCallback((acceptedFiles, rejectedFiles) => {
if (Object.keys(rejectedFiles).length !== 0) {
}
else {
files.addFile(acceptedFiles);
let popupBody = document.getElementsByClassName("popup-container")[0];
popupBody.innerHTML = "";
let cropElement = (<ReactCrop src = {URL.createObjectURL(acceptedFiles[0])} crop={crop}
onImageLoaded={onImageLoaded}
onComplete={onCropComplete}
onChange={onCrop} />);
ReactDOM.render(cropElement, popupBody);
}, [])
const {getRootProps, getInputProps, isDragActive} = useDropzone({onDrop, noKeyboard: true})
return (
<div className = "popup-container">
<p>Upload new photo</p>
{
(() => {
if (files.length == undefined) {
return (<div>
<input className = "testinput" {...getInputProps()} />
<div className = "popup-body" {...getRootProps()}>
<img src={require("../Resources/upload-image.png")} className = "image-upload-img"/>
<p style = {{'font-family':'Verdana'}}>Chouse a <b className = "file-manualy-
upload">file</b> or drag it here</p>
</div> </div>)
}
else {
}
})()}
</div>
)
}
I also face the same issue, Here you are trying to add disabled, locked in crop property but it is invalid because crop property takes following values:
{
unit: 'px', // default, can be 'px' or '%'
x: 130,
y: 50,
width: 200,
height: 200
}
To solve this add disabled, locked as a props in component instead of crop property.
I'm not sure sure why you would do a ReactDom.render from within the function. It seems to be counter to what React is and guessing that the library isn't binding events properly.
I would suggest, after the user drops the image, you load the image using a file reader and set it as state and change the display to show the <ReactCrop src=={this.state.img} />. The crop also needs to be in a state, so that when it changes, it updates the crop rectangle.
Here's a working example from the creator himself: https://codesandbox.io/s/72py4jlll6

React track height of div in state?

I found the code snippet from this answer for tracking page size to be useful. I want to switch window.innerHeight with $("#list_container").height:
constructor(props) {
super(props);
this.state = { width: 0, height: 0 };
this.updateWindowDimensions = this.updateWindowDimensions.bind(this);
}
componentDidMount() {
this.updateWindowDimensions();
window.addEventListener('resize', this.updateWindowDimensions);
}
componentWillUnmount() {
window.removeEventListener('resize', this.updateWindowDimensions);
}
updateWindowDimensions() {
// !!! This works:
this.setState({ width: window.innerWidth, height: window.innerHeight });
// !!! This doesn't work:
this.setState({ width: $("#list_container").width(), height: $("#list_container").height() });
}
Edit: Updated to .width() and .height(), had tried both but neither is working for me.
List container is defined in the same module as the outer div:
render() {
return (
<div id="list_container">
<ListGroup>
<VirtualList
width='100%'
height={this.state.height}
itemCount={this.state.networks.length}
itemSize={50}
renderItem={({index, style}) =>
<ListGroupItem
onClick={() => this.updateNetwork(this.state.networks[index].ssid, this.state.networks[index].security)}
action>
{this.state.networks[index].ssid}
</ListGroupItem>
}
/>
</ListGroup>
<Modal/>
// ...
Note:
If I do:
<p>{this.state.height}</p>
It isn't 0 just empty, with the example that doesn't work.
If it's jQuery you're using, width() and height() are functions, not properties. Try:
this.setState({ width: $("#list_container").width(), height: $("#list_container").height() });
you need to use refs since the js might be running before the component renders to the dom.
React documentation on refs
render() {
return (
<div id="list_container" ref={ el => this.componentName = el }>
// ...
to reference this dom node in this component you just have to call this.componentName
updateWindowDimensions = () => {
$(this.componentName).height() // returns height
}
To expand on Erics answer using refs, to pull it off without jQuery, you can use getBoundingClientRect:
const listRect = document.getElementById('list_container').getBoundingClientRect();
or if using the ref from Erics answer:
const listRect = this.componentName.getBoundingClientRect();
followed by
this.setState({
width: listRect.width,
height: listRect.height
});
I would suggest this page for some good examples of life without jQuery.
Also, I would strongly suggest debouncing the resize handler.

Resources