feat: Implement enhanced card physics with rotational dynamics, mass scaling, stacking behavior, and drag torque.
This commit is contained in:
@@ -4,7 +4,8 @@ export const handleDragStart = (store: LayoutStore, selection: string[], leading
|
|||||||
const leadingCard = store.items.get(leadingId);
|
const leadingCard = store.items.get(leadingId);
|
||||||
if (!leadingCard) return;
|
if (!leadingCard) return;
|
||||||
|
|
||||||
// Snap all non-leading cards to the leading card's position
|
// 1. Snap all followers to leader's center
|
||||||
|
// 2. Attach them as followers to the leader's physics
|
||||||
selection.forEach(id => {
|
selection.forEach(id => {
|
||||||
if (id === leadingId) return;
|
if (id === leadingId) return;
|
||||||
const card = store.items.get(id);
|
const card = store.items.get(id);
|
||||||
@@ -19,30 +20,17 @@ export const handleDragStart = (store: LayoutStore, selection: string[], leading
|
|||||||
const targetY = leadingCenterY - card.height / 2;
|
const targetY = leadingCenterY - card.height / 2;
|
||||||
|
|
||||||
card.snapTo(targetX, targetY);
|
card.snapTo(targetX, targetY);
|
||||||
|
|
||||||
|
// Stop any existing physics on the follower
|
||||||
|
card.physics.stop();
|
||||||
|
|
||||||
|
// Attach as follower
|
||||||
|
leadingCard.physics.addFollower(card);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
selection.forEach(id => {
|
// 3. Begin drag ONLY on the leader
|
||||||
const card = store.items.get(id);
|
leadingCard.physics.beginDrag(offset, pointerId);
|
||||||
if (!card) return;
|
|
||||||
|
|
||||||
// If card is already dragging by another pointer, skip it
|
|
||||||
if (card.physics.isDragging && card.physics.dragPointerId !== pointerId) return;
|
|
||||||
|
|
||||||
let myOffset = offset;
|
|
||||||
|
|
||||||
if (id !== leadingId) {
|
|
||||||
const pointerX = leadingCard.x + offset.x;
|
|
||||||
const pointerY = leadingCard.y + offset.y;
|
|
||||||
|
|
||||||
myOffset = {
|
|
||||||
x: pointerX - card.x,
|
|
||||||
y: pointerY - card.y
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
card.physics.beginDrag(myOffset, pointerId);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const attachToDragGroup = (store: LayoutStore, selection: string[], leadingId: string, pointerId: number) => {
|
export const attachToDragGroup = (store: LayoutStore, selection: string[], leadingId: string, pointerId: number) => {
|
||||||
@@ -62,7 +50,12 @@ export const attachToDragGroup = (store: LayoutStore, selection: string[], leadi
|
|||||||
const targetY = leadingCenterY - card.height / 2;
|
const targetY = leadingCenterY - card.height / 2;
|
||||||
|
|
||||||
card.snapTo(targetX, targetY);
|
card.snapTo(targetX, targetY);
|
||||||
card.physics.beginDrag({ x: 0, y: 0 }, pointerId);
|
|
||||||
|
// Stop any existing physics
|
||||||
|
card.physics.stop();
|
||||||
|
|
||||||
|
// Attach as follower
|
||||||
|
leadingCard.physics.addFollower(card);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,28 +1,66 @@
|
|||||||
import { LayoutCard } from './LayoutSystem';
|
import { LayoutCard } from './LayoutSystem';
|
||||||
|
|
||||||
export class CardPhysics {
|
export class CardPhysics {
|
||||||
// State
|
|
||||||
private velocity: { x: number, y: number, rotation: number } = { x: 0, y: 0, rotation: 0 };
|
private velocity: { x: number, y: number, rotation: number } = { x: 0, y: 0, rotation: 0 };
|
||||||
private _isDragging: boolean = false;
|
private _isDragging: boolean = false;
|
||||||
public dragPointerId: number | null = null;
|
public dragPointerId: number | null = null;
|
||||||
private pendingDelta: { x: number, y: number } = { x: 0, y: 0 };
|
private pendingDelta: { x: number, y: number } = { x: 0, y: 0 };
|
||||||
|
|
||||||
|
public mass: number = 30;
|
||||||
|
private baseMass: number = 30;
|
||||||
|
private massScale: number = 1;
|
||||||
|
private angularVelocity: number = 0;
|
||||||
|
private lastTimestamp: number = 0;
|
||||||
|
|
||||||
|
private dragOffset: { x: number, y: number } | null = null;
|
||||||
|
|
||||||
get isDragging(): boolean {
|
get isDragging(): boolean {
|
||||||
return this._isDragging;
|
return this._isDragging;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loop State
|
|
||||||
private physicsRafId: number | null = null;
|
private physicsRafId: number | null = null;
|
||||||
private lastTickTime: number = 0;
|
private lastTickTime: number = 0;
|
||||||
private card: LayoutCard;
|
private card: LayoutCard;
|
||||||
|
|
||||||
constructor(card: LayoutCard) {
|
constructor(card: LayoutCard) {
|
||||||
this.card = card;
|
this.card = card;
|
||||||
|
this.updateMass(card.pageCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMass(pageCount: number) {
|
||||||
|
const pages = Math.max(1, pageCount);
|
||||||
|
this.baseMass = 30 + 5 * pages;
|
||||||
|
this.mass = this.baseMass;
|
||||||
|
this.updateMassScale();
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateMassScale() {
|
||||||
|
this.massScale = Math.max(this.mass / 30, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeAngle(angle: number): number {
|
||||||
|
let a = angle % 360;
|
||||||
|
if (a > 180) a -= 360;
|
||||||
|
if (a <= -180) a += 360;
|
||||||
|
return a;
|
||||||
}
|
}
|
||||||
|
|
||||||
beginDrag(offset: { x: number, y: number }, pointerId: number) {
|
beginDrag(offset: { x: number, y: number }, pointerId: number) {
|
||||||
this._isDragging = true;
|
this._isDragging = true;
|
||||||
this.dragPointerId = pointerId;
|
this.dragPointerId = pointerId;
|
||||||
|
|
||||||
|
// Store the offset from center where we grabbed the card
|
||||||
|
// Convert world offset to local offset (rotate by -rotation)
|
||||||
|
const rad = -this.card.rotation * Math.PI / 180;
|
||||||
|
const cos = Math.cos(rad);
|
||||||
|
const sin = Math.sin(rad);
|
||||||
|
|
||||||
|
this.dragOffset = {
|
||||||
|
x: offset.x * cos - offset.y * sin,
|
||||||
|
y: offset.x * sin + offset.y * cos
|
||||||
|
};
|
||||||
|
|
||||||
|
this.angularVelocity = 0;
|
||||||
this.startPhysicsLoop();
|
this.startPhysicsLoop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +72,9 @@ export class CardPhysics {
|
|||||||
finishDrag() {
|
finishDrag() {
|
||||||
this._isDragging = false;
|
this._isDragging = false;
|
||||||
this.dragPointerId = null;
|
this.dragPointerId = null;
|
||||||
// Loop continues for decay/settling if needed
|
// Keep dragOffset for inertia pivot correction
|
||||||
|
this.lastTimestamp = performance.now();
|
||||||
|
this.angularVelocity = 0; // Kill momentum on release
|
||||||
}
|
}
|
||||||
|
|
||||||
private startPhysicsLoop() {
|
private startPhysicsLoop() {
|
||||||
@@ -43,50 +83,219 @@ export class CardPhysics {
|
|||||||
this.physicsRafId = requestAnimationFrame(this.physicsTick);
|
this.physicsRafId = requestAnimationFrame(this.physicsTick);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public stop() {
|
||||||
|
this.stopPhysicsLoop();
|
||||||
|
}
|
||||||
|
|
||||||
private stopPhysicsLoop() {
|
private stopPhysicsLoop() {
|
||||||
if (this.physicsRafId) {
|
if (this.physicsRafId) {
|
||||||
cancelAnimationFrame(this.physicsRafId);
|
cancelAnimationFrame(this.physicsRafId);
|
||||||
this.physicsRafId = null;
|
this.physicsRafId = null;
|
||||||
}
|
}
|
||||||
|
this.dragOffset = null;
|
||||||
|
this.clearFollowers();
|
||||||
}
|
}
|
||||||
|
|
||||||
private physicsTick = (time: number) => {
|
private physicsTick = (time: number) => {
|
||||||
const dt = Math.min((time - this.lastTickTime) / 1000, 0.1);
|
const rawDt = (time - this.lastTickTime) / 1000;
|
||||||
|
const dt = Math.max(1 / 120, Math.min(rawDt, 1 / 20));
|
||||||
this.lastTickTime = time;
|
this.lastTickTime = time;
|
||||||
|
|
||||||
if (this._isDragging) {
|
this.updatePhysics(dt);
|
||||||
// Minimal Physics: Apply pending delta directly
|
|
||||||
const dx = this.pendingDelta.x;
|
|
||||||
const dy = this.pendingDelta.y;
|
|
||||||
|
|
||||||
// Update velocity (simple instantaneous)
|
if (this.physicsRafId) {
|
||||||
if (dt > 0.001) {
|
this.physicsRafId = requestAnimationFrame(this.physicsTick);
|
||||||
this.velocity.x = dx / dt;
|
}
|
||||||
this.velocity.y = dy / dt;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
// Reset pending delta
|
private readonly ROTATION_LIMIT = 5;
|
||||||
this.pendingDelta = { x: 0, y: 0 };
|
private minLimit: number = -5;
|
||||||
|
private maxLimit: number = 5;
|
||||||
|
|
||||||
// Update position
|
private followers: { card: LayoutCard, offsetRotation: number }[] = [];
|
||||||
const newX = this.card.x + dx;
|
|
||||||
const newY = this.card.y + dy;
|
|
||||||
|
|
||||||
// Apply Constraints
|
addFollower(card: LayoutCard) {
|
||||||
const constrained = this.card.getConstrainedPosition(newX, newY);
|
// Calculate relative rotation
|
||||||
|
// follower = leader + offset => offset = follower - leader
|
||||||
|
const offset = this.normalizeAngle(card.rotation - this.card.rotation);
|
||||||
|
this.followers.push({ card, offsetRotation: offset });
|
||||||
|
|
||||||
this.card.update({
|
// Add follower mass to leader
|
||||||
x: constrained.x,
|
this.mass += card.physics.mass;
|
||||||
y: constrained.y,
|
this.updateMassScale();
|
||||||
rotation: this.card.rotation // No rotation change for now
|
|
||||||
}, { markDirty: true });
|
// Constrain leader limits to ensure follower stays within [-5, 5]
|
||||||
} else {
|
// -5 <= leader + offset <= 5
|
||||||
// Minimal Decay: Stop immediately for now (skeleton behavior)
|
// -5 - offset <= leader <= 5 - offset
|
||||||
this.stopPhysicsLoop();
|
this.minLimit = Math.max(this.minLimit, -this.ROTATION_LIMIT - offset);
|
||||||
return;
|
this.maxLimit = Math.min(this.maxLimit, this.ROTATION_LIMIT - offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearFollowers() {
|
||||||
|
this.followers = [];
|
||||||
|
// Reset mass to base mass
|
||||||
|
this.mass = this.baseMass;
|
||||||
|
this.updateMassScale();
|
||||||
|
// Reset limits
|
||||||
|
this.minLimit = -this.ROTATION_LIMIT;
|
||||||
|
this.maxLimit = this.ROTATION_LIMIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
private updatePhysics(dt: number) {
|
||||||
|
const dx = this.pendingDelta.x;
|
||||||
|
const dy = this.pendingDelta.y;
|
||||||
|
|
||||||
|
this.pendingDelta = { x: 0, y: 0 };
|
||||||
|
|
||||||
|
const vx = dx / dt;
|
||||||
|
const vy = dy / dt;
|
||||||
|
|
||||||
|
// 1. Update Position (Direct 1:1 movement)
|
||||||
|
const newX = this.card.x + dx;
|
||||||
|
const newY = this.card.y + dy;
|
||||||
|
const constrained = this.card.getConstrainedPosition(newX, newY);
|
||||||
|
|
||||||
|
// 2. Calculate Torque & Forces
|
||||||
|
let torque = 0;
|
||||||
|
let recoveryTorque = 0;
|
||||||
|
let isRecovering = false;
|
||||||
|
|
||||||
|
// Drag Torque
|
||||||
|
if (this.dragOffset && this._isDragging) {
|
||||||
|
const rad = this.card.rotation * Math.PI / 180;
|
||||||
|
const cos = Math.cos(rad);
|
||||||
|
const sin = Math.sin(rad);
|
||||||
|
|
||||||
|
const worldLeverX = this.dragOffset.x * cos - this.dragOffset.y * sin;
|
||||||
|
const worldLeverY = this.dragOffset.x * sin + this.dragOffset.y * cos;
|
||||||
|
|
||||||
|
torque = worldLeverX * vy - worldLeverY * vx;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request next frame
|
// Recovery Torque
|
||||||
this.physicsRafId = requestAnimationFrame(this.physicsTick);
|
const normRot = this.normalizeAngle(this.card.rotation);
|
||||||
};
|
if (normRot > this.maxLimit) {
|
||||||
|
recoveryTorque = (this.maxLimit - normRot) * 2500;
|
||||||
|
isRecovering = true;
|
||||||
|
} else if (normRot < this.minLimit) {
|
||||||
|
recoveryTorque = (this.minLimit - normRot) * 2500;
|
||||||
|
isRecovering = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalTorque = torque + recoveryTorque;
|
||||||
|
const alpha = (totalTorque * 0.05) / this.massScale;
|
||||||
|
this.angularVelocity += alpha * dt;
|
||||||
|
|
||||||
|
// Friction
|
||||||
|
this.angularVelocity *= 0.85;
|
||||||
|
if (isRecovering) {
|
||||||
|
this.angularVelocity *= 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deadzone
|
||||||
|
if (Math.abs(this.angularVelocity) < 1) {
|
||||||
|
this.angularVelocity = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Update Rotation
|
||||||
|
let newRot = this.card.rotation + this.angularVelocity * dt;
|
||||||
|
|
||||||
|
// Ratchet clamping
|
||||||
|
const newNormRot = this.normalizeAngle(newRot);
|
||||||
|
|
||||||
|
if (newNormRot > this.maxLimit) {
|
||||||
|
// If moving further past max, clamp
|
||||||
|
if (newNormRot > normRot) {
|
||||||
|
newRot = this.card.rotation + (this.maxLimit - normRot);
|
||||||
|
this.angularVelocity = 0;
|
||||||
|
}
|
||||||
|
} else if (newNormRot < this.minLimit) {
|
||||||
|
// If moving further past min, clamp
|
||||||
|
if (newNormRot < normRot) {
|
||||||
|
newRot = this.card.rotation + (this.minLimit - normRot);
|
||||||
|
this.angularVelocity = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Pivot Correction
|
||||||
|
let correctionX = 0;
|
||||||
|
let correctionY = 0;
|
||||||
|
|
||||||
|
if (this.dragOffset) {
|
||||||
|
const oldRad = this.card.rotation * Math.PI / 180;
|
||||||
|
const oldCos = Math.cos(oldRad);
|
||||||
|
const oldSin = Math.sin(oldRad);
|
||||||
|
|
||||||
|
const newRad = newRot * Math.PI / 180;
|
||||||
|
const newCos = Math.cos(newRad);
|
||||||
|
const newSin = Math.sin(newRad);
|
||||||
|
|
||||||
|
const oldLeverX = this.dragOffset.x * oldCos - this.dragOffset.y * oldSin;
|
||||||
|
const oldLeverY = this.dragOffset.x * oldSin + this.dragOffset.y * oldCos;
|
||||||
|
|
||||||
|
const newLeverX = this.dragOffset.x * newCos - this.dragOffset.y * newSin;
|
||||||
|
const newLeverY = this.dragOffset.x * newSin + this.dragOffset.y * newCos;
|
||||||
|
|
||||||
|
correctionX = oldLeverX - newLeverX;
|
||||||
|
correctionY = oldLeverY - newLeverY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop Condition
|
||||||
|
if (!this._isDragging) {
|
||||||
|
const currentNormRot = this.normalizeAngle(this.card.rotation);
|
||||||
|
const isOutside = currentNormRot > this.maxLimit || currentNormRot < this.minLimit;
|
||||||
|
|
||||||
|
if (isOutside) {
|
||||||
|
const targetAngle = currentNormRot > this.maxLimit ? this.maxLimit : this.minLimit;
|
||||||
|
const dist = Math.abs(this.normalizeAngle(currentNormRot - targetAngle));
|
||||||
|
|
||||||
|
if (Math.abs(this.angularVelocity) < 0.5 && dist < 0.1) {
|
||||||
|
this.card.update({ rotation: targetAngle }, { markDirty: true });
|
||||||
|
this.angularVelocity = 0;
|
||||||
|
this.stopPhysicsLoop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Inside range - just stop if slow
|
||||||
|
if (Math.abs(this.angularVelocity) < 0.5) {
|
||||||
|
this.angularVelocity = 0;
|
||||||
|
this.stopPhysicsLoop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Apply to Leader
|
||||||
|
const finalX = constrained.x + correctionX;
|
||||||
|
const finalY = constrained.y + correctionY;
|
||||||
|
const finalConstrained = this.card.getConstrainedPosition(finalX, finalY);
|
||||||
|
|
||||||
|
this.card.update({
|
||||||
|
x: finalConstrained.x,
|
||||||
|
y: finalConstrained.y,
|
||||||
|
rotation: newRot
|
||||||
|
}, { markDirty: true });
|
||||||
|
|
||||||
|
// 6. Apply to Followers
|
||||||
|
for (const follower of this.followers) {
|
||||||
|
// Followers match leader's position exactly (center aligned)
|
||||||
|
// But we need to account for their own dimensions if we want center-to-center alignment
|
||||||
|
// The LayoutCard.x/y is top-left.
|
||||||
|
// Leader Center: finalConstrained.x + leader.width/2, finalConstrained.y + leader.height/2
|
||||||
|
|
||||||
|
const leaderCenterX = finalConstrained.x + this.card.width / 2;
|
||||||
|
const leaderCenterY = finalConstrained.y + this.card.height / 2;
|
||||||
|
|
||||||
|
const followerX = leaderCenterX - follower.card.width / 2;
|
||||||
|
const followerY = leaderCenterY - follower.card.height / 2;
|
||||||
|
|
||||||
|
const followerRot = newRot + follower.offsetRotation;
|
||||||
|
|
||||||
|
follower.card.update({
|
||||||
|
x: followerX,
|
||||||
|
y: followerY,
|
||||||
|
rotation: followerRot
|
||||||
|
}, { markDirty: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -252,9 +252,14 @@ const DesktopWorkspaceContent: React.FC<DesktopWorkspaceProps> = ({
|
|||||||
const isSelected = selectedDocumentIds.includes(docId);
|
const isSelected = selectedDocumentIds.includes(docId);
|
||||||
const size = ensureDocumentSize(doc);
|
const size = ensureDocumentSize(doc);
|
||||||
|
|
||||||
|
// Extract page count
|
||||||
|
const metadata = doc.current_version?.metadata as { page_count?: number } | undefined;
|
||||||
|
const pageCount = metadata?.page_count ?? 1;
|
||||||
|
|
||||||
const layoutCard = layoutStore.initialize(docId, null, {
|
const layoutCard = layoutStore.initialize(docId, null, {
|
||||||
width: Number.isFinite(size.width) && size.width > 0 ? size.width : 200,
|
width: Number.isFinite(size.width) && size.width > 0 ? size.width : 200,
|
||||||
height: Number.isFinite(size.height) && size.height > 0 ? size.height : 200
|
height: Number.isFinite(size.height) && size.height > 0 ? size.height : 200,
|
||||||
|
pageCount
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export interface LayoutCardState {
|
|||||||
rotation: number;
|
rotation: number;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
|
pageCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LayoutCard implements LayoutCardState {
|
export class LayoutCard implements LayoutCardState {
|
||||||
@@ -21,6 +22,7 @@ export class LayoutCard implements LayoutCardState {
|
|||||||
rotation: number = 0;
|
rotation: number = 0;
|
||||||
width: number = 0;
|
width: number = 0;
|
||||||
height: number = 0;
|
height: number = 0;
|
||||||
|
pageCount: number = 1;
|
||||||
ref: HTMLElement | null = null;
|
ref: HTMLElement | null = null;
|
||||||
|
|
||||||
private _innerRadius: number = 0;
|
private _innerRadius: number = 0;
|
||||||
@@ -50,8 +52,6 @@ export class LayoutCard implements LayoutCardState {
|
|||||||
this.applyTransform();
|
this.applyTransform();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
getConstrainedPosition(x: number, y: number): { x: number, y: number } {
|
getConstrainedPosition(x: number, y: number): { x: number, y: number } {
|
||||||
const rad = (this.rotation * Math.PI) / 180;
|
const rad = (this.rotation * Math.PI) / 180;
|
||||||
const sin = Math.abs(Math.sin(rad));
|
const sin = Math.abs(Math.sin(rad));
|
||||||
@@ -90,6 +90,10 @@ export class LayoutCard implements LayoutCardState {
|
|||||||
this.recalculateCenters();
|
this.recalculateCenters();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (changes.pageCount !== undefined) {
|
||||||
|
this.physics.updateMass(changes.pageCount);
|
||||||
|
}
|
||||||
|
|
||||||
if (options.markDirty) {
|
if (options.markDirty) {
|
||||||
this.isDirty = true;
|
this.isDirty = true;
|
||||||
}
|
}
|
||||||
@@ -240,7 +244,8 @@ export class LayoutCard implements LayoutCardState {
|
|||||||
z: this.z,
|
z: this.z,
|
||||||
rotation: this.rotation,
|
rotation: this.rotation,
|
||||||
width: this.width,
|
width: this.width,
|
||||||
height: this.height
|
height: this.height,
|
||||||
|
pageCount: this.pageCount
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,6 +283,7 @@ export class LayoutStore {
|
|||||||
initialize(id: string, ref: HTMLElement | null, config: {
|
initialize(id: string, ref: HTMLElement | null, config: {
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
|
pageCount: number;
|
||||||
}) {
|
}) {
|
||||||
let card = this.items.get(id);
|
let card = this.items.get(id);
|
||||||
|
|
||||||
@@ -309,14 +315,16 @@ export class LayoutStore {
|
|||||||
rotation,
|
rotation,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
z
|
z,
|
||||||
|
pageCount: config.pageCount
|
||||||
}, ref);
|
}, ref);
|
||||||
} else {
|
} else {
|
||||||
// Create card with temporary position
|
// Create card with temporary position
|
||||||
card = new LayoutCard(id, this, {
|
card = new LayoutCard(id, this, {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
z: this.zCounter++
|
z: this.zCounter++,
|
||||||
|
pageCount: config.pageCount
|
||||||
}, ref);
|
}, ref);
|
||||||
|
|
||||||
// Calculate initial position using the card instance
|
// Calculate initial position using the card instance
|
||||||
@@ -333,7 +341,7 @@ export class LayoutStore {
|
|||||||
|
|
||||||
this.items.set(id, card);
|
this.items.set(id, card);
|
||||||
} else {
|
} else {
|
||||||
card.update({ width, height }, { markDirty: false });
|
card.update({ width, height, pageCount: config.pageCount }, { markDirty: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always update ref and ensure transform is applied
|
// Always update ref and ensure transform is applied
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.desk-item--swoop {
|
.desk-item--swoop {
|
||||||
transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
|
transition: transform 0.5s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.desk-item__card img {
|
.desk-item__card img {
|
||||||
|
|||||||
Reference in New Issue
Block a user