diff --git a/config/assets.json b/config/assets.json index c4edc8f..17d3d78 100644 --- a/config/assets.json +++ b/config/assets.json @@ -2,7 +2,7 @@ "1": { "gsplatUrl": "splatting/1完成.ply", "skyboxUrl": "cube/helipad-env-atlas.png", - "scale": 0.6, + "scale": 0.8, "additionalInfo": { "description": "这款 3D 打印土楼作品,以福建客家土楼为原型,精准还原了其 “外圆内方” 的经典布局与环形夯土墙结构。细节处复刻了错落的檐角、拱形窗洞与中心祖堂轮廓,用打印层叠纹理模拟夯土质感,既保留传统建筑的厚重韵味,又以轻量化材质呈现土楼的对称美学,成为可触摸的非遗建筑缩影。", "author": "宣洗楼", @@ -14,8 +14,8 @@ } }, "2": { - "gsplatUrl": "splatting/2完成.ply", - "skyboxUrl": "cube/FN_HDRI_029.hdr", + "gsplatUrl": "http://qny.xitaiworkshop.space/1finish.ply", + "skyboxUrl": "cube/helipad-env-atlas.png", "scale": 1, "additionalInfo": { "description": "城市街道场景模型,展示城市环境", @@ -29,7 +29,7 @@ }, "3": { "gsplatUrl": "splatting/3完成.ply", - "skyboxUrl": "cube/wide-street.hdr", + "skyboxUrl": "cube/helipad-env-atlas.png", "scale": 0.6, "additionalInfo": { "description": "人物角色模型,用于演示动画和交互", @@ -43,7 +43,7 @@ }, "4": { "gsplatUrl": "splatting/4完成.ply", - "skyboxUrl": "cube/wide-street.hdr", + "skyboxUrl": "cube/helipad-env-atlas.png", "scale": 1, "additionalInfo": { "description": "交通工具模型,展示精细机械结构", @@ -57,7 +57,7 @@ }, "5": { "gsplatUrl": "splatting/9完成.ply", - "skyboxUrl": "cube/wide-street.hdr", + "skyboxUrl": "cube/helipad-env-atlas.png", "scale": 0.3, "additionalInfo": { "description": "小型道具模型,用于场景细节装饰", @@ -71,7 +71,7 @@ }, "6": { "gsplatUrl": "splatting/10完成.ply", - "skyboxUrl": "cube/wide-street.hdr", + "skyboxUrl": "cube/helipad-env-atlas.png", "scale": 0.3, "additionalInfo": { "description": "室内场景组件,展示家居环境", @@ -85,7 +85,7 @@ }, "7": { "gsplatUrl": "splatting/12完成.ply", - "skyboxUrl": "cube/wide-street.hdr", + "skyboxUrl": "cube/helipad-env-atlas.png", "scale": 0.6, "additionalInfo": { "description": "自然环境模型,展示自然风光", @@ -99,7 +99,7 @@ }, "8": { "gsplatUrl": "splatting/13完成.ply", - "skyboxUrl": "cube/wide-street.hdr", + "skyboxUrl": "cube/helipad-env-atlas.png", "scale": 0.4, "additionalInfo": { "description": "特殊效果模型,展示视觉特效", @@ -113,7 +113,7 @@ }, "9": { "gsplatUrl": "splatting/14完成.ply", - "skyboxUrl": "cube/wide-street.hdr", + "skyboxUrl": "cube/helipad-env-atlas.png", "scale": 0.5, "additionalInfo": { "description": "抽象艺术模型,展示创意设计", @@ -127,7 +127,7 @@ }, "10": { "gsplatUrl": "splatting/15完成.ply", - "skyboxUrl": "cube/empty-room.hdr", + "skyboxUrl": "cube/helipad-env-atlas.png", "scale": 0.5, "additionalInfo": { "description": "产品展示模型,用于商业展示", diff --git a/config/projectcard.json b/config/projectcard.json new file mode 100644 index 0000000..9b600c2 --- /dev/null +++ b/config/projectcard.json @@ -0,0 +1,195 @@ +{ + "semesters": [ + { + "year": 2025, + "semester": "下学期", + "semesterClass": "upper", + "sectionClass": "semester-lower", + "models": [ + { + "id": 1, + "title": "模型 1", + "description": "厦门工学院档案馆", + "href": "/1", + "thumbnail": "thumbnails/1.png" + }, + { + "id": 2, + "title": "模型 2", + "description": "厦门工学院档案馆", + "href": "/2", + "thumbnail": "thumbnails/2.png" + }, + { + "id": 3, + "title": "模型 3", + "description": "厦门工学院档案馆", + "href": "/3", + "thumbnail": "thumbnails/3.png" + }, + { + "id": 4, + "title": "模型 4", + "description": "厦门工学院档案馆", + "href": "/4", + "thumbnail": "thumbnails/4.png" + }, + { + "id": 5, + "title": "模型 5", + "description": "厦门工学院档案馆", + "href": "/5", + "thumbnail": "thumbnails/5.png" + }, + { + "id": 6, + "title": "模型 6", + "description": "厦门工学院档案馆", + "href": "/6", + "thumbnail": "thumbnails/6.png" + }, + { + "id": 7, + "title": "模型 7", + "description": "厦门工学院档案馆", + "href": "/7", + "thumbnail": "thumbnails/7.png" + }, + { + "id": 8, + "title": "模型 8", + "description": "厦门工学院档案馆", + "href": "/8", + "thumbnail": "thumbnails/8.png" + }, + { + "id": 9, + "title": "模型 9", + "description": "厦门工学院档案馆", + "href": "/9", + "thumbnail": "thumbnails/9.png" + }, + { + "id": 10, + "title": "模型 10", + "description": "厦门工学院档案馆", + "href": "/10", + "thumbnail": "thumbnails/10.png" + }, + { + "id": 11, + "title": "模型 11", + "description": "厦门工学院档案馆", + "href": "/11", + "thumbnail": "thumbnails/1.png" + }, + { + "id": 12, + "title": "模型 12", + "description": "厦门工学院档案馆", + "href": "/12", + "thumbnail": "thumbnails/2.png" + }, + { + "id": 13, + "title": "模型 13", + "description": "厦门工学院档案馆", + "href": "/13", + "thumbnail": "thumbnails/3.png" + }, + { + "id": 14, + "title": "模型 14", + "description": "厦门工学院档案馆", + "href": "/14", + "thumbnail": "thumbnails/4.png" + }, + { + "id": 15, + "title": "模型 15", + "description": "厦门工学院档案馆", + "href": "/15", + "thumbnail": "thumbnails/5.png" + } + ] + }, + { + "year": 2025, + "semester": "上学期", + "semesterClass": "lower", + "sectionClass": "semester-upper", + "models": [ + { + "id": 1, + "title": "模型 1", + "description": "厦门工学院档案馆", + "href": "/1", + "thumbnail": "thumbnails/6.png" + }, + { + "id": 2, + "title": "模型 2", + "description": "厦门工学院档案馆", + "href": "/2", + "thumbnail": "thumbnails/7.png" + }, + { + "id": 3, + "title": "模型 3", + "description": "厦门工学院档案馆", + "href": "/3", + "thumbnail": "thumbnails/8.png" + }, + { + "id": 4, + "title": "模型 4", + "description": "厦门工学院档案馆", + "href": "/4", + "thumbnail": "thumbnails/9.png" + }, + { + "id": 5, + "title": "模型 5", + "description": "厦门工学院档案馆", + "href": "/5", + "thumbnail": "thumbnails/10.png" + }, + { + "id": 6, + "title": "模型 6", + "description": "厦门工学院档案馆", + "href": "/6", + "thumbnail": "thumbnails/1.png" + }, + { + "id": 7, + "title": "模型 7", + "description": "厦门工学院档案馆", + "href": "/7", + "thumbnail": "thumbnails/2.png" + }, + { + "id": 8, + "title": "模型 8", + "description": "厦门工学院档案馆", + "href": "/8", + "thumbnail": "thumbnails/3.png" + }, + { + "id": 9, + "title": "模型 9", + "description": "厦门工学院档案馆", + "href": "/9", + "thumbnail": "thumbnails/4.png" + }, + { + "id": 10, + "title": "模型 10", + "description": "厦门工学院档案馆", + "href": "/10", + "thumbnail": "thumbnails/5.png" + } + ] + } + ] +} \ No newline at end of file diff --git a/controls/camera-animation.ts b/controls/camera-animation.ts deleted file mode 100644 index 1f49af6..0000000 --- a/controls/camera-animation.ts +++ /dev/null @@ -1,69 +0,0 @@ -import * as pc from 'playcanvas'; - -/** - * 相机动画函数 - * @param time 时间参数 - * @param camera 相机实体 - * @param targetEntity 目标实体 - * @param orbitRadius 轨道半径 - * @param horizontalRange 水平范围 - * @param verticalRange 垂直范围 - */ -export function animateCamera(time: number, camera: pc.Entity, targetEntity: pc.Entity, orbitRadius: number, horizontalRange: number, verticalRange: number) { - // 水平方向:幅度由horizontalRange决定,速度由time系数决定 - const horizontalAngle = horizontalRange * Math.sin(time * 0.6); - - // 垂直方向 - const verticalAngle = verticalRange * Math.cos(time * 0.6); - - // 计算位置 - const x = orbitRadius * Math.sin(horizontalAngle); - const y = 2 + orbitRadius * Math.sin(verticalAngle); - const z = orbitRadius * Math.cos(horizontalAngle); - - camera.setLocalPosition(x, y, z); - camera.lookAt(targetEntity.getPosition()); -} - -/** - * 处理相机动画效果 - * @param dt 时间增量 - * @param currentTime 当前时间 - * @param autoRotate 是否自动旋转 - * @param lastMouseActivityTime 上次鼠标活动时间 - * @param autoRotateDelay 自动旋转延迟 - * @param camera 相机实体 - * @param targetEntity 目标实体 - * @param scriptInstance 脚本实例 - * @returns 更新后的时间 - */ -export function handleCameraTransition( - dt: number, - currentTime: number, - autoRotate: boolean, - lastMouseActivityTime: number, - autoRotateDelay: number, - camera: pc.Entity, - targetEntity: pc.Entity, - scriptInstance: any -): number { - let time = currentTime; - time += dt; - - // 检查是否应该恢复自动转动 - if (!autoRotate && time - lastMouseActivityTime > autoRotateDelay) { - autoRotate = true; - } - - // 根据状态决定是自动转动还是由脚本控制 - if (autoRotate) { - // 自动转动摄像头 - animateCamera(time, camera, targetEntity, 6, Math.PI / 6, Math.PI / 12); - } else { - if (scriptInstance) { - scriptInstance.skipUpdate = false; - } - } - - return time; // 返回更新后的时间 -} \ No newline at end of file diff --git a/controls/camera-controls.mjs b/controls/camera-controls.mjs index ae62cb7..eacefe8 100644 --- a/controls/camera-controls.mjs +++ b/controls/camera-controls.mjs @@ -1,827 +1,322 @@ -import { - math, - DualGestureSource, - FlyController, - FocusController, - GamepadSource, - InputFrame, - KeyboardMouseSource, - MultiTouchSource, - OrbitController, - Pose, - PROJECTION_PERSPECTIVE, - Script, - Vec2, - Vec3 -} from 'playcanvas'; - -/** @import { CameraComponent, InputController } from 'playcanvas' */ - -/** - * @typedef {object} CameraControlsState - * @property {Vec3} axis - The axis. - * @property {number} shift - The shift. - * @property {number} ctrl - The ctrl. - * @property {number[]} mouse - The mouse. - * @property {number} touches - The touches. - */ - -const tmpV1 = new Vec3(); -const tmpV2 = new Vec3(); - -const pose = new Pose(); - -const frame = new InputFrame({ - move: [0, 0, 0], - rotate: [0, 0, 0] -}); - -/** - * Calculate the damp rate. - * - * @param {number} damping - The damping. - * @param {number} dt - The delta time. - * @returns {number} - The lerp rate. - */ -export const damp = (damping, dt) => 1 - Math.pow(damping, dt * 1000); - -/** - * @param {number[]} stick - The stick - * @param {number} low - The low dead zone - * @param {number} high - The high dead zone - */ -const applyDeadZone = (stick, low, high) => { - const mag = Math.sqrt(stick[0] * stick[0] + stick[1] * stick[1]); - if (mag < low) { - stick.fill(0); +/* + 经典 PlayCanvas 脚本(资产方式加载)。 + - 不使用 ESM import;依赖全局 pc + - 通过 pc.createScript('cameraControls') 注册 + - 保留原有功能与方法签名(setTarget、setAutoRotateDelay、setAutoRotateSpeed、setAutoRotate、syncFromCamera) + - 追加 setTargetEntity(entity) 以兼容现有调用 +*/ + +/* global pc */ + +// 定义脚本类型 +var CameraControls = pc.createScript('cameraControls'); + +// 属性(实例字段) +CameraControls.prototype._camera = null; +CameraControls.prototype._target = new pc.Vec3(0, -1, 0); +CameraControls.prototype._distance = 8; +CameraControls.prototype._defaultDistance = 8; +CameraControls.prototype._yaw = 45; +CameraControls.prototype._pitch = 30; +CameraControls.prototype._minDistance = 3; +CameraControls.prototype._maxDistance = 15; +CameraControls.prototype._minPitch = -10; +CameraControls.prototype._maxPitch = 60; +CameraControls.prototype._minYaw = -120; +CameraControls.prototype._maxYaw = 120; +CameraControls.prototype._isDragging = false; +CameraControls.prototype._lastX = 0; +CameraControls.prototype._lastY = 0; +CameraControls.prototype._autoRotate = false; +CameraControls.prototype._isTransitioning = false; +CameraControls.prototype._transitionTime = 0; +CameraControls.prototype._transitionDuration = 3.0; +CameraControls.prototype._autoAnimationTime = 0; +CameraControls.prototype._lastMouseActivityTime = 0; +CameraControls.prototype._autoRotateDelay = 3; +CameraControls.prototype._autoRotateSpeed = 0.8; +CameraControls.prototype._needsStateSync = false; +CameraControls.prototype._horizontalSwingRange = 30; +CameraControls.prototype._verticalSwingRange = 15; +CameraControls.prototype._basePitch = 15; +CameraControls.prototype._lastTouchDistance = 0; +CameraControls.prototype._isPinching = false; +CameraControls.prototype._targetEntity = null; + +// 初始化(原 constructor) +CameraControls.prototype.initialize = function () { + if (!this.entity.camera) { + console.error('CameraControls: camera component not found'); return; } - const scale = (mag - low) / (high - low); - stick[0] *= scale / mag; - stick[1] *= scale / mag; + this._camera = this.entity.camera; + + this._updateControllerFromCamera(); + + var canvas = this.app.graphicsDevice.canvas; + // 事件监听 + this._onMouseDownBound = this._onMouseDown.bind(this); + this._onMouseMoveBound = this._onMouseMove.bind(this); + this._onMouseUpBound = this._onMouseUp.bind(this); + this._onMouseWheelBound = this._onMouseWheel.bind(this); + this._onTouchStartBound = this._onTouchStart.bind(this); + this._onTouchMoveBound = this._onTouchMove.bind(this); + this._onTouchEndBound = this._onTouchEnd.bind(this); + + canvas.addEventListener('mousedown', this._onMouseDownBound); + canvas.addEventListener('mousemove', this._onMouseMoveBound); + window.addEventListener('mouseup', this._onMouseUpBound); + canvas.addEventListener('wheel', this._onMouseWheelBound, { passive: false }); + canvas.addEventListener('touchstart', this._onTouchStartBound, { passive: false }); + canvas.addEventListener('touchmove', this._onTouchMoveBound, { passive: false }); + canvas.addEventListener('touchend', this._onTouchEndBound, { passive: false }); + + this._autoAnimationTime = Date.now() / 1000; + this._lastMouseActivityTime = Date.now() / 1000; }; -/** - * Converts screen space mouse deltas to world space pan vector. - * - * @param {CameraComponent} camera - The camera component. - * @param {number} dx - The mouse delta x value. - * @param {number} dy - The mouse delta y value. - * @param {number} dz - The world space zoom delta value. - * @param {Vec3} [out] - The output vector to store the pan result. - * @returns {Vec3} - The pan vector in world space. - * @private - */ -const screenToWorld = (camera, dx, dy, dz, out = new Vec3()) => { - const { system, fov, aspectRatio, horizontalFov, projection, orthoHeight } = camera; - const { width, height } = system.app.graphicsDevice.clientRect; - - // normalize deltas to device coord space - out.set( - -(dx / width) * 2, - (dy / height) * 2, - 0 - ); - - // calculate half size of the view frustum at the current distance - const halfSize = tmpV2.set(0, 0, 0); - if (projection === PROJECTION_PERSPECTIVE) { - const halfSlice = dz * Math.tan(0.5 * fov * math.DEG_TO_RAD); - if (horizontalFov) { - halfSize.set( - halfSlice, - halfSlice / aspectRatio, - 0 - ); - } else { - halfSize.set( - halfSlice * aspectRatio, - halfSlice, - 0 - ); - } - } else { - halfSize.set( - orthoHeight * aspectRatio, - orthoHeight, - 0 - ); +// 销毁 +CameraControls.prototype.onDestroy = function () { + var canvas = this.app.graphicsDevice.canvas; + if (canvas) { + canvas.removeEventListener('mousedown', this._onMouseDownBound); + canvas.removeEventListener('mousemove', this._onMouseMoveBound); + window.removeEventListener('mouseup', this._onMouseUpBound); + canvas.removeEventListener('wheel', this._onMouseWheelBound); + canvas.removeEventListener('touchstart', this._onTouchStartBound); + canvas.removeEventListener('touchmove', this._onTouchMoveBound); + canvas.removeEventListener('touchend', this._onTouchEndBound); } - - // scale by device coord space - out.mul(halfSize); - - return out; }; -class CameraControls extends Script { - static scriptName = 'cameraControls'; - - /** - * @type {CameraComponent} - * @private - */ - // @ts-ignore - _camera; - - /** - * @type {boolean} - * @private - */ - _enableOrbit = true; - - /** - * @type {boolean} - * @private - */ - _enableFly = true; - - /** - * @type {number} - * @private - */ - _startZoomDist = 0; - - /** - * @type {Vec2} - * @private - */ - _pitchRange = new Vec2(-360, 360); - - /** - * @type {Vec2} - * @private - */ - _yawRange = new Vec2(-360, 360); - - /** - * @type {Vec2} - * @private - */ - _zoomRange = new Vec2(0.01, 0); - - /** - * @type {KeyboardMouseSource} - * @private - */ - _desktopInput = new KeyboardMouseSource(); - - /** - * @type {MultiTouchSource} - * @private - */ - _orbitMobileInput = new MultiTouchSource(); - - /** - * @type {DualGestureSource} - * @private - */ - _flyMobileInput = new DualGestureSource(); - - /** - * @type {GamepadSource} - * @private - */ - _gamepadInput = new GamepadSource(); - - /** - * @type {FlyController} - * @private - */ - _flyController = new FlyController(); - - /** - * @type {OrbitController} - * @private - */ - _orbitController = new OrbitController(); - - /** - * @type {FocusController} - * @private - */ - _focusController = new FocusController(); - - /** - * @type {InputController} - * @private - */ - // @ts-ignore - _controller; - - /** - * @type {Pose} - * @private - */ - _pose = new Pose(); - - /** - * @type {'orbit' | 'fly' | 'focus'} - * @private - */ - // @ts-ignore - _mode; - - /** - * @type {CameraControlsState} - * @private - */ - _state = { - axis: new Vec3(), - shift: 0, - ctrl: 0, - mouse: [0, 0, 0], - touches: 0 - }; - - /** - * Whether to skip the update. - * - * @attribute - * @title Skip Update - * @type {boolean} - */ - skipUpdate = false; - - /** - * Enable panning. - * - * @attribute - * @title Enable Panning - * @type {boolean} - */ - enablePan = true; - - /** - * The scene size. The zoom, pan and fly speeds are relative to this size. - * - * @attribute - * @title Scene Size - * @type {number} - */ - sceneSize = 100; - - /** - * The rotation speed. - * - * @attribute - * @title Rotate Speed - * @type {number} - */ - rotateSpeed = 0.2; - - /** - * The rotation joystick sensitivity. - * - * @attribute - * @title Rotate Joystick Sensitivity - * @type {number} - */ - rotateJoystickSens = 2; - - /** - * The fly move speed relative to the scene size. - * - * @attribute - * @title Move Speed - * @type {number} - */ - moveSpeed = 2; - - /** - * The fast fly move speed relative to the scene size. - * - * @attribute - * @title Move Fast Speed - * @type {number} - */ - moveFastSpeed = 4; - - /** - * The slow fly move speed relative to the scene size. - * - * @attribute - * @title Move Slow Speed - * @type {number} - */ - moveSlowSpeed = 1; - - /** - * The zoom speed relative to the scene size. - * - * @attribute - * @title Zoom Speed - * @type {number} - */ - zoomSpeed = 0.001; - - /** - * The touch zoom pinch sensitivity. - * - * @attribute - * @title Zoom - * @type {number} - */ - zoomPinchSens = 5; - - /** - * The gamepad dead zone. - * - * @attribute - * @title Gamepad Dead Zone - * @type {Vec2} - */ - gamepadDeadZone = new Vec2(0.3, 0.6); - - /** - * The joystick event name for the UI position for the base and stick elements. - * The event name is appended with the side: 'left' or 'right'. - * - * @attribute - * @title Joystick Base Event Name - * @type {string} - */ - joystickEventName = 'joystick'; - - constructor({ app, entity, ...args }) { - super({ app, entity, ...args }); - if (!this.entity.camera) { - console.error('CameraControls: camera component not found'); - return; - } - this._camera = this.entity.camera; - - // set orbit controller defaults - this._orbitController.zoomRange = new Vec2(0.01, Infinity); - - // attach input - this._desktopInput.attach(this.app.graphicsDevice.canvas); - this._orbitMobileInput.attach(this.app.graphicsDevice.canvas); - this._flyMobileInput.attach(this.app.graphicsDevice.canvas); - this._gamepadInput.attach(this.app.graphicsDevice.canvas); - - // expose ui events - this._flyMobileInput.on('joystick:position:left', ([bx, by, sx, sy]) => { - if (this._mode !== 'fly') { - return; - } - this.app.fire(`${this.joystickEventName}:left`, bx, by, sx, sy); - }); - this._flyMobileInput.on('joystick:position:right', ([bx, by, sx, sy]) => { - if (this._mode !== 'fly') { - return; - } - this.app.fire(`${this.joystickEventName}:right`, bx, by, sx, sy); - }); - - // pose - this._pose.look(this._camera.entity.getPosition(), Vec3.ZERO); - - // mode - this._setMode('orbit'); - - // destroy - this.on('destroy', this._destroy, this); - } +// 每帧更新 +CameraControls.prototype.update = function (dt) { + var currentTime = Date.now() / 1000; - /** - * Enable orbit camera controls. - * - * @attribute - * @title Enable Orbit - * @type {boolean} - * @default true - */ - set enableOrbit(enable) { - this._enableOrbit = enable; - - if (!this._enableOrbit && this._mode === 'orbit') { - this._setMode('fly'); - } + // 若绑定了目标实体,则每帧同步目标点 + if (this._targetEntity && this._targetEntity.getPosition) { + this._target.copy(this._targetEntity.getPosition()); } - get enableOrbit() { - return this._enableOrbit; + if (this._needsStateSync) { + this._updateControllerFromCamera(); + this._needsStateSync = false; } - /** - * Enable fly camera controls. - * - * @attribute - * @title Enable Fly - * @type {boolean} - * @default true - */ - set enableFly(enable) { - this._enableFly = enable; - - if (!this._enableFly && this._mode === 'fly') { - this._setMode('orbit'); - } + if (!this._autoRotate && !this._isDragging && currentTime - this._lastMouseActivityTime > this._autoRotateDelay) { + this._autoRotate = true; + this._isTransitioning = true; + this._transitionTime = 0; } - get enableFly() { - return this._enableFly; - } - - /** - * The focus point. - * - * @attribute - * @title Focus Point - * @type {Vec3} - * @default [0, 0, 0] - */ - set focusPoint(point) { - const position = this._camera.entity.getPosition(); - this._startZoomDist = position.distance(point); - this._controller.attach(this._pose.look(position, point), false); - } - - get focusPoint() { - return this._pose.getFocus(tmpV1); + if (this._autoRotate) { + this._performAutoRotate(dt); } +}; - /** - * The focus damping. A higher value means more damping. A value of 0 means no damping. - * The damping is applied to the orbit mode. - * - * @attribute - * @title Rotate Damping - * @type {number} - * @default 0.98 - */ - set focusDamping(damping) { - this._focusController.focusDamping = damping; - } +// 公共方法 +CameraControls.prototype.setAutoRotateDelay = function (delay) { + this._autoRotateDelay = delay; +}; - get focusDamping() { - return this._focusController.focusDamping; - } +CameraControls.prototype.setAutoRotateSpeed = function (speed) { + this._autoRotateSpeed = speed; +}; - /** - * The rotate damping. In the range 0 to 1, where a value of 0 means no damping and 1 means full - * damping. The damping is applied to both the fly and orbit modes. - * - * @attribute - * @title Rotate Damping - * @type {number} - * @default 0.98 - */ - set rotateDamping(damping) { - this._flyController.rotateDamping = damping; - this._orbitController.rotateDamping = damping; +CameraControls.prototype.setAutoRotate = function (autoRotate) { + if (autoRotate && !this._autoRotate) { + this._isTransitioning = true; + this._transitionTime = 0; } + this._autoRotate = autoRotate; +}; - get rotateDamping() { - return this._orbitController.rotateDamping; - } +CameraControls.prototype.setTarget = function (target) { + this._target.copy(target); + this._updateCameraFromController(); +}; - /** - * The move damping. In the range 0 to 1, where a value of 0 means no damping and 1 means full - * damping. The damping is applied to the fly mode and the orbit mode when panning. - * - * @attribute - * @title Move Damping - * @type {number} - * @default 0.98 - */ - set moveDamping(damping) { - this._flyController.moveDamping = damping; +CameraControls.prototype.setTargetEntity = function (entity) { + this._targetEntity = entity || null; + if (this._targetEntity && this._targetEntity.getPosition) { + this._target.copy(this._targetEntity.getPosition()); + this._updateCameraFromController(); } +}; - get moveDamping() { - return this._flyController.moveDamping; - } +CameraControls.prototype.syncFromCamera = function () { + this._needsStateSync = true; +}; - /** - * The zoom damping. In the range 0 to 1, where a value of 0 means no damping and 1 means full - * damping. The damping is applied to the orbit mode. - * - * @attribute - * @title Zoom Damping - * @type {number} - * @default 0.98 - */ - set zoomDamping(damping) { - this._orbitController.zoomDamping = damping; +// 私有:从相机位置初始化控制器状态 +CameraControls.prototype._updateControllerFromCamera = function () { + var cameraPos = this.entity.getPosition(); + var direction = new pc.Vec3(); + direction.sub2(cameraPos, this._target); + if (direction.length() > 0.001) { + direction.normalize(); + this._yaw = Math.atan2(direction.x, direction.z) * 180 / Math.PI; + this._pitch = Math.asin(direction.y) * 180 / Math.PI; } + this._updateCameraFromController(); +}; - get zoomDamping() { - return this._orbitController.zoomDamping; - } +// 私有:根据控制器状态更新相机 +CameraControls.prototype._updateCameraFromController = function () { + this._yaw = Math.max(this._minYaw, Math.min(this._maxYaw, this._yaw)); + this._pitch = Math.max(this._minPitch, Math.min(this._maxPitch, this._pitch)); + this._distance = Math.max(this._minDistance, Math.min(this._maxDistance, this._distance)); + + var radYaw = this._yaw * Math.PI / 180; + var radPitch = this._pitch * Math.PI / 180; + var x = this._target.x + this._distance * Math.cos(radPitch) * Math.sin(radYaw); + var y = this._target.y + this._distance * Math.sin(radPitch); + var z = this._target.z + this._distance * Math.cos(radPitch) * Math.cos(radYaw); + this.entity.setPosition(x, y, z); + this.entity.lookAt(this._target); +}; - /** - * The pitch range. In the range -360 to 360 degrees. The pitch range is applied to the fly mode - * and the orbit mode. - * - * @attribute - * @title Pitch Range - * @type {Vec2} - * @default [-360, 360] - */ - set pitchRange(range) { - this._pitchRange.x = math.clamp(range.x, -360, 360); - this._pitchRange.y = math.clamp(range.y, -360, 360); - this._flyController.pitchRange = this._pitchRange; - this._orbitController.pitchRange = this._pitchRange; - } +// 私有:自动旋转 +CameraControls.prototype._performAutoRotate = function (dt) { + if (this._isTransitioning) { + this._transitionTime += dt; + var progress = Math.min(this._transitionTime / this._transitionDuration, 1.0); + var easeProgress = 1 - Math.pow(1 - progress, 3); + this._autoAnimationTime += dt * this._autoRotateSpeed; - get pitchRange() { - return this._pitchRange; - } + var baseYaw = 0; + var maxYawRange = Math.min(this._horizontalSwingRange, (this._maxYaw - this._minYaw) / 2); + var targetYaw = Math.max(this._minYaw, Math.min(this._maxYaw, baseYaw + maxYawRange * Math.sin(this._autoAnimationTime * 0.8))); - /** - * The yaw range. In the range -360 to 360 degrees. The pitch range is applied to the fly mode - * and the orbit mode. - * - * @attribute - * @title Yaw Range - * @type {Vec2} - * @default [-360, 360] - */ - set yawRange(range) { - this._yawRange.x = math.clamp(range.x, -360, 360); - this._yawRange.y = math.clamp(range.y, -360, 360); - this._flyController.yawRange = this._yawRange; - this._orbitController.yawRange = this._yawRange; - } + var maxPitchRange = Math.min(this._verticalSwingRange, Math.min(this._maxPitch - this._basePitch, this._basePitch - this._minPitch)); + var targetPitch = Math.max(this._minPitch, Math.min(this._maxPitch, this._basePitch + maxPitchRange * Math.cos(this._autoAnimationTime * 0.8))); - get yawRange() { - return this._yawRange; - } + var targetDistance = Math.max(this._minDistance, Math.min(this._maxDistance, this._defaultDistance)); - /** - * The zoom range. - * - * @attribute - * @title Zoom Range - * @type {Vec2} - * @default [0.01, 0] - */ - set zoomRange(range) { - this._zoomRange.x = range.x; - this._zoomRange.y = range.y <= range.x ? Infinity : range.y; - this._orbitController.zoomRange = this._zoomRange; - } + this._yaw = this._yaw + (targetYaw - this._yaw) * easeProgress; + this._pitch = this._pitch + (targetPitch - this._pitch) * easeProgress; + this._distance = this._distance + (targetDistance - this._distance) * easeProgress; - get zoomRange() { - return this._zoomRange; - } + this._yaw = Math.max(this._minYaw, Math.min(this._maxYaw, this._yaw)); + this._pitch = Math.max(this._minPitch, Math.min(this._maxPitch, this._pitch)); + this._distance = Math.max(this._minDistance, Math.min(this._maxDistance, this._distance)); - /** - * The layout of the mobile input. The layout can be one of the following: - * - * - `joystick-joystick`: Two virtual joysticks. - * - `joystick-touch`: One virtual joystick and one touch. - * - `touch-joystick`: One touch and one virtual joystick. - * - `touch-touch`: Two touches. - * - * Default is `joystick-touch`. - * - * @attribute - * @title Use Virtual Gamepad - * @type {string} - * @default 'joystick-touch' - */ - set mobileInputLayout(layout) { - if (!/(?:joystick|touch)-(?:joystick|touch)/.test(layout)) { - console.warn(`CameraControls: invalid mobile input layout: ${layout}`); - return; + if (progress >= 1.0) { + this._isTransitioning = false; + this._distance = Math.max(this._minDistance, Math.min(this._maxDistance, this._defaultDistance)); } - this._flyMobileInput.layout = layout; - } + } else { + this._distance = Math.max(this._minDistance, Math.min(this._maxDistance, this._defaultDistance)); + this._autoAnimationTime += dt * this._autoRotateSpeed; - get mobileInputLayout() { - return this._flyMobileInput.layout; - } + var baseYaw2 = 0; + var maxYawRange2 = Math.min(this._horizontalSwingRange, (this._maxYaw - this._minYaw) / 2); + this._yaw = Math.max(this._minYaw, Math.min(this._maxYaw, baseYaw2 + maxYawRange2 * Math.sin(this._autoAnimationTime * 0.8))); - /** - * @private - */ - _destroy() { - this._desktopInput.destroy(); - this._orbitMobileInput.destroy(); - this._flyMobileInput.destroy(); - this._gamepadInput.destroy(); - - this._flyController.destroy(); - this._orbitController.destroy(); + var maxPitchRange2 = Math.min(this._verticalSwingRange, Math.min(this._maxPitch - this._basePitch, this._basePitch - this._minPitch)); + this._pitch = Math.max(this._minPitch, Math.min(this._maxPitch, this._basePitch + maxPitchRange2 * Math.cos(this._autoAnimationTime * 0.8))); } - /** - * @param {'orbit' | 'fly' | 'focus'} mode - The mode to set. - * @private - */ - _setMode(mode) { - // override mode depending on enabled features - switch (true) { - case this.enableFly && !this.enableOrbit: { - mode = 'fly'; - break; - } - case !this.enableFly && this.enableOrbit: { - mode = 'orbit'; - break; - } - case !this.enableFly && !this.enableOrbit: { - console.warn('CameraControls: both fly and orbit modes are disabled'); - return; - } - } - - // check if mode is the same - if (this._mode === mode) { - return; - } - this._mode = mode; - - // detach old controller - if (this._controller) { - this._controller.detach(); - } - - // attach new controller - switch (this._mode) { - case 'orbit': { - this._controller = this._orbitController; - break; - } - case 'fly': { - this._controller = this._flyController; - break; - } - case 'focus': { - this._controller = this._focusController; - break; - } - } - this._controller.attach(this._pose, false); - } + this._updateCameraFromController(); +}; - /** - * @param {Vec3} focus - The focus point. - * @param {boolean} [resetZoom] - Whether to reset the zoom. - */ - focus(focus, resetZoom = false) { - this._setMode('focus'); - const zoomDist = resetZoom ? - this._startZoomDist : this._camera.entity.getPosition().distance(focus); - const position = tmpV1.copy(this._camera.entity.forward) - .mulScalar(-zoomDist) - .add(focus); - this._controller.attach(pose.look(position, focus)); +// 交互事件 +CameraControls.prototype._onMouseDown = function (e) { + if (e.button === 0) { + this._isDragging = true; + this._lastX = e.clientX; + this._lastY = e.clientY; + this._autoRotate = false; + this._lastMouseActivityTime = Date.now() / 1000; } +}; - /** - * @param {Vec3} focus - The focus point. - * @param {boolean} [resetZoom] - Whether to reset the zoom. - */ - look(focus, resetZoom = false) { - this._setMode('focus'); - const position = resetZoom ? - tmpV1.copy(this._camera.entity.getPosition()) - .sub(focus) - .normalize() - .mulScalar(this._startZoomDist) - .add(focus) : this._camera.entity.getPosition(); - this._controller.attach(pose.look(position, focus)); +CameraControls.prototype._onMouseMove = function (e) { + if (this._isDragging) { + var deltaX = e.clientX - this._lastX; + var deltaY = e.clientY - this._lastY; + this._yaw -= deltaX * 0.15; + this._pitch += deltaY * 0.15; + this._pitch = Math.max(this._minPitch, Math.min(this._maxPitch, this._pitch)); + this._lastX = e.clientX; + this._lastY = e.clientY; + this._lastMouseActivityTime = Date.now() / 1000; + this._updateCameraFromController(); } +}; - /** - * @param {Vec3} focus - The focus point. - * @param {Vec3} position - The start point. - */ - reset(focus, position) { - this._setMode('focus'); - this._controller.attach(pose.look(position, focus)); +CameraControls.prototype._onMouseUp = function () { + if (this._isDragging) { + this._isDragging = false; + this._lastMouseActivityTime = Date.now() / 1000; } +}; - /** - * @param {number} dt - The time delta. - */ - update(dt) { - const { keyCode } = KeyboardMouseSource; - - const { key, button, mouse, wheel } = this._desktopInput.read(); - const { touch, pinch, count } = this._orbitMobileInput.read(); - const { leftInput, rightInput } = this._flyMobileInput.read(); - const { leftStick, rightStick } = this._gamepadInput.read(); - - // apply dead zone to gamepad sticks - applyDeadZone(leftStick, this.gamepadDeadZone.x, this.gamepadDeadZone.y); - applyDeadZone(rightStick, this.gamepadDeadZone.x, this.gamepadDeadZone.y); - - // update state - this._state.axis.add(tmpV1.set( - (key[keyCode.D] - key[keyCode.A]) + (key[keyCode.RIGHT] - key[keyCode.LEFT]), - (key[keyCode.E] - key[keyCode.Q]), - (key[keyCode.W] - key[keyCode.S]) + (key[keyCode.UP] - key[keyCode.DOWN]) - )); - for (let i = 0; i < this._state.mouse.length; i++) { - this._state.mouse[i] += button[i]; - } - this._state.shift += key[keyCode.SHIFT]; - this._state.ctrl += key[keyCode.CTRL]; - this._state.touches += count[0]; - - if (button[0] === 1 || button[1] === 1 || wheel[0] !== 0) { - // left mouse button, middle mouse button, mouse wheel - this._setMode('orbit'); - } else if (button[2] === 1 || this._state.axis.length() > 0) { - // right mouse button or any movement - this._setMode('fly'); - } - - const orbit = +(this._mode === 'orbit'); - const fly = +(this._mode === 'fly'); - const double = +(this._state.touches > 1); - const pan = +this.enablePan && - ((orbit && this._state.shift) || this._state.mouse[1] || +(button[1] === -1)); - const mobileJoystick = +(this._flyMobileInput.layout.endsWith('joystick')); - - // multipliers - const moveMult = (this._state.shift ? this.moveFastSpeed : this._state.ctrl ? - this.moveSlowSpeed : this.moveSpeed) * this.sceneSize * dt; - const zoomMult = this.zoomSpeed * 60 * dt; - const zoomTouchMult = zoomMult * this.zoomPinchSens; - const rotateMult = this.rotateSpeed * 60 * dt; - const rotateJoystickMult = this.rotateSpeed * this.rotateJoystickSens * 60 * dt; - - const { deltas } = frame; - - // desktop move - const v = tmpV1.set(0, 0, 0); - const keyMove = this._state.axis.clone().normalize(); - v.add(keyMove.mulScalar(fly * moveMult)); - const panMove = screenToWorld(this._camera, mouse[0], mouse[1], this._pose.distance); - v.add(panMove.mulScalar(orbit * pan)); - const wheelMove = new Vec3(0, 0, wheel[0]); - v.add(wheelMove.mulScalar(orbit * zoomMult)); - deltas.move.append([v.x, v.y, v.z]); - - // desktop rotate - v.set(0, 0, 0); - const mouseRotate = new Vec3(mouse[0], mouse[1], 0); - v.add(mouseRotate.mulScalar((1 - pan) * rotateMult)); - deltas.rotate.append([v.x, v.y, v.z]); - - // mobile move - v.set(0, 0, 0); - const flyMove = new Vec3(leftInput[0], 0, -leftInput[1]); - v.add(flyMove.mulScalar(fly * moveMult)); - const orbitMove = screenToWorld(this._camera, touch[0], touch[1], this._pose.distance); - v.add(orbitMove.mulScalar(orbit * double)); - const pinchMove = new Vec3(0, 0, pinch[0]); - v.add(pinchMove.mulScalar(orbit * double * zoomTouchMult)); - deltas.move.append([v.x, v.y, v.z]); - - // mobile rotate - v.set(0, 0, 0); - const orbitRotate = new Vec3(touch[0], touch[1], 0); - v.add(orbitRotate.mulScalar(orbit * (1 - double) * rotateMult)); - const flyRotate = new Vec3(rightInput[0], rightInput[1], 0); - v.add(flyRotate.mulScalar(fly * (mobileJoystick ? rotateJoystickMult : rotateMult))); - deltas.rotate.append([v.x, v.y, v.z]); - - // gamepad move - v.set(0, 0, 0); - const stickMove = new Vec3(leftStick[0], 0, -leftStick[1]); - v.add(stickMove.mulScalar(fly * moveMult)); - deltas.move.append([v.x, v.y, v.z]); - - // gamepad rotate - v.set(0, 0, 0); - const stickRotate = new Vec3(rightStick[0], rightStick[1], 0); - v.add(stickRotate.mulScalar(fly * rotateJoystickMult)); - deltas.rotate.append([v.x, v.y, v.z]); - - // check for skip update, just read frame to clear it - if (this.skipUpdate) { - frame.read(); - return; - } +CameraControls.prototype._onMouseWheel = function (e) { + e.preventDefault(); + var zoomFactor = e.deltaY > 0 ? 1.1 : 0.9; + this._distance = Math.max(this._minDistance, Math.min(this._maxDistance, this._distance * zoomFactor)); + this._autoRotate = false; + this._lastMouseActivityTime = Date.now() / 1000; + this._updateCameraFromController(); +}; - // check if XR is active, just read frame to clear it - if (this.app.xr?.active) { - frame.read(); - return; - } +CameraControls.prototype._onTouchStart = function (e) { + e.preventDefault(); + if (e.touches.length === 2) { + this._isPinching = true; + var dx = e.touches[0].clientX - e.touches[1].clientX; + var dy = e.touches[0].clientY - e.touches[1].clientY; + this._lastTouchDistance = Math.sqrt(dx * dx + dy * dy); + this._autoRotate = false; + this._lastMouseActivityTime = Date.now() / 1000; + } else if (e.touches.length === 1 && !this._isPinching) { + this._isDragging = true; + this._lastX = e.touches[0].clientX; + this._lastY = e.touches[0].clientY; + this._autoRotate = false; + this._lastMouseActivityTime = Date.now() / 1000; + } +}; - // check focus end - if (this._mode === 'focus') { - const focusInterrupt = deltas.move.length() + deltas.rotate.length() > 0; - const focusComplete = this._focusController.complete(); - if (focusInterrupt || focusComplete) { - this._setMode('orbit'); - } +CameraControls.prototype._onTouchMove = function (e) { + e.preventDefault(); + if (e.touches.length === 2) { + this._isPinching = true; + var dx = e.touches[0].clientX - e.touches[1].clientX; + var dy = e.touches[0].clientY - e.touches[1].clientY; + var currentDistance = Math.sqrt(dx * dx + dy * dy); + if (this._lastTouchDistance > 0) { + var scaleFactor = currentDistance / this._lastTouchDistance; + this._distance = Math.max(this._minDistance, Math.min(this._maxDistance, this._distance / scaleFactor)); + this._autoRotate = false; + this._lastMouseActivityTime = Date.now() / 1000; + this._updateCameraFromController(); } + this._lastTouchDistance = currentDistance; + } else if (this._isDragging && e.touches.length === 1 && !this._isPinching) { + var currentX = e.touches[0].clientX; + var currentY = e.touches[0].clientY; + var deltaX = currentX - this._lastX; + var deltaY = currentY - this._lastY; + this._yaw -= deltaX * 0.15; + this._pitch += deltaY * 0.15; + this._pitch = Math.max(this._minPitch, Math.min(this._maxPitch, this._pitch)); + this._lastX = currentX; + this._lastY = currentY; + this._lastMouseActivityTime = Date.now() / 1000; + this._updateCameraFromController(); + } +}; - // update controller by consuming frame - this._pose.copy(this._controller.update(frame, dt)); - this._camera.entity.setPosition(this._pose.position); - this._camera.entity.setEulerAngles(this._pose.angles); +CameraControls.prototype._onTouchEnd = function () { + if (this._isDragging || this._isPinching) { + this._isDragging = false; + this._isPinching = false; + this._lastTouchDistance = 0; + this._lastMouseActivityTime = Date.now() / 1000; } -} +}; -export { CameraControls }; \ No newline at end of file +// 兼容默认导出(若被误作为模块导入,不影响资产加载) +try { module && (module.exports = CameraControls); } catch (e) { /* ignore in browser */ } diff --git a/custom-home.html b/custom-home.html index b61b584..f551bd3 100644 --- a/custom-home.html +++ b/custom-home.html @@ -18,11 +18,152 @@ min-height: 100vh; } - .empty-area { + .hero-section { text-align: center; - margin-bottom: 40px; - color: #666; - font-size: 18px; + margin: -40px -20px 40px -20px; + padding: 60px 20px; + background: linear-gradient(135deg, #e6f2ff 0%, #f0f8ff 40%, #ffffff 100%); + position: relative; + overflow: hidden; + } + + .hero-section::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: url('data:image/svg+xml,'); + opacity: 0.4; + } + + .hero-title { + font-size: 3.5rem; + font-weight: 700; + color: #2c3e50; + margin-bottom: 16px; + position: relative; + z-index: 2; + letter-spacing: -0.02em; + } + + .hero-subtitle { + font-size: 1.3rem; + color: #6c757d; + margin-bottom: 24px; + font-weight: 400; + position: relative; + z-index: 2; + } + + .hero-description { + font-size: 1rem; + color: #495057; + max-width: 500px; + margin: 0 auto 32px auto; + line-height: 1.6; + position: relative; + z-index: 2; + } + + .hero-features { + display: flex; + justify-content: center; + gap: 40px; + margin-top: 32px; + position: relative; + z-index: 2; + } + + .feature-item { + text-align: center; + padding: 20px; + background: rgba(255, 255, 255, 0.4); + border-radius: 8px; + transition: transform 0.3s ease, background 0.3s ease; + } + + .feature-item:hover { + transform: translateY(-2px); + background: rgba(255, 255, 255, 0.6); + } + + .feature-icon { + font-size: 2rem; + margin-bottom: 8px; + display: block; + } + + .feature-text { + font-size: 0.9rem; + color: #495057; + font-weight: 500; + } + + .hero-divider { + width: 60px; + height: 2px; + background: linear-gradient(90deg, #4a90e2, #7bb3f0); + margin: 24px auto; + border-radius: 1px; + position: relative; + z-index: 2; + } + + /* 响应式设计 */ + @media (max-width: 768px) { + .hero-section { + margin: -40px -15px 40px -15px; + padding: 40px 15px; + } + + .hero-title { + font-size: 2.5rem; + } + + .hero-subtitle { + font-size: 1.1rem; + } + + .hero-description { + font-size: 0.95rem; + padding: 0 20px; + } + + .hero-features { + gap: 20px; + flex-wrap: wrap; + } + + .feature-item { + padding: 15px; + flex: 1; + min-width: 120px; + } + + .feature-icon { + font-size: 1.5rem; + } + } + + @media (max-width: 480px) { + .hero-title { + font-size: 2rem; + } + + .hero-features { + gap: 15px; + } + + .feature-item { + padding: 12px; + min-width: 100px; + } + + .feature-text { + font-size: 0.8rem; + } } .year { @@ -102,21 +243,7 @@ background: linear-gradient(to top, rgba(0,0,0,0.6), transparent); } - .card:nth-child(1) .card-top { background-image: url('https://picsum.photos/id/1/400/300'); } - .card:nth-child(2) .card-top { background-image: url('https://picsum.photos/id/20/400/300'); } - .card:nth-child(3) .card-top { background-image: url('https://picsum.photos/id/42/400/300'); } - .card:nth-child(4) .card-top { background-image: url('https://picsum.photos/id/65/400/300'); } - .card:nth-child(5) .card-top { background-image: url('https://picsum.photos/id/91/400/300'); } - .card:nth-child(6) .card-top { background-image: url('https://picsum.photos/id/119/400/300'); } - .card:nth-child(7) .card-top { background-image: url('https://picsum.photos/id/152/400/300'); } - .card:nth-child(8) .card-top { background-image: url('https://picsum.photos/id/180/400/300'); } - .card:nth-child(9) .card-top { background-image: url('https://picsum.photos/id/210/400/300'); } - .card:nth-child(10) .card-top { background-image: url('https://picsum.photos/id/237/400/300'); } - .card:nth-child(11) .card-top { background-image: url('https://picsum.photos/id/260/400/300'); } - .card:nth-child(12) .card-top { background-image: url('https://picsum.photos/id/287/400/300'); } - .card:nth-child(13) .card-top { background-image: url('https://picsum.photos/id/312/400/300'); } - .card:nth-child(14) .card-top { background-image: url('https://picsum.photos/id/335/400/300'); } - .card:nth-child(15) .card-top { background-image: url('https://picsum.photos/id/350/400/300'); } + /* 背景图片现在通过JavaScript动态设置,不再需要硬编码的CSS规则 */ .card-bottom { background-color: white; @@ -182,266 +309,129 @@
-+ 汇聚厦工学子创意与智慧的作品展示空间, + 展现年轻设计师的创新思维与精湛技艺。 +
+ +