From 670108dcdcb6ffd3e34c4997649d34059a86c7f8 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Tue, 31 Oct 2017 15:59:23 -0700 Subject: [PATCH] Use Color ubiquitously --- src/data/program_configuration.js | 9 +- src/render/draw_background.js | 7 +- src/render/draw_fill.js | 2 +- src/render/draw_fill_extrusion.js | 2 +- .../expression/definitions/coercion.js | 4 +- .../expression/definitions/index.js | 8 +- .../expression/definitions/interpolate.js | 8 +- .../expression/evaluation_context.js | 4 +- src/style-spec/expression/index.js | 24 +++-- src/style-spec/expression/values.js | 15 +--- src/style-spec/function/index.js | 10 +-- src/style-spec/util/color.js | 49 ++++++++++ src/style-spec/util/color_spaces.js | 90 +++++++++++-------- src/style-spec/util/interpolate.js | 31 +++---- src/style-spec/util/parse_color.js | 25 ------ src/style/style_declaration.js | 4 +- src/style/style_layer.js | 4 +- .../style_layer/fill_extrusion_style_layer.js | 2 +- src/style/style_layer/heatmap_style_layer.js | 12 +-- test/expression.test.js | 7 +- test/unit/style-spec/color.js | 15 ++++ test/unit/style-spec/function.test.js | 41 ++++----- test/unit/style-spec/interpolate.test.js | 3 +- test/unit/style-spec/parse_color.js | 13 --- .../unit/style-spec/util/color_spaces.test.js | 17 ++-- test/unit/style/light.test.js | 5 +- test/unit/style/style.test.js | 3 +- test/unit/style/style_declaration.test.js | 7 +- test/unit/style/style_layer.test.js | 9 +- 29 files changed, 229 insertions(+), 201 deletions(-) create mode 100644 src/style-spec/util/color.js delete mode 100644 src/style-spec/util/parse_color.js create mode 100644 test/unit/style-spec/color.js delete mode 100644 test/unit/style-spec/parse_color.js diff --git a/src/data/program_configuration.js b/src/data/program_configuration.js index f4e5e860ec6..b722cfe636d 100644 --- a/src/data/program_configuration.js +++ b/src/data/program_configuration.js @@ -8,6 +8,7 @@ import type StyleLayer from '../style/style_layer'; import type {ViewType, StructArray, SerializedStructArray, StructArrayTypeParameters} from '../util/struct_array'; import type Program from '../render/program'; import type {Feature} from '../style-spec/expression'; +import type Color from '../style-spec/util/color'; type LayoutAttribute = { name: string, @@ -35,10 +36,10 @@ export type ProgramInterface = { indexArrayType2?: Class } -function packColor(color: [number, number, number, number]): [number, number] { +function packColor(color: Color): [number, number] { return [ - packUint8ToFloat(255 * color[0], 255 * color[1]), - packUint8ToFloat(255 * color[2], 255 * color[3]) + packUint8ToFloat(255 * color.r, 255 * color.g), + packUint8ToFloat(255 * color.b, 255 * color.a) ]; } @@ -82,7 +83,7 @@ class ConstantBinder implements Binder { setUniforms(gl: WebGLRenderingContext, program: Program, layer: StyleLayer, {zoom}: { zoom: number }) { const value = layer.getPaintValue(this.property, { zoom: this.useIntegerZoom ? Math.floor(zoom) : zoom }); if (this.type === 'color') { - gl.uniform4fv(program.uniforms[`u_${this.name}`], value); + gl.uniform4f(program.uniforms[`u_${this.name}`], value.r, value.g, value.b, value.a); } else { gl.uniform1f(program.uniforms[`u_${this.name}`], value); } diff --git a/src/render/draw_background.js b/src/render/draw_background.js index 0e6c63329b6..6eb52573c82 100644 --- a/src/render/draw_background.js +++ b/src/render/draw_background.js @@ -5,6 +5,7 @@ const pattern = require('./pattern'); import type Painter from './painter'; import type SourceCache from '../source/source_cache'; import type BackgroundStyleLayer from '../style/style_layer/background_style_layer'; +import type Color from '../style-spec/util/color'; module.exports = drawBackground; @@ -14,11 +15,11 @@ function drawBackground(painter: Painter, sourceCache: SourceCache, layer: Backg const gl = painter.gl; const transform = painter.transform; const tileSize = transform.tileSize; - const color = layer.paint['background-color']; + const color: Color = layer.paint['background-color']; const image = layer.paint['background-pattern']; const opacity = layer.paint['background-opacity']; - const pass = (!image && color[3] === 1 && opacity === 1) ? 'opaque' : 'translucent'; + const pass = (!image && color.a === 1 && opacity === 1) ? 'opaque' : 'translucent'; if (painter.renderPass !== pass) return; gl.disable(gl.STENCIL_TEST); @@ -33,7 +34,7 @@ function drawBackground(painter: Painter, sourceCache: SourceCache, layer: Backg painter.tileExtentPatternVAO.bind(gl, program, painter.tileExtentBuffer); } else { program = painter.useProgram('fill', painter.basicFillProgramConfiguration); - gl.uniform4fv(program.uniforms.u_color, color); + gl.uniform4f(program.uniforms.u_color, color.r, color.g, color.b, color.a); painter.tileExtentVAO.bind(gl, program, painter.tileExtentBuffer); } diff --git a/src/render/draw_fill.js b/src/render/draw_fill.js index ec0fd87724f..d6f9d79548a 100644 --- a/src/render/draw_fill.js +++ b/src/render/draw_fill.js @@ -19,7 +19,7 @@ function drawFill(painter: Painter, sourceCache: SourceCache, layer: FillStyleLa const pass = (!layer.paint['fill-pattern'] && layer.isPaintValueFeatureConstant('fill-color') && layer.isPaintValueFeatureConstant('fill-opacity') && - layer.paint['fill-color'][3] === 1 && + layer.paint['fill-color'].a === 1 && layer.paint['fill-opacity'] === 1) ? 'opaque' : 'translucent'; // Draw fill diff --git a/src/render/draw_fill_extrusion.js b/src/render/draw_fill_extrusion.js index 7d51147c236..502172ff679 100644 --- a/src/render/draw_fill_extrusion.js +++ b/src/render/draw_fill_extrusion.js @@ -111,5 +111,5 @@ function setLight(program, painter) { gl.uniform3fv(program.uniforms.u_lightpos, lightPos); gl.uniform1f(program.uniforms.u_lightintensity, light.calculated.intensity); - gl.uniform3fv(program.uniforms.u_lightcolor, light.calculated.color.slice(0, 3)); + gl.uniform3f(program.uniforms.u_lightcolor, light.calculated.color.r, light.calculated.color.g, light.calculated.color.b); } diff --git a/src/style-spec/expression/definitions/coercion.js b/src/style-spec/expression/definitions/coercion.js index dccc92ecfc7..86550c89ec9 100644 --- a/src/style-spec/expression/definitions/coercion.js +++ b/src/style-spec/expression/definitions/coercion.js @@ -7,7 +7,7 @@ const { NumberType, } = require('../types'); -const { Color, validateRGBA, unwrap } = require('../values'); +const { Color, validateRGBA } = require('../values'); const RuntimeError = require('../runtime_error'); import type { Expression } from '../expression'; @@ -88,7 +88,7 @@ class Coercion implements Expression { if (isNaN(num)) continue; return num; } - throw new RuntimeError(`Could not convert ${JSON.stringify(unwrap(value))} to number.`); + throw new RuntimeError(`Could not convert ${JSON.stringify(value)} to number.`); } } diff --git a/src/style-spec/expression/definitions/index.js b/src/style-spec/expression/definitions/index.js index c933c4872cc..17c8c28b11d 100644 --- a/src/style-spec/expression/definitions/index.js +++ b/src/style-spec/expression/definitions/index.js @@ -103,8 +103,7 @@ CompoundExpression.register(expressions, { if (v === null || type === 'string' || type === 'number' || type === 'boolean') { return String(v); } else if (v instanceof Color) { - const [r, g, b, a] = v.value; - return `rgba(${r * 255},${g * 255},${b * 255},${a})`; + return `rgba(${v.r * 255},${v.g * 255},${v.b * 255},${v.a})`; } else { return JSON.stringify(v); } @@ -118,7 +117,10 @@ CompoundExpression.register(expressions, { 'to-rgba': [ array(NumberType, 4), [ColorType], - (ctx, [v]) => v.evaluate(ctx).value + (ctx, [v]) => { + const {r, g, b, a} = v.evaluate(ctx); + return [r, g, b, a]; + } ], 'rgb': [ ColorType, diff --git a/src/style-spec/expression/definitions/interpolate.js b/src/style-spec/expression/definitions/interpolate.js index 11c1c6fc9cd..05622bfd7f1 100644 --- a/src/style-spec/expression/definitions/interpolate.js +++ b/src/style-spec/expression/definitions/interpolate.js @@ -3,7 +3,6 @@ const UnitBezier = require('@mapbox/unitbezier'); const interpolate = require('../../util/interpolate'); const { toString, NumberType } = require('../types'); -const { Color } = require('../values'); const { findStopLessThanOrEqualTo } = require("../stops"); import type { Stops } from '../stops'; @@ -167,12 +166,7 @@ class Interpolate implements Expression { const outputLower = outputs[index].evaluate(ctx); const outputUpper = outputs[index + 1].evaluate(ctx); - const type = this.type.kind.toLowerCase(); - if (type === 'color') { - return new Color(...interpolate.color((outputLower: any).value, (outputUpper: any).value, t)); - } - - return interpolate[type](outputLower, outputUpper, t); + return (interpolate[this.type.kind.toLowerCase()]: any)(outputLower, outputUpper, t); } eachChild(fn: (Expression) => void) { diff --git a/src/style-spec/expression/evaluation_context.js b/src/style-spec/expression/evaluation_context.js index bf0351d45f6..002e7e44093 100644 --- a/src/style-spec/expression/evaluation_context.js +++ b/src/style-spec/expression/evaluation_context.js @@ -2,7 +2,6 @@ const assert = require('assert'); const Scope = require('./scope'); -const parseColor = require('../util/parse_color'); const {Color} = require('./values'); import type { Feature, GlobalProperties } from './index'; @@ -46,8 +45,7 @@ class EvaluationContext { parseColor(input: string): ?Color { let cached = this._parseColorCache[input]; if (!cached) { - const c = parseColor(input); - cached = this._parseColorCache[input] = c ? new Color(c[0], c[1], c[2], c[3]) : null; + cached = this._parseColorCache[input] = Color.parse(input); } return cached; } diff --git a/src/style-spec/expression/index.js b/src/style-spec/expression/index.js index 158a0945776..f27a171b2f0 100644 --- a/src/style-spec/expression/index.js +++ b/src/style-spec/expression/index.js @@ -11,7 +11,6 @@ const Coalesce = require('./definitions/coalesce'); const Let = require('./definitions/let'); const definitions = require('./definitions'); const isConstant = require('./is_constant'); -const {unwrap} = require('./values'); import type {Type} from './types'; import type {Value} from './values'; @@ -147,9 +146,9 @@ function createExpression(expression: mixed, try { const val = parsed.evaluate(evaluator); if (val === null || val === undefined) { - return unwrap(defaultValue); + return defaultValue; } - return unwrap(val); + return val; } catch (e) { if (!warningHistory[e.message]) { warningHistory[e.message] = true; @@ -157,7 +156,7 @@ function createExpression(expression: mixed, console.warn(e.message); } } - return unwrap(defaultValue); + return defaultValue; } }; } @@ -293,20 +292,19 @@ function getExpectedType(spec: StylePropertySpecification): Type | null { } const {isFunction} = require('../function'); -const parseColor = require('../util/parse_color'); const {Color} = require('./values'); -function getDefaultValue(spec: StylePropertySpecification): Value | null { - const defaultValue = spec.default; - if (spec.type === 'color' && isFunction(defaultValue)) { +function getDefaultValue(spec: StylePropertySpecification): Value { + if (spec.type === 'color' && isFunction(spec.default)) { // Special case for heatmap-color: it uses the 'default:' to define a // default color ramp, but createExpression expects a simple value to fall // back to in case of runtime errors - return [0, 0, 0, 0]; + return new Color(0, 0, 0, 0); } else if (spec.type === 'color') { - const c: [number, number, number, number] = (parseColor((defaultValue: any)): any); - assert(Array.isArray(c)); - return new Color(c[0], c[1], c[2], c[3]); + return Color.parse(spec.default) || null; + } else if (spec.default === undefined) { + return null; + } else { + return spec.default; } - return defaultValue === undefined ? null : defaultValue; } diff --git a/src/style-spec/expression/values.js b/src/style-spec/expression/values.js index 68ddee6f4c6..f9b7fc23095 100644 --- a/src/style-spec/expression/values.js +++ b/src/style-spec/expression/values.js @@ -1,6 +1,7 @@ // @flow const assert = require('assert'); +const Color = require('../util/color'); const { NullType, @@ -15,13 +16,6 @@ const { import type { Type } from './types'; -class Color { - value: [number, number, number, number]; - constructor(r: number, g: number, b: number, a: number = 1) { - this.value = [r, g, b, a]; - } -} - function validateRGBA(r: mixed, g: mixed, b: mixed, a?: mixed): ?string { if (!( typeof r === 'number' && r >= 0 && r <= 255 && @@ -107,14 +101,9 @@ function typeOf(value: Value): Type { } } -function unwrap(value: Value): mixed { - return value instanceof Color ? value.value : value; -} - module.exports = { Color, validateRGBA, isValue, - typeOf, - unwrap + typeOf }; diff --git a/src/style-spec/function/index.js b/src/style-spec/function/index.js index 36fb734aeb9..278fcae0050 100644 --- a/src/style-spec/function/index.js +++ b/src/style-spec/function/index.js @@ -1,6 +1,6 @@ const colorSpaces = require('../util/color_spaces'); -const parseColor = require('../util/parse_color'); +const Color = require('../util/color'); const extend = require('../util/extend'); const getType = require('../util/get_type'); const interpolate = require('../util/interpolate'); @@ -26,14 +26,14 @@ function createFunction(parameters, propertySpec, name) { if (parameters.stops) { parameters.stops = parameters.stops.map((stop) => { - return [stop[0], parseColor(stop[1])]; + return [stop[0], Color.parse(stop[1])]; }); } if (parameters.default) { - parameters.default = parseColor(parameters.default); + parameters.default = Color.parse(parameters.default); } else { - parameters.default = parseColor(propertySpec.default); + parameters.default = Color.parse(propertySpec.default); } } @@ -216,7 +216,7 @@ function evaluateExponentialFunction(parameters, propertySpec, input) { function evaluateIdentityFunction(parameters, propertySpec, input) { if (propertySpec.type === 'color') { - input = parseColor(input); + input = Color.parse(input); } else if (getType(input) !== propertySpec.type && (propertySpec.type !== 'enum' || !propertySpec.values[input])) { input = undefined; } diff --git a/src/style-spec/util/color.js b/src/style-spec/util/color.js new file mode 100644 index 00000000000..907284ae87c --- /dev/null +++ b/src/style-spec/util/color.js @@ -0,0 +1,49 @@ +// @flow + +const {parseCSSColor} = require('csscolorparser'); + +/** + * An RGBA color value. All components are in the range [0, 1] and R, B, and G are premultiplied by A. + * @private + */ +class Color { + r: number; + g: number; + b: number; + a: number; + + constructor(r: number, g: number, b: number, a: number = 1) { + this.r = r; + this.g = g; + this.b = b; + this.a = a; + } + + static parse(input: ?string): Color | void { + if (!input) { + return undefined; + } + + if (input instanceof Color) { + return input; + } + + if (typeof input !== 'string') { + return undefined; + } + + const rgba = parseCSSColor(input); + if (!rgba) { + return undefined; + } + + return new Color( + rgba[0] / 255 * rgba[3], + rgba[1] / 255 * rgba[3], + rgba[2] / 255 * rgba[3], + rgba[3] + ); + } +} + +module.exports = Color; diff --git a/src/style-spec/util/color_spaces.js b/src/style-spec/util/color_spaces.js index fcc69a29d6d..17ac271cbb7 100644 --- a/src/style-spec/util/color_spaces.js +++ b/src/style-spec/util/color_spaces.js @@ -1,3 +1,20 @@ +// @flow + +const Color = require('./color'); + +type LABColor = { + l: number, + a: number, + b: number, + alpha: number +}; + +type HCLColor = { + h: number, + c: number, + l: number, + alpha: number +}; // Constants const Xn = 0.950470, // D65 standard referent @@ -29,62 +46,59 @@ function rgb2xyz(x) { } // LAB -function rgbToLab(rgbColor) { - const b = rgb2xyz(rgbColor[0]), - a = rgb2xyz(rgbColor[1]), - l = rgb2xyz(rgbColor[2]), +function rgbToLab(rgbColor: Color): LABColor { + const b = rgb2xyz(rgbColor.r), + a = rgb2xyz(rgbColor.g), + l = rgb2xyz(rgbColor.b), x = xyz2lab((0.4124564 * b + 0.3575761 * a + 0.1804375 * l) / Xn), y = xyz2lab((0.2126729 * b + 0.7151522 * a + 0.0721750 * l) / Yn), z = xyz2lab((0.0193339 * b + 0.1191920 * a + 0.9503041 * l) / Zn); - return [ - 116 * y - 16, - 500 * (x - y), - 200 * (y - z), - rgbColor[3] - ]; + return { + l: 116 * y - 16, + a: 500 * (x - y), + b: 200 * (y - z), + alpha: rgbColor.a + }; } -function labToRgb(labColor) { - let y = (labColor[0] + 16) / 116, - x = isNaN(labColor[1]) ? y : y + labColor[1] / 500, - z = isNaN(labColor[2]) ? y : y - labColor[2] / 200; +function labToRgb(labColor: LABColor): Color { + let y = (labColor.l + 16) / 116, + x = isNaN(labColor.a) ? y : y + labColor.a / 500, + z = isNaN(labColor.b) ? y : y - labColor.b / 200; y = Yn * lab2xyz(y); x = Xn * lab2xyz(x); z = Zn * lab2xyz(z); - return [ + return new Color( xyz2rgb(3.2404542 * x - 1.5371385 * y - 0.4985314 * z), // D65 -> sRGB xyz2rgb(-0.9692660 * x + 1.8760108 * y + 0.0415560 * z), xyz2rgb(0.0556434 * x - 0.2040259 * y + 1.0572252 * z), - labColor[3] - ]; + labColor.alpha + ); } // HCL -function rgbToHcl(rgbColor) { - const labColor = rgbToLab(rgbColor); - const l = labColor[0], - a = labColor[1], - b = labColor[2]; +function rgbToHcl(rgbColor: Color): HCLColor { + const {l, a, b} = rgbToLab(rgbColor); const h = Math.atan2(b, a) * rad2deg; - return [ - h < 0 ? h + 360 : h, - Math.sqrt(a * a + b * b), - l, - rgbColor[3] - ]; + return { + h: h < 0 ? h + 360 : h, + c: Math.sqrt(a * a + b * b), + l: l, + alpha: rgbColor.a + }; } -function hclToRgb(hclColor) { - const h = hclColor[0] * deg2rad, - c = hclColor[1], - l = hclColor[2]; - return labToRgb([ - l, - Math.cos(h) * c, - Math.sin(h) * c, - hclColor[3] - ]); +function hclToRgb(hclColor: HCLColor): Color { + const h = hclColor.h * deg2rad, + c = hclColor.c, + l = hclColor.l; + return labToRgb({ + l: l, + a: Math.cos(h) * c, + b: Math.sin(h) * c, + alpha: hclColor.alpha + }); } module.exports = { diff --git a/src/style-spec/util/interpolate.js b/src/style-spec/util/interpolate.js index 62261a1ab07..38172c28748 100644 --- a/src/style-spec/util/interpolate.js +++ b/src/style-spec/util/interpolate.js @@ -1,37 +1,32 @@ +// @flow + +const Color = require('./color'); module.exports = interpolate; -function interpolate(a, b, t) { +function interpolate(a: number, b: number, t: number) { return (a * (1 - t)) + (b * t); } interpolate.number = interpolate; -interpolate.vec2 = function(from, to, t) { +interpolate.vec2 = function(from: [number, number], to: [number, number], t: number) { return [ interpolate(from[0], to[0], t), interpolate(from[1], to[1], t) ]; }; -/* - * Interpolate between two colors given as 4-element arrays. - * - * @param {Color} from - * @param {Color} to - * @param {number} t interpolation factor between 0 and 1 - * @returns {Color} interpolated color - */ -interpolate.color = function(from, to, t) { - return [ - interpolate(from[0], to[0], t), - interpolate(from[1], to[1], t), - interpolate(from[2], to[2], t), - interpolate(from[3], to[3], t) - ]; +interpolate.color = function(from: Color, to: Color, t: number) { + return new Color( + interpolate(from.r, to.r, t), + interpolate(from.g, to.g, t), + interpolate(from.b, to.b, t), + interpolate(from.a, to.a, t) + ); }; -interpolate.array = function(from, to, t) { +interpolate.array = function(from: Array, to: Array, t: number) { return from.map((d, i) => { return interpolate(d, to[i], t); }); diff --git a/src/style-spec/util/parse_color.js b/src/style-spec/util/parse_color.js deleted file mode 100644 index ad27a314af2..00000000000 --- a/src/style-spec/util/parse_color.js +++ /dev/null @@ -1,25 +0,0 @@ -// @flow - -const parseColorString = require('csscolorparser').parseCSSColor; - -module.exports = function parseColor(input: string | [number, number, number, number]): ?[number, number, number, number] { - if (typeof input === 'string') { - const rgba = parseColorString(input); - if (!rgba) { return undefined; } - - // GL expects all components to be in the range [0, 1] and to be - // multipled by the alpha value. - return [ - rgba[0] / 255 * rgba[3], - rgba[1] / 255 * rgba[3], - rgba[2] / 255 * rgba[3], - rgba[3] - ]; - - } else if (Array.isArray(input)) { - return input; - - } else { - return undefined; - } -}; diff --git a/src/style/style_declaration.js b/src/style/style_declaration.js index fc5c0072522..555435b188f 100644 --- a/src/style/style_declaration.js +++ b/src/style/style_declaration.js @@ -1,6 +1,6 @@ // @flow -const parseColor = require('../style-spec/util/parse_color'); +const Color = require('../style-spec/util/color'); const {isFunction, createFunction} = require('../style-spec/function'); const {isExpression, createExpression} = require('../style-spec/expression'); const util = require('../util/util'); @@ -24,7 +24,7 @@ function normalizeToExpression(parameters, propertySpec, name): StyleDeclaration } } else { if (typeof parameters === 'string' && propertySpec.type === 'color') { - parameters = parseColor(parameters); + parameters = Color.parse(parameters); } return { diff --git a/src/style/style_layer.js b/src/style/style_layer.js index 252fb08f809..e465857fa0e 100644 --- a/src/style/style_layer.js +++ b/src/style/style_layer.js @@ -5,7 +5,7 @@ const StyleTransition = require('./style_transition'); const StyleDeclaration = require('./style_declaration'); const styleSpec = require('../style-spec/reference/latest'); const validateStyle = require('./validate_style'); -const parseColor = require('./../style-spec/util/parse_color'); +const Color = require('./../style-spec/util/color'); const Evented = require('../util/evented'); import type {Bucket, BucketParameters} from '../data/bucket'; @@ -171,7 +171,7 @@ class StyleLayer extends Evented { if (transition && (transition.declaration.expression.isFeatureConstant || feature)) { return transition.calculate(globals, feature); } else if (specification.type === 'color' && specification.default) { - return parseColor(specification.default); + return Color.parse(specification.default); } else { return specification.default; } diff --git a/src/style/style_layer/fill_extrusion_style_layer.js b/src/style/style_layer/fill_extrusion_style_layer.js index acf9d63ab09..a216f5fdf35 100644 --- a/src/style/style_layer/fill_extrusion_style_layer.js +++ b/src/style/style_layer/fill_extrusion_style_layer.js @@ -14,7 +14,7 @@ class FillExtrusionStyleLayer extends StyleLayer { getPaintValue(name: string, globals: GlobalProperties, feature?: Feature) { const value = super.getPaintValue(name, globals, feature); if (name === 'fill-extrusion-color' && value) { - value[3] = 1; + value.a = 1; } return value; } diff --git a/src/style/style_layer/heatmap_style_layer.js b/src/style/style_layer/heatmap_style_layer.js index ee85e226555..a3a4220269b 100644 --- a/src/style/style_layer/heatmap_style_layer.js +++ b/src/style/style_layer/heatmap_style_layer.js @@ -5,6 +5,7 @@ const HeatmapBucket = require('../../data/bucket/heatmap_bucket'); const RGBAImage = require('../../util/image').RGBAImage; import type Texture from '../../render/texture'; +import type Color from '../../style-spec/util/color'; class HeatmapStyleLayer extends StyleLayer { @@ -40,14 +41,13 @@ class HeatmapStyleLayer extends StyleLayer { if (name === 'heatmap-color') { const len = this.colorRampData.length; for (let i = 4; i < len; i += 4) { - const pxColor = this.getPaintValue('heatmap-color', {heatmapDensity: i / len, zoom: -1}); - const alpha = pxColor[3]; + const pxColor: Color = this.getPaintValue('heatmap-color', {heatmapDensity: i / len, zoom: -1}); // the colors are being unpremultiplied because getPaintValue returns // premultiplied values, and the Texture class expects unpremultiplied ones - this.colorRampData[i + 0] = Math.floor(pxColor[0] * 255 / alpha); - this.colorRampData[i + 1] = Math.floor(pxColor[1] * 255 / alpha); - this.colorRampData[i + 2] = Math.floor(pxColor[2] * 255 / alpha); - this.colorRampData[i + 3] = Math.floor(alpha * 255); + this.colorRampData[i + 0] = Math.floor(pxColor.r * 255 / pxColor.a); + this.colorRampData[i + 1] = Math.floor(pxColor.g * 255 / pxColor.a); + this.colorRampData[i + 2] = Math.floor(pxColor.b * 255 / pxColor.a); + this.colorRampData[i + 3] = Math.floor(pxColor.a * 255); } this.colorRamp = RGBAImage.create({width: 256, height: 1}, this.colorRampData); this.colorRampTexture = null; diff --git a/test/expression.test.js b/test/expression.test.js index 38dba142656..b9a0dc4470d 100644 --- a/test/expression.test.js +++ b/test/expression.test.js @@ -4,7 +4,6 @@ require('flow-remove-types/register'); const expressionSuite = require('./integration').expression; const { createExpression } = require('../src/style-spec/expression'); const { toString } = require('../src/style-spec/expression/types'); -const { unwrap } = require('../src/style-spec/expression/values'); let tests; @@ -50,7 +49,11 @@ expressionSuite.run('js', {tests: tests}, (fixture) => { if ('geometry' in input[1]) { feature.type = input[1].geometry.type; } - outputs.push(unwrap(expression.evaluate(input[0], feature))); + let value = expression.evaluate(input[0], feature); + if (expression.parsed.type.kind === 'color') { + value = [value.r, value.g, value.b, value.a]; + } + outputs.push(value); } catch (error) { if (error.name === 'ExpressionEvaluationError') { outputs.push({ error: error.toJSON() }); diff --git a/test/unit/style-spec/color.js b/test/unit/style-spec/color.js new file mode 100644 index 00000000000..64e0ca55603 --- /dev/null +++ b/test/unit/style-spec/color.js @@ -0,0 +1,15 @@ +// @flow + +'use strict'; + +const test = require('mapbox-gl-js-test').test; +const Color = require('../../../src/style-spec/util/color'); + +test('Color.parse', (t) => { + t.deepEqual(Color.parse('red'), new Color(1, 0, 0, 1)); + t.deepEqual(Color.parse('#ff00ff'), new Color(1, 0, 1, 1)); + t.deepEqual(Color.parse('invalid'), undefined); + t.deepEqual(Color.parse(null), undefined); + t.deepEqual(Color.parse(undefined), undefined); + t.end(); +}); diff --git a/test/unit/style-spec/function.test.js b/test/unit/style-spec/function.test.js index d2120565014..44b5d7aef4b 100644 --- a/test/unit/style-spec/function.test.js +++ b/test/unit/style-spec/function.test.js @@ -2,6 +2,7 @@ const test = require('mapbox-gl-js-test').test; const {createFunction} = require('../../../src/style-spec/function'); +const Color = require('../../../src/style-spec/util/color'); test('binary search', (t) => { t.test('will eventually terminate.', (t) => { @@ -176,9 +177,9 @@ test('exponential function', (t) => { type: 'color' }).evaluate; - t.deepEqual(f({zoom: 0}), [1, 0, 0, 1]); - t.deepEqual(f({zoom: 5}), [0.6, 0, 0.4, 1]); - t.deepEqual(f({zoom: 11}), [0, 0, 1, 1]); + t.deepEqual(f({zoom: 0}), new Color(1, 0, 0, 1)); + t.deepEqual(f({zoom: 5}), new Color(0.6, 0, 0.4, 1)); + t.deepEqual(f({zoom: 11}), new Color(0, 0, 1, 1)); t.end(); }); @@ -192,10 +193,10 @@ test('exponential function', (t) => { type: 'color' }).evaluate; - t.deepEqual(f({zoom: 0}), [0, 0, 0, 1]); + t.deepEqual(f({zoom: 0}), new Color(0, 0, 0, 1)); t.deepEqual(f({zoom: 5}).map((n) => { return parseFloat(n.toFixed(3)); - }), [0, 0.444, 0.444, 1]); + }), new Color(0, 0.444, 0.444, 1)); t.end(); }); @@ -211,7 +212,7 @@ test('exponential function', (t) => { t.deepEqual(f({zoom: 5}).map((n) => { return parseFloat(n.toFixed(3)); - }), [0.5, 0.5, 0.5, 1]); + }), new Color(0.5, 0.5, 0.5, 1)); t.end(); }); @@ -554,9 +555,9 @@ test('interval function', (t) => { type: 'color' }).evaluate; - t.deepEqual(f({zoom: 0}), [1, 0, 0, 1]); - t.deepEqual(f({zoom: 0}), [1, 0, 0, 1]); - t.deepEqual(f({zoom: 11}), [0, 0, 1, 1]); + t.deepEqual(f({zoom: 0}), new Color(1, 0, 0, 1)); + t.deepEqual(f({zoom: 0}), new Color(1, 0, 0, 1)); + t.deepEqual(f({zoom: 11}), new Color(0, 0, 1, 1)); t.end(); }); @@ -747,8 +748,8 @@ test('categorical function', (t) => { type: 'color' }).evaluate; - t.deepEqual(f({zoom: 0}, {properties: {foo: 0}}), [1, 0, 0, 1]); - t.deepEqual(f({zoom: 1}, {properties: {foo: 1}}), [0, 0, 1, 1]); + t.deepEqual(f({zoom: 0}, {properties: {foo: 0}}), new Color(1, 0, 0, 1)); + t.deepEqual(f({zoom: 1}, {properties: {foo: 1}}), new Color(0, 0, 1, 1)); t.end(); }); @@ -763,8 +764,8 @@ test('categorical function', (t) => { type: 'color' }).evaluate; - t.deepEqual(f({zoom: 0}, {properties: {}}), [0, 1, 0, 1]); - t.deepEqual(f({zoom: 0}, {properties: {foo: 3}}), [0, 1, 0, 1]); + t.deepEqual(f({zoom: 0}, {properties: {}}), new Color(0, 1, 0, 1)); + t.deepEqual(f({zoom: 0}, {properties: {foo: 3}}), new Color(0, 1, 0, 1)); t.end(); }); @@ -779,8 +780,8 @@ test('categorical function', (t) => { default: 'lime' }).evaluate; - t.deepEqual(f({zoom: 0}, {properties: {}}), [0, 1, 0, 1]); - t.deepEqual(f({zoom: 0}, {properties: {foo: 3}}), [0, 1, 0, 1]); + t.deepEqual(f({zoom: 0}, {properties: {}}), new Color(0, 1, 0, 1)); + t.deepEqual(f({zoom: 0}, {properties: {foo: 3}}), new Color(0, 1, 0, 1)); t.end(); }); @@ -853,8 +854,8 @@ test('identity function', (t) => { type: 'color' }).evaluate; - t.deepEqual(f({zoom: 0}, {properties: {foo: 'red'}}), [1, 0, 0, 1]); - t.deepEqual(f({zoom: 1}, {properties: {foo: 'blue'}}), [0, 0, 1, 1]); + t.deepEqual(f({zoom: 0}, {properties: {foo: 'red'}}), new Color(1, 0, 0, 1)); + t.deepEqual(f({zoom: 1}, {properties: {foo: 'blue'}}), new Color(0, 0, 1, 1)); t.end(); }); @@ -868,7 +869,7 @@ test('identity function', (t) => { type: 'color' }).evaluate; - t.deepEqual(f({zoom: 0}, {properties: {}}), [1, 0, 0, 1]); + t.deepEqual(f({zoom: 0}, {properties: {}}), new Color(1, 0, 0, 1)); t.end(); }); @@ -882,7 +883,7 @@ test('identity function', (t) => { default: 'red' }).evaluate; - t.deepEqual(f({zoom: 0}, {properties: {}}), [1, 0, 0, 1]); + t.deepEqual(f({zoom: 0}, {properties: {}}), new Color(1, 0, 0, 1)); t.end(); }); @@ -896,7 +897,7 @@ test('identity function', (t) => { default: 'red' }).evaluate; - t.deepEqual(f({zoom: 0}, {properties: {foo: 'invalid'}}), [1, 0, 0, 1]); + t.deepEqual(f({zoom: 0}, {properties: {foo: 'invalid'}}), new Color(1, 0, 0, 1)); t.end(); }); diff --git a/test/unit/style-spec/interpolate.test.js b/test/unit/style-spec/interpolate.test.js index 85dd80b3533..28d7b4d40f5 100644 --- a/test/unit/style-spec/interpolate.test.js +++ b/test/unit/style-spec/interpolate.test.js @@ -2,6 +2,7 @@ const test = require('mapbox-gl-js-test').test; const interpolate = require('../../../src/style-spec/util/interpolate'); +const Color = require('../../../src/style-spec/util/color'); test('interpolate.number', (t) => { t.equal(interpolate(0, 1, 0.5), 0.5); @@ -19,7 +20,7 @@ test('interpolate.vec2', (t) => { }); test('interpolate.color', (t) => { - t.deepEqual(interpolate.color([0, 0, 0, 0], [1, 2, 3, 4], 0.5), [0.5, 1, 3 / 2, 2]); + t.deepEqual(interpolate.color(new Color(0, 0, 0, 0), new Color(1, 2, 3, 4), 0.5), new Color(0.5, 1, 3 / 2, 2)); t.end(); }); diff --git a/test/unit/style-spec/parse_color.js b/test/unit/style-spec/parse_color.js deleted file mode 100644 index c52d4ac4553..00000000000 --- a/test/unit/style-spec/parse_color.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -const test = require('mapbox-gl-js-test').test; -const parseColor = require('../../../src/style-spec/util/parse_color'); - -test('parseColor', (t) => { - t.deepEqual(parseColor('red'), [ 1, 0, 0, 1 ]); - t.deepEqual(parseColor('#ff00ff'), [ 1, 0, 1, 1 ]); - t.deepEqual(parseColor([ 1, 0, 0, 1 ]), [ 1, 0, 0, 1 ]); - t.deepEqual(parseColor(null), undefined); - t.deepEqual(parseColor('#00000'), undefined); - t.end(); -}); diff --git a/test/unit/style-spec/util/color_spaces.test.js b/test/unit/style-spec/util/color_spaces.test.js index 5fbc350dbb6..fc82148faaf 100644 --- a/test/unit/style-spec/util/color_spaces.test.js +++ b/test/unit/style-spec/util/color_spaces.test.js @@ -2,31 +2,32 @@ const test = require('mapbox-gl-js-test').test; const colorSpaces = require('../../../../src/style-spec/util/color_spaces'); +const Color = require('../../../../src/style-spec/util/color'); test('#hclToRgb zero', (t) => { - const hclColor = [0, 0, 0, null]; - const rgbColor = [0, 0, 0, null]; + const hclColor = {h: 0, c: 0, l: 0, alpha: null}; + const rgbColor = new Color(0, 0, 0, null); t.deepEqual(colorSpaces.hcl.reverse(hclColor), rgbColor); t.end(); }); test('#hclToRgb', (t) => { - const hclColor = [20, 20, 20, null]; - const rgbColor = [76.04087881379073, 36.681898967046166, 39.08743507357837, null]; + const hclColor = {h: 20, c: 20, l: 20, alpha: null}; + const rgbColor = new Color(76.04087881379073, 36.681898967046166, 39.08743507357837, null); t.deepEqual(colorSpaces.hcl.reverse(hclColor), rgbColor); t.end(); }); test('#rgbToHcl zero', (t) => { - const hclColor = [0, 0, 0, null]; - const rgbColor = [0, 0, 0, null]; + const hclColor = {h: 0, c: 0, l: 0, alpha: null}; + const rgbColor = new Color(0, 0, 0, null); t.deepEqual(colorSpaces.hcl.forward(rgbColor), hclColor); t.end(); }); test('#rgbToHcl', (t) => { - const hclColor = [158.19859051364818, 0.0000029334887311101764, 6.318928745123017, null]; - const rgbColor = [20, 20, 20, null]; + const hclColor = {h: 158.19859051364818, c: 0.0000029334887311101764, l: 6.318928745123017, alpha: null}; + const rgbColor = new Color(20, 20, 20, null); t.deepEqual(colorSpaces.hcl.forward(rgbColor), hclColor); t.end(); }); diff --git a/test/unit/style/light.test.js b/test/unit/style/light.test.js index d119e56c183..1e0efc45706 100644 --- a/test/unit/style/light.test.js +++ b/test/unit/style/light.test.js @@ -3,6 +3,7 @@ const test = require('mapbox-gl-js-test').test; const Light = require('../../../src/style/light'); const spec = require('../../../src/style-spec/reference/latest').light; +const Color = require('../../../src/style-spec/util/color'); test('Light', (t) => { t.test('creates default light with no options', (t) => { @@ -83,7 +84,7 @@ test('Light#getLightValue', (t) => { light.updateLightTransitions({ transition: true }, null, createAnimationLoop()); t.equal(light.getLightValue('intensity', { zoom: 16.5 }), 0.5); - t.deepEqual(light.getLightValue('color', { zoom: 16.5 }), [1, 0, 0, 1]); + t.deepEqual(light.getLightValue('color', { zoom: 16.5 }), new Color(1, 0, 0, 1)); t.deepEqual(light.getLightValue('position', { zoom: 16.5 }), { x: 0.2875, y: -0.4979646071760521, z: 0.9959292143521045 }); t.end(); @@ -94,7 +95,7 @@ test('Light#setLight', (t) => { light.setLight({ color: 'red', "color-transition": { duration: 3000 }}); light.updateLightTransitions({ transition: true }, null, createAnimationLoop()); - t.deepEqual(light.getLightValue('color', { zoom: 16 }), [1, 0, 0, 1]); + t.deepEqual(light.getLightValue('color', { zoom: 16 }), new Color(1, 0, 0, 1)); t.end(); }); diff --git a/test/unit/style/style.test.js b/test/unit/style/style.test.js index 9f6fca50d94..1c9b043e77d 100644 --- a/test/unit/style/style.test.js +++ b/test/unit/style/style.test.js @@ -12,6 +12,7 @@ const window = require('../../../src/util/window'); const rtlTextPlugin = require('../../../src/source/rtl_text_plugin'); const ajax = require('../../../src/util/ajax'); const browser = require('../../../src/util/browser'); +const Color = require('../../../src/style-spec/util/color'); function createStyleJSON(properties) { return util.extend({ @@ -1558,7 +1559,7 @@ test('Style#queryRenderedFeatures', (t) => { t.test('includes paint properties', (t) => { const results = style.queryRenderedFeatures([{column: 1, row: 1, zoom: 1}], {}, 0, 0); - t.deepEqual(results[2].layer.paint['line-color'], [1, 0, 0, 1]); + t.deepEqual(results[2].layer.paint['line-color'], new Color(1, 0, 0, 1)); t.end(); }); diff --git a/test/unit/style/style_declaration.test.js b/test/unit/style/style_declaration.test.js index 755c3533309..012bd4448ca 100644 --- a/test/unit/style/style_declaration.test.js +++ b/test/unit/style/style_declaration.test.js @@ -2,6 +2,7 @@ const test = require('mapbox-gl-js-test').test; const StyleDeclaration = require('../../../src/style/style_declaration'); +const Color = require('../../../src/style-spec/util/color'); test('StyleDeclaration', (t) => { t.test('with minimum value', (t) => { @@ -42,9 +43,9 @@ test('StyleDeclaration', (t) => { t.ok(d.expression.isZoomConstant); t.ok(d.expression.isFeatureConstant); - t.deepEqual(d.calculate({zoom: 0}), [1, 0, 0, 1]); - t.deepEqual(d.calculate({zoom: 1}), [1, 0, 0, 1]); - t.deepEqual(d.calculate({zoom: 2}), [1, 0, 0, 1]); + t.deepEqual(d.calculate({zoom: 0}), new Color(1, 0, 0, 1)); + t.deepEqual(d.calculate({zoom: 1}), new Color(1, 0, 0, 1)); + t.deepEqual(d.calculate({zoom: 2}), new Color(1, 0, 0, 1)); t.end(); }); diff --git a/test/unit/style/style_layer.test.js b/test/unit/style/style_layer.test.js index 723e196f1d5..302eda68a11 100644 --- a/test/unit/style/style_layer.test.js +++ b/test/unit/style/style_layer.test.js @@ -4,6 +4,7 @@ const test = require('mapbox-gl-js-test').test; const StyleLayer = require('../../../src/style/style_layer'); const FillStyleLayer = require('../../../src/style/style_layer/fill_style_layer'); const util = require('../../../src/util/util'); +const Color = require('../../../src/style-spec/util/color'); test('StyleLayer', (t) => { t.test('instantiates the correct subclass', (t) => { @@ -27,7 +28,7 @@ test('StyleLayer#updatePaintTransition', (t) => { } }); layer.updatePaintTransition('background-color', [], {}); - t.deepEqual(layer.getPaintValue('background-color'), [1, 0, 0, 1]); + t.deepEqual(layer.getPaintValue('background-color'), new Color(1, 0, 0, 1)); t.end(); }); @@ -75,7 +76,7 @@ test('StyleLayer#setPaintProperty', (t) => { layer.setPaintProperty('background-color', null); layer.updatePaintTransitions([], {transition: false}, null, createAnimationLoop()); - t.deepEqual(layer.getPaintValue('background-color'), [0, 0, 0, 1]); + t.deepEqual(layer.getPaintValue('background-color'), new Color(0, 0, 0, 1)); t.equal(layer.getPaintProperty('background-color'), undefined); t.equal(layer.getPaintValue('background-opacity'), 1); t.equal(layer.getPaintProperty('background-opacity'), 1); @@ -158,10 +159,10 @@ test('StyleLayer#setPaintProperty', (t) => { layer.setPaintProperty('fill-outline-color', '#f00'); layer.updatePaintTransitions([], {transition: false}, null, createAnimationLoop()); - t.deepEqual(layer.getPaintValue('fill-outline-color'), [1, 0, 0, 1]); + t.deepEqual(layer.getPaintValue('fill-outline-color'), new Color(1, 0, 0, 1)); layer.setPaintProperty('fill-outline-color', undefined); layer.updatePaintTransitions([], {transition: false}, null, createAnimationLoop()); - t.deepEqual(layer.getPaintValue('fill-outline-color'), [0, 0, 1, 1]); + t.deepEqual(layer.getPaintValue('fill-outline-color'), new Color(0, 0, 1, 1)); t.end(); });