Source: src/Parallax.ts

  1. import { ALIGN, isMobile, TRANSFORM } from "./consts";
  2. import { $, isWindow, assign } from "./utils";
  3. import { IAlign, IJQuery, PositionType, SizeType, InnerSizeType, ClientSizeType, IInfiniteGridItemElement, OffsetSizeType, IInfiniteGridItem } from "./types";
  4. export interface IParallaxStyle {
  5. position: PositionType;
  6. size: SizeType;
  7. cammelSize: string;
  8. coordinate: string;
  9. }
  10. const style: {
  11. vertical: IParallaxStyle;
  12. horizontal: IParallaxStyle;
  13. } = {
  14. vertical: { position: "top", size: "height", cammelSize: "Height", coordinate: "Y" },
  15. horizontal: { position: "left", size: "width", cammelSize: "Width", coordinate: "X" },
  16. };
  17. const { START, CENTER } = ALIGN;
  18. /**
  19. * @classdesc Parallax is a displacement or difference in the apparent position of an object viewed along two different lines of sight. You can apply parallax by scrolling the image and speed of the item.
  20. * @ko Parallax는 서로 다른 두 개의 시선에서 바라본 물체의 외관상 위치의 변위 또는 차이입니다. 스크롤에 따라 이미지와 아이템의 속도를 차이를 줌으로써 parallax을 적용할 수 있습니다.
  21. * @class eg.Parallax
  22. * @param {Element|String} [root=window] Scrolling target. If you scroll in the body, set window. 스크롤하는 대상. 만약 body에서 스크롤하면 window로 설정한다.
  23. * @param {Object} [options] The option object of eg.Parallax module <ko>eg.Parallax 모듈의 옵션 객체</ko>
  24. * @param {Boolean} [options.horizontal=false] Direction of the scroll movement (false: vertical, true: horizontal) <ko>스크롤 이동 방향 (false: 세로방향, true: 가로방향)</ko>
  25. * @param {Element|String} [options.container=null] Container wrapping items. If root and container have no gaps, do not set option. <ko> 아이템들을 감싸고 있는 컨테이너. 만약 root와 container간의 차이가 없으면, 옵션을 설정하지 않아도 된다.</ko>
  26. * @param {String} [options.selector="img"] The selector of the image to apply the parallax in the item <ko> 아이템안에 있는 parallax를 적용할 이미지의 selector </ko>
  27. * @param {Boolean} [options.strength=1] Dimensions that indicate the sensitivity of parallax. The higher the strength, the faster.
  28. * @param {Boolean} [options.center=0] The middle point of parallax. The top is 1 and the bottom is -1. <ko> parallax가 가운데로 오는 점. 상단이 1이고 하단이 -1이다. </ko>
  29. * @param {Boolean} [options.range=[-1, 1]] Range to apply the parallax. The top is 1 and the bottom is -1. <ko> parallax가 적용되는 범위, 상단이 1이고 하단이 -1이다. </ko>
  30. * @param {Boolean} [options.align="start"] The alignment of the image in the item. ("start" : top or left, "center": middle) <ko> 아이템안의 이미지의 정렬 </ko>
  31. * @example
  32. ```
  33. <script>
  34. // isOverflowScroll: false
  35. var parallax = new eg.Parallax(window, {
  36. container: ".container",
  37. selector: "img.parallax",
  38. strength: 0.8,
  39. center: 0,
  40. range: [-1, 1],
  41. align: "center",
  42. horizontal: true,
  43. });
  44. // isOverflowScroll: ture
  45. var parallax = new eg.Parallax(".container", {
  46. selector: "img.parallax",
  47. strength: 0.8,
  48. center: 0,
  49. range: [-1, 1],
  50. align: "center",
  51. horizontal: true,
  52. });
  53. // item interface
  54. var item = {
  55. // original size
  56. size: {
  57. width: 100,
  58. height: 100,
  59. },
  60. // view size
  61. rect: {
  62. top: 100,
  63. left: 100,
  64. width: 100,
  65. height: 100,
  66. }
  67. };
  68. </script>
  69. ```
  70. **/
  71. class Parallax {
  72. public options: {
  73. container: HTMLElement;
  74. selector: string;
  75. strength: number;
  76. center: number;
  77. range: number[];
  78. align: IAlign[keyof IAlign];
  79. horizontal: boolean;
  80. };
  81. private _root: Window | HTMLElement;
  82. private _container: HTMLElement;
  83. private _rootSize: number;
  84. private _containerPosition: number;
  85. private _style: IParallaxStyle;
  86. constructor(
  87. root: Window | HTMLElement | IJQuery | string = window,
  88. options: Partial<Parallax["options"]> = {}) {
  89. this.options = assign({
  90. container: null,
  91. selector: "img",
  92. strength: 1,
  93. center: 0,
  94. range: [-1, 1],
  95. align: START,
  96. horizontal: false,
  97. }, options);
  98. this._root = $(root);
  99. this._container = this.options.container && $(this.options.container);
  100. this._rootSize = 0;
  101. this._containerPosition = 0;
  102. this._style = style[this.options.horizontal ? "horizontal" : "vertical"];
  103. this.resize();
  104. }
  105. /**
  106. * As the browser is resized, the gaps between the root and the container and the size of the items are updated.
  107. * @ko 브라우저의 크기가 변경됨으로 써 root와 container의 간격과 아이템들의 크기를 갱신한다.
  108. * @method eg.Parallax#resize
  109. * @param {Array} [items = []] Items to apply parallax. It does not apply if it is not in visible range. <ko>parallax를 적용할 아이템들. 가시거리에 존재하지 않으면 적용이 안된다.</ko>
  110. * @return {eg.Parallax} An instance of a module itself<ko>모듈 자신의 인스턴스</ko>
  111. * @example
  112. ```js
  113. window.addEventListener("resize", function (e) {
  114. parallax.resize(items);
  115. });
  116. ```
  117. */
  118. public resize(items: IInfiniteGridItem[] = []) {
  119. const root = this._root;
  120. const container = this._container;
  121. const positionName = this._style.position;
  122. const sizeName = this._style.cammelSize;
  123. if (!container || root === container) {
  124. this._containerPosition = 0;
  125. } else {
  126. const rootRect = (isWindow(root) ? document.body : root).getBoundingClientRect();
  127. const containertRect = container.getBoundingClientRect();
  128. this._containerPosition = containertRect[positionName] - rootRect[positionName];
  129. }
  130. this._rootSize = isWindow(root) ?
  131. window[`inner${sizeName}` as InnerSizeType] ||
  132. document.documentElement[`client${sizeName}` as ClientSizeType] :
  133. root[`client${sizeName}` as ClientSizeType];
  134. if (isMobile && isWindow(root)) {
  135. const bodyWidth = document.body.offsetWidth || document.documentElement.offsetWidth;
  136. const windowWidth = window.innerWidth;
  137. this._rootSize = this._rootSize / (bodyWidth / windowWidth);
  138. }
  139. items.forEach(item => {
  140. this._checkParallaxItem(item.el!);
  141. });
  142. return this;
  143. }
  144. /**
  145. * Scrolls the image in the item by a parallax.
  146. * @ko 스크롤하면 아이템안의 이미지를 시차적용시킨다.
  147. * @method eg.Parallax#refresh
  148. * @param {Array} [items = []] Items to apply parallax. It does not apply if it is not in visible range. <ko>parallax를 적용할 아이템들. 가시거리에 존재하지 않으면 적용이 안된다.</ko>
  149. * @param {Number} [scrollPositionStart = 0] The scroll position.
  150. * @return {eg.Parallax} An instance of a module itself<ko>모듈 자신의 인스턴스</ko>
  151. * @example
  152. ```js
  153. document.body.addEventListener("scroll", function (e) {
  154. parallax.refresh(items, e.scrollTop);
  155. });
  156. ```
  157. */
  158. public refresh(items: IInfiniteGridItem[] = [], scrollPositionStart = 0) {
  159. const styleNames = this._style;
  160. const positionName = styleNames.position;
  161. const coordinateName = styleNames.coordinate;
  162. const sizeName = styleNames.size;
  163. const options = this.options;
  164. const { strength, center, range, align } = options;
  165. const rootSize = this._rootSize;
  166. const scrollPositionEnd = scrollPositionStart + rootSize;
  167. const containerPosition = this._containerPosition;
  168. items.forEach(item => {
  169. if (!item.rect || !item.size || !item.el) {
  170. return;
  171. }
  172. const position = containerPosition + item.rect[positionName];
  173. const itemSize = item.rect[sizeName] || item.size[sizeName];
  174. // check item is in container.
  175. if (scrollPositionStart > position + itemSize ||
  176. scrollPositionEnd < position) {
  177. return;
  178. }
  179. const el = item.el;
  180. if (!el.__IMAGE__) {
  181. this._checkParallaxItem(el);
  182. }
  183. if (el.__IMAGE__ === -1) {
  184. return;
  185. }
  186. const imageElement = el.__IMAGE__!;
  187. const boxElement = el.__BOX__!;
  188. const boxSize = boxElement.__SIZE__!;
  189. const imageSize = imageElement.__SIZE__!;
  190. // no parallax
  191. if (boxSize >= imageSize) {
  192. // remove transform style
  193. imageElement.style[TRANSFORM] = "";
  194. return;
  195. }
  196. // if area's position is center, ratio is 0.
  197. // if area is hidden at the top, ratio is 1.
  198. // if area is hidden at the bottom, ratio is -1.
  199. const imagePosition = position + boxSize / 2;
  200. let ratio = (scrollPositionStart + rootSize / 2 -
  201. (rootSize + boxSize) / 2 * center - imagePosition) /
  202. (rootSize + boxSize) * 2 * strength;
  203. // if ratio is out of the range of -1 and 1, show empty space.
  204. ratio = Math.max(Math.min(ratio, range[1]), range[0]);
  205. // dist is the position when thumnail's image is centered.
  206. const dist = (boxSize - imageSize) / 2;
  207. let translate = dist * (1 - ratio);
  208. if (align === CENTER) {
  209. translate -= dist;
  210. }
  211. imageElement.__TRANSLATE__ = translate;
  212. imageElement.__RATIO__ = ratio;
  213. imageElement.style[TRANSFORM] = `translate${coordinateName}(${translate}px)`;
  214. });
  215. return this;
  216. }
  217. private _checkParallaxItem(element: IInfiniteGridItemElement) {
  218. if (!element) {
  219. return;
  220. }
  221. const selector = this.options.selector;
  222. if (!element.__IMAGE__) {
  223. const img = element.querySelector<IInfiniteGridItemElement>(selector);
  224. element.__IMAGE__ = img || -1;
  225. if (!img) {
  226. return;
  227. }
  228. element.__BOX__ = img.parentNode as IInfiniteGridItemElement;
  229. }
  230. if (element.__IMAGE__ === -1) {
  231. return;
  232. }
  233. const sizeName = this._style.cammelSize;
  234. element.__IMAGE__.__SIZE__ = element.__IMAGE__[`offset${sizeName}` as OffsetSizeType];
  235. element.__BOX__!.__SIZE__ = element.__BOX__![`offset${sizeName}` as OffsetSizeType];
  236. }
  237. }
  238. export default Parallax;
comments powered by Disqus