From c2e91bca1cf627fdcf7164f7edd44a8897fccd8c Mon Sep 17 00:00:00 2001
From: Chop <28534054+RChopin@users.noreply.github.com>
Date: Thu, 26 Jun 2025 20:00:04 +0200
Subject: [PATCH] init
---
index.html | 67 ++++
readme.md | 0
script.js | 948 +++++++++++++++++++++++++++++++++++++++++++++++++++++
styles.css | 343 +++++++++++++++++++
4 files changed, 1358 insertions(+)
create mode 100644 index.html
create mode 100644 readme.md
create mode 100644 script.js
create mode 100644 styles.css
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..9f16ded
--- /dev/null
+++ b/index.html
@@ -0,0 +1,67 @@
+
+
+
+
+
+ Object Flow Designer
+
+
+
+
+
+
+
+
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..e69de29
diff --git a/script.js b/script.js
new file mode 100644
index 0000000..290fd4e
--- /dev/null
+++ b/script.js
@@ -0,0 +1,948 @@
+class ObjectFlowDesigner {
+ constructor() {
+ this.canvas = document.getElementById("canvas");
+ this.ctx = this.canvas.getContext("2d");
+ this.objects = [];
+ this.connections = [];
+ this.selectedObject = null;
+ this.selectedConnection = null;
+ this.mode = "select"; // 'select' or 'connect'
+ this.connectionStart = null;
+ this.dragOffset = { x: 0, y: 0 };
+ this.isDragging = false;
+ this.nextId = 1;
+
+ this.setupEventListeners();
+ this.resizeCanvas();
+ this.render();
+ }
+
+ setupEventListeners() {
+ // Canvas events
+ this.canvas.addEventListener("mousedown", this.handleMouseDown.bind(this));
+ this.canvas.addEventListener("mousemove", this.handleMouseMove.bind(this));
+ this.canvas.addEventListener("mouseup", this.handleMouseUp.bind(this));
+ this.canvas.addEventListener("click", this.handleClick.bind(this));
+
+ // Palette events
+ document.querySelectorAll(".palette-item").forEach((item) => {
+ item.addEventListener("click", this.handlePaletteClick.bind(this));
+ });
+
+ // Control buttons
+ document
+ .getElementById("connectionBtn")
+ .addEventListener("click", () => this.setMode("connect"));
+ document
+ .getElementById("selectBtn")
+ .addEventListener("click", () => this.setMode("select"));
+ document
+ .getElementById("clearBtn")
+ .addEventListener("click", this.clearAll.bind(this));
+ document
+ .getElementById("exportBtn")
+ .addEventListener("click", this.exportData.bind(this));
+ document.getElementById("importBtn").addEventListener("click", () => {
+ document.getElementById("fileInput").click();
+ });
+ document
+ .getElementById("fileInput")
+ .addEventListener("change", this.importData.bind(this));
+
+ // Window resize
+ window.addEventListener("resize", this.resizeCanvas.bind(this));
+ }
+
+ resizeCanvas() {
+ const rect = this.canvas.getBoundingClientRect();
+ this.canvas.width = rect.width;
+ this.canvas.height = rect.height;
+ this.render();
+ }
+
+ setMode(mode) {
+ this.mode = mode;
+ this.connectionStart = null;
+ this.selectedObject = null;
+ this.selectedConnection = null;
+ this.updatePropertiesPanel();
+
+ document.body.className = `mode-${mode}`;
+
+ // Update button states
+ document
+ .getElementById("connectionBtn")
+ .classList.toggle("btn-primary", mode === "connect");
+ document
+ .getElementById("connectionBtn")
+ .classList.toggle("btn-outline", mode !== "connect");
+ document
+ .getElementById("selectBtn")
+ .classList.toggle("btn-primary", mode === "select");
+ document
+ .getElementById("selectBtn")
+ .classList.toggle("btn-outline", mode !== "select");
+
+ this.render();
+ }
+
+ handlePaletteClick(event) {
+ const type = event.currentTarget.dataset.type;
+
+ // Check if triangle already exists
+ if (
+ type === "triangle" &&
+ this.objects.some((obj) => obj.type === "triangle")
+ ) {
+ this.showError("Only one triangle is allowed!");
+ return;
+ }
+
+ this.createObject(type, 100, 100);
+ }
+
+ createObject(type, x, y) {
+ const id = this.nextId++;
+ const obj = {
+ id,
+ type,
+ x,
+ y,
+ width: type === "triangle" ? 40 : 50,
+ height: type === "triangle" ? 40 : 50,
+ data: {
+ name: `${type.charAt(0).toUpperCase() + type.slice(1)} ${id}`,
+ description: "",
+ value: "",
+ metadata: {},
+ },
+ connections: {
+ inputs: [],
+ outputs: [],
+ },
+ };
+
+ this.objects.push(obj);
+ this.render();
+
+ // Auto-select the new object
+ this.selectedObject = obj;
+ this.updatePropertiesPanel();
+ }
+
+ handleMouseDown(event) {
+ const rect = this.canvas.getBoundingClientRect();
+ const x = event.clientX - rect.left;
+ const y = event.clientY - rect.top;
+
+ const clickedObject = this.getObjectAt(x, y);
+ const clickedConnection = this.getConnectionAt(x, y);
+
+ if (this.mode === "select") {
+ if (clickedObject) {
+ this.selectedObject = clickedObject;
+ this.selectedConnection = null;
+ this.isDragging = true;
+ this.dragOffset = {
+ x: x - clickedObject.x,
+ y: y - clickedObject.y,
+ };
+ this.updatePropertiesPanel();
+ } else if (clickedConnection) {
+ this.selectedConnection = clickedConnection;
+ this.selectedObject = null;
+ this.updatePropertiesPanel();
+ } else {
+ this.selectedObject = null;
+ this.selectedConnection = null;
+ this.updatePropertiesPanel();
+ }
+ } else if (this.mode === "connect" && clickedObject) {
+ if (!this.connectionStart) {
+ // Start connection
+ this.connectionStart = clickedObject;
+ } else {
+ // End connection
+ this.createConnection(this.connectionStart, clickedObject);
+ this.connectionStart = null;
+ }
+ } else {
+ this.selectedObject = null;
+ this.selectedConnection = null;
+ this.connectionStart = null;
+ this.updatePropertiesPanel();
+ }
+
+ this.render();
+ }
+
+ handleMouseMove(event) {
+ const rect = this.canvas.getBoundingClientRect();
+ const x = event.clientX - rect.left;
+ const y = event.clientY - rect.top;
+
+ if (this.isDragging && this.selectedObject) {
+ this.selectedObject.x = x - this.dragOffset.x;
+ this.selectedObject.y = y - this.dragOffset.y;
+ this.render();
+ }
+
+ // Show tooltip
+ const hoveredObject = this.getObjectAt(x, y);
+ this.showTooltip(hoveredObject, x, y);
+ }
+
+ handleMouseUp() {
+ this.isDragging = false;
+ }
+
+ handleClick(event) {
+ // Handle click events that don't require dragging
+ }
+
+ getObjectAt(x, y) {
+ for (let i = this.objects.length - 1; i >= 0; i--) {
+ const obj = this.objects[i];
+ if (obj.type === "triangle") {
+ // Triangle collision detection
+ const centerX = obj.x + obj.width / 2;
+ const centerY = obj.y + obj.height / 2;
+ const distance = Math.sqrt((x - centerX) ** 2 + (y - centerY) ** 2);
+ if (distance <= obj.width / 2) {
+ return obj;
+ }
+ } else {
+ // Rectangle collision detection
+ if (
+ x >= obj.x &&
+ x <= obj.x + obj.width &&
+ y >= obj.y &&
+ y <= obj.y + obj.height
+ ) {
+ return obj;
+ }
+ }
+ }
+ return null;
+ }
+
+ getConnectionAt(x, y) {
+ const tolerance = 10; // Distance tolerance for clicking on connection lines
+
+ for (let connection of this.connections) {
+ const fromPoint = this.getConnectionPoint(connection.from, "output");
+ const toPoint = this.getConnectionPoint(connection.to, "input");
+
+ // Calculate bezier curve points
+ const controlX1 = fromPoint.x + 50;
+ const controlY1 = fromPoint.y;
+ const controlX2 = toPoint.x - 50;
+ const controlY2 = toPoint.y;
+
+ // Sample points along the bezier curve and check distance
+ for (let t = 0; t <= 1; t += 0.05) {
+ const curvePoint = this.getBezierPoint(
+ fromPoint.x,
+ fromPoint.y,
+ controlX1,
+ controlY1,
+ controlX2,
+ controlY2,
+ toPoint.x,
+ toPoint.y,
+ t
+ );
+
+ const distance = Math.sqrt(
+ (x - curvePoint.x) ** 2 + (y - curvePoint.y) ** 2
+ );
+ if (distance <= tolerance) {
+ return connection;
+ }
+ }
+ }
+ return null;
+ }
+
+ getBezierPoint(x1, y1, cx1, cy1, cx2, cy2, x2, y2, t) {
+ const u = 1 - t;
+ const tt = t * t;
+ const uu = u * u;
+ const uuu = uu * u;
+ const ttt = tt * t;
+
+ return {
+ x: uuu * x1 + 3 * uu * t * cx1 + 3 * u * tt * cx2 + ttt * x2,
+ y: uuu * y1 + 3 * uu * t * cy1 + 3 * u * tt * cy2 + ttt * y2,
+ };
+ }
+
+ createConnection(fromObj, toObj) {
+ // Validate connection rules
+ if (fromObj === toObj) {
+ this.showError("Cannot connect object to itself!");
+ return;
+ }
+
+ // Check if connection already exists
+ if (
+ this.connections.some(
+ (conn) => conn.from === fromObj && conn.to === toObj
+ )
+ ) {
+ this.showError("Connection already exists!");
+ return;
+ }
+
+ // Triangle can only have one output
+ if (
+ fromObj.type === "triangle" &&
+ fromObj.connections.outputs.length >= 1
+ ) {
+ this.showError("Triangle can only have one outgoing connection!");
+ return;
+ }
+
+ // Square can only have one input from the left
+ if (toObj.type === "square" && toObj.connections.inputs.length >= 1) {
+ this.showError("Square can only have one incoming connection!");
+ return;
+ }
+
+ // Only allow triangle->square or square->square connections
+ if (fromObj.type === "square" && toObj.type === "triangle") {
+ this.showError("Cannot connect from square to triangle!");
+ return;
+ }
+
+ const connection = {
+ id: this.nextId++,
+ from: fromObj,
+ to: toObj,
+ data: {
+ label: `Connection ${this.nextId - 1}`,
+ description: "",
+ weight: 1,
+ dataType: "default",
+ metadata: {},
+ },
+ };
+
+ this.connections.push(connection);
+ fromObj.connections.outputs.push(connection);
+ toObj.connections.inputs.push(connection);
+
+ this.render();
+ }
+
+ render() {
+ // Clear canvas
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
+
+ // Draw grid
+ this.drawGrid();
+
+ // Draw connections
+ this.connections.forEach((connection) => this.drawConnection(connection));
+
+ // Draw objects
+ this.objects.forEach((obj) => this.drawObject(obj));
+
+ // Draw connection preview
+ if (this.mode === "connect" && this.connectionStart) {
+ this.drawConnectionPreview();
+ }
+ }
+
+ drawGrid() {
+ const gridSize = 20;
+ this.ctx.strokeStyle = "#f0f0f0";
+ this.ctx.lineWidth = 1;
+
+ for (let x = 0; x < this.canvas.width; x += gridSize) {
+ this.ctx.beginPath();
+ this.ctx.moveTo(x, 0);
+ this.ctx.lineTo(x, this.canvas.height);
+ this.ctx.stroke();
+ }
+
+ for (let y = 0; y < this.canvas.height; y += gridSize) {
+ this.ctx.beginPath();
+ this.ctx.moveTo(0, y);
+ this.ctx.lineTo(this.canvas.width, y);
+ this.ctx.stroke();
+ }
+ }
+
+ drawObject(obj) {
+ const isSelected = obj === this.selectedObject;
+ const isConnectionStart = obj === this.connectionStart;
+
+ this.ctx.save();
+
+ if (obj.type === "triangle") {
+ this.drawTriangle(obj, isSelected, isConnectionStart);
+ } else {
+ this.drawSquare(obj, isSelected, isConnectionStart);
+ }
+
+ this.ctx.restore();
+ }
+
+ drawTriangle(obj, isSelected, isConnectionStart) {
+ const centerX = obj.x + obj.width / 2;
+ const centerY = obj.y + obj.height / 2;
+
+ this.ctx.beginPath();
+ this.ctx.moveTo(centerX, obj.y);
+ this.ctx.lineTo(obj.x, obj.y + obj.height);
+ this.ctx.lineTo(obj.x + obj.width, obj.y + obj.height);
+ this.ctx.closePath();
+
+ // Fill
+ this.ctx.fillStyle = isConnectionStart ? "#38a169" : "#48bb78";
+ this.ctx.fill();
+
+ // Border
+ if (isSelected) {
+ this.ctx.strokeStyle = "#2d3748";
+ this.ctx.lineWidth = 3;
+ } else {
+ this.ctx.strokeStyle = "#2f855a";
+ this.ctx.lineWidth = 2;
+ }
+ this.ctx.stroke();
+
+ // Label
+ this.ctx.fillStyle = "white";
+ this.ctx.font = "12px Arial";
+ this.ctx.textAlign = "center";
+ this.ctx.fillText("▲", centerX, centerY + 4);
+ }
+
+ drawSquare(obj, isSelected, isConnectionStart) {
+ // Fill
+ this.ctx.fillStyle = isConnectionStart ? "#2b6cb0" : "#4299e1";
+ this.ctx.fillRect(obj.x, obj.y, obj.width, obj.height);
+
+ // Border
+ if (isSelected) {
+ this.ctx.strokeStyle = "#2d3748";
+ this.ctx.lineWidth = 3;
+ } else {
+ this.ctx.strokeStyle = "#2c5282";
+ this.ctx.lineWidth = 2;
+ }
+ this.ctx.strokeRect(obj.x, obj.y, obj.width, obj.height);
+
+ // Label
+ this.ctx.fillStyle = "white";
+ this.ctx.font = "12px Arial";
+ this.ctx.textAlign = "center";
+ this.ctx.fillText("■", obj.x + obj.width / 2, obj.y + obj.height / 2 + 4);
+ }
+
+ drawConnection(connection) {
+ const fromObj = connection.from;
+ const toObj = connection.to;
+ const isSelected = connection === this.selectedConnection;
+
+ // Calculate connection points
+ const fromPoint = this.getConnectionPoint(fromObj, "output");
+ const toPoint = this.getConnectionPoint(toObj, "input");
+
+ this.ctx.save();
+ this.ctx.strokeStyle = isSelected ? "#2d3748" : "#4a5568";
+ this.ctx.lineWidth = isSelected ? 4 : 3;
+ this.ctx.lineCap = "round";
+
+ // Draw curved line
+ this.ctx.beginPath();
+ this.ctx.moveTo(fromPoint.x, fromPoint.y);
+
+ const controlX1 = fromPoint.x + 50;
+ const controlY1 = fromPoint.y;
+ const controlX2 = toPoint.x - 50;
+ const controlY2 = toPoint.y;
+
+ this.ctx.bezierCurveTo(
+ controlX1,
+ controlY1,
+ controlX2,
+ controlY2,
+ toPoint.x,
+ toPoint.y
+ );
+ this.ctx.stroke();
+
+ // Draw arrowhead
+ this.drawArrowhead(
+ toPoint.x,
+ toPoint.y,
+ Math.atan2(controlY2 - toPoint.y, controlX2 - toPoint.x),
+ isSelected
+ );
+
+ // Draw connection label if it exists
+ if (connection.data.label) {
+ const midPoint = this.getBezierPoint(
+ fromPoint.x,
+ fromPoint.y,
+ controlX1,
+ controlY1,
+ controlX2,
+ controlY2,
+ toPoint.x,
+ toPoint.y,
+ 0.5
+ );
+
+ this.ctx.fillStyle = "white";
+ this.ctx.strokeStyle = "#4a5568";
+ this.ctx.lineWidth = 2;
+ this.ctx.font = "12px Arial";
+ this.ctx.textAlign = "center";
+
+ const textWidth = this.ctx.measureText(connection.data.label).width;
+ const padding = 4;
+
+ // Draw background
+ this.ctx.fillRect(
+ midPoint.x - textWidth / 2 - padding,
+ midPoint.y - 8 - padding,
+ textWidth + padding * 2,
+ 16 + padding * 2
+ );
+ this.ctx.strokeRect(
+ midPoint.x - textWidth / 2 - padding,
+ midPoint.y - 8 - padding,
+ textWidth + padding * 2,
+ 16 + padding * 2
+ );
+
+ // Draw text
+ this.ctx.fillStyle = "#4a5568";
+ this.ctx.fillText(connection.data.label, midPoint.x, midPoint.y + 4);
+ }
+
+ this.ctx.restore();
+ }
+
+ drawConnectionPreview() {
+ if (!this.connectionStart) return;
+
+ const rect = this.canvas.getBoundingClientRect();
+ const mouseX = event?.clientX - rect.left || 0;
+ const mouseY = event?.clientY - rect.top || 0;
+
+ const fromPoint = this.getConnectionPoint(this.connectionStart, "output");
+
+ this.ctx.save();
+ this.ctx.strokeStyle = "#a0aec0";
+ this.ctx.lineWidth = 2;
+ this.ctx.setLineDash([5, 5]);
+
+ this.ctx.beginPath();
+ this.ctx.moveTo(fromPoint.x, fromPoint.y);
+ this.ctx.lineTo(mouseX, mouseY);
+ this.ctx.stroke();
+
+ this.ctx.restore();
+ }
+
+ getConnectionPoint(obj, type) {
+ if (type === "input") {
+ return {
+ x: obj.x,
+ y: obj.y + obj.height / 2,
+ };
+ } else {
+ return {
+ x: obj.x + obj.width,
+ y: obj.y + obj.height / 2,
+ };
+ }
+ }
+
+ drawArrowhead(x, y, angle, isSelected = false) {
+ const headLength = isSelected ? 12 : 10;
+
+ this.ctx.save();
+ this.ctx.translate(x, y);
+ this.ctx.rotate(angle);
+
+ this.ctx.beginPath();
+ this.ctx.moveTo(0, 0);
+ this.ctx.lineTo(-headLength, -headLength / 2);
+ this.ctx.lineTo(-headLength, headLength / 2);
+ this.ctx.closePath();
+
+ this.ctx.fillStyle = isSelected ? "#2d3748" : "#4a5568";
+ this.ctx.fill();
+
+ this.ctx.restore();
+ }
+
+ updatePropertiesPanel() {
+ const panel = document.getElementById("objectProperties");
+
+ if (this.selectedObject) {
+ this.updateObjectProperties(panel);
+ } else if (this.selectedConnection) {
+ this.updateConnectionProperties(panel);
+ } else {
+ panel.innerHTML = 'No object selected
';
+ }
+ }
+
+ updateObjectProperties(panel) {
+ const obj = this.selectedObject;
+ panel.innerHTML = `
+
+
Object Properties
+
+
+
+
+
+
+
+
+
+
Object Info
+
Type: ${obj.type}
+
ID: ${obj.id}
+
Position: (${Math.round(
+ obj.x
+ )}, ${Math.round(obj.y)})
+
Inputs: ${obj.connections.inputs.length}
+
Outputs: ${
+ obj.connections.outputs.length
+ }
+
+
+
+
Actions
+
+
+ `;
+
+ // Add event listeners for property changes
+ document.getElementById("objName").addEventListener("input", (e) => {
+ obj.data.name = e.target.value;
+ });
+
+ document.getElementById("objDescription").addEventListener("input", (e) => {
+ obj.data.description = e.target.value;
+ });
+
+ document.getElementById("objValue").addEventListener("input", (e) => {
+ obj.data.value = e.target.value;
+ });
+ }
+
+ updateConnectionProperties(panel) {
+ const conn = this.selectedConnection;
+ panel.innerHTML = `
+
+
Connection Properties
+
+
+
+
+
+
+
+
+
+
+
+
Connection Info
+
ID: ${conn.id}
+
From: ${conn.from.data.name} (${
+ conn.from.type
+ })
+
To: ${conn.to.data.name} (${
+ conn.to.type
+ })
+
Flow Direction: ${conn.from.data.name} → ${
+ conn.to.data.name
+ }
+
+
+
+
Actions
+
+
+ `;
+
+ // Add event listeners for property changes
+ document.getElementById("connLabel").addEventListener("input", (e) => {
+ conn.data.label = e.target.value;
+ this.render(); // Re-render to show label changes
+ });
+
+ document
+ .getElementById("connDescription")
+ .addEventListener("input", (e) => {
+ conn.data.description = e.target.value;
+ });
+
+ document.getElementById("connWeight").addEventListener("input", (e) => {
+ conn.data.weight = parseFloat(e.target.value) || 0;
+ });
+
+ document.getElementById("connDataType").addEventListener("change", (e) => {
+ conn.data.dataType = e.target.value;
+ });
+ }
+
+ deleteObject(id) {
+ const objIndex = this.objects.findIndex((obj) => obj.id === id);
+ if (objIndex === -1) return;
+
+ const obj = this.objects[objIndex];
+
+ // Remove all connections involving this object
+ this.connections = this.connections.filter((conn) => {
+ if (conn.from === obj || conn.to === obj) {
+ // Remove connection references from other objects
+ if (conn.from !== obj) {
+ conn.from.connections.outputs = conn.from.connections.outputs.filter(
+ (c) => c !== conn
+ );
+ }
+ if (conn.to !== obj) {
+ conn.to.connections.inputs = conn.to.connections.inputs.filter(
+ (c) => c !== conn
+ );
+ }
+ return false;
+ }
+ return true;
+ });
+
+ // Remove the object
+ this.objects.splice(objIndex, 1);
+
+ if (this.selectedObject === obj) {
+ this.selectedObject = null;
+ this.updatePropertiesPanel();
+ }
+
+ this.render();
+ }
+
+ deleteConnection(id) {
+ const connIndex = this.connections.findIndex((conn) => conn.id === id);
+ if (connIndex === -1) return;
+
+ const connection = this.connections[connIndex];
+
+ // Remove connection references from objects
+ connection.from.connections.outputs =
+ connection.from.connections.outputs.filter((c) => c !== connection);
+ connection.to.connections.inputs = connection.to.connections.inputs.filter(
+ (c) => c !== connection
+ );
+
+ // Remove the connection
+ this.connections.splice(connIndex, 1);
+
+ if (this.selectedConnection === connection) {
+ this.selectedConnection = null;
+ this.updatePropertiesPanel();
+ }
+
+ this.render();
+ }
+
+ showTooltip(obj, x, y) {
+ const tooltip = document.getElementById("tooltip");
+
+ if (obj) {
+ tooltip.style.display = "block";
+ tooltip.style.left = x + 10 + "px";
+ tooltip.style.top = y - 30 + "px";
+ tooltip.textContent = `${obj.data.name} (${obj.type})`;
+ } else {
+ // Check if hovering over a connection
+ const connection = this.getConnectionAt(x, y);
+ if (connection) {
+ tooltip.style.display = "block";
+ tooltip.style.left = x + 10 + "px";
+ tooltip.style.top = y - 30 + "px";
+ tooltip.textContent = `${connection.data.label || "Connection"} (${
+ connection.from.data.name
+ } → ${connection.to.data.name})`;
+ } else {
+ tooltip.style.display = "none";
+ }
+ }
+ }
+
+ showError(message) {
+ // Create temporary error message
+ const error = document.createElement("div");
+ error.style.cssText = `
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ background: #e53e3e;
+ color: white;
+ padding: 1rem;
+ border-radius: 8px;
+ z-index: 10000;
+ animation: slideIn 0.3s ease;
+ `;
+ error.textContent = message;
+ document.body.appendChild(error);
+
+ setTimeout(() => {
+ error.remove();
+ }, 3000);
+ }
+
+ clearAll() {
+ if (
+ confirm("Are you sure you want to clear all objects and connections?")
+ ) {
+ this.objects = [];
+ this.connections = [];
+ this.selectedObject = null;
+ this.selectedConnection = null;
+ this.connectionStart = null;
+ this.nextId = 1;
+ this.updatePropertiesPanel();
+ this.render();
+ }
+ }
+
+ exportData() {
+ const data = {
+ objects: this.objects.map((obj) => ({
+ ...obj,
+ connections: {
+ inputs: obj.connections.inputs.map((conn) => conn.id),
+ outputs: obj.connections.outputs.map((conn) => conn.id),
+ },
+ })),
+ connections: this.connections.map((conn) => ({
+ ...conn,
+ from: conn.from.id,
+ to: conn.to.id,
+ })),
+ nextId: this.nextId,
+ };
+
+ const blob = new Blob([JSON.stringify(data, null, 2)], {
+ type: "application/json",
+ });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = "flow-design.json";
+ a.click();
+ URL.revokeObjectURL(url);
+ }
+
+ importData(event) {
+ const file = event.target.files[0];
+ if (!file) return;
+
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ try {
+ const data = JSON.parse(e.target.result);
+
+ // Clear current data
+ this.objects = [];
+ this.connections = [];
+ this.selectedObject = null;
+ this.selectedConnection = null;
+ this.connectionStart = null;
+
+ // Restore objects
+ this.objects = data.objects.map((obj) => ({
+ ...obj,
+ connections: { inputs: [], outputs: [] },
+ }));
+
+ // Restore connections
+ data.connections.forEach((connData) => {
+ const fromObj = this.objects.find((obj) => obj.id === connData.from);
+ const toObj = this.objects.find((obj) => obj.id === connData.to);
+
+ if (fromObj && toObj) {
+ const connection = {
+ ...connData,
+ from: fromObj,
+ to: toObj,
+ };
+
+ this.connections.push(connection);
+ fromObj.connections.outputs.push(connection);
+ toObj.connections.inputs.push(connection);
+ }
+ });
+
+ this.nextId = data.nextId || this.nextId;
+ this.updatePropertiesPanel();
+ this.render();
+ } catch (error) {
+ this.showError("Failed to import file: Invalid format");
+ }
+ };
+ reader.readAsText(file);
+
+ // Reset file input
+ event.target.value = "";
+ }
+}
+
+// Initialize the application
+let designer;
+
+document.addEventListener("DOMContentLoaded", () => {
+ designer = new ObjectFlowDesigner();
+});
+
+// Add CSS animation keyframes
+const style = document.createElement("style");
+style.textContent = `
+ @keyframes slideIn {
+ from {
+ transform: translateX(100%);
+ opacity: 0;
+ }
+ to {
+ transform: translateX(0);
+ opacity: 1;
+ }
+ }
+`;
+document.head.appendChild(style);
diff --git a/styles.css b/styles.css
new file mode 100644
index 0000000..992656b
--- /dev/null
+++ b/styles.css
@@ -0,0 +1,343 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ min-height: 100vh;
+ color: #333;
+}
+
+.container {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+}
+
+.header {
+ background: rgba(255, 255, 255, 0.95);
+ backdrop-filter: blur(10px);
+ padding: 1rem 2rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ box-shadow: 0 2px 20px rgba(0, 0, 0, 0.1);
+ border-bottom: 1px solid rgba(255, 255, 255, 0.2);
+}
+
+.header h1 {
+ color: #4a5568;
+ font-size: 1.8rem;
+ font-weight: 600;
+}
+
+.controls {
+ display: flex;
+ gap: 1rem;
+}
+
+.btn {
+ padding: 0.5rem 1rem;
+ border: none;
+ border-radius: 8px;
+ cursor: pointer;
+ font-weight: 500;
+ transition: all 0.3s ease;
+ text-decoration: none;
+ display: inline-block;
+}
+
+.btn-primary {
+ background: #4299e1;
+ color: white;
+}
+
+.btn-primary:hover {
+ background: #3182ce;
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(66, 153, 225, 0.4);
+}
+
+.btn-secondary {
+ background: #718096;
+ color: white;
+}
+
+.btn-secondary:hover {
+ background: #4a5568;
+ transform: translateY(-2px);
+}
+
+.btn-danger {
+ background: #e53e3e;
+ color: white;
+}
+
+.btn-danger:hover {
+ background: #c53030;
+ transform: translateY(-2px);
+}
+
+.btn-outline {
+ background: transparent;
+ color: #4299e1;
+ border: 2px solid #4299e1;
+}
+
+.btn-outline:hover {
+ background: #4299e1;
+ color: white;
+}
+
+.btn:active {
+ transform: translateY(0);
+}
+
+.main-content {
+ display: flex;
+ flex: 1;
+ overflow: hidden;
+}
+
+.toolbar {
+ width: 250px;
+ background: rgba(255, 255, 255, 0.95);
+ backdrop-filter: blur(10px);
+ padding: 1.5rem;
+ border-right: 1px solid rgba(255, 255, 255, 0.2);
+ overflow-y: auto;
+}
+
+.toolbar h3,
+.toolbar h4 {
+ color: #4a5568;
+ margin-bottom: 1rem;
+}
+
+.object-palette {
+ margin-bottom: 2rem;
+}
+
+.palette-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 1rem;
+ margin-bottom: 1rem;
+ background: white;
+ border-radius: 12px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ border: 2px solid transparent;
+}
+
+.palette-item:hover {
+ transform: translateY(-3px);
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
+ border-color: #4299e1;
+}
+
+.palette-item span {
+ font-weight: 600;
+ color: #4a5568;
+ margin: 0.5rem 0;
+}
+
+.palette-item small {
+ color: #718096;
+ font-size: 0.8rem;
+ text-align: center;
+}
+
+.triangle-preview {
+ width: 0;
+ height: 0;
+ border-left: 15px solid transparent;
+ border-right: 15px solid transparent;
+ border-bottom: 25px solid #48bb78;
+}
+
+.square-preview {
+ width: 30px;
+ height: 30px;
+ background: #4299e1;
+ border-radius: 4px;
+}
+
+.connection-mode {
+ border-top: 1px solid rgba(0, 0, 0, 0.1);
+ padding-top: 1rem;
+}
+
+.connection-mode .btn {
+ width: 100%;
+ margin-bottom: 0.5rem;
+}
+
+.canvas-container {
+ flex: 1;
+ position: relative;
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(5px);
+ border-radius: 0 0 0 20px;
+ overflow: hidden;
+}
+
+#canvas {
+ width: 100%;
+ height: 100%;
+ background: white;
+ cursor: default;
+ border-radius: 0 0 0 20px;
+}
+
+.properties-panel {
+ width: 300px;
+ background: rgba(255, 255, 255, 0.95);
+ backdrop-filter: blur(10px);
+ padding: 1.5rem;
+ border-left: 1px solid rgba(255, 255, 255, 0.2);
+ overflow-y: auto;
+}
+
+.properties-panel h3 {
+ color: #4a5568;
+ margin-bottom: 1rem;
+}
+
+.no-selection {
+ color: #718096;
+ font-style: italic;
+ text-align: center;
+ padding: 2rem 0;
+}
+
+.property-group {
+ margin-bottom: 1.5rem;
+ padding: 1rem;
+ background: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.property-group h4 {
+ color: #4a5568;
+ margin-bottom: 0.5rem;
+ font-size: 0.9rem;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+}
+
+.property-group input,
+.property-group textarea,
+.property-group select {
+ width: 100%;
+ padding: 0.5rem;
+ border: 1px solid #e2e8f0;
+ border-radius: 4px;
+ font-size: 0.9rem;
+ margin-bottom: 0.5rem;
+}
+
+.property-group textarea {
+ resize: vertical;
+ min-height: 60px;
+}
+
+.tooltip {
+ position: absolute;
+ background: rgba(0, 0, 0, 0.8);
+ color: white;
+ padding: 0.5rem;
+ border-radius: 4px;
+ font-size: 0.8rem;
+ pointer-events: none;
+ z-index: 1000;
+ display: none;
+}
+
+.mode-connecting #canvas {
+ cursor: crosshair;
+}
+
+.mode-selecting #canvas {
+ cursor: default;
+}
+
+/* Animation classes */
+.shake {
+ animation: shake 0.5s ease-in-out;
+}
+
+@keyframes shake {
+ 0%,
+ 100% {
+ transform: translateX(0);
+ }
+ 25% {
+ transform: translateX(-5px);
+ }
+ 75% {
+ transform: translateX(5px);
+ }
+}
+
+.pulse {
+ animation: pulse 1s infinite;
+}
+
+@keyframes pulse {
+ 0% {
+ transform: scale(1);
+ }
+ 50% {
+ transform: scale(1.05);
+ }
+ 100% {
+ transform: scale(1);
+ }
+}
+
+/* Connection label styling */
+.connection-label {
+ background: rgba(255, 255, 255, 0.9);
+ border: 1px solid #4a5568;
+ border-radius: 4px;
+ padding: 2px 6px;
+ font-size: 11px;
+ color: #4a5568;
+ pointer-events: none;
+}
+
+.property-group label {
+ display: block;
+ margin-bottom: 0.25rem;
+ font-weight: 500;
+ color: #4a5568;
+ font-size: 0.85rem;
+}
+
+/* Responsive design */
+@media (max-width: 1024px) {
+ .main-content {
+ flex-direction: column;
+ }
+
+ .toolbar,
+ .properties-panel {
+ width: 100%;
+ max-height: 200px;
+ }
+
+ .canvas-container {
+ border-radius: 0;
+ }
+
+ #canvas {
+ border-radius: 0;
+ }
+}