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);},setPosition(left: number, top: number) {this.set({ left, top });},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 FromPort
FromPort: any = fabric.util.createClass(fabric.Path, {type: 'port',superType: 'port',initialize(options: any = {}) {const { radius = 12, connected } = options;const path = connected? [['M', -radius, 0],['A', radius, radius, 0, 1, 0, radius, 0],['A', radius, radius, 0, 1, 0, -radius, 0],['Z'],]: [['M', -radius, 0], ['A', radius, radius * 0.75, 0, 0, 0, radius, 0], ['L', 0, 0], ['Z']];this.callSuper('initialize', path, options);},setPosition(left: number, top: number) {this.set({ left, top: top + (this.connected ? 0 : this.height + (this.strokeWidth ?? 0)) });},setConnected(connected?: boolean) {const radius = connected ? 4 : 12;const strokeWidth = connected ? 0 : 2;const fill = connected ? this.connectedFill : this.originFill;this.initialize({ ...this.toObject(), connected, radius, strokeWidth, fill });this.setCoords();this.canvas.requestRenderAll();},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'),connected: this.get('connected'),});},_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 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: port.originFill,});} 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: port.originFill });} 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(port: Partial<PortObject>, direction: string) {const { left = 0, top = 0, width = 0, height = 0, strokeWidth = 0 } = port || {};switch (direction) {case 'R':return { x: left + width / 2, y: top };case 'L':return { x: left - width / 2, y: top };case 'T':return { x: left, y: top - height / 2 - strokeWidth / 2 };case 'B':return { x: left, y: top + height / 2 + strokeWidth / 2 };default:return { x: left, y: top };}},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 || 240;const height = this.fromNode?.height || 60;const curvedOffset = Math.floor(p1.x) === Math.floor(p2.x) ? 0 : 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 + curvedOffset}, ${p2.x} ${p1.y === p2.y ? p2.y : p2.y - curvedOffset}, ${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 || {};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,options.descriptor.actionButton ? 142 : 168,48,);name = text;fontSize = size;}this.label = new fabric.Text(name || 'Default Node', {fontSize,fontFamily,fontWeight: 400,fill: '#fff',});this.rect = new fabric.Rect({rx: 12,ry: 12,width: 240,height: 60,strokeWidth: 1,fill: options.fill,stroke: options.stroke,});this.nodeIcon = this.createNodeIcon(options);this.errorFlag = this.createErrorFlag();const node = [this.rect, this.nodeIcon, this.label, this.errorFlag];if (options.descriptor.actionButton) {this.button = this.createActionButton();node.push(this.button);}const option = Object.assign({}, options, {id: options.id || uuid(),width: 240,height: 60,originX: 'left',originY: 'top',hasRotatingPoint: false,hasControls: false,fontSize,fontFamily,color: options.color,subTargetCheck: !!options.descriptor.actionButton,originStroke: options.stroke,});this.callSuper('initialize', node, option);this.label.set({left: this.nodeIcon.left + this.nodeIcon.width + 10,top: this.nodeIcon.top + this.nodeIcon.height / 2 - this.label.height / 2,});this.errorFlag.set({ visible: options.errors });if (options.descriptor.actionButton) {this.button.set({left: this.rect.left + this.rect.width - this.button.width + 1,top: this.rect.top + this.rect.height - this.button.height + 1,});this.button.setCoords();}},defaultPortOption() {return {nodeId: this.id,hasBorders: false,hasControls: false,hasRotatingPoint: false,selectable: false,originX: 'center',originY: 'center',lockScalingX: true,lockScalingY: true,superType: 'port',connectedFill: this.color || '#fff',disabledFill: 'red',enabledFill: 'green',originFill: '#5f646b',fill: '#5f646b',hoverCursor: 'pointer',strokeWidth: 2,stroke: this.stroke,links: [] as LinkObject[],enabled: true,};},toPortOption() {return {...this.defaultPortOption(),height: 6,width: 24,};},fromPortOption() {return {...this.defaultPortOption(),radius: 12,};},createNodeIcon(options: any) {const { h, s, v } = Color(options.color).hsv().object();const iconBox = new fabric.Rect({width: 48,height: 48,rx: 10,ry: 10,left: 8,top: 7,strokeWidth: 0,});iconBox.set('fill',new fabric.Gradient({type: 'linear',coords: { x1: 0, y1: 0, x2: 0, y2: 40 },colorStops: [{ offset: 0, color: options.color },{ offset: 1, color: Color.hsv(h, s, v - 60).hex() },],}),);const icon = new fabric.IText(options.icon || '\uE174', {fontFamily: 'Font Awesome 5 Free',fontWeight: 900,fontSize: 24,fill: '#fff',});icon.set({ left: iconBox.width / 2 - icon.width / 2 + 8, top: icon.height / 2 + 5 });return new fabric.Group([iconBox, icon]);},createErrorFlag() {const icon = new fabric.IText('\uf071', {fontFamily: 'Font Awesome 5 Free',fontWeight: 900,fontSize: 12,fill: '#fff',left: 2,top: 2,});const box = this.createIconBoxPath({ width: 20, height: 20, fill: 'red', strokeWidth: 0 }, 12);return new fabric.Group([box, icon], { left: 0, top: 0 });},createActionButton() {const width = 24;const height = 60;const radius = 12;const r = Math.min(radius, height / 2, width / 2); // 한계 보정const path = [`M 0 0`, // 좌상단`L ${width - r} 0`, // 우상단 - 라운드 시작 전`A ${r} ${r} 0 0 1 ${width} ${r}`, // 오른쪽 위 라운드`L ${width} ${height - r}`, // 우하단 - 라운드 시작 전`A ${r} ${r} 0 0 1 ${width - r} ${height}`, // 오른쪽 아래 라운드`L 0 ${height}`, // 좌하단`Z`, // 닫기].join(' ');const box = new fabric.Path(path, { fill: '#5f646b' });const icon = new fabric.IText('\uf04b', {fontFamily: 'Font Awesome 5 Free',fontWeight: 900,fontSize: 14,fill: '#fff',});icon.set({ left: box.width / 2 - icon.width / 2, top: box.height / 2 - icon.height / 2 });return new fabric.Group([box, icon], { hoverCursor: 'pointer' });},createIconBoxPath(options: fabric.IPathOptions, radius: number) {const { width, height, ...other } = options;const path = [`M ${radius} 0`,`Q 0 0 0 ${radius}`,`L 0 ${height}`,`L ${width - radius} ${height}`,`Q ${width} ${height} ${width} ${height - radius}`,`L ${width} 0`,`Z`,].join(' ');return new fabric.Path(path, other);},createToPort(left: number, top: number) {if (this.descriptor.inEnabled) {this.toPort = new ToPort({id: 'defaultInPort',type: 'toPort',...this.toPortOption(),left,top,});this.toPort.setPosition(left, top);}return this.toPort;},createFromPort(left: number, top: number) {if (this.descriptor.outPortType === OUT_PORT_TYPE.STATIC) {const offset = 60;this.fromPort = this.descriptor.outPorts.map((outPort: string, i: number) => {const fill = i === 0 ? '#ff3030' : '#15cc08';const targetLeft = i === 0 ? left - offset : left + offset;const port = new FromPort({id: outPort,type: 'fromPort',left: targetLeft,top,leftDiff: i === 0 ? -offset : offset,...this.fromPortOption(),fill,originFill: fill,label: outPort,color: fill,fontSize: 14,fontFamily: 'Noto Sans',});port.setPosition(targetLeft, top);return port;});} else if (this.descriptor.outPortType === OUT_PORT_TYPE.DYNAMIC) {this.fromPort = [];} else if (this.descriptor.outPortType === OUT_PORT_TYPE.NONE) {this.fromPort = [];} else {const port = new FromPort({id: 'defaultFromPort',type: 'fromPort',...this.fromPortOption(),left,top,});port.setPosition(left, top);this.fromPort = [port];}return this.fromPort;},setErrors(errors: any) {this.set({ errors });if (errors) {this.errorFlag.set({ visible: true });this.rect.set({ stroke: 'red' });} else {this.errorFlag.set({ visible: false });this.rect.set({ stroke: this.originStroke });}},setName(name: string) {const context = this.canvas.getContext('2d');const { text, fontSize, height } = fitTextToRect(context,name,this.fontSize,this.fontFamily,this.descriptor.actionButton ? 142 : 168,48,);this.label.set({fontSize,text,// -19 magic constanttop: this.height > 60 ? -height / 2 - 19 : -height / 2,});},select() {this.rect.set({ strokeDashArray: [3, 3], strokeWidth: 2 });this.canvas.requestRenderAll();},unselect() {this.rect.set({ strokeDashArray: null, strokeWidth: 1 });this.canvas.requestRenderAll();},duplicate() {const options = this.toObject();options.id = uuid();options.name = `${options.name}_clone`;return new Node(options);},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'),});},_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);},setPosition(left: number, top: number) {this.set({ left, top });},setConnected(connected?: boolean) {const fill = connected ? this.connectedFill : this.originFill;this.initialize({ ...this.toObject(), connected, fill });this.setCoords();this.canvas.requestRenderAll();},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'),connected: this.get('connected'),});},_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 ToPort
ToPort: any = fabric.util.createClass(fabric.Path, {type: 'port',superType: 'port',initialize(options: any = {}) {const { width, height, connected, radius = 10 } = options;const path = connected? [['M', -radius, 0],['A', radius, radius, 0, 1, 0, radius, 0],['A', radius, radius, 0, 1, 0, -radius, 0],['Z'],]: [['M', 0, 0], ['H', width], ['V', height], ['H', 0], ['Z']];this.callSuper('initialize', path, options);},setPosition(left: number, top: number) {this.set({left,top: top - (this.connected ? 0 : this.height + (this.strokeWidth ?? 0)),});},setConnected(connected?: boolean) {const radius = connected ? 4 : 0;const strokeWidth = connected ? 0 : 2;const fill = connected ? this.connectedFill : this.originFill;const width = connected ? undefined : 24;const height = connected ? undefined : 6;this.initialize({ ...this.toObject(), connected, radius, strokeWidth, fill, width, height });this.setCoords();this.canvas.requestRenderAll();},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'),});},})
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