Add particle animation for connection activity
This commit is contained in:
parent
083a25f302
commit
585404c549
180
connections.js
180
connections.js
@ -48,6 +48,21 @@ export const connectionStyles = css`
|
|||||||
/* Ensure buttons are always on top of nodes */
|
/* Ensure buttons are always on top of nodes */
|
||||||
z-index: 1000;
|
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) {
|
export const connections = function(superClass) {
|
||||||
@ -59,6 +74,15 @@ export const connections = function(superClass) {
|
|||||||
this.mousePosition = { x: 0, y: 0 };
|
this.mousePosition = { x: 0, y: 0 };
|
||||||
this._updatePreviewConnection = this._updatePreviewConnection.bind(this)
|
this._updatePreviewConnection = this._updatePreviewConnection.bind(this)
|
||||||
this._conDocClick = this._conDocClick.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() {
|
firstUpdated() {
|
||||||
@ -75,6 +99,9 @@ export const connections = function(superClass) {
|
|||||||
this.removeEventListener('port-click', this._handlePortClick);
|
this.removeEventListener('port-click', this._handlePortClick);
|
||||||
document.removeEventListener('mousemove', this._updatePreviewConnection);
|
document.removeEventListener('mousemove', this._updatePreviewConnection);
|
||||||
document.removeEventListener('click', this._conDocClick);
|
document.removeEventListener('click', this._conDocClick);
|
||||||
|
|
||||||
|
this._activeParticles.forEach(particle => particle.remove());
|
||||||
|
this._activeParticles.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
_conDocClick(e) {
|
_conDocClick(e) {
|
||||||
@ -336,6 +363,7 @@ export const connections = function(superClass) {
|
|||||||
this.draggingConnection = {
|
this.draggingConnection = {
|
||||||
sourceNodeId: nodeId,
|
sourceNodeId: nodeId,
|
||||||
sourcePortId: portId,
|
sourcePortId: portId,
|
||||||
|
sourcePortName: portName,
|
||||||
sourcePortType: portType,
|
sourcePortType: portType,
|
||||||
sourceNode: node
|
sourceNode: node
|
||||||
};
|
};
|
||||||
@ -364,8 +392,10 @@ export const connections = function(superClass) {
|
|||||||
id: `conn_${Date.now()}`,
|
id: `conn_${Date.now()}`,
|
||||||
sourceNodeId: this.draggingConnection.sourceNodeId,
|
sourceNodeId: this.draggingConnection.sourceNodeId,
|
||||||
sourcePortId: this.draggingConnection.sourcePortId,
|
sourcePortId: this.draggingConnection.sourcePortId,
|
||||||
|
sourcePortName: this.draggingConnection.sourcePortName,
|
||||||
targetNodeId: nodeId,
|
targetNodeId: nodeId,
|
||||||
targetPortId: portId
|
targetPortId: portId,
|
||||||
|
targetPortName: portName
|
||||||
};
|
};
|
||||||
|
|
||||||
this.connections = [...this.connections, connection];
|
this.connections = [...this.connections, connection];
|
||||||
@ -412,5 +442,153 @@ export const connections = function(superClass) {
|
|||||||
height: maxY - minY + (padding * 2)
|
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 = `
|
||||||
|
<filter id="particleGlow" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
|
<feGaussianBlur stdDeviation="2" result="coloredBlur"/>
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="coloredBlur"/>
|
||||||
|
<feMergeNode in="SourceGraphic"/>
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
`;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@tp/tp-flow-nodes",
|
"name": "@tp/tp-flow-nodes",
|
||||||
"version": "1.1.0",
|
"version": "1.2.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "tp-flow-nodes.js",
|
"main": "tp-flow-nodes.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -98,21 +98,21 @@ export class TpFlowNodes extends zoom(panning(connections(LitElement))) {
|
|||||||
this.connections = [];
|
this.connections = [];
|
||||||
|
|
||||||
// Create all nodes first
|
// Create all nodes first
|
||||||
const nodes = flowData.nodes.map(nodeData => {
|
const nodes = flowData.nodes?.map(nodeData => {
|
||||||
const node = this.createNode(nodeData.type);
|
const node = this.createNode(nodeData.type);
|
||||||
node.importData(nodeData);
|
node.importData(nodeData);
|
||||||
return node;
|
return node;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set nodes and wait for render
|
// Set nodes and wait for render
|
||||||
this.nodes = nodes;
|
this.nodes = nodes || [];
|
||||||
await this.updateComplete;
|
await this.updateComplete;
|
||||||
|
|
||||||
// Wait for all nodes to be ready
|
// Wait for all nodes to be ready
|
||||||
await Promise.all(this.nodes.map(node => node.updateComplete));
|
await Promise.all(this.nodes.map(node => node.updateComplete));
|
||||||
|
|
||||||
// Only after nodes are ready, restore connections
|
// Only after nodes are ready, restore connections
|
||||||
this.connections = flowData.connections;
|
this.connections = flowData.connections || [];
|
||||||
|
|
||||||
// Restore canvas state
|
// Restore canvas state
|
||||||
if (flowData.canvas) {
|
if (flowData.canvas) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user