Files
refboard-ayon/frontend/src/canvas/sprites/FrameSprite.ts
T
Hiren Kangad e9509ef7b0 feat(refboard): canvas polish — transform box sync, dot grid, snap tuning, perf fixes
- Fix transform box not updating after alignment/arrangement/normalize/flip operations
  (shortcuts, context menu, and selection toolbar all fixed with DRY _opUpdate helper)
- Replace 100ms polling loop with event-driven viewport updates (moved + wheel-scroll)
- Add adaptive dot grid background that responds to zoom/pan
- Reduce snap guide threshold from 8px to 4px for subtler snapping
- Remove PresenceOverlay (remote selection highlighting) — too heavy for minimal benefit
- Offset multiple dropped images so they don't overlap
- Add new canvas modules: SnapGuides, clipboard, grouping, FrameSprite, DrawingSprite,
  LaserPointer, context-menu-items, SelectionToolbar, Minimap
- Extract Editor hooks into dedicated files (useBoardLoader, useCanvasSetup,
  useShortcutHandler, useLayerPanel, useSaveManager, useFollowMode)
- Sync improvements: real-time transform broadcast, board rooms, viewport sync
2026-03-10 03:58:53 +05:30

147 lines
4.0 KiB
TypeScript

/**
* FrameSprite — visual container for groups/frames.
*
* Renders as a colored BORDER (not fill) with rounded corners + title label.
* Extends Container. The border and label are the first children,
* so actual group children render on top.
* Lightweight: one Graphics rect + one Text. No filters or shaders.
*/
import { Container, Graphics, Text, TextStyle } from 'pixi.js';
import type { GroupObject } from '../scene-format';
const DEFAULT_PADDING = 16;
const LABEL_FONT_SIZE = 11;
const BORDER_WIDTH = 2;
const CORNER_RADIUS = 6;
const FRAME_COLORS = [
'#4a90d9', '#69db7c', '#ff6b6b', '#7950f2', '#38d9a9',
'#ffa94d', '#e64980', '#20c997', '#4dabf7', '#ffd43b',
];
/** Pick a random frame color for new groups. */
export function randomFrameColor(): string {
return FRAME_COLORS[Math.floor(Math.random() * FRAME_COLORS.length)];
}
export class FrameSprite extends Container {
private _bg: Graphics;
private _label: Text;
private _labelBg: Graphics;
private _bgColor: string;
private _padding: number;
private _frameW: number;
private _frameH: number;
private _labelText: string;
constructor(w: number, h: number, bgColor?: string, label?: string, padding?: number) {
super();
this._bgColor = bgColor || '';
this._padding = padding ?? DEFAULT_PADDING;
this._frameW = w;
this._frameH = h;
this._labelText = label || '';
// Border rect (drawn first = behind everything)
this._bg = new Graphics();
this._bg.label = '__frame_bg';
this.addChild(this._bg);
// Label background pill
this._labelBg = new Graphics();
this._labelBg.label = '__frame_labelbg';
this.addChild(this._labelBg);
// Title label
this._label = new Text({
text: this._labelText,
style: new TextStyle({
fontSize: LABEL_FONT_SIZE,
fontFamily: 'system-ui, -apple-system, sans-serif',
fill: '#ffffff',
fontWeight: '600',
}),
});
this._label.label = '__frame_label';
this.addChild(this._label);
this._redraw();
}
get bgColor(): string { return this._bgColor; }
get padding(): number { return this._padding; }
setBgColor(color: string): void {
this._bgColor = color;
this._redraw();
}
setLabel(text: string): void {
this._labelText = text;
this._label.text = text;
this._redraw();
}
/** Update frame dimensions (call after children bounds change). */
setFrameSize(w: number, h: number): void {
this._frameW = w;
this._frameH = h;
this._redraw();
}
/** Update from GroupObject data. */
updateFromData(data: GroupObject): void {
this._bgColor = data.bgColor || '';
this._labelText = data.label || '';
this._padding = data.padding ?? DEFAULT_PADDING;
this._frameW = data.w;
this._frameH = data.h;
this._label.text = this._labelText;
this._redraw();
}
private _redraw(): void {
this._bg.clear();
this._labelBg.clear();
if (!this._bgColor) {
this._bg.visible = false;
this._labelBg.visible = false;
this._label.visible = false;
return;
}
this._bg.visible = true;
const pad = this._padding;
const color = parseInt(this._bgColor.replace('#', ''), 16);
// Draw border-only rounded rect (no fill, just stroke)
this._bg.roundRect(-pad, -pad, this._frameW + pad * 2, this._frameH + pad * 2, CORNER_RADIUS);
this._bg.stroke({
color,
alpha: 0.6,
width: BORDER_WIDTH,
});
// Label positioned at top-left corner, overlapping the border
const hasLabel = this._labelText.length > 0;
this._label.visible = hasLabel;
this._labelBg.visible = hasLabel;
if (hasLabel) {
const lx = -pad;
const ly = -pad - LABEL_FONT_SIZE - 6;
this._label.position.set(lx + 8, ly + 3);
// Background pill behind label text
const lw = this._label.width + 16;
const lh = LABEL_FONT_SIZE + 6;
this._labelBg.roundRect(lx, ly, lw, lh, CORNER_RADIUS);
this._labelBg.fill({ color, alpha: 0.8 });
}
}
}