radial space fixes

This commit is contained in:
Orion Reed 2024-12-14 04:37:31 -05:00
parent ffbcde33de
commit 1e0868bddc
4 changed files with 157 additions and 79 deletions

View File

@ -19,23 +19,22 @@
} }
folk-shape { folk-shape {
background: rgb(187, 178, 178); background: rgb(189, 184, 181);
} }
folk-space-radial { folk-space-radial {
width: 500px; width: 700px;
height: 500px; height: 700px;
} }
</style> </style>
</head> </head>
<body> <body>
<folk-space-radial> <folk-shape id="0" x="0" y="100" width="50" height="50"></folk-shape>
<folk-shape x="0" y="100" width="50" height="50"></folk-shape> <folk-shape id="1" x="0" y="200" width="50" height="50"></folk-shape>
<folk-shape x="0" y="200" width="50" height="50"></folk-shape> <folk-shape id="2" x="20" y="300" width="50" height="50" rotation="45"></folk-shape>
<folk-shape x="20" y="300" width="50" height="50" rotation="45"></folk-shape> <folk-shape id="3" x="20" y="400" width="50" height="50" rotation="45"></folk-shape>
<folk-shape x="20" y="400" width="50" height="50" rotation="45"></folk-shape> <folk-shape id="4" x="20" y="500" width="50" height="50" rotation="45"></folk-shape>
<folk-shape x="20" y="500" width="50" height="50" rotation="45"></folk-shape> <folk-space-radial sources="folk-shape"> </folk-space-radial>
</folk-space-radial>
<script type="module"> <script type="module">
import '../src/standalone/folk-shape.ts'; import '../src/standalone/folk-shape.ts';

View File

@ -0,0 +1,10 @@
export class Experimental {
static canMoveBefore() {
const enabled = !!(Element.prototype as any).moveBefore;
if (!enabled) {
console.warn('moveBefore() API requires Chrome Canary with chrome://flags/#atomic-move enabled');
alert('moveBefore() API requires Chrome Canary with chrome://flags/#atomic-move enabled');
}
return enabled;
}
}

View File

