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 文件。
当前进度:
- 已新增 `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 和时间/档线规则
目标:服务器优先级、国服预估、档位、时区和活动状态从工具函数变成可测试规则。

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

View File

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

View File

@ -1,7 +1,9 @@
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';
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 {
width: number;
@ -24,7 +26,7 @@ export function drawRoundedRect({
width,
height,
radius = 25,
color = '#ffffff',
color = BANGDREAM_RENDER_THEME.color.surface,
opacity = 0.9,
strokeColor = '#bbbbbb',
strokeWidth = 0,
@ -119,14 +121,14 @@ interface RoundedRectWithText {
*/
export function drawRoundedRectWithText({
text,
font = 'old',
textColor = '#ffffff',
font = BANGDREAM_RENDER_THEME.font.body,
textColor = BANGDREAM_RENDER_THEME.color.surface,
textSize,
textAlign = 'center',
height = (textSize * 4) / 3,
width = getTextWidth(text, textSize, font) + height,
radius = height / 2,
color = '#5b5b5b',
color = BANGDREAM_RENDER_THEME.color.labelBackground,
opacity = 1,
strokeColor = color,
strokeWidth = 0,
@ -144,7 +146,7 @@ export function drawRoundedRectWithText({
ctx.fillStyle = textColor;
ctx.textBaseline = 'alphabetic';
ctx.font = `${textSize}px old,Microsoft Yahei`;
ctx.font = `${textSize}px ${font},${BANGDREAM_RENDER_THEME.font.fallback}`;
let x = 0,
y = 0;

View File

@ -4,10 +4,12 @@ import {
Canvas,
CanvasRenderingContext2D,
} from 'skia-canvas';
import { assetsRootPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
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')]);
FontLibrary.use('FangZhengHeiTi', [
`${assetsRootPath}/Fonts/FangZhengHeiTi_GBK.ttf`,
getBangDreamAssetPath('fontFangZhengHeiTi'),
]);
interface WrapTextOptions {
@ -31,8 +33,8 @@ export function drawText({
textSize = 40,
maxWidth,
lineHeight = (textSize * 4) / 3,
color = '#505050',
font = 'old',
color = BANGDREAM_RENDER_THEME.color.primaryText,
font = BANGDREAM_RENDER_THEME.font.body,
}: WrapTextOptions): Canvas {
const wrappedTextData = wrapText({ text, maxWidth, lineHeight, textSize });
let canvas: Canvas;
@ -131,8 +133,8 @@ export function drawTextWithImages({
lineHeight = (textSize * 4) / 3,
content,
spacing = textSize / 3,
color = '#505050',
font = 'old',
color = BANGDREAM_RENDER_THEME.color.primaryText,
font = BANGDREAM_RENDER_THEME.font.body,
}: TextWithImagesOptions) {
const wrappedTextData = wrapTextWithImages({
textSize,
@ -305,5 +307,5 @@ export const setFontStyle = function (
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,
} from '@/qqbot/plugins/bangDream/tsugu/render-blocks/list-song';
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({
width: 800,
height: 10,
startX: 5,
startY: 5,
endX: 795,
endY: 5,
radius: 2,
gap: 10,
color: '#a8a8a8',
});
const songSeparatorLine = drawDottedLine(
createHorizontalSeparatorSpec({ height: 10 }),
);
/**
* QQBot
@ -393,7 +387,7 @@ export async function drawEventDetail(
const eventBannerImage = await event.getBannerImage();
const eventBannerImageCanvas = drawBannerImageCanvas(eventBannerImage);
list.push(eventBannerImageCanvas);
list.push(new Canvas(800, 30));
list.push(new Canvas(BANGDREAM_RENDER_THEME.layout.contentWidth, 30));
//标题
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 { createTsuguEntityMatcher } from '@/qqbot/plugins/bangDream/tsugu/search/entity-list-matcher';
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 maxColumns = 7;
//表格用默认虚线
export const line2: Canvas = drawDottedLine({
width: 30,
height: 7000,
startX: 5,
startY: 0,
endX: 15,
endY: 6995,
radius: 2,
gap: 10,
color: '#a8a8a8',
});
export const line2: Canvas = drawDottedLine(
createVerticalSeparatorSpec(7000, { startX: 5 }),
);
/**
* QQBot

View File

@ -15,32 +15,16 @@ import { globalDefaultServer } from '@/qqbot/plugins/bangDream/tsugu/runtime/con
import { drawSongDetail } from './song-detail';
import { createTsuguEntityMatcher } from '@/qqbot/plugins/bangDream/tsugu/search/entity-list-matcher';
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({
width: 800,
height: 10,
startX: 5,
startY: 5,
endX: 795,
endY: 5,
radius: 2,
gap: 10,
color: '#a8a8a8',
});
const line = drawDottedLine(createHorizontalSeparatorSpec({ height: 10 }));
//表格用默认竖向虚线
const line2: Canvas = drawDottedLine({
width: 30,
height: 6000,
startX: 10,
startY: 0,
endX: 15,
endY: 5990,
radius: 2,
gap: 10,
color: '#a8a8a8',
});
const line2: Canvas = drawDottedLine(createVerticalSeparatorSpec(6000));
/**
* 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 { Band } from '@/qqbot/plugins/bangDream/tsugu/models/band';
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 { downloadFileCache } from '@/qqbot/plugins/bangDream/tsugu/data-clients/asset-cache-client';
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 { bestdoriUrl } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
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 starList: { [type: string]: Image } = {};
@ -20,25 +20,23 @@ let limitBreakIcon: Image;
*/
async function loadImageOnce() {
cardTypeIconList.limited = await loadImageFromPath(
path.join(assetsRootPath, '/Card/L.png'),
getBangDreamAssetPath('cardLimited'),
);
cardTypeIconList.dreamfes = await loadImageFromPath(
path.join(assetsRootPath, '/Card/D.png'),
getBangDreamAssetPath('cardDreamfes'),
);
cardTypeIconList.kirafes = await loadImageFromPath(
path.join(assetsRootPath, '/Card/K.png'),
getBangDreamAssetPath('cardKirafes'),
);
cardTypeIconList.birthday = await loadImageFromPath(
path.join(assetsRootPath, '/Card/B.png'),
);
starList.normal = await loadImageFromPath(
path.join(assetsRootPath, '/Card/star.png'),
getBangDreamAssetPath('cardBirthday'),
);
starList.normal = await loadImageFromPath(getBangDreamAssetPath('cardStar'));
starList.trained = await loadImageFromPath(
path.join(assetsRootPath, '/Card/star_trained.png'),
getBangDreamAssetPath('cardStarTrained'),
);
limitBreakIcon = await loadImageFromPath(
path.join(assetsRootPath, '/Card/limitBreakRank.png'),
getBangDreamAssetPath('cardLimitBreakRank'),
);
}
@ -136,17 +134,17 @@ export async function drawCardIcon({
if (cardIdVisible) {
ctx.textAlign = 'start';
ctx.textBaseline = 'middle';
setFontStyle(ctx, 30, 'old');
ctx.fillStyle = '#a7a7a7';
setFontStyle(ctx, 30, BANGDREAM_RENDER_THEME.font.body);
ctx.fillStyle = BANGDREAM_RENDER_THEME.color.mutedText;
ctx.fillText(`ID:${card.cardId}`, 4, 195);
}
//如果显示技能类型,在右上显示
if (skillLevel != undefined) {
ctx.fillStyle = '#ff0000';
ctx.fillStyle = BANGDREAM_RENDER_THEME.color.skillLevelBackground;
ctx.fillRect(138, 91, 35, 39);
ctx.fillStyle = '#ffffff';
ctx.fillStyle = BANGDREAM_RENDER_THEME.color.surface;
ctx.textAlign = 'center';
setFontStyle(ctx, 35, 'old');
setFontStyle(ctx, 35, BANGDREAM_RENDER_THEME.font.body);
ctx.fillText(skillLevel.toString(), 155.5, 107.5);
}
//如果显示技能类型,在右上显示
@ -169,8 +167,8 @@ export async function drawCardIcon({
ctx.drawImage(bandIcon, 0, 0, 45, 45);
if (limitBreakRank != 0) {
ctx.drawImage(limitBreakIcon, 137, 51, 39, 39);
setFontStyle(ctx, 25, 'old');
ctx.fillStyle = '#ffffff';
setFontStyle(ctx, 25, BANGDREAM_RENDER_THEME.font.body);
ctx.fillStyle = BANGDREAM_RENDER_THEME.color.surface;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(limitBreakRank.toString(), 155, 70);

View File

@ -47,30 +47,22 @@ import {
} from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
import { Canvas, Image } from 'skia-canvas';
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({
width: 365,
height: 20,
startX: 5,
startY: 10,
endX: 360,
endY: 10,
radius: 2,
gap: 10,
color: '#a8a8a8',
});
const songDetailSeparator = drawDottedLine(
createHorizontalSeparatorSpec({
width: 365,
height: 20,
endX: 360,
}),
);
const songMetaSeparator = drawDottedLine({
width: 800,
height: 10,
startX: 5,
startY: 5,
endX: 795,
endY: 5,
radius: 2,
gap: 10,
color: '#a8a8a8',
});
const songMetaSeparator = drawDottedLine(
createHorizontalSeparatorSpec({
height: 10,
}),
);
/**
* Info块
@ -327,7 +319,7 @@ export async function drawCharacterHalfBlock(
await character.initFull(false);
const color = character.colorCode
? character.colorCode.toLowerCase()
: '#ffffff';
: BANGDREAM_RENDER_THEME.color.surface;
ctx.drawImage(
drawRoundedRect({
width,
@ -382,14 +374,14 @@ export async function drawCharacterHalfBlock(
const nameTextImage = drawText({
text: character.characterName[server],
textSize: 40,
color: '#ffffff',
color: BANGDREAM_RENDER_THEME.color.surface,
maxWidth: width,
});
list.push(drawImageListCenter([nameTextImage], width));
const idTextImage = drawText({
text: `ID: ${character.characterId}`,
textSize: 30,
color: '#ffffff',
color: BANGDREAM_RENDER_THEME.color.surface,
maxWidth: width,
});
list.push(drawImageListCenter([idTextImage], width));
@ -409,13 +401,13 @@ export async function drawPlayerDetailBlockWithIllustration(
const list: Array<Canvas | Image> = [];
const playerText = drawText({
text: player.profile.userName,
maxWidth: 800,
maxWidth: BANGDREAM_RENDER_THEME.layout.contentWidth,
textSize: 75,
});
list.push(drawImageListCenter([playerText]));
const levelText = drawText({
text: `等级 ${player.profile.rank}`,
maxWidth: 800,
maxWidth: BANGDREAM_RENDER_THEME.layout.contentWidth,
textSize: 35,
});
list.push(drawImageListCenter([levelText]));
@ -436,7 +428,7 @@ export async function drawPlayerDetailBlockWithIllustration(
const introductionText = drawText({
text: player.profile.introduction,
maxWidth: 800,
maxWidth: BANGDREAM_RENDER_THEME.layout.contentWidth,
textSize: 35,
});
list.push(drawImageListCenter([introductionText]));
@ -447,7 +439,7 @@ export async function drawPlayerDetailBlockWithIllustration(
: 'ID未公开';
const idText = drawTextWithImages({
content: [await getIcon(player.server), userId],
maxWidth: 800,
maxWidth: BANGDREAM_RENDER_THEME.layout.contentWidth,
textSize: 35,
});
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';
import { stackImageHorizontal } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/image-stack';
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({
width: 800,
height: 30,
startX: 5,
startY: 15,
endX: 795,
endY: 15,
radius: 2,
gap: 10,
color: '#a8a8a8',
});
export const line: Canvas = drawDottedLine(createHorizontalSeparatorSpec());
interface ListOptions {
key?: string;
@ -51,8 +46,8 @@ export function drawList({
textSize = 40,
lineHeight = textSize * 1.5,
spacing = textSize / 3,
color = '#505050',
maxWidth = 800,
color = BANGDREAM_RENDER_THEME.color.primaryText,
maxWidth = BANGDREAM_RENDER_THEME.layout.contentWidth,
}: ListOptions): Canvas {
const xmax = maxWidth - 40;
const keyImage = drawRoundedRectWithText({
@ -76,14 +71,21 @@ export function drawList({
textImage = new Canvas(0, 0);
}
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 canvas = new Canvas(maxWidth, ymax);
const ctx = canvas.getContext('2d');
ctx.drawImage(keyImage, 0, 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;
}
@ -107,7 +109,9 @@ export function drawTipsInList({
lineHeight = textSize * 1.5,
spacing = textSize / 3,
}: tipsOptions) {
const xmax = 760;
const xmax =
BANGDREAM_RENDER_THEME.layout.contentWidth -
BANGDREAM_RENDER_THEME.layout.listIndent * 2;
let textImage: Canvas;
if (typeof text == 'string') {
textImage = drawText({ text, textSize, maxWidth: xmax, lineHeight });
@ -122,11 +126,19 @@ export function drawTipsInList({
} else {
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');
ctx.fillStyle = '#f1f1f1';
ctx.fillRect(0, 10, 800, textImage.height);
ctx.drawImage(textImage, 20, 10);
ctx.fillStyle = BANGDREAM_RENDER_THEME.color.subtlePanel;
ctx.fillRect(
0,
10,
BANGDREAM_RENDER_THEME.layout.contentWidth,
textImage.height,
);
ctx.drawImage(textImage, BANGDREAM_RENDER_THEME.layout.listIndent, 10);
return canvas;
}
@ -142,7 +154,7 @@ export async function drawListByServerList(
content: Array<string | null>,
key?: string,
serverList: Server[] = globalDefaultServer,
maxWidth = 800,
maxWidth: number = BANGDREAM_RENDER_THEME.layout.contentWidth,
) {
const tempcontent: Array<string | Image | Canvas> = [];
@ -211,13 +223,16 @@ export function drawListMerge(imageList: Array<Canvas | Image>): Canvas {
maxHeight = element.height;
}
}
const canvas = new Canvas(800, maxHeight);
const canvas = new Canvas(
BANGDREAM_RENDER_THEME.layout.contentWidth,
maxHeight,
);
const ctx = canvas.getContext('2d');
let x = 0;
for (let i = 0; i < imageList.length; i++) {
const element = imageList[i];
ctx.drawImage(element, x, 0);
x += 800 / imageList.length;
x += BANGDREAM_RENDER_THEME.layout.contentWidth / imageList.length;
}
return canvas;
}
@ -232,7 +247,7 @@ export function drawListMerge(imageList: Array<Canvas | Image>): Canvas {
*/
export function drawImageListCenter(
imageList: Array<Canvas | Image>,
maxWidth = 800,
maxWidth: number = BANGDREAM_RENDER_THEME.layout.contentWidth,
): Canvas {
interface imageLine {
imageList: Array<Canvas | Image>;
@ -320,10 +335,14 @@ export function drawListWithLine(textImageList: Array<Canvas | Image>): Canvas {
const element = textImageList[i];
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');
ctx.fillStyle = '#a8a8a8';
ctx.fillRect(10, 10, 5, height + 20);
ctx.fillStyle = BANGDREAM_RENDER_THEME.color.separator;
const lineSpec = createVerticalSeparatorSpec(height + 20);
ctx.fillRect(lineSpec.startX, 10, 5, height + 20);
for (let i = 0; i < textImageList.length; i++) {
const element = textImageList[i];
ctx.drawImage(element, x, y);

View File

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

View File

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

View File

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

View File

@ -1,6 +1,10 @@
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 {
BangDreamLocalAssetKey,
getBangDreamAssetPath,
} from '@/qqbot/plugins/bangDream/tsugu/runtime/asset-manifest';
import { BANGDREAM_RENDER_THEME } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/theme';
interface BestdoriPreviewPayload {
id: number;
@ -70,6 +74,23 @@ const NOTE_IMAGE_KEYS = [
'RightArrow',
'RightArrowEnd',
] 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 = [
'Single',
'SingleOff',
@ -451,7 +472,9 @@ async function loadNoteImages(): Promise<Record<string, Image>> {
const entries = await Promise.all(
NOTE_IMAGE_KEYS.map(async (key) => [
key,
await loadImageFromPath(`${assetsRootPath}/SongChart/note/${key}.png`),
await loadImageFromPath(
getBangDreamAssetPath(NOTE_IMAGE_ASSET_KEYS[key]),
),
]),
);
return Object.fromEntries(entries);
@ -467,7 +490,7 @@ async function loadCoverImage(cover: string | Buffer): Promise<Image> {
try {
return await loadImage(cover);
} catch {
return await loadImageFromPath(`${assetsRootPath}/SongChart/jacket.png`);
return await loadImageFromPath(getBangDreamAssetPath('songChartJacket'));
}
}
@ -526,7 +549,7 @@ function drawBaseInfo(
): void {
const { id, diff, level } = payload;
ctx.save();
ctx.fillStyle = '#000';
ctx.fillStyle = BANGDREAM_RENDER_THEME.color.chartBackground;
ctx.fillRect(0, 0, layout.width, layout.height);
ctx.restore();
@ -539,10 +562,10 @@ function drawBaseInfo(
);
ctx.save();
ctx.fillStyle = '#1f1e33';
ctx.fillStyle = BANGDREAM_RENDER_THEME.color.chartPanel;
ctx.fillRect(OFFSET - 8, OFFSET - 8, 128, 24);
ctx.fillStyle = '#FFF';
ctx.font = '16px "Arial"';
ctx.fillStyle = BANGDREAM_RENDER_THEME.color.chartText;
ctx.font = `16px "${BANGDREAM_RENDER_THEME.font.chart}"`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(`${id}`, OFFSET + 56, OFFSET + 4, 128);
@ -550,10 +573,12 @@ function drawBaseInfo(
const coverWidth = layout.infoAreaWidth - 16;
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.fillStyle = '#FFF';
ctx.font = '16px "Arial"';
ctx.fillStyle = BANGDREAM_RENDER_THEME.color.chartText;
ctx.font = `16px "${BANGDREAM_RENDER_THEME.font.chart}"`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(`${diff} ${level}`, 8 + coverWidth - 52, 8 + coverWidth, 128);
@ -657,8 +682,8 @@ function drawBeatLines(
*/
function drawTimeline(ctx: any, layout: PreviewLayout): void {
ctx.save();
ctx.font = '18px "Arial"';
ctx.fillStyle = '#FFF';
ctx.font = `18px "${BANGDREAM_RENDER_THEME.font.chart}"`;
ctx.fillStyle = BANGDREAM_RENDER_THEME.color.chartText;
ctx.textAlign = 'right';
for (let i = 0; i <= layout.chartLength; i += 5) {
const { x, y } = getTimePosition(layout, i);
@ -692,20 +717,20 @@ function drawCountAndBpmLines(
if (count % 50 !== 0) {
continue;
}
ctx.font = '18px "Arial"';
ctx.font = `18px "${BANGDREAM_RENDER_THEME.font.chart}"`;
ctx.fillStyle = 'rgba(128, 128, 128, 0.5)';
ctx.textAlign = 'left';
ctx.fillRect(x, y - 1, w, 2);
ctx.fillStyle = '#FFF';
ctx.fillStyle = BANGDREAM_RENDER_THEME.color.chartText;
setAdaptiveTextBaseline(ctx, layout, 18, y);
ctx.fillText(`${count}`, x + w + 8, y);
continue;
}
if (note.type === 'BPM') {
ctx.fillStyle = '#C34FBB';
ctx.fillStyle = BANGDREAM_RENDER_THEME.color.chartBpm;
ctx.fillRect(x, y - 1, w, 2);
ctx.font = '18px "Arial"';
ctx.font = `18px "${BANGDREAM_RENDER_THEME.font.chart}"`;
ctx.textAlign = 'left';
setAdaptiveTextBaseline(ctx, layout, 18, 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 { 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 { getBangDreamAssetPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/asset-manifest';
import { BANGDREAM_RENDER_THEME } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/theme';
let titleImage: Image;
/**
* Once
*/
async function loadImageOnce() {
titleImage = await loadImageFromPath(path.join(assetsRootPath, '/title.png'));
titleImage = await loadImageFromPath(getBangDreamAssetPath('title'));
}
loadImageOnce();
@ -29,16 +29,16 @@ export function drawTitle(title1: string, title2: string): Canvas {
maxWidth: 900,
lineHeight: 50,
textSize: 30,
color: '#ffffff',
font: 'old',
color: BANGDREAM_RENDER_THEME.color.surface,
font: BANGDREAM_RENDER_THEME.font.body,
});
const text2 = drawText({
text: title2,
maxWidth: 900,
lineHeight: 68,
textSize: 40,
color: '#5b5b5b',
font: 'old',
color: BANGDREAM_RENDER_THEME.color.labelBackground,
font: BANGDREAM_RENDER_THEME.font.body,
});
ctx.drawImage(text1, 74, 0);
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]);
}