Browse Source

feat: 添加漫游方式

bill 1 month ago
parent
commit
35da50b33d

+ 119 - 100
src/core/hook/use-animation.ts

@@ -1,149 +1,168 @@
-import { onUnmounted, ref, Ref, watch } from "vue"
-import { Tween, Easing } from '@tweenjs/tween.js'
-import { inRevise, startAnimation } from "@/utils/shared.ts"
-import { Color, RGB } from "three"
-import { DC } from "@/deconstruction"
-import { Shape } from "konva/lib/Shape"
+import { onUnmounted, ref, Ref, watch } from "vue";
+import { Tween, Easing } from "@tweenjs/tween.js";
+import { inRevise, startAnimation } from "@/utils/shared.ts";
+import { Color, RGB } from "three";
+import { DC } from "@/deconstruction";
+import { Shape } from "konva/lib/Shape";
 
-export const animation = <T extends object>(origin: T, target: T, update?: (data: T) => void) => {
-  let isStop = false
-  const stop = () => {
-    tw.stop()
-    isStop = true
-  }
+export const animation = <T extends object>(
+  origin: T,
+  target: T,
+  update?: (data: T) => void,
+  time = 300
+) => {
+  let stop: (() => void) & {promise: Promise<void>};
+  const promise = new Promise<void>((resolve, reject) => {
+    let isStop = false;
+    let isComp = false
+    stop = (() => {
+      tw.stop();
+      isStop = true;
+      isComp || reject('动画中断')
+    }) as any;
 
-  const tw = new Tween(origin)
-  .to(target, 300)
-  .easing(easing)
-  .start()
-  .onComplete(() => {
-    stop()
-  })
-  if (update) {
-    tw.onUpdate(() => {
-      update && update(origin)
-    })
-  }
+    const tw = new Tween(origin)
+      .to(target, time)
+      .easing(easing)
+      .start()
+      .onComplete(() => {
+        resolve()
+        isComp = true
+        stop();
+      });
+    if (update) {
+      tw.onUpdate(() => {
+        update && update(origin);
+      });
+    }
 
-  const start = () => {
-    requestAnimationFrame(() => {
-      tw.update()
-      isStop || start()
-    })
-  }
-  start()
-  return stop
-}
+    const start = () => {
+      requestAnimationFrame(() => {
+        tw.update();
+        isStop || start();
+      });
+    };
+    start();
+  });
+  stop!.promise = promise
+  return stop!;
+};
 
