diff --git a/demo/chains-of-thought/index.html b/demo/chains-of-thought/index.html index d8a691c..2831060 100644 --- a/demo/chains-of-thought/index.html +++ b/demo/chains-of-thought/index.html @@ -46,10 +46,10 @@ border-radius: 7px; &::part(rotate), - &::part(resize-nw), - &::part(resize-ne), - &::part(resize-se), - &::part(resize-sw) { + &::part(resize-top-left), + &::part(resize-top-right), + &::part(resize-bottom-right), + &::part(resize-bottom-left) { display: none; } diff --git a/demo/proximity-based-music.html b/demo/proximity-based-music.html index 39bb660..e82cec3 100644 --- a/demo/proximity-based-music.html +++ b/demo/proximity-based-music.html @@ -18,10 +18,10 @@ folk-shape:has(record-player) { box-shadow: 10px 0px 150px 0px rgba(0, 0, 0, 0.61); - &::part(resize-nw), - &::part(resize-ne), - &::part(resize-se), - &::part(resize-sw) { + &::part(resize-top-left), + &::part(resize-top-right), + &::part(resize-bottom-right), + &::part(resize-bottom-left) { display: none; } } diff --git a/src/common/DOMRectTransform.ts b/src/common/DOMRectTransform.ts index 0180892..47a4e01 100644 --- a/src/common/DOMRectTransform.ts +++ b/src/common/DOMRectTransform.ts @@ -293,6 +293,8 @@ export class DOMRectTransform implements DOMRect { }; } + // TODO: these setters work but surely there's a better way + /** * Sets the **top-left** corner of the rectangle in **local space**, adjusting the position, width, and height accordingly, * and keeps the **bottom-right corner** fixed in the **parent space**. diff --git a/src/folk-shape.ts b/src/folk-shape.ts index 4805654..ca75acf 100644 --- a/src/folk-shape.ts +++ b/src/folk-shape.ts @@ -14,8 +14,8 @@ declare global { const resizeObserver = new ResizeObserverManager(); -type ResizeHandle = 'resize-nw' | 'resize-ne' | 'resize-se' | 'resize-sw'; -type RotateHandle = 'rotation-nw' | 'rotation-ne' | 'rotation-se' | 'rotation-sw'; +type ResizeHandle = 'resize-top-left' | 'resize-top-right' | 'resize-bottom-right' | 'resize-bottom-left'; +type RotateHandle = 'rotation-top-left' | 'rotation-top-right' | 'rotation-bottom-right' | 'rotation-bottom-left'; type Handle = ResizeHandle | RotateHandle | 'move'; export type Dimension = number | 'auto'; @@ -61,17 +61,17 @@ const styles = css` :host(:state(move)), :host(:state(rotate)), - :host(:state(resize-nw)), - :host(:state(resize-ne)), - :host(:state(resize-se)), - :host(:state(resize-sw)) { + :host(:state(resize-top-left)), + :host(:state(resize-top-right)), + :host(:state(resize-bottom-right)), + :host(:state(resize-bottom-left)) { user-select: none; } - [part='resize-nw'], - [part='resize-ne'], - [part='resize-se'], - [part='resize-sw'] { + [part='resize-top-left'], + [part='resize-top-right'], + [part='resize-bottom-right'], + [part='resize-bottom-left'] { display: block; position: absolute; box-sizing: border-box; @@ -85,33 +85,33 @@ const styles = css` border-radius: 2px; } - [part='resize-nw'] { + [part='resize-top-left'] { top: 0; left: 0; } - [part='resize-ne'] { + [part='resize-top-right'] { top: 0; left: 100%; } - [part='resize-se'] { + [part='resize-bottom-right'] { top: 100%; left: 100%; } - [part='resize-sw'] { + [part='resize-bottom-left'] { top: 100%; left: 0; } - [part='resize-nw'], - [part='resize-se'] { + [part='resize-top-left'], + [part='resize-bottom-right'] { cursor: var(--resize-handle-cursor-nw); } - [part='resize-ne'], - [part='resize-sw'] { + [part='resize-top-right'], + [part='resize-bottom-left'] { cursor: var(--resize-handle-cursor-ne); } @@ -128,25 +128,25 @@ const styles = css` cursor: var(--fc-rotate, url('${getRotateCursorUrl(0)}') 16 16, pointer); } - [part='rotation-nw'] { + [part='rotation-top-left'] { top: 0; left: 0; translate: -100% -100%; } - [part='rotation-ne'] { + [part='rotation-top-right'] { top: 0; left: 100%; translate: 0% -100%; } - [part='rotation-se'] { + [part='rotation-bottom-right'] { top: 100%; left: 100%; translate: 0% 0%; } - [part='rotation-sw'] { + [part='rotation-bottom-left'] { top: 100%; left: 0; translate: -100% 0%; @@ -257,14 +257,14 @@ export class FolkShape extends HTMLElement { // Ideally we would creating these lazily on first focus, but the resize handlers need to be around for delegate focus to work. // Maybe can add the first resize handler here, and lazily instantiate the rest when needed? // I can see it becoming important at scale - this.#shadow.innerHTML = html` - - - - - - - + this.#shadow.innerHTML = html` + + + + + + +
`; this.x = Number(this.getAttribute('x')) || 0; @@ -344,10 +344,10 @@ export class FolkShape extends HTMLElement { // Map handle names to corner points const HANDLE_TO_CORNER: Record = { - 'resize-nw': rect.topLeft, - 'resize-ne': rect.topRight, - 'resize-se': rect.bottomRight, - 'resize-sw': rect.bottomLeft, + 'resize-top-left': rect.topLeft, + 'resize-top-right': rect.topRight, + 'resize-bottom-right': rect.bottomRight, + 'resize-bottom-left': rect.bottomLeft, }; const currentPos = rect.toParentSpace(HANDLE_TO_CORNER[handle]); @@ -410,7 +410,11 @@ export class FolkShape extends HTMLElement { // Store initial angle on rotation start if (target.getAttribute('part')?.startsWith('rotation')) { this.#initialRotation = this.#rect.rotation; - const parentRotateOrigin = this.#rect.toParentSpace(this.#rect.rotateOrigin); + // Calculate the absolute rotation origin in parent space + const parentRotateOrigin = this.#rect.toParentSpace({ + x: this.#rect.width * this.#rect.rotateOrigin.x, + y: this.#rect.height * this.#rect.rotateOrigin.y, + }); this.#startAngle = Vector.angleFromOrigin({ x: event.clientX, y: event.clientY }, parentRotateOrigin); } @@ -440,32 +444,37 @@ export class FolkShape extends HTMLElement { const handle = target.getAttribute('part') as Handle; if (handle === null) return; - if (handle.includes('resize')) { + if (handle.startsWith('resize')) { const mouse = { x: event.clientX, y: event.clientY }; this.#handleResize(handle as ResizeHandle, mouse, target, event); return; } if (handle.startsWith('rotation')) { - const parentRotateOrigin = this.#rect.toParentSpace(this.#rect.rotateOrigin); + // Calculate the absolute rotation origin in parent space + const parentRotateOrigin = this.#rect.toParentSpace({ + x: this.#rect.width * this.#rect.rotateOrigin.x, + y: this.#rect.height * this.#rect.rotateOrigin.y, + }); const currentAngle = Vector.angleFromOrigin({ x: event.clientX, y: event.clientY }, parentRotateOrigin); const rotation = this.#initialRotation + (currentAngle - this.#startAngle); - let degrees = (rotation * 180) / Math.PI; + let cursorRotation = (rotation * 180) / Math.PI; switch (handle) { - case 'rotation-ne': - degrees = (degrees + 90) % 360; + case 'rotation-top-right': + cursorRotation = (cursorRotation + 90) % 360; break; - case 'rotation-se': - degrees = (degrees + 180) % 360; + case 'rotation-bottom-right': + cursorRotation = (cursorRotation + 180) % 360; break; - case 'rotation-sw': - degrees = (degrees + 270) % 360; + case 'rotation-bottom-left': + cursorRotation = (cursorRotation + 270) % 360; break; + // top-left handle doesn't need adjustment } const target = event.composedPath()[0] as HTMLElement; - const rotateCursor = getRotateCursorUrl(degrees); + const rotateCursor = getRotateCursorUrl(cursorRotation); target.style.setProperty('cursor', rotateCursor); this.rotation = rotation; return; @@ -554,29 +563,29 @@ export class FolkShape extends HTMLElement { // TODO use css variables const dynamicStyles = ` - [part='resize-nw'], - [part='resize-se'] { + [part='resize-top-left'], + [part='resize-bottom-right'] { cursor: ${resizeCursor0}; } - [part='resize-ne'], - [part='resize-sw'] { + [part='resize-top-right'], + [part='resize-bottom-left'] { cursor: ${resizeCursor90}; } - [part='rotation-nw'] { + [part='rotation-top-left'] { cursor: ${rotateCursor0}; } - [part='rotation-ne'] { + [part='rotation-top-right'] { cursor: ${rotateCursor90}; } - [part='rotation-se'] { + [part='rotation-bottom-right'] { cursor: ${rotateCursor180}; } - [part='rotation-sw'] { + [part='rotation-bottom-left'] { cursor: ${rotateCursor270}; } `; @@ -588,16 +597,16 @@ export class FolkShape extends HTMLElement { const localPointer = this.#rect.toLocalSpace(pointerPos); switch (handle) { - case 'resize-se': + case 'resize-bottom-right': this.#rect.setBottomRight(localPointer); break; - case 'resize-sw': + case 'resize-bottom-left': this.#rect.setBottomLeft(localPointer); break; - case 'resize-nw': + case 'resize-top-left': this.#rect.setTopLeft(localPointer); break; - case 'resize-ne': + case 'resize-top-right': this.#rect.setTopRight(localPointer); break; } @@ -610,28 +619,28 @@ export class FolkShape extends HTMLElement { if (flipWidth && flipHeight) { // Both axes flipped const oppositeHandleMap: Record = { - 'resize-se': 'resize-nw', - 'resize-sw': 'resize-ne', - 'resize-nw': 'resize-se', - 'resize-ne': 'resize-sw', + 'resize-bottom-right': 'resize-top-left', + 'resize-bottom-left': 'resize-top-right', + 'resize-top-left': 'resize-bottom-right', + 'resize-top-right': 'resize-bottom-left', }; nextHandle = oppositeHandleMap[handle]; } else if (flipWidth) { // Only X axis flipped const flipXHandleMap: Record = { - 'resize-se': 'resize-sw', - 'resize-sw': 'resize-se', - 'resize-nw': 'resize-ne', - 'resize-ne': 'resize-nw', + 'resize-bottom-right': 'resize-bottom-left', + 'resize-bottom-left': 'resize-bottom-right', + 'resize-top-left': 'resize-top-right', + 'resize-top-right': 'resize-top-left', }; nextHandle = flipXHandleMap[handle]; } else if (flipHeight) { // Only Y axis flipped const flipYHandleMap: Record = { - 'resize-se': 'resize-ne', - 'resize-sw': 'resize-nw', - 'resize-nw': 'resize-sw', - 'resize-ne': 'resize-se', + 'resize-bottom-right': 'resize-top-right', + 'resize-bottom-left': 'resize-top-left', + 'resize-top-left': 'resize-bottom-left', + 'resize-top-right': 'resize-bottom-right', }; nextHandle = flipYHandleMap[handle]; }