feat: 涂色识字完善、找字涂色完成

This commit is contained in:
R524809
2026-05-09 09:56:16 +08:00
parent b85afde7c0
commit c21ac9e935
27 changed files with 328 additions and 1135 deletions
@@ -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=0X和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=0X和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
],
// 模板54个正方形
[
// 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
],
// 模板64个十字形
[
// 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 为 alphabeticy 对应字母基线(类似左下角)
*
* 注意:所有尺寸已转换为逻辑像素(除以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;
@@ -1,6 +1,6 @@
import type { DebugPublishMeta } from '../../utils/debugPublish';
import { inferGradeFromAge } from '../../utils/debugPublish';
import type { TemplateType } from '../../service/drawServiceFactory';
import type { TemplateType } from '../shared/draw/drawServiceFactory';
/** 本地 worksheet 元数据(与云库 `worksheets` 文档 `_id` 对齐) */
interface WordColoringWorksheetDefinition {
@@ -35,9 +35,9 @@ export const WORD_COLORING_WORKSHEET_DEFINITIONS = [
icon: 'search',
title: '找字涂色',
subtitle: '在字海中找到目标字并涂色',
ageMin: 4,
ageMax: 7,
difficulty: 2,
ageMin: 3,
ageMax: 6,
difficulty: 1,
tags: ['识字', '涂色', '找字'],
sortOrder: 31,
},
@@ -12,7 +12,7 @@
"debug-publish-tools": "../../components3.0/debug-publish-tools/debug-publish-tools",
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
"toy-icon": "../../toy/icon/icon",
"color-picker": "../../components/color-picker/color-picker",
"word-picker": "../../components/word-picker/word-picker"
"color-picker": "../../components3.0/color-picker/color-picker",
"word-picker": "../../components3.0/word-picker/word-picker"
}
}
@@ -1,6 +1,9 @@
import { WATER_COLORS } from '../../constants/colors';
import { WORDS } from '../../constants/words';
import { DrawServiceFactory, type IDrawService } from '../../service/drawServiceFactory';
import { WORDS } from '../../core/data/words';
import {
DrawServiceFactory,
type IDrawService,
} from '../shared/draw/drawServiceFactory';
import { createPage, type CanvasDataState } from '../../base/pageMixin';
import { defaultShareConfig } from '../../config/config';
import {
@@ -12,14 +15,26 @@ import {
WORD_COLORING_WORKSHEET_DEFINITIONS,
} from './wordColoring.config';
import type { DebugPublishMeta } from '../../utils/debugPublish';
import { addFavorite, removeFavorite, batchCheckFavorited } from '../../utils/favorites';
import {
addFavorite,
removeFavorite,
batchCheckFavorited,
} from '../../utils/favorites';
type CardItem = { color: string; word: string };
const COLOR_MAP: Record<string, string> = {
: '#FF0000', : '#0000FF', 绿: '#00FF00', : '#FFFF00',
: '#000000', : '#FFFFFF', : '#800080', : '#FF7F00',
: '#FF69B4', : '#A52A2A', : '#808080',
: '#FF0000',
: '#0000FF',
绿: '#00FF00',
: '#FFFF00',
: '#000000',
: '#FFFFFF',
: '#800080',
: '#FF7F00',
: '#FF69B4',
: '#A52A2A',
: '#808080',
};
const MAX_WORDS = 3;
@@ -109,7 +124,13 @@ createPage(
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) => DrawServiceFactory.create(templateType, canvas, ctx, options),
) =>
DrawServiceFactory.create(
templateType,
canvas,
ctx,
options,
),
drawServiceOptions: {
title: this.data.pageTitle,
},
@@ -121,7 +142,7 @@ createPage(
});
},
// PLACEHOLDER_TS_PART3
// PLACEHOLDER_TS_PART3
/** 执行 Canvas 绘制 */
async drawCanvas() {
@@ -183,23 +204,33 @@ createPage(
const w = allWords[idx];
if (!selected.includes(w)) selected.push(w);
}
const colors = [...WATER_COLORS.basic12].sort(() => Math.random() - 0.5);
const colors = [...WATER_COLORS.basic12].sort(
() => Math.random() - 0.5,
);
const cardList = selected.map((word, i) => ({
word,
color: COLOR_MAP[word] || colors[i % colors.length].hex,
}));
const inputValue = selected.join('');
this.setData({ cardList, selectedWords: selected, inputValue }, () => {
this.drawCanvas();
});
this.setData(
{ cardList, selectedWords: selected, inputValue },
() => {
this.drawCanvas();
},
);
},
/** 清空文字卡片 */
clearCardList() {
this.setData({ cardList: [], selectedWords: [], inputValue: '', hasContent: false });
this.setData({
cardList: [],
selectedWords: [],
inputValue: '',
hasContent: false,
});
},
// PLACEHOLDER_TS_PART4
// PLACEHOLDER_TS_PART4
/** 输入框内容变化:同步 cardList */
onInputChange(e: WechatMiniprogram.Input) {
@@ -215,7 +246,12 @@ createPage(
/** 清空输入框及卡片 */
onClearInput() {
this.setData({ inputValue: '', cardList: [], selectedWords: [], hasContent: false });
this.setData({
inputValue: '',
cardList: [],
selectedWords: [],
hasContent: false,
});
if (this.drawService) {
this.drawCanvas();
}
@@ -223,9 +259,15 @@ createPage(
/** 将输入文本同步为 cardList */
syncCardsFromInput(value: string) {
const chars = [...new Set(value.split('').filter((c: string) => c.trim()))].slice(0, 6);
const oldCardMap = new Map<string, string>(this.data.cardList.map((c: CardItem) => [c.word, c.color]));
const allColors = WATER_COLORS.basic12.map((c: { hex: string }) => c.hex);
const chars = [
...new Set(value.split('').filter((c: string) => c.trim())),
].slice(0, 6);
const oldCardMap = new Map<string, string>(
this.data.cardList.map((c: CardItem) => [c.word, c.color]),
);
const allColors = WATER_COLORS.basic12.map(
(c: { hex: string }) => c.hex,
);
const usedColors = new Set<string>();
const cardList = chars.map((char) => {
@@ -241,7 +283,9 @@ createPage(
}
const available = allColors.filter((c) => !usedColors.has(c));
const shuffled = [...available].sort(() => Math.random() - 0.5);
const color = shuffled[0] || allColors[Math.floor(Math.random() * allColors.length)];
const color =
shuffled[0] ||
allColors[Math.floor(Math.random() * allColors.length)];
usedColors.add(color);
return { word: char, color };
});
@@ -256,17 +300,28 @@ createPage(
onColorDotTap(e: WechatMiniprogram.TouchEvent) {
const key = Number(e.currentTarget.dataset.key);
const color = e.currentTarget.dataset.color as string;
this.setData({ showColorPopup: true, currentKey: key, currentColor: color });
this.setData({
showColorPopup: true,
currentKey: key,
currentColor: color,
});
},
/** 分类标签点击:打开弹窗定位到对应分类 */
onCategoryTap(e: WechatMiniprogram.TouchEvent) {
const categoryId = Number(e.currentTarget.dataset.categoryId);
this.setData({ showSelectWordPopup: true, pickerCurrentTab: categoryId });
this.setData({
showSelectWordPopup: true,
pickerCurrentTab: categoryId,
});
},
onCloseColorPopup() {
this.setData({ showColorPopup: false, currentKey: 0, currentColor: '' });
this.setData({
showColorPopup: false,
currentKey: 0,
currentColor: '',
});
},
onChangeColor(e: WechatMiniprogram.CustomEvent) {
@@ -290,8 +345,12 @@ createPage(
onChangeWord(e: WechatMiniprogram.CustomEvent) {
const selectedWords = e.detail.selectedWords as string[];
const oldCardMap = new Map<string, string>(this.data.cardList.map((c: CardItem) => [c.word, c.color]));
const allColors = WATER_COLORS.basic12.map((c: { hex: string }) => c.hex);
const oldCardMap = new Map<string, string>(
this.data.cardList.map((c: CardItem) => [c.word, c.color]),
);
const allColors = WATER_COLORS.basic12.map(
(c: { hex: string }) => c.hex,
);
const usedColors = new Set<string>();
const cardList = selectedWords.map((word) => {
@@ -307,15 +366,25 @@ createPage(
}
const available = allColors.filter((c) => !usedColors.has(c));
const shuffled = [...available].sort(() => Math.random() - 0.5);
const color = shuffled[0] || allColors[Math.floor(Math.random() * allColors.length)];
const color =
shuffled[0] ||
allColors[Math.floor(Math.random() * allColors.length)];
usedColors.add(color);
return { word, color };
});
const inputValue = selectedWords.join('');
this.setData({ showSelectWordPopup: false, cardList, selectedWords, inputValue }, () => {
this.drawCanvas();
});
this.setData(
{
showSelectWordPopup: false,
cardList,
selectedWords,
inputValue,
},
() => {
this.drawCanvas();
},
);
},
/** 收藏 */
@@ -328,7 +397,10 @@ createPage(
if (next) addFavorite(id);
else removeFavorite(id);
}
wx.showToast({ title: next ? '收藏成功' : '已取消收藏', icon: 'none' });
wx.showToast({
title: next ? '收藏成功' : '已取消收藏',
icon: 'none',
});
},
async loadFavoritedMap() {