Compare commits

...

1 Commits
main ... dev

Author SHA1 Message Date
devingfx a57110514f Initial commit 2021-04-24 17:16:00 +02:00
11 changed files with 1182 additions and 0 deletions

93
Box.js Normal file
View File

@ -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`<foreignObject width="180" height="100">
<box xmlns="http://www.w3.org/1999/xhtml">
<header>${title}</header>
${outputs.map( str=> `<socket output>${str}</socket>`).join('')}
${inputs.map( str=> `<socket input>${str}</socket>`).join('')}
</box>
</foreignObject>`[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`<circle class="anchor"/>`[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 }

265
DOM.js Normal file
View File

@ -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`<svg xmlns="http://www.w3.org/2000/svg">${String.merge(ss,...pp)}</svg>`
.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 ){}
}
})
/** <line> **/
// @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 ( <rect>, <circle>, <g>, ... )
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
, $
, $$
}

135
Draggable.js Normal file
View File

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

98
EventojLancilo.js Normal file
View File

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

8
Group.js Normal file
View File

@ -0,0 +1,8 @@
import { SVG } from './DOM.js'
const Group = ( cls )=> {
let $dom = SVG`<g${cls ? ` class="${cls}"` : ''}/>`[0]
return $dom
}
export { Group }

61
Overlay.js Normal file
View File

@ -0,0 +1,61 @@
import { SVG, DOM } from './DOM.js'
/**
* Context menus globally managed
* <node>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 <foreignObject>.
*/
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`<foreignObject width="100%" height="100%"/>`[0]
html.append( ...items )
html.one`click`( e=> /*e.target == html && */html.remove() )
return html
}
const Menu = ( items, label )=> {
let html = DOM`<menu xmlns="http://www.w3.org/1999/xhtml"${label ? ` label="${label}"` : ''}></menu>`.children[0]
, btn
html.append(
...Object.keys( items )
.map( k=> typeof items[k] == 'object'
? Menu( items[k], k )
: ( btn = DOM`<button>${k}</button>`.children[0], btn.on`click`( items[k] ), btn )
)
// ...items.map( item=> DOM`<button>${item.label}</button>`)
)
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
}

393
Viewport.js Normal file
View File

@ -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 =
`<svg width="100%" height="100%">
<defs>
<pattern id="smallGrid" width="10" height="10" patternUnits="userSpaceOnUse">
<path d="M 10 0 L 0 0 0 10" fill="none" stroke="var(--grid-color)" stroke-width="0.5"></path>
</pattern>
<pattern id="grid100" width="100" height="100" patternUnits="userSpaceOnUse">
<rect width="100" height="100" fill="url(#smallGrid)"></rect>
<path d="M 100 0 L 0 0 0 100" fill="none" stroke="var(--grid-color)" stroke-width="1"></path>
</pattern>
<pattern id="grid" width="1000" height="1000" patternUnits="userSpaceOnUse">
<rect width="1000" height="1000" fill="url(#grid100)"></rect>
<path d="M 1000 0 L 0 0 0 1000" fill="none" stroke="var(--grid-color)" stroke-width="2.5"></path>
</pattern>
</defs>
<g canvas coordinate>
<rect grid fill="url(#grid)"></rect>
<circle origin r=3 />
<line y-axis />
<line x-axis />
</g>
<g mouse>
<circle fill="red" r=5 />
<line y-axis stroke="red" y1="-1000000" y2="1000000" />
<line x-axis stroke="red" x1="-1000000" x2="1000000" />
</g>
</svg>
<slot></slot>`
// <rect view stroke="red" fill="aliceblue" width=1366 height=663 />
this.shadowRoot.adoptedStyleSheets = [Viewport.styles]
this.$svg = this.shadowRoot.querySelector('svg')
this.$svg.append(
DOM`<style>${Viewport.stylesForSVG}</style>`
)
//.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
* <node>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 <foreignObject>.
*/
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`<svg width="100%" height="100%">
<defs>
<pattern id="smallGrid" width="10" height="10" patternUnits="userSpaceOnUse">
<path d="M 10 0 L 0 0 0 10" fill="none" stroke="var(--grid-color)" stroke-width="0.5"></path>
</pattern>
<pattern id="grid100" width="100" height="100" patternUnits="userSpaceOnUse">
<rect width="100" height="100" fill="url(#smallGrid)"></rect>
<path d="M 100 0 L 0 0 0 100" fill="none" stroke="var(--grid-color)" stroke-width="1"></path>
</pattern>
<pattern id="grid" width="1000" height="1000" patternUnits="userSpaceOnUse">
<rect width="1000" height="1000" fill="url(#grid100)"></rect>
<path d="M 1000 0 L 0 0 0 1000" fill="none" stroke="var(--grid-color)" stroke-width="2.5"></path>
</pattern>
</defs>
<g canvas coordinate>
<rect grid fill="url(#grid)"></rect>
<circle origin r=3 />
<line y-axis />
<line x-axis />
</g>
<g mouse>
<circle fill="red" r=5 />
<line y-axis stroke="red" y1="-1000000" y2="1000000" />
<line x-axis stroke="red" x1="-1000000" x2="1000000" />
</g>
</svg>
`
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`<style>${
// [...Viewport.stylesForSVG.rules]
// .map( r=> r.cssText )
// .join('\n')
// }</style>`
// )
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 }

