Browse Source

feat: attempt 5 at creating standalone draaggable

master
Muthu Kumar 1 month ago
parent
commit
5e9eebb4ab
Failed to extract signature
  1. 236
      src/draggable.attempts/5/Draggable.ts
  2. 40
      src/draggable.attempts/5/Draggable2.tsx

236
src/draggable.attempts/5/Draggable.ts

@ -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 = "";
};
}

40
src/draggable.attempts/5/Draggable2.tsx

@ -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…
Cancel
Save