examples: add math example (#1461)

* examples: add math example

* docs: add math example
This commit is contained in:
Braks
2024-06-08 20:35:18 +02:00
committed by GitHub
parent f74be96b2d
commit a8ba97331f
23 changed files with 748 additions and 11 deletions

View File

@@ -18,6 +18,10 @@ export const routes: RouterOptions['routes'] = [
path: '/snap-handle',
component: () => import('./src/SnapHandle/SnapHandleExample.vue'),
},
{
path: '/math',
component: () => import('./src/Math/MathExample.vue'),
},
{
path: '/node-resizer',
component: () => import('./src/NodeResizer/NodeResizerExample.vue'),

View File

@@ -0,0 +1,33 @@
<script lang="ts" setup>
defineProps<{
name: '+' | '-' | '*' | '/'
}>()
</script>
<template>
<svg v-if="name === '+'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="currentColor" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6z" />
</svg>
<svg v-else-if="name === '-'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="currentColor" d="M19 13H5v-2h14v2z" />
</svg>
<svg v-else-if="name === '*'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 12L6 6m6 6l6 6m-6-6l6-6m-6 6l-6 6"
/>
</svg>
<svg v-else-if="name === '/'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M19 13H5v-2h14zm-7-8a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0 10a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2"
/>
</svg>
</template>

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
import { VueFlow } from '@vue-flow/core'
import { Background } from '@vue-flow/background'
import { initialEdges, initialNodes } from './initialElements'
import ValueNode from './ValueNode.vue'
import OperatorNode from './OperatorNode.vue'
import ResultNode from './ResultNode.vue'
import './styles.css'
const nodes = ref(initialNodes)
const edges = ref(initialEdges)
</script>
<template>
<VueFlow class="math-flow" :nodes="nodes" :edges="edges" fit-view-on-init>
<template #node-value="props">
<ValueNode :id="props.id" :data="props.data" />
</template>
<template #node-operator="props">
<OperatorNode :id="props.id" :data="props.data" />
</template>
<template #node-result="props">
<ResultNode :id="props.id" />
</template>
<Background />
</VueFlow>
</template>

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
import type { NodeProps } from '@vue-flow/core'
import { Handle, Position, useVueFlow } from '@vue-flow/core'
import type { Operator, OperatorNodeData } from './types'
import Icon from './Icon.vue'
const props = defineProps<Pick<NodeProps<OperatorNodeData>, 'id' | 'data'>>()
const operators: Operator[] = ['+', '-', '*', '/']
const { updateNodeData } = useVueFlow()
</script>
<template>
<label :for="`${id}-operator`">Operator</label>
<div class="buttons nodrag">
<button
v-for="operator of operators"
:key="`${id}-${operator}-operator`"
:class="{ selected: data.operator === operator }"
@click="updateNodeData(props.id, { operator })"
>
<Icon :name="operator" />
</button>
</div>
<Handle type="source" :position="Position.Right" :connectable="false" />
<Handle id="target-a" type="target" :position="Position.Left" :connectable="false" />
<Handle id="target-b" type="target" :position="Position.Left" :connectable="false" />
</template>

View File

@@ -0,0 +1,76 @@
<script setup lang="ts">
import type { GraphNode } from '@vue-flow/core'
import { Handle, Position, useHandleConnections, useNodesData, useVueFlow } from '@vue-flow/core'
import type { OperatorNodeData, ValueNodeData } from './types'
import { mathFunctions } from './utils'
defineProps<{ id: string }>()
const { getConnectedEdges } = useVueFlow()
// Get the source connections of the result node. In this example it's only one operator node.
const sourceConnections = useHandleConnections({
// type target means all connections where *this* node is the target
// that means we go backwards in the graph to find the source of the connection(s)
type: 'target',
})
// Get the source connections of the operator node
const operatorSourceConnections = computed(() =>
getConnectedEdges(sourceConnections.value[0].source).filter((e) => e.source !== sourceConnections.value[0].source),
)
const operatorData = useNodesData<GraphNode<OperatorNodeData>>(() =>
sourceConnections.value.map((connection) => connection.source),
)
const valueData = useNodesData<GraphNode<ValueNodeData>>(() =>
operatorSourceConnections.value.map((connection) => connection.source),
)
const result = computed(() => {
const currResult = operatorData.value.reduce((acc, { data }) => {
const operator = data?.operator
if (operator) {
const [a, b] = valueData.value.map(({ data }) => data?.value)
if (a && b) {
return mathFunctions[operator](a, b)
}
}
return acc
}, 0)
// Round to 2 decimal places
return Math.round(currResult * 100) / 100
})
</script>
<template>
<div class="calculation">
<template v-for="(value, i) in valueData" :key="`${value.id}-${value.data}`">
<span>
{{ value.data?.value }}
</span>
<span v-if="i !== valueData.length - 1">
{{ operatorData[0].data?.operator }}
</span>
</template>
</div>
<span> = </span>
<span class="counter" :style="{ color: result > 0 ? '#5EC697' : '#f15a16' }">
{{ result }}
</span>
<Handle
type="target"
:position="Position.Left"
:connectable="false"
:style="{ background: result > 0 ? '#5EC697' : '#f15a16' }"
/>
</template>

View File

@@ -0,0 +1,28 @@
<script setup lang="ts">
import type { NodeProps } from '@vue-flow/core'
import { Handle, Position, useVueFlow } from '@vue-flow/core'
import type { ValueNodeData } from './types'
const props = defineProps<Pick<NodeProps<ValueNodeData>, 'id' | 'data'>>()
const { updateNodeData } = useVueFlow()
function onChange(event: Event) {
const evt = event as InputEvent
const target = evt.target as HTMLInputElement
const value = Number.parseFloat(target.value)
if (!Number.isNaN(value)) {
updateNodeData(props.id, { value })
}
}
</script>
<template>
<label :for="`${id}-input`">Value</label>
<input :id="`${id}-input`" :value="data.value" type="number" class="nodrag" @change="onChange" />
<Handle type="source" :position="Position.Right" :connectable="false" />
</template>

View File

@@ -0,0 +1,34 @@
import type { Edge, Node } from '@vue-flow/core'
// these are some math nodes, inputs and outputs
export const initialNodes: Node[] = [
{
id: '1',
position: { x: 0, y: 0 },
type: 'value',
data: { value: 10 },
},
{
id: '2',
position: { x: 0, y: 100 },
type: 'value',
data: { value: 30 },
},
{
id: '3',
position: { x: 400, y: 35 },
type: 'operator',
data: { operator: '+' },
},
{
id: '4',
position: { x: 700, y: 40 },
type: 'result',
},
]
export const initialEdges: Edge[] = [
{ id: 'e1-3', source: '1', target: '3', animated: true, targetHandle: 'target-a' },
{ id: 'e2-3', source: '2', target: '3', animated: true, targetHandle: 'target-b' },
{ id: 'e3-4', source: '3', target: '4', animated: true },
]

View File

@@ -0,0 +1,123 @@
.math-flow {
background-color: #edf2f7;
height: 100%;
width: 100%;
}
.vue-flow__handle {
height: 24px;
width: 10px;
background: #aaa;
border-radius: 4px
}
.vue-flow__edges path {
stroke-width: 3;
}
.vue-flow__node-value {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background-color: #f3f4f6;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
}
.vue-flow__node-value.selected {
box-shadow: 0 0 0 2px #ec4899;
}
.vue-flow__node-value input {
flex: 1;
padding: 8px;
border: none;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.vue-flow__node-value input:focus {
outline: none;
}
.vue-flow__node-value .vue-flow__handle {
background-color: #ec4899;
}
.vue-flow__node-operator {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 8px 16px;
background-color: #f3f4f6;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
}
.vue-flow__node-operator.selected {
box-shadow: 0 0 0 2px #0EA5E9;
}
.vue-flow__node-operator .buttons {
display: flex;
gap: 8px;
}
.vue-flow__node-operator button {
border: none;
cursor: pointer;
background-color: #4a5568;
border-radius: 8px;
color: white;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
width: 40px;
height: 40px;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
}
.vue-flow__node-operator button:hover {
background-color: #0EA5E9;
transition: background-color 0.2s;
}
.vue-flow__node-operator button.selected {
background-color: #0EA5E9;
}
.vue-flow__node-operator .vue-flow__handle[data-handleid="target-a"] {
top: 25%;
}
.vue-flow__node-operator .vue-flow__handle[data-handleid="target-b"] {
top: 75%;
}
.vue-flow__node-operator .vue-flow__handle {
background-color: #0EA5E9;
}
.vue-flow__node-result {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 8px 16px;
background-color: #f3f4f6;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
}
.vue-flow__node-result.selected {
box-shadow: 0 0 0 2px #5EC697;
}
.vue-flow__node-result .calculation {
display: flex;
gap: 8px;
font-size: 24px;
}

View File

@@ -0,0 +1,9 @@
export interface ValueNodeData {
value: number
}
export type Operator = '+' | '-' | '*' | '/'
export interface OperatorNodeData {
operator: Operator
}

View File

@@ -0,0 +1,8 @@
import type { Operator } from './types'
export const mathFunctions: Record<Operator, (a: number, b: number) => number> = {
'+': (a: number, b: number) => a + b,
'-': (a: number, b: number) => a - b,
'*': (a: number, b: number) => a * b,
'/': (a: number, b: number) => a / b,
}