aboutsummaryrefslogtreecommitdiff
path: root/src/components/TileTree.vue
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/TileTree.vue')
-rw-r--r--src/components/TileTree.vue571
1 files changed, 0 insertions, 571 deletions
diff --git a/src/components/TileTree.vue b/src/components/TileTree.vue
deleted file mode 100644
index c0e4197..0000000
--- a/src/components/TileTree.vue
+++ /dev/null
@@ -1,571 +0,0 @@
-<script lang="ts">
-import {defineComponent, PropType} from 'vue';
-import Tile from './Tile.vue';
-import ParentBar from './ParentBar.vue';
-import TileInfoModal from './TileInfoModal.vue';
-import SearchModal from './SearchModal.vue';
-import HelpModal from './HelpModal.vue';
-import Settings from './Settings.vue';
-import {TolNode, LayoutNode, initLayoutTree, initLayoutMap, tryLayout, randWeightedChoice} from '../lib';
-import type {LayoutOptions} from '../lib';
-// Import paths lack a .ts or .js extension because .ts makes vue-tsc complain, and .js makes vite complain
-
-// Obtain tree-of-life data
-import tolRaw from '../tol.json';
-function preprocessTol(node: any): any {
- function helper(node: any, parent: any){
- //Add 'children' field if missing
- if (node.children == null){
- node.children = [];
- }
- //Add 'parent' field
- node.parent = parent;
- node.children.forEach((child: any) => helper(child, node));
- }
- helper(node, null);
- return node;
-}
-const tol: TolNode = preprocessTol(tolRaw);
-function getTolMap(tol: TolNode): Map<string,TolNode> {
- function helper(node: TolNode, map: Map<string,TolNode>){
- map.set(node.name, node);
- node.children.forEach(child => helper(child, map));
- }
- let map = new Map();
- helper(tol, map);
- return map;
-}
-const tolMap = getTolMap(tol);
-
-// Configurable settings
-const defaultLayoutOptions: LayoutOptions = {
- tileSpacing: 8, //px
- headerSz: 20, //px
- minTileSz: 50, //px
- maxTileSz: 200, //px
- layoutType: 'sweep', //'sqr' | 'rect' | 'sweep'
- rectMode: 'auto', //'horz' | 'vert' | 'linear' | 'auto'
- sweepMode: 'left', //'left' | 'top' | 'shorter' | 'auto'
- sweptNodesPrio: 'pow-2/3', //'linear' | 'sqrt' | 'pow-2/3'
- sweepingToParent: true,
-};
-const defaultComponentOptions = {
- // For leaf/non_leaf tile and separated-parent components
- borderRadius: 5, //px
- shadowNormal: '0 0 2px black',
- shadowHighlight: '0 0 1px 2px greenyellow',
- shadowFocused: '0 0 1px 2px orange',
- // For leaf and separated-parent components
- imgTilePadding: 4, //px
- imgTileFontSz: 15, //px
- imgTileColor: '#fafaf9',
- expandableImgTileColor: 'greenyellow', //yellow, greenyellow, turquoise,
- infoIconSz: 18, //px
- infoIconPadding: 2, //px
- infoIconColor: 'rgba(250,250,250,0.3)',
- infoIconHoverColor: 'white',
- // For non-leaf tile-group components
- nonLeafBgColors: ['#44403c', '#57534e'], //tiles at depth N use the Nth color, repeating from the start as needed
- nonLeafHeaderFontSz: 15, //px
- nonLeafHeaderColor: '#fafaf9',
- nonLeafHeaderBgColor: '#1c1917',
- // For tile-info modal
- infoModalImgSz: 200,
- // Timing related
- transitionDuration: 300, //ms
- clickHoldDuration: 400, //ms (duration after mousedown when a click-and-hold is recognised)
-};
-const defaultOwnOptions = {
- tileAreaOffset: 5, //px (space between root tile and display boundary)
- parentBarSz: defaultLayoutOptions.minTileSz * 2, //px (breadth of separated-parents area)
-};
-
-// Component holds a tree structure representing a subtree of 'tol' to be rendered
-// Collects events about tile expansion/collapse and window-resize, and initiates relayout of tiles
-export default defineComponent({
- data(){
- let layoutTree = initLayoutTree(tol, 0);
- return {
- layoutTree: layoutTree,
- activeRoot: layoutTree,
- layoutMap: initLayoutMap(layoutTree), // Maps names to LayoutNode objects
- tolMap: tolMap, // Maps names to TolNode objects
- //
- infoModalNode: null as TolNode | null, // Hides/unhides info modal, and provides the node to display
- searchOpen: false,
- settingsOpen: false,
- lastFocused: null as LayoutNode | null,
- animationActive: false,
- autoWaitTime: 500, //ms (in auto mode, time to wait after an action ends)
- helpOpen: false,
- // Options
- layoutOptions: {...defaultLayoutOptions},
- componentOptions: {...defaultComponentOptions},
- ...defaultOwnOptions,
- // For window-resize handling
- width: document.documentElement.clientWidth,
- height: document.documentElement.clientHeight,
- resizeThrottled: false,
- resizeDelay: 50, //ms (increasing to 100 seems to cause resize-skipping when opening browser mobile-view)
- };
- },
- computed: {
- wideArea(): boolean{
- return this.width >= this.height;
- },
- sepdParents(): LayoutNode[] | null {
- if (this.activeRoot == this.layoutTree){
- return null;
- }
- let parents = [];
- let node = this.activeRoot.parent;
- while (node != null){
- parents.push(node);
- node = node.parent;
- }
- return parents.reverse();
- },
- tileAreaPos(){
- let pos = [this.tileAreaOffset, this.tileAreaOffset] as [number, number];
- if (this.sepdParents != null){
- if (this.wideArea){
- pos[0] += this.parentBarSz;
- } else {
- pos[1] += this.parentBarSz;
- }
- }
- return pos;
- },
- tileAreaDims(){
- let dims = [
- this.width - this.tileAreaOffset*2,
- this.height - this.tileAreaOffset*2
- ] as [number, number];
- if (this.sepdParents != null){
- if (this.wideArea){
- dims[0] -= this.parentBarSz;
- } else {
- dims[1] -= this.parentBarSz;
- }
- }
- return dims;
- },
- parentBarDims(): [number, number] {
- if (this.wideArea){
- return [this.parentBarSz, this.height];
- } else {
- return [this.width, this.parentBarSz];
- }
- },
- styles(): Record<string,string> {
- return {
- position: 'absolute',
- left: '0',
- top: '0',
- width: '100vw', // Making this dynamic causes white flashes when resizing
- height: '100vh',
- backgroundColor: '#292524',
- overflow: 'hidden',
- };
- },
- },
- methods: {
- onResize(){
- if (!this.resizeThrottled){
- this.width = document.documentElement.clientWidth;
- this.height = document.documentElement.clientHeight;
- tryLayout(this.activeRoot, this.layoutMap,
- this.tileAreaPos, this.tileAreaDims, this.layoutOptions, true);
- // Prevent re-triggering until after a delay
- this.resizeThrottled = true;
- setTimeout(() => {this.resizeThrottled = false;}, this.resizeDelay);
- }
- },
- // For tile expand/collapse events
- onInnerLeafClicked({layoutNode, domNode}: {layoutNode: LayoutNode, domNode?: HTMLElement}){
- let success = tryLayout(this.activeRoot, this.layoutMap,
- this.tileAreaPos, this.tileAreaDims, this.layoutOptions, false, {type: 'expand', node: layoutNode});
- if (!success && domNode != null){
- // Trigger failure animation
- domNode.classList.remove('animate-expand-shrink');
- domNode.offsetWidth; // Triggers reflow
- domNode.classList.add('animate-expand-shrink');
- }
- return success;
- },
- onInnerHeaderClicked({layoutNode, domNode}: {layoutNode: LayoutNode, domNode?: HTMLElement}){
- let oldChildren = layoutNode.children;
- let success = tryLayout(this.activeRoot, this.layoutMap,
- this.tileAreaPos, this.tileAreaDims, this.layoutOptions, false, {type: 'collapse', node: layoutNode});
- if (!success && domNode != null){
- // Trigger failure animation
- domNode.classList.remove('animate-shrink-expand');
- domNode.offsetWidth; // Triggers reflow
- domNode.classList.add('animate-shrink-expand');
- }
- return success;
- },
- // For expand-to-view events
- onInnerLeafClickHeld(layoutNode: LayoutNode){
- if (layoutNode == this.activeRoot){
- console.log('Ignored expand-to-view on root node');
- return;
- }
- LayoutNode.hideUpward(layoutNode);
- this.activeRoot = layoutNode;
- tryLayout(this.activeRoot, this.layoutMap,
- this.tileAreaPos, this.tileAreaDims, this.layoutOptions, true, {type: 'expand', node: layoutNode});
- },
- onInnerHeaderClickHeld(layoutNode: LayoutNode){
- if (layoutNode == this.activeRoot){
- console.log('Ignored expand-to-view on active-root node');
- return;
- }
- LayoutNode.hideUpward(layoutNode);
- this.activeRoot = layoutNode;
- tryLayout(this.activeRoot, this.layoutMap, this.tileAreaPos, this.tileAreaDims, this.layoutOptions, true);
- },
- onSepdParentClicked(layoutNode: LayoutNode){
- LayoutNode.showDownward(layoutNode);
- this.activeRoot = layoutNode;
- tryLayout(this.activeRoot, this.layoutMap, this.tileAreaPos, this.tileAreaDims, this.layoutOptions, true);
- },
- // For info modal events
- onInnerInfoIconClicked(node: LayoutNode){
- this.closeModesAndSettings();
- this.infoModalNode = node.tolNode;
- },
- onInfoModalClose(){
- this.infoModalNode = null;
- },
- //
- onSettingsIconClick(){
- this.closeModesAndSettings();
- this.settingsOpen = true;
- },
- onSettingsClose(){
- this.settingsOpen = false;
- },
- onLayoutOptionChange(){
- tryLayout(this.activeRoot, this.layoutMap, this.tileAreaPos, this.tileAreaDims, this.layoutOptions, true);
- },
- //
- onSearchIconClick(){
- this.closeModesAndSettings();
- this.searchOpen = true;
- },
- onSearchClose(){
- this.searchOpen = false;
- },
- onSearchNode(tolNode: TolNode){
- this.searchOpen = false;
- this.animationActive = true;
- this.expandToTolNode(tolNode);
- },
- //
- closeModesAndSettings(){
- this.infoModalNode = null;
- this.searchOpen = false;
- this.helpOpen = false;
- this.settingsOpen = false;
- this.animationActive = false;
- this.setLastFocused(null);
- },
- onKeyUp(evt: KeyboardEvent){
- if (evt.key == 'Escape'){
- this.closeModesAndSettings();
- } else if (evt.key == 'F' && evt.ctrlKey){
- if (!this.searchOpen){
- this.onSearchIconClick();
- } else {
- this.$refs.searchModal.focusInput();
- }
- }
- },
- expandToTolNode(tolNode: TolNode){
- if (!this.animationActive){
- return;
- }
- // Check if searched node is shown
- let layoutNode = this.layoutMap.get(tolNode.name);
- if (layoutNode != null && !layoutNode.hidden){
- this.setLastFocused(layoutNode);
- this.animationActive = false;
- return;
- }
- // Get nearest in-layout-tree ancestor
- let ancestor = tolNode;
- while (this.layoutMap.get(ancestor.name) == null){
- ancestor = ancestor.parent!;
- }
- layoutNode = this.layoutMap.get(ancestor.name)!;
- // If hidden, expand ancestor in parent-bar
- if (layoutNode.hidden){
- // Get self/ancestor in parent-bar
- while (!this.sepdParents!.includes(layoutNode)){
- ancestor = ancestor.parent!;
- layoutNode = this.layoutMap.get(ancestor.name)!;
- }
- this.onSepdParentClicked(layoutNode!);
- setTimeout(() => this.expandToTolNode(tolNode), this.componentOptions.transitionDuration);
- return;
- }
- // Attempt tile-expand
- let success = this.onInnerLeafClicked({layoutNode});
- if (success){
- setTimeout(() => this.expandToTolNode(tolNode), this.componentOptions.transitionDuration);
- return;
- }
- // Attempt expand-to-view on ancestor just below activeRoot
- if (ancestor.name == this.activeRoot.tolNode.name){
- console.log('Unable to complete search (not enough room to expand active root)');
- // Happens if screen is very small or node has very many children
- this.animationActive = false;
- return;
- }
- while (true){
- if (ancestor.parent!.name == this.activeRoot.tolNode.name){
- break;
- }
- ancestor = ancestor.parent!;
- }
- layoutNode = this.layoutMap.get(ancestor.name)!;
- this.onInnerHeaderClickHeld(layoutNode);
- setTimeout(() => this.expandToTolNode(tolNode), this.componentOptions.transitionDuration);
- },
- onOverlayClick(){
- this.animationActive = false;
- },
- onPlayIconClick(){
- this.closeModesAndSettings();
- this.animationActive = true;
- this.autoAction();
- },
- autoAction(){
- if (!this.animationActive){
- this.setLastFocused(null);
- return;
- }
- if (this.lastFocused == null){
- // Get random leaf LayoutNode
- let layoutNode = this.activeRoot;
- while (layoutNode.children.length > 0){
- let idx = Math.floor(Math.random() * layoutNode.children.length);
- layoutNode = layoutNode.children[idx];
- }
- this.setLastFocused(layoutNode);
- setTimeout(this.autoAction, this.autoWaitTime);
- } else {
- // Perform action
- let node = this.lastFocused;
- if (node.children.length == 0){
- const Action = {MoveAcross:0, MoveUpward:1, Expand:2};
- let actionWeights = [1, 2, 4];
- // Zero weights for disallowed actions
- if (node == this.activeRoot || node.parent!.children.length == 1){
- actionWeights[Action.MoveAcross] = 0;
- }
- if (node == this.activeRoot){
- actionWeights[Action.MoveUpward] = 0;
- }
- if (node.tolNode.children.length == 0){
- actionWeights[Action.Expand] = 0;
- }
- let action = randWeightedChoice(actionWeights);
- switch (action){
- case Action.MoveAcross:
- let siblings = node.parent!.children.filter(n => n != node);
- this.setLastFocused(siblings[Math.floor(Math.random() * siblings.length)]);
- break;
- case Action.MoveUpward:
- this.setLastFocused(node.parent!);
- break;
- case Action.Expand:
- this.onInnerLeafClicked({layoutNode: node});
- break;
- }
- } else {
- const Action = {MoveAcross:0, MoveDown:1, MoveUp:2, Collapse:3, ExpandToView:4, ExpandParentBar:5};
- let actionWeights = [1, 2, 1, 1, 1, 1];
- // Zero weights for disallowed actions
- if (node == this.activeRoot || node.parent!.children.length == 1){
- actionWeights[Action.MoveAcross] = 0;
- }
- if (node == this.activeRoot){
- actionWeights[Action.MoveUp] = 0;
- }
- if (!node.children.every(n => n.children.length == 0)){
- actionWeights[Action.Collapse] = 0; // Only collapse if all children are leaves
- }
- if (node.parent != this.activeRoot){
- actionWeights[Action.ExpandToView] = 0; // Only expand-to-view if direct child of activeRoot
- }
- if (this.activeRoot.parent == null || node != this.activeRoot){
- actionWeights[Action.ExpandParentBar] = 0; // Only expand parent-bar if able and activeRoot
- }
- let action = randWeightedChoice(actionWeights);
- switch (action){
- case Action.MoveAcross:
- let siblings = node.parent!.children.filter(n => n != node);
- this.setLastFocused(siblings[Math.floor(Math.random() * siblings.length)]);
- break;
- case Action.MoveDown:
- let idx = Math.floor(Math.random() * node.children.length);
- this.setLastFocused(node.children[idx]);
- break;
- case Action.MoveUp:
- this.setLastFocused(node.parent!);
- break;
- case Action.Collapse:
- this.onInnerHeaderClicked({layoutNode: node});
- break;
- case Action.ExpandToView:
- this.onInnerHeaderClickHeld(node);
- break;
- case Action.ExpandParentBar:
- this.onSepdParentClicked(node.parent!);
- break;
- }
- }
- setTimeout(this.autoAction, this.componentOptions.transitionDuration + this.autoWaitTime);
- }
- },
- setLastFocused(node: LayoutNode | null){
- if (this.lastFocused != null){
- this.lastFocused.hasFocus = false;
- }
- this.lastFocused = node;
- if (node != null){
- node.hasFocus = true;
- }
- },
- onHelpIconClick(){
- this.closeModesAndSettings();
- this.helpOpen = true;
- },
- onHelpModalClose(){
- this.helpOpen = false;
- },
- },
- created(){
- window.addEventListener('resize', this.onResize);
- window.addEventListener('keyup', this.onKeyUp);
- tryLayout(this.activeRoot, this.layoutMap, this.tileAreaPos, this.tileAreaDims, this.layoutOptions, true);
- },
- unmounted(){
- window.removeEventListener('resize', this.onResize);
- window.removeEventListener('keyup', this.onKeyUp);
- },
- components: {Tile, ParentBar, TileInfoModal, Settings, SearchModal, HelpModal, },
-});
-</script>
-
-<template>
-<div :style="styles">
- <tile :layoutNode="layoutTree"
- :headerSz="layoutOptions.headerSz" :tileSpacing="layoutOptions.tileSpacing" :options="componentOptions"
- @leaf-clicked="onInnerLeafClicked" @header-clicked="onInnerHeaderClicked"
- @leaf-click-held="onInnerLeafClickHeld" @header-click-held="onInnerHeaderClickHeld"
- @info-icon-clicked="onInnerInfoIconClicked"/>
- <parent-bar v-if="sepdParents != null"
- :pos="[0,0]" :dims="parentBarDims" :nodes="sepdParents" :options="componentOptions"
- @sepd-parent-clicked="onSepdParentClicked" @info-icon-clicked="onInnerInfoIconClicked"/>
- <!-- Settings -->
- <!-- outer div prevents overflow from transitioning to/from off-screen -->
- <div class="fixed left-0 top-0 w-full h-full overflow-hidden invisible">
- <transition name="slide-bottom-right">
- <settings v-if="settingsOpen" :layoutOptions="layoutOptions" :componentOptions="componentOptions"
- @settings-close="onSettingsClose" @layout-option-change="onLayoutOptionChange"/>
- <!-- outer div prevents transition interference with inner rotate -->
- <div v-else class="absolute bottom-0 right-0 w-[100px] h-[100px]">
- <div class="absolute bottom-[-50px] right-[-50px] w-[100px] h-[100px] visible -rotate-45
- bg-black text-white hover:cursor-pointer" @click="onSettingsIconClick">
- <svg class="w-6 h-6 mx-auto mt-2"><use href="#svg-settings"/></svg>
- </div>
- </div>
- </transition>
- </div>
- <!-- Icons -->
- <svg class="absolute top-[6px] right-[54px] w-[18px] h-[18px] text-white/40 hover:text-white hover:cursor-pointer"
- @click="onSearchIconClick">
- <use href="#svg-search"/>
- </svg>
- <svg class="absolute top-[6px] right-[30px] w-[18px] h-[18px] text-white/40 hover:text-white hover:cursor-pointer"
- @click="onPlayIconClick">
- <use href="#svg-play"/>
- </svg>
- <svg class="absolute top-[6px] right-[6px] w-[18px] h-[18px] text-white/40 hover:text-white hover:cursor-pointer"
- @click="onHelpIconClick">
- <use href="#svg-help"/>
- </svg>
- <!-- Modals -->
- <transition name="fade">
- <tile-info-modal v-if="infoModalNode != null" :tolNode="infoModalNode" :options="componentOptions"
- @info-modal-close="onInfoModalClose"/>
- </transition>
- <transition name="fade">
- <search-modal v-if="searchOpen" :layoutTree="layoutTree" :tolMap="tolMap" :options="componentOptions"
- @search-close="onSearchClose" @search-node="onSearchNode" ref="searchModal"/>
- </transition>
- <transition name="fade">
- <help-modal v-if="helpOpen" :options="componentOptions" @help-modal-close="onHelpModalClose"/>
- </transition>
- <!-- Overlay used to prevent interaction and capture clicks -->
- <div :style="{visibility: animationActive ? 'visible' : 'hidden'}"
- class="absolute left-0 top-0 w-full h-full" @click="onOverlayClick"></div>
-</div>
-</template>
-
-<style>
-.animate-expand-shrink {
- animation-name: expand-shrink;
- animation-duration: 300ms;
- animation-iteration-count: 1;
- animation-timing-function: ease-in-out;
-}
-@keyframes expand-shrink {
- from {
- transform: scale(1, 1);
- }
- 50% {
- transform: scale(1.1, 1.1);
- }
- to {
- transform: scale(1, 1);
- }
-}
-.animate-shrink-expand {
- animation-name: shrink-expand;
- animation-duration: 300ms;
- animation-iteration-count: 1;
- animation-timing-function: ease-in-out;
-}
-@keyframes shrink-expand {
- from {
- transform: translate3d(0,0,0) scale(1, 1);
- }
- 50% {
- transform: translate3d(0,0,0) scale(0.9, 0.9);
- }
- to {
- transform: translate3d(0,0,0) scale(1, 1);
- }
-}
-.fade-enter-from, .fade-leave-to {
- opacity: 0;
-}
-.fade-enter-active, .fade-leave-active {
- transition-property: opacity;
- transition-duration: 300ms;
- transition-timing-function: ease-out;
-}
-.slide-bottom-right-enter-from, .slide-bottom-right-leave-to {
- transform: translate(100%, 100%);
- opacity: 0;
-}
-.slide-bottom-right-enter-active, .slide-bottom-right-leave-active {
- transition-property: transform, opacity;
- transition-duration: 300ms;
- transition-timing-function: ease-in-out;
-}
-</style>