From 4b3d812031dd07c251623d7155159115e99d2671 Mon Sep 17 00:00:00 2001 From: Ben Beltran Date: Mon, 28 Aug 2017 22:54:26 -0500 Subject: Commit everything hastily I left evertything until the last minute, so I didn't have a chance to do it nicely. SORRY. --- lib/components/status.js | 16 ++++ lib/components/wave_renderer.js | 157 ++++++++++++++++++++++++++++++++++++++++ lib/services/data.js | 30 ++++++++ lib/sorting_hat.js | 30 ++++++++ 4 files changed, 233 insertions(+) create mode 100644 lib/components/status.js create mode 100644 lib/components/wave_renderer.js create mode 100644 lib/services/data.js create mode 100644 lib/sorting_hat.js (limited to 'lib') diff --git a/lib/components/status.js b/lib/components/status.js new file mode 100644 index 0000000..4a8b099 --- /dev/null +++ b/lib/components/status.js @@ -0,0 +1,16 @@ +import Vue from 'vue'; +import DataService from '../services/data'; + +const internals = {}; + +export default Vue.component('posts', { + template: '
' + + '' + + '
Waiting
' + + '
Could not read you
' + + '
{{winner}}
' + + '
' + + '
', + + data: DataService.data +}); diff --git a/lib/components/wave_renderer.js b/lib/components/wave_renderer.js new file mode 100644 index 0000000..c9cadc0 --- /dev/null +++ b/lib/components/wave_renderer.js @@ -0,0 +1,157 @@ +import Vue from 'vue'; +import DataService from '../services/data'; + +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; + } +}; + +export default Vue.component('posts', { + template: '' + + '' + + '', + + 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((averages, averageCategory) => { + + const runningAverage = this.runningAverages[averageCategory]; + averages[averageCategory] = runningAverage.average; + return averages; + }, {}); + + const max = Math.max(...Object.values(averages)); + const min = Math.min(...Object.values(averages)); + + const scaledAverages = Object.keys(averages).reduce((scaledAverages, averageCategory) => { + + const value = averages[averageCategory]; + scaledAverages[averageCategory] = internals.scale(value, min, max); + return scaledAverages; + }, {}); + + return scaledAverages; + } + }, + + methods: { + 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 currentSegment = 0; currentSegment < segments; ++currentSegment) { + const segmentStart = currentSegment * period; + const segmentEnd = currentSegment + period; + + const category = keys[currentSegment]; + const magnitude = values[currentSegment]; + 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 = currentSegment * gradientBandWidth + internals.kGradientFade; + fillGradient.addColorStop(currentGradientPosition, color); + currentGradientPosition = (currentSegment + 1) * gradientBandWidth - internals.kGradientFade; + fillGradient.addColorStop(currentGradientPosition, color); + + // This draws the sine wave + + for (let angle = 0; angle < 180; ++angle) { + const currentPixel = segmentStart + angle * period / 180; + const currentAngle = angle + 180 * (currentSegment % 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..cc6c5ee --- /dev/null +++ b/lib/services/data.js @@ -0,0 +1,30 @@ + +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)); + }); + } +}; + +export default { + 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..4534a9f --- /dev/null +++ b/lib/sorting_hat.js @@ -0,0 +1,30 @@ +import Vue from 'vue'; + +import WaveRendererComponent from './components/wave_renderer'; +import StatusComponent from './components/status'; + +/* global window */ + +const internals = {}; + +internals.SortingHat = { + + start() { + + this.vm = new Vue({ + el: '#sorting-hat', + components: { + waveRenderer: WaveRendererComponent, + status: StatusComponent + } + }); + } +}; + + +internals.run = function () { + + internals.SortingHat.start(); +}; + +window.addEventListener('load', internals.run); -- cgit From c6425032e13c139bedd0546119f6ed1c93365822 Mon Sep 17 00:00:00 2001 From: Ben Beltran Date: Wed, 30 Aug 2017 00:26:01 -0500 Subject: Add jsdoc, fix linter --- doc/README.md | 103 ++++++++++++++++++++++++++++++++++++++++ lib/components/status.js | 8 +++- lib/components/wave_renderer.js | 44 ++++++++++------- lib/services/data.js | 39 +++++++++++++++ lib/sorting_hat.js | 7 +++ 5 files changed, 183 insertions(+), 18 deletions(-) (limited to 'lib') diff --git a/doc/README.md b/doc/README.md index e69de29..495c624 100644 --- a/doc/README.md +++ b/doc/README.md @@ -0,0 +1,103 @@ +## Classes + +
+
StatusComponent
+
+
WaveRenderer
+
+
DataService
+
+
SortingHat
+
+
+ +## Typedefs + +
+
tSortingHatData : object
+

The data structure representing the sorting hat data

+
+
tRunningAverages : object
+

The running averages, including the current sum and count

