feat: 样式修改、13 字母半表、三字母精练开发
This commit is contained in:
@@ -10,6 +10,7 @@
|
|||||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||||
"draw-ad": "../../components/draw-ad/draw-ad",
|
"draw-ad": "../../components/draw-ad/draw-ad",
|
||||||
"preview-footer-actions": "../../components3.0/preview-footer-actions/preview-footer-actions",
|
"preview-footer-actions": "../../components3.0/preview-footer-actions/preview-footer-actions",
|
||||||
"toy-icon": "../../toy/icon/icon"
|
"toy-icon": "../../toy/icon/icon",
|
||||||
|
"van-icon": "@vant/weapp/icon/index"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,134 +66,93 @@ page {
|
|||||||
box-shadow: @shadow;
|
box-shadow: @shadow;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== 参数面板 (bg-surface-container rounded-xl p-6 space-y-6) ===== */
|
/* ===== 独立 Section 区块(参考 UI 稿 space-y-4) ===== */
|
||||||
.lt-params {
|
.lt-section {
|
||||||
/* surface-container = #f0e7d6 */
|
|
||||||
background: @bg-card;
|
|
||||||
border-radius: @radius-xl;
|
|
||||||
padding: 48rpx;
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 48rpx;
|
gap: 32rpx;
|
||||||
box-shadow: @shadow;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 标题行 (gap-2 = 8px → 16rpx) */
|
.lt-section-title {
|
||||||
.lt-params-head {
|
font-size: 32rpx;
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lt-params-emoji {
|
|
||||||
/* text-xl = 20px → 40rpx */
|
|
||||||
font-size: 40rpx;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lt-params-title {
|
|
||||||
/* text-lg = 18px → 36rpx */
|
|
||||||
font-size: @fs-section-head-title;
|
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: @text-title;
|
color: #6d3b00;
|
||||||
|
padding-left: 8rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 参数区块内间距 (space-y-3 = 12px → 24rpx) */
|
.lt-section-body {
|
||||||
.param-block {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
/* space-y-3 = 12px → 24rpx */
|
|
||||||
gap: 24rpx;
|
gap: 24rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 字段标签 (text-sm font-bold text-on-surface-variant ml-1) */
|
/* ===== 练习类型 2列卡片网格 (grid-cols-2 gap-4) ===== */
|
||||||
.lt-field-label {
|
.lt-mode-grid {
|
||||||
display: block;
|
|
||||||
/* text-sm = 14px → 28rpx */
|
|
||||||
font-size: 28rpx;
|
|
||||||
font-weight: 700;
|
|
||||||
/* on-surface-variant = #605b50 */
|
|
||||||
color: @text-secondary;
|
|
||||||
/* ml-1 = 4px → 8rpx */
|
|
||||||
margin-left: 8rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 练习类型 Tab (grid-cols-2 gap-2 bg-surface-container-high p-1.5 rounded-lg) ===== */
|
|
||||||
.lt-mode-seg {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
/* gap-2 = 8px → 16rpx */
|
gap: 32rpx;
|
||||||
gap: 16rpx;
|
|
||||||
/* p-1.5 = 6px → 12rpx */
|
|
||||||
padding: 12rpx;
|
|
||||||
/* surface-container-high = #eae2d0 */
|
|
||||||
background: #eae2d0;
|
|
||||||
border-radius: 32rpx;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 未选中 item (py-2.5 px-3 text-sm font-medium rounded-lg text-on-surface-variant) */
|
.lt-mode-card {
|
||||||
.lt-mode-item {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
/* py-2.5 = 10px → 20rpx; px-3 = 12px → 24rpx */
|
gap: 16rpx;
|
||||||
padding: 20rpx 24rpx;
|
padding: 32rpx 24rpx;
|
||||||
/* rounded-lg = 8px → 16rpx */
|
border-radius: 24rpx;
|
||||||
border-radius: 32rpx;
|
background: #f8f0e0;
|
||||||
background: transparent;
|
|
||||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
// transition: all 1s ease;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
transition:
|
||||||
|
transform 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
|
background 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
|
box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 选中 item (bg-primary-container text-on-primary-container font-bold shadow-sm) */
|
.lt-mode-card__icon {
|
||||||
.lt-mode-item--active {
|
display: inline-flex;
|
||||||
/* primary-container = #ffd709 */
|
align-items: center;
|
||||||
background: @brand;
|
justify-content: center;
|
||||||
box-shadow: 0 2rpx 8rpx rgba(50, 46, 37, 0.08);
|
margin-bottom: 4rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lt-mode-item__text {
|
.lt-mode-card--active {
|
||||||
/* text-sm = 14px → 28rpx */
|
background: linear-gradient(145deg, #ffd709 0%, #efc900 100%);
|
||||||
font-size: 28rpx;
|
box-shadow: 0 8rpx 32rpx rgba(50, 46, 37, 0.06);
|
||||||
/* 未选中 font-medium = 500 */
|
}
|
||||||
font-weight: 500;
|
|
||||||
/* on-surface-variant = #605b50 */
|
.lt-mode-card--pressed {
|
||||||
color: @text-secondary; //#5b4b00
|
transform: scale(0.95);
|
||||||
line-height: 40rpx;
|
}
|
||||||
|
|
||||||
|
.lt-mode-card__label {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: @text-secondary;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 选中态字色 (on-primary-container = #5b4b00, font-bold = 700) */
|
.lt-mode-card--active .lt-mode-card__label {
|
||||||
.lt-mode-item--active .lt-mode-item__text {
|
color: #453900;
|
||||||
/* on-primary-container = #5b4b00 */
|
|
||||||
color: @text-selected-btn;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== 字母选择网格 (grid-cols-6 gap-2) ===== */
|
/* ===== 字母选择网格 (grid-cols-6 gap-3) ===== */
|
||||||
.lt-letter-grid {
|
.lt-letter-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
/* 稿 grid-cols-6 */
|
|
||||||
grid-template-columns: repeat(6, 1fr);
|
grid-template-columns: repeat(6, 1fr);
|
||||||
/* gap-2 = 8px → 16rpx */
|
gap: 28rpx;
|
||||||
gap: 16rpx;
|
padding: 0 8rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 字母格子 (aspect-square rounded-lg bg-surface-container-lowest) */
|
/* 字母格子 (aspect-square rounded-[12px] bg-surface-container) */
|
||||||
.lt-letter-cell {
|
.lt-letter-cell {
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-radius: 32rpx;
|
border-radius: 24rpx;
|
||||||
/* surface-container-lowest = #ffffff */
|
background: @bg-card;
|
||||||
background: @bg-white;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
/* 不用 :active:小程序 view 上背景色常在松手后才刷新;用 hover-class 才能按下即变色,并做过渡 */
|
|
||||||
transition:
|
transition:
|
||||||
background-color 0.2s ease,
|
background-color 0.2s ease,
|
||||||
transform 0.2s ease,
|
transform 0.2s ease,
|
||||||
@@ -215,10 +174,8 @@ page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.lt-letter-text {
|
.lt-letter-text {
|
||||||
/* text-lg = 18px → 36rpx */
|
font-size: 32rpx;
|
||||||
font-size: 36rpx;
|
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
/* on-surface-variant = #605b50 */
|
|
||||||
color: @text-secondary;
|
color: @text-secondary;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
transition: color 0.2s ease;
|
transition: color 0.2s ease;
|
||||||
@@ -233,27 +190,79 @@ page {
|
|||||||
color: @text-selected-btn;
|
color: @text-selected-btn;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== 大小写切换 ===== */
|
/* ===== 展开/收起按钮(UI 稿 rounded-full bg-surface-container-low) ===== */
|
||||||
|
.lt-expand-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
padding: 12rpx 32rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: #f8f0e0;
|
||||||
|
align-self: center;
|
||||||
|
margin: 16rpx auto 0;
|
||||||
|
transition:
|
||||||
|
background-color 0.2s ease,
|
||||||
|
transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lt-expand-toggle--hover {
|
||||||
|
background: @bg-card;
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lt-expand-toggle__text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: @text-secondary;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lt-expand-toggle__icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 大小写切换 / 字母分组选择 ===== */
|
||||||
.lt-chip-row {
|
.lt-chip-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
/* gap-2 = 8px → 16rpx */
|
/* gap-2 = 8px → 16rpx */
|
||||||
gap: 16rpx;
|
gap: 32rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 未选中 chip (同练习类型 item 风格) */
|
.lt-chip-row--wrap {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.lt-chip {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lt-chip-row--triple {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 24rpx;
|
||||||
|
|
||||||
|
.lt-chip {
|
||||||
|
flex: unset;
|
||||||
|
width: 100%;
|
||||||
|
padding: 24rpx 16rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 未选中 chip(与字母格子统一底色) */
|
||||||
.lt-chip {
|
.lt-chip {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
box-sizing: border-box;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
/* py-2.5 px-3 */
|
|
||||||
padding: 20rpx 24rpx;
|
padding: 20rpx 24rpx;
|
||||||
/* text-sm = 14px → 28rpx */
|
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
font-weight: 500;
|
font-weight: 700;
|
||||||
line-height: 40rpx;
|
line-height: 40rpx;
|
||||||
color: @text-secondary;
|
color: @text-secondary;
|
||||||
background: #fff;
|
background: @bg-card;
|
||||||
/* rounded-lg */
|
|
||||||
border-radius: 32rpx;
|
border-radius: 32rpx;
|
||||||
transition:
|
transition:
|
||||||
background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
type LetterTracingGeneratorConfig,
|
type LetterTracingGeneratorConfig,
|
||||||
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, LETTERS_PAIRS } from '../shared/data/alphabet';
|
||||||
import {
|
import {
|
||||||
DEFAULT_LETTER_PROFILE,
|
DEFAULT_LETTER_PROFILE,
|
||||||
getFontProfile,
|
getFontProfile,
|
||||||
@@ -21,34 +21,35 @@ import { loadLetterFont } from '../shared/draw/drawTools';
|
|||||||
const MODES = [
|
const MODES = [
|
||||||
{
|
{
|
||||||
id: 'letter-tracing-single',
|
id: 'letter-tracing-single',
|
||||||
label: '看图识字',
|
label: '默认字帖',
|
||||||
desc: '配图、例句与描红',
|
desc: '配图、例句与描红',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'letter-tracing-upper-lower',
|
id: 'letter-tracing-upper-lower',
|
||||||
label: '字母总览',
|
// label: '字母总览',
|
||||||
|
label: '基础描红',
|
||||||
desc: 'Uppercase / Lowercase 总览',
|
desc: 'Uppercase / Lowercase 总览',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'letter-tracing-case-pairing',
|
id: 'letter-tracing-case-pairing',
|
||||||
label: '大小写练习',
|
label: '大小写对照',
|
||||||
desc: '半组字母左大写右小写',
|
desc: '半组字母左大写右小写',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'letter-tracing-two-column',
|
id: 'letter-tracing-two-column',
|
||||||
label: '两列练习',
|
label: '两列描红',
|
||||||
desc: '左 A–M、右 N–Z 配对描红',
|
desc: '左 A–M、右 N–Z 配对描红',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'letter-tracing-single-line',
|
||||||
|
label: '13字母半表',
|
||||||
|
desc: '每行一个字母,13 字母半表',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'letter-tracing-triple',
|
id: 'letter-tracing-triple',
|
||||||
label: '三字母精练',
|
label: '三字母精练',
|
||||||
desc: '每页聚焦 3 个字母深度书写',
|
desc: '每页聚焦 3 个字母深度书写',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'letter-tracing-single-line',
|
|
||||||
label: '单字母逐行',
|
|
||||||
desc: '每行一个字母,13 字母半表',
|
|
||||||
},
|
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const MODE_MAP = Object.fromEntries(MODES.map((m) => [m.id, m])) as Record<
|
const MODE_MAP = Object.fromEntries(MODES.map((m) => [m.id, m])) as Record<
|
||||||
@@ -56,6 +57,15 @@ const MODE_MAP = Object.fromEntries(MODES.map((m) => [m.id, m])) as Record<
|
|||||||
(typeof MODES)[number]
|
(typeof MODES)[number]
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/** 三字母精练分组:26 字母每 3 个一组 */
|
||||||
|
const TRIPLE_GROUPS: { label: string; letters: string[] }[] = [];
|
||||||
|
for (let i = 0; i < LETTERS_UPPER.length; i += 3) {
|
||||||
|
const group = LETTERS_UPPER.slice(i, i + 3);
|
||||||
|
const label =
|
||||||
|
group.length > 1 ? `${group[0]}–${group[group.length - 1]}` : group[0];
|
||||||
|
TRIPLE_GROUPS.push({ label, letters: [...group] });
|
||||||
|
}
|
||||||
|
|
||||||
/** 根据 worksheetId 查找页面元信息 */
|
/** 根据 worksheetId 查找页面元信息 */
|
||||||
function pageInfoLookup(id: string) {
|
function pageInfoLookup(id: string) {
|
||||||
const m = MODE_MAP[id];
|
const m = MODE_MAP[id];
|
||||||
@@ -66,11 +76,17 @@ type PageData = CanvasDataState & {
|
|||||||
worksheetId: string;
|
worksheetId: string;
|
||||||
traceMode: LetterTracingMode;
|
traceMode: LetterTracingMode;
|
||||||
selectedLetter: string;
|
selectedLetter: string;
|
||||||
|
selectedLetterPair: string;
|
||||||
letterCaseLower: boolean;
|
letterCaseLower: boolean;
|
||||||
showLetterPicker: boolean;
|
showLetterPicker: boolean;
|
||||||
showCaseToggle: boolean;
|
showCaseToggle: boolean;
|
||||||
showNextLetter: boolean;
|
showNextLetter: boolean;
|
||||||
|
showTripleGroupPicker: boolean;
|
||||||
|
tripleGroups: { label: string; letters: string[] }[];
|
||||||
|
selectedTripleGroupIdx: number;
|
||||||
letterGrid: string[];
|
letterGrid: string[];
|
||||||
|
letterGridExpanded: boolean;
|
||||||
|
letterGridPreview: string[];
|
||||||
modeOptions: ReadonlyArray<{ id: string; label: string; desc: string }>;
|
modeOptions: ReadonlyArray<{ id: string; label: string; desc: string }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -91,11 +107,17 @@ createPage(
|
|||||||
worksheetId: 'letter-tracing-single',
|
worksheetId: 'letter-tracing-single',
|
||||||
traceMode: 'letter-tracing-single',
|
traceMode: 'letter-tracing-single',
|
||||||
selectedLetter: 'A',
|
selectedLetter: 'A',
|
||||||
|
selectedLetterPair: 'Aa',
|
||||||
letterCaseLower: false,
|
letterCaseLower: false,
|
||||||
showLetterPicker: true,
|
showLetterPicker: true,
|
||||||
showCaseToggle: false,
|
showCaseToggle: false,
|
||||||
showNextLetter: true,
|
showNextLetter: true,
|
||||||
letterGrid: LETTERS_UPPER,
|
showTripleGroupPicker: false,
|
||||||
|
tripleGroups: TRIPLE_GROUPS,
|
||||||
|
selectedTripleGroupIdx: 0,
|
||||||
|
letterGrid: LETTERS_PAIRS,
|
||||||
|
letterGridExpanded: false,
|
||||||
|
letterGridPreview: LETTERS_PAIRS.slice(0, 6),
|
||||||
modeOptions: [...MODES],
|
modeOptions: [...MODES],
|
||||||
} as unknown as PageData,
|
} as unknown as PageData,
|
||||||
|
|
||||||
@@ -119,7 +141,10 @@ createPage(
|
|||||||
const updates: Partial<PageData> = {};
|
const updates: Partial<PageData> = {};
|
||||||
if (options.letter) {
|
if (options.letter) {
|
||||||
const normalized = normalizeLetter(options.letter);
|
const normalized = normalizeLetter(options.letter);
|
||||||
if (normalized) updates.selectedLetter = normalized;
|
if (normalized) {
|
||||||
|
updates.selectedLetter = normalized;
|
||||||
|
updates.selectedLetterPair = `${normalized}${normalized.toLowerCase()}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (options.case === 'lower') {
|
if (options.case === 'lower') {
|
||||||
updates.letterCaseLower = true;
|
updates.letterCaseLower = true;
|
||||||
@@ -174,6 +199,7 @@ createPage(
|
|||||||
const needsHalf =
|
const needsHalf =
|
||||||
this.data.traceMode === 'letter-tracing-case-pairing' ||
|
this.data.traceMode === 'letter-tracing-case-pairing' ||
|
||||||
this.data.traceMode === 'letter-tracing-single-line';
|
this.data.traceMode === 'letter-tracing-single-line';
|
||||||
|
const isTriple = this.data.traceMode === 'letter-tracing-triple';
|
||||||
return mergeLetterTracingConfig(base, {
|
return mergeLetterTracingConfig(base, {
|
||||||
selectedLetter: this.data.selectedLetter,
|
selectedLetter: this.data.selectedLetter,
|
||||||
letterCase,
|
letterCase,
|
||||||
@@ -184,6 +210,13 @@ createPage(
|
|||||||
: 'A-M',
|
: 'A-M',
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
|
...(isTriple
|
||||||
|
? {
|
||||||
|
tripleLetters:
|
||||||
|
TRIPLE_GROUPS[this.data.selectedTripleGroupIdx]
|
||||||
|
?.letters,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -203,11 +236,22 @@ createPage(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 用户在字母网格中选择了一个字母 */
|
/** 展开/收起字母网格 */
|
||||||
|
toggleLetterGrid() {
|
||||||
|
this.setData({
|
||||||
|
letterGridExpanded: !this.data.letterGridExpanded,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 用户在字母网格中选择了一个字母(item 为 "Aa" 格式,取首字母作为 selectedLetter) */
|
||||||
onSelectLetter(e: WechatMiniprogram.TouchEvent) {
|
onSelectLetter(e: WechatMiniprogram.TouchEvent) {
|
||||||
const letter = e.currentTarget.dataset.letter as string;
|
const raw = e.currentTarget.dataset.letter as string;
|
||||||
if (!letter) return;
|
if (!raw) return;
|
||||||
this.setData({ selectedLetter: letter });
|
const letter = raw.charAt(0).toUpperCase();
|
||||||
|
this.setData({
|
||||||
|
selectedLetter: letter,
|
||||||
|
selectedLetterPair: `${letter}${letter.toLowerCase()}`,
|
||||||
|
});
|
||||||
this.drawCanvas();
|
this.drawCanvas();
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -228,7 +272,18 @@ createPage(
|
|||||||
const list = LETTERS_UPPER;
|
const list = LETTERS_UPPER;
|
||||||
const i = list.indexOf(this.data.selectedLetter);
|
const i = list.indexOf(this.data.selectedLetter);
|
||||||
const next = list[(i + 1) % list.length];
|
const next = list[(i + 1) % list.length];
|
||||||
this.setData({ selectedLetter: next });
|
this.setData({
|
||||||
|
selectedLetter: next,
|
||||||
|
selectedLetterPair: `${next}${next.toLowerCase()}`,
|
||||||
|
});
|
||||||
|
this.drawCanvas();
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 用户在三字母分组选择器中选择了一组 */
|
||||||
|
onSelectTripleGroup(e: WechatMiniprogram.TouchEvent) {
|
||||||
|
const idx = Number(e.currentTarget.dataset.idx);
|
||||||
|
if (isNaN(idx) || idx === this.data.selectedTripleGroupIdx) return;
|
||||||
|
this.setData({ selectedTripleGroupIdx: idx });
|
||||||
this.drawCanvas();
|
this.drawCanvas();
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -253,13 +308,12 @@ createPage(
|
|||||||
const merged = mergeLetterTracingConfig(base, {});
|
const merged = mergeLetterTracingConfig(base, {});
|
||||||
|
|
||||||
const traceMode = merged.mode;
|
const traceMode = merged.mode;
|
||||||
const showLetterPicker =
|
const showLetterPicker = traceMode === 'letter-tracing-single';
|
||||||
traceMode === 'letter-tracing-single' ||
|
|
||||||
traceMode === 'letter-tracing-triple';
|
|
||||||
const showCaseToggle =
|
const showCaseToggle =
|
||||||
traceMode === 'letter-tracing-case-pairing' ||
|
traceMode === 'letter-tracing-case-pairing' ||
|
||||||
traceMode === 'letter-tracing-single-line';
|
traceMode === 'letter-tracing-single-line';
|
||||||
const showNextLetter = traceMode === 'letter-tracing-single';
|
const showNextLetter = traceMode === 'letter-tracing-single';
|
||||||
|
const showTripleGroupPicker = traceMode === 'letter-tracing-triple';
|
||||||
const selectedLetter = options?.preserveLetter
|
const selectedLetter = options?.preserveLetter
|
||||||
? (this.data.selectedLetter ?? 'A')
|
? (this.data.selectedLetter ?? 'A')
|
||||||
: normalizeLetter(merged.selectedLetter ?? '');
|
: normalizeLetter(merged.selectedLetter ?? '');
|
||||||
@@ -277,10 +331,12 @@ createPage(
|
|||||||
functionId: worksheetId,
|
functionId: worksheetId,
|
||||||
traceMode,
|
traceMode,
|
||||||
selectedLetter,
|
selectedLetter,
|
||||||
|
selectedLetterPair: `${selectedLetter}${selectedLetter.toLowerCase()}`,
|
||||||
letterCaseLower,
|
letterCaseLower,
|
||||||
showLetterPicker,
|
showLetterPicker,
|
||||||
showCaseToggle,
|
showCaseToggle,
|
||||||
showNextLetter,
|
showNextLetter,
|
||||||
|
showTripleGroupPicker,
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
this.initPageInfo(
|
this.initPageInfo(
|
||||||
|
|||||||
@@ -15,33 +15,38 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="lt-params">
|
<!-- 练习类型(2列卡片网格) -->
|
||||||
<view class="lt-params-head">
|
<view class="lt-section">
|
||||||
<text class="lt-params-emoji">⚙️</text>
|
<text class="lt-section-title">练习类型</text>
|
||||||
<text class="lt-params-title">参数设置</text>
|
<view class="lt-mode-grid">
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="param-block">
|
|
||||||
<text class="lt-field-label">练习类型</text>
|
|
||||||
<view class="lt-mode-seg">
|
|
||||||
<view
|
<view
|
||||||
wx:for="{{modeOptions}}"
|
wx:for="{{modeOptions}}"
|
||||||
wx:key="id"
|
wx:key="id"
|
||||||
class="lt-mode-item {{worksheetId === item.id ? 'lt-mode-item--active' : ''}}"
|
class="lt-mode-card {{worksheetId === item.id ? 'lt-mode-card--active' : ''}}"
|
||||||
data-id="{{item.id}}"
|
data-id="{{item.id}}"
|
||||||
|
hover-class="lt-mode-card--pressed"
|
||||||
|
hover-start-time="0"
|
||||||
|
hover-stay-time="70"
|
||||||
bindtap="onSelectMode">
|
bindtap="onSelectMode">
|
||||||
<text class="lt-mode-item__text">{{item.label}}</text>
|
<toy-icon
|
||||||
|
name="moon"
|
||||||
|
size="56rpx"
|
||||||
|
color="{{worksheetId === item.id ? '#453900' : '#605b50'}}"
|
||||||
|
custom-class="lt-mode-card__icon" />
|
||||||
|
<text class="lt-mode-card__label">{{item.label}}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view wx:if="{{showLetterPicker}}" class="param-block">
|
<!-- 选择字母(默认字帖模式) -->
|
||||||
<text class="lt-field-label">选择字母</text>
|
<view wx:if="{{showLetterPicker}}" class="lt-section">
|
||||||
<view class="lt-letter-grid">
|
<text class="lt-section-title">选择字母</text>
|
||||||
|
<view class="lt-section-body">
|
||||||
|
<view class="lt-letter-grid" wx:if="{{!letterGridExpanded}}">
|
||||||
<view
|
<view
|
||||||
wx:for="{{letterGrid}}"
|
wx:for="{{letterGridPreview}}"
|
||||||
wx:key="*this"
|
wx:key="*this"
|
||||||
class="lt-letter-cell {{selectedLetter === item ? 'lt-letter-cell--active' : ''}}"
|
class="lt-letter-cell {{selectedLetterPair === item ? 'lt-letter-cell--active' : ''}}"
|
||||||
data-letter="{{item}}"
|
data-letter="{{item}}"
|
||||||
hover-class="lt-letter-cell--pressed"
|
hover-class="lt-letter-cell--pressed"
|
||||||
hover-start-time="0"
|
hover-start-time="0"
|
||||||
@@ -50,10 +55,47 @@
|
|||||||
<text class="lt-letter-text">{{item}}</text>
|
<text class="lt-letter-text">{{item}}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
<view class="lt-letter-grid" wx:if="{{letterGridExpanded}}">
|
||||||
|
<view
|
||||||
|
wx:for="{{letterGrid}}"
|
||||||
|
wx:key="*this"
|
||||||
|
class="lt-letter-cell {{selectedLetterPair === item ? 'lt-letter-cell--active' : ''}}"
|
||||||
|
data-letter="{{item}}"
|
||||||
|
hover-class="lt-letter-cell--pressed"
|
||||||
|
hover-start-time="0"
|
||||||
|
hover-stay-time="70"
|
||||||
|
bind:tap="onSelectLetter">
|
||||||
|
<text class="lt-letter-text">{{item}}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="lt-expand-toggle"
|
||||||
|
hover-class="lt-expand-toggle--hover"
|
||||||
|
hover-start-time="0"
|
||||||
|
hover-stay-time="70"
|
||||||
|
bind:tap="toggleLetterGrid">
|
||||||
|
<text class="lt-expand-toggle__text"
|
||||||
|
>{{letterGridExpanded ? '收起' : '展开全部'}}</text
|
||||||
|
>
|
||||||
|
<van-icon
|
||||||
|
wx:if="{{letterGridExpanded}}"
|
||||||
|
name="arrow-up"
|
||||||
|
size="24rpx"
|
||||||
|
color="#605b50"
|
||||||
|
custom-class="lt-expand-toggle__icon" />
|
||||||
|
<van-icon
|
||||||
|
wx:else
|
||||||
|
name="arrow-down"
|
||||||
|
size="24rpx"
|
||||||
|
color="#605b50"
|
||||||
|
custom-class="lt-expand-toggle__icon" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view wx:if="{{showCaseToggle}}" class="param-block">
|
<!-- 选择字母组(大小写对照 / 13字母半表模式) -->
|
||||||
<text class="lt-field-label">半组字母</text>
|
<view wx:if="{{showCaseToggle}}" class="lt-section">
|
||||||
|
<text class="lt-section-title">选择字母组</text>
|
||||||
<view class="lt-chip-row">
|
<view class="lt-chip-row">
|
||||||
<view
|
<view
|
||||||
class="lt-chip {{!letterCaseLower ? 'lt-chip--active' : ''}}"
|
class="lt-chip {{!letterCaseLower ? 'lt-chip--active' : ''}}"
|
||||||
@@ -73,6 +115,23 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 选择字母组(三字母精练模式) -->
|
||||||
|
<view wx:if="{{showTripleGroupPicker}}" class="lt-section">
|
||||||
|
<text class="lt-section-title">选择字母组</text>
|
||||||
|
<view class="lt-chip-row lt-chip-row--wrap lt-chip-row--triple">
|
||||||
|
<view
|
||||||
|
wx:for="{{tripleGroups}}"
|
||||||
|
wx:key="label"
|
||||||
|
class="lt-chip lt-chip--triple {{selectedTripleGroupIdx === index ? 'lt-chip--active' : ''}}"
|
||||||
|
hover-class="lt-chip--pressed"
|
||||||
|
hover-start-time="0"
|
||||||
|
hover-stay-time="70"
|
||||||
|
data-idx="{{index}}"
|
||||||
|
bind:tap="onSelectTripleGroup">
|
||||||
|
{{item.label}}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view
|
<view
|
||||||
|
|||||||
@@ -37,3 +37,4 @@ export const ALPHABET_BY_LETTER: Record<string, AlphabetEntry> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const LETTERS_UPPER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
|
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 CasePairingDraw from './casePairingDraw';
|
||||||
import TwoColumnDraw from './twoColumnDraw';
|
import TwoColumnDraw from './twoColumnDraw';
|
||||||
import UpperLowerDraw from './upperLowerDraw';
|
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-upper-lower': UpperLowerDraw,
|
||||||
'letter-tracing-case-pairing': CasePairingDraw,
|
'letter-tracing-case-pairing': CasePairingDraw,
|
||||||
'letter-tracing-two-column': TwoColumnDraw,
|
'letter-tracing-two-column': TwoColumnDraw,
|
||||||
|
'letter-tracing-single-line': SingleLineDraw,
|
||||||
|
'letter-tracing-triple': TripleDraw,
|
||||||
}[data.mode];
|
}[data.mode];
|
||||||
|
|
||||||
const delegate = new DrawClass(this.canvas, this.ctx, this.options);
|
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'
|
| 'letter-tracing-case-pairing'
|
||||||
/** 4 分栏对照:半组字母,左栏大写、右栏小写,每行 4 个 */
|
/** 4 分栏对照:半组字母,左栏大写、右栏小写,每行 4 个 */
|
||||||
| 'letter-tracing-two-column'
|
| 'letter-tracing-two-column'
|
||||||
/** 5 三字母精练:每页聚焦 3 个字母,各含大写行 + 小写行 */
|
/** 5 三字母精练:每页聚焦 3 个字母,每字母 4 行(2 行示范 + 2 行简练) */
|
||||||
| 'letter-tracing-triple'
|
| 'letter-tracing-triple'
|
||||||
/** 6 单字母逐行:每行一个字母,13 字母半表(A–M 或 N–Z) */
|
/** 6 单字母逐行:每行一个字母,13 字母半表(A–M 或 N–Z) */
|
||||||
| 'letter-tracing-single-line';
|
| 'letter-tracing-single-line';
|
||||||
@@ -31,7 +31,7 @@ export interface LetterTracingGeneratorConfig {
|
|||||||
/** 分栏对照 / 单字母逐行:练习 A–M 或 N–Z */
|
/** 分栏对照 / 单字母逐行:练习 A–M 或 N–Z */
|
||||||
alphabetHalf?: AlphabetHalf;
|
alphabetHalf?: AlphabetHalf;
|
||||||
/** 三字母精练:当前页聚焦的 3 个字母(如 ['A','B','C']) */
|
/** 三字母精练:当前页聚焦的 3 个字母(如 ['A','B','C']) */
|
||||||
tripleLetters?: [string, string, string];
|
tripleLetters?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 单个描红格子的数据 */
|
/** 单个描红格子的数据 */
|
||||||
@@ -49,6 +49,28 @@ export interface TracingRow {
|
|||||||
cells: TracingCell[];
|
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 网格,混入目标字母) */
|
/** Highlight It 格子数据(3×6 网格,混入目标字母) */
|
||||||
export interface HighlightGrid {
|
export interface HighlightGrid {
|
||||||
rows: string[][];
|
rows: string[][];
|
||||||
@@ -88,6 +110,14 @@ export type LetterTracingData =
|
|||||||
leftRows: TracingRow[];
|
leftRows: TracingRow[];
|
||||||
rightRows: TracingRow[];
|
rightRows: TracingRow[];
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
mode: 'letter-tracing-single-line';
|
||||||
|
rows: SingleLineRow[];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
mode: 'letter-tracing-triple';
|
||||||
|
sections: TripleSection[];
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
mode: 'letter-tracing-upper-lower';
|
mode: 'letter-tracing-upper-lower';
|
||||||
upperRows: string[][];
|
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 行描红
|
* - 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
|
// letter-tracing-upper-lower
|
||||||
return {
|
return {
|
||||||
mode: 'letter-tracing-upper-lower',
|
mode: 'letter-tracing-upper-lower',
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Design System Strategy: Doodle Mini
|
||||||
|
|
||||||
|
## 1. Overview & Creative North Star: "The Sun-Drenched Studio"
|
||||||
|
The creative direction for this design system is **"The Sun-Drenched Studio."** We are moving away from the cold, clinical "educational app" look and toward a space that feels like a physical, tactile craft room bathed in morning light.
|
||||||
|
|
||||||
|
Instead of a rigid, boxed-in grid, we embrace **Organic Asymmetry**. Elements should feel like they were placed on a table by hand, not snapped to a pixel-perfect wireframe. We break the "template" look by using exaggerated corner radii, overlapping "paper" layers, and a hierarchy that prioritizes warmth and play over traditional data density.
|
||||||
|
|
||||||
|
## 2. Colors: Tonal Radiance
|
||||||
|
The palette is anchored by `primary` (#6c5a00) and `primary_container` (#ffd709), creating a "Yaya Yellow" experience that is vibrant yet sophisticated.
|
||||||
|
|
||||||
|
* **The "No-Line" Rule:** Under no circumstances should 1px solid borders be used to separate content. We define boundaries exclusively through background shifts. Use `surface_container_low` (#f8f0e0) for main content areas and `surface` (#fef6e7) for the global backdrop.
|
||||||
|
* **Surface Hierarchy & Nesting:** Treat the UI as stacked sheets of heavy-weight construction paper.
|
||||||
|
* **Level 0:** `background` (#fef6e7) – The desk surface.
|
||||||
|
* **Level 1:** `surface_container` (#f0e7d6) – The worksheet base.
|
||||||
|
* **Level 2:** `surface_container_lowest` (#ffffff) – Active drawing cards or "Paper" modals.
|
||||||
|
* **The Glass & Gradient Rule:** For navigation bars or floating tool palettes, use Glassmorphism. Apply `surface_container_low` at 80% opacity with a `20px` backdrop blur. This ensures the "sunny" background bleeds through, maintaining the "Sun-Drenched" atmosphere.
|
||||||
|
* **Signature Textures:** Main CTAs should not be flat. Apply a subtle linear gradient from `primary_fixed` (#ffd709) to `primary_fixed_dim` (#efc900) at a 145-degree angle to give buttons a "pillowy" 3D volume.
|
||||||
|
|
||||||
|
## 3. Typography: The Friendly Guide
|
||||||
|
We use **Plus Jakarta Sans** exclusively. Its geometric yet soft terminals provide a modern, legible, and approachable voice.
|
||||||
|
|
||||||
|
* **Display & Headlines:** Use `display-lg` (3.5rem) for "Doodle Mini" brand moments. Headlines (`headline-md`) should use a tighter letter-spacing (-0.02em) to feel "hugged" and friendly.
|
||||||
|
* **Body & Labels:** Use `body-lg` for all instructional text. In a children's context, readability is paramount; ensure a line height of at least 1.6x the font size.
|
||||||
|
* **Hierarchy as Personality:** Use `on_secondary_container` (#6d3b00) for sub-headers to provide a warm, "chocolatey" contrast against the yellow, moving away from harsh blacks.
|
||||||
|
|
||||||
|
## 4. Elevation & Depth: Tonal Layering
|
||||||
|
Depth in this system is a result of light and shadow, not lines and strokes.
|
||||||
|
|
||||||
|
* **The Layering Principle:** To lift a component, place a `surface_container_highest` (#e5dcc9) element inside a `surface_container_low` (#f8f0e0) environment. This creates a "recessed" or "elevated" feel through color value alone.
|
||||||
|
* **Ambient Shadows:** Use shadows sparingly. When an object "floats" (like a floating action button), use a shadow color derived from `on_surface` (#322e25) at 6% opacity, with a blur of `32px` and a Y-offset of `8px`. It should look like a soft glow, not a drop shadow.
|
||||||
|
* **The "Ghost Border" Fallback:** If a separator is required for accessibility in input fields, use `outline_variant` (#b3ac9f) at **15% opacity**. It should be felt, not seen.
|
||||||
|
|
||||||
|
## 5. Components: Tactile & Safe
|
||||||
|
Every component must feel "squishy" and safe to touch.
|
||||||
|
|
||||||
|
* **Buttons:**
|
||||||
|
* **Primary:** Uses `primary_container` (#ffd709) with a `full` (9999px) radius.
|
||||||
|
* **States:** On press, scale the button down to 96% size to simulate a physical "click" into the screen.
|
||||||
|
* **Cards & Lists:** Forbid divider lines. Separate list items using a `spacing-3` (1rem) gap and alternating background colors between `surface_container_low` and `surface_container`.
|
||||||
|
* **Input Fields:** Use `surface_container_highest` for the input track. Corners must be `xl` (3rem). Labels sit *above* the field in `title-sm` to ensure children can always see the context.
|
||||||
|
* **Selection Chips:** Use `secondary_container` (#ffc697) for selected states. The roundedness must be `full` to mimic smooth river stones.
|
||||||
|
* **The "Joy" Progress Bar:** A custom component for Doodle Mini. Use a thick track (`1.5rem` height) in `surface_container_highest` with the progress indicator using a gradient of `tertiary` (#006b1b) to `tertiary_fixed` (#91f78e).
|
||||||
|
|
||||||
|
## 6. Do’s and Don’ts
|
||||||
|
|
||||||
|
### Do:
|
||||||
|
* **Do** use `xl` and `full` corner radii for almost everything. Sharp corners are "scary."
|
||||||
|
* **Do** lean into white space. Use `spacing-12` (4rem) between major sections to let the design breathe.
|
||||||
|
* **Do** use `on_tertiary_container` (dark green) for success states; it feels organic and "earthy" against the yellow.
|
||||||
|
|
||||||
|
### Don’t:
|
||||||
|
* **Don’t** use pure black (#000000). Use `on_surface` (#322e25) for all dark text to maintain the warm, sunny tone.
|
||||||
|
* **Don’t** use 1px dividers. If you feel you need one, use a `surface_variant` (#e5dcc9) color block that is `4px` thick with `full` rounded caps.
|
||||||
|
* **Don’t** crowd the interface. If a screen feels busy, increase the background "paper" size and use a scrollable layout instead of shrinking elements.
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
Reference in New Issue
Block a user