@photo-sphere-viewer/autorotate-plugin
Version:
Photo Sphere Viewer plugin to add an automatic rotation of the panorama.
530 lines (522 loc) • 17.5 kB
JavaScript
/*!
* Photo Sphere Viewer / Autorotate Plugin 5.13.4
* @copyright 2015-2025 Damien "Mistic" Sorel
* @licence MIT (https://opensource.org/licenses/MIT)
*/
var __defProp = Object.defineProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
// src/index.ts
import { DEFAULTS, registerButton } from "@photo-sphere-viewer/core";
// src/AutorotateButton.ts
import { AbstractButton } from "@photo-sphere-viewer/core";
// src/events.ts
var events_exports = {};
__export(events_exports, {
AutorotateEvent: () => AutorotateEvent
});
import { TypedEvent } from "@photo-sphere-viewer/core";
var _AutorotateEvent = class _AutorotateEvent extends TypedEvent {
/** @internal */
constructor(autorotateEnabled) {
super(_AutorotateEvent.type);
this.autorotateEnabled = autorotateEnabled;
}
};
_AutorotateEvent.type = "autorotate";
var AutorotateEvent = _AutorotateEvent;
// src/icons/play-active.svg
var play_active_default = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 41 41" overflow="visible"><g fill="currentColor" transform-origin="center" transform="scale(1.3)"><path d="M40.5 14.1c-.1-.1-1.2-.5-2.898-1-.102 0-.202-.1-.202-.2C34.5 6.5 28 2 20.5 2S6.6 6.5 3.7 12.9c0 .1-.1.1-.2.2-1.7.6-2.8 1-2.9 1l-.6.3v12.1l.6.2c.1 0 1.1.399 2.7.899.1 0 .2.101.2.199C6.3 34.4 12.9 39 20.5 39c7.602 0 14.102-4.6 16.9-11.1 0-.102.1-.102.199-.2 1.699-.601 2.699-1 2.801-1l.6-.3V14.3l-.5-.2zM6.701 11.5C9.7 7 14.8 4 20.5 4c5.8 0 10.9 3 13.8 7.5.2.3-.1.6-.399.5-3.799-1-8.799-2-13.6-2-4.7 0-9.5 1-13.2 2-.3.1-.5-.2-.4-.5zM25.1 20.3L18.7 24c-.3.2-.7 0-.7-.5v-7.4c0-.4.4-.6.7-.4l6.399 3.8c.301.1.301.6.001.8zm9.4 8.901A16.421 16.421 0 0 1 20.5 37c-5.9 0-11.1-3.1-14-7.898-.2-.302.1-.602.4-.5 3.9 1 8.9 2.1 13.6 2.1 5 0 9.9-1 13.602-2 .298-.1.5.198.398.499z"/></g><!--Created by Nick Bluth from the Noun Project--></svg>\n';
// src/icons/play.svg
var play_default = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 41 41" overflow="visible"><g fill="currentColor" transform-origin="center" transform="scale(1.3)"><path d="M40.5 14.1c-.1-.1-1.2-.5-2.899-1-.101 0-.2-.1-.2-.2C34.5 6.5 28 2 20.5 2S6.6 6.5 3.7 12.9c0 .1-.1.1-.2.2-1.7.6-2.8 1-2.9 1l-.6.3v12.1l.6.2c.1 0 1.1.4 2.7.9.1 0 .2.1.2.199C6.3 34.4 12.9 39 20.5 39c7.601 0 14.101-4.6 16.9-11.1 0-.101.1-.101.2-.2 1.699-.6 2.699-1 2.8-1l.6-.3V14.3l-.5-.2zM20.5 4c5.8 0 10.9 3 13.8 7.5.2.3-.1.6-.399.5-3.8-1-8.8-2-13.6-2-4.7 0-9.5 1-13.2 2-.3.1-.5-.2-.4-.5C9.7 7 14.8 4 20.5 4zm0 33c-5.9 0-11.1-3.1-14-7.899-.2-.301.1-.601.4-.5 3.9 1 8.9 2.1 13.6 2.1 5 0 9.9-1 13.601-2 .3-.1.5.2.399.5A16.422 16.422 0 0 1 20.5 37zm18.601-12.1c0 .1-.101.3-.2.3-2.5.9-10.4 3.6-18.4 3.6-7.1 0-15.6-2.699-18.3-3.6C2.1 25.2 2 25 2 24.9V16c0-.1.1-.3.2-.3 2.6-.9 10.6-3.6 18.2-3.6 7.5 0 15.899 2.7 18.5 3.6.1 0 .2.2.2.3v8.9z"/><path d="M18.7 24l6.4-3.7c.3-.2.3-.7 0-.8l-6.4-3.8c-.3-.2-.7 0-.7.4v7.4c0 .5.4.7.7.5z"/></g><!--Created by Nick Bluth from the Noun Project--></svg>\n';
// src/AutorotateButton.ts
var AutorotateButton = class extends AbstractButton {
constructor(navbar) {
super(navbar, {
className: "psv-autorotate-button",
hoverScale: true,
collapsable: true,
tabbable: true,
icon: play_default,
iconActive: play_active_default
});
this.plugin = this.viewer.getPlugin("autorotate");
this.plugin?.addEventListener(AutorotateEvent.type, this);
}
destroy() {
this.plugin?.removeEventListener(AutorotateEvent.type, this);
super.destroy();
}
isSupported() {
return !!this.plugin;
}
handleEvent(e) {
if (e instanceof AutorotateEvent) {
this.toggleActive(e.autorotateEnabled);
}
}
onClick() {
if (this.plugin.isEnabled()) {
this.plugin.disableOnIdle();
}
this.plugin.toggle();
}
};
AutorotateButton.id = "autorotate";
// src/AutorotatePlugin.ts
import { AbstractConfigurablePlugin, CONSTANTS, events, PSVError, utils } from "@photo-sphere-viewer/core";
import { MathUtils, SplineCurve, Vector2 } from "three";
var getConfig = utils.getConfigParser(
{
autostartDelay: 2e3,
autostartOnIdle: true,
autorotateSpeed: utils.parseSpeed("2rpm"),
autorotatePitch: null,
autorotateZoomLvl: null,
keypoints: null,
startFromClosest: true
},
{
autostartOnIdle: (autostartOnIdle, { rawConfig }) => {
if (autostartOnIdle && utils.isNil(rawConfig.autostartDelay)) {
utils.logWarn("autostartOnIdle requires a non null autostartDelay");
return false;
}
return autostartOnIdle;
},
autorotateSpeed: (autorotateSpeed) => {
return utils.parseSpeed(autorotateSpeed);
},
autorotatePitch: (autorotatePitch) => {
if (!utils.isNil(autorotatePitch)) {
return utils.parseAngle(autorotatePitch, true);
}
return null;
},
autorotateZoomLvl: (autorotateZoomLvl) => {
if (!utils.isNil(autorotateZoomLvl)) {
return MathUtils.clamp(autorotateZoomLvl, 0, 100);
}
return null;
}
}
);
var NUM_STEPS = 16;
function serializePt(position) {
return [position.yaw, position.pitch];
}
var _AutorotatePlugin = class _AutorotatePlugin extends AbstractConfigurablePlugin {
constructor(viewer, config) {
super(viewer, config);
this.state = {
initialStart: true,
disableOnIdle: false,
/** if the automatic rotation is enabled */
enabled: false,
/** current index in keypoints */
idx: -1,
/** curve between idx and idx + 1 */
curve: [],
/** start point of the current step */
startStep: null,
/** end point of the current step */
endStep: null,
/** start time of the current step */
startTime: null,
/** expected duration of the step */
stepDuration: null,
/** time remaining for the pause */
remainingPause: null,
/** previous timestamp in render loop */
lastTime: null,
/** currently displayed tooltip */
tooltip: null
};
this.state.initialStart = !utils.isNil(this.config.autostartDelay);
}
static withConfig(config) {
return [_AutorotatePlugin, config];
}
/**
* @internal
*/
init() {
super.init();
this.video = this.viewer.getPlugin("video");
this.markers = this.viewer.getPlugin("markers");
if (this.config.keypoints) {
this.setKeypoints(this.config.keypoints);
delete this.config.keypoints;
}
this.viewer.addEventListener(events.StopAllEvent.type, this);
this.viewer.addEventListener(events.BeforeRenderEvent.type, this);
if (!this.video) {
this.viewer.addEventListener(events.KeypressEvent.type, this);
}
}
/**
* @internal
*/
destroy() {
this.viewer.removeEventListener(events.StopAllEvent.type, this);
this.viewer.removeEventListener(events.BeforeRenderEvent.type, this);
this.viewer.removeEventListener(events.KeypressEvent.type, this);
delete this.video;
delete this.markers;
delete this.keypoints;
super.destroy();
}
/**
* @internal
*/
handleEvent(e) {
switch (e.type) {
case events.StopAllEvent.type:
this.stop();
break;
case events.BeforeRenderEvent.type: {
this.__beforeRender(e.timestamp);
break;
}
case events.KeypressEvent.type:
this.__onKeyPress(e.originalEvent);
break;
}
}
/**
* Changes the keypoints
* @throws {@link PSVError} if the configuration is invalid
*/
setKeypoints(keypoints) {
if (!keypoints) {
this.keypoints = null;
} else {
if (keypoints.length < 2) {
throw new PSVError("At least two points are required");
}
this.keypoints = keypoints.map((pt, i) => {
const keypoint = {
position: null,
markerId: null,
pause: 0,
tooltip: null
};
let position;
if (typeof pt === "string") {
keypoint.markerId = pt;
} else if (utils.isExtendedPosition(pt)) {
position = pt;
} else {
keypoint.markerId = pt.markerId;
keypoint.pause = pt.pause;
position = pt.position;
if (pt.tooltip && typeof pt.tooltip === "object") {
keypoint.tooltip = pt.tooltip;
} else if (typeof pt.tooltip === "string") {
keypoint.tooltip = { content: pt.tooltip };
}
}
if (keypoint.markerId) {
if (!this.markers) {
throw new PSVError(`Keypoint #${i} references a marker but the markers plugin is not loaded`);
}
const marker = this.markers.getMarker(keypoint.markerId);
keypoint.position = serializePt(marker.state.position);
} else if (position) {
keypoint.position = serializePt(this.viewer.dataHelper.cleanPosition(position));
} else {
throw new PSVError(`Keypoint #${i} is missing marker or position`);
}
return keypoint;
});
}
if (this.isEnabled()) {
this.stop();
this.start();
}
}
/**
* Checks if the automatic rotation is enabled
*/
isEnabled() {
return this.state.enabled;
}
/**
* Starts the automatic rotation
*/
start() {
if (this.isEnabled()) {
return;
}
this.viewer.stopAll();
if (!this.keypoints) {
this.__animate();
} else if (this.config.startFromClosest) {
this.__shiftKeypoints();
}
this.state.initialStart = false;
this.state.disableOnIdle = false;
this.state.enabled = true;
this.dispatchEvent(new AutorotateEvent(true));
}
/**
* Stops the automatic rotation
*/
stop() {
if (!this.isEnabled()) {
return;
}
this.__hideTooltip();
this.__reset();
this.viewer.stopAnimation();
this.viewer.dynamics.position.stop();
this.viewer.dynamics.zoom.stop();
this.state.enabled = false;
this.dispatchEvent(new AutorotateEvent(false));
}
/**
* Starts or stops the automatic rotation
*/
toggle() {
if (this.isEnabled()) {
this.stop();
} else {
this.start();
}
}
/**
* @internal
*/
reverse() {
if (this.isEnabled() && !this.keypoints) {
this.config.autorotateSpeed = -this.config.autorotateSpeed;
this.__animate();
}
}
/**
* @internal
*/
disableOnIdle() {
this.state.disableOnIdle = true;
}
/**
* Launches the standard animation
*/
__animate() {
let p;
if (!utils.isNil(this.config.autorotateZoomLvl)) {
p = this.viewer.animate({
zoom: this.config.autorotateZoomLvl,
// "2" is magic, and kinda related to the "PI/4" in getAnimationProperties()
speed: `${this.viewer.config.zoomSpeed * 2}rpm`
});
} else {
p = Promise.resolve(true);
}
p.then((done) => {
if (done) {
this.viewer.dynamics.position.roll(
{
yaw: this.config.autorotateSpeed < 0
},
Math.abs(this.config.autorotateSpeed / this.viewer.config.moveSpeed)
);
if (!utils.isNil(this.config.autorotatePitch)) {
this.viewer.dynamics.position.goto(
{
pitch: this.config.autorotatePitch
},
Math.abs(this.config.autorotateSpeed / this.viewer.config.moveSpeed)
);
}
}
});
}
/**
* Resets all the curve variables
*/
__reset() {
this.state.idx = -1;
this.state.curve = [];
this.state.startStep = null;
this.state.endStep = null;
this.state.startTime = null;
this.state.stepDuration = null;
this.state.remainingPause = null;
this.state.lastTime = null;
this.state.tooltip = null;
}
/**
* Automatically starts if the delay is reached
* Performs keypoints animation
*/
__beforeRender(timestamp) {
if ((this.state.initialStart || this.config.autostartOnIdle && !this.state.disableOnIdle) && this.viewer.state.idleTime > 0 && timestamp - this.viewer.state.idleTime > this.config.autostartDelay) {
this.start();
}
if (this.isEnabled() && this.keypoints) {
if (!this.state.startTime) {
this.state.endStep = serializePt(this.viewer.getPosition());
this.__nextStep();
this.state.startTime = timestamp;
this.state.lastTime = timestamp;
}
this.__nextFrame(timestamp);
}
}
__shiftKeypoints() {
const currentPosition = serializePt(this.viewer.getPosition());
const index = this.__findMinIndex(this.keypoints, (keypoint) => {
return utils.greatArcDistance(keypoint.position, currentPosition);
});
this.keypoints.push(...this.keypoints.splice(0, index));
}
__incrementIdx() {
this.state.idx++;
if (this.state.idx === this.keypoints.length) {
this.state.idx = 0;
}
}
__showTooltip() {
const keypoint = this.keypoints[this.state.idx];
if (keypoint.tooltip) {
const position = this.viewer.dataHelper.vector3ToViewerCoords(this.viewer.state.direction);
this.state.tooltip = this.viewer.createTooltip({
content: keypoint.tooltip.content,
position: keypoint.tooltip.position,
top: position.y,
left: position.x
});
} else if (keypoint.markerId) {
const marker = this.markers.getMarker(keypoint.markerId);
marker.showTooltip();
this.state.tooltip = marker.tooltip;
}
}
__hideTooltip() {
if (this.state.tooltip) {
const keypoint = this.keypoints[this.state.idx];
if (keypoint.tooltip) {
this.state.tooltip.hide();
} else if (keypoint.markerId) {
const marker = this.markers.getMarker(keypoint.markerId);
marker.hideTooltip();
}
this.state.tooltip = null;
}
}
__nextPoint() {
const workPoints = [];
if (this.state.idx === -1) {
const currentPosition = serializePt(this.viewer.getPosition());
workPoints.push(
currentPosition,
currentPosition,
this.keypoints[0].position,
this.keypoints[1].position
);
} else {
for (let i = -1; i < 3; i++) {
const keypoint = this.state.idx + i < 0 ? this.keypoints[this.keypoints.length - 1] : this.keypoints[(this.state.idx + i) % this.keypoints.length];
workPoints.push(keypoint.position);
}
}
const workVectors = [new Vector2(workPoints[0][0], workPoints[0][1])];
let k = 0;
for (let i = 1; i <= 3; i++) {
const d = workPoints[i - 1][0] - workPoints[i][0];
if (d > Math.PI) {
k += 1;
} else if (d < -Math.PI) {
k -= 1;
}
if (k !== 0 && i === 1) {
workVectors[0].x -= k * 2 * Math.PI;
k = 0;
}
workVectors.push(new Vector2(workPoints[i][0] + k * 2 * Math.PI, workPoints[i][1]));
}
const curve = new SplineCurve(workVectors).getPoints(NUM_STEPS * 3).map((p) => [p.x, p.y]);
this.state.curve = curve.slice(NUM_STEPS + 1, NUM_STEPS * 2 + 1);
if (this.state.idx !== -1) {
this.state.remainingPause = this.keypoints[this.state.idx].pause;
if (this.state.remainingPause) {
this.__showTooltip();
} else {
this.__incrementIdx();
}
} else {
this.__incrementIdx();
}
}
__nextStep() {
if (this.state.curve.length === 0) {
this.__nextPoint();
this.state.endStep[0] = utils.parseAngle(this.state.endStep[0]);
}
this.state.startStep = this.state.endStep;
this.state.endStep = this.state.curve.shift();
const distance = utils.greatArcDistance(this.state.startStep, this.state.endStep);
this.state.stepDuration = distance * 1e3 / Math.abs(this.config.autorotateSpeed);
if (distance === 0) {
this.__nextStep();
}
}
__nextFrame(timestamp) {
const ellapsed = timestamp - this.state.lastTime;
this.state.lastTime = timestamp;
if (this.state.remainingPause) {
this.state.remainingPause = Math.max(0, this.state.remainingPause - ellapsed);
if (this.state.remainingPause > 0) {
return;
} else {
this.__hideTooltip();
this.__incrementIdx();
this.state.startTime = timestamp;
}
}
let progress = (timestamp - this.state.startTime) / this.state.stepDuration;
if (progress >= 1) {
this.__nextStep();
progress = 0;
this.state.startTime = timestamp;
}
this.viewer.rotate({
yaw: this.state.startStep[0] + (this.state.endStep[0] - this.state.startStep[0]) * progress,
pitch: this.state.startStep[1] + (this.state.endStep[1] - this.state.startStep[1]) * progress
});
}
__findMinIndex(array, mapper) {
let idx = 0;
let current = Number.MAX_VALUE;
array.forEach((item, i) => {
const value = mapper(item);
if (value < current) {
current = value;
idx = i;
}
});
return idx;
}
__onKeyPress(e) {
if (this.viewer.state.keyboardEnabled && e.key === CONSTANTS.KEY_CODES.Space && !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey) {
this.toggle();
e.preventDefault();
}
}
};
_AutorotatePlugin.id = "autorotate";
_AutorotatePlugin.VERSION = "5.13.4";
_AutorotatePlugin.configParser = getConfig;
_AutorotatePlugin.readonlyOptions = ["keypoints"];
var AutorotatePlugin = _AutorotatePlugin;
// src/index.ts
registerButton(AutorotateButton, "start");
DEFAULTS.lang[AutorotateButton.id] = "Automatic rotation";
export {
AutorotatePlugin,
events_exports as events
};
//# sourceMappingURL=index.module.js.map