From 7f61e530042148510fbd0d984d7cdcd34bb0eea7 Mon Sep 17 00:00:00 2001 From: Orion Reed Date: Fri, 20 Dec 2024 11:35:27 -0500 Subject: [PATCH] gizmos --- labs/folk-shape.ts | 5 +- labs/folk-space.ts | 18 +-- labs/folk-transformed-space.ts | 96 +++++++++++-- lib/folk-gizmos.ts | 211 ++++++++++++++++++++++++++++ lib/utils.ts | 8 +- website/canvas/space-transform.html | 2 + 6 files changed, 304 insertions(+), 36 deletions(-) create mode 100644 lib/folk-gizmos.ts diff --git a/labs/folk-shape.ts b/labs/folk-shape.ts index 46f1bd7..c81981e 100644 --- a/labs/folk-shape.ts +++ b/labs/folk-shape.ts @@ -2,6 +2,7 @@ import { getResizeCursorUrl, getRotateCursorUrl } from '@labs/utils/cursors'; import { DOMRectTransform, DOMRectTransformReadonly, FolkElement, Point, TransformEvent, Vector } from '@lib'; import { ResizeManager } from '@lib/resize-manger'; import { html } from '@lib/tags'; +import { MAX_Z_INDEX } from '@lib/utils'; import { css, PropertyValues } from '@lit/reactive-element'; const resizeManager = new ResizeManager(); @@ -70,7 +71,7 @@ const styles = css` :host(:focus-within), :host(:focus-visible) { - z-index: calc(infinity - 1); + z-index: calc(${MAX_Z_INDEX} - 1); outline: solid 1px hsl(214, 84%, 56%); } @@ -92,7 +93,7 @@ const styles = css` aspect-ratio: 1; display: none; position: absolute; - z-index: calc(infinity); + z-index: calc(${MAX_Z_INDEX} - 1); padding: 0; } diff --git a/labs/folk-space.ts b/labs/folk-space.ts index e6453c5..bba63cd 100644 --- a/labs/folk-space.ts +++ b/labs/folk-space.ts @@ -1,5 +1,4 @@ import { FolkElement } from '@lib'; -import { DOMTransform } from '@lib/DOMTransform'; import { html } from '@lib/tags'; import { Point } from '@lib/types'; import { css } from '@lit/reactive-element'; @@ -94,8 +93,6 @@ export class FolkSpace extends FolkElement { const matrix = new DOMMatrix() .translate(centerX, centerY) .multiply(new DOMMatrix([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, -1 / this.#perspective, 0, 0, 0, 1])) - .translate(-centerX, -centerY) - .translate(centerX, centerY) .multiply(face === 'front' ? this.#frontMatrix : this.#backMatrix) .translate(-centerX, -centerY); @@ -139,7 +136,6 @@ export class FolkSpace extends FolkElement { y: rect.y + rect.height / 2, }; - // Transform center point const transformedCenter = this.localToScreen(center, face); return { @@ -167,18 +163,6 @@ export class FolkSpace extends FolkElement { return this.transformRect(rect, face); } - // Fallback to getBoundingClientRect - const rect = element.getBoundingClientRect(); - const spaceRect = this.getBoundingClientRect(); - - return this.transformRect( - { - x: rect.x - spaceRect.x, - y: rect.y - spaceRect.y, - width: rect.width, - height: rect.height, - }, - face, - ); + return null; } } diff --git a/labs/folk-transformed-space.ts b/labs/folk-transformed-space.ts index 642df67..c65c729 100644 --- a/labs/folk-transformed-space.ts +++ b/labs/folk-transformed-space.ts @@ -1,15 +1,25 @@ -import { FolkElement } from '@lib'; +import { FolkElement, type Point } from '@lib'; +import { Gizmos } from '@lib/folk-gizmos'; import { html } from '@lib/tags'; import { TransformEvent } from '@lib/TransformEvent'; import { css } from '@lit/reactive-element'; +interface TransformRect { + x: number; + y: number; + width: number; + height: number; +} + export class FolkTransformedSpace extends FolkElement { static override tagName = 'folk-transformed-space'; + static #perspective = 1000; + static styles = css` :host { display: block; - // perspective: 1000px; + perspective: ${this.#perspective}px; position: relative; width: 100%; height: 100%; @@ -49,22 +59,80 @@ export class FolkTransformedSpace extends FolkElement { if (space instanceof HTMLElement) { space.style.transform = this.#matrix.toString(); } + + Gizmos.clear(); } #handleTransform = (event: TransformEvent) => { - // Extract rotation angles from the transformation matrix - const rotationX = Math.atan2(this.#matrix.m32, this.#matrix.m33); - const rotationY = Math.atan2( - -this.#matrix.m31, - Math.sqrt(this.#matrix.m32 * this.#matrix.m32 + this.#matrix.m33 * this.#matrix.m33), - ); + const previous = this.transformRect(event.previous); + const current = this.transformRect(event.current); - // Calculate projection factors for both axes - const projectionFactorY = 1 / Math.cos(rotationX); - const projectionFactorX = 1 / Math.cos(rotationY); + Gizmos.rect(event.current, { + color: 'rgba(0, 0, 255, 0.1)', + width: 2, + layer: 'default', + }); - // Apply the transformed movement with both projection factors - event.current.x *= projectionFactorX; - event.current.y *= projectionFactorY; + Gizmos.line(event.current, current, { + color: 'gray', + width: 2, + layer: 'transformed', + }); + Gizmos.point(event.current, { + color: 'blue', + size: 3, + layer: 'transformed', + }); + Gizmos.point(current, { + color: 'red', + size: 3, + layer: 'transformed', + }); + + const delta = { + x: current.x - previous.x, + y: current.y - previous.y, + }; + + event.current.x += delta.x; + event.current.y += delta.y; }; + + localToScreen(point: Point): Point { + const spaceRect = this.getBoundingClientRect(); + const centerX = spaceRect.width / 2; + const centerY = spaceRect.height / 2; + + // Use the same matrix we're using for CSS + const matrix = new DOMMatrix().translate(centerX, centerY).multiply(this.#matrix).translate(-centerX, -centerY); + + const transformedPoint = matrix.transformPoint(new DOMPoint(point.x, point.y, 0, 1)); + + const w = transformedPoint.w || 1; + return { + x: transformedPoint.x / w, + y: transformedPoint.y / w, + }; + } + + /** + * Transforms a rect from an element in either face to screen coordinates + */ + transformRect(rect: TransformRect): TransformRect { + // Get center point + const center = { + x: rect.x, + y: rect.y, + }; + + // Transform center point + const transformedCenter = this.localToScreen(center); + + return { + x: transformedCenter.x, + y: transformedCenter.y, + width: rect.width, + height: rect.height, + }; + } } diff --git a/lib/folk-gizmos.ts b/lib/folk-gizmos.ts new file mode 100644 index 0000000..25fe997 --- /dev/null +++ b/lib/folk-gizmos.ts @@ -0,0 +1,211 @@ +import { DOMRectTransform, FolkElement, type Point } from '@lib'; +import { html } from '@lib/tags'; +import { css } from '@lit/reactive-element'; + +interface GizmoOptions { + color?: string; + layer?: string; +} + +interface PointOptions extends GizmoOptions { + size?: number; +} + +interface LineOptions extends GizmoOptions { + width?: number; +} + +interface RectOptions extends LineOptions { + fill?: string; +} + +/** + * Visual debugging system that renders canvas overlays in DOM containers. + * + * Creates full-size canvas overlays that can be placed anywhere in the DOM. + * Supports multiple instances with isolated drawing layers. + * + * Usage: + * ```html + * + * ``` + * + * Drawing methods: + * ```ts + * Gizmos.point({x, y}); + * Gizmos.line(start, end, { color: 'red' }); + * Gizmos.rect(domRect, { fill: 'blue' }); + * ``` + */ +export class Gizmos extends FolkElement { + static override tagName = 'folk-gizmos'; + static #layers = new Map< + string, + { + ctx: CanvasRenderingContext2D; + canvas: HTMLCanvasElement; + } + >(); + static #defaultLayer = 'default'; + static #hasLoggedDrawWarning = false; + static #hasLoggedInitMessage = false; + + static styles = css` + :host { + display: block; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: calc(Infinity); + } + + .gizmos-canvas { + position: absolute; + width: 100%; + height: 100%; + } + `; + + readonly #layer: string; + + constructor() { + super(); + this.#layer = this.getAttribute('layer') ?? Gizmos.#defaultLayer; + } + + override createRenderRoot() { + const root = super.createRenderRoot() as ShadowRoot; + + root.setHTMLUnsafe(html` `); + + const canvas = root.querySelector('.gizmos-canvas') as HTMLCanvasElement; + const ctx = canvas?.getContext('2d') ?? null; + + if (canvas && ctx) { + Gizmos.#layers.set(this.#layer, { canvas, ctx }); + } + + this.#handleResize(); + window.addEventListener('resize', () => this.#handleResize()); + + return root; + } + + /** Draws a point */ + static point(point: Point, { color = 'red', size = 5, layer = Gizmos.#defaultLayer }: PointOptions = {}) { + const ctx = Gizmos.#getContext(layer); + if (!ctx) return; + + ctx.beginPath(); + ctx.fillStyle = color; + ctx.arc(point.x, point.y, size, 0, Math.PI * 2); + ctx.fill(); + } + + /** Draws a line between two points */ + static line(start: Point, end: Point, { color = 'blue', width = 2, layer = Gizmos.#defaultLayer }: LineOptions = {}) { + const ctx = Gizmos.#getContext(layer); + if (!ctx) return; + + ctx.beginPath(); + ctx.strokeStyle = color; + ctx.lineWidth = width; + ctx.moveTo(start.x, start.y); + ctx.lineTo(end.x, end.y); + ctx.stroke(); + } + + /** Draws a rectangle, can be a regular DOMRect or a DOMRectTransform */ + static rect( + rect: DOMRect | DOMRectTransform, + { color = 'blue', width = 2, fill, layer = Gizmos.#defaultLayer }: RectOptions = {}, + ) { + const ctx = Gizmos.#getContext(layer); + if (!ctx) return; + + ctx.beginPath(); + ctx.strokeStyle = color; + ctx.lineWidth = width; + + if (rect instanceof DOMRectTransform) { + // For transformed rectangles, draw using the vertices + const vertices = rect.vertices().map((p) => rect.toParentSpace(p)); + ctx.moveTo(vertices[0].x, vertices[0].y); + for (let i = 1; i < vertices.length; i++) { + ctx.lineTo(vertices[i].x, vertices[i].y); + } + ctx.closePath(); + } else { + // For regular DOMRects, draw a simple rectangle + ctx.rect(rect.x, rect.y, rect.width, rect.height); + } + + if (fill) { + ctx.fillStyle = fill; + ctx.fill(); + } + ctx.stroke(); + } + + /** Clears drawings from a specific layer or all layers if no layer specified */ + static clear(layer?: string) { + if (layer) { + const layerData = Gizmos.#layers.get(layer); + if (!layerData) return; + const { ctx, canvas } = layerData; + ctx.clearRect(0, 0, canvas.width, canvas.height); + } else { + // Clear all layers + Gizmos.#layers.forEach(({ ctx, canvas }) => { + ctx.clearRect(0, 0, canvas.width, canvas.height); + }); + } + } + + #handleResize() { + const layerData = Gizmos.#layers.get(this.#layer); + if (!layerData) return; + + const { canvas, ctx } = layerData; + const rect = this.getBoundingClientRect(); + const dpr = window.devicePixelRatio; + + canvas.width = rect.width * dpr; + canvas.height = rect.height * dpr; + + ctx.scale(dpr, dpr); + } + + static #getContext(layer = Gizmos.#defaultLayer) { + if (!Gizmos.#hasLoggedInitMessage) { + const gizmos = document.querySelectorAll(Gizmos.tagName); + console.info( + '%cGizmos', + 'font-weight: bold; color: #4CAF50;', + '\n• Gizmo elements:', + gizmos.length, + '\n• Layers:', + `[${Array.from(Gizmos.#layers.keys()).join(', ')}]`, + '\n• Default layer:', + Gizmos.#defaultLayer, + ); + Gizmos.#hasLoggedInitMessage = true; + } + + const layerData = Gizmos.#layers.get(layer); + if (!layerData && !Gizmos.#hasLoggedDrawWarning) { + console.warn(`Gizmos cannot draw: layer "${layer}" not found`); + Gizmos.#hasLoggedDrawWarning = true; + return null; + } + + return layerData?.ctx; + } +} + +if (!customElements.get('folk-gizmos')) { + customElements.define('folk-gizmos', Gizmos); +} diff --git a/lib/utils.ts b/lib/utils.ts index 3a0f1b1..a2c517a 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -3,6 +3,8 @@ import type { Point } from './types.ts'; import { Vector } from './Vector.ts'; +export const MAX_Z_INDEX = 2147483647; + // Distance squared from a point p to the line segment vw function distanceToSegmentSq(p: Point, v: Point, w: Point): number { const l2 = Vector.distanceSquared(v, w); @@ -49,7 +51,7 @@ function getPointsOnBezierCurveWithSplitting( points: readonly Point[], offset: number, tolerance: number, - newPoints?: Point[] + newPoints?: Point[], ): Point[] { const outPoints = newPoints || []; if (flatness(points, offset) < tolerance) { @@ -97,7 +99,7 @@ export function simplifyPoints( start: number, end: number, epsilon: number, - newPoints?: Point[] + newPoints?: Point[], ): Point[] { const outPoints = newPoints || []; @@ -155,7 +157,7 @@ export function getSvgPathFromStroke(stroke: number[][]): string { acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2); return acc; }, - ['M', ...stroke[0], 'Q'] + ['M', ...stroke[0], 'Q'], ); d.push('Z'); diff --git a/website/canvas/space-transform.html b/website/canvas/space-transform.html index 28837de..646ced5 100644 --- a/website/canvas/space-transform.html +++ b/website/canvas/space-transform.html @@ -20,7 +20,9 @@ + +