@tomtom-org/maps-sdk-plugin-map-theme 0.2.3 → 0.3.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/LICENSE.txt +4 -4
- package/README.md +1 -1
- package/dist/index.d.ts +30 -32
- package/dist/index.es.js +1 -2
- package/package.json +5 -4
- package/dist/index.es.js.map +0 -1
package/LICENSE.txt
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
TOMTOM MAPS SDK FOR JAVASCRIPT LICENSE AGREEMENT (“AGREEMENT”)
|
|
2
2
|
|
|
3
|
-
Last update:
|
|
3
|
+
Last update: October 2026
|
|
4
4
|
|
|
5
|
-
By downloading or accessing the “TomTom Maps SDK for JavaScript” (the “SDK”), you are entering into a binding agreement with TomTom Global Content B.V. (“TomTom”). The SDK comprises all software, binaries and source code made available by TomTom for it, whether through the TomTom Developer Portal or public code repositories (e.g. GitHub). This Agreement with TomTom consists of the terms and conditions below together with the Data Processing Schedule. You should not use the SDK if you cannot agree to both.
|
|
5
|
+
By downloading or accessing the “TomTom Maps SDK for JavaScript” (the “SDK”), you are entering into a binding agreement with TomTom Global Content B.V. (“TomTom”). The SDK comprises all software, binaries and source code made available by TomTom for it, whether through the TomTom Developer Portal, package registries (e.g. npm) or otherwise. The example applications that TomTom publishes in public code repositories (e.g. GitHub) are not part of the SDK: they are licensed separately, under the license distributed with them. This Agreement with TomTom consists of the terms and conditions below together with the Data Processing Schedule. You should not use the SDK if you cannot agree to both.
|
|
6
6
|
|
|
7
7
|
You may not use the SDK or accept this Agreement if you are barred from receiving it under the laws of the European Union or of any other country, including the country in which you are resident or from which you use the SDK. If you accept this Agreement on behalf of your employer or another entity, you represent and warrant that you have full legal authority to bind that employer or entity. If you do not have that authority, you may not accept this Agreement or use the SDK on their behalf.
|
|
8
8
|
|
|
@@ -14,7 +14,7 @@ You may not use the SDK or accept this Agreement if you are barred from receivin
|
|
|
14
14
|
|
|
15
15
|
3.1. You agree to use the SDK only for purposes permitted by (a) this Agreement and (b) any applicable law, regulation or generally accepted practice or guideline in the relevant jurisdictions (including any laws regarding the export of data or software to and from the United States or other relevant countries).
|
|
16
16
|
|
|
17
|
-
3.2. Other than as permitted by applicable legislation, you are prohibited from copying (except for backup purposes), modifying, adapting, (re)distributing, renting, lending, publicly presenting, decompiling, reverse engineering, disassembling or creating derivative works of the SDK or any part of it. Notwithstanding the foregoing,
|
|
17
|
+
3.2. Other than as permitted by applicable legislation, you are prohibited from copying (except for backup purposes), modifying, adapting, (re)distributing, renting, lending, publicly presenting, decompiling, reverse engineering, disassembling or creating derivative works of the SDK or any part of it. Notwithstanding the foregoing, you may inspect the SDK as distributed to you, including any source code embedded in its source maps, solely for the purpose of debugging your Permitted Application, provided that it is not shared or redistributed. The readability of the SDK’s source code in its distribution does not constitute “Open-Source” licensing and grants you no rights under any Open Source Initiative (OSI) approved license, nor shall it be construed as a grant of rights to modify, fork or redistribute the SDK beyond those expressly granted in Clause 1 and this Clause 3.2.
|
|
18
18
|
|
|
19
19
|
3.3. You may not use the SDK in connection with any illegal, fraudulent, dishonest or unethical activity or behaviour, and in particular not to create or develop, or help others create or develop, a product, service or library that competes, directly or indirectly, with the SDK or the TomTom APIs.
|
|
20
20
|
|
|
@@ -78,7 +78,7 @@ You may not use the SDK or accept this Agreement if you are barred from receivin
|
|
|
78
78
|
|
|
79
79
|
11.4. The TomTom APIs and the SDK are provided as “Commercial Computer Software” or “restricted computer software”. Use, duplication or disclosure by the U.S. Government or a U.S. Government subcontractor is subject to the restrictions set forth in 48 C.F.R. Section 12.212 or 48 C.F.R. 227.2702, as applicable, or successor provisions. In respect of the Map API and Search API, the manufacturer is TomTom Global Content B.V.
|
|
80
80
|
|
|
81
|
-
11.5. By submitting any suggestion, bug fix or code contribution (“Contribution”) to TomTom (e.g. via a GitHub
|
|
81
|
+
11.5. By submitting any suggestion, bug fix or code contribution (“Contribution”) to TomTom (e.g. via a GitHub issue or discussion), you grant TomTom a perpetual, irrevocable, worldwide, royalty-free and non-exclusive license to use, reproduce, modify and distribute the Contribution as part of the SDK. You represent that you have the legal right to grant this license and that the Contribution does not infringe any third-party intellectual property rights.
|
|
82
82
|
|
|
83
83
|
THIRD PARTY SOFTWARE NOTICES
|
|
84
84
|
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Plugin for the [TomTom Maps SDK for JavaScript](https://docs.tomtom.com/maps-sdk
|
|
|
9
9
|
| `cssColors` | A stylesheet's text | Hex colours, most used first |
|
|
10
10
|
| `deriveMapColorsFromImage` | An image — `File`, `Blob`, `<img>`, `ImageBitmap`, CORS-readable URL or raw pixels | The map colours, through its dominant colours |
|
|
11
11
|
| `dominantColors` | Raw pixels | Hex colours, largest area first |
|
|
12
|
-
| `mapColorsLightDark` | The map colours | `'light'` or `'dark'`: switch to a style of the same (`
|
|
12
|
+
| `mapColorsLightDark` | The map colours | `'light'` or `'dark'`: switch to a style of the same (`streetDark` for a dark theme) before applying them |
|
|
13
13
|
|
|
14
14
|
## Docs & examples
|
|
15
15
|
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { LightDark, MapColors } from "@tomtom-org/maps-sdk/map";
|
|
|
7
7
|
*/
|
|
8
8
|
export declare const mapThemeModes: readonly ["light", "dark", "auto"];
|
|
9
9
|
/**
|
|
10
|
-
* Whether a derived theme reads as a light or a dark map. `auto
|
|
10
|
+
* Whether a derived theme reads as a light or a dark map. `auto`, the default, picks dark when the
|
|
11
11
|
* source colours are mostly dark.
|
|
12
12
|
*
|
|
13
13
|
* @group Map Theme
|
|
@@ -35,9 +35,8 @@ type MapThemeExtractionOptions = MapThemeOptions & {
|
|
|
35
35
|
/** @ignore */
|
|
36
36
|
declare const DEFAULT_COLOR_COUNT = 6;
|
|
37
37
|
/**
|
|
38
|
-
* Raw
|
|
39
|
-
*
|
|
40
|
-
* without a canvas.
|
|
38
|
+
* Raw row-major RGBA pixel bytes in the shape of a DOM `ImageData`. {@link deriveMapColorsFromImage}
|
|
39
|
+
* accepts them, so you can theme an image decoded elsewhere (a worker, a test) without a canvas.
|
|
41
40
|
*
|
|
42
41
|
* @group Map Theme
|
|
43
42
|
*/
|
|
@@ -47,7 +46,7 @@ type ImagePixels = {
|
|
|
47
46
|
height: number;
|
|
48
47
|
};
|
|
49
48
|
/**
|
|
50
|
-
* What {@link deriveMapColorsFromImage} accepts:
|
|
49
|
+
* What {@link deriveMapColorsFromImage} accepts: a CORS-readable image URL, a `Blob`/`File`,
|
|
51
50
|
* a loaded `HTMLImageElement`, an `ImageBitmap`, or raw {@link ImagePixels}.
|
|
52
51
|
*
|
|
53
52
|
* @group Map Theme
|
|
@@ -56,16 +55,16 @@ type MapThemeImageSource = string | Blob | HTMLImageElement | ImageBitmap | Imag
|
|
|
56
55
|
//#endregion
|
|
57
56
|
//#region src/cssColors.d.ts
|
|
58
57
|
/**
|
|
59
|
-
* The colours a stylesheet paints with, most used first, as hex strings
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* {@link deriveMapColorsFromCss} does both.
|
|
58
|
+
* The colours a stylesheet paints with, most used first, as hex strings. It resolves `var()`
|
|
59
|
+
* references, weights fills over borders and shadows, drops translucent colours and a colour's
|
|
60
|
+
* shades, and keeps at most two greys while colourful ones remain. Feed the result to
|
|
61
|
+
* {@link deriveMapColors}. {@link deriveMapColorsFromCss} does both.
|
|
63
62
|
*
|
|
64
63
|
* Rules for one colour scheme (`prefers-color-scheme`, a `.dark` or `.light` class, a `data-*="dark"`
|
|
65
|
-
* attribute) are read only with that `mode`,
|
|
64
|
+
* attribute) are read only with that `mode`, then override the defaults. `auto` reads neither.
|
|
66
65
|
* A `var()` resolves against its own rule or the page root, not a custom property set on another selector.
|
|
67
66
|
*
|
|
68
|
-
* @param cssText - The stylesheet's text, e.g. `await file.text()
|
|
67
|
+
* @param cssText - The stylesheet's text, e.g. `await file.text()`. `@import`s are not followed.
|
|
69
68
|
*
|
|
70
69
|
* @group Map Theme
|
|
71
70
|
*/
|
|
@@ -73,15 +72,14 @@ export declare const cssColors: (cssText: string, options?: MapThemeExtractionOp
|
|
|
73
72
|
//#endregion
|
|
74
73
|
//#region src/cssTheme.d.ts
|
|
75
74
|
/**
|
|
76
|
-
* Derives the map colours from a stylesheet
|
|
77
|
-
*
|
|
78
|
-
* {@link deriveMapColors}.
|
|
75
|
+
* Derives the map colours from a stylesheet, such as a site's CSS or a design system's tokens.
|
|
76
|
+
* It reads its colours with {@link cssColors} and assigns them roles with {@link deriveMapColors}.
|
|
79
77
|
*
|
|
80
|
-
* With `mode: 'auto'`, the default, the map
|
|
81
|
-
* `:root`, `html` or `body`, or else the grey most rules fill with, and light when neither
|
|
82
|
-
*
|
|
78
|
+
* With `mode: 'auto'`, the default, the map follows the page's lightness, read off its background
|
|
79
|
+
* on `:root`, `html` or `body`, or else the grey most rules fill with, and is light when neither
|
|
80
|
+
* resolves. It needs no DOM.
|
|
83
81
|
*
|
|
84
|
-
* @param cssText - The stylesheet's text, e.g. `await file.text()
|
|
82
|
+
* @param cssText - The stylesheet's text, e.g. `await file.text()`. `@import`s are not followed.
|
|
85
83
|
* @throws `RangeError` when the stylesheet paints with no opaque colour.
|
|
86
84
|
*
|
|
87
85
|
* @example
|
|
@@ -96,11 +94,11 @@ export declare const deriveMapColorsFromCss: (cssText: string, options?: MapThem
|
|
|
96
94
|
//#endregion
|
|
97
95
|
//#region src/dominantColors.d.ts
|
|
98
96
|
/**
|
|
99
|
-
* The dominant colours of an image, largest area first, as hex strings
|
|
100
|
-
* sample of opaque pixels,
|
|
101
|
-
* {@link deriveMapColors}
|
|
97
|
+
* The dominant colours of an image, largest area first, as hex strings. A small k-means over a
|
|
98
|
+
* sample of opaque pixels finds them, merging near-identical clusters. Feed the result to
|
|
99
|
+
* {@link deriveMapColors}. {@link deriveMapColorsFromImage} does both.
|
|
102
100
|
*
|
|
103
|
-
* @param count - The most colours to return (default 6)
|
|
101
|
+
* @param count - The most colours to return (default 6). Merged clusters can leave fewer.
|
|
104
102
|
*
|
|
105
103
|
* @group Map Theme
|
|
106
104
|
*/
|
|
@@ -108,7 +106,7 @@ export declare const dominantColors: (pixels: ImagePixels, count?: number) => st
|
|
|
108
106
|
//#endregion
|
|
109
107
|
//#region src/imageTheme.d.ts
|
|
110
108
|
/**
|
|
111
|
-
* Derives the map colours from an image
|
|
109
|
+
* Derives the map colours from an image (a photo, a logo, a poster) by extracting its
|
|
112
110
|
* dominant colours and assigning them roles with {@link deriveMapColors}.
|
|
113
111
|
*
|
|
114
112
|
* @throws `RangeError` when the image has no opaque pixels, or an error when it cannot be read (not
|
|
@@ -126,8 +124,8 @@ export declare const deriveMapColorsFromImage: (source: MapThemeImageSource, opt
|
|
|
126
124
|
//#endregion
|
|
127
125
|
//#region src/roleAssignment.d.ts
|
|
128
126
|
/**
|
|
129
|
-
* Assigns a handful of colours
|
|
130
|
-
* the ten base-map ones plus `accent`, ready for {@link StylingFoundationsModule.setMapColors}.
|
|
127
|
+
* Assigns a handful of colours (a brand's, a website's, an artist's, a mood's) to the map colours,
|
|
128
|
+
* namely the ten base-map ones plus `accent`, ready for {@link StylingFoundationsModule.setMapColors}.
|
|
131
129
|
*
|
|
132
130
|
* @remarks
|
|
133
131
|
* Deterministic, and no two roles share a colour:
|
|
@@ -137,11 +135,11 @@ export declare const deriveMapColorsFromImage: (source: MapThemeImageSource, opt
|
|
|
137
135
|
* - `label`: the one contrasting most with `land`, when it reaches WCAG 4.5:1.
|
|
138
136
|
* - `accent`: the most colourful one left reading against `land` (3:1) and apart from `roadMajor`.
|
|
139
137
|
*
|
|
140
|
-
* Picked colours come back as given
|
|
141
|
-
* `roadMajor`'s complement
|
|
142
|
-
* dark
|
|
138
|
+
* Picked colours come back as given. Roles no colour fits are `hsl()` shades of `land`, or
|
|
139
|
+
* `roadMajor`'s complement for the accent, so the result is complete. Apply it over a style of the
|
|
140
|
+
* same light or dark (see {@link mapColorsLightDark}).
|
|
143
141
|
*
|
|
144
|
-
* @param colors - One to roughly eight CSS colours
|
|
142
|
+
* @param colors - One to roughly eight CSS colours, in any order. Near-duplicates count once.
|
|
145
143
|
* @throws `RangeError` when no colour can be parsed.
|
|
146
144
|
*
|
|
147
145
|
* @example
|
|
@@ -155,14 +153,14 @@ export declare const deriveMapColorsFromImage: (source: MapThemeImageSource, opt
|
|
|
155
153
|
export declare const deriveMapColors: (colors: string[], options?: MapThemeOptions) => Required<MapColors>;
|
|
156
154
|
/**
|
|
157
155
|
* Whether map colours make a light or a dark map (dark when labels are lighter than land), to apply
|
|
158
|
-
* them over a style of the same
|
|
159
|
-
* offsets, which a dark land over `
|
|
156
|
+
* them over a style of the same. {@link StylingFoundationsModule.setMapColors} keeps the loaded style's shade
|
|
157
|
+
* offsets, which a dark land over `streetLight` clamps to black. It tells what `auto` chose.
|
|
160
158
|
*
|
|
161
159
|
* @example
|
|
162
160
|
* ```typescript
|
|
163
161
|
* const colors = await deriveMapColorsFromImage(file);
|
|
164
162
|
* const lightDark = mapColorsLightDark(colors);
|
|
165
|
-
* if (lightDark !== map.styleLightDarkTheme) await map.setStyle(lightDark === 'dark' ? '
|
|
163
|
+
* if (lightDark !== map.styleLightDarkTheme) await map.setStyle(lightDark === 'dark' ? 'streetDark' : 'streetLight');
|
|
166
164
|
* styling.setMapColors(colors);
|
|
167
165
|
* ```
|
|
168
166
|
*
|
package/dist/index.es.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
import{clampRgb as e,differenceEuclidean as t,differenceHueNaive as o,formatHex as r,modeHsl as s,modeLrgb as a,modeOklab as n,modeOklch as l,modeRgb as i,parse as c,useMode as h,wcagContrast as d,wcagLuminance as p}from"culori/fn";import{formatStyleHsl as u,hueDistance as m,toHsl as f}from"@tomtom-org/maps-sdk/map";h(i),h(a),h(s),h(n);var g=h(l),w=t("oklab"),k=(e,t)=>w(e,t)<.035,b=([e,t,o])=>({mode:"rgb",r:e/255,g:t/255,b:o/255}),M=["light","dark","auto"],v=(e,t)=>new RegExp(String.raw`prefers-color-scheme\s*:\s*${e}|\.(?:theme-)?${e}(?:-(?:mode|theme))?(?![\w-])|\[data-[\w-]+=["']?${e}["']?\]`,t),x=["light","dark"],C={light:v("light","i"),dark:v("dark","i")},S=v("(?:light|dark)","gi"),y=/^(?:(?::root|html|body)\s*)+$/,I=/^background(-color)?$/,E=/url\((?:"[^"]*"|'[^']*'|[^)]*)\)|"[^"]*"|'[^']*'/gi,$=new RegExp([/#[\da-f]{3,8}\b/,/\b(?:rgba?|hsla?|oklab|oklch)\([^()]*\)/,/\b[a-z]+\b/].map(({source:e})=>e).join("|"),"i"),
|
|
2
|
-
//# sourceMappingURL=index.es.js.map
|
|
1
|
+
import{clampRgb as e,differenceEuclidean as t,differenceHueNaive as o,formatHex as r,modeHsl as s,modeLrgb as a,modeOklab as n,modeOklch as l,modeRgb as i,parse as c,useMode as h,wcagContrast as d,wcagLuminance as p}from"culori/fn";import{formatStyleHsl as u,hueDistance as m,toHsl as f}from"@tomtom-org/maps-sdk/map";h(i),h(a),h(s),h(n);var g=h(l),w=t("oklab"),k=(e,t)=>w(e,t)<.035,b=([e,t,o])=>({mode:"rgb",r:e/255,g:t/255,b:o/255}),M=["light","dark","auto"],v=(e,t)=>new RegExp(String.raw`prefers-color-scheme\s*:\s*${e}|\.(?:theme-)?${e}(?:-(?:mode|theme))?(?![\w-])|\[data-[\w-]+=["']?${e}["']?\]`,t),x=["light","dark"],C={light:v("light","i"),dark:v("dark","i")},S=v("(?:light|dark)","gi"),y=/^(?:(?::root|html|body)\s*)+$/,I=/^background(-color)?$/,E=/url\((?:"[^"]*"|'[^']*'|[^)]*)\)|"[^"]*"|'[^']*'/gi,$=new RegExp([/#[\da-f]{3,8}\b/,/\b(?:rgba?|hsla?|oklab|oklch)\([^()]*\)/,/\b[a-z]+\b/].map(({source:e})=>e).join("|"),"i"),R=new RegExp($,"gi"),z=/var\(\s*(--[\w-]+)\s*(?:,([^()]*(?:\([^()]*\)[^()]*)*))?\)/g,F=[[/^(background|fill|accent-color|caret-color)|^color$/,1],[/^(border|outline|stroke|column-rule|text-decoration)/,.3],[/shadow$/,.1]],O=/brand|primary|accent/i,q=e=>e.startsWith("--"),H=e=>""===e||e.split(",").some(e=>y.test(e.trim())),L=(e,t)=>`${H(e)?"":e}\0${t}`,j=e=>x.find(t=>e.some(e=>C[t].test(e))),A=(e,t,o)=>{const r=e[t];if(o.escaped)o.escaped=!1;else if("\\"===r)o.escaped=!0;else if(o.quote)r===o.quote&&(o.quote=void 0);else if('"'===r||"'"===r)o.quote=r;else if("("===r)o.parentheses++;else if(")"===r)o.parentheses=Math.max(0,o.parentheses-1);else if(o.parentheses||"{"!==r){if(!o.parentheses&&(";"===r||"}"===r)){const s=e.slice(o.start,t);return o.start=t+1,{text:s,closesRule:"}"===r}}}else o.preludes.push(e.slice(o.start,t).trim()),o.start=t+1},B=(e,t)=>{const o=e.indexOf(":"),r=t.at(-1);if(o<=0||void 0===r)return;const s=e.slice(0,o).trim();return{property:q(s)?s:s.toLowerCase(),value:e.slice(o+1).trim(),selector:r.replace(S,"").replace(/\s+/g," ").trim(),scheme:j(t)}},D=(e,t,o,r=0)=>r>8?e:e.replace(z,(e,s,a="")=>{const n=o.get(L(t,s))??o.get(L("",s));return D(n??a,t,o,r+1)}),N=(t,o,s)=>{for(const[a]of o.replace(E," ").matchAll(R)){const o=c(a);if(!o||(o.alpha??1)<1)continue;const n=e(o),l=r(n),i=t.get(l)??{hex:l,weight:0,color:g(n)};i.weight+=s,t.set(l,i)}},P=(e,t)=>q(e)?$.exec(t)?.[0]!==t.trim()?0:O.test(e)?1:.5:F.find(([t])=>t.test(e))?.[1]??0,T=(e,t)=>{const o=(e=>{const t=e.replace(/\/\*[\s\S]*?\*\//g,""),o={preludes:[],start:0,parentheses:0,escaped:!1},r=[];for(let s=0;s<t.length;s++){const e=A(t,s,o);if(!e)continue;const a=B(e.text,o.preludes);a&&r.push(a),e.closesRule&&o.preludes.pop()}return r})(e),r=({selector:e,property:t})=>L(e,t),s=new Set(o.filter(({scheme:e})=>e===t).map(r));return o.filter(e=>e.scheme?e.scheme===t:!s.has(r(e)))},V=e=>e.c<.03,W=(e,t)=>t.weight-e.weight,Y=e=>{const t=[];for(const r of[...e].toSorted(W)){const e=t.find(e=>{return t=e.color,s=r.color,V(t)||V(s)?V(t)&&V(s)&&Math.abs(t.l-s.l)<.06:Math.abs(o({h:t.h??0},{h:s.h??0}))<20&&Math.abs(t.l-s.l)<.2;var t,s});e?e.weight+=r.weight:t.push({...r})}return t.toSorted(W)},_=(e,t)=>{const o=(e=>{const[t,...o]=e,r=o.find(e=>Math.abs(e.color.l-t.color.l)>=.4);return r?[t,r,...o.filter(e=>e!==r)]:e})(e.filter(e=>V(e.color))),r=e.filter(e=>!V(e.color)),s=Math.min(t,Math.max(Math.min(o.length,2),t-r.length));return[...o.slice(0,s),...r.slice(0,t-s)].toSorted(W).map(e=>e.hex)},G=(e,t)=>{const o=e.filter(({property:e})=>I.test(e)),r=o.findLast(({selector:e})=>H(e)),s=/* @__PURE__ */new Map;r&&N(s,D(r.value,r.selector,t),1);const a=/* @__PURE__ */new Map;for(const{value:l,selector:i}of o)N(a,D(l,i,t),1);const[n]=s.size?s.values():[...a.values()].filter(e=>V(e.color)).toSorted(W);return n&&n.color.l<.5?"dark":"light"},J=(e,{mode:t="auto",colorCount:o=6}={})=>{const r=T(e,t),s=(e=>new Map(e.filter(({property:e})=>q(e)).map(({selector:e,property:t,value:o})=>[L(e,t),o])))(r),a=/* @__PURE__ */new Map;for(const{property:n,value:l,selector:i}of r){const e=D(l,i,s),t=P(n,e);t&&N(a,e,t)}return{colors:_(Y(a.values()),o),mode:"auto"===t?G(r,s):t}},K=(e,t={})=>J(e,t).colors,Q=[170,260],U=[150,280],X=[70,170],Z=(e,[t,o])=>void 0!==e.h&&e.h>=t&&e.h<=o,ee=(e,t,o)=>u({h:e,s:t,l:o}),te=e=>{const t=f(e);return t&&{...t,css:e,chroma:g(t).c}},oe=class{pool;dark;land;constructor(e,t){this.pool=[...e];const o=e.reduce((e,t)=>e+t.l,0)/e.length;var r;this.dark="dark"===t||"auto"===t&&o<.45,this.land=(r=this.take(this.pickLand())??e[0]).s<=.14?r:te(ee(r.h,.14,r.l))??r}pickLand(){const e=this.pool.filter(e=>e.s<=.4);return(e.length?e:this.pool).toSorted((e,t)=>this.dark?e.l-t.l:t.l-e.l)[0]}take(e){if(e)return this.pool.splice(this.pool.indexOf(e),1),e}takeMostColorful(e){const t=this.pool.filter(e).toSorted((e,t)=>t.chroma-e.chroma),o=t.find(e=>!Z(e,U)&&!Z(e,X));return this.take(o??t[0])}takeHue(e){const t=this.pool.filter(t=>t.s>=.12&&Z(t,e));return this.take(t.toSorted((e,t)=>t.s-e.s)[0])}takeMostContrasting(){const e=e=>d(e,this.land),t=this.pool.toSorted((t,o)=>e(o)-e(t))[0];return t&&e(t)>=4.5?this.take(t):void 0}shade(e,t={}){return ee(t.h??this.land.h,t.s??this.land.s,this.land.l+(this.dark?1:-1)*e)}labelShade(e,t){const o=this.shade(e,{s:t});return d(o,this.land)>=4.5?o:this.shade(-e,{s:t})}},re=(e,t={})=>{const o=e.map(te).filter(e=>void 0!==e),r=o.filter((e,t)=>!o.slice(0,t).some(t=>k(t,e)));if(!r.length)throw new RangeError("deriveMapColors needs at least one parseable CSS colour.");const s=new oe(r,t.mode??"auto"),{dark:a,land:n}=s,l=s.takeMostColorful(e=>e.s>=.2)??te(s.shade(a?.55:.45,{s:.7}))??n,i=(s.takeHue(Q)??s.takeHue(U))?.css??s.shade(a?.12:.14,{h:205,s:Math.max(.35,n.s)}),c=s.takeHue(X)?.css??s.shade(a?.08:.09,{h:110,s:Math.max(.18,.6*n.s)}),h=s.takeMostContrasting()?.css??s.labelShade(a?.8:.78,Math.min(n.s,.2)),p=((e,t)=>{const{dark:o,land:r}=e,s=e=>void 0!==e&&d(e,r)>=3,a=e.takeMostColorful(e=>e.s>=.3&&s(e)&&m(e,t)>=30);if(a)return a.css;const n=(t.h??0)+180;return(o?[.62,.7,.78]:[.38,.3,.22]).map(e=>te(ee(n,.7,e))).find(s)?.css??ee(n,.7,o?.85:.15)})(s,l),u=te(c)??n;return{land:n.css,water:i,vegetation:c,park:ee(u.h,u.s,u.l+(a?.04:.05)),artificial:s.shade(a?.04:.03),roadMajor:l.css,road:s.shade(a?.1:-.08,{s:.5*n.s}),roadOutline:s.shade(a?.24:.22,{s:.6*n.s}),label:h,labelHalo:s.shade(-.03),accent:p}},se=({land:e,label:t})=>p(t)>p(e)?"dark":"light",ae=(e,t={})=>{const{colors:o,mode:r}=J(e,t);if(!o.length)throw new RangeError("deriveMapColorsFromCss: the stylesheet paints with no opaque colour to take a theme from.");return re(o,{mode:r})},ne=(e,t)=>(e[0]-t[0])**2+(e[1]-t[1])**2+(e[2]-t[2])**2,le=(e,t)=>{let o=0,r=Number.POSITIVE_INFINITY;for(let s=0;s<t.length;s++){const a=ne(e,t[s]);a<r&&(r=a,o=s)}return o},ie=(e,t=6)=>{const o=(({data:e})=>{const t=Math.floor(e.length/4),o=Math.max(1,Math.floor(t/4096)),r=[];for(let s=0;s<t;s+=o){const t=4*s;e[t+3]<128||r.push([e[t],e[t+1],e[t+2]])}return r})(e);if(!o.length)return[];const{centroids:s,sizes:a}=((e,t)=>{let o=t,r=[];for(let s=0;s<8;s++){const t=o.map(()=>[0,0,0]);r=o.map(()=>0);for(const s of e){const e=le(s,o);r[e]++,t[e][0]+=s[0],t[e][1]+=s[1],t[e][2]+=s[2]}o=o.map((e,o)=>r[o]?t[o].map(e=>e/r[o]):e)}return{centroids:o,sizes:r}})(o,((e,t)=>{const o=e=>p(b(e)),r=[[...e.toSorted((e,t)=>o(e)-o(t))[Math.floor(e.length/2)]]];for(;r.length<t;){let t=e[0],o=-1;for(const s of e){const e=ne(s,r[le(s,r)]);e>o&&(o=e,t=s)}if(0===o)break;r.push([...t])}return r})(o,Math.min(t,o.length)));return((e,t)=>{const o=[];for(const[r,s]of e.entries()){if(!t[r])continue;const e=o.find(e=>k(b(e.rgb),b(s)));e?e.size+=t[r]:o.push({rgb:s,size:t[r]})}return o})(s,a).toSorted((e,t)=>t.size-e.size).map(e=>r(b(e.rgb)))},ce=64,he=async e=>{return"string"==typeof e?(t=e,new Promise((e,o)=>{const r=new Image;r.crossOrigin="anonymous",r.onload=()=>e(r),r.onerror=()=>o(/* @__PURE__ */new Error(`deriveMapColorsFromImage: could not load '${t}'.`)),r.src=t})):e instanceof Blob?createImageBitmap(e):e;var t},de=async(e,t={})=>{const o=(e=>"object"==typeof e&&"data"in e)(e)?e:await(async e=>{if("undefined"==typeof document)throw new TypeError("deriveMapColorsFromImage needs a DOM to decode images; pass ImagePixels instead.");const t=await he(e),o=document.createElement("canvas");o.width=ce,o.height=ce;const r=o.getContext("2d");if(!r)throw new Error("deriveMapColorsFromImage: no 2D canvas context available.");r.drawImage(t,0,0,ce,ce);try{return r.getImageData(0,0,ce,ce)}catch{throw new Error("deriveMapColorsFromImage: the image is not CORS-readable (canvas tainted). Serve it with Access-Control-Allow-Origin or pass a Blob/File.")}})(e),r=ie(o,t.colorCount);if(!r.length)throw new RangeError("deriveMapColorsFromImage: the image has no opaque pixels to take colours from.");return re(r,t)};export{K as cssColors,re as deriveMapColors,ae as deriveMapColorsFromCss,de as deriveMapColorsFromImage,ie as dominantColors,se as mapColorsLightDark,M as mapThemeModes};
|
package/package.json
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
"name": "@tomtom-org/maps-sdk-plugin-map-theme",
|
|
3
3
|
"author": "TomTom International B.V.",
|
|
4
4
|
"license": "See in LICENSE.txt",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.3.1",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"description": "Theme a TomTomMap from wherever a look comes from — a brand's colours, a website's, a described mood, an artist's palette, or an image — by assigning them to the SDK's map colours",
|
|
8
8
|
"types": "dist/index.d.ts",
|
|
9
9
|
"peerDependencies": {
|
|
10
|
-
"@tomtom-org/maps-sdk": ">=0.
|
|
10
|
+
"@tomtom-org/maps-sdk": ">=0.64.0 <1.0.0 || >=1.0.0-rc.0 <1.0.0",
|
|
11
11
|
"culori": "^4.0.2"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
@@ -19,14 +19,15 @@
|
|
|
19
19
|
"typescript": "^6.0.3",
|
|
20
20
|
"vite": "^8.3.2",
|
|
21
21
|
"vitest": "^4.1.11",
|
|
22
|
-
"@tomtom-org/maps-sdk": "0.
|
|
22
|
+
"@tomtom-org/maps-sdk": "1.0.0-rc.0"
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
25
|
"dist",
|
|
26
26
|
"package.json",
|
|
27
27
|
"README.md",
|
|
28
28
|
"LICENSE.txt",
|
|
29
|
-
"THIRD_PARTY.txt"
|
|
29
|
+
"THIRD_PARTY.txt",
|
|
30
|
+
"!**/*.map"
|
|
30
31
|
],
|
|
31
32
|
"exports": {
|
|
32
33
|
".": {
|
package/dist/index.es.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.es.js","names":["registerColorSpace","clampRgb","differenceEuclidean","differenceHueNaive","formatHex","modeHsl","modeLrgb","modeOklab","modeOklch","modeRgb","parse","useMode","wcagContrast","wcagLuminance","formatStyleHsl","hueDistance","toHsl","toOklch","oklabDistance","isSameColor","a","b","rgbFromBytes","red","green","blue","mode","r","g","mapThemeModes","schemeMarker","scheme","flags","RegExp","String","raw","COLOR_SCHEMES","SCHEME_MARKERS","light","dark","ANY_SCHEME_MARKER","PAGE_ROOT","PAGE_BACKGROUND","NON_PAINTING_TEXT","COLOR_TOKEN","map","source","join","COLOR_TOKENS","VARIABLE_REFERENCE","USAGE_WEIGHTS","BRAND_NAME","isCustomProperty","property","startsWith","isPageRoot","selector","split","some","part","test","trim","scopeKey","schemeOf","preludes","find","prelude","scanCharacter","css","index","state","character","escaped","quote","parentheses","Math","max","text","slice","start","closesRule","push","declarationOf","colon","indexOf","at","toLowerCase","value","replace","resolveVariables","variables","depth","_reference","name","fallback","definition","get","tally","swatches","weight","token","matchAll","parsed","alpha","rgb","hex","swatch","color","set","weightOf","resolvedValue","exec","pattern","declarationsFor","cssText","declarations","length","scanned","declaration","pop","declarationsOf","key","overridden","Set","filter","has","isNeutral","c","byWeight","mergeShades","tallied","merged","toSorted","main","existing","isShadeOf","abs","l","h","pickSwatches","count","neutrals","first","rest","opposite","neutralsByRole","colorful","neutralCount","min","pageMode","backgrounds","rootBackground","findLast","rootColors","Map","fills","page","size","values","readStylesheet","colorCount","variablesOf","colors","cssColors","options","WATER_HUES","WIDER_WATER_HUES","VEGETATION_HUES","isInHues","from","to","hsl","s","parseColor","chroma","RoleAssigner","pool","land","constructor","this","meanLightness","reduce","sum","take","pickLand","splice","takeMostColorful","isEligible","candidates","unclaimed","takeHue","hues","inRange","takeMostContrasting","againstLand","best","shade","lightnessOffset","labelShade","towards","deriveMapColors","distinct","seen","RangeError","roles","roadMajor","water","vegetation","label","accent","readsOnLand","picked","complementHue","lightness","deriveAccent","park","artificial","road","roadOutline","labelOutline","mapColorsLightDark","deriveMapColorsFromCss","squaredDistance","nearestIndex","sample","centroids","nearest","nearestDistance","Number","POSITIVE_INFINITY","distance","dominantColors","pixels","samples","data","pixelCount","floor","stride","offset","sampleOpaquePixels","sizes","seeds","iteration","sums","centroid","cluster","kMeans","luminanceOf","farthest","farthestDistance","seedCentroids","entries","twin","entry","mergeClusters","DECODE_SIDE","toDrawable","async","url","Promise","resolve","reject","image","Image","crossOrigin","onload","onerror","Error","src","Blob","createImageBitmap","deriveMapColorsFromImage","isImagePixels","document","TypeError","drawable","canvas","createElement","width","height","context","getContext","drawImage","getImageData","readPixels"],"sources":["../src/colorSpaces.ts","../src/types.ts","../src/cssColors.ts","../src/roleAssignment.ts","../src/cssTheme.ts","../src/dominantColors.ts","../src/imageTheme.ts"],"sourcesContent":["import type { Color, Rgb } from 'culori/fn';\nimport {\n differenceEuclidean,\n modeHsl,\n modeLrgb,\n modeOklab,\n modeOklch,\n modeRgb,\n useMode as registerColorSpace,\n} from 'culori/fn';\n\n// The colour spaces this plugin's own culori calls need: sRGB for hex, linear sRGB for luminance and\n// contrast, HSL and OKLab for the colours a stylesheet writes in them, OKLCH for chroma. The SDK\n// registers some of them too, but an app can resolve a second culori copy for the plugin.\nregisterColorSpace(modeRgb);\nregisterColorSpace(modeLrgb);\nregisterColorSpace(modeHsl);\nregisterColorSpace(modeOklab);\n\n/**\n * A colour in OKLCH: perceptual lightness, chroma (how colourful it looks) and hue.\n * @ignore\n */\nexport const toOklch = registerColorSpace(modeOklch);\n\n// Below this OKLab distance two colours read as one.\nconst SAME_COLOR_DISTANCE = 0.035;\nconst oklabDistance = differenceEuclidean('oklab');\n\n/**\n * Whether two colours read as one: an image's near-identical clusters, a palette's near-duplicates.\n * @ignore\n */\nexport const isSameColor = (a: Color, b: Color): boolean => oklabDistance(a, b) < SAME_COLOR_DISTANCE;\n\n/**\n * Red, green and blue bytes (0–255), as image pixels carry them, as a culori colour.\n * @ignore\n */\nexport const rgbFromBytes = ([red, green, blue]: readonly number[]): Rgb => ({\n mode: 'rgb',\n r: red / 255,\n g: green / 255,\n b: blue / 255,\n});\n","import type { LightDark } from '@tomtom-org/maps-sdk/map';\n\n/**\n * Every {@link MapThemeMode}, for validating one at runtime.\n *\n * @group Map Theme\n */\nexport const mapThemeModes = ['light', 'dark', 'auto'] as const satisfies readonly (LightDark | 'auto')[];\n\n/**\n * Whether a derived theme reads as a light or a dark map. `auto` (the default) picks dark when the\n * source colours are mostly dark.\n *\n * @group Map Theme\n */\nexport type MapThemeMode = (typeof mapThemeModes)[number];\n\n/**\n * Options for {@link deriveMapColors}.\n *\n * @group Map Theme\n */\nexport type MapThemeOptions = {\n /** Default `auto`. */\n mode?: MapThemeMode;\n};\n\n/**\n * Options for {@link deriveMapColorsFromImage} and {@link deriveMapColorsFromCss}, which extract a few\n * colours from their source before assigning them roles.\n *\n * @group Map Theme\n */\nexport type MapThemeExtractionOptions = MapThemeOptions & {\n /** The most colours to extract. Default 6. */\n colorCount?: number;\n};\n\n/** @ignore */\nexport const DEFAULT_COLOR_COUNT = 6;\n\n/**\n * Raw pixels in the shape of a DOM `ImageData`: RGBA bytes, row-major. Accepted by\n * {@link deriveMapColorsFromImage} so an image decoded elsewhere (a worker, a test) can be themed\n * without a canvas.\n *\n * @group Map Theme\n */\nexport type ImagePixels = { data: Uint8ClampedArray | Uint8Array | number[]; width: number; height: number };\n\n/**\n * What {@link deriveMapColorsFromImage} accepts: an image URL (must be CORS-readable), a `Blob`/`File`,\n * a loaded `HTMLImageElement`, an `ImageBitmap`, or raw {@link ImagePixels}.\n *\n * @group Map Theme\n */\nexport type MapThemeImageSource = string | Blob | HTMLImageElement | ImageBitmap | ImagePixels;\n","import type { LightDark } from '@tomtom-org/maps-sdk/map';\nimport { clampRgb, differenceHueNaive, formatHex, type Oklch, parse } from 'culori/fn';\nimport { toOklch } from './colorSpaces';\nimport { DEFAULT_COLOR_COUNT, type MapThemeExtractionOptions, type MapThemeMode } from './types';\n\n// `scheme` is set on a rule for one colour scheme only. `selector` has the class or attribute that\n// marks the scheme removed, so `.dark` and `html[data-theme=dark]` select the page root as `:root` does.\ntype Declaration = { property: string; value: string; selector: string; scheme?: LightDark };\n\ntype Swatch = { hex: string; weight: number; color: Oklch };\n\n// What marks a rule as one colour scheme's: the media query, or the class or `data-*` attribute a\n// site toggles (`.dark`, `.dark-mode`, `.theme-dark`, `[data-theme=\"dark\"]`) — not a utility class\n// such as `.bg-dark`.\nconst schemeMarker = (scheme: string, flags: string): RegExp =>\n new RegExp(\n String.raw`prefers-color-scheme\\s*:\\s*${scheme}|\\.(?:theme-)?${scheme}(?:-(?:mode|theme))?(?![\\w-])|\\[data-[\\w-]+=[\"']?${scheme}[\"']?\\]`,\n flags,\n );\nconst COLOR_SCHEMES = ['light', 'dark'] as const satisfies readonly LightDark[];\nconst SCHEME_MARKERS: Record<LightDark, RegExp> = {\n light: schemeMarker('light', 'i'),\n dark: schemeMarker('dark', 'i'),\n};\nconst ANY_SCHEME_MARKER = schemeMarker('(?:light|dark)', 'gi');\nconst PAGE_ROOT = /^(?:(?::root|html|body)\\s*)+$/;\nconst PAGE_BACKGROUND = /^background(-color)?$/;\n// Text in a value that paints nothing, though words in it read as colour names: `url(/img/tan.png)`.\nconst NON_PAINTING_TEXT = /url\\((?:\"[^\"]*\"|'[^']*'|[^)]*)\\)|\"[^\"]*\"|'[^']*'/gi;\nconst HEX_COLOR = /#[\\da-f]{3,8}\\b/;\nconst COLOR_FUNCTION = /\\b(?:rgba?|hsla?|oklab|oklch)\\([^()]*\\)/;\nconst COLOR_NAME = /\\b[a-z]+\\b/;\nconst COLOR_TOKEN = new RegExp([HEX_COLOR, COLOR_FUNCTION, COLOR_NAME].map(({ source }) => source).join('|'), 'i');\nconst COLOR_TOKENS = new RegExp(COLOR_TOKEN, 'gi');\nconst VARIABLE_REFERENCE = /var\\(\\s*(--[\\w-]+)\\s*(?:,([^()]*(?:\\([^()]*\\)[^()]*)*))?\\)/g;\nconst MAX_VARIABLE_DEPTH = 8;\n\n// What a colour counts for where it is used: what a page fills with dominates its look, lines and\n// shadows barely register, and a property that paints nothing (a font, a grid area) counts for 0.\nconst USAGE_WEIGHTS: [RegExp, number][] = [\n [/^(background|fill|accent-color|caret-color)|^color$/, 1],\n [/^(border|outline|stroke|column-rule|text-decoration)/, 0.3],\n [/shadow$/, 0.1],\n];\n// A custom property holding one colour counts once where it is defined, so a file of design tokens\n// alone still ranks them; one named as the brand's counts double.\nconst DEFINITION_WEIGHT = 0.5;\nconst BRAND_NAME = /brand|primary|accent/i;\n\n// Below this OKLCH chroma a colour reads as a grey. A stylesheet has many; a theme needs two of them\n// at most (a background and a text colour) while colourful ones remain.\nconst NEUTRAL_CHROMA = 0.03;\nconst MAX_NEUTRALS = 2;\n// OKLCH lightness apart that a background and a text colour are at least.\nconst OPPOSITE_LIGHTNESS = 0.4;\n\nconst isCustomProperty = (property: string): boolean => property.startsWith('--');\n\n// Whether a rule styles the whole page, so that its custom properties reach every other rule.\nconst isPageRoot = (selector: string): boolean =>\n selector === '' || selector.split(',').some((part) => PAGE_ROOT.test(part.trim()));\n\n// Declarations with one key are one setting: the same property, on the same selector or anywhere on\n// the page root.\nconst scopeKey = (selector: string, property: string): string =>\n `${isPageRoot(selector) ? '' : selector}\\u0000${property}`;\n\nconst schemeOf = (preludes: string[]): LightDark | undefined =>\n COLOR_SCHEMES.find((scheme) => preludes.some((prelude) => SCHEME_MARKERS[scheme].test(prelude)));\n\ntype ScanState = { preludes: string[]; start: number; parentheses: number; quote?: string; escaped: boolean };\n\ntype ScannedDeclaration = { text: string; closesRule: boolean };\n\n// Advances the scan over one character; returns a declaration's text when the character ends one.\n// Escaped characters, quoted strings and parentheses are stepped over, so neither the `;` in\n// `url(\"data:…;base64,…\")` nor the quotes in `content: \"\\\\\"` or `.content-\\[\\'x\\'\\]` end anything.\nconst scanCharacter = (css: string, index: number, state: ScanState): ScannedDeclaration | undefined => {\n const character = css[index];\n if (state.escaped) {\n state.escaped = false;\n } else if (character === '\\\\') {\n state.escaped = true;\n } else if (state.quote) {\n if (character === state.quote) state.quote = undefined;\n } else if (character === '\"' || character === \"'\") {\n state.quote = character;\n } else if (character === '(') {\n state.parentheses++;\n } else if (character === ')') {\n state.parentheses = Math.max(0, state.parentheses - 1);\n } else if (!state.parentheses && character === '{') {\n state.preludes.push(css.slice(state.start, index).trim());\n state.start = index + 1;\n } else if (!state.parentheses && (character === ';' || character === '}')) {\n const text = css.slice(state.start, index);\n state.start = index + 1;\n return { text, closesRule: character === '}' };\n }\n return undefined;\n};\n\n// Undefined for text that declares nothing: an empty rule, a stray `;`, text outside any rule.\nconst declarationOf = (text: string, preludes: string[]): Declaration | undefined => {\n const colon = text.indexOf(':');\n const selector = preludes.at(-1);\n if (colon <= 0 || selector === undefined) return undefined;\n\n const property = text.slice(0, colon).trim();\n return {\n property: isCustomProperty(property) ? property : property.toLowerCase(),\n value: text.slice(colon + 1).trim(),\n selector: selector.replace(ANY_SCHEME_MARKER, '').replace(/\\s+/g, ' ').trim(),\n scheme: schemeOf(preludes),\n };\n};\n\n// A stylesheet's declarations, each with the selector of the rule holding it.\nconst declarationsOf = (cssText: string): Declaration[] => {\n const css = cssText.replace(/\\/\\*[\\s\\S]*?\\*\\//g, '');\n const state: ScanState = { preludes: [], start: 0, parentheses: 0, escaped: false };\n const declarations: Declaration[] = [];\n for (let index = 0; index < css.length; index++) {\n const scanned = scanCharacter(css, index, state);\n if (!scanned) continue;\n\n const declaration = declarationOf(scanned.text, state.preludes);\n if (declaration) declarations.push(declaration);\n if (scanned.closesRule) state.preludes.pop();\n }\n return declarations;\n};\n\n// Custom properties by `scopeKey`. A `var()` reads the definition on its own rule, else the page\n// root's: which elements a definition on another selector (`.inverse { --bg: … }`) reaches is more\n// than a flat read of the rules can tell, so it resolves nothing outside its own rule.\ntype Variables = Map<string, string>;\n\nconst variablesOf = (declarations: Declaration[]): Variables =>\n new Map(\n declarations\n .filter(({ property }) => isCustomProperty(property))\n .map(({ selector, property, value }) => [scopeKey(selector, property), value]),\n );\n\nconst resolveVariables = (value: string, selector: string, variables: Variables, depth = 0): string =>\n depth > MAX_VARIABLE_DEPTH\n ? value\n : value.replace(VARIABLE_REFERENCE, (_reference, name: string, fallback = '') => {\n const definition = variables.get(scopeKey(selector, name)) ?? variables.get(scopeKey('', name));\n return resolveVariables(definition ?? fallback, selector, variables, depth + 1);\n });\n\n// Adds `weight` to every opaque colour in a value, keyed by hex, in the order the value writes them.\n// Translucent colours are overlays and shadows, not the palette.\nconst tally = (swatches: Map<string, Swatch>, value: string, weight: number): void => {\n for (const [token] of value.replace(NON_PAINTING_TEXT, ' ').matchAll(COLOR_TOKENS)) {\n const parsed = parse(token);\n if (!parsed || (parsed.alpha ?? 1) < 1) continue;\n\n const rgb = clampRgb(parsed);\n const hex = formatHex(rgb);\n const swatch = swatches.get(hex) ?? { hex, weight: 0, color: toOklch(rgb) };\n swatch.weight += weight;\n swatches.set(hex, swatch);\n }\n};\n\nconst weightOf = (property: string, resolvedValue: string): number => {\n if (!isCustomProperty(property)) return USAGE_WEIGHTS.find(([pattern]) => pattern.test(property))?.[1] ?? 0;\n\n if (COLOR_TOKEN.exec(resolvedValue)?.[0] !== resolvedValue.trim()) return 0;\n\n return BRAND_NAME.test(property) ? 2 * DEFINITION_WEIGHT : DEFINITION_WEIGHT;\n};\n\n// The declarations in effect for a mode: the defaults, and for `light` or `dark` that scheme's rules\n// too, each replacing the defaults with its `scopeKey`. `auto` reads the page as it is by default.\nconst declarationsFor = (cssText: string, mode: MapThemeMode): Declaration[] => {\n const declarations = declarationsOf(cssText);\n const key = ({ selector, property }: Declaration) => scopeKey(selector, property);\n const overridden = new Set(declarations.filter(({ scheme }) => scheme === mode).map(key));\n return declarations.filter((declaration) =>\n declaration.scheme ? declaration.scheme === mode : !overridden.has(key(declaration)),\n );\n};\n\nconst isNeutral = (color: Oklch): boolean => color.c < NEUTRAL_CHROMA;\n\n// Shades of one colour (a button and its hover, two greys a step apart) are one swatch.\nconst isShadeOf = (a: Oklch, b: Oklch): boolean => {\n if (isNeutral(a) || isNeutral(b)) return isNeutral(a) && isNeutral(b) && Math.abs(a.l - b.l) < 0.06;\n\n return Math.abs(differenceHueNaive({ h: a.h ?? 0 }, { h: b.h ?? 0 })) < 20 && Math.abs(a.l - b.l) < 0.2;\n};\n\nconst byWeight = (a: Swatch, b: Swatch): number => b.weight - a.weight;\n\n// Swatches heaviest first, each named by its most-used shade.\nconst mergeShades = (tallied: Iterable<Swatch>): Swatch[] => {\n const merged: Swatch[] = [];\n for (const swatch of [...tallied].toSorted(byWeight)) {\n const main = merged.find((existing) => isShadeOf(existing.color, swatch.color));\n if (main) {\n main.weight += swatch.weight;\n } else {\n merged.push({ ...swatch });\n }\n }\n return merged.toSorted(byWeight);\n};\n\n// The most-used grey, then the most-used one across the lightness scale from it (a background and a\n// text colour), then the rest by use.\nconst neutralsByRole = (neutrals: Swatch[]): Swatch[] => {\n const [first, ...rest] = neutrals;\n const opposite = rest.find((swatch) => Math.abs(swatch.color.l - first.color.l) >= OPPOSITE_LIGHTNESS);\n return opposite ? [first, opposite, ...rest.filter((swatch) => swatch !== opposite)] : neutrals;\n};\n\nconst pickSwatches = (swatches: Swatch[], count: number): string[] => {\n const neutrals = neutralsByRole(swatches.filter((swatch) => isNeutral(swatch.color)));\n const colorful = swatches.filter((swatch) => !isNeutral(swatch.color));\n const neutralCount = Math.min(count, Math.max(Math.min(neutrals.length, MAX_NEUTRALS), count - colorful.length));\n return [...neutrals.slice(0, neutralCount), ...colorful.slice(0, count - neutralCount)]\n .toSorted(byWeight)\n .map((swatch) => swatch.hex);\n};\n\n// Light or dark as the page itself is: the last background set on the root or the body, or else the\n// grey the stylesheet fills the most rules with; light when neither resolves to a colour.\nconst pageMode = (declarations: Declaration[], variables: Variables): LightDark => {\n const backgrounds = declarations.filter(({ property }) => PAGE_BACKGROUND.test(property));\n const rootBackground = backgrounds.findLast(({ selector }) => isPageRoot(selector));\n const rootColors = new Map<string, Swatch>();\n if (rootBackground) {\n tally(rootColors, resolveVariables(rootBackground.value, rootBackground.selector, variables), 1);\n }\n\n const fills = new Map<string, Swatch>();\n for (const { value, selector } of backgrounds) tally(fills, resolveVariables(value, selector, variables), 1);\n\n const [page] = rootColors.size\n ? rootColors.values()\n : [...fills.values()].filter((swatch) => isNeutral(swatch.color)).toSorted(byWeight);\n return page && page.color.l < 0.5 ? 'dark' : 'light';\n};\n\n/**\n * The colours a stylesheet paints with, and the mode given or, for `auto`, the one its page sets.\n * @ignore\n */\nexport const readStylesheet = (\n cssText: string,\n { mode = 'auto', colorCount = DEFAULT_COLOR_COUNT }: MapThemeExtractionOptions = {},\n): { colors: string[]; mode: LightDark } => {\n const declarations = declarationsFor(cssText, mode);\n const variables = variablesOf(declarations);\n const tallied = new Map<string, Swatch>();\n for (const { property, value, selector } of declarations) {\n const resolvedValue = resolveVariables(value, selector, variables);\n const weight = weightOf(property, resolvedValue);\n if (weight) tally(tallied, resolvedValue, weight);\n }\n return {\n colors: pickSwatches(mergeShades(tallied.values()), colorCount),\n mode: mode === 'auto' ? pageMode(declarations, variables) : mode,\n };\n};\n\n/**\n * The colours a stylesheet paints with, most used first, as hex strings: `var()` references\n * resolved, fills weighted over borders and shadows, translucent colours and a colour's shades left\n * out, and at most two greys while colourful ones remain. Feed the result to {@link deriveMapColors};\n * {@link deriveMapColorsFromCss} does both.\n *\n * Rules for one colour scheme (`prefers-color-scheme`, a `.dark` or `.light` class, a `data-*=\"dark\"`\n * attribute) are read only with that `mode`, and then override the defaults; `auto` reads neither.\n * A `var()` resolves against its own rule or the page root, not a custom property set on another selector.\n *\n * @param cssText - The stylesheet's text, e.g. `await file.text()`; `@import`s are not followed.\n *\n * @group Map Theme\n */\nexport const cssColors = (cssText: string, options: MapThemeExtractionOptions = {}): string[] =>\n readStylesheet(cssText, options).colors;\n","import {\n formatStyleHsl,\n type HslColor,\n hueDistance,\n type LightDark,\n type MapColors,\n toHsl,\n} from '@tomtom-org/maps-sdk/map';\nimport { wcagContrast, wcagLuminance } from 'culori/fn';\nimport { isSameColor, toOklch } from './colorSpaces';\nimport type { MapThemeMode, MapThemeOptions } from './types';\n\n// A source colour in HSL (saturation and lightness 0–1), with the CSS it was given as and its OKLCH\n// chroma.\ntype ParsedColor = HslColor & { css: string; chroma: number };\n\nconst NEUTRAL_SATURATION = 0.4;\n// Land-use tints in the built-up colour's hue family are re-derived as its shades, saturation\n// included, and `artificial` is a shade of land; a saturated land makes them garish, so it is toned\n// down to a true neutral.\nconst MAX_LAND_SATURATION = 0.14;\nconst MIN_LABEL_CONTRAST = 4.5;\nconst MIN_ROAD_MAJOR_SATURATION = 0.2;\n// WCAG's floor for graphical objects: a route line has to read against the land it crosses.\nconst MIN_ACCENT_CONTRAST = 3;\nconst MIN_PICKED_ACCENT_SATURATION = 0.3;\n// Hue distance keeping the accent apart from the major roads a route follows.\nconst MIN_ACCENT_HUE_DISTANCE = 30;\n\n// Hue ranges, in degrees, water and vegetation take their colour from: water tries the first and\n// widens to the second.\ntype HueRange = readonly [from: number, to: number];\nconst WATER_HUES: HueRange = [170, 260];\nconst WIDER_WATER_HUES: HueRange = [150, 280];\nconst VEGETATION_HUES: HueRange = [70, 170];\n\nconst isInHues = (color: ParsedColor, [from, to]: HueRange): boolean =>\n color.h !== undefined && color.h >= from && color.h <= to;\n\nconst hsl = (h: number | undefined, s: number, l: number): string => formatStyleHsl({ h, s, l });\n\nconst parseColor = (css: string): ParsedColor | undefined => {\n const parsed = toHsl(css);\n return parsed && { ...parsed, css, chroma: toOklch(parsed).c };\n};\n\nconst asNeutral = (color: ParsedColor): ParsedColor => {\n if (color.s <= MAX_LAND_SATURATION) return color;\n\n return parseColor(hsl(color.h, MAX_LAND_SATURATION, color.l)) ?? color;\n};\n\n// Assigns the map colour roles from an unordered list of colours. Each pick removes the colour\n// from the pool so no two roles share one; a role with no fitting colour is derived from `land`\n// so the result is always a coherent, complete palette.\nclass RoleAssigner {\n private readonly pool: ParsedColor[];\n readonly dark: boolean;\n readonly land: ParsedColor;\n\n constructor(colors: ParsedColor[], mode: MapThemeMode) {\n this.pool = [...colors];\n const meanLightness = colors.reduce((sum, color) => sum + color.l, 0) / colors.length;\n this.dark = mode === 'dark' || (mode === 'auto' && meanLightness < 0.45);\n this.land = asNeutral(this.take(this.pickLand()) ?? colors[0]);\n }\n\n private pickLand(): ParsedColor | undefined {\n const neutrals = this.pool.filter((color) => color.s <= NEUTRAL_SATURATION);\n const candidates = neutrals.length ? neutrals : this.pool;\n // Darkest first on a dark theme, lightest first otherwise; the sort is stable, so equally\n // light candidates keep the caller's order.\n return candidates.toSorted((a, b) => (this.dark ? a.l - b.l : b.l - a.l))[0];\n }\n\n private take(color: ParsedColor | undefined): ParsedColor | undefined {\n if (!color) return undefined;\n\n this.pool.splice(this.pool.indexOf(color), 1);\n return color;\n }\n\n // The most colourful eligible colour left, ranked by chroma rather than HSL saturation, in which a\n // dark navy is as saturated as a vivid red. A colour outside the water and vegetation hues goes\n // first, so neither `roadMajor` nor the accent spends the only blue or green.\n takeMostColorful(isEligible: (color: ParsedColor) => boolean): ParsedColor | undefined {\n const candidates = this.pool.filter(isEligible).toSorted((a, b) => b.chroma - a.chroma);\n const unclaimed = candidates.find(\n (color) => !isInHues(color, WIDER_WATER_HUES) && !isInHues(color, VEGETATION_HUES),\n );\n return this.take(unclaimed ?? candidates[0]);\n }\n\n takeHue(hues: HueRange): ParsedColor | undefined {\n const inRange = this.pool.filter((color) => color.s >= 0.12 && isInHues(color, hues));\n return this.take(inRange.toSorted((a, b) => b.s - a.s)[0]);\n }\n\n // The colour that contrasts most with land; undefined when none reaches the readable minimum.\n takeMostContrasting(): ParsedColor | undefined {\n const againstLand = (color: ParsedColor) => wcagContrast(color, this.land);\n const best = this.pool.toSorted((a, b) => againstLand(b) - againstLand(a))[0];\n return best && againstLand(best) >= MIN_LABEL_CONTRAST ? this.take(best) : undefined;\n }\n\n // A shade of land, `lightnessOffset` towards the label side (lighter on a dark theme, darker on a\n // light one; negative goes the other way), optionally re-hued or re-saturated.\n shade(lightnessOffset: number, options: { h?: number; s?: number } = {}): string {\n return hsl(\n options.h ?? this.land.h,\n options.s ?? this.land.s,\n this.land.l + (this.dark ? 1 : -1) * lightnessOffset,\n );\n }\n\n // A label shade towards the label side, or away from it when land sits so near that end that the\n // shade clamps to a colour too close to land to read.\n labelShade(lightnessOffset: number, s: number): string {\n const towards = this.shade(lightnessOffset, { s });\n return wcagContrast(towards, this.land) >= MIN_LABEL_CONTRAST ? towards : this.shade(-lightnessOffset, { s });\n }\n}\n\n// The colour for what is drawn on top of the map: the most colourful one left that reads against\n// land and apart from `roadMajor`, else `roadMajor`'s complement at the first lightness that does.\nconst deriveAccent = (roles: RoleAssigner, roadMajor: ParsedColor): string => {\n const { dark, land } = roles;\n const readsOnLand = (color: ParsedColor | undefined): color is ParsedColor =>\n color !== undefined && wcagContrast(color, land) >= MIN_ACCENT_CONTRAST;\n\n const picked = roles.takeMostColorful(\n (color) =>\n color.s >= MIN_PICKED_ACCENT_SATURATION &&\n readsOnLand(color) &&\n hueDistance(color, roadMajor) >= MIN_ACCENT_HUE_DISTANCE,\n );\n if (picked) return picked.css;\n\n const complementHue = (roadMajor.h ?? 0) + 180;\n const complement = (dark ? [0.62, 0.7, 0.78] : [0.38, 0.3, 0.22])\n .map((lightness) => parseColor(hsl(complementHue, 0.7, lightness)))\n .find(readsOnLand);\n return complement?.css ?? hsl(complementHue, 0.7, dark ? 0.85 : 0.15);\n};\n\n/**\n * Assigns a handful of colours — a brand's, a website's, an artist's, a mood's — to the map colours:\n * the ten base-map ones plus `accent`, ready for {@link StylingFoundationsModule.setMapColors}.\n *\n * @remarks\n * Deterministic, and no two roles share a colour:\n * - `land`: the lightest low-saturation colour (the darkest, on a dark theme), toned down if saturated.\n * - `roadMajor`: the most colourful by OKLCH chroma, preferring one that is not a blue or a green.\n * - `water`: a blue or cyan; `vegetation`: a green.\n * - `label`: the one contrasting most with `land`, when it reaches WCAG 4.5:1.\n * - `accent`: the most colourful one left reading against `land` (3:1) and apart from `roadMajor`.\n *\n * Picked colours come back as given; roles no colour fits are `hsl()` shades of `land` — the accent\n * `roadMajor`'s complement — so the result is complete. Apply it over a style of the same light or\n * dark: {@link mapColorsLightDark}.\n *\n * @param colors - One to roughly eight CSS colours; order does not matter, near-duplicates count once.\n * @throws `RangeError` when no colour can be parsed.\n *\n * @example\n * ```typescript\n * const colors = deriveMapColors(['#0b2a3a', '#0fa8c8', '#f6efe3', '#d9822b'], { mode: 'light' });\n * styling.setMapColors(colors); // `colors` also tells a UI what became what\n * ```\n *\n * @group Map Theme\n */\nexport const deriveMapColors = (colors: string[], options: MapThemeOptions = {}): Required<MapColors> => {\n const parsed = colors.map(parseColor).filter((color): color is ParsedColor => color !== undefined);\n const distinct = parsed.filter((color, index) => !parsed.slice(0, index).some((seen) => isSameColor(seen, color)));\n if (!distinct.length) {\n throw new RangeError('deriveMapColors needs at least one parseable CSS colour.');\n }\n\n const roles = new RoleAssigner(distinct, options.mode ?? 'auto');\n const { dark, land } = roles;\n\n const roadMajor =\n roles.takeMostColorful((color) => color.s >= MIN_ROAD_MAJOR_SATURATION) ??\n parseColor(roles.shade(dark ? 0.55 : 0.45, { s: 0.7 })) ??\n land;\n const water =\n (roles.takeHue(WATER_HUES) ?? roles.takeHue(WIDER_WATER_HUES))?.css ??\n roles.shade(dark ? 0.12 : 0.14, { h: 205, s: Math.max(0.35, land.s) });\n const vegetation =\n roles.takeHue(VEGETATION_HUES)?.css ??\n roles.shade(dark ? 0.08 : 0.09, { h: 110, s: Math.max(0.18, land.s * 0.6) });\n const label = roles.takeMostContrasting()?.css ?? roles.labelShade(dark ? 0.8 : 0.78, Math.min(land.s, 0.2));\n const accent = deriveAccent(roles, roadMajor);\n const park = parseColor(vegetation) ?? land;\n\n return {\n land: land.css,\n water,\n vegetation,\n park: hsl(park.h, park.s, park.l + (dark ? 0.04 : 0.05)),\n artificial: roles.shade(dark ? 0.04 : 0.03),\n roadMajor: roadMajor.css,\n road: roles.shade(dark ? 0.1 : -0.08, { s: land.s * 0.5 }),\n roadOutline: roles.shade(dark ? 0.24 : 0.22, { s: land.s * 0.6 }),\n label,\n labelOutline: roles.shade(-0.03),\n accent,\n };\n};\n\n/**\n * Whether map colours make a light or a dark map (dark when labels are lighter than land), to apply\n * them over a style of the same: {@link StylingFoundationsModule.setMapColors} keeps the loaded style's shade\n * offsets, which a dark land over `standardLight` clamps to black. Tells what `auto` chose.\n *\n * @example\n * ```typescript\n * const colors = await deriveMapColorsFromImage(file);\n * const lightDark = mapColorsLightDark(colors);\n * if (lightDark !== map.styleLightDarkTheme) await map.setStyle(lightDark === 'dark' ? 'standardDark' : 'standardLight');\n * styling.setMapColors(colors);\n * ```\n *\n * @group Map Theme\n */\nexport const mapColorsLightDark = ({ land, label }: Pick<Required<MapColors>, 'land' | 'label'>): LightDark =>\n wcagLuminance(label) > wcagLuminance(land) ? 'dark' : 'light';\n","import type { MapColors } from '@tomtom-org/maps-sdk/map';\nimport { readStylesheet } from './cssColors';\nimport { deriveMapColors } from './roleAssignment';\nimport type { MapThemeExtractionOptions } from './types';\n\n/**\n * Derives the map colours from a stylesheet — a site's CSS, a design system's tokens —\n * by reading the colours it paints with ({@link cssColors}) and assigning them roles with\n * {@link deriveMapColors}.\n *\n * With `mode: 'auto'`, the default, the map is light or dark as the page is: its background on\n * `:root`, `html` or `body`, or else the grey most rules fill with, and light when neither resolves.\n * Needs no DOM.\n *\n * @param cssText - The stylesheet's text, e.g. `await file.text()`; `@import`s are not followed.\n * @throws `RangeError` when the stylesheet paints with no opaque colour.\n *\n * @example\n * ```typescript\n * // A File from an <input type=\"file\" accept=\".css\">, or CSS a server fetched\n * styling.setMapColors(deriveMapColorsFromCss(await fileInput.files[0].text()));\n * ```\n *\n * @group Map Theme\n */\nexport const deriveMapColorsFromCss = (\n cssText: string,\n options: MapThemeExtractionOptions = {},\n): Required<MapColors> => {\n const { colors, mode } = readStylesheet(cssText, options);\n if (!colors.length) {\n throw new RangeError(\n 'deriveMapColorsFromCss: the stylesheet paints with no opaque colour to take a theme from.',\n );\n }\n\n return deriveMapColors(colors, { mode });\n};\n","import { formatHex, wcagLuminance } from 'culori/fn';\nimport { isSameColor, rgbFromBytes } from './colorSpaces';\nimport { DEFAULT_COLOR_COUNT, type ImagePixels } from './types';\n\nconst SAMPLE_BUDGET = 4096;\nconst KMEANS_ITERATIONS = 8;\n\n// In RGB bytes: cheap enough for the k-means loop, which only needs a nearest centroid.\nconst squaredDistance = (a: number[], b: number[]): number =>\n (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2 + (a[2] - b[2]) ** 2;\n\n// Up to SAMPLE_BUDGET opaque pixels, evenly strided through the image.\nconst sampleOpaquePixels = ({ data }: ImagePixels): number[][] => {\n const pixelCount = Math.floor(data.length / 4);\n const stride = Math.max(1, Math.floor(pixelCount / SAMPLE_BUDGET));\n const samples: number[][] = [];\n for (let index = 0; index < pixelCount; index += stride) {\n const offset = index * 4;\n if (data[offset + 3] < 128) continue;\n\n samples.push([data[offset], data[offset + 1], data[offset + 2]]);\n }\n return samples;\n};\n\nconst nearestIndex = (sample: number[], centroids: number[][]): number => {\n let nearest = 0;\n let nearestDistance = Number.POSITIVE_INFINITY;\n for (let c = 0; c < centroids.length; c++) {\n const distance = squaredDistance(sample, centroids[c]);\n if (distance < nearestDistance) {\n nearestDistance = distance;\n nearest = c;\n }\n }\n return nearest;\n};\n\n// Farthest-point seeding: start from the median-luminance pixel, then keep adding the pixel farthest\n// from every seed so far, so distinct colours each get a cluster even when they are all dark.\nconst seedCentroids = (samples: number[][], count: number): number[][] => {\n const luminanceOf = (rgb: number[]) => wcagLuminance(rgbFromBytes(rgb));\n const byLuminance = samples.toSorted((a, b) => luminanceOf(a) - luminanceOf(b));\n const centroids = [[...byLuminance[Math.floor(samples.length / 2)]]];\n while (centroids.length < count) {\n let farthest = samples[0];\n let farthestDistance = -1;\n for (const sample of samples) {\n const distance = squaredDistance(sample, centroids[nearestIndex(sample, centroids)]);\n if (distance > farthestDistance) {\n farthestDistance = distance;\n farthest = sample;\n }\n }\n if (farthestDistance === 0) break;\n\n centroids.push([...farthest]);\n }\n return centroids;\n};\n\nconst kMeans = (samples: number[][], seeds: number[][]): { centroids: number[][]; sizes: number[] } => {\n let centroids = seeds;\n let sizes: number[] = [];\n for (let iteration = 0; iteration < KMEANS_ITERATIONS; iteration++) {\n const sums = centroids.map(() => [0, 0, 0]);\n sizes = centroids.map(() => 0);\n for (const sample of samples) {\n const nearest = nearestIndex(sample, centroids);\n sizes[nearest]++;\n sums[nearest][0] += sample[0];\n sums[nearest][1] += sample[1];\n sums[nearest][2] += sample[2];\n }\n centroids = centroids.map((centroid, cluster) =>\n sizes[cluster] ? sums[cluster].map((sum) => sum / sizes[cluster]) : centroid,\n );\n }\n return { centroids, sizes };\n};\n\ntype Cluster = { rgb: number[]; size: number };\n\nconst mergeClusters = (centroids: number[][], sizes: number[]): Cluster[] => {\n const merged: Cluster[] = [];\n for (const [cluster, rgb] of centroids.entries()) {\n if (!sizes[cluster]) continue;\n\n const twin = merged.find((entry) => isSameColor(rgbFromBytes(entry.rgb), rgbFromBytes(rgb)));\n if (twin) {\n twin.size += sizes[cluster];\n } else {\n merged.push({ rgb, size: sizes[cluster] });\n }\n }\n return merged;\n};\n\n/**\n * The dominant colours of an image, largest area first, as hex strings — a small k-means over a\n * sample of opaque pixels, with near-identical clusters merged. Feed the result to\n * {@link deriveMapColors}; {@link deriveMapColorsFromImage} does both.\n *\n * @param count - The most colours to return (default 6); merged clusters can leave fewer.\n *\n * @group Map Theme\n */\nexport const dominantColors = (pixels: ImagePixels, count = DEFAULT_COLOR_COUNT): string[] => {\n const samples = sampleOpaquePixels(pixels);\n if (!samples.length) return [];\n\n const { centroids, sizes } = kMeans(samples, seedCentroids(samples, Math.min(count, samples.length)));\n return mergeClusters(centroids, sizes)\n .toSorted((a, b) => b.size - a.size)\n .map((entry) => formatHex(rgbFromBytes(entry.rgb)));\n};\n","import type { MapColors } from '@tomtom-org/maps-sdk/map';\nimport { dominantColors } from './dominantColors';\nimport { deriveMapColors } from './roleAssignment';\nimport type { ImagePixels, MapThemeExtractionOptions, MapThemeImageSource } from './types';\n\n// The dominant colours do not need every pixel, so the image is drawn into a square this small.\nconst DECODE_SIDE = 64;\n\nconst isImagePixels = (source: MapThemeImageSource): source is ImagePixels =>\n typeof source === 'object' && 'data' in source;\n\nconst loadImageElement = (url: string): Promise<HTMLImageElement> =>\n new Promise((resolve, reject) => {\n const image = new Image();\n image.crossOrigin = 'anonymous';\n image.onload = () => resolve(image);\n image.onerror = () => reject(new Error(`deriveMapColorsFromImage: could not load '${url}'.`));\n image.src = url;\n });\n\nconst toDrawable = async (source: Exclude<MapThemeImageSource, ImagePixels>): Promise<CanvasImageSource> => {\n if (typeof source === 'string') return loadImageElement(source);\n\n if (source instanceof Blob) return createImageBitmap(source);\n\n return source;\n};\n\nconst readPixels = async (source: Exclude<MapThemeImageSource, ImagePixels>): Promise<ImagePixels> => {\n if (typeof document === 'undefined') {\n throw new TypeError('deriveMapColorsFromImage needs a DOM to decode images; pass ImagePixels instead.');\n }\n\n const drawable = await toDrawable(source);\n const canvas = document.createElement('canvas');\n canvas.width = DECODE_SIDE;\n canvas.height = DECODE_SIDE;\n const context = canvas.getContext('2d');\n if (!context) {\n throw new Error('deriveMapColorsFromImage: no 2D canvas context available.');\n }\n\n context.drawImage(drawable, 0, 0, DECODE_SIDE, DECODE_SIDE);\n try {\n return context.getImageData(0, 0, DECODE_SIDE, DECODE_SIDE);\n } catch {\n throw new Error(\n 'deriveMapColorsFromImage: the image is not CORS-readable (canvas tainted). Serve it with ' +\n 'Access-Control-Allow-Origin or pass a Blob/File.',\n );\n }\n};\n\n/**\n * Derives the map colours from an image — a photo, a logo, a poster — by extracting its\n * dominant colours and assigning them roles with {@link deriveMapColors}.\n *\n * @throws `RangeError` when the image has no opaque pixels, or an error when it cannot be read (not\n * CORS-readable, no DOM to decode it).\n *\n * @example\n * ```typescript\n * // A File from an <input type=\"file\">, a Blob, an <img>, an ImageBitmap or a CORS-readable URL\n * styling.setMapColors(await deriveMapColorsFromImage(fileInput.files[0], { mode: 'dark' }));\n * ```\n *\n * @group Map Theme\n */\nexport const deriveMapColorsFromImage = async (\n source: MapThemeImageSource,\n options: MapThemeExtractionOptions = {},\n): Promise<Required<MapColors>> => {\n const pixels = isImagePixels(source) ? source : await readPixels(source);\n const colors = dominantColors(pixels, options.colorCount);\n if (!colors.length) {\n throw new RangeError('deriveMapColorsFromImage: the image has no opaque pixels to take colours from.');\n }\n\n return deriveMapColors(colors, options);\n};\n"],"mappings":"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"}
|