Port operations.ts from Fabric.js to PixiJS SceneItem API
Replace all FabricObject usage with SceneItem, accessing item.data.x/y/w/h/sx/sy instead of obj.left/top/width*scaleX. Sync displayObject position/scale after mutations. Remove breakSelection (no ActiveSelection needed), setCoords calls, and all fabric imports. Toggle grayscale uses PixiJS ColorMatrixFilter.desaturate(). arrangeByZOrder now uses item.data.z directly instead of canvas object index.
This commit is contained in:
+208
-191
@@ -1,358 +1,375 @@
|
||||
/**
|
||||
* Canvas operations — pure functions for alignment, arrangement, normalize, flip, etc.
|
||||
* Each mutates objects in place and calls setCoords(). Caller must requestRenderAll().
|
||||
*
|
||||
* IMPORTANT: When multiple objects are selected in Fabric.js, they live inside an
|
||||
* ActiveSelection group. Their left/top are RELATIVE to the group center, not the canvas.
|
||||
* We must use getBoundingRect() or the canvas-level coordinates for position calculations,
|
||||
* then convert back. The helper getAbsPos/setAbsPos handles this.
|
||||
* Each mutates item.data in place and syncs the displayObject. Caller should trigger
|
||||
* any needed re-render or persistence.
|
||||
*/
|
||||
|
||||
import { Canvas, FabricObject } from 'fabric';
|
||||
import type { SceneItem } from './SceneManager';
|
||||
import type { ImageObject } from './scene-format';
|
||||
import { ColorMatrixFilter } from 'pixi.js';
|
||||
|
||||
// ─── Helpers ───
|
||||
|
||||
function scaledW(o: FabricObject): number {
|
||||
return (o.width || 0) * (o.scaleX || 1);
|
||||
function scaledW(item: SceneItem): number {
|
||||
return item.data.w * item.data.sx;
|
||||
}
|
||||
|
||||
function scaledH(o: FabricObject): number {
|
||||
return (o.height || 0) * (o.scaleY || 1);
|
||||
function scaledH(item: SceneItem): number {
|
||||
return item.data.h * item.data.sy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Break ActiveSelection so objects have canvas-level coordinates.
|
||||
* Returns a cleanup function to restore selection afterward.
|
||||
*/
|
||||
export function breakSelection(canvas: Canvas): { objects: FabricObject[]; restore: () => void } {
|
||||
const activeObj = canvas.getActiveObject();
|
||||
const objects = canvas.getActiveObjects();
|
||||
if (!activeObj || objects.length <= 1) {
|
||||
return { objects, restore: () => {} };
|
||||
}
|
||||
// Discard the ActiveSelection — this updates each object's left/top to canvas coordinates
|
||||
canvas.discardActiveObject();
|
||||
return {
|
||||
objects,
|
||||
restore: () => {
|
||||
// Re-select after operation
|
||||
const fabricNs = (window as any).fabric;
|
||||
if (fabricNs?.ActiveSelection && objects.length > 1) {
|
||||
const sel = new fabricNs.ActiveSelection(objects, { canvas });
|
||||
canvas.setActiveObject(sel);
|
||||
} else if (objects.length === 1) {
|
||||
canvas.setActiveObject(objects[0]);
|
||||
}
|
||||
},
|
||||
};
|
||||
/** Sync displayObject position from item.data. */
|
||||
function syncPosition(item: SceneItem): void {
|
||||
item.displayObject.position.set(item.data.x, item.data.y);
|
||||
}
|
||||
|
||||
/** Sync displayObject scale from item.data. */
|
||||
function syncScale(item: SceneItem): void {
|
||||
item.displayObject.scale.set(item.data.sx, item.data.sy);
|
||||
}
|
||||
|
||||
/** Sync both position and scale. */
|
||||
function syncTransform(item: SceneItem): void {
|
||||
syncPosition(item);
|
||||
syncScale(item);
|
||||
item.displayObject.angle = item.data.angle;
|
||||
}
|
||||
|
||||
// ─── Alignment ───
|
||||
|
||||
export function alignLeft(objects: FabricObject[]) {
|
||||
export function alignLeft(objects: SceneItem[]) {
|
||||
if (objects.length < 2) return;
|
||||
const minLeft = Math.min(...objects.map((o) => o.left ?? 0));
|
||||
objects.forEach((o) => { o.set({ left: minLeft } as any); o.setCoords(); });
|
||||
const minLeft = Math.min(...objects.map((item) => item.data.x));
|
||||
objects.forEach((item) => {
|
||||
item.data.x = minLeft;
|
||||
syncPosition(item);
|
||||
});
|
||||
}
|
||||
|
||||
export function alignRight(objects: FabricObject[]) {
|
||||
export function alignRight(objects: SceneItem[]) {
|
||||
if (objects.length < 2) return;
|
||||
const maxRight = Math.max(...objects.map((o) => (o.left ?? 0) + scaledW(o)));
|
||||
objects.forEach((o) => { o.set({ left: maxRight - scaledW(o) } as any); o.setCoords(); });
|
||||
const maxRight = Math.max(...objects.map((item) => item.data.x + scaledW(item)));
|
||||
objects.forEach((item) => {
|
||||
item.data.x = maxRight - scaledW(item);
|
||||
syncPosition(item);
|
||||
});
|
||||
}
|
||||
|
||||
export function alignTop(objects: FabricObject[]) {
|
||||
export function alignTop(objects: SceneItem[]) {
|
||||
if (objects.length < 2) return;
|
||||
const minTop = Math.min(...objects.map((o) => o.top ?? 0));
|
||||
objects.forEach((o) => { o.set({ top: minTop } as any); o.setCoords(); });
|
||||
const minTop = Math.min(...objects.map((item) => item.data.y));
|
||||
objects.forEach((item) => {
|
||||
item.data.y = minTop;
|
||||
syncPosition(item);
|
||||
});
|
||||
}
|
||||
|
||||
export function alignBottom(objects: FabricObject[]) {
|
||||
export function alignBottom(objects: SceneItem[]) {
|
||||
if (objects.length < 2) return;
|
||||
const maxBottom = Math.max(...objects.map((o) => (o.top ?? 0) + scaledH(o)));
|
||||
objects.forEach((o) => { o.set({ top: maxBottom - scaledH(o) } as any); o.setCoords(); });
|
||||
const maxBottom = Math.max(...objects.map((item) => item.data.y + scaledH(item)));
|
||||
objects.forEach((item) => {
|
||||
item.data.y = maxBottom - scaledH(item);
|
||||
syncPosition(item);
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Distribution ───
|
||||
|
||||
export function distributeHorizontal(objects: FabricObject[]) {
|
||||
export function distributeHorizontal(objects: SceneItem[]) {
|
||||
if (objects.length < 3) return;
|
||||
const sorted = [...objects].sort((a, b) => (a.left ?? 0) - (b.left ?? 0));
|
||||
const sorted = [...objects].sort((a, b) => a.data.x - b.data.x);
|
||||
const first = sorted[0];
|
||||
const last = sorted[sorted.length - 1];
|
||||
const totalSpan = (last.left ?? 0) + scaledW(last) - (first.left ?? 0);
|
||||
const totalWidth = sorted.reduce((s, o) => s + scaledW(o), 0);
|
||||
const totalSpan = last.data.x + scaledW(last) - first.data.x;
|
||||
const totalWidth = sorted.reduce((s, item) => s + scaledW(item), 0);
|
||||
const gap = (totalSpan - totalWidth) / (sorted.length - 1);
|
||||
let x = (first.left ?? 0) + scaledW(first) + gap;
|
||||
let x = first.data.x + scaledW(first) + gap;
|
||||
for (let i = 1; i < sorted.length - 1; i++) {
|
||||
sorted[i].set({ left: x } as any);
|
||||
sorted[i].setCoords();
|
||||
sorted[i].data.x = x;
|
||||
syncPosition(sorted[i]);
|
||||
x += scaledW(sorted[i]) + gap;
|
||||
}
|
||||
}
|
||||
|
||||
export function distributeVertical(objects: FabricObject[]) {
|
||||
export function distributeVertical(objects: SceneItem[]) {
|
||||
if (objects.length < 3) return;
|
||||
const sorted = [...objects].sort((a, b) => (a.top ?? 0) - (b.top ?? 0));
|
||||
const sorted = [...objects].sort((a, b) => a.data.y - b.data.y);
|
||||
const first = sorted[0];
|
||||
const last = sorted[sorted.length - 1];
|
||||
const totalSpan = (last.top ?? 0) + scaledH(last) - (first.top ?? 0);
|
||||
const totalHeight = sorted.reduce((s, o) => s + scaledH(o), 0);
|
||||
const totalSpan = last.data.y + scaledH(last) - first.data.y;
|
||||
const totalHeight = sorted.reduce((s, item) => s + scaledH(item), 0);
|
||||
const gap = (totalSpan - totalHeight) / (sorted.length - 1);
|
||||
let y = (first.top ?? 0) + scaledH(first) + gap;
|
||||
let y = first.data.y + scaledH(first) + gap;
|
||||
for (let i = 1; i < sorted.length - 1; i++) {
|
||||
sorted[i].set({ top: y } as any);
|
||||
sorted[i].setCoords();
|
||||
sorted[i].data.y = y;
|
||||
syncPosition(sorted[i]);
|
||||
y += scaledH(sorted[i]) + gap;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Normalize ───
|
||||
|
||||
export function normalizeSize(objects: FabricObject[]) {
|
||||
export function normalizeSize(objects: SceneItem[]) {
|
||||
if (objects.length < 2) return;
|
||||
const areas = objects.map((o) => scaledW(o) * scaledH(o));
|
||||
const areas = objects.map((item) => scaledW(item) * scaledH(item));
|
||||
const avgArea = areas.reduce((a, b) => a + b, 0) / areas.length;
|
||||
objects.forEach((o) => {
|
||||
const currentArea = scaledW(o) * scaledH(o);
|
||||
objects.forEach((item) => {
|
||||
const currentArea = scaledW(item) * scaledH(item);
|
||||
if (currentArea <= 0) return;
|
||||
const ratio = Math.sqrt(avgArea / currentArea);
|
||||
o.set({ scaleX: (o.scaleX || 1) * ratio, scaleY: (o.scaleY || 1) * ratio } as any);
|
||||
o.setCoords();
|
||||
item.data.sx *= ratio;
|
||||
item.data.sy *= ratio;
|
||||
syncScale(item);
|
||||
});
|
||||
}
|
||||
|
||||
export function normalizeScale(objects: FabricObject[]) {
|
||||
export function normalizeScale(objects: SceneItem[]) {
|
||||
if (objects.length < 2) return;
|
||||
const avgSX = objects.reduce((s, o) => s + (o.scaleX || 1), 0) / objects.length;
|
||||
const avgSY = objects.reduce((s, o) => s + (o.scaleY || 1), 0) / objects.length;
|
||||
objects.forEach((o) => { o.set({ scaleX: avgSX, scaleY: avgSY } as any); o.setCoords(); });
|
||||
const avgSX = objects.reduce((s, item) => s + item.data.sx, 0) / objects.length;
|
||||
const avgSY = objects.reduce((s, item) => s + item.data.sy, 0) / objects.length;
|
||||
objects.forEach((item) => {
|
||||
item.data.sx = avgSX;
|
||||
item.data.sy = avgSY;
|
||||
syncScale(item);
|
||||
});
|
||||
}
|
||||
|
||||
export function normalizeHeight(objects: FabricObject[]) {
|
||||
export function normalizeHeight(objects: SceneItem[]) {
|
||||
if (objects.length < 2) return;
|
||||
const avgH = objects.reduce((s, o) => s + scaledH(o), 0) / objects.length;
|
||||
objects.forEach((o) => {
|
||||
const h = scaledH(o);
|
||||
const avgH = objects.reduce((s, item) => s + scaledH(item), 0) / objects.length;
|
||||
objects.forEach((item) => {
|
||||
const h = scaledH(item);
|
||||
if (h <= 0) return;
|
||||
const ratio = avgH / h;
|
||||
o.set({ scaleX: (o.scaleX || 1) * ratio, scaleY: (o.scaleY || 1) * ratio } as any);
|
||||
o.setCoords();
|
||||
item.data.sx *= ratio;
|
||||
item.data.sy *= ratio;
|
||||
syncScale(item);
|
||||
});
|
||||
}
|
||||
|
||||
export function normalizeWidth(objects: FabricObject[]) {
|
||||
export function normalizeWidth(objects: SceneItem[]) {
|
||||
if (objects.length < 2) return;
|
||||
const avgW = objects.reduce((s, o) => s + scaledW(o), 0) / objects.length;
|
||||
objects.forEach((o) => {
|
||||
const w = scaledW(o);
|
||||
const avgW = objects.reduce((s, item) => s + scaledW(item), 0) / objects.length;
|
||||
objects.forEach((item) => {
|
||||
const w = scaledW(item);
|
||||
if (w <= 0) return;
|
||||
const ratio = avgW / w;
|
||||
o.set({ scaleX: (o.scaleX || 1) * ratio, scaleY: (o.scaleY || 1) * ratio } as any);
|
||||
o.setCoords();
|
||||
item.data.sx *= ratio;
|
||||
item.data.sy *= ratio;
|
||||
syncScale(item);
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Arrangement ───
|
||||
|
||||
export function arrangeOptimal(objects: FabricObject[]) {
|
||||
export function arrangeOptimal(objects: SceneItem[]) {
|
||||
if (objects.length < 2) return;
|
||||
// Shelf-based bin packing, sorted by height descending
|
||||
const sorted = [...objects].sort((a, b) => scaledH(b) - scaledH(a));
|
||||
const gap = 10;
|
||||
const totalArea = sorted.reduce((s, o) => s + scaledW(o) * scaledH(o), 0);
|
||||
const totalArea = sorted.reduce((s, item) => s + scaledW(item) * scaledH(item), 0);
|
||||
const shelfWidth = Math.sqrt(totalArea) * 1.3;
|
||||
const startX = sorted[0]?.left ?? 0;
|
||||
const startY = sorted[0]?.top ?? 0;
|
||||
const startX = sorted[0].data.x;
|
||||
const startY = sorted[0].data.y;
|
||||
let x = 0, y = 0, shelfHeight = 0;
|
||||
sorted.forEach((obj) => {
|
||||
const w = scaledW(obj);
|
||||
const h = scaledH(obj);
|
||||
sorted.forEach((item) => {
|
||||
const w = scaledW(item);
|
||||
const h = scaledH(item);
|
||||
if (x + w > shelfWidth && x > 0) {
|
||||
x = 0;
|
||||
y += shelfHeight + gap;
|
||||
shelfHeight = 0;
|
||||
}
|
||||
obj.set({ left: startX + x, top: startY + y } as any);
|
||||
obj.setCoords();
|
||||
item.data.x = startX + x;
|
||||
item.data.y = startY + y;
|
||||
syncPosition(item);
|
||||
shelfHeight = Math.max(shelfHeight, h);
|
||||
x += w + gap;
|
||||
});
|
||||
}
|
||||
|
||||
export function arrangeGrid(objects: FabricObject[]) {
|
||||
export function arrangeGrid(objects: SceneItem[]) {
|
||||
if (objects.length < 2) return;
|
||||
const gap = 20;
|
||||
const cols = Math.ceil(Math.sqrt(objects.length));
|
||||
const startX = objects[0]?.left ?? 0;
|
||||
const startY = objects[0]?.top ?? 0;
|
||||
// Find max cell size
|
||||
const startX = objects[0].data.x;
|
||||
const startY = objects[0].data.y;
|
||||
const maxW = Math.max(...objects.map(scaledW));
|
||||
const maxH = Math.max(...objects.map(scaledH));
|
||||
objects.forEach((obj, i) => {
|
||||
objects.forEach((item, i) => {
|
||||
const col = i % cols;
|
||||
const row = Math.floor(i / cols);
|
||||
obj.set({ left: startX + col * (maxW + gap), top: startY + row * (maxH + gap) } as any);
|
||||
obj.setCoords();
|
||||
item.data.x = startX + col * (maxW + gap);
|
||||
item.data.y = startY + row * (maxH + gap);
|
||||
syncPosition(item);
|
||||
});
|
||||
}
|
||||
|
||||
export function arrangeRow(objects: FabricObject[]) {
|
||||
export function arrangeRow(objects: SceneItem[]) {
|
||||
if (objects.length < 2) return;
|
||||
const gap = 20;
|
||||
const sorted = [...objects].sort((a, b) => (a.left ?? 0) - (b.left ?? 0));
|
||||
const startY = sorted[0]?.top ?? 0;
|
||||
let x = sorted[0]?.left ?? 0;
|
||||
sorted.forEach((obj) => {
|
||||
obj.set({ left: x, top: startY } as any);
|
||||
obj.setCoords();
|
||||
x += scaledW(obj) + gap;
|
||||
const sorted = [...objects].sort((a, b) => a.data.x - b.data.x);
|
||||
const startY = sorted[0].data.y;
|
||||
let x = sorted[0].data.x;
|
||||
sorted.forEach((item) => {
|
||||
item.data.x = x;
|
||||
item.data.y = startY;
|
||||
syncPosition(item);
|
||||
x += scaledW(item) + gap;
|
||||
});
|
||||
}
|
||||
|
||||
export function arrangeColumn(objects: FabricObject[]) {
|
||||
export function arrangeColumn(objects: SceneItem[]) {
|
||||
if (objects.length < 2) return;
|
||||
const gap = 20;
|
||||
const sorted = [...objects].sort((a, b) => (a.top ?? 0) - (b.top ?? 0));
|
||||
const startX = sorted[0]?.left ?? 0;
|
||||
let y = sorted[0]?.top ?? 0;
|
||||
sorted.forEach((obj) => {
|
||||
obj.set({ left: startX, top: y } as any);
|
||||
obj.setCoords();
|
||||
y += scaledH(obj) + gap;
|
||||
const sorted = [...objects].sort((a, b) => a.data.y - b.data.y);
|
||||
const startX = sorted[0].data.x;
|
||||
let y = sorted[0].data.y;
|
||||
sorted.forEach((item) => {
|
||||
item.data.x = startX;
|
||||
item.data.y = y;
|
||||
syncPosition(item);
|
||||
y += scaledH(item) + gap;
|
||||
});
|
||||
}
|
||||
|
||||
export function stackObjects(objects: FabricObject[]) {
|
||||
export function stackObjects(objects: SceneItem[]) {
|
||||
if (objects.length < 2) return;
|
||||
const cx = objects.reduce((s, o) => s + (o.left ?? 0) + scaledW(o) / 2, 0) / objects.length;
|
||||
const cy = objects.reduce((s, o) => s + (o.top ?? 0) + scaledH(o) / 2, 0) / objects.length;
|
||||
objects.forEach((o) => {
|
||||
o.set({ left: cx - scaledW(o) / 2, top: cy - scaledH(o) / 2 } as any);
|
||||
o.setCoords();
|
||||
const cx = objects.reduce((s, item) => s + item.data.x + scaledW(item) / 2, 0) / objects.length;
|
||||
const cy = objects.reduce((s, item) => s + item.data.y + scaledH(item) / 2, 0) / objects.length;
|
||||
objects.forEach((item) => {
|
||||
item.data.x = cx - scaledW(item) / 2;
|
||||
item.data.y = cy - scaledH(item) / 2;
|
||||
syncPosition(item);
|
||||
});
|
||||
}
|
||||
|
||||
export function arrangeByName(objects: FabricObject[]) {
|
||||
export function arrangeByName(objects: SceneItem[]) {
|
||||
const sorted = [...objects].sort((a, b) =>
|
||||
((a as any).name || '').localeCompare((b as any).name || '')
|
||||
(a.data.name || '').localeCompare(b.data.name || '')
|
||||
);
|
||||
layoutAsGrid(sorted);
|
||||
}
|
||||
|
||||
export function arrangeByZOrder(objects: FabricObject[], canvasObjects: FabricObject[]) {
|
||||
// Sort by z-order (position in canvas.getObjects())
|
||||
const indexMap = new Map(canvasObjects.map((o, i) => [o, i]));
|
||||
const sorted = [...objects].sort((a, b) => (indexMap.get(a) ?? 0) - (indexMap.get(b) ?? 0));
|
||||
export function arrangeByZOrder(objects: SceneItem[]) {
|
||||
// Sort by z-order stored in item.data.z
|
||||
const sorted = [...objects].sort((a, b) => a.data.z - b.data.z);
|
||||
layoutAsGrid(sorted);
|
||||
}
|
||||
|
||||
export function arrangeRandomly(objects: FabricObject[]) {
|
||||
export function arrangeRandomly(objects: SceneItem[]) {
|
||||
if (objects.length < 2) return;
|
||||
// Get bounding box of current positions
|
||||
const minX = Math.min(...objects.map((o) => o.left ?? 0));
|
||||
const minY = Math.min(...objects.map((o) => o.top ?? 0));
|
||||
const maxX = Math.max(...objects.map((o) => (o.left ?? 0) + scaledW(o)));
|
||||
const maxY = Math.max(...objects.map((o) => (o.top ?? 0) + scaledH(o)));
|
||||
objects.forEach((o) => {
|
||||
o.set({
|
||||
left: minX + Math.random() * (maxX - minX - scaledW(o)),
|
||||
top: minY + Math.random() * (maxY - minY - scaledH(o)),
|
||||
} as any);
|
||||
o.setCoords();
|
||||
const minX = Math.min(...objects.map((item) => item.data.x));
|
||||
const minY = Math.min(...objects.map((item) => item.data.y));
|
||||
const maxX = Math.max(...objects.map((item) => item.data.x + scaledW(item)));
|
||||
const maxY = Math.max(...objects.map((item) => item.data.y + scaledH(item)));
|
||||
objects.forEach((item) => {
|
||||
item.data.x = minX + Math.random() * (maxX - minX - scaledW(item));
|
||||
item.data.y = minY + Math.random() * (maxY - minY - scaledH(item));
|
||||
syncPosition(item);
|
||||
});
|
||||
}
|
||||
|
||||
function layoutAsGrid(sorted: FabricObject[]) {
|
||||
function layoutAsGrid(sorted: SceneItem[]) {
|
||||
if (sorted.length < 2) return;
|
||||
const gap = 20;
|
||||
const cols = Math.ceil(Math.sqrt(sorted.length));
|
||||
const startX = sorted[0]?.left ?? 0;
|
||||
const startY = sorted[0]?.top ?? 0;
|
||||
const startX = sorted[0].data.x;
|
||||
const startY = sorted[0].data.y;
|
||||
const maxW = Math.max(...sorted.map(scaledW));
|
||||
const maxH = Math.max(...sorted.map(scaledH));
|
||||
sorted.forEach((obj, i) => {
|
||||
sorted.forEach((item, i) => {
|
||||
const col = i % cols;
|
||||
const row = Math.floor(i / cols);
|
||||
obj.set({ left: startX + col * (maxW + gap), top: startY + row * (maxH + gap) } as any);
|
||||
obj.setCoords();
|
||||
item.data.x = startX + col * (maxW + gap);
|
||||
item.data.y = startY + row * (maxH + gap);
|
||||
syncPosition(item);
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Flip ───
|
||||
|
||||
export function flipHorizontal(objects: FabricObject[]) {
|
||||
objects.forEach((o) => { o.set({ flipX: !o.flipX } as any); o.setCoords(); });
|
||||
export function flipHorizontal(objects: SceneItem[]) {
|
||||
objects.forEach((item) => {
|
||||
item.data.flipX = !item.data.flipX;
|
||||
item.displayObject.scale.x = item.data.sx * (item.data.flipX ? -1 : 1);
|
||||
});
|
||||
}
|
||||
|
||||
export function flipVertical(objects: FabricObject[]) {
|
||||
objects.forEach((o) => { o.set({ flipY: !o.flipY } as any); o.setCoords(); });
|
||||
export function flipVertical(objects: SceneItem[]) {
|
||||
objects.forEach((item) => {
|
||||
item.data.flipY = !item.data.flipY;
|
||||
item.displayObject.scale.y = item.data.sy * (item.data.flipY ? -1 : 1);
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Transform ───
|
||||
|
||||
export function resetTransform(objects: FabricObject[]) {
|
||||
objects.forEach((o) => {
|
||||
o.set({
|
||||
scaleX: 1, scaleY: 1, angle: 0,
|
||||
skewX: 0, skewY: 0, flipX: false, flipY: false,
|
||||
} as any);
|
||||
o.setCoords();
|
||||
export function resetTransform(objects: SceneItem[]) {
|
||||
objects.forEach((item) => {
|
||||
item.data.sx = 1;
|
||||
item.data.sy = 1;
|
||||
item.data.angle = 0;
|
||||
item.data.flipX = false;
|
||||
item.data.flipY = false;
|
||||
syncTransform(item);
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Grayscale ───
|
||||
|
||||
export function toggleGrayscale(objects: FabricObject[]) {
|
||||
objects.forEach((o) => {
|
||||
if (o.type !== 'image') return;
|
||||
const img = o as any;
|
||||
if (!img.filters) img.filters = [];
|
||||
// Check if grayscale filter already applied
|
||||
const idx = img.filters.findIndex((f: any) => f?.type === 'Grayscale');
|
||||
if (idx >= 0) {
|
||||
img.filters.splice(idx, 1);
|
||||
export function toggleGrayscale(objects: SceneItem[]) {
|
||||
objects.forEach((item) => {
|
||||
if (item.type !== 'image') return;
|
||||
const imgData = item.data as ImageObject;
|
||||
const obj = item.displayObject;
|
||||
|
||||
// Check if a desaturate filter is already applied
|
||||
const hasGrayscale = imgData.filters.includes('Grayscale');
|
||||
|
||||
if (hasGrayscale) {
|
||||
// Remove grayscale: clear the ColorMatrixFilter and remove from data
|
||||
imgData.filters = imgData.filters.filter((f) => f !== 'Grayscale');
|
||||
obj.filters = (obj.filters || []).filter((f) => !(f instanceof ColorMatrixFilter));
|
||||
} else {
|
||||
// Dynamically access Grayscale filter from Fabric
|
||||
const fabric = (window as any).fabric;
|
||||
if (fabric?.filters?.Grayscale) {
|
||||
img.filters.push(new fabric.filters.Grayscale());
|
||||
}
|
||||
// Add grayscale
|
||||
imgData.filters.push('Grayscale');
|
||||
const filter = new ColorMatrixFilter();
|
||||
filter.desaturate();
|
||||
obj.filters = [...(obj.filters || []), filter];
|
||||
}
|
||||
img.applyFilters?.();
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Lock ───
|
||||
|
||||
export function toggleLocked(objects: FabricObject[]) {
|
||||
objects.forEach((o) => {
|
||||
const isLocked = !o.selectable;
|
||||
o.set({
|
||||
selectable: isLocked,
|
||||
evented: isLocked,
|
||||
} as any);
|
||||
export function toggleLocked(objects: SceneItem[]) {
|
||||
objects.forEach((item) => {
|
||||
item.data.locked = !item.data.locked;
|
||||
item.displayObject.eventMode = item.data.locked ? 'none' : 'static';
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Overlay / Compare ───
|
||||
|
||||
export function overlayCompare(objects: FabricObject[]) {
|
||||
export function overlayCompare(objects: SceneItem[]) {
|
||||
if (objects.length < 2) return;
|
||||
// If all are at 0.5 opacity, restore to 1; otherwise set to 0.5 and center-stack
|
||||
const allHalf = objects.every((o) => Math.abs((o.opacity ?? 1) - 0.5) < 0.05);
|
||||
const allHalf = objects.every((item) => Math.abs(item.data.opacity - 0.5) < 0.05);
|
||||
if (allHalf) {
|
||||
objects.forEach((o) => { o.set({ opacity: 1 } as any); });
|
||||
objects.forEach((item) => {
|
||||
item.data.opacity = 1;
|
||||
item.displayObject.alpha = 1;
|
||||
});
|
||||
} else {
|
||||
const cx = objects.reduce((s, o) => s + (o.left ?? 0) + scaledW(o) / 2, 0) / objects.length;
|
||||
const cy = objects.reduce((s, o) => s + (o.top ?? 0) + scaledH(o) / 2, 0) / objects.length;
|
||||
objects.forEach((o) => {
|
||||
o.set({
|
||||
opacity: 0.5,
|
||||
left: cx - scaledW(o) / 2,
|
||||
top: cy - scaledH(o) / 2,
|
||||
} as any);
|
||||
o.setCoords();
|
||||
const cx = objects.reduce((s, item) => s + item.data.x + scaledW(item) / 2, 0) / objects.length;
|
||||
const cy = objects.reduce((s, item) => s + item.data.y + scaledH(item) / 2, 0) / objects.length;
|
||||
objects.forEach((item) => {
|
||||
item.data.opacity = 0.5;
|
||||
item.data.x = cx - scaledW(item) / 2;
|
||||
item.data.y = cy - scaledH(item) / 2;
|
||||
item.displayObject.alpha = 0.5;
|
||||
syncPosition(item);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user