Skip to content

Commit

Permalink
chore(docs): simplify animated layout example (#1747)
Browse files Browse the repository at this point in the history
* chore(examples): update ts example of layouting

Signed-off-by: braks <[email protected]>

* chore(docs): cleanup animated layout example

Signed-off-by: braks <[email protected]>

---------

Signed-off-by: braks <[email protected]>
  • Loading branch information
bcakmakoglu authored Jan 12, 2025
1 parent 25f9dd1 commit e94611a
Show file tree
Hide file tree
Showing 25 changed files with 1,062 additions and 687 deletions.
3 changes: 1 addition & 2 deletions docs/examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { IntersectionApp, IntersectionCSS } from './intersection'
import { SnapToHandleApp, SnappableConnectionLine } from './connection-radius'
import { NodeResizerApp, ResizableNode } from './node-resizer'
import { ToolbarApp, ToolbarNode } from './node-toolbar'
import { LayoutApp, LayoutEdge, LayoutElements, LayoutIcon, LayoutNode, useLayout, useRunProcess, useShuffle } from './layout'
import { LayoutApp, LayoutEdge, LayoutElements, LayoutIcon, LayoutNode, useLayout, useRunProcess } from './layout'
import { SimpleLayoutApp, SimpleLayoutElements, SimpleLayoutIcon, useSimpleLayout } from './layout-simple'
import { LoopbackApp, LoopbackCSS, LoopbackEdge } from './loopback'
import { MathApp, MathCSS, MathElements, MathIcon, MathOperatorNode, MathResultNode, MathValueNode } from './math'
Expand Down Expand Up @@ -136,7 +136,6 @@ export const exampleImports = {
'ProcessNode.vue': LayoutNode,
'AnimationEdge.vue': LayoutEdge,
'useRunProcess.js': useRunProcess,
'useShuffle.js': useShuffle,
'useLayout.js': useLayout,
'Icon.vue': LayoutIcon,
'additionalImports': {
Expand Down
143 changes: 86 additions & 57 deletions docs/examples/layout/AnimationEdge.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script setup>
import { computed, ref, toRef, watch } from 'vue'
import { computed, nextTick, ref, watch } from 'vue'
import { BaseEdge, EdgeLabelRenderer, Position, getSmoothStepPath, useNodesData, useVueFlow } from '@vue-flow/core'
import { ProcessStatus } from './useRunProcess'
const props = defineProps({
id: {
Expand Down Expand Up @@ -39,95 +40,124 @@ const props = defineProps({
type: String,
default: Position.Left,
},
data: {
type: Object,
required: false,
},
})
const { updateEdgeData } = useVueFlow()
const nodesData = useNodesData([props.target, props.source])
/**
* We call `useNodesData` to get the data of the source and target nodes, which
* contain the information about the status of each nodes' process.
*/
const nodesData = useNodesData(() => [props.target, props.source])
const labelRef = ref()
const edgeRef = ref()
/**
* We extract the source and target node data from the nodes data.
* We only need the first element of the array since we are only connecting two nodes.
*/
const targetNodeData = computed(() => nodesData.value[0].data)
const sourceNodeData = computed(() => nodesData.value[1].data)
const isFinished = toRef(() => sourceNodeData.value.isFinished)
const isCancelled = toRef(() => targetNodeData.value.isCancelled)
const isAnimating = ref(false)
const isAnimating = computed({
get: () => props.data.isAnimating || false,
set: (value) => {
updateEdgeData(props.id, { isAnimating: value })
},
})
let animation = null
const path = computed(() => getSmoothStepPath(props))
const edgeColor = computed(() => {
if (targetNodeData.value.hasError) {
return '#f87171'
}
if (targetNodeData.value.isFinished) {
return '#42B983'
}
if (targetNodeData.value.isCancelled || targetNodeData.value.isSkipped) {
return '#fbbf24'
}
if (targetNodeData.value.isRunning || isAnimating.value) {
return '#2563eb'
}
return '#6b7280'
})
watch(isCancelled, (isCancelled) => {
if (isCancelled) {
animation?.cancel()
switch (targetNodeData.value.status) {
case ProcessStatus.ERROR:
return '#f87171'
case ProcessStatus.FINISHED:
return '#42B983'
case ProcessStatus.CANCELLED:
case ProcessStatus.SKIPPED:
return '#fbbf24'
case ProcessStatus.RUNNING:
return '#2563eb'
default:
return '#6b7280'
}
})
watch(isAnimating, (isAnimating) => {
updateEdgeData(props.id, { isAnimating })
})
watch(isFinished, (isFinished) => {
if (isFinished) {
runAnimation()
}
})
// Cancel the animation if the target nodes' process was cancelled
watch(
() => targetNodeData.value.status === ProcessStatus.CANCELLED,
(isCancelled) => {
if (isCancelled) {
animation?.cancel()
}
},
)
// Run the animation when the source nodes' process is finished
watch(
() => sourceNodeData.value.status === ProcessStatus.FINISHED,
(isFinished) => {
if (isFinished) {
runAnimation()
}
},
)
function runAnimation() {
const pathEl = edgeRef.value?.pathEl
const labelEl = labelRef.value
if (!pathEl) {
if (!pathEl || !labelEl) {
console.warn('Path or label element not found')
return
}
const totalLength = pathEl.getTotalLength()
isAnimating.value = true
const keyframes = [{ offsetDistance: '0%' }, { offsetDistance: '100%' }]
// use path length as a possible measure for the animation duration
const pathLengthDuration = totalLength * 10
animation = labelRef.value.animate(keyframes, {
duration: Math.min(Math.max(pathLengthDuration, 1500), 3000), // clamp duration between 1.5s and 3s
direction: 'normal',
easing: 'ease-in-out',
iterations: 1,
// We need to wait for the next tick to ensure that the label element is rendered
nextTick(() => {
const keyframes = [{ offsetDistance: '0%' }, { offsetDistance: '100%' }]
// use path length as a possible measure for the animation duration
const pathLengthDuration = totalLength * 10
/**
* We animate the label element along the path of the edge using the `offsetDistance` property and
* the Web Animations API.
*
* The `animate` method returns an `Animation` object that we can use to listen to events like `finish` or `cancel`.
*
* The animation duration is calculated based on the total length of the path and clamped between 1.5s and 3s.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/Element/animate
*/
const labelAnimation = labelEl.animate(keyframes, {
duration: Math.min(Math.max(pathLengthDuration, 1500), 3000), // clamp duration between 1.5s and 3s
direction: 'normal',
easing: 'ease-in-out',
iterations: 1,
})
const handleAnimationEnd = () => {
isAnimating.value = false
}
labelAnimation.onfinish = handleAnimationEnd
labelAnimation.oncancel = handleAnimationEnd
animation = labelAnimation
})
animation.onfinish = handleAnimationEnd
animation.oncancel = handleAnimationEnd
}
function handleAnimationEnd() {
isAnimating.value = false
}
</script>
Expand All @@ -152,7 +182,6 @@ export default {
offsetRotate: '0deg',
offsetAnchor: 'center',
}"
class="animated-edge-label"
>
<span class="truck">
<span class="box">📦</span>
Expand Down
26 changes: 4 additions & 22 deletions docs/examples/layout/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import AnimationEdge from './AnimationEdge.vue'
import { initialEdges, initialNodes } from './initial-elements.js'
import { useRunProcess } from './useRunProcess'
import { useShuffle } from './useShuffle'
import { useLayout } from './useLayout'
const nodes = ref(initialNodes)
Expand All @@ -17,26 +16,12 @@ const edges = ref(initialEdges)
const cancelOnError = ref(true)
const shuffle = useShuffle()
const { graph, layout, previousDirection } = useLayout()
const { graph, layout } = useLayout()
const { run, stop, reset, isRunning } = useRunProcess({ graph, cancelOnError })
const { fitView } = useVueFlow()
async function shuffleGraph() {
await stop()
reset(nodes.value)
edges.value = shuffle(nodes.value)
nextTick(() => {
layoutGraph(previousDirection.value)
})
}
async function layoutGraph(direction) {
await stop()
Expand All @@ -53,8 +38,8 @@ async function layoutGraph(direction) {
<template>
<div class="layout-flow">
<VueFlow
:nodes="nodes"
:edges="edges"
v-model:nodes="nodes"
v-model:edges="edges"
:default-edge-options="{ type: 'animation', animated: true }"
@nodes-initialized="layoutGraph('LR')"
>
Expand All @@ -73,6 +58,7 @@ async function layoutGraph(direction) {
:targetY="edgeProps.targetY"
:source-position="edgeProps.sourcePosition"
:target-position="edgeProps.targetPosition"
:data="edgeProps.data"
/>
</template>

Expand All @@ -95,10 +81,6 @@ async function layoutGraph(direction) {
<button title="set vertical layout" @click="layoutGraph('TB')">
<Icon name="vertical" />
</button>

<button title="shuffle graph" @click="shuffleGraph">
<Icon name="shuffle" />
</button>
</div>

<div class="checkbox-panel">
Expand Down
7 changes: 0 additions & 7 deletions docs/examples/layout/Icon.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,4 @@ defineProps({
<path d="M7,7 L12,2 L17,7" stroke="currentColor" stroke-width="2" fill="none" />
<path d="M7,17 L12,22 L17,17" stroke="currentColor" stroke-width="2" fill="none" />
</svg>

<svg v-else-if="name === 'shuffle'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M14 20v-2h2.6l-3.175-3.175L14.85 13.4L18 16.55V14h2v6zm-8.6 0L4 18.6L16.6 6H14V4h6v6h-2V7.4zm3.775-9.425L4 5.4L5.4 4l5.175 5.175z"
/>
</svg>
</template>
72 changes: 35 additions & 37 deletions docs/examples/layout/ProcessNode.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script setup>
import { computed, toRef } from 'vue'
import { Handle, useNodeConnections } from '@vue-flow/core'
import { ProcessStatus } from './useRunProcess'
const props = defineProps({
data: {
Expand All @@ -23,64 +24,61 @@ const targetConnections = useNodeConnections({
handleType: 'source',
})
const isSender = toRef(() => sourceConnections.value.length <= 0)
const isStartNode = toRef(() => sourceConnections.value.length <= 0)
const isReceiver = toRef(() => targetConnections.value.length <= 0)
const isEndNode = toRef(() => targetConnections.value.length <= 0)
const status = toRef(() => props.data.status)
const bgColor = computed(() => {
if (isSender.value) {
if (isStartNode.value) {
return '#2563eb'
}
if (props.data.hasError) {
return '#f87171'
}
if (props.data.isFinished) {
return '#42B983'
switch (status.value) {
case ProcessStatus.ERROR:
return '#f87171'
case ProcessStatus.FINISHED:
return '#42B983'
case ProcessStatus.CANCELLED:
return '#fbbf24'
default:
return '#4b5563'
}
if (props.data.isCancelled) {
return '#fbbf24'
}
return '#4b5563'
})
const processLabel = computed(() => {
if (props.data.hasError) {
return ''
}
if (props.data.isSkipped) {
return '🚧'
}
if (props.data.isCancelled) {
return '🚫'
}
if (isSender.value) {
if (isStartNode.value) {
return '📦'
}
if (props.data.isFinished) {
return '😎'
switch (status.value) {
case ProcessStatus.ERROR:
return ''
case ProcessStatus.SKIPPED:
return '🚧'
case ProcessStatus.CANCELLED:
return '🚫'
case ProcessStatus.FINISHED:
return '😎'
default:
return '🏠'
}
return '🏠'
})
</script>

<template>
<div class="process-node" :style="{ backgroundColor: bgColor, boxShadow: data.isRunning ? '0 0 10px rgba(0, 0, 0, 0.5)' : '' }">
<Handle v-if="!isSender" type="target" :position="targetPosition">
<span v-if="!data.isRunning && !data.isFinished && !data.isCancelled && !data.isSkipped && !data.hasError">📥 </span>
<div
class="process-node"
:style="{ backgroundColor: bgColor, boxShadow: status === ProcessStatus.RUNNING ? '0 0 10px rgba(0, 0, 0, 0.5)' : '' }"
>
<Handle v-if="!isStartNode" type="target" :position="targetPosition">
<span v-if="status === null">📥 </span>
</Handle>

<Handle v-if="!isReceiver" type="source" :position="sourcePosition" />
<Handle v-if="!isEndNode" type="source" :position="sourcePosition" />

<div v-if="!isSender && data.isRunning" class="spinner" />
<div v-if="status === ProcessStatus.RUNNING" class="spinner" />
<span v-else>
{{ processLabel }}
</span>
Expand Down
Loading

0 comments on commit e94611a

Please sign in to comment.