Source: src/layouts/FrameLayout.ts

  1. import { DUMMY_POSITION } from "../consts";
  2. import { getStyleNames, assignOptions, fill, cloneItems } from "../utils";
  3. import { ILayout, IRectlProperties, ISize, IInfiniteGridGroup, IInfiniteGridItem } from "../types";
  4. export type FrameType = number[][];
  5. export interface IFrameShape {
  6. left?: number;
  7. top?: number;
  8. type: any;
  9. width: number;
  10. height: number;
  11. index?: number;
  12. }
  13. export interface IFrameLayoutInterface {
  14. horizontal: boolean;
  15. margin: number;
  16. frame: FrameType;
  17. frameFill: boolean;
  18. itemSize: number | ISize;
  19. [key: string]: any;
  20. }
  21. /*
  22. Frame
  23. [
  24. [1, 1, 1, 1, 1],
  25. [0, 0, 2, 2, 2],
  26. [0, 0, 2, 2, 2],
  27. [3, 4, 5, 5, 5],
  28. ]
  29. */
  30. function disableFrame(
  31. frame: FrameType,
  32. type: number,
  33. top: number,
  34. left: number,
  35. width: number,
  36. height: number,
  37. ) {
  38. for (let i = top; i < top + height; ++i) {
  39. for (let j = left; j < left + width; ++j) {
  40. if (type !== frame[i][j]) {
  41. continue;
  42. }
  43. frame[i][j] = 0;
  44. }
  45. }
  46. }
  47. function searchShapeInFrame(
  48. frame: FrameType,
  49. type: number,
  50. top: number,
  51. left: number,
  52. width: number,
  53. height: number,
  54. ) {
  55. const size: IFrameShape = {
  56. left,
  57. top,
  58. type,
  59. width: 1,
  60. height: 1,
  61. };
  62. for (let i = left; i < width; ++i) {
  63. if (frame[top][i] === type) {
  64. size.width = i - left + 1;
  65. continue;
  66. }
  67. break;
  68. }
  69. for (let i = top; i < height; ++i) {
  70. if (frame[i][left] === type) {
  71. size.height = i - top + 1;
  72. continue;
  73. }
  74. break;
  75. }
  76. // After finding the shape, it will not find again.
  77. disableFrame(frame, type, top, left, size.width, size.height);
  78. return size;
  79. }
  80. function getShapes(frame: FrameType) {
  81. const height = frame.length;
  82. const width = height ? frame[0].length : 0;
  83. const shapes: IFrameShape[] = [];
  84. for (let i = 0; i < height; ++i) {
  85. for (let j = 0; j < width; ++j) {
  86. const type = frame[i][j];
  87. if (!type) {
  88. continue;
  89. }
  90. // Separate shapes with other numbers.
  91. shapes.push(searchShapeInFrame(frame, type, i, j, width, height));
  92. }
  93. }
  94. shapes.sort((a, b) => (a.type < b.type ? -1 : 1));
  95. return {
  96. shapes,
  97. width,
  98. height,
  99. };
  100. }
  101. /**
  102. * @classdesc FrameLayout is a layout that allows you to place cards in a given frame. It is a layout that corresponds to a level intermediate between the placement of the image directly by the designer and the layout using the algorithm.
  103. * @ko FrameLayout은 주어진 프레임에 맞춰 카드를 배치하는 레이아웃입니다. 디자이너가 직접 이미지를 배치하는 것과 알고리즘을 사용한 배치의 중간 정도 수준에 해당하는 레이아웃이다.
  104. * @class eg.InfiniteGrid.FrameLayout
  105. * @param {Object} [options] The option object of eg.InfiniteGrid.FrameLayout module <ko>eg.InfiniteGrid.FrameLayout 모듈의 옵션 객체</ko>
  106. * @param {String} [options.margin=0] Margin used to create space around items <ko>아이템들 사이의 공간</ko>
  107. * @param {Boolean} [options.horizontal=false] Direction of the scroll movement (false: vertical, true: horizontal) <ko>스크롤 이동 방향 (false: 세로방향, true: 가로방향)</ko>
  108. * @param {Boolean} [options.itemSize=0] The size of the items. If it is 0, it is calculated as the size of the first item in items. <ko> 아이템의 사이즈. 만약 아이템 사이즈가 0이면, 아이템들의 첫번째 아이템의 사이즈로 계산이 된다. </ko>
  109. * @param {Boolean} [options.frame=[]] The size of the items. If it is 0, it is calculated as the size of the first item in items. <ko> 아이템의 사이즈. 만약 아이템 사이즈가 0이면, 아이템들의 첫번째 아이템의 사이즈로 계산이 된다. </ko>
  110. * @param {Boolean} [options.frameFill=true] Make sure that the frame can be attached after the previous frame. <ko> 다음 프레임이 전 프레임에 이어 붙일 수 있는지 있는지 확인한다. </ko>
  111. * @example
  112. ```
  113. <script>
  114. var ig = new eg.InfiniteGrid("#grid". {
  115. horizontal: true,
  116. });
  117. ig.setLayout(eg.InfiniteGrid.FrameLayout, {
  118. margin: 10,
  119. itemSize: 200,
  120. frame: [
  121. [1, 1, 1, 1, 1],
  122. [0, 0, 2, 2, 2],
  123. [0, 0, 2, 2, 2],
  124. [3, 4, 5, 5, 5],
  125. ],
  126. });
  127. // or
  128. var layout = new eg.InfiniteGrid.FrameLayout({
  129. margin: 10,
  130. itemSize: 200,
  131. horizontal: true,
  132. frame: [
  133. [1, 1, 1, 1, 1],
  134. [0, 0, 2, 2, 2],
  135. [0, 0, 2, 2, 2],
  136. [3, 4, 5, 5, 5],
  137. ],
  138. });
  139. </script>
  140. ```
  141. **/
  142. class FrameLayout implements ILayout {
  143. public options: IFrameLayoutInterface;
  144. protected _itemSize: number | ISize;
  145. protected _shapes: {
  146. shapes: IFrameShape[],
  147. width?: number,
  148. height?: number,
  149. };
  150. protected _size: number;
  151. protected _style: IRectlProperties;
  152. constructor(options: Partial<IFrameLayoutInterface> = {}) {
  153. this.options = assignOptions({
  154. margin: 0,
  155. horizontal: false,
  156. itemSize: 0,
  157. frame: [],
  158. frameFill: true,
  159. }, options);
  160. const frame = this.options.frame.map(row => row.slice());
  161. this._itemSize = this.options.itemSize || 0;
  162. // divide frame into shapes.
  163. this._shapes = getShapes(frame);
  164. this._size = 0;
  165. this._style = getStyleNames(this.options.horizontal);
  166. }
  167. /**
  168. * Adds items of groups at the bottom of a outline.
  169. * @ko 그룹들의 아이템들을 아웃라인 아래에 추가한다.
  170. * @method eg.InfiniteGrid.FrameLayout#layout
  171. * @param {Array} groups Array of groups to be layouted <ko>레이아웃할 그룹들의 배열</ko>
  172. * @param {Array} outline Array of outline points to be reference points <ko>기준점이 되는 아웃라인 점들의 배열</ko>
  173. * @return {eg.InfiniteGrid.FrameLayout} An instance of a module itself<ko>모듈 자신의 인스턴스</ko>
  174. * @example
  175. * layout.layout(groups, [100, 200, 300, 400]);
  176. */
  177. public layout(groups: IInfiniteGridGroup[] = [], outline: number[] = []) {
  178. const length = groups.length;
  179. let point = outline;
  180. for (let i = 0; i < length; ++i) {
  181. const group = groups[i];
  182. const outlines = this._layout(group.items, point, true);
  183. group.outlines = outlines;
  184. point = outlines.end;
  185. }
  186. return this;
  187. }
  188. /**
  189. * Set the viewport size of the layout.
  190. * @ko 레이아웃의 가시 사이즈를 설정한다.
  191. * @method eg.InfiniteGrid.FrameLayout#setSize
  192. * @param {Number} size The viewport size of container area where items are added to a layout <ko>레이아웃에 아이템을 추가하는 컨테이너 영역의 가시 사이즈</ko>
  193. * @return {eg.InfiniteGrid.FrameLayout} An instance of a module itself<ko>모듈 자신의 인스턴스</ko>
  194. * @example
  195. * layout.setSize(800);
  196. */
  197. public setSize(size: number) {
  198. this._size = size;
  199. return this;
  200. }
  201. /**
  202. * Adds items at the bottom of a outline.
  203. * @ko 아이템들을 아웃라인 아래에 추가한다.
  204. * @method eg.InfiniteGrid.FrameLayout#append
  205. * @param {Array} items Array of items to be layouted <ko>레이아웃할 아이템들의 배열</ko>
  206. * @param {Array} [outline=[]] Array of outline points to be reference points <ko>기준점이 되는 아웃라인 점들의 배열</ko>
  207. * @return {Object} Layouted items and outline of start and end <ko> 레이아웃이 된 아이템과 시작과 끝의 아웃라인이 담긴 정보</ko>
  208. * @example
  209. * layout.prepend(items, [100]);
  210. */
  211. public append(items: IInfiniteGridItem[], outline?: number[], cache?: boolean) {
  212. return this._insert(items, outline, true, cache);
  213. }
  214. /**
  215. * Adds items at the top of a outline.
  216. * @ko 아이템을 아웃라인 위에 추가한다.
  217. * @method eg.InfiniteGrid.FrameLayout#prepend
  218. * @param {Array} items Array of items to be layouted <ko>레이아웃할 아이템들의 배열</ko>
  219. * @param {Array} [outline=[]] Array of outline points to be reference points <ko>기준점이 되는 아웃라인 점들의 배열</ko>
  220. * @return {Object} Layouted items and outline of start and end <ko> 레이아웃이 된 아이템과 시작과 끝의 아웃라인이 담긴 정보</ko>
  221. * @example
  222. * layout.prepend(items, [100]);
  223. */
  224. public prepend(items: IInfiniteGridItem[], outline?: number[], cache?: boolean) {
  225. return this._insert(items, outline, false, cache);
  226. }
  227. protected _getItemSize() {
  228. this._checkItemSize();
  229. return this._itemSize;
  230. }
  231. protected _checkItemSize() {
  232. if (this.options.itemSize) {
  233. this._itemSize = this.options.itemSize;
  234. return;
  235. }
  236. const style = this._style;
  237. const size = style.size2;
  238. const margin = this.options.margin;
  239. // if itemSize is not in options, caculate itemSize from size.
  240. this._itemSize = (this._size + margin) / this._shapes[size]! - margin;
  241. }
  242. protected _layout(items: IInfiniteGridItem[], outline: number[] = [], isAppend?: boolean) {
  243. const length = items.length;
  244. const style = this._style;
  245. const { margin, frameFill } = this.options;
  246. const size1Name = style.size1;
  247. const size2Name = style.size2;
  248. const pos1Name = style.startPos1;
  249. const pos2Name = style.startPos2;
  250. const itemSize = this._getItemSize();
  251. const isItemObject = typeof itemSize === "object";
  252. const itemSize2 = isItemObject ? (itemSize as ISize)[size2Name] : itemSize as number;
  253. const itemSize1 = isItemObject ? (itemSize as ISize)[size1Name] : itemSize as number;
  254. const shapesSize = this._shapes[size2Name]!;
  255. const shapes = this._shapes.shapes;
  256. const shapesLength = shapes.length;
  257. const startOutline = fill(new Array(shapesSize), DUMMY_POSITION);
  258. const endOutline = fill(new Array(shapesSize), DUMMY_POSITION);
  259. let dist = 0;
  260. let end = 0;
  261. if (!shapesLength) {
  262. return { start: outline, end: outline };
  263. }
  264. for (let i = 0; i < length; i += shapesLength) {
  265. for (let j = 0; j < shapesLength && i + j < length; ++j) {
  266. const item = items[i + j];
  267. const shape = shapes[j];
  268. const shapePos1 = shape[pos1Name]!;
  269. const shapePos2 = shape[pos2Name]!;
  270. const shapeSize1 = shape[size1Name]!;
  271. const shapeSize2 = shape[size2Name]!;
  272. const pos1 = end - dist + shapePos1 * (itemSize1 + margin);
  273. const pos2 = shapePos2 * (itemSize2 + margin);
  274. const size1 = shapeSize1 * (itemSize1 + margin) - margin;
  275. const size2 = shapeSize2 * (itemSize2 + margin) - margin;
  276. for (let k = shapePos2; k < shapePos2 + shapeSize2 && k < shapesSize; ++k) {
  277. if (startOutline[k] === DUMMY_POSITION) {
  278. startOutline[k] = pos1;
  279. }
  280. startOutline[k] = Math.min(startOutline[k], pos1);
  281. endOutline[k] = Math.max(endOutline[k], pos1 + size1 + margin);
  282. }
  283. item.rect = {
  284. [pos1Name]: pos1,
  285. [pos2Name]: pos2,
  286. [size1Name]: size1,
  287. [size2Name]: size2,
  288. } as any;
  289. }
  290. end = Math.max(...endOutline);
  291. // check dist once
  292. if (i !== 0) {
  293. continue;
  294. }
  295. // find & fill empty block
  296. if (!frameFill) {
  297. dist = 0;
  298. continue;
  299. }
  300. dist = end;
  301. for (let j = 0; j < shapesSize; ++j) {
  302. if (startOutline[j] === DUMMY_POSITION) {
  303. continue;
  304. }
  305. // the dist between frame's end outline and next frame's start outline
  306. // expect that next frame's start outline is startOutline[j] + end
  307. dist = Math.min(startOutline[j] + end - endOutline[j], dist);
  308. }
  309. }
  310. for (let i = 0; i < shapesSize; ++i) {
  311. if (startOutline[i] !== DUMMY_POSITION) {
  312. continue;
  313. }
  314. startOutline[i] = Math.max(...startOutline);
  315. endOutline[i] = startOutline[i];
  316. }
  317. // The target outline is start outline when type is appending
  318. const targetOutline = isAppend ? startOutline : endOutline;
  319. const prevOutlineEnd = outline.length === 0 ? 0 : Math[isAppend ? "max" : "min"](...outline);
  320. let prevOutlineDist = isAppend ? 0 : end;
  321. if (frameFill && outline.length === shapesSize) {
  322. prevOutlineDist = -DUMMY_POSITION;
  323. for (let i = 0; i < shapesSize; ++i) {
  324. if (startOutline[i] === endOutline[i]) {
  325. continue;
  326. }
  327. // if appending type is prepend(false), subtract dist from appending group's height.
  328. prevOutlineDist = Math.min(targetOutline[i] + prevOutlineEnd - outline[i], prevOutlineDist);
  329. }
  330. }
  331. for (let i = 0; i < shapesSize; ++i) {
  332. startOutline[i] += prevOutlineEnd - prevOutlineDist;
  333. endOutline[i] += prevOutlineEnd - prevOutlineDist;
  334. }
  335. items.forEach(item => {
  336. item.rect[pos1Name] += prevOutlineEnd - prevOutlineDist;
  337. });
  338. return {
  339. start: startOutline.map(point => parseInt(point, 10)),
  340. end: endOutline.map(point => parseInt(point, 10)),
  341. };
  342. }
  343. private _insert(items: IInfiniteGridItem[] = [], outline: number[] = [], isAppend?: boolean, cache?: boolean) {
  344. // this only needs the size of the item.
  345. const clone = cache ? items : cloneItems(items);
  346. return {
  347. items: clone,
  348. outlines: this._layout(clone, outline, isAppend),
  349. };
  350. }
  351. }
  352. export default FrameLayout;
comments powered by Disqus