feat: 样式修改、13 字母半表、三字母精练开发
This commit is contained in:
@@ -37,3 +37,4 @@ export const ALPHABET_BY_LETTER: Record<string, AlphabetEntry> = {
|
||||
};
|
||||
|
||||
export const LETTERS_UPPER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
|
||||
export const LETTERS_PAIRS = LETTERS_UPPER.map((l) => `${l}${l.toLowerCase()}`);
|
||||
|
||||
@@ -4,6 +4,8 @@ import singleLetterDraw from './singleLetterDraw';
|
||||
import CasePairingDraw from './casePairingDraw';
|
||||
import TwoColumnDraw from './twoColumnDraw';
|
||||
import UpperLowerDraw from './upperLowerDraw';
|
||||
import SingleLineDraw from './singleLineDraw';
|
||||
import TripleDraw from './tripleDraw';
|
||||
|
||||
/**
|
||||
* 字母描红绘制分发层。
|
||||
@@ -16,6 +18,8 @@ export default class LetterTracingDraw extends BaseDrawService {
|
||||
'letter-tracing-upper-lower': UpperLowerDraw,
|
||||
'letter-tracing-case-pairing': CasePairingDraw,
|
||||
'letter-tracing-two-column': TwoColumnDraw,
|
||||
'letter-tracing-single-line': SingleLineDraw,
|
||||
'letter-tracing-triple': TripleDraw,
|
||||
}[data.mode];
|
||||
|
||||
const delegate = new DrawClass(this.canvas, this.ctx, this.options);
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { BaseDrawService } from '../../../core/draw/baseDraw';
|
||||
import type {
|
||||
LetterTracingData,
|
||||
SingleLineRow,
|
||||
} from '../generators/letter-tracing-generator';
|
||||
import {
|
||||
fontFamilyOf,
|
||||
PRINT_CLEARLY_DASHED,
|
||||
} from '../data/fontProfiles';
|
||||
import {
|
||||
calcLetterFontSizes,
|
||||
drawFourLineGrid,
|
||||
drawLetterInFourLineGrid,
|
||||
FOUR_LINE_GRID_H,
|
||||
loadLetterFont,
|
||||
type LetterFontSizes,
|
||||
} from './drawTools';
|
||||
|
||||
const SLOTS_PER_ROW = 6;
|
||||
const PAIR_GAP = 25;
|
||||
|
||||
type SingleLineData = Extract<
|
||||
LetterTracingData,
|
||||
{ mode: 'letter-tracing-single-line' }
|
||||
>;
|
||||
|
||||
/**
|
||||
* 单字母逐行绘制:
|
||||
* 13 行四线三格横跨整页宽度,每行划分为 6 组位置。
|
||||
* 前 4 组展示大小写字母(Aa),后 2 组空白供用户书写。
|
||||
* 颜色递进:黑色 → 浅红 → 半透明浅红 → 虚线字体+半透明浅红。
|
||||
*/
|
||||
export default class SingleLineDraw extends BaseDrawService {
|
||||
private letterSizes: LetterFontSizes =
|
||||
calcLetterFontSizes(FOUR_LINE_GRID_H);
|
||||
|
||||
private dashedSizes: LetterFontSizes =
|
||||
calcLetterFontSizes(FOUR_LINE_GRID_H, PRINT_CLEARLY_DASHED);
|
||||
|
||||
async draw(data: SingleLineData) {
|
||||
this.prepareDraw();
|
||||
await Promise.all([
|
||||
this.drawHeaderAndDivider(),
|
||||
loadLetterFont(PRINT_CLEARLY_DASHED),
|
||||
]);
|
||||
this.drawContent(data.rows);
|
||||
this.drawPrintFooter();
|
||||
}
|
||||
|
||||
private drawContent(rows: SingleLineRow[]) {
|
||||
const M = 24;
|
||||
const lineW = this.canvasWidth - M * 2;
|
||||
const rowCount = rows.length;
|
||||
|
||||
const footerReserve = 56;
|
||||
const availableH =
|
||||
this.canvasHeight - this.currentY - footerReserve - 12;
|
||||
const rowGap = Math.max(
|
||||
4,
|
||||
(availableH - rowCount * FOUR_LINE_GRID_H) / (rowCount + 1),
|
||||
);
|
||||
const y0 = this.currentY + rowGap;
|
||||
|
||||
const slotW = lineW / SLOTS_PER_ROW;
|
||||
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const y = y0 + i * (FOUR_LINE_GRID_H + rowGap);
|
||||
|
||||
drawFourLineGrid(this.ctx, M, y, lineW, FOUR_LINE_GRID_H);
|
||||
|
||||
const row = rows[i];
|
||||
for (let j = 0; j < row.cells.length; j++) {
|
||||
const cell = row.cells[j];
|
||||
if (!cell) continue;
|
||||
|
||||
const pairCx = M + j * slotW + slotW / 2;
|
||||
const isDashed = cell.style === 'dashed';
|
||||
const sizes = isDashed ? this.dashedSizes : this.letterSizes;
|
||||
const fontFamily = fontFamilyOf(
|
||||
isDashed ? PRINT_CLEARLY_DASHED : sizes._profile,
|
||||
);
|
||||
|
||||
this.ctx.save();
|
||||
this.ctx.globalAlpha = cell.alpha;
|
||||
|
||||
drawLetterInFourLineGrid(
|
||||
this.ctx,
|
||||
cell.upper,
|
||||
pairCx - PAIR_GAP / 2,
|
||||
y,
|
||||
FOUR_LINE_GRID_H,
|
||||
sizes,
|
||||
{ fontFamily, color: cell.color },
|
||||
);
|
||||
|
||||
drawLetterInFourLineGrid(
|
||||
this.ctx,
|
||||
cell.lower,
|
||||
pairCx + PAIR_GAP / 2,
|
||||
y,
|
||||
FOUR_LINE_GRID_H,
|
||||
sizes,
|
||||
{ fontFamily, color: cell.color },
|
||||
);
|
||||
|
||||
this.ctx.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { BaseDrawService } from '../../../core/draw/baseDraw';
|
||||
import type {
|
||||
LetterTracingData,
|
||||
TripleSection,
|
||||
SingleLineRow,
|
||||
} from '../generators/letter-tracing-generator';
|
||||
import { fontFamilyOf, PRINT_CLEARLY_DASHED } from '../data/fontProfiles';
|
||||
import {
|
||||
calcLetterFontSizes,
|
||||
drawFourLineGrid,
|
||||
drawLetterInFourLineGrid,
|
||||
FOUR_LINE_GRID_H,
|
||||
loadLetterFont,
|
||||
type LetterFontSizes,
|
||||
} from './drawTools';
|
||||
|
||||
// 每行可容纳的格子数(用于三字母精练模板,一行展示 6 个位置)
|
||||
const SLOTS_PER_ROW = 6;
|
||||
// 大小写字母配对间的间隔(px,字母间距)
|
||||
const PAIR_GAP = 25;
|
||||
// 同一字母区域内相邻两行四线三格之间的间距(px)
|
||||
const TRIPLE_ROW_GAP = 10;
|
||||
// 相邻两个字母区域之间的间距(px),大于 TRIPLE_ROW_GAP
|
||||
const TRIPLE_SECTION_GAP = 20;
|
||||
|
||||
type TripleData = Extract<LetterTracingData, { mode: 'letter-tracing-triple' }>;
|
||||
|
||||
/**
|
||||
* 三字母精练绘制:
|
||||
* 3 个字母区域,每区域 4 行四线三格。
|
||||
* 前两行展示完整颜色递进(黑→浅红→半透明浅红→虚线),
|
||||
* 后两行只展示黑色+浅红色两组,其余空白供用户书写。
|
||||
* 区域之间间距大于行间距。
|
||||
*/
|
||||
export default class TripleDraw extends BaseDrawService {
|
||||
private letterSizes: LetterFontSizes =
|
||||
calcLetterFontSizes(FOUR_LINE_GRID_H);
|
||||
|
||||
private dashedSizes: LetterFontSizes = calcLetterFontSizes(
|
||||
FOUR_LINE_GRID_H,
|
||||
PRINT_CLEARLY_DASHED,
|
||||
);
|
||||
|
||||
async draw(data: TripleData) {
|
||||
this.prepareDraw();
|
||||
await Promise.all([
|
||||
this.drawHeaderAndDivider(),
|
||||
loadLetterFont(PRINT_CLEARLY_DASHED),
|
||||
]);
|
||||
this.drawContent(data.sections);
|
||||
this.drawPrintFooter();
|
||||
}
|
||||
|
||||
private drawContent(sections: TripleSection[]) {
|
||||
const M = 24;
|
||||
const lineW = this.canvasWidth - M * 2;
|
||||
const slotW = lineW / SLOTS_PER_ROW;
|
||||
|
||||
let y = this.currentY + 10;
|
||||
|
||||
for (let s = 0; s < sections.length; s++) {
|
||||
const section = sections[s];
|
||||
|
||||
for (let r = 0; r < section.rows.length; r++) {
|
||||
drawFourLineGrid(this.ctx, M, y, lineW, FOUR_LINE_GRID_H);
|
||||
this.drawRow(section.rows[r], M, y, slotW);
|
||||
y += FOUR_LINE_GRID_H;
|
||||
if (r < section.rows.length - 1) {
|
||||
y += TRIPLE_ROW_GAP;
|
||||
}
|
||||
}
|
||||
|
||||
if (s < sections.length - 1) {
|
||||
y += TRIPLE_SECTION_GAP;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private drawRow(
|
||||
row: SingleLineRow,
|
||||
marginX: number,
|
||||
y: number,
|
||||
slotW: number,
|
||||
) {
|
||||
for (let j = 0; j < row.cells.length; j++) {
|
||||
const cell = row.cells[j];
|
||||
if (!cell) continue;
|
||||
|
||||
const pairCx = marginX + j * slotW + slotW / 2;
|
||||
const isDashed = cell.style === 'dashed';
|
||||
const sizes = isDashed ? this.dashedSizes : this.letterSizes;
|
||||
const fontFamily = fontFamilyOf(
|
||||
isDashed ? PRINT_CLEARLY_DASHED : sizes._profile,
|
||||
);
|
||||
|
||||
this.ctx.save();
|
||||
this.ctx.globalAlpha = cell.alpha;
|
||||
|
||||
drawLetterInFourLineGrid(
|
||||
this.ctx,
|
||||
cell.upper,
|
||||
pairCx - PAIR_GAP / 2,
|
||||
y,
|
||||
FOUR_LINE_GRID_H,
|
||||
sizes,
|
||||
{ fontFamily, color: cell.color },
|
||||
);
|
||||
|
||||
drawLetterInFourLineGrid(
|
||||
this.ctx,
|
||||
cell.lower,
|
||||
pairCx + PAIR_GAP / 2,
|
||||
y,
|
||||
FOUR_LINE_GRID_H,
|
||||
sizes,
|
||||
{ fontFamily, color: cell.color },
|
||||
);
|
||||
|
||||
this.ctx.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ export type LetterTracingMode =
|
||||
| 'letter-tracing-case-pairing'
|
||||
/** 4 分栏对照:半组字母,左栏大写、右栏小写,每行 4 个 */
|
||||
| 'letter-tracing-two-column'
|
||||
/** 5 三字母精练:每页聚焦 3 个字母,各含大写行 + 小写行 */
|
||||
/** 5 三字母精练:每页聚焦 3 个字母,每字母 4 行(2 行示范 + 2 行简练) */
|
||||
| 'letter-tracing-triple'
|
||||
/** 6 单字母逐行:每行一个字母,13 字母半表(A–M 或 N–Z) */
|
||||
| 'letter-tracing-single-line';
|
||||
@@ -31,7 +31,7 @@ export interface LetterTracingGeneratorConfig {
|
||||
/** 分栏对照 / 单字母逐行:练习 A–M 或 N–Z */
|
||||
alphabetHalf?: AlphabetHalf;
|
||||
/** 三字母精练:当前页聚焦的 3 个字母(如 ['A','B','C']) */
|
||||
tripleLetters?: [string, string, string];
|
||||
tripleLetters?: string[];
|
||||
}
|
||||
|
||||
/** 单个描红格子的数据 */
|
||||
@@ -49,6 +49,28 @@ export interface TracingRow {
|
||||
cells: TracingCell[];
|
||||
}
|
||||
|
||||
/** 单字母逐行:一行中一个大小写配对的展示格子 */
|
||||
export interface SingleLineCell {
|
||||
upper: string;
|
||||
lower: string;
|
||||
/** 'solid' 正常渲染 | 'dashed' 使用虚线字体 */
|
||||
style: 'solid' | 'dashed';
|
||||
color: string;
|
||||
alpha: number;
|
||||
}
|
||||
|
||||
/** 单字母逐行:一行数据(6 个位置,前 4 个有字母,后 2 个空) */
|
||||
export interface SingleLineRow {
|
||||
letter: string;
|
||||
cells: (SingleLineCell | null)[];
|
||||
}
|
||||
|
||||
/** 三字母精练:一个字母区域(4 行),前两行完整示范,后两行简练(黑+浅红) */
|
||||
export interface TripleSection {
|
||||
letter: string;
|
||||
rows: SingleLineRow[];
|
||||
}
|
||||
|
||||
/** Highlight It 格子数据(3×6 网格,混入目标字母) */
|
||||
export interface HighlightGrid {
|
||||
rows: string[][];
|
||||
@@ -88,6 +110,14 @@ export type LetterTracingData =
|
||||
leftRows: TracingRow[];
|
||||
rightRows: TracingRow[];
|
||||
}
|
||||
| {
|
||||
mode: 'letter-tracing-single-line';
|
||||
rows: SingleLineRow[];
|
||||
}
|
||||
| {
|
||||
mode: 'letter-tracing-triple';
|
||||
sections: TripleSection[];
|
||||
}
|
||||
| {
|
||||
mode: 'letter-tracing-upper-lower';
|
||||
upperRows: string[][];
|
||||
@@ -200,6 +230,35 @@ function overviewRowsFrom(letters: string[]): string[][] {
|
||||
];
|
||||
}
|
||||
|
||||
const _COLOR_BLACK = '#1a1a1a';
|
||||
const _COLOR_LIGHT_RED = '#FF8E8E';
|
||||
|
||||
/** 构建 6 slot 的单行字母数据:前 4 格有字母(颜色递进),后 2 格空白 */
|
||||
function buildSingleLineCells(L: string): (SingleLineCell | null)[] {
|
||||
const lower = L.toLowerCase();
|
||||
return [
|
||||
{ upper: L, lower, style: 'solid', color: _COLOR_BLACK, alpha: 1 },
|
||||
{ upper: L, lower, style: 'solid', color: _COLOR_LIGHT_RED, alpha: 1 },
|
||||
{ upper: L, lower, style: 'solid', color: _COLOR_LIGHT_RED, alpha: 0.5 },
|
||||
{ upper: L, lower, style: 'dashed', color: _COLOR_LIGHT_RED, alpha: 0.5 },
|
||||
null,
|
||||
null,
|
||||
];
|
||||
}
|
||||
|
||||
/** 构建 6 slot 的简练行数据:前 2 格有字母(黑色+浅红色),后 4 格空白 */
|
||||
function buildSimpleCells(L: string): (SingleLineCell | null)[] {
|
||||
const lower = L.toLowerCase();
|
||||
return [
|
||||
{ upper: L, lower, style: 'solid', color: _COLOR_BLACK, alpha: 1 },
|
||||
{ upper: L, lower, style: 'solid', color: _COLOR_LIGHT_RED, alpha: 1 },
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据配置生成字母描红数据
|
||||
* - picture-tracing:教学区 + 6 行描红
|
||||
@@ -273,6 +332,39 @@ export function generateLetterTracing(
|
||||
};
|
||||
}
|
||||
|
||||
if (mode === 'letter-tracing-triple') {
|
||||
const letters = config.tripleLetters ?? ['A', 'B', 'C'];
|
||||
const sections: TripleSection[] = letters.map((L) => {
|
||||
const filledRow: SingleLineRow = {
|
||||
letter: L,
|
||||
cells: buildSingleLineCells(L),
|
||||
};
|
||||
const simpleRow: SingleLineRow = {
|
||||
letter: L,
|
||||
cells: buildSimpleCells(L),
|
||||
};
|
||||
return {
|
||||
letter: L,
|
||||
rows: [filledRow, filledRow, simpleRow, simpleRow],
|
||||
};
|
||||
});
|
||||
return { mode: 'letter-tracing-triple', sections };
|
||||
}
|
||||
|
||||
if (mode === 'letter-tracing-single-line') {
|
||||
const half =
|
||||
config.alphabetHalf === 'N-Z'
|
||||
? LETTERS_UPPER.slice(13)
|
||||
: LETTERS_UPPER.slice(0, 13);
|
||||
|
||||
const rows: SingleLineRow[] = half.map((L) => ({
|
||||
letter: L,
|
||||
cells: buildSingleLineCells(L),
|
||||
}));
|
||||
|
||||
return { mode: 'letter-tracing-single-line', rows };
|
||||
}
|
||||
|
||||
// letter-tracing-upper-lower
|
||||
return {
|
||||
mode: 'letter-tracing-upper-lower',
|
||||
|
||||
Reference in New Issue
Block a user