-const pickColors = <T extends object>(origin: T): Record<string, RGB>  => {
-  const originColors = {} as any
+const pickColors = <T extends object>(origin: T): Record<string, RGB> => {
+  const originColors = {} as any;
   for (const [key, val] of Object.entries(origin)) {
-    if (typeof val === 'string') {
-      if (val.startsWith('#') || val.startsWith('rgb')) {
-        originColors[key] = new Color(val).getRGB({} as any)
-      } 
+    if (typeof val === "string") {
+      if (val.startsWith("#") || val.startsWith("rgb")) {
+        originColors[key] = new Color(val).getRGB({} as any);
+      }
     }
   }
-  return originColors
-}
-const resumeColors = <T extends object>(origin: T, colors: Record<string, RGB>) => {
+  return originColors;
+};
+const resumeColors = <T extends object>(
+  origin: T,
+  colors: Record<string, RGB>
+) => {
   for (const [key, val] of Object.entries(colors)) {
-    (origin as any)[key] = `#${new Color(val.r, val.g, val.b).getHexString()}`
+    (origin as any)[key] = `#${new Color(val.r, val.g, val.b).getHexString()}`;
   }
-}
+};
 
-const easing = Easing.Quadratic.InOut
-const animationProperty = <T extends object>(origin: T, target: T, update?: (data: T) => void) => {
-  let isStop = false
+const easing = Easing.Quadratic.InOut;
+const animationProperty = <T extends object>(
+  origin: T,
+  target: T,
+  update?: (data: T) => void
+) => {
+  let isStop = false;
   const stop = () => {
-    numTw.stop()
-    colorTw.stop()
-    isStop = true
-  }
+    numTw.stop();
+    colorTw.stop();
+    isStop = true;
+  };
   const oColors = pickColors(origin);
-  const tColors = pickColors(target)
-  const tOrigin = {...origin}
-  const tTarget = {...target}
+  const tColors = pickColors(target);
+  const tOrigin = { ...origin };
+  const tTarget = { ...target };
 
   for (const key in oColors) {
     if (!(key in tColors)) {
-      ;(origin as any)[key] = null
-      delete oColors[key]
+      (origin as any)[key] = null;
+      delete oColors[key];
     }
   }
 
   for (const key in tOrigin) {
-    if (typeof tOrigin[key] === 'string') {
-      delete tOrigin[key]
+    if (typeof tOrigin[key] === "string") {
+      delete tOrigin[key];
     }
   }
   for (const key in tTarget) {
-    if (typeof tTarget[key] === 'string') {
-      delete tTarget[key]
+    if (typeof tTarget[key] === "string") {
+      delete tTarget[key];
     }
   }
 
-  let count = 0
+  let count = 0;
   const tw = (origin: any, target: any) => {
-    ++count
+    ++count;
     return new Tween(origin)
       .to(target, 300)
       .easing(easing)
       .start()
       .onComplete(() => {
-        if (--count === 0) stop()
-      })
-  }
+        if (--count === 0) stop();
+      });
+  };
+
+  const numTw = tw(tOrigin, tTarget).onUpdate(() => {
+    Object.assign(origin, tOrigin);
+    update && update(origin);
+  });
 
-  const numTw = tw(tOrigin, tTarget)
-    .onUpdate(() => {
-      Object.assign(origin, tOrigin)
-      update && update(origin)
-    })
-  
-  const colorTw = tw(oColors, tColors)
-    .onUpdate(() => {
-      resumeColors(origin, oColors)
-      update && update(origin)
-    })
+  const colorTw = tw(oColors, tColors).onUpdate(() => {
+    resumeColors(origin, oColors);
+    update && update(origin);
+  });
 
   const start = () => {
     requestAnimationFrame(() => {
-      numTw.update()
-      colorTw.update()
-      isStop || start()
-    })
-  }
-  start()
-  return stop
-}
+      numTw.update();
+      colorTw.update();
+      isStop || start();
+    });
+  };
+  start();
+  return stop;
+};
 
 export const useAniamtion = <T extends object>(data: Ref<T>) => {
-  const atData = ref(JSON.parse(JSON.stringify(data.value))) as Ref<T>
-  let animationStop: () => void
-  let isPause = false
+  const atData = ref(JSON.parse(JSON.stringify(data.value))) as Ref<T>;
+  let animationStop: () => void;
+  let isPause = false;
   watch(data, (newData) => {
     if (!inRevise(newData, atData.value)) return;
-    animationStop! && animationStop()
+    animationStop! && animationStop();
     if (isPause) {
-      atData.value = newData
+      atData.value = newData;
     } else {
-      animationStop = animationProperty(atData.value, newData)
+      animationStop = animationProperty(atData.value, newData);
     }
-  })
+  });
 
   onUnmounted(() => {
-    animationStop && animationStop()
-  })
-  return [
-    atData, () => isPause = true, () => isPause = false
-  ] as const
-}
+    animationStop && animationStop();
+  });
+  return [atData, () => (isPause = true), () => (isPause = false)] as const;
+};
 
-type DA = DC<Shape> | undefined
+type DA = DC<Shape> | undefined;
 export const useDashAnimation = (shapes: Ref<DA | DA[]>) => {
   watch(
-    () => (Array.isArray(shapes.value) ? shapes.value : [shapes.value]).map((i) => i),
+    () =>
+      (Array.isArray(shapes.value) ? shapes.value : [shapes.value]).map(
+        (i) => i
+      ),
     (shapes, _, onCleanup) => {
       for (const shape of shapes) {
         if (!shape) continue;
@@ -157,4 +176,4 @@ export const useDashAnimation = (shapes: Ref<DA | DA[]>) => {
       }
     }
   );
-}
+};

