Skip to content

Instantly share code, notes, and snippets.

@Jeremiah-England
Created March 4, 2025 17:33
Show Gist options
  • Save Jeremiah-England/d7fe559c0e3f834a410bab8dd4b01e68 to your computer and use it in GitHub Desktop.
Save Jeremiah-England/d7fe559c0e3f834a410bab8dd4b01e68 to your computer and use it in GitHub Desktop.
Snowflake Generator - Canvas
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Snowflake Generator - JavaScript</title>
<style>
body {
background-color: black;
color: white;
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
overflow: hidden;
height: 100vh;
display: flex;
flex-direction: column;
}
#header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 5px 20px;
background-color: rgba(20, 20, 20, 0.8);
flex-shrink: 0;
}
h1 {
margin: 0;
flex: 1;
text-align: center;
}
#info {
color: #aaa;
font-size: 16px;
margin-right: 20px;
min-width: 150px;
}
#controls {
display: flex;
gap: 10px;
}
#game-canvas {
width: 100%;
flex: 1;
margin: 0 auto;
display: flex;
justify-content: center;
align-items: center;
}
button {
padding: 6px 12px;
background-color: #444;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #555;
}
/* Info Modal Styles */
#info-button {
margin-left: 10px;
font-size: 16px;
width: 30px;
height: 30px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background-color: #555;
}
#modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.7);
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
}
#info-modal {
background-color: #222;
border-radius: 8px;
padding: 20px;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
color: #ddd;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
}
#info-modal h2 {
margin-top: 0;
color: #fff;
border-bottom: 1px solid #444;
padding-bottom: 10px;
}
#info-modal section {
margin-bottom: 20px;
}
#info-modal h3 {
color: #aaa;
margin-bottom: 8px;
}
#modal-close {
background-color: #444;
color: white;
border: none;
border-radius: 4px;
padding: 8px 16px;
cursor: pointer;
margin-top: 10px;
}
#modal-close:hover {
background-color: #555;
}
</style>
</head>
<body>
<div id="header">
<div id="info"></div>
<h1>Snowflake Generator</h1>
<div id="controls">
<button id="pause-btn">Pause</button>
<button id="zoom-in-btn">Zoom In</button>
<button id="zoom-out-btn">Zoom Out</button>
<button id="info-button">ⓘ</button>
</div>
</div>
<canvas id="game-canvas" width="1600" height="900"></canvas>
<!-- Information Modal -->
<div id="modal-overlay">
<div id="info-modal">
<h2>About Snowflake Generator</h2>
<section>
<h3>Controls</h3>
<ul>
<li><strong>Drag mouse:</strong> Pan camera</li>
<li><strong>Mouse wheel:</strong> Zoom in/out</li>
<li><strong>Arrow keys / WASD:</strong> Pan camera</li>
<li><strong>Space:</strong> Pause/Resume</li>
<li><strong>+/- keys:</strong> Zoom in/out</li>
</ul>
</section>
<section>
<h3>About This Application</h3>
<p>This Snowflake Generator uses JavaScript and HTML5 Canvas to create procedurally generated snowflakes using L-systems. It allows interactive panning and zooming, with realistic physics for the snowflakes.</p>
</section>
<section>
<h3>Credits</h3>
<p>Converted to JavaScript based on the original PyGame implementation.</p>
</section>
<button id="modal-close">Close</button>
</div>
</div>
<script>
// Get DOM elements
const canvas = document.getElementById("game-canvas");
const ctx = canvas.getContext("2d");
const infoDiv = document.getElementById("info");
const pauseBtn = document.getElementById("pause-btn");
const zoomInBtn = document.getElementById("zoom-in-btn");
const zoomOutBtn = document.getElementById("zoom-out-btn");
const infoButton = document.getElementById("info-button");
const modalOverlay = document.getElementById("modal-overlay");
const modalClose = document.getElementById("modal-close");
// Constants
const SCREEN_WIDTH = 1600;
const SCREEN_HEIGHT = 900;
const NUM_SNOWFLAKES = 150;
const NUM_DESIGNS = 25;
const MIN_SCALE = 0.05;
// Variables
let paused = false;
let zoom = 1.0;
let cameraX = 0;
let cameraY = 0;
let dragging = false;
let lastMousePos = null;
let keysPressed = new Set();
let snowflakeDesigns = [];
let snowflakes = [];
// L-system rules
const axiom = "F";
const rules = {
"F": ["F[+F]F[-F]F", "F[+F]F", "F[-F]F", "F[+F][-F]"]
};
// Generate L-system string
function generateLSystem(iterations) {
let current = axiom;
for (let i = 0; i < iterations; i++) {
let next = "";
for (const char of current) {
if (char in rules) {
next += rules[char][Math.floor(Math.random() * rules[char].length)];
} else {
next += char;
}
}
current = next;
}
return current;
}
// Draw L-system on canvas
function drawLSystem(ctx, string, length, angleStep, scaleFactor) {
let angle = 0;
let x = 0;
let y = 0;
let depth = 0;
const stack = [];
ctx.beginPath();
ctx.moveTo(x, y);
for (const char of string) {
if (char === "F") {
const currentLength = length * (scaleFactor ** depth);
const newX = x + currentLength * Math.cos(angle * Math.PI / 180);
const newY = y + currentLength * Math.sin(angle * Math.PI / 180);
ctx.lineTo(newX, newY);
x = newX;
y = newY;
} else if (char === "+") {
angle += angleStep;
} else if (char === "-") {
angle -= angleStep;
} else if (char === "[") {
stack.push({x, y, angle, depth});
depth += 1;
} else if (char === "]") {
({x, y, angle, depth} = stack.pop());
ctx.moveTo(x, y);
}
}
ctx.stroke();
}
// Snowflake class
class Snowflake {
constructor(designIndex, x, y, dx, dy, rotationSpeed, scale) {
this.designIndex = designIndex;
this.x = x;
this.y = y;
this.dx = dx;
this.dy = dy;
this.rotation = 0;
this.rotationSpeed = rotationSpeed;
this.scale = scale;
}
update() {
if (paused) return;
this.x += this.dx;
this.y += this.dy;
this.rotation += this.rotationSpeed;
// Calculate visible area
const visibleWidth = SCREEN_WIDTH / zoom;
const visibleHeight = SCREEN_HEIGHT / zoom;
const worldLeft = cameraX - visibleWidth / 2;
const worldTop = cameraY - visibleHeight / 2;
const worldRight = cameraX + visibleWidth / 2;
const worldBottom = cameraY + visibleHeight / 2;
const buffer = 100 / zoom;
// Respawn if off-screen
if (this.y > worldBottom + buffer || this.y < worldTop - buffer ||
this.x > worldRight + buffer || this.x < worldLeft - buffer) {
const edge = Math.floor(Math.random() * 4);
if (edge === 0) { // Top
this.y = worldTop - buffer;
this.x = Math.random() * (worldRight - worldLeft) + worldLeft;
} else if (edge === 1) { // Right
this.x = worldRight + buffer;
this.y = Math.random() * (worldBottom - worldTop) + worldTop;
} else if (edge === 2) { // Bottom
this.y = worldBottom + buffer;
this.x = Math.random() * (worldRight - worldLeft) + worldLeft;
} else { // Left
this.x = worldLeft - buffer;
this.y = Math.random() * (worldBottom - worldTop) + worldTop;
}
}
}
draw(ctx) {
const design = snowflakeDesigns[this.designIndex];
const totalScale = this.scale * zoom;
if (totalScale <= MIN_SCALE) return;
// Calculate screen position
const screenX = (this.x - cameraX) * zoom + SCREEN_WIDTH / 2;
const screenY = (this.y - cameraY) * zoom + SCREEN_HEIGHT / 2;
if (screenX < -100 || screenX > SCREEN_WIDTH + 100 ||
screenY < -100 || screenY > SCREEN_HEIGHT + 100) return;
ctx.save();
ctx.translate(screenX, screenY);
ctx.rotate(this.rotation * Math.PI / 180);
ctx.scale(totalScale, totalScale);
ctx.strokeStyle = "white";
for (let i = 0; i < 6; i++) {
ctx.save();
ctx.rotate(i * 60 * Math.PI / 180);
drawLSystem(ctx, design, 10, 60, 0.7);
ctx.restore();
}
ctx.restore();
}
}
// Generate snowflake designs
function generateSnowflakeDesigns() {
for (let i = 0; i < NUM_DESIGNS; i++) {
snowflakeDesigns.push(generateLSystem(2));
}
}
// Create snowflakes
function createSnowflakes() {
for (let i = 0; i < NUM_SNOWFLAKES; i++) {
const designIndex = Math.floor(Math.random() * NUM_DESIGNS);
const x = Math.random() * SCREEN_WIDTH * 2 - SCREEN_WIDTH;
const y = Math.random() * SCREEN_HEIGHT * 2 - SCREEN_HEIGHT;
const dx = Math.random() * 1 - 0.5;
const dy = Math.random() * 2 + 1;
const rotationSpeed = Math.random() * 2 - 1;
const scale = Math.random() * 1 + 0.5;
snowflakes.push(new Snowflake(designIndex, x, y, dx, dy, rotationSpeed, scale));
}
}
// Animation loop
let lastTime = performance.now();
let frameCount = 0;
let fps = 0;
function animate(currentTime) {
// Calculate FPS
frameCount++;
const deltaTime = currentTime - lastTime;
if (deltaTime >= 1000) {
fps = frameCount / (deltaTime / 1000);
frameCount = 0;
lastTime = currentTime;
}
// Update snowflakes
snowflakes.forEach(snowflake => snowflake.update());
// Handle keyboard panning
const panSpeed = 5 / zoom;
if (keysPressed.has("ArrowLeft") || keysPressed.has("KeyA")) cameraX -= panSpeed;
if (keysPressed.has("ArrowRight") || keysPressed.has("KeyD")) cameraX += panSpeed;
if (keysPressed.has("ArrowUp") || keysPressed.has("KeyW")) cameraY -= panSpeed;
if (keysPressed.has("ArrowDown") || keysPressed.has("KeyS")) cameraY += panSpeed;
if (keysPressed.has("Equal") || keysPressed.has("NumpadAdd")) zoom = Math.min(5.0, zoom * 1.1);
if (keysPressed.has("Minus") || keysPressed.has("NumpadSubtract")) zoom = Math.max(0.1, zoom / 1.1);
// Clear canvas
ctx.fillStyle = "black";
ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
// Draw snowflakes
snowflakes.forEach(snowflake => snowflake.draw(ctx));
// Update info
infoDiv.textContent = `Zoom: ${zoom.toFixed(1)}x | FPS: ${Math.round(fps)}`;
requestAnimationFrame(animate);
}
// Event listeners
pauseBtn.addEventListener("click", () => {
paused = !paused;
pauseBtn.textContent = paused ? "Resume" : "Pause";
});
zoomInBtn.addEventListener("click", () => {
zoom = Math.min(5.0, zoom * 1.1);
});
zoomOutBtn.addEventListener("click", () => {
zoom = Math.max(0.1, zoom / 1.1);
});
infoButton.addEventListener("click", () => {
modalOverlay.style.display = "flex";
});
modalClose.addEventListener("click", () => {
modalOverlay.style.display = "none";
});
modalOverlay.addEventListener("click", (e) => {
if (e.target === modalOverlay) {
modalOverlay.style.display = "none";
}
});
// Mouse events for panning
canvas.addEventListener("mousedown", (e) => {
dragging = true;
lastMousePos = {x: e.clientX, y: e.clientY};
});
document.addEventListener("mouseup", () => {
dragging = false;
});
document.addEventListener("mousemove", (e) => {
if (dragging) {
const dx = e.clientX - lastMousePos.x;
const dy = e.clientY - lastMousePos.y;
cameraX -= dx / zoom;
cameraY -= dy / zoom;
lastMousePos = {x: e.clientX, y: e.clientY};
}
});
// Mouse wheel for zooming
canvas.addEventListener("wheel", (e) => {
e.preventDefault();
const delta = e.deltaY > 0 ? 0.9 : 1.1;
const mouseX = e.offsetX;
const mouseY = e.offsetY;
const worldXBefore = (mouseX - SCREEN_WIDTH / 2) / zoom + cameraX;
const worldYBefore = (mouseY - SCREEN_HEIGHT / 2) / zoom + cameraY;
zoom = Math.max(0.1, Math.min(5.0, zoom * delta));
const worldXAfter = (mouseX - SCREEN_WIDTH / 2) / zoom + cameraX;
const worldYAfter = (mouseY - SCREEN_HEIGHT / 2) / zoom + cameraY;
cameraX += worldXBefore - worldXAfter;
cameraY += worldYBefore - worldYAfter;
});
// Keyboard events
document.addEventListener("keydown", (e) => {
keysPressed.add(e.code);
if (e.code === "Space") {
e.preventDefault();
paused = !paused;
pauseBtn.textContent = paused ? "Resume" : "Pause";
}
});
document.addEventListener("keyup", (e) => {
keysPressed.delete(e.code);
});
// Initialize
generateSnowflakeDesigns();
createSnowflakes();
requestAnimationFrame(animate);
</script>
</body>
</html>
@Jeremiah-England
Copy link
Author

This is a rewrite of https://gist.github.com/Jeremiah-England/7ee1a484fb99796f5c3ba84492e6a56b into plain javascript by Grok3 which is much faster and looks nicer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment