Test / lib /fabric-elements.ts
TrikozikGames's picture
Add source files
eddc354
Raw
History Blame Contribute Delete
5.53 kB
import * as fabric from "fabric";
import { pathLengthPx, type ElDef, type ThreadColor } from "@/lib/tatting-ru";
export type TattingObject = fabric.Group & {
elId?: string;
elName?: string;
knots?: number;
pathPx?: number;
threadName?: string;
isGrid?: boolean;
isRuler?: boolean;
};
export const CUSTOM_PROPS = ["elId", "elName", "knots", "pathPx", "threadName", "isGrid", "isRuler"];
const strokeOf = (t: ThreadColor, size: number): string | fabric.Gradient<"linear"> =>
t.to
? new fabric.Gradient({
type: "linear",
gradientUnits: "pixels",
coords: { x1: -size, y1: -size, x2: size, y2: size },
colorStops: [
{ offset: 0, color: t.value },
{ offset: 1, color: t.to },
],
})
: t.value;
export function buildElement(def: ElDef, thread: ThreadColor, width: number): TattingObject {
const r = def.radius;
if (def.kind === "text") {
const text = new fabric.IText(def.id === "rownum" ? "Ряд 1" : "Подпись", {
fontSize: def.radius * 1.6,
fontFamily: "DM Sans, system-ui, sans-serif",
fill: thread.value,
originX: "center",
originY: "center",
}) as unknown as TattingObject;
text.elId = def.id;
text.elName = def.name;
text.knots = 0;
text.pathPx = 0;
text.threadName = thread.name;
return text;
}
const stroke = strokeOf(thread, r * 1.4);
const picotR = Math.max(3, r * 0.13);
const base = { fill: "", stroke, strokeWidth: width, originX: "center", originY: "center" } as const;
const parts: fabric.FabricObject[] = [];
const picotRing = (count: number, rad: number) => {
for (let i = 0; i < count; i++) {
const a = (i / count) * Math.PI * 2 - Math.PI / 2;
parts.push(
new fabric.Circle({
...base,
radius: picotR,
left: Math.cos(a) * (rad + picotR * 0.8),
top: Math.sin(a) * (rad + picotR * 0.8),
}),
);
}
};
if (def.kind === "ring") {
parts.push(new fabric.Circle({ ...base, radius: r, left: 0, top: 0 }));
parts.push(
new fabric.Circle({ ...base, radius: r - 5, left: 0, top: 0, strokeWidth: width * 0.4, opacity: 0.5 }),
);
picotRing(def.picots, r);
} else if (def.kind === "arc" || def.kind === "arc-curved") {
const sweep = def.kind === "arc-curved" ? 1.35 : 1;
const half = (Math.PI * sweep) / 2;
const x0 = -Math.sin(half) * r;
const y0 = Math.cos(half) * r;
parts.push(
new fabric.Path(`M ${x0} ${y0} A ${r} ${r} 0 ${sweep > 1 ? 1 : 0} 1 ${-x0} ${y0}`, {
...base,
left: 0,
top: 0,
}),
);
parts.push(
new fabric.Path(`M ${x0 * 0.88} ${y0 * 0.88} A ${r - 5} ${r - 5} 0 ${sweep > 1 ? 1 : 0} 1 ${-x0 * 0.88} ${y0 * 0.88}`, {
...base,
left: 0,
top: 0,
strokeWidth: width * 0.4,
opacity: 0.5,
}),
);
for (let i = 0; i < def.picots; i++) {
const t = (i + 1) / (def.picots + 1);
const a = -half + t * half * 2;
parts.push(
new fabric.Circle({
...base,
radius: picotR,
left: Math.sin(a) * (r + picotR * 0.8),
top: -Math.cos(a) * (r + picotR * 0.8) + r,
}),
);
}
} else if (def.kind === "picot") {
parts.push(new fabric.Path(`M ${-r} 0 Q 0 ${-r * 1.8} ${r} 0`, { ...base, left: 0, top: 0 }));
parts.push(new fabric.Circle({ ...base, radius: r * 0.35, left: 0, top: -r * 0.6 }));
} else if (def.kind === "bead") {
parts.push(
new fabric.Circle({
radius: r,
left: 0,
top: 0,
originX: "center",
originY: "center",
fill: stroke,
stroke,
strokeWidth: 1,
}),
);
} else {
const pts = [
{ x: 0, y: -r * 1.3 },
{ x: r, y: 0 },
{ x: 0, y: r * 1.3 },
{ x: -r, y: 0 },
];
parts.push(
new fabric.Polygon(pts, {
left: 0,
top: 0,
originX: "center",
originY: "center",
fill: stroke,
stroke,
strokeWidth: 1,
}),
);
parts.push(new fabric.Polygon(pts.map((p) => ({ x: p.x * 0.5, y: p.y * 0.5 })), {
left: 0,
top: 0,
originX: "center",
originY: "center",
fill: "",
stroke: "#ffffff",
opacity: 0.6,
strokeWidth: 1,
}));
}
const group = new fabric.Group(parts, {
originX: "center",
originY: "center",
objectCaching: false,
}) as TattingObject;
group.elId = def.id;
group.elName = def.name;
group.knots = def.knots;
group.pathPx = pathLengthPx(def);
group.threadName = thread.name;
return group;
}
export function applyThread(obj: TattingObject, thread: ThreadColor) {
if (typeof obj.getObjects !== "function") {
obj.set("fill", thread.value);
obj.threadName = thread.name;
obj.dirty = true;
return;
}
const size = Math.max(obj.width ?? 40, obj.height ?? 40) * 0.7;
const filled = obj.elId === "bead" || obj.elId === "crystal";
obj.getObjects().forEach((child, i) => {
child.set("stroke", strokeOf(thread, size));
if (filled && i === 0) child.set("fill", strokeOf(thread, size));
});
obj.threadName = thread.name;
obj.dirty = true;
}
export function applyWidth(obj: TattingObject, width: number) {
if (typeof obj.getObjects !== "function") return;
obj.getObjects().forEach((child) => {
const thin = (child.strokeWidth ?? 1) < 1.5;
child.set("strokeWidth", thin ? width * 0.4 : width);
});
obj.dirty = true;
}