├── .gitignore ├── favicon.png ├── todo.md ├── manifest.webmanifest ├── favicon.svg ├── worker.js ├── LICENSE ├── index.html ├── style.css └── app.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | reference -------------------------------------------------------------------------------- /favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/transkatgirl/TheMoodMeter/HEAD/favicon.png -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | # todos 2 | 3 | - add color coding based on time of day 4 | - add time-based display options (like display past day worth of data points) 5 | - add archiving functionality for data points >1 month old, rather than discarding data points entirely 6 | -------------------------------------------------------------------------------- /manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "background_color": "white", 3 | "description": "Track your moods using a popular mood chart.", 4 | "display": "standalone", 5 | "icons": [ 6 | { 7 | "src": "favicon.png", 8 | "sizes": "512x512", 9 | "type": "image/png" 10 | } 11 | ], 12 | "name": "The Mood Meter", 13 | "short_name": "Mood Meter", 14 | "start_url": "/TheMoodMeter/" 15 | } -------------------------------------------------------------------------------- /favicon.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /worker.js: -------------------------------------------------------------------------------- 1 | const version = "21"; 2 | 3 | const app_files = [ 4 | "./", 5 | "index.html", 6 | "app.js", 7 | "style.css", 8 | "favicon.svg", 9 | "favicon.png", 10 | "manifest.webmanifest" 11 | ]; 12 | 13 | self.addEventListener("install", (e) => { 14 | e.waitUntil( 15 | (async () => { 16 | const cache = await caches.open("app_cache-v" + version); 17 | await cache.addAll(app_files); 18 | })() 19 | ); 20 | }); 21 | 22 | self.addEventListener("fetch", (e) => { 23 | e.respondWith( 24 | (async () => { 25 | const r = await caches.match(e.request); 26 | if (r) { 27 | return r; 28 | } 29 | const response = await fetch(e.request); 30 | const cache = await caches.open("app_cache-v" + version); 31 | cache.put(e.request, response.clone()); 32 | return response; 33 | })() 34 | ); 35 | }); 36 | 37 | self.addEventListener("activate", (e) => { 38 | e.waitUntil( 39 | caches.keys().then((keyList) => { 40 | return Promise.all( 41 | keyList.map((key) => { 42 | if (key === "app_cache-v" + version) { 43 | return; 44 | } 45 | return caches.delete(key); 46 | }) 47 | ); 48 | }) 49 | ); 50 | }); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | The Mood Meter 13 | 14 | 15 | 16 | 17 |
18 |
19 |

The Mood Meter

20 | ← High Energy to Low energy → 21 | 22 |

An interative mood meter chart. The chart is 23 | seperated into different areas based on energy levels and pleasantness, and the moods that have been 24 | recorded are plotted onto the chart. To plot a new mood data point on the chart, the chart can be 25 | clicked in the area where the data point should be placed.

26 |
27 |
28 | ← Unpleasant to Pleasant → 29 |
30 |
31 |
32 | Settings 33 |

Appearance

34 | 41 |

42 |
43 |

Data

44 |
45 |
48 |

49 | 50 | 51 | 52 | 53 | 54 | 55 |

56 |
57 |

Graphing

58 |
59 |
61 |

62 | 63 | 64 | 65 | 66 |

67 |
68 |

Troubleshooting

