A browser source overlay for winter vibes for your Live Streams or Videos

added raw aseprite file for snowplow and snowplow animations

assets/snowplow.aseprite

This is a binary file and will not be displayed.

+65
assets/snowplow.json
···
+
{ "frames": [
+
{
+
"filename": "snowplow 0.aseprite",
+
"frame": { "x": 0, "y": 0, "w": 44, "h": 28 },
+
"rotated": false,
+
"trimmed": false,
+
"spriteSourceSize": { "x": 0, "y": 0, "w": 44, "h": 28 },
+
"sourceSize": { "w": 44, "h": 28 },
+
"duration": 100
+
},
+
{
+
"filename": "snowplow 1.aseprite",
+
"frame": { "x": 45, "y": 0, "w": 44, "h": 28 },
+
"rotated": false,
+
"trimmed": false,
+
"spriteSourceSize": { "x": 0, "y": 0, "w": 44, "h": 28 },
+
"sourceSize": { "w": 44, "h": 28 },
+
"duration": 100
+
},
+
{
+
"filename": "snowplow 2.aseprite",
+
"frame": { "x": 90, "y": 0, "w": 44, "h": 28 },
+
"rotated": false,
+
"trimmed": false,
+
"spriteSourceSize": { "x": 0, "y": 0, "w": 44, "h": 28 },
+
"sourceSize": { "w": 44, "h": 28 },
+
"duration": 100
+
},
+
{
+
"filename": "snowplow 3.aseprite",
+
"frame": { "x": 0, "y": 29, "w": 44, "h": 28 },
+
"rotated": false,
+
"trimmed": false,
+
"spriteSourceSize": { "x": 0, "y": 0, "w": 44, "h": 28 },
+
"sourceSize": { "w": 44, "h": 28 },
+
"duration": 100
+
},
+
{
+
"filename": "snowplow 4.aseprite",
+
"frame": { "x": 45, "y": 29, "w": 44, "h": 28 },
+
"rotated": false,
+
"trimmed": false,
+
"spriteSourceSize": { "x": 0, "y": 0, "w": 44, "h": 28 },
+
"sourceSize": { "w": 44, "h": 28 },
+
"duration": 100
+
},
+
{
+
"filename": "snowplow 5.aseprite",
+
"frame": { "x": 90, "y": 29, "w": 44, "h": 28 },
+
"rotated": false,
+
"trimmed": false,
+
"spriteSourceSize": { "x": 0, "y": 0, "w": 44, "h": 28 },
+
"sourceSize": { "w": 44, "h": 28 },
+
"duration": 100
+
}
+
],
+
"meta": {
+
"app": "https://www.aseprite.org/",
+
"version": "1.3.10.1-x64",
+
"image": "snowplow.png",
+
"format": "RGBA8888",
+
"size": { "w": 134, "h": 57 },
+
"scale": "1"
+
}
+
}
assets/snowplow.png

This is a binary file and will not be displayed.

assets/sprite_sheet.png

This is a binary file and will not be displayed.

