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` + +
${title}
+ ${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`${String.merge(ss,...pp)}` + .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'