Source: src/Parallax.ts

import { ALIGN, isMobile, TRANSFORM } from "./consts";
import { $, isWindow, assign } from "./utils";
import { IAlign, IJQuery, PositionType, SizeType, InnerSizeType, ClientSizeType, IInfiniteGridItemElement, OffsetSizeType, IInfiniteGridItem } from "./types";

export interface IParallaxStyle {
	position: PositionType;
	size: SizeType;
	cammelSize: string;
	coordinate: string;
}
const style: {
	vertical: IParallaxStyle;
	horizontal: IParallaxStyle;
} = {
	vertical: { position: "top", size: "height", cammelSize: "Height", coordinate: "Y" },
	horizontal: { position: "left", size: "width", cammelSize: "Width", coordinate: "X" },
};
const { START, CENTER } = ALIGN;

/**
 * @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.
 * @ko Parallax는 서로 다른 두 개의 시선에서 바라본 물체의 외관상 위치의 변위 또는 차이입니다. 스크롤에 따라 이미지와 아이템의 속도를 차이를 줌으로써 parallax을 적용할 수 있습니다.
 * @class eg.Parallax
 * @param {Element|String} [root=window] Scrolling target. If you scroll in the body, set window. 스크롤하는 대상. 만약 body에서 스크롤하면 window로 설정한다.
 * @param {Object} [options] The option object of eg.Parallax module <ko>eg.Parallax 모듈의 옵션 객체</ko>
 * @param {Boolean} [options.horizontal=false] Direction of the scroll movement (false: vertical, true: horizontal) <ko>스크롤 이동 방향 (false: 세로방향, true: 가로방향)</ko>
 * @param {Element|String} [options.container=null] Container wrapping items. If root and container have no gaps, do not set option. <ko> 아이템들을 감싸고 있는 컨테이너. 만약 root와 container간의 차이가 없으면, 옵션을 설정하지 않아도 된다.</ko>
 * @param {String} [options.selector="img"] The selector of the image to apply the parallax in the item <ko> 아이템안에 있는 parallax를 적용할 이미지의 selector </ko>
 * @param {Boolean} [options.strength=1] Dimensions that indicate the sensitivity of parallax. The higher the strength, the faster.
 * @param {Boolean} [options.center=0] The middle point of parallax. The top is 1 and the bottom is -1. <ko> parallax가 가운데로 오는 점. 상단이 1이고 하단이 -1이다. </ko>
 * @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>
 * @param {Boolean} [options.align="start"] The alignment of the image in the item. ("start" : top or left, "center": middle) <ko> 아이템안의 이미지의 정렬 </ko>
 * @example
```
<script>
// isOverflowScroll: false
var parallax = new eg.Parallax(window, {
  container: ".container",
  selector: "img.parallax",
  strength: 0.8,
  center: 0,
  range: [-1, 1],
  align: "center",
  horizontal: true,
});

// isOverflowScroll: ture
var parallax = new eg.Parallax(".container", {
  selector: "img.parallax",
  strength: 0.8,
  center: 0,
  range: [-1, 1],
  align: "center",
  horizontal: true,
});

// item interface
var item = {
  // original size
  size: {
    width: 100,
    height: 100,
  },
  // view size
  rect: {
    top: 100,
    left: 100,
    width: 100,
    height: 100,
  }
};
</script>
```
 **/
