diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/Tile.vue | 4 | ||||
| -rw-r--r-- | src/components/TileImg.vue | 2 | ||||
| -rw-r--r-- | src/components/TileTree.vue | 129 | ||||
| -rw-r--r-- | src/lib.ts | 27 |
4 files changed, 118 insertions, 44 deletions
diff --git a/src/components/Tile.vue b/src/components/Tile.vue index 3485f53..82599ff 100644 --- a/src/components/Tile.vue +++ b/src/components/Tile.vue @@ -63,7 +63,7 @@ export default defineComponent({ backgroundColor: this.nonLeafBgColor, borderRadius: this.options.borderRadius + 'px', boxShadow: this.nonLeafHighlight ? this.options.shadowHighlight : - (this.layoutNode.searchResult ? this.options.shadowSearchResult : this.options.shadowNormal), + (this.layoutNode.hasFocus ? this.options.shadowFocused : this.options.shadowNormal), }; if (this.layoutNode.sepSweptArea != null){ let r = this.options.borderRadius + 'px'; @@ -94,7 +94,7 @@ export default defineComponent({ position: 'absolute', backgroundColor: this.nonLeafBgColor, boxShadow: this.nonLeafHighlight ? this.options.shadowHighlight : - (this.layoutNode.searchResult ? this.options.shadowSearchResult : this.options.shadowNormal), + (this.layoutNode.hasFocus ? this.options.shadowFocused : this.options.shadowNormal), transitionDuration: this.options.transitionDuration + 'ms', transitionProperty: 'left, top, width, height', transitionTimingFunction: 'ease-out', diff --git a/src/components/TileImg.vue b/src/components/TileImg.vue index 1eb056a..849be8c 100644 --- a/src/components/TileImg.vue +++ b/src/components/TileImg.vue @@ -36,7 +36,7 @@ export default defineComponent({ // Other borderRadius: this.options.borderRadius + 'px', boxShadow: this.highlight ? this.options.shadowHighlight : - (this.layoutNode.searchResult ? this.options.shadowSearchResult : this.options.shadowNormal), + (this.layoutNode.hasFocus ? this.options.shadowFocused : this.options.shadowNormal), }; }, headerStyles(): Record<string,string> { diff --git a/src/components/TileTree.vue b/src/components/TileTree.vue index 6cdf70d..86e289f 100644 --- a/src/components/TileTree.vue +++ b/src/components/TileTree.vue @@ -5,7 +5,7 @@ import ParentBar from './ParentBar.vue'; import TileInfoModal from './TileInfoModal.vue'; import SearchModal from './SearchModal.vue'; import Settings from './Settings.vue'; -import {TolNode, LayoutNode, initLayoutTree, initLayoutMap, tryLayout} from '../lib'; +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 @@ -53,7 +53,7 @@ const defaultComponentOptions = { borderRadius: 5, //px shadowNormal: '0 0 2px black', shadowHighlight: '0 0 1px 2px greenyellow', - shadowSearchResult: '0 0 1px 2px orange', + shadowFocused: '0 0 1px 2px orange', // For leaf and separated-parent components imgTilePadding: 4, //px imgTileFontSz: 15, //px @@ -93,7 +93,7 @@ export default defineComponent({ infoModalNode: null as TolNode | null, // Hides/unhides info modal, and provides the node to display searchOpen: false, settingsOpen: false, - lastSearchResult: null as LayoutNode | null, + lastFocused: null as LayoutNode | null, animationActive: false, autoWaitTime: 500, //ms (in auto mode, time to wait after an action ends) // Options @@ -256,9 +256,7 @@ export default defineComponent({ }, onSearchNode(tolNode: TolNode){ this.searchOpen = false; - if (this.lastSearchResult != null){ - this.lastSearchResult.searchResult = false; - } + this.setLastFocused(null); this.animationActive = true; this.expandToTolNode(tolNode); }, @@ -284,8 +282,7 @@ export default defineComponent({ // Check if searched node is shown let layoutNode = this.layoutMap.get(tolNode.name); if (layoutNode != null && !layoutNode.hidden){ - layoutNode.searchResult = true; - this.lastSearchResult = layoutNode; + this.setLastFocused(layoutNode); this.animationActive = false; return; } @@ -335,44 +332,106 @@ export default defineComponent({ onPlayIconClick(){ this.closeModalsAndSettings(); this.animationActive = true; + this.setLastFocused(null); this.autoAction(); }, autoAction(){ if (!this.animationActive){ + this.setLastFocused(null); return; } - // Get random LayoutNode - let layoutNode: LayoutNode; - let keyIdx = Math.floor(Math.random() * this.layoutMap.size); - let c = 0; - for (let key of this.layoutMap.keys()){ - if (c == keyIdx){ - layoutNode = this.layoutMap.get(key)!; - } - c++; - } - // Perform action - layoutNode = layoutNode!; // Hint for typescript - if (layoutNode.hidden){ - // Expand self/ancestor in parent-bar - while (!this.sepdParents!.includes(layoutNode)){ - layoutNode = layoutNode.parent!; - } - this.onSepdParentClicked(layoutNode); - } else if (layoutNode.children.length > 0){ - if (Math.random() > 0.5){ - this.onInnerHeaderClicked({layoutNode}); - } else { - this.onInnerHeaderClickHeld(layoutNode); + 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 { - if (Math.random() > 0.5){ - this.onInnerLeafClicked({layoutNode}); + // 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 { - this.onInnerLeafClickHeld(layoutNode); + 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; } - setTimeout(this.autoAction, (this.componentOptions.transitionDuration + this.autoWaitTime)); }, }, created(){ @@ -29,7 +29,7 @@ export class LayoutNode { showHeader: boolean; sepSweptArea: SepSweptArea | null; hidden: boolean; - searchResult: boolean; + hasFocus: boolean; // Used for layout heuristics and info display dCount: number; // Number of descendant leaf nodes depth: number; // Number of ancestor nodes @@ -44,7 +44,7 @@ export class LayoutNode { this.showHeader = false; this.sepSweptArea = null; this.hidden = false; - this.searchResult = false; + this.hasFocus = false; this.dCount = children.length == 0 ? 1 : arraySum(children.map(n => n.dCount)); this.depth = 0; this.empSpc = 0; @@ -755,15 +755,15 @@ let sweepLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse } // Returns [0 ... len] -function range(len: number){ +export function range(len: number){ return [...Array(len).keys()]; } // Returns sum of array values -function arraySum(array: number[]){ +export function arraySum(array: number[]){ return array.reduce((x,y) => x+y); } // Returns array copy with vals clipped to within [min,max], redistributing to compensate (returns null on failure) -function limitVals(arr: number[], min: number, max: number){ +export function limitVals(arr: number[], min: number, max: number){ let vals = [...arr]; let clipped = new Array(vals.length).fill(false); let owedChg = 0; // Stores total change made after clipping values @@ -801,7 +801,7 @@ function limitVals(arr: number[], min: number, max: number){ } // Usable to iterate through possible int arrays with ascending values in the range 0 to maxLen-1, starting with [0] // eg: With maxLen 3, updates [0] to [0,1], then to [0,2], then [0,1,2], then null -function updateAscSeq(seq: number[], maxLen: number){ +export function updateAscSeq(seq: number[], maxLen: number){ // Try increasing last element, then preceding elements, then extending the array let i = seq.length - 1; while (true){ @@ -821,3 +821,18 @@ function updateAscSeq(seq: number[], maxLen: number){ } } } +// Given an array of positive weights, returns a array index chosen with weighted pseudorandomness +export function randWeightedChoice(weights: number[]){ + let thresholds = Array(weights.length); + let sum = 0; + for (let i = 0; i < weights.length; i++){ + sum += weights[i]; + thresholds[i] = sum; + } + let rand = Math.random(); + for (let i = 0; i < weights.length; i++){ + if (rand <= thresholds[i] / sum){ + return i; + } + } +} |
