UNPKG

maplibre-gl

Version:

BSD licensed community fork of mapbox-gl, a WebGL interactive maps library

145 lines (121 loc) 4.98 kB
/* eslint-disable key-spacing */ import potpack from 'potpack'; import {RGBAImage} from '../util/image.ts'; import {ImagePosition} from './image_atlas.ts'; import {Texture} from '../webgl/texture.ts'; import type {ImageManager} from './image_manager.ts'; import type {StyleImage} from '../style/style_image.ts'; import type {Context} from '../webgl/context.ts'; import type {PotpackBox} from 'potpack'; type Pattern = { bin: PotpackBox; position: ImagePosition; /** * The image this entry was packed from. Kept to notice when an image was removed and added * again under the same id, in which case the entry has to be rebuilt even though the version * of the new image may happen to match the one of the old. */ image: StyleImage; }; /** * When copied into the atlas texture, pattern images are padded by one pixel on each side with a * copy of the image data wrapped from the opposite side. This ensures the correct behavior of * GL_LINEAR texture sampling mode. */ const PADDING = 1; /** * A texture atlas of the pattern images that are drawn without a tile's own {@link ImageAtlas}, * i.e. `background-pattern`. Entries are packed on demand and re-packed whenever one of them is * added or changes, which is rare enough for the repacking cost not to matter. */ export class PatternAtlas { private _imageManager: ImageManager; private _entries: Record<string, Pattern>; private _image: RGBAImage; private _texture: Texture; private _dirty: boolean; constructor(imageManager: ImageManager) { this._imageManager = imageManager; this._entries = {}; this._image = new RGBAImage({width: 1, height: 1}); this._dirty = true; } destroy(): void { if (this._texture) { this._texture.destroy(); this._texture = null; } this._entries = {}; this._image = new RGBAImage({width: 1, height: 1}); this._dirty = true; } getPixelSize(): {width: number; height: number} { const {width, height} = this._image; return {width, height}; } /** * @returns the position of the pattern within the atlas, or `null` if there is no such image */ getPattern(id: string): ImagePosition { const image = this._imageManager.getImage(id); if (!image) { return null; } const entry = this._entries[id]; if (entry?.image !== image) { // there is no entry yet, or the image was replaced by another one under the same id, // in which case the slot has to be packed anew - the new image may have another size const w = image.data.width + PADDING * 2; const h = image.data.height + PADDING * 2; const bin = {w, h, x: 0, y: 0}; this._entries[id] = {bin, position: new ImagePosition(bin, image), image}; } else if (entry.position.version !== image.version) { entry.position.version = image.version; } else { return entry.position; } this._update(); return this._entries[id].position; } bind(context: Context): void { const gl = context.gl; if (!this._texture) { this._texture = new Texture(context, this._image, gl.RGBA); this._dirty = false; } else if (this._dirty) { this._texture.update(this._image); this._dirty = false; } this._texture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); } private _update(): void { // drop the entries whose image is gone - they hold on to a slot of the atlas for nothing for (const id in this._entries) { if (!this._imageManager.getImage(id)) { delete this._entries[id]; } } const bins = []; for (const id in this._entries) { bins.push(this._entries[id].bin); } const {w, h} = potpack(bins); const dst = this._image; dst.resize({width: w || 1, height: h || 1}); for (const id in this._entries) { const {bin} = this._entries[id]; const x = bin.x + PADDING; const y = bin.y + PADDING; const src = this._entries[id].image.data; const w = src.width; const h = src.height; RGBAImage.copy(src, dst, {x: 0, y: 0}, {x, y}, {width: w, height: h}); // Add 1 pixel wrapped padding on each side of the image. RGBAImage.copy(src, dst, {x: 0, y: h - 1}, {x, y: y - 1}, {width: w, height: 1}); // T RGBAImage.copy(src, dst, {x: 0, y: 0}, {x, y: y + h}, {width: w, height: 1}); // B RGBAImage.copy(src, dst, {x: w - 1, y: 0}, {x: x - 1, y}, {width: 1, height: h}); // L RGBAImage.copy(src, dst, {x: 0, y: 0}, {x: x + w, y}, {width: 1, height: h}); // R } this._dirty = true; } }