render resize handlers

This commit is contained in:
“chrisshank” 2024-08-18 12:49:01 -07:00
parent 47c4381791
commit 929f3384f9
3 changed files with 155 additions and 23 deletions

View File

@ -5,8 +5,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shapes</title> <title>Shapes</title>
<style> <style>
@import url('../src/elements/main.css');
html { html {
height: 100%; height: 100%;
} }
@ -18,7 +16,9 @@
} }
spatial-geometry { spatial-geometry {
border: 2px solid black; width: 50px;
height: 50px;
background: rgb(187, 178, 178);
} }
</style> </style>
</head> </head>

View File

@ -1,11 +0,0 @@
spatial-canvas {
display: block;
position: relative;
}
spatial-geometry {
display: block;
position: absolute;
cursor: pointer;
content-visibility: auto;
}

View File

@ -3,15 +3,125 @@ export type Shape = 'rectangle' | 'circle' | 'triangle';
// Can we make adding new shapes extensible via a static property? // Can we make adding new shapes extensible via a static property?
const shapes = new Set(['rectangle', 'circle', 'triangle']); const shapes = new Set(['rectangle', 'circle', 'triangle']);
export type Vector = { x: number; y: number; movementX: number; movementY: number }; export type MoveEventDetail = { x: number; y: number; movementX: number; movementY: number };
// Should the move event bubble? // Should the move event bubble?
export class MoveEvent extends CustomEvent<Vector> { export class MoveEvent extends CustomEvent<MoveEventDetail> {
constructor(vector: Vector) { constructor(vector: MoveEventDetail) {
super('move', { detail: vector, cancelable: true, bubbles: true }); super('move', { detail: vector, cancelable: true, bubbles: true });
} }
} }
const styles = new CSSStyleSheet();
styles.replaceSync(`
:host {
display: block;
position: absolute;
cursor: pointer;
}
:host(:not([selected])) [resize-handler] {
display: none;
}
:host([selected]) [resize-handler] {
display: block;
position: absolute;
box-sizing: border-box;
padding: 0;
background: hsl(210, 20%, 98%);
&[resize-handler="top-left"],
&[resize-handler="top-right"],
&[resize-handler="bottom-right"],
&[resize-handler="bottom-left"] {
width: 13px;
aspect-ratio: 1;
transform: translate(-50%, -50%);
border: 1.5px solid hsl(214, 84%, 56%);
border-radius: 2px;
z-index: 2;
}
&[resize-handler="top-left"] {
top: 0;
left: 0;
}
&[resize-handler="top-right"] {
top: 0;
left: 100%;
}
&[resize-handler="bottom-right"] {
top: 100%;
left: 100%;
}
&[resize-handler="bottom-left"] {
top: 100%;
left: 0;
}
&[resize-handler="top-left"], &[resize-handler="bottom-right"] {
cursor: url("data:image/svg+xml,<svg height='32' width='32' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' style='color: black;'><defs><filter id='shadow' y='-40%' x='-40%' width='180px' height='180%' color-interpolation-filters='sRGB'><feDropShadow dx='1' dy='-0.9999999999999999' stdDeviation='1.2' flood-opacity='.5'/></filter></defs><g fill='none' transform='rotate(90 16 16)' filter='url(%23shadow)'><path d='m19.7432 17.0869-4.072 4.068 2.829 2.828-8.473-.013-.013-8.47 2.841 2.842 4.075-4.068 1.414-1.415-2.844-2.842h8.486v8.484l-2.83-2.827z' fill='%23fff'/><path d='m18.6826 16.7334-4.427 4.424 1.828 1.828-5.056-.016-.014-5.054 1.842 1.841 4.428-4.422 2.474-2.475-1.844-1.843h5.073v5.071l-1.83-1.828z' fill='%23000'/></g></svg>") 16 16, pointer;
}
&[resize-handler="top-right"], &[resize-handler="bottom-left"] {
cursor: url("data:image/svg+xml,<svg height='32' width='32' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' style='color: black;'><defs><filter id='shadow' y='-40%' x='-40%' width='180px' height='180%' color-interpolation-filters='sRGB'><feDropShadow dx='1' dy='1' stdDeviation='1.2' flood-opacity='.5'/></filter></defs><g fill='none' transform='rotate(0 16 16)' filter='url(%23shadow)'><path d='m19.7432 17.0869-4.072 4.068 2.829 2.828-8.473-.013-.013-8.47 2.841 2.842 4.075-4.068 1.414-1.415-2.844-2.842h8.486v8.484l-2.83-2.827z' fill='%23fff'/><path d='m18.6826 16.7334-4.427 4.424 1.828 1.828-5.056-.016-.014-5.054 1.842 1.841 4.428-4.422 2.474-2.475-1.844-1.843h5.073v5.071l-1.83-1.828z' fill='%23000'/></g></svg>") 16 16, pointer;
}
&[resize-handler="top"],
&[resize-handler="right"],
&[resize-handler="bottom"],
&[resize-handler="left"] {
background-color: hsl(214, 84%, 56%);
background-clip: content-box;
border: unset;
z-index: 1;
}
&[resize-handler="top"] {
top: 0;
left: 0;
right: 0;
transform: translate(0, -50%);
}
&[resize-handler="right"] {
top: 0;
bottom: 0;
right: 0;
transform: translate(50%, 0);
}
&[resize-handler="bottom"] {
bottom:0;
left: 0;
right: 0;
transform: translate(0, 50%);
}
&[resize-handler="left"] {
top: 0;
bottom: 0;
left: 0;
transform: translate(-50%, 0);
}
&[resize-handler="top"], &[resize-handler="bottom"] {
height: 6px;
padding: 2px 0;
cursor: url("data:image/svg+xml,<svg height='32' width='32' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' style='color: black;'><defs><filter id='shadow' y='-40%' x='-40%' width='180px' height='180%' color-interpolation-filters='sRGB'><feDropShadow dx='1' dy='-0.9999999999999999' stdDeviation='1.2' flood-opacity='.5'/></filter></defs><g fill='none' transform='rotate(90 16 16)' filter='url(%23shadow)'><path d='m9 17.9907v.005l5.997 5.996.001-3.999h1.999 2.02v4l5.98-6.001-5.98-5.999.001 4.019-2.021.002h-2l.001-4.022zm1.411.003 3.587-3.588-.001 2.587h3.5 2.521v-2.585l3.565 3.586-3.564 3.585-.001-2.585h-2.521l-3.499-.001-.001 2.586z' fill='%23fff'/><path d='m17.4971 18.9932h2.521v2.586l3.565-3.586-3.565-3.585v2.605h-2.521-3.5v-2.607l-3.586 3.587 3.586 3.586v-2.587z' fill='%23000'/></g></svg>") 16 16, pointer;
}
&[resize-handler="right"], &[resize-handler="left"] {
width: 6px;
padding: 0 2px;
cursor: url("data:image/svg+xml,<svg height='32' width='32' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' style='color: black;'><defs><filter id='shadow' y='-40%' x='-40%' width='180px' height='180%' color-interpolation-filters='sRGB'><feDropShadow dx='1' dy='1' stdDeviation='1.2' flood-opacity='.5'/></filter></defs><g fill='none' transform='rotate(0 16 16)' filter='url(%23shadow)'><path d='m9 17.9907v.005l5.997 5.996.001-3.999h1.999 2.02v4l5.98-6.001-5.98-5.999.001 4.019-2.021.002h-2l.001-4.022zm1.411.003 3.587-3.588-.001 2.587h3.5 2.521v-2.585l3.565 3.586-3.564 3.585-.001-2.585h-2.521l-3.499-.001-.001 2.586z' fill='%23fff'/><path d='m17.4971 18.9932h2.521v2.586l3.565-3.586-3.565-3.585v2.605h-2.521-3.5v-2.607l-3.586 3.587 3.586 3.586v-2.587z' fill='%23000'/></g></svg>") 16 16, pointer;
}
}`);
// TODO: add z coordinate? // TODO: add z coordinate?
export class SpatialGeometry extends HTMLElement { export class SpatialGeometry extends HTMLElement {
static tagName = 'spatial-geometry'; static tagName = 'spatial-geometry';
@ -29,6 +139,9 @@ export class SpatialGeometry extends HTMLElement {
this.addEventListener('lostpointercapture', this); this.addEventListener('lostpointercapture', this);
this.addEventListener('touchstart', this); this.addEventListener('touchstart', this);
this.addEventListener('dragstart', this); this.addEventListener('dragstart', this);
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.adoptedStyleSheets.push(styles);
} }
#type: Shape = 'rectangle'; #type: Shape = 'rectangle';
@ -82,29 +195,42 @@ export class SpatialGeometry extends HTMLElement {
} }
// Similar to `Element.getClientBoundingRect()`, but returns an SVG path that precisely outlines the shape. // Similar to `Element.getClientBoundingRect()`, but returns an SVG path that precisely outlines the shape.
// We might also want some kind of utility function that maps a path into an approximate set of vertices.
getBoundingPath(): string { getBoundingPath(): string {
return ''; return '';
} }
// We might also want some kind of utility function that maps a path into an approximate set of vertices.
getBoundingVertices() {
return [];
}
handleEvent(event: PointerEvent) { handleEvent(event: PointerEvent) {
switch (event.type) { switch (event.type) {
case 'pointerdown': { case 'pointerdown': {
if (event.button !== 0 || event.ctrlKey) return; if (event.button !== 0 || event.ctrlKey) return;
this.addEventListener('pointermove', this); const target = event.composedPath()[0] as HTMLElement;
this.setPointerCapture(event.pointerId);
target.addEventListener('pointermove', this);
target.setPointerCapture(event.pointerId);
this.setAttribute('selected', '');
this.#createResizeHandlers();
this.style.userSelect = 'none'; this.style.userSelect = 'none';
return; return;
} }
case 'pointermove': { case 'pointermove': {
this.x += event.movementX; if (event.target === this) {
this.y += event.movementY; this.x += event.movementX;
this.y += event.movementY;
} else if ((event.target as HTMLElement).matches('[resize-handler]')) {
console.log('resizing');
}
return; return;
} }
case 'lostpointercapture': { case 'lostpointercapture': {
this.style.userSelect = ''; this.style.userSelect = '';
this.removeEventListener('pointermove', this); const target = event.composedPath()[0] as HTMLElement;
target.removeEventListener('pointermove', this);
return; return;
} }
case 'touchstart': case 'touchstart':
@ -169,4 +295,21 @@ export class SpatialGeometry extends HTMLElement {
} }
} }
} }
#firstSelection = true;
#createResizeHandlers() {
// lazily create resize handlers on first selection
if (this.#firstSelection && this.shadowRoot !== null) {
this.shadowRoot.innerHTML = `
<button resize-handler="top-left"></button>
<button resize-handler="top"></button>
<button resize-handler="top-right"></button>
<button resize-handler="right"></button>
<button resize-handler="bottom-right"></button>
<button resize-handler="bottom"></button>
<button resize-handler="bottom-left"></button>
<button resize-handler="left"></button>`;
this.#firstSelection = false;
}
}
} }