feat: zoom-bucket text sharpness + remove fontSize toolbar controls
Add TextSharpnessManager for crisp text rendering at all zoom levels: - Discrete zoom buckets (0.5–3x) avoid texture churn on small zoom changes - Initial bucket applied to all items on setup and newly created items via SceneManager.onItemCreated callback (fixes blurry-on-load) - Visibility check uses getItemWorldBounds for correct grouped item coords - TextSprite/StickySprite gain setZoomBucket() for resolution control Remove fontSize +/- controls from contextual toolbar — font size is now controlled purely through direct manipulation (resize → bake).
This commit is contained in:
@@ -161,6 +161,7 @@ export class SceneManager {
|
|||||||
|
|
||||||
private _onChange: (() => void) | null = null;
|
private _onChange: (() => void) | null = null;
|
||||||
private _onItemDimensionsChanged: ((itemId: string) => void) | null = null;
|
private _onItemDimensionsChanged: ((itemId: string) => void) | null = null;
|
||||||
|
private _onItemCreated: ((item: SceneItem) => void) | null = null;
|
||||||
private _zCounter: number = 0;
|
private _zCounter: number = 0;
|
||||||
|
|
||||||
constructor(viewport: Viewport, textures: TextureManager, springs: SpringManager) {
|
constructor(viewport: Viewport, textures: TextureManager, springs: SpringManager) {
|
||||||
@@ -190,6 +191,10 @@ export class SceneManager {
|
|||||||
this._onItemDimensionsChanged = fn;
|
this._onItemDimensionsChanged = fn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set onItemCreated(fn: ((item: SceneItem) => void) | null) {
|
||||||
|
this._onItemCreated = fn;
|
||||||
|
}
|
||||||
|
|
||||||
get onChange(): (() => void) | null {
|
get onChange(): (() => void) | null {
|
||||||
return this._onChange;
|
return this._onChange;
|
||||||
}
|
}
|
||||||
@@ -387,6 +392,7 @@ export class SceneManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// No entrance animation — items must be visible and draggable immediately.
|
// No entrance animation — items must be visible and draggable immediately.
|
||||||
|
this._onItemCreated?.(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Item Update ---------------------------------------------------------
|
// -- Item Update ---------------------------------------------------------
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export class StickySprite extends Container {
|
|||||||
|
|
||||||
// Background redraw cache: skip Graphics.clear()+redraw when shape is unchanged
|
// Background redraw cache: skip Graphics.clear()+redraw when shape is unchanged
|
||||||
private _bgCacheKey = '';
|
private _bgCacheKey = '';
|
||||||
|
private _zoomBucket = 1;
|
||||||
|
|
||||||
/** After layout, the computed card height. Parent should sync to data.h. */
|
/** After layout, the computed card height. Parent should sync to data.h. */
|
||||||
get computedHeight(): number {
|
get computedHeight(): number {
|
||||||
@@ -60,6 +61,16 @@ export class StickySprite extends Container {
|
|||||||
this._text.visible = visible;
|
this._text.visible = visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update text rasterization resolution for zoom-bucket crisp rendering.
|
||||||
|
* Only re-rasterizes when the bucket actually changes.
|
||||||
|
*/
|
||||||
|
setZoomBucket(bucket: number): void {
|
||||||
|
if (bucket === this._zoomBucket) return;
|
||||||
|
this._zoomBucket = bucket;
|
||||||
|
this._text.resolution = bucket;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(data: StickyObject) {
|
constructor(data: StickyObject) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export class TextSprite extends Container {
|
|||||||
private _lastFontSize = 24;
|
private _lastFontSize = 24;
|
||||||
private _lastFontFamily = 'sans-serif';
|
private _lastFontFamily = 'sans-serif';
|
||||||
private _lastFill = '#ffffff';
|
private _lastFill = '#ffffff';
|
||||||
|
private _zoomBucket = 1;
|
||||||
|
|
||||||
/** Measured width after last text change. */
|
/** Measured width after last text change. */
|
||||||
get measuredWidth(): number {
|
get measuredWidth(): number {
|
||||||
@@ -39,6 +40,16 @@ export class TextSprite extends Container {
|
|||||||
this._text.visible = visible;
|
this._text.visible = visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update text rasterization resolution for zoom-bucket crisp rendering.
|
||||||
|
* Only re-rasterizes when the bucket actually changes.
|
||||||
|
*/
|
||||||
|
setZoomBucket(bucket: number): void {
|
||||||
|
if (bucket === this._zoomBucket) return;
|
||||||
|
this._zoomBucket = bucket;
|
||||||
|
this._text.resolution = bucket;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(data: TextObject) {
|
constructor(data: TextObject) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
/**
|
||||||
|
* textSharpness — zoom-bucket-aware text rendering.
|
||||||
|
*
|
||||||
|
* Tracks the viewport zoom level and groups it into discrete buckets.
|
||||||
|
* When the bucket changes, visible text/sticky sprites are re-rasterized
|
||||||
|
* at the new resolution for crisp rendering. Small zoom movements within
|
||||||
|
* the same bucket are ignored to avoid unnecessary texture churn.
|
||||||
|
*
|
||||||
|
* This does NOT mutate scene data (fontSize stays stable).
|
||||||
|
* It only changes the render resolution of text textures.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Viewport } from 'pixi-viewport';
|
||||||
|
import type { SceneManager, SceneItem } from './SceneManager';
|
||||||
|
import { getItemWorldBounds } from './SceneManager';
|
||||||
|
import { TextSprite } from './sprites/TextSprite';
|
||||||
|
import { StickySprite } from './sprites/StickySprite';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map continuous zoom to discrete bucket.
|
||||||
|
* Fewer buckets = fewer re-rasterizations = better perf.
|
||||||
|
*/
|
||||||
|
export function getTextZoomBucket(scale: number): number {
|
||||||
|
if (scale < 0.4) return 0.5;
|
||||||
|
if (scale < 0.75) return 0.75;
|
||||||
|
if (scale < 1.25) return 1;
|
||||||
|
if (scale < 1.75) return 1.5;
|
||||||
|
if (scale < 2.5) return 2;
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TextSharpnessManager {
|
||||||
|
private _viewport: Viewport;
|
||||||
|
private _scene: SceneManager;
|
||||||
|
private _currentBucket = 1;
|
||||||
|
|
||||||
|
constructor(viewport: Viewport, scene: SceneManager) {
|
||||||
|
this._viewport = viewport;
|
||||||
|
this._scene = scene;
|
||||||
|
this._currentBucket = getTextZoomBucket(viewport.scale.x);
|
||||||
|
// Apply initial bucket to all existing text/sticky items
|
||||||
|
this._refreshAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call on viewport zoom/move. Returns true if bucket changed.
|
||||||
|
*/
|
||||||
|
check(): boolean {
|
||||||
|
const bucket = getTextZoomBucket(this._viewport.scale.x);
|
||||||
|
if (bucket === this._currentBucket) return false;
|
||||||
|
this._currentBucket = bucket;
|
||||||
|
this._refreshVisible();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply current zoom bucket to a single item.
|
||||||
|
* Call this when a new text/sticky sprite is created.
|
||||||
|
*/
|
||||||
|
applyToItem(item: SceneItem): void {
|
||||||
|
if (item.displayObject instanceof TextSprite) {
|
||||||
|
item.displayObject.setZoomBucket(this._currentBucket);
|
||||||
|
} else if (item.displayObject instanceof StickySprite) {
|
||||||
|
item.displayObject.setZoomBucket(this._currentBucket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Refresh all text/sticky items (used on initial setup). */
|
||||||
|
private _refreshAll(): void {
|
||||||
|
const bucket = this._currentBucket;
|
||||||
|
for (const item of this._scene.getAllItems()) {
|
||||||
|
if (item.type !== 'text' && item.type !== 'sticky') continue;
|
||||||
|
if (item.displayObject instanceof TextSprite) {
|
||||||
|
item.displayObject.setZoomBucket(bucket);
|
||||||
|
} else if (item.displayObject instanceof StickySprite) {
|
||||||
|
item.displayObject.setZoomBucket(bucket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Refresh visible text/sticky items at current bucket (on zoom change). */
|
||||||
|
private _refreshVisible(): void {
|
||||||
|
const bucket = this._currentBucket;
|
||||||
|
const items = this._scene.getAllItems();
|
||||||
|
const vb = this._getViewportWorldBounds();
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.type !== 'text' && item.type !== 'sticky') continue;
|
||||||
|
// Skip off-screen items
|
||||||
|
if (vb && !this._intersects(item, vb)) continue;
|
||||||
|
|
||||||
|
if (item.displayObject instanceof TextSprite) {
|
||||||
|
item.displayObject.setZoomBucket(bucket);
|
||||||
|
} else if (item.displayObject instanceof StickySprite) {
|
||||||
|
item.displayObject.setZoomBucket(bucket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get viewport bounds in world coordinates. */
|
||||||
|
private _getViewportWorldBounds(): { x: number; y: number; w: number; h: number } | null {
|
||||||
|
const vp = this._viewport;
|
||||||
|
if (!vp.screenWidth || !vp.screenHeight) return null;
|
||||||
|
const tl = vp.toWorld(0, 0);
|
||||||
|
const br = vp.toWorld(vp.screenWidth, vp.screenHeight);
|
||||||
|
return { x: tl.x, y: tl.y, w: br.x - tl.x, h: br.y - tl.y };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check if item intersects viewport bounds (with margin). Uses world bounds for group children. */
|
||||||
|
private _intersects(item: SceneItem, vb: { x: number; y: number; w: number; h: number }): boolean {
|
||||||
|
const margin = 200; // world-space margin to include near-screen items
|
||||||
|
const { x: ix, y: iy, w: iw, h: ih } = getItemWorldBounds(item);
|
||||||
|
return (
|
||||||
|
ix + iw >= vb.x - margin &&
|
||||||
|
iy + ih >= vb.y - margin &&
|
||||||
|
ix <= vb.x + vb.w + margin &&
|
||||||
|
iy <= vb.y + vb.h + margin
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Current bucket value (for initial setup of new items). */
|
||||||
|
get currentBucket(): number {
|
||||||
|
return this._currentBucket;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
// nothing to clean up currently
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,14 +4,12 @@ interface TextFormatToolbarProps {
|
|||||||
kind: 'text' | 'sticky';
|
kind: 'text' | 'sticky';
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
fontSize: number;
|
|
||||||
fontFamily: string;
|
fontFamily: string;
|
||||||
/** Text color (both text and sticky). */
|
/** Text color (both text and sticky). */
|
||||||
fill: string;
|
fill: string;
|
||||||
/** Note background color (sticky only). */
|
/** Note background color (sticky only). */
|
||||||
noteFill?: string;
|
noteFill?: string;
|
||||||
position?: 'above' | 'below';
|
position?: 'above' | 'below';
|
||||||
onFontSizeChange: (size: number) => void;
|
|
||||||
onFontFamilyChange: (family: string) => void;
|
onFontFamilyChange: (family: string) => void;
|
||||||
onFillChange: (color: string) => void;
|
onFillChange: (color: string) => void;
|
||||||
/** Called when sticky note background color changes. */
|
/** Called when sticky note background color changes. */
|
||||||
@@ -35,7 +33,7 @@ const PRESET_COLORS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export default function TextFormatToolbar(props: TextFormatToolbarProps) {
|
export default function TextFormatToolbar(props: TextFormatToolbarProps) {
|
||||||
const { kind, x, y, fontSize, fontFamily, fill, noteFill, position = 'above', onFontSizeChange, onFontFamilyChange, onFillChange, onNoteFillChange } = props;
|
const { kind, x, y, fontFamily, fill, noteFill, position = 'above', onFontFamilyChange, onFillChange, onNoteFillChange } = props;
|
||||||
const [showFontMenu, setShowFontMenu] = useState(false);
|
const [showFontMenu, setShowFontMenu] = useState(false);
|
||||||
const [showColorPicker, setShowColorPicker] = useState(false);
|
const [showColorPicker, setShowColorPicker] = useState(false);
|
||||||
const [showNoteFillPicker, setShowNoteFillPicker] = useState(false);
|
const [showNoteFillPicker, setShowNoteFillPicker] = useState(false);
|
||||||
@@ -97,30 +95,6 @@ export default function TextFormatToolbar(props: TextFormatToolbarProps) {
|
|||||||
}}
|
}}
|
||||||
onPointerDown={(e) => e.stopPropagation()}
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{/* Font size: decrease / value / increase */}
|
|
||||||
<button
|
|
||||||
style={{ ...btnStyle, width: '26px', padding: 0 }}
|
|
||||||
onClick={() => onFontSizeChange(Math.max(8, fontSize - 2))}
|
|
||||||
title="Decrease font size"
|
|
||||||
{...hoverHandlers}
|
|
||||||
>
|
|
||||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.5"><line x1="2" y1="6" x2="10" y2="6" /></svg>
|
|
||||||
</button>
|
|
||||||
<span style={{ color: '#ccc', fontSize: '11px', minWidth: '24px', textAlign: 'center', userSelect: 'none' }}>
|
|
||||||
{fontSize}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
style={{ ...btnStyle, width: '26px', padding: 0 }}
|
|
||||||
onClick={() => onFontSizeChange(Math.min(200, fontSize + 2))}
|
|
||||||
title="Increase font size"
|
|
||||||
{...hoverHandlers}
|
|
||||||
>
|
|
||||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.5"><line x1="2" y1="6" x2="10" y2="6" /><line x1="6" y1="2" x2="6" y2="10" /></svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Divider */}
|
|
||||||
<div style={{ width: '1px', height: '18px', background: '#333', margin: '0 2px', flexShrink: 0 }} />
|
|
||||||
|
|
||||||
{/* Font family dropdown */}
|
{/* Font family dropdown */}
|
||||||
<div style={{ position: 'relative' }}>
|
<div style={{ position: 'relative' }}>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { AnnotationStore } from '../stores/annotationStore';
|
|||||||
import { PinOverlay } from '../canvas/PinOverlay';
|
import { PinOverlay } from '../canvas/PinOverlay';
|
||||||
import { CropOverlay } from '../canvas/CropOverlay';
|
import { CropOverlay } from '../canvas/CropOverlay';
|
||||||
import { TextSprite } from '../canvas/sprites/TextSprite';
|
import { TextSprite } from '../canvas/sprites/TextSprite';
|
||||||
|
import { TextSharpnessManager } from '../canvas/textSharpness';
|
||||||
// PresenceOverlay removed — remote selection highlighting was too heavy for minimal benefit
|
// PresenceOverlay removed — remote selection highlighting was too heavy for minimal benefit
|
||||||
import { connectSocket, disconnectSocket } from '../socket';
|
import { connectSocket, disconnectSocket } from '../socket';
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
@@ -70,6 +71,7 @@ export function useCanvasSetup(deps: CanvasSetupDeps) {
|
|||||||
const dropCleanupRef = useRef<(() => void) | null>(null);
|
const dropCleanupRef = useRef<(() => void) | null>(null);
|
||||||
const pasteCleanupRef = useRef<(() => void) | null>(null);
|
const pasteCleanupRef = useRef<(() => void) | null>(null);
|
||||||
const laserCleanupRef = useRef<(() => void) | null>(null);
|
const laserCleanupRef = useRef<(() => void) | null>(null);
|
||||||
|
const sharpnessCleanupRef = useRef<(() => void) | null>(null);
|
||||||
const annotationStoreRef = useRef<AnnotationStore | null>(null);
|
const annotationStoreRef = useRef<AnnotationStore | null>(null);
|
||||||
if (!annotationStoreRef.current) annotationStoreRef.current = new AnnotationStore();
|
if (!annotationStoreRef.current) annotationStoreRef.current = new AnnotationStore();
|
||||||
const pinOverlayRef = useRef<PinOverlay | null>(null);
|
const pinOverlayRef = useRef<PinOverlay | null>(null);
|
||||||
@@ -161,6 +163,17 @@ export function useCanvasSetup(deps: CanvasSetupDeps) {
|
|||||||
// Create UndoManager
|
// Create UndoManager
|
||||||
undoRef.current = new UndoManager(scene);
|
undoRef.current = new UndoManager(scene);
|
||||||
|
|
||||||
|
// Zoom-bucket text sharpness — re-rasterize visible text when zoom crosses bucket boundary
|
||||||
|
const sharpness = new TextSharpnessManager(viewport, scene);
|
||||||
|
const onZoomBucketCheck = () => { sharpness.check(); };
|
||||||
|
viewport.on('zoomed', onZoomBucketCheck);
|
||||||
|
scene.onItemCreated = (item) => { sharpness.applyToItem(item); };
|
||||||
|
sharpnessCleanupRef.current = () => {
|
||||||
|
viewport.off('zoomed', onZoomBucketCheck);
|
||||||
|
scene.onItemCreated = null;
|
||||||
|
sharpness.destroy();
|
||||||
|
};
|
||||||
|
|
||||||
// Create InboxZone and add to viewport
|
// Create InboxZone and add to viewport
|
||||||
const inboxZone = new InboxZone(scene.textures, scene.springs);
|
const inboxZone = new InboxZone(scene.textures, scene.springs);
|
||||||
viewport.addChild(inboxZone);
|
viewport.addChild(inboxZone);
|
||||||
@@ -475,6 +488,8 @@ export function useCanvasSetup(deps: CanvasSetupDeps) {
|
|||||||
pinOverlayRef.current = null;
|
pinOverlayRef.current = null;
|
||||||
}
|
}
|
||||||
annotationStoreRef.current?.clear();
|
annotationStoreRef.current?.clear();
|
||||||
|
sharpnessCleanupRef.current?.();
|
||||||
|
sharpnessCleanupRef.current = null;
|
||||||
disconnectSocket();
|
disconnectSocket();
|
||||||
};
|
};
|
||||||
}, [boardData, resolvedBoardId, user, isPublicView, onCanvasChange, showToast, canvasRef, selectionRef, undoRef, syncRef, inboxZoneRef, uploadManager, setOnlineUsers, setSelectedLayerIds]);
|
}, [boardData, resolvedBoardId, user, isPublicView, onCanvasChange, showToast, canvasRef, selectionRef, undoRef, syncRef, inboxZoneRef, uploadManager, setOnlineUsers, setSelectedLayerIds]);
|
||||||
|
|||||||
@@ -122,8 +122,8 @@ export default function Editor({ isPublicView }: EditorProps) {
|
|||||||
const [selectedLayerIds, setSelectedLayerIds] = useState<string[]>([]);
|
const [selectedLayerIds, setSelectedLayerIds] = useState<string[]>([]);
|
||||||
const [selToolbar, setSelToolbar] = useState<{ x: number; y: number; count: number } | null>(null);
|
const [selToolbar, setSelToolbar] = useState<{ x: number; y: number; count: number } | null>(null);
|
||||||
const [textToolbar, setTextToolbar] = useState<
|
const [textToolbar, setTextToolbar] = useState<
|
||||||
| { kind: 'text'; x: number; y: number; fontSize: number; fontFamily: string; fill: string; items: SceneItem[] }
|
| { kind: 'text'; x: number; y: number; fontFamily: string; fill: string; items: SceneItem[] }
|
||||||
| { kind: 'sticky'; x: number; y: number; fontSize: number; fontFamily: string; textColor: string; fill: string; items: SceneItem[] }
|
| { kind: 'sticky'; x: number; y: number; fontFamily: string; textColor: string; fill: string; items: SceneItem[] }
|
||||||
| null
|
| null
|
||||||
>(null);
|
>(null);
|
||||||
const [videoCtrl, setVideoCtrl] = useState<{ videoSprite: VideoSprite; screenRect: { x: number; y: number; w: number; h: number } } | null>(null);
|
const [videoCtrl, setVideoCtrl] = useState<{ videoSprite: VideoSprite; screenRect: { x: number; y: number; w: number; h: number } } | null>(null);
|
||||||
@@ -619,14 +619,14 @@ export default function Editor({ isPublicView }: EditorProps) {
|
|||||||
const td = textItems[0].data as TextObject;
|
const td = textItems[0].data as TextObject;
|
||||||
setTextToolbar({
|
setTextToolbar({
|
||||||
kind: 'text', x: posX, y: posY,
|
kind: 'text', x: posX, y: posY,
|
||||||
fontSize: td.fontSize, fontFamily: td.fontFamily, fill: td.fill,
|
fontFamily: td.fontFamily, fill: td.fill,
|
||||||
items: textItems,
|
items: textItems,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const sd = stickyItems[0].data as StickyObject;
|
const sd = stickyItems[0].data as StickyObject;
|
||||||
setTextToolbar({
|
setTextToolbar({
|
||||||
kind: 'sticky', x: posX, y: posY,
|
kind: 'sticky', x: posX, y: posY,
|
||||||
fontSize: sd.fontSize || 14, fontFamily: sd.fontFamily || 'Inter, system-ui, sans-serif',
|
fontFamily: sd.fontFamily || 'Inter, system-ui, sans-serif',
|
||||||
textColor: sd.textColor || '#1a1a1a', fill: sd.fill || '#ffd43b',
|
textColor: sd.textColor || '#1a1a1a', fill: sd.fill || '#ffd43b',
|
||||||
items: stickyItems,
|
items: stickyItems,
|
||||||
});
|
});
|
||||||
@@ -912,36 +912,10 @@ export default function Editor({ isPublicView }: EditorProps) {
|
|||||||
kind={textToolbar.kind}
|
kind={textToolbar.kind}
|
||||||
x={textToolbar.x}
|
x={textToolbar.x}
|
||||||
y={textToolbar.y}
|
y={textToolbar.y}
|
||||||
fontSize={textToolbar.fontSize}
|
|
||||||
fontFamily={textToolbar.fontFamily}
|
fontFamily={textToolbar.fontFamily}
|
||||||
fill={textToolbar.kind === 'sticky' ? textToolbar.textColor : textToolbar.fill}
|
fill={textToolbar.kind === 'sticky' ? textToolbar.textColor : textToolbar.fill}
|
||||||
noteFill={textToolbar.kind === 'sticky' ? textToolbar.fill : undefined}
|
noteFill={textToolbar.kind === 'sticky' ? textToolbar.fill : undefined}
|
||||||
position={textToolbar.items.length >= 2 ? 'below' : 'above'}
|
position={textToolbar.items.length >= 2 ? 'below' : 'above'}
|
||||||
onFontSizeChange={(size) => {
|
|
||||||
const scene = canvasRef.current?.getScene();
|
|
||||||
for (const item of textToolbar.items) {
|
|
||||||
if (textToolbar.kind === 'text') {
|
|
||||||
const d = item.data as TextObject;
|
|
||||||
d.fontSize = size;
|
|
||||||
if (item.displayObject instanceof TextSprite) {
|
|
||||||
item.displayObject.updateFromData(d);
|
|
||||||
d.w = item.displayObject.measuredWidth;
|
|
||||||
d.h = item.displayObject.measuredHeight;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const d = item.data as StickyObject;
|
|
||||||
d.fontSize = size;
|
|
||||||
if (item.displayObject instanceof StickySprite) {
|
|
||||||
item.displayObject.updateFromData(d);
|
|
||||||
d.h = item.displayObject.computedHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (scene) scene.updateSpatialEntry(item);
|
|
||||||
}
|
|
||||||
selectionRef.current?.transformBox.update(textToolbar.items);
|
|
||||||
onCanvasChange(textToolbar.items.map(i => i.id));
|
|
||||||
updateOverlays();
|
|
||||||
}}
|
|
||||||
onFontFamilyChange={(family) => {
|
onFontFamilyChange={(family) => {
|
||||||
const scene = canvasRef.current?.getScene();
|
const scene = canvasRef.current?.getScene();
|
||||||
for (const item of textToolbar.items) {
|
for (const item of textToolbar.items) {
|
||||||
|
|||||||
Reference in New Issue
Block a user