diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index a8f8fd4e6..275ed7a8b 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -22,7 +22,7 @@ importers: specifier: ~0.5.7 version: 0.5.7 '@visactor/vrender': - specifier: workspace:1.0.12 + specifier: workspace:1.0.13 version: link:../packages/vrender '@visactor/vutils': specifier: 1.0.6 @@ -95,7 +95,7 @@ importers: ../../packages/react-vrender: dependencies: '@visactor/vrender': - specifier: workspace:1.0.12 + specifier: workspace:1.0.13 version: link:../vrender '@visactor/vutils': specifier: 1.0.6 @@ -153,10 +153,10 @@ importers: ../../packages/react-vrender-utils: dependencies: '@visactor/react-vrender': - specifier: workspace:1.0.12 + specifier: workspace:1.0.13 version: link:../react-vrender '@visactor/vrender': - specifier: workspace:1.0.12 + specifier: workspace:1.0.13 version: link:../vrender '@visactor/vutils': specifier: 1.0.6 @@ -211,13 +211,13 @@ importers: ../../packages/vrender: dependencies: '@visactor/vrender-animate': - specifier: workspace:1.0.12 + specifier: workspace:1.0.13 version: link:../vrender-animate '@visactor/vrender-core': - specifier: workspace:1.0.12 + specifier: workspace:1.0.13 version: link:../vrender-core '@visactor/vrender-kits': - specifier: workspace:1.0.12 + specifier: workspace:1.0.13 version: link:../vrender-kits devDependencies: '@internal/bundler': @@ -284,7 +284,7 @@ importers: ../../packages/vrender-animate: dependencies: '@visactor/vrender-core': - specifier: workspace:1.0.12 + specifier: workspace:1.0.13 version: link:../vrender-core '@visactor/vutils': specifier: 1.0.6 @@ -342,13 +342,13 @@ importers: ../../packages/vrender-components: dependencies: '@visactor/vrender-animate': - specifier: workspace:1.0.12 + specifier: workspace:1.0.13 version: link:../vrender-animate '@visactor/vrender-core': - specifier: workspace:1.0.12 + specifier: workspace:1.0.13 version: link:../vrender-core '@visactor/vrender-kits': - specifier: workspace:1.0.12 + specifier: workspace:1.0.13 version: link:../vrender-kits '@visactor/vscale': specifier: 1.0.6 @@ -467,7 +467,7 @@ importers: specifier: 2.4.1 version: 2.4.1 '@visactor/vrender-core': - specifier: workspace:1.0.12 + specifier: workspace:1.0.13 version: link:../vrender-core '@visactor/vutils': specifier: 1.0.6 @@ -583,19 +583,19 @@ importers: ../../tools/bugserver-trigger: dependencies: '@visactor/vrender': - specifier: workspace:1.0.12 + specifier: workspace:1.0.13 version: link:../../packages/vrender '@visactor/vrender-animate': - specifier: workspace:1.0.12 + specifier: workspace:1.0.13 version: link:../../packages/vrender-animate '@visactor/vrender-components': - specifier: workspace:1.0.12 + specifier: workspace:1.0.13 version: link:../../packages/vrender-components '@visactor/vrender-core': - specifier: workspace:1.0.12 + specifier: workspace:1.0.13 version: link:../../packages/vrender-core '@visactor/vrender-kits': - specifier: workspace:1.0.12 + specifier: workspace:1.0.13 version: link:../../packages/vrender-kits devDependencies: '@internal/bundler': diff --git a/common/config/rush/version-policies.json b/common/config/rush/version-policies.json index 294cb71bb..12f6b8ddf 100644 --- a/common/config/rush/version-policies.json +++ b/common/config/rush/version-policies.json @@ -1 +1 @@ -[{"definitionName":"lockStepVersion","policyName":"vrenderMain","version":"1.0.12","nextBump":"patch"}] +[{"definitionName":"lockStepVersion","policyName":"vrenderMain","version":"1.0.13","nextBump":"patch"}] diff --git a/docs/assets/changelog/en/changelog.md b/docs/assets/changelog/en/changelog.md index b4f382f30..90490b1e6 100644 --- a/docs/assets/changelog/en/changelog.md +++ b/docs/assets/changelog/en/changelog.md @@ -1,3 +1,19 @@ +# v1.0.12 + +2025-08-20 + + +**What's Changed** + +* Main by @neuqzxy in https://github.com/VisActor/VRender/pull/1919 +* feat: support hideOnOverflow by @xuefei1313 in https://github.com/VisActor/VRender/pull/1920 +* [Auto release] release 1.0.12 by @github-actions[bot] in https://github.com/VisActor/VRender/pull/1921 + + +**Full Changelog**: https://github.com/VisActor/VRender/compare/v1.0.11...v1.0.12 + +[more detail about v1.0.12](https://github.com/VisActor/VRender/releases/tag/v1.0.12) + # v0.22.11 2025-04-28 diff --git a/docs/assets/changelog/zh/changelog.md b/docs/assets/changelog/zh/changelog.md index a18dc48c9..23a1a4f38 100644 --- a/docs/assets/changelog/zh/changelog.md +++ b/docs/assets/changelog/zh/changelog.md @@ -1,3 +1,19 @@ +# v1.0.12 + +2025-08-20 + + +**What's Changed** + +* Main by @neuqzxy in https://github.com/VisActor/VRender/pull/1919 +* feat: support hideOnOverflow by @xuefei1313 in https://github.com/VisActor/VRender/pull/1920 +* [Auto release] release 1.0.12 by @github-actions[bot] in https://github.com/VisActor/VRender/pull/1921 + + +**Full Changelog**: https://github.com/VisActor/VRender/compare/v1.0.11...v1.0.12 + +[更多详情请查看 v1.0.12](https://github.com/VisActor/VRender/releases/tag/v1.0.12) + # v0.22.11 2025-04-28 diff --git a/docs/package.json b/docs/package.json index b06c896b0..b40e8996e 100644 --- a/docs/package.json +++ b/docs/package.json @@ -13,7 +13,7 @@ "@visactor/vchart": "1.3.0", "@visactor/vutils": "1.0.6", "@visactor/vgrammar": "~0.5.7", - "@visactor/vrender": "workspace:1.0.12", + "@visactor/vrender": "workspace:1.0.13", "markdown-it": "^13.0.0", "highlight.js": "^11.8.0", "axios": "^1.4.0", diff --git a/packages/react-vrender-utils/CHANGELOG.json b/packages/react-vrender-utils/CHANGELOG.json index c63546585..201089dc4 100644 --- a/packages/react-vrender-utils/CHANGELOG.json +++ b/packages/react-vrender-utils/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/react-vrender-utils", "entries": [ + { + "version": "1.0.13", + "tag": "@visactor/react-vrender-utils_v1.0.13", + "date": "Tue, 26 Aug 2025 11:35:34 GMT", + "comments": {} + }, { "version": "1.0.12", "tag": "@visactor/react-vrender-utils_v1.0.12", diff --git a/packages/react-vrender-utils/CHANGELOG.md b/packages/react-vrender-utils/CHANGELOG.md index 1f45461b6..a8eb322dc 100644 --- a/packages/react-vrender-utils/CHANGELOG.md +++ b/packages/react-vrender-utils/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/react-vrender-utils -This log was last generated on Wed, 20 Aug 2025 07:07:52 GMT and should not be manually modified. +This log was last generated on Tue, 26 Aug 2025 11:35:34 GMT and should not be manually modified. + +## 1.0.13 +Tue, 26 Aug 2025 11:35:34 GMT + +_Version update only_ ## 1.0.12 Wed, 20 Aug 2025 07:07:52 GMT diff --git a/packages/react-vrender-utils/package.json b/packages/react-vrender-utils/package.json index c50228c2c..a484df8a1 100644 --- a/packages/react-vrender-utils/package.json +++ b/packages/react-vrender-utils/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/react-vrender-utils", - "version": "1.0.12", + "version": "1.0.13", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -24,8 +24,8 @@ "react-dom": "^18.2.0" }, "dependencies": { - "@visactor/vrender": "workspace:1.0.12", - "@visactor/react-vrender": "workspace:1.0.12", + "@visactor/vrender": "workspace:1.0.13", + "@visactor/react-vrender": "workspace:1.0.13", "@visactor/vutils": "1.0.6", "react-reconciler": "^0.29.0", "tslib": "^2.3.1" diff --git a/packages/react-vrender/CHANGELOG.json b/packages/react-vrender/CHANGELOG.json index 98d16a9d3..a27f07cd8 100644 --- a/packages/react-vrender/CHANGELOG.json +++ b/packages/react-vrender/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/react-vrender", "entries": [ + { + "version": "1.0.13", + "tag": "@visactor/react-vrender_v1.0.13", + "date": "Tue, 26 Aug 2025 11:35:34 GMT", + "comments": {} + }, { "version": "1.0.12", "tag": "@visactor/react-vrender_v1.0.12", diff --git a/packages/react-vrender/CHANGELOG.md b/packages/react-vrender/CHANGELOG.md index 83bf669e6..51e807a1b 100644 --- a/packages/react-vrender/CHANGELOG.md +++ b/packages/react-vrender/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/react-vrender -This log was last generated on Wed, 20 Aug 2025 07:07:52 GMT and should not be manually modified. +This log was last generated on Tue, 26 Aug 2025 11:35:34 GMT and should not be manually modified. + +## 1.0.13 +Tue, 26 Aug 2025 11:35:34 GMT + +_Version update only_ ## 1.0.12 Wed, 20 Aug 2025 07:07:52 GMT diff --git a/packages/react-vrender/package.json b/packages/react-vrender/package.json index 56fb1ec95..7b5fdb843 100644 --- a/packages/react-vrender/package.json +++ b/packages/react-vrender/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/react-vrender", - "version": "1.0.12", + "version": "1.0.13", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -23,7 +23,7 @@ "react": "^18.2.0" }, "dependencies": { - "@visactor/vrender": "workspace:1.0.12", + "@visactor/vrender": "workspace:1.0.13", "@visactor/vutils": "1.0.6", "react-reconciler": "^0.29.0", "tslib": "^2.3.1" diff --git a/packages/vrender-animate/CHANGELOG.json b/packages/vrender-animate/CHANGELOG.json index b89b67d1a..8ec668ee7 100644 --- a/packages/vrender-animate/CHANGELOG.json +++ b/packages/vrender-animate/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/vrender-animate", "entries": [ + { + "version": "1.0.13", + "tag": "@visactor/vrender-animate_v1.0.13", + "date": "Tue, 26 Aug 2025 11:35:34 GMT", + "comments": {} + }, { "version": "1.0.12", "tag": "@visactor/vrender-animate_v1.0.12", diff --git a/packages/vrender-animate/CHANGELOG.md b/packages/vrender-animate/CHANGELOG.md index 41d96f7fb..ff334e6e8 100644 --- a/packages/vrender-animate/CHANGELOG.md +++ b/packages/vrender-animate/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/vrender-animate -This log was last generated on Wed, 20 Aug 2025 07:07:52 GMT and should not be manually modified. +This log was last generated on Tue, 26 Aug 2025 11:35:34 GMT and should not be manually modified. + +## 1.0.13 +Tue, 26 Aug 2025 11:35:34 GMT + +_Version update only_ ## 1.0.12 Wed, 20 Aug 2025 07:07:52 GMT diff --git a/packages/vrender-animate/package.json b/packages/vrender-animate/package.json index 83bf59a94..974c14270 100644 --- a/packages/vrender-animate/package.json +++ b/packages/vrender-animate/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-animate", - "version": "1.0.12", + "version": "1.0.13", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -21,7 +21,7 @@ }, "dependencies": { "@visactor/vutils": "1.0.6", - "@visactor/vrender-core": "workspace:1.0.12" + "@visactor/vrender-core": "workspace:1.0.13" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/packages/vrender-animate/src/custom/disappear/base/CustomEffectBase.ts b/packages/vrender-animate/src/custom/disappear/base/CustomEffectBase.ts new file mode 100644 index 000000000..6ea099d1c --- /dev/null +++ b/packages/vrender-animate/src/custom/disappear/base/CustomEffectBase.ts @@ -0,0 +1,145 @@ +import type { EasingType } from '@visactor/vrender-core'; +import { DisappearAnimateBase } from './DisappearAnimateBase'; + +/** + * 仅支持WebGL的特效基类 + * 适用于复杂的GPU计算特效,如粒子系统、复杂着色器特效等 + */ +export abstract class WebGLEffectBase extends DisappearAnimateBase { + constructor(from: null, to: null, duration: number, easing: EasingType, params: any) { + super(from, to, duration, easing, params); + } + + // 必须实现WebGL相关方法 + protected abstract getShaderSources(): { vertex: string; fragment: string }; + protected abstract applyWebGLEffect(canvas: HTMLCanvasElement): HTMLCanvasElement; + + // Canvas 2D回退:简单的透明度动画或返回原图 + protected applyCanvas2DEffect(canvas: HTMLCanvasElement): HTMLCanvasElement { + console.warn(`${this.constructor.name}: WebGL不可用,使用简单透明度回退动画`); + + const outputCanvas = this.createOutputCanvas(canvas); + if (!outputCanvas) { + return canvas; + } + + const { ctx } = outputCanvas; + + // 简单的透明度渐变作为回退 + ctx.globalAlpha = Math.max(0, 1 - this.currentAnimationRatio); + ctx.drawImage(canvas, 0, 0); + + return outputCanvas.canvas; + } +} + +/** + * 仅支持Canvas 2D的特效基类 + * 适用于简单的2D图像处理特效,如模糊、颜色调整等 + */ +export abstract class Canvas2DEffectBase extends DisappearAnimateBase { + constructor(from: null, to: null, duration: number, easing: EasingType, params: any) { + super(from, to, duration, easing, params); + } + + // 必须实现Canvas 2D方法 + protected abstract applyCanvas2DEffect(canvas: HTMLCanvasElement): HTMLCanvasElement; + + // 不支持WebGL,返回null + protected getShaderSources(): { vertex: string; fragment: string } | null { + return null; + } + + protected applyWebGLEffect(canvas: HTMLCanvasElement): HTMLCanvasElement | null { + return null; + } +} + +/** + * 混合实现特效基类 + * 既支持WebGL也支持Canvas 2D,根据环境自动选择 + */ +export class HybridEffectBase extends DisappearAnimateBase { + constructor(from: null, to: null, duration: number, easing: EasingType, params: any) { + super(from, to, duration, easing, params); + } + + // 可选实现WebGL方法 + protected getShaderSources(): { vertex: string; fragment: string } | null { + return null; // 子类可以重写 + } + + protected applyWebGLEffect(canvas: HTMLCanvasElement): HTMLCanvasElement | null { + return null; // 子类可以重写 + } + + // 可选实现Canvas 2D方法 + protected applyCanvas2DEffect(canvas: HTMLCanvasElement): HTMLCanvasElement | null { + return null; // 子类可以重写 + } + + // 重写检查方法,使用更准确的检测 + protected supportsWebGL(): boolean { + return this.getShaderSources !== HybridEffectBase.prototype.getShaderSources && this.getShaderSources() !== null; + } + + protected supportsCanvas2D(): boolean { + return this.applyCanvas2DEffect !== HybridEffectBase.prototype.applyCanvas2DEffect; + } + + /** + * 重写渲染方法,支持用户配置的 useWebGL 控制 + */ + protected afterStageRender(stage: any, canvas: HTMLCanvasElement): HTMLCanvasElement | void | null | false { + let result: HTMLCanvasElement | null = null; + + // 根据用户配置决定渲染策略 + if (this.params?.options?.useWebGL !== false) { + // 用户允许使用WebGL,按照父类的自动判别逻辑 + // 优先尝试WebGL实现 + if (this.supportsWebGL()) { + if (!this.gl && !this.initWebGL(canvas)) { + console.warn('WebGL初始化失败,尝试Canvas 2D回退'); + } + + if (this.gl) { + result = this.applyWebGLEffect(canvas); + if (result) { + return result; + } + console.warn('WebGL特效执行失败,尝试Canvas 2D回退'); + } + } + + // WebGL不可用或执行失败,尝试Canvas 2D回退 + if (this.supportsCanvas2D()) { + result = this.applyCanvas2DEffect(canvas); + if (result) { + return result; + } + console.warn('Canvas 2D特效执行失败'); + } + } else { + // 用户禁用WebGL,直接使用Canvas 2D + if (this.supportsCanvas2D()) { + result = this.applyCanvas2DEffect(canvas); + if (result) { + return result; + } + console.warn('Canvas 2D特效执行失败'); + } else { + console.warn(`${this.constructor.name}: useWebGL=false 但未实现Canvas 2D方法`); + } + } + + // 如果都不支持或都失败了,给出明确的错误信息 + if (!this.supportsWebGL() && !this.supportsCanvas2D()) { + console.error( + `特效类 ${this.constructor.name} 未实现任何渲染方法。请实现 applyWebGLEffect 或 applyCanvas2DEffect 方法。` + ); + } + + // 返回原图作为最后的回退 + return canvas; + } +} diff --git a/packages/vrender-animate/src/custom/disappear/base/DisappearAnimateBase.ts b/packages/vrender-animate/src/custom/disappear/base/DisappearAnimateBase.ts new file mode 100644 index 000000000..150757763 --- /dev/null +++ b/packages/vrender-animate/src/custom/disappear/base/DisappearAnimateBase.ts @@ -0,0 +1,354 @@ +import type { EasingType } from '@visactor/vrender-core'; +import { vglobal } from '@visactor/vrender-core'; +import { AStageAnimate } from '../../custom-animate'; + +/** + * 特效动画基类,提取公共的WebGL和Canvas 2D操作 + */ +export abstract class DisappearAnimateBase extends AStageAnimate { + protected webglCanvas: HTMLCanvasElement | null = null; + protected gl: WebGLRenderingContext | null = null; + protected program: WebGLProgram | null = null; + protected currentAnimationRatio = 0; + protected animationTime = 0; + + constructor(from: null, to: null, duration: number, easing: EasingType, params: any) { + super(from, to, duration, easing, params); + } + + onUpdate(end: boolean, ratio: number, out: any): void { + super.onUpdate(end, ratio, out); + this.currentAnimationRatio = ratio; + this.animationTime = ratio * Math.PI * 2; + } + + /** + * 获取基于动画进度的时间 + */ + protected getAnimationTime(): number { + if (this.currentAnimationRatio > 0) { + return this.animationTime; + } + return Date.now() / 1000.0; + } + + /** + * 获取动画持续时间 + */ + protected getDurationFromParent(): number { + return this.duration || 1000; + } + + /** + * 初始化WebGL上下文 - WebGL公共逻辑 + */ + protected initWebGL(canvas: HTMLCanvasElement): boolean { + try { + this.webglCanvas = vglobal.createCanvas({ + width: canvas.width, + height: canvas.height, + dpr: vglobal.devicePixelRatio + }); + + if (!this.webglCanvas) { + console.warn('WebGL canvas creation failed'); + return false; + } + + this.webglCanvas.style.width = canvas.style.width || `${canvas.width}px`; + this.webglCanvas.style.height = canvas.style.height || `${canvas.height}px`; + + let glContext: WebGLRenderingContext | null = null; + try { + glContext = this.webglCanvas.getContext('webgl') as WebGLRenderingContext; + if (!glContext) { + glContext = this.webglCanvas.getContext('experimental-webgl') as WebGLRenderingContext; + } + } catch (e) { + console.warn('Failed to get WebGL context:', e); + } + + this.gl = glContext; + if (!this.gl) { + console.warn('WebGL not supported'); + return false; + } + + const shaders = this.getShaderSources(); + this.program = this.createShaderProgram(shaders.vertex, shaders.fragment); + return this.program !== null; + } catch (error) { + console.warn('Failed to initialize WebGL:', error); + return false; + } + } + + /** + * 创建着色器程序 - WebGL公共逻辑 + */ + protected createShaderProgram(vertexSource: string, fragmentSource: string): WebGLProgram | null { + if (!this.gl) { + return null; + } + + const vertexShader = this.createShader(this.gl.VERTEX_SHADER, vertexSource); + const fragmentShader = this.createShader(this.gl.FRAGMENT_SHADER, fragmentSource); + + if (!vertexShader || !fragmentShader) { + return null; + } + + const program = this.gl.createProgram(); + if (!program) { + return null; + } + + this.gl.attachShader(program, vertexShader); + this.gl.attachShader(program, fragmentShader); + this.gl.linkProgram(program); + + if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) { + console.error('Shader program link error:', this.gl.getProgramInfoLog(program)); + return null; + } + + return program; + } + + /** + * 创建着色器 - WebGL公共逻辑 + */ + protected createShader(type: number, source: string): WebGLShader | null { + if (!this.gl) { + return null; + } + + const shader = this.gl.createShader(type); + if (!shader) { + return null; + } + + this.gl.shaderSource(shader, source); + this.gl.compileShader(shader); + + if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) { + console.error('Shader compile error:', this.gl.getShaderInfoLog(shader)); + this.gl.deleteShader(shader); + return null; + } + + return shader; + } + + /** + * 设置WebGL视口和基本状态 - WebGL公共逻辑 + */ + protected setupWebGLState(canvas: HTMLCanvasElement): void { + if (!this.gl || !this.webglCanvas) { + return; + } + + // 确保WebGL canvas尺寸正确 + if (this.webglCanvas.width !== canvas.width || this.webglCanvas.height !== canvas.height) { + this.webglCanvas.width = canvas.width; + this.webglCanvas.height = canvas.height; + } + + this.gl.viewport(0, 0, this.webglCanvas.width, this.webglCanvas.height); + this.gl.clearColor(0.0, 0.0, 0.0, 0.0); + this.gl.clear(this.gl.COLOR_BUFFER_BIT); + } + + /** + * 创建标准的全屏四边形顶点缓冲区 - WebGL公共逻辑 + */ + protected createFullScreenQuad(): WebGLBuffer | null { + if (!this.gl) { + return null; + } + + const vertices = new Float32Array([ + // 位置 纹理坐标 + -1, + -1, + 0, + 1, // 左下角 -> 左上角纹理 + 1, + -1, + 1, + 1, // 右下角 -> 右上角纹理 + -1, + 1, + 0, + 0, // 左上角 -> 左下角纹理 + 1, + 1, + 1, + 0 // 右上角 -> 右下角纹理 + ]); + + const vertexBuffer = this.gl.createBuffer(); + this.gl.bindBuffer(this.gl.ARRAY_BUFFER, vertexBuffer); + this.gl.bufferData(this.gl.ARRAY_BUFFER, vertices, this.gl.STATIC_DRAW); + + return vertexBuffer; + } + + /** + * 创建纹理 - WebGL公共逻辑 + */ + protected createTextureFromCanvas(canvas: HTMLCanvasElement): WebGLTexture | null { + if (!this.gl) { + return null; + } + + const texture = this.gl.createTexture(); + this.gl.activeTexture(this.gl.TEXTURE0); // 激活纹理单元0 + this.gl.bindTexture(this.gl.TEXTURE_2D, texture); + this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, canvas); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR); + + return texture; + } + + /** + * 设置顶点属性 - WebGL公共逻辑 + */ + protected setupVertexAttributes(): void { + if (!this.gl || !this.program) { + return; + } + + const positionLocation = this.gl.getAttribLocation(this.program, 'a_position'); + const texCoordLocation = this.gl.getAttribLocation(this.program, 'a_texCoord'); + + this.gl.enableVertexAttribArray(positionLocation); + this.gl.vertexAttribPointer(positionLocation, 2, this.gl.FLOAT, false, 16, 0); + + this.gl.enableVertexAttribArray(texCoordLocation); + this.gl.vertexAttribPointer(texCoordLocation, 2, this.gl.FLOAT, false, 16, 8); + } + + /** + * 创建Canvas 2D输出画布 - Canvas 2D公共逻辑 + */ + protected createOutputCanvas( + canvas: HTMLCanvasElement + ): { canvas: HTMLCanvasElement; ctx: CanvasRenderingContext2D } | null { + const outputCanvas = vglobal.createCanvas({ + width: canvas.width, + height: canvas.height, + dpr: vglobal.devicePixelRatio + }); + const ctx = outputCanvas.getContext('2d'); + if (!ctx) { + return null; + } + + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.drawImage(canvas, 0, 0); + + return { canvas: outputCanvas, ctx }; + } + + // 可选的抽象方法,由子类选择性实现 + protected getShaderSources(): { vertex: string; fragment: string } | null { + // 默认返回null,表示不支持WebGL实现 + return null; + } + + protected applyWebGLEffect(canvas: HTMLCanvasElement): HTMLCanvasElement | null { + // 默认返回null,表示不支持WebGL实现 + return null; + } + + protected applyCanvas2DEffect(canvas: HTMLCanvasElement): HTMLCanvasElement | null { + // 默认返回null,表示不支持Canvas 2D实现 + return null; + } + + /** + * 检查是否支持WebGL实现 + */ + protected supportsWebGL(): boolean { + return this.getShaderSources() !== null; + } + + /** + * 检查是否支持Canvas 2D实现 + */ + protected supportsCanvas2D(): boolean { + // 通过尝试调用来检查是否有实现 + // 这里通过检查方法是否被重写来判断 + return this.applyCanvas2DEffect !== DisappearAnimateBase.prototype.applyCanvas2DEffect; + } + + /** + * 释放WebGL资源 + */ + release(): void { + super.release(); + + // 清理WebGL资源 + if (this.gl) { + // 删除着色器程序 + if (this.program) { + this.gl.deleteProgram(this.program); + this.program = null; + } + + // WebGL上下文会在canvas被垃圾回收时自动清理 + this.gl = null; + } + + // 清理WebGL canvas + if (this.webglCanvas) { + this.webglCanvas = null; + } + + // 重置动画状态 + this.currentAnimationRatio = 0; + this.animationTime = 0; + } + + protected afterStageRender(stage: any, canvas: HTMLCanvasElement): HTMLCanvasElement | void | null | false { + let result: HTMLCanvasElement | null = null; + + // 优先尝试WebGL实现 + if (this.supportsWebGL()) { + if (!this.gl && !this.initWebGL(canvas)) { + console.warn('WebGL初始化失败,尝试Canvas 2D回退'); + } + + if (this.gl) { + result = this.applyWebGLEffect(canvas); + if (result) { + return result; + } + console.warn('WebGL特效执行失败,尝试Canvas 2D回退'); + } + } + + // 尝试Canvas 2D实现 + if (this.supportsCanvas2D()) { + result = this.applyCanvas2DEffect(canvas); + if (result) { + return result; + } + console.warn('Canvas 2D特效执行失败'); + } + + // 如果都不支持或都失败了,给出明确的错误信息 + if (!this.supportsWebGL() && !this.supportsCanvas2D()) { + console.error( + `特效类 ${this.constructor.name} 未实现任何渲染方法。请实现 applyWebGLEffect 或 applyCanvas2DEffect 方法。` + ); + } + + // 返回原图作为最后的回退 + return canvas; + } +} diff --git a/packages/vrender-animate/src/custom/disappear/base/ImageProcessUtils.ts b/packages/vrender-animate/src/custom/disappear/base/ImageProcessUtils.ts new file mode 100644 index 000000000..1581db091 --- /dev/null +++ b/packages/vrender-animate/src/custom/disappear/base/ImageProcessUtils.ts @@ -0,0 +1,219 @@ +import { vglobal } from '@visactor/vrender-core'; + +/** + * 图像处理工具类,公共的图像处理逻辑 + */ +export class ImageProcessUtils { + /** + * 创建临时Canvas用于图像处理 + */ + static createTempCanvas(width: number, height: number, dpr?: number): HTMLCanvasElement { + return vglobal.createCanvas({ + width, + height, + dpr: dpr || vglobal.devicePixelRatio + }); + } + + /** + * 复制图像数据 + */ + static cloneImageData(imageData: ImageData): ImageData { + const clonedData = new Uint8ClampedArray(imageData.data); + return new ImageData(clonedData, imageData.width, imageData.height); + } + + /** + * 线性插值 + */ + static lerp(start: number, end: number, t: number): number { + return start * (1 - t) + end * t; + } + + /** + * 平滑步进函数 + */ + static smoothstep(edge0: number, edge1: number, x: number): number { + const t = Math.max(0, Math.min(1, (x - edge0) / (edge1 - edge0))); + return t * t * (3 - 2 * t); + } + + /** + * 计算两点之间的距离 + */ + static distance(x1: number, y1: number, x2: number, y2: number): number { + const dx = x2 - x1; + const dy = y2 - y1; + return Math.sqrt(dx * dx + dy * dy); + } + + /** + * 归一化角度到0-1范围 + */ + static normalizeAngle(angle: number): number { + return (angle + Math.PI) / (2 * Math.PI); + } + + /** + * 基于像素网格的噪声函数 + */ + static pixelNoise(x: number, y: number, pixelSize: number): number { + if (pixelSize <= 0) { + return 0; + } + + const gridX = Math.floor(x / pixelSize) * pixelSize; + const gridY = Math.floor(y / pixelSize) * pixelSize; + + const n = Math.sin(gridX * 12.9898 + gridY * 78.233) * 43758.5453; + return n - Math.floor(n); + } + + /** + * 生成噪声纹理数据 + */ + static generateNoiseTexture(width: number, height: number): Uint8Array { + const data = new Uint8Array(width * height); + for (let i = 0; i < data.length; i++) { + data[i] = Math.floor(Math.random() * 256); + } + return data; + } + + /** + * 应用CSS滤镜(如果支持) + */ + static applyCSSFilter(canvas: HTMLCanvasElement, filter: string): HTMLCanvasElement { + const outputCanvas = this.createTempCanvas(canvas.width, canvas.height); + const ctx = outputCanvas.getContext('2d'); + if (!ctx) { + return canvas; + } + + ctx.filter = filter; + ctx.drawImage(canvas, 0, 0); + ctx.filter = 'none'; + + return outputCanvas; + } + + /** + * 提取颜色通道 + */ + static extractChannel(imageData: ImageData, channelIndex: number): ImageData { + const { data, width, height } = imageData; + const channelData = new Uint8ClampedArray(data.length); + + for (let i = 0; i < data.length; i += 4) { + // 清空所有通道 + channelData[i] = 0; // R + channelData[i + 1] = 0; // G + channelData[i + 2] = 0; // B + channelData[i + 3] = data[i + 3]; // 保持Alpha通道 + + // 只保留指定通道的数据 + if (channelIndex >= 0 && channelIndex <= 2) { + channelData[i + channelIndex] = data[i + channelIndex]; + } + } + + return new ImageData(channelData, width, height); + } + + /** + * 混合两个图像数据 + */ + static blendImageData(imageData1: ImageData, imageData2: ImageData, ratio: number): ImageData { + const { data: data1, width, height } = imageData1; + const { data: data2 } = imageData2; + const result = new Uint8ClampedArray(data1.length); + + for (let i = 0; i < data1.length; i += 4) { + result[i] = Math.round(this.lerp(data1[i], data2[i], ratio)); // R + result[i + 1] = Math.round(this.lerp(data1[i + 1], data2[i + 1], ratio)); // G + result[i + 2] = Math.round(this.lerp(data1[i + 2], data2[i + 2], ratio)); // B + result[i + 3] = Math.round(this.lerp(data1[i + 3], data2[i + 3], ratio)); // A + } + + return new ImageData(result, width, height); + } + + /** + * 计算像素亮度 + */ + static getLuminance(r: number, g: number, b: number): number { + return r * 0.299 + g * 0.587 + b * 0.114; + } + + /** + * 应用褐色调效果 + */ + static applySepiaToPixel(r: number, g: number, b: number): [number, number, number] { + const sepiaR = Math.min(255, r * 0.393 + g * 0.769 + b * 0.189); + const sepiaG = Math.min(255, r * 0.349 + g * 0.686 + b * 0.168); + const sepiaB = Math.min(255, r * 0.272 + g * 0.534 + b * 0.131); + return [sepiaR, sepiaG, sepiaB]; + } + + /** + * 动态计算强度(基于动画时间的线性增长) + */ + static calculateDynamicStrength(baseStrength: number, animationTime: number): number { + // 时间范围0-2π,标准化到0-1 + return baseStrength * (animationTime / (Math.PI * 2)); + } +} + +/** + * WebGL着色器片段库 + */ +export class ShaderLibrary { + /** + * 标准顶点着色器 + */ + static readonly STANDARD_VERTEX_SHADER = ` + attribute vec2 a_position; + attribute vec2 a_texCoord; + varying vec2 v_texCoord; + + void main() { + gl_Position = vec4(a_position, 0.0, 1.0); + v_texCoord = a_texCoord; + } + `; + + /** + * 常用的着色器函数库 + */ + static readonly SHADER_FUNCTIONS = ` + // 亮度计算函数 + float luminance(vec3 color) { + return dot(color, vec3(0.299, 0.587, 0.114)); + } + + // 褐色调函数 + vec3 sepia(vec3 color) { + float r = color.r * 0.393 + color.g * 0.769 + color.b * 0.189; + float g = color.r * 0.349 + color.g * 0.686 + color.b * 0.168; + float b = color.r * 0.272 + color.g * 0.534 + color.b * 0.131; + return vec3(r, g, b); + } + + // 线性插值函数 + float lerp(float a, float b, float t) { + return a * (1.0 - t) + b * t; + } + + + // 简单噪声函数 + float pixelNoise(vec2 coord, float pixelSize) { + vec2 gridCoord = floor(coord / pixelSize) * pixelSize; + return fract(sin(dot(gridCoord, vec2(12.9898, 78.233))) * 43758.5453123); + } + + // 动态强度计算 + float calculateDynamicStrength(float baseStrength, float time) { + return baseStrength * (time / 6.28318531); // 2π + } + `; +} diff --git a/packages/vrender-animate/src/custom/disappear/dissolve.ts b/packages/vrender-animate/src/custom/disappear/dissolve.ts new file mode 100644 index 000000000..65cd29c61 --- /dev/null +++ b/packages/vrender-animate/src/custom/disappear/dissolve.ts @@ -0,0 +1,825 @@ +import type { EasingType } from '@visactor/vrender-core'; +import { HybridEffectBase } from './base/CustomEffectBase'; +import { ImageProcessUtils, ShaderLibrary } from './base/ImageProcessUtils'; + +// 向外溶解效果配置接口 +export interface DissolveConfig { + dissolveType?: 'outward' | 'inward' | 'radial' | 'leftToRight' | 'rightToLeft' | 'topToBottom' | 'bottomToTop'; // 溶解效果类型 + useWebGL?: boolean; // 是否使用WebGL实现 + noiseScale?: number; // 溶解颗粒大小(像素值,0为平滑溶解,1-20为颗粒溶解) + fadeEdge?: boolean; // 是否启用边缘渐变 +} + +/** + * 溶解效果类 - 使用HybridEffectBase重构 + * 支持多种溶解模式:向外、向内、径向、方向性溶解等 + */ +export class Dissolve extends HybridEffectBase { + // 溶解配置,参数验证并设置默认值 + private dissolveConfig: Required; + + // WebGL噪声纹理缓存 + private noiseData: Uint8Array | null = null; + + constructor(from: null, to: null, duration: number, easing: EasingType, params: any) { + super(from, to, duration, easing, params); + + // 初始化溶解配置,使用传入的参数或默认值,并进行参数验证 + const rawNoiseScale = params?.options?.noiseScale; + const clampedNoiseScale = rawNoiseScale !== undefined ? Math.max(0, Math.floor(rawNoiseScale)) : 8; + + this.dissolveConfig = { + dissolveType: params?.options?.dissolveType || 'outward', + useWebGL: params?.options?.useWebGL !== undefined ? params.options.useWebGL : true, + noiseScale: clampedNoiseScale, // 确保是非负整数,默认8px颗粒,0为平滑 + fadeEdge: params?.options?.fadeEdge !== undefined ? params.options.fadeEdge : true + }; + } + + /** + * WebGL着色器源码 + */ + protected getShaderSources(): { vertex: string; fragment: string } { + const vertexShader = ShaderLibrary.STANDARD_VERTEX_SHADER; + + const fragmentShader = ` + precision mediump float; + uniform sampler2D u_texture; + uniform sampler2D u_noiseTexture; + uniform float u_time; + uniform int u_dissolveType; + uniform vec2 u_resolution; + uniform float u_noiseScale; + uniform bool u_fadeEdge; + varying vec2 v_texCoord; + + ${ShaderLibrary.SHADER_FUNCTIONS} + + // 向外溶解函数 + float outwardDissolve(vec2 uv, float time, float pixelSize, vec2 resolution) { + vec2 center = vec2(0.5, 0.5); + float distFromCenter = length(uv - center); + float maxDist = length(vec2(0.5, 0.5)); + + // 归一化距离 (0为中心,1为边缘) + float normalizedDist = distFromCenter / maxDist; + + // 向外溶解:从边缘开始溶解,time控制溶解进度 + // 增加安全边距,确保动画结束时完全溶解 + float edgeThreshold = 1.2 - time * 1.5; + + // 当pixelSize > 0时添加颗粒效果 + if (pixelSize > 0.0) { + // 添加基于像素大小的噪声,让边缘呈现颗粒状 + vec2 pixelCoord = uv * resolution; // 转换为像素坐标 + float noiseValue = pixelNoise(pixelCoord, pixelSize); + float noiseInfluence = (noiseValue - 0.5) * 0.4; // 增强噪声影响 + edgeThreshold += noiseInfluence; + return normalizedDist > edgeThreshold ? 0.0 : 1.0; + } else { + // 平滑溶解:根据fadeEdge决定是否使用渐变 + if (u_fadeEdge) { + // 柔和边缘:返回渐变值 + float fadeWidth = 0.15; // 渐变宽度 + return 1.0 - smoothstep(edgeThreshold - fadeWidth, edgeThreshold, normalizedDist); + } else { + // 硬边缘:返回0或1 + return normalizedDist > edgeThreshold ? 0.0 : 1.0; + } + } + } + + // 向内溶解函数 + float inwardDissolve(vec2 uv, float time, float pixelSize, vec2 resolution) { + vec2 center = vec2(0.5, 0.5); + float distFromCenter = length(uv - center); + float maxDist = length(vec2(0.5, 0.5)); + + float normalizedDist = distFromCenter / maxDist; + + // 向内溶解:从中心开始溶解,time控制溶解进度 + // 增加系数,确保动画结束时完全溶解 + float centerThreshold = time * 1.4; + + // 当pixelSize > 0时添加颗粒效果 + if (pixelSize > 0.0) { + vec2 pixelCoord = uv * resolution; + float noiseValue = pixelNoise(pixelCoord, pixelSize); + float noiseInfluence = (noiseValue - 0.5) * 0.4; + centerThreshold += noiseInfluence; + return normalizedDist < centerThreshold ? 0.0 : 1.0; + } else { + // 平滑溶解:根据fadeEdge决定是否使用渐变 + if (u_fadeEdge) { + // 柔和边缘:返回渐变值 + float fadeWidth = 0.15; // 渐变宽度 + return smoothstep(centerThreshold, centerThreshold + fadeWidth, normalizedDist); + } else { + // 硬边缘:返回0或1 + return normalizedDist < centerThreshold ? 0.0 : 1.0; + } + } + } + + // 径向溶解函数 + float radialDissolve(vec2 uv, float time, float pixelSize, vec2 resolution) { + vec2 center = vec2(0.5, 0.5); + float angle = atan(uv.y - center.y, uv.x - center.x); + float normalizedAngle = (angle + 3.14159) / (2.0 * 3.14159); + + // 径向溶解:按角度顺序溶解,time控制溶解进度 + // 增加系数,确保动画结束时完全溶解 + float angleThreshold = time * 1.2; + + // 当pixelSize > 0时添加颗粒效果 + if (pixelSize > 0.0) { + vec2 pixelCoord = uv * resolution; + float noiseValue = pixelNoise(pixelCoord, pixelSize); + float noiseInfluence = (noiseValue - 0.5) * 0.3; + angleThreshold += noiseInfluence; + return normalizedAngle < angleThreshold ? 0.0 : 1.0; + } else { + // 平滑溶解:根据fadeEdge决定是否使用渐变 + if (u_fadeEdge) { + // 柔和边缘:返回渐变值 + float fadeWidth = 0.08; // 渐变宽度 + return smoothstep(angleThreshold, angleThreshold + fadeWidth, normalizedAngle); + } else { + // 硬边缘:返回0或1 + return normalizedAngle < angleThreshold ? 0.0 : 1.0; + } + } + } + + // 从左到右溶解函数 + float leftToRightDissolve(vec2 uv, float time, float pixelSize, vec2 resolution) { + // 左到右溶解:从x=0开始向x=1溶解 + float dissolvePosition = time * 1.2; // 增加系数确保完全溶解 + + // 当pixelSize > 0时添加颗粒效果 + if (pixelSize > 0.0) { + vec2 pixelCoord = uv * resolution; + float noiseValue = pixelNoise(pixelCoord, pixelSize); + float noiseInfluence = (noiseValue - 0.5) * 0.3; + dissolvePosition += noiseInfluence; + return uv.x < dissolvePosition ? 0.0 : 1.0; + } else { + // 平滑溶解:根据fadeEdge决定是否使用渐变 + if (u_fadeEdge) { + // 柔和边缘:返回渐变值 + float fadeWidth = 0.08; // 渐变宽度 + return smoothstep(dissolvePosition, dissolvePosition + fadeWidth, uv.x); + } else { + // 硬边缘:返回0或1 + return uv.x < dissolvePosition ? 0.0 : 1.0; + } + } + } + + // 从右到左溶解函数 + float rightToLeftDissolve(vec2 uv, float time, float pixelSize, vec2 resolution) { + // 右到左溶解:从x=1开始向x=0溶解 + float dissolvePosition = 1.0 - time * 1.2; // 增加系数确保完全溶解 + + // 当pixelSize > 0时添加颗粒效果 + if (pixelSize > 0.0) { + vec2 pixelCoord = uv * resolution; + float noiseValue = pixelNoise(pixelCoord, pixelSize); + float noiseInfluence = (noiseValue - 0.5) * 0.3; + dissolvePosition += noiseInfluence; + return uv.x > dissolvePosition ? 0.0 : 1.0; + } else { + // 平滑溶解:根据fadeEdge决定是否使用渐变 + if (u_fadeEdge) { + // 柔和边缘:返回渐变值 + float fadeWidth = 0.08; // 渐变宽度 + return smoothstep(dissolvePosition - fadeWidth, dissolvePosition, uv.x); + } else { + // 硬边缘:返回0或1 + return uv.x > dissolvePosition ? 0.0 : 1.0; + } + } + } + + // 从上到下溶解函数 + float topToBottomDissolve(vec2 uv, float time, float pixelSize, vec2 resolution) { + // 上到下溶解:从y=0开始向y=1溶解 + float dissolvePosition = time * 1.2; // 增加系数确保完全溶解 + + // 当pixelSize > 0时添加颗粒效果 + if (pixelSize > 0.0) { + vec2 pixelCoord = uv * resolution; + float noiseValue = pixelNoise(pixelCoord, pixelSize); + float noiseInfluence = (noiseValue - 0.5) * 0.3; + dissolvePosition += noiseInfluence; + return uv.y < dissolvePosition ? 0.0 : 1.0; + } else { + // 平滑溶解:根据fadeEdge决定是否使用渐变 + if (u_fadeEdge) { + // 柔和边缘:返回渐变值 + float fadeWidth = 0.08; // 渐变宽度 + return smoothstep(dissolvePosition, dissolvePosition + fadeWidth, uv.y); + } else { + // 硬边缘:返回0或1 + return uv.y < dissolvePosition ? 0.0 : 1.0; + } + } + } + + // 从下到上溶解函数 + float bottomToTopDissolve(vec2 uv, float time, float pixelSize, vec2 resolution) { + // 下到上溶解:从y=1开始向y=0溶解 + float dissolvePosition = 1.0 - time * 1.2; // 增加系数确保完全溶解 + + // 当pixelSize > 0时添加颗粒效果 + if (pixelSize > 0.0) { + vec2 pixelCoord = uv * resolution; + float noiseValue = pixelNoise(pixelCoord, pixelSize); + float noiseInfluence = (noiseValue - 0.5) * 0.3; + dissolvePosition += noiseInfluence; + return uv.y > dissolvePosition ? 0.0 : 1.0; + } else { + // 平滑溶解:根据fadeEdge决定是否使用渐变 + if (u_fadeEdge) { + // 柔和边缘:返回渐变值 + float fadeWidth = 0.08; // 渐变宽度 + return smoothstep(dissolvePosition - fadeWidth, dissolvePosition, uv.y); + } else { + // 硬边缘:返回0或1 + return uv.y > dissolvePosition ? 0.0 : 1.0; + } + } + } + + void main() { + vec2 uv = v_texCoord; + vec4 texColor = texture2D(u_texture, uv); + + float alpha = 1.0; + + // 根据溶解类型选择对应的溶解函数 + if (u_dissolveType == 0) { + alpha = outwardDissolve(uv, u_time, u_noiseScale, u_resolution); + } else if (u_dissolveType == 1) { + alpha = inwardDissolve(uv, u_time, u_noiseScale, u_resolution); + } else if (u_dissolveType == 2) { + alpha = radialDissolve(uv, u_time, u_noiseScale, u_resolution); + } else if (u_dissolveType == 3) { + alpha = leftToRightDissolve(uv, u_time, u_noiseScale, u_resolution); + } else if (u_dissolveType == 4) { + alpha = rightToLeftDissolve(uv, u_time, u_noiseScale, u_resolution); + } else if (u_dissolveType == 5) { + alpha = topToBottomDissolve(uv, u_time, u_noiseScale, u_resolution); + } else if (u_dissolveType == 6) { + alpha = bottomToTopDissolve(uv, u_time, u_noiseScale, u_resolution); + } + + gl_FragColor = vec4(texColor.rgb, texColor.a * alpha); + } + `; + + return { vertex: vertexShader, fragment: fragmentShader }; + } + + /** + * WebGL溶解效果实现 + */ + protected applyWebGLEffect(canvas: HTMLCanvasElement): HTMLCanvasElement { + if (!this.gl || !this.program || !this.webglCanvas) { + return canvas; + } + + // 设置WebGL状态 + this.setupWebGLState(canvas); + + // 创建主纹理 + const texture = this.createTextureFromCanvas(canvas); + if (!texture) { + return canvas; + } + + // 创建噪声纹理 + if (!this.noiseData) { + this.noiseData = ImageProcessUtils.generateNoiseTexture(256, 256); + } + const noiseTexture = this.gl.createTexture(); + this.gl.activeTexture(this.gl.TEXTURE1); + this.gl.bindTexture(this.gl.TEXTURE_2D, noiseTexture); + this.gl.texImage2D( + this.gl.TEXTURE_2D, + 0, + this.gl.LUMINANCE, + 256, + 256, + 0, + this.gl.LUMINANCE, + this.gl.UNSIGNED_BYTE, + this.noiseData + ); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.REPEAT); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.REPEAT); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR); + + // 创建顶点缓冲区 + const vertexBuffer = this.createFullScreenQuad(); + if (!vertexBuffer) { + return canvas; + } + + // 使用着色器程序并设置属性 + this.gl.useProgram(this.program); + this.setupVertexAttributes(); + + // 设置uniform变量 + this.setUniforms(); + + // 启用混合以支持透明度 + this.gl.enable(this.gl.BLEND); + this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA); + + // 绘制 + this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4); + + // 清理资源 + this.gl.deleteTexture(texture); + this.gl.deleteTexture(noiseTexture); + this.gl.deleteBuffer(vertexBuffer); + + return this.webglCanvas; + } + + /** + * 设置WebGL uniform变量 + */ + private setUniforms(): void { + if (!this.gl || !this.program || !this.webglCanvas) { + return; + } + + const textureLocation = this.gl.getUniformLocation(this.program, 'u_texture'); + const noiseTextureLocation = this.gl.getUniformLocation(this.program, 'u_noiseTexture'); + const timeLocation = this.gl.getUniformLocation(this.program, 'u_time'); + const dissolveTypeLocation = this.gl.getUniformLocation(this.program, 'u_dissolveType'); + const resolutionLocation = this.gl.getUniformLocation(this.program, 'u_resolution'); + const noiseScaleLocation = this.gl.getUniformLocation(this.program, 'u_noiseScale'); + const fadeEdgeLocation = this.gl.getUniformLocation(this.program, 'u_fadeEdge'); + + this.gl.uniform1i(textureLocation, 0); + this.gl.uniform1i(noiseTextureLocation, 1); + this.gl.uniform1f(timeLocation, this.currentAnimationRatio); + this.gl.uniform2f(resolutionLocation, this.webglCanvas.width, this.webglCanvas.height); + this.gl.uniform1f(noiseScaleLocation, this.dissolveConfig.noiseScale); + this.gl.uniform1i(fadeEdgeLocation, this.dissolveConfig.fadeEdge ? 1 : 0); + + // 设置溶解类型映射 + const dissolveTypeMap: { [key: string]: number } = { + outward: 0, + inward: 1, + radial: 2, + leftToRight: 3, + rightToLeft: 4, + topToBottom: 5, + bottomToTop: 6 + }; + this.gl.uniform1i(dissolveTypeLocation, dissolveTypeMap[this.dissolveConfig.dissolveType] || 0); + } + + /** + * Canvas 2D溶解效果实现 + */ + protected applyCanvas2DEffect(canvas: HTMLCanvasElement): HTMLCanvasElement { + const outputCanvas = this.createOutputCanvas(canvas); + if (!outputCanvas) { + return canvas; + } + + const { canvas: outputCanvasElement, ctx } = outputCanvas; + + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const progress = this.currentAnimationRatio; + + let dissolvedImageData: ImageData; + + // 根据溶解类型应用不同的溶解算法 + switch (this.dissolveConfig.dissolveType) { + case 'outward': + dissolvedImageData = this.applyOutwardDissolve(imageData, progress); + break; + case 'inward': + dissolvedImageData = this.applyInwardDissolve(imageData, progress); + break; + case 'radial': + dissolvedImageData = this.applyRadialDissolve(imageData, progress); + break; + case 'leftToRight': + dissolvedImageData = this.applyLeftToRightDissolve(imageData, progress); + break; + case 'rightToLeft': + dissolvedImageData = this.applyRightToLeftDissolve(imageData, progress); + break; + case 'topToBottom': + dissolvedImageData = this.applyTopToBottomDissolve(imageData, progress); + break; + case 'bottomToTop': + dissolvedImageData = this.applyBottomToTopDissolve(imageData, progress); + break; + default: + dissolvedImageData = imageData; + } + + ctx.putImageData(dissolvedImageData, 0, 0); + return outputCanvasElement; + } + + // Canvas 2D 实现 - 向外溶解 + private applyOutwardDissolve(imageData: ImageData, progress: number): ImageData { + const { data, width, height } = imageData; + const result = new Uint8ClampedArray(data.length); + result.set(data); + + const centerX = width / 2; + const centerY = height / 2; + const maxDist = Math.sqrt(centerX * centerX + centerY * centerY); + + // 直接使用noiseScale作为像素颗粒大小 + const pixelSize = this.dissolveConfig.noiseScale; + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const dx = x - centerX; + const dy = y - centerY; + const distFromCenter = Math.sqrt(dx * dx + dy * dy); + const normalizedDist = distFromCenter / maxDist; + + // 向外溶解:从边缘开始,增加安全边距确保完全溶解 + let dissolveThreshold = 1.2 - progress * 1.4; + let alpha = 1.0; + + if (pixelSize > 0) { + // 颗粒效果:使用基于像素网格的噪声,产生颗粒状效果 + const noiseValue = ImageProcessUtils.pixelNoise(x, y, pixelSize); + const noiseInfluence = (noiseValue - 0.5) * 0.4; // 增强噪声影响 + dissolveThreshold += noiseInfluence; + + alpha = normalizedDist > dissolveThreshold ? 0.0 : 1.0; + } else { + // 平滑溶解:根据fadeEdge决定是否使用渐变 + if (this.dissolveConfig.fadeEdge) { + // 柔和边缘:使用渐变值 + const fadeWidth = 0.15; // 渐变宽度 + const fadeStart = dissolveThreshold - fadeWidth; + const fadeEnd = dissolveThreshold; + + if (normalizedDist < fadeStart) { + alpha = 1.0; + } else if (normalizedDist > fadeEnd) { + alpha = 0.0; + } else { + // 线性插值产生渐变 + alpha = 1.0 - (normalizedDist - fadeStart) / (fadeEnd - fadeStart); + } + } else { + // 硬边缘:使用0或1 + alpha = normalizedDist > dissolveThreshold ? 0.0 : 1.0; + } + } + + const index = (y * width + x) * 4; + result[index + 3] = Math.floor(result[index + 3] * alpha); + } + } + + return new ImageData(result, width, height); + } + + // Canvas 2D 实现 - 向内溶解 + private applyInwardDissolve(imageData: ImageData, progress: number): ImageData { + const { data, width, height } = imageData; + const result = new Uint8ClampedArray(data.length); + result.set(data); + + const centerX = width / 2; + const centerY = height / 2; + const maxDist = Math.sqrt(centerX * centerX + centerY * centerY); + + const pixelSize = this.dissolveConfig.noiseScale; + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const dx = x - centerX; + const dy = y - centerY; + const distFromCenter = Math.sqrt(dx * dx + dy * dy); + const normalizedDist = distFromCenter / maxDist; + + // 向内溶解:从中心开始,增加系数确保完全溶解 + let dissolveThreshold = progress * 1.4; + let alpha = 1.0; + + if (pixelSize > 0) { + // 颗粒效果 + const noiseValue = ImageProcessUtils.pixelNoise(x, y, pixelSize); + const noiseInfluence = (noiseValue - 0.5) * 0.4; + dissolveThreshold += noiseInfluence; + + alpha = normalizedDist < dissolveThreshold ? 0.0 : 1.0; + } else { + // 平滑溶解:根据fadeEdge决定是否使用渐变 + if (this.dissolveConfig.fadeEdge) { + // 柔和边缘:使用渐变值 + const fadeWidth = 0.15; // 渐变宽度 + const fadeStart = dissolveThreshold; + const fadeEnd = dissolveThreshold + fadeWidth; + + if (normalizedDist < fadeStart) { + alpha = 0.0; + } else if (normalizedDist > fadeEnd) { + alpha = 1.0; + } else { + // 线性插值产生渐变 + alpha = (normalizedDist - fadeStart) / (fadeEnd - fadeStart); + } + } else { + // 硬边缘:使用0或1 + alpha = normalizedDist < dissolveThreshold ? 0.0 : 1.0; + } + } + + const index = (y * width + x) * 4; + result[index + 3] = Math.floor(result[index + 3] * alpha); + } + } + + return new ImageData(result, width, height); + } + + // Canvas 2D 实现 - 径向溶解 + private applyRadialDissolve(imageData: ImageData, progress: number): ImageData { + const { data, width, height } = imageData; + const result = new Uint8ClampedArray(data.length); + result.set(data); + + const centerX = width / 2; + const centerY = height / 2; + const pixelSize = this.dissolveConfig.noiseScale; + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const dx = x - centerX; + const dy = y - centerY; + const angle = Math.atan2(dy, dx); + const normalizedAngle = (angle + Math.PI) / (2 * Math.PI); + + // 径向溶解:按角度顺序,增加系数确保完全溶解 + let dissolveThreshold = progress * 1.2; + let alpha = 1.0; + + if (pixelSize > 0) { + // 颗粒效果 + const noiseValue = ImageProcessUtils.pixelNoise(x, y, pixelSize); + const noiseInfluence = (noiseValue - 0.5) * 0.3; + dissolveThreshold += noiseInfluence; + + alpha = normalizedAngle < dissolveThreshold ? 0.0 : 1.0; + } else { + // 平滑溶解:根据fadeEdge决定是否使用渐变 + if (this.dissolveConfig.fadeEdge) { + // 柔和边缘:使用渐变值 + const fadeWidth = 0.08; // 渐变宽度 + const fadeStart = dissolveThreshold; + const fadeEnd = dissolveThreshold + fadeWidth; + + if (normalizedAngle < fadeStart) { + alpha = 0.0; + } else if (normalizedAngle > fadeEnd) { + alpha = 1.0; + } else { + // 线性插值产生渐变 + alpha = (normalizedAngle - fadeStart) / (fadeEnd - fadeStart); + } + } else { + // 硬边缘:使用0或1 + alpha = normalizedAngle < dissolveThreshold ? 0.0 : 1.0; + } + } + + const index = (y * width + x) * 4; + result[index + 3] = Math.floor(result[index + 3] * alpha); + } + } + + return new ImageData(result, width, height); + } + + // Canvas 2D 实现 - 从左到右溶解 + private applyLeftToRightDissolve(imageData: ImageData, progress: number): ImageData { + const { data, width, height } = imageData; + const result = new Uint8ClampedArray(data.length); + result.set(data); + + const pixelSize = this.dissolveConfig.noiseScale; + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const normalizedX = x / width; + + // 从左到右溶解:增加系数确保完全溶解 + let dissolveThreshold = progress * 1.2; + let alpha = 1.0; + + if (pixelSize > 0) { + // 颗粒效果 + const noiseValue = ImageProcessUtils.pixelNoise(x, y, pixelSize); + const noiseInfluence = (noiseValue - 0.5) * 0.3; + dissolveThreshold += noiseInfluence; + + alpha = normalizedX < dissolveThreshold ? 0.0 : 1.0; + } else { + // 平滑溶解:根据fadeEdge决定是否使用渐变 + if (this.dissolveConfig.fadeEdge) { + // 柔和边缘:使用渐变值 + const fadeWidth = 0.08; // 渐变宽度 + const fadeStart = dissolveThreshold; + const fadeEnd = dissolveThreshold + fadeWidth; + + if (normalizedX < fadeStart) { + alpha = 0.0; + } else if (normalizedX > fadeEnd) { + alpha = 1.0; + } else { + // 线性插值产生渐变 + alpha = (normalizedX - fadeStart) / (fadeEnd - fadeStart); + } + } else { + // 硬边缘:使用0或1 + alpha = normalizedX < dissolveThreshold ? 0.0 : 1.0; + } + } + + const index = (y * width + x) * 4; + result[index + 3] = Math.floor(result[index + 3] * alpha); + } + } + + return new ImageData(result, width, height); + } + + // Canvas 2D 实现 - 从右到左溶解 + private applyRightToLeftDissolve(imageData: ImageData, progress: number): ImageData { + const { data, width, height } = imageData; + const result = new Uint8ClampedArray(data.length); + result.set(data); + + const pixelSize = this.dissolveConfig.noiseScale; + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const normalizedX = x / width; + + // 从右到左溶解:增加系数确保完全溶解 + let dissolveThreshold = 1.0 - progress * 1.2; + let alpha = 1.0; + + if (pixelSize > 0) { + // 颗粒效果 + const noiseValue = ImageProcessUtils.pixelNoise(x, y, pixelSize); + const noiseInfluence = (noiseValue - 0.5) * 0.3; + dissolveThreshold += noiseInfluence; + + alpha = normalizedX > dissolveThreshold ? 0.0 : 1.0; + } else { + // 平滑溶解:根据fadeEdge决定是否使用渐变 + if (this.dissolveConfig.fadeEdge) { + // 柔和边缘:使用渐变值 + const fadeWidth = 0.08; // 渐变宽度 + const fadeStart = dissolveThreshold - fadeWidth; + const fadeEnd = dissolveThreshold; + + if (normalizedX < fadeStart) { + alpha = 1.0; + } else if (normalizedX > fadeEnd) { + alpha = 0.0; + } else { + // 线性插值产生渐变 + alpha = 1.0 - (normalizedX - fadeStart) / (fadeEnd - fadeStart); + } + } else { + // 硬边缘:使用0或1 + alpha = normalizedX > dissolveThreshold ? 0.0 : 1.0; + } + } + + const index = (y * width + x) * 4; + result[index + 3] = Math.floor(result[index + 3] * alpha); + } + } + + return new ImageData(result, width, height); + } + + // Canvas 2D 实现 - 从上到下溶解 + private applyTopToBottomDissolve(imageData: ImageData, progress: number): ImageData { + const { data, width, height } = imageData; + const result = new Uint8ClampedArray(data.length); + result.set(data); + + const pixelSize = this.dissolveConfig.noiseScale; + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const normalizedY = y / height; + + // 从上到下溶解:增加系数确保完全溶解 + let dissolveThreshold = progress * 1.2; + let alpha = 1.0; + + if (pixelSize > 0) { + // 颗粒效果 + const noiseValue = ImageProcessUtils.pixelNoise(x, y, pixelSize); + const noiseInfluence = (noiseValue - 0.5) * 0.3; + dissolveThreshold += noiseInfluence; + + alpha = normalizedY < dissolveThreshold ? 0.0 : 1.0; + } else { + // 平滑溶解:根据fadeEdge决定是否使用渐变 + if (this.dissolveConfig.fadeEdge) { + // 柔和边缘:使用渐变值 + const fadeWidth = 0.08; // 渐变宽度 + const fadeStart = dissolveThreshold; + const fadeEnd = dissolveThreshold + fadeWidth; + + if (normalizedY < fadeStart) { + alpha = 0.0; + } else if (normalizedY > fadeEnd) { + alpha = 1.0; + } else { + // 线性插值产生渐变 + alpha = (normalizedY - fadeStart) / (fadeEnd - fadeStart); + } + } else { + // 硬边缘:使用0或1 + alpha = normalizedY < dissolveThreshold ? 0.0 : 1.0; + } + } + + const index = (y * width + x) * 4; + result[index + 3] = Math.floor(result[index + 3] * alpha); + } + } + + return new ImageData(result, width, height); + } + + // Canvas 2D 实现 - 从下到上溶解 + private applyBottomToTopDissolve(imageData: ImageData, progress: number): ImageData { + const { data, width, height } = imageData; + const result = new Uint8ClampedArray(data.length); + result.set(data); + + const pixelSize = this.dissolveConfig.noiseScale; + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const normalizedY = y / height; + + // 从下到上溶解:增加系数确保完全溶解 + let dissolveThreshold = 1.0 - progress * 1.2; + let alpha = 1.0; + + if (pixelSize > 0) { + // 颗粒效果 + const noiseValue = ImageProcessUtils.pixelNoise(x, y, pixelSize); + const noiseInfluence = (noiseValue - 0.5) * 0.3; + dissolveThreshold += noiseInfluence; + + alpha = normalizedY > dissolveThreshold ? 0.0 : 1.0; + } else { + // 平滑溶解:根据fadeEdge决定是否使用渐变 + if (this.dissolveConfig.fadeEdge) { + // 柔和边缘:使用渐变值 + const fadeWidth = 0.08; // 渐变宽度 + const fadeStart = dissolveThreshold - fadeWidth; + const fadeEnd = dissolveThreshold; + + if (normalizedY < fadeStart) { + alpha = 1.0; + } else if (normalizedY > fadeEnd) { + alpha = 0.0; + } else { + // 线性插值产生渐变 + alpha = 1.0 - (normalizedY - fadeStart) / (fadeEnd - fadeStart); + } + } else { + // 硬边缘:使用0或1 + alpha = normalizedY > dissolveThreshold ? 0.0 : 1.0; + } + } + + const index = (y * width + x) * 4; + result[index + 3] = Math.floor(result[index + 3] * alpha); + } + } + + return new ImageData(result, width, height); + } +} diff --git a/packages/vrender-animate/src/custom/disappear/distortion.ts b/packages/vrender-animate/src/custom/disappear/distortion.ts new file mode 100644 index 000000000..c4961989e --- /dev/null +++ b/packages/vrender-animate/src/custom/disappear/distortion.ts @@ -0,0 +1,342 @@ +import type { EasingType } from '@visactor/vrender-core'; +import { HybridEffectBase } from './base/CustomEffectBase'; + +// 扭曲效果配置接口 +export interface DistortionConfig { + distortionType?: 'wave' | 'ripple' | 'swirl'; // 扭曲效果类型 + strength?: number; // 扭曲强度 + useWebGL?: boolean; // 是否使用WebGL实现 +} +/** + * 扭曲消失动画效果 - 重构版 + * 使用HybridEffectBase实现WebGL和Canvas 2D双重支持 + */ +export class Distortion extends HybridEffectBase { + private distortionConfig: Partial; + + constructor(from: null, to: null, duration: number, easing: EasingType, params: any) { + super(from, to, duration, easing, params); + + this.distortionConfig = { + distortionType: params?.options?.distortionType || 'wave', + strength: params?.options?.strength || 0.3, + useWebGL: params?.options?.useWebGL !== undefined ? params.options.useWebGL : true + }; + } + + /** + * WebGL实现:着色器扭曲效果 + */ + protected getShaderSources(): { vertex: string; fragment: string } | null { + const vertexShader = ` + attribute vec2 a_position; + attribute vec2 a_texCoord; + varying vec2 v_texCoord; + + void main() { + gl_Position = vec4(a_position, 0.0, 1.0); + v_texCoord = a_texCoord; + } + `; + + const fragmentShader = ` + precision mediump float; + uniform sampler2D u_texture; + uniform float u_time; + uniform float u_strength; + uniform int u_distortionType; + uniform vec2 u_resolution; + varying vec2 v_texCoord; + + // 波浪扭曲函数 + vec2 wave(vec2 uv, float time, float strength) { + float waveX = sin(uv.y * 10.0 + time * 3.0) * strength * 0.1; + float waveY = sin(uv.x * 10.0 + time * 2.0) * strength * 0.1; + return uv + vec2(waveX, waveY); + } + + // 涟漪扭曲函数 + vec2 ripple(vec2 uv, float time, float strength) { + vec2 center = vec2(0.5, 0.5); + float distance = length(uv - center); + float ripple = sin(distance * 20.0 - time * 5.0) * strength * 0.1; + vec2 direction = normalize(uv - center); + return uv + direction * ripple; + } + + // 漩涡扭曲函数 + vec2 swirl(vec2 uv, float time, float strength) { + vec2 center = vec2(0.5, 0.5); + vec2 delta = uv - center; + float dist = length(delta); + float originalAngle = atan(delta.y, delta.x); + float rotationAngle = dist * strength * time * 2.0; + float finalAngle = originalAngle + rotationAngle; + return center + dist * vec2(cos(finalAngle), sin(finalAngle)); + } + + void main() { + vec2 uv = v_texCoord; + + // 根据扭曲类型应用相应变换 + if (u_distortionType == 0) { + uv = wave(uv, u_time, u_strength); + } else if (u_distortionType == 1) { + uv = ripple(uv, u_time, u_strength); + } else if (u_distortionType == 2) { + uv = swirl(uv, u_time, u_strength); + } + + // 边界检查 + if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) { + gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0); + } else { + gl_FragColor = texture2D(u_texture, uv); + } + } + `; + + return { vertex: vertexShader, fragment: fragmentShader }; + } + + protected applyWebGLEffect(canvas: HTMLCanvasElement): HTMLCanvasElement | null { + if (!this.gl || !this.program || !this.webglCanvas) { + return null; + } + + // 使用基类提供的公共方法 + this.setupWebGLState(canvas); + + // 创建纹理 + const texture = this.createTextureFromCanvas(canvas); + if (!texture) { + return null; + } + + // 创建顶点缓冲区 + const vertexBuffer = this.createFullScreenQuad(); + if (!vertexBuffer) { + this.gl.deleteTexture(texture); + return null; + } + + try { + // 使用着色器程序 + this.gl.useProgram(this.program); + + // 设置顶点属性 + this.setupVertexAttributes(); + + // 设置uniform变量 + this.setDistortionUniforms(); + + // 绘制 + this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4); + + return this.webglCanvas; + } finally { + // 清理资源 + this.gl.deleteTexture(texture); + this.gl.deleteBuffer(vertexBuffer); + } + } + + /** + * 设置扭曲效果的uniform变量 + */ + private setDistortionUniforms(): void { + if (!this.gl || !this.program) { + return; + } + + const currentTime = this.getAnimationTime(); + + // 获取uniform位置 + const timeLocation = this.gl.getUniformLocation(this.program, 'u_time'); + const strengthLocation = this.gl.getUniformLocation(this.program, 'u_strength'); + const distortionTypeLocation = this.gl.getUniformLocation(this.program, 'u_distortionType'); + const resolutionLocation = this.gl.getUniformLocation(this.program, 'u_resolution'); + + // 设置uniform值 + this.gl.uniform1f(timeLocation, currentTime); + this.gl.uniform1f(strengthLocation, this.distortionConfig.strength); + this.gl.uniform2f(resolutionLocation, this.webglCanvas.width, this.webglCanvas.height); + + // 扭曲类型映射 + const distortionTypeMap: { [key: string]: number } = { + wave: 0, + ripple: 1, + swirl: 2 + }; + this.gl.uniform1i(distortionTypeLocation, distortionTypeMap[this.distortionConfig.distortionType] || 0); + } + + /** + * Canvas 2D实现:软件扭曲效果 + */ + protected applyCanvas2DEffect(canvas: HTMLCanvasElement): HTMLCanvasElement | null { + const outputCanvas = this.createOutputCanvas(canvas); + if (!outputCanvas) { + return null; + } + + const { ctx } = outputCanvas; + + try { + // 获取图像数据 + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const currentTime = this.getAnimationTime(); + + // 应用对应的扭曲算法 + let distortedImageData: ImageData; + + switch (this.distortionConfig.distortionType) { + case 'wave': + distortedImageData = this.applyWaveDistortion(imageData, this.distortionConfig.strength, currentTime); + break; + case 'ripple': + distortedImageData = this.applyRippleDistortion(imageData, this.distortionConfig.strength, currentTime); + break; + case 'swirl': + distortedImageData = this.applySwirlDistortion(imageData, this.distortionConfig.strength, currentTime); + break; + default: + distortedImageData = imageData; + } + + // 清空画布并绘制扭曲后的图像 + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.putImageData(distortedImageData, 0, 0); + + return outputCanvas.canvas; + } catch (error) { + console.warn('Canvas 2D distortion effect failed:', error); + return null; + } + } + + /** + * Canvas 2D波浪扭曲实现 + */ + private applyWaveDistortion(imageData: ImageData, strength: number, time: number): ImageData { + const { data, width, height } = imageData; + const result = new Uint8ClampedArray(data.length); + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + // 波浪扭曲计算 + const waveX = Math.sin(y * 0.1 + time * 3) * strength * 20; + const waveY = Math.sin(x * 0.1 + time * 2) * strength * 20; + + const sourceX = Math.round(x - waveX); + const sourceY = Math.round(y - waveY); + + const targetIndex = (y * width + x) * 4; + + if (sourceX >= 0 && sourceX < width && sourceY >= 0 && sourceY < height) { + const sourceIndex = (sourceY * width + sourceX) * 4; + result[targetIndex] = data[sourceIndex]; + result[targetIndex + 1] = data[sourceIndex + 1]; + result[targetIndex + 2] = data[sourceIndex + 2]; + result[targetIndex + 3] = data[sourceIndex + 3]; + } else { + result[targetIndex + 3] = 0; // 透明 + } + } + } + + return new ImageData(result, width, height); + } + + /** + * Canvas 2D涟漪扭曲实现 + */ + private applyRippleDistortion(imageData: ImageData, strength: number, time: number): ImageData { + const { data, width, height } = imageData; + const result = new Uint8ClampedArray(data.length); + const centerX = width / 2; + const centerY = height / 2; + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const dx = x - centerX; + const dy = y - centerY; + const distance = Math.sqrt(dx * dx + dy * dy); + + // 涟漪效果 + const ripple = Math.sin(distance * 0.2 - time * 5) * strength * 10; + const angle = Math.atan2(dy, dx); + + const sourceX = Math.round(x - Math.cos(angle) * ripple); + const sourceY = Math.round(y - Math.sin(angle) * ripple); + + const targetIndex = (y * width + x) * 4; + + if (sourceX >= 0 && sourceX < width && sourceY >= 0 && sourceY < height) { + const sourceIndex = (sourceY * width + sourceX) * 4; + result[targetIndex] = data[sourceIndex]; + result[targetIndex + 1] = data[sourceIndex + 1]; + result[targetIndex + 2] = data[sourceIndex + 2]; + result[targetIndex + 3] = data[sourceIndex + 3]; + } else { + result[targetIndex + 3] = 0; + } + } + } + + return new ImageData(result, width, height); + } + + /** + * Canvas 2D漩涡扭曲实现 + */ + private applySwirlDistortion(imageData: ImageData, strength: number, time: number): ImageData { + const { data, width, height } = imageData; + const result = new Uint8ClampedArray(data.length); + const centerX = width / 2; + const centerY = height / 2; + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const dx = x - centerX; + const dy = y - centerY; + const distance = Math.sqrt(dx * dx + dy * dy); + const originalAngle = Math.atan2(dy, dx); + + // 旋转角度随时间和强度增长 + const rotationAngle = distance * strength * time * 0.02; + const finalAngle = originalAngle + rotationAngle; + + const sourceX = Math.round(centerX + distance * Math.cos(finalAngle)); + const sourceY = Math.round(centerY + distance * Math.sin(finalAngle)); + + const targetIndex = (y * width + x) * 4; + + if (sourceX >= 0 && sourceX < width && sourceY >= 0 && sourceY < height) { + const sourceIndex = (sourceY * width + sourceX) * 4; + result[targetIndex] = data[sourceIndex]; + result[targetIndex + 1] = data[sourceIndex + 1]; + result[targetIndex + 2] = data[sourceIndex + 2]; + result[targetIndex + 3] = data[sourceIndex + 3]; + } else { + result[targetIndex + 3] = 0; + } + } + } + + return new ImageData(result, width, height); + } + + /** + * 重写主要渲染方法,添加强度检查 + */ + protected afterStageRender(stage: any, canvas: HTMLCanvasElement): HTMLCanvasElement | void | null | false { + // 如果强度为0,直接返回原图 + if (this.distortionConfig.strength <= 0) { + return canvas; + } + + // 调用父类的智能渲染选择逻辑 + return super.afterStageRender(stage, canvas); + } +} diff --git a/packages/vrender-animate/src/custom/disappear/gaussian-blur.ts b/packages/vrender-animate/src/custom/disappear/gaussian-blur.ts new file mode 100644 index 000000000..dbaab4ddf --- /dev/null +++ b/packages/vrender-animate/src/custom/disappear/gaussian-blur.ts @@ -0,0 +1,143 @@ +import type { EasingType } from '@visactor/vrender-core'; +import { vglobal } from '@visactor/vrender-core'; +import { AStageAnimate } from '../custom-animate'; + +// 模糊效果配置接口 +export interface BlurConfig { + blurRadius: number; + useOptimizedBlur: boolean; +} + +export class GaussianBlur extends AStageAnimate { + // 模糊配置 + private blurConfig: BlurConfig; + + constructor(from: null, to: null, duration: number, easing: EasingType, params: any) { + super(from, to, duration, easing, params); + + this.blurConfig = { + blurRadius: params?.options?.blurRadius || 8, + useOptimizedBlur: params?.options?.useOptimizedBlur !== undefined ? params.options.useOptimizedBlur : true + }; + } + // 使用CSS滤镜(性能最好) + private applyCSSBlur(canvas: HTMLCanvasElement, radius: number): HTMLCanvasElement { + const c = vglobal.createCanvas({ + width: canvas.width, + height: canvas.height, + dpr: vglobal.devicePixelRatio + }); + const ctx = c.getContext('2d'); + if (!ctx) { + return canvas; + } + + // 使用CSS滤镜进行模糊 + ctx.filter = `blur(${radius}px)`; + ctx.drawImage(canvas, 0, 0); + ctx.filter = 'none'; + + return c; + } + + // 降采样模糊(减少计算量) + private applyDownsampleBlur(imageData: ImageData, radius: number): ImageData { + const { width, height } = imageData; + + // 降采样因子,减少计算量 + const downsample = Math.max(1, Math.floor(radius / 2)); + const smallWidth = Math.floor(width / downsample); + const smallHeight = Math.floor(height / downsample); + + // 创建小尺寸的临时canvas + const tempCanvas = vglobal.createCanvas({ + width: smallWidth, + height: smallHeight, + dpr: 1 + }); + const tempCtx = tempCanvas.getContext('2d'); + if (!tempCtx) { + return imageData; + } + + // 将图像绘制到小canvas上 + const originalCanvas = vglobal.createCanvas({ + width: width, + height: height, + dpr: 1 + }); + const originalCtx = originalCanvas.getContext('2d'); + if (!originalCtx) { + return imageData; + } + + originalCtx.putImageData(imageData, 0, 0); + + // 缩小绘制(自动插值) + tempCtx.drawImage(originalCanvas, 0, 0, smallWidth, smallHeight); + + // 应用模糊到小图像 + tempCtx.filter = `blur(${radius / downsample}px)`; + tempCtx.drawImage(tempCanvas, 0, 0); + tempCtx.filter = 'none'; + + // 放大回原尺寸 + originalCtx.clearRect(0, 0, width, height); + originalCtx.drawImage(tempCanvas, 0, 0, width, height); + + return originalCtx.getImageData(0, 0, width, height); + } + + protected afterStageRender(stage: any, canvas: HTMLCanvasElement): HTMLCanvasElement | void | null | false { + // 如果模糊强度为0,直接返回原图 + if (this.blurConfig.blurRadius <= 0) { + return canvas; + } + + let result: HTMLCanvasElement; + + if (this.blurConfig.useOptimizedBlur) { + // 使用CSS滤镜(性能最好) + result = this.applyCSSBlur(canvas, this.blurConfig.blurRadius); + } else { + // 使用传统的像素级模糊 + const c = vglobal.createCanvas({ + width: canvas.width, + height: canvas.height, + dpr: vglobal.devicePixelRatio + }); + const ctx = c.getContext('2d'); + if (!ctx) { + return false; + } + + // 清空画布 + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // 绘制原始图像 + ctx.drawImage(canvas, 0, 0); + + // 获取图像数据 + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + + // 高质量模式下统一使用降采样模糊 + const blurredImageData = this.applyDownsampleBlur(imageData, this.blurConfig.blurRadius); + + // 将模糊后的图像数据绘制到画布上 + ctx.putImageData(blurredImageData, 0, 0); + + result = c; + } + + // 添加一个半透明的覆盖层来增强效果 + const ctx = result.getContext('2d'); + if (ctx) { + ctx.globalCompositeOperation = 'overlay'; + ctx.fillStyle = 'rgba(255, 255, 255, 0.1)'; + ctx.fillRect(0, 0, result.width, result.height); + ctx.globalCompositeOperation = 'source-over'; + } + + return result; + } +} diff --git a/packages/vrender-animate/src/custom/disappear/glitch.ts b/packages/vrender-animate/src/custom/disappear/glitch.ts new file mode 100644 index 000000000..c70af0961 --- /dev/null +++ b/packages/vrender-animate/src/custom/disappear/glitch.ts @@ -0,0 +1,387 @@ +import type { EasingType } from '@visactor/vrender-core'; +import { Canvas2DEffectBase } from './base/CustomEffectBase'; +// import { GlitchEffectConfig, EffectConfigFactory, EffectType } from './base/DisappearEffectConfig'; +import { ImageProcessUtils } from './base/ImageProcessUtils'; + +// 故障效果配置接口 +export interface GlitchConfig { + effectType?: 'rgb-shift' | 'digital-distortion' | 'scan-lines' | 'data-corruption'; // 故障效果类型 + intensity?: number; // 故障强度 0-1 +} + +/** + * 故障消失动画效果 - 重构版 + * 使用Canvas2DEffectBase实现,专注于2D图像处理的故障效果 + */ +export class Glitch extends Canvas2DEffectBase { + private glitchConfig: Required; + + constructor(from: null, to: null, duration: number, easing: EasingType, params: any) { + super(from, to, duration, easing, params); + + this.glitchConfig = { + effectType: params?.options?.effectType || 'rgb-shift', + // intensity 可能为 0 + intensity: params?.options?.intensity !== undefined ? params.options.intensity : 0.5 + }; + } + + /** + * Canvas 2D故障效果主入口 + */ + protected applyCanvas2DEffect(canvas: HTMLCanvasElement): HTMLCanvasElement | null { + // 如果强度为0,创建一个原图副本返回 + if (this.glitchConfig.intensity <= 0) { + const outputCanvas = this.createOutputCanvas(canvas); + return outputCanvas ? outputCanvas.canvas : null; + } + + try { + // 根据故障类型应用对应效果 + switch (this.glitchConfig.effectType) { + case 'rgb-shift': + return this.applyRGBShiftGlitch(canvas); + case 'digital-distortion': + return this.applyDigitalDistortionGlitch(canvas); + case 'scan-lines': + return this.applyScanLineGlitch(canvas); + case 'data-corruption': + return this.applyDataCorruptionGlitch(canvas); + default: + return this.applyRGBShiftGlitch(canvas); + } + } catch (error) { + console.warn('Glitch effect failed:', error); + return null; + } + } + + /** + * RGB通道偏移故障效果 + * 分离RGB通道并应用不同的偏移,产生色散效果 + */ + private applyRGBShiftGlitch(canvas: HTMLCanvasElement): HTMLCanvasElement | null { + const outputCanvas = this.createOutputCanvas(canvas); + if (!outputCanvas) { + return null; + } + + const { ctx } = outputCanvas; + + try { + // 清空画布 + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // 计算基于动画进度的动态强度 + const dynamicIntensity = ImageProcessUtils.calculateDynamicStrength( + this.glitchConfig.intensity, + this.getAnimationTime() + ); + + // 计算偏移量 - 增强色散效果 + const maxOffset = Math.floor(dynamicIntensity * 20); + const redOffset = this.generateRandomOffset(maxOffset); + const greenOffset = this.generateRandomOffset(maxOffset, 0.3); + const blueOffset = this.generateRandomOffset(-maxOffset); + + // 获取原始图像数据 + const tempCanvas = ImageProcessUtils.createTempCanvas(canvas.width, canvas.height); + const tempCtx = tempCanvas.getContext('2d'); + tempCtx.drawImage(canvas, 0, 0); + const originalImageData = tempCtx.getImageData(0, 0, canvas.width, canvas.height); + + // 创建RGB通道分离的图像数据 + const redChannelData = ImageProcessUtils.extractChannel(originalImageData, 0); + const greenChannelData = ImageProcessUtils.extractChannel(originalImageData, 1); + const blueChannelData = ImageProcessUtils.extractChannel(originalImageData, 2); + + // 使用screen混合模式绘制分离的通道 + ctx.globalCompositeOperation = 'screen'; + + // 绘制红色通道 + tempCtx.clearRect(0, 0, canvas.width, canvas.height); + tempCtx.putImageData(redChannelData, 0, 0); + ctx.drawImage(tempCanvas, redOffset.x, redOffset.y); + + // 绘制绿色通道 + tempCtx.clearRect(0, 0, canvas.width, canvas.height); + tempCtx.putImageData(greenChannelData, 0, 0); + ctx.drawImage(tempCanvas, greenOffset.x, greenOffset.y); + + // 绘制蓝色通道 + tempCtx.clearRect(0, 0, canvas.width, canvas.height); + tempCtx.putImageData(blueChannelData, 0, 0); + ctx.drawImage(tempCanvas, blueOffset.x, blueOffset.y); + + // 恢复正常混合模式 + ctx.globalCompositeOperation = 'source-over'; + + return outputCanvas.canvas; + } catch (error) { + console.warn('RGB shift glitch failed:', error); + return null; + } + } + + /** + * 数字扭曲故障效果 + * 应用水平切片偏移和随机像素噪声 + */ + private applyDigitalDistortionGlitch(canvas: HTMLCanvasElement): HTMLCanvasElement | null { + const outputCanvas = this.createOutputCanvas(canvas); + if (!outputCanvas) { + return null; + } + + const { ctx } = outputCanvas; + + try { + // 获取图像数据 + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const dynamicIntensity = ImageProcessUtils.calculateDynamicStrength( + this.glitchConfig.intensity, + this.getAnimationTime() + ); + + // 应用数字扭曲 + const distortedImageData = this.processDigitalDistortion(imageData, dynamicIntensity); + + // 清空画布并绘制扭曲后的图像 + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.putImageData(distortedImageData, 0, 0); + + return outputCanvas.canvas; + } catch (error) { + console.warn('Digital distortion glitch failed:', error); + return null; + } + } + + /** + * 扫描线故障效果 + * 添加水平扫描线和随机亮线效果 + */ + private applyScanLineGlitch(canvas: HTMLCanvasElement): HTMLCanvasElement | null { + const outputCanvas = this.createOutputCanvas(canvas); + if (!outputCanvas) { + return null; + } + + const { ctx } = outputCanvas; + + try { + const dynamicIntensity = ImageProcessUtils.calculateDynamicStrength( + this.glitchConfig.intensity, + this.getAnimationTime() + ); + + // 添加暗扫描线 + const lineSpacing = Math.max(2, Math.floor(10 - dynamicIntensity * 8)); + ctx.globalCompositeOperation = 'multiply'; + + for (let y = 0; y < canvas.height; y += lineSpacing) { + if (Math.random() < dynamicIntensity) { + const opacity = 0.1 + dynamicIntensity * 0.4; + ctx.fillStyle = `rgba(0, 0, 0, ${opacity})`; + ctx.fillRect(0, y, canvas.width, 1); + } + } + + // 添加随机亮线 + ctx.globalCompositeOperation = 'screen'; + const brightLineCount = Math.floor(dynamicIntensity * 20); + for (let i = 0; i < brightLineCount; i++) { + const y = Math.random() * canvas.height; + const opacity = dynamicIntensity * 0.3; + ctx.fillStyle = `rgba(255, 255, 255, ${opacity})`; + ctx.fillRect(0, Math.floor(y), canvas.width, 1); + } + + // 恢复正常混合模式 + ctx.globalCompositeOperation = 'source-over'; + + return outputCanvas.canvas; + } catch (error) { + console.warn('Scan line glitch failed:', error); + return null; + } + } + + /** + * 数据损坏故障效果 + * 创建垂直条纹和随机块状损坏效果 + */ + private applyDataCorruptionGlitch(canvas: HTMLCanvasElement): HTMLCanvasElement | null { + const outputCanvas = this.createOutputCanvas(canvas); + if (!outputCanvas) { + return null; + } + + const { ctx } = outputCanvas; + + try { + // 获取图像数据 + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const dynamicIntensity = ImageProcessUtils.calculateDynamicStrength( + this.glitchConfig.intensity, + this.getAnimationTime() + ); + + // 应用数据损坏 + const corruptedImageData = this.processDataCorruption(imageData, dynamicIntensity); + + // 清空画布并绘制损坏后的图像 + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.putImageData(corruptedImageData, 0, 0); + + return outputCanvas.canvas; + } catch (error) { + console.warn('Data corruption glitch failed:', error); + return null; + } + } + + /** + * 生成随机偏移量 + */ + private generateRandomOffset(maxOffset: number, scale: number = 1): { x: number; y: number } { + return { + x: (Math.random() - 0.5) * maxOffset, + y: (Math.random() - 0.5) * maxOffset * scale + }; + } + + /** + * 处理数字扭曲算法 + */ + private processDigitalDistortion(imageData: ImageData, intensity: number): ImageData { + const { data, width, height } = imageData; + const result = new Uint8ClampedArray(data); + + // 随机水平切片 + const sliceCount = Math.floor(intensity * 20) + 5; + const sliceHeight = Math.floor(height / sliceCount); + + for (let i = 0; i < sliceCount; i++) { + if (Math.random() < intensity) { + const y = i * sliceHeight; + const sliceEnd = Math.min(y + sliceHeight, height); + const offset = Math.floor((Math.random() - 0.5) * width * intensity * 0.1); + + // 水平偏移切片 + this.shiftSliceHorizontal(result, width, height, y, sliceEnd, offset); + } + } + + // 添加随机像素噪声 + const noiseIntensity = intensity * 0.3; + for (let i = 0; i < data.length; i += 4) { + if (Math.random() < noiseIntensity) { + result[i] = Math.random() * 255; // R + result[i + 1] = Math.random() * 255; // G + result[i + 2] = Math.random() * 255; // B + } + } + + return new ImageData(result, width, height); + } + + /** + * 水平切片偏移算法 + */ + private shiftSliceHorizontal( + data: Uint8ClampedArray, + width: number, + height: number, + startY: number, + endY: number, + offset: number + ): void { + const tempRow = new Uint8ClampedArray(width * 4); + + for (let y = startY; y < endY; y++) { + const rowStart = y * width * 4; + + // 保存当前行 + for (let x = 0; x < width * 4; x++) { + tempRow[x] = data[rowStart + x]; + } + + // 应用偏移 + for (let x = 0; x < width; x++) { + const sourceX = (x - offset + width) % width; + const targetIndex = rowStart + x * 4; + const sourceIndex = sourceX * 4; + + data[targetIndex] = tempRow[sourceIndex]; + data[targetIndex + 1] = tempRow[sourceIndex + 1]; + data[targetIndex + 2] = tempRow[sourceIndex + 2]; + data[targetIndex + 3] = tempRow[sourceIndex + 3]; + } + } + } + + /** + * 处理数据损坏算法 + */ + private processDataCorruption(imageData: ImageData, intensity: number): ImageData { + const { data, width, height } = imageData; + const result = new Uint8ClampedArray(data); + + // 随机垂直条纹 + const stripeCount = Math.floor(intensity * 15) + 5; + for (let i = 0; i < stripeCount; i++) { + if (Math.random() < intensity) { + const x = Math.floor(Math.random() * width); + const stripeWidth = Math.floor(Math.random() * 5) + 1; + const color = Math.random() < 0.5 ? 0 : 255; + + for (let y = 0; y < height; y++) { + for (let dx = 0; dx < stripeWidth && x + dx < width; dx++) { + const index = (y * width + x + dx) * 4; + result[index] = color; // R + result[index + 1] = color; // G + result[index + 2] = color; // B + } + } + } + } + + // 随机块状损坏 + const corruptionCount = Math.floor(intensity * 20); + for (let i = 0; i < corruptionCount; i++) { + const blockX = Math.floor(Math.random() * width); + const blockY = Math.floor(Math.random() * height); + const blockW = Math.floor(Math.random() * 20) + 5; + const blockH = Math.floor(Math.random() * 10) + 2; + + this.corruptBlock(result, width, height, blockX, blockY, blockW, blockH); + } + + return new ImageData(result, width, height); + } + + /** + * 损坏指定区域的像素块 + */ + private corruptBlock( + data: Uint8ClampedArray, + width: number, + height: number, + x: number, + y: number, + w: number, + h: number + ): void { + for (let dy = 0; dy < h && y + dy < height; dy++) { + for (let dx = 0; dx < w && x + dx < width; dx++) { + const index = ((y + dy) * width + (x + dx)) * 4; + if (Math.random() < 0.7) { + data[index] = Math.random() * 255; + data[index + 1] = Math.random() * 255; + data[index + 2] = Math.random() * 255; + } + } + } + } +} diff --git a/packages/vrender-animate/src/custom/disappear/grayscale.ts b/packages/vrender-animate/src/custom/disappear/grayscale.ts new file mode 100644 index 000000000..029b91da4 --- /dev/null +++ b/packages/vrender-animate/src/custom/disappear/grayscale.ts @@ -0,0 +1,313 @@ +import type { EasingType } from '@visactor/vrender-core'; +import { HybridEffectBase } from './base/CustomEffectBase'; +import { ImageProcessUtils, ShaderLibrary } from './base/ImageProcessUtils'; + +export interface ColorEffectConfig { + effectType: 'grayscale' | 'sepia'; + strength: number; + useWebGL: boolean; +} +/** + * 灰度/褐色调消失动画效果 - 重构版 + * 使用HybridEffectBase实现WebGL和Canvas 2D双重支持 + */ +export class Grayscale extends HybridEffectBase { + private colorConfig: Required; + + constructor(from: null, to: null, duration: number, easing: EasingType, params: any) { + super(from, to, duration, easing, params); + + // 获取strength并限制在0-1范围内 + const rawStrength = params?.options?.strength !== undefined ? params.options.strength : 1.0; + const clampedStrength = Math.max(0, Math.min(1, rawStrength)); + + this.colorConfig = { + effectType: params?.options?.effectType || 'grayscale', // 'grayscale' | 'sepia' + strength: clampedStrength, // 限制在 0.0 - 1.0 范围内 + useWebGL: params?.options?.useWebGL !== undefined ? params.options.useWebGL : true // 是否使用WebGL实现 + }; + } + + /** + * WebGL实现:高性能着色器颜色转换 + */ + protected getShaderSources(): { vertex: string; fragment: string } | null { + const vertexShader = ShaderLibrary.STANDARD_VERTEX_SHADER; + + const fragmentShader = ` + precision mediump float; + uniform sampler2D u_texture; + uniform float u_time; + uniform float u_strength; + uniform int u_effectType; + uniform vec2 u_resolution; + varying vec2 v_texCoord; + + ${ShaderLibrary.SHADER_FUNCTIONS} + + void main() { + vec2 uv = v_texCoord; + vec4 originalColor = texture2D(u_texture, uv); + vec3 color = originalColor.rgb; + + // 计算动态强度 + float dynamicStrength = calculateDynamicStrength(u_strength, u_time); + + if (u_effectType == 0) { + // 灰度效果 + float gray = luminance(color); + vec3 grayColor = vec3(gray); + color = mix(color, grayColor, dynamicStrength); + } else if (u_effectType == 1) { + // 褐色调效果 + vec3 sepiaColor = sepia(color); + color = mix(color, sepiaColor, dynamicStrength); + } + + gl_FragColor = vec4(color, originalColor.a); + } + `; + + return { vertex: vertexShader, fragment: fragmentShader }; + } + + protected applyWebGLEffect(canvas: HTMLCanvasElement): HTMLCanvasElement | null { + if (!this.gl || !this.program || !this.webglCanvas) { + return null; + } + + // 使用基类提供的公共方法 + this.setupWebGLState(canvas); + + // 创建纹理 + const texture = this.createTextureFromCanvas(canvas); + if (!texture) { + return null; + } + + // 创建顶点缓冲区 + const vertexBuffer = this.createFullScreenQuad(); + if (!vertexBuffer) { + this.gl.deleteTexture(texture); + return null; + } + + try { + // 使用着色器程序 + this.gl.useProgram(this.program); + + // 设置顶点属性 + this.setupVertexAttributes(); + + // 设置uniform变量 + this.setColorUniforms(); + + // 绘制 + this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4); + + return this.webglCanvas; + } finally { + // 清理资源 + this.gl.deleteTexture(texture); + this.gl.deleteBuffer(vertexBuffer); + } + } + + /** + * 设置颜色效果的uniform变量 + */ + private setColorUniforms(): void { + if (!this.gl || !this.program) { + return; + } + + const currentTime = this.getAnimationTime(); + + // 获取uniform位置 + const timeLocation = this.gl.getUniformLocation(this.program, 'u_time'); + const strengthLocation = this.gl.getUniformLocation(this.program, 'u_strength'); + const effectTypeLocation = this.gl.getUniformLocation(this.program, 'u_effectType'); + const resolutionLocation = this.gl.getUniformLocation(this.program, 'u_resolution'); + + // 设置uniform值 + this.gl.uniform1f(timeLocation, currentTime); + this.gl.uniform1f(strengthLocation, this.colorConfig.strength); + this.gl.uniform2f(resolutionLocation, this.webglCanvas.width, this.webglCanvas.height); + + // 效果类型映射 + const effectTypeMap: { [key: string]: number } = { + grayscale: 0, + sepia: 1 + }; + this.gl.uniform1i(effectTypeLocation, effectTypeMap[this.colorConfig.effectType] || 0); + } + + /** + * Canvas 2D实现:软件颜色转换 + */ + protected applyCanvas2DEffect(canvas: HTMLCanvasElement): HTMLCanvasElement | null { + // 如果强度为0,创建原图副本 + if (this.colorConfig.strength <= 0) { + const outputCanvas = this.createOutputCanvas(canvas); + return outputCanvas ? outputCanvas.canvas : null; + } + + // 检查是否使用CSS Filter API(如果支持) + if (this.canUseCSSFilter()) { + return this.applyCSSFilter(canvas); + } + + // 使用像素级处理 + const outputCanvas = this.createOutputCanvas(canvas); + if (!outputCanvas) { + return null; + } + + const { ctx } = outputCanvas; + + try { + // 获取图像数据 + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const currentTime = this.getAnimationTime(); + + // 应用对应的颜色效果 + let processedImageData: ImageData; + + switch (this.colorConfig.effectType) { + case 'grayscale': + processedImageData = this.applyGrayscaleEffect(imageData, this.colorConfig.strength, currentTime); + break; + case 'sepia': + processedImageData = this.applySepiaEffect(imageData, this.colorConfig.strength, currentTime); + break; + default: + processedImageData = this.applyGrayscaleEffect(imageData, this.colorConfig.strength, currentTime); + } + + // 清空画布并绘制处理后的图像 + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.putImageData(processedImageData, 0, 0); + + return outputCanvas.canvas; + } catch (error) { + console.warn('Canvas 2D color effect failed:', error); + return null; + } + } + + /** + * 检查是否可以使用CSS Filter API + */ + private canUseCSSFilter(): boolean { + // 检查全局配置或浏览器支持 + return !!(window as any).useFilterAPI && typeof CSS !== 'undefined' && CSS.supports?.('filter', 'grayscale(1)'); + } + + /** + * 使用CSS Filter API应用颜色效果 + */ + private applyCSSFilter(canvas: HTMLCanvasElement): HTMLCanvasElement | null { + try { + const outputCanvas = ImageProcessUtils.createTempCanvas(canvas.width, canvas.height); + const ctx = outputCanvas.getContext('2d'); + if (!ctx) { + return null; + } + + // 计算动态强度 + const currentTime = this.getAnimationTime(); + const dynamicStrength = ImageProcessUtils.calculateDynamicStrength(this.colorConfig.strength, currentTime); + + // 应用CSS滤镜 + let filterValue = ''; + if (this.colorConfig.effectType === 'grayscale') { + filterValue = `grayscale(${Math.min(1, dynamicStrength)})`; + } else if (this.colorConfig.effectType === 'sepia') { + filterValue = `sepia(${Math.min(1, dynamicStrength)})`; + } + + ctx.filter = filterValue; + ctx.drawImage(canvas, 0, 0); + ctx.filter = 'none'; + + return outputCanvas; + } catch (error) { + console.warn('CSS Filter API failed, falling back to pixel processing:', error); + return null; + } + } + + /** + * Canvas 2D灰度效果实现 + */ + private applyGrayscaleEffect(imageData: ImageData, strength: number, time: number): ImageData { + const { data, width, height } = imageData; + const result = new Uint8ClampedArray(data.length); + + // 计算动态强度 + const dynamicStrength = ImageProcessUtils.calculateDynamicStrength(strength, time); + + for (let i = 0; i < data.length; i += 4) { + const r = data[i]; + const g = data[i + 1]; + const b = data[i + 2]; + const a = data[i + 3]; + + // 使用标准亮度公式计算灰度值 + const gray = ImageProcessUtils.getLuminance(r, g, b); + + // 根据动态强度混合原色和灰度色 + result[i] = Math.round(ImageProcessUtils.lerp(r, gray, dynamicStrength)); + result[i + 1] = Math.round(ImageProcessUtils.lerp(g, gray, dynamicStrength)); + result[i + 2] = Math.round(ImageProcessUtils.lerp(b, gray, dynamicStrength)); + result[i + 3] = a; + } + + return new ImageData(result, width, height); + } + + /** + * Canvas 2D褐色调效果实现 + */ + private applySepiaEffect(imageData: ImageData, strength: number, time: number): ImageData { + const { data, width, height } = imageData; + const result = new Uint8ClampedArray(data.length); + + // 计算动态强度 + const dynamicStrength = ImageProcessUtils.calculateDynamicStrength(strength, time); + + for (let i = 0; i < data.length; i += 4) { + const r = data[i]; + const g = data[i + 1]; + const b = data[i + 2]; + const a = data[i + 3]; + + // 使用工具类计算褐色调 + const [sepiaR, sepiaG, sepiaB] = ImageProcessUtils.applySepiaToPixel(r, g, b); + + // 根据动态强度混合原色和褐色调 + result[i] = Math.round(ImageProcessUtils.lerp(r, sepiaR, dynamicStrength)); + result[i + 1] = Math.round(ImageProcessUtils.lerp(g, sepiaG, dynamicStrength)); + result[i + 2] = Math.round(ImageProcessUtils.lerp(b, sepiaB, dynamicStrength)); + result[i + 3] = a; + } + + return new ImageData(result, width, height); + } + + /** + * 重写主要渲染方法,添加CSS Filter快速路径 + */ + protected afterStageRender(stage: any, canvas: HTMLCanvasElement): HTMLCanvasElement | void | null | false { + // 检查是否使用CSS Filter API作为快速路径 + if (this.canUseCSSFilter() && this.colorConfig.strength > 0) { + const cssResult = this.applyCSSFilter(canvas); + if (cssResult) { + return cssResult; + } + } + + // 调用父类的智能渲染选择逻辑 + return super.afterStageRender(stage, canvas); + } +} diff --git a/packages/vrender-animate/src/custom/disappear/particle.ts b/packages/vrender-animate/src/custom/disappear/particle.ts new file mode 100644 index 000000000..c70cadd17 --- /dev/null +++ b/packages/vrender-animate/src/custom/disappear/particle.ts @@ -0,0 +1,514 @@ +import type { EasingType } from '@visactor/vrender-core'; +import { HybridEffectBase } from './base/CustomEffectBase'; +import { ImageProcessUtils } from './base/ImageProcessUtils'; + +export interface ParticleConfig { + effectType?: 'explode' | 'vortex' | 'gravity'; // 粒子效果类型 + count?: number; // 粒子数量 + size?: number; // 粒子大小 + strength?: number; // 力场强度 + useWebGL: boolean; +} +// 粒子数据结构 +export interface ParticleData { + x: number; + y: number; + originX: number; + originY: number; + vx: number; + vy: number; + r: number; + g: number; + b: number; + a: number; + life: number; + size: number; +} + +/** + * 重构后的粒子消散特效 + * 使用HybridEffectBase,优先WebGL实现,Canvas 2D回退 + */ +export class Particle extends HybridEffectBase { + private particles: ParticleData[] = []; + private positionBuffer: WebGLBuffer | null = null; + private colorBuffer: WebGLBuffer | null = null; + private particleConfig: ParticleConfig; + + constructor(from: null, to: null, duration: number, easing: EasingType, params: any) { + super(from, to, duration, easing, params); + + this.particleConfig = { + effectType: params?.options?.effectType || 'gravity', //'explode' | 'vortex' | 'gravity'; // 粒子效果类型 + count: params?.options?.count || 4000, + size: params?.options?.size || 20, + strength: params?.options?.strength || 1.5, + useWebGL: params?.options?.useWebGL !== undefined ? params.options.useWebGL : true // 是否使用WebGL实现 + }; + } + + // WebGL实现 - 高性能版本 + protected getShaderSources(): { vertex: string; fragment: string } | null { + const vertexShader = ` + attribute vec2 a_position; + attribute vec4 a_color; + attribute float a_size; + + uniform vec2 u_resolution; + uniform float u_time; + uniform float u_forceStrength; + uniform int u_effectType; + + varying vec4 v_color; + + void main() { + // 将像素坐标转换为剪辑空间坐标 + vec2 clipSpace = ((a_position / u_resolution) * 2.0) - 1.0; + clipSpace.y = -clipSpace.y; // 翻转Y轴 + + gl_Position = vec4(clipSpace, 0.0, 1.0); + gl_PointSize = a_size; + v_color = a_color; + } + `; + + const fragmentShader = ` + precision mediump float; + varying vec4 v_color; + + void main() { + // 创建圆形粒子 + vec2 coord = gl_PointCoord - vec2(0.5); + float distance = length(coord); + + if (distance > 0.5) { + discard; + } + + // 保持原始颜色,只调整透明度渐变 + gl_FragColor = vec4(v_color.rgb, v_color.a); + } + `; + + return { vertex: vertexShader, fragment: fragmentShader }; + } + + protected applyWebGLEffect(canvas: HTMLCanvasElement): HTMLCanvasElement | null { + if (!this.gl || !this.program || !this.webglCanvas) { + return null; + } + + // 使用基类提供的WebGL状态设置 + this.setupWebGLState(canvas); + + // 如果没有粒子,提取粒子数据 + if (this.particles.length === 0) { + this.extractParticles(canvas); + } + + // 更新粒子物理 + this.updateParticles(canvas); + + const gl = this.gl; + + // 启用混合 + gl.enable(gl.BLEND); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + + gl.useProgram(this.program); + + // 准备粒子数据并绘制 + this.prepareAndDrawParticles(gl); + + return this.webglCanvas; + } + + // Canvas 2D回退实现 - 简化版本,主要用于兼容性 + protected applyCanvas2DEffect(canvas: HTMLCanvasElement): HTMLCanvasElement | null { + const output = this.createOutputCanvas(canvas); + if (!output) { + return null; + } + + const { canvas: outputCanvas, ctx } = output; + + // 简化的粒子效果:使用透明度和简单变换模拟粒子消散 + const progress = this.currentAnimationRatio; + + // 根据效果类型应用不同的Canvas 2D模拟 + switch (this.particleConfig.effectType) { + case 'explode': + this.applyCanvas2DExplode(ctx, canvas, progress); + break; + case 'gravity': + this.applyCanvas2DGravity(ctx, canvas, progress); + break; + case 'vortex': + this.applyCanvas2DVortex(ctx, canvas, progress); + break; + default: + // 默认简单透明度淡出 + ctx.globalAlpha = Math.max(0, 1 - progress); + ctx.drawImage(canvas, 0, 0); + } + + return outputCanvas; + } + + /** + * 从canvas提取粒子数据 + */ + private extractParticles(canvas: HTMLCanvasElement): void { + const tempCanvas = ImageProcessUtils.createTempCanvas(canvas.width, canvas.height, 1); + const tempCtx = tempCanvas.getContext('2d'); + if (!tempCtx) { + return; + } + + // 绘制原始图像到临时canvas + tempCtx.drawImage(canvas, 0, 0, tempCanvas.width, tempCanvas.height); + + // 获取图像数据 + const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height); + const data = imageData.data; + + this.particles = []; + + // 计算采样步长 + const step = Math.max( + 1, + Math.floor(Math.sqrt((tempCanvas.width * tempCanvas.height) / (this.particleConfig.count * 1.5))) + ); + + for (let y = 0; y < tempCanvas.height; y += step) { + for (let x = 0; x < tempCanvas.width; x += step) { + const index = (y * tempCanvas.width + x) * 4; + + const r = data[index]; + const g = data[index + 1]; + const b = data[index + 2]; + const a = data[index + 3]; + + // 只创建非透明像素的粒子 + if (a > 5) { + // 将坐标转换回原始canvas尺寸 + const realX = (x / tempCanvas.width) * canvas.width; + const realY = (y / tempCanvas.height) * canvas.height; + + const particle: ParticleData = { + x: realX, + y: realY, + originX: realX, + originY: realY, + vx: 0, + vy: 0, + r: r / 255, + g: g / 255, + b: b / 255, + a: Math.max(0.6, a / 255), + life: 1.0, + size: this.particleConfig.size * (1 + Math.random() * 0.5) + }; + + this.particles.push(particle); + } + } + } + } + + /** + * 更新粒子物理模拟 + */ + private updateParticles(canvas: HTMLCanvasElement): void { + const centerX = canvas.width / 2; + const centerY = canvas.height / 2; + const progress = this.currentAnimationRatio; + const duration = this.getDurationFromParent(); + const isShortAnimation = duration < 2000; + const timeMultiplier = isShortAnimation ? Math.max(1.5, 3000 / duration) : 1.0; + const intensityBoost = isShortAnimation ? Math.min(2.0, 2000 / duration) : 1.0; + + this.particles.forEach(particle => { + const dx = particle.x - centerX; + const dy = particle.y - centerY; + const distance = Math.sqrt(dx * dx + dy * dy); + const angle = Math.atan2(dy, dx); + + // 根据效果类型应用不同的物理力 + this.applyParticleForces(particle, angle, distance, progress, intensityBoost, canvas); + + // 更新粒子属性 + this.updateParticleProperties(particle, progress, isShortAnimation, timeMultiplier, intensityBoost); + }); + } + + /** + * 根据效果类型应用粒子力 + */ + private applyParticleForces( + particle: ParticleData, + angle: number, + distance: number, + progress: number, + intensityBoost: number, + canvas: HTMLCanvasElement + ): void { + const time = this.getAnimationTime(); + + switch (this.particleConfig.effectType) { + case 'explode': + const explodeIntensity = progress * this.particleConfig.strength * intensityBoost * 5; + particle.vx += Math.cos(angle) * explodeIntensity; + particle.vy += Math.sin(angle) * explodeIntensity; + break; + + case 'gravity': + this.applyGravityEffect(particle, progress, intensityBoost, canvas, time); + break; + + case 'vortex': + this.applyVortexEffect(particle, progress, intensityBoost, canvas, angle, distance); + break; + } + } + + /** + * 应用重力效果 + */ + private applyGravityEffect( + particle: ParticleData, + progress: number, + intensityBoost: number, + canvas: HTMLCanvasElement, + time: number + ): void { + const gravityThreshold = ((particle.originX + particle.originY * 0.7) / (canvas.width + canvas.height)) * 0.8; + + if (progress > gravityThreshold) { + const gravityProgress = (progress - gravityThreshold) / (1 - gravityThreshold); + const gravityForce = this.particleConfig.strength * gravityProgress * gravityProgress * 12 * intensityBoost; + + particle.vy += gravityForce; + + // 添加水平随机扰动 + const turbulence = Math.sin(time * 3 + particle.originX * 0.02) * Math.cos(time * 2 + particle.originY * 0.015); + particle.vx += turbulence * this.particleConfig.strength * 2 * intensityBoost; + } + } + + /** + * 应用漩涡效果 + */ + private applyVortexEffect( + particle: ParticleData, + progress: number, + intensityBoost: number, + canvas: HTMLCanvasElement, + angle: number, + distance: number + ): void { + const centerX = canvas.width / 2; + const centerY = canvas.height / 2; + + const spiralAngle = angle + progress * Math.PI * 0.8; + const targetRadius = distance + progress * Math.max(canvas.width, canvas.height) * 0.7 * 1.8; + + const targetX = centerX + Math.cos(spiralAngle) * targetRadius; + const targetY = centerY + Math.sin(spiralAngle) * targetRadius; + + const baseForce = progress * this.particleConfig.strength * 0.08 * intensityBoost; + + particle.vx += (targetX - particle.x) * baseForce; + particle.vy += (targetY - particle.y) * baseForce; + } + + /** + * 更新粒子生命周期属性 + */ + private updateParticleProperties( + particle: ParticleData, + progress: number, + isShortAnimation: boolean, + timeMultiplier: number, + intensityBoost: number + ): void { + // 应用阻力 + const dragCoeff = isShortAnimation ? 0.99 : 0.98; + particle.vx *= dragCoeff; + particle.vy *= dragCoeff; + + // 更新位置 + particle.x += particle.vx; + particle.y += particle.vy; + + // 更新生命值和透明度 + if (isShortAnimation) { + const lifeDecayRate = Math.max(0.1, 0.5 / timeMultiplier); + particle.life = Math.max(0, 1 - progress * lifeDecayRate); + particle.a = Math.max(0.2, particle.life * Math.min(1, particle.a * 1.2)); + particle.size = Math.max(this.particleConfig.size * 0.7, this.particleConfig.size * (0.5 + particle.life * 0.5)); + } else { + particle.life = Math.max(0, 1 - progress * 0.2); + particle.a = Math.max(0.1, particle.life * Math.min(1, particle.a * 1.5)); + particle.size = Math.max(this.particleConfig.size * 0.5, this.particleConfig.size * (0.3 + particle.life * 0.7)); + } + } + + /** + * 准备粒子数据并绘制 + */ + private prepareAndDrawParticles(gl: WebGLRenderingContext): void { + const positions = new Float32Array(this.particles.length * 2); + const colors = new Float32Array(this.particles.length * 4); + const sizes = new Float32Array(this.particles.length); + + this.particles.forEach((particle, i) => { + positions[i * 2] = particle.x; + positions[i * 2 + 1] = particle.y; + + colors[i * 4] = particle.r; + colors[i * 4 + 1] = particle.g; + colors[i * 4 + 2] = particle.b; + colors[i * 4 + 3] = Math.max(0.1, particle.a); + + sizes[i] = Math.max(6, particle.size * 1.5); + }); + + // 更新缓冲区 + this.updateParticleBuffers(gl, positions, colors, sizes); + + // 设置uniforms + this.setParticleUniforms(gl); + + // 绘制粒子 + gl.drawArrays(gl.POINTS, 0, this.particles.length); + + // 清理临时缓冲区 + this.cleanupTempBuffers(gl); + } + + /** + * 更新粒子缓冲区 + */ + private updateParticleBuffers( + gl: WebGLRenderingContext, + positions: Float32Array, + colors: Float32Array, + sizes: Float32Array + ): void { + // 位置缓冲区 + if (!this.positionBuffer) { + this.positionBuffer = gl.createBuffer(); + } + gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer); + gl.bufferData(gl.ARRAY_BUFFER, positions, gl.DYNAMIC_DRAW); + + const positionLocation = gl.getAttribLocation(this.program, 'a_position'); + gl.enableVertexAttribArray(positionLocation); + gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0); + + // 颜色缓冲区 + if (!this.colorBuffer) { + this.colorBuffer = gl.createBuffer(); + } + gl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer); + gl.bufferData(gl.ARRAY_BUFFER, colors, gl.DYNAMIC_DRAW); + + const colorLocation = gl.getAttribLocation(this.program, 'a_color'); + gl.enableVertexAttribArray(colorLocation); + gl.vertexAttribPointer(colorLocation, 4, gl.FLOAT, false, 0, 0); + + // 大小缓冲区 + const sizeBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, sizeBuffer); + gl.bufferData(gl.ARRAY_BUFFER, sizes, gl.DYNAMIC_DRAW); + + const sizeLocation = gl.getAttribLocation(this.program, 'a_size'); + gl.enableVertexAttribArray(sizeLocation); + gl.vertexAttribPointer(sizeLocation, 1, gl.FLOAT, false, 0, 0); + + // 保存临时缓冲区引用,用于清理 + (this as any)._tempSizeBuffer = sizeBuffer; + } + + /** + * 设置粒子着色器uniforms + */ + private setParticleUniforms(gl: WebGLRenderingContext): void { + const resolutionLocation = gl.getUniformLocation(this.program, 'u_resolution'); + const timeLocation = gl.getUniformLocation(this.program, 'u_time'); + const forceStrengthLocation = gl.getUniformLocation(this.program, 'u_forceStrength'); + const effectTypeLocation = gl.getUniformLocation(this.program, 'u_effectType'); + + gl.uniform2f(resolutionLocation, this.webglCanvas.width, this.webglCanvas.height); + gl.uniform1f(timeLocation, this.getAnimationTime()); + gl.uniform1f(forceStrengthLocation, this.particleConfig.strength); + + const effectTypeMap: { [key: string]: number } = { + explode: 0, + vortex: 1, + gravity: 2 + }; + gl.uniform1i(effectTypeLocation, effectTypeMap[this.particleConfig.effectType] || 0); + } + + /** + * 清理临时缓冲区 + */ + private cleanupTempBuffers(gl: WebGLRenderingContext): void { + const tempSizeBuffer = (this as any)._tempSizeBuffer; + if (tempSizeBuffer) { + gl.deleteBuffer(tempSizeBuffer); + delete (this as any)._tempSizeBuffer; + } + } + + // Canvas 2D回退实现的具体方法 + + /** + * Canvas 2D爆炸效果模拟 + */ + private applyCanvas2DExplode(ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, progress: number): void { + const centerX = canvas.width / 2; + const centerY = canvas.height / 2; + + // 简单的放大和透明度模拟爆炸效果 + ctx.save(); + ctx.globalAlpha = Math.max(0, 1 - progress); + ctx.translate(centerX, centerY); + const scale = 1 + progress * 0.5; + ctx.scale(scale, scale); + ctx.translate(-centerX, -centerY); + ctx.drawImage(canvas, 0, 0); + ctx.restore(); + } + + /** + * Canvas 2D重力效果模拟 + */ + private applyCanvas2DGravity(ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, progress: number): void { + // 使用垂直偏移和透明度模拟重力下落 + ctx.save(); + ctx.globalAlpha = Math.max(0, 1 - progress); + const offsetY = progress * canvas.height * 0.3; + ctx.drawImage(canvas, 0, offsetY); + ctx.restore(); + } + + /** + * Canvas 2D漩涡效果模拟 + */ + private applyCanvas2DVortex(ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, progress: number): void { + const centerX = canvas.width / 2; + const centerY = canvas.height / 2; + + // 使用旋转和透明度模拟漩涡效果 + ctx.save(); + ctx.globalAlpha = Math.max(0, 1 - progress); + ctx.translate(centerX, centerY); + ctx.rotate(progress * Math.PI * 2); + ctx.translate(-centerX, -centerY); + ctx.drawImage(canvas, 0, 0); + ctx.restore(); + } +} diff --git a/packages/vrender-animate/src/custom/disappear/pixelation.ts b/packages/vrender-animate/src/custom/disappear/pixelation.ts new file mode 100644 index 000000000..8696b0844 --- /dev/null +++ b/packages/vrender-animate/src/custom/disappear/pixelation.ts @@ -0,0 +1,95 @@ +import type { EasingType } from '@visactor/vrender-core'; +import { vglobal } from '@visactor/vrender-core'; +import { DisappearAnimateBase } from './base/DisappearAnimateBase'; + +export interface PixelationConfig { + maxPixelSize?: number; // 最大像素化强度 + method?: 'out' | 'in'; // 像素化方法:out为出场效果,in为入场效果 +} + +export class Pixelation extends DisappearAnimateBase { + private pixelationConfig: Required; + + constructor(from: null, to: null, duration: number, easing: EasingType, params: any) { + super(from, to, duration, easing, params); + + this.pixelationConfig = { + maxPixelSize: params?.options?.maxPixelSize || 20, + method: params?.options?.method || 'out' + }; + } + + // Canvas 2D 降采样像素化 + private applyDownsamplePixelation(canvas: HTMLCanvasElement, pixelSize: number): HTMLCanvasElement { + if (pixelSize <= 1) { + return canvas; + } + + const { width, height } = canvas; + + // 创建小尺寸的离屏Canvas + const smallWidth = Math.ceil(width / pixelSize); + const smallHeight = Math.ceil(height / pixelSize); + + const smallCanvas = vglobal.createCanvas({ + width: smallWidth, + height: smallHeight, + dpr: 1 + }); + const smallCtx = smallCanvas.getContext('2d'); + if (!smallCtx) { + return canvas; + } + + // 创建输出Canvas + const outputCanvas = vglobal.createCanvas({ + width: width, + height: height, + dpr: vglobal.devicePixelRatio + }); + const outputCtx = outputCanvas.getContext('2d'); + if (!outputCtx) { + return canvas; + } + + // 关闭图像平滑以获得清晰的像素块效果 + smallCtx.imageSmoothingEnabled = false; + outputCtx.imageSmoothingEnabled = false; + + // 将原图绘制到小Canvas上(自动降采样) + smallCtx.drawImage(canvas, 0, 0, smallWidth, smallHeight); + + // 将小图放大绘制到输出Canvas上 + outputCtx.drawImage(smallCanvas, 0, 0, width, height); + + return outputCanvas; + } + + private updateAnimationProgress(): number { + // 直接根据动画进度计算像素化强度 + + if (this.pixelationConfig.method === 'in') { + // 入场效果:从最大值逐渐减小到1 + const currentPixelSize = + this.pixelationConfig.maxPixelSize - this.currentAnimationRatio * (this.pixelationConfig.maxPixelSize - 1); + return currentPixelSize; + } + // 退场效果:从1逐渐增加到最大值(默认行为) + const currentPixelSize = 1 + this.currentAnimationRatio * (this.pixelationConfig.maxPixelSize - 1); + return currentPixelSize; + } + + protected afterStageRender(stage: any, canvas: HTMLCanvasElement): HTMLCanvasElement | void | null | false { + // 更新动画进度并获取当前像素化强度 + const currentPixelSize = this.updateAnimationProgress(); + + // 如果像素化强度为1或更小,直接返回原图 + if (currentPixelSize <= 1) { + return canvas; + } + + // 直接使用降采样像素化方法 + const result: HTMLCanvasElement = this.applyDownsamplePixelation(canvas, currentPixelSize); + return result; + } +} diff --git a/packages/vrender-animate/src/custom/register.ts b/packages/vrender-animate/src/custom/register.ts index 9fbbda35f..62fdf8d1b 100644 --- a/packages/vrender-animate/src/custom/register.ts +++ b/packages/vrender-animate/src/custom/register.ts @@ -46,6 +46,13 @@ import { MotionPath } from './motionPath'; import { FromTo } from './fromTo'; import { GroupFadeIn, GroupFadeOut } from './groupFade'; import { StreamLight } from './streamLight'; +import { Dissolve } from './disappear/dissolve'; +import { Grayscale } from './disappear/grayscale'; +import { Distortion } from './disappear/distortion'; +import { Particle } from './disappear/particle'; +import { Glitch } from './disappear/glitch'; +import { GaussianBlur } from './disappear/gaussian-blur'; +import { Pixelation } from './disappear/pixelation'; export const registerCustomAnimate = () => { // 基础动画 @@ -118,6 +125,15 @@ export const registerCustomAnimate = () => { AnimateExecutor.registerBuiltInAnimate('MotionPath', MotionPath); // 流光动画 AnimateExecutor.registerBuiltInAnimate('streamLight', StreamLight); + + // 退场动画 + AnimateExecutor.registerBuiltInAnimate('dissolve', Dissolve); + AnimateExecutor.registerBuiltInAnimate('grayscale', Grayscale); + AnimateExecutor.registerBuiltInAnimate('distortion', Distortion); + AnimateExecutor.registerBuiltInAnimate('particle', Particle); + AnimateExecutor.registerBuiltInAnimate('glitch', Glitch); + AnimateExecutor.registerBuiltInAnimate('gaussianBlur', GaussianBlur); + AnimateExecutor.registerBuiltInAnimate('pixelation', Pixelation); }; export { @@ -176,5 +192,12 @@ export { GroupFadeIn, GroupFadeOut, FromTo, - StreamLight + StreamLight, + Dissolve, + Grayscale, + Distortion, + Particle, + Glitch, + GaussianBlur, + Pixelation }; diff --git a/packages/vrender-animate/src/step.ts b/packages/vrender-animate/src/step.ts index 342f53852..d7b9bc504 100644 --- a/packages/vrender-animate/src/step.ts +++ b/packages/vrender-animate/src/step.ts @@ -329,9 +329,7 @@ export class WaitStep extends Step { onStart(): void { super.onStart(); - } - onFirstRun(): void { - // 设置上一个阶段的props到attribute + const fromProps = this.getFromProps(); this.target.setAttributes(fromProps); } diff --git a/packages/vrender-components/CHANGELOG.json b/packages/vrender-components/CHANGELOG.json index d42c364f2..484090329 100644 --- a/packages/vrender-components/CHANGELOG.json +++ b/packages/vrender-components/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/vrender-components", "entries": [ + { + "version": "1.0.13", + "tag": "@visactor/vrender-components_v1.0.13", + "date": "Tue, 26 Aug 2025 11:35:34 GMT", + "comments": {} + }, { "version": "1.0.12", "tag": "@visactor/vrender-components_v1.0.12", diff --git a/packages/vrender-components/CHANGELOG.md b/packages/vrender-components/CHANGELOG.md index 79c7427b2..d77c0cfa9 100644 --- a/packages/vrender-components/CHANGELOG.md +++ b/packages/vrender-components/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/vrender-components -This log was last generated on Wed, 20 Aug 2025 07:07:52 GMT and should not be manually modified. +This log was last generated on Tue, 26 Aug 2025 11:35:34 GMT and should not be manually modified. + +## 1.0.13 +Tue, 26 Aug 2025 11:35:34 GMT + +_Version update only_ ## 1.0.12 Wed, 20 Aug 2025 07:07:52 GMT diff --git a/packages/vrender-components/package.json b/packages/vrender-components/package.json index 64207876b..63565ad4d 100644 --- a/packages/vrender-components/package.json +++ b/packages/vrender-components/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-components", - "version": "1.0.12", + "version": "1.0.13", "description": "components library for dp visualization", "sideEffects": false, "main": "cjs/index.js", @@ -27,9 +27,9 @@ "dependencies": { "@visactor/vutils": "1.0.6", "@visactor/vscale": "1.0.6", - "@visactor/vrender-core": "workspace:1.0.12", - "@visactor/vrender-kits": "workspace:1.0.12", - "@visactor/vrender-animate": "workspace:1.0.12" + "@visactor/vrender-core": "workspace:1.0.13", + "@visactor/vrender-kits": "workspace:1.0.13", + "@visactor/vrender-animate": "workspace:1.0.13" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/packages/vrender-core/CHANGELOG.json b/packages/vrender-core/CHANGELOG.json index 0e863604d..b8b9e3e0a 100644 --- a/packages/vrender-core/CHANGELOG.json +++ b/packages/vrender-core/CHANGELOG.json @@ -1,6 +1,21 @@ { "name": "@visactor/vrender-core", "entries": [ + { + "version": "1.0.13", + "tag": "@visactor/vrender-core_v1.0.13", + "date": "Tue, 26 Aug 2025 11:35:34 GMT", + "comments": { + "none": [ + { + "comment": "feat: linear-gradient support ignore percent, closed #1926" + }, + { + "comment": "fix: fix issue with scaled shadowBounds and editLine in richtext-edit-plugin, closed #1911" + } + ] + } + }, { "version": "1.0.12", "tag": "@visactor/vrender-core_v1.0.12", diff --git a/packages/vrender-core/CHANGELOG.md b/packages/vrender-core/CHANGELOG.md index bc6a66f47..952adf7a6 100644 --- a/packages/vrender-core/CHANGELOG.md +++ b/packages/vrender-core/CHANGELOG.md @@ -1,6 +1,14 @@ # Change Log - @visactor/vrender-core -This log was last generated on Wed, 20 Aug 2025 07:07:52 GMT and should not be manually modified. +This log was last generated on Tue, 26 Aug 2025 11:35:34 GMT and should not be manually modified. + +## 1.0.13 +Tue, 26 Aug 2025 11:35:34 GMT + +### Updates + +- feat: linear-gradient support ignore percent, closed #1926 +- fix: fix issue with scaled shadowBounds and editLine in richtext-edit-plugin, closed #1911 ## 1.0.12 Wed, 20 Aug 2025 07:07:52 GMT diff --git a/packages/vrender-core/package.json b/packages/vrender-core/package.json index 8bc5734e2..5b2194125 100644 --- a/packages/vrender-core/package.json +++ b/packages/vrender-core/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-core", - "version": "1.0.12", + "version": "1.0.13", "description": "", "sideEffects": [ "./src/modules.ts", diff --git a/packages/vrender-core/src/common/color-utils.ts b/packages/vrender-core/src/common/color-utils.ts index 8c85830ec..1c6024712 100644 --- a/packages/vrender-core/src/common/color-utils.ts +++ b/packages/vrender-core/src/common/color-utils.ts @@ -371,6 +371,60 @@ export class GradientParser { } return c; } + static processColorStops( + colorStops: Array<{ value: string; length?: { value: string } }> + ): { color: string; offset: number }[] { + if (!colorStops || colorStops.length === 0) { + return []; + } + + const anyStopHasLength = colorStops.some((item: any) => item.length); + + if (anyStopHasLength) { + const stops = colorStops.map((item: any) => ({ + color: item.value, + offset: item.length ? parseFloat(item.length.value) / 100 : -1 + })); + + // If first color stop has no position, it defaults to 0% + if (stops[0].offset < 0) { + stops[0].offset = 0; + } + + // If last color stop has no position, it defaults to 100% + if (stops[stops.length - 1].offset < 0) { + stops[stops.length - 1].offset = 1; + } + + // If a color stop in between has no position, its position is the average of the preceding and succeeding color stops with positions. + for (let i = 1; i < stops.length - 1; i++) { + if (stops[i].offset < 0) { + const prevWithOffsetIdx = i - 1; + let nextWithOffsetIdx = i + 1; + while (nextWithOffsetIdx < stops.length && stops[nextWithOffsetIdx].offset < 0) { + nextWithOffsetIdx++; + } + + const startOffset = stops[prevWithOffsetIdx].offset; + const endOffset = stops[nextWithOffsetIdx].offset; + const unspecCount = nextWithOffsetIdx - prevWithOffsetIdx; + + for (let j = 1; j < unspecCount; j++) { + stops[prevWithOffsetIdx + j].offset = startOffset + ((endOffset - startOffset) * j) / unspecCount; + } + i = nextWithOffsetIdx - 1; + } + } + return stops; + } + return colorStops.map((item: any, index: number) => { + const offset = colorStops.length > 1 ? index / (colorStops.length - 1) : 0; + return { + color: item.value, + offset + }; + }); + } private static ParseConic(datum: any): IConicalGradient { const { orientation, colorStops = [] } = datum; const halfPi = pi / 2; @@ -381,12 +435,7 @@ export class GradientParser { y: 0.5, startAngle: sa, endAngle: sa + pi2, - stops: colorStops.map((item: any) => { - return { - color: item.value, - offset: parseFloat(item.length.value) / 100 - }; - }) + stops: GradientParser.processColorStops(colorStops) }; } private static ParseRadial(datum: any): IRadialGradient { @@ -399,12 +448,7 @@ export class GradientParser { y1: 0.5, r0: 0, r1: 1, - stops: colorStops.map((item: any) => { - return { - color: item.value, - offset: parseFloat(item.length.value) / 100 - }; - }) + stops: GradientParser.processColorStops(colorStops) }; } private static ParseLinear(datum: any): ILinearGradient { @@ -448,12 +492,7 @@ export class GradientParser { y0, x1, y1, - stops: colorStops.map((item: any) => { - return { - color: item.value, - offset: parseFloat(item.length.value) / 100 - }; - }) + stops: GradientParser.processColorStops(colorStops) }; } } diff --git a/packages/vrender-core/src/graphic/richtext.ts b/packages/vrender-core/src/graphic/richtext.ts index 2cb3eaef9..7cc11baf0 100644 --- a/packages/vrender-core/src/graphic/richtext.ts +++ b/packages/vrender-core/src/graphic/richtext.ts @@ -310,6 +310,13 @@ export class RichText extends Graphic implements IRic default: break; } + if (!height) { + if (this.verticalDirection === 'middle') { + deltaY -= aabbBounds.height() / 2; + } else if (this.verticalDirection === 'bottom') { + deltaY -= aabbBounds.height(); + } + } aabbBounds.translate(deltaX, deltaY); application.graphicService.updateTempAABBBounds(aabbBounds); diff --git a/packages/vrender-core/src/index.ts b/packages/vrender-core/src/index.ts index a999a6b29..81f694540 100644 --- a/packages/vrender-core/src/index.ts +++ b/packages/vrender-core/src/index.ts @@ -21,6 +21,7 @@ export * from './factory'; /* export common */ export * from './common/text'; +export * from './common/color-utils'; export * from './common/bezier-utils'; export * from './common/bounds-context'; export * from './common/seg-context'; diff --git a/packages/vrender-core/src/plugins/builtin-plugin/richtext-edit-plugin.ts b/packages/vrender-core/src/plugins/builtin-plugin/richtext-edit-plugin.ts index b37593641..90094f491 100644 --- a/packages/vrender-core/src/plugins/builtin-plugin/richtext-edit-plugin.ts +++ b/packages/vrender-core/src/plugins/builtin-plugin/richtext-edit-plugin.ts @@ -632,6 +632,8 @@ export class RichTextEditPlugin implements IPlugin { this.shadowBounds.setAttributes({ x: 0, y: 0, + scaleX: 1 / (this.currRt.attribute.scaleX ?? 1), + scaleY: 1 / (this.currRt.attribute.scaleY ?? 1), width, height, fill: false, @@ -815,7 +817,7 @@ export class RichTextEditPlugin implements IPlugin { } else { const x = 0; const y1 = 0; - const y2 = getRichTextBounds({ ...target.attribute, textConfig: [{ text: 'a' }] }).height(); + const y2 = getRichTextBounds({ ...target.attribute, textConfig: [{ text: 'a' }], scaleX: 1, scaleY: 1 }).height(); this.startCursorPos = { x, y: (y1 + y2) / 2 }; this.curCursorIdx = -0.1; this.selectionStartCursorIdx = -0.1; @@ -862,7 +864,7 @@ export class RichTextEditPlugin implements IPlugin { if (!attr.textConfig.length) { attr = { ...attr, textConfig: [{ text: 'a' }] }; } - b = getRichTextBounds(attr); + b = getRichTextBounds({ ...attr, scaleX: 1, scaleY: 1 }); } if (textBaseline === 'middle') { dy = -b.height() / 2; @@ -1218,10 +1220,10 @@ export class RichTextEditPlugin implements IPlugin { const { textBaseline } = rt.attribute; let dy = 0; if (textBaseline === 'middle') { - const b = getRichTextBounds(rt.attribute); + const b = getRichTextBounds({ ...rt.attribute, scaleX: 1, scaleY: 1 }); dy = b.height() / 2; } else if (textBaseline === 'bottom') { - const b = getRichTextBounds(rt.attribute); + const b = getRichTextBounds({ ...rt.attribute, scaleX: 1, scaleY: 1 }); dy = b.height(); } p1.y += dy; diff --git a/packages/vrender-kits/CHANGELOG.json b/packages/vrender-kits/CHANGELOG.json index 75491eb50..25eaa56d5 100644 --- a/packages/vrender-kits/CHANGELOG.json +++ b/packages/vrender-kits/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/vrender-kits", "entries": [ + { + "version": "1.0.13", + "tag": "@visactor/vrender-kits_v1.0.13", + "date": "Tue, 26 Aug 2025 11:35:34 GMT", + "comments": {} + }, { "version": "1.0.12", "tag": "@visactor/vrender-kits_v1.0.12", diff --git a/packages/vrender-kits/CHANGELOG.md b/packages/vrender-kits/CHANGELOG.md index 37fdeb8bd..0dc20181e 100644 --- a/packages/vrender-kits/CHANGELOG.md +++ b/packages/vrender-kits/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/vrender-kits -This log was last generated on Wed, 20 Aug 2025 07:07:52 GMT and should not be manually modified. +This log was last generated on Tue, 26 Aug 2025 11:35:34 GMT and should not be manually modified. + +## 1.0.13 +Tue, 26 Aug 2025 11:35:34 GMT + +_Version update only_ ## 1.0.12 Wed, 20 Aug 2025 07:07:52 GMT diff --git a/packages/vrender-kits/package.json b/packages/vrender-kits/package.json index 259dc8592..3f20da9c1 100644 --- a/packages/vrender-kits/package.json +++ b/packages/vrender-kits/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-kits", - "version": "1.0.12", + "version": "1.0.13", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -21,7 +21,7 @@ }, "dependencies": { "@visactor/vutils": "1.0.6", - "@visactor/vrender-core": "workspace:1.0.12", + "@visactor/vrender-core": "workspace:1.0.13", "@resvg/resvg-js": "2.4.1", "roughjs": "4.5.2", "gifuct-js": "2.1.2", diff --git a/packages/vrender/CHANGELOG.json b/packages/vrender/CHANGELOG.json index 02a4775eb..54cdc6e43 100644 --- a/packages/vrender/CHANGELOG.json +++ b/packages/vrender/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/vrender", "entries": [ + { + "version": "1.0.13", + "tag": "@visactor/vrender_v1.0.13", + "date": "Tue, 26 Aug 2025 11:35:34 GMT", + "comments": {} + }, { "version": "1.0.12", "tag": "@visactor/vrender_v1.0.12", diff --git a/packages/vrender/CHANGELOG.md b/packages/vrender/CHANGELOG.md index bca7f743e..407e9c915 100644 --- a/packages/vrender/CHANGELOG.md +++ b/packages/vrender/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/vrender -This log was last generated on Wed, 20 Aug 2025 07:07:52 GMT and should not be manually modified. +This log was last generated on Tue, 26 Aug 2025 11:35:34 GMT and should not be manually modified. + +## 1.0.13 +Tue, 26 Aug 2025 11:35:34 GMT + +_Version update only_ ## 1.0.12 Wed, 20 Aug 2025 07:07:52 GMT diff --git a/packages/vrender/__tests__/browser/src/pages/rect.ts b/packages/vrender/__tests__/browser/src/pages/rect.ts index 93f71d824..bfe6d7d67 100644 --- a/packages/vrender/__tests__/browser/src/pages/rect.ts +++ b/packages/vrender/__tests__/browser/src/pages/rect.ts @@ -1,7 +1,7 @@ import { createStage, container, createRect, IGraphic, createGroup, createSymbol } from '@visactor/vrender'; import { roughModule } from '@visactor/vrender-kits'; -container.load(roughModule); +// container.load(roughModule); export const page = () => { const graphics: IGraphic[] = []; // graphics.push( @@ -78,8 +78,8 @@ export const page = () => { stroke: 'red', // scaleCenter: ['50%', '50%'], // _debug_bounds: true, - fill: 'conic-gradient(from 90deg, rgba(5,0,255,1) 16%, rgba(0,255,10,1) 41%, rgba(9,9,121,1) 53%, rgba(0,212,255,1) 100%)', - // fill: 'linear-gradient(90deg, #215F97 0%, #FF948F 100%)', + // fill: 'conic-gradient(from 90deg, rgba(5,0,255,1) 16%, rgba(0,255,10,1) 41%, rgba(9,9,121,1) 53%, rgba(0,212,255,1) 100%)', + fill: 'linear-gradient(90deg, #215F97, #FF948F)', // cornerRadius: [5, 10, 15, 20], lineWidth: 5, anchor: ['50%', '50%'], diff --git a/packages/vrender/__tests__/browser/src/pages/richtext-editor.ts b/packages/vrender/__tests__/browser/src/pages/richtext-editor.ts index 7501b9c0c..220e9bfdb 100644 --- a/packages/vrender/__tests__/browser/src/pages/richtext-editor.ts +++ b/packages/vrender/__tests__/browser/src/pages/richtext-editor.ts @@ -39,55 +39,35 @@ export const page = () => { graphicBaseline: 'middle', fill: '#1F2329', ignoreBuf: true, - anchor: [-162.07207758976318, 216.49803822714284], - angle: 0, + // anchor: [-162.07207758976318, 216.49803822714284], + // angle: 0, editOptions: { placeholder: '请输入文本', placeholderColor: '#B3B8C3', keepHeightWhileEmpty: true, - boundsStrokeWhenInput: '#3073F2', + boundsStrokeWhenInput: 'red', syncPlaceholderToTextConfig: false, stopPropagation: true }, fontFamily: 'D-Din', height: 0, - heightLimit: 999999, - lineHeight: '150%', - maxWidth: 120, + // heightLimit: 999999, + // lineHeight: '150%', + // maxWidth: 120, strokeBoundsBuffer: -1, - textBaseline: 'top', + scaleX: 3, + scaleY: 3, + _debug_bounds: true, + // textBaseline: 'top', textConfig: [ { fill: '#1F2329', stroke: false, fontSize: 16, fontWeight: 'normal', - fontFamily: 'D-Din', - lineHeight: '150%', - text: 'a', - dy: 10, - isComposing: false - }, - { - fill: '#1F2329', - stroke: false, - fontSize: 16, - fontWeight: 'normal', - fontFamily: 'D-Din', - lineHeight: '150%', - text: 'b', - dy: 20, - isComposing: false - }, - { - fill: '#1F2329', - stroke: false, - fontSize: 16, - fontWeight: 'normal', - fontFamily: 'D-Din', - lineHeight: '150%', - text: 'c', - dx: 30, + // fontFamily: 'D-Din', + // lineHeight: '150%', + text: '这是什么内容', isComposing: false } ], @@ -95,7 +75,7 @@ export const page = () => { lineHeight: true, multiBreakLine: true }, - verticalDirection: 'middle', + verticalDirection: 'bottom', width: 0 }) ); diff --git a/packages/vrender/__tests__/common/color.test.ts b/packages/vrender/__tests__/common/color.test.ts new file mode 100644 index 000000000..92d97760d --- /dev/null +++ b/packages/vrender/__tests__/common/color.test.ts @@ -0,0 +1,28 @@ +import { GradientParser } from '../../src/index'; + +it('gradient-color', () => { + expect(GradientParser.processColorStops([{ value: 'rgb(100, 0, 0)' }, { value: 'rgb(0, 100, 0)' }])).toEqual([ + { color: 'rgb(100, 0, 0)', offset: 0 }, + { color: 'rgb(0, 100, 0)', offset: 1 } + ]); + expect( + GradientParser.processColorStops([ + { value: 'rgb(100, 0, 0)', length: { value: '0' } }, + { value: 'rgb(0, 100, 0)', length: { value: '100' } } + ]) + ).toEqual([ + { color: 'rgb(100, 0, 0)', offset: 0 }, + { color: 'rgb(0, 100, 0)', offset: 1 } + ]); + expect( + GradientParser.processColorStops([ + { value: 'rgb(100, 0, 0)', length: { value: '0' } }, + { value: 'rgb(0, 0, 100)', length: { value: '30' } }, + { value: 'rgb(0, 100, 0)', length: { value: '100' } } + ]) + ).toEqual([ + { color: 'rgb(100, 0, 0)', offset: 0 }, + { color: 'rgb(0, 0, 100)', offset: 0.3 }, + { color: 'rgb(0, 100, 0)', offset: 1 } + ]); +}); diff --git a/packages/vrender/jest.config.js b/packages/vrender/jest.config.js index a569fa5b8..809c0c42c 100644 --- a/packages/vrender/jest.config.js +++ b/packages/vrender/jest.config.js @@ -45,6 +45,7 @@ module.exports = { }, moduleNameMapper: { '@visactor/vrender-kits': path.resolve(__dirname, '../vrender-kits/src/index.ts'), - '@visactor/vrender-core': path.resolve(__dirname, '../vrender-core/src/index.ts') + '@visactor/vrender-core': path.resolve(__dirname, '../vrender-core/src/index.ts'), + '@visactor/vrender-animate': path.resolve(__dirname, '../vrender-animate/src/index.ts') } }; diff --git a/packages/vrender/package.json b/packages/vrender/package.json index 9f6042c80..9d61dc2c3 100644 --- a/packages/vrender/package.json +++ b/packages/vrender/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender", - "version": "1.0.12", + "version": "1.0.13", "description": "", "sideEffects": true, "main": "cjs/index.js", @@ -24,9 +24,9 @@ "test-watch": "cross-env DEBUG_MODE=1 jest --watch" }, "dependencies": { - "@visactor/vrender-core": "workspace:1.0.12", - "@visactor/vrender-kits": "workspace:1.0.12", - "@visactor/vrender-animate": "workspace:1.0.12" + "@visactor/vrender-core": "workspace:1.0.13", + "@visactor/vrender-kits": "workspace:1.0.13", + "@visactor/vrender-animate": "workspace:1.0.13" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/packages/vrender/tsconfig.test.json b/packages/vrender/tsconfig.test.json index d2021a0ef..bb1aab707 100644 --- a/packages/vrender/tsconfig.test.json +++ b/packages/vrender/tsconfig.test.json @@ -4,7 +4,7 @@ "paths": { "@visactor/vrender-core": ["../vrender-core/src"], "@visactor/vrender-kits": ["../vrender-kits/src"], - "@visactor/vrender-animate": ["../vrender-aniamte/src"] + "@visactor/vrender-animate": ["../vrender-animate/src"] } }, "references": [] diff --git a/tools/bugserver-trigger/package.json b/tools/bugserver-trigger/package.json index 7565536ec..c84bfa474 100644 --- a/tools/bugserver-trigger/package.json +++ b/tools/bugserver-trigger/package.json @@ -8,11 +8,11 @@ "ci": "ts-node --transpileOnly --skipProject ./scripts/trigger-test.ts" }, "dependencies": { - "@visactor/vrender": "workspace:1.0.12", - "@visactor/vrender-core": "workspace:1.0.12", - "@visactor/vrender-kits": "workspace:1.0.12", - "@visactor/vrender-components": "workspace:1.0.12", - "@visactor/vrender-animate": "workspace:1.0.12" + "@visactor/vrender": "workspace:1.0.13", + "@visactor/vrender-core": "workspace:1.0.13", + "@visactor/vrender-kits": "workspace:1.0.13", + "@visactor/vrender-components": "workspace:1.0.13", + "@visactor/vrender-animate": "workspace:1.0.13" }, "devDependencies": { "@rushstack/eslint-patch": "~1.1.4",