@heyramzi/ui 2.22.0 → 2.22.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heyramzi/ui",
3
- "version": "2.22.0",
3
+ "version": "2.22.1",
4
4
  "description": "Svelte 5 UI for apps: primitives, effects and animations, plus work (tasks and docs), charts, slides, an editor, onboarding and a canvas as subpaths.",
5
5
  "homepage": "https://ui.heyramzi.com",
6
6
  "license": "MIT",
@@ -16,6 +16,9 @@
16
16
  color?: string;
17
17
  }
18
18
 
19
+ /** x, y, width, height in device pixels. */
20
+ type Rect = [number, number, number, number];
21
+
19
22
  // =========================
20
23
  // CONSTANTS
21
24
  // =========================
@@ -58,6 +61,16 @@
58
61
  // Pan redraws every frame, so a tile is built once per size and colour.
59
62
  // eslint-disable-next-line svelte/prefer-svelte-reactivity -- render cache written inside the draw effect, never rendered
60
63
  const tiles = new Map<string, HTMLCanvasElement>();
64
+ // What the canvas holds now, its origin in device pixels. Plain: only the draw effect reads it.
65
+ let painted: {
66
+ zoom: number;
67
+ ratio: number;
68
+ fill: string;
69
+ w: number;
70
+ h: number;
71
+ x: number;
72
+ y: number;
73
+ } | null = null;
61
74
 
62
75
  function dotTile(step: number, radius: number, fill: string) {
63
76
  const key = `${step}:${radius.toFixed(2)}:${fill}`;
@@ -77,21 +90,20 @@
77
90
  return tile;
78
91
  }
79
92
 
93
+ /** Fills the given device-pixel rects with the dots, the grid's origin at `originX`, `originY`. */
80
94
  // fallow-ignore-next-line complexity
81
- function draw(
82
- element: HTMLCanvasElement,
83
- view: { x: number; y: number; zoom: number },
95
+ function paint(
96
+ context: CanvasRenderingContext2D,
97
+ zoom: number,
84
98
  fill: string,
99
+ originX: number,
100
+ originY: number,
101
+ rects: Rect[],
85
102
  ) {
86
- const context = element.getContext("2d");
87
- if (!context) return;
88
- context.setTransform(1, 0, 0, 1, 0, 0);
89
- context.clearRect(0, 0, element.width, element.height);
90
-
91
103
  for (let level = 0; level < LEVELS; level++) {
92
- const spacing = gap * 2 ** level * view.zoom;
104
+ const spacing = gap * 2 ** level * zoom;
93
105
  // A coarser level draws bigger dots, so zooming out reads as moving away.
94
- const screenRadius = (size / 2) * 2 ** level * view.zoom;
106
+ const screenRadius = (size / 2) * 2 ** level * zoom;
95
107
  const radius = Math.min(MAX_RADIUS, Math.max(MIN_RADIUS, screenRadius)) * ratio;
96
108
  const last = level === LEVELS - 1;
97
109
  const alpha = last ? 1 : smoothstep(FADE_START, FADE_END, spacing);
@@ -102,16 +114,64 @@
102
114
  const pattern = context.createPattern(dotTile(tileSize, radius / scale, fill), "repeat");
103
115
  if (!pattern) return;
104
116
  pattern.setTransform(
105
- new DOMMatrix([scale, 0, 0, scale, view.x * ratio - step / 2, view.y * ratio - step / 2]),
117
+ new DOMMatrix([scale, 0, 0, scale, originX - step / 2, originY - step / 2]),
106
118
  );
107
119
  context.globalAlpha = alpha;
108
120
  context.fillStyle = pattern;
109
- context.fillRect(0, 0, element.width, element.height);
121
+ for (const [x, y, w, h] of rects) context.fillRect(x, y, w, h);
110
122
  if (alpha >= 0.999) break;
111
123
  }
112
124
  context.globalAlpha = 1;
113
125
  }
114
126
 
127
+ /** The strips a shift of `dx`, `dy` uncovers: a column on one side and a row on the other. */
128
+ function uncovered(dx: number, dy: number, w: number, h: number): Rect[] {
129
+ const rects: Rect[] = [];
130
+ if (dx > 0) rects.push([0, 0, dx, h]);
131
+ if (dx < 0) rects.push([w + dx, 0, -dx, h]);
132
+ if (dy > 0) rects.push([0, 0, w, dy]);
133
+ if (dy < 0) rects.push([0, h + dy, w, -dy]);
134
+ return rects;
135
+ }
136
+
137
+ // A pan moves the pixels already drawn and paints only the strip it uncovered; anything else repaints it all.
138
+ // fallow-ignore-next-line complexity
139
+ function draw(
140
+ element: HTMLCanvasElement,
141
+ view: { x: number; y: number; zoom: number },
142
+ fill: string,
143
+ ) {
144
+ const context = element.getContext("2d");
145
+ if (!context) return;
146
+ const { width: w, height: h } = element;
147
+ const x = view.x * ratio;
148
+ const y = view.y * ratio;
149
+ const last = painted;
150
+ const same =
151
+ last &&
152
+ last.zoom === view.zoom &&
153
+ last.ratio === ratio &&
154
+ last.fill === fill &&
155
+ last.w === w &&
156
+ last.h === h;
157
+ // Whole pixels only, so the shifted dots stay sharp; the rest waits for the next pan or zoom, never past half a pixel.
158
+ const dx = same ? Math.round(x - last.x) : 0;
159
+ const dy = same ? Math.round(y - last.y) : 0;
160
+ if (same && Math.abs(dx) < w && Math.abs(dy) < h) {
161
+ if (!dx && !dy) return;
162
+ context.globalCompositeOperation = "copy";
163
+ context.drawImage(element, dx, dy);
164
+ context.globalCompositeOperation = "source-over";
165
+ painted = { ...last, x: last.x + dx, y: last.y + dy };
166
+ paint(context, view.zoom, fill, painted.x, painted.y, uncovered(dx, dy, w, h));
167
+ return;
168
+ }
169
+ context.setTransform(1, 0, 0, 1, 0, 0);
170
+ context.clearRect(0, 0, w, h);
171
+ painted = { zoom: view.zoom, ratio, fill, w, h, x, y };
172
+ paint(context, view.zoom, fill, x, y, [[0, 0, w, h]]);
173
+ }
174
+
115
175
  // =========================
116
176
  // EFFECTS
117
177
  // =========================