/** * @file CameraFX.ts * @description 大招镜头聚焦工具(表现层) * * 项目内场景 Camera 从未启用(MapViewScene.camera 可能为 null), * 与 ScreenShake 同理改为操作地图根节点 smc.map.MapView.node: * 以施法者位置为锚点做 scale + 位移 tween,实现"画面瞬间拉近再回弹"的镜头突进感, * UI 层(oops.gui 挂在独立根节点)天然不受影响。 * * 与 ScreenShake 的区别:本工具是"有明确锚点的缩放聚焦", * ScreenShake 是"无锚点的随机抖动",两者可叠加(大招命中瞬间聚焦回弹 + 震屏)。 * * 用法: * CameraFX.focusOn(heroView.node, 1.2, 0.5); */ import { Node, tween, Tween, v3, Vec3, view } from "cc"; import { smc } from "./SingletonModuleComp"; export class CameraFX { /** 根节点基准状态(首次聚焦时快照:位置 + 缩放,用于回弹复位,防多次触发漂移) */ private static basePos: Vec3 | null = null; private static baseScale: Vec3 | null = null; /** * 镜头聚焦:画面瞬间向锚点拉近,随后回弹复位 * @param anchor 聚焦锚点节点(通常是施法者节点),以其世界坐标为画面中心 * @param zoom 拉近倍率(建议 1.15~1.25,过大容易出戏) * @param holdTime 聚焦保持到回弹完成的总时长(秒,建议 0.4~0.6) */ static focusOn(anchor: Node | null | undefined, zoom: number = 1.2, holdTime: number = 0.5) { const root = smc.map?.MapView?.node; if (!root || !root.isValid || !anchor || !anchor.isValid) return; if (!this.basePos) { this.basePos = root.position.clone(); this.baseScale = root.scale.clone(); } // 打断进行中的聚焦(新大招覆盖旧大招,避免多英雄连续放大时 tween 叠加失控) Tween.stopAllByTarget(root); // 锚点世界坐标 -> 根节点本地坐标,作为缩放后的画面中心 const worldPos = anchor.worldPosition; const localPos = v3(); root.inverseTransformPoint(localPos, worldPos); const designSize = view.getDesignResolutionSize(); const focusScale = this.baseScale.x * zoom; // 缩放会围绕根节点原点(0,0)放大画面,需反向平移让锚点保持在屏幕中心附近: // 目标位置 = 基准位置 - 锚点本地坐标 * (zoom - 1) * 基准缩放 // 同时按设计分辨率的一半作为屏幕中心基准,避免锚点在屏幕边缘时拉出黑边 const offsetX = localPos.x * (zoom - 1) * this.baseScale.x; const offsetY = localPos.y * (zoom - 1) * this.baseScale.y; const clampX = designSize.width * 0.25; // 平移量限幅:最多偏移 1/4 屏,防边缘英雄把画面拉飞 const clampY = designSize.height * 0.25; const targetPos = v3( this.basePos.x - Math.max(-clampX, Math.min(clampX, offsetX)), this.basePos.y - Math.max(-clampY, Math.min(clampY, offsetY)), this.basePos.z ); const focusScaleV3 = v3(focusScale, focusScale, this.baseScale.z); // 三段式节奏:快速突进(0.12s) -> 短暂停留 -> 慢速回弹(holdTime) tween(root) .to(0.12, { position: targetPos, scale: focusScaleV3 }, { easing: "sineOut" }) .delay(Math.max(0.05, holdTime * 0.3)) .to(holdTime, { position: this.basePos, scale: this.baseScale }, { easing: "sineInOut" }) .start(); } /** * 复位镜头状态(关卡切换/战斗结束时调用,防止状态残留) */ static reset() { const root = smc.map?.MapView?.node; if (root && root.isValid && this.basePos && this.baseScale) { Tween.stopAllByTarget(root); root.setPosition(this.basePos); root.setScale(this.baseScale); } this.basePos = null; this.baseScale = null; } }