feat: annotation UX overhaul, animated packing, toolbar redesign

- PinOverlay: zero-allocation rendering with in-place Graphics/Text updates,
  fix WebGL texture crash (addressModeU null) from orphaned Text objects
- Pins always visible on media, track during drag via updatePositions() fast path
- Replace eraser with Review mode in toolbar tool group (orange gradient)
- Add tool instruction hints bar below toolbar (context-sensitive per tool)
- Feedback panel: glassmorphism, improved ThreadDetail/CommentItem/ThreadListItem
- Timestamps: relative with "ago" suffix, full date on hover tooltip
- De-emphasize pin numbers, show status text in thread list
- Animated arrangement transitions (easeOutCubic, 320ms) for pack/grid/row/column
- Disable snap guides (user requested)
- Paste now selects newly created items
- Remove eraser keyboard shortcuts, remap tool shortcuts
- Orphaned thread cleanup: safe destroy check for deleted media pins
This commit is contained in:
Hiren Kangad
2026-03-11 01:53:56 +05:30
parent ed8424d9a1
commit fc2d9df741
16 changed files with 611 additions and 360 deletions
+59 -2
View File
@@ -18,9 +18,66 @@ function scaledH(item: SceneItem): number {
return item.data.h * item.data.sy;
}
/** Sync displayObject position from item.data. */
// ─── Animated position sync ───
let _animTargets = new Map<SceneItem, { startX: number; startY: number; endX: number; endY: number; t: number }>();
let _animRaf = 0;
let _animCallback: ((items: SceneItem[]) => void) | null = null;
const ANIM_DURATION = 320; // ms
const ANIM_STEP = 1000 / 60;
function easeOutCubic(t: number): number {
return 1 - Math.pow(1 - t, 3);
}
function _animTick() {
const dt = ANIM_STEP / ANIM_DURATION;
let done = true;
for (const [item, anim] of _animTargets) {
anim.t = Math.min(1, anim.t + dt);
const e = easeOutCubic(anim.t);
item.displayObject.position.set(
anim.startX + (anim.endX - anim.startX) * e,
anim.startY + (anim.endY - anim.startY) * e,
);
if (anim.t < 1) done = false;
}
if (!done) {
_animRaf = requestAnimationFrame(_animTick);
} else {
// Snap to final positions and clean up
for (const [item, anim] of _animTargets) {
item.displayObject.position.set(anim.endX, anim.endY);
}
const items = Array.from(_animTargets.keys());
_animTargets.clear();
_animRaf = 0;
_animCallback?.(items);
}
}
/** Sync displayObject position from item.data with smooth animation. */
function syncPosition(item: SceneItem): void {
item.displayObject.position.set(item.data.x, item.data.y);
_animTargets.set(item, {
startX: item.displayObject.x,
startY: item.displayObject.y,
endX: item.data.x,
endY: item.data.y,
t: 0,
});
if (!_animRaf) {
_animRaf = requestAnimationFrame(_animTick);
}
}
/**
* Register a callback that fires once when the current arrangement animation finishes.
* Use this to persist/broadcast final positions.
*/
export function onArrangeAnimationDone(cb: (items: SceneItem[]) => void): void {
_animCallback = cb;
}
/** Sync displayObject scale from item.data. */