69 | 70 | 71 | 72 |
73 |
74 | Recorded moods 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 |
DateTimeMoodNotes
83 |
84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font: 1em sans-serif; 3 | } 4 | 5 | .container { 6 | display: flex; 7 | } 8 | 9 | .vertical { 10 | writing-mode: vertical-rl; 11 | } 12 | 13 | canvas { 14 | max-width: 38em; 15 | } 16 | 17 | details { 18 | max-width: 32em; 19 | width: fit-content; 20 | } 21 | 22 | details { 23 | margin: .5em; 24 | } 25 | 26 | th, 27 | td { 28 | max-width: 13em; 29 | } 30 | 31 | td, 32 | th, 33 | summary, 34 | details[open] { 35 | padding: .5em; 36 | } 37 | 38 | button, 39 | input, 40 | select, 41 | details { 42 | border: .125em solid black; 43 | } 44 | 45 | td:not(:last-child), 46 | th:not(:last-child) { 47 | border-right: .125em solid black; 48 | } 49 | 50 | tr:not(:last-child) th, 51 | tr:not(:last-child) td, 52 | details[open] summary { 53 | border-bottom: .125em solid black; 54 | } 55 | 56 | hr { 57 | border: .0625em solid black; 58 | } 59 | 60 | details { 61 | padding: .5em .5em 0; 62 | } 63 | 64 | table { 65 | overflow: auto; 66 | display: inline-block; 67 | max-height: 32em; 68 | border-spacing: 0; 69 | } 70 | 71 | summary { 72 | font-weight: bold; 73 | margin: -0.5em -0.5em 0; 74 | } 75 | 76 | details[open] summary { 77 | margin-bottom: .5em; 78 | } 79 | 80 | #upload { 81 | display: none; 82 | } 83 | 84 | button, 85 | input, 86 | select { 87 | background-color: gainsboro; 88 | padding: .2em .4em; 89 | margin: .2em 0; 90 | font-size: .8em; 91 | color: black; 92 | } 93 | 94 | button, 95 | input, 96 | select, 97 | canvas, 98 | details { 99 | border-radius: .25em; 100 | } 101 | 102 | .meter { 103 | display: grid; 104 | grid-template-columns: 1em 1fr; 105 | width: min-content; 106 | margin-right: .5em; 107 | } 108 | 109 | span { 110 | text-align: center; 111 | padding: .1em; 112 | } 113 | 114 | td a { 115 | color: inherit; 116 | text-decoration: none; 117 | } 118 | 119 | @media screen and (min-width: 69.5em) { 120 | details { 121 | float: right; 122 | } 123 | } 124 | 125 | @media screen and (min-width: 74.75em) { 126 | .meter { 127 | float: left; 128 | } 129 | } 130 | 131 | @media screen and (max-width: 41em) { 132 | canvas { 133 | width: 100%; 134 | } 135 | 136 | .meter { 137 | width: 97.5%; 138 | } 139 | } 140 | 141 | @media screen and (max-width: 35.25em) { 142 | table { 143 | max-width: 100%; 144 | } 145 | 146 | details { 147 | max-width: 100%; 148 | } 149 | } 150 | 151 | @media screen and (max-width: 25.1875em) { 152 | details { 153 | width: unset; 154 | } 155 | 156 | td, 157 | th { 158 | max-width: 6.25em; 159 | } 160 | } 161 | 162 | @media screen and (min-width: 25.25em) { 163 | 164 | td:last-child, 165 | th:last-child { 166 | word-break: break-word; 167 | } 168 | } 169 | 170 | @media (prefers-color-scheme: dark) { 171 | body { 172 | background-color: #1c1c1c; 173 | color: #eaeaea; 174 | } 175 | 176 | button, 177 | input, 178 | select { 179 | background-color: #383838; 180 | color: #eaeaea; 181 | } 182 | 183 | button, 184 | input, 185 | select, 186 | details { 187 | border: .125em solid #383838; 188 | } 189 | 190 | details[open] summary { 191 | border-bottom: .125em solid #383838; 192 | } 193 | 194 | td:not(:last-child), 195 | th:not(:last-child) { 196 | border-right: .125em solid #545454; 197 | } 198 | 199 | tr:not(:last-child) th, 200 | tr:not(:last-child) td { 201 | border-bottom: .125em solid #545454; 202 | } 203 | 204 | hr { 205 | border: .0625em solid #545454; 206 | } 207 | } -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | // yes i'm aware the code is a mess, i'll clean it up soon 2 | 3 | const canvas = document.getElementById("canvas"); 4 | const ctx = canvas.getContext("2d"); 5 | 6 | canvas.templateCanvas = document.createElement("canvas"); 7 | canvas.templateCanvas.width = canvas.width; 8 | canvas.templateCanvas.height = canvas.height; 9 | 10 | const offscreen_ctx = canvas.templateCanvas.getContext("2d"); 11 | 12 | const table = document.getElementById("table"); 13 | 14 | var mood_data = []; 15 | var config = { moods_open: true, settings_open: false, theme: "1", maximum_data_points: 1200, maximum_graphed_points: 15, minimum_minutes: 5, data_hide_time: 2.5 }; 16 | 17 | var default_config = config; 18 | 19 | const download_link = document.getElementById("download"); 20 | 21 | const mood_menu = document.getElementById("moods"); 22 | const settings_menu = document.getElementById("settings"); 23 | const theme_input = document.getElementById("theme"); 24 | const data_limit_input = document.getElementById("maxData"); 25 | const data_graph_limit_input = document.getElementById("maxGraphData"); 26 | const data_combine_input = document.getElementById("minTime"); 27 | const data_hide_input = document.getElementById("hideTime"); 28 | 29 | function renderTemplate0(ctx) { 30 | ctx.fillStyle = "firebrick"; 31 | ctx.fillRect(0, 0, canvas.width * 0.5, canvas.height * 0.5); 32 | ctx.fillStyle = "lightblue"; 33 | ctx.fillRect(0, canvas.height * 0.5, canvas.width * 0.5, canvas.height * 0.5); 34 | ctx.fillStyle = "darkseagreen"; 35 | ctx.fillRect(canvas.width * 0.5, canvas.height * 0.5, canvas.width * 0.5, canvas.height * 0.5); 36 | ctx.fillStyle = "darkolivegreen"; 37 | ctx.fillRect(canvas.width * 0.5, 0, canvas.width * 0.5, canvas.height * 0.5); 38 | 39 | ctx.lineWidth = Math.min(canvas.width, canvas.height) * 0.005; 40 | ctx.strokeStyle = "black"; 41 | ctx.beginPath(); 42 | ctx.moveTo(canvas.width * 0.5, 0); 43 | ctx.lineTo(canvas.width * 0.5, canvas.height); 44 | ctx.moveTo(0, canvas.height * 0.5); 45 | ctx.lineTo(canvas.width, canvas.height * 0.5); 46 | ctx.stroke(); 47 | } 48 | 49 | function renderTemplate1(ctx) { 50 | ctx.fillStyle = "firebrick"; 51 | ctx.fillRect(0, 0, canvas.width * 0.5, canvas.height * 0.5); 52 | ctx.fillStyle = "lightblue"; 53 | ctx.fillRect(0, canvas.height * 0.5, canvas.width * 0.5, canvas.height * 0.5); 54 | ctx.fillStyle = "darkseagreen"; 55 | ctx.fillRect(canvas.width * 0.5, canvas.height * 0.5, canvas.width * 0.5, canvas.height * 0.5); 56 | ctx.fillStyle = "darkolivegreen"; 57 | ctx.fillRect(canvas.width * 0.5, 0, canvas.width * 0.5, canvas.height * 0.5); 58 | 59 | ctx.fillStyle = "orange"; 60 | ctx.fillRect(0, canvas.height * (0.5 - (0.17 - 0.02)), canvas.width * (0.17 + (0.5 - 0.02)), canvas.height * 0.17); 61 | ctx.fillRect(canvas.width * (0.5 - 0.02), canvas.height * 0.5, canvas.width * 0.17, canvas.height * 0.5); 62 | 63 | ctx.fillStyle = "cornflowerblue"; 64 | ctx.fillRect(0, canvas.height * 0.85, canvas.width * 0.15, canvas.height * 0.15); 65 | ctx.fillStyle = "khaki"; 66 | ctx.fillRect(canvas.width * 0.85, 0, canvas.width * 0.15, canvas.height * 0.15); 67 | 68 | ctx.fillStyle = "black"; 69 | ctx.textAlign = "center"; 70 | ctx.textBaseline = "middle"; 71 | 72 | let textSize = Math.min(canvas.width, canvas.height) * 0.02; 73 | ctx.font = textSize + "px sans-serif"; 74 | 75 | ctx.fillText("Mom would", canvas.width * 0.075, (canvas.height * 0.925) - (textSize / 2)); 76 | ctx.fillText("be sad", canvas.width * 0.075, (canvas.height * 0.925) + (textSize / 2)); 77 | ctx.fillText("LETS FUCKING", canvas.width * 0.925, (canvas.height * 0.075) - (textSize / 2)); 78 | ctx.fillText("GOOOOOOOO", canvas.width * 0.925, (canvas.height * 0.075) + (textSize / 2)); 79 | 80 | textSize = Math.min(canvas.width, canvas.height) * 0.09; 81 | ctx.font = textSize + "px sans-serif"; 82 | 83 | ctx.fillText("It's so", canvas.width * 0.24, (canvas.height * 0.685) - (textSize / 2)); 84 | ctx.fillText("over", canvas.width * 0.24, (canvas.height * 0.685) + (textSize / 2)); 85 | ctx.fillText("We", canvas.width * 0.825, (canvas.height * 0.75) - (textSize / 2)); 86 | ctx.fillText("vibing", canvas.width * 0.825, (canvas.height * 0.75) + (textSize / 2)); 87 | ctx.fillText("Fuck it", canvas.width * 0.25, (canvas.height * 0.175) - (textSize / 2)); 88 | ctx.fillText("we ball", canvas.width * 0.25, (canvas.height * 0.175) + (textSize / 2)); 89 | 90 | ctx.fillText("It is", canvas.width * 0.36, canvas.height * 0.435); 91 | 92 | ctx.save(); 93 | ctx.translate(canvas.width * (0.565 - 0.02125), canvas.height * (0.435 + 0.02125)); 94 | ctx.rotate(Math.PI / 4); 95 | ctx.fillText("what", 0, 0); 96 | ctx.restore(); 97 | ctx.save(); 98 | ctx.translate(canvas.width * 0.565, canvas.height * 0.64); 99 | ctx.rotate(Math.PI / 2); 100 | ctx.fillText("it is", 0, 0); 101 | ctx.restore(); 102 | ctx.save(); 103 | ctx.translate(canvas.width * 0.75, canvas.height * 0.25); 104 | ctx.rotate(Math.PI / 3.6); 105 | ctx.fillText("We are so", 0, - (textSize / 2)); 106 | ctx.fillText("fucking back", 0, (textSize / 2)); 107 | ctx.restore(); 108 | } 109 | 110 | function renderTemplate2(ctx) { 111 | ctx.fillStyle = "firebrick"; 112 | ctx.fillRect(0, 0, canvas.width * 0.166, canvas.height * 0.5); 113 | ctx.fillStyle = "chocolate"; 114 | ctx.fillRect(canvas.width * 0.166, 0, canvas.width * 0.167, canvas.height * 0.5); 115 | ctx.fillStyle = "lightcoral"; 116 | ctx.fillRect(canvas.width * 0.333, 0, canvas.width * 0.167, canvas.height * 0.5); 117 | ctx.fillStyle = "lightsalmon"; 118 | ctx.fillRect(canvas.width * 0.5, 0, canvas.width * 0.167, canvas.height * 0.5); 119 | ctx.fillStyle = "khaki"; 120 | ctx.fillRect(canvas.width * 0.667, 0, canvas.width * 0.167, canvas.height * 0.5); 121 | ctx.fillStyle = "goldenrod"; 122 | ctx.fillRect(canvas.width * 0.834, 0, canvas.width * 0.166, canvas.height * 0.5); 123 | ctx.fillStyle = "cornflowerblue"; 124 | ctx.fillRect(0, canvas.height * 0.5, canvas.width * 0.166, canvas.height * 0.5); 125 | ctx.fillStyle = "cadetblue"; 126 | ctx.fillRect(canvas.width * 0.166, canvas.height * 0.5, canvas.width * 0.167, canvas.height * 0.5); 127 | ctx.fillStyle = "paleturquoise"; 128 | ctx.fillRect(canvas.width * 0.333, canvas.height * 0.5, canvas.width * 0.167, canvas.height * 0.5); 129 | ctx.fillStyle = "darkseagreen"; 130 | ctx.fillRect(canvas.width * 0.5, canvas.height * 0.5, canvas.width * 0.167, canvas.height * 0.5); 131 | ctx.fillStyle = "lightgreen"; 132 | ctx.fillRect(canvas.width * 0.667, canvas.height * 0.5, canvas.width * 0.167, canvas.height * 0.5); 133 | ctx.fillStyle = "darkolivegreen"; 134 | ctx.fillRect(canvas.width * 0.834, canvas.height * 0.5, canvas.width * 0.166, canvas.height * 0.5); 135 | 136 | ctx.lineWidth = Math.min(canvas.width, canvas.height) * 0.005; 137 | ctx.strokeStyle = "white"; 138 | ctx.beginPath(); 139 | ctx.moveTo(canvas.width * 0.166, 0); 140 | ctx.lineTo(canvas.width * 0.166, canvas.height); 141 | ctx.moveTo(canvas.width * 0.333, 0); 142 | ctx.lineTo(canvas.width * 0.333, canvas.height); 143 | ctx.moveTo(canvas.width * 0.5, 0); 144 | ctx.lineTo(canvas.width * 0.5, canvas.height); 145 | ctx.moveTo(canvas.width * 0.667, 0); 146 | ctx.lineTo(canvas.width * 0.667, canvas.height); 147 | ctx.moveTo(canvas.width * 0.834, 0); 148 | ctx.lineTo(canvas.width * 0.834, canvas.height); 149 | ctx.moveTo(0, canvas.height * 0.166); 150 | ctx.lineTo(canvas.width, canvas.height * 0.166); 151 | ctx.moveTo(0, canvas.height * 0.333); 152 | ctx.lineTo(canvas.width, canvas.height * 0.333); 153 | ctx.moveTo(0, canvas.height * 0.5); 154 | ctx.lineTo(canvas.width, canvas.height * 0.5); 155 | ctx.moveTo(0, canvas.height * 0.667); 156 | ctx.lineTo(canvas.width, canvas.height * 0.667); 157 | ctx.moveTo(0, canvas.height * 0.834); 158 | ctx.lineTo(canvas.width, canvas.height * 0.834); 159 | ctx.stroke(); 160 | 161 | ctx.fillStyle = "black"; 162 | ctx.textAlign = "center"; 163 | ctx.textBaseline = "middle"; 164 | 165 | let textSize = Math.min(canvas.width, canvas.height) * 0.021; 166 | ctx.font = textSize + "px sans-serif"; 167 | ctx.fillText("I am a", canvas.width * 0.083, (canvas.height * 0.083) - (textSize + (textSize / 2))); 168 | ctx.fillText("genuine", canvas.width * 0.083, (canvas.height * 0.083) - (textSize / 2)); 169 | ctx.fillText("threat to", canvas.width * 0.083, (canvas.height * 0.083) + (textSize / 2)); 170 | ctx.fillText("society", canvas.width * 0.083, (canvas.height * 0.083) + (textSize + (textSize / 2))); 171 | ctx.fillText("(In Minecraft)", canvas.width * 0.2495, (canvas.height * 0.083)); 172 | ctx.fillText("Goblin", canvas.width * 0.4165, (canvas.height * 0.083) - (textSize / 2)); 173 | ctx.fillText("Mode", canvas.width * 0.4165, (canvas.height * 0.083) + (textSize / 2)); 174 | ctx.fillText("Silly", canvas.width * 0.5835, (canvas.height * 0.083) - (textSize / 2)); 175 | ctx.fillText("Goose", canvas.width * 0.5835, (canvas.height * 0.083) + (textSize / 2)); 176 | ctx.fillText("Let's", canvas.width * 0.7505, (canvas.height * 0.083) - (textSize / 2)); 177 | ctx.fillText("gooooooo", canvas.width * 0.7505, (canvas.height * 0.083) + (textSize / 2)); 178 | ctx.fillText("LETS FUCKING", canvas.width * 0.917, (canvas.height * 0.083) - (textSize / 2)); 179 | ctx.fillText("GOOOOOOOO", canvas.width * 0.917, (canvas.height * 0.083) + (textSize / 2)); 180 | 181 | ctx.fillText("You've got to", canvas.width * 0.083, (canvas.height * 0.2495) - textSize); 182 | ctx.fillText("be fucking", canvas.width * 0.083, (canvas.height * 0.2495)); 183 | ctx.fillText("kidding me", canvas.width * 0.083, (canvas.height * 0.2495) + textSize); 184 | ctx.fillText("FUCK", canvas.width * 0.2495, (canvas.height * 0.2495)); 185 | ctx.fillText("Fuck it", canvas.width * 0.4165, (canvas.height * 0.2495) - (textSize / 2)); 186 | ctx.fillText("we ball", canvas.width * 0.4165, (canvas.height * 0.2495) + (textSize / 2)); 187 | ctx.fillText("Bitches love", canvas.width * 0.5835, (canvas.height * 0.2495) - (textSize / 2)); 188 | ctx.fillText("my mustache", canvas.width * 0.5835, (canvas.height * 0.2495) + (textSize / 2)); 189 | ctx.fillText("We're so", canvas.width * 0.7505, (canvas.height * 0.2495) - (textSize / 2)); 190 | ctx.fillText("back", canvas.width * 0.7505, (canvas.height * 0.2495) + (textSize / 2)); 191 | ctx.fillText("We're so", canvas.width * 0.917, (canvas.height * 0.2495) - (textSize / 2)); 192 | ctx.fillText("fucking back", canvas.width * 0.917, (canvas.height * 0.2495) + (textSize / 2)); 193 | 194 | ctx.fillText("I hate", canvas.width * 0.083, (canvas.height * 0.4165) - (textSize / 2)); 195 | ctx.fillText("everyone", canvas.width * 0.083, (canvas.height * 0.4165) + (textSize / 2)); 196 | ctx.fillText("I don't think we", canvas.width * 0.2495, (canvas.height * 0.4165) - textSize); 197 | ctx.fillText("making it out of", canvas.width * 0.2495, (canvas.height * 0.4165)); 198 | ctx.fillText("the hood bro", canvas.width * 0.2495, (canvas.height * 0.4165) + textSize); 199 | ctx.fillText("*Internal", canvas.width * 0.4165, (canvas.height * 0.4165) - (textSize / 2)); 200 | ctx.fillText("screaming*", canvas.width * 0.4165, (canvas.height * 0.4165) + (textSize / 2)); 201 | ctx.fillText("It is", canvas.width * 0.5835, (canvas.height * 0.4165) - textSize); 202 | ctx.fillText("what it", canvas.width * 0.5835, (canvas.height * 0.4165)); 203 | ctx.fillText("is", canvas.width * 0.5835, (canvas.height * 0.4165) + textSize); 204 | ctx.fillText("We're gonna", canvas.width * 0.7505, (canvas.height * 0.4165) - (textSize / 2)); 205 | ctx.fillText("make it bro", canvas.width * 0.7505, (canvas.height * 0.4165) + (textSize / 2)); 206 | ctx.fillText("Modelo", canvas.width * 0.917, (canvas.height * 0.4165) - (textSize / 2)); 207 | ctx.fillText("time", canvas.width * 0.917, (canvas.height * 0.4165) + (textSize / 2)); 208 | 209 | ctx.fillText("It's the kind", canvas.width * 0.083, (canvas.height * 0.5835) - (textSize + (textSize / 2))); 210 | ctx.fillText("of tired that", canvas.width * 0.083, (canvas.height * 0.5835) - (textSize / 2)); 211 | ctx.fillText("sleep won't", canvas.width * 0.083, (canvas.height * 0.5835) + (textSize / 2)); 212 | ctx.fillText("fix...", canvas.width * 0.083, (canvas.height * 0.5835) + (textSize + (textSize / 2))); 213 | ctx.fillText("It's just", canvas.width * 0.2495, (canvas.height * 0.5835) - (textSize + (textSize / 2))); 214 | ctx.fillText("one of", canvas.width * 0.2495, (canvas.height * 0.5835) - (textSize / 2)); 215 | ctx.fillText("those", canvas.width * 0.2495, (canvas.height * 0.5835) + (textSize / 2)); 216 | ctx.fillText("days...", canvas.width * 0.2495, (canvas.height * 0.5835) + (textSize + (textSize / 2))); 217 | ctx.fillText("yeah bro, I", canvas.width * 0.4165, (canvas.height * 0.5835) - textSize); 218 | ctx.fillText("just need", canvas.width * 0.4165, (canvas.height * 0.5835)); 219 | ctx.fillText("some sleep", canvas.width * 0.4165, (canvas.height * 0.5835) + textSize); 220 | ctx.fillText("It really do", canvas.width * 0.5835, (canvas.height * 0.5835) - textSize); 221 | ctx.fillText("be like that", canvas.width * 0.5835, (canvas.height * 0.5835)); 222 | ctx.fillText("sometimes", canvas.width * 0.5835, (canvas.height * 0.5835) + textSize); 223 | ctx.fillText("Straight", canvas.width * 0.7505, (canvas.height * 0.5835) - (textSize / 2)); 224 | ctx.fillText("chilling", canvas.width * 0.7505, (canvas.height * 0.5835) + (textSize / 2)); 225 | ctx.fillText("\"neat\"", canvas.width * 0.917, (canvas.height * 0.5835)); 226 | 227 | ctx.fillText("My", canvas.width * 0.083, (canvas.height * 0.7505) - (textSize + (textSize / 2))); 228 | ctx.fillText("mom", canvas.width * 0.083, (canvas.height * 0.7505) - (textSize / 2)); 229 | ctx.fillText("would", canvas.width * 0.083, (canvas.height * 0.7505) + (textSize / 2)); 230 | ctx.fillText("be sad", canvas.width * 0.083, (canvas.height * 0.7505) + (textSize + (textSize / 2))); 231 | ctx.fillText("It's so", canvas.width * 0.2495, (canvas.height * 0.7505) - (textSize / 2)); 232 | ctx.fillText("over", canvas.width * 0.2495, (canvas.height * 0.7505) + (textSize / 2)); 233 | ctx.fillText("This time", canvas.width * 0.4165, (canvas.height * 0.7505) - textSize); 234 | ctx.fillText("I'm really", canvas.width * 0.4165, (canvas.height * 0.7505)); 235 | ctx.fillText("gonna do it", canvas.width * 0.4165, (canvas.height * 0.7505) + textSize); 236 | ctx.fillText("Anotha day,", canvas.width * 0.5835, (canvas.height * 0.7505) - (textSize / 2)); 237 | ctx.fillText("anotha dolla", canvas.width * 0.5835, (canvas.height * 0.7505) + (textSize / 2)); 238 | ctx.fillText("\"cool\"", canvas.width * 0.7505, (canvas.height * 0.7505)); 239 | ctx.fillText("We", canvas.width * 0.917, (canvas.height * 0.7505) - (textSize / 2)); 240 | ctx.fillText("vibing", canvas.width * 0.917, (canvas.height * 0.7505) + (textSize / 2)); 241 | 242 | ctx.fillText("I geuss", canvas.width * 0.083, (canvas.height * 0.917) - (textSize + (textSize / 2))); 243 | ctx.fillText("that's it", canvas.width * 0.083, (canvas.height * 0.917) - (textSize / 2)); 244 | ctx.fillText("(Ronnie", canvas.width * 0.083, (canvas.height * 0.917) + (textSize / 2)); 245 | ctx.fillText("McNutt)", canvas.width * 0.083, (canvas.height * 0.917) + (textSize + (textSize / 2))); 246 | ctx.fillText("My dog", canvas.width * 0.2495, (canvas.height * 0.917) - textSize); 247 | ctx.fillText("wouldn't", canvas.width * 0.2495, (canvas.height * 0.917)); 248 | ctx.fillText("understand", canvas.width * 0.2495, (canvas.height * 0.917) + textSize); 249 | ctx.fillText("One day", canvas.width * 0.4165, (canvas.height * 0.917) - textSize); 250 | ctx.fillText("something", canvas.width * 0.4165, (canvas.height * 0.917)); 251 | ctx.fillText("will kill me", canvas.width * 0.4165, (canvas.height * 0.917) + textSize); 252 | ctx.fillText("Comfortably", canvas.width * 0.5835, (canvas.height * 0.917) - (textSize / 2)); 253 | ctx.fillText("numb", canvas.width * 0.5835, (canvas.height * 0.917) + (textSize / 2)); 254 | ctx.fillText("\"aight\"", canvas.width * 0.7505, (canvas.height * 0.917)); 255 | ctx.fillText("I'm", canvas.width * 0.917, (canvas.height * 0.917) - textSize); 256 | ctx.fillText("Bing", canvas.width * 0.917, (canvas.height * 0.917)); 257 | ctx.fillText("chilling", canvas.width * 0.917, (canvas.height * 0.917) + textSize); 258 | } 259 | 260 | function addNotes(timestamp) { 261 | let point = mood_data.find(element => new Date(element.timestamp).valueOf() == new Date(timestamp).valueOf()); 262 | 263 | let oldnotes = ""; 264 | 265 | if (point.notes != undefined) { 266 | oldnotes = point.notes; 267 | } 268 | 269 | let notes = window.prompt("What notes would you like to add to the data point recorded on " + new Date(point.timestamp).toLocaleString() + "?", oldnotes); 270 | 271 | if (notes == null) { 272 | return; 273 | } 274 | 275 | point.notes = notes; 276 | window.localStorage.setItem("mood_data", JSON.stringify(mood_data)); 277 | 278 | let row = Array.from(table.rows).find(row => row.cells[0].innerText == new Date(timestamp).toLocaleDateString() && row.cells[1].innerText == new Date(timestamp).toLocaleTimeString() && row.cells[2].innerText == "(" + Math.round(point.valence * 1000) / 100 + ", " + Math.round(point.arousal * 1000) / 100 + ")"); 279 | 280 | row.cells[3].innerText = ""; 281 | row.cells[3].appendChild(document.createTextNode(notes)); 282 | } 283 | 284 | function addTableRow(table, dataT, dataX, dataY, notes) { 285 | let row = table.insertRow(1); 286 | 287 | row.insertCell().innerText = new Date(dataT).toLocaleDateString(); 288 | row.insertCell().innerText = new Date(dataT).toLocaleTimeString(); 289 | row.insertCell().innerHTML = "(" + Math.round(dataX * 1000) / 100 + ", " + Math.round(dataY * 1000) / 100 + ")"; 290 | 291 | if (notes == undefined) { 292 | row.insertCell().innerText = ""; 293 | } else { 294 | row.insertCell().appendChild(document.createTextNode(notes)); 295 | } 296 | 297 | row.onclick = function () { 298 | addNotes(dataT); 299 | }; 300 | } 301 | 302 | function clearTable(table) { 303 | for (var i = 1, row; row = table.rows.length - 1; i++) { 304 | table.deleteRow(1); 305 | } 306 | } 307 | 308 | function addData(dataT, dataX, dataY, notes) { 309 | if ((mood_data.length > 0) && (dataT - mood_data[mood_data.length - 1].timestamp < 60 * 1000 * config.minimum_minutes)) { 310 | let oldnotes = mood_data[mood_data.length - 1].notes; 311 | if (oldnotes != undefined && notes == "") { 312 | notes = oldnotes; 313 | } 314 | 315 | table.deleteRow(1); 316 | mood_data.pop(); 317 | } 318 | 319 | addTableRow(table, dataT, dataX, dataY, notes); 320 | mood_data.push({ timestamp: new Date(dataT), valence: dataX, arousal: dataY, notes: notes }); 321 | 322 | truncateData(table); 323 | window.localStorage.setItem("mood_data", JSON.stringify(mood_data)); 324 | } 325 | 326 | function truncateData(table) { 327 | while (mood_data.length > config.maximum_data_points) { 328 | table.deleteRow(table.rows.length - 1); 329 | mood_data.shift(); 330 | } 331 | } 332 | 333 | function loadData(dataString, saveData) { 334 | if (dataString == null) { 335 | dataString = "[]"; 336 | } 337 | 338 | mood_data = JSON.parse(dataString); 339 | for (var i = 0; i < mood_data.length; i++) { 340 | mood_data[i].timestamp = new Date(mood_data[i].timestamp); 341 | } 342 | mood_data.sort(function (a, b) { 343 | return a.timestamp - b.timestamp; 344 | }); 345 | 346 | clearTable(table); 347 | for (var i = 0; i < mood_data.length; i++) { 348 | addTableRow(table, mood_data[i].timestamp, mood_data[i].valence, mood_data[i].arousal, mood_data[i].notes); 349 | } 350 | 351 | if (saveData) { 352 | truncateData(table); 353 | window.localStorage.setItem("mood_data", JSON.stringify(mood_data)); 354 | } 355 | } 356 | 357 | function clearData() { 358 | if (window.confirm("Do you really want to clear all recorded mood data?")) { 359 | mood_data = []; 360 | window.localStorage.removeItem("mood_data"); 361 | clearTable(table); 362 | ctx.drawImage(canvas.templateCanvas, 0, 0); 363 | } 364 | } 365 | 366 | function handleCanvasClick(event) { 367 | let dataT = new Date(); 368 | let dataX = (event.offsetX / canvas.offsetWidth); 369 | let dataY = (1 - (event.offsetY / canvas.offsetHeight)); 370 | 371 | addData(dataT, dataX, dataY, ""); 372 | 373 | if (config.data_hide_time > 0) { 374 | graphData(1); 375 | 376 | setTimeout(handleCanvasAfterClick, config.data_hide_time * 1000); 377 | } else { 378 | graphData(config.maximum_graphed_points); 379 | } 380 | 381 | } 382 | 383 | function handleCanvasAfterClick() { 384 | if (new Date() - mood_data[mood_data.length - 1].timestamp > ((config.data_hide_time * 1000) - 100)) { 385 | graphData(config.maximum_graphed_points); 386 | } 387 | } 388 | 389 | function handleDataUpdate(event) { 390 | if (event.key === "config") { 391 | loadConfig(event.newValue); 392 | loadTheme(); 393 | } 394 | if (event.key === "mood_data") { 395 | loadData(event.newValue, false); 396 | } 397 | graphData(config.maximum_graphed_points); 398 | } 399 | 400 | function handleFileDownload() { 401 | let blob = new Blob([JSON.stringify(mood_data)], { type: "text/json" }); 402 | 403 | download_link.download = "mood_data-" + Date.now() + ".json"; 404 | download_link.href = URL.createObjectURL(blob); 405 | download_link.click(); 406 | } 407 | 408 | function handleCSVFileDownload() { 409 | let csv = "Date & Time,Pleasantness,Energy,Notes\n"; 410 | 411 | for (var i = 0; i < mood_data.length; i++) { 412 | if (mood_data[i].notes != undefined) { 413 | var notes = JSON.stringify(mood_data[i].notes); 414 | } else { 415 | var notes = ""; 416 | }; 417 | 418 | csv += new Date(mood_data[i].timestamp).toDateString() + " " + new Date(mood_data[i].timestamp).toLocaleTimeString("en") + "," + Math.round(mood_data[i].valence * 1000) / 100 + "," + Math.round(mood_data[i].arousal * 1000) / 100 + "," + notes + "\n"; 419 | } 420 | 421 | let blob = new Blob([csv], { type: "text/csv" }); 422 | 423 | download_link.download = "mood_data-" + Date.now() + ".csv"; 424 | download_link.href = URL.createObjectURL(blob); 425 | download_link.click(); 426 | } 427 | 428 | function handleGraphDownload() { 429 | download_link.download = "mood_graph-" + Date.now() + ".png"; 430 | download_link.href = canvas.toDataURL('image/png'); 431 | download_link.click(); 432 | } 433 | 434 | function handleFileUpload() { 435 | const [file] = document.querySelector("input[type=file]").files; 436 | const reader = new FileReader(); 437 | 438 | reader.addEventListener( 439 | "load", 440 | () => { 441 | loadData(reader.result, true); 442 | graphData(config.maximum_graphed_points); 443 | }, 444 | false 445 | ); 446 | 447 | if (file) { 448 | reader.readAsText(file); 449 | } 450 | } 451 | 452 | function loadTheme() { 453 | switch (config.theme) { 454 | case "1": 455 | renderTemplate1(offscreen_ctx); 456 | break; 457 | case "2": 458 | renderTemplate2(offscreen_ctx); 459 | break; 460 | default: 461 | renderTemplate0(offscreen_ctx); 462 | } 463 | } 464 | 465 | function loadConfig(configString) { 466 | if (configString == null) { 467 | configString = JSON.stringify(default_config); 468 | } 469 | 470 | config = JSON.parse(configString); 471 | 472 | if (config.moods_open) { 473 | mood_menu.setAttribute("open", ""); 474 | } else { 475 | mood_menu.removeAttribute("open"); 476 | } 477 | if (config.settings_open) { 478 | settings_menu.setAttribute("open", ""); 479 | } else { 480 | settings_menu.removeAttribute("open"); 481 | } 482 | theme_input.value = config.theme; 483 | data_limit_input.value = config.maximum_data_points; 484 | data_graph_limit_input.value = config.maximum_graphed_points; 485 | data_combine_input.value = config.minimum_minutes; 486 | data_hide_input.value = config.data_hide_time; 487 | } 488 | 489 | function writeConfig() { 490 | config.moods_open = mood_menu.hasAttribute("open"); 491 | config.settings_open = settings_menu.hasAttribute("open"); 492 | config.theme = theme_input.value; 493 | config.maximum_data_points = data_limit_input.value; 494 | config.maximum_graphed_points = data_graph_limit_input.value; 495 | config.minimum_minutes = data_combine_input.value; 496 | config.data_hide_time = data_hide_input.value; 497 | 498 | window.localStorage.setItem("config", JSON.stringify(config)); 499 | } 500 | 501 | function resetConfig() { 502 | if (window.confirm("Do you really want to reset settings to defaults?")) { 503 | loadConfig(null); 504 | settings_menu.setAttribute("open", ""); 505 | writeConfig(); 506 | } 507 | } 508 | 509 | function graphData(items) { 510 | ctx.drawImage(canvas.templateCanvas, 0, 0); 511 | 512 | let dotSize = Math.min(canvas.width, canvas.height) * 0.06; 513 | let start = Math.max(mood_data.length - items, 0); 514 | 515 | ctx.lineWidth = Math.min(canvas.width, canvas.height) * 0.006; 516 | 517 | for (var i = start; i < mood_data.length; i++) { 518 | let value = (mood_data[i].timestamp - mood_data[start].timestamp) / (mood_data[mood_data.length - 1].timestamp - mood_data[start].timestamp); 519 | 520 | value2 = (i - start) / ((mood_data.length - 1) - start); 521 | 522 | if (isNaN(value)) { 523 | value = 1; 524 | } 525 | 526 | if (isNaN(value2)) { 527 | value2 = 1; 528 | } 529 | 530 | ctx.fillStyle = "hsl(" + 300 + " " + ((value * 40) + 60) + "% " + ((value2 * 20) + (value * 20)) + "% / 0.6)"; 531 | ctx.strokeStyle = "hsl(" + 300 + " " + ((value * 40) + 60) + "% " + ((value2 * 20) + (value * 20)) + "% / 0.5)"; 532 | 533 | if ((i == mood_data.length - 1) && ((new Date() - mood_data[i].timestamp < 60 * 1000 * config.minimum_minutes) || (config.minimum_minutes == 0))) { 534 | ctx.fillStyle = "hsl(300 100% 50% / 0.9)"; 535 | ctx.strokeStyle = "hsl(300 100% 50% / 0.5)"; 536 | } 537 | 538 | ctx.beginPath(); 539 | ctx.moveTo(mood_data[Math.max(i - 1, start)].valence * canvas.width, (1 - mood_data[Math.max(i - 1, start)].arousal) * canvas.height); 540 | ctx.lineTo(mood_data[i].valence * canvas.width, (1 - mood_data[i].arousal) * canvas.height); 541 | ctx.stroke(); 542 | 543 | ctx.fillRect((mood_data[i].valence * canvas.width) - (dotSize / 2), ((1 - mood_data[i].arousal) * canvas.height) - (dotSize / 2), dotSize, dotSize); 544 | } 545 | } 546 | 547 | function heatmapData() { 548 | ctx.drawImage(canvas.templateCanvas, 0, 0); 549 | let dotSize = Math.min(canvas.width, canvas.height) * 0.2; 550 | 551 | ctx.fillStyle = "hsl(300 100% 19% / " + Math.min(Math.max(0.2 / Math.log10(mood_data.length), 0.01), 0.75) + ")"; 552 | 553 | for (var i = 0; i < mood_data.length; i++) { 554 | ctx.fillRect((mood_data[i].valence * canvas.width) - (dotSize / 2), ((1 - mood_data[i].arousal) * canvas.height) - (dotSize / 2), dotSize, dotSize); 555 | } 556 | 557 | } 558 | 559 | function handleForceUpdate() { 560 | if (navigator.onLine || window.confirm("You appear to be offline, are you sure you want to clear the cache?")) { 561 | navigator.serviceWorker.getRegistrations().then(function (registrations) { 562 | for (let registration of registrations) { 563 | registration.unregister(); 564 | } 565 | location.reload(); 566 | }); 567 | } 568 | } 569 | 570 | loadConfig(window.localStorage.getItem("config")); 571 | 572 | loadData(window.localStorage.getItem("mood_data"), false); 573 | window.addEventListener('storage', handleDataUpdate); 574 | 575 | loadTheme(); 576 | graphData(config.maximum_graphed_points); 577 | canvas.addEventListener("click", handleCanvasClick); 578 | 579 | mood_menu.addEventListener("toggle", (event) => { 580 | writeConfig(); 581 | }); 582 | 583 | settings_menu.addEventListener("toggle", (event) => { 584 | writeConfig(); 585 | }); 586 | 587 | theme_input.onchange = function () { 588 | writeConfig(); 589 | loadTheme(); 590 | graphData(config.maximum_graphed_points); 591 | }; 592 | 593 | data_limit_input.onchange = function () { 594 | if (mood_data.length - 10 > data_limit_input.value) { 595 | if (!(window.confirm("Do you really want to remove " + (mood_data.length - data_limit_input.value) + " data points?"))) { 596 | data_limit_input.value = config.maximum_data_points; 597 | return; 598 | } 599 | } 600 | 601 | writeConfig(); 602 | truncateData(table); 603 | graphData(config.maximum_graphed_points); 604 | window.localStorage.setItem("mood_data", JSON.stringify(mood_data)); 605 | }; 606 | 607 | data_graph_limit_input.onchange = function () { 608 | writeConfig(); 609 | graphData(config.maximum_graphed_points); 610 | }; 611 | 612 | data_combine_input.onchange = function () { 613 | writeConfig(); 614 | graphData(config.maximum_graphed_points); 615 | }; 616 | 617 | data_hide_input.onchange = function () { 618 | writeConfig(); 619 | graphData(config.maximum_graphed_points); 620 | }; 621 | 622 | if (location.hostname === "localhost" || location.hostname === "127.0.0.1") { 623 | document.getElementById("cache_button").style.display = "none"; 624 | } else { 625 | navigator.serviceWorker.register("worker.js"); 626 | document.getElementById("cache_button").innerText = "Clear cache (v" + new Date(document.lastModified).toISOString().split(".")[0] + ")"; 627 | } 628 | --------------------------------------------------------------------------------