class Parallax {
	public options: {
		container: HTMLElement;
		selector: string;
		strength: number;
		center: number;
		range: number[];
		align: IAlign[keyof IAlign];
		horizontal: boolean;
	};
	private _root: Window | HTMLElement;
	private _container: HTMLElement;
	private _rootSize: number;
	private _containerPosition: number;
	private _style: IParallaxStyle;
	constructor(
		root: Window | HTMLElement | IJQuery | string = window,
		options: Partial<Parallax["options"]> = {}) {
		this.options = assign({
			container: null,
			selector: "img",
			strength: 1,
			center: 0,
			range: [-1, 1],
			align: START,
			horizontal: false,
		}, options);
		this._root = $(root);
		this._container = this.options.container && $(this.options.container);
		this._rootSize = 0;
		this._containerPosition = 0;
		this._style = style[this.options.horizontal ? "horizontal" : "vertical"];
		this.resize();
	}
	/**
	 * As the browser is resized, the gaps between the root and the container and the size of the items are updated.
	 * @ko 브라우저의 크기가 변경됨으로 써 root와 container의 간격과 아이템들의 크기를 갱신한다.
	 * @method eg.Parallax#resize
	 * @param {Array} [items = []] Items to apply parallax. It does not apply if it is not in visible range. <ko>parallax를 적용할 아이템들. 가시거리에 존재하지 않으면 적용이 안된다.</ko>
	 * @return {eg.Parallax} An instance of a module itself<ko>모듈 자신의 인스턴스</ko>
	 * @example
  ```js
  window.addEventListener("resize", function (e) {
	parallax.resize(items);
  });
  ```
	 */
	public resize(items: IInfiniteGridItem[] = []) {
		const root = this._root;
		const container = this._container;
		const positionName = this._style.position;
		const sizeName = this._style.cammelSize;

		if (!container || root === container) {
			this._containerPosition = 0;
		} else {
			const rootRect = (isWindow(root) ? document.body : root).getBoundingClientRect();
			const containertRect = container.getBoundingClientRect();

			this._containerPosition = containertRect[positionName] - rootRect[positionName];
		}
		this._rootSize = isWindow(root) ?
			window[`inner${sizeName}` as InnerSizeType] ||
			document.documentElement[`client${sizeName}` as ClientSizeType] :
			root[`client${sizeName}` as ClientSizeType];

		if (isMobile && isWindow(root)) {
			const bodyWidth = document.body.offsetWidth || document.documentElement.offsetWidth;
			const windowWidth = window.innerWidth;

			this._rootSize = this._rootSize / (bodyWidth / windowWidth);
		}
		items.forEach(item => {
			this._checkParallaxItem(item.el!);
		});

		return this;
	}
	/**
	 * Scrolls the image in the item by a parallax.
	 * @ko 스크롤하면 아이템안의 이미지를 시차적용시킨다.
	 * @method eg.Parallax#refresh
	 * @param {Array} [items = []] Items to apply parallax. It does not apply if it is not in visible range. <ko>parallax를 적용할 아이템들. 가시거리에 존재하지 않으면 적용이 안된다.</ko>
	 * @param {Number} [scrollPositionStart = 0] The scroll position.
	 * @return {eg.Parallax} An instance of a module itself<ko>모듈 자신의 인스턴스</ko>
	 * @example
  ```js
  document.body.addEventListener("scroll", function (e) {
	parallax.refresh(items, e.scrollTop);
  });
  ```
	 */
	public refresh(items: IInfiniteGridItem[] = [], scrollPositionStart = 0) {
		const styleNames = this._style;
		const positionName = styleNames.position;
		const coordinateName = styleNames.coordinate;
		const sizeName = styleNames.size;
		const options = this.options;
		const { strength, center, range, align } = options;
		const rootSize = this._rootSize;
		const scrollPositionEnd = scrollPositionStart + rootSize;
		const containerPosition = this._containerPosition;

		items.forEach(item => {
			if (!item.rect || !item.size || !item.el) {
				return;
			}
			const position = containerPosition + item.rect[positionName];
			const itemSize = item.rect[sizeName] || item.size[sizeName];

			// check item is in container.
			if (scrollPositionStart > position + itemSize ||
				scrollPositionEnd < position) {
				return;
			}
			const el = item.el;

			if (!el.__IMAGE__) {
				this._checkParallaxItem(el);
			}
			if (el.__IMAGE__ === -1) {
				return;
			}
			const imageElement = el.__IMAGE__!;
			const boxElement = el.__BOX__!;
			const boxSize = boxElement.__SIZE__!;
			const imageSize = imageElement.__SIZE__!;

			// no parallax
			if (boxSize >= imageSize) {
				// remove transform style
				imageElement.style[TRANSFORM] = "";
				return;
			}

			// if area's position is center, ratio is 0.
			// if area is hidden at the top, ratio is 1.
			// if area is hidden at the bottom, ratio is -1.
			const imagePosition = position + boxSize / 2;
			let ratio = (scrollPositionStart + rootSize / 2 -
				(rootSize + boxSize) / 2 * center - imagePosition) /
				(rootSize + boxSize) * 2 * strength;

			// if ratio is out of the range of -1 and 1, show empty space.
			ratio = Math.max(Math.min(ratio, range[1]), range[0]);

			// dist is the position when thumnail's image is centered.
			const dist = (boxSize - imageSize) / 2;
			let translate = dist * (1 - ratio);

			if (align === CENTER) {
				translate -= dist;
			}

			imageElement.__TRANSLATE__ = translate;
			imageElement.__RATIO__ = ratio;
			imageElement.style[TRANSFORM] = `translate${coordinateName}(${translate}px)`;
		});
		return this;
	}
	private _checkParallaxItem(element: IInfiniteGridItemElement) {
		if (!element) {
			return;
		}
		const selector = this.options.selector;

		if (!element.__IMAGE__) {
			const img = element.querySelector<IInfiniteGridItemElement>(selector);

			element.__IMAGE__ = img || -1;
			if (!img) {
				return;
			}
			element.__BOX__ = img.parentNode as IInfiniteGridItemElement;
		}
		if (element.__IMAGE__ === -1) {
			return;
		}
		const sizeName = this._style.cammelSize;

		element.__IMAGE__.__SIZE__ = element.__IMAGE__[`offset${sizeName}` as OffsetSizeType];
		element.__BOX__!.__SIZE__ = element.__BOX__![`offset${sizeName}` as OffsetSizeType];
	}
}

export default Parallax;
comments powered by Disqus