+
+
+ + + +## StatusComponent +**Kind**: global class + + +### new StatusComponent() +The status class, renders the winner during the poll stage, and a +message while waiting + + + +## WaveRenderer +**Kind**: global class + + +### new WaveRenderer() +The wave renderer, draws some waves in a canvas to represent a set of +cateogirzed averages + + + +## DataService +**Kind**: global class + +* [DataService](#DataService) + * [new DataService()](#new_DataService_new) + * [.data()](#DataService.data) ⇒ + + + +### new DataService() +The main data service, connects to a socket and updates the internal +data structure + + + +### DataService.data() ⇒ +Returns the internal data structure, intended to be used as the data +property in vue components + +**Kind**: static method of [DataService](#DataService) +**Returns**: tSortingHatData + + +## SortingHat +**Kind**: global class + + +### new SortingHat() +The main vue application, it is composed by the other components, no real +logic otherwise + + + +## tSortingHatData : object +The data structure representing the sorting hat data + +**Kind**: global typedef + +| Param | Type | Description | +| --- | --- | --- | +| state | number | the current state: 0 for waiting, 1 for polling, 2 for cool down. | +| [winner] | string | the winner after polling, might be null if no winner is detected | +| runningAverages | Object.<string, tRunningAverages> | the running averages for the different categories, used to render the waves | + + + +## tRunningAverages : object +The running averages, including the current sum and count + +**Kind**: global typedef + +| Param | Type | Description | +| --- | --- | --- | +| sum | number | the current total | +| count | number | the number of samples | +| average | number | the average (sum / count) | + diff --git a/lib/components/status.js b/lib/components/status.js index 4a8b099..0f79788 100644 --- a/lib/components/status.js +++ b/lib/components/status.js @@ -3,7 +3,13 @@ import DataService from '../services/data'; const internals = {}; -export default Vue.component('posts', { +/** + * The status class, renders the winner during the poll stage, and a + * message while waiting + * + * @class StatusComponent + */ +export default Vue.component('status', { template: '
' + '' + '
Waiting
' + diff --git a/lib/components/wave_renderer.js b/lib/components/wave_renderer.js index c9cadc0..022f9ff 100644 --- a/lib/components/wave_renderer.js +++ b/lib/components/wave_renderer.js @@ -1,6 +1,8 @@ import Vue from 'vue'; import DataService from '../services/data'; +/* global window */ + const internals = { // Constants @@ -26,7 +28,13 @@ const internals = { } }; -export default Vue.component('posts', { +/** + * The wave renderer, draws some waves in a canvas to represent a set of + * cateogirzed averages + * + * @class WaveRenderer + */ +export default Vue.component('waveRenderer', { template: '' + '' + '', @@ -43,21 +51,21 @@ export default Vue.component('posts', { const keys = Object.keys(this.runningAverages); - const averages = keys.reduce((averages, averageCategory) => { + const averages = keys.reduce((averagesObject, averageCategory) => { const runningAverage = this.runningAverages[averageCategory]; - averages[averageCategory] = runningAverage.average; - return averages; + 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((scaledAverages, averageCategory) => { + const scaledAverages = Object.keys(averages).reduce((scaledAveragesObject, averageCategory) => { const value = averages[averageCategory]; - scaledAverages[averageCategory] = internals.scale(value, min, max); - return scaledAverages; + scaledAveragesObject[averageCategory] = internals.scale(value, min, max); + return scaledAveragesObject; }, {}); return scaledAverages; @@ -65,6 +73,9 @@ export default Vue.component('posts', { }, methods: { + + // Reset the size of the canvas on resize + onResize() { this.$el.width = window.innerWidth; @@ -114,28 +125,27 @@ export default Vue.component('posts', { // Iterate over the segments - for (let currentSegment = 0; currentSegment < segments; ++currentSegment) { - const segmentStart = currentSegment * period; - const segmentEnd = currentSegment + period; + for (let i = 0; i < segments; ++i) { + const segmentStart = i * period; - const category = keys[currentSegment]; - const magnitude = values[currentSegment]; + 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 = currentSegment * gradientBandWidth + internals.kGradientFade; + let currentGradientPosition = i * gradientBandWidth + internals.kGradientFade; fillGradient.addColorStop(currentGradientPosition, color); - currentGradientPosition = (currentSegment + 1) * gradientBandWidth - internals.kGradientFade; + currentGradientPosition = (i + 1) * gradientBandWidth - internals.kGradientFade; fillGradient.addColorStop(currentGradientPosition, color); // This draws the sine wave - for (let angle = 0; angle < 180; ++angle) { - const currentPixel = segmentStart + angle * period / 180; - const currentAngle = angle + 180 * (currentSegment % 2); + 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); diff --git a/lib/services/data.js b/lib/services/data.js index cc6c5ee..c7f6744 100644 --- a/lib/services/data.js +++ b/lib/services/data.js @@ -1,3 +1,4 @@ +/* global WebSocket */ const internals = { kSocketLocation: 'ws://localhost:1987', @@ -18,7 +19,45 @@ const internals = { } }; +/** + * 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.} 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) { diff --git a/lib/sorting_hat.js b/lib/sorting_hat.js index 4534a9f..2568b75 100644 --- a/lib/sorting_hat.js +++ b/lib/sorting_hat.js @@ -7,6 +7,12 @@ import StatusComponent from './components/status'; const internals = {}; +/** + * The main vue application, it is composed by the other components, no real + * logic otherwise + * + * @class SortingHat + */ internals.SortingHat = { start() { @@ -21,6 +27,7 @@ internals.SortingHat = { } }; +// Instantiates the app internals.run = function () { -- cgit