Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Perf] Use single path for all non-image snowflakes; use for..of loops over forEach; store Math.PI * 2 as a constant #71

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 25 additions & 7 deletions packages/react-snowfall/src/SnowfallCanvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ export class SnowfallCanvas {
this.snowflakes = this.snowflakes.slice(0, this.config.snowflakeCount)
}

this.snowflakes.forEach((snowflake) => snowflake.updateConfig(this.config))
for (const snowflake of this.snowflakes) {
snowflake.updateConfig(this.config)
}
}

/**
Expand All @@ -66,18 +68,34 @@ export class SnowfallCanvas {
private render(framesPassed = 1) {
const { ctx, canvas, snowflakes } = this

if (!ctx || !canvas) return

const { offsetWidth, offsetHeight } = canvas

// Update the position of each snowflake
snowflakes.forEach((snowflake) => snowflake.update(offsetWidth, offsetHeight, framesPassed))
for (const snowflake of snowflakes) {
snowflake.update(offsetWidth, offsetHeight, framesPassed)
}

// Render them if the canvas is available
if (ctx) {
ctx.setTransform(1, 0, 0, 1, 0, 0)
ctx.clearRect(0, 0, offsetWidth, offsetHeight)
// Render the snowflakes
ctx.setTransform(1, 0, 0, 1, 0, 0)
ctx.clearRect(0, 0, offsetWidth, offsetHeight)

// If using images, draw each image individually
if (this.config.images && this.config.images.length > 0) {
for (const snowflake of snowflakes) {
snowflake.drawImage(ctx)
}
return
}

snowflakes.forEach((snowflake) => snowflake.draw(ctx))
// Not using images, draw circles in a single path
ctx.beginPath()
for (const snowflake of snowflakes) {
snowflake.drawCircle(ctx)
}
ctx.fillStyle = this.config.color!
ctx.fill()
}

private animationFrame: number | undefined
Expand Down
64 changes: 42 additions & 22 deletions packages/react-snowfall/src/Snowflake.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import isEqual from 'react-fast-compare'
import { lerp, random, randomElement } from './utils.js'
import { lerp, random, randomElement, twoPi } from './utils.js'

export interface SnowflakeProps {
/** The color of the snowflake, can be any valid CSS color. */
Expand Down Expand Up @@ -212,29 +212,49 @@ class Snowflake {
return sizes[size] ?? image
}

public draw(ctx: CanvasRenderingContext2D): void {
/**
* Draws a circular snowflake to the canvas.
*
* This method should only be called if our config does not have images.
*
* This method assumes that a path has already been started on the canvas.
* `ctx.beginPath()` should be called before calling this method.
*
* After calling this method, the fillStyle should be set to the snowflake's
* color and `ctx.fill()` should be called to fill the snowflake.
*
* Calling `ctx.fill()` after multiple snowflakes have had `drawCircle` called
* will render all of the snowflakes since the last call to `ctx.beginPath()`.
*
* @param ctx The canvas context to draw to
*/
public drawCircle(ctx: CanvasRenderingContext2D): void {
ctx.moveTo(this.params.x, this.params.y)
ctx.arc(this.params.x, this.params.y, this.params.radius, 0, twoPi)
}

/**
* Draws an image-based snowflake to the canvas.
*
* This method should only be called if our config has images.
*
* @param ctx The canvas context to draw to
*/
public drawImage(ctx: CanvasRenderingContext2D): void {
const { x, y, rotation, radius } = this.params

if (this.image) {
const radian = (rotation * Math.PI) / 180
const cos = Math.cos(radian)
const sin = Math.sin(radian)

// Translate to the location that we will be drawing the snowflake, including any rotation that needs to be applied
// The arguments for setTransform are: a, b, c, d, e, f
// a (scaleX), b (skewY), c (skewX), d (scaleY), e (translateX), f (translateY)
ctx.setTransform(cos, sin, -sin, cos, x, y)

// Draw the image with the center of the image at the center of the current location
const image = this.getImageOffscreenCanvas(this.image, radius)
ctx.drawImage(image, -(radius / 2), -(radius / 2), radius, radius)
} else {
// Not using images so no need to use transforms, just draw an arc in the right location
ctx.beginPath()
ctx.arc(x, y, radius, 0, 2 * Math.PI)
ctx.fillStyle = this.config.color
ctx.fill()
}
const radian = (rotation * Math.PI) / 180
const cos = Math.cos(radian)
const sin = Math.sin(radian)

// Translate to the location that we will be drawing the snowflake, including any rotation that needs to be applied
// The arguments for setTransform are: a, b, c, d, e, f
// a (scaleX), b (skewY), c (skewX), d (scaleY), e (translateX), f (translateY)
ctx.setTransform(cos, sin, -sin, cos, x, y)

// Draw the image with the center of the image at the center of the current location
const image = this.getImageOffscreenCanvas(this.image!, radius)
ctx.drawImage(image, -(radius / 2), -(radius / 2), radius, radius)
}
}

Expand Down
7 changes: 7 additions & 0 deletions packages/react-snowfall/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,10 @@ export function getSize(element?: HTMLElement | null) {
width: element.offsetWidth,
}
}

/**
* Store the value of PI * 2.
*
* This is so we can avoid calculating this value every time we draw a circle.
*/
export const twoPi = Math.PI * 2
Loading