diff --git a/build/generate-struct-arrays.js b/build/generate-struct-arrays.js index f2ce2887159..439af9d65ba 100644 --- a/build/generate-struct-arrays.js +++ b/build/generate-struct-arrays.js @@ -129,6 +129,7 @@ const circleAttributes = require('../src/data/bucket/circle_attributes').default const fillAttributes = require('../src/data/bucket/fill_attributes').default; const fillExtrusionAttributes = require('../src/data/bucket/fill_extrusion_attributes').default; const lineAttributes = require('../src/data/bucket/line_attributes').default; +const lineAttributesExt = require('../src/data/bucket/line_attributes_ext').default; const patternAttributes = require('../src/data/bucket/pattern_attributes').default; // layout vertex arrays @@ -138,6 +139,7 @@ const layoutAttributes = { 'fill-extrusion': fillExtrusionAttributes, heatmap: circleAttributes, line: lineAttributes, + lineExt: lineAttributesExt, pattern: patternAttributes }; for (const name in layoutAttributes) { diff --git a/src/data/array_types.js b/src/data/array_types.js index db28da52273..5449a116689 100644 --- a/src/data/array_types.js +++ b/src/data/array_types.js @@ -147,6 +147,40 @@ class StructArrayLayout2i4ub8 extends StructArray { StructArrayLayout2i4ub8.prototype.bytesPerElement = 8; register('StructArrayLayout2i4ub8', StructArrayLayout2i4ub8); +/** + * Implementation of the StructArray layout: + * [0]: Float32[4] + * + * @private + */ +class StructArrayLayout4f16 extends StructArray { + uint8: Uint8Array; + float32: Float32Array; + + _refreshViews() { + this.uint8 = new Uint8Array(this.arrayBuffer); + this.float32 = new Float32Array(this.arrayBuffer); + } + + emplaceBack(v0: number, v1: number, v2: number, v3: number) { + const i = this.length; + this.resize(i + 1); + return this.emplace(i, v0, v1, v2, v3); + } + + emplace(i: number, v0: number, v1: number, v2: number, v3: number) { + const o4 = i * 4; + this.float32[o4 + 0] = v0; + this.float32[o4 + 1] = v1; + this.float32[o4 + 2] = v2; + this.float32[o4 + 3] = v3; + return i; + } +} + +StructArrayLayout4f16.prototype.bytesPerElement = 16; +register('StructArrayLayout4f16', StructArrayLayout4f16); + /** * Implementation of the StructArray layout: * [0]: Uint16[10] @@ -816,40 +850,6 @@ class StructArrayLayout2f8 extends StructArray { StructArrayLayout2f8.prototype.bytesPerElement = 8; register('StructArrayLayout2f8', StructArrayLayout2f8); -/** - * Implementation of the StructArray layout: - * [0]: Float32[4] - * - * @private - */ -class StructArrayLayout4f16 extends StructArray { - uint8: Uint8Array; - float32: Float32Array; - - _refreshViews() { - this.uint8 = new Uint8Array(this.arrayBuffer); - this.float32 = new Float32Array(this.arrayBuffer); - } - - emplaceBack(v0: number, v1: number, v2: number, v3: number) { - const i = this.length; - this.resize(i + 1); - return this.emplace(i, v0, v1, v2, v3); - } - - emplace(i: number, v0: number, v1: number, v2: number, v3: number) { - const o4 = i * 4; - this.float32[o4 + 0] = v0; - this.float32[o4 + 1] = v1; - this.float32[o4 + 2] = v2; - this.float32[o4 + 3] = v3; - return i; - } -} - -StructArrayLayout4f16.prototype.bytesPerElement = 16; -register('StructArrayLayout4f16', StructArrayLayout4f16); - class CollisionBoxStruct extends Struct { _structArray: CollisionBoxArray; anchorPointX: number; @@ -1095,6 +1095,7 @@ export { StructArrayLayout4i8, StructArrayLayout2i4i12, StructArrayLayout2i4ub8, + StructArrayLayout4f16, StructArrayLayout10ui20, StructArrayLayout4i4ui4i24, StructArrayLayout3f12, @@ -1112,7 +1113,6 @@ export { StructArrayLayout2ui4, StructArrayLayout1ui2, StructArrayLayout2f8, - StructArrayLayout4f16, StructArrayLayout2i4 as PosArray, StructArrayLayout4i8 as RasterBoundsArray, StructArrayLayout2i4 as CircleLayoutArray, @@ -1120,6 +1120,7 @@ export { StructArrayLayout2i4i12 as FillExtrusionLayoutArray, StructArrayLayout2i4 as HeatmapLayoutArray, StructArrayLayout2i4ub8 as LineLayoutArray, + StructArrayLayout4f16 as LineExtLayoutArray, StructArrayLayout10ui20 as PatternLayoutArray, StructArrayLayout4i4ui4i24 as SymbolLayoutArray, StructArrayLayout3f12 as SymbolDynamicLayoutArray, diff --git a/src/data/bucket/line_attributes_ext.js b/src/data/bucket/line_attributes_ext.js index aa754c4c00e..a672cb85f4c 100644 --- a/src/data/bucket/line_attributes_ext.js +++ b/src/data/bucket/line_attributes_ext.js @@ -2,8 +2,10 @@ import {createLayout} from '../../util/struct_array'; const lineLayoutAttributesExt = createLayout([ - {name: 'a_line_progress', components: 1, type: 'Float32'} -], 4); + {name: 'a_line_progress', components: 1, type: 'Float32'}, + {name: 'a_line_clips', components: 2, type: 'Float32'}, + {name: 'a_split_index', components: 1, type: 'Float32'}, +]); export default lineLayoutAttributesExt; export const {members, size, alignment} = lineLayoutAttributesExt; diff --git a/src/data/bucket/line_bucket.js b/src/data/bucket/line_bucket.js index e2d7534c06b..fc3414dab20 100644 --- a/src/data/bucket/line_bucket.js +++ b/src/data/bucket/line_bucket.js @@ -1,6 +1,6 @@ // @flow -import {LineLayoutArray, StructArrayLayout1f4} from '../array_types'; +import {LineLayoutArray, LineExtLayoutArray} from '../array_types'; import {members as layoutAttributes} from './line_attributes'; import {members as layoutAttributesExt} from './line_attributes_ext'; @@ -26,7 +26,9 @@ import type { import type LineStyleLayer from '../../style/style_layer/line_style_layer'; import type Point from '@mapbox/point-geometry'; import type {Segment} from '../segment'; +import {RGBAImage} from '../../util/image'; import type Context from '../../gl/context'; +import type Texture from '../../render/texture'; import type IndexBuffer from '../../gl/index_buffer'; import type VertexBuffer from '../../gl/vertex_buffer'; import type {FeatureStates} from '../../source/source_state'; @@ -69,8 +71,8 @@ const LINE_DISTANCE_SCALE = 1 / 2; const MAX_LINE_DISTANCE = Math.pow(2, LINE_DISTANCE_BUFFER_BITS - 1) / LINE_DISTANCE_SCALE; type LineClips = { - clipStart: number; - clipEnd: number; + start: number; + end: number; } /** @@ -94,11 +96,14 @@ class LineBucket implements Bucket { stateDependentLayers: Array; stateDependentLayerIds: Array; patternFeatures: Array; + lineClipsArray: Array; layoutVertexArray: LineLayoutArray; layoutVertexBuffer: VertexBuffer; - layoutVertexArray2: StructArrayLayout1f4; + layoutVertexArray2: LineExtLayoutArray; layoutVertexBuffer2: VertexBuffer; + gradientTexture: ?Texture; + gradient: ?RGBAImage; indexArray: TriangleIndexArray; indexBuffer: IndexBuffer; @@ -118,9 +123,10 @@ class LineBucket implements Bucket { this.hasPattern = false; this.requiresHighPrecisionLineProgress = false; this.patternFeatures = []; + this.lineClipsArray = []; this.layoutVertexArray = new LineLayoutArray(); - this.layoutVertexArray2 = new StructArrayLayout1f4(); + this.layoutVertexArray2 = new LineExtLayoutArray(); this.indexArray = new TriangleIndexArray(); this.programConfigurations = new ProgramConfigurationSet(options.layers, options.zoom); this.segments = new SegmentVector(); @@ -229,9 +235,9 @@ class LineBucket implements Bucket { lineFeatureClips(feature: BucketFeature): ?LineClips { if (!!feature.properties && feature.properties.hasOwnProperty('mapbox_clip_start') && feature.properties.hasOwnProperty('mapbox_clip_end')) { - const clipStart = +feature.properties['mapbox_clip_start']; - const clipEnd = +feature.properties['mapbox_clip_end']; - return {clipStart, clipEnd}; + const start = +feature.properties['mapbox_clip_start']; + const end = +feature.properties['mapbox_clip_end']; + return {start, end}; } } @@ -246,7 +252,7 @@ class LineBucket implements Bucket { totalFeatureDistance += line[i].dist(line[i + 1]); } } - const clipDiff = lineClips.clipEnd - lineClips.clipStart; + const clipDiff = lineClips.end - lineClips.start; if (totalFeatureDistance / clipDiff > MAX_LINE_DISTANCE) { return true; } @@ -280,8 +286,9 @@ class LineBucket implements Bucket { this.totalDistance += vertices[i].dist(vertices[i + 1]); } this.updateScaledDistance(); + this.maxLineLength = Math.max(this.maxLineLength, this.totalDistance); //$FlowFixMe - this.maxLineLength = Math.max(this.maxLineLength, this.totalDistance / (this.lineClips.clipEnd - this.lineClips.clipStart)); + this.lineClipsArray.push(this.lineClips); } const isPolygon = vectorTileFeatureTypes[feature.type] === 'Polygon'; @@ -555,8 +562,12 @@ class LineBucket implements Bucket { linesofarScaled >> 6); // Constructs a second vertex buffer with higher precision line progress - if (this.requiresHighPrecisionLineProgress) { - this.layoutVertexArray2.emplaceBack(this.scaledDistance); + if (this.lineClips) { + this.layoutVertexArray2.emplaceBack( + this.scaledDistance, + this.lineClips.start, + this.lineClips.end, + this.lineClipsArray.length); } const e = segment.vertexLength++; @@ -577,7 +588,7 @@ class LineBucket implements Bucket { // (in tile units) of the current vertex, we can determine the relative distance // of this vertex along the full linestring feature and scale it to [0, 2^15) this.scaledDistance = this.lineClips ? - this.lineClips.clipStart + (this.lineClips.clipEnd - this.lineClips.clipStart) * this.distance / this.totalDistance : + this.lineClips.start + (this.lineClips.end - this.lineClips.start) * this.distance / this.totalDistance : this.distance; } diff --git a/src/render/draw_line.js b/src/render/draw_line.js index 3345ab6d248..524afabc7aa 100644 --- a/src/render/draw_line.js +++ b/src/render/draw_line.js @@ -15,6 +15,8 @@ import type SourceCache from '../source/source_cache'; import type LineStyleLayer from '../style/style_layer/line_style_layer'; import type LineBucket from '../data/bucket/line_bucket'; import type {OverscaledTileID} from '../source/tile_id'; +import {clamp, nextPowerOfTwo} from '../util/util'; +import {renderColorRamp, type ColorRampParams} from '../util/color_ramp'; import EXTENT from '../data/extent'; export default function drawLine(painter: Painter, sourceCache: SourceCache, layer: LineStyleLayer, coords: Array) { @@ -67,7 +69,7 @@ export default function drawLine(painter: Painter, sourceCache: SourceCache, lay const uniformValues = image ? linePatternUniformValues(painter, tile, layer, crossfade) : dasharray ? lineSDFUniformValues(painter, tile, layer, dasharray, crossfade) : - gradient ? lineGradientUniformValues(painter, tile, layer) : + gradient ? lineGradientUniformValues(painter, tile, layer, bucket.lineClipsArray.length) : lineUniformValues(painter, tile, layer); if (image) { @@ -77,16 +79,25 @@ export default function drawLine(painter: Painter, sourceCache: SourceCache, lay } else if (dasharray && (programChanged || painter.lineAtlas.dirty)) { context.activeTexture.set(gl.TEXTURE0); painter.lineAtlas.bind(context); - } else if (gradient && firstTile) { - let gradientTexture = layer.gradientTexture; - if (!gradientTexture) { + } else if (gradient) { + let gradientTexture = bucket.gradientTexture; + if (!gradientTexture || layer.gradientTextureInvalidated) { const sourceMaxZoom = sourceCache.getSource().maxzoom; const lineLength = bucket.maxLineLength / EXTENT; + // Logical pixel tile size is 512px, and 1024px right before current zoom + 1 + const maxTilePixelSize = 1024; // Maximum possible texture coverage heuristic, bound by hardware max texture size - const lineTileCoverageAtMaxZoom = Math.ceil((1 << (sourceMaxZoom - coord.canonical.z)) * lineLength); - - layer.renderGradientFromExpression(lineTileCoverageAtMaxZoom, context.maxTextureSize); - gradientTexture = layer.gradientTexture = new Texture(context, layer.gradient, gl.RGBA); + const maxTextureCoverage = lineLength * maxTilePixelSize; + const textureResolution = nextPowerOfTwo(clamp(maxTextureCoverage, 256, context.maxTextureSize)); + const renderGradientParams = { + expression: layer.gradientExpression(), + evaluationKey: 'lineProgress', + resolution: layer.stepInterpolant ? textureResolution : 256, + image: bucket.gradient || undefined, + clips: bucket.lineClipsArray + }; + bucket.gradient = renderColorRamp(renderGradientParams); + gradientTexture = bucket.gradientTexture = new Texture(context, bucket.gradient, gl.RGBA); } context.activeTexture.set(gl.TEXTURE0); gradientTexture.bind(layer.stepInterpolant ? gl.NEAREST : gl.LINEAR, gl.CLAMP_TO_EDGE); @@ -100,4 +111,6 @@ export default function drawLine(painter: Painter, sourceCache: SourceCache, lay firstTile = false; // once refactored so that bound texture state is managed, we'll also be able to remove this firstTile/programChanged logic } + + layer.gradientTextureInvalidated = false; } diff --git a/src/render/program/line_program.js b/src/render/program/line_program.js index 4ab16c3e5c8..79fa83c2340 100644 --- a/src/render/program/line_program.js +++ b/src/render/program/line_program.js @@ -32,7 +32,8 @@ export type LineGradientUniformsType = {| 'u_ratio': Uniform1f, 'u_device_pixel_ratio': Uniform1f, 'u_units_to_pixels': Uniform2f, - 'u_image': Uniform1i + 'u_image': Uniform1i, + 'u_image_height': Uniform1f, |}; export type LinePatternUniformsType = {| @@ -72,7 +73,8 @@ const lineGradientUniforms = (context: Context, locations: UniformLocations): Li 'u_ratio': new Uniform1f(context, locations.u_ratio), 'u_device_pixel_ratio': new Uniform1f(context, locations.u_device_pixel_ratio), 'u_units_to_pixels': new Uniform2f(context, locations.u_units_to_pixels), - 'u_image': new Uniform1i(context, locations.u_image) + 'u_image': new Uniform1i(context, locations.u_image), + 'u_image_height': new Uniform1f(context, locations.u_image_height), }); const linePatternUniforms = (context: Context, locations: UniformLocations): LinePatternUniformsType => ({ @@ -121,10 +123,12 @@ const lineUniformValues = ( const lineGradientUniformValues = ( painter: Painter, tile: Tile, - layer: LineStyleLayer + layer: LineStyleLayer, + imageHeight: number ): UniformValues => { return extend(lineUniformValues(painter, tile, layer), { - 'u_image': 0 + 'u_image': 0, + 'u_image_height': imageHeight, }); }; diff --git a/src/shaders/line_gradient.fragment.glsl b/src/shaders/line_gradient.fragment.glsl index ea168c4966e..71cf5f41414 100644 --- a/src/shaders/line_gradient.fragment.glsl +++ b/src/shaders/line_gradient.fragment.glsl @@ -1,14 +1,21 @@ uniform lowp float u_device_pixel_ratio; uniform sampler2D u_image; +uniform float u_image_height; varying vec2 v_width2; varying vec2 v_normal; varying float v_gamma_scale; varying highp float v_lineprogress; +varying highp vec2 v_line_clips; +varying float v_split_index; #pragma mapbox: define lowp float blur #pragma mapbox: define lowp float opacity +float map(float value, float start, float end, float new_start, float new_end) { + return ((value - start) * (new_end - new_start)) / (end - start) + new_start; +} + void main() { #pragma mapbox: initialize lowp float blur #pragma mapbox: initialize lowp float opacity @@ -22,9 +29,15 @@ void main() { float blur2 = (blur + 1.0 / u_device_pixel_ratio) * v_gamma_scale; float alpha = clamp(min(dist - (v_width2.t - blur2), v_width2.s - dist) / blur2, 0.0, 1.0); - // For gradient lines, v_lineprogress is the ratio along the entire line, - // scaled to [0, 2^15), and the gradient ramp is stored in a texture. - vec4 color = texture2D(u_image, vec2(v_lineprogress, 0.5)); + float texel_height = 1.0 / u_image_height; + float half_texel_height = 0.5 * texel_height; + vec2 uv = vec2( + map(v_lineprogress, v_line_clips.x, v_line_clips.y, 0.0, 1.0), + v_split_index * texel_height - half_texel_height); + + // For gradient lines, v_lineprogress is the ratio along the + // entire line, the gradient ramp is stored in a texture. + vec4 color = texture2D(u_image, uv); gl_FragColor = color * (alpha * opacity); diff --git a/src/shaders/line_gradient.vertex.glsl b/src/shaders/line_gradient.vertex.glsl index 6398d238061..5b1150436f5 100644 --- a/src/shaders/line_gradient.vertex.glsl +++ b/src/shaders/line_gradient.vertex.glsl @@ -13,6 +13,8 @@ attribute vec2 a_pos_normal; attribute vec4 a_data; attribute float a_line_progress; +attribute vec2 a_line_clips; +attribute float a_split_index; uniform mat4 u_matrix; uniform mediump float u_ratio; @@ -23,6 +25,8 @@ varying vec2 v_normal; varying vec2 v_width2; varying float v_gamma_scale; varying highp float v_lineprogress; +varying highp vec2 v_line_clips; +varying float v_split_index; #pragma mapbox: define lowp float blur #pragma mapbox: define lowp float opacity @@ -45,6 +49,8 @@ void main() { float a_direction = mod(a_data.z, 4.0) - 1.0; v_lineprogress = a_line_progress != 0.0 ? a_line_progress : (floor(a_data.z / 4.0) + a_data.w * 64.0) * 2.0 / MAX_LINE_DISTANCE; + v_line_clips = a_line_clips; + v_split_index = a_split_index; vec2 pos = floor(a_pos_normal * 0.5); diff --git a/src/style/style_layer/heatmap_style_layer.js b/src/style/style_layer/heatmap_style_layer.js index 2ae9575346d..a8f115c798e 100644 --- a/src/style/style_layer/heatmap_style_layer.js +++ b/src/style/style_layer/heatmap_style_layer.js @@ -5,7 +5,7 @@ import StyleLayer from '../style_layer'; import HeatmapBucket from '../../data/bucket/heatmap_bucket'; import {RGBAImage} from '../../util/image'; import properties from './heatmap_style_layer_properties'; -import renderColorRamp from '../../util/color_ramp'; +import {renderColorRamp} from '../../util/color_ramp'; import {Transitionable, Transitioning, PossiblyEvaluated} from '../properties'; import type Texture from '../../render/texture'; @@ -42,7 +42,11 @@ class HeatmapStyleLayer extends StyleLayer { _updateColorRamp() { const expression = this._transitionablePaint._values['heatmap-color'].value.expression; - this.colorRamp = renderColorRamp(expression, 'heatmapDensity'); + this.colorRamp = renderColorRamp({ + expression, + evaluationKey: 'heatmapDensity', + image: this.colorRamp + }); this.colorRampTexture = null; } diff --git a/src/style/style_layer/line_style_layer.js b/src/style/style_layer/line_style_layer.js index 981fb6badf9..fa08fed7008 100644 --- a/src/style/style_layer/line_style_layer.js +++ b/src/style/style_layer/line_style_layer.js @@ -4,13 +4,11 @@ import Point from '@mapbox/point-geometry'; import StyleLayer from '../style_layer'; import LineBucket from '../../data/bucket/line_bucket'; -import {RGBAImage} from '../../util/image'; import {polygonIntersectsBufferedMultiLine} from '../../util/intersection_tests'; import {getMaximumPaintValue, translateDistance, translate} from '../query_utils'; import properties from './line_style_layer_properties'; -import {extend, clamp, nextPowerOfTwo} from '../../util/util'; +import {extend} from '../../util/util'; import EvaluationParameters from '../evaluation_parameters'; -import renderColorRamp from '../../util/color_ramp'; import {Transitionable, Transitioning, Layout, PossiblyEvaluated, DataDrivenProperty} from '../properties'; import Step from '../../style-spec/expression/definitions/step'; @@ -18,7 +16,6 @@ import type {FeatureState, ZoomConstantExpression} from '../../style-spec/expres import type {Bucket, BucketParameters} from '../../data/bucket'; import type {LayoutProps, PaintProps} from './line_style_layer_properties'; import type Transform from '../../geo/transform'; -import type Texture from '../../render/texture'; import type {LayerSpecification} from '../../style-spec/types'; class LineFloorwidthProperty extends DataDrivenProperty { @@ -47,8 +44,7 @@ class LineStyleLayer extends StyleLayer { _unevaluatedLayout: Layout; layout: PossiblyEvaluated; - gradient: ?RGBAImage; - gradientTexture: ?Texture; + gradientTextureInvalidated: boolean; stepInterpolant: boolean; _transitionablePaint: Transitionable; @@ -63,17 +59,12 @@ class LineStyleLayer extends StyleLayer { if (name === 'line-gradient') { const expression: ZoomConstantExpression<'source'> = ((this._transitionablePaint._values['line-gradient'].value.expression): any); this.stepInterpolant = expression._styleExpression.expression instanceof Step; - this.gradientTexture = null; + this.gradientTextureInvalidated = true; } } - renderGradientFromExpression(geometryTileCoverageAtMaxZoom: number, maxTextureSize: number) { - const expression = this._transitionablePaint._values['line-gradient'].value.expression; - // Logical pixel tile size is 512px, and 1024px right before current zoom + 1 - const maxTilePixelSize = 1024; - const maxTextureCoverage = geometryTileCoverageAtMaxZoom * maxTilePixelSize; - const textureResolution = nextPowerOfTwo(clamp(maxTextureCoverage, 256, maxTextureSize)); - this.gradient = renderColorRamp(expression, 'lineProgress', this.stepInterpolant ? textureResolution : 256, this.gradient || undefined); + gradientExpression() { + return this._transitionablePaint._values['line-gradient'].value.expression; } recalculate(parameters: EvaluationParameters, availableImages: Array) { diff --git a/src/util/color_ramp.js b/src/util/color_ramp.js index 70218b0f034..fd68e5fbae2 100644 --- a/src/util/color_ramp.js +++ b/src/util/color_ramp.js @@ -1,39 +1,63 @@ // @flow import {RGBAImage} from './image'; -import {isPowerOfTwo} from './util'; +import {isPowerOfTwo, mapValue} from './util'; import assert from 'assert'; import type {StylePropertyExpression} from '../style-spec/expression/index'; +export type ColorRampParams = { + expression: StylePropertyExpression; + evaluationKey: string; + resolution?: number; + image?: RGBAImage; + clips?: Array; +} + /** - * Given an expression that should evaluate to a color ramp, return - * a 256x1 px RGBA image representing that ramp expression. + * Given an expression that should evaluate to a color ramp, + * return a RGBA image representing that ramp expression. * * @private */ -export default function renderColorRamp(expression: StylePropertyExpression, colorRampEvaluationParameter: string, textureResolution?: number, image?: RGBAImage): RGBAImage { - const resolution = textureResolution || 256; - assert(isPowerOfTwo(resolution)); - let outImage; +export function renderColorRamp(params: ColorRampParams): RGBAImage { + const evaluationGlobals = {}; + const width = params.resolution || 256; + const height = params.clips ? params.clips.length : 1; + const image = params.image || new RGBAImage({width: width, height: height}); - if (image && image.width === resolution) { - outImage = image; - } else { - outImage = new RGBAImage({width: resolution, height: 1}); - } + assert(isPowerOfTwo(width)); - const evaluationGlobals = {}; - for (let i = 0, j = 0; i < resolution; i++, j += 4) { - evaluationGlobals[colorRampEvaluationParameter] = i / (resolution - 1); - const pxColor = expression.evaluate((evaluationGlobals: any)); + const renderPixel = (stride, index, progress) => { + evaluationGlobals[params.evaluationKey] = progress; + const pxColor = params.expression.evaluate((evaluationGlobals: any)); // the colors are being unpremultiplied because Color uses // premultiplied values, and the Texture class expects unpremultiplied ones - outImage.data[j + 0] = Math.floor(pxColor.r * 255 / pxColor.a); - outImage.data[j + 1] = Math.floor(pxColor.g * 255 / pxColor.a); - outImage.data[j + 2] = Math.floor(pxColor.b * 255 / pxColor.a); - outImage.data[j + 3] = Math.floor(pxColor.a * 255); + image.data[stride + index + 0] = Math.floor(pxColor.r * 255 / pxColor.a); + image.data[stride + index + 1] = Math.floor(pxColor.g * 255 / pxColor.a); + image.data[stride + index + 2] = Math.floor(pxColor.b * 255 / pxColor.a); + image.data[stride + index + 3] = Math.floor(pxColor.a * 255); } - return outImage; -} + if (!params.clips) { + for (let i = 0, j = 0; i < width; i++, j += 4) { + const progress = i / (width - 1); + + renderPixel(0, j, progress); + } + } else { + for (let clip = 0, stride = 0; clip < height; ++clip, stride += width * 4) { + for (let i = 0, j = 0; i < width; i++, j += 4) { + // Remap progress between clips + const progress = i / (width - 1); + const evaluationProgress = mapValue(progress, 0.0, 1.0, + params.clips[clip].start, + params.clips[clip].end); + + renderPixel(stride, j, evaluationProgress); + } + } + } + + return image; +} \ No newline at end of file diff --git a/src/util/util.js b/src/util/util.js index 7051e998c2d..0c84381662b 100644 --- a/src/util/util.js +++ b/src/util/util.js @@ -226,6 +226,22 @@ export function nextPowerOfTwo(value: number): number { return Math.pow(2, Math.ceil(Math.log2(value))); } +/** + * Remaps a value from one range to another + * @param value the value to remap + * @param start the start of previous range + * @param end the end of previous range + * @param newStart the new starting range + * @param newEnd the new ending range + * @private + */ +export function mapValue(value: number, start: number, end: number, newStart: number, newEnd: number): number { + if (end - start === 0.0) { + return 0.0; + } + return ((value - start) * (newEnd - newStart)) / (end - start) + newStart; +} + /** * Validate a string to match UUID(v4) of the * form: xxxxxxxx-xxxx-4xxx-[89ab]xxx-xxxxxxxxxxxx diff --git a/test/integration/render-tests/line-gradient/gradient-step-zoomed/expected.png b/test/integration/render-tests/line-gradient/gradient-step-zoomed/expected.png index 9d03f36a8f8..b8a81340fec 100644 Binary files a/test/integration/render-tests/line-gradient/gradient-step-zoomed/expected.png and b/test/integration/render-tests/line-gradient/gradient-step-zoomed/expected.png differ diff --git a/test/integration/render-tests/line-gradient/gradient-step/expected.png b/test/integration/render-tests/line-gradient/gradient-step/expected.png index 1f65c3d6b61..33fd6f5e87d 100644 Binary files a/test/integration/render-tests/line-gradient/gradient-step/expected.png and b/test/integration/render-tests/line-gradient/gradient-step/expected.png differ diff --git a/test/unit/util/color_ramp.test.js b/test/unit/util/color_ramp.test.js index 9c4e4bc1879..d048d27b6e1 100644 --- a/test/unit/util/color_ramp.test.js +++ b/test/unit/util/color_ramp.test.js @@ -1,6 +1,6 @@ import {test} from '../../util/test'; -import renderColorRamp from '../../../src/util/color_ramp'; +import {renderColorRamp} from '../../../src/util/color_ramp'; import {createPropertyExpression} from '../../../src/style-spec/expression'; const spec = { @@ -34,7 +34,7 @@ test('renderColorRamp linear', (t) => { 1, 'red' ], spec, {handleErrors: false}).value; - const ramp = renderColorRamp(expression, 'lineProgress'); + const ramp = renderColorRamp({ expression, evaluationKey: 'lineProgress' }); t.equal(ramp.width, 256); t.equal(ramp.height, 1); @@ -61,7 +61,7 @@ test('renderColorRamp step', (t) => { 1, 'black' ], spec, {handleErrors: false}).value; - const ramp = renderColorRamp(expression, 'lineProgress', 512); + const ramp = renderColorRamp({ expression, evaluationKey: 'lineProgress', resolution: 512 }); t.equal(ramp.width, 512); t.equal(ramp.height, 1); @@ -89,12 +89,12 @@ test('renderColorRamp usePlacement', (t) => { 1, 'white' ], spec, {handleErrors: false}).value; - let ramp = renderColorRamp(expression, 'lineProgress', 512); + let ramp = renderColorRamp({ expression, evaluationKey: 'lineProgress', resolution: 512 }); t.equal(ramp.width, 512); t.equal(ramp.height, 1); - ramp = renderColorRamp(expression, 'lineProgress', 512, ramp); + renderColorRamp({ expression, evaluationKey: 'lineProgress', resolution: 512, image: ramp }); t.equal(pixelAt(ramp, 0)[3], 127, 'pixel at 0.0 matches input alpha'); t.ok(nearlyEquals(pixelAt(ramp, 50), [255, 0, 0, 127]), 'pixel < 0.1 matches input'); diff --git a/test/unit/util/util.test.js b/test/unit/util/util.test.js index bad799c54bb..00e9543bedb 100644 --- a/test/unit/util/util.test.js +++ b/test/unit/util/util.test.js @@ -2,7 +2,7 @@ import {test} from '../../util/test'; -import {easeCubicInOut, keysDifference, extend, pick, uniqueId, bindAll, asyncAll, clamp, wrap, bezier, endsWith, mapObject, filterObject, deepEqual, clone, arraysIntersect, isCounterClockwise, isClosedPolygon, parseCacheControl, uuid, validateUuid, nextPowerOfTwo, isPowerOfTwo} from '../../../src/util/util'; +import {easeCubicInOut, mapValue, keysDifference, extend, pick, uniqueId, bindAll, asyncAll, clamp, wrap, bezier, endsWith, mapObject, filterObject, deepEqual, clone, arraysIntersect, isCounterClockwise, isClosedPolygon, parseCacheControl, uuid, validateUuid, nextPowerOfTwo, isPowerOfTwo} from '../../../src/util/util'; import Point from '@mapbox/point-geometry'; test('util', (t) => { @@ -107,6 +107,18 @@ test('util', (t) => { t.end(); }); + t.test('mapValue', (t) => { + t.equal(mapValue(0.0, 0.0, 1.0, 1.0, 2.0), 1.0); + t.equal(mapValue(1.0, 0.0, 2.0, 10.0, 20.0), 15.0); + t.equal(mapValue(5.0, 0.0, 10.0, 50.0, 100.0), 75.0); + t.equal(mapValue(0.0, -1.0, 1.0, 2.0, 3.0), 2.5); + t.equal(mapValue(-15.0, -10.0, -20.0, 10.0, 20.0), 15.0); + t.equal(mapValue(0.6, 0.0, 1.0, 1.0, 0.0), 0.4); + t.equal(mapValue(0.0, 0.0, 1.0, 0.0, 0.0), 0.0); + t.equal(mapValue(0.0, 0.0, 0.0, 0.0, 1.0), 0.0); + t.end(); + }); + t.test('clamp', (t) => { t.equal(clamp(0, 0, 1), 0); t.equal(clamp(1, 0, 1), 1);