Skip to content

Commit 14e72a9

Browse files
authored
add screensaver (#17)
1 parent 9f412c0 commit 14e72a9

File tree

7 files changed

+166
-23
lines changed

7 files changed

+166
-23
lines changed

client/idler.ts

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
1+
import { ScreensaverUI } from "./uis/screensaver";
2+
13
export class Idler {
24
private _idle: boolean = false;
35
private _lastActivity: number = Date.now();
46
private _idleAfter: number = 300000; // 5 minutes
5-
private _takeActionAfter: number = 2520000; // 42 minutes
67
private _isLockScreen: boolean;
8+
private _screensaverUI: ScreensaverUI;
79

810
public constructor(isLockScreen: boolean = false) {
911
this._isLockScreen = isLockScreen;
12+
this._screensaverUI = new ScreensaverUI(isLockScreen);
1013

1114
// Listen for keyboard and mouse events
12-
window.addEventListener("keydown", this._unidle.bind(this));
13-
window.addEventListener("mousemove", this._unidle.bind(this));
14-
window.addEventListener("mousedown", this._unidle.bind(this));
15+
window.addEventListener("keydown", this._stopIdling.bind(this));
16+
window.addEventListener("mousemove", this._stopIdling.bind(this));
17+
window.addEventListener("mousedown", this._stopIdling.bind(this));
1518

1619
// Check for idle at a regular interval
1720
setInterval(this._checkIdle.bind(this), 1000);
@@ -25,38 +28,41 @@ export class Idler {
2528
return this._idle;
2629
}
2730

28-
private _unidle(): void {
29-
this._lastActivity = Date.now();
30-
this._idle = false;
31-
}
32-
33-
private _action(): void {
34-
// TODO: start screensaver?
35-
// Warning: this function is called from a setInterval, so it should return if the action is already running
31+
/**
32+
* Start idling and show the screensaver.
33+
*/
34+
private _startIdling(): void {
35+
this._idle = true;
36+
this._screensaverUI.start();
3637
}
3738

38-
private _checkIfActionNeeded(): boolean {
39+
/**
40+
* Stop the screensaver and reset the idle timer.
41+
* @param ev The event that triggered this function.
42+
*/
43+
private _stopIdling(ev: Event | null = null): void {
44+
this._lastActivity = Date.now();
3945
if (this._idle) {
40-
// Check if we should take action
41-
if (Date.now() - this._lastActivity >= this._takeActionAfter) {
42-
this._action();
43-
return true;
46+
if (ev) {
47+
ev.preventDefault(); // Prevent the event from bubbling up and causing unwanted UI interactions
4448
}
49+
this._screensaverUI.stop();
50+
this._idle = false;
4551
}
46-
return false;
4752
}
4853

54+
/**
55+
* Check if the computer is idling (e.g. no keyboard or mouse interactions for a certain amount of time).
56+
* @returns True if the computer is idling, false otherwise.
57+
*/
4958
private _checkIdle(): boolean {
5059
if (this._idle) {
51-
this._checkIfActionNeeded();
5260
return true;
5361
}
5462

5563
// Check if we're idle
5664
if (Date.now() - this._lastActivity >= this._idleAfter) {
57-
this._idle = true;
58-
console.log("Now idling...");
59-
this._checkIfActionNeeded(); // Needed if idleAfter and takeActionAfter share the same value
65+
this._startIdling();
6066
return true;
6167
}
6268
return false;

client/uis/screensaver.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { BlankScreensaver } from "./screensavers/blank";
2+
3+
export class ScreensaverUI {
4+
private _canvasWrapper: HTMLElement;
5+
private _canvas: HTMLCanvasElement;
6+
private _isRunning: boolean = false;
7+
private _screensaver = new BlankScreensaver();
8+
private _isLockScreen: boolean;
9+
10+
public constructor(isLockScreen: boolean = false) {
11+
this._isLockScreen = isLockScreen;
12+
this._canvasWrapper = document.getElementById('screensaver-wrapper') as HTMLElement;
13+
this._canvas = document.getElementById('screensaver-canvas') as HTMLCanvasElement;
14+
}
15+
16+
/**
17+
* Draw 1 frame of the currently selected screensaver.
18+
*/
19+
private _draw(): void {
20+
if (!this._isRunning) {
21+
return;
22+
}
23+
24+
const ctx = this._canvas.getContext('2d');
25+
if (!ctx) {
26+
return;
27+
}
28+
29+
this._screensaver.draw(ctx, this._canvas.width, this._canvas.height);
30+
31+
if (this._isRunning && this._screensaver.getName() !== 'Blank') {
32+
requestAnimationFrame(this._draw.bind(this));
33+
}
34+
};
35+
36+
/**
37+
* Start running the screensaver.
38+
*/
39+
public start(): void {
40+
this._isRunning = true;
41+
this._canvasWrapper.classList.add('active');
42+
this._canvas.width = window.innerWidth;
43+
this._canvas.height = window.innerHeight;
44+
this._draw();
45+
};
46+
47+
/**
48+
* Stop running the screensaver.
49+
*/
50+
public stop(): void {
51+
this._isRunning = false;
52+
this._canvasWrapper.classList.remove('active');
53+
};
54+
}

client/uis/screensavers/base.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export abstract class ScreensaverBase {
2+
/**
3+
* Get the name of the screensaver (should return a static string).
4+
*/
5+
public abstract getName(): string;
6+
7+
/**
8+
* Draw 1 frame of the screensaver. Warning: do not request the next animation frame here, it will be done automatically.
9+
* @param ctx The rendering context of the canvas elemnt, in 2D mode.
10+
* @param canvasWidth The width of the canvas in pixels.
11+
* @param canvasHeight The height of the canvas in pixels.
12+
*/
13+
public abstract draw(ctx: CanvasRenderingContext2D, canvasWidth: number, canvasHeight: number): void;
14+
}

client/uis/screensavers/blank.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { ScreensaverBase } from "./base";
2+
3+
export class BlankScreensaver extends ScreensaverBase {
4+
public getName(): string {
5+
return "Blank";
6+
}
7+
8+
public draw(ctx: CanvasRenderingContext2D, canvasWidth: number, canvasHeight: number): void {
9+
// Draw a black rectangle that covers the entire canvas
10+
ctx.fillStyle = 'black';
11+
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
12+
}
13+
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"build": "tsc",
88
"bundle": "webpack --mode=production",
99
"bundle-dev": "webpack --mode=development",
10-
"test": "echo \"Error: no test specified\" && exit 1"
10+
"test": "open dist/index.html"
1111
},
1212
"repository": {
1313
"type": "git",

static/index.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ <h3>This computer is reserved for an exam between <span id="exam-mode-start">...
9494
</div>
9595
</footer>
9696

97+
<!-- Screensaver -->
98+
<div id="screensaver-wrapper">
99+
<canvas id="screensaver-canvas" width="1920" height="1080"></canvas>
100+
</div>
101+
97102
<!-- Scripts (paths are relative to the dist folder) -->
98103
<script src="bundle.js"></script>
99104
</body>

static/styles.css

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,3 +610,54 @@ dialog.calendar-event-dialog .dialog-close-button {
610610
dialog.calendar-event-dialog .dialog-close-button:hover {
611611
color: var(--color-text-primary);
612612
}
613+
614+
/* ***************************************** */
615+
/* Screensaver */
616+
/* ***************************************** */
617+
618+
#screensaver-wrapper {
619+
display: block;
620+
position: fixed;
621+
top: 0;
622+
left: 0;
623+
width: 100%;
624+
height: 100%;
625+
background-color: var(--color-bg);
626+
z-index: 999;
627+
opacity: 0;
628+
pointer-events: none;
629+
/* play fade-out animation once */
630+
animation: fade-out 0.15s ease-in-out 0s 1 normal;
631+
}
632+
633+
#screensaver-wrapper.active {
634+
opacity: 1;
635+
pointer-events: all; /* take over the screen */
636+
/* play fade-in animation once */
637+
animation: fade-in 0.5s ease-in-out 0s 1 normal;
638+
cursor: none !important; /* hide cursor */
639+
}
640+
641+
@keyframes fade-in {
642+
0% {
643+
opacity: 0;
644+
}
645+
100% {
646+
opacity: 1;
647+
}
648+
}
649+
650+
@keyframes fade-out {
651+
0% {
652+
opacity: 1;
653+
}
654+
100% {
655+
opacity: 0;
656+
}
657+
}
658+
659+
#screensaver-canvas {
660+
width: 100%;
661+
height: 100%;
662+
object-fit: cover; /* in case the screen isn't perfectly 16:9 */
663+
}

0 commit comments

Comments
 (0)