working graph editor
This commit is contained in:
parent
f6b4e43509
commit
0645651fd9
207
src/App.svelte
207
src/App.svelte
|
@ -1,12 +1,17 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { addNode, nodes, inputs, output, filters, previewCommand } from "./stores.js";
|
import {
|
||||||
import Input from "./Input.svelte";
|
selectedFilter,
|
||||||
import Output from "./Output.svelte";
|
addNode,
|
||||||
|
nodes,
|
||||||
|
inputs,
|
||||||
|
output,
|
||||||
|
filters,
|
||||||
|
previewCommand,
|
||||||
|
} from "./stores.js";
|
||||||
import Filter from "./Filter.svelte";
|
import Filter from "./Filter.svelte";
|
||||||
import FilterPicker from "./FilterPicker.svelte";
|
import FilterPicker from "./FilterPicker.svelte";
|
||||||
import Graph from "./Graph.svelte";
|
import Graph from "./Graph.svelte";
|
||||||
// import GraphOld from "./GraphOld.svelte";
|
|
||||||
import { FFmpeg } from "@ffmpeg/ffmpeg";
|
import { FFmpeg } from "@ffmpeg/ffmpeg";
|
||||||
import { fetchFile, toBlobURL } from "@ffmpeg/util";
|
import { fetchFile, toBlobURL } from "@ffmpeg/util";
|
||||||
import { dndzone } from "svelte-dnd-action";
|
import { dndzone } from "svelte-dnd-action";
|
||||||
|
@ -46,18 +51,22 @@
|
||||||
for (let vid of $inputs) {
|
for (let vid of $inputs) {
|
||||||
await ffmpeg.writeFile(vid.name, await fetchFile("/" + vid.name));
|
await ffmpeg.writeFile(vid.name, await fetchFile("/" + vid.name));
|
||||||
}
|
}
|
||||||
let clist = $previewCommand.replaceAll('"', '').replace("ffmpeg", "").split(" ").filter(i => i.trim() != '');
|
let clist = $previewCommand
|
||||||
|
.replaceAll('"', "")
|
||||||
|
.replace("ffmpeg", "")
|
||||||
|
.split(" ")
|
||||||
|
.filter((i) => i.trim() != "");
|
||||||
console.log("command", clist);
|
console.log("command", clist);
|
||||||
// command.push("-pix_fmt");
|
// command.push("-pix_fmt");
|
||||||
// command.push("yuv420p");
|
// command.push("yuv420p");
|
||||||
// command.push("out.mp4");
|
// command.push("out.mp4");
|
||||||
await ffmpeg.exec(clist, TIMEOUT);
|
await ffmpeg.exec(clist, TIMEOUT);
|
||||||
// await ffmpeg.exec(["-f", "lavfi", "-i", "color=size=1280x720:rate=25:color=red", "-t", "5", "out.mp4"])
|
// await ffmpeg.exec(["-f", "lavfi", "-i", "color=size=1280x720:rate=25:color=red", "-t", "5", "out.mp4"])
|
||||||
const data = await ffmpeg.readFile("out.mp4");
|
const data = await ffmpeg.readFile("out.mp4");
|
||||||
rendering = false;
|
rendering = false;
|
||||||
videoValue = URL.createObjectURL(new Blob([data.buffer], { type: "video/mp4" }));
|
videoValue = URL.createObjectURL(new Blob([data.buffer], { type: "video/mp4" }));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
log += "Failed";
|
log += "Failed";
|
||||||
}
|
}
|
||||||
rendering = false;
|
rendering = false;
|
||||||
|
@ -84,11 +93,6 @@
|
||||||
ffmpegLoaded = true;
|
ffmpegLoaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function handleFilterSort(e) {
|
function handleFilterSort(e) {
|
||||||
filters.set(e.detail.items);
|
filters.set(e.detail.items);
|
||||||
}
|
}
|
||||||
|
@ -105,50 +109,49 @@
|
||||||
<main>
|
<main>
|
||||||
<section class="header">
|
<section class="header">
|
||||||
<h1>FFmpeg Explorer</h1>
|
<h1>FFmpeg Explorer</h1>
|
||||||
<p>
|
<div class="help">
|
||||||
A tool to help you explore <a href="https://www.ffmpeg.org/" target="_blank">FFmpeg</a>
|
<p>
|
||||||
filters and options. To use: select one or more input videos (there are currently two options),
|
A tool to help you explore <a href="https://www.ffmpeg.org/" target="_blank">FFmpeg</a>
|
||||||
export and add some filters, and then hit "render" to preview the output in browser. Note: this
|
filters. To use:
|
||||||
is a work in progress, many things may still be broken! Only audio to audio and video to video
|
</p>
|
||||||
filters are included. If it hangs/crashes refresh the page. Post issues/feedback to
|
<ol>
|
||||||
<a href="https://github.com/antiboredom/ffmpeg-explorer/" target="_blank">GitHub</a>. By
|
<li>Add filters from the list on the left.</li>
|
||||||
<a href="https://lav.io" target="_blank">Sam Lavigne</a>.
|
<li>Click on filters in the center panel to edit options.</li>
|
||||||
</p>
|
<li>Hit "render" to preview the output in browser.</li>
|
||||||
|
<li>For more complex filtergraphs, disable "automatic layout."</li>
|
||||||
|
</ol>
|
||||||
|
<p>
|
||||||
|
Note: this is a work in progress, many things may still be broken! 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>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<!-- {message} -->
|
<!-- {message} -->
|
||||||
<section class="command">
|
<section class="command">
|
||||||
<h3>Output Command</h3>
|
<h3>Output Command</h3>
|
||||||
<div class="inner-command">
|
<div class="inner-command">
|
||||||
<textarea readonly class="actual-command" bind:this={commandRef}>{$previewCommand}</textarea>
|
<textarea readonly class="actual-command" bind:this={commandRef} on:click={() => commandRef.select()}>{$previewCommand}</textarea>
|
||||||
<div>
|
<div>
|
||||||
<button on:click={copyCommand}>Copy Command</button>
|
<button on:click={copyCommand}>Copy Command</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="inputs">
|
|
||||||
<div class="section-header">
|
|
||||||
<h3>Inputs</h3>
|
|
||||||
<button on:click={newInput}>Add Input</button>
|
|
||||||
</div>
|
|
||||||
{#each $nodes as node, index}
|
|
||||||
{#if node.nodeType === "input"}
|
|
||||||
<Input bind:filename={node.data.name} id={node.id} {index} />
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="log">
|
<section class="log">
|
||||||
<h3>FFmpeg Log</h3>
|
<h3>FFmpeg Log</h3>
|
||||||
<textarea readonly class="the-log" bind:this={logbox}>{log}</textarea>
|
<textarea readonly class="the-log" bind:this={logbox}>{log}</textarea>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="preview">
|
<section class="preview">
|
||||||
{#if rendering}
|
<div class="vid-holder">
|
||||||
<div class="rendering-video"><span>Rendering...</span></div>
|
{#if rendering}
|
||||||
{/if}
|
<div class="rendering-video"><span>Rendering...</span></div>
|
||||||
<video controls src={videoValue} />
|
{/if}
|
||||||
<div style="text-align: right;margin-top:5px;">
|
<video controls src={videoValue} />
|
||||||
|
</div>
|
||||||
|
<div style="text-align: right;padding-top:5px;">
|
||||||
<button on:click={render} disabled={!ffmpegLoaded || rendering}>
|
<button on:click={render} disabled={!ffmpegLoaded || rendering}>
|
||||||
{#if ffmpegLoaded}
|
{#if ffmpegLoaded}
|
||||||
{#if rendering}
|
{#if rendering}
|
||||||
|
@ -163,58 +166,39 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="output">
|
|
||||||
<h3>Output</h3>
|
|
||||||
{#each $nodes as node}
|
|
||||||
{#if node.nodeType==="output"}
|
|
||||||
<Output bind:filename={node.data.name} />
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="filters">
|
<section class="filters">
|
||||||
<h3>Filters (click to add)</h3>
|
<h3>Filters (click to add)</h3>
|
||||||
<div class="inner-filters">
|
<div class="filter-picker">
|
||||||
<div class="filter-picker">
|
<FilterPicker select={"video"} />
|
||||||
<FilterPicker select={"video"} />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="filters-holder"
|
|
||||||
use:dndzone={{ items: $filters }}
|
|
||||||
on:consider={handleFilterSort}
|
|
||||||
on:finalize={handleFilterSort}
|
|
||||||
>
|
|
||||||
{#each $nodes as f (f.id)}
|
|
||||||
{#if f.nodeType === "filter"}
|
|
||||||
<div class="filter">
|
|
||||||
<Filter bind:filter={f.data} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- <section class="graph"> -->
|
|
||||||
<!-- <GraphOld /> -->
|
|
||||||
<!-- </section> -->
|
|
||||||
|
|
||||||
<section class="graph">
|
<section class="graph">
|
||||||
<Graph />
|
<Graph />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="filter-editor">
|
||||||
|
{#if $selectedFilter && $nodes.length > 0 && $nodes[$selectedFilter]}
|
||||||
|
<Filter bind:filter={$nodes[$selectedFilter].data} />
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
main {
|
main {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: 300px 1fr 1fr 1fr 1fr 300px;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"hdr cmd cmd"
|
"hdr log log log prv prv"
|
||||||
"inp log prv"
|
"hdr cmd cmd cmd prv prv"
|
||||||
"out log prv"
|
"flt gra gra gra gra edt";
|
||||||
"flt flt flt";
|
/* grid-template-rows: 16% 17% 77%; */
|
||||||
|
grid-template-rows: 15% 15% calc(70% - 40px);
|
||||||
|
padding: 10px;
|
||||||
grid-gap: 20px;
|
grid-gap: 20px;
|
||||||
padding: 20px;
|
height: 100vh;
|
||||||
|
border: 1px solid blue;
|
||||||
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
section {
|
section {
|
||||||
|
@ -228,6 +212,7 @@
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
grid-area: hdr;
|
grid-area: hdr;
|
||||||
|
overflow: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
.command {
|
.command {
|
||||||
|
@ -236,17 +221,26 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputs {
|
|
||||||
grid-area: inp;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview {
|
.preview {
|
||||||
grid-area: prv;
|
grid-area: prv;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.output {
|
.preview video {
|
||||||
grid-area: out;
|
width: 100%;
|
||||||
|
/* object-fit: contain; */
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vid-holder {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% - 30px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.log {
|
.log {
|
||||||
|
@ -255,19 +249,25 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inner-filters {
|
.filters {
|
||||||
|
grid-area: flt;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph {
|
||||||
|
grid-area: gra;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filters {
|
.filter-editor {
|
||||||
grid-area: flt;
|
grid-area: edt;
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-picker {
|
.filter-picker {
|
||||||
max-height: 500px;
|
/* max-height: 500px; */
|
||||||
width: 400px;
|
/* width: 400px; */
|
||||||
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
|
@ -302,14 +302,20 @@
|
||||||
.inner-command {
|
.inner-command {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: 10px 0px;
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
outline: none;
|
||||||
|
-webkit-box-shadow: none;
|
||||||
|
-moz-box-shadow: none;
|
||||||
|
box-shadow: none;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
.actual-command {
|
.actual-command {
|
||||||
border: none;
|
border: none;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
resize: none;
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
|
@ -324,6 +330,7 @@
|
||||||
.the-log {
|
.the-log {
|
||||||
border: none;
|
border: none;
|
||||||
resize: none;
|
resize: none;
|
||||||
|
padding: 5px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
.rendering-video {
|
.rendering-video {
|
||||||
|
@ -339,6 +346,17 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.help {
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
ol {
|
||||||
|
margin: 5px 0px;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
ol li {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 1400px) {
|
@media only screen and (max-width: 1400px) {
|
||||||
.filters-holder {
|
.filters-holder {
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
@ -368,9 +386,6 @@
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
box-shadow: 2px 2px 0px #000;
|
box-shadow: 2px 2px 0px #000;
|
||||||
}
|
}
|
||||||
.inner-filters {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.filter-picker {
|
.filter-picker {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
description: "",
|
description: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
let show = false;
|
let show = true;
|
||||||
|
|
||||||
function remove() {
|
function remove() {
|
||||||
removeNode(filter.id);
|
removeNode(filter.id);
|
||||||
|
@ -27,12 +27,6 @@
|
||||||
<div class="filter-holder">
|
<div class="filter-holder">
|
||||||
<div class="head">
|
<div class="head">
|
||||||
<div class="name"><h3>{filter.name}</h3></div>
|
<div class="name"><h3>{filter.name}</h3></div>
|
||||||
<div>
|
|
||||||
{#if filter.params && filter.params.length > 0}
|
|
||||||
<button on:click={() => (show = !show)}>{show ? "Hide" : "Show"} Options</button>
|
|
||||||
{/if}
|
|
||||||
<button on:click={remove}>X</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="description">
|
<div class="description">
|
||||||
|
@ -93,6 +87,8 @@
|
||||||
/* border: 1px solid #999; */
|
/* border: 1px solid #999; */
|
||||||
border: 1px solid var(--b1);
|
border: 1px solid var(--b1);
|
||||||
/* box-shadow: 5px 5px 0px #000; */
|
/* box-shadow: 5px 5px 0px #000; */
|
||||||
|
overflow-y: scroll;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
.filter-holder,
|
.filter-holder,
|
||||||
input,
|
input,
|
||||||
|
@ -123,7 +119,11 @@
|
||||||
}
|
}
|
||||||
.p-value {
|
.p-value {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
justify-content: stretch;
|
||||||
}
|
}
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
input[type="range"] {
|
input[type="range"] {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,7 @@
|
||||||
|
|
||||||
<div class="holder">
|
<div class="holder">
|
||||||
<div class="search">
|
<div class="search">
|
||||||
<input placeholder="Search Filters" on:keyup={update} bind:value={q} type="text" />
|
<input placeholder="Search Filters" on:keyup={update} bind:value={q} type="text" /><button on:click={() => {reset(); update();}}>X</button>
|
||||||
<select on:change={reset} bind:value={select}>
|
<select on:change={reset} bind:value={select}>
|
||||||
<option value="video">Video Filters</option>
|
<option value="video">Video Filters</option>
|
||||||
<option value="audio">Audio Filters</option>
|
<option value="audio">Audio Filters</option>
|
||||||
|
@ -73,12 +73,16 @@
|
||||||
}
|
}
|
||||||
.search {
|
.search {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-items: stretch;
|
justify-content: stretch;
|
||||||
}
|
}
|
||||||
input {
|
input {
|
||||||
|
width: 100%;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
margin-right: 10px;
|
|
||||||
}
|
}
|
||||||
|
button {
|
||||||
|
margin-left: 1px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
.type {
|
.type {
|
||||||
color: #999;
|
color: #999;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
|
|
|
@ -9,4 +9,4 @@
|
||||||
if ($auto) fitView();
|
if ($auto) fitView();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<button>Fit</button>
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { nodes, edges, auto } from "./stores.js";
|
import { addNode, nodes, edges, auto, selectedFilter } from "./stores.js";
|
||||||
import {
|
import {
|
||||||
SvelteFlowProvider,
|
SvelteFlowProvider,
|
||||||
SvelteFlow,
|
SvelteFlow,
|
||||||
|
@ -14,36 +14,63 @@
|
||||||
ffmpeg: Node,
|
ffmpeg: Node,
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultEdgeOptions = {
|
function onClick(e) {
|
||||||
deletable: true,
|
if (e.detail.nodeType === "filter") {
|
||||||
};
|
const newSelected = $nodes.findIndex((n) => n.id === e.detail.id);
|
||||||
|
if (newSelected > -1) {
|
||||||
function onEdgeUpdate(e) {
|
$selectedFilter = newSelected;
|
||||||
console.log(e);
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onEdgeUpdateStart(e) {
|
function addInput() {
|
||||||
console.log(e);
|
addNode({ name: "shoe.mp4" }, "input");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function onEdgeUpdateEnd(e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
function onMoveStart(e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
function onConnect(e) {
|
|
||||||
console.log("connect", e);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SvelteFlowProvider>
|
<SvelteFlowProvider>
|
||||||
<FitComp/>
|
<div class="holder">
|
||||||
<label for="auto"><input id="auto" type="checkbox" bind:checked={$auto} />Automatic Layout</label>
|
<FitComp />
|
||||||
<div style="width: 900px; height: 500px;">
|
<div class="nav">
|
||||||
<SvelteFlow {nodeTypes} {nodes} {edges} snapGrid={[10, 10]} fitView>
|
<button on:click={addInput}>Add Input</button>
|
||||||
<Controls />
|
<label for="auto"
|
||||||
<Background variant={BackgroundVariant.Dots} />
|
><input id="auto" type="checkbox" bind:checked={$auto} />Automatic Layout</label
|
||||||
</SvelteFlow>
|
>
|
||||||
|
</div>
|
||||||
|
<div class="flow">
|
||||||
|
<div style="height: 100%; width: 100%">
|
||||||
|
<SvelteFlow
|
||||||
|
nodesConnectable={!auto}
|
||||||
|
panOnDrag={!auto}
|
||||||
|
edgesUpdatable={!auto}
|
||||||
|
connectOnClick={true}
|
||||||
|
nodesFocusable={!auto}
|
||||||
|
edgesFocusable={!auto}
|
||||||
|
on:nodeclick={onClick}
|
||||||
|
{nodeTypes}
|
||||||
|
{nodes}
|
||||||
|
{edges}
|
||||||
|
snapGrid={[10, 10]}
|
||||||
|
fitView
|
||||||
|
>
|
||||||
|
<Controls />
|
||||||
|
<Background variant={BackgroundVariant.Dots} />
|
||||||
|
</SvelteFlow>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SvelteFlowProvider>
|
</SvelteFlowProvider>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.holder {
|
||||||
|
flex-direction: column;
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.flow {
|
||||||
|
flex: 1;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
95
src/app.css
95
src/app.css
|
@ -2,12 +2,12 @@
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
:root {
|
:root {
|
||||||
/* --b1: #004dff; */
|
/* --b1: #004dff; */
|
||||||
/* --b2: #f19696b3; */
|
/* --b2: #f19696b3; */
|
||||||
--b1: #004dff;
|
--b1: #004dff;
|
||||||
--b2: #ffdadab3;
|
--b2: #ffdadab3;
|
||||||
/* --b1: #ff0000; */
|
/* --b1: #ff0000; */
|
||||||
/* --b2: #b6e3f2b3; */
|
/* --b2: #b6e3f2b3; */
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
@ -15,8 +15,8 @@ a {
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
background-color: var(--b1);
|
background-color: var(--b1);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
@ -24,6 +24,11 @@ body {
|
||||||
font:
|
font:
|
||||||
16px Times,
|
16px Times,
|
||||||
serif;
|
serif;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
textrea,
|
textrea,
|
||||||
select,
|
select,
|
||||||
|
@ -32,26 +37,26 @@ button {
|
||||||
font: inherit;
|
font: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
button, input:not([type="range"]) , select {
|
button,
|
||||||
border: 1px solid var(--b1);
|
input:not([type="range"]),
|
||||||
background-color: white;
|
select {
|
||||||
box-shadow: 2px 2px 0px var(--b2);
|
border: 1px solid var(--b1);
|
||||||
|
background-color: white;
|
||||||
|
box-shadow: 2px 2px 0px var(--b2);
|
||||||
}
|
}
|
||||||
button:active {
|
button:active {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
left: 2px;
|
left: 2px;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
|
||||||
video {
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
input[type="range"] {
|
input[type="range"] {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Removes default focus */
|
/* Removes default focus */
|
||||||
|
@ -62,53 +67,53 @@ input[type="range"]:focus {
|
||||||
/***** Chrome, Safari, Opera and Edge Chromium styles *****/
|
/***** Chrome, Safari, Opera and Edge Chromium styles *****/
|
||||||
/* slider track */
|
/* slider track */
|
||||||
input[type="range"]::-webkit-slider-runnable-track {
|
input[type="range"]::-webkit-slider-runnable-track {
|
||||||
background-color: var(--b1);
|
background-color: var(--b1);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
height: 0.3rem;
|
height: 0.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* slider thumb */
|
/* slider thumb */
|
||||||
input[type="range"]::-webkit-slider-thumb {
|
input[type="range"]::-webkit-slider-thumb {
|
||||||
-webkit-appearance: none; /* Override default look */
|
-webkit-appearance: none; /* Override default look */
|
||||||
appearance: none;
|
appearance: none;
|
||||||
margin-top: -5px; /* Centers thumb on the track */
|
margin-top: -5px; /* Centers thumb on the track */
|
||||||
|
|
||||||
/*custom styles*/
|
/*custom styles*/
|
||||||
background-color: var(--b1);
|
background-color: var(--b1);
|
||||||
height: 15px;
|
height: 15px;
|
||||||
width: 10px;
|
width: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="range"]:focus::-webkit-slider-thumb {
|
input[type="range"]:focus::-webkit-slider-thumb {
|
||||||
/* border: 1px solid #053a5f; */
|
/* border: 1px solid #053a5f; */
|
||||||
}
|
}
|
||||||
|
|
||||||
/******** Firefox styles ********/
|
/******** Firefox styles ********/
|
||||||
/* slider track */
|
/* slider track */
|
||||||
input[type="range"]::-moz-range-track {
|
input[type="range"]::-moz-range-track {
|
||||||
background-color: var(--b1);
|
background-color: var(--b1);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
height: 0.3rem;
|
height: 0.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* slider thumb */
|
/* slider thumb */
|
||||||
input[type="range"]::-moz-range-thumb {
|
input[type="range"]::-moz-range-thumb {
|
||||||
border: none; /*Removes extra border that FF applies*/
|
border: none; /*Removes extra border that FF applies*/
|
||||||
border-radius: 0; /*Removes default border-radius that FF applies*/
|
border-radius: 0; /*Removes default border-radius that FF applies*/
|
||||||
|
|
||||||
/*custom styles*/
|
/*custom styles*/
|
||||||
background-color: var(--b1);
|
background-color: var(--b1);
|
||||||
height: 15px;
|
height: 15px;
|
||||||
width: 10px;
|
width: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="range"]:focus::-moz-range-thumb {
|
input[type="range"]:focus::-moz-range-thumb {
|
||||||
}
|
}
|
||||||
.svelte-flow__node {
|
.svelte-flow__node {
|
||||||
border-radius: 0px !important;
|
border-radius: 0px !important;
|
||||||
border: 1px solid var(--b1) !important;
|
border: 1px solid var(--b1) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.svelte-flow__attribution {
|
.svelte-flow__attribution {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,96 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Handle, Position } from "@xyflow/svelte";
|
import { Handle, Position } from "@xyflow/svelte";
|
||||||
|
import { removeNode } from "../stores.js";
|
||||||
|
|
||||||
export let data = { name: "", inputs: [], outputs: [], onChange: () => {} };
|
export let data = { nodeType: "", name: "", inputs: [], outputs: [] };
|
||||||
|
export let id;
|
||||||
|
|
||||||
|
function remove() {
|
||||||
|
removeNode(id);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="node">
|
<div class="node {data.nodeType}">
|
||||||
{data.name}
|
<div class="head">
|
||||||
|
<div class="node-type">{data.nodeType}</div>
|
||||||
|
<button on:click={remove}>X</button>
|
||||||
|
</div>
|
||||||
|
<div class="body">
|
||||||
|
{#if data.nodeType == "input"}
|
||||||
|
<select bind:value={data.name}>
|
||||||
|
<option value="punch.mp4">punch.mp4</option>
|
||||||
|
<option value="shoe.mp4">shoe.mp4</option>
|
||||||
|
</select>
|
||||||
|
{:else}
|
||||||
|
{data.name}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#each data.inputs as inp, index}
|
{#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'}" />
|
<Handle
|
||||||
|
type="target"
|
||||||
|
position={Position.Left}
|
||||||
|
id={inp + "_" + index}
|
||||||
|
class="handle {inp}"
|
||||||
|
style="top: {index * 12 + 4}px; left: -7px;">{inp}</Handle
|
||||||
|
>
|
||||||
{/each}
|
{/each}
|
||||||
{#each data.outputs as out, index}
|
{#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'}" />
|
<Handle
|
||||||
|
type="source"
|
||||||
|
id={out + "_" + index}
|
||||||
|
position={Position.Right}
|
||||||
|
class="handle {out}"
|
||||||
|
style="top: {index * 12 + 4}px; left: 107%;">{out}</Handle
|
||||||
|
>
|
||||||
{/each}
|
{/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 -->
|
|
||||||
<!-- /> -->
|
|
||||||
<style>
|
<style>
|
||||||
.node {
|
:global(:root) {
|
||||||
padding: 5px;
|
--edge-color: var(--b1) !important;
|
||||||
}
|
--edge-color-selected: black;
|
||||||
|
}
|
||||||
|
:global(.svelte-flow__node) {
|
||||||
|
box-shadow: 2px 2px 0px var(--b2);
|
||||||
|
}
|
||||||
|
:global(.svelte-flow__node.selected) {
|
||||||
|
outline: 1px solid var(--b1) !important;
|
||||||
|
}
|
||||||
|
:global(.handle) {
|
||||||
|
width: 10px !important;
|
||||||
|
height: 10px !important;
|
||||||
|
border: 1px solid var(--b1) !important;
|
||||||
|
border-radius: 0px !important;
|
||||||
|
background-color: white !important;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 6px;
|
||||||
|
}
|
||||||
|
:global(.handle.a) {
|
||||||
|
/* border: 1px solid var(--b2) !important; */
|
||||||
|
}
|
||||||
|
.node {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
.head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.head button {
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 8px;
|
||||||
|
padding: 2px 2px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
.node-type {
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.node.input {
|
||||||
|
}
|
||||||
|
.node.filter {
|
||||||
|
}
|
||||||
|
.node.output {
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { writable, derived, get } from "svelte/store";
|
||||||
export const nodes = writable([]);
|
export const nodes = writable([]);
|
||||||
export const edges = writable([]);
|
export const edges = writable([]);
|
||||||
export const auto = writable(true);
|
export const auto = writable(true);
|
||||||
|
export const selectedFilter = writable();
|
||||||
|
|
||||||
const PREFIX = "";
|
const PREFIX = "";
|
||||||
|
|
||||||
|
@ -146,19 +147,22 @@ export const previewCommand = derived([edges, nodes], ([$edges, $nodes]) => {
|
||||||
const source = $nodes.find((n) => n.id === e.source);
|
const source = $nodes.find((n) => n.id === e.source);
|
||||||
const target = $nodes.find((n) => n.id === e.target);
|
const target = $nodes.find((n) => n.id === e.target);
|
||||||
|
|
||||||
if (source.nodeType === "input") {
|
if (source && target) {
|
||||||
if (e.sourceHandle.includes("v")) {
|
|
||||||
edgeIds[e.id] = inputIds[source.id] + ":v";
|
|
||||||
}
|
|
||||||
if (e.sourceHandle.includes("a")) {
|
|
||||||
edgeIds[e.id] = inputIds[source.id] + ":a";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (target.nodeType === "output") {
|
if (source.nodeType === "input") {
|
||||||
const outType = e.targetHandle.includes("a") ? "aud_out" : "vid_out";
|
if (e.sourceHandle.includes("v")) {
|
||||||
edgeIds[e.id] = outType;
|
edgeIds[e.id] = inputIds[source.id] + ":v";
|
||||||
}
|
}
|
||||||
|
if (e.sourceHandle.includes("a")) {
|
||||||
|
edgeIds[e.id] = inputIds[source.id] + ":a";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.nodeType === "output") {
|
||||||
|
const outType = e.targetHandle.includes("a") ? "aud_out" : "vid_out";
|
||||||
|
edgeIds[e.id] = outType;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let n of $nodes.filter((n) => n.nodeType == "filter")) {
|
for (let n of $nodes.filter((n) => n.nodeType == "filter")) {
|
||||||
|
@ -330,6 +334,7 @@ export function addNode(data, type) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data.nodeType = type;
|
||||||
data.inputs = ins;
|
data.inputs = ins;
|
||||||
data.outputs = outs;
|
data.outputs = outs;
|
||||||
|
|
||||||
|
@ -347,9 +352,9 @@ export function addNode(data, type) {
|
||||||
const isAuto = get(auto);
|
const isAuto = get(auto);
|
||||||
|
|
||||||
if (isAuto) {
|
if (isAuto) {
|
||||||
const w = 100;
|
const w = 120;
|
||||||
const h = 50;
|
const h = 50;
|
||||||
const margin = 10;
|
const margin = 50;
|
||||||
let prev = null;
|
let prev = null;
|
||||||
|
|
||||||
for (let n of _nodes) {
|
for (let n of _nodes) {
|
||||||
|
@ -362,7 +367,7 @@ export function addNode(data, type) {
|
||||||
for (let n of _nodes) {
|
for (let n of _nodes) {
|
||||||
if (n.nodeType === "filter") {
|
if (n.nodeType === "filter") {
|
||||||
let _w = prev && prev.width ? prev.width : w;
|
let _w = prev && prev.width ? prev.width : w;
|
||||||
n.position = { x: prev ? prev.position.x + _w + margin : 0, y: -30 };
|
n.position = { x: prev ? prev.position.x + _w + margin : 0, y: -50 };
|
||||||
prev = n;
|
prev = n;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -374,8 +379,13 @@ export function addNode(data, type) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (node.nodeType === "filter") {
|
||||||
|
selectedFilter.set(_nodes.length - 1);
|
||||||
|
}
|
||||||
return _nodes;
|
return _nodes;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeNode(id) {
|
export function removeNode(id) {
|
||||||
|
|
Loading…
Reference in New Issue