Skip to content

Commit 8cb30d3

Browse files
authored
Move types, clean imports, move interpolate method (#903)
1 parent 4ebf797 commit 8cb30d3

27 files changed

+306
-318
lines changed

src/expression/compound_expression.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@ import type {Expression, ExpressionRegistry} from './expression';
2727
import type {Value} from './values';
2828
import type {Type} from './types';
2929

30-
import {typeOf, Color, validateRGBA, toString as valueToString} from './values';
30+
import {typeOf, validateRGBA, toString as valueToString} from './values';
3131
import RuntimeError from './runtime_error';
32+
import Color from './types/color';
3233

3334
export type Varargs = {
3435
type: Type;

src/expression/definitions/coercion.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import {BooleanType, ColorType, NumberType, StringType, ValueType} from '../types';
2-
import {Color, Padding, VariableAnchorOffsetCollection, toString as valueToString, validateRGBA} from '../values';
2+
import {toString as valueToString, validateRGBA} from '../values';
33
import RuntimeError from '../runtime_error';
44
import Formatted from '../types/formatted';
55
import ResolvedImage from '../types/resolved_image';
6+
import Color from '../types/color';
7+
import Padding from '../types/padding';
8+
import VariableAnchorOffsetCollection from '../types/variable_anchor_offset_collection';
69

710
import type {Expression} from '../expression';
811
import type ParsingContext from '../parsing_context';

src/expression/definitions/interpolate.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import UnitBezier from '@mapbox/unitbezier';
22

3-
import interpolate from '../../util/interpolate';
43
import {array, ArrayType, ColorType, ColorTypeT, NumberType, NumberTypeT, PaddingType, PaddingTypeT, VariableAnchorOffsetCollectionType, VariableAnchorOffsetCollectionTypeT, toString, verifyType} from '../types';
54
import {findStopLessThanOrEqualTo} from '../stops';
65

@@ -9,6 +8,10 @@ import type {Expression} from '../expression';
98
import type ParsingContext from '../parsing_context';
109
import type EvaluationContext from '../evaluation_context';
1110
import type {Type} from '../types';
11+
import Color from '../types/color';
12+
import {interpolateArray, interpolateNumber} from '../../util/interpolate-primitives';
13+
import Padding from '../types/padding';
14+
import VariableAnchorOffsetCollection from '../types/variable_anchor_offset_collection';
1215

1316
export type InterpolationType = {
1417
name: 'linear';
@@ -173,11 +176,22 @@ class Interpolate implements Expression {
173176

174177
switch (this.operator) {
175178
case 'interpolate':
176-
return interpolate[this.type.kind](outputLower, outputUpper, t);
179+
switch (this.type.kind) {
180+
case 'number':
181+
return interpolateNumber(outputLower, outputUpper, t)
182+
case 'color':
183+
return Color.interpolate(outputLower, outputUpper, t);
184+
case 'padding':
185+
return Padding.interpolate(outputLower, outputUpper, t);
186+
case 'variableAnchorOffsetCollection':
187+
return VariableAnchorOffsetCollection.interpolate(outputLower, outputUpper, t);
188+
case 'array':
189+
return interpolateArray(outputLower, outputUpper, t);
190+
}
177191
case 'interpolate-hcl':
178-
return interpolate.color(outputLower, outputUpper, t, 'hcl');
192+
return Color.interpolate(outputLower, outputUpper, t, 'hcl');
179193
case 'interpolate-lab':
180-
return interpolate.color(outputLower, outputUpper, t, 'lab');
194+
return Color.interpolate(outputLower, outputUpper, t, 'lab');
181195
}
182196
}
183197

@@ -242,3 +256,11 @@ function exponentialInterpolation(input, base, lowerValue, upperValue) {
242256
}
243257

244258
export default Interpolate;
259+
260+
export const interpolateFactory = {
261+
color: Color.interpolate,
262+
number: interpolateNumber,
263+
padding: Padding.interpolate,
264+
variableAnchorOffsetCollection: VariableAnchorOffsetCollection.interpolate,
265+
array: interpolateArray
266+
}

src/expression/evaluation_context.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import {Color} from './values';
21
import type {FormattedSection} from './types/formatted';
32
import type {GlobalProperties, Feature, FeatureState} from './index';
43
import {ICanonicalTileID} from '../tiles_and_coordinates';
54
import {hasMultipleOuterRings} from '../util/classify_rings';
5+
import Color from './types/color';
66

77
const geometryTypes = ['Unknown', 'Point', 'LineString', 'Polygon'];
88
const simpleGeometryType = {
@@ -84,7 +84,7 @@ class EvaluationContext {
8484
parseColor(input: string): Color {
8585
let cached = this._parseColorCache[input];
8686
if (!cached) {
87-
cached = this._parseColorCache[input] = Color.parse(input) as Color;
87+
cached = this._parseColorCache[input] = Color.parse(input);
8888
}
8989
return cached;
9090
}

src/expression/expression.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import definitions from './definitions';
33
import v8 from '../reference/v8.json' with {type: 'json'};
44
import {createExpression, ICanonicalTileID, StyleExpression, StylePropertySpecification} from '..';
55
import ParsingError from './parsing_error';
6-
import {VariableAnchorOffsetCollection} from './values';
76
import {getGeometry} from '../../test/lib/geometry';
7+
import VariableAnchorOffsetCollection from './types/variable_anchor_offset_collection';
88

99
// filter out internal "error" and "filter-*" expressions from definition list
1010
const filterExpressionRegex = /filter-/;

src/expression/index.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,22 @@ import RuntimeError from './runtime_error';
2020
import {success, error} from '../util/result';
2121
import {supportsPropertyExpression, supportsZoomExpression, supportsInterpolation} from '../util/properties';
2222

23-
import type {Type, EvaluationKind} from './types';
23+
import {ColorType, StringType, NumberType, BooleanType, ValueType, FormattedType, PaddingType, ResolvedImageType, VariableAnchorOffsetCollectionType, array, type Type, type EvaluationKind} from './types';
2424
import type {Value} from './values';
2525
import type {Expression} from './expression';
2626
import type {StylePropertySpecification} from '..';
2727
import type {Result} from '../util/result';
2828
import type {InterpolationType} from './definitions/interpolate';
29-
import type {PropertyValueSpecification, VariableAnchorOffsetCollectionSpecification} from '../types.g';
29+
import type {PaddingSpecification, PropertyValueSpecification, VariableAnchorOffsetCollectionSpecification} from '../types.g';
3030
import type {FormattedSection} from './types/formatted';
3131
import type {Point2D} from '../point2d';
3232

33+
import {ICanonicalTileID} from '../tiles_and_coordinates';
34+
import {isFunction, createFunction} from '../function';
35+
import Color from './types/color';
36+
import Padding from './types/padding';
37+
import VariableAnchorOffsetCollection from './types/variable_anchor_offset_collection';
38+
3339
export type Feature = {
3440
readonly type: 0 | 1 | 2 | 3 | 'Unknown' | 'Point' | 'MultiPoint' | 'LineString' | 'MultiLineString' | 'Polygon' | 'MultiPolygon';
3541
readonly id?: any;
@@ -333,9 +339,6 @@ export function createPropertyExpression(expressionInput: unknown, propertySpec:
333339
(new ZoomDependentExpression('composite', expression.value, zoomCurve.labels, interpolationType) as CompositeExpression));
334340
}
335341

336-
import {isFunction, createFunction} from '../function';
337-
import {Color, VariableAnchorOffsetCollection} from './values';
338-
339342
// serialization wrapper for old-style stop functions normalized to the
340343
// expression interface
341344
export class StylePropertyFunction<T> {
@@ -388,7 +391,7 @@ export function normalizePropertyExpression<T>(
388391
if (specification.type === 'color' && typeof value === 'string') {
389392
constant = Color.parse(value);
390393
} else if (specification.type === 'padding' && (typeof value === 'number' || Array.isArray(value))) {
391-
constant = Padding.parse(value as (number | number[]));
394+
constant = Padding.parse(value as PaddingSpecification);
392395
} else if (specification.type === 'variableAnchorOffsetCollection' && Array.isArray(value)) {
393396
constant = VariableAnchorOffsetCollection.parse(value as VariableAnchorOffsetCollectionSpecification);
394397
}
@@ -440,10 +443,6 @@ function findZoomCurve(expression: Expression): Step | Interpolate | ExpressionP
440443
return result;
441444
}
442445

443-
import {ColorType, StringType, NumberType, BooleanType, ValueType, FormattedType, PaddingType, ResolvedImageType, VariableAnchorOffsetCollectionType, array} from './types';
444-
import Padding from '../util/padding';
445-
import {ICanonicalTileID} from '../tiles_and_coordinates';
446-
447446
function getExpectedType(spec: StylePropertySpecification): Type {
448447
const types = {
449448
color: ColorType,
Lines changed: 57 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,55 @@
1-
import {expectToMatchColor} from '../../test/lib/util';
2-
import interpolate, {isSupportedInterpolationColorSpace} from './interpolate';
3-
import Color from './color';
4-
import Padding from './padding';
5-
import VariableAnchorOffsetCollection from './variable_anchor_offset_collection';
6-
7-
describe('interpolate', () => {
8-
9-
test('interpolate number', () => {
10-
expect(interpolate.number(-5, 5, 0.00)).toBe(-5.0);
11-
expect(interpolate.number(-5, 5, 0.25)).toBe(-2.5);
12-
expect(interpolate.number(-5, 5, 0.50)).toBe(0);
13-
expect(interpolate.number(-5, 5, 0.75)).toBe(2.5);
14-
expect(interpolate.number(-5, 5, 1.00)).toBe(5.0);
15-
16-
expect(interpolate.number(0, 1, 0.5)).toBe(0.5);
17-
expect(interpolate.number(-10, -5, 0.5)).toBe(-7.5);
18-
expect(interpolate.number(5, 10, 0.5)).toBe(7.5);
1+
import {expectCloseToArray, expectToMatchColor} from '../../../test/lib/util';
2+
import Color, {isSupportedInterpolationColorSpace} from './color';
3+
4+
describe('Color class', () => {
5+
6+
describe('parsing', () => {
7+
8+
test('should parse valid css color strings', () => {
9+
expectToMatchColor(Color.parse('RED'), 'rgb(100% 0% 0% / 1)');
10+
expectToMatchColor(Color.parse('#f00C'), 'rgb(100% 0% 0% / .8)');
11+
expectToMatchColor(Color.parse('rgb(0 0 127.5 / 20%)'), 'rgb(0% 0% 50% / .2)');
12+
expectToMatchColor(Color.parse('hsl(300deg 100% 25.1% / 0.7)'), 'rgb(50.2% 0% 50.2% / .7)');
13+
});
14+
15+
test('should return undefined when provided with invalid CSS color string', () => {
16+
expect(Color.parse(undefined)).toBeUndefined();
17+
expect(Color.parse(null)).toBeUndefined();
18+
expect(Color.parse('#invalid')).toBeUndefined();
19+
expect(Color.parse('$123')).toBeUndefined();
20+
expect(Color.parse('0F91')).toBeUndefined();
21+
expect(Color.parse('rgb(#123)')).toBeUndefined();
22+
expect(Color.parse('hsl(0,0,0)')).toBeUndefined();
23+
expect(Color.parse('rgb(0deg,0,0)')).toBeUndefined();
24+
});
25+
26+
test('should accept instances of Color class', () => {
27+
const color = new Color(0, 0, 0, 0);
28+
expect(Color.parse(color)).toBe(color);
29+
});
30+
31+
});
32+
33+
test('should keep a reference to the original color when alpha=0', () => {
34+
const color = new Color(0, 0, 0.5, 0, false);
35+
expect(color).toMatchObject({r: 0, g: 0, b: 0, a: 0});
36+
expect(Object.hasOwn(color, 'rgb')).toBe(true);
37+
expectCloseToArray(color.rgb, [0, 0, 0.5, 0]);
38+
});
39+
40+
test('should not keep a reference to the original color when alpha!=0', () => {
41+
const color = new Color(0, 0, 0.5, 0.001, false);
42+
expect(color).toMatchObject({r: 0, g: 0, b: expect.closeTo(0.5 * 0.001, 5), a: 0.001});
43+
expect(Object.hasOwn(color, 'rgb')).toBe(false);
44+
});
45+
46+
test('should serialize to rgba format', () => {
47+
expect(`${new Color(1, 1, 0, 1, false)}`).toBe('rgba(255,255,0,1)');
48+
expect(`${new Color(0.2, 0, 1, 0.3, false)}`).toBe('rgba(51,0,255,0.3)');
49+
expect(`${new Color(1, 1, 0, 0, false)}`).toBe('rgba(255,255,0,0)');
50+
expect(`${Color.parse('purple')}`).toBe('rgba(128,0,128,1)');
51+
expect(`${Color.parse('rgba(26,207,26,.73)')}`).toBe('rgba(26,207,26,0.73)');
52+
expect(`${Color.parse('rgba(26,207,26,0)')}`).toBe('rgba(26,207,26,0)');
1953
});
2054

2155
describe('interpolation color space', () => {
@@ -44,7 +78,7 @@ describe('interpolate', () => {
4478
const color = Color.parse('rgba(0,0,255,1)');
4579
const targetColor = Color.parse('rgba(0,255,0,.6)');
4680

47-
const i11nFn = (t: number) => interpolate.color(color, targetColor, t, 'rgb');
81+
const i11nFn = (t: number) => Color.interpolate(color, targetColor, t, 'rgb');
4882
expectToMatchColor(i11nFn(0.00), 'rgb(0% 0% 100% / 1)');
4983
expectToMatchColor(i11nFn(0.25), 'rgb(0% 25% 75% / 0.9)');
5084
expectToMatchColor(i11nFn(0.50), 'rgb(0% 50% 50% / 0.8)');
@@ -56,7 +90,7 @@ describe('interpolate', () => {
5690
const color = Color.parse('rgba(0,0,255,1)');
5791
const targetColor = Color.parse('rgba(0,255,0,.6)');
5892

59-
const i11nFn = (t: number) => interpolate.color(color, targetColor, t, 'hcl');
93+
const i11nFn = (t: number) => Color.interpolate(color, targetColor, t, 'hcl');
6094
expectToMatchColor(i11nFn(0.00), 'rgb(0% 0% 100% / 1)');
6195
expectToMatchColor(i11nFn(0.25), 'rgb(0% 49.37% 100% / 0.9)', 4);
6296
expectToMatchColor(i11nFn(0.50), 'rgb(0% 70.44% 100% / 0.8)', 4);
@@ -68,7 +102,7 @@ describe('interpolate', () => {
68102
const color = Color.parse('rgba(0,0,255,1)');
69103
const targetColor = Color.parse('rgba(0,255,0,.6)');
70104

71-
const i11nFn = (t: number) => interpolate.color(color, targetColor, t, 'lab');
105+
const i11nFn = (t: number) => Color.interpolate(color, targetColor, t, 'lab');
72106
expectToMatchColor(i11nFn(0.00), 'rgb(0% 0% 100% / 1)');
73107
expectToMatchColor(i11nFn(0.25), 'rgb(39.64% 34.55% 83.36% / 0.9)', 4);
74108
expectToMatchColor(i11nFn(0.50), 'rgb(46.42% 56.82% 65.91% / 0.8)', 4);
@@ -80,7 +114,7 @@ describe('interpolate', () => {
80114
const color = Color.parse('rgba(0,0,255,0)');
81115
const targetColor = Color.parse('rgba(0,255,0,1)');
82116

83-
const i11nFn = (t: number) => interpolate.color(color, targetColor, t, 'rgb');
117+
const i11nFn = (t: number) => Color.interpolate(color, targetColor, t, 'rgb');
84118
expectToMatchColor(i11nFn(0.00), 'rgb(0% 0% 0% / 0)');
85119
expectToMatchColor(i11nFn(0.25), 'rgb(0% 25% 75% / 0.25)');
86120
expectToMatchColor(i11nFn(0.50), 'rgb(0% 50% 50% / 0.5)');
@@ -93,7 +127,7 @@ describe('interpolate', () => {
93127
const targetColor = Color.parse('cyan');
94128

95129
for (const space of ['rgb', 'hcl', 'lab'] as const) {
96-
const i11nFn = (t: number) => interpolate.color(color, targetColor, t, space);
130+
const i11nFn = (t: number) => Color.interpolate(color, targetColor, t, space);
97131
const colorInBetween = i11nFn(0.5);
98132
for (const key of ['r', 'g', 'b', 'a'] as const) {
99133
expect(colorInBetween[ key ]).toBeGreaterThanOrEqual(0);
@@ -104,31 +138,4 @@ describe('interpolate', () => {
104138

105139
});
106140

107-
test('interpolate array', () => {
108-
expect(interpolate.array([0, 0, 0, 0], [1, 2, 3, 4], 0.5)).toEqual([0.5, 1, 3 / 2, 2]);
109-
});
110-
111-
test('interpolate padding', () => {
112-
const padding = new Padding([0, 0, 0, 0]);
113-
const targetPadding = new Padding([1, 2, 6, 4]);
114-
115-
const i11nFn = (t: number) => interpolate.padding(padding, targetPadding, t);
116-
expect(i11nFn(0.5)).toBeInstanceOf(Padding);
117-
expect(i11nFn(0.5)).toEqual(new Padding([0.5, 1, 3, 2]));
118-
});
119-
120-
describe('interpolate variableAnchorOffsetCollection', () => {
121-
const i11nFn = interpolate.variableAnchorOffsetCollection;
122-
const parseFn = VariableAnchorOffsetCollection.parse;
123-
124-
test('should throw with mismatched endpoints', () => {
125-
expect(() => i11nFn(parseFn(['top', [0, 0]]), parseFn(['bottom', [1, 1]]), 0.5)).toThrow('Cannot interpolate values containing mismatched anchors. from[0]: top, to[0]: bottom');
126-
expect(() => i11nFn(parseFn(['top', [0, 0]]), parseFn(['top', [1, 1], 'bottom', [2, 2]]), 0.5)).toThrow('Cannot interpolate values of different length. from: ["top",[0,0]], to: ["top",[1,1],"bottom",[2,2]]');
127-
});
128-
129-
test('should interpolate offsets', () => {
130-
expect(i11nFn(parseFn(['top', [0, 0], 'bottom', [2, 2]]), parseFn(['top', [1, 1], 'bottom', [4, 4]]), 0.5).values).toEqual(['top', [0.5, 0.5], 'bottom', [3, 3]]);
131-
});
132-
});
133-
134141
});

src/util/color.ts renamed to src/expression/types/color.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
1-
import {HCLColor, LABColor, RGBColor, rgbToHcl, rgbToLab} from './color_spaces';
1+
import {HCLColor, hclToRgb, LABColor, labToRgb, RGBColor, rgbToHcl, rgbToLab} from './color_spaces';
22
import {parseCssColor} from './parse_css_color';
3+
import {interpolateArray, interpolateNumber} from '../../util/interpolate-primitives';
4+
5+
export type InterpolationColorSpace = 'rgb' | 'hcl' | 'lab';
6+
7+
/**
8+
* Checks whether the specified color space is one of the supported interpolation color spaces.
9+
*
10+
* @param colorSpace Color space key to verify.
11+
* @returns `true` if the specified color space is one of the supported
12+
* interpolation color spaces, `false` otherwise
13+
*/
14+
export function isSupportedInterpolationColorSpace(colorSpace: string): colorSpace is InterpolationColorSpace {
15+
return colorSpace === 'rgb' || colorSpace === 'hcl' || colorSpace === 'lab';
16+
}
317

418
/**
519
* Color representation used by WebGL.
@@ -144,6 +158,52 @@ class Color {
144158
return `rgba(${[r, g, b].map(n => Math.round(n * 255)).join(',')},${a})`;
145159
}
146160

161+
static interpolate(from: Color, to: Color, t: number, spaceKey: InterpolationColorSpace = 'rgb'): Color {
162+
switch (spaceKey) {
163+
case 'rgb': {
164+
const [r, g, b, alpha] = interpolateArray(from.rgb, to.rgb, t);
165+
return new Color(r, g, b, alpha, false);
166+
}
167+
case 'hcl': {
168+
const [hue0, chroma0, light0, alphaF] = from.hcl;
169+
const [hue1, chroma1, light1, alphaT] = to.hcl;
170+
171+
// https://github.com/gka/chroma.js/blob/cd1b3c0926c7a85cbdc3b1453b3a94006de91a92/src/interpolator/_hsx.js
172+
let hue, chroma;
173+
174+
if (!isNaN(hue0) && !isNaN(hue1)) {
175+
let dh = hue1 - hue0;
176+
if (hue1 > hue0 && dh > 180) {
177+
dh -= 360;
178+
} else if (hue1 < hue0 && hue0 - hue1 > 180) {
179+
dh += 360;
180+
}
181+
hue = hue0 + t * dh;
182+
} else if (!isNaN(hue0)) {
183+
hue = hue0;
184+
if (light1 === 1 || light1 === 0) chroma = chroma0;
185+
} else if (!isNaN(hue1)) {
186+
hue = hue1;
187+
if (light0 === 1 || light0 === 0) chroma = chroma1;
188+
} else {
189+
hue = NaN;
190+
}
191+
192+
const [r, g, b, alpha] = hclToRgb([
193+
hue,
194+
chroma ?? interpolateNumber(chroma0, chroma1, t),
195+
interpolateNumber(light0, light1, t),
196+
interpolateNumber(alphaF, alphaT, t),
197+
]);
198+
return new Color(r, g, b, alpha, false);
199+
}
200+
case 'lab': {
201+
const [r, g, b, alpha] = labToRgb(interpolateArray(from.lab, to.lab, t));
202+
return new Color(r, g, b, alpha, false);
203+
}
204+
}
205+
}
206+
147207
}
148208

149209
Color.black = new Color(0, 0, 0, 1);

src/util/color_spaces.test.ts renamed to src/expression/types/color_spaces.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {expectCloseToArray} from '../../test/lib/util';
1+
import {expectCloseToArray} from '../../../test/lib/util';
22
import {hclToRgb, hslToRgb, labToRgb, rgbToHcl, rgbToLab} from './color_spaces';
33

44
describe('color spaces', () => {
File renamed without changes.

0 commit comments

Comments
 (0)