Zoom and pan on a SVG map - reactjs

I have a SVG map as one of my react components, and I am trying to make it so you can zoom in on the map, and pan across so certain sections are easier to see. I have managed to implement being able to zoom in on the map, however the map just enlarges on the screen rather than zooming within the view box I have, and the zoom is quite sensitive and the map will often disappear if it zooms out too far. Does anyone have experience who can help with this. The code for the zoom came from someone who was using it to zoom in on images. `
Here is the code minus all the path data for the SVG as it was too many characters
import React, {useState, useRef, useEffect} from 'react'
export default function Map () {
const [pos, setPos] = useState({ x: 0, y: 0, scale: 1 });
const onScroll = (e) => {
const delta = e.deltaY * -0.01;
const newScale = pos.scale + delta;
const ratio = 1 - newScale / pos.scale;
setPos({
scale: newScale,
x: pos.x + (e.clientX - pos.x) * ratio,
y: pos.y + (e.clientY - pos.y) * ratio
});
};
return (
<>
<div className='viewBox'>
<div onWheelCapture={onScroll}>
<svg baseProfile="tiny" fill="#7c7c7c" height="1982" stroke="#ffffff" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" version="1.2" viewBox="0 0 1000 1982" width="1000" xmlns="http://www.w3.org/2000/svg" style={{transformOrigin: '0 0', transform: `translate(${pos.x}px, ${pos.y}px) scale(${pos.scale})`}}>

Related

Best way to make a custom smooth scroll and scrollto coexist (ReactJs - Framer motion)

Made a smoothscroll component using framer motion that's working well :
export default function SmoothScroll({ children }: Props) {
const { width } = useWindowSize();
const scrollContainer = useRef() as RefObject<HTMLDivElement>;
const [pageHeight, setPageHeight] = useState(0);
useEffect(() => {
setTimeout(() => {
// added a setTimeout so the page has the time to load and it still fits
const scrollContainerSize =
scrollContainer.current?.getBoundingClientRect();
scrollContainerSize && setPageHeight(scrollContainerSize.height);
}, 500);
}, [width]);
const { scrollY } = useScroll(); // measures how many pixels user has scrolled vertically
// as scrollY changes between 0px and the scrollable height, create a negative scroll value...
// ... based on current scroll position to translateY
const transform = useTransform(scrollY, [0, pageHeight], [0, -pageHeight]);
const physics = { damping: 15, mass: 0.17, stiffness: 55 }; // easing of smooth scroll
const spring = useSpring(transform, physics); // apply easing to the negative scroll value
return (
<>
<motion.div
ref={scrollContainer}
style={{ y: spring }} // translateY of scroll container using negative scroll value
className="app fixed overflow-hidden w-screen"
>
{children}
</motion.div>
<motion.div style={{ height: pageHeight }} />
</>
);
}
The thing is, I'd like to scrollTo sections of my page upon click on the navbar but don't really know how to implement it without removing the smoothScroll ...
Tried the following logic but obviously it did not work as the vanilla scroll has been hijacked :
const scrollToSection = (
e: React.MouseEvent<HTMLLIElement, globalThis.MouseEvent>,
anchor?: string
) => {
e.preventDefault();
if (!anchor) return;
const section = document.querySelector(anchor);
section?.scrollIntoView({ behavior: "smooth" });
};
Is it doable ?

Next.js + Framer Motion scroll progress animation

So I want to create a viewport scroll progress circle with Framer Motion in a Next.js app (using tailwind as CSS).
I used the example code from Framer Motion:
https://codesandbox.io/s/framer-motion-viewport-scroll-and-svg-path-animation-mwi35?from-embed=&file=/src/Example.tsx:686-1070
The code I have down here displays the circle, but when I enable strokeDasharray or pathLength (in the style attribute) it disappears. And on scroll I don't see anything happening.
I tried different svg versions, but I didn't get it working. I also looked at my json.config and compare it with the example, but since it uses TypeScript I could not figure it out.
import * as React from 'react';
import { useEffect, useState } from 'react';
import {
motion,
useViewportScroll,
useSpring,
useTransform,
} from 'framer-motion';
const CircleIndicator = () => {
const { scrollYProgress } = useViewportScroll();
const yRange = useTransform(scrollYProgress, [0, 0.9], [0, 1]);
const pathLength = useSpring(yRange, { stiffness: 400, damping: 90 });
useEffect(() => yRange.onChange((v) => setIsComplete(v >= 1)), [yRange]);
return (
<>
<svg className='fixed w-12 h-12 bottom-7 left-7' viewBox='0 0 60 60'>
<motion.path
fill='none'
strokeWidth='2'
stroke='white'
// strokeDasharray='0 1'
d='M 0, 20 a 20, 20 0 1,0 40,0 a 20, 20 0 1,0 -40,0'
style={
{
// pathLength,
// rotate: 90,
// translateX: 5,
// translateY: 5,
// scaleX: -1, // Reverse direction of line animation
}
}
/>
</svg>
</>
);
};
export default CircleIndicator;
It would be great if I could get the animation to work.
If your css codes have below
html,
body: {
width: 100%
}
delete it and try again.
I has the same problem and solved it this way.

React-hooks and d3.forceSimulation

I'm having some issues with my forceSimulation in React (using Hooks). The problem is that whenever data is changed the position of each bubble is translated into the middle of the screen, rather than considering its previous position (see gif), making the bubbles jump in an annoying way.
I do not have much experience with forceSimulation, d3 and react-hooks in conjunction, and its hard to find any similar examples; so I would really appreciate any guidance.
import React, { useEffect, useRef } from 'react'
import { forceSimulation, forceX, forceY, forceCollide, select } from 'd3'
export default function Bubbles({ data, width, height }) {
const ref = useRef()
const svg = select(ref.current)
useEffect(() => {
const simulation = forceSimulation(data)
.force('x', forceX().strength(0.02))
.force('y', forceY().strength(0.02))
.force(
'collide',
forceCollide((d) => {
return d.value * 20 + 3
}).strength(0.3)
)
simulation.on('tick', () =>
svg
.selectAll('circle')
.data(data)
.join('circle')
.style('fill', () => 'red')
.attr('cx', (d) => d.x)
.attr('cy', (d) => d.y)
.attr('r', (d) => d.value * 20)
)
}, [data])
return (
<svg
viewBox={`${-width / 2} ${-height / 2} ${height} ${width}`}
width={width}
height={height}
ref={ref}
style={{
marginRight: '0px',
marginLeft: '0px'
}}
></svg>
)
}
I think the problem here is that React renders the SVG, but D3 renders the circles. When React re-renders on update it creates the SVG and discards any children because it doesn't know about them.
Thus the data can't be bound to the DOM in the usual D3 way, but should be stored in your React component's state.

When ViewPager move to next slide then previous slide should decrease opacity value for every move

My requirement is when ViewPager click and drag image for next image then previous image should decrease opacity for every drag. This is my codesandbox code.
This image is small example of my requirement.
import React, { useRef } from 'react'
import clamp from 'lodash-es/clamp'
import { useSprings, animated } from 'react-spring'
import { useDrag } from 'react-use-gesture'
import './styles.css'
function Viewpager() {
const index = useRef(0)
const [props, set] = useSprings(alphabets.length, i => ({
x: i * window.innerWidth,
scale: 1,
opacity: 1,
display: 'block'
}))
const bind = useDrag(({ active, down, movement: [mx], direction: [xDir], distance, cancel }) => {
set(i => {
if (i < index.current - 1 || i > index.current + 1) return { display: 'none' }
const x = (i - index.current) * window.innerWidth + (down ? mx : 0)
const scale = down ? 1 - distance / window.innerWidth / 2 : 1
const opacity = active ? distance * 0.01 : 1
return { x, scale, opacity, display: 'block' }
})
})
return props.map(({ x, display, scale, opacity }, i) => (
<animated.div
{...bind()}
key={i}
style={{
opacity,
display,
x
}}
/>
))
}
I need same functionality like codesandbox code and reduce opacity parallely when drag card to other card. If Anyone have any other solution in other library like framer-motion. That is also helpful.
You should interpolate the x value to the opacity. The x value is between 0 and negative innerWitdh / 2. You should transfer it to get a value between 1 to 0.
Here is the working formula.
opacity: x.to(x => 1 + x / (window.innerWidth / 2)),
Here is the example:
https://codesandbox.io/s/card-slider-jwf3f
Side Note:
You can check, the formula easily if you display it on screen. All you have to do is put it as the text of an animated.div. For example:
<animated.div>{x.to(x => 1 - Math.abs(x / (window.innerWidth / 2)))}</animated.div>

How to create drag behaviour for overlapping SVG elements in React

Problem illustrated in https://codesandbox.io/s/dragging-overlapping-svgs-1jd5m
I would like to do something quite simple: implement drag behaviour in my SVG elements in my React App. So my initial thoughts were to capture start and stop dragging by onMouseDown and onMouseUp events, and to implement the updating in the onMouseMove event. In principle, like so:
<circle
...
onMouseDown={() => startDrag()}
onMouseUp={() => endDrag()}
onMouseMove={event => dragging && drag(event)}
/>
In my React class Circ I implemented the behaviours like so:
const Circ = ({ cx, cy, r, fill, stroke }) => {
const [draggingPosition, setDraggingPosition] = useState({ x: 0, y: 0 });
const startDrag = () => {
setDragging(true);
};
const endDrag = () => {
setDragging(false);
};
const drag = event => {
event.stopPropagation();
setDraggingPosition({
x: draggingPosition.x + event.movementX,
y: draggingPosition.y + event.movementY
});
};
...
}
Well, that basically worked. But now when my SVG contains several overlapping elements, it starts to break down:
const SVG = () => (
<svg width={617} height={316} viewBox="0 0 400 400">
<g>
<Circ cx={108} cy={108} r="100" fill="#0ff" stroke="#000" />
<Circ cx={180} cy={209} r="100" fill="#ff0" stroke="#000" />
<Circ cx={220} cy={109} r="100" fill="#f0f" stroke="#000" />
</g>
</svg>
);
When I drag the blue circle, its behaviour is fine until an overlapping circle captures the mouse event and my dragging stops.
So my question is: how do I make sure that once I start dragging, the mouse event is restricted to only that current element? Ideally, also when there are other elements in the editor (Rect, G, etc...) that my Circ class does not necessarily know about?
I am now solving it with registering dragging at the global level and a
pointerEvents={ globalDragging ? "none" : "auto"}
on all my elements. I'm not really happy with this solution, so I'm still happy to hear alternatives.

Resources