react-pannellum-next
Version:
The `PanoramaViewer` component is a React component that provides a panoramic image viewer using the Pannellum library. It allows you to display a 360-degree image and add interactive hotspots to it.
902 lines (897 loc) • 221 kB
JavaScript
require("./index.css");
var $b4te3$react = require("react");
function $parcel$export(e, n, v, s) {
Object.defineProperty(e, n, {get: v, set: s, enumerable: true, configurable: true});
}
function $parcel$interopDefault(a) {
return a && a.__esModule ? a.default : a;
}
$parcel$export(module.exports, "PanoramaViewer", function () { return $332b06753a23f2ac$export$2e2bcd8739ae039; });
/*
* libpannellum - A WebGL and CSS 3D transform based Panorama Renderer
* Copyright (c) 2012-2022 Matthew Petroff
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/ window.libpannellum = function(window1, document1, undefined) {
"use strict";
/**
* Creates a new panorama renderer.
* @constructor
* @param {HTMLElement} container - The container element for the renderer.
* @param {WebGLRenderingContext} [context] - Existing WebGL context (instead of container).
*/ function Renderer(container, context) {
var canvas;
if (container) {
canvas = document1.createElement("canvas");
canvas.style.width = canvas.style.height = "100%";
container.appendChild(canvas);
}
var program, gl, vs, fs;
var previewProgram, previewVs, previewFs;
var fallbackImgSize;
var world;
var vtmps;
var pose;
var image, imageType;
var texCoordBuffer, cubeVertBuf, cubeVertTexCoordBuf, cubeVertIndBuf;
var globalParams;
var sides = [
"f",
"b",
"u",
"d",
"l",
"r"
];
var fallbackSides = [
"f",
"r",
"b",
"l",
"u",
"d"
];
if (context) gl = context;
/**
* Initialize renderer.
* @memberof Renderer
* @instance
* @param {Image|Array|Object} image - Input image; format varies based on
* `imageType`. For `equirectangular`, this is an image; for
* `cubemap`, this is an array of images for the cube faces in the
* order [+z, +x, -z, -x, +y, -y]; for `multires`, this is a
* configuration object.
* @param {string} imageType - The type of the image: `equirectangular`,
* `cubemap`, or `multires`.
* @param {number} haov - Initial horizontal angle of view.
* @param {number} vaov - Initial vertical angle of view.
* @param {number} voffset - Initial vertical offset angle.
* @param {function} callback - Load callback function.
* @param {Object} [params] - Other configuration parameters (`horizonPitch`, `horizonRoll`, `backgroundColor`).
*/ this.init = function(_image, _imageType, haov, vaov, voffset, callback, params) {
// Default argument for image type
if (_imageType === undefined) _imageType = "equirectangular";
if (_imageType != "equirectangular" && _imageType != "cubemap" && _imageType != "multires") {
console.log("Error: invalid image type specified!");
throw {
type: "config error"
};
}
imageType = _imageType;
image = _image;
globalParams = params || {};
// Clear old data
if (program) {
if (vs) {
gl.detachShader(program, vs);
gl.deleteShader(vs);
}
if (fs) {
gl.detachShader(program, fs);
gl.deleteShader(fs);
}
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
if (program.texture) gl.deleteTexture(program.texture);
if (program.nodeCache) for(var i = 0; i < program.nodeCache.length; i++)gl.deleteTexture(program.nodeCache[i].texture);
if (program.textureLoads) {
pendingTextureRequests = [];
while(program.textureLoads.length > 0)program.textureLoads.shift()(false);
}
gl.deleteProgram(program);
program = undefined;
}
if (previewProgram) {
if (previewVs) {
gl.detachShader(previewProgram, previewVs);
gl.deleteShader(previewVs);
}
if (previewFs) {
gl.detachShader(previewProgram, previewFs);
gl.deleteShader(previewFs);
}
gl.deleteProgram(previewProgram);
previewProgram = undefined;
}
pose = undefined;
var s;
var faceMissing = false;
var cubeImgWidth;
if (imageType == "cubemap") {
for(s = 0; s < 6; s++)if (image[s].width > 0) {
if (cubeImgWidth === undefined) cubeImgWidth = image[s].width;
if (cubeImgWidth != image[s].width) console.log("Cube faces have inconsistent widths: " + cubeImgWidth + " vs. " + image[s].width);
} else faceMissing = true;
}
function fillMissingFaces(imgSize) {
if (faceMissing) {
var nbytes = imgSize * imgSize * 4; // RGB, plus non-functional alpha
var imageArray = new Uint8ClampedArray(nbytes);
var rgb = params.backgroundColor ? params.backgroundColor : [
0,
0,
0
];
rgb[0] *= 255;
rgb[1] *= 255;
rgb[2] *= 255;
// Maybe filling could be done faster, see e.g., https://stackoverflow.com/questions/1295584/most-efficient-way-to-create-a-zero-filled-javascript-array
for(var i = 0; i < nbytes; i++){
imageArray[i++] = rgb[0];
imageArray[i++] = rgb[1];
imageArray[i++] = rgb[2];
}
var backgroundSquare = new ImageData(imageArray, imgSize, imgSize);
for(s = 0; s < 6; s++)if (image[s].width == 0) image[s] = backgroundSquare;
}
}
// This awful browser specific test exists because iOS 8/9 and IE 11
// don't display non-power-of-two cubemap textures but also don't
// throw an error (tested on an iPhone 5c / iOS 8.1.3 / iOS 9.2 /
// iOS 10.3.1).
// Therefore, the WebGL context is never created for these browsers for
// NPOT cubemaps, and the CSS 3D transform fallback renderer is used
// instead.
if (!(imageType == "cubemap" && (cubeImgWidth & cubeImgWidth - 1) !== 0 && (navigator.userAgent.toLowerCase().match(/(iphone|ipod|ipad).* os 8_/) || navigator.userAgent.toLowerCase().match(/(iphone|ipod|ipad).* os 9_/) || navigator.userAgent.toLowerCase().match(/(iphone|ipod|ipad).* os 10_/) || navigator.userAgent.match(/Trident.*rv[ :]*11\./)))) {
// Enable WebGL on canvas
if (!gl) gl = canvas.getContext("experimental-webgl", {
alpha: false,
depth: false
});
if (gl && gl.getError() == 1286) handleWebGLError1286();
}
// If there is no WebGL, fall back to CSS 3D transform renderer.
// This will discard the image loaded so far and load the fallback image.
// While browser specific tests are usually frowned upon, the
// fallback viewer only really works with WebKit/Blink and IE 10/11
// (it doesn't work properly in Firefox).
if (!gl && (imageType == "multires" && image.hasOwnProperty("fallbackPath") || imageType == "cubemap") && ("WebkitAppearance" in document1.documentElement.style || navigator.userAgent.match(/Trident.*rv[ :]*11\./) || navigator.appVersion.indexOf("MSIE 10") !== -1)) {
// Remove old world if it exists
if (world) container.removeChild(world);
// Initialize renderer
world = document1.createElement("div");
world.className = "pnlm-world";
// Add images
var path;
if (image.basePath) path = image.basePath + image.fallbackPath;
else path = image.fallbackPath;
var loaded = 0;
var onLoad = function() {
// Draw image on canvas
var faceCanvas = document1.createElement("canvas");
faceCanvas.className = "pnlm-face pnlm-" + fallbackSides[this.side] + "face";
world.appendChild(faceCanvas);
var faceContext = faceCanvas.getContext("2d");
faceCanvas.style.width = this.width + 4 + "px";
faceCanvas.style.height = this.height + 4 + "px";
faceCanvas.width = this.width + 4;
faceCanvas.height = this.height + 4;
faceContext.drawImage(this, 2, 2);
var imgData = faceContext.getImageData(0, 0, faceCanvas.width, faceCanvas.height);
var data = imgData.data;
// Duplicate edge pixels
var i;
var j;
for(i = 2; i < faceCanvas.width - 2; i++)for(j = 0; j < 4; j++){
data[(i + faceCanvas.width) * 4 + j] = data[(i + faceCanvas.width * 2) * 4 + j];
data[(i + faceCanvas.width * (faceCanvas.height - 2)) * 4 + j] = data[(i + faceCanvas.width * (faceCanvas.height - 3)) * 4 + j];
}
for(i = 2; i < faceCanvas.height - 2; i++)for(j = 0; j < 4; j++){
data[(i * faceCanvas.width + 1) * 4 + j] = data[(i * faceCanvas.width + 2) * 4 + j];
data[((i + 1) * faceCanvas.width - 2) * 4 + j] = data[((i + 1) * faceCanvas.width - 3) * 4 + j];
}
for(j = 0; j < 4; j++){
data[(faceCanvas.width + 1) * 4 + j] = data[(faceCanvas.width * 2 + 2) * 4 + j];
data[(faceCanvas.width * 2 - 2) * 4 + j] = data[(faceCanvas.width * 3 - 3) * 4 + j];
data[(faceCanvas.width * (faceCanvas.height - 2) + 1) * 4 + j] = data[(faceCanvas.width * (faceCanvas.height - 3) + 2) * 4 + j];
data[(faceCanvas.width * (faceCanvas.height - 1) - 2) * 4 + j] = data[(faceCanvas.width * (faceCanvas.height - 2) - 3) * 4 + j];
}
for(i = 1; i < faceCanvas.width - 1; i++)for(j = 0; j < 4; j++){
data[i * 4 + j] = data[(i + faceCanvas.width) * 4 + j];
data[(i + faceCanvas.width * (faceCanvas.height - 1)) * 4 + j] = data[(i + faceCanvas.width * (faceCanvas.height - 2)) * 4 + j];
}
for(i = 1; i < faceCanvas.height - 1; i++)for(j = 0; j < 4; j++){
data[i * faceCanvas.width * 4 + j] = data[(i * faceCanvas.width + 1) * 4 + j];
data[((i + 1) * faceCanvas.width - 1) * 4 + j] = data[((i + 1) * faceCanvas.width - 2) * 4 + j];
}
for(j = 0; j < 4; j++){
data[j] = data[(faceCanvas.width + 1) * 4 + j];
data[(faceCanvas.width - 1) * 4 + j] = data[(faceCanvas.width * 2 - 2) * 4 + j];
data[faceCanvas.width * (faceCanvas.height - 1) * 4 + j] = data[(faceCanvas.width * (faceCanvas.height - 2) + 1) * 4 + j];
data[(faceCanvas.width * faceCanvas.height - 1) * 4 + j] = data[(faceCanvas.width * (faceCanvas.height - 1) - 2) * 4 + j];
}
// Draw image width duplicated edge pixels on canvas
faceContext.putImageData(imgData, 0, 0);
incLoaded.call(this);
};
var incLoaded = function() {
if (this.width > 0) {
if (fallbackImgSize === undefined) fallbackImgSize = this.width;
if (fallbackImgSize != this.width) console.log("Fallback faces have inconsistent widths: " + fallbackImgSize + " vs. " + this.width);
} else faceMissing = true;
loaded++;
if (loaded == 6) {
fallbackImgSize = this.width;
container.appendChild(world);
callback();
}
};
faceMissing = false;
for(s = 0; s < 6; s++){
var faceImg = new Image();
faceImg.crossOrigin = globalParams.crossOrigin ? globalParams.crossOrigin : "anonymous";
faceImg.side = s;
faceImg.onload = onLoad;
faceImg.onerror = incLoaded; // ignore missing face to support partial fallback image
if (imageType == "multires") faceImg.src = path.replace("%s", fallbackSides[s]) + (image.extension ? "." + image.extension : "");
else faceImg.src = image[s].src;
}
fillMissingFaces(fallbackImgSize);
return;
} else if (!gl) {
console.log("Error: no WebGL support detected!");
throw {
type: "no webgl"
};
}
if (imageType == "cubemap") fillMissingFaces(cubeImgWidth);
if (image.basePath) image.fullpath = image.basePath + image.path;
else image.fullpath = image.path;
image.invTileResolution = 1 / image.tileResolution;
var vertices = createCube();
vtmps = [];
for(s = 0; s < 6; s++){
vtmps[s] = vertices.slice(s * 12, s * 12 + 12);
vertices = createCube();
}
// Make sure image isn't too big
var maxWidth = 0;
if (imageType == "equirectangular") {
maxWidth = gl.getParameter(gl.MAX_TEXTURE_SIZE);
if (Math.max(image.width / 2, image.height) > maxWidth) {
console.log("Error: The image is too big; it's " + image.width + "px wide, " + "but this device's maximum supported size is " + maxWidth * 2 + "px.");
throw {
type: "webgl size error",
width: image.width,
maxWidth: maxWidth * 2
};
}
} else if (imageType == "cubemap") {
if (cubeImgWidth > gl.getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE)) {
console.log("Error: The image is too big; it's " + cubeImgWidth + "px wide, " + "but this device's maximum supported size is " + maxWidth + "px.");
throw {
type: "webgl size error",
width: cubeImgWidth,
maxWidth: maxWidth
};
}
}
// Store horizon pitch and roll if applicable
if (params !== undefined) {
var horizonPitch = isNaN(params.horizonPitch) ? 0 : Number(params.horizonPitch), horizonRoll = isNaN(params.horizonRoll) ? 0 : Number(params.horizonRoll);
if (horizonPitch != 0 || horizonRoll != 0) pose = [
horizonPitch,
horizonRoll
];
}
// Set 2d texture binding
var glBindType = gl.TEXTURE_2D;
// Create viewport for entire canvas
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
// Check precision support
if (gl.getShaderPrecisionFormat) {
var precision = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT);
if (precision && precision.precision < 1) // `highp` precision not supported; https://stackoverflow.com/a/33308927
fragEquiCubeBase = fragEquiCubeBase.replace("highp", "mediump");
}
// Create vertex shader
vs = gl.createShader(gl.VERTEX_SHADER);
var vertexSrc = v;
if (imageType == "multires") vertexSrc = vMulti;
gl.shaderSource(vs, vertexSrc);
gl.compileShader(vs);
// Create fragment shader
fs = gl.createShader(gl.FRAGMENT_SHADER);
var fragmentSrc = fragEquirectangular;
if (imageType == "cubemap") {
glBindType = gl.TEXTURE_CUBE_MAP;
fragmentSrc = fragCube;
} else if (imageType == "multires") fragmentSrc = fragMulti;
gl.shaderSource(fs, fragmentSrc);
gl.compileShader(fs);
// Link WebGL program
program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
// Log errors
if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS)) console.log(gl.getShaderInfoLog(vs));
if (!gl.getShaderParameter(fs, gl.COMPILE_STATUS)) console.log(gl.getShaderInfoLog(fs));
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) console.log(gl.getProgramInfoLog(program));
// Use WebGL program
gl.useProgram(program);
program.drawInProgress = false;
// Set background clear color (does not apply to cubemap/fallback image)
if (params.backgroundColor !== null) {
var color = params.backgroundColor ? params.backgroundColor : [
0,
0,
0
];
gl.clearColor(color[0], color[1], color[2], 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
}
// Look up texture coordinates location
program.texCoordLocation = gl.getAttribLocation(program, "a_texCoord");
gl.enableVertexAttribArray(program.texCoordLocation);
if (imageType != "multires") {
// Provide texture coordinates for rectangle
if (!texCoordBuffer) texCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-1,
1,
1,
1,
1,
-1,
-1,
1,
1,
-1,
-1,
-1
]), gl.STATIC_DRAW);
gl.vertexAttribPointer(program.texCoordLocation, 2, gl.FLOAT, false, 0, 0);
// Pass aspect ratio
program.aspectRatio = gl.getUniformLocation(program, "u_aspectRatio");
gl.uniform1f(program.aspectRatio, gl.drawingBufferWidth / gl.drawingBufferHeight);
// Locate psi, theta, focal length, horizontal extent, vertical extent, and vertical offset
program.psi = gl.getUniformLocation(program, "u_psi");
program.theta = gl.getUniformLocation(program, "u_theta");
program.f = gl.getUniformLocation(program, "u_f");
program.h = gl.getUniformLocation(program, "u_h");
program.v = gl.getUniformLocation(program, "u_v");
program.vo = gl.getUniformLocation(program, "u_vo");
program.rot = gl.getUniformLocation(program, "u_rot");
// Pass horizontal extent, vertical extent, and vertical offset
gl.uniform1f(program.h, haov / (Math.PI * 2.0));
gl.uniform1f(program.v, vaov / Math.PI);
gl.uniform1f(program.vo, voffset / Math.PI * 2);
// Set background color
if (imageType == "equirectangular") {
program.backgroundColor = gl.getUniformLocation(program, "u_backgroundColor");
gl.uniform4fv(program.backgroundColor, color.concat([
1
]));
}
// Create texture
program.texture = gl.createTexture();
gl.bindTexture(glBindType, program.texture);
// Upload images to texture depending on type
if (imageType == "cubemap") {
// Load all six sides of the cube map
gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image[1]);
gl.texImage2D(gl.TEXTURE_CUBE_MAP_NEGATIVE_X, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image[3]);
gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_Y, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image[4]);
gl.texImage2D(gl.TEXTURE_CUBE_MAP_NEGATIVE_Y, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image[5]);
gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_Z, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image[0]);
gl.texImage2D(gl.TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image[2]);
} else if (image.width <= maxWidth) {
gl.uniform1i(gl.getUniformLocation(program, "u_splitImage"), 0);
// Upload image to the texture
gl.texImage2D(glBindType, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);
} else {
// Image needs to be split into two parts due to texture size limits
gl.uniform1i(gl.getUniformLocation(program, "u_splitImage"), 1);
// Draw image on canvas
var cropCanvas = document1.createElement("canvas");
cropCanvas.width = image.width / 2;
cropCanvas.height = image.height;
var cropContext = cropCanvas.getContext("2d");
cropContext.drawImage(image, 0, 0);
// Upload first half of image to the texture
var cropImage = cropContext.getImageData(0, 0, image.width / 2, image.height);
gl.texImage2D(glBindType, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, cropImage);
// Create and bind texture for second half of image
program.texture2 = gl.createTexture();
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(glBindType, program.texture2);
gl.uniform1i(gl.getUniformLocation(program, "u_image1"), 1);
// Upload second half of image to the texture
cropContext.drawImage(image, -image.width / 2, 0);
cropImage = cropContext.getImageData(0, 0, image.width / 2, image.height);
gl.texImage2D(glBindType, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, cropImage);
// Set parameters for rendering any size
gl.texParameteri(glBindType, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(glBindType, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(glBindType, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(glBindType, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
// Reactivate first texture unit
gl.activeTexture(gl.TEXTURE0);
}
// Set parameters for rendering any size
if (imageType != "cubemap" && image.width && image.width <= maxWidth && haov == 2 * Math.PI && (image.width & image.width - 1) == 0) gl.texParameteri(glBindType, gl.TEXTURE_WRAP_S, gl.REPEAT); // Only supported for power-of-two images in WebGL 1
else gl.texParameteri(glBindType, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(glBindType, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(glBindType, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(glBindType, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
} else {
// Look up vertex coordinates location
program.vertPosLocation = gl.getAttribLocation(program, "a_vertCoord");
gl.enableVertexAttribArray(program.vertPosLocation);
// Create buffers
if (!cubeVertBuf) cubeVertBuf = gl.createBuffer();
if (!cubeVertTexCoordBuf) cubeVertTexCoordBuf = gl.createBuffer();
if (!cubeVertIndBuf) cubeVertIndBuf = gl.createBuffer();
// Bind texture coordinate buffer and pass coordinates to WebGL
gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertTexCoordBuf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
0,
0,
1,
0,
1,
1,
0,
1
]), gl.STATIC_DRAW);
gl.vertexAttribPointer(program.texCoordLocation, 2, gl.FLOAT, false, 0, 0);
// Bind square index buffer and pass indices to WebGL
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertIndBuf);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([
0,
1,
2,
0,
2,
3
]), gl.STATIC_DRAW);
// Bind vertex buffer
gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertBuf);
gl.vertexAttribPointer(program.vertPosLocation, 3, gl.FLOAT, false, 0, 0);
// Find uniforms
program.perspUniform = gl.getUniformLocation(program, "u_perspMatrix");
program.cubeUniform = gl.getUniformLocation(program, "u_cubeMatrix");
//program.colorUniform = gl.getUniformLocation(program, 'u_color');
program.currentNodes = [];
program.nodeCache = [];
program.nodeCacheTimestamp = 0;
program.textureLoads = [];
if (image.shtHash || image.equirectangularThumbnail) {
// Create vertex shader
previewVs = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(previewVs, v);
gl.compileShader(previewVs);
// Create fragment shader
previewFs = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(previewFs, fragEquirectangular);
gl.compileShader(previewFs);
// Link WebGL program
previewProgram = gl.createProgram();
gl.attachShader(previewProgram, previewVs);
gl.attachShader(previewProgram, previewFs);
gl.linkProgram(previewProgram);
// Log errors
if (!gl.getShaderParameter(previewVs, gl.COMPILE_STATUS)) console.log(gl.getShaderInfoLog(previewVs));
if (!gl.getShaderParameter(previewFs, gl.COMPILE_STATUS)) console.log(gl.getShaderInfoLog(previewFs));
if (!gl.getProgramParameter(previewProgram, gl.LINK_STATUS)) console.log(gl.getProgramInfoLog(previewProgram));
// Use WebGL program
gl.useProgram(previewProgram);
// Look up texture coordinates location
previewProgram.texCoordLocation = gl.getAttribLocation(previewProgram, "a_texCoord");
gl.enableVertexAttribArray(previewProgram.texCoordLocation);
// Provide texture coordinates for rectangle
if (!texCoordBuffer) texCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-1,
1,
1,
1,
1,
-1,
-1,
1,
1,
-1,
-1,
-1
]), gl.STATIC_DRAW);
gl.vertexAttribPointer(previewProgram.texCoordLocation, 2, gl.FLOAT, false, 0, 0);
// Pass aspect ratio
previewProgram.aspectRatio = gl.getUniformLocation(previewProgram, "u_aspectRatio");
gl.uniform1f(previewProgram.aspectRatio, gl.drawingBufferWidth / gl.drawingBufferHeight);
// Locate psi, theta, focal length, horizontal extent, vertical extent, and vertical offset
previewProgram.psi = gl.getUniformLocation(previewProgram, "u_psi");
previewProgram.theta = gl.getUniformLocation(previewProgram, "u_theta");
previewProgram.f = gl.getUniformLocation(previewProgram, "u_f");
previewProgram.h = gl.getUniformLocation(previewProgram, "u_h");
previewProgram.v = gl.getUniformLocation(previewProgram, "u_v");
previewProgram.vo = gl.getUniformLocation(previewProgram, "u_vo");
previewProgram.rot = gl.getUniformLocation(previewProgram, "u_rot");
// Pass horizontal extent
gl.uniform1f(previewProgram.h, 1.0);
// Create texture
previewProgram.texture = gl.createTexture();
gl.bindTexture(glBindType, previewProgram.texture);
// Upload preview image to the texture
var previewImage, vext, voff;
var uploadPreview = function() {
gl.useProgram(previewProgram);
gl.uniform1i(gl.getUniformLocation(previewProgram, "u_splitImage"), 0);
gl.texImage2D(glBindType, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, previewImage);
// Set parameters for rendering any size
gl.texParameteri(glBindType, gl.TEXTURE_WRAP_S, gl.REPEAT);
gl.texParameteri(glBindType, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(glBindType, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(glBindType, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
// Pass vertical extent and vertical offset
gl.uniform1f(previewProgram.v, vext);
gl.uniform1f(previewProgram.vo, voff);
gl.useProgram(program);
};
if (image.shtHash) {
previewImage = shtDecodeImage(image.shtHash);
// Vertical extent & offset are chosen to set the top and bottom
// pixels in the preview image to be exactly at the zenith and
// nadir, respectively, which matches the pre-calculated Ylm
vext = (2 + 1 / 31) / 2;
voff = 1 - (2 + 1 / 31) / 2;
uploadPreview();
}
if (image.equirectangularThumbnail) {
if (typeof image.equirectangularThumbnail === "string") {
if (image.equirectangularThumbnail.slice(0, 5) == "data:") {
// Data URI
previewImage = new Image();
previewImage.onload = function() {
vext = 1;
voff = 0;
uploadPreview();
};
previewImage.src = image.equirectangularThumbnail;
} else {
console.log("Error: thumbnail string is not a data URI!");
throw {
type: "config error"
};
}
} else {
// ImageData / ImageBitmap / HTMLImageElement / HTMLCanvasElement
previewImage = image.equirectangularThumbnail;
vext = 1;
voff = 0;
uploadPreview();
}
}
// Reactivate main program
gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertBuf);
gl.vertexAttribPointer(program.vertPosLocation, 3, gl.FLOAT, false, 0, 0);
gl.useProgram(program);
}
}
// Check if there was an error
var err = gl.getError();
if (err !== 0) {
console.log("Error: Something went wrong with WebGL!", err);
throw {
type: "webgl error"
};
}
callback();
};
/**
* Destroy renderer.
* @memberof Renderer
* @instance
*/ this.destroy = function() {
if (container !== undefined) {
if (canvas !== undefined && container.contains(canvas)) container.removeChild(canvas);
if (world !== undefined && container.contains(world)) container.removeChild(world);
}
if (gl) {
// The spec says this is only supposed to simulate losing the WebGL
// context, but in practice it tends to actually free the memory.
var extension = gl.getExtension("WEBGL_lose_context");
if (extension) extension.loseContext();
}
};
/**
* Resize renderer (call after resizing container).
* @memberof Renderer
* @instance
*/ this.resize = function() {
var pixelRatio = window1.devicePixelRatio || 1;
canvas.width = canvas.clientWidth * pixelRatio;
canvas.height = canvas.clientHeight * pixelRatio;
if (gl) {
if (gl.getError() == 1286) handleWebGLError1286();
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
if (imageType != "multires") gl.uniform1f(program.aspectRatio, canvas.clientWidth / canvas.clientHeight);
else if (image.shtHash) {
gl.useProgram(previewProgram);
gl.uniform1f(previewProgram.aspectRatio, canvas.clientWidth / canvas.clientHeight);
gl.useProgram(program);
}
}
};
// Initialize canvas size
if (canvas) this.resize();
/**
* Set renderer horizon pitch and roll.
* @memberof Renderer
* @instance
* @param {number} horizonPitch - Pitch of horizon (in radians).
* @param {number} horizonRoll - Roll of horizon (in radians).
*/ this.setPose = function(horizonPitch, horizonRoll) {
horizonPitch = isNaN(horizonPitch) ? 0 : Number(horizonPitch);
horizonRoll = isNaN(horizonRoll) ? 0 : Number(horizonRoll);
if (horizonPitch == 0 && horizonRoll == 0) pose = undefined;
else pose = [
horizonPitch,
horizonRoll
];
};
/**
* Render new view of panorama.
* @memberof Renderer
* @instance
* @param {number} pitch - Pitch to render at (in radians).
* @param {number} yaw - Yaw to render at (in radians).
* @param {number} hfov - Horizontal field of view to render with (in radians).
* @param {Object} [params] - Extra configuration parameters.
* @param {number} [params.roll] - Camera roll (in radians).
* @param {string} [params.returnImage] - Return rendered image? If specified, should be 'ImageBitmap', 'image/jpeg', or 'image/png'.
* @param {function} [params.hook] - Hook for executing arbitrary function in this environment.
* @param {boolean} [params.dynamic] - Whether or not the image is dynamic (e.g., video) and should be updated.
*/ this.render = function(pitch, yaw, hfov, params) {
var focal, i, s, roll = 0;
if (params === undefined) params = {};
if (params.roll) roll = params.roll;
if (params.dynamic) var dynamic = params.dynamic;
// Apply pitch and roll transformation if applicable
if (pose !== undefined) {
var horizonPitch = pose[0], horizonRoll = pose[1];
// Calculate new pitch and yaw
var orig_pitch = pitch, orig_yaw = yaw, x = Math.cos(horizonRoll) * Math.sin(pitch) * Math.sin(horizonPitch) + Math.cos(pitch) * (Math.cos(horizonPitch) * Math.cos(yaw) + Math.sin(horizonRoll) * Math.sin(horizonPitch) * Math.sin(yaw)), y = -Math.sin(pitch) * Math.sin(horizonRoll) + Math.cos(pitch) * Math.cos(horizonRoll) * Math.sin(yaw), z = Math.cos(horizonRoll) * Math.cos(horizonPitch) * Math.sin(pitch) + Math.cos(pitch) * (-Math.cos(yaw) * Math.sin(horizonPitch) + Math.cos(horizonPitch) * Math.sin(horizonRoll) * Math.sin(yaw));
pitch = Math.asin(Math.max(Math.min(z, 1), -1));
yaw = Math.atan2(y, x);
// Calculate roll
var v = [
Math.cos(orig_pitch) * (Math.sin(horizonRoll) * Math.sin(horizonPitch) * Math.cos(orig_yaw) - Math.cos(horizonPitch) * Math.sin(orig_yaw)),
Math.cos(orig_pitch) * Math.cos(horizonRoll) * Math.cos(orig_yaw),
Math.cos(orig_pitch) * (Math.cos(horizonPitch) * Math.sin(horizonRoll) * Math.cos(orig_yaw) + Math.sin(orig_yaw) * Math.sin(horizonPitch))
], w = [
-Math.cos(pitch) * Math.sin(yaw),
Math.cos(pitch) * Math.cos(yaw)
];
var roll_adj = Math.acos(Math.max(Math.min((v[0] * w[0] + v[1] * w[1]) / (Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]) * Math.sqrt(w[0] * w[0] + w[1] * w[1])), 1), -1));
if (v[2] < 0) roll_adj = 2 * Math.PI - roll_adj;
roll += roll_adj;
}
// Execute function hook
if (params.hook) params.hook({
gl: gl,
program: program,
previewProgram: previewProgram,
imageType: imageType,
texCoordBuffer: texCoordBuffer,
cubeVertBuf: cubeVertBuf,
cubeVertTexCoordBuf: cubeVertTexCoordBuf,
cubeVertIndBuf: cubeVertIndBuf
});
// If no WebGL
if (!gl && (imageType == "multires" || imageType == "cubemap")) {
// Determine face transforms
s = fallbackImgSize / 2;
var transforms = {
f: "translate3d(-" + (s + 2) + "px, -" + (s + 2) + "px, -" + s + "px)",
b: "translate3d(" + (s + 2) + "px, -" + (s + 2) + "px, " + s + "px) rotateX(180deg) rotateZ(180deg)",
u: "translate3d(-" + (s + 2) + "px, -" + s + "px, " + (s + 2) + "px) rotateX(270deg)",
d: "translate3d(-" + (s + 2) + "px, " + s + "px, -" + (s + 2) + "px) rotateX(90deg)",
l: "translate3d(-" + s + "px, -" + (s + 2) + "px, " + (s + 2) + "px) rotateX(180deg) rotateY(90deg) rotateZ(180deg)",
r: "translate3d(" + s + "px, -" + (s + 2) + "px, -" + (s + 2) + "px) rotateY(270deg)"
};
focal = 1 / Math.tan(hfov / 2);
var zoom = focal * canvas.clientWidth / 2 + "px";
var transform = "perspective(" + zoom + ") translateZ(" + zoom + ") rotateX(" + pitch + "rad) rotateY(" + yaw + "rad) ";
// Apply face transforms
var faces = Object.keys(transforms);
for(i = 0; i < 6; i++){
var face = world.querySelector(".pnlm-" + faces[i] + "face");
if (!face) continue; // ignore missing face to support partial cubemap/fallback image
face.style.webkitTransform = transform + transforms[faces[i]];
face.style.transform = transform + transforms[faces[i]];
}
return;
}
if (imageType != "multires") {
// Calculate focal length from vertical field of view
var vfov = 2 * Math.atan(Math.tan(hfov * 0.5) / (gl.drawingBufferWidth / gl.drawingBufferHeight));
focal = 1 / Math.tan(vfov * 0.5);
// Pass psi, theta, roll, and focal length
gl.uniform1f(program.psi, yaw);
gl.uniform1f(program.theta, pitch);
gl.uniform1f(program.rot, roll);
gl.uniform1f(program.f, focal);
if (dynamic === true) // Update texture if dynamic
{
if (imageType == "equirectangular") {
gl.bindTexture(gl.TEXTURE_2D, program.texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);
}
}
// Draw using current buffer
gl.drawArrays(gl.TRIANGLES, 0, 6);
} else {
// Draw SHT hash preview, if needed
var isPreview = typeof image.shtHash !== "undefined" || typeof image.equirectangularThumbnail !== "undefined";
var drawPreview = isPreview;
if (isPreview && program.currentNodes.length >= 6) {
drawPreview = false;
for(var i = 0; i < 6; i++)if (!program.currentNodes[i].textureLoaded) {
drawPreview = true;
break;
}
}
if (drawPreview) {
gl.useProgram(previewProgram);
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.vertexAttribPointer(previewProgram.texCoordLocation, 2, gl.FLOAT, false, 0, 0);
gl.bindTexture(gl.TEXTURE_2D, previewProgram.texture);
// Calculate focal length from vertical field of view
var vfov = 2 * Math.atan(Math.tan(hfov * 0.5) / (gl.drawingBufferWidth / gl.drawingBufferHeight));
focal = 1 / Math.tan(vfov * 0.5);
// Pass psi, theta, roll, and focal length
gl.uniform1f(previewProgram.psi, yaw);
gl.uniform1f(previewProgram.theta, pitch);
gl.uniform1f(previewProgram.rot, roll);
gl.uniform1f(previewProgram.f, focal);
// Draw using current buffer
gl.drawArrays(gl.TRIANGLES, 0, 6);
gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertBuf);
gl.vertexAttribPointer(program.vertPosLocation, 3, gl.FLOAT, false, 0, 0);
gl.useProgram(program);
}
// Create perspective matrix
var perspMatrix = makePersp(hfov, gl.drawingBufferWidth / gl.drawingBufferHeight, 0.1, 100.0);
var perspMatrixNoClip = makePersp(hfov, gl.drawingBufferWidth / gl.drawingBufferHeight, -100, 100.0);
// Create rotation matrix
var matrix = identityMatrix3();
matrix = rotateMatrix(matrix, -roll, "z");
matrix = rotateMatrix(matrix, -pitch, "x");
matrix = rotateMatrix(matrix, yaw, "y");
matrix = makeMatrix4(matrix);
// Set matrix uniforms
gl.uniformMatrix4fv(program.perspUniform, false, transposeMatrix4(perspMatrix));
gl.uniformMatrix4fv(program.cubeUniform, false, transposeMatrix4(matrix));
// Find current nodes
var rotPersp = rotatePersp(perspMatrix, matrix);
var rotPerspNoClip = rotatePersp(perspMatrixNoClip, matrix);
program.nodeCache.sort(multiresNodeSort);
if (program.nodeCache.length > 200 && program.nodeCache.length > program.currentNodes.length + 50) {
// Remove older nodes from cache
var removed = program.nodeCache.splice(200, program.nodeCache.length - 200);
for(var j = 0; j < removed.length; j++)// Explicitly delete textures
gl.deleteTexture(removed[j].texture);
}
program.currentNodes = [];
for(s = 0; s < 6; s++){
var ntmp = new MultiresNode(vtmps[s], sides[s], 1, 0, 0, image.fullpath, null);
testMultiresNode(rotPersp, rotPerspNoClip, ntmp, pitch, yaw, hfov);
}
program.currentNodes.sort(multiresNodeRenderSort);
// Unqueue any pending requests for nodes that are no longer visible
for(i = pendingTextureRequests.length - 1; i >= 0; i--)if (program.currentNodes.indexOf(pendingTextureRequests[i].node) === -1) {
pendingTextureRequests[i].node.textureLoad = false;
pendingTextureRequests.splice(i, 1);
}
// Allow one request to be pending, so that we can create a texture buffer for that in advance of loading actually beginning
if (pendingTextureRequests.length === 0) for(i = 0; i < program.currentNodes.length; i++){
var node = program.currentNodes[i];
if (!node.texture && !node.textureLoad) {
node.textureLoad = true;
setTimeout(processNextTile, 0, node);
break;
}
}
// Process one pending image tile
// This is synchronized to rendering to avoid dropping frames due
// to texture loading happening at an inopportune time.
if (program.textureLoads.length > 0) program.textureLoads.shift()(true);
// Draw tiles
multiresDraw(!isPreview);
}
if (params.returnImage !== undefined) {
if (window1.createImageBitmap && params.returnImage == "ImageBitmap") return createImageBitmap(canvas);
else {
if (params.returnImage.toString().indexOf("image/") == 0) return canvas.toDataURL(params.returnImage);
else return canvas.toDataURL("image/png"); // Old default
}
}
};
/**
* Check if images are loading.
* @memberof Renderer
* @instance
* @returns {boolean} Whether or not images are loading.
*/ this.isLoading = function() {
if (gl && imageType == "multires") for(var i = 0; i < program.currentNodes.length; i++){
if (!program.currentNodes[i].textureLoaded) return true;
}
return false;
};
/**
* Check if base image tiles are loaded.
* @memberof Renderer
* @instance
* @returns {boolean} Whether or not base image tiles are loaded.
*/ this.isBaseLoaded = function() {
if (program.currentNodes.length >= 6) {
for(var i = 0; i < 6; i++){
if (!program.currentNodes[i].textureLoaded) return false;
}
return true;
}
return false;
};
/**
* Retrieve renderer's canvas.
* @memberof Renderer
* @instance
* @returns {HTMLElement} Renderer's canvas.
*/ this.getCanvas = function() {
return canvas;
};
/**
* Sorting method for multires nodes.
* @private
* @param {MultiresNode} a - First node.
* @param {MultiresNode} b - Second node.
* @returns {number} Base tiles first, then higher timestamp first.
*/ function multiresNodeSort(a, b) {
// Base tiles are always first
if (a.level == 1 && b.level != 1) return -1;
if (b.level == 1 && a.level != 1) return 1;
// Higher timestamp first
return b.timestamp - a.timestamp;
}
/**
*