Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/banner-caption-additive-sheen.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 3 additions & 2 deletions src/content/banner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
20 changes: 10 additions & 10 deletions src/content/banner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,23 +38,23 @@ 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);
sheen.alt = '';
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) {
Expand Down
5 changes: 3 additions & 2 deletions src/content/eldenBanner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
37 changes: 16 additions & 21 deletions src/content/eldenBanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down
14 changes: 8 additions & 6 deletions src/content/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
Loading