commit 3979a0f68efbd6b90761955f9300a96f29ae86cc Author: Interitio Date: Fri Mar 13 23:27:09 2026 +1000 Initial timer commit diff --git a/assets/mute.png b/assets/mute.png new file mode 100644 index 0000000..a27aa73 Binary files /dev/null and b/assets/mute.png differ diff --git a/assets/unmute.png b/assets/unmute.png new file mode 100644 index 0000000..dc201a9 Binary files /dev/null and b/assets/unmute.png differ diff --git a/audio/break_alert.wav b/audio/break_alert.wav new file mode 100644 index 0000000..66a4fcc Binary files /dev/null and b/audio/break_alert.wav differ diff --git a/audio/focus_alert.wav b/audio/focus_alert.wav new file mode 100644 index 0000000..66a4fcc Binary files /dev/null and b/audio/focus_alert.wav differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..a7ad952 --- /dev/null +++ b/index.html @@ -0,0 +1,26 @@ + + + + + + + + Croccy Timer + + +
+
+
+ 00:00 +
+
+ + BREAK + + + 10/12 + +
+
+ + diff --git a/timer.css b/timer.css new file mode 100644 index 0000000..118794d --- /dev/null +++ b/timer.css @@ -0,0 +1,85 @@ +:root { + --break-color: #8CD5CA; + --focus-color: #EED59F; + --stage-color: var(--break-color); + --clip-path: none; +} + + +.timer-box{ + height: 175px; + width: 397px; + padding: 5px; +} + +.timer-bg { + height: 100%; + width: 100%; + height: 175px; + width: 397px; + position: absolute; + background-color: #1E202F; + border-radius: 40px; + outline-offset: -5px; + outline: dotted 10px #534F72; +} +.timer-bg:before { + content: ""; + position: absolute; + height: 175px; + width: 397px; + border-radius: 40px; + outline-offset: -5px; + outline: dotted 10px var(--stage-color); + border-radius: 40px; + clip-path: var(--clip-path); +} + + +.timer-time { + font-size: 100px; + font-family: Inter; + font-weight: 400; + letter-spacing: 0%; + text-align: center; + color: var(--break-color); +} + +.timer-details { + display: flex; + justify-content: space-between; + align-items: center; + text-align: center; + width: 65%; + margin: auto; + margin-top: -2%; + color: #FFFFFF; +} + +.timer-details-stage img { + height: 30px; + margin-right: 5%; +} + +.timer-details-stage { + display: flex; + justify-content: center; + align-items: center; + text-align: center; +} + +.timer-details-stage-text { + font-size: 30px; + font-family: Inter; + font-weight: 400; + letter-spacing: 6px; + text-align: center; +} + +.timer-details-block { + font-size: 30px; + font-family: Inter; + font-weight: 400; + letter-spacing: 0%; + text-align: center; +} diff --git a/timer.js b/timer.js new file mode 100644 index 0000000..f4eec4b --- /dev/null +++ b/timer.js @@ -0,0 +1,278 @@ +window.addEventListener("DOMContentLoaded", () => { + const websocket = new WebSocket("ws://adamwalsh.name:9500"); + + websocket.addEventListener("open", () => { + communicate(websocket); + }); + +}); + + +class Timer { + constructor(renderer, start_at, focus_length, break_length, block_goal) { + this.renderer = renderer; + this.start_at = start_at; + this.focus_length = focus_length; + this.break_length = break_length; + this.block_goal = block_goal; + + this.counter = this.count_now; + this.break_notified = true; + + this.destroying = false; + } + + get block_length() { + // Return total block duration in seconds + return this.focus_length + this.break_length; + } + + get block_start() { + // Get the Date when this block started. + return new Date(this.start_at.getTime() + (this.counter * this.block_length) * 1000); + } + + get count_now() { + // Current actual block number. 0 for the first block, -1 if not running + var diff = Math.floor((new Date() - this.start_at) / 1000); + if (diff < 0) { + return -1; + } else { + var stage = Math.floor(diff / this.block_length); + return stage; + } + } + + get is_break() { + // Whether the current block is on break. + if (this.counter < 0) { + return true + } else { + return ((new Date() - this.block_start) / 1000) > this.focus_length; + } + } + + render_time() { + // Render timer status to the document + let timestr; + let proportion; + + if (this.counter < 0) { + timestr = "--:--"; + proportion = 0; + } else { + // How many seconds have passed in this block + var dur_seconds = Math.floor((new Date() - this.block_start) / 1000); + if (dur_seconds < this.focus_length) { + // How many seconds are left in the focus session + dur_seconds = this.focus_length - dur_seconds; + // What proportion of the focus session remains + proportion = dur_seconds / this.focus_length; + } else { + dur_seconds = this.block_length - dur_seconds; + proportion = dur_seconds / (this.break_length); + } + var seconds = dur_seconds % 60; + dur_seconds = dur_seconds - seconds; + var minutes = Math.floor(dur_seconds / 60); + var hours = Math.floor(minutes / 60); + minutes = minutes - 60 * hours; + + timestr = String(hours).padStart(2, '0') + ':' + String(minutes).padStart(2, '0'); + } + + this.renderer.update_time(timestr); + this.renderer.update_proportion(proportion); + this.renderer.update_block(String(this.counter + 1) + '/' + String(this.block_goal)); + if (this.is_break) { + this.renderer.update_stage("BREAK"); + this.renderer.set_break_color(); + } else { + this.renderer.update_stage("FOCUS"); + this.renderer.set_focus_color(); + } + } + + tick() { + // Recursive call run per second to update and change stage + + if (this.destroying) { + return + } + + if (this.is_break && !this.break_notified && this.counter >= 0) { + // Notify for break + this.renderer.break_alert.play(); + console.log("Notifying Break.") + this.break_notified = true; + } else if (this.count_now > this.counter) { + this.counter++; + this.break_notified = false; + // Notify for focus + this.renderer.focus_alert.play(); + console.log("Notifying Focus.") + } + this.render_time(); + setTimeout(this.tick.bind(this), 1000); + } +} + +class TimerRenderer { + constructor() { + this.background = document.getElementsByClassName("timer-bg")[0]; + this.time_element = document.getElementsByClassName("timer-time")[0]; + this.stage_element = document.getElementsByClassName("timer-details-stage-text")[0]; + this.block_element = document.getElementsByClassName("timer-details-block")[0]; + + this.break_alert = new Audio('audio/break_alert.wav'); + this.focus_alert = new Audio('audio/focus_alert.wav'); + + this.bg_width = parseInt( + window.getComputedStyle( + this.background, null + ).getPropertyValue('width'), + 10 + ); + this.bg_height = parseInt( + window.getComputedStyle( + this.background, null + ).getPropertyValue('height'), + 10 + ) + } + + update_time(timestr) { + this.time_element.textContent = timestr; + } + + update_proportion(proportion) { + var bounds = rectBounds( + proportion, this.bg_width, this.bg_height + ); + this.background.style.setProperty( + '--clip-path', + "polygon(" + bounds.join(', ') + ")" + ); + } + + update_block(blockstr) { + this.block_element.textContent = blockstr; + } + + update_stage(stagestr) { + this.stage_element.textContent = stagestr; + } + + set_break_color() { + this.background.style.setProperty('--stage-color', 'var(--break-color)'); + this.time_element.style.color = "var(--break-color)"; + } + + set_focus_color() { + this.background.style.setProperty('--stage-color', 'var(--focus-color)'); + this.time_element.style.color = "var(--focus-color)"; + } + +} + +function rectBounds(prop, rectHeight, rectWidth) { + var totalCircum = 2 * rectHeight + 2 * rectWidth; + var propCircum = prop * totalCircum; + let bounds; + + if (propCircum < rectWidth / 2) { + var xprop = Math.floor(( + (rectWidth / 2 + propCircum) / rectWidth + ) * 1000) / 10; + bounds = [ + "50% -5%", + `${xprop}% -5%`, + `${xprop}% 4%`, + "50% 4%" + ]; + } else if (propCircum < rectWidth / 2 + rectHeight) { + var yprop = ( + (propCircum - rectWidth / 2) / rectHeight + ) * 100; + bounds = [ + "50% -5%", "110% -5%", + `110% ${yprop}%`, `96% ${yprop}%`, + "95% 4%", "50% 4%" + ]; + } else if (propCircum < 3 * rectWidth / 2 + rectHeight) { + var xprop = ( + ((3 * rectWidth / 2 + rectHeight) - propCircum) / rectWidth + ) * 100; + bounds = [ + "50% -5%", "110% -5%", + "110% 110%", + `${xprop}% 110%`, `${xprop}% 96%`, + "95% 96%", + "95% 4%", "50% 4%" + ]; + } else if (propCircum < 3 * rectWidth / 2 + 2 * rectHeight) { + var yprop = ( + ((3 * rectWidth / 2 + 2 * rectHeight) - propCircum) / rectHeight + ) * 100; + bounds = [ + "50% -5%", "110% -5%", + "110% 110%", "-5% 110%", + `-5% ${yprop}%`, `4% ${yprop}%`, + "5% 96%", "95% 96%", + "95% 4%", "50% 4%" + ]; + } else { + var xprop = ( + (propCircum - (3 * rectWidth / 2 + 2 * rectHeight)) / rectWidth + ) * 100; + bounds = [ + "50% -5%", "110% -5%", + "110% 110%", "-5% 110%", + "-5% -5%", + `${xprop}% -5%`, `${xprop}% 4%`, + "4% 4%", + "5% 96%", "95% 96%", + "95% 4%", "50% 4%" + ]; + } + return bounds; +} + +function communicate(websocket) { + console.log("Communicating"); + websocket.send(JSON.stringify({ type: "init", channel: "Timer" })); + + var renderer = new TimerRenderer(); + let timer; + + websocket.addEventListener("message", ({ data }) => { + console.log("Rec Event " + data); + const event = JSON.parse(data); + switch (event.type) { + case "DO": + let args = event.args; + + // Call the specified method + switch (event.method) { + case "setTimer": + if (timer != null) { + timer.destroying = true; + } + timer = new Timer( + renderer, + new Date(args.start_at), + args.focus_length, + args.break_length, + args.block_goal + ) + timer.tick(); + break; + default: + throw new Error(`Unsupported method requested: ${event.method}.`) + } + break; + default: + throw new Error(`Unsupported event type: ${event.type}.`); + } + }); +}