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" />
<title>Shapes</title>
<style>
@import url('../src/elements/main.css');
html {
height: 100%;
}
@ -18,7 +16,9 @@
}
spatial-geometry {
border: 2px solid black;
width: 50px;
height: 50px;
background: rgb(187, 178, 178);
}
</style>
</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?
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?
export class MoveEvent extends CustomEvent<Vector> {
constructor(vector: Vector) {
export class MoveEvent extends CustomEvent<MoveEventDetail> {
constructor(vector: MoveEventDetail) {
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?
export class SpatialGeometry extends HTMLElement {
static tagName = 'spatial-geometry';
@ -29,6 +139,9 @@ export class SpatialGeometry extends HTMLElement {
this.addEventListener('lostpointercapture', this);
this.addEventListener('touchstart', this);
this.addEventListener('dragstart', this);
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.adoptedStyleSheets.push(styles);
}
#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.
// We might also want some kind of utility function that maps a path into an approximate set of vertices.
getBoundingPath(): string {
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) {
switch (event.type) {
case 'pointerdown': {
if (event.button !== 0 || event.ctrlKey) return;
this.addEventListener('pointermove', this);
this.setPointerCapture(event.pointerId);
const target = event.composedPath()[0] as HTMLElement;
target.addEventListener('pointermove', this);
target.setPointerCapture(event.pointerId);
this.setAttribute('selected', '');
this.#createResizeHandlers();
this.style.userSelect = 'none';
return;
}
case 'pointermove': {
this.x += event.movementX;
this.y += event.movementY;
if (event.target === this) {
this.x += event.movementX;
this.y += event.movementY;
} else if ((event.target as HTMLElement).matches('[resize-handler]')) {
console.log('resizing');
}
return;
}
case 'lostpointercapture': {
this.style.userSelect = '';
this.removeEventListener('pointermove', this);
const target = event.composedPath()[0] as HTMLElement;
target.removeEventListener('pointermove', this);
return;
}
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;
}
}
}