diff --git a/renderer/src/gacha/page/page.css b/renderer/src/gacha/page/page.css index b105c1d..a229089 100644 --- a/renderer/src/gacha/page/page.css +++ b/renderer/src/gacha/page/page.css @@ -132,3 +132,54 @@ body { .wish-sparkle::after { transform: rotate(90deg); } + +/* The mirror glint that sweeps across the card once it lands (page.js). */ +.wish-glint-clip { + border-radius: inherit; + inset: 0; + overflow: hidden; + pointer-events: none; + position: absolute; + z-index: 3; +} + +.wish-glint { + background: linear-gradient( + 135deg, + transparent 36%, + rgb(255 255 255 / 0.18) 43%, + rgb(255 255 255 / 0.85) 49%, + #ffffff 50%, + rgb(255 255 255 / 0.85) 51%, + rgb(255 255 255 / 0.18) 57%, + transparent 64% + ); + inset: 0; + position: absolute; + transform: translate(-100%, -100%); +} + +/* A four-ray flare riding the band's centre, which travels corner to corner. */ +.wish-glint-flare { + background: radial-gradient(circle, #ffffff 0 14%, rgb(255 255 255 / 0.6) 30%, transparent 64%); + aspect-ratio: 1; + left: 50%; + position: absolute; + top: 50%; + width: 46%; +} + +.wish-glint-flare::before, +.wish-glint-flare::after { + background: linear-gradient(90deg, transparent, #ffffff 46% 54%, transparent); + content: ''; + height: 5%; + left: -40%; + position: absolute; + top: 47.5%; + width: 180%; +} + +.wish-glint-flare::after { + transform: rotate(90deg); +} diff --git a/renderer/src/gacha/page/page.js b/renderer/src/gacha/page/page.js index e8e7c9a..d290ff6 100644 --- a/renderer/src/gacha/page/page.js +++ b/renderer/src/gacha/page/page.js @@ -35,15 +35,16 @@ const el = (tag, className, text) => { const createFace = () => { const face = el('article', 'wish-card'); - const header = el('header', 'recap-card-header'); - header.append(el('span', '', 'GACHA'), el('span', '', `RANK ${wish.rank}`)); const content = el('div', 'recap-card-content'); const title = el('h2', 'wish-label', wish.label); title.setAttribute('data-foil-text', ''); content.append(el('span', 'wish-rank', wish.rank), title, el('p', 'wish-stars', '★'.repeat(wish.stars))); - const footer = el('footer', 'recap-card-footer'); - footer.append(el('span', '', `${wish.stars}★`), el('span', '', '01 / 01')); - face.append(header, content, footer); + const clip = el('span', 'wish-glint-clip'); + clip.setAttribute('aria-hidden', 'true'); + const band = el('span', 'wish-glint'); + band.append(el('span', 'wish-glint-flare')); + clip.append(band); + face.append(content, clip); return face; }; @@ -125,6 +126,41 @@ const burstSparkles = (deck, delay, duration) => { setTimeout(() => layer.remove(), delay + duration + 1200); }; +const glint = Object.freeze({duration: 650, easing: 'cubic-bezier(.45, 0, .55, 1)'}); + +/** + * Keeps the card's text hidden while it spins and fades it in once the card + * lies flat, then sweeps a mirror glint from the top-left corner to the + * bottom-right one as the card comes to rest. The glint band is a gradient + * across its own card-sized box, so moving that box from one corner to the + * other carries the band diagonally over the whole face. + * + * @param {Element} turn + * @param {number} delay Milliseconds until the spin starts. + * @param {number} duration Length of the spin. + */ +const revealFace = (turn, delay, duration) => { + turn.querySelector('.wish-card .recap-card-content')?.animate( + [{opacity: 0}, {opacity: 0, offset: flip.flatAt}, {opacity: 1}], + {delay, duration, easing: 'linear', fill: 'both'}, + ); + const band = turn.querySelector('.wish-glint'); + band?.animate([{transform: 'translate(-100%, -100%)'}, {transform: 'translate(100%, 100%)'}], { + delay: delay + duration, + duration: glint.duration, + easing: glint.easing, + fill: 'both', + }); + band?.querySelector('.wish-glint-flare')?.animate( + [ + {transform: 'translate(-50%, -50%) scale(0) rotate(0deg)', opacity: 0}, + {transform: 'translate(-50%, -50%) scale(1) rotate(45deg)', opacity: 1, offset: 0.5}, + {transform: 'translate(-50%, -50%) scale(0) rotate(90deg)', opacity: 0}, + ], + {delay: delay + duration, duration: glint.duration, easing: glint.easing, fill: 'both'}, + ); +}; + /** * Engravings carry the polychrome (page.css): 4★ cards are tooled with the * epic contour, 5★ cards with the legendary rings or facets, picked by @@ -178,6 +214,7 @@ Element.prototype.animate = function (keyframes, options) { if (this.parentElement) { burstSparkles(this.parentElement, flight.clearAt, duration); } + revealFace(this, flight.clearAt, duration); const flat = keyframes[1] ?? {}; return animate.call( this, @@ -188,10 +225,11 @@ Element.prototype.animate = function (keyframes, options) { return animate.call(this, keyframes, options); }; +// Blank label and title keep the pack wrapper free of printed text. const view = createPackView(stage, { - label: 'Gacha', + label: '', artwork: wish.artwork, - labels: {title: 'Gacha', swipe: '', tap: ''}, + labels: {title: '', swipe: '', tap: ''}, appearance: {opening: 'animated', motion: 'interactive', glow: wish.glow, pack_zoom: true, rarities: engravings}, });