mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-14 08:18:44 +00:00
feat(gacha): refine beta wish animation and SSS reveal
This commit is contained in:
1 parent
11f458fee5
commit
adea684a2f
5 files changed
+152
-41
No files matched your search
@@ -1,4 +1,5 @@
|
||||
import {useMemo} from 'react';
|
||||
import {Star} from '@remotion/shapes';
|
||||
import {AbsoluteFill, Easing, interpolate, useCurrentFrame, useVideoConfig} from 'remotion';
|
||||
import {
|
||||
gachaBetaTimeline,
|
||||
@@ -18,9 +19,9 @@ const baseFont =
|
||||
|
||||
const clamp = /** @type {const} */ ({extrapolateLeft: 'clamp', extrapolateRight: 'clamp'});
|
||||
|
||||
/** Toon shading: flat colour bands with hard edges and no outline. */
|
||||
const skyBands =
|
||||
'linear-gradient(180deg, #0a0c2c 0% 28%, #15185a 28% 52%, #262a80 52% 72%, #3a44a6 72% 88%, #5664c4 88%)';
|
||||
/** A luminous night sky behind flat, outline-free cloud shading. */
|
||||
const skyBackground =
|
||||
'radial-gradient(ellipse at 50% 105%, #7886ce 0%, #353f83 32%, #171b49 65%, #090e29 100%)';
|
||||
const cloudShade = 'radial-gradient(circle at 38% 32%, #f6f4ff 0% 44%, #cdd1f6 44% 70%, #959cdb 70%)';
|
||||
const heroShade = 'radial-gradient(circle at 38% 30%, #ffffff 0% 46%, #d8dcfb 46% 72%, #a0a8e4 72%)';
|
||||
|
||||
@@ -91,11 +92,26 @@ const withAlpha = (hex, alpha) => {
|
||||
* @param {{size: number, fill: string, shade: string}} props
|
||||
*/
|
||||
const ToonStar = ({size, fill, shade}) => (
|
||||
<svg height={size} style={{display: 'block'}} viewBox="0 0 24 24" width={size}>
|
||||
<path d="M12 0.8l3.2 7.9 8.3.6-6.4 5.3 2.1 8.2L12 18.3l-7.2 4.5 2.1-8.2L0.5 9.3l8.3-.6z" fill={shade} />
|
||||
<path d="M12 0.8l3.2 7.9 8.3.6-6.4 5.3L12 14.2z" fill={fill} />
|
||||
<path d="M12 0.8L8.8 8.7l-8.3.6 6.4 5.3L12 14.2z" fill={fill} opacity={0.82} />
|
||||
</svg>
|
||||
<div style={{height: size, position: 'relative', width: size}}>
|
||||
<Star
|
||||
cornerRadius={size * 0.035}
|
||||
fill={shade}
|
||||
innerRadius={size * 0.23}
|
||||
outerRadius={size / 2}
|
||||
points={5}
|
||||
style={{height: size, position: 'absolute', width: size}}
|
||||
/>
|
||||
<div style={{clipPath: 'polygon(0 0, 100% 0, 100% 42%, 50% 62%, 0 42%)', inset: 0, position: 'absolute'}}>
|
||||
<Star
|
||||
cornerRadius={size * 0.035}
|
||||
fill={fill}
|
||||
innerRadius={size * 0.23}
|
||||
outerRadius={size / 2}
|
||||
points={5}
|
||||
style={{height: size, width: size}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -126,7 +142,9 @@ export const GachaBetaComposition = (props) => {
|
||||
const cometVisible = seconds >= tl.cometAppear && seconds < tl.flashPeak;
|
||||
const burst = interpolate(seconds, [tl.flightEnd - 0.1, tl.flashPeak], [0, 1], clamp);
|
||||
const flash = interpolate(seconds, [tl.flightEnd, tl.flashPeak, tl.flashEnd], [0, 1, 0], clamp);
|
||||
const cardVisible = seconds >= tl.cardIn - 0.2;
|
||||
const cardVisible = seconds >= tl.cardIn;
|
||||
const skyIn = interpolate(seconds, [0, 0.65], [0, 1], clamp);
|
||||
const revealIn = interpolate(seconds, [tl.cardIn, tl.flashEnd + 0.2], [0, 1], clamp);
|
||||
const pierceBurst = interpolate(seconds, [pierceTime, pierceTime + 0.9], [0, 1], {
|
||||
...clamp,
|
||||
easing: Easing.out(Easing.cubic),
|
||||
@@ -179,13 +197,14 @@ export const GachaBetaComposition = (props) => {
|
||||
drawables.push({
|
||||
depth: hero.depth,
|
||||
node: (
|
||||
<div key="hero" style={{left: hero.x, position: 'absolute', top: hero.y}}>
|
||||
<div key="hero" style={{left: hero.x, opacity: skyIn, position: 'absolute', top: hero.y}}>
|
||||
{scene.heroPuffs.map((puff, index) => {
|
||||
const distance = Math.hypot(puff.dx, puff.dy) || 1;
|
||||
const push = pierceBurst * (0.6 + (1 - Math.min(1, distance)) * 1.4);
|
||||
const ox = (puff.dx / distance) * push;
|
||||
const oy = (puff.dy / distance) * push;
|
||||
const opacity = 1 - pierceBurst * (distance < 0.5 ? 0.95 : 0.55);
|
||||
const angle = (index / scene.heroPuffs.length) * Math.PI * 2;
|
||||
const ox = Math.cos(angle) * push;
|
||||
const oy = Math.sin(angle) * push;
|
||||
const opacity = 1 - pierceBurst * (distance < 0.5 ? 1 : 0.65);
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
@@ -197,7 +216,7 @@ export const GachaBetaComposition = (props) => {
|
||||
opacity,
|
||||
position: 'absolute',
|
||||
top: (puff.dy + oy) * radius - puff.r * radius,
|
||||
transform: `scale(${1 + pierceBurst * 0.3})`,
|
||||
transform: `rotate(${puff.dx * 12}deg) scale(${1 + pierceBurst * 0.3}, 0.78)`,
|
||||
width: puff.r * radius * 2,
|
||||
}}
|
||||
/>
|
||||
@@ -276,7 +295,7 @@ export const GachaBetaComposition = (props) => {
|
||||
left: cometScreen.x - headSize / 2,
|
||||
position: 'absolute',
|
||||
top: cometScreen.y - headSize / 2,
|
||||
transform: `rotate(${seconds * 140}deg)`,
|
||||
transform: `perspective(${height}px) rotateY(${Math.sin(seconds * 4) * 32}deg) rotateZ(${seconds * 100}deg)`,
|
||||
}}
|
||||
>
|
||||
<ToonStar fill="#ffffff" shade={palette.core} size={headSize} />
|
||||
@@ -328,9 +347,34 @@ export const GachaBetaComposition = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (hero && seconds >= pierceTime && seconds < pierceTime + 0.65) {
|
||||
const impact = (seconds - pierceTime) / 0.65;
|
||||
const diameter = heroCloud.radius * hero.scale * (0.4 + impact * 3.5);
|
||||
drawables.push({
|
||||
depth: hero.depth - 1,
|
||||
node: (
|
||||
<div
|
||||
key="cloud-impact"
|
||||
style={{
|
||||
border: `${Math.max(1, 8 * (1 - impact))}px solid ${palette.core}`,
|
||||
borderRadius: '50%',
|
||||
boxShadow: `0 0 ${height * 0.04}px ${palette.glow}`,
|
||||
height: diameter,
|
||||
left: hero.x - diameter / 2,
|
||||
opacity: 1 - impact,
|
||||
position: 'absolute',
|
||||
top: hero.y - diameter / 2,
|
||||
transform: 'rotateX(48deg)',
|
||||
width: diameter,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
drawables.sort((a, b) => b.depth - a.depth);
|
||||
|
||||
const rank = getBetaRank(props.rarity);
|
||||
const rank = getBetaRank();
|
||||
const cardPop = interpolate(seconds, [tl.cardIn, tl.cardIn + 0.35, tl.cardIn + 0.55], [0.6, 1.08, 1], {
|
||||
...clamp,
|
||||
easing: Easing.out(Easing.cubic),
|
||||
@@ -341,10 +385,11 @@ export const GachaBetaComposition = (props) => {
|
||||
<AbsoluteFill style={{background: '#0a0c2c', fontFamily: baseFont, overflow: 'hidden'}}>
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
opacity: skyIn,
|
||||
transform: `translate(${shake.x}px, ${shake.y}px) rotate(${camera.roll}deg) scale(1.18)`,
|
||||
}}
|
||||
>
|
||||
<AbsoluteFill style={{background: skyBands}} />
|
||||
<AbsoluteFill style={{background: skyBackground}} />
|
||||
|
||||
<svg
|
||||
height={height}
|
||||
@@ -446,11 +491,11 @@ export const GachaBetaComposition = (props) => {
|
||||
|
||||
{cardVisible ? (
|
||||
<AbsoluteFill
|
||||
style={{background: `radial-gradient(circle, ${palette.deep} 0% 30%, #120f3a 30% 60%, #0a0c2c 60%)`}}
|
||||
style={{background: `radial-gradient(ellipse, ${withAlpha(palette.deep, 0.9)}, #0a0c2cee)`, opacity: revealIn}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: `repeating-conic-gradient(from ${seconds * 12}deg, ${withAlpha(palette.glow, 0.32)} 0deg 10deg, transparent 10deg 20deg)`,
|
||||
background: `repeating-conic-gradient(from ${seconds * 4}deg, ${withAlpha(palette.glow, 0.16)} 0deg 5deg, transparent 5deg 30deg)`,
|
||||
borderRadius: '50%',
|
||||
height: width * 1.5,
|
||||
left: width / 2 - width * 0.75,
|
||||
@@ -476,6 +521,34 @@ export const GachaBetaComposition = (props) => {
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div
|
||||
style={{
|
||||
border: `1px solid ${withAlpha(palette.core, 0.35)}`,
|
||||
borderRadius: '50%',
|
||||
height: height * 0.8,
|
||||
left: width / 2 - height * 0.4,
|
||||
position: 'absolute',
|
||||
top: height * 0.5 - height * 0.4,
|
||||
transform: `rotate(${seconds * 5}deg)`,
|
||||
width: height * 0.8,
|
||||
}}
|
||||
>
|
||||
{Array.from({length: 12}, (_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
background: palette.core,
|
||||
height: height * 0.018,
|
||||
left: '50%',
|
||||
opacity: 0.65,
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
transform: `rotate(${index * 30}deg) translateY(${-height * 0.4}px)`,
|
||||
width: 2,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
alignItems: 'center',
|
||||
@@ -501,7 +574,7 @@ export const GachaBetaComposition = (props) => {
|
||||
transform: `scale(${rankPop})`,
|
||||
WebkitBackgroundClip: 'text',
|
||||
backgroundClip: 'text',
|
||||
filter: `drop-shadow(0 ${height * 0.012}px 0 #8a4b00) drop-shadow(0 0 ${height * 0.04}px ${palette.glow})`,
|
||||
filter: `drop-shadow(0 0 ${height * 0.04}px ${palette.glow})`,
|
||||
}}
|
||||
>
|
||||
{rank}
|
||||
@@ -516,7 +589,7 @@ export const GachaBetaComposition = (props) => {
|
||||
maxWidth: width * 0.8,
|
||||
overflowWrap: 'anywhere',
|
||||
textAlign: 'center',
|
||||
textShadow: `0 ${height * 0.01}px 0 ${palette.deep}, 0 0 ${height * 0.03}px ${withAlpha(palette.glow, 0.8)}`,
|
||||
textShadow: `0 0 ${height * 0.03}px ${withAlpha(palette.glow, 0.8)}`,
|
||||
}}
|
||||
>
|
||||
{props.label}
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
* React so the choreography can be unit tested.
|
||||
*/
|
||||
|
||||
/** @typedef {3 | 4 | 5} GachaRarity */
|
||||
|
||||
/**
|
||||
* @typedef {object} Vec3
|
||||
* @property {number} x
|
||||
@@ -48,8 +46,8 @@ export const gachaBetaTimeline = Object.freeze({
|
||||
/** The cloud the camera dollies toward and the comet pierces. */
|
||||
export const heroCloud = Object.freeze({x: 0, y: 30, z: 800, radius: 170});
|
||||
|
||||
/** Where the comet waits behind the hero cloud, then where it exits in front. */
|
||||
const cometHidden = Object.freeze({x: 60, y: 10, z: 1500});
|
||||
/** The approach behind the cloud, the entry point, and the exit in front. */
|
||||
const cometHidden = Object.freeze({x: -1050, y: -430, z: 1500});
|
||||
const cometPierceFrom = Object.freeze({x: 40, y: 20, z: 1300});
|
||||
const cometPierceTo = Object.freeze({x: 0, y: 30, z: 500});
|
||||
|
||||
@@ -92,7 +90,7 @@ const easeOutCubic = (t) => 1 - (1 - clamp01(t)) ** 3;
|
||||
const lerp3 = (a, b, t) => ({x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t, z: a.z + (b.z - a.z) * t});
|
||||
|
||||
/**
|
||||
* Comet position in world units. It waits behind the hero cloud, punches
|
||||
* Comet position in world units. It approaches behind the hero cloud, punches
|
||||
* straight through it toward the camera, then flies off on a rising arc and
|
||||
* slows as it gathers light for the burst.
|
||||
*
|
||||
@@ -202,11 +200,5 @@ export const getBetaShake = (seconds, height) => {
|
||||
return {x, y};
|
||||
};
|
||||
|
||||
/** @type {Record<GachaRarity, 'S' | 'SS' | 'SSS'>} */
|
||||
const betaRanks = {3: 'S', 4: 'SS', 5: 'SSS'};
|
||||
|
||||
/**
|
||||
* @param {GachaRarity} rarity
|
||||
* @returns {'S' | 'SS' | 'SSS'}
|
||||
*/
|
||||
export const getBetaRank = (rarity) => betaRanks[rarity];
|
||||
/** The beta food wish always reveals SSS; rarity still controls its light palette. */
|
||||
export const getBetaRank = () => 'SSS';
|
||||
Reference in new issue
Block a user