Spaces:
Sleeping
Sleeping
| 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; | |
| } | |