From 220c9bf8b3bee11f3119860bb14bdb5da3f040e9 Mon Sep 17 00:00:00 2001
From: Terry Truong <terry06890@gmail.com>
Date: Tue, 18 Oct 2022 23:20:59 +1100
Subject: Add basic grid-like event layout

Add lines linking events to timeline
Add scrim on event image
---
 src/components/TimeLine.vue | 92 +++++++++++++++++++++++++++++++++++++++------
 1 file changed, 80 insertions(+), 12 deletions(-)

(limited to 'src/components/TimeLine.vue')

diff --git a/src/components/TimeLine.vue b/src/components/TimeLine.vue
index 88ac62d..c581634 100644
--- a/src/components/TimeLine.vue
+++ b/src/components/TimeLine.vue
@@ -5,8 +5,6 @@
 	@wheel.exact.prevent="onWheel" @wheel.shift.exact.prevent="onShiftWheel"
 	ref="rootRef">
 	<svg :viewBox="`0 0 ${width} ${height}`">
-		<!-- 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="date, idx in ticks.dates" :key="date.toInt()">
 			<line v-if="date.equals(MIN_DATE, scale) || date.equals(MAX_DATE, scale)"
@@ -20,16 +18,22 @@
 				:stroke="store.color.alt" stroke-width="1px"
 				:style="tickStyles(idx)" class="animate-fadein"/>
 		</template>
+		<!-- Event lines -->
+		<line v-for="id in eventLines.keys()" :key="id"
+			x1="0" y1="0" x2="1" y2="0" :stroke="store.color.altDark2" stroke-width="1px"
+			:style="eventLineStyles(id)" class="animate-fadein"/>
 		<!-- Tick labels -->
 		<text v-for="date, idx in ticks.dates" :key="date.toInt()"
 			x="0" y="0" :text-anchor="vert ? 'start' : 'middle'" dominant-baseline="middle"
 			:fill="store.color.textDark" :style="tickLabelStyles(idx)" class="text-sm animate-fadein">
 			{{date}}
 		</text>
+		<!-- 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"/>
 	</svg>
 	<!-- Events -->
 	<div v-for="id in idToPos.keys()" :key="id"
-		class="absolute bg-black text-white border border-white rounded animate-fadein"
+		class="absolute bg-black text-white rounded animate-fadein text-sm"
 		:style="eventStyles(id)">
 		{{idToEvent.get(id)!.title}}
 	</div>
