feat: improve zoom performance by adding a tiny rand offset to nodes

This commit is contained in:
max_richter 2024-04-18 14:10:08 +02:00
parent 06ba3a8fe9
commit c33e2642e1
7 changed files with 178 additions and 168 deletions

View File

@ -119,6 +119,7 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any }>
const nodeType = this.nodeRegistry.getNode(node.type); const nodeType = this.nodeRegistry.getNode(node.type);
if (nodeType) { if (nodeType) {
node.tmp = { node.tmp = {
random: (Math.random() - 0.5) * 2,
type: nodeType type: nodeType
}; };
} }
@ -173,6 +174,7 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any }>
return; return;
} }
node.tmp = node.tmp || {}; node.tmp = node.tmp || {};
node.tmp.random = (Math.random() - 0.5) * 2;
node.tmp.type = nodeType; node.tmp.type = nodeType;
} }

View File

@ -1,78 +1,80 @@
<script lang="ts"> <script lang="ts">
import type { Edge as EdgeType, Node as NodeType } from '@nodes/types'; import type { Edge as EdgeType, Node as NodeType } from "@nodes/types";
import { HTML } from '@threlte/extras'; import { HTML } from "@threlte/extras";
import Edge from '../edges/Edge.svelte'; import Edge from "../edges/Edge.svelte";
import Node from '../node/Node.svelte'; import Node from "../node/Node.svelte";
import { getContext, onMount } from 'svelte'; import { getContext, onMount } from "svelte";
import type { Writable } from 'svelte/store'; import type { Writable } from "svelte/store";
import { activeSocket } from './stores.js'; import { activeSocket } from "./stores.js";
export let nodes: Writable<Map<number, NodeType>>; export let nodes: Writable<Map<number, NodeType>>;
export let edges: Writable<EdgeType[]>; export let edges: Writable<EdgeType[]>;
export let cameraPosition = [0, 0, 4]; export let cameraPosition = [0, 0, 4];
const isNodeInView = getContext<(n: NodeType) => boolean>('isNodeInView'); const isNodeInView = getContext<(n: NodeType) => boolean>("isNodeInView");
const getSocketPosition = const getSocketPosition =
getContext<(node: NodeType, index: string | number) => [number, number]>('getSocketPosition'); getContext<(node: NodeType, index: string | number) => [number, number]>(
"getSocketPosition",
);
function getEdgePosition(edge: EdgeType) { function getEdgePosition(edge: EdgeType) {
const pos1 = getSocketPosition(edge[0], edge[1]); const pos1 = getSocketPosition(edge[0], edge[1]);
const pos2 = getSocketPosition(edge[2], edge[3]); const pos2 = getSocketPosition(edge[2], edge[3]);
return [pos1[0], pos1[1], pos2[0], pos2[1]]; return [pos1[0], pos1[1], pos2[0], pos2[1]];
} }
onMount(() => { onMount(() => {
for (const node of $nodes.values()) { for (const node of $nodes.values()) {
if (node?.tmp?.ref) { if (node?.tmp?.ref) {
node.tmp.ref.style.setProperty('--nx', `${node.position[0] * 10}px`); node.tmp.ref.style.setProperty("--nx", `${node.position[0] * 10}px`);
node.tmp.ref.style.setProperty('--ny', `${node.position[1] * 10}px`); node.tmp.ref.style.setProperty("--ny", `${node.position[1] * 10}px`);
} }
} }
}); });
</script> </script>
{#each $edges as edge (`${edge[0].id}-${edge[1]}-${edge[2].id}-${edge[3]}`)} {#each $edges as edge (`${edge[0].id}-${edge[1]}-${edge[2].id}-${edge[3]}`)}
{@const pos = getEdgePosition(edge)} {@const pos = getEdgePosition(edge)}
{@const [x1, y1, x2, y2] = pos} {@const [x1, y1, x2, y2] = pos}
<Edge <Edge
from={{ from={{
x: x1, x: x1,
y: y1 y: y1,
}} }}
to={{ to={{
x: x2, x: x2,
y: y2 y: y2,
}} }}
/> />
{/each} {/each}
<HTML transform={false}> <HTML transform={false}>
<div <div
role="tree" role="tree"
id="graph" id="graph"
tabindex="0" tabindex="0"
class="wrapper" class="wrapper"
class:zoom-small={cameraPosition[2] < 2} style:transform={`scale(${cameraPosition[2] * 0.1})`}
class:hovering-sockets={activeSocket} class:hovering-sockets={activeSocket}
style={`--cz: ${cameraPosition[2]}; --node-display: ${cameraPosition[2] < 2 ? 'none' : 'block'};`} >
> {#each $nodes.values() as node (node.id)}
{#each $nodes.values() as node (node.id)} <Node
<Node {node} inView={cameraPosition && isNodeInView(node)} z={cameraPosition[2]} /> {node}
{/each} inView={cameraPosition && isNodeInView(node)}
</div> z={cameraPosition[2]}
/>
{/each}
</div>
</HTML> </HTML>
<style> <style>
.wrapper { .wrapper {
position: absolute; position: absolute;
z-index: 100; z-index: 100;
width: 0px; width: 0px;
height: 0px; height: 0px;
transform: scale(calc(var(--cz) * 0.1)); }
display: var(--node-display, block);
opacity: calc((var(--cz) - 2.5) / 3.5);
}
</style> </style>

View File

@ -1,129 +1,138 @@
<script lang="ts"> <script lang="ts">
import type { Node } from '@nodes/types'; import type { Node } from "@nodes/types";
import { getContext, onMount } from 'svelte'; import { getContext, onMount } from "svelte";
import NodeHeader from './NodeHeader.svelte'; import NodeHeader from "./NodeHeader.svelte";
import NodeParameter from './NodeParameter.svelte'; import NodeParameter from "./NodeParameter.svelte";
import { activeNodeId, selectedNodes } from '../graph/stores.js'; import { activeNodeId, selectedNodes } from "../graph/stores.js";
import { T } from '@threlte/core'; import { T } from "@threlte/core";
import { Color, type Mesh } from 'three'; import { Color, type Mesh } from "three";
import NodeFrag from './Node.frag'; import NodeFrag from "./Node.frag";
import NodeVert from './Node.vert'; import NodeVert from "./Node.vert";
export let node: Node; export let node: Node;
export let inView = true; export let inView = true;
export let z = 2; export let z = 2;
$: isActive = $activeNodeId === node.id; $: isActive = $activeNodeId === node.id;
$: isSelected = !!$selectedNodes?.has(node.id); $: isSelected = !!$selectedNodes?.has(node.id);
const updateNodePosition = getContext<(n: Node) => void>('updateNodePosition'); const updateNodePosition =
getContext<(n: Node) => void>("updateNodePosition");
const getNodeHeight = getContext<(n: string) => number>('getNodeHeight'); const getNodeHeight = getContext<(n: string) => number>("getNodeHeight");
const type = node?.tmp?.type; const type = node?.tmp?.type;
const parameters = Object.entries(type?.inputs || {}) const zOffset = (node.tmp?.random || 0) * 0.5;
.filter((p) => p[1].type !== 'seed') const zLimit = 2 - zOffset;
.filter((p) => !('setting' in p[1])); $: visible = inView && z > zLimit;
let ref: HTMLDivElement; const parameters = Object.entries(type?.inputs || {})
let meshRef: Mesh; .filter((p) => p[1].type !== "seed")
.filter((p) => !("setting" in p[1]));
const height = getNodeHeight(node.type); let ref: HTMLDivElement;
let meshRef: Mesh;
$: if (node && ref && meshRef) { const height = getNodeHeight(node.type);
node.tmp = node.tmp || {};
node.tmp.ref = ref;
node.tmp.mesh = meshRef;
updateNodePosition(node);
}
onMount(() => { $: if (node && ref && meshRef) {
node.tmp = node.tmp || {}; node.tmp = node.tmp || {};
node.tmp.ref = ref; node.tmp.ref = ref;
node.tmp.mesh = meshRef; node.tmp.mesh = meshRef;
updateNodePosition(node); updateNodePosition(node);
}); }
const colorDark = new Color(); onMount(() => {
colorDark.setStyle('#151515'); node.tmp = node.tmp || {};
//colorDark.(); node.tmp.ref = ref;
node.tmp.mesh = meshRef;
updateNodePosition(node);
});
const colorBright = new Color('#202020'); const colorDark = new Color();
//colorBright.convertLinearToSRGB(); colorDark.setStyle("#151515");
//colorDark.();
const colorBright = new Color("#202020");
//colorBright.convertLinearToSRGB();
</script> </script>
<T.Mesh <T.Mesh
position.x={node.position[0] + 10} position.x={node.position[0] + 10}
position.z={node.position[1] + height / 2} position.z={node.position[1] + height / 2}
position.y={0.8} position.y={0.8}
rotation.x={-Math.PI / 2} rotation.x={-Math.PI / 2}
bind:ref={meshRef} bind:ref={meshRef}
visible={z < 7} visible={inView && z < 7}
> >
<T.PlaneGeometry args={[20, height]} radius={1} /> <T.PlaneGeometry args={[20, height]} radius={1} />
<T.ShaderMaterial <T.ShaderMaterial
vertexShader={NodeVert} vertexShader={NodeVert}
fragmentShader={NodeFrag} fragmentShader={NodeFrag}
transparent transparent
uniforms={{ uniforms={{
uColorBright: { value: colorBright }, uColorBright: { value: colorBright },
uColorDark: { value: colorDark }, uColorDark: { value: colorDark },
uSelectedColor: { value: new Color('#9d5f28') }, uSelectedColor: { value: new Color("#9d5f28") },
uActiveColor: { value: new Color('white') }, uActiveColor: { value: new Color("white") },
uSelected: { value: false }, uSelected: { value: false },
uActive: { value: false }, uActive: { value: false },
uStrokeWidth: { value: 1.0 }, uStrokeWidth: { value: 1.0 },
uWidth: { value: 20 }, uWidth: { value: 20 },
uHeight: { value: height } uHeight: { value: height },
}} }}
uniforms.uSelected.value={isSelected} uniforms.uSelected.value={isSelected}
uniforms.uActive.value={isActive} uniforms.uActive.value={isActive}
uniforms.uStrokeWidth.value={(7 - z) / 3} uniforms.uStrokeWidth.value={(7 - z) / 3}
/> />
</T.Mesh> </T.Mesh>
<div <div
class="node" class="node"
class:active={isActive} class:active={isActive}
class:selected={isSelected} style:--cz={z + zOffset}
class:out-of-view={!inView} style:display={inView && z > zLimit ? "block" : "none"}
data-node-id={node.id} class:selected={isSelected}
bind:this={ref} class:out-of-view={!inView}
data-node-id={node.id}
bind:this={ref}
> >
<NodeHeader {node} /> <NodeHeader {node} />
{#each parameters as [key, value], i} {#each parameters as [key, value], i}
<NodeParameter bind:node id={key} input={value} isLast={i == parameters.length - 1} /> <NodeParameter
{/each} bind:node
id={key}
input={value}
isLast={i == parameters.length - 1}
/>
{/each}
</div> </div>
<style> <style>
.node { .node {
position: absolute; position: absolute;
box-sizing: border-box; box-sizing: border-box;
user-select: none !important; user-select: none !important;
cursor: pointer; cursor: pointer;
width: 200px; width: 200px;
color: var(--text-color); color: var(--text-color);
transform: translate3d(var(--nx), var(--ny), 0); transform: translate3d(var(--nx), var(--ny), 0);
z-index: 1; z-index: 1;
font-weight: 300; opacity: calc((var(--cz) - 2.5) / 3.5);
--stroke: var(--background-color-lighter); font-weight: 300;
--stroke-width: 2px; --stroke: var(--background-color-lighter);
} --stroke-width: 2px;
}
.node.active { .node.active {
--stroke: white; --stroke: white;
--stroke-width: 1px; --stroke-width: 1px;
} }
.node.selected { .node.selected {
--stroke: #9d5f28; --stroke: #9d5f28;
--stroke-width: 1px; --stroke-width: 1px;
} }
.node.out-of-view {
display: none;
}
</style> </style>

View File

@ -156,10 +156,6 @@
box-sizing: border-box; box-sizing: border-box;
} }
:global(.zoom-small) .content {
display: none;
}
svg { svg {
position: absolute; position: absolute;
box-sizing: border-box; box-sizing: border-box;

View File

@ -183,7 +183,7 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
return value; return value;
}); });
console.log(transformed_inputs); // console.log(transformed_inputs);
const a2 = performance.now(); const a2 = performance.now();
@ -191,7 +191,7 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
const _inputs = concat_encoded(transformed_inputs); const _inputs = concat_encoded(transformed_inputs);
const a3 = performance.now(); const a3 = performance.now();
console.log(`executing ${node_type.id || node.id}`, _inputs); // console.log(`executing ${node_type.id || node.id}`, _inputs);
results[node.id] = node_type.execute(_inputs) as number; results[node.id] = node_type.execute(_inputs) as number;
const duration = performance.now() - a3; const duration = performance.now() - a3;
if (duration > 5) { if (duration > 5) {

View File

@ -75,7 +75,7 @@
header header
<button <button
on:click={() => { on:click={() => {
graph = templates.grid(10, 10); graph = templates.grid(15, 15);
}}>grid stress-test</button }}>grid stress-test</button
> >
</header> </header>

View File

@ -8,6 +8,7 @@ export type Node = {
tmp?: { tmp?: {
depth?: number; depth?: number;
mesh?: any; mesh?: any;
random?: number;
parents?: Node[], parents?: Node[],
children?: Node[], children?: Node[],
inputNodes?: Record<string, Node> inputNodes?: Record<string, Node>