@ -315,6 +315,7 @@ export class FolkShape extends HTMLElement {
this.#update(); this.#update();
} }
// todo: rename to `getDOMRectTransform`
getTransformDOMRect() { getTransformDOMRect() {
return this.#readonlyRect; return this.#readonlyRect;
} }
@ -471,9 +472,7 @@ export class FolkShape extends HTMLElement {
} }
#dispatchTransformEvent() { #dispatchTransformEvent() {
this.#readonlyRect = new DOMRectTransformReadonly(this.#rect); const event = new TransformEvent(this.#rect, this.#previousRect);
const event = new TransformEvent(this.#readonlyRect, this.#previousRect);
this.dispatchEvent(event); this.dispatchEvent(event);
if (event.xPrevented) { if (event.xPrevented) {

View File

@ -1,91 +1,161 @@
import { css, html } from './common/tags';
import { FolkShape } from './folk-shape';
import { DOMRectTransform } from './common/DOMRectTransform'; import { DOMRectTransform } from './common/DOMRectTransform';
import { css, type PropertyValues } from '@lit/reactive-element';
import { TransformEvent } from './common/TransformEvent';
import { FolkShape } from './folk-shape';
import { Experimental } from './common/Experimental';
import { FolkBaseSet } from './folk-base-set';
import { Vector } from './common/Vector';
import type { Point } from './common/types';
const styles = css` export class FolkSpaceRadial extends FolkBaseSet {
:host { static override tagName = 'folk-space-radial';
display: block; static override styles = [
position: relative; FolkBaseSet.styles,
border: 2px dashed hsl(214, 84%, 56%); css`
border-radius: 50%; :host {
border: 2px dashed hsl(214, 84%, 56%);
border-radius: 50%;
box-sizing: border-box;
}
.center-point {
position: absolute;
width: 8px;
height: 8px;
background: hsl(214, 84%, 56%);
border-radius: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
left: 50%;
top: 50%;
}
`,
];
#centerPoint: HTMLDivElement | null = null;
override firstUpdated(changedProperties: PropertyValues<this>) {
super.firstUpdated(changedProperties);
this.#centerPoint = document.createElement('div');
this.#centerPoint.className = 'center-point';
this.renderRoot.appendChild(this.#centerPoint);
// Add transform listeners to source elements
this.sourceElements.forEach((element) => {
if (element instanceof FolkShape) {
element.addEventListener('transform', this.#onTransform);
}
});
} }
::slotted(*) { protected override updated(changedProperties: PropertyValues<this>) {
position: absolute; super.updated(changedProperties);
transform-origin: 50% 0%;
// Update transform listeners when source elements change
this.sourceElements.forEach((element) => {
if (element instanceof FolkShape) {
element.addEventListener('transform', (event) => {
this.#onTransform(event);
// this.#handleMoveBefore(event);
});
}
});
} }
.center-point { #handleMoveBefore(event: Event) {
position: absolute; if (!Experimental.canMoveBefore()) return;
width: 8px;
height: 8px;
background: hsl(214, 84%, 56%);
border-radius: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
}
`;
export class FolkSpaceRadial extends HTMLElement { const shapeElement = event.target as HTMLElement;
static tagName = 'folk-space-radial';
static define() { // Calculate the center of the shape
if (!customElements.get(this.tagName)) { const shapeBounds = shapeElement.getBoundingClientRect();
customElements.define(this.tagName, this); const shapeCenter = {
x: shapeBounds.left + shapeBounds.width / 2,
y: shapeBounds.top + shapeBounds.height / 2,
};
const bounds = this.getBoundingClientRect();
const spaceCenter = { x: bounds.left + bounds.width / 2, y: bounds.top + bounds.height / 2 };
// Calculate distance from shape center to circle center
const distance = Vector.distance(shapeCenter, spaceCenter);
const circleRadius = bounds.width / 2;
const isInsideCircle = distance <= circleRadius;
const isInHost = shapeElement.parentElement === this;
if (isInsideCircle && !isInHost) {
(this as any).moveBefore(shapeElement, null);
// this.#ignoredShapes.delete(shapeElement);
} else if (!isInsideCircle && isInHost) {
(document.body as any).moveBefore(shapeElement, null);
// this.#ignoredShapes.add(shapeElement);
} }
} }
#shadow = this.attachShadow({ mode: 'open' }); #onTransform = (event: Event) => {
#centerPoint: HTMLDivElement; if (!(event instanceof TransformEvent)) return;
constructor() { const transform = event.current as DOMRectTransform;
super();
this.#shadow.adoptedStyleSheets = [styles];
// Create center point marker // Set rotateOrigin to center
this.#centerPoint = document.createElement('div'); transform.rotateOrigin = { x: 0.5, y: 0.5 };
this.#centerPoint.className = 'center-point';
this.#shadow.setHTMLUnsafe(html`<slot></slot> `); // Get the center of the radial space
this.#shadow.appendChild(this.#centerPoint); const bounds = this.getBoundingClientRect();
const spaceCenter = { x: bounds.left + bounds.width / 2, y: bounds.top + bounds.height / 2 };
// Listen for changes in the slot to layout children when they change // Calculate the absolute position of the rotateOrigin in local space
const slot = this.#shadow.querySelector('slot'); const rotateOriginLocal = {
slot?.addEventListener('slotchange', () => this.#layoutChildren()); x: transform.width * transform.rotateOrigin.x,
} y: transform.height * transform.rotateOrigin.y,
};
connectedCallback() { // Convert the local rotateOrigin to parent space
this.#layoutChildren(); const rotateOriginParent = transform.toParentSpace(rotateOriginLocal);
}
#layoutChildren() { const distance = Vector.distance(rotateOriginParent, spaceCenter);
const slot = this.#shadow.querySelector('slot');
const assignedElements = slot?.assignedElements() || [];
const count = assignedElements.length;
// Determine the radius and center of the radial layout // if the shape is outside the circle, don't move it
const radius = Math.min(this.clientWidth, this.clientHeight) / 2 - 50; // tried using moveBefore, but will leave this here for now
const centerX = this.clientWidth / 2; if (distance > bounds.width / 2) {
const centerY = this.clientHeight / 2; return;
}
assignedElements.forEach((element, index) => { // Compute vector from space center to rotateOrigin in parent space
if (!(element instanceof FolkShape)) return; const dx = rotateOriginParent.x - spaceCenter.x;
const dy = rotateOriginParent.y - spaceCenter.y;
// Calculate the angle for each child // Calculate radius and angle
const angle = (index / count) * 2 * Math.PI; const radius = Math.sqrt(dx * dx + dy * dy);
const angle = Math.atan2(dy, dx);
// Create a transform for each child // Update position so that rotateOrigin moves along the circle
const transform = new DOMRectTransform({ const newRotateOriginParent = {
x: centerX + radius * Math.cos(angle), x: spaceCenter.x + radius * Math.cos(angle),
y: centerY + radius * Math.sin(angle), y: spaceCenter.y + radius * Math.sin(angle),
rotation: angle, };
transformOrigin: { x: 0.5, y: 0 },
});
// Set the transform on the child // Calculate the delta to move the shape so that its rotateOrigin is at the new position
// element.setTransform(transform); const deltaX = newRotateOriginParent.x - rotateOriginParent.x;
const deltaY = newRotateOriginParent.y - rotateOriginParent.y;
// Update transform position
transform.x += deltaX;
transform.y += deltaY;
// Update rotation
transform.rotation = angle;
};
override disconnectedCallback() {
super.disconnectedCallback();
// Clean up event listeners
this.sourceElements.forEach((element) => {
if (element instanceof FolkShape) {
element.removeEventListener('transform', this.#onTransform);
}
}); });
} }
} }
FolkSpaceRadial.define();