THREE.JS镜头随鼠标晃动效果

请添加图片描述
为了让动画更灵活并且简单 借助gsap让其具有更多可能,在未来更容易扩充其他动效
gsap Dom跟随鼠标移动 gsap.quickTo()


首先要监听鼠标移动,并且将移动的值转换到 -1 和 1 之间 方便处理
 private mousemove(e: MouseEvent) {const x = (e.clientX / innerWidth);const y = (e.clientY / innerHeight);}

上面将 位置 / 屏幕宽高 将值缩放在 0 和 1之间
然后通过 乘2减1 将其限制在-1 和 1之间

 private mousemove(e: MouseEvent) {const x = (e.clientX / innerWidth) * 2 - 1;const y = (e.clientY / innerHeight) * 2 - 1;}

在three中y轴 上面是1下面是-1 而我们窗口上面是-1 下面是1 所以取y轴剩余的高度让两者行为统一,也就是实现向着鼠标方向

 private mousemove(e: MouseEvent) {const x = (e.clientX / innerWidth) * 2 - 1) ;const y = ((innerHeight - e.clientY) / innerHeight) * 2 - 1;}

如此 我们获取了鼠标移动的增量,将这一向量加在camera的x和y轴即可,这里不能使用+=这样会越来越偏 所以保存相机的原始位置。那么当前鼠标所在相机的位置,应当是原始位置加上鼠标移动的增量
这里就可以使用gsap来控制position变化

private xQuickTo = gsap.quickTo(this.camera.position, "x", {duration: 0.5,
});

上述代码 this.camera.position.x 经过0.5秒后变化到传入值,如:this.xQuickTo(this.cameraPosition.x + x)

class Shake {cameraPosition = this.camera.position.clone();private xQuickTo = gsap.quickTo(this.camera.position, "x", {duration: 0.5,});private yQuickTo = gsap.quickTo(this.camera.position, "y", {duration: 0.5,});private mousemove(e: MouseEvent) {const x = ((e.clientX / innerWidth) * 2 - 1) / this.amplitude;const y =(((innerHeight - e.clientY) / innerHeight) * 2 - 1) /this.amplitude;this.xQuickTo(this.cameraPosition.x + x).play();this.yQuickTo(this.cameraPosition.y + y).play();}}

如此 核心逻辑便完成,丰富事件监听并且加入振幅,控制相机移动范围 后完成这个class

export class Shake {/** 振幅 鼠标晃动的影响 */amplitude = 1;cameraPosition = this.camera.position.clone();private xQuickTo = gsap.quickTo(this.camera.position, "x", {duration: 0.5,});private yQuickTo = gsap.quickTo(this.camera.position, "y", {duration: 0.5,});constructor(public camera: THREE.Camera, public domElement: HTMLElement) {this.domElement.addEventListener("mousemove", this.selfMouseMove);}private mousemove(e: MouseEvent) {const x = ((e.clientX / innerWidth) * 2 - 1) / this.amplitude;const y =(((innerHeight - e.clientY) / innerHeight) * 2 - 1) /this.amplitude;this.xQuickTo(this.cameraPosition.x + x)this.yQuickTo(this.cameraPosition.y + y)}private selfMouseMove = (e: MouseEvent) => this.mousemove.call(this, e);destroyMouseMove() {this.domElement.removeEventListener("mousemove", this.selfMouseMove);}
}

接下来可以扩充一下功能, 加入鼠标按下时可以拖拽旋转,鼠标松开后回到相机在当前鼠标位置位置时的位置
为了支持这些功能 需要给shake增加暂停动画的能力 以及mousemove时记录当前鼠标对相机的位置影响,松开鼠标,相机回去时直接回到mousemove中的位置


