完成阶段3-4
This commit is contained in:
1 parent
f0f4104b0c
commit
4e2e123269
5 files changed
+1158
-50
No files matched your search
+275
-29
@@ -115,10 +115,18 @@ export interface EmitterConfig {
|
||||
rotationOverLifeMode: AttributeMode
|
||||
rotationOverLifeCurve: CurvePoint[]
|
||||
// 物理
|
||||
/** 旧版重力值,保留用于配置迁移 */
|
||||
gravity: number
|
||||
gravityMode: 'fixed' | 'random'
|
||||
gravityMin: number
|
||||
gravityMax: number
|
||||
/** 数学角度:0 向右,-90 向屏幕下方 */
|
||||
gravityDirection: number
|
||||
/** 生命周期内重力倍率,纵轴范围 0~1 */
|
||||
gravityCurve: CurvePoint[]
|
||||
/** 阻尼(速度衰减系数,0=无) */
|
||||
damping: number
|
||||
/** 随机种子(确定性随机,0=每次不同) */
|
||||
/** 全局随机种子;包括 0 在内的任意整数都可确定性复现 */
|
||||
seed: number
|
||||
// 颜色
|
||||
colorStart: string
|
||||
@@ -149,17 +157,43 @@ export interface EmitterConfig {
|
||||
// ===== 修改器 (Modifiers) =====
|
||||
/** 开启移动噪声 */
|
||||
noise: boolean
|
||||
/** 噪声影响比例(0~100%) */
|
||||
noiseAmt: number
|
||||
/** 粒子偏离原轨迹的最大距离(px) */
|
||||
noiseRange: number
|
||||
/** 噪声每秒变化速度 */
|
||||
noiseSpeed: number
|
||||
/** 开启重力(由 gravity 控制) */
|
||||
useGravity: boolean
|
||||
/** 开启风力 */
|
||||
wind: boolean
|
||||
/** 旧版二维风力,保留用于配置迁移 */
|
||||
windX: number
|
||||
windY: number
|
||||
windMode: 'fixed' | 'random'
|
||||
windMin: number
|
||||
windMax: number
|
||||
/** 数学角度:0 向右,-90 向屏幕下方 */
|
||||
windDirection: number
|
||||
/** 生命周期内风力倍率,纵轴范围 0~1 */
|
||||
windCurve: CurvePoint[]
|
||||
/** 开启力场(吸力/斥力) */
|
||||
forceField: boolean
|
||||
forceStrength: number
|
||||
forceRadius: number
|
||||
/** 力场中心使用场景全局数学坐标 */
|
||||
forceCenterX: number
|
||||
forceCenterY: number
|
||||
forceFalloff: 'constant' | 'linear' | 'inverse'
|
||||
/** 开启吸附:粒子在延迟后从当前位置移动到全局吸附点,到达范围后死亡 */
|
||||
attraction: boolean
|
||||
attractionCenterX: number
|
||||
attractionCenterY: number
|
||||
attractionRadius: number
|
||||
attractionDuration: number
|
||||
attractionDelay: number
|
||||
attractionDelayMode: 'global' | 'particle'
|
||||
attractionPath: 'linear' | 'curve'
|
||||
/** 开启拖尾 */
|
||||
trail: boolean
|
||||
/** 开启碰撞(预留) */
|
||||
@@ -269,7 +303,12 @@ export function defaultConfig(): EmitterConfig {
|
||||
rotationOverLifeEnabled: false,
|
||||
rotationOverLifeMode: 'random',
|
||||
rotationOverLifeCurve: [{ x: 0, y: 0 }, { x: 1, y: 1 }],
|
||||
gravity: 0,
|
||||
gravity: 980,
|
||||
gravityMode: 'fixed',
|
||||
gravityMin: 980,
|
||||
gravityMax: 980,
|
||||
gravityDirection: -90,
|
||||
gravityCurve: [{ x: 0, y: 1 }, { x: 1, y: 1 }],
|
||||
damping: 0,
|
||||
seed: 0,
|
||||
colorStart: '#ffffff',
|
||||
@@ -288,14 +327,32 @@ export function defaultConfig(): EmitterConfig {
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
noise: false,
|
||||
noiseAmt: 0,
|
||||
useGravity: true,
|
||||
noiseAmt: 30,
|
||||
noiseRange: 100,
|
||||
noiseSpeed: 1,
|
||||
useGravity: false,
|
||||
wind: false,
|
||||
windX: 0,
|
||||
windY: 0,
|
||||
windMode: 'fixed',
|
||||
windMin: 100,
|
||||
windMax: 100,
|
||||
windDirection: 0,
|
||||
windCurve: [{ x: 0, y: 1 }, { x: 1, y: 1 }],
|
||||
forceField: false,
|
||||
forceStrength: 0,
|
||||
forceStrength: 10,
|
||||
forceRadius: 100,
|
||||
forceCenterX: 0,
|
||||
forceCenterY: 0,
|
||||
forceFalloff: 'linear',
|
||||
attraction: false,
|
||||
attractionCenterX: 200,
|
||||
attractionCenterY: 0,
|
||||
attractionRadius: 20,
|
||||
attractionDuration: 0.5,
|
||||
attractionDelay: 0.1,
|
||||
attractionDelayMode: 'particle',
|
||||
attractionPath: 'curve',
|
||||
trail: false,
|
||||
collision: false,
|
||||
pathFollow: false,
|
||||
@@ -307,6 +364,13 @@ export function ensureEmitterConfig(config: EmitterConfig): EmitterConfig {
|
||||
const defaults = defaultConfig() as unknown as Record<string, unknown>
|
||||
const target = config as unknown as Record<string, unknown>
|
||||
const needsImageWeightMigration = target.imageWeightVersion !== 1
|
||||
const needsGravityMigration = target.gravityMode == null
|
||||
const legacyGravity = Number(target.gravity)
|
||||
const needsWindMigration = target.windMode == null
|
||||
const legacyWindX = Number(target.windX)
|
||||
const legacyWindY = Number(target.windY)
|
||||
const needsForceFieldMigration = target.forceFalloff == null
|
||||
const legacyForceStrength = Number(target.forceStrength)
|
||||
for (const [key, value] of Object.entries(defaults)) {
|
||||
if (target[key] !== undefined && target[key] !== null) continue
|
||||
target[key] = Array.isArray(value)
|
||||
@@ -357,6 +421,21 @@ export function ensureEmitterConfig(config: EmitterConfig): EmitterConfig {
|
||||
})
|
||||
}
|
||||
config.imageWeightVersion = 1
|
||||
if (needsGravityMigration) {
|
||||
const migratedStrength = Number.isFinite(legacyGravity) && legacyGravity !== 0 ? legacyGravity : 980
|
||||
config.gravity = migratedStrength
|
||||
config.gravityMin = migratedStrength
|
||||
config.gravityMax = migratedStrength
|
||||
}
|
||||
if (needsWindMigration) {
|
||||
const hasLegacyWind = Number.isFinite(legacyWindX) && Number.isFinite(legacyWindY) && (legacyWindX !== 0 || legacyWindY !== 0)
|
||||
config.windMin = hasLegacyWind ? Math.hypot(legacyWindX, legacyWindY) : 100
|
||||
config.windMax = config.windMin
|
||||
config.windDirection = hasLegacyWind ? Math.atan2(-legacyWindY, legacyWindX) * 180 / Math.PI : 0
|
||||
}
|
||||
if (needsForceFieldMigration) {
|
||||
config.forceStrength = Number.isFinite(legacyForceStrength) && legacyForceStrength !== 0 ? legacyForceStrength : 10
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
@@ -374,6 +453,14 @@ interface Particle {
|
||||
speedScale: number
|
||||
speedScaleTarget: number
|
||||
scaleOverLifeX: number; scaleOverLifeY: number
|
||||
noisePhaseX: number; noisePhaseY: number
|
||||
gravityStrength: number
|
||||
windStrength: number
|
||||
attractionActive: boolean
|
||||
attractionElapsed: number
|
||||
attractionStartWorldX: number
|
||||
attractionStartWorldY: number
|
||||
attractionCurveSide: number
|
||||
resourceId: number
|
||||
}
|
||||
|
||||
@@ -388,6 +475,9 @@ export class ParticleEmitter extends Container {
|
||||
private burstTimer = 0
|
||||
/** 发射器累计运行时间(秒) — 非响应式,避免污染 config */
|
||||
private _elapsed = 0
|
||||
/** 用于“统一时间”延迟:记录吸附从关闭切换为开启时的发射器时间。 */
|
||||
private _attractionEnabledAt = 0
|
||||
private _attractionWasEnabled = false
|
||||
private _emitterPos: (() => IPointData) | null = null // root 世界坐标(0,0)
|
||||
/** 活动粒子状态快照(供骨架 / 导出) */
|
||||
states: ParticleState[] = []
|
||||
@@ -406,25 +496,32 @@ export class ParticleEmitter extends Container {
|
||||
const s = this.cfg.seed
|
||||
if (s === this._lastSeed) return
|
||||
this._lastSeed = s
|
||||
if (s > 0) {
|
||||
this.rng = mulberry32(s >>> 0)
|
||||
} else {
|
||||
this.rng = Math.random
|
||||
}
|
||||
this.rng = mulberry32(s >>> 0)
|
||||
}
|
||||
|
||||
private _ensurePool(n: number) {
|
||||
while (this.pool.length < n) {
|
||||
const poolIndex = this.pool.length
|
||||
const sprite = new Sprite(this.cfg.texture)
|
||||
sprite.blendMode = BLEND_MAP[this.cfg.blend] as any
|
||||
sprite.anchor.set(0.5)
|
||||
sprite.visible = false
|
||||
this.addChild(sprite)
|
||||
this.pool.push({
|
||||
sprite, active: false, boneName: 'p_' + this.pool.length,
|
||||
sprite, active: false, boneName: 'p_' + poolIndex,
|
||||
life: 0, maxLife: 1, x: 0, y: 0, vx: 0, vy: 0,
|
||||
scaleStartX: 1, scaleStartY: 1, alphaStart: 1, alphaEnd: 1, rotation: 0, rotationSpeed: 0,
|
||||
speedScale: 1, speedScaleTarget: 1, scaleOverLifeX: 1, scaleOverLifeY: 1, resourceId: 1,
|
||||
speedScale: 1, speedScaleTarget: 1, scaleOverLifeX: 1, scaleOverLifeY: 1,
|
||||
noisePhaseX: 0,
|
||||
noisePhaseY: 0,
|
||||
gravityStrength: 980,
|
||||
windStrength: 100,
|
||||
attractionActive: false,
|
||||
attractionElapsed: 0,
|
||||
attractionStartWorldX: 0,
|
||||
attractionStartWorldY: 0,
|
||||
attractionCurveSide: 1,
|
||||
resourceId: 1,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -439,11 +536,21 @@ export class ParticleEmitter extends Container {
|
||||
cfg.lifeMode == null || cfg.speedMode == null || cfg.scaleMode == null ||
|
||||
cfg.initialRotationMode == null || cfg.speedOverLifeEnabled == null ||
|
||||
cfg.scaleOverLifeEnabled == null || cfg.rotationOverLifeEnabled == null ||
|
||||
!Array.isArray(cfg.scaleOverLifeCurve)
|
||||
!Array.isArray(cfg.scaleOverLifeCurve) || cfg.attraction == null
|
||||
) ensureEmitterConfig(cfg)
|
||||
this.reseed() // 种子变化即重建随机源
|
||||
// 累计发射时间
|
||||
this._elapsed += dt
|
||||
if (cfg.attraction && !this._attractionWasEnabled) {
|
||||
this._attractionEnabledAt = this._elapsed
|
||||
for (const particle of this.pool) {
|
||||
particle.attractionActive = false
|
||||
particle.attractionElapsed = 0
|
||||
}
|
||||
} else if (!cfg.attraction && this._attractionWasEnabled) {
|
||||
for (const particle of this.pool) particle.attractionActive = false
|
||||
}
|
||||
this._attractionWasEnabled = cfg.attraction
|
||||
const inWindow = cfg.delay <= 0 || this._elapsed >= cfg.delay
|
||||
const withinDuration = cfg.duration <= 0 || this._elapsed < cfg.delay + cfg.duration
|
||||
// 发射(仅在窗口内且未超时长)
|
||||
@@ -461,24 +568,53 @@ export class ParticleEmitter extends Container {
|
||||
}
|
||||
}
|
||||
|
||||
const base = this._emitterPos ? this._emitterPos() : { x: this.cfg.offsetX, y: this.cfg.offsetY }
|
||||
const states: ParticleState[] = []
|
||||
for (let i = 0; i < this.pool.length; i++) {
|
||||
const p = this.pool[i]
|
||||
if (!p.active) continue
|
||||
p.life -= dt
|
||||
// 粒子自身生命周期优先:即使正在吸附途中,生命耗尽也立即死亡,不因吸附时长延寿。
|
||||
if (p.life <= 0) { this.kill(i); continue }
|
||||
if (cfg.useGravity) p.vy += cfg.gravity * dt
|
||||
const lifeT = tFromLife(p)
|
||||
if (cfg.useGravity) {
|
||||
const gravityMultiplier = Math.min(1, Math.max(0, curveAt(cfg.gravityCurve, lifeT)))
|
||||
const gravityForce = (cfg.gravityMode === 'random' ? p.gravityStrength : cfg.gravityMin) * gravityMultiplier
|
||||
const gravityAngle = cfg.gravityDirection * Math.PI / 180
|
||||
p.vx += Math.cos(gravityAngle) * gravityForce * dt
|
||||
p.vy += -Math.sin(gravityAngle) * gravityForce * dt
|
||||
}
|
||||
// 风力
|
||||
if (cfg.wind) { p.vx += cfg.windX * dt; p.vy += cfg.windY * dt }
|
||||
// 力场(吸力/斥力,作用在发射点附近)
|
||||
if (cfg.forceField && cfg.forceStrength !== 0) {
|
||||
const dx = base.x - p.x, dy = base.y - p.y
|
||||
const dist = Math.hypot(dx, dy) || 0.0001
|
||||
if (dist < cfg.forceRadius) {
|
||||
const f = cfg.forceStrength * (1 - dist / cfg.forceRadius)
|
||||
p.vx += (dx / dist) * f * dt * 10
|
||||
p.vy += (dy / dist) * f * dt * 10
|
||||
if (cfg.wind) {
|
||||
const windMultiplier = Math.min(1, Math.max(0, curveAt(cfg.windCurve, lifeT)))
|
||||
const windForce = (cfg.windMode === 'random' ? p.windStrength : cfg.windMin) * windMultiplier
|
||||
const windAngle = cfg.windDirection * Math.PI / 180
|
||||
p.vx += Math.cos(windAngle) * windForce * dt
|
||||
p.vy += -Math.sin(windAngle) * windForce * dt
|
||||
}
|
||||
// 全局力场:先把粒子局部坐标转换到场景数学坐标,计算力后再转回发射器局部坐标。
|
||||
if (cfg.forceField && cfg.forceStrength !== 0 && cfg.forceRadius > 0) {
|
||||
const rootScaleX = Math.max(0.0001, Math.abs(cfg.rootScaleX))
|
||||
const rootScaleY = Math.max(0.0001, Math.abs(cfg.rootScaleY))
|
||||
const rootAngle = -cfg.rootRotation * Math.PI / 180
|
||||
const cosRoot = Math.cos(rootAngle), sinRoot = Math.sin(rootAngle)
|
||||
const particleScreenX = cfg.centerX + cosRoot * rootScaleX * p.x - sinRoot * rootScaleY * p.y
|
||||
const particleScreenY = -cfg.centerY + sinRoot * rootScaleX * p.x + cosRoot * rootScaleY * p.y
|
||||
const particleWorldX = particleScreenX
|
||||
const particleWorldY = -particleScreenY
|
||||
const dx = cfg.forceCenterX - particleWorldX
|
||||
const dy = cfg.forceCenterY - particleWorldY
|
||||
const distance = Math.hypot(dx, dy)
|
||||
if (distance > 0.0001 && distance <= cfg.forceRadius) {
|
||||
let falloff = 1
|
||||
if (cfg.forceFalloff === 'linear') falloff = 1 - distance / cfg.forceRadius
|
||||
else if (cfg.forceFalloff === 'inverse') falloff = Math.min(10, cfg.forceRadius / Math.max(distance, cfg.forceRadius * 0.1))
|
||||
const force = cfg.forceStrength * falloff * 10
|
||||
const accelWorldX = dx / distance * force
|
||||
const accelWorldY = dy / distance * force
|
||||
const accelScreenX = accelWorldX
|
||||
const accelScreenY = -accelWorldY
|
||||
p.vx += (cosRoot * accelScreenX + sinRoot * accelScreenY) / rootScaleX * dt
|
||||
p.vy += (-sinRoot * accelScreenX + cosRoot * accelScreenY) / rootScaleY * dt
|
||||
}
|
||||
}
|
||||
// 阻尼:速度按衰减系数逐渐减小
|
||||
@@ -488,7 +624,60 @@ export class ParticleEmitter extends Container {
|
||||
}
|
||||
p.x += p.vx * dt
|
||||
p.y += p.vy * dt
|
||||
const t = 1 - p.life / p.maxLife
|
||||
|
||||
// 吸附使用场景全局数学坐标。延迟结束时锁定当前位置,随后沿直线或确定性弧线到达目标。
|
||||
if (cfg.attraction) {
|
||||
const age = p.maxLife - p.life
|
||||
const delayReached = cfg.attractionDelayMode === 'particle'
|
||||
? age >= Math.max(0, cfg.attractionDelay)
|
||||
: this._elapsed - this._attractionEnabledAt >= Math.max(0, cfg.attractionDelay)
|
||||
if (delayReached) {
|
||||
const currentWorld = localToWorldMath(cfg, p.x, p.y)
|
||||
const targetX = cfg.attractionCenterX
|
||||
const targetY = cfg.attractionCenterY
|
||||
const radius = Math.max(0, cfg.attractionRadius)
|
||||
if (!p.attractionActive) {
|
||||
if (Math.hypot(targetX - currentWorld.x, targetY - currentWorld.y) <= radius) {
|
||||
this.kill(i)
|
||||
continue
|
||||
}
|
||||
p.attractionActive = true
|
||||
p.attractionElapsed = 0
|
||||
p.attractionStartWorldX = currentWorld.x
|
||||
p.attractionStartWorldY = currentWorld.y
|
||||
}
|
||||
p.attractionElapsed += dt
|
||||
const duration = Math.max(0.0001, cfg.attractionDuration)
|
||||
const progress = Math.min(1, p.attractionElapsed / duration)
|
||||
let worldX = lerp(p.attractionStartWorldX, targetX, progress)
|
||||
let worldY = lerp(p.attractionStartWorldY, targetY, progress)
|
||||
if (cfg.attractionPath === 'curve') {
|
||||
const dx = targetX - p.attractionStartWorldX
|
||||
const dy = targetY - p.attractionStartWorldY
|
||||
const distance = Math.hypot(dx, dy)
|
||||
if (distance > 0.0001) {
|
||||
const curveOffset = distance * 0.25 * p.attractionCurveSide
|
||||
const controlX = (p.attractionStartWorldX + targetX) * 0.5 - dy / distance * curveOffset
|
||||
const controlY = (p.attractionStartWorldY + targetY) * 0.5 + dx / distance * curveOffset
|
||||
const oneMinusT = 1 - progress
|
||||
worldX = oneMinusT * oneMinusT * p.attractionStartWorldX + 2 * oneMinusT * progress * controlX + progress * progress * targetX
|
||||
worldY = oneMinusT * oneMinusT * p.attractionStartWorldY + 2 * oneMinusT * progress * controlY + progress * progress * targetY
|
||||
}
|
||||
}
|
||||
const remainingDistance = Math.hypot(targetX - worldX, targetY - worldY)
|
||||
if (progress >= 1 || remainingDistance <= radius) {
|
||||
this.kill(i)
|
||||
continue
|
||||
}
|
||||
const local = worldMathToLocal(cfg, worldX, worldY)
|
||||
p.x = local.x
|
||||
p.y = local.y
|
||||
// 吸附期间位置由路径控制,清除速度以免抵达后发生额外积分偏移。
|
||||
p.vx = 0
|
||||
p.vy = 0
|
||||
}
|
||||
}
|
||||
const t = lifeT
|
||||
const s = p.sprite
|
||||
const resource = cfg.imageResources.find((item) => item.id === p.resourceId)
|
||||
|
||||
@@ -503,7 +692,16 @@ export class ParticleEmitter extends Container {
|
||||
rotationSpeed = lerp(cfg.rotationSpeedMin, cfg.rotationSpeedMax, curveAt(cfg.rotationOverLifeCurve, t))
|
||||
}
|
||||
p.rotation += rotationSpeed * dt
|
||||
s.x = p.x; s.y = p.y; s.rotation = -p.rotation * Math.PI / 180
|
||||
let renderX = p.x, renderY = p.y
|
||||
if (cfg.noise && cfg.noiseAmt > 0 && cfg.noiseRange > 0 && cfg.noiseSpeed > 0) {
|
||||
const age = p.maxLife - p.life
|
||||
const amplitude = Math.max(0, cfg.noiseRange) * Math.min(1, Math.max(0, cfg.noiseAmt) / 100) * 0.5
|
||||
const phase = age * Math.max(0, cfg.noiseSpeed) * Math.PI * 2
|
||||
// 减去出生时的初始采样,确保粒子仍从发射点开始;差值最大不超过设定波动范围。
|
||||
renderX += (Math.sin(p.noisePhaseX + phase) - Math.sin(p.noisePhaseX)) * amplitude
|
||||
renderY += (Math.sin(p.noisePhaseY + phase * 0.83) - Math.sin(p.noisePhaseY)) * amplitude
|
||||
}
|
||||
s.x = renderX; s.y = renderY; s.rotation = -p.rotation * Math.PI / 180
|
||||
let scaleMulX = 1, scaleMulY = 1
|
||||
if (cfg.scaleOverLifeEnabled) {
|
||||
scaleMulX = overLifeValue(cfg.scaleOverLifeMode, cfg.scaleOverLifeMin, cfg.scaleOverLifeMax, cfg.scaleOverLifeCurve, t, p.scaleOverLifeX)
|
||||
@@ -527,7 +725,7 @@ export class ParticleEmitter extends Container {
|
||||
: hexToNumber(colorStart)
|
||||
s.tint = rgb
|
||||
s.blendMode = BLEND_MAP[resource?.blend || 'normal'] as any
|
||||
states.push({ boneName: p.boneName, x: p.x, y: p.y, rotation: p.rotation, scaleX: s.scale.x, scaleY: s.scale.y, alpha: s.alpha, colorHex: rgb, resourceId: p.resourceId, active: true })
|
||||
states.push({ boneName: p.boneName, x: renderX, y: renderY, rotation: p.rotation, scaleX: s.scale.x, scaleY: s.scale.y, alpha: s.alpha, colorHex: rgb, resourceId: p.resourceId, active: true })
|
||||
}
|
||||
this.states = states
|
||||
return states
|
||||
@@ -572,6 +770,18 @@ export class ParticleEmitter extends Container {
|
||||
}
|
||||
const p = this.pool[idx]
|
||||
const resource = this.pickImageResource()
|
||||
// 噪声相位由全局随机种子和粒子池编号确定,同一配置重复录制得到相同轨迹。
|
||||
p.noisePhaseX = seededUnit(cfg.seed, idx, 0x68bc21eb) * Math.PI * 2
|
||||
p.noisePhaseY = seededUnit(cfg.seed, idx, 0x02e5be93) * Math.PI * 2
|
||||
p.gravityStrength = cfg.useGravity && cfg.gravityMode === 'random'
|
||||
? lerp(cfg.gravityMin, cfg.gravityMax, this.rng())
|
||||
: cfg.gravityMin
|
||||
p.windStrength = cfg.wind && cfg.windMode === 'random'
|
||||
? lerp(cfg.windMin, cfg.windMax, this.rng())
|
||||
: cfg.windMin
|
||||
p.attractionActive = false
|
||||
p.attractionElapsed = 0
|
||||
p.attractionCurveSide = seededUnit(cfg.seed, idx, 0xa341316c) < 0.5 ? -1 : 1
|
||||
p.active = true; p.life = life; p.maxLife = life
|
||||
p.x = ox; p.y = oy
|
||||
p.vx = Math.cos(dirAng) * speed
|
||||
@@ -686,13 +896,49 @@ export class ParticleEmitter extends Container {
|
||||
this.accumulator = 0
|
||||
this.burstTimer = 0
|
||||
this._elapsed = 0
|
||||
// 从头重新模拟:若种子>0,重设随机源到序列头,保证同一种子每次从头录制结果一致
|
||||
if (this.cfg.seed > 0) { this._lastSeed = this.cfg.seed; this.rng = mulberry32(this.cfg.seed >>> 0) }
|
||||
this._attractionEnabledAt = 0
|
||||
this._attractionWasEnabled = false
|
||||
// 从头重新模拟:任何整数种子(包括 0)都会回到同一随机序列。
|
||||
this._lastSeed = this.cfg.seed
|
||||
this.rng = mulberry32(this.cfg.seed >>> 0)
|
||||
}
|
||||
}
|
||||
|
||||
function lerp(a: number, b: number, t: number) { return a + (b - a) * t }
|
||||
function finite(value: number, fallback: number) { return Number.isFinite(value) ? value : fallback }
|
||||
function localToWorldMath(cfg: EmitterConfig, localX: number, localY: number) {
|
||||
const scaleX = Math.max(0.0001, Math.abs(cfg.rootScaleX))
|
||||
const scaleY = Math.max(0.0001, Math.abs(cfg.rootScaleY))
|
||||
const angle = -cfg.rootRotation * Math.PI / 180
|
||||
const cos = Math.cos(angle), sin = Math.sin(angle)
|
||||
const screenX = cfg.centerX + cos * scaleX * localX - sin * scaleY * localY
|
||||
const screenY = -cfg.centerY + sin * scaleX * localX + cos * scaleY * localY
|
||||
return { x: screenX, y: -screenY }
|
||||
}
|
||||
function worldMathToLocal(cfg: EmitterConfig, worldX: number, worldY: number) {
|
||||
const scaleX = Math.max(0.0001, Math.abs(cfg.rootScaleX))
|
||||
const scaleY = Math.max(0.0001, Math.abs(cfg.rootScaleY))
|
||||
const angle = -cfg.rootRotation * Math.PI / 180
|
||||
const cos = Math.cos(angle), sin = Math.sin(angle)
|
||||
const screenX = worldX - cfg.centerX
|
||||
const screenY = -worldY + cfg.centerY
|
||||
return {
|
||||
x: (cos * screenX + sin * screenY) / scaleX,
|
||||
y: (-sin * screenX + cos * screenY) / scaleY,
|
||||
}
|
||||
}
|
||||
function tFromLife(particle: Pick<Particle, 'life' | 'maxLife'>) {
|
||||
return Math.min(1, Math.max(0, 1 - particle.life / Math.max(0.0001, particle.maxLife)))
|
||||
}
|
||||
function seededUnit(seed: number, index: number, salt: number) {
|
||||
let value = ((seed >>> 0) ^ Math.imul(index + 1, 0x9e3779b1) ^ salt) >>> 0
|
||||
value ^= value >>> 16
|
||||
value = Math.imul(value, 0x7feb352d)
|
||||
value ^= value >>> 15
|
||||
value = Math.imul(value, 0x846ca68b)
|
||||
value ^= value >>> 16
|
||||
return (value >>> 0) / 4294967296
|
||||
}
|
||||
function initialValue(mode: AttributeMode, min: number, max: number, curve: CurvePoint[], random: number) {
|
||||
if (mode === 'fixed') return min
|
||||
if (mode === 'curve') return lerp(min, max, curveAt(curve, random))
|
||||
|
||||
Reference in new issue
Block a user