animation.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637
  1. import {
  2. AnimationModel,
  3. AnimationModelAction,
  4. AnimationModelFrame,
  5. AnimationModelPath,
  6. AnimationModelSubtitle,
  7. } from "@/api";
  8. import sdk, {
  9. AnimationGroup,
  10. AnimationModel3D,
  11. AnimationModelAction3D,
  12. AnimationModelFrame3D,
  13. AnimationModelPath3D,
  14. SDK,
  15. sdk as _sdk,
  16. } from "../sdk";
  17. import {
  18. computed,
  19. nextTick,
  20. reactive,
  21. ref,
  22. toRaw,
  23. watch,
  24. watchEffect,
  25. } from "vue";
  26. import { ams } from "@/store/animation";
  27. import { mergeFuns, uuid } from "@/components/drawing/hook";
  28. import { getPathNode } from "./path";
  29. import { debounce, diffArrayChange, mount, round } from "@/utils";
  30. import { Pos } from "@/utils/event";
  31. import Subtitle from "@/components/subtitle/index.vue";
  32. import { Size } from "@/components/drawing/dec";
  33. import router, { RoutesName } from "@/router";
  34. import { isEdit, isTemploraryID, paths } from "@/store";
  35. import { Color } from "three";
  36. import { custom, showAMsStack } from "@/env";
  37. export let animationGroup: AnimationGroup;
  38. export const getAMKey = (am: AnimationModel) => am.key || am.id;
  39. export const amMap: Record<
  40. string,
  41. {
  42. am?: AnimationModel3D;
  43. globalFrame?: AnimationModelFrame3D;
  44. frames: Record<string, AnimationModelFrame3D>;
  45. actions: Record<string, AnimationModelAction3D>;
  46. paths: Record<string, AnimationModelPath3D>;
  47. subtitles: Record<string, () => void>;
  48. }
  49. > = reactive({});
  50. export const addAM = (data: AnimationModel): Promise<AnimationModel3D> => {
  51. const key = getAMKey(data);
  52. const stopLoad = watch(
  53. () => {
  54. const exixts = ams.value.some((am) => getAMKey(am) === key);
  55. return [key, exixts] as const;
  56. },
  57. ([key, exixts]) => {
  58. if (!exixts) {
  59. const des = amMap[key];
  60. if (!des) return;
  61. Object.values(des.frames || {}).forEach((frame) => frame.destroy());
  62. Object.values(des.actions || {}).forEach((frame) => frame.destroy());
  63. Object.values(des.paths || {}).forEach((frame) => frame.destroy());
  64. des.am?.destroy();
  65. console.error("destory", key, data);
  66. delete amMap[key];
  67. } else if (!amMap[key]) {
  68. amMap[key] = {
  69. frames: {},
  70. actions: {},
  71. paths: {},
  72. subtitles: {},
  73. };
  74. const am = animationGroup.addAnimationModel({
  75. ...data,
  76. quaAtPath: data.mat?.quaAtPath,
  77. });
  78. am.bus.on("loadDone", () => {
  79. amMap[key].am = am;
  80. if (isTemploraryID(data.id)) {
  81. console.error('putInFrontOfCam', data.id)
  82. am.putInFrontOfCam();
  83. }
  84. console.log("0.0", am);
  85. });
  86. }
  87. },
  88. { immediate: true }
  89. );
  90. const stopAttrib = mergeFuns(
  91. watchEffect(() =>
  92. amMap[key]?.am?.changeVisibilityRange(
  93. data.globalVisibility ? undefined : data.visibilityRange
  94. )
  95. ),
  96. watchEffect(() => amMap[key]?.am?.changeTitle(data.title)),
  97. watchEffect(() => amMap[key]?.am?.visibilityTitle(data.showTitle)),
  98. watchEffect(() => amMap[key]?.am?.changeFontSize(data.fontSize))
  99. );
  100. const stopWatch = watch(
  101. () => ams.value.includes(data),
  102. (exists) => {
  103. if (!exists) {
  104. stopLoad();
  105. stopAttrib();
  106. stopWatch();
  107. }
  108. },
  109. { flush: "post" }
  110. );
  111. return new Promise((resolve) => {
  112. const stopWatch = watchEffect(() => {
  113. if (amMap[key]?.am) {
  114. resolve(amMap[key]!.am!);
  115. nextTick(() => stopWatch());
  116. }
  117. });
  118. });
  119. };
  120. export const addFrame = (
  121. data: AnimationModelFrame
  122. ): Promise<AnimationModelFrame3D> => {
  123. console.log("addFrame");
  124. const am = ams.value.find((item) =>
  125. item.frames.find(({ id }) => id === data.id)
  126. );
  127. if (!am) {
  128. throw "找不到am数据";
  129. }
  130. const key = getAMKey(am);
  131. const stopLoad = watch(
  132. () => {
  133. const exists = am.frames.some(({ id }) => id === data.id);
  134. amMap[key]?.am;
  135. return [amMap[key], exists] as const;
  136. },
  137. ([map, exists]) => {
  138. if (!map?.am) return;
  139. if (exists && !map.frames[data.id]) {
  140. map.frames[data.id] = map.am.addFrame(data);
  141. } else if (!exists && map.frames[data.id]) {
  142. map.frames[data.id].destroy();
  143. delete map.frames[data.id];
  144. }
  145. },
  146. { immediate: true }
  147. );
  148. const stopAttrib = mergeFuns(
  149. watchEffect(() => amMap[key]?.frames[data.id]?.changeTime(data.time)),
  150. watchEffect(() => data.mat && amMap[key]?.frames[data.id]?.setMat(data.mat))
  151. );
  152. const stopWatch = watch(
  153. () => am.frames.includes(data),
  154. (exists) => {
  155. if (!exists) {
  156. stopLoad();
  157. stopAttrib();
  158. stopWatch();
  159. }
  160. },
  161. { flush: "post" }
  162. );
  163. return new Promise((resolve) => {
  164. const stopWatch = watchEffect(() => {
  165. if (amMap[key]?.frames[data.id]) {
  166. resolve(amMap[key].frames[data.id]);
  167. nextTick(() => stopWatch());
  168. }
  169. });
  170. });
  171. };
  172. export const addAction = (
  173. data: AnimationModelAction
  174. ): Promise<AnimationModelAction3D> => {
  175. const am = ams.value.find((item) =>
  176. item.actions.find(({ id }) => id === data.id)
  177. );
  178. if (!am) {
  179. throw "找不到am数据";
  180. }
  181. const key = getAMKey(am);
  182. const stopLoad = watch(
  183. () => {
  184. const exists = am.actions.some(({ id }) => id === data.id);
  185. amMap[key]?.am;
  186. return [amMap[key], exists] as const;
  187. },
  188. ([map, exists]) => {
  189. if (!map?.am) return;
  190. if (exists && !map.actions[data.id]) {
  191. map.actions[data.id] = map.am.addAction(data);
  192. } else if (!exists && map.actions[data.id]) {
  193. map.actions[data.id].destroy();
  194. delete map.actions[data.id];
  195. }
  196. },
  197. { immediate: true }
  198. );
  199. const stopAttrib = mergeFuns(
  200. watchEffect(() => amMap[key]?.actions[data.id]?.changeTime(data.time)),
  201. watchEffect(() => {
  202. amMap[key]?.actions[data.id]?.changeAmplitude(data.amplitude);
  203. }),
  204. watchEffect(() => amMap[key]?.actions[data.id]?.changeSpeed(data.speed)),
  205. watchEffect(() =>
  206. amMap[key]?.actions[data.id]?.changeDuration(data.duration)
  207. )
  208. );
  209. const stopWatch = watch(
  210. () => am.actions.includes(data),
  211. (exists) => {
  212. if (!exists) {
  213. stopLoad();
  214. stopAttrib();
  215. stopWatch();
  216. }
  217. },
  218. { flush: "post" }
  219. );
  220. return new Promise((resolve) => {
  221. const stopWatch = watchEffect(() => {
  222. if (amMap[key]?.actions[data.id]) {
  223. resolve(amMap[key].actions[data.id]);
  224. nextTick(() => stopWatch());
  225. }
  226. });
  227. });
  228. };
  229. export const addPath = (
  230. data: AnimationModelPath
  231. ): Promise<AnimationModelPath3D> => {
  232. const am = ams.value.find((item) =>
  233. item.paths.find(({ id }) => id === data.id)
  234. );
  235. if (!am) {
  236. throw "找不到am数据";
  237. }
  238. const path = computed(() =>
  239. data.pathId ? getPathNode(data.pathId) : undefined
  240. );
  241. const pathData = computed(() =>
  242. paths.value.find((item) => item.id === data.pathId)
  243. );
  244. const key = getAMKey(am);
  245. const stopLoad = watch(
  246. () => {
  247. const exists = am.paths.some(({ id }) => id === data.id);
  248. amMap[key]?.am;
  249. return [amMap[key], exists, path.value] as const;
  250. },
  251. ([map, exists, path]) => {
  252. if (!map?.am || !path) return;
  253. if (exists && !map.paths[data.id]) {
  254. map.paths[data.id] = map.am.addPath({ ...data, path });
  255. } else if (!exists && map.paths[data.id]) {
  256. map.paths[data.id].destroy();
  257. delete map.paths[data.id];
  258. }
  259. },
  260. { immediate: true }
  261. );
  262. const stopAttrib = mergeFuns(
  263. watchEffect(() => amMap[key]?.paths[data.id]?.changeTime(data.time)),
  264. watchEffect(() => amMap[key]?.paths[data.id]?.changeReverse(data.reverse)),
  265. watchEffect(() =>
  266. amMap[key]?.paths[data.id]?.changeDuration(data.duration)
  267. ),
  268. watch(
  269. path,
  270. (p) => {
  271. const path = toRaw(p);
  272. path && amMap[key]?.paths[data.id]?.changePath(path);
  273. },
  274. { immediate: true }
  275. )
  276. );
  277. const stopWatch = watch(
  278. () => am.paths.includes(data),
  279. (exists) => {
  280. if (!exists) {
  281. stopLoad();
  282. stopAttrib();
  283. stopWatch();
  284. }
  285. },
  286. { flush: "post" }
  287. );
  288. return new Promise((resolve) => {
  289. const stopWatch = watchEffect(() => {
  290. if (amMap[key]?.paths[data.id]) {
  291. resolve(amMap[key].paths[data.id]);
  292. nextTick(() => stopWatch());
  293. }
  294. });
  295. });
  296. };
  297. export const addSubtitle = (data: AnimationModelSubtitle) => {
  298. const am = ams.value.find((item) =>
  299. item.subtitles.find(({ id }) => id === data.id)
  300. );
  301. if (!am) {
  302. throw "找不到am数据";
  303. }
  304. const key = getAMKey(am);
  305. const size = ref({ width: 0, height: 0 });
  306. const show = ref(false);
  307. const pixel = ref<Pos>();
  308. const modelShow = ref(true);
  309. const textShow = ref(true);
  310. const stopLoad = watch(
  311. () => {
  312. const exists = am.subtitles.some(({ id }) => id === data.id);
  313. amMap[key]?.am;
  314. return [amMap[key], exists] as const;
  315. },
  316. ([map, exists], _, onCleanup) => {
  317. if (!map?.am) return;
  318. if (exists && !map.subtitles[data.id]) {
  319. const rawChangeShow = map?.am.changeShow;
  320. map.am.changeShow = (f: boolean) => {
  321. modelShow.value = f;
  322. return rawChangeShow(f);
  323. };
  324. onCleanup(() => (map.am!.changeShow = rawChangeShow));
  325. const mountEl = document.querySelector("#app")!;
  326. const layer = document.createElement("div");
  327. layer.className = "subtitle";
  328. mountEl.appendChild(layer);
  329. const cleanups = [
  330. watchEffect(() => {
  331. layer.innerHTML = data.content;
  332. textShow.value = !!layer.textContent?.trim()?.length;
  333. size.value = {
  334. width: layer.offsetWidth,
  335. height: layer.offsetHeight,
  336. };
  337. }),
  338. watchEffect(() => {
  339. const theme = new Color();
  340. theme.setHex(parseInt(data.background.substring(1), 16));
  341. const rgb = { r: 0, g: 0, b: 0 };
  342. theme.getRGB(rgb);
  343. layer.style.backgroundColor = `rgba(${round(
  344. rgb.r * 255,
  345. 2
  346. )},${round(rgb.g * 255, 2)},${round(rgb.b * 255, 2)},0.5)`;
  347. }),
  348. watchEffect(() => {
  349. // console.log(pixel.value, show.value, modelShow.value, textShow.value)
  350. layer.style.visibility =
  351. pixel.value && show.value && modelShow.value && textShow.value
  352. ? "visible"
  353. : "hidden";
  354. }),
  355. watchEffect(() => {
  356. if (pixel.value) {
  357. layer.style.transform = `translate(${round(
  358. pixel.value.x,
  359. 0
  360. )}px, calc(${round(pixel.value.y, 0)}px - 50%))`;
  361. }
  362. }),
  363. () => {
  364. console.error("rm dom", data.id);
  365. layer.parentNode?.removeChild(layer);
  366. },
  367. ];
  368. // map.subtitles[data.id] = mergeFuns(cleanups);
  369. onCleanup(() => {
  370. mergeFuns(cleanups)();
  371. delete map.subtitles[data.id];
  372. });
  373. } else if (!exists && map.subtitles[data.id]) {
  374. map.subtitles[data.id]();
  375. delete map.subtitles[data.id];
  376. }
  377. },
  378. { immediate: true }
  379. );
  380. let isRun = false;
  381. const update = () => {
  382. // if (isRun) return;
  383. // isRun = true
  384. // setTimeout(() => {
  385. pixel.value = amMap[getAMKey(am)]?.am?.getCurrentSubtitlePixel(size.value);
  386. // isRun = false
  387. // }, 16);
  388. };
  389. const stopAttrib = mergeFuns(
  390. watch(
  391. [
  392. () => router.currentRoute.value.name,
  393. currentTime,
  394. () => amMap[getAMKey(am)]?.am,
  395. () => data.time,
  396. () => data.duration,
  397. size,
  398. isEdit,
  399. play,
  400. () => amMap[key]?.am,
  401. ],
  402. (_a, _b, onCleanup) => {
  403. if (
  404. !play.value &&
  405. router.currentRoute.value.name !== RoutesName.animation &&
  406. !(router.currentRoute.value.name === RoutesName.guide && isEdit.value)
  407. ) {
  408. show.value = false;
  409. } else if (
  410. currentTime.value >= data.time &&
  411. currentTime.value < data.time + data.duration
  412. ) {
  413. update();
  414. show.value = true;
  415. onCleanup(
  416. mergeFuns(
  417. watch(
  418. play,
  419. (play, _a, onCleanup) => {
  420. if (!play && _sdk) {
  421. _sdk.sceneBus.on("cameraChange", update);
  422. onCleanup(() => _sdk.sceneBus.off("cameraChange", update));
  423. }
  424. },
  425. { immediate: true }
  426. ),
  427. watch(
  428. () => amMap[getAMKey(am)]?.am,
  429. (am, _, onCleanup) => {
  430. am && am.bus.on("transformChanged", update);
  431. onCleanup(() => am && am.bus.off("transformChanged", update))
  432. },
  433. { immediate: true }
  434. )
  435. )
  436. );
  437. } else {
  438. show.value = false;
  439. }
  440. },
  441. { immediate: true }
  442. )
  443. );
  444. const stopWatch = watch(
  445. () => am.subtitles.includes(data),
  446. (exists) => {
  447. if (!exists) {
  448. stopLoad();
  449. stopAttrib();
  450. stopWatch();
  451. }
  452. },
  453. { flush: "post" }
  454. );
  455. };
  456. export const endTime = computed(() => {
  457. const amsEndTime = ams.value.map((am) => {
  458. const endPoints = [
  459. ...am.frames.map((item) => ({ ...item, duration: 0 })),
  460. ...am.actions,
  461. ...am.subtitles,
  462. ...am.paths,
  463. ].map((item) => item.time + (item.duration || 0));
  464. return Math.max(...endPoints);
  465. });
  466. return (
  467. Math.max(...amsEndTime) +
  468. ((animationGroup.delayEndTime && animationGroup.delayEndTime()) || 0)
  469. );
  470. });
  471. export const play = ref(false);
  472. watch(play, (_a, _b, onCleanup) => {
  473. play.value ? animationGroup?.play() : animationGroup?.pause();
  474. if (!play.value) return;
  475. if (currentTime.value >= endTime.value) {
  476. currentTime.value = 0;
  477. }
  478. onCleanup(
  479. watchEffect(() => {
  480. if (currentTime.value >= endTime.value) {
  481. play.value = false;
  482. }
  483. })
  484. );
  485. });
  486. export const currentTime = ref(0);
  487. export const associationAnimation = (sdk: SDK, el: HTMLDivElement) => {
  488. animationGroup = sdk.createAnimationGroup();
  489. watchEffect(
  490. () => {
  491. animationGroup.setCurrentTime(currentTime.value);
  492. },
  493. { flush: "sync" }
  494. );
  495. animationGroup.bus.on("currentTime", (time) => {
  496. currentTime.value = time;
  497. });
  498. watch(
  499. () => [...ams.value],
  500. (newv, oldv = []) => {
  501. const { added, deleted } = diffArrayChange(newv, oldv);
  502. console.log("diffam", added, deleted, ams.value);
  503. added.forEach(addAM);
  504. },
  505. { immediate: true, deep: true }
  506. );
  507. watch(
  508. () => ams.value.flatMap((am) => am.frames),
  509. (newv, oldv = []) => {
  510. const { added } = diffArrayChange(newv, oldv);
  511. added.forEach(addFrame);
  512. },
  513. { immediate: true, deep: true }
  514. );
  515. watch(
  516. () => ams.value.flatMap((am) => am.actions),
  517. (newv, oldv = []) => {
  518. const { added } = diffArrayChange(newv, oldv);
  519. added.forEach(addAction);
  520. },
  521. { immediate: true, deep: true }
  522. );
  523. watch(
  524. () => ams.value.flatMap((am) => am.paths),
  525. (newv, oldv = []) => {
  526. const { added } = diffArrayChange(newv, oldv);
  527. added.forEach(addPath);
  528. },
  529. { immediate: true, deep: true }
  530. );
  531. watch(
  532. () => ams.value.flatMap((am) => am.subtitles),
  533. (newv, oldv = []) => {
  534. const { added } = diffArrayChange(newv, oldv);
  535. added.forEach(addSubtitle);
  536. },
  537. { immediate: true, deep: true }
  538. );
  539. let cleanupMap: Record<string, () => void> = {};
  540. watch(
  541. () => {
  542. // !am.frames.length &&
  543. const gAms = ams.value.filter((am) => amMap[getAMKey(am)]?.am);
  544. return gAms;
  545. },
  546. (am3ds, oldAm3ds = []) => {
  547. const { added, deleted } = diffArrayChange(am3ds, oldAm3ds);
  548. for (const am of added) {
  549. const am3d = amMap[getAMKey(am)];
  550. if (!am3d || !am3d.am) continue;
  551. const getMat = () => {
  552. return am.mat && "position" in am.mat
  553. ? am.mat
  554. : am3d.am!.getModelPose();
  555. };
  556. am3d.am.setDefaultPose && am3d.am.setDefaultPose(getMat());
  557. const mat = am.mat || am3d.am.getModelPose();
  558. am3d.am.changePosition(mat.position!);
  559. am3d.am.changeRotation(mat.rotation!);
  560. am3d.am.changeScale(mat.scale!);
  561. cleanupMap[getAMKey(am)] = mergeFuns(
  562. watchEffect(() => {
  563. if (am.mat && am3d.am) {
  564. am3d.am.setDefaultPose && am3d.am.setDefaultPose(getMat());
  565. }
  566. console.log("set-default-pose", am.mat);
  567. }),
  568. () => {
  569. am3d.am?.addFrame;
  570. am3d.globalFrame = undefined;
  571. delete cleanupMap[getAMKey(am)];
  572. }
  573. );
  574. }
  575. for (const am of deleted) {
  576. cleanupMap[getAMKey(am)] && cleanupMap[getAMKey(am)]();
  577. }
  578. },
  579. { flush: "post", immediate: true }
  580. );
  581. watch(
  582. () =>
  583. [Object.values(amMap).map((item) => item.am), custom.showAMs] as const,
  584. ([ams, show]) => {
  585. console.error(ams, "show", show);
  586. ams.forEach((am) => {
  587. am?.changeShow(show);
  588. });
  589. }
  590. );
  591. };
  592. showAMsStack.push(play);