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