-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathtimer-logic.js
More file actions
124 lines (113 loc) · 4.25 KB
/
Copy pathtimer-logic.js
File metadata and controls
124 lines (113 loc) · 4.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
// Pure functions for the pomodoro state machine and weight system.
// Kept dependency-free so they run in the service worker, content scripts,
// the popup, and the node test runner.
(function (root) {
const DEFAULT_WORK_MINUTES = 25;
const BREAK_MINUTES = 5;
const MIN_WORK_MINUTES = 1;
const MAX_WORK_MINUTES = 120;
const WARNING_SECONDS = 30;
// Weight stages as a function of continuous work hours.
// A "stage" determines how much of the screen the cat occupies.
// Stage maps to a viewport-relative size factor (0.20 = 20% of min(vw,vh)).
function weightStageForHours(hours) {
if (hours < 1) return { stage: 0, sizeFactor: 0.22, label: "smug" };
if (hours < 2) return { stage: 1, sizeFactor: 0.32, label: "chunky" };
if (hours < 3) return { stage: 2, sizeFactor: 0.45, label: "rotund" };
if (hours < 5) return { stage: 3, sizeFactor: 0.60, label: "obscene" };
return { stage: 4, sizeFactor: 0.85, label: "eclipse" };
}
// Stretch factor during the break window. At 5:00 the cat is compact;
// it stretches out as the break ticks down to 4:47 (the "getting comfortable" arc).
// After 4:47 it stays stretched for the rest of the break.
function stretchFactor(secondsRemaining, breakSeconds) {
const totalStretchWindow = 13; // 5:00 -> 4:47
const elapsed = breakSeconds - secondsRemaining;
if (elapsed <= 0) return 1.0;
if (elapsed >= totalStretchWindow) return 1.6;
return 1.0 + (elapsed / totalStretchWindow) * 0.6;
}
function clampWorkMinutes(n) {
const v = Math.floor(Number(n));
if (!Number.isFinite(v)) return DEFAULT_WORK_MINUTES;
return Math.min(MAX_WORK_MINUTES, Math.max(MIN_WORK_MINUTES, v));
}
function formatMMSS(seconds) {
const s = Math.max(0, Math.floor(seconds));
const m = Math.floor(s / 60);
const r = s % 60;
return `${m}:${r.toString().padStart(2, "0")}`;
}
// Compute remaining seconds in the current phase given a timestamp.
// state: { phase: 'work'|'break'|'idle', phaseEndsAt: epochMs }
function remainingSeconds(state, nowMs) {
if (!state || state.phase === "idle") return 0;
return Math.max(0, Math.ceil((state.phaseEndsAt - nowMs) / 1000));
}
// True when a non-blocking warning should appear: in work phase, in the
// final WARNING_SECONDS, before the cat sits on the user.
function shouldWarn(state, nowMs) {
if (!state || state.phase !== "work") return false;
const r = remainingSeconds(state, nowMs);
return r > 0 && r <= WARNING_SECONDS;
}
// Continuous work hours used to calculate weight.
// workStreakStartedAt resets only when a break is actually completed.
function continuousWorkHours(workStreakStartedAt, nowMs) {
if (workStreakStartedAt === null || workStreakStartedAt === undefined) return 0;
return Math.max(0, (nowMs - workStreakStartedAt) / (1000 * 60 * 60));
}
// Determine the next state when the current phase elapses.
function advancePhase(state, settings, nowMs) {
const workMs = clampWorkMinutes(settings.workMinutes) * 60 * 1000;
const breakMs = BREAK_MINUTES * 60 * 1000;
if (state.phase === "work") {
const streak =
state.workStreakStartedAt === null || state.workStreakStartedAt === undefined
? nowMs
: state.workStreakStartedAt;
return {
phase: "break",
phaseEndsAt: nowMs + breakMs,
workStreakStartedAt: streak,
breakStartedAt: nowMs,
};
}
if (state.phase === "break") {
// Break completed -> reset streak, start a fresh work cycle.
return {
phase: "work",
phaseEndsAt: nowMs + workMs,
workStreakStartedAt: nowMs,
breakStartedAt: null,
};
}
// idle -> work
return {
phase: "work",
phaseEndsAt: nowMs + workMs,
workStreakStartedAt: nowMs,
breakStartedAt: null,
};
}
const api = {
DEFAULT_WORK_MINUTES,
BREAK_MINUTES,
MIN_WORK_MINUTES,
MAX_WORK_MINUTES,
WARNING_SECONDS,
weightStageForHours,
stretchFactor,
clampWorkMinutes,
formatMMSS,
remainingSeconds,
shouldWarn,
continuousWorkHours,
advancePhase,
};
if (typeof module !== "undefined" && module.exports) {
module.exports = api;
} else {
root.CatTimer = api;
}
})(typeof self !== "undefined" ? self : this);