Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add changelog entry and unit tests for raster-blend-mode
  • Loading branch information
JoAllg committed Nov 4, 2025
commit 6733ceb0ca57bc731bbf286753f4c38e52e8fce2
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## main

### Features and improvements ✨

- Add `raster-blend-mode` property for raster layers with hardware-accelerated blend modes (`multiply`, `screen`, `darken`, `lighten`). Uses native WebGL blend functions for zero performance overhead.

## 3.16.0

### Features and improvements ✨
Expand Down
150 changes: 150 additions & 0 deletions test/unit/style/style_layer/raster_style_layer_blend_modes.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});