mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-11 03:13:46 +00:00
feat(gacha): ease the tear, pack drop, and card rise exponentially
The scripted drag across the seal, the torn pack falling away, and the card rising out of it now ease exponentially in and out. The pack keeps pack-cards' 1.35s drop and hold, and the card still clears the pack when the spin starts.
This commit is contained in:
1 parent
9e3cf9d78a
commit
76c96464a8
4 files changed
+63
-8
No files matched your search
@@ -34,6 +34,26 @@ body {
|
||||
width: min(36vh, 60vw);
|
||||
}
|
||||
|
||||
/*
|
||||
* The torn pack keeps pack-cards' 1.35s drop and its short hold, but falls
|
||||
* in one exponential ease-in-out. Not scoped to #stage: the pack also drops
|
||||
* inside pack-cards' flight overlay on <body>.
|
||||
*/
|
||||
html .recap-pack-torn :is(.recap-webgl-pack, .recap-pack-lining) {
|
||||
animation-name: wish-pack-away;
|
||||
animation-timing-function: cubic-bezier(0.87, 0, 0.13, 1);
|
||||
}
|
||||
|
||||
@keyframes wish-pack-away {
|
||||
0%,
|
||||
20% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(calc(100vh + 100%));
|
||||
}
|
||||
}
|
||||
|
||||
/* A video has no buttons or prompts to read: hide the interactive captions. */
|
||||
#stage .recap-pack-scene > .eyebrow,
|
||||
#stage .recap-pack-title,
|
||||
|
||||
@@ -52,12 +52,14 @@ const createFace = () => {
|
||||
* over 1750ms: the card is clear of the pack at 900ms, then it flips with a
|
||||
* half turn (rotateY 180deg to 0deg) and the pack drops away while it glides
|
||||
* to the centre. The page reshapes those animations without changing the
|
||||
* library: the rise keeps its speed, everything after the card clears the
|
||||
* pack is stretched so the flip has time for three extra turns, and the flip
|
||||
* eases out like a flicked card slowing to a stop while sparkles burst
|
||||
* around it.
|
||||
* library: the rise out of the pack eases exponentially in and out,
|
||||
* everything after the card clears the pack is stretched so the flip has time
|
||||
* for three extra turns, and the flip eases out like a flicked card slowing to
|
||||
* a stop while sparkles burst around it.
|
||||
*/
|
||||
const flight = Object.freeze({duration: 1750, clearAt: 900});
|
||||
/** Exponential ease-in-out, as a CSS timing function. */
|
||||
const easeInOutExpo = 'cubic-bezier(.87, 0, .13, 1)';
|
||||
const afterClearStretch = 1.65;
|
||||
const flip = Object.freeze({startDegrees: 180 + 3 * 360, easing: 'cubic-bezier(.3, .4, .25, 1)'});
|
||||
|
||||
@@ -139,9 +141,16 @@ Element.prototype.animate = function (keyframes, options) {
|
||||
// The flight path: keyframes spaced evenly over the whole flight.
|
||||
if (options.duration === flight.duration && options.easing === 'linear') {
|
||||
const total = stretch(flight.duration);
|
||||
const stretched = keyframes.map((frame) => ({
|
||||
const at = (/** @type {Keyframe} */ frame) => Number(frame.offset) * flight.duration;
|
||||
// Until the card clears the pack it only translates, so the rise is one
|
||||
// straight segment: keep its two ends and ease between them.
|
||||
const cleared = keyframes.findIndex((frame) => at(frame) > flight.clearAt);
|
||||
const [start] = keyframes;
|
||||
const eased =
|
||||
start && cleared > 1 ? [{...start, easing: easeInOutExpo}, ...keyframes.slice(cleared - 1)] : keyframes;
|
||||
const stretched = eased.map((frame) => ({
|
||||
...frame,
|
||||
offset: stretch(Number(frame.offset) * flight.duration) / total,
|
||||
offset: stretch(at(frame)) / total,
|
||||
}));
|
||||
return animate.call(this, stretched, {...options, duration: total});
|
||||
}
|
||||
|
||||
@@ -82,10 +82,23 @@ export const getWishPageProps = (request) => {
|
||||
return {label: request.label, rank, stars: request.rarity, rarity, glow, artwork: {accent, tint}};
|
||||
};
|
||||
|
||||
/**
|
||||
* Exponential ease-in-out over 0..1.
|
||||
*
|
||||
* @param {number} t
|
||||
*/
|
||||
const easeInOutExpo = (t) => {
|
||||
if (t <= 0 || t >= 1) {
|
||||
return Math.min(1, Math.max(0, t));
|
||||
}
|
||||
return t < 0.5 ? 2 ** (20 * t - 10) / 2 : (2 - 2 ** (10 - 20 * t)) / 2;
|
||||
};
|
||||
|
||||
/**
|
||||
* Pointer position for a frame during the drag, as fractions of the pack
|
||||
* control's box, or null outside it. The button is held while the pointer
|
||||
* sweeps across the seal.
|
||||
* sweeps across the seal, easing exponentially in and out like a hand
|
||||
* tearing it.
|
||||
*
|
||||
* @param {number} seconds
|
||||
* @returns {{x: number, y: number} | null}
|
||||
@@ -95,7 +108,7 @@ export const getDragPointer = (seconds) => {
|
||||
if (seconds < dragStart || seconds > dragEnd) {
|
||||
return null;
|
||||
}
|
||||
return {x: 0.08 + 0.9 * ((seconds - dragStart) / (dragEnd - dragStart)), y: 0.12};
|
||||
return {x: 0.08 + 0.9 * easeInOutExpo((seconds - dragStart) / (dragEnd - dragStart)), y: 0.12};
|
||||
};
|
||||
|
||||
const contentTypes = Object.freeze({
|
||||
|
||||
@@ -46,6 +46,19 @@ describe('gacha wish plan', () => {
|
||||
expect(dragEnd + 2).toBeLessThan(gachaTotalSeconds);
|
||||
});
|
||||
|
||||
test('the drag eases exponentially in and out across the seal', () => {
|
||||
const {dragStart, dragEnd} = wishTimeline;
|
||||
const x = (/** @type {number} */ fraction) =>
|
||||
getDragPointer(dragStart + (dragEnd - dragStart) * fraction)?.x ?? NaN;
|
||||
expect(x(0)).toBeCloseTo(0.08);
|
||||
expect(x(1)).toBeCloseTo(0.98);
|
||||
expect(x(0.5)).toBeCloseTo(0.53);
|
||||
// Slow at both ends, fast through the middle.
|
||||
expect(x(0.2) - x(0)).toBeLessThan(0.01);
|
||||
expect(x(1) - x(0.8)).toBeLessThan(0.01);
|
||||
expect(x(0.6) - x(0.4)).toBeGreaterThan(0.6);
|
||||
});
|
||||
|
||||
test('page requests map to the page directory and the installed package', () => {
|
||||
expect(resolveWishAsset(`${wishOrigin}/index.html`, roots)).toEqual({
|
||||
file: path.join(roots.page, 'index.html'),
|
||||
|
||||
Reference in new issue
Block a user