|
/* |
|
============================== |
|
仿sekai转场特效 |
|
@author: RibomBalt |
|
============================== |
|
*/ |
|
|
|
import * as PIXI from 'pixi.js'; |
|
import { registerPerform } from '@/Core/util/pixiPerformManager/pixiPerformManager'; |
|
import { WebGAL } from '@/Core/WebGAL'; |
|
import { SCREEN_CONSTANTS } from '@/Core/util/constants'; |
|
import { sum } from 'lodash'; |
|
|
|
// 生成高斯分布随机数 |
|
function randn(mean=0, stdev=1) { |
|
const u = 1 - Math.random(); // Converting [0,1) to (0,1] |
|
const v = Math.random(); |
|
const z = Math.sqrt( -2.0 * Math.log( u ) ) * Math.cos( 2.0 * Math.PI * v ); |
|
// Transform to the desired mean and standard deviation: |
|
return z * stdev + mean; |
|
} |
|
|
|
// 从Array中随机选择一个元素 |
|
function choose<T>(arr: T[], weight: number[]): T { |
|
const weight_sum = sum(weight); |
|
const cum_weight = weight.map((w, i) => sum(weight.slice(0, i + 1))); |
|
const rand = Math.random() * weight_sum; |
|
for (let i = 0; i < arr.length; i++) { |
|
if (rand < cum_weight[i]) { |
|
return arr[i]; |
|
} |
|
} |
|
throw new Error(`choose failed: ${rand} not in [0, ${weight_sum})`); |
|
} |
|
|
|
// 实现HSL到RGB的转换(返回[0-1, 0-1, 0-1]范围的RGB数组) |
|
const hslToRgb = (h: number, s: number, l: number): [number, number, number] => { |
|
// 参数约束 |
|
h = ((h % 360) + 360) % 360; // 将色相限制在0-360范围 |
|
s = Math.min(1, Math.max(0, s)); |
|
l = Math.min(1, Math.max(0, l)); |
|
|
|
const c = (1 - Math.abs(2 * l - 1)) * s; |
|
const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); |
|
const m = l - c / 2; |
|
|
|
let r: number, g: number, b: number; |
|
|
|
if (0 <= h && h < 60) { |
|
[r, g, b] = [c, x, 0]; |
|
} else if (60 <= h && h < 120) { |
|
[r, g, b] = [x, c, 0]; |
|
} else if (120 <= h && h < 180) { |
|
[r, g, b] = [0, c, x]; |
|
} else if (180 <= h && h < 240) { |
|
[r, g, b] = [0, x, c]; |
|
} else if (240 <= h && h < 300) { |
|
[r, g, b] = [x, 0, c]; |
|
} else { |
|
[r, g, b] = [c, 0, x]; |
|
} |
|
|
|
return [ |
|
Math.round((r + m) * 100) / 100, // 保留两位小数避免精度问题 |
|
Math.round((g + m) * 100) / 100, |
|
Math.round((b + m) * 100) / 100 |
|
]; |
|
}; |
|
|
|
interface Color { |
|
h: number; |
|
s: number; |
|
l: number; |
|
} |
|
|
|
interface TransitionConfig { |
|
speed: number; |
|
spawnPosition: { x: number; y: number }; |
|
flipAngleRange: { min: number; max: number }; |
|
colorRange: Array<Color>; |
|
weightRange: number[]; |
|
alphaRange: { min: number; max: number }; |
|
triangleSize: number; |
|
triangleCount: number; |
|
} |
|
|
|
interface Triangle { |
|
graphics: PIXI.Graphics; |
|
velocity: { x: number; y: number }; |
|
rotationSpeed: number; |
|
currentAngle: number; |
|
damping: number; |
|
randomForce: number; |
|
life: number; |
|
} |
|
|
|
const motionAdvance = (triangle: Triangle, delta: number) => { |
|
// https://pixijs.huashengweilai.com/guide/start/10.moving-sprites.html#%E6%B8%B8%E6%88%8F%E5%BE%AA%E7%8E%AF-game-loop |
|
// delta almost is one per frame, usually called 60 fps |
|
// 阻尼 |
|
triangle.velocity.x -= triangle.damping * (triangle.life / 30) * triangle.velocity.x * delta; |
|
triangle.velocity.y -= triangle.damping * (triangle.life / 30) * triangle.velocity.x * delta; |
|
|
|
// 随机力 |
|
triangle.velocity.x += (Math.random() - 0.5) * triangle.randomForce; |
|
triangle.velocity.y += (Math.random() - 0.5) * triangle.randomForce; |
|
|
|
// 更新位置 |
|
triangle.graphics.x += triangle.velocity.x * delta; |
|
triangle.graphics.y += triangle.velocity.y * delta; |
|
|
|
// 更新3D旋转效果 |
|
triangle.currentAngle += triangle.rotationSpeed * delta; |
|
const scaleX = Math.cos(triangle.currentAngle); |
|
const skewY = Math.tan(-triangle.currentAngle * 0.5); |
|
|
|
triangle.graphics.scale.set(scaleX, 1); |
|
triangle.graphics.skew.set(0, skewY); |
|
|
|
// 更新自转 |
|
triangle.graphics.rotation += delta * 0.02; |
|
|
|
// update life |
|
if (triangle.life > 90) { |
|
triangle.graphics.alpha = Math.max(0, triangle.graphics.alpha - delta * 0.01 * Math.random()); |
|
} |
|
|
|
triangle.life += delta; |
|
} |
|
|
|
const pixiTransition = (config: TransitionConfig) => { |
|
const effectsContainer = WebGAL.gameplay.pixiStage!.effectsContainer!; |
|
const app = WebGAL.gameplay.pixiStage!.currentApp!; |
|
const container = new PIXI.Container(); |
|
effectsContainer.addChild(container); |
|
|
|
// 创建背景渐变 |
|
const background = new PIXI.Graphics(); |
|
background.beginFill(0xffffff, 1.0); |
|
background.drawRect(0, 0, SCREEN_CONSTANTS.width, SCREEN_CONSTANTS.height); |
|
background.endFill(); |
|
container.addChild(background); |
|
|
|
// 动画状态 |
|
let backgroundAlpha = 0; |
|
let phase: 'fadeIn' | 'fadeOut' = 'fadeIn'; |
|
const triangles: Triangle[] = []; |
|
|
|
// 创建三角形池 |
|
const createTriangle = () => { |
|
const graphics = new PIXI.Graphics(); |
|
const size = config.triangleSize * (0.4 + Math.random() * 0.8); |
|
|
|
// 设置颜色 |
|
const color = choose(config.colorRange, config.weightRange); |
|
// const hue = config.colorRange.hueMin + Math.random() * (config.colorRange.hueMax - config.colorRange.hueMin); |
|
// const [r, g, b] = hslToRgb(hue, config.colorRange.saturation, config.colorRange.lightness); |
|
const [r, g, b] = hslToRgb(color.h, color.s, color.l); |
|
graphics.tint = PIXI.utils.rgb2hex([r, g, b]); |
|
|
|
// 设置透明度 |
|
graphics.alpha = config.alphaRange.min + Math.random() * (config.alphaRange.max - config.alphaRange.min); |
|
|
|
// 初始化旋转速度 |
|
let rotationSpeed = (Math.random() - 0.5) * 0.02; |
|
let currentAngle = config.flipAngleRange.min + Math.random() * (config.flipAngleRange.max - config.flipAngleRange.min) |
|
|
|
// 随机形状 |
|
const shape_rand = Math.random(); |
|
if (shape_rand < 0.5) { |
|
// 绘制等边三角形 |
|
graphics.beginFill(0xffffff); |
|
graphics.moveTo(0, size * (Math.sqrt(3) - 1)); |
|
graphics.lineTo(size / 2, size * (Math.sqrt(3) - 2)); |
|
graphics.lineTo(-size / 2, size * (Math.sqrt(3) - 2)); |
|
graphics.closePath(); |
|
graphics.endFill(); |
|
|
|
} else if (shape_rand < 0.8) { |
|
// 绘制空心三角形 |
|
graphics.beginFill(0xffffff); |
|
graphics.moveTo(0, size * (Math.sqrt(3) - 1)); |
|
graphics.lineTo(size / 2, size * (Math.sqrt(3) - 2)); |
|
graphics.lineTo(-size / 2, size * (Math.sqrt(3) - 2)); |
|
graphics.closePath(); |
|
graphics.endFill(); |
|
|
|
// create a hole in between |
|
const hole_size = size * 0.9; |
|
graphics.beginHole(); |
|
graphics.moveTo(0, hole_size * (Math.sqrt(3) - 1)); |
|
graphics.lineTo(hole_size / 2, hole_size * (Math.sqrt(3) - 2)); |
|
graphics.lineTo(-hole_size / 2, hole_size * (Math.sqrt(3) - 2)); |
|
graphics.closePath(); |
|
graphics.endHole(); |
|
|
|
} else { |
|
// 绘制圆形 |
|
graphics.beginFill(0xffffff); |
|
graphics.drawCircle(0, 0, size * 0.2); |
|
graphics.endFill(); |
|
|
|
graphics.beginHole(); |
|
graphics.drawCircle(0, 0, size * 0.2 * 0.9); |
|
graphics.endHole(); |
|
|
|
// only draw white circle |
|
graphics.tint = 0xffffff; |
|
// for circle, no rotation needed |
|
rotationSpeed = 0; |
|
currentAngle = 0; |
|
|
|
} |
|
|
|
// 设置初始位置速度 |
|
const spawn_x = SCREEN_CONSTANTS.width * (1 + Math.random() * 0.1); |
|
const spawn_y = SCREEN_CONSTANTS.height * randn(0.3, 0.3); |
|
|
|
graphics.position.set(spawn_x, spawn_y); |
|
graphics.pivot.set(0.5, 0.5); |
|
graphics.rotation = Math.random() * Math.PI * 2; |
|
|
|
// 设置运动参数 |
|
const angle = randn(Math.PI, Math.PI / 6); |
|
const speed = config.speed * (0.4 + Math.random() * 0.8); |
|
const velocity = { |
|
x: Math.cos(angle) * speed, |
|
y: Math.sin(angle) * speed |
|
}; |
|
const damping = Math.random() * 0.2 / 60; |
|
const randomForce = Math.random() * 0.2; |
|
|
|
return { |
|
graphics, |
|
velocity, |
|
rotationSpeed, |
|
currentAngle, |
|
damping, |
|
randomForce, |
|
life: 0, |
|
}; |
|
}; |
|
|
|
// record time, assume single threaded |
|
let total_time = 0; |
|
// 动画更新函数 |
|
const tickerFn = (delta: number) => { |
|
// 更新背景渐变 |
|
if (phase === 'fadeIn') { |
|
backgroundAlpha = Math.min(1, backgroundAlpha + delta * 0.01); |
|
} else { |
|
backgroundAlpha = Math.max(0, backgroundAlpha - delta * 0.01); |
|
} |
|
background.alpha = backgroundAlpha; |
|
|
|
// white background fade out after 4 seconds |
|
if (total_time >= 60 * 4) { |
|
phase = 'fadeOut'; |
|
} |
|
|
|
// 如果时间不超过3秒,可以以每帧80%的概率生成新三角形 |
|
if (total_time < 60 * 5 && Math.random() < 0.8) { |
|
const triangle = createTriangle(); |
|
container.addChild(triangle.graphics); |
|
triangles.push(triangle); |
|
} |
|
// 更新三角形状态 |
|
triangles.forEach(triangle => { |
|
motionAdvance(triangle, delta); |
|
}); |
|
// remove all triangles with alpha = 0 |
|
triangles.forEach((triangle, index) => { |
|
if (triangle.graphics.alpha <= 0) { |
|
container.removeChild(triangle.graphics); |
|
triangles.splice(index, 1); |
|
} |
|
}); |
|
|
|
// update time ticker |
|
total_time += delta; |
|
}; |
|
|
|
WebGAL.gameplay.pixiStage?.registerAnimation( |
|
{ setStartState: () => { }, setEndState: () => { }, tickerFunc: tickerFn }, |
|
'transition-Ticker' |
|
); |
|
|
|
return { container, tickerKey: 'transition-Ticker' }; |
|
}; |
|
|
|
// 注册转场特效 |
|
registerPerform('sekai', () => pixiTransition({ |
|
speed: 30, |
|
spawnPosition: { x: SCREEN_CONSTANTS.width / 2, y: SCREEN_CONSTANTS.height / 2 }, |
|
flipAngleRange: { min: -Math.PI / 3, max: Math.PI / 3 }, |
|
colorRange: [ |
|
{ h: 310, s: 0.8, l: 0.8 }, |
|
{ h: 180, s: 0.8, l: 0.8 }, |
|
{ h: 60, s: 1.0, l: 0.5 }, |
|
{ h: 0, s: 0.0, l: 1.0 }, |
|
], |
|
weightRange: [2, 2, 2, 1], |
|
alphaRange: { min: 0.7, max: 0.9 }, |
|
triangleSize: 200, |
|
triangleCount: 50 |
|
})); |