Procházet zdrojové kódy

Hotspot dragging, tile loader, and other features.

* Hotspot dragging (hotspot attribute draggable + dragHandler function)
* External tile loading (allows to pass a function that loads tiles)
* Accept images as parameter
* Option orientationAlignNorth - use absolute orientation values if enabled
* Add getters and setters for orientationOnByDefault
* Enable orientationSupport for android regardless of the protocol
pull/881/head
Nico Höllerich před 4 roky
rodič
revize
31a43dae1b
3 změnil soubory, kde provedl 149 přidání a 18 odebrání
  1. +30
    -0
      doc/json-config-parameters.md
  2. +32
    -8
      src/js/libpannellum.js
  3. +87
    -10
      src/js/pannellum.js

+ 30
- 0
doc/json-config-parameters.md Zobrazit soubor

@@ -85,6 +85,12 @@ to be activated by pressing a button. Defaults to `false`. Note that a secure
HTTPS connection is required for device orientation access in most browsers.


### `orientationAlignNorth` (boolean)

If set to `true` and device orientation control is used, the yaw of the panorama
is set to the azimuth of the device. Defaults to `false`.


### `showZoomCtrl` (boolean)

If set to `false`, the zoom controls will not be displayed. Defaults to `true`.
@@ -323,6 +329,15 @@ If `clickHandlerFunc` is specified, this function is added as an event handler
for the hot spot's `click` event. The event object and the contents of
`clickHandlerArgs` are passed to the function as arguments.

#### `draggable`

If specified, the hotspot can moved using the mouse or by touch.

#### `dragHandlerFunc` (function)

If `dragHandlerFunc` is specified, this function is added as an event handler
when dragging of the hotspot starts and ends. The event object are passed to the function as arguments.

#### `scale` (boolean)

When `true`, the hot spot is scaled to match changes in the field of view,
@@ -430,6 +445,21 @@ to `multiRes.basePath`, which is relative to `basePath`. Format parameters are
`%y` for the y index. For each tile, `.extension` is appended.



#### `loader` (function)

Supply a loader function instead of an URL to load tiles.

Input: node, HTMLImageElement. Output: Promise

Node has the following properties: `x` (number), `y` (number),
`level` (number), `vertices` ([[number]]) part of the full image
where the full image corresponds to [-1,1]², `path` (string) `path` parameter
from config with %-parameters resolved, `uri` (string) `basePath` + `path` + `extension`.

The Promise can be resolved with HTMLCanvasElement, ImageBitmap, HTMLImageElement. Alternatively,
one can save ressources and load the content into the provided HTMLImageElement.

#### `fallbackPath` (string)

This is a format string for the location of the fallback tiles for the CSS 3D


+ 32
- 8
src/js/libpannellum.js Zobrazit soubor

@@ -865,7 +865,12 @@ function Renderer(container) {
this.level = level;
this.x = x;
this.y = y;
this.path = path.replace('%s',side).replace('%l',level).replace('%x',x).replace('%y',y);
if (!path) {
this.path = side + '_' + level + '_' + x + '_' + y;
} else {
this.path = path.replace('%s', side).replace('%l', level).replace('%x', x).replace('%y', y);
}
this.uri = encodeURI(this.path + '.' + image.extension);
}

/**
@@ -1127,12 +1132,16 @@ function Renderer(container) {
/**
* Processes a loaded texture image into a WebGL texture.
* @private
* @param {Image} img - Input image.
* @param {Image | ImageBitmap | ImageData | HTMLCanvasElement} img - Input image.
* @param {WebGLTexture} tex - Texture to bind image to.
*/
function processLoadedTexture(img, tex) {
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, img);
if (img instanceof HTMLCanvasElement) {
var data = img.getContext('2d').getImageData(0, 0, img.width, img.height);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, data)
}else
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, img);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
@@ -1166,10 +1175,25 @@ function Renderer(container) {
this.image.addEventListener('error', loadFn); // ignore missing tile file to support partial image, otherwise retry loop causes high CPU load
}

