feat: add theming basics

This commit is contained in:
max_richter 2024-03-14 16:28:38 +01:00
parent f9d211eb72
commit 9b76299272
20 changed files with 307 additions and 167 deletions

View File

@ -1,12 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/svelte.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/svelte.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@ -3,12 +3,13 @@
import { OrbitControls } from "@threlte/extras";
import { onMount } from "svelte";
import { MOUSE, type OrthographicCamera } from "three";
import type { OrbitControls as OrbitControlsType } from "three/examples/jsm/Addons.js";
export let camera: OrthographicCamera | undefined = undefined;
export let maxZoom = 150;
export let minZoom = 4;
let controls: OrbitControls | undefined = undefined;
export let controls: OrbitControlsType | undefined = undefined;
export const position: [number, number, number] = [0, 1, 0];
@ -56,8 +57,8 @@
<T.OrthographicCamera bind:ref={camera} position.y={10} makeDefault>
<OrbitControls
args={[camera, window]}
mouseButtons={{ LEFT: MOUSE.PAN, MIDDLE: 0, RIGHT: 0 }}
args={[camera, document.body]}
mouseButtons={{ LEFT: 0, MIDDLE: 0, RIGHT: MOUSE.PAN }}
bind:ref={controls}
enableZoom={true}
zoomSpeed={2}

View File

