From 125fb5fd5f4ea80a9414f0a459a0dc325e594903 Mon Sep 17 00:00:00 2001 From: Kai Sassnowski Date: Wed, 21 Aug 2024 11:02:36 +0200 Subject: [PATCH] feat: add cursors interactivity to components --- src/components/Circle.vue | 6 + src/components/Ellipse.vue | 10 ++ src/components/Graph.vue | 24 ++- src/components/Label.vue | 9 + src/components/Line.vue | 14 ++ src/components/Point.vue | 22 ++- src/components/PolyLine.vue | 15 ++ src/components/Polygon.vue | 6 + src/components/Sector.vue | 9 + src/components/Vector.vue | 16 +- src/composables/usePointerIntersection.ts | 20 +++ src/types.ts | 3 +- src/utils/geometry.ts | 205 ++++++++++++++++++++++ 13 files changed, 355 insertions(+), 4 deletions(-) create mode 100644 src/composables/usePointerIntersection.ts create mode 100644 src/utils/geometry.ts diff --git a/src/components/Circle.vue b/src/components/Circle.vue index 3770972..9f0ccca 100644 --- a/src/components/Circle.vue +++ b/src/components/Circle.vue @@ -18,6 +18,8 @@ import { type PossibleVector2, Vector2 } from "../utils/Vector2.ts"; import { useGraphContext } from "../composables/useGraphContext.ts"; import { type Color } from "../types.ts"; import { useColors } from "../composables/useColors.ts"; +import { usePointerIntersection } from "../composables/usePointerIntersection.ts"; +import { pointInsideCircle } from "../utils/geometry.ts"; const props = withDefaults( defineProps<{ @@ -40,6 +42,10 @@ const { parseColor } = useColors(); const stroke = parseColor(toRef(props, "color"), "stroke"); const fill = parseColor(toRef(props, "fill")); +const active = defineModel("active", { default: false }); +usePointerIntersection(active, (point) => + pointInsideCircle(Vector2.wrap(props.position), props.radius, point), +); const position = computed(() => new Vector2(props.position).transform(matrix.value), diff --git a/src/components/Ellipse.vue b/src/components/Ellipse.vue index d1f1f69..f7832f7 100644 --- a/src/components/Ellipse.vue +++ b/src/components/Ellipse.vue @@ -20,6 +20,8 @@ import { type PossibleVector2, Vector2 } from "../utils/Vector2.ts"; import { useGraphContext } from "../composables/useGraphContext.ts"; import { type Color } from "../types.ts"; import { useColors } from "../composables/useColors.ts"; +import { usePointerIntersection } from "../composables/usePointerIntersection.ts"; +import { pointInsideEllipse } from "../utils/geometry.ts"; const props = withDefaults( defineProps<{ @@ -47,6 +49,14 @@ const { parseColor } = useColors(); const stroke = parseColor(toRef(props, "color"), "stroke"); const fill = parseColor(toRef(props, "fill")); +const active = defineModel("active", { default: false }); +usePointerIntersection(active, (point) => + pointInsideEllipse( + Vector2.wrap(props.position), + Vector2.wrap(props.radius), + point, + ), +); const position = computed(() => new Vector2(props.position).transform(matrix.value), diff --git a/src/components/Graph.vue b/src/components/Graph.vue index ab20068..196f833 100644 --- a/src/components/Graph.vue +++ b/src/components/Graph.vue @@ -5,6 +5,7 @@ :width="size.x" :height="size.y" xmlns="http://www.w3.org/2000/svg" + v-on="interactive ? { mousemove: onMouseMove } : {}" > @@ -134,6 +135,7 @@ const props = withDefaults( axis?: boolean; grid?: boolean; units?: boolean; + interactive?: boolean; }>(), { width: 300, @@ -145,13 +147,15 @@ const props = withDefaults( axis: true, grid: true, units: true, + interactive: false, }, ); const id = Math.random().toString(16).slice(2); const { colors } = useColors(); -const el = ref(); +const el = ref(); const containerSize = ref(new Vector2(props.width, props.height)); +const svgPoint = ref(); const origin = computed(() => { if (props.origin) { @@ -189,6 +193,16 @@ const matrixWorld = computed(() => { return matrix; }); +const cursor = computed(() => { + if (!svgPoint.value) { + return null; + } + const pos = svgPoint.value!.matrixTransform( + el.value!.getScreenCTM()!.inverse(), + ); + return new Vector2(pos.x, pos.y).transform(matrixWorld.value.inverse); +}); + const context = { size, scale, @@ -196,6 +210,7 @@ const context = { offset, domain, invScale, + cursor, matrix: matrixWorld, }; @@ -205,6 +220,13 @@ function formatLabelValue(value: number) { return value.toFixed(2).replace(/\.?0+$/, ""); } +function onMouseMove(event: MouseEvent) { + const point = el.value!.createSVGPoint(); + point.x = event.clientX; + point.y = event.clientY; + svgPoint.value = point; +} + onMounted(() => { const observer = new ResizeObserver((entries) => { const entry = entries[0]; diff --git a/src/components/Label.vue b/src/components/Label.vue index 2cedd30..7b1ec52 100644 --- a/src/components/Label.vue +++ b/src/components/Label.vue @@ -32,6 +32,8 @@ import { type Color } from "../types.ts"; import { type PossibleVector2, Vector2 } from "../utils/Vector2.ts"; import { useGraphContext } from "../composables/useGraphContext.ts"; import { useColors } from "../composables/useColors.ts"; +import { usePointerIntersection } from "../composables/usePointerIntersection.ts"; +import { pointInsideRectangle } from "../utils/geometry.ts"; const props = withDefaults( defineProps<{ @@ -66,6 +68,13 @@ const { matrix, invScale } = useGraphContext(); const { colors, parseColor } = useColors(); const color = parseColor(toRef(props, "color"), "stroke"); +const active = defineModel("active", { default: false }); +usePointerIntersection(active, (point) => { + const center = Vector2.wrap(props.position); + const width = boxWidth.value / matrix.value.a; + const height = boxHeight.value / matrix.value.a; + return pointInsideRectangle(center, new Vector2(width, height), point); +}); const position = computed(() => Vector2.wrap(props.position).transform(matrix.value), diff --git a/src/components/Line.vue b/src/components/Line.vue index 9b9437b..94a1394 100644 --- a/src/components/Line.vue +++ b/src/components/Line.vue @@ -27,6 +27,8 @@ import { PossibleVector2, Vector2 } from "../utils/Vector2.ts"; import Label from "./Label.vue"; import { useGraphContext } from "../composables/useGraphContext.ts"; import { useColors } from "../composables/useColors.ts"; +import { usePointerIntersection } from "../composables/usePointerIntersection.ts"; +import { distanceToLineSegment } from "../utils/geometry.ts"; const props = withDefaults( defineProps<{ @@ -39,12 +41,14 @@ const props = withDefaults( lineWidth?: number; label?: string; labelSize?: "small" | "normal" | "large"; + highlightThreshold?: number; }>(), { dashed: false, lineWidth: 1.75, yIntercept: 0, labelSize: "small", + highlightThreshold: 0.25, }, ); @@ -56,6 +60,16 @@ const { domain, matrix, invScale } = useGraphContext(); const { parseColor } = useColors(); const color = parseColor(toRef(props, "color"), "stroke"); +const active = defineModel("active", { default: false }); +usePointerIntersection( + active, + (point) => + distanceToLineSegment( + from.value.transform(matrix.value.inverse), + to.value.transform(matrix.value.inverse), + point, + ) <= props.highlightThreshold, +); function clamp(x: number, min: number, max: number) { return Math.min(max, Math.max(min, x)); diff --git a/src/components/Point.vue b/src/components/Point.vue index 927b029..53201fc 100644 --- a/src/components/Point.vue +++ b/src/components/Point.vue @@ -11,6 +11,7 @@