diff --git a/lib/client.js b/lib/client.js index 05ed9a1..a65991a 100644 --- a/lib/client.js +++ b/lib/client.js @@ -106,6 +106,74 @@ window.__ModuleLoader__.load({ }; } //#endregion + //#region src/client/background.ts + /** 无主题或亮色时的不透明分享图底色,对应卡片 `var()` 兜底 `#f7f8fa`。 */ + const SHARE_LIGHT_BACKGROUND = "rgb(247, 248, 250)"; + /** 官方暗色 `--dsw-alias-bg-base` 的实色,避免皮肤把 token 设成 transparent。 */ + const SHARE_DARK_BACKGROUND = "rgb(21, 21, 23)"; + function shareFallbackBackground(document) { + return document.body.hasAttribute("data-ds-dark-theme") ? SHARE_DARK_BACKGROUND : SHARE_LIGHT_BACKGROUND; + } + function clampByte(value) { + return Math.min(255, Math.max(0, Math.round(value))); + } + function parseAlpha(raw) { + 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) { + 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] === void 0 ? 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] === void 0 ? 1 : parseAlpha(space[4]) + }; + return null; + } + /** 把计算色压成不透明实色。透明走主题兜底;半透明叠在兜底之上。无法解析的颜色原样返回。 */ + function flattenOpaqueBackground(color, fallback) { + 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("rgb(247, 248, 250)"); + 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))})`; + } + function shareExportBackground(element) { + return flattenOpaqueBackground(element.ownerDocument.defaultView?.getComputedStyle(element).backgroundColor ?? "", shareFallbackBackground(element.ownerDocument)); + } + //#endregion //#region src/client/settings.ts const WIDTH_PRESETS = { phone: 375, @@ -334,6 +402,7 @@ window.__ModuleLoader__.load({ card.append(footer); host.append(card); document.body.append(host); + card.style.backgroundColor = shareExportBackground(card); return { element: card, dispose: () => host.remove() @@ -3359,7 +3428,7 @@ window.__ModuleLoader__.load({ const TRANSPARENT_IMAGE_PLACEHOLDER = "data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs="; async function renderShareImage(element) { const blob = await (0, import_lib.toBlob)(element, { - backgroundColor: getComputedStyle(element).backgroundColor, + backgroundColor: shareExportBackground(element), imagePlaceholder: TRANSPARENT_IMAGE_PLACEHOLDER, pixelRatio: 2, skipFonts: true diff --git a/lib/types/client/background.d.ts b/lib/types/client/background.d.ts new file mode 100644 index 0000000..9dd171f --- /dev/null +++ b/lib/types/client/background.d.ts @@ -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 \ No newline at end of file diff --git a/lib/types/client/background.d.ts.map b/lib/types/client/background.d.ts.map new file mode 100644 index 0000000..6dfea96 --- /dev/null +++ b/lib/types/client/background.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"background.d.ts","sourceRoot":"","sources":["../../../src/client/background.ts"],"names":[],"mappings":"AAAA,kDAAkD;AAClD,eAAO,MAAM,sBAAsB,uBAAuB,CAAA;AAC1D,iEAAiE;AACjE,eAAO,MAAM,qBAAqB,oBAAoB,CAAA;AAEtD,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAIlE;AAkED,iDAAiD;AACjD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAY/E;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAIlE"} \ No newline at end of file diff --git a/lib/types/client/card.d.ts.map b/lib/types/client/card.d.ts.map index 178d6da..de17b2b 100644 --- a/lib/types/client/card.d.ts.map +++ b/lib/types/client/card.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"card.d.ts","sourceRoot":"","sources":["../../../src/client/card.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAChD,OAAO,EAIL,KAAK,aAAa,EACnB,MAAM,eAAe,CAAA;AAEtB,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,WAAW,CAAA;IACpB,OAAO,IAAI,IAAI,CAAA;CAChB;AAwED,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,IAAI,GAAG,IAAI,EACnB,aAAa,UAAQ,GACpB,WAAW,CAmCb;AAED,iCAAiC;AACjC,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,SAAS,WAAW,EAAE,EAChC,WAAW,EAAE,OAAO,GACnB,WAAW,EAAE,CAIf;AA2CD,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,SAAS,YAAY,EAAE,EACjC,MAAM,EAAE,IAAI,GAAG,IAAI,EACnB,QAAQ,GAAE,aAAsC,GAC/C,SAAS,CAwFX"} \ No newline at end of file +{"version":3,"file":"card.d.ts","sourceRoot":"","sources":["../../../src/client/card.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAChD,OAAO,EAIL,KAAK,aAAa,EACnB,MAAM,eAAe,CAAA;AAEtB,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,WAAW,CAAA;IACpB,OAAO,IAAI,IAAI,CAAA;CAChB;AAwED,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,IAAI,GAAG,IAAI,EACnB,aAAa,UAAQ,GACpB,WAAW,CAmCb;AAED,iCAAiC;AACjC,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,SAAS,WAAW,EAAE,EAChC,WAAW,EAAE,OAAO,GACnB,WAAW,EAAE,CAIf;AA2CD,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,SAAS,YAAY,EAAE,EACjC,MAAM,EAAE,IAAI,GAAG,IAAI,EACnB,QAAQ,GAAE,aAAsC,GAC/C,SAAS,CA0FX"} \ No newline at end of file diff --git a/lib/types/client/preview-dialog.d.ts.map b/lib/types/client/preview-dialog.d.ts.map index 8d52726..da34027 100644 --- a/lib/types/client/preview-dialog.d.ts.map +++ b/lib/types/client/preview-dialog.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"preview-dialog.d.ts","sourceRoot":"","sources":["../../../src/client/preview-dialog.ts"],"names":[],"mappings":"AACA,OAAO,EAAK,KAAK,WAAW,EAAoB,MAAM,WAAW,CAAA;AACjE,OAAO,EAKL,KAAK,aAAa,EAEnB,MAAM,eAAe,CAAA;AAKtB,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;AAEnE,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAS1E;AAOD,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAQ/E;AAED,UAAU,oBAAoB;IAC5B,SAAS,IAAI,WAAW,CAAA;IACxB,gBAAgB,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI,CAAA;IAC/C,SAAS,IAAI,IAAI,CAAA;CAClB;AAED,qBAAa,aAAa;IAgBtB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAhB1B,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAA;IACnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAa;IACnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkB;IACxC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAa;IACrC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAa;IACpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAmB;IAC9C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAmB;IAClD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAkB;IACnD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;IACnD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAS;IAClC,OAAO,CAAC,eAAe,CAAe;IACtC,OAAO,CAAC,IAAI,CAAC,CAAM;IACnB,OAAO,CAAC,SAAS,CAAC,CAAQ;gBAGP,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,oBAAoB;IA6GhD,IAAI,QAAQ,IAAI,aAAa,CAE5B;IAED,+CAA+C;IAC/C,OAAO,CAAC,UAAU;IAiClB,WAAW,CACT,SAAS,EAAE,MAAM,EACjB,eAAe,UAAQ,EACvB,eAAe,UAAQ,GACtB,IAAI;IAwBP,uCAAuC;IACvC,iBAAiB,IAAI,IAAI;IAmBzB,wCAAwC;IAClC,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,SAAS,GAAE,MAAM,OAAoB,GAAG,OAAO,CAAC,OAAO,CAAC;IAmCrF,SAAS,CAAC,eAAe,UAAQ,GAAG,IAAI;IAuBxC,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,SAAS;IAMjB,OAAO,IAAI,IAAI;IAKf,OAAO,CAAC,IAAI;IAMZ,OAAO,CAAC,KAAK;IASb,OAAO,CAAC,YAAY;IAwBpB,OAAO,CAAC,kBAAkB;IAU1B,OAAO,CAAC,WAAW;IAInB,OAAO,CAAC,gBAAgB;YASV,IAAI;IAmBlB,OAAO,CAAC,QAAQ;CAQjB"} \ No newline at end of file +{"version":3,"file":"preview-dialog.d.ts","sourceRoot":"","sources":["../../../src/client/preview-dialog.ts"],"names":[],"mappings":"AAEA,OAAO,EAAK,KAAK,WAAW,EAAoB,MAAM,WAAW,CAAA;AACjE,OAAO,EAKL,KAAK,aAAa,EAEnB,MAAM,eAAe,CAAA;AAKtB,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;AAEnE,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAS1E;AAOD,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAQ/E;AAED,UAAU,oBAAoB;IAC5B,SAAS,IAAI,WAAW,CAAA;IACxB,gBAAgB,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI,CAAA;IAC/C,SAAS,IAAI,IAAI,CAAA;CAClB;AAED,qBAAa,aAAa;IAgBtB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAhB1B,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAA;IACnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAa;IACnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkB;IACxC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAa;IACrC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAa;IACpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAmB;IAC9C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAmB;IAClD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAkB;IACnD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;IACnD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAS;IAClC,OAAO,CAAC,eAAe,CAAe;IACtC,OAAO,CAAC,IAAI,CAAC,CAAM;IACnB,OAAO,CAAC,SAAS,CAAC,CAAQ;gBAGP,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,oBAAoB;IA6GhD,IAAI,QAAQ,IAAI,aAAa,CAE5B;IAED,+CAA+C;IAC/C,OAAO,CAAC,UAAU;IAiClB,WAAW,CACT,SAAS,EAAE,MAAM,EACjB,eAAe,UAAQ,EACvB,eAAe,UAAQ,GACtB,IAAI;IAwBP,uCAAuC;IACvC,iBAAiB,IAAI,IAAI;IAmBzB,wCAAwC;IAClC,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,SAAS,GAAE,MAAM,OAAoB,GAAG,OAAO,CAAC,OAAO,CAAC;IAmCrF,SAAS,CAAC,eAAe,UAAQ,GAAG,IAAI;IAuBxC,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,SAAS;IAMjB,OAAO,IAAI,IAAI;IAKf,OAAO,CAAC,IAAI;IAMZ,OAAO,CAAC,KAAK;IASb,OAAO,CAAC,YAAY;IAwBpB,OAAO,CAAC,kBAAkB;IAU1B,OAAO,CAAC,WAAW;IAInB,OAAO,CAAC,gBAAgB;YASV,IAAI;IAmBlB,OAAO,CAAC,QAAQ;CAQjB"} \ No newline at end of file diff --git a/src/client/background.ts b/src/client/background.ts new file mode 100644 index 0000000..e8dafbc --- /dev/null +++ b/src/client/background.ts @@ -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)) +} diff --git a/src/client/card.ts b/src/client/card.ts index d92ad3d..5a17156 100644 --- a/src/client/card.ts +++ b/src/client/card.ts @@ -1,3 +1,4 @@ +import { shareExportBackground } from './background.ts' import type { ShareMessage } from './content.ts' import { DEFAULT_SHARE_SETTINGS, @@ -261,6 +262,8 @@ export function createShareCard( host.append(card) document.body.append(host) + // 皮肤可能把 --dsw-alias-bg-base 设成 transparent;分享图需要不透明底色。 + card.style.backgroundColor = shareExportBackground(card) return { element: card, diff --git a/src/client/preview-dialog.ts b/src/client/preview-dialog.ts index 866e1da..d175eaa 100644 --- a/src/client/preview-dialog.ts +++ b/src/client/preview-dialog.ts @@ -1,4 +1,5 @@ import { toBlob } from 'html-to-image' +import { shareExportBackground } from './background.ts' import { t, type ShareLocale, type Translation } from './i18n.ts' import { loadShareSettings, @@ -16,7 +17,7 @@ export type ImageRenderer = (element: HTMLElement) => Promise export async function renderShareImage(element: HTMLElement): Promise { const blob = await toBlob(element, { - backgroundColor: getComputedStyle(element).backgroundColor, + backgroundColor: shareExportBackground(element), imagePlaceholder: TRANSPARENT_IMAGE_PLACEHOLDER, pixelRatio: 2, skipFonts: true, diff --git a/tests/background.spec.ts b/tests/background.spec.ts new file mode 100644 index 0000000..c8f9e0c --- /dev/null +++ b/tests/background.spec.ts @@ -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) + }) +}) diff --git a/tests/card.spec.ts b/tests/card.spec.ts index c9a418d..1d5f73e 100644 --- a/tests/card.spec.ts +++ b/tests/card.spec.ts @@ -1,6 +1,7 @@ // @vitest-environment jsdom import { afterEach, describe, expect, it } from 'vitest' +import { SHARE_DARK_BACKGROUND, SHARE_LIGHT_BACKGROUND } from '../src/client/background.ts' import { createShareCard } from '../src/client/card.ts' import type { ShareMessage } from '../src/client/content.ts' @@ -17,6 +18,7 @@ function pair( afterEach(() => { document.body.innerHTML = '' + document.body.removeAttribute('data-ds-dark-theme') }) describe('分享图片卡片', () => { @@ -207,4 +209,20 @@ describe('分享图片卡片', () => { .toBe('rgb(250, 250, 250)') card.dispose() }) + + it('卡片导出底色始终是不透明实色', () => { + const prompt = document.createElement('div') + prompt.textContent = '问题' + const answer = document.createElement('div') + answer.textContent = '回答' + + const light = createShareCard(document, pair(prompt, [answer]), 'zh') + expect(light.element.style.backgroundColor).toBe(SHARE_LIGHT_BACKGROUND) + light.dispose() + + document.body.setAttribute('data-ds-dark-theme', '') + const dark = createShareCard(document, pair(prompt, [answer]), 'zh') + expect(dark.element.style.backgroundColor).toBe(SHARE_DARK_BACKGROUND) + dark.dispose() + }) }) diff --git a/tests/preview-dialog.spec.ts b/tests/preview-dialog.spec.ts new file mode 100644 index 0000000..93c0759 --- /dev/null +++ b/tests/preview-dialog.spec.ts @@ -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, + }) + }) +})