holoplay
Version:
A library for the Looking Glass 3D display that relies on three.js to show content in true 3D from the browser.
1,495 lines (1,339 loc) • 48.3 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = global || self, factory(global.HoloPlay = {}));
}(this, (function (exports) { 'use strict';
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
function createCommonjsModule(fn, module) {
return module = { exports: {} }, fn(module, module.exports), module.exports;
}
var cbor = createCommonjsModule(function (module) {
/*
* The MIT License (MIT)
*
* Copyright (c) 2014 Patrick Gansterer <paroga@paroga.com>
*
* 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.
*/
(function(global, undefined$1) {var POW_2_24 = Math.pow(2, -24),
POW_2_32 = Math.pow(2, 32),
POW_2_53 = Math.pow(2, 53);
function encode(value) {
var data = new ArrayBuffer(256);
var dataView = new DataView(data);
var lastLength;
var offset = 0;
function ensureSpace(length) {
var newByteLength = data.byteLength;
var requiredLength = offset + length;
while (newByteLength < requiredLength)
newByteLength *= 2;
if (newByteLength !== data.byteLength) {
var oldDataView = dataView;
data = new ArrayBuffer(newByteLength);
dataView = new DataView(data);
var uint32count = (offset + 3) >> 2;
for (var i = 0; i < uint32count; ++i)
dataView.setUint32(i * 4, oldDataView.getUint32(i * 4));
}
lastLength = length;
return dataView;
}
function write() {
offset += lastLength;
}
function writeFloat64(value) {
write(ensureSpace(8).setFloat64(offset, value));
}
function writeUint8(value) {
write(ensureSpace(1).setUint8(offset, value));
}
function writeUint8Array(value) {
var dataView = ensureSpace(value.length);
for (var i = 0; i < value.length; ++i)
dataView.setUint8(offset + i, value[i]);
write();
}
function writeUint16(value) {
write(ensureSpace(2).setUint16(offset, value));
}
function writeUint32(value) {
write(ensureSpace(4).setUint32(offset, value));
}
function writeUint64(value) {
var low = value % POW_2_32;
var high = (value - low) / POW_2_32;
var dataView = ensureSpace(8);
dataView.setUint32(offset, high);
dataView.setUint32(offset + 4, low);
write();
}
function writeTypeAndLength(type, length) {
if (length < 24) {
writeUint8(type << 5 | length);
} else if (length < 0x100) {
writeUint8(type << 5 | 24);
writeUint8(length);
} else if (length < 0x10000) {
writeUint8(type << 5 | 25);
writeUint16(length);
} else if (length < 0x100000000) {
writeUint8(type << 5 | 26);
writeUint32(length);
} else {
writeUint8(type << 5 | 27);
writeUint64(length);
}
}
function encodeItem(value) {
var i;
if (value === false)
return writeUint8(0xf4);
if (value === true)
return writeUint8(0xf5);
if (value === null)
return writeUint8(0xf6);
if (value === undefined$1)
return writeUint8(0xf7);
switch (typeof value) {
case "number":
if (Math.floor(value) === value) {
if (0 <= value && value <= POW_2_53)
return writeTypeAndLength(0, value);
if (-POW_2_53 <= value && value < 0)
return writeTypeAndLength(1, -(value + 1));
}
writeUint8(0xfb);
return writeFloat64(value);
case "string":
var utf8data = [];
for (i = 0; i < value.length; ++i) {
var charCode = value.charCodeAt(i);
if (charCode < 0x80) {
utf8data.push(charCode);
} else if (charCode < 0x800) {
utf8data.push(0xc0 | charCode >> 6);
utf8data.push(0x80 | charCode & 0x3f);
} else if (charCode < 0xd800) {
utf8data.push(0xe0 | charCode >> 12);
utf8data.push(0x80 | (charCode >> 6) & 0x3f);
utf8data.push(0x80 | charCode & 0x3f);
} else {
charCode = (charCode & 0x3ff) << 10;
charCode |= value.charCodeAt(++i) & 0x3ff;
charCode += 0x10000;
utf8data.push(0xf0 | charCode >> 18);
utf8data.push(0x80 | (charCode >> 12) & 0x3f);
utf8data.push(0x80 | (charCode >> 6) & 0x3f);
utf8data.push(0x80 | charCode & 0x3f);
}
}
writeTypeAndLength(3, utf8data.length);
return writeUint8Array(utf8data);
default:
var length;
if (Array.isArray(value)) {
length = value.length;
writeTypeAndLength(4, length);
for (i = 0; i < length; ++i)
encodeItem(value[i]);
} else if (value instanceof Uint8Array) {
writeTypeAndLength(2, value.length);
writeUint8Array(value);
} else {
var keys = Object.keys(value);
length = keys.length;
writeTypeAndLength(5, length);
for (i = 0; i < length; ++i) {
var key = keys[i];
encodeItem(key);
encodeItem(value[key]);
}
}
}
}
encodeItem(value);
if ("slice" in data)
return data.slice(0, offset);
var ret = new ArrayBuffer(offset);
var retView = new DataView(ret);
for (var i = 0; i < offset; ++i)
retView.setUint8(i, dataView.getUint8(i));
return ret;
}
function decode(data, tagger, simpleValue) {
var dataView = new DataView(data);
var offset = 0;
if (typeof tagger !== "function")
tagger = function(value) { return value; };
if (typeof simpleValue !== "function")
simpleValue = function() { return undefined$1; };
function read(value, length) {
offset += length;
return value;
}
function readArrayBuffer(length) {
return read(new Uint8Array(data, offset, length), length);
}
function readFloat16() {
var tempArrayBuffer = new ArrayBuffer(4);
var tempDataView = new DataView(tempArrayBuffer);
var value = readUint16();
var sign = value & 0x8000;
var exponent = value & 0x7c00;
var fraction = value & 0x03ff;
if (exponent === 0x7c00)
exponent = 0xff << 10;
else if (exponent !== 0)
exponent += (127 - 15) << 10;
else if (fraction !== 0)
return fraction * POW_2_24;
tempDataView.setUint32(0, sign << 16 | exponent << 13 | fraction << 13);
return tempDataView.getFloat32(0);
}
function readFloat32() {
return read(dataView.getFloat32(offset), 4);
}
function readFloat64() {
return read(dataView.getFloat64(offset), 8);
}
function readUint8() {
return read(dataView.getUint8(offset), 1);
}
function readUint16() {
return read(dataView.getUint16(offset), 2);
}
function readUint32() {
return read(dataView.getUint32(offset), 4);
}
function readUint64() {
return readUint32() * POW_2_32 + readUint32();
}
function readBreak() {
if (dataView.getUint8(offset) !== 0xff)
return false;
offset += 1;
return true;
}
function readLength(additionalInformation) {
if (additionalInformation < 24)
return additionalInformation;
if (additionalInformation === 24)
return readUint8();
if (additionalInformation === 25)
return readUint16();
if (additionalInformation === 26)
return readUint32();
if (additionalInformation === 27)
return readUint64();
if (additionalInformation === 31)
return -1;
throw "Invalid length encoding";
}
function readIndefiniteStringLength(majorType) {
var initialByte = readUint8();
if (initialByte === 0xff)
return -1;
var length = readLength(initialByte & 0x1f);
if (length < 0 || (initialByte >> 5) !== majorType)
throw "Invalid indefinite length element";
return length;
}
function appendUtf16data(utf16data, length) {
for (var i = 0; i < length; ++i) {
var value = readUint8();
if (value & 0x80) {
if (value < 0xe0) {
value = (value & 0x1f) << 6
| (readUint8() & 0x3f);
length -= 1;
} else if (value < 0xf0) {
value = (value & 0x0f) << 12
| (readUint8() & 0x3f) << 6
| (readUint8() & 0x3f);
length -= 2;
} else {
value = (value & 0x0f) << 18
| (readUint8() & 0x3f) << 12
| (readUint8() & 0x3f) << 6
| (readUint8() & 0x3f);
length -= 3;
}
}
if (value < 0x10000) {
utf16data.push(value);
} else {
value -= 0x10000;
utf16data.push(0xd800 | (value >> 10));
utf16data.push(0xdc00 | (value & 0x3ff));
}
}
}
function decodeItem() {
var initialByte = readUint8();
var majorType = initialByte >> 5;
var additionalInformation = initialByte & 0x1f;
var i;
var length;
if (majorType === 7) {
switch (additionalInformation) {
case 25:
return readFloat16();
case 26:
return readFloat32();
case 27:
return readFloat64();
}
}
length = readLength(additionalInformation);
if (length < 0 && (majorType < 2 || 6 < majorType))
throw "Invalid length";
switch (majorType) {
case 0:
return length;
case 1:
return -1 - length;
case 2:
if (length < 0) {
var elements = [];
var fullArrayLength = 0;
while ((length = readIndefiniteStringLength(majorType)) >= 0) {
fullArrayLength += length;
elements.push(readArrayBuffer(length));
}
var fullArray = new Uint8Array(fullArrayLength);
var fullArrayOffset = 0;
for (i = 0; i < elements.length; ++i) {
fullArray.set(elements[i], fullArrayOffset);
fullArrayOffset += elements[i].length;
}
return fullArray;
}
return readArrayBuffer(length);
case 3:
var utf16data = [];
if (length < 0) {
while ((length = readIndefiniteStringLength(majorType)) >= 0)
appendUtf16data(utf16data, length);
} else
appendUtf16data(utf16data, length);
return String.fromCharCode.apply(null, utf16data);
case 4:
var retArray;
if (length < 0) {
retArray = [];
while (!readBreak())
retArray.push(decodeItem());
} else {
retArray = new Array(length);
for (i = 0; i < length; ++i)
retArray[i] = decodeItem();
}
return retArray;
case 5:
var retObject = {};
for (i = 0; i < length || length < 0 && !readBreak(); ++i) {
var key = decodeItem();
retObject[key] = decodeItem();
}
return retObject;
case 6:
return tagger(decodeItem(), length);
case 7:
switch (length) {
case 20:
return false;
case 21:
return true;
case 22:
return null;
case 23:
return undefined$1;
default:
return simpleValue(length);
}
}
}
var ret = decodeItem();
if (offset !== data.byteLength)
throw "Remaining bytes";
return ret;
}
var obj = { encode: encode, decode: decode };
if (typeof undefined$1 === "function" && undefined$1.amd)
undefined$1("cbor/cbor", obj);
else if ( module.exports)
module.exports = obj;
else if (!global.CBOR)
global.CBOR = obj;
})(commonjsGlobal);
});
/**
* This files defines the HoloPlayClient class and Message class.
*
* Copyright (c) [2019] [Looking Glass Factory]
*
* @link https://lookingglassfactory.com/
* @file This files defines the HoloPlayClient class and Message class.
* @author Looking Glass Factory.
* @version 0.0.8
* @license SEE LICENSE IN LICENSE.md
*/
// Polyfill WebSocket for nodejs applications.
const WebSocket =
typeof window === 'undefined' ? require('ws') : window.WebSocket;
/** Class representing a client to communicates with the HoloPlayService. */
class Client {
/**
* Establish a client to talk to HoloPlayService.
* @constructor
* @param {function} initCallback - optional; a function to trigger when
* response is received
* @param {function} errCallback - optional; a function to trigger when there
* is a connection error
* @param {function} closeCallback - optional; a function to trigger when the
* socket is closed
* @param {boolean} debug - optional; default is false
* @param {string} appId - optional
* @param {boolean} isGreedy - optional
* @param {string} oncloseBehavior - optional, can be 'wipe', 'hide', 'none'
*/
constructor(
initCallback, errCallback, closeCallback, debug = false, appId, isGreedy,
oncloseBehavior) {
this.reqs = [];
this.reps = [];
this.requestId = this.getRequestId();
this.debug = debug;
this.isGreedy = isGreedy;
this.errCallback = errCallback;
this.closeCallback = closeCallback;
this.alwaysdebug = false;
this.isConnected = false;
let initCmd = null;
if (appId || isGreedy || oncloseBehavior) {
initCmd = new InitMessage(appId, isGreedy, oncloseBehavior, this.debug);
} else {
if (debug) this.alwaysdebug = true;
if (typeof initCallback == 'function') initCmd = new InfoMessage();
}
this.openWebsocket(initCmd, initCallback);
}
/**
* Send a message over the websocket to HoloPlayService.
* @public
* @param {Message} msg - message object
* @param {integer} timeoutSecs - optional, default is 60 seconds
*/
sendMessage(msg, timeoutSecs = 60) {
if (this.alwaysdebug) msg.cmd.debug = true;
let cborData = msg.toCbor();
return this.sendRequestObj(cborData, timeoutSecs);
}
/**
* Disconnects from the web socket.
* @public
*/
disconnect() {
this.ws.close();
}
/**
* Open a websocket and set handlers
* @private
*/
openWebsocket(firstCmd = null, initCallback = null) {
this.ws =
new WebSocket('ws://localhost:11222/driver', ['rep.sp.nanomsg.org']);
this.ws.parent = this;
this.ws.binaryType = 'arraybuffer';
this.ws.onmessage = this.messageHandler;
this.ws.onopen = (() => {
this.isConnected = true;
if (this.debug) {
console.log('socket open');
}
if (firstCmd != null) {
this.sendMessage(firstCmd).then(initCallback);
}
});
this.ws.onerror = this.onSocketError;
this.ws.onclose = this.onClose;
}
/**
* Send a request object over websocket
* @private
*/
sendRequestObj(data, timeoutSecs) {
return new Promise((resolve, reject) => {
let reqObj = {
id: this.requestId++,
parent: this,
payload: data,
success: resolve,
error: reject,
send: function() {
if (this.debug)
console.log('attemtping to send request with ID ' + this.id);
this.timeout = setTimeout(reqObj.send.bind(this), timeoutSecs * 1000);
let tmp = new Uint8Array(data.byteLength + 4);
let view = new DataView(tmp.buffer);
view.setUint32(0, this.id);
tmp.set(new Uint8Array(this.payload), 4);
this.parent.ws.send(tmp.buffer);
}
};
this.reqs.push(reqObj);
reqObj.send();
});
}
/**
* Handles a message when received
* @private
*/
messageHandler(event) {
console.log('message');
let data = event.data;
if (data.byteLength < 4) return;
let view = new DataView(data);
let replyId = view.getUint32(0);
if (replyId < 0x80000000) {
this.parent.err('bad nng header');
return;
}
let i = this.parent.findReqIndex(replyId);
if (i == -1) {
this.parent.err('got reply that doesn\'t match known request!');
return;
}
let rep = {id: replyId, payload: cbor.decode(data.slice(4))};
if (rep.payload.error == 0) {
this.parent.reqs[i].success(rep.payload);
} else {
this.parent.reqs[i].error(rep.payload);
}
clearTimeout(this.parent.reqs[i].timeout);
this.parent.reqs.splice(i, 1);
this.parent.reps.push(rep);
if (this.debug) {
console.log(rep.payload);
}
}
getRequestId() {
return Math.floor(this.prng() * (0x7fffffff)) + 0x80000000;
}
onClose(event) {
this.parent.isConnected = false;
if (this.parent.debug) {
console.log('socket closed');
}
if (typeof this.parent.closeCallback == 'function')
this.parent.closeCallback(event);
}
onSocketError(error) {
if (this.parent.debug) {
console.log(error);
}
if (typeof this.parent.errCallback == 'function') {
this.parent.errCallback(error);
}
}
err(errorMsg) {
if (this.debug) {
console.log('[DRIVER ERROR]' + errorMsg);
}
// TODO : make this return an event obj rather than a string
// if (typeof this.errCallback == 'function')
// this.errCallback(errorMsg);
}
findReqIndex(replyId) {
let i = 0;
for (; i < this.reqs.length; i++) {
if (this.reqs[i].id == replyId) {
return i;
}
}
return -1;
}
prng() {
if (this.rng == undefined) {
this.rng = generateRng();
}
return this.rng();
}
}
/** A class to represent messages being sent over to HoloPlay Service */
class Message {
/**
* Construct a barebone message.
* @constructor
*/
constructor(cmd, bin) {
this.cmd = cmd;
this.bin = bin;
}
/**
* Convert the class instance to the CBOR format
* @public
* @returns {CBOR} - cbor object of the message
*/
toCbor() {
return cbor.encode(this);
}
}
/** Message to init. Extends the base Message class. */
class InitMessage extends Message {
/**
* @constructor
* @param {string} appId - a unique id for app
* @param {boolean} isGreedy - will it take over screen
* @param {string} oncloseBehavior - can be 'wipe', 'hide', 'none'
*/
constructor(appId = '', isGreedy = false, onclose = '', debug = false) {
let cmd = {'init': {}};
if (appId != '') cmd['init'].appid = appId;
if (onclose != '') cmd['init'].onclose = onclose;
if (isGreedy) cmd['init'].greedy = true;
if (debug) cmd['init'].debug = true;
super(cmd, null);
}
}
/** Get info from the HoloPlayService */
class InfoMessage extends Message {
/**
* @constructor
*/
constructor() {
let cmd = {'info': {}};
super(cmd, null);
}
}
/* helper function */
function generateRng() {
function xmur3(str) {
for (var i = 0, h = 1779033703 ^ str.length; i < str.length; i++)
h = Math.imul(h ^ str.charCodeAt(i), 3432918353), h = h << 13 | h >>> 19;
return function() {
h = Math.imul(h ^ h >>> 16, 2246822507);
h = Math.imul(h ^ h >>> 13, 3266489909);
return (h ^= h >>> 16) >>> 0;
}
}
function xoshiro128ss(a, b, c, d) {
return (() => {
var t = b << 9, r = a * 5;
r = (r << 7 | r >>> 25) * 9;
c ^= a;
d ^= b;
b ^= c;
a ^= d;
c ^= t;
d = d << 11 | d >>> 21;
return (r >>> 0) / 4294967296;
})
} var state = Date.now();
var seed = xmur3(state.toString());
return xoshiro128ss(seed(), seed(), seed(), seed());
}
const DEFAULT_CALIBRATION = {
'configVersion': '1.0',
'serial': '00000',
'pitch': {'value': 47.556365966796878},
'slope': {'value': -5.488804340362549},
'center': {'value': 0.15815216302871705},
'viewCone': {'value': 40.0},
'invView': {'value': 1.0},
'verticalAngle': {'value': 0.0},
'DPI': {'value': 338.0},
'screenW': {'value': 2560.0},
'screenH': {'value': 1600.0},
'flipImageX': {'value': 0.0},
'flipImageY': {'value': 0.0},
'flipSubp': {'value': 0.0}
};
function checkDriverInstall() {
if (confirm(
'HoloPlayService not detected! Click OK to download. If you have it already installed, please make sure it is running.')) {
window.location.href = 'http://look.glass/holoplayservice';
}
}
let cachedPromise = null;
function getDevices() {
if (!cachedPromise) {
cachedPromise = new Promise((resolve, reject) => {
new Client(
(data) => {
if (data.devices.length == 0) {
alert(
'No Looking Glass detected. Please make sure your Looking Glass is plugged in.');
}
resolve(data.devices);
},
(err) => {
console.error('error loading calibration', err);
checkDriverInstall();
reject(err);
},
console.log);
});
}
return cachedPromise;
}
function getFirstDevice() {
return getDevices().then((devices) => {
if (devices.length == 0) {
throw new Error('no devices');
}
return devices[0];
});
}
function getCalibration() {
return getFirstDevice()
.then((d) => {
return d.calibration;
})
.catch((err) => {
console.error('no devices connected. using default calibration.');
return DEFAULT_CALIBRATION;
});
}
const IDEAL_PROPERTIES = {
'standard': {
'quiltResolution': 4096,
'tileCount': new THREE.Vector2(5, 9),
'viewCone': 35,
'fov': 12.5,
},
'large': {
'quiltResolution': 4096,
'tileCount': new THREE.Vector2(5, 9),
'viewCone': 35,
'fov': 12.5,
},
'pro': {
'quiltResolution': 4096,
'tileCount': new THREE.Vector2(5, 9),
'viewCone': 35,
'fov': 12.5,
},
'8k': {
'quiltResolution': 8192,
'tileCount': new THREE.Vector2(5, 9),
'viewCone': 35,
'fov': 12.5,
},
'default': {
'quiltResolution': 2048,
'tileCount': new THREE.Vector2(4, 8),
'viewCone': 35,
'fov': 12.5,
},
};
function getIdealProperty(propName) {
return getFirstDevice()
.then((d) => {
return IDEAL_PROPERTIES[d.hardwareVersion][propName];
})
.catch((err) => {
return IDEAL_PROPERTIES['default'][propName];
});
}
function getIdealQuiltResolution() {
return getIdealProperty('quiltResolution');
}
function getIdealQuiltTileCount() {
return getIdealProperty('tileCount');
}
function getIdealViewCone() {
return getIdealProperty('viewCone');
}
function getIdealFov() {
return getIdealProperty('fov');
}
var calibration = /*#__PURE__*/Object.freeze({
__proto__: null,
DEFAULT_CALIBRATION: DEFAULT_CALIBRATION,
getFirstDevice: getFirstDevice,
getCalibration: getCalibration,
getIdealQuiltResolution: getIdealQuiltResolution,
getIdealQuiltTileCount: getIdealQuiltTileCount,
getIdealViewCone: getIdealViewCone,
getIdealFov: getIdealFov
});
const QUILT_VERTEX_SHADER = `
varying vec2 iUv;
void main() {
iUv = uv;
vec4 modelViewPosition = modelViewMatrix * vec4(position, 1.0);
gl_Position = projectionMatrix * modelViewPosition;
}
`;
const QUILT_FRAGMENT_SHADER = `
uniform sampler2D quiltTexture;
uniform float pitch;
uniform float tilt;
uniform float center;
uniform float invView;
uniform float flipX;
uniform float flipY;
uniform float subp;
uniform float tilesX;
uniform float tilesY;
uniform vec2 quiltViewPortion;
varying vec2 iUv;
vec2 texArr(vec3 uvz) {
float z = floor(uvz.z * tilesX * tilesY);
float x = (mod(z, tilesX) + uvz.x) / tilesX;
float y = (floor(z / tilesX) + uvz.y) / tilesY;
return vec2(x, y) * quiltViewPortion;
}
float remap(float value, float from1, float to1, float from2, float to2) {
return (value - from1) / (to1 - from1) * (to2 - from2) + from2;
}
void main() {
vec4 rgb[3];
vec3 nuv = vec3(iUv.xy, 0.0);
// Flip UVs if necessary
nuv.x = (1.0 - flipX) * nuv.x + flipX * (1.0 - nuv.x);
nuv.y = (1.0 - flipY) * nuv.y + flipY * (1.0 - nuv.y);
for (int i = 0; i < 3; i++) {
nuv.z = (iUv.x + float(i) * subp + iUv.y * tilt) * pitch - center;
nuv.z = mod(nuv.z + ceil(abs(nuv.z)), 1.0);
nuv.z = (1.0 - invView) * nuv.z + invView * (1.0 - nuv.z);
rgb[i] = texture2D(quiltTexture, texArr(vec3(iUv.x, iUv.y, nuv.z)));
}
gl_FragColor = vec4(rgb[0].r, rgb[1].g, rgb[2].b, 1);
}
`;
const QUILT_SHADER_PROPERTIES = {
uniforms: {
quiltTexture: {value: null},
pitch: {value: 0},
tilt: {value: 0},
center: {value: 0},
invView: {value: 0},
flipX: {value: 0},
flipY: {value: 0},
subp: {value: 0},
tilesX: {value: 0},
tilesY: {value: 0},
screenW: {value: 0},
screenH: {value: 0},
quiltViewPortion: {value: new THREE.Vector2(1, 1)},
},
vertexShader: QUILT_VERTEX_SHADER,
fragmentShader: QUILT_FRAGMENT_SHADER,
};
class QuiltRenderer {
constructor(tileCount, opt_renderer) {
getCalibration().then((calibration) => {
this.calibrationData = calibration;
this.updateCalibration();
});
this.tileCount = tileCount;
this.scene = new THREE.Scene();
const quiltPlaneGeometry = new THREE.PlaneGeometry(1, 1);
const quiltPlaneMaterial =
new THREE.ShaderMaterial(QUILT_SHADER_PROPERTIES);
this.quiltPlane = new THREE.Mesh(quiltPlaneGeometry, quiltPlaneMaterial);
this.scene.add(this.quiltPlane);
this.camera = new THREE.OrthographicCamera(-0.5, 0.5, 0.5, -0.5, 0);
this.renderer = opt_renderer || new THREE.WebGLRenderer();
this.renderSize = new THREE.Vector2(window.innerWidth, window.innerHeight);
this.domElement = this.renderer.domElement;
this.domElement.style.position = 'absolute';
}
render() {
this.renderer.setSize(this.renderSize.x, this.renderSize.y);
this.negateWindowZoomAndOffset();
this.renderer.render(this.scene, this.camera);
}
update(quiltResolution, tileCount) {
this.tileCount.copy(tileCount);
this.quiltPlane.material.uniforms.tilesX.value = this.tileCount.x;
this.quiltPlane.material.uniforms.tilesY.value = this.tileCount.y;
this.quiltPlane.material.uniforms.quiltViewPortion.value.set(
(Math.floor(quiltResolution / this.tileCount.x) * this.tileCount.x) /
quiltResolution,
(Math.floor(quiltResolution / this.tileCount.y) * this.tileCount.y) /
quiltResolution);
}
negateWindowZoomAndOffset() {
const windowZoom =
window.outerWidth / document.body.getBoundingClientRect().width;
const browserToolbarHeight = window.outerHeight - window.innerHeight;
this.domElement.style.width = `${this.domElement.width / windowZoom}px`;
this.domElement.style.height = `${this.domElement.height / windowZoom}px`;
this.domElement.style.left =
`${(window.screen.availLeft - window.screenLeft) / windowZoom}px`;
this.domElement.style.top =
`${- (window.screenTop + browserToolbarHeight) / windowZoom}px`;
}
setQuiltImageURL(url) {
const quiltImage = new Image();
quiltImage.src = url;
this.setQuiltImage(quiltImage);
}
setQuiltImage(quiltImage) {
const imageTexture = new THREE.Texture();
imageTexture.image = quiltImage;
imageTexture.minFilter = THREE.NearestFilter;
imageTexture.magFilter = THREE.NearestFilter;
this.setQuiltTexture(imageTexture);
// Update the texture when the image loads.
quiltImage.addEventListener('load', () => {
imageTexture.needsUpdate = true;
this.render();
});
if (quiltImage.complete) {
// Force a load event for an already downloaded file.
quiltImage.load();
}
}
setQuiltTexture(quiltTexture) {
this.quiltPlane.material.uniforms.quiltTexture.value = quiltTexture;
}
updateCalibration() {
if (!this.calibrationData) {
return;
}
this.renderSize.set(
this.calibrationData.screenW.value, this.calibrationData.screenH.value);
const quiltMaterial = this.quiltPlane.material;
const screenInches =
this.calibrationData.screenW.value / this.calibrationData.DPI.value;
let newPitch = this.calibrationData.pitch.value * screenInches;
// account for tilt in measuring pitch horizontally
newPitch *= Math.cos(Math.atan(1.0 / this.calibrationData.slope.value));
quiltMaterial.uniforms.pitch.value = newPitch;
// tilt
let newTilt = this.calibrationData.screenH.value /
(this.calibrationData.screenW.value * this.calibrationData.slope.value);
if (this.calibrationData.flipImageX.value == 1) {
newTilt *= -1;
}
quiltMaterial.uniforms.tilt.value = newTilt;
// center
// I need the relationship between the amount of pixels I have moved over to
// the amount of lenticulars I have jumped
// ie how many pixels are there to a lenticular?
quiltMaterial.uniforms.center.value = this.calibrationData.center.value;
// should we invert?
quiltMaterial.uniforms.invView.value = this.calibrationData.invView.value;
// Should we flip it for peppers?
quiltMaterial.uniforms.flipX.value = this.calibrationData.flipImageX.value;
quiltMaterial.uniforms.flipY.value = this.calibrationData.flipImageY.value;
quiltMaterial.uniforms.subp.value =
1 / (this.calibrationData.screenW.value * 3);
// tiles
quiltMaterial.uniforms.tilesX.value = this.tileCount.x;
quiltMaterial.uniforms.tilesY.value = this.tileCount.y;
};
}
/*! (c) Andrea Giammarchi - ISC */
var self$1 = undefined || /* istanbul ignore next */ {};
try {
self$1.EventTarget = (new EventTarget).constructor;
} catch(EventTarget) {
(function (Object, wm) {
var create = Object.create;
var defineProperty = Object.defineProperty;
var proto = EventTarget.prototype;
define(proto, 'addEventListener', function (type, listener, options) {
for (var
secret = wm.get(this),
listeners = secret[type] || (secret[type] = []),
i = 0, length = listeners.length; i < length; i++
) {
if (listeners[i].listener === listener)
return;
}
listeners.push({target: this, listener: listener, options: options});
});
define(proto, 'dispatchEvent', function (event) {
var secret = wm.get(this);
var listeners = secret[event.type];
if (listeners) {
define(event, 'target', this);
define(event, 'currentTarget', this);
listeners.slice(0).forEach(dispatch, event);
delete event.currentTarget;
delete event.target;
}
return true;
});
define(proto, 'removeEventListener', function (type, listener) {
for (var
secret = wm.get(this),
listeners = secret[type] || (secret[type] = []),
i = 0, length = listeners.length; i < length; i++
) {
if (listeners[i].listener === listener) {
listeners.splice(i, 1);
return;
}
}
});
self$1.EventTarget = EventTarget;
function EventTarget() { wm.set(this, create(null));
}
function define(target, name, value) {
defineProperty(
target,
name,
{
configurable: true,
writable: true,
value: value
}
);
}
function dispatch(info) {
var options = info.options;
if (options && options.once)
info.target.removeEventListener(this.type, info.listener);
if (typeof info.listener === 'function')
info.listener.call(info.target, this);
else
info.listener.handleEvent(this);
}
}(Object, new WeakMap));
}
var EventTarget$1 = self$1.EventTarget;
const BUTTON_NAMES = ['square', 'left', 'right', 'circle'];
class Buttons extends EventTarget$1 {
constructor() {
super();
this.lastButtonState = [];
for (let i = 0; i < BUTTON_NAMES.length; i++) {
this.lastButtonState.push(false);
}
this.gamepadInfo = null;
window.addEventListener('gamepadconnected', (e) => {
if (e.gamepad.id.indexOf('HoloPlay') > -1) {
this.gamepadInfo = navigator.getGamepads()[e.gamepad.index];
}
});
window.addEventListener('gamepaddisconnected', (e) => {
if (e.gamepad.id.indexOf('HoloPlay') > -1) {
this.gamepadInfo = null;
}
});
requestAnimationFrame(this.update.bind(this));
}
update() {
requestAnimationFrame(this.update.bind(this));
if (this.gamepadInfo == null) {
return;
}
let holoplayGamepad;
let allGamepads = navigator.getGamepads();
for (let i = 0; i < allGamepads.length; i++) {
if (allGamepads[i].index == this.gamepadInfo.index) {
holoplayGamepad = allGamepads[i];
break;
}
}
if (!holoplayGamepad) {
return;
}
for (let i = 0; i < holoplayGamepad.buttons.length; i++) {
if (!this.lastButtonState[i] && holoplayGamepad.buttons[i].pressed) {
this.dispatchButtonEvent('buttonDown', i);
this.dispatchButtonEvent('buttonPressed', i);
} else if (
this.lastButtonState[i] && holoplayGamepad.buttons[i].pressed) {
this.dispatchButtonEvent('buttonPressed', i);
} else if (
this.lastButtonState[i] && !holoplayGamepad.buttons[i].pressed) {
this.dispatchButtonEvent('buttonUp', i);
}
this.lastButtonState[i] = holoplayGamepad.buttons[i].pressed;
}
}
dispatchButtonEvent(type, buttonIndex) {
this.dispatchEvent(new CustomEvent(type, {
detail: {
name: BUTTON_NAMES[buttonIndex],
index: buttonIndex,
}
}));
}
}
class Camera extends THREE.ArrayCamera {
constructor() {
super();
this.tileCount = new THREE.Vector2();
this.target = new THREE.Vector3(0, 0, 0);
this.position.set(0, 0, 1);
// just some defaults. we'll get them from calibration in a sec.
this.fov = 12.5;
this.aspect = 2560 / 1600;
this.viewCone = 35;
getIdealViewCone().then((viewCone) => {
this.viewCone = viewCone;
});
getIdealFov().then((fov) => {
this.fov = fov;
this.cameras.forEach((c) => {
c.fov = this.fov;
c.updateProjectionMatrix();
});
});
getCalibration().then((calibration) => {
this.aspect = calibration.screenW.value / calibration.screenH.value;
this.cameras.forEach((c) => {
c.aspect = this.aspect;
c.updateProjectionMatrix();
});
});
}
update(quiltResolution, tileCount) {
// rebuild camera array if the camera count changes
if (this.tileCount.x != tileCount.x || this.tileCount.y != tileCount.y) {
this.cameras = [];
for (let i = 0; i < tileCount.x * tileCount.y; i++) {
const subcamera = new THREE.PerspectiveCamera(this.fov, this.aspect);
subcamera.viewport = new THREE.Vector4();
this.cameras.push(subcamera);
}
this.tileCount.copy(tileCount);
}
const tileSize = new THREE.Vector2(
Math.floor(quiltResolution / tileCount.x),
Math.floor(quiltResolution / tileCount.y));
for (let i = 0; i < this.cameras.length; i++) {
const subcamera = this.cameras[i];
subcamera.rotation.copy(this.rotation);
const tileGridPos =
new THREE.Vector2(i % tileCount.x, Math.floor(i / tileCount.x));
subcamera.viewport.set(
tileGridPos.x * tileSize.x, tileGridPos.y * tileSize.y, tileSize.x,
tileSize.y);
const distanceToTarget = this.position.distanceTo(this.target);
const angleToThisCamera = THREE.Math.degToRad(THREE.Math.lerp(
-this.viewCone / 2, this.viewCone / 2,
i / (this.cameras.length - 1)));
const offsetAlongBaseline =
distanceToTarget * Math.tan(angleToThisCamera);
subcamera.position.copy(
this.localToWorld(new THREE.Vector3(offsetAlongBaseline, 0, 0)));
subcamera.updateMatrixWorld();
subcamera.projectionMatrix.elements[8] = -2 * offsetAlongBaseline /
(2 * distanceToTarget *
Math.tan(0.5 * THREE.Math.degToRad(subcamera.fov)) *
subcamera.aspect);
}
}
lookAt(targetPosition) {
super.lookAt(targetPosition);
this.target.copy(targetPosition);
}
}
/*!
* fullscreen-polyfill
* 1.0.2 - 5/23/2018
* https://github.com/nguyenj/fullscreen-polyfill#readme
* (c) John Nguyen; MIT License
*/
var FullscreenPolyfill=function(){var l=["fullscreen","fullscreenEnabled","fullscreenElement","fullscreenchange","fullscreenerror","exitFullscreen","requestFullscreen"],e=["webkitIsFullScreen","webkitFullscreenEnabled","webkitFullscreenElement","webkitfullscreenchange","webkitfullscreenerror","webkitExitFullscreen","webkitRequestFullscreen"],n=["mozFullScreen","mozFullScreenEnabled","mozFullScreenElement","mozfullscreenchange","mozfullscreenerror","mozCancelFullScreen","mozRequestFullScreen"],u=["","msFullscreenEnabled","msFullscreenElement","MSFullscreenChange","MSFullscreenError","msExitFullscreen","msRequestFullscreen"];document||(document={});var t,c=(t=[l[1],e[1],n[1],u[1]].find(function(e){return document[e]}),[l,e,n,u].find(function(e){return e.find(function(e){return e===t})})||[]);function r(e,n){document[l[0]]=document[c[0]]||!!document[c[2]]||!1,document[l[1]]=document[c[1]]||!1,document[l[2]]=document[c[2]]||null,document.dispatchEvent(new Event(e),n.target);}return document[l[1]]?{}:(document[l[0]]=document[c[0]]||!!document[c[2]]||!1,document[l[1]]=document[c[1]]||!1,document[l[2]]=document[c[2]]||null,document.addEventListener(c[3],r.bind(document,l[3]),!1),document.addEventListener(c[4],r.bind(document,l[4]),!1),document[l[5]]=function(){return document[c[5]]()},void(Element.prototype[l[6]]=function(){return this[c[6]].apply(this,arguments)}))}();
class FullscreenHelper {
constructor(renderer) {
this.renderer = renderer;
this.lkgDevice = null;
getFirstDevice().then((d) => {
this.lkgDevice = d;
const startOnLkg = this.isOnLkg();
if (startOnLkg) {
this.renderer.render2d = false;
this.moveWarning.style.display = 'none';
} else {
this.renderer.render2d = true;
this.moveWarning.style.display = '';
}
requestAnimationFrame(this.update.bind(this));
});
this.onLkg = null;
this.moveWarning = this.makeMoveWarning();
document.body.appendChild(this.moveWarning);
this.fullscreenButton = this.makeFullscreenButton();
document.body.appendChild(this.fullscreenButton);
}
isOnLkg() {
const sameResolution =
window.screen.width == this.lkgDevice.calibration.screenW.value &&
window.screen.height == this.lkgDevice.calibration.screenH.value;
const sameScreenPos =
window.screen.availLeft == this.lkgDevice.windowCoords[0];
return sameResolution && sameScreenPos;
}
update() {
requestAnimationFrame(this.update.bind(this));
const nowOnLkg = this.isOnLkg();
if (nowOnLkg && !this.onLkg) {
// just moved to lkg
this.renderer.render2d = false;
this.moveWarning.style.display = 'none';
} else if (!nowOnLkg && this.onLkg) {
// just moved OFF lkg
this.renderer.render2d = true;
this.moveWarning.style.display = '';
}
this.onLkg = nowOnLkg;
if (this.onLkg && !document.fullscreen) {
if (document.hasFocus()) {
this.fullscreenButton.innerText = 'click to go fullscreen';
} else {
this.fullscreenButton.innerText = 'click to focus window';
}
this.fullscreenButton.style.display = '';
} else {
this.fullscreenButton.style.display = 'none';
}
}
makeMoveWarning() {
const moveWarning = document.createElement('div');
moveWarning.innerText = 'drag this window to the Looking Glass';
moveWarning.style.cssText = `
background: white;
border-radius: 8px;
bottom: 0px;
display: none;
font-family: sans-serif;
font-size: 6em;
left: 0px;
margin: 16px;
opacity: 0.5;
padding: 8px;
position: absolute;
z-index: ${Number.MAX_SAFE_INTEGER};
`;
return moveWarning;
}
makeFullscreenButton() {
const fullscreenButton = document.createElement('div');
fullscreenButton.innerText = 'go fullscreen';
fullscreenButton.style.cssText = `
background: white;
border-radius: 8px;
bottom: 0px;
display: none;
font-family: sans-serif;
font-size: 6em;
font-weight: bold;
left: 0px;
margin: 16px;
opacity: 0.75;
padding: 8px;
pointer-events: none;
position: absolute;
z-index: ${Number.MAX_SAFE_INTEGER};
`;
window.addEventListener('click', () => {
this.renderer.domElement.requestFullscreen();
});
return fullscreenButton;
}
}
class Renderer {
constructor(opt_options) {
this.enforceMandatoryDocumentStyle();
let options = opt_options || {};
this.quiltResolution = options.quiltResolution || 4096;
this.tileCount = options.tileCount || new THREE.Vector2(5, 9);
this.render2d = options.render2d || false;
this.renderQuilt = options.render2d || false;
this.fullscreenHelper = null;
if (!options.disableFullscreenUi) {
this.fullscreenHelper = new FullscreenHelper(this);
}
this.webglRenderer = new THREE.WebGLRenderer();
this.domElement = this.webglRenderer.domElement;
this.quiltRenderTarget = new THREE.WebGLRenderTarget(
this.quiltResolution, this.quiltResolution, {format: THREE.RGBFormat});
this.quiltRenderer = new QuiltRenderer(this.tileCount, this.webglRenderer);
this.quiltRenderer.setQuiltTexture(this.quiltRenderTarget.texture);
if (!options.quiltResolution) {
getIdealQuiltResolution().then((res) => {
this.quiltResolution = res;
});
}
if (!options.tileCount) {
getIdealQuiltTileCount().then((count) => {
this.tileCount.copy(count);
});
}
this.debug2dCamera = new THREE.PerspectiveCamera();
getIdealFov().then((fov) => {
this.debug2dCamera.fov = fov;
this.debug2dCamera.updateProjectionMatrix();
});
}
enforceMandatoryDocumentStyle() {
document.body.style.margin = '0px';
document.body.style.overflow = 'hidden';
}
render(scene, camera) {
this.enforceMandatoryDocumentStyle();
if (this.render2d) {
this.debug2dCamera.position.copy(camera.position);
this.debug2dCamera.rotation.copy(camera.rotation);
const aspect = window.innerWidth / window.innerHeight;
if (aspect != this.debug2dCamera.aspect) {
this.debug2dCamera.aspect = aspect;
this.debug2dCamera.updateProjectionMatrix();
}
this.webglRenderer.domElement.style.top =
this.webglRenderer.domElement.style.left = '0px';
this.webglRenderer.setSize(window.innerWidth, window.innerHeight);
this.webglRenderer.render(scene, this.debug2dCamera);
} else if (this.renderQuilt) {
camera.update(this.quiltResolution, this.tileCount);
const minWindowDimension =
Math.min(window.innerWidth, window.innerHeight);
this.webglRenderer.domElement.style.width =
this.webglRenderer.domElement.style.height =
`${minWindowDimension}px`;
this.webglRenderer.domElement.style.top =
this.webglRenderer.domElement.style.left = '0px';
this.webglRenderer.setRenderTarget(null);
this.webglRenderer.setSize(
this.quiltResolution, this.quiltResolution, false);
this.webglRenderer.render(scene, camera);
} else {
this.quiltRenderTarget.setSize(
this.quiltResolution, this.quiltResolution);
camera.update(this.quiltResolution, this.tileCount);
this.webglRenderer.setRenderTarget(this.quiltRenderTarget);
this.webglRenderer.setSize(
this.quiltResolution, this.quiltResolution, false);
this.webglRenderer.render(scene, camera);
this.webglRenderer.setRenderTarget(null);
this.quiltRenderer.update(this.quiltResolution, this.tileCount);
this.quiltRenderer.render();
}
}
}
exports.Buttons = Buttons;
exports.Calibration = calibration;
exports.Camera = Camera;
exports.FullscreenHelper = FullscreenHelper;
exports.QuiltRenderer = QuiltRenderer;
exports.Renderer = Renderer;
Object.defineProperty(exports, '__esModule', { value: true });
})));