/** * PhotoSwipe Deep Zoom plugin * v1.1.2 * by Dmytro Semenov * https://github.com/dimsemenov/photoswipe-deep-zoom-plugin */ function getTileKey(x, y, z) { return x + '_' + y + '_' + z; } function slideIsTiled(slide) { if (slide.data.tileUrl) { return true; } return false; } const LOAD_STATE = { IDLE: 'idle', LOADING: 'loading', LOADED: 'loaded', ERROR: 'error', }; const DECODING_STATE = { IDLE: 'idle', DECODING: 'decoding', DECODED: 'decoded' }; class TileImage { constructor(url, x, y, z) { this.x = x; this.y = y; this.z = z; this.key = getTileKey(x, y, z); this.url = url; this.hasParent = false; // the Image has an Tile that uses it this.loadState = LOAD_STATE.IDLE; this.decodingState = DECODING_STATE.IDLE; this.styles = undefined; this._createImage(); } isLoaded() { return this.loadState === LOAD_STATE.LOADED; } isDecoded() { return this.decodingState === DECODING_STATE.DECODED; } setStyles(styles) { if (styles) { this.styles = styles; for(let name in this.styles) { this.imageElement.style[name] = this.styles[name]; } } } tileDetached() { this.hasParent = false; if (this.decodingState !== DECODING_STATE.DECODING) { this.decodingState = DECODING_STATE.IDLE; } } tileAttached() { this.hasParent = true; } _createImage() { this.imageElement = new Image(); this.imageElement.setAttribute('role', 'presentation'); this.imageElement.setAttribute('alt', ''); this.setStyles(this.styles); } decode() { if (this.loadState === LOAD_STATE.LOADED) { // if image is loaded and decoded, just exit if (this.decodingState === DECODING_STATE.DECODED) { this._onDecoded(); return; } // if image is decoding, just wait for it if (this.decodingState === DECODING_STATE.DECODING) { return; } // start decoding if (this.decodingState === DECODING_STATE.IDLE) { this._startDecode(); } } else if (this.loadState === LOAD_STATE.LOADING) { if (this.decodingState === DECODING_STATE.DECODING) { // if image is decoding, just wait for it return; } } else { // this.loadState === LOAD_STATE.IDLE || this.loadState === LOAD_STATE.ERROR // Image is not loaded yet, or needs a reload this._startDecode(); } } _startDecode() { if (this.loadState === LOAD_STATE.ERROR) { this._createImage(); } if (!this.imageElement.src) { this.imageElement.src = this.url; } this.decodingState = DECODING_STATE.DECODING; if (this.loadState !== LOAD_STATE.LOADED) { this.loadState = LOAD_STATE.LOADING; } if ('decode' in this.imageElement) { this.imageElement.decode().then(() => { this._onDecoded(); }).catch((error) => { this._onDecodeError(); }); } else { if (this.loadState === LOAD_STATE.LOADED) { this._onDecoded(); } else { this.imageElement.onload = () => { this._onDecoded(); }; this.imageElement.onerror = (e) => { this._onDecodeError(); }; } } } _onDecoded() { // If image has no parent, it won't be added to dom, // thus it's not guaranteed that it's decoded this.decodingState = this.hasParent ? DECODING_STATE.DECODED : DECODING_STATE.IDLE; this.loadState = LOAD_STATE.LOADED; if (this.onDecoded) { this.onDecoded(this); } } _onDecodeError() { this.decodingState = DECODING_STATE.IDLE; this.loadState = LOAD_STATE.ERROR; if (this.onError) { this.onError(this); } } destroy() { if (this.imageElement) { this.imageElement.onload = null; this.imageElement.onerror = null; this.imageElement = null; } this.onError = null; this.onDecoded = null; } } class Tile { constructor(x, y, z, tiler) { this.tiler = tiler; this.tiledLayer = tiler.getLayer(z); this.x = x; this.y = y; this.z = z; this.isPlaceholder = false; this.isInActiveLayer = false; this.isAttached = false; this.isDestroyed = false; this.isFullyDisplayed = false; } _initImage() { if (this.tileImage) { return; } const imageStyles = { position: 'absolute', left: 0, top: 0, width: 'auto', height: 'auto', pointerEvents: 'none', // This helps with scaling up images in safari imageRendering: '-webkit-optimize-contrast' }; imageStyles.willChange = 'transform'; // debug if (window.pswpDebug && window.pswpDebug.display_layer_borders) { let colors = ['red','blue','green','white','yellow','purple','black','orange','violet']; colors = colors.concat(colors).concat(colors).concat(colors).concat(colors); imageStyles.outline = 'solid 5px ' + colors[this.z]; imageStyles.outlineOffset = '-5px'; } // size const tileSize = this.tiler.getBaseTileWidth(this.z); const overlap = this.tiler.overlap; const xOffset = (this.x > 0 ? this.x * tileSize - overlap : 0); const yOffset = (this.y > 0 ? this.y * tileSize - overlap : 0); imageStyles.transform = 'translate(' + xOffset + 'px, ' + yOffset + 'px)'; this.tileImage = this.tiler.manager.decodingQueue.getOrCreateImage( this.tiler.getTileUrl(this.x, this.y, this.z), this.x, this.y, this.z ); if (this.isInActiveLayer && this.isPlaceholder) { this.tileImage.isLowRes = true; } this.tileImage.setStyles(imageStyles); } attach() { if (!this.isAttached) { this.isAttached = true; this.load(); } } detach() { if (this.isAttached) { this.tileImage.tileDetached(); if (this.tileImage.imageElement && this.tileImage.imageElement.parentNode) { this.tileImage.imageElement.remove(); } this.isAttached = false; this.isFading = false; this.isFullyDisplayed = false; } if (this.fadeRaf) { cancelAnimationFrame(this.fadeRaf); this.fadeRaf = null; } if (this.timeout) { clearTimeout(this.timeout); this.timeout = null; } } canBeDisplayed() { return this.tileImage && this.tileImage.loadState === LOAD_STATE.LOADED; } destroy() { this.detach(); this.placeholderTile = null; this.isDestroyed = true; } load() { this._initImage(); this.tileImage.tileAttached(); this.tileImage.onDecoded = () => { this._onImageDecoded(); }; this.tileImage.onError = () => { this._onImageError(); }; this.tiler.manager.decodingQueue.loadImage(this.tileImage); } _onImageDecoded(e) { this._addToDOM(); } _onImageError() { // remove tile image from cache this.tiler.manager.decodingQueue.cache.removeByKey(this.tileImage.key); } _addToDOM() { if (!this.isAttached) { // since decoding is async, it may happen after tile is detached return; } // todo const fadeInDuration = this.tiler.options.fadeInDuration; if (this.tileImage.imageElement.parentNode) { return; } if (!fadeInDuration || this.tiledLayer.isLowRes || this.isPlaceholder) { this.tileImage.imageElement.style.opacity = 1; this.tileImage.imageElement.style.transition = 'none'; this.tiledLayer.addTileToDOMWithRaf(this, () => { this.isFullyDisplayed = true; this.triggerDisplayed(); }); return; } this.isFading = true; this.tileImage.imageElement.style.opacity = 0; this.tileImage.imageElement.style.transition = 'opacity ' + fadeInDuration + 'ms linear'; this.tiledLayer.addTileToDOMWithRaf(this, () => { this.fadeRaf = requestAnimationFrame(() => { this.tileImage.imageElement.style.opacity = 1; this.timeout = setTimeout(() => { if (this.isAttached) { this.timeout = null; this.tileImage.imageElement.transition = 'none'; this.isFading = false; this.isFullyDisplayed = true; this.triggerDisplayed(); } }, fadeInDuration + 200); this.fadeRaf = null; }); }); } triggerDisplayed() { this.tiler.onTileDisplayed(this); } } class TileImagesCache { constructor(limit) { this.limit = limit; this._items = []; } /** * @param {TileImage} tileImage */ add(tileImage) { // no need to destroy, as we're just moving the item to the end of arr this.removeByKey(tileImage.key); this._items.push(tileImage); if (this._items.length > this.limit) { // Destroy the first image that has no parent and isn't from low-res layer const indexToRemove = this._items.findIndex(item => !item.hasParent && !item.isLowRes); if (indexToRemove !== -1) { let removedItem = this._items.splice(indexToRemove, 1)[0]; removedItem.destroy(); } } } /** * Removes an image from cache, does not destroy() it, just removes. * * @param {String} key */ removeByKey(key) { const indexToRemove = this._items.findIndex(item => item.key === key); if (indexToRemove !== -1) { this._items.splice(indexToRemove, 1); } } getByKey(key) { return this._items.find(tileImage => tileImage.key === key); } destroy() { this._items = null; } } class DecodingQueue { constructor(options) { this.images = []; this.maxDecodingCount = options.maxDecodingCount; this.minBatchRequestCount = options.minBatchRequestCount; this.cacheLimit = options.cacheLimit; this._imagesDecodingCount = 0; this.cache = new TileImagesCache(this.cacheLimit); } /** * @param {TileImage} tileImage */ cacheImage(tileImage) { if (tileImage.loadState !== LOAD_STATE.ERROR) { this.cache.add(tileImage); } } hasLoadedImage(x, y, z) { let image = this.getImage(x, y, z); if (image && image.loadState === LOAD_STATE.LOADED) { return true; } return false; } getImage(x, y, z) { let image = this.cache.getByKey( getTileKey(x, y, z) ); if (!image) { image = this.images.find((item) => { return item.x === x && item.y === y && item.z === z; }); } return image; } getOrCreateImage(url, x, y, z) { let image = this.getImage(x, y, z); if (!image) { image = new TileImage(url, x, y, z); } return image; } /** * * @param {TileImage} imag */ loadImage(image) { // The queue already contains this image if (this.images.includes(image)) { return; } this.images.push(image); if (!this._rafLoop) { this.refresh(); } } refresh() { this._imagesDecodingCount = 0; this._imagesLoadingCount = 0; this.images = this.images.filter((image) => { if (image.loadState === LOAD_STATE.ERROR) { // remove if loaded with an error return false; } if (image.loadState === LOAD_STATE.LOADED && image.decodingState === DECODING_STATE.DECODED) { // remove if image is fully loaded and decoded return false; } if (!image.hasParent && image.decodingState === DECODING_STATE.IDLE) { // image not started decoding yet, and has no parent Tile that is attached, // we can safely remove it from queue return false; } if (image.loadState === LOAD_STATE.LOADING) { this._imagesLoadingCount++; } if (image.decodingState === DECODING_STATE.DECODING) { this._imagesDecodingCount++; return true; } return true; }); // Decode images that were loaded before, // but then were removed, and now are added back this.images.forEach((image) => { if (image.hasParent && image.decodingState === DECODING_STATE.IDLE && image.loadState === LOAD_STATE.LOADED) { // Instantly decode low-res images ignoring max count if (this._imagesDecodingCount < this.maxDecodingCount || image.isLowRes) { this._decodeImage(image); } } }); // Make sure we run requests simultaneously if (this._imagesLoadingCount < this.minBatchRequestCount) { // This should send network requests to load images this.images.forEach((image) => { if (image.hasParent && image.decodingState !== DECODING_STATE.DECODING && this._imagesDecodingCount < this.maxDecodingCount) { this._decodeImage(image); } }); } if (this.images.length === 0) { this.stop(); } else { this._loop(); } } _decodeImage(image) { this._imagesDecodingCount++; image.decode(); this.cacheImage(image); } _loop() { this._rafLoop = requestAnimationFrame(() => { this.refresh(); }); } stop() { if (this._rafLoop) { cancelAnimationFrame(this._rafLoop); this._rafLoop = null; } } /** * Add to queue * * @param {Array} tiles */ add(tiles) { const tilesThatWereLoadedBefore = []; const activeLayerTiles = []; const otherTiles = []; // todo: make tiles load from center? tiles.forEach((tile) => { if (tile.imageLoaded || tile.imageLoading) { tilesThatWereLoadedBefore.push(tile); } else if(tile.isInActiveLayer) { activeLayerTiles.push(tile); } else { otherTiles.push(tile); } }); tilesThatWereLoadedBefore.sort(function(tile1, tile2) { return tile1.z - tile2.z; }); this.images = this.images .concat(tilesThatWereLoadedBefore) .concat(activeLayerTiles) .concat(otherTiles); } clear() { this.images = []; this.stop(); } destroy() { this.clear(); this.cache.destroy(); this.cache = undefined; this.images = []; } } class TilesManager { constructor(tiler) { this.tiler = tiler; this.tiles = {}; this.decodingQueue = new DecodingQueue(tiler.options); } getOrCreateTile(x, y, z) { const key = getTileKey(x, y, z); let tile = this.getTileByKey(key); if (!tile) { tile = new Tile(x, y, z, this.tiler); } this.tiles[key] = tile; return tile; } getTileByKey(key) { return this.tiles[key]; } getTile(x, y, z) { return this.getTileByKey( getTileKey(x, y, z) ); } _detachTile(x, y, z) { const key = getTileKey(x, y, z); if (this.tiles[key]) { this.tiles[key].detach(); delete this.tiles[key]; } } /** * Show tile below the main one * (it's generally displayed until the main tile is loaded) * * @param {Integer} x * @param {Integer} y * @param {Integer} z * @returns Tile|false */ showPlaceholderTileBelow(x, y, z) { x = Math.floor(x / 2); y = Math.floor(y / 2); z -= 1; if (z < 0) { return false; } const parentImage = this.decodingQueue.getImage(x, y, z); if (parentImage && parentImage.isLoaded()) { this.createPlaceholderTile(x, y, z); if (parentImage.isDecoded()) { return true; } } return this.showPlaceholderTileBelow(x, y, z); } createPlaceholderTile(x, y, z) { const tile = this.getOrCreateTile(x, y, z); tile.isPlaceholder = true; } showPlaceholderTilesAbove(x, y, z, maxZ) { z += 1; if (z > maxZ) { return false; } let visibleChildTilesCount = 0; for (let childX = x * 2; childX < (x * 2 + 2); childX++) { for (let childY = y * 2; childY < (y * 2 + 2); childY++) { let childImage = this.decodingQueue.getImage(childX, childY, z); if (childImage && childImage.isLoaded()) { this.createPlaceholderTile(childX, childY, z); if (childImage.isDecoded()) { visibleChildTilesCount++; } } } } // if all 4 tiles are visible - it means viewport is covered, // otherwise try to display a layer above: if (visibleChildTilesCount < 4) { for (let childX = x * 2; childX < (x * 2 + 2); childX++) { for (let childY = y * 2; childY < (y * 2 + 2); childY++) { this.showPlaceholderTilesAbove(childX, childY, z, maxZ); } } } } displayTiles() { let tilesToAttach = []; for(let key in this.tiles) { let tile = this.tiles[key]; if (tile.isInActiveLayer || tile.isPlaceholder) { if (tile.isAttached) ; else { tilesToAttach.push(tile); } } } const tilesThatWereLoadedBefore = []; const activeLayerTiles = []; const otherTiles = []; tilesToAttach.forEach((tile) => { if (tile.imageLoaded || tile.imageLoading) { tilesThatWereLoadedBefore.push(tile); } else if(tile.isInActiveLayer) { activeLayerTiles.push(tile); } else { otherTiles.push(tile); } }); tilesThatWereLoadedBefore.sort(function(tile1, tile2) { return tile1.z - tile2.z; }); tilesToAttach = []; tilesToAttach = tilesToAttach .concat(tilesThatWereLoadedBefore) .concat(activeLayerTiles) .concat(otherTiles); tilesToAttach.forEach((tile) => { tile.attach(); this.decodingQueue.cacheImage(tile.tileImage); }); } destroyUnusedTiles() { for(let key in this.tiles) { let tile = this.tiles[key]; if (!tile.isPlaceholder && !tile.isInActiveLayer) { this._detachTile(tile.x, tile.y, tile.z); } } } activeTilesLoaded() { for(let key in this.tiles) { let tile = this.tiles[key]; if (tile.isInActiveLayer && !tile.isFullyDisplayed && !tile.isFading) { return false; } } return true; } resetTilesRelations() { for(let key in this.tiles) { let tile = this.tiles[key]; tile.isPlaceholder = false; tile.isInActiveLayer = false; } } destroy() { for(let key in this.tiles) { let tile = this.tiles[key]; tile.destroy(); } this.decodingQueue.destroy(); //this.queue.destroy(); this.tiles = {}; } } function getTileCoordinateByPosition(pos, tileSize, maxTiles) { let tileCoordinate = Math.floor(pos / tileSize); tileCoordinate = Math.max(tileCoordinate, 0); tileCoordinate = Math.min(tileCoordinate, maxTiles - 1); return tileCoordinate; } class TiledLayer { /** * @param {Tiler} * @param {Integer} z In dzi, each layer (z) item corresponds to folder * @param {Number} scale Layer scale relative to the original (largest) size * (1 is original image) * @param {Number} originalWidth Total width of the layer (if none tiles are scaled) * @param {Number} originalHeight Total height of the layer * @param {Integer} numXTiles Number of horizontal tiles * @param {Integer} numYTiles Number of vertical tiles */ constructor(tiler, z, scale, originalWidth, originalHeight, numXTiles, numYTiles) { this.tiler = tiler; this.z = z; this.scale = scale; this.originalWidth = originalWidth; this.originalHeight = originalHeight; this.numXTiles = numXTiles; this.numYTiles = numYTiles; this.tileScale = 1; this.element = undefined; this.preventNewTiles = false; this.isActive = false; } activate() { if (!this.isActive) { this.isActive = true; if (!this.element) { this.element = document.createElement('div'); this.element.className = 'pswp__deepzoom-tiles-container'; this.element.style.position = 'absolute'; this.element.style.left = 0; this.element.dataset.z = this.z; this.element.style.top = 0; this.element.style.zIndex = this.z * 10 + 10; // todo: configurable zindex this.tiler.slide.container.appendChild(this.element); } } } addTileToDOMWithRaf(tile, onAdded) { requestAnimationFrame(() => { if (tile.isAttached && tile.tileImage.imageElement && !tile.tileImage.imageElement.parentNode) { this.element.appendChild(tile.tileImage.imageElement); if (onAdded) { onAdded(); } } }); } updateScale() { this.tileScale = this.tiler.width / this.originalWidth; if (this.element) { this.element.style.transform = 'scale('+this.tileScale+')'; } } updateTilesVisibility(isLowRes) { if (!this.isActive) { return []; } this.updateScale(); let tileCoordinatesToAttach = this.getTileCoordinatesInViewport(); // mark tiles to attach tileCoordinatesToAttach.forEach((coordinate) => { let tile = this.tiler.manager.getOrCreateTile(coordinate.x, coordinate.y, this.z); tile.isInActiveLayer = true; tile.isPlaceholder = isLowRes ? true : false; if (!isLowRes) { if (!tile.canBeDisplayed() || !tile.isFullyDisplayed) { this.tiler.manager.showPlaceholderTileBelow(tile.x, tile.y, tile.z); this.tiler.manager.showPlaceholderTilesAbove(tile.x, tile.y, tile.z, this.z + 1); } } }); } getTileCoordinatesInViewport() { const { slide } = this.tiler; const scale = slide.currZoomLevel / (slide.currentResolution || slide.zoomLevels.initial); const tileWidth = this.tiler.getBaseTileHeight(this.z) * this.tileScale * scale; const tileHeight = tileWidth; const tileLeft = slide.pan.x; const tileTop = slide.pan.y; const viewportRight = this.tiler.pswp.viewportSize.x; const viewportBottom = this.tiler.pswp.viewportSize.y; const leftTileX = getTileCoordinateByPosition(-tileLeft, tileWidth, this.numXTiles); const rightTileX = getTileCoordinateByPosition(viewportRight - tileLeft, tileWidth, this.numXTiles); const topTileY = getTileCoordinateByPosition(-tileTop, tileHeight, this.numYTiles); const bottomTileY = getTileCoordinateByPosition(viewportBottom - tileTop, tileHeight, this.numYTiles); //const tilesToAttach = []; const tileCoordinates = []; for(let y = topTileY; y <= bottomTileY; y++) { for(let x = leftTileX; x <= rightTileX; x++) { tileCoordinates.push({x, y, z: this.z}); } } return tileCoordinates; } destroy() { if (this.element) { this.element.remove(); this.element = undefined; } } } class Tiler { constructor(slide, options) { this.slide = slide; this.data = slide.data; this.pswp = slide.pswp; this.options = options; this.tileSize = this.data.tileSize || options.tileSize; this.tileType = this.data.tileType || this.options.tileType || 'deepzoom'; this.overlap = this.data.tileOverlap || options.tileOverlap || 0; this.maxWidth = this.data.maxWidth; this.maxHeight = this.data.maxHeight; if (this.options.maxTilePixelRatio > 1 && window.devicePixelRatio > 1) { this.tilePixelRatio = Math.min(window.devicePixelRatio, this.options.maxTilePixelRatio); } else { this.tilePixelRatio = 1; } this.layers = []; this.manager = new TilesManager(this); this.blockLoading = false; this.activeLayer = undefined; this.prevActiveLayer = undefined; this._prevProps = {}; if (this.tileType === 'deepzoom') { this.setupDeepzoomLayers(); } else if (this.tileType === 'zoomify') { this.setupZoomifyLayers(); } this.createLayers(); } setupDeepzoomLayers() { this.minZoomLayer = 0; this.maxZoomLayer = Math.ceil( Math.log( Math.max( this.maxWidth, this.maxHeight ) ) / Math.log( 2 ) ); } setupZoomifyLayers() { let imageWidth = this.maxWidth; let imageHeight = this.maxHeight; this._zoomifyLayers = []; this._addZoomifyLayer(imageWidth, imageHeight); this._totalZoomifyTilesCount = 0; while (imageWidth > this.tileSize || imageHeight > this.tileSize) { imageWidth = imageWidth / 2; imageHeight = imageHeight / 2; this._addZoomifyLayer(imageWidth, imageHeight); } this._zoomifyLayers.reverse(); this.minZoomLayer = 0; this.maxZoomLayer = this._zoomifyLayers.length - 1; } createLayers() { let scale; let width; let height; for(let i = this.minZoomLayer; i <= this.maxZoomLayer; i++) { scale = 1 / Math.pow(2, this.maxZoomLayer - i); width = Math.ceil(this.maxWidth * scale); height = Math.ceil(this.maxHeight * scale); this.layers.push(new TiledLayer( this, i, scale, width, height, Math.ceil(width / this.getBaseTileWidth(i)), Math.ceil(height / this.getBaseTileWidth(i)), )); } } getBaseTileWidth(z) { return this.tileSize; } getBaseTileHeight(z) { return this.tileSize; } setSize(width, height, forceUpdate, forceDelay) { const { slide } = this; const scale = slide.currZoomLevel / (slide.currentResolution || slide.zoomLevels.initial); if (scale !== 1) { // slide is animating / or zoom gesture is performed return; } // Size of image after it's zoomed this.width = width; this.height = height; let sizeChanged; if (width !== this._prevProps.width || height !== this._prevProps.height) { sizeChanged = true; } if (slide.pan.x !== this._prevProps.x || slide.pan.y !== this._prevProps.y) ; this._prevProps.width = width; this._prevProps.height = height; this._prevProps.x = slide.pan.x; this._prevProps.y = slide.pan.y; if (sizeChanged) { // Update right away if size is changed to sync with PhotoSwipe core this.updateSize(); if (this._updateSizeRaf) { cancelAnimationFrame(this._updateSizeRaf); this._updateSizeRaf = undefined; } return; } if (this._updateSizeRaf) { // update size is already scheduled, just wait return; } this._updateSizeRaf = requestAnimationFrame(() => { this._updateSizeRaf = undefined; this.updateSize(); }); } /** * Hide the primary image if viewer is zoomed beyond its size. * * @returns Boolean True if primary image is visible */ updatePrimaryImageVisibility() { if (this.slide.primaryImageWidth && this.width) { // Do not show tiles if image is smaller than "fit" zoom level if (this.width <= Math.round(this.pswp.currSlide.zoomLevels.fit * this.maxWidth)) { return true; } if (this.slide.primaryImageWidth / this.tilePixelRatio >= this.width) { return true; } } return false; } updateSize() { const useLowResLayer = this.options.useLowResLayer; this.manager.resetTilesRelations(); let lowResLayer; if (useLowResLayer) { lowResLayer = this.layers.find((layer) => { return layer.originalWidth >= this.tileSize || layer.originalHeight >= this.tileSize; }); } const primaryImageVisible = this.updatePrimaryImageVisibility(); if (primaryImageVisible) { this.manager.destroyUnusedTiles(); return; } // this.slide.image.style.display = 'none'; // Always display the most optimal layer let newActiveLayer = this.layers.find((layer) => { return (layer.originalWidth / this.tilePixelRatio) >= this.width; }); if (!newActiveLayer) { newActiveLayer = this.layers[this.layers.length - 1]; } this.activeLayer = newActiveLayer; this.layers.forEach((layer) => { layer.activate(); if (layer === this.activeLayer) { layer.updateTilesVisibility(); } else if (layer === lowResLayer) { layer.updateTilesVisibility(true); } else { layer.updateScale(); } }); // Destroy tiles even if loading is blocked, // as user can zoom in layer to ridiculous size this.manager.destroyUnusedTiles(); if (!this.blockLoading) { this.manager.displayTiles(); } } onTileDisplayed(tile) { this.setSize(this.width, this.height, false, true); } getLayer(z) { return this.layers.find((layer) => { return layer.z === z; }); } getTileUrl(x, y, z) { if (this.options.getTileUrlFn) { return this.options.getTileUrlFn(this.data, x, y, z); } switch(this.tileType) { case 'deepzoom': return this.getDeepzoomTileUrl(x, y, z); case 'zoomify': return this.getZoomifyTileUrl(x, y, z); default: return false; } } getDeepzoomTileUrl(x, y, z) { return (this.data.tileUrl || this.options.tileUrl) .replace('{x}', x) .replace('{y}', y) .replace('{z}', z); } getZoomifyTileUrl(x, y, z) { // Zoomify generator keeps up to 256 images per folder // based on the Openseadragon implementation https://github.com/openseadragon/openseadragon // find the absolute tile number let tileNumber = 0; for (let i = 0; i < z; i++) { tileNumber += this._zoomifyLayers[i].xTilesCount * this._zoomifyLayers[i].yTilesCount; } tileNumber += this._zoomifyLayers[z].xTilesCount * y + x; return (this.data.tileUrl || this.options.tileUrl) .replace('{zoomify_group}', Math.floor(tileNumber / 256)) .replace('{x}', x) .replace('{y}', y) .replace('{z}', z); } _addZoomifyLayer(layerWidth, layerHeight) { this._zoomifyLayers.push({ xTilesCount: Math.ceil(layerWidth / this.tileSize), yTilesCount: Math.ceil(layerHeight / this.tileSize) }); } destroy() { clearTimeout(this._setSizeTimeout); this._setSizeTimeout = undefined; this.layers.forEach((layer) => { layer.destroy(); }); this.manager.destroy(); if (this._updateLayersRaf) { cancelAnimationFrame(this._updateLayersRaf); } } } class DeepZoomUI { constructor(pswp, options) { this.pswp = pswp; this.options = options; pswp.on('uiRegister', () => { if (options.incrementalZoomButtons) { this.addButtons(); } }); pswp.on('imageClickAction', (e) => { if (slideIsTiled(pswp.currSlide)) { e.preventDefault(); this.incrementalZoomIn(e.point); } }); pswp.on('doubleTapAction', (e) => { if (slideIsTiled(pswp.currSlide)) { e.preventDefault(); this.incrementalZoomIn(e.point); } }); pswp.on('keydown', (e) => { const origEvent = e.originalEvent; let action; if (origEvent.keyCode === 187) { // = (+) action = 'ZoomIn'; } else if (origEvent.keyCode === 189) { // - action = 'ZoomOut'; } if (action && !origEvent.metaKey && !origEvent.altKey && !origEvent.ctrlKey) { e.preventDefault(); origEvent.preventDefault(); this['incremental' + action](false); } }); this.adjustPreloaderBehavior(); } addButtons() { this.pswp.ui.registerElement({ name: 'incrementalZoomIn', title: 'Zoom In', order: 10, isButton: true, html: { isCustomSVG: true, inner: '' + '' + '', outlineID: 'pswp__icn-incremental-zoom-in' }, onClick: (e, zoomInBtnElement) => { this.incrementalZoomIn(false); this.updateZoomInButtonState(zoomInBtnElement); }, onInit: (zoomInBtnElement) => { pswp.on('zoomPanUpdate', () => { this.updateZoomInButtonState(zoomInBtnElement); }); } }); this.pswp.ui.registerElement({ name: 'incrementalZoomOut', title: 'Zoom Out', order: 9, isButton: true, html: { isCustomSVG: true, inner: '' + '', outlineID: 'pswp__icn-incremental-zoom-out' }, onClick: (e, zoomInBtnElement) => { this.incrementalZoomOut(false); this.updateZoomOutButtonState(zoomInBtnElement); }, onInit: (zoomInBtnElement) => { pswp.on('zoomPanUpdate', () => { this.updateZoomOutButtonState(zoomInBtnElement); }); } }); this.pswp.ui.registerElement({ name: 'zoomToStart', title: 'Zoom to start position', order: 8, isButton: true, html: { isCustomSVG: true, inner: '', outlineID: 'pswp__icn-zoom-to-start' }, onClick: (e, zoomToStartBtnElement) => { this.zoomToStart(); this.updateZoomToStartButtonState(zoomToStartBtnElement); }, onInit: (zoomToStartBtnElement) => { pswp.on('zoomPanUpdate', () => { this.updateZoomToStartButtonState(zoomToStartBtnElement); }); } }); } /** * Return the closest layer scale * * @param {Number} scale */ getClosestLayerZoomLevel(scale) { const { tiler } = this.pswp.currSlide; if (!tiler) { return scale; } const layersScale = tiler.layers.map((layer) => layer.scale); const closestZoomLevel = layersScale.reduce((prev, curr) => { return ( Math.abs(curr - scale) < Math.abs(prev - scale) ? curr : prev ); }); return closestZoomLevel; } adjustPreloaderBehavior() { this.pswp.on('afterInit', () => { this.preloaderInterval = setInterval(() => { if (!document.hidden && pswp.ui.updatePreloaderVisibility) { pswp.ui.updatePreloaderVisibility(); } }, 500); }); this.pswp.addFilter('isContentLoading', (isLoading, content) => { if (!isLoading && content.slide && content.slide.tiler) { return !content.slide.tiler.manager.activeTilesLoaded(); } return isLoading; }); this.pswp.on('destroy', () => { if (this.preloaderInterval) { clearInterval(this.preloaderInterval); this.preloaderInterval = null; } }); } incrementalZoomIn(point) { const { tiler } = this.pswp.currSlide; let destZoomLevel; if (tiler) { destZoomLevel = this.pswp.currSlide.currZoomLevel * 2; const closestZoomLevel = this.getClosestLayerZoomLevel(destZoomLevel); if (closestZoomLevel > this.pswp.currSlide.currZoomLevel) { destZoomLevel = closestZoomLevel; } destZoomLevel = Math.min(destZoomLevel, this.pswp.currSlide.zoomLevels.secondary); } else { destZoomLevel = this.pswp.currSlide.zoomLevels.secondary; } this.pswp.zoomTo( destZoomLevel, point, this.pswp.options.zoomAnimationDuration ); } zoomToStart() { this.pswp.zoomTo( this.pswp.currSlide.zoomLevels.fit, false, this.pswp.options.zoomAnimationDuration ); } incrementalZoomOut(point) { const { tiler } = this.pswp.currSlide; let destZoomLevel; if (tiler) { destZoomLevel = this.pswp.currSlide.currZoomLevel / 2; const closestZoomLevel = this.getClosestLayerZoomLevel(destZoomLevel); if (closestZoomLevel < this.pswp.currSlide.currZoomLevel) { destZoomLevel = closestZoomLevel; } destZoomLevel = Math.max(destZoomLevel, this.pswp.currSlide.zoomLevels.initial); } else { destZoomLevel = this.pswp.currSlide.zoomLevels.initial; } this.pswp.zoomTo( destZoomLevel, point, this.pswp.options.zoomAnimationDuration ); } updateZoomInButtonState(el) { if (!this.pswp.currSlide.currZoomLevel || !this.pswp.currSlide.isZoomable() || this.pswp.currSlide.currZoomLevel >= this.pswp.currSlide.zoomLevels.secondary) { el.setAttribute('disabled', 'disabled'); } else { el.removeAttribute('disabled'); } } updateZoomOutButtonState(el) { if (!this.pswp.currSlide.currZoomLevel || !this.pswp.currSlide.isZoomable() || this.pswp.currSlide.currZoomLevel <= this.pswp.currSlide.zoomLevels.fit) { el.setAttribute('disabled', 'disabled'); } else { el.removeAttribute('disabled'); } } updateZoomToStartButtonState(el) { if (!this.pswp.currSlide.currZoomLevel || !this.pswp.currSlide.isZoomable() || this.pswp.currSlide.currZoomLevel <= this.pswp.currSlide.zoomLevels.initial * 3) { el.setAttribute('disabled', 'disabled'); el.style.display = 'none'; } else { el.removeAttribute('disabled'); el.style.display = 'block'; } } } const WHEEL_DEBOUNCE_DELAY = 85; // ms const defaultOptions = { fadeInDuration: 150, tileWidth: 256, tileOverlap: 0, incrementalZoomButtons: true, maxTilePixelRatio: 2, forceWillChange: true, cacheLimit: 200, maxDecodingCount: 15, minBatchRequestCount: 6 }; class PhotoSwipeDeepZoom { constructor(lightbox, options) { lightbox.on('init', () => { this.handlePhotoSwipeOpen(lightbox.pswp, options); }); } handlePhotoSwipeOpen(pswp, options) { this.pswp = pswp; this.options = { ...defaultOptions, ...options }; this.ui = new DeepZoomUI(pswp, this.options); pswp.on('itemData', (e) => { this.parseItemData(e.itemData); }); pswp.on('zoomLevelsUpdate', (e) => { if (e.slideData.tileUrl) { // Custom limit for the max zoom if (e.slideData.maxZoomWidth) { const maxWidth = e.slideData.maxZoomWidth; if (maxWidth) { const newMaxZoomLevel = maxWidth / e.zoomLevels.elementSize.x; e.zoomLevels.max = Math.max( e.zoomLevels.initial, newMaxZoomLevel ); } } // For incremental zoom buttons e.zoomLevels.secondary = e.zoomLevels.max; } }); pswp.on('slideInit', (e) => { if (slideIsTiled(e.slide)) { this._handleTiledSlideInit(e.slide); } }); pswp.on('slideActivate', (e) => { if (slideIsTiled(e.slide)) { this.createTiler(e.slide); } }); pswp.on('slideDeactivate', (e) => { if (slideIsTiled(e.slide)) { this.destroyTiler(e.slide); } }); pswp.on('slideDestroy', (e) => { if (slideIsTiled(e.slide)) { this.destroyTiler(e.slide); } }); pswp.on('appendHeavyContent', (e) => { if (slideIsTiled(e.slide)) { this._appendHeavyContent(e.slide); } }); pswp.on('zoomPanUpdate', (e) => { if (slideIsTiled(e.slide)) { this._handleZoomPanChange(e.slide); } }); pswp.on('imageSizeChange', (e) => { if (slideIsTiled(e.slide)) { this.updateTilerSize(e.slide); } }); pswp.on('change', () => { if (slideIsTiled(pswp.currSlide)) { this.updateTilerSize(pswp.currSlide); } }); pswp.on('loadComplete', (e) => { if (slideIsTiled(e.slide) && e.slide.tiler) { e.slide.tiler.updatePrimaryImageVisibility(); } }); // Block tile loading until wheel acion is finished // (to prevent unnessesary tile reuqests) this._wheelTimeout = undefined; pswp.on('wheel', (e) => { if (slideIsTiled(pswp.currSlide)) { if (pswp.currSlide.tiler) { pswp.currSlide.tiler.blockLoading = true; } if (this._wheelTimeout) { clearTimeout(this._wheelTimeout); } this._wheelTimeout = setTimeout(() => { pswp.currSlide.tiler.blockLoading = false; pswp.currSlide.tiler.updateSize(); this._wheelTimeout = undefined; }, WHEEL_DEBOUNCE_DELAY); } }); } createTiler(slide) { if (!slide.tiler) { slide.tiler = new Tiler(slide, this.options); } } destroyTiler(slide) { if (slide.tiler) { slide.tiler.destroy(); slide.tiler = undefined; if (slide.image) { slide.image.style.display = 'block'; } } } _handleTiledSlideInit(slide) { if (!slide.primaryImageWidth) { slide.primaryImageWidth = slide.width; slide.primaryImageHeight = slide.height; slide.width = slide.data.maxWidth; slide.height = slide.data.maxHeight; } } _appendHeavyContent(slide) { this.createTiler(slide); this.updateTilerSize(slide); } _handleZoomPanChange(slide) { if (slide.isActive && slide.tiler) { this.updateTilerSize(slide); } } updateTilerSize(slide) { const scaleMultiplier = slide.currentResolution || slide.zoomLevels.initial; if (slide.tiler && slide.isActive) { const slideImage = slide.content.element; if (slide.placeholder) { this._setImgStyles(slide.placeholder.element, 5); } const width = Math.round(slide.width * scaleMultiplier); const height = Math.round(slide.height * scaleMultiplier); if (slideImage) { this._setImgStyles(slideImage, 7); if (width >= slide.primaryImageWidth) { if (slideImage.srcset) { // adjust sizes attribute so it's based on primary image size, // and not based on full (tiled) size const prevSizes = parseInt(slideImage.sizes, 10); if (prevSizes >= slide.primaryImageWidth) { slideImage.sizes = slide.primaryImageWidth + 'px'; slideImage.dataset.largestUsedSize = width; } } // scale image instead of changing width/height slideImage.style.width = slide.primaryImageWidth + 'px'; slideImage.style.height = slide.primaryImageHeight + 'px'; const scale = width / slide.primaryImageWidth; slideImage.style.transform = 'scale3d('+scale+','+scale+',1)'; slideImage.style.transformOrigin = '0 0'; } else { slideImage.style.transform = 'none'; } } slide.tiler.setSize(width, height); } else { if (slide.image) { slide.image.style.transform = 'none'; } } } parseItemData(itemData) { const element = itemData.element; if (!element) { return; } const linkEl = element.tagName === 'A' ? element : element.querySelector('a'); if (!linkEl) { return; } if (linkEl.dataset.pswpTileUrl) { itemData.tileUrl = linkEl.dataset.pswpTileUrl; } if (linkEl.dataset.pswpTileType) { itemData.tileType = linkEl.dataset.pswpTileType; } if (linkEl.dataset.pswpTileSize) { itemData.tileSize = parseInt(linkEl.dataset.pswpTileSize, 10); } if (linkEl.dataset.pswpMaxWidth) { itemData.maxWidth = parseInt(linkEl.dataset.pswpMaxWidth, 10); } if (linkEl.dataset.pswpMaxZoomWidth) { itemData.maxZoomWidth = parseInt(linkEl.dataset.pswpMaxZoomWidth, 10); } if (linkEl.dataset.pswpMaxHeight) { itemData.maxHeight = parseInt(linkEl.dataset.pswpMaxHeight, 10); } itemData.tileOverlap = parseInt(linkEl.dataset.pswpTileOverlap, 10) || 0; } _setImgStyles(el, zIndex) { if (el && el.tagName === 'IMG') { el.style.zIndex = zIndex; if (this.options.forceWillChange) { el.style.willChange = 'transform'; } } } } export { PhotoSwipeDeepZoom as default };