From a57110514f0916a9d99f79d67567ae02bce42110 Mon Sep 17 00:00:00 2001 From: devingfx Date: Sat, 24 Apr 2021 17:16:00 +0200 Subject: [PATCH] Initial commit --- Box.js | 93 +++++ DOM.js | 265 ++++++++++++ Draggable.js | 135 ++++++ EventojLancilo.js | 98 +++++ Group.js | 8 + Overlay.js | 61 +++ Viewport.js | 393 ++++++++++++++++++ ...NaPDcZTIAOhVxoMyOr9n_E7ffAzHGIVzY4SY.woff2 | Bin 0 -> 6680 bytes index-local.html | 38 ++ index.html | 75 ++++ index.js | 16 + 11 files changed, 1182 insertions(+) create mode 100644 Box.js create mode 100644 DOM.js create mode 100644 Draggable.js create mode 100644 EventojLancilo.js create mode 100644 Group.js create mode 100644 Overlay.js create mode 100644 Viewport.js create mode 100644 fonts/TitilliumWeb/NaPDcZTIAOhVxoMyOr9n_E7ffAzHGIVzY4SY.woff2 create mode 100644 index-local.html create mode 100644 index.html create mode 100644 index.js 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 0000000000000000000000000000000000000000..9a09587b5d62bde63c72aaee50c8124e34d240a0 GIT binary patch literal 6680 zcmV+z8t3JAPew8T0RR9102&wo4*&oF083y102#jk0RR9100000000000000000000 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 +@import url('https://fonts.googleapis.com/css2?family=Titillium+Web:wght@200&display=swap'); +html, body, svg, vp-viewport { + margin: 0; padding: 0; width: 100%; height: 100%; + --grid-color: grey; +} + + + + + + + + + + + + + + + + + \ 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'