diff options
Diffstat (limited to 'src/components')
| -rw-r--r-- | src/components/nodes/Input.css | 103 | ||||
| -rw-r--r-- | src/components/nodes/Input.tsx | 97 | ||||
| -rw-r--r-- | src/components/nodes/NodeShell.css | 47 | ||||
| -rw-r--r-- | src/components/nodes/NodeShell.tsx | 46 | ||||
| -rw-r--r-- | src/components/nodes/Output.css | 20 | ||||
| -rw-r--r-- | src/components/nodes/Output.tsx | 19 | ||||
| -rw-r--r-- | src/components/nodes/Socket.css | 20 | ||||
| -rw-r--r-- | src/components/nodes/Socket.tsx | 41 | ||||
| -rw-r--r-- | src/components/nodes/index.ts | 3 |
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'; |
