diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b3b88e0..e8fda03 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index 56fbe18..082ca16 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/canvas/SelectionManager.ts b/frontend/src/canvas/SelectionManager.ts index 28fec72..259d9ed 100644 --- a/frontend/src/canvas/SelectionManager.ts +++ b/frontend/src/canvas/SelectionManager.ts @@ -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 { diff --git a/frontend/src/canvas/sprites/ImageSprite.ts b/frontend/src/canvas/sprites/ImageSprite.ts index c024374..b330b15 100644 --- a/frontend/src/canvas/sprites/ImageSprite.ts +++ b/frontend/src/canvas/sprites/ImageSprite.ts @@ -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.