The great move >.<
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
.file-browser {
|
||||
background: var(--c-bg-0);
|
||||
margin-left: calc(var(--sp-2) * -1);
|
||||
}
|
||||
|
||||
.file-browser-list {
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
padding: 0 0 0 var(--sp-2);
|
||||
}
|
||||
|
||||
.file-brwoser-item {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
|
||||
.file-browser-label {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding: var(--sp-1);
|
||||
border-radius: var(--sp-1);
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
margin-right: var(--sp-1);
|
||||
}
|
||||
|
||||
&.active,
|
||||
&:hover {
|
||||
background: var(--c-bg-2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import "./FileBrowser.css";
|
||||
import {
|
||||
type Accessor,
|
||||
type Component,
|
||||
createSignal,
|
||||
For,
|
||||
Show,
|
||||
} from "solid-js";
|
||||
import { FileIcon } from "./icons/File.tsx";
|
||||
import { FolderIcon } from "./icons/Folder.tsx";
|
||||
import { FolderOpenIcon } from "./icons/FolderOpen.tsx";
|
||||
|
||||
export type BrowserFile = {
|
||||
path: string;
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
export type BrowserFolder = Map<string, BrowserFile | BrowserFolder>;
|
||||
|
||||
export type FileHandler = (path: string) => void;
|
||||
|
||||
function isFolder(entry: BrowserFile | BrowserFolder): entry is BrowserFolder {
|
||||
return typeof (entry as BrowserFolder).size === "number";
|
||||
}
|
||||
|
||||
export const FileBrowser: Component<{
|
||||
root: Accessor<BrowserFolder>;
|
||||
handler: FileHandler;
|
||||
}> = (props) => {
|
||||
const FileEntry: Component<{
|
||||
name: string;
|
||||
entry: BrowserFile;
|
||||
}> = ({ name, entry }) => {
|
||||
return (
|
||||
<div
|
||||
class="file-browser-label"
|
||||
classList={{
|
||||
active: entry.isActive,
|
||||
}}
|
||||
onClick={() => props.handler(entry.path)}
|
||||
>
|
||||
<FileIcon />
|
||||
<span>{name}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FolderEntry: Component<{
|
||||
folder: BrowserFolder;
|
||||
name: string;
|
||||
open: boolean;
|
||||
}> = ({ folder, name, open }) => {
|
||||
const [isOpen, setIsOpen] = createSignal(open);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="file-browser-label" onClick={() => setIsOpen(!isOpen())}>
|
||||
{isOpen() ? <FolderOpenIcon /> : <FolderIcon />}
|
||||
<span>{name}</span>
|
||||
</div>
|
||||
|
||||
<Show when={isOpen()}>
|
||||
<FileList folder={folder} />
|
||||
</Show>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const FileList: Component<{ folder: BrowserFolder }> = (props) => {
|
||||
return (
|
||||
<ul class="file-browser-list">
|
||||
<For each={[...props.folder.entries()]}>
|
||||
{(item, index) => {
|
||||
const [name, entry] = item;
|
||||
|
||||
return (
|
||||
<li class="file-browser-item">
|
||||
{isFolder(entry) ? (
|
||||
<FolderEntry folder={entry} name={name} open={index() < 1} />
|
||||
) : (
|
||||
<FileEntry name={name} entry={entry} />
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="file-browser">
|
||||
<FileList folder={props.root()} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
.graph-browser {
|
||||
border-radius: var(--sp-1);
|
||||
overflow: hidden;
|
||||
|
||||
canvas {
|
||||
display: block;
|
||||
user-select: none;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
import "./GraphBrowser.css";
|
||||
import { type Component, createSignal, onMount } from "solid-js";
|
||||
import {
|
||||
type Edge,
|
||||
type Graph,
|
||||
getNeighbours,
|
||||
type Node,
|
||||
} from "#/engine/Graph.ts";
|
||||
import { mulberry32 } from "#/engine/lib/mulberry32.ts";
|
||||
import { currentScene } from "#/engine/Scene.ts";
|
||||
import {
|
||||
createVector,
|
||||
getDistance,
|
||||
scale,
|
||||
setDistanceNormal,
|
||||
type Vector,
|
||||
} from "#/engine/Vector.ts";
|
||||
|
||||
type PhysicsPartial = {
|
||||
position: Vector;
|
||||
velocity: Vector;
|
||||
acceleration: Vector;
|
||||
};
|
||||
|
||||
function drawNode(
|
||||
node: Node<PhysicsPartial>,
|
||||
ctx: CanvasRenderingContext2D,
|
||||
color: string = "#dbdee1",
|
||||
): void {
|
||||
const text = (node as PhysicsPartial & { name: string }).name;
|
||||
const metrics = ctx.measureText(text);
|
||||
|
||||
ctx.fillStyle = color;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(
|
||||
node.position.x - metrics.width / 2,
|
||||
node.position.y,
|
||||
11,
|
||||
Math.PI * 0.5,
|
||||
Math.PI * 1.5,
|
||||
);
|
||||
ctx.arc(
|
||||
node.position.x + metrics.width / 2,
|
||||
node.position.y,
|
||||
11,
|
||||
-Math.PI * 0.5,
|
||||
-Math.PI * 1.5,
|
||||
);
|
||||
ctx.fill();
|
||||
|
||||
ctx.shadowColor = "#000";
|
||||
ctx.fillStyle = "#fff";
|
||||
ctx.fillText(text, node.position.x, node.position.y);
|
||||
ctx.shadowColor = "transparent";
|
||||
}
|
||||
|
||||
function drawEdge(
|
||||
edge: Edge<PhysicsPartial>,
|
||||
ctx: CanvasRenderingContext2D,
|
||||
color: string = "#dbdee1",
|
||||
): void {
|
||||
const tail = edge[0].position;
|
||||
const tip = edge[1].position;
|
||||
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeStyle = color;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(tail.x, tail.y);
|
||||
ctx.lineTo(tip.x, tip.y);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function applyRepulsion(nodes: Array<PhysicsPartial>): void {
|
||||
const strength = 4;
|
||||
|
||||
nodes.forEach((node, index) => {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
if (index === i) {
|
||||
continue;
|
||||
}
|
||||
const a = node.position;
|
||||
const b = nodes[i].position;
|
||||
|
||||
const dx = a.x - b.x;
|
||||
const dy = a.y - b.y;
|
||||
|
||||
const direction = createVector(dx, dy);
|
||||
|
||||
const distanceSquare = dx * dx + dy * dy;
|
||||
const magnitude = strength / (distanceSquare + Number.EPSILON);
|
||||
|
||||
scale(direction, magnitude);
|
||||
|
||||
node.acceleration.x += direction.x;
|
||||
node.acceleration.y += direction.y;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function hookEdge<T extends PhysicsPartial>([a, b]: Edge<T>): void {
|
||||
const restLength = 86;
|
||||
const stiffness = 0.05;
|
||||
|
||||
const direction = createVector();
|
||||
setDistanceNormal(direction, a.position, b.position);
|
||||
|
||||
const distance = getDistance(a.position, b.position);
|
||||
const magnitude = (distance - restLength) * stiffness + Number.EPSILON;
|
||||
|
||||
scale(direction, magnitude);
|
||||
|
||||
a.acceleration.x -= direction.x;
|
||||
a.acceleration.y -= direction.y;
|
||||
|
||||
b.acceleration.x += direction.x;
|
||||
b.acceleration.y += direction.y;
|
||||
}
|
||||
|
||||
export const GraphBrowser: Component<{
|
||||
graph: Graph<string>;
|
||||
seed: number;
|
||||
}> = (props) => {
|
||||
let canvasRef!: HTMLCanvasElement;
|
||||
const next = mulberry32(props.seed);
|
||||
|
||||
const [width] = createSignal(300);
|
||||
const [height] = createSignal(300);
|
||||
|
||||
const enhanceNode = (node: string) => {
|
||||
return {
|
||||
name: node,
|
||||
position: createVector(next() * 128 - 64, next() * 32 - 16),
|
||||
velocity: createVector(),
|
||||
acceleration: createVector(),
|
||||
};
|
||||
};
|
||||
|
||||
const nodes: Array<Node<PhysicsPartial & { name: string }>> = [];
|
||||
|
||||
let edges: Array<Edge<PhysicsPartial>> = [];
|
||||
const updateEdges = () => {
|
||||
edges = props.graph.entries().reduce(
|
||||
(acc, [name, neighbours]) => {
|
||||
const node = nodes.find((n) => n.name === name)!;
|
||||
for (const neighbour of neighbours) {
|
||||
const n = nodes.find((node) => node.name === neighbour);
|
||||
if (n !== undefined) {
|
||||
acc.push([node, n]);
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[] as Array<Edge<PhysicsPartial>>,
|
||||
);
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
const ctx: CanvasRenderingContext2D = canvasRef.getContext("2d", {
|
||||
alpha: false,
|
||||
})!;
|
||||
|
||||
ctx.font = "18px sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.shadowBlur = 2;
|
||||
|
||||
const damping = 0.8;
|
||||
const animate = (): void => {
|
||||
for (const node of props.graph.keys()) {
|
||||
if (nodes.find((n) => n.name === node) === undefined) {
|
||||
nodes.push(enhanceNode(node));
|
||||
updateEdges();
|
||||
}
|
||||
}
|
||||
|
||||
const activeNode = props.graph
|
||||
.keys()
|
||||
.find((name) => name === currentScene.data.name);
|
||||
const activeNeighbours = getNeighbours(props.graph, activeNode!);
|
||||
|
||||
applyRepulsion(nodes);
|
||||
for (const edge of edges) {
|
||||
hookEdge(edge);
|
||||
}
|
||||
|
||||
let absV = 0;
|
||||
for (const node of nodes) {
|
||||
node.velocity.x += node.acceleration.x;
|
||||
node.velocity.y += node.acceleration.y;
|
||||
|
||||
node.velocity.x *= damping;
|
||||
node.velocity.y *= damping;
|
||||
|
||||
node.position.x += node.velocity.x;
|
||||
node.position.y += node.velocity.y;
|
||||
|
||||
node.acceleration.x = 0;
|
||||
node.acceleration.y = 0;
|
||||
|
||||
absV = Math.abs(node.velocity.x) + Math.abs(node.velocity.y);
|
||||
}
|
||||
|
||||
if (absV < 0.01) {
|
||||
ctx.restore();
|
||||
ctx.clearRect(0, 0, canvasRef.width, canvasRef.height);
|
||||
ctx.save();
|
||||
const activeRenderNode = nodes.find((n) => n.name === activeNode);
|
||||
if (!activeRenderNode) {
|
||||
setTimeout(animate, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.translate(
|
||||
width() / 2 - activeRenderNode.position.x,
|
||||
height() / 2 - activeRenderNode.position.y,
|
||||
);
|
||||
|
||||
for (const edge of edges) {
|
||||
drawEdge(edge, ctx);
|
||||
}
|
||||
|
||||
for (const node of nodes) {
|
||||
if (activeNeighbours.includes(node.name)) {
|
||||
drawNode(node, ctx, "#6572f5");
|
||||
} else {
|
||||
drawNode(node, ctx, "#404249");
|
||||
}
|
||||
}
|
||||
drawNode(activeRenderNode, ctx, "#ee459e");
|
||||
|
||||
setTimeout(animate, 100);
|
||||
} else {
|
||||
animate();
|
||||
}
|
||||
};
|
||||
animate();
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="graph-browser">
|
||||
<canvas ref={canvasRef} width={width()} height={height()}></canvas>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
.item-list {
|
||||
ul {
|
||||
background: var(--c-bg-1);
|
||||
color: var(--c-fg-1);
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-radius: 0 0 var(--sp-1) var(--sp-1);
|
||||
}
|
||||
|
||||
li {
|
||||
padding: var(--sp-1) var(--sp-2);
|
||||
border-radius: var(--sp-1);
|
||||
cursor: pointer;
|
||||
|
||||
&.active,
|
||||
&:hover {
|
||||
background: var(--c-bg-3);
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
color: inherit;
|
||||
border-radius: var(--sp-1);
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: var(--sp-1);
|
||||
margin-left: var(--sp-1);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
button:hover {
|
||||
background: var(--c-bg-3);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.item-list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--c-fg-3);
|
||||
}
|
||||
|
||||
.item-list-name {
|
||||
padding: 0 var(--sp-1);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: fit-content;
|
||||
border-radius: var(--sp-1);
|
||||
|
||||
&:hover {
|
||||
background: var(--c-bg-3);
|
||||
}
|
||||
|
||||
svg {
|
||||
margin-left: var(--sp-1);
|
||||
width: 0.875em;
|
||||
height: 0.875em;
|
||||
}
|
||||
}
|
||||
|
||||
.item-list-name.hide {
|
||||
svg {
|
||||
rotate: 0.75turn;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import "./ItemList.css";
|
||||
import {
|
||||
type Accessor,
|
||||
type Component,
|
||||
createEffect,
|
||||
createSignal,
|
||||
For,
|
||||
Match,
|
||||
Show,
|
||||
type Signal,
|
||||
Switch,
|
||||
} from "solid-js";
|
||||
import { ArrowNarrowDownIcon } from "./icons/ArrowNarrowDown.tsx";
|
||||
import { ArrowNarrowUpIcon } from "./icons/ArrowNarrowUp.tsx";
|
||||
import { ArrowsSortIcon } from "./icons/ArrowsSort.tsx";
|
||||
import { ChevronDownIcon } from "./icons/ChevronDown.tsx";
|
||||
|
||||
export type ListItem = { label: string; isActive: boolean };
|
||||
|
||||
export type ItemsRef = Array<{ item: ListItem; index: number }>;
|
||||
|
||||
export type ItemHandler = (items: ItemsRef) => void;
|
||||
|
||||
export type Direction = "default" | "asc" | "desc";
|
||||
|
||||
function mapActiveItem(items: ItemsRef, index: number): ItemsRef {
|
||||
return items.map((ref, idx) => ({
|
||||
item: {
|
||||
...ref.item,
|
||||
isActive: idx === index,
|
||||
},
|
||||
index: ref.index,
|
||||
}));
|
||||
}
|
||||
|
||||
function useDirection(
|
||||
signal: Signal<Direction>,
|
||||
): [Accessor<Direction>, (forcedDirection?: Direction) => Direction] {
|
||||
const [direction, setDirection] = signal;
|
||||
|
||||
return [
|
||||
direction,
|
||||
(forcedDirection) => {
|
||||
if (forcedDirection) {
|
||||
setDirection(forcedDirection);
|
||||
return direction();
|
||||
}
|
||||
|
||||
switch (direction()) {
|
||||
case "asc": {
|
||||
setDirection("desc");
|
||||
break;
|
||||
}
|
||||
case "desc": {
|
||||
setDirection("default");
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
setDirection("asc");
|
||||
}
|
||||
}
|
||||
|
||||
return direction();
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function createItemsRef(items: Array<ListItem>): ItemsRef {
|
||||
return items.map((item, index) => ({
|
||||
item,
|
||||
index,
|
||||
}));
|
||||
}
|
||||
|
||||
export const ItemList: Component<{
|
||||
name: string;
|
||||
items: Accessor<string[]>;
|
||||
handler: ItemHandler;
|
||||
}> = (props) => {
|
||||
const [showList, setShowList] = createSignal(true);
|
||||
const [items, setItems] = createSignal(
|
||||
createItemsRef(props.items().map((i) => ({ label: i, isActive: false }))),
|
||||
);
|
||||
const [direction, toggleDirection] = useDirection(
|
||||
createSignal<Direction>("default"),
|
||||
);
|
||||
|
||||
createEffect(() => {
|
||||
sortItems(
|
||||
createItemsRef(props.items().map((i) => ({ label: i, isActive: false }))),
|
||||
direction(),
|
||||
);
|
||||
});
|
||||
|
||||
const sortItems = (items: ItemsRef, direction: Direction) => {
|
||||
const newItems = [...items];
|
||||
if (direction === "default") {
|
||||
newItems.sort((a, b) => a.index - b.index);
|
||||
setItems(newItems);
|
||||
return;
|
||||
}
|
||||
|
||||
newItems.sort((a, b) => {
|
||||
const labelA = a.item.label.toLocaleLowerCase();
|
||||
const labelB = b.item.label.toLocaleLowerCase();
|
||||
|
||||
if (labelA < labelB) {
|
||||
return direction === "asc" ? -1 : 1;
|
||||
}
|
||||
|
||||
if (labelA > labelB) {
|
||||
return direction === "asc" ? 1 : -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
setItems(newItems);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="item-list">
|
||||
<div class="item-list-header">
|
||||
<div
|
||||
class="item-list-name"
|
||||
classList={{
|
||||
hide: !showList(),
|
||||
}}
|
||||
role="button"
|
||||
onClick={() => setShowList(!showList())}
|
||||
>
|
||||
{props.name}
|
||||
<ChevronDownIcon />
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
sortItems(items(), toggleDirection());
|
||||
}}
|
||||
>
|
||||
<Switch fallback={<ArrowsSortIcon />}>
|
||||
<Match when={direction() === "asc"}>
|
||||
<ArrowNarrowUpIcon />
|
||||
</Match>
|
||||
<Match when={direction() === "desc"}>
|
||||
<ArrowNarrowDownIcon />
|
||||
</Match>
|
||||
</Switch>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Show when={showList()}>
|
||||
<ul>
|
||||
<For each={items()}>
|
||||
{(ref, index) => {
|
||||
return (
|
||||
<li
|
||||
classList={{
|
||||
active: ref.item.isActive,
|
||||
}}
|
||||
onClick={() => {
|
||||
const newItems = mapActiveItem(items(), index());
|
||||
setItems(newItems);
|
||||
props.handler(newItems);
|
||||
}}
|
||||
>
|
||||
{ref.item.label}
|
||||
</li>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</ul>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
.input-field {
|
||||
width: 100%;
|
||||
display: block;
|
||||
background: var(--c-bg-2);
|
||||
color: var(--c-fg-1);
|
||||
border-radius: var(--sp-1);
|
||||
}
|
||||
|
||||
.input-field input {
|
||||
width: 100%;
|
||||
height: 2em;
|
||||
font-size: medium;
|
||||
margin: 0;
|
||||
padding: var(--sp-1);
|
||||
border: none;
|
||||
background: var(--c-bg-2);
|
||||
color: inherit;
|
||||
border-radius: var(--sp-1);
|
||||
}
|
||||
|
||||
.input-field.icon {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
|
||||
svg {
|
||||
padding: var(--sp-1);
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
&:has(:focus) {
|
||||
outline-width: 2px;
|
||||
outline-style: solid;
|
||||
outline-color: var(--c-primary);
|
||||
|
||||
input {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import "./InputField.css";
|
||||
import type { Component, JSX } from "solid-js";
|
||||
|
||||
export const InputField: Component<{
|
||||
name: string;
|
||||
value: string;
|
||||
type?: string;
|
||||
icon?: JSX.Element;
|
||||
children?: JSX.Element;
|
||||
onChange?: (value: string) => void;
|
||||
onInput?: (value: string) => void;
|
||||
}> = (props) => (
|
||||
<label class="input-field" classList={{ icon: !!props.icon }}>
|
||||
{props.children}
|
||||
{props.icon}
|
||||
<input
|
||||
type={props.type ?? "text"}
|
||||
name={props.name}
|
||||
value={props.value}
|
||||
onChange={(e) => props.onChange?.(e.target.value)}
|
||||
onInput={(e) => props.onInput?.(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
@@ -0,0 +1,69 @@
|
||||
.range-slider {
|
||||
width: 100%;
|
||||
color: var(--c-fg-1);
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
font-family: var(--font-sans);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.range-slider [type="range"] {
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
border-radius: var(--sp-2);
|
||||
height: var(--sp-3);
|
||||
flex: auto;
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&::-webkit-slider-runnable-track {
|
||||
width: 100%;
|
||||
height: var(--sp-2);
|
||||
border-radius: var(--sp-3);
|
||||
color: transparent;
|
||||
background: var(--c-bg-2);
|
||||
border: none;
|
||||
}
|
||||
&::-webkit-slider-thumb {
|
||||
height: var(--sp-3);
|
||||
width: var(--sp-3);
|
||||
border-radius: var(--sp-3);
|
||||
border: none;
|
||||
background: var(--c-fg-1);
|
||||
margin-top: calc(var(--sp-1) * -1);
|
||||
--webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
&:focus::-webkit-slider-thumb {
|
||||
outline-width: 2px;
|
||||
outline-style: solid;
|
||||
outline-color: var(--c-primary);
|
||||
}
|
||||
|
||||
&::-moz-range-track {
|
||||
width: 100%;
|
||||
height: var(--sp-2);
|
||||
border-radius: var(--sp-3);
|
||||
color: transparent;
|
||||
background: var(--c-bg-2);
|
||||
border: none;
|
||||
}
|
||||
&::-moz-range-thumb {
|
||||
height: var(--sp-3);
|
||||
width: var(--sp-3);
|
||||
border-radius: var(--sp-3);
|
||||
border: none;
|
||||
background: var(--c-fg-1);
|
||||
}
|
||||
&:focus::-moz-range-thumb {
|
||||
outline-width: 2px;
|
||||
outline-style: solid;
|
||||
outline-color: var(--c-primary);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import "./RangeSlider.css";
|
||||
import type { Component, JSX } from "solid-js";
|
||||
|
||||
export const RangeSlider: Component<{
|
||||
name: string;
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
value: number;
|
||||
children?: JSX.Element;
|
||||
onChange?: (value: number) => void;
|
||||
onInput?: (value: number) => void;
|
||||
}> = (props) => (
|
||||
<label class="range-slider">
|
||||
{props.children}
|
||||
<input
|
||||
type="range"
|
||||
name={props.name}
|
||||
min={props.min}
|
||||
max={props.max}
|
||||
step={props.step}
|
||||
value={props.value}
|
||||
onChange={(e) => props.onChange?.(e.target.valueAsNumber)}
|
||||
onInput={(e) => props.onInput?.(e.target.valueAsNumber)}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { BrowserFolder } from "./FileBrowser.tsx";
|
||||
|
||||
export async function fetchBrowserData(dir: string): Promise<BrowserFolder> {
|
||||
const indexRef: string[] = await (await fetch(`/api/${dir}`)).json();
|
||||
|
||||
const scenes = indexRef.reduce((root, entry) => {
|
||||
const [name] = entry.substring(1).split("/");
|
||||
root.set(name, { path: name, isActive: false });
|
||||
|
||||
return root;
|
||||
}, new Map() as BrowserFolder);
|
||||
|
||||
return new Map([[dir, scenes]]);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export const ArrowAutofitHeightIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 20H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h6m6 10v7m0-18v7m-3 8l3 3l3-3M15 6l3-3l3 3"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
export const ArrowAutofitWidthIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 12V6a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v6m-10 6H3m18 0h-7m-8-3l-3 3l3 3m12-6l3 3l-3 3"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
export const ArrowNarrowDownIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 5v14m4-4l-4 4m-4-4l4 4"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
export const ArrowNarrowUpIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 5v14m4-10l-4-4M8 9l4-4"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
export const ArrowsSortIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m3 9l4-4l4 4M7 5v14m14-4l-4 4l-4-4m4 4V5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
export const ChevronDownIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m6 9l6 6l6-6"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,19 @@
|
||||
export const FileIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||
<path d="M17 21H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7l5 5v11a2 2 0 0 1-2 2"></path>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,20 @@
|
||||
export const FileDeltaIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||
<path d="M17 21H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7l5 5v11a2 2 0 0 1-2 2"></path>
|
||||
<path d="M9 17h6l-3-6z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,20 @@
|
||||
export const FileMusicIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||
<path d="M17 21H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7l5 5v11a2 2 0 0 1-2 2"></path>
|
||||
<path d="M10 16a1 1 0 1 0 2 0a1 1 0 1 0-2 0m2 0v-5l2 1"></path>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,20 @@
|
||||
export const FilePencilIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||
<path d="M17 21H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7l5 5v11a2 2 0 0 1-2 2"></path>
|
||||
<path d="m10 18l5-5a1.414 1.414 0 0 0-2-2l-5 5v2z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,20 @@
|
||||
export const FileUnknownIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||
<path d="M17 21H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7l5 5v11a2 2 0 0 1-2 2m-5-4v.01"></path>
|
||||
<path d="M12 14a1.5 1.5 0 1 0-1.14-2.474"></path>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,19 @@
|
||||
export const FloppyDiscIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M6 4h10l4 4v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2"></path>
|
||||
<path d="M10 14a2 2 0 1 0 4 0a2 2 0 1 0-4 0m4-10v4H8V4"></path>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
export const FolderIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 4h4l3 3h7a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
export const FolderOpenIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m5 19l2.757-7.351A1 1 0 0 1 8.693 11H21a1 1 0 0 1 .986 1.164l-.996 5.211A2 2 0 0 1 19.026 19za2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h4l3 3h7a2 2 0 0 1 2 2v2"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,20 @@
|
||||
export const LassoIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M4.028 13.252C3.371 12.28 3 11.174 3 10c0-3.866 4.03-7 9-7s9 3.134 9 7s-4.03 7-9 7c-1.913 0-3.686-.464-5.144-1.255"></path>
|
||||
<path d="M3 15a2 2 0 1 0 4 0a2 2 0 1 0-4 0"></path>
|
||||
<path d="M5 17c0 1.42.316 2.805 1 4"></path>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,20 @@
|
||||
export const LassoPolygonIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M4.028 13.252L3 10l2-7l7 5l8-3l1 9l-9 3l-5.144-1.255"></path>
|
||||
<path d="M3 15a2 2 0 1 0 4 0a2 2 0 1 0-4 0"></path>
|
||||
<path d="M5 17c0 1.42.316 2.805 1 4"></path>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
export const LayoutGridIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1zm10 0a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1zM4 15a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1zm10 0a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1z"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
export const LetterXIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m7 4l10 16m0-16L7 20"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
export const LetterYIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m7 4l5 9l5-9m-5 9v7"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
export const PencilIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 20h4L18.5 9.5a2.828 2.828 0 1 0-4-4L4 16zm9.5-13.5l4 4"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
export const PolygonIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 5a2 2 0 1 0 4 0a2 2 0 1 0-4 0m7 3a2 2 0 1 0 4 0a2 2 0 1 0-4 0M3 11a2 2 0 1 0 4 0a2 2 0 1 0-4 0m10 8a2 2 0 1 0 4 0a2 2 0 1 0-4 0M6.5 9.5l3.5-3m4-1L17 7m1.5 3L16 17m-2.5.5l-7-5"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
export const RefreshIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M20 11A8.1 8.1 0 0 0 4.5 9M4 5v4h4m-4 4a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
export const RefreshAlertIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M20 11A8.1 8.1 0 0 0 4.5 9M4 5v4h4m-4 4a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4m-4-6v3m0 3h.01"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
export const RefreshDotIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M20 11A8.1 8.1 0 0 0 4.5 9M4 5v4h4m-4 4a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4m-5-3a1 1 0 1 0 2 0a1 1 0 1 0-2 0"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,19 @@
|
||||
export const ReloadIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
|
||||
<path d="M20 4v5h-5" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
export const ScriptIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M17 20H6a3 3 0 0 1 0-6h11a3 3 0 0 0 0 6h1a3 3 0 0 0 3-3V6a2 2 0 0 0-2-2H9a2 2 0 0 0-2 2v8"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,19 @@
|
||||
export const StackBackwardIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="m14 12l6-3l-8-4l-8 4l6 3"></path>
|
||||
<path fill="currentColor" d="m10 12l-6 3l8 4l8-4l-6-3l-2 1z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,19 @@
|
||||
export const StackForwardIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path fill="currentColor" d="M12 5L4 9l8 4l8-4z"></path>
|
||||
<path d="m10 12l-6 3l8 4l8-4l-6-3"></path>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
export const TransformPointIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1zm0 14a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1zM17 4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1zm0 14a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1zM11 5h2m-8 6v2m14-2v2m-8 6h2"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
export const ZoomScanIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 8V6a2 2 0 0 1 2-2h2M4 16v2a2 2 0 0 0 2 2h2m8-16h2a2 2 0 0 1 2 2v2m-4 12h2a2 2 0 0 0 2-2v-2M8 11a3 3 0 1 0 6 0a3 3 0 0 0-6 0m8 5l-2.5-2.5"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,141 @@
|
||||
.dev-tools {
|
||||
--font-sans:
|
||||
ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
|
||||
"Segoe UI Symbol", "Noto Color Emoji";
|
||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-mono:
|
||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||
"Courier New", monospace;
|
||||
|
||||
--rgb-blurple: 101, 114, 245;
|
||||
--c-blurple: rgb(var(--rgb-blurple));
|
||||
|
||||
--rgb-fuchsia: 238, 69, 158;
|
||||
--c-fuchsia: rgb(var(--rgb-fuchsia));
|
||||
|
||||
--rgb-green: 35, 165, 89;
|
||||
--c-green: rgb(var(--rgb-green));
|
||||
|
||||
--rgb-yellow: 240, 178, 50;
|
||||
--c-yellow: rgb(var(--rgb-yellow));
|
||||
|
||||
--rgb-orange: 255, 144, 0;
|
||||
--c-orange: rgb(var(--rgb-orange));
|
||||
|
||||
--rgb-red: 218, 55, 60;
|
||||
--c-red: rgb(var(--rgb-red));
|
||||
|
||||
--rgb-cyan: 95, 205, 228;
|
||||
--c-cyan: rgb(var(--rgb-cyan));
|
||||
|
||||
--rgb-pink: 246, 42, 245;
|
||||
--c-pink: rgb(var(--rgb-pink));
|
||||
|
||||
--rgb-gray: 117, 126, 138;
|
||||
--c-gray: rgb(var(--rgb-gray));
|
||||
|
||||
--rgb-white: 255, 255, 255;
|
||||
--c-white: rgb(var(--rgb-white));
|
||||
|
||||
--rgb-black: 30, 31, 34;
|
||||
--c-black: rgb(var(--rgb-black));
|
||||
|
||||
--c-bg-0: rgb(30, 31, 34);
|
||||
--c-bg-1: rgb(43, 45, 49);
|
||||
--c-bg-2: rgb(49, 51, 56);
|
||||
--c-bg-3: rgb(64, 66, 73);
|
||||
|
||||
--c-fg-0: rgb(255 255 225);
|
||||
--c-fg-1: rgb(219 222 225);
|
||||
--c-fg-2: rgb(181 186 193);
|
||||
--c-fg-3: rgb(113 116 120);
|
||||
|
||||
--sp-1: 4px;
|
||||
--sp-2: 8px;
|
||||
--sp-3: 16px;
|
||||
--sp-4: 24px;
|
||||
--sp-5: 32px;
|
||||
--sp-6: 48px;
|
||||
|
||||
height: 100%;
|
||||
min-width: 1px;
|
||||
max-width: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
order: -1;
|
||||
font-size: medium;
|
||||
font-family: var(--font-sans);
|
||||
|
||||
*,
|
||||
*::after,
|
||||
*::before {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
*:focus {
|
||||
outline-width: 2px;
|
||||
outline-style: solid;
|
||||
outline-color: var(--c-blurple);
|
||||
}
|
||||
}
|
||||
|
||||
.tool-bar {
|
||||
overflow: hidden;
|
||||
border-right: 1px solid var(--c-fg-3);
|
||||
background: var(--c-bg-0);
|
||||
color: var(--c-fg-1);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: var(--sp-2);
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
gap: var(--sp-2);
|
||||
}
|
||||
|
||||
.tool-bar-resize {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -3px;
|
||||
width: 5px;
|
||||
height: 100%;
|
||||
cursor: ew-resize;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.zoom-bar {
|
||||
display: flex;
|
||||
gap: var(--sp-2);
|
||||
|
||||
button {
|
||||
color: inherit;
|
||||
border-radius: var(--sp-1);
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
button:hover {
|
||||
background: var(--c-bg-3);
|
||||
}
|
||||
}
|
||||
|
||||
.scene-props {
|
||||
display: flex;
|
||||
gap: var(--sp-2);
|
||||
flex-flow: row wrap;
|
||||
|
||||
& > :not(:first-child) {
|
||||
width: calc(50% - var(--sp-1));
|
||||
flex-basis: calc(50% - var(--sp-1));
|
||||
}
|
||||
}
|
||||
|
||||
.entity-props {
|
||||
display: grid;
|
||||
gap: var(--sp-2);
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
import "./main.css";
|
||||
import {
|
||||
type Component,
|
||||
createEffect,
|
||||
createSignal,
|
||||
onMount,
|
||||
Show,
|
||||
} from "solid-js";
|
||||
import { render } from "solid-js/web";
|
||||
import { spriteMap } from "#/engine/Animation.ts";
|
||||
import type { Entity } from "#/engine/Entity.ts";
|
||||
import {
|
||||
currentScene,
|
||||
type SceneData,
|
||||
sceneChangedHandlers,
|
||||
sceneGraph,
|
||||
sceneProcessMap,
|
||||
setScene,
|
||||
} from "#/engine/Scene.ts";
|
||||
import { setScale, viewport } from "#/engine/Viewport.ts";
|
||||
import { InputField } from "./controls/InputField.tsx";
|
||||
import { RangeSlider } from "./controls/RangeSlider.tsx";
|
||||
import { GraphBrowser } from "./GraphBrowser.tsx";
|
||||
import { ItemList, type ItemsRef } from "./ItemList.tsx";
|
||||
import { ArrowAutofitHeightIcon } from "./icons/ArrowAutofitHeight.tsx";
|
||||
import { ArrowAutofitWidthIcon } from "./icons/ArrowAutofitWidth.tsx";
|
||||
import { FileIcon } from "./icons/File.tsx";
|
||||
import { LetterXIcon } from "./icons/LetterX.tsx";
|
||||
import { LetterYIcon } from "./icons/LetterY.tsx";
|
||||
import { ZoomScanIcon } from "./icons/ZoomScan.tsx";
|
||||
|
||||
async function fetchScenes(): Promise<string[]> {
|
||||
const indexRef: string[] = await (await fetch(`/api/scenes`)).json();
|
||||
const items = indexRef.filter((value) => value.includes("scene"));
|
||||
|
||||
return items.map((i) => i.substring(1).split("/")[0]);
|
||||
}
|
||||
|
||||
function mapSceneEntities(sceneData: SceneData): string[] {
|
||||
return sceneData.entities.map((entity) => entity.name);
|
||||
}
|
||||
|
||||
function adjustGameContainer(gameContainer: HTMLElement, width: number): void {
|
||||
gameContainer.style = `position: relative; top: 0; left: ${width}px; width: ${document.body.offsetWidth - width}px;`;
|
||||
}
|
||||
|
||||
const scenesRaw = await fetchScenes();
|
||||
|
||||
const DevTools: Component<{ gameContainer: HTMLElement }> = ({
|
||||
gameContainer,
|
||||
}) => {
|
||||
const [width, setWidth] = createSignal(320);
|
||||
let isResizing = false;
|
||||
|
||||
const startResizing = (): void => {
|
||||
isResizing = true;
|
||||
};
|
||||
|
||||
const stopResizing = (): void => {
|
||||
isResizing = false;
|
||||
};
|
||||
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
if (isResizing) {
|
||||
setWidth(event.clientX);
|
||||
self.dispatchEvent(new Event("resize"));
|
||||
}
|
||||
};
|
||||
|
||||
adjustGameContainer(gameContainer, width());
|
||||
|
||||
const [currentScale, setCurrentScale] = createSignal(viewport.scale);
|
||||
const [sceneData, setSceneData] = createSignal(currentScene.data);
|
||||
const [scenes] = createSignal(scenesRaw);
|
||||
const [entities, setEntities] = createSignal(
|
||||
mapSceneEntities(currentScene.data),
|
||||
);
|
||||
const [activeEntity, setActiveEntity] = createSignal<null | Entity<unknown>>(
|
||||
null,
|
||||
);
|
||||
|
||||
createEffect(() => {
|
||||
setScale(currentScale());
|
||||
});
|
||||
|
||||
const setCurrentScene = async (items: ItemsRef): Promise<void> => {
|
||||
const name = items.find((ref) => ref.item.isActive)!.item.label;
|
||||
await setScene(name);
|
||||
};
|
||||
|
||||
sceneChangedHandlers.add((data) => {
|
||||
setActiveEntity(null);
|
||||
setSceneData(data);
|
||||
setEntities(mapSceneEntities(data));
|
||||
});
|
||||
|
||||
const selectEntity = (items: ItemsRef): void => {
|
||||
const activeRef = items.find((ref) => ref.item.isActive)!;
|
||||
const instances = currentScene.entityInstanceMap.get(
|
||||
activeRef.item.label,
|
||||
)!.instances;
|
||||
|
||||
const entityRefs = items.filter(
|
||||
(ref) => ref.item.label === activeRef.item.label,
|
||||
);
|
||||
const instance =
|
||||
instances[entityRefs.findIndex((ref) => ref.item.isActive)];
|
||||
|
||||
setActiveEntity(instance);
|
||||
const sprite = spriteMap.get(instance.animation.spriteSrc)!;
|
||||
|
||||
const processProxy = sceneProcessMap.get(currentScene.data.name)!;
|
||||
const ctx = viewport.ctx;
|
||||
currentScene.process = (...args) => {
|
||||
processProxy(...args);
|
||||
|
||||
ctx.save();
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeStyle = "#6572f5";
|
||||
viewport.ctx.strokeRect(
|
||||
instance.position.x - 0.5,
|
||||
instance.position.y - 0.5,
|
||||
sprite.image.width / sprite.xFrames + 1,
|
||||
sprite.image.height / sprite.yFrames + 1,
|
||||
);
|
||||
|
||||
ctx.fillStyle = "#ee459e";
|
||||
viewport.ctx.fillRect(
|
||||
instance.position.x - 1,
|
||||
instance.position.y - 1,
|
||||
2,
|
||||
2,
|
||||
);
|
||||
ctx.restore();
|
||||
};
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
self.addEventListener("resize", () => {
|
||||
adjustGameContainer(gameContainer, width());
|
||||
});
|
||||
self.dispatchEvent(new Event("resize"));
|
||||
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseup", stopResizing);
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="dev-tools" style={{ width: `${width()}px` }}>
|
||||
<div class="tool-bar">
|
||||
<div class="zoom-bar">
|
||||
<button type="button" onClick={() => setCurrentScale(4)}>
|
||||
<ZoomScanIcon />
|
||||
</button>
|
||||
<RangeSlider
|
||||
name="scale"
|
||||
min={1}
|
||||
max={8}
|
||||
step={1}
|
||||
value={currentScale()}
|
||||
onInput={(value) => setCurrentScale(value)}
|
||||
></RangeSlider>
|
||||
<span>{currentScale()}</span>
|
||||
</div>
|
||||
|
||||
<div class="scene-props">
|
||||
<InputField
|
||||
name="file"
|
||||
value={sceneData().name}
|
||||
icon={FileIcon()}
|
||||
></InputField>
|
||||
<InputField
|
||||
type="number"
|
||||
name="width"
|
||||
value={String(sceneData().width)}
|
||||
icon={ArrowAutofitWidthIcon()}
|
||||
onChange={(value) => {
|
||||
currentScene.data.width = parseInt(value);
|
||||
self.dispatchEvent(new Event("resize"));
|
||||
}}
|
||||
></InputField>
|
||||
<InputField
|
||||
type="number"
|
||||
name="height"
|
||||
value={String(sceneData().height)}
|
||||
icon={ArrowAutofitHeightIcon()}
|
||||
onChange={(value) => {
|
||||
currentScene.data.height = parseInt(value);
|
||||
self.dispatchEvent(new Event("resize"));
|
||||
}}
|
||||
></InputField>
|
||||
</div>
|
||||
|
||||
<ItemList name="Scenes" handler={setCurrentScene} items={scenes} />
|
||||
<GraphBrowser graph={sceneGraph} seed={2802446152} />
|
||||
|
||||
<ItemList name="Entities" handler={selectEntity} items={entities} />
|
||||
|
||||
<Show when={activeEntity() !== null}>
|
||||
<div class="entity-props">
|
||||
<InputField
|
||||
type="number"
|
||||
name="x"
|
||||
value={String(activeEntity()!.position.x)}
|
||||
icon={LetterXIcon()}
|
||||
onChange={(value) => {
|
||||
activeEntity()!.position.x = parseInt(value);
|
||||
}}
|
||||
></InputField>
|
||||
<InputField
|
||||
type="number"
|
||||
name="y"
|
||||
value={String(activeEntity()!.position.y)}
|
||||
icon={LetterYIcon()}
|
||||
onChange={(value) => {
|
||||
activeEntity()!.position.y = parseInt(value);
|
||||
}}
|
||||
></InputField>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="tool-bar-resize" onMouseDown={startResizing}></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function use(container: HTMLElement): void {
|
||||
render(() => <DevTools gameContainer={container} />, document.body);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { readdirSync, writeFileSync } from "node:fs";
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import { root } from "../../../vite.config.ts";
|
||||
|
||||
function buildBrowserIndex(dir: string): string[] {
|
||||
const dirents = readdirSync(dir, {
|
||||
withFileTypes: true,
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
return dirents
|
||||
.filter((dirent) => dirent.isFile())
|
||||
.map((dirent) => {
|
||||
const path = dirent.parentPath.substring(dir.length - 1);
|
||||
|
||||
return `${path}/${dirent.name}`;
|
||||
});
|
||||
}
|
||||
|
||||
function getScenes(req: IncomingMessage, res: ServerResponse) {
|
||||
const sceneData = buildBrowserIndex(`${root}/game/scenes`);
|
||||
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify(sceneData));
|
||||
}
|
||||
|
||||
function get(req: IncomingMessage, res: ServerResponse): void {
|
||||
const match = req.url?.match(/^\/(\w+)$/);
|
||||
if (!match) {
|
||||
console.error(req.url);
|
||||
res.statusCode = 400;
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (req.url) {
|
||||
case "/scenes": {
|
||||
getScenes(req, res);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function post(req: IncomingMessage, res: ServerResponse): void {
|
||||
// let body = "";
|
||||
// req.on("data", (chunk) => {
|
||||
// body += chunk.toString();
|
||||
// });
|
||||
// req.on("end", () => {
|
||||
// const data: SceneData = JSON.parse(body);
|
||||
// writeFileSync(
|
||||
// `${dev.scenesDir}/${data.name}.json`,
|
||||
// JSON.stringify(data, null, 2),
|
||||
// "utf8",
|
||||
// );
|
||||
// res.statusCode = 204;
|
||||
// res.end();
|
||||
// });
|
||||
}
|
||||
|
||||
export default function (req: IncomingMessage, res: ServerResponse): void {
|
||||
switch (req.method) {
|
||||
case "GET": {
|
||||
get(req, res);
|
||||
break;
|
||||
}
|
||||
case "POST": {
|
||||
post(req, res);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user