A browser source overlay for winter vibes for your Live Streams or Videos
1import "./style.css";
2import Config from "./config";
3import { createSnowflake, drawSnowflake, moveSnowflake } from "./snowflake";
4import {
5 accumulateSnow,
6 snowAccumulatorCollisionY,
7 createSnowAccumulator,
8 drawSnowAccumulators,
9 resetSnowAccumulator,
10 snowAccumulatorCollisionX,
11} from "./snow_accumulator";
12import { createPlow, drawPlow, movePlow, plowDone } from "./plow";
13
14/**
15 * @type{Array.<Snowflake>}
16 */
17const snowFlakes = [];
18
19/**
20 * @type SnowAccumulator[]
21 */
22const floor = [];
23
24const sectionSize = Config.canvas.width / Config.groundAccumulator.slices;
25/**
26 * @type {HTMLCanvasElement}
27 */
28let canvas;
29
30/**
31 * @type {CanvasRenderingContext2D}
32 */
33let ctx;
34
35/**
36 * @type {Plow}
37 */
38let plow;
39
40let lastSpawn = 0;
41let lastFrame = 0;
42
43let isPlowing = false;
44
45window.onload = init;
46
47function init() {
48 // @ts-ignore -- I dunno how to get ts / jsdoc to be ok with this
49 canvas = document.getElementById("main");
50
51 ctx = canvas.getContext("2d");
52 canvas.width = Config.canvas.width;
53 canvas.height = Config.canvas.height;
54 ctx.imageSmoothingEnabled = false;
55
56 for (let i = 0; i < Config.groundAccumulator.slices; i++) {
57 floor.push(
58 createSnowAccumulator(i * sectionSize, Config.canvas.height, sectionSize),
59 );
60 }
61
62 window.requestAnimationFrame(gameLoop);
63}
64
65/**
66 * @param time {DOMHighResTimeStamp}
67 */
68function gameLoop(time) {
69 const delta = time - lastFrame;
70
71 ctx.clearRect(0, 0, Config.canvas.width, Config.canvas.height);
72 if (time - lastSpawn > 500 && snowFlakes.length < Config.snow.max) {
73 for (let i = 0; i < Math.floor(Math.random() * 2); i++) {
74 snowFlakes.push(createSnowflake());
75 }
76 }
77
78 snowFlakes.forEach((flake, idx) => {
79 moveSnowflake(flake, delta);
80 const floorIdx = Math.floor(flake.x / sectionSize);
81
82 if (snowAccumulatorCollisionY(floor[floorIdx], flake)) {
83 if (!isPlowing) {
84 accumulateSnow(floor[floorIdx], flake);
85 }
86 snowFlakes[idx] = createSnowflake();
87 }
88 drawSnowflake(flake, ctx);
89 });
90
91 const avgHeight =
92 floor.reduce((acc, sa) => acc + sa.height, 0) / floor.length;
93
94 if (avgHeight > Config.groundAccumulator.max) {
95 isPlowing = true;
96 }
97
98 drawSnowAccumulators(ctx, floor, plow);
99
100 if (isPlowing) {
101 plow ??= createPlow();
102 movePlow(plow, delta);
103 drawPlow(plow, ctx);
104 if (plowDone(plow)) {
105 isPlowing = false;
106 plow = null;
107 floor.forEach((sa) => resetSnowAccumulator(sa));
108 }
109 }
110
111 lastFrame = time;
112 window.requestAnimationFrame(gameLoop);
113}