aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/components/status.js22
-rw-r--r--lib/components/wave_renderer.js167
-rw-r--r--lib/services/data.js69
-rw-r--r--lib/sorting_hat.js37
4 files changed, 295 insertions, 0 deletions
diff --git a/lib/components/status.js b/lib/components/status.js
new file mode 100644
index 0000000..0f79788
--- /dev/null
+++ b/lib/components/status.js
@@ -0,0 +1,22 @@
+import Vue from 'vue';
+import DataService from '../services/data';
+
+const internals = {};
+
+/**
+ * The status class, renders the winner during the poll stage, and a
+ * message while waiting
+ *
+ * @class StatusComponent
+ */
+export default Vue.component('status', {
+ template: '<div class="status-widget">' +
+ '<transition name="fade">' +
+ '<div v-if="state === 0" class="waiting-message">Waiting</div>' +
+ '<div v-if="state === 2 && !winner" class="no-winner">Could not read you</div>' +
+ '<div v-if="state === 2 && winner" class="winner" v-bind:class="[winner]">{{winner}}</div>' +
+ '</transition>' +
+ '</div>',
+
+ data: DataService.data
+});
diff --git a/lib/components/wave_renderer.js b/lib/components/wave_renderer.js
new file mode 100644
index 0000000..022f9ff
--- /dev/null
+++ b/lib/components/wave_renderer.js
@@ -0,0 +1,167 @@
+import Vue from 'vue';
+import DataService from '../services/data';
+
+/* global window */
+
+const internals = {
+
+ // Constants
+
+ kColors: {
+ donatello: 'purple',
+ leonardo: 'blue',
+ michaelangelo: 'orange',
+ raphael: 'red'
+ },
+ kFrequency: 100,
+ kGradientFade: 0.1,
+ kMinValue: 25,
+ kMaxValue: 100,
+ kRandomJitter: 10,
+ kTargetFPS: 60,
+
+ // Utility functions
+
+ scale(value, min, max) {
+
+ return ((value - min) / (max - min)) * (internals.kMaxValue - internals.kMinValue) - internals.kMinValue;
+ }
+};
+
+/**
+ * The wave renderer, draws some waves in a canvas to represent a set of
+ * cateogirzed averages
+ *
+ * @class WaveRenderer
+ */
+export default Vue.component('waveRenderer', {
+ template: '<transition name="fade">' +
+ '<canvas v-show="state === 1" class="wave-renderer"></canvas>' +
+ '</transition>',
+
+ data: DataService.data,
+
+ computed: {
+
+ // Convert from the min / max value in the measurement to a
+ // predefined min value between 0 and 100 (see kMinValue and
+ // kMaxValue for actual values)
+
+ scaledAverages() {
+
+ const keys = Object.keys(this.runningAverages);
+
+ const averages = keys.reduce((averagesObject, averageCategory) => {
+
+ const runningAverage = this.runningAverages[averageCategory];
+ averagesObject[averageCategory] = runningAverage.average;
+ return averagesObject;
+ }, {});
+
+ const max = Math.max(...Object.values(averages));
+ const min = Math.min(...Object.values(averages));
+
+ const scaledAverages = Object.keys(averages).reduce((scaledAveragesObject, averageCategory) => {
+
+ const value = averages[averageCategory];
+ scaledAveragesObject[averageCategory] = internals.scale(value, min, max);
+ return scaledAveragesObject;
+ }, {});
+
+ return scaledAverages;
+ }
+ },
+
+ methods: {
+
+ // Reset the size of the canvas on resize
+
+ onResize() {
+
+ this.$el.width = window.innerWidth;
+ this.$el.height = window.innerHeight;
+ }
+ },
+
+ // Initiates the animation loop
+
+ mounted() {
+
+ // Make sure we resize, do an initial sizing
+
+ window.addEventListener('resize', this.onResize.bind(this));
+ this.onResize();
+
+ // Start the whole animation (Sorry it's a mess)
+
+ const canvas = this.$el;
+
+ const context = canvas.getContext('2d');
+ const interval = 1000 / internals.kTargetFPS;
+
+ let lastTimestamp = 0;
+
+ const animationHandler = (timestamp) => {
+
+ window.requestAnimationFrame(animationHandler);
+ const delta = timestamp - lastTimestamp;
+
+ if (delta > interval) {
+
+ const keys = Object.keys(this.scaledAverages);
+ const values = Object.values(this.scaledAverages);
+ const segments = keys.length;
+ const period = canvas.width / segments;
+
+ const fillGradient = context.createLinearGradient(0, 0, canvas.width, 0);
+ const gradientBandWidth = 1 / segments;
+
+ // Position the drawing cursor left-center of screen
+
+ context.clearRect(0, 0, canvas.width, canvas.height);
+ context.beginPath();
+ context.moveTo(0, canvas.height);
+ context.lineTo(0, canvas.height / 2);
+
+ // Iterate over the segments
+
+ for (let i = 0; i < segments; ++i) {
+ const segmentStart = i * period;
+
+ const category = keys[i];
+ const magnitude = values[i];
+ const segmentHeight = Math.round(Math.random() * internals.kRandomJitter + magnitude * (canvas.height / 2) / 100);
+
+ // Calculate the gradient using the correct color according to
+ // scale
+
+ const color = internals.kColors[category] || 'black';
+ let currentGradientPosition = i * gradientBandWidth + internals.kGradientFade;
+ fillGradient.addColorStop(currentGradientPosition, color);
+ currentGradientPosition = (i + 1) * gradientBandWidth - internals.kGradientFade;
+ fillGradient.addColorStop(currentGradientPosition, color);
+
+ // This draws the sine wave
+
+ for (let j = 0; j < 180; ++j) {
+ const currentPixel = segmentStart + j * period / 180;
+ const currentAngle = j + 180 * (i % 2);
+ const currentRadians = currentAngle * Math.PI / 180;
+ const currentHeight = segmentHeight * Math.sin(internals.kFrequency * currentRadians);
+
+ context.lineTo(currentPixel, currentHeight + canvas.height / 2);
+ }
+ }
+
+ context.lineTo(canvas.width, canvas.height / 2);
+ context.lineTo(canvas.width, canvas.height);
+ context.fillStyle = fillGradient;
+ context.fill();
+
+ lastTimestamp = timestamp;
+ }
+ };
+
+ window.requestAnimationFrame(animationHandler);
+ }
+});
diff --git a/lib/services/data.js b/lib/services/data.js
new file mode 100644
index 0000000..c7f6744
--- /dev/null
+++ b/lib/services/data.js
@@ -0,0 +1,69 @@
+/* global WebSocket */
+
+const internals = {
+ kSocketLocation: 'ws://localhost:1987',
+
+ data: {
+ state: 0,
+ runningAverages: {},
+ winner: null
+ },
+
+ initSocket() {
+
+ internals.socket = new WebSocket(internals.kSocketLocation);
+ internals.socket.addEventListener('message', (data) => {
+
+ Object.assign(internals.data, JSON.parse(data.data));
+ });
+ }
+};
+
+/**
+ * The data structure representing the sorting hat data
+ *
+ * @typedef tSortingHatData
+ * @type object
+ * @param {number} state the current state: 0 for waiting, 1 for
+ * polling, 2 for cool down.
+ * @param {string} [winner] the winner after polling, might be null if
+ * no winner is detected
+ * @param {Object.<string,tRunningAverages>} runningAverages the running averages for
+ * the different categories, used to render the waves
+ */
+
+/**
+ * The running averages, including the current sum and count
+ *
+ * @typedef tRunningAverages
+ * @type object
+ * @param {number} sum the current total
+ * @param {number} count the number of samples
+ * @param {number} average the average (sum / count)
+ */
+
+/**
+ * The main data service, connects to a socket and updates the internal
+ * data structure
+ *
+ * @class DataService
+ */
+export default {
+
+ /**
+ * Returns the internal data structure, intended to be used as the data
+ * property in vue components
+ *
+ * @memberof DataService
+ * @method data
+ * @return tSortingHatData
+ */
+ data() {
+
+ if (!internals.socket) {
+ internals.initSocket();
+ }
+
+ return internals.data;
+ }
+};
diff --git a/lib/sorting_hat.js b/lib/sorting_hat.js
new file mode 100644
index 0000000..2568b75
--- /dev/null
+++ b/lib/sorting_hat.js
@@ -0,0 +1,37 @@
+import Vue from 'vue';
+
+import WaveRendererComponent from './components/wave_renderer';
+import StatusComponent from './components/status';
+
+/* global window */
+
+const internals = {};
+
+/**
+ * The main vue application, it is composed by the other components, no real
+ * logic otherwise
+ *
+ * @class SortingHat
+ */
+internals.SortingHat = {
+
+ start() {
+
+ this.vm = new Vue({
+ el: '#sorting-hat',
+ components: {
+ waveRenderer: WaveRendererComponent,
+ status: StatusComponent
+ }
+ });
+ }
+};
+
+// Instantiates the app
+
+internals.run = function () {
+
+ internals.SortingHat.start();
+};
+
+window.addEventListener('load', internals.run);