diff options
Diffstat (limited to 'src/components')
| -rw-r--r-- | src/components/TimeLine.vue | 150 |
1 files changed, 69 insertions, 81 deletions
diff --git a/src/components/TimeLine.vue b/src/components/TimeLine.vue index 91743f6..89e7057 100644 --- a/src/components/TimeLine.vue +++ b/src/components/TimeLine.vue @@ -14,13 +14,13 @@ <!-- Tick markers --> <template v-for="date, idx in ticks.dates" :key="date.toInt()"> <line v-if="date.equals(MIN_DATE, scale) || date.equals(MAX_DATE, scale)" - :x1="vert ? -END_TICK_SZ : 0" :y1="vert ? 0 : -END_TICK_SZ" - :x2="vert ? END_TICK_SZ : 0" :y2="vert ? 0 : END_TICK_SZ" - :stroke="store.color.alt" :stroke-width="`${END_TICK_SZ * 2}px`" + :x1="vert ? -store.endTickSz / 2 : 0" :y1="vert ? 0 : -store.endTickSz / 2" + :x2="vert ? store.endTickSz / 2 : 0" :y2="vert ? 0 : store.endTickSz / 2" + :stroke="store.color.alt" :stroke-width="`${store.endTickSz}px`" :style="tickStyles(idx)" class="animate-fadein"/> <line v-else - :x1="vert ? -TICK_LEN : 0" :y1="vert ? 0 : -TICK_LEN" - :x2="vert ? TICK_LEN : 0" :y2="vert ? 0 : TICK_LEN" + :x1="vert ? -store.tickLen / 2 : 0" :y1="vert ? 0 : -store.tickLen / 2" + :x2="vert ? store.tickLen / 2 : 0" :y2="vert ? 0 : store.tickLen / 2" :stroke="store.color.alt" stroke-width="1px" :style="tickStyles(idx)" class="animate-fadein"/> </template> @@ -115,22 +115,18 @@ const resizeObserver = new ResizeObserver((entries) => { onMounted(() => resizeObserver.observe(rootRef.value as HTMLElement)); // -const MAINLINE_WIDTH = 80; // Breadth of mainline area (including ticks and labels) -const EVENT_IMG_SZ = 100; // Width/height of event images -const EVENT_LABEL_HEIGHT = 20; -const eventWidth = computed(() => EVENT_IMG_SZ); -const eventHeight = computed(() => EVENT_IMG_SZ + EVENT_LABEL_HEIGHT); +const eventWidth = computed(() => store.eventImgSz); +const eventHeight = computed(() => store.eventImgSz + store.eventLabelHeight); const eventMajorSz = computed(() => props.vert ? eventHeight.value : eventWidth.value); const eventMinorSz = computed(() => props.vert ? eventWidth.value : eventHeight.value) -const SPACING = 10; const sideMainline = computed( // True if unable to fit mainline in middle with events on both sides - () => availBreadth.value < MAINLINE_WIDTH + (eventMinorSz.value + SPACING * 2) * 2); + () => availBreadth.value < store.mainlineBreadth + (eventMinorSz.value + store.spacing * 2) * 2); const mainlineOffset = computed(() => { // Distance mainline-area line to side of display area if (!sideMainline.value){ - return availBreadth.value / 2 - MAINLINE_WIDTH /2 + LARGE_TICK_LEN; + return availBreadth.value / 2 - store.mainlineBreadth /2 + store.largeTickLen / 2; } else { - return availBreadth.value - SPACING - tickLabelMargin.value - - (props.vert ? tickLabelWidth.value : TICK_LABEL_HEIGHT); + return availBreadth.value - store.spacing - tickLabelMargin.value + - (props.vert ? tickLabelWidth.value : store.tickLabelHeight); } }); @@ -138,11 +134,10 @@ const mainlineOffset = computed(() => { // Distance mainline-area line to side o const ID = props.initialState.id as number; const startDate = ref(props.initialState.startDate); // Earliest date to display const endDate = ref(props.initialState.endDate); -const INITIAL_EXTRA_OFFSET = 0.5; -const startOffset = ref(INITIAL_EXTRA_OFFSET); // Fraction of a scale unit before startDate to show +const startOffset = ref(store.defaultEndTickOffset); // Fraction of a scale unit before startDate to show // Note: Without this, the timeline can only move if the distance is over one unit, which makes dragging awkward, // can cause unexpected jumps when zooming, and limits display when a unit has many ticks on the next scale -const endOffset = ref(INITIAL_EXTRA_OFFSET); +const endOffset = ref(store.defaultEndTickOffset); const scaleIdx = ref(0); // Index of current scale in SCALES const scale = computed(() => SCALES[scaleIdx.value]) if (props.initialState.startOffset != null){ @@ -163,12 +158,12 @@ function initScale(){ // Initialises to smallest usable scale } else { let dayDiff = startDate.value.getDayDiff(endDate.value) + startOffset.value + endOffset.value; // Check for day scale usability - if (availLen.value / dayDiff >= MIN_TICK_SEP){ + if (availLen.value / dayDiff >= store.minTickSep){ scaleIdx.value = SCALES.findIndex(s => s == DAY_SCALE); } else { // Check for month scale usability let monthDiff = startDate.value.getMonthDiff(endDate.value) + startOffset.value + endOffset.value; - if (availLen.value / monthDiff >= MIN_TICK_SEP){ + if (availLen.value / monthDiff >= store.minTickSep){ scaleIdx.value = SCALES.findIndex(s => s == MONTH_SCALE); } else { // Use a yearly scale scaleIdx.value = getYearlyScale(startDate.value, endDate.value, availLen.value); @@ -186,7 +181,7 @@ function getYearlyScale(startDate: HistDate, endDate: HistDate, availLen: number while (idx < SCALES.length - 1){ // Check for usable smaller scales let nextScale = SCALES[idx + 1] let adjustedYearDiff = yearDiff + startOffset.value * nextScale + endOffset.value * nextScale; - if (availLen / (adjustedYearDiff / nextScale) >= MIN_TICK_SEP){ + if (availLen / (adjustedYearDiff / nextScale) >= store.minTickSep){ idx += 1; } else { break; @@ -196,14 +191,8 @@ function getYearlyScale(startDate: HistDate, endDate: HistDate, availLen: number } // Tick data -const TICK_LEN = 8; // Length of half of tick -const LARGE_TICK_LEN = 16; -const END_TICK_SZ = 4; // Size for MIN_DATE/MAX_DATE ticks -const MIN_TICK_SEP = 30; // Smallest px separation between ticks -const MIN_LAST_TICKS = 3; // When at smallest scale, don't zoom further into less than this many ticks -const TICK_LABEL_HEIGHT = 10; const tickLabelMargin = computed(() => props.vert ? 20 : 30); // Distance from label to mainline -const tickLabelWidth = computed(() => MAINLINE_WIDTH - LARGE_TICK_LEN - tickLabelMargin.value); +const tickLabelWidth = computed(() => store.mainlineBreadth - store.largeTickLen / 2 - tickLabelMargin.value); type Ticks = { dates: HistDate[], // One for each tick to render startIdx: number, @@ -259,7 +248,7 @@ const ticks = computed((): Ticks => { let datesBefore: HistDate[] = []; let datesAfter: HistDate[] = []; if (scaleIdx.value > 0 && - availLen.value / (numUnits * store.zoomRatio) < MIN_TICK_SEP){ // If zoom-out would decrease scale + availLen.value / (numUnits * store.zoomRatio) < store.minTickSep){ // If zoom-out would decrease scale let zoomUnits = numUnits * (store.zoomRatio - 1); // Potential distance shifted upon a zoom-out if (zoomUnits > panUnits){ let zoomedScale = SCALES[scaleIdx.value-1]; @@ -318,28 +307,28 @@ const idToPos = computed(() => { let afterMainlineIdx: number | null = null; // Index of first column after the mainline, if there is one if (!sideMainline.value){ // Get columns before mainline area - let columnOffset = availBreadth.value / 2 - MAINLINE_WIDTH / 2 - SPACING - eventMinorSz.value; - while (columnOffset >= SPACING){ + let columnOffset = availBreadth.value / 2 - store.mainlineBreadth / 2 - store.spacing - eventMinorSz.value; + while (columnOffset >= store.spacing){ cols.push([]); colOffsets.push(columnOffset); - columnOffset -= eventMinorSz.value + SPACING; + columnOffset -= eventMinorSz.value + store.spacing; } colOffsets.reverse(); afterMainlineIdx = cols.length; // Get columns after mainline area - columnOffset = availBreadth.value / 2 + MAINLINE_WIDTH / 2 + SPACING; - while (columnOffset + eventMinorSz.value + SPACING < availBreadth.value){ + columnOffset = availBreadth.value / 2 + store.mainlineBreadth / 2 + store.spacing; + while (columnOffset + eventMinorSz.value + store.spacing < availBreadth.value){ cols.push([]); colOffsets.push(columnOffset); - columnOffset += eventMinorSz.value + SPACING; + columnOffset += eventMinorSz.value + store.spacing; } } else { // Get columns before mainline area - let columnOffset = mainlineOffset.value - SPACING - eventMinorSz.value - SPACING; - while (columnOffset >= SPACING){ + let columnOffset = mainlineOffset.value - store.spacing - eventMinorSz.value - store.spacing; + while (columnOffset >= store.spacing){ cols.push([]); colOffsets.push(columnOffset); - columnOffset -= eventMinorSz.value + SPACING; + columnOffset -= eventMinorSz.value + store.spacing; } colOffsets.reverse(); } @@ -367,15 +356,16 @@ const idToPos = computed(() => { let bestIdx: number | null = null; // Index of insertion for bestOffset // Check for empty column if (cols[colIdx].length == 0){ - let offset = Math.min(Math.max(SPACING, targetOffset), availLen.value - eventMajorSz.value - SPACING); + let offset = Math.max(store.spacing, targetOffset) + offset = Math.min(offset, availLen.value - eventMajorSz.value - store.spacing); positions.push([colIdx, 0, offset]); break; } // Check placement before first event in column - let offset = cols[colIdx][0][1] - eventMajorSz.value - SPACING; - if (offset >= SPACING){ + let offset = cols[colIdx][0][1] - eventMajorSz.value - store.spacing; + if (offset >= store.spacing){ if (offset >= targetOffset){ - positions.push([colIdx, 0, Math.max(SPACING, targetOffset)]); + positions.push([colIdx, 0, Math.max(store.spacing, targetOffset)]); break; } else { bestOffset = offset; @@ -384,14 +374,14 @@ const idToPos = computed(() => { } // Check placement after each event element in column for (let elIdx = 0; elIdx < cols[colIdx].length; elIdx++){ - offset = cols[colIdx][elIdx][1] + eventMajorSz.value + SPACING; + offset = cols[colIdx][elIdx][1] + eventMajorSz.value + store.spacing; if (elIdx == cols[colIdx].length - 1){ // If last element in column - if (offset < availLen.value - eventMajorSz.value - SPACING){ + if (offset < availLen.value - eventMajorSz.value - store.spacing){ // Check for better offset if (bestOffset == null || Math.abs(targetOffset - offset) < Math.abs(targetOffset - bestOffset)){ if (offset <= targetOffset){ - offset = Math.min(targetOffset, availLen.value - eventMajorSz.value - SPACING); + offset = Math.min(targetOffset, availLen.value - eventMajorSz.value - store.spacing); positions.push([colIdx, elIdx + 1, offset]); break columnLoop; } else { @@ -403,19 +393,19 @@ const idToPos = computed(() => { } else { // If not last event in column // Check for space between this and next element let nextOffset = cols[colIdx][elIdx + 1][1]; - if (nextOffset - offset < eventMajorSz.value + SPACING){ + if (nextOffset - offset < eventMajorSz.value + store.spacing){ continue; } // Check for better offset if (bestOffset == null || Math.abs(targetOffset - offset) < Math.abs(targetOffset - bestOffset)){ - if (offset <= targetOffset && targetOffset <= nextOffset - eventMajorSz.value - SPACING){ + if (offset <= targetOffset && targetOffset <= nextOffset - eventMajorSz.value - store.spacing){ positions.push([colIdx, elIdx + 1, targetOffset]); break columnLoop; } else { if (offset > targetOffset){ bestOffset = offset; } else { - bestOffset = nextOffset - eventMajorSz.value - SPACING; + bestOffset = nextOffset - eventMajorSz.value - store.spacing; } bestIdx = elIdx + 1; } @@ -462,7 +452,7 @@ const idToPos = computed(() => { } } // If more events could be displayed, notify parent - let colFillThreshold = (availLen.value - SPACING) / (eventMajorSz.value + SPACING) * 2/3; + let colFillThreshold = (availLen.value - store.spacing) / (eventMajorSz.value + store.spacing) * 2/3; let full = cols.every(col => col.length >= colFillThreshold); if (!full){ emit('event-req', startDate.value, endDate.value); @@ -531,15 +521,15 @@ function panTimeline(scrollRatio: number){ if (scrollRatio > 0){ while (true){ if (newEnd.equals(MAX_DATE, scale.value)){ - // Pan up to an offset of INITIAL_EXTRA_OFFSET - if (INITIAL_EXTRA_OFFSET == endOffset.value){ + // Pan up to an offset of store.defaultEndTickOffset + if (store.defaultEndTickOffset == endOffset.value){ console.log('Reached maximum date limit'); newStartOffset = startOffset.value; newEndOffset = endOffset.value; } else { - if (numEndSteps > 0 || newEndOffset >= INITIAL_EXTRA_OFFSET){ - chgUnits = INITIAL_EXTRA_OFFSET - endOffset.value; - newEndOffset = INITIAL_EXTRA_OFFSET; + if (numEndSteps > 0 || newEndOffset >= store.defaultEndTickOffset){ + chgUnits = store.defaultEndTickOffset - endOffset.value; + newEndOffset = store.defaultEndTickOffset; let extraStartSteps: number; [extraStartSteps, , newStartOffset, ] = getMovedBounds(startOffset.value, endOffset.value, chgUnits, chgUnits); @@ -570,15 +560,15 @@ function panTimeline(scrollRatio: number){ } else { while (true){ if (MIN_DATE.equals(newStart, scale.value)){ - // Pan up to an offset of INITIAL_EXTRA_OFFSET - if (INITIAL_EXTRA_OFFSET == startOffset.value){ + // Pan up to an offset of store.defaultEndTickOffset + if (store.defaultEndTickOffset == startOffset.value){ console.log('Reached minimum date limit'); newStartOffset = startOffset.value; newEndOffset = endOffset.value; } else { - if (numStartSteps < 0 || newStartOffset >= INITIAL_EXTRA_OFFSET){ - chgUnits = -INITIAL_EXTRA_OFFSET + startOffset.value; - newStartOffset = INITIAL_EXTRA_OFFSET; + if (numStartSteps < 0 || newStartOffset >= store.defaultEndTickOffset){ + chgUnits = -store.defaultEndTickOffset + startOffset.value; + newStartOffset = store.defaultEndTickOffset; let extraEndSteps: number; [, extraEndSteps, , newEndOffset] = getMovedBounds(startOffset.value, endOffset.value, chgUnits, chgUnits); @@ -662,11 +652,11 @@ function zoomTimeline(zoomRatio: number){ while (numStartSteps < 0){ if (newStart.equals(MIN_CAL_DATE, scale.value) && (scale.value == MONTH_SCALE || scale.value == DAY_SCALE)){ console.log('Restricting new range to dates where month/day scale is usable'); - newStartOffset = INITIAL_EXTRA_OFFSET; + newStartOffset = store.defaultEndTickOffset; break; } if (MIN_DATE.equals(newStart, scale.value)){ - newStartOffset = INITIAL_EXTRA_OFFSET; + newStartOffset = store.defaultEndTickOffset; break; } stepDate(newStart, scale.value, {forward: false, inplace: true}); @@ -675,7 +665,7 @@ function zoomTimeline(zoomRatio: number){ } while (numEndSteps > 0){ if (MAX_DATE.equals(newEnd, scale.value)){ - newEndOffset = INITIAL_EXTRA_OFFSET; + newEndOffset = store.defaultEndTickOffset; break; } stepDate(newEnd, scale.value, {inplace: true}); @@ -686,7 +676,7 @@ function zoomTimeline(zoomRatio: number){ } // Possibly change the scale let tickDiff = availLen.value / newNumUnits; - if (tickDiff < MIN_TICK_SEP){ // Possibly zoom out + if (tickDiff < store.minTickSep){ // Possibly zoom out if (scaleIdx.value == 0){ console.log('Reached zoom out limit'); return; @@ -753,7 +743,7 @@ function zoomTimeline(zoomRatio: number){ } } else { // Possibly zoom in if (scaleIdx.value == SCALES.length - 1){ - if (newNumUnits < MIN_LAST_TICKS){ + if (newNumUnits < store.minLastTicks){ console.log('Reached zoom in limit'); return; } @@ -761,7 +751,7 @@ function zoomTimeline(zoomRatio: number){ let newScale = SCALES[scaleIdx.value + 1]; let newUnitsPerOld = getScaleRatio(newScale, scale.value); let zoomedTickDiff = tickDiff / newUnitsPerOld; - if (zoomedTickDiff > MIN_TICK_SEP){ + if (zoomedTickDiff > store.minTickSep){ // Update offsets newStartOffset *= newUnitsPerOld; stepDate(newStart, newScale, {forward: false, count: Math.floor(newStartOffset), inplace: true}); @@ -927,16 +917,14 @@ const skipTransition = ref(true); onMounted(() => setTimeout(() => {skipTransition.value = false}, 100)); // Styles -const transitionDuration = '300ms'; -const transitionTimingFunction = 'ease-out'; const mainlineStyles = computed(() => { return { transform: props.vert ? `translate(${mainlineOffset.value}px, 0) rotate(90deg) scale(${availLen.value},1)` : `translate(0, ${mainlineOffset.value}px) rotate(0deg) scale(${availLen.value},1)`, transitionProperty: skipTransition.value ? 'none' : 'transform', - transitionDuration, - transitionTimingFunction, + transitionDuration: store.transitionDuration + 'ms', + transitionTimingFunction: 'ease-out', }; }); function tickStyles(idx: number){ @@ -946,15 +934,15 @@ function tickStyles(idx: number){ let scaleFactor = 1; if (scaleIdx.value > 0 && inDateScale(ticks.value.dates[idx], SCALES[scaleIdx.value-1])){ // If tick exists on larger scale - scaleFactor = LARGE_TICK_LEN / TICK_LEN; + scaleFactor = store.largeTickLen / store.tickLen; } return { transform: props.vert ? `translate(${mainlineOffset.value}px, ${offset}px) scale(${scaleFactor})` : `translate(${offset}px, ${mainlineOffset.value}px) scale(${scaleFactor})`, transitionProperty: skipTransition.value ? 'none' : 'transform, opacity', - transitionDuration, - transitionTimingFunction, + transitionDuration: store.transitionDuration + 'ms', + transitionTimingFunction: 'ease-out', opacity: (offset >= 0 && offset <= availLen.value) ? 1 : 0, } } @@ -962,14 +950,14 @@ function tickLabelStyles(idx: number){ let offset = (idx - ticks.value.startIdx + startOffset.value) / (ticks.value.endIdx - ticks.value.startIdx + startOffset.value + endOffset.value) * availLen.value; - let labelSz = props.vert ? TICK_LABEL_HEIGHT : tickLabelWidth.value; + let labelSz = props.vert ? store.tickLabelHeight : tickLabelWidth.value; return { transform: props.vert ? `translate(${mainlineOffset.value + tickLabelMargin.value}px, ${offset}px)` : `translate(${offset}px, ${mainlineOffset.value + tickLabelMargin.value}px)`, transitionProperty: skipTransition.value ? 'none' : 'transform, opacity', - transitionDuration, - transitionTimingFunction, + transitionDuration: store.transitionDuration + 'ms', + transitionTimingFunction: 'ease-out', opacity: (offset >= labelSz && offset <= availLen.value - labelSz) ? 1 : 0, } } @@ -981,15 +969,15 @@ function eventStyles(eventId: number){ width: w + 'px', height: h + 'px', transitionProperty: skipTransition.value ? 'none' : 'all', - transitionDuration, - transitionTimingFunction, + transitionDuration: store.transitionDuration + 'ms', + transitionTimingFunction: 'ease-out', }; } function eventImgStyles(eventId: number){ const event = idToEvent.value.get(eventId)!; return { - width: EVENT_IMG_SZ + 'px', - height: EVENT_IMG_SZ + 'px', + width: store.eventImgSz + 'px', + height: store.eventImgSz + 'px', backgroundImage: `url(${getImagePath(event.imgId)})`, backgroundSize: 'cover', }; @@ -999,8 +987,8 @@ function eventLineStyles(eventId: number){ return { transform: `translate(${x}px, ${y}px) rotate(${a}deg) scaleX(${l})`, transitionProperty: skipTransition.value ? 'none' : 'transform', - transitionDuration, - transitionTimingFunction, + transitionDuration: store.transitionDuration + 'ms', + transitionTimingFunction: 'ease-out', }; } </script> |
