diff --git a/Box.js b/Box.js
new file mode 100644
index 0000000..80a60ae
--- /dev/null
+++ b/Box.js
@@ -0,0 +1,93 @@
+import { SVG } from './DOM.js'
+import { makeDraggable } from './Draggable.js'
+import { Group } from './Group.js'
+
+const Box = ( title, inputs, outputs )=> {
+ let $dom = makeDraggable( Group('Box') ), $fo, $box
+
+ $dom.append(
+ $fo = SVG`
+
+
+ ${outputs.map( str=> `${str}`).join('')}
+ ${inputs.map( str=> `${str}`).join('')}
+
+ `[0]
+ )
+ $box = $fo.$`box`
+ $dom.resize = ({ width, height })=> {
+ width && $fo.setAttribute('width', width )
+ height && $fo.setAttribute('height', height )
+ $dom.updateAnchors()
+ }
+
+ let heightInvalidated = false
+ $dom.invalidateHeight = ()=> {
+ heightInvalidated = true
+ requestAnimationFrame( ()=> $dom.resize({height: $box.offsetHeight }) )
+ }
+ let widthInvalidated = false
+ $dom.invalidateWidth = ()=> {
+ widthInvalidated = true
+ requestAnimationFrame( ()=> $dom.resize({width: $box.offsetWidth }) )
+ }
+
+ setTimeout( $dom.invalidateHeight, 1000 )
+
+
+ // Draw anchors
+ $box.$$`socket`.map( sock=> sock.anchor = SVG``[0] )
+ .map( a=> $dom.append(a) )
+
+ $dom.updateAnchors = ()=>
+ $box.$$`socket`
+ .map( sock=>
+ sock.anchor.moveTo({
+ x: sock.hasAttribute('output')
+ ? $box.offsetWidth
+ : 0
+ , y: sock.offsetTop + ( sock.offsetHeight / 2 ) + 1
+ })
+ )
+
+ $dom.on`contextmenu`( e=> e.menu = Object.assign(e.menu || {}, {
+ 'Box': {
+ 'Debug': e=> console.log($dom)
+ , 'Convert to >': {
+ ZigZag: e=> $box.style.setProperty('background','orange')
+ , Flat: e=> $dom.style.setProperty('--wave-color','var(--fl-color)')
+ , Impulsive: e=> $dom.style.setProperty('--wave-color','var(--im-color)')
+ }
+ // 'Convert to ZZ':e=> $dom.style.setProperty('--wave-color','var(--zz-color)')
+// , ...( $dom.fibos ? {['Toggle fibos']: $dom.toggleFibos} : {} )
+// , ...( w.isEmpty ? {['Subdivise']: $dom.subdivise} : {} )
+ }
+ }) , {capture:true} )
+
+ return $dom
+}
+
+
+
+// class Box extends HTMLElement {
+
+// static styles = CSS`
+
+// `
+
+// constructor()
+// {
+// super()
+// this.$svg =
+// this.addListeners()
+
+// }
+
+// addListeners()
+// {
+
+// }
+// }
+// customElements.define( 'vp-box', Box )
+
+export { Box }
\ No newline at end of file
diff --git a/DOM.js b/DOM.js
new file mode 100644
index 0000000..ff9dbee
--- /dev/null
+++ b/DOM.js
@@ -0,0 +1,265 @@
+import { EventojLancilo } from './EventojLancilo.js'
+EventojLancilo( EventTarget ) // same event handler for everybody
+
+String.merge = (ss,...pp)=> [].concat(ss).map( (s,i)=> s+(i in pp?pp[i]:'') ).join('')
+const logNpass = o=> console.log(o)||o
+// const ss_pp = (ss,...pp)=> [].concat(ss).map( (s,i)=> s+(i in pp?pp[i]:'') ).join('')
+// const $ = (ss,...pp)=> document.querySelector( ss_pp(ss,...pp) )
+// const $$ = (ss,...pp)=> [...document.querySelectorAll( ss_pp(ss,...pp) )]
+const DOM = (ss,...pp)=> {
+ let t = document.createElementNS( 'http://www.w3.org/1999/xhtml', 'template' )
+ t.innerHTML = String.merge(ss,...pp)
+ return t.content
+}
+const SVG = (ss,...pp)=> DOM``
+ .children[0].childNodes
+
+const CSS = (ss,...pp)=> {
+ let styles = new CSSStyleSheet()
+ styles.replaceSync( String.merge(ss,...pp) )
+ return styles
+}
+
+const $ = (ss,...pp)=> document.$( ss,...pp )
+const $$ = (ss,...pp)=> document.$$( ss,...pp )
+Node.prototype.$ = function(ss,...pp)
+{
+ return this.querySelector( String.merge(ss,...pp) )
+}
+Node.prototype.$$ = function(ss,...pp)
+{
+ return [...this.querySelectorAll( String.merge(ss,...pp) )]
+}
+Node.prototype.add = function( ...args )
+{
+ this.append( ...args )
+ return this
+}
+// Node.prototype.on = function(ss,...pp)
+// {
+// return (...args)=> ( this.addEventListener( ss_pp(ss,...pp), ...args ), this )
+// }
+
+
+
+Object.defineProperties( SVGRect.prototype, {
+ center: {
+ get(){ return {
+ x: this.x + ( this.width/2 )
+ , y: this.y + ( this.height/2 )
+ }}
+ , set( v ){}
+ }
+})
+
+/** **/
+
+// @see https://cp-algorithms.com/geometry/segment-to-line.html
+// SVGLineElement.prototype.getCoef = function line()
+// {
+// let P = { x: this.x1.baseVal.value, y: this.y1.baseVal.value }
+// let Q = { x: this.x2.baseVal.value, y: this.y2.baseVal.value }
+// //console.log(P,Q)
+// let A = P.y - Q.y
+// let B = Q.x - P.x
+// let C = ( -A * P.x ) - ( B * P.y )
+// return [A,B,C]
+// }
+// // @see https://cp-algorithms.com/geometry/lines-intersection.html
+// SVGLineElement.prototype.intersect = function intersect( line )
+// {
+// let [ a1,b1,c1 ] = this.getCoef()
+// // console.log(this.getCoef())
+// let [ a2,b2,c2 ] = line.getCoef()
+// // console.log(line.getCoef())
+// let x = - ((c1*b2)-(c2*b1))/((a1*b2)-(a2*b1))
+// let y = - ((a1*c2)-(a2*c1))/((a1*b2)-(a2*b1))
+// return { x, y }
+// }
+//PointProperty('_p1','x1','y1')
+const PointProperty = ( _, x, y )=> ({
+ get(){ return this[_] = this[_] || new DOMPoint(
+ this[x].baseVal.value,
+ this[y].baseVal.value
+ )}
+, set( obj ){
+ if( 'x' in obj && 'y' in obj )
+ {
+ const ev = e=> {
+ this.setAttribute( x, obj.x )
+ this.setAttribute( y, obj.y )
+ this.dispatchEvent( new Event('move') )
+ }
+ this[_]
+ && this[_].removeEventListener
+ && this[_].removeEventListener('move', ev)
+ this[_] = obj
+ obj.addEventListener && obj.addEventListener('move', ev)
+ ev()
+ this.dispatchEvent( new Event('pointchange') )
+ }
+ }
+})
+Object.defineProperties(SVGLineElement.prototype, {
+ p1: PointProperty('_p1','x1','y1')
+// {
+// get(){ return this._p1 = this._p1 || new DOMPoint(
+// this.x1.baseVal.value,
+// this.y1.baseVal.value
+// )
+// }
+// , set( obj ){
+// if( 'x' in obj && 'y' in obj )
+// {
+// const ev = e=> {
+// this.setAttribute('x1', obj.x)
+// this.setAttribute('y1', obj.y)
+// this.dispatchEvent( new Event('move') )
+// }
+// this._p1
+// && this._p1.removeEventListener
+// && this._p1.removeEventListener('move', ev)
+// this._p1 = obj
+// obj.addEventListener && obj.addEventListener('move', ev)
+// ev()
+// this.dispatchEvent( new Event('pointchange') )
+// }
+// }
+// }
+, p2: PointProperty('_p2','x2','y2')
+// {
+// get(){ return this._p2 = this._p2 || new DOMPoint(
+// this.x2.baseVal.value,
+// this.y2.baseVal.value
+// )}
+// , set( obj ){
+// if( 'x' in obj && 'y' in obj )
+// {
+// const ev = e=> {
+// this.setAttribute('x2', obj.x)
+// this.setAttribute('y2', obj.y)
+// this.dispatchEvent( new Event('move') )
+// }
+// this._p2
+// && this._p2.removeEventListener
+// && this._p2.removeEventListener('move', ev)
+// this._p2 = obj
+// obj.addEventListener && obj.addEventListener('move', ev)
+// ev()
+// this.dispatchEvent( new Event('pointchange') )
+// }
+// }
+// }
+})
+
+//@TODO Debounce move event fired twice when changing x and y
+//@TODO Adapt which attribute is set ( x, cx, x1, transform ), depending on localName ( , , , ... )
+Object.defineProperties(SVGElement.prototype, {
+ x: {
+ get(){ return parseFloat((this.attributes.x||this.attributes.cx||this.attributes.x1||{value:0}).value) }
+ , set( v ){
+ if( !isFinite(v) ) return v
+ if( this.localName == 'g' )
+ this.setAttribute('transform',`translate(${v},${this.y})`)
+ this.setAttribute('x',v)
+ this.setAttribute('cx',v)
+ this.setAttribute('x1',v)
+ }
+ }
+, y: {
+ get(){ return parseFloat((this.attributes.y||this.attributes.cy||this.attributes.y1||{value:0}).value) }
+ , set( v ){
+ if( !isFinite(v) ) return v
+ if( this.localName == 'g' )
+ this.setAttribute('transform',`translate(${this.x},${v})`)
+ this.setAttribute('y',v)
+ this.setAttribute('cy',v)
+ this.setAttribute('y1',v)
+ }
+ }
+, moveTo: {
+ value: function({ x, y }){
+ this.x = x
+ this.y = y
+ this.dispatchEvent( new Event('move') )
+ return this
+ }
+ }
+, moveBy: {
+ value: function({ x, y }){
+ this.x += x
+ this.y += y
+ this.dispatchEvent( new Event('move') )
+ return this
+ }
+ }
+
+})
+
+
+Object.defineProperties(SVGSVGElement.prototype, {
+ zoom: {
+ get(){ return parseFloat((this.attributes.x||this.attributes.cx||this.attributes.x1||{value:0}).value) }
+ , set( v ){
+ if( !isFinite(v) ) return v
+ if( this.localName == 'g' )
+ this.setAttribute('transform',`translate(${v},${this.y})`)
+ this.setAttribute('x',v)
+ this.setAttribute('cx',v)
+ this.setAttribute('x1',v)
+ }
+ }
+, pan: //PointProperty('_pan','pan-x','pan-y')
+ {
+ get(){ return this._pan = this._pan || new DOMPoint(
+ this.x2.baseVal.value,
+ this.y2.baseVal.value
+ )}
+ , set( obj ){
+ if( 'x' in obj && 'y' in obj )
+ {
+ const ev = e=> {
+ this.setAttribute('x2', obj.x)
+ this.setAttribute('y2', obj.y)
+ this.dispatchEvent( new Event('move') )
+ }
+ this._p2
+ && this._p2.removeEventListener
+ && this._p2.removeEventListener('move', ev)
+ this._p2 = obj
+ obj.addEventListener && obj.addEventListener('move', ev)
+ ev()
+ this.dispatchEvent( new Event('pointchange') )
+ }
+ }
+ }
+, panAndZoom: {
+ value( x, y, z )
+ {
+ let { x:vx, y:vy, width:vw, height:vh } = this.viewBox.baseVal
+ , vz
+ vz = Math.min( vw, vh ) / 2
+ vx = vx + ( vw / 2 )
+ vy = vy + ( vh / 2 )
+
+ requestAnimationFrame( ()=> {
+ // this.setAttribute('viewBox', `${typeof x == 'number' ? x-(z||vz) : vx} ${typeof y == 'number' ? y-(z||vz) : vy} ${z ? z*2 : vw} ${z ? z*2 : vh}`)
+ this.setAttribute('viewBox', `${(x||vx)-(z||vz)} ${(y||vy)-(z||vz)} ${(z||vz)*2} ${(z||vz)*2}`)
+ console.log('viewBox', `${(x||vx)-(z||vz)} ${(y||vy)-(z||vz)} ${(z||vz)*2} ${(z||vz)*2}`)
+
+ // this.setAttribute('pan', `${x} ${y}`)
+ // this.setAttribute('zoom', `${z}`)
+ })
+ }
+ }
+})
+
+
+export {
+ logNpass
+, DOM
+, SVG
+, CSS
+, $
+, $$
+}
\ No newline at end of file
diff --git a/Draggable.js b/Draggable.js
new file mode 100644
index 0000000..4bf8413
--- /dev/null
+++ b/Draggable.js
@@ -0,0 +1,135 @@
+// const UI = {}
+
+
+// SVG.Point = ( x, y )=> Object.assign( document.documentElement.createSVGPoint(), {x,y} )
+
+Symbol.Draggable = Symbol`Draggable`
+
+var Draggable = obj=> {
+
+ if( Symbol.Draggable in obj ) return obj
+ obj[Symbol.Draggable] = true
+
+ obj.startDrag = e=> {
+ let mouse = new DOMPoint( e.clientX , e.clientY )//.matrixTransform( obj.getCTM().inverse() )
+ mouse.x /= obj.ownerSVGElement.currentScale
+ mouse.y /= obj.ownerSVGElement.currentScale
+
+ obj.drag.offset = new DOMPoint( mouse.x - obj.x, mouse.y - obj.y )
+ obj.setAttribute( 'dragged', true )
+ console.log(e, mouse, obj.drag.offset)
+ }
+
+ obj.drag = function(e)
+ {//debugger;
+ if(e.buttons === 1)
+ {
+ let mouse = new DOMPoint( e.clientX / obj.ownerSVGElement.currentScale, e.clientY / obj.ownerSVGElement.currentScale )//.matrixTransform( obj.getScreenCTM().inverse() )
+ , pos = new DOMPoint( mouse.x - obj.drag.offset.x, mouse.y - obj.drag.offset.y)
+
+ // pos.x /= obj.ownerSVGElement.currentScale
+ // pos.y /= obj.ownerSVGElement.currentScale
+ this.moveTo( pos )
+ this.dispatchEvent( new Event('move') )
+ }
+ }
+
+ obj.stopDrag = e=> obj.removeAttribute('dragged')
+
+ obj.on`mousedown`( obj.startDrag )
+ // ,onmousemove:e=> e.buttons === 1 && e.target.drag(e)
+ //obj.onmouseup = e=> e.currentTarget.setAttribute(' dragged', false )
+
+ return obj
+}
+Draggable.startGlobalDrag = e=> e.target.$$`[dragged]`.filter(el=>el.draggable).map( el=> el.drag(e) )
+Draggable.stopGlobalDrag = e=> e.target.$$`[dragged]`.map( el=> el.removeAttribute('dragged') )
+
+// $$`[draggable]`
+// .map( Draggable )
+
+// document.documentElement.on`mousemove`( e=> $$`[dragged]`.filter(el=>el.draggable).map( el=> el.drag(e) ) , true )
+// document.documentElement.onmouseup = e=> $$`[dragged]`.map( el=> el.removeAttribute('dragged') )
+
+
+// Makes an element in an SVG document draggable.
+// Fires custom `dragstart`, `drag`, and `dragend` events on the
+// element with the `detail` property of the event carrying XY
+// coordinates for the location of the element.
+function makeDraggable(el)
+{
+ if (!el) return console.error('makeDraggable() needs an element')
+
+ let svg, pt, doc, root
+ //var svg = el;
+ //while (svg && svg.tagName!='svg') svg=svg.parentNode;
+ //if (!svg) return console.error(el,'must be inside an SVG wrapper');
+ //var pt=svg.createSVGPoint(), doc=svg.ownerDocument;
+
+ //var root = doc.rootElement || doc.body || svg;
+ let xlate, txStartX, txStartY, mouseStart
+ let xforms = el.transform.baseVal
+
+ el.addEventListener('mousedown', e=> e.which == 1 && startMove(e), false )
+
+ function startMove(evt)
+ {
+ svg = el.ownerSVGElement
+ pt = svg.createSVGPoint()
+ doc = svg.ownerDocument
+ root = doc.rootElement || doc.body || svg
+
+ // We listen for mousemove/up on the root-most
+ // element in case the mouse is not over el.
+ root.addEventListener('mousemove',handleMove,false)
+ root.addEventListener('mouseup', finishMove,false)
+
+ // Ensure that the first transform is a translate()
+ xlate = xforms.numberOfItems>0 && xforms.getItem(0)
+ if (!xlate || xlate.type != SVGTransform.SVG_TRANSFORM_TRANSLATE){
+ xlate = xforms.createSVGTransformFromMatrix( svg.createSVGMatrix() )
+ xforms.insertItemBefore( xlate, 0 )
+ }
+ txStartX = xlate.matrix.e
+ txStartY = xlate.matrix.f
+ mouseStart = inElementSpace(evt)
+ fireEvent('dragstart')
+ }
+
+ function handleMove(evt)
+ {
+ var point = inElementSpace(evt)
+ xlate.setTranslate(
+ txStartX + point.x - mouseStart.x,
+ txStartY + point.y - mouseStart.y
+ )
+ fireEvent('drag')
+ }
+
+ function finishMove(evt)
+ {
+ root.removeEventListener('mousemove',handleMove,false)
+ root.removeEventListener('mouseup', finishMove,false)
+ fireEvent('dragend')
+ }
+
+ function fireEvent(eventName)
+ {
+ var event = new Event(eventName)
+ event.detail = { x:xlate.matrix.e, y:xlate.matrix.f }
+ return el.dispatchEvent(event)
+ }
+
+ // Convert mouse position from screen space to coordinates of el
+ function inElementSpace(evt)
+ {
+ pt.x = evt.clientX
+ pt.y = evt.clientY
+ return pt.matrixTransform( el.parentNode.getScreenCTM().inverse() )
+ }
+
+ return el
+}
+
+
+export { Draggable, makeDraggable }
\ No newline at end of file
diff --git a/EventojLancilo.js b/EventojLancilo.js
new file mode 100644
index 0000000..dfb80f0
--- /dev/null
+++ b/EventojLancilo.js
@@ -0,0 +1,98 @@
+
+var Event = Event || class Event { constructor(type){this.type=type} }
+// eventoj lancilo = events launcher
+var EventojLancilo = function( obj )
+{
+ obj = this instanceof EventojLancilo ? this : obj
+ obj = obj instanceof Function ? obj.prototype : obj
+
+ let has = {
+ add: obj.addEventListener || obj.on
+ , rem: obj.removeEventListener || obj.off
+ , emit: obj.dispatchEvent || obj.emit || obj.fire || obj.fireEvent
+ }
+ // console.log(has)
+ if( !has.add )
+ obj[Symbol.Eventoj] = {}
+
+ // obj.on = tplOrCall(add) tpl?
+ // obj.on('a',cb) n ok
+ // obj.on('a',cb,opt) n ok
+ // obj.on('a b',cb) n ok
+ // obj.on(['a','b'],cb) y ko
+ // obj.on(['a','b'],cb,opt) n ok
+ // obj.on(['a','b','c'],cb,{}) y ko
+ // obj.on({a:cba,b:cb2,c:cb3}) n ok
+
+ // obj.on('a')(cb) n ko
+ // obj.on(['a'])(cb) y ok
+
+ // obj.on`a`(cb) y ok
+ // obj.on`${str}change`(cb) y ok
+ function tplOrCall( fn )
+ {
+ return function( ss, ...pp )
+ {
+ let isTpl = Array.isArray( ss ) && ss.every( o=> typeof o == 'string' ) && ss.length == pp.length + 1
+ return isTpl
+ ? (...a)=> fn.bind(this)( String.merge(ss,...pp), ...a )
+ : fn.bind(this)( ss, ...pp )
+ }
+ }
+ // obj.on(['a','b'],cb,opt)
+ // obj.on({a:cba,b:cb2,c:cb3},opt)
+ // obj.on('a b',cb,opt)
+ function multi( fn )
+ {
+ return ( ev, ...args )=>
+ Array.isArray( ev )
+ ? ev.map( e=> fn(e,...args) )
+ : typeof ev == 'object'
+ ? Object.keys( ev ).map( e=> fn(e,ev[e],...args) )
+ : fn(ev,...args)
+ }
+ function on( event, handler, options )
+ {
+ let evs = this[Symbol.Eventoj][event] = this[Symbol.Eventoj][event] || []
+ !evs.includes( handler )
+ && evs.push( handler )
+ return this
+ }
+ function one( event, handler, options )
+ {
+ return on( event, (...args)=> off(handler) && handler(...args) )
+ }
+ function off( event, handler )
+ {
+ let evs = obj[Symbol.Eventoj][event] = obj[Symbol.Eventoj][event] || []
+ evs.includes( handler )
+ && evs.splice( evs.indexOf(handler), 1 )
+ return obj
+ }
+ function fire( event, ...args )
+ {
+ event = typeof event == 'string'
+ ? new Event( event )
+ : event
+ event.target = obj
+ event.details = args
+ event.type in obj[Symbol.Eventoj]
+ && obj[Symbol.Eventoj][event.type].map( h=> h.call(obj, event, ...args) )
+ return event
+ }
+ Object.defineProperties( obj, {
+ on: { value: tplOrCall(has.add ? has.add : on) }
+ , off: { value: tplOrCall(has.rem ? has.rem : off) }
+ , fire: { value: tplOrCall(has.emit ? has.emit : fire) }
+ , one: { value: tplOrCall(has.add ? has.add : one) }
+ , once: { value: tplOrCall(has.add ? has.add : one) }
+ , addEventListener: { value: tplOrCall(has.add ? has.add : on) }
+ , removeEventListener: { value: tplOrCall(has.rem ? has.rem : off) }
+ , dispatchEvent: { value: tplOrCall(has.emit ? has.emit : fire) }
+ , emit: { value: tplOrCall(has.emit ? has.emit : fire) }
+ })
+}
+Symbol.Eventoj = Symbol`Eventoj`
+
+
+export { Event, EventojLancilo }
\ No newline at end of file
diff --git a/Group.js b/Group.js
new file mode 100644
index 0000000..1adbac5
--- /dev/null
+++ b/Group.js
@@ -0,0 +1,8 @@
+import { SVG } from './DOM.js'
+
+const Group = ( cls )=> {
+ let $dom = SVG``[0]
+ return $dom
+}
+
+export { Group }
\ No newline at end of file
diff --git a/Overlay.js b/Overlay.js
new file mode 100644
index 0000000..89725c9
--- /dev/null
+++ b/Overlay.js
@@ -0,0 +1,61 @@
+import { SVG, DOM } from './DOM.js'
+/**
+ * Context menus globally managed
+ * s that have a contextmenu handler at capture time put their menu
+ * under event.menu property and this final bubble handler on document
+ * creates the menu HTML in a SVG's .
+ */
+document.xxxoncontextmenu = e=> {
+ console.log(e)
+ // console.log(e.path)
+ console.log(e.menu)
+ // let items = e.menu//path.reverse().reduce( (m,n)=> ({...m,...(n.menu?n.menu:{})}),{})
+ // console.log(items, Object.keys(items).length )
+ if( e.menu )
+ {
+ e.preventDefault()
+ // let menu
+ // , layer = UI.Overlay( menu = UI.Menu( items ) )
+ // menu.setAttribute( 'x', e.clientX )
+ // menu.setAttribute( 'y', e.clientY )
+ // menu.style.transform = `translate( ${e.clientX}px, ${e.clientY}px )`
+ // menu.style.position = 'absolute'
+ // document.documentElement.append( layer )
+ document.documentElement.append(
+ Overlay(
+ ContextMenu( e, e.menu )
+ )
+ )
+ }
+}
+const Overlay = ( ...items )=> {
+ let html = SVG``[0]
+ html.append( ...items )
+ html.one`click`( e=> /*e.target == html && */html.remove() )
+ return html
+}
+const Menu = ( items, label )=> {
+ let html = DOM``.children[0]
+ , btn
+ html.append(
+ ...Object.keys( items )
+ .map( k=> typeof items[k] == 'object'
+ ? Menu( items[k], k )
+ : ( btn = DOM``.children[0], btn.on`click`( items[k] ), btn )
+ )
+ // ...items.map( item=> DOM``)
+ )
+ return html
+}
+const ContextMenu = ( {x,y}, items )=> {
+ let menu = Menu( items )
+ menu.style.transform = `translate( ${x}px, ${y}px )`
+ menu.style.position = 'absolute'
+ return menu
+}
+
+export {
+ Overlay
+, Menu
+, ContextMenu
+}
\ No newline at end of file
diff --git a/Viewport.js b/Viewport.js
new file mode 100644
index 0000000..d53d549
--- /dev/null
+++ b/Viewport.js
@@ -0,0 +1,393 @@
+import { DOM, SVG, CSS } from './DOM.js'
+import { makeDraggable, Draggable } from './Draggable.js'
+
+class Viewport extends HTMLElement {
+
+ static stylesForSVG = `
+ @namespace html "http://www.w3.org/1999/xhtml";
+
+ /*TitilliumWeb-ExtraLight.ttf*/
+ :root {
+ --canvas-size: 1000000px;
+ background-color: hsl(259 14% 12% / 1);
+ --grid-color-hsl: 256deg 5% 46%;
+ --grid-color: hsl(var(--grid-color-hsl));
+
+ display: inline-flex;
+ }
+ :root([debug]) g.Box foreignObject { border: 1px dashed red; }
+
+ [coordinate] { display: none }
+ :root([grid]) [coordinate] { display: inline }
+
+ [coordinate] [origin],
+ [coordinate] [x-axis],
+ [coordinate] [y-axis] {
+ fill: var(--grid-color);
+ stroke: var(--grid-color);
+ stroke-width: 3px;
+ }
+ [coordinate] [x-axis] {
+ x1: calc(var(--canvas-size) * -1);
+ x2: var(--canvas-size);
+ }
+ [coordinate] [y-axis] {
+ y1: calc(var(--canvas-size) * -1);
+ y2: var(--canvas-size);
+ }
+ [coordinate] [grid] {
+ x: calc(var(--canvas-size) * -1);
+ y: calc(var(--canvas-size) * -1);
+ width: calc(var(--canvas-size) * 2);
+ height: calc(var(--canvas-size) * 2);
+ }
+
+ [mouse] { pointer-events:none }
+
+ [draggable] { cursor: move }
+
+ g.Box { --color: rgb(55, 50, 66); }
+ g.Box foreignObject { font-family: 'Titillium Web', sans-serif; filter: drop-shadow(black 0px 2px 3px) drop-shadow(rgba(0, 0, 0, 0.5) 0px 2px 10px); }
+ g.Box foreignObject * { pointer-events: none; user-select: none }
+ g.Box foreignObject > html|box {
+ display: flex;
+ background: var(--color);
+ border-radius: 5px;
+ padding: 2em .5em .3em;
+ color: white;
+ flex-direction: column;
+ place-items: stretch;
+ box-sizing: border-box;
+ }
+ g.Box foreignObject > html|box > html|header {
+ position: fixed;
+ width: 100%;
+ background: rgba(255,255,255,0.3);
+ top: 0;
+ left: 0;
+ border-radius: 5px 5px 0 0;
+ padding: .3em;
+ box-sizing: border-box;
+ }
+ g.Box foreignObject > html|box > html|socket[output] {
+ align-self: flex-end;
+ }
+ g.Box .anchor {
+ r: 5px;
+ fill: rgb(22, 199, 118);
+ stroke: var(--color);
+ stroke-width: 2;
+ }
+ g.Box .anchor.string {
+ fill: chocolate;
+ }
+ `
+ static styles = CSS`
+
+ menu {
+ padding: .2em;
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ color: wheat;
+ background: #FFF1;
+ font-family: sans-serif;
+ font-size: 10px;
+ /* box-shadow: 0 2px 4px black; */
+ }
+
+ menu button {
+ white-space: nowrap;
+ background: transparent;
+ border: none;
+ text-align: start;
+ font-family: sans-serif;
+ color: wheat;
+ outline: none;
+ }
+
+ menu[label]::before {
+ content: attr(label);
+ }
+
+ button:hover {
+ background: #FFF2;
+ }
+
+ foreignObject > menu {
+ box-shadow: 0 2px 4px black;
+ padding: .3em;
+ background: #131722e0;
+ backdrop-filter: blur(3px);
+ }
+
+ `
+
+ constructor()
+ {
+ super()
+ this.attachShadow({ mode: 'open' })
+ this.shadowRoot.insertBefore()
+ this.shadowRoot.innerHTML =
+ `
+ `
+//
+ this.shadowRoot.adoptedStyleSheets = [Viewport.styles]
+ this.$svg = this.shadowRoot.querySelector('svg')
+ this.$svg.append(
+ DOM``
+ )
+
+ //.ownerDocument.adoptedStyleSheets = [Viewport.stylesForSVG]
+ this.$canvas = this.$svg.querySelector('g[canvas]')
+ this.$mouse = this.$svg.querySelector('g[mouse]')
+// this.$view = this.$svg.querySelector('rect[view]')
+// makeDraggable( this.$view )
+// this.$view.on`drag`( e=> this.$svg.setAttribute('viewBox',`${e.target.transform.baseVal[0].matrix.e} ${e.target.transform.baseVal[0].matrix.f} ${e.target.width.baseVal.value} ${e.target.width.baseVal.value}`) )
+ this.$slot = this.shadowRoot.querySelector('slot')
+
+ }
+
+ connectedCallback()
+ {
+ if( !this.$svg.hasAttribute('viewBox') )
+ this.$svg.setAttribute('viewBox', `0 0 ${this.offsetWidth} ${this.offsetHeight}`)
+ this.addListeners()
+ }
+
+ addListeners()
+ {
+ // Children svg append
+ this.$slot.on`slotchange`( e=>
+ this.$slot.assignedNodes()
+ .filter( node=> node.$svg )
+ .map( node=> this.$canvas.append(node.$svg) )
+
+ )
+
+ // Mouse
+ this.onmousemove = e=> /*console.log('mouse',e.movementX,e.movementY)||*/ requestAnimationFrame( ()=>this.$mouse.moveTo(getMousePosition(e) ) )
+
+ // Zoom
+ //@TODO Disable zoom while panning
+ //@TODO Get min(width,height)
+ this.onmousewheel = e=>
+ this.$svg.panAndZoom( false, false, (this.$svg.viewBox.baseVal.height/2) * ( 1-(e.deltaY/1000) ) )
+// this.onmousewheel = e=> this.$svg.currentScale *= 1 - ( e.deltaY / 1000 )
+ // this.onmousewheel = e=> this.setView({
+ // width: 1 + ( e.deltaY / 1000 ),
+ // height: 1 + ( e.deltaY / 1000 )
+ // })
+
+const getMousePosition = e=> {
+ var CTM = this.$svg.getScreenCTM()
+ if (e.touches) { e = e.touches[0] }
+ return {
+ x: (e.x - CTM.e) / CTM.a,
+ y: (e.y - CTM.f) / CTM.d
+ }
+}
+
+// Convert mouse position from screen space to coordinates of el
+const inElementSpace = e=> {
+ var pt = this.$svg.createSVGPoint()
+ pt.x = e.x
+ pt.y = e.y
+ return pt.matrixTransform( this.$svg.getScreenCTM().inverse() )
+}
+
+ // Pan
+ //@TODO x Change to moving $canvas
+ //@TODO x Get initial CTM when start pan and don't update it while panning
+ this.pan = e=> {
+
+ this._mouseOffset = { x: e.clientX, y: e.clientY }//{ x: this.$mouse.x, y: this.$mouse.y }
+// this.$svg.dataset.dragX = e.x
+// this.$svg.dataset.dragY = e.y
+// this._panStartCTM = this.$svg.getScreenCTM()
+// let rect = this.$svg.viewBox.baseVal
+// this.$svg.dataset.curTrX = rect.x + ( rect.width/2 )//this.$svg.currentTranslate.x
+// this.$svg.dataset.curTrY = rect.y + ( rect.height/2 )//this.$svg.currentTranslate.y
+// this._startPan = {
+// x: rect.x + ( rect.width/2 )
+// , y: rect.y + ( rect.height/2 )
+// }
+ this._startPan = this.$svg.viewBox.baseVal.center
+ this.addEventListener('mousemove', this.panning )
+
+ }
+ this.panning = e=> {
+// console.log(e)
+// this.$svg.currentTranslate.x = this.$svg.dataset.curTrX - ( this.$svg.dataset.dragX - e.x )
+// this.$svg.currentTranslate.y = this.$svg.dataset.curTrY - ( this.$svg.dataset.dragY - e.y )
+// let mouse = getMousePosition( e )
+ let _mouseDiff = { x: e.clientX - this._mouseOffset.x, y: e.clientY - this._mouseOffset.y }
+ _mouseDiff.x /= this.$svg.clientHeight / this.$svg.viewBox.baseVal.height
+ _mouseDiff.y /= this.$svg.clientHeight / this.$svg.viewBox.baseVal.height
+// let _mouseDiffCTM = getMousePosition( _mouseDiff )
+// let _mouseDiffSpace = inElementSpace( _mouseDiff )
+// let rect = this.$svg.viewBox.baseVal
+// let center = this.$svg.viewBox.baseVal.center//{
+// x: rect.x + ( rect.width/2 )
+// , y: rect.y + ( rect.height/2 )
+// }
+ this.$svg.panAndZoom(
+// this._startPan.x - ( mouse.x - this._mouseOffset.x )
+// , this._startPan.y - ( mouse.y - this._mouseOffset.y )
+ this._startPan.x - _mouseDiff.x
+ , this._startPan.y - _mouseDiff.y
+// center.x - ( _mouseDiffCTM.x )
+// , center.y - ( _mouseDiffCTM.y )
+ )
+ }
+ this.finishPan = e=> this.removeEventListener('mousemove', this.panning )
+ this.cancelPan = e=> console.warn('@TODO')
+
+ this.onmousedown = e=> {
+ if( e.which == 2 )// Wheel click
+ this.pan( e )
+ }
+ this.onmouseup = e=> this.finishPan( e )
+
+ // Draggable stuff
+ Draggable.startGlobalDrag = e=> this.$svg.$$`[dragged]`.filter(el=>el.draggable).map( el=> el.drag(e) )
+ Draggable.stopGlobalDrag = e=> this.$svg.$$`[dragged]`.map( el=> el.removeAttribute('dragged') )
+
+ // Draggable
+ window.on`mousemove`( Draggable.startGlobalDrag , true )
+ window.on`mouseup`( Draggable.stopGlobalDrag )
+
+
+ /**
+ * Context menus globally managed
+ * s that have a contextmenu handler at capture time put their menu
+ * under event.menu property and this final bubble handler on document
+ * creates the menu HTML in a SVG's .
+ */
+ this.$svg.oncontextmenu = e=> {
+ console.log(e)
+ // console.log(e.path)
+ console.log(e.menu)
+ // let items = e.menu//path.reverse().reduce( (m,n)=> ({...m,...(n.menu?n.menu:{})}),{})
+ // console.log(items, Object.keys(items).length )
+ if( e.menu )
+ {
+ e.preventDefault()
+ // let menu
+ // , layer = UI.Overlay( menu = UI.Menu( items ) )
+ // menu.setAttribute( 'x', e.clientX )
+ // menu.setAttribute( 'y', e.clientY )
+ // menu.style.transform = `translate( ${e.clientX}px, ${e.clientY}px )`
+ // menu.style.position = 'absolute'
+ // document.documentElement.append( layer )
+ this.$svg.append(
+ Overlay(
+ ContextMenu( e, e.menu )
+ )
+ )
+ }
+ }
+ }
+
+ newDocument()
+ {
+ let doc = DOM`
+ `
+ return doc
+ }
+
+ loadDocument()
+ {
+
+ }
+
+ openDocument( doc )
+ {
+ this.sha
+ }
+
+ async saveToFile()
+ {
+ let file = await showSaveFilePicker()
+ , W = await file.createWritable()
+ await W.write( this.toString() )
+ await W.close()
+ }
+
+ toString()
+ {
+ let styles
+ , dolly = this.$svg.cloneNode(true)
+// dolly.append(
+// styles = DOM``
+// )
+
+ dolly.removeAttribute('width')
+ dolly.removeAttribute('height')
+
+ dolly.setAttribute('xmlns', 'http://www.w3.org/2000/svg' )
+ dolly.setAttribute('xmlns:svg', 'http://www.w3.org/2000/svg' )
+ dolly.setAttribute('xmlns:html', 'http://www.w3.org/1999/xhtml' )
+
+ return dolly.outerHTML
+ }
+}
+customElements.define( 'nb-viewport', Viewport )
+
+
+
+export { Viewport }
\ No newline at end of file
diff --git a/fonts/TitilliumWeb/NaPDcZTIAOhVxoMyOr9n_E7ffAzHGIVzY4SY.woff2 b/fonts/TitilliumWeb/NaPDcZTIAOhVxoMyOr9n_E7ffAzHGIVzY4SY.woff2
new file mode 100644
index 0000000..9a09587
Binary files /dev/null and b/fonts/TitilliumWeb/NaPDcZTIAOhVxoMyOr9n_E7ffAzHGIVzY4SY.woff2 differ
diff --git a/index-local.html b/index-local.html
new file mode 100644
index 0000000..7f38063
--- /dev/null
+++ b/index-local.html
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..91910d4
--- /dev/null
+++ b/index.html
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..e8daa2c
--- /dev/null
+++ b/index.js
@@ -0,0 +1,16 @@
+
+export { EventojLancilo } from './EventojLancilo.js'
+export {
+ logNpass
+, DOM
+, SVG
+, CSS
+, $
+, $$
+} from './DOM.js'
+export { Draggable, makeDraggable } from './Draggable.js'
+export { Overlay, Menu } from './Overlay.js'
+export { Group } from './Group.js'
+export { Box } from './Box.js'
+
+export { Viewport } from './Viewport.js'