-
Notifications
You must be signed in to change notification settings - Fork 1
fix(client): keep exported share images on an opaque background #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| /** 无主题或亮色时的不透明分享图底色,对应卡片 `var()` 兜底 `#f7f8fa`。 */ | ||
| export declare const SHARE_LIGHT_BACKGROUND = "rgb(247, 248, 250)"; | ||
| /** 官方暗色 `--dsw-alias-bg-base` 的实色,避免皮肤把 token 设成 transparent。 */ | ||
| export declare const SHARE_DARK_BACKGROUND = "rgb(21, 21, 23)"; | ||
| export declare function shareFallbackBackground(document: Document): string; | ||
| /** 把计算色压成不透明实色。透明走主题兜底;半透明叠在兜底之上。无法解析的颜色原样返回。 */ | ||
| export declare function flattenOpaqueBackground(color: string, fallback: string): string; | ||
| export declare function shareExportBackground(element: HTMLElement): string; | ||
| //# sourceMappingURL=background.d.ts.map |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| /** 无主题或亮色时的不透明分享图底色,对应卡片 `var()` 兜底 `#f7f8fa`。 */ | ||
| export const SHARE_LIGHT_BACKGROUND = 'rgb(247, 248, 250)' | ||
| /** 官方暗色 `--dsw-alias-bg-base` 的实色,避免皮肤把 token 设成 transparent。 */ | ||
| export const SHARE_DARK_BACKGROUND = 'rgb(21, 21, 23)' | ||
|
|
||
| export function shareFallbackBackground(document: Document): string { | ||
| return document.body.hasAttribute('data-ds-dark-theme') | ||
| ? SHARE_DARK_BACKGROUND | ||
| : SHARE_LIGHT_BACKGROUND | ||
| } | ||
|
|
||
| interface CssColor { | ||
| r: number | ||
| g: number | ||
| b: number | ||
| a: number | ||
| } | ||
|
|
||
| function clampByte(value: number): number { | ||
| return Math.min(255, Math.max(0, Math.round(value))) | ||
| } | ||
|
|
||
| function parseAlpha(raw: string): number { | ||
| if (raw.endsWith('%')) return Math.min(1, Math.max(0, Number(raw.slice(0, -1)) / 100)) | ||
| const value = Number(raw) | ||
| if (!Number.isFinite(value)) return 1 | ||
| return Math.min(1, Math.max(0, value)) | ||
| } | ||
|
|
||
| function parseCssColor(color: string): CssColor | null { | ||
| const value = color.trim().toLowerCase() | ||
| if (!value || value === 'transparent' || value === 'none') { | ||
| return { r: 0, g: 0, b: 0, a: 0 } | ||
| } | ||
|
|
||
| const hex = value.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/) | ||
| if (hex) { | ||
| const digits = hex[1].length === 3 | ||
| ? hex[1].split('').map(digit => digit + digit).join('') | ||
| : hex[1] | ||
| return { | ||
| r: Number.parseInt(digits.slice(0, 2), 16), | ||
| g: Number.parseInt(digits.slice(2, 4), 16), | ||
| b: Number.parseInt(digits.slice(4, 6), 16), | ||
| a: 1, | ||
| } | ||
| } | ||
|
|
||
| const comma = value.match( | ||
| /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+%?))?\s*\)$/, | ||
| ) | ||
| if (comma) { | ||
| return { | ||
| r: Number(comma[1]), | ||
| g: Number(comma[2]), | ||
| b: Number(comma[3]), | ||
| a: comma[4] === undefined ? 1 : parseAlpha(comma[4]), | ||
| } | ||
| } | ||
|
|
||
| const space = value.match( | ||
| /^rgba?\(\s*([\d.]+)\s+([\d.]+)\s+([\d.]+)(?:\s*\/\s*([\d.]+%?))?\s*\)$/, | ||
| ) | ||
| if (space) { | ||
| return { | ||
| r: Number(space[1]), | ||
| g: Number(space[2]), | ||
| b: Number(space[3]), | ||
| a: space[4] === undefined ? 1 : parseAlpha(space[4]), | ||
| } | ||
| } | ||
|
|
||
| return null | ||
| } | ||
|
|
||
| /** 把计算色压成不透明实色。透明走主题兜底;半透明叠在兜底之上。无法解析的颜色原样返回。 */ | ||
| export function flattenOpaqueBackground(color: string, fallback: string): string { | ||
| const parsed = parseCssColor(color) | ||
| if (!parsed) { | ||
| const normalized = color.trim().toLowerCase() | ||
| return !normalized || normalized === 'transparent' ? fallback : color | ||
| } | ||
| if (parsed.a <= 0) return fallback | ||
| if (parsed.a >= 1) return `rgb(${clampByte(parsed.r)}, ${clampByte(parsed.g)}, ${clampByte(parsed.b)})` | ||
|
|
||
| const base = parseCssColor(fallback) ?? parseCssColor(SHARE_LIGHT_BACKGROUND)! | ||
| const alpha = parsed.a | ||
| return `rgb(${clampByte(parsed.r * alpha + base.r * (1 - alpha))}, ${clampByte(parsed.g * alpha + base.g * (1 - alpha))}, ${clampByte(parsed.b * alpha + base.b * (1 - alpha))})` | ||
| } | ||
|
|
||
| export function shareExportBackground(element: HTMLElement): string { | ||
| const view = element.ownerDocument.defaultView | ||
| const computed = view?.getComputedStyle(element).backgroundColor ?? '' | ||
| return flattenOpaqueBackground(computed, shareFallbackBackground(element.ownerDocument)) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| // @vitest-environment jsdom | ||
|
|
||
| import { afterEach, describe, expect, it } from 'vitest' | ||
| import { | ||
| SHARE_DARK_BACKGROUND, | ||
| SHARE_LIGHT_BACKGROUND, | ||
| flattenOpaqueBackground, | ||
| shareExportBackground, | ||
| shareFallbackBackground, | ||
| } from '../src/client/background.ts' | ||
|
|
||
| afterEach(() => { | ||
| document.body.innerHTML = '' | ||
| document.body.removeAttribute('data-ds-dark-theme') | ||
| }) | ||
|
|
||
| describe('分享图底色', () => { | ||
| it('把透明和缺省色换成不透明兜底,半透明叠在兜底上', () => { | ||
| expect(flattenOpaqueBackground('transparent', SHARE_LIGHT_BACKGROUND)).toBe(SHARE_LIGHT_BACKGROUND) | ||
| expect(flattenOpaqueBackground('rgba(0, 0, 0, 0)', SHARE_DARK_BACKGROUND)).toBe(SHARE_DARK_BACKGROUND) | ||
| expect(flattenOpaqueBackground('rgb(255, 255, 255)', SHARE_LIGHT_BACKGROUND)).toBe('rgb(255, 255, 255)') | ||
| expect(flattenOpaqueBackground('#f7f8fa', SHARE_DARK_BACKGROUND)).toBe(SHARE_LIGHT_BACKGROUND) | ||
| expect(flattenOpaqueBackground('rgba(13, 16, 30, 0.86)', SHARE_DARK_BACKGROUND)).toBe('rgb(14, 17, 29)') | ||
| expect(flattenOpaqueBackground('color(srgb 1 1 1)', SHARE_LIGHT_BACKGROUND)).toBe('color(srgb 1 1 1)') | ||
| }) | ||
|
|
||
| it('亮色和暗色主题使用不同的不透明兜底', () => { | ||
| expect(shareFallbackBackground(document)).toBe(SHARE_LIGHT_BACKGROUND) | ||
| document.body.setAttribute('data-ds-dark-theme', '') | ||
| expect(shareFallbackBackground(document)).toBe(SHARE_DARK_BACKGROUND) | ||
| }) | ||
|
|
||
| it('从元素计算样式得到不透明导出底色', () => { | ||
| const element = document.createElement('div') | ||
| element.style.backgroundColor = 'transparent' | ||
| document.body.append(element) | ||
| expect(shareExportBackground(element)).toBe(SHARE_LIGHT_BACKGROUND) | ||
|
|
||
| document.body.setAttribute('data-ds-dark-theme', '') | ||
| element.style.backgroundColor = 'rgba(0, 0, 0, 0)' | ||
| expect(shareExportBackground(element)).toBe(SHARE_DARK_BACKGROUND) | ||
| }) | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| // @vitest-environment jsdom | ||
|
|
||
| import { afterEach, describe, expect, it, vi } from 'vitest' | ||
| import { SHARE_DARK_BACKGROUND, SHARE_LIGHT_BACKGROUND } from '../src/client/background.ts' | ||
|
|
||
| const toBlob = vi.hoisted(() => vi.fn()) | ||
|
|
||
| vi.mock('html-to-image', () => ({ toBlob })) | ||
|
|
||
| afterEach(() => { | ||
| document.body.innerHTML = '' | ||
| document.body.removeAttribute('data-ds-dark-theme') | ||
| toBlob.mockReset() | ||
| }) | ||
|
|
||
| describe('分享图渲染', () => { | ||
| it('把透明计算底色换成不透明颜色再交给 html-to-image', async () => { | ||
| toBlob.mockResolvedValue(new Blob(['png'], { type: 'image/png' })) | ||
| const { renderShareImage } = await import('../src/client/preview-dialog.ts') | ||
| const element = document.createElement('article') | ||
| element.style.backgroundColor = 'transparent' | ||
| document.body.append(element) | ||
|
|
||
| await renderShareImage(element) | ||
|
|
||
| expect(toBlob).toHaveBeenCalledTimes(1) | ||
| expect(toBlob.mock.calls[0]?.[1]).toMatchObject({ | ||
| backgroundColor: SHARE_LIGHT_BACKGROUND, | ||
| pixelRatio: 2, | ||
| skipFonts: true, | ||
| }) | ||
| }) | ||
|
|
||
| it('暗色主题下透明底色改用官方暗色实色', async () => { | ||
| toBlob.mockResolvedValue(new Blob(['png'], { type: 'image/png' })) | ||
| document.body.setAttribute('data-ds-dark-theme', '') | ||
| const { renderShareImage } = await import('../src/client/preview-dialog.ts') | ||
| const element = document.createElement('article') | ||
| element.style.backgroundColor = 'rgba(0, 0, 0, 0)' | ||
| document.body.append(element) | ||
|
|
||
| await renderShareImage(element) | ||
|
|
||
| expect(toBlob.mock.calls[0]?.[1]).toMatchObject({ | ||
| backgroundColor: SHARE_DARK_BACKGROUND, | ||
| }) | ||
| }) | ||
| }) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.