aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.vue81
-rw-r--r--src/components/BaseLine.vue43
-rw-r--r--src/components/TimeLine.vue165
-rw-r--r--src/lib.ts10
-rw-r--r--src/store.ts28
5 files changed, 195 insertions, 132 deletions
diff --git a/src/App.vue b/src/App.vue
index 7d7cd98..308137b 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,33 +1,34 @@
<template>
-<div class="absolute left-0 top-0 w-screen h-screen overflow-hidden flex flex-col" style="bg-stone-800" >
+<div class="absolute left-0 top-0 w-screen h-screen overflow-hidden flex flex-col">
<!-- Title bar -->
- <div class="flex gap-2 p-2 bg-stone-900 text-yellow-500">
- <h1 class="my-auto ml-2 text-4xl">Histplorer</h1>
+ <div class="flex gap-2 p-2" :style="{backgroundColor: store.color.bgDark2}">
+ <h1 class="my-auto ml-2 text-4xl" :style="{color: store.color.altDark}">Histplorer</h1>
<div class="mx-auto"/> <!-- Spacer -->
<!-- Icons -->
- <icon-button :size="45" class="text-stone-50 bg-yellow-600" @click="onTimelineAdd" title="Add a timeline">
+ <icon-button :size="45" :style="buttonStyles" @click="onTimelineAdd" title="Add a timeline">
<plus-icon/>
</icon-button>
- <icon-button :size="45" class="text-stone-50 bg-yellow-600">
+ <icon-button :size="45" :style="buttonStyles">
<settings-icon/>
</icon-button>
- <icon-button :size="45" class="text-stone-50 bg-yellow-600">
+ <icon-button :size="45" :style="buttonStyles">
<help-icon/>
</icon-button>
</div>
<!-- Content area -->
- <div class="grow min-h-0 bg-stone-800 flex" :class="{'flex-col': !vert}" ref="contentAreaRef">
- <time-line v-for="(data, idx) in timelineData" :key="data"
- :vert="vert" :initialStart="data.start" :initialEnd="data.end"
+ <div class="grow min-h-0 flex" :class="{'flex-col': !vert}"
+ :style="{backgroundColor: store.color.bg}" ref="contentAreaRef">
+ <time-line v-for="(range, idx) in timelineRanges" :key="range.id"
+ :vert="vert" :initialStart="range.start" :initialEnd="range.end"
class="grow basis-full min-h-0 outline outline-1"
- @close="onTimelineClose(idx)" @bound-chg="onBoundChg($event, idx)"/>
- <base-line :vert="vert" :timelineData="timelineData"/>
+ @remove="onTimelineRemove(idx)" @range-chg="onRangeChg($event, idx)"/>
+ <base-line :vert="vert" :timelineRanges="timelineRanges"/>
</div>
</div>
</template>
<script setup lang="ts">
-import {ref, computed, onMounted, onUnmounted} from 'vue';
+import {ref, computed, onMounted, onUnmounted, Ref} from 'vue';
// Components
import TimeLine from './components/TimeLine.vue';
import BaseLine from './components/BaseLine.vue';
@@ -36,15 +37,22 @@ import IconButton from './components/IconButton.vue';
import PlusIcon from './components/icon/PlusIcon.vue';
import SettingsIcon from './components/icon/SettingsIcon.vue';
import HelpIcon from './components/icon/HelpIcon.vue';
+// Other
+import {TimelineRange} from './lib';
+import {useStore} from './store';
// Refs
const contentAreaRef = ref(null as HTMLElement | null);
-// For content sizing
+// Global store
+const store = useStore();
+
+// For content sizing (used to decide between horizontal and vertical mode)
const contentWidth = ref(window.innerWidth);
const contentHeight = ref(window.innerHeight);
// Setting this and contentWidth to 0 makes it likely that 'vert' will change on startup,
// and trigger unwanted transitions (like baseline spans changing size)
+const vert = computed(() => contentHeight.value > contentWidth.value);
function updateAreaDims(){
let contentAreaEl = contentAreaRef.value!;
contentWidth.value = contentAreaEl.offsetWidth;
@@ -52,35 +60,36 @@ function updateAreaDims(){
}
onMounted(updateAreaDims)
-// For multiple timelines
-const vert = computed(() => contentHeight.value > contentWidth.value);
-const timelineData = ref([]);
+// Timeline data
+const timelineRanges: Ref<TimelineRange[]> = ref([]);
let nextTimelineId = 1;
-function genTimelineData(){
- let data = {id: nextTimelineId, start: -500, end: 500};
+function addNewTimelineRange(){
+ timelineRanges.value.push({id: nextTimelineId, start: -500, end: 500});
nextTimelineId++;
- return data;
}
-timelineData.value.push(genTimelineData());
+addNewTimelineRange();
+function onRangeChg(newBounds: [number, number], idx: number){
+ let range = timelineRanges.value[idx];
+ range.start = newBounds[0];
+ range.end = newBounds[1];
+}
+
+// For timeline addition/removal
+const MIN_TIMELINE_BREADTH = 150;
function onTimelineAdd(){
- if (vert.value && contentWidth.value / (timelineData.value.length + 1) < 150 ||
- !vert.value && contentHeight.value / (timelineData.value.length + 1) < 150){
- console.log('Reached timeline min size');
+ if (vert.value && contentWidth.value / (timelineRanges.value.length + 1) < MIN_TIMELINE_BREADTH ||
+ !vert.value && contentHeight.value / (timelineRanges.value.length + 1) < MIN_TIMELINE_BREADTH){
+ console.log('Reached timeline minimum breadth');
return;
}
- timelineData.value.push(genTimelineData());
+ addNewTimelineRange();
}
-function onTimelineClose(idx: number){
- if (timelineData.value.length == 1){
- console.log('Ignored close for last timeline')
+function onTimelineRemove(idx: number){
+ if (timelineRanges.value.length == 1){
+ console.log('Ignored removal of last timeline')
return;
}
- timelineData.value.splice(idx, 1);
-}
-function onBoundChg(newBounds: [number, number], idx: number){
- let data = timelineData.value[idx];
- data.start = newBounds[0];
- data.end = newBounds[1];
+ timelineRanges.value.splice(idx, 1);
}
// For resize handling
@@ -107,4 +116,10 @@ async function onResize(){
}
onMounted(() => window.addEventListener('resize', onResize));
onUnmounted(() => window.removeEventListener('resize', onResize));
+
+// Styles
+const buttonStyles = computed(() => ({
+ color: store.color.text,
+ backgroundColor: store.color.altDark2,
+}));
</script>
diff --git a/src/components/BaseLine.vue b/src/components/BaseLine.vue
index 6f2dbc9..3f51a86 100644
--- a/src/components/BaseLine.vue
+++ b/src/components/BaseLine.vue
@@ -1,41 +1,45 @@
<template>
-<div class="bg-stone-900 text-stone-50 flex relative" :class="{'flex-col': vert}" ref="rootRef">
+<div class="flex relative" :class="{'flex-col': vert}"
+ :style="{color: store.color.text, backgroundColor: store.color.bgDark}" ref="rootRef">
<div v-for="p in periods" :key="p.label" :style="periodStyles(p)">
<div :style="labelStyles">{{p.label}}</div>
</div>
<TransitionGroup name="fade">
- <div v-for="d in timelineData" :key="d.id"
- class="absolute bg-yellow-200/30" :style="spanStyles(d)">
- {{d.id}}
+ <div v-for="range in timelineRanges" :key="range.id" class="absolute" :style="spanStyles(range)">
+ {{range.id}}
</div>
</TransitionGroup>
</div>
</template>
<script setup lang="ts">
-import {ref, computed, onMounted} from 'vue';
-import {MIN_DATE, MAX_DATE} from '../lib';
+import {ref, computed, onMounted, PropType, Ref} from 'vue';
+import {MIN_DATE, MAX_DATE, WRITING_MODE_HORZ, TimelineRange} from '../lib';
+import {useStore} from '../store';
// Refs
const rootRef = ref(null as HTMLElement | null);
+// Global store
+const store = useStore();
+
// Props
const props = defineProps({
vert: {type: Boolean, required: true},
- timelineData: {type: Object, required: true},
+ timelineRanges: {type: Object as PropType<TimelineRange[]>, required: true},
});
-// Static time periods to represent
-const periods = ref([
+// Static time periods
+type Period = {label: string, len: number};
+const periods: Ref<Period[]> = ref([
{label: 'One', len: 1},
{label: 'Two', len: 2},
{label: 'Three', len: 1},
]);
-// For size tracking
+// For size tracking (used to prevent time spans shrinking below 1 pixel)
const width = ref(0);
const height = ref(0);
-const WRITING_MODE_HORZ = window.getComputedStyle(document.body)['writing-mode'].startsWith('horizontal');
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries){
if (entry.contentBoxSize){
@@ -48,36 +52,36 @@ const resizeObserver = new ResizeObserver((entries) => {
onMounted(() => resizeObserver.observe(rootRef.value as HTMLElement));
// Styles
-function periodStyles(period){
+function periodStyles(period: Period){
return {
outline: '1px solid gray',
flexGrow: period.len,
};
}
-const labelStyles: Record<string,string> = computed(() => ({
+const labelStyles = computed((): Record<string, string> => ({
transform: props.vert ? 'rotate(90deg) translate(50%, 0)' : 'none',
whiteSpace: 'nowrap',
width: props.vert ? '40px' : 'auto',
padding: props.vert ? '0' : '4px',
}));
-function spanStyles(d){
+function spanStyles(range: TimelineRange){
let styles: Record<string,string>;
let availLen = props.vert ? height.value : width.value;
- let startFrac = (d.start - MIN_DATE) / (MAX_DATE - MIN_DATE);
- let lenFrac = (d.end - d.start) / (MAX_DATE - MIN_DATE);
+ let startFrac = (range.start - MIN_DATE) / (MAX_DATE - MIN_DATE);
+ let lenFrac = (range.end - range.start) / (MAX_DATE - MIN_DATE);
let startPx = Math.max(0, availLen * startFrac); // Prevent negatives due to end-padding
let lenPx = Math.min(availLen - startPx, availLen * lenFrac);
lenPx = Math.max(1, lenPx);
if (props.vert){
styles = {
top: startPx + 'px',
- left: 0,
+ left: '0',
height: lenPx + 'px',
width: '100%',
}
} else {
styles = {
- top: 0,
+ top: '0',
left: startPx + 'px',
height: '100%',
width: lenPx + 'px',
@@ -86,6 +90,9 @@ function spanStyles(d){
return {
...styles,
transition: 'all 300ms ease-out',
+ color: 'black',
+ backgroundColor: store.color.alt,
+ opacity: 0.3,
};
}
</script>
diff --git a/src/components/TimeLine.vue b/src/components/TimeLine.vue
index 12dd7bc..bf886b1 100644
--- a/src/components/TimeLine.vue
+++ b/src/components/TimeLine.vue
@@ -1,31 +1,35 @@
<template>
<div class="touch-none relative"
- @wheel.exact.prevent="onWheel" @wheel.shift.exact.prevent="onShiftWheel"
@pointerdown.prevent="onPointerDown" @pointermove.prevent="onPointerMove" @pointerup.prevent="onPointerUp"
@pointercancel.prevent="onPointerUp" @pointerout.prevent="onPointerUp" @pointerleave.prevent="onPointerUp"
+ @wheel.exact.prevent="onWheel" @wheel.shift.exact.prevent="onShiftWheel"
ref="rootRef">
<svg :viewBox="`0 0 ${width} ${height}`">
- <line stroke="yellow" stroke-width="2px" x1="-1" y1="0" x2="2" y2="0" :style="mainlineStyles"/>
- <!-- Line from (0,0) to (0,1), with extra length to avoid gaps during resize transitions -->
+ <!-- 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="n in ticks" :key="n">
<line v-if="n == MIN_DATE || n == MAX_DATE"
: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="yellow" :stroke-width="`${END_TICK_SZ * 2}px`" :style="tickStyles(n)" class="animate-fadein"/>
+ :stroke="store.color.alt" :stroke-width="`${END_TICK_SZ * 2}px`"
+ :style="tickStyles(n)" 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"
- stroke="yellow" stroke-width="1px" :style="tickStyles(n)" class="animate-fadein"/>
+ :stroke="store.color.alt" stroke-width="1px" :style="tickStyles(n)" class="animate-fadein"/>
</template>
- <text fill="#606060" v-for="n in ticks" :key="n"
+ <!-- Tick labels -->
+ <text :fill="store.color.textDark" v-for="n in ticks" :key="n"
x="0" y="0" :text-anchor="vert ? 'start' : 'middle'" dominant-baseline="middle"
:style="tickLabelStyles(n)" class="text-sm animate-fadein">
{{Math.floor(n * 10) / 10}}
</text>
</svg>
- <!-- Icons -->
- <icon-button :size="30" class="absolute top-2 right-2 text-stone-50 bg-yellow-600"
- @click="onClose" title="Remove timeline">
+ <!-- Buttons -->
+ <icon-button :size="30" class="absolute top-2 right-2"
+ :style="{color: store.color.text, backgroundColor: store.color.altDark2}"
+ @click="emit('remove')" title="Remove timeline">
<minus-icon/>
</icon-button>
</div>
@@ -33,38 +37,41 @@
<script setup lang="ts">
import {ref, onMounted, computed, watch} from 'vue';
-import {MIN_DATE, MAX_DATE, SCALES} from '../lib';
// Components
import IconButton from './IconButton.vue';
// Icons
import MinusIcon from './icon/MinusIcon.vue';
+// Other
+import {MIN_DATE, MAX_DATE, SCALES, WRITING_MODE_HORZ} from '../lib';
+import {useStore} from '../store';
// Refs
const rootRef = ref(null as HTMLElement | null);
+// Global store
+const store = useStore();
+
// Props + events
const props = defineProps({
vert: {type: Boolean, required: true},
initialStart: {type: Number, required: true},
initialEnd: {type: Number, required: true},
});
-const emit = defineEmits(['close', 'bound-chg']);
-
-// For skipping transitions on startup (and horz/vert switch)
-const skipTransition = ref(true);
-onMounted(() => setTimeout(() => {skipTransition.value = false}, 100));
+const emit = defineEmits(['remove', 'range-chg']);
// For size tracking
const width = ref(0);
const height = ref(0);
-const prevVert = ref(props.vert);
-const WRITING_MODE_HORZ = window.getComputedStyle(document.body)['writing-mode'].startsWith('horizontal');
+const availLen = computed(() => props.vert ? height.value : width.value);
+const prevVert = ref(props.vert); // For skipping transitions on horz/vert swap
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries){
if (entry.contentBoxSize){
+ // Get resized dimensions
const boxSize = Array.isArray(entry.contentBoxSize) ? entry.contentBoxSize[0] : entry.contentBoxSize;
width.value = WRITING_MODE_HORZ ? boxSize.inlineSize : boxSize.blockSize;
height.value = WRITING_MODE_HORZ ? boxSize.blockSize : boxSize.inlineSize;
+ // Check for horz/vert swap
if (props.vert != prevVert.value){
skipTransition.value = true;
setTimeout(() => {skipTransition.value = false}, 100); // Note: Using nextTick() doesn't work
@@ -75,19 +82,11 @@ const resizeObserver = new ResizeObserver((entries) => {
});
onMounted(() => resizeObserver.observe(rootRef.value as HTMLElement));
-// Vars
+// Timeline data
const startDate = ref(props.initialStart); // Lowest date on displayed timeline
const endDate = ref(props.initialEnd);
let scaleIdx = 0; // Index of current scale in SCALES
-const SCROLL_SHIFT_CHG = 0.2; // Proportion of timeline length to shift by upon scroll
-const ZOOM_RATIO = 1.5; // When zooming out, the timeline length gets multiplied by this ratio
-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 padUnits = computed(() => props.vert ? 0.5 : 1); // Amount of extra scale units to add before/after min/max date
-const TICK_LEN = 8;
-const END_TICK_SZ = 4; // Size for MIN_DATE/MAX_DATE ticks
-const availLen = computed(() => props.vert ? height.value : width.value);
-
// Initialise to smallest usable scale
function initScale(){
let dateLen = endDate.value - startDate.value;
@@ -101,46 +100,51 @@ function initScale(){
}
onMounted(initScale);
-const ticks = computed((): number[] => { // Holds date value for each tick
+// Tick data
+const TICK_LEN = 8;
+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 ticks = computed((): number[] => { // Array of date values for each tick
let dateLen = endDate.value - startDate.value;
- let shiftChg = dateLen * SCROLL_SHIFT_CHG;
- let scaleChg = dateLen * (ZOOM_RATIO - 1) / 2;
+ let panLen = dateLen * store.scrollRatio;
+ let zoomLen = dateLen * (store.zoomRatio - 1) / 2;
let scale = SCALES[scaleIdx];
- // Get ticks in new range, and add hidden ticks that might transition in on a shift action
- let tempTicks = [];
- let next = Math.ceil((Math.max(MIN_DATE, startDate.value - shiftChg) - MIN_DATE) / scale);
- let last = Math.floor((Math.min(MAX_DATE, endDate.value + shiftChg) - MIN_DATE) / scale);
+ // Get ticks in new range, and add hidden ticks that might transition in after panning
+ let tempTicks: number[] = [];
+ let next = Math.ceil((Math.max(MIN_DATE, startDate.value - panLen) - MIN_DATE) / scale);
+ let last = Math.floor((Math.min(MAX_DATE, endDate.value + panLen) - MIN_DATE) / scale);
while (next <= last){
tempTicks.push(MIN_DATE + next * scale);
next++;
}
- // Get hidden ticks that might transition in on a zoom action
- let tempTicks2 = [];
- let tempTicks3 = [];
+ // Get hidden ticks that might transition in after zooming
+ let tempTicks2: number[] = [];
+ let tempTicks3: number[] = [];
if (scaleIdx > 0){
scale = SCALES[scaleIdx-1];
- let first = Math.ceil((Math.max(MIN_DATE, startDate.value - scaleChg) - MIN_DATE) / scale);
- while ((MIN_DATE + first * scale) < tempTicks[0]){
+ let first = Math.ceil((Math.max(MIN_DATE, startDate.value - zoomLen) - MIN_DATE) / scale);
+ while (MIN_DATE + first * scale < tempTicks[0]){
tempTicks2.push(MIN_DATE + first * scale);
first++;
}
- let last = Math.floor((Math.min(MAX_DATE, endDate.value + scaleChg) - MIN_DATE) / scale);
+ let last = Math.floor((Math.min(MAX_DATE, endDate.value + zoomLen) - MIN_DATE) / scale);
let next = Math.floor((tempTicks[tempTicks.length - 1] - MIN_DATE) / scale) + 1;
while (next <= last){
tempTicks3.push(MIN_DATE + next * scale);
next++;
}
}
- //
- return [].concat(tempTicks2, tempTicks, tempTicks3);
+ // Join into tick array
+ return [...tempTicks2, ...tempTicks, ...tempTicks3];
});
-// Performs a shift action
-function shiftTimeline(n: number){
+// For panning/zooming
+function panTimeline(n: number){
let dateLen = endDate.value - startDate.value;
- let extraPad = padUnits.value * SCALES[scaleIdx]
- let paddedMinDate = MIN_DATE - extraPad;
- let paddedMaxDate = MAX_DATE + extraPad;
+ let extraLen = padUnits.value * SCALES[scaleIdx]
+ let paddedMinDate = MIN_DATE - extraLen;
+ let paddedMaxDate = MAX_DATE + extraLen;
let chg = dateLen * n;
if (startDate.value + chg < paddedMinDate){
if (startDate.value == paddedMinDate){
@@ -163,13 +167,12 @@ function shiftTimeline(n: number){
endDate.value += chg;
}
}
-// Performs a zoom action
function zoomTimeline(frac: number){
let oldDateLen = endDate.value - startDate.value;
let newDateLen = oldDateLen * frac;
- let extraPad = padUnits.value * SCALES[scaleIdx]
- let paddedMinDate = MIN_DATE - extraPad;
- let paddedMaxDate = MAX_DATE + extraPad;
+ let extraLen = padUnits.value * SCALES[scaleIdx]
+ let paddedMinDate = MIN_DATE - extraLen;
+ let paddedMaxDate = MAX_DATE + extraLen;
// Get new bounds
let newStart: number;
let newEnd: number;
@@ -178,12 +181,14 @@ function zoomTimeline(frac: number){
let lenChg = newDateLen - oldDateLen
newStart = startDate.value - lenChg / 2;
newEnd = endDate.value + lenChg / 2;
- } else {
- let innerOffset = 0; // Element-relative ptrOffset
+ } else { // Pointer-centered zoom
+ // Get element-relative ptrOffset
+ let innerOffset = 0;
if (rootRef.value != null){ // Can become null during dev-server hot-reload for some reason
let rect = rootRef.value.getBoundingClientRect();
innerOffset = props.vert ? ptrOffset - rect.top : ptrOffset - rect.left;
}
+ //
let zoomCenter = startDate.value + (innerOffset / availLen.value) * oldDateLen;
newStart = zoomCenter - (zoomCenter - startDate.value) * frac;
newEnd = zoomCenter + (endDate.value - zoomCenter) * frac;
@@ -239,22 +244,22 @@ function zoomTimeline(frac: number){
}
// For mouse/etc handling
-let pointerX = null; // Stores pointer position (used for pointer-centered zooming)
-let pointerY = null;
-const ptrEvtCache = []; // Holds last captured PointerEvent for each pointerId (used for pinch-zoom)
+let pointerX: number | null = null; // Used for pointer-centered zooming
+let pointerY: number | null = null;
+const ptrEvtCache: PointerEvent[] = []; // Holds last captured PointerEvent for each pointerId (used for pinch-zoom)
let lastPinchDiff = -1; // Holds last x/y distance between two pointers that are down
let dragDiff = 0; // Holds accumlated change in pointer's x/y coordinate while dragging
let dragHandler = 0; // Set by a setTimeout() to a handler for pointer dragging
let dragVelocity: number; // Used to add 'drag momentum'
-let vUpdateTime: number; // Holds timestamp for last update of 'dragVelocityY'
+let vUpdateTime: number; // Holds timestamp for last update of 'dragVelocity'
let vPrevPointer: null | number; // Holds pointerX/pointerY used for last update of 'dragVelocity'
let vUpdater = 0; // Set by a setInterval(), used to update 'dragVelocity'
function onPointerDown(evt: PointerEvent){
ptrEvtCache.push(evt);
- // Update stored cursor position
+ // Update pointer position
pointerX = evt.clientX;
pointerY = evt.clientY;
- // Update vars for dragging
+ // Update data for dragging
dragDiff = 0;
dragVelocity = 0;
vUpdateTime = Date.now();
@@ -262,7 +267,7 @@ function onPointerDown(evt: PointerEvent){
vUpdater = setInterval(() => {
if (vPrevPointer != null){
let time = Date.now();
- let ptrDiff = (props.vert ? pointerY : pointerX) - vPrevPointer;
+ let ptrDiff = (props.vert ? pointerY! : pointerX!) - vPrevPointer;
dragVelocity = ptrDiff / (time - vUpdateTime) * 1000;
vUpdateTime = time;
}
@@ -276,11 +281,11 @@ function onPointerMove(evt: PointerEvent){
//
if (ptrEvtCache.length == 1){
// Handle pointer dragging
- dragDiff += props.vert ? evt.clientY - pointerY : evt.clientX - pointerX;
+ dragDiff += props.vert ? evt.clientY - pointerY! : evt.clientX - pointerX!;
if (dragHandler == 0){
dragHandler = setTimeout(() => {
if (Math.abs(dragDiff) > 2){
- shiftTimeline(-dragDiff / availLen.value);
+ panTimeline(-dragDiff / availLen.value);
dragDiff = 0;
}
dragHandler = 0;
@@ -315,8 +320,8 @@ function onPointerUp(evt: PointerEvent){
clearInterval(vUpdater);
vUpdater = 0;
if (lastPinchDiff == -1 && Math.abs(dragVelocity) > 10){
- let scrollChg = dragVelocity * 0.1;
- shiftTimeline(-scrollChg / availLen.value);
+ let scrollChg = dragVelocity * store.dragInertia;
+ panTimeline(-scrollChg / availLen.value);
}
}
//
@@ -330,34 +335,32 @@ function onWheel(evt: WheelEvent){
if (!props.vert){
shiftDir *= -1;
}
- shiftTimeline(shiftDir * SCROLL_SHIFT_CHG);
+ panTimeline(shiftDir * store.scrollRatio);
}
function onShiftWheel(evt: WheelEvent){
- if (evt.deltaY > 0){
- zoomTimeline(ZOOM_RATIO);
- } else {
- zoomTimeline(1/ZOOM_RATIO);
- }
-}
-
-// For button handling
-function onClose(){
- emit('close');
+ let zoomRatio = evt.deltaY > 0 ? store.zoomRatio : 1/store.zoomRatio;
+ zoomTimeline(zoomRatio);
}
// For bound-change signalling
watch(startDate, () => {
- emit('bound-chg', [startDate.value, endDate.value]);
+ emit('range-chg', [startDate.value, endDate.value]);
});
+// For skipping transitions on startup (and on horz/vert swap)
+const skipTransition = ref(true);
+onMounted(() => setTimeout(() => {skipTransition.value = false}, 100));
+
// Styles
+const transitionDuration = '300ms';
+const transitionTimingFunction = 'ease-out';
const mainlineStyles = computed(() => ({
transform: props.vert ?
`translate(${width.value/2}px, 0) rotate(90deg) scale(${height.value},1)` :
`translate(0, ${height.value/2}px) scale(${width.value},1)`,
transitionProperty: skipTransition.value ? 'none' : 'transform',
- transitionDuration: '300ms',
- transitionTimingFunction: 'ease-out',
+ transitionDuration,
+ transitionTimingFunction,
}));
function tickStyles(tick: number){
let offset = (tick - startDate.value) / (endDate.value - startDate.value) * availLen.value;
@@ -370,8 +373,8 @@ function tickStyles(tick: number){
`translate(${width.value/2}px, ${offset}px) scale(${scale})` :
`translate(${offset}px, ${height.value/2}px) scale(${scale})`,
transitionProperty: skipTransition.value ? 'none' : 'transform, opacity',
- transitionDuration: '300ms',
- transitionTimingFunction: 'ease-out',
+ transitionDuration,
+ transitionTimingFunction,
opacity: (offset >= 0 && offset <= availLen.value) ? 1 : 0,
}
}
@@ -383,8 +386,8 @@ function tickLabelStyles(tick: number){
`translate(${width.value / 2 + 20}px, ${offset}px)` :
`translate(${offset}px, ${height.value / 2 + 30}px)`,
transitionProperty: skipTransition.value ? 'none' : 'transform, opacity',
- transitionDuration: '300ms',
- transitionTimingFunction: 'ease-out',
+ transitionDuration,
+ transitionTimingFunction,
opacity: (offset >= labelSz && offset <= availLen.value - labelSz) ? 1 : 0,
}
}
diff --git a/src/lib.ts b/src/lib.ts
index 1e4b1f9..28994f1 100644
--- a/src/lib.ts
+++ b/src/lib.ts
@@ -5,3 +5,13 @@
export const MIN_DATE = -1000;
export const MAX_DATE = 1000;
export const SCALES = [200, 50, 10, 1, 0.2]; // Timeline gets divided into units of SCALES[0], then SCALES[1], etc
+
+export const WRITING_MODE_HORZ =
+ window.getComputedStyle(document.body)['writing-mode' as any].startsWith('horizontal');
+ // Used with ResizeObserver callbacks, to determine which resized dimensions are width and height
+
+export type TimelineRange = {
+ id: number,
+ start: number,
+ end: number,
+};
diff --git a/src/store.ts b/src/store.ts
new file mode 100644
index 0000000..33088b2
--- /dev/null
+++ b/src/store.ts
@@ -0,0 +1,28 @@
+/*
+ * Defines a global store for UI settings, palette colors, etc
+ */
+
+import {defineStore} from 'pinia';
+
+export const useStore = defineStore('store', {
+ state: () => {
+ const color = { // Note: For scrollbar colors on chrome, edit ./index.css
+ text: '#fafaf9', // stone-50
+ textDark: '#a8a29e', // stone-400
+ bg: '#292524', // stone-800
+ bgLight: '#44403c', // stone-700
+ bgDark: '#1c1917', // stone-900
+ bgLight2: '#57534e', // stone-600
+ bgDark2: '#0e0c0b', // darker version of stone-900
+ alt: '#fde047', // yellow-300
+ altDark: '#eab308', // yellow-500
+ altDark2: '#ca8a04', // yellow-600
+ };
+ return {
+ color,
+ scrollRatio: 0.2, // Fraction of timeline length to move by upon scroll
+ zoomRatio: 1.5, // Ratio of timeline expansion upon zooming out
+ dragInertia: 0.1, // Multiplied by final-drag-speed (pixels-per-sec) to get extra scroll distance
+ };
+ },
+});