+ 5 - 2
src/core/hook/use-global-vars.ts

@@ -277,7 +277,8 @@ export const usePointerIntersection = installGlobalVar(() => {
     },
   };
 });
-export const useDownKeys = installGlobalVar(() => {
+
+export const getDownKeys = () => {
   const keyKeys = reactive(new Set<string>());
   const mouseKeys = reactive(new Set<string>());
   const evHandler = (ev: KeyboardEvent | MouseEvent, keys: Set<string>) => {
@@ -315,7 +316,9 @@ export const useDownKeys = installGlobalVar(() => {
     var: keys,
     onDestroy: cleanup,
   };
-});
+}
+
+export const useDownKeys = installGlobalVar(getDownKeys);
 
 export const useLayers = () => {
   const stage = useStage();

+ 84 - 0
src/core/renderer-three/hook/use-animation.ts

@@ -0,0 +1,84 @@
+import { animation } from "@/core/hook/use-animation";
+import { Matrix4, Quaternion, Vector3 } from "three";
+import { installThreeGlobalVar, useCamera, useRender } from "./use-stage";
+import { Tween, Easing } from "@tweenjs/tween.js";
+import { mergeFuns } from "@/utils/shared";
+
+const easing = Easing.Quadratic.InOut;
+export const quatAnimation = (
+  startQuat: Quaternion,
+  endQuat: Quaternion,
+  update?: (data: Quaternion) => void,
+  time = 300
+) => {
+  let stop: (() => void) & { promise: Promise<void> };
+  const promise = new Promise<void>((resolve, reject) => {
+    let isStop = false;
+    let isComp = false;
+    stop = (() => {
+      tw.stop();
+      isStop = true;
+      isComp || reject("动画中断");
+    }) as any;
+
+    const tw = new Tween({ t: 0 })
+      .to({ t: 1 }, time)
+      .easing(easing)
+      .start()
+      .onComplete(() => {
+        resolve();
+        isComp = true;
+        stop();
+      });
+    if (update) {
+      tw.onUpdate((data) => {
+        startQuat.slerp(endQuat, data.t);
+        update && update(startQuat);
+      });
+    }
+
+    const start = () => {
+      requestAnimationFrame(() => {
+        tw.update();
+        isStop || start();
+      });
+    };
+    start();
+  });
+  stop!.promise = promise;
+  return stop!;
+};
+
+export const useCameraAnimation = installThreeGlobalVar(() => {
+  const camera = useCamera();
+  const _render = useRender();
+  const render = () => {
+    _render();
+    camera.bus.emit("change");
+  };
+
+  let prevStop: (() => void) | null = null;
+  return (position: Vector3, target: Vector3, time = 800) => {
+    if (prevStop) {
+      prevStop();
+      prevStop = null;
+    }
+
+    const startPosition = camera.position;
+    const startQuat = camera.quaternion;
+
+    const endQuat = new Quaternion().setFromRotationMatrix(
+      new Matrix4().lookAt(position, target, camera.up)
+    );
+
+    const stopQuat = quatAnimation(startQuat, endQuat, () => {}, time);
+    const stopPosition = animation(startPosition, position, render, time);
+    const promise = Promise.all([stopQuat.promise, stopPosition.promise]);
+
+    prevStop = mergeFuns(stopQuat, stopPosition);
+    promise.finally(() => {
+      prevStop = null;
+    });
+    return promise;
+  };
+});

+ 66 - 41
src/core/renderer-three/hook/use-controls.ts

@@ -1,4 +1,4 @@
-import { ref, watch } from "vue";
+import { ref, watch, watchEffect } from "vue";
 import {
   installThreeGlobalVar,
   useCamera,
@@ -8,9 +8,12 @@ import {
 import { OrbitControls } from "three/examples/jsm/Addons.js";
 import { listener } from "@/utils/event";
 import { PerspectiveCamera, Vector3 } from "three";
-import { mergeFuns } from "@/utils/shared";
+import { frameEebounce, frameInterval, mergeFuns } from "@/utils/shared";
 import { useMouseEventRegister } from "./use-event";
 import { subgroupName } from "../container";
+import { useCameraAnimation } from "./use-animation";
+import { getDownKeys } from "@/core/hook/use-global-vars";
+import { getMoveDirectrionByKeys } from "./use-downkeys";
 
 const getModelControls = (
   container: HTMLDivElement,
@@ -42,19 +45,17 @@ const getModelControls = (
     enable() {
       controls.enabled = true;
     },
-    recover() {
-      if (prevOrigin && prevDire) {
-        camera.position.copy(prevOrigin);
-        camera.lookAt(prevOrigin.add(prevDire));
-        controls.update();
-        render();
-      }
+    get current() {
+      return {
+        prevOrigin,
+        prevDire,
+      };
     },
   };
 };
 
 const getRoamingControls = (
-container: HTMLDivElement,
+  container: HTMLDivElement,
   camera: PerspectiveCamera,
   render: () => void
 ) => {
@@ -73,15 +74,41 @@ container: HTMLDivElement,
   let prevDire: Vector3 | null = null;
 
   const unListener = listener(controls as any, "change", render);
+  const { direction, onDestory: onDownDestory } = getMoveDirectrionByKeys();
+  const stopWatchDirection = watch(direction, (dire, _, onCleanup) => {
+    if (!controls.enabled || !dire) return;
+    onCleanup(frameInterval(() => {
+      const cameraDire = camera.getWorldDirection(new Vector3())
+      cameraDire.y = 0
+      cameraDire.normalize()
+
+      const rightDire = new Vector3(0, 1, 0).cross(cameraDire)
+      const moveDire = new Vector3()
+      if (dire.y) {
+        moveDire.addScaledVector(cameraDire, dire.y)
+      }
+
+      if (dire.x) {
+        moveDire.addScaledVector(rightDire, -dire.x)
+      }
+
+      if (moveDire.length() > 0) {
+        camera.position.addScaledVector(moveDire.normalize(), 2)
+        syncCamera();
+        render();
+      }
+    }));
+  });
 
   return {
     controls,
     onDestory() {
       controls.dispose();
       unListener();
+      onDownDestory();
+      stopWatchDirection();
     },
     syncCamera,
-
     disable() {
       prevOrigin = camera.position.clone();
       prevDire = camera.getWorldDirection(new Vector3());
@@ -91,15 +118,11 @@ container: HTMLDivElement,
       controls.enabled = true;
       render();
     },
-    recover() {
-      if (prevOrigin && prevDire) {
-        camera.position.copy(prevOrigin);
-        camera.lookAt(prevOrigin.add(prevDire));
-        prevOrigin = prevDire = null;
-
-        syncCamera();
-        render();
-      }
+    get current() {
+      return {
+        prevOrigin,
+        prevDire,
+      };
     },
   };
 };
@@ -164,53 +187,55 @@ export const useControls = installThreeGlobalVar(() => {
 export const useFlyRoaming = installThreeGlobalVar(() => {
   const camera = useCamera();
   const { type, value: controls } = useControls();
+  const cameraAnimation = useCameraAnimation();
 
-  return (point: Vector3) => {
+  return async (point: Vector3) => {
     type.value = undefined;
     const position = point.clone().add({ x: 0, y: 100, z: 0 });
     const direction = camera.getWorldDirection(new Vector3());
     const target = position.clone().add(direction);
-    camera.position.copy(position);
-    camera.lookAt(target);
-
+    await cameraAnimation(position, target);
     type.value = "roaming";
     controls.value!.syncCamera();
   };
-})
+});
 
 export const useFlyModel = installThreeGlobalVar(() => {
   const camera = useCamera();
   const { type, value: controls } = useControls();
+  const cameraAnimation = useCameraAnimation();
 
-  return (set?: {point: Vector3, direction: Vector3}) => {
+  return async (set: { point?: Vector3; direction?: Vector3 } = {}) => {
     type.value = "model";
-    if (set) {
-      const position = set.point.clone().add({ x: 0, y: 100, z: 0 });
-      const direction = set.direction
-      const target = position.clone().add(direction);
-      camera.position.copy(position);
-      camera.lookAt(target);
-      controls.value?.syncCamera()
-    } else {
-      controls.value!.recover();
+    const prev = controls.value!.current;
+    if (!set.point) {
+      set.point = prev.prevOrigin || camera.position.clone();
+    }
+    if (!set.direction) {
+      set.direction = prev.prevDire || camera.getWorldDirection(new Vector3());
     }
-  }
-})
+
+    const direction = set.direction;
+    const target = set.point.clone().add(direction);
+    await cameraAnimation(set.point, target);
+    controls.value?.syncCamera();
+  };
+});
 
 export const installAutoSwitchControls = installThreeGlobalVar(() => {
   const { type } = useControls();
   const mouseRegister = useMouseEventRegister();
-  const flyRoaming = useFlyRoaming()
-  const flyModel = useFlyModel()
+  const flyRoaming = useFlyRoaming();
+  const flyModel = useFlyModel();
 
   type.value = "model";
   const onDestroy = mergeFuns(
     listener(document.documentElement, "keydown", (ev) => {
       if (ev.key === "Escape" && type.value !== "model") {
-        flyModel()
+        flyModel();
       }
     }),
-    mouseRegister(subgroupName, "dblclick", ({ point }) => flyRoaming(point)),
+    mouseRegister(subgroupName, "dblclick", ({ point }) => flyRoaming(point))
   );
 
   return { onDestroy };

+ 36 - 0
src/core/renderer-three/hook/use-downkeys.ts

@@ -0,0 +1,36 @@
+import { getDownKeys } from "@/core/hook/use-global-vars";
+import { mergeFuns } from "@/utils/shared";
+import { Vector3 } from "three";
+import { shallowRef, watch } from "vue";
+
+
+export const getMoveDirectrionByKeys = () => {
+  const { var: keys, onDestroy: onDownDestory } = getDownKeys();
+  const direction = shallowRef<Vector3>()
+
+  const stopWatch = watch(keys, (keys) => {
+    let dire = new Vector3()
+    if (keys.has('a')) {
+      dire.setX(-1)
+    }
+    if (keys.has('d')) {
+      dire.setX(1)
+    }
+    if (keys.has('w')) {
+      dire.setY(1)
+    }
+    if (keys.has('s')) {
+      dire.setY(-1)
+    }
+    if (dire.x || dire.y) {
+      direction.value = dire.normalize()
+    } else {
+      direction.value = undefined
+    }
+  }, { deep: true });
+
+  return {
+    direction,
+    onDestory: mergeFuns(stopWatch, onDownDestory)
+  }
+}

+ 1 - 1
src/core/renderer-three/hook/use-stage.ts

@@ -142,7 +142,7 @@ export const useRender = installThreeGlobalVar(() => {
   const render = frameEebounce(() => {
     loop.trigger();
     renderer.render(scene, camera);
-  });
+  }, true);
 
   renderer.bus.on("sizeChange", render);
   return render;

+ 18 - 2
src/utils/shared.ts

@@ -136,18 +136,34 @@ export const debounce = <T extends (...args: any) => any>(
 };
 
 // 防抖
-export const frameEebounce = <T extends (...args: any) => any>(fn: T) => {
+export const frameEebounce = <T extends (...args: any) => any>(fn: T, enforce = false) => {
   let count = 0;
   return function (...args: Parameters<T>) {
     let current = ++count;
     requestAnimationFrame(() => {
-      if (current === count) {
+      if (enforce || current === count) {
         fn.apply(null, args);
       }
     });
   };
 };
 
+export const frameInterval = (fn: () => void) => {
+  let isStop = false
+  const animation = () => {
+    requestAnimationFrame(() => {
+      fn()
+      if (!isStop) {
+        animation()
+      }
+    })
+  }
+  animation()
+  return () => {
+    isStop = true
+  }
+}
+
 /**
  * 获取数据变化
  * @param newIds