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
+19 -292
View File
@@ -8,8 +8,8 @@
"name": "refboard-frontend",
"version": "1.0.0",
"dependencies": {
"@pixi/filter-drop-shadow": "^5.2.0",
"axios": "^1.7.9",
"pixi-filters": "^6.1.5",
"pixi-viewport": "^6.0.3",
"pixi.js": "^8.17.0",
"react": "^18.3.1",
@@ -799,132 +799,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@pixi/color": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/@pixi/color/-/color-7.4.3.tgz",
"integrity": "sha512-a6R+bXKeXMDcRmjYQoBIK+v2EYqxSX49wcjAY579EYM/WrFKS98nSees6lqVUcLKrcQh2DT9srJHX7XMny3voQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@pixi/colord": "^2.9.6"
}
},
"node_modules/@pixi/colord": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@pixi/colord/-/colord-2.9.6.tgz",
"integrity": "sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA==",
"license": "MIT"
},
"node_modules/@pixi/constants": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/@pixi/constants/-/constants-7.4.3.tgz",
"integrity": "sha512-QGmwJUNQy/vVEHzL6VGQvnwawLZ1wceZMI8HwJAT4/I2uAzbBeFDdmCS8WsTpSWLZjF/DszDc1D8BFp4pVJ5UQ==",
"license": "MIT",
"peer": true
},
"node_modules/@pixi/core": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/@pixi/core/-/core-7.4.3.tgz",
"integrity": "sha512-5YDs11faWgVVTL8VZtLU05/Fl47vaP5Tnsbf+y/WRR0VSW3KhRRGTBU1J3Gdc2xEWbJhUK07KGP7eSZpvtPVgA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@pixi/color": "7.4.3",
"@pixi/constants": "7.4.3",
"@pixi/extensions": "7.4.3",
"@pixi/math": "7.4.3",
"@pixi/runner": "7.4.3",
"@pixi/settings": "7.4.3",
"@pixi/ticker": "7.4.3",
"@pixi/utils": "7.4.3"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/pixijs"
}
},
"node_modules/@pixi/extensions": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/@pixi/extensions/-/extensions-7.4.3.tgz",
"integrity": "sha512-FhoiYkHQEDYHUE7wXhqfsTRz6KxLXjuMbSiAwnLb9uG1vAgp6q6qd6HEsf4X30YaZbLFY8a4KY6hFZWjF+4Fdw==",
"license": "MIT",
"peer": true
},
"node_modules/@pixi/filter-drop-shadow": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@pixi/filter-drop-shadow/-/filter-drop-shadow-5.2.0.tgz",
"integrity": "sha512-cYS2KDER7cwCu0V4VNSxTHGvzmNcEXdC9j3031YBOkUAE3+p17LMS/TAt6XeMfJV7KaPuusvXy2NFgGkv3RDbw==",
"license": "MIT",
"dependencies": {
"@pixi/filter-kawase-blur": "5.1.1"
},
"peerDependencies": {
"@pixi/core": "^7.0.0-X"
}
},
"node_modules/@pixi/filter-kawase-blur": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@pixi/filter-kawase-blur/-/filter-kawase-blur-5.1.1.tgz",
"integrity": "sha512-nPnJ1ChBFP+4pgFCwC0RJgHAJCetiHcQU3INH7zCdq88cFABmVmhN+wCKRNg4H7lF1EJjaXgFDkTrTreOD/bnw==",
"license": "MIT",
"peerDependencies": {
"@pixi/core": "^7.0.0-X"
}
},
"node_modules/@pixi/math": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/@pixi/math/-/math-7.4.3.tgz",
"integrity": "sha512-/uJOVhR2DOZ+zgdI6Bs/CwcXT4bNRKsS+TqX3ekRIxPCwaLra+Qdm7aDxT5cTToDzdxbKL5+rwiLu3Y1egILDw==",
"license": "MIT",
"peer": true
},
"node_modules/@pixi/runner": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/@pixi/runner/-/runner-7.4.3.tgz",
"integrity": "sha512-TJyfp7y23u5vvRAyYhVSa7ytq0PdKSvPLXu4G3meoFh1oxTLHH6g/RIzLuxUAThPG2z7ftthuW3qWq6dRV+dhw==",
"license": "MIT",
"peer": true
},
"node_modules/@pixi/settings": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/@pixi/settings/-/settings-7.4.3.tgz",
"integrity": "sha512-SmGK8smc0PxRB9nr0UJioEtE9hl4gvj9OedCvZx3bxBwA3omA5BmP3CyhQfN8XJ29+o2OUL01r3zAPVol4l4lA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@pixi/constants": "7.4.3",
"@types/css-font-loading-module": "^0.0.12",
"ismobilejs": "^1.1.0"
}
},
"node_modules/@pixi/ticker": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/@pixi/ticker/-/ticker-7.4.3.tgz",
"integrity": "sha512-tHsAD0iOUb6QSGGw+c8cyRBvxsq/NlfzIFBZLEHhWZ+Bx4a0MmXup6I/yJDGmyPCYE+ctCcAfY13wKAzdiVFgQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@pixi/extensions": "7.4.3",
"@pixi/settings": "7.4.3",
"@pixi/utils": "7.4.3"
}
},
"node_modules/@pixi/utils": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/@pixi/utils/-/utils-7.4.3.tgz",
"integrity": "sha512-NO3Y9HAn2UKS1YdxffqsPp+kDpVm8XWvkZcS/E+rBzY9VTLnNOI7cawSRm+dacdET3a8Jad3aDKEDZ0HmAqAFA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@pixi/color": "7.4.3",
"@pixi/constants": "7.4.3",
"@pixi/settings": "7.4.3",
"@types/earcut": "^2.1.0",
"earcut": "^2.2.4",
"eventemitter3": "^4.0.0",
"url": "^0.11.0"
}
},
"node_modules/@remix-run/router": {
"version": "1.23.2",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
@@ -1342,20 +1222,6 @@
"@babel/types": "^7.28.2"
}
},
"node_modules/@types/css-font-loading-module": {
"version": "0.0.12",
"resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.12.tgz",
"integrity": "sha512-x2tZZYkSxXqWvTDgveSynfjq/T2HyiZHXb00j/+gy19yp70PHCizM48XFdjBCWH7eHBD0R5i/pw9yMBP/BH5uA==",
"license": "MIT",
"peer": true
},
"node_modules/@types/earcut": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-2.1.4.tgz",
"integrity": "sha512-qp3m9PPz4gULB9MhjGID7wpo3gJ4bTGXm7ltNDsmOvsPduTeHp8wSW9YckBj3mljeOh4F0m2z/0JKAALRKbmLQ==",
"license": "MIT",
"peer": true
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -1363,6 +1229,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/gradient-parser": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/@types/gradient-parser/-/gradient-parser-0.1.5.tgz",
"integrity": "sha512-r7K3NkJz3A95WkVVmjs0NcchhHstC2C/VIYNX4JC6tieviUNo774FFeOHjThr3Vw/WCeMP9kAT77MKbIRlO/4w==",
"license": "MIT"
},
"node_modules/@types/prop-types": {
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
@@ -1504,23 +1376,6 @@
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"peer": true,
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001777",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz",
@@ -1608,13 +1463,6 @@
"node": ">= 0.4"
}
},
"node_modules/earcut": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz",
"integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==",
"license": "ISC",
"peer": true
},
"node_modules/electron-to-chromium": {
"version": "1.5.307",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz",
@@ -1762,13 +1610,6 @@
"node": ">=6"
}
},
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"license": "MIT",
"peer": true
},
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@@ -2082,19 +1923,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/parse-svg-path": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz",
@@ -2121,6 +1949,18 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pixi-filters": {
"version": "6.1.5",
"resolved": "https://registry.npmjs.org/pixi-filters/-/pixi-filters-6.1.5.tgz",
"integrity": "sha512-Ewb/J+kxAbaNN+0/ATJbglAJG+skGJfh7BIDP3ILIDdD6wWk1p0pGa25pVf1T8hGBOQSUNVAmwwJBwkj+cyLLA==",
"license": "MIT",
"dependencies": {
"@types/gradient-parser": "^0.1.2"
},
"peerDependencies": {
"pixi.js": ">=8.0.0-0"
}
},
"node_modules/pixi-viewport": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/pixi-viewport/-/pixi-viewport-6.0.3.tgz",
@@ -2209,22 +2049,6 @@
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/qs": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
"integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==",
"license": "BSD-3-Clause",
"peer": true,
"dependencies": {
"side-channel": "^1.1.0"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
@@ -2356,82 +2180,6 @@
"semver": "bin/semver.js"
}
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"license": "MIT",
"peer": true,
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3",
"side-channel-list": "^1.0.0",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-list": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"license": "MIT",
"peer": true,
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-map": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"license": "MIT",
"peer": true,
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-weakmap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"license": "MIT",
"peer": true,
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3",
"side-channel-map": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/socket.io-client": {
"version": "4.8.3",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz",
@@ -2541,27 +2289,6 @@
"browserslist": ">= 4.21.0"
}
},
"node_modules/url": {
"version": "0.11.4",
"resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz",
"integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==",
"license": "MIT",
"peer": true,
"dependencies": {
"punycode": "^1.4.1",
"qs": "^6.12.3"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/url/node_modules/punycode": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
"integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
"license": "MIT",
"peer": true
},
"node_modules/vite": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
+1 -1
View File
@@ -9,8 +9,8 @@
"preview": "vite preview"
},
"dependencies": {
"@pixi/filter-drop-shadow": "^5.2.0",
"axios": "^1.7.9",
"pixi-filters": "^6.1.5",
"pixi-viewport": "^6.0.3",
"pixi.js": "^8.17.0",
"react": "^18.3.1",
+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.