I'm trying to recreate a JS draggable slider in React/Next from this codepen but I just can't make it slide on mouse drag. Nothing happens on first click, but at the mouseUp event, the slider starts to slide and it won't stop until you reload the page.
Here is my codesandbox so far.
And here is the original from codepen:
const track = document.getElementById("image-track");
const handleOnDown = e => track.dataset.mouseDownAt = e.clientX;
const handleOnUp = () => {
track.dataset.mouseDownAt = "0";
track.dataset.prevPercentage = track.dataset.percentage;
}
const handleOnMove = e => {
if(track.dataset.mouseDownAt === "0") return;
const mouseDelta = parseFloat(track.dataset.mouseDownAt) - e.clientX,
maxDelta = window.innerWidth / 2;
const percentage = (mouseDelta / maxDelta) * -100,
nextPercentageUnconstrained = parseFloat(track.dataset.prevPercentage) + percentage,
nextPercentage = Math.max(Math.min(nextPercentageUnconstrained, 0), -100);
track.dataset.percentage = nextPercentage;
track.animate({
transform: `translate(${nextPercentage}%, -50%)`
}, { duration: 1200, fill: "forwards" });
for(const image of track.getElementsByClassName("image")) {
image.animate({
objectPosition: `${100 + nextPercentage}% center`
}, { duration: 1200, fill: "forwards" });
}
}
I just figured it out. I was using onClick to activate dragging functionality, which was wrong. The correct event is onMouseDown.
Related
I have an array of image urls. I want to display one image at a time on canvas. I've setup useEffect to be triggered when index of current image is changed.
I've researched online and found out that images load async and i must use onload callback to draw new image on canvas (Why won't my image show on canvas?).
I've setup effect and callback like this:
const [image, setImage] = useState<HTMLImageElement>()
useEffect(() => {
const image = new Image()
image.onload = () => update
image.src = images[imageState.imageIndex]?.url
setImage(image)
}, [imageState.imageIndex, images, update])
const update = useCallback(() => {
if (!canvasRef.current) return
const ctx = canvasRef.current.getContext('2d')
if (!ctx || !image) return
ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height)
canvasRef.current.width = image.width
canvasRef.current.height = image.height
ctx.drawImage(image, 0, 0)
}, [image])
canvas is setup like this:
export const ImageGallery = (props: ImageGalleryProps) => {
const classes = useImageGalleryStyles()
return (
<div className={classes.outerContainer}>
<canvas
style={{
left: `${imageState.position.x}px`,
top: `${imageState.position.y}px`,
transform: `rotate(${imageState.rotate}deg) scale(${imageState.scale})`,
}}
ref={canvas.ref}
className={classes.image}
/>
</div>
)
}
Images are changed like this:
Callback never called and canvas remains blank. What is proper way to update canvas?
Ended up with this:
useEffect(() => {
const image = new Image()
image.onload = function() {
if (!canvasRef.current) return
const ctx = canvasRef.current.getContext('2d')
if (!ctx || !image) return
ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height)
canvasRef.current.width = image.width
canvasRef.current.height = image.height
ctx.drawImage(image, 0, 0)
coordinates.map(c => {
switch (c.coordinates.$type) {
case DefectCoordinatesType.WideSide:
c.coordinates.WideSides.map(ws => {
if (image) {
ctx.beginPath()
ctx.imageSmoothingEnabled = false
ctx.moveTo(ws[0].x * image.width, ws[0].y * image.height)
ctx.lineTo(ws[1].x * image.width, ws[1].y * image.height)
ctx.closePath()
ctx.strokeStyle = 'red'
ctx.lineWidth = 2
ctx.stroke()
}
})
}
})
}
image.src = images[imageState.imageIndex]?.url
}, [checkBoundaries, coordinates, imageState.imageIndex, images, prevIndex, zoomOut])
I am making a chess game, and I am using React Drag n Drop for the piece movement. I have a problem where the drag image is not scaling with the window size. The image stays at a static 60x60px (the same as the image I am serving from my static folder to the client).
Here is a gif of the problem so you can see:
Drag image not scaling with original image
Here is a code snippet of the "Piece" component:
import React, { useState, useEffect } from "react";
// React DnD Imports
import { useDrag, DragPreviewImage, DragLayer } from "react-dnd";
function Piece({ piece }) {
const { imageFile } = piece;
// drag and drop configuration
const [{ isDragging }, drag, preview] = useDrag({
item: { type: "piece", piece: piece },
collect: (monitor) => {
return { isDragging: !!monitor.isDragging() };
},
});
const opacityStyle = {
opacity: isDragging ? 0.5 : 1,
};
return (
<React.Fragment>
<DragPreviewImage connect={preview} src={imageFile} />
<img style={{ ...opacityStyle }} ref={drag} src={imageFile}></img>
</React.Fragment>
);
}
I've tried the following, but it doesn't work exactly right:
useEffect(() => {
const img = new Image();
img.src = imageFile;
const ctx = document.createElement("canvas").getContext("2d");
ctx.canvas.width = "100%";
ctx.canvas.height = "100%";
img.onload = () => {
ctx.drawImage(img, 0, 0, ctx.canvas.width, ctx.canvas.height);
img.src = ctx.canvas.toDataURL();
preview(img);
};
}, []);
Drag Image is transparent
If you start dragging between UseEffect calls, you start dragging the whole tile behind the image
I really would love a way to easily tell my DragPreviewImage to scale with the original image... Thank you!
EDIT:
I have discovered the previewOptions property! I got my preview image to center on drag by doing this, but I can't get it to increase in size. Code below:
function Piece({ piece }) {
const { imageFile } = piece;
const previewOptions = {
offsetX: 27,
offsetY: 28,
};
// drag and drop configuration
const [{ isDragging }, drag, preview] = useDrag({
item: { type: "piece", piece: piece },
previewOptions,
collect: (monitor) => {
return { isDragging: !!monitor.isDragging() };
},
});
const img = new Image();
img.src = imageFile;
preview(img, previewOptions);
const callbackRef = useCallback(
(node) => {
drag(node);
preview(node, previewOptions);
},
[drag, preview],
);
const opacityStyle = {
opacity: isDragging ? 0.5 : 1,
};
return (
<React.Fragment>
<DragPreviewImage connect={preview} src={imageFile} />
<img style={{ ...opacityStyle }} ref={drag} src={imageFile} />
</React.Fragment>
);
}
I've looked through the documentation for react-dnd and what I found is that the only way to make the preview image the size you want is to edit the source image itself. In my case the images are 500x500 while the board tile is 75x75, so by shrinking the image in Photoshop / GIMP it got to the size I wanted. However if you want it to be responsive I have no idea. I tried to add style={{height: , width: }} to the DraggablePreview but that had no effect, tried with scale() also no effect.
But if the chess board you have is static (width and height) editing the images would be best.
I have a question about React. How can I trigger a certain function in the useEffect hook, depending on the position of the cursor?
I need to trigger a function that will open a popup in two cases - one thing is to open a modal after a certain time (which I succeeded with a timeout method), but the other case is to trigger the modal opening function once the mouse is at the very top of the website right in the middle. Thanks for the help.
For now I have that, but I'm struggling with the mouse position part:
function App(e) {
const [modalOpened, setModalOpened] = useState(false);
console.log(e)
useEffect(() => {
const timeout = setTimeout(() => {
setModalOpened(true);
}, 30000);
return () => clearTimeout(timeout);
}, []);
const handleCloseModal = () => setModalOpened(false);
You could use a custom hook that checks the conditions you want.
I'm sure this could be cleaner, but this hook works.
export const useMouseTopCenter = () => {
const [position, setPosition] = useState({ x: 0, y: 0 });
const [isTopCenter, setIsTopCenter] = useState(false);
const width = window.innerWidth;
// Tracks mouse position
useEffect(() => {
const setFromEvent = (e) => setPosition({ x: e.clientX, y: e.clientY });
window.addEventListener("mousemove", setFromEvent);
return () => {
window.removeEventListener("mousemove", setFromEvent);
};
}, []);
// Tracks whether mouse position is in the top 100px and middle third of the screen.
useEffect(() => {
const centerLeft = width / 3;
const centerRight = (width / 3) * 2;
if (
position.x > centerLeft &&
position.x < centerRight &&
position.y < 100
) {
setIsTopCenter(true);
} else {
setIsTopCenter(false);
}
}, [position, width]);
return isTopCenter;
};
Then you would simply add const isCenterTop = useMouseTopCenter(); to your app component.
Checkout this code sandbox, which is just a variation of this custom hook.
It is regarding the following example:
https://codesandbox.io/s/cards-forked-4bcix?file=/src/index.js
I want a tinder like functionality where I can trigger the same transition as drag.
I am trying to add like and dislike button functionality like tinder, but since the buttons are not part of the useSprings props loop, it is hard to align which card I should transform. I want the like or dislike button to communicate with useDrag to trigger a drag, I have tried toggling by useState and passing as an argument of useDrag, and onClick handler on button which set(x:1000, y:0) but that removes all of the cards.
Spent a whole day figuring it out, and I need to deliver things very soon, help will be great please!
Below is the code, and I am using Next.js
import React, { useState, useRef } from "react";
import { useSprings, animated } from "react-spring";
import { useDrag } from "react-use-gesture";
const Try: React.SFC = () => {
const cards = [
"https://upload.wikimedia.org/wikipedia/en/5/53/RWS_Tarot_16_Tower.jpg",
"https://upload.wikimedia.org/wikipedia/en/9/9b/RWS_Tarot_07_Chariot.jpg",
"https://upload.wikimedia.org/wikipedia/en/d/db/RWS_Tarot_06_Lovers.jpg",
// "https://upload.wikimedia.org/wikipedia/en/thumb/8/88/RWS_Tarot_02_High_Priestess.jpg/690px-RWS_Tarot_02_High_Priestess.jpg",
"https://upload.wikimedia.org/wikipedia/en/d/de/RWS_Tarot_01_Magician.jpg",
];
const [hasLiked, setHasLiked] = useState(false);
const [props, set] = useSprings(cards.length, (i) => ({
x: 0,
y: 0,
}));
const [gone, setGone] = useState(() => new Set()); // The set flags all the cards that are flicked out
const itemsRef = useRef([]);
const bind = useDrag(
({
args: [index, hasLiked],
down,
movement: [mx],
distance,
direction: [xDir],
velocity,
}) => {
const trigger = velocity > 0.2;
const dir = xDir < 0 ? -1 : 1;
if (!down && trigger) gone.add(index); // If button/finger's up and trigger velocity is reached, we flag the card ready to fly out
set((i) => {
if (index !== i) return; // We're only interested in changing spring-data for the current spring
const isGone = gone.has(index);
const x = isGone ? (200 + window.innerWidth) * dir : down ? mx : 0; // When a card is gone it flys out left or right, otherwise goes back to zero
const rot = mx / 100 + (isGone ? dir * 10 * velocity : 0); // How much the card tilts, flicking it harder makes it rotate faster
const scale = down ? 1.1 : 1; // Active cards lift up a bit
return {
x,
rot,
scale,
delay: undefined,
config: { friction: 50, tension: down ? 800 : isGone ? 200 : 500 },
};
});
if (!down && gone.size === cards.length)
setTimeout(() => gone.clear(), 600);
}
);
console.log(gone);
function handleLikeButtonClick(e) {
gone.add(1);
}
function handleDisLikeButtonClick(e) {
set({ x: -1000, y: 0 });
}
return (
<>
<div id="deckContainer">
{props.map(({ x, y }, i) => (
<animated.div
{...bind(i, hasLiked)}
key={i}
style={{
x,
y,
}}
ref={(el) => (itemsRef.current[i] = el)}
>
<img src={`${cards[i]}`} />
</animated.div>
))}
</div>
<div className="buttonsContainer">
<button onClick={(e) => handleLikeButtonClick(e)}>Like</button>
<button onClick={(e) => handleDisLikeButtonClick(e)}>Dislike</button>
</div>
<style jsx>{`
.buttonsContainer {
background-color: tomato;
}
#deckContainer {
display: flex;
flex-direction: column;
align-items: center;
width: 100vw;
height: 100vh;
position: relative;
}
`}</style>
</>
);
};
export default Try;
done needed to be smart with js:
function handleLikeButtonClick(e) {
let untransformedCards = [];
//loop through all cards and add ones who still have not been transformed
[].forEach.call(itemsRef.current, (p) => {
if (p.style.transform == "none") {
untransformedCards.push(p);
}
});
// add transform property to the latest card
untransformedCards[untransformedCards.length - 1].style.transform =
"translate3d(1000px, 0 ,0)";
}
function handleDisLikeButtonClick(e) {
let untransformedCards = [];
[].forEach.call(itemsRef.current, (p) => {
if (p.style.transform == "none") {
untransformedCards.push(p);
}
});
untransformedCards[untransformedCards.length - 1].style.transform =
"translate3d(-1000px, 0 ,0)";
}
I am trying to make a finger-painting canvas, which should work on mobile browsers.
My canvas is working properly on pc browsers (by mouse), but not on mobile browsers. The strokes just disappear even if my finger did not leave the screen.
Is it because the swipe gestures in Chrome (which I am using) overwrite the onPointerMove event on some time point?
You can try my canvas here (my server is not always on). The whole project contains too much code unrelated to this issue so I am not posting those code here.
And my code for Canvas.js is like this:
// canvas.js
// modified from https://pusher.com/tutorials/live-paint-react
import React, { Component } from 'react';
class Canvas extends Component {
constructor(props) {
super(props);
this.onPointerDown = this.onPointerDown.bind(this);
this.onPointerMove = this.onPointerMove.bind(this);
this.endPaintEvent = this.endPaintEvent.bind(this);
}
isPainting = false;
userStrokeStyle = '#EE92C2';
line = [];
prevPos = { offsetX: 0, offsetY: 0 };
onPointerDown({ nativeEvent }) {
const { offsetX, offsetY } = nativeEvent;
this.isPainting = true;
this.prevPos = { offsetX, offsetY };
}
onPointerMove({ nativeEvent }) {
if (this.isPainting) {
const { offsetX, offsetY } = nativeEvent;
const offSetData = { offsetX, offsetY };
// Set the start and stop position of the paint event.
const positionData = {
start: { ...this.prevPos },
stop: { ...offSetData },
};
// Add the position to the line array
this.line = this.line.concat(positionData);
this.paint(this.prevPos, offSetData, this.userStrokeStyle);
}
}
endPaintEvent() {
if (this.isPainting) {
this.isPainting = false;
//this.sendPaintData();
}
}
paint(prevPos, currPos, strokeStyle) {
const { offsetX, offsetY } = currPos;
const { offsetX: x, offsetY: y } = prevPos;
this.ctx.beginPath();
this.ctx.strokeStyle = strokeStyle;
// Move the the prevPosition of the mouse
this.ctx.moveTo(x, y);
// Draw a line to the current position of the mouse
this.ctx.lineTo(offsetX, offsetY);
// Visualize the line using the strokeStyle
this.ctx.stroke();
this.prevPos = { offsetX, offsetY };
}
componentDidMount() {
// Here we set up the properties of the canvas element.
this.canvas.width = 1000;
this.canvas.height = 800;
this.ctx = this.canvas.getContext('2d');
this.ctx.lineJoin = 'round';
this.ctx.lineCap = 'round';
this.ctx.lineWidth = 5;
}
render() {
return (
<canvas
// We use the ref attribute to get direct access to the canvas element.
ref={(ref) => (this.canvas = ref)}
style={{ background: 'black' }}
onPointerDown={this.onPointerDown}
onPointerUp={this.endPaintEvent}
onPointerMove={this.onPointerMove}
/>
);
}
}
export default Canvas;
I tried to disable the scrolling and zooming by these code:
index.css
html, body {
overflow-x:hidden;
overflow-y:hidden;
}
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
position: relative;
}
index.html
<script>
document.body.addEventListener("touchstart", function (e) {
if (e.target == canvas) {
e.preventDefault();
}
}, false);
document.body.addEventListener("touchend", function (e) {
if (e.target == canvas) {
e.preventDefault();
}
}, false);
document.body.addEventListener("touchmove", function (e) {
if (e.target == canvas) {
e.preventDefault();
}
}, false);
</script>
And I change onPointer events to onTouch events (simply change onPointer to onTouch
e.g. onPointerDown -> onTouchStart
But nothing happened when I draw on the canvas, I am not sure whether I am implementing onTouch events correctly.