From c0f14de6650911ad7838b9114e6373a3270b470a Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Mon, 17 Oct 2022 12:18:05 -0700 Subject: [PATCH] incomplete: non-uniform mobile layout (#73) --- src/components/Timeline.vue | 27 +++---- src/services/Layout.ts | 157 ++++++++++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+), 14 deletions(-) create mode 100644 src/services/Layout.ts diff --git a/src/components/Timeline.vue b/src/components/Timeline.vue index 3e133962..220d4ec0 100644 --- a/src/components/Timeline.vue +++ b/src/components/Timeline.vue @@ -103,9 +103,9 @@ import GlobalMixin from '../mixins/GlobalMixin'; import moment from 'moment'; import { ViewerManager } from "../services/Viewer"; +import { getLayout } from "../services/Layout"; import * as dav from "../services/DavRequests"; import * as utils from "../services/Utils"; -import justifiedLayout from "justified-layout"; import axios from '@nextcloud/axios' import Folder from "./frame/Folder.vue"; import Tag from "./frame/Tag.vue"; @@ -715,17 +715,16 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) { } // Create justified layout with correct params - const justify = justifiedLayout(day.detail.map(p => { + const justify = getLayout(day.detail.map(p => { return { - width: (this.squareMode ? null : p.w) || this.rowHeight, - height: (this.squareMode ? null : p.h) || this.rowHeight, + width: p.w || this.rowHeight, + height: p.h || this.rowHeight, }; }), { - containerWidth: this.rowWidth, - containerPadding: 0, - boxSpacing: 0, - targetRowHeight: this.rowHeight, - targetRowHeightTolerance: 0.1, + rowWidth: this.rowWidth, + rowHeight: this.rowHeight, + squareMode: this.squareMode, + numCols: this.numCols, }); // Check if some rows were added @@ -742,7 +741,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) { const scrollY = this.getScrollY(); // Previous justified row - let prevJustifyTop = justify.boxes[0]?.top || 0; + let prevJustifyTop = justify[0]?.top || 0; // Add all rows let dataIdx = 0; @@ -756,7 +755,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) { } // Go to the next row - const jbox = justify.boxes[dataIdx]; + const jbox = justify[dataIdx]; if (jbox.top !== prevJustifyTop) { prevJustifyTop = jbox.top; rowIdx++; @@ -765,11 +764,11 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) { // Set row height const row = this.list[rowIdx]; - const jH = Math.round(jbox.height); + const jH = this.squareMode ? this.rowHeight : Math.round(jbox.height); const delta = jH - row.size; // If the difference is too small, it's not worth risking an adjustment // especially on square layouts on mobile. Also don't do this if animating. - if (Math.abs(delta) > 5 && !isAnimating) { + if (!isAnimating && Math.abs(delta) > 5) { rowSizeDelta += delta; row.size = jH; } @@ -788,8 +787,8 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) { const setPos = () => { photo.dispWp = utils.round(jbox.width / this.rowWidth, 4, true); photo.dispXp = utils.round(jbox.left / this.rowWidth, 4, true); + photo.dispH = this.squareMode ? utils.roundHalf(jbox.height) : 0; photo.dispY = 0; - photo.dispH = 0; photo.dispRowNum = row.num; }; if (photo.dispWp !== undefined) { // photo already displayed: animate diff --git a/src/services/Layout.ts b/src/services/Layout.ts new file mode 100644 index 00000000..b0bfd636 --- /dev/null +++ b/src/services/Layout.ts @@ -0,0 +1,157 @@ +import justifiedLayout from "justified-layout"; + +/** + * Generate the layout matrix. + * + * If we are in square mode, do this manually to get non-uniformity. + * Otherwise, use flickr/justified-layout (at least for now). + */ +export function getLayout( + input: { width: number, height: number }[], + opts: { + rowWidth: number, + rowHeight: number, + squareMode: boolean, + numCols: number, + } +): { + top: number, + left: number, + width: number, + height: number, +}[] { + if (!opts.squareMode) { + return justifiedLayout((input), { + containerPadding: 0, + boxSpacing: 0, + containerWidth: opts.rowWidth, + targetRowHeight: opts.rowHeight, + targetRowHeightTolerance: 0.1, + }).boxes; + } + + // Binary flags + const FLAG_USE = 1; + const FLAG_USED = 2; + const FLAG_USE4 = 4; + + // Create 2d matrix to work in + const origRowLen = Math.ceil(input.length / opts.numCols); + const matrix: number[][] = new Array(origRowLen * 3); // todo: dynamic length + for (let i = 0; i < matrix.length; i++) { + matrix[i] = new Array(opts.numCols).fill(0); + } + + // Useful for debugging + const printMatrix = () => { + let str = ''; + for (let i = 0; i < matrix.length; i++) { + const rstr = matrix[i].map(v => v.toString(2).padStart(4, '0')).join(' '); + str += i.toString().padStart(2) + ' | ' + rstr + '\n'; + } + console.log(str); + } + + // Fill in the matrix + let row = 0; + let col = 0; + let photoId = 0; + while (photoId < input.length) { + // Check if we reached the end of row + if (col >= opts.numCols) { + row++; col = 0; + } + + // Check if already used + if (matrix[row][col] & FLAG_USED) { + col++; continue; + } + + // Use this slot + matrix[row][col] |= FLAG_USE; + photoId++; + + // Check if previous row has something used + // or something beside this is used + // We don't do these one after another + if ((row > 0 && matrix[row-1].some(v => v & FLAG_USED)) || + (col > 0 && matrix[row][col-1] & FLAG_USED) || + (col < opts.numCols-1 && matrix[row][col+1] & FLAG_USED) + ) { + col++; continue; + } + + // Check if we can use 4 blocks + let canUse4 = + // We have enough space + (row + 1 < matrix.length && col+1 < opts.numCols) && + // Nothing used in vicinity (redundant check) + !(matrix[row+1][col] & FLAG_USED) && + !(matrix[row][col+1] & FLAG_USED) && + !(matrix[row+1][col+1] & FLAG_USED) && + // This cannot end up being a widow (conservative) + (input.length-photoId-1 >= ((opts.numCols-col-2) + (opts.numCols-2))); + + // Use four with 60% probability + if (canUse4 && Math.random() < 0.6) { + matrix[row][col] |= FLAG_USE4; + matrix[row+1][col] |= FLAG_USED; + matrix[row][col+1] |= FLAG_USED; + matrix[row+1][col+1] |= FLAG_USED; + } + + // Go ahead + col++; + } + + // REMOVE BEFORE PUSH + if (input.length == 10) + printMatrix(); + + // Square layout matrix + const absMatrix: { + top: number, + left: number, + width: number, + height: number, + }[] = []; + + let currTop = 0; + row = 0; col = 0; photoId = 0; + while (photoId < input.length) { + // Check if we reached the end of row + if (col >= opts.numCols) { + row++; col = 0; + currTop += opts.rowHeight; + continue; + } + + // Skip if used + if (!(matrix[row][col] & FLAG_USE)) { + col++; continue; + } + + // Create basic object + const sqsize = opts.rowHeight; + const p = { + top: currTop, + left: col * sqsize, + width: sqsize, + height: sqsize, + } + + // Use twice the space + if (matrix[row][col] & FLAG_USE4) { + p.width *= 2; + p.height *= 2; + col += 2; + } else { + col += 1; + } + + absMatrix.push(p); + photoId++; + } + + return absMatrix; +} \ No newline at end of file