@photo-sphere-viewer/markers-plugin
Version: 
Photo Sphere Viewer plugin to display various markers/hotspots on the viewer.
1 lines • 165 kB
Source Map (JSON)
{"version":3,"sources":["src/index.ts","src/events.ts","src/MarkersButton.ts","src/icons/pin.svg","src/MarkersListButton.ts","src/constants.ts","src/icons/pin-list.svg","src/MarkersPlugin.ts","src/CSS3DContainer.ts","../node_modules/three/examples/jsm/renderers/CSS3DRenderer.js","src/MarkerType.ts","src/markers/AbstractStandardMarker.ts","src/markers/AbstractDomMarker.ts","src/markers/Marker.ts","src/markers/Marker3D.ts","../shared/ChromaKeyMaterial.ts","../shared/shaders/chromaKey.fragment.glsl","../shared/shaders/chromaKey.vertex.glsl","../shared/video-utils.ts","src/utils.ts","src/markers/MarkerCSS3D.ts","src/markers/MarkerNormal.ts","src/markers/MarkerPolygon.ts","src/markers/MarkerSvg.ts"],"sourcesContent":["import { DEFAULTS, registerButton } from '@photo-sphere-viewer/core';\nimport * as events from './events';\nimport { MarkersButton } from './MarkersButton';\nimport { MarkersListButton } from './MarkersListButton';\n\nDEFAULTS.lang[MarkersButton.id] = 'Markers';\nDEFAULTS.lang[MarkersListButton.id] = 'Markers list';\nregisterButton(MarkersButton, 'caption:left');\nregisterButton(MarkersListButton, 'caption:left');\n\nexport type { Marker } from './markers/Marker';\nexport type { MarkerType } from './MarkerType';\nexport { MarkersPlugin } from './MarkersPlugin';\nexport * from './model';\nexport { events };\n\n/** @internal  */\nimport './styles/index.scss';\n","import { TypedEvent } from '@photo-sphere-viewer/core';\nimport type { Marker } from './markers/Marker';\nimport type { MarkersPlugin } from './MarkersPlugin';\n\n/**\n * Base class for events dispatched by {@link MarkersPlugin}\n */\nexport abstract class MarkersPluginEvent extends TypedEvent<MarkersPlugin> {}\n\n/**\n * @event Triggered when the visibility of a marker changes\n */\nexport class MarkerVisibilityEvent extends MarkersPluginEvent {\n    static override readonly type = 'marker-visibility';\n    override type: 'marker-visibility';\n\n    /** @internal */\n    constructor(\n        public readonly marker: Marker,\n        public readonly visible: boolean,\n    ) {\n        super(MarkerVisibilityEvent.type);\n    }\n}\n\n/**\n * @event Triggered when the animation to a marker is done\n */\nexport class GotoMarkerDoneEvent extends MarkersPluginEvent {\n    static override readonly type = 'goto-marker-done';\n    override type: 'goto-marker-done';\n\n    /** @internal */\n    constructor(public readonly marker: Marker) {\n        super(GotoMarkerDoneEvent.type);\n    }\n}\n\n/**\n * @event Triggered when the user puts the cursor away from a marker\n */\nexport class LeaveMarkerEvent extends MarkersPluginEvent {\n    static override readonly type = 'leave-marker';\n    override type: 'leave-marker';\n\n    /** @internal */\n    constructor(public readonly marker: Marker) {\n        super(LeaveMarkerEvent.type);\n    }\n}\n\n/**\n * @event Triggered when the user puts the cursor hover a marker\n */\nexport class EnterMarkerEvent extends MarkersPluginEvent {\n    static override readonly type = 'enter-marker';\n    override type: 'enter-marker';\n\n    /** @internal */\n    constructor(public readonly marker: Marker) {\n        super(EnterMarkerEvent.type);\n    }\n}\n\n/**\n * @event Triggered when the user clicks on a marker\n */\nexport class SelectMarkerEvent extends MarkersPluginEvent {\n    static override readonly type = 'select-marker';\n    override type: 'select-marker';\n\n    /** @internal */\n    constructor(\n        public readonly marker: Marker,\n        public readonly doubleClick: boolean,\n        public readonly rightClick: boolean,\n    ) {\n        super(SelectMarkerEvent.type);\n    }\n}\n\n/**\n * @event Triggered when a marker is selected from the side panel\n */\nexport class SelectMarkerListEvent extends MarkersPluginEvent {\n    static override readonly type = 'select-marker-list';\n    override type: 'select-marker-list';\n\n    /** @internal */\n    constructor(public readonly marker: Marker) {\n        super(SelectMarkerListEvent.type);\n    }\n}\n\n/**\n * @event Triggered when a marker was selected and the user clicks elsewhere\n */\nexport class UnselectMarkerEvent extends MarkersPluginEvent {\n    static override readonly type = 'unselect-marker';\n    override type: 'unselect-marker';\n\n    /** @internal */\n    constructor(public readonly marker: Marker) {\n        super(UnselectMarkerEvent.type);\n    }\n}\n\n/**\n * @event Triggered when the markers are hidden\n */\nexport class HideMarkersEvent extends MarkersPluginEvent {\n    static override readonly type = 'hide-markers';\n    override type: 'hide-markers';\n\n    /** @internal */\n    constructor() {\n        super(HideMarkersEvent.type);\n    }\n}\n\n/**\n * @event Triggered when the markers change\n */\nexport class SetMarkersEvent extends MarkersPluginEvent {\n    static override readonly type = 'set-markers';\n    override type: 'set-markers';\n\n    /** @internal */\n    constructor(public readonly markers: Marker[]) {\n        super(SetMarkersEvent.type);\n    }\n}\n\n/**\n * @event Triggered when the markers are shown\n */\nexport class ShowMarkersEvent extends MarkersPluginEvent {\n    static override readonly type = 'show-markers';\n    override type: 'show-markers';\n\n    /** @internal */\n    constructor() {\n        super(ShowMarkersEvent.type);\n    }\n}\n\n/**\n * @event Used to alter the list of markers displayed in the side-panel\n */\nexport class RenderMarkersListEvent extends MarkersPluginEvent {\n    static override readonly type = 'render-markers-list';\n    override type: 'render-markers-list';\n\n    /** @internal */\n    constructor(\n        /** the list of markers to display, can be modified */\n        public markers: Marker[],\n    ) {\n        super(RenderMarkersListEvent.type);\n    }\n}\n\nexport type MarkersPluginEvents =\n    | MarkerVisibilityEvent\n    | GotoMarkerDoneEvent\n    | LeaveMarkerEvent\n    | EnterMarkerEvent\n    | SelectMarkerEvent\n    | SelectMarkerListEvent\n    | UnselectMarkerEvent\n    | HideMarkersEvent\n    | SetMarkersEvent\n    | ShowMarkersEvent\n    | RenderMarkersListEvent;\n","import type { Navbar } from '@photo-sphere-viewer/core';\nimport { AbstractButton } from '@photo-sphere-viewer/core';\nimport { HideMarkersEvent, ShowMarkersEvent } from './events';\nimport type { MarkersPlugin } from './MarkersPlugin';\nimport pin from './icons/pin.svg';\n\nexport class MarkersButton extends AbstractButton {\n    static override readonly id = 'markers';\n\n    private readonly plugin: MarkersPlugin;\n\n    constructor(navbar: Navbar) {\n        super(navbar, {\n            className: 'psv-markers-button',\n            icon: pin,\n            hoverScale: true,\n            collapsable: true,\n            tabbable: true,\n        });\n\n        this.plugin = this.viewer.getPlugin('markers');\n\n        if (this.plugin) {\n            this.plugin.addEventListener(ShowMarkersEvent.type, this);\n            this.plugin.addEventListener(HideMarkersEvent.type, this);\n\n            this.toggleActive(true);\n        }\n    }\n\n    override destroy() {\n        if (this.plugin) {\n            this.plugin.removeEventListener(ShowMarkersEvent.type, this);\n            this.plugin.removeEventListener(HideMarkersEvent.type, this);\n        }\n\n        super.destroy();\n    }\n\n    override isSupported() {\n        return !!this.plugin;\n    }\n\n    handleEvent(e: Event) {\n        if (e instanceof ShowMarkersEvent) {\n            this.toggleActive(true);\n        } else if (e instanceof HideMarkersEvent) {\n            this.toggleActive(false);\n        }\n    }\n\n    onClick() {\n        this.plugin.toggleAllMarkers();\n    }\n}\n","<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"10 9 81 81\"><path fill=\"currentColor\" d=\"M50.5 90S22.9 51.9 22.9 36.6 35.2 9 50.5 9s27.6 12.4 27.6 27.6S50.5 90 50.5 90zm0-66.3c-6.1 0-11 4.9-11 11s4.9 11 11 11 11-4.9 11-11-4.9-11-11-11z\"/><!--Created by Rohith M S from the Noun Project--></svg>\n","import type { Navbar } from '@photo-sphere-viewer/core';\nimport { AbstractButton, events } from '@photo-sphere-viewer/core';\nimport { ID_PANEL_MARKERS_LIST } from './constants';\nimport type { MarkersPlugin } from './MarkersPlugin';\nimport pinList from './icons/pin-list.svg';\n\nexport class MarkersListButton extends AbstractButton {\n    static override readonly id = 'markersList';\n\n    private readonly plugin: MarkersPlugin;\n\n    constructor(navbar: Navbar) {\n        super(navbar, {\n            className: ' psv-markers-list-button',\n            icon: pinList,\n            hoverScale: true,\n            collapsable: true,\n            tabbable: true,\n        });\n\n        this.plugin = this.viewer.getPlugin('markers');\n\n        if (this.plugin) {\n            this.viewer.addEventListener(events.ShowPanelEvent.type, this);\n            this.viewer.addEventListener(events.HidePanelEvent.type, this);\n        }\n    }\n\n    override destroy() {\n        this.viewer.removeEventListener(events.ShowPanelEvent.type, this);\n        this.viewer.removeEventListener(events.HidePanelEvent.type, this);\n\n        super.destroy();\n    }\n\n    override isSupported() {\n        return !!this.plugin;\n    }\n\n    handleEvent(e: Event) {\n        if (e instanceof events.ShowPanelEvent) {\n            this.toggleActive(e.panelId === ID_PANEL_MARKERS_LIST);\n        } else if (e instanceof events.HidePanelEvent) {\n            this.toggleActive(false);\n        }\n    }\n\n    onClick() {\n        this.plugin.toggleMarkersList();\n    }\n}\n","import { utils } from '@photo-sphere-viewer/core';\nimport type { Marker } from './markers/Marker';\nimport icon from './icons/pin-list.svg';\n\n/**\n * Namespace for SVG creation\n * @internal\n */\nexport const SVG_NS = 'http://www.w3.org/2000/svg';\n\n/**\n * Property name added to marker elements\n * @internal\n */\nexport const MARKER_DATA = 'psvMarker';\n\n/**\n * Property name added to marker elements (dash-case)\n * @internal\n */\nexport const MARKER_DATA_KEY = utils.dasherize(MARKER_DATA);\n\n/**\n * Panel identifier for marker content\n * @internal\n */\nexport const ID_PANEL_MARKER = 'marker';\n\n/**\n * Panel identifier for markers list\n * @internal\n */\nexport const ID_PANEL_MARKERS_LIST = 'markersList';\n\n/**\n * Default configuration for the \"hoverScale\" parameters\n * @internal\n */\nexport const DEFAULT_HOVER_SCALE = {\n    amount: 2,\n    duration: 100,\n    easing: 'linear',\n};\n\n/**\n * Markers list template\n * @internal\n */\nexport const MARKERS_LIST_TEMPLATE = (markers: Marker[], title: string) => `\n<div class=\"psv-panel-menu psv-panel-menu--stripped\">\n    <h1 class=\"psv-panel-menu-title\">${icon} ${title}</h1>\n    <ul class=\"psv-panel-menu-list\">\n    ${markers.map(marker => `\n        <li data-${MARKER_DATA_KEY}=\"${marker.id}\" class=\"psv-panel-menu-item\" tabindex=\"0\">\n          ${marker.type === 'image' ? `<span class=\"psv-panel-menu-item-icon\"><img src=\"${marker.definition}\"/></span>` : ''}\n          <span class=\"psv-panel-menu-item-label\">${marker.getListContent()}</span>\n        </li>\n    `).join('')}\n    </ul>\n</div>\n`;\n","<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"9 9 81 81\"><path fill=\"currentColor\" d=\"M37.5 90S9.9 51.9 9.9 36.6 22.2 9 37.5 9s27.6 12.4 27.6 27.6S37.5 90 37.5 90zm0-66.3c-6.1 0-11 4.9-11 11s4.9 11 11 11 11-4.9 11-11-4.9-11-11-11zM86.7 55H70c-1.8 0-3.3-1.5-3.3-3.3s1.5-3.3 3.3-3.3h16.7c1.8 0 3.3 1.5 3.3 3.3S88.5 55 86.7 55zm0-25h-15a3.3 3.3 0 0 1-3.3-3.3c0-1.8 1.5-3.3 3.3-3.3h15c1.8 0 3.3 1.5 3.3 3.3 0 1.8-1.5 3.3-3.3 3.3zM56.5 73h30c1.8 0 3.3 1.5 3.3 3.3 0 1.8-1.5 3.3-3.3 3.3h-30a3.3 3.3 0 0 1-3.3-3.3 3.2 3.2 0 0 1 3.3-3.3z\"/><!--Created by Rohith M S from the Noun Project--></svg>\n","import type { PluginConstructor, Point, Viewer } from '@photo-sphere-viewer/core';\nimport { AbstractConfigurablePlugin, PSVError, events, utils } from '@photo-sphere-viewer/core';\nimport { Object3D } from 'three';\nimport { CSS3DContainer } from './CSS3DContainer';\nimport { getMarkerType } from './MarkerType';\nimport { MarkersButton } from './MarkersButton';\nimport { MarkersListButton } from './MarkersListButton';\nimport {\n    DEFAULT_HOVER_SCALE,\n    ID_PANEL_MARKER,\n    ID_PANEL_MARKERS_LIST,\n    MARKERS_LIST_TEMPLATE,\n    MARKER_DATA,\n    SVG_NS,\n} from './constants';\nimport {\n    EnterMarkerEvent,\n    GotoMarkerDoneEvent,\n    HideMarkersEvent,\n    LeaveMarkerEvent,\n    MarkerVisibilityEvent,\n    MarkersPluginEvents,\n    RenderMarkersListEvent,\n    SelectMarkerEvent,\n    SelectMarkerListEvent,\n    SetMarkersEvent,\n    ShowMarkersEvent,\n    UnselectMarkerEvent,\n} from './events';\nimport { AbstractStandardMarker } from './markers/AbstractStandardMarker';\nimport { Marker } from './markers/Marker';\nimport { Marker3D } from './markers/Marker3D';\nimport { MarkerCSS3D } from './markers/MarkerCSS3D';\nimport { MarkerNormal } from './markers/MarkerNormal';\nimport { MarkerPolygon } from './markers/MarkerPolygon';\nimport { MarkerSvg } from './markers/MarkerSvg';\nimport { MarkerConfig, MarkersPluginConfig, ParsedMarkersPluginConfig, UpdatableMarkersPluginConfig } from './model';\n\nconst getConfig = utils.getConfigParser<MarkersPluginConfig, ParsedMarkersPluginConfig>(\n    {\n        clickEventOnMarker: false,\n        gotoMarkerSpeed: '8rpm',\n        markers: null,\n        defaultHoverScale: null,\n    },\n    {\n        defaultHoverScale(defaultHoverScale) {\n            if (!defaultHoverScale) {\n                return null;\n            }\n            if (defaultHoverScale === true) {\n                defaultHoverScale = DEFAULT_HOVER_SCALE;\n            }\n            if (typeof defaultHoverScale === 'number') {\n                defaultHoverScale = { amount: defaultHoverScale };\n            }\n            return {\n                ...DEFAULT_HOVER_SCALE,\n                ...defaultHoverScale,\n            };\n        },\n    },\n);\n\nfunction getMarkerCtor(config: MarkerConfig): typeof Marker {\n    const type = getMarkerType(config, false);\n\n    switch (type) {\n        case 'image':\n        case 'html':\n        case 'element':\n            return MarkerNormal;\n        case 'imageLayer':\n        case 'videoLayer':\n            return Marker3D;\n        case 'elementLayer':\n            return MarkerCSS3D;\n        case 'polygon':\n        case 'polyline':\n        case 'polygonPixels':\n        case 'polylinePixels':\n            return MarkerPolygon;\n        case 'square':\n        case 'rect':\n        case 'circle':\n        case 'ellipse':\n        case 'path':\n            return MarkerSvg;\n        default:\n            throw new PSVError('invalid marker type');\n    }\n}\n\n/**\n * Displays various markers on the viewer\n */\nexport class MarkersPlugin extends AbstractConfigurablePlugin<\n    MarkersPluginConfig,\n    ParsedMarkersPluginConfig,\n    UpdatableMarkersPluginConfig,\n    MarkersPluginEvents\n> {\n    static override readonly id = 'markers';\n    static override readonly VERSION = PKG_VERSION;\n    static override readonly configParser = getConfig;\n    static override readonly readonlyOptions: Array<keyof MarkersPluginConfig> = ['markers'];\n\n    private readonly markers: Record<string, Marker> = {};\n\n    private readonly state = {\n        allVisible: true,\n        showAllTooltips: false,\n        currentMarker: null as Marker,\n        hoveringMarker: null as Marker,\n        // require a 2nd render (only the scene) when 3d markers visibility changes\n        needsReRender: false,\n        // use when updating a polygon marker in order to keep the current position\n        lastClientX: null as number,\n        lastClientY: null as number,\n    };\n\n    private readonly container: HTMLElement;\n    private readonly svgContainer: SVGElement;\n    private readonly css3DContainer: CSS3DContainer;\n\n    static withConfig(config: MarkersPluginConfig): [PluginConstructor, any] {\n        return [MarkersPlugin, config];\n    }\n\n    constructor(viewer: Viewer, config: MarkersPluginConfig) {\n        super(viewer, config);\n\n        this.container = document.createElement('div');\n        this.container.className = 'psv-markers';\n        this.viewer.container.appendChild(this.container);\n\n        this.container.addEventListener('contextmenu', e => e.preventDefault());\n\n        this.svgContainer = document.createElementNS(SVG_NS, 'svg');\n        this.svgContainer.setAttribute('class', 'psv-markers-svg-container');\n        this.container.appendChild(this.svgContainer);\n\n        this.css3DContainer = new CSS3DContainer(viewer);\n        this.container.appendChild(this.css3DContainer.element);\n\n        // Markers events via delegation\n        this.container.addEventListener('mouseenter', this, true);\n        this.container.addEventListener('mouseleave', this, true);\n        this.container.addEventListener('mousemove', this, true);\n    }\n\n    /**\n     * @internal\n     */\n    override init() {\n        super.init();\n\n        utils.checkStylesheet(this.viewer.container, 'markers-plugin');\n\n        // Viewer events\n        this.viewer.addEventListener(events.ClickEvent.type, this);\n        this.viewer.addEventListener(events.DoubleClickEvent.type, this);\n        this.viewer.addEventListener(events.RenderEvent.type, this);\n        this.viewer.addEventListener(events.ConfigChangedEvent.type, this);\n        this.viewer.addEventListener(events.ObjectEnterEvent.type, this);\n        this.viewer.addEventListener(events.ObjectHoverEvent.type, this);\n        this.viewer.addEventListener(events.ObjectLeaveEvent.type, this);\n        this.viewer.addEventListener(events.ReadyEvent.type, this, { once: true });\n    }\n\n    /**\n     * @internal\n     */\n    override destroy() {\n        this.clearMarkers(false);\n\n        this.viewer.unobserveObjects(MARKER_DATA);\n\n        this.viewer.removeEventListener(events.ClickEvent.type, this);\n        this.viewer.removeEventListener(events.DoubleClickEvent.type, this);\n        this.viewer.removeEventListener(events.RenderEvent.type, this);\n        this.viewer.removeEventListener(events.ObjectEnterEvent.type, this);\n        this.viewer.removeEventListener(events.ObjectHoverEvent.type, this);\n        this.viewer.removeEventListener(events.ObjectLeaveEvent.type, this);\n        this.viewer.removeEventListener(events.ReadyEvent.type, this);\n\n        this.css3DContainer.destroy();\n        this.viewer.container.removeChild(this.container);\n\n        super.destroy();\n    }\n\n    /**\n     * @internal\n     */\n    handleEvent(e: Event) {\n        switch (e.type) {\n            case events.ReadyEvent.type:\n                if (this.config.markers) {\n                    this.setMarkers(this.config.markers);\n                    delete this.config.markers;\n                }\n                break;\n\n            case events.RenderEvent.type:\n                this.renderMarkers();\n                break;\n\n            case events.ClickEvent.type:\n                this.__onClick(e as events.ClickEvent, false);\n                break;\n\n            case events.DoubleClickEvent.type:\n                this.__onClick(e as events.DoubleClickEvent, true);\n                break;\n\n            case events.ObjectEnterEvent.type:\n            case events.ObjectLeaveEvent.type:\n            case events.ObjectHoverEvent.type:\n                if ((e as events.ObjectEvent).userDataKey === MARKER_DATA) {\n                    const event = (e as events.ObjectEvent).originalEvent;\n                    const marker: Marker = (e as events.ObjectEvent).object.userData[MARKER_DATA];\n                    switch (e.type) {\n                        case events.ObjectEnterEvent.type:\n                            if (marker.config.style?.cursor) {\n                                this.viewer.setCursor(marker.config.style.cursor);\n                            } else if (marker.config.tooltip || marker.config.content) {\n                                this.viewer.setCursor('pointer');\n                            }\n                            this.__onEnterMarker(event, marker);\n                            break;\n                        case events.ObjectLeaveEvent.type:\n                            this.viewer.setCursor(null);\n                            this.__onLeaveMarker(marker);\n                            break;\n                        case events.ObjectHoverEvent.type:\n                            this.__onHoverMarker(event, marker);\n                            break;\n                    }\n                }\n                break;\n\n            case 'mouseenter': {\n                const marker = this.__getTargetMarker(utils.getEventTarget(e));\n                this.__onEnterMarker(e as MouseEvent, marker);\n                break;\n            }\n\n            case 'mouseleave': {\n                const marker = this.__getTargetMarker(utils.getEventTarget(e));\n                this.__onLeaveMarker(marker);\n                break;\n            }\n\n            case 'mousemove': {\n                const marker = this.__getTargetMarker(utils.getEventTarget(e), true);\n                this.__onHoverMarker(e as MouseEvent, marker);\n                break;\n            }\n        }\n    }\n\n    /**\n     * Toggles all markers\n     */\n    toggleAllMarkers() {\n        if (this.state.allVisible) {\n            this.hideAllMarkers();\n        } else {\n            this.showAllMarkers();\n        }\n    }\n\n    /**\n     * Shows all markers\n     */\n    showAllMarkers() {\n        this.state.allVisible = true;\n        Object.values(this.markers).forEach((marker) => {\n            marker.config.visible = true;\n        });\n        this.renderMarkers();\n        this.dispatchEvent(new ShowMarkersEvent());\n    }\n\n    /**\n     * Hides all markers\n     */\n    hideAllMarkers() {\n        this.state.allVisible = false;\n        Object.values(this.markers).forEach((marker) => {\n            marker.config.visible = false;\n        });\n        this.renderMarkers();\n        this.dispatchEvent(new HideMarkersEvent());\n    }\n\n    /**\n     * Toggles the visibility of all tooltips\n     */\n    toggleAllTooltips() {\n        if (this.state.showAllTooltips) {\n            this.hideAllTooltips();\n        } else {\n            this.showAllTooltips();\n        }\n    }\n\n    /**\n     *  Displays all tooltips\n     */\n    showAllTooltips() {\n        this.state.showAllTooltips = true;\n        Object.values(this.markers).forEach((marker) => {\n            marker.state.staticTooltip = true;\n            marker.showTooltip();\n        });\n    }\n\n    /**\n     * Hides all tooltips\n     */\n    hideAllTooltips() {\n        this.state.showAllTooltips = false;\n        Object.values(this.markers).forEach((marker) => {\n            marker.state.staticTooltip = false;\n            marker.hideTooltip();\n        });\n    }\n\n    /**\n     * Returns the total number of markers\n     */\n    getNbMarkers(): number {\n        return Object.keys(this.markers).length;\n    }\n\n    /**\n     * Returns all the markers\n     */\n    getMarkers(): Marker[] {\n        return Object.values(this.markers);\n    }\n\n    /**\n     * Adds a new marker to viewer\n     * @throws {@link PSVError} when the marker's id is missing or already exists\n     */\n    addMarker(config: MarkerConfig, render = true) {\n        if (this.markers[config.id]) {\n            throw new PSVError(`marker \"${config.id}\" already exists`);\n        }\n\n        // @ts-ignore\n        const marker: Marker = new (getMarkerCtor(config))(this.viewer, this, config);\n\n        if (marker.isPoly()) {\n            this.svgContainer.appendChild(marker.domElement);\n        } else if (marker.isCss3d()) {\n            this.css3DContainer.addObject(marker as MarkerCSS3D);\n        } else if (marker.is3d()) {\n            this.viewer.renderer.addObject(marker.threeElement);\n        } else {\n            this.container.appendChild(marker.domElement);\n        }\n\n        this.markers[marker.id] = marker;\n\n        if (this.state.showAllTooltips) {\n            marker.state.staticTooltip = true;\n        }\n\n        if (render) {\n            this.__afterChangeMarkers();\n        }\n    }\n\n    /**\n     * Returns the internal marker object for a marker id\n     * @throws {@link PSVError} when the marker cannot be found\n     */\n    getMarker(markerId: string | MarkerConfig): Marker {\n        const id = typeof markerId === 'object' ? markerId.id : markerId;\n\n        if (!this.markers[id]) {\n            throw new PSVError(`cannot find marker \"${id}\"`);\n        }\n\n        return this.markers[id];\n    }\n\n    /**\n     * Returns the last marker selected by the user\n     */\n    getCurrentMarker(): Marker {\n        return this.state.currentMarker;\n    }\n\n    /**\n     * Updates the existing marker with the same id\n     * Every property can be changed but you can't change its type (Eg: `image` to `html`)\n     */\n    updateMarker(config: MarkerConfig, render = true) {\n        const marker = this.getMarker(config.id);\n\n        marker.update(config);\n\n        if (render) {\n            this.__afterChangeMarkers();\n\n            if (\n                (marker === this.state.hoveringMarker && marker.config.tooltip?.trigger === 'hover')\n                || marker.state.staticTooltip\n            ) {\n                marker.showTooltip(this.state.lastClientX, this.state.lastClientY, true);\n            }\n        }\n    }\n\n    /**\n     * Removes a marker from the viewer\n     */\n    removeMarker(markerId: string | MarkerConfig, render = true) {\n        const marker = this.getMarker(markerId);\n\n        if (marker.isPoly()) {\n            this.svgContainer.removeChild(marker.domElement);\n        } else if (marker.isCss3d()) {\n            this.css3DContainer.removeObject(marker as MarkerCSS3D);\n        } else if (marker.is3d()) {\n            this.viewer.renderer.removeObject(marker.threeElement);\n        } else {\n            this.container.removeChild(marker.domElement);\n        }\n\n        if (this.state.hoveringMarker === marker) {\n            this.state.hoveringMarker = null;\n        }\n\n        if (this.state.currentMarker === marker) {\n            this.state.currentMarker = null;\n        }\n\n        marker.destroy();\n        delete this.markers[marker.id];\n\n        if (render) {\n            this.__afterChangeMarkers();\n        }\n    }\n\n    /**\n     * Removes multiple markers\n     */\n    removeMarkers(markerIds: string[], render = true) {\n        markerIds.forEach(markerId => this.removeMarker(markerId, false));\n\n        if (render) {\n            this.__afterChangeMarkers();\n        }\n    }\n\n    /**\n     * Replaces all markers\n     */\n    setMarkers(markers: MarkerConfig[] | null, render = true) {\n        this.clearMarkers(false);\n\n        markers?.forEach((marker) => {\n            this.addMarker(marker, false);\n        });\n\n        if (render) {\n            this.__afterChangeMarkers();\n        }\n    }\n\n    /**\n     * Removes all markers\n     */\n    clearMarkers(render = true) {\n        Object.keys(this.markers).forEach((markerId) => {\n            this.removeMarker(markerId, false);\n        });\n\n        if (render) {\n            this.__afterChangeMarkers();\n        }\n    }\n\n    /**\n     * Rotate the view to face the marker\n     */\n    gotoMarker(markerId: string | MarkerConfig, speed: string | number = this.config.gotoMarkerSpeed): Promise<void> {\n        const marker = this.getMarker(markerId);\n\n        if (!speed) {\n            this.viewer.rotate(marker.state.position);\n            if (!utils.isNil(marker.config.zoomLvl)) {\n                this.viewer.zoom(marker.config.zoomLvl);\n            }\n            this.dispatchEvent(new GotoMarkerDoneEvent(marker));\n            return Promise.resolve();\n        } else {\n            return this.viewer\n                .animate({\n                    ...marker.state.position,\n                    zoom: marker.config.zoomLvl,\n                    speed: speed,\n                })\n                .then(() => {\n                    this.dispatchEvent(new GotoMarkerDoneEvent(marker));\n                });\n        }\n    }\n\n    /**\n     * Hides a marker\n     */\n    hideMarker(markerId: string | MarkerConfig) {\n        this.toggleMarker(markerId, false);\n    }\n\n    /**\n     * Shows a marker\n     */\n    showMarker(markerId: string | MarkerConfig) {\n        this.toggleMarker(markerId, true);\n    }\n\n    /**\n     * Forces the display of the tooltip of a marker\n     */\n    showMarkerTooltip(markerId: string | MarkerConfig) {\n        const marker = this.getMarker(markerId);\n        marker.state.staticTooltip = true;\n        marker.showTooltip();\n    }\n\n    /**\n     * Hides the tooltip of a marker\n     */\n    hideMarkerTooltip(markerId: string | MarkerConfig) {\n        const marker = this.getMarker(markerId);\n        marker.state.staticTooltip = false;\n        marker.hideTooltip();\n    }\n\n    /**\n     * Toggles a marker visibility\n     */\n    toggleMarker(markerId: string | MarkerConfig, visible?: boolean) {\n        const marker = this.getMarker(markerId);\n        marker.config.visible = utils.isNil(visible) ? !marker.config.visible : visible;\n        this.renderMarkers();\n    }\n\n    /**\n     * Opens the panel with the content of the marker\n     */\n    showMarkerPanel(markerId: string | MarkerConfig) {\n        const marker = this.getMarker(markerId);\n\n        if (marker.config.content) {\n            this.viewer.panel.show({\n                id: ID_PANEL_MARKER,\n                content: marker.config.content,\n            });\n        } else {\n            this.hideMarkerPanel();\n        }\n    }\n\n    /**\n     * Closes the panel if currently showing the content of a marker\n     */\n    hideMarkerPanel() {\n        this.viewer.panel.hide(ID_PANEL_MARKER);\n    }\n\n    /**\n     * Toggles the visibility of the list of markers\n     */\n    toggleMarkersList() {\n        if (this.viewer.panel.isVisible(ID_PANEL_MARKERS_LIST)) {\n            this.hideMarkersList();\n        } else {\n            this.showMarkersList();\n        }\n    }\n\n    /**\n     * Opens side panel with the list of markers\n     */\n    showMarkersList() {\n        let markers: Marker[] = [];\n        Object.values(this.markers).forEach((marker) => {\n            if (marker.config.visible && !marker.config.hideList) {\n                markers.push(marker);\n            }\n        });\n\n        const e = new RenderMarkersListEvent(markers);\n        this.dispatchEvent(e);\n        markers = e.markers;\n\n        this.viewer.panel.show({\n            id: ID_PANEL_MARKERS_LIST,\n            content: MARKERS_LIST_TEMPLATE(markers, this.viewer.config.lang[MarkersButton.id]),\n            noMargin: true,\n            clickHandler: (target) => {\n                const li = utils.getClosest(target, '.psv-panel-menu-item');\n                const markerId = li ? li.dataset[MARKER_DATA] : undefined;\n\n                if (markerId) {\n                    const marker = this.getMarker(markerId);\n\n                    this.dispatchEvent(new SelectMarkerListEvent(marker));\n\n                    this.gotoMarker(marker.id);\n                    this.hideMarkersList();\n                }\n            },\n        });\n    }\n\n    /**\n     * Closes side panel if it contains the list of markers\n     */\n    hideMarkersList() {\n        this.viewer.panel.hide(ID_PANEL_MARKERS_LIST);\n    }\n\n    /**\n     * Updates the visibility and the position of all markers\n     */\n    renderMarkers() {\n        if (this.state.needsReRender) {\n            this.state.needsReRender = false;\n            return;\n        }\n\n        const zoomLevel = this.viewer.getZoomLevel();\n        const viewerPosition = this.viewer.getPosition();\n        const hoveringMarker = this.state.hoveringMarker;\n\n        Object.values(this.markers).forEach((marker) => {\n            let isVisible = marker.config.visible;\n            let visibilityChanged = false;\n            let position: Point = null;\n\n            if (isVisible) {\n                position = marker.render({ viewerPosition, zoomLevel, hoveringMarker });\n                isVisible = !!position;\n            }\n\n            visibilityChanged = marker.state.visible !== isVisible;\n            marker.state.visible = isVisible;\n            marker.state.position2D = position;\n\n            if (marker.domElement) {\n                utils.toggleClass(marker.domElement, 'psv-marker--visible', isVisible);\n            }\n\n            if (!isVisible) {\n                marker.hideTooltip();\n            } else if (marker.state.staticTooltip) {\n                marker.showTooltip();\n            } else if (marker !== this.state.hoveringMarker) {\n                marker.hideTooltip();\n            }\n\n            if (visibilityChanged) {\n                this.dispatchEvent(new MarkerVisibilityEvent(marker, isVisible));\n\n                if (marker.is3d() || marker.isCss3d()) {\n                    this.state.needsReRender = true;\n                }\n            }\n        });\n\n        if (this.state.needsReRender) {\n            this.viewer.needsUpdate();\n        }\n    }\n\n    /**\n     * Returns the marker associated to an event target\n     */\n    private __getTargetMarker(target: HTMLElement, closest?: boolean): Marker;\n    private __getTargetMarker(target: Object3D[]): Marker;\n    private __getTargetMarker(target: HTMLElement | Object3D[], closest = false): Marker {\n        if (target instanceof Node) {\n            const target2 = closest ? utils.getClosest(target, '.psv-marker') : target;\n            return target2 ? (target2 as any)[MARKER_DATA] : undefined;\n        } else if (Array.isArray(target)) {\n            return target\n                .map(o => o.userData[MARKER_DATA] as Marker)\n                .filter(m => !!m)\n                .sort((a, b) => b.config.zIndex - a.config.zIndex)[0];\n        } else {\n            return null;\n        }\n    }\n\n    /**\n     * Handles mouse enter events, show the tooltip for non polygon markers\n     */\n    private __onEnterMarker(e: MouseEvent, marker?: Marker) {\n        if (marker) {\n            this.state.hoveringMarker = marker;\n            this.state.lastClientX = e.clientX;\n            this.state.lastClientY = e.clientY;\n\n            this.dispatchEvent(new EnterMarkerEvent(marker));\n\n            if (marker instanceof AbstractStandardMarker) {\n                marker.applyScale({\n                    zoomLevel: this.viewer.getZoomLevel(),\n                    viewerPosition: this.viewer.getPosition(),\n                    mouseover: true,\n                });\n            }\n\n            if (!marker.state.staticTooltip && marker.config.tooltip?.trigger === 'hover') {\n                marker.showTooltip(e.clientX, e.clientY);\n            }\n        }\n    }\n\n    /**\n     * Handles mouse leave events, hide the tooltip\n     */\n    private __onLeaveMarker(marker?: Marker) {\n        if (marker) {\n            this.dispatchEvent(new LeaveMarkerEvent(marker));\n\n            if (marker instanceof AbstractStandardMarker) {\n                marker.applyScale({\n                    zoomLevel: this.viewer.getZoomLevel(),\n                    viewerPosition: this.viewer.getPosition(),\n                    mouseover: false,\n                });\n            }\n\n            this.state.hoveringMarker = null;\n\n            if (!marker.state.staticTooltip && marker.config.tooltip?.trigger === 'hover') {\n                marker.hideTooltip();\n            } else if (marker.state.staticTooltip) {\n                marker.showTooltip();\n            }\n        }\n    }\n\n    /**\n     * Handles mouse move events, refresh the tooltip for polygon markers\n     */\n    private __onHoverMarker(e: MouseEvent, marker?: Marker) {\n        if (marker) {\n            this.state.lastClientX = e.clientX;\n            this.state.lastClientY = e.clientY;\n\n            if (marker.isPoly() || marker.is3d() || marker.isCss3d()) {\n                if (marker.config.tooltip?.trigger === 'hover') {\n                    marker.showTooltip(e.clientX, e.clientY);\n                }\n            }\n        }\n    }\n\n    /**\n     * Handles mouse click events, select the marker and open the panel if necessary\n     */\n    private __onClick(e: events.ClickEvent | events.DoubleClickEvent, dblclick: boolean) {\n        const threeMarker = this.__getTargetMarker(e.data.objects);\n        const stdMarker = this.__getTargetMarker(e.data.target, true);\n\n        // give priority to standard markers which are always on top of Three markers\n        const marker = stdMarker || threeMarker;\n\n        if (this.state.currentMarker && this.state.currentMarker !== marker) {\n            this.dispatchEvent(new UnselectMarkerEvent(this.state.currentMarker));\n\n            this.viewer.panel.hide(ID_PANEL_MARKER);\n\n            if (!this.state.showAllTooltips && this.state.currentMarker.config.tooltip?.trigger === 'click') {\n                this.hideMarkerTooltip(this.state.currentMarker.id);\n            }\n\n            this.state.currentMarker = null;\n        }\n\n        if (marker) {\n            this.state.currentMarker = marker;\n\n            this.dispatchEvent(new SelectMarkerEvent(marker, dblclick, e.data.rightclick));\n\n            if (this.config.clickEventOnMarker) {\n                // add the marker to event data\n                e.data.marker = marker;\n            } else {\n                e.stopImmediatePropagation();\n            }\n\n            // the marker could have been deleted in an event handler\n            if (this.markers[marker.id] && !e.data.rightclick) {\n                if (marker.config.tooltip?.trigger === 'click') {\n                    if (marker.tooltip) {\n                        this.hideMarkerTooltip(marker.id);\n                    } else {\n                        this.showMarkerTooltip(marker.id);\n                    }\n                } else {\n                    this.showMarkerPanel(marker.id);\n                }\n            }\n        }\n    }\n\n    private __afterChangeMarkers() {\n        this.__refreshUi();\n        this.__checkObjectsObserver();\n        this.viewer.needsUpdate();\n        this.dispatchEvent(new SetMarkersEvent(this.getMarkers()));\n    }\n\n    /**\n     * Updates the visiblity of the panel and the buttons\n     */\n    private __refreshUi() {\n        const nbMarkers = Object.values(this.markers).filter(m => !m.config.hideList).length;\n\n        if (nbMarkers === 0) {\n            this.viewer.panel.hide(ID_PANEL_MARKER);\n            this.viewer.panel.hide(ID_PANEL_MARKERS_LIST);\n        } else {\n            if (this.viewer.panel.isVisible(ID_PANEL_MARKERS_LIST)) {\n                this.showMarkersList();\n            } else if (this.viewer.panel.isVisible(ID_PANEL_MARKER)) {\n                this.state.currentMarker ? this.showMarkerPanel(this.state.currentMarker.id) : this.viewer.panel.hide();\n            }\n        }\n\n        this.viewer.navbar.getButton(MarkersButton.id, false)?.toggle(nbMarkers > 0);\n        this.viewer.navbar.getButton(MarkersListButton.id, false)?.toggle(nbMarkers > 0);\n    }\n\n    /**\n     * Adds or remove the objects observer if there are 3D markers\n     */\n    private __checkObjectsObserver() {\n        const has3d = Object.values(this.markers).some(marker => marker.is3d());\n\n        if (has3d) {\n            this.viewer.observeObjects(MARKER_DATA);\n        } else {\n            this.viewer.unobserveObjects(MARKER_DATA);\n        }\n    }\n}\n","import { events, type Viewer } from '@photo-sphere-viewer/core';\nimport { Scene } from 'three';\nimport { CSS3DRenderer } from 'three/examples/jsm/renderers/CSS3DRenderer.js';\nimport { MARKER_DATA } from './constants';\nimport { MarkerCSS3D } from './markers/MarkerCSS3D';\n\n/**\n * @internal\n */\nexport class CSS3DContainer {\n    element: HTMLElement;\n\n    private readonly renderer: CSS3DRenderer;\n    private readonly scene: Scene;\n    private readonly intersectionObserver: IntersectionObserver;\n\n    constructor(\n        private viewer: Viewer,\n    ) {\n        this.element = document.createElement('div');\n        this.element.className = 'psv-markers-css3d-container';\n\n        this.renderer = new CSS3DRenderer({ element: this.element });\n        this.scene = new Scene();\n\n        this.intersectionObserver = new IntersectionObserver((entries) => {\n            entries.forEach((entry) => {\n                const marker = (entry.target as any)[MARKER_DATA] as MarkerCSS3D;\n                if (marker.config.visible) {\n                    marker.viewportIntersection = entry.isIntersecting;\n                }\n            });\n        }, {\n            root: this.element,\n        });\n\n        viewer.addEventListener(events.ReadyEvent.type, this, { once: true });\n        viewer.addEventListener(events.SizeUpdatedEvent.type, this);\n        viewer.addEventListener(events.RenderEvent.type, this);\n    }\n\n    handleEvent(e: Event) {\n        switch (e.type) {\n            case events.ReadyEvent.type:\n            case events.SizeUpdatedEvent.type:\n                this.updateSize();\n                break;\n            case events.RenderEvent.type:\n                this.render();\n                break;\n        }\n    }\n\n    destroy(): void {\n        this.viewer.removeEventListener(events.ReadyEvent.type, this);\n        this.viewer.removeEventListener(events.SizeUpdatedEvent.type, this);\n        this.viewer.removeEventListener(events.RenderEvent.type, this);\n\n        this.intersectionObserver.disconnect();\n    }\n\n    private updateSize() {\n        const size = this.viewer.getSize();\n        this.renderer.setSize(size.width, size.height);\n    }\n\n    private render() {\n        this.renderer.render(this.scene, this.viewer.renderer.camera);\n    }\n\n    addObject(marker: MarkerCSS3D) {\n        this.scene.add(marker.threeElement);\n        this.intersectionObserver.observe(marker.domElement);\n    }\n\n    removeObject(marker: MarkerCSS3D) {\n        this.scene.remove(marker.threeElement);\n        this.intersectionObserver.unobserve(marker.domElement);\n    }\n}\n","import {\n\tMatrix4,\n\tObject3D,\n\tQuaternion,\n\tVector3\n} from 'three';\n\n// Based on http://www.emagix.net/academic/mscs-project/item/camera-sync-with-css3-and-webgl-threejs\n\nconst _position = new Vector3();\nconst _quaternion = new Quaternion();\nconst _scale = new Vector3();\n\n/**\n * The base 3D object that is supported by {@link CSS3DRenderer}.\n *\n * @augments Object3D\n * @three_import import { CSS3DObject } from 'three/addons/renderers/CSS3DRenderer.js';\n */\nclass CSS3DObject extends Object3D {\n\n\t/**\n\t * Constructs a new CSS3D object.\n\t *\n\t * @param {DOMElement} [element] - The DOM element.\n\t */\n\tconstructor( element = document.createElement( 'div' ) ) {\n\n\t\tsuper();\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isCSS3DObject = true;\n\n\t\t/**\n\t\t * The DOM element which defines the appearance of this 3D object.\n\t\t *\n\t\t * @type {DOMElement}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.element = element;\n\t\tthis.element.style.position = 'absolute';\n\t\tthis.element.style.pointerEvents = 'auto';\n\t\tthis.element.style.userSelect = 'none';\n\n\t\tthis.element.setAttribute( 'draggable', false );\n\n\t\tthis.addEventListener( 'removed', function () {\n\n\t\t\tthis.traverse( function ( object ) {\n\n\t\t\t\tif (\n\t\t\t\t\tobject.element instanceof object.element.ownerDocument.defaultView.Element &&\n\t\t\t\t\tobject.element.parentNode !== null\n\t\t\t\t) {\n\n\t\t\t\t\tobject.element.remove();\n\n\t\t\t\t}\n\n\t\t\t} );\n\n\t\t} );\n\n\t}\n\n\tcopy( source, recursive ) {\n\n\t\tsuper.copy( source, recursive );\n\n\t\tthis.element = source.element.cloneNode( true );\n\n\t\treturn this;\n\n\t}\n\n}\n\n/**\n * A specialized version of {@link CSS3DObject} that represents\n * DOM elements as sprites.\n *\n * @augments CSS3DObject\n * @three_import import { CSS3DSprite } from 'three/addons/renderers/CSS3DRenderer.js';\n */\nclass CSS3DSprite extends CSS3DObject {\n\n\t/**\n\t * Constructs a new CSS3D sprite object.\n\t *\n\t * @param {DOMElement} [element] - The DOM element.\n\t */\n\tconstructor( element ) {\n\n\t\tsuper( element );\n\n\t\t/**\n\t\t * This flag can be used for type testing.\n\t\t *\n\t\t * @type {boolean}\n\t\t * @readonly\n\t\t * @default true\n\t\t */\n\t\tthis.isCSS3DSprite = true;\n\n\t\t/**\n\t\t * The sprite's rotation in radians.\n\t\t *\n\t\t * @type {number}\n\t\t * @default 0\n\t\t */\n\t\tthis.rotation2D = 0;\n\n\t}\n\n\tcopy( source, recursive ) {\n\n\t\tsuper.copy( source, recursive );\n\n\t\tthis.rotation2D = source.rotation2D;\n\n\t\treturn this;\n\n\t}\n\n}\n\n//\n\nconst _matrix = new Matrix4();\nconst _matrix2 = new Matrix4();\n\n/**\n * This renderer can be used to apply hierarchical 3D transformations to DOM elements\n * via the CSS3 [transform]{@link https://www.w3schools.com/cssref/css3_pr_transform.asp} property.\n * `CSS3DRenderer` is particularly interesting if you want to apply 3D effects to a website without\n * canvas based rendering. It can also be used in order to combine DOM elements with WebGLcontent.\n *\n * There are, however, some important limitations:\n *\n * - It's not possible to use the material system of *three.js*.\n * - It's also not possible to use geometries.\n * - The renderer only supports 100% browser and display zoom.\n *\n * So `CSS3DRenderer` is just focused on ordinary DOM elements. These elements are wrapped into special\n * 3D objects ({@link CSS3DObject} or {@link CSS3DSprite}) and then added to the scene graph.\n *\n * @three_import import { CSS3DRenderer } from 'three/addons/renderers/CSS3DRenderer.js';\n */\nclass CSS3DRenderer {\n\n\t/**\n\t * Constructs a new CSS3D renderer.\n\t *\n\t * @param {CSS3DRenderer~Parameters} [parameters] - The parameters.\n\t */\n\tconstructor( parameters = {} ) {\n\n\t\tconst _this = this;\n\n\t\tlet _width, _height;\n\t\tlet _widthHalf, _heightHalf;\n\n\t\tconst cache = {\n\t\t\tcamera: { style: '' },\n\t\t\tobjects: new WeakMap()\n\t\t};\n\n\t\tconst domElement = parameters.element !== undefined ? parameters.element : document.createElement( 'div' );\n\n\t\tdomElement.style.overflow = 'hidden';\n\n\t\t/**\n\t\t * The DOM where the renderer appends its child-elements.\n\t\t *\n\t\t * @type {DOMElement}\n\t\t */\n\t\tthis.domElement = domElement;\n\n\t\tconst viewElement = document.createElement( 'div' );\n\t\tviewElement.style.transformOrigin = '0 0';\n\t\tviewElement.style.pointerEvents = 'none';\n\t\tdomElement.appendChild( viewElement );\n\n\t\tconst cameraElement = document.createElement( 'div' );\n\n\t\tcameraElement.style.transformStyle = 'preserve-3d';\n\n\t\tviewElement.appendChild( cameraElement );\n\n\t\t/**\n\t\t * Returns an object containing the width and height of the renderer.\n\t\t *\n\t\t * @return {{width:number,height:number}} The size of the renderer.\n\t\t */\n\t\tthis.getSize = function () {\n\n\t\t\treturn {\n\t\t\t\twidth: _width,\n\t\t\t\theight: _height\n\t\t\t};\n\n\t\t};\n\n\t\t/**\n\t\t * Renders the given scene using the given camera.\n\t\t *\n\t\t * @param {Object3D} scene - A scene or any other type of 3D object.\n\t\t * @param {Camera} camera - The camera.\n\t\t */\n\t\tthis.render = function ( scene, camera ) {\n\n\t\t\tconst fov = camera.projectionMatrix.elements[ 5 ] * _heightHalf;\n\n\t\t\tif ( camera.view && camera.view.enabled ) {\n\n\t\t\t\t// view offset\n\t\t\t\tviewElement.style.transform = `translate( ${ - camera.view.offsetX * ( _width / camera.view.width ) }px, ${ - camera.view.offsetY * ( _height / camera.view.height ) }px )`;\n\n\t\t\t\t// view fullWidth and fullHeight, view width and height\n\t\t\t\tviewElement.style.transform += `scale( ${ camera.view.fullWidth / camera.view.width }, ${ camera.view.fullHeight / camera.view.height } )`;\n\n\t\t\t} else {\n\n\t\t\t\tviewElement.style.transform = '';\n\n\t\t\t}\n\n\t\t\tif ( scene.matrixWorldAutoUpdate === true ) scene.updateMatrixWorld();\n\t\t\tif ( camera.parent === null && camera.matrixWorldAutoUpdate === true ) camera.updateMatrixWorld();\n\n\t\t\tlet tx, ty;\n\n\t\t\tif ( camera.isOrthographicCamera ) {\n\n\t\t\t\ttx = - ( camera.right + camera.left ) / 2;\n\t\t\t\tty = ( camera.top + camera.bottom ) / 2;\n\n\t\t\t}\n\n\t\t\tconst scaleByViewOffset = camera.view && camera.view.enabled ? camera.view.height / camera.view.fullHeight : 1;\n\t\t\tconst cameraCSSMatrix = camera.isOrthographicCamera ?\n\t\t\t\t`scale( ${ scaleByViewOffset } )` + 'scale(' + fov + ')' + 'translate(' + epsilon( tx ) + 'px,' + epsilon( ty ) + 'px)' + getCameraCSSMatrix( camera.matrixWorldInverse ) :\n\t\t\t\t`scale( ${ scaleByViewOffset } )` + 'translateZ(' + fov + 'px)' + getCameraCSSMatrix( camera.matrixWorldInverse );\n\t\t\tconst perspective = camera.isPerspectiveCamera ? 'perspective(' + fov + 'px) ' : '';\n\n\t\t\tconst style = perspective + cameraCSSMatrix +\n\t\t\t\t'translate(' + _widthHalf + 'px,' + _heightHalf + 'px)';\n\n\t\t\tif ( cache.camera.style !== style ) {\n\n\t\t\t\tcameraElement.style.transform = style;\n\n\t\t\t\tcache.camera.style = style;\n\n\t\t\t}\n\n\t\t\trenderObject( scene, scene, camera, cameraCSSMatrix );\n\n\t\t};\n\n\t\t/**\n\t\t * Resizes the renderer to the given width and height.\n\t\t *\n\t\t * @param {number} width - The width of the renderer.\n\t\t * @param {number} height -