feat: 完成三类字母描红开发
This commit is contained in:
@@ -325,7 +325,7 @@ export const ALL_WORKSHEETS: WorksheetDefinition[] = [
|
|||||||
{
|
{
|
||||||
id: 'letter-tracing-upper-lower',
|
id: 'letter-tracing-upper-lower',
|
||||||
title: '字母总览',
|
title: '字母总览',
|
||||||
desc: 'Uppercase / Lowercase 分区,9+9+8 行',
|
desc: 'Uppercase / Lowercase 分区,每区四行(7+7+6+6)',
|
||||||
category: 'english',
|
category: 'english',
|
||||||
page: 'letterTracing',
|
page: 'letterTracing',
|
||||||
subpackage: 'englishPages',
|
subpackage: 'englishPages',
|
||||||
@@ -387,6 +387,72 @@ export const ALL_WORKSHEETS: WorksheetDefinition[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
sheet(
|
||||||
|
{
|
||||||
|
id: 'letter-tracing-triple',
|
||||||
|
title: '三字母精练',
|
||||||
|
desc: '每页聚焦 3 个字母,大写 + 小写深度书写',
|
||||||
|
category: 'english',
|
||||||
|
page: 'letterTracing',
|
||||||
|
subpackage: 'englishPages',
|
||||||
|
icon: '🔤',
|
||||||
|
img: '',
|
||||||
|
ageRange: [4, 7],
|
||||||
|
difficulty: 'basic',
|
||||||
|
previewBg: '#e8f4ff',
|
||||||
|
previewText: 'ABC',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subcategory: 'letter-tracing',
|
||||||
|
template: 'tracing-writing',
|
||||||
|
generator: 'letter-tracing',
|
||||||
|
sortOrder: 46,
|
||||||
|
layoutConfig: {
|
||||||
|
...L.trace,
|
||||||
|
instructionText: '每页 3 个字母,逐字精练',
|
||||||
|
},
|
||||||
|
generatorConfig: {
|
||||||
|
mode: 'triple-focus',
|
||||||
|
letterCase: 'both',
|
||||||
|
repetitions: 5,
|
||||||
|
fadePattern: 'gradient',
|
||||||
|
tripleLetters: ['A', 'B', 'C'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
sheet(
|
||||||
|
{
|
||||||
|
id: 'letter-tracing-single-line',
|
||||||
|
title: '单字母逐行',
|
||||||
|
desc: '每行一个字母,13 字母半表逐字练习',
|
||||||
|
category: 'english',
|
||||||
|
page: 'letterTracing',
|
||||||
|
subpackage: 'englishPages',
|
||||||
|
icon: '📝',
|
||||||
|
img: '',
|
||||||
|
ageRange: [4, 7],
|
||||||
|
difficulty: 'basic',
|
||||||
|
previewBg: '#e8f4ff',
|
||||||
|
previewText: 'A–M',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subcategory: 'letter-tracing',
|
||||||
|
template: 'tracing-writing',
|
||||||
|
generator: 'letter-tracing',
|
||||||
|
sortOrder: 47,
|
||||||
|
layoutConfig: {
|
||||||
|
...L.trace,
|
||||||
|
instructionText: '每行一个字母,逐字反复练习',
|
||||||
|
},
|
||||||
|
generatorConfig: {
|
||||||
|
mode: 'single-line',
|
||||||
|
letterCase: 'both',
|
||||||
|
alphabetHalf: 'A-M',
|
||||||
|
repetitions: 5,
|
||||||
|
fadePattern: 'first-only',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
export const MATH_FUNCTION_TYPES: MathFunctionType[] = ALL_WORKSHEETS.filter(
|
export const MATH_FUNCTION_TYPES: MathFunctionType[] = ALL_WORKSHEETS.filter(
|
||||||
|
|||||||
@@ -10,41 +10,58 @@ import {
|
|||||||
type LetterTracingMode,
|
type LetterTracingMode,
|
||||||
} from '../shared/generators/letter-tracing-generator';
|
} from '../shared/generators/letter-tracing-generator';
|
||||||
import { LETTERS_UPPER } from '../shared/data/alphabet';
|
import { LETTERS_UPPER } from '../shared/data/alphabet';
|
||||||
|
import {
|
||||||
|
DEFAULT_LETTER_PROFILE,
|
||||||
|
getFontProfile,
|
||||||
|
type FontProfile,
|
||||||
|
} from '../shared/data/fontProfiles';
|
||||||
import { loadLetterFont } from '../shared/draw/drawTools';
|
import { loadLetterFont } from '../shared/draw/drawTools';
|
||||||
|
|
||||||
/** 各 worksheetId 对应的页面标题和描述 */
|
/** 练习模式定义(顺序与产品文档一致),同时用于模式选择器和页面元信息 */
|
||||||
const PAGE_META: Record<string, { title: string; desc: string }> = {
|
const MODES = [
|
||||||
'letter-tracing-single': {
|
{
|
||||||
title: '看图描红',
|
id: 'letter-tracing-single',
|
||||||
desc: '配图、例句与六行描红',
|
label: '看图识字',
|
||||||
|
desc: '配图、例句与描红',
|
||||||
},
|
},
|
||||||
'letter-tracing-full': {
|
{
|
||||||
title: '分栏对照',
|
id: 'letter-tracing-upper-lower',
|
||||||
desc: '半组字母左大写右小写',
|
label: '字母总览',
|
||||||
},
|
|
||||||
'letter-tracing-upper-lower': {
|
|
||||||
title: '字母总览',
|
|
||||||
desc: 'Uppercase / Lowercase 总览',
|
desc: 'Uppercase / Lowercase 总览',
|
||||||
},
|
},
|
||||||
'letter-tracing-two-column': {
|
{
|
||||||
title: '大小写配对',
|
id: 'letter-tracing-two-column',
|
||||||
|
label: '大小写配对',
|
||||||
desc: '左 A–M、右 N–Z 配对描红',
|
desc: '左 A–M、右 N–Z 配对描红',
|
||||||
},
|
},
|
||||||
};
|
{
|
||||||
|
id: 'letter-tracing-full',
|
||||||
|
label: '分栏对照',
|
||||||
|
desc: '半组字母左大写右小写',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'letter-tracing-triple',
|
||||||
|
label: '三字母精练',
|
||||||
|
desc: '每页聚焦 3 个字母深度书写',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'letter-tracing-single-line',
|
||||||
|
label: '单字母逐行',
|
||||||
|
desc: '每行一个字母,13 字母半表',
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const MODE_MAP = Object.fromEntries(MODES.map((m) => [m.id, m])) as Record<
|
||||||
|
string,
|
||||||
|
(typeof MODES)[number]
|
||||||
|
>;
|
||||||
|
|
||||||
/** 根据 worksheetId 查找页面元信息 */
|
/** 根据 worksheetId 查找页面元信息 */
|
||||||
function pageInfoLookup(id: string) {
|
function pageInfoLookup(id: string) {
|
||||||
return PAGE_META[id];
|
const m = MODE_MAP[id];
|
||||||
|
return m ? { title: m.label, desc: m.desc } : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 模式选择器的选项列表 */
|
|
||||||
const MODE_OPTIONS = [
|
|
||||||
{ id: 'letter-tracing-single', label: '看图描红' },
|
|
||||||
{ id: 'letter-tracing-full', label: '分栏对照' },
|
|
||||||
{ id: 'letter-tracing-upper-lower', label: '字母总览' },
|
|
||||||
{ id: 'letter-tracing-two-column', label: '大小写配对' },
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
type PageData = CanvasDataState & {
|
type PageData = CanvasDataState & {
|
||||||
worksheetId: string;
|
worksheetId: string;
|
||||||
traceMode: LetterTracingMode;
|
traceMode: LetterTracingMode;
|
||||||
@@ -54,7 +71,7 @@ type PageData = CanvasDataState & {
|
|||||||
showCaseToggle: boolean;
|
showCaseToggle: boolean;
|
||||||
showNextLetter: boolean;
|
showNextLetter: boolean;
|
||||||
letterGrid: string[];
|
letterGrid: string[];
|
||||||
modeOptions: ReadonlyArray<{ id: string; label: string }>;
|
modeOptions: ReadonlyArray<{ id: string; label: string; desc: string }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
createPage(
|
createPage(
|
||||||
@@ -64,6 +81,7 @@ createPage(
|
|||||||
boxHeight: 0,
|
boxHeight: 0,
|
||||||
boxWidth: 0,
|
boxWidth: 0,
|
||||||
drawService: null as LetterTracingDraw | null,
|
drawService: null as LetterTracingDraw | null,
|
||||||
|
fontProfile: DEFAULT_LETTER_PROFILE as FontProfile,
|
||||||
|
|
||||||
data: {
|
data: {
|
||||||
pageTitle: '字母描红',
|
pageTitle: '字母描红',
|
||||||
@@ -78,18 +96,22 @@ createPage(
|
|||||||
showCaseToggle: false,
|
showCaseToggle: false,
|
||||||
showNextLetter: true,
|
showNextLetter: true,
|
||||||
letterGrid: LETTERS_UPPER,
|
letterGrid: LETTERS_UPPER,
|
||||||
modeOptions: [...MODE_OPTIONS],
|
modeOptions: [...MODES],
|
||||||
} as unknown as PageData,
|
} as unknown as PageData,
|
||||||
|
|
||||||
/** 页面加载:从路由参数获取 worksheetId、letter、case 并应用,同时预加载字体 */
|
/** 页面加载:从路由参数获取 worksheetId、letter、case、font 并应用,同时预加载字体 */
|
||||||
onLoad(options: {
|
onLoad(options: {
|
||||||
id?: string;
|
id?: string;
|
||||||
letter?: string;
|
letter?: string;
|
||||||
case?: 'upper' | 'lower';
|
case?: 'upper' | 'lower';
|
||||||
|
font?: string;
|
||||||
}) {
|
}) {
|
||||||
loadLetterFont().catch(() => {});
|
if (options.font) {
|
||||||
|
this.fontProfile = getFontProfile(options.font);
|
||||||
|
}
|
||||||
|
loadLetterFont(this.fontProfile).catch(() => {});
|
||||||
const worksheetId =
|
const worksheetId =
|
||||||
options.id && PAGE_META[options.id]
|
options.id && MODE_MAP[options.id]
|
||||||
? options.id
|
? options.id
|
||||||
: 'letter-tracing-single';
|
: 'letter-tracing-single';
|
||||||
this.applyWorksheet(worksheetId);
|
this.applyWorksheet(worksheetId);
|
||||||
@@ -149,10 +171,13 @@ createPage(
|
|||||||
: this.data.letterCaseLower
|
: this.data.letterCaseLower
|
||||||
? 'lower'
|
? 'lower'
|
||||||
: 'upper';
|
: 'upper';
|
||||||
|
const needsHalf =
|
||||||
|
this.data.traceMode === 'column-compare' ||
|
||||||
|
this.data.traceMode === 'single-line';
|
||||||
return mergeLetterTracingConfig(base, {
|
return mergeLetterTracingConfig(base, {
|
||||||
selectedLetter: this.data.selectedLetter,
|
selectedLetter: this.data.selectedLetter,
|
||||||
letterCase,
|
letterCase,
|
||||||
...(this.data.traceMode === 'column-compare'
|
...(needsHalf
|
||||||
? {
|
? {
|
||||||
alphabetHalf: this.data.letterCaseLower
|
alphabetHalf: this.data.letterCaseLower
|
||||||
? 'N-Z'
|
? 'N-Z'
|
||||||
@@ -166,7 +191,7 @@ createPage(
|
|||||||
async drawCanvas() {
|
async drawCanvas() {
|
||||||
if (!this.ctx || !this.drawService) return;
|
if (!this.ctx || !this.drawService) return;
|
||||||
try {
|
try {
|
||||||
await loadLetterFont();
|
await loadLetterFont(this.fontProfile);
|
||||||
const config = this.buildRuntimeConfig();
|
const config = this.buildRuntimeConfig();
|
||||||
const data = generateLetterTracing(config);
|
const data = generateLetterTracing(config);
|
||||||
console.log('drawCanvas data', data);
|
console.log('drawCanvas data', data);
|
||||||
@@ -228,14 +253,16 @@ createPage(
|
|||||||
const merged = mergeLetterTracingConfig(base, {});
|
const merged = mergeLetterTracingConfig(base, {});
|
||||||
|
|
||||||
const traceMode = merged.mode;
|
const traceMode = merged.mode;
|
||||||
const showLetterPicker = traceMode === 'picture-tracing';
|
const showLetterPicker =
|
||||||
const showCaseToggle = traceMode === 'column-compare';
|
traceMode === 'picture-tracing' || traceMode === 'triple-focus';
|
||||||
|
const showCaseToggle =
|
||||||
|
traceMode === 'column-compare' || traceMode === 'single-line';
|
||||||
const showNextLetter = traceMode === 'picture-tracing';
|
const showNextLetter = traceMode === 'picture-tracing';
|
||||||
const selectedLetter = options?.preserveLetter
|
const selectedLetter = options?.preserveLetter
|
||||||
? (this.data.selectedLetter ?? 'A')
|
? (this.data.selectedLetter ?? 'A')
|
||||||
: normalizeLetter(merged.selectedLetter ?? '');
|
: normalizeLetter(merged.selectedLetter ?? '');
|
||||||
const letterCaseLower =
|
const letterCaseLower =
|
||||||
traceMode === 'column-compare'
|
traceMode === 'column-compare' || traceMode === 'single-line'
|
||||||
? merged.alphabetHalf === 'N-Z'
|
? merged.alphabetHalf === 'N-Z'
|
||||||
: traceMode === 'picture-tracing'
|
: traceMode === 'picture-tracing'
|
||||||
? merged.letterCase === 'lower'
|
? merged.letterCase === 'lower'
|
||||||
@@ -255,7 +282,7 @@ createPage(
|
|||||||
() => {
|
() => {
|
||||||
this.initPageInfo(
|
this.initPageInfo(
|
||||||
worksheetId,
|
worksheetId,
|
||||||
PAGE_META[worksheetId]?.title ?? '字母描红',
|
MODE_MAP[worksheetId]?.label ?? '字母描红',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.drawService) {
|
if (this.drawService) {
|
||||||
|
|||||||
@@ -1,20 +1,84 @@
|
|||||||
## 练习类型
|
## 练习类型
|
||||||
|
|
||||||
1. 大小写配对 — 26 字母双栏,每组大写+小写并排
|
共 6 种练习模式,从单字母精练到全表总览逐步递进。
|
||||||
2. 分栏对照 — 半组字母,左栏大写右栏小写
|
|
||||||
3. 字母总览 — 上下分区展示全部大小写
|
|
||||||
4. 看图描红 — 单字母教学配图 + 描红练习
|
|
||||||
|
|
||||||
### 类型描述
|
---
|
||||||
|
|
||||||
1. 一页展示 26 个字母,每个包含大小写,分左右两栏,左栏A-M,右栏N-Z,每栏的一行展示 4 组字母,每一组都是一个大写和小写字母的组合。同一行是相同的字母
|
### 1. 看图识字(picture-tracing)—— 单字母教学 + 配图 + 描红
|
||||||
2. 一页展示 A-M 或N-Z的字母,分左右两栏,左栏每行展示 4 个大小字母,右栏展示 4 个对应的小写字母。同一行是相同的字母
|
|
||||||
3. 一页分上下两块区域展示,上面展示大写字母,下面展示小写字母,上面是Uppercase Letters 标题加三行,前面两行展示 9 个字母,最后一行展示剩余的 8 个字母。下面区域title是 lowercase Letters,行的展示逻辑和上面一致
|
> 每页聚焦一个字母,适合初学者逐字认读和书写。
|
||||||
4. 一页分为上下两部分,上部分有分左右两部分,左边展示超大字体的大小写字母,下面展示一段方面记忆的短句,例如:“L is for Lion”,上部分右边展示对应的Lion 的图片。下部分展示 6 行字母描红,前三行大写,后三行小写。
|
|
||||||
|
页面分为上下两部分:
|
||||||
|
|
||||||
|
- **上部 · 教学区**:左侧展示超大字体的大小写字母及助记短句(如 "L is for Lion"),右侧展示对应单词的配图。中间包含"Highlight It"字母识别小游戏和"Trace It"笔顺演示区。
|
||||||
|
- **下部 · 描红区**:4 行四线三格,每行重复书写该字母的大小写组合,透明度逐渐递减形成倒三角描红引导。
|
||||||
|
|
||||||
|
### 2. 字母总览(overview)—— 26 字母全表速览
|
||||||
|
|
||||||
|
> 一页纵览全部大小写字母,适合复习或张贴展示。
|
||||||
|
|
||||||
|
页面分上下两大区域:
|
||||||
|
|
||||||
|
- **上区 · Uppercase Letters**:4 行四线三格,按 7-7-6-6 排列全部大写字母。
|
||||||
|
- **下区 · lowercase letters**:布局同上,展示对应的小写字母。
|
||||||
|
|
||||||
|
所有字母以浅红色描红字体呈现,可打印后用铅笔覆写练习。
|
||||||
|
|
||||||
|
### 3. 大小写配对(case-pairing)—— 左右分栏 · 大小写成对练习
|
||||||
|
|
||||||
|
> 以 Aa 为一组进行配对描红,强化大小写对应关系。
|
||||||
|
|
||||||
|
页面分左右两栏(左栏 A–M,右栏 N–Z),每行展示 4 组大小写配对字母。同一行的字母互相对应,每组内包含示范字母(实色)和渐淡描红格,引导从临摹到独立书写。
|
||||||
|
|
||||||
|
### 4. 分栏对照(column-compare)—— 半表字母 · 左大写右小写对照
|
||||||
|
|
||||||
|
> 半组字母(A–M 或 N–Z)逐行展开,左大写右小写一一对照。
|
||||||
|
|
||||||
|
四线三格横跨整页宽度,每行分为左右两半:左半栏写大写字母,右半栏写对应的小写字母。第一个字母为黑色示范,后续为浅红色描红。可切换 A–M / N–Z 两个半表。
|
||||||
|
|
||||||
|
### 5. 三字母精练(triple-focus)—— 每页 3 个字母 · 深度书写
|
||||||
|
|
||||||
|
> 每页聚焦 3 个字母,兼顾专注度和练习量,适合进阶阶段。
|
||||||
|
|
||||||
|
页面纵向分为 3 个等高区域,每个区域展示一个字母:
|
||||||
|
|
||||||
|
- 每区包含一行大写描红和一行小写描红(四线三格),每行重复书写该字母若干次,透明度逐渐递减。
|
||||||
|
- 字母组可按 ABC/DEF/… 顺序自动分组,也可由用户自选 3 个字母。
|
||||||
|
|
||||||
|
### 6. 单字母逐行(single-line)—— 每行一个字母 · 13 字母半表
|
||||||
|
|
||||||
|
> 每页 13 个字母(A–M 或 N–Z),每个字母独占一行,专注逐字练习。
|
||||||
|
|
||||||
|
页面纵向排列 13 行四线三格,每行对应一个字母。行内先展示一个黑色示范字母,后接若干浅红色描红字母。可切换 A–M / N–Z 半表。布局紧凑,适合大量反复练习。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 扩展思考:更多可补充的练习类型
|
||||||
|
|
||||||
|
以下为后续可扩展的练习模式,供产品迭代参考:
|
||||||
|
|
||||||
|
### 书写方向类
|
||||||
|
|
||||||
|
- **笔顺分步练习**:将每个字母的笔画拆解为 2–4 步,每步用不同颜色高亮当前笔画,配合箭头指示书写方向,逐步引导完成整个字母。
|
||||||
|
- **起笔点标注**:在四线三格中标注每个字母的起笔位置(圆点)和运笔路径(虚线箭头),帮助建立正确的书写习惯。
|
||||||
|
|
||||||
|
### 认知强化类
|
||||||
|
|
||||||
|
- **大小写连线**:左列随机排列大写字母,右列随机排列对应的小写字母,练习者用线条将配对字母连起来。
|
||||||
|
- **缺失字母填空**:展示部分字母表序列(如 A B _ D _ F),要求填入缺失字母,巩固字母顺序记忆。
|
||||||
|
- **字母迷宫 / 字母涂色**:在迷宫或涂色图案中隐藏目标字母,通过寻找和涂色加深印象。
|
||||||
|
|
||||||
|
### 综合应用类
|
||||||
|
|
||||||
|
- **字母+单词抄写**:每行先展示一个字母及对应单词(如 A — Apple),后面是该单词的描红格,从字母过渡到单词书写。
|
||||||
|
- **字母排序练习**:给出一组乱序字母,要求按字母表顺序重新抄写排列。
|
||||||
|
- **听写预备格**:纯空白四线三格(无任何预设字母),配合家长/老师口述使用,适合听写测试场景。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 生成要求
|
## 生成要求
|
||||||
|
|
||||||
字母行统一使用 四线三格,颜色为偏黑色,其中最上边和最下边的线条为实线,中间的两个线条为虚线。
|
字母行统一使用四线三格,颜色为偏黑色,其中最上边和最下边的线条为实线,中间的两个线条为虚线。
|
||||||
|
|
||||||
## 字母书写顺序资料
|
## 字母书写顺序资料
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ export interface LetterOverride {
|
|||||||
|
|
||||||
/** 一套完整的字体渲染配置 */
|
/** 一套完整的字体渲染配置 */
|
||||||
export interface FontProfile {
|
export interface FontProfile {
|
||||||
|
/** 唯一标识,同时用作 wx.loadFontFace 的 family 和按名称查找的 key */
|
||||||
|
name: string;
|
||||||
/** 字体 URL */
|
/** 字体 URL */
|
||||||
url: string;
|
url: string;
|
||||||
/** 各分类的默认渲染参数 */
|
/** 各分类的默认渲染参数 */
|
||||||
@@ -41,6 +43,11 @@ export interface FontProfile {
|
|||||||
letterOverrides?: Record<string, LetterOverride>;
|
letterOverrides?: Record<string, LetterOverride>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 构建 CSS font-family 串(含回退字体) */
|
||||||
|
export function fontFamilyOf(profile: FontProfile): string {
|
||||||
|
return `${profile.name}, Roboto, sans-serif`;
|
||||||
|
}
|
||||||
|
|
||||||
// ── 字母分类集合 ──
|
// ── 字母分类集合 ──
|
||||||
const TALL_LOWERCASE = new Set(['b', 'd', 'h', 'k', 'l']);
|
const TALL_LOWERCASE = new Set(['b', 'd', 'h', 'k', 'l']);
|
||||||
const DESCENDERS = new Set(['g', 'p', 'q', 'y']);
|
const DESCENDERS = new Set(['g', 'p', 'q', 'y']);
|
||||||
@@ -88,88 +95,121 @@ export function getLetterMetrics(
|
|||||||
|
|
||||||
/** ZhiyongWrite 手写字体 */
|
/** ZhiyongWrite 手写字体 */
|
||||||
export const ZHIYONG_WRITE: FontProfile = {
|
export const ZHIYONG_WRITE: FontProfile = {
|
||||||
|
name: 'ZhiyongWrite',
|
||||||
url: 'https://cdn.joeyone.cn/doodle/fonts/ZhiyongWrite.ttf',
|
url: 'https://cdn.joeyone.cn/doodle/fonts/ZhiyongWrite.ttf',
|
||||||
|
// categories: {
|
||||||
|
// uppercase: { scale: 0.93, baselineOffset: 0 },
|
||||||
|
// tallLower: { scale: 0.95, baselineOffset: 0 },
|
||||||
|
// descender: { scale: 1.12, baselineOffset: 0.18 },
|
||||||
|
// lowercase: { scale: 0.83, baselineOffset: 0.03 },
|
||||||
|
// },
|
||||||
|
// letterOverrides: {
|
||||||
|
// i: { scale: 0.78, baselineOffset: 0.02 },
|
||||||
|
// f: { scale: 1.37, baselineOffset: 0.34 },
|
||||||
|
// t: { scale: 0.85, baselineOffset: 0 },
|
||||||
|
// j: { scale: 1, baselineOffset: 0.15 },
|
||||||
|
// },
|
||||||
categories: {
|
categories: {
|
||||||
uppercase: { scale: 0.93, baselineOffset: 0 },
|
uppercase: { scale: 0.95, baselineOffset: 0 },
|
||||||
tallLower: { scale: 0.95, baselineOffset: 0 },
|
tallLower: { scale: 0.85, baselineOffset: 0 },
|
||||||
descender: { scale: 1.12, baselineOffset: 0.18 },
|
descender: { scale: 1, baselineOffset: 0.12 },
|
||||||
lowercase: { scale: 0.83, baselineOffset: 0.03 },
|
lowercase: { scale: 0.75, baselineOffset: 0 },
|
||||||
},
|
},
|
||||||
letterOverrides: {
|
letterOverrides: {
|
||||||
i: { scale: 0.78, baselineOffset: 0.02 },
|
i: { scale: 0.8, baselineOffset: 0 },
|
||||||
f: { scale: 1.37, baselineOffset: 0.34 },
|
f: { scale: 1, baselineOffset: 0.05 },
|
||||||
t: { scale: 0.85, baselineOffset: 0 },
|
t: { scale: 0.85, baselineOffset: 0 },
|
||||||
j: { scale: 1, baselineOffset: 0.15 },
|
j: { scale: 0.85, baselineOffset: 0.1 },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Handlee Regular — Google 手写风格 */
|
/** Handlee Regular — Google 手写风格 */
|
||||||
export const HANDLEE_REGULAR: FontProfile = {
|
export const HANDLEE_REGULAR: FontProfile = {
|
||||||
|
name: 'HandleeRegular',
|
||||||
url: 'https://cdn.joeyone.cn/doodle/fonts/Handlee-Regular.ttf',
|
url: 'https://cdn.joeyone.cn/doodle/fonts/Handlee-Regular.ttf',
|
||||||
|
// categories: {
|
||||||
|
// uppercase: { scale: 0.9, baselineOffset: 0 },
|
||||||
|
// tallLower: { scale: 0.73, baselineOffset: 0.03 },
|
||||||
|
// descender: { scale: 0.8, baselineOffset: 0.05 },
|
||||||
|
// lowercase: { scale: 0.73, baselineOffset: 0.02 },
|
||||||
|
// },
|
||||||
|
// letterOverrides: {
|
||||||
|
// i: { scale: 0.78, baselineOffset: 0.02 },
|
||||||
|
// f: { scale: 0.82, baselineOffset: 0 },
|
||||||
|
// t: { scale: 0.77, baselineOffset: 0.04 },
|
||||||
|
// j: { scale: 0.85, baselineOffset: 0.08 },
|
||||||
|
// },
|
||||||
categories: {
|
categories: {
|
||||||
uppercase: { scale: 0.9, baselineOffset: 0 },
|
uppercase: { scale: 0.9, baselineOffset: 0 },
|
||||||
tallLower: { scale: 0.73, baselineOffset: 0.03 },
|
tallLower: { scale: 0.68, baselineOffset: 0 },
|
||||||
descender: { scale: 0.8, baselineOffset: 0.05 },
|
descender: { scale: 0.68, baselineOffset: 0 },
|
||||||
lowercase: { scale: 0.73, baselineOffset: 0.02 },
|
lowercase: { scale: 0.68, baselineOffset: 0 },
|
||||||
},
|
},
|
||||||
letterOverrides: {
|
letterOverrides: {
|
||||||
i: { scale: 0.78, baselineOffset: 0.02 },
|
i: { scale: 0.68, baselineOffset: 0 },
|
||||||
f: { scale: 0.82, baselineOffset: 0 },
|
f: { scale: 0.68, baselineOffset: 0 },
|
||||||
t: { scale: 0.77, baselineOffset: 0.04 },
|
t: { scale: 0.68, baselineOffset: 0 },
|
||||||
j: { scale: 0.85, baselineOffset: 0.08 },
|
j: { scale: 0.68, baselineOffset: 0 },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Print Clearly Regular */
|
/** Print Clearly Regular */
|
||||||
export const PRINT_CLEARLY_REGULAR: FontProfile = {
|
export const PRINT_CLEARLY_REGULAR: FontProfile = {
|
||||||
|
name: 'PrintClearlyRegular',
|
||||||
url: 'https://cdn.joeyone.cn/doodle/fonts/print_clearly_regular_tt.ttf',
|
url: 'https://cdn.joeyone.cn/doodle/fonts/print_clearly_regular_tt.ttf',
|
||||||
categories: {
|
categories: {
|
||||||
uppercase: { scale: 0.95, baselineOffset: 0 },
|
uppercase: { scale: 1, baselineOffset: 0 },
|
||||||
tallLower: { scale: 0.95, baselineOffset: 0 },
|
tallLower: { scale: 1, baselineOffset: 0 },
|
||||||
descender: { scale: 1.05, baselineOffset: 0 },
|
descender: { scale: 1, baselineOffset: 0 },
|
||||||
lowercase: { scale: 0.85, baselineOffset: 0 },
|
lowercase: { scale: 1, baselineOffset: 0 },
|
||||||
},
|
},
|
||||||
letterOverrides: {
|
letterOverrides: {
|
||||||
f: { scale: 1.35, baselineOffset: 0 },
|
i: { scale: 1, baselineOffset: 0 },
|
||||||
t: { scale: 0.85, baselineOffset: 0 },
|
f: { scale: 1, baselineOffset: 0 },
|
||||||
j: { scale: 1.15, baselineOffset: 0 },
|
t: { scale: 1, baselineOffset: 0 },
|
||||||
|
j: { scale: 1, baselineOffset: 0 },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Print Clearly Bold */
|
/** Print Clearly Bold */
|
||||||
export const PRINT_CLEARLY_BOLD: FontProfile = {
|
export const PRINT_CLEARLY_BOLD: FontProfile = {
|
||||||
|
name: 'PrintClearlyBold',
|
||||||
url: 'https://cdn.joeyone.cn/doodle/fonts/print_clearly_bold_tt.ttf',
|
url: 'https://cdn.joeyone.cn/doodle/fonts/print_clearly_bold_tt.ttf',
|
||||||
categories: {
|
categories: {
|
||||||
uppercase: { scale: 0.95, baselineOffset: 0 },
|
uppercase: { scale: 1, baselineOffset: 0 },
|
||||||
tallLower: { scale: 0.95, baselineOffset: 0 },
|
tallLower: { scale: 1, baselineOffset: 0 },
|
||||||
descender: { scale: 1.05, baselineOffset: 0 },
|
descender: { scale: 1, baselineOffset: 0 },
|
||||||
lowercase: { scale: 0.85, baselineOffset: 0 },
|
lowercase: { scale: 1, baselineOffset: 0 },
|
||||||
},
|
},
|
||||||
letterOverrides: {
|
letterOverrides: {
|
||||||
f: { scale: 1.35, baselineOffset: 0 },
|
i: { scale: 1, baselineOffset: 0 },
|
||||||
t: { scale: 0.85, baselineOffset: 0 },
|
f: { scale: 1, baselineOffset: 0 },
|
||||||
j: { scale: 1.15, baselineOffset: 0 },
|
t: { scale: 1, baselineOffset: 0 },
|
||||||
|
j: { scale: 1, baselineOffset: 0 },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Print Clearly Dashed — 虚线描红字体 */
|
/** Print Clearly Dashed — 虚线描红字体 */
|
||||||
export const PRINT_CLEARLY_DASHED: FontProfile = {
|
export const PRINT_CLEARLY_DASHED: FontProfile = {
|
||||||
|
name: 'PrintClearlyDashed',
|
||||||
url: 'https://cdn.joeyone.cn/doodle/fonts/print_clearly_dashed_tt.ttf',
|
url: 'https://cdn.joeyone.cn/doodle/fonts/print_clearly_dashed_tt.ttf',
|
||||||
categories: {
|
categories: {
|
||||||
uppercase: { scale: 0.95, baselineOffset: 0 },
|
uppercase: { scale: 1, baselineOffset: 0 },
|
||||||
tallLower: { scale: 0.95, baselineOffset: 0 },
|
tallLower: { scale: 1, baselineOffset: 0 },
|
||||||
descender: { scale: 1.05, baselineOffset: 0 },
|
descender: { scale: 1, baselineOffset: 0 },
|
||||||
lowercase: { scale: 0.85, baselineOffset: 0 },
|
lowercase: { scale: 1, baselineOffset: 0 },
|
||||||
},
|
},
|
||||||
letterOverrides: {
|
letterOverrides: {
|
||||||
f: { scale: 1.35, baselineOffset: 0 },
|
i: { scale: 1, baselineOffset: 0 },
|
||||||
t: { scale: 0.85, baselineOffset: 0 },
|
f: { scale: 1, baselineOffset: 0 },
|
||||||
j: { scale: 1.15, baselineOffset: 0 },
|
t: { scale: 1, baselineOffset: 0 },
|
||||||
|
j: { scale: 1, baselineOffset: 0 },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Num 数字字体 */
|
/** Num 数字字体 */
|
||||||
export const NUM_FONT: FontProfile = {
|
export const NUM_FONT: FontProfile = {
|
||||||
|
name: 'NumFont',
|
||||||
url: 'https://cdn.joeyone.cn/doodle/fonts/num.woff2',
|
url: 'https://cdn.joeyone.cn/doodle/fonts/num.woff2',
|
||||||
categories: {
|
categories: {
|
||||||
uppercase: { scale: 0.95, baselineOffset: 0 },
|
uppercase: { scale: 0.95, baselineOffset: 0 },
|
||||||
@@ -184,6 +224,25 @@ export const NUM_FONT: FontProfile = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 当前激活的字体配置(切换字体只需修改这里) */
|
// ── 字体注册表:按 name 索引所有可用字体 ──
|
||||||
// export const ACTIVE_FONT_PROFILE = ZHIYONG_WRITE;
|
|
||||||
export const ACTIVE_FONT_PROFILE = HANDLEE_REGULAR;
|
const FONT_PROFILES: ReadonlyArray<FontProfile> = [
|
||||||
|
ZHIYONG_WRITE,
|
||||||
|
HANDLEE_REGULAR,
|
||||||
|
PRINT_CLEARLY_REGULAR,
|
||||||
|
PRINT_CLEARLY_BOLD,
|
||||||
|
PRINT_CLEARLY_DASHED,
|
||||||
|
NUM_FONT,
|
||||||
|
];
|
||||||
|
|
||||||
|
const _profileByName = new Map<string, FontProfile>(
|
||||||
|
FONT_PROFILES.map((p) => [p.name, p]),
|
||||||
|
);
|
||||||
|
|
||||||
|
/** 默认字母字体 */
|
||||||
|
export const DEFAULT_LETTER_PROFILE = PRINT_CLEARLY_REGULAR;
|
||||||
|
|
||||||
|
/** 按名称查找字体配置,找不到返回默认 */
|
||||||
|
export function getFontProfile(name: string): FontProfile {
|
||||||
|
return _profileByName.get(name) ?? DEFAULT_LETTER_PROFILE;
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import type {
|
|||||||
} from '../generators/letter-tracing-generator';
|
} from '../generators/letter-tracing-generator';
|
||||||
import { drawFourLineGrid } from './drawTools';
|
import { drawFourLineGrid } from './drawTools';
|
||||||
|
|
||||||
const LETTER_FACE =
|
const LETTER_FACE = 'Helvetica, Arial, "Segoe UI", Roboto, sans-serif';
|
||||||
'Helvetica, Arial, "Segoe UI", Roboto, sans-serif';
|
|
||||||
|
|
||||||
type CasePairingData = Extract<LetterTracingData, { mode: 'case-pairing' }>;
|
type CasePairingData = Extract<LetterTracingData, { mode: 'case-pairing' }>;
|
||||||
|
|
||||||
|
|||||||
@@ -3,28 +3,32 @@ import type {
|
|||||||
LetterTracingData,
|
LetterTracingData,
|
||||||
TracingRow,
|
TracingRow,
|
||||||
} from '../generators/letter-tracing-generator';
|
} from '../generators/letter-tracing-generator';
|
||||||
|
import { fontFamilyOf } from '../data/fontProfiles';
|
||||||
import {
|
import {
|
||||||
calcLetterFontSizes,
|
calcLetterFontSizes,
|
||||||
drawFourLineGrid,
|
drawFourLineGrid,
|
||||||
drawLetterInFourLineGrid,
|
drawLetterInFourLineGrid,
|
||||||
FOUR_LINE_GRID_H,
|
FOUR_LINE_GRID_H,
|
||||||
TOY_FONT_LETTER,
|
|
||||||
type LetterFontSizes,
|
type LetterFontSizes,
|
||||||
} from './drawTools';
|
} from './drawTools';
|
||||||
|
|
||||||
const LETTER_FACE = `${TOY_FONT_LETTER}, Roboto, sans-serif`;
|
|
||||||
const COLOR_BLACK = '#1a1a1a';
|
const COLOR_BLACK = '#1a1a1a';
|
||||||
const COLOR_LIGHT_RED = 'rgba(252, 46, 0, 0.3)';
|
const COLOR_LIGHT_RED = 'rgba(252, 46, 0, 0.3)';
|
||||||
|
|
||||||
type ColumnCompareData = Extract<
|
/** 左右大写/小写两栏之间的间距(相对行宽 + 下限,避免过窄屏过小) */
|
||||||
LetterTracingData,
|
const COLUMN_GUTTER_RATIO = 0.02;
|
||||||
{ mode: 'column-compare' }
|
const COLUMN_GUTTER_MIN = 14;
|
||||||
>;
|
|
||||||
|
/**
|
||||||
|
* 单栏内字母排版区域占栏宽比例(<1 则同栏字母更紧凑、字间距更小)
|
||||||
|
*/
|
||||||
|
const INTRA_COLUMN_LETTER_BAND_RATIO = 1;
|
||||||
|
|
||||||
|
type ColumnCompareData = Extract<LetterTracingData, { mode: 'column-compare' }>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分栏对照绘制:
|
* 分栏对照绘制:
|
||||||
* 四线三格横跨整页宽度,字母分左右两侧书写——
|
* 四线三格横跨整页宽度,字母分左右两侧书写——
|
||||||
* 左半写大写(repetitions 个),右半写小写(repetitions 个),
|
* 左栏大写、右栏小写(各 repetitions 个),中间留间距;同栏内字母略收紧。
|
||||||
* 每侧第一个字母黑色,后续浅红色。
|
* 每侧第一个字母黑色,后续浅红色。
|
||||||
*/
|
*/
|
||||||
export default class ColumnCompareDraw extends BaseDrawService {
|
export default class ColumnCompareDraw extends BaseDrawService {
|
||||||
@@ -52,7 +56,14 @@ export default class ColumnCompareDraw extends BaseDrawService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const y0 = this.currentY + rowGap;
|
const y0 = this.currentY + rowGap;
|
||||||
const halfW = lineW / 2;
|
|
||||||
|
const columnGutter = Math.max(
|
||||||
|
COLUMN_GUTTER_MIN,
|
||||||
|
Math.round(lineW * COLUMN_GUTTER_RATIO),
|
||||||
|
);
|
||||||
|
const halfContentW = (lineW - columnGutter) / 2;
|
||||||
|
const leftX = M;
|
||||||
|
const rightX = M + halfContentW + columnGutter;
|
||||||
|
|
||||||
for (let i = 0; i < maxR; i++) {
|
for (let i = 0; i < maxR; i++) {
|
||||||
const y = y0 + i * (FOUR_LINE_GRID_H + rowGap);
|
const y = y0 + i * (FOUR_LINE_GRID_H + rowGap);
|
||||||
@@ -60,10 +71,22 @@ export default class ColumnCompareDraw extends BaseDrawService {
|
|||||||
drawFourLineGrid(this.ctx, M, y, lineW, FOUR_LINE_GRID_H);
|
drawFourLineGrid(this.ctx, M, y, lineW, FOUR_LINE_GRID_H);
|
||||||
|
|
||||||
if (i < leftRows.length) {
|
if (i < leftRows.length) {
|
||||||
this.drawLettersInHalf(leftRows[i], M, y, halfW);
|
this.drawLettersInHalf(
|
||||||
|
leftRows[i],
|
||||||
|
leftX,
|
||||||
|
y,
|
||||||
|
halfContentW,
|
||||||
|
INTRA_COLUMN_LETTER_BAND_RATIO,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (i < rightRows.length) {
|
if (i < rightRows.length) {
|
||||||
this.drawLettersInHalf(rightRows[i], M + halfW, y, halfW);
|
this.drawLettersInHalf(
|
||||||
|
rightRows[i],
|
||||||
|
rightX,
|
||||||
|
y,
|
||||||
|
halfContentW,
|
||||||
|
INTRA_COLUMN_LETTER_BAND_RATIO,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,16 +99,19 @@ export default class ColumnCompareDraw extends BaseDrawService {
|
|||||||
row: TracingRow,
|
row: TracingRow,
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
halfW: number,
|
columnW: number,
|
||||||
|
letterBandRatio: number,
|
||||||
) {
|
) {
|
||||||
const n = row.cells.length;
|
const n = row.cells.length;
|
||||||
if (n === 0) return;
|
if (n === 0) return;
|
||||||
|
|
||||||
const slotW = halfW / n;
|
const bandW = columnW * letterBandRatio;
|
||||||
|
const inset = (columnW - bandW) / 2;
|
||||||
|
const slotW = bandW / n;
|
||||||
for (let i = 0; i < n; i++) {
|
for (let i = 0; i < n; i++) {
|
||||||
const cell = row.cells[i];
|
const cell = row.cells[i];
|
||||||
const color = i === 0 ? COLOR_BLACK : COLOR_LIGHT_RED;
|
const color = i === 0 ? COLOR_BLACK : COLOR_LIGHT_RED;
|
||||||
const cx = x + slotW * i + slotW / 2;
|
const cx = x + inset + slotW * i + slotW / 2;
|
||||||
|
|
||||||
drawLetterInFourLineGrid(
|
drawLetterInFourLineGrid(
|
||||||
this.ctx,
|
this.ctx,
|
||||||
@@ -94,7 +120,7 @@ export default class ColumnCompareDraw extends BaseDrawService {
|
|||||||
y,
|
y,
|
||||||
FOUR_LINE_GRID_H,
|
FOUR_LINE_GRID_H,
|
||||||
this.letterSizes,
|
this.letterSizes,
|
||||||
{ fontFamily: LETTER_FACE, color },
|
{ fontFamily: fontFamilyOf(this.letterSizes._profile), color },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* 绘制工具集合(后续可继续扩展其他通用绘制方法)
|
* 四线三格 & 字母绘制工具集
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ACTIVE_FONT_PROFILE,
|
DEFAULT_LETTER_PROFILE,
|
||||||
|
fontFamilyOf,
|
||||||
getLetterMetrics,
|
getLetterMetrics,
|
||||||
type FontProfile,
|
type FontProfile,
|
||||||
} from '../data/fontProfiles';
|
} from '../data/fontProfiles';
|
||||||
@@ -11,47 +12,47 @@ import {
|
|||||||
// ── 四线三格统一高度(所有页面共用) ──
|
// ── 四线三格统一高度(所有页面共用) ──
|
||||||
export const FOUR_LINE_GRID_H = 43;
|
export const FOUR_LINE_GRID_H = 43;
|
||||||
|
|
||||||
// ── 英语字母字体集合 ──
|
// ── 字体加载(支持多字体,按 URL 去重) ──
|
||||||
export const TOY_FONT_LETTER = 'ToyLetterFont';
|
|
||||||
|
|
||||||
let _fontLoaded = false;
|
const _loadedUrls = new Set<string>();
|
||||||
export function loadLetterFont(): Promise<void> {
|
|
||||||
if (_fontLoaded) return Promise.resolve();
|
/**
|
||||||
|
* 加载字体到 Canvas native 渲染管线。
|
||||||
|
* 同一 URL 只加载一次;不同 FontProfile 可安全并发调用。
|
||||||
|
*/
|
||||||
|
export function loadLetterFont(
|
||||||
|
profile: FontProfile = DEFAULT_LETTER_PROFILE,
|
||||||
|
): Promise<void> {
|
||||||
|
if (_loadedUrls.has(profile.url)) return Promise.resolve();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
wx.loadFontFace({
|
wx.loadFontFace({
|
||||||
family: TOY_FONT_LETTER,
|
family: profile.name,
|
||||||
source: `url("${ACTIVE_FONT_PROFILE.url}")`,
|
source: `url("${profile.url}")`,
|
||||||
scopes: ['native'],
|
scopes: ['native'],
|
||||||
success: () => {
|
success: () => {
|
||||||
_fontLoaded = true;
|
_loadedUrls.add(profile.url);
|
||||||
resolve();
|
resolve();
|
||||||
},
|
},
|
||||||
fail: (err) => {
|
fail: (err) => {
|
||||||
console.error('loadFontFace failed', err);
|
console.error(`loadLetterFont [${profile.name}] failed`, err);
|
||||||
reject(err);
|
reject(err);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 向后兼容:LetterFontSizes 类型保留,供外部已有调用使用 ──
|
// ── LetterFontSizes:持有 FontProfile 引用,运行时按字母查表 ──
|
||||||
|
|
||||||
export type LetterFontSizes = {
|
export type LetterFontSizes = {
|
||||||
/** 字体配置(新机制) */
|
|
||||||
_profile: FontProfile;
|
_profile: FontProfile;
|
||||||
/** gridH(用于运行时计算) */
|
|
||||||
_gridH: number;
|
_gridH: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
export function calcLetterFontSizes(
|
||||||
* 根据四线三格总高度,构建 LetterFontSizes。
|
gridH: number,
|
||||||
* 内部持有 FontProfile 引用,按字母查表计算。
|
profile: FontProfile = DEFAULT_LETTER_PROFILE,
|
||||||
*/
|
): LetterFontSizes {
|
||||||
export function calcLetterFontSizes(gridH: number): LetterFontSizes {
|
return { _profile: profile, _gridH: gridH };
|
||||||
return {
|
|
||||||
_profile: ACTIVE_FONT_PROFILE,
|
|
||||||
_gridH: gridH,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 根据字母和字体配置,计算对应 fontSize */
|
/** 根据字母和字体配置,计算对应 fontSize */
|
||||||
@@ -97,7 +98,7 @@ export function drawLetterInFourLineGrid(
|
|||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const fontFamily =
|
const fontFamily =
|
||||||
options?.fontFamily ?? `${TOY_FONT_LETTER}, Roboto, sans-serif`;
|
options?.fontFamily ?? fontFamilyOf(sizes._profile);
|
||||||
const color = options?.color ?? '#1a1a1a';
|
const color = options?.color ?? '#1a1a1a';
|
||||||
const bold = options?.bold ?? false;
|
const bold = options?.bold ?? false;
|
||||||
|
|
||||||
|
|||||||
@@ -1,87 +1,133 @@
|
|||||||
import { BaseDrawService } from '../../../core/draw/baseDraw';
|
import { BaseDrawService } from '../../../core/draw/baseDraw';
|
||||||
import type { LetterTracingData } from '../generators/letter-tracing-generator';
|
import type { LetterTracingData } from '../generators/letter-tracing-generator';
|
||||||
import { drawFourLineGrid } from './drawTools';
|
import {
|
||||||
|
PRINT_CLEARLY_BOLD,
|
||||||
const LETTER_FACE =
|
PRINT_CLEARLY_DASHED,
|
||||||
'Helvetica, Arial, "Segoe UI", Roboto, sans-serif';
|
fontFamilyOf,
|
||||||
|
} from '../data/fontProfiles';
|
||||||
|
import {
|
||||||
|
calcLetterFontSizes,
|
||||||
|
drawFourLineGrid,
|
||||||
|
drawLetterInFourLineGrid,
|
||||||
|
FOUR_LINE_GRID_H,
|
||||||
|
loadLetterFont,
|
||||||
|
type LetterFontSizes,
|
||||||
|
} from './drawTools';
|
||||||
|
|
||||||
type OverviewData = Extract<LetterTracingData, { mode: 'overview' }>;
|
type OverviewData = Extract<LetterTracingData, { mode: 'overview' }>;
|
||||||
|
|
||||||
|
/** 页面左右边距(margin,px),用于排版时留白 */
|
||||||
|
const M = 24;
|
||||||
|
/** 上下两大区(Uppercase / lowercase)之间的间距 */
|
||||||
|
const SECTION_GAP = 25;
|
||||||
|
/** 标题字号 */
|
||||||
|
const TITLE_SIZE = 28;
|
||||||
|
/** 标题与第一行四线三格的间距 */
|
||||||
|
const TITLE_GAP = 25;
|
||||||
|
/** 同一区域内相邻四线三格行之间的间距 */
|
||||||
|
const ROW_GAP = 24;
|
||||||
|
const TITLE_COLOR = '#000';
|
||||||
|
|
||||||
|
/** 浅红色描红(不透明 hex,避免打印时半透明发灰) */
|
||||||
|
const TRACE_LIGHT_RED = '#FF8E8E';
|
||||||
|
|
||||||
|
const DASHED_PROFILE = PRINT_CLEARLY_DASHED;
|
||||||
|
const TITLE_PROFILE = PRINT_CLEARLY_BOLD;
|
||||||
|
const LETTER_FACE = fontFamilyOf(DASHED_PROFILE);
|
||||||
|
const TITLE_FACE = fontFamilyOf(TITLE_PROFILE);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字母总览:上区大写、下区小写;每区标题 + 4 行四线三格(7+7+6+6)。
|
||||||
|
* 列宽按 7 列均分;后两行 6 字左对齐,与前两行逐列对齐(占前 6 列)。
|
||||||
|
*/
|
||||||
export default class OverviewDraw extends BaseDrawService {
|
export default class OverviewDraw extends BaseDrawService {
|
||||||
|
private letterSizes: LetterFontSizes = calcLetterFontSizes(
|
||||||
|
FOUR_LINE_GRID_H,
|
||||||
|
DASHED_PROFILE,
|
||||||
|
);
|
||||||
|
|
||||||
async draw(data: OverviewData) {
|
async draw(data: OverviewData) {
|
||||||
this.prepareDraw();
|
this.prepareDraw();
|
||||||
|
await Promise.allSettled([
|
||||||
|
loadLetterFont(DASHED_PROFILE),
|
||||||
|
loadLetterFont(TITLE_PROFILE),
|
||||||
|
]);
|
||||||
await this.drawHeaderAndDivider();
|
await this.drawHeaderAndDivider();
|
||||||
this.drawContent(data.upperRows, data.lowerRows);
|
this.drawOverviewBody(data.upperRows, data.lowerRows);
|
||||||
this.drawPrintFooter();
|
await this.drawPrintFooter();
|
||||||
}
|
}
|
||||||
|
|
||||||
private drawContent(upperRows: string[][], lowerRows: string[][]) {
|
private drawOverviewBody(
|
||||||
const ctx = this.ctx;
|
upperRows: string[][],
|
||||||
const M = 24;
|
lowerRows: string[][],
|
||||||
let y = this.currentY + 6;
|
): void {
|
||||||
const innerW = this.canvasWidth - M * 2;
|
const lineW = this.canvasWidth - M * 2;
|
||||||
|
const slotW = lineW / 7;
|
||||||
|
|
||||||
const drawTitle = (t: string) => {
|
let y = this.currentY + SECTION_GAP - 7;
|
||||||
ctx.fillStyle = '#111';
|
|
||||||
ctx.font = 'bold 17px sans-serif';
|
|
||||||
ctx.textAlign = 'left';
|
|
||||||
ctx.textBaseline = 'top';
|
|
||||||
ctx.fillText(t, M, y);
|
|
||||||
y += 24;
|
|
||||||
};
|
|
||||||
|
|
||||||
const drawBand = (rows: string[][]) => {
|
y = this.drawLetterBlock(
|
||||||
for (const line of rows) {
|
'Uppercase Letters',
|
||||||
const n = line.length;
|
upperRows,
|
||||||
const gap = 4;
|
y,
|
||||||
const cellW = (innerW - (n - 1) * gap) / n;
|
lineW,
|
||||||
const cellH = 30;
|
slotW,
|
||||||
for (let i = 0; i < n; i++) {
|
);
|
||||||
const cx = M + i * (cellW + gap);
|
y += SECTION_GAP + 7;
|
||||||
drawFourLineGrid(ctx, cx, y, cellW, cellH);
|
this.drawLetterBlock('lowercase letters', lowerRows, y, lineW, slotW);
|
||||||
this.drawCharInCell(
|
|
||||||
line[i],
|
|
||||||
cx,
|
|
||||||
y,
|
|
||||||
cellW,
|
|
||||||
cellH,
|
|
||||||
0.9,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
y += cellH + 8;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
drawTitle('Uppercase Letters');
|
|
||||||
drawBand(upperRows);
|
|
||||||
y += 6;
|
|
||||||
this.drawDashedDivider(y, M, '#bbb');
|
|
||||||
y += 12;
|
|
||||||
drawTitle('lowercase Letters');
|
|
||||||
drawBand(lowerRows);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private drawCharInCell(
|
/**
|
||||||
ch: string,
|
* 绘制一个分区:居中标题 + 4 行四线三格与字母。
|
||||||
x: number,
|
* @returns 该分区最后一行四线三格底边 Y
|
||||||
y: number,
|
*/
|
||||||
w: number,
|
private drawLetterBlock(
|
||||||
h: number,
|
title: string,
|
||||||
opacity: number,
|
rows: string[][],
|
||||||
bold: boolean,
|
startY: number,
|
||||||
) {
|
lineW: number,
|
||||||
|
slotW: number,
|
||||||
|
): number {
|
||||||
const ctx = this.ctx;
|
const ctx = this.ctx;
|
||||||
if (opacity <= 0) return;
|
const centerX = M + lineW / 2;
|
||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.globalAlpha = opacity;
|
ctx.font = `bold ${TITLE_SIZE}px ${TITLE_FACE}`;
|
||||||
ctx.fillStyle = bold ? '#1a1a1a' : '#4a4a4a';
|
ctx.fillStyle = TITLE_COLOR;
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.textBaseline = 'alphabetic';
|
ctx.textBaseline = 'top';
|
||||||
const baselineY = y + h * 0.72;
|
ctx.fillText(title, centerX, startY);
|
||||||
const fontSize = Math.min(h * 0.72, w * 0.85);
|
|
||||||
ctx.font = `${bold ? 'bold ' : ''}${fontSize}px ${LETTER_FACE}`;
|
|
||||||
ctx.fillText(ch, x + w / 2, baselineY);
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|
||||||
|
let y = startY + TITLE_SIZE + TITLE_GAP;
|
||||||
|
|
||||||
|
for (let r = 0; r < rows.length; r++) {
|
||||||
|
const chars = rows[r];
|
||||||
|
drawFourLineGrid(ctx, M, y, lineW, FOUR_LINE_GRID_H);
|
||||||
|
|
||||||
|
/** 与 7 字行共用列宽 slotW,后两行 6 字左对齐(占前 6 列),与上行逐列对齐 */
|
||||||
|
const offsetX = M;
|
||||||
|
for (let i = 0; i < chars.length; i++) {
|
||||||
|
const cx = offsetX + slotW * (i + 0.5);
|
||||||
|
drawLetterInFourLineGrid(
|
||||||
|
ctx,
|
||||||
|
chars[i],
|
||||||
|
cx,
|
||||||
|
y,
|
||||||
|
FOUR_LINE_GRID_H,
|
||||||
|
this.letterSizes,
|
||||||
|
{
|
||||||
|
fontFamily: LETTER_FACE,
|
||||||
|
color: TRACE_LIGHT_RED,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
y += FOUR_LINE_GRID_H;
|
||||||
|
if (r < rows.length - 1) {
|
||||||
|
y += ROW_GAP;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,29 +4,29 @@ import type {
|
|||||||
LetterTracingData,
|
LetterTracingData,
|
||||||
HighlightGrid,
|
HighlightGrid,
|
||||||
} from '../generators/letter-tracing-generator';
|
} from '../generators/letter-tracing-generator';
|
||||||
|
import { fontFamilyOf } from '../data/fontProfiles';
|
||||||
import {
|
import {
|
||||||
calcLetterFontSizes,
|
calcLetterFontSizes,
|
||||||
drawFourLineGrid,
|
drawFourLineGrid,
|
||||||
drawLetterInFourLineGrid,
|
drawLetterInFourLineGrid,
|
||||||
FOUR_LINE_GRID_H,
|
FOUR_LINE_GRID_H,
|
||||||
getLetterFontSize,
|
getLetterFontSize,
|
||||||
TOY_FONT_LETTER,
|
|
||||||
type LetterFontSizes,
|
type LetterFontSizes,
|
||||||
} from './drawTools';
|
} from './drawTools';
|
||||||
|
|
||||||
const LINE_INK_ALPHA = 'rgba(50, 46, 37, 1)';
|
const LINE_INK_ALPHA = 'rgba(50, 46, 37, 1)';
|
||||||
const ACCENT_RED = 'rgba(252, 46, 0, 1)';
|
const ACCENT_RED = 'rgba(252, 46, 0, 1)';
|
||||||
//'Helvetica, Arial, "Segoe UI", Roboto, sans-serif';
|
|
||||||
const LETTER_FACE = `${TOY_FONT_LETTER}, Roboto, sans-serif`;
|
|
||||||
// const TITLE_EMOJI_FONT =
|
|
||||||
// "16px 'Apple Color Emoji','Segoe UI Emoji','Noto Color Emoji',sans-serif";
|
|
||||||
const TITLE_TEXT_FONT = '14px sans-serif';
|
const TITLE_TEXT_FONT = '14px sans-serif';
|
||||||
const TITLE_ICON_GAP = 4;
|
const TITLE_ICON_GAP = 4;
|
||||||
|
|
||||||
type PictureData = Extract<LetterTracingData, { mode: 'picture-tracing' }>;
|
type PictureData = Extract<LetterTracingData, { mode: 'picture-tracing' }>;
|
||||||
|
|
||||||
export default class PictureTracingDraw extends BaseDrawService {
|
export default class PictureTracingDraw extends BaseDrawService {
|
||||||
private letterSizes: LetterFontSizes = calcLetterFontSizes(FOUR_LINE_GRID_H);
|
private letterSizes: LetterFontSizes =
|
||||||
|
calcLetterFontSizes(FOUR_LINE_GRID_H);
|
||||||
|
private get letterFace(): string {
|
||||||
|
return fontFamilyOf(this.letterSizes._profile);
|
||||||
|
}
|
||||||
|
|
||||||
async draw(data: PictureData) {
|
async draw(data: PictureData) {
|
||||||
this.prepareDraw();
|
this.prepareDraw();
|
||||||
@@ -68,7 +68,7 @@ export default class PictureTracingDraw extends BaseDrawService {
|
|||||||
dh,
|
dh,
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
ctx.font = `bold 60px ${LETTER_FACE}`;
|
ctx.font = `bold 60px ${this.letterFace}`;
|
||||||
ctx.fillStyle = '#111';
|
ctx.fillStyle = '#111';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.textBaseline = 'middle';
|
ctx.textBaseline = 'middle';
|
||||||
@@ -195,11 +195,11 @@ export default class PictureTracingDraw extends BaseDrawService {
|
|||||||
const pairGap = 3;
|
const pairGap = 3;
|
||||||
|
|
||||||
const upperFs = getLetterFontSize(upper, sizes);
|
const upperFs = getLetterFontSize(upper, sizes);
|
||||||
ctx.font = `${upperFs}px ${LETTER_FACE}`;
|
ctx.font = `${upperFs}px ${this.letterFace}`;
|
||||||
const upperW = ctx.measureText(upper).width;
|
const upperW = ctx.measureText(upper).width;
|
||||||
|
|
||||||
const lowerFs = getLetterFontSize(lower, sizes);
|
const lowerFs = getLetterFontSize(lower, sizes);
|
||||||
ctx.font = `${lowerFs}px ${LETTER_FACE}`;
|
ctx.font = `${lowerFs}px ${this.letterFace}`;
|
||||||
const lowerW = ctx.measureText(lower).width;
|
const lowerW = ctx.measureText(lower).width;
|
||||||
|
|
||||||
const totalPairW = upperW + pairGap + lowerW;
|
const totalPairW = upperW + pairGap + lowerW;
|
||||||
@@ -213,7 +213,7 @@ export default class PictureTracingDraw extends BaseDrawService {
|
|||||||
y,
|
y,
|
||||||
FOUR_LINE_GRID_H,
|
FOUR_LINE_GRID_H,
|
||||||
sizes,
|
sizes,
|
||||||
{ fontFamily: LETTER_FACE, color },
|
{ fontFamily: this.letterFace, color },
|
||||||
);
|
);
|
||||||
drawLetterInFourLineGrid(
|
drawLetterInFourLineGrid(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -222,7 +222,7 @@ export default class PictureTracingDraw extends BaseDrawService {
|
|||||||
y,
|
y,
|
||||||
FOUR_LINE_GRID_H,
|
FOUR_LINE_GRID_H,
|
||||||
sizes,
|
sizes,
|
||||||
{ fontFamily: LETTER_FACE, color },
|
{ fontFamily: this.letterFace, color },
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
@@ -235,7 +235,7 @@ export default class PictureTracingDraw extends BaseDrawService {
|
|||||||
y: number,
|
y: number,
|
||||||
) {
|
) {
|
||||||
const ctx = this.ctx;
|
const ctx = this.ctx;
|
||||||
ctx.font = `36px ${LETTER_FACE}`;
|
ctx.font = `36px ${this.letterFace}`;
|
||||||
ctx.textAlign = 'left';
|
ctx.textAlign = 'left';
|
||||||
ctx.textBaseline = 'top';
|
ctx.textBaseline = 'top';
|
||||||
|
|
||||||
@@ -286,7 +286,7 @@ export default class PictureTracingDraw extends BaseDrawService {
|
|||||||
ctx.setLineDash([]);
|
ctx.setLineDash([]);
|
||||||
ctx.strokeRect(cx, cy, cellSize, cellSize);
|
ctx.strokeRect(cx, cy, cellSize, cellSize);
|
||||||
|
|
||||||
ctx.font = `18px ${LETTER_FACE}`;
|
ctx.font = `18px ${this.letterFace}`;
|
||||||
ctx.fillStyle = '#000';
|
ctx.fillStyle = '#000';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.textBaseline = 'middle';
|
ctx.textBaseline = 'middle';
|
||||||
@@ -380,7 +380,7 @@ export default class PictureTracingDraw extends BaseDrawService {
|
|||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
const fs = Math.min(imgH * 0.6, imgW * 0.18);
|
const fs = Math.min(imgH * 0.6, imgW * 0.18);
|
||||||
ctx.font = `bold ${fs}px ${LETTER_FACE}`;
|
ctx.font = `bold ${fs}px ${this.letterFace}`;
|
||||||
ctx.fillStyle = '#222';
|
ctx.fillStyle = '#222';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.textBaseline = 'middle';
|
ctx.textBaseline = 'middle';
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
import { ALPHABET_BY_LETTER, LETTERS_UPPER } from '../data/alphabet';
|
import { ALPHABET_BY_LETTER, LETTERS_UPPER } from '../data/alphabet';
|
||||||
|
|
||||||
/** 字母描红的布局模式(与产品文档四类练习一致) */
|
/** 字母描红的布局模式(与产品文档六类练习一致) */
|
||||||
export type LetterTracingMode =
|
export type LetterTracingMode =
|
||||||
/** 4 看图描红:教学区 + 6 行描红(前三行大写、后三行小写) */
|
/** 1 看图识字:教学区 + 描红行(前三行大写、后三行小写) */
|
||||||
| 'picture-tracing'
|
| 'picture-tracing'
|
||||||
/** 1 大小写配对:左栏 A–M、右栏 N–Z,每行最多 4 组 Aa */
|
/** 2 字母总览:Uppercase / Lowercase 分区,每区四行 7+7+6+6 */
|
||||||
|
| 'overview'
|
||||||
|
/** 3 大小写配对:左栏 A–M、右栏 N–Z,每行最多 4 组 Aa */
|
||||||
| 'case-pairing'
|
| 'case-pairing'
|
||||||
/** 2 分栏对照:半组字母,左栏大写、右栏小写,每行 4 个 */
|
/** 4 分栏对照:半组字母,左栏大写、右栏小写,每行 4 个 */
|
||||||
| 'column-compare'
|
| 'column-compare'
|
||||||
/** 3 字母总览:Uppercase / Lowercase 分区,每区 9+9+8 */
|
/** 5 三字母精练:每页聚焦 3 个字母,各含大写行 + 小写行 */
|
||||||
| 'overview';
|
| 'triple-focus'
|
||||||
|
/** 6 单字母逐行:每行一个字母,13 字母半表(A–M 或 N–Z) */
|
||||||
|
| 'single-line';
|
||||||
|
|
||||||
/** 字母大小写模式(部分模式不使用) */
|
/** 字母大小写模式(部分模式不使用) */
|
||||||
export type LetterCaseMode = 'upper' | 'lower' | 'both';
|
export type LetterCaseMode = 'upper' | 'lower' | 'both';
|
||||||
@@ -24,8 +28,10 @@ export interface LetterTracingGeneratorConfig {
|
|||||||
selectedLetter?: string;
|
selectedLetter?: string;
|
||||||
repetitions: number;
|
repetitions: number;
|
||||||
fadePattern: 'gradient' | 'first-only';
|
fadePattern: 'gradient' | 'first-only';
|
||||||
/** 分栏对照:练习 A–M 或 N–Z */
|
/** 分栏对照 / 单字母逐行:练习 A–M 或 N–Z */
|
||||||
alphabetHalf?: AlphabetHalf;
|
alphabetHalf?: AlphabetHalf;
|
||||||
|
/** 三字母精练:当前页聚焦的 3 个字母(如 ['A','B','C']) */
|
||||||
|
tripleLetters?: [string, string, string];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 单个描红格子的数据 */
|
/** 单个描红格子的数据 */
|
||||||
@@ -184,9 +190,14 @@ function chunk<T>(arr: T[], size: number): T[][] {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 总览区 26 字母拆成 9 + 9 + 8 三行 */
|
/** 总览区 26 字母拆成 7 + 7 + 6 + 6 四行(前两行 7 个、后两行 6 个,与四线三格排版一致) */
|
||||||
function overviewRowsFrom(letters: string[]): string[][] {
|
function overviewRowsFrom(letters: string[]): string[][] {
|
||||||
return [letters.slice(0, 9), letters.slice(9, 18), letters.slice(18, 26)];
|
return [
|
||||||
|
letters.slice(0, 7),
|
||||||
|
letters.slice(7, 14),
|
||||||
|
letters.slice(14, 20),
|
||||||
|
letters.slice(20, 26),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -194,7 +205,7 @@ function overviewRowsFrom(letters: string[]): string[][] {
|
|||||||
* - picture-tracing:教学区 + 6 行描红
|
* - picture-tracing:教学区 + 6 行描红
|
||||||
* - case-pairing:左 A–M / 右 N–Z,每行最多 4 组大小写配对
|
* - case-pairing:左 A–M / 右 N–Z,每行最多 4 组大小写配对
|
||||||
* - column-compare:半表,左大写右小写,每行 4 个
|
* - column-compare:半表,左大写右小写,每行 4 个
|
||||||
* - overview:Uppercase / Lowercase Letters,9+9+8
|
* - overview:Uppercase / Lowercase Letters,每区四行 7+7+6+6
|
||||||
*/
|
*/
|
||||||
export function generateLetterTracing(
|
export function generateLetterTracing(
|
||||||
config: LetterTracingGeneratorConfig,
|
config: LetterTracingGeneratorConfig,
|
||||||
@@ -295,5 +306,7 @@ export function mergeLetterTracingConfig(
|
|||||||
alphabetHalf: (overrides.alphabetHalf ??
|
alphabetHalf: (overrides.alphabetHalf ??
|
||||||
base.alphabetHalf ??
|
base.alphabetHalf ??
|
||||||
'A-M') as AlphabetHalf,
|
'A-M') as AlphabetHalf,
|
||||||
|
tripleLetters: overrides.tripleLetters ??
|
||||||
|
base.tripleLetters ?? ['A', 'B', 'C'],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -286,7 +286,7 @@ export const HOME_PRODUCT_TARGET_DATA: HomeDisplayDataset = {
|
|||||||
icon: '🔠',
|
icon: '🔠',
|
||||||
path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-single',
|
path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-single',
|
||||||
available: true,
|
available: true,
|
||||||
source: 'letter-tracing',
|
source: 'letter-tracing-single',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'target-english-2',
|
id: 'target-english-2',
|
||||||
|
|||||||
@@ -14,6 +14,25 @@ const STORAGE_KEY_WATCHED_AD_TODAY = 'watchedAdToday';
|
|||||||
// 免费下载次数限制
|
// 免费下载次数限制
|
||||||
const FREE_DOWNLOAD_LIMIT = 5;
|
const FREE_DOWNLOAD_LIMIT = 5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开发调试环境:本地 develop 包或开发者工具(含桌面模拟器)
|
||||||
|
* 此环境下不弹广告、不限制下载次数
|
||||||
|
*/
|
||||||
|
function isDevBypassLimits(): boolean {
|
||||||
|
try {
|
||||||
|
const env = wx.getAccountInfoSync().miniProgram.envVersion || 'release';
|
||||||
|
if (env === 'develop') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (wx.getSystemInfoSync().platform === 'devtools') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// 激励视频广告单元ID
|
// 激励视频广告单元ID
|
||||||
const AD_UNIT_ID = 'adunit-eff5626f8b68c2e0';
|
const AD_UNIT_ID = 'adunit-eff5626f8b68c2e0';
|
||||||
|
|
||||||
@@ -221,6 +240,11 @@ export async function downloadPrint(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isDevBypassLimits()) {
|
||||||
|
doDownload(canvas, options);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 获取今日下载次数
|
// 获取今日下载次数
|
||||||
const downloadCount = getTodayDownloadCount();
|
const downloadCount = getTodayDownloadCount();
|
||||||
const hasWatchedAd = hasWatchedAdToday();
|
const hasWatchedAd = hasWatchedAdToday();
|
||||||
|
|||||||
@@ -26,10 +26,17 @@
|
|||||||
{
|
{
|
||||||
"name": "englishPages/letterTracing/letterTracing",
|
"name": "englishPages/letterTracing/letterTracing",
|
||||||
"pathName": "englishPages/letterTracing/letterTracing",
|
"pathName": "englishPages/letterTracing/letterTracing",
|
||||||
"query": "id=letter-tracing-full",
|
"query": "id=letter-tracing-upper-lower",
|
||||||
"scene": null,
|
"scene": null,
|
||||||
"launchMode": "default"
|
"launchMode": "default"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "englishPages/letterTracing/letterTracing",
|
||||||
|
"pathName": "englishPages/letterTracing/letterTracing",
|
||||||
|
"query": "id=letter-tracing-full",
|
||||||
|
"launchMode": "default",
|
||||||
|
"scene": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "englishPages/letterTracing/letterTracing",
|
"name": "englishPages/letterTracing/letterTracing",
|
||||||
"pathName": "englishPages/letterTracing/letterTracing",
|
"pathName": "englishPages/letterTracing/letterTracing",
|
||||||
|
|||||||
Reference in New Issue
Block a user