diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/SearchModal.vue | 10 | ||||
| -rw-r--r-- | src/components/TimeLine.vue | 91 | ||||
| -rw-r--r-- | src/index.css | 13 | ||||
| -rw-r--r-- | src/lib.ts | 5 |
4 files changed, 97 insertions, 22 deletions
diff --git a/src/components/SearchModal.vue b/src/components/SearchModal.vue index dd387e1..ade86be 100644 --- a/src/components/SearchModal.vue +++ b/src/components/SearchModal.vue @@ -31,7 +31,7 @@ import {ref, computed, onMounted, PropType} from 'vue'; import SearchIcon from './icon/SearchIcon.vue'; import InfoIcon from './icon/InfoIcon.vue'; -import {HistEvent, queryServer, EventInfoJson, jsonToEventInfo, SuggResponseJson} from '../lib'; +import {HistEvent, queryServer, EventInfoJson, jsonToEventInfo, SuggResponseJson, animateWithClass} from '../lib'; import {useStore} from '../store'; import {RBTree} from '../rbtree'; @@ -187,12 +187,8 @@ async function resolveSearch(eventTitle: string){ return; } emit('search', eventInfo.event); - } else { - // Trigger failure animation - let input = inputRef.value!; - input.classList.remove('animate-red-then-fade'); - input.offsetWidth; // Triggers reflow - input.classList.add('animate-red-then-fade'); + } else { // Trigger failure animation + animateWithClass(inputRef.value!, 'animate-red-then-fade'); } } diff --git a/src/components/TimeLine.vue b/src/components/TimeLine.vue index c72fbff..14f9d16 100644 --- a/src/components/TimeLine.vue +++ b/src/components/TimeLine.vue @@ -25,18 +25,11 @@ <!-- Main line (unit horizontal line that gets transformed, with extra length to avoid gaps when panning) --> <line :stroke="store.color.alt" stroke-width="2px" x1="-1" y1="0" x2="2" y2="0" :style="mainlineStyles"/> <!-- Tick markers --> - <template v-for="tick in ticks" :key="tick.date.toInt()"> - <line v-if="tick.major && (tick.date.equals(MIN_DATE, scale) || tick.date.equals(MAX_DATE, scale))" - :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(tick)" class="animate-fadein"/> - <line v-else - :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(tick)" class="animate-fadein"/> - </template> + <line v-for="tick in ticks" :key="tick.date.toInt()" + :x1="tick.x1" :y1="tick.y1" :x2="tick.x2" :y2="tick.y2" + :stroke="store.color.alt" :stroke-width="`${tick.width}px`" + :style="tickStyles(tick)" class="animate-fadein" + :class="{'max-tick': tick.bound == 'max', 'min-tick': tick.bound == 'min'}"/> <!-- Tick labels --> <template v-for="tick in ticks" :key="tick.date.toInt()"> <text v-if="tick.major || store.showMinorTicks" @@ -47,6 +40,10 @@ </text> </template> </svg> + <!-- Movement fail indicators --> + <div class="absolute z-20" :style="failDivStyles(true)" ref="minFailRef"></div> + <div class="absolute z-20" :style="failDivStyles(false)" ref="maxFailRef"></div> + <div class="absolute top-0 left-0 w-full h-full z-20" ref="bgFailRef"></div> <!-- Events --> <div v-for="id in idToPos.keys()" :key="id" class="absolute animate-fadein z-20" :style="eventStyles(id)"> <!-- Image --> @@ -80,13 +77,16 @@ import CloseIcon from './icon/CloseIcon.vue'; import {WRITING_MODE_HORZ, MIN_DATE, MAX_DATE, MONTH_SCALE, DAY_SCALE, SCALES, MONTH_NAMES, MIN_CAL_DATE, getDaysInMonth, HistDate, stepDate, getScaleRatio, getNumSubUnits, getUnitDiff, getEventPrecision, dateToUnit, dateToScaleDate, - moduloPositive, TimelineState, HistEvent, getImagePath} from '../lib'; + moduloPositive, TimelineState, HistEvent, getImagePath, animateWithClass} from '../lib'; import {useStore} from '../store'; import {RBTree} from '../rbtree'; // Refs const rootRef: Ref<HTMLElement | null> = ref(null); const svgRef: Ref<HTMLElement | null> = ref(null); +const minFailRef: Ref<HTMLElement | null> = ref(null); +const maxFailRef: Ref<HTMLElement | null> = ref(null); +const bgFailRef: Ref<HTMLElement | null> = ref(null); // Global store const store = useStore(); @@ -239,10 +239,32 @@ class Tick { date: HistDate; major: boolean; // False if tick is on the minor scale offset: number; // Distance from start of visible timeline, in major units - constructor(date: HistDate, major: boolean, offset: number){ + bound: null | 'min' | 'max'; // Indicates MIN_DATE or MAX_DATE tick + // SVG attributes + x1: number; + y1: number; + x2: number; + y2: number; + width: number; + // + constructor(date: HistDate, major: boolean, offset: number, bound=null as null | 'min' | 'max'){ this.date = date; this.major = major; this.offset = offset; + this.bound = bound; + if (this.bound == null){ + this.x1 = props.vert ? -store.tickLen / 2 : 0; + this.y1 = props.vert ? 0 : -store.tickLen / 2; + this.x2 = props.vert ? store.tickLen / 2 : 0; + this.y2 = props.vert ? 0 : store.tickLen / 2; + this.width = 1; + } else { + this.x1 = props.vert ? -store.endTickSz / 2 : 0; + this.y1 = props.vert ? 0 : -store.endTickSz / 2; + this.x2 = props.vert ? store.endTickSz / 2 : 0; + this.y2 = props.vert ? 0 : store.endTickSz / 2; + this.width = store.endTickSz; + } } } function getNumDisplayUnits({inclOffsets=true} = {}): number { // Get num major units in display range @@ -303,7 +325,15 @@ const ticks = computed((): Tick[] => { date = startDate.value.clone(); let numMajorUnits = getNumDisplayUnits({inclOffsets: false}); for (let i = 0; i <= numMajorUnits; i++){ - ticks.push(new Tick(date, true, startOffset.value + i)); + // Check for MIN_DATE or MAX_DATE + let minOrMax = null as null | 'min' | 'max'; + if (i == 0 && date.equals(MIN_DATE, scale.value)){ + minOrMax = 'min'; + } else if (i == numMajorUnits && date.equals(MAX_DATE, scale.value)){ + minOrMax = 'max'; + } + // Add ticks + ticks.push(new Tick(date, true, startOffset.value + i, minOrMax)); if (i == numMajorUnits){ break; } @@ -777,6 +807,7 @@ function panTimeline(scrollRatio: number){ } else { // Pan up to an offset of store.defaultEndTickOffset if (store.defaultEndTickOffset == endOffset.value){ + animateFailDiv('max'); console.log('Reached maximum date limit'); newStartOffset = startOffset.value; newEndOffset = endOffset.value; @@ -813,6 +844,7 @@ function panTimeline(scrollRatio: number){ } else { // Pan up to an offset of store.defaultEndTickOffset if (store.defaultEndTickOffset == startOffset.value){ + animateFailDiv('min'); console.log('Reached minimum date limit'); newStartOffset = startOffset.value; newEndOffset = endOffset.value; @@ -850,6 +882,7 @@ function zoomTimeline(zoomRatio: number, ignorePointer=false){ if (zoomRatio > 1 && startDate.value.equals(MIN_DATE, scale.value) && endDate.value.equals(MAX_DATE, scale.value)){ + animateFailDiv('both'); console.log('Reached upper scale limit'); return; } @@ -914,6 +947,7 @@ function zoomTimeline(zoomRatio: number, ignorePointer=false){ let tickDiff = availLen.value / newNumUnits; if (tickDiff < store.minTickSep){ // Zoom out into new scale if (scaleIdx.value == 0){ + animateFailDiv('both'); console.log('Reached zoom out limit'); return; } else { @@ -978,6 +1012,7 @@ function zoomTimeline(zoomRatio: number, ignorePointer=false){ } else { // If trying to zoom in if (scaleIdx.value == SCALES.length - 1){ if (newNumUnits < store.minLastTicks){ + animateFailDiv('bg'); console.log('Reached zoom in limit'); return; } @@ -1009,6 +1044,7 @@ function zoomTimeline(zoomRatio: number, ignorePointer=false){ } // Account for zooming into sub-year dates before MIN_CAL_DATE if (newStart.isEarlier(MIN_CAL_DATE, newScale) && (newScale == MONTH_SCALE || newScale == DAY_SCALE)){ + animateFailDiv('bg'); console.log('Unable to zoom into range where month/day scale is invalid'); return; } @@ -1330,4 +1366,29 @@ function countDivStyles(tickIdx: number, count: number): Record<string,string> { transitionTimingFunction: 'linear', } } +function animateFailDiv(which: 'min' | 'max' | 'both' | 'bg'){ + if (which == 'min'){ + animateWithClass(minFailRef.value!, 'animate-show-then-fade'); + } else if (which == 'max'){ + animateWithClass(maxFailRef.value!, 'animate-show-then-fade'); + } else if (which == 'both'){ + animateWithClass(minFailRef.value!, 'animate-show-then-fade'); + animateWithClass(maxFailRef.value!, 'animate-show-then-fade'); + } else { + animateWithClass(bgFailRef.value!, 'animate-red-then-fade'); + } +} +function failDivStyles(minDiv: boolean){ + const gradientDir = props.vert ? (minDiv ? 'top' : 'bottom') : (minDiv ? 'left' : 'right'); + return { + top: props.vert ? (minDiv ? 0 : 'auto') : 0, + bottom: props.vert ? (minDiv ? 'auto' : 0) : 'auto', + left: props.vert ? 0 : (minDiv ? 0 : 'auto'), + right: props.vert ? 'auto' : (minDiv ? 'auto' : 0), + width: props.vert ? '100%' : '2cm', + height: props.vert ? '2cm' : '100%', + backgroundImage: `linear-gradient(to ${gradientDir}, rgba(255,0,0,0), rgba(255,0,0,0.3))`, + opacity: 0, + }; +} </script> diff --git a/src/index.css b/src/index.css index 2a7819b..e0545a1 100644 --- a/src/index.css +++ b/src/index.css @@ -38,6 +38,19 @@ background-color: transparent; } } +.animate-show-then-fade { + animation-name: show-then-fade; + animation-duration: 500ms; + animation-timing-function: ease-in; +} +@keyframes show-then-fade { + from { + opacity: 1; + } + to { + opacity: 0; + } +} .animate-flash-yellow { animation-name: flash-yellow; animation-duration: 700ms; @@ -59,6 +59,11 @@ export function getNumTrailingZeros(n: number): number { } throw new Error('Exceeded floating point precision'); } +export function animateWithClass(el: HTMLElement, className: string){ + el.classList.remove(className); + el.offsetWidth; // Triggers reflow + el.classList.add(className); +} // For calendar conversion (mostly copied from backend/hist_data/cal.py) export function gregorianToJdn(year: number, month: number, day: number): number { |
