feat: 涂色识字完善、找字涂色完成
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 识字绘制服务工厂
|
||||
* 根据模板类型创建对应的绘制服务实例
|
||||
*
|
||||
* 支持的模板类型:
|
||||
* - grid:网格模板
|
||||
* - find:找字模板
|
||||
*
|
||||
* 支持的绘制服务:
|
||||
* - TextDrawService:文字涂色服务
|
||||
* - FindWordDrawService:找字涂色服务
|
||||
*/
|
||||
|
||||
import TextDrawService from './textDrawService';
|
||||
import FindWordDrawService from './findWordDrawService';
|
||||
|
||||
/**
|
||||
* 绘制服务接口
|
||||
*/
|
||||
export interface IDrawService {
|
||||
draw(list: Array<{ color: string; word: string }>): Promise<void>;
|
||||
clear(): void;
|
||||
setPaper(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 模板类型
|
||||
*/
|
||||
export type TemplateType = 'grid' | 'find';
|
||||
|
||||
/**
|
||||
* 绘制服务工厂类
|
||||
* 根据模板类型创建对应的绘制服务实例
|
||||
*/
|
||||
export class DrawServiceFactory {
|
||||
/**
|
||||
* 创建绘制服务实例
|
||||
* @param templateType 模板类型
|
||||
* @param canvas Canvas 对象
|
||||
* @param ctx 渲染上下文
|
||||
* @param options 可选配置
|
||||
* @returns 绘制服务实例
|
||||
*/
|
||||
static create(
|
||||
templateType: TemplateType,
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
): IDrawService {
|
||||
switch (templateType) {
|
||||
case 'grid':
|
||||
return new TextDrawService(canvas, ctx, options);
|
||||
case 'find':
|
||||
return new FindWordDrawService(canvas, ctx, options);
|
||||
default:
|
||||
// 默认使用网格模板
|
||||
return new TextDrawService(canvas, ctx, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* 找字涂色服务
|
||||
* 根据模板类型绘制到Canvas
|
||||
*
|
||||
* 支持的模板类型:
|
||||
* - grid:网格模板
|
||||
* - find:找字模板
|
||||
*
|
||||
* 支持的绘制服务:
|
||||
* - TextDrawService:文字涂色服务
|
||||
* - FindWordDrawService:找字涂色服务
|
||||
*/
|
||||
import { BaseDrawService } from '../../../core/draw/baseDraw';
|
||||
import { POSITION_TEMPLATES } from './findWordTemplate';
|
||||
|
||||
// ==================== Debug 开关 ====================
|
||||
// 设置为 true 时,其他文字会显示坐标系的索引值(第几个位置),方便手动调整坐标系
|
||||
const DEBUG = false;
|
||||
// ====================================================
|
||||
|
||||
/**
|
||||
* 从固定模板中获取位置
|
||||
* @param centerX 中心X坐标
|
||||
* @param centerY 中心Y坐标
|
||||
* @param count 需要的位置数量
|
||||
* @param padding 边距
|
||||
* @param canvasWidth 画布宽度
|
||||
* @param canvasHeight 画布高度
|
||||
* @param radius 字符圆半径
|
||||
* @returns 位置数组
|
||||
*/
|
||||
/**
|
||||
* 随机选择一个模板并返回模板的实际位置数量
|
||||
* @returns 模板索引和模板的实际位置数量
|
||||
*/
|
||||
function selectTemplate(): { templateIndex: number } {
|
||||
const templateIndex = Math.floor(Math.random() * POSITION_TEMPLATES.length);
|
||||
return {
|
||||
templateIndex,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 从指定模板中获取位置
|
||||
* @param templateIndex 模板索引
|
||||
* @param centerX 中心X坐标(逻辑像素)
|
||||
* @param centerY 中心Y坐标(逻辑像素)
|
||||
* @returns 位置数组(逻辑像素)
|
||||
*
|
||||
* 注意:POSITION_TEMPLATES 中的坐标是基于原始像素的,需要转换为逻辑像素(除以3)
|
||||
*/
|
||||
function getPositionsFromTemplate(
|
||||
templateIndex: number,
|
||||
centerX: number,
|
||||
centerY: number,
|
||||
): Array<{ x: number; y: number }> {
|
||||
const template = POSITION_TEMPLATES[templateIndex];
|
||||
|
||||
// 转换为绝对坐标(模板坐标除以3转换为逻辑像素)
|
||||
const positions: Array<{ x: number; y: number }> = [];
|
||||
for (const pos of template) {
|
||||
const x = centerX + pos.x / 3; // 转换为逻辑像素
|
||||
const y = centerY + pos.y / 3; // 转换为逻辑像素
|
||||
|
||||
positions.push({ x, y });
|
||||
}
|
||||
return positions;
|
||||
}
|
||||
|
||||
class FindWordDrawService extends BaseDrawService {
|
||||
colors: string[];
|
||||
characters: string[];
|
||||
debug: boolean = false; // Debug模式开关
|
||||
|
||||
constructor(
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
) {
|
||||
options = options || {};
|
||||
super(canvas, ctx, {
|
||||
title: '找一找 涂 色',
|
||||
subTitle: '找出相同的文字涂色',
|
||||
...options,
|
||||
});
|
||||
// 从options中读取debug参数,如果没有则使用全局DEBUG常量
|
||||
this.debug = options.debug === true || DEBUG;
|
||||
this.colors = ['#000'];
|
||||
this.characters = ['王'];
|
||||
}
|
||||
|
||||
async draw(list: Array<{ color: string; word: string }>) {
|
||||
this.setPrintConfig();
|
||||
|
||||
this.colors = list.map((item) => item.color || '#000');
|
||||
this.characters = list.map((item) => item.word || '日');
|
||||
this.clear();
|
||||
this.setPaper();
|
||||
|
||||
await this.drawHeader();
|
||||
|
||||
// 找字模板没有 drawLegend 部分
|
||||
this.drawDivider();
|
||||
this.drawContent();
|
||||
// await this.drawPrintFooter();
|
||||
}
|
||||
|
||||
drawContent() {
|
||||
const { ctx, characters, colors } = this;
|
||||
if (characters.length <= 0) return;
|
||||
|
||||
// 第一个字作为中心大字
|
||||
const firstChar = characters[0];
|
||||
const firstColor = colors[0];
|
||||
|
||||
// 计算内容区域(逻辑像素)
|
||||
const contentTop = this.currentY + 17; // 50/3≈17
|
||||
const contentBottom = this.canvasHeight - 27; // 80/3≈27
|
||||
const contentHeight = contentBottom - contentTop;
|
||||
|
||||
// 中心大字的参数
|
||||
const centerX = this.canvasWidth / 2;
|
||||
const centerY = contentTop + contentHeight / 2;
|
||||
|
||||
// 普通圆的参数(逻辑像素)
|
||||
const radius = 27; // 80/3≈27
|
||||
const fontSize = 24; // 72/3=24
|
||||
|
||||
// 先选择模板,获取模板的实际位置数量
|
||||
const { templateIndex } = selectTemplate();
|
||||
// 从固定模板中获取位置
|
||||
const positions = getPositionsFromTemplate(
|
||||
templateIndex,
|
||||
centerX,
|
||||
centerY,
|
||||
);
|
||||
const targetCount = positions.length; // 使用模板的实际位置数量(中心字已单独绘制)
|
||||
|
||||
// 生成所有要绘制的字符列表(包括第一个字和其他字)
|
||||
const allChars: Array<{ char: string; color: string }> = [];
|
||||
|
||||
// 如果只有一个字符,全部使用第一个字符
|
||||
if (characters.length === 1) {
|
||||
for (let i = 0; i < targetCount; i++) {
|
||||
allChars.push({ char: firstChar, color: firstColor });
|
||||
}
|
||||
} else {
|
||||
// 第一个字出现多次(根据总字符数决定,确保有足够的第一个字)
|
||||
const firstCharCount = Math.max(
|
||||
Math.floor(targetCount * 0.4),
|
||||
Math.floor(characters.length * 2),
|
||||
);
|
||||
for (let i = 0; i < firstCharCount; i++) {
|
||||
allChars.push({ char: firstChar, color: firstColor });
|
||||
}
|
||||
|
||||
// 其他字符各出现几次
|
||||
const remainingCount = targetCount - firstCharCount;
|
||||
const otherCharCount = Math.max(
|
||||
1,
|
||||
Math.floor(remainingCount / (characters.length - 1)),
|
||||
);
|
||||
for (let i = 1; i < characters.length; i++) {
|
||||
for (let j = 0; j < otherCharCount; j++) {
|
||||
allChars.push({ char: characters[i], color: colors[i] });
|
||||
}
|
||||
}
|
||||
|
||||
// 如果生成的字符数量还不够,用第一个字符补齐
|
||||
while (allChars.length < targetCount) {
|
||||
allChars.push({ char: firstChar, color: firstColor });
|
||||
}
|
||||
}
|
||||
|
||||
// 打乱顺序
|
||||
for (let i = allChars.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[allChars[i], allChars[j]] = [allChars[j], allChars[i]];
|
||||
}
|
||||
|
||||
// 根据实际生成的位置数量来截取字符(确保数量匹配)
|
||||
const charsToDraw = allChars.slice(0, positions.length);
|
||||
|
||||
// 绘制中心大字(去掉圆圈border,只绘制文字)
|
||||
ctx.fillStyle = '#333';
|
||||
ctx.font = `bold ${fontSize * 3}px "Microsoft Yahei"`; // 中心字是其他字的3倍
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(firstChar, centerX, centerY);
|
||||
|
||||
// 绘制四周的字符
|
||||
positions.forEach((pos, index) => {
|
||||
const item = charsToDraw[index];
|
||||
const y = pos.y; // 已经是绝对坐标
|
||||
|
||||
// 绘制圆
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.strokeStyle = '#000';
|
||||
ctx.lineWidth = 1; // 4/3≈1
|
||||
ctx.beginPath();
|
||||
ctx.arc(pos.x, y, radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.closePath();
|
||||
|
||||
// 绘制字符
|
||||
ctx.fillStyle = '#333';
|
||||
ctx.font = `bold ${fontSize}px "Microsoft Yahei"`;
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.textAlign = 'center';
|
||||
// Debug模式:显示坐标系索引值,否则显示文字
|
||||
const displayText = this.debug ? String(index) : item.char;
|
||||
ctx.fillText(displayText, pos.x, y, radius * 2);
|
||||
});
|
||||
}
|
||||
|
||||
// drawLine 已由基类的 drawDivider 替代,保留此方法以保持兼容
|
||||
drawLine(linY: number) {
|
||||
this.drawDivider();
|
||||
}
|
||||
}
|
||||
|
||||
export default FindWordDrawService;
|
||||
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* 找字模板
|
||||
* 包含多个模板,每个模板包含多个位置,每个位置包含x和y坐标
|
||||
*/
|
||||
export const POSITION_TEMPLATES: Array<Array<{ x: number; y: number }>> = [
|
||||
// 模板1:椭圆形分布-上下更密集 28个位置,相邻间距约100-150px
|
||||
[
|
||||
// 0-14号位置
|
||||
{ x: -600, y: 0 }, // 0 - 左侧
|
||||
{ x: -440, y: -240 }, // 1
|
||||
{ x: -580, y: -430 }, // 2
|
||||
{ x: -340, y: -600 }, // 3
|
||||
{ x: -330, y: -880 }, // 4
|
||||
{ x: -560, y: -780 }, // 5
|
||||
{ x: -50, y: -890 }, // 6
|
||||
{ x: -80, y: -670 }, // 7 - 上方
|
||||
{ x: 180, y: -880 }, // 8
|
||||
{ x: 200, y: -620 }, // 9
|
||||
{ x: 440, y: -850 }, // 10
|
||||
{ x: 600, y: -620 }, // 11
|
||||
{ x: 420, y: -460 }, // 12
|
||||
{ x: 500, y: -220 }, // 13
|
||||
{ x: 600, y: 0 }, // 14 - 右侧
|
||||
// 15-28号位置:基于0-14号位置以x轴为镜像轴生成(y坐标取反,x坐标不变)
|
||||
{ x: 600, y: 0 }, // 15 - 14的镜像
|
||||
{ x: 500, y: 220 }, // 16 - 13的镜像
|
||||
{ x: 420, y: 460 }, // 17 - 12的镜像
|
||||
{ x: 600, y: 620 }, // 18 - 11的镜像
|
||||
{ x: 440, y: 850 }, // 19 - 10的镜像
|
||||
{ x: 200, y: 620 }, // 20 - 9的镜像
|
||||
{ x: 180, y: 880 }, // 21 - 8的镜像
|
||||
{ x: -80, y: 670 }, // 22 - 7的镜像
|
||||
{ x: -50, y: 890 }, // 23 - 6的镜像
|
||||
{ x: -560, y: 780 }, // 24 - 5的镜像
|
||||
{ x: -330, y: 880 }, // 25 - 4的镜像
|
||||
{ x: -340, y: 600 }, // 26 - 3的镜像
|
||||
{ x: -580, y: 430 }, // 27 - 2的镜像
|
||||
{ x: -440, y: 240 }, // 28 - 1的镜像
|
||||
],
|
||||
// 模板2:基于模板1,在上下各增加一个坐标系,并适度调整原始位置
|
||||
[
|
||||
{ x: -620, y: 0 }, // 0 - 左侧(向外扩展)
|
||||
{ x: -420, y: -200 }, // 1
|
||||
{ x: -640, y: -350 }, // 2
|
||||
{ x: -400, y: -480 }, // 3
|
||||
{ x: -600, y: -600 }, // 4
|
||||
{ x: -420, y: -740 }, // 5
|
||||
{ x: -280, y: -910 }, // 6
|
||||
{ x: -200, y: -680 }, // 7
|
||||
{ x: -60, y: -890 }, // 8
|
||||
{ x: -10, y: -660 }, // 9 - 上方
|
||||
{ x: 180, y: -890 }, // 10
|
||||
{ x: 200, y: -630 }, // 11
|
||||
{ x: 400, y: -800 }, // 12
|
||||
{ x: 590, y: -660 }, // 13
|
||||
{ x: 410, y: -440 }, // 14
|
||||
{ x: 680, y: -420 }, // 15
|
||||
{ x: 570, y: -210 }, // 16
|
||||
{ x: 670, y: 0 }, // 17 - 右侧
|
||||
// 18-35号位置:基于0-17号位置以x轴为镜像轴生成(y坐标取反,x坐标不变)
|
||||
{ x: 570, y: 210 }, // 18 - 16的镜像
|
||||
{ x: 680, y: 420 }, // 19 - 15的镜像
|
||||
{ x: 410, y: 440 }, // 20 - 14的镜像
|
||||
{ x: 590, y: 660 }, // 21 - 13的镜像
|
||||
{ x: 400, y: 800 }, // 22 - 12的镜像
|
||||
{ x: 200, y: 630 }, // 23 - 11的镜像
|
||||
{ x: 180, y: 890 }, // 24 - 10的镜像
|
||||
{ x: -10, y: 660 }, // 25 - 9的镜像
|
||||
{ x: -60, y: 890 }, // 26 - 8的镜像
|
||||
{ x: -200, y: 680 }, // 27 - 7的镜像
|
||||
{ x: -300, y: 930 }, // 28 - 6的镜像
|
||||
{ x: -440, y: 780 }, // 29 - 5的镜像
|
||||
{ x: -600, y: 600 }, // 30 - 4的镜像
|
||||
{ x: -400, y: 480 }, // 31 - 3的镜像
|
||||
{ x: -640, y: 350 }, // 32 - 2的镜像
|
||||
{ x: -420, y: 200 }, // 33 - 1的镜像
|
||||
],
|
||||
// 模板3
|
||||
[
|
||||
// 0-15号位置
|
||||
{ x: -660, y: -100 }, // 0 - 左侧
|
||||
{ x: -500, y: -280 }, // 1
|
||||
{ x: -660, y: -480 }, // 2
|
||||
{ x: -660, y: -720 }, // 3
|
||||
{ x: -440, y: -920 }, // 4
|
||||
{ x: -250, y: -720 }, // 5
|
||||
{ x: -380, y: -480 }, // 6 - 上方
|
||||
{ x: -140, y: -480 }, // 7
|
||||
{ x: 90, y: -480 }, // 8
|
||||
{ x: 340, y: -480 }, // 9
|
||||
{ x: 250, y: -720 }, // 10
|
||||
{ x: 440, y: -920 }, // 11
|
||||
{ x: 660, y: -720 }, // 12
|
||||
{ x: 660, y: -480 }, // 13
|
||||
{ x: 500, y: -280 }, // 14
|
||||
{ x: 660, y: -100 }, // 15 - 右侧
|
||||
// 16-32号位置:基于0-15号位置以x轴为镜像轴生成(y坐标取反,x坐标不变)
|
||||
{ x: 660, y: 120 }, // 16 - 15的镜像
|
||||
{ x: 500, y: 300 }, // 17 - 14的镜像
|
||||
{ x: 680, y: 480 }, // 18 - 13的镜像
|
||||
{ x: 680, y: 720 }, // 19 - 12的镜像
|
||||
{ x: 500, y: 920 }, // 20 - 11的镜像
|
||||
{ x: 250, y: 720 }, // 21 - 10的镜像
|
||||
{ x: 340, y: 480 }, // 22 - 9的镜像
|
||||
{ x: 90, y: 480 }, // 23 - 8的镜像
|
||||
{ x: -140, y: 480 }, // 24 - 7的镜像
|
||||
{ x: -380, y: 480 }, // 25 - 6的镜像
|
||||
{ x: -250, y: 720 }, // 26 - 5的镜像
|
||||
{ x: -500, y: 920 }, // 27 - 4的镜像
|
||||
{ x: -660, y: 720 }, // 28 - 3的镜像
|
||||
{ x: -680, y: 480 }, // 29 - 2的镜像
|
||||
{ x: -500, y: 300 }, // 30 - 1的镜像
|
||||
{ x: -660, y: 120 }, // 31 - 0的镜像
|
||||
],
|
||||
// 模板4:三角形和梯形
|
||||
[
|
||||
// 0-5号位置
|
||||
{ x: -350, y: 0 }, // 0 - 顶点(不变)
|
||||
{ x: -500, y: 150 }, // 1 - 左侧上方45度
|
||||
{ x: -650, y: 300 }, // 2 - 左侧上方45度(更远)
|
||||
{ x: -500, y: -150 }, // 3 - 左侧下方45度
|
||||
{ x: -650, y: -300 }, // 4 - 左侧下方45度(更远)
|
||||
{ x: -650, y: 0 }, // 5 - Y=0,X和2、4相同
|
||||
|
||||
// 6-11号位置
|
||||
{ x: 350, y: 0 }, // 6 - 顶点(不变)
|
||||
{ x: 500, y: 150 }, // 7 - 右侧上方45度
|
||||
{ x: 650, y: 300 }, // 8 - 右侧上方45度(更远)
|
||||
{ x: 500, y: -150 }, // 9 - 右侧下方45度
|
||||
{ x: 650, y: -300 }, // 10 - 右侧下方45度(更远)
|
||||
{ x: 650, y: 0 }, // 11 - Y=0,X和8、10相同
|
||||
|
||||
// 倒着的梯形
|
||||
{ x: -440, y: -790 }, // 12
|
||||
{ x: -320, y: -570 }, // 13
|
||||
{ x: -200, y: -350 }, // 14
|
||||
{ x: 0, y: -350 }, // 15
|
||||
{ x: 200, y: -350 }, // 16
|
||||
{ x: 320, y: -570 }, // 17
|
||||
{ x: 440, y: -790 }, // 18
|
||||
{ x: -150, y: -790 }, // 19
|
||||
{ x: 150, y: -790 }, // 20
|
||||
|
||||
// 正着的梯形
|
||||
{ x: -440, y: 790 }, // 21
|
||||
{ x: -320, y: 570 }, // 22
|
||||
{ x: -200, y: 350 }, // 23
|
||||
{ x: 0, y: 350 }, // 24
|
||||
{ x: 200, y: 350 }, // 25
|
||||
{ x: 320, y: 570 }, // 26
|
||||
{ x: 440, y: 790 }, // 27
|
||||
{ x: 150, y: 790 }, // 28
|
||||
{ x: -150, y: 790 }, // 29
|
||||
],
|
||||
// 模板5:4个正方形
|
||||
[
|
||||
// 0-7号位置
|
||||
{ x: -250, y: -250 }, // 0
|
||||
{ x: -250, y: -500 }, // 1
|
||||
{ x: -250, y: -750 }, // 2
|
||||
{ x: -500, y: -750 }, // 3
|
||||
{ x: -750, y: -750 }, // 4
|
||||
{ x: -750, y: -500 }, // 5
|
||||
{ x: -750, y: -250 }, // 6
|
||||
{ x: -500, y: -250 }, // 7
|
||||
|
||||
// 8-15号位置
|
||||
{ x: 250, y: -250 }, // 8
|
||||
{ x: 250, y: -500 }, // 9
|
||||
{ x: 250, y: -750 }, // 10
|
||||
{ x: 500, y: -750 }, // 11
|
||||
{ x: 750, y: -750 }, // 12
|
||||
{ x: 750, y: -500 }, // 13
|
||||
{ x: 750, y: -250 }, // 14
|
||||
{ x: 500, y: -250 }, // 15
|
||||
|
||||
// 16-23号位置
|
||||
{ x: -250, y: 250 }, // 16
|
||||
{ x: -250, y: 500 }, // 17
|
||||
{ x: -250, y: 750 }, // 18
|
||||
{ x: -500, y: 750 }, // 19
|
||||
{ x: -750, y: 750 }, // 20
|
||||
{ x: -750, y: 500 }, // 21
|
||||
{ x: -750, y: 250 }, // 22
|
||||
{ x: -500, y: 250 }, // 23
|
||||
|
||||
// 24-31号位置
|
||||
{ x: 250, y: 250 }, // 24
|
||||
{ x: 250, y: 500 }, // 25
|
||||
{ x: 250, y: 750 }, // 26
|
||||
{ x: 500, y: 750 }, // 27
|
||||
{ x: 750, y: 750 }, // 28
|
||||
{ x: 750, y: 500 }, // 29
|
||||
{ x: 750, y: 250 }, // 30
|
||||
{ x: 500, y: 250 }, // 31
|
||||
],
|
||||
// 模板6:4个十字形
|
||||
[
|
||||
// 0-4号位置
|
||||
{ x: -500, y: -500 }, // 0
|
||||
{ x: -250, y: -500 }, // 1
|
||||
{ x: -500, y: -750 }, // 2
|
||||
{ x: -750, y: -500 }, // 3
|
||||
{ x: -500, y: -250 }, // 4
|
||||
|
||||
// 5-9号位置
|
||||
{ x: 500, y: -500 }, // 5
|
||||
{ x: 250, y: -500 }, // 6
|
||||
{ x: 500, y: -750 }, // 7
|
||||
{ x: 750, y: -500 }, // 8
|
||||
{ x: 500, y: -250 }, // 9
|
||||
|
||||
// 10-14号位置
|
||||
{ x: -500, y: 500 }, // 10
|
||||
{ x: -250, y: 500 }, // 11
|
||||
{ x: -500, y: 750 }, // 12
|
||||
{ x: -750, y: 500 }, // 13
|
||||
{ x: -500, y: 250 }, // 14
|
||||
|
||||
// 15-19号位置
|
||||
{ x: 500, y: 500 }, // 15
|
||||
{ x: 250, y: 500 }, // 16
|
||||
{ x: 500, y: 750 }, // 17
|
||||
{ x: 750, y: 500 }, // 18
|
||||
{ x: 500, y: 250 }, // 19
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* 找一找涂色,文字涂色服务
|
||||
* 根据文字列表绘制到Canvas
|
||||
*/
|
||||
|
||||
import { BaseDrawService } from '../../../core/draw/baseDraw';
|
||||
/**
|
||||
* 计算示例区域圆的中心点
|
||||
* @param canvasWidth 画布宽度(逻辑像素)
|
||||
* @param circleCount 圆的数量
|
||||
* @param padding 圆的间距(逻辑像素)
|
||||
* @param radius 圆的半径(逻辑像素)
|
||||
* @returns 圆的中心点
|
||||
*/
|
||||
function calculateCircleCenters(
|
||||
canvasWidth: number,
|
||||
circleCount: number,
|
||||
padding: number,
|
||||
radius: number,
|
||||
) {
|
||||
const availableWidth = canvasWidth - 2 * padding;
|
||||
const maxSpacing = 5.5 * radius;
|
||||
const centers = [];
|
||||
|
||||
if (circleCount === 1) {
|
||||
centers.push(canvasWidth / 2);
|
||||
} else {
|
||||
const requiredSpace = 2 * radius * circleCount;
|
||||
const spaceBetween =
|
||||
(availableWidth - requiredSpace) / (circleCount - 1);
|
||||
|
||||
if (spaceBetween > maxSpacing) {
|
||||
const totalWidth = 2 * radius + (circleCount - 1) * maxSpacing;
|
||||
const startX = (canvasWidth - totalWidth) / 2 + radius;
|
||||
|
||||
for (let i = 0; i < circleCount; i++) {
|
||||
centers.push(startX + i * maxSpacing);
|
||||
}
|
||||
} else {
|
||||
const startX = padding + radius;
|
||||
|
||||
for (let i = 0; i < circleCount; i++) {
|
||||
centers.push(startX + i * (spaceBetween + 2 * radius));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return centers;
|
||||
}
|
||||
|
||||
class TextDrawService extends BaseDrawService {
|
||||
colors: string[];
|
||||
characters: string[];
|
||||
|
||||
constructor(
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
) {
|
||||
options = options || {};
|
||||
super(canvas, ctx, {
|
||||
title: '找一找 涂 色',
|
||||
subTitle: '给文字涂上相同的颜色',
|
||||
...options,
|
||||
});
|
||||
this.colors = ['#000'];
|
||||
this.characters = ['王'];
|
||||
}
|
||||
|
||||
async draw(list: Array<{ color: string; word: string }>) {
|
||||
this.setPrintConfig();
|
||||
|
||||
this.colors = list.map((item) => item.color || '#000');
|
||||
this.characters = list.map((item) => item.word || '日');
|
||||
this.clear();
|
||||
this.setPaper();
|
||||
|
||||
await this.drawHeader();
|
||||
this.drawDivider();
|
||||
this.drawLegend();
|
||||
this.drawContent();
|
||||
// await this.drawPrintFooter();
|
||||
}
|
||||
|
||||
/** 绘制示例
|
||||
* 矩形: x、y为左上角
|
||||
* 圆形:x、y为圆心
|
||||
* 文字:x textAlign 为 start,文本左边缘对齐
|
||||
* y textBaseline 为 alphabetic,y 对应字母基线(类似左下角)
|
||||
*
|
||||
* 注意:所有尺寸已转换为逻辑像素(除以3)
|
||||
* */
|
||||
drawLegend() {
|
||||
const { ctx, colors, characters } = this;
|
||||
if (characters.length <= 0) return;
|
||||
|
||||
const legendTopY = this.currentY;
|
||||
const radius = 22; // 65/3≈22
|
||||
const rectWidth = 60; // 180/3=60
|
||||
const rectHeight = 27; // 80/3≈27
|
||||
const startX = 87 + radius; // (260+65)/3≈108
|
||||
const startY = legendTopY + 10 + radius; // (30+65)/3≈32
|
||||
const len = characters.length || 4;
|
||||
const centers = calculateCircleCenters(
|
||||
this.canvasWidth,
|
||||
len,
|
||||
73,
|
||||
radius,
|
||||
); // 220/3≈73
|
||||
|
||||
/** 计算每个圆之间的间距 */
|
||||
const spaceWidth =
|
||||
len > 3 ? (this.canvasWidth - startX * 2) / (len - 1) : 0;
|
||||
|
||||
ctx.moveTo(startX, startY);
|
||||
colors.forEach((color: string, index: number) => {
|
||||
const x = centers[index] || startX + index * spaceWidth;
|
||||
const y = startY;
|
||||
// 绘制圆
|
||||
ctx.strokeStyle = '#000';
|
||||
ctx.fillStyle = color;
|
||||
ctx.lineWidth = 1; // 4/3≈1
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.closePath();
|
||||
|
||||
// 绘制长方形
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.strokeStyle = '#000';
|
||||
ctx.lineWidth = 1; // 4/3≈1
|
||||
const rectangleX = x - rectWidth / 2;
|
||||
const rectangleY = y + radius + 14; // 43/3≈14
|
||||
ctx.strokeRect(rectangleX, rectangleY, rectWidth, rectHeight);
|
||||
|
||||
// 绘制字符
|
||||
ctx.fillStyle = '#333';
|
||||
ctx.font = 'bold 16px "Microsoft Yahei"'; // 48/3=16
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(
|
||||
characters[index],
|
||||
rectangleX + rectWidth / 2,
|
||||
rectangleY + rectHeight / 2,
|
||||
rectWidth,
|
||||
);
|
||||
});
|
||||
|
||||
this.currentY = startY + radius + 14 + rectHeight + 24;
|
||||
this.drawDivider();
|
||||
}
|
||||
|
||||
drawContent() {
|
||||
const { ctx, characters } = this;
|
||||
if (characters.length <= 0) return;
|
||||
|
||||
const len = characters.length;
|
||||
const rows = 8;
|
||||
const radius = 27; // 80/3≈27
|
||||
const fontSize = 24; // 72/3=24
|
||||
|
||||
const startY = this.currentY + 10 + radius; // 62/3≈21
|
||||
const startX1 = 103 + radius; // 310/3≈103
|
||||
const startX2 = 67 + radius; // 200/3≈67
|
||||
const spaceWidthFrist = (this.canvasWidth - startX1 * 2) / (5 - 1);
|
||||
const spaceWidthSecond = (this.canvasWidth - startX2 * 2) / (6 - 1);
|
||||
const spaceHeight = 18; // 54/3=18
|
||||
|
||||
ctx.moveTo(startX1, startY);
|
||||
for (let i = 0; i < rows; i++) {
|
||||
const cols = i % 2 === 0 ? 5 : 6;
|
||||
|
||||
for (let j = 0; j < cols; j++) {
|
||||
const y = startY + i * (spaceHeight + radius * 2);
|
||||
const x =
|
||||
i % 2 === 0
|
||||
? startX1 + j * spaceWidthFrist
|
||||
: startX2 + j * spaceWidthSecond;
|
||||
|
||||
const character = characters[Math.floor(Math.random() * len)];
|
||||
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.strokeStyle = '#000';
|
||||
ctx.lineWidth = 1; // 4/3≈1
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.closePath();
|
||||
|
||||
ctx.fillStyle = '#333';
|
||||
ctx.font = `${fontSize}px "Microsoft Yahei"`;
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(character, x, y, radius * 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// drawLine 已由基类的 drawDivider 替代,保留此方法以保持兼容
|
||||
drawLine(linY: number) {
|
||||
this.drawDivider();
|
||||
}
|
||||
}
|
||||
|
||||
export default TextDrawService;
|
||||
Reference in New Issue
Block a user