Happy New Year! Here's a snowflake generator I created using an HTML canvas
:
And here's the JavaScript. Happy coding!
class SnowflakeGenerator {
constructor(canvas, lineWidth, numberOfSegments, branchAngle, speed) {
this.canvas = canvas
this.lineWidth = lineWidth
this.numberOfSegments = numberOfSegments
this.branchAngle = branchAngle
this.speed = speed
this.canceled = false
}
create() {
this.resizeCanvas()
this.defineLayers()
this.drawBackground()
this.computeLengths()
window.requestAnimationFrame(this.drawFrame.bind(this))
}
resizeCanvas() {
let rect = this.canvas.getBoundingClientRect()
this.canvas.width = rect.width * 4
this.canvas.height = this.canvas.width
this.context = this.canvas.getContext('2d')
this.center = this.canvas.width / 2
}
defineLayers() {
this.context.lineCap = 'round'
this.layers = [
{ color: '#ffffff', lineWidth: this.lineWidth, shadowColor: '#c0c0c0', shadowBlur: 25 },
{ color: '#ffffff', lineWidth: this.lineWidth },
{ color: '#000000', lineWidth: 1 }
]
}
drawBackground() {
let gradient = this.context.createLinearGradient(0, 1000, 1000, 0)
gradient.addColorStop(0, '#0b3774')
gradient.addColorStop(1, '#3480eb')
this.context.fillStyle = gradient
}
computeLengths() {
this.spokeLength = this.canvas.width * .45
this.segmentLength = this.spokeLength / this.numberOfSegments
// Make the branches of the middle segements longer than those at the center and outside
this.branchLengths = []
for (let i = 0; i < this.numberOfSegments; i++) {
let length = (this.numberOfSegments / 2 - Math.abs(this.numberOfSegments / 2 - i)) * this.segmentLength * 0.75
this.branchLengths.push(length)
}
}
drawFrame(timestamp) {
if (this.canceled) { return } // Abort if a new snowflake generation has started
this.advanceElapsedTime(timestamp)
this.computeVisibleSpokeLength()
this.buildLines()
this.drawLines()
// If there's more to draw, request a new animation frame
if (this.spokeLength > this.visibleSpokeLength) { window.requestAnimationFrame(this.drawFrame.bind(this)) }
}
advanceElapsedTime(timestamp) {
if (!this.startTime) { this.startTime = timestamp }
this.elapsedSeconds = (timestamp - this.startTime) / 1000
}
// Compute how much of each spoke should be visible for the current animation frame
computeVisibleSpokeLength() {
this.visibleSpokeLength = this.elapsedSeconds > 0 ? this.elapsedSeconds * this.speed / 5 * this.spokeLength : 0
if (this.visibleSpokeLength > this.spokeLength) { this.visibleSpokeLength = this.spokeLength }
this.remainingLength = this.visibleSpokeLength
this.maxLineWidth = (this.visibleSpokeLength > this.spokeLength) ? this.lineWidth : this.lineWidth * this.visibleSpokeLength / this.spokeLength
}
buildLines() {
let spokes = []
for (let i = 0; i < 6; i++) { spokes.push({ x: this.center, y: this.center }) }
this.lines = []
for (let segment = 0; segment < this.numberOfSegments; segment++) {
let degree = 0
let visibleSegmentLength = this.segmentLength
let visibleBranchLength = this.branchLengths[segment]
// Compute how much of the segment to draw
if (this.remainingLength > visibleSegmentLength) {
this.remainingLength -= visibleSegmentLength
} else {
visibleBranchLength *= this.remainingLength / visibleSegmentLength
visibleSegmentLength = this.remainingLength
this.remainingLength = 0
}
if (visibleSegmentLength < 0) { break }
spokes.forEach(spoke => {
let x1 = spoke.x
let y1 = spoke.y
// Ice crystals form in a hexagonal pattern.
// Move 60° and draw the next segment for the current spoke.
degree += 60
let line = this.computeLine(degree, x1, y1, visibleSegmentLength)
this.lines.push(line)
spoke.x = line.x2
spoke.y = line.y2
this.lines.push(this.computeLine(degree + this.branchAngle, x1, y1, visibleBranchLength))
this.lines.push(this.computeLine(degree - this.branchAngle, x1, y1, visibleBranchLength))
})
}
}
computeLine(degree, x1, y1, length) {
let radians = degree * Math.PI / 180
let x2 = x1 + length * Math.cos(radians)
let y2 = y1 + length * Math.sin(radians)
return { x1: x1, x2: x2, y1: y1, y2: y2 }
}
drawLines() {
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height)
this.context.fillRect(0, 0, this.canvas.width, this.canvas.height)
this.layers.forEach(layer => {
this.lines.forEach(line => { this.drawLine(line, layer) })
})
}
drawLine(line, layer) {
let lineWidth = layer.lineWidth > this.maxLineWidth ? this.maxLineWidth : layer.lineWidth
this.context.beginPath()
this.context.strokeStyle = layer.color
this.context.lineWidth = lineWidth
this.context.shadowColor = layer.shadowColor
this.context.shadowBlur = layer.shadowBlur || 0
this.context.moveTo(line.x1, line.y1)
this.context.lineTo(line.x2, line.y2)
this.context.stroke()
}
cancel() {
this.canceled = true
}
}