mirror of https://github.com/mkrhere/pw2
2 changed files with 276 additions and 0 deletions
@ -0,0 +1,236 @@ |
|||
interface Vec2 { |
|||
x: number; |
|||
y: number; |
|||
} |
|||
|
|||
function getCursorPositionRelativeToElement( |
|||
cursor: Vec2, |
|||
element: HTMLElement, |
|||
): Vec2 { |
|||
const size = { |
|||
x: element.offsetWidth, |
|||
y: element.offsetHeight, |
|||
}; |
|||
|
|||
const boundingRect = element.getBoundingClientRect(); |
|||
|
|||
const computedStyle = window.getComputedStyle(element); |
|||
const transformValue = computedStyle.transform; |
|||
|
|||
if (transformValue === "none" || !transformValue) |
|||
return { |
|||
x: cursor.x - boundingRect.left, |
|||
y: cursor.y - boundingRect.top, |
|||
}; |
|||
|
|||
const matrix = new DOMMatrix(transformValue); |
|||
|
|||
const centerX = boundingRect.left + boundingRect.width / 2; |
|||
const centerY = boundingRect.top + boundingRect.height / 2; |
|||
|
|||
const relativeToCenter = { |
|||
x: cursor.x - centerX, |
|||
y: cursor.y - centerY, |
|||
}; |
|||
|
|||
const inverseMatrix = matrix.inverse(); |
|||
|
|||
inverseMatrix.e = 0; |
|||
inverseMatrix.f = 0; |
|||
|
|||
const transformedPoint = { |
|||
x: |
|||
relativeToCenter.x * inverseMatrix.a + |
|||
relativeToCenter.y * inverseMatrix.c, |
|||
y: |
|||
relativeToCenter.x * inverseMatrix.b + |
|||
relativeToCenter.y * inverseMatrix.d, |
|||
}; |
|||
|
|||
return { |
|||
x: transformedPoint.x + size.x / 2, |
|||
y: transformedPoint.y + size.y / 2, |
|||
}; |
|||
} |
|||
|
|||
export function makeDraggable(card: HTMLElement) { |
|||
let position: Vec2 = { x: 0, y: 0 }; |
|||
let rotation = 0; |
|||
let dragging = false; |
|||
let offset__local: Vec2 = { x: 0, y: 0 }; |
|||
let grabAngleLocal = 0; |
|||
|
|||
let velocity: Vec2 = { x: 0, y: 0 }; |
|||
let angularVelocity = 0; |
|||
|
|||
const dampingFactor = 0.5; |
|||
const springFactor = 0.2; |
|||
const maxAngularVelocity = 0.95; |
|||
const momentumDampening = 0.98; |
|||
|
|||
let lastMousePosition__page: Vec2 = { x: 0, y: 0 }; |
|||
let activePointerId: number | null = null; |
|||
|
|||
// Keep original factors for momentum physics
|
|||
const momentumDampingFactor = 0.7; // Renamed for clarity
|
|||
const momentumSpringFactor = 0.2; // Renamed for clarity
|
|||
const momentumMaxAngularVelocity = 0.95; |
|||
const momentumDampeningDecay = 0.98; // Renamed for clarity
|
|||
|
|||
// Add a specific damping factor for the active drag rotation spring
|
|||
const dragRotationDamping = 0.4; // TUNABLE: Higher value = more damping (less springy/oscillating)
|
|||
|
|||
function clamp(value: number, min: number, max: number): number { |
|||
return Math.min(Math.max(value, min), max); |
|||
} |
|||
|
|||
const down = (e: PointerEvent) => { |
|||
if (activePointerId !== null) return; |
|||
|
|||
dragging = true; |
|||
activePointerId = e.pointerId; |
|||
card.setPointerCapture(e.pointerId); |
|||
|
|||
velocity = { x: 0, y: 0 }; |
|||
angularVelocity = 0; |
|||
|
|||
offset__local = getCursorPositionRelativeToElement( |
|||
{ x: e.pageX, y: e.pageY }, |
|||
card, |
|||
); |
|||
|
|||
const cardWidth = card.offsetWidth; |
|||
const cardHeight = card.offsetHeight; |
|||
const grabCentre__local = { |
|||
x: offset__local.x - cardWidth / 2, |
|||
y: offset__local.y - cardHeight / 2, |
|||
}; |
|||
grabAngleLocal = Math.atan2(grabCentre__local.y, grabCentre__local.x); |
|||
|
|||
lastMousePosition__page = { x: e.pageX, y: e.pageY }; |
|||
}; |
|||
|
|||
// TODO: this function is still not working as expected
|
|||
// Come back to it some day
|
|||
const move = (e: PointerEvent) => { |
|||
if (!dragging || e.pointerId !== activePointerId) return; |
|||
|
|||
const mx = e.pageX; |
|||
const my = e.pageY; |
|||
|
|||
// Calculate mouse movement delta
|
|||
const mouseDelta = { |
|||
x: mx - lastMousePosition__page.x, |
|||
y: my - lastMousePosition__page.y, |
|||
}; |
|||
|
|||
position.x += mouseDelta.x; |
|||
position.y += mouseDelta.y; |
|||
|
|||
// Update velocity (for momentum) and last mouse position
|
|||
velocity = mouseDelta; // Store the delta for momentum phase
|
|||
lastMousePosition__page = { x: mx, y: my }; |
|||
|
|||
const rect = card.getBoundingClientRect(); |
|||
// Use rect dimensions in case transforms (like scale) affect offsetWidth/offsetHeight differently
|
|||
const cardWidth = rect.width; |
|||
const cardHeight = rect.height; |
|||
|
|||
const currentCenter__page = { |
|||
x: rect.left + cardWidth / 2 + window.scrollX, |
|||
y: rect.top + cardHeight / 2 + window.scrollY, |
|||
}; |
|||
|
|||
const angleToMouse = Math.atan2( |
|||
my - currentCenter__page.y, |
|||
mx - currentCenter__page.x, |
|||
); |
|||
|
|||
const angleToMouse_Local = angleToMouse - grabAngleLocal; |
|||
|
|||
const px = offset__local.x; |
|||
const py = offset__local.y; |
|||
|
|||
const targetRotation = |
|||
Math.atan2(my - position.y, mx - position.x) - Math.atan2(py, px); |
|||
|
|||
angularVelocity += (targetRotation - rotation) * springFactor; |
|||
angularVelocity *= dampingFactor; |
|||
angularVelocity = clamp( |
|||
angularVelocity, |
|||
-maxAngularVelocity, |
|||
maxAngularVelocity, |
|||
); |
|||
|
|||
rotation += angularVelocity; |
|||
|
|||
const cos = Math.cos(rotation); |
|||
const sin = Math.sin(rotation); |
|||
|
|||
const rx = px * cos - py * sin; |
|||
const ry = px * sin + py * cos; |
|||
|
|||
// position = {
|
|||
// x: mx - rx,
|
|||
// y: my - ry,
|
|||
// };
|
|||
}; |
|||
|
|||
const up = (e: PointerEvent) => { |
|||
if (e.pointerId === activePointerId) { |
|||
dragging = false; |
|||
activePointerId = null; |
|||
// When dragging stops, the existing angularVelocity
|
|||
// is used by the render() loop for momentum.
|
|||
} |
|||
}; |
|||
|
|||
card.addEventListener("pointerdown", down, { passive: false }); |
|||
window.addEventListener("pointermove", move, { passive: false }); |
|||
window.addEventListener("pointerup", up, { passive: false }); |
|||
|
|||
let frame = 0; |
|||
function render() { |
|||
if (!dragging) { |
|||
// --- Momentum Phase ---
|
|||
// Use the angularVelocity calculated by the last 'move' or previous 'render' frame
|
|||
if (Math.abs(angularVelocity) > 0.001) { |
|||
rotation += angularVelocity; // Apply momentum rotation
|
|||
angularVelocity *= momentumDampeningDecay; // Apply decay damping
|
|||
} else { |
|||
angularVelocity = 0; |
|||
} |
|||
|
|||
const speed = Math.sqrt( |
|||
velocity.x * velocity.x + velocity.y * velocity.y, |
|||
); |
|||
if (speed > 0.01) { |
|||
position.x += velocity.x; |
|||
position.y += velocity.y; |
|||
velocity.x *= momentumDampening; |
|||
velocity.y *= momentumDampening; |
|||
} else { |
|||
velocity = { x: 0, y: 0 }; |
|||
} |
|||
} // else: if dragging, rotation is updated in move()
|
|||
|
|||
// --- Apply Transform ---
|
|||
card.style.transform = ` |
|||
translate(${position.x}px, ${position.y}px) |
|||
rotate(${rotation}rad) |
|||
`;
|
|||
card.style.transformOrigin = "50% 50%"; |
|||
frame = requestAnimationFrame(render); |
|||
} |
|||
render(); |
|||
|
|||
return () => { |
|||
card.removeEventListener("pointerdown", down); |
|||
window.removeEventListener("pointermove", move); |
|||
window.removeEventListener("pointerup", up); |
|||
cancelAnimationFrame(frame); |
|||
// Optional: Reset transform on cleanup?
|
|||
// card.style.transform = originalTransform || 'none';
|
|||
card.style.transformOrigin = ""; |
|||
}; |
|||
} |
@ -0,0 +1,40 @@ |
|||
import React, { forwardRef, useEffect, useRef } from "react"; |
|||
import { makeDraggable } from "./Draggable"; |
|||
import { composeRefs } from "../../util"; |
|||
import { css, cx } from "@emotion/css"; |
|||
|
|||
export type DraggableProps = React.ComponentPropsWithRef<any> & { |
|||
as?: React.ElementType; |
|||
children: React.ReactNode; |
|||
}; |
|||
|
|||
export const Draggable = forwardRef<HTMLElement, DraggableProps>( |
|||
( |
|||
{ as: Comp = "div", children, className, ...props }: DraggableProps, |
|||
ref, |
|||
) => { |
|||
const cardRef = useRef<HTMLElement>(null); |
|||
|
|||
useEffect(() => { |
|||
if (!cardRef.current) return; |
|||
return makeDraggable(cardRef.current); |
|||
}, []); |
|||
|
|||
return ( |
|||
<Comp |
|||
className={cx( |
|||
className, |
|||
"draggable", |
|||
css` |
|||
cursor: grab; |
|||
`,
|
|||
)} |
|||
ref={composeRefs(cardRef, ref)} |
|||
{...props}> |
|||
{children} |
|||
</Comp> |
|||
); |
|||
}, |
|||
); |
|||
|
|||
Draggable.displayName = "Draggable"; |
Loading…
Reference in new issue