UNPKG

@visactor/vrender-animate

Version:

This module provides a graph-based animation system for VRender.

186 lines (178 loc) 12.4 kB
import { HybridEffectBase } from "./base/CustomEffectBase"; import { ImageProcessUtils } from "./base/ImageProcessUtils"; export class Particle extends HybridEffectBase { constructor(from, to, duration, easing, params) { var _a, _b, _c, _d, _e; super(from, to, duration, easing, params), this.particles = [], this.positionBuffer = null, this.colorBuffer = null, this.particleConfig = { effectType: (null === (_a = null == params ? void 0 : params.options) || void 0 === _a ? void 0 : _a.effectType) || "gravity", count: (null === (_b = null == params ? void 0 : params.options) || void 0 === _b ? void 0 : _b.count) || 4e3, size: (null === (_c = null == params ? void 0 : params.options) || void 0 === _c ? void 0 : _c.size) || 20, strength: (null === (_d = null == params ? void 0 : params.options) || void 0 === _d ? void 0 : _d.strength) || 1.5, useWebGL: void 0 === (null === (_e = null == params ? void 0 : params.options) || void 0 === _e ? void 0 : _e.useWebGL) || params.options.useWebGL }; } getShaderSources() { return { vertex: "\n attribute vec2 a_position;\n attribute vec4 a_color;\n attribute float a_size;\n\n uniform vec2 u_resolution;\n uniform float u_time;\n uniform float u_forceStrength;\n uniform int u_effectType;\n\n varying vec4 v_color;\n\n void main() {\n // 将像素坐标转换为剪辑空间坐标\n vec2 clipSpace = ((a_position / u_resolution) * 2.0) - 1.0;\n clipSpace.y = -clipSpace.y; // 翻转Y轴\n\n gl_Position = vec4(clipSpace, 0.0, 1.0);\n gl_PointSize = a_size;\n v_color = a_color;\n }\n ", fragment: "\n precision mediump float;\n varying vec4 v_color;\n\n void main() {\n // 创建圆形粒子\n vec2 coord = gl_PointCoord - vec2(0.5);\n float distance = length(coord);\n\n if (distance > 0.5) {\n discard;\n }\n\n // 保持原始颜色,只调整透明度渐变\n gl_FragColor = vec4(v_color.rgb, v_color.a);\n }\n " }; } applyWebGLEffect(canvas) { if (!this.gl || !this.program || !this.webglCanvas) return null; this.setupWebGLState(canvas), 0 === this.particles.length && this.extractParticles(canvas), this.updateParticles(canvas); const gl = this.gl; return gl.enable(gl.BLEND), gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA), gl.useProgram(this.program), this.prepareAndDrawParticles(gl), this.webglCanvas; } applyCanvas2DEffect(canvas) { const output = this.createOutputCanvas(canvas); if (!output) return null; const {canvas: outputCanvas, ctx: ctx} = output, progress = this.currentAnimationRatio; 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; } extractParticles(canvas) { const tempCanvas = ImageProcessUtils.createTempCanvas(canvas.width, canvas.height, 1), tempCtx = tempCanvas.getContext("2d"); if (!tempCtx) return; tempCtx.drawImage(canvas, 0, 0, tempCanvas.width, tempCanvas.height); const data = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height).data; this.particles = []; const step = Math.max(1, Math.floor(Math.sqrt(tempCanvas.width * tempCanvas.height / (1.5 * this.particleConfig.count)))); for (let y = 0; y < tempCanvas.height; y += step) for (let x = 0; x < tempCanvas.width; x += step) { const index = 4 * (y * tempCanvas.width + x), r = data[index], g = data[index + 1], b = data[index + 2], a = data[index + 3]; if (a > 5) { const realX = x / tempCanvas.width * canvas.width, realY = y / tempCanvas.height * canvas.height, particle = { x: realX, y: realY, originX: realX, originY: realY, vx: 0, vy: 0, r: r / 255, g: g / 255, b: b / 255, a: Math.max(.6, a / 255), life: 1, size: this.particleConfig.size * (1 + .5 * Math.random()) }; this.particles.push(particle); } } } updateParticles(canvas) { const centerX = canvas.width / 2, centerY = canvas.height / 2, progress = this.currentAnimationRatio, duration = this.getDurationFromParent(), isShortAnimation = duration < 2e3, timeMultiplier = isShortAnimation ? Math.max(1.5, 3e3 / duration) : 1, intensityBoost = isShortAnimation ? Math.min(2, 2e3 / duration) : 1; this.particles.forEach((particle => { const dx = particle.x - centerX, dy = particle.y - centerY, distance = Math.sqrt(dx * dx + dy * dy), angle = Math.atan2(dy, dx); this.applyParticleForces(particle, angle, distance, progress, intensityBoost, canvas), this.updateParticleProperties(particle, progress, isShortAnimation, timeMultiplier, intensityBoost); })); } applyParticleForces(particle, angle, distance, progress, intensityBoost, canvas) { 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); } } applyGravityEffect(particle, progress, intensityBoost, canvas, time) { const gravityThreshold = (particle.originX + .7 * particle.originY) / (canvas.width + canvas.height) * .8; if (progress > gravityThreshold) { const gravityProgress = (progress - gravityThreshold) / (1 - gravityThreshold), gravityForce = this.particleConfig.strength * gravityProgress * gravityProgress * 12 * intensityBoost; particle.vy += gravityForce; const turbulence = Math.sin(3 * time + .02 * particle.originX) * Math.cos(2 * time + .015 * particle.originY); particle.vx += turbulence * this.particleConfig.strength * 2 * intensityBoost; } } applyVortexEffect(particle, progress, intensityBoost, canvas, angle, distance) { const centerX = canvas.width / 2, centerY = canvas.height / 2, spiralAngle = angle + progress * Math.PI * .8, targetRadius = distance + progress * Math.max(canvas.width, canvas.height) * .7 * 1.8, targetX = centerX + Math.cos(spiralAngle) * targetRadius, targetY = centerY + Math.sin(spiralAngle) * targetRadius, baseForce = progress * this.particleConfig.strength * .08 * intensityBoost; particle.vx += (targetX - particle.x) * baseForce, particle.vy += (targetY - particle.y) * baseForce; } updateParticleProperties(particle, progress, isShortAnimation, timeMultiplier, intensityBoost) { const dragCoeff = isShortAnimation ? .99 : .98; if (particle.vx *= dragCoeff, particle.vy *= dragCoeff, particle.x += particle.vx, particle.y += particle.vy, isShortAnimation) { const lifeDecayRate = Math.max(.1, .5 / timeMultiplier); particle.life = Math.max(0, 1 - progress * lifeDecayRate), particle.a = Math.max(.2, particle.life * Math.min(1, 1.2 * particle.a)), particle.size = Math.max(.7 * this.particleConfig.size, this.particleConfig.size * (.5 + .5 * particle.life)); } else particle.life = Math.max(0, 1 - .2 * progress), particle.a = Math.max(.1, particle.life * Math.min(1, 1.5 * particle.a)), particle.size = Math.max(.5 * this.particleConfig.size, this.particleConfig.size * (.3 + .7 * particle.life)); } prepareAndDrawParticles(gl) { const positions = new Float32Array(2 * this.particles.length), colors = new Float32Array(4 * this.particles.length), sizes = new Float32Array(this.particles.length); this.particles.forEach(((particle, i) => { positions[2 * i] = particle.x, positions[2 * i + 1] = particle.y, colors[4 * i] = particle.r, colors[4 * i + 1] = particle.g, colors[4 * i + 2] = particle.b, colors[4 * i + 3] = Math.max(.1, particle.a), sizes[i] = Math.max(6, 1.5 * particle.size); })), this.updateParticleBuffers(gl, positions, colors, sizes), this.setParticleUniforms(gl), gl.drawArrays(gl.POINTS, 0, this.particles.length), this.cleanupTempBuffers(gl); } updateParticleBuffers(gl, positions, colors, sizes) { 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, !1, 0, 0), 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, !1, 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, !1, 0, 0), this._tempSizeBuffer = sizeBuffer; } setParticleUniforms(gl) { const resolutionLocation = gl.getUniformLocation(this.program, "u_resolution"), timeLocation = gl.getUniformLocation(this.program, "u_time"), forceStrengthLocation = gl.getUniformLocation(this.program, "u_forceStrength"), 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); gl.uniform1i(effectTypeLocation, { explode: 0, vortex: 1, gravity: 2 }[this.particleConfig.effectType] || 0); } cleanupTempBuffers(gl) { const tempSizeBuffer = this._tempSizeBuffer; tempSizeBuffer && (gl.deleteBuffer(tempSizeBuffer), delete this._tempSizeBuffer); } applyCanvas2DExplode(ctx, canvas, progress) { const centerX = canvas.width / 2, centerY = canvas.height / 2; ctx.save(), ctx.globalAlpha = Math.max(0, 1 - progress), ctx.translate(centerX, centerY); const scale = 1 + .5 * progress; ctx.scale(scale, scale), ctx.translate(-centerX, -centerY), ctx.drawImage(canvas, 0, 0), ctx.restore(); } applyCanvas2DGravity(ctx, canvas, progress) { ctx.save(), ctx.globalAlpha = Math.max(0, 1 - progress); const offsetY = progress * canvas.height * .3; ctx.drawImage(canvas, 0, offsetY), ctx.restore(); } applyCanvas2DVortex(ctx, canvas, progress) { const centerX = canvas.width / 2, 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(); } } //# sourceMappingURL=particle.js.map