aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/components/Tile.vue4
-rw-r--r--src/components/TileImg.vue2
-rw-r--r--src/components/TileTree.vue129
-rw-r--r--src/lib.ts27
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(){
diff --git a/src/lib.ts b/src/lib.ts
index 957b508..fd8657d 100644
--- a/src/lib.ts
+++ b/src/lib.ts
@@ -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;
+ }
+ }
+}