Variables
Const ARROW_DOWN
ARROW_DOWN: "ArrowDown" = "ArrowDown"
Const ARROW_LEFT
ARROW_LEFT: "ArrowLeft" = "ArrowLeft"
Const ARROW_RIGHT
ARROW_RIGHT: "ArrowRight" = "ArrowRight"
Const ARROW_UP
ARROW_UP: "ArrowUp" = "ArrowUp"
Const Arrow
Arrow: any = fabric.util.createClass(fabric.Line, {type: 'arrow',superType: 'drawing',initialize(points: any, options: any) {if (!points) {const { x1, x2, y1, y2 } = options;points = [x1, y1, x2, y2];}options = options || {};this.callSuper('initialize', points, options);},_render(ctx: CanvasRenderingContext2D) {this.callSuper('_render', ctx);ctx.save();const xDiff = this.x2 - this.x1;const yDiff = this.y2 - this.y1;const angle = Math.atan2(yDiff, xDiff);ctx.translate((this.x2 - this.x1) / 2, (this.y2 - this.y1) / 2);ctx.rotate(angle);ctx.beginPath();// Move 5px in front of line to start the arrow so it does not have the square line end showing in front (0,0)ctx.moveTo(5, 0);ctx.lineTo(-5, 5);ctx.lineTo(-5, -5);ctx.closePath();ctx.fillStyle = this.stroke;ctx.fill();ctx.restore();},})
Const BACKSPACE
BACKSPACE: "Backspace" = "Backspace"
Const Canvas
Canvas
: React.FC<CanvasProps> = React.forwardRef<CanvasInstance, CanvasProps>((props, ref) => {const canvasRef = useRef<InternalCanvas>();React.useImperativeHandle(ref, () => ({handler: canvasRef.current.handler,canvas: canvasRef.current.canvas,container: canvasRef.current.container,}));return <InternalCanvas ref={canvasRef} {...props} />;})
Const Chart
Chart: any = fabric.util.createClass(fabric.Rect, {type: 'chart',superType: 'element',hasRotatingPoint: false,initialize(chartOption: echarts.EChartOption, options: any) {options = options || {};this.callSuper('initialize', options);this.set({chartOption,fill: 'rgba(255, 255, 255, 0)',stroke: 'rgba(255, 255, 255, 0)',});},setSource(source: echarts.EChartOption | string) {if (typeof source === 'string') {this.setChartOptionStr(source);} else {this.setChartOption(source);}},setChartOptionStr(chartOptionStr: string) {this.set({chartOptionStr,});},setChartOption(chartOption: echarts.EChartOption) {this.set({chartOption,});this.distroyChart();this.createChart(chartOption);},createChart(chartOption: echarts.EChartOption) {this.instance = echarts.init(this.element);if (!chartOption) {this.instance.setOption({xAxis: {},yAxis: {},series: [{type: 'line',data: [[0, 1],[1, 2],[2, 3],[3, 4],],},],});} else {this.instance.setOption(chartOption);}},distroyChart() {if (this.instance) {this.instance.dispose();}},toObject(propertiesToInclude: string[]) {return toObject(this, propertiesToInclude, {chartOption: this.get('chartOption'),container: this.get('container'),editable: this.get('editable'),});},_render(ctx: CanvasRenderingContext2D) {this.callSuper('_render', ctx);if (!this.instance) {const { id, scaleX, scaleY, width, height, angle, editable, chartOption } = this;const zoom = this.canvas.getZoom();const left = this.calcCoords().tl.x;const top = this.calcCoords().tl.y;const padLeft = (width * scaleX * zoom - width) / 2;const padTop = (height * scaleY * zoom - height) / 2;this.element = fabric.util.makeElement('div', {id: `${id}_container`,style: `transform: rotate(${angle}deg) scale(${scaleX * zoom}, ${scaleY * zoom});width: ${width}px;height: ${height}px;left: ${left + padLeft}px;top: ${top + padTop}px;position: absolute;user-select: ${editable ? 'none' : 'auto'};pointer-events: ${editable ? 'none' : 'auto'};`,}) as HTMLDivElement;this.createChart(chartOption);const container = document.getElementById(this.container);container.appendChild(this.element);}},})
Const CirclePort
CirclePort: any = fabric.util.createClass(fabric.Circle, {type: 'port',superType: 'port',initialize(options: any) {options = options || {};this.callSuper('initialize', options);},toObject() {return fabric.util.object.extend(this.callSuper('toObject'), {id: this.get('id'),superType: this.get('superType'),enabled: this.get('enabled'),nodeId: this.get('nodeId'),});},_render(ctx: CanvasRenderingContext2D) {this.callSuper('_render', ctx);},})
Const Cube
Cube: any = fabric.util.createClass(fabric.Object, {type: 'cube',superType: 'shape',initialize(options: any) {options = options || {};this.callSuper('initialize', options);},shadeColor(color: any, percent: number) {color = color.substr(1);const num = parseInt(color, 16);const amt = Math.round(2.55 * percent);const R = (num >> 16) + amt;const G = ((num >> 8) & 0x00ff) + amt;const B = (num & 0x0000ff) + amt;return ('#' +(0x1000000 +(R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 +(G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 +(B < 255 ? (B < 1 ? 0 : B) : 255)).toString(16).slice(1));},_render(ctx: CanvasRenderingContext2D) {const { width, height, fill } = this;const wx = width / 2;const wy = width / 2;const h = height / 2;const x = 0;const y = wy;ctx.beginPath();ctx.moveTo(x, y);ctx.lineTo(x - wx, y - wx * 0.5);ctx.lineTo(x - wx, y - h - wx * 0.5);ctx.lineTo(x, y - h * 1);ctx.closePath();ctx.fillStyle = this.shadeColor(fill, -10);ctx.strokeStyle = fill;ctx.stroke();ctx.fill();ctx.beginPath();ctx.moveTo(x, y);ctx.lineTo(x + wy, y - wy * 0.5);ctx.lineTo(x + wy, y - h - wy * 0.5);ctx.lineTo(x, y - h * 1);ctx.closePath();ctx.fillStyle = this.shadeColor(fill, 10);ctx.strokeStyle = this.shadeColor(fill, 50);ctx.stroke();ctx.fill();ctx.beginPath();ctx.moveTo(x, y - h);ctx.lineTo(x - wx, y - h - wx * 0.5);ctx.lineTo(x - wx + wy, y - h - (wx * 0.5 + wy * 0.5));ctx.lineTo(x + wy, y - h - wy * 0.5);ctx.closePath();ctx.fillStyle = this.shadeColor(fill, 20);ctx.strokeStyle = this.shadeColor(fill, 60);ctx.stroke();ctx.fill();ctx.restore();},})
Const CurvedLink
CurvedLink: any = fabric.util.createClass(Link, {type: 'curvedLink',superType: 'link',initialize(fromNode: Partial<NodeObject>,fromPort: Partial<PortObject>,toNode: Partial<NodeObject>,toPort: Partial<PortObject>,options: Partial<LinkObject>,) {options = options || {};this.callSuper('initialize', fromNode, fromPort, toNode, toPort, options);},_render(ctx: CanvasRenderingContext2D) {// Drawing curved linkconst { x1, y1, x2, y2 } = this;ctx.lineWidth = this.strokeWidth;ctx.strokeStyle = this.stroke;const fp = { x: (x1 - x2) / 2, y: (y1 - y2) / 2 };const sp = { x: (x2 - x1) / 2, y: (y2 - y1) / 2 };ctx.beginPath();ctx.moveTo(fp.x, fp.y);ctx.bezierCurveTo(fp.x, sp.y, sp.x, fp.y, sp.x, sp.y);ctx.stroke();ctx.save();if (this.fromNode.descriptor?.outPortType === 'STATIC' || this.fromNode.outPortType === 'STATIC') {ctx.font = '12px flomon-icon';ctx.fillStyle = this.fromNode.fromPort.filter((port: PortObject) => port.id === this.fromPort.id,)[0].originFill;ctx.fillText(this.fromPort.id.toUpperCase(), (fp.x + sp.x) / 2 + 10, (fp.y + sp.y) / 2 - 10);}const xDiff = x2 - x1;const yDiff = y2 - y1;const angle = Math.atan2(yDiff, xDiff);ctx.translate((x2 - x1) / 2, (y2 - y1) / 2);ctx.rotate(angle >= 0 ? 1.57 : -1.57);ctx.beginPath();if (this.arrow) {// Move 5px in front of line to start the arrow so it does not have the square line end showing in front (0,0)ctx.moveTo(5, 0);ctx.lineTo(-5, 5);ctx.lineTo(-5, -5);}ctx.closePath();ctx.fillStyle = this.stroke;ctx.fill();ctx.restore();},})
Const DELETE
DELETE: "Delete" = "Delete"
Const EMBOSS_MATRIX
EMBOSS_MATRIX: number[] = [1, 1, 1, 1, 0.7, -1, -1, -1, -1]
Const EQUAL
EQUAL: "Equal" = "Equal"
Const ESCAPE
ESCAPE: "Escape" = "Escape"
Const Element
Element: any = fabric.util.createClass(fabric.Rect, {type: 'element',superType: 'element',hasRotatingPoint: false,initialize(code = initialCode, options: any) {options = options || {};this.callSuper('initialize', options);this.set({code,fill: 'rgba(255, 255, 255, 0)',stroke: 'rgba(255, 255, 255, 0)',});},setSource(source: any) {this.setCode(source);},setCode(code = initialCode) {this.set({code,});const { css, js, html } = code;this.styleEl.innerHTML = css;this.scriptEl.innerHTML = js;this.element.innerHTML = html;},toObject(propertiesToInclude: string[]) {return toObject(this, propertiesToInclude, {code: this.get('code'),container: this.get('container'),editable: this.get('editable'),});},_render(ctx: CanvasRenderingContext2D) {this.callSuper('_render', ctx);if (!this.element) {const { id, scaleX, scaleY, width, height, angle, editable, code } = this;const zoom = this.canvas.getZoom();const left = this.calcCoords().tl.x;const top = this.calcCoords().tl.y;const padLeft = (width * scaleX * zoom - width) / 2;const padTop = (height * scaleY * zoom - height) / 2;this.element = fabric.util.makeElement('div', {id: `${id}_container`,style: `transform: rotate(${angle}deg) scale(${scaleX * zoom}, ${scaleY * zoom});width: ${width}px;height: ${height}px;left: ${left + padLeft}px;top: ${top + padTop}px;position: absolute;user-select: ${editable ? 'none' : 'auto'};pointer-events: ${editable ? 'none' : 'auto'};`,}) as HTMLDivElement;const { html, css, js } = code;this.styleEl = document.createElement('style');this.styleEl.id = `${id}_style`;this.styleEl.type = 'text/css';this.styleEl.innerHTML = css;document.head.appendChild(this.styleEl);this.scriptEl = document.createElement('script');this.scriptEl.id = `${id}_script`;this.scriptEl.type = 'text/javascript';this.scriptEl.innerHTML = js;document.head.appendChild(this.scriptEl);const container = document.getElementById(this.container);container.appendChild(this.element);this.element.innerHTML = html;}},})
Const FILTER_TYPES
FILTER_TYPES: string[] = ['grayscale','invert','remove-color','sepia','brownie','brightness','contrast','saturation','noise','vintage','pixelate','blur','sharpen','emboss','technicolor','polaroid','blend-color','gamma','kodachrome','blackwhite','blend-image','hue','resize','tint','mask','multiply','sepia2',]
Const Gif
Gif: any = fabric.util.createClass(fabric.Image, {type: 'gif',superType: 'image',gifCanvas: null,gifler: undefined,isStarted: false,initialize(options: any) {options = options || {};this.gifCanvas = document.createElement('canvas');this.callSuper('initialize', this.gifCanvas, options);},drawFrame(ctx: CanvasRenderingContext2D, frame: any) {// update canvas sizethis.gifCanvas.width = frame.width;this.gifCanvas.height = frame.height;// update canvas that we are using for fabric.jsctx.drawImage(frame.buffer, 0, 0);this.canvas?.renderAll();},_render(ctx: CanvasRenderingContext2D) {this.callSuper('_render', ctx);this.dirty = true;if (!this.isStarted) {this.isStarted = true;this.gifler = window// @ts-ignore.gifler('https://themadcreator.github.io/gifler/assets/gif/nyan.gif')// .gifler('./images/sample/earth.gif').frames(this.gifCanvas, (context: CanvasRenderingContext2D, frame: any) => {this.isStarted = true;this.drawFrame(context, frame);});}},})
Const Iframe
Iframe: any = fabric.util.createClass(fabric.Rect, {type: 'iframe',superType: 'element',hasRotatingPoint: false,initialize(src: string = '', options: any) {options = options || {};this.callSuper('initialize', options);this.set({src,fill: 'rgba(255, 255, 255, 0)',stroke: 'rgba(255, 255, 255, 0)',});},setSource(source: any) {this.setSrc(source);},setSrc(src: string) {this.set({src,});this.iframeElement.src = src;},toObject(propertiesToInclude: string[]) {return toObject(this, propertiesToInclude, {src: this.get('src'),container: this.get('container'),editable: this.get('editable'),});},_render(ctx: CanvasRenderingContext2D) {this.callSuper('_render', ctx);if (!this.element) {const { id, scaleX, scaleY, width, height, angle, editable, src } = this;const zoom = this.canvas.getZoom();const left = this.calcCoords().tl.x;const top = this.calcCoords().tl.y;const padLeft = (width * scaleX * zoom - width) / 2;const padTop = (height * scaleY * zoom - height) / 2;this.iframeElement = fabric.util.makeElement('iframe', {id,src,width: '100%',height: '100%',});this.element = fabric.util.wrapElement(this.iframeElement, 'div', {id: `${id}_container`,style: `transform: rotate(${angle}deg) scale(${scaleX * zoom}, ${scaleY * zoom});width: ${width}px;height: ${height}px;left: ${left + padLeft}px;top: ${top + padTop}px;position: absolute;user-select: ${editable ? 'none' : 'auto'};pointer-events: ${editable ? 'none' : 'auto'};`,}) as HTMLDivElement;const container = document.getElementById(this.container);container.appendChild(this.element);}},})
Const KEY_A
KEY_A: "KeyA" = "KeyA"
Const KEY_C
KEY_C: "KeyC" = "KeyC"
Const KEY_O
KEY_O: "KeyO" = "KeyO"
Const KEY_P
KEY_P: "KeyP" = "KeyP"
Const KEY_Q
KEY_Q: "KeyQ" = "KeyQ"
Const KEY_V
KEY_V: "KeyV" = "KeyV"
Const KEY_W
KEY_W: "KeyW" = "KeyW"
Const KEY_X
KEY_X: "KeyX" = "KeyX"
Const KEY_Y
KEY_Y: "KeyY" = "KeyY"
Const KEY_Z
KEY_Z: "KeyZ" = "KeyZ"
Const Line
Line: any = fabric.util.createClass(fabric.Line, {type: 'line',superType: 'drawing',initialize(points: any, options: any) {if (!points) {const { x1, x2, y1, y2 } = options;points = [x1, y1, x2, y2];}options = options || {};this.callSuper('initialize', points, options);},_render(ctx: CanvasRenderingContext2D) {this.callSuper('_render', ctx);},})
Const LineLink
LineLink: any = fabric.util.createClass(fabric.Line, {type: 'lineLink',superType: 'link',initialize(fromNode: Partial<NodeObject>,fromPort: Partial<PortObject>,toNode: Partial<NodeObject>,toPort: Partial<PortObject>,options: Partial<LineLinkObject>,) {options = options || {};const coords = [fromPort.left, fromPort.top, toPort.left, toPort.top];Object.assign(options, {strokeWidth: 4,id: options.id || uuid(),originX: 'center',originY: 'center',lockScalingX: true,lockScalingY: true,lockRotation: true,hasRotatingPoint: false,hasControls: false,hasBorders: false,perPixelTargetFind: true,lockMovementX: true,lockMovementY: true,selectable: false,fromNode,fromPort,toNode,toPort,hoverCursor: 'pointer',});this.callSuper('initialize', coords, options);},setPort(fromNode: NodeObject, fromPort: PortObject, _toNode: NodeObject, toPort: PortObject) {if (fromNode.type === 'BroadcastNode') {fromPort = fromNode.fromPort[0];}fromPort.links.push(this);toPort.links.push(this);this.setPortEnabled(fromNode, fromPort, false);},setPortEnabled(node: NodeObject, port: PortObject, enabled: boolean) {if (node.descriptor.outPortType !== OUT_PORT_TYPE.BROADCAST) {port.set({enabled,fill: enabled ? port.originFill : port.selectFill,});} else {if (node.toPort.id === port.id) {return;}port.links.forEach((link, index) => {link.set({fromPort: port,fromPortIndex: index,});});node.set({configuration: {outputCount: port.links.length,},});}},toObject() {return fabric.util.object.extend(this.callSuper('toObject'), {id: this.get('id'),name: this.get('name'),superType: this.get('superType'),configuration: this.get('configuration'),fromNode: this.get('fromNode'),fromPort: this.get('fromPort'),toNode: this.get('toNode'),toPort: this.get('toPort'),});},_render(ctx: CanvasRenderingContext2D) {this.callSuper('_render', ctx);ctx.save();const xDiff = this.x2 - this.x1;const yDiff = this.y2 - this.y1;const angle = Math.atan2(yDiff, xDiff);ctx.translate((this.x2 - this.x1) / 2, (this.y2 - this.y1) / 2);ctx.rotate(angle);ctx.beginPath();if (this.arrow) {// Move 5px in front of line to start the arrow so it does not have the square line end showing in front (0,0)ctx.moveTo(5, 0);ctx.lineTo(-5, 5);ctx.lineTo(-5, -5);}ctx.closePath();ctx.lineWidth = this.strokeWidth;ctx.fillStyle = this.stroke;ctx.fill();ctx.restore();},})
Const Link
Link: any = fabric.util.createClass(fabric.Group, {type: 'link',superType: 'link',initialize(fromNode: Partial<NodeObject>,fromPort: Partial<PortObject>,toNode: Partial<NodeObject>,toPort: Partial<PortObject>,options: Partial<LinkObject>,) {const { left, top, ...other } = options || {};this.fromNode = fromNode;this.fromPort = fromPort;this.toNode = toNode;this.toPort = toPort;const { line, arrow } = this.draw(fromPort, toPort, options);this.line = line;this.arrow = arrow;Object.assign(other, {id: options.id || uuid(),originX: 'center',originY: 'center',lockScalingX: true,lockScalingY: true,lockRotation: true,hasRotatingPoint: false,hasControls: false,hasBorders: false,perPixelTargetFind: true,lockMovementX: true,lockMovementY: true,selectable: false,fromNode,fromPort,toNode,toPort,hoverCursor: 'pointer',objectCaching: false,});this.callSuper('initialize', [line, arrow], other);},setPort(fromNode: NodeObject, fromPort: PortObject, _toNode: NodeObject, toPort: PortObject) {if (fromNode.outPortType === 'BROADCAST') {fromPort = fromNode.fromPort[0];}fromPort.links.push(this);toPort.links.push(this);this.setPortEnabled(fromNode, fromPort, false);},setPortEnabled(node: NodeObject, port: PortObject, enabled: boolean) {if (node.descriptor.outPortType !== OUT_PORT_TYPE.BROADCAST) {port.set({ enabled, fill: enabled ? port.originFill : port.selectFill });} else {if (node.toPort.id === port.id) {return;}port.links.forEach((link, index) => link.set({ fromPort: port, fromPortIndex: index }));node.set({ configuration: { outputCount: port.links.length } });}},setColor(color: string) {this.line.set({ stroke: color });this.arrow.set({ fill: color });},/*** fabric.Path용 setPath 헬퍼 (FabricJS v4.6.0)* @param {string} pathStr - 새로운 SVG path 문자열*/parsePath(pathStr: string) {const tempPathObj = new fabric.Path(pathStr);return tempPathObj.path;},getPortPosition(node: NodeObject, direction: string) {const { left, top, width, height } = node || {};switch (direction) {case 'R':return { x: left + width, y: top + height / 2 };case 'L':return { x: left, y: top + height / 2 };case 'T':return { x: width ? left + width / 2 : left, y: top };case 'B':return { x: left, y: top };default:return { x: 0, y: 0 };}},draw(fromPort: PortObject, toPort: PortObject, options: any = {}) {const { strokeWidth = 2, stroke } = options;const { path, midX, midY, angle } = this.calculatePath(fromPort, toPort);const line = new fabric.Path(path, {strokeWidth: strokeWidth || 2,fill: '',originX: 'center',originY: 'center',stroke,selectable: false,evented: false,strokeLineJoin: 'round',objectCaching: false,});const arrow = new fabric.Triangle({left: midX,top: midY,originX: 'center',originY: 'center',angle: angle,width: 9,height: 9,fill: stroke,selectable: false,evented: false,});return { line, arrow };},update(fromPort: Partial<PortObject>, toPort: Partial<PortObject>) {const { path, midX, midY, angle } = this.calculatePath(fromPort, toPort);this.removeWithUpdate(this.line);this.line = new fabric.Path(path, {strokeWidth: 2,fill: '',originX: 'center',originY: 'center',stroke: this.stroke,selectable: false,evented: false,strokeLineJoin: 'round',objectCaching: false,});this.addWithUpdate(this.line);this.arrow.set({ left: midX - this.left, top: midY - this.top, angle: angle });this.arrow.setCoords();this.canvas.requestRenderAll();},calculatePath(fromPort: Partial<PortObject>, toPort: Partial<PortObject>) {const p1 = this.getPortPosition(fromPort, 'B');const p2 = this.getPortPosition(toPort, 'T');const width = this.fromNode?.width || 200;const height = this.fromNode?.height || 40;const offset = 40;const fromGroup = this.fromNode.group;const toGroup = this.toNode.group;const fromNodeLeft = this.fromNode.left + (fromGroup ? fromGroup.left + fromGroup.width / 2 : 0);const toNodeLeft = this.toNode.left + (toGroup ? toGroup.left + toGroup.width / 2 : 0);let x1 = p1.x;let y1 = p1.y;let x2 = x1;let y2 = y1 + height / 2;let x3 = x2 - fromPort.left + fromNodeLeft - offset;let y3 = p2.y - height / 2;let x4 = p2.x;let y4 = p2.y;const useCurve = p2.y > p1.y;const diff = x3 - (x2 - width);let path;if (useCurve) {path = `M ${p1.x} ${p1.y} C ${p1.x} ${p1.y + offset}, ${p2.x} ${p1.y === p2.y ? p2.y : p2.y - offset}, ${p2.x} ${p2.y}`;} else {const baseRadius = 10;const dx = p1.x - p2.x;const isUpward = width - diff <= dx && dx >= 0;const distance = Math.abs(width - diff - dx);let ratio = Math.min(1, distance / offset);let radius = baseRadius * ratio;if (this.onlyLeft) {path = [`M ${x1} ${y1}`,`L ${x2} ${y2 - baseRadius}`,`Q ${x2} ${y2} ${x2 - baseRadius} ${y2}`,`L ${x3 + baseRadius} ${y2}`,`Q ${x3} ${y2} ${x3} ${y2 - baseRadius}`,`L ${x3} ${y3 + radius}`,`Q ${x3} ${y3} ${isUpward ? x3 - radius : x3 + radius} ${y3}`,`L ${isUpward ? x4 + radius : x4 - radius} ${y3}`,`Q ${x4} ${y3} ${x4} ${y3 + radius}`,`L ${x4} ${y4}`,].join(' ');} else {const nodeCenterGap = fromNodeLeft + this.fromNode?.width / 2 - (toNodeLeft + this.toNode?.width / 2);const gap = isNaN(nodeCenterGap) ? x1 - x4 : nodeCenterGap;const isNegativeShift = gap <= 0;if (!isNegativeShift) {x3 = fromNodeLeft + this.fromNode.width + offset;}path = [`M ${x1} ${y1}`,`L ${x2} ${y2 - baseRadius}`,`Q ${x2} ${y2} ${isNegativeShift ? x2 - baseRadius : x2 + baseRadius} ${y2}`,`L ${isNegativeShift ? x3 + baseRadius : x3 - baseRadius} ${y2}`,`Q ${x3} ${y2} ${x3} ${y2 - baseRadius}`,`L ${x3} ${y3 + baseRadius}`,`Q ${x3} ${y3} ${isNegativeShift ? x3 + baseRadius : x3 - baseRadius} ${y3}`,`L ${isNegativeShift ? x4 - baseRadius : x4 + baseRadius} ${y3}`,`Q ${x4} ${y3} ${x4} ${y3 + baseRadius}`,`L ${x4} ${y4}`,].join(' ');}}let midX = x3;let midY = (y3 + y2) / 2;let angle = 0;const properties = new svgPathProperties(path);const totalLength = properties.getTotalLength();if (useCurve) {const delta = 1;const ahead = properties.getPointAtLength(totalLength / 2 + delta);const behind = properties.getPointAtLength(totalLength / 2 - delta);const dx = ahead.x - behind.x;const dy = ahead.y - behind.y;midX = (p1.x + p2.x) / 2;midY = (p1.y + p2.y) / 2;angle = Math.atan2(dy, dx) * (180 / Math.PI) + 90;}this.pathProperties = properties;this.samplePoints = [];const length = properties.getTotalLength();const steps = Math.floor(length / 5);for (let i = 0; i <= steps; i++) {const pt = properties.getPointAtLength((i / steps) * length);this.samplePoints.push(pt);}return { path, midX, midY, angle };},isPointNear(pointer: fabric.Point, tolerance = 5) {if (!this.samplePoints) return false;for (const pt of this.samplePoints) {const dx = pointer.x - pt.x;const dy = pointer.y - pt.y;if (Math.sqrt(dx * dx + dy * dy) <= tolerance) {return true;}}return false;},toObject() {return fabric.util.object.extend(this.callSuper('toObject'), {id: this.get('id'),name: this.get('name'),superType: this.get('superType'),configuration: this.get('configuration'),fromNode: this.get('fromNode'),fromNodeId: this.get('fromNodeId'),fromPort: this.get('fromPort'),toNode: this.get('toNode'),toNodeId: this.get('toNodeId'),toPort: this.get('toPort'),});},})
Const MINUS
MINUS: "Minus" = "Minus"
Const Node
Node: any = fabric.util.createClass(fabric.Group, {type: 'node',superType: 'node',initialize(options: any) {options = options || {};const icon = new fabric.IText(options.icon || '\uE174', {fontFamily: 'Font Awesome 5 Free',fontWeight: 900,fontSize: 20,fill: options.color || '#fff',});let name = options.name || 'Default Node';let fontSize = options.fontSize || 16;const fontFamily = options.fontFamily || 'Noto Sans';if (options.name) {const canvas = document.createElement('canvas');const ctx = canvas.getContext('2d');const { text, fontSize: size } = fitTextToRect(ctx!, options.name, fontSize, fontFamily, 150, 32);name = text;fontSize = size;}this.label = new fabric.Text(name || 'Default Node', {fontSize,fontFamily,fontWeight: 400,fill: '#fff',});const rect = new fabric.Rect({rx: 10,ry: 10,width: 200,height: 40,fill: options.fill,stroke: options.stroke,strokeWidth: 2,});this.errorFlag = new fabric.IText('\uf071', {fontFamily: 'Font Awesome 5 Free',fontWeight: 900,fontSize: 14,fill: 'red',visible: options.errors,});const node = [rect, icon, this.label, this.errorFlag];const option = Object.assign({}, options, {id: options.id || uuid(),width: 200,height: 40,originX: 'left',originY: 'top',hasRotatingPoint: false,hasControls: false,fontSize,fontFamily,color: options.color,});this.callSuper('initialize', node, option);icon.set({top: icon.top! + 10,left: icon.left! + 10,});this.label.set({top: this.label.top + this.label.height / 2 + 4,left: this.label.left + 35,});this.errorFlag.set({left: rect.left,top: rect.top,visible: options.errors,});},toObject() {return fabric.util.object.extend(this.callSuper('toObject'), {id: this.get('id'),name: this.get('name'),icon: this.get('icon'),color: this.get('color'),fontSize: this.get('fontSize'),fontFamily: this.get('fontFamily'),description: this.get('description'),superType: this.get('superType'),configuration: this.get('configuration'),nodeClazz: this.get('nodeClazz'),descriptor: this.get('descriptor'),borderColor: this.get('borderColor'),borderScaleFactor: this.get('borderScaleFactor'),dblclick: this.get('dblclick'),deletable: this.get('deletable'),cloneable: this.get('cloneable'),fromPort: this.get('fromPort'),toPort: this.get('toPort'),});},defaultPortOption() {return {nodeId: this.id,hasBorders: false,hasControls: false,hasRotatingPoint: false,selectable: false,originX: 'center',originY: 'center',lockScalingX: true,lockScalingY: true,superType: 'port',originFill: this.fill,hoverFill: this.fill,selectFill: this.fill,fill: this.fill,hoverCursor: 'pointer',strokeWidth: 2,stroke: this.stroke,width: 10,height: 10,links: [] as LinkObject[],enabled: true,};},toPortOption() {return {...this.defaultPortOption(),};},fromPortOption() {return {...this.defaultPortOption(),angle: 45,};},createToPort(left: number, top: number) {if (this.descriptor.inEnabled) {this.toPort = new Port({id: 'defaultInPort',type: 'toPort',...this.toPortOption(),left,top,});}return this.toPort;},createFromPort(left: number, top: number) {if (this.descriptor.outPortType === OUT_PORT_TYPE.BROADCAST) {this.fromPort = this.broadcastPort({ ...this.fromPortOption(), left, top });} else if (this.descriptor.outPortType === OUT_PORT_TYPE.STATIC) {this.fromPort = this.staticPort(left, top);} else if (this.descriptor.outPortType === OUT_PORT_TYPE.DYNAMIC) {this.fromPort = this.dynamicPort({ ...this.fromPortOption(), left, top });} else if (this.descriptor.outPortType === OUT_PORT_TYPE.NONE) {this.fromPort = [];} else {this.fromPort = this.singlePort({ ...this.fromPortOption(), left, top });}return this.fromPort;},singlePort(portOption: any) {const fromPort = new Port({id: 'defaultFromPort',type: 'fromPort',...portOption,});return [fromPort];},staticPort(left: number, top: number) {return this.descriptor.outPorts.map((outPort: string, i: number) => {const fill = i === 0 ? 'rgba(255, 0, 0, 1)' : 'rgba(0, 255, 0, 1)';return new Port({id: outPort,type: 'fromPort',left: i === 0 ? left - 40 : left + 40,top,leftDiff: i === 0 ? -40 : 40,...this.fromPortOption(),fill,originFill: fill,hoverFill: fill,selectFill: fill,label: outPort,color: fill,fontSize: 14,fontFamily: 'Noto Sans',});});},dynamicPort(_portOption: any): any[] {return [];},broadcastPort(portOption: any) {return this.singlePort(portOption);},setErrors(errors: any) {this.set({ errors });if (errors) {this.errorFlag.set({ visible: true });} else {this.errorFlag.set({ visible: false });}},setName(name: string) {const context = this.canvas.getContext('2d');const { text, fontSize, height } = fitTextToRect(context, name, this.fontSize, this.fontFamily, 150, 32);this.label.set({fontSize,text,// -19 magic constanttop: this.height > 60 ? -height / 2 - 19 : -height / 2,});},duplicate() {const options = this.toObject();options.id = uuid();options.name = `${options.name}_clone`;return new Node(options);},_render(ctx: CanvasRenderingContext2D) {this.callSuper('_render', ctx);},})
Const OrthogonalLink
OrthogonalLink: any = fabric.util.createClass(Link, {type: 'OrthogonalLink',superType: 'link',initialize(fromNode: Partial<NodeObject>,fromPort: Partial<PortObject>,toNode: Partial<NodeObject>,toPort: Partial<PortObject>,options: Partial<LinkObject>,) {options = options || {};this.callSuper('initialize', fromNode, fromPort, toNode, toPort, options);},_render(ctx: CanvasRenderingContext2D) {// Drawing orthogonal linkconst { x1, y1, x2, y2 } = this;ctx.lineWidth = this.strokeWidth;ctx.strokeStyle = this.stroke;const fp = { x: (x1 - x2) / 2, y: (y1 - y2) / 2 };const sp = { x: (x2 - x1) / 2, y: (y2 - y1) / 2 };ctx.lineJoin = 'round';ctx.beginPath();ctx.moveTo(fp.x, fp.y);ctx.lineTo(fp.x, sp.y / 2);ctx.lineTo(sp.x, sp.y / 2);ctx.lineTo(sp.x, sp.y);ctx.stroke();ctx.save();if (this.fromNode.descriptor?.outPortType === 'STATIC' || this.fromNode.outPortType === 'STATIC') {ctx.font = '12px flomon-icon';ctx.fillStyle = this.fromNode.fromPort.filter((port: PortObject) => port.id === this.fromPort.id,)[0].originFill;ctx.fillText(this.fromPort.id.toUpperCase(), (fp.x + sp.x) / 2 + 10, (fp.y + sp.y) / 2 - 10);}const xDiff = this.x2 - this.x1;const yDiff = this.y2 - this.y1;const angle = Math.atan2(yDiff, xDiff);ctx.translate((this.x2 - this.x1) / 2, (this.y2 - this.y1) / 2);ctx.rotate(angle >= 0 ? 1.57 : -1.57);ctx.beginPath();if (this.arrow) {// Move 5px in front of line to start the arrow so it does not have the square line end showing in front (0,0)ctx.moveTo(5, 0);ctx.lineTo(-5, 5);ctx.lineTo(-5, -5);}ctx.closePath();ctx.fillStyle = this.stroke;ctx.fill();ctx.restore();},})
Const Port
Port: any = fabric.util.createClass(fabric.Rect, {type: 'port',superType: 'port',initialize(options: any) {options = options || {};this.callSuper('initialize', options);},toObject() {return fabric.util.object.extend(this.callSuper('toObject'), {id: this.get('id'),superType: this.get('superType'),enabled: this.get('enabled'),nodeId: this.get('nodeId'),label: this.get('label'),fontSize: this.get('fontSize'),fontFamily: this.get('fontFamily'),color: this.get('color'),});},_render(ctx: CanvasRenderingContext2D) {this.callSuper('_render', ctx);if (this.label) {ctx.save();ctx.font = `${this.fontSize || 12}px ${this.fontFamily || 'Helvetica'}`;ctx.fillStyle = this.color || '#000';const { width } = ctx.measureText(this.label);ctx.rotate((360 - this.angle) * (Math.PI / 180));ctx.fillText(this.label, -width / 2, this.height + 14);ctx.restore();}},})
Const SHARPEN_MATRIX
SHARPEN_MATRIX: number[] = [0, -1, 0, -1, 5, -1, 0, -1, 0]
Const SPACE
SPACE: "Space" = "Space"
Const Svg
Svg: any = fabric.util.createClass(fabric.Group, {type: 'svg',initialize(option: SvgOption = {}) {this.callSuper('initialize', [], option);this.loadSvg(option);},addSvgElements(objects: FabricObject[], options: SvgOption) {const createdObj = fabric.util.groupSVGElements(objects, options) as SvgObject;const { height, scaleY } = this;const scale = height ? (height * scaleY) / createdObj.height : createdObj.scaleY;this.set({ ...options, scaleX: scale, scaleY: scale });if (this._objects?.length) {(this as FabricGroup).getObjects().forEach(obj => {this.remove(obj);});}if (createdObj.getObjects) {(createdObj as FabricGroup).getObjects().forEach(obj => {this.add(obj);if (options.fill) {obj.set('fill', options.fill);}if (options.stroke) {obj.set('stroke', options.stroke);}});} else {createdObj.set({originX: 'center',originY: 'center',});if (options.fill) {createdObj.set({fill: options.fill,});}if (options.stroke) {createdObj.set({stroke: options.stroke,});}if (this._objects?.length) {(this as FabricGroup)._objects.forEach(obj => this.remove(obj));}this.add(createdObj);}this.setCoords();if (this.canvas) {this.canvas.requestRenderAll();}return this;},loadSvg(option: SvgOption) {const { src, svg, loadType, fill, stroke } = option;return new Promise<SvgObject>(resolve => {if (loadType === 'svg') {fabric.loadSVGFromString(svg || src, (objects, options) => {resolve(this.addSvgElements(objects, { ...options, fill, stroke }));});} else {fabric.loadSVGFromURL(svg || src, (objects, options) => {resolve(this.addSvgElements(objects, { ...options, fill, stroke }));});}});},setFill(value: any, filter: (obj: FabricObject) => boolean = () => true) {this.getObjects().filter(filter).forEach((obj: FabricObject) => obj.set('fill', value));this.canvas.requestRenderAll();return this;},setStroke(value: any, filter: (obj: FabricObject) => boolean = () => true) {this.getObjects().filter(filter).forEach((obj: FabricObject) => obj.set('stroke', value));this.canvas.requestRenderAll();return this;},toObject(propertiesToInclude: string[]) {return toObject(this, propertiesToInclude, {src: this.get('src'),loadType: this.get('loadType'),});},_render(ctx: CanvasRenderingContext2D) {this.callSuper('_render', ctx);},})
Const Video
Video: any = fabric.util.createClass(fabric.Rect, {type: 'video',superType: 'element',hasRotatingPoint: false,initialize(source: string | File, options: any) {options = options || {};this.callSuper('initialize', options);if (source instanceof File) {this.set({file: source,src: null,});} else {this.set({file: null,src: source,});}this.set({fill: 'rgba(255, 255, 255, 0)',stroke: 'rgba(255, 255, 255, 0)',});},setSource(source: any) {if (source instanceof File) {this.setFile(source);} else {this.setSrc(source);}},setFile(file: File) {this.set({file,src: null,});const reader = new FileReader();reader.onload = () => {this.player.setSrc(reader.result);};reader.readAsDataURL(file);},setSrc(src: string) {this.set({file: null,src,});this.player.setSrc(src);},toObject(propertiesToInclude: string[]) {return toObject(this, propertiesToInclude, {src: this.get('src'),file: this.get('file'),container: this.get('container'),editable: this.get('editable'),});},_render(ctx: CanvasRenderingContext2D) {this.callSuper('_render', ctx);if (!this.element) {const { id, scaleX, scaleY, width, height, angle, editable, src, file, autoplay, muted, loop } = this;const zoom = this.canvas.getZoom();const left = this.calcCoords().tl.x;const top = this.calcCoords().tl.y;const padLeft = (width * scaleX * zoom - width) / 2;const padTop = (height * scaleY * zoom - height) / 2;this.videoElement = fabric.util.makeElement('video', {id,autoplay: editable ? false : autoplay,muted: editable ? false : muted,loop: editable ? false : loop,preload: 'none',controls: false,});this.element = fabric.util.wrapElement(this.videoElement, 'div', {id: `${id}_container`,style: `transform: rotate(${angle}deg) scale(${scaleX * zoom}, ${scaleY * zoom});width: ${width}px;height: ${height}px;left: ${left + padLeft}px;top: ${top + padTop}px;position: absolute;user-select: ${editable ? 'none' : 'auto'};pointer-events: ${editable ? 'none' : 'auto'};`,}) as HTMLDivElement;const container = document.getElementById(this.container);container.appendChild(this.element);this.player = new MediaElementPlayer(id, {pauseOtherPlayers: false,videoWidth: '100%',videoHeight: '100%',success: (_mediaeElement: any, _originalNode: any, instance: any) => {if (editable) {instance.pause();}},});this.player.setPlayerSize(width, height);if (src) {this.setSrc(src);} else if (file) {this.setFile(file);}}},})
Const propertiesToInclude
propertiesToInclude: string[] = ['id', 'name', 'locked', 'editable']
toObject util