diff --git a/.github/workflows/publish-style-spec.yml b/.github/workflows/publish-style-spec.yml index 3b8365f7d..6daebed5e 100644 --- a/.github/workflows/publish-style-spec.yml +++ b/.github/workflows/publish-style-spec.yml @@ -66,7 +66,7 @@ jobs: with: tag: ${{ steps.prepare_release.outputs.version_tag }} name: ${{steps.prepare_release.outputs.version_tag }} - body: ${{ steps.release_notes.outputs.release_notes }} + bodyFile: ${{ steps.release_notes.outputs.release_notes }} draft: false prerelease: false @@ -82,4 +82,4 @@ jobs: run: | npm publish --access=public --tag next env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_ORG_TOKEN }} \ No newline at end of file + NODE_AUTH_TOKEN: ${{ secrets.NPM_ORG_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ad322f76..6468dd51b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,23 +1,36 @@ ## main +## 19.0.1 + +### 🐛 Bug fixes + +* Restore support for `Color` instances in `Color.parse` [#128](https://github.com/maplibre/maplibre-style-spec/pull/128) + + +## 19.0.0 + ### ✨ Features and improvements * Add support for defining colors using CSS Color 4 syntax [#94](https://github.com/maplibre/maplibre-style-spec/pull/94) -* [Breaking] Interpretation of interpolation of colors with alpha channel equal to 0 has changed. When interpolating colors with alpha channel equal to 0, the values of the other color channels in the specified color space are included in the calculation. Previously they were ignored and only the alpha value of the other, not fully transparent, color was interpolated. [#94](https://github.com/maplibre/maplibre-style-spec/pull/94) ### 🐛 Bug fixes * Fix incorrect color interpolation in HCL and LAB color spaces when interpolation results are outside the sRGB gamut. [#94](https://github.com/maplibre/maplibre-style-spec/pull/94) +### Breaking changes + +Interpretation of interpolation of colors with alpha channel equal to 0 has changed. When interpolating colors with alpha channel equal to 0, the values of the other color channels in the specified color space are included in the calculation. Previously they were ignored and only the alpha value of the other, not fully transparent, color was interpolated. [#94](https://github.com/maplibre/maplibre-style-spec/pull/94) + + ## 18.0.0 * The maplibre style specification and utilities now has it's own repository. The major bump is mainly a precautionary measure, in case the restructuring causes friction. ## 17.1.0 -* Add support for multiple sprites in one style (#1805) +* Add support for multiple sprites in one style [#1805](https://github.com/maplibre/maplibre-style-spec/pull/1805) ## 17.0.2 -* Fix errors when running style-spec bin scripts and added missing help. Removed unnecessary script 'gl-style-composite'. ([#1971](https://github.com/maplibre/maplibre-gl-js/pull/1971)) +* Fix errors when running style-spec bin scripts and added missing help. Removed unnecessary script 'gl-style-composite'. [#1971](https://github.com/maplibre/maplibre-gl-js/pull/1971) ## 17.0.1 diff --git a/docs/public/maplibre-logo-dark.svg b/docs/public/maplibre-logo-dark.svg new file mode 100644 index 000000000..d859574f3 --- /dev/null +++ b/docs/public/maplibre-logo-dark.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/docs/src/components/header/header.tsx b/docs/src/components/header/header.tsx index 775df066f..416898fc3 100644 --- a/docs/src/components/header/header.tsx +++ b/docs/src/components/header/header.tsx @@ -2,7 +2,7 @@ import {useNavigate} from 'solid-start'; import style from './header.module.scss'; import {setShowNavOverlay} from '../app/app'; -const logo = `${import.meta.env.BASE_URL}maplibre-logo-big.svg`; +const logo = `${import.meta.env.BASE_URL}maplibre-logo-dark.svg`; export function Header() { const navigate = useNavigate(); diff --git a/package-lock.json b/package-lock.json index 1e3359111..673e2ec0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@maplibre/maplibre-gl-style-spec", - "version": "18.0.1", + "version": "19.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@maplibre/maplibre-gl-style-spec", - "version": "18.0.1", + "version": "19.0.1", "license": "ISC", "dependencies": { "@mapbox/jsonlint-lines-primitives": "~2.0.2", @@ -25,7 +25,7 @@ "gl-style-validate": "dist/gl-style-validate.mjs" }, "devDependencies": { - "@rollup/plugin-commonjs": "^24.0.1", + "@rollup/plugin-commonjs": "^24.1.0", "@rollup/plugin-json": "^6.0.0", "@rollup/plugin-node-resolve": "^15.0.2", "@rollup/plugin-replace": "^5.0.2", @@ -38,7 +38,7 @@ "@types/node": "^18.15.11", "@typescript-eslint/eslint-plugin": "^5.58.0", "@typescript-eslint/parser": "^5.58.0", - "dts-bundle-generator": "^7.2.0", + "dts-bundle-generator": "^8.0.1", "eslint": "^8.38.0", "eslint-config-mourner": "^3.0.0", "eslint-plugin-html": "^7.1.0", @@ -49,10 +49,10 @@ "jest": "^29.5.0", "jest-canvas-mock": "^2.5.0", "jest-environment-jsdom": "^29.5.0", - "rollup": "^3.20.2", + "rollup": "^3.20.4", "rollup-plugin-preserve-shebang": "^1.0.1", "rollup-plugin-sourcemaps": "^0.6.3", - "semver": "^7.3.8", + "semver": "^7.4.0", "ts-jest": "^29.1.0", "ts-node": "^10.9.1", "tslib": "^2.5.0", @@ -1332,9 +1332,9 @@ } }, "node_modules/@rollup/plugin-commonjs": { - "version": "24.0.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-24.0.1.tgz", - "integrity": "sha512-15LsiWRZk4eOGqvrJyu3z3DaBu5BhXIMeWnijSRvd8irrrg9SHpQ1pH+BUK4H6Z9wL9yOxZJMTLU+Au86XHxow==", + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-24.1.0.tgz", + "integrity": "sha512-eSL45hjhCWI0jCCXcNtLVqM5N1JlBGvlFfY0m6oOYnLCJ6N0qEXoZql4sY2MOUArzhH4SA/qBpTxvvZp2Sc+DQ==", "dev": true, "dependencies": { "@rollup/pluginutils": "^5.0.1", @@ -2896,12 +2896,12 @@ } }, "node_modules/dts-bundle-generator": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/dts-bundle-generator/-/dts-bundle-generator-7.2.0.tgz", - "integrity": "sha512-pHjRo52hvvLDRijzIYRTS9eJR7vAOs3gd/7jx+7YVnLU8ay3yPUWGtHXPtuMBSlJYk/s4nq1SvXObDCZVguYMg==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/dts-bundle-generator/-/dts-bundle-generator-8.0.1.tgz", + "integrity": "sha512-9JVw78/OXdKfq+RUrmpLm6WAUJp+aOUGEHimVqIlOEH2VugRt1I8CVIoQZlirWZko+/SVZkNgpWCyZubUuzzPA==", "dev": true, "dependencies": { - "typescript": ">=4.5.2", + "typescript": ">=5.0.2", "yargs": "^17.6.0" }, "bin": { @@ -6527,9 +6527,9 @@ } }, "node_modules/rollup": { - "version": "3.20.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.20.2.tgz", - "integrity": "sha512-3zwkBQl7Ai7MFYQE0y1MeQ15+9jsi7XxfrqwTb/9EK8D9C9+//EBR4M+CuA1KODRaNbFez/lWxA5vhEGZp4MUg==", + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.20.4.tgz", + "integrity": "sha512-n7J4tuctZXUErM9Uc916httwqmTc63zzCr2+TLCiSCpfO/Xuk3g/marGN1IlRJZi+QF3XMYx75PxXRfZDVgaRw==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -6692,9 +6692,9 @@ } }, "node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.4.0.tgz", + "integrity": "sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" diff --git a/package.json b/package.json index 2d06e7700..86bf7337c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@maplibre/maplibre-gl-style-spec", "description": "a specification for maplibre gl styles", - "version": "18.0.1", + "version": "19.0.1", "author": "MapLibre", "keywords": [ "mapbox", @@ -52,7 +52,7 @@ }, "sideEffects": false, "devDependencies": { - "@rollup/plugin-commonjs": "^24.0.1", + "@rollup/plugin-commonjs": "^24.1.0", "@rollup/plugin-json": "^6.0.0", "@rollup/plugin-node-resolve": "^15.0.2", "@rollup/plugin-replace": "^5.0.2", @@ -65,7 +65,7 @@ "@types/node": "^18.15.11", "@typescript-eslint/eslint-plugin": "^5.58.0", "@typescript-eslint/parser": "^5.58.0", - "dts-bundle-generator": "^7.2.0", + "dts-bundle-generator": "^8.0.1", "eslint": "^8.38.0", "eslint-config-mourner": "^3.0.0", "eslint-plugin-html": "^7.1.0", @@ -76,10 +76,10 @@ "jest": "^29.5.0", "jest-canvas-mock": "^2.5.0", "jest-environment-jsdom": "^29.5.0", - "rollup": "^3.20.2", + "rollup": "^3.20.4", "rollup-plugin-preserve-shebang": "^1.0.1", "rollup-plugin-sourcemaps": "^0.6.3", - "semver": "^7.3.8", + "semver": "^7.4.0", "ts-jest": "^29.1.0", "ts-node": "^10.9.1", "tslib": "^2.5.0", diff --git a/src/function/index.test.ts b/src/function/index.test.ts index ea1fb66cc..abb098a3e 100644 --- a/src/function/index.test.ts +++ b/src/function/index.test.ts @@ -448,6 +448,40 @@ describe('exponential function', () => { }); + test('zoom-and-property function, color', () => { + const f = createFunction({ + type: 'exponential', + property: 'prop', + stops: [ + [{zoom: 0, value: 0}, 'red'], [{zoom: 1, value: 0}, 'blue'], + [{zoom: 0, value: 1}, 'lime'], [{zoom: 1, value: 1}, 'magenta'], + ], + }, { + type: 'color', + }).evaluate; + + expect(f({zoom: 0.0}, {properties: {prop: 0}})).toEqual(new Color(1, 0, 0, 1)); + expect(f({zoom: 1.0}, {properties: {prop: 0}})).toEqual(new Color(0, 0, 1, 1)); + expect(f({zoom: 0.5}, {properties: {prop: 1}})).toEqual(new Color(0.5, 0.5, 0.5, 1)); + }); + + test('zoom-and-property function, padding', () => { + const f = createFunction({ + type: 'exponential', + property: 'prop', + stops: [ + [{zoom: 0, value: 0}, [2]], [{zoom: 1, value: 0}, [4]], + [{zoom: 0, value: 1}, [6]], [{zoom: 1, value: 1}, [8]], + ], + }, { + type: 'padding', + }).evaluate; + + expect(f({zoom: 0.0}, {properties: {prop: 0}})).toEqual(new Padding([2, 2, 2, 2])); + expect(f({zoom: 1.0}, {properties: {prop: 0}})).toEqual(new Padding([4, 4, 4, 4])); + expect(f({zoom: 0.5}, {properties: {prop: 1}})).toEqual(new Padding([7, 7, 7, 7])); + }); + }); describe('interval function', () => { diff --git a/src/migrate.test.ts b/src/migrate.test.ts index fcb8bbb08..ed0cc8a60 100644 --- a/src/migrate.test.ts +++ b/src/migrate.test.ts @@ -109,4 +109,34 @@ describe('migrate', () => { ]); expect(validate(migrated, v8)).toEqual([]); }); + + test('converts colors to supported format', () => { + const migrated = migrate({ + version: 8, + sources: {}, + layers: [{ + id: '1', + type: 'fill', + source: 'vector', + paint: { + 'fill-color': 'hsl(100,0.3,.2)', + 'fill-outline-color': [ + 'interpolate', ['linear'], ['zoom'], + 0, 'hsl(110, 0.7, 0.055)', + 10, 'hsla(330,0.85,50%)', + ], + }, + }], + }); + + expect(migrated.layers[0].paint).toEqual({ + 'fill-color': 'hsl(100,30%,20%)', + 'fill-outline-color': [ + 'interpolate', ['linear'], ['zoom'], + 0, 'hsl(110,70%,5.5%)', + 10, 'hsl(330,85%,50%)', + ], + }); + }); + }); diff --git a/src/migrate.ts b/src/migrate.ts index 949bcd60c..bf64eadde 100644 --- a/src/migrate.ts +++ b/src/migrate.ts @@ -1,6 +1,8 @@ import migrateToV8 from './migrate/v8'; import migrateToExpressions from './migrate/expressions'; +import migrateColors from './migrate/migrate_colors'; +import {eachProperty} from './visit'; import type {StyleSpecification} from './types.g'; /** @@ -29,6 +31,12 @@ export default function migrate(style: StyleSpecification): StyleSpecification { migrated = true; } + eachProperty(style, {paint: true, layout: true}, ({value, reference, set}) => { + if (reference.type === 'color') { + set(migrateColors(value)); + } + }); + if (!migrated) { throw new Error(`Cannot migrate from ${style.version}`); } diff --git a/src/migrate/migrate_colors.test.ts b/src/migrate/migrate_colors.test.ts new file mode 100644 index 000000000..d5b654871 --- /dev/null +++ b/src/migrate/migrate_colors.test.ts @@ -0,0 +1,24 @@ +import migrateColors from './migrate_colors'; + +describe('migrate colors', () => { + + test('should convert hsl to a format compliant with CSS Color specification', () => { + expect(migrateColors('hsla(0, 0, 0, 0)')).toBe('hsla(0,0%,0%,0)'); + expect(migrateColors('hsl(900, 0.15, 90%)')).toBe('hsl(900,15%,90%)'); + expect(migrateColors('hsla(900, .15, .9)')).toBe('hsl(900,15%,90%)'); + expect(migrateColors('hsl(900, 15%, 90%)')).toBe('hsl(900,15%,90%)'); + expect(migrateColors('hsla(900, 15%, 90%)')).toBe('hsl(900,15%,90%)'); + expect(migrateColors('hsla(900, 15%, 90%, 1)')).toBe('hsla(900,15%,90%,1)'); + expect(migrateColors([ + 'interpolate', ['linear'], ['zoom'], + 0, 'hsla(900,0.85,0.05,0)', + 10, 'hsla(900, .20, .0155, 1)', + ])).toEqual([ + 'interpolate', ['linear'], ['zoom'], + 0, 'hsla(900,85%,5%,0)', + 10, 'hsla(900,20%,1.55%,1)', + ]); + expect(migrateColors('hsl(9001590)')).toBe('hsl(9001590)'); // invalid - no changes + }); + +}); diff --git a/src/migrate/migrate_colors.ts b/src/migrate/migrate_colors.ts new file mode 100644 index 000000000..670a3b290 --- /dev/null +++ b/src/migrate/migrate_colors.ts @@ -0,0 +1,35 @@ +/** + * Migrate color style values to supported format. + * + * @param colorToMigrate Color value to migrate, could be a string or an expression. + * @returns Color style value in supported format. + */ +export default function migrateColors(colorToMigrate: T): T { + return JSON.parse(migrateHslColors(JSON.stringify(colorToMigrate))); +} + +/** + * Created to migrate from colors supported by the former CSS color parsing + * library `csscolorparser` but not compliant with the CSS Color specification, + * like `hsl(900, 0.15, 90%)`. + * + * @param colorToMigrate Serialized color style value. + * @returns A serialized color style value in which all non-standard hsl color values + * have been converted to a format that complies with the CSS Color specification. + * + * @example + * migrateHslColors('"hsl(900, 0.15, 90%)"'); // returns '"hsl(900, 15%, 90%)"' + * migrateHslColors('"hsla(900, .15, .9)"'); // returns '"hsl(900, 15%, 90%)"' + * migrateHslColors('"hsl(900, 15%, 90%)"'); // returns '"hsl(900, 15%, 90%)"' - no changes + */ +function migrateHslColors(colorToMigrate: string): string { + return colorToMigrate.replace(/"hsla?\((.+?)\)"/gi, (match, hslArgs) => { + const argsMatch = hslArgs.match(/^(.+?)\s*,\s*(.+?)\s*,\s*(.+?)(?:\s*,\s*(.+))?$/i); + if (argsMatch) { + let [h, s, l, a] = argsMatch.slice(1); + [s, l] = [s, l].map(v => v.endsWith('%') ? v : `${parseFloat(v) * 100}%`); + return `"hsl${typeof a === 'string' ? 'a' : ''}(${[h, s, l, a].filter(Boolean).join(',')})"`; + } + return match; + }); +} diff --git a/src/util/color.test.ts b/src/util/color.test.ts index 7bd861baa..efe22d1f7 100644 --- a/src/util/color.test.ts +++ b/src/util/color.test.ts @@ -55,6 +55,11 @@ describe('Color class', () => { expect(Color.parse('rgb(0deg,0,0)')).toBeUndefined(); }); + test('should accept instances of Color class', () => { + const color = new Color(0, 0, 0, 0); + expect(Color.parse(color)).toBe(color); + }); + }); test('should keep a reference to the original color when alpha=0', () => { diff --git a/src/util/color.ts b/src/util/color.ts index 253d79089..fc0d96f12 100644 --- a/src/util/color.ts +++ b/src/util/color.ts @@ -58,7 +58,12 @@ class Color { * @param input CSS color string to parse. * @returns A `Color` instance, or `undefined` if the input is not a valid color string. */ - static parse(input: string | undefined | null): Color | undefined { + static parse(input: Color | string | undefined | null): Color | undefined { + // in zoom-and-property function input could be an instance of Color class + if (input instanceof Color) { + return input; + } + if (typeof input !== 'string') { return; }