/**
* SeekQuarry/Yioop --
* Open Source Pure PHP Search Engine, Crawler, and Indexer
*
* Copyright (C) 2009 - 2026 Chris Pollett chris@pollett.org
*
* LICENSE:
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* END LICENSE
*
* @author Chris Pollett chris@pollett.org
* @license https://www.gnu.org/licenses/ GPL3
* @link https://www.seekquarry.com/
* @copyright 2009 - 2026
* @filesource
*/
function draw360(id_360, image_360)
{
var vr_png =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAAmCAYAAAB0xJ2ZA"+
"AAMI2lDQ1BJQ0MgUHJvZmlsZQAASImVVwdUk8kWnr8kISGhBRCQEnoTpUiXGlroUgUb"+
"IQkklBADQcWOLCqwFlRUwIauiii6FkAWGxZQWRTsdbGgouiiLjZQ3iQBdN1XzrvnzMx"+
"37tx757vTzgwAypFskSgDVQEgU5gjjgr0ZUxJSGSQHgEE4EAVKIJxbE62yCcyMhRAGW"+
"n/Lu9vQGsoV22ksf7Z/19FlcvL5gCAREKczM3mZEJ8BADciSMS5wBA6IV649k5IoiJk"+
"CVQF0OCEJtIcaocu0hxshyHymxiopgQJwGgQGWzxakAKEl5MXI5qTCOUgnEtkKuQAhx"+
"E8SeHD6bC/EgxOMyM7MgVraA2CL5uzipf4uZPBqTzU4dxfJcZKLgJ8gWZbDn/p/T8b8"+
"lM0MyMoYxLFS+OChKmrN03tKzQqSYCnGrMDk8AmI1iK8JuDJ7KX7KlwTFDtt/5GQz4Z"+
"wBTQBQKpftFwKxLsRGkvRYn2HsyRbLfKE9mpjHj4mXx0eF4qyo4fhonjAjPHQ4Tgmfx"+
"xrBVbxs/+gRmxRBAAtiuIZogyCHFTMcszVXEBcOsRLE97LTo0OGfV/k8Znho2NJoqSc"+
"4ZpjIDN7JBfMJEUcECW3x5z4Alb4sD40hx8TJPfFZnDYMg5aEKfxsqeEjvDh8vz85Xy"+
"wfJ4wdpgnVirK8Y0att8pyogctseaeBmBUr0RxO3ZudEjvn05cLPJc8FBGjs4Uj4uri"+
"7KiYyRc8MZIBQwgR9gAAksySALpAFBe299LxjpCQBsIAapgAdshjUjHvGyHiGso0Eee"+
"AURD2SP+vnKenkgF+q/jGrltQ1IkfXmyjzSwVOIM3Ed3BN3x0Nh7Q2LPe6Cu474MZRH"+
"RiX6E/2IQcQAouVMQb74h7gMwIEZZMAiBiGw5cGspByEI9y/xSE8JXQQHhGuE7oIt0E"+
"ceALtBP/I8Fs0waguDHTBqAHD2SV/nx1uBlk74r64B+QPueOauA6wwSfCTHxwL5ibI9"+
"R+m7V/x10ywppsS0bJY8jeZIsf7ZSslBxHfaS5fc9Tzit5NBPmaM+PozG/y40L25AfL"+
"bHl2GGsBTuNXcCasHrAwE5iDVgbdlyKR/fGE9neGBktSsYnHcYRjNjY1tj22A7+MDZ7"+
"eHyxbP1BDm9OjvTgMLNEc8WCVH4Owwfe1jwGS8gZP45hb2sHb1Hp3S+/Wvouy+50RFv"+
"1m25JFgCTKoeGho5904XdB+DIKwAod77pzIvgcc4FoLWcIxHnynW4tCIAClCGJ0Ub6M"+
"O7ywJmZA+cgDvwBv4gGESAGJAAZsB55oNMyHo2mA+WgEJQDFaD9aAcbAU7wB6wHxwC9"+
"aAJnAbnwSVwBVwHd+Fe6QYvQR94DwYQBCEhNISOaCMGiClijdgjLogn4o+EIlFIApKE"+
"pCJCRILMR5YixUgpUo5sR6qRX5FjyGnkAtKB3EYeIj3IW+QziqFUVB3VQ83QCagL6oO"+
"GoDHodDQVnYXmoQXoSnQjWoXuQ+vQ0+gl9Drahb5E+zGAKWKamCFmg7lgTCwCS8RSMD"+
"G2ECvCyrAqrBZrhCt9FevCerFPOBGn4wzcBu7XIDwW5+Cz8IV4CV6O78Hr8LP4Vfwh3"+
"od/JdAIugRrghuBRZhCSCXMJhQSygi7CEcJ5+CZ6ia8JxKJmkRzojM8qwnENOI8Yglx"+
"M/EA8RSxg/iY2E8ikbRJ1iQPUgSJTcohFZI2kfaRTpI6Sd2kjwqKCgYK9goBCokKQoV"+
"8hTKFvQonFDoVnikMkFXIpmQ3cgSZS55LXkXeSW4kXyZ3kwcoqhRzigclhpJGWULZSK"+
"mlnKPco7xTVFQ0UnRVnKwoUFysuFHxoGKr4kPFT1Q1qhWVSZ1GlVBXUndTT1FvU9/Ra"+
"DQzmjctkZZDW0mrpp2hPaB9VKIrjVdiKXGVFilVKNUpdSq9ViYrmyr7KM9QzlMuUz6s"+
"fFm5V4WsYqbCVGGrLFSpUDmmclOlX5WuaqcaoZqpWqK6V/WC6nM1kpqZmr8aV61AbYf"+
"aGbXHdIxuTGfSOfSl9J30c/RudaK6uTpLPU29WH2/ert6n4aaxkSNOI05GhUaxzW6ND"+
"FNM02WZobmKs1Dmjc0P4/RG+MzhjdmxZjaMZ1jPmiN1fLW4mkVaR3Quq71WZuh7a+dr"+
"r1Gu177vg6uY6UzWWe2zhadczq9Y9XHuo/ljC0ae2jsHV1U10o3Snee7g7dNt1+PX29"+
"QD2R3ia9M3q9+pr63vpp+uv0T+j3GNANPA0EBusMThq8YGgwfBgZjI2Ms4w+Q13DIEO"+
"J4XbDdsMBI3OjWKN8owNG940pxi7GKcbrjJuN+0wMTMJM5pvUmNwxJZu6mPJNN5i2mH"+
"4wMzeLN1tmVm/23FzLnGWeZ15jfs+CZuFlMcuiyuKaJdHSxTLdcrPlFSvUytGKb1Vhd"+
"dkatXayFlhvtu4YRxjnOk44rmrcTRuqjY9Nrk2NzcPxmuNDx+ePrx//eoLJhMQJaya0"+
"TPhq62ibYbvT9q6dml2wXb5do91beyt7jn2F/TUHmkOAwyKHBoc3E60n8iZumXjLke4"+
"Y5rjMsdnxi5Ozk9ip1qnH2cQ5ybnS+aaLukukS4lLqyvB1dd1kWuT6yc3J7cct0Nuf7"+
"rbuKe773V/Psl8Em/SzkmPPYw82B7bPbo8GZ5Jnts8u7wMvdheVV6PvI29ud67vJ/5W"+
"Pqk+ezzee1r6yv2Per7genGXMA85Yf5BfoV+bX7q/nH+pf7PwgwCkgNqAnoC3QMnBd4"+
"KogQFBK0JugmS4/FYVWz+oKdgxcEnw2hhkSHlIc8CrUKFYc2hqFhwWFrw+6Fm4YLw+s"+
"jQAQrYm3E/UjzyFmRv00mTo6cXDH5aZRd1Pyolmh69MzovdHvY3xjVsXcjbWIlcQ2xy"+
"nHTYurjvsQ7xdfGt81ZcKUBVMuJegkCBIaEkmJcYm7Evun+k9dP7V7muO0wmk3pptPn"+
"zP9wgydGRkzjs9UnsmeeTiJkBSftDdpkB3BrmL3J7OSK5P7OEzOBs5Lrjd3HbeH58Er"+
"5T1L8UgpTXme6pG6NrWH78Uv4/cKmIJywZu0oLStaR/SI9J3pw9lxGccyFTITMo8JlQ"+
"TpgvPZulnzcnqEFmLCkVds9xmrZ/VJw4R78pGsqdnN+Sow0d2m8RC8pPkYa5nbkXux9"+
"lxsw/PUZ0jnNM212ruirnP8gLyfpmHz+PMa55vOH/J/IcLfBZsX4gsTF7YvMh4UcGi7"+
"sWBi/csoSxJX/J7vm1+af5fS+OXNhboFSwuePxT4E81hUqF4sKby9yXbV2OLxcsb1/h"+
"sGLTiq9F3KKLxbbFZcWDJZySiz/b/bzx56GVKSvbVzmt2rKauFq4+sYarzV7SlVL80o"+
"frw1bW7eOsa5o3V/rZ66/UDaxbOsGygbJhq6NoRsbNplsWr1psJxffr3Ct+JApW7lis"+
"oPm7mbO7d4b6ndqre1eOvnbYJtt7YHbq+rMqsq20Hckbvj6c64nS2/uPxSvUtnV/GuL"+
"7uFu7v2RO05W+1cXb1Xd++qGrRGUtOzb9q+K/v99jfU2tRuP6B5oPggOCg5+OLXpF9v"+
"HAo51HzY5XDtEdMjlUfpR4vqkLq5dX31/PquhoSGjmPBx5ob3RuP/jb+t91Nhk0VxzW"+
"OrzpBOVFwYuhk3sn+U6JTvadTTz9untl898yUM9fOTj7bfi7kXOv5gPNnWnxaTrZ6tD"+
"ZdcLtw7KLLxfpLTpfq2hzbjv7u+PvRdqf2usvOlxuuuF5p7JjUcaLTq/P0Vb+r56+xr"+
"l26Hn6940bsjVs3p93susW99fx2xu03d3LvDNxdfI9wr+i+yv2yB7oPqv6w/ONAl1PX"+
"8Yd+D9seRT+6+5jz+OWT7CeD3QVPaU/Lnhk8q35u/7ypJ6DnyoupL7pfil4O9Ba+Un1"+
"V+dri9ZE/vf9s65vS1/1G/Gbobck77Xe7/5r4V3N/ZP+D95nvBz4UfdT+uOeTy6eWz/"+
"Gfnw3MHiQNbvxi+aXxa8jXe0OZQ0MitpgtewpgsKApKQC83Q0ALQEA+hX4fpgq/5vJB"+
"JH/J2UI/Ccs/7/JxAmAWthIn+HMUwAchMXMG8ZeDEAEbGO8AergMFqGJTvFwV4eS6kG"+
"AJLh0NBb+L4hwzIYODQ0EDk09KUSkr0GwInn8j+hVKR/0G22UtRpcBj8KP8CUQ9xO6a"+
"BOOgAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAIEaVRYdFhNTDpjb20uYWRvYmUueG1wAA"+
"AAAAA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYT"+
"VAgQ29yZSA1LjQuMCI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53"+
"My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3J"+
"pcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly"+
"9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIgogICAgICAgICAgICB4bWxuczp0aWZmPSJod"+
"HRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDxleGlmOlBpeGVs"+
"WURpbWVuc2lvbj41MjA8L2V4aWY6UGl4ZWxZRGltZW5zaW9uPgogICAgICAgICA8ZXh"+
"pZjpQaXhlbFhEaW1lbnNpb24+Njc0PC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgIC"+
"AgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgP"+
"C9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Cr4+4A4A"+
"AAZtSURBVGgF7VjfS9VLEJ/jj1QKwZQiS1FBL6EWPXRB7UVK/R9CDUXCrtiDgqJoD0V"+
"kQYjYheoh9dqT0JM+iaVSSJKKP8AMwkoNiSI0f/bDs3c/Y/Nls3OOeyTuFW1kz+7O7s"+
"7sfGZmd/26lCbaxRSwi21n038DsKUI2EFJs7UIcG0Jtm25aGsAbEtTtrapXQ9AkC1ub"+
"rfbdur/Os/lchGKLbl24jsATxtbEHxGgAhaW1uj0dFR+vjxIwUGBtJ2ezvBWNlTcnIy"+
"HThwgPs2IFgBsLCwQLf+vkXn8s9RREQEffnyhQIC9PGB61CiTa5G9KVtxqHwZb6MiQy"+
"pwTfb0pf5Un+XI4aHhoZSa2srff36lbKzs38NAKILERAbE0tpaWkUHBws7G1Xnzp1ig"+
"HwZ2M+I0AEwdsrKyu0uLjIEfDt27f1CJAJ/1EtKblRHfhIzc+fP3Mt497myzhqawCgQ"+
"AiAcAoIQ9eebgnkoE0eGmJ+arqVvn10Spj6oMuULWmwZ88egnNAtnp9vgNECIyHcFFk"+
"7hI8FAHFrLHeEzDmel9trA1wrYONNs4eEHSYsmVfSE9p+5JrjllFgCjDWWASlAlIb96"+
"8ofHxcVpaWqKgoCCKiYmh48ePc5sNwaHpBwmo7969o0ePHtHc3BzLwiF35MgROn36NO"+
"3du3cd4O8HIoARkGxVWQMA400AxHgYd//+fZqdnaUzZ87Q/v37ORfHxsbowYMHVFJSQ"+
"ocPH+aNYoM2pNwa2AAXDQ0NUXNzMxUWFlJSUhIDsLq6SgMDA1RVVUWVlZUsm/elMxTO"+
"AEB+kTbEK2njeEwfgKqmpka9ffuW+zrPnDWt/7SqO3fuOH2zoSNCnT9/XmnvMVtv1Bm"+
"GbCkOUzdkjva80oar9+/fm8NOe3h4WJWXlyvtcYfX3d2tWlpauO9JtjPRaFi5BMia+a"+
"XXM8gfPnygwaFBys3N5T4OIK2YC7xy9OhRysjIoL6+vh+cgvWQKQV9kSkT8fDKzMykq"+
"KgojjzIxRypkV54k0xOTsqSLV3RVgAgdAEAlJuEfMcm8AgB4bDEXBQx6NChQzQ/P+8s"+
"Ax+Gg5qamji/0Qdf1shkmYe+2fY0Dh7Onl96C5iK4NGNwg8ePMjP4+npaZ4qRgAobAb"+
"05MkTSkxM/GEcnY6ODpqZmaGenh7q7+9n0EyAjx07Rr29vYQoA7AAAAXgokaE4GBMSE"+
"hg2fKDt4BfpDftlfSGeAw5f+XKFfXy5UvuI08lV/VGVHFxsXr9+vUPcvRhpW7fvq3u3"+
"r3r8CEHNDg4qC78dYHby8vLqqCgQD1//pz7mCN69SGoLl68qJDvOtoUZH769Ek9fPhQ"+
"lZaWKg0gr9EHH9fPnj1TjY2N3MaPyHEYHhrWtwA8anpIwjw1NZUqKiqora2N9u3bxzm"+
"Lk3p6appO/nmSsrKy2CGIHshAtDQ0NFB9fT3zw8LCqK6ujqqrq+nSpUsUGxvLOQ/5J0"+
"6coOjoaOrq6qLHjx/zWwQexhV77do15xrEXBDke0oVHvT24wEUh2UiePPmTTUxMcFj4"+
"n10zDZObn0oqampKfaWCBLP63+qVFFRkXox8YKHwJexV69ecSTIqQ++qR96tPEikmvR"+
"LbW+ev2OAKtDEOABZa3oJxzBl8jAv6Hx8fHsoZCQEOZjTJ7R169fp7Nnz1LSH0ksC3w"+
"UyI2Li6OysjKqra0lnRbMxzNYW8pyoAevURBkgi+el03B+/6eAdYAQKE8MtA2CxRjUz"+
"BEioAiIXnv3j0+DHG1bdy8gJCSkkJ5eXl0+fJlPnADA9bBEaMhW+RCBtpSBAQdOdK0q"+
"q0BQH6JJ1HDMLPAG+BLQR8Fczo7O0k/oig/P583hc0LMLJLzIUx6enpfP8jWkDQK7Ig"+
"W9pmjTmg8PBwlisgMXOTH6tDEDLgfVxbCHF8IBEDYAxIPIE+CryF94E+N6i9vZ1u3Lj"+
"hzMPmN5IpLycnh3VcvXqV0PYZ1vpJ4V5z84GI5zf26Un+Rn3S9/lNUIzD5p4+fcolMj"+
"KSlYAnXhAPiPEYQ4HH8B0BX2jwIAIo4PkiyMBa0MjICH+DgHzZi7e1WAP5+EcJNwnIl"+
"OV1nZ607kJvMyBI/2mTfMzYfMhmMyLFn7myxqz9We8zArwJtcDMWSqbEa86AxYNf3JZ"+
"xEGPP7qsARAFO63++TTaaRZuYs9vADYBaMcP/46AHe/iTQzc9RHwLwbmk9SnRNmGAAA"+
"AAElFTkSuQmCC";
var vr_display = null;
var frame_data = null;
var mat4_identity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
/**
* Zeroes out a 4x4 matrix in-place (16-element flat array).
*
* @param {Float32Array|Array} matrix the matrix to clear
*/
function mat4Zero(matrix)
{
for (var i = 0; i < 16; i++) {
matrix[i] = 0;
}
}
/**
* Loads the identity matrix into a 4x4 matrix in-place.
*
* @param {Float32Array|Array} matrix the matrix to overwrite
*/
function mat4Identity(matrix)
{
for (var i = 0; i < 16; i++) {
matrix[i] = mat4_identity[i];
}
}
/**
* Builds a perspective projection matrix into $out.
*
* @param {Float32Array} out destination 4x4 matrix
* @param {number} fovy vertical field of view in radians
* @param {number} aspect viewport aspect ratio (width / height)
* @param {number} near near clipping plane distance
* @param {number} far far clipping plane distance; pass null or
* Infinity for an infinite-far-plane projection
* @return {Float32Array} the populated $out matrix
*/
function mat4Perspective(out, fovy, aspect, near, far)
{
let f = 1.0 / Math.tan(fovy / 2), nf;
mat4Zero(out);
out[0] = f / aspect;
out[5] = f;
out[11] = -1;
if (far != null && far !== Infinity) {
nf = 1 / (near - far);
out[10] = (far + near) * nf;
out[14] = (2 * far * near) * nf;
} else {
out[10] = -1;
out[14] = -2 * near;
}
return out;
}
/**
* Builds a 4x4 rotation matrix from a unit quaternion.
*
* @param {Float32Array} out destination 4x4 matrix
* @param {Array} q unit quaternion [x, y, z, w]
* @return {Float32Array} the populated $out matrix
*/
function mat4FromQuat(out, q)
{
let x = q[0], y = q[1], z = q[2], w = q[3];
let x2 = x + x;
let y2 = y + y;
let z2 = z + z;
let xx = x * x2;
let yx = y * x2;
let yy = y * y2;
let zx = z * x2;
let zy = z * y2;
let zz = z * z2;
let wx = w * x2;
let wy = w * y2;
let wz = w * z2;
out[0] = 1 - yy - zz;
out[1] = yx + wz;
out[2] = zx - wy;
out[3] = 0;
out[4] = yx - wz;
out[5] = 1 - xx - zz;
out[6] = zy + wx;
out[7] = 0;
out[8] = zx + wy;
out[9] = zy - wx;
out[10] = 1 - xx - yy;
out[11] = 0;
out[12] = 0;
out[13] = 0;
out[14] = 0;
out[15] = 1;
return out;
}
/**
* Computes the inverse of 4x4 matrix $a into $out using the
* standard cofactor-expansion formula.
*
* @param {Float32Array} out destination 4x4 matrix
* @param {Float32Array} a source 4x4 matrix
* @return {Float32Array} the populated $out matrix (or null when
* $a is singular)
*/
function mat4Invert(out, a)
{
let a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3];
let a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7];
let a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11];
let a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15];
let b00 = a00 * a11 - a01 * a10;
let b01 = a00 * a12 - a02 * a10;
let b02 = a00 * a13 - a03 * a10;
let b03 = a01 * a12 - a02 * a11;
let b04 = a01 * a13 - a03 * a11;
let b05 = a02 * a13 - a03 * a12;
let b06 = a20 * a31 - a21 * a30;
let b07 = a20 * a32 - a22 * a30;
let b08 = a20 * a33 - a23 * a30;
let b09 = a21 * a32 - a22 * a31;
let b10 = a21 * a33 - a23 * a31;
let b11 = a22 * a33 - a23 * a32;
// Calculate the determinant
let det = b00 * b11 - b01 * b10 + b02 * b09 +
b03 * b08 - b04 * b07 + b05 * b06;
if (!det) {
return null;
}
det = 1.0 / det;
out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det;
out[1] = (a02 * b10 - a01 * b11 - a03 * b09) * det;
out[2] = (a31 * b05 - a32 * b04 + a33 * b03) * det;
out[3] = (a22 * b04 - a21 * b05 - a23 * b03) * det;
out[4] = (a12 * b08 - a10 * b11 - a13 * b07) * det;
out[5] = (a00 * b11 - a02 * b08 + a03 * b07) * det;
out[6] = (a32 * b02 - a30 * b05 - a33 * b01) * det;
out[7] = (a20 * b05 - a22 * b02 + a23 * b01) * det;
out[8] = (a10 * b10 - a11 * b08 + a13 * b06) * det;
out[9] = (a01 * b08 - a00 * b10 - a03 * b06) * det;
out[10] = (a30 * b04 - a31 * b02 + a33 * b00) * det;
out[11] = (a21 * b02 - a20 * b04 - a23 * b00) * det;
out[12] = (a11 * b07 - a10 * b09 - a12 * b06) * det;
out[13] = (a00 * b09 - a01 * b07 + a02 * b06) * det;
out[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det;
out[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det;
return out;
}
/**
* Creates a styled button-shaped DOM element inside the
* VR-button container (creating the container the first time)
* and appends it.
*
* @param {string} message the visible label text (or HTML)
* @param {string} key optional keyboard shortcut to show in the
* label as " (key)"
* @param {string} icon optional icon URL rendered above the
* message
* @return {HTMLElement} the newly-appended button element
*/
function addButtonElement(message, key, icon)
{
var button_elt = document.createElement("div");
var webgl_canvas = document.getElementById(id_360);
var button_container = document.getElementById("vr-button-container" +
id_360);
if (!button_container) {
button_container = document.createElement("div");
button_container.id = "vr-button-container" + id_360;
Object.assign(button_container.style, {
fontFamily : "sans-serif",
position : "absolute",
zIndex : "999",
left : "0",
bottom : "0",
right : "0",
margin : "0",
padding : "0"
});
button_container.align = "right";
webgl_canvas.parentNode.appendChild(button_container);
}
Object.assign(button_elt.style, {
color: "#FFF",
fontWeight: "bold",
backgroundColor : "#888",
borderRadius : "5px",
border : "3px solid #555",
position : "relative",
display : "inline-block",
margin : "0.5em",
padding : "0.75em",
cursor : "pointer",
});
button_elt.align = "center";
if (icon) {
button_elt.innerHTML = "<img src='" + icon + "'/><br>" +
message;
} else {
button_elt.innerHTML = message;
}
if (key) {
var key_elt = document.createElement("span");
Object.assign(key_elt.style, {fontSize : "0.75em",
fontStyle : "italic" });
key_elt.innerHTML = " (" + key + ")";
button_elt.appendChild(key_elt);
}
button_container.appendChild(button_elt);
return button_elt;
}
/**
* Creates a button via addButtonElement and wires it up to
* $callback on both click and (optionally) keypress events.
*
* @param {string} message the visible label text (or HTML)
* @param {string} key optional keyboard shortcut character to
* also trigger the callback
* @param {string} icon optional icon URL
* @param {Function} callback invoked with the originating event
* @return {object} {element, keyListener} pair suitable for
* passing back to removeButton
*/
function addButton(message, key, icon, callback)
{
var key_listener = null;
if (key) {
var keyCode = key.charCodeAt(0);
/**
* Keyboard handler that invokes $callback when the
* configured keyCode is pressed.
*
* @param {KeyboardEvent} event
*/
key_listener = function (event) {
if (event.keyCode === keyCode) {
callback(event);
}
};
document.addEventListener("keydown", key_listener, false);
}
var element = addButtonElement(message, key, icon);
element.addEventListener("click", function (event) {
callback(event);
event.preventDefault();
}, false);
return {
element: element,
keyListener: key_listener
};
}
/**
* Removes a button previously returned by addButton from the
* DOM, and detaches its keyboard listener if present.
*
* @param {object} button {element, keyListener} pair
*/
function removeButton(button)
{
if (!button) {
return;
}
if (button.element.parentElement) {
button.element.parentElement.removeChild(button.element);
}
if (button.keyListener) {
document.removeEventListener("keydown", button.keyListener, false);
}
}
var project_matrix = new Float32Array(mat4_identity);
var view_matrix = new Float32Array(mat4_identity);
var vr_present_button = null;
// WebGL setup.
var gl = null;
var panorama = null;
/**
* WebGL context-loss handler: clears the cached gl/panorama
* objects so the next animation frame becomes a no-op until
* the context is restored.
*
* @param {Event} event the webglcontextlost event
*/
function onContextLost(event)
{
event.preventDefault();
console.log( 'WebGL Context Lost.' );
gl = null;
panorama = null;
}
/**
* WebGL context-restoration handler: re-runs init so the
* canvas is set back up with a fresh context.
*
* @param {Event} event the webglcontextrestored event
*/
function onContextRestored(event)
{
console.log( 'WebGL Context Restored.' );
init(vr_display ? vr_display.capabilities.hasExternalDisplay : false);
}
var webgl_canvas = document.getElementById(id_360);
webgl_canvas.addEventListener('webglcontextlost', onContextLost, false );
webgl_canvas.addEventListener('webglcontextrestored', onContextRestored,
false);
/**
* Acquires a WebGL context on the canvas, instantiates the
* Panorama renderer, and starts the animation loop.
*
* @param {boolean} preserve_drawing_buffer pass-through to
* getContext(); set to true when the canvas needs to
* remain readable across frames (required by some VR
* compositors)
*/
function init(preserve_drawing_buffer)
{
var glAttribs = {
alpha: false,
antialias: false,
preserveDrawingBuffer: preserve_drawing_buffer
};
gl = webgl_canvas.getContext("webgl", glAttribs);
if (!gl) {
gl = webgl_canvas.getContext("experimental-webgl", glAttribs);
if (!gl) {
return;
}
}
gl.enable(gl.DEPTH_TEST);
gl.enable(gl.CULL_FACE);
panorama = new VRPanorama(gl);
panorama.setImage(image_360);
// Wait until we have a WebGL context to resize and start rendering.
window.addEventListener("resize", onResize, false);
onResize();
window.requestAnimationFrame(onAnimationFrame);
}
// ================================
// WebVR-specific code begins here.
// ================================
/**
* Requests that the active VRDisplay begin presenting the
* canvas as a stereoscopic VR view; logs and swallows the
* error on failure so the page keeps working.
*/
function onVRRequestPresent() {
vr_display.requestPresent([{ source: webgl_canvas }]).then(function () {
}, function (err) {
var errMsg = "requestPresent failed.";
if (err && err.message) {
errMsg += "<br>" + err.message
}
console.log(errMsg);
});
}
/**
* Asks the VRDisplay to stop presenting; no-op when it isn't
* currently presenting.
*/
function onVRExitPresent()
{
if (!vr_display.isPresenting) {
return;
}
vr_display.exitPresent().then(function () {}, function () {
console.log("exitPresent failed.");
});
}
/**
* Listener for "vrdisplaypresentchange": swaps the VR button
* label between "enter VR" and "exit VR" as appropriate and
* resizes the canvas to match the current presentation state.
*/
function onVRPresentChange()
{
onResize();
var tl = document.getElementById('tl');
if (vr_display.isPresenting) {
if (vr_display.capabilities.hasExternalDisplay) {
removeButton(vr_present_button);
vr_present_button = addButton(tl['exit_vr'], "E", vr_png,
onVRExitPresent);
}
} else {
if (vr_display.capabilities.hasExternalDisplay) {
removeButton(vr_present_button);
vr_present_button = addButton(tl['enter_vr'], "E", vr_png,
onVRRequestPresent);
}
}
}
if (navigator.getVRDisplays) {
frame_data = new VRFrameData();
navigator.getVRDisplays().then(function (displays) {
var tl = document.getElementById('tl');
if (displays.length > 0) {
vr_display = displays[displays.length - 1];
vr_display.depthNear = 0.1;
vr_display.depthFar = 1024.0;
init(true);
if (vr_display.capabilities.canPresent) {
vr_present_button = addButton(tl["enter_vr"], "E",
vr_png, onVRRequestPresent);
}
// For the benefit of automated testing. Safe to ignore.
if (vr_display.capabilities.canPresent &&
WGLUUrl.getBool('canvasClickPresents', false)) {
webgl_canvas.addEventListener("click", onVRRequestPresent,
false);
}
window.addEventListener('vrdisplaypresentchange',
onVRPresentChange, false);
window.addEventListener('vrdisplayactivate', onVRRequestPresent,
false);
window.addEventListener('vrdisplaydeactivate', onVRExitPresent,
false);
} else {
init(false);
console.log("No VRDisplays found.");
}
},
function () {
console.log("No WebVR support.");
});
} else if (navigator.getVRDevices) {
init(false);
console.log("WebVR version too old.");
} else {
init(false);
console.log("No WebVR support.");
}
/**
* Resizes the WebGL canvas to fit either the VR headset's
* combined-eye resolution (when presenting) or the on-page
* element size scaled by devicePixelRatio (when not).
*/
function onResize() {
if (vr_display && vr_display.isPresenting) {
var left_eye = vr_display.getEyeParameters("left");
var right_eye = vr_display.getEyeParameters("right");
webgl_canvas.width = Math.max(left_eye.renderWidth,
right_eye.renderWidth) * 2;
webgl_canvas.height = Math.max(left_eye.renderHeight,
right_eye.renderHeight);
} else {
webgl_canvas.width =
webgl_canvas.offsetWidth * window.devicePixelRatio;
webgl_canvas.height =
webgl_canvas.offsetHeight * window.devicePixelRatio;
}
}
/**
* Converts a VR pose's orientation quaternion into a view
* matrix (inverted rotation, since the view matrix transforms
* the world relative to the head).
*
* @param {Float32Array} out destination view matrix
* @param {VRPose} pose the pose object from VRFrameData
*/
function getPoseMatrix(out, pose)
{
var orientation = pose.orientation;
if (!orientation) {
orientation = [0, 0, 0, 1];
}
mat4FromQuat(out, orientation);
mat4Invert(out, out);
}
/**
* Per-frame render callback: clears the canvas, asks the
* VRDisplay (or window) for the next frame, computes the pose
* matrix, and delegates rendering to the panorama for each eye
* (or a single mono view when not presenting).
*
* @param {number} t high-resolution timestamp from
* requestAnimationFrame
*/
function onAnimationFrame(t)
{
// do not attempt to render if there is no available WebGL context
if (!gl || !panorama) {
return;
}
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
if (vr_display) {
vr_display.requestAnimationFrame(onAnimationFrame);
vr_display.getFrameData(frame_data);
getPoseMatrix(view_matrix, frame_data.pose);
if (vr_display.isPresenting) {
gl.viewport(0, 0, webgl_canvas.width * 0.5,
webgl_canvas.height);
panorama.render(frame_data.leftProjectionMatrix, view_matrix);
gl.viewport(webgl_canvas.width * 0.5, 0,
webgl_canvas.width * 0.5, webgl_canvas.height);
panorama.render(frame_data.rightProjectionMatrix, view_matrix);
vr_display.submitFrame();
} else {
gl.viewport(0, 0, webgl_canvas.width, webgl_canvas.height);
mat4Perspective(project_matrix, Math.PI * 0.4,
webgl_canvas.width / webgl_canvas.height, 0.1, 1024.0);
panorama.render(project_matrix, view_matrix);
}
} else {
window.requestAnimationFrame(onAnimationFrame);
// No VRDisplay found.
gl.viewport(0, 0, webgl_canvas.width, webgl_canvas.height);
mat4Perspective(project_matrix, Math.PI * 0.4,
webgl_canvas.width / webgl_canvas.height, 0.1, 1024.0);
mat4Identity(view_matrix);
panorama.render(project_matrix, view_matrix);
}
}
};