@@ -55,6 +59,8 @@ import {WRITING_MODE_HORZ, MIN_DATE, MAX_DATE, MONTH_SCALE, DAY_SCALE, SCALES, M
 import {useStore} from '../store';
 import {RBTree} from '../rbtree';
 
+const SCRIM_GRADIENT = 'linear-gradient(to bottom, rgba(0,0,0,0.4), #0000 40%)';
+
 // Refs
 const rootRef: Ref<HTMLElement | null> = ref(null);
 
@@ -73,6 +79,7 @@ const emit = defineEmits(['remove', 'state-chg', 'event-req', 'event-display']);
 const width = ref(0);
 const height = ref(0);
 const availLen = computed(() => props.vert ? height.value : width.value);
+const availBreadth = computed(() => props.vert ? width.value : height.value);
 const prevVert = ref(props.vert); // For skipping transitions on horz/vert swap
 const mounted = ref(false);
 onMounted(() => {
@@ -273,15 +280,42 @@ const idToPos = computed(() => {
 		return new Map();
 	}
 	let map: Map<number, [number, number, number, number]> = new Map(); // Maps visible event IDs to x/y/w/h
-	let numUnits = getNumVisibleUnits();
-	let minorAxisStep = 0;
+	// Do basic grid-like layout
+	let spacing = 10, sz = 80, midOffset = [10, 40];
+	let posX = spacing, posY = spacing;
 	for (let event of idToEvent.value.values()){
-		let unitOffset = getUnitDiff(event.start, startDate.value, scale.value) + startOffset.value;
-		let posFrac = unitOffset / numUnits;
-		let posX = props.vert ? minorAxisStep : availLen.value * posFrac;
-		let posY = props.vert ? availLen.value * posFrac : minorAxisStep;
-		map.set(event.id, [posX, posY, 100, 100]);
-		minorAxisStep += 10;
+		map.set(event.id, [posX, posY, sz, sz]);
+		if (props.vert){
+			posY += sz + spacing;
+			if (posY + sz + spacing > availLen.value){ // If at end of column
+				posY = spacing;
+				posX += sz + spacing;
+				// Avoid collision with timeline
+				if (posX <= availBreadth.value / 2 + midOffset[1] &&
+						posX + sz >= availBreadth.value / 2 - midOffset[0]){
+					posX = availBreadth.value / 2 + midOffset[1];
+				}
+				// If finished last row
+				if (posX + sz + spacing > availBreadth.value){
+					break;
+				}
+			}
+		} else {
+			posX += sz + spacing;
+			if (posX + sz + spacing > availLen.value){ // If at end of row
+				posX = spacing;
+				posY += sz + spacing;
+				// Avoid collision with timeline
+				if (posY <= availBreadth.value / 2 + midOffset[1] &&
+						posY + sz >= availBreadth.value / 2 - midOffset[0]){
+					posY = availBreadth.value / 2 + midOffset[1];
+				}
+				// If finished last column
+				if (posY + sz + spacing > availBreadth.value){
+					break;
+				}
+			}
+		}
 	}
 	// If more events could be displayed, notify parent
 	if (map.size < 3){
@@ -292,6 +326,31 @@ const idToPos = computed(() => {
 	//
 	return map;
 });
+const eventLines = computed(() => {
+	let lineCoords: Map<number, [number, number, number, number]> = new Map();
+		// Maps event ID to x, y, length, and angle
+	let numUnits = getNumVisibleUnits();
+	for (let [id, [eventX, eventY, eventW, eventH]] of idToPos.value){
+		let x = eventX + eventW/2;
+		let y = eventY + eventH/2;
+		let x2: number;
+		let y2: number;
+		let event = idToEvent.value.get(id)!;
+		let unitOffset = getUnitDiff(event.start, startDate.value, scale.value) + startOffset.value;
+		let posFrac = unitOffset / numUnits;
+		if (props.vert){
+			x2 = availBreadth.value / 2;
+			y2 = posFrac * availLen.value;
+		} else {
+			x2 = posFrac * availLen.value;
+			y2 = availBreadth.value / 2;
+		}
+		let l = Math.sqrt((x-x2)**2 + (y-y2)**2);
+		let a = Math.atan2(y2-y, x2-x) * 180 / Math.PI;
+		lineCoords.set(id, [x, y, l, a]);
+	}
+	return lineCoords;
+});
 
 // For panning/zooming
 function panTimeline(scrollRatio: number){
@@ -748,11 +807,20 @@ function eventStyles(eventId: number){
 		top: y + 'px',
 		width: w + 'px',
 		height: h + 'px',
-		backgroundImage: `url(${getImagePath(event.imgId)})`,
+		backgroundImage: `${SCRIM_GRADIENT},url(${getImagePath(event.imgId)})`,
 		backgroundSize: 'cover',
 		transitionProperty: skipTransition.value ? 'none' : 'all',
 		transitionDuration,
 		transitionTimingFunction,
 	};
 }
+function eventLineStyles(eventId: number){
+	const [x, y, l, a] = eventLines.value.get(eventId)!;
+	return {
+		transform: `translate(${x}px, ${y}px) rotate(${a}deg) scaleX(${l})`,
+		transitionProperty: skipTransition.value ? 'none' : 'transform',
+		transitionDuration,
+		transitionTimingFunction,
+	};
+}
 </script>
-- 
cgit v1.2.3

