canvasPlayer.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. import { isMobile } from "./isMoblie";
  2. import { throttle, debounce, clamp } from "./fn";
  3. import gsap from "gsap";
  4. import mitt from "mitt";
  5. class Mitt {
  6. constructor(e) {
  7. Object.assign(this, mitt(e));
  8. }
  9. }
  10. export class CanvasPlayer extends Mitt {
  11. constructor(canvasId, setting) {
  12. super();
  13. this.canvas = null;
  14. this.canvasId = canvasId;
  15. this.context = null;
  16. this.vw = null;
  17. this.vh = null;
  18. this.imageW = null;
  19. this.imageH = null;
  20. this.setting = setting;
  21. this.currentFrame = 0; // 当前真正
  22. this.movingFrame = 0; // 当前前进
  23. this.scrollFrame = 0;
  24. this.lastScroll = 0;
  25. this.isRendering = false;
  26. this.localRender = [];
  27. this.currentType = 0;
  28. this.frames = [];
  29. this.clips = [];
  30. this.canScroll = false;
  31. this.resize = this.resize.bind(this);
  32. this.watchScroll = this.watchScroll.bind(this);
  33. }
  34. init() {
  35. this.canvas = document.getElementById(this.canvasId);
  36. this.context = this.canvas.getContext("2d", {
  37. alpha: true,
  38. desynchronized: true,
  39. powerPreference: "high-performance",
  40. });
  41. this.vw = this.canvas.width = window.innerWidth;
  42. this.vh = this.canvas.height = window.innerHeight;
  43. this.imageW = isMobile() ? 750 : 1920;
  44. this.imageH = isMobile() ? 1334 : 1080;
  45. this.proload();
  46. this.initClipAnimate();
  47. this.loadEvent();
  48. }
  49. loadImage(url) {
  50. return new Promise((resolve, reject) => {
  51. const img = new Image(this.imageW, this.imageH);
  52. img.onload = () => {
  53. resolve(img);
  54. };
  55. img.onerror = (error) => {
  56. resolve();
  57. };
  58. img.src = url;
  59. });
  60. }
  61. proload() {
  62. if (this.setting) {
  63. const list = [];
  64. const total = Array.from(this.setting).reduce(
  65. (pre, current) => pre + current["total"],
  66. 0
  67. );
  68. Array.from(this.setting).forEach((item, framekey) => {
  69. const base = [];
  70. const clip = {
  71. id: item.sectionType,
  72. total: item.total,
  73. x: 0,
  74. animation: null,
  75. };
  76. this.clips.push(clip);
  77. for (let key = 0; key < item.total; key++) {
  78. const padLength = item.total.toString().length + 1;
  79. let imgIndex = String(key).padStart(padLength, "0");
  80. let url = `${item.imageUrl}/${imgIndex}.webp`;
  81. const res = this.loadImage(url).then((image) => {
  82. if (image) {
  83. const frame = {
  84. id: item.sectionType,
  85. index: key,
  86. image: image,
  87. total: item.total,
  88. };
  89. this.context.drawImage(image, 0, 0, this.vw, this.vh);
  90. this.frames.push(frame);
  91. const cu = key + framekey * item.total;
  92. const process = Math.floor(Number(cu / total) * 100);
  93. this.emit("updatePress", process);
  94. }
  95. });
  96. list.push(res);
  97. }
  98. });
  99. Promise.all(list).then(() => {
  100. console.warn("all load");
  101. this.emit("updatePress", 100);
  102. this.emit("loaded");
  103. });
  104. }
  105. }
  106. loadEvent() {
  107. window.addEventListener("resize", this.resize, false);
  108. window.addEventListener("wheel", this.watchScroll, false);
  109. }
  110. unLoadEvent() {
  111. window.removeEventListener("resize", this.resize, false);
  112. window.removeEventListener("wheel", this.watchScroll, false);
  113. }
  114. resize() {
  115. this.vw = this.canvas.width = window.innerWidth;
  116. this.vh = this.canvas.height = window.innerHeight;
  117. }
  118. enableScroll(type = 0) {
  119. this.canScroll = true;
  120. this.currentType = type;
  121. this.initFirstFrame();
  122. }
  123. unEnableScroll() {
  124. this.canScroll = false;
  125. }
  126. manualScroll(event, type) {
  127. if (!this.canScroll) {
  128. this.enableScroll(type);
  129. this.initFirstFrame();
  130. }
  131. const scrollY = event.target.scrollTop;
  132. if (scrollY > 0 && this.lastScroll <= scrollY) {
  133. this.lastScroll = scrollY;
  134. // console.log("Scrolling DOWN");
  135. this.toScroll(scrollY, true, event);
  136. } else {
  137. this.lastScroll = scrollY;
  138. // console.log("Scrolling UP");
  139. this.toScroll(scrollY, false, event);
  140. }
  141. // deltaY = scrollY - lastKnownScrollPosition;
  142. // const deltaHeight = clip.total * 100 - window.innerHeight;
  143. // const prcess = scrollY / deltaHeight;
  144. // const frame = Math.round(clip.total * prcess);
  145. }
  146. toScroll(scrollY, na, event) {
  147. let timer, completeTimer;
  148. const clip = Array.from(this.clips).find(
  149. (item) => item.id === this.currentType
  150. );
  151. // const deltaHeight = clip.total * 100 + window.innerHeight;
  152. // const prcess = scrollY / deltaHeight;
  153. // const startFrame = Math.floor(clip.total * prcess);
  154. const startFrame = this.getframeByHeight(scrollY);
  155. if (timer) {
  156. clearTimeout(timer);
  157. timer = null;
  158. }
  159. this.currentFrame = startFrame;
  160. const matchDis = 10;
  161. const dynamicDistance = na
  162. ? this.currentFrame + matchDis
  163. : this.currentFrame - matchDis;
  164. // 少于frame
  165. const dis = clamp(dynamicDistance, 0, clip.total);
  166. console.log("startFrame", this.currentFrame, dis, this.movingFrame);
  167. if (this.scrollAni) {
  168. // console.log("11", this.scrollAni.duration());
  169. this.scrollAni.kill();
  170. this.scrollAni = null;
  171. }
  172. timer = setTimeout(() => {
  173. this.scrollAni = gsap.timeline();
  174. this.isRendering = true;
  175. this.scrollAni.to(this, {
  176. movingFrame: dis,
  177. // ease: "power1.inOut",
  178. // duration: 0.6,
  179. yoyo: true,
  180. onUpdate: () => {
  181. this.isRendering = true;
  182. const mFrame = Math.floor(this.movingFrame);
  183. this.reDraw(mFrame, this.currentType);
  184. },
  185. onStart: () => {},
  186. onComplete: () => {
  187. completeTimer = setTimeout(this.toRunPatch, 0);
  188. this.scrollAni && this.scrollAni.pause();
  189. },
  190. });
  191. }, 40);
  192. }
  193. watchScroll(event) {
  194. if (this.canScroll) {
  195. if (this.currentFrame < 0) {
  196. this.currentFrame = 0;
  197. }
  198. if (event.deltaY > 0) {
  199. // this.autoIncrement(true);
  200. } else {
  201. // this.autoIncrement(false);
  202. }
  203. }
  204. }
  205. getframeByHeight(height) {
  206. if (this.currentType) {
  207. const clip = Array.from(this.clips).find(
  208. (item) => item.id === this.currentType
  209. );
  210. const deltaHeight = clip.total * 100 - window.innerHeight;
  211. const prcess = height / deltaHeight;
  212. const frame = Math.round(clip.total * prcess);
  213. return clamp(frame, 0, clip.total);
  214. }
  215. return 0;
  216. }
  217. getFrameScrollTop(frame) {
  218. if (this.currentType) {
  219. const clip = Array.from(this.clips).find(
  220. (item) => item.id === this.currentType
  221. );
  222. const updateHeight = clamp(frame * 100, 0, clip.total * 100);
  223. return updateHeight;
  224. }
  225. return 0;
  226. }
  227. updateScrollTop(frame) {
  228. if (this.currentType) {
  229. const bar = document.querySelector(`.scroll-bar-${this.currentType}`);
  230. const updateHeight = frame * 100 + window.innerHeight;
  231. console.log("updateScrollTop", updateHeight);
  232. bar.style.scrollTop = updateHeight;
  233. }
  234. }
  235. initClipScrollheight() {
  236. if (this.currentType) {
  237. const bar = document.querySelector(
  238. `.scroll-bar-${this.currentType}-placeholder`
  239. );
  240. const clip = Array.from(this.clips).find(
  241. (item) => item.id === this.currentType
  242. );
  243. const deltaHeight = clip.total * 100;
  244. bar.style.height = `${deltaHeight}px`;
  245. }
  246. }
  247. resetClipScrollTop() {
  248. if (this.currentType) {
  249. const bar = document.querySelector(`.scroll-bar-${this.currentType}`);
  250. bar.style.scrollTop = 0;
  251. }
  252. }
  253. noticeProcess() {
  254. const clip = Array.from(this.clips).find(
  255. (item) => item.id === this.currentType
  256. );
  257. const process = Number(this.currentFrame / clip.total) * 100;
  258. this.emit("process", {
  259. process,
  260. type: this.currentType,
  261. });
  262. }
  263. test() {
  264. const height = this.getFrameScrollTop(this.currentFrame);
  265. console.log("target-height", height);
  266. document.querySelector(".scroll-bar-3").scrollTop = height;
  267. }
  268. play(frame) {
  269. console.log("play", frame);
  270. const height = this.getFrameScrollTop(frame);
  271. console.log("target-height", height);
  272. this.lastScroll = height;
  273. this.currentFrame = frame;
  274. document.querySelector(".scroll-bar-3").scrollTo({ top: height, left: 0 });
  275. }
  276. initClipAnimate() {
  277. Array.from(this.clips).forEach((item, key) => {
  278. const duration = this.clips[key].total / 15;
  279. const anmi = gsap.to(this.clips[key], duration, {
  280. x: this.clips[0].total - 1,
  281. repeat: 0,
  282. duration: (this.clips[key].total / 2) * 1000,
  283. ease: "none",
  284. yoyo: true,
  285. onComplete: () => {
  286. console.log("done");
  287. },
  288. onUpdate: () => {
  289. const frame = Math.floor(this.clips[key].x);
  290. this.reDraw(frame, this.currentType);
  291. },
  292. });
  293. anmi.pause();
  294. this.clips[key].animation = anmi;
  295. });
  296. }
  297. autoPlay() {
  298. console.log("this.clips[0]", this.clips[0].animation);
  299. this.clips[0].animation.restart();
  300. }
  301. initFirstFrame() {
  302. if (this.currentType) {
  303. const frameItem = this.frames.find(
  304. (item) => item.id == this.currentType && item.index == 1
  305. );
  306. this.context.clearRect(0, 0, this.vw, this.vh);
  307. this.context.drawImage(frameItem.image, 0, 0, this.vw, this.vh);
  308. this.initClipScrollheight();
  309. this.scrollAni = gsap.timeline();
  310. let doneScroll = () => {
  311. this.isRendering = false;
  312. if (this.currentFrame === 0) {
  313. this.emit("scroll-prev");
  314. }
  315. if (this.currentFrame === frameItem.total) {
  316. this.emit("scroll-next");
  317. }
  318. console.warn("scroll done", this.currentFrame, this.movingFrame);
  319. };
  320. this.toRunPatch = debounce(doneScroll, 400);
  321. console.log("initFirstFrame");
  322. }
  323. }
  324. reDraw(frame, type) {
  325. if (isMobile()) {
  326. } else {
  327. const frameItem = this.frames.find(
  328. (item) => item.id == type && item.index == frame
  329. );
  330. if (frameItem && frameItem.index <= frameItem.total) {
  331. this.context.clearRect(0, 0, this.vw, this.vh);
  332. this.context.drawImage(frameItem.image, 0, 0, this.vw, this.vh);
  333. }
  334. }
  335. }
  336. update() {}
  337. }