LongImage.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  1. <template>
  2. <div
  3. class="long-image"
  4. @mousedown="onMouseDown"
  5. @mousemove="onMouseMove"
  6. @mouseup="onMouseUp"
  7. @mouseleave="onMouseLeave"
  8. @touchstart.passive="onTouchStart"
  9. @touchmove.prevent="onTouchMove"
  10. @touchend="onTouchEnd"
  11. @touchcancel="onTouchCancel"
  12. @wheel.passive="onWheel"
  13. >
  14. <audio
  15. ref="bgAudio$"
  16. loop
  17. autoplay
  18. :src="bgAudio"
  19. @play="bgAudioStatus = true"
  20. @pause="bgAudioStatus = false"
  21. />
  22. <div ref="longref$">
  23. <component
  24. class="time-item"
  25. v-for="(timeItem, index) in timeList"
  26. :key="timeItem.id"
  27. :is="timeItem.component"
  28. :info="{ ...timeItem.info, id: timeItem.id }"
  29. :style="{
  30. left: `calc(${itemW}${$isMobile ? 'vw' : 'vh'} * ${index} - ${translateLength}px)`,
  31. width: `${itemW}${$isMobile ? 'vw' : 'vh'}`,
  32. maxWidth: `${itemW}${isMobile ? 'vw' : 'vh'}`,
  33. }"
  34. @onClickTimeItem="onClickTimeItem"
  35. />
  36. </div>
  37. <Interaction ref="interaction$" :currentTimeIdx="currentTimeIdx" :list="timeList" />
  38. <Vmenu
  39. :currentTimeIdx="currentTimeIdx"
  40. @onClickMenuItem="onClickMenuItem"
  41. @onClickTimeItem="onClickTimeItem"
  42. :list="timeList"
  43. :bgAudioStatus="bgAudioStatus"
  44. />
  45. <teleport to="body">
  46. <Transition>
  47. <div v-if="isLongImageVideo" class="fade-in-video-wrap">
  48. <button class="skip-button" v-if="isShowSkip" @click="onSkipClick">
  49. <img :src="skipImg" alt="" draggable="false">
  50. </button>
  51. <button
  52. class="bofang-button"
  53. @click="onVideoCanPlayThrough"
  54. v-if="isNeedToBofang"
  55. >
  56. <img :src="bofangImg" alt="" draggable="false">
  57. </button>
  58. <video
  59. ref="video$"
  60. muted
  61. autoplay
  62. :poster="videoPostImg"
  63. class="initial-video"
  64. playsinline="true"
  65. x5-playsinline="true"
  66. webkit-playsinline="true"
  67. :src="`${config.cdnDir}videos/video2.mp4`"
  68. @playing="isNeedToBofang = false"
  69. @ended="onFadeInVideoEnd"
  70. @mousedown.passive.stop
  71. @touchstart.passive.stop
  72. @canplaythrough="onVideoCanPlayThrough"
  73. @wheel.passive.stop
  74. />
  75. </div>
  76. </Transition>
  77. <Transition>
  78. <div v-if="isShowDir">
  79. <Directory @close="isShowDir = false" />
  80. </div>
  81. </Transition>
  82. <Transition>
  83. <div class="guide" v-if="isShowGuide">
  84. <img :src="guideImg" alt="" draggable="false">
  85. <div class="tips">
  86. <p>滚动鼠标滚轮,浏览更多内容</p>
  87. <img :src="mouseImg" alt="" draggable="false">
  88. <div @click="onCloseGuide">
  89. <img :src="comfirImg" alt="" draggable="false">
  90. </div>
  91. </div>
  92. </div>
  93. </Transition>
  94. <div v-if="store.currentHotspot">
  95. <Hotspot @close="store.currentHotspot = ''" />
  96. </div>
  97. </teleport>
  98. </div>
  99. </template>
  100. <script setup>
  101. import { ref, getCurrentInstance, watch, computed, onMounted } from "vue"
  102. import appStore from "@/store/index";
  103. import timeList from "@/data/index";
  104. import Vmenu from "@/components/menu.vue"
  105. import Directory from "@/components/directory.vue"
  106. import Hotspot from "@/components/Hotspot.vue"
  107. import Interaction from "@/components/Interaction.vue"
  108. import { useRouter } from "vue-router"
  109. const router = useRouter()
  110. const store = appStore();
  111. const isMouseDown = ref(false);
  112. const lastMoveEventTimeStamp = ref(0);
  113. const moveSpeed = ref(0);
  114. const lastTouchPos = ref(0);
  115. const maxTranslateLength = ref(0);
  116. const guideImg = utils.getImageUrl(`guide.jpg`)
  117. const mouseImg = utils.getImageUrl(`mouse.png`)
  118. const comfirImg = utils.getImageUrl(`btn_concern.png`)
  119. const skipImg = utils.getImageUrl(`skip.png`)
  120. const bofangImg = utils.getImageUrl(`bofang.png`)
  121. const longref$ = ref(null)
  122. const video$ = ref(null)
  123. const interaction$ = ref(null)
  124. // 背景音乐相关
  125. const bgAudio = utils.getAudioUrl('bg.mp3')
  126. const bgAudio$ = ref(null)
  127. onMounted(() => {
  128. bgAudio$.value.volume = 0.2
  129. })
  130. const bgAudioStatus = ref(false)
  131. function switchBgAudio() {
  132. if (bgAudio$.value.paused) {
  133. bgAudio$.value.play()
  134. } else {
  135. bgAudio$.value.pause()
  136. }
  137. }
  138. // 过渡视频相关
  139. const videoPostImg = utils.getImageUrl(`videobg.jpg`)
  140. const isLongImageVideo = ref(true)
  141. const isNeedToBofang = ref(true)
  142. const isShowSkip = ref(false)
  143. const onVideoCanPlayThrough = () => {
  144. if (video$.value) {
  145. video$.value.play()
  146. isNeedToBofang.value = false
  147. }
  148. }
  149. const onSkipClick = () => {
  150. isShowGuide.value = true
  151. setTimeout(() => {
  152. isLongImageVideo.value = false
  153. }, 100);
  154. }
  155. function onFadeInVideoEnd() {
  156. isShowGuide.value = true
  157. setTimeout(() => {
  158. isLongImageVideo.value = false
  159. }, 100);
  160. }
  161. // 动画帧相关
  162. const lastAnimationTimeStamp = ref(0);
  163. const animationFrameId = ref(0);
  164. // 镜头平移相关
  165. const translateLength = ref(0);
  166. const currentTimeIdx = ref(0);
  167. const instance = getCurrentInstance()
  168. const globalProperties = instance.appContext.app.config.globalProperties
  169. const itemW = computed(() => 280)
  170. const isShowDir = ref(false)
  171. const isShowGuide = ref(false)
  172. let firstIn = true
  173. const onCloseGuide = () => {
  174. isShowGuide.value = false
  175. if (firstIn) {
  176. firstIn = false
  177. interaction$.value.handleShow()
  178. store.canPlayLongImageBgAudio = true
  179. }
  180. }
  181. const animationFrameTask = () => {
  182. const timeStamp = Date.now()
  183. const timeElapsed = timeStamp - lastAnimationTimeStamp.value
  184. // 速度减慢
  185. if (moveSpeed.value > 0) {
  186. moveSpeed.value -= (globalProperties.$isMobile ? 0.001 : 0.003) * timeElapsed
  187. if (moveSpeed.value < 0) {
  188. moveSpeed.value = 0
  189. }
  190. } else if (moveSpeed.value < 0) {
  191. moveSpeed.value += (globalProperties.$isMobile ? 0.001 : 0.003) * timeElapsed
  192. if (moveSpeed.value > 0) {
  193. moveSpeed.value = 0
  194. }
  195. }
  196. // 根据速度更新距离
  197. if (store.canMoveCamera) {
  198. translateLength.value += moveSpeed.value * timeElapsed
  199. if (translateLength.value < 0) {
  200. translateLength.value = 0
  201. } else if (translateLength.value > maxTranslateLength.value) {
  202. translateLength.value = maxTranslateLength.value
  203. moveSpeed.value = 0
  204. }
  205. }
  206. lastAnimationTimeStamp.value = timeStamp
  207. animationFrameId.value = requestAnimationFrame(animationFrameTask)
  208. }
  209. const onClickTimeItem = (index) => {
  210. translateLength.value = longref$.value.children[0].offsetWidth * index
  211. console.log('result:', longref$.value.children[0].offsetWidth * index);
  212. }
  213. const onClickMenuItem = (item) => {
  214. if (item.id === 'bgAudio') {
  215. switchBgAudio()
  216. } else if (item.id === 'search') {
  217. isShowDir.value = true
  218. } else if (item.id === 'tip') {
  219. isShowGuide.value = true
  220. } else if (item.id === 'home') {
  221. router.push({ path: '/' })
  222. }
  223. }
  224. const calcTranslateLimit = () => {
  225. maxTranslateLength.value = longref$.value.children[0].offsetWidth * (timeList.length - 1)
  226. }
  227. const onMouseDown = () => {
  228. isMouseDown.value = true
  229. moveSpeed.value = 0
  230. lastMoveEventTimeStamp.value = 0
  231. lastAnimationTimeStamp.value = Date.now()
  232. }
  233. const onMouseMove = (e) => {
  234. if (isMouseDown.value) {
  235. // 有些pc端浏览器比如firefox会有两次事件时间戳相同的情况发生。
  236. if (lastMoveEventTimeStamp.value && (e.timeStamp - lastMoveEventTimeStamp.value > 1)) {
  237. // 更新speed
  238. const currentMoveSpeed = - e.movementX / (e.timeStamp - lastMoveEventTimeStamp.value)
  239. moveSpeed.value = moveSpeed.value * 0.9 + currentMoveSpeed * 0.1
  240. }
  241. lastMoveEventTimeStamp.value = e.timeStamp
  242. }
  243. }
  244. const onMouseUp = () => {
  245. isMouseDown.value = false
  246. }
  247. const onMouseLeave = () => {
  248. isMouseDown.value = false
  249. }
  250. const onTouchStart = (e) => {
  251. isMouseDown.value = true
  252. moveSpeed.value = 0
  253. lastMoveEventTimeStamp.value = 0
  254. lastAnimationTimeStamp.value = Date.now()
  255. lastTouchPos.value = (globalProperties.$isRotate ? e.changedTouches[0].clientY : e.changedTouches[0].clientX)
  256. }
  257. const onTouchMove = (e) => {
  258. if (isMouseDown.value && e.changedTouches.length === 1) {
  259. // 疯狂操作的极端情况下两个时间戳之间的时差会不合理,甚至为0
  260. if (lastMoveEventTimeStamp.value && (e.timeStamp - lastMoveEventTimeStamp.value > 1)) {
  261. // 更新speed
  262. const currentMoveSpeed = - ((globalProperties.$isRotate ? e.changedTouches[0].clientY : e.changedTouches[0].clientX) - lastTouchPos.value) / (e.timeStamp - lastMoveEventTimeStamp.value) * (globalProperties.$isFirefox ? 2.2 : 1.5)
  263. moveSpeed.value = moveSpeed.value * 0.9 + currentMoveSpeed * 0.1
  264. lastTouchPos.value = globalProperties.$isRotate ? e.changedTouches[0].clientY : e.changedTouches[0].clientX
  265. }
  266. lastMoveEventTimeStamp.value = e.timeStamp
  267. }
  268. }
  269. const onTouchEnd = () => {
  270. isMouseDown.value = false
  271. }
  272. const onTouchCancel = () => {
  273. isMouseDown.value = false
  274. }
  275. const onWheel = (e) => {
  276. if (store.canMoveCamera) {
  277. translateLength.value += e.deltaY
  278. if (translateLength.value < 0) {
  279. translateLength.value = 0
  280. } else if (translateLength.value > maxTranslateLength.value) {
  281. translateLength.value = maxTranslateLength.value
  282. moveSpeed.value = 0
  283. }
  284. }
  285. }
  286. watch(translateLength, (vNew) => {
  287. try {
  288. currentTimeIdx.value = Math.round(translateLength.value / longref$.value.children[0].offsetWidth)
  289. } catch (error) {
  290. console.error('translateLength error: ', error)
  291. }
  292. })
  293. onMounted(() => {
  294. animationFrameId.value = requestAnimationFrame(animationFrameTask)
  295. if (store.longImageTranslateLengthRecord) {
  296. translateLength.value = store.longImageTranslateLengthRecord
  297. store.longImageTranslateLengthRecord = null
  298. }
  299. calcTranslateLimit()
  300. window.addEventListener('resize', calcTranslateLimit)
  301. setTimeout(() => {
  302. isShowSkip.value = true
  303. }, 6000);
  304. })
  305. </script>
  306. <style lang="scss" scoped>
  307. .long-image {
  308. height: 100%;
  309. width: 100%;
  310. position: relative;
  311. overflow: hidden;
  312. .time-item {
  313. position: absolute;
  314. top: 0;
  315. height: 100%;
  316. text-align: center;
  317. justify-content: flex-start;
  318. display: flex;
  319. align-items: center;
  320. }
  321. }
  322. .guide {
  323. position: fixed;
  324. left: 0;
  325. right: 0;
  326. bottom: 0;
  327. top: 0;
  328. width: 100%;
  329. height: 100%;
  330. z-index: 99;
  331. background-color: rgba($color: #000000, $alpha: 0.65);
  332. backdrop-filter: blur(15px);
  333. display: flex;
  334. justify-content: center;
  335. align-items: flex-end;
  336. >img {
  337. width: 100%;
  338. }
  339. .tips {
  340. position: absolute;
  341. top: 25%;
  342. left: 50%;
  343. transform: translateX(-50%);
  344. z-index: 100;
  345. text-align: center;
  346. color: #fff;
  347. width: 20%;
  348. >img {
  349. margin: 1.63rem 0 1rem;
  350. width: 80%;
  351. }
  352. >div {
  353. cursor: pointer;
  354. >img {
  355. width: 7rem;
  356. }
  357. }
  358. }
  359. }
  360. .fade-in-video-wrap {
  361. .skip-button {
  362. position: absolute;
  363. top: 1.21rem;
  364. right: 1.46rem;
  365. height: 1.75rem;
  366. z-index: 10000;
  367. img {
  368. height: 100%;
  369. }
  370. }
  371. .bofang-button {
  372. position: absolute;
  373. left: 50%;
  374. bottom: 30%;
  375. transform: translateX(-50%);
  376. z-index: 10000;
  377. width: 6rem;
  378. img {
  379. width: 100%;
  380. }
  381. }
  382. .initial-video {
  383. position: absolute; // 微信内嵌浏览器里视频必须决定对定位且z-index为负,否则,开始播放后,应该层叠在其上的决对定位的元素不会显示。
  384. top: 0;
  385. left: 0;
  386. width: 100%;
  387. height: 100%;
  388. background: #1f0f05;
  389. object-fit: cover;
  390. }
  391. }
  392. </style>