feat: init frontend
This commit is contained in:
13
frontend/src/app.d.ts
vendored
Normal file
13
frontend/src/app.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
12
frontend/src/app.html
Normal file
12
frontend/src/app.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!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>
|
||||
</html>
|
36
frontend/src/lib/components/App.svelte
Normal file
36
frontend/src/lib/components/App.svelte
Normal file
@ -0,0 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { Canvas } from "@threlte/core";
|
||||
import Scene from "./Scene.svelte";
|
||||
import type { Graph } from "$lib/types";
|
||||
|
||||
const graph: Graph = {
|
||||
edges: [],
|
||||
nodes: [],
|
||||
};
|
||||
|
||||
for (let i = 0; i < 40; i++) {
|
||||
const x = i % 20;
|
||||
const y = Math.floor(i / 20);
|
||||
|
||||
graph.nodes.push({
|
||||
id: `${i.toString()}`,
|
||||
tmp: {
|
||||
visible: false,
|
||||
},
|
||||
position: {
|
||||
x: x * 7.5,
|
||||
y: y * 5,
|
||||
},
|
||||
type: "test",
|
||||
});
|
||||
|
||||
graph.edges.push({
|
||||
from: i.toString(),
|
||||
to: (i + 1).toString(),
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<Canvas shadows={false}>
|
||||
<Scene {graph} />
|
||||
</Canvas>
|
61
frontend/src/lib/components/Background.frag
Normal file
61
frontend/src/lib/components/Background.frag
Normal file
@ -0,0 +1,61 @@
|
||||
precision highp float;
|
||||
|
||||
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;
|
||||
|
||||
float grid(float x, float y, float divisions, float thickness) {
|
||||
x = fract(x * divisions);
|
||||
x = min(x, 1.0 - x);
|
||||
|
||||
float xdelta = fwidth(x);
|
||||
x = smoothstep(x - xdelta, x + xdelta, thickness);
|
||||
|
||||
y = fract(y * divisions);
|
||||
y = min(y, 1.0 - y);
|
||||
|
||||
float ydelta = fwidth(y);
|
||||
y = smoothstep(y - ydelta, y + ydelta, thickness);
|
||||
|
||||
return clamp(x + y, 0.0, 1.0);
|
||||
}
|
||||
|
||||
float circle_grid(float x, float y, float divisions, float circleRadius) {
|
||||
|
||||
float gridX = mod(x + divisions/2.0, divisions) - divisions / 2.0;
|
||||
float gridY = mod(y + divisions/2.0, divisions) - divisions / 2.0;
|
||||
|
||||
// Calculate the distance from the center of the grid
|
||||
float gridDistance = length(vec2(gridX, gridY));
|
||||
|
||||
// Use smoothstep to create a smooth transition at the edges of the circle
|
||||
float circle = 1.0 - smoothstep(circleRadius - 0.5, circleRadius + 0.5, gridDistance);
|
||||
|
||||
return circle;
|
||||
}
|
||||
|
||||
void main(void) {
|
||||
float divisions = 0.1/cz;
|
||||
float thickness = 0.05/cz;
|
||||
float delta = 0.1 / 2.0;
|
||||
|
||||
float ux = (vUv.x-0.5) * width + cx*cz;
|
||||
float uy = (vUv.y-0.5) * height - cy*cz;
|
||||
|
||||
float c1 = grid(ux, uy, divisions, thickness) * 0.1;
|
||||
float c2 = grid(ux, uy, divisions*2.0, thickness) * 0.1;
|
||||
float c = max(c1, c2);
|
||||
|
||||
float s1 = circle_grid(ux, uy, cz*10.0, 2.0) * 0.2;
|
||||
c = max(c, s1);
|
||||
|
||||
gl_FragColor = vec4(c, c, c, 1.0);
|
||||
}
|
15
frontend/src/lib/components/Background.vert
Normal file
15
frontend/src/lib/components/Background.vert
Normal file
@ -0,0 +1,15 @@
|
||||
varying vec2 vUv;
|
||||
varying vec3 vPosition;
|
||||
|
||||
void main() {
|
||||
|
||||
vUv = uv;
|
||||
|
||||
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
|
||||
|
||||
vec4 viewPosition = viewMatrix * modelPosition;
|
||||
vec4 projectedPosition = projectionMatrix * viewPosition;
|
||||
|
||||
gl_Position = projectedPosition;
|
||||
}
|
||||
|
52
frontend/src/lib/components/Edge.svelte
Normal file
52
frontend/src/lib/components/Edge.svelte
Normal file
@ -0,0 +1,52 @@
|
||||
<script lang="ts">
|
||||
import type { Node } from "$lib/types";
|
||||
import { T } from "@threlte/core";
|
||||
import { MeshLineGeometry, MeshLineMaterial } from "@threlte/extras";
|
||||
import { CubicBezierCurve, Vector2, Vector3 } from "three";
|
||||
|
||||
export let from: Node;
|
||||
export let to: Node;
|
||||
|
||||
let samples = 20;
|
||||
|
||||
console.log("edge");
|
||||
|
||||
const curve = new CubicBezierCurve(
|
||||
new Vector2(from.position.x + 20, from.position.y),
|
||||
new Vector2(from.position.x + 2, from.position.y),
|
||||
new Vector2(to.position.x - 2, to.position.y),
|
||||
new Vector2(to.position.x, to.position.y),
|
||||
);
|
||||
|
||||
let points: Vector3[] = [];
|
||||
|
||||
let last_from_x = 0;
|
||||
let last_from_y = 0;
|
||||
|
||||
function update(force = false) {
|
||||
if (!force) {
|
||||
const new_x = from.position.x + to.position.x;
|
||||
const new_y = from.position.y + to.position.y;
|
||||
if (last_from_x === new_x && last_from_y === new_y) {
|
||||
return;
|
||||
}
|
||||
last_from_x = new_x;
|
||||
last_from_y = new_y;
|
||||
}
|
||||
curve.v0.set(from.position.x + 5, from.position.y + 1.25);
|
||||
curve.v1.set(from.position.x + 6, from.position.y + 1.25);
|
||||
curve.v2.set(to.position.x - 1, to.position.y + 1.25);
|
||||
curve.v3.set(to.position.x, to.position.y + 1.25);
|
||||
points = curve.getPoints(samples).map((p) => new Vector3(p.x, 0, p.y));
|
||||
}
|
||||
|
||||
update();
|
||||
$: if (from.position || to.position) {
|
||||
update();
|
||||
}
|
||||
</script>
|
||||
|
||||
<T.Mesh>
|
||||
<MeshLineGeometry {points} />
|
||||
<MeshLineMaterial width={1} attenuate={false} color={0xffffff} />
|
||||
</T.Mesh>
|
255
frontend/src/lib/components/Scene.svelte
Normal file
255
frontend/src/lib/components/Scene.svelte
Normal file
@ -0,0 +1,255 @@
|
||||
<script lang="ts">
|
||||
import type { Graph, Node } from "$lib/types";
|
||||
import Edge from "./Edge.svelte";
|
||||
import { T, useTask } from "@threlte/core";
|
||||
import { settings } from "$lib/stores/settings";
|
||||
import type { OrthographicCamera } from "three";
|
||||
import { HTML, OrbitControls } from "@threlte/extras";
|
||||
import { onMount } from "svelte";
|
||||
import BackgroundVert from "./Background.vert";
|
||||
import BackgroundFrag from "./Background.frag";
|
||||
import { max } from "three/examples/jsm/nodes/Nodes.js";
|
||||
|
||||
export let camera: OrthographicCamera;
|
||||
|
||||
export let graph: Graph;
|
||||
|
||||
let cx = 0;
|
||||
let cy = 0;
|
||||
let cz = 30;
|
||||
let bw = 2;
|
||||
let bh = 2;
|
||||
|
||||
const minZoom = 4;
|
||||
const maxZoom = 150;
|
||||
|
||||
let width = globalThis?.innerWidth || 100;
|
||||
let height = globalThis?.innerHeight || 100;
|
||||
|
||||
let mouseX = 0;
|
||||
let mouseY = 0;
|
||||
|
||||
let mouseDown = false;
|
||||
let mouseDownX = 0;
|
||||
let mouseDownY = 0;
|
||||
|
||||
let activeNodeId: string;
|
||||
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
if (event.key === "a") {
|
||||
$settings.useHtml = !$settings.useHtml;
|
||||
}
|
||||
}
|
||||
|
||||
function snapToGrid(value: number) {
|
||||
return Math.round(value / 2.5) * 2.5;
|
||||
}
|
||||
|
||||
function handleMouseMove(event: MouseEvent) {
|
||||
cx = camera.position.x || 0;
|
||||
cy = camera.position.z || 0;
|
||||
cz = camera.zoom || 30;
|
||||
mouseX = cx + (event.clientX - width / 2) / cz;
|
||||
mouseY = cy + (event.clientY - height / 2) / cz;
|
||||
|
||||
if (activeNodeId && mouseDown) {
|
||||
graph.nodes = graph.nodes.map((node) => {
|
||||
if (node.id === activeNodeId) {
|
||||
node.position.x =
|
||||
(node?.tmp?.downX || 0) + (event.clientX - mouseDownX) / cz;
|
||||
node.position.y =
|
||||
(node?.tmp?.downY || 0) + (event.clientY - mouseDownY) / cz;
|
||||
|
||||
if (event.ctrlKey) {
|
||||
node.position.x = snapToGrid(node.position.x);
|
||||
node.position.y = snapToGrid(node.position.y);
|
||||
}
|
||||
|
||||
edges = [...edges];
|
||||
}
|
||||
return node;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let edges: [Node, Node][] = [];
|
||||
function calculateEdges() {
|
||||
edges = graph.edges
|
||||
.map((edge) => {
|
||||
const from = graph.nodes.find((node) => node.id === edge.from);
|
||||
const to = graph.nodes.find((node) => node.id === edge.to);
|
||||
if (!from || !to) return;
|
||||
return [from, to] as const;
|
||||
})
|
||||
.filter(Boolean) as unknown as [Node, Node][];
|
||||
}
|
||||
|
||||
calculateEdges();
|
||||
|
||||
function handleMouseDown(ev: MouseEvent) {
|
||||
activeNodeId = ev?.target?.dataset?.nodeId;
|
||||
mouseDown = true;
|
||||
mouseDownX = ev.clientX;
|
||||
mouseDownY = ev.clientY;
|
||||
const node = graph.nodes.find((node) => node.id === activeNodeId);
|
||||
if (!node) return;
|
||||
node.tmp = node.tmp || {};
|
||||
node.tmp.downX = node.position.x;
|
||||
node.tmp.downY = node.position.y;
|
||||
}
|
||||
|
||||
function handleMouseUp() {
|
||||
mouseDown = false;
|
||||
}
|
||||
|
||||
function updateCameraProps() {
|
||||
cx = camera.position.x || 0;
|
||||
cy = camera.position.z || 0;
|
||||
cz = camera.zoom || 30;
|
||||
width = window.innerWidth;
|
||||
height = window.innerHeight;
|
||||
bw = width / cz;
|
||||
bh = height / cz;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
updateCameraProps();
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
on:keydown={handleKeyDown}
|
||||
on:mousemove={handleMouseMove}
|
||||
on:mouseup={handleMouseUp}
|
||||
on:wheel={updateCameraProps}
|
||||
on:resize={updateCameraProps}
|
||||
/>
|
||||
|
||||
{#if true}
|
||||
<T.Group position.x={mouseX} position.z={mouseY}>
|
||||
<T.Mesh rotation.x={-Math.PI / 2} position.y={0.2}>
|
||||
<T.CircleGeometry args={[0.5, 16]} />
|
||||
<T.MeshBasicMaterial color="green" />
|
||||
</T.Mesh>
|
||||
</T.Group>
|
||||
{/if}
|
||||
|
||||
<T.Group position.x={cx} position.z={cy} position.y={-1.0}>
|
||||
<T.Mesh rotation.x={-Math.PI / 2} position.y={0.2} scale.x={bw} scale.y={bh}>
|
||||
<T.PlaneGeometry args={[1, 1]} />
|
||||
<T.ShaderMaterial
|
||||
transparent
|
||||
vertexShader={BackgroundVert}
|
||||
fragmentShader={BackgroundFrag}
|
||||
uniforms={{
|
||||
cx: {
|
||||
value: 0,
|
||||
},
|
||||
cy: {
|
||||
value: 0,
|
||||
},
|
||||
cz: {
|
||||
value: 30,
|
||||
},
|
||||
minz: {
|
||||
value: minZoom,
|
||||
},
|
||||
maxz: {
|
||||
value: maxZoom,
|
||||
},
|
||||
height: {
|
||||
value: 100,
|
||||
},
|
||||
width: {
|
||||
value: 100,
|
||||
},
|
||||
}}
|
||||
uniforms.cx.value={cx}
|
||||
uniforms.cy.value={cy}
|
||||
uniforms.cz.value={cz}
|
||||
uniforms.width.value={width}
|
||||
uniforms.height.value={height}
|
||||
/>
|
||||
</T.Mesh>
|
||||
</T.Group>
|
||||
|
||||
<T.OrthographicCamera
|
||||
bind:ref={camera}
|
||||
makeDefault
|
||||
position={[0, 1, 0]}
|
||||
zoom={30}
|
||||
>
|
||||
<OrbitControls
|
||||
enableZoom={true}
|
||||
target.y={0}
|
||||
rotateSpeed={0}
|
||||
minPolarAngle={0}
|
||||
maxPolarAngle={0}
|
||||
enablePan={true}
|
||||
zoomToCursor
|
||||
{maxZoom}
|
||||
{minZoom}
|
||||
/>
|
||||
</T.OrthographicCamera>
|
||||
|
||||
{#each edges as edge}
|
||||
<Edge from={edge[0]} to={edge[1]} />
|
||||
{/each}
|
||||
|
||||
<HTML transform={false}>
|
||||
<div class="wrapper" style={`--cz: ${cz}`} on:mousedown={handleMouseDown}>
|
||||
{#each graph.nodes as node}
|
||||
<div
|
||||
class="node"
|
||||
data-node-id={node.id}
|
||||
style={`--nx:${node.position.x * 10}px;
|
||||
--ny: ${node.position.y * 10}px`}
|
||||
>
|
||||
{node.id}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</HTML>
|
||||
|
||||
<style>
|
||||
:global(body) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
transform: scale(calc(var(--cz) * 0.1));
|
||||
}
|
||||
|
||||
.node {
|
||||
position: absolute;
|
||||
border-radius: 1px;
|
||||
user-select: none !important;
|
||||
cursor: pointer;
|
||||
width: 50px;
|
||||
height: 25px;
|
||||
background: linear-gradient(-11.1grad, #000 0%, #0b0b0b 100%);
|
||||
color: white;
|
||||
transform: translate(var(--nx), var(--ny));
|
||||
z-index: 1;
|
||||
box-shadow: 0px 0px 0px calc(15px / var(--cz)) rgba(255, 255, 255, 0.3);
|
||||
font-weight: 300;
|
||||
font-size: 0.5em;
|
||||
}
|
||||
|
||||
.node::after {
|
||||
/* content: ""; */
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: white;
|
||||
border-radius: 2px;
|
||||
transform: scale(1.01);
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
</style>
|
11
frontend/src/lib/components/nodes/Output.svelte
Normal file
11
frontend/src/lib/components/nodes/Output.svelte
Normal file
@ -0,0 +1,11 @@
|
||||
<script context="module" lang="ts">
|
||||
export type Props = {
|
||||
value: number;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let props = { value: 0 };
|
||||
</script>
|
||||
|
||||
<p>output</p>
|
11
frontend/src/lib/components/nodes/Test.svelte
Normal file
11
frontend/src/lib/components/nodes/Test.svelte
Normal file
@ -0,0 +1,11 @@
|
||||
<script context="module" lang="ts">
|
||||
export type Props = {
|
||||
value: number;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let props = { value: 0 };
|
||||
</script>
|
||||
|
||||
<input type="number" bind:value={props.value} />
|
6
frontend/src/lib/components/nodes/index.ts
Normal file
6
frontend/src/lib/components/nodes/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import Test from './Test.svelte';
|
||||
import Output from './Output.svelte';
|
||||
export const nodes = {
|
||||
test: Test,
|
||||
output: Output,
|
||||
} as const;
|
1
frontend/src/lib/index.ts
Normal file
1
frontend/src/lib/index.ts
Normal file
@ -0,0 +1 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
5
frontend/src/lib/stores/settings.ts
Normal file
5
frontend/src/lib/stores/settings.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
export const settings = writable({
|
||||
useHtml: false
|
||||
});
|
34
frontend/src/lib/types.ts
Normal file
34
frontend/src/lib/types.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { nodes } from "$lib/components/nodes"
|
||||
|
||||
export type Node = {
|
||||
id: string;
|
||||
type: keyof typeof nodes;
|
||||
props?: Record<string, any>,
|
||||
tmp?: {
|
||||
downX?: number;
|
||||
downY?: number;
|
||||
visible?: boolean;
|
||||
},
|
||||
meta?: {
|
||||
title?: string;
|
||||
lastModified?: string;
|
||||
},
|
||||
position: {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
}
|
||||
|
||||
export type Edge = {
|
||||
from: string;
|
||||
to: string;
|
||||
}
|
||||
|
||||
export type Graph = {
|
||||
meta?: {
|
||||
title?: string;
|
||||
lastModified?: string;
|
||||
},
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
}
|
5
frontend/src/routes/+layout.svelte
Normal file
5
frontend/src/routes/+layout.svelte
Normal file
@ -0,0 +1,5 @@
|
||||
<script lang="ts">
|
||||
import "./app.css";
|
||||
</script>
|
||||
|
||||
<slot />
|
2
frontend/src/routes/+layout.ts
Normal file
2
frontend/src/routes/+layout.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export const prerender = true
|
||||
export const ssr = false
|
47
frontend/src/routes/+page.svelte
Normal file
47
frontend/src/routes/+page.svelte
Normal file
@ -0,0 +1,47 @@
|
||||
<script lang="ts">
|
||||
import App from "$lib/components/App.svelte";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const res = await invoke("greet", { name: "Dude" });
|
||||
console.log({ res });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
try {
|
||||
const res2 = await invoke("run_nodes", {});
|
||||
console.log({ res2 });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<App />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
:global(html) {
|
||||
background: rgb(13, 19, 32);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(13, 19, 32, 1) 0%,
|
||||
rgba(8, 12, 21, 1) 100%
|
||||
);
|
||||
}
|
||||
|
||||
:global(body) {
|
||||
margin: 0;
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
25
frontend/src/routes/app.css
Normal file
25
frontend/src/routes/app.css
Normal file
@ -0,0 +1,25 @@
|
||||
/* fira-code-300 - latin */
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
/* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: 'Fira Code';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url('/fonts/fira-code-v22-latin-300.woff2') format('woff2');
|
||||
/* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* fira-code-600 - latin */
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
/* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: 'Fira Code';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url('/fonts/fira-code-v22-latin-600.woff2') format('woff2');
|
||||
/* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: 'Fira Code', monospace;
|
||||
}
|
Reference in New Issue
Block a user