cut only on defocus

This commit is contained in:
Orion Reed 2024-12-14 02:27:42 -05:00
parent 0856204579
commit 3bcd80b32a
2 changed files with 61 additions and 30 deletions

View File

@ -9,8 +9,14 @@ export class FolkEventPropagator extends FolkRope {
static styles = [ static styles = [
...FolkRope.styles, ...FolkRope.styles,
css` css`
textarea { .input-container {
position: absolute; position: absolute;
display: flex;
flex-direction: column;
translate: -50% -50%;
}
textarea {
width: auto; width: auto;
min-width: 3ch; min-width: 3ch;
height: auto; height: auto;
@ -21,8 +27,17 @@ export class FolkEventPropagator extends FolkRope {
pointer-events: auto; pointer-events: auto;
overflow: hidden; overflow: hidden;
field-sizing: content; field-sizing: content;
translate: -50% -50%; box-sizing: content-box;
border-radius: 5px; }
.trigger {
border-radius: 5px 5px 0 0;
border-bottom: none;
width: fit-content;
}
.expression {
border-radius: 0 5px 5px 5px;
} }
`, `,
]; ];
@ -33,10 +48,16 @@ export class FolkEventPropagator extends FolkRope {
#triggerTextarea = document.createElement('textarea'); #triggerTextarea = document.createElement('textarea');
#expressionTextarea = document.createElement('textarea'); #expressionTextarea = document.createElement('textarea');
#propagator: Propagator | null = null; #propagator: Propagator | null = null;
#container = document.createElement('div');
#hasError = false;
override firstUpdated(changedProperties: PropertyValues<this>): void { override firstUpdated(changedProperties: PropertyValues<this>): void {
super.firstUpdated(changedProperties); super.firstUpdated(changedProperties);
this.#container.className = 'input-container';
this.#triggerTextarea.className = 'trigger';
this.#expressionTextarea.className = 'expression';
this.#triggerTextarea.addEventListener('change', () => { this.#triggerTextarea.addEventListener('change', () => {
this.trigger = this.#triggerTextarea.value; this.trigger = this.#triggerTextarea.value;
}); });
@ -45,10 +66,17 @@ export class FolkEventPropagator extends FolkRope {
this.expression = this.#expressionTextarea.value; this.expression = this.#expressionTextarea.value;
}); });
this.#expressionTextarea.addEventListener('focusout', () => {
if (this.#hasError) {
this.cut();
}
});
this.#triggerTextarea.value = this.trigger; this.#triggerTextarea.value = this.trigger;
this.#expressionTextarea.value = this.expression; this.#expressionTextarea.value = this.expression;
this.renderRoot.append(this.#triggerTextarea, this.#expressionTextarea); this.#container.append(this.#triggerTextarea, this.#expressionTextarea);
this.renderRoot.append(this.#container);
this.#initializePropagator(); this.#initializePropagator();
} }
@ -73,24 +101,23 @@ export class FolkEventPropagator extends FolkRope {
target: this.targetElement, target: this.targetElement,
event: this.trigger, event: this.trigger,
handler: this.expression, handler: this.expression,
onParseError: () => this.cut(), onParseError: () => {
onParseSuccess: () => this.mend(), this.#hasError = true;
},
onParseSuccess: () => {
this.#hasError = false;
this.mend();
},
}); });
} }
override render() { override render() {
super.render(); super.render();
const triggerPoint = this.points[Math.floor(this.points.length / 5)]; const point = this.getPointAt(0.5);
if (triggerPoint) { if (point) {
this.#triggerTextarea.style.left = `${triggerPoint.pos.x}px`; this.#container.style.left = `${point.pos.x}px`;
this.#triggerTextarea.style.top = `${triggerPoint.pos.y}px`; this.#container.style.top = `${point.pos.y}px`;
}
const expressionPoint = this.points[Math.floor(this.points.length / 2)];
if (expressionPoint) {
this.#expressionTextarea.style.left = `${expressionPoint.pos.x}px`;
this.#expressionTextarea.style.top = `${expressionPoint.pos.y}px`;
} }
} }
} }

