summaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/nodes/Input.css103
-rw-r--r--src/components/nodes/Input.tsx97
-rw-r--r--src/components/nodes/NodeShell.css47
-rw-r--r--src/components/nodes/NodeShell.tsx46
-rw-r--r--src/components/nodes/Output.css20
-rw-r--r--src/components/nodes/Output.tsx19
-rw-r--r--src/components/nodes/Socket.css20
-rw-r--r--src/components/nodes/Socket.tsx41
-rw-r--r--src/components/nodes/index.ts3
9 files changed, 396 insertions, 0 deletions
diff --git a/src/components/nodes/Input.css b/src/components/nodes/Input.css
new file mode 100644
index 0000000..aa01558
--- /dev/null
+++ b/src/components/nodes/Input.css
@@ -0,0 +1,103 @@
+@scope (.__Input) {
+ :scope {
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ padding: 4px 0;
+
+ .__Output + & {
+ padding-top: 8px;
+ }
+
+ input {
+ background-color: var(--overlay);
+ border: none;
+ border-radius: 4px;
+ outline: none;
+ color: var(--text);
+ font-size: 1em;
+ text-align: right;
+ width: 100%;
+ max-width: 200px;
+ margin-right: 12px;
+ padding-top: 3px;
+ padding-bottom: 2px;
+
+ .linked & {
+ display: none;
+ }
+ }
+ }
+}
+
+@scope (.__InputNum) {
+ & + & {
+ padding-top: 0;
+ }
+
+ :scope {
+ --socket-color: var(--data-float);
+
+ span {
+ position: relative;
+ left: 8px;
+ width: 0;
+ white-space: nowrap;
+ padding-bottom: 2px;
+ }
+ }
+}
+
+@scope (.__InputVector) {
+ :scope {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 1px;
+ --socket-color: var(--data-vector);
+
+ & :first-child {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 2px;
+ margin-bottom: 4px;
+ .linked & {
+ margin-bottom: 0;
+ }
+ }
+
+ input {
+ width: 89%;
+ margin-left: 8px;
+ margin-right: 16px;
+
+ &:nth-child(2) { border-radius: 4px 4px 0 0 }
+ &:nth-child(3) { border-radius: 0 }
+ &:nth-child(4) { border-radius: 0 0 4px 4px }
+
+ .linked & {
+ display: none;
+ }
+ }
+ }
+}
+
+@scope (.__InputSelect) {
+ :scope {
+ --socket-color: var(--data-float);
+
+ select {
+ background-color: color-mix(in srgb, var(--base) 50%, var(--surface));
+ color: var(--text);
+ border: 1px solid color-mix(in srgb, var(--overlay) 50%, var(--surface));
+ border-radius: 4px;
+ width: 100%;
+ margin-right: 12px;
+ padding: 3px 1px;
+
+ .linked & {
+ display: none;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/components/nodes/Input.tsx b/src/components/nodes/Input.tsx
new file mode 100644
index 0000000..1160f3f
--- /dev/null
+++ b/src/components/nodes/Input.tsx
@@ -0,0 +1,97 @@
+import { InputSocket } from '../../dataflow.ts';
+import { InSocket } from './Socket.tsx';
+import './Input.css';
+
+interface InProps {
+ children: ComponentChildren;
+ linked?: boolean;
+ class: string;
+}
+
+const Input = ({ children, linked = false, ...props }: InProps) => (
+ <li class={'__Input ' + props.class + (linked ? 'linked' : '')}>{children}</li>
+);
+
+export interface InputProps<T> {
+ name: string;
+ label: string;
+ value: InputSocket<T>;
+}
+
+export const InputAny = ({ name, label }: Omit<InputProps<any>, 'value'>) => {
+ return (
+ <Input class="__InputAny">
+ <InSocket name={name} />
+ {label}
+ </Input>
+ );
+};
+
+export const InputArray = ({ name, label }: Omit<InputProps<any>, 'value'>) => {
+ return (
+ <Input class="__InputArray">
+ <InSocket name={name} />
+ {label}
+ </Input>
+ );
+};
+
+const InputNum = (parseFunc: (string) => number) => ({ name, label, value }: InputProps<number>) => {
+ const onInput = (event: InputEvent) => {
+ value.value = parseFunc((event.target as HTMLInputElement).value);
+ }
+ return (
+ <Input class={'__InputNum' + (value.link.value ? ' linked' : '')}>
+ <InSocket name={name} />
+ <span>{label}</span>
+ <input type="number" value={value.manual.value} onInput={onInput} />
+ </Input>
+ );
+};
+
+export const InputInteger = InputNum(parseInt);
+export const InputNumber = InputNum(parseFloat);
+
+export const InputVector = ({ name, label, value }: InputProps<[number, number, number]>) => {
+ const onInput = (i: 0 | 1 | 2) => (event: InputEvent) => {
+ const newValue: [number, number, number] = [...value.value];
+ newValue[i] = parseFloat((event.target as HTMLInputElement).value);
+ value.value = newValue;
+ };
+ return (
+ <Input class={'__InputVector' + (value.link.value ? ' linked' : '')}>
+ <div>
+ <InSocket name={name} />
+ {label}
+ </div>
+ <input type="number" value={value.value[0]} onInput={onInput(0)} />
+ <input type="number" value={value.value[1]} onInput={onInput(1)} />
+ <input type="number" value={value.value[2]} onInput={onInput(2)} />
+ </Input>
+ );
+};
+
+export interface InputSelectProps extends InputProps<string> {
+ options: string[] | Record<string, string[]>;
+}
+
+export const InputSelect = ({ name, label, value, options }: InputSelectProps) => {
+ const onChange = (event: InputEvent) => {
+ value.value = (event.target as HTMLSelectElement).value;
+ }
+ return (
+ <Input class={'__InputSelect' + (value.link.value ? ' linked' : '')}>
+ <InSocket name={name} />
+ <select aria-label={label} onChange={onChange}>
+ {Array.isArray(options)
+ ? options.map(opt => <option value={opt}>{opt}</option>)
+ : Object.entries(options).map(([label, group]) => (
+ <optgroup label={label}>
+ {group.map(opt => <option value={opt}>{opt}</option>)}
+ </optgroup>
+ ))
+ }
+ </select>
+ </Input>
+ );
+};
diff --git a/src/components/nodes/NodeShell.css b/src/components/nodes/NodeShell.css
new file mode 100644
index 0000000..5e4fb2a
--- /dev/null
+++ b/src/components/nodes/NodeShell.css
@@ -0,0 +1,47 @@
+@scope (.__NodeShell) {
+ :scope {
+ width: 0;
+ height: 0;
+ overflow: visible;
+
+ > details {
+ background-color: var(--surface);
+ min-width: 180px;
+ width: fit-content;
+ font-size: 0.9rem;
+ margin: 4px;
+ padding-bottom: 8px;
+ display: flex;
+ flex-direction: column;
+ border-radius: 6px;
+ user-select: none;
+ --shadow-color: 0deg 0% 0%;
+ box-shadow:
+ 0.1px 0.4px 0.6px hsl(var(--shadow-color) / 0.04),
+ 0.5px 3.3px 4.8px -0.2px hsl(var(--shadow-color) / 0.15),
+ 1.4px 10.2px 14.7px -0.5px hsl(var(--shadow-color) / 0.27);
+
+ &:focus-within {
+ outline: 1px white solid;
+ }
+
+ summary {
+ background-color: var(--primary);
+ padding: 3px;
+ padding-left: 6px;
+ margin-bottom: 4px;
+ border-radius: 6px 6px 0 0;
+ list-style-image: url('../../icons/chevron-down.svg');
+ }
+ &:not([open]) summary {
+ list-style-image: url('../../icons/chevron-right.svg');
+ }
+
+ ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ }
+ }
+ }
+}
diff --git a/src/components/nodes/NodeShell.tsx b/src/components/nodes/NodeShell.tsx
new file mode 100644
index 0000000..0de1a6d
--- /dev/null
+++ b/src/components/nodes/NodeShell.tsx
@@ -0,0 +1,46 @@
+import { ComponentChildren } from 'preact';
+import { batch, Signal } from '@preact/signals';
+import { NodeId } from '../../node.ts';
+import './NodeShell.css';
+
+export interface NodeShellProps {
+ children: ComponentChildren;
+ id: number;
+ name: string;
+ x: Signal<number>;
+ y: Signal<number>;
+}
+
+const NodeShell = ({ children, id, name, x, y }: NodeShellProps) => {
+ const onMouseDown = (event: MouseEvent) => {
+ event.stopPropagation();
+
+ const onMouseMove = (event: MouseEvent) => batch(() => {
+ x.value += event.movementX;
+ y.value += event.movementY;
+ });
+
+ const onMouseUp = () => {
+ window.removeEventListener('mousemove', onMouseMove);
+ window.removeEventListener('mouseup', onMouseUp);
+ };
+
+ window.addEventListener('mousemove', onMouseMove);
+ window.addEventListener('mouseup', onMouseUp);
+ };
+
+ return (
+ <foreignObject x={x} y={y} width="0" height="0" class="__NodeShell">
+ <details open tabindex={0} class="node" onMouseDown={onMouseDown}>
+ <summary><span onClick={e => e.stopPropagation()}>{name}</span></summary>
+ <ul>
+ <NodeId.Provider value={id}>
+ {children}
+ </NodeId.Provider>
+ </ul>
+ </details>
+ </foreignObject>
+ );
+};
+
+export default NodeShell; \ No newline at end of file
diff --git a/src/components/nodes/Output.css b/src/components/nodes/Output.css
new file mode 100644
index 0000000..2c752a1
--- /dev/null
+++ b/src/components/nodes/Output.css
@@ -0,0 +1,20 @@
+@scope (.__Output) {
+ :scope {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ padding: 4px 0;
+ }
+}
+
+@scope (.__OutputNumber) {
+ :scope {
+ --socket-color: var(--data-float);
+ }
+}
+
+@scope (.__OutputVector) {
+ :scope {
+ --socket-color: var(--data-vector);
+ }
+} \ No newline at end of file
diff --git a/src/components/nodes/Output.tsx b/src/components/nodes/Output.tsx
new file mode 100644
index 0000000..8cd9c98
--- /dev/null
+++ b/src/components/nodes/Output.tsx
@@ -0,0 +1,19 @@
+import { OutSocket } from './Socket.tsx';
+import './Output.css';
+
+export interface OutputProps {
+ name: string;
+ label: string;
+}
+
+const Output = (type: string, cls: string) => ({ name, label }: OutputProps) => {
+ return (
+ <li class={cls + ' __Output out ' + type}>
+ {label}
+ <OutSocket name={name} />
+ </li>
+ );
+};
+
+export const OutputNumber = Output('number', '__OutputNumber');
+export const OutputVector = Output('vector', '__OutputVector');
diff --git a/src/components/nodes/Socket.css b/src/components/nodes/Socket.css
new file mode 100644
index 0000000..5879b32
--- /dev/null
+++ b/src/components/nodes/Socket.css
@@ -0,0 +1,20 @@
+@scope (.__Socket) {
+ :scope {
+ position: relative;
+ circle {
+ fill: var(--socket-color, var(--data-float));
+ }
+ }
+}
+
+@scope (.__InSocket) {
+ :scope {
+ right: 4px;
+ }
+}
+
+@scope (.__OutSocket) {
+ :scope {
+ left: 4px;
+ }
+}
diff --git a/src/components/nodes/Socket.tsx b/src/components/nodes/Socket.tsx
new file mode 100644
index 0000000..a4f38e1
--- /dev/null
+++ b/src/components/nodes/Socket.tsx
@@ -0,0 +1,41 @@
+import { useContext } from 'preact/hooks';
+import { NodeId, SocketHandlers } from '../../node.ts';
+import './Socket.css';
+
+interface SocketProps {
+ name: string;
+ class?: string;
+ onMouseDown?: SocketHandler;
+ onMouseUp?: SocketHandler;
+}
+
+const Socket = ({ name, onMouseDown, onMouseUp, ...props }: SocketProps) => {
+ const nodeId = useContext(NodeId);
+ const wrap = (func?: SocketHandler) => (event: MouseEvent) => func && func(nodeId, name, event);
+ return (
+ <svg
+ width="10"
+ height="10"
+ viewBox="0 0 16 16"
+ class={'__Socket ' + props.class}
+ onMouseDown={wrap(onMouseDown)}
+ onMouseUp={wrap(onMouseUp)}
+ >
+ <circle cx="8" cy="8" r="7" stroke="black" stroke-width="1" />
+ </svg>
+ );
+};
+
+export const InSocket = ({ name }: { name: string }) => {
+ const handlers = useContext(SocketHandlers);
+ return (
+ <Socket name={name} class="__InSocket" onMouseDown={handlers.onInMouseDown} onMouseUp={handlers.onInMouseUp} />
+ );
+};
+
+export const OutSocket = ({ name }: { name: string }) => {
+ const handlers = useContext(SocketHandlers);
+ return (
+ <Socket name={name} class="__OutSocket" onMouseDown={handlers.onOutMouseDown} onMouseUp={handlers.onOutMouseUp} />
+ );
+};
diff --git a/src/components/nodes/index.ts b/src/components/nodes/index.ts
new file mode 100644
index 0000000..d29f39f
--- /dev/null
+++ b/src/components/nodes/index.ts
@@ -0,0 +1,3 @@
+export { default as NodeShell, NodeShellProps } from './NodeShell.tsx';
+export { OutputNumber, OutputVector, OutputProps } from './Output.tsx';
+export { InputAny, InputArray, InputInteger, InputNumber, InputVector, InputSelect, InputProps, InputSelectProps } from './Input.tsx';