heatspot 0.1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 heatspot contributors
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,98 @@
1
+ # heatspot
2
+
3
+ `heatspot` is an ESM TypeScript library for capturing pointer heat data and rendering an embeddable heatmap web component.
4
+
5
+ ## Features
6
+
7
+ - ESM package output with TypeScript declarations
8
+ - Pointer heat tracking utility API
9
+ - Reusable `<heat-map>` component with slot-based content
10
+ - Built-in toggle icon (top-left) to show heat overlay
11
+ - Configurable `toolbar` attribute: `simple` (default) or `hidden`
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install heatspot
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ### 1. Import the library
22
+
23
+ ```ts
24
+ import "heatspot";
25
+ ```
26
+
27
+ The `heat-map` custom element is registered on import.
28
+
29
+ ### 2. Render the component
30
+
31
+ ```html
32
+ <heat-map toolbar="simple">
33
+ <section>
34
+ <h2>Example Panel</h2>
35
+ <p>Move the mouse over this area, then click the icon in the top-left.</p>
36
+ </section>
37
+ </heat-map>
38
+ ```
39
+
40
+ `toolbar` options:
41
+
42
+ - `simple` - show the heatmap toggle icon
43
+ - `hidden` - hide the heatmap toggle icon
44
+
45
+ Example with hidden toolbar:
46
+
47
+ ```html
48
+ <heat-map toolbar="hidden">
49
+ <section>
50
+ <h2>Passive Tracking Panel</h2>
51
+ <p>The heatmap toggle icon is not rendered in this mode.</p>
52
+ </section>
53
+ </heat-map>
54
+ ```
55
+
56
+ ### Example
57
+
58
+ ![example.jps](assets/example.jpg)
59
+
60
+ ### 3. Optional tracking API
61
+
62
+ ```ts
63
+ import {
64
+ configureMouseHeatmap,
65
+ getMouseHeatmapData,
66
+ resetMouseHeatmap,
67
+ startMouseTracking,
68
+ stopMouseTracking
69
+ } from "heatspot";
70
+
71
+ configureMouseHeatmap({ mergeRadius: 24, maxHotspots: 450 });
72
+ startMouseTracking();
73
+
74
+ const snapshot = getMouseHeatmapData();
75
+ console.log(snapshot.hotspots);
76
+
77
+ stopMouseTracking();
78
+ resetMouseHeatmap();
79
+ ```
80
+
81
+ ## Scripts
82
+
83
+ - `npm run build` - compile library to `dist/`
84
+ - `npm run test` - run Vitest tests
85
+ - `npm run test:coverage` - run tests with coverage reports in `coverage/`
86
+ - `npm run build:verify` - run tests and harness build
87
+ - `npm run pack:check` - show package contents using `npm pack --dry-run`
88
+
89
+ ## Development
90
+
91
+ ```bash
92
+ npm install
93
+ npm start
94
+ ```
95
+
96
+ ## License
97
+
98
+ MIT
@@ -0,0 +1,44 @@
1
+ import { type HeatmapConfig } from "../contracts/heatmap-contracts.js";
2
+ import { type HeatColor } from "../contracts/rendering-contracts.js";
3
+ /**
4
+ * Default clustering configuration used by the global tracker API.
5
+ */
6
+ export declare const DEFAULT_HEATMAP_CONFIG: HeatmapConfig;
7
+ /** Minimum allowed hotspot merge radius in pixels. */
8
+ export declare const MIN_MERGE_RADIUS = 4;
9
+ /** Minimum allowed hotspot storage size for a tracker instance. */
10
+ export declare const MIN_MAX_HOTSPOTS = 20;
11
+ /**
12
+ * Tracker configuration used by the `heat-map` web component instance.
13
+ */
14
+ export declare const ELEMENT_TRACKER_CONFIG: HeatmapConfig;
15
+ /**
16
+ * Ordered heat palette from coolest to hottest.
17
+ */
18
+ export declare const DEFAULT_HEAT_COLORS: readonly HeatColor[];
19
+ /**
20
+ * Discrete contour levels used to pick the dominant output heat color.
21
+ */
22
+ export declare const DOMINANT_HEAT_LEVELS: readonly [0.2, 0.35, 0.5, 0.65, 0.8, 1];
23
+ /** Exponent used to bias relative hotspot strength scaling. */
24
+ export declare const RELATIVE_STRENGTH_EXPONENT = 0.85;
25
+ /** Max normalized distance multiplier for hotspot influence cutoff. */
26
+ export declare const FIELD_DISTANCE_MULTIPLIER = 2.2;
27
+ /** Falloff divisor controlling Gaussian-style strength decay around hotspots. */
28
+ export declare const FIELD_FALLOFF_DIVISOR = 0.8;
29
+ /** Strength below this value is treated as low-heat rendering. */
30
+ export declare const LOW_HEAT_THRESHOLD = 0.05;
31
+ /** Alpha used for low-heat visible floor color. */
32
+ export declare const LOW_HEAT_ALPHA = 0.16;
33
+ /** Base alpha for dominant contour color levels. */
34
+ export declare const HEAT_ALPHA_BASE = 0.22;
35
+ /** Additional alpha multiplier per dominant contour level. */
36
+ export declare const HEAT_ALPHA_MULTIPLIER = 0.45;
37
+ /** Alpha used for maximum heat color output. */
38
+ export declare const MAX_HEAT_ALPHA = 0.68;
39
+ /** Sampling step in pixels for rasterized heat field rendering. */
40
+ export declare const DEFAULT_SAMPLE_STEP = 4;
41
+ /** Minimum contour radius in pixels for visible heat blobs. */
42
+ export declare const MIN_CONTOUR_RADIUS = 14;
43
+ /** Height/width ratio used to derive contour radius from viewport size. */
44
+ export declare const CONTOUR_RADIUS_RATIO = 0.07;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Default clustering configuration used by the global tracker API.
3
+ */
4
+ export const DEFAULT_HEATMAP_CONFIG = {
5
+ mergeRadius: 28,
6
+ maxHotspots: 400
7
+ };
8
+ /** Minimum allowed hotspot merge radius in pixels. */
9
+ export const MIN_MERGE_RADIUS = 4;
10
+ /** Minimum allowed hotspot storage size for a tracker instance. */
11
+ export const MIN_MAX_HOTSPOTS = 20;
12
+ /**
13
+ * Tracker configuration used by the `heat-map` web component instance.
14
+ */
15
+ export const ELEMENT_TRACKER_CONFIG = {
16
+ mergeRadius: 24,
17
+ maxHotspots: 450
18
+ };
19
+ /**
20
+ * Ordered heat palette from coolest to hottest.
21
+ */
22
+ export const DEFAULT_HEAT_COLORS = [
23
+ [20, 34, 120],
24
+ [0, 145, 255],
25
+ [0, 200, 125],
26
+ [255, 225, 70],
27
+ [255, 140, 30],
28
+ [225, 35, 30],
29
+ [255, 255, 255]
30
+ ];
31
+ /**
32
+ * Discrete contour levels used to pick the dominant output heat color.
33
+ */
34
+ export const DOMINANT_HEAT_LEVELS = [0.2, 0.35, 0.5, 0.65, 0.8, 1];
35
+ /** Exponent used to bias relative hotspot strength scaling. */
36
+ export const RELATIVE_STRENGTH_EXPONENT = 0.85;
37
+ /** Max normalized distance multiplier for hotspot influence cutoff. */
38
+ export const FIELD_DISTANCE_MULTIPLIER = 2.2;
39
+ /** Falloff divisor controlling Gaussian-style strength decay around hotspots. */
40
+ export const FIELD_FALLOFF_DIVISOR = 0.8;
41
+ /** Strength below this value is treated as low-heat rendering. */
42
+ export const LOW_HEAT_THRESHOLD = 0.05;
43
+ /** Alpha used for low-heat visible floor color. */
44
+ export const LOW_HEAT_ALPHA = 0.16;
45
+ /** Base alpha for dominant contour color levels. */
46
+ export const HEAT_ALPHA_BASE = 0.22;
47
+ /** Additional alpha multiplier per dominant contour level. */
48
+ export const HEAT_ALPHA_MULTIPLIER = 0.45;
49
+ /** Alpha used for maximum heat color output. */
50
+ export const MAX_HEAT_ALPHA = 0.68;
51
+ /** Sampling step in pixels for rasterized heat field rendering. */
52
+ export const DEFAULT_SAMPLE_STEP = 4;
53
+ /** Minimum contour radius in pixels for visible heat blobs. */
54
+ export const MIN_CONTOUR_RADIUS = 14;
55
+ /** Height/width ratio used to derive contour radius from viewport size. */
56
+ export const CONTOUR_RADIUS_RATIO = 0.07;
@@ -0,0 +1,22 @@
1
+ export interface ViewportSize {
2
+ width: number;
3
+ height: number;
4
+ }
5
+ export interface HeatmapConfig {
6
+ mergeRadius: number;
7
+ maxHotspots: number;
8
+ }
9
+ export type HeatmapToolbarMode = "simple" | "hidden";
10
+ export interface HeatmapHotspot {
11
+ id: string;
12
+ x: number;
13
+ y: number;
14
+ count: number;
15
+ intensity: number;
16
+ }
17
+ export interface HeatmapSnapshot {
18
+ totalSamples: number;
19
+ trackedSince: number | null;
20
+ viewport: ViewportSize;
21
+ hotspots: HeatmapHotspot[];
22
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ export type HeatColor = readonly [number, number, number];
2
+ export interface HeatmapRenderOptions {
3
+ sampleStep?: number;
4
+ maxContourRadius?: number;
5
+ colors?: readonly HeatColor[];
6
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,29 @@
1
+ import { type HeatmapConfig, type HeatmapSnapshot, type ViewportSize } from "../contracts/heatmap-contracts.js";
2
+ /**
3
+ * Stores and aggregates pointer samples into clustered hotspots.
4
+ */
5
+ export declare class HeatmapTracker {
6
+ private config;
7
+ private sampleCount;
8
+ private trackedSince;
9
+ private latestViewport;
10
+ private readonly trackedHotspots;
11
+ private nextHotspotId;
12
+ constructor(initialConfig?: Partial<HeatmapConfig>);
13
+ /**
14
+ * Updates clustering configuration.
15
+ */
16
+ configure(nextConfig: Partial<HeatmapConfig>): void;
17
+ /**
18
+ * Records a pointer position and merges it into the nearest hotspot.
19
+ */
20
+ recordPosition(x: number, y: number, viewport: ViewportSize): void;
21
+ /**
22
+ * Clears all tracked samples and hotspots.
23
+ */
24
+ reset(): void;
25
+ /**
26
+ * Returns a snapshot suitable for rendering or analytics.
27
+ */
28
+ getSnapshot(): HeatmapSnapshot;
29
+ }
@@ -0,0 +1,111 @@
1
+ import { DEFAULT_HEATMAP_CONFIG, MIN_MAX_HOTSPOTS, MIN_MERGE_RADIUS } from "../constants/constants.js";
2
+ function clamp(value, min, max) {
3
+ return Math.min(max, Math.max(min, value));
4
+ }
5
+ function getDistanceSquared(aX, aY, bX, bY) {
6
+ const dX = aX - bX;
7
+ const dY = aY - bY;
8
+ return dX * dX + dY * dY;
9
+ }
10
+ /**
11
+ * Stores and aggregates pointer samples into clustered hotspots.
12
+ */
13
+ export class HeatmapTracker {
14
+ config;
15
+ sampleCount = 0;
16
+ trackedSince = null;
17
+ latestViewport = { width: 0, height: 0 };
18
+ trackedHotspots = [];
19
+ nextHotspotId = 0;
20
+ constructor(initialConfig = {}) {
21
+ this.config = { ...DEFAULT_HEATMAP_CONFIG };
22
+ this.configure(initialConfig);
23
+ }
24
+ /**
25
+ * Updates clustering configuration.
26
+ */
27
+ configure(nextConfig) {
28
+ const mergeRadius = nextConfig.mergeRadius ?? this.config.mergeRadius;
29
+ const maxHotspots = nextConfig.maxHotspots ?? this.config.maxHotspots;
30
+ this.config = {
31
+ mergeRadius: Math.max(MIN_MERGE_RADIUS, mergeRadius),
32
+ maxHotspots: Math.max(MIN_MAX_HOTSPOTS, Math.floor(maxHotspots))
33
+ };
34
+ }
35
+ /**
36
+ * Records a pointer position and merges it into the nearest hotspot.
37
+ */
38
+ recordPosition(x, y, viewport) {
39
+ if (viewport.width <= 0 || viewport.height <= 0) {
40
+ return;
41
+ }
42
+ const clampedX = clamp(x, 0, viewport.width);
43
+ const clampedY = clamp(y, 0, viewport.height);
44
+ const mergeRadiusSquared = this.config.mergeRadius * this.config.mergeRadius;
45
+ let closest = null;
46
+ let closestDistance = Number.POSITIVE_INFINITY;
47
+ for (const hotspot of this.trackedHotspots) {
48
+ const distance = getDistanceSquared(clampedX, clampedY, hotspot.x, hotspot.y);
49
+ if (distance <= mergeRadiusSquared && distance < closestDistance) {
50
+ closest = hotspot;
51
+ closestDistance = distance;
52
+ }
53
+ }
54
+ if (closest) {
55
+ const nextCount = closest.count + 1;
56
+ closest.x = (closest.x * closest.count + clampedX) / nextCount;
57
+ closest.y = (closest.y * closest.count + clampedY) / nextCount;
58
+ closest.count = nextCount;
59
+ }
60
+ else {
61
+ this.trackedHotspots.push({
62
+ id: `hs-${this.nextHotspotId++}`,
63
+ x: clampedX,
64
+ y: clampedY,
65
+ count: 1,
66
+ intensity: 0
67
+ });
68
+ }
69
+ if (this.trackedHotspots.length > this.config.maxHotspots) {
70
+ this.trackedHotspots.sort((a, b) => a.count - b.count);
71
+ this.trackedHotspots.shift();
72
+ }
73
+ this.sampleCount += 1;
74
+ this.latestViewport = viewport;
75
+ if (this.trackedSince === null) {
76
+ this.trackedSince = Date.now();
77
+ }
78
+ }
79
+ /**
80
+ * Clears all tracked samples and hotspots.
81
+ */
82
+ reset() {
83
+ this.sampleCount = 0;
84
+ this.trackedSince = null;
85
+ this.latestViewport = { width: 0, height: 0 };
86
+ this.trackedHotspots.length = 0;
87
+ }
88
+ /**
89
+ * Returns a snapshot suitable for rendering or analytics.
90
+ */
91
+ getSnapshot() {
92
+ let maxCount = 0;
93
+ for (const hotspot of this.trackedHotspots) {
94
+ if (hotspot.count > maxCount) {
95
+ maxCount = hotspot.count;
96
+ }
97
+ }
98
+ const hotspots = this.trackedHotspots
99
+ .map((hotspot) => ({
100
+ ...hotspot,
101
+ intensity: maxCount === 0 ? 0 : hotspot.count / maxCount
102
+ }))
103
+ .sort((a, b) => b.count - a.count);
104
+ return {
105
+ totalSamples: this.sampleCount,
106
+ trackedSince: this.trackedSince,
107
+ viewport: { ...this.latestViewport },
108
+ hotspots
109
+ };
110
+ }
111
+ }
@@ -0,0 +1,47 @@
1
+ import { LitElement } from "lit";
2
+ export declare class HeatMapElement extends LitElement {
3
+ static properties: {
4
+ heatmapVisible: {
5
+ state: boolean;
6
+ };
7
+ toolbar: {
8
+ type: StringConstructor;
9
+ reflect: boolean;
10
+ };
11
+ };
12
+ private heatmapVisible;
13
+ private toolbar;
14
+ private drawLoopId;
15
+ private readonly tracker;
16
+ static styles: import("lit").CSSResult;
17
+ constructor();
18
+ /**
19
+ * Stops frame rendering when element leaves the document.
20
+ */
21
+ disconnectedCallback(): void;
22
+ /**
23
+ * Starts or stops rendering based on the overlay visibility.
24
+ */
25
+ updated(): void;
26
+ /**
27
+ * Toggles heatmap overlay visibility.
28
+ */
29
+ private toggleHeatmap;
30
+ /**
31
+ * Tracks pointer motion while overlay is hidden.
32
+ */
33
+ private onPointerMove;
34
+ /**
35
+ * Creates a requestAnimationFrame loop for continuous heatmap drawing.
36
+ */
37
+ private startDrawLoop;
38
+ /**
39
+ * Stops the active requestAnimationFrame drawing loop.
40
+ */
41
+ private stopDrawLoop;
42
+ /**
43
+ * Renders the latest tracked hotspot data onto the overlay canvas.
44
+ */
45
+ private drawHeatmap;
46
+ render(): import("lit-html").TemplateResult<1>;
47
+ }
@@ -0,0 +1,176 @@
1
+ import { LitElement, css, html } from "lit";
2
+ import { HeatmapTracker } from "./core/heatmap-tracker.js";
3
+ import { ELEMENT_TRACKER_CONFIG } from "./constants/constants.js";
4
+ import { renderHeatmapOverlay } from "./rendering/renderer.js";
5
+ export class HeatMapElement extends LitElement {
6
+ static properties = {
7
+ heatmapVisible: { state: true },
8
+ toolbar: { type: String, reflect: true }
9
+ };
10
+ drawLoopId = null;
11
+ tracker = new HeatmapTracker(ELEMENT_TRACKER_CONFIG);
12
+ static styles = css `
13
+ :host {
14
+ position: relative;
15
+ display: block;
16
+ min-height: 220px;
17
+ border-radius: 12px;
18
+ overflow: hidden;
19
+ isolation: isolate;
20
+ background: #f4f7ff;
21
+ }
22
+
23
+ .surface {
24
+ position: relative;
25
+ min-height: inherit;
26
+ width: 100%;
27
+ height: 100%;
28
+ z-index: 1;
29
+ }
30
+
31
+ .toggle {
32
+ position: absolute;
33
+ top: 0.55rem;
34
+ left: 0.55rem;
35
+ width: 1.9rem;
36
+ height: 1.9rem;
37
+ border-radius: 999px;
38
+ border: 1px solid rgba(255, 255, 255, 0.5);
39
+ background: rgba(15, 23, 42, 0.82);
40
+ color: #ffffff;
41
+ padding: 0;
42
+ line-height: 1;
43
+ font-size: 0.92rem;
44
+ font-weight: 700;
45
+ cursor: pointer;
46
+ z-index: 10001;
47
+ }
48
+
49
+ .toggle:hover {
50
+ background: rgba(2, 6, 23, 0.95);
51
+ }
52
+
53
+ .overlay {
54
+ position: absolute;
55
+ inset: 0;
56
+ width: 100%;
57
+ height: 100%;
58
+ pointer-events: none;
59
+ z-index: 10000;
60
+ }
61
+ `;
62
+ constructor() {
63
+ super();
64
+ this.heatmapVisible = false;
65
+ this.toolbar = "simple";
66
+ }
67
+ /**
68
+ * Stops frame rendering when element leaves the document.
69
+ */
70
+ disconnectedCallback() {
71
+ this.stopDrawLoop();
72
+ super.disconnectedCallback();
73
+ }
74
+ /**
75
+ * Starts or stops rendering based on the overlay visibility.
76
+ */
77
+ updated() {
78
+ if (this.toolbar !== "simple" && this.toolbar !== "hidden") {
79
+ this.toolbar = "simple";
80
+ }
81
+ if (this.heatmapVisible) {
82
+ this.startDrawLoop();
83
+ return;
84
+ }
85
+ this.stopDrawLoop();
86
+ }
87
+ /**
88
+ * Toggles heatmap overlay visibility.
89
+ */
90
+ toggleHeatmap() {
91
+ this.heatmapVisible = !this.heatmapVisible;
92
+ }
93
+ /**
94
+ * Tracks pointer motion while overlay is hidden.
95
+ */
96
+ onPointerMove(event) {
97
+ if (this.heatmapVisible) {
98
+ return;
99
+ }
100
+ const surface = this.renderRoot.querySelector(".surface");
101
+ if (!surface) {
102
+ return;
103
+ }
104
+ const rect = surface.getBoundingClientRect();
105
+ if (rect.width <= 0 || rect.height <= 0) {
106
+ return;
107
+ }
108
+ const x = Math.min(rect.width, Math.max(0, event.clientX - rect.left));
109
+ const y = Math.min(rect.height, Math.max(0, event.clientY - rect.top));
110
+ this.tracker.recordPosition(x, y, { width: rect.width, height: rect.height });
111
+ }
112
+ /**
113
+ * Creates a requestAnimationFrame loop for continuous heatmap drawing.
114
+ */
115
+ startDrawLoop() {
116
+ if (this.drawLoopId !== null) {
117
+ return;
118
+ }
119
+ const drawFrame = () => {
120
+ this.drawHeatmap();
121
+ this.drawLoopId = window.requestAnimationFrame(drawFrame);
122
+ };
123
+ this.drawLoopId = window.requestAnimationFrame(drawFrame);
124
+ }
125
+ /**
126
+ * Stops the active requestAnimationFrame drawing loop.
127
+ */
128
+ stopDrawLoop() {
129
+ if (this.drawLoopId === null) {
130
+ return;
131
+ }
132
+ window.cancelAnimationFrame(this.drawLoopId);
133
+ this.drawLoopId = null;
134
+ }
135
+ /**
136
+ * Renders the latest tracked hotspot data onto the overlay canvas.
137
+ */
138
+ drawHeatmap() {
139
+ const canvas = this.renderRoot.querySelector("#heatmap");
140
+ const surface = this.renderRoot.querySelector(".surface");
141
+ if (!canvas || !surface) {
142
+ return;
143
+ }
144
+ const width = Math.round(surface.clientWidth);
145
+ const height = Math.round(surface.clientHeight);
146
+ if (width <= 0 || height <= 0) {
147
+ return;
148
+ }
149
+ if (canvas.width !== width || canvas.height !== height) {
150
+ canvas.width = width;
151
+ canvas.height = height;
152
+ }
153
+ const context = canvas.getContext("2d");
154
+ if (!context) {
155
+ return;
156
+ }
157
+ const snapshot = this.tracker.getSnapshot();
158
+ renderHeatmapOverlay(context, width, height, snapshot.hotspots);
159
+ }
160
+ render() {
161
+ return html `
162
+ <div class="surface" @pointermove=${this.onPointerMove}>
163
+ <slot></slot>
164
+ </div>
165
+ ${this.toolbar === "hidden"
166
+ ? null
167
+ : html `<button class="toggle" @click=${this.toggleHeatmap} aria-label="Toggle heatmap">
168
+ ${this.heatmapVisible ? "x" : "*"}
169
+ </button>`}
170
+ ${this.heatmapVisible ? html `<canvas id="heatmap" class="overlay"></canvas>` : null}
171
+ `;
172
+ }
173
+ }
174
+ if (typeof customElements !== "undefined" && !customElements.get("heat-map")) {
175
+ customElements.define("heat-map", HeatMapElement);
176
+ }
@@ -0,0 +1,32 @@
1
+ import { HeatmapTracker } from "./core/heatmap-tracker.js";
2
+ import { DEFAULT_HEATMAP_CONFIG } from "./constants/constants.js";
3
+ import { HeatMapElement } from "./heatmap-element.js";
4
+ import { type HeatmapConfig, type HeatmapSnapshot, type ViewportSize } from "./contracts/heatmap-contracts.js";
5
+ export declare const hello = "hello";
6
+ export { HeatMapElement };
7
+ export { DEFAULT_HEATMAP_CONFIG, HeatmapTracker };
8
+ export type { HeatmapConfig, HeatmapHotspot, HeatmapToolbarMode, HeatmapSnapshot, ViewportSize } from "./contracts/heatmap-contracts.js";
9
+ /**
10
+ * Updates tracker settings for globally tracked mouse heatmap data.
11
+ */
12
+ export declare function configureMouseHeatmap(nextConfig: Partial<HeatmapConfig>): void;
13
+ /**
14
+ * Records a single mouse position sample into the global tracker.
15
+ */
16
+ export declare function recordMousePosition(x: number, y: number, viewport?: ViewportSize): void;
17
+ /**
18
+ * Starts browser pointer tracking for the global tracker.
19
+ */
20
+ export declare function startMouseTracking(): void;
21
+ /**
22
+ * Stops browser pointer tracking for the global tracker.
23
+ */
24
+ export declare function stopMouseTracking(): void;
25
+ /**
26
+ * Clears all currently tracked global heatmap data.
27
+ */
28
+ export declare function resetMouseHeatmap(): void;
29
+ /**
30
+ * Returns the latest global heatmap snapshot.
31
+ */
32
+ export declare function getMouseHeatmapData(): HeatmapSnapshot;
package/dist/index.js ADDED
@@ -0,0 +1,66 @@
1
+ import { HeatmapTracker } from "./core/heatmap-tracker.js";
2
+ import { DEFAULT_HEATMAP_CONFIG } from "./constants/constants.js";
3
+ import { HeatMapElement } from "./heatmap-element.js";
4
+ export const hello = "hello";
5
+ export { HeatMapElement };
6
+ export { DEFAULT_HEATMAP_CONFIG, HeatmapTracker };
7
+ const globalTracker = new HeatmapTracker(DEFAULT_HEATMAP_CONFIG);
8
+ let moveListener = null;
9
+ function getViewportFromWindow() {
10
+ if (typeof window === "undefined") {
11
+ return { width: 0, height: 0 };
12
+ }
13
+ return {
14
+ width: Math.max(0, window.innerWidth),
15
+ height: Math.max(0, window.innerHeight)
16
+ };
17
+ }
18
+ /**
19
+ * Updates tracker settings for globally tracked mouse heatmap data.
20
+ */
21
+ export function configureMouseHeatmap(nextConfig) {
22
+ globalTracker.configure(nextConfig);
23
+ }
24
+ /**
25
+ * Records a single mouse position sample into the global tracker.
26
+ */
27
+ export function recordMousePosition(x, y, viewport = getViewportFromWindow()) {
28
+ globalTracker.recordPosition(x, y, viewport);
29
+ }
30
+ /**
31
+ * Starts browser pointer tracking for the global tracker.
32
+ */
33
+ export function startMouseTracking() {
34
+ if (moveListener || typeof window === "undefined") {
35
+ return;
36
+ }
37
+ moveListener = (event) => {
38
+ recordMousePosition(event.clientX, event.clientY, {
39
+ width: window.innerWidth,
40
+ height: window.innerHeight
41
+ });
42
+ };
43
+ window.addEventListener("pointermove", moveListener, { passive: true });
44
+ }
45
+ /**
46
+ * Stops browser pointer tracking for the global tracker.
47
+ */
48
+ export function stopMouseTracking() {
49
+ if (!moveListener || typeof window === "undefined") {
50
+ return;
51
+ }
52
+ window.removeEventListener("pointermove", moveListener);
53
+ moveListener = null;
54
+ }
55
+ /**
56
+ * Clears all currently tracked global heatmap data.
57
+ */
58
+ export function resetMouseHeatmap() {
59
+ globalTracker.reset();
60
+ }
61
+ /**
62
+ * Returns the latest global heatmap snapshot.
63
+ */
64
+ export function getMouseHeatmapData() {
65
+ return globalTracker.getSnapshot();
66
+ }
@@ -0,0 +1,26 @@
1
+ import { type HeatmapHotspot } from "../contracts/heatmap-contracts.js";
2
+ import { type HeatColor, type HeatmapRenderOptions } from "../contracts/rendering-contracts.js";
3
+ /**
4
+ * Converts an absolute hotspot count into a relative strength ratio.
5
+ */
6
+ export declare function getRelativeStrength(count: number, maxCount: number): number;
7
+ /**
8
+ * Interpolates the heat scale into a single RGB color.
9
+ */
10
+ export declare function interpolateHeatColor(strength: number, colors?: readonly HeatColor[]): [number, number, number];
11
+ /**
12
+ * Calculates merged field strength at a specific coordinate.
13
+ */
14
+ export declare function calculateFieldStrength(x: number, y: number, hotspots: Array<{
15
+ x: number;
16
+ y: number;
17
+ count: number;
18
+ }>, radius: number, maxHotspotCount: number): number;
19
+ /**
20
+ * Maps field strength into a discrete dominant color and alpha.
21
+ */
22
+ export declare function getDominantHeatColor(strength: number, colors?: readonly HeatColor[]): [number, number, number, number];
23
+ /**
24
+ * Draws a contour-style heatmap overlay from hotspots onto a canvas context.
25
+ */
26
+ export declare function renderHeatmapOverlay(context: CanvasRenderingContext2D, width: number, height: number, hotspots: HeatmapHotspot[], options?: HeatmapRenderOptions): void;
@@ -0,0 +1,105 @@
1
+ import { CONTOUR_RADIUS_RATIO, DEFAULT_HEAT_COLORS, DEFAULT_SAMPLE_STEP, DOMINANT_HEAT_LEVELS, FIELD_DISTANCE_MULTIPLIER, FIELD_FALLOFF_DIVISOR, HEAT_ALPHA_BASE, HEAT_ALPHA_MULTIPLIER, LOW_HEAT_ALPHA, LOW_HEAT_THRESHOLD, MAX_HEAT_ALPHA, MIN_CONTOUR_RADIUS, RELATIVE_STRENGTH_EXPONENT } from "../constants/constants.js";
2
+ /**
3
+ * Converts an absolute hotspot count into a relative strength ratio.
4
+ */
5
+ export function getRelativeStrength(count, maxCount) {
6
+ if (maxCount <= 0) {
7
+ return 0;
8
+ }
9
+ return Math.pow(count / maxCount, RELATIVE_STRENGTH_EXPONENT);
10
+ }
11
+ /**
12
+ * Interpolates the heat scale into a single RGB color.
13
+ */
14
+ export function interpolateHeatColor(strength, colors = DEFAULT_HEAT_COLORS) {
15
+ const segments = colors.length - 1;
16
+ const scaled = Math.max(0, Math.min(1, strength)) * segments;
17
+ const lowerIndex = Math.floor(scaled);
18
+ const upperIndex = Math.min(segments, lowerIndex + 1);
19
+ const localT = scaled - lowerIndex;
20
+ const low = colors[lowerIndex];
21
+ const high = colors[upperIndex];
22
+ return [
23
+ Math.round(low[0] + (high[0] - low[0]) * localT),
24
+ Math.round(low[1] + (high[1] - low[1]) * localT),
25
+ Math.round(low[2] + (high[2] - low[2]) * localT)
26
+ ];
27
+ }
28
+ /**
29
+ * Calculates merged field strength at a specific coordinate.
30
+ */
31
+ export function calculateFieldStrength(x, y, hotspots, radius, maxHotspotCount) {
32
+ const radiusSquared = radius * radius;
33
+ let strength = 0;
34
+ for (const spot of hotspots) {
35
+ const dx = x - spot.x;
36
+ const dy = y - spot.y;
37
+ const distanceSquared = dx * dx + dy * dy;
38
+ if (distanceSquared > radiusSquared * FIELD_DISTANCE_MULTIPLIER) {
39
+ continue;
40
+ }
41
+ const localStrength = getRelativeStrength(spot.count, maxHotspotCount);
42
+ const falloff = Math.exp(-distanceSquared / (radiusSquared * FIELD_FALLOFF_DIVISOR));
43
+ strength += localStrength * falloff;
44
+ }
45
+ return Math.min(1, strength);
46
+ }
47
+ /**
48
+ * Maps field strength into a discrete dominant color and alpha.
49
+ */
50
+ export function getDominantHeatColor(strength, colors = DEFAULT_HEAT_COLORS) {
51
+ if (strength <= 0) {
52
+ return [0, 0, 0, 0];
53
+ }
54
+ if (strength < LOW_HEAT_THRESHOLD) {
55
+ const [r, g, b] = interpolateHeatColor(0, colors);
56
+ return [r, g, b, LOW_HEAT_ALPHA];
57
+ }
58
+ for (const level of DOMINANT_HEAT_LEVELS) {
59
+ if (strength <= level) {
60
+ const [r, g, b] = interpolateHeatColor(level, colors);
61
+ return [r, g, b, HEAT_ALPHA_BASE + level * HEAT_ALPHA_MULTIPLIER];
62
+ }
63
+ }
64
+ const [r, g, b] = interpolateHeatColor(1, colors);
65
+ return [r, g, b, MAX_HEAT_ALPHA];
66
+ }
67
+ /**
68
+ * Draws a contour-style heatmap overlay from hotspots onto a canvas context.
69
+ */
70
+ export function renderHeatmapOverlay(context, width, height, hotspots, options = {}) {
71
+ context.clearRect(0, 0, width, height);
72
+ if (hotspots.length === 0) {
73
+ return;
74
+ }
75
+ const sampleStep = options.sampleStep ?? DEFAULT_SAMPLE_STEP;
76
+ const maxContourRadius = options.maxContourRadius ?? Math.max(MIN_CONTOUR_RADIUS, Math.min(width, height) * CONTOUR_RADIUS_RATIO);
77
+ const colors = options.colors ?? DEFAULT_HEAT_COLORS;
78
+ const maxHotspotCount = hotspots.reduce((max, spot) => Math.max(max, spot.count), 1);
79
+ const sampleWidth = Math.ceil(width / sampleStep);
80
+ const sampleHeight = Math.ceil(height / sampleStep);
81
+ const image = context.createImageData(sampleWidth, sampleHeight);
82
+ for (let y = 0; y < sampleHeight; y += 1) {
83
+ for (let x = 0; x < sampleWidth; x += 1) {
84
+ const sampleX = x * sampleStep;
85
+ const sampleY = y * sampleStep;
86
+ const strength = calculateFieldStrength(sampleX, sampleY, hotspots, maxContourRadius, maxHotspotCount);
87
+ const [r, g, b, a] = getDominantHeatColor(strength, colors);
88
+ const index = (y * sampleWidth + x) * 4;
89
+ image.data[index] = r;
90
+ image.data[index + 1] = g;
91
+ image.data[index + 2] = b;
92
+ image.data[index + 3] = Math.round(a * 255);
93
+ }
94
+ }
95
+ const offscreen = document.createElement("canvas");
96
+ offscreen.width = sampleWidth;
97
+ offscreen.height = sampleHeight;
98
+ const offscreenContext = offscreen.getContext("2d");
99
+ if (!offscreenContext) {
100
+ return;
101
+ }
102
+ offscreenContext.putImageData(image, 0, 0);
103
+ context.imageSmoothingEnabled = true;
104
+ context.drawImage(offscreen, 0, 0, sampleWidth, sampleHeight, 0, 0, width, height);
105
+ }
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "heatspot",
3
+ "version": "0.1.0",
4
+ "description": "A tiny ESM TypeScript library.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "prebuild": "rimraf dist",
19
+ "build": "tsc -p tsconfig.build.json",
20
+ "build:verify": "npm run test:coverage && npm run build:harness",
21
+ "prepublishOnly": "npm run test && npm run build",
22
+ "pack:check": "npm pack --dry-run",
23
+ "start": "npm run build && vite --config harness/vite.config.ts",
24
+ "build:harness": "npm run build && vite build --config harness/vite.config.ts",
25
+ "lint": "eslint .",
26
+ "lint:fix": "eslint . --fix",
27
+ "test": "vitest run",
28
+ "test:coverage": "vitest run --coverage",
29
+ "test:check": "tsc -p tsconfig.json --noEmit",
30
+ "test:watch": "vitest"
31
+ },
32
+ "keywords": [
33
+ "heatmap",
34
+ "lit",
35
+ "web-components",
36
+ "typescript",
37
+ "esm"
38
+ ],
39
+ "author": "",
40
+ "license": "MIT",
41
+ "engines": {
42
+ "node": ">=18"
43
+ },
44
+ "dependencies": {
45
+ "lit": "^3.3.2"
46
+ },
47
+ "devDependencies": {
48
+ "@eslint/js": "^9.21.0",
49
+ "@types/node": "^25.3.5",
50
+ "@typescript-eslint/eslint-plugin": "^8.26.1",
51
+ "@typescript-eslint/parser": "^8.26.1",
52
+ "eslint": "^9.21.0",
53
+ "eslint-plugin-import": "^2.31.0",
54
+ "globals": "^16.0.0",
55
+ "jsdom": "^26.0.0",
56
+ "rimraf": "^6.0.1",
57
+ "typescript": "^5.9.3",
58
+ "vite": "^7.3.1",
59
+ "@vitest/coverage-v8": "^4.0.18",
60
+ "vitest": "^4.0.18"
61
+ }
62
+ }