diff --git a/js/config.js b/js/config.js index 4157d15..c69af06 100644 --- a/js/config.js +++ b/js/config.js @@ -75,6 +75,7 @@ const defaults = { slant: 0, // The angle at which rain falls; the orientation of the glyph grid resolution: 1, // An overall scale multiplier useHalfFloat: false, + renderer: "webgpu", // The preferred web graphics API }; const versions = { @@ -205,6 +206,30 @@ const versions = { { hsl: [0.1, 1.0, 0.9], at: 1.0 }, ], }, + + holoplay: { + ...defaults, + ...fonts.resurrections, + numColumns: 40, + fallSpeed: 0.35, + cycleStyle: "cycleRandomly", + cycleSpeed: 0.8, + glyphEdgeCrop: 0.1, + paletteEntries: [ + { hsl: [0.39, 0.9, 0.0], at: 0.0 }, + { hsl: [0.39, 1.0, 0.6], at: 0.5 }, + { hsl: [0.39, 1.0, 1.0], at: 1.0 }, + ], + raindropLength: 1.4, + highPassThreshold: 0.2, + cursorEffectThreshold: 0.8, + + renderer: "regl", + bloomSize: 0, + volumetric: true, + forwardSpeed: 0, + density: 3, + }, }; versions.throwback = versions.operator; versions["1999"] = versions.operator; @@ -247,6 +272,7 @@ const paramMapping = { stripeColors: { key: "stripeColors", parser: (s) => s }, backgroundColor: { key: "backgroundColor", parser: (s) => s.split(",").map(parseFloat) }, volumetric: { key: "volumetric", parser: (s) => s.toLowerCase().includes("true") }, + renderer: { key: "renderer", parser: (s) => s }, }; paramMapping.dropLength = paramMapping.raindropLength; paramMapping.angle = paramMapping.slant; diff --git a/js/main.js b/js/main.js index 3ef18c7..36eaeb6 100644 --- a/js/main.js +++ b/js/main.js @@ -12,8 +12,8 @@ const supportsWebGPU = async () => { document.body.onload = async () => { const urlParams = Object.fromEntries(new URLSearchParams(window.location.search).entries()); - const useREGL = !(await supportsWebGPU()) || ["webgl", "regl"].includes(urlParams.renderer?.toLowerCase()); - const solution = import(`./${useREGL ? "regl" : "webgpu"}/main.js`); const config = makeConfig(urlParams); + const useREGL = !(await supportsWebGPU()) || ["webgl", "regl"].includes(config.renderer?.toLowerCase()); + const solution = import(`./${useREGL ? "regl" : "webgpu"}/main.js`); (await solution).default(canvas, config); }; diff --git a/js/regl/main.js b/js/regl/main.js index b2c907a..21479ca 100644 --- a/js/regl/main.js +++ b/js/regl/main.js @@ -6,6 +6,9 @@ import makePalettePass from "./palettePass.js"; import makeStripePass from "./stripePass.js"; import makeImagePass from "./imagePass.js"; import makeResurrectionPass from "./resurrectionPass.js"; +import makeQuiltPass from "./quiltPass.js"; + +import * as HoloPlayCore from "../../lib/holoplaycore.module.js"; const effects = { none: null, @@ -48,10 +51,100 @@ export default async (canvas, config) => { optionalExtensions: ["EXT_color_buffer_half_float", "WEBGL_color_buffer_float", "OES_standard_derivatives"], }); + const lkg = await new Promise((resolve, reject) => { + const client = new HoloPlayCore.Client((data) => { + /* + data = { + devices: [ + { + buttons: [ 0, 0, 0, 0 ], + calibration: + { + DPI: { value: 324 }, + center: { value: 0.15018756687641144 }, + configVersion: "3.0", + flipImageX: { value: 0 }, + flipImageY: { value: 0 }, + flipSubp: { value: 0 }, + fringe: { value: 0 }, + invView: { value: 1 }, + pitch: { value: 52.58013153076172 }, + screenH: { value: 2048 }, + screenW: { value: 1536 }, + slope: { value: -7.145165920257568 }, + verticalAngle: { value: 0 }, + viewCone: { value: 40 } + }, + defaultQuilt: + { + quiltAspect: 0.75, + quiltX: 3840, + quiltY: 3840, + tileX: 8, + tileY: 6 + }, + hardwareVersion: "portrait", + hwid: "LKG-P11063", + index: 0, + joystickIndex: -1, + state: "ok", + unityIndex: 1, + windowCoords: [ 1440, 900 ] + } + ], + error: 0, + version: "1.2.2" + }; + /**/ + + + if (data.devices.length === 0) { + resolve({ tileX: 1, tileY: 1, fov: 90 }); + return; + } + + const device = data.devices[0]; + const defaultQuilt = device.defaultQuilt; + + const {quiltX, quiltY, tileX, tileY} = defaultQuilt; + + const fov = 15; // But is it? + + const calibration = Object.fromEntries( + Object.entries(device.calibration) + .map(([key, value]) => ([key, value.value])) + .filter(([key, value]) => (value != null)) + ); + + const screenInches = calibration.screenW / calibration.DPI; + const pitch = calibration.pitch * screenInches * Math.cos(Math.atan(1.0 / calibration.slope)); + const tilt = calibration.screenH / (calibration.screenW * calibration.slope) * (calibration.flipImageX * 2 - 1); + const subp = 1 / (calibration.screenW * 3); + + const quiltViewPortion = [ + (Math.floor(quiltX / tileX) * tileX) / quiltX, + (Math.floor(quiltY / tileY) * tileY) / quiltY, + ]; + + const output = { + ...defaultQuilt, + ...calibration, + pitch, + tilt, + subp, + + quiltViewPortion, + fov + }; + + resolve(output); + }, reject); + }); + // All this takes place in a full screen quad. const fullScreenQuad = makeFullScreenQuad(regl); const effectName = config.effect in effects ? config.effect : "plain"; - const pipeline = makePipeline({ regl, config }, [makeRain, makeBloomPass, effects[effectName]]); + const pipeline = makePipeline({ regl, config, lkg }, [makeRain, makeBloomPass, effects[effectName], makeQuiltPass]); const screenUniforms = { tex: pipeline[pipeline.length - 1].outputs.primary }; const drawToScreen = regl({ uniforms: screenUniforms }); await Promise.all(pipeline.map((step) => step.ready)); diff --git a/js/regl/quiltPass.js b/js/regl/quiltPass.js new file mode 100644 index 0000000..fac6a5c --- /dev/null +++ b/js/regl/quiltPass.js @@ -0,0 +1,34 @@ +import { loadImage, loadText, makePassFBO, makePass } from "./utils.js"; + +// Multiplies the rendered rain and bloom by a loaded in image + +export default ({ regl, config, lkg }, inputs) => { + let enabled = lkg.tileX * lkg.tileY > 1; + + // enabled = false; + + if (!enabled) { + return makePass({ + primary: inputs.primary, + }); + } + + const output = makePassFBO(regl, config.useHalfFloat); + const quiltPassFrag = loadText("shaders/glsl/quiltPass.frag.glsl"); + const render = regl({ + frag: regl.prop("frag"), + uniforms: { + quiltTexture: inputs.primary, + ...lkg, + }, + framebuffer: output, + }); + return makePass( + { + primary: output, + }, + Promise.all([quiltPassFrag.loaded]), + (w, h) => output.resize(w, h), + () => render({ frag: quiltPassFrag.text() }) + ); +}; diff --git a/js/regl/rainPass.js b/js/regl/rainPass.js index c5a9a53..bdbe96f 100644 --- a/js/regl/rainPass.js +++ b/js/regl/rainPass.js @@ -19,7 +19,7 @@ const blVert = [1, 0]; const brVert = [1, 1]; const quadVertices = [tlVert, trVert, brVert, tlVert, brVert, blVert]; -export default ({ regl, config }) => { +export default ({ regl, config, lkg }) => { // The volumetric mode multiplies the number of columns // to reach the desired density, and then overlaps them const volumetric = config.volumetric; @@ -143,6 +143,8 @@ export default ({ regl, config }) => { screenSize: regl.prop("screenSize"), }, + viewport: regl.prop("viewport"), + attributes: { aPosition: quadPositions, aCorner: Array(numQuads).fill(quadVertices), @@ -163,9 +165,16 @@ export default ({ regl, config }) => { mat4.scale(transform, transform, vec3.fromValues(1, 1, 2)); } else { mat4.translate(transform, transform, vec3.fromValues(0, 0, -1)); + + // mat4.rotateX(transform, transform, (Math.PI * 1) / 8); + // mat4.rotateY(transform, transform, (Math.PI * 1) / 4); + // mat4.translate(transform, transform, vec3.fromValues(0, 0, -1)); + // mat4.scale(transform, transform, vec3.fromValues(1, 1, 2)); } const camera = mat4.create(); + const vantagePoints = []; + return makePass( { primary: output, @@ -174,14 +183,47 @@ export default ({ regl, config }) => { (w, h) => { output.resize(w, h); const aspectRatio = w / h; - if (config.effect === "none") { - if (aspectRatio > 1) { - mat4.ortho(camera, -1.5 * aspectRatio, 1.5 * aspectRatio, -1.5, 1.5, -1000, 1000); - } else { - mat4.ortho(camera, -1.5, 1.5, -1.5 / aspectRatio, 1.5 / aspectRatio, -1000, 1000); + + const [numTileColumns, numTileRows] = [lkg.tileX, lkg.tileY]; + const numVantagePoints = numTileRows * numTileColumns; + const tileSize = [Math.floor(w /*lkg.quiltX*/ / numTileColumns), Math.floor(h /*lkg.quiltY*/ / numTileRows)]; + vantagePoints.length = 0; + for (let row = 0; row < numTileRows; row++) { + for (let column = 0; column < numTileColumns; column++) { + const index = column + row * numTileColumns; + const camera = mat4.create(); + + if (config.effect === "none") { + if (aspectRatio > 1) { + mat4.ortho(camera, -1.5 * aspectRatio, 1.5 * aspectRatio, -1.5, 1.5, -1000, 1000); + } else { + mat4.ortho(camera, -1.5, 1.5, -1.5 / aspectRatio, 1.5 / aspectRatio, -1000, 1000); + } + } else { + mat4.perspective(camera, (Math.PI / 180) * lkg.fov, aspectRatio, 0.0001, 1000); + + mat4.translate(camera, camera, vec3.fromValues(0, 0, -1)); + + const distanceToTarget = 1; // TODO: Get from somewhere else + let vantagePointAngle = (Math.PI / 180) * lkg.viewCone * (index / (numVantagePoints - 1) - 0.5); + if (isNaN(vantagePointAngle)) { + vantagePointAngle = 0; + } + const xOffset = distanceToTarget * Math.tan(vantagePointAngle); + + mat4.translate(camera, camera, vec3.fromValues(xOffset, 0, 0)); + + camera[8] = -xOffset / (distanceToTarget * Math.tan((Math.PI / 180) * 0.5 * lkg.fov) * aspectRatio); // Is this right?? + } + + const viewport = { + x: column * tileSize[0], + y: row * tileSize[1], + width: tileSize[0], + height: tileSize[1], + }; + vantagePoints.push({ camera, viewport }); } - } else { - mat4.perspective(camera, (Math.PI / 180) * 90, aspectRatio, 0.0001, 1000); } [screenSize[0], screenSize[1]] = aspectRatio > 1 ? [1, aspectRatio] : [1 / aspectRatio, 1]; }, @@ -192,7 +234,18 @@ export default ({ regl, config }) => { color: [0, 0, 0, 1], framebuffer: output, }); - render({ camera, transform, screenSize, vert: rainPassVert.text(), frag: rainPassFrag.text() }); + + // const now = Date.now(); + + // mat4.identity(transform); + // mat4.rotateX(transform, transform, (Math.PI * 1) / 8); + // mat4.rotateY(transform, transform, Math.sin(0.001 * now)); + // mat4.translate(transform, transform, vec3.fromValues(0, 0, -1)); + // mat4.scale(transform, transform, vec3.fromValues(1, 1, 2)); + + for (const vantagePoint of vantagePoints) { + render({ ...vantagePoint, transform, screenSize, vert: rainPassVert.text(), frag: rainPassFrag.text() }); + } } ); }; diff --git a/lib/holoplaycore.module.js b/lib/holoplaycore.module.js new file mode 100644 index 0000000..05adc9e --- /dev/null +++ b/lib/holoplaycore.module.js @@ -0,0 +1,787 @@ +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 + * + * 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); + } +} +/** Delete a quilt from HoloPlayService. Extends the base Message class. */ +class DeleteMessage extends Message { + /** + * @constructor + * @param {string} name - name of the quilt + */ + constructor(name = '') { + let cmd = {'delete': {'name': name}}; + super(cmd, null); + } +} +/** Check if a quilt exist in cache. Extends the base Message class. */ +class CheckMessage extends Message { + /** + * @constructor + * @param {string} name - name of the quilt + */ + constructor(name = '') { + let cmd = {'check': {'name': name}}; + super(cmd, null); + } +} +/** Wipes the image in Looking Glass and displays the background image */ +class WipeMessage extends Message { + /** + * @constructor + * @param {number} targetDisplay - optional, if not provided, default is 0 + */ + constructor(targetDisplay = null) { + let cmd = {'wipe': {}}; + if (targetDisplay != null) cmd['wipe'].targetDisplay = targetDisplay; + super(cmd, null); + } +} +/** Get info from the HoloPlayService */ +class InfoMessage extends Message { + /** + * @constructor + */ + constructor() { + let cmd = {'info': {}}; + super(cmd, null); + } +} +/** Get shader uniforms from HoloPlayService */ +class UniformsMessage extends Message { + /** + * @constructor + * @param {object} + */ + constructor() { + let cmd = {'uniforms': {}}; + super(cmd, bindata); + } +} +/** Get GLSL shader code from HoloPlayService */ +class ShaderMessage extends Message { + /** + * @constructor + * @param {object} + */ + constructor() { + let cmd = {'shader': {}}; + super(cmd, bindata); + } +} +/** Show a quilt in the Looking Glass with the binary data of quilt provided */ +class ShowMessage extends Message { + /** + * @constructor + * @param {object} + */ + constructor( + settings = {vx: 5, vy: 9, aspect: 1.6}, bindata = '', + targetDisplay = null) { + let cmd = { + 'show': { + 'source': 'bindata', + 'quilt': {'type': 'image', 'settings': settings} + } + }; + if (targetDisplay != null) cmd['show']['targetDisplay'] = targetDisplay; + super(cmd, bindata); + } +} +/** extends the base Message class */ +class CacheMessage extends Message { + constructor( + name, settings = {vx: 5, vy: 9, aspect: 1.6}, bindata = '', + show = false) { + let cmd = { + 'cache': { + 'show': show, + 'quilt': { + 'name': name, + 'type': 'image', + 'settings': settings, + } + } + }; + super(cmd, bindata); + } +} + +class ShowCachedMessage extends Message { + constructor(name, targetDisplay = null, settings = null) { + let cmd = {'show': {'source': 'cache', 'quilt': {'name': name}}}; + if (targetDisplay != null) cmd['show']['targetDisplay'] = targetDisplay; + if (settings != null) cmd['show']['quilt'].settings = settings; + 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()); +} + +export { CacheMessage, CheckMessage, Client, DeleteMessage, InfoMessage, InitMessage, Message, ShaderMessage, ShowCachedMessage, ShowMessage, UniformsMessage, WipeMessage }; diff --git a/shaders/glsl/quiltPass.frag.glsl b/shaders/glsl/quiltPass.frag.glsl new file mode 100644 index 0000000..3aeb889 --- /dev/null +++ b/shaders/glsl/quiltPass.frag.glsl @@ -0,0 +1,42 @@ +precision mediump float; +uniform sampler2D quiltTexture; +uniform float pitch; +uniform float tilt; +uniform float center; +uniform float invView; +uniform float flipImageX; +uniform float flipImageY; +uniform float subp; +uniform float tileX; +uniform float tileY; +uniform vec2 quiltViewPortion; +varying vec2 vUV; + +vec2 texArr(vec3 uvz) { + float z = floor(uvz.z * tileX * tileY); + float x = (mod(z, tileX) + uvz.x) / tileX; + float y = (floor(z / tileX) + uvz.y) / tileY; + 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(vUV.xy, 0.0); + + // Flip UVs if necessary + nuv.x = (1.0 - flipImageX) * nuv.x + flipImageX * (1.0 - nuv.x); + nuv.y = (1.0 - flipImageY) * nuv.y + flipImageY * (1.0 - nuv.y); + + for (int i = 0; i < 3; i++) { + nuv.z = (vUV.x + float(i) * subp + vUV.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(vUV.x, vUV.y, nuv.z))); + } + + gl_FragColor = vec4(rgb[0].r, rgb[1].g, rgb[2].b, 1); +}