feat: introduce physics-based dynamic rotation for dragged documents by adding angular velocity and mass properties, and refine group drag selection logic.
This commit is contained in:
@@ -65,6 +65,9 @@ interface DragGroupItemInternal extends EngineGroupItem {
|
||||
offsetY?: number;
|
||||
targetRotation?: number;
|
||||
initialRotation?: number;
|
||||
angularVelocity?: number;
|
||||
dynamicRotation?: number;
|
||||
massGrams?: number;
|
||||
}
|
||||
|
||||
type EnsureDocumentSizeFn = (doc: DocumentLike | null) => DocumentSizeInfo | null;
|
||||
@@ -317,33 +320,49 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => {
|
||||
}
|
||||
preventAll(event);
|
||||
|
||||
const docId = docIdInput != null ? docIdInput : null;
|
||||
const docKey = docId != null ? String(docId) : null;
|
||||
if (!docKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
engine?.cancelInertiaAnimation?.(docKey);
|
||||
|
||||
const doc = documentLookup.get(docKey);
|
||||
if (!doc) {
|
||||
return;
|
||||
}
|
||||
const massGrams = computeDocumentMassGrams(doc);
|
||||
// 1. Get initial selection from options
|
||||
const draggedDocIds = options.draggedDocIds;
|
||||
|
||||
let selectionIds: string[] = draggedDocIds;
|
||||
|
||||
// 2. Filter for valid documents
|
||||
selectionIds = selectionIds.filter((id, index, array) => array.indexOf(id) === index && documentLookup.has(id));
|
||||
|
||||
if (!selectionIds.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isGroupDrag = selectionIds.length > 1;
|
||||
// 3. Sort by Z-index (ascending) so the top-most card is last
|
||||
const layout = layoutRef.current;
|
||||
const sortedSelectionIds = [...selectionIds]
|
||||
.sort((a, b) => {
|
||||
const aZ = layout.get(a)?.z ?? 0;
|
||||
const bZ = layout.get(b)?.z ?? 0;
|
||||
return aZ - bZ;
|
||||
});
|
||||
|
||||
// 4. Determine Anchor
|
||||
// Anchor is the top-most valid card (last in sorted list)
|
||||
// We iterate backwards to find the first one with a valid layout
|
||||
let anchorId = sortedSelectionIds[sortedSelectionIds.length - 1];
|
||||
for (let i = sortedSelectionIds.length - 1; i >= 0; i--) {
|
||||
if (layout.has(sortedSelectionIds[i])) {
|
||||
anchorId = sortedSelectionIds[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const docKey = anchorId;
|
||||
const doc = documentLookup.get(docKey);
|
||||
if (!doc) {
|
||||
return;
|
||||
}
|
||||
|
||||
engine?.cancelInertiaAnimation?.(docKey);
|
||||
|
||||
const isGroupDrag = sortedSelectionIds.length > 1;
|
||||
|
||||
if (isGroupDrag) {
|
||||
selectionIds.forEach((id) => {
|
||||
sortedSelectionIds.forEach((id) => {
|
||||
if (id !== docKey) {
|
||||
engine?.cancelInertiaAnimation?.(id);
|
||||
}
|
||||
@@ -366,20 +385,11 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => {
|
||||
const modifierPressed = Boolean(options?.modifierActive);
|
||||
if (!modifierPressed) {
|
||||
if (isGroupDrag) {
|
||||
const layout = layoutRef.current;
|
||||
const ordered = [...selectionIds]
|
||||
.filter((id, index, array) => array.indexOf(id) === index)
|
||||
.sort((a, b) => {
|
||||
const aZ = layout.get(a)?.z ?? 0;
|
||||
const bZ = layout.get(b)?.z ?? 0;
|
||||
return aZ - bZ;
|
||||
});
|
||||
|
||||
ordered.forEach((id) => {
|
||||
bringToFront(id === docKey ? docId : id);
|
||||
sortedSelectionIds.forEach((id) => {
|
||||
bringToFront(id);
|
||||
});
|
||||
} else {
|
||||
bringToFront(docId);
|
||||
bringToFront(docKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -412,7 +422,7 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => {
|
||||
const localPointerOffsetX = pointerOffsetX * cosInitial - pointerOffsetY * sinInitial;
|
||||
const localPointerOffsetY = pointerOffsetX * sinInitial + pointerOffsetY * cosInitial;
|
||||
|
||||
const groupItems: DragGroupItemInternal[] = selectionIds.map((id) => {
|
||||
const groupItems: DragGroupItemInternal[] = sortedSelectionIds.map((id) => {
|
||||
const itemDoc = documentLookup.get(id);
|
||||
const itemSize = ensureDocumentSize(itemDoc) || sizeInfo;
|
||||
const itemWidth = itemSize.width || docWidth;
|
||||
@@ -425,6 +435,8 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => {
|
||||
const baseOffsetX = itemCenterX - centerX;
|
||||
const baseOffsetY = itemCenterY - centerY;
|
||||
const initialRotation = itemEntry?.rotation ?? 0;
|
||||
const itemMass = computeDocumentMassGrams(itemDoc);
|
||||
|
||||
return {
|
||||
docId: id,
|
||||
width: itemWidth,
|
||||
@@ -437,7 +449,9 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => {
|
||||
offsetY: baseOffsetY,
|
||||
targetRotation: initialRotation,
|
||||
displayRotation: initialRotation,
|
||||
|
||||
angularVelocity: 0,
|
||||
dynamicRotation: 0,
|
||||
massGrams: itemMass,
|
||||
} satisfies DragGroupItemInternal;
|
||||
});
|
||||
|
||||
@@ -448,6 +462,8 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => {
|
||||
? performance.now()
|
||||
: Date.now();
|
||||
|
||||
const massGrams = computeDocumentMassGrams(doc);
|
||||
|
||||
dragStateRef.current = {
|
||||
docId: docKey,
|
||||
docKey,
|
||||
@@ -477,7 +493,7 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => {
|
||||
containerRectLeft: containerLeft,
|
||||
containerRectTop: containerTop,
|
||||
isGroup: isGroupDrag,
|
||||
activeDocIds: selectionIds,
|
||||
activeDocIds: sortedSelectionIds,
|
||||
groupItems,
|
||||
groupElevated: !isGroupDrag,
|
||||
stackSelectionApplied: true,
|
||||
@@ -559,60 +575,106 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => {
|
||||
}
|
||||
preventAll(event);
|
||||
|
||||
const currentTimestamp =
|
||||
(Number.isFinite(event?.timeStamp))
|
||||
? event.timeStamp
|
||||
: performance?.now
|
||||
? performance.now()
|
||||
: Date.now();
|
||||
const previousTimestamp = state.lastTimestamp ?? currentTimestamp;
|
||||
let dt = (currentTimestamp - previousTimestamp) / 1000;
|
||||
if (!Number.isFinite(dt) || dt <= 0) {
|
||||
dt = MIN_TIMESTEP;
|
||||
}
|
||||
dt = clamp(dt, MIN_TIMESTEP, MAX_TIMESTEP);
|
||||
|
||||
state.lastClientX = event.clientX;
|
||||
state.lastClientY = event.clientY;
|
||||
state.lastTimestamp = currentTimestamp;
|
||||
|
||||
// Helper for updating angular velocity based on pointer movement
|
||||
const updatePointerAngularVelocity = (
|
||||
pointerCanvasX: number,
|
||||
pointerCanvasY: number,
|
||||
centerX: number,
|
||||
centerY: number,
|
||||
dtSeconds: number,
|
||||
pX: number,
|
||||
pY: number,
|
||||
cX: number,
|
||||
cY: number,
|
||||
dtSec: number,
|
||||
targetState: DragStateInternal | DragGroupItemInternal = state
|
||||
) => {
|
||||
if (!Number.isFinite(dtSeconds) || dtSeconds <= 0) {
|
||||
if (!Number.isFinite(dtSec) || dtSec <= 0) {
|
||||
return;
|
||||
}
|
||||
const leverX = pointerCanvasX - centerX;
|
||||
const leverY = pointerCanvasY - centerY;
|
||||
const leverX = pX - cX;
|
||||
const leverY = pY - cY;
|
||||
if (!Number.isFinite(leverX) || !Number.isFinite(leverY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use shared state for previous pointer position to calculate velocity
|
||||
// Note: For group items, we use the same pointer velocity
|
||||
const prevCanvasX = Number.isFinite(state.lastPointerCanvasX)
|
||||
? state.lastPointerCanvasX
|
||||
: pointerCanvasX;
|
||||
: pX;
|
||||
const prevCanvasY = Number.isFinite(state.lastPointerCanvasY)
|
||||
? state.lastPointerCanvasY
|
||||
: pointerCanvasY;
|
||||
const velocityCanvasX = (pointerCanvasX - prevCanvasX) / dtSeconds;
|
||||
const velocityCanvasY = (pointerCanvasY - prevCanvasY) / dtSeconds;
|
||||
state.lastPointerCanvasX = pointerCanvasX;
|
||||
state.lastPointerCanvasY = pointerCanvasY;
|
||||
: pY;
|
||||
|
||||
const velocityCanvasX = (pX - prevCanvasX) / dtSec;
|
||||
const velocityCanvasY = (pY - prevCanvasY) / dtSec;
|
||||
|
||||
if (!Number.isFinite(velocityCanvasX) || !Number.isFinite(velocityCanvasY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const torque = leverX * velocityCanvasY - leverY * velocityCanvasX;
|
||||
const influenceRadius = Math.max(state.width, state.height) / 2 || 1;
|
||||
const influenceRadius = Math.max(targetState.width, targetState.height) / 2 || 1;
|
||||
const radiusScale = clamp(Math.hypot(leverX, leverY) / influenceRadius, 0.2, 2.5);
|
||||
state.pointerRadiusScale = radiusScale;
|
||||
|
||||
// If it's the main state, update pointerRadiusScale
|
||||
if (targetState === state) {
|
||||
state.pointerRadiusScale = radiusScale;
|
||||
}
|
||||
|
||||
const torqueResponse = 0.0025 * radiusScale;
|
||||
const angularVelocityDeg = clamp(
|
||||
torque * torqueResponse,
|
||||
-MAX_ANGULAR_VELOCITY,
|
||||
MAX_ANGULAR_VELOCITY,
|
||||
);
|
||||
const mass = Math.max(state.massGrams || CARD_BASE_WEIGHT_GRAMS, CARD_BASE_WEIGHT_GRAMS);
|
||||
|
||||
const mass = Math.max(targetState.massGrams || CARD_BASE_WEIGHT_GRAMS, CARD_BASE_WEIGHT_GRAMS);
|
||||
const massScale = Math.max(mass / CARD_BASE_WEIGHT_GRAMS, 1);
|
||||
state.angularVelocity = angularVelocityDeg / massScale;
|
||||
targetState.angularVelocity = angularVelocityDeg / massScale;
|
||||
};
|
||||
|
||||
const applyDynamicRotation = (dtSeconds: number, dampingFactor = 0.94) => {
|
||||
if (!Number.isFinite(dtSeconds) || dtSeconds <= 0) {
|
||||
// Helper for applying dynamic rotation
|
||||
const applyDynamicRotation = (
|
||||
dtSec: number,
|
||||
targetState: DragStateInternal | DragGroupItemInternal = state,
|
||||
dampingFactor = 0.94
|
||||
) => {
|
||||
if (!Number.isFinite(dtSec) || dtSec <= 0) {
|
||||
return;
|
||||
}
|
||||
// Use state.pointerRadiusScale as a proxy for influence if not available on item?
|
||||
// Actually, let's just use 1 if not available, or recalculate.
|
||||
// For simplicity, we'll use the one calculated in updatePointerAngularVelocity if available,
|
||||
// or default.
|
||||
const radiusInfluence = clamp(state.pointerRadiusScale || 1, 0.3, 3);
|
||||
const response = 1.1 * radiusInfluence;
|
||||
let nextDynamic = state.dynamicRotation + state.angularVelocity * dtSeconds * response;
|
||||
|
||||
let nextDynamic = (targetState.dynamicRotation || 0) + (targetState.angularVelocity || 0) * dtSec * response;
|
||||
nextDynamic = clamp(nextDynamic, -MAX_DYNAMIC_ROTATION, MAX_DYNAMIC_ROTATION);
|
||||
const adjustedDamping = Math.pow(dampingFactor, 1 / Math.max(radiusInfluence, 0.8));
|
||||
state.dynamicRotation = nextDynamic * adjustedDamping;
|
||||
state.rotation = state.restRotation + state.dynamicRotation;
|
||||
targetState.dynamicRotation = nextDynamic * adjustedDamping;
|
||||
|
||||
if (targetState === state) {
|
||||
state.rotation = state.restRotation + state.dynamicRotation;
|
||||
} else {
|
||||
// For group items
|
||||
const item = targetState as DragGroupItemInternal;
|
||||
item.displayRotation = (item.initialRotation || 0) + item.dynamicRotation;
|
||||
}
|
||||
};
|
||||
|
||||
if (state.isGroup) {
|
||||
@@ -633,81 +695,56 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => {
|
||||
return;
|
||||
}
|
||||
state.moved = true;
|
||||
if (
|
||||
!state.stackSelectionApplied
|
||||
) {
|
||||
if (!state.stackSelectionApplied) {
|
||||
state.stackSelectionApplied = true;
|
||||
}
|
||||
if (!state.groupElevated) {
|
||||
const layout = layoutRef.current;
|
||||
const sortedGroup = state.activeDocIds
|
||||
.filter((id) => id !== state.docKey)
|
||||
.sort((a, b) => {
|
||||
const aZ = layout.get(a)?.z ?? 0;
|
||||
const bZ = layout.get(b)?.z ?? 0;
|
||||
return aZ - bZ;
|
||||
});
|
||||
|
||||
sortedGroup.forEach((id) => bringToFront(id));
|
||||
bringToFront(state.docKey);
|
||||
// Ensure Z-order is preserved during drag
|
||||
state.activeDocIds.forEach((id) => bringToFront(id));
|
||||
state.groupElevated = true;
|
||||
}
|
||||
}
|
||||
|
||||
const docWidth = state.width;
|
||||
const docHeight = state.height;
|
||||
const halfWidth = docWidth / 2;
|
||||
const halfHeight = docHeight / 2;
|
||||
const canvasWidth = canvasSize.width || defaultCanvasWidth;
|
||||
const canvasHeight = canvasSize.height || defaultCanvasHeight;
|
||||
const minCenterX = canvasPadding + halfWidth;
|
||||
const maxCenterX = Math.max(minCenterX, canvasWidth - canvasPadding - halfWidth);
|
||||
const minCenterY = canvasPadding + halfHeight;
|
||||
const maxCenterY = Math.max(minCenterY, canvasHeight - canvasPadding - halfHeight);
|
||||
|
||||
const desiredCenterX = pointerCanvasX - state.localPointerOffsetX;
|
||||
const desiredCenterY = pointerCanvasY - state.localPointerOffsetY;
|
||||
const centerX = clamp(desiredCenterX, minCenterX, maxCenterX);
|
||||
const centerY = clamp(desiredCenterY, minCenterY, maxCenterY);
|
||||
|
||||
state.currentCenterX = centerX;
|
||||
state.currentCenterY = centerY;
|
||||
|
||||
// Independent Physics for each item
|
||||
state.groupItems.forEach((item) => {
|
||||
const isPrimary = item.docId === state.docKey;
|
||||
// 1. Calculate Physics (Torque & Rotation)
|
||||
updatePointerAngularVelocity(
|
||||
pointerCanvasX,
|
||||
pointerCanvasY,
|
||||
item.currentCenterX,
|
||||
item.currentCenterY,
|
||||
dt,
|
||||
item
|
||||
);
|
||||
|
||||
if (isPrimary) {
|
||||
item.currentCenterX = centerX;
|
||||
item.currentCenterY = centerY;
|
||||
item.offsetX = item.baseOffsetX ?? 0;
|
||||
item.offsetY = item.baseOffsetY ?? 0;
|
||||
item.displayRotation = state.rotation ?? item.displayRotation ?? 0;
|
||||
} else {
|
||||
const decay = 0.82;
|
||||
const currentOffsetX = (item.offsetX ?? item.baseOffsetX ?? 0) * decay;
|
||||
const currentOffsetY = (item.offsetY ?? item.baseOffsetY ?? 0) * decay;
|
||||
item.offsetX = Math.abs(currentOffsetX) < 0.5 ? 0 : currentOffsetX;
|
||||
item.offsetY = Math.abs(currentOffsetY) < 0.5 ? 0 : currentOffsetY;
|
||||
applyDynamicRotation(dt, item, 0.96);
|
||||
|
||||
const targetX = centerX + item.offsetX;
|
||||
const targetY = centerY + item.offsetY;
|
||||
const smoothing = 0.18;
|
||||
item.currentCenterX += (targetX - item.currentCenterX) * smoothing;
|
||||
item.currentCenterY += (targetY - item.currentCenterY) * smoothing;
|
||||
// 2. Calculate Target Position
|
||||
const targetX = pointerCanvasX - state.localPointerOffsetX + (item.baseOffsetX || 0);
|
||||
const targetY = pointerCanvasY - state.localPointerOffsetY + (item.baseOffsetY || 0);
|
||||
|
||||
const halfW = item.width / 2;
|
||||
const halfH = item.height / 2;
|
||||
const minX = canvasPadding + halfW;
|
||||
const maxX = Math.max(minX, canvasWidth - canvasPadding - halfW);
|
||||
const minY = canvasPadding + halfH;
|
||||
const maxY = Math.max(minY, canvasHeight - canvasPadding - halfH);
|
||||
item.currentCenterX = clamp(item.currentCenterX, minX, maxX);
|
||||
item.currentCenterY = clamp(item.currentCenterY, minY, maxY);
|
||||
// 3. Apply Smoothing / Damping
|
||||
const smoothing = 0.18; // Base smoothing
|
||||
const stackFriction = 0.85; // Additional damping for stack feel
|
||||
const effectiveSmoothing = smoothing * stackFriction;
|
||||
|
||||
const rotationBlend = 0.16;
|
||||
item.displayRotation += (item.targetRotation - item.displayRotation) * rotationBlend;
|
||||
}
|
||||
item.currentCenterX += (targetX - item.currentCenterX) * effectiveSmoothing;
|
||||
item.currentCenterY += (targetY - item.currentCenterY) * effectiveSmoothing;
|
||||
|
||||
// Clamp to canvas
|
||||
const halfW = item.width / 2;
|
||||
const halfH = item.height / 2;
|
||||
const minX = canvasPadding + halfW;
|
||||
const maxX = Math.max(minX, canvasWidth - canvasPadding - halfW);
|
||||
const minY = canvasPadding + halfH;
|
||||
const maxY = Math.max(minY, canvasHeight - canvasPadding - halfH);
|
||||
item.currentCenterX = clamp(item.currentCenterX, minX, maxX);
|
||||
item.currentCenterY = clamp(item.currentCenterY, minY, maxY);
|
||||
|
||||
// Apply transform
|
||||
const entry = layoutRef.current.get(item.docId) || null;
|
||||
const payload = {
|
||||
centerX: item.currentCenterX,
|
||||
@@ -715,7 +752,7 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => {
|
||||
rotation: item.displayRotation ?? 0,
|
||||
width: item.width,
|
||||
height: item.height,
|
||||
scale: isPrimary ? state.dragScale || 1 : 1,
|
||||
scale: item.docId === state.docKey ? state.dragScale || 1 : 1,
|
||||
zIndex: entry?.z,
|
||||
};
|
||||
|
||||
@@ -724,33 +761,15 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => {
|
||||
applyDomTransform(node, payload);
|
||||
});
|
||||
|
||||
const currentTimestampGroup =
|
||||
(Number.isFinite(event?.timeStamp))
|
||||
? event.timeStamp
|
||||
: performance?.now
|
||||
? performance.now()
|
||||
: Date.now();
|
||||
const previousTimestampGroup = state.lastTimestamp ?? currentTimestampGroup;
|
||||
let dtGroup = (currentTimestampGroup - previousTimestampGroup) / 1000;
|
||||
if (!Number.isFinite(dtGroup) || dtGroup <= 0) {
|
||||
dtGroup = MIN_TIMESTEP;
|
||||
}
|
||||
dtGroup = clamp(dtGroup, MIN_TIMESTEP, MAX_TIMESTEP);
|
||||
|
||||
state.lastClientX = event.clientX;
|
||||
state.lastClientY = event.clientY;
|
||||
state.lastTimestamp = currentTimestampGroup;
|
||||
|
||||
updatePointerAngularVelocity(pointerCanvasX, pointerCanvasY, centerX, centerY, dtGroup);
|
||||
applyDynamicRotation(dtGroup, 0.96);
|
||||
state.groupItems.forEach((item) => {
|
||||
if (item.docId === state.docKey) {
|
||||
item.displayRotation = state.rotation ?? item.displayRotation ?? 0;
|
||||
}
|
||||
});
|
||||
// Update shared state for next frame velocity calculation
|
||||
state.lastPointerCanvasX = pointerCanvasX;
|
||||
state.lastPointerCanvasY = pointerCanvasY;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Single Item Drag Logic ---
|
||||
|
||||
if (state.locked) {
|
||||
return;
|
||||
}
|
||||
@@ -758,11 +777,6 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => {
|
||||
const deltaX = event.clientX - state.startX;
|
||||
const deltaY = event.clientY - state.startY;
|
||||
|
||||
const docWidth = state.width;
|
||||
const docHeight = state.height;
|
||||
const halfWidth = docWidth / 2;
|
||||
const halfHeight = docHeight / 2;
|
||||
|
||||
const containerRect = containerRef.current?.getBoundingClientRect?.();
|
||||
if (containerRect) {
|
||||
state.containerRectLeft = containerRect.left;
|
||||
@@ -774,7 +788,6 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => {
|
||||
const pointerCanvasX = event.clientX - containerLeft;
|
||||
const pointerCanvasY = event.clientY - containerTop;
|
||||
|
||||
const entry = layoutRef.current.get(state.docKey) || {};
|
||||
const previousCenterX = Number.isFinite(state.currentCenterX)
|
||||
? state.currentCenterX
|
||||
: state.originCenterX;
|
||||
@@ -782,29 +795,25 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => {
|
||||
? state.currentCenterY
|
||||
: state.originCenterY;
|
||||
|
||||
const currentTimestamp =
|
||||
(Number.isFinite(event?.timeStamp))
|
||||
? event.timeStamp
|
||||
: performance?.now
|
||||
? performance.now()
|
||||
: Date.now();
|
||||
const previousTimestamp = state.lastTimestamp ?? currentTimestamp;
|
||||
let dt = (currentTimestamp - previousTimestamp) / 1000;
|
||||
if (!Number.isFinite(dt) || dt <= 0) {
|
||||
dt = MIN_TIMESTEP;
|
||||
}
|
||||
dt = clamp(dt, MIN_TIMESTEP, MAX_TIMESTEP);
|
||||
|
||||
const torqueCenterX = Number.isFinite(previousCenterX) ? previousCenterX : state.originCenterX;
|
||||
const torqueCenterY = Number.isFinite(previousCenterY) ? previousCenterY : state.originCenterY;
|
||||
updatePointerAngularVelocity(pointerCanvasX, pointerCanvasY, torqueCenterX, torqueCenterY, dt);
|
||||
applyDynamicRotation(dt);
|
||||
|
||||
state.lastClientX = event.clientX;
|
||||
state.lastClientY = event.clientY;
|
||||
state.lastTimestamp = currentTimestamp;
|
||||
updatePointerAngularVelocity(pointerCanvasX, pointerCanvasY, torqueCenterX, torqueCenterY, dt, state);
|
||||
applyDynamicRotation(dt, state, 0.96);
|
||||
|
||||
const rotationDeg = state.rotation ?? entry.rotation ?? 0;
|
||||
state.lastPointerCanvasX = pointerCanvasX;
|
||||
state.lastPointerCanvasY = pointerCanvasY;
|
||||
|
||||
// Position Calculation
|
||||
const docWidth = state.width;
|
||||
const docHeight = state.height;
|
||||
const halfWidth = docWidth / 2;
|
||||
const halfHeight = docHeight / 2;
|
||||
const canvasWidth = canvasSize.width || defaultCanvasWidth;
|
||||
const canvasHeight = canvasSize.height || defaultCanvasHeight;
|
||||
|
||||
|
||||
const rotationDeg = state.rotation ?? 0;
|
||||
const rotationRad = (rotationDeg * Math.PI) / 180;
|
||||
const cosRot = Math.cos(rotationRad);
|
||||
const sinRot = Math.sin(rotationRad);
|
||||
@@ -817,26 +826,16 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => {
|
||||
const absSin = Math.abs(sinRot);
|
||||
const rotatedHalfWidth = absCos * halfWidth + absSin * halfHeight;
|
||||
const rotatedHalfHeight = absSin * halfWidth + absCos * halfHeight;
|
||||
const canvasWidth = canvasSize.width || defaultCanvasWidth;
|
||||
const canvasHeight = canvasSize.height || defaultCanvasHeight;
|
||||
const minCenterX = canvasPadding + rotatedHalfWidth;
|
||||
const maxCenterX = Math.max(minCenterX, canvasWidth - canvasPadding - rotatedHalfWidth);
|
||||
const minCenterY = canvasPadding + rotatedHalfHeight;
|
||||
const maxCenterY = Math.max(minCenterY, canvasHeight - canvasPadding - rotatedHalfHeight);
|
||||
|
||||
const pointerRelativePrevX = pointerCanvasX - previousCenterX;
|
||||
const pointerRelativePrevY = pointerCanvasY - previousCenterY;
|
||||
const cosInversePrev = Math.cos(-rotationRad);
|
||||
const sinInversePrev = Math.sin(-rotationRad);
|
||||
const pointerLocalPrevX = pointerRelativePrevX * cosInversePrev - pointerRelativePrevY * sinInversePrev;
|
||||
const pointerLocalPrevY = pointerRelativePrevX * sinInversePrev + pointerRelativePrevY * cosInversePrev;
|
||||
const pointerInsideRelativeToPrev =
|
||||
Math.abs(pointerLocalPrevX) <= halfWidth && Math.abs(pointerLocalPrevY) <= halfHeight;
|
||||
const minCenterXRotated = canvasPadding + rotatedHalfWidth;
|
||||
const maxCenterXRotated = Math.max(minCenterXRotated, canvasWidth - canvasPadding - rotatedHalfWidth);
|
||||
const minCenterYRotated = canvasPadding + rotatedHalfHeight;
|
||||
const maxCenterYRotated = Math.max(minCenterYRotated, canvasHeight - canvasPadding - rotatedHalfHeight);
|
||||
|
||||
const desiredCenterX = pointerCanvasX - rotatedOffsetX;
|
||||
const desiredCenterY = pointerCanvasY - rotatedOffsetY;
|
||||
const clampedCenterX = clamp(desiredCenterX, minCenterX, maxCenterX);
|
||||
const clampedCenterY = clamp(desiredCenterY, minCenterY, maxCenterY);
|
||||
const clampedCenterX = clamp(desiredCenterX, minCenterXRotated, maxCenterXRotated);
|
||||
const clampedCenterY = clamp(desiredCenterY, minCenterYRotated, maxCenterYRotated);
|
||||
|
||||
if (!state.moved) {
|
||||
const distanceSquared = deltaX * deltaX + deltaY * deltaY;
|
||||
@@ -847,12 +846,23 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => {
|
||||
state.moved = true;
|
||||
}
|
||||
|
||||
// Edge collision logic
|
||||
const collidedWithHorizontalEdge =
|
||||
Math.abs(desiredCenterY - clampedCenterY) > EDGE_COLLISION_THRESHOLD;
|
||||
const collidedWithVerticalEdge =
|
||||
Math.abs(desiredCenterX - clampedCenterX) > EDGE_COLLISION_THRESHOLD;
|
||||
const collidedWithEdge = collidedWithHorizontalEdge || collidedWithVerticalEdge;
|
||||
|
||||
// Check if pointer is inside the card relative to previous position
|
||||
const pointerRelativePrevX = pointerCanvasX - previousCenterX;
|
||||
const pointerRelativePrevY = pointerCanvasY - previousCenterY;
|
||||
const cosInversePrev = Math.cos(-rotationRad);
|
||||
const sinInversePrev = Math.sin(-rotationRad);
|
||||
const pointerLocalPrevX = pointerRelativePrevX * cosInversePrev - pointerRelativePrevY * sinInversePrev;
|
||||
const pointerLocalPrevY = pointerRelativePrevX * sinInversePrev + pointerRelativePrevY * cosInversePrev;
|
||||
const pointerInsideRelativeToPrev =
|
||||
Math.abs(pointerLocalPrevX) <= halfWidth && Math.abs(pointerLocalPrevY) <= halfHeight;
|
||||
|
||||
let currentCenterX = clampedCenterX;
|
||||
let currentCenterY = clampedCenterY;
|
||||
if (collidedWithEdge && !pointerInsideRelativeToPrev) {
|
||||
@@ -878,6 +888,7 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => {
|
||||
const primaryNode = itemRefs.current.get(state.docKey);
|
||||
applyDomTransform(primaryNode, transformPayload);
|
||||
|
||||
// Update local pointer offset if we didn't collide or pointer is inside
|
||||
const offsetX = pointerCanvasX - currentCenterX;
|
||||
const offsetY = pointerCanvasY - currentCenterY;
|
||||
const cosInverseCurrent = Math.cos(-rotationRad);
|
||||
@@ -887,15 +898,17 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => {
|
||||
const pointerInsideCard =
|
||||
Math.abs(pointerLocalX) <= halfWidth && Math.abs(pointerLocalY) <= halfHeight;
|
||||
|
||||
const rotationForOffsetDeg = state.rotation || 0;
|
||||
const rotationForOffsetRad = (rotationForOffsetDeg * Math.PI) / 180;
|
||||
const cosInverse = Math.cos(-rotationForOffsetRad);
|
||||
const sinInverse = Math.sin(-rotationForOffsetRad);
|
||||
const pointerRelativeX = pointerCanvasX - currentCenterX;
|
||||
const pointerRelativeY = pointerCanvasY - currentCenterY;
|
||||
const updatedLocalOffsetX = pointerRelativeX * cosInverse - pointerRelativeY * sinInverse;
|
||||
const updatedLocalOffsetY = pointerRelativeX * sinInverse + pointerRelativeY * cosInverse;
|
||||
if (!collidedWithEdge || pointerInsideCard) {
|
||||
// Recalculate local offset based on current rotation
|
||||
const rotationForOffsetDeg = state.rotation || 0;
|
||||
const rotationForOffsetRad = (rotationForOffsetDeg * Math.PI) / 180;
|
||||
const cosInverse = Math.cos(-rotationForOffsetRad);
|
||||
const sinInverse = Math.sin(-rotationForOffsetRad);
|
||||
const pointerRelativeX = pointerCanvasX - currentCenterX;
|
||||
const pointerRelativeY = pointerCanvasY - currentCenterY;
|
||||
const updatedLocalOffsetX = pointerRelativeX * cosInverse - pointerRelativeY * sinInverse;
|
||||
const updatedLocalOffsetY = pointerRelativeX * sinInverse + pointerRelativeY * cosInverse;
|
||||
|
||||
state.localPointerOffsetX = updatedLocalOffsetX;
|
||||
state.localPointerOffsetY = updatedLocalOffsetY;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user