keyboard shape input

This commit is contained in:
Orion Reed 2024-12-03 17:58:33 -05:00
parent 53a87fa344
commit b0b74d077c
1 changed files with 240 additions and 138 deletions

View File

@ -327,6 +327,8 @@ export class FolkShape extends HTMLElement {
super(); super();
this.addEventListener('pointerdown', this); this.addEventListener('pointerdown', this);
this.addEventListener('keydown', this);
this.setAttribute('tabindex', '0');
this.#shadow.adoptedStyleSheets = [styles, this.#dynamicStyles]; this.#shadow.adoptedStyleSheets = [styles, this.#dynamicStyles];
// Ideally we would creating these lazily on first focus, but the resize handlers need to be around for delegate focus to work. // Ideally we would creating these lazily on first focus, but the resize handlers need to be around for delegate focus to work.
@ -404,7 +406,92 @@ export class FolkShape extends HTMLElement {
return []; return [];
} }
handleEvent(event: PointerEvent) { handleEvent(event: PointerEvent | KeyboardEvent) {
if (event instanceof KeyboardEvent) {
const MOVEMENT_DELTA = event.shiftKey ? 20 : 2;
const ROTATION_DELTA = event.shiftKey ? Math.PI / 12 : Math.PI / 36; // 15 or 5 degrees
// Get the focused element to check if it's a resize handle
const focusedElement = this.#shadow.activeElement;
const handle = focusedElement?.getAttribute('part') as Handle | null;
// Create synthetic mouse coordinates for keyboard events
let syntheticMouse: Point | null = null;
if (handle?.startsWith('resize')) {
const anyChange =
event.key === 'ArrowUp' ||
event.key === 'ArrowDown' ||
event.key === 'ArrowLeft' ||
event.key === 'ArrowRight';
if (!anyChange) return;
// Get the corner coordinates of the shape for the corresponding handle
const corners = this.getClientRect().corners(); // Returns an array of Points: [NW, NE, SE, SW]
// Map handle names to corner indices
const handleToCornerIndex: { [key: string]: number } = {
'resize-nw': 0, // Top-left corner
'resize-ne': 1, // Top-right corner
'resize-se': 2, // Bottom-right corner
'resize-sw': 3, // Bottom-left corner
};
const cornerIndex = handleToCornerIndex[handle];
const currentPos = corners[cornerIndex];
// Calculate movement based on arrow keys
const isVertical = event.key === 'ArrowUp' || event.key === 'ArrowDown';
const isIncreasing = event.key === 'ArrowRight' || event.key === 'ArrowDown';
const delta = isIncreasing ? MOVEMENT_DELTA : -MOVEMENT_DELTA;
syntheticMouse = {
x: currentPos.x + (isVertical ? 0 : delta),
y: currentPos.y + (isVertical ? delta : 0),
};
// Process resize using the same logic as mouse events
this.#handleResize(handle, syntheticMouse, focusedElement as HTMLElement);
event.preventDefault();
return;
}
// Handle rotation with Alt key
if (event.altKey) {
switch (event.key) {
case 'ArrowLeft':
this.rotation -= ROTATION_DELTA;
event.preventDefault();
return;
case 'ArrowRight':
this.rotation += ROTATION_DELTA;
event.preventDefault();
return;
}
}
switch (event.key) {
case 'ArrowLeft':
this.x -= MOVEMENT_DELTA;
event.preventDefault();
return;
case 'ArrowRight':
this.x += MOVEMENT_DELTA;
event.preventDefault();
return;
case 'ArrowUp':
this.y -= MOVEMENT_DELTA;
event.preventDefault();
return;
case 'ArrowDown':
this.y += MOVEMENT_DELTA;
event.preventDefault();
return;
}
return;
}
if (event instanceof PointerEvent) {
switch (event.type) { switch (event.type) {
case 'pointerdown': { case 'pointerdown': {
if (event.button !== 0 || event.ctrlKey) return; if (event.button !== 0 || event.ctrlKey) return;
@ -446,80 +533,7 @@ export class FolkShape extends HTMLElement {
if (handle.includes('resize')) { if (handle.includes('resize')) {
const mouse = { x: event.clientX, y: event.clientY }; const mouse = { x: event.clientX, y: event.clientY };
this.#handleResize(handle, mouse, target, event);
// Map each resize handle to its opposite corner index
const OPPOSITE_CORNERS = {
'resize-se': 0,
'resize-sw': 1,
'resize-nw': 2,
'resize-ne': 3,
} as const;
// Get the opposite corner for the current resize handle
const corners = this.getClientRect().corners();
const oppositeCorner = corners[OPPOSITE_CORNERS[handle as keyof typeof OPPOSITE_CORNERS]];
// Calculate new dimensions based on mouse position and opposite corner
const newCenter = Vector.lerp(oppositeCorner, mouse, 0.5);
const unrotatedHandle = Vector.rotateAround(mouse, newCenter, -this.rotation);
const unrotatedAnchor = Vector.rotateAround(oppositeCorner, newCenter, -this.rotation);
const HANDLE_BEHAVIOR = {
'resize-se': {
flipX: unrotatedHandle.x < unrotatedAnchor.x,
flipY: unrotatedHandle.y < unrotatedAnchor.y,
handleX: 'resize-sw',
handleY: 'resize-ne',
},
'resize-sw': {
flipX: unrotatedHandle.x > unrotatedAnchor.x,
flipY: unrotatedHandle.y < unrotatedAnchor.y,
handleX: 'resize-se',
handleY: 'resize-nw',
},
'resize-nw': {
flipX: unrotatedHandle.x > unrotatedAnchor.x,
flipY: unrotatedHandle.y > unrotatedAnchor.y,
handleX: 'resize-ne',
handleY: 'resize-sw',
},
'resize-ne': {
flipX: unrotatedHandle.x < unrotatedAnchor.x,
flipY: unrotatedHandle.y > unrotatedAnchor.y,
handleX: 'resize-nw',
handleY: 'resize-se',
},
} as const;
const behavior = HANDLE_BEHAVIOR[handle as keyof typeof HANDLE_BEHAVIOR];
const hasFlippedX = behavior.flipX;
const hasFlippedY = behavior.flipY;
if (hasFlippedX || hasFlippedY) {
const nextHandle = hasFlippedX ? behavior.handleX : behavior.handleY;
const newTarget = this.#shadow.querySelector(`[part="${nextHandle}"]`) as HTMLElement;
if (newTarget) {
// Clean up old handle state
this.#internals.states.delete(handle);
target.removeEventListener('pointermove', this);
target.removeEventListener('lostpointercapture', this);
// Set up new handle state
this.#internals.states.add(nextHandle);
newTarget.addEventListener('pointermove', this);
newTarget.addEventListener('lostpointercapture', this);
// Transfer pointer capture
target.releasePointerCapture(event.pointerId);
newTarget.setPointerCapture(event.pointerId);
}
}
this.x = Math.min(unrotatedHandle.x, unrotatedAnchor.x);
this.y = Math.min(unrotatedHandle.y, unrotatedAnchor.y);
this.width = Math.abs(unrotatedAnchor.x - unrotatedHandle.x);
this.height = Math.abs(unrotatedAnchor.y - unrotatedHandle.y);
return; return;
} }
@ -566,6 +580,7 @@ export class FolkShape extends HTMLElement {
} }
} }
} }
}
#updatedProperties = new Set<string>(); #updatedProperties = new Set<string>();
#isUpdating = false; #isUpdating = false;
@ -683,6 +698,93 @@ export class FolkShape extends HTMLElement {
this.#dynamicStyles.replaceSync(dynamicStyles); this.#dynamicStyles.replaceSync(dynamicStyles);
} }
// Updated helper method to handle resize operations
#handleResize(handle: Handle, mouse: Point, target: HTMLElement, event?: PointerEvent) {
// Map each resize handle to its opposite corner index
const OPPOSITE_CORNERS = {
'resize-se': 0,
'resize-sw': 1,
'resize-nw': 2,
'resize-ne': 3,
} as const;
// Get the opposite corner for the current resize handle
const corners = this.getClientRect().corners();
const oppositeCorner = corners[OPPOSITE_CORNERS[handle as keyof typeof OPPOSITE_CORNERS]];
// Calculate new dimensions based on mouse position and opposite corner
const newCenter = Vector.lerp(oppositeCorner, mouse, 0.5);
const unrotatedHandle = Vector.rotateAround(mouse, newCenter, -this.rotation);
const unrotatedAnchor = Vector.rotateAround(oppositeCorner, newCenter, -this.rotation);
const HANDLE_BEHAVIOR = {
'resize-se': {
flipX: unrotatedHandle.x < unrotatedAnchor.x,
flipY: unrotatedHandle.y < unrotatedAnchor.y,
handleX: 'resize-sw',
handleY: 'resize-ne',
},
'resize-sw': {
flipX: unrotatedHandle.x > unrotatedAnchor.x,
flipY: unrotatedHandle.y < unrotatedAnchor.y,
handleX: 'resize-se',
handleY: 'resize-nw',
},
'resize-nw': {
flipX: unrotatedHandle.x > unrotatedAnchor.x,
flipY: unrotatedHandle.y > unrotatedAnchor.y,
handleX: 'resize-ne',
handleY: 'resize-sw',
},
'resize-ne': {
flipX: unrotatedHandle.x < unrotatedAnchor.x,
flipY: unrotatedHandle.y > unrotatedAnchor.y,
handleX: 'resize-nw',
handleY: 'resize-se',
},
} as const;
// Handle flipping logic
const behavior = HANDLE_BEHAVIOR[handle as keyof typeof HANDLE_BEHAVIOR];
const hasFlippedX = behavior.flipX;
const hasFlippedY = behavior.flipY;
if (hasFlippedX || hasFlippedY) {
const nextHandle = hasFlippedX ? behavior.handleX : behavior.handleY;
const newTarget = this.#shadow.querySelector(`[part="${nextHandle}"]`) as HTMLElement;
if (newTarget) {
// Update focus for keyboard events
newTarget.focus();
// Update handle state
this.#internals.states.delete(handle);
this.#internals.states.add(nextHandle);
// Handle pointer capture swap for mouse events
if (event && 'setPointerCapture' in target) {
// Clean up old handle state
target.removeEventListener('pointermove', this);
target.removeEventListener('lostpointercapture', this);
// Set up new handle state
newTarget.addEventListener('pointermove', this);
newTarget.addEventListener('lostpointercapture', this);
// Transfer pointer capture
target.releasePointerCapture(event.pointerId);
newTarget.setPointerCapture(event.pointerId);
}
}
}
// Update dimensions
this.x = Math.min(unrotatedHandle.x, unrotatedAnchor.x);
this.y = Math.min(unrotatedHandle.y, unrotatedAnchor.y);
this.width = Math.abs(unrotatedAnchor.x - unrotatedHandle.x);
this.height = Math.abs(unrotatedAnchor.y - unrotatedHandle.y);
}
} }
FolkShape.define(); FolkShape.define();