diff --git a/samples/particle-shape-spin.html b/samples/particle-shape-spin.html
new file mode 100644
index 0000000..ef695b8
--- /dev/null
+++ b/samples/particle-shape-spin.html
@@ -0,0 +1,47 @@
+
+
+
+
+ Title
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/particle-shape.html b/samples/particle-shape.html
index 58d27eb..aed667b 100644
--- a/samples/particle-shape.html
+++ b/samples/particle-shape.html
@@ -42,7 +42,7 @@
minR: 30,
shape: [
'circle', 'triangle', 'star', 'star:4:0.5', 'star:30:0.5',
- 'https://img30.360buyimg.com/ling/jfs/t1/42528/38/16521/191828/60ecf690E1b8b016d/060dbecad0b8042a.png'
+ 'https://raw.githubusercontent.com/Barrior/assets/main/smiling-face.gif'
],
})
})
diff --git a/samples/snow-shape-spin.html b/samples/snow-shape-spin.html
new file mode 100644
index 0000000..430d393
--- /dev/null
+++ b/samples/snow-shape-spin.html
@@ -0,0 +1,125 @@
+
+
+
+
+ Title
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/common/mask.ts b/src/common/mask.ts
index 1afe3de..0d81d40 100644
--- a/src/common/mask.ts
+++ b/src/common/mask.ts
@@ -6,12 +6,12 @@ export type modeMethodNames = 'modeNormal' | 'modeGhost'
export default abstract class Mask extends Base {
// 遮罩图像对象
- protected maskImage?: CanvasImageSource
+ protected maskImage?: ICanvasImageSource
// 扩展 mask 相关属性
protected readonly options!: Options &
CommonConfig & {
- mask?: string | CanvasImageSource
+ mask?: string | ICanvasImageSource
maskMode?: 'normal' | 'ghost'
}
@@ -41,7 +41,7 @@ export default abstract class Mask extends Base {
this.maskImage = image
})
} else {
- this.maskImage = mask as CanvasImageSource
+ this.maskImage = mask as ICanvasImageSource
}
}
diff --git a/src/common/shape.ts b/src/common/shape.ts
index c6de79a..01fe1ac 100644
--- a/src/common/shape.ts
+++ b/src/common/shape.ts
@@ -1,5 +1,5 @@
import Base from '@src/common/base'
-import { doublePi, regExp } from '@src/common/constants'
+import { doublePi, piBy180, regExp } from '@src/common/constants'
import type { CommonConfig } from '@src/types/common-config'
import type { ValueOf } from '@src/types/utility-types'
import { isArray, isString, loadImage } from '@src/utils'
@@ -12,13 +12,13 @@ export type NormalShapeType = Exclude, 'image'>
// 1. `star:[边数][凹值]`, 示例:`star:5:0.5`, 表示五角星
// 2. 图片 HTTP 地址,示例:`https://xxx.com/a.jpg`
// 3. 图片 Base64 格式,示例:`data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA`
-export type ShapeType = NormalShapeType | string | CanvasImageSource
+export type ShapeType = NormalShapeType | string | ICanvasImageSource
export interface ShapeData {
// 形状类型
type: ValueOf
// 当为图片类型时,图片加载完毕的源文件
- source?: CanvasImageSource
+ source?: ICanvasImageSource
// 当为图片类型时,图片是否加载完毕
isImageLoaded?: boolean
// 当为 star 类型时,星形的边数
@@ -75,7 +75,6 @@ function generateShapeData(shape: ShapeType): ShapeData {
// 处理 CanvasImageSource 类型
if (
shape instanceof HTMLImageElement ||
- shape instanceof SVGImageElement ||
shape instanceof HTMLVideoElement ||
shape instanceof HTMLCanvasElement ||
shape instanceof ImageBitmap ||
@@ -174,6 +173,7 @@ export default abstract class Shape extends Base {
r: number
color: string
shape: ShapeData
+ rotate: number
}): void {
const { type, isImageLoaded, source, sides, dent } = data.shape
@@ -181,38 +181,43 @@ export default abstract class Shape extends Base {
return
}
+ if (type === 'image' && (!isImageLoaded || !source)) {
+ return
+ }
+
this.ctx.save()
+ // 旋转渲染
+ this.ctx.translate(data.x, data.y)
+ this.ctx.rotate(data.rotate * piBy180)
+
if (type === 'image') {
- if (isImageLoaded) {
- const width = data.r * 2
- this.ctx.drawImage(
- source!,
- 0,
- 0,
- (source?.width as number) || width,
- (source?.height as number) || width,
- data.x - data.r,
- data.y - data.r,
- width,
- width
- )
- }
+ const width = data.r * 2
+ this.ctx.drawImage(
+ source!,
+ 0,
+ 0,
+ source!.width || width,
+ source!.height || width,
+ -data.r,
+ -data.r,
+ width,
+ width
+ )
} else {
this.ctx.beginPath()
switch (data.shape.type) {
case 'circle':
- this.ctx.arc(data.x, data.y, data.r, 0, doublePi)
+ this.ctx.arc(0, 0, data.r, 0, doublePi)
break
case 'triangle':
- drawStar(this.ctx, data.x, data.y, data.r, 3, 0.5)
+ drawStar(this.ctx, 0, 0, data.r, 3, 0.5)
break
case 'star':
- drawStar(this.ctx, data.x, data.y, data.r, sides!, dent!)
+ drawStar(this.ctx, 0, 0, data.r, sides!, dent!)
break
}
-
this.ctx.fillStyle = data.color
this.ctx.fill()
}
diff --git a/src/particle.ts b/src/particle.ts
index 8437900..3e1547a 100644
--- a/src/particle.ts
+++ b/src/particle.ts
@@ -63,6 +63,15 @@ export default class Particle extends Shape {
// 视差强度,值越小视差效果越强烈
parallaxStrength: 3,
+
+ // 是否自旋
+ spin: false,
+
+ // 粒子最大运动角速度(0, 360)
+ spinMaxSpeed: 5,
+
+ // 粒子最小运动角速度(0, 360)
+ spinMinSpeed: 1,
}
protected elements!: IElement[]
@@ -185,7 +194,16 @@ export default class Particle extends Shape {
*/
private createDots(): void {
const { canvasWidth, canvasHeight, getColor } = this
- const { maxR, minR, maxSpeed, minSpeed, parallaxLayer } = this.options
+ const {
+ maxR,
+ minR,
+ maxSpeed,
+ minSpeed,
+ parallaxLayer,
+ spin,
+ spinMaxSpeed,
+ spinMinSpeed,
+ } = this.options
const layerLength = parallaxLayer.length
let { num } = this.options
@@ -204,6 +222,10 @@ export default class Particle extends Shape {
// 定义粒子视差的偏移值
parallaxOffsetX: 0,
parallaxOffsetY: 0,
+ // 定义粒子的旋转角度
+ rotate: spin ? randomInRange(0, 360) : 0,
+ // 粒子的旋转速度
+ rotateSpeed: randomSpeed(spinMaxSpeed, spinMinSpeed),
})
}
}
@@ -226,6 +248,10 @@ export default class Particle extends Shape {
// 绘制粒子
this.elements.forEach((dot) => {
const { x, y, parallaxOffsetX, parallaxOffsetY } = dot
+
+ // 更新粒子旋转角度
+ this.updateElementRotate(dot)
+
this.drawShape({
...dot,
x: x + parallaxOffsetX,
@@ -240,6 +266,23 @@ export default class Particle extends Shape {
this.requestAnimationFrame()
}
+ /**
+ * 更新元素自旋数据
+ */
+ private updateElementRotate(element: IElement) {
+ if (!this.options.spin || this.isPaused) {
+ return
+ }
+
+ // 更新旋转角度
+ element.rotate += element.rotateSpeed
+
+ // 大于等于 360 度时,回到 0-360 范围,避免变量数值一直累计超过 MAX_SAFE_INTEGER
+ if (element.rotate >= 360) {
+ element.rotate = element.rotate - 360
+ }
+ }
+
/**
* 连接粒子,绘制线段
*/
diff --git a/src/snow.ts b/src/snow.ts
index 7514f3f..5416ed2 100644
--- a/src/snow.ts
+++ b/src/snow.ts
@@ -15,6 +15,9 @@ export default class Snow extends Shape {
swing: true,
swingInterval: 2000,
swingProbability: 0.06,
+ spin: false,
+ spinMaxSpeed: 5,
+ spinMinSpeed: 1,
}
protected elements!: IElement[]
@@ -41,8 +44,17 @@ export default class Snow extends Shape {
* 创建单个雪花,包含大小、位置、速度等信息
*/
private createSnowflake(): IElement {
- const { maxR, minR, maxSpeed, minSpeed } = this.options
+ const {
+ maxR,
+ minR,
+ maxSpeed,
+ minSpeed,
+ spin,
+ spinMaxSpeed,
+ spinMinSpeed,
+ } = this.options
const r = randomInRange(maxR, minR)
+
return {
r,
x: Math.random() * this.canvasWidth,
@@ -53,6 +65,10 @@ export default class Snow extends Shape {
color: this.getColor(),
swingAt: Date.now(),
shape: this.getShapeData(),
+ // 定义粒子的旋转角度
+ rotate: spin ? randomInRange(0, 360) : 0,
+ // 粒子的旋转速度
+ rotateSpeed: randomSpeed(spinMaxSpeed, spinMinSpeed),
}
}
@@ -74,14 +90,22 @@ export default class Snow extends Shape {
*/
protected draw(): void {
const { canvasWidth, canvasHeight, isPaused } = this
- const { maxR, swing, swingInterval, swingProbability, duration } =
- this.options
+ const {
+ maxR,
+ swing,
+ swingInterval,
+ swingProbability,
+ duration,
+ } = this.options
this.clearCanvasAndSetGlobalAttrs()
this.elements.forEach((snowflake, i, array) => {
const { x, y, r } = snowflake
+ // 更新旋转角度
+ this.updateElementRotate(snowflake)
+
this.drawShape(snowflake)
// 暂停(isPaused)且窗口改变(resize)的时候也会调用绘图方法
@@ -137,6 +161,23 @@ export default class Snow extends Shape {
}
}
+ /**
+ * 更新元素自旋数据
+ */
+ private updateElementRotate(element: IElement) {
+ if (!this.options.spin || this.isPaused) {
+ return
+ }
+
+ // 更新旋转角度
+ element.rotate += element.rotateSpeed
+
+ // 大于等于 360 度时,回到 0-360 范围,避免变量数值一直累计超过 MAX_SAFE_INTEGER
+ if (element.rotate >= 360) {
+ element.rotate = element.rotate - 360
+ }
+ }
+
/**
* 方法:当存在持续时间时,再次下雪
*/
diff --git a/src/types/global.d.ts b/src/types/global.d.ts
index bbce3af..e2f04fb 100644
--- a/src/types/global.d.ts
+++ b/src/types/global.d.ts
@@ -11,3 +11,8 @@ interface Window {
mozRequestAnimationFrame?: AnimationFrameProvider['requestAnimationFrame']
WebKitMutationObserver?: MutationObserver
}
+
+/**
+ * 排除没有 width 和 height 属性的元素
+ */
+type ICanvasImageSource = Exclude
diff --git a/src/types/particle.d.ts b/src/types/particle.d.ts
index c6c2c20..038614f 100644
--- a/src/types/particle.d.ts
+++ b/src/types/particle.d.ts
@@ -51,6 +51,15 @@ export interface Options extends Partial {
// 视差强度,值越小视差效果越强烈
parallaxStrength: number
+
+ // 粒子是否自旋
+ spin: boolean
+
+ // 粒子最大运动角速度(0, 360)
+ spinMaxSpeed: number
+
+ // 粒子最小运动角速度(0, 360)
+ spinMinSpeed: number
}
export interface IElement {
@@ -64,6 +73,10 @@ export interface IElement {
vx: number
// 每次绘制时,粒子在 y 轴方向的步进值,正负值
vy: number
+ // 粒子的旋转角度
+ rotate: number
+ // 粒子的旋转速度
+ rotateSpeed: number
// 粒子颜色
color: string
// 形状数据
diff --git a/src/types/snow.d.ts b/src/types/snow.d.ts
index 730efac..2633f08 100644
--- a/src/types/snow.d.ts
+++ b/src/types/snow.d.ts
@@ -22,6 +22,12 @@ export interface Options extends Partial {
swingInterval: number
// 变换方向的概率(达到时间间隔后),取值范围 [0, 1]
swingProbability: number
+ // 雪花是否自旋
+ spin: boolean
+ // 雪花最大运动角速度(0, 360)
+ spinMaxSpeed: number
+ // 雪花最小运动角速度(0, 360)
+ spinMinSpeed: number
}
export interface IElement {
@@ -41,4 +47,8 @@ export interface IElement {
swingAt: number
// 形状数据
shape: ShapeData
+ // 旋转角度
+ rotate: number
+ // 旋转速度
+ rotateSpeed: number
}