@tomtom-org/maps-sdk-plugin-map-theme 0.3.6 → 0.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  This plugin for the [TomTom Maps SDK for JavaScript](https://docs.tomtom.com/maps-sdk-js/introduction/overview) themes a `TomTomMap` from wherever a look comes from. That's a brand's colours, a website's, a described mood, an artist's palette or an image. It assigns them to the SDK's ten map colours, plus the accent that routes, geometries and pins take. Then `StylingFoundationsModule.setMapColors` applies them.
4
4
 
5
+ > **Public preview.** This plugin is in [Public Preview](https://docs.tomtom.com/legal/public-preview) until its 1.0.0 release.
6
+
5
7
  | Function | From | To |
6
8
  | --- | --- | --- |
7
9
  | `deriveMapColors` | A handful of CSS colours | The map colours, `accent` included |
@@ -39,3 +41,7 @@ styling.setMapColors(await deriveMapColorsFromImage(fileInput.files[0], { mode:
39
41
  ## License
40
42
 
41
43
  See `LICENSE.txt`.
44
+
45
+ ## Security
46
+
47
+ To report a security vulnerability, follow our [security policy](https://github.com/tomtom-international/maps-sdk-js/security/policy), which keeps it private.
package/dist/index.d.ts CHANGED
@@ -79,6 +79,9 @@ export declare const cssColors: (cssText: string, options?: MapThemeExtractionOp
79
79
  * on `:root`, `html` or `body`, or else the grey most rules fill with, and is light when neither
80
80
  * resolves. It needs no DOM.
81
81
  *
82
+ * @remarks
83
+ * **[Public Preview](https://docs.tomtom.com/legal/public-preview)** until the plugin's 1.0.0 release.
84
+ *
82
85
  * @param cssText - The stylesheet's text, e.g. `await file.text()`. `@import`s are not followed.
83
86
  * @throws `RangeError` when the stylesheet paints with no opaque colour.
84
87
  *
@@ -109,6 +112,9 @@ export declare const dominantColors: (pixels: ImagePixels, count?: number) => st
109
112
  * Derives the map colours from an image (a photo, a logo, a poster) by extracting its
110
113
  * dominant colours and assigning them roles with {@link deriveMapColors}.
111
114
  *
115
+ * @remarks
116
+ * **[Public Preview](https://docs.tomtom.com/legal/public-preview)** until the plugin's 1.0.0 release.
117
+ *
112
118
  * @throws `RangeError` when the image has no opaque pixels, or an error when it cannot be read (not
113
119
  * CORS-readable, no DOM to decode it).
114
120
  *
@@ -128,8 +134,11 @@ export declare const deriveMapColorsFromImage: (source: MapThemeImageSource, opt
128
134
  * namely the ten base-map ones plus `accent`, ready for {@link StylingFoundationsModule.setMapColors}.
129
135
  *
130
136
  * @remarks
137
+ * **[Public Preview](https://docs.tomtom.com/legal/public-preview)** until the plugin's 1.0.0 release.
138
+ *
131
139
  * Deterministic, and no two roles share a colour:
132
- * - `land`: the lightest low-saturation colour (the darkest, on a dark theme), toned down if saturated.
140
+ * - `land`: the lightest colour of low OKLCH chroma (the darkest, on a dark theme), toned down if saturated.
141
+ * It comes from the theme's side of mid-lightness, any colour there before the nearest one on the other side.
133
142
  * - `roadMajor`: the most colourful by OKLCH chroma, preferring one that is not a blue or a green.
134
143
  * - `water`: a blue or cyan; `vegetation`: a green.
135
144
  * - `label`: the one contrasting most with `land`, when it reaches WCAG 4.5:1.
package/dist/index.es.js CHANGED
@@ -1 +1 @@
1
- import{clampRgb,differenceEuclidean,differenceHueNaive,formatHex,modeHsl,modeLrgb,modeOklab,modeOklch,modeRgb,parse,useMode,wcagContrast,wcagLuminance}from"culori/fn";import{formatStyleHsl,hueDistance,toHsl}from"@tomtom-org/maps-sdk/map";useMode(modeRgb),useMode(modeLrgb),useMode(modeHsl),useMode(modeOklab);var toOklch=useMode(modeOklch),oklabDistance=/* @__PURE__ */differenceEuclidean("oklab"),isSameColor=(e,t)=>oklabDistance(e,t)<.035,rgbFromBytes=([e,t,o])=>({mode:"rgb",r:e/255,g:t/255,b:o/255}),mapThemeModes=["light","dark","auto"],schemeMarker=(e,t)=>new RegExp(String.raw`prefers-color-scheme\s*:\s*${e}|\.(?:theme-)?${e}(?:-(?:mode|theme))?(?![\w-])|\[data-[\w-]+=["']?${e}["']?\]`,t),COLOR_SCHEMES=["light","dark"],SCHEME_MARKERS={light:/* @__PURE__ */schemeMarker("light","i"),dark:/* @__PURE__ */schemeMarker("dark","i")},ANY_SCHEME_MARKER=/* @__PURE__ */schemeMarker("(?:light|dark)","gi"),PAGE_ROOT=/^(?:(?::root|html|body)\s*)+$/,PAGE_BACKGROUND=/^background(-color)?$/,NON_PAINTING_TEXT=/url\((?:"[^"]*"|'[^']*'|[^)]*)\)|"[^"]*"|'[^']*'/gi,HEX_COLOR=/#[\da-f]{3,8}\b/,COLOR_FUNCTION=/\b(?:rgba?|hsla?|oklab|oklch)\([^()]*\)/,COLOR_NAME=/\b[a-z]+\b/,COLOR_TOKEN=/* @__PURE__ */(()=>new RegExp([HEX_COLOR,COLOR_FUNCTION,COLOR_NAME].map(({source:e})=>e).join("|"),"i"))(),COLOR_TOKENS=/* @__PURE__ */new RegExp(COLOR_TOKEN,"gi"),VARIABLE_REFERENCE=/var\(\s*(--[\w-]+)\s*(?:,([^()]*(?:\([^()]*\)[^()]*)*))?\)/g,USAGE_WEIGHTS=[[/^(background|fill|accent-color|caret-color)|^color$/,1],[/^(border|outline|stroke|column-rule|text-decoration)/,.3],[/shadow$/,.1]],BRAND_NAME=/brand|primary|accent/i,isCustomProperty=e=>e.startsWith("--"),isPageRoot=e=>""===e||e.split(",").some(e=>PAGE_ROOT.test(e.trim())),scopeKey=(e,t)=>`${isPageRoot(e)?"":e}\0${t}`,schemeOf=e=>COLOR_SCHEMES.find(t=>e.some(e=>SCHEME_MARKERS[t].test(e))),scanCharacter=(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},declarationOf=(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:isCustomProperty(s)?s:s.toLowerCase(),value:e.slice(o+1).trim(),selector:r.replace(ANY_SCHEME_MARKER,"").replace(/\s+/g," ").trim(),scheme:schemeOf(t)}},resolveVariables=(e,t,o,r=0)=>r>8?e:e.replace(VARIABLE_REFERENCE,(e,s,a="")=>{const l=o.get(scopeKey(t,s))??o.get(scopeKey("",s));return resolveVariables(l??a,t,o,r+1)}),tally=(e,t,o)=>{for(const[r]of t.replace(NON_PAINTING_TEXT," ").matchAll(COLOR_TOKENS)){const t=parse(r);if(!t||(t.alpha??1)<1)continue;const s=clampRgb(t),a=formatHex(s),l=e.get(a)??{hex:a,weight:0,color:toOklch(s)};l.weight+=o,e.set(a,l)}},weightOf=(e,t)=>isCustomProperty(e)?COLOR_TOKEN.exec(t)?.[0]!==t.trim()?0:BRAND_NAME.test(e)?1:.5:USAGE_WEIGHTS.find(([t])=>t.test(e))?.[1]??0,isNeutral=e=>e.c<.03,byWeight=(e,t)=>t.weight-e.weight,mergeShades=e=>{const t=[];for(const o of[...e].toSorted(byWeight)){const e=t.find(e=>{return t=e.color,r=o.color,isNeutral(t)||isNeutral(r)?isNeutral(t)&&isNeutral(r)&&Math.abs(t.l-r.l)<.06:Math.abs(differenceHueNaive({h:t.h??0},{h:r.h??0}))<20&&Math.abs(t.l-r.l)<.2;var t,r});e?e.weight+=o.weight:t.push({...o})}return t.toSorted(byWeight)},pickSwatches=(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=>isNeutral(e.color))),r=e.filter(e=>!isNeutral(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(byWeight).map(e=>e.hex)},pageMode=(e,t)=>{const o=e.filter(({property:e})=>PAGE_BACKGROUND.test(e)),r=o.findLast(({selector:e})=>isPageRoot(e)),s=/* @__PURE__ */new Map;r&&tally(s,resolveVariables(r.value,r.selector,t),1);const a=/* @__PURE__ */new Map;for(const{value:n,selector:i}of o)tally(a,resolveVariables(n,i,t),1);const[l]=s.size?s.values():[...a.values()].filter(e=>isNeutral(e.color)).toSorted(byWeight);return l&&l.color.l<.5?"dark":"light"},readStylesheet=(e,{mode:t="auto",colorCount:o=6}={})=>{const r=((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=scanCharacter(t,s,o);if(!e)continue;const a=declarationOf(e.text,o.preludes);a&&r.push(a),e.closesRule&&o.preludes.pop()}return r})(e),r=({selector:e,property:t})=>scopeKey(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)))})(e,t),s=(e=>new Map(e.filter(({property:e})=>isCustomProperty(e)).map(({selector:e,property:t,value:o})=>[scopeKey(e,t),o])))(r),a=/* @__PURE__ */new Map;for(const{property:l,value:n,selector:i}of r){const e=resolveVariables(n,i,s),t=weightOf(l,e);t&&tally(a,e,t)}return{colors:pickSwatches(mergeShades(a.values()),o),mode:"auto"===t?pageMode(r,s):t}},cssColors=(e,t={})=>readStylesheet(e,t).colors,WATER_HUES=[170,260],WIDER_WATER_HUES=[150,280],VEGETATION_HUES=[70,170],isInHues=(e,[t,o])=>void 0!==e.h&&e.h>=t&&e.h<=o,hsl=(e,t,o)=>formatStyleHsl({h:e,s:t,l:o}),parseColor=e=>{const t=toHsl(e);return t&&{...t,css:e,chroma:toOklch(t).c}},RoleAssigner=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:parseColor(hsl(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=>!isInHues(e,WIDER_WATER_HUES)&&!isInHues(e,VEGETATION_HUES));return this.take(o??t[0])}takeHue(e){const t=this.pool.filter(t=>t.s>=.12&&isInHues(t,e));return this.take(t.toSorted((e,t)=>t.s-e.s)[0])}takeMostContrasting(){const e=e=>wcagContrast(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 hsl(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 wcagContrast(o,this.land)>=4.5?o:this.shade(-e,{s:t})}},deriveMapColors=(e,t={})=>{const o=e.map(parseColor).filter(e=>void 0!==e),r=o.filter((e,t)=>!o.slice(0,t).some(t=>isSameColor(t,e)));if(!r.length)throw new RangeError("deriveMapColors needs at least one parseable CSS colour.");const s=new RoleAssigner(r,t.mode??"auto"),{dark:a,land:l}=s,n=s.takeMostColorful(e=>e.s>=.2)??parseColor(s.shade(a?.55:.45,{s:.7}))??l,i=(s.takeHue(WATER_HUES)??s.takeHue(WIDER_WATER_HUES))?.css??s.shade(a?.12:.14,{h:205,s:Math.max(.35,l.s)}),c=s.takeHue(VEGETATION_HUES)?.css??s.shade(a?.08:.09,{h:110,s:Math.max(.18,.6*l.s)}),h=s.takeMostContrasting()?.css??s.labelShade(a?.8:.78,Math.min(l.s,.2)),d=((e,t)=>{const{dark:o,land:r}=e,s=e=>void 0!==e&&wcagContrast(e,r)>=3,a=e.takeMostColorful(e=>e.s>=.3&&s(e)&&hueDistance(e,t)>=30);if(a)return a.css;const l=(t.h??0)+180;return(o?[.62,.7,.78]:[.38,.3,.22]).map(e=>parseColor(hsl(l,.7,e))).find(s)?.css??hsl(l,.7,o?.85:.15)})(s,n),m=parseColor(c)??l;return{land:l.css,water:i,vegetation:c,park:hsl(m.h,m.s,m.l+(a?.04:.05)),artificial:s.shade(a?.04:.03),roadMajor:n.css,road:s.shade(a?.1:-.08,{s:.5*l.s}),roadOutline:s.shade(a?.24:.22,{s:.6*l.s}),label:h,labelHalo:s.shade(-.03),accent:d}},mapColorsLightDark=({land:e,label:t})=>wcagLuminance(t)>wcagLuminance(e)?"dark":"light",deriveMapColorsFromCss=(e,t={})=>{const{colors:o,mode:r}=readStylesheet(e,t);if(!o.length)throw new RangeError("deriveMapColorsFromCss: the stylesheet paints with no opaque colour to take a theme from.");return deriveMapColors(o,{mode:r})},squaredDistance=(e,t)=>(e[0]-t[0])**2+(e[1]-t[1])**2+(e[2]-t[2])**2,nearestIndex=(e,t)=>{let o=0,r=Number.POSITIVE_INFINITY;for(let s=0;s<t.length;s++){const a=squaredDistance(e,t[s]);a<r&&(r=a,o=s)}return o},dominantColors=(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:r,sizes:s}=((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=nearestIndex(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=>wcagLuminance(rgbFromBytes(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=squaredDistance(s,r[nearestIndex(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=>isSameColor(rgbFromBytes(e.rgb),rgbFromBytes(s)));e?e.size+=t[r]:o.push({rgb:s,size:t[r]})}return o})(r,s).toSorted((e,t)=>t.size-e.size).map(e=>formatHex(rgbFromBytes(e.rgb)))},deriveMapColorsFromImage=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(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})(e),o=document.createElement("canvas");o.width=64,o.height=64;const r=o.getContext("2d");if(!r)throw new Error("deriveMapColorsFromImage: no 2D canvas context available.");r.drawImage(t,0,0,64,64);try{return r.getImageData(0,0,64,64)}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=dominantColors(o,t.colorCount);if(!r.length)throw new RangeError("deriveMapColorsFromImage: the image has no opaque pixels to take colours from.");return deriveMapColors(r,t)};export{cssColors,deriveMapColors,deriveMapColorsFromCss,deriveMapColorsFromImage,dominantColors,mapColorsLightDark,mapThemeModes};
1
+ import{clampRgb,differenceEuclidean,differenceHueNaive,formatHex,modeHsl,modeLrgb,modeOklab,modeOklch,modeRgb,parse,useMode,wcagContrast,wcagLuminance}from"culori/fn";import{formatStyleHsl,hueDistance,toHsl}from"@tomtom-org/maps-sdk/map";useMode(modeRgb),useMode(modeLrgb),useMode(modeHsl),useMode(modeOklab);var toOklch=useMode(modeOklch),oklabDistance=/* @__PURE__ */differenceEuclidean("oklab"),isSameColor=(e,t)=>oklabDistance(e,t)<.035,rgbFromBytes=([e,t,o])=>({mode:"rgb",r:e/255,g:t/255,b:o/255}),mapThemeModes=["light","dark","auto"],schemeMarker=(e,t)=>new RegExp(String.raw`prefers-color-scheme\s*:\s*${e}|\.(?:theme-)?${e}(?:-(?:mode|theme))?(?![\w-])|\[data-[\w-]+=["']?${e}["']?\]`,t),COLOR_SCHEMES=["light","dark"],SCHEME_MARKERS={light:/* @__PURE__ */schemeMarker("light","i"),dark:/* @__PURE__ */schemeMarker("dark","i")},ANY_SCHEME_MARKER=/* @__PURE__ */schemeMarker("(?:light|dark)","gi"),PAGE_ROOT=/^(?:(?::root|html|body)\s*)+$/,PAGE_BACKGROUND=/^background(-color)?$/,NON_PAINTING_TEXT=/url\((?:"[^"]*"|'[^']*'|[^)]*)\)|"[^"]*"|'[^']*'/gi,HEX_COLOR=/#[\da-f]{3,8}\b/,COLOR_FUNCTION=/\b(?:rgba?|hsla?|oklab|oklch)\([^()]*\)/,COLOR_NAME=/\b[a-z]+\b/,COLOR_TOKEN=/* @__PURE__ */(()=>new RegExp([HEX_COLOR,COLOR_FUNCTION,COLOR_NAME].map(({source:e})=>e).join("|"),"i"))(),COLOR_TOKENS=/* @__PURE__ */new RegExp(COLOR_TOKEN,"gi"),VARIABLE_REFERENCE=/var\(\s*(--[\w-]+)\s*(?:,([^()]*(?:\([^()]*\)[^()]*)*))?\)/g,USAGE_WEIGHTS=[[/^(background|fill|accent-color|caret-color)|^color$/,1],[/^(border|outline|stroke|column-rule|text-decoration)/,.3],[/shadow$/,.1]],BRAND_NAME=/brand|primary|accent/i,isCustomProperty=e=>e.startsWith("--"),isPageRoot=e=>""===e||e.split(",").some(e=>PAGE_ROOT.test(e.trim())),scopeKey=(e,t)=>`${isPageRoot(e)?"":e}\0${t}`,schemeOf=e=>COLOR_SCHEMES.find(t=>e.some(e=>SCHEME_MARKERS[t].test(e))),scanCharacter=(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},declarationOf=(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:isCustomProperty(s)?s:s.toLowerCase(),value:e.slice(o+1).trim(),selector:r.replace(ANY_SCHEME_MARKER,"").replace(/\s+/g," ").trim(),scheme:schemeOf(t)}},resolveVariables=(e,t,o,r=0)=>r>8?e:e.replace(VARIABLE_REFERENCE,(e,s,a="")=>{const l=o.get(scopeKey(t,s))??o.get(scopeKey("",s));return resolveVariables(l??a,t,o,r+1)}),tally=(e,t,o)=>{for(const[r]of t.replace(NON_PAINTING_TEXT," ").matchAll(COLOR_TOKENS)){const t=parse(r);if(!t||(t.alpha??1)<1)continue;const s=clampRgb(t),a=formatHex(s),l=e.get(a)??{hex:a,weight:0,color:toOklch(s)};l.weight+=o,e.set(a,l)}},weightOf=(e,t)=>isCustomProperty(e)?COLOR_TOKEN.exec(t)?.[0]!==t.trim()?0:BRAND_NAME.test(e)?1:.5:USAGE_WEIGHTS.find(([t])=>t.test(e))?.[1]??0,isNeutral=e=>e.c<.03,byWeight=(e,t)=>t.weight-e.weight,mergeShades=e=>{const t=[];for(const o of[...e].toSorted(byWeight)){const e=t.find(e=>{return t=e.color,r=o.color,isNeutral(t)||isNeutral(r)?isNeutral(t)&&isNeutral(r)&&Math.abs(t.l-r.l)<.06:Math.abs(differenceHueNaive({h:t.h??0},{h:r.h??0}))<20&&Math.abs(t.l-r.l)<.2;var t,r});e?e.weight+=o.weight:t.push({...o})}return t.toSorted(byWeight)},pickSwatches=(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=>isNeutral(e.color))),r=e.filter(e=>!isNeutral(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(byWeight).map(e=>e.hex)},pageMode=(e,t)=>{const o=e.filter(({property:e})=>PAGE_BACKGROUND.test(e)),r=o.findLast(({selector:e})=>isPageRoot(e)),s=/* @__PURE__ */new Map;r&&tally(s,resolveVariables(r.value,r.selector,t),1);const a=/* @__PURE__ */new Map;for(const{value:n,selector:i}of o)tally(a,resolveVariables(n,i,t),1);const[l]=s.size?s.values():[...a.values()].filter(e=>isNeutral(e.color)).toSorted(byWeight);return l&&l.color.l<.5?"dark":"light"},readStylesheet=(e,{mode:t="auto",colorCount:o=6}={})=>{const r=((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=scanCharacter(t,s,o);if(!e)continue;const a=declarationOf(e.text,o.preludes);a&&r.push(a),e.closesRule&&o.preludes.pop()}return r})(e),r=({selector:e,property:t})=>scopeKey(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)))})(e,t),s=(e=>new Map(e.filter(({property:e})=>isCustomProperty(e)).map(({selector:e,property:t,value:o})=>[scopeKey(e,t),o])))(r),a=/* @__PURE__ */new Map;for(const{property:l,value:n,selector:i}of r){const e=resolveVariables(n,i,s),t=weightOf(l,e);t&&tally(a,e,t)}return{colors:pickSwatches(mergeShades(a.values()),o),mode:"auto"===t?pageMode(r,s):t}},cssColors=(e,t={})=>readStylesheet(e,t).colors,WATER_HUES=[170,260],WIDER_WATER_HUES=[150,280],VEGETATION_HUES=[70,170],isInHues=(e,[t,o])=>void 0!==e.h&&e.h>=t&&e.h<=o,hsl=(e,t,o)=>formatStyleHsl({h:e,s:t,l:o}),parseColor=e=>{const t=toHsl(e);return t&&{...t,css:e,chroma:toOklch(t).c}},RoleAssigner=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:parseColor(hsl(r.h,.14,r.l))??r}pickLand(){const e=e=>e.toSorted((e,t)=>this.dark?e.l-t.l:t.l-e.l)[0],t=e=>e.l<.5===this.dark;return e(this.pool.filter(e=>e.chroma<=.08).filter(t))??e(this.pool.filter(t))??e(this.pool)}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=>!isInHues(e,WIDER_WATER_HUES)&&!isInHues(e,VEGETATION_HUES));return this.take(o??t[0])}takeHue(e){const t=this.pool.filter(t=>t.s>=.12&&isInHues(t,e));return this.take(t.toSorted((e,t)=>t.s-e.s)[0])}takeMostContrasting(){const e=e=>wcagContrast(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 hsl(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 wcagContrast(o,this.land)>=4.5?o:this.shade(-e,{s:t})}},deriveMapColors=(e,t={})=>{const o=e.map(parseColor).filter(e=>void 0!==e),r=o.filter((e,t)=>!o.slice(0,t).some(t=>isSameColor(t,e)));if(!r.length)throw new RangeError("deriveMapColors needs at least one parseable CSS colour.");const s=new RoleAssigner(r,t.mode??"auto"),{dark:a,land:l}=s,n=s.takeMostColorful(e=>e.s>=.2)??parseColor(s.shade(a?.55:.45,{s:.7}))??l,i=(s.takeHue(WATER_HUES)??s.takeHue(WIDER_WATER_HUES))?.css??s.shade(a?.12:.14,{h:205,s:Math.max(.35,l.s)}),c=s.takeHue(VEGETATION_HUES)?.css??s.shade(a?.08:.09,{h:110,s:Math.max(.18,.6*l.s)}),h=s.takeMostContrasting()?.css??s.labelShade(a?.8:.78,Math.min(l.s,.2)),d=((e,t)=>{const{dark:o,land:r}=e,s=e=>void 0!==e&&wcagContrast(e,r)>=3,a=e.takeMostColorful(e=>e.s>=.3&&s(e)&&hueDistance(e,t)>=30);if(a)return a.css;const l=(t.h??0)+180;return(o?[.62,.7,.78]:[.38,.3,.22]).map(e=>parseColor(hsl(l,.7,e))).find(s)?.css??hsl(l,.7,o?.85:.15)})(s,n),m=parseColor(c)??l;return{land:l.css,water:i,vegetation:c,park:hsl(m.h,m.s,m.l+(a?.04:.05)),artificial:s.shade(a?.04:.03),roadMajor:n.css,road:s.shade(a?.1:-.08,{s:.5*l.s}),roadOutline:s.shade(a?.24:.22,{s:.6*l.s}),label:h,labelHalo:s.shade(-.03),accent:d}},mapColorsLightDark=({land:e,label:t})=>wcagLuminance(t)>wcagLuminance(e)?"dark":"light",deriveMapColorsFromCss=(e,t={})=>{const{colors:o,mode:r}=readStylesheet(e,t);if(!o.length)throw new RangeError("deriveMapColorsFromCss: the stylesheet paints with no opaque colour to take a theme from.");return deriveMapColors(o,{mode:r})},squaredDistance=(e,t)=>(e[0]-t[0])**2+(e[1]-t[1])**2+(e[2]-t[2])**2,nearestIndex=(e,t)=>{let o=0,r=Number.POSITIVE_INFINITY;for(let s=0;s<t.length;s++){const a=squaredDistance(e,t[s]);a<r&&(r=a,o=s)}return o},dominantColors=(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:r,sizes:s}=((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=nearestIndex(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=>wcagLuminance(rgbFromBytes(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=squaredDistance(s,r[nearestIndex(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=>isSameColor(rgbFromBytes(e.rgb),rgbFromBytes(s)));e?e.size+=t[r]:o.push({rgb:s,size:t[r]})}return o})(r,s).toSorted((e,t)=>t.size-e.size).map(e=>formatHex(rgbFromBytes(e.rgb)))},deriveMapColorsFromImage=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(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})(e),o=document.createElement("canvas");o.width=64,o.height=64;const r=o.getContext("2d");if(!r)throw new Error("deriveMapColorsFromImage: no 2D canvas context available.");r.drawImage(t,0,0,64,64);try{return r.getImageData(0,0,64,64)}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=dominantColors(o,t.colorCount);if(!r.length)throw new RangeError("deriveMapColorsFromImage: the image has no opaque pixels to take colours from.");return deriveMapColors(r,t)};export{cssColors,deriveMapColors,deriveMapColorsFromCss,deriveMapColorsFromImage,dominantColors,mapColorsLightDark,mapThemeModes};
package/package.json CHANGED
@@ -1,26 +1,26 @@
1
1
  {
2
2
  "name": "@tomtom-org/maps-sdk-plugin-map-theme",
3
3
  "author": "TomTom International B.V.",
4
- "license": "See in LICENSE.txt",
5
- "version": "0.3.6",
4
+ "license": "SEE LICENSE IN LICENSE.txt",
5
+ "version": "0.3.8",
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
  "sideEffects": false,
10
10
  "peerDependencies": {
11
- "@tomtom-org/maps-sdk": ">=0.64.0 <1.0.0 || >=1.0.0-rc.0 <1.0.0",
11
+ "@tomtom-org/maps-sdk": ">=0.64.0 <1.0.0 || ^1.0.0-rc.0",
12
12
  "culori": "^4.0.2"
13
13
  },
14
14
  "devDependencies": {
15
15
  "@tsconfig/recommended": "^1.0.13",
16
16
  "@types/culori": "^4.0.1",
17
- "@types/node": "^26.6.4",
18
- "@vitest/coverage-v8": "^5.0.3",
19
- "rolldown": "^1.2.13",
17
+ "@types/node": "^26.6.5",
18
+ "@vitest/coverage-v8": "^5.0.5",
19
+ "rolldown": "^1.2.14",
20
20
  "typescript": "^6.0.3",
21
- "vite": "8.3.3",
22
- "vitest": "^5.0.3",
23
- "@tomtom-org/maps-sdk": "1.0.0-rc.3"
21
+ "vite": "^8.3.4",
22
+ "vitest": "^5.0.5",
23
+ "@tomtom-org/maps-sdk": "1.0.0-rc.5"
24
24
  },
25
25
  "files": [
26
26
  "dist",