Files
doodle-mini/miniprogram/focusPages/focusDraw/registry.ts
T

692 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { BaseDrawService } from '../../core/draw/baseDraw';
import ColorShapeMatchDraw from '../shared/service/colorShapeMatchDraw';
import ShapeSymbolDraw from '../shared/service/shapeSymbolDraw';
import PositionColoringDraw from '../shared/service/positionColoringDraw';
import ColorPatternDraw from '../shared/service/colorPatternDraw';
import MatchConnectDraw from '../shared/service/matchConnectDraw';
import LineRecognitionDraw from '../shared/service/lineRecognitionDraw';
import GridReasoningDraw, {
type GridPosition,
} from '../shared/service/gridReasoningDraw';
import CodeConnectDraw from '../shared/service/codeConnectDraw';
import DotConnectDraw from '../shared/service/dotConnectDraw';
import GridDraw, { type GridGroup } from '../shared/service/gridDraw';
import {
getRandomUniqueNumberColors,
getNumberColors,
getRandomNumberColor,
NUMBER_COLORS,
WATER_COLORS,
} from '../../constants/colors';
import { SHAPE_SYMBOL_SHAPES } from '../shared/shapes/shapeSymbolShapes';
import { generateCompletePattern } from '../shared/utils/gridUtils';
import { ALL_3X3_SHAPES } from '../shared/shapes/gridShapes3x3';
import { ALL_5X5_SHAPES } from '../shared/shapes/gridShapes5x5';
import { ALL_7X7_SHAPES } from '../shared/shapes/gridShapes7x7';
import type {
ShapeTemplate,
GridCell,
GridConfig,
} from '../shared/types/gridTypes';
// ─── Types ───
export interface FocusTypeAction {
value: string;
label: string;
}
export interface FocusTypeConfig {
id: string;
title: string;
subTitle: string;
icon: string;
actionsTitle?: string;
actions?: FocusTypeAction[];
defaultMode?: string;
getTitle?: (mode: string) => string;
getSubTitle?: (mode: string) => string;
createDrawService: (
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) => BaseDrawService;
generateData: (mode?: string) => any;
}
// ─── Helper: dotConnect ───
function generateNonBacktrackingSequence(length: number): number[] {
const getPosition = (num: number): [number, number] => {
const row = Math.floor((num - 1) / 3);
const col = (num - 1) % 3;
return [row, col];
};
const sequence: number[] = [];
const used = new Set<number>();
const startNum = Math.floor(Math.random() * 9) + 1;
sequence.push(startNum);
used.add(startNum);
let prevPos = getPosition(startNum);
let prevPrevPos: [number, number] | null = null;
while (sequence.length < length) {
const candidates: number[] = [];
for (let num = 1; num <= 9; num++) {
if (used.has(num)) continue;
const currentPos = getPosition(num);
if (
prevPrevPos &&
currentPos[0] === prevPrevPos[0] &&
currentPos[1] === prevPrevPos[1]
) {
continue;
}
candidates.push(num);
}
const pool =
candidates.length > 0
? candidates
: Array.from({ length: 9 }, (_, i) => i + 1).filter(
(n) => !used.has(n),
);
if (pool.length === 0) break;
const nextNum = pool[Math.floor(Math.random() * pool.length)];
sequence.push(nextNum);
used.add(nextNum);
prevPrevPos = prevPos;
prevPos = getPosition(nextNum);
}
return sequence;
}
// ─── Helpers: gridReasoning ───
function generateRandomPositions(count: number): GridPosition[] {
const allPositions: GridPosition[] = [];
for (let y = 0; y < 3; y++) {
for (let x = 0; x < 3; x++) {
allPositions.push({ x, y });
}
}
const shuffled = [...allPositions].sort(() => Math.random() - 0.5);
return shuffled.slice(0, count);
}
function unionPositions(
pos1: GridPosition[],
pos2: GridPosition[],
): GridPosition[] {
const set = new Set<string>();
const result: GridPosition[] = [];
for (const pos of pos1) {
const key = `${pos.x},${pos.y}`;
if (!set.has(key)) {
set.add(key);
result.push(pos);
}
}
for (const pos of pos2) {
const key = `${pos.x},${pos.y}`;
if (!set.has(key)) {
set.add(key);
result.push(pos);
}
}
return result;
}
function subtractPositions(
pos1: GridPosition[],
pos2: GridPosition[],
): GridPosition[] {
const set2 = new Set(pos2.map((p) => `${p.x},${p.y}`));
return pos1.filter((p) => !set2.has(`${p.x},${p.y}`));
}
// ─── Helpers: gridDrawing ───
function generateUniqueGroups(
allShapes: ShapeTemplate[],
count: number,
config: GridConfig,
): GridGroup[] {
const groups: GridGroup[] = [];
const usedPatterns = new Set<string>();
const availableShapes = [...allShapes];
for (let i = availableShapes.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[availableShapes[i], availableShapes[j]] = [
availableShapes[j],
availableShapes[i],
];
}
for (let i = 0; i < count; i++) {
const shapeIndex = i % availableShapes.length;
const shape = availableShapes[shapeIndex];
const cells = generateCompletePattern(shape, config);
const sig = cells
.map((c: GridCell) => `${c.x},${c.y},${c.color}`)
.sort()
.join('|');
if (
usedPatterns.has(sig) &&
usedPatterns.size < Math.min(count, allShapes.length)
) {
let found = false;
for (const nextShape of availableShapes) {
const nextCells = generateCompletePattern(nextShape, config);
const nextSig = nextCells
.map((c: GridCell) => `${c.x},${c.y},${c.color}`)
.sort()
.join('|');
if (!usedPatterns.has(nextSig)) {
groups.push({ filledCells: nextCells, emptyCells: [] });
usedPatterns.add(nextSig);
found = true;
break;
}
}
if (!found) {
groups.push({ filledCells: cells, emptyCells: [] });
}
} else {
groups.push({ filledCells: cells, emptyCells: [] });
usedPatterns.add(sig);
}
}
return groups;
}
// ─── Data Generators ───
function generateColorShapeMatchData() {
const shapes = [...SHAPE_SYMBOL_SHAPES].filter((s) => s.id !== 'cross');
const selectedShapes = shapes.sort(() => Math.random() - 0.5).slice(0, 4);
const colors = getRandomUniqueNumberColors(4);
const legendItems = selectedShapes.map((shape, i) => ({
shapeId: shape.id,
color: colors[i],
}));
const allColors: (string | null)[] = new Array(25).fill(null);
const positions = Array.from({ length: 25 }, (_, i) => i);
const shuffledPos = [...positions].sort(() => Math.random() - 0.5);
for (let i = 0; i < 4; i++) allColors[shuffledPos[i]] = colors[i];
for (let i = 4; i < 25; i++)
allColors[shuffledPos[i]] =
colors[Math.floor(Math.random() * colors.length)];
const practiceGrid: Array<Array<string | null>> = [];
for (let row = 0; row < 5; row++) {
practiceGrid.push(
Array.from({ length: 5 }, (_, col) => allColors[row * 5 + col]),
);
}
return { legendItems, practiceGrid };
}
function generateShapeSymbolData() {
const shuffled = [...SHAPE_SYMBOL_SHAPES].sort(() => Math.random() - 0.5);
const selectedShapes = shuffled.slice(0, 4);
const symbols = ['+', '-', '×', '✓'];
const shuffledSymbols = [...symbols].sort(() => Math.random() - 0.5);
const shapeColorMap = new Map<string, string>();
const usedColors = new Set<string>();
const legendMapping: Array<{
shapeId: string;
symbol: string;
color: string;
}> = [];
for (let i = 0; i < 4; i++) {
const shape = selectedShapes[i];
let color: string;
if (shapeColorMap.has(shape.id)) {
color = shapeColorMap.get(shape.id)!;
} else {
do {
color = getRandomNumberColor();
} while (usedColors.has(color));
shapeColorMap.set(shape.id, color);
usedColors.add(color);
}
legendMapping.push({
shapeId: shape.id,
symbol: shuffledSymbols[i],
color,
});
}
const practiceRows: Array<
Array<{ shapeId: string | null; symbol: string | null }>
> = [];
for (let g = 0; g < 4; g++) {
const shapeRow = Array.from({ length: 8 }, () => {
const rs =
selectedShapes[
Math.floor(Math.random() * selectedShapes.length)
];
return { shapeId: rs.id, symbol: null };
});
practiceRows.push(shapeRow);
practiceRows.push(
Array.from({ length: 8 }, () => ({ shapeId: null, symbol: null })),
);
}
return {
legendMapping,
practiceRows,
shapeColorMap: Object.fromEntries(shapeColorMap),
};
}
function generatePositionColoringData() {
const allImageIndices = Array.from({ length: 33 }, (_, i) => i + 1);
const shuffled = [...allImageIndices].sort(() => Math.random() - 0.5);
const selectedImages = shuffled.slice(0, 9);
const referenceGrid: number[] = [...selectedImages];
const taskImageIndices = [...selectedImages].sort(
() => Math.random() - 0.5,
);
const tasks = taskImageIndices.map((imageIndex) => {
const gridIndex = referenceGrid.indexOf(imageIndex);
return {
imageIndex,
position: { x: gridIndex % 3, y: Math.floor(gridIndex / 3) },
};
});
return { referenceGrid, tasks };
}
function generateColorPatternData() {
const allShapes = [...SHAPE_SYMBOL_SHAPES].sort(() => Math.random() - 0.5);
const selectedShapes = allShapes.slice(0, 8);
const allColors = getNumberColors();
const groups: Array<{ shapeId: string; colors: Array<string | null> }> = [];
for (let gi = 0; gi < 8; gi++) {
const shuffledColors = [...allColors].sort(() => Math.random() - 0.5);
const selectedColors = shuffledColors.slice(0, 2);
const patternColors: string[] = [];
for (let i = 0; i < 4; i++) {
patternColors.push(
selectedColors[
Math.floor(Math.random() * selectedColors.length)
],
);
}
if (new Set(patternColors).size === 1) {
const other = selectedColors.filter((c) => c !== patternColors[0]);
if (other.length > 0) {
patternColors[Math.floor(Math.random() * 4)] =
other[Math.floor(Math.random() * other.length)];
}
}
groups.push({
shapeId: selectedShapes[gi].id,
colors: [...patternColors, ...new Array(6).fill(null)],
});
}
return { groups };
}
function generateMatchConnectData() {
const allImageIndices = Array.from({ length: 33 }, (_, i) => i + 1);
const shuffled = [...allImageIndices].sort(() => Math.random() - 0.5);
const selectedImages = shuffled.slice(0, 5);
const referenceSequence = [...selectedImages];
const boxes = Array.from({ length: 6 }, () => ({
imageIndices: [...selectedImages],
}));
return { referenceSequence, boxes };
}
function generateLineRecognitionData() {
const LINE_TYPES: Array<{ type: string; name: string }> = [
{ type: 'straight', name: '直线' },
{ type: 'dashed', name: '虚线' },
{ type: 'wavy', name: '波浪线' },
{ type: 'zigzag', name: '锯齿线' },
{ type: 'spiral', name: '电话线' },
];
const colorKeys = Object.keys(NUMBER_COLORS)
.map(Number)
.filter((k) => k !== 11 && k !== 12);
const shuffledColors = [...colorKeys].sort(() => Math.random() - 0.5);
const selectedColorKeys = shuffledColors.slice(0, 5);
const shuffledLineTypes = [...LINE_TYPES].sort(() => Math.random() - 0.5);
const referenceLines = shuffledLineTypes.map((lt, i) => ({
type: lt.type,
name: lt.name,
color: NUMBER_COLORS[selectedColorKeys[i]],
}));
const practiceBlocks: Array<{
color: string;
lineType: string;
x: number;
y: number;
}> = [];
for (let row = 0; row < 8; row++) {
for (let col = 0; col < 2; col++) {
const sel =
referenceLines[
Math.floor(Math.random() * referenceLines.length)
];
practiceBlocks.push({
color: sel.color,
lineType: sel.type,
x: (Math.random() - 0.5) * 40,
y: (Math.random() - 0.5) * 16,
});
}
}
return { referenceLines, practiceBlocks };
}
function generateGridReasoningData(mode?: string) {
const operatorType = mode || 'mixed';
const colorKeys = Object.keys(NUMBER_COLORS).map(Number);
const selectedColor =
NUMBER_COLORS[colorKeys[Math.floor(Math.random() * colorKeys.length)]];
const rows: Array<{
grid1: GridPosition[];
grid2: GridPosition[];
operator: '+' | '-';
result: GridPosition[];
}> = [];
for (let i = 0; i < 5; i++) {
let operator: '+' | '-';
if (operatorType === 'addition') operator = '+';
else if (operatorType === 'subtraction') operator = '-';
else operator = Math.random() > 0.5 ? '+' : '-';
const grid1Count =
operator === '-'
? Math.floor(Math.random() * 8) + 2
: Math.floor(Math.random() * 9) + 1;
const grid1 = generateRandomPositions(grid1Count);
let grid2: GridPosition[];
let result: GridPosition[];
if (operator === '+') {
grid2 = generateRandomPositions(Math.floor(Math.random() * 9) + 1);
result = unionPositions(grid1, grid2);
} else {
const maxG2 = grid1Count - 1;
grid2 = generateRandomPositions(
Math.floor(Math.random() * maxG2) + 1,
);
result = subtractPositions(grid1, grid2);
}
rows.push({ grid1, grid2, operator, result });
}
const answerOptions = rows.map((r, i) => ({
positions: r.result,
rowIndex: i,
}));
answerOptions.sort(() => Math.random() - 0.5);
return { rows, answerOptions, color: selectedColor };
}
function generateCodeConnectData() {
const startOptions = [1, 2, 3];
const startNumber =
startOptions[Math.floor(Math.random() * startOptions.length)];
const numbers = Array.from({ length: 8 }, (_, i) => startNumber + i);
const colorIndices = Array.from(
{ length: WATER_COLORS.extended24.length },
(_, i) => i,
);
const shuffledIdx = [...colorIndices]
.sort(() => Math.random() - 0.5)
.slice(0, 8);
const colorMap = numbers.map((num, i) => ({
number: num,
color: WATER_COLORS.extended24[shuffledIdx[i]].hex,
}));
const groups: Array<{
sequence: number[];
dots: Array<{ number: number; color: string; angle: number }>;
}> = [];
for (let i = 0; i < 4; i++) {
const sequence = [...numbers]
.sort(() => Math.random() - 0.5)
.slice(0, 5);
const dots = numbers.map((num, j) => {
const angle = (j / 8) * Math.PI * 2 - Math.PI / 2;
const info = colorMap.find((m) => m.number === num);
return { number: num, color: info?.color || '#000', angle };
});
groups.push({ sequence, dots });
}
return { colorMap, startNumber, groups };
}
function generateDotConnectData() {
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const colors = getRandomUniqueNumberColors(9);
const colorMap = numbers.map((num, i) => ({
number: num,
color: colors[i],
}));
const groups = Array.from({ length: 9 }, () => {
const seqLen = Math.floor(Math.random() * 3) + 4;
return { sequence: generateNonBacktrackingSequence(seqLen) };
});
return { colorMap, groups };
}
function generateGridDrawingData(mode?: string) {
const m = mode || '3x3';
let allShapes: ShapeTemplate[];
let config: GridConfig;
let groupsPerRow: number;
let totalRows: number;
if (m === '5x5') {
allShapes = ALL_5X5_SHAPES;
config = { rows: 5, cols: 5 };
groupsPerRow = 2;
totalRows = 4;
} else if (m === '7x7') {
allShapes = ALL_7X7_SHAPES;
config = { rows: 7, cols: 7 };
groupsPerRow = 1;
totalRows = 2;
} else {
allShapes = ALL_3X3_SHAPES;
config = { rows: 3, cols: 3 };
groupsPerRow = 2;
totalRows = 4;
}
const totalGroups = groupsPerRow * totalRows;
const groups = generateUniqueGroups(allShapes, totalGroups, config);
return { config, groups, groupsPerRow, totalRows };
}
// ─── Registry ───
const GRID_DRAWING_SUBTITLES: Record<string, string> = {
'3x3': '简单有趣,培养专注力',
'5x5': '创意挑战,提升观察力',
'7x7': '大师挑战,锻炼耐心',
};
export const FOCUS_TYPE_CONFIGS: FocusTypeConfig[] = [
{
id: 'color-shape-match',
title: '根据颜色画图形',
subTitle: '根据颜色画出对应的图形',
icon: '🎯',
createDrawService: (canvas, ctx, opts) =>
new ColorShapeMatchDraw(canvas, ctx, opts),
generateData: generateColorShapeMatchData,
},
{
id: 'shape-symbol',
title: '图形符号配对',
subTitle: '根据图形画对应的符号',
icon: '🔗',
createDrawService: (canvas, ctx, opts) =>
new ShapeSymbolDraw(canvas, ctx, opts),
generateData: generateShapeSymbolData,
},
{
id: 'position-coloring',
title: '方位涂涂乐',
subTitle: '观察卡片位置,在对应的方格中涂上颜色',
icon: '📍',
createDrawService: (canvas, ctx, opts) =>
new PositionColoringDraw(canvas, ctx, opts),
generateData: generatePositionColoringData,
},
{
id: 'color-pattern',
title: '颜色找规律',
subTitle: '观察颜色规律,在空白图形中涂上颜色',
icon: '🎨',
createDrawService: (canvas, ctx, opts) =>
new ColorPatternDraw(canvas, ctx, opts),
generateData: generateColorPatternData,
},
{
id: 'match-connect',
title: '连连看',
subTitle: '快来根据物品连一连吧!',
icon: '🔗',
createDrawService: (canvas, ctx, opts) =>
new MatchConnectDraw(canvas, ctx, opts),
generateData: generateMatchConnectData,
},
{
id: 'line-recognition',
title: '线条识别',
subTitle: '认识不同线条,画出颜色对应的线条',
icon: '📏',
createDrawService: (canvas, ctx, opts) =>
new LineRecognitionDraw(canvas, ctx, opts),
generateData: generateLineRecognitionData,
},
{
id: 'grid-reasoning',
title: '方格推理',
subTitle: '仔细观察,推理出合并方格并连线',
icon: '🧩',
actionsTitle: '选择运算',
actions: [
{ value: 'addition', label: '加法运算' },
{ value: 'subtraction', label: '减法运算' },
{ value: 'mixed', label: '混合运算' },
],
defaultMode: 'mixed',
createDrawService: (canvas, ctx, opts) =>
new GridReasoningDraw(canvas, ctx, opts),
generateData: generateGridReasoningData,
},
{
id: 'code-connect',
title: '译码连线',
subTitle: '按照数字顺序,将数字对应的颜色连起来',
icon: '🔢',
createDrawService: (canvas, ctx, opts) =>
new CodeConnectDraw(canvas, ctx, opts),
generateData: generateCodeConnectData,
},
{
id: 'dot-connect',
title: '数字点连线',
subTitle: '按数字顺序连点成图',
icon: '🔗',
createDrawService: (canvas, ctx, opts) =>
new DotConnectDraw(canvas, ctx, opts),
generateData: generateDotConnectData,
},
{
id: 'grid-drawing',
title: '格子仿画',
subTitle: '在网格中填充颜色,形成各种形状',
icon: '🎨',
actionsTitle: '选择难度',
actions: [
{ value: '3x3', label: '3×3' },
{ value: '5x5', label: '5×5' },
{ value: '7x7', label: '7×7' },
],
defaultMode: '3x3',
getTitle: (mode: string) => {
const labels: Record<string, string> = {
'3x3': '3×3',
'5x5': '5×5',
'7x7': '7×7',
};
return `格子仿画 ${labels[mode] || '3×3'}`;
},
getSubTitle: (mode: string) =>
GRID_DRAWING_SUBTITLES[mode] || GRID_DRAWING_SUBTITLES['3x3'],
createDrawService: (canvas, ctx, opts) =>
new GridDraw(canvas, ctx, opts),
generateData: generateGridDrawingData,
},
];
/**
* 通过路由 ID 查找类型配置
* 处理 grid-drawing-3x3 → grid-drawing + mode=3x3 等别名
*/
export function findTypeByRouteId(
routeId: string,
): { typeConfig: FocusTypeConfig; mode?: string } | null {
const direct = FOCUS_TYPE_CONFIGS.find((t) => t.id === routeId);
if (direct) return { typeConfig: direct };
if (routeId.startsWith('grid-drawing-')) {
const mode = routeId.replace('grid-drawing-', '');
const cfg = FOCUS_TYPE_CONFIGS.find((t) => t.id === 'grid-drawing');
if (cfg) return { typeConfig: cfg, mode };
}
return { typeConfig: FOCUS_TYPE_CONFIGS[0] };
}