attempt at nodes

This commit is contained in:
Sam Lavigne
2023-08-22 21:57:53 -04:00
parent 73f3c57690
commit 473b46edf2
13 changed files with 888 additions and 296 deletions

View File

@ -1,6 +1,6 @@
<script>
import { onMount } from "svelte";
import { inputs, addInput, output, filters } from "./stores.js";
import { addNode, nodes, inputs, output, filters, previewCommand } from "./stores.js";
import Input from "./Input.svelte";
import Output from "./Output.svelte";
import Filter from "./Filter.svelte";
@ -26,7 +26,7 @@
let commandRef;
function newInput() {
addInput("punch.mp4");
addNode({ name: "punch.mp4" }, "input");
}
function render() {
@ -81,38 +81,7 @@
ffmpegLoaded = true;
}
function updateCommand() {
console.log($inputs);
const cInputs = $inputs.map((i) => `-i ${i.name}`).join(" ");
const cOutput = $output;
const cFilters = $filters.map(makeFilterArgs).join(",");
let out = `ffmpeg ${cInputs}`;
if (cFilters) out += ` -filter_complex "${cFilters}"`;
out += ` ${cOutput}`;
command = out;
return out;
}
function makeFilterArgs(f) {
let fCommand = f.name;
if (f.params && f.params.length > 0) {
let params = f.params
.map((p) => {
if (p.value === "" || p.value === null || p.value === p.default) return null;
return `${p.name}=${p.value}`;
})
.filter((p) => p !== null)
.join(":");
if (params) fCommand += "=" + params;
}
return fCommand;
}
function commandList() {
let command = [];
@ -141,9 +110,9 @@
filters.set(e.detail.items);
}
inputs.subscribe(updateCommand);
output.subscribe(updateCommand);
filters.subscribe(updateCommand);
// inputs.subscribe(updateCommand);
// output.subscribe(updateCommand);
// filters.subscribe(updateCommand);
onMount(async () => {
loadFFmpeg();
@ -159,16 +128,15 @@
export and add some filters, and then hit "render" to preview the output in browser. Note: this
is a work in progress, many things may still be broken! Only audio to audio and video to video
filters are included. If it hangs/crashes refresh the page. Post issues/feedback to
<a href="https://github.com/antiboredom/ffmpeg-explorer/" target="_blank"
>GitHub</a
>. By <a href="https://lav.io" target="_blank">Sam Lavigne</a>.
<a href="https://github.com/antiboredom/ffmpeg-explorer/" target="_blank">GitHub</a>. By
<a href="https://lav.io" target="_blank">Sam Lavigne</a>.
</p>
</section>
<!-- {message} -->
<section class="command">
<h3>Output Command</h3>
<div class="inner-command">
<textarea readonly class="actual-command" bind:this={commandRef}>{command}</textarea>
<textarea readonly class="actual-command" bind:this={commandRef}>{$previewCommand}</textarea>
<div>
<button on:click={copyCommand}>Copy Command</button>
</div>
@ -180,8 +148,10 @@
<h3>Inputs</h3>
<button on:click={newInput}>Add Input</button>
</div>
{#each $inputs as inp, index}
<Input bind:filename={inp.name} id={inp.id} {index} />
{#each $nodes as node, index}
{#if node.nodeType === "input"}
<Input bind:filename={node.data.name} id={node.id} {index} />
{/if}
{/each}
</section>
@ -212,7 +182,11 @@
<section class="output">
<h3>Output</h3>
<Output bind:filename={$output} />
{#each $nodes as node}
{#if node.nodeType==="output"}
<Output bind:filename={node.data.name} />
{/if}
{/each}
</section>
<section class="filters">
@ -227,18 +201,20 @@
on:consider={handleFilterSort}
on:finalize={handleFilterSort}
>
{#each $filters as f (f.id)}
<div class="filter">
<Filter bind:filter={f} />
</div>
{#each $nodes as f (f.id)}
{#if f.nodeType === "filter"}
<div class="filter">
<Filter bind:filter={f.data} />
</div>
{/if}
{/each}
</div>
</div>
</section>
<section class="graph">
<Graph />
</section>
<section class="graph">
<Graph />
</section>
</main>
<style>

View File

@ -1,5 +1,5 @@
<script>
import { filters, removeFilter } from "./stores.js";
import { filters, removeNode } from "./stores.js";
export let filter = {
name: "",
@ -10,7 +10,7 @@
let show = false;
function remove() {
removeFilter(filter.id);
removeNode(filter.id);
}
function reset() {

View File

@ -1,7 +1,7 @@
<script>
import uFuzzy from "@leeoniya/ufuzzy";
import FILTERS from "./filters.json";
import { addFilter } from "./stores.js";
import { addNode } from "./stores.js";
export let select = "video";
$: selectedFilters = selectFilters(select);
@ -25,7 +25,7 @@
}
function add(f) {
addFilter(f);
addNode(f, "filter");
}
function update() {

View File

@ -1,201 +1,94 @@
<script>
import { inputs, output, filters } from "./stores.js";
import { Anchor, Node, Svelvet, Minimap, Controls } from "svelvet";
import { nodes, edges, command } from "./stores.js";
import { SvelteFlow, Controls, Background, BackgroundVariant, MiniMap } from "@xyflow/svelte";
import Node from "./nodes/Node.svelte";
// let nodes = [];
// for (let inp of $inputs) {
// nodes.push({item: inp});
// }
//
// for (let filter of $filters) {
// nodes.push({item: filter});
// }
let edges = [];
let command = '';
const nodeTypes = {
ffmpeg: Node,
};
function makeCommand() {
for (let e of edges) {
const [from, to] = e;
const [fromAnchorId, fromNodeId] = from.split("/");
const [toAnchorId, toNodeId] = to.split("/");
}
console.log($nodes);
// const nodes = writable([
// {
// id: "1",
// type: "ffmpeg",
// data: { label: "test.mp4", inputs: [], outputs: ["v", "a"] },
// position: { x: 0, y: 0 },
// },
// {
// id: "2",
// type: "ffmpeg",
// data: { label: "filter", inputs: ["v"], outputs: ["v"] },
// position: { x: 0, y: 150 },
// },
// {
// id: "3",
// type: "ffmpeg",
// data: { label: "output.mp4", inputs: ["v", "a"], outputs: [] },
// position: { x: 100, y: 20 },
// },
// ]);
// same for edges
// const edges = writable([
// {
// id: "1-2",
// type: "default",
// source: "1",
// sourceHandle: "v_0",
// targetHandle: "v_0",
// target: "2",
// label: "Edge Text",
// },
// {
// id: "2-3",
// type: "default",
// source: "2",
// target: "3",
// sourceHandle: "v_0",
// targetHandle: "v_0",
// label: "Edge Text",
// },
// ]);
const defaultEdgeOptions = {
deletable: true,
};
function onEdgeUpdate(e) {
console.log(e);
}
function onEdgeUpdateStart(e) {
console.log(e);
}
function onEdgeUpdateEnd(e) {
console.log(e);
}
function onMoveStart(e) {
console.log(e);
}
function onConnect(e){
console.log('connect', e);
}
function countInputs(f) {
const [ins, outs] = f.type.split("->");
if (ins == "N") return 1;
return ins.length;
}
function countCons(f) {
const [ins, outs] = f.type.split("->");
return { in: ins.split(""), out: outs.split("") };
}
function countOutputs(f) {
const [ins, outs] = f.type.split("->");
if (outs == "N") return 1;
return outs.length;
}
function onConnect(e) {
const sourceAnchor = e.detail.sourceAnchor;
const targetAnchor = e.detail.targetAnchor;
const sourceNode = e.detail.sourceNode;
const targetNode = e.detail.targetNode;
// console.log(e);
// console.log(sourceNode.id, "->", targetNode.id)
// console.log(sourceAnchor.id, "->", targetAnchor.id)
edges.push([sourceAnchor.id, targetAnchor.id])
edges = edges;
makeCommand();
}
function onDisconnect(e) {
const sourceAnchor = e.detail.sourceAnchor;
const targetAnchor = e.detail.targetAnchor;
const sourceNode = e.detail.sourceNode;
const targetNode = e.detail.targetNode;
// console.log(sourceNode.id, "-/>", targetNode.id)
// console.log(sourceAnchor.id, "-/>", targetAnchor.id)
const index = edges.findIndex((e) => e[0] === sourceAnchor.id && e[1] === targetAnchor.id);
edges.splice(index, 1);
edges = edges;
makeCommand();
}
</script>
<Svelvet
id="my-canvas"
width={800}
height={500}
snapTo={25}
on:disconnection={onDisconnect}
on:connection={onConnect}
>
{#each $inputs as inp, index}
<Node inputs={0} outputs={2} id={"input_" + inp.id}>
<div class="node">
<div class="header">
{inp.name}
</div>
<slot />
</div>
<div class="output-anchors">
<Anchor id={"video"} let:linked let:connecting let:hovering output>
<div class:linked class:hovering class:connecting class="anchor video">v</div>
</Anchor>
<Anchor id={"audio"} let:linked let:connecting let:hovering output>
<div class:linked class:hovering class:connecting class="anchor audio">a</div>
</Anchor>
</div>
</Node>
{/each}
{#each $filters as f, index}
<Node inputs={countInputs(f)} outputs={countOutputs(f)} id={"filter_" + f.id}>
<div class="node">
<div class="header">
{f.name}
</div>
<slot />
</div>
<div class="input-anchors">
{#each countCons(f).in as inp, index}
<Anchor id={"in_" + inp + "_" + index} let:linked let:connecting let:hovering input>
<div class:linked class:hovering class:connecting class="anchor in {inp}">{inp}</div>
</Anchor>
{/each}
</div>
<div class="output-anchors">
{#each countCons(f).out as out}
<Anchor id={"out_" + out + "_" + index} let:linked let:connecting let:hovering output>
<div class:linked class:hovering class:connecting class="anchor in {out}">{out}</div>
</Anchor>
{/each}
</div>
</Node>
{/each}
<Node inputs={2} outputs="0" id="output" position={{ x: 600, y: 250 }}>
<div class="node">
<div class="header">
{$output}
</div>
<slot />
</div>
<div class="input-anchors">
<Anchor id={"output_video"} let:linked let:connecting let:hovering input>
<div class:linked class:hovering class:connecting class="anchor video">v</div>
</Anchor>
<Anchor id={"output_audio"} let:linked let:connecting let:hovering input>
<div class:linked class:hovering class:connecting class="anchor audio">a</div>
</Anchor>
</div>
</Node>
<Controls />
</Svelvet>
<div>
{#each edges as e}
<p>
{e[0]} =======> {e[1]}
</p>
{/each}
<div style="width: 900px; height: 500px;">
<SvelteFlow
{nodeTypes}
{nodes}
{edges}
snapGrid={[10, 10]}
fitView
>
<Controls />
<Background variant={BackgroundVariant.Dots} />
</SvelteFlow>
</div>
<style>
.node {
box-sizing: border-box;
width: fit-content;
height: fit-content;
position: relative;
pointer-events: auto;
display: flex;
flex-direction: column;
padding: 10px;
gap: 10px;
background-color: #fff;
border: 1px solid var(--b1);
font: 12px Times, serif;
box-shadow: none !important;
}
.output-anchors {
position: absolute;
right: -16px;
top: 0px;
display: flex;
flex-direction: column;
gap: 2px;
}
.input-anchors {
position: absolute;
left: -16px;
top: 0px;
display: flex;
flex-direction: column;
gap: 2px;
}
.anchor {
background-color: #fff;
font-size: 12px;
text-align: center;
line-height: 12px;
width: 14px;
height: 14px;
font-family: Times, serif;
border: solid 1px white;
border-color: var(--b1);
}
.hovering {
scale: 1.2;
}
.linked {
background-color: rgb(17, 214, 17) !important;
}
.connecting {
background-color: goldenrod;
}
</style>
<div>
{JSON.stringify(command)}
</div>

201
src/Graph_old.svelte Normal file
View File

@ -0,0 +1,201 @@
<script>
import { inputs, output, filters } from "./stores.js";
import { Anchor, Node, Svelvet, Minimap, Controls } from "svelvet";
// let nodes = [];
// for (let inp of $inputs) {
// nodes.push({item: inp});
// }
//
// for (let filter of $filters) {
// nodes.push({item: filter});
// }
let edges = [];
let command = '';
function makeCommand() {
for (let e of edges) {
const [from, to] = e;
const [fromAnchorId, fromNodeId] = from.split("/");
const [toAnchorId, toNodeId] = to.split("/");
}
}
function countInputs(f) {
const [ins, outs] = f.type.split("->");
if (ins == "N") return 1;
return ins.length;
}
function countCons(f) {
const [ins, outs] = f.type.split("->");
return { in: ins.split(""), out: outs.split("") };
}
function countOutputs(f) {
const [ins, outs] = f.type.split("->");
if (outs == "N") return 1;
return outs.length;
}
function onConnect(e) {
const sourceAnchor = e.detail.sourceAnchor;
const targetAnchor = e.detail.targetAnchor;
const sourceNode = e.detail.sourceNode;
const targetNode = e.detail.targetNode;
// console.log(e);
// console.log(sourceNode.id, "->", targetNode.id)
// console.log(sourceAnchor.id, "->", targetAnchor.id)
edges.push([sourceAnchor.id, targetAnchor.id])
edges = edges;
makeCommand();
}
function onDisconnect(e) {
const sourceAnchor = e.detail.sourceAnchor;
const targetAnchor = e.detail.targetAnchor;
const sourceNode = e.detail.sourceNode;
const targetNode = e.detail.targetNode;
// console.log(sourceNode.id, "-/>", targetNode.id)
// console.log(sourceAnchor.id, "-/>", targetAnchor.id)
const index = edges.findIndex((e) => e[0] === sourceAnchor.id && e[1] === targetAnchor.id);
edges.splice(index, 1);
edges = edges;
makeCommand();
}
</script>
<Svelvet
id="my-canvas"
width={800}
height={500}
snapTo={25}
on:disconnection={onDisconnect}
on:connection={onConnect}
>
{#each $inputs as inp, index}
<Node inputs={0} outputs={2} id={"input_" + inp.id}>
<div class="node">
<div class="header">
{inp.name}
</div>
<slot />
</div>
<div class="output-anchors">
<Anchor id={"video"} let:linked let:connecting let:hovering output>
<div class:linked class:hovering class:connecting class="anchor video">v</div>
</Anchor>
<Anchor id={"audio"} let:linked let:connecting let:hovering output>
<div class:linked class:hovering class:connecting class="anchor audio">a</div>
</Anchor>
</div>
</Node>
{/each}
{#each $filters as f, index}
<Node inputs={countInputs(f)} outputs={countOutputs(f)} id={"filter_" + f.id}>
<div class="node">
<div class="header">
{f.name}
</div>
<slot />
</div>
<div class="input-anchors">
{#each countCons(f).in as inp, index}
<Anchor id={"in_" + inp + "_" + index} let:linked let:connecting let:hovering input>
<div class:linked class:hovering class:connecting class="anchor in {inp}">{inp}</div>
</Anchor>
{/each}
</div>
<div class="output-anchors">
{#each countCons(f).out as out}
<Anchor id={"out_" + out + "_" + index} let:linked let:connecting let:hovering output>
<div class:linked class:hovering class:connecting class="anchor in {out}">{out}</div>
</Anchor>
{/each}
</div>
</Node>
{/each}
<Node inputs={2} outputs="0" id="output" position={{ x: 600, y: 250 }}>
<div class="node">
<div class="header">
{$output}
</div>
<slot />
</div>
<div class="input-anchors">
<Anchor id={"output_video"} let:linked let:connecting let:hovering input>
<div class:linked class:hovering class:connecting class="anchor video">v</div>
</Anchor>
<Anchor id={"output_audio"} let:linked let:connecting let:hovering input>
<div class:linked class:hovering class:connecting class="anchor audio">a</div>
</Anchor>
</div>
</Node>
<Controls />
</Svelvet>
<div>
{#each edges as e}
<p>
{e[0]} =======> {e[1]}
</p>
{/each}
</div>
<style>
.node {
box-sizing: border-box;
width: fit-content;
height: fit-content;
position: relative;
pointer-events: auto;
display: flex;
flex-direction: column;
padding: 10px;
gap: 10px;
background-color: #fff;
border: 1px solid var(--b1);
font: 12px Times, serif;
box-shadow: none !important;
}
.output-anchors {
position: absolute;
right: -16px;
top: 0px;
display: flex;
flex-direction: column;
gap: 2px;
}
.input-anchors {
position: absolute;
left: -16px;
top: 0px;
display: flex;
flex-direction: column;
gap: 2px;
}
.anchor {
background-color: #fff;
font-size: 12px;
text-align: center;
line-height: 12px;
width: 14px;
height: 14px;
font-family: Times, serif;
border: solid 1px white;
border-color: var(--b1);
}
.hovering {
scale: 1.2;
}
.linked {
background-color: rgb(17, 214, 17) !important;
}
.connecting {
background-color: goldenrod;
}
</style>

View File

@ -1,12 +1,11 @@
<script>
import { removeInput } from "./stores.js";
import { removeNode } from "./stores.js";
export let filename = "punch.mp4";
export let id;
export let index;
function remove() {
console.log(id);
removeInput(id);
removeNode(id);
}
</script>

View File

View File

23
src/nodes/Node.svelte Normal file
View File

@ -0,0 +1,23 @@
<script lang="ts">
import { Handle, Position } from "@xyflow/svelte";
export let data = { name: "", inputs: [], outputs: [], onChange: () => {} };
</script>
<div>
{data.name}
</div>
{#each data.inputs as inp, index}
<Handle type="target" position={Position.Left} id={inp + "_" + index} style="top: {index*10}px; background-color: {inp == 'v' ? 'blue' : 'red'}" on:connect />
{/each}
{#each data.outputs as out, index}
<Handle type="source" id={out + "_" + index} position={Position.Right} style="top: {index*10}px; background-color: {out == 'v' ? 'blue' : 'red'}" on:connect />
{/each}
<!-- <Handle type="source" position={Position.Right} id="a" style="top: 10px;" on:connect /> -->
<!-- <Handle -->
<!-- type="source" -->
<!-- position={Position.Right} -->
<!-- id="b" -->
<!-- style="top: auto; bottom: 10px;" -->
<!-- on:connect -->
<!-- /> -->

View File

View File

@ -1,55 +1,148 @@
import { v4 as uuidv4 } from "uuid";
import { writable } from 'svelte/store';
import { writable, derived, get } from "svelte/store";
export const inputs = writable([{name: "punch.mp4", id: uuidv4()}]);
export const output = writable("out.mp4");
export const filters = writable([]);
// export const inputs = writable([]);
// export const output = writable("out.mp4");
// export const filters = writable([]);
export const nodes = writable([]);
export const edges = writable([]);
export function addFilter(f) {
const newFilter = { ...f, filterId: f.id, id: uuidv4() };
if (f.params) {
newFilter.params = f.params.map((p) => {
p.value = null;
if (p.default != null) p.value = p.default;
return p;
});
}
filters.update((filts) => {
filts.push(newFilter)
return filts;
})
addNode({ name: "punch.mp4" }, "input");
addNode({ name: "out.mp4" }, "output");
function makeFilterArgs(f) {
let fCommand = f.name;
if (f.params && f.params.length > 0) {
let params = f.params
.map((p) => {
if (p.value === "" || p.value === null || p.value === p.default) return null;
return `${p.name}=${p.value}`;
})
.filter((p) => p !== null)
.join(":");
if (params) fCommand += "=" + params;
}
return fCommand;
}
export function removeFilter(id) {
filters.update((filts) => {
const index = filts.findIndex((f) => f.id === id);
filts.splice(index, 1);
return filts;
});
export const command = derived(edges, ($edges) => {
console.log($edges);
return $edges;
});
export const previewCommand = derived(nodes, ($nodes) => {
const cInputs = $nodes
.filter((n) => n.nodeType === "input")
.map((i) => `-i ${i.data.name}`)
.join(" ");
const cOutput = $nodes
.filter((n) => n.nodeType === "output")
.map((i) => `${i.data.name}`)
.join(" ");
const cFilters = $nodes
.filter((n) => n.nodeType === "filter")
.map((n) => n.data)
.map(makeFilterArgs)
.join(",");
let out = `ffmpeg ${cInputs}`;
if (cFilters) out += ` -filter_complex "${cFilters}"`;
out += ` ${cOutput}`;
return out;
});
export const inputs = derived(nodes, ($nodes) => {
return $nodes.filter((n) => n.nodeType === "input").map((n) => n.data);
});
export const filters = derived(nodes, ($nodes) => {
return $nodes.filter((n) => n.nodeType === "filter").map((n) => n.data);
});
export const output = derived(nodes, ($nodes) => {
return $nodes.filter((n) => n.nodeType === "output").map((n) => n.data);
});
export function addNode(data, type) {
let ins = [];
let outs = [];
if (type === "input") {
outs = ["v", "a"];
} else if (type === "output") {
ins = ["v", "a"];
} else if (type === "filter") {
const [_ins, _outs] = data.type.split("->");
ins = _ins.toLowerCase().split("");
outs = _outs.toLowerCase().split("");
if (data.params) {
data.params = data.params.map((p) => {
p.value = null;
if (p.default != null) p.value = p.default;
return p;
});
}
}
let x = 0;
let y = 0;
const w = 100;
const h = 50;
const margin = 10;
const existing = get(nodes);
if (type === "input") {
const inps = existing.filter((n) => n.nodeType === "input");
y = inps.length * h;
} else if (type === "filter") {
const flts = existing.filter((n) => n.nodeType === "filter");
x = (flts.length + 1) * w;
} else if (type === "output") {
x = 500;
}
data.inputs = ins;
data.outputs = outs;
let node = {
id: uuidv4(),
type: "ffmpeg",
data: data,
nodeType: type,
position: { x, y },
};
nodes.update((n) => {
n.push(node);
return n;
});
edges.update((_edges) => {
console.log('EXISTING', existing);
const target = existing[existing.length -2];
if (!target) return _edges;
const newEdge = {
id: uuidv4(),
type: "default",
source: node.id,
target: target.id,
};
console.log("NEW EDGE", newEdge);
_edges.push(newEdge);
return _edges;
});
}
export function addOutput(f) {
}
export function removeOutput(f) {
}
export function addInput(f) {
const newInput = {name: f, id: uuidv4()}
inputs.update((inps) => {
inps.push(newInput);
return inps;
});
}
export function removeInput(id) {
inputs.update((inps) => {
const index = inps.findIndex((f) => f.id === id);
inps.splice(index, 1);
return inps;
});
export function removeNode(id) {
nodes.update((_nodes) => {
const index = _nodes.findIndex((n) => n.id === id);
_nodes.splice(index, 1);
return _nodes;
});
}