UNPKG

@egjs/grid

Version:

A component that can arrange items according to the type of grids

326 lines (296 loc) 11.9 kB
/** * egjs-grid * Copyright (c) 2021-present NAVER Corp. * MIT license */ import Grid from "../Grid"; import { PROPERTY_TYPE } from "../consts"; import { GridOptions, Properties, GridOutlines } from "../types"; import { GetterSetter } from "../utils"; import { GridItem } from "../GridItem"; import BoxModel from "./lib/BoxModel"; function getCost(originLength: number, length: number) { let cost = originLength / length; if (cost < 1) { cost = 1 / cost; } return cost - 1; } function fitArea( item: BoxModel, bestFitArea: BoxModel, itemFitSize: { inlineSize: number, contentSize: number }, containerFitSize: { inlineSize: number, contentSize: number }, isContentDirection: boolean, ) { item.contentSize = itemFitSize.contentSize; item.inlineSize = itemFitSize.inlineSize; bestFitArea.contentSize = containerFitSize.contentSize; bestFitArea.inlineSize = containerFitSize.inlineSize; if (isContentDirection) { item.contentPos = bestFitArea.contentPos + bestFitArea.contentSize; item.inlinePos = bestFitArea.inlinePos; } else { item.inlinePos = bestFitArea.inlinePos + bestFitArea.inlineSize; item.contentPos = bestFitArea.contentPos; } } /** * @typedef * @memberof Grid.PackingGrid * @extends Grid.GridOptions */ export interface PackingGridOptions extends GridOptions { /** * The aspect ratio (inlineSize / contentSize) of the container with items. * <ko>아이템들을 가진 컨테이너의 종횡비(inlineSize / contentSize).</ko> * @default 1 */ aspectRatio?: number; /** * The size weight when placing items. * <ko>아이템들을 배치하는데 사이즈 가중치.</ko> * @default 1 */ sizeWeight?: number; /** * The weight to keep ratio when placing items. * <ko>아이템들을 배치하는데 비율을 유지하는 가중치.</ko> * @default 1 */ ratioWeight?: number; /** * The priority that determines the weight of the item. "size" = (sizeWieght: 100, ratioWeight: 1), "ratio" = (sizeWeight: 1, ratioWeight; 100), "custom" = (set sizeWeight, ratioWeight) * item's weight = item's ratio(inlineSize / contentSize) change * `ratioWeight` + size(inlineSize * contentSize) change * `sizeWeight`. * <ko> 아이템의 가중치를 결정하는 우선수치. "size" = (sizeWieght: 100, ratioWeight: 1), "ratio" = (sizeWeight: 1, ratioWeight; 100), "custom" = (set sizeWeight, ratioWeight). 아이템의 가중치 = ratio(inlineSize / contentSize)의 변화량 * `ratioWeight` + size(inlineSize * contentSize)의 변화량 * `sizeWeight`.</ko> * @default "custom" */ weightPriority?: "size" | "ratio" | "custom"; } /** * The PackingGrid is a grid that shows the important items bigger without sacrificing the weight of the items. * Rows and columns are separated so that items are dynamically placed within the horizontal and vertical space rather than arranged in an orderly fashion. * If `sizeWeight` is higher than `ratioWeight`, the size of items is preserved as much as possible. * Conversely, if `ratioWeight` is higher than `sizeWeight`, the ratio of items is preserved as much as possible. * @ko PackingGrid는 아이템의 본래 크기에 따른 비중을 해치지 않으면서 중요한 카드는 더 크게 보여 주는 레이아웃이다. * 행과 열이 구분돼 아이템을 정돈되게 배치하는 대신 가로세로 일정 공간 내에서 동적으로 아이템을 배치한다. * `sizeWeight`가 `ratioWeight`보다 높으면 아이템들의 size가 최대한 보존이 된다. * 반대로 `ratioWeight`가 `sizeWeight`보다 높으면 아이템들의 비율이 최대한 보존이 된다. * @memberof Grid * @param {HTMLElement | string} container - A base element for a module <ko>모듈을 적용할 기준 엘리먼트</ko> * @param {Grid.PackingGrid.PackingGridOptions} options - The option object of the PackingGrid module <ko>PackingGrid 모듈의 옵션 객체</ko> */ @GetterSetter export class PackingGrid extends Grid<PackingGridOptions> { public static propertyTypes = { ...Grid.propertyTypes, aspectRatio: PROPERTY_TYPE.RENDER_PROPERTY, sizeWeight: PROPERTY_TYPE.RENDER_PROPERTY, ratioWeight: PROPERTY_TYPE.RENDER_PROPERTY, weightPriority: PROPERTY_TYPE.RENDER_PROPERTY, }; public static defaultOptions: Required<PackingGridOptions> = { ...Grid.defaultOptions, aspectRatio: 1, sizeWeight: 1, ratioWeight: 1, weightPriority: "custom", }; public applyGrid(items: GridItem[], direction: "start" | "end", outline: number[]): GridOutlines { const { aspectRatio } = this.options; const containerInlineSize = this.getContainerInlineSize(); const containerContentSize = containerInlineSize / aspectRatio; const inlineGap = this.getInlineGap(); const contentGap = this.getContentGap(); const prevOutline = outline.length ? outline : [0]; const startPoint = direction === "end" ? Math.max(...prevOutline) : Math.min(...prevOutline) - containerContentSize - contentGap; const endPoint = startPoint + containerContentSize + contentGap; const container = new BoxModel({}); items.forEach((item) => { const model = new BoxModel({ inlineSize: item.orgInlineSize, contentSize: item.orgContentSize, orgInlineSize: item.orgInlineSize, orgContentSize: item.orgContentSize, }); this._findBestFitArea(container, model); container.push(model); container.scaleTo(containerInlineSize + inlineGap, containerContentSize + contentGap); }); items.forEach((item, i) => { const boxItem = container.items[i]; const inlineSize = boxItem.inlineSize - inlineGap; const contentSize = boxItem.contentSize - contentGap; const contentPos = startPoint + boxItem.contentPos; const inlinePos = boxItem.inlinePos; item.setCSSGridRect({ inlinePos, contentPos, inlineSize, contentSize, }); }); return { start: [startPoint], end: [endPoint], }; } private _findBestFitArea(container: BoxModel, item: BoxModel) { if (container.getRatio() === 0) { // 아이템 최초 삽입시 전체영역 지정 container.orgInlineSize = item.inlineSize; container.orgContentSize = item.contentSize; container.inlineSize = item.inlineSize; container.contentSize = item.contentSize; return; } let bestFitArea!: BoxModel; let minCost = Infinity; let isContentDirection = false; const itemFitSize = { inlineSize: 0, contentSize: 0, }; const containerFitSize = { inlineSize: 0, contentSize: 0, }; const sizeWeight = this._getWeight("size"); const ratioWeight = this._getWeight("ratio"); container.items.forEach((child) => { const containerSizeCost = getCost(child.getOrgSizeWeight(), child.getSize()) * sizeWeight; const containerRatioCost = getCost(child.getOrgRatio(), child.getRatio()) * ratioWeight; const inlineSize = child.inlineSize; const contentSize = child.contentSize; for (let i = 0; i < 2; ++i) { let itemInlineSize; let itemContentSize; let containerInlineSize; let containerContentSize; if (i === 0) { // add item to content pos (top, bottom) itemInlineSize = inlineSize; itemContentSize = contentSize * (item.contentSize / (child.orgContentSize + item.contentSize)); containerInlineSize = inlineSize; containerContentSize = contentSize - itemContentSize; } else { // add item to inline pos (left, right) itemContentSize = contentSize; itemInlineSize = inlineSize * (item.inlineSize / (child.orgInlineSize + item.inlineSize)); containerContentSize = contentSize; containerInlineSize = inlineSize - itemInlineSize; } const itemSize = itemInlineSize * itemContentSize; const itemRatio = itemInlineSize / itemContentSize; const containerSize = containerInlineSize * containerContentSize; const containerRatio = containerContentSize / containerContentSize; let cost = getCost(item.getSize(), itemSize) * sizeWeight; cost += getCost(item.getRatio(), itemRatio) * ratioWeight; cost += getCost(child.getOrgSizeWeight(), containerSize) * sizeWeight - containerSizeCost; cost += getCost(child.getOrgRatio(), containerRatio) * ratioWeight - containerRatioCost; if (cost === Math.min(cost, minCost)) { minCost = cost; bestFitArea = child; isContentDirection = (i === 0); itemFitSize.inlineSize = itemInlineSize; itemFitSize.contentSize = itemContentSize; containerFitSize.inlineSize = containerInlineSize; containerFitSize.contentSize = containerContentSize; } } }); fitArea(item, bestFitArea, itemFitSize, containerFitSize, isContentDirection); } public getComputedOutlineLength() { return 1; } public getComputedOutlineSize() { return this.getContainerInlineSize(); } private _getWeight(type: "size" | "ratio"): number { const options = this.options; const weightPriority = options.weightPriority; if (weightPriority === type) { return 100; } else if (weightPriority === "custom") { return options[`${type}Weight`]; } return 1; } } export interface PackingGrid extends Properties<typeof PackingGrid> { } /** * The aspect ratio (inlineSize / contentSize) of the container with items. * @ko 아이템들을 가진 컨테이너의 종횡비(inlineSize / contentSize). * @name Grid.PackingGrid#aspectRatio * @type {$ts:Grid.PackingGrid.PackingGridOptions["aspectRatio"]} * @default 1 * @example * ```js * import { PackingGrid } from "@egjs/grid"; * * const grid = new PackingGrid(container, { * aspectRatio: 1, * }); * * grid.aspectRatio = 1.5; * ``` */ /** * The priority that determines the weight of the item. "size" = (sizeWieght: 2, ratioWeight: 1), "ratio" = (sizeWeight: 1, ratioWeight; 2), "custom" = (set sizeWeight, ratioWeight) * item's weight = item's ratio(inlineSize / contentSize) change * `ratioWeight` + size(inlineSize * contentSize) change * `sizeWeight`. * @ko 아이템의 가중치를 결정하는 우선수치. "size" = (sizeWieght: 2, ratioWeight: 1), "ratio" = (sizeWeight: 1, ratioWeight; 2), "custom" = (set sizeWeight, ratioWeight). 아이템의 가중치 = ratio(inlineSize / contentSize)의 변화량 * `ratioWeight` + size(inlineSize * contentSize)의 변화량 * `sizeWeight`. * @name Grid.PackingGrid#weightPriority * @type {$ts:Grid.PackingGrid.PackingGridOptions["weightPriority"]} * @default "custom" * @example * ```js * import { PackingGrid } from "@egjs/grid"; * * const grid = new PackingGrid(container, { * weightPriority: "custom", * sizeWeight: 1, * ratioWeight: 1, * }); * * grid.weightPriority = "size"; * // or * grid.weightPriority = "ratio"; * ``` */ /** * The size weight when placing items. * @ko 아이템들을 배치하는데 사이즈 가중치. * @name Grid.PackingGrid#sizeWeight * @type {$ts:Grid.PackingGrid.PackingGridOptions["sizeWeight"]} * @default 1 * @example * ```js * import { PackingGrid } from "@egjs/grid"; * * const grid = new PackingGrid(container, { * sizeWeight: 1, * }); * * grid.sizeWeight = 10; * ``` */ /** * The weight to keep ratio when placing items. * @ko 아이템들을 배치하는데 비율을 유지하는 가중치. * @name Grid.PackingGrid#ratioWeight * @type {$ts:Grid.PackingGrid.PackingGridOptions["ratioWeight"]} * @default 1 * @example * ```js * import { PackingGrid } from "@egjs/grid"; * * const grid = new PackingGrid(container, { * ratioWeight: 1, * }); * * grid.ratioWeight = 10; * ``` */