basic ui, some filters

This commit is contained in:
2019-01-21 20:39:56 +01:00
parent e76aafc35a
commit 6394f2a869
30 changed files with 974 additions and 111 deletions

77
ui/Components/App.vue Normal file
View 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>

View 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>

View 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);
}
})
})
}
}

View File

@@ -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
View 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>

View File

@@ -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()"
>&times;</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>

View File

@@ -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>

View File

@@ -11,7 +11,7 @@
export default {
props: {
type: {
type: Array,
type: null,
required: true,
},
value: {

View File

@@ -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,
}
}
}

View File

@@ -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}})