+8 -6
src/config.js
···
// default configuration values
const PARAM_DEFAULTS = [
{ name: "v", value: 1 }, // version
-
{ name: "cw", value: 640 }, // canvas width
-
{ name: "ch", value: 360 }, // canvas height
+
{ name: "cw", value: 320 }, // canvas width
+
{ name: "ch", value: 180 }, // canvas height
{ name: "smx", value: 40 }, // snow max
{ name: "smnsz", value: 1 }, // snow min size
{ name: "smxsz", value: 4 }, // snow max size
{ name: "smxspd", value: 0.07 }, // snow max speed
{ name: "snmnspd", value: 0.04 }, // snow min speed
-
{ name: "pspd", value: 0.1 }, // plow speed
-
{ name: "pd", value: "left" }, // plow direction
+
{ name: "plwspd", value: 0.1 }, // plow speed
+
{ name: "plwd", value: "left" }, // plow direction
+
{ name: "plws", value: 1 }, // plow scale
{ name: "gamax", value: 4 }, // ground accumulator max
{ name: "gas", value: 20 }, // ground accumulatior slices
];
···
minSpeed: parseFloat(urlParams.get("snmnspd")),
},
plow: {
-
speed: parseFloat(urlParams.get("pspd")),
+
speed: parseFloat(urlParams.get("plwspd")),
// @ts-ignore -- I dunno how to get ts / jsdoc to be ok with this
-
direction: urlParams.get("pd"),
+
direction: urlParams.get("plwd"),
+
scale: parseFloat(urlParams.get("plws")),
},
groundAccumulator: {
max: parseInt(urlParams.get("gamax")),
+1 -1
src/main.js
···
if (isPlowing) {
plow ??= createPlow();
movePlow(plow, delta);
-
drawPlow(plow, ctx);
+
drawPlow(plow, ctx, time);
if (plowDone(plow)) {
isPlowing = false;
plow = null;
+12 -8
src/plow.js
···
+
import config from "../vite.config.js";
import Config from "./config.js";
-
import { drawSprite } from "./sprites.js";
+
import { drawSprite, loadSprite } from "./sprite.js";
+
const SPRITE = await loadSprite("snowplow");
/**
* @returns {Plow}
*/
export function createPlow() {
return {
-
x: Config.plow.direction === "left" ? Config.canvas.width : -88,
-
y: Config.canvas.height - 56,
+
x:
+
Config.plow.direction === "left"
+
? Config.canvas.width
+
: SPRITE.width * Config.plow.scale,
+
y: Config.canvas.height - SPRITE.height * Config.plow.scale,
speed: Config.plow.speed,
direction: Config.plow.direction,
-
height: 56,
-
width: 88,
};
}
···
/**
* @param {Plow} plow
* @param {CanvasRenderingContext2D} ctx
+
* @param {DOMHighResTimeStamp} timestamp
*/
-
export function drawPlow(plow, ctx) {
-
drawSprite(ctx, 0, 0, 44, 28, plow.x, plow.y, plow.width, plow.height);
+
export function drawPlow(plow, ctx, timestamp) {
+
drawSprite(ctx, SPRITE, plow.x, plow.y, Config.plow.scale, timestamp);
}
/** @param {Plow} plow */
export function plowDone(plow) {
return plow.direction === "left"
-
? plow.x + plow.width < 0
+
? plow.x + SPRITE.width * Config.plow.scale < 0
: plow.x > Config.canvas.width;
}
+1 -1
src/snow_accumulator.js
···
});
plow &&
normalized.length > 0 &&
-
ctx.lineTo(plow.x, normalized[normalized.length - 1][1]);
+
ctx.lineTo(plow.x + 2, normalized[normalized.length - 1][1]);
plow && ctx.lineTo(plow.x, Config.canvas.height);
!plow &&
ctx.lineTo(Config.canvas.width, normalized[normalized.length - 1][1]);
+53
src/sprite.js
···
+
/**
+
* @param {string} slug
+
* @returns {Promise<Sprite>}
+
*/
+
export async function loadSprite(slug) {
+
const image = new Image();
+
image.src = `./assets/${slug}.png`;
+
+
const { frames } = await fetch(`./assets/${slug}.json`).then((res) =>
+
res.json(),
+
);
+
return {
+
image,
+
width: frames[0].frame.w,
+
height: frames[0].frame.h,
+
currentFrame: -1,
+
lastUpdate: 0,
+
frames: frames.map(({ frame: { x, y }, duration }) => ({
+
x,
+
y,
+
duration,
+
})),
+
};
+
}
+
+
/**
+
* @param {CanvasRenderingContext2D} ctx
+
* @param {Sprite} sprite
+
* @param {number} x
+
* @param {number} y
+
* @param {DOMHighResTimeStamp} timestamp
+
*/
+
export function drawSprite(ctx, sprite, x, y, scale, timestamp) {
+
if (
+
sprite.currentFrame === -1 ||
+
timestamp - sprite.lastUpdate > sprite.frames[sprite.currentFrame].duration
+
) {
+
sprite.currentFrame = (sprite.currentFrame + 1) % sprite.frames.length;
+
sprite.lastUpdate = timestamp;
+
}
+
const frame = sprite.frames[sprite.currentFrame];
+
ctx.drawImage(
+
sprite.image,
+
frame.x,
+
frame.y,
+
sprite.width,
+
sprite.height,
+
x,
+
y,
+
sprite.width * scale,
+
sprite.height * scale,
+
);
+
}
-12
src/sprites.js
···
-
function loadSpriteSheet() {
-
const image = new Image();
-
image.src = "./assets/sprite_sheet.png";
-
return image;
-
}
-
const sheet = loadSpriteSheet();
-
-
export function drawSprite(ctx, x, y, width, height, dx, dy, dWidth, dHeight) {
-
ctx.drawImage(sheet, x, y, width, height, dx, dy, dWidth, dHeight);
-
}
-
-
export default sheet;
+19 -4
src/types.d.ts
···
export {};
declare global {
+
type PlowDirection = "left" | "right" | "random";
type Config = {
version: number;
canvas: {
···
};
plow: {
speed: number;
-
direction: "left" | "right" | "random";
+
direction: PlowDirection;
+
scale: number;
};
};
+
type SpriteFrame = {
+
x: number;
+
y: number;
+
duration: number;
+
};
+
+
type Sprite = {
+
image: HTMLImageElement;
+
width: number;
+
height: number;
+
lastUpdate: number;
+
currentFrame: number;
+
frames: SpriteFrame[];
+
};
+
type Snowflake = {
x: number;
y: number;
···
speed: number;
x: number;
y: number;
-
height: number;
-
width: number;
-
direction: "left" | "right";
+
direction: PlowDirection;
};
}