diff --git a/dist/index.html b/dist/index.html
index 02f3f74..a2ac09a 100644
--- a/dist/index.html
+++ b/dist/index.html
@@ -1 +1,12 @@
-
Output Management
\ No newline at end of file
+
+
+
+
+ Output Management
+
+
+
+
+
+
+
diff --git a/src/Animation.js b/src/Animation.js
new file mode 100644
index 0000000..8a1a13b
--- /dev/null
+++ b/src/Animation.js
@@ -0,0 +1,44 @@
+import { select } from "d3";
+import { Triangle } from "./Triangle";
+import { animationConfig } from "../utils/constants";
+
+const { width, height, count, delay, drawRate } = animationConfig;
+const [x, y] = [width / 2, height / 2];
+
+export class Animation {
+ intervalId;
+ intervalCount = 0;
+
+ constructor() {
+ this.triangles = [];
+
+ this.canvas = select("body")
+ .append("canvas")
+ .attr("id", "animation")
+ .attr("width", width)
+ .attr("height", height);
+
+ this.ctx = this.canvas.node().getContext("2d");
+
+ [...Array(count)].forEach((d, i) =>
+ setTimeout(() => this.triangles.push(new Triangle(this.ctx)), i * delay)
+ );
+ }
+
+ draw() {
+ this.intervalCount++;
+ this.ctx.fillStyle = "rgba(210, 210, 210, 0.05)";
+ this.ctx.fillRect(0, 0, width, height); // clear canvas
+ this.triangles.map((t) => t.update([x, y]));
+ }
+
+ startAnimation() {
+ // note: doing `()=> this.function` maintains the proper scoping for the method
+ // so that it has access to the object as `this`
+ this.intervalId = setInterval(() => this.draw(), drawRate);
+ }
+
+ stopAnimation() {
+ clearInterval(this.intervalId);
+ }
+}
diff --git a/src/Title.js b/src/Title.js
index df41c71..f4afb06 100644
--- a/src/Title.js
+++ b/src/Title.js
@@ -1,7 +1,19 @@
import { select } from "d3";
-const Title = () => {
- console.log("testing");
- select("#container").append("h1").text("Interactive Data Visualization");
-};
-export default Title;
+export class Title {
+ constructor() {
+ this.el = select("body")
+ .append("div")
+ .attr("id", "page-title")
+ .text("Interactive Data Visualization")
+ .classed("visible", false);
+ }
+
+ makeVisible() {
+ this.el.classed("visible", true);
+ }
+
+ makeHidden() {
+ this.el.classed("visible", false);
+ }
+}
diff --git a/src/Triangle.js b/src/Triangle.js
new file mode 100644
index 0000000..13c71d4
--- /dev/null
+++ b/src/Triangle.js
@@ -0,0 +1,72 @@
+import { getRandom, animationConfig } from "../utils/constants";
+
+const { width, height, count, delay, drawRate } = animationConfig;
+
+export class Triangle {
+ constructor(ctx) {
+ this.ctx = ctx;
+ this.resetPoints();
+ this.opacity = 1;
+ this.draw();
+ this.count = 0;
+ }
+
+ draw() {
+ const [cx, cy] = this.calcCentroid();
+ this.ctx.save();
+ this.ctx.translate(width / 2 - cx, height / 2 - cy);
+ this.ctx.beginPath();
+ this.ctx.strokeStyle = "steelblue";
+ this.ctx.globalAlpha = this.opacity;
+ this.ctx.moveTo(this.p1.x, this.p1.y);
+ this.ctx.lineTo(this.p2.x, this.p2.y);
+ this.ctx.lineTo(this.p3.x, this.p3.y);
+ this.ctx.closePath();
+ this.ctx.stroke();
+ this.ctx.restore();
+ }
+
+ update() {
+ this.opacity = Math.max(this.opacity - 0.003, 0);
+ this.p1 = this.updatePoint(this.p1);
+ this.p2 = this.updatePoint(this.p2);
+ this.p3 = this.updatePoint(this.p3);
+ this.count += 1;
+ this.checkBounds();
+ this.draw();
+ }
+
+ checkBounds() {
+ const inBounds = [this.p1, this.p2, this.p3].reduce(
+ (inBounds, { x, y }) => inBounds && x < width && y < height,
+ true
+ );
+ if (!inBounds && this.count > (delay / drawRate) * count)
+ this.resetPoints();
+ }
+
+ resetPoints() {
+ this.opacity = 1;
+ this.p1 = { x: 0, y: 0, ...this.getRandomUnitVector() };
+ this.p2 = { x: 0, y: 0, ...this.getRandomUnitVector() };
+ this.p3 = { x: 0, y: 0, ...this.getRandomUnitVector() };
+ this.count = 0;
+ }
+
+ getRandomUnitVector() {
+ const [mx, my] = [getRandom(), getRandom()]; //[deltaX, deltaY]
+ const mag = Math.sqrt(mx ** 2 + my ** 2);
+ return { mx: mx / mag, my: my / mag };
+ }
+
+ calcCentroid() {
+ const { p1, p2, p3 } = this;
+ return [(p1.x + p2.x + p3.x) / 3, (p1.y + p2.y + p3.y) / 3];
+ }
+
+ updatePoint({ x, y, mx, my }) {
+ // they appear to get slower as they get larger, so gradually increase
+ // the mx/my so that they accelerate out
+ return { x: x + mx, y: y + my, mx: mx * 1.005, my: my * 1.005 };
+ }
+}
diff --git a/src/index.js b/src/index.js
index b8d5502..2bdc3ee 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,6 +1,24 @@
-import { select } from "d3";
-import Title from "./Title";
+import { Animation } from "./Animation";
+import { Title } from "./Title";
+
import "./style.scss";
-select("#container").text("Interactive Data Visualization");
-Title();
+import { appConfig } from "../utils/constants";
+const { titleDuration } = appConfig;
+
+class Controller {
+ state = {
+ selectedStudent: null,
+ isAnimating: true,
+ };
+
+ constructor() {
+ this.animation = new Animation();
+ this.title = new Title();
+ this.animation.startAnimation();
+
+ setTimeout(() => this.title.makeVisible(), titleDuration);
+ }
+}
+
+new Controller();
diff --git a/src/style.scss b/src/style.scss
index 1f92011..c2f42b9 100644
--- a/src/style.scss
+++ b/src/style.scss
@@ -1,3 +1,22 @@
body {
- background: palegoldenrod;
+ font-family: "Open Sans";
+}
+
+canvas#animation {
+ position: absolute;
+ top: 0;
+ left: 0;
+}
+
+#page-title {
+ position: relative;
+ font-weight: 700;
+ font-size: 4em;
+ visibility: hidden;
+
+ &.visible {
+ visibility: visible;
+ color: transparent;
+ -webkit-text-stroke: black 1px;
+ }
}
diff --git a/utils/constants.js b/utils/constants.js
new file mode 100644
index 0000000..9db4ac3
--- /dev/null
+++ b/utils/constants.js
@@ -0,0 +1,14 @@
+export const getRandom = (min = -1, max = 1) =>
+ Math.random() * (max - min) + min;
+
+export const appConfig = {
+ titleDuration: 2500,
+};
+
+export const animationConfig = {
+ width: window.innerWidth,
+ height: window.innerHeight,
+ count: 25,
+ delay: 1500,
+ drawRate: 35,
+};