Make scroller non-linear
parent
fc0b19dbf9
commit
eacd1e19f7
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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 */
|
||||||
|
|
Loading…
Reference in New Issue