refactor: 收口 BangDream 渲染主题资源

This commit is contained in:
sunlei 2026-06-06 20:47:20 +08:00
parent 9f0293a977
commit 31e0da78cf
19 changed files with 336 additions and 201 deletions

View File

@ -339,6 +339,13 @@ export interface TsuguHook {
- 关键图片命令输出非空,尺寸在既有范围。 - 关键图片命令输出非空,尺寸在既有范围。
- 视觉 token 修改只改 theme/spec 文件。 - 视觉 token 修改只改 theme/spec 文件。
当前进度:
- 已新增 `render-blocks/theme.ts`,先收口公共文字颜色、分割线颜色、简易背景色、图表背景/文字色和默认字体名。
- 已新增 `render-blocks/layout-spec.ts`,统一横向/纵向虚线分割规格,歌曲列表、活动列表、活动详情和通用列表框架不再重复维护分割线宽高/颜色。
- 已新增 `runtime/asset-manifest.ts`,收口本地 `BG`、`Card`、`Skill`、`SongChart`、字体和标题资源路径;`canvas/text.ts`、`canvas/rect.ts`、`canvas/output.ts`、`canvas/background.ts`、`card-art.ts`、`list-rarity.ts`、`skill-text.ts`、`title.ts`、`song-chart-preview.ts` 已改走 manifest。
- 已生成查曲、查活动和查谱面 smoke 图片,验证 theme/spec/manifest 第一段迁移后本地图片输出非空,谱面预览资源路径未断。
### Phase 6策略 policy 和时间/档线规则 ### Phase 6策略 policy 和时间/档线规则
目标:服务器优先级、国服预估、档位、时区和活动状态从工具函数变成可测试规则。 目标:服务器优先级、国服预估、档位、时区和活动状态从工具函数变成可测试规则。

View File

@ -2,9 +2,9 @@ import { createBlurredTrianglePattern } from '@/qqbot/plugins/bangDream/tsugu/ca
import { scatterImages } from '@/qqbot/plugins/bangDream/tsugu/canvas/background-star-scatter'; import { scatterImages } from '@/qqbot/plugins/bangDream/tsugu/canvas/background-star-scatter';
import { drawTextOnCanvas } from '@/qqbot/plugins/bangDream/tsugu/canvas/background-text'; import { drawTextOnCanvas } from '@/qqbot/plugins/bangDream/tsugu/canvas/background-text';
import { loadImage, Image, Canvas } from 'skia-canvas'; import { loadImage, Image, Canvas } from 'skia-canvas';
import { assetsRootPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
import * as path from 'path';
import { loadImageFromPath } from '@/qqbot/plugins/bangDream/tsugu/canvas/image-utils'; import { loadImageFromPath } from '@/qqbot/plugins/bangDream/tsugu/canvas/image-utils';
import { getBangDreamAssetPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/asset-manifest';
import { BANGDREAM_RENDER_THEME } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/theme';
interface BackgroundOptions { interface BackgroundOptions {
image?: Image | Canvas | any; image?: Image | Canvas | any;
@ -118,14 +118,10 @@ let defaultBGTexture: Image;
* Once * Once
*/ */
async function loadImageOnce() { async function loadImageOnce() {
star.push( star.push(await loadImageFromPath(getBangDreamAssetPath('backgroundStar1')));
await loadImageFromPath(path.join(assetsRootPath, '/BG/star1.png')), star.push(await loadImageFromPath(getBangDreamAssetPath('backgroundStar2')));
);
star.push(
await loadImageFromPath(path.join(assetsRootPath, '/BG/star2.png')),
);
defaultBGTexture = await loadImageFromPath( defaultBGTexture = await loadImageFromPath(
path.join(assetsRootPath, '/BG/bg_object_big.png'), getBangDreamAssetPath('backgroundObjectBig'),
); );
} }
loadImageOnce(); loadImageOnce();
@ -136,7 +132,7 @@ loadImageOnce();
* @param options1 - options1参数 * @param options1 - options1参数
*/ */
export async function createEasyBackground({ width, height }) { export async function createEasyBackground({ width, height }) {
const bgColor = '#fef3ef'; const bgColor = BANGDREAM_RENDER_THEME.color.backgroundEasy;
const canvas: Canvas = new Canvas(width, height); const canvas: Canvas = new Canvas(width, height);
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
ctx.fillStyle = bgColor; ctx.fillStyle = bgColor;

View File

@ -4,9 +4,8 @@ import {
createEasyBackground, createEasyBackground,
createImageBackground, createImageBackground,
} from '@/qqbot/plugins/bangDream/tsugu/canvas/background'; } from '@/qqbot/plugins/bangDream/tsugu/canvas/background';
import { assetsRootPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
import * as path from 'path';
import { loadImageFromPath } from '@/qqbot/plugins/bangDream/tsugu/canvas/image-utils'; import { loadImageFromPath } from '@/qqbot/plugins/bangDream/tsugu/canvas/image-utils';
import { getBangDreamAssetPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/asset-manifest';
let BGDefaultImage: Image; let BGDefaultImage: Image;
/** /**
@ -14,7 +13,7 @@ let BGDefaultImage: Image;
*/ */
async function loadImageOnce() { async function loadImageOnce() {
BGDefaultImage = await loadImageFromPath( BGDefaultImage = await loadImageFromPath(
path.join(assetsRootPath, '/BG/live.png'), getBangDreamAssetPath('backgroundLive'),
); );
} }
loadImageOnce(); loadImageOnce();

View File

@ -1,7 +1,9 @@
import { Canvas, FontLibrary } from 'skia-canvas'; import { Canvas, FontLibrary } from 'skia-canvas';
import { assetsRootPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
import { getTextWidth } from '@/qqbot/plugins/bangDream/tsugu/canvas/image-utils'; import { getTextWidth } from '@/qqbot/plugins/bangDream/tsugu/canvas/image-utils';
FontLibrary.use('old', [`${assetsRootPath}/Fonts/old.ttf`]); import { getBangDreamAssetPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/asset-manifest';
import { BANGDREAM_RENDER_THEME } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/theme';
FontLibrary.use('old', [getBangDreamAssetPath('fontOld')]);
interface RoundedRect { interface RoundedRect {
width: number; width: number;
@ -24,7 +26,7 @@ export function drawRoundedRect({
width, width,
height, height,
radius = 25, radius = 25,
color = '#ffffff', color = BANGDREAM_RENDER_THEME.color.surface,
opacity = 0.9, opacity = 0.9,
strokeColor = '#bbbbbb', strokeColor = '#bbbbbb',
strokeWidth = 0, strokeWidth = 0,
@ -119,14 +121,14 @@ interface RoundedRectWithText {
*/ */
export function drawRoundedRectWithText({ export function drawRoundedRectWithText({
text, text,
font = 'old', font = BANGDREAM_RENDER_THEME.font.body,
textColor = '#ffffff', textColor = BANGDREAM_RENDER_THEME.color.surface,
textSize, textSize,
textAlign = 'center', textAlign = 'center',
height = (textSize * 4) / 3, height = (textSize * 4) / 3,
width = getTextWidth(text, textSize, font) + height, width = getTextWidth(text, textSize, font) + height,
radius = height / 2, radius = height / 2,
color = '#5b5b5b', color = BANGDREAM_RENDER_THEME.color.labelBackground,
opacity = 1, opacity = 1,
strokeColor = color, strokeColor = color,
strokeWidth = 0, strokeWidth = 0,
@ -144,7 +146,7 @@ export function drawRoundedRectWithText({
ctx.fillStyle = textColor; ctx.fillStyle = textColor;
ctx.textBaseline = 'alphabetic'; ctx.textBaseline = 'alphabetic';
ctx.font = `${textSize}px old,Microsoft Yahei`; ctx.font = `${textSize}px ${font},${BANGDREAM_RENDER_THEME.font.fallback}`;
let x = 0, let x = 0,
y = 0; y = 0;

View File

@ -4,10 +4,12 @@ import {
Canvas, Canvas,
CanvasRenderingContext2D, CanvasRenderingContext2D,
} from 'skia-canvas'; } from 'skia-canvas';
import { assetsRootPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/config'; import { getBangDreamAssetPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/asset-manifest';
FontLibrary.use('old', [`${assetsRootPath}/Fonts/old.ttf`]); import { BANGDREAM_RENDER_THEME } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/theme';
FontLibrary.use('old', [getBangDreamAssetPath('fontOld')]);
FontLibrary.use('FangZhengHeiTi', [ FontLibrary.use('FangZhengHeiTi', [
`${assetsRootPath}/Fonts/FangZhengHeiTi_GBK.ttf`, getBangDreamAssetPath('fontFangZhengHeiTi'),
]); ]);
interface WrapTextOptions { interface WrapTextOptions {
@ -31,8 +33,8 @@ export function drawText({
textSize = 40, textSize = 40,
maxWidth, maxWidth,
lineHeight = (textSize * 4) / 3, lineHeight = (textSize * 4) / 3,
color = '#505050', color = BANGDREAM_RENDER_THEME.color.primaryText,
font = 'old', font = BANGDREAM_RENDER_THEME.font.body,
}: WrapTextOptions): Canvas { }: WrapTextOptions): Canvas {
const wrappedTextData = wrapText({ text, maxWidth, lineHeight, textSize }); const wrappedTextData = wrapText({ text, maxWidth, lineHeight, textSize });
let canvas: Canvas; let canvas: Canvas;
@ -131,8 +133,8 @@ export function drawTextWithImages({
lineHeight = (textSize * 4) / 3, lineHeight = (textSize * 4) / 3,
content, content,
spacing = textSize / 3, spacing = textSize / 3,
color = '#505050', color = BANGDREAM_RENDER_THEME.color.primaryText,
font = 'old', font = BANGDREAM_RENDER_THEME.font.body,
}: TextWithImagesOptions) { }: TextWithImagesOptions) {
const wrappedTextData = wrapTextWithImages({ const wrappedTextData = wrapTextWithImages({
textSize, textSize,
@ -305,5 +307,5 @@ export const setFontStyle = function (
font: string, font: string,
) { ) {
//设置字体大小 //设置字体大小
ctx.font = textSize + 'px ' + font + ',Microsoft Yahei'; ctx.font = `${textSize}px ${font},${BANGDREAM_RENDER_THEME.font.fallback}`;
}; };

View File

@ -36,18 +36,12 @@ import {
drawSongListInList, drawSongListInList,
} from '@/qqbot/plugins/bangDream/tsugu/render-blocks/list-song'; } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/list-song';
import { drawDottedLine } from '@/qqbot/plugins/bangDream/tsugu/canvas/dotted-line'; import { drawDottedLine } from '@/qqbot/plugins/bangDream/tsugu/canvas/dotted-line';
import { createHorizontalSeparatorSpec } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/layout-spec';
import { BANGDREAM_RENDER_THEME } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/theme';
const songSeparatorLine = drawDottedLine({ const songSeparatorLine = drawDottedLine(
width: 800, createHorizontalSeparatorSpec({ height: 10 }),
height: 10, );
startX: 5,
startY: 5,
endX: 795,
endY: 5,
radius: 2,
gap: 10,
color: '#a8a8a8',
});
/** /**
* QQBot * QQBot
@ -393,7 +387,7 @@ export async function drawEventDetail(
const eventBannerImage = await event.getBannerImage(); const eventBannerImage = await event.getBannerImage();
const eventBannerImageCanvas = drawBannerImageCanvas(eventBannerImage); const eventBannerImageCanvas = drawBannerImageCanvas(eventBannerImage);
list.push(eventBannerImageCanvas); list.push(eventBannerImageCanvas);
list.push(new Canvas(800, 30)); list.push(new Canvas(BANGDREAM_RENDER_THEME.layout.contentWidth, 30));
//标题 //标题
list.push( list.push(

View File

@ -38,22 +38,15 @@ import { statConfig } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/list-s
import { globalDefaultServer } from '@/qqbot/plugins/bangDream/tsugu/runtime/config'; import { globalDefaultServer } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
import { createTsuguEntityMatcher } from '@/qqbot/plugins/bangDream/tsugu/search/entity-list-matcher'; import { createTsuguEntityMatcher } from '@/qqbot/plugins/bangDream/tsugu/search/entity-list-matcher';
import { eventRepository } from '@/qqbot/plugins/bangDream/tsugu/models/event-repository'; import { eventRepository } from '@/qqbot/plugins/bangDream/tsugu/models/event-repository';
import { createVerticalSeparatorSpec } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/layout-spec';
const maxHeight = 7000; const maxHeight = 7000;
const maxColumns = 7; const maxColumns = 7;
//表格用默认虚线 //表格用默认虚线
export const line2: Canvas = drawDottedLine({ export const line2: Canvas = drawDottedLine(
width: 30, createVerticalSeparatorSpec(7000, { startX: 5 }),
height: 7000, );
startX: 5,
startY: 0,
endX: 15,
endY: 6995,
radius: 2,
gap: 10,
color: '#a8a8a8',
});
/** /**
* QQBot * QQBot

View File

@ -15,32 +15,16 @@ import { globalDefaultServer } from '@/qqbot/plugins/bangDream/tsugu/runtime/con
import { drawSongDetail } from './song-detail'; import { drawSongDetail } from './song-detail';
import { createTsuguEntityMatcher } from '@/qqbot/plugins/bangDream/tsugu/search/entity-list-matcher'; import { createTsuguEntityMatcher } from '@/qqbot/plugins/bangDream/tsugu/search/entity-list-matcher';
import { songRepository } from '@/qqbot/plugins/bangDream/tsugu/models/song-repository'; import { songRepository } from '@/qqbot/plugins/bangDream/tsugu/models/song-repository';
import {
createHorizontalSeparatorSpec,
createVerticalSeparatorSpec,
} from '@/qqbot/plugins/bangDream/tsugu/render-blocks/layout-spec';
// 紧凑化虚线分割 // 紧凑化虚线分割
const line = drawDottedLine({ const line = drawDottedLine(createHorizontalSeparatorSpec({ height: 10 }));
width: 800,
height: 10,
startX: 5,
startY: 5,
endX: 795,
endY: 5,
radius: 2,
gap: 10,
color: '#a8a8a8',
});
//表格用默认竖向虚线 //表格用默认竖向虚线
const line2: Canvas = drawDottedLine({ const line2: Canvas = drawDottedLine(createVerticalSeparatorSpec(6000));
width: 30,
height: 6000,
startX: 10,
startY: 0,
endX: 15,
endY: 5990,
radius: 2,
gap: 10,
color: '#a8a8a8',
});
/** /**
* QQBot * QQBot

View File

@ -1,4 +1,3 @@
import { assetsRootPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
import { setFontStyle } from '@/qqbot/plugins/bangDream/tsugu/canvas/text'; import { setFontStyle } from '@/qqbot/plugins/bangDream/tsugu/canvas/text';
import { Band } from '@/qqbot/plugins/bangDream/tsugu/models/band'; import { Band } from '@/qqbot/plugins/bangDream/tsugu/models/band';
import { Attribute } from '@/qqbot/plugins/bangDream/tsugu/models/attribute'; import { Attribute } from '@/qqbot/plugins/bangDream/tsugu/models/attribute';
@ -6,10 +5,11 @@ import { Card } from '@/qqbot/plugins/bangDream/tsugu/models/card';
import { Image, Canvas, loadImage } from 'skia-canvas'; import { Image, Canvas, loadImage } from 'skia-canvas';
import { downloadFileCache } from '@/qqbot/plugins/bangDream/tsugu/data-clients/asset-cache-client'; import { downloadFileCache } from '@/qqbot/plugins/bangDream/tsugu/data-clients/asset-cache-client';
import { drawCardIconSkill } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/skill-text'; import { drawCardIconSkill } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/skill-text';
import * as path from 'path';
import { Skill } from '@/qqbot/plugins/bangDream/tsugu/models/skill'; import { Skill } from '@/qqbot/plugins/bangDream/tsugu/models/skill';
import { bestdoriUrl } from '@/qqbot/plugins/bangDream/tsugu/runtime/config'; import { bestdoriUrl } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
import { loadImageFromPath } from '@/qqbot/plugins/bangDream/tsugu/canvas/image-utils'; import { loadImageFromPath } from '@/qqbot/plugins/bangDream/tsugu/canvas/image-utils';
import { getBangDreamAssetPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/asset-manifest';
import { BANGDREAM_RENDER_THEME } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/theme';
const cardTypeIconList: { [type: string]: Image } = {}; const cardTypeIconList: { [type: string]: Image } = {};
const starList: { [type: string]: Image } = {}; const starList: { [type: string]: Image } = {};
@ -20,25 +20,23 @@ let limitBreakIcon: Image;
*/ */
async function loadImageOnce() { async function loadImageOnce() {
cardTypeIconList.limited = await loadImageFromPath( cardTypeIconList.limited = await loadImageFromPath(
path.join(assetsRootPath, '/Card/L.png'), getBangDreamAssetPath('cardLimited'),
); );
cardTypeIconList.dreamfes = await loadImageFromPath( cardTypeIconList.dreamfes = await loadImageFromPath(
path.join(assetsRootPath, '/Card/D.png'), getBangDreamAssetPath('cardDreamfes'),
); );
cardTypeIconList.kirafes = await loadImageFromPath( cardTypeIconList.kirafes = await loadImageFromPath(
path.join(assetsRootPath, '/Card/K.png'), getBangDreamAssetPath('cardKirafes'),
); );
cardTypeIconList.birthday = await loadImageFromPath( cardTypeIconList.birthday = await loadImageFromPath(
path.join(assetsRootPath, '/Card/B.png'), getBangDreamAssetPath('cardBirthday'),
);
starList.normal = await loadImageFromPath(
path.join(assetsRootPath, '/Card/star.png'),
); );
starList.normal = await loadImageFromPath(getBangDreamAssetPath('cardStar'));
starList.trained = await loadImageFromPath( starList.trained = await loadImageFromPath(
path.join(assetsRootPath, '/Card/star_trained.png'), getBangDreamAssetPath('cardStarTrained'),
); );
limitBreakIcon = await loadImageFromPath( limitBreakIcon = await loadImageFromPath(
path.join(assetsRootPath, '/Card/limitBreakRank.png'), getBangDreamAssetPath('cardLimitBreakRank'),
); );
} }
@ -136,17 +134,17 @@ export async function drawCardIcon({
if (cardIdVisible) { if (cardIdVisible) {
ctx.textAlign = 'start'; ctx.textAlign = 'start';
ctx.textBaseline = 'middle'; ctx.textBaseline = 'middle';
setFontStyle(ctx, 30, 'old'); setFontStyle(ctx, 30, BANGDREAM_RENDER_THEME.font.body);
ctx.fillStyle = '#a7a7a7'; ctx.fillStyle = BANGDREAM_RENDER_THEME.color.mutedText;
ctx.fillText(`ID:${card.cardId}`, 4, 195); ctx.fillText(`ID:${card.cardId}`, 4, 195);
} }
//如果显示技能类型,在右上显示 //如果显示技能类型,在右上显示
if (skillLevel != undefined) { if (skillLevel != undefined) {
ctx.fillStyle = '#ff0000'; ctx.fillStyle = BANGDREAM_RENDER_THEME.color.skillLevelBackground;
ctx.fillRect(138, 91, 35, 39); ctx.fillRect(138, 91, 35, 39);
ctx.fillStyle = '#ffffff'; ctx.fillStyle = BANGDREAM_RENDER_THEME.color.surface;
ctx.textAlign = 'center'; ctx.textAlign = 'center';
setFontStyle(ctx, 35, 'old'); setFontStyle(ctx, 35, BANGDREAM_RENDER_THEME.font.body);
ctx.fillText(skillLevel.toString(), 155.5, 107.5); ctx.fillText(skillLevel.toString(), 155.5, 107.5);
} }
//如果显示技能类型,在右上显示 //如果显示技能类型,在右上显示
@ -169,8 +167,8 @@ export async function drawCardIcon({
ctx.drawImage(bandIcon, 0, 0, 45, 45); ctx.drawImage(bandIcon, 0, 0, 45, 45);
if (limitBreakRank != 0) { if (limitBreakRank != 0) {
ctx.drawImage(limitBreakIcon, 137, 51, 39, 39); ctx.drawImage(limitBreakIcon, 137, 51, 39, 39);
setFontStyle(ctx, 25, 'old'); setFontStyle(ctx, 25, BANGDREAM_RENDER_THEME.font.body);
ctx.fillStyle = '#ffffff'; ctx.fillStyle = BANGDREAM_RENDER_THEME.color.surface;
ctx.textAlign = 'center'; ctx.textAlign = 'center';
ctx.textBaseline = 'middle'; ctx.textBaseline = 'middle';
ctx.fillText(limitBreakRank.toString(), 155, 70); ctx.fillText(limitBreakRank.toString(), 155, 70);

View File

@ -47,30 +47,22 @@ import {
} from '@/qqbot/plugins/bangDream/tsugu/runtime/config'; } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
import { Canvas, Image } from 'skia-canvas'; import { Canvas, Image } from 'skia-canvas';
import { Band } from '@/qqbot/plugins/bangDream/tsugu/models/band'; import { Band } from '@/qqbot/plugins/bangDream/tsugu/models/band';
import { BANGDREAM_RENDER_THEME } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/theme';
import { createHorizontalSeparatorSpec } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/layout-spec';
const songDetailSeparator = drawDottedLine({ const songDetailSeparator = drawDottedLine(
width: 365, createHorizontalSeparatorSpec({
height: 20, width: 365,
startX: 5, height: 20,
startY: 10, endX: 360,
endX: 360, }),
endY: 10, );
radius: 2,
gap: 10,
color: '#a8a8a8',
});
const songMetaSeparator = drawDottedLine({ const songMetaSeparator = drawDottedLine(
width: 800, createHorizontalSeparatorSpec({
height: 10, height: 10,
startX: 5, }),
startY: 5, );
endX: 795,
endY: 5,
radius: 2,
gap: 10,
color: '#a8a8a8',
});
/** /**
* Info块 * Info块
@ -327,7 +319,7 @@ export async function drawCharacterHalfBlock(
await character.initFull(false); await character.initFull(false);
const color = character.colorCode const color = character.colorCode
? character.colorCode.toLowerCase() ? character.colorCode.toLowerCase()
: '#ffffff'; : BANGDREAM_RENDER_THEME.color.surface;
ctx.drawImage( ctx.drawImage(
drawRoundedRect({ drawRoundedRect({
width, width,
@ -382,14 +374,14 @@ export async function drawCharacterHalfBlock(
const nameTextImage = drawText({ const nameTextImage = drawText({
text: character.characterName[server], text: character.characterName[server],
textSize: 40, textSize: 40,
color: '#ffffff', color: BANGDREAM_RENDER_THEME.color.surface,
maxWidth: width, maxWidth: width,
}); });
list.push(drawImageListCenter([nameTextImage], width)); list.push(drawImageListCenter([nameTextImage], width));
const idTextImage = drawText({ const idTextImage = drawText({
text: `ID: ${character.characterId}`, text: `ID: ${character.characterId}`,
textSize: 30, textSize: 30,
color: '#ffffff', color: BANGDREAM_RENDER_THEME.color.surface,
maxWidth: width, maxWidth: width,
}); });
list.push(drawImageListCenter([idTextImage], width)); list.push(drawImageListCenter([idTextImage], width));
@ -409,13 +401,13 @@ export async function drawPlayerDetailBlockWithIllustration(
const list: Array<Canvas | Image> = []; const list: Array<Canvas | Image> = [];
const playerText = drawText({ const playerText = drawText({
text: player.profile.userName, text: player.profile.userName,
maxWidth: 800, maxWidth: BANGDREAM_RENDER_THEME.layout.contentWidth,
textSize: 75, textSize: 75,
}); });
list.push(drawImageListCenter([playerText])); list.push(drawImageListCenter([playerText]));
const levelText = drawText({ const levelText = drawText({
text: `等级 ${player.profile.rank}`, text: `等级 ${player.profile.rank}`,
maxWidth: 800, maxWidth: BANGDREAM_RENDER_THEME.layout.contentWidth,
textSize: 35, textSize: 35,
}); });
list.push(drawImageListCenter([levelText])); list.push(drawImageListCenter([levelText]));
@ -436,7 +428,7 @@ export async function drawPlayerDetailBlockWithIllustration(
const introductionText = drawText({ const introductionText = drawText({
text: player.profile.introduction, text: player.profile.introduction,
maxWidth: 800, maxWidth: BANGDREAM_RENDER_THEME.layout.contentWidth,
textSize: 35, textSize: 35,
}); });
list.push(drawImageListCenter([introductionText])); list.push(drawImageListCenter([introductionText]));
@ -447,7 +439,7 @@ export async function drawPlayerDetailBlockWithIllustration(
: 'ID未公开'; : 'ID未公开';
const idText = drawTextWithImages({ const idText = drawTextWithImages({
content: [await getIcon(player.server), userId], content: [await getIcon(player.server), userId],
maxWidth: 800, maxWidth: BANGDREAM_RENDER_THEME.layout.contentWidth,
textSize: 35, textSize: 35,
}); });
list.push(drawImageListCenter([idText])); list.push(drawImageListCenter([idText]));

View File

@ -0,0 +1,57 @@
import { BANGDREAM_RENDER_THEME } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/theme';
interface SeparatorSpecOptions {
width?: number;
height?: number;
startX?: number;
endX?: number;
}
/**
* 线
*
* @param options - 线
*/
export function createHorizontalSeparatorSpec({
width = BANGDREAM_RENDER_THEME.layout.contentWidth,
height = 30,
startX = 5,
endX = width - 5,
}: SeparatorSpecOptions = {}) {
const y = height / 2;
return {
width,
height,
startX,
startY: y,
endX,
endY: y,
radius: 2,
gap: 10,
color: BANGDREAM_RENDER_THEME.color.separator,
};
}
/**
* 线
*
* @param height - 线
* @param options -
*/
export function createVerticalSeparatorSpec(
height: number,
options: Pick<SeparatorSpecOptions, 'startX' | 'endX'> = {},
) {
const { startX = 10, endX = 15 } = options;
return {
width: 30,
height,
startX,
startY: 0,
endX,
endY: height - 10,
radius: 2,
gap: 10,
color: BANGDREAM_RENDER_THEME.color.separator,
};
}

View File

@ -12,19 +12,14 @@ import {
} from '@/qqbot/plugins/bangDream/tsugu/models/server'; } from '@/qqbot/plugins/bangDream/tsugu/models/server';
import { stackImageHorizontal } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/image-stack'; import { stackImageHorizontal } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/image-stack';
import { globalDefaultServer } from '@/qqbot/plugins/bangDream/tsugu/runtime/config'; import { globalDefaultServer } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
import { BANGDREAM_RENDER_THEME } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/theme';
import {
createHorizontalSeparatorSpec,
createVerticalSeparatorSpec,
} from '@/qqbot/plugins/bangDream/tsugu/render-blocks/layout-spec';
//表格用默认虚线 //表格用默认虚线
export const line: Canvas = drawDottedLine({ export const line: Canvas = drawDottedLine(createHorizontalSeparatorSpec());
width: 800,
height: 30,
startX: 5,
startY: 15,
endX: 795,
endY: 15,
radius: 2,
gap: 10,
color: '#a8a8a8',
});
interface ListOptions { interface ListOptions {
key?: string; key?: string;
@ -51,8 +46,8 @@ export function drawList({
textSize = 40, textSize = 40,
lineHeight = textSize * 1.5, lineHeight = textSize * 1.5,
spacing = textSize / 3, spacing = textSize / 3,
color = '#505050', color = BANGDREAM_RENDER_THEME.color.primaryText,
maxWidth = 800, maxWidth = BANGDREAM_RENDER_THEME.layout.contentWidth,
}: ListOptions): Canvas { }: ListOptions): Canvas {
const xmax = maxWidth - 40; const xmax = maxWidth - 40;
const keyImage = drawRoundedRectWithText({ const keyImage = drawRoundedRectWithText({
@ -76,14 +71,21 @@ export function drawList({
textImage = new Canvas(0, 0); textImage = new Canvas(0, 0);
} }
if (key == undefined) { if (key == undefined) {
return stackImageHorizontal([new Canvas(20, 1), textImage]); return stackImageHorizontal([
new Canvas(BANGDREAM_RENDER_THEME.layout.listIndent, 1),
textImage,
]);
} }
const ymax = textImage.height + keyImage.height + 10; const ymax = textImage.height + keyImage.height + 10;
const canvas = new Canvas(maxWidth, ymax); const canvas = new Canvas(maxWidth, ymax);
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
ctx.drawImage(keyImage, 0, 0); ctx.drawImage(keyImage, 0, 0);
if (textImage.height != 0) { if (textImage.height != 0) {
ctx.drawImage(textImage, 20, keyImage.height + 10); ctx.drawImage(
textImage,
BANGDREAM_RENDER_THEME.layout.listIndent,
keyImage.height + 10,
);
} }
return canvas; return canvas;
} }
@ -107,7 +109,9 @@ export function drawTipsInList({
lineHeight = textSize * 1.5, lineHeight = textSize * 1.5,
spacing = textSize / 3, spacing = textSize / 3,
}: tipsOptions) { }: tipsOptions) {
const xmax = 760; const xmax =
BANGDREAM_RENDER_THEME.layout.contentWidth -
BANGDREAM_RENDER_THEME.layout.listIndent * 2;
let textImage: Canvas; let textImage: Canvas;
if (typeof text == 'string') { if (typeof text == 'string') {
textImage = drawText({ text, textSize, maxWidth: xmax, lineHeight }); textImage = drawText({ text, textSize, maxWidth: xmax, lineHeight });
@ -122,11 +126,19 @@ export function drawTipsInList({
} else { } else {
textImage = new Canvas(1, 1); textImage = new Canvas(1, 1);
} }
const canvas = new Canvas(800, textImage.height + 10); const canvas = new Canvas(
BANGDREAM_RENDER_THEME.layout.contentWidth,
textImage.height + 10,
);
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
ctx.fillStyle = '#f1f1f1'; ctx.fillStyle = BANGDREAM_RENDER_THEME.color.subtlePanel;
ctx.fillRect(0, 10, 800, textImage.height); ctx.fillRect(
ctx.drawImage(textImage, 20, 10); 0,
10,
BANGDREAM_RENDER_THEME.layout.contentWidth,
textImage.height,
);
ctx.drawImage(textImage, BANGDREAM_RENDER_THEME.layout.listIndent, 10);
return canvas; return canvas;
} }
@ -142,7 +154,7 @@ export async function drawListByServerList(
content: Array<string | null>, content: Array<string | null>,
key?: string, key?: string,
serverList: Server[] = globalDefaultServer, serverList: Server[] = globalDefaultServer,
maxWidth = 800, maxWidth: number = BANGDREAM_RENDER_THEME.layout.contentWidth,
) { ) {
const tempcontent: Array<string | Image | Canvas> = []; const tempcontent: Array<string | Image | Canvas> = [];
@ -211,13 +223,16 @@ export function drawListMerge(imageList: Array<Canvas | Image>): Canvas {
maxHeight = element.height; maxHeight = element.height;
} }
} }
const canvas = new Canvas(800, maxHeight); const canvas = new Canvas(
BANGDREAM_RENDER_THEME.layout.contentWidth,
maxHeight,
);
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
let x = 0; let x = 0;
for (let i = 0; i < imageList.length; i++) { for (let i = 0; i < imageList.length; i++) {
const element = imageList[i]; const element = imageList[i];
ctx.drawImage(element, x, 0); ctx.drawImage(element, x, 0);
x += 800 / imageList.length; x += BANGDREAM_RENDER_THEME.layout.contentWidth / imageList.length;
} }
return canvas; return canvas;
} }
@ -232,7 +247,7 @@ export function drawListMerge(imageList: Array<Canvas | Image>): Canvas {
*/ */
export function drawImageListCenter( export function drawImageListCenter(
imageList: Array<Canvas | Image>, imageList: Array<Canvas | Image>,
maxWidth = 800, maxWidth: number = BANGDREAM_RENDER_THEME.layout.contentWidth,
): Canvas { ): Canvas {
interface imageLine { interface imageLine {
imageList: Array<Canvas | Image>; imageList: Array<Canvas | Image>;
@ -320,10 +335,14 @@ export function drawListWithLine(textImageList: Array<Canvas | Image>): Canvas {
const element = textImageList[i]; const element = textImageList[i];
height += element.height; height += element.height;
} }
const canvas = new Canvas(800, height + 10); const canvas = new Canvas(
BANGDREAM_RENDER_THEME.layout.contentWidth,
height + 10,
);
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
ctx.fillStyle = '#a8a8a8'; ctx.fillStyle = BANGDREAM_RENDER_THEME.color.separator;
ctx.fillRect(10, 10, 5, height + 20); const lineSpec = createVerticalSeparatorSpec(height + 20);
ctx.fillRect(lineSpec.startX, 10, 5, height + 20);
for (let i = 0; i < textImageList.length; i++) { for (let i = 0; i < textImageList.length; i++) {
const element = textImageList[i]; const element = textImageList[i];
ctx.drawImage(element, x, y); ctx.drawImage(element, x, y);

View File

@ -1,8 +1,7 @@
import { Canvas, Image } from 'skia-canvas'; import { Canvas, Image } from 'skia-canvas';
import { drawList } from './list-frame'; import { drawList } from './list-frame';
import { assetsRootPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
import * as path from 'path';
import { loadImageFromPath } from '@/qqbot/plugins/bangDream/tsugu/canvas/image-utils'; import { loadImageFromPath } from '@/qqbot/plugins/bangDream/tsugu/canvas/image-utils';
import { getBangDreamAssetPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/asset-manifest';
interface RarityInListOptions { interface RarityInListOptions {
key?: string; key?: string;
@ -16,11 +15,9 @@ export const starList: { [type: string]: Image } = {};
* Once * Once
*/ */
async function loadImageOnce() { async function loadImageOnce() {
starList.normal = await loadImageFromPath( starList.normal = await loadImageFromPath(getBangDreamAssetPath('cardStar'));
path.join(assetsRootPath, '/Card/star.png'),
);
starList.trained = await loadImageFromPath( starList.trained = await loadImageFromPath(
path.join(assetsRootPath, '/Card/star_trained.png'), getBangDreamAssetPath('cardStarTrained'),
); );
} }
loadImageOnce(); loadImageOnce();

View File

@ -11,6 +11,8 @@ import { drawDifficultyList, drawDifficulty } from './list-difficulty';
import { globalDefaultServer } from '@/qqbot/plugins/bangDream/tsugu/runtime/config'; import { globalDefaultServer } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
import { drawList } from './list-frame'; import { drawList } from './list-frame';
import { drawDottedLine } from '@/qqbot/plugins/bangDream/tsugu/canvas/dotted-line'; import { drawDottedLine } from '@/qqbot/plugins/bangDream/tsugu/canvas/dotted-line';
import { BANGDREAM_RENDER_THEME } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/theme';
import { createHorizontalSeparatorSpec } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/layout-spec';
/** /**
* In列表 * In列表
@ -34,7 +36,7 @@ export async function drawSongInList(
heightMax: 80, heightMax: 80,
}); });
const canvas = new Canvas(800, 75); const canvas = new Canvas(BANGDREAM_RENDER_THEME.layout.contentWidth, 75);
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
ctx.drawImage(songImage, 50, 5, 65, 65); ctx.drawImage(songImage, 50, 5, 65, 65);
//id //id
@ -42,7 +44,7 @@ export async function drawSongInList(
text: song.songId.toString(), text: song.songId.toString(),
textSize: 23, textSize: 23,
lineHeight: 37.5, lineHeight: 37.5,
maxWidth: 800, maxWidth: BANGDREAM_RENDER_THEME.layout.contentWidth,
}); });
ctx.drawImage(idImage, 0, 0); ctx.drawImage(idImage, 0, 0);
//曲名与乐队名 //曲名与乐队名
@ -58,7 +60,7 @@ export async function drawSongInList(
text: fullText, text: fullText,
textSize: 23, textSize: 23,
lineHeight: 37.5, lineHeight: 37.5,
maxWidth: 800, maxWidth: BANGDREAM_RENDER_THEME.layout.contentWidth,
}); });
ctx.drawImage(textImage, 120, 0); ctx.drawImage(textImage, 120, 0);
@ -69,7 +71,7 @@ export async function drawSongInList(
: drawDifficulty(difficulty, song.difficulty[difficulty].playLevel, 45); : drawDifficulty(difficulty, song.difficulty[difficulty].playLevel, 45);
ctx.drawImage( ctx.drawImage(
difficultyImage, difficultyImage,
800 - difficultyImage.width, BANGDREAM_RENDER_THEME.layout.contentWidth - difficultyImage.width,
75 / 2 - difficultyImage.height / 2, 75 / 2 - difficultyImage.height / 2,
); );
return canvas; return canvas;
@ -96,17 +98,7 @@ export async function drawSongListInList(
const x = 0; const x = 0;
let y = 0; let y = 0;
const views: Canvas[] = []; const views: Canvas[] = [];
const line = drawDottedLine({ const line = drawDottedLine(createHorizontalSeparatorSpec({ height: 10 }));
width: 800,
height: 10,
startX: 5,
startY: 5,
endX: 795,
endY: 5,
radius: 2,
gap: 10,
color: '#a8a8a8',
});
for (let i = 0; i < songs.length; i++) { for (let i = 0; i < songs.length; i++) {
views.push( views.push(
resizeImage({ resizeImage({

View File

@ -1,23 +1,21 @@
import { Skill } from '@/qqbot/plugins/bangDream/tsugu/models/skill'; import { Skill } from '@/qqbot/plugins/bangDream/tsugu/models/skill';
import { Image, Canvas } from 'skia-canvas'; import { Image, Canvas } from 'skia-canvas';
import { assetsRootPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
import { drawTextWithImages } from '@/qqbot/plugins/bangDream/tsugu/canvas/text'; import { drawTextWithImages } from '@/qqbot/plugins/bangDream/tsugu/canvas/text';
import * as path from 'path';
import { loadImageFromPath } from '@/qqbot/plugins/bangDream/tsugu/canvas/image-utils'; import { loadImageFromPath } from '@/qqbot/plugins/bangDream/tsugu/canvas/image-utils';
import { getBangDreamAssetPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/asset-manifest';
import { BANGDREAM_RENDER_THEME } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/theme';
const skillIcon: { [skillType: string]: Image } = {}; const skillIcon: { [skillType: string]: Image } = {};
/** /**
* Once * Once
*/ */
async function loadImageOnce() { async function loadImageOnce() {
skillIcon.life = await loadImageFromPath( skillIcon.life = await loadImageFromPath(getBangDreamAssetPath('skillLife'));
path.join(assetsRootPath, '/Skill/life.png'),
);
skillIcon.judge = await loadImageFromPath( skillIcon.judge = await loadImageFromPath(
path.join(assetsRootPath, '/Skill/judge.png'), getBangDreamAssetPath('skillJudge'),
); );
skillIcon.damage = await loadImageFromPath( skillIcon.damage = await loadImageFromPath(
path.join(assetsRootPath, '/Skill/damage.png'), getBangDreamAssetPath('skillDamage'),
); );
} }
loadImageOnce(); loadImageOnce();
@ -70,11 +68,11 @@ export async function drawCardIconSkill(skill: Skill): Promise<Canvas> {
textSize: 27, textSize: 27,
lineHeight: 30, lineHeight: 30,
spacing: 3, spacing: 3,
color: '#ffffff', color: BANGDREAM_RENDER_THEME.color.surface,
font: 'old', font: BANGDREAM_RENDER_THEME.font.body,
}); });
const textbase = await loadImageFromPath( const textbase = await loadImageFromPath(
path.join(assetsRootPath, '/Card/text.png'), getBangDreamAssetPath('cardSkillTextBase'),
); );
const canvas = new Canvas(stringWithImage.width + 15, 45); const canvas = new Canvas(stringWithImage.width + 15, 45);
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');

View File

@ -1,6 +1,10 @@
import { Canvas, Image, loadImage } from 'skia-canvas'; import { Canvas, Image, loadImage } from 'skia-canvas';
import { assetsRootPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
import { loadImageFromPath } from '@/qqbot/plugins/bangDream/tsugu/canvas/image-utils'; import { loadImageFromPath } from '@/qqbot/plugins/bangDream/tsugu/canvas/image-utils';
import {
BangDreamLocalAssetKey,
getBangDreamAssetPath,
} from '@/qqbot/plugins/bangDream/tsugu/runtime/asset-manifest';
import { BANGDREAM_RENDER_THEME } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/theme';
interface BestdoriPreviewPayload { interface BestdoriPreviewPayload {
id: number; id: number;
@ -70,6 +74,23 @@ const NOTE_IMAGE_KEYS = [
'RightArrow', 'RightArrow',
'RightArrowEnd', 'RightArrowEnd',
] as const; ] as const;
const NOTE_IMAGE_ASSET_KEYS: Record<
(typeof NOTE_IMAGE_KEYS)[number],
BangDreamLocalAssetKey
> = {
Flick: 'songChartNoteFlick',
FlickTop: 'songChartNoteFlickTop',
LeftArrow: 'songChartNoteLeftArrow',
LeftArrowEnd: 'songChartNoteLeftArrowEnd',
Long: 'songChartNoteLong',
RightArrow: 'songChartNoteRightArrow',
RightArrowEnd: 'songChartNoteRightArrowEnd',
Sim: 'songChartNoteSim',
Single: 'songChartNoteSingle',
SingleOff: 'songChartNoteSingleOff',
Skill: 'songChartNoteSkill',
Tick: 'songChartNoteTick',
};
const DISPLAY_NOTE_TYPES = [ const DISPLAY_NOTE_TYPES = [
'Single', 'Single',
'SingleOff', 'SingleOff',
@ -451,7 +472,9 @@ async function loadNoteImages(): Promise<Record<string, Image>> {
const entries = await Promise.all( const entries = await Promise.all(
NOTE_IMAGE_KEYS.map(async (key) => [ NOTE_IMAGE_KEYS.map(async (key) => [
key, key,
await loadImageFromPath(`${assetsRootPath}/SongChart/note/${key}.png`), await loadImageFromPath(
getBangDreamAssetPath(NOTE_IMAGE_ASSET_KEYS[key]),
),
]), ]),
); );
return Object.fromEntries(entries); return Object.fromEntries(entries);
@ -467,7 +490,7 @@ async function loadCoverImage(cover: string | Buffer): Promise<Image> {
try { try {
return await loadImage(cover); return await loadImage(cover);
} catch { } catch {
return await loadImageFromPath(`${assetsRootPath}/SongChart/jacket.png`); return await loadImageFromPath(getBangDreamAssetPath('songChartJacket'));
} }
} }
@ -526,7 +549,7 @@ function drawBaseInfo(
): void { ): void {
const { id, diff, level } = payload; const { id, diff, level } = payload;
ctx.save(); ctx.save();
ctx.fillStyle = '#000'; ctx.fillStyle = BANGDREAM_RENDER_THEME.color.chartBackground;
ctx.fillRect(0, 0, layout.width, layout.height); ctx.fillRect(0, 0, layout.width, layout.height);
ctx.restore(); ctx.restore();
@ -539,10 +562,10 @@ function drawBaseInfo(
); );
ctx.save(); ctx.save();
ctx.fillStyle = '#1f1e33'; ctx.fillStyle = BANGDREAM_RENDER_THEME.color.chartPanel;
ctx.fillRect(OFFSET - 8, OFFSET - 8, 128, 24); ctx.fillRect(OFFSET - 8, OFFSET - 8, 128, 24);
ctx.fillStyle = '#FFF'; ctx.fillStyle = BANGDREAM_RENDER_THEME.color.chartText;
ctx.font = '16px "Arial"'; ctx.font = `16px "${BANGDREAM_RENDER_THEME.font.chart}"`;
ctx.textAlign = 'center'; ctx.textAlign = 'center';
ctx.textBaseline = 'middle'; ctx.textBaseline = 'middle';
ctx.fillText(`${id}`, OFFSET + 56, OFFSET + 4, 128); ctx.fillText(`${id}`, OFFSET + 56, OFFSET + 4, 128);
@ -550,10 +573,12 @@ function drawBaseInfo(
const coverWidth = layout.infoAreaWidth - 16; const coverWidth = layout.infoAreaWidth - 16;
ctx.save(); ctx.save();
ctx.fillStyle = DIFFICULTY_COLOR_LIST[diff] ?? '#777'; ctx.fillStyle =
DIFFICULTY_COLOR_LIST[diff] ??
BANGDREAM_RENDER_THEME.color.chartDifficultyFallback;
ctx.fillRect(8 + coverWidth - 116, 8 + coverWidth - 12, 128, 24); ctx.fillRect(8 + coverWidth - 116, 8 + coverWidth - 12, 128, 24);
ctx.fillStyle = '#FFF'; ctx.fillStyle = BANGDREAM_RENDER_THEME.color.chartText;
ctx.font = '16px "Arial"'; ctx.font = `16px "${BANGDREAM_RENDER_THEME.font.chart}"`;
ctx.textAlign = 'center'; ctx.textAlign = 'center';
ctx.textBaseline = 'middle'; ctx.textBaseline = 'middle';
ctx.fillText(`${diff} ${level}`, 8 + coverWidth - 52, 8 + coverWidth, 128); ctx.fillText(`${diff} ${level}`, 8 + coverWidth - 52, 8 + coverWidth, 128);
@ -657,8 +682,8 @@ function drawBeatLines(
*/ */
function drawTimeline(ctx: any, layout: PreviewLayout): void { function drawTimeline(ctx: any, layout: PreviewLayout): void {
ctx.save(); ctx.save();
ctx.font = '18px "Arial"'; ctx.font = `18px "${BANGDREAM_RENDER_THEME.font.chart}"`;
ctx.fillStyle = '#FFF'; ctx.fillStyle = BANGDREAM_RENDER_THEME.color.chartText;
ctx.textAlign = 'right'; ctx.textAlign = 'right';
for (let i = 0; i <= layout.chartLength; i += 5) { for (let i = 0; i <= layout.chartLength; i += 5) {
const { x, y } = getTimePosition(layout, i); const { x, y } = getTimePosition(layout, i);
@ -692,20 +717,20 @@ function drawCountAndBpmLines(
if (count % 50 !== 0) { if (count % 50 !== 0) {
continue; continue;
} }
ctx.font = '18px "Arial"'; ctx.font = `18px "${BANGDREAM_RENDER_THEME.font.chart}"`;
ctx.fillStyle = 'rgba(128, 128, 128, 0.5)'; ctx.fillStyle = 'rgba(128, 128, 128, 0.5)';
ctx.textAlign = 'left'; ctx.textAlign = 'left';
ctx.fillRect(x, y - 1, w, 2); ctx.fillRect(x, y - 1, w, 2);
ctx.fillStyle = '#FFF'; ctx.fillStyle = BANGDREAM_RENDER_THEME.color.chartText;
setAdaptiveTextBaseline(ctx, layout, 18, y); setAdaptiveTextBaseline(ctx, layout, 18, y);
ctx.fillText(`${count}`, x + w + 8, y); ctx.fillText(`${count}`, x + w + 8, y);
continue; continue;
} }
if (note.type === 'BPM') { if (note.type === 'BPM') {
ctx.fillStyle = '#C34FBB'; ctx.fillStyle = BANGDREAM_RENDER_THEME.color.chartBpm;
ctx.fillRect(x, y - 1, w, 2); ctx.fillRect(x, y - 1, w, 2);
ctx.font = '18px "Arial"'; ctx.font = `18px "${BANGDREAM_RENDER_THEME.font.chart}"`;
ctx.textAlign = 'left'; ctx.textAlign = 'left';
setAdaptiveTextBaseline(ctx, layout, 18, y); setAdaptiveTextBaseline(ctx, layout, 18, y);
ctx.fillText(`${note.bpm}`, x + w + 8, y); ctx.fillText(`${note.bpm}`, x + w + 8, y);

View File

@ -0,0 +1,31 @@
export const BANGDREAM_RENDER_THEME = {
color: {
backgroundEasy: '#fef3ef',
chartBackground: '#000',
chartBpm: '#C34FBB',
chartDifficultyFallback: '#777',
chartPanel: '#1f1e33',
chartText: '#FFF',
labelBackground: '#5b5b5b',
mutedText: '#a7a7a7',
primaryText: '#505050',
separator: '#a8a8a8',
skillLevelBackground: '#ff0000',
subtlePanel: '#f1f1f1',
surface: '#ffffff',
},
font: {
body: 'old',
chart: 'Arial',
chinese: 'FangZhengHeiTi',
fallback: 'Microsoft Yahei',
},
layout: {
contentWidth: 800,
defaultGap: 20,
listIndent: 20,
},
} as const;
export type BangDreamRenderFont =
(typeof BANGDREAM_RENDER_THEME.font)[keyof typeof BANGDREAM_RENDER_THEME.font];

View File

@ -1,15 +1,15 @@
import { Canvas, Image } from 'skia-canvas'; import { Canvas, Image } from 'skia-canvas';
import { drawText } from '@/qqbot/plugins/bangDream/tsugu/canvas/text'; import { drawText } from '@/qqbot/plugins/bangDream/tsugu/canvas/text';
import { assetsRootPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
import * as path from 'path';
import { loadImageFromPath } from '@/qqbot/plugins/bangDream/tsugu/canvas/image-utils'; import { loadImageFromPath } from '@/qqbot/plugins/bangDream/tsugu/canvas/image-utils';
import { getBangDreamAssetPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/asset-manifest';
import { BANGDREAM_RENDER_THEME } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/theme';
let titleImage: Image; let titleImage: Image;
/** /**
* Once * Once
*/ */
async function loadImageOnce() { async function loadImageOnce() {
titleImage = await loadImageFromPath(path.join(assetsRootPath, '/title.png')); titleImage = await loadImageFromPath(getBangDreamAssetPath('title'));
} }
loadImageOnce(); loadImageOnce();
@ -29,16 +29,16 @@ export function drawTitle(title1: string, title2: string): Canvas {
maxWidth: 900, maxWidth: 900,
lineHeight: 50, lineHeight: 50,
textSize: 30, textSize: 30,
color: '#ffffff', color: BANGDREAM_RENDER_THEME.color.surface,
font: 'old', font: BANGDREAM_RENDER_THEME.font.body,
}); });
const text2 = drawText({ const text2 = drawText({
text: title2, text: title2,
maxWidth: 900, maxWidth: 900,
lineHeight: 68, lineHeight: 68,
textSize: 40, textSize: 40,
color: '#5b5b5b', color: BANGDREAM_RENDER_THEME.color.labelBackground,
font: 'old', font: BANGDREAM_RENDER_THEME.font.body,
}); });
ctx.drawImage(text1, 74, 0); ctx.drawImage(text1, 74, 0);
ctx.drawImage(text2, 74, 42); ctx.drawImage(text2, 74, 42);

View File

@ -0,0 +1,49 @@
import * as path from 'path';
import { assetsRootPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
export const BANGDREAM_LOCAL_ASSETS = {
backgroundLive: 'BG/live.png',
backgroundObjectBig: 'BG/bg_object_big.png',
backgroundStar1: 'BG/star1.png',
backgroundStar2: 'BG/star2.png',
cardBirthday: 'Card/B.png',
cardDreamfes: 'Card/D.png',
cardKirafes: 'Card/K.png',
cardLimited: 'Card/L.png',
cardLimitBreakRank: 'Card/limitBreakRank.png',
cardSkillTextBase: 'Card/text.png',
cardStar: 'Card/star.png',
cardStarTrained: 'Card/star_trained.png',
fontFangZhengHeiTi: 'Fonts/FangZhengHeiTi_GBK.ttf',
fontOld: 'Fonts/old.ttf',
skillDamage: 'Skill/damage.png',
skillJudge: 'Skill/judge.png',
skillLife: 'Skill/life.png',
songChartJacket: 'SongChart/jacket.png',
songChartNoteBar: 'SongChart/note/Bar.png',
songChartNoteFlick: 'SongChart/note/Flick.png',
songChartNoteFlickTop: 'SongChart/note/FlickTop.png',
songChartNoteLeftArrow: 'SongChart/note/LeftArrow.png',
songChartNoteLeftArrowEnd: 'SongChart/note/LeftArrowEnd.png',
songChartNoteLong: 'SongChart/note/Long.png',
songChartNoteRightArrow: 'SongChart/note/RightArrow.png',
songChartNoteRightArrowEnd: 'SongChart/note/RightArrowEnd.png',
songChartNoteSim: 'SongChart/note/Sim.png',
songChartNoteSingle: 'SongChart/note/Single.png',
songChartNoteSingleOff: 'SongChart/note/SingleOff.png',
songChartNoteSkill: 'SongChart/note/Skill.png',
songChartNoteTick: 'SongChart/note/Tick.png',
title: 'title.png',
twServerIcon: 'tw.png',
} as const;
export type BangDreamLocalAssetKey = keyof typeof BANGDREAM_LOCAL_ASSETS;
/**
* Tsugu
*
* @param key - manifest
*/
export function getBangDreamAssetPath(key: BangDreamLocalAssetKey): string {
return path.join(assetsRootPath, BANGDREAM_LOCAL_ASSETS[key]);
}