diff --git a/.changeset/banner-caption-additive-sheen.md b/.changeset/banner-caption-additive-sheen.md new file mode 100644 index 0000000..5c2b7f9 --- /dev/null +++ b/.changeset/banner-caption-additive-sheen.md @@ -0,0 +1,12 @@ +--- +'elden-ring-github': patch +--- + +Restore the layered-gold look of the reference banner artwork. The caption is a +deep matte gold face with the image-creator's warm ghost (blurTint [255,208,66] +@ 0.18) stacked on top and blended additively via `mix-blend-mode: plus-lighter` +(falling back to `screen`). Where the ghost aligns over the face it adds to it — +the bright overlap tone (~255,213,57) — and where it spreads past the letters it +becomes the dim offset echo, so overlapping text shows a real blended colour +instead of flat gold. The ghost is a separate layer so its outward-spread +(scaleX -> 1.11) can animate after the fade-in. diff --git a/src/content/banner.test.ts b/src/content/banner.test.ts index ba6ff71..4b05c50 100644 --- a/src/content/banner.test.ts +++ b/src/content/banner.test.ts @@ -85,9 +85,10 @@ describe('renderBanner', () => { expect(band?.src).toBe('data:image/png;base64,mock'); expect(sheen?.src).toBe('data:image/png;base64,mock'); expect(caption?.src).toBe('data:image/png;base64,mock'); - // Caption must come after the sheen so the opaque letters mask its centre. + // Sheen must come after the caption so its additive ghost blends on top + // of the gold face (bright overlap tone) and spreads into the echo. expect( - sheen!.compareDocumentPosition(caption!) & Node.DOCUMENT_POSITION_FOLLOWING, + caption!.compareDocumentPosition(sheen!) & Node.DOCUMENT_POSITION_FOLLOWING, ).toBeTruthy(); vi.runAllTimers(); diff --git a/src/content/banner.ts b/src/content/banner.ts index 2ef6967..e0c9476 100644 --- a/src/content/banner.ts +++ b/src/content/banner.ts @@ -38,9 +38,16 @@ export const renderBanner = ({ band.setAttribute('aria-hidden', 'true'); banner.appendChild(band); - // Middle layer: the faint echo that spreads outward after the fade-in. - // Stacked below the caption so its centre stays masked by the opaque - // letters and only the spreading fringe shows. + // Middle layer: the deep matte gold caption face. + const caption_ = document.createElement('img'); + caption_.className = 'banner-caption'; + caption_.src = generateCaptionDataUrl(resolvedCaption); + caption_.alt = resolvedCaption; + banner.appendChild(caption_); + + // Top layer: the warm additive sheen, blended over the face. Where it aligns + // it brightens the gold (the layered overlap tone); as it spreads outward + // after the fade-in it becomes the dim offset echo. const sheen = document.createElement('img'); sheen.className = 'banner-sheen'; sheen.src = generateSheenDataUrl(resolvedCaption); @@ -48,13 +55,6 @@ export const renderBanner = ({ sheen.setAttribute('aria-hidden', 'true'); banner.appendChild(sheen); - // Top layer: the opaque gold caption. - const caption_ = document.createElement('img'); - caption_.className = 'banner-caption'; - caption_.src = generateCaptionDataUrl(resolvedCaption); - caption_.alt = resolvedCaption; - banner.appendChild(caption_); - document.body.appendChild(banner); if (soundEnabled) { diff --git a/src/content/eldenBanner.test.ts b/src/content/eldenBanner.test.ts index d1aa744..76d16bf 100644 --- a/src/content/eldenBanner.test.ts +++ b/src/content/eldenBanner.test.ts @@ -59,8 +59,9 @@ describe('generateCaptionDataUrl', () => { expect(result).toBe('data:image/png;base64,mock'); expect(fillTextCalls.some((args) => args[0] === 'PULL REQUEST MERGED')).toBe(true); - // glow/shadow and gold face passes render the caption (sheen is separate now) - expect(fillTextCalls.length).toBeGreaterThanOrEqual(2); + // The front caption is a single translucent gold pass; the back gold layer + // and its animation live in the separate sheen image. + expect(fillTextCalls.length).toBeGreaterThanOrEqual(1); }); it('returns an empty string when a 2d context is unavailable', () => { diff --git a/src/content/eldenBanner.ts b/src/content/eldenBanner.ts index 93c92b3..89fe911 100644 --- a/src/content/eldenBanner.ts +++ b/src/content/eldenBanner.ts @@ -31,18 +31,20 @@ const SPECK_COUNT = 45; const SPECK_COLOR = '225, 195, 140'; // --- Matte gold caption --- -const SHEEN_COLOR = 'rgb(230, 190, 120)'; -const SHEEN_OPACITY = 0.12; -// The sheen's horizontal spread (SHEEN_SCALE_X) is applied in CSS as an -// animated transform, so the sheen image itself is drawn aligned (scaleX 1). -const GLOW_COLOR = 'rgba(255, 205, 110, 0.35)'; -const GLOW_BLUR = 0.14; // fraction of font size -const SHADOW_FILL = 'rgba(60, 40, 18, 0.55)'; // soft dark base for legibility - -// Flat, matte amber gold — the near-uniform in-game caption colour (the -// image-creator's Elden "Victory" preset textColor), deeper than a pastel gold. +// The caption face is the image-creator's deep matte "Victory" gold, drawn as a +// single clean opaque pass — matching the reference banner PNGs exactly (no +// bloom, no dark base; those made it either too bright or muddy). const GOLD_COLOR = 'rgb(220, 175, 45)'; +// The .banner-sheen layer is the image-creator's additive ghost (blurTint +// [255,208,66] @ blurOpacity 0.18), stacked on top of the face and blended +// additively (mix-blend-mode:plus-lighter in CSS). Where it aligns over the +// face it adds to the deep gold -> the bright overlap tone (~255,213,57); where +// it spreads past the letters it becomes the dim offset echo. Its outward +// spread (SHEEN_SCALE_X in CSS) is animated after the fade-in. +const SHEEN_COLOR = 'rgb(255, 208, 66)'; +const SHEEN_OPACITY = 0.18; + const applyFont = (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement): number => { const fontSize = FONT_SIZE * (canvas.height / 1080); ctx.font = `${FONT_WEIGHT} ${fontSize}px ${FONT_FAMILY}`; @@ -131,18 +133,9 @@ const drawEldenText = ( ): void => { const centerX = canvas.width / 2; const centerY = canvas.height / 2; - const fontSize = applyFont(ctx, canvas); - - // Warm halo plus a soft dark base so the text stays legible over any page. - ctx.save(); applyFont(ctx, canvas); - ctx.shadowColor = GLOW_COLOR; - ctx.shadowBlur = fontSize * GLOW_BLUR; - ctx.fillStyle = SHADOW_FILL; - ctx.fillText(caption, centerX, centerY + fontSize * 0.012); - ctx.restore(); - // Flat, matte gold face. + // Opaque deep matte gold face — a single clean pass, matching the reference. ctx.save(); applyFont(ctx, canvas); ctx.fillStyle = GOLD_COLOR; @@ -158,7 +151,9 @@ const drawSheenEcho = ( const centerX = canvas.width / 2; const centerY = canvas.height / 2; - // Aligned (scaleX 1) faint echo; the outward spread is animated in CSS. + // Dim warm echo, drawn aligned (scaleX 1); the outward spread is animated in + // CSS. Stacked below the opaque caption, so its core is masked and only the + // faint spreading fringe shows as the see-through echo. ctx.save(); applyFont(ctx, canvas); ctx.globalAlpha = SHEEN_OPACITY; diff --git a/src/content/styles.css b/src/content/styles.css index a22ac87..e4b6a42 100644 --- a/src/content/styles.css +++ b/src/content/styles.css @@ -40,20 +40,22 @@ display: block; } -/* Sheen layer: the caption echo, stacked *below* the caption (earlier in the - DOM) so the opaque letters mask its aligned core and only the offset echo - emerges from behind them. It blends additively over the dark band, starts - aligned (scaleX 1), then spreads outward after the fade-in — a visible second - layer of text peeking out to either side. */ +/* Sheen layer: the image-creator's additive ghost, stacked *above* the caption + (later in the DOM). Blended additively (plus-lighter) so where it aligns over + the gold face it adds to it — the bright overlap tone — and where it spreads + it becomes the dim offset echo. Starts aligned (scaleX 1), then spreads + outward after the fade-in. Falls back to screen where plus-lighter is + unsupported. */ .banner-sheen { mix-blend-mode: screen; + mix-blend-mode: plus-lighter; transform-origin: center; transform: scaleX(1); transition: transform 0.6s ease-out 0.2s; } #elden-ring-banner.show .banner-sheen { - transform: scaleX(1.06); + transform: scaleX(1.11); } /* Fallback text banner for when no image is available */