View File

@ -8,8 +8,6 @@ import { css, PropertyValues } from '@lit/reactive-element';
import { AnimationFrameController, AnimationFrameControllerHost } from './common/animation-frame-controller.ts'; import { AnimationFrameController, AnimationFrameControllerHost } from './common/animation-frame-controller.ts';
import { property } from '@lit/reactive-element/decorators.js'; import { property } from '@lit/reactive-element/decorators.js';
const lerp = (first: number, second: number, percentage: number) => first + (second - first) * percentage;
// Each rope part is one of these uses a high precision variant of StörmerVerlet integration to keep the simulation consistent otherwise it would "explode"! // Each rope part is one of these uses a high precision variant of StörmerVerlet integration to keep the simulation consistent otherwise it would "explode"!
interface RopePoint { interface RopePoint {
pos: Point; pos: Point;
@ -103,20 +101,20 @@ export class FolkRope extends FolkBaseConnection implements AnimationFrameContro
let target: Point; let target: Point;
if (sourceRect instanceof DOMRectTransform) { if (sourceRect instanceof DOMRectTransform) {
source = sourceRect.toParentSpace({ x: sourceRect.width / 2, y: sourceRect.height }); source = sourceRect.center;
} else { } else {
source = { source = {
x: sourceRect.x + sourceRect.width / 2, x: sourceRect.x + sourceRect.width / 2,
y: sourceRect.y + sourceRect.height, y: sourceRect.y + sourceRect.height / 2,
}; };
} }
if (targetRect instanceof DOMRectTransform) { if (targetRect instanceof DOMRectTransform) {
target = targetRect.toParentSpace({ x: targetRect.width / 2, y: targetRect.height }); target = targetRect.center;
} else { } else {
target = { target = {
x: targetRect.x + targetRect.width / 2, x: targetRect.x + targetRect.width / 2,
y: targetRect.y + targetRect.height, y: targetRect.y + targetRect.height / 2,
}; };
} }
@ -173,10 +171,7 @@ export class FolkRope extends FolkBaseConnection implements AnimationFrameContro
for (let i = 0; i < pointsLen; i++) { for (let i = 0; i < pointsLen; i++) {
const percentage = i / (pointsLen - 1); const percentage = i / (pointsLen - 1);
const pos = { const pos = Vector.lerp(start, end, percentage);
x: lerp(start.x, end.x, percentage),
y: lerp(start.y, end.y, percentage),
};
points.push({ points.push({
pos, pos,
@ -226,19 +221,28 @@ export class FolkRope extends FolkBaseConnection implements AnimationFrameContro
if (point.prev) applyConstraint(point, point.prev); if (point.prev) applyConstraint(point, point.prev);
} }
cut(index = Math.floor(this.#points.length / 2)) { cut(atPercentage = 0.5) {
if (index < 0 || index >= this.#points.length - 1) return; const index = this.#getPointIndexAt(atPercentage);
this.#points[index].next = null; this.#points[index].next = null;
this.#points[index + 1].prev = null; this.#points[index + 1].prev = null;
} }
mend(index = Math.floor(this.#points.length / 2)) { mend(atPercentage = 0.5) {
if (index < 0 || index >= this.#points.length - 1) return; const index = this.#getPointIndexAt(atPercentage);
this.#points[index].next = this.#points[index + 1]; this.#points[index].next = this.#points[index + 1];
this.#points[index + 1].prev = this.#points[index]; this.#points[index + 1].prev = this.#points[index];
} }
getPointAt(percentage: number) {
return this.#points[this.#getPointIndexAt(percentage)];
}
#getPointIndexAt(percentage: number) {
const clamped = Math.min(Math.max(percentage, 0), 1);
return Math.floor(this.#points.length * clamped);
}
} }
function applyConstraint(p1: RopePoint, p2: RopePoint) { function applyConstraint(p1: RopePoint, p2: RopePoint) {