nouilles/Viewport.js

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 }