SerialFrames.vue 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. <template>
  2. <div class="frames-wrap">
  3. <img v-if="needMask && isShowMask" class="mask-image" :src="imageSrcFunc(1)" alt="" draggable="false">
  4. <!-- {{ frameTotalNum }} -->
  5. <img
  6. v-for="index of frameTotalNum"
  7. v-show="
  8. frameCurNum === index - 1 ||
  9. (frameCurNum - 1 === index - 1) ||
  10. (frameCurNum === 0 && index === frameTotalNum)
  11. "
  12. :key="index"
  13. :style="{
  14. zIndex: (frameCurNum === 0 && index === frameTotalNum) ? -1 : 0,
  15. opacity: needMask && isShowMask ? 0 : 1
  16. }"
  17. :src="imageSrcFunc(index)"
  18. alt=""
  19. draggable="false"
  20. @load="onFrameLoad(index - 1)"
  21. @error="onFrameError(index - 1)"
  22. >
  23. <audio v-if="audioUrl" ref="audio" loop :src="audioUrl" controls @timeupdate="onAudioCurTimeChange" />
  24. </div>
  25. </template>
  26. <script>
  27. import appStore from "@/store/index";
  28. export default {
  29. props: {
  30. isDebug: {
  31. type: Boolean,
  32. default: false,
  33. },
  34. frameTotalNum: {
  35. type: Number,
  36. required: true,
  37. },
  38. frameInterval: {
  39. type: Number,
  40. default: 41.667
  41. },
  42. imageSrcFunc: {
  43. type: Function,
  44. required: true,
  45. },
  46. autoPlay: {
  47. type: Boolean,
  48. default: false,
  49. },
  50. repeat: {
  51. type: Boolean,
  52. default: false,
  53. },
  54. audioUrl: {
  55. type: String,
  56. default: '',
  57. },
  58. audioVolumeRatio: {
  59. type: Number,
  60. default: 1,
  61. },
  62. },
  63. emits: ['over'],
  64. setup() {
  65. const store = appStore();
  66. return { store }
  67. },
  68. data() {
  69. return {
  70. isShowMask: true,
  71. repeatCount: 0,
  72. frameCurNum: 0,
  73. frameStateList: new Array(this.frameTotalNum),
  74. frameIntervalId: null,
  75. isPlaying: false,
  76. isPaused: false, // paused时也算playing
  77. intersectionObserverForFrame: null,
  78. intersectionObserverForAudio: null,
  79. intersectionRatio: 0,
  80. audioFadeoutRate: 1,
  81. firstIn: true,
  82. // frameTotalNum: 24
  83. }
  84. },
  85. computed: {
  86. // 在移动端火狐浏览器中,有透明度的序列帧的头几轮播放会有闪烁的现象,所以需要临时用第一帧盖住。
  87. needMask() {
  88. if (this.repeat && /FireFox/i.test(navigator.userAgent)) {
  89. return true
  90. } else {
  91. return false
  92. }
  93. },
  94. audioVolume() {
  95. if (this.store.canPlayLongImageBgAudio) {
  96. if (this.firstIn) {
  97. this.firstIn = false
  98. this.$refs.audio && (this.$refs.audio.currentTime = 0)
  99. }
  100. return this.intersectionRatio * this.audioFadeoutRate * this.audioVolumeRatio
  101. } else {
  102. return 0
  103. }
  104. }
  105. },
  106. watch: {
  107. frameCurNum: {
  108. handler(vNew) {
  109. if (this.isDebug) {
  110. console.log('SerialFrames frameCurNum: ', vNew)
  111. }
  112. },
  113. immediate: true,
  114. },
  115. intersectionRatio: {
  116. handler(vNew, vOld) {
  117. console.log('result:', vNew, vOld);
  118. if (this.isDebug) {
  119. console.log('intersectionRation change: ', vNew)
  120. }
  121. const audioNode = this.$refs.audio
  122. if (!this.$refs.audio) {
  123. return
  124. }
  125. if (vNew > 0) {
  126. if (audioNode.paused && vOld <= 0 && !this.$isSafari) { // safari里只能在用户操作回调函数中成功调用play。
  127. console.log('result:bofang');
  128. audioNode.currentTime = 0
  129. audioNode.play()
  130. }
  131. } else {
  132. if (!audioNode.paused && vOld > 0 && !this.$isSafari) {
  133. audioNode.pause()
  134. }
  135. }
  136. },
  137. immediate: false,
  138. },
  139. audioVolume: {
  140. handler(vNew) {
  141. setTimeout(() => {
  142. const audioNode = this.$refs.audio
  143. if (!this.$refs.audio) {
  144. return
  145. }
  146. if (this.isDebug) {
  147. console.log('audio volume: ', vNew)
  148. }
  149. audioNode.volume = vNew
  150. }, 0)
  151. },
  152. immediate: true,
  153. }
  154. },
  155. mounted() {
  156. if (this.autoPlay) {
  157. this.intersectionObserverForFrame = new IntersectionObserver((entries) => {
  158. let entry = entries[entries.length - 1]
  159. if (entry.intersectionRatio > 0) {
  160. if (this.isDebug) {
  161. console.log('play!')
  162. }
  163. this.play()
  164. } else {
  165. if (this.isDebug) {
  166. console.log('stop!')
  167. }
  168. this.stop()
  169. }
  170. }, {
  171. root: document.getElementsByClassName('long-image')[0],
  172. threshold: [
  173. 0.0,
  174. ]
  175. })
  176. this.intersectionObserverForFrame.observe(this.$el)
  177. }
  178. if (this.audioUrl) {
  179. this.intersectionObserverForAudio = new IntersectionObserver((entries) => {
  180. let entry = entries[entries.length - 1]
  181. this.intersectionRatio = entry.intersectionRatio
  182. }, {
  183. root: document.getElementsByClassName('long-image')[0],
  184. threshold: [
  185. 0.0,
  186. 0.1,
  187. 0.2,
  188. 0.3,
  189. 0.4,
  190. 0.5,
  191. 0.6,
  192. 0.7,
  193. 0.8,
  194. 0.9,
  195. 1.0
  196. ]
  197. })
  198. this.intersectionObserverForAudio.observe(this.$el)
  199. }
  200. },
  201. unmounted() {
  202. clearInterval(this.frameIntervalId)
  203. if (this.intersectionObserverForFrame) {
  204. this.intersectionObserverForFrame.disconnect()
  205. }
  206. if (this.intersectionObserverForAudio) {
  207. this.intersectionObserverForAudio.disconnect()
  208. }
  209. if (this.$refs.audio && !this.$refs.audio.paused) {
  210. this.$refs.audio.pause()
  211. }
  212. this.firstIn = true
  213. },
  214. methods: {
  215. onFrameLoad(idx) {
  216. this.frameStateList[idx] = true
  217. },
  218. onFrameError(idx) {
  219. this.frameStateList[idx] = false
  220. },
  221. play() {
  222. if (this.isPlaying) {
  223. return
  224. }
  225. this.isPlaying = true
  226. this.frameCurNum = 0
  227. this.frameIntervalId = setInterval(() => {
  228. if (this.isPaused) {
  229. return
  230. }
  231. const frameNumBackup = this.frameCurNum
  232. this.frameCurNum++
  233. if (this.frameCurNum === this.frameTotalNum) {
  234. if (this.repeat) {
  235. this.frameCurNum = 0
  236. this.repeatCount++
  237. } else {
  238. clearInterval(this.frameIntervalId)
  239. this.isPlaying = false
  240. this.isPaused = false
  241. this.$emit('over')
  242. return
  243. }
  244. }
  245. while (this.frameStateList[this.frameCurNum] === false) {
  246. this.frameCurNum++
  247. if (this.frameCurNum === this.frameTotalNum) {
  248. this.frameCurNum = 0
  249. }
  250. }
  251. if (this.frameStateList[this.frameCurNum] === undefined) {
  252. if (this.frameCurNum < frameNumBackup) {
  253. this.repeatCount--
  254. }
  255. this.frameCurNum = frameNumBackup
  256. }
  257. if (this.repeatCount === 1 && this.needMask) {
  258. this.isShowMask = false
  259. }
  260. }, this.frameInterval)
  261. },
  262. pause() {
  263. if (!this.isPlaying) {
  264. return
  265. }
  266. this.isPaused = true
  267. },
  268. resume() {
  269. if (!this.isPlaying) {
  270. return
  271. }
  272. this.isPaused = false
  273. },
  274. stop() {
  275. if (!this.isPlaying) {
  276. return
  277. }
  278. clearInterval(this.frameIntervalId)
  279. this.isPlaying = false
  280. this.isPaused = false
  281. this.frameCurNum = 0
  282. this.repeatCount = 0
  283. if (this.needMask) {
  284. this.isShowMask = true
  285. }
  286. },
  287. onAudioCurTimeChange() {
  288. const audioNode = this.$refs.audio
  289. if (!this.$refs.audio) {
  290. return
  291. }
  292. if (audioNode.currentTime <= 2) {
  293. this.audioFadeoutRate = audioNode.currentTime / 2
  294. }
  295. if (audioNode.duration - audioNode.currentTime <= 2) {
  296. this.audioFadeoutRate = (audioNode.duration - audioNode.currentTime) / 2
  297. } else {
  298. this.audioFadeoutRate = 1
  299. }
  300. }
  301. },
  302. }
  303. </script>
  304. <style lang="scss" scoped>
  305. .frames-wrap {
  306. text-align: left;
  307. flex-basis: auto;
  308. >img {
  309. height: 100%;
  310. }
  311. }
  312. </style>