Skip to content

Commit 9807b85

Browse files
authored
Add tick-tock virus (#18)
* Add tick-tock virus Grid of identical analog clocks ticking in lockstep. Each cycle the grid densifies — cells shrink, clocks multiply — until the screen is a near-solid mass, then snaps back to sparse and begins again. Canvas 2D for perf at peak density; degrades from rim+hand to single line to single pixel as cell size drops below thresholds. * Fix tick-tock rendering precision and stage timing - Round canvas backing-store dims so fractional DPRs don't clip edges - Snap center-dot coords to integers to keep 1px dots crisp - Advance stage by elapsed count so backgrounded tabs catch up
1 parent 4b759a5 commit 9807b85

4 files changed

Lines changed: 165 additions & 0 deletions

File tree

components/Playlist.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export default class Playlist {
1818
'void',
1919
'crane-game',
2020
'sky',
21+
'tick-tock',
2122
];
2223

2324
premixes: VirusMix[] = [

viruses/tick-tock/index.html

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta
6+
name="viewport"
7+
content="width=device-width, height=device-height, initial-scale = 1.0, maximum-scale = 1.0"
8+
/>
9+
<link rel="shortcut icon" href="/favicon.png" />
10+
<link
11+
rel="stylesheet"
12+
href="/css/reset.css"
13+
type="text/css"
14+
media="screen"
15+
/>
16+
</head>
17+
<body>
18+
<div id="container"></div>
19+
<script type="module" src="./tick-tock.ts"></script>
20+
</body>
21+
</html>

viruses/tick-tock/tick-tock.scss

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
@use '../../sass/animations.scss';
2+
3+
html,
4+
body {
5+
margin: 0;
6+
padding: 0;
7+
background: #000;
8+
width: 100%;
9+
height: 100%;
10+
overflow: hidden;
11+
}
12+
13+
#container {
14+
position: fixed;
15+
inset: 0;
16+
background: #000;
17+
18+
canvas {
19+
display: block;
20+
width: 100%;
21+
height: 100%;
22+
}
23+
}

viruses/tick-tock/tick-tock.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { isMobile } from '../../utils/misc';
2+
import './tick-tock.scss';
3+
4+
const DESKTOP_SIZES = [160, 112, 80, 56, 40, 28, 20, 14, 10, 7, 5, 3];
5+
const MOBILE_SIZES = [96, 72, 52, 38, 28, 20, 14, 10, 7, 5, 3];
6+
const STAGE_MS = 1000;
7+
const TICK_PERIOD_MS = 2000;
8+
9+
const sizes = isMobile() ? MOBILE_SIZES : DESKTOP_SIZES;
10+
11+
const container = document.getElementById('container')!;
12+
const canvas = document.createElement('canvas');
13+
container.appendChild(canvas);
14+
const ctx = canvas.getContext('2d')!;
15+
16+
let cssW = 0;
17+
let cssH = 0;
18+
let stageIdx = 0;
19+
let stageStart = performance.now();
20+
let rafHandle = 0;
21+
22+
function resize() {
23+
const dpr = window.devicePixelRatio || 1;
24+
cssW = container.clientWidth;
25+
cssH = container.clientHeight;
26+
canvas.style.width = `${String(cssW)}px`;
27+
canvas.style.height = `${String(cssH)}px`;
28+
canvas.width = Math.round(cssW * dpr);
29+
canvas.height = Math.round(cssH * dpr);
30+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
31+
}
32+
33+
function drawClock(cx: number, cy: number, cell: number, angle: number) {
34+
const radius = cell * 0.42;
35+
36+
if (cell <= 4) {
37+
ctx.fillRect(Math.floor(cx), Math.floor(cy), 1, 1);
38+
return;
39+
}
40+
41+
if (cell < 8) {
42+
ctx.lineWidth = 1;
43+
ctx.beginPath();
44+
ctx.moveTo(cx, cy);
45+
ctx.lineTo(cx + Math.cos(angle) * radius, cy + Math.sin(angle) * radius);
46+
ctx.stroke();
47+
return;
48+
}
49+
50+
if (cell < 14) {
51+
ctx.lineWidth = 1;
52+
ctx.beginPath();
53+
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
54+
ctx.stroke();
55+
ctx.beginPath();
56+
ctx.moveTo(cx, cy);
57+
ctx.lineTo(
58+
cx + Math.cos(angle) * radius * 0.85,
59+
cy + Math.sin(angle) * radius * 0.85
60+
);
61+
ctx.stroke();
62+
return;
63+
}
64+
65+
ctx.lineWidth = Math.max(1, cell * 0.05);
66+
ctx.beginPath();
67+
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
68+
ctx.stroke();
69+
70+
ctx.lineWidth = Math.max(1, cell * 0.07);
71+
ctx.beginPath();
72+
ctx.moveTo(cx, cy);
73+
ctx.lineTo(
74+
cx + Math.cos(angle) * radius * 0.85,
75+
cy + Math.sin(angle) * radius * 0.85
76+
);
77+
ctx.stroke();
78+
79+
const dot = cell >= 40 ? 2 : 1;
80+
const dotX = Math.round(cx - dot / 2);
81+
const dotY = Math.round(cy - dot / 2);
82+
ctx.fillRect(dotX, dotY, dot, dot);
83+
}
84+
85+
function frame(now: number) {
86+
const elapsedStages = Math.floor((now - stageStart) / STAGE_MS);
87+
if (elapsedStages > 0) {
88+
stageIdx = (stageIdx + elapsedStages) % sizes.length;
89+
stageStart += elapsedStages * STAGE_MS;
90+
}
91+
92+
const cell = sizes[stageIdx];
93+
const cols = Math.ceil(cssW / cell);
94+
const rows = Math.ceil(cssH / cell);
95+
const angle =
96+
((now % TICK_PERIOD_MS) / TICK_PERIOD_MS) * Math.PI * 2 - Math.PI / 2;
97+
98+
ctx.fillStyle = '#000';
99+
ctx.fillRect(0, 0, cssW, cssH);
100+
ctx.strokeStyle = '#fff';
101+
ctx.fillStyle = '#fff';
102+
103+
const half = cell / 2;
104+
for (let r = 0; r < rows; r++) {
105+
const cy = r * cell + half;
106+
for (let c = 0; c < cols; c++) {
107+
drawClock(c * cell + half, cy, cell, angle);
108+
}
109+
}
110+
111+
rafHandle = requestAnimationFrame(frame);
112+
}
113+
114+
window.addEventListener('resize', resize);
115+
window.addEventListener('pagehide', () => {
116+
cancelAnimationFrame(rafHandle);
117+
});
118+
119+
resize();
120+
rafHandle = requestAnimationFrame(frame);

0 commit comments

Comments
 (0)