Animating Line position in Konva - javascript

I have a network that I want to draw with Konva (and the react-konva bindings). When positions update I want to animate the nodes in the network to their new positions while also animating the start and end position of the link that connects them.
I started with the following simple example, but can't seem to get a Line to animate in the same way that the nodes do.
Is there a way to fix this, or am I approaching it in the wrong way?
import React from "react";
import { Stage, Layer, Rect, Line } from "react-konva";
class Node extends React.Component {
componentDidUpdate() {
this.rect.to({
x: this.props.x,
y: this.props.y,
});
}
render() {
const { id } = this.props;
const color = id === "a" ? "blue" : "red";
return (
<Rect
ref={node => {
this.rect = node;
}}
width={5}
height={5}
fill={color}
/>
);
}
}
class Link extends React.Component {
componentDidUpdate() {
const x0 = 0;
const y0 = 0;
const x1 = 100;
const y1 = 100;
this.line.to({
x: x0,
y: y0,
points: [x1, y1, x0, y0],
});
}
render() {
const color = "#ccc";
return (
<Line
ref={node => {
this.line = node;
}}
stroke={color}
/>
);
}
}
class Graph extends React.Component {
constructor(props) {
super(props);
this.state = {
nodes: [{ id: "a", x: 0, y: 0 }, { id: "b", x: 200, y: 200 }],
links: [
{
source: "a",
target: "b",
},
],
};
}
handleClick = () => {
const nodes = this.state.nodes.map(node => {
const position = node.x === 0 ? { x: 200, y: 200 } : { x: 0, y: 0 };
return Object.assign({}, node, position);
});
this.setState({
nodes,
});
};
render() {
const { links, nodes } = this.state;
return (
<React.Fragment>
<Stage width={800} height={800}>
<Layer>
{nodes.map((node, index) => {
return (
<Node
key={`node-${index}`}
x={node.x}
y={node.y}
id={node.id}
/>
);
})}
</Layer>
<Layer>
{links.map(link => {
return (
<Link
source={nodes.find(node => node.id === link.source)}
target={nodes.find(node => node.id === link.target)}
/>
);
})}
</Layer>
</Stage>
<button onClick={this.handleClick}>Click me</button>
</React.Fragment>
);
}
}
export default Graph;

You may need to set initial values for points attribute for a better tween.
Also, you are not using the source and target in the Link component. You should use that props for calculating animations.
import React from "react";
import { render } from "react-dom";
import { Stage, Layer, Rect, Line } from "react-konva";
class Node extends React.Component {
componentDidMount() {
this.rect.setAttrs({
x: this.props.x,
y: this.props.y
});
}
componentDidUpdate() {
this.rect.to({
x: this.props.x,
y: this.props.y
});
}
render() {
const { id } = this.props;
const color = id === "a" ? "blue" : "red";
return (
<Rect
ref={node => {
this.rect = node;
}}
width={5}
height={5}
fill={color}
/>
);
}
}
class Link extends React.Component {
componentDidMount() {
// set initial value:
const { source, target } = this.props;
console.log(source, target);
this.line.setAttrs({
points: [source.x, source.y, target.x, target.y]
});
}
componentDidUpdate() {
this.animate();
}
animate() {
const { source, target } = this.props;
this.line.to({
points: [source.x, source.y, target.x, target.y]
});
}
render() {
const color = "#ccc";
return (
<Line
ref={node => {
this.line = node;
}}
stroke={color}
/>
);
}
}
class Graph extends React.Component {
constructor(props) {
super(props);
this.state = {
nodes: [{ id: "a", x: 0, y: 0 }, { id: "b", x: 200, y: 200 }],
links: [
{
source: "a",
target: "b"
}
]
};
}
handleClick = () => {
const nodes = this.state.nodes.map(node => {
const position = node.x === 0 ? { x: 200, y: 200 } : { x: 0, y: 0 };
return Object.assign({}, node, position);
});
this.setState({
nodes
});
};
render() {
const { links, nodes } = this.state;
return (
<React.Fragment>
<Stage width={800} height={300}>
<Layer>
{nodes.map((node, index) => {
return (
<Node
key={`node-${index}`}
x={node.x}
y={node.y}
id={node.id}
/>
);
})}
</Layer>
<Layer>
{links.map(link => {
return (
<Link
source={nodes.find(node => node.id === link.source)}
target={nodes.find(node => node.id === link.target)}
/>
);
})}
</Layer>
</Stage>
<button onClick={this.handleClick}>Click me</button>
</React.Fragment>
);
}
}
render(<Graph />, document.getElementById("root"));
Demo: https://codesandbox.io/s/react-konva-animating-line-demo-erufn

