From d8e482a7c369562781b9d1bf9d7c3b283fd677b1 Mon Sep 17 00:00:00 2001 From: jkuehner Date: Tue, 27 Aug 2024 09:14:00 +0200 Subject: [PATCH 1/7] Use get-box-quads-polyfill for drawing selection rectangle --- app/components/index.js | 3 ++ app/components/selection/handle.element.css | 20 ++---------- app/components/selection/handles.element.js | 36 ++++++++++++++------- package-lock.json | 6 ++++ package.json | 1 + 5 files changed, 37 insertions(+), 29 deletions(-) diff --git a/app/components/index.js b/app/components/index.js index f6aeb776..88f15f72 100644 --- a/app/components/index.js +++ b/app/components/index.js @@ -1,3 +1,6 @@ +import { addPolyfill } from "get-box-quads-polyfill"; +addPolyfill(window); + export { Handles } from './selection/handles.element' export { Handle } from './selection/handle.element' export { Hover } from './selection/hover.element' diff --git a/app/components/selection/handle.element.css b/app/components/selection/handle.element.css index 9d26d5a0..4e877a56 100644 --- a/app/components/selection/handle.element.css +++ b/app/components/selection/handle.element.css @@ -30,24 +30,8 @@ } } -:host([placement^="top"]) { - --align-self: start; - --translate-y: -50%; -} - -:host([placement^="bottom"]) { - --align-self: end; - --translate-y: 50%; -} - -:host([placement$="start"]) { - --justify-self: start; - --translate-x: -50%; -} - -:host([placement$="end"]) { - --justify-self: end; - --translate-x: 50%; +:host { + translate: -50% -50%; } :host([placement^="top"]), diff --git a/app/components/selection/handles.element.js b/app/components/selection/handles.element.js index 90785ea2..4c3647ec 100644 --- a/app/components/selection/handles.element.js +++ b/app/components/selection/handles.element.js @@ -40,7 +40,7 @@ export class Handles extends HTMLElement { } set position({el, node_label_id}) { - this.$shadow.innerHTML = this.render(el.getBoundingClientRect(), node_label_id, isFixed(el)) + this.$shadow.innerHTML = this.render(el.getBoxQuads()[0], node_label_id, isFixed(el)) if (this._backdrop) { this.backdrop = { @@ -60,9 +60,23 @@ export class Handles extends HTMLElement { : this.$shadow.appendChild(bd.element) } - render({ x, y, width, height, top, left }, node_label_id, isFixed) { + /** + * + * @param {DOMQuad} quad + * @param {string} node_label_id + * @param {boolean} isFixed + * @returns + */ + render(quad, node_label_id, isFixed) { this.$shadow.host.setAttribute('data-label-id', node_label_id) + const left = Math.min(quad.p1.x,quad.p2.x,quad.p3.x,quad.p4.x); + const right = Math.max(quad.p1.x,quad.p2.x,quad.p3.x,quad.p4.x); + const top = Math.min(quad.p1.y,quad.p2.y,quad.p3.y,quad.p4.y); + const bottom = Math.max(quad.p1.y,quad.p2.y,quad.p3.y,quad.p4.y); + const width = right - left; + const height = bottom - top; + this.style.setProperty('--top', `${top + (isFixed ? 0 : window.scrollY)}px`) this.style.setProperty('--left', `${left}px`) this.style.setProperty('--position', isFixed ? 'fixed' : 'absolute') @@ -76,16 +90,16 @@ export class Handles extends HTMLElement { viewBox="0 0 ${width} ${height}" version="1.1" xmlns="http://www.w3.org/2000/svg" > - + - - - - - - - - + + + + + + + + ` } } diff --git a/package-lock.json b/package-lock.json index 7f890141..2d5f74ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "blingblingjs": "^2.3.0", "colorjs.io": "^0.5.0", "construct-style-sheets-polyfill": "^2.4.2", + "get-box-quads-polyfill": "^4.12.0", "hotkeys-js": "^3.13.7", "query-selector-shadow-dom": "^1.0.1" }, @@ -8234,6 +8235,11 @@ "loader-utils": "^1.1.0" } }, + "node_modules/get-box-quads-polyfill": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/get-box-quads-polyfill/-/get-box-quads-polyfill-4.12.0.tgz", + "integrity": "sha512-MBzwk1/j5n1NT6NwxMNwUEqyjs8qGWhDHJHhfuSoODvJvvz6msEos1CgFKQqRhS6i9aQIhCPoxlIfeYBrYgtxw==" + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", diff --git a/package.json b/package.json index dc0b3c3b..e0cca9e9 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "blingblingjs": "^2.3.0", "colorjs.io": "^0.5.0", "construct-style-sheets-polyfill": "^2.4.2", + "get-box-quads-polyfill": "^4.12.0", "hotkeys-js": "^3.13.7", "query-selector-shadow-dom": "^1.0.1" }, From 3f8e743310bb40bc2c4220f8ee1c4bf1cc0a691e Mon Sep 17 00:00:00 2001 From: jkuehner Date: Tue, 27 Aug 2024 20:13:10 +0200 Subject: [PATCH 2/7] fixed version of getBoxQuads --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2d5f74ee..67c5d4ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "blingblingjs": "^2.3.0", "colorjs.io": "^0.5.0", "construct-style-sheets-polyfill": "^2.4.2", - "get-box-quads-polyfill": "^4.12.0", + "get-box-quads-polyfill": "^4.15.0", "hotkeys-js": "^3.13.7", "query-selector-shadow-dom": "^1.0.1" }, @@ -8236,9 +8236,9 @@ } }, "node_modules/get-box-quads-polyfill": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/get-box-quads-polyfill/-/get-box-quads-polyfill-4.12.0.tgz", - "integrity": "sha512-MBzwk1/j5n1NT6NwxMNwUEqyjs8qGWhDHJHhfuSoODvJvvz6msEos1CgFKQqRhS6i9aQIhCPoxlIfeYBrYgtxw==" + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/get-box-quads-polyfill/-/get-box-quads-polyfill-4.15.0.tgz", + "integrity": "sha512-IEU++wyBywpHtyICShNu5lrpmilfF5QkneXBs8Du6aoS+AgI5kilRRPdlqUVBBCvbKXSJ+Vu1gKaiitrc08oaw==" }, "node_modules/get-caller-file": { "version": "2.0.5", diff --git a/package.json b/package.json index e0cca9e9..4c5de4cc 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "blingblingjs": "^2.3.0", "colorjs.io": "^0.5.0", "construct-style-sheets-polyfill": "^2.4.2", - "get-box-quads-polyfill": "^4.12.0", + "get-box-quads-polyfill": "^4.15.0", "hotkeys-js": "^3.13.7", "query-selector-shadow-dom": "^1.0.1" }, From 206e0b51b4e52e8c427c1feebf6d7353841350c8 Mon Sep 17 00:00:00 2001 From: jkuehner Date: Tue, 27 Aug 2024 20:36:54 +0200 Subject: [PATCH 3/7] fix a null ref in getBoxQuad --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 67c5d4ad..c48cc616 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "blingblingjs": "^2.3.0", "colorjs.io": "^0.5.0", "construct-style-sheets-polyfill": "^2.4.2", - "get-box-quads-polyfill": "^4.15.0", + "get-box-quads-polyfill": "^4.16.0", "hotkeys-js": "^3.13.7", "query-selector-shadow-dom": "^1.0.1" }, @@ -8236,9 +8236,9 @@ } }, "node_modules/get-box-quads-polyfill": { - "version": "4.15.0", - "resolved": "https://registry.npmjs.org/get-box-quads-polyfill/-/get-box-quads-polyfill-4.15.0.tgz", - "integrity": "sha512-IEU++wyBywpHtyICShNu5lrpmilfF5QkneXBs8Du6aoS+AgI5kilRRPdlqUVBBCvbKXSJ+Vu1gKaiitrc08oaw==" + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/get-box-quads-polyfill/-/get-box-quads-polyfill-4.16.0.tgz", + "integrity": "sha512-8kxOfwcV+EQH3MBgdpTjCRDDaN8y+kphUrjFXDlqsTAVaD0WbTxf0mz+n+UEoSM2MlpbIPd6yW9QWIgYvMbbIg==" }, "node_modules/get-caller-file": { "version": "2.0.5", diff --git a/package.json b/package.json index 4c5de4cc..92a2286d 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "blingblingjs": "^2.3.0", "colorjs.io": "^0.5.0", "construct-style-sheets-polyfill": "^2.4.2", - "get-box-quads-polyfill": "^4.15.0", + "get-box-quads-polyfill": "^4.16.0", "hotkeys-js": "^3.13.7", "query-selector-shadow-dom": "^1.0.1" }, From 058c73dcb08d07f0c0158acefa64994b4ce3fc6c Mon Sep 17 00:00:00 2001 From: jkuehner Date: Tue, 27 Aug 2024 20:37:12 +0200 Subject: [PATCH 4/7] resize a transformed element --- app/components/selection/handle.element.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/app/components/selection/handle.element.js b/app/components/selection/handle.element.js index 6721aaf9..04127abb 100644 --- a/app/components/selection/handle.element.js +++ b/app/components/selection/handle.element.js @@ -13,7 +13,7 @@ export class Handle extends HTMLElement { connectedCallback() { this.$shadow.adoptedStyleSheets = this.styles this.$shadow.innerHTML = this.render() - + this.button = this.$shadow.querySelector('button') this.button.addEventListener('pointerdown', this.on_element_resize_start.bind(this)) @@ -30,6 +30,9 @@ export class Handle extends HTMLElement { } } + /** + * @param {PointerEvent} e + */ on_element_resize_start(e) { e.preventDefault() e.stopPropagation() @@ -39,11 +42,14 @@ export class Handle extends HTMLElement { const placement = this.placement const handlesEl = e.composedPath().find(el => el.tagName === 'VISBUG-HANDLES') const nodeLabelId = handlesEl.getAttribute('data-label-id') + /** @type {Element[]} */ const [sourceEl] = $(`[data-label-id="${nodeLabelId}"]`) if (!sourceEl) return - const { x: initialX, y: initialY } = e + const t = sourceEl.convertPointFromNode(e, document.body.parentElement); + + const { x: initialX, y: initialY } = t const initialStyle = getComputedStyle(sourceEl) const initialWidth = parseFloat(initialStyle.width) const initialHeight = parseFloat(initialStyle.height) @@ -62,9 +68,11 @@ export class Handle extends HTMLElement { e.preventDefault() e.stopPropagation() - const newX = clamp(0, e.clientX, document.documentElement.clientWidth) - const newY = clamp(0, e.clientY, document.documentElement.clientHeight) - + const t = sourceEl.convertPointFromNode({ x: e.clientX, y: e.clientY }, document.body.parentElement); + + const newX = clamp(0, t.x, document.documentElement.clientWidth) + const newY = clamp(0, t.y, document.documentElement.clientHeight) + const diffX = newX - initialX const diffY = newY - initialY From b545aea045dc1d725a9240571087e592fd7247c3 Mon Sep 17 00:00:00 2001 From: jkuehner Date: Tue, 27 Aug 2024 22:47:54 +0200 Subject: [PATCH 5/7] code formating --- app/components/selection/handles.element.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/components/selection/handles.element.js b/app/components/selection/handles.element.js index 4c3647ec..0e2c5916 100644 --- a/app/components/selection/handles.element.js +++ b/app/components/selection/handles.element.js @@ -70,10 +70,10 @@ export class Handles extends HTMLElement { render(quad, node_label_id, isFixed) { this.$shadow.host.setAttribute('data-label-id', node_label_id) - const left = Math.min(quad.p1.x,quad.p2.x,quad.p3.x,quad.p4.x); - const right = Math.max(quad.p1.x,quad.p2.x,quad.p3.x,quad.p4.x); - const top = Math.min(quad.p1.y,quad.p2.y,quad.p3.y,quad.p4.y); - const bottom = Math.max(quad.p1.y,quad.p2.y,quad.p3.y,quad.p4.y); + const left = Math.min(quad.p1.x, quad.p2.x, quad.p3.x, quad.p4.x); + const right = Math.max(quad.p1.x, quad.p2.x, quad.p3.x, quad.p4.x); + const top = Math.min(quad.p1.y, quad.p2.y, quad.p3.y, quad.p4.y); + const bottom = Math.max(quad.p1.y, quad.p2.y, quad.p3.y, quad.p4.y); const width = right - left; const height = bottom - top; From 42a8d6fb9db52ab757912dc39e6ffce49392b960 Mon Sep 17 00:00:00 2001 From: jkuehner Date: Tue, 27 Aug 2024 22:48:09 +0200 Subject: [PATCH 6/7] padding / margin extension overlay when transformed --- app/components/selection/box-model.element.css | 15 +-------------- app/components/selection/box-model.element.js | 17 ++++++++++++++--- app/features/margin.js | 5 +++-- app/features/padding.js | 5 +++-- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/app/components/selection/box-model.element.css b/app/components/selection/box-model.element.css index 7d116455..2e2e854a 100644 --- a/app/components/selection/box-model.element.css +++ b/app/components/selection/box-model.element.css @@ -2,17 +2,4 @@ pointer-events: none; position: absolute; z-index: var(--layer-5); - width: var(--width); - height: var(--height); - top: var(--top); - left: var(--left); - background-color: var(--bg); - clip-path: polygon( - 0% 0%, 0% 100%, var(--target-left) 100%, - var(--target-left) var(--target-top), - var(--offset-right) var(--target-top), - var(--offset-right) var(--offset-bottom), - 0 var(--offset-bottom), 0 100%, - 100% 100%, 100% 0% - ); -} \ No newline at end of file +} diff --git a/app/components/selection/box-model.element.js b/app/components/selection/box-model.element.js index dbeb8c0f..2378d147 100644 --- a/app/components/selection/box-model.element.js +++ b/app/components/selection/box-model.element.js @@ -20,26 +20,36 @@ export class BoxModel extends HTMLElement { this.createMeasurements(payload) } - render({mode, bounds, sides, color = 'pink'}) { + render({mode, bounds, sides, color = 'pink', element}) { const total_height = bounds.height + sides.bottom + sides.top const total_width = bounds.width + sides.right + sides.left + const q1 = element.getBoxQuads({ box: 'border' })[0]; + const left = Math.min(q1.p1.x, q1.p2.x, q1.p3.x, q1.p4.x); + const top = Math.min(q1.p1.y, q1.p2.y, q1.p3.y, q1.p4.y); + if (mode === 'padding') { + const q2 = element.getBoxQuads({ box: 'content' })[0]; this.drawable = { height: bounds.height - (sides.borders.top + sides.borders.bottom), width: bounds.width - (sides.borders.right + sides.borders.left), top: 0 + sides.borders.top, left: 0 + sides.borders.left, rotation: 'rotate(-45)', + d: "M" + [q1.p1, q1.p2, q1.p3, q1.p4].map(x => (x.x - left) + ',' + (x.y - top)).join(' ') + 'Z ' + + "M" + [q2.p1, q2.p2, q2.p3, q2.p4].map(x => (x.x - left) + ',' + (x.y - top)).join(' ') + 'Z' } } else if (mode === 'margin') { + const q2 = element.getBoxQuads({ box: 'margin' })[0]; this.drawable = { height: total_height, width: total_width, top: 0 - sides.top, left: 0 - sides.left, rotation: 'rotate(45)', + d: "M" + [q1.p1, q1.p2, q1.p3, q1.p4].map(x => (x.x - left) + ',' + (x.y - top)).join(' ') + 'Z ' + + "M" + [q2.p1, q2.p2, q2.p3, q2.p4].map(x => (x.x - left) + ',' + (x.y - top)).join(' ') + 'Z' } } @@ -56,13 +66,14 @@ export class BoxModel extends HTMLElement { return `
- + - + +
` diff --git a/app/features/margin.js b/app/features/margin.js index 8b66ff30..23dc1b52 100644 --- a/app/features/margin.js +++ b/app/features/margin.js @@ -108,11 +108,12 @@ export function createMarginVisual(el, hover = false) { sides[side] = Math.round(val.toFixed(1) * 100) / 100 }) - boxdisplay.position = { + boxdisplay.position = { mode: 'margin', color: hover ? 'purple' : 'pink', - bounds, + bounds, sides, + element: el } } diff --git a/app/features/padding.js b/app/features/padding.js index 9f672b12..94c5e5ee 100644 --- a/app/features/padding.js +++ b/app/features/padding.js @@ -109,14 +109,15 @@ export function createPaddingVisual(el, hover = false) { sides[side] = Math.round(val.toFixed(1) * 100) / 100 }) - boxdisplay.position = { + boxdisplay.position = { mode: 'padding', color: hover ? 'purple' : 'pink', - bounds, + bounds, sides: { ...sides, borders: calculatedBorder, }, + element: el } } From f12a6817627aa7073939318cfda6343b05ff599b Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Sat, 8 Aug 2026 13:17:27 +0200 Subject: [PATCH 7/7] Finish transformed getBoxQuads overlays --- app/components/index.js | 6 +- .../selection/box-model.element.css | 8 ++ app/components/selection/box-model.element.js | 135 +++++------------- app/components/selection/distance.element.css | 3 + app/components/selection/distance.element.js | 14 +- app/components/selection/handle.element.css | 1 + app/components/selection/handle.element.js | 78 +++++----- app/components/selection/handles.element.css | 2 +- app/components/selection/handles.element.js | 71 ++++----- app/components/selection/hover.element.css | 3 +- app/components/selection/hover.element.js | 12 +- app/components/selection/quad.js | 60 ++++++++ app/components/selection/quad.test.js | 56 ++++++++ package-lock.json | 9 +- package.json | 2 +- 15 files changed, 266 insertions(+), 194 deletions(-) create mode 100644 app/components/selection/quad.js create mode 100644 app/components/selection/quad.test.js diff --git a/app/components/index.js b/app/components/index.js index 88f15f72..6c7396f3 100644 --- a/app/components/index.js +++ b/app/components/index.js @@ -1,5 +1,7 @@ -import { addPolyfill } from "get-box-quads-polyfill"; -addPolyfill(window); +import { addPolyfill } from 'get-box-quads-polyfill' + +// The polyfill preserves native GeometryUtils methods and only fills in missing ones. +addPolyfill(window) export { Handles } from './selection/handles.element' export { Handle } from './selection/handle.element' diff --git a/app/components/selection/box-model.element.css b/app/components/selection/box-model.element.css index 2e2e854a..fc664d37 100644 --- a/app/components/selection/box-model.element.css +++ b/app/components/selection/box-model.element.css @@ -1,5 +1,13 @@ :host [mask] { + inset: 0; + height: 100%; pointer-events: none; position: absolute; + width: 100%; z-index: var(--layer-5); } + +:host svg { + display: block; + overflow: visible; +} diff --git a/app/components/selection/box-model.element.js b/app/components/selection/box-model.element.js index 2378d147..89182149 100644 --- a/app/components/selection/box-model.element.js +++ b/app/components/selection/box-model.element.js @@ -1,4 +1,5 @@ import { BoxModelStyles } from '../styles.store' +import { getBoxQuad, quadBounds, quadPath, sideMidpoint } from './quad' export class BoxModel extends HTMLElement { @@ -16,43 +17,44 @@ export class BoxModel extends HTMLElement { set position(payload) { this.$shadow.innerHTML = this.render(payload) - if (!this.drawable.measurements) // && payload.color === 'pink' - this.createMeasurements(payload) + this.createMeasurements({...payload, ...this.drawable.measurementQuads}) } render({mode, bounds, sides, color = 'pink', element}) { const total_height = bounds.height + sides.bottom + sides.top const total_width = bounds.width + sides.right + sides.left - - const q1 = element.getBoxQuads({ box: 'border' })[0]; - const left = Math.min(q1.p1.x, q1.p2.x, q1.p3.x, q1.p4.x); - const top = Math.min(q1.p1.y, q1.p2.y, q1.p3.y, q1.p4.y); + const borderQuad = getBoxQuad(element) + const borderBounds = quadBounds(borderQuad) + const origin = {x: borderBounds.left, y: borderBounds.top} + let outerQuad + let innerQuad if (mode === 'padding') { - const q2 = element.getBoxQuads({ box: 'content' })[0]; + outerQuad = getBoxQuad(element, 'padding') + innerQuad = getBoxQuad(element, 'content') this.drawable = { height: bounds.height - (sides.borders.top + sides.borders.bottom), width: bounds.width - (sides.borders.right + sides.borders.left), top: 0 + sides.borders.top, left: 0 + sides.borders.left, rotation: 'rotate(-45)', - d: "M" + [q1.p1, q1.p2, q1.p3, q1.p4].map(x => (x.x - left) + ',' + (x.y - top)).join(' ') + 'Z ' - + "M" + [q2.p1, q2.p2, q2.p3, q2.p4].map(x => (x.x - left) + ',' + (x.y - top)).join(' ') + 'Z' } } else if (mode === 'margin') { - const q2 = element.getBoxQuads({ box: 'margin' })[0]; + outerQuad = getBoxQuad(element, 'margin') + innerQuad = borderQuad this.drawable = { height: total_height, width: total_width, top: 0 - sides.top, left: 0 - sides.left, rotation: 'rotate(45)', - d: "M" + [q1.p1, q1.p2, q1.p3, q1.p4].map(x => (x.x - left) + ',' + (x.y - top)).join(' ') + 'Z ' - + "M" + [q2.p1, q2.p2, q2.p3, q2.p4].map(x => (x.x - left) + ',' + (x.y - top)).join(' ') + 'Z' } } + this.drawable.d = `${quadPath(outerQuad, origin)} ${quadPath(innerQuad, origin)}` + this.drawable.measurementQuads = {outerQuad, innerQuad, origin} + if (color === 'pink') { this.drawable.bg = 'color(display-p3 1 0 1 / 15%)' this.drawable.stripe = 'color(display-p3 1 0 1 / 80%)' @@ -95,94 +97,27 @@ export class BoxModel extends HTMLElement { this.style.setProperty('--offset-bottom', `${this.drawable.height - sides.bottom}px`) } - createMeasurements({mode, bounds, sides, color}) { - const win_width = window.innerWidth - const pill_height = 18 - const offset = 3 - - if (mode === 'margin') { - if (sides.top) { - this.createMeasurement({ - x: (bounds.width / 2) - offset, - y: (window.scrollY * -1) - sides.top, - d: sides.top, - q: 'top', - v: true, - color, - }) - } - if (sides.bottom) { - this.createMeasurement({ - x: (bounds.width / 2) - offset, - y: (window.scrollY * -1) + bounds.height, - d: sides.bottom, - q: 'bottom', - v: true, - color, - }) - } - if (sides.right) { - this.createMeasurement({ - x: bounds.width, - y: (window.scrollY * -1) + (bounds.height / 2) - offset, - d: sides.right, - q: 'right', - v: false, - color, - }) - } - if (sides.left) { - this.createMeasurement({ - x: sides.left * -1, - y: (window.scrollY * -1) + (bounds.height / 2) - offset, - d: sides.left, - q: 'left', - v: false, - color, - }) - } - } - else if (mode === 'padding') { - if (sides.top) { - this.createMeasurement({ - x: (bounds.width / 2) - offset, - y: (window.scrollY * -1) + sides.borders.top, - d: sides.top, - q: 'top', - v: true, - color, - }) - } - if (sides.bottom) { - this.createMeasurement({ - x: (bounds.width / 2) - offset, - y: (window.scrollY * -1) + (bounds.height - sides.bottom - sides.borders.bottom), - d: sides.bottom, - q: 'bottom', - v: true, - color, - }) - } - if (sides.right) { - this.createMeasurement({ - x: bounds.width - sides.right - sides.borders.right, - y: (window.scrollY * -1) + (bounds.height / 2) - offset, - d: sides.right, - q: 'right', - v: false, - color, - }) - } - if (sides.left) { - this.createMeasurement({ - x: 0 + sides.borders.left, - y: (window.scrollY * -1) + (bounds.height / 2) - offset, - d: sides.left, - q: 'left', - v: false, - color, - }) - } + createMeasurements({sides, color, outerQuad, innerQuad, origin}) { + for (const side of ['top', 'right', 'bottom', 'left']) { + if (!sides[side]) continue + + const start = sideMidpoint(outerQuad, side) + const end = sideMidpoint(innerQuad, side) + const dx = end.x - start.x + const dy = end.y - start.y + + this.createMeasurement({ + x: start.x - origin.x, + y: start.y - origin.y, + d: sides[side], + length: Math.hypot(dx, dy), + angle: Math.atan2(dy, dx), + q: side, + v: false, + local: true, + centered: true, + color, + }) } } diff --git a/app/components/selection/distance.element.css b/app/components/selection/distance.element.css index 5c0d959c..f89f7013 100644 --- a/app/components/selection/distance.element.css +++ b/app/components/selection/distance.element.css @@ -30,6 +30,8 @@ align-items: center; justify-content: var(--justify, 'flex-start'); flex-direction: var(--direction); + transform: rotate(var(--angle, 0rad)); + transform-origin: top left; } :host > figure figcaption { @@ -49,6 +51,7 @@ line-height: 1.1; font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif; padding: 0 1ex; + transform: rotate(var(--caption-angle, 0rad)); } :host > figure span { diff --git a/app/components/selection/distance.element.js b/app/components/selection/distance.element.js index 36a68e89..81d95200 100644 --- a/app/components/selection/distance.element.js +++ b/app/components/selection/distance.element.js @@ -21,19 +21,23 @@ export class Distance extends HTMLElement { this.$shadow.innerHTML = this.render(line_model, node_label_id) } - set styleProps({y,x,d,q,v = false, color}) { - this.style.setProperty('--top', `${Math.round(y + window.scrollY)}px`) + set styleProps({y,x,d,q,v = false, color, local = false, length = d, angle = 0, centered = false}) { + this.style.setProperty('--top', `${Math.round(y + (local ? 0 : window.scrollY))}px`) this.style.setProperty('--right', 'auto') this.style.setProperty('--left', `${x}px`) this.style.setProperty('--direction', v ? 'column' : 'row') this.style.setProperty('--quadrant', q) + this.style.setProperty('--angle', `${angle}rad`) + this.style.setProperty('--caption-angle', `${angle * -1}rad`) - if (q === 'left') + if (centered) + this.style.setProperty('--justify', 'center') + else if (q === 'left') this.style.setProperty('--justify', 'flex-end') v - ? this.style.setProperty('--distance-h', `${d}px`) - : this.style.setProperty('--distance-w', `${d}px`) + ? this.style.setProperty('--distance-h', `${length}px`) + : this.style.setProperty('--distance-w', `${length}px`) v ? this.style.setProperty('--line-h', `var(--line-w)`) diff --git a/app/components/selection/handle.element.css b/app/components/selection/handle.element.css index 4e877a56..7c039f6b 100644 --- a/app/components/selection/handle.element.css +++ b/app/components/selection/handle.element.css @@ -4,6 +4,7 @@ display: grid; grid-area: 1 / -1; place-self: var(--align-self, center) var(--justify-self, center); + position: absolute; transform: translate(var(--translate-x, 0), var(--translate-y, 0)); } diff --git a/app/components/selection/handle.element.js b/app/components/selection/handle.element.js index 04127abb..607b9710 100644 --- a/app/components/selection/handle.element.js +++ b/app/components/selection/handle.element.js @@ -1,6 +1,5 @@ import $ from 'blingblingjs' import { HandleStyles } from '../styles.store' -import { clamp } from '../../utilities/numbers' export class Handle extends HTMLElement { @@ -8,6 +7,7 @@ export class Handle extends HTMLElement { super() this.$shadow = this.attachShadow({mode: 'closed'}) this.styles = [HandleStyles] + this.on_resize_start = this.on_element_resize_start.bind(this) } connectedCallback() { @@ -15,7 +15,7 @@ export class Handle extends HTMLElement { this.$shadow.innerHTML = this.render() this.button = this.$shadow.querySelector('button') - this.button.addEventListener('pointerdown', this.on_element_resize_start.bind(this)) + this.button.addEventListener('pointerdown', this.on_resize_start) this.placement = this.getAttribute('placement') } @@ -47,7 +47,7 @@ export class Handle extends HTMLElement { if (!sourceEl) return - const t = sourceEl.convertPointFromNode(e, document.body.parentElement); + const t = sourceEl.convertPointFromNode(e, document.documentElement) const { x: initialX, y: initialY } = t const initialStyle = getComputedStyle(sourceEl) @@ -68,94 +68,82 @@ export class Handle extends HTMLElement { e.preventDefault() e.stopPropagation() - const t = sourceEl.convertPointFromNode({ x: e.clientX, y: e.clientY }, document.body.parentElement); - - const newX = clamp(0, t.x, document.documentElement.clientWidth) - const newY = clamp(0, t.y, document.documentElement.clientHeight) + const {x: newX, y: newY} = sourceEl.convertPointFromNode(e, document.documentElement) const diffX = newX - initialX const diffY = newY - initialY + const leftWidth = Math.max(0, initialWidth - diffX) + const rightWidth = Math.max(0, initialWidth + diffX) + const topHeight = Math.max(0, initialHeight - diffY) + const bottomHeight = Math.max(0, initialHeight + diffY) + const leftShift = initialWidth - leftWidth + const topShift = initialHeight - topHeight switch (placement) { case 'top-start': { - const newWidth = initialWidth - diffX - const newHeight = initialHeight - diffY - const newTranslate = initialTransform.translate(diffX, diffY).transformPoint() + const newTransform = initialTransform.translate(leftShift, topShift) requestAnimationFrame(() => { - sourceEl.style.width = `${newWidth}px` - sourceEl.style.height = `${newHeight}px` - sourceEl.style.transform = `translate(${newTranslate.x}px, ${newTranslate.y}px)` + sourceEl.style.width = `${leftWidth}px` + sourceEl.style.height = `${topHeight}px` + sourceEl.style.transform = newTransform.toString() }) break } case 'top-center': { - const newHeight = initialHeight - diffY - const newTranslate = initialTransform.translate(0, diffY).transformPoint() + const newTransform = initialTransform.translate(0, topShift) requestAnimationFrame(() => { - sourceEl.style.height = `${newHeight}px` - sourceEl.style.transform = `translate(${newTranslate.x}px, ${newTranslate.y}px)` + sourceEl.style.height = `${topHeight}px` + sourceEl.style.transform = newTransform.toString() }) break } case 'top-end': { - const newWidth = initialWidth + diffX - const newHeight = initialHeight - diffY - const newTranslate = initialTransform.translate(0, diffY).transformPoint() + const newTransform = initialTransform.translate(0, topShift) requestAnimationFrame(() => { - sourceEl.style.width = `${newWidth}px` - sourceEl.style.height = `${newHeight}px` - sourceEl.style.transform = `translate(${newTranslate.x}px, ${newTranslate.y}px)` + sourceEl.style.width = `${rightWidth}px` + sourceEl.style.height = `${topHeight}px` + sourceEl.style.transform = newTransform.toString() }) break } case 'middle-start': { - const newWidth = initialWidth - diffX - const newTranslate = initialTransform.translate(diffX).transformPoint() + const newTransform = initialTransform.translate(leftShift) requestAnimationFrame(() => { - sourceEl.style.width = `${newWidth}px` - sourceEl.style.transform = `translate(${newTranslate.x}px, ${newTranslate.y}px)` + sourceEl.style.width = `${leftWidth}px` + sourceEl.style.transform = newTransform.toString() }) break } case 'middle-end': { - const newWidth = initialWidth + diffX - requestAnimationFrame(() => { - sourceEl.style.width = `${newWidth}px` + sourceEl.style.width = `${rightWidth}px` }) break } case 'bottom-start': { - const newWidth = initialWidth - diffX - const newHeight = initialHeight + diffY - const newTranslate = initialTransform.translate(diffX, 0).transformPoint() + const newTransform = initialTransform.translate(leftShift, 0) requestAnimationFrame(() => { - sourceEl.style.width = `${newWidth}px` - sourceEl.style.height = `${newHeight}px` - sourceEl.style.transform = `translate(${newTranslate.x}px, ${newTranslate.y}px)` + sourceEl.style.width = `${leftWidth}px` + sourceEl.style.height = `${bottomHeight}px` + sourceEl.style.transform = newTransform.toString() }) break } case 'bottom-center': { - const newHeight = initialHeight + diffY - requestAnimationFrame(() => { - sourceEl.style.height = `${newHeight}px` + sourceEl.style.height = `${bottomHeight}px` }) break } case 'bottom-end': { - const newWidth = initialWidth + diffX - const newHeight = initialHeight + diffY - requestAnimationFrame(() => { - sourceEl.style.width = `${newWidth}px` - sourceEl.style.height = `${newHeight}px` + sourceEl.style.width = `${rightWidth}px` + sourceEl.style.height = `${bottomHeight}px` }) break } @@ -174,7 +162,7 @@ export class Handle extends HTMLElement { } disconnectedCallback() { - this.button.removeEventListener('pointerdown', this.on_element_resize_start.bind(this)) + this.button && this.button.removeEventListener('pointerdown', this.on_resize_start) } render() { diff --git a/app/components/selection/handles.element.css b/app/components/selection/handles.element.css index 25085966..575c5d65 100644 --- a/app/components/selection/handles.element.css +++ b/app/components/selection/handles.element.css @@ -1,7 +1,7 @@ @import "../_variables.css"; :host { - position: var(--position, 'absolute'); + position: var(--position, absolute); inset: var(--top) auto auto var(--left); background: transparent; border: none; diff --git a/app/components/selection/handles.element.js b/app/components/selection/handles.element.js index 0e2c5916..d7a890c7 100644 --- a/app/components/selection/handles.element.js +++ b/app/components/selection/handles.element.js @@ -1,6 +1,6 @@ -import $ from 'blingblingjs' import { HandlesStyles } from '../styles.store' -import { isFixed } from '../../utilities/'; +import { isFixed } from '../../utilities/' +import { getBoxQuad, quadBounds, quadPath, sideMidpoint } from './quad' export class Handles extends HTMLElement { @@ -8,39 +8,43 @@ export class Handles extends HTMLElement { super() this.$shadow = this.attachShadow({mode: 'closed'}) this.styles = [HandlesStyles] - this.on_resize = this.on_window_resize.bind(this) + this.on_position_change = this.on_position_change.bind(this) + this.position_frame = null + this.source_el = null } connectedCallback() { this.$shadow.adoptedStyleSheets = this.styles this.setAttribute('popover', 'manual') this.showPopover && this.showPopover() - window.addEventListener('resize', this.on_window_resize) + window.addEventListener('resize', this.on_position_change) + window.addEventListener('scroll', this.on_position_change, true) } disconnectedCallback() { - if (this.hidePopover && this.hidePopover()) this.hidePopover && this.hidePopover() - window.removeEventListener('resize', this.on_window_resize) + this.hidePopover && this.hidePopover() + window.removeEventListener('resize', this.on_position_change) + window.removeEventListener('scroll', this.on_position_change, true) + this.position_frame && window.cancelAnimationFrame(this.position_frame) } - on_window_resize() { - if (!this.$shadow) return - window.requestAnimationFrame(() => { - const node_label_id = this.$shadow.host.getAttribute('data-label-id') - const [source_el] = $(`[data-label-id="${node_label_id}"]`) + on_position_change() { + if (this.position_frame || !this.source_el) return - if (!source_el) return + this.position_frame = window.requestAnimationFrame(() => { + this.position_frame = null + if (!this.source_el.isConnected) return this.position = { - node_label_id, - el: source_el, - isFixed: isFixed(source_el), + node_label_id: this.getAttribute('data-label-id'), + el: this.source_el, } }) } set position({el, node_label_id}) { - this.$shadow.innerHTML = this.render(el.getBoxQuads()[0], node_label_id, isFixed(el)) + this.source_el = el + this.$shadow.innerHTML = this.render(getBoxQuad(el), node_label_id, isFixed(el)) if (this._backdrop) { this.backdrop = { @@ -70,15 +74,21 @@ export class Handles extends HTMLElement { render(quad, node_label_id, isFixed) { this.$shadow.host.setAttribute('data-label-id', node_label_id) - const left = Math.min(quad.p1.x, quad.p2.x, quad.p3.x, quad.p4.x); - const right = Math.max(quad.p1.x, quad.p2.x, quad.p3.x, quad.p4.x); - const top = Math.min(quad.p1.y, quad.p2.y, quad.p3.y, quad.p4.y); - const bottom = Math.max(quad.p1.y, quad.p2.y, quad.p3.y, quad.p4.y); - const width = right - left; - const height = bottom - top; + const {left, top, width, height} = quadBounds(quad) + const origin = {x: left, y: top} + const positions = { + 'top-start': quad.p1, + 'top-center': sideMidpoint(quad, 'top'), + 'top-end': quad.p2, + 'middle-start': sideMidpoint(quad, 'left'), + 'middle-end': sideMidpoint(quad, 'right'), + 'bottom-start': quad.p4, + 'bottom-center': sideMidpoint(quad, 'bottom'), + 'bottom-end': quad.p3, + } this.style.setProperty('--top', `${top + (isFixed ? 0 : window.scrollY)}px`) - this.style.setProperty('--left', `${left}px`) + this.style.setProperty('--left', `${left + (isFixed ? 0 : window.scrollX)}px`) this.style.setProperty('--position', isFixed ? 'fixed' : 'absolute') this.style.setProperty('--width', `${width}px`) this.style.setProperty('--height', `${height}px`) @@ -90,16 +100,13 @@ export class Handles extends HTMLElement { viewBox="0 0 ${width} ${height}" version="1.1" xmlns="http://www.w3.org/2000/svg" > - + - - - - - - - - + ${Object.entries(positions).map(([placement, point]) => ` + `).join('')} ` } } diff --git a/app/components/selection/hover.element.css b/app/components/selection/hover.element.css index 82f824d9..24d16609 100644 --- a/app/components/selection/hover.element.css +++ b/app/components/selection/hover.element.css @@ -1,6 +1,7 @@ @import "../_variables.css"; -:host rect { +:host rect, +:host path { width: 100%; height: 100%; vector-effect: non-scaling-stroke; diff --git a/app/components/selection/hover.element.js b/app/components/selection/hover.element.js index ee4175de..412b6373 100644 --- a/app/components/selection/hover.element.js +++ b/app/components/selection/hover.element.js @@ -1,5 +1,6 @@ import { Handles } from './handles.element' import { HandlesStyles, HoverStyles } from '../styles.store' +import { quadBounds, quadPath } from './quad' export class Hover extends Handles { @@ -14,17 +15,22 @@ export class Hover extends Handles { disconnectedCallback() {} - render({ width, height, top, left }, node_label_id, isFixed) { + render(quad, node_label_id, isFixed) { + const {width, height, top, left} = quadBounds(quad) + this.style.setProperty('--top', `${top + (isFixed ? 0 : window.scrollY)}px`) - this.style.setProperty('--left', `${left}px`) + this.style.setProperty('--left', `${left + (isFixed ? 0 : window.scrollX)}px`) this.style.setProperty('--position', isFixed ? 'fixed' : 'absolute') + this.style.setProperty('--width', `${width}px`) + this.style.setProperty('--height', `${height}px`) return ` - + + ` } } diff --git a/app/components/selection/quad.js b/app/components/selection/quad.js new file mode 100644 index 00000000..01b94033 --- /dev/null +++ b/app/components/selection/quad.js @@ -0,0 +1,60 @@ +import { getBoxQuads as getPolyfillBoxQuads } from 'get-box-quads-polyfill' + +export const quadBounds = quad => { + const points = [quad.p1, quad.p2, quad.p3, quad.p4] + const left = Math.min(...points.map(point => point.x)) + const right = Math.max(...points.map(point => point.x)) + const top = Math.min(...points.map(point => point.y)) + const bottom = Math.max(...points.map(point => point.y)) + + return { + left, + right, + top, + bottom, + width: right - left, + height: bottom - top, + } +} + +export const quadPath = (quad, origin = {x: 0, y: 0}) => + `M${[quad.p1, quad.p2, quad.p3, quad.p4] + .map(point => `${point.x - origin.x},${point.y - origin.y}`) + .join(' ')}Z` + +export const sideMidpoint = (quad, side) => { + const [start, end] = { + top: [quad.p1, quad.p2], + right: [quad.p2, quad.p3], + bottom: [quad.p4, quad.p3], + left: [quad.p1, quad.p4], + }[side] + + return { + x: start.x + ((end.x - start.x) / 2), + y: start.y + ((end.y - start.y) / 2), + } +} + +const rectToQuad = ({left, right, top, bottom}) => ({ + p1: {x: left, y: top}, + p2: {x: right, y: top}, + p3: {x: right, y: bottom}, + p4: {x: left, y: bottom}, +}) + +export const getBoxQuad = (element, box = 'border') => { + try { + const [quad] = element.getBoxQuads({box}) + if (quad) return quad + } + catch {} + + try { + const [quad] = getPolyfillBoxQuads(element, {box}) + if (quad) return quad + } + catch {} + + return rectToQuad(element.getBoundingClientRect()) +} diff --git a/app/components/selection/quad.test.js b/app/components/selection/quad.test.js new file mode 100644 index 00000000..f68da220 --- /dev/null +++ b/app/components/selection/quad.test.js @@ -0,0 +1,56 @@ +import test from 'ava' +import { getBoxQuad, quadBounds, quadPath, sideMidpoint } from './quad' + +const quad = { + p1: {x: 10, y: 20}, + p2: {x: 50, y: 10}, + p3: {x: 60, y: 40}, + p4: {x: 20, y: 50}, +} + +test('calculates the axis-aligned bounds of a transformed quad', t => { + t.deepEqual(quadBounds(quad), { + left: 10, + right: 60, + top: 10, + bottom: 50, + width: 50, + height: 40, + }) +}) + +test('creates a local SVG path for a transformed quad', t => { + t.is(quadPath(quad, {x: 10, y: 10}), 'M0,10 40,0 50,30 10,40Z') +}) + +test('calculates transformed side midpoints', t => { + t.deepEqual(sideMidpoint(quad, 'top'), {x: 30, y: 15}) + t.deepEqual(sideMidpoint(quad, 'right'), {x: 55, y: 25}) + t.deepEqual(sideMidpoint(quad, 'bottom'), {x: 40, y: 45}) + t.deepEqual(sideMidpoint(quad, 'left'), {x: 15, y: 35}) +}) + +test('uses getBoxQuads when the element provides a quad', t => { + const element = { + getBoxQuads: options => { + t.deepEqual(options, {box: 'padding'}) + return [quad] + }, + } + + t.is(getBoxQuad(element, 'padding'), quad) +}) + +test('falls back to getBoundingClientRect for an empty quad list', t => { + const element = { + getBoxQuads: () => [], + getBoundingClientRect: () => ({left: 1, right: 5, top: 2, bottom: 8}), + } + + t.deepEqual(getBoxQuad(element), { + p1: {x: 1, y: 2}, + p2: {x: 5, y: 2}, + p3: {x: 5, y: 8}, + p4: {x: 1, y: 8}, + }) +}) diff --git a/package-lock.json b/package-lock.json index c48cc616..3a01fd51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "blingblingjs": "^2.3.0", "colorjs.io": "^0.5.0", "construct-style-sheets-polyfill": "^2.4.2", - "get-box-quads-polyfill": "^4.16.0", + "get-box-quads-polyfill": "^4.47.0", "hotkeys-js": "^3.13.7", "query-selector-shadow-dom": "^1.0.1" }, @@ -8236,9 +8236,10 @@ } }, "node_modules/get-box-quads-polyfill": { - "version": "4.16.0", - "resolved": "https://registry.npmjs.org/get-box-quads-polyfill/-/get-box-quads-polyfill-4.16.0.tgz", - "integrity": "sha512-8kxOfwcV+EQH3MBgdpTjCRDDaN8y+kphUrjFXDlqsTAVaD0WbTxf0mz+n+UEoSM2MlpbIPd6yW9QWIgYvMbbIg==" + "version": "4.47.0", + "resolved": "https://registry.npmjs.org/get-box-quads-polyfill/-/get-box-quads-polyfill-4.47.0.tgz", + "integrity": "sha512-e/zXXHWm0wYuizK9XsWHOjyxdrPNwv0qIkMAaJIOQ1eovlGWecjHGlNAjkLO4cWlABZdVN6XC3OncaRgoY/ong==", + "license": "MIT" }, "node_modules/get-caller-file": { "version": "2.0.5", diff --git a/package.json b/package.json index 92a2286d..fce3bfcc 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "blingblingjs": "^2.3.0", "colorjs.io": "^0.5.0", "construct-style-sheets-polyfill": "^2.4.2", - "get-box-quads-polyfill": "^4.16.0", + "get-box-quads-polyfill": "^4.47.0", "hotkeys-js": "^3.13.7", "query-selector-shadow-dom": "^1.0.1" },