import * as go from "gojs";
import { getFillColor } from "./template-utils";
const $ = go.GraphObject.make;
export const circleAnimationAdornment = (state = "default") => {
const $ = go.GraphObject.make;
const color = getFillColor(state);
return $(
go.Adornment,
"Spot",
{
name: "ANIMATION_ADORNMENT",
layerName: "Tool",
},
$(go.Placeholder),
$(go.Shape, "Circle", {
name: "GLOW_EFFECT",
width: 110,
height: 110,
stroke: color,
strokeWidth: 6,
fill: "transparent",
opacity: 0.5,
}),
$(go.Shape, "Circle", {
name: "RIPPLE_EFFECT",
width: 100,
height: 100,
stroke: color,
strokeWidth: 3,
fill: "transparent",
opacity: 0.7,
})
);
};
import * as go from "gojs";
import { FROM_PORT, SELECTION_ADORNMENT_COLOR, TO_PORT } from "../constants";
import {
hideAddNodeAdornment,
showAddNodeAdornment,
zoomInNodeIcon,
zoomOutNodeIcon,
} from "./template-utils";
import "./shapes";
const $ = go.GraphObject.make;
export const circleNodeTemplate = $(
go.Node,
{
isShadowed: true,
shadowBlur: 12,
shadowOffset: new go.Point(0, 6),
shadowColor: "rgba(122,124, 141, 0.2)",
selectionAdornmentTemplate: $(
go.Adornment,
"Spot",
{
name: "SELECTIONADORNMENTTEMPLATEGO",
zOrder: 99, // zOrder should be less than zOrder of AddNodeAdornment
mouseEnter: (e, adornment) => {
zoomInNodeIcon(adornment.adornedObject.part);
showAddNodeAdornment(adornment.adornedObject.part);
},
mouseLeave: (e, adornment, nextObj) => {
zoomOutNodeIcon(adornment.adornedObject.part);
hideAddNodeAdornment(adornment.adornedObject.part, nextObj);
},
},
$(go.Placeholder),
$(go.Shape, "Circle", {
strokeWidth: 5,
stroke: SELECTION_ADORNMENT_COLOR,
width: 95,
height: 95,
fill: "transparent",
})
),
selectionObjectName: "SELECTIONADORNMENTGO",
},
$(
go.Panel,
"Spot",
$(
go.Panel,
"Spot",
{
mouseEnter: (e, node) => {
zoomInNodeIcon(node);
showAddNodeAdornment(node);
},
mouseLeave: (e, node, nextObj) => {
zoomOutNodeIcon(node);
hideAddNodeAdornment(node, nextObj);
},
},
$(go.Shape, "Circle", {
width: 100,
height: 100,
fill: "white",
stroke: "#E4E5E8",
name: "SELECTIONADORNMENTGO",
}),
$(
go.Panel,
"Spot",
{ isClipping: true },
$(go.Shape, "Circle", {
width: 75,
height: 75,
fill: "transparent",
stroke: "transparent",
}),
$(
go.Picture,
{ width: 45, height: 45, name: "NODEICON" },
new go.Binding("source", "_src")
)
),
$(go.Shape, "Circle", {
cursor: "pointer",
fill: "transparent",
stroke: "transparent",
width: 1,
height: 1,
portId: TO_PORT,
alignment: go.Spot.Left,
toSpot: go.Spot.Left,
toLinkable: true,
}),
$(go.Shape, "Circle", {
fill: "transparent",
stroke: "transparent",
width: 1,
height: 1,
portId: FROM_PORT,
alignment: go.Spot.Right,
fromSpot: go.Spot.Right,
fromLinkable: true,
name: "FROMPORTGO",
}),
$(go.Shape, "Circle", {
width: 100,
height: 100,
fill: "#263238",
opacity: 0.2,
name: "HOVEROVERLAY",
stroke: "transparent",
visible: false,
}),
$(go.Shape, "Circle", {
name: "CENTER_ANCHOR",
width: 5,
height: 5,
stroke: "red", // ❗ temporary to debug — red dot
fill: "red",
alignment: go.Spot.Center,
})
),
$(
go.Panel,
"Vertical",
{
alignment: new go.Spot(0.5, 1, 0, 32),
width: 195,
// define a tooltip for each node
toolTip: new go.Adornment(
"Auto", // that has several labels around it
{ background: "black" }
) // avoid hiding tooltip when mouse moves
.add(
// new go.Placeholder(), // placeholder will be a bit bigger than node
new go.TextBlock({ margin: 4, stroke: "white" }).bind(
"text",
"hoverDescription"
)
)
.bind("visible", "", (data) => data.hoverDescription?.length > 0), // end Adornment
},
$(
go.TextBlock,
{
font: "16px Inter",
height: 30,
spacingAbove: 10,
spacingBelow: 10,
overflow: go.TextOverflow.Ellipsis,
},
new go.Binding("text", "description"),
new go.Binding("visible", "", (data) => !!data.description)
),
$(
go.TextBlock,
{
font: "14px Inter",
height: 30,
overflow: go.TextOverflow.Ellipsis,
},
new go.Binding("text", "name"),
new go.Binding("font", "", (data) => {
return data.description ? "14px Inter" : "16px Inter";
}),
new go.Binding("spacingAbove", "", (data) => {
return data.description ? 0 : 10;
}),
new go.Binding("spacingBelow", "", (data) => {
return data.description ? 0 : 10;
})
),
$(
go.Panel,
"Spot",
{
name: "NODENUMBER",
},
$(go.Shape, "CapsuleH", {
fill: "#cfd8dc",
stroke: "transparent",
// strokeWidth: 1,
width: 30,
height: 25,
}),
$(
go.TextBlock,
{ stroke: "black" },
new go.Binding("text", "nodeNumber")
),
new go.Binding("visible", "", (data) => !!data.nodeNumber)
)
),
$(
go.Panel,
"Spot",
{
alignment: new go.Spot(0.8, 0.1),
name: "ERRORNODEGO",
isActionable: true,
cursor: "pointer",
},
$(
go.Shape,
"CapsuleH",
{
fill: "red",
stroke: "white",
strokeWidth: 3,
width: 30,
height: 25,
},
new go.Binding("fill", "", (data) => {
return data?.warnings?.length > 0 ? "#fb8c00" : "red";
})
),
$(
go.TextBlock,
{ stroke: "white" },
new go.Binding(
"text",
"",
(data) => (data?.errors?.length || 0) + (data.warnings?.length || 0)
)
),
$(go.Shape, "CapsuleH", {
fill: "transparent",
stroke: "transparent",
width: 30,
height: 25,
mouseEnter: (e, thisObj) => {
thisObj.fill = "rgba(38, 50, 56, 0.3)";
},
mouseLeave: (e, thisObj) => {
thisObj.fill = "transparent";
},
}),
new go.Binding("visible", "", (data) => {
return data?.errors?.length > 0 || data?.warnings?.length > 0;
})
)
),
new go.Binding("location", "location", go.Point.parse).makeTwoWay(
go.Point.stringify
)
);
/**
* Stop animation and restore stroke based on node's final state.
* @param {go.Node} node
*/
export const stopNodeAnimation = (node) => {
if (!node || !node.diagram) return;
const diagram = node.diagram;
// 🛑 Stop animation
if (node._runningAnimation) {
node._runningAnimation.stop();
node._runningAnimation = null;
}
// 🧼 Remove animation adornment
const adornment = node.findAdornment("ANIMATION_ADORNMENT");
if (adornment) diagram.remove(adornment);
// ✅ Apply stroke color to main shape based on stored state
const state = node._effectiveState;
const color = getFillColor(state);
const shape = node.findObject("SELECTIONADORNMENTGO");
if (shape && color) {
shape.stroke = color;
}
};
/**
* Animate node with ripple/glow, using a tool-layer adornment.
* @param {go.Node} node
* @param {string} state
* @param {boolean} isAborted
*/
export const animateNode = (node, state, isAborted = false) => {
if (!node || !node.diagram) return;
const diagram = node.diagram;
const effectiveState = isAborted ? "aborted" : state;
// const strokeColor = getFillColor(effectiveState);
const runOnce = ["end_node", "error", "aborted"].includes(effectiveState);
// Save the state on the node for later stroke restoration
node._effectiveState = effectiveState;
// 🧼 Remove existing animation adornment if any
const oldAdornment = node.findAdornment("ANIMATION_ADORNMENT");
if (oldAdornment) diagram.remove(oldAdornment);
// ✅ Create new animation adornment
const adornment = circleAnimationAdornment(effectiveState);
adornment.adornedObject = node;
diagram.add(adornment);
const anchor = node.findObject("CENTER_ANCHOR");
if (anchor) {
const center = anchor.getDocumentPoint(go.Spot.Center);
adornment.location = center; // 🎯 place (0,0) of adornment here
} else {
adornment.location = node.getDocumentPoint(go.Spot.Center);
}
// 🔁 Animate
const glow = adornment.findObject("GLOW_EFFECT");
const ripple = adornment.findObject("RIPPLE_EFFECT");
if (!glow || !ripple) return;
const animation = new go.Animation();
animation.duration = 1000;
animation.reversible = true;
animation.runCount = runOnce ? 1 : Number.POSITIVE_INFINITY;
animation.add(glow, "opacity", 0.5, 0.2);
animation.add(ripple, "width", 100, 200);
animation.add(ripple, "height", 100, 200);
animation.add(ripple, "opacity", 0.7, 0);
node._runningAnimation = animation;
if (runOnce) {
animation.finished = () => stopNodeAnimation(node);
}
diagram.commandHandler.scrollToPart(node);
animation.start();
};
hey Walter,
Above is the code for my circlNodeTemplate and circleNodeAdornment (which is shown when animate node is called)
However, I am facing an issue with placing the adornment over the circle (see screenshot)
can u please help to fix this issue?