diff --git a/src/components/Timeline.vue b/src/components/Timeline.vue index 2484e273..6fffa9b2 100644 --- a/src/components/Timeline.vue +++ b/src/components/Timeline.vue @@ -53,8 +53,12 @@ class="photo-row" :style="{ height: item.size + 'px', width: rowWidth + 'px' }"> -
+
@@ -331,7 +336,9 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) { row.photos[j] = { flag: this.c.FLAG_PLACEHOLDER, fileid: Math.random(), - dispWp: utils.round(100 / this.numCols, 2, true), + dispWp: utils.round(1 / this.numCols, 4, true), + dispXp: utils.round(j / this.numCols, 4, true), + dispY: 0, }; } delete row.pct; @@ -534,6 +541,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) { // Create header for this day const head: IHeadRow = { id: `${day.dayid}-head`, + num: -1, size: 40, type: IRowType.HEAD, selected: false, @@ -642,6 +650,8 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) { * @param isAnimating prevents glitches due to height changes */ processDay(dayId: number, data: IPhoto[], isAnimating=false) { + if (!data) return; + const head = this.heads[dayId]; const day = head.day; this.loadedDays.add(dayId); @@ -657,6 +667,11 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) { day.count = data.length; day.detail = data; + // Reset rows including placeholders + for (const row of head.day.rows || []) { + row.photos = []; + } + // Create justified layout with correct params const justify = justifiedLayout(day.detail.map(p => { return { @@ -671,13 +686,6 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) { targetRowHeightTolerance: 0.1, }); - // Reset rows including placeholders - if (head.day?.rows) { - for (const row of head.day.rows) { - row.photos = []; - } - } - // Check if some rows were added let addedRows: IRow[] = []; @@ -735,7 +743,33 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) { utils.convertFlags(photo); // Get aspect ratio - photo.dispWp = utils.round(100 * jbox.width / this.rowWidth, 2, true); + const setPos = () => { + photo.dispWp = utils.round(jbox.width / this.rowWidth, 4, true); + photo.dispXp = utils.round(jbox.left / this.rowWidth, 4, true); + photo.dispY = 0; + photo.dispH = 0; + photo.dispRowNum = row.num; + }; + if (photo.dispWp !== undefined) { // photo already displayed: animate + window.setTimeout(setPos, 50); + + if (photo.dispRowNum !== undefined && + photo.dispRowNum !== row.num && + photo.dispRowNum >= 0 && + photo.dispRowNum < day.rows.length + ) { // Row change animation + const start = Math.min(photo.dispRowNum, row.num); + const end = Math.max(photo.dispRowNum, row.num); + const sizeDelta = day.rows.slice(start, end).reduce((acc, r) => { + acc += r.size; + return acc; + }, 0); + photo.dispY = sizeDelta * (photo.dispRowNum < row.num ? -1 : 1); + photo.dispH = day.rows[photo.dispRowNum].size; + } + } else { + setPos(); + } // Move to next index of photo dataIdx++; @@ -807,6 +841,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) { // Create new row const row = { id: `${day.dayid}-${day.rows.length}`, + num: day.rows.length, photos: [], type: rowType, size: this.rowHeight, @@ -831,8 +866,6 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) { /** * Delete elements from main view with some animation - * This function looks horribly slow, probably isn't that bad - * in all practical scenarios. * * This is also going to update day.detail for you and make * a call to processDay so just pass it the list of ids to @@ -860,44 +893,11 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) { // clear selection at this point this.selectionManager.clearSelection(delPhotos); - // Speculate day reflow for animation - const exitedLeft = new Set(); - for (const day of updatedDays) { - let nextExit = false; - for (const row of day.rows) { - for (const photo of row.photos) { - if (photo.flag & this.c.FLAG_LEAVING) { - nextExit = true; - } else if (nextExit) { - photo.flag |= this.c.FLAG_EXIT_LEFT; - exitedLeft.add(photo); - } - } - } - } - - // wait for 200ms - await new Promise(resolve => setTimeout(resolve, 200)); - // Reflow all touched days for (const day of updatedDays) { const newDetail = day.detail.filter(p => !delPhotosSet.has(p)); this.processDay(day.dayid, newDetail, true); } - - // Enter from right all photos that exited left - exitedLeft.forEach((photo: any) => { - photo.flag &= ~this.c.FLAG_EXIT_LEFT; - photo.flag |= this.c.FLAG_ENTER_RIGHT; - }); - - // wait for 200ms - await new Promise(resolve => setTimeout(resolve, 200)); - - // Clear enter right flags - exitedLeft.forEach((photo: any) => { - photo.flag &= ~this.c.FLAG_ENTER_RIGHT; - }); } } @@ -926,12 +926,14 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) { } .photo-row > .photo { - display: inline-block; - position: relative; + display: block; + position: absolute; + top: 0; left: 0; cursor: pointer; - vertical-align: top; height: 100%; - transition: width 0.2s ease-in-out; // reflow justification + transition: width 0.2s ease-in-out, + height 0.2s ease-in-out, + transform 0.2s ease-in-out; // reflow } .head-row { diff --git a/src/components/frame/Photo.vue b/src/components/frame/Photo.vue index e160f847..fd5da80d 100644 --- a/src/components/frame/Photo.vue +++ b/src/components/frame/Photo.vue @@ -4,8 +4,6 @@ 'selected': (data.flag & c.FLAG_SELECTED), 'placeholder': (data.flag & c.FLAG_PLACEHOLDER), 'leaving': (data.flag & c.FLAG_LEAVING), - 'exit-left': (data.flag & c.FLAG_EXIT_LEFT), - 'enter-right': (data.flag & c.FLAG_ENTER_RIGHT), 'error': (data.flag & c.FLAG_LOAD_FAIL), }"> @@ -70,9 +68,6 @@ export default class Photo extends Mixins(GlobalMixin) { return undefined; } else if (this.data.flag & this.c.FLAG_LOAD_FAIL) { return errorsvg; - } else if (this.data.flag & this.c.FLAG_FORCE_RELOAD) { - this.data.flag &= ~this.c.FLAG_FORCE_RELOAD; - return undefined; } else { return this.url; } @@ -211,19 +206,6 @@ export default class Photo extends Mixins(GlobalMixin) { transform: scale(0.9); opacity: 0; } - &.exit-left { - transition: all 0.2s ease-in; - transform: translateX(-20%); - opacity: 0.8; - } - &.enter-right { - animation: enter-right 0.2s ease-out forwards; - } -} - -@keyframes enter-right { - from { transform: translateX(20%); opacity: 0.8; } - to { transform: translateX(0); opacity: 1; } } // Distance of icon from border diff --git a/src/types.ts b/src/types.ts index f32f7193..039f1248 100644 --- a/src/types.ts +++ b/src/types.ts @@ -45,6 +45,14 @@ export type IPhoto = { h?: number; /** Grid display width percentage */ dispWp?: number; + /** Grid display height (forced) */ + dispH?: number; + /** Grid display X percentage */ + dispXp?: number; + /** Grid display Y px */ + dispY?: number; + /** Grid display row id (relative to head) */ + dispRowNum?: number; /** Reference to day object */ d?: IDay; /** Video flag from server */ @@ -84,6 +92,8 @@ export interface ITag extends IPhoto { export type IRow = { /** Vue Recycler identifier */ id?: string; + /** Row ID from head */ + num: number; /** Day ID */ dayId: number; /** Refrence to day object */