diff --git a/demo/arrow.html b/demo/arrow.html
deleted file mode 100644
index f562b96..0000000
--- a/demo/arrow.html
+++ /dev/null
@@ -1,61 +0,0 @@
-
-
-
-
-
- Arrow
-
-
-
-
-
-
-
-
-
-
diff --git a/demo/event-propagator.html b/demo/event-propagator.html
new file mode 100644
index 0000000..1b3e1e1
--- /dev/null
+++ b/demo/event-propagator.html
@@ -0,0 +1,47 @@
+
+
+
+
+
+ Event Propagator
+
+
+
+
+ Hello World
+
+
+
+
+
diff --git a/src/arrows/event-propagator.ts b/src/arrows/event-propagator.ts
new file mode 100644
index 0000000..ae0e25e
--- /dev/null
+++ b/src/arrows/event-propagator.ts
@@ -0,0 +1,65 @@
+import { SpatialConnection } from './spatial-connection';
+
+export class EventPropagator extends SpatialConnection {
+ static tagName = 'event-propagator';
+
+ #triggers = (this.getAttribute('triggers') || '').split(',');
+ get triggers() {
+ return this.#triggers;
+ }
+ set triggers(triggers) {
+ this.#triggers = triggers;
+ }
+
+ #expression = '';
+ #function = new Function();
+ get expression() {
+ return this.#expression;
+ }
+ set expression(expression) {
+ this.#expression = expression;
+ this.#function = new Function('$source', '$target', '$event', expression);
+ }
+
+ constructor() {
+ super();
+
+ this.expression = this.getAttribute('expression') || '';
+ }
+
+ observeSource() {
+ super.observeSource();
+
+ for (const trigger of this.#triggers) {
+ // TODO: add special triggers for intersection, rAF, etc.
+ this.sourceElement?.addEventListener(trigger, this.evaluateExpression);
+ }
+
+ this.evaluateExpression();
+ }
+
+ unobserveSource() {
+ super.unobserveSource();
+
+ for (const trigger of this.#triggers) {
+ // TODO: add special triggers for intersection, rAF, etc.
+ this.sourceElement?.removeEventListener(trigger, this.evaluateExpression);
+ }
+ }
+
+ observeTarget() {
+ super.observeTarget();
+ this.evaluateExpression();
+ }
+
+ unobserveTarget() {
+ super.unobserveTarget();
+ }
+
+ // Do we need the event at all?
+ evaluateExpression = (event?: Event) => {
+ if (this.sourceElement === null || this.targetElement === null) return;
+
+ this.#function(this.sourceElement, this.targetElement, event);
+ };
+}
diff --git a/src/arrows/scoped-propagator.ts b/src/arrows/scoped-propagator.ts
deleted file mode 100644
index 9b54e4b..0000000
--- a/src/arrows/scoped-propagator.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-import { getBoxToBoxArrow } from 'perfect-arrows';
-import { AbstractArrow } from './abstract-arrow';
-import { pointsOnBezierCurves } from './points-on-path';
-import getStroke, { StrokeOptions } from 'perfect-freehand';
-
-export type Arrow = [
- /** The x position of the (padded) starting point. */
- sx: number,
- /** The y position of the (padded) starting point. */
- sy: number,
- /** The x position of the control point. */
- cx: number,
- /** The y position of the control point. */
- cy: number,
- /** The x position of the (padded) ending point. */
- ex: number,
- /** The y position of the (padded) ending point. */
- ey: number,
- /** The angle (in radians) for an ending arrowhead. */
- ae: number,
- /** The angle (in radians) for a starting arrowhead. */
- as: number,
- /** The angle (in radians) for a center arrowhead. */
- ac: number
-];
-
-export class ScopedPropagator extends AbstractArrow {
- static tagName = 'scoped-propagator';
-
- #options: StrokeOptions = {
- size: 10,
- thinning: 0.5,
- smoothing: 0.5,
- streamline: 0.5,
- simulatePressure: true,
- // TODO: figure out how to expose these as attributes
- easing: (t) => t,
- start: {
- taper: 50,
- easing: (t) => t,
- cap: true,
- },
- end: {
- taper: 0,
- easing: (t) => t,
- cap: true,
- },
- };
-
- render(sourceRect: DOMRectReadOnly, targetRect: DOMRectReadOnly) {
- const [sx, sy, cx, cy, ex, ey, ae] = getBoxToBoxArrow(
- sourceRect.x,
- sourceRect.y,
- sourceRect.width,
- sourceRect.height,
- targetRect.x,
- targetRect.y,
- targetRect.width,
- targetRect.height
- ) as Arrow;
-
- const points = pointsOnBezierCurves([
- [sx, sy],
- [cx, cy],
- [ex, ey],
- [ex, ey],
- ]);
-
- const stroke = getStroke(points, this.#options);
- const path = getSvgPathFromStroke(stroke);
- this.style.clipPath = `path('${path}')`;
- this.style.backgroundColor = 'black';
- }
-}
-
-function getSvgPathFromStroke(stroke: number[][]): string {
- if (stroke.length === 0) return '';
-
- for (const point of stroke) {
- point[0] = Math.round(point[0] * 100) / 100;
- point[1] = Math.round(point[1] * 100) / 100;
- }
-
- const d = stroke.reduce(
- (acc, [x0, y0], i, arr) => {
- const [x1, y1] = arr[(i + 1) % arr.length];
- acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2);
- return acc;
- },
- ['M', ...stroke[0], 'Q']
- );
-
- d.push('Z');
- return d.join(' ');
-}