export class Shake {pause = false;/** 振幅 鼠标晃动的影响 */amplitude = 1;cameraPosition = this.camera.position.clone();xQuickTo = gsap.quickTo(this.camera.position, "x", {duration: 0.5,});yQuickTo = gsap.quickTo(this.camera.position, "y", {duration: 0.5,});private yQuickToTween: gsap.core.Tween | undefined;private xQuickToTween: gsap.core.Tween | undefined;point = new Vector2();constructor(public camera: THREE.Camera, public domElement: HTMLElement) {this.domElement.addEventListener("mousemove", this.selfMouseMove);}private mousemove(e: MouseEvent) {const x = ((e.clientX / innerWidth) * 2 - 1) / this.amplitude;const y =(((innerHeight - e.clientY) / innerHeight) * 2 - 1) /this.amplitude;this.point.set(x, y);if (this.pause) {this.xQuickToTween && this.xQuickToTween.pause();this.yQuickToTween && this.yQuickToTween.pause();}else {this.xQuickToTween = this.xQuickTo(this.cameraPosition.x + x);this.yQuickToTween = this.yQuickTo(this.cameraPosition.y + y);}}
}
export class CameraShake extends Shake {constructor(...params: ConstructorParameters<typeof Shake>) {super(...params);this.domElement.addEventListener("mousedown", this.selfMouseDown);}mousedown() {if (this.pause) return;this.pause = true;document.addEventListener("mouseup", this.selfMouseUp, { once: true });}mouseup() {const { x, y, z } = this.cameraPosition;gsap.to(this.camera.position, {x: x + this.point.x,y: y + this.point.y,z,duration: 0.8,onComplete: () => {this.pause = false;},});}selfMouseDown = () => this.mousedown.call(this);selfMouseUp = () => this.mouseup.call(this);destroyMouseEvent() {this.destroyMouseMove();this.domElement.removeEventListener("mousedown", this.selfMouseDown);}
}

