From 585404c549b913fe3e4ec65f3d3a1bb273186d89 Mon Sep 17 00:00:00 2001 From: pk Date: Fri, 7 Mar 2025 23:55:55 +0100 Subject: [PATCH] Add particle animation for connection activity --- connections.js | 180 ++++++++++++++++++++++++++++++++++++++++++++++- package.json | 2 +- tp-flow-nodes.js | 6 +- 3 files changed, 183 insertions(+), 5 deletions(-) diff --git a/connections.js b/connections.js index c10013e..a71a562 100644 --- a/connections.js +++ b/connections.js @@ -48,6 +48,21 @@ export const connectionStyles = css` /* Ensure buttons are always on top of nodes */ z-index: 1000; } + + .data-flow-particle { + fill: var(--data-flow-particle-color, #00c3ff); + filter: url(#particleGlow); + } + + @keyframes particlePulse { + 0% { opacity: 0.7; } + 50% { opacity: 1; } + 100% { opacity: 0.7; } + } + + .data-flow-particle { + animation: particlePulse 0.5s ease-in-out infinite; + } `; export const connections = function(superClass) { @@ -59,6 +74,15 @@ export const connections = function(superClass) { this.mousePosition = { x: 0, y: 0 }; this._updatePreviewConnection = this._updatePreviewConnection.bind(this) this._conDocClick = this._conDocClick.bind(this); + + // Particle animation + this.minParticleDelay = 100; + this.maxQueueLength = 5; // Maximum particles per connection queue + this.particleAnimationDuration = 500; + this._particleQueue = new Map(); // Queue for each connection + this._particleTimers = new Map(); // Last particle time for each connection + this._activeParticles = new Set(); + this._filterCreated = false; } firstUpdated() { @@ -75,6 +99,9 @@ export const connections = function(superClass) { this.removeEventListener('port-click', this._handlePortClick); document.removeEventListener('mousemove', this._updatePreviewConnection); document.removeEventListener('click', this._conDocClick); + + this._activeParticles.forEach(particle => particle.remove()); + this._activeParticles.clear(); } _conDocClick(e) { @@ -336,6 +363,7 @@ export const connections = function(superClass) { this.draggingConnection = { sourceNodeId: nodeId, sourcePortId: portId, + sourcePortName: portName, sourcePortType: portType, sourceNode: node }; @@ -364,8 +392,10 @@ export const connections = function(superClass) { id: `conn_${Date.now()}`, sourceNodeId: this.draggingConnection.sourceNodeId, sourcePortId: this.draggingConnection.sourcePortId, + sourcePortName: this.draggingConnection.sourcePortName, targetNodeId: nodeId, - targetPortId: portId + targetPortId: portId, + targetPortName: portName }; this.connections = [...this.connections, connection]; @@ -412,5 +442,153 @@ export const connections = function(superClass) { height: maxY - minY + (padding * 2) }; } + + async animateDataFlow(sourceNodeId, targetNodeId, sourcePortName, targetPortName) { + const connection = this.connections.find(conn => + conn.sourceNodeId === sourceNodeId && + conn.targetNodeId === targetNodeId && + conn.sourcePortName === sourcePortName && + conn.targetPortName === targetPortName + ); + + if (!connection) return; + + const pathElement = this.shadowRoot.querySelector(`path#${connection.id}`); + if (!pathElement) return; + + // Create shared filter if it doesn't exist + if (!this._filterCreated) { + const svgElement = this.shadowRoot.querySelector('svg.connections'); + const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs"); + defs.innerHTML = ` + + + + + + + + `; + svgElement.insertBefore(defs, svgElement.firstChild); + this._filterCreated = true; + } + + // Create particle group + const animationGroup = document.createElementNS("http://www.w3.org/2000/svg", "g"); + const particle = document.createElementNS("http://www.w3.org/2000/svg", "circle"); + + particle.setAttribute("r", "4"); + particle.classList.add("data-flow-particle"); + + animationGroup.appendChild(particle); + pathElement.parentNode.appendChild(animationGroup); + + // Track this particle + this._activeParticles.add(animationGroup); + + // Get the total length of the path + const pathLength = pathElement.getTotalLength(); + + return new Promise((resolve) => { + let startTime = null; + const duration = this.particleAnimationDuration; + + const animate = (timestamp) => { + if (!startTime) startTime = timestamp; + const progress = (timestamp - startTime) / duration; + + if (progress <= 1) { + // Get point at current position along the path + const point = pathElement.getPointAtLength(progress * pathLength); + + // Update particle position + particle.setAttribute("cx", point.x); + particle.setAttribute("cy", point.y); + + requestAnimationFrame(animate); + } else { + // Animation complete + this._activeParticles.delete(animationGroup); + animationGroup.remove(); + resolve(); + } + }; + + requestAnimationFrame(animate); + }); + } + + // Add method to animate multiple particles + async animateDataFlowBurst(sourceNodeId, targetNodeId, sourcePortName, targetPortName, count = 3, delay = 200) { + const animations = []; + for (let i = 0; i < count; i++) { + animations.push( + new Promise(resolve => { + setTimeout(() => { + this.animateDataFlow(sourceNodeId, targetNodeId, sourcePortName, targetPortName) + .then(resolve); + }, i * delay); + }) + ); + } + return Promise.all(animations); + } + + // Method to get a unique connection identifier + _getConnectionKey(sourceNodeId, targetNodeId, sourcePortName, targetPortName) { + return `${sourceNodeId}-${sourcePortName}-${targetNodeId}-${targetPortName}`; + } + + async queueParticle(sourceNodeId, targetNodeId, sourcePortName, targetPortName) { + const connectionKey = this._getConnectionKey(sourceNodeId, targetNodeId, sourcePortName, targetPortName); + + // Create queue for this connection if it doesn't exist + if (!this._particleQueue.has(connectionKey)) { + this._particleQueue.set(connectionKey, []); + } + + const queue = this._particleQueue.get(connectionKey); + + if (queue.length >= this.maxQueueLength) { + // Ignore new particles when queue is full + return Promise.resolve(); + } + + // Add to queue and process + return new Promise(resolve => { + queue.push(resolve); + this._processParticleQueue(connectionKey, sourceNodeId, targetNodeId, sourcePortName, targetPortName); + }); + } + + async _processParticleQueue(connectionKey, sourceNodeId, targetNodeId, sourcePortName, targetPortName) { + const queue = this._particleQueue.get(connectionKey); + if (!queue || queue.length === 0) return; + + const now = Date.now(); + const lastTime = this._particleTimers.get(connectionKey) || 0; + const timeSinceLastParticle = now - lastTime; + + if (timeSinceLastParticle >= this.minParticleDelay) { + // Process next particle + const resolve = queue.shift(); + this._particleTimers.set(connectionKey, now); + + const result = await this.animateDataFlow(sourceNodeId, targetNodeId, sourcePortName, targetPortName); + resolve(result); + + // Process next in queue if any + if (queue.length > 0) { + setTimeout(() => { + this._processParticleQueue(connectionKey, sourceNodeId, targetNodeId, sourcePortName, targetPortName); + }, this.minParticleDelay); + } + } else { + // Wait for minimum delay + setTimeout(() => { + this._processParticleQueue(connectionKey, sourceNodeId, targetNodeId, sourcePortName, targetPortName); + }, this.minParticleDelay - timeSinceLastParticle); + } + } }; } \ No newline at end of file diff --git a/package.json b/package.json index 1840926..4424693 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tp/tp-flow-nodes", - "version": "1.1.0", + "version": "1.2.0", "description": "", "main": "tp-flow-nodes.js", "scripts": { diff --git a/tp-flow-nodes.js b/tp-flow-nodes.js index 36d0297..aa69b07 100644 --- a/tp-flow-nodes.js +++ b/tp-flow-nodes.js @@ -98,21 +98,21 @@ export class TpFlowNodes extends zoom(panning(connections(LitElement))) { this.connections = []; // Create all nodes first - const nodes = flowData.nodes.map(nodeData => { + const nodes = flowData.nodes?.map(nodeData => { const node = this.createNode(nodeData.type); node.importData(nodeData); return node; }); // Set nodes and wait for render - this.nodes = nodes; + this.nodes = nodes || []; await this.updateComplete; // Wait for all nodes to be ready await Promise.all(this.nodes.map(node => node.updateComplete)); // Only after nodes are ready, restore connections - this.connections = flowData.connections; + this.connections = flowData.connections || []; // Restore canvas state if (flowData.canvas) {