TextureImageLoader.prototype.loadTexture = function(src, texture, callback) {
TextureImageLoader.prototype.loadTexture = function(node, src, texture, callback) {
this.texture = texture;
this.callback = callback;
this.image.src = src;
if (src instanceof Function) {
src(JSON.parse(JSON.stringify(node)), this.image, this.texture).then(img => {
if (!img)
this.callback(this.texture, false);
else if (img != this.image) {
processLoadedTexture(img, this.texture);
this.callback(this.texture, true);
}
releaseTextureImageLoader(this);
}).catch(() => {
this.callback(this.texture, false);
releaseTextureImageLoader(this);
});
} else {
this.image.src = src;
}
};

function PendingTextureRequest(node, src, texture, callback) {
@@ -1182,7 +1206,7 @@ function Renderer(container) {
function releaseTextureImageLoader(til) {
if (pendingTextureRequests.length) {
var req = pendingTextureRequests.shift();
til.loadTexture(req.src, req.texture, req.callback);
til.loadTexture(req.node, req.src, req.texture, req.callback);
} else
textureImageCache[cacheTop++] = til;
}
@@ -1194,7 +1218,7 @@ function Renderer(container) {
crossOrigin = _crossOrigin;
var texture = gl.createTexture();
if (cacheTop)
textureImageCache[--cacheTop].loadTexture(src, texture, callback);
textureImageCache[--cacheTop].loadTexture(node, src, texture, callback);
else
pendingTextureRequests.push(new PendingTextureRequest(node, src, texture, callback));
return texture;
@@ -1207,7 +1231,7 @@ function Renderer(container) {
* @param {MultiresNode} node - Input node.
*/
function processNextTile(node) {
loadTexture(node, node.path + '.' + image.extension, function(texture, loaded) {
loadTexture(node, image.loader || node.path + '.' + image.extension, function (texture, loaded) {
node.texture = texture;
node.textureLoaded = loaded ? 2 : 1;
}, globalParams.crossOrigin);


+ 87
- 10
src/js/pannellum.js Zobrazit soubor

@@ -40,6 +40,7 @@ var _this = this;
var config,
renderer,
preview,
draggingHotSpot,
isUserInteracting = false,
latestInteraction = Date.now(),
onPointerDownPointerX = 0,
@@ -101,6 +102,7 @@ var defaultConfig = {
autoLoad: false,
showControls: true,
orientationOnByDefault: false,
orientationAlignNorth: false,
hotSpotDebug: false,
backgroundColor: [0, 0, 0],
avoidShowingBackground: false,
@@ -255,7 +257,7 @@ controls.orientation.addEventListener('touchstart', function(e) {e.stopPropagati
controls.orientation.addEventListener('pointerdown', function(e) {e.stopPropagation();});
controls.orientation.className = 'pnlm-orientation-button pnlm-orientation-button-inactive pnlm-sprite pnlm-controls pnlm-control';
var orientationSupport = false;
if (window.DeviceOrientationEvent && location.protocol == 'https:' &&
if (window.DeviceOrientationEvent && (location.protocol == 'https:' || navigator.userAgent.toLowerCase().indexOf('android')) &&
navigator.userAgent.toLowerCase().indexOf('mobi') >= 0) {
// This user agent check is here because there's no way to check if a
// device has an inertia measurement unit. We used to be able to check if a
@@ -312,6 +314,7 @@ function init() {
infoDisplay.load.lbar.style.display = 'none';
} else if (config.type == 'multires') {
var c = JSON.parse(JSON.stringify(config.multiRes)); // Deep copy
c.loader = config.multiRes.loader;
// Avoid "undefined" in path, check (optional) multiRes.basePath, too
// Use only multiRes.basePath if it's an absolute URL
if (config.basePath && config.multiRes.basePath &&
@@ -378,8 +381,14 @@ function init() {
if (config.dynamic !== true) {
// Still image
if (config.panorama instanceof Image) {
panoImage = config.panorama;
onImageLoad();
return;
}

p = absoluteURL(config.panorama) ? config.panorama : p + config.panorama;
panoImage.onload = function() {
window.URL.revokeObjectURL(this.src); // Clean up
onImageLoad();
@@ -712,7 +721,7 @@ function onDocumentMouseDown(event) {
container.focus();
// Only do something if the panorama is loaded
if (!loaded || !config.draggable) {
if (!loaded || !config.draggable || config.draggingHotSpot) {
return;
}
@@ -797,7 +806,10 @@ function mouseEventToCoords(event) {
* @param {MouseEvent} event - Document mouse move event.
*/
function onDocumentMouseMove(event) {
if (isUserInteracting && loaded) {
if (draggingHotSpot) {
moveHotSpot(draggingHotSpot, event);
}
else if (isUserInteracting && loaded) {
latestInteraction = Date.now();
var canvas = renderer.getCanvas();
var canvasWidth = canvas.clientWidth,
@@ -821,6 +833,10 @@ function onDocumentMouseMove(event) {
* @private
*/
function onDocumentMouseUp(event) {
if (draggingHotSpot && draggingHotSpot.dragHandlerFunc)
draggingHotSpot.dragHandlerFunc(event);
draggingHotSpot = null;

if (!isUserInteracting) {
return;
}
@@ -845,7 +861,7 @@ function onDocumentMouseUp(event) {
*/
function onDocumentTouchStart(event) {
// Only do something if the panorama is loaded
if (!loaded || !config.draggable) {
if (!loaded || !config.draggable || draggingHotSpot) {
return;
}

@@ -936,6 +952,8 @@ function onDocumentTouchMove(event) {
* @private
*/
function onDocumentTouchEnd() {
draggingHotSpot = null;

isUserInteracting = false;
if (Date.now() - latestInteraction > 150) {
speed.pitch = speed.yaw = 0;
@@ -973,6 +991,11 @@ function onDocumentPointerDown(event) {
*/
function onDocumentPointerMove(event) {
if (event.pointerType == 'touch') {
if (draggingHotSpot) {
moveHotSpot(draggingHotSpot, event);
return;
}

if (!config.draggable)
return;
for (var i = 0; i < pointerIDs.length; i++) {
@@ -994,6 +1017,10 @@ function onDocumentPointerMove(event) {
* @param {PointerEvent} event - Document pointer up event.
*/
function onDocumentPointerUp(event) {
if (draggingHotSpot && draggingHotSpot.dragHandlerFunc)
draggingHotSpot.dragHandlerFunc(event);
draggingHotSpot = null;

if (event.pointerType == 'touch') {
var defined = false;
for (var i = 0; i < pointerIDs.length; i++) {
@@ -1412,7 +1439,8 @@ function animate() {
} else if (renderer && (renderer.isLoading() || (config.dynamic === true && update))) {
requestAnimationFrame(animate);
} else {
fireEvent('animatefinished', {pitch: _this.getPitch(), yaw: _this.getYaw(), hfov: _this.getHfov()});
if (_this.getPitch && _this.getYaw && _this.getHfov)
fireEvent('animatefinished', {pitch: _this.getPitch(), yaw: _this.getYaw(), hfov: _this.getHfov()});
animating = false;
prevTime = undefined;
var autoRotateStartTime = config.autoRotateInactivityDelay -
@@ -1626,7 +1654,7 @@ function orientationListener(e) {
orientation += 1;
} else if (orientation === 10) {
// Record starting yaw to prevent jumping
orientationYawOffset = q[2] / Math.PI * 180 + config.yaw;
orientationYawOffset = q[2] / Math.PI * 180 + (config.orientationAlignNorth ? (config.northOffset || 0) : config.yaw);
orientation = true;
requestAnimationFrame(animate);
} else {
@@ -1813,10 +1841,47 @@ function createHotSpot(hs) {
div.className += ' pnlm-pointer';
span.className += ' pnlm-pointer';
}
if (hs.draggable) {
// handle mouse by container event listeners
div.addEventListener('mousedown', (e) => {
draggingHotSpot = hs;
});

if (document.documentElement.style.pointerAction === '' &&
document.documentElement.style.touchAction === '') {
div.addEventListener('pointerdown', (e) => {
draggingHotSpot = hs;
});
}

// handle touch events by hotspot event listener
div.addEventListener('touchmove', (e) => {
moveHotSpot(hs, e.targetTouches[0]);
});
div.addEventListener('touchend', (e) => {
hs.dragHandlerFunc(event);
draggingHotSpot = null;
})
}
hs.div = div;
}

/**
*
* @param {hotspot} hs
* @param {MouseEvent} event
* @private
*
*/
function moveHotSpot(hs, event){
let coords = mouseEventToCoords(event);
hs.pitch = coords[0];
hs.yaw = coords[1];
renderHotSpot(hs);
};

/**
* Creates hot spot elements for the current scene.
* @private
*/
@@ -2216,7 +2281,7 @@ function zoomOut() {
}

/**
* Clamps horzontal field of view to viewer's limits.
* Clamps horizontal field of view to viewer's limits.
* @private
* @param {number} hfov - Input horizontal field of view (in degrees)
* @return {number} - Clamped horizontal field of view (in degrees)
@@ -2314,8 +2379,8 @@ function loadScene(sceneId, targetPitch, targetYaw, targetHfov, fadeDone) {
fadeImg.onload = function() {
loadScene(sceneId, targetPitch, targetYaw, targetHfov, true);
};
fadeImg.src = data;
renderContainer.appendChild(fadeImg);
fadeImg.src = data;
renderer.fadeImg = fadeImg;
return;
}
@@ -2381,7 +2446,7 @@ function stopOrientation() {
function startOrientation() {
if (!orientationSupport)
return;
if (typeof DeviceMotionEvent !== 'undefined' &&
if (typeof DeviceMotionEvent !== undefined &&
typeof DeviceMotionEvent.requestPermission === 'function') {
DeviceOrientationEvent.requestPermission().then(function(response) {
if (response == 'granted') {
@@ -2867,6 +2932,18 @@ this.setUpdate = function(bool) {
};

/**
* Sets update flag for dynamic content.
* @memberof Viewer
* @instance
* @param {boolean} bool - Whether or not orientation is enabled by default
* @returns {Viewer} `this`
*/
this.setOrientationOnByDefault = function (bool) {
config.orientationOnByDefault = bool === true;
return this;
}

/**
* Calculate panorama pitch and yaw from location of mouse event.
* @memberof Viewer
* @instance


Načítá se…
Zrušit
Uložit