Skip to content

Commit

Permalink
feat: add cursors interactivity to components
Browse files Browse the repository at this point in the history
  • Loading branch information
ksassnowski committed Aug 22, 2024
1 parent 5a6a025 commit 125fb5f
Show file tree
Hide file tree
Showing 13 changed files with 355 additions and 4 deletions.
6 changes: 6 additions & 0 deletions src/components/Circle.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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<{
Expand All @@ -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),
Expand Down
10 changes: 10 additions & 0 deletions src/components/Ellipse.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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<{
Expand Down Expand Up @@ -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),
Expand Down
24 changes: 23 additions & 1 deletion src/components/Graph.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
:width="size.x"
:height="size.y"
xmlns="http://www.w3.org/2000/svg"
v-on="interactive ? { mousemove: onMouseMove } : {}"
>
<defs>
<clipPath :id="`clip-${id}`">
Expand Down Expand Up @@ -134,6 +135,7 @@ const props = withDefaults(
axis?: boolean;
grid?: boolean;
units?: boolean;
interactive?: boolean;
}>(),
{
width: 300,
Expand All @@ -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<SVGElement | null>();
const el = ref<SVGSVGElement | null>();
const containerSize = ref(new Vector2(props.width, props.height));
const svgPoint = ref<SVGPoint | null>();
const origin = computed(() => {
if (props.origin) {
Expand Down Expand Up @@ -189,13 +193,24 @@ const matrixWorld = computed(() => {
return matrix;
});
const cursor = computed<Vector2 | null>(() => {
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,
origin,
offset,
domain,
invScale,
cursor,
matrix: matrixWorld,
};
Expand All @@ -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];
Expand Down
9 changes: 9 additions & 0 deletions src/components/Label.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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<{
Expand Down Expand Up @@ -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),
Expand Down
14 changes: 14 additions & 0 deletions src/components/Line.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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<{
Expand All @@ -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,
},
);
Expand All @@ -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));
Expand Down
22 changes: 21 additions & 1 deletion src/components/Point.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

<Label
v-if="label"
v-model:active="labelActive"
:text="label"
:position="labelPosition"
:color="color"
Expand All @@ -20,13 +21,15 @@
</template>

<script setup lang="ts">
import { computed, toRef } from "vue";
import { ref, computed, toRef } from "vue";
import { type Color } from "../types.ts";
import { type 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 { pointInsideCircle } from "../utils/geometry.ts";
type LabelPosition = "top" | "bottom" | "left" | "right";
Expand All @@ -39,19 +42,36 @@ const props = withDefaults(
label?: string;
filled?: boolean;
lineWidth?: number;
highlightThreshold?: number;
}>(),
{
radius: 4,
position: () => new Vector2(),
labelPosition: "bottom",
filled: true,
lineWidth: 1.5,
highlightThreshold: 0.1,
},
);
const { matrix, invScale } = useGraphContext();
const { parseColor } = useColors();
const color = parseColor(toRef(props, "color"), "points");
const labelActive = ref(false);
const active = defineModel("active", { default: false });
usePointerIntersection(active, (point) => {
const center = Vector2.wrap(props.position);
const pointActive = pointInsideCircle(
center,
(props.radius + props.lineWidth) / matrix.value.a +
props.highlightThreshold,
point,
);
if (!props.label) {
return pointActive;
}
return labelActive.value || pointActive;
});
const padding = 25;
const position = computed(() => new Vector2(props.position));
Expand Down
15 changes: 15 additions & 0 deletions src/components/PolyLine.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,39 @@ import { type Color } from "../types.ts";
import { 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 { distanceToLineSegment } from "../utils/geometry.ts";
const props = withDefaults(
defineProps<{
points: PossibleVector2[];
color?: Color;
lineWidth?: number;
dashed?: boolean;
highlightThreshold?: number;
}>(),
{
dashed: false,
lineWidth: 1.5,
highlightThreshold: 0.25,
},
);
const { matrix, invScale } = useGraphContext();
const { parseColor } = useColors();
const color = parseColor(toRef(props, "color"), "stroke");
const active = defineModel("active", { default: false });
usePointerIntersection(active, (point) => {
for (let i = 0; i < props.points.length - 1; i++) {
const p0 = Vector2.wrap(props.points[i]);
const p1 = Vector2.wrap(props.points[i + 1]);
if (distanceToLineSegment(p0, p1, point) <= props.highlightThreshold) {
return true;
}
}
return false;
});
const parsedPoints = computed(() =>
props.points.map((point) => Vector2.wrap(point).transform(matrix.value)),
Expand Down
6 changes: 6 additions & 0 deletions src/components/Polygon.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import Angle from "./Angle.vue";
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 { pointInsidePolygon } from "../utils/geometry.ts";
const props = withDefaults(
defineProps<{
Expand Down Expand Up @@ -55,6 +57,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) =>
pointInsidePolygon(vertices.value, point),
);
const vertices = computed(() => props.vertices.map((v) => Vector2.wrap(v)));
const points = computed(() =>
Expand Down
9 changes: 9 additions & 0 deletions src/components/Sector.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { PossibleVector2, Vector2 } from "../utils/Vector2.ts";
import { DEG2RAD } from "../utils/constants.ts";
import { useGraphContext } from "../composables/useGraphContext.ts";
import { useColors } from "../composables/useColors.ts";
import { usePointerIntersection } from "../composables/usePointerIntersection.ts";
import { pointInsideSector } from "../utils/geometry.ts";
const props = withDefaults(
defineProps<{
Expand All @@ -44,6 +46,13 @@ const props = withDefaults(
const { matrix, invScale } = useGraphContext();
const { color, fill } = toRefs(props);
const { parseColor } = useColors();
const active = defineModel("active", { default: false });
usePointerIntersection(active, (point) => {
const b = Vector2.wrap(props.position);
const a = Vector2.fromPolar(from.value, props.radius).add(b);
const c = Vector2.fromPolar(to.value, props.radius).add(b);
return pointInsideSector(a, b, c, props.radius, point);
});
const strokeColor = parseColor(color, "stroke");
const fillColor = parseColor(fill);
Expand Down
16 changes: 15 additions & 1 deletion src/components/Vector.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,14 @@
</template>

<script setup lang="ts">
import { computed} from "vue";
import { computed } from "vue";
import { type PossibleVector2, Vector2 } from "../utils/Vector2.ts";
import Label from "../components/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<{
Expand All @@ -55,13 +57,15 @@ const props = withDefaults(
labelSize?: "small" | "normal" | "large";
lineWidth?: number;
arrowSize?: number;
highlightThreshold?: number;
}>(),
{
from: () => new Vector2(),
dashed: false,
lineWidth: 1.75,
labelSize: "normal",
arrowSize: 18,
highlightThreshold: 0.25,
},
);
Expand All @@ -70,6 +74,16 @@ const id = Math.random().toString(16).slice(2);
const { matrix, invScale } = useGraphContext();
const { colors } = useColors();
const color = computed(() => props.color ?? colors.value.stroke);
const active = defineModel("active", { default: false });
usePointerIntersection(
active,
(point) =>
distanceToLineSegment(
Vector2.wrap(props.from),
Vector2.wrap(props.to),
point,
) <= props.highlightThreshold,
);
const pixelVector = computed(() =>
Vector2.wrap(props.to).transform(matrix.value).sub(from.value),
Expand Down
20 changes: 20 additions & 0 deletions src/composables/usePointerIntersection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Vector2 } from "src";
import { useGraphContext } from "./useGraphContext";
import { Ref, watch } from "vue";

export type IntersectionFn = (pointer: Vector2) => boolean;

export function usePointerIntersection(
ref: Ref<boolean>,
intersectionFn: IntersectionFn,
) {
const { cursor } = useGraphContext();

watch(cursor, (position: Vector2 | null) => {
if (!position) {
ref.value = false;
return;
}
ref.value = intersectionFn(position);
});
}
Loading

0 comments on commit 125fb5f

Please sign in to comment.