diff --git a/package-lock.json b/package-lock.json index 1b218252d59893..e7b345b534188e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -896,6 +896,7 @@ "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" @@ -957,6 +958,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1244,6 +1246,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -1519,7 +1522,8 @@ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz", "integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==", "dev": true, - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/dom-serializer": { "version": "2.0.0", @@ -1701,6 +1705,7 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3210,6 +3215,7 @@ "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, diff --git a/src/math/Color.js b/src/math/Color.js index f42d664478a695..5fc1168670fccb 100644 --- a/src/math/Color.js +++ b/src/math/Color.js @@ -285,11 +285,11 @@ class Color { */ setStyle( style, colorSpace = SRGBColorSpace ) { - function handleAlpha( string ) { + function handleAlpha( value ) { - if ( string === undefined ) return; + if ( value === undefined ) return; - if ( parseFloat( string ) < 1 ) { + if ( value < 1 ) { warn( 'Color: Alpha component of ' + style + ' will be ignored.' ); @@ -297,15 +297,103 @@ class Color { } + function parseAlpha( string ) { + + if ( string === undefined ) return undefined; + + const match = /^([+-]?\d*\.?\d+)(%)?$/.exec( string.trim() ); + if ( match === null ) return null; + + let alpha = parseFloat( match[ 1 ] ); + + if ( match[ 2 ] === '%' ) { + + alpha /= 100; + + } + + return clamp( alpha, 0, 1 ); + + } + + function parseFunctionComponents( components ) { + + if ( components.includes( ',' ) ) { + + const parts = components.split( /\s*,\s*/ ); + if ( parts.length !== 3 && parts.length !== 4 ) return null; + + if ( parts.some( ( part ) => part.length === 0 ) ) return null; + + return { + values: parts.slice( 0, 3 ), + alpha: parts[ 3 ] + }; + + } + + const slashParts = components.split( /\s*\/\s*/ ); + + if ( slashParts.length > 2 ) return null; + + const values = slashParts[ 0 ].trim().split( /\s+/ ); + if ( values.length !== 3 || values.some( ( value ) => value.length === 0 ) ) return null; + + return { + values: values, + alpha: slashParts[ 1 ] + }; + + } + + function parseRGBValue( string ) { + + const match = /^([+-]?\d*\.?\d+)(%)?$/.exec( string.trim() ); + if ( match === null ) return null; + + let value = parseFloat( match[ 1 ] ); + + if ( match[ 2 ] === '%' ) { + + value = Math.min( 100, value ) / 100; + + } else { + + value = Math.min( 255, value ) / 255; + + } + + return value; + + } + + function parseHue( string ) { + + const match = /^([+-]?\d*\.?\d+)(deg)?$/i.exec( string.trim() ); + if ( match === null ) return null; + + return parseFloat( match[ 1 ] ); + + } + + function parsePercent( string ) { + + const match = /^([+-]?\d*\.?\d+)%$/.exec( string.trim() ); + if ( match === null ) return null; + + return parseFloat( match[ 1 ] ) / 100; + + } + let m; - if ( m = /^(\w+)\(([^\)]*)\)/.exec( style ) ) { + if ( m = /^\s*([A-Za-z]+)\(([^\)]*)\)\s*$/.exec( style ) ) { // rgb / hsl let color; - const name = m[ 1 ]; + const name = m[ 1 ].toLowerCase(); const components = m[ 2 ]; switch ( name ) { @@ -313,56 +401,57 @@ class Color { case 'rgb': case 'rgba': - if ( color = /^\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec( components ) ) { - - // rgb(255,0,0) rgba(255,0,0,0.5) + color = parseFunctionComponents( components ); - handleAlpha( color[ 4 ] ); + if ( color !== null ) { - return this.setRGB( - Math.min( 255, parseInt( color[ 1 ], 10 ) ) / 255, - Math.min( 255, parseInt( color[ 2 ], 10 ) ) / 255, - Math.min( 255, parseInt( color[ 3 ], 10 ) ) / 255, - colorSpace - ); + const alpha = parseAlpha( color.alpha ); + const r = parseRGBValue( color.values[ 0 ] ); + const g = parseRGBValue( color.values[ 1 ] ); + const b = parseRGBValue( color.values[ 2 ] ); - } + if ( alpha !== null && r !== null && g !== null && b !== null ) { - if ( color = /^\s*(\d+)\%\s*,\s*(\d+)\%\s*,\s*(\d+)\%\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec( components ) ) { + // rgb(255,0,0), rgb(255 0 0), rgb(255 0 0 / 50%), rgba(...) - // rgb(100%,0%,0%) rgba(100%,0%,0%,0.5) + handleAlpha( alpha ); - handleAlpha( color[ 4 ] ); + return this.setRGB( r, g, b, colorSpace ); - return this.setRGB( - Math.min( 100, parseInt( color[ 1 ], 10 ) ) / 100, - Math.min( 100, parseInt( color[ 2 ], 10 ) ) / 100, - Math.min( 100, parseInt( color[ 3 ], 10 ) ) / 100, - colorSpace - ); + } } + warn( 'Color: Invalid color components ' + style ); + break; case 'hsl': case 'hsla': - if ( color = /^\s*(\d*\.?\d+)\s*,\s*(\d*\.?\d+)\%\s*,\s*(\d*\.?\d+)\%\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec( components ) ) { + color = parseFunctionComponents( components ); + + if ( color !== null ) { + + const alpha = parseAlpha( color.alpha ); + const h = parseHue( color.values[ 0 ] ); + const s = parsePercent( color.values[ 1 ] ); + const l = parsePercent( color.values[ 2 ] ); - // hsl(120,50%,50%) hsla(120,50%,50%,0.5) + if ( alpha !== null && h !== null && s !== null && l !== null ) { - handleAlpha( color[ 4 ] ); + // hsl(120,50%,50%), hsl(120deg 50% 50% / 50%), hsla(...) - return this.setHSL( - parseFloat( color[ 1 ] ) / 360, - parseFloat( color[ 2 ] ) / 100, - parseFloat( color[ 3 ] ) / 100, - colorSpace - ); + handleAlpha( alpha ); + + return this.setHSL( h / 360, s, l, colorSpace ); + + } } + warn( 'Color: Invalid color components ' + style ); + break; default: diff --git a/test/unit/src/math/Color.tests.js b/test/unit/src/math/Color.tests.js index e1c74dc17eca00..1b549c2769700f 100644 --- a/test/unit/src/math/Color.tests.js +++ b/test/unit/src/math/Color.tests.js @@ -690,6 +690,50 @@ export default QUnit.module( 'Maths', () => { } ); + QUnit.test( 'setStyleRGBSpaceSeparated', ( assert ) => { + + ColorManagement.enabled = false; // TODO: Update and enable. + + const c = new Color(); + c.setStyle( 'rgb(255 0 0)' ); + assert.ok( c.r == 1, 'Red: ' + c.r ); + assert.ok( c.g === 0, 'Green: ' + c.g ); + assert.ok( c.b === 0, 'Blue: ' + c.b ); + + } ); + + QUnit.test( 'setStyleRGBSpaceSeparatedWithAlpha', ( assert ) => { + + ColorManagement.enabled = false; // TODO: Update and enable. + + const c = new Color(); + + console.level = CONSOLE_LEVEL.ERROR; + c.setStyle( 'rgb(255 0 0 / 0.5)' ); + console.level = CONSOLE_LEVEL.DEFAULT; + + assert.ok( c.r == 1, 'Red: ' + c.r ); + assert.ok( c.g === 0, 'Green: ' + c.g ); + assert.ok( c.b === 0, 'Blue: ' + c.b ); + + } ); + + QUnit.test( 'setStyleRGBPercentAlphaPercent', ( assert ) => { + + ColorManagement.enabled = false; // TODO: Update and enable. + + const c = new Color(); + + console.level = CONSOLE_LEVEL.ERROR; + c.setStyle( 'rgb(100%,50%,10%,50%)' ); + console.level = CONSOLE_LEVEL.DEFAULT; + + assert.ok( c.r == 1, 'Red: ' + c.r ); + assert.ok( c.g == 0.5, 'Green: ' + c.g ); + assert.ok( c.b == 0.1, 'Blue: ' + c.b ); + + } ); + QUnit.test( 'setStyleHSLRed', ( assert ) => { ColorManagement.enabled = false; // TODO: Update and enable. @@ -774,6 +818,56 @@ export default QUnit.module( 'Maths', () => { } ); + QUnit.test( 'setStyleHSLSpaceSeparatedWithDegrees', ( assert ) => { + + ColorManagement.enabled = false; // TODO: Update and enable. + + const c = new Color(); + c.setStyle( 'hsl(120deg 100% 50%)' ); + assert.ok( Math.abs( c.r ) <= eps, 'Red: ' + c.r ); + assert.ok( c.g == 1, 'Green: ' + c.g ); + assert.ok( Math.abs( c.b ) <= eps, 'Blue: ' + c.b ); + + } ); + + QUnit.test( 'setStyleUpperCaseRGBModel', ( assert ) => { + + ColorManagement.enabled = false; // TODO: Update and enable. + + const c = new Color(); + c.setStyle( 'RGB(255,0,0)' ); + assert.ok( c.r == 1, 'Red: ' + c.r ); + assert.ok( c.g === 0, 'Green: ' + c.g ); + assert.ok( c.b === 0, 'Blue: ' + c.b ); + + } ); + + QUnit.test( 'setStyleUpperCaseHSLModel', ( assert ) => { + + ColorManagement.enabled = false; // TODO: Update and enable. + + const c = new Color(); + c.setStyle( 'HSL(120,100%,50%)' ); + assert.ok( Math.abs( c.r ) <= eps, 'Red: ' + c.r ); + assert.ok( c.g == 1, 'Green: ' + c.g ); + assert.ok( Math.abs( c.b ) <= eps, 'Blue: ' + c.b ); + + } ); + + QUnit.test( 'setStyleInvalidKnownModelDoesNotApply', ( assert ) => { + + ColorManagement.enabled = false; // TODO: Update and enable. + + const c = new Color( 0x123456 ); + + console.level = CONSOLE_LEVEL.ERROR; + c.setStyle( 'rgb(255,0,oops)' ); + console.level = CONSOLE_LEVEL.DEFAULT; + + assert.strictEqual( c.getHex(), 0x123456, 'Invalid known-model components should not mutate color.' ); + + } ); + QUnit.test( 'setStyleHexSkyBlue', ( assert ) => { ColorManagement.enabled = false; // TODO: Update and enable.