393 lines
12 KiB
JavaScript
393 lines
12 KiB
JavaScript
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 } |