Source: src/SpinViewer/SpriteImage.ts

  1. import Component, { ComponentEvent } from "@egjs/component";
  2. import { TRANSFORM, SUPPORT_WILLCHANGE } from "../utils/browserFeature";
  3. import { VERSION } from "../version";
  4. import { SpinViewerOptions } from "./SpinViewer";
  5. import { DEFAULT_IMAGE_CLASS, DEFAULT_WRAPPER_CLASS } from "./consts";
  6. export interface SpriteImageEvent {
  7. /**
  8. * Events that occur when component loading is complete
  9. * @ko 컴포넌트 로딩이 완료되면 발생하는 이벤트
  10. * @name eg.view360.SpriteImage#load
  11. * @event
  12. * @param {Object} param The object of data to be sent to an event <ko>이벤트에 전달되는 데이터 객체</ko>
  13. * @param {HTMLElement} param.target The target element for which to display the image <ko>이미지를 보여줄 대상 엘리먼트</ko>
  14. * @param {HTMLElement} param.bgElement Generated background image element <ko>생성된 background 이미지 엘리먼트</ko>
  15. *
  16. * @example
  17. *
  18. * sprites.on({
  19. * "load" : function(evt) {
  20. * console.log("load event fired - e.target", e.target, "e.bgElement", e.bgElement);
  21. * }
  22. * });
  23. */
  24. load: {
  25. target: HTMLElement;
  26. bgElement: HTMLDivElement;
  27. };
  28. /**
  29. * An event that occurs when the image index is changed by the user's left / right panning
  30. * @ko 사용자의 좌우 Panning 에 의해 이미지 인덱스가 변경되었을때 발생하는 이벤트
  31. * @name eg.view360.SpriteImage#imageError
  32. * @event
  33. * @param {Object} param The object of data to be sent to an event <ko>이벤트에 전달되는 데이터 객체</ko>
  34. * @param {String} param.imageUrl User-specified image URL <ko>사용자가 지정한 이미지 URL</ko>
  35. *
  36. * @example
  37. *
  38. * sprites.on({
  39. * "imageError" : function(evt) {
  40. * // Error handling
  41. * console.log(e.imageUrl);
  42. * }
  43. * });
  44. */
  45. imageError: {
  46. imageUrl?: string;
  47. };
  48. }
  49. /**
  50. * @memberof eg.view360
  51. * @extends eg.Component
  52. * SpriteImage
  53. */
  54. class SpriteImage extends Component<SpriteImageEvent> {
  55. private static _createBgDiv(wrapperInContainer: HTMLDivElement | null, img: HTMLImageElement, rowCount: number, colCount: number, autoHeight: boolean) {
  56. const el = wrapperInContainer || document.createElement("div");
  57. el.style.position = "relative";
  58. el.style.overflow = "hidden";
  59. img.style.position = "absolute";
  60. img.style.width = `${colCount * 100}%`;
  61. img.style.height = `${rowCount * 100}%`;
  62. /** Prevent image from being dragged on IE10, IE11, Safari especially */
  63. img.ondragstart = () => (false); // img.style.pointerEvents = "none";
  64. // Use hardware accelerator if available
  65. if (SUPPORT_WILLCHANGE) {
  66. (img.style.willChange = "transform");
  67. }
  68. el.appendChild(img);
  69. const unitWidth = img.naturalWidth / colCount;
  70. const unitHeight = img.naturalHeight / rowCount;
  71. if (autoHeight) {
  72. const r = unitHeight / unitWidth;
  73. el.style.paddingBottom = `${r * 100}%`;
  74. } else {
  75. el.style.height = "100%";
  76. }
  77. return el;
  78. }
  79. private static _getSizeString(size) {
  80. if (typeof size === "number") {
  81. return `${size}px`;
  82. }
  83. return size;
  84. }
  85. public static VERSION = VERSION;
  86. private _el: HTMLElement;
  87. private _rowCount: number;
  88. private _colCount: number;
  89. private _totalCount: number;
  90. private _width: number | string;
  91. private _height: number | string;
  92. private _autoHeight: boolean;
  93. private _colRow: number[];
  94. private _image: HTMLImageElement;
  95. private _bg: HTMLDivElement;
  96. private _autoPlayReservedInfo: { interval: number; playCount: number } | null;
  97. private _autoPlayTimer: number;
  98. /**
  99. * @class eg.view360.SpriteImage
  100. * @classdesc A module that displays a single or continuous image of any one of the "sprite images". SpinViewer internally uses SpriteImage to show each frame of the sprite image.
  101. * @ko 스프라이트 이미지 중 임의의 한 프레임을 단발성 혹은 연속적으로 보여주는 컴포넌트입니다. SpinViewer 는 내부적으로 SpriteImage 를 사용하여 스프라이트 이미지의 각 프레임을 보여줍니다.
  102. * @extends eg.Component
  103. *
  104. * @param {HTMLElement} element The element to show the image <ko>이미지를 보여줄 대상 요소</ko>
  105. * @param {Object} options The option object<ko>파라미터 객체</ko>
  106. * @param {String} options.imageUrl The url of the sprite image <ko>스프라이트 이미지의 url</ko>
  107. * @param {Number} [options.rowCount=1] Number of horizontal frames in the sprite image <ko>스프라이트 이미지의 가로 프레임 갯수</ko>
  108. * @param {Number} [options.colCount=1] Number of vertical frames in the sprite image <ko>스프라이트 이미지의 세로 프레임 갯수</ko>
  109. * @param {Number|String} [options.width="auto"] The width of the target element to show the image <ko>이미지를 보여줄 대상 요소의 너비</ko>
  110. * @param {Number|String} [options.height="auto"] The height of the target element to show the image <ko>이미지를 보여줄 대상 요소의 높이</ko>
  111. * @param {Boolean} [options.autoHeight=true] Whether to automatically set the height of the image area to match the original image's proportion <ko>원본 이미지 비율에 맞게 이미지 영역의 높이를 자동으로 설정할지 여부</ko>
  112. * @param {Number[]} [options.colRow=[0, 0]] The column, row coordinates of the first frame of the sprite image (based on 0 index) <ko> 스프라이트 이미지 중 처음 보여줄 프레임의 (column, row) 좌표 (0 index 기반)</ko>
  113. * @param {Number} [options.frameIndex=0] frameIndex specifies the index of the frame to be displayed in the "Sprite image". The frameIndex order is zero-based and indexed in Z form (left-to-right, top-to-bottom, and newline again from left to right).<br>- colRow is equivalent to frameIndex. However, if colRow is specified at the same time, colRow takes precedence.<ko>스프라이트 이미지 중에서 보여질 프레임의 인덱스를 지정합니다. frameIndex 순서는 0부터 시작하며 Z 형태(왼쪽에서 오른쪽, 위에서 아래, 개행 시 다시 왼쪽 부터)로 인덱싱합니다.<br>- colRow 는 frameIndex 와 동일한 기능을 합니다. 단, colRow 가 동시에 지정된 경우 colRow 가 우선합니다.</ko>
  114. * @param {Number} [options.scale=1] Spin scale (The larger the spin, the more).<ko>Spin 배율 (클 수록 더 많이 움직임)</ko>
  115. *
  116. * @support {"ie": "9+", "ch" : "latest", "ff" : "latest", "sf" : "latest", "edge" : "latest", "ios" : "7+", "an" : "2.3+ (except 3.x)"}
  117. * @example
  118. *
  119. * // Initialize SpriteImage
  120. *
  121. * var el = document.getElementById("image-div");
  122. * var sprites = new eg.view360.SpriteImage(el, {
  123. * imageUrl: "/img/bag360.jpg", // required
  124. * rowCount: 24
  125. * });
  126. */
  127. public constructor(element: HTMLElement, options: Partial<SpinViewerOptions> = {}) {
  128. super();
  129. const opt = options || {};
  130. this._el = element;
  131. this._rowCount = opt.rowCount || 1;
  132. this._colCount = opt.colCount || 1;
  133. this._totalCount = this._rowCount * this._colCount; // total frames
  134. this._width = opt.width || "auto";
  135. this._height = opt.height || "auto";
  136. this._autoHeight = opt.autoHeight != null ? opt.autoHeight : true; // If autoHeight is specified, _height will be overwritten.
  137. this._colRow = [0, 0];
  138. if (opt.colRow) {
  139. this._colRow = opt.colRow;
  140. } else if (opt.frameIndex) {
  141. this.setFrameIndex(opt.frameIndex);
  142. }
  143. this._el.style.width = SpriteImage._getSizeString(this._width);
  144. this._el.style.height = SpriteImage._getSizeString(this._height);
  145. const wrapperClass = opt.wrapperClass || DEFAULT_WRAPPER_CLASS;
  146. const imageClass = opt.imageClass || DEFAULT_IMAGE_CLASS;
  147. if (!opt.imageUrl) {
  148. setTimeout(() => {
  149. this.trigger(new ComponentEvent("imageError", {
  150. imageUrl: opt.imageUrl
  151. }));
  152. }, 0);
  153. return;
  154. }
  155. const imageInContainer = element.querySelector<HTMLImageElement>(`.${imageClass}`);
  156. const wrapperInContainer = element.querySelector<HTMLDivElement>(`.${wrapperClass}`);
  157. if (wrapperInContainer && imageInContainer) {
  158. // Set it to invisible to prevent wrapper being resized
  159. imageInContainer.style.display = "none";
  160. }
  161. this._image = imageInContainer || new Image();
  162. /**
  163. * Event
  164. */
  165. const image = this._image;
  166. image.onload = () => {
  167. if (wrapperInContainer && imageInContainer) {
  168. imageInContainer.style.display = "";
  169. }
  170. this._bg = SpriteImage._createBgDiv(
  171. wrapperInContainer,
  172. image,
  173. this._rowCount,
  174. this._colCount,
  175. this._autoHeight
  176. );
  177. this._el.appendChild(this._bg);
  178. this.setColRow(this._colRow[0], this._colRow[1]);
  179. this.trigger(new ComponentEvent("load", {
  180. target: this._el,
  181. bgElement: this._bg
  182. }));
  183. if (this._autoPlayReservedInfo) {
  184. this.play(this._autoPlayReservedInfo);
  185. this._autoPlayReservedInfo = null;
  186. }
  187. };
  188. image.onerror = () => {
  189. this.trigger(new ComponentEvent("imageError", {
  190. imageUrl: opt.imageUrl
  191. }));
  192. };
  193. image.src = opt.imageUrl;
  194. }
  195. /**
  196. * Specifies the frameIndex of the frame to be shown in the sprite image.
  197. * @ko 스프라이트 이미지 중 보여질 프레임의 frameIndex 값을 지정
  198. * @method eg.view360.SpriteImage#setFrameIndex
  199. * @param {Number} frameIndex frame index of a frame<ko>프레임의 인덱스</ko>
  200. *
  201. * @example
  202. *
  203. * sprites.setFrameIndex(0, 1);// col = 0, row = 1
  204. */
  205. public setFrameIndex(index: number) {
  206. const colRow = this.toColRow(index);
  207. this.setColRow(colRow[0], colRow[1]);
  208. }
  209. /**
  210. * Returns the frameIndex of the frame to be shown in the sprite image.
  211. * @ko 스프라이트 이미지 중 보여지는 프레임의 index 값을 반환
  212. * @method eg.view360.SpriteImage#getFrameIndex
  213. * @return {Number} frame index <ko>frame 인덱스</ko>
  214. *
  215. * @example
  216. *
  217. * var frameIndex = sprites.getFrameIndex(); // eg. frameIndex = 1
  218. *
  219. */
  220. public getFrameIndex() {
  221. return this._colRow[1] * this._colCount + this._colRow[0];
  222. }
  223. /**
  224. * Specifies the col and row values of the frame to be shown in the sprite image.
  225. * @ko 스프라이트 이미지 중 보여질 프레임의 col, row 값을 지정
  226. * @method eg.view360.SpriteImage#setColRow
  227. * @param {Number} col Column number of a frame<ko>프레임의 행값</ko>
  228. * @param {Number} row Row number of a frame<ko>프레임의 열값</ko>
  229. *
  230. * @example
  231. *
  232. * sprites.setlColRow(1, 2); // col = 1, row = 2
  233. */
  234. public setColRow(col: number, row: number) {
  235. if (row > this._rowCount - 1 || col > this._colCount - 1) {
  236. return;
  237. }
  238. if (this._image && TRANSFORM) {
  239. // NOTE: Currently, do not apply translate3D for using layer hack. Do we need layer hack for old browser?
  240. this._image.style[TRANSFORM] = `translate(${-(col / this._colCount * 100)}%, ${-(row / this._rowCount * 100)}%)`;
  241. }
  242. this._colRow = [col, row];
  243. }
  244. /**
  245. * Returns the col and row values of the frame to be shown in the sprite image.
  246. * @ko 스프라이트 이미지 중 보여지는 프레임의 col, row 값을환반환
  247. * @method eg.view360.SpriteImage#gelColRow
  248. * @return {Number[]} Array containing col, row<ko>col, row 정보를 담는 배열</ko>
  249. *
  250. * @example
  251. *
  252. * var colRow = sprites.getlColRow();
  253. * // colRow = [1, 2] - index of col is 1, index of row is 2
  254. *
  255. */
  256. public getColRow() {
  257. return this._colRow;
  258. }
  259. /**
  260. * Stop playing
  261. * @ko play 되고 있던 프레임 재생을 중지합니다.
  262. * @method eg.view360.SpriteImage#stop
  263. *
  264. * @example
  265. *
  266. * viewer.stop();
  267. *
  268. */
  269. public stop() {
  270. if (this._autoPlayTimer) {
  271. clearInterval(this._autoPlayTimer);
  272. this._autoPlayTimer = -1;
  273. }
  274. }
  275. /**
  276. * Switches frames sequentially in the 'interval' starting from the currently displayed frame and plays all frames by 'playCount'.
  277. * @ko 현재 보여지고 있는 프레임을 시작으로 'interval' 간격으로 순차적으로 프레임을 전환하며 모든 프레임을 'playCount' 만큼 재생한다.
  278. * @method eg.view360.SpriteImage#play
  279. * @param {Object} param The parameter object<ko>파라미터 객체</ko>
  280. * @param {Number} [param.interval=1000 / totalFrameCount] Interframe Interval - in milliseconds<ko>프레임간 간격 - 밀리세컨드 단위</ko>
  281. * @param {Number} [param.playCount=0] PlayCount = 1 in which all frames are reproduced once, and playCount = n in which all frames are repeated n times. playCount = 0 in which all frames are repeated infinitely<ko>모든 프레임을 1회씩 재생한 것이 playCount = 1, 모든 프레임을 n 회 재상한 것이 playCount = n 이 된다. 0 dms 무한반복</ko>
  282. *
  283. * @example
  284. *
  285. * viewer.play({angle: 16, playCount: 1});
  286. *
  287. */
  288. public play({ interval, playCount } = { interval: 1000 / this._totalCount, playCount: 0 }) {
  289. if (!this._bg) {
  290. this._autoPlayReservedInfo = {interval, playCount};
  291. return;
  292. }
  293. if (this._autoPlayTimer) {
  294. clearInterval(this._autoPlayTimer);
  295. this._autoPlayTimer = -1;
  296. }
  297. let frameIndex = this.getFrameIndex();
  298. let count = 0;
  299. let frameCount = 0; // for checking 1 cycle
  300. this._autoPlayTimer = window.setInterval(() => {
  301. frameIndex %= this._totalCount;
  302. const colRow = this.toColRow(frameIndex);
  303. this.setColRow(colRow[0], colRow[1]);
  304. frameIndex++;
  305. // Done 1 Cycle?
  306. if (++frameCount === this._totalCount) {
  307. frameCount = 0;
  308. count++;
  309. }
  310. if (playCount > 0 && count === playCount) {
  311. clearInterval(this._autoPlayTimer);
  312. }
  313. }, interval);
  314. }
  315. public toColRow(frameIndex: number) {
  316. const colCount = this._colCount;
  317. const rowCount = this._rowCount;
  318. if (frameIndex < 0) {
  319. return [0, 0];
  320. } else if (frameIndex >= this._totalCount) {
  321. return [colCount - 1, rowCount - 1];
  322. }
  323. const col = frameIndex % colCount;
  324. const row = Math.floor(frameIndex / colCount);
  325. // console.log(frameIndex, col, row);
  326. return [col, row];
  327. }
  328. }
  329. export default SpriteImage;
comments powered by Disqus