截至目前还有一个问题 ,鼠标松开后第一次移动鼠标 动画会闪一下,他的运动路线实际是鼠标按下的位置到当前鼠标移动的位置,因为mousemove中的quickTo被暂停了 mousemove开始动画是从上次播放的位置开始计算的,因此我们需要在mouseup后的mousemove传入quickTo第二哥参数 start 也就是从哪开始计算,这是松开鼠标后第一次 mousemove ,其他时候就可以穿入undefined让其自行计算。

 mouseMove(e: MouseEvent) {// -1  ~  1const x = (e.clientX / innerWidth) * 2 - 1;const y = ((innerHeight - e.clientY) / innerHeight) * 2 - 1;if (this.pause) {this.xQuickToTween && this.xQuickToTween.pause();this.yQuickToTween && this.yQuickToTween.pause();this.point.set(x, y);} else {this.xQuickToTween = this.xQuickTo(this.cameraPosition.x + x,Number.isNaN(this.point.x)? undefined: this.cameraPosition.x + this.point.x);this.yQuickToTween = this.yQuickTo(this.cameraPosition.y + y,Number.isNaN(this.point.y)? undefined: this.cameraPosition.y + this.point.y);this.point.set(NaN, NaN);}}

完整代码:

import { Vector2, Vector3 } from "three";
import { gsap } from "gsap";export class Shake {pause = false;/** 振幅 鼠标晃动的影响 */amplitude = 1;cameraPosition = this.camera.position.clone();xQuickTo = gsap.quickTo(this.camera.position, "x", {duration: 0.5,// ease: "bounce",});yQuickTo = gsap.quickTo(this.camera.position, "y", {duration: 0.5,});xQuickToTween?: gsap.core.Tween;yQuickToTween?: gsap.core.Tween;point = new Vector2();constructor(public camera: THREE.Camera, public dom: HTMLElement) {dom.addEventListener("mousemove", this.selfMouseMove);}selfMouseMove = (e: MouseEvent) => this.mouseMove.call(this, e);destroyMouseMove = () => {this.dom.addEventListener("mousemove", this.selfMouseMove);};mouseMove(e: MouseEvent) {// -1  ~  1const x = ((e.clientX / innerWidth) * 2 - 1) * this.amplitude;const y = (((innerHeight - e.clientY) / innerHeight) * 2 - 1) * this.amplitude;if (this.pause) {this.xQuickToTween && this.xQuickToTween.pause();this.yQuickToTween && this.yQuickToTween.pause();this.point.set(x, y);} else {this.xQuickToTween = this.xQuickTo(this.cameraPosition.x + x,Number.isNaN(this.point.x)? undefined: this.cameraPosition.x + this.point.x);this.yQuickToTween = this.yQuickTo(this.cameraPosition.y + y,Number.isNaN(this.point.y)? undefined: this.cameraPosition.y + this.point.y);this.point.set(NaN, NaN);}}
}export class CameraShake extends Shake {constructor(...params: ConstructorParameters<typeof Shake>) {super(...params);this.dom.addEventListener("mousedown", this.selfMouseDown);}mouseDown = () => {this.pause = true;this.dom.addEventListener("mouseup", this.selfMouseUp, {once: true,});};selfMouseDown = () => this.mouseDown.call(this);mouseUp = () => {//鼠标按下又抬起未移动 point是NaN不能参与计算 也不需要计算if (Number.isNaN(this.point.x)) return (this.pause = false);gsap.to(this.camera.position, {x: this.cameraPosition.x + this.point.x,y: this.cameraPosition.y + this.point.y,z: this.cameraPosition.z,duration: 0.5,onComplete: () => {this.pause = false;},});};selfMouseUp = () => this.mouseUp.call(this);destroyMouseEvent = () => {this.destroyMouseMove();this.dom.removeEventListener("mousedown", this.selfMouseDown);};
}

gitee仓库:
CameraShake.ts

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1077.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

华为配置LLDP基本功能

华为配置LLDP基本功能 1.什么是lldp协议 定义 LLDP(Link Layer Discovery Protocol)是IEEE 802.1ab中定义的链路层发现协议。LLDP是一种标准的二层发现方式,可以将本端设备的管理地址、设备标识、接口标识等信息组织起来,并发布给自己的邻居设备,邻居设备收到这些信息后将…

SSH远程直连Docker容器

文章目录 1. 下载docker镜像2. 安装ssh服务3. 本地局域网测试4. 安装cpolar5. 配置公网访问地址6. SSH公网远程连接测试7.固定连接公网地址8. SSH固定地址连接测试8. SSH固定地址连接测试 转载自cpolar极点云文章&#xff1a;SSH远程直连Docker容器 在某些特殊需求下,我们想ssh…

45、Spring Boot自动配置原理

Spring Boot自动配置原理 lmport Configuration Spring spi 自动配置类由各个starter提供&#xff0c;使用Configuration Bean定义配置类&#xff0c;放到META-INF/spring.factories下使用Spring spi扫描META-INF/spring.factories下的配置类使用lmport导入自动配置类

[游戏开发][Unity] TPS射击游戏相机实现

技术难点&#xff1a;由于是第三人称射击游戏&#xff0c;角色和相机之间有夹角&#xff0c;所以枪口点和准星是有误差的&#xff0c;下面是和平精英手游截图&#xff0c;我用AK射击zhuzi using System.Collections; using System.Collections.Generic; using UnityEngine;publ…

❤️创意网页:如何创建一个漂亮的3D正六边形

✨博主&#xff1a;命运之光 &#x1f338;专栏&#xff1a;Python星辰秘典 &#x1f433;专栏&#xff1a;web开发&#xff08;简单好用又好看&#xff09; ❤️专栏&#xff1a;Java经典程序设计 ☀️博主的其他文章&#xff1a;点击进入博主的主页 前言&#xff1a;欢迎踏入…

上手vue2的学习笔记5之在vue2项目中调用elment-ui

前言 上手vue2的学习笔记4之搭建vue环境 参考链接&#xff1a;vue2.0项目引入element-ui 一、安装elment-ui 进入搭建的vue项目中 cd vue_bing_test 安装 element npm i element-ui二、引入elment-ui elment官方教程 将main.js改成如下内容&#xff1a; import Vue fro…

我们正在开发一套组件库,欢迎你的加入~

项目地址 github地址 可以先点进来康康~ 技术栈 目前我们整体采用的是vue3typescriptless作为整体的开发的选择 需要说的是&#xff0c;我们并没有采用很多组件库采用的TSX的写法&#xff0c;而是选择了SFC的写法&#xff0c;这是因为我们觉得对于大部分的vue开发者来说&am…

MySQL八股学习记录4事务的实现from小林coding

MySQL八股学习记录4事务的实现from小林coding 事务的概念与特性并行事务引发的问题脏读不可重复读幻读 MySQL的应对策略InnoDB引擎可重复读详解ReadView在MVCC中的工作方式两种隔离级别通过MVCC实现幻读被完全解决了吗 事务的概念与特性 概念:一个操作要么执行成功,要么回滚到…

ORACLE实时SQL监控视图

引言 实时的SQL监控&#xff08;Real Time SQL Monitoring&#xff09;是Oracle 11g的一个新特性&#xff0c;它是一项强大的工具&#xff0c;用于监视和分析正在执行的SQL语句的性能和执行计划。该功能允许我们实时地跟踪SQL查询的执行过程&#xff0c;以及了解其资源消耗、等…

Java的五种数据类型解析

Java的五种数据类型解析 不知道大家对java的简单数据类型是否了解&#xff0c;下面针对Java的五种类型简单数据类型表示数字和字符&#xff0c;进行详细的讲解和分析。 一、简单数据类型初始化 在Java语言中&#xff0c;简单数据类型作为类的成员变量声明时自动初始化为默认值…

Ext JS中定义和使用类Ext JS风格的html 按钮

Ext JS 的按钮样式如下: 按钮的背景色默认为应用的主色调或是灰色系, 也可以通过样式设置按钮的背景色, 详细可以参考: Ext JS 如何设置工具栏按钮和一般按钮保持统一样式 但是, 在实际开发的场景中有可能某些按钮是不能通过Ext JS的Button 类进行创建的, 但是页面效果…

如何快速入门「组织结构图」?

组织结构图&#xff0c;对于公司老板和HR可谓是家常便饭&#xff0c;公司发展好&#xff0c;组织结构要升级和调整&#xff0c;人员“正常”流动&#xff0c;都需要用到它。组织结构图可以让人快速的了解公司的行政结构和权利体系&#xff0c;也是让新员工或客户快速了解公司相…

JAVA+Selenium最简单的处理登录弹窗的方式

在做自动化测试遇到需要处理登录弹窗情况&#xff0c;例如我的用户名为admin, 密码为admin, 那么想要登录http://10.10.168.1, 只需要使用以下链接访问即可立即登录, 并免除弹窗: http://账号:密码网址

composer的劈坑

现在是php8盛行的天下&#xff0c;安装php8我就不多说了&#xff0c;宝塔、小出面板一大堆&#xff0c;一键安装。真心说方便。&#xff08;好吧&#xff0c;不打广告了&#xff09;&#xff0c;以下是针对 linux 系统 1、安装composer 安装composer之前&#xff0c;需要要先在…

Unity VR 开发教程 OpenXR+XR Interaction Toolkit(八)手指触控 Poke Interaction

文章目录 &#x1f4d5;教程说明&#x1f4d5;XR Poke Interactor&#x1f4d5;与 UI 进行触控交互⭐添加 Tracked Device Graphic Raycaster 和 XR UI Input Module 让 UI 可被交互 &#x1f4d5;与物体进行交互⭐XR Simple Interactable⭐XR Poke Filter 往期回顾&#xff1a…

简单工厂模式详解

文章目录 前言一、简单工厂模式定义二、举个例子三、简单工厂模式的缺点总结 前言 本篇我们了解一下简单工厂模式&#xff0c;它是设计模式的雏形&#xff0c;是学习设计模式的开端&#xff0c;我会结合案例说明它的设计思路。 一、简单工厂模式定义 简单工厂模式并不是GoF23…

看一眼Mysql查询语句

目录 &#x1f6fb; 查询数据 &#x1f6fb;基本查询语句 &#x1f6fb;单表查询 &#x1f695;查询所有字段 &#x1f695;查询指定字段 &#x1f695;查询指定记录 &#x1f695;带in关键字的查询 &#x1f695;带between and的范围查询 &#x1f695;带like的字符匹…

10个在线ai文案自动生成器,轻松写出高质量原创文案

在数字营销时代&#xff0c;撰写引人注目的标题和吸引人的营销文案至关重要。然而&#xff0c;写作优质文案需要耗费时间和精力。幸运的是&#xff0c;现在有许多在线AI文案自动生成器可以帮助我们快速创作出高质量的标题和营销文案。本文将介绍十款值得推荐的在线AI文案自动生…

微信小程序事件点击跳转页面的五种方法

第一种:标签 这是最常见的一种跳转方式,相当于html里的a标签 <navigator url"/pages/main/main"></navigator>第二种:wx.navigateTo({})方法 1.前端wxml <button bindtap"getCeshi" type"primary"> 测试按钮 </button>…

flink水位线传播及任务事件时间

背景 本文来讲解一下flink的水位线传播及对其对任务事件时间的影响 水位线 首先flink是通过从源头生成水位线记录的方式来实现水位线传播的&#xff0c;也就是说水位线是嵌入在正常的记录流中的特殊记录&#xff0c;携带者水位线的时间戳&#xff0c;以下我们就通过图片的方…