@johnhenry/math-sample 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,150 @@
1
+ /** A rectangle of data space: the part of the plane a plot currently shows. */
2
+ export interface Viewport {
3
+ xMin: number;
4
+ xMax: number;
5
+ yMin: number;
6
+ yMax: number;
7
+ }
8
+
9
+ /** Data x to a screen coordinate in `[0, width]` (left to right). */
10
+ export function toScreenX(x: number, viewport: Viewport, width: number): number {
11
+ return ((x - viewport.xMin) / (viewport.xMax - viewport.xMin)) * width;
12
+ }
13
+
14
+ /** Data y to a screen coordinate in `[0, height]` (screen y grows downward, so this flips). */
15
+ export function toScreenY(y: number, viewport: Viewport, height: number): number {
16
+ return height - ((y - viewport.yMin) / (viewport.yMax - viewport.yMin)) * height;
17
+ }
18
+
19
+ /** Inverse of {@link toScreenX}. */
20
+ export function toDataX(sx: number, viewport: Viewport, width: number): number {
21
+ return viewport.xMin + (sx / width) * (viewport.xMax - viewport.xMin);
22
+ }
23
+
24
+ /** Inverse of {@link toScreenY}. */
25
+ export function toDataY(sy: number, viewport: Viewport, height: number): number {
26
+ return viewport.yMin + ((height - sy) / height) * (viewport.yMax - viewport.yMin);
27
+ }
28
+
29
+ /**
30
+ * The viewport of the given data-space span in which data point
31
+ * (`anchorX`, `anchorY`) sits under screen point (`sx`, `sy`). Panning,
32
+ * wheel-zoom and pinch-zoom all reduce to this: panning keeps the span and
33
+ * moves the anchor's screen position; zooming keeps the anchor's screen
34
+ * position and changes the span.
35
+ */
36
+ export function viewportFromAnchor(
37
+ anchorX: number,
38
+ anchorY: number,
39
+ sx: number,
40
+ sy: number,
41
+ spanX: number,
42
+ spanY: number,
43
+ width: number,
44
+ height: number,
45
+ ): Viewport {
46
+ const xMin = anchorX - (sx / width) * spanX;
47
+ const yMin = anchorY - ((height - sy) / height) * spanY;
48
+ return { xMin, xMax: xMin + spanX, yMin, yMax: yMin + spanY };
49
+ }
50
+
51
+ /**
52
+ * Pan by a pointer drag of (`dxPx`, `dyPx`) screen pixels: the content follows
53
+ * the pointer, so dragging right moves the window left and dragging down moves
54
+ * it up. Pixels are whatever unit `width`/`height` use (CSS px or device px,
55
+ * as long as they agree).
56
+ */
57
+ export function panViewport(viewport: Viewport, dxPx: number, dyPx: number, width: number, height: number): Viewport {
58
+ const dx = (dxPx / width) * (viewport.xMax - viewport.xMin);
59
+ const dy = (dyPx / height) * (viewport.yMax - viewport.yMin);
60
+ return {
61
+ xMin: viewport.xMin - dx,
62
+ xMax: viewport.xMax - dx,
63
+ yMin: viewport.yMin + dy,
64
+ yMax: viewport.yMax + dy,
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Scale both spans by `factor` (below 1 zooms in, above 1 zooms out) while
70
+ * keeping the data point under screen point (`sx`, `sy`) fixed there.
71
+ */
72
+ export function zoomViewport(
73
+ viewport: Viewport,
74
+ factor: number,
75
+ sx: number,
76
+ sy: number,
77
+ width: number,
78
+ height: number,
79
+ ): Viewport {
80
+ const ax = toDataX(sx, viewport, width);
81
+ const ay = toDataY(sy, viewport, height);
82
+ return viewportFromAnchor(
83
+ ax,
84
+ ay,
85
+ sx,
86
+ sy,
87
+ (viewport.xMax - viewport.xMin) * factor,
88
+ (viewport.yMax - viewport.yMin) * factor,
89
+ width,
90
+ height,
91
+ );
92
+ }
93
+
94
+ /** Wheel step: scrolling down (`deltaY > 0`) zooms out (factor above 1), up zooms in. Feed to {@link zoomViewport}. */
95
+ export function wheelZoomFactor(deltaY: number, step = 1.1): number {
96
+ return deltaY > 0 ? step : 1 / step;
97
+ }
98
+
99
+ /** Pinch span factor: fingers moving apart zoom in (factor below 1). Feed to {@link zoomViewport}. */
100
+ export function pinchZoomFactor(startDistancePx: number, currentDistancePx: number): number {
101
+ return startDistancePx / currentDistancePx;
102
+ }
103
+
104
+ /**
105
+ * The viewport that bounds the finite `(x, y)` points, grown by `padding` (a
106
+ * fraction of each span, default 0.05). A degenerate extent (one point, or a
107
+ * constant) is widened to a span of 2 so the result is always drawable.
108
+ * Non-finite values (gaps) are ignored. With no finite points, returns `[-1, 1]^2`.
109
+ */
110
+ export function fitViewport(x: ArrayLike<number>, y: ArrayLike<number>, padding = 0.05): Viewport {
111
+ let xMin = Number.POSITIVE_INFINITY;
112
+ let xMax = Number.NEGATIVE_INFINITY;
113
+ let yMin = Number.POSITIVE_INFINITY;
114
+ let yMax = Number.NEGATIVE_INFINITY;
115
+ const n = Math.min(x.length, y.length);
116
+ for (let i = 0; i < n; i++) {
117
+ const px = x[i];
118
+ const py = y[i];
119
+ if (!Number.isFinite(px) || !Number.isFinite(py)) continue;
120
+ if (px < xMin) xMin = px;
121
+ if (px > xMax) xMax = px;
122
+ if (py < yMin) yMin = py;
123
+ if (py > yMax) yMax = py;
124
+ }
125
+ if (xMin > xMax) return { xMin: -1, xMax: 1, yMin: -1, yMax: 1 };
126
+ const pad = (lo: number, hi: number): [number, number] => {
127
+ if (hi === lo) return [lo - 1, hi + 1];
128
+ const p = (hi - lo) * padding;
129
+ return [lo - p, hi + p];
130
+ };
131
+ const [x0, x1] = pad(xMin, xMax);
132
+ const [y0, y1] = pad(yMin, yMax);
133
+ return { xMin: x0, xMax: x1, yMin: y0, yMax: y1 };
134
+ }
135
+
136
+ /**
137
+ * Canvas backing-store size for a CSS-pixel box at a device pixel ratio:
138
+ * set `canvas.width/height` to `width`/`height` and `ctx.scale(scale, scale)`,
139
+ * then draw in CSS pixels. A non-finite or non-positive `dpr` is treated as 1.
140
+ * Sample against the CSS size (or the backing size) consistently; the
141
+ * viewport maths is unit-agnostic.
142
+ */
143
+ export function backingStoreSize(
144
+ cssWidth: number,
145
+ cssHeight: number,
146
+ dpr: number,
147
+ ): { width: number; height: number; scale: number } {
148
+ const scale = Number.isFinite(dpr) && dpr > 0 ? dpr : 1;
149
+ return { width: Math.round(cssWidth * scale), height: Math.round(cssHeight * scale), scale };
150
+ }