feat: text tool UX, image crop, text format toolbar, double-click focus

- Text tool: click-to-place immediately opens inline editor, auto-switches
  back to select tool. Empty text cleanup on save.
- Crop: select image + press C (or right-click > Crop) to enter crop mode.
  8 drag handles with rule-of-thirds grid, dimmed outside area.
  Enter confirms, Escape cancels. Non-destructive (stored as normalized rect).
- Text format toolbar: appears when text items selected, with font size +/-,
  font family dropdown, and color picker with presets.
- Double-click image: zoom-to-fit (PureRef-style focus)
- Entrance animation: replaced bounce with simple fade-in (no delay before
  items become interactive)
- TextEditor: allow empty text (caller handles cleanup)
This commit is contained in:
Hiren Kangad
2026-03-11 17:22:25 +05:30
parent 4ed475c62e
commit 58da934ec1
14 changed files with 785 additions and 50 deletions
+5 -41
View File
@@ -291,7 +291,9 @@ export class SceneManager {
if (isGifAsset(imgData.asset)) {
displayObject = new AnimatedGifSprite(imgData.asset, imgData.w, imgData.h, this.textures);
} else {
displayObject = new ImageSprite(imgData.asset, imgData.w, imgData.h, this.textures);
const imgSprite = new ImageSprite(imgData.asset, imgData.w, imgData.h, this.textures);
if (imgData.crop) imgSprite.applyCrop(imgData.crop);
displayObject = imgSprite;
}
break;
}
@@ -378,49 +380,11 @@ export class SceneManager {
};
}
// Animate entrance: spring scale from center 0 → 1.05 → 1.0 with fade-in
// Animate entrance: quick fade-in (no bounce — items are immediately interactive)
if (animate) {
displayObject.scale.set(0, 0);
displayObject.alpha = 0;
// Scale from center by adjusting position to keep center point fixed
const halfW = data.w * data.sx / 2;
const halfH = data.h * data.sy / 2;
const finalX = data.x;
const finalY = data.y;
const scaleSpring = new Spring(0, 1.05, PRESETS.bounce);
scaleSpring.onUpdate = (v) => {
if (!displayObject.destroyed) {
displayObject.scale.set(v * data.sx, v * data.sy);
displayObject.position.set(
finalX + halfW * (1 - v),
finalY + halfH * (1 - v),
);
}
};
scaleSpring.onComplete = () => {
const settleSpring = new Spring(1.05, 1.0, PRESETS.snappy);
settleSpring.onUpdate = (v) => {
if (!displayObject.destroyed) {
displayObject.scale.set(v * data.sx, v * data.sy);
displayObject.position.set(
finalX + halfW * (1 - v),
finalY + halfH * (1 - v),
);
}
};
settleSpring.onComplete = () => {
if (!displayObject.destroyed) {
displayObject.position.set(finalX, finalY);
displayObject.scale.set(data.sx, data.sy);
}
};
this.springs.add(settleSpring);
};
this.springs.add(scaleSpring);
const alphaSpring = new Spring(0, data.opacity, PRESETS.gentle);
const alphaSpring = new Spring(0, data.opacity, PRESETS.snappy);
alphaSpring.onUpdate = (v) => {
if (!displayObject.destroyed) {
displayObject.alpha = v;