31 |
32 |
33 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/photos/balanced.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/techsolutioncat/Javascript-Binary-Tree/a8c380f5ed2b85fe110b1874d382cec0eac92271/photos/balanced.png
--------------------------------------------------------------------------------
/photos/blank.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/techsolutioncat/Javascript-Binary-Tree/a8c380f5ed2b85fe110b1874d382cec0eac92271/photos/blank.png
--------------------------------------------------------------------------------
/photos/fill.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/techsolutioncat/Javascript-Binary-Tree/a8c380f5ed2b85fe110b1874d382cec0eac92271/photos/fill.png
--------------------------------------------------------------------------------
/photos/manynodes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/techsolutioncat/Javascript-Binary-Tree/a8c380f5ed2b85fe110b1874d382cec0eac92271/photos/manynodes.png
--------------------------------------------------------------------------------
/photos/search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/techsolutioncat/Javascript-Binary-Tree/a8c380f5ed2b85fe110b1874d382cec0eac92271/photos/search.png
--------------------------------------------------------------------------------
/photos/singlenode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/techsolutioncat/Javascript-Binary-Tree/a8c380f5ed2b85fe110b1874d382cec0eac92271/photos/singlenode.png
--------------------------------------------------------------------------------
/src/Controls.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Controls.js
3 | *
4 | * Thie file defines the Controls class, which provides a friendly button-based
5 | * interface to the Tree class's animation methods
6 | *
7 | * The Tree class relies on the Controls "animationInterval" property to set
8 | * the time in between animation frames
9 | ***/
10 | class Controls {
11 | // Using the static keyword to define readable class constants
12 |
13 | // Constants for the ids of various user interface elements
14 | static CLEARID = "clear-btn";
15 | static QUICKFILLID = "quick-fill-btn";
16 | static SLOWFILLID = "slow-fill-btn";
17 | static ADDID = "add-btn";
18 | static SEARCHID = "search-btn";
19 | static SLIDERID = "speed-slider";
20 | static BINARY = "canvas-placeholder";
21 |
22 | // The number of nodes above which the user will be warned before running
23 | // "Fill" or "Quick Fill"
24 | static NODELIMIT = 500;
25 |
26 | constructor(tree) {
27 | this.tree = tree;
28 | this.tree.bindControls(this); // Provide the tree a reference to this class
29 |
30 | this.animationInterval = null; // Property Tree class relies on for animation
31 |
32 | // Store all of the user interface elements based on the IDs
33 | this.clearBtn = document.getElementById(Controls.CLEARID);
34 | // this.quickFillBtn = document.getElementById(Controls.QUICKFILLID);
35 | // this.slowFillBtn = document.getElementById(Controls.SLOWFILLID);
36 | //this.addBtn = document.getElementById(Controls.ADDID);
37 | this.searchBtn = document.getElementById(Controls.SEARCHID);
38 | this.speedSlider = document.getElementById(Controls.SLIDERID);
39 | this.node = document.getElementById(Controls.BINARY);
40 |
41 | // Set the animation interval based on the slider's value
42 | this.setAnimationSpeed();
43 |
44 | // Append event listeners to run each animation
45 | this.clearBtn.addEventListener('click',
46 | () => this.triggerAnimation(this.clear));
47 | // this.quickFillBtn.addEventListener('click',
48 | // () => this.triggerAnimation(this.quickFill));
49 | // this.slowFillBtn.addEventListener('click',
50 | // () => this.triggerAnimation(this.slowFill));
51 | document.addEventListener('keyup', event => {
52 | if (event.code === 'Space') {//vitaly
53 | //Controls.triggerAnimation(Controls.add);
54 | var value = Math.floor(random(-100, 100));
55 |
56 | if(value !== null && this.tree.search(value)) {
57 | alert(value + ' is already in the tree');
58 | } else if(value !== null){
59 | this.tree.addValueVisual(value);
60 | }
61 | }
62 | })
63 | //this.addBtn.addEventListener('click',
64 | //() => this.triggerAnimation(this.add));
65 |
66 | this.node.addEventListener('click', function(e) {
67 | console.log(e);
68 | });
69 |
70 | this.searchBtn.addEventListener('click',
71 | () => this.triggerAnimation(this.search));
72 |
73 | // Append an event listener to change the animation interval
74 | this.speedSlider.addEventListener('input', this.setAnimationSpeed.bind(this));
75 | }
76 |
77 | // Completly resets the tree, removing all nodes, stopping all animations
78 | clear() {
79 | this.tree.clear();
80 | this.tree.stopAnimation(() => {})
81 | this.tree.draw();
82 | }
83 |
84 | // Called by event listeners to run a certain animation if one is not running
85 | triggerAnimation(animation) {
86 | if(this.tree.running) {
87 | alert('Please wait for the current animation to finish');
88 | } else {
89 | // Bind the animation here so it doesn't have to be bound as an argument
90 | animation.bind(this)();
91 | }
92 | }
93 |
94 | // Prompts the user with a given piece of text
95 | // Returns: null => if no valid number was entered
96 | // postive integer => if a valid positive integer is provided
97 | getNumber(text) {
98 | var value = prompt(text);
99 |
100 | if(value === null) {
101 | return null;
102 | } else if(isNaN(parseInt(value)) || value === "" || parseInt(value) < 0) {
103 | alert('Please enter a positive integer');
104 | return null;
105 | } else {
106 | return parseInt(value);
107 | }
108 | }
109 |
110 | // Method for the Quick Fill animation
111 | quickFill() {
112 | var count = this.getNumber("Number of nodes: ");
113 |
114 | if(count !== null && (count < Controls.NODELIMIT ||
115 | confirm(count + ' nodes may reduce performance. Continue anyways?'))) {
116 | this.tree.fill(count);
117 | }
118 | }
119 |
120 | // Method for the Fill animation
121 | slowFill() {
122 | var count = this.getNumber("Number of nodes: ");
123 |
124 | if(count !== null && (count < Controls.NODELIMIT ||
125 | confirm(count + ' nodes may reduce performance. Continue anyways?'))) {
126 | this.tree.fillVisual(count);
127 | }
128 | }
129 |
130 | // Method for the Add animation
131 | add() {
132 | //var value = this.getNumber("Value to add: ");
133 |
134 | var value = Math.floor(random(-100, 100));
135 |
136 | if(value !== null && this.tree.search(value)) {
137 | alert(value + ' is already in the tree');
138 | } else if(value !== null){
139 | this.tree.addValueVisual(value);
140 | }
141 | }
142 |
143 | // Method for the search animation
144 | search() {
145 | var value = this.getNumber("Value to search for: ");
146 |
147 | if(value !== null) {
148 | this.tree.searchVisual(value)
149 | }
150 | }
151 |
152 | // Inverts and exponentiates the linear output of the slider to set the interval
153 | setAnimationSpeed() {
154 | this.animationInterval= 1000/Math.pow(10, this.speedSlider.value);
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/src/Explorer.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Explorer.js
3 | *
4 | * This file defines the Explorer class, which transforms the graphics buffer
5 | * that the Tree class uses to draw itself to allow for users to pan and zoom
6 | * as they view the binary tree.
7 | *
8 | * The Explorer uses the translate and scale functions without resetting the
9 | * transformation at the end of the frame. This allows pieces of the picture to
10 | * be updated without redrawing the entire picture, which can improve
11 | * performance. The Explorer currently must redraw the picture whenever the user
12 | * pans or zooms. Ideally, the explorer could simply change the size and
13 | * position of the graphics, but the way the canvas works, the picture would
14 | * be blurry when zoomed in. It may be possible to achieve this behavoir with
15 | * svg graphics instead, but that's a project for the future.
16 | *
17 | * The Explorer class relies on a graphics buffer to transform, a draw function
18 | * to redraw the picture in the graphics buffer, and a DOM object for the canvas
19 | * to register event listeners for panning and zooming
20 | ***/
21 |
22 | class Explorer {
23 | static ZOOMFACTOR = 1.1; // The factor by which zooming occurs
24 |
25 | constructor(canvas, graphicsBuffer, drawFunction, zoomFactor = Explorer.ZOOMFACTOR) {
26 | // Canvas that clicks and mouse wheel events are detected on
27 | this.canvas = canvas;
28 |
29 | // The buffer that should be modified for panning and zooming
30 | this.graphicsBuffer = graphicsBuffer;
31 |
32 | // Function that will redraw the picture in the graphics buffer
33 | this.drawFunction = drawFunction;
34 |
35 | // The total transformations that have been applied to the graphics buffer
36 | this.offsetX = 0;
37 | this.offsetY = 0;
38 | this.zoom = 1;
39 |
40 | // The factor by which zooming occurs
41 | this.zoomFactor = zoomFactor;
42 |
43 | // Reset the graphics buffer (so it can be poppe to reset it later)
44 | this.graphicsBuffer.push();
45 |
46 | // Register event listeners for zooming and panning
47 | canvas.addEventListener("wheel", this.wheel.bind(this));
48 | canvas.addEventListener("mousemove", this.mouseMove.bind(this));
49 | }
50 |
51 | // Returns the graphics buffer to its "Home" view. Not used right now
52 | reset() {
53 | this.offsetX = 0;
54 | this.offsetY = 0;
55 | this.zoom = 1;
56 |
57 | this.graphicsBuffer.pop();
58 | this.graphicsBuffer.push();
59 |
60 | this.drawFunction();
61 | }
62 |
63 | // Event listener for panning. Detects when the mouse is pressed to detect
64 | // dragging, and uses the movementX/Y of the event to determine distance to pan
65 | mouseMove(event) {
66 | if(mouseIsPressed) {
67 | var deltaX = (1/this.zoom) * event.movementX;
68 | var deltaY = (1/this.zoom) * event.movementY;
69 |
70 | this.graphicsBuffer.translate(deltaX, deltaY);
71 |
72 | this.offsetX += deltaX;
73 | this.offsetY += deltaY;
74 |
75 | this.drawFunction();
76 | }
77 | }
78 |
79 | // Even listener for zooming. Uses the event's deltaY to determine whether
80 | // to zoom in or zoom out
81 | wheel(event) {
82 | var deltaX = (CANVASWIDTH/2) - this.offsetX;
83 | var deltaY = (CANVASHEIGHT/2) - this.offsetY;
84 | var zoomFactor;
85 |
86 | if(event.deltaY < 0) {
87 | zoomFactor = this.zoomFactor
88 | } else if(event.deltaY > 0){
89 | zoomFactor = 1/this.zoomFactor
90 | } else {
91 | // In case of sideways scroll on trackpad
92 | zoomFactor = 1;
93 | }
94 |
95 | this.graphicsBuffer.translate(deltaX, deltaY);
96 | this.graphicsBuffer.scale(zoomFactor);
97 | this.graphicsBuffer.translate(-deltaX, -deltaY);
98 |
99 | this.zoom *= zoomFactor;
100 |
101 | this.drawFunction();
102 |
103 | return false; // Prevent default scrolling behavoir (maybe?)
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/Node.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Node.js
3 | *
4 | * This file defines the Node class. Nodes serve as the building blocks of the
5 | * binary tree. The Node has basic data structure properties that would be
6 | * required of a non-visual node object. This includes storing its value, and
7 | * storing references to its left and right children. A node is said to be
8 | * filled if it contains a value other than null. When a node is filled, it will
9 | * create two empty (not filled) nodes as it children. This allows recursive
10 | * calls to simply check if the node being processed is filled, rather than
11 | * checking that each left and right child of a node has been defined.
12 | *
13 | * Nodes also store information that is necessary to visualize them. This
14 | * includes all the properties of the appearance of the circle which represents
15 | * the node (e.g. size, color, outline), as well as properties that are
16 | * necessary to visualize the edge connecting the node to its parent.
17 | ***/
18 |
19 | class Node {
20 | // Note: this section uses the static keyword to define static properties.
21 | // Although perhaps not the intended function, it makes defining static
22 | // properties much more readable than adding them at the end
23 |
24 | // Constants that control the default appearance of the nodes
25 | static SIZE = 20; // Diameter of the nodes
26 | static COLOR = color(255, 255, 255); // Fill color of the nodes
27 | static STROKE = color(0, 0, 0, 0); // Outline color of the nodes
28 | static TEXTSIZE = 10; // Text size of the node values
29 | static TEXTCOLOR = color(0, 0, 0); // Text color of the node values
30 | static EDGECOLOR = color(0, 0, 0); // Color of this node's upper edge
31 | static EDGETHICKNESS = 2; // Thickness this node's upper edge
32 |
33 | // Color-related constants for visualization purposes
34 | static VISITED = color(0, 0, 255); // Color when this node has been visited
35 | static SUCCESS = color(0, 255, 0); // Color when this node was added/the
36 | // value inside was being searched for
37 | static FAILURE = color(255, 0, 0); // Color when the value being searched
38 | // for is not found in this node
39 |
40 | // Constants controlling the positions of the nodes relative to one another
41 | static HORIZONTALSPACING = 15; // Horizontal distance between two nodes
42 | static VERTICALSPACING = 50; // Vertical distance between tow nodes
43 |
44 | constructor(graphicsBuffer, parent = null, size = Node.SIZE,
45 | color = Node.COLOR, stroke = Node.STROKE,
46 | textSize = Node.TEXTSIZE, textColor = Node.TEXTCOLOR,
47 | edgeColor = Node.EDGECOLOR,
48 | edgeThickness = Node.EDGETHICKNESS) {
49 |
50 | this.value = null; // The value this node is holding
51 |
52 | // Reference to left/right children on this node
53 | this.leftNode = null;
54 | this.rightNode = null;
55 |
56 | // The off-screen buffer this node should draw itself to
57 | this.graphicsBuffer = graphicsBuffer;
58 |
59 | // A reference to the parent for drawing purposes
60 | this.parent = parent;
61 |
62 | // x and y coordinates to draw the node at
63 | this.x = 0;
64 | this.y = 0;
65 |
66 | // The horizontal space between this node and its left/right children
67 | this.rightSpacing = 0;
68 | this.leftSpacing = 0;
69 |
70 | // The total horizontal space all nodes below this node use in either direction
71 | this.cumulativeRightSpacing = 0;
72 | this.cumulativeLeftSpacing = 0;
73 |
74 | // Properties controlling the appearance of the node
75 | this.size = size;
76 | this.color = color;
77 | this.stroke = stroke;
78 | this.textSize = textSize;
79 | this.textColor = textColor;
80 |
81 | // Properties controlling the appearance of the upper edge of this node
82 | this.edgeColor = edgeColor;
83 | this.edgeThickness = edgeThickness;
84 | }
85 |
86 | // Definition of a "filled" node that should be processed recursively
87 | isFilled() {
88 | return this.value !== null;
89 | }
90 |
91 | // Checks if a node has a parent, or is the root of a tree
92 | hasParent() {
93 | return this.parent !== null;
94 | }
95 |
96 | /***
97 | * Adds the specified value to the structure at or below this node
98 | *
99 | * Returns: The highest-level node whose coordinates (and children's
100 | * coordinates) must be adjusted to account for the addition of the new node.
101 | *
102 | * Returning the highest-level that needs to be adjusted increases
103 | * performance, because the coordinates of only a subset of the tree must
104 | * be recalculated
105 | ***/
106 | addValue(value) {
107 | if (!this.isFilled()) {
108 | // If the node hasn't been filled yet, fill this node with the value
109 | // This node needs to have its coordinates set, to return this
110 |
111 | this.value = value;
112 | this.leftNode = new Node(this.graphicsBuffer, this);
113 | this.rightNode = new Node(this.graphicsBuffer, this);
114 |
115 | return this;
116 |
117 | } else if (value < this.value) {
118 | // The value is less than this node's value, so it belongs to the left
119 |
120 | var initialLeftSpacing = this.leftNode.cumulativeRightSpacing
121 | + Node.HORIZONTALSPACING;
122 |
123 | // Add this value to the left half of the tree
124 | var shiftedNode = this.leftNode.addValue(value);
125 |
126 | // To prevent overlapping nodes, the left child should be offset
127 | // slightly farther to the left than all the space taken up to the
128 | // right of the left node
129 | this.leftSpacing = this.leftNode.cumulativeRightSpacing
130 | + Node.HORIZONTALSPACING;
131 |
132 | // Update total spacing taken up to the left of this node
133 | this.cumulativeLeftSpacing = this.leftNode.cumulativeLeftSpacing
134 | + this.leftSpacing;
135 |
136 | // If this node's left spacing changed, then the coordinates of its
137 | // left child must be updated to account for this change, so return
138 | // the left child
139 | if(this.leftSpacing !== initialLeftSpacing) {
140 | return this.leftNode;
141 | }
142 |
143 | // If the left spacing didn't change, return the lower node that
144 | // needs to be adjusted
145 | return shiftedNode;
146 |
147 | } else if(value > this.value){
148 | // The value is greater than this node's value, so it belongs to the left
149 |
150 | // The code below parallels the code above, but handles adding nodes
151 | // to the right half of this node
152 |
153 | var rightSpacing = this.rightNode.cumulativeLeftSpacing
154 | + Node.HORIZONTALSPACING;
155 |
156 | var shiftedNode = this.rightNode.addValue(value);
157 |
158 | this.rightSpacing = this.rightNode.cumulativeLeftSpacing
159 | + Node.HORIZONTALSPACING;
160 |
161 | this.cumulativeRightSpacing = this.rightNode.cumulativeRightSpacing
162 | + this.rightSpacing;
163 |
164 | if(this.rightSpacing !== rightSpacing) {
165 | return this.rightNode;
166 | }
167 |
168 | return shiftedNode;
169 | }
170 | }
171 |
172 | // Recursively sets the coordinates of this node and all nodes below it.
173 | // If no coordinates are supplied, the coordinates are based on the parent
174 | // node's location and spacing. If coordinates are supplied, the coordinates
175 | // are se to the specified values.
176 | // This function is called by the Tree class after a value is inserted
177 | // to position the nodes in the tree correctly
178 | setCoordinates(x, y) {
179 | if(this.isFilled()) {
180 | if(typeof x === "undefined" && typeof y === "undefined") {
181 | // No coordinates were passed into the function
182 | if(this.value < this.parent.value) {
183 | // Left node
184 | this.x = this.parent.x - this.parent.leftSpacing;
185 | } else {
186 | // Right node
187 | this.x = this.parent.x + this.parent.rightSpacing;
188 | }
189 |
190 | this.y = this.parent.y + Node.VERTICALSPACING;
191 |
192 | } else {
193 | // Coordinates were passed into the function
194 | this.x = x;
195 | this.y = y;
196 | }
197 |
198 | this.leftNode.setCoordinates();
199 | this.rightNode.setCoordinates();
200 | }
201 | }
202 |
203 | /***
204 | * Recursively searches the tree for the specified value
205 | *
206 | * Returns: Boolean; if value was found in the tree (true) or not (false)
207 | ***/
208 | search(value) {
209 | if (!this.isFilled()) {
210 | return false;
211 |
212 | } else if (this.value === value) {
213 | return true;
214 |
215 | } else if (value < this.value) {
216 | return this.leftNode.search(value);
217 |
218 | } else if (value > this.value) {
219 | return this.rightNode.search(value);
220 | }
221 | }
222 |
223 | // Draws this node's upper level edge, if the node has a parent
224 | drawEdge() {
225 | if (this.hasParent()) {
226 | this.graphicsBuffer.stroke(this.edgeColor);
227 | this.graphicsBuffer.strokeWeight(this.edgeThickness);
228 | this.graphicsBuffer.line(this.x, this.y, this.parent.x, this.parent.y);
229 | }
230 | }
231 |
232 | // Draws this node's circular face
233 | drawNode() {
234 | this.graphicsBuffer.fill(this.color);
235 | this.graphicsBuffer.stroke(this.stroke);
236 | this.graphicsBuffer.ellipse(this.x, this.y, this.size, this.size);
237 |
238 | this.graphicsBuffer.noStroke();
239 | this.graphicsBuffer.fill(this.textColor);
240 | this.graphicsBuffer.textAlign(CENTER, CENTER);
241 | this.graphicsBuffer.textSize(this.textSize);
242 | this.graphicsBuffer.text(this.value, this.x, this.y + 1);
243 | }
244 |
245 | // Recursively draws this node and all nodes below it
246 | // Note: the parent of the node originally calling this function must be
247 | // redrawn, because the edge will cover the parent node. Use redraw()
248 | // if you want to redraw a single node
249 | draw() {
250 | if(this.isFilled()) {
251 | this.leftNode.draw();
252 | this.rightNode.draw();
253 |
254 | this.drawEdge();
255 | this.drawNode();
256 | }
257 | }
258 |
259 | // Redraws a singular node on the tree with no side-effects
260 | redraw() {
261 | if(this.isFilled()) {
262 | this.drawEdge();
263 |
264 | this.drawNode();
265 |
266 | if(this.hasParent()) {
267 | this.parent.drawNode();
268 | }
269 | }
270 | }
271 |
272 | // Recursively sets the color and edge color of this node and all nodes
273 | // below it to the specified color
274 | recursivePaint(color) {
275 | if(this.isFilled()) {
276 | this.color = color;
277 | this.edgeColor = color;
278 |
279 | this.leftNode.recursivePaint(color);
280 | this.rightNode.recursivePaint(color);
281 | }
282 | }
283 |
284 | // Sets the color and edge color of this node, and redraws the node
285 | paint(color) {
286 | this.color = color;
287 | this.edgeColor = color;
288 |
289 | this.redraw();
290 | }
291 |
292 | // Recursively set the appearnace of this node and all nodes below it to
293 | // defaults for the class
294 | resetVisuals() {
295 | if(this.isFilled()) {
296 | this.size = Node.SIZE;
297 | this.color = Node.COLOR;
298 | this.stroke = Node.STROKE;
299 | this.textSize = Node.TEXTSIZE;
300 | this.textColor = Node.TEXTCOLOR;
301 |
302 | this.edgeColor = Node.EDGECOLOR;
303 | this.edgeThickness = Node.EDGETHICKNESS;
304 |
305 | this.leftNode.resetVisuals();
306 | this.rightNode.resetVisuals();
307 | }
308 | }
309 | }
310 |
--------------------------------------------------------------------------------
/src/Tree.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Tree.js
3 | *
4 | * This file defines the Tree class. The Tree class serves as a wrapper for a
5 | * root node, wrapping methods such as addValue or search, but also provides
6 | * higher-level functionality, such as setting the coordinates of the entire
7 | * tree, or drawing the entire tree.
8 | *
9 | * The tree class is also responsible for animating itself, and relies only
10 | * on an instance of the Controls class to provide the interval between frames
11 | * of animation
12 | ***/
13 |
14 | class Tree {
15 | constructor(x, y, backgroundColor) {
16 | // The buffer that this tree, and all the nodes in the tree draw to
17 | this.graphicsBuffer = createGraphics(CANVASWIDTH, CANVASHEIGHT);
18 |
19 | // The root node; the upper level node of a binary tree
20 | this.root = new Node(this.graphicsBuffer);
21 |
22 | // A reference to a Controls instance
23 | this.controls = null;
24 |
25 | // The x and y coordinate that the root node is drawn at
26 | this.x = x;
27 | this.y = y;
28 |
29 | // The background color of the tree visualization
30 | this.backgroundColor = backgroundColor;
31 |
32 | // Various properties for tracking animations
33 | this.running = false; // Whether or not an animation is running
34 | this.timeout = null; // The current timeout, so animations can be cancelled
35 | this.node = null; // The current node being animated
36 |
37 | // Draw the tree upon creation (to show the background)
38 | this.draw();
39 | }
40 |
41 | // Sets this instance's reference to a Controls instance so that an
42 | // animation interval can be set
43 | bindControls(controls) {
44 | this.controls = controls;
45 | }
46 |
47 | // Resets the root node to an empty node, effectively clearing all nodes
48 | clear() {
49 | this.root = new Node(this.graphicsBuffer);
50 | }
51 |
52 | // Returns: a random number in the range [0, max) not yet in the tree
53 | uniqueRandom(max) {
54 | while(true) {
55 | var value = Math.floor(random(0, max));
56 |
57 | if(!this.search(value)) {
58 | return value;
59 | }
60 | }
61 | }
62 |
63 | // Quickly fills the tree with a certain number of nodes
64 | fill(count) {
65 | this.clear();
66 |
67 | for(var i = 0; i < count; i++) {
68 | var value = this.uniqueRandom(count);
69 |
70 | this.addValue(value);
71 | }
72 |
73 | this.draw();
74 | }
75 |
76 | // Wraps the Node class's addValue method, and sets the coordinate of the
77 | // subset of the tree that needs to be adjusted after the value was added
78 | addValue(value) {
79 | var shiftedNode = this.root.addValue(value);
80 |
81 | this.setCoordinates(shiftedNode);
82 | }
83 |
84 | // Wraps the Node class's search method directly
85 | search(value) {
86 | return this.root.search(value);
87 | }
88 |
89 | // Wraps the Node class's setCoordinates method. Sets the root's position
90 | // to the x and y coordinates of the tree, or allows nodes to determine
91 | // their own position based off their parents (by passing no arguments)
92 | setCoordinates(node) {
93 | if(node === this.root) {
94 | node.setCoordinates(this.x, this.y);
95 | } else {
96 | node.setCoordinates();
97 | }
98 | }
99 |
100 | // Draws the entire visulizatino, including the background, and each node
101 | draw() {
102 | this.graphicsBuffer.background(this.backgroundColor);
103 |
104 | if(this.root.isFilled()) {
105 | this.root.draw();
106 | }
107 |
108 | this.updateDrawing();
109 | }
110 |
111 | // Displays the tree without redrawing every node in the tree
112 | // This function is used when the color of singular nodes are updated
113 | updateDrawing() {
114 | image(this.graphicsBuffer, 0, 0);
115 | }
116 |
117 | // Wraps the Node class's resetVisuals method, and draws the result
118 | resetVisuals() {
119 | this.root.resetVisuals();
120 |
121 | this.draw();
122 | }
123 |
124 | // The first function called before an animation. Checks that no animation
125 | // is currently running, and then initializes various properties to track
126 | // the progress of the animation
127 | // The frame argument is a function representing one frame of the animation
128 | startAnimation(frame, ...args) {
129 | if(this.running) {
130 | throw Error('Animation is currently running');
131 | } else {
132 | this.running = true;
133 | this.node = this.root;
134 |
135 | this.resetVisuals();
136 |
137 | // Bind the frame method here so it doesn't have to be bound
138 | // when it is passed as an argument
139 | this.continueAnimation(frame.bind(this), ...args)
140 | }
141 | }
142 |
143 | // Schedules the next frame of the animation
144 | continueAnimation(frame, ...args) {
145 | // Bind the frame function inside the method, so it doesnt have to be
146 | // bound as an arguemtn
147 | this.timeout = setTimeout(() => frame.bind(this)(...args),
148 | this.controls.animationInterval);
149 | }
150 |
151 | // The last function called to end an animation. Resets various properties
152 | // tracking the progression of the animation so that a new animation can be
153 | // run, and runs a specified function with specified arguments when the
154 | // animation is complete
155 | stopAnimation(complete = () => {}, ...callbackArgs) {
156 | this.running = false;
157 | this.node = null;
158 |
159 | clearTimeout(this.timeout);
160 |
161 | setTimeout(() => complete(...callbackArgs), this.controls.animationInterval);
162 | }
163 |
164 | // Call for starting an addValue animation
165 | // value is the value to add
166 | // complete is a callback called when the animation finishes
167 | addValueVisual(value, complete = () => {}, ...callbackArgs) {
168 | this.startAnimation(this.addValueFrame, value, complete, ...callbackArgs);
169 | }
170 |
171 | // Single frame for the addValue animation
172 | // Arguments match addValueVisual
173 | addValueFrame(value, complete, ...callbackArgs) {
174 | if(!this.node.isFilled()) {
175 | this.addValue(value); // Add the value to the data structure
176 |
177 | this.node.paint(Node.SUCCESS); // Mark this node as inserted
178 |
179 | this.draw(); // Show the tree with the new value
180 |
181 | this.stopAnimation(complete, ...callbackArgs);
182 |
183 | } else {
184 | this.node.paint(Node.VISITED); // Mark this node as visited
185 |
186 | this.updateDrawing(); // Display the new color
187 |
188 | // Determine the node for the next frame
189 | if(value < this.node.value) {
190 | this.node = this.node.leftNode;
191 |
192 | } else if(value > this.node.value) {
193 | this.node = this.node.rightNode;
194 | }
195 |
196 | // Schedule the next frame, passing in all arguments for the next call
197 | this.continueAnimation(this.addValueFrame, value, complete, ...callbackArgs)
198 | }
199 | }
200 |
201 | // Call for starting the search animation
202 | // value is the value to search for
203 | // complete is a callback called when the animation finishes
204 | searchVisual(value, complete = () => {}, ...callbackArgs) {
205 | this.startAnimation(this.searchFrame, value, complete, ...callbackArgs);
206 | console.log('searching visually')
207 | }
208 |
209 | // Single frame for the search animatino
210 | // Arugment match serchVisual
211 | searchFrame(value, complete, ...callbackArgs) {
212 | if(this.node.color !== Node.VISITED) {
213 | // Mark the root node as visited first, then continue the search
214 | this.root.paint(Node.VISITED);
215 |
216 | this.updateDrawing();
217 |
218 | this.continueAnimation(this.searchFrame, value, complete, ...callbackArgs);
219 |
220 | } else if(!this.node.isFilled()) {
221 | // The value isn't in the tree, stop the animation
222 |
223 | this.stopAnimation(complete, ...callbackArgs);
224 |
225 | } else if(this.node.value === value) {
226 | // The value is in this node
227 |
228 | this.node.paint(Node.SUCCESS); // Mark the node as found
229 |
230 | this.updateDrawing(); // Display the new color
231 |
232 | this.stopAnimation(complete, ...callbackArgs);
233 |
234 | } else {
235 | // The value may be in another node
236 |
237 | var nextHalf; // The half of the tree being searched next
238 | var cutHalf; // The hal of the tree that can be cut from search
239 |
240 | // Set the two variables correctly
241 | if(value < this.node.value) {
242 | nextHalf = this.node.leftNode;
243 | cutHalf = this.node.rightNode;
244 |
245 | } else if(value > this.node.value) {
246 | nextHalf = this.node.rightNode;
247 | cutHalf = this.node.leftNode;
248 | }
249 |
250 | // Set the node for the next frame
251 | this.node = nextHalf;
252 |
253 | // Mark the half of the tree the node is not in, draw it
254 | cutHalf.recursivePaint(Node.FAILURE);
255 | cutHalf.draw();
256 |
257 | // Mark the next node as visited
258 | nextHalf.paint(Node.VISITED);
259 |
260 | // Display all the changes
261 | this.updateDrawing();
262 |
263 | this.continueAnimation(this.searchFrame, value, complete, ...callbackArgs);
264 | }
265 | }
266 |
267 | // Call for starting the fill animation
268 | // count is the number of nodes to add
269 | // complete is a callback called when the animation finishes
270 | fillVisual(count, complete = () => {}) {
271 | this.clear();
272 |
273 | this.startAnimation(this.fillFrame, count, 0, complete);
274 | }
275 |
276 | // Single frame of the fill animation
277 | // count is the number of nodes to add
278 | // filled is the number of nodes added so far
279 | // complete is a callback called when the animation finishes
280 | fillFrame(count, filled, complete) {
281 | if(filled === count) {
282 | // Stop the animation if the correct number of nodes were inserted
283 | this.stopAnimation(complete);
284 | } else {
285 | // Temporarily stop the fill animation to start the addValue animation
286 | this.stopAnimation();
287 |
288 | var value = this.uniqueRandom(count);
289 |
290 | // Start the addValue animation, calling this frame again when the
291 | // animation is complete, and incrementing the number of nodes
292 | // filled so far
293 | this.startAnimation(this.addValueFrame, value,
294 | this.fillFrame.bind(this), count, filled + 1, complete);
295 | }
296 | }
297 | }
298 |
--------------------------------------------------------------------------------
/src/sketch.js:
--------------------------------------------------------------------------------
1 | /***
2 | * sketch.js
3 | *
4 | * The main file for the binary tree visualization, an easy-to-use web-based
5 | * binary tree visualization
6 | *
7 | * Created by Colin Siles on July 6th, 2019
8 | * Last updated July 14th, 2019
9 | *
10 | * Inspired by Coding Train's Binary Tree Coding Challenge:
11 | * https://thecodingtrain.com/CodingChallenges/065.2-binary-tree-viz.html
12 | ***/
13 |
14 | // Controls the size of the visualization. Defaults to full-screen
15 | var CANVASWIDTH = window.innerWidth;
16 | var CANVASHEIGHT = window.innerHeight;
17 |
18 | // Constants for controlling the position of the binary tree
19 | const TREEX = CANVASWIDTH / 2; // The x-coordinate of the root node
20 | const TREEY = 100; // The y-coordinate of the root node
21 | const BACKGROUNDCOLOR = color(50); // Background color of the visualization
22 |
23 | function setup() {
24 | // Create the canvas and place it in the provided placeholder
25 | var canvas = createCanvas(CANVASWIDTH, CANVASHEIGHT);
26 | canvas.parent('canvas-placeholder');
27 |
28 | // Create other necessary objects for the visualization
29 | var tree = new Tree(TREEX, TREEY, BACKGROUNDCOLOR);
30 | var explorer = new Explorer(canvas.canvas, tree.graphicsBuffer, tree.draw.bind(tree));
31 | var controls = new Controls(tree)
32 | }
33 |
--------------------------------------------------------------------------------
/src/style.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | margin: 0;
3 | padding: 0;
4 | }
5 |
6 | canvas {
7 | display: block;
8 | }
9 |
10 | button {
11 | min-width: 64px;
12 | background-color: #4CAF50;
13 | border: none;
14 | color: #FFFFFF;
15 | padding: 15px 32px;
16 | text-align: center;
17 | -webkit-transition-duration: 0.4s;
18 | transition-duration: 0.4s;
19 | margin: 16px 15px !important;
20 | text-decoration: none;
21 | font-size: 16px;
22 | cursor: pointer;
23 | }
24 |
25 | #controls {
26 | position: absolute;
27 | padding: 16px;
28 | top: 0px;
29 | display: flex;
30 | }
31 |
32 | #speed-label, #help-label {
33 | line-height: 200%;
34 | padding: 0px 8px;
35 | color: white;
36 | margin-top: 23px;
37 | margin-right: 10px;
38 | }
39 |
--------------------------------------------------------------------------------