✨ basic ui, some filters
This commit is contained in:
77
ui/Components/App.vue
Normal file
77
ui/Components/App.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div class="app">
|
||||
<div class="app__toolbar">
|
||||
<select v-model="selectedSaveSlot">
|
||||
<option
|
||||
v-for="save in saveSlots"
|
||||
:key="save"
|
||||
:value="save"
|
||||
>{{save}}</option>
|
||||
</select>
|
||||
<button @click="save">Save</button>
|
||||
<button @click="load">Load</button>
|
||||
</div>
|
||||
<Graph
|
||||
class="app__scene"
|
||||
:graph="graph"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Graph from './Graph.vue';
|
||||
import {load, save} from '../persistent';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Graph,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
graph: [],
|
||||
currentSelectedSaveSlot: localStorage.getItem('IFP__SELECTED_SAVE_SLOT') || 'FIRST',
|
||||
saveSlots: [
|
||||
'FIRST',
|
||||
'SECOND',
|
||||
'THIRD',
|
||||
],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectedSaveSlot: {
|
||||
get() {
|
||||
return this.currentSelectedSaveSlot;
|
||||
},
|
||||
set(val) {
|
||||
this.currentSelectedSaveSlot = val;
|
||||
localStorage.setItem('IFP__SELECTED_SAVE_SLOT', val);
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
methods: {
|
||||
load() {
|
||||
this.graph = load(this.selectedSaveSlot);
|
||||
},
|
||||
save() {
|
||||
save(this.selectedSaveSlot, this.graph);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.load();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.app {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.app__toolbar {
|
||||
padding: 10px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
68
ui/Components/ContextMenu.vue
Normal file
68
ui/Components/ContextMenu.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<ul
|
||||
v-if="open"
|
||||
class="context-menu"
|
||||
:style="{
|
||||
'--left': `${currentPosition.x}px`,
|
||||
'--top': `${currentPosition.y}px`,
|
||||
}"
|
||||
>
|
||||
<li
|
||||
v-for="item in items"
|
||||
:key="item.label"
|
||||
class="context-menu__item"
|
||||
@click="select(item)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
position: {
|
||||
type: null,
|
||||
required: true,
|
||||
},
|
||||
open: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentPosition() {
|
||||
return Object.assign({x: 0, y: 0}, this.position);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
select(item) {
|
||||
this.$emit('selected', {
|
||||
x: this.currentPosition.x,
|
||||
y: this.currentPosition.y,
|
||||
item,
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.context-menu {
|
||||
--left: 0;
|
||||
--top: 0;
|
||||
position: absolute;
|
||||
left: var(--left);
|
||||
top: var(--top);
|
||||
background-color: white;
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.context-menu__item {
|
||||
border-bottom: 1px solid
|
||||
}
|
||||
</style>
|
||||
24
ui/Components/Drag/Draggable.js
Normal file
24
ui/Components/Drag/Draggable.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import interact from 'interactjs';
|
||||
|
||||
export default {
|
||||
abstract: true,
|
||||
render() {
|
||||
try {
|
||||
return this.$slots.default[0];
|
||||
} catch (e) {
|
||||
throw new Error('Exactly one child component needed.');
|
||||
}
|
||||
return null;
|
||||
},
|
||||
mounted () {
|
||||
const el = this.$slots.default[0].elm;
|
||||
this.$nextTick(() => {
|
||||
interact(el)
|
||||
.draggable({
|
||||
onmove: (event) => {
|
||||
this.$emit('move', event);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,47 @@
|
||||
<template>
|
||||
<div>
|
||||
<Node
|
||||
<div class="graph">
|
||||
<canvas
|
||||
v-if="boundingRect"
|
||||
class="graph__bg"
|
||||
ref="bg"
|
||||
:width="boundingRect.width"
|
||||
:height="boundingRect.height"
|
||||
@click="showContextMenu"
|
||||
></canvas>
|
||||
<Draggable
|
||||
v-for="(node, index) in graph"
|
||||
:key="index"
|
||||
:node="node"
|
||||
@move="updateNodePosition(node, $event)"
|
||||
>
|
||||
<Node
|
||||
class="graph__node"
|
||||
:style="{transform: `translate(${node.x}px, ${node.y}px)`}"
|
||||
:node="node.node"
|
||||
ref="node"
|
||||
:selectedInput="selectedInput"
|
||||
:selectedOutput="selectedOutput"
|
||||
@outputSelected="selectedOutput = $event"
|
||||
@inputSelected="selectedInput = $event"
|
||||
/>
|
||||
</Draggable>
|
||||
<ContextMenu
|
||||
:open="!!contextMenuPosition"
|
||||
:position="contextMenuPosition"
|
||||
:items="availableNodes"
|
||||
@selected="addNode"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import * as nodes from '../../src/nodes';
|
||||
import Node from './Node.vue';
|
||||
import Draggable from './Drag/Draggable.js';
|
||||
import ContextMenu from './ContextMenu.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ContextMenu,
|
||||
Draggable,
|
||||
Node,
|
||||
},
|
||||
props: {
|
||||
@@ -19,6 +49,132 @@ export default {
|
||||
type: Array,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedOutput: null,
|
||||
selectedInput: null,
|
||||
boundingRect: null,
|
||||
contextMenuPosition: null,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selectedOutput() {
|
||||
this.tryConnectIO();
|
||||
},
|
||||
selectedInput() {
|
||||
this.tryConnectIO();
|
||||
},
|
||||
graph: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.drawConnections();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showContextMenu(event) {
|
||||
const {offsetX, offsetY} = event;
|
||||
if (this.contextMenuPosition) {
|
||||
this.contextMenuPosition = null;
|
||||
} else {
|
||||
this.contextMenuPosition = {
|
||||
x: offsetX,
|
||||
y: offsetY,
|
||||
};
|
||||
}
|
||||
},
|
||||
updateNodePosition(node, {clientX, clientY, x0, y0}) {
|
||||
const x = clientX - this.$el.offsetLeft;
|
||||
const y = clientY - this.$el.offsetTop;
|
||||
console.log('updateNodePosition', x, y)
|
||||
node.x = x;
|
||||
node.y = y;
|
||||
},
|
||||
tryConnectIO() {
|
||||
if (this.selectedInput && this.selectedOutput) {
|
||||
this.selectedInput.connect(this.selectedOutput);
|
||||
this.selectedInput = null;
|
||||
this.selectedOutput = null;
|
||||
}
|
||||
},
|
||||
recomputeSize() {
|
||||
this.boundingRect = this.$el.getBoundingClientRect();
|
||||
},
|
||||
drawConnections() {
|
||||
try {
|
||||
const flattenObjectArray = (ary) =>
|
||||
ary.reduce((acc, nxt) => Object.assign(acc, nxt), {});
|
||||
const inputs = flattenObjectArray(this.$refs.node
|
||||
.map((vm) => vm.inputPositions()));
|
||||
const outputs = flattenObjectArray(this.$refs.node
|
||||
.map((vm) => vm.outputPositions()));
|
||||
|
||||
const bg = this.$refs.bg;
|
||||
const ctx = bg.getContext('2d');
|
||||
ctx.clearRect(0, 0, bg.width, bg.height);
|
||||
ctx.strokeStyle = "#eaa11a";
|
||||
ctx.lineWidth = 3;
|
||||
for (let input of Object.values(inputs)) {
|
||||
if (!input.output) continue;
|
||||
const output = outputs[input.output.id];
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(input.rect.left, input.rect.top);
|
||||
const halfWay = {x: (input.rect.left + output.rect.right)/2, y: (input.rect.y + output.rect.y)/2};
|
||||
ctx.bezierCurveTo(
|
||||
Math.min(halfWay.x, input.rect.left - Math.max(Math.abs(halfWay.x - input.rect.left), 50)), input.rect.top,
|
||||
Math.max(halfWay.x, output.rect.right + Math.max(Math.abs(halfWay.x - output.rect.right), 50)), output.rect.top,
|
||||
// Math.min(halfWay.x, output.rect.right - (halfWay.x - output.rect.right)), output.rect.top,
|
||||
//halfWay.x, output.rect.top,
|
||||
output.rect.right, output.rect.top
|
||||
);
|
||||
ctx.stroke();
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('drawConnection', err);
|
||||
}
|
||||
},
|
||||
addNode({x, y, item: {node}}) {
|
||||
this.graph.push({
|
||||
x, y,
|
||||
node: new node(),
|
||||
});
|
||||
this.contextMenuPosition = null;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
availableNodes() {
|
||||
return Object.keys(nodes)
|
||||
.map((name) => ({
|
||||
label: name,
|
||||
node: nodes[name],
|
||||
}))
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.recomputeSize();
|
||||
this.drawConnections();
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.graph {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: linear-gradient(#333333, #151515);
|
||||
}
|
||||
|
||||
.graph__node {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.graph__bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
34
ui/Components/Input.vue
Normal file
34
ui/Components/Input.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<canvas></canvas>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
export const getStream = (callback) => {
|
||||
navigator.getUserMedia({video: true}, (stream) => {
|
||||
console.log(stream)
|
||||
/** @type {HTMLVideoElement} */
|
||||
const video = document.getElementById('Input');
|
||||
video.srcObject = stream;
|
||||
video.play();
|
||||
let lastFrameTime = null;
|
||||
const feedLoop = () => {
|
||||
if (lastFrameTime !== video.currentTime) {
|
||||
lastFrameTime = video.currentTime;
|
||||
callback(video);
|
||||
|
||||
}
|
||||
requestAnimationFrame(feedLoop);
|
||||
};
|
||||
feedLoop();
|
||||
}, console.error.bind(console))
|
||||
}
|
||||
|
||||
|
||||
getStream((video) => resize.in.image.value = video);
|
||||
|
||||
export default {
|
||||
mounted() {
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,25 +1,52 @@
|
||||
<template>
|
||||
<div
|
||||
style="
|
||||
border: 2px solid black;
|
||||
margin: 5px;
|
||||
padding: 2px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
"
|
||||
class="node"
|
||||
>
|
||||
<div style="flex: 1">
|
||||
<strong>Inputs:</strong>
|
||||
<div v-for="(type, name) in node.in.variables" :key="name">
|
||||
<div>{{ name }} <i>{{typeName(type)}}</i></div>
|
||||
<Value :type="type" :value="node.in[name]" />
|
||||
<div style="flex: 1" class="node__vars node__vars--input">
|
||||
<div
|
||||
v-for="(type, name) in node.in.variables"
|
||||
:key="name"
|
||||
:ref="`in-${name}`"
|
||||
class="node__var node-var node-var--input"
|
||||
:class="{'node-var--selected': node.in[name] === selectedInput }"
|
||||
>
|
||||
<div
|
||||
class="node-var__label"
|
||||
@click="$emit('inputSelected', node.in[name])"
|
||||
>
|
||||
{{ name }} <!--<i>{{typeName(type)}}</i> -->
|
||||
<span
|
||||
v-if="node.in[name].output"
|
||||
@click.stop="node.in[name].disconnect()"
|
||||
>×</span>
|
||||
</div>
|
||||
<Value
|
||||
:type="type"
|
||||
:value="node.in[name]"
|
||||
class="node-var__value"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div style="flex: 1">
|
||||
<strong>Outputs:</strong>
|
||||
<div v-for="(type, name) in node.out.variables" :key="name">
|
||||
<div>{{ name }} <i>{{typeName(type)}}</i></div>
|
||||
<Value :type="type" :value="node.out[name]" />
|
||||
<div>
|
||||
{{node.name}}
|
||||
</div>
|
||||
<div
|
||||
v-for="(type, name) in node.out.variables"
|
||||
:key="name"
|
||||
:ref="`out-${name}`"
|
||||
class="node__var node-var node-var--output"
|
||||
:class="{'node-var--selected': node.out[name] === selectedOutput }"
|
||||
>
|
||||
<div
|
||||
class="node-var__label"
|
||||
@click="$emit('outputSelected', node.out[name])"
|
||||
>{{ name }} <!--<i>{{typeName(type)}}</i>--></div>
|
||||
<Value
|
||||
:type="type"
|
||||
:value="node.out[name]"
|
||||
class="node-var__value"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -32,6 +59,14 @@ export default {
|
||||
Value,
|
||||
},
|
||||
props: {
|
||||
selectedInput: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
selectedOutput: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
node: {
|
||||
type: Object,
|
||||
required: true,
|
||||
@@ -40,7 +75,73 @@ export default {
|
||||
methods: {
|
||||
typeName(type) {
|
||||
return Array.isArray(type) ? 'Option' : type;
|
||||
}
|
||||
},
|
||||
outputPositions() {
|
||||
const outs = {};
|
||||
for (let out in this.node.out.variables) {
|
||||
outs[this.node.out[out].id] = {
|
||||
rect: this.$refs[`out-${out}`][0].getBoundingClientRect(),
|
||||
}
|
||||
}
|
||||
return outs;
|
||||
},
|
||||
inputPositions() {
|
||||
const ins = {};
|
||||
for (let i in this.node.in.variables) {
|
||||
ins[this.node.in[i].id] = {
|
||||
output: this.node.in[i].output,
|
||||
rect: this.$refs[`in-${i}`][0].getBoundingClientRect(),
|
||||
}
|
||||
}
|
||||
return ins;
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.node-var {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
}
|
||||
.node-var__label {
|
||||
width: 5em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.node-var__value {
|
||||
width: 5em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.node-var--selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.node-var--input {
|
||||
border-left: 5px solid green;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.node-var--output {
|
||||
border-right: 5px solid orange;
|
||||
padding-right: 4px;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.node {
|
||||
margin: 5px;
|
||||
padding: 5px 0;
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
background-color: #EEE;
|
||||
border-radius: 5px;
|
||||
box-shadow: 1px 1px 5px rgba(0,0,0,0.35);
|
||||
}
|
||||
|
||||
.node__var {
|
||||
margin-bottom: 2px
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<template>
|
||||
<canvas
|
||||
style="
|
||||
width: 150px;
|
||||
box-shadow: 0 0 6px;
|
||||
"
|
||||
@click="isRunning = !isRunning"
|
||||
/>
|
||||
<div
|
||||
class="image-value"
|
||||
@click="download"
|
||||
>
|
||||
<canvas class="image-value__thumb" ref="thumb" />
|
||||
<canvas class="image-value__preview" ref="preview" />
|
||||
</div>
|
||||
|
||||
|
||||
</template>
|
||||
<script>
|
||||
import {paintToCanvas, mediaSize} from '../../../src/nodes/canvas.js'
|
||||
@@ -26,7 +28,7 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isRunning: false,
|
||||
isRunning: true,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
@@ -35,29 +37,75 @@ export default {
|
||||
this.value.onchange(this.redraw);
|
||||
})
|
||||
},
|
||||
watch: {
|
||||
isRunning: {
|
||||
handler() {
|
||||
this.redraw();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
redrawCanvas(canvas) {
|
||||
const value = this.value.value;
|
||||
/** @type {HTMLCanvasElement} */
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!value) {
|
||||
canvas.width = 1;
|
||||
canvas.height = 1;
|
||||
ctx.clearRect(0, 0, 1, 1);
|
||||
} else {
|
||||
const {width, height} = mediaSize(value);
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
paintToCanvas(canvas, value, {
|
||||
top: 0,
|
||||
left: 0,
|
||||
width, height,
|
||||
});
|
||||
}
|
||||
},
|
||||
redraw() {
|
||||
if (this.isRunning) {
|
||||
const value = this.value.value;
|
||||
/** @type {HTMLCanvasElement} */
|
||||
const canvas = this.$el;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!value) {
|
||||
canvas.width = 1;
|
||||
canvas.height = 1;
|
||||
ctx.clearRect(0, 0, 1, 1);
|
||||
} else {
|
||||
const {width, height} = mediaSize(value);
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
paintToCanvas(canvas, value, {
|
||||
top: 0,
|
||||
left: 0,
|
||||
width, height,
|
||||
});
|
||||
}
|
||||
this.redrawCanvas(this.$refs.thumb);
|
||||
this.redrawCanvas(this.$refs.preview)
|
||||
}
|
||||
},
|
||||
download() {
|
||||
const a = document.createElement('a');
|
||||
a.href = this.$refs.preview.toDataURL();
|
||||
a.download = 'sample.png';
|
||||
a.click();
|
||||
console.log('download');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.image-value {
|
||||
position: relative;
|
||||
}
|
||||
.image-value__thumb {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
object-fit: contain;
|
||||
background-color: black;
|
||||
box-shadow: 0 0 6px;
|
||||
}
|
||||
|
||||
.image-value__preview {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 30vw;
|
||||
height: 30vh;
|
||||
object-fit: contain;
|
||||
background-color: black;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.image-value__thumb:hover ~ .image-value__preview {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
export default {
|
||||
props: {
|
||||
type: {
|
||||
type: Array,
|
||||
type: null,
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
<template>
|
||||
<input v-model="value.value">
|
||||
<input
|
||||
v-model="value.value"
|
||||
:type="
|
||||
type === 'Number' ? 'number' :
|
||||
type === 'Color' ? 'color' :
|
||||
'text'
|
||||
"
|
||||
>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
@@ -7,6 +14,10 @@ export default {
|
||||
value: {
|
||||
type: null,
|
||||
required: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ export default {
|
||||
return h(
|
||||
this.type === 'Image' ? ImageValue :
|
||||
this.type === 'Number' ? StringValue :
|
||||
this.type === 'Color' ? StringValue :
|
||||
Array.isArray(this.type) ? SelectValue :
|
||||
null,
|
||||
{props: {value: this.value, type: this.type}})
|
||||
|
||||
Reference in New Issue
Block a user