Make scroller non-linear

old-stable24
Varun Patil 2022-10-20 23:48:28 -07:00
parent fc0b19dbf9
commit eacd1e19f7
2 changed files with 122 additions and 91 deletions

View File

@ -74,7 +74,7 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
/** View size reflow timer */ /** View size reflow timer */
private reflowRequest = false; private reflowRequest = false;
/** Tick adjust timer */ /** Tick adjust timer */
private adjustTimer = null as number | null; private adjustRequest = false;
/** Get the visible ticks */ /** Get the visible ticks */
get visibleTicks() { get visibleTicks() {
@ -101,7 +101,7 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
} }
/** Recycler scroll event, must be called by timeline */ /** Recycler scroll event, must be called by timeline */
public recyclerScrolled(event?: any) { public recyclerScrolled() {
// Ignore if not initialized // Ignore if not initialized
if (!this.ticks.length) return; if (!this.ticks.length) return;
@ -109,7 +109,11 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
const scroll = this.recycler?.$el?.scrollTop || 0; const scroll = this.recycler?.$el?.scrollTop || 0;
// Move hover cursor to px position // Move hover cursor to px position
this.cursorY = utils.roundHalf(scroll * this.height / this.recyclerHeight); const {top1, top2, y1, y2} = this.getCoords(scroll, 'y');
const topfrac = (scroll - y1) / (y2 - y1);
const rtop = top1 + (top2 - top1) * topfrac;
this.cursorY = utils.roundHalf(rtop);
this.moveHoverCursor(this.cursorY); this.moveHoverCursor(this.cursorY);
// Show the scroller for some time // Show the scroller for some time
@ -123,22 +127,13 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
/** Re-create tick data in the next frame */ /** Re-create tick data in the next frame */
public async reflow() { public async reflow() {
if (this.reflowRequest) { if (this.reflowRequest) return;
return;
}
this.reflowRequest = true; this.reflowRequest = true;
await this.$nextTick(); await this.$nextTick();
this.reflowNow(); this.reflowNow();
this.reflowRequest = false; this.reflowRequest = false;
} }
private setTickTop(tick: ITick) {
const extraY = this.recyclerBefore?.clientHeight || 0;
tick.topF = (extraY + tick.y) * (this.height / this.recyclerHeight);
tick.top = utils.roundHalf(tick.topF);
}
/** Re-create tick data */ /** Re-create tick data */
private reflowNow() { private reflowNow() {
// Ignore if not initialized // Ignore if not initialized
@ -150,40 +145,32 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
// Recreate ticks data // Recreate ticks data
this.recreate(); this.recreate();
// Recompute which ticks are visible // Adjust top
this.computeVisibleTicks(); this.adjustNow();
} }
/** Recreate from scratch */ /** Recreate from scratch */
private recreate() { private recreate() {
// Clear and override any adjust timer // Clear and override any adjust timer
this.ticks = []; this.ticks = [];
window.clearTimeout(this.adjustTimer || 0);
this.adjustTimer = null;
// Ticks // Ticks
let y = 0;
let prevYear = 9999; let prevYear = 9999;
let prevMonth = 0; let prevMonth = 0;
const thisYear = new Date().getFullYear(); const thisYear = new Date().getFullYear();
// Get a new tick // Get a new tick
const getTick = (dayId: number, text?: string | number): ITick => { const getTick = (dayId: number, isMonth=false, text?: string | number): ITick => {
const tick = { return {
dayId, dayId, isMonth, text,
y: y, y: 0, count: 0, topF: 0, top: 0, s: false,
text,
topF: 0,
top: 0,
s: false,
}; };
this.setTickTop(tick);
return tick;
} }
// Itearte over rows // Iterate over rows
for (const row of this.rows) { for (const row of this.rows) {
if (row.type === IRowType.HEAD) { if (row.type === IRowType.HEAD) {
// Create tick
if (this.TagDayIDValueSet.has(row.dayId)) { if (this.TagDayIDValueSet.has(row.dayId)) {
// Blank tick // Blank tick
this.ticks.push(getTick(row.dayId)); this.ticks.push(getTick(row.dayId));
@ -191,20 +178,75 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
// Make date string // Make date string
const dateTaken = utils.dayIdToDate(row.dayId); const dateTaken = utils.dayIdToDate(row.dayId);
// Create tick if month changed // Create tick
const dtYear = dateTaken.getUTCFullYear(); const dtYear = dateTaken.getUTCFullYear();
const dtMonth = dateTaken.getUTCMonth() const dtMonth = dateTaken.getUTCMonth()
if (Number.isInteger(row.dayId) && (dtMonth !== prevMonth || dtYear !== prevYear)) { const isMonth = (dtMonth !== prevMonth || dtYear !== prevYear);
const text = (dtYear === prevYear || dtYear === thisYear) ? undefined : dtYear; const text = (dtYear === prevYear || dtYear === thisYear) ? undefined : dtYear;
this.ticks.push(getTick(row.dayId, text)); this.ticks.push(getTick(row.dayId, isMonth, text));
}
prevMonth = dtMonth; prevMonth = dtMonth;
prevYear = dtYear; prevYear = dtYear;
} }
} }
}
}
/**
* Update tick positions without truncating the list
* This is much cheaper than reflowing the whole thing
*/
public async adjust() {
if (this.adjustRequest) return;
this.adjustRequest = true;
await this.$nextTick();
this.adjustNow();
this.adjustRequest = false;
}
/** Do adjustment synchronously */
private adjustNow() {
// Refresh height of recycler
this.recyclerHeight = this.recycler.$refs.wrapper.clientHeight;
const extraY = this.recyclerBefore?.clientHeight || 0;
// Start with the first tick. Walk over all rows counting the
// y position. When you hit a row with the tick, update y and
// top values and move to the next tick.
let tickId = 0;
let y = extraY;
let count = 0;
// We only need to recompute top and visible ticks if count
// of some tick has changed.
let needRecomputeTop = false;
for (const row of this.rows) {
// Check if tick is valid
if (tickId >= this.ticks.length) break;
// Check if we hit the next tick
const tick = this.ticks[tickId];
if (tick.dayId === row.dayId) {
tick.y = y;
// Check if count has changed
needRecomputeTop ||= (tick.count !== count);
tick.count = count;
// Move to next tick
count += row.day.count;
tickId++;
}
y += row.size; y += row.size;
} }
// Compute visible ticks
if (needRecomputeTop) {
this.setTicksTop(count);
this.computeVisibleTicks();
}
} }
/** Mark ticks as visible or invisible */ /** Mark ticks as visible or invisible */
@ -216,22 +258,22 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
const minGap = fontSizePx + (window.innerWidth <= 768 ? 5 : 2); const minGap = fontSizePx + (window.innerWidth <= 768 ? 5 : 2);
let prevShow = -9999; let prevShow = -9999;
for (const [idx, tick] of this.ticks.entries()) { for (const [idx, tick] of this.ticks.entries()) {
// Conservative
tick.s = false;
// These aren't for showing
if (!tick.isMonth) continue;
// You can't see these anyway, why bother? // You can't see these anyway, why bother?
if (tick.top < minGap || tick.top > this.height - minGap) { if (tick.top < minGap || tick.top > this.height - minGap) continue;
tick.s = false;
continue;
}
// Will overlap with the previous tick. Skip anyway. // Will overlap with the previous tick. Skip anyway.
if (tick.top - prevShow < minGap) { if (tick.top - prevShow < minGap) continue;
tick.s = false;
continue;
}
// This is a labelled tick then show it anyway for the sake of best effort // This is a labelled tick then show it anyway for the sake of best effort
if (tick.text) { if (tick.text) {
tick.s = true;
prevShow = tick.top; prevShow = tick.top;
tick.s = true;
continue; continue;
} }
@ -249,7 +291,6 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
const nextLabelledTick = this.ticks[i]; const nextLabelledTick = this.ticks[i];
if (tick.top + minGap > nextLabelledTick.top && if (tick.top + minGap > nextLabelledTick.top &&
nextLabelledTick.top < this.height - minGap) { // make sure this will be shown nextLabelledTick.top < this.height - minGap) { // make sure this will be shown
tick.s = false;
continue; continue;
} }
} }
@ -260,45 +301,10 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
} }
} }
/** private setTicksTop(total: number) {
* Update tick positions without truncating the list for (const tick of this.ticks) {
* This is much cheaper than reflowing the whole thing tick.topF = this.height * (tick.count / total);
*/ tick.top = utils.roundHalf(tick.topF);
public adjust() {
if (this.adjustTimer) return;
this.adjustTimer = window.setTimeout(() => {
this.adjustTimer = null;
this.adjustNow();
this.computeVisibleTicks();
}, 300);
}
/** Do adjustment synchrnously */
private adjustNow() {
// Refresh height of recycler
this.recyclerHeight = this.recycler.$refs.wrapper.clientHeight;
// Start with the first tick. Walk over all rows counting the
// y position. When you hit a row with the tick, update y and
// top values and move to the next tick.
let tickId = 0;
let y = 0;
for (const row of this.rows) {
// Check if tick is valid
if (tickId >= this.ticks.length) {
return;
}
// Check if we hit the next tick
const tick = this.ticks[tickId];
if (tick.dayId === row.dayId) {
tick.y = y;
this.setTickTop(tick);
tickId++;
}
y += row.size;
} }
} }
@ -342,9 +348,37 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
this.moveHoverCursor(this.cursorY); this.moveHoverCursor(this.cursorY);
} }
/** Binary search and get coords surrounding position */
private getCoords(y: number, field: 'topF' | 'y') {
// Top of first and second ticks
let top1 = 0, top2 = 0, y1 = 0, y2 = 0;
// Get index of previous tick
let idx = utils.binarySearch(this.ticks, y, field);
if (idx <= 0) {
top1 = 0; top2 = this.ticks[0].top;
y1 = 0; y2 = this.ticks[0].y;
} else if (idx >= this.ticks.length) {
const t = this.ticks[this.ticks.length - 1];
top1 = t.top; top2 = this.height;
y1 = t.y; y2 = this.recyclerHeight;
} else {
const t1 = this.ticks[idx - 1];
const t2 = this.ticks[idx];
top1 = t1.top; top2 = t2.top;
y1 = t1.y; y2 = t2.y;
}
return {top1, top2, y1, y2};
}
/** Move to given scroller Y */ /** Move to given scroller Y */
private moveto(y: number) { private moveto(y: number) {
this.recycler.scrollToPosition(this.getRecyclerY(y)); const {top1, top2, y1, y2} = this.getCoords(y, 'topF');
const yfrac = (y - top1) / (top2 - top1);
const ry = y1 + (y2 - y1) * yfrac;
this.recycler.scrollToPosition(ry);
this.handleScroll(); this.handleScroll();
} }
@ -371,13 +405,6 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
this.scrollingTimer = null; this.scrollingTimer = null;
}, 1500); }, 1500);
} }
/** Get recycler equivalent position from event */
private getRecyclerY(y: number) {
const tH = this.recyclerHeight;
const maxH = this.height;
return y * tH / maxH;
}
} }
</script> </script>

View File

@ -142,6 +142,10 @@ export type ITick = {
top: number; top: number;
/** Y coordinate on recycler */ /** Y coordinate on recycler */
y: number; y: number;
/** Cumulative number of photos before this tick */
count: number;
/** Is a new month */
isMonth: boolean;
/** Text if any (e.g. year) */ /** Text if any (e.g. year) */
text?: string | number; text?: string | number;
/** Whether this tick should be shown */ /** Whether this tick should be shown */