Trying to animate a bunch of objects to their positions, and useSpring works for me (for single element), but useSprings is not for 3 objects.
Here is demo (mousedown red ball):
https://codesandbox.io/s/try-to-do-zgit5?file=/src/App.js:1254-1857
Here is code:
// working for me
const { z } = useSpring({
from: { z: 0 },
to: { z: mousedown ? 0 : -0.5 }
})
// not working with state change
// although working with a
// loop: { reverse: true, delay: 0 },
// but not accounting state changes
const [springs] = useSprings(3, (i) => ({
from: { x: 0, y: i * 2 - 2 },
to: { x: mousedown ? 0.5 : -0.5, y: i * 2 - 2 + 1 },
}))
and the elements:
// works
<a.mesh position-z={z}
onPointerDown={(e) => setMousedown(true)}
onPointerUp={(e) => setMousedown(false)}>...</a.mesh>
// not changing positions with state
{springs.map(({ x, y }, index) => (
<a.mesh position-x={x} position-y={y} key={`0${index}`}>...</a.mesh>
))}
As im new to react-spring, this might be a ridiculous simple problem, but im just stuck with this myself now
Solved thanx to Paul Henschel in this sandbox https://codesandbox.io/s/try-to-do-forked-gwy21?file=/src/App.js , addition of [mousedown] dependency parameter to useSprings hook was needed.
Related
I've recently started working with D3 and I am moving all my existing charts over from Chartjs and so far my attempts have been successful. There is this one chart however that I am unable to produce exactly the same way in D3.
So with Chartjs, there's properties built in to the library that we can use to set the colors for values above and below a certain point on a Line chart. Here's what I had used to get the intended chart with Chartjs:
...config,
fill: {
above: '#4E4AFF20',
below: '#FF515114',
target: 'origin'
},
...config
And this is what the chart in Chartjs ended up looking like:
But D3 doesn't seem to have such a thing as far as I can tell. There's only gradients. So here's what I was able to build in D3:
As you can see, this looks way different from what I had earlier with Chartjs. Also notice how the gradient exists in both the line and the colored area underneath. I know it's there because I added it but that's not what I want and everywhere I look, that's the only way people are doing it. I have done countless attempts to fix this to no avail hence now I'm here asking for your help. Here's the D3 code I have right now:
import * as d3 from 'd3';
import { useEffect, useRef } from 'react';
interface Data {
x: number;
y: number;
}
const width = 350;
const height = 117;
const zeroPoint = 0;
const data: Data[] = [
{ x: 0, y: -20 },
{ x: 10, y: -20 },
{ x: 20, y: -20 },
{ x: 40, y: -20 },
{ x: 50, y: -20 },
{ x: 60, y: -20 },
{ x: 70, y: -20 },
{ x: 80, y: 0 },
{ x: 90, y: 20 },
{ x: 100, y: 20 },
{ x: 110, y: 20 },
{ x: 120, y: 20 },
{ x: 130, y: 20 },
{ x: 140, y: 20 },
{ x: 150, y: 20 }
];
export const Chart: React.FC = () => {
const ref = useRef<SVGSVGElement>(null);
const generateLinePath = (
element: d3.Selection<SVGSVGElement, unknown, null, undefined>,
data: Data[],
xScale: d3.ScaleLinear<number, number>,
yScale: d3.ScaleLinear<number, number>
) => {
const lineGenerator = d3
.line<Data>()
.x(d => xScale(d.x))
.y(d => yScale(d.y));
element.append('path').attr('d', lineGenerator(data));
};
const drawZeroLine = (element: d3.Selection<SVGSVGElement, unknown, null, undefined>, yScale: d3.ScaleLinear<number, number>) => {
element
.append('line')
.attr('x1', '0')
.attr('y1', yScale(zeroPoint))
.attr('x2', width)
.attr('y2', yScale(zeroPoint))
.attr('stroke', '#c4c4c4');
};
const createChart = (data: Data[]) => {
const svg = d3.select(ref.current!).attr('viewBox', `0 0 ${width} ${height}`);
svg.selectAll('*').remove();
const [minX, maxX] = d3.extent(data, d => d.x);
const [minY, maxY] = d3.extent(data, d => d.y);
const xScale = d3.scaleLinear().domain([minX!, maxX!]).range([0, width]);
const yScale = d3
.scaleLinear()
.domain([minY!, maxY!])
.range([height, 0]);
svg
.append('linearGradient')
.attr('id', 'line-gradient')
.attr('gradientUnits', 'userSpaceOnUse')
.attr('x1', 0)
.attr('x2', width)
.selectAll('stop')
.data(data)
.join('stop')
.attr('offset', d => xScale(d.x) / width)
.attr('stop-color', d => (d.y < zeroPoint ? '#FF5151' : '#4E4AFF'));
svg
.append('linearGradient')
.attr('id', 'area-gradient')
.attr('gradientUnits', 'userSpaceOnUse')
.attr('x1', xScale(data[0].x))
.attr('x2', xScale(data[data.length - 1].x))
.selectAll('stop')
.data([
{ color: '#FF515110', offset: '0%' },
{ color: '#4E4AFF20', offset: '100%' }
])
.enter()
.append('stop')
.attr('offset', function (d) {
return d.offset;
})
.attr('stop-color', function (d) {
return d.color;
});
svg.attr('stroke', 'url(#line-gradient)').attr('fill', 'url(#area-gradient)');
generateLinePath(svg, data, xScale, yScale);
drawZeroLine(svg, yScale);
};
useEffect(() => {
createChart(data);
}, []);
return <svg ref={ref} />;
};
So there's two problems I am looking to get solved with your help. The more important one is to give different colors to areas under and above the zero line in D3 the way I was able to do with Chartjs and the other one is moving away from gradients and get solid colors without any smooth transitions on both the line and the colored areas underneath.
Alright I managed to recreate the same chart in D3 using a workaround.
So it's not as straightforward as it's in Chartjs but it works pretty well. The idea is to create polygons under and over the line using the same data used to generate the line.
So my chart works like this. The grey line is a straight zero line and the values below zero go under that line with a red color and the ones above are purple. And here's what the Chart data looks like:
data = [
{ x: 0, y: 0 },
{ x: 1, y: 1 },
...
]
Anyways, here's the steps
Generate the scales
const [minX, maxX] = d3.extent(data, d => d.x);
const [minY, maxY] = d3.extent(data, d => d.y);
const xScale = d3.scaleLinear().domain([minX, maxX]).range([0, width]);
const yScale = d3.scaleLinear().domain([minY, maxY]).range([height, 0]);
Generate the line chart using D3's line() function. And don't give any stroke to the generated line.
const lineGenerator = d3.line().x(d => xScale(d.x)).y(d => yScale(d.y));
element.append('path').attr('d', lineGenerator(data));
Add a red SVG polygon that starts from the left side at zero line, then goes to the left bottom, then to where the value starts becoming negative and then finally to where the line reaches the zero line again.
svg.append('polygon').attr(
'points',
`
${xScale(minX - 1)},${yScale(0)} // top left point
${xScale(minX - 1)},${yScale(minY)} // bottom left point
${xScale(data[indexWhereRedPointStartsBecomingPositive].x)},${yScale(data[indexWhereRedPointStartsBecomingPositive].y)} // bottom right point
${xScale(data[indexWhereXReachesZeroLine].x)},${yScale(0)} // top right point
`
)
.attr('fill', 'lightRed')
.attr('stroke', 'darkRed');
Notice how we gave the red stroke to the polygon? That's the reason why we got rid of the stroke from the line and gave it here instead. This is because we need two separate colors (red for below and purple for above) for the chart. The reason why we do minX - 1 is because the stroke is applied to all four sides of the polygon and we want to hide it from the left side so we subtract 1px from the left.
Add another purple SVG polygon that starts from the left side at zero line (where the purple area starts somewhere in the middle), then goes all the way to the right end of the chart and then goes up to the top.
svg.append('polygon').attr(
'points',
`
${xScale(data[indexWhereValueStartsGoingPositive].x)},${yScale(0)}
${width + 1},${yScale(data[data.length - 1].y)}
${width + 1},${yScale(0)}
`
)
.attr('fill', 'lightPurple')
.attr('stroke', 'darkPurple');
Here we do width + 1 to hide the stroke of this purple polygon on the right side the same way we did minX - 1 with the left side of the red box.
So in conclusion, instead of giving stroke to the line generated using d3.line(), give strokes to the two polygons created using the same data that was used to generate the line chart and create the polygons 1px larger than the chart data so the strokes don't appear on the left and right side of the charts.
That's quite a lot I know but I couldn't think of any other way to get the chart to look like this. Anyways, I hope this helps anyone else experiencing a similar problem.
Im using library keen-slider, all the slides have the same size but the active. I want that all the slides to have the same size but the active one, which always must be a bit bigger than the rest.
I already tried this using slider.update:
slider.update(() => content.map(
(_, index) => {
if (index === cardInView) {
return {
size: 0.4,
spacing: 0.05,
}
}
return {
size: 0.2,
spacing: 0.05,
}
}
),
defaultAnimation: {
duration: 2000,
}, idx)
It kind of worked but it lost the animation. Anyone know how could i achieve this?
I have a code as below. Although I update the sphere position with every frame (useFrame), it does not reflect on my scene. Can someone please help me understand why it will not work.
PS : I am new to this and am trying to do some quick proof of concepts.
function Marble() {
const controls = useControls()
const [sphereRef, sphereApi] = useSphere(() => ({
type: "Dynamic",
mass: 1,
position: [0, 2, 0]
}));
//sphereApi.position.set([0,0,0])
//console.log("SHM sphereAPI position", sphereRef.current.position);
useFrame(() => {
const { forward, backward, left, right, brake, reset } = controls.current
if (forward == true) {
console.log("sphereRef position", sphereRef.current.position);
console.log("sphereAPI position", sphereApi.position);
//console.log("model position", model.current.position)
// sphereApi.velocity.set(2,2,2);
sphereApi.position.set(5,0,0)
// sphereRef.current.position.set(5,0,0);
}
})
return (
<mesh ref={sphereRef} castShadow>
<sphereBufferGeometry attach="geometry" args={[1, 32, 32]}></sphereBufferGeometry>
<meshStandardMaterial color="white" />
</mesh>
);
})
( See it online: Stackblitz )
Doing sphereApi.position.set(5,0,0) on every frame, just sets the sphere position to x=5 on every frame.
So you should create a state first to store the x position, then update it on every frame to +=5, and then set the sphere position to it:
const [sphereX, setSphereX] = useState(0);
useFrame(() => {
setSphereX((sphereX) => sphereX + 0.05); // set state x position to +0.05 (5 is fast)
sphereApi.position.set(sphereX, 0, 0); // apply the state to the sphere position
});
Also make sure to use allowSleep={false} since changing position directly isn't a physical movement, so the physical scene may get sleep.
Online: Stackblitz
I want to tween my camera, from Position A to B. I did it a few times using react spring with a little workaround:
import { useSpring } from "react-spring/three";
const springProps = useSpring({
config: { duration: 1000, easing: easings.easeCubicInOut },
to: {
position: props.position,
lookAt: props.lookAt,
offset: props.offset,
},
onRest: (ya) => {
if (props.enableOrbit) {
props.parentStateModifier({ enableOrbit: true });
}
},
});
useFrame(({ clock, camera, mouse }) => {
camera.position.x = springProps.position.payload[0].value;
camera.position.y = springProps.position.payload[1].value;
camera.position.z = springProps.position.payload[2].value;
camera.lookAt(
springProps.lookAt.payload[0].value,
springProps.lookAt.payload[1].value,
springProps.lookAt.payload[2].value
);
camera.updateProjectionMatrix();
});
This was'nt a very good approach, but it worked.
I am now using #react-spring/three and its not working anymore. "payload" is undefined now and I was able to get the coordinates by calling springProps.x.animation.values[0]._value
But values is empty when not animating. I'm sure there must be a better way to animate camera.
I love react spring for animating three meshes. I hope I can use it for my camera as well.
Ok, I managed to update my code so that my camera is animating again:
const { gl, camera } = useThree();
const springProps = useSpring({
config: { duration: 1000, easing: easings.easeCubic },
from: {
x: - 0.1,
y: - 0.1,
z: - 0.1,
lookAtX: camera.lookAt.x - 0.1,
lookAtY: camera.lookAt.y - 0.1,
lookAtZ: camera.lookAt.z - 0.1,
},
to: {
x: cameraData.position[0],
y: cameraData.position[1],
z: cameraData.position[2],
lookAtX: cameraData.lookAt[0],
lookAtY: cameraData.lookAt[1],
lookAtZ: cameraData.lookAt[2],
}
});
useFrame((state, delta) => {
if (!orbit) {
camera.position.x = springProps.x.animation.values[0]._value;
camera.position.y = springProps.y.animation.values[0]._value;
camera.position.z = springProps.z.animation.values[0]._value;
camera.lookAt(
springProps.lookAtX.animation.values[0]._value,
springProps.lookAtY.animation.values[0]._value,
springProps.lookAtZ.animation.values[0]._value
);
}
});
Problem is, that if the from and to values are the same, values[0] will be undefined. So for example this will not work with my code:
from: {
x: 0
y: 0,
z: 0,
},
to: {
x: 0,
y: 2,
z: 4,
}
because x has the same value. I hope someone can provide a better way of animating cameras in r3f.
TL;DR: In #react-google-maps/api, I want to be able to make dynamic cluster icons/symbol in the style of pie charts, based on the markers in the cluster, but it seems I can only make icons from a static array, and cannot pass the the markers as parameters.
Full Description:
I am using typescript react with the package #react-google-maps/api, and I'm trying to find a way with the ClustererComponent/MarkerClusterer to take a callback or similar in order to be able to be able to create an svg for each cluster based on the markers in the given cluster.
The current issue is that the way I understand it, I am limited to a static array of urls to icons, and thought I can make an svg in those, I have no way to pass parameters into those svgs, as the only way the package allows me to chose a style is thought index in the style array.
I have read thought the following material, but have not been able to get find a way to make an icon dynamically based on the markers:
Documentation for #react-google-maps/api: https://react-google-maps-api-docs.netlify.app/#markerclustere
Documentation for google maps markerclusterer: https://developers.google.com/maps/documentation/javascript/marker-clustering
I have found libraries like this: https://github.com/hassanlatif/google-map-chart-marker-clusterer, that should be able to be used as a solution, but they don't seem to work with the #react-google-maps/api, only with earlier versions of google map. If this is not the case, and these can be used directly, then I would be more then happy with an answer describing how to use libraries like the one above with #react-google-maps/api, as that should allow be to make clusters in the same way as the picture below.
EDIT: as I got reminded in the comments, here is the code I have so far:
What I've tried: I have tried to find any way to set in an svg element instead of a url, but have since just decided to make a url with the svg data, as shown below. I have tried to edit the url of the clusters under the MarkerClusterer thought the callback for onClusteringBegin, onClusteringEnd and onLoad, but so far, no luck.
How I make the svg into url-data, so it can be used for img src
/*
* Pie Chart SVG Icon in URL form
*
* Inspiration taken from: https://medium.com/hackernoon/a-simple-pie-chart-in-svg-dbdd653b6936
*
* Note: As of right now, I am identifying the difference in marker types by setting the type-number I use in the title of the marker
*/
const serializeXmlNode = (xmlNode: any) => {
if (typeof window.XMLSerializer != "undefined") {
return (new window.XMLSerializer()).serializeToString(xmlNode);
} else if (typeof xmlNode.xml != "undefined") {
return xmlNode.xml;
}
return "";
}
function getCoordinatesForPercent(percent: number) {
const x = Math.cos(2 * Math.PI * percent);
const y = Math.sin(2 * Math.PI * percent);
return [x, y];
}
const makePieChartIcon = (slices: any[]) => {
const svgNS = 'http://www.w3.org/2000/svg';
var svg = document.createElementNS(svgNS, 'svg')
svg.setAttribute('viewBox', '-1.1 -1.1 2.2 2.2')
svg.setAttribute('style', 'transform: rotate(-90deg)')
svg.setAttribute('height', '60')
var circle = document.createElementNS(svgNS, 'circle')
circle.setAttribute('r', '1.1')
circle.setAttribute('fill', 'white')
svg.appendChild(circle);
let cumulativePercent = 0;
slices.map((slice: any) => {
const [startX, startY] = getCoordinatesForPercent(cumulativePercent);
cumulativePercent += slice.percent;
const [endX, endY] = getCoordinatesForPercent(cumulativePercent);
const largeArcFlag = slice.percent > .5 ? 1 : 0;
const pathData = [
`M ${startX} ${startY}`, // Move
`A 1 1 0 ${largeArcFlag} 1 ${endX} ${endY}`, // Arc
`L 0 0`, // Line
].join(' ');
const path = document.createElementNS(svgNS, 'path');
path.setAttribute('d', pathData);
path.setAttribute('fill', slice.color);
svg.appendChild(path);
})
var svgUrl = 'data:image/svg+xml;charset=UTF-8,' + serializeXmlNode(svg)
return svgUrl
}
const makeDynamicClusterIcon = (markers: any[]) => {
var numMarkers = markers.length;
var slices = markers.reduce((acc: any, marker: any) => {
acc[parseInt(marker.title)].percent += 1 / numMarkers;
return acc;
}, [
{ percent: 0, color: 'Green' },
{ percent: 0, color: 'Blue' },
{ percent: 0, color: 'Red' },
])
var newIconURL = makePieChartIcon(slices)
return newIconURL;
}
How I use the MarkerClusterer Component
<MarkerClusterer
options={{
averageCenter: true,
styles: clusterStyles,
}}
>
{(clusterer) =>
markerData.map((marker: any) => (
<Marker
key={marker.key}
title={String(marker.type)}
position={{ lat: marker.lat, lng: marker.lng }}
clusterer={clusterer}
/>
))
}
</MarkerClusterer>
Right now, I can only use some static styles, but I have them as the following for testing:
const clusterStyles = [
{
height: 50, textColor: '#ffffff', width: 50,
url: 'data:image/svg+xml;charset=UTF-8,%3Csvg xmlns="http://www.w3.org/2000/svg" height="50" width="100"%3E%3Ccircle cx="25" cy="25" r="20" stroke="black" stroke-width="3" fill="green" /%3E%3C/svg%3E',
},
{
height: 50, textColor: '#ffffff', width: 50,
url: 'data:image/svg+xml;charset=UTF-8,%3Csvg xmlns="http://www.w3.org/2000/svg" height="50" width="100"%3E%3Ccircle cx="25" cy="25" r="20" stroke="black" stroke-width="3" fill="red" /%3E%3C/svg%3E',
}
];
I found a solution, by finding out that the style array for each cluster (ClusterStyles) can be changed, and then I have change it with the data from the specific markers in the given cluster. I ended up doing this in the callback onClusteringEnd, as here:
{/* Added to the MarkerClusterer */}
onClusteringEnd={(clusterer) => {
clusterer.clusters.map((cluster) => {
cluster.clusterIcon.styles = makeDynamicClusterIcon(cluster.markers)
})
}}
And I changed the last line with return of the makeDynamicClusterIcon function I showed above to instead say:
return [{ url: newIconURL, height: 60, width: 60, textColor: '#FFFFFF', textSize: 22 }];