38
index-local.html Normal file
View File

@ -0,0 +1,38 @@
<style>
@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;
}
</style>
<script src="./EventojLancilo.js"></script>
<script src="./DOM.js"></script>
<script src="./Draggable.js"></script>
<script src="./Overlay.js"></script>
<script src="./Group.js"></script>
<script src="./Box.js"></script>
<script src="./Viewport.js"></script>
<vp-viewport grid></vp-viewport>
<!-- <vp-box title="Name tag">
<vp-socket output name="output1"></vp-socket>
<vp-socket input name="input1"></vp-socket>
<vp-socket input name="input2"></vp-socket>
</vp-box>
</vp-viewport> -->
<script>
const view = $`vp-viewport`.$svg
view.append(
Box('Name',['aze'],['qsd'])
, Box('Other',['float', 'list', 'list', 'list', 'list', 'list', 'list', 'list'],['foo'])
, Box('Awesome',['position:Vector3'],['scale:Vector3'])
, Box('SVG output',['svg:SVGSvgElement'],[])
, Box('String input',[],['string:String'])
, Box('Selector',['css:String','root:Node'],['nodes:NodeList'])
)
</script>

75
index.html Normal file
View File

@ -0,0 +1,75 @@
<style>
@import url('./fonts/TitilliumWeb/TitilliumWeb.css');
html, body, svg, vp-viewport {
font-family: 'Titillium Web', sans-serif;
margin: 0; padding: 0; width: 100%; height: 100%;
--grid-color: grey;
}
</style>
<!--
<script src="./EventojLancilo.js"></script>
<script src="./DOM.js"></script>
<script src="./Draggable.js"></script>
<script src="./Overlay.js"></script>
<script src="./Group.js"></script>
<script src="./Box.js"></script>
<script src="./Viewport.js"></script>
-->
<script type="module">
import {
EventojLancilo
, logNpass
, DOM
, SVG
, CSS
, $
, $$
, Draggable
, Overlay
, Group
, Box
, Viewport
} from "./index.js"
window.nouilles = {
EventojLancilo
, logNpass
, DOM
, SVG
, CSS
, $
, $$
, Draggable
, Overlay
, Group
, Box
, Viewport
}
</script>
<nb-viewport grid></nb-viewport>
<!-- <vp-box title="Name tag">
<vp-socket output name="output1"></vp-socket>
<vp-socket input name="input1"></vp-socket>
<vp-socket input name="input2"></vp-socket>
</vp-box>
</vp-viewport> -->
<script type="module">
import { $ } from './DOM.js'
import { Box } from './Box.js'
const view = $`vp-viewport`
view.$svg.append(
Box('Name',['aze'],['qsd'])
, Box('Other',['float', 'list', 'list', 'list', 'list', 'list', 'list', 'list'],['foo'])
, Box('Awesome',['position:Vector3'],['scale:Vector3'])
, Box('SVG output',['svg:SVGSvgElement'],[])
, Box('String input',[],['string:String'])
, Box('Selector',['css:String','root:Node'],['nodes:NodeList'])
)
window.view = view
</script>

16
index.js Normal file
View File

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