Dies ist das Repository meines kleinen Portfolios.
Im Hintergrund läuft eine Planetensimulation, geschrieben in JavaScript und Three.js.
Die zu sehenden Texturen stammen von:
https://www.solarsystemscope.com/textures/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
3338 lines
63 KiB
3338 lines
63 KiB
/** |
|
* https://github.com/sunag/flow |
|
*/ |
|
|
|
function __flow__addCSS( css ) { |
|
|
|
try { |
|
|
|
const style = document.createElement( 'style' ); |
|
|
|
style.setAttribute( 'type', 'text/css' ); |
|
style.innerHTML = css; |
|
document.head.appendChild( style ); |
|
|
|
} catch( e ) {} |
|
|
|
} |
|
|
|
__flow__addCSS( `@keyframes f-animation-open { 0% { transform: scale(.5); opacity: 0; } 100% { transform: scale(1); opacity: 1; }}f-canvas,f-canvas canvas { position: absolute; top: 0; left: 0; margin: 0; padding: 0; width: 100%; height: 100%; -webkit-touch-callout: none; }f-canvas { overflow: auto; cursor: grab;}f-canvas canvas.front { z-index: 10;}body.dragging f-canvas,body.connecting f-canvas { overflow: hidden !important;}body.dragging *:not(.drag) { pointer-events: none !important;}f-canvas.grabbing * { cursor: grabbing; user-select: none;}f-canvas canvas { position: fixed; overflow: hidden; pointer-events: none;}f-canvas::-webkit-scrollbar { width: 8px; height: 8px;}f-canvas::-webkit-scrollbar-thumb:hover{ background: #014fc5;}f-canvas::-webkit-scrollbar-track { background: #363636;}f-canvas::-webkit-scrollbar-thumb { background-color: #666666; border-radius: 10px; border: 0;}f-canvas f-content,f-canvas f-area { position: absolute; display: block;}f-node { position: absolute; margin: 0; padding: 0; user-select: none; width: 320px; z-index: 1; cursor: auto; filter: drop-shadow(0 0 10px #00000061); backdrop-filter: blur(4px);}f-node.selected { z-index: 2;}f-node.selected,f-canvas.dragging-rio f-node:hover,f-canvas.dragging-lio f-node:hover { filter: drop-shadow(0 0 10px #00000061) drop-shadow(0 0 8px #4444dd);}f-node.closed f-element:not(:first-child) { display: none;}f-node.center { top: 50%; left: 50%; transform: translate( -50%, -50% );}f-node.top-right { top: 0; right: 0;}f-node.top-center { top: 0; left: 50%; transform: translateX( -50% );}f-node.top-left { top: 0; left: 0;}f-node { transition: filter 0.2s ease;}f-node { animation: .2s f-animation-open 1 alternate ease-out;}f-drop,f-menu,f-menu button,f-element,f-element input,f-element select,f-element button,f-element textarea { font-family: 'Open Sans', sans-serif; font-size: 13px; text-transform: capitalize; color: #eeeeee; outline: solid 0px #000; letter-spacing: .2px; margin: 0; padding: 0; border: 0; user-select: none; -webkit-tap-highlight-color: transparent; transition: background 0.2s ease;}f-element input { transition: background 0.1s ease;}f-element input,f-element select,f-element button,f-element textarea { background-color: #242427;}f-element { position: relative; width: calc( 100% - 14px ); background: rgba(45, 45, 48, 0.95); pointer-events: auto; border-bottom: 2px solid #232323; display: flex; padding-left: 7px; padding-right: 7px; padding-top: 2px; padding-bottom: 2px;}f-element { height: 24px;}f-element input { margin-top: 2px; margin-bottom: 2px; box-shadow: inset 0px 1px 1px rgb(0 0 0 / 20%), 0px 1px 0px rgb(255 255 255 / 5%); margin-left: 2px; margin-right: 2px; width: 100%; padding-left: 4px; padding-right: 4px;}f-element input.number { cursor: col-resize;}f-element input:focus[type='text'], f-element input:focus[type='range'], f-element input:focus[type='color'] { background: rgba( 0, 0, 0, 0.6 ); outline: solid 1px rgba( 0, 80, 200, 0.98 );}f-element input[type='color'] { appearance: none; padding: 0; margin-left: 2px; margin-right: 2px; height: calc( 100% - 4px ); margin-top: 2px; border: none; }f-element input[type='color']::-webkit-color-swatch-wrapper { padding: 2px;}f-element input[type='color']::-webkit-color-swatch { border: none; cursor: alias;}f-element input[type='range'] { appearance: none; width: 100%; overflow: hidden; padding: 0; cursor: ew-resize;}f-element input[type='range']::-webkit-slider-runnable-track { appearance: none; height: 10px; color: #13bba4; margin: 0;}f-element input[type='range']::-webkit-slider-thumb { appearance: none; width: 0; background: #434343; box-shadow: -500px 0 0 500px rgba( 0, 120, 255, 0.98 ); border-radius: 50%; border: 0 !important;}f-element input[type='range']::-webkit-slider-runnable-track { margin-left: -4px; margin-right: -5px;}f-element input[type='checkbox'] { appearance: none; cursor: pointer;}f-element input[type='checkbox'].toggle { height: 20px; width: 45px; border-radius: 16px; display: inline-block; position: relative; margin: 0; margin-top: 2px; background: linear-gradient( 0deg, #292929 0%, #0a0a0ac2 100% ); transition: all 0.2s ease;}f-element input[type='checkbox'].toggle:after { content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%; background: white; box-shadow: 0 1px 2px rgba(44, 44, 44, 0.2); transition: all 0.2s cubic-bezier(0.5, 0.1, 0.75, 1.35);}f-element input[type='checkbox'].toggle:checked { background: linear-gradient( 0deg, #0177fb 0%, #0177fb 100% );}f-element input[type='checkbox'].toggle:checked:after { transform: translatex(25px);}f-element.auto-height { display: table;}f-element textarea { width: calc( 100% - 18px ); padding-top: 1px; padding-bottom: 3px; padding-left: 3px; padding-right: 8px; margin-top: 2px; margin-left: 2px; height: calc( 100% - 8px ); max-height: 300px; border-radius: 2px; resize: none; box-shadow: inset 0px 1px 1px rgb(0 0 0 / 20%), 0px 1px 0px rgb(255 255 255 / 5%);}f-element.auto-height textarea { resize: auto;}f-element select { width: 100%; margin-top: 2px; margin-bottom: 2px; margin-left: 2px; margin-right: 2px; padding-left: 5px; cursor: pointer; box-shadow: inset 0px 1px 1px rgb(0 0 0 / 20%), 0px 1px 0px rgb(255 255 255 / 5%);}f-element f-toolbar { position: absolute; display: flex; top: 0; width: 100%; height: 100%; align-content: space-around;}f-element.output-right f-toolbar { right: 7px; float: right; justify-content: end;}f-element f-toolbar { margin-top: auto; margin-bottom: auto; margin-left: 3px; margin-right: 3px; font-size: 18px; line-height: 18px;}f-element f-toolbar button { opacity: .7; cursor: pointer; font-size: 14px; width: unset; height: unset; border-radius: unset; border: unset; outline: 0; background-color: unset; box-shadow: unset;}f-element f-toolbar button:hover,f-element f-toolbar button:active { opacity: 1; border: 0; background-color: unset;}f-element input.range-value { width: 60px; text-align: center;}f-menu.context button,f-element button { width: 100%; height: calc( 100% - 4px ); margin-left: 2px; margin-right: 2px; margin-top: 2px; border-radius: 3px; cursor: pointer;}f-element button { box-shadow: inset 1px 1px 1px 0 rgb(255 255 255 / 17%), inset -2px -2px 2px 0 rgb(0 0 0 / 26%);}f-element button:hover { color: #fff; background-color: #2a2a2a;}f-element button:active { border: 1px solid rgba( 0, 120, 255, 0.98 );}f-element f-inputs,f-element f-subinputs { display: flex; width: 100%;}f-element f-inputs { left: 100px; top: 50%; transform: translateY(-50%); position: absolute; width: calc( 100% - 106px ); height: calc( 100% - 4px ); z-index: 1;}f-element.inputs-disable f-inputs { filter: grayscale(100%); opacity: .5;}f-element.inputs-disable f-inputs input { pointer-events: none;}f-element f-label,f-element span { margin: auto; text-shadow: 1px 1px 0px #0007;}f-element f-label { padding-left: 4px; white-space: nowrap; position: absolute; top: 50%; transform: translateY(-50%); width: calc( 100% - 20px );}f-element.right f-label { text-align: right;}f-element f-label i { float: left; font-size: 18px; margin-right: 6px;}f-element f-label.center { width: 100%; text-align: center; display: block;}f-element.title { height: 29px; background-color: #3a3a3ab0; background-color: #3b3b43ed; cursor: all-scroll; border-top-left-radius: 6px; border-top-right-radius: 6px;}f-element.blue { background-color: #014fc5;}f-element.red { background-color: #bd0b0b;}f-element.green { background-color: #148d05;}f-element.yellow { background-color: #d6b100;}f-element.title.left { text-align: left; display: inline-grid; justify-content: start;}f-element.title span { text-align: center; font-size: 15px; padding-top: 2px;}f-element.title i { font-size: 18px; position: absolute; right: 10px; top: 50%; transform: translateY(-50%); opacity: .5;}f-element.output-right.title i { left: 10px; right: unset;}f-element.title.left span { text-align: left;}f-element f-io { border: 2px solid #dadada; width: 7px; height: 7px; position: absolute; background: #242427; border-radius: 8px; float: left; left: -7px; top: calc( 50% - 5px ); cursor: alias; box-shadow: 0 0 3px 2px #0000005e; z-index: 1;}f-element f-io.connect,f-canvas.dragging-rio f-element:hover f-io.lio,f-canvas.dragging-lio f-element:hover f-io.rio { border: 2px solid #0177fb; zoom: 1.4;}f-node.io-connect f-io:not(.connect) { border: 2px solid #dadada !important; zoom: 1 !important;}f-element f-io.rio { float: right; right: -7px; left: unset;}f-element f-disconnect { position: absolute; left: -35px; top: 50%; font-size: 22px; transform: translateY( -50% ); filter: drop-shadow(0 0 5px #000); text-shadow: 0px 0px 5px black; cursor: pointer;}f-element.output-right f-disconnect { right: -35px; left: unset;}f-element f-disconnect:hover { color: #ff3300;}f-element textarea::-webkit-scrollbar { width: 6px;}f-element textarea::-webkit-scrollbar-track { background: #111; } f-element textarea::-webkit-scrollbar-thumb { background: #0177fb; }f-element textarea::-webkit-scrollbar-thumb:hover { background: #1187ff; }f-element.small { height: 18px;}f-element.large { height: 36px;}body.connecting f-node:not(.io-connect) f-element:hover,f-element.select { background-color: rgba(61, 70, 82, 0.98);}f-drop { width: 100%; height: 100%; position: sticky; left: 0; top: 0; background: #02358417; text-align: center; justify-content: center; align-items: center; display: flex; box-shadow: inset 0 0 20px 10px #464ace17; pointer-events: none; transition: all .07s; opacity: 0; visibility: hidden;}f-drop.visible { visibility: unset; opacity: unset; transition: all .23s;}f-drop span { opacity: .5; font-size: 40px; text-shadow: 0px 0px 5px #000; font-weight: bold;}f-tooltip { pointer-events: none;}f-tooltip { position: absolute; left: 0; top: 0; background: rgba(0,0,0,.8); backdrop-filter: blur(4px); font-size: 14px; padding: 7px; border-radius: 10px; top: 50%; transform: translateY(-50%); visibility: hidden; pointer-events: none; opacity: 0; transition: all 0.3s ease; z-index: 150; white-space: nowrap;}f-menu.context { position: absolute; width: 170px; padding: 2px; margin: 0; background: #17171794; z-index: 110; font-size: 12px; border-radius: 6px; backdrop-filter: blur(6px); border: 1px solid #7e7e7e45; box-shadow: 3px 3px 6px rgba(0,0,0,.2); transition: opacity 0.2s ease, transform 0.1s ease;}f-menu.context.hidden { visibility: hidden; opacity: 0;}f-menu.context f-item { display: block; position: relative; margin: 0; padding: 0; white-space: nowrap;}f-menu.context f-item.submenu::after { content: ""; position: absolute; right: 6px; top: 50%; -webkit-transform: translateY(-50%); transform: translateY(-50%); border: 5px solid transparent; border-left-color: #808080;}f-menu.context f-item:hover > f-menu,f-menu.context f-item.active > f-menu { visibility: unset; transform: unset; opacity: unset;}f-menu.context f-menu { top: 0px; left: calc( 100% - 4px );}f-menu.context f-item button { overflow: visible; display: block; width: calc( 100% - 6px ); text-align: left; cursor: pointer; white-space: nowrap; padding: 6px 8px; border-radius: 3px; background: #2d2d32; border: 0; color: #ddd; margin: 3px; text-shadow: 1px 1px 0px #0007;}f-menu.context f-item button i { float: left; font-size: 16px;}f-menu.context f-item button span { margin-left: 6px;}f-menu.context f-item:hover > button,f-menu.context f-item.active > button { color: #fff; background: #313136;}f-menu.context f-item button:active { outline: solid 1px rgba( 0, 80, 200, 0.98 );}f-menu.context f-item f-tooltip { margin-left: 120px;}f-menu.circle { position: absolute; left: 30px; top: 30px; z-index: 100;}f-menu.circle f-item { display: flex; justify-content: end; align-content: space-around; margin-bottom: 12px;}f-menu.circle f-item button { width: 50px; height: 50px; font-size: 26px; background: #17171794; border-radius: 50%; backdrop-filter: blur(6px); border: 1px solid #7e7e7e45; line-height: 100%; cursor: pointer; box-shadow: 3px 3px 6px rgba(0,0,0,.2);}f-menu.circle f-item f-tooltip { margin-left: 50px;}.f-rounded f-node f-element,.f-rounded f-node f-element.title.left { border-radius: 10px 5px 10px 5px;}.f-rounded f-node f-element input, .f-rounded f-node f-element select,.f-rounded f-node f-element button,.f-rounded f-node f-element textarea,.f-rounded f-node f-element input[type='checkbox'].toggle,.f-rounded f-node f-element input[type='checkbox'].toggle:after { border-radius: 20px 10px;}.f-rounded f-node f-element input { padding-left: 7px; padding-right: 7px;}.f-rounded f-menu.context,.f-rounded f-menu.context f-item button { border-radius: 20px 10px;}@media (hover: hover) and (pointer: fine) { f-node:not(.selected):hover { filter: drop-shadow(0 0 6px #66666630); } f-element f-toolbar { visibility: hidden; opacity: 0; transition: opacity 0.2s ease; } f-node:hover > f-element f-toolbar { visibility: visible; opacity: 1; } f-element f-io:hover { border: 2px solid #0177fb; zoom: 1.4; } f-menu.circle f-item button:hover { background-color: #2a2a2a; } f-menu.circle f-item button:hover > f-tooltip, f-menu.context f-item button:hover > f-tooltip { visibility: visible; transform: translate(10px, -50%); opacity: 1; } f-menu.circle f-item button:focus > f-tooltip, f-menu.context f-item button:focus > f-tooltip { visibility: hidden; opacity: 0; }}f-canvas { will-change: top, left;}f-node { will-change: transform !important;}` ); |
|
|
|
const REVISION = '1'; |
|
|
|
const Styles = { |
|
icons: { |
|
close: '', |
|
unlink: '' |
|
} |
|
}; |
|
|
|
let _id = 0; |
|
|
|
class Serializer extends EventTarget { |
|
|
|
constructor() { |
|
|
|
super(); |
|
|
|
this._id = _id ++; |
|
|
|
this._serializable = true; |
|
|
|
} |
|
|
|
get id() { |
|
|
|
return this._id; |
|
|
|
} |
|
|
|
setSerializable( value ) { |
|
|
|
this._serializable = value; |
|
|
|
return this; |
|
|
|
} |
|
|
|
getSerializable() { |
|
|
|
return this._serializable; |
|
|
|
} |
|
|
|
serialize( /*data*/ ) { |
|
|
|
console.warn( 'Serializer: Abstract function.' ); |
|
|
|
} |
|
|
|
deserialize( /*data*/ ) { |
|
|
|
console.warn( 'Serializer: Abstract function.' ); |
|
|
|
} |
|
|
|
toJSON( data = null ) { |
|
|
|
let object = null; |
|
|
|
const id = this.id; |
|
|
|
if ( data !== null ) { |
|
|
|
const objects = data.objects; |
|
|
|
object = objects[ id ]; |
|
|
|
if ( object === undefined ) { |
|
|
|
object = { objects }; |
|
|
|
this.serialize( object ); |
|
|
|
delete object.objects; |
|
|
|
objects[ id ] = object; |
|
|
|
} |
|
|
|
} else { |
|
|
|
object = { objects: {} }; |
|
|
|
this.serialize( object ); |
|
|
|
} |
|
|
|
object.id = id; |
|
object.type = this.constructor.name; |
|
|
|
return object; |
|
|
|
} |
|
|
|
} |
|
|
|
const draggableDOM = ( dom, callback = null, className = 'dragging' ) => { |
|
|
|
let dragData = null; |
|
|
|
const getZoom = () => { |
|
|
|
let zoomDOM = dom; |
|
|
|
while ( zoomDOM && zoomDOM !== document ) { |
|
|
|
const zoom = zoomDOM.style.zoom; |
|
|
|
if ( zoom ) { |
|
|
|
return Number( zoom ); |
|
|
|
} |
|
|
|
zoomDOM = zoomDOM.parentNode; |
|
|
|
} |
|
|
|
return 1; |
|
|
|
}; |
|
|
|
const onMouseDown = ( e ) => { |
|
|
|
const event = e.touches ? e.touches[ 0 ] : e; |
|
|
|
e.stopImmediatePropagation(); |
|
|
|
dragData = { |
|
client: { x: event.clientX, y: event.clientY }, |
|
delta: { x: 0, y: 0 }, |
|
start: { x: dom.offsetLeft, y: dom.offsetTop }, |
|
dragging: false, |
|
isTouch: !! e.touches |
|
}; |
|
|
|
window.addEventListener( 'mousemove', onGlobalMouseMove ); |
|
window.addEventListener( 'mouseup', onGlobalMouseUp ); |
|
|
|
window.addEventListener( 'touchmove', onGlobalMouseMove ); |
|
window.addEventListener( 'touchend', onGlobalMouseUp ); |
|
|
|
}; |
|
|
|
const onGlobalMouseMove = ( e ) => { |
|
|
|
const { start, delta, client } = dragData; |
|
|
|
const event = e.touches ? e.touches[ 0 ] : e; |
|
|
|
const zoom = getZoom(); |
|
|
|
delta.x = ( event.clientX - client.x ) / zoom; |
|
delta.y = ( event.clientY - client.y ) / zoom; |
|
|
|
dragData.x = start.x + delta.x; |
|
dragData.y = start.y + delta.y; |
|
|
|
if ( dragData.dragging === true ) { |
|
|
|
if ( callback !== null ) { |
|
|
|
callback( dragData ); |
|
|
|
} else { |
|
|
|
dom.style.cssText += `; left: ${ dragData.x }px; top: ${ dragData.y }px;`; |
|
|
|
} |
|
|
|
e.stopImmediatePropagation(); |
|
|
|
} else { |
|
|
|
if ( Math.abs( delta.x ) > 2 || Math.abs( delta.y ) > 2 ) { |
|
|
|
dragData.dragging = true; |
|
|
|
dom.classList.add( 'drag' ); |
|
|
|
if ( className ) document.body.classList.add( className ); |
|
|
|
e.stopImmediatePropagation(); |
|
|
|
} |
|
|
|
} |
|
|
|
}; |
|
|
|
const onGlobalMouseUp = ( e ) => { |
|
|
|
e.stopImmediatePropagation(); |
|
|
|
dom.classList.remove( 'drag' ); |
|
|
|
if ( className ) document.body.classList.remove( className ); |
|
|
|
window.removeEventListener( 'mousemove', onGlobalMouseMove ); |
|
window.removeEventListener( 'mouseup', onGlobalMouseUp ); |
|
|
|
window.removeEventListener( 'touchmove', onGlobalMouseMove ); |
|
window.removeEventListener( 'touchend', onGlobalMouseUp ); |
|
|
|
if ( callback === null ) { |
|
|
|
dom.removeEventListener( 'mousedown', onMouseDown ); |
|
dom.removeEventListener( 'touchstart', onMouseDown ); |
|
|
|
} |
|
|
|
dragData.dragging = false; |
|
|
|
if ( callback !== null ) { |
|
|
|
callback( dragData ); |
|
|
|
} |
|
|
|
}; |
|
|
|
if ( dom instanceof Event ) { |
|
|
|
const e = dom; |
|
dom = e.target; |
|
|
|
onMouseDown( e ); |
|
|
|
} else { |
|
|
|
dom.addEventListener( 'mousedown', onMouseDown ); |
|
dom.addEventListener( 'touchstart', onMouseDown ); |
|
|
|
} |
|
|
|
}; |
|
|
|
const dispatchEventList = ( list, ...params ) => { |
|
|
|
for ( const callback of list ) { |
|
|
|
callback( ...params ); |
|
|
|
} |
|
|
|
}; |
|
|
|
const toPX = ( val ) => { |
|
|
|
if ( isNaN( val ) === false ) { |
|
|
|
val = `${ val }px`; |
|
|
|
} |
|
|
|
return val; |
|
|
|
}; |
|
|
|
const toHex = ( val ) => { |
|
|
|
if ( isNaN( val ) === false ) { |
|
|
|
val = `#${ val.toString( 16 ).padStart( 6, '0' ) }`; |
|
|
|
} |
|
|
|
return val; |
|
|
|
}; |
|
|
|
var Utils = /*#__PURE__*/Object.freeze({ |
|
__proto__: null, |
|
draggableDOM: draggableDOM, |
|
dispatchEventList: dispatchEventList, |
|
toPX: toPX, |
|
toHex: toHex |
|
}); |
|
|
|
class Link { |
|
|
|
constructor( inputElement = null, outputElement = null ) { |
|
|
|
this.inputElement = inputElement; |
|
this.outputElement = outputElement; |
|
|
|
} |
|
|
|
get lioElement() { |
|
|
|
if ( Link.InputDirection === 'left' ) { |
|
|
|
return this.outputElement; |
|
|
|
} else { |
|
|
|
return this.inputElement; |
|
|
|
} |
|
|
|
} |
|
|
|
get rioElement() { |
|
|
|
if ( Link.InputDirection === 'left' ) { |
|
|
|
return this.inputElement; |
|
|
|
} else { |
|
|
|
return this.outputElement; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
//Link.InputDirection = 'right'; |
|
Link.InputDirection = 'left'; |
|
|
|
let selected = null; |
|
|
|
class Element extends Serializer { |
|
|
|
constructor( draggable = false ) { |
|
|
|
super(); |
|
|
|
const dom = document.createElement( 'f-element' ); |
|
dom.element = this; |
|
|
|
const onSelect = ( e ) => { |
|
|
|
let element = this; |
|
|
|
if ( e.changedTouches && e.changedTouches.length > 0 ) { |
|
|
|
const touch = e.changedTouches[ 0 ]; |
|
|
|
let overDOM = document.elementFromPoint( touch.clientX, touch.clientY ); |
|
|
|
while ( overDOM && ( ! overDOM.element || ! overDOM.element.isElement ) ) { |
|
|
|
overDOM = overDOM.parentNode; |
|
|
|
} |
|
|
|
element = overDOM ? overDOM.element : null; |
|
|
|
} |
|
|
|
selected = element; |
|
|
|
}; |
|
|
|
if ( draggable === false ) { |
|
|
|
dom.ontouchstart = dom.onmousedown = ( e ) => { |
|
|
|
e.stopPropagation(); |
|
|
|
}; |
|
|
|
} |
|
|
|
dom.addEventListener( 'mouseup', onSelect, true ); |
|
dom.addEventListener( 'touchend', onSelect ); |
|
|
|
this.inputs = []; |
|
|
|
this.links = []; |
|
|
|
this.dom = dom; |
|
|
|
this.lioLength = 0; |
|
this.rioLength = 0; |
|
|
|
this.events = { |
|
'connect': [], |
|
'connectChildren': [] |
|
}; |
|
|
|
this.node = null; |
|
|
|
this.style = ''; |
|
|
|
this.extra = null; |
|
|
|
this.enabledInputs = true; |
|
|
|
this.visible = true; |
|
|
|
this.inputsDOM = dom; |
|
|
|
this.disconnectDOM = null; |
|
|
|
this.lioDOM = this._createIO( 'lio' ); |
|
this.rioDOM = this._createIO( 'rio' ); |
|
|
|
this.dom.classList.add( `output-${ Link.InputDirection }` ); |
|
|
|
this.dom.appendChild( this.lioDOM ); |
|
this.dom.appendChild( this.rioDOM ); |
|
|
|
this.addEventListener( 'connect', ( ) => { |
|
|
|
dispatchEventList( this.events.connect, this ); |
|
|
|
} ); |
|
|
|
this.addEventListener( 'connectChildren', ( ) => { |
|
|
|
dispatchEventList( this.events.connectChildren, this ); |
|
|
|
} ); |
|
|
|
} |
|
|
|
onConnect( callback, childrens = false ) { |
|
|
|
this.events.connect.push( callback ); |
|
|
|
if ( childrens ) { |
|
|
|
this.events.connectChildren.push( callback ); |
|
|
|
} |
|
|
|
return this; |
|
|
|
} |
|
|
|
setExtra( value ) { |
|
|
|
this.extra = value; |
|
|
|
return this; |
|
|
|
} |
|
|
|
getExtra() { |
|
|
|
return this.extra; |
|
|
|
} |
|
|
|
setVisible( value ) { |
|
|
|
this.visible = value; |
|
|
|
this.dom.style.display = value ? '' : 'none'; |
|
|
|
return this; |
|
|
|
} |
|
|
|
getVisible() { |
|
|
|
return this.visible; |
|
|
|
} |
|
|
|
setEnabledInputs( value ) { |
|
|
|
const dom = this.dom; |
|
|
|
if ( !this.enabledInputs ) dom.classList.remove( 'inputs-disable'); |
|
|
|
if ( !value ) dom.classList.add( 'inputs-disable' ); |
|
|
|
this.enabledInputs = value; |
|
|
|
return this; |
|
|
|
} |
|
|
|
getEnabledInputs() { |
|
|
|
return this.enabledInputs; |
|
|
|
} |
|
|
|
setStyle( style ) { |
|
|
|
const dom = this.dom; |
|
|
|
if ( this.style ) dom.classList.remove( this.style ); |
|
|
|
if ( style ) dom.classList.add( style ); |
|
|
|
this.style = style; |
|
|
|
return this; |
|
|
|
} |
|
|
|
setInput( length ) { |
|
|
|
if ( Link.InputDirection === 'left' ) { |
|
|
|
return this.setLIO( length ); |
|
|
|
} else { |
|
|
|
return this.setRIO( length ); |
|
|
|
} |
|
|
|
} |
|
|
|
setOutput( length ) { |
|
|
|
if ( Link.InputDirection === 'left' ) { |
|
|
|
return this.setRIO( length ); |
|
|
|
} else { |
|
|
|
return this.setLIO( length ); |
|
|
|
} |
|
|
|
} |
|
|
|
get inputLength() { |
|
|
|
if ( Link.InputDirection === 'left' ) { |
|
|
|
return this.lioLength; |
|
|
|
} else { |
|
|
|
return this.rioLength; |
|
|
|
} |
|
|
|
} |
|
|
|
get outputLength() { |
|
|
|
if ( Link.InputDirection === 'left' ) { |
|
|
|
return this.rioLength; |
|
|
|
} else { |
|
|
|
return this.lioLength; |
|
|
|
} |
|
|
|
} |
|
|
|
setLIO( length ) { |
|
|
|
this.lioLength = length; |
|
|
|
this.lioDOM.style.visibility = length > 0 ? '' : 'hidden'; |
|
|
|
return this; |
|
|
|
} |
|
|
|
setRIO( length ) { |
|
|
|
this.rioLength = length; |
|
|
|
this.rioDOM.style.visibility = length > 0 ? '' : 'hidden'; |
|
|
|
return this; |
|
|
|
} |
|
|
|
add( input ) { |
|
|
|
this.inputs.push( input ); |
|
|
|
input.element = this; |
|
|
|
this.inputsDOM.appendChild( input.dom ); |
|
|
|
return this; |
|
|
|
} |
|
|
|
setHeight( val ) { |
|
|
|
this.dom.style.height = toPX( val ); |
|
|
|
return this; |
|
|
|
} |
|
|
|
getHeight() { |
|
|
|
return this.dom.style.height; |
|
|
|
} |
|
|
|
connect( element = null ) { |
|
|
|
if ( this.disconnectDOM !== null ) { |
|
|
|
// remove the current input |
|
|
|
this.disconnectDOM.dispatchEvent( new Event( 'disconnect' ) ); |
|
|
|
} |
|
|
|
if ( element !== null ) { |
|
|
|
const link = new Link( this, element ); |
|
|
|
this.links.push( link ); |
|
|
|
if ( this.disconnectDOM === null ) { |
|
|
|
this.disconnectDOM = document.createElement( 'f-disconnect' ); |
|
this.disconnectDOM.innerHTML = Styles.icons.unlink ? `<i class='${ Styles.icons.unlink }'></i>` : '✖'; |
|
|
|
this.dom.appendChild( this.disconnectDOM ); |
|
|
|
const onDisconnect = () => { |
|
|
|
this.links = []; |
|
this.dom.removeChild( this.disconnectDOM ); |
|
|
|
this.disconnectDOM.removeEventListener( 'mousedown', onClick, true ); |
|
this.disconnectDOM.removeEventListener( 'touchstart', onClick, true ); |
|
this.disconnectDOM.removeEventListener( 'disconnect', onDisconnect, true ); |
|
|
|
element.removeEventListener( 'connect', onConnect ); |
|
element.removeEventListener( 'connectChildren', onConnect ); |
|
element.removeEventListener( 'nodeConnect', onConnect ); |
|
element.removeEventListener( 'nodeConnectChildren', onConnect ); |
|
element.removeEventListener( 'dispose', onDispose ); |
|
|
|
this.disconnectDOM = null; |
|
|
|
}; |
|
|
|
const onConnect = ( e ) => { |
|
|
|
this.dispatchEvent( new Event( 'connectChildren' ) ); |
|
|
|
}; |
|
|
|
const onDispose = () => { |
|
|
|
this.connect(); |
|
|
|
}; |
|
|
|
const onClick = ( e ) => { |
|
|
|
e.stopPropagation(); |
|
|
|
this.connect(); |
|
|
|
}; |
|
|
|
this.disconnectDOM.addEventListener( 'mousedown', onClick, true ); |
|
this.disconnectDOM.addEventListener( 'touchstart', onClick, true ); |
|
this.disconnectDOM.addEventListener( 'disconnect', onDisconnect, true ); |
|
element.addEventListener( 'connect', onConnect ); |
|
element.addEventListener( 'connectChildren', onConnect ); |
|
element.addEventListener( 'nodeConnect', onConnect ); |
|
element.addEventListener( 'nodeConnectChildren', onConnect ); |
|
element.addEventListener( 'dispose', onDispose ); |
|
|
|
} |
|
|
|
} |
|
|
|
this.dispatchEvent( new Event( 'connect' ) ); |
|
|
|
return this; |
|
|
|
} |
|
|
|
dispose() { |
|
|
|
this.dispatchEvent( new Event( 'dispose' ) ); |
|
|
|
} |
|
|
|
serialize( data ) { |
|
|
|
const height = this.getHeight(); |
|
|
|
const inputs = []; |
|
const links = []; |
|
|
|
for ( const input of this.inputs ) { |
|
|
|
inputs.push( input.toJSON( data ).id ); |
|
|
|
} |
|
|
|
for ( const link of this.links ) { |
|
|
|
if ( link.inputElement !== null && link.outputElement !== null ) { |
|
|
|
links.push( link.outputElement.toJSON( data ).id ); |
|
|
|
} |
|
|
|
} |
|
|
|
if ( this.inputLength > 0 ) data.inputLength = this.inputLength; |
|
if ( this.outputLength > 0 ) data.outputLength = this.outputLength; |
|
|
|
if ( inputs.length > 0 ) data.inputs = inputs; |
|
if ( links.length > 0 ) data.links = links; |
|
|
|
if ( this.style !== '' ) { |
|
|
|
data.style = this.style; |
|
|
|
} |
|
|
|
if ( height !== '' ) { |
|
|
|
data.height = height; |
|
|
|
} |
|
|
|
} |
|
|
|
deserialize( data ) { |
|
|
|
if ( data.inputLength !== undefined ) this.setInput( data.inputLength ); |
|
if ( data.outputLength !== undefined ) this.setOutput( data.outputLength ); |
|
|
|
if ( data.inputs !== undefined ) { |
|
|
|
const inputs = this.inputs; |
|
|
|
if ( inputs.length > 0 ) { |
|
|
|
let index = 0; |
|
|
|
for ( const id of data.inputs ) { |
|
|
|
data.objects[ id ] = inputs[ index ++ ]; |
|
|
|
} |
|
|
|
} else { |
|
|
|
for ( const id of data.inputs ) { |
|
|
|
this.add( data.objects[ id ] ); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
if ( data.links !== undefined ) { |
|
|
|
for ( const id of data.links ) { |
|
|
|
this.connect( data.objects[ id ] ); |
|
|
|
} |
|
|
|
} |
|
|
|
if ( data.style !== undefined ) { |
|
|
|
this.setStyle( data.style ); |
|
|
|
} |
|
|
|
if ( data.height !== undefined ) { |
|
|
|
this.setHeight( data.height ); |
|
|
|
} |
|
|
|
} |
|
|
|
get linkedExtra() { |
|
|
|
const linkedElement = this.linkedElement; |
|
|
|
return linkedElement ? linkedElement.getExtra() : null; |
|
|
|
} |
|
|
|
get linkedElement() { |
|
|
|
const link = this.link; |
|
|
|
return link ? link.outputElement : null; |
|
|
|
} |
|
|
|
get link() { |
|
|
|
return this.links[ 0 ]; |
|
|
|
} |
|
|
|
_createIO( type ) { |
|
|
|
const { dom } = this; |
|
|
|
const ioDOM = document.createElement( 'f-io' ); |
|
ioDOM.style.visibility = 'hidden'; |
|
ioDOM.className = type; |
|
|
|
const onConnectEvent = ( e ) => { |
|
|
|
e.preventDefault(); |
|
|
|
e.stopPropagation(); |
|
|
|
selected = null; |
|
|
|
const nodeDOM = this.node.dom; |
|
|
|
nodeDOM.classList.add( 'io-connect' ); |
|
|
|
ioDOM.classList.add( 'connect' ); |
|
dom.classList.add( 'select' ); |
|
|
|
const defaultOutput = Link.InputDirection === 'left' ? 'lio' : 'rio'; |
|
|
|
const link = type === defaultOutput ? new Link( this ) : new Link( null, this ); |
|
|
|
this.links.push( link ); |
|
|
|
draggableDOM( e, ( data ) => { |
|
|
|
if ( data.dragging === false ) { |
|
|
|
nodeDOM.classList.remove( 'io-connect' ); |
|
|
|
ioDOM.classList.remove( 'connect' ); |
|
dom.classList.remove( 'select' ); |
|
|
|
this.links.splice( this.links.indexOf( link ), 1 ); |
|
|
|
if ( selected !== null ) { |
|
|
|
if ( type === defaultOutput ) { |
|
|
|
link.outputElement = selected; |
|
|
|
} else { |
|
|
|
link.inputElement = selected; |
|
|
|
} |
|
|
|
// check if is an is circular link |
|
|
|
if ( link.outputElement.node.isCircular( link.inputElement.node ) ) { |
|
|
|
return; |
|
|
|
} |
|
|
|
// |
|
|
|
if ( link.inputElement.inputLength > 0 && link.outputElement.outputLength > 0 ) { |
|
|
|
link.inputElement.connect( link.outputElement ); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
}, 'connecting' ); |
|
|
|
}; |
|
|
|
ioDOM.addEventListener( 'mousedown', onConnectEvent, true ); |
|
ioDOM.addEventListener( 'touchstart', onConnectEvent, true ); |
|
|
|
return ioDOM; |
|
|
|
} |
|
|
|
} |
|
|
|
Element.prototype.isElement = true; |
|
|
|
class Input extends Serializer { |
|
|
|
constructor( dom ) { |
|
|
|
super(); |
|
|
|
this.dom = dom; |
|
|
|
this.element = null; |
|
|
|
this.extra = null; |
|
|
|
this.events = { |
|
'change': [], |
|
'click': [] |
|
}; |
|
|
|
this.addEventListener( 'change', ( ) => { |
|
|
|
dispatchEventList( this.events.change, this ); |
|
|
|
} ); |
|
|
|
this.addEventListener( 'click', ( ) => { |
|
|
|
dispatchEventList( this.events.click, this ); |
|
|
|
} ); |
|
|
|
} |
|
|
|
setExtra( value ) { |
|
|
|
this.extra = value; |
|
|
|
return this; |
|
|
|
} |
|
|
|
getExtra() { |
|
|
|
return this.extra; |
|
|
|
} |
|
|
|
setToolTip( text ) { |
|
|
|
const div = document.createElement( 'f-tooltip' ); |
|
div.innerText = text; |
|
|
|
this.dom.appendChild( div ); |
|
|
|
return this; |
|
|
|
} |
|
|
|
onChange( callback ) { |
|
|
|
this.events.change.push( callback ); |
|
|
|
return this; |
|
|
|
} |
|
|
|
onClick( callback ) { |
|
|
|
this.events.click.push( callback ); |
|
|
|
return this; |
|
|
|
} |
|
|
|
setValue( value, dispatch = true ) { |
|
|
|
this.dom.value = value; |
|
|
|
if ( dispatch ) this.dispatchEvent( new Event( 'change' ) ); |
|
|
|
return this; |
|
|
|
} |
|
|
|
getValue() { |
|
|
|
return this.dom.value; |
|
|
|
} |
|
|
|
serialize( data ) { |
|
|
|
data.value = this.getValue(); |
|
|
|
} |
|
|
|
deserialize( data ) { |
|
|
|
this.setValue( data.value ); |
|
|
|
} |
|
|
|
} |
|
|
|
Input.prototype.isInput = true; |
|
|
|
class Node extends Serializer { |
|
|
|
constructor() { |
|
|
|
super(); |
|
|
|
const dom = document.createElement( 'f-node' ); |
|
|
|
const onDown = () => { |
|
|
|
const canvas = this.canvas; |
|
|
|
if ( canvas !== null ) { |
|
|
|
canvas.select( this ); |
|
|
|
} |
|
|
|
}; |
|
|
|
dom.addEventListener( 'mousedown', onDown, true ); |
|
dom.addEventListener( 'touchstart', onDown, true ); |
|
|
|
this._onConnect = ( e ) => { |
|
|
|
const { target } = e; |
|
|
|
for ( const element of this.elements ) { |
|
|
|
if ( element !== target ) { |
|
|
|
element.dispatchEvent( new Event( 'nodeConnect' ) ); |
|
|
|
} |
|
|
|
} |
|
|
|
}; |
|
|
|
this._onConnectChildren = ( e ) => { |
|
|
|
const { target } = e; |
|
|
|
for ( const element of this.elements ) { |
|
|
|
if ( element !== target ) { |
|
|
|
element.dispatchEvent( new Event( 'nodeConnectChildren' ) ); |
|
|
|
} |
|
|
|
} |
|
|
|
}; |
|
|
|
this.dom = dom; |
|
|
|
this.style = ''; |
|
|
|
this.canvas = null; |
|
|
|
this.elements = []; |
|
|
|
this.events = { |
|
'focus': [], |
|
'blur': [] |
|
}; |
|
|
|
this.setWidth( 300 ).setPosition( 0, 0 ); |
|
|
|
} |
|
|
|
onFocus( callback ) { |
|
|
|
this.events.focus.push( callback ); |
|
|
|
return this; |
|
|
|
} |
|
|
|
onBlur( callback ) { |
|
|
|
this.events.blur.push( callback ); |
|
|
|
return this; |
|
|
|
} |
|
|
|
setStyle( style ) { |
|
|
|
const dom = this.dom; |
|
|
|
if ( this.style ) dom.classList.remove( this.style ); |
|
|
|
if ( style ) dom.classList.add( style ); |
|
|
|
this.style = style; |
|
|
|
return this; |
|
|
|
} |
|
|
|
setPosition( x, y ) { |
|
|
|
const dom = this.dom; |
|
|
|
dom.style.left = toPX( x ); |
|
dom.style.top = toPX( y ); |
|
|
|
return this; |
|
|
|
} |
|
|
|
getPosition() { |
|
|
|
const dom = this.dom; |
|
|
|
return { |
|
x: parseInt( dom.style.left ), |
|
y: parseInt( dom.style.top ) |
|
}; |
|
|
|
} |
|
|
|
setWidth( val ) { |
|
|
|
this.dom.style.width = toPX( val ); |
|
|
|
return this; |
|
|
|
} |
|
|
|
getWidth() { |
|
|
|
return parseInt( this.dom.style.width ); |
|
|
|
} |
|
|
|
add( element ) { |
|
|
|
this.elements.push( element ); |
|
|
|
element.node = this; |
|
element.addEventListener( 'connect', this._onConnect ); |
|
element.addEventListener( 'connectChildren', this._onConnectChildren ); |
|
|
|
this.dom.appendChild( element.dom ); |
|
|
|
return this; |
|
|
|
} |
|
|
|
remove( element ) { |
|
|
|
this.elements.splice( this.elements.indexOf( element ), 1 ); |
|
|
|
element.node = null; |
|
element.removeEventListener( 'connect', this._onConnect ); |
|
element.removeEventListener( 'connectChildren', this._onConnectChildren ); |
|
|
|
this.dom.removeChild( element.dom ); |
|
|
|
return this; |
|
|
|
} |
|
|
|
dispose() { |
|
|
|
const canvas = this.canvas; |
|
|
|
if ( canvas !== null ) canvas.remove( this ); |
|
|
|
for ( const element of this.elements ) { |
|
|
|
element.dispose(); |
|
|
|
} |
|
|
|
this.dispatchEvent( new Event( 'dispose' ) ); |
|
|
|
} |
|
|
|
isCircular( node ) { |
|
|
|
if ( node === this ) return true; |
|
|
|
const links = this.getLinks(); |
|
|
|
for ( const link of links ) { |
|
|
|
if ( link.outputElement.node.isCircular( node ) ) { |
|
|
|
return true; |
|
|
|
} |
|
|
|
} |
|
|
|
return false; |
|
|
|
} |
|
|
|
getLinks() { |
|
|
|
const links = []; |
|
|
|
for ( const element of this.elements ) { |
|
|
|
links.push( ...element.links ); |
|
|
|
} |
|
|
|
return links; |
|
|
|
} |
|
|
|
serialize( data ) { |
|
|
|
const { x, y, style } = this.getPosition(); |
|
|
|
const elements = []; |
|
|
|
for ( const element of this.elements ) { |
|
|
|
elements.push( element.toJSON( data ).id ); |
|
|
|
} |
|
|
|
data.x = x; |
|
data.y = y; |
|
data.width = this.getWidth(); |
|
data.elements = elements; |
|
|
|
if ( style !== '' ) { |
|
|
|
data.style = style; |
|
|
|
} |
|
|
|
} |
|
|
|
deserialize( data ) { |
|
|
|
this.setPosition( data.x, data.y ); |
|
this.setWidth( data.width ); |
|
|
|
if ( data.style !== undefined ) { |
|
|
|
this.setStyle( data.style ); |
|
|
|
} |
|
|
|
const elements = this.elements; |
|
|
|
if ( elements.length > 0 ) { |
|
|
|
let index = 0; |
|
|
|
for ( const id of data.elements ) { |
|
|
|
data.objects[ id ] = elements[ index ++ ]; |
|
|
|
} |
|
|
|
} else { |
|
|
|
for ( const id of data.elements ) { |
|
|
|
this.add( data.objects[ id ] ); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
Node.prototype.isNode = true; |
|
|
|
class DraggableElement extends Element { |
|
|
|
constructor( draggable = true ) { |
|
|
|
super( true ); |
|
|
|
this.draggable = draggable; |
|
|
|
const onDrag = ( e ) => { |
|
|
|
e.preventDefault(); |
|
|
|
if ( this.draggable === true ) { |
|
|
|
draggableDOM( this.node.dom ); |
|
|
|
} |
|
|
|
}; |
|
|
|
const { dom } = this; |
|
|
|
dom.addEventListener( 'mousedown', onDrag, true ); |
|
dom.addEventListener( 'touchstart', onDrag, true ); |
|
|
|
} |
|
|
|
} |
|
|
|
class TitleElement extends DraggableElement { |
|
|
|
constructor( title, draggable = true ) { |
|
|
|
super( draggable ); |
|
|
|
const { dom } = this; |
|
|
|
dom.className = 'title'; |
|
|
|
const spanDOM = document.createElement( 'span' ); |
|
spanDOM.innerText = title; |
|
|
|
const iconDOM = document.createElement( 'i' ); |
|
|
|
const toolbarDOM = document.createElement( 'f-toolbar' ); |
|
|
|
this.buttons = []; |
|
|
|
this.spanDOM = spanDOM; |
|
this.iconDOM = iconDOM; |
|
this.toolbarDOM = toolbarDOM; |
|
|
|
dom.appendChild( spanDOM ); |
|
dom.appendChild( iconDOM ); |
|
dom.appendChild( toolbarDOM ); |
|
|
|
} |
|
|
|
setIcon( value ) { |
|
|
|
this.iconDOM.className = value; |
|
|
|
return this; |
|
|
|
} |
|
|
|
getIcon() { |
|
|
|
return this.iconDOM.className; |
|
|
|
} |
|
|
|
setTitle( value ) { |
|
|
|
this.spanDOM.innerText = value; |
|
|
|
return this; |
|
|
|
} |
|
|
|
getTitle() { |
|
|
|
return this.spanDOM.innerText; |
|
|
|
} |
|
|
|
addButton( button ) { |
|
|
|
this.buttons.push( button ); |
|
|
|
this.toolbarDOM.appendChild( button.dom ); |
|
|
|
return this; |
|
|
|
} |
|
|
|
serialize( data ) { |
|
|
|
super.serialize( data ); |
|
|
|
const title = this.getTitle(); |
|
const icon = this.getIcon(); |
|
|
|
data.title = title; |
|
|
|
if ( icon !== '' ) { |
|
|
|
data.icon = icon; |
|
|
|
} |
|
|
|
} |
|
|
|
deserialize( data ) { |
|
|
|
super.deserialize( data ); |
|
|
|
this.setTitle( data.title ); |
|
|
|
if ( data.icon !== undefined ) { |
|
|
|
this.setIcon( data.icon ); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
const drawLine = ( p1x, p1y, p2x, p2y, invert, size, color, ctx ) => { |
|
|
|
const offset = 100 * ( invert ? - 1 : 1 ); |
|
|
|
ctx.beginPath(); |
|
|
|
ctx.moveTo( p1x, p1y ); |
|
|
|
ctx.bezierCurveTo( |
|
p1x + offset, p1y, |
|
p2x - offset, p2y, |
|
p2x, p2y |
|
); |
|
|
|
ctx.lineWidth = size; |
|
ctx.strokeStyle = color; |
|
ctx.stroke(); |
|
|
|
}; |
|
|
|
const colors = [ |
|
'#ff4444', |
|
'#44ff44', |
|
'#4444ff' |
|
]; |
|
|
|
const dropNode = new Node().add( new TitleElement( 'File' ) ).setWidth( 250 ); |
|
|
|
class Canvas extends Serializer { |
|
|
|
constructor() { |
|
|
|
super(); |
|
|
|
const dom = document.createElement( 'f-canvas' ); |
|
const contentDOM = document.createElement( 'f-content' ); |
|
const areaDOM = document.createElement( 'f-area' ); |
|
const dropDOM = document.createElement( 'f-drop' ); |
|
|
|
const canvas = document.createElement( 'canvas' ); |
|
const frontCanvas = document.createElement( 'canvas' ); |
|
|
|
const context = canvas.getContext( '2d' ); |
|
const frontContext = frontCanvas.getContext( '2d' ); |
|
|
|
this.dom = dom; |
|
|
|
this.contentDOM = contentDOM; |
|
this.areaDOM = areaDOM; |
|
this.dropDOM = dropDOM; |
|
|
|
this.canvas = canvas; |
|
this.frontCanvas = frontCanvas; |
|
|
|
this.context = context; |
|
this.frontContext = frontContext; |
|
|
|
this.width = 10000; |
|
this.height = 10000; |
|
|
|
this.clientX = 0; |
|
this.clientY = 0; |
|
|
|
this.relativeClientX = 0; |
|
this.relativeClientY = 0; |
|
|
|
this.zoom = 1; |
|
|
|
this.nodes = []; |
|
|
|
this.selected = null; |
|
|
|
this.updating = false; |
|
|
|
this.droppedItems = []; |
|
|
|
this.events = { |
|
'drop': [] |
|
}; |
|
|
|
frontCanvas.className = 'front'; |
|
|
|
contentDOM.style.left = toPX( this.centerX ); |
|
contentDOM.style.top = toPX( this.centerY ); |
|
|
|
areaDOM.style.width = `calc( 100% + ${ this.width }px )`; |
|
areaDOM.style.height = `calc( 100% + ${ this.height }px )`; |
|
|
|
dropDOM.innerHTML = '<span>drop your file</span>'; |
|
|
|
dom.appendChild( dropDOM ); |
|
dom.appendChild( canvas ); |
|
dom.appendChild( frontCanvas ); |
|
dom.appendChild( contentDOM ); |
|
dom.appendChild( areaDOM ); |
|
/* |
|
let zoomTouchData = null; |
|
|
|
const onZoomStart = () => { |
|
|
|
zoomTouchData = null; |
|
|
|
}; |
|
*/ |
|
const onZoom = ( e ) => { |
|
|
|
if ( e.touches ) { |
|
|
|
if ( e.touches.length === 2 ) { |
|
|
|
e.preventDefault(); |
|
|
|
e.stopImmediatePropagation(); |
|
/* |
|
const clientX = ( e.touches[ 0 ].clientX + e.touches[ 1 ].clientX ) / 2; |
|
const clientY = ( e.touches[ 0 ].clientY + e.touches[ 1 ].clientY ) / 2; |
|
|
|
const distance = Math.hypot( |
|
e.touches[ 0 ].clientX - e.touches[ 1 ].clientX, |
|
e.touches[ 0 ].clientY - e.touches[ 1 ].clientY |
|
); |
|
|
|
if ( zoomTouchData === null ) { |
|
|
|
zoomTouchData = { |
|
distance |
|
}; |
|
|
|
} |
|
|
|
const delta = ( zoomTouchData.distance - distance ); |
|
zoomTouchData.distance = distance; |
|
|
|
let zoom = Math.min( Math.max( this.zoom - delta * .01, .5 ), 1.2 ); |
|
|
|
if ( zoom < .52 ) zoom = .5; |
|
else if ( zoom > .98 ) zoom = 1; |
|
|
|
contentDOM.style.left = toPX( this.centerX / zoom ); |
|
contentDOM.style.top = toPX( this.centerY / zoom ); |
|
contentDOM.style.zoom = this.zoom = zoom; |
|
*/ |
|
|
|
} |
|
|
|
} else { |
|
|
|
e.preventDefault(); |
|
|
|
e.stopImmediatePropagation(); |
|
/* |
|
const delta = e.deltaY / 100; |
|
const zoom = Math.min( Math.max( this.zoom - delta * .1, .5 ), 1 ); |
|
|
|
contentDOM.style.left = toPX( this.centerX / zoom ); |
|
contentDOM.style.top = toPX( this.centerY / zoom ); |
|
contentDOM.style.zoom = this.zoom = zoom; |
|
*/ |
|
|
|
} |
|
|
|
}; |
|
|
|
dom.addEventListener( 'wheel', onZoom ); |
|
dom.addEventListener( 'touchmove', onZoom ); |
|
//dom.addEventListener( 'touchstart', onZoomStart ); |
|
|
|
let dropEnterCount = 0; |
|
|
|
const dragState = ( enter ) => { |
|
|
|
if ( enter ) { |
|
|
|
if ( dropEnterCount ++ === 0 ) { |
|
|
|
this.droppedItems = []; |
|
|
|
dropDOM.classList.add( 'visible' ); |
|
|
|
this.add( dropNode ); |
|
|
|
} |
|
|
|
} else if ( -- dropEnterCount === 0 ) { |
|
|
|
dropDOM.classList.remove( 'visible' ); |
|
|
|
this.remove( dropNode ); |
|
|
|
} |
|
|
|
}; |
|
|
|
dom.addEventListener( 'dragenter', () => { |
|
|
|
dragState( true ); |
|
|
|
} ); |
|
|
|
dom.addEventListener( 'dragleave', () => { |
|
|
|
dragState( false ); |
|
|
|
} ); |
|
|
|
dom.addEventListener( 'dragover', ( e ) => { |
|
|
|
e.preventDefault(); |
|
|
|
const { relativeClientX, relativeClientY } = this; |
|
|
|
const centerNodeX = dropNode.getWidth() / 2; |
|
|
|
dropNode.setPosition( relativeClientX - centerNodeX, relativeClientY - 20 ); |
|
|
|
} ); |
|
|
|
dom.addEventListener( 'drop', ( e ) => { |
|
|
|
e.preventDefault(); |
|
|
|
dragState( false ); |
|
|
|
this.droppedItems = e.dataTransfer.items; |
|
|
|
dispatchEventList( this.events.drop, this ); |
|
|
|
} ); |
|
|
|
draggableDOM( dom, ( data ) => { |
|
|
|
const { delta, isTouch } = data; |
|
|
|
if ( ! isTouch ) { |
|
|
|
if ( data.scrollTop === undefined ) { |
|
|
|
data.scrollLeft = dom.scrollLeft; |
|
data.scrollTop = dom.scrollTop; |
|
|
|
} |
|
|
|
dom.scrollLeft = data.scrollLeft - delta.x; |
|
dom.scrollTop = data.scrollTop - delta.y; |
|
|
|
} |
|
|
|
if ( data.dragging ) { |
|
|
|
dom.classList.add( 'grabbing' ); |
|
|
|
} else { |
|
|
|
dom.classList.remove( 'grabbing' ); |
|
|
|
} |
|
|
|
}, 'dragging-canvas' ); |
|
|
|
this._onMoveEvent = ( e ) => { |
|
|
|
const event = e.touches ? e.touches[ 0 ] : e; |
|
const { zoom, rect } = this; |
|
|
|
this.clientX = event.clientX; |
|
this.clientY = event.clientY; |
|
|
|
this.relativeClientX = ( ( ( dom.scrollLeft - this.centerX ) + event.clientX ) - rect.left ) / zoom; |
|
this.relativeClientY = ( ( ( dom.scrollTop - this.centerY ) + event.clientY ) - rect.top ) / zoom; |
|
|
|
}; |
|
|
|
this._onContentLoaded = () => { |
|
|
|
this.centralize(); |
|
|
|
}; |
|
|
|
this._onUpdate = () => { |
|
|
|
this.update(); |
|
|
|
}; |
|
|
|
this.start(); |
|
|
|
} |
|
|
|
get rect() { |
|
|
|
return this.dom.getBoundingClientRect(); |
|
|
|
} |
|
|
|
get relativeX() { |
|
|
|
return this.dom.scrollLeft - this.centerX; |
|
|
|
} |
|
|
|
get relativeY() { |
|
|
|
return this.dom.scrollTop - this.centerY; |
|
|
|
} |
|
|
|
get centerX() { |
|
|
|
return this.width / 2; |
|
|
|
} |
|
|
|
get centerY() { |
|
|
|
return this.height / 2; |
|
|
|
} |
|
|
|
onDrop( callback ) { |
|
|
|
this.events.drop.push( callback ); |
|
|
|
return this; |
|
|
|
} |
|
|
|
start() { |
|
|
|
this.updating = true; |
|
|
|
document.addEventListener( 'wheel', this._onMoveEvent, true ); |
|
|
|
document.addEventListener( 'mousedown', this._onMoveEvent, true ); |
|
document.addEventListener( 'touchstart', this._onMoveEvent, true ); |
|
|
|
document.addEventListener( 'mousemove', this._onMoveEvent, true ); |
|
document.addEventListener( 'touchmove', this._onMoveEvent, true ); |
|
|
|
document.addEventListener( 'dragover', this._onMoveEvent, true ); |
|
|
|
document.addEventListener( 'DOMContentLoaded', this._onContentLoaded ); |
|
|
|
requestAnimationFrame( this._onUpdate ); |
|
|
|
} |
|
|
|
stop() { |
|
|
|
this.updating = false; |
|
|
|
document.removeEventListener( 'wheel', this._onMoveEvent, true ); |
|
|
|
document.removeEventListener( 'mousedown', this._onMoveEvent, true ); |
|
document.removeEventListener( 'touchstart', this._onMoveEvent, true ); |
|
|
|
document.removeEventListener( 'mousemove', this._onMoveEvent, true ); |
|
document.removeEventListener( 'touchmove', this._onMoveEvent, true ); |
|
|
|
document.removeEventListener( 'dragover', this._onMoveEvent, true ); |
|
|
|
document.removeEventListener( 'DOMContentLoaded', this._onContentLoaded ); |
|
|
|
} |
|
|
|
add( node ) { |
|
|
|
this.nodes.push( node ); |
|
|
|
node.canvas = this; |
|
|
|
this.contentDOM.appendChild( node.dom ); |
|
|
|
return this; |
|
|
|
} |
|
|
|
remove( node ) { |
|
|
|
if ( node === this.selected ) { |
|
|
|
this.select(); |
|
|
|
} |
|
|
|
this.unlink( node ); |
|
|
|
const nodes = this.nodes; |
|
|
|
nodes.splice( nodes.indexOf( node ), 1 ); |
|
|
|
node.canvas = null; |
|
|
|
this.contentDOM.removeChild( node.dom ); |
|
|
|
return this; |
|
|
|
} |
|
|
|
clear() { |
|
|
|
const nodes = this.nodes; |
|
|
|
while ( nodes.length > 0 ) { |
|
|
|
this.remove( nodes[ 0 ] ); |
|
|
|
} |
|
|
|
return this; |
|
|
|
} |
|
|
|
unlink( node ) { |
|
|
|
const links = this.getLinks(); |
|
|
|
for ( const link of links ) { |
|
|
|
if ( link.outputElement && link.outputElement.node === node ) { |
|
|
|
link.inputElement.connect(); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
getLinks() { |
|
|
|
const links = []; |
|
|
|
for ( const node of this.nodes ) { |
|
|
|
links.push( ...node.getLinks() ); |
|
|
|
} |
|
|
|
return links; |
|
|
|
} |
|
|
|
centralize() { |
|
|
|
this.dom.scroll( this.centerX, this.centerY ); |
|
|
|
return this; |
|
|
|
} |
|
|
|
select( node = null ) { |
|
|
|
if ( node === this.selected ) return; |
|
|
|
const previousNode = this.selected; |
|
|
|
if ( previousNode !== null ) { |
|
|
|
previousNode.dom.classList.remove( 'selected' ); |
|
|
|
this.selected = null; |
|
|
|
dispatchEventList( previousNode.events.blur, previousNode ); |
|
|
|
} |
|
|
|
if ( node !== null ) { |
|
|
|
node.dom.classList.add( 'selected' ); |
|
|
|
this.selected = node; |
|
|
|
dispatchEventList( node.events.focus, node ); |
|
|
|
} |
|
|
|
} |
|
|
|
update() { |
|
|
|
if ( this.updating === false ) return; |
|
|
|
requestAnimationFrame( this._onUpdate ); |
|
|
|
const { dom, zoom, canvas, frontCanvas, frontContext, context } = this; |
|
|
|
const width = window.innerWidth; |
|
const height = window.innerHeight; |
|
|
|
const domRect = this.rect; |
|
|
|
if ( canvas.width !== width || canvas.height !== height ) { |
|
|
|
canvas.width = width; |
|
canvas.height = height; |
|
|
|
frontCanvas.width = width; |
|
frontCanvas.height = height; |
|
|
|
} |
|
|
|
context.clearRect( 0, 0, width, height ); |
|
frontContext.clearRect( 0, 0, width, height ); |
|
|
|
context.globalCompositeOperation = 'lighter'; |
|
frontContext.globalCompositeOperation = 'source-over'; |
|
|
|
const links = this.getLinks(); |
|
|
|
const aPos = { x: 0, y: 0 }; |
|
const bPos = { x: 0, y: 0 }; |
|
|
|
const offsetIORadius = 10; |
|
|
|
let dragging = ''; |
|
|
|
for ( const link of links ) { |
|
|
|
const { lioElement, rioElement } = link; |
|
|
|
let draggingLink = ''; |
|
let length = 0; |
|
|
|
if ( lioElement !== null ) { |
|
|
|
const rect = lioElement.dom.getBoundingClientRect(); |
|
|
|
length = Math.max( length, lioElement.rioLength ); |
|
|
|
aPos.x = rect.x + rect.width; |
|
aPos.y = rect.y + ( rect.height / 2 ); |
|
|
|
} else { |
|
|
|
aPos.x = this.clientX; |
|
aPos.y = this.clientY; |
|
|
|
draggingLink = 'lio'; |
|
|
|
} |
|
|
|
if ( rioElement !== null ) { |
|
|
|
const rect = rioElement.dom.getBoundingClientRect(); |
|
|
|
length = Math.max( length, rioElement.lioLength ); |
|
|
|
bPos.x = rect.x; |
|
bPos.y = rect.y + ( rect.height / 2 ); |
|
|
|
} else { |
|
|
|
bPos.x = this.clientX; |
|
bPos.y = this.clientY; |
|
|
|
draggingLink = 'rio'; |
|
|
|
} |
|
|
|
dragging = dragging || draggingLink; |
|
|
|
const drawContext = draggingLink ? frontContext : context; |
|
|
|
if ( draggingLink || length === 1 ) { |
|
|
|
if ( draggingLink === 'rio' ) { |
|
|
|
aPos.x += offsetIORadius; |
|
bPos.x /= zoom; |
|
bPos.y /= zoom; |
|
|
|
} else if ( draggingLink === 'lio' ) { |
|
|
|
bPos.x -= offsetIORadius; |
|
aPos.x /= zoom; |
|
aPos.y /= zoom; |
|
|
|
} |
|
|
|
drawLine( |
|
aPos.x * zoom, aPos.y * zoom, |
|
bPos.x * zoom, bPos.y * zoom, |
|
false, 2, '#ffffff', drawContext |
|
); |
|
|
|
} else { |
|
|
|
length = Math.min( length, 4 ); |
|
|
|
for ( let i = 0; i < length; i ++ ) { |
|
|
|
const color = colors[ i ] || '#ffffff'; |
|
|
|
const marginY = 4; |
|
|
|
const rioLength = Math.min( lioElement.rioLength, length ); |
|
const lioLength = Math.min( rioElement.lioLength, length ); |
|
|
|
const aCenterY = ( ( rioLength * marginY ) * .5 ) - ( marginY / 2 ); |
|
const bCenterY = ( ( lioLength * marginY ) * .5 ) - ( marginY / 2 ); |
|
|
|
const aIndex = Math.min( i, rioLength - 1 ); |
|
const bIndex = Math.min( i, lioLength - 1 ); |
|
|
|
const aPosY = aIndex * marginY; |
|
const bPosY = bIndex * marginY; |
|
|
|
drawLine( |
|
aPos.x * zoom, ( ( aPos.y + aPosY ) - aCenterY ) * zoom, |
|
bPos.x * zoom, ( ( bPos.y + bPosY ) - bCenterY ) * zoom, |
|
false, 2, color, drawContext |
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
context.globalCompositeOperation = 'destination-in'; |
|
|
|
context.fillRect( domRect.x, domRect.y, domRect.width, domRect.height ); |
|
|
|
if ( dragging !== '' ) { |
|
|
|
dom.classList.add( 'dragging-' + dragging ); |
|
|
|
} else { |
|
|
|
dom.classList.remove( 'dragging-lio' ); |
|
dom.classList.remove( 'dragging-rio' ); |
|
|
|
} |
|
|
|
} |
|
|
|
serialize( data ) { |
|
|
|
const nodes = []; |
|
|
|
for ( const node of this.nodes ) { |
|
|
|
nodes.push( node.toJSON( data ).id ); |
|
|
|
} |
|
|
|
data.nodes = nodes; |
|
|
|
} |
|
|
|
deserialize( data ) { |
|
|
|
for ( const id of data.nodes ) { |
|
|
|
this.add( data.objects[ id ] ); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
class ButtonInput extends Input { |
|
|
|
constructor( innterText = '' ) { |
|
|
|
const dom = document.createElement( 'button' ); |
|
|
|
const spanDOM = document.createElement( 'span' ); |
|
dom.appendChild( spanDOM ); |
|
|
|
const iconDOM = document.createElement( 'i' ); |
|
dom.appendChild( iconDOM ); |
|
|
|
super( dom ); |
|
|
|
this.spanDOM = spanDOM; |
|
this.iconDOM = iconDOM; |
|
|
|
spanDOM.innerText = innterText; |
|
|
|
dom.onmouseover = () => { |
|
|
|
this.dispatchEvent( new Event( 'mouseover' ) ); |
|
|
|
}; |
|
|
|
dom.onclick = dom.ontouchstart = ( e ) => { |
|
|
|
e.preventDefault(); |
|
|
|
e.stopPropagation(); |
|
|
|
this.dispatchEvent( new Event( 'click' ) ); |
|
|
|
}; |
|
|
|
} |
|
|
|
setIcon( className ) { |
|
|
|
this.iconDOM.className = className; |
|
|
|
return this; |
|
|
|
} |
|
|
|
setValue( val ) { |
|
|
|
this.spanDOM.innerText = val; |
|
|
|
return this; |
|
|
|
} |
|
|
|
getValue() { |
|
|
|
return this.spanDOM.innerText; |
|
|
|
} |
|
|
|
} |
|
|
|
class ObjectNode extends Node { |
|
|
|
constructor( name, inputLength, extra = null, width = 300 ) { |
|
|
|
super(); |
|
|
|
this.setWidth( width ); |
|
|
|
const title = new TitleElement( name ) |
|
.setExtra( extra ) |
|
.setOutput( inputLength ); |
|
|
|
const closeButton = new ButtonInput( Styles.icons.close || '✕' ).onClick( () => { |
|
|
|
this.dispose(); |
|
|
|
} ).setIcon( Styles.icons.close ); |
|
|
|
title.addButton( closeButton ); |
|
|
|
this.add( title ); |
|
|
|
this.title = title; |
|
this.closeButton = closeButton; |
|
|
|
} |
|
|
|
setExtra( value ) { |
|
|
|
this.title.setExtra( value ); |
|
|
|
return this; |
|
|
|
} |
|
|
|
getExtra( value ) { |
|
|
|
return this.title.getExtra(); |
|
|
|
} |
|
|
|
invalidate() { |
|
|
|
this.title.dispatchEvent( new Event( 'connect' ) ); |
|
|
|
} |
|
|
|
} |
|
|
|
const ENTER_KEY$1 = 13; |
|
|
|
class StringInput extends Input { |
|
|
|
constructor( value = '' ) { |
|
|
|
const dom = document.createElement( 'input' ); |
|
super( dom ); |
|
|
|
dom.type = 'text'; |
|
dom.value = value; |
|
dom.spellcheck = false; |
|
dom.autocomplete = 'off'; |
|
|
|
dom.onblur = () => { |
|
|
|
this.dispatchEvent( new Event( 'blur' ) ); |
|
|
|
}; |
|
|
|
dom.onchange = () => { |
|
|
|
this.dispatchEvent( new Event( 'change' ) ); |
|
|
|
}; |
|
|
|
dom.onkeyup = ( e ) => { |
|
|
|
if ( e.keyCode === ENTER_KEY$1 ) { |
|
|
|
e.target.blur(); |
|
|
|
} |
|
|
|
e.stopPropagation(); |
|
|
|
this.dispatchEvent( new Event( 'change' ) ); |
|
|
|
}; |
|
|
|
} |
|
|
|
} |
|
|
|
const ENTER_KEY = 13; |
|
|
|
class NumberInput extends Input { |
|
|
|
constructor( value = 0, min = - Infinity, max = Infinity, step = .01 ) { |
|
|
|
const dom = document.createElement( 'input' ); |
|
super( dom ); |
|
|
|
this.min = min; |
|
this.max = max; |
|
this.step = step; |
|
|
|
this.integer = false; |
|
|
|
dom.type = 'text'; |
|
dom.className = 'number'; |
|
dom.value = this._getString( value ); |
|
dom.spellcheck = false; |
|
dom.autocomplete = 'off'; |
|
|
|
dom.ondragstart = dom.oncontextmenu = ( e ) => { |
|
|
|
e.preventDefault(); |
|
|
|
e.stopPropagation(); |
|
|
|
}; |
|
|
|
dom.onfocus = dom.onclick = () => { |
|
|
|
dom.select(); |
|
|
|
}; |
|
|
|
dom.onblur = () => { |
|
|
|
this.dispatchEvent( new Event( 'blur' ) ); |
|
|
|
}; |
|
|
|
dom.onchange = () => { |
|
|
|
this.dispatchEvent( new Event( 'change' ) ); |
|
|
|
}; |
|
|
|
dom.onkeydown = ( e ) => { |
|
|
|
if ( e.key.length === 1 && /\d|\./.test( e.key ) !== true ) { |
|
|
|
return false; |
|
|
|
} |
|
|
|
if ( e.keyCode === ENTER_KEY ) { |
|
|
|
e.target.blur(); |
|
|
|
} |
|
|
|
e.stopPropagation(); |
|
|
|
}; |
|
|
|
draggableDOM( dom, ( data ) => { |
|
|
|
const { delta } = data; |
|
|
|
if ( data.value === undefined ) { |
|
|
|
data.value = this.getValue(); |
|
|
|
} |
|
|
|
const diff = delta.x - delta.y; |
|
|
|
const value = data.value + ( diff * this.step ); |
|
|
|
this.dom.value = this._getString( value.toFixed( this.precision ) ); |
|
|
|
this.dispatchEvent( new Event( 'change' ) ); |
|
|
|
} ); |
|
|
|
} |
|
|
|
setRange( min, max, step ) { |
|
|
|
this.min = min; |
|
this.max = max; |
|
this.step = step; |
|
|
|
this.dispatchEvent( new Event( 'range' ) ); |
|
|
|
return this.setValue( this.getValue() ); |
|
|
|
} |
|
|
|
get precision() { |
|
|
|
if ( this.integer === true ) return 0; |
|
|
|
const fract = this.step % 1; |
|
|
|
return fract !== 0 ? fract.toString().split( '.' )[ 1 ].length : 1; |
|
|
|
} |
|
|
|
setValue( val, dispatch = true ) { |
|
|
|
return super.setValue( this._getString( val ), dispatch ); |
|
|
|
} |
|
|
|
getValue() { |
|
|
|
return Number( this.dom.value ); |
|
|
|
} |
|
|
|
serialize( data ) { |
|
|
|
const { min, max } = this; |
|
|
|
if ( min !== - Infinity && max !== Infinity ) { |
|
|
|
data.min = this.min; |
|
data.max = this.max; |
|
data.step = this.step; |
|
|
|
} |
|
|
|
super.serialize( data ); |
|
|
|
} |
|
|
|
deserialize( data ) { |
|
|
|
if ( data.min !== undefined ) { |
|
|
|
const { min, max, step } = this; |
|
|
|
this.setRange( min, max, step ); |
|
|
|
} |
|
|
|
super.deserialize( data ); |
|
|
|
} |
|
|
|
_getString( value ) { |
|
|
|
let num = Math.min( Math.max( Number( value ), this.min ), this.max ); |
|
|
|
if ( this.integer === true ) { |
|
|
|
return Math.floor( num ); |
|
|
|
} else { |
|
|
|
return num + ( num % 1 ? '' : '.0' ); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
const getStep = ( min, max ) => { |
|
|
|
const sensibility = .001; |
|
|
|
return ( max - min ) * sensibility; |
|
|
|
}; |
|
|
|
class SliderInput extends Input { |
|
|
|
constructor( value = 0, min = 0, max = 100 ) { |
|
|
|
const dom = document.createElement( 'f-subinputs' ); |
|
super( dom ); |
|
|
|
value = Math.min( Math.max( value, min ), max ); |
|
|
|
const step = getStep( min, max ); |
|
|
|
const rangeDOM = document.createElement( 'input' ); |
|
rangeDOM.type = 'range'; |
|
rangeDOM.min = min; |
|
rangeDOM.max = max; |
|
rangeDOM.step = step; |
|
rangeDOM.value = value; |
|
|
|
const field = new NumberInput( value, min, max, step ); |
|
field.dom.className = 'range-value'; |
|
field.onChange( () => { |
|
|
|
rangeDOM.value = field.getValue(); |
|
|
|
} ); |
|
|
|
field.addEventListener( 'range', () => { |
|
|
|
rangeDOM.min = field.min; |
|
rangeDOM.max = field.max; |
|
rangeDOM.step = field.step; |
|
rangeDOM.value = field.getValue(); |
|
|
|
} ); |
|
|
|
dom.appendChild( rangeDOM ); |
|
dom.appendChild( field.dom ); |
|
|
|
this.rangeDOM = rangeDOM; |
|
this.field = field; |
|
|
|
const updateRangeValue = () => { |
|
|
|
let value = Number( rangeDOM.value ); |
|
|
|
if ( value !== this.max && value + this.step >= this.max ) { |
|
|
|
// fix not end range fraction |
|
|
|
rangeDOM.value = value = this.max; |
|
|
|
} |
|
|
|
this.field.setValue( value ); |
|
|
|
}; |
|
|
|
draggableDOM( rangeDOM, () => { |
|
|
|
updateRangeValue(); |
|
|
|
this.dispatchEvent( new Event( 'change' ) ); |
|
|
|
}, '' ); |
|
|
|
} |
|
|
|
get min() { |
|
|
|
return this.field.min; |
|
|
|
} |
|
|
|
get max() { |
|
|
|
return this.field.max; |
|
|
|
} |
|
|
|
get step() { |
|
|
|
return this.field.step; |
|
|
|
} |
|
|
|
setRange( min, max ) { |
|
|
|
this.field.setRange( min, max, getStep( min, max ) ); |
|
|
|
this.dispatchEvent( new Event( 'range' ) ); |
|
this.dispatchEvent( new Event( 'change' ) ); |
|
|
|
return this; |
|
|
|
} |
|
|
|
setValue( val, dispatch = true ) { |
|
|
|
this.field.setValue( val ); |
|
this.rangeDOM.value = val; |
|
|
|
if ( dispatch ) this.dispatchEvent( new Event( 'change' ) ); |
|
|
|
return this; |
|
|
|
} |
|
|
|
getValue() { |
|
|
|
return this.field.getValue(); |
|
|
|
} |
|
|
|
serialize( data ) { |
|
|
|
data.min = this.min; |
|
data.max = this.max; |
|
|
|
super.serialize( data ); |
|
|
|
} |
|
|
|
deserialize( data ) { |
|
|
|
const { min, max } = data; |
|
|
|
this.setRange( min, max ); |
|
|
|
super.deserialize( data ); |
|
|
|
} |
|
|
|
} |
|
|
|
class ColorInput extends Input { |
|
|
|
constructor( value = 0x0099ff ) { |
|
|
|
const dom = document.createElement( 'input' ); |
|
super( dom ); |
|
|
|
dom.type = 'color'; |
|
dom.value = toHex( value ); |
|
|
|
dom.oninput = () => { |
|
|
|
this.dispatchEvent( new Event( 'change' ) ); |
|
|
|
}; |
|
|
|
} |
|
|
|
setValue( value, dispatch = true ) { |
|
|
|
return super.setValue( toHex( value ), dispatch ); |
|
|
|
} |
|
|
|
getValue() { |
|
|
|
return parseInt( super.getValue().substr( 1 ), 16 ); |
|
|
|
} |
|
|
|
} |
|
|
|
class TextInput extends Input { |
|
|
|
constructor( innerText = '' ) { |
|
|
|
const dom = document.createElement( 'textarea' ); |
|
super( dom ); |
|
|
|
dom.innerText = innerText; |
|
|
|
} |
|
|
|
setValue( val ) { |
|
|
|
this.dom.innerText = val; |
|
|
|
return this; |
|
|
|
} |
|
|
|
getValue() { |
|
|
|
return this.dom.innerText; |
|
|
|
} |
|
|
|
} |
|
|
|
class LabelElement extends Element { |
|
|
|
constructor( label = '', align = '' ) { |
|
|
|
super(); |
|
|
|
this.labelDOM = document.createElement( 'f-label' ); |
|
this.inputsDOM = document.createElement( 'f-inputs' ); |
|
|
|
const spanDOM = document.createElement( 'span' ); |
|
const iconDOM = document.createElement( 'i' ); |
|
|
|
this.spanDOM = spanDOM; |
|
this.iconDOM = iconDOM; |
|
|
|
this.labelDOM.appendChild( this.spanDOM ); |
|
this.labelDOM.appendChild( this.iconDOM ); |
|
|
|
this.dom.appendChild( this.labelDOM ); |
|
this.dom.appendChild( this.inputsDOM ); |
|
|
|
this.setLabel( label ); |
|
this.setAlign( align ); |
|
|
|
} |
|
|
|
setIcon( value ) { |
|
|
|
this.iconDOM.className = value; |
|
|
|
return this; |
|
|
|
} |
|
|
|
getIcon() { |
|
|
|
return this.iconDOM.className; |
|
|
|
} |
|
|
|
setAlign( align ) { |
|
|
|
this.labelDOM.className = align; |
|
|
|
} |
|
|
|
setLabel( val ) { |
|
|
|
this.spanDOM.innerText = val; |
|
|
|
} |
|
|
|
getLabel() { |
|
|
|
return this.spanDOM.innerText; |
|
|
|
} |
|
|
|
serialize( data ) { |
|
|
|
super.serialize( data ); |
|
|
|
const label = this.getLabel(); |
|
const icon = this.getIcon(); |
|
|
|
data.label = label; |
|
|
|
if ( icon !== '' ) { |
|
|
|
data.icon = icon; |
|
|
|
} |
|
|
|
} |
|
|
|
deserialize( data ) { |
|
|
|
super.deserialize( data ); |
|
|
|
this.setLabel( data.label ); |
|
|
|
if ( data.icon !== undefined ) { |
|
|
|
this.setIcon( data.icon ); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
class PanelNode extends Node { |
|
|
|
constructor( title = 'Panel', align = 'top-right' ) { |
|
|
|
super(); |
|
|
|
const titleElement = new TitleElement( title ); |
|
this.add( titleElement ); |
|
|
|
const collapseButton = new ButtonInput( '🗕' ); |
|
collapseButton.onClick( () => { |
|
|
|
this.setCollapse( ! this.collapsed ); |
|
|
|
} ); |
|
|
|
titleElement.addButton( collapseButton ); |
|
|
|
this.collapseButton = collapseButton; |
|
this.titleElement = titleElement; |
|
this.align = align; |
|
this.collapsed = false; |
|
|
|
this.setAlign( align ); |
|
this.setStyle( 'rouded' ); |
|
|
|
} |
|
|
|
setCollapse( value ) { |
|
|
|
const cssClass = 'closed'; |
|
|
|
this.dom.classList.remove( cssClass ); |
|
|
|
this.collapsed = value; |
|
|
|
this.collapseButton.value = value ? '🗖' : '🗕'; |
|
|
|
if ( value === true ) { |
|
|
|
this.dom.classList.add( cssClass ); |
|
|
|
} |
|
|
|
return this; |
|
|
|
} |
|
|
|
setAlign( align ) { |
|
|
|
if ( this.align ) this.dom.classList.remove( this.align ); |
|
this.dom.classList.add( align ); |
|
|
|
this.align = align; |
|
|
|
return this; |
|
|
|
} |
|
|
|
addInput( inputClass, object, property, ...params ) { |
|
|
|
const value = object[ property ]; |
|
|
|
const input = new inputClass( value, ...params ); |
|
input.onChange( () => { |
|
|
|
object[ property ] = input.value; |
|
|
|
} ); |
|
|
|
this.add( new LabelElement( property ).add( input ) ); |
|
|
|
return input; |
|
|
|
} |
|
|
|
addSlider( object, property, min, max ) { |
|
|
|
return this.addInput( SliderInput, object, property, min, max ); |
|
|
|
} |
|
|
|
addNumber( object, property ) { |
|
|
|
return this.addInput( NumberInput, object, property ); |
|
|
|
} |
|
|
|
addColor( object, property ) { |
|
|
|
return this.addInput( ColorInput, object, property ); |
|
|
|
} |
|
|
|
addString( object, property ) { |
|
|
|
return this.addInput( StringInput, object, property ); |
|
|
|
} |
|
|
|
addText( object, property ) { |
|
|
|
const input = this.addInput( TextInput, object, property ); |
|
input.element.setHeight( 70 ); |
|
|
|
return input; |
|
|
|
} |
|
|
|
addButton( name ) { |
|
|
|
const input = new ButtonInput( name ); |
|
|
|
this.add( new Element().setHeight( 34 ).add( input ) ); |
|
|
|
return input; |
|
|
|
} |
|
|
|
} |
|
|
|
class Menu extends EventTarget { |
|
|
|
constructor( className, target = null ) { |
|
|
|
super(); |
|
|
|
const dom = document.createElement( 'f-menu' ); |
|
dom.className = className + ' hidden'; |
|
|
|
this.dom = dom; |
|
|
|
this.visible = false; |
|
|
|
this.subMenus = new WeakMap(); |
|
this.domButtons = new WeakMap(); |
|
|
|
this.events = { |
|
'context': [] |
|
}; |
|
|
|
this.addEventListener( 'context', ( ) => { |
|
|
|
dispatchEventList( this.events.context, this ); |
|
|
|
} ); |
|
|
|
this._lastButtonClick = null; |
|
|
|
this._onButtonClick = ( e = null ) => { |
|
|
|
const button = e ? e.target : null; |
|
|
|
if ( this._lastButtonClick ) { |
|
|
|
this._lastButtonClick.dom.parentElement.classList.remove( 'active' ); |
|
|
|
} |
|
|
|
this._lastButtonClick = button; |
|
|
|
if ( button ) { |
|
|
|
if ( this.subMenus.has( button ) ) { |
|
|
|
this.subMenus.get( button )._onButtonClick(); |
|
|
|
} |
|
|
|
button.dom.parentElement.classList.add( 'active' ); |
|
|
|
} |
|
|
|
}; |
|
|
|
this._onButtonMouseOver = ( e ) => { |
|
|
|
const button = e.target; |
|
|
|
if ( this.subMenus.has( button ) && this._lastButtonClick !== button ) { |
|
|
|
this._onButtonClick(); |
|
|
|
} |
|
|
|
}; |
|
|
|
this.setTarget( target ); |
|
|
|
} |
|
|
|
onContext( callback ) { |
|
|
|
this.events.context.push( callback ); |
|
|
|
return this; |
|
|
|
} |
|
|
|
show( x = null, y = null ) { |
|
|
|
this._onButtonClick(); |
|
|
|
if ( x !== null && y !== null ) { |
|
|
|
this.setPosition( x, y ); |
|
|
|
} |
|
|
|
this.dom.classList.remove( 'hidden' ); |
|
|
|
this.visible = true; |
|
|
|
this.dispatchEvent( new Event( 'show' ) ); |
|
|
|
return this; |
|
|
|
} |
|
|
|
hide() { |
|
|
|
this.dom.classList.add( 'hidden' ); |
|
|
|
this.dispatchEvent( new Event( 'hide' ) ); |
|
|
|
this.visible = false; |
|
|
|
} |
|
|
|
setTarget( target = null ) { |
|
|
|
if ( target !== null ) { |
|
|
|
const onContextMenu = ( e ) => { |
|
|
|
e.preventDefault(); |
|
|
|
if ( e.pointerType !== 'mouse' || ( e.pageX === 0 && e.pageY === 0 ) ) return; |
|
|
|
const rect = this.target.getBoundingClientRect(); |
|
|
|
this.dispatchEvent( new Event( 'context' ) ); |
|
|
|
this.show( e.pageX - rect.left, e.pageY - rect.top ); |
|
|
|
}; |
|
|
|
const onDown = ( e ) => { |
|
|
|
if ( this.visible === true && e.target.closest( 'f-menu' ) === null ) { |
|
|
|
this.hide(); |
|
|
|
} |
|
|
|
}; |
|
|
|
this.target = target; |
|
|
|
target.addEventListener( 'mousedown', onDown, true ); |
|
target.addEventListener( 'touchstart', onDown, true ); |
|
|
|
target.addEventListener( 'contextmenu', onContextMenu, false ); |
|
|
|
target.appendChild( this.dom ); |
|
|
|
} |
|
|
|
return this; |
|
|
|
} |
|
|
|
add( button, submenu = null ) { |
|
|
|
const liDOM = document.createElement( 'f-item' ); |
|
|
|
if ( submenu !== null ) { |
|
|
|
liDOM.classList.add( 'submenu' ); |
|
|
|
liDOM.appendChild( submenu.dom ); |
|
|
|
this.subMenus.set( button, submenu ); |
|
|
|
} |
|
|
|
liDOM.appendChild( button.dom ); |
|
|
|
button.addEventListener( 'click', this._onButtonClick ); |
|
button.addEventListener( 'mouseover', this._onButtonMouseOver ); |
|
|
|
this.dom.appendChild( liDOM ); |
|
|
|
this.domButtons.set( liDOM, button ); |
|
|
|
return this; |
|
|
|
} |
|
|
|
setPosition( x, y ) { |
|
|
|
const dom = this.dom; |
|
|
|
dom.style.left = toPX( x ); |
|
dom.style.top = toPX( y ); |
|
|
|
return this; |
|
|
|
} |
|
|
|
} |
|
|
|
let lastContext = null; |
|
|
|
class ContextMenu extends Menu { |
|
|
|
constructor( target = null ) { |
|
|
|
super( 'context', target ); |
|
|
|
} |
|
|
|
show( x, y ) { |
|
|
|
if ( lastContext !== null ) { |
|
|
|
lastContext.hide(); |
|
|
|
} |
|
|
|
lastContext = this; |
|
|
|
return super.show( x, y ); |
|
|
|
} |
|
|
|
hide() { |
|
|
|
if ( lastContext === this ) { |
|
|
|
lastContext = null; |
|
|
|
} |
|
|
|
return super.hide(); |
|
|
|
} |
|
|
|
} |
|
|
|
class CircleMenu extends Menu { |
|
|
|
constructor( target = null ) { |
|
|
|
super( 'circle', target ); |
|
|
|
} |
|
|
|
} |
|
|
|
class SelectInput extends Input { |
|
|
|
constructor( options = [] ) { |
|
|
|
const dom = document.createElement( 'select' ); |
|
super( dom ); |
|
|
|
dom.onchange = () => { |
|
|
|
this.dispatchEvent( new Event( 'change' ) ); |
|
|
|
}; |
|
|
|
this.setOptions( options ); |
|
|
|
} |
|
|
|
setOptions( options ) { |
|
|
|
const dom = this.dom; |
|
|
|
this.options = options; |
|
|
|
dom.innerHTML = ''; |
|
|
|
for ( let index = 0; index < options.length; index ++ ) { |
|
|
|
let opt = options[ index ]; |
|
|
|
if ( typeof opt === 'string' ) { |
|
|
|
opt = { name: opt, value: index }; |
|
|
|
} |
|
|
|
const option = document.createElement( 'option' ); |
|
option.innerText = opt.name; |
|
option.value = opt.value; |
|
|
|
dom.appendChild( option ); |
|
|
|
} |
|
|
|
return this; |
|
|
|
} |
|
|
|
getOptions() { |
|
|
|
return this._options; |
|
|
|
} |
|
|
|
serialize( data ) { |
|
|
|
data.options = [ ...this.options ]; |
|
|
|
super.serialize( data ); |
|
|
|
} |
|
|
|
deserialize( data ) { |
|
|
|
const currentOptions = this.options; |
|
|
|
if ( currentOptions.length > 0 ) { |
|
|
|
this.setOptions( data.options ); |
|
|
|
} |
|
|
|
super.deserialize( data ); |
|
|
|
} |
|
|
|
} |
|
|
|
class ToggleInput extends Input { |
|
|
|
constructor( value = false ) { |
|
|
|
const dom = document.createElement( 'input' ); |
|
super( dom ); |
|
|
|
dom.type = 'checkbox'; |
|
dom.className = 'toggle'; |
|
dom.checked = value; |
|
|
|
} |
|
|
|
setValue( val ) { |
|
|
|
this.dom.checked = val; |
|
|
|
this.dispatchEvent( new Event( 'change' ) ); |
|
|
|
return this; |
|
|
|
} |
|
|
|
getValue() { |
|
|
|
return this.dom.checked; |
|
|
|
} |
|
|
|
} |
|
|
|
var Flow = /*#__PURE__*/Object.freeze({ |
|
__proto__: null, |
|
Element: Element, |
|
Input: Input, |
|
Node: Node, |
|
Canvas: Canvas, |
|
Serializer: Serializer, |
|
Styles: Styles, |
|
ObjectNode: ObjectNode, |
|
PanelNode: PanelNode, |
|
Menu: Menu, |
|
ContextMenu: ContextMenu, |
|
CircleMenu: CircleMenu, |
|
DraggableElement: DraggableElement, |
|
LabelElement: LabelElement, |
|
TitleElement: TitleElement, |
|
ButtonInput: ButtonInput, |
|
ColorInput: ColorInput, |
|
NumberInput: NumberInput, |
|
SelectInput: SelectInput, |
|
SliderInput: SliderInput, |
|
StringInput: StringInput, |
|
TextInput: TextInput, |
|
ToggleInput: ToggleInput |
|
}); |
|
|
|
class Loader extends EventTarget { |
|
|
|
constructor( parseType = Loader.DEFAULT ) { |
|
|
|
super(); |
|
|
|
this.parseType = parseType; |
|
|
|
this.events = { |
|
'load': [] |
|
}; |
|
|
|
} |
|
|
|
setParseType( type ) { |
|
|
|
this.parseType = type; |
|
|
|
return this; |
|
|
|
} |
|
|
|
getParseType() { |
|
|
|
return this.parseType; |
|
|
|
} |
|
|
|
onLoad( callback ) { |
|
|
|
this.events.load.push( callback ); |
|
|
|
return this; |
|
|
|
} |
|
|
|
async load( url, lib = null ) { |
|
|
|
return await fetch( url ) |
|
.then( response => response.json() ) |
|
.then( result => { |
|
|
|
this.data = this.parse( result, lib ); |
|
|
|
dispatchEventList( this.events.load, this ); |
|
|
|
return this.data; |
|
|
|
} ) |
|
.catch( err => { |
|
|
|
console.error( 'Loader:', err ); |
|
|
|
} ); |
|
|
|
} |
|
|
|
parse( json, lib = null ) { |
|
|
|
json = this._parseObjects( json, lib ); |
|
|
|
const parseType = this.parseType; |
|
|
|
if ( parseType === Loader.DEFAULT ) { |
|
|
|
const flowObj = new Flow[ json.type ](); |
|
|
|
if ( flowObj.getSerializable() ) { |
|
|
|
flowObj.deserialize( json ); |
|
|
|
} |
|
|
|
return flowObj; |
|
|
|
} else if ( parseType === Loader.OBJECTS ) { |
|
|
|
return json; |
|
|
|
} |
|
|
|
} |
|
|
|
_parseObjects( json, lib = null ) { |
|
|
|
json = { ...json }; |
|
|
|
const objects = {}; |
|
|
|
for ( const id in json.objects ) { |
|
|
|
const obj = json.objects[ id ]; |
|
obj.objects = objects; |
|
|
|
const Class = lib && lib[ obj.type ] ? lib[ obj.type ] : Flow[ obj.type ]; |
|
|
|
objects[ id ] = new Class(); |
|
|
|
} |
|
|
|
const ref = new WeakMap(); |
|
|
|
const deserializePass = ( prop = null ) => { |
|
|
|
for ( const id in json.objects ) { |
|
|
|
const newObject = objects[ id ]; |
|
|
|
if ( ref.has( newObject ) === false && ( prop === null || newObject[ prop ] === true ) ) { |
|
|
|
ref.set( newObject, true ); |
|
|
|
if ( newObject.getSerializable() ) { |
|
|
|
newObject.deserialize( json.objects[ id ] ); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
}; |
|
|
|
deserializePass( 'isNode' ); |
|
deserializePass( 'isElement' ); |
|
deserializePass( 'isInput' ); |
|
deserializePass(); |
|
|
|
json.objects = objects; |
|
|
|
return json; |
|
|
|
} |
|
|
|
} |
|
|
|
Loader.DEFAULT = 'default'; |
|
Loader.OBJECTS = 'objects'; |
|
|
|
export { ButtonInput, Canvas, CircleMenu, ColorInput, ContextMenu, DraggableElement, Element, Input, LabelElement, Loader, Menu, Node, NumberInput, ObjectNode, PanelNode, REVISION, SelectInput, Serializer, SliderInput, StringInput, Styles, TextInput, TitleElement, ToggleInput, Utils };
|
|
|