diff --git a/renderer/plans/journals/2026-10-02-gacha-glint-additive-blend-and-full-card-coverage.md b/renderer/plans/journals/2026-10-02-gacha-glint-additive-blend-and-full-card-coverage.md new file mode 100644 index 0000000..dc67a15 --- /dev/null +++ b/renderer/plans/journals/2026-10-02-gacha-glint-additive-blend-and-full-card-coverage.md @@ -0,0 +1,33 @@ +--- +title: "Gacha glint: additive blend and full-card coverage" +date: 2026-10-02 +summary: "Mirror glint now blends additively in the card's accent tone and sweeps every corner of the card" +--- + +# Gacha glint: additive blend and full-card coverage + +## What happened +- The user asked for an additive blend on the gacha card's mirror glint. If that cost too much GPU, they wanted lower alpha and a colour that matches the card's tone instead. +- They also noticed the glint did not cover the whole card. + +## Root cause (coverage) +- `.wish-glint` was a card-sized box carrying a 135deg gradient, translated from (-100%,-100%) to (100%,100%). +- A band only paints inside its own box, so as the box slid diagonally the top-right and bottom-left corners were never inside it at the moment the band passed. With a 63:88 card, the top-right corner sits about 5px outside the box when the band reaches it. + +## Changes +- `src/gacha/page/page.css`: + - The glint clip uses `mix-blend-mode: plus-lighter`. + - Band and flare colours are tinted with `--glint`, the card accent, with peak alpha around 0.75. + - The band box is now 300% of the card, centred on it, with gradient stops rescaled by 1/3. The flare width goes from 46% to 15.3%. +- `src/gacha/page/page.js`: + - `--glint` is set on the clip itself, because the card flies in an overlay outside #stage. + - The sweep now runs from translate(-26.7%) to translate(26.7%), which is 0.8 card sizes each way. + +## Evidence +- Docker render timings showed additive and normal blending within noise: 14.2s vs 14.4s and 11.6s vs 11.5s per clip. Headless Chrome composites in software here, and blending costs nothing measurable. +- Frames at 3.8s (3-star) and 3.75s (5-star) show the band reaching the card edges, tinted blue and gold. + +## Next steps +- The user reviews the fixtures before commit. + +> Historical work record — not durable authority. Prefer docs/specs/ADRs for current decisions. diff --git a/renderer/src/gacha/page/page.css b/renderer/src/gacha/page/page.css index 513c331..307c5cd 100644 --- a/renderer/src/gacha/page/page.css +++ b/renderer/src/gacha/page/page.css @@ -133,10 +133,17 @@ body { transform: rotate(90deg); } -/* The mirror glint that sweeps across the card once it lands (page.js). */ +/* + * The mirror glint that sweeps across the card once it lands (page.js). It is + * blended additively (plus-lighter), so it brightens the card's own colours + * instead of painting flat white over them; the light is tinted with the + * card's accent (--glint) so it matches the card's tone. + */ .wish-glint-clip { + --glint-core: color-mix(in srgb, var(--glint, #fff) 35%, #fff); border-radius: inherit; inset: 0; + mix-blend-mode: plus-lighter; overflow: hidden; pointer-events: none; position: absolute; @@ -146,32 +153,43 @@ body { .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% + transparent 45.3%, + color-mix(in srgb, var(--glint, #fff) 14%, transparent) 47.7%, + color-mix(in srgb, var(--glint-core) 60%, transparent) 49.7%, + color-mix(in srgb, var(--glint-core) 75%, transparent) 50%, + color-mix(in srgb, var(--glint-core) 60%, transparent) 50.3%, + color-mix(in srgb, var(--glint, #fff) 14%, transparent) 52.3%, + transparent 54.7% ); - inset: 0; + height: 300%; + left: -100%; position: absolute; - transform: translate(-100%, -100%); + top: -100%; + transform: translate(-26.7%, -26.7%); + width: 300%; } -/* A four-ray flare riding the band's centre, which travels corner to corner. */ +/* + * A four-ray flare riding the band's centre, which travels corner to corner. + * Sized against the band's 3x box, so it spans 46% of the card. + */ .wish-glint-flare { - background: radial-gradient(circle, #ffffff 0 14%, rgb(255 255 255 / 0.6) 30%, transparent 64%); + background: radial-gradient( + circle, + color-mix(in srgb, var(--glint-core) 85%, transparent) 0 14%, + color-mix(in srgb, var(--glint) 45%, transparent) 30%, + transparent 64% + ); aspect-ratio: 1; left: 50%; position: absolute; top: 50%; - width: 46%; + width: 15.3%; } .wish-glint-flare::before, .wish-glint-flare::after { - background: linear-gradient(90deg, transparent, #ffffff 46% 54%, transparent); + background: linear-gradient(90deg, transparent, var(--glint-core) 46% 54%, transparent); content: ''; height: 5%; left: -40%; diff --git a/renderer/src/gacha/page/page.js b/renderer/src/gacha/page/page.js index e45e205..d41230f 100644 --- a/renderer/src/gacha/page/page.js +++ b/renderer/src/gacha/page/page.js @@ -41,6 +41,8 @@ const createFace = () => { content.append(el('span', 'wish-rank', wish.rank), title, el('p', 'wish-stars', '★'.repeat(wish.stars))); const clip = el('span', 'wish-glint-clip'); clip.setAttribute('aria-hidden', 'true'); + // Set on the clip itself: the card flies in an overlay outside #stage. + clip.style.setProperty('--glint', wish.artwork.accent); const band = el('span', 'wish-glint'); band.append(el('span', 'wish-glint-flare')); clip.append(band); @@ -162,9 +164,12 @@ const glint = Object.freeze({duration: 650, easing: 'cubic-bezier(.45, 0, .55, 1 * Keeps the card's text hidden while it spins and switches it on as the card * turns edge-on for its last swing face up, so the text rides in on the final * turn without visibly popping on. Then it 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. + * top-left corner to the bottom-right one as the card comes to rest. The glint + * band is a gradient across a box three times the card's size and centred on + * it, so the card stays inside that box for the whole sweep and every corner is + * lit; a card-sized box would leave the top-right and bottom-left corners + * outside it as it slid diagonally. Moving the box 0.8 card-widths and heights + * each way starts and ends the band clear of the card. * * @param {Element} turn * @param {number} delay Milliseconds until the spin starts. @@ -176,7 +181,7 @@ const revealFace = (turn, delay, duration) => { {delay, duration, easing: 'linear', fill: 'both'}, ); const band = turn.querySelector('.wish-glint'); - band?.animate([{transform: 'translate(-100%, -100%)'}, {transform: 'translate(100%, 100%)'}], { + band?.animate([{transform: 'translate(-26.7%, -26.7%)'}, {transform: 'translate(26.7%, 26.7%)'}], { delay: delay + duration, duration: glint.duration, easing: glint.easing,