Related

Access to other components variables in react

I'm using react three fiber and i have two components
one to make a box and the other is to make an array of this box
here's how they look like, the platform component:
export function Plat() {
const [active, setActive] = useState(0)
const { scale } = useSpring({ scale: active ? 1 : 0.4 })
const [mat] = useState(() => new THREE.TextureLoader().load('/matcap.png'))
function Shape(props) {
return (
<animated.mesh {...props} scale={scale} onPointerOver={() => setActive(Number(!active))} >
<RoundedBox args={[60,20,60]} radius={4} smoothness={4} position={[0, -10, 0]} dispose={null} >
<meshMatcapMaterial matcap={mat} />
</RoundedBox>
</animated.mesh>
);
}
useEffect(() => {
(function() {
setActive((1))
})();
},[]);
return (
<Shape />
)
}
and the component that makes an array of this platform:
import {Plat} from './Plat'
export default function Boxes() {
function MyShape(props) {
return (
<mesh {...props} >
<Plat />
</mesh>
);
}
const [shapes, setShapes] = useState([<MyShape key={0} position={[100, 100, 100]} />, <MyShape key={1} position={[120, 120, 120]} />, <MyShape key={2} position={[130, 130, 130]} />]);
return (
<group >
{[...shapes]}
</group>
)
}
(I have more than 3 elements in the array)
I wanna know if there's a way to access the variables inside each of the array's platform components
how would I do something like this:
console.log(shapes[2].position) or change the position of this specific shape in the array
or this
shapes[1].setActive(1)
is it even possible?
Few pointer
Avoid nesting functional components
Plat
Shape // declare this outside and pass it the params from parent
If you need to change the position of the children, consider creating a array with the information you need to change
const [shapes, setShapes] = useState<>([{ key: 1, position: { x: 5, y: 10 } }])
const changeShapePostions = (key) => {
setShapes(items => {
return items.map(shape => {
if (shape.id === key) {
return { ...shape, position: { x: updatedX, y: updated: y} }
} else {
return shape
}
}
}
}
const setActive = (key) => {
setShapes(items => {
return items.map(shape => {
if (shape.id === key) {
return { ...shape, isActive: true }
} else {
return shape
}
}
}
}
return (
<group >
{
shapes.map(shape => {
return (<MyShape key={shape.key} position={shape.position} setActive={setActive} />)
}
}
</group>
)
you can check standard practices on working with arrays
Hope it helps in some ways

Item is dropped in wrong position using react-grid-layout on ResponsiveReactGridLayout

I am new to react-grid-layout, I have created a sample example with drag and drop using this example https://react-grid-layout.github.io/react-grid-layout/examples/15-drag-from-outside.html. When the item is dropped it’s always placed to the first column of the grid. X and Y are correct from the alert,
What is wrong in my code?
This is my code:
onDrop = (layout, layoutItem, _event) => {
alert(`Dropped element props:\n${JSON.stringify(layoutItem, ['x',
'y', 'w', 'h'], 2)}`);
this.setState(prevState => ({
layouts: {
...prevState.layouts,
[prevState.currentBreakpoint]: [
...prevState.layouts[prevState.currentBreakpoint],
layoutItem
]
}
}));
};
render() {
return (
<div>
<div className="toolBar">
<div
className="droppable-element"
draggable={true}
unselectable="on"
>
Button 1
</div>
</div>
<div>
<ResponsiveReactGridLayout
{...this.props}
layouts={this.state.layouts}
onBreakpointChange={this.onBreakpointChange}
onLayoutChange={this.onLayoutChange}
onDrop={this.onDrop}
droppingItem={this.props.item}
measureBeforeMount={false}
useCSSTransforms={this.state.mounted}
compactType={this.state.compactType}
preventCollision={true}
isDroppable={true}
>
{this.generateDOM()}
</ResponsiveReactGridLayout>
</div>
</div>
);
}
I figured out. I added data-grid={el} to the element which should be dropped. This is the code.
import React from "react";
import _ from "lodash";
import { Responsive, WidthProvider } from "react-grid-layout";
const ResponsiveReactGridLayout = WidthProvider(Responsive);
export default class DragFromOutsideLayout extends React.Component {
static defaultProps = {
className: "layout",
rowHeight: 30,
onLayoutChange: function () { },
cols: { lg: 8, md: 5, sm: 6, xs: 4, xxs: 2 },
verticalCompact: false,
preventCollision: true
};
state = {
currentBreakpoint: "lg",
compactType: "horizontal",
mounted: false,
layouts: { lg: generateLayout() },
newCounter: 0
};
generateDOM(el) {
return (
<div key={el.i} data-grid={el}>
<span className="text">{el.i}</span>
</div>
);
}
onBreakpointChange = breakpoint => {
this.setState({
currentBreakpoint: breakpoint
});
};
onCompactTypeChange = () => {
const { compactType: oldCompactType } = this.state;
const compactType =
oldCompactType === "horizontal"
? "vertical"
: oldCompactType === "vertical"
? null
: "horizontal";
this.setState({ compactType });
};
onDrop = (layout, layoutItem, _event) => {
this.setState({
layouts: {
lg: this.state.layouts.lg.concat({
i: this.state.newCounter.toString(),
x: layoutItem.x,
y: layoutItem.y,
w: layoutItem.w,
h: layoutItem.h
})
},
newCounter: this.state.newCounter + 1
});
};
render() {
return (
<div>
<div className="toolBar">
<div
className="droppable-element"
draggable={true}
unselectable="on"
>
Button 1
</div>
</div>
<div className="space"></div>
<div className="gridL">
<span>Drop the item here</span>
<ResponsiveReactGridLayout
{...this.props}
onDrop={this.onDrop}
droppingItem={this.props.item}
measureBeforeMount={false}
useCSSTransforms={this.state.mounted}
compactType={this.state.compactType}
preventCollision={true}
isDroppable={true}
>
{_.map(this.state.layouts.lg, el => this.generateDOM(el))}
</ResponsiveReactGridLayout>
</div>
</div>
);
}
}
function generateLayout() {
return _.map(_.range(0, 0), function (item, i) {
var y = Math.ceil(Math.random() * 4) + 1;
return {
x: Math.round(Math.random() * 5) * 2,
y: Math.floor(i / 6) * y,
w: 2,
h: y,
i: i.toString(),
static: Math.random() < 0.05
};
});
}
const containerElement = document.getElementById('root');
ReactDOM.render(<DragFromOutsideLayout />, containerElement);

React doesn't see mapped props

I have this message in browser:
TypeError: data.map is not a function
I am passing an array from another component here as props. What I am doing wrong?
Thank you in advance!
EDITED:
event-data.js
const months = ['January', 'February', 'March'];
const eventType = ['Party', 'Karaoke', 'Concert'];
const monthObject = [
{ id: 'sort-by-month' },
{ name: 'By month' },
{ values: months },
];
const eventObject = [
{ id: 'sort-by-category' },
{ name: 'By category' },
{ values: eventType },
];
const eventData = { monthObject, eventObject };
event-filter-bar.js
import eventData from '../../data/event-data';
class EventFilterBar extends React.Component {
render() {
return (
<FilterToolbar data={eventData} />
);
}
}
filter-toolbar.js
class FilterToolbar extends Component {
render() {
const { data } = this.props;
return (
<ButtonToolbar className="justify-content-center">
<DropdownMaker data={data} />
<DropdownWithDate />
<ResetButton />
</ButtonToolbar>
);
}
}
FilterToolbar.propTypes = {
data: PropTypes.array.isRequired,
};
dropdown-maker.js
class DropdownMaker extends Component {
render() {
const { data } = this.props;
const eventFilters = data.map((e) => (
<DropdownMenu
id={e.id}
name={e.name}
values={e.values}
key={e.id}
/>
));
return (
{ eventFilters }
);
}
}
DropdownMaker.propTypes = {
data: PropTypes.array.isRequired,
};
Check if the data is actually populated or not before map through it.
class DropdownMaker extends Component {
render() {
const { data } = this.props;
const eventFilters = (data && data.length > 0) && data.map((e) => (
<DropdownMenu
id={e.id}
name={e.name}
values={e.values}
key={e.id} //<-- don't forget to add a unique key prop while use loop
/>
));
return (
{ eventFilters }
);
}
}
DropdownMaker.propTypes = {
data: PropTypes.array.isRequired,
};
Feel free to comment if it's not working.

Victory - data visualization

I am trying to use Victory for my React project but it is not working for some reason.
The code I am using is:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
data: this.getData()
};
}
componentDidMount() {
this.setStateInterval = window.setInterval(() => {
this.setState({
data: this.getData()
});
}, 3000);
}
componentWillUnmount() {
window.clearInterval(this.setStateInterval);
}
getData() {
const bars = random(6, 10);
return range(bars).map((bar) => {
return {x: bar + 1, y: random(2, 10)};
});
}
render() {
return (
<VictoryChart
domainPadding={{ x: 20 }}
animate={{duration: 500}}
>
<VictoryBar
data={this.state.data}
style={{
data: { fill: "tomato", width: 12 }
}}
animate={{
onExit: {
duration: 500,
before: () => ({
_y: 0,
fill: "orange",
label: "BYE"
})
}
}}
/>
</VictoryChart>
);
}
}
ReactDOM.render(<App/>, mountNode)
where I have used most components in different parts of my function.
The error I am getting is:
Line 28:18: 'random' is not defined no-undef
Line 29:12: 'range' is not defined no-undef
Line 30:30: 'random' is not defined no-undef
Search for the keywords to learn more about each error.
I don't know what to import as I have just added the Victory components
Here's an alternate function to the one you were using, it wasn't valid javascript. Where'd you get it from?
const getData = () => {
let arr = [];
for (let i = 0; i < 15; i++) {
arr.push({ x: x[i], y: y[Math.random(2, 10)] });
}
return arr;
};

Updating state on first render only?

Using react-native, I'm creating sub-Components within the parent App and providing their position to the array this.state.objLocation within the parent App.
I can get the initial location data into the array straight after the render, but because my subcomponents are draggable, each time they re-render on drag, it adds a new position object to the array.
I'd like to avoid this, and I thought that creating this.state = { firstRender: true } in the constructor and then using componentDidMount = () => { this.setState({ firstRender: false }) } after the first render would allow me to create a 'gate' to stop the addition of the extra position objects.
I can see that if I comment out //componentDidMount = () => { this.setState({ firstRender: false }) } then I will get multiple entries to my array but if it's included in the class I get absolutely none.
So possibly my interpretation of the render lifecycle and componentDidMount is incorrect?
Here is my code.
// App
import React, { Component } from 'react';
import { View, Text, } from 'react-native';
import styles from './cust/styles';
import Draggable from './cust/draggable';
const dataArray = [{num: 1,id: 'A',},{num: 2,id: 'B',},{num: 3,id: 'Z',}]
export default class Viewport extends Component {
constructor(props){
super(props);
this.state = {
dID : null,
objLocation: [],
firstRender: true,
};
}
render(){
return (
<View style={styles.mainContainer}>
<View style={styles.draggableContainer}>
<Text>Draggable Container</Text> {dataArray.map( d => { return(
<Draggable
id={d.id}
onLayout={ e=> this.onLayout(e)}
onPanResponderGrant={(dID) =>this.setState({ dID })}
onPanResponderRelease={() => this.setState({dID: null})} /> ) })}
<View style={[styles.findPoint ]} />
</View>
<View style={styles.infoBar}>
<Text>{this.state.dID ? this.state.dID : ''}</Text>{this.compFrame()}
</View>
</View>
);
}
onLayout = (e) => {
if ( e && this.state.firstRender) {
const n = e.nativeEvent.layout;
const position = {
width: n.width,
height: n.height,
x: n.x,
y: n.y
}
console.log(position);
this.setState({
objLocation: this.state.objLocation.concat([position])
});
}
}
componentWillMount = () => {
console.log("START");
}
compFrame = () => {
return(
this.state.objLocation.map( d => {<View style={[styles.findPoint2,{left: d.x, top: d.y, width: d.width, height: d.height} ]} ></View>})
)
}
componentDidMount = () => {
this.setState({firstRender: true })
console.log(this.state.objLocation.length);
}
}
// Draggable
import React, { Component } from 'react';
import { Text, PanResponder, Animated } from 'react-native';
import styles from './styles';
class Draggable extends Component {
constructor(props) {
super(props);
this.state = {
pan: new Animated.ValueXY(),
};
this.panResponder = PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderGrant: () => {
this.props.onPanResponderGrant(this.props.id);
},
onPanResponderMove: Animated.event([ null, {
dx: this.state.pan.x,
dy: this.state.pan.y,
},
]),
onPanResponderRelease: () => {
Animated.spring(this.state.pan, { toValue: { x: 0, y: 0 } }).start();
this.props.onPanResponderRelease();
},
});
}
render() {
return (
<Animated.View
onLayout={ (e) => this.props.onLayout(e) }
{...this.panResponder.panHandlers}
style={[this.state.pan.getLayout(), styles.circleAlt, styles.position]}>
<Text style={styles.textAlt}>Drag me!</Text>
<Text style={styles.textNum}>{this.props.id}</Text>
</Animated.View>
);
}
componentDidMount = () => {
this.props.onLayout(this.props.dragEvent)
}
}
export default Draggable;
// Output of console.log
START xxx
0
{width:108,height:108,x:133.5,y:376.5}
{width:108,height:108,x:133.5,y:78.5}
{width:108,height:108,x:133.5,y:227.5}
You could set the firstRender state in onLayout function
onLayout = (e) => {
if ( e && this.state.firstRender) {
const n = e.nativeEvent.layout;
const position = {
width: n.width,
height: n.height,
x: n.x,
y: n.y
}
console.log(position);
this.setState({
firstRender: false,
objLocation: this.state.objLocation.concat([position])
});
}
}
According to the information provided by you, your onLayout function is called by the component so its not included in the component lifecycle process, so when the component completes its lifecycle it goes into componentDidMount after mounting (which is not calling onLayout func) & thus changed the firstRender state to false and hence when you drag the component each time it goes from true to false.
I hope this explains
I feel like I've hacked this, to get it to work, so please correct me as to correct procedure.
This is the onLayout method from the App. I've included an if statement that checks if the new positions array length is equal too the dataArray length that the draggable items are based on.
It looks like this.
onLayout = (e) => {
if ( this.state.objLocation.length != dataArray.length ) {
if ( e ) {
const n = e.nativeEvent.layout;
const position = {
width: n.width,
height: n.height,
x: n.x,
y: n.y
}
console.log(position);
this.setState({
objLocation: this.state.objLocation.concat([position])
});
}
}
}

Categories

Resources