// @flow import {RGBAImage} from '../util/image'; import {register} from '../util/web_worker_transfer'; import potpack from 'potpack'; import type {StyleImage} from '../style/style_image'; import type ImageManager from './image_manager'; import type Texture from './texture'; const IMAGE_PADDING: number = 1; export {IMAGE_PADDING}; type Rect = { x: number, y: number, w: number, h: number }; export class ImagePosition { paddedRect: Rect; pixelRatio: number; version: number; stretchY: ?Array<[number, number]>; stretchX: ?Array<[number, number]>; content: ?[number, number, number, number]; constructor(paddedRect: Rect, {pixelRatio, version, stretchX, stretchY, content}: StyleImage) { this.paddedRect = paddedRect; this.pixelRatio = pixelRatio; this.stretchX = stretchX; this.stretchY = stretchY; this.content = content; this.version = version; } get tl(): [number, number] { return [ this.paddedRect.x + IMAGE_PADDING, this.paddedRect.y + IMAGE_PADDING ]; } get br(): [number, number] { return [ this.paddedRect.x + this.paddedRect.w - IMAGE_PADDING, this.paddedRect.y + this.paddedRect.h - IMAGE_PADDING ]; } get tlbr(): Array { return this.tl.concat(this.br); } get displaySize(): [number, number] { return [ (this.paddedRect.w - IMAGE_PADDING * 2) / this.pixelRatio, (this.paddedRect.h - IMAGE_PADDING * 2) / this.pixelRatio ]; } } export default class ImageAtlas { image: RGBAImage; iconPositions: {[_: string]: ImagePosition}; patternPositions: {[_: string]: ImagePosition}; haveRenderCallbacks: Array; uploaded: ?boolean; constructor(icons: {[_: string]: StyleImage}, patterns: {[_: string]: StyleImage}) { const iconPositions = {}, patternPositions = {}; this.haveRenderCallbacks = []; const bins = []; this.addImages(icons, iconPositions, bins); this.addImages(patterns, patternPositions, bins); const {w, h} = potpack(bins); const image = new RGBAImage({width: w || 1, height: h || 1}); for (const id in icons) { const src = icons[id]; const bin = iconPositions[id].paddedRect; RGBAImage.copy(src.data, image, {x: 0, y: 0}, {x: bin.x + IMAGE_PADDING, y: bin.y + IMAGE_PADDING}, src.data); } for (const id in patterns) { const src = patterns[id]; const bin = patternPositions[id].paddedRect; const x = bin.x + IMAGE_PADDING, y = bin.y + IMAGE_PADDING, w = src.data.width, h = src.data.height; RGBAImage.copy(src.data, image, {x: 0, y: 0}, {x, y}, src.data); // Add 1 pixel wrapped padding on each side of the image. RGBAImage.copy(src.data, image, {x: 0, y: h - 1}, {x, y: y - 1}, {width: w, height: 1}); // T RGBAImage.copy(src.data, image, {x: 0, y: 0}, {x, y: y + h}, {width: w, height: 1}); // B RGBAImage.copy(src.data, image, {x: w - 1, y: 0}, {x: x - 1, y}, {width: 1, height: h}); // L RGBAImage.copy(src.data, image, {x: 0, y: 0}, {x: x + w, y}, {width: 1, height: h}); // R } this.image = image; this.iconPositions = iconPositions; this.patternPositions = patternPositions; } addImages(images: {[_: string]: StyleImage}, positions: {[_: string]: ImagePosition}, bins: Array) { for (const id in images) { const src = images[id]; const bin = { x: 0, y: 0, w: src.data.width + 2 * IMAGE_PADDING, h: src.data.height + 2 * IMAGE_PADDING, }; bins.push(bin); positions[id] = new ImagePosition(bin, src); if (src.hasRenderCallback) { this.haveRenderCallbacks.push(id); } } } patchUpdatedImages(imageManager: ImageManager, texture: Texture) { imageManager.dispatchRenderCallbacks(this.haveRenderCallbacks); for (const name in imageManager.updatedImages) { this.patchUpdatedImage(this.iconPositions[name], imageManager.getImage(name), texture); this.patchUpdatedImage(this.patternPositions[name], imageManager.getImage(name), texture); } } patchUpdatedImage(position: ?ImagePosition, image: ?StyleImage, texture: Texture) { if (!position || !image) return; if (position.version === image.version) return; position.version = image.version; const [x, y] = position.tl; texture.update(image.data, undefined, {x, y}); } } register('ImagePosition', ImagePosition); register('ImageAtlas', ImageAtlas);