alignment brush

This commit is contained in:
Orion Reed 2024-12-22 22:03:50 -05:00
parent 161de351d8
commit d494e8cd46
8 changed files with 730 additions and 28 deletions

527
labs/folk-brush-field.ts Normal file
View File

@ -0,0 +1,527 @@
import { Gizmos, Point, Vector } from '@lib';
import { FolkBaseSet } from './folk-base-set';
import { FolkShape } from './folk-shape';
interface AlignmentLine {
shapes: Set<FolkShape>;
points: Map<FolkShape, Point>;
lineStart: Point;
lineEnd: Point;
isHorizontal: boolean;
}
export class FolkBrushField extends FolkBaseSet {
static override tagName = 'folk-brush-field';
// Core structure
#alignments = new Set<AlignmentLine>();
#shapeToAlignments = new Map<FolkShape, Set<AlignmentLine>>();
// Interaction state
#isPointerDown = false;
#lastPointerPosition: Point | null = null;
#selectedShapes = new Set<FolkShape>();
// Canvas for brush visualization
#canvas!: HTMLCanvasElement;
#ctx!: CanvasRenderingContext2D;
// Brush settings
readonly #BRUSH_RADIUS = 60;
readonly #TARGET_PADDING = 20;
// Add new constants for removal threshold
readonly #REMOVAL_THRESHOLD_MULTIPLIER = 1.0; // Multiplied by shape size
// Add property to track dragging state
#draggedShape: FolkShape | null = null;
connectedCallback() {
super.connectedCallback();
this.#setupCanvas();
this.#setupEventListeners();
requestAnimationFrame(this.#updateCanvas);
}
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener('resize', this.#handleResize);
}
#setupCanvas() {
this.#canvas = document.createElement('canvas');
this.#canvas.style.cssText = `
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
`;
const ctx = this.#canvas.getContext('2d');
if (!ctx) throw new Error('Could not get canvas context');
this.#ctx = ctx;
this.renderRoot.prepend(this.#canvas);
this.#handleResize();
}
#setupEventListeners() {
this.addEventListener('pointerdown', this.#handlePointerDown);
this.addEventListener('pointermove', this.#handlePointerMove);
this.addEventListener('pointerup', this.#handlePointerUp);
this.addEventListener('pointerleave', this.#handlePointerUp);
window.addEventListener('resize', this.#handleResize);
this.addEventListener('pointerdown', this.#handleShapePointerDown, true);
this.addEventListener('pointerup', this.#handleShapePointerUp, true);
}
#handleResize = () => {
const { width, height } = this.getBoundingClientRect();
this.#canvas.width = width;
this.#canvas.height = height;
};
#handlePointerDown = (event: PointerEvent) => {
if (event.button !== 0) return;
if (event.target !== this) return;
const rect = this.#canvas.getBoundingClientRect();
const point = {
x: event.clientX - rect.left,
y: event.clientY - rect.top,
};
this.#selectedShapes.clear();
this.#isPointerDown = true;
this.#lastPointerPosition = point;
// Find shapes under initial point
this.sourceElements.forEach((element) => {
if (element instanceof FolkShape) {
const rect = element.getTransformDOMRect();
if (
point.x >= rect.x &&
point.x <= rect.x + rect.width &&
point.y >= rect.y &&
point.y <= rect.y + rect.height
) {
this.#selectedShapes.add(element);
}
}
});
};
#handlePointerMove = (event: PointerEvent) => {
if (!this.#isPointerDown || !this.#lastPointerPosition) return;
const rect = this.#canvas.getBoundingClientRect();
const point = {
x: event.clientX - rect.left,
y: event.clientY - rect.top,
};
// Check for shapes under the new line segment
this.sourceElements.forEach((element) => {
if (element instanceof FolkShape) {
const shapeRect = element.getTransformDOMRect();
if (this.#isLineIntersectingRect(this.#lastPointerPosition!, point, shapeRect)) {
this.#selectedShapes.add(element);
}
}
});
this.#drawBrushStroke(this.#lastPointerPosition, point);
this.#lastPointerPosition = point;
};
#handlePointerUp = () => {
if (this.#selectedShapes.size >= 2) {
// Find existing alignments that overlap with selected shapes
const overlappingAlignments = new Set<AlignmentLine>();
const isHorizontal = this.#determineAlignment(this.#selectedShapes);
// Find all overlapping alignments with the same orientation
for (const shape of this.#selectedShapes) {
const shapeAlignments = this.#shapeToAlignments.get(shape);
if (shapeAlignments) {
for (const alignment of shapeAlignments) {
if (alignment.isHorizontal === isHorizontal) {
overlappingAlignments.add(alignment);
}
}
}
}
// Check if we've selected all shapes from multiple alignments
const allShapesSelected = Array.from(overlappingAlignments).every((alignment) =>
Array.from(alignment.shapes).every((shape) => this.#selectedShapes.has(shape)),
);
if (overlappingAlignments.size === 0) {
// Case: No overlapping alignments - create new alignment
this.#createAlignment(this.#selectedShapes);
} else if (overlappingAlignments.size === 1 || !allShapesSelected) {
// Case: One overlapping alignment or partial selection - merge into it
const existingAlignment = overlappingAlignments.values().next().value;
if (existingAlignment) {
this.#mergeIntoAlignment(existingAlignment, this.#selectedShapes);
}
} else {
// Case: Multiple complete alignments selected - merge all into new alignment
const allShapes = new Set<FolkShape>();
this.#selectedShapes.forEach((shape) => allShapes.add(shape));
overlappingAlignments.forEach((alignment) => {
alignment.shapes.forEach((shape) => allShapes.add(shape));
this.#removeAlignment(alignment);
});
this.#createAlignment(allShapes);
}
}
this.#isPointerDown = false;
this.#lastPointerPosition = null;
this.#selectedShapes.clear();
};
#isLineIntersectingRect(lineStart: Point, lineEnd: Point, rect: DOMRect): boolean {
// Simple AABB check
const minX = Math.min(lineStart.x, lineEnd.x);
const maxX = Math.max(lineStart.x, lineEnd.x);
const minY = Math.min(lineStart.y, lineEnd.y);
const maxY = Math.max(lineStart.y, lineEnd.y);
return !(maxX < rect.left || minX > rect.right || maxY < rect.top || minY > rect.bottom);
}
#createAlignment(shapes: Set<FolkShape>) {
if (shapes.size < 2) {
return;
}
// Calculate axis based on shape distribution
const centers = Array.from(shapes).map((shape) => shape.getTransformDOMRect().center);
const bounds = Vector.bounds(centers);
const isHorizontal = bounds.max.x - bounds.min.x > bounds.max.y - bounds.min.y;
const center = Vector.center(centers);
const positions = this.#calculateLinePoints(shapes, isHorizontal, center);
const alignment: AlignmentLine = {
shapes: new Set(shapes), // Create a new Set to avoid reference issues
isHorizontal,
...positions,
};
// Update lookups
this.#alignments.add(alignment);
shapes.forEach((shape) => {
if (!this.#shapeToAlignments.has(shape)) {
this.#shapeToAlignments.set(shape, new Set());
}
this.#shapeToAlignments.get(shape)!.add(alignment);
});
}
#drawBrushStroke(from: Point, to: Point) {
this.#ctx.beginPath();
this.#ctx.moveTo(from.x, from.y);
this.#ctx.lineTo(to.x, to.y);
this.#ctx.lineWidth = this.#BRUSH_RADIUS;
this.#ctx.strokeStyle = 'rgba(150, 190, 255, 0.3)';
this.#ctx.lineCap = 'round';
this.#ctx.stroke();
}
#calculateLinePoints(
shapes: Set<FolkShape>,
isHorizontal: boolean,
centerPoint: Point,
): {
points: Map<FolkShape, Point>;
lineStart: Point;
lineEnd: Point;
} {
const targetPositions = new Map<FolkShape, Point>();
// Get centers and calculate total extent
const shapeInfo = Array.from(shapes).map((shape) => {
const rect = shape.getTransformDOMRect();
return {
shape,
rect,
center: rect.center,
size: isHorizontal ? rect.width : rect.height,
};
});
// Sort shapes along the primary axis
shapeInfo.sort((a, b) => (isHorizontal ? a.center.x - b.center.x : a.center.y - b.center.y));
// Calculate total length including padding between shapes
const totalLength = shapeInfo.reduce((sum, info, index) => {
return sum + info.size + (index < shapeInfo.length - 1 ? this.#TARGET_PADDING : 0);
}, 0);
// Start position (centered around centerPoint)
let currentPos = -totalLength / 2;
// Calculate positions along the line
shapeInfo.forEach((info) => {
const point = isHorizontal
? { x: centerPoint.x + currentPos + info.size / 2, y: centerPoint.y }
: { x: centerPoint.x, y: centerPoint.y + currentPos + info.size / 2 };
targetPositions.set(info.shape, point);
currentPos += info.size + this.#TARGET_PADDING;
});
// Calculate line extent (add padding to ends)
const halfLength = totalLength / 2;
const lineStart = isHorizontal
? { x: centerPoint.x - halfLength, y: centerPoint.y }
: { x: centerPoint.x, y: centerPoint.y - halfLength };
const lineEnd = isHorizontal
? { x: centerPoint.x + halfLength, y: centerPoint.y }
: { x: centerPoint.x, y: centerPoint.y + halfLength };
return {
points: targetPositions,
lineStart,
lineEnd,
};
}
#updateCanvas = () => {
// Clear canvas with fade effect
this.#ctx.fillStyle = 'rgba(255, 255, 255, 0.1)';
this.#ctx.fillRect(0, 0, this.#canvas.width, this.#canvas.height);
// Update shape positions
this.#lerpShapesTowardsTargets();
this.#visualizeAlignments();
requestAnimationFrame(this.#updateCanvas);
};
#lerpShapesTowardsTargets() {
// Check for dragged shapes that could join existing alignments
const activeShape = document.activeElement instanceof FolkShape ? document.activeElement : null;
if (activeShape && !this.#shapeToAlignments.has(activeShape)) {
const rect = activeShape.getTransformDOMRect();
// Find closest alignment within threshold
for (const alignment of this.#alignments) {
const perpDistance = alignment.isHorizontal
? Math.abs(rect.center.y - alignment.lineStart.y)
: Math.abs(rect.center.x - alignment.lineStart.x);
const linePos = alignment.isHorizontal ? rect.center.x : rect.center.y;
const lineStart = alignment.isHorizontal ? alignment.lineStart.x : alignment.lineStart.y;
const lineEnd = alignment.isHorizontal ? alignment.lineEnd.x : alignment.lineEnd.y;
const threshold = (alignment.isHorizontal ? rect.height : rect.width) * this.#REMOVAL_THRESHOLD_MULTIPLIER;
if (perpDistance <= threshold && linePos >= lineStart && linePos <= lineEnd) {
this.#mergeIntoAlignment(alignment, new Set([activeShape]));
break;
}
}
}
// Existing alignment handling
for (const alignment of this.#alignments) {
alignment.shapes.forEach((shape) => {
if (shape === this.#draggedShape) {
// Handle dragged shape logic
const rect = shape.getTransformDOMRect();
const current = rect.center;
// Calculate perpendicular and parallel distances from line
const perpDistance = alignment.isHorizontal
? Math.abs(current.y - alignment.lineStart.y)
: Math.abs(current.x - alignment.lineStart.x);
const linePos = alignment.isHorizontal ? current.x : current.y;
const lineStart = alignment.isHorizontal ? alignment.lineStart.x : alignment.lineStart.y;
const lineEnd = alignment.isHorizontal ? alignment.lineEnd.x : alignment.lineEnd.y;
// Calculate removal thresholds
const perpThreshold =
(alignment.isHorizontal ? rect.height : rect.width) * this.#REMOVAL_THRESHOLD_MULTIPLIER;
// Remove if too far perpendicular to line or beyond line endpoints
if (perpDistance > perpThreshold || linePos < lineStart || linePos > lineEnd) {
this.#removeShapeFromAlignment(shape, alignment);
} else {
// Recalculate positions while keeping the line fixed
const positions = this.#calculateLinePoints(alignment.shapes, alignment.isHorizontal, {
x: alignment.isHorizontal ? (alignment.lineStart.x + alignment.lineEnd.x) / 2 : alignment.lineStart.x,
y: alignment.isHorizontal ? alignment.lineStart.y : (alignment.lineStart.y + alignment.lineEnd.y) / 2,
});
alignment.points = positions.points;
}
return;
} else {
// Move non-dragged shapes
const target = alignment.points.get(shape)!;
const current = shape.getTransformDOMRect().center;
shape.x += (target.x - current.x) * 0.25;
shape.y += (target.y - current.y) * 0.25;
}
});
}
}
#removeShapeFromAlignment(shape: FolkShape, alignment: AlignmentLine) {
alignment.shapes.delete(shape);
alignment.points.delete(shape);
const shapeAlignments = this.#shapeToAlignments.get(shape);
if (shapeAlignments) {
shapeAlignments.delete(alignment);
if (shapeAlignments.size === 0) {
this.#shapeToAlignments.delete(shape);
}
}
// Remove alignment if less than 2 shapes remain
if (alignment.shapes.size < 2) {
this.#removeAlignment(alignment);
} else {
// Recalculate positions while keeping line fixed
const center = {
x: alignment.isHorizontal ? (alignment.lineStart.x + alignment.lineEnd.x) / 2 : alignment.lineStart.x,
y: alignment.isHorizontal ? alignment.lineStart.y : (alignment.lineStart.y + alignment.lineEnd.y) / 2,
};
const positions = this.#calculateLinePoints(alignment.shapes, alignment.isHorizontal, center);
alignment.points = positions.points;
alignment.lineStart = positions.lineStart;
alignment.lineEnd = positions.lineEnd;
}
}
#visualizeAlignments() {
Gizmos.clear();
// Show active alignments
for (const alignment of this.#alignments) {
const style = { color: 'blue', width: 2 };
// Draw alignment line
Gizmos.line(alignment.lineStart, alignment.lineEnd, style);
// Draw shape connections and targets
alignment.shapes.forEach((shape) => {
const rect = shape.getTransformDOMRect();
const current = rect.center;
const target = alignment.points.get(shape)!;
Gizmos.line(current, target, {
color: 'rgba(150, 150, 150, 0.5)',
width: 1,
});
Gizmos.point(target, {
color: style.color,
size: 4,
});
});
}
// Show potential alignment
if (this.#isPointerDown && this.#selectedShapes.size >= 2) {
const centers = Array.from(this.#selectedShapes).map((shape) => shape.getTransformDOMRect().center);
const bounds = Vector.bounds(centers);
const isHorizontal = bounds.max.x - bounds.min.x > bounds.max.y - bounds.min.y;
const center = Vector.center(centers);
const positions = this.#calculateLinePoints(this.#selectedShapes, isHorizontal, center);
const potential = {
shapes: this.#selectedShapes,
isHorizontal,
lineStart: positions.lineStart,
lineEnd: positions.lineEnd,
points: positions.points,
};
// Draw potential alignment
const style = { color: 'green', width: 2 };
Gizmos.line(potential.lineStart, potential.lineEnd, style);
potential.shapes.forEach((shape) => {
const rect = shape.getTransformDOMRect();
const current = rect.center;
const target = potential.points.get(shape)!;
Gizmos.line(current, target, {
color: 'rgba(150, 150, 150, 0.5)',
width: 1,
});
Gizmos.point(target, {
color: style.color,
size: 4,
});
});
}
}
// Helper methods to support the new functionality
#determineAlignment(shapes: Set<FolkShape>): boolean {
const centers = Array.from(shapes).map((shape) => shape.getTransformDOMRect().center);
const bounds = Vector.bounds(centers);
return bounds.max.x - bounds.min.x > bounds.max.y - bounds.min.y;
}
#mergeIntoAlignment(alignment: AlignmentLine, newShapes: Set<FolkShape>) {
// Add new shapes to existing alignment
newShapes.forEach((shape) => alignment.shapes.add(shape));
// Recalculate alignment positions while keeping line fixed
const center = {
x: alignment.isHorizontal ? (alignment.lineStart.x + alignment.lineEnd.x) / 2 : alignment.lineStart.x,
y: alignment.isHorizontal ? alignment.lineStart.y : (alignment.lineStart.y + alignment.lineEnd.y) / 2,
};
const positions = this.#calculateLinePoints(alignment.shapes, alignment.isHorizontal, center);
alignment.points = positions.points;
alignment.lineStart = positions.lineStart;
alignment.lineEnd = positions.lineEnd;
// Update shape-to-alignment mappings
newShapes.forEach((shape) => {
if (!this.#shapeToAlignments.has(shape)) {
this.#shapeToAlignments.set(shape, new Set());
}
this.#shapeToAlignments.get(shape)!.add(alignment);
});
}
#removeAlignment(alignment: AlignmentLine) {
// Remove from main set
this.#alignments.delete(alignment);
// Remove from all shape mappings
alignment.shapes.forEach((shape) => {
const alignments = this.#shapeToAlignments.get(shape);
if (alignments) {
alignments.delete(alignment);
if (alignments.size === 0) {
this.#shapeToAlignments.delete(shape);
}
}
});
}
// gross but here it is
#handleShapePointerDown = (event: PointerEvent) => {
if (event.target instanceof FolkShape) {
this.#draggedShape = event.target;
}
};
#handleShapePointerUp = () => {
this.#draggedShape = null;
};
}

View File

@ -0,0 +1,5 @@
import { FolkBrushField } from '../folk-brush-field';
FolkBrushField.define();
export { FolkBrushField };

View File

@ -11,6 +11,38 @@ export class Vector {
return { x: 0, y: 0 };
}
/**
* Unit vector pointing right (1,0)
* @returns {Point} A point representing a right vector
*/
static right(): Point {
return { x: 1, y: 0 };
}
/**
* Unit vector pointing left (-1,0)
* @returns {Point} A point representing a left vector
*/
static left(): Point {
return { x: -1, y: 0 };
}
/**
* Unit vector pointing up (0,-1)
* @returns {Point} A point representing an up vector
*/
static up(): Point {
return { x: 0, y: -1 };
}
/**
* Unit vector pointing down (0,1)
* @returns {Point} A point representing a down vector
*/
static down(): Point {
return { x: 0, y: 1 };
}
/**
* Subtracts vector b from vector a
* @param {Point} a - The first vector
@ -190,4 +222,44 @@ export class Vector {
static magSquared(v: Point): number {
return v.x * v.x + v.y * v.y;
}
/**
* Calculates the bounding box of a set of points
* @param {Point[]} points - Array of points to find bounds for
* @returns {{ min: Point, max: Point }} Object containing min and max points of the bounds
*/
static bounds(points: Point[]): { min: Point; max: Point } {
return points.reduce(
(acc, p) => ({
min: { x: Math.min(acc.min.x, p.x), y: Math.min(acc.min.y, p.y) },
max: { x: Math.max(acc.max.x, p.x), y: Math.max(acc.max.y, p.y) },
}),
{ min: { x: Infinity, y: Infinity }, max: { x: -Infinity, y: -Infinity } },
);
}
/**
* Calculates the center point of a set of points
* @param {Point[]} points - Array of points to find center for
* @returns {Point} The center point
*/
static center(points: Point[]): Point {
const bounds = Vector.bounds(points);
return {
x: (bounds.min.x + bounds.max.x) / 2,
y: (bounds.min.y + bounds.max.y) / 2,
};
}
/**
* Projects a point onto an axis
* @param {Point} point - The point to project
* @param {Point} axis - The axis to project onto
* @returns {Point} The projected point
*/
static project(point: Point, axis: Point): Point {
const normalized = Vector.normalized(axis);
const dot = point.x * normalized.x + point.y * normalized.y;
return Vector.scale(normalized, dot);
}
}

View File

@ -1,5 +1,6 @@
import { DOMRectTransform, FolkElement, type Point } from '@lib';
import { html } from '@lib/tags';
import { Vector } from '@lib/Vector';
import { css } from '@lit/reactive-element';
interface GizmoOptions {
@ -49,6 +50,7 @@ export class Gizmos extends FolkElement {
{
ctx: CanvasRenderingContext2D;
canvas: HTMLCanvasElement;
hidden: boolean;
}
>();
static #defaultLayer = 'default';
@ -75,10 +77,12 @@ export class Gizmos extends FolkElement {
`;
readonly #layer: string;
#hidden: boolean;
constructor() {
super();
this.#layer = this.getAttribute('layer') ?? Gizmos.#defaultLayer;
this.#hidden = this.hasAttribute('hidden');
}
override createRenderRoot() {
@ -90,7 +94,7 @@ export class Gizmos extends FolkElement {
const ctx = canvas?.getContext('2d') ?? null;
if (canvas && ctx) {
Gizmos.#layers.set(this.#layer, { canvas, ctx });
Gizmos.#layers.set(this.#layer, { canvas, ctx, hidden: this.#hidden });
}
this.#handleResize();
@ -155,37 +159,47 @@ export class Gizmos extends FolkElement {
ctx.stroke();
}
/** Draws a vector with an arrow head */
/**
* Draws a vector with an arrow head starting from `origin`.
* @param origin The starting point
* @param vector The vector endpoint in local coordinates relative to origin
*/
static vector(
origin: Point,
vector: Point,
localEndpoint: Point,
{ color = 'blue', width = 2, size = 10, layer = Gizmos.#defaultLayer }: VectorOptions = {},
) {
const ctx = Gizmos.#getContext(layer);
if (!ctx) return;
// Calculate angle and length
const angle = Math.atan2(vector.y - origin.y, vector.x - origin.x);
const arrowAngle = Math.PI / 6; // 30 degrees
// Convert local endpoint to global coordinates
const globalEndpoint = Vector.add(origin, localEndpoint);
// Calculate angle and normalized direction
const angle = Vector.angle(localEndpoint);
const length = Vector.mag(localEndpoint);
// Calculate where the line should end (where arrow head begins)
const lineEndX = vector.x - size * Math.cos(angle);
const lineEndY = vector.y - size * Math.sin(angle);
const lineEnd = Vector.add(origin, Vector.scale(Vector.normalized(localEndpoint), length - size));
// Draw the main line
ctx.beginPath();
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.moveTo(origin.x, origin.y);
ctx.lineTo(lineEndX, lineEndY);
ctx.lineTo(lineEnd.x, lineEnd.y);
ctx.stroke();
// Draw arrow head as a connected triangle
const arrowAngle = Math.PI / 6; // 30 degrees
const leftPoint = Vector.add(globalEndpoint, Vector.rotate({ x: -size, y: 0 }, angle - arrowAngle));
const rightPoint = Vector.add(globalEndpoint, Vector.rotate({ x: -size, y: 0 }, angle + arrowAngle));
ctx.beginPath();
ctx.moveTo(vector.x, vector.y); // Tip of the arrow
ctx.lineTo(vector.x - size * Math.cos(angle - arrowAngle), vector.y - size * Math.sin(angle - arrowAngle));
ctx.lineTo(vector.x - size * Math.cos(angle + arrowAngle), vector.y - size * Math.sin(angle + arrowAngle));
ctx.lineTo(vector.x, vector.y); // Back to the tip
ctx.moveTo(globalEndpoint.x, globalEndpoint.y);
ctx.lineTo(leftPoint.x, leftPoint.y);
ctx.lineTo(rightPoint.x, rightPoint.y);
ctx.lineTo(globalEndpoint.x, globalEndpoint.y);
ctx.fillStyle = color;
ctx.fill();
}
@ -219,19 +233,47 @@ export class Gizmos extends FolkElement {
ctx.scale(dpr, dpr);
}
static #log() {
return {
message: '',
styles: [] as string[],
text(str: string) {
this.message += str;
return this;
},
color(str: string, color: string) {
this.message += '%c' + str + '%c';
this.styles.push(`font-weight: bold; color: ${color}`, '');
return this;
},
print() {
console.info(this.message, ...this.styles);
},
};
}
static #getContext(layer = Gizmos.#defaultLayer) {
if (!Gizmos.#hasLoggedInitMessage) {
const gizmos = document.querySelectorAll<Gizmos>(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,
);
const layers = Array.from(Gizmos.#layers.entries());
const log = Gizmos.#log()
.color('Gizmos', '#4CAF50')
.text('\n• Gizmo elements: ' + gizmos.length)
.text('\n• Layers: [');
layers.forEach(([key, value], i) => {
if (i > 0) log.text(', ');
log.text(key);
if (value.hidden) {
log.color(' (hidden)', '#FFA500');
}
});
log.text(']').print();
Gizmos.#hasLoggedInitMessage = true;
}
@ -242,7 +284,7 @@ export class Gizmos extends FolkElement {
return null;
}
return layerData?.ctx;
return layerData?.hidden ? null : layerData?.ctx;
}
}

View File

@ -8,6 +8,7 @@ export * from './folk-observer';
export * from './resize-manger';
// Core utilities and types
export * from './folk-gizmos';
export * from './Matrix';
export * from './types';
export * from './Vector';

View File

@ -130,8 +130,8 @@ rotation: from.x"
Gizmos.vector(
center,
{
x: center.x + gravity.x / 10,
y: center.y + gravity.y / 10,
x: gravity.x / 10,
y: gravity.y / 10,
},
{ color: 'grey', width: 3, size: 15 },
);

View File

@ -0,0 +1,55 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Brush Field Demo</title>
<style>
html {
height: 100%;
}
body {
min-height: 100%;
position: relative;
margin: 0;
overscroll-behavior: none;
}
folk-shape {
position: absolute;
background-color: rgba(255, 255, 255, 0.8);
border-radius: 2px;
border: 2px solid rgba(0, 0, 0, 0.5);
}
folk-brush-field {
pointer-events: all;
}
</style>
</head>
<body>
<folk-gizmos></folk-gizmos>
<folk-brush-field>
<folk-shape x="100" y="100" width="50" height="50"></folk-shape>
<folk-shape x="100" y="200" width="50" height="50"></folk-shape>
<folk-shape x="100" y="300" width="50" height="50"></folk-shape>
<folk-shape x="300" y="150" width="80" height="40"></folk-shape>
<folk-shape x="400" y="250" width="60" height="90"></folk-shape>
<folk-shape x="200" y="400" width="100" height="100"></folk-shape>
<folk-shape x="500" y="100" width="30" height="70"></folk-shape>
<folk-shape x="600" y="300" width="70" height="70"></folk-shape>
<folk-shape x="250" y="50" width="45" height="65"></folk-shape>
<folk-shape x="450" y="400" width="90" height="40"></folk-shape>
<folk-shape x="550" y="200" width="40" height="120"></folk-shape>
<folk-shape x="150" y="500" width="55" height="55"></folk-shape>
<folk-shape x="350" y="350" width="75" height="45"></folk-shape>
</folk-brush-field>
<script type="module">
import '@labs/standalone/folk-shape.ts';
import '@labs/standalone/folk-brush-field.ts';
import { Gizmos } from '@lib/folk-gizmos.ts';
</script>
</body>
</html>

View File

@ -99,8 +99,8 @@
Gizmos.vector(
center,
{
x: center.x + gravity.x / 2,
y: center.y + gravity.y / 2,
x: gravity.x / 2,
y: gravity.y / 2,
},
{ color: 'grey', width: 3, size: 15 },
);