Skip to content

Commit 4130d86

Browse files
authored
AP-25491: Introduce a WebGL-rendered AI Annotation Skeleton
This replaces the static HTML-based `SkeletonAnnotation` with a WebGL-based `AISkeletonAnnotation` rendered directly in canvas.
1 parent e54ad9e commit 4130d86

20 files changed

+1575
-73
lines changed

org.knime.ui.js/src/components/workflowEditor/WebGLKanvas/Workflow.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { useWorkflowStore } from "@/store/workflow/workflow";
1515
import type { ContainerInst } from "@/vue3-pixi";
1616
1717
import SelectionRectangle from "./SelectionRectangle/SelectionRectangle.vue";
18+
import AISkeletonAnnotation from "./ai/AISkeletonAnnotation.vue";
1819
import StaticWorkflowAnnotation from "./annotations/StaticWorkflowAnnotation.vue";
1920
import Connector from "./connectors/Connector.vue";
2021
import ConnectorLabel from "./connectors/ConnectorLabel.vue";
@@ -125,6 +126,8 @@ const annotations = computed(
125126
/>
126127
</Container>
127128

129+
<AISkeletonAnnotation />
130+
128131
<MetanodePortBars
129132
v-if="
130133
activeWorkflow!.info.containerType ===

org.knime.ui.js/src/components/workflowEditor/WebGLKanvas/WorkflowCanvas.vue

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@ import { useAddNodeViaFileUpload } from "@/components/nodeTemplates/useAddNodeVi
1717
import { isBrowser } from "@/environment";
1818
import { KANVAS_ID } from "@/lib/workflow-canvas";
1919
import { useAnalytics } from "@/services/analytics";
20-
import { useAiQuickActionsStore } from "@/store/ai/aiQuickActions";
21-
import { QuickActionId } from "@/store/ai/types";
2220
import { useCanvasStateTrackingStore } from "@/store/application/canvasStateTracking";
2321
import { useLifecycleStore } from "@/store/application/lifecycle";
2422
import { useWebGLCanvasStore } from "@/store/canvas/canvas-webgl";
@@ -31,7 +29,6 @@ import { useArrowKeyNavigation } from "../useArrowKeyNavigation";
3129
3230
import Workflow from "./Workflow.vue";
3331
import EditableWorkflowAnnotation from "./annotations/EditableWorkflowAnnotation.vue";
34-
import SkeletonAnnotation from "./annotations/SkeletonAnnotation.vue";
3532
import { usePointerDownDoubleClick } from "./common/usePointerDownDoubleClick";
3633
import FloatingCanvasTools from "./floatingToolbar/canvasTools/FloatingCanvasTools.vue";
3734
import Kanvas from "./kanvas/Kanvas.vue";
@@ -44,18 +41,11 @@ const { isLoadingWorkflow } = storeToRefs(useLifecycleStore());
4441
const { activeWorkflow, isWorkflowEmpty, isWritable } = storeToRefs(
4542
useWorkflowStore(),
4643
);
47-
const aiQuickActionsStore = useAiQuickActionsStore();
4844
const canvasStore = useWebGLCanvasStore();
4945
5046
const { containerSize, shouldHideMiniMap, interactionsEnabled } =
5147
storeToRefs(canvasStore);
5248
53-
const skeletonAnnotationData = computed(
54-
() =>
55-
aiQuickActionsStore.processingActions[QuickActionId.GenerateAnnotation] ??
56-
null,
57-
);
58-
5949
const { isPointerDownDoubleClick } = usePointerDownDoubleClick({
6050
eventHandledChecker: (event) => isMarkedEvent(event),
6151
});
@@ -264,11 +254,6 @@ const onCanvasDrop = (event: DragEvent) => {
264254

265255
<EditableWorkflowAnnotation />
266256

267-
<SkeletonAnnotation
268-
v-if="skeletonAnnotationData"
269-
:bounds="skeletonAnnotationData.bounds"
270-
/>
271-
272257
<NodeNameEditor />
273258

274259
<NodeLabelEditor />
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<script setup lang="ts">
2+
import { computed } from "vue";
3+
4+
import { useAiQuickActionsStore } from "@/store/ai/aiQuickActions";
5+
import { QuickActionId } from "@/store/ai/types";
6+
import * as $colors from "@/style/colors";
7+
import * as $shapes from "@/style/shapes";
8+
import type { GraphicsInst } from "@/vue3-pixi";
9+
import BorderWithRotatingGradient from "../common/BorderWithRotatingGradient.vue";
10+
import {
11+
type GlowConfig,
12+
type GradientStop,
13+
} from "../common/renderGradientBorder";
14+
15+
const aiQuickActionsStore = useAiQuickActionsStore();
16+
const bounds = computed(
17+
() =>
18+
aiQuickActionsStore.processingActions[QuickActionId.GenerateAnnotation]
19+
?.bounds ?? null,
20+
);
21+
22+
// Border with rotating gradient and teal glow
23+
const { Cyan, Yellow, Teal } = $colors.aiGradient;
24+
const gradient: GradientStop[] = [
25+
{ position: 0, color: Cyan },
26+
{ position: 0.25, color: Yellow },
27+
{ position: 0.47, color: Teal },
28+
{ position: 0.75, color: Yellow },
29+
{ position: 1, color: Cyan },
30+
];
31+
32+
const borderStrokeWidth = $shapes.annotationBorderWidth;
33+
const glowConfig: GlowConfig = {
34+
gradientStopIndex: 2, // teal
35+
softness: 60,
36+
spread: 0.7,
37+
};
38+
const secondsPerRotation = 1.2;
39+
const borderRadius = 0;
40+
41+
// Text skeletons. SkeletonItem from @knime/components doesn't have a WebGL equivalent,
42+
// so we simply draw these as rounded rectangles here directly.
43+
const skeletonColor = $colors.GrayUltraLight;
44+
45+
const padding = 10;
46+
const skeletonPadding = borderStrokeWidth + padding;
47+
const skeletonGap = 10;
48+
const skeletonBorderRadius = 2;
49+
50+
const skeletonHeadingHeight = 16;
51+
const skeletonBodyHeight = 13;
52+
53+
const skeletonHeadingRelativeWidth = 0.6;
54+
const skeletonBodyRelativeWidth = 0.9;
55+
56+
const renderSkeletonBars = (graphics: GraphicsInst) => {
57+
if (!bounds.value) {
58+
return;
59+
}
60+
61+
const totalWidth = bounds.value.width - 2 * skeletonPadding;
62+
63+
graphics.clear();
64+
65+
// Heading bar
66+
graphics.roundRect(
67+
skeletonPadding,
68+
skeletonPadding,
69+
totalWidth * skeletonHeadingRelativeWidth,
70+
skeletonHeadingHeight,
71+
skeletonBorderRadius,
72+
);
73+
graphics.fill(skeletonColor);
74+
75+
// Body bar
76+
graphics.roundRect(
77+
skeletonPadding,
78+
skeletonPadding + skeletonHeadingHeight + skeletonGap,
79+
totalWidth * skeletonBodyRelativeWidth,
80+
skeletonBodyHeight,
81+
skeletonBorderRadius,
82+
);
83+
graphics.fill(skeletonColor);
84+
};
85+
</script>
86+
87+
<template>
88+
<Container
89+
v-if="bounds"
90+
label="AISkeletonAnnotation"
91+
:x="bounds.x"
92+
:y="bounds.y"
93+
>
94+
<BorderWithRotatingGradient
95+
:width="bounds.width"
96+
:height="bounds.height"
97+
:stroke-width="borderStrokeWidth"
98+
:gradient="gradient"
99+
:glow-config="glowConfig"
100+
:seconds-per-rotation="secondsPerRotation"
101+
:border-radius="borderRadius"
102+
/>
103+
104+
<Graphics
105+
label="SkeletonBars"
106+
event-mode="none"
107+
@render="renderSkeletonBars"
108+
/>
109+
</Container>
110+
</template>

org.knime.ui.js/src/components/workflowEditor/WebGLKanvas/annotations/SkeletonAnnotation.vue

Lines changed: 0 additions & 57 deletions
This file was deleted.
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<!-- eslint-disable no-undefined -->
2+
<script setup lang="ts">
3+
import { useTemplateRef } from "vue";
4+
import { BlurFilter, type Graphics } from "pixi.js";
5+
6+
import type { GlowConfig, GradientStop } from "./renderGradientBorder";
7+
import { drawGlowCutout, renderGradientBorder } from "./renderGradientBorder";
8+
9+
type Props = {
10+
width: number;
11+
height: number;
12+
strokeWidth: number;
13+
gradient: GradientStop[];
14+
secondsPerRotation: number;
15+
borderRadius: number;
16+
glowConfig?: GlowConfig;
17+
};
18+
19+
const props = withDefaults(defineProps<Props>(), {
20+
glowConfig: undefined,
21+
});
22+
23+
const borderRef = useTemplateRef<Graphics>("borderRef");
24+
const glowDotsRef = useTemplateRef<Graphics>("glowDotsRef");
25+
26+
const glowBlur = props.glowConfig
27+
? new BlurFilter({
28+
strength: props.glowConfig.softness,
29+
quality: 4,
30+
})
31+
: null;
32+
33+
const renderGlowCutout = (g: Graphics) =>
34+
drawGlowCutout(
35+
g,
36+
props.strokeWidth,
37+
props.width,
38+
props.height,
39+
props.borderRadius,
40+
);
41+
42+
renderGradientBorder({
43+
config: {
44+
width: props.width,
45+
height: props.height,
46+
strokeWidth: props.strokeWidth,
47+
gradient: props.gradient,
48+
glowConfig: props.glowConfig,
49+
secondsPerRotation: props.secondsPerRotation,
50+
borderRadius: props.borderRadius,
51+
},
52+
refs: { borderRef, glowDotsRef },
53+
});
54+
</script>
55+
56+
<template>
57+
<Container label="BorderWithRotatingGradient" event-mode="none">
58+
<Graphics ref="borderRef" label="RotatingGradient" event-mode="none" />
59+
60+
<Container v-if="glowBlur" label="Glow">
61+
<Graphics
62+
ref="glowDotsRef"
63+
label="GlowDots"
64+
event-mode="none"
65+
:filters="[glowBlur]"
66+
/>
67+
<Graphics
68+
label="GlowCutout"
69+
event-mode="none"
70+
blend-mode="erase"
71+
@render="renderGlowCutout"
72+
/>
73+
</Container>
74+
</Container>
75+
</template>

0 commit comments

Comments
 (0)