mapbox-gl
Version:
A WebGL interactive maps library
295 lines (245 loc) • 9.62 kB
JavaScript
'use strict';
const ShelfPack = require('@mapbox/shelf-pack');
const browser = require('../util/browser');
const util = require('../util/util');
const window = require('../util/window');
const Evented = require('../util/evented');
// The SpriteAtlas class is responsible for turning a sprite and assorted
// other images added at runtime into a texture that can be consumed by WebGL.
class SpriteAtlas extends Evented {
constructor(width, height) {
super();
this.width = width;
this.height = height;
this.shelfPack = new ShelfPack(width, height);
this.images = {};
this.data = false;
this.texture = 0; // WebGL ID
this.filter = 0; // WebGL ID
this.pixelRatio = 1;
this.dirty = true;
}
allocateImage(pixelWidth, pixelHeight) {
pixelWidth = pixelWidth / this.pixelRatio;
pixelHeight = pixelHeight / this.pixelRatio;
// Increase to next number divisible by 4, but at least 1.
// This is so we can scale down the texture coordinates and pack them
// into 2 bytes rather than 4 bytes.
// Pad icons to prevent them from polluting neighbours during linear interpolation
const padding = 2;
const packWidth = pixelWidth + padding + (4 - (pixelWidth + padding) % 4);
const packHeight = pixelHeight + padding + (4 - (pixelHeight + padding) % 4);// + 4;
const rect = this.shelfPack.packOne(packWidth, packHeight);
if (!rect) {
util.warnOnce('SpriteAtlas out of space.');
return null;
}
return rect;
}
addImage(name, pixels, options) {
let width, height, pixelRatio;
if (pixels instanceof window.HTMLImageElement) {
width = pixels.width;
height = pixels.height;
pixels = browser.getImageData(pixels);
pixelRatio = this.pixelRatio;
} else {
width = options.width;
height = options.height;
pixelRatio = options.pixelRatio || this.pixelRatio;
}
if (ArrayBuffer.isView(pixels)) {
pixels = new Uint32Array(pixels.buffer);
}
if (!(pixels instanceof Uint32Array)) {
return this.fire('error', {error: new Error('Image provided in an invalid format. Supported formats are HTMLImageElement, ImageData, and ArrayBufferView.')});
}
if (this.images[name]) {
return this.fire('error', {error: new Error('An image with this name already exists.')});
}
const rect = this.allocateImage(width, height);
if (!rect) {
return this.fire('error', {error: new Error('There is not enough space to add this image.')});
}
const image = {
rect,
width: width / pixelRatio,
height: height / pixelRatio,
sdf: false,
pixelRatio: 1
};
this.images[name] = image;
this.copy(pixels, width, rect, {pixelRatio, x: 0, y: 0, width, height}, false);
this.fire('data', {dataType: 'style'});
}
removeImage(name) {
const image = this.images[name];
delete this.images[name];
if (!image) {
return this.fire('error', {error: new Error('No image with this name exists.')});
}
this.shelfPack.unref(image.rect);
this.fire('data', {dataType: 'style'});
}
getImage(name, wrap) {
if (this.images[name]) {
return this.images[name];
}
if (!this.sprite) {
return null;
}
const pos = this.sprite.getSpritePosition(name);
if (!pos.width || !pos.height) {
return null;
}
const rect = this.allocateImage(pos.width, pos.height);
if (!rect) {
return null;
}
const image = {
rect,
width: pos.width / pos.pixelRatio,
height: pos.height / pos.pixelRatio,
sdf: pos.sdf,
pixelRatio: pos.pixelRatio / this.pixelRatio
};
this.images[name] = image;
if (!this.sprite.imgData) return null;
const srcImg = new Uint32Array(this.sprite.imgData.buffer);
this.copy(srcImg, this.sprite.width, rect, pos, wrap);
return image;
}
// Return position of a repeating fill pattern.
getPosition(name, repeating) {
const image = this.getImage(name, repeating);
const rect = image && image.rect;
if (!rect) {
return null;
}
const width = image.width * image.pixelRatio;
const height = image.height * image.pixelRatio;
const padding = 1;
return {
size: [image.width, image.height],
tl: [(rect.x + padding) / this.width, (rect.y + padding) / this.height],
br: [(rect.x + padding + width) / this.width, (rect.y + padding + height) / this.height]
};
}
allocate() {
if (!this.data) {
const w = Math.floor(this.width * this.pixelRatio);
const h = Math.floor(this.height * this.pixelRatio);
this.data = new Uint32Array(w * h);
for (let i = 0; i < this.data.length; i++) {
this.data[i] = 0;
}
}
}
// Copy some portion of srcImage into `SpriteAtlas#data`
copy(srcImg, srcImgWidth, dstPos, srcPos, wrap) {
this.allocate();
const dstImg = this.data;
const padding = 1;
copyBitmap(
/* source buffer */ srcImg,
/* source stride */ srcImgWidth,
/* source x */ srcPos.x,
/* source y */ srcPos.y,
/* dest buffer */ dstImg,
/* dest stride */ this.width * this.pixelRatio,
/* dest x */ (dstPos.x + padding) * this.pixelRatio,
/* dest y */ (dstPos.y + padding) * this.pixelRatio,
/* icon dimension */ srcPos.width,
/* icon dimension */ srcPos.height,
/* wrap */ wrap
);
// Indicates that `SpriteAtlas#data` has changed and needs to be
// reuploaded into the GL texture specified by `SpriteAtlas#texture`.
this.dirty = true;
}
setSprite(sprite) {
if (sprite) {
this.pixelRatio = browser.devicePixelRatio > 1 ? 2 : 1;
if (this.canvas) {
this.canvas.width = this.width * this.pixelRatio;
this.canvas.height = this.height * this.pixelRatio;
}
}
this.sprite = sprite;
}
addIcons(icons, callback) {
for (let i = 0; i < icons.length; i++) {
this.getImage(icons[i]);
}
callback(null, this.images);
}
bind(gl, linear) {
let first = false;
if (!this.texture) {
this.texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, this.texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
first = true;
} else {
gl.bindTexture(gl.TEXTURE_2D, this.texture);
}
const filterVal = linear ? gl.LINEAR : gl.NEAREST;
if (filterVal !== this.filter) {
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filterVal);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filterVal);
this.filter = filterVal;
}
if (this.dirty) {
this.allocate();
if (first) {
gl.texImage2D(
gl.TEXTURE_2D, // enum target
0, // ind level
gl.RGBA, // ind internalformat
this.width * this.pixelRatio, // GLsizei width
this.height * this.pixelRatio, // GLsizei height
0, // ind border
gl.RGBA, // enum format
gl.UNSIGNED_BYTE, // enum type
new Uint8Array(this.data.buffer) // Object data
);
} else {
gl.texSubImage2D(
gl.TEXTURE_2D, // enum target
0, // int level
0, // int xoffset
0, // int yoffset
this.width * this.pixelRatio, // long width
this.height * this.pixelRatio, // long height
gl.RGBA, // enum format
gl.UNSIGNED_BYTE, // enum type
new Uint8Array(this.data.buffer) // Object pixels
);
}
this.dirty = false;
}
}
}
module.exports = SpriteAtlas;
function copyBitmap(src, srcStride, srcX, srcY, dst, dstStride, dstX, dstY, width, height, wrap) {
let srcI = srcY * srcStride + srcX;
let dstI = dstY * dstStride + dstX;
let x, y;
if (wrap) {
// add 1 pixel wrapped padding on each side of the image
dstI -= dstStride;
for (y = -1; y <= height; y++, dstI += dstStride) {
srcI = ((y + height) % height + srcY) * srcStride + srcX;
for (x = -1; x <= width; x++) {
dst[dstI + x] = src[srcI + ((x + width) % width)];
}
}
} else {
for (y = 0; y < height; y++, srcI += srcStride, dstI += dstStride) {
for (x = 0; x < width; x++) {
dst[dstI + x] = src[srcI + x];
}
}
}
}