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 }