Add drop shadow system with animated lift/drop during drag

DropShadowFilter applied to all ImageSprites for photos-on-a-desk feel.
SelectionManager now handles object dragging with shadow lift/drop and
spring-animated scale (1.0 <-> 1.03). Replaced incompatible @pixi/filter-drop-shadow
v5 with pixi-filters v6 for PixiJS v8 compatibility.
This commit is contained in:
Hiren Kangad
2026-03-09 23:35:02 +05:30
parent 7f6c9cd409
commit 9e4a427f97
4 changed files with 148 additions and 297 deletions
+98 -4
View File
@@ -9,6 +9,8 @@ import { Container, Graphics, FederatedPointerEvent } from 'pixi.js';
import type { Viewport } from 'pixi-viewport';
import { SceneManager, SceneItem } from './SceneManager';
import { TransformBox } from './TransformBox';
import { ImageSprite } from './sprites/ImageSprite';
import { Spring, PRESETS } from './spring';
// ---------------------------------------------------------------------------
// Constants
@@ -20,6 +22,8 @@ const BAND_STROKE = 0x4a90d9;
const BAND_STROKE_ALPHA = 0.6;
const BAND_STROKE_WIDTH = 1;
const RUBBER_BAND_THRESHOLD = 5; // px in screen space before rubber band activates
const DRAG_THRESHOLD = 5; // px in screen space before object drag activates
const DRAG_LIFT_SCALE = 1.03; // scale during drag
// ---------------------------------------------------------------------------
// SelectionManager
@@ -44,6 +48,11 @@ export class SelectionManager {
private _rubberBanding = false;
private _hitItemOnDown: SceneItem | null = null;
// Object drag state
private _objectDragging = false;
private _lastDragWorldX = 0;
private _lastDragWorldY = 0;
constructor(viewport: Viewport, scene: SceneManager) {
this._viewport = viewport;
this._scene = scene;
@@ -177,13 +186,46 @@ export class SelectionManager {
private _onPointerMove(e: FederatedPointerEvent): void {
if (!this._pointerDown) return;
// If we clicked on an object, don't start rubber banding
if (this._hitItemOnDown) return;
const dx = e.global.x - this._downScreenX;
const dy = e.global.y - this._downScreenY;
const dist = Math.sqrt(dx * dx + dy * dy);
// Object drag: clicked on a selected item
if (this._hitItemOnDown && this.selectedIds.has(this._hitItemOnDown.id)) {
if (!this._objectDragging && dist >= DRAG_THRESHOLD) {
this._objectDragging = true;
this._lastDragWorldX = this._downWorldX;
this._lastDragWorldY = this._downWorldY;
// Pause viewport drag so object drag works
this._viewport.plugins.pause('drag');
// Lift shadow + spring scale on all selected image sprites
this._applyLift();
}
if (this._objectDragging) {
const currentWorld = this._viewport.toWorld(e.global.x, e.global.y);
const ddx = currentWorld.x - this._lastDragWorldX;
const ddy = currentWorld.y - this._lastDragWorldY;
this._lastDragWorldX = currentWorld.x;
this._lastDragWorldY = currentWorld.y;
// Move all selected items by delta
for (const item of this.getSelectedItems()) {
item.displayObject.x += ddx;
item.displayObject.y += ddy;
item.data.x = item.displayObject.x;
item.data.y = item.displayObject.y;
}
this.transformBox.update(this.getSelectedItems());
}
return;
}
// Rubber band: clicked on empty space
if (this._hitItemOnDown) return;
if (!this._rubberBanding && dist >= RUBBER_BAND_THRESHOLD) {
this._rubberBanding = true;
// Pause viewport drag so rubber band works
@@ -200,7 +242,17 @@ export class SelectionManager {
if (!this._pointerDown) return;
this._pointerDown = false;
if (this._rubberBanding) {
if (this._objectDragging) {
// End object drag — drop shadow + spring scale back
this._applyDrop();
this._objectDragging = false;
// Resume viewport drag
this._viewport.plugins.resume('drag');
// Notify change (positions updated)
this._emitChange();
} else if (this._rubberBanding) {
// Finish rubber band selection
const currentWorld = this._viewport.toWorld(e.global.x, e.global.y);
this._finishRubberBand(this._downWorldX, this._downWorldY, currentWorld.x, currentWorld.y, e.shiftKey);
@@ -220,6 +272,48 @@ export class SelectionManager {
this._hitItemOnDown = null;
}
// -- Drag Shadow Lift / Drop ----------------------------------------------
/** Lift shadows and spring-scale selected items up for drag. */
private _applyLift(): void {
const items = this.getSelectedItems();
for (const item of items) {
const dobj = item.displayObject;
// Lift shadow on ImageSprites
if (dobj instanceof ImageSprite) {
dobj.liftShadow();
}
// Spring scale 1.0 → 1.03
const spring = new Spring(1.0, DRAG_LIFT_SCALE, PRESETS.snappy);
spring.onUpdate = (v) => {
dobj.scale.set(v, v);
};
this._scene.springs.add(spring);
}
}
/** Drop shadows and spring-scale selected items back to rest. */
private _applyDrop(): void {
const items = this.getSelectedItems();
for (const item of items) {
const dobj = item.displayObject;
// Drop shadow on ImageSprites
if (dobj instanceof ImageSprite) {
dobj.dropShadow();
}
// Spring scale 1.03 → 1.0
const spring = new Spring(DRAG_LIFT_SCALE, 1.0, PRESETS.snappy);
spring.onUpdate = (v) => {
dobj.scale.set(v, v);
};
this._scene.springs.add(spring);
}
}
// -- Rubber Band ----------------------------------------------------------
private _drawRubberBand(x1: number, y1: number, x2: number, y2: number): void {
@@ -1,4 +1,5 @@
import { Sprite, Texture, Graphics } from "pixi.js";
import { DropShadowFilter } from "pixi-filters";
import { TextureManager, LODTier } from "../TextureManager";
/**
@@ -6,8 +7,14 @@ import { TextureManager, LODTier } from "../TextureManager";
* Shows a placeholder shimmer rect until the first texture tier loads,
* then swaps textures as zoom level changes.
*/
// Shadow defaults (resting state)
const SHADOW_REST = { offsetX: 3, offsetY: 3, blur: 4, alpha: 0.25 };
// Shadow lifted state (during drag)
const SHADOW_LIFT = { offsetX: 6, offsetY: 8, blur: 8, alpha: 0.35 };
export class ImageSprite extends Sprite {
readonly assetKey: string;
readonly shadow: DropShadowFilter;
private textures: TextureManager;
private currentTier: LODTier | null = null;
@@ -32,6 +39,15 @@ export class ImageSprite extends Sprite {
this.width = w;
this.height = h;
// Drop shadow for photos-on-a-desk feel
this.shadow = new DropShadowFilter({
offset: { x: SHADOW_REST.offsetX, y: SHADOW_REST.offsetY },
blur: SHADOW_REST.blur,
alpha: SHADOW_REST.alpha,
color: 0x000000,
});
this.filters = [this.shadow];
// Create placeholder: dark rect shown until first texture loads
const placeholder = new Graphics();
placeholder.rect(0, 0, w, h).fill(0x2a2a2a);
@@ -42,6 +58,20 @@ export class ImageSprite extends Sprite {
this.loadTier("thumb");
}
/** Expand shadow for drag-lift effect. */
liftShadow(): void {
this.shadow.offset = { x: SHADOW_LIFT.offsetX, y: SHADOW_LIFT.offsetY };
this.shadow.blur = SHADOW_LIFT.blur;
this.shadow.alpha = SHADOW_LIFT.alpha;
}
/** Restore shadow to resting state. */
dropShadow(): void {
this.shadow.offset = { x: SHADOW_REST.offsetX, y: SHADOW_REST.offsetY };
this.shadow.blur = SHADOW_REST.blur;
this.shadow.alpha = SHADOW_REST.alpha;
}
/**
* Load a specific LOD tier texture for this sprite.
* Skips if already at the requested tier or currently loading.