@weasel-js/loupe 1.9.4 → 1.10.0
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/README.md +2 -2
- package/dist/index.js +4 -10
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
The magnifier's model, with no surface attached: where it is aimed, how far it
|
|
4
4
|
magnifies, whether it is showing re-rendered content or actual pixels, and what
|
|
5
|
-
|
|
5
|
+
color it is over.
|
|
6
6
|
|
|
7
7
|
A **painter** draws it on one kind of surface and answers the few questions the
|
|
8
|
-
model asks — does the lens cover this point, what
|
|
8
|
+
model asks — does the lens cover this point, what color is here, repaint. The
|
|
9
9
|
painters ship with the surfaces they know: `@weasel-js/hud` draws one into a
|
|
10
10
|
WebGL canvas, `@weasel-js/labkit` draws one over a lab's own content.
|
|
11
11
|
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalizeZoom } from '@weasel-js/geom';
|
|
2
|
+
import { createEmitter } from '@weasel-js/registry';
|
|
2
3
|
|
|
3
4
|
// src/model.ts
|
|
4
5
|
|
|
@@ -127,8 +128,6 @@ function createLoupeModel(opts) {
|
|
|
127
128
|
dispose: teardown
|
|
128
129
|
};
|
|
129
130
|
}
|
|
130
|
-
|
|
131
|
-
// src/canvasSource.ts
|
|
132
131
|
var MISSED_FRAMES_BEFORE_WARNING = 120;
|
|
133
132
|
function isWebGL(ctx) {
|
|
134
133
|
return "drawArrays" in ctx;
|
|
@@ -156,7 +155,7 @@ function createCanvasSource(target, options = {}) {
|
|
|
156
155
|
let missed = 0;
|
|
157
156
|
let warned = false;
|
|
158
157
|
let probe = null;
|
|
159
|
-
const
|
|
158
|
+
const frames = createEmitter();
|
|
160
159
|
const source = {
|
|
161
160
|
canvas,
|
|
162
161
|
get captured() {
|
|
@@ -179,7 +178,7 @@ function createCanvasSource(target, options = {}) {
|
|
|
179
178
|
ctx.drawImage(canvas, 0, 0);
|
|
180
179
|
current = true;
|
|
181
180
|
missed = 0;
|
|
182
|
-
|
|
181
|
+
frames.emit();
|
|
183
182
|
},
|
|
184
183
|
frame() {
|
|
185
184
|
if (!captured) return canvas.width > 0 && canvas.height > 0 ? canvas : null;
|
|
@@ -222,12 +221,7 @@ function createCanvasSource(target, options = {}) {
|
|
|
222
221
|
readers--;
|
|
223
222
|
};
|
|
224
223
|
},
|
|
225
|
-
subscribeFrame
|
|
226
|
-
listeners.add(fn);
|
|
227
|
-
return () => {
|
|
228
|
-
listeners.delete(fn);
|
|
229
|
-
};
|
|
230
|
-
}
|
|
224
|
+
subscribeFrame: frames.subscribe
|
|
231
225
|
};
|
|
232
226
|
return source;
|
|
233
227
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/geometry.ts","../src/model.ts","../src/canvasSource.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { View } from '@weasel-js/geom';\n\n/** A point on the magnified surface, in its own CSS pixels. */\nexport interface LoupePoint {\n x: number;\n y: number;\n}\n\n/** How big a lens is, in CSS px: one number for a round or square lens, or a\n * width and a height. */\nexport type LoupeSize = number | { width: number; height: number };\n\n/** `size` as a width and a height. */\nexport function loupeExtent(size: LoupeSize): { width: number; height: number } {\n return typeof size === 'number' ? { width: size, height: size } : size;\n}\n\n/** The lens' rectangle on that surface. */\nexport interface LoupeRect extends LoupePoint {\n w: number;\n h: number;\n}\n\n/**\n * The inner `View` a loupe renders its content through: the outer view's\n * magnification times `factor`, positioned so `target` sits at the center of\n * a viewport the size of `rect`.\n */\nexport function loupeInnerView(\n target: LoupePoint,\n outer: View,\n rect: LoupeRect,\n factor: number,\n): View {\n const scale = { x: outer.scale.x * factor, y: outer.scale.y * factor };\n return {\n x: target.x - rect.w / 2 / scale.x,\n y: target.y - rect.h / 2 / scale.y,\n scale,\n };\n}\n\n/**\n * Where on the outer surface a point inside the lens is looking, in\n * screen-space CSS px.\n *\n * The inverse of {@link loupeInnerView}: the lens centers `aim` in `rect` and\n * magnifies by `factor`, and the outer view's own scale cancels out of the\n * round trip — so this needs no `View`.\n */\nexport function loupeSourcePoint(\n p: LoupePoint,\n rect: LoupeRect,\n aim: LoupePoint,\n factor: number,\n): LoupePoint {\n return {\n x: aim.x + (p.x - (rect.x + rect.w / 2)) / factor,\n y: aim.y + (p.y - (rect.y + rect.h / 2)) / factor,\n };\n}\n\n/** What {@link placeBand} fits. */\nexport interface PlaceBandArgs {\n /** The region to show whole, in host CSS px. */\n band: LoupeRect;\n /** The most it may be magnified: the lens's own factor. */\n factor: number;\n /** The host's width: the lens is never wider, and is moved to stay inside it. */\n maxWidth: number;\n /** The host's height, likewise. Omitted, the height is not limited. */\n maxHeight?: number;\n}\n\n/** A lens fitted to a band, in the shape a loupe's `place` returns. */\nexport interface BandPlacement {\n /** Where the lens is drawn, moved as far as staying on the host needs. */\n center: LoupePoint;\n /** The band's center, which the lens shows at its middle wherever it is drawn. */\n shows: LoupePoint;\n width: number;\n height: number;\n factor: number;\n}\n\n/**\n * A lens showing all of `band`: magnified by `factor`, or by less where that\n * would make the lens wider than `maxWidth` or taller than `maxHeight`, and\n * drawn over the band except as far as it must move to stay on the host.\n */\nexport function placeBand({ band, factor, maxWidth, maxHeight }: PlaceBandArgs): BandPlacement {\n let fit = factor;\n if (band.w > 0) fit = Math.min(fit, maxWidth / band.w);\n if (maxHeight !== undefined && band.h > 0) fit = Math.min(fit, maxHeight / band.h);\n const width = band.w * fit;\n const height = band.h * fit;\n const shows = { x: band.x + band.w / 2, y: band.y + band.h / 2 };\n const within = (c: number, half: number, max: number): number =>\n Math.min(Math.max(c, half), Math.max(half, max - half));\n return {\n center: {\n x: within(shows.x, width / 2, maxWidth),\n y: maxHeight === undefined ? shows.y : within(shows.y, height / 2, maxHeight),\n },\n shows,\n width,\n height,\n factor: fit,\n };\n}\n","import { normalizeZoom } from '@weasel-js/geom';\nimport { type LoupePoint, type LoupeRect, loupeSourcePoint } from './geometry';\n\n/** How a loupe magnifies. `'vector'` re-renders the source through a zoomed-in\n * view, so content stays sharp at any factor; `'pixel'` blows up the actual\n * pixels the surface presented. */\nexport type LoupeMode = 'vector' | 'pixel';\n\n/**\n * What a loupe needs from the surface it magnifies. A painter implements it,\n * and the model asks nothing about how the lens is drawn — only where it is,\n * what colour is under a point, and whether anyone can still see it.\n */\nexport interface LoupeSurface {\n /**\n * The lens' rectangle, or `null` while it has none. A lens placed somewhere\n * other than centered on the aim also says what it shows: `shows` is the\n * surface point at its middle, and `factor` the magnification it shows it at.\n * Either one omitted is the aim, or the model's own factor.\n */\n lens(): (LoupeRect & { shows?: LoupePoint; factor?: number }) | null;\n /**\n * Does the lens itself cover this point? A stationary lens does over its own\n * frame, which is what the freeze rule below is for; a lens that follows the\n * pointer never covers it and answers `false`.\n */\n covers(p: LoupePoint): boolean;\n /** Hex colour at a surface point, or `null` when the surface cannot say. */\n sample(p: LoupePoint): string | null;\n /** Is the lens off screen? Aims are ignored while it is. */\n hidden(): boolean;\n /** Is the lens gone for good? The model tears itself down when it is. */\n gone(): boolean;\n /** Something the painter must redraw for has changed. */\n changed(): void;\n /**\n * Run `fn` after each frame the surface lands; returns an unsubscribe.\n * Implement it when `sample` reads pixels that only show an aim once a\n * frame has painted — a framebuffer read back. The model then samples the\n * aim on the next landed frame, so `color` settles a frame after the aim.\n * Omit it when `sample` can answer for a fresh aim right away.\n */\n subscribeFrame?(fn: () => void): () => void;\n}\n\n/** Options for {@link createLoupeModel}. */\nexport interface LoupeModelOptions {\n surface: LoupeSurface;\n mode?: LoupeMode;\n /** Magnification. Default 8. Always positive and finite: a factor that is\n * not becomes `ZOOM_FLOOR`, the same rule a view's zoom follows. */\n factor?: number;\n /** Bounds `setFactor` clamps to. Unset means bounded only by that rule. */\n minFactor?: number;\n maxFactor?: number;\n /** Called with the hex colour under the aim point whenever it changes. */\n onColorChange?: (hex: string) => void;\n /** Called when a pick lands — an eyedropper on the magnified surface. Where\n * the colour goes is the consumer's business. */\n onPick?: (hex: string) => void;\n /** Called when the model tears itself down because the surface reported the\n * lens gone, so the painter can drop what it was holding. */\n onDispose?: () => void;\n}\n\n/** Where a loupe is aimed, how far it magnifies, and what colour it is over. */\nexport interface LoupeModel {\n readonly mode: LoupeMode;\n readonly factor: number;\n /** Last aimed point, in the surface's CSS px. */\n readonly aim: LoupePoint;\n /** Hex colour under the aim point. `null` until something answers. */\n readonly color: string | null;\n setMode(mode: LoupeMode): void;\n setFactor(factor: number): void;\n /** Aim at a surface point. Ignored while the lens covers it, is hidden, or\n * the model is disposed. */\n aimAt(p: LoupePoint): void;\n /** Sample what the lens shows at `p`, a point inside it, and report that to\n * `onPick`. Omit `p` to pick at the aim point. Leaves the aim, and `color`,\n * alone. */\n pick(p?: LoupePoint): string | null;\n /** Sample the aim again, now: the pixels under it changed without it moving.\n * Reports through `onColorChange` like an aim does. Ignored while hidden. */\n resample(): void;\n dispose(): void;\n}\n\n/**\n * Build the loupe's model over a surface. It holds the state a magnifier has —\n * aim, factor, mode, colour — and none of the drawing.\n */\nexport function createLoupeModel(opts: LoupeModelOptions): LoupeModel {\n const { surface } = opts;\n let mode: LoupeMode = opts.mode ?? 'vector';\n let factor = normalizeZoom(opts.factor ?? 8);\n let aim: LoupePoint = { x: 0, y: 0 };\n let color: string | null = null;\n let disposed = false;\n let aimUnsampled = false;\n\n const clamp = (n: number): number =>\n normalizeZoom(Math.min(opts.maxFactor ?? Number.POSITIVE_INFINITY,\n Math.max(opts.minFactor ?? Number.NEGATIVE_INFINITY, n)));\n\n const sampleColor = () => {\n const hex = surface.sample(aim);\n if (hex === null || hex === color) return;\n color = hex;\n opts.onColorChange?.(hex);\n };\n\n const unsubscribeFrame = surface.subscribeFrame?.(() => {\n if (!aimUnsampled) return;\n aimUnsampled = false;\n sampleColor();\n });\n\n const teardown = () => {\n if (disposed) return;\n disposed = true;\n unsubscribeFrame?.();\n opts.onDispose?.();\n };\n\n return {\n get mode() { return mode; },\n get factor() { return factor; },\n get aim() { return aim; },\n get color() { return color; },\n\n setMode(next) {\n if (disposed || next === mode) return;\n mode = next;\n surface.changed();\n },\n\n setFactor(next) {\n if (disposed) return;\n factor = clamp(next);\n surface.changed();\n },\n\n aimAt(p) {\n if (disposed) return;\n // A lens can be taken away by whatever owns it, without coming back\n // through `dispose`. Follow it down rather than keep sampling for a\n // magnifier nobody can see.\n if (surface.gone()) { teardown(); return; }\n if (surface.hidden()) return;\n if (surface.covers(p)) return;\n aim = p;\n if (unsubscribeFrame) aimUnsampled = true;\n else sampleColor();\n surface.changed();\n },\n\n pick(p) {\n if (disposed) return null;\n let at = aim;\n if (p) {\n const rect = surface.lens();\n if (!rect) return null;\n at = loupeSourcePoint(p, rect, rect.shows ?? aim, rect.factor ?? factor);\n // The lens is part of the picture the surface presents. A point near\n // its frame maps back underneath it, and sampling there reports the\n // lens' own chrome as artwork.\n if (surface.covers(at)) return null;\n }\n const hex = surface.sample(at);\n if (hex !== null) opts.onPick?.(hex);\n return hex;\n },\n\n resample() {\n if (disposed || surface.hidden()) return;\n aimUnsampled = false;\n sampleColor();\n },\n\n dispose: teardown,\n };\n}\n","import { type LoupePoint, type LoupeSize, loupeExtent } from './geometry';\n\n/** A canvas whose pixels a loupe can read. */\nexport type SourceCanvas = HTMLCanvasElement | OffscreenCanvas;\n\n/** A rendering context on a {@link SourceCanvas}. Passing one, rather than the\n * bare canvas, is what tells a source whether the canvas keeps its pixels. */\nexport type SourceContext =\n | CanvasRenderingContext2D\n | OffscreenCanvasRenderingContext2D\n | WebGLRenderingContext\n | WebGL2RenderingContext\n | ImageBitmapRenderingContext;\n\n/** Where a source canvas's pixels sit on the surface being magnified, in that\n * surface's CSS px. The backing store is stretched over this box, so its\n * width against the canvas's own `width` is the device-pixel ratio. */\nexport interface SourceBox {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/** Options for {@link createCanvasSource}. */\nexport interface CanvasSourceOptions {\n /**\n * Called when a reader starts and there is no current frame to read: draw\n * the canvas again, and call `capture()` after it. Only needed by a canvas\n * that is captured and does not redraw every frame on its own — without it\n * such a lens stays empty until the next redraw.\n */\n requestRedraw?: () => void;\n /** Where the canvas sits on the magnified surface. Omitted, a reader works\n * it out — a painter over the DOM measures the canvas's own box. */\n box?: () => SourceBox | null;\n}\n\n/**\n * Any canvas, as something a loupe reads pixels from: 2D, WebGL or WebGL2,\n * on the page or offscreen, owned by whoever drew it.\n *\n * A WebGL canvas made without `preserveDrawingBuffer` is cleared once the\n * browser composites it, so reading it from any later task — the lens's own\n * frame — returns nothing. Such a source is *captured*: whoever draws the\n * canvas calls {@link capture} right after drawing, in the same task, and the\n * source copies the frame while it still exists. Every other canvas holds its\n * pixels and is read directly.\n */\nexport interface CanvasSource {\n readonly canvas: SourceCanvas;\n /**\n * Whether frames reach readers only through {@link capture}. True for a\n * source made from a WebGL context without `preserveDrawingBuffer`, and for\n * any source once `capture()` has been called on it.\n */\n readonly captured: boolean;\n readonly box?: () => SourceBox | null;\n /**\n * Copy the frame just drawn. Call it in the same task as the draw — right\n * after `gl.drawArrays`, `renderer.render(...)` or whatever finished the\n * frame. Free while nothing is reading: it copies only while a reader holds\n * the source.\n */\n capture(): void;\n /** The pixels to read now: the last capture, or the canvas itself when it\n * keeps its own. `null` while a captured source has no current frame. */\n frame(): CanvasImageSource | null;\n /** Hex color of the backing-store pixel at `p` (device px, not CSS px), or\n * `null` where it is transparent, off the canvas, or there is no frame. */\n sample(p: LoupePoint): string | null;\n /** Start reading. While any reader holds the source, `capture` copies;\n * returns the release. */\n retain(): () => void;\n /** Run `fn` after each capture lands; returns an unsubscribe. */\n subscribeFrame(fn: () => void): () => void;\n}\n\n/** A frame read this many times with no capture behind it means the drawing\n * code is not calling `capture()`. About two seconds of lens frames. */\nconst MISSED_FRAMES_BEFORE_WARNING = 120;\n\nfunction isWebGL(ctx: SourceContext): ctx is WebGLRenderingContext | WebGL2RenderingContext {\n return 'drawArrays' in ctx;\n}\n\nfunction isContext(target: SourceCanvas | SourceContext): target is SourceContext {\n return 'canvas' in target && !('getContext' in target);\n}\n\nfunction makeCanvas(width: number, height: number): SourceCanvas {\n if (typeof OffscreenCanvas !== 'undefined') return new OffscreenCanvas(width, height);\n const c = document.createElement('canvas');\n c.width = width;\n c.height = height;\n return c;\n}\n\ntype Ctx2D = CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;\n\nfunction context2d(canvas: SourceCanvas, willReadFrequently = false): Ctx2D | null {\n return canvas.getContext('2d', { willReadFrequently }) as Ctx2D | null;\n}\n\n/**\n * Make a {@link CanvasSource} for a canvas, or for a context on one.\n *\n * Pass the context when there is one: a WebGL context without\n * `preserveDrawingBuffer` is then known to need capturing before anything has\n * gone blank. A bare canvas is read directly until `capture()` is first\n * called on it. The source never calls `getContext` on the canvas it reads —\n * on a canvas with no context yet, that would create one with the wrong\n * attributes and leave whoever draws it unable to get theirs.\n */\nexport function createCanvasSource(\n target: SourceCanvas | SourceContext,\n options: CanvasSourceOptions = {},\n): CanvasSource {\n const canvas = (isContext(target) ? target.canvas : target) as SourceCanvas;\n const mustCapture =\n isContext(target) && isWebGL(target) && !target.getContextAttributes()?.preserveDrawingBuffer;\n\n let captured = mustCapture;\n let readers = 0;\n let snapshot: SourceCanvas | null = null;\n let current = false;\n let missed = 0;\n let warned = false;\n let probe: Ctx2D | null = null;\n const listeners = new Set<() => void>();\n\n const source: CanvasSource = {\n canvas,\n get captured() {\n return captured;\n },\n box: options.box,\n\n capture() {\n captured = true;\n if (readers === 0) return;\n const { width, height } = canvas;\n if (width === 0 || height === 0) return;\n if (!snapshot) snapshot = makeCanvas(width, height);\n else if (snapshot.width !== width || snapshot.height !== height) {\n snapshot.width = width;\n snapshot.height = height;\n }\n const ctx = context2d(snapshot);\n if (!ctx) return;\n ctx.globalCompositeOperation = 'copy';\n ctx.drawImage(canvas, 0, 0);\n current = true;\n missed = 0;\n for (const fn of listeners) fn();\n },\n\n frame() {\n if (!captured) return canvas.width > 0 && canvas.height > 0 ? canvas : null;\n if (current && snapshot) return snapshot;\n if (!warned && ++missed >= MISSED_FRAMES_BEFORE_WARNING) {\n warned = true;\n console.warn(\n '[weasel loupe] A WebGL canvas without preserveDrawingBuffer is being read, ' +\n 'and nothing has called capture() on its source. Call source.capture() right ' +\n 'after drawing each frame, or pass requestRedraw if the canvas draws on demand.',\n );\n }\n return null;\n },\n\n sample(p) {\n const frame = source.frame();\n if (!frame) return null;\n const x = Math.floor(p.x);\n const y = Math.floor(p.y);\n if (x < 0 || y < 0 || x >= canvas.width || y >= canvas.height) return null;\n if (!probe) {\n const c = makeCanvas(1, 1);\n probe = context2d(c, true);\n if (!probe) return null;\n }\n probe.globalCompositeOperation = 'copy';\n probe.drawImage(frame, x, y, 1, 1, 0, 0, 1, 1);\n const [r, g, b, a] = probe.getImageData(0, 0, 1, 1).data;\n if (a === 0) return null;\n return `#${[r, g, b].map((c) => c.toString(16).padStart(2, '0')).join('')}`;\n },\n\n retain() {\n readers++;\n if (readers === 1 && captured) {\n // Whatever was copied before the last reader left may be long stale.\n current = false;\n options.requestRedraw?.();\n }\n let held = true;\n return () => {\n if (!held) return;\n held = false;\n readers--;\n };\n },\n\n subscribeFrame(fn) {\n listeners.add(fn);\n return () => {\n listeners.delete(fn);\n };\n },\n };\n return source;\n}\n\n/**\n * The rectangle of `source`'s backing store a lens of `size` CSS px\n * magnifying `factor` times about `aim` shows, in device px. `box` is where\n * the canvas sits in the same CSS px as `aim`; the scale between the two is\n * measured per axis, so a canvas stretched unevenly is read correctly too.\n */\nexport function sourceRegion(\n aim: LoupePoint,\n factor: number,\n size: LoupeSize,\n box: SourceBox,\n backing: { width: number; height: number },\n): { sx: number; sy: number; sw: number; sh: number } {\n const { width, height } = loupeExtent(size);\n const spanX = width / factor;\n const spanY = height / factor;\n const kx = box.width > 0 ? backing.width / box.width : 1;\n const ky = box.height > 0 ? backing.height / box.height : 1;\n return {\n sx: (aim.x - spanX / 2 - box.x) * kx,\n sy: (aim.y - spanY / 2 - box.y) * ky,\n sw: spanX * kx,\n sh: spanY * ky,\n };\n}\n\n/** A surface point in CSS px, as the backing-store pixel of a canvas laid\n * over `box` that it lands on. */\nexport function sourcePixel(\n p: LoupePoint,\n box: SourceBox,\n backing: { width: number; height: number },\n): LoupePoint {\n const kx = box.width > 0 ? backing.width / box.width : 1;\n const ky = box.height > 0 ? backing.height / box.height : 1;\n return { x: (p.x - box.x) * kx, y: (p.y - box.y) * ky };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/geometry.ts","../src/model.ts","../src/canvasSource.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { View } from '@weasel-js/geom';\n\n/** A point on the magnified surface, in its own CSS pixels. */\nexport interface LoupePoint {\n x: number;\n y: number;\n}\n\n/** How big a lens is, in CSS px: one number for a round or square lens, or a\n * width and a height. */\nexport type LoupeSize = number | { width: number; height: number };\n\n/** `size` as a width and a height. */\nexport function loupeExtent(size: LoupeSize): { width: number; height: number } {\n return typeof size === 'number' ? { width: size, height: size } : size;\n}\n\n/** The lens' rectangle on that surface. */\nexport interface LoupeRect extends LoupePoint {\n w: number;\n h: number;\n}\n\n/**\n * The inner `View` a loupe renders its content through: the outer view's\n * magnification times `factor`, positioned so `target` sits at the center of\n * a viewport the size of `rect`.\n */\nexport function loupeInnerView(\n target: LoupePoint,\n outer: View,\n rect: LoupeRect,\n factor: number,\n): View {\n const scale = { x: outer.scale.x * factor, y: outer.scale.y * factor };\n return {\n x: target.x - rect.w / 2 / scale.x,\n y: target.y - rect.h / 2 / scale.y,\n scale,\n };\n}\n\n/**\n * Where on the outer surface a point inside the lens is looking, in\n * screen-space CSS px.\n *\n * The inverse of {@link loupeInnerView}: the lens centers `aim` in `rect` and\n * magnifies by `factor`, and the outer view's own scale cancels out of the\n * round trip — so this needs no `View`.\n */\nexport function loupeSourcePoint(\n p: LoupePoint,\n rect: LoupeRect,\n aim: LoupePoint,\n factor: number,\n): LoupePoint {\n return {\n x: aim.x + (p.x - (rect.x + rect.w / 2)) / factor,\n y: aim.y + (p.y - (rect.y + rect.h / 2)) / factor,\n };\n}\n\n/** What {@link placeBand} fits. */\nexport interface PlaceBandArgs {\n /** The region to show whole, in host CSS px. */\n band: LoupeRect;\n /** The most it may be magnified: the lens's own factor. */\n factor: number;\n /** The host's width: the lens is never wider, and is moved to stay inside it. */\n maxWidth: number;\n /** The host's height, likewise. Omitted, the height is not limited. */\n maxHeight?: number;\n}\n\n/** A lens fitted to a band, in the shape a loupe's `place` returns. */\nexport interface BandPlacement {\n /** Where the lens is drawn, moved as far as staying on the host needs. */\n center: LoupePoint;\n /** The band's center, which the lens shows at its middle wherever it is drawn. */\n shows: LoupePoint;\n width: number;\n height: number;\n factor: number;\n}\n\n/**\n * A lens showing all of `band`: magnified by `factor`, or by less where that\n * would make the lens wider than `maxWidth` or taller than `maxHeight`, and\n * drawn over the band except as far as it must move to stay on the host.\n */\nexport function placeBand({ band, factor, maxWidth, maxHeight }: PlaceBandArgs): BandPlacement {\n let fit = factor;\n if (band.w > 0) fit = Math.min(fit, maxWidth / band.w);\n if (maxHeight !== undefined && band.h > 0) fit = Math.min(fit, maxHeight / band.h);\n const width = band.w * fit;\n const height = band.h * fit;\n const shows = { x: band.x + band.w / 2, y: band.y + band.h / 2 };\n const within = (c: number, half: number, max: number): number =>\n Math.min(Math.max(c, half), Math.max(half, max - half));\n return {\n center: {\n x: within(shows.x, width / 2, maxWidth),\n y: maxHeight === undefined ? shows.y : within(shows.y, height / 2, maxHeight),\n },\n shows,\n width,\n height,\n factor: fit,\n };\n}\n","import { normalizeZoom } from '@weasel-js/geom';\nimport { type LoupePoint, type LoupeRect, loupeSourcePoint } from './geometry';\n\n/** How a loupe magnifies. `'vector'` re-renders the source through a zoomed-in\n * view, so content stays sharp at any factor; `'pixel'` blows up the actual\n * pixels the surface presented. */\nexport type LoupeMode = 'vector' | 'pixel';\n\n/**\n * What a loupe needs from the surface it magnifies. A painter implements it,\n * and the model asks nothing about how the lens is drawn — only where it is,\n * what colour is under a point, and whether anyone can still see it.\n */\nexport interface LoupeSurface {\n /**\n * The lens' rectangle, or `null` while it has none. A lens placed somewhere\n * other than centered on the aim also says what it shows: `shows` is the\n * surface point at its middle, and `factor` the magnification it shows it at.\n * Either one omitted is the aim, or the model's own factor.\n */\n lens(): (LoupeRect & { shows?: LoupePoint; factor?: number }) | null;\n /**\n * Does the lens itself cover this point? A stationary lens does over its own\n * frame, which is what the freeze rule below is for; a lens that follows the\n * pointer never covers it and answers `false`.\n */\n covers(p: LoupePoint): boolean;\n /** Hex colour at a surface point, or `null` when the surface cannot say. */\n sample(p: LoupePoint): string | null;\n /** Is the lens off screen? Aims are ignored while it is. */\n hidden(): boolean;\n /** Is the lens gone for good? The model tears itself down when it is. */\n gone(): boolean;\n /** Something the painter must redraw for has changed. */\n changed(): void;\n /**\n * Run `fn` after each frame the surface lands; returns an unsubscribe.\n * Implement it when `sample` reads pixels that only show an aim once a\n * frame has painted — a framebuffer read back. The model then samples the\n * aim on the next landed frame, so `color` settles a frame after the aim.\n * Omit it when `sample` can answer for a fresh aim right away.\n */\n subscribeFrame?(fn: () => void): () => void;\n}\n\n/** Options for {@link createLoupeModel}. */\nexport interface LoupeModelOptions {\n surface: LoupeSurface;\n mode?: LoupeMode;\n /** Magnification. Default 8. Always positive and finite: a factor that is\n * not becomes `ZOOM_FLOOR`, the same rule a view's zoom follows. */\n factor?: number;\n /** Bounds `setFactor` clamps to. Unset means bounded only by that rule. */\n minFactor?: number;\n maxFactor?: number;\n /** Called with the hex colour under the aim point whenever it changes. */\n onColorChange?: (hex: string) => void;\n /** Called when a pick lands — an eyedropper on the magnified surface. Where\n * the colour goes is the consumer's business. */\n onPick?: (hex: string) => void;\n /** Called when the model tears itself down because the surface reported the\n * lens gone, so the painter can drop what it was holding. */\n onDispose?: () => void;\n}\n\n/** Where a loupe is aimed, how far it magnifies, and what colour it is over. */\nexport interface LoupeModel {\n readonly mode: LoupeMode;\n readonly factor: number;\n /** Last aimed point, in the surface's CSS px. */\n readonly aim: LoupePoint;\n /** Hex colour under the aim point. `null` until something answers. */\n readonly color: string | null;\n setMode(mode: LoupeMode): void;\n setFactor(factor: number): void;\n /** Aim at a surface point. Ignored while the lens covers it, is hidden, or\n * the model is disposed. */\n aimAt(p: LoupePoint): void;\n /** Sample what the lens shows at `p`, a point inside it, and report that to\n * `onPick`. Omit `p` to pick at the aim point. Leaves the aim, and `color`,\n * alone. */\n pick(p?: LoupePoint): string | null;\n /** Sample the aim again, now: the pixels under it changed without it moving.\n * Reports through `onColorChange` like an aim does. Ignored while hidden. */\n resample(): void;\n dispose(): void;\n}\n\n/**\n * Build the loupe's model over a surface. It holds the state a magnifier has —\n * aim, factor, mode, colour — and none of the drawing.\n */\nexport function createLoupeModel(opts: LoupeModelOptions): LoupeModel {\n const { surface } = opts;\n let mode: LoupeMode = opts.mode ?? 'vector';\n let factor = normalizeZoom(opts.factor ?? 8);\n let aim: LoupePoint = { x: 0, y: 0 };\n let color: string | null = null;\n let disposed = false;\n let aimUnsampled = false;\n\n const clamp = (n: number): number =>\n normalizeZoom(Math.min(opts.maxFactor ?? Number.POSITIVE_INFINITY,\n Math.max(opts.minFactor ?? Number.NEGATIVE_INFINITY, n)));\n\n const sampleColor = () => {\n const hex = surface.sample(aim);\n if (hex === null || hex === color) return;\n color = hex;\n opts.onColorChange?.(hex);\n };\n\n const unsubscribeFrame = surface.subscribeFrame?.(() => {\n if (!aimUnsampled) return;\n aimUnsampled = false;\n sampleColor();\n });\n\n const teardown = () => {\n if (disposed) return;\n disposed = true;\n unsubscribeFrame?.();\n opts.onDispose?.();\n };\n\n return {\n get mode() { return mode; },\n get factor() { return factor; },\n get aim() { return aim; },\n get color() { return color; },\n\n setMode(next) {\n if (disposed || next === mode) return;\n mode = next;\n surface.changed();\n },\n\n setFactor(next) {\n if (disposed) return;\n factor = clamp(next);\n surface.changed();\n },\n\n aimAt(p) {\n if (disposed) return;\n // A lens can be taken away by whatever owns it, without coming back\n // through `dispose`. Follow it down rather than keep sampling for a\n // magnifier nobody can see.\n if (surface.gone()) { teardown(); return; }\n if (surface.hidden()) return;\n if (surface.covers(p)) return;\n aim = p;\n if (unsubscribeFrame) aimUnsampled = true;\n else sampleColor();\n surface.changed();\n },\n\n pick(p) {\n if (disposed) return null;\n let at = aim;\n if (p) {\n const rect = surface.lens();\n if (!rect) return null;\n at = loupeSourcePoint(p, rect, rect.shows ?? aim, rect.factor ?? factor);\n // The lens is part of the picture the surface presents. A point near\n // its frame maps back underneath it, and sampling there reports the\n // lens' own chrome as artwork.\n if (surface.covers(at)) return null;\n }\n const hex = surface.sample(at);\n if (hex !== null) opts.onPick?.(hex);\n return hex;\n },\n\n resample() {\n if (disposed || surface.hidden()) return;\n aimUnsampled = false;\n sampleColor();\n },\n\n dispose: teardown,\n };\n}\n","import { createEmitter } from '@weasel-js/registry';\nimport { type LoupePoint, type LoupeSize, loupeExtent } from './geometry';\n\n/** A canvas whose pixels a loupe can read. */\nexport type SourceCanvas = HTMLCanvasElement | OffscreenCanvas;\n\n/** A rendering context on a {@link SourceCanvas}. Passing one, rather than the\n * bare canvas, is what tells a source whether the canvas keeps its pixels. */\nexport type SourceContext =\n | CanvasRenderingContext2D\n | OffscreenCanvasRenderingContext2D\n | WebGLRenderingContext\n | WebGL2RenderingContext\n | ImageBitmapRenderingContext;\n\n/** Where a source canvas's pixels sit on the surface being magnified, in that\n * surface's CSS px. The backing store is stretched over this box, so its\n * width against the canvas's own `width` is the device-pixel ratio. */\nexport interface SourceBox {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/** Options for {@link createCanvasSource}. */\nexport interface CanvasSourceOptions {\n /**\n * Called when a reader starts and there is no current frame to read: draw\n * the canvas again, and call `capture()` after it. Only needed by a canvas\n * that is captured and does not redraw every frame on its own — without it\n * such a lens stays empty until the next redraw.\n */\n requestRedraw?: () => void;\n /** Where the canvas sits on the magnified surface. Omitted, a reader works\n * it out — a painter over the DOM measures the canvas's own box. */\n box?: () => SourceBox | null;\n}\n\n/**\n * Any canvas, as something a loupe reads pixels from: 2D, WebGL or WebGL2,\n * on the page or offscreen, owned by whoever drew it.\n *\n * A WebGL canvas made without `preserveDrawingBuffer` is cleared once the\n * browser composites it, so reading it from any later task — the lens's own\n * frame — returns nothing. Such a source is *captured*: whoever draws the\n * canvas calls {@link capture} right after drawing, in the same task, and the\n * source copies the frame while it still exists. Every other canvas holds its\n * pixels and is read directly.\n */\nexport interface CanvasSource {\n readonly canvas: SourceCanvas;\n /**\n * Whether frames reach readers only through {@link capture}. True for a\n * source made from a WebGL context without `preserveDrawingBuffer`, and for\n * any source once `capture()` has been called on it.\n */\n readonly captured: boolean;\n readonly box?: () => SourceBox | null;\n /**\n * Copy the frame just drawn. Call it in the same task as the draw — right\n * after `gl.drawArrays`, `renderer.render(...)` or whatever finished the\n * frame. Free while nothing is reading: it copies only while a reader holds\n * the source.\n */\n capture(): void;\n /** The pixels to read now: the last capture, or the canvas itself when it\n * keeps its own. `null` while a captured source has no current frame. */\n frame(): CanvasImageSource | null;\n /** Hex color of the backing-store pixel at `p` (device px, not CSS px), or\n * `null` where it is transparent, off the canvas, or there is no frame. */\n sample(p: LoupePoint): string | null;\n /** Start reading. While any reader holds the source, `capture` copies;\n * returns the release. */\n retain(): () => void;\n /** Run `fn` after each capture lands; returns an unsubscribe. */\n subscribeFrame(fn: () => void): () => void;\n}\n\n/** A frame read this many times with no capture behind it means the drawing\n * code is not calling `capture()`. About two seconds of lens frames. */\nconst MISSED_FRAMES_BEFORE_WARNING = 120;\n\nfunction isWebGL(ctx: SourceContext): ctx is WebGLRenderingContext | WebGL2RenderingContext {\n return 'drawArrays' in ctx;\n}\n\nfunction isContext(target: SourceCanvas | SourceContext): target is SourceContext {\n return 'canvas' in target && !('getContext' in target);\n}\n\nfunction makeCanvas(width: number, height: number): SourceCanvas {\n if (typeof OffscreenCanvas !== 'undefined') return new OffscreenCanvas(width, height);\n const c = document.createElement('canvas');\n c.width = width;\n c.height = height;\n return c;\n}\n\ntype Ctx2D = CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;\n\nfunction context2d(canvas: SourceCanvas, willReadFrequently = false): Ctx2D | null {\n return canvas.getContext('2d', { willReadFrequently }) as Ctx2D | null;\n}\n\n/**\n * Make a {@link CanvasSource} for a canvas, or for a context on one.\n *\n * Pass the context when there is one: a WebGL context without\n * `preserveDrawingBuffer` is then known to need capturing before anything has\n * gone blank. A bare canvas is read directly until `capture()` is first\n * called on it. The source never calls `getContext` on the canvas it reads —\n * on a canvas with no context yet, that would create one with the wrong\n * attributes and leave whoever draws it unable to get theirs.\n */\nexport function createCanvasSource(\n target: SourceCanvas | SourceContext,\n options: CanvasSourceOptions = {},\n): CanvasSource {\n const canvas = (isContext(target) ? target.canvas : target) as SourceCanvas;\n const mustCapture =\n isContext(target) && isWebGL(target) && !target.getContextAttributes()?.preserveDrawingBuffer;\n\n let captured = mustCapture;\n let readers = 0;\n let snapshot: SourceCanvas | null = null;\n let current = false;\n let missed = 0;\n let warned = false;\n let probe: Ctx2D | null = null;\n const frames = createEmitter();\n\n const source: CanvasSource = {\n canvas,\n get captured() {\n return captured;\n },\n box: options.box,\n\n capture() {\n captured = true;\n if (readers === 0) return;\n const { width, height } = canvas;\n if (width === 0 || height === 0) return;\n if (!snapshot) snapshot = makeCanvas(width, height);\n else if (snapshot.width !== width || snapshot.height !== height) {\n snapshot.width = width;\n snapshot.height = height;\n }\n const ctx = context2d(snapshot);\n if (!ctx) return;\n ctx.globalCompositeOperation = 'copy';\n ctx.drawImage(canvas, 0, 0);\n current = true;\n missed = 0;\n frames.emit();\n },\n\n frame() {\n if (!captured) return canvas.width > 0 && canvas.height > 0 ? canvas : null;\n if (current && snapshot) return snapshot;\n if (!warned && ++missed >= MISSED_FRAMES_BEFORE_WARNING) {\n warned = true;\n console.warn(\n '[weasel loupe] A WebGL canvas without preserveDrawingBuffer is being read, ' +\n 'and nothing has called capture() on its source. Call source.capture() right ' +\n 'after drawing each frame, or pass requestRedraw if the canvas draws on demand.',\n );\n }\n return null;\n },\n\n sample(p) {\n const frame = source.frame();\n if (!frame) return null;\n const x = Math.floor(p.x);\n const y = Math.floor(p.y);\n if (x < 0 || y < 0 || x >= canvas.width || y >= canvas.height) return null;\n if (!probe) {\n const c = makeCanvas(1, 1);\n probe = context2d(c, true);\n if (!probe) return null;\n }\n probe.globalCompositeOperation = 'copy';\n probe.drawImage(frame, x, y, 1, 1, 0, 0, 1, 1);\n const [r, g, b, a] = probe.getImageData(0, 0, 1, 1).data;\n if (a === 0) return null;\n return `#${[r, g, b].map((c) => c.toString(16).padStart(2, '0')).join('')}`;\n },\n\n retain() {\n readers++;\n if (readers === 1 && captured) {\n // Whatever was copied before the last reader left may be long stale.\n current = false;\n options.requestRedraw?.();\n }\n let held = true;\n return () => {\n if (!held) return;\n held = false;\n readers--;\n };\n },\n\n subscribeFrame: frames.subscribe,\n };\n return source;\n}\n\n/**\n * The rectangle of `source`'s backing store a lens of `size` CSS px\n * magnifying `factor` times about `aim` shows, in device px. `box` is where\n * the canvas sits in the same CSS px as `aim`; the scale between the two is\n * measured per axis, so a canvas stretched unevenly is read correctly too.\n */\nexport function sourceRegion(\n aim: LoupePoint,\n factor: number,\n size: LoupeSize,\n box: SourceBox,\n backing: { width: number; height: number },\n): { sx: number; sy: number; sw: number; sh: number } {\n const { width, height } = loupeExtent(size);\n const spanX = width / factor;\n const spanY = height / factor;\n const kx = box.width > 0 ? backing.width / box.width : 1;\n const ky = box.height > 0 ? backing.height / box.height : 1;\n return {\n sx: (aim.x - spanX / 2 - box.x) * kx,\n sy: (aim.y - spanY / 2 - box.y) * ky,\n sw: spanX * kx,\n sh: spanY * ky,\n };\n}\n\n/** A surface point in CSS px, as the backing-store pixel of a canvas laid\n * over `box` that it lands on. */\nexport function sourcePixel(\n p: LoupePoint,\n box: SourceBox,\n backing: { width: number; height: number },\n): LoupePoint {\n const kx = box.width > 0 ? backing.width / box.width : 1;\n const ky = box.height > 0 ? backing.height / box.height : 1;\n return { x: (p.x - box.x) * kx, y: (p.y - box.y) * ky };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weasel-js/loupe",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "Surface-free magnifier model: where a loupe is aimed, how far it magnifies, and what colour it is over. Painters live with the surfaces they draw on.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -29,10 +29,11 @@
|
|
|
29
29
|
"node": ">=22"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@weasel-js/geom": "1.
|
|
32
|
+
"@weasel-js/geom": "1.10.0",
|
|
33
|
+
"@weasel-js/registry": "1.10.0"
|
|
33
34
|
},
|
|
34
35
|
"devDependencies": {
|
|
35
|
-
"@weasel-js/core": "1.
|
|
36
|
+
"@weasel-js/core": "1.10.0"
|
|
36
37
|
},
|
|
37
38
|
"files": [
|
|
38
39
|
"dist",
|