@weasel-js/loupe 1.4.0-pre.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/LICENSE +21 -0
- package/README.md +17 -0
- package/dist/index.d.ts +99 -0
- package/dist/index.js +94 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 orochi235
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# @weasel-js/loupe
|
|
2
|
+
|
|
3
|
+
The magnifier's model, with no surface attached: where it is aimed, how far it
|
|
4
|
+
magnifies, whether it is showing re-rendered content or actual pixels, and what
|
|
5
|
+
colour it is over.
|
|
6
|
+
|
|
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 colour is here, repaint. The
|
|
9
|
+
painters ship with the surfaces they know: `@weasel-js/hud` draws one into a
|
|
10
|
+
WebGL canvas, `@weasel-js/labkit` draws one over a lab's own content.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { createLoupeModel } from '@weasel-js/loupe';
|
|
14
|
+
|
|
15
|
+
const loupe = createLoupeModel({ surface, factor: 8 });
|
|
16
|
+
loupe.aimAt({ x, y });
|
|
17
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { View } from '@weasel-js/core';
|
|
2
|
+
|
|
3
|
+
/** A point on the magnified surface, in its own CSS pixels. */
|
|
4
|
+
interface LoupePoint {
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
}
|
|
8
|
+
/** The lens' rectangle on that surface. */
|
|
9
|
+
interface LoupeRect extends LoupePoint {
|
|
10
|
+
w: number;
|
|
11
|
+
h: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The inner `View` a loupe renders its content through: the outer view's
|
|
15
|
+
* magnification times `factor`, positioned so `target` sits at the center of
|
|
16
|
+
* a viewport the size of `rect`.
|
|
17
|
+
*/
|
|
18
|
+
declare function loupeInnerView(target: LoupePoint, outer: View, rect: LoupeRect, factor: number): View;
|
|
19
|
+
/**
|
|
20
|
+
* Where on the outer surface a point inside the lens is looking, in
|
|
21
|
+
* screen-space CSS px.
|
|
22
|
+
*
|
|
23
|
+
* The inverse of {@link loupeInnerView}: the lens centers `aim` in `rect` and
|
|
24
|
+
* magnifies by `factor`, and the outer view's own scale cancels out of the
|
|
25
|
+
* round trip — so this needs no `View`.
|
|
26
|
+
*/
|
|
27
|
+
declare function loupeSourcePoint(p: LoupePoint, rect: LoupeRect, aim: LoupePoint, factor: number): LoupePoint;
|
|
28
|
+
|
|
29
|
+
/** How a loupe magnifies. `'vector'` re-renders the source through a zoomed-in
|
|
30
|
+
* view, so content stays sharp at any factor; `'pixel'` blows up the actual
|
|
31
|
+
* pixels the surface presented. */
|
|
32
|
+
type LoupeMode = 'vector' | 'pixel';
|
|
33
|
+
/**
|
|
34
|
+
* What a loupe needs from the surface it magnifies. A painter implements it,
|
|
35
|
+
* and the model asks nothing about how the lens is drawn — only where it is,
|
|
36
|
+
* what colour is under a point, and whether anyone can still see it.
|
|
37
|
+
*/
|
|
38
|
+
interface LoupeSurface {
|
|
39
|
+
/** The lens' rectangle, or `null` while it has none. */
|
|
40
|
+
lens(): LoupeRect | null;
|
|
41
|
+
/**
|
|
42
|
+
* Does the lens itself cover this point? A stationary lens does over its own
|
|
43
|
+
* frame, which is what the freeze rule below is for; a lens that follows the
|
|
44
|
+
* pointer never covers it and answers `false`.
|
|
45
|
+
*/
|
|
46
|
+
covers(p: LoupePoint): boolean;
|
|
47
|
+
/** Hex colour at a surface point, or `null` when the surface cannot say. */
|
|
48
|
+
sample(p: LoupePoint): string | null;
|
|
49
|
+
/** Is the lens off screen? Aims are ignored while it is. */
|
|
50
|
+
hidden(): boolean;
|
|
51
|
+
/** Is the lens gone for good? The model tears itself down when it is. */
|
|
52
|
+
gone(): boolean;
|
|
53
|
+
/** Something the painter must redraw for has changed. */
|
|
54
|
+
changed(): void;
|
|
55
|
+
}
|
|
56
|
+
/** Options for {@link createLoupeModel}. */
|
|
57
|
+
interface LoupeModelOptions {
|
|
58
|
+
surface: LoupeSurface;
|
|
59
|
+
mode?: LoupeMode;
|
|
60
|
+
/** Magnification. Default 8. */
|
|
61
|
+
factor?: number;
|
|
62
|
+
/** Bounds `setFactor` clamps to. Unset means unclamped. */
|
|
63
|
+
minFactor?: number;
|
|
64
|
+
maxFactor?: number;
|
|
65
|
+
/** Called with the hex colour under the aim point whenever it changes. */
|
|
66
|
+
onColorChange?: (hex: string) => void;
|
|
67
|
+
/** Called when a pick lands — an eyedropper on the magnified surface. Where
|
|
68
|
+
* the colour goes is the consumer's business. */
|
|
69
|
+
onPick?: (hex: string) => void;
|
|
70
|
+
/** Called when the model tears itself down because the surface reported the
|
|
71
|
+
* lens gone, so the painter can drop what it was holding. */
|
|
72
|
+
onDispose?: () => void;
|
|
73
|
+
}
|
|
74
|
+
/** Where a loupe is aimed, how far it magnifies, and what colour it is over. */
|
|
75
|
+
interface LoupeModel {
|
|
76
|
+
readonly mode: LoupeMode;
|
|
77
|
+
readonly factor: number;
|
|
78
|
+
/** Last aimed point, in the surface's CSS px. */
|
|
79
|
+
readonly aim: LoupePoint;
|
|
80
|
+
/** Hex colour under the aim point. `null` until something answers. */
|
|
81
|
+
readonly color: string | null;
|
|
82
|
+
setMode(mode: LoupeMode): void;
|
|
83
|
+
setFactor(factor: number): void;
|
|
84
|
+
/** Aim at a surface point. Ignored while the lens covers it, is hidden, or
|
|
85
|
+
* the model is disposed. */
|
|
86
|
+
aimAt(p: LoupePoint): void;
|
|
87
|
+
/** Sample what the lens shows at `p`, a point inside it, and report that to
|
|
88
|
+
* `onPick`. Omit `p` to pick at the aim point. Leaves the aim, and `color`,
|
|
89
|
+
* alone. */
|
|
90
|
+
pick(p?: LoupePoint): string | null;
|
|
91
|
+
dispose(): void;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Build the loupe's model over a surface. It holds the state a magnifier has —
|
|
95
|
+
* aim, factor, mode, colour — and none of the drawing.
|
|
96
|
+
*/
|
|
97
|
+
declare function createLoupeModel(opts: LoupeModelOptions): LoupeModel;
|
|
98
|
+
|
|
99
|
+
export { type LoupeMode, type LoupeModel, type LoupeModelOptions, type LoupePoint, type LoupeRect, type LoupeSurface, createLoupeModel, loupeInnerView, loupeSourcePoint };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// src/geometry.ts
|
|
2
|
+
function loupeInnerView(target, outer, rect, factor) {
|
|
3
|
+
const scale = { x: outer.scale.x * factor, y: outer.scale.y * factor };
|
|
4
|
+
return {
|
|
5
|
+
x: target.x - rect.w / 2 / scale.x,
|
|
6
|
+
y: target.y - rect.h / 2 / scale.y,
|
|
7
|
+
scale
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function loupeSourcePoint(p, rect, aim, factor) {
|
|
11
|
+
return {
|
|
12
|
+
x: aim.x + (p.x - (rect.x + rect.w / 2)) / factor,
|
|
13
|
+
y: aim.y + (p.y - (rect.y + rect.h / 2)) / factor
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/model.ts
|
|
18
|
+
function createLoupeModel(opts) {
|
|
19
|
+
const { surface } = opts;
|
|
20
|
+
let mode = opts.mode ?? "vector";
|
|
21
|
+
let factor = opts.factor ?? 8;
|
|
22
|
+
let aim = { x: 0, y: 0 };
|
|
23
|
+
let color = null;
|
|
24
|
+
let disposed = false;
|
|
25
|
+
const clamp = (n) => Math.min(
|
|
26
|
+
opts.maxFactor ?? Number.POSITIVE_INFINITY,
|
|
27
|
+
Math.max(opts.minFactor ?? Number.NEGATIVE_INFINITY, n)
|
|
28
|
+
);
|
|
29
|
+
const teardown = () => {
|
|
30
|
+
if (disposed) return;
|
|
31
|
+
disposed = true;
|
|
32
|
+
opts.onDispose?.();
|
|
33
|
+
};
|
|
34
|
+
const sampleColor = () => {
|
|
35
|
+
const hex = surface.sample(aim);
|
|
36
|
+
if (hex === null || hex === color) return;
|
|
37
|
+
color = hex;
|
|
38
|
+
opts.onColorChange?.(hex);
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
get mode() {
|
|
42
|
+
return mode;
|
|
43
|
+
},
|
|
44
|
+
get factor() {
|
|
45
|
+
return factor;
|
|
46
|
+
},
|
|
47
|
+
get aim() {
|
|
48
|
+
return aim;
|
|
49
|
+
},
|
|
50
|
+
get color() {
|
|
51
|
+
return color;
|
|
52
|
+
},
|
|
53
|
+
setMode(next) {
|
|
54
|
+
if (disposed || next === mode) return;
|
|
55
|
+
mode = next;
|
|
56
|
+
surface.changed();
|
|
57
|
+
},
|
|
58
|
+
setFactor(next) {
|
|
59
|
+
if (disposed) return;
|
|
60
|
+
factor = clamp(next);
|
|
61
|
+
surface.changed();
|
|
62
|
+
},
|
|
63
|
+
aimAt(p) {
|
|
64
|
+
if (disposed) return;
|
|
65
|
+
if (surface.gone()) {
|
|
66
|
+
teardown();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (surface.hidden()) return;
|
|
70
|
+
if (surface.covers(p)) return;
|
|
71
|
+
aim = p;
|
|
72
|
+
sampleColor();
|
|
73
|
+
surface.changed();
|
|
74
|
+
},
|
|
75
|
+
pick(p) {
|
|
76
|
+
if (disposed) return null;
|
|
77
|
+
let at = aim;
|
|
78
|
+
if (p) {
|
|
79
|
+
const rect = surface.lens();
|
|
80
|
+
if (!rect) return null;
|
|
81
|
+
at = loupeSourcePoint(p, rect, aim, factor);
|
|
82
|
+
if (surface.covers(at)) return null;
|
|
83
|
+
}
|
|
84
|
+
const hex = surface.sample(at);
|
|
85
|
+
if (hex !== null) opts.onPick?.(hex);
|
|
86
|
+
return hex;
|
|
87
|
+
},
|
|
88
|
+
dispose: teardown
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export { createLoupeModel, loupeInnerView, loupeSourcePoint };
|
|
93
|
+
//# sourceMappingURL=index.js.map
|
|
94
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/geometry.ts","../src/model.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { View } from '@weasel-js/core';\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/** 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","import { 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 /** The lens' rectangle, or `null` while it has none. */\n lens(): LoupeRect | 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\n/** Options for {@link createLoupeModel}. */\nexport interface LoupeModelOptions {\n surface: LoupeSurface;\n mode?: LoupeMode;\n /** Magnification. Default 8. */\n factor?: number;\n /** Bounds `setFactor` clamps to. Unset means unclamped. */\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 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 = opts.factor ?? 8;\n let aim: LoupePoint = { x: 0, y: 0 };\n let color: string | null = null;\n let disposed = false;\n\n const clamp = (n: number): number =>\n Math.min(opts.maxFactor ?? Number.POSITIVE_INFINITY,\n Math.max(opts.minFactor ?? Number.NEGATIVE_INFINITY, n));\n\n const teardown = () => {\n if (disposed) return;\n disposed = true;\n opts.onDispose?.();\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 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 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, aim, 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 dispose: teardown,\n };\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@weasel-js/loupe",
|
|
3
|
+
"version": "1.4.0-pre.0",
|
|
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
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts"
|
|
15
|
+
},
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"author": "orochi235",
|
|
19
|
+
"homepage": "https://orochi235.github.io/weasel/",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/orochi235/weasel.git",
|
|
23
|
+
"directory": "packages/loupe"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/orochi235/weasel/issues"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=22"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@weasel-js/core": "1.4.0-pre.0"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsup"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public",
|
|
44
|
+
"provenance": true
|
|
45
|
+
}
|
|
46
|
+
}
|