@ -1,6 +1,6 @@
<script lang="ts">
import type { Node } from "$lib/types";
import { getContext, onMount } from "svelte";
import { getContext } from "svelte";
import NodeHeader from "./NodeHeader.svelte";
import NodeParameter from "./NodeParameter.svelte";
import { activeNodeId, selectedNodes } from "./graph/stores";
@ -38,7 +38,6 @@
<NodeParameter
{node}
id={key}
index={i}
input={value}
isLast={i == parameters.length - 1}
/>
@ -51,25 +50,24 @@
box-sizing: border-box;
user-select: none !important;
cursor: pointer;
width: 100px;
width: 200px;
color: var(--text-color);
transform: translate3d(var(--nx), var(--ny), 0);
z-index: 1;
font-weight: 300;
font-size: 0.5em;
display: none;
--stroke: #777;
--stroke-width: 0.1px;
--stroke: var(--background-color-lighter);
--stroke-width: 2px;
}
.node.active {
--stroke: white;
--stroke-width: 0.3px;
--stroke-width: 1px;
}
.node.selected {
--stroke: #f2be90;
--stroke-width: 0.2px;
--stroke-width: 1px;
}
.node.in-view {

View File

@ -81,7 +81,7 @@
.wrapper {
position: relative;
width: 100%;
height: 25px;
height: 50px;
}
.click-target {
@ -89,8 +89,8 @@
right: 0px;
top: 50%;
transform: translateX(50%) translateY(-50%);
height: 15px;
width: 15px;
height: 30px;
width: 30px;
z-index: 100;
border-radius: 50%;
/* background: red; */
@ -114,7 +114,9 @@
svg path {
stroke-width: 0.2px;
transition: 0.2s;
transition:
d 0.3s ease,
fill 0.3s ease;
fill: var(--background-color-lighter);
stroke: var(--stroke);
stroke-width: var(--stroke-width);
@ -125,7 +127,7 @@
font-size: 1em;
display: flex;
align-items: center;
padding-left: 5px;
padding-left: 20px;
height: 100%;
}

View File

@ -47,9 +47,9 @@
aspectRatio,
});
const pathHover = createNodePath({
depth: 8,
height: 24,
y: 50,
depth: 6,
height: 18,
y: 50.5,
cornerBottom,
leftBump,
aspectRatio,
@ -102,7 +102,7 @@
.wrapper {
position: relative;
width: 100%;
height: 50px;
height: 100px;
transform: translateY(-0.5px);
}
@ -116,13 +116,13 @@
}
.small.target {
width: 15px;
height: 15px;
width: 30px;
height: 30px;
}
.large.target {
width: 30px;
height: 30px;
width: 60px;
height: 60px;
cursor: unset;
pointer-events: none;
}
@ -133,14 +133,13 @@
.content {
position: relative;
padding: 2px 5px;
padding: 10px 20px;
display: flex;
flex-direction: column;
height: 100%;
justify-content: space-around;
box-sizing: border-box;
opacity: calc((2 * var(--cz)) / 150 - 0.5);
opacity: calc((var(--cz) - 10) / 20);
opacity: var(--input-opacity);
}
:global(.zoom-small) .content {
@ -150,14 +149,11 @@
.input {
width: 100%;
box-sizing: border-box;
border-radius: 2px;
border-radius: 3px;
font-size: 1em;
padding: 2px 2px;
padding: 10px;
background: #111;
}
label {
font-size: 1em;
background: var(--background-color-lighter);
}
svg {
@ -172,7 +168,9 @@
}
svg path {
transition: 0.2s;
transition:
d 0.3s ease,
fill 0.3s ease;
fill: var(--background-color);
stroke: var(--stroke);
stroke-width: var(--stroke-width);

View File

@ -4,13 +4,10 @@ varying vec2 vUv;
const float PI = 3.14159265359;
uniform float width;
uniform float height;
uniform float cx;
uniform float cy;
uniform float cz;
uniform float minZ;
uniform float maxZ;
uniform vec2 dimensions;
uniform vec3 camPos;
uniform vec2 zoomLimits;
uniform vec3 backgroundColor;
float grid(float x, float y, float divisions, float thickness) {
x = fract(x * divisions);
@ -47,6 +44,17 @@ float lerp(float a, float b,float t) {
}
void main(void) {
float cx = camPos.x;
float cy = camPos.y;
float cz = camPos.z;
float width = dimensions.x;
float height = dimensions.y;
float minZ = zoomLimits.x;
float maxZ = zoomLimits.y;
float divisions = 0.1/cz;
float thickness = 0.05/cz;
float delta = 0.1 / 2.0;
@ -84,7 +92,7 @@ void main(void) {
float c = mix(large, small, min(nz*2.0+0.05, 1.0));
c = mix(c, xsmall, max(min((nz-0.3)/0.7, 1.0), 0.0));
//c = xsmall;
vec3 color = mix(backgroundColor, vec3(1.0), c*0.5);
gl_FragColor = vec4(c, c, c, 1.0);
gl_FragColor = vec4(color, 1.0);
}

View File

@ -1,9 +1,10 @@
<script lang="ts">
import { T } from "@threlte/core";
import { onMount } from "svelte";
import BackgroundVert from "./Background.vert";
import BackgroundFrag from "./Background.frag";
import { Color } from "three";
import { colors } from "../graph/stores";
export let minZoom = 4;
export let maxZoom = 150;
@ -34,33 +35,23 @@
vertexShader={BackgroundVert}
fragmentShader={BackgroundFrag}
uniforms={{
cx: {
value: 0,
camPos: {
value: [0, 1, 0],
},
cy: {
value: 0,
backgroundColor: {
value: [0, 0, 0],
},
cz: {
value: 30,
zoomLimits: {
value: [2, 50],
},
minZ: {
value: minZoom,
},
maxZ: {
value: maxZoom,
},
height: {
value: 100,
},
width: {
value: 100,
dimensions: {
value: [100, 100],
},
}}
uniforms.cx.value={cameraPosition[0]}
uniforms.cy.value={cameraPosition[1]}
uniforms.cz.value={cameraPosition[2]}
uniforms.width.value={width}
uniforms.height.value={height}
uniforms.camPos.value={cameraPosition}
uniforms.backgroundColor.value={$colors.backgroundColorDarker}
uniforms.zoomLimits.value={[minZoom, maxZoom]}
uniforms.dimensions.value={[width, height]}
/>
</T.Mesh>
</T.Group>

View File

@ -22,6 +22,10 @@
let mesh: Mesh;
import { colors } from "../graph/stores";
$: color = $colors.backgroundColorLighter;
export const update = function (force = false) {
if (!force) {
const new_x = from.x + to.x;
@ -64,8 +68,8 @@
position.y={0.8}
rotation.x={-Math.PI / 2}
>
<T.CircleGeometry args={[0.2, 16]} />
<T.MeshBasicMaterial color={0x555555} />
<T.CircleGeometry args={[0.3, 16]} />
<T.MeshBasicMaterial {color} />
</T.Mesh>
<T.Mesh
@ -74,11 +78,11 @@
position.y={0.8}
rotation.x={-Math.PI / 2}
>
<T.CircleGeometry args={[0.2, 16]} />
<T.MeshBasicMaterial color={0x555555} />
<T.CircleGeometry args={[0.3, 16]} />
<T.MeshBasicMaterial {color} />
</T.Mesh>
<T.Mesh position.y={0.5} bind:ref={mesh}>
<MeshLineGeometry {points} />
<MeshLineMaterial width={2} attenuate={false} color={0x555555} />
<MeshLineMaterial width={2} attenuate={false} {color} />
</T.Mesh>

View File

@ -19,6 +19,7 @@
selectedNodes,
} from "./stores";
import BoxSelection from "../BoxSelection.svelte";
import type { OrbitControls } from "three/examples/jsm/Addons.js";
export let graph: GraphManager;
setContext("graphManager", graph);
@ -27,8 +28,9 @@
const edges = graph.edges;
let camera: OrthographicCamera;
const minZoom = 4;
const maxZoom = 100;
let controls: OrbitControls;
const minZoom = 2;
const maxZoom = 40;
let mousePosition = [0, 0];
let mouseDown: null | [number, number] = null;
let boxSelection = false;
@ -80,16 +82,16 @@
}
const node = graph.getNodeType(nodeTypeId);
if (!node?.inputs) {
return 2.5;
return 5;
}
const height = 2.5 + 5 * Object.keys(node.inputs).length;
const height = 5 + 10 * Object.keys(node.inputs).length;
nodeHeightCache[nodeTypeId] = height;
return height;
}
setContext("isNodeInView", (node: NodeType) => {
const height = getNodeHeight(node.type);
const width = 10;
const width = 20;
return (
// check x-axis
node.position.x > cameraBounds[0] - width &&
@ -159,14 +161,14 @@
): [number, number] {
if (typeof index === "number") {
return [
(node?.tmp?.x ?? node.position.x) + 10,
(node?.tmp?.y ?? node.position.y) + 1.25 + 5 * index,
(node?.tmp?.x ?? node.position.x) + 20,
(node?.tmp?.y ?? node.position.y) + 2.5 + 10 * index,
];
} else {
const _index = Object.keys(node.tmp?.type?.inputs || {}).indexOf(index);
return [
node?.tmp?.x ?? node.position.x,
(node?.tmp?.y ?? node.position.y) + 5 + 5 * _index,
(node?.tmp?.y ?? node.position.y) + 10 + 10 * _index,
];
}
}
@ -209,71 +211,74 @@
// handle box selection
if (boxSelection) {
event.preventDefault();
event.stopPropagation();
const mouseD = projectScreenToWorld(mouseDown[0], mouseDown[1]);
const x1 = Math.min(mouseD[0], mousePosition[0]);
const x2 = Math.max(mouseD[0], mousePosition[0]);
const y1 = Math.min(mouseD[1], mousePosition[1]);
const y2 = Math.max(mouseD[1], mousePosition[1]);
const _selectedNodes = $selectedNodes || new Set();
for (const node of $nodes.values()) {
if (!node?.tmp) continue;
const x = node.position.x;
const y = node.position.y;
if (x > x1 && x < x2 && y > y1 && y < y2) {
_selectedNodes.add(node.id);
const height = getNodeHeight(node.type);
if (x > x1 - 20 && x < x2 && y > y1 - height && y < y2) {
$selectedNodes?.add(node.id);
} else {
_selectedNodes?.delete(node.id);
$selectedNodes?.delete(node.id);
}
}
$selectedNodes = _selectedNodes;
$selectedNodes = $selectedNodes;
return;
}
// here we are handling dragging of nodes
if ($activeNodeId === -1) return;
if ($activeNodeId !== -1) {
const node = graph.getNode($activeNodeId);
if (!node || event.buttons !== 1) return;
const node = graph.getNode($activeNodeId);
if (!node || event.buttons !== 1) return;
node.tmp = node.tmp || {};
node.tmp = node.tmp || {};
const oldX = node.tmp.downX || 0;
const oldY = node.tmp.downY || 0;
const oldX = node.tmp.downX || 0;
const oldY = node.tmp.downY || 0;
let newX = oldX + (event.clientX - mouseDown[0]) / cameraPosition[2];
let newY = oldY + (event.clientY - mouseDown[1]) / cameraPosition[2];
let newX = oldX + (event.clientX - mouseDown[0]) / cameraPosition[2];
let newY = oldY + (event.clientY - mouseDown[1]) / cameraPosition[2];
if (event.ctrlKey) {
const snapLevel = getSnapLevel();
newX = snapToGrid(newX, 5 / snapLevel);
newY = snapToGrid(newY, 5 / snapLevel);
}
if (!node.tmp.isMoving) {
const dist = Math.sqrt((oldX - newX) ** 2 + (oldY - newY) ** 2);
if (dist > 0.2) {
node.tmp.isMoving = true;
if (event.ctrlKey) {
const snapLevel = getSnapLevel();
newX = snapToGrid(newX, 5 / snapLevel);
newY = snapToGrid(newY, 5 / snapLevel);
}
}
const vecX = oldX - newX;
const vecY = oldY - newY;
if ($selectedNodes?.size) {
for (const nodeId of $selectedNodes) {
const n = graph.getNode(nodeId);
if (!n?.tmp) continue;
n.tmp.x = (n?.tmp?.downX || 0) - vecX;
n.tmp.y = (n?.tmp?.downY || 0) - vecY;
updateNodePosition(n);
if (!node.tmp.isMoving) {
const dist = Math.sqrt((oldX - newX) ** 2 + (oldY - newY) ** 2);
if (dist > 0.2) {
node.tmp.isMoving = true;
}
}
const vecX = oldX - newX;
const vecY = oldY - newY;
if ($selectedNodes?.size) {
for (const nodeId of $selectedNodes) {
const n = graph.getNode(nodeId);
if (!n?.tmp) continue;
n.tmp.x = (n?.tmp?.downX || 0) - vecX;
n.tmp.y = (n?.tmp?.downY || 0) - vecY;
updateNodePosition(n);
}
}
node.tmp.x = newX;
node.tmp.y = newY;
updateNodePosition(node);
$edges = $edges;
}
node.tmp.x = newX;
node.tmp.y = newY;
updateNodePosition(node);
$edges = $edges;
}
function handleMouseDown(event: MouseEvent) {
@ -316,6 +321,7 @@
}
} else if (event.ctrlKey) {
boxSelection = true;
controls.enabled = false;
} else {
$activeNodeId = -1;
$selectedNodes?.clear();
@ -489,6 +495,7 @@
}
mouseDown = null;
controls.enabled = true;
boxSelection = false;
$activeSocket = null;
$possibleSockets = [];
@ -497,18 +504,24 @@
}
</script>
<svelte:window
<svelte:document
on:mousemove={handleMouseMove}
on:mouseup={handleMouseUp}
on:mousedown={handleMouseDown}
on:keydown={handleKeyDown}
bind:innerWidth={width}
bind:innerHeight={height}
/>
<svelte:window bind:innerWidth={width} bind:innerHeight={height} />
<Debug />
<Camera bind:camera {maxZoom} {minZoom} bind:position={cameraPosition} />
<Camera
bind:controls
bind:camera
{maxZoom}
{minZoom}
bind:position={cameraPosition}
/>
<Background {cameraPosition} {maxZoom} {minZoom} {width} {height} />

View File

@ -1,11 +1,11 @@
<script lang="ts">
import type { Edge as EdgeType, Node as NodeType, Socket } from "$lib/types";
import type { Edge as EdgeType, Node as NodeType } from "$lib/types";
import { HTML } from "@threlte/extras";
import Edge from "../edges/Edge.svelte";
import Node from "../Node.svelte";
import { getContext, onMount } from "svelte";
import type { Writable } from "svelte/store";
import { activeSocket } from "./stores";
import { activeSocket, colors } from "./stores";
export let nodes: Writable<Map<number, NodeType>>;
export let edges: Writable<EdgeType[]>;
@ -40,6 +40,7 @@
{@const pos = getEdgePosition(edge)}
{@const [x1, y1, x2, y2] = pos}
<Edge
color={$colors.backgroundColorLighter}
from={{
x: x1,
y: y1,
@ -56,9 +57,9 @@
role="tree"
tabindex="0"
class="wrapper"
class:zoom-small={cameraPosition[2] < 10}
class:zoom-small={cameraPosition[2] < 2}
class:hovering-sockets={activeSocket}
style={`--cz: ${cameraPosition[2]}`}
style={`--cz: ${cameraPosition[2]};`}
>
{#each $nodes.values() as node (node.id)}
<Node {node} inView={cameraPosition && isNodeInView(node)} />
@ -73,5 +74,6 @@
width: 0px;
height: 0px;
transform: scale(calc(var(--cz) * 0.1));
--input-opacity: calc((var(--cz) - 2) / 5);
}
</style>

View File

@ -1,5 +1,7 @@
import type { Node, Socket } from "$lib/types";
import { browser } from "$app/environment";
import type { Socket } from "$lib/types";
import { writable, type Writable } from "svelte/store";
import { Color } from "three";
export const activeNodeId: Writable<number> = writable(-1);
export const selectedNodes: Writable<Set<number> | null> = writable(null);
@ -8,3 +10,41 @@ export const activeSocket: Writable<Socket | null> = writable(null);
export const hoveredSocket: Writable<Socket | null> = writable(null);
export const possibleSockets: Writable<Socket[]> = writable([]);
export const possibleSocketIds: Writable<Set<string> | null> = writable(null);
export const colors = writable({
backgroundColorDarker: new Color().setStyle("#101010"),
backgroundColor: new Color().setStyle("#151515"),
backgroundColorLighter: new Color().setStyle("#202020")
});
if (browser) {
const body = document.body;
function updateColors() {
const style = getComputedStyle(body);
const backgroundColorDarker = style.getPropertyValue("--background-color-darker");
const backgroundColor = style.getPropertyValue("--background-color");
const backgroundColorLighter = style.getPropertyValue("--background-color-lighter");
colors.update(col => {
col.backgroundColorDarker.setStyle(backgroundColorDarker);
col.backgroundColor.setStyle(backgroundColor);
col.backgroundColorLighter.setStyle(backgroundColorLighter);
return col;
});
}
globalThis["updateColors"] = updateColors;
body.addEventListener("transitionstart", () => {
updateColors();
})
window.onload = () => {
updateColors();
}
}

View File

@ -0,0 +1,17 @@
<script lang="ts">
import type { Hst } from "@histoire/plugin-svelte";
export let Hst: Hst;
import Checkbox from "./Checkbox.svelte";
</script>
<Hst.Story>
<div>
<Checkbox checked={false} />
</div>
</Hst.Story>
<style>
div {
padding: 1em;
}
</style>

View File

@ -0,0 +1,58 @@
<script lang="ts">
export let checked: boolean;
</script>
<input type="checkbox" bind:checked />
<style>
input[type="checkbox"] {
/* Add if not using autoprefixer */
-webkit-appearance: none;
/* Remove most all native input styles */
appearance: none;
/* For iOS < 15 */
background-color: var(--form-background);
/* Not removed via appearance */
margin: 0;
font: inherit;
color: currentColor;
width: 1.15em;
height: 1.15em;
border: 0.15em solid currentColor;
border-radius: 0.15em;
transform: translateY(-0.075em);
display: grid;
place-content: center;
}
input[type="checkbox"]::before {
content: "";
width: 0.65em;
height: 0.65em;
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
transform: scale(0);
transform-origin: bottom left;
transition: 120ms transform ease-in-out;
box-shadow: inset 1em 1em var(--form-control-color);
/* Windows High Contrast Mode */
background-color: CanvasText;
}
input[type="checkbox"]:checked::before {
transform: scale(1);
}
input[type="checkbox"]:focus {
outline: max(2px, 0.15em) solid currentColor;
outline-offset: max(2px, 0.15em);
}
input[type="checkbox"]:disabled {
--form-control-color: var(--form-control-disabled);
color: var(--form-control-disabled);
cursor: not-allowed;
}
</style>

View File

View File

View File

View File

@ -319,8 +319,8 @@ export class GraphManager {
visible: false,
},
position: {
x: x * 15,
y: y * 20,
x: x * 30,
y: y * 40,
},
props: i == 0 ? { value: 0 } : {},
type: i == 0 ? "input/float" : "math",
@ -335,8 +335,8 @@ export class GraphManager {
visible: false,
},
position: {
x: width * 15,
y: (height - 1) * 20,
x: width * 30,
y: (height - 1) * 40,
},
type: "output",
props: {},

View File

@ -61,4 +61,12 @@ export function createNodePath({
: ` H0`
}
Z`.replace(/\s+/g, " ");
}
}
export const debounce = (fn: Function, ms = 300) => {
let timeoutId: ReturnType<typeof setTimeout>;
return function (this: any, ...args: any[]) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), ms);
};
};

View File

@ -37,7 +37,7 @@
</div>
<div class="canvas-wrapper">
<Canvas shadows={false} renderMode="on-demand" autoRender={true}>
<Canvas shadows={false} renderMode="on-demand" colorManagementEnabled={false}>
<!-- <PerfMonitor /> -->
<Graph {graph} bind:debug />
</Canvas>

View File

@ -23,28 +23,6 @@
:root {
font-family: 'Fira Code', monospace;
/* Colors */
--primary-color: #007bff;
/* Primary brand color */
--secondary-color: #6c757d;
/* Secondary color */
--background-color: #151515;
--background-color-lightest: #ffffff;
--background-color-lighter: #202020;
--background-color-dark: #dae0e5;
--background-color-darkest: #c8d1d7;
/* Background color */
--text-color: #aeaeae;
/* Text color */
--accent-color: #ffc107;
/* Accent color */
/* Typography */
--font-family: Arial, sans-serif;
--font-size-base: 16px;
/* Base font size */
--line-height-base: 1.5;
/* Base line height */
/* Spacing */
--spacing-xs: 4px;
@ -62,4 +40,23 @@
body {
overflow: hidden;
/* Secondary color */
--secondary-color: #6c757d;
/* Background color */
--background-color-lighter: #202020;
--background-color: #151515;
--background-color-darker: #101010;
--text-color: #aeaeae;
}
body.theme-catppuccin {
--text-color: #CDD6F4;
--background-color-lighter: #313244;
--background-color: #1E1E2E;
--background-color-darker: #11111b;
}
/* canvas { */
/* display: none !important; */
/* } */