+ WebGL blend functions with neutral color blending. Zero performance overhead.
+ Layers: Satellite (base) + OSM overlay (top, with blend mode applied)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Current: 0.7
+
+ Controls opacity of the OSM overlay layer (top layer)
+
+
+
+
+
Current: normal @ 0.7
+
+ Standard alpha blending. OSM layer semi-transparent over satellite imagery.
+
+
+
+
+ Debug Info:
+ Shader premultiply: ?
+ Actual blend mode: ?
+ Actual opacity: ?
+
+
+
+ ⚠️ Compatibility Warning:
+
+
+
+
+ 📍 Map Position:
+ Center: -122.2129, 37.5047
+ Zoom: 13.68
+ Move the map to find interesting areas!
+
+
+
+
+
+
+
diff --git a/src/gl/color_mode.ts b/src/gl/color_mode.ts
index 44d8f88a1f1..524309c8c8d 100644
--- a/src/gl/color_mode.ts
+++ b/src/gl/color_mode.ts
@@ -4,8 +4,10 @@ import type {BlendEquationType, BlendFuncType, ColorMaskType} from './types';
export const ZERO = 0x0000;
export const ONE = 0x0001;
+export const ONE_MINUS_SRC_COLOR = 0x0301;
export const SRC_ALPHA = 0x0302;
export const ONE_MINUS_SRC_ALPHA = 0x0303;
+export const ONE_MINUS_DST_ALPHA = 0x0305;
export const DST_COLOR = 0x0306;
export default class ColorMode {
@@ -29,6 +31,9 @@ export default class ColorMode {
static alphaBlendedNonPremultiplied: Readonly;
static multiply: Readonly;
static additive: Readonly;
+ static screen: Readonly;
+ static darken: Readonly;
+ static lighten: Readonly;
}
ColorMode.Replace = [ONE, ZERO, ONE, ZERO];
@@ -37,5 +42,10 @@ ColorMode.disabled = new ColorMode(ColorMode.Replace, Color.transparent, [false,
ColorMode.unblended = new ColorMode(ColorMode.Replace, Color.transparent, [true, true, true, true]);
ColorMode.alphaBlended = new ColorMode([ONE, ONE_MINUS_SRC_ALPHA, ONE, ONE_MINUS_SRC_ALPHA], Color.transparent, [true, true, true, true]);
ColorMode.alphaBlendedNonPremultiplied = new ColorMode([SRC_ALPHA, ONE_MINUS_SRC_ALPHA, SRC_ALPHA, ONE_MINUS_SRC_ALPHA], Color.transparent, [true, true, true, true]);
-ColorMode.multiply = new ColorMode([DST_COLOR, ZERO, DST_COLOR, ZERO], Color.transparent, [true, true, true, true]);
-ColorMode.additive = new ColorMode([ONE, ONE, ONE, ONE], Color.transparent, [true, true, true, true]);
+// Blend modes: RGB uses blend formula, Alpha uses standard blending
+// Note: multiply and screen use premultiplied alpha for correct opacity behavior
+ColorMode.multiply = new ColorMode([DST_COLOR, ZERO, SRC_ALPHA, ONE_MINUS_SRC_ALPHA], Color.transparent, [true, true, true, true]);
+ColorMode.additive = new ColorMode([ONE, ONE, SRC_ALPHA, ONE_MINUS_SRC_ALPHA], Color.transparent, [true, true, true, true]);
+ColorMode.screen = new ColorMode([ONE, ONE_MINUS_SRC_COLOR, SRC_ALPHA, ONE_MINUS_SRC_ALPHA], Color.transparent, [true, true, true, true]);
+ColorMode.darken = new ColorMode([ONE, ONE, SRC_ALPHA, ONE_MINUS_SRC_ALPHA], Color.transparent, [true, true, true, true], 0x8007 /* MIN */);
+ColorMode.lighten = new ColorMode([ONE, ONE, SRC_ALPHA, ONE_MINUS_SRC_ALPHA], Color.transparent, [true, true, true, true], 0x8008 /* MAX */);
diff --git a/src/render/draw_raster.ts b/src/render/draw_raster.ts
index 344b20af5bb..3ecccc85284 100644
--- a/src/render/draw_raster.ts
+++ b/src/render/draw_raster.ts
@@ -2,6 +2,7 @@ import assert from 'assert';
import ImageSource from '../source/image_source';
import StencilMode from '../gl/stencil_mode';
import DepthMode from '../gl/depth_mode';
+import ColorMode from '../gl/color_mode';
import CullFaceMode from '../gl/cull_face_mode';
import Texture from './texture';
import {rasterPoleUniformValues, rasterUniformValues} from './program/raster_program';
@@ -84,7 +85,39 @@ function drawRaster(painter: Painter, sourceCache: SourceCache, layer: RasterSty
const emissiveStrength = layer.paint.get('raster-emissive-strength');
- const colorMode = painter.colorModeForDrapableLayerRenderPass(emissiveStrength);
+ // Select color mode and blending parameters based on blend mode
+ const blendMode = layer.paint.get('raster-blend-mode');
+ let colorMode: ColorMode;
+ let blendNeutral: number; // Neutral color for blend modes: 1.0=white (multiply), 0.0=black (screen), -1.0=none
+
+ // If no blend mode specified, use standard alpha blending
+ if (!blendMode) {
+ colorMode = painter.colorModeForDrapableLayerRenderPass(emissiveStrength);
+ blendNeutral = -1.0; // Not used for standard alpha blending
+ } else {
+ switch (blendMode) {
+ case 'multiply':
+ colorMode = ColorMode.multiply;
+ blendNeutral = 1.0; // White is neutral for multiply
+ break;
+ case 'screen':
+ colorMode = ColorMode.screen;
+ blendNeutral = 0.0; // Black is neutral for screen
+ break;
+ case 'darken':
+ colorMode = ColorMode.darken;
+ blendNeutral = -1.0; // No neutral blending - darken has limited opacity support
+ break;
+ case 'lighten':
+ colorMode = ColorMode.lighten;
+ blendNeutral = 0.0; // Black is neutral for lighten (MAX(black, x) = x)
+ break;
+ default:
+ colorMode = painter.colorModeForDrapableLayerRenderPass(emissiveStrength);
+ blendNeutral = -1.0; // Fallback to standard alpha blending
+ break;
+ }
+ }
// When rendering to texture, coordinates are already sorted: primary by
// proxy id and secondary sort is by Z.
@@ -95,12 +128,14 @@ function drawRaster(painter: Painter, sourceCache: SourceCache, layer: RasterSty
if (source instanceof ImageSource && !tileIDs.length && (source.onNorthPole || source.onSouthPole)) {
const stencilMode = renderingWithElevation ? painter.stencilModeFor3D() : StencilMode.disabled;
+ // Use premultiplied alpha only when no blend mode is specified (standard alpha blending)
+ const isPremultiplied = !blendMode ? 1.0 : 0.0;
if (source.onNorthPole) {
- drawPole(true, null, painter, sourceCache, layer, emissiveStrength, rasterConfig, CullFaceMode.disabled, stencilMode);
+ drawPole(true, null, painter, sourceCache, layer, emissiveStrength, rasterConfig, CullFaceMode.disabled, stencilMode, colorMode, isPremultiplied, blendNeutral);
} else {
- drawPole(false, null, painter, sourceCache, layer, emissiveStrength, rasterConfig, CullFaceMode.disabled, stencilMode);
+ drawPole(false, null, painter, sourceCache, layer, emissiveStrength, rasterConfig, CullFaceMode.disabled, stencilMode, colorMode, isPremultiplied, blendNeutral);
}
return;
}
@@ -227,6 +262,14 @@ function drawRaster(painter: Painter, sourceCache: SourceCache, layer: RasterSty
gridMatrix = new Float32Array(9);
}
+ // Determine if we should use premultiplied alpha output
+ // - Normal mode uses premultiplied alpha for standard alpha blending
+ // - Blend modes (multiply, screen) use neutral color blending in shader for opacity control
+ // - Darken/lighten use non-premultiplied with limited opacity support
+ // (MIN/MAX blend equations ignore blend factors, so opacity only affects alpha channel)
+ // Use premultiplied alpha only when no blend mode is specified (standard alpha blending)
+ const isPremultiplied = !blendMode ? 1.0 : 0.0;
+
const uniformValues = rasterUniformValues(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
projMatrix,
@@ -249,7 +292,9 @@ function drawRaster(painter: Painter, sourceCache: SourceCache, layer: RasterSty
rasterConfig.range,
tileSize,
buffer,
- emissiveStrength
+ emissiveStrength,
+ isPremultiplied,
+ blendNeutral
);
const affectedByFog = painter.isTileAffectedByFog(coord);
@@ -287,7 +332,7 @@ function drawRaster(painter: Painter, sourceCache: SourceCache, layer: RasterSty
assert(indexBuffer);
assert(segments);
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
- program.draw(painter, gl.TRIANGLES, depthMode, elevatedStencilMode || stencilMode, painter.colorModeForRenderPass(), cullFaceMode, uniformValues, layer.id, buffer, indexBuffer, segments);
+ program.draw(painter, gl.TRIANGLES, depthMode, elevatedStencilMode || stencilMode, colorMode, cullFaceMode, uniformValues, layer.id, buffer, indexBuffer, segments);
}
} else {
const {tileBoundsBuffer, tileBoundsIndexBuffer, tileBoundsSegments} = painter.getTileBoundsBuffers(tile);
@@ -300,16 +345,39 @@ function drawRaster(painter: Painter, sourceCache: SourceCache, layer: RasterSty
}
if (!(source instanceof ImageSource) && renderingElevatedOnGlobe) {
+ // Determine if we should use premultiplied alpha output
+ // Normal mode uses premultiplied, all blend modes use non-premultiplied
+ // Use premultiplied alpha only when no blend mode is specified (standard alpha blending)
+ const isPremultiplied = !blendMode ? 1.0 : 0.0;
+ // Set blend neutral values (must match logic above)
+ let poleBlendNeutral: number;
+ switch (blendMode) {
+ case 'multiply':
+ poleBlendNeutral = 1.0;
+ break;
+ case 'screen':
+ poleBlendNeutral = 0.0;
+ break;
+ case 'darken':
+ poleBlendNeutral = -1.0;
+ break;
+ case 'lighten':
+ poleBlendNeutral = 0.0;
+ break;
+ default:
+ poleBlendNeutral = -1.0;
+ break;
+ }
for (const coord of tiles) {
const topCap = coord.canonical.y === 0;
const bottomCap = coord.canonical.y === (1 << coord.canonical.z) - 1;
if (topCap) {
- drawPole(true, coord, painter, sourceCache, layer, emissiveStrength, rasterConfig, cullFaceMode, elevatedStencilMode || StencilMode.disabled);
+ drawPole(true, coord, painter, sourceCache, layer, emissiveStrength, rasterConfig, cullFaceMode, elevatedStencilMode || StencilMode.disabled, colorMode, isPremultiplied, poleBlendNeutral);
}
if (bottomCap) {
- drawPole(false, coord, painter, sourceCache, layer, emissiveStrength, rasterConfig, cullFaceMode === CullFaceMode.frontCW ? CullFaceMode.backCW : CullFaceMode.frontCW, elevatedStencilMode || StencilMode.disabled);
+ drawPole(false, coord, painter, sourceCache, layer, emissiveStrength, rasterConfig, cullFaceMode === CullFaceMode.frontCW ? CullFaceMode.backCW : CullFaceMode.frontCW, elevatedStencilMode || StencilMode.disabled, colorMode, isPremultiplied, poleBlendNeutral);
}
}
}
@@ -328,7 +396,7 @@ function drawRaster(painter: Painter, sourceCache: SourceCache, layer: RasterSty
painter.resetStencilClippingMasks();
}
-function drawPole(isNorth: boolean, coord: OverscaledTileID | null | undefined, painter: Painter, sourceCache: SourceCache, layer: RasterStyleLayer, emissiveStrength: number, rasterConfig: RasterConfig, cullFaceMode: CullFaceMode, stencilMode: StencilMode) {
+function drawPole(isNorth: boolean, coord: OverscaledTileID | null | undefined, painter: Painter, sourceCache: SourceCache, layer: RasterStyleLayer, emissiveStrength: number, rasterConfig: RasterConfig, cullFaceMode: CullFaceMode, stencilMode: StencilMode, colorMode: ColorMode, isPremultiplied: number, blendNeutral: number) {
const source = sourceCache.getSource();
const sharedBuffers = painter.globeSharedBuffers;
if (!sharedBuffers) return;
@@ -355,7 +423,6 @@ function drawPole(isNorth: boolean, coord: OverscaledTileID | null | undefined,
const context = painter.context;
const gl = context.gl;
const textureFilter = layer.paint.get('raster-resampling') === 'nearest' ? gl.NEAREST : gl.LINEAR;
- const colorMode = painter.colorModeForDrapableLayerRenderPass(emissiveStrength);
const defines = rasterConfig.defines;
defines.push("GLOBE_POLES");
@@ -394,7 +461,7 @@ function drawPole(isNorth: boolean, coord: OverscaledTileID | null | undefined,
}
const rasterColorMix = adjustColorMix(rasterConfig.mix);
- const uniformValues = rasterPoleUniformValues(projMatrix, normalizeMatrix, globeMatrix as Float32Array, globeToMercatorTransition(painter.transform.zoom), fade, layer, [0, 0], elevation, RASTER_COLOR_TEXTURE_UNIT, rasterColorMix, rasterConfig.offset, rasterConfig.range, emissiveStrength);
+ const uniformValues = rasterPoleUniformValues(projMatrix, normalizeMatrix, globeMatrix as Float32Array, globeToMercatorTransition(painter.transform.zoom), fade, layer, [0, 0], elevation, RASTER_COLOR_TEXTURE_UNIT, rasterColorMix, rasterConfig.offset, rasterConfig.range, emissiveStrength, isPremultiplied, blendNeutral);
const program = painter.getOrCreateProgram('raster', {defines});
painter.uploadCommonUniforms(context, program, null);
diff --git a/src/render/program/raster_program.ts b/src/render/program/raster_program.ts
index eed4281f0bc..73a61967463 100644
--- a/src/render/program/raster_program.ts
+++ b/src/render/program/raster_program.ts
@@ -43,6 +43,8 @@ export type RasterUniformsType = {
['u_texture_offset']: Uniform2f;
['u_texture_res']: Uniform2f;
['u_emissive_strength']: Uniform1f;
+ ['u_is_premultiplied']: Uniform1f;
+ ['u_blend_neutral']: Uniform1f;
};
export type RasterDefinesType = 'RASTER_COLOR' | 'RENDER_CUTOFF' | 'RASTER_ARRAY' | 'RASTER_ARRAY_LINEAR';
@@ -74,7 +76,9 @@ const rasterUniforms = (context: Context): RasterUniformsType => ({
'u_color_ramp': new Uniform1i(context),
'u_texture_offset': new Uniform2f(context),
'u_texture_res': new Uniform2f(context),
- 'u_emissive_strength': new Uniform1f(context)
+ 'u_emissive_strength': new Uniform1f(context),
+ 'u_is_premultiplied': new Uniform1f(context),
+ 'u_blend_neutral': new Uniform1f(context)
});
const rasterUniformValues = (
@@ -102,6 +106,8 @@ const rasterUniformValues = (
tileSize: number,
buffer: number,
emissiveStrength: number,
+ isPremultiplied: number,
+ blendNeutral: number,
): UniformValues => ({
'u_matrix': matrix,
'u_normalize_matrix': normalizeMatrix,
@@ -136,7 +142,9 @@ const rasterUniformValues = (
tileSize / (tileSize + 2 * buffer)
],
'u_texture_res': [tileSize + 2 * buffer, tileSize + 2 * buffer],
- 'u_emissive_strength': emissiveStrength
+ 'u_emissive_strength': emissiveStrength,
+ 'u_is_premultiplied': isPremultiplied,
+ 'u_blend_neutral': blendNeutral
});
const rasterPoleUniformValues = (
@@ -156,6 +164,8 @@ const rasterPoleUniformValues = (
colorOffset: number,
colorRange: [number, number],
emissiveStrength: number,
+ isPremultiplied: number,
+ blendNeutral: number,
): UniformValues => (rasterUniformValues(
matrix,
normalizeMatrix,
@@ -178,6 +188,8 @@ const rasterPoleUniformValues = (
1,
0,
emissiveStrength,
+ isPremultiplied,
+ blendNeutral,
));
function spinWeights(angle: number): [number, number, number] {
diff --git a/src/shaders/raster.fragment.glsl b/src/shaders/raster.fragment.glsl
index 16a6e1cc1b0..138530d5dbd 100644
--- a/src/shaders/raster.fragment.glsl
+++ b/src/shaders/raster.fragment.glsl
@@ -6,6 +6,8 @@ uniform float u_fade_t;
uniform float u_opacity;
uniform highp float u_raster_elevation;
uniform highp float u_zoom_transition;
+uniform float u_is_premultiplied;
+uniform float u_blend_neutral;
in vec2 v_pos0;
in vec2 v_pos1;
@@ -119,7 +121,24 @@ void main() {
out_color = fog_dither(fog_apply(out_color, v_fog_pos, fog_limit));
#endif
- glFragColor = vec4(out_color * color.a, color.a);
+ // Handle different blend mode output formats
+ vec3 final_color;
+ if (u_is_premultiplied > 0.5) {
+ // Normal mode: premultiplied alpha for standard alpha blending
+ final_color = out_color * color.a;
+ } else if (u_blend_neutral >= 0.0) {
+ // Blend modes with neutral color support (multiply, screen, lighten):
+ // multiply: neutral=1.0 (white has no darkening effect)
+ // screen: neutral=0.0 (black has no lightening effect)
+ // lighten: neutral=0.0 (black has no lightening effect)
+ final_color = mix(vec3(u_blend_neutral), out_color, color.a);
+ } else {
+ // Darken mode: direct non-premultiplied output
+ // Note: Limited opacity support due to WebGL MIN equation constraints
+ // Opacity only affects alpha channel, not RGB blending behavior
+ final_color = out_color;
+ }
+ glFragColor = vec4(final_color, color.a);
#ifdef PROJECTION_GLOBE_VIEW
glFragColor *= mix(1.0, 1.0 - smoothstep(0.0, 0.05, u_zoom_transition), smoothstep(0.8, 0.9, v_split_fade));
#endif
diff --git a/src/style-spec/reference/v8.json b/src/style-spec/reference/v8.json
index abf217b7b68..af09f60c091 100644
--- a/src/style-spec/reference/v8.json
+++ b/src/style-spec/reference/v8.json
@@ -10268,6 +10268,36 @@
},
"property-type": "data-constant"
},
+ "raster-blend-mode": {
+ "type": "enum",
+ "doc": "Blend mode for compositing the raster layer with underlying layers. Uses native WebGL blend functions for zero performance overhead. If not specified, standard alpha blending is used.",
+ "values": {
+ "multiply": {
+ "doc": "Multiplies the colors, resulting in a darker image. Useful for blending raster overlays with base maps."
+ },
+ "screen": {
+ "doc": "Inverted multiply effect, resulting in a lighter image"
+ },
+ "darken": {
+ "doc": "Selects the darker of the two colors for each pixel. Note: Due to WebGL 1.0 limitations, this mode has limited opacity support. Use opacity values of 0 or 1 only; intermediate values may produce unexpected results."
+ },
+ "lighten": {
+ "doc": "Selects the lighter of the two colors for each pixel. Note: Due to WebGL 1.0 limitations, this mode has limited opacity support. Use opacity values of 0 or 1 only; intermediate values may produce unexpected results."
+ }
+ },
+ "sdk-support": {
+ "basic functionality": {
+ "js": "3.8.0"
+ }
+ },
+ "expression": {
+ "interpolated": false,
+ "parameters": [
+ "zoom"
+ ]
+ },
+ "property-type": "data-constant"
+ },
"raster-fade-duration": {
"type": "number",
"default": 300,
diff --git a/src/style-spec/types.ts b/src/style-spec/types.ts
index c03bc2737a1..bf18118e6ec 100644
--- a/src/style-spec/types.ts
+++ b/src/style-spec/types.ts
@@ -1307,6 +1307,7 @@ export type RasterLayerSpecification = {
"raster-contrast"?: PropertyValueSpecification,
"raster-contrast-transition"?: TransitionSpecification,
"raster-resampling"?: PropertyValueSpecification<"linear" | "nearest">,
+ "raster-blend-mode"?: PropertyValueSpecification<"multiply" | "screen" | "darken" | "lighten">,
"raster-fade-duration"?: PropertyValueSpecification,
"raster-emissive-strength"?: PropertyValueSpecification,
"raster-emissive-strength-transition"?: TransitionSpecification,
diff --git a/src/style/style_layer/raster_style_layer.ts b/src/style/style_layer/raster_style_layer.ts
index 5fe4058a14f..6f5d4a9e729 100644
--- a/src/style/style_layer/raster_style_layer.ts
+++ b/src/style/style_layer/raster_style_layer.ts
@@ -73,6 +73,37 @@ class RasterStyleLayer extends StyleLayer {
this.updateColorRamp();
}
+
+ // Validate blend mode and opacity compatibility
+ if (name === 'raster-blend-mode' || name === 'raster-opacity') {
+ this._validateBlendModeOpacity();
+ }
+ }
+
+ /**
+ * Validates blend mode and opacity compatibility. Warns when darken mode
+ * is used with partial opacity values, as WebGL's MIN equation cannot
+ * properly support opacity interpolation.
+ * @private
+ */
+ _validateBlendModeOpacity() {
+ if (!this.paint) {
+ return;
+ }
+
+ const blendMode = this.paint.get('raster-blend-mode');
+ const opacity = this.paint.get('raster-opacity');
+
+ // Darken mode has fundamental limitations with opacity due to WebGL MIN equation
+ // The MIN operation cannot be interpolated without reading the framebuffer
+ if (blendMode === 'darken' && opacity !== 0 && opacity !== 1) {
+ console.warn(
+ `Layer "${this.id}": raster-blend-mode "darken" has limited opacity support. ` +
+ `Opacity values between 0 and 1 (current: ${opacity.toFixed(2)}) may produce unexpected results. ` +
+ `WebGL's MIN blend equation cannot properly support opacity interpolation. ` +
+ `For opacity-controlled darkening, use "multiply" blend mode instead.`
+ );
+ }
}
override _clear() {
diff --git a/src/style/style_layer/raster_style_layer_properties.ts b/src/style/style_layer/raster_style_layer_properties.ts
index 829d4e2aa7d..1a4a286e5bc 100644
--- a/src/style/style_layer/raster_style_layer_properties.ts
+++ b/src/style/style_layer/raster_style_layer_properties.ts
@@ -35,6 +35,7 @@ export type PaintProps = {
"raster-saturation": DataConstantProperty;
"raster-contrast": DataConstantProperty;
"raster-resampling": DataConstantProperty<"linear" | "nearest">;
+ "raster-blend-mode": DataConstantProperty<"multiply" | "screen" | "darken" | "lighten">;
"raster-fade-duration": DataConstantProperty;
"raster-emissive-strength": DataConstantProperty;
"raster-array-band": DataConstantProperty;
@@ -54,6 +55,7 @@ export const getPaintProperties = (): Properties => paint || (paint
"raster-saturation": new DataConstantProperty(styleSpec["paint_raster"]["raster-saturation"]),
"raster-contrast": new DataConstantProperty(styleSpec["paint_raster"]["raster-contrast"]),
"raster-resampling": new DataConstantProperty(styleSpec["paint_raster"]["raster-resampling"]),
+ "raster-blend-mode": new DataConstantProperty(styleSpec["paint_raster"]["raster-blend-mode"]),
"raster-fade-duration": new DataConstantProperty(styleSpec["paint_raster"]["raster-fade-duration"]),
"raster-emissive-strength": new DataConstantProperty(styleSpec["paint_raster"]["raster-emissive-strength"]),
"raster-array-band": new DataConstantProperty(styleSpec["paint_raster"]["raster-array-band"]),
diff --git a/test/unit/style/style_layer/raster_style_layer_blend_modes.test.ts b/test/unit/style/style_layer/raster_style_layer_blend_modes.test.ts
new file mode 100644
index 00000000000..3b574252fe1
--- /dev/null
+++ b/test/unit/style/style_layer/raster_style_layer_blend_modes.test.ts
@@ -0,0 +1,150 @@
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-nocheck
+import {describe, test, expect, vi} from '../../../util/vitest';
+import createStyleLayer from '../../../../src/style/create_style_layer';
+import RasterStyleLayer from '../../../../src/style/style_layer/raster_style_layer';
+
+describe('RasterStyleLayer#raster-blend-mode', () => {
+ function createRasterLayer(blendMode?: string, opacity?: number) {
+ const config: any = {
+ "id": "test-raster",
+ "type": "raster",
+ "source": "test-source",
+ "paint": {}
+ };
+
+ if (blendMode !== undefined) {
+ config.paint['raster-blend-mode'] = blendMode;
+ }
+ if (opacity !== undefined) {
+ config.paint['raster-opacity'] = opacity;
+ }
+
+ const layer = createStyleLayer(config);
+ layer.updateTransitions({});
+ layer.recalculate({zoom: 0});
+ return layer;
+ }
+
+ test('instantiates as RasterStyleLayer', () => {
+ const layer = createRasterLayer();
+ expect(layer instanceof RasterStyleLayer).toBeTruthy();
+ });
+
+ test('defaults to undefined when not specified', () => {
+ const layer = createRasterLayer();
+ expect(layer.getPaintProperty('raster-blend-mode')).toEqual(undefined);
+ });
+
+ test('sets multiply blend mode', () => {
+ const layer = createRasterLayer('multiply');
+ expect(layer.getPaintProperty('raster-blend-mode')).toEqual('multiply');
+ expect(layer.paint.get('raster-blend-mode')).toEqual('multiply');
+ });
+
+ test('sets screen blend mode', () => {
+ const layer = createRasterLayer('screen');
+ expect(layer.getPaintProperty('raster-blend-mode')).toEqual('screen');
+ expect(layer.paint.get('raster-blend-mode')).toEqual('screen');
+ });
+
+ test('sets darken blend mode', () => {
+ const layer = createRasterLayer('darken');
+ expect(layer.getPaintProperty('raster-blend-mode')).toEqual('darken');
+ expect(layer.paint.get('raster-blend-mode')).toEqual('darken');
+ });
+
+ test('sets lighten blend mode', () => {
+ const layer = createRasterLayer('lighten');
+ expect(layer.getPaintProperty('raster-blend-mode')).toEqual('lighten');
+ expect(layer.paint.get('raster-blend-mode')).toEqual('lighten');
+ });
+
+ test('updates blend mode value', () => {
+ const layer = createRasterLayer('multiply');
+ layer.setPaintProperty('raster-blend-mode', 'screen');
+ expect(layer.getPaintProperty('raster-blend-mode')).toEqual('screen');
+ });
+
+ test('unsets blend mode value', () => {
+ const layer = createRasterLayer('multiply');
+ layer.setPaintProperty('raster-blend-mode', null);
+ expect(layer.getPaintProperty('raster-blend-mode')).toEqual(undefined);
+ });
+});
+
+describe('RasterStyleLayer#_validateBlendModeOpacity', () => {
+ function createRasterLayer(blendMode?: string, opacity?: number) {
+ const config: any = {
+ "id": "test-raster",
+ "type": "raster",
+ "source": "test-source",
+ "paint": {}
+ };
+
+ if (blendMode !== undefined) {
+ config.paint['raster-blend-mode'] = blendMode;
+ }
+ if (opacity !== undefined) {
+ config.paint['raster-opacity'] = opacity;
+ }
+
+ const layer = createStyleLayer(config);
+ layer.updateTransitions({});
+ layer.recalculate({zoom: 0});
+ return layer;
+ }
+
+ test('does not warn for darken with opacity 0', () => {
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
+ const layer = createRasterLayer('darken', 0);
+ layer.setPaintProperty('raster-blend-mode', 'darken');
+ expect(console.warn).not.toHaveBeenCalled();
+ });
+
+ test('does not warn for darken with opacity 1', () => {
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
+ const layer = createRasterLayer('darken', 1);
+ layer.setPaintProperty('raster-blend-mode', 'darken');
+ expect(console.warn).not.toHaveBeenCalled();
+ });
+
+ test('warns for darken with partial opacity', () => {
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
+ const layer = createRasterLayer('darken', 0.5);
+ layer.setPaintProperty('raster-blend-mode', 'darken');
+ expect(console.warn).toHaveBeenCalledTimes(1);
+ expect(
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+ console.warn.mock.calls[0][0]
+ ).toMatch(/raster-blend-mode "darken" has limited opacity support/);
+ });
+
+ test('does not warn for multiply with partial opacity', () => {
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
+ const layer = createRasterLayer('multiply', 0.5);
+ layer.setPaintProperty('raster-blend-mode', 'multiply');
+ expect(console.warn).not.toHaveBeenCalled();
+ });
+
+ test('does not warn for screen with partial opacity', () => {
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
+ const layer = createRasterLayer('screen', 0.5);
+ layer.setPaintProperty('raster-blend-mode', 'screen');
+ expect(console.warn).not.toHaveBeenCalled();
+ });
+
+ test('does not warn for lighten with partial opacity', () => {
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
+ const layer = createRasterLayer('lighten', 0.5);
+ layer.setPaintProperty('raster-blend-mode', 'lighten');
+ expect(console.warn).not.toHaveBeenCalled();
+ });
+
+ test('does not warn when no blend mode is set', () => {
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
+ const layer = createRasterLayer(undefined, 0.5);
+ layer.setPaintProperty('raster-opacity', 0.7);
+ expect(console.warn).not.toHaveBeenCalled();
+ });
+});