UNPKG

@egjs/view3d

Version:

Fast & Customizable glTF 3D model viewer, packed with full of features!

449 lines (384 loc) 13.5 kB
/* * Copyright (c) 2020 NAVER Corp. * egjs projects are licensed under the MIT license */ import * as THREE from "three"; import Component from "@egjs/component"; import View3D from "../View3D"; import Motion from "../core/Motion"; import Pose from "../core/Pose"; import * as BROWSER from "../const/browser"; import * as DEFAULT from "../const/default"; import { CONTROL_EVENTS } from "../const/internal"; import { AUTO, INPUT_TYPE, ZOOM_TYPE } from "../const/external"; import { getObjectOption } from "../utils"; import { ControlEvents, OptionGetters, ValueOf } from "../type/utils"; import CameraControl from "./CameraControl"; /** * @interface * @param {ZOOM_TYPE} [type="fov"] Zoom control type. * @param {number} [scale=1] Scale factor for panning. * @param {number} [duration=300] Duration of the input animation (ms) * @param {number} [minFov=1] Minimum vertical fov(field of view). * Only available when type is "fov". * You can get a bigger image with the smaller value of this. * @param {number} [maxFov="auto"] Maximum vertical fov(field of view). * Only available when type is "fov". * You can get a smaller image with the bigger value of this. * If `"auto"` is given, it will use Math.min(default fov + 45, 175). * @param {number} [minDistance=0.1] Minimum camera distance. This will be scaled to camera's default distance({@link camera.baseDistance Camera#baseDistance}) * Only available when type is "distance". * @param {number} [maxDistance=2] Maximum camera distance. This will be scaled to camera's default distance({@link camera.baseDistance Camera#baseDistance}) * Only available when type is "distance". * @param {boolean|object} [doubleTap=true] Configures double tap to zoom behavior, `false` to disable. * @param {number} [doubleTap.zoomIn=0.8] Zoom-in value, relative to fov/distance range.max. * @param {boolean} [doubleTap.useZoomOut=true] Whether to use zoom-out behavior on double tap. * @param {number} [doubleTap.duration=300] Duration of the zoom-in and zoom-out animation. * @param {number} [doubleTap.easing=EASING.EASE_OUT_CUBIC] Easing function of the zoom-in and zoom-out animation. * @param {function} [easing=EASING.EASE_OUT_CUBIC] Easing function of the animation. */ export interface ZoomControlOptions { type: ValueOf<typeof ZOOM_TYPE>; scale: number; duration: number; minFov: number; maxFov: typeof AUTO | number; minDistance: number; maxDistance: number; doubleTap: boolean | Partial<{ zoomIn: number; useZoomOut: boolean; duration: number; easing: (x: number) => number; }>; easing: (x: number) => number; } /** * Distance controller handling both mouse wheel and pinch zoom(fov) */ class ZoomControl extends Component<ControlEvents> implements CameraControl, OptionGetters<ZoomControlOptions> { // Options private _type: ZoomControlOptions["type"]; private _scale: ZoomControlOptions["scale"]; private _duration: ZoomControlOptions["duration"]; private _minFov: ZoomControlOptions["minFov"]; private _maxFov: ZoomControlOptions["maxFov"]; private _minDistance: ZoomControlOptions["minDistance"]; private _maxDistance: ZoomControlOptions["maxDistance"]; private _doubleTap: ZoomControlOptions["doubleTap"]; private _easing: ZoomControlOptions["easing"]; // Internal values private _view3D: View3D; private _scaleModifier: number = 1; private _wheelModifier: number = 0.02; private _touchModifier: number = 0.05; private _motion: Motion; private _prevTouchDistance: number = -1; private _enabled: boolean = false; private _isFirstTouch: boolean = true; private _isWheelScrolling: boolean = false; /** * Whether this control is enabled or not * @readonly */ public get enabled() { return this._enabled; } /** * Whether this control is animating the camera * @readonly * @type {boolean} */ public get animating() { return this._motion.activated; } /** * Currenet fov/distance range * @readonly * @type {Range} */ public get range() { return this._motion.range; } /** * Current control type * @readonly * @see {ZOOM_TYPE} * @default "fov" */ public get type() { return this._type; } /** * Scale factor of the zoom * @type number * @default 1 */ public get scale() { return this._scale; } /** * Duration of the input animation (ms) * @type {number} * @default 300 */ public get duration() { return this._duration; } /** * Minimum vertical fov(field of view). * Only available when type is "fov". * You can get a bigger image with the smaller value of this. * @type {number} * @default 1 */ public get minFov() { return this._minFov; } /** * Maximum vertical fov(field of view). * Only available when type is "fov". * You can get a smaller image with the bigger value of this. * If `"auto"` is given, it will use Math.min(default fov + 45, 175). * @type {"auto" | number} * @default "auto" */ public get maxFov() { return this._maxFov; } /** * Minimum camera distance. This will be scaled to camera's default distance({@link camera.baseDistance Camera#baseDistance}) * Only available when type is "distance". * @type {number} * @default 0.1 */ public get minDistance() { return this._minDistance; } /** * Maximum camera distance. This will be scaled to camera's default distance({@link camera.baseDistance Camera#baseDistance}) * Only available when type is "distance". * @type {number} * @default 2 */ public get maxDistance() { return this._maxDistance; } public get doubleTap() { return this._doubleTap; } /** * Easing function of the animation * @type {function} * @default EASING.EASE_OUT_CUBIC * @see EASING */ public get easing() { return this._easing; } public set type(val: ZoomControlOptions["type"]) { this._type = val; } public set scale(val: ZoomControlOptions["scale"]) { this._scale = val; } /** * Create new ZoomControl instance * @param {View3D} view3D An instance of View3D * @param {ZoomControlOptions} [options={}] Options */ public constructor(view3D: View3D, { type = ZOOM_TYPE.FOV, scale = 1, duration = DEFAULT.ANIMATION_DURATION, minFov = 1, maxFov = AUTO, minDistance = 0.1, maxDistance = 2, doubleTap = true, easing = DEFAULT.EASING }: Partial<ZoomControlOptions> = {}) { super(); this._view3D = view3D; this._type = type; this._scale = scale; this._duration = duration; this._minFov = minFov; this._maxFov = maxFov; this._minDistance = minDistance; this._maxDistance = maxDistance; this._doubleTap = doubleTap; this._easing = easing; this._motion = new Motion({ duration, easing, range: { min: -Infinity, max: Infinity } }); } /** * Destroy the instance and remove all event listeners attached * @returns {void} */ public destroy(): void { this.disable(); this.reset(); this.off(); } /** * Reset internal values * @returns {void} */ public reset(): void { this._prevTouchDistance = -1; this._isFirstTouch = true; this._isWheelScrolling = false; } /** * Update control by given deltaTime * @param deltaTime Number of milisec to update * @returns {void} */ public update(deltaTime: number): void { const camera = this._view3D.camera; const newPose = camera.newPose; const motion = this._motion; const prevProgress = motion.progress; const delta = motion.update(deltaTime); const newProgress = motion.progress; newPose.zoom -= delta; if (this._isWheelScrolling && prevProgress < 1 && newProgress >= 1) { this.trigger(CONTROL_EVENTS.RELEASE, { inputType: INPUT_TYPE.ZOOM }); this._isWheelScrolling = false; } } // eslint-disable-next-line @typescript-eslint/no-unused-vars public resize(size: { width: number; height: number }) { // DO NOTHING } /** * Enable this input and add event listeners * @returns {void} */ public enable(): void { if (this._enabled) return; const targetEl = this._view3D.renderer.canvas; targetEl.addEventListener(BROWSER.EVENTS.WHEEL, this._onWheel, { passive: false, capture: false }); targetEl.addEventListener(BROWSER.EVENTS.TOUCH_MOVE, this._onTouchMove, { passive: false, capture: false }); targetEl.addEventListener(BROWSER.EVENTS.TOUCH_END, this._onTouchEnd, { passive: false, capture: false }); targetEl.addEventListener(BROWSER.EVENTS.DOUBLE_CLICK, this._onDoubleClick); this._enabled = true; this.sync(); this.trigger(CONTROL_EVENTS.ENABLE, { inputType: INPUT_TYPE.ZOOM }); } /** * Disable this input and remove all event handlers * @returns {void} */ public disable(): void { if (!this._enabled) return; const targetEl = this._view3D.renderer.canvas; targetEl.removeEventListener(BROWSER.EVENTS.WHEEL, this._onWheel, false); targetEl.removeEventListener(BROWSER.EVENTS.TOUCH_MOVE, this._onTouchMove, false); targetEl.removeEventListener(BROWSER.EVENTS.TOUCH_END, this._onTouchEnd, false); targetEl.removeEventListener(BROWSER.EVENTS.DOUBLE_CLICK, this._onDoubleClick); this._enabled = false; this.trigger(CONTROL_EVENTS.DISABLE, { inputType: INPUT_TYPE.ZOOM }); } /** * Synchronize this control's state to given camera position * @returns {void} */ public sync(): void { const camera = this._view3D.camera; const motion = this._motion; motion.reset(-camera.zoom); if (this._type === ZOOM_TYPE.FOV) { this._scaleModifier = -1; } else { this._scaleModifier = -camera.baseDistance / 44; } } /** * Update fov range by the camera's current fov value * @returns {void} */ public updateRange(): void { const range = this._motion.range; const { camera } = this._view3D; if (this._type === ZOOM_TYPE.FOV) { const baseFov = camera.baseFov; const maxFov = this._maxFov; range.max = maxFov === AUTO ? Math.min(baseFov + 45, 175) - baseFov : maxFov - baseFov; range.min = this._minFov - baseFov; } else { range.max = camera.baseDistance * this._maxDistance - camera.baseDistance; range.min = camera.baseDistance * this._minDistance - camera.baseDistance; } } private _onWheel = (evt: WheelEvent) => { const wheelScrollable = this._view3D.wheelScrollable; if (evt.deltaY === 0 || wheelScrollable) return; evt.preventDefault(); evt.stopPropagation(); const motion = this._motion; const delta = -this._scale * this._scaleModifier * this._wheelModifier * evt.deltaY; if (!this._isWheelScrolling) { this.trigger(CONTROL_EVENTS.HOLD, { inputType: INPUT_TYPE.ZOOM }); } this._isWheelScrolling = true; motion.setEndDelta(delta); }; private _onTouchMove = (evt: TouchEvent) => { const touches = evt.touches; if (touches.length !== 2) return; if (evt.cancelable !== false) { evt.preventDefault(); } evt.stopPropagation(); const motion = this._motion; const prevTouchDistance = this._prevTouchDistance; const touchPoint1 = new THREE.Vector2(touches[0].pageX, touches[0].pageY); const touchPoint2 = new THREE.Vector2(touches[1].pageX, touches[1].pageY); const touchDiff = touchPoint1.sub(touchPoint2); const touchDistance = touchDiff.length() * this._scale * this._scaleModifier * this._touchModifier; const delta = this._isFirstTouch ? 0 : touchDistance - prevTouchDistance; this._prevTouchDistance = touchDistance; if (this._isFirstTouch) { this.trigger(CONTROL_EVENTS.HOLD, { inputType: INPUT_TYPE.ZOOM }); } this._isFirstTouch = false; motion.setEndDelta(delta); }; private _onTouchEnd = (evt: TouchEvent) => { if (evt.touches.length !== 0) return; this.trigger(CONTROL_EVENTS.RELEASE, { inputType: INPUT_TYPE.ZOOM }); this._prevTouchDistance = -1; this._isFirstTouch = true; }; private _onDoubleClick = (evt: MouseEvent) => { const view3D = this._view3D; if (!this._doubleTap || !view3D.model) return; const { zoomIn = 0.8, duration = DEFAULT.ANIMATION_DURATION, easing = DEFAULT.EASING, useZoomOut = true } = getObjectOption(this._doubleTap); const zoomRange = this._motion.range; const maxZoom = -zoomRange.min * zoomIn; if (view3D.camera.zoom >= maxZoom && useZoomOut) { const resetPose = view3D.camera.currentPose.clone(); resetPose.zoom = 0; void view3D.camera.reset(duration, easing, resetPose); return; } const raycaster = new THREE.Raycaster(); const pointer = new THREE.Vector2(); const canvasSize = view3D.renderer.canvasSize; pointer.x = (evt.offsetX / canvasSize.x) * 2 - 1; pointer.y = -(evt.offsetY / canvasSize.y) * 2 + 1; raycaster.setFromCamera(pointer, view3D.camera.threeCamera); const intersects = raycaster.intersectObject(view3D.model.scene); if (!intersects.length) return; // Nearest const intersect = intersects[0]; const newPivot = intersect.point; const { yaw, pitch } = view3D.camera; const resetPose = new Pose(yaw, pitch, maxZoom, newPivot.toArray()); void view3D.camera.reset(duration, easing, resetPose); }; } export default ZoomControl;