feat: 完成绘制汉字基础功能
This commit is contained in:
+13
-1
@@ -4,7 +4,7 @@
|
||||
"pages/shape/index",
|
||||
"pages/debug/debug",
|
||||
"pages/wordDemo/index",
|
||||
"pages/copyBook/index",
|
||||
"pages/copyBook/copyBook",
|
||||
"pages/buttonTest/buttonTest"
|
||||
],
|
||||
"window": {},
|
||||
@@ -42,6 +42,18 @@
|
||||
"iconPath": "assets/tabBar/icon-shape.png",
|
||||
"selectedIconPath": "assets/tabBar/icon-shape-active.png",
|
||||
"text": "图形"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/copyBook/copyBook",
|
||||
"iconPath": "assets/tabBar/icon-shape.png",
|
||||
"selectedIconPath": "assets/tabBar/icon-shape-active.png",
|
||||
"text": "练字"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/wordDemo/index",
|
||||
"iconPath": "assets/tabBar/icon-shape.png",
|
||||
"selectedIconPath": "assets/tabBar/icon-shape-active.png",
|
||||
"text": "练字2"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -18,6 +18,10 @@ Component({
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
disabledColor: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
/**
|
||||
* 组件的初始数据
|
||||
@@ -27,7 +31,7 @@ Component({
|
||||
color: '',
|
||||
},
|
||||
lifetimes: {
|
||||
attached() {},
|
||||
attached() { },
|
||||
},
|
||||
/**
|
||||
* 组件的方法列表
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<view class="word-card">
|
||||
<view
|
||||
wx:if="{{!disabledColor}}"
|
||||
class="color-box"
|
||||
style="background-color: {{color}};"
|
||||
bindtap="onColorTap"></view>
|
||||
|
||||
@@ -30,12 +30,12 @@ Component({
|
||||
ready() {
|
||||
},
|
||||
},
|
||||
observers: {
|
||||
/* observers: {
|
||||
cardList(newVal: CardList) {
|
||||
const selectedWords = newVal.map((item) => item.word);
|
||||
this.setData({ selectedWords });
|
||||
},
|
||||
},
|
||||
}, */
|
||||
/**
|
||||
* 组件的方法列表
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
{
|
||||
"其": [
|
||||
"M8.1 14.04 L9.72 15.66 L27 13.5 L44.82 11.88 L42.66 10.26 L39.96 10.8 L27 12.42 Z",
|
||||
"M18.36 35.1 L18.36 7.02 L19.44 4.86 L16.2 1.62 L16.74 7.02 L16.74 35.1 Z",
|
||||
"M34.56 18.9 L34.56 5.4 L35.64 3.24 L32.4 0 L32.94 5.4 L32.94 18.9 L31.86 34.02 L33.48 34.02 Z",
|
||||
"M17.82 20.52 L18.9 21.6 L24.3 21.06 L29.7 20.52 L27.54 18.9 L25.38 19.44 L24.3 19.98 Z",
|
||||
"M17.82 28.08 L18.9 29.16 L24.3 28.62 L29.7 28.08 L28.08 26.46 L24.3 27.54 Z",
|
||||
"M2.7 36.18 L4.32 37.8 L26.46 35.64 L48.6 34.02 L45.9 31.86 L43.2 32.94 L26.46 34.56 Z",
|
||||
"M19.98 38.34 L18.9 40.5 L16.74 42.66 L11.34 46.98 L8.1 49.14 L4.86 50.76 L6.48 50.76 L10.26 48.6 L16.74 44.28 L21.06 42.12 Z",
|
||||
"M41.58 49.14 L41.58 45.36 L35.1 41.58 L28.62 38.34 Z"
|
||||
],
|
||||
"值": [
|
||||
"M15.12 9.18 L17.82 5.94 L15.66 2.16 L14.58 6.48 L10.26 14.58 L4.32 22.14 L1.08 25.38 L2.16 25.38 L7.56 19.98 Z",
|
||||
"M11.88 42.66 L11.88 14.58 L10.8 14.58 L10.8 42.66 L9.72 46.44 L11.88 50.22 Z",
|
||||
"M16.74 12.42 L18.36 14.04 L31.86 11.88 L45.36 10.8 L43.2 8.64 L39.42 9.72 L31.86 10.8 Z",
|
||||
"M31.32 9.18 L31.32 5.94 L32.4 3.78 L29.16 0.54 L29.7 5.94 L29.7 9.18 L29.16 18.36 L30.24 18.36 Z",
|
||||
"M23.22 44.82 L23.22 19.98 L23.22 18.9 L21.6 16.2 L21.6 18.36 L22.14 19.98 L22.14 44.82 Z",
|
||||
"M38.34 43.74 L39.96 19.44 L41.58 17.82 L39.42 15.66 L37.26 17.82 L23.76 18.9 L23.76 19.98 L38.34 18.9 L36.72 43.74 Z",
|
||||
"M22.68 25.38 L23.76 26.46 L29.16 25.92 L34.56 24.84 L33.48 23.76 L29.16 24.84 Z",
|
||||
"M22.68 31.32 L23.76 32.4 L28.62 31.86 L34.02 31.32 L32.94 30.24 L28.62 30.78 Z",
|
||||
"M22.68 37.8 L23.76 38.88 L28.62 38.34 L34.02 37.26 L32.94 36.18 L28.62 37.26 Z",
|
||||
"M14.04 45.36 L15.66 46.98 L31.86 45.36 L48.06 44.82 L45.9 42.66 L42.12 43.74 L31.86 44.28 Z"
|
||||
],
|
||||
"窿": [
|
||||
"M27 5.94 L27.54 3.24 L22.68 0.54 Z",
|
||||
"M9.72 4.86 L8.64 9.72 L6.48 14.04 L7.56 16.2 L8.64 14.04 L9.72 10.26 Z",
|
||||
"M42.66 8.64 L43.2 7.56 L45.36 5.4 L43.2 3.24 L41.04 5.4 L10.26 8.1 L10.26 9.18 L41.58 6.48 L41.04 8.64 L39.96 11.34 Z",
|
||||
"M7.56 18.9 L15.12 15.12 L19.44 12.42 L21.06 11.34 L20.52 8.64 L16.2 12.96 L11.34 16.2 L7.02 18.9 Z",
|
||||
"M30.24 9.18 L29.7 9.18 L36.18 12.96 L41.58 16.74 L40.5 13.5 Z",
|
||||
"M17.82 22.68 L18.9 20.52 L21.06 18.36 L18.9 16.2 L16.74 18.36 L10.26 19.44 L10.26 20.52 L17.82 19.44 L16.74 22.68 L15.12 25.92 L16.74 27.54 L18.36 30.78 L18.36 32.94 L17.82 35.64 L11.88 32.94 L17.28 38.34 L18.9 36.72 L19.44 34.56 L19.44 30.78 L17.28 26.46 L16.2 25.38 Z",
|
||||
"M10.8 43.2 L10.8 21.06 L10.8 19.98 L9.18 17.28 L9.18 19.44 L9.72 21.06 L9.72 43.2 L8.64 46.98 L10.8 50.76 Z",
|
||||
"M21.06 25.38 L25.92 19.98 L27.54 16.2 L29.7 14.04 L27.54 10.8 L27.54 13.5 L24.84 19.98 L20.52 25.38 Z",
|
||||
"M35.64 20.52 L37.26 18.36 L38.88 16.74 L36.72 15.12 L34.56 16.74 L27 17.28 L27 18.36 L35.64 17.82 L34.02 20.52 L29.16 25.38 L25.38 28.08 L19.98 31.32 L19.98 31.86 L26.46 28.62 L32.4 23.76 Z",
|
||||
"M42.12 31.32 L49.68 29.7 L42.12 29.16 L36.18 25.92 L29.16 20.52 L27 20.52 L30.24 22.68 Z",
|
||||
"M24.84 29.16 L25.92 30.24 L31.86 29.16 L37.8 28.08 L36.72 27 L34.02 27.54 L31.86 28.08 Z",
|
||||
"M19.98 42.12 L23.76 36.72 L25.38 32.94 L23.76 31.32 L23.22 35.1 L19.44 42.12 Z",
|
||||
"M23.22 35.1 L24.3 36.18 L32.94 35.1 L42.12 34.02 L39.96 32.4 L36.72 33.48 L32.94 34.02 Z",
|
||||
"M23.76 41.04 L24.84 42.12 L32.94 41.04 L41.04 40.5 L38.88 38.88 L36.72 39.42 L32.94 39.96 Z",
|
||||
"M32.4 47.52 L32.4 32.4 L32.94 30.78 L30.24 28.08 L30.24 29.16 L30.78 32.4 L30.78 47.52 Z",
|
||||
"M17.82 48.06 L19.44 49.68 L32.94 48.06 L46.44 46.98 L44.28 45.36 L41.58 45.9 L32.94 46.98 Z"
|
||||
],
|
||||
"怪": [
|
||||
"M7.02 12.42 L5.4 19.44 L3.78 23.76 L4.86 26.46 L5.4 25.38 L6.48 21.06 Z",
|
||||
"M19.98 17.82 L19.44 14.58 L12.96 11.88 Z",
|
||||
"M12.96 43.74 L12.96 24.84 L12.96 5.4 L14.04 2.7 L10.8 0.54 L11.34 2.16 L11.88 5.4 L11.88 24.3 L11.88 43.74 L10.26 46.98 L12.42 50.76 L12.96 47.52 Z",
|
||||
"M37.26 10.26 L39.42 7.02 L41.58 4.86 L39.42 2.7 L37.26 4.86 L25.92 6.48 L20.52 7.02 L23.22 8.64 L25.92 7.56 L37.8 5.94 L35.64 10.26 L29.7 18.36 L22.68 25.38 L14.58 31.32 L15.66 31.32 L23.22 25.92 L31.32 18.9 Z",
|
||||
"M41.58 28.62 L50.22 27.54 L44.28 26.46 L39.96 24.3 L34.02 19.98 L25.38 11.88 L24.84 10.8 L22.14 10.8 L25.92 13.5 Z",
|
||||
"M19.44 35.1 L21.06 36.72 L31.32 35.1 L42.12 34.02 L39.96 31.86 L36.18 32.94 L31.32 34.02 Z",
|
||||
"M31.32 44.82 L31.32 28.08 L32.4 25.38 L30.24 23.76 L29.16 22.68 L28.62 22.68 L29.16 26.46 L29.7 28.08 L29.7 44.82 Z",
|
||||
"M15.12 45.36 L16.74 46.98 L32.4 45.9 L48.06 44.82 L45.9 42.66 L42.12 43.74 L32.4 44.82 Z"
|
||||
],
|
||||
"活": [
|
||||
"M9.72 5.4 L9.18 5.4 L12.42 9.18 L15.12 12.96 L15.66 9.18 Z",
|
||||
"M5.94 19.44 L4.86 19.44 L7.56 22.14 L10.8 25.92 L11.34 22.68 Z",
|
||||
"M18.36 19.44 L18.36 17.82 L10.26 42.66 L9.72 43.2 L5.94 39.96 L5.4 39.96 L10.8 46.44 Z",
|
||||
"M41.58 5.4 L38.34 2.7 L35.1 5.4 L27 8.64 L18.9 11.34 L23.76 10.8 Z",
|
||||
"M18.36 21.06 L19.98 22.68 L33.48 20.52 L46.98 19.44 L44.82 17.28 L41.04 18.36 L33.48 19.44 Z",
|
||||
"M31.86 16.74 L31.86 11.88 L32.4 10.26 L29.7 7.56 L29.7 8.64 L30.24 11.88 L30.24 16.74 L29.7 31.32 L31.32 31.32 Z",
|
||||
"M23.76 44.28 L23.76 32.4 L23.76 31.32 L22.14 28.62 L22.14 30.78 L22.68 32.4 L22.68 44.28 L21.6 46.98 L23.22 49.68 L23.76 46.44 Z",
|
||||
"M38.34 42.66 L39.96 32.4 L42.12 30.24 L39.96 28.62 L38.34 30.24 L23.76 32.4 L23.76 33.48 L38.34 31.32 L36.72 42.66 Z",
|
||||
"M23.22 44.28 L24.3 45.36 L31.86 44.82 L39.96 44.28 L38.34 42.66 L31.86 43.74 Z"
|
||||
],
|
||||
"甜": [
|
||||
"M23.22 7.02 L21.06 4.86 L18.9 7.02 L4.32 12.96 L5.4 12.96 Z",
|
||||
"M2.16 20.52 L3.78 22.14 L14.04 19.98 L24.84 18.36 L22.68 16.74 L19.44 17.82 L14.04 18.9 Z",
|
||||
"M15.12 29.16 L15.12 9.72 L13.5 9.72 L13.5 29.16 Z",
|
||||
"M9.72 42.12 L9.72 31.86 L9.72 30.24 L7.56 26.46 L7.56 28.62 L8.1 31.86 L8.1 42.12 L7.56 44.28 L9.72 46.98 Z",
|
||||
"M19.44 39.96 L19.98 29.7 L21.6 28.08 L19.44 26.46 L17.82 28.08 L9.72 29.16 L9.72 30.24 L18.36 29.16 L17.82 39.96 Z",
|
||||
"M9.18 41.04 L10.26 42.12 L15.66 41.04 L21.06 40.5 L19.98 39.42 L15.66 39.96 Z",
|
||||
"M21.6 21.06 L23.22 22.68 L36.18 20.52 L49.14 18.9 L46.98 17.28 L44.28 17.82 L36.18 19.44 Z",
|
||||
"M29.7 43.74 L29.7 25.38 L29.7 6.48 L30.24 4.86 L27.54 2.16 L27.54 3.24 L28.08 6.48 L28.08 24.84 L28.08 43.74 L27 46.44 L29.16 50.22 L29.7 46.44 Z",
|
||||
"M42.12 8.1 L42.12 6.48 L43.2 4.32 L39.96 1.08 L40.5 6.48 L40.5 8.1 L38.88 40.5 L37.8 45.9 L39.42 48.6 L40.5 40.5 Z",
|
||||
"M28.62 31.86 L29.7 32.94 L32.94 32.4 L36.72 31.86 L35.64 30.78 L32.94 31.32 Z",
|
||||
"M28.62 44.82 L29.7 45.9 L33.48 45.36 L37.26 44.82 L35.64 43.2 L33.48 44.28 Z"
|
||||
],
|
||||
"赛": [
|
||||
"M24.84 0.54 L24.3 0.54 L25.92 3.78 L27 7.02 L28.62 4.32 Z",
|
||||
"M11.88 5.94 L10.8 10.26 L8.64 14.04 L10.26 16.2 L10.8 14.58 L11.88 10.26 Z",
|
||||
"M43.2 9.18 L44.28 7.56 L45.9 5.94 L43.74 4.32 L42.12 5.94 L12.42 8.64 L12.42 9.72 L42.66 7.02 L41.58 9.18 L39.42 11.88 L40.5 11.88 Z",
|
||||
"M12.96 14.58 L14.04 15.66 L26.46 14.58 L38.88 13.5 L36.72 11.88 L34.56 12.42 L26.46 13.5 Z",
|
||||
"M14.04 19.44 L15.12 20.52 L27 19.44 L39.42 18.36 L37.26 16.74 L35.1 17.28 L27 18.36 Z",
|
||||
"M22.14 23.76 L22.14 14.04 L22.68 11.34 L19.98 8.64 L20.52 10.26 L21.06 14.04 L21.06 23.76 Z",
|
||||
"M31.86 16.2 L31.86 11.88 L32.4 10.26 L29.7 7.56 L29.7 8.64 L30.24 11.88 L30.24 16.2 L29.7 23.76 L30.78 23.76 Z",
|
||||
"M5.94 25.38 L7.56 27 L26.46 24.84 L45.9 23.22 L43.74 21.6 L40.5 22.68 L26.46 23.76 Z",
|
||||
"M17.28 26.46 L12.42 31.32 L7.02 35.1 L2.16 37.8 L3.24 37.8 L9.18 34.56 L14.04 31.32 L17.82 28.08 L20.52 25.38 Z",
|
||||
"M42.12 35.64 L49.14 35.1 L43.2 33.48 L38.34 30.24 L32.4 24.3 L30.78 24.3 Z",
|
||||
"M18.9 38.88 L18.9 32.4 L18.9 31.32 L17.28 28.62 L17.28 30.78 L17.82 32.4 L17.82 38.88 L17.28 40.5 L18.9 42.12 Z",
|
||||
"M34.02 36.72 L34.02 30.78 L35.1 29.7 L33.48 28.08 L32.4 29.16 L18.36 30.78 L18.36 31.86 L32.94 30.24 L32.94 36.72 L31.86 40.5 L33.48 42.12 L34.02 38.88 Z",
|
||||
"M24.84 32.94 L24.84 36.18 L22.68 42.12 L19.98 45.36 L16.2 48.06 L10.8 50.76 L13.5 50.22 L19.44 46.98 L23.22 43.2 L25.38 38.88 L27 34.56 Z",
|
||||
"M28.08 42.66 L34.56 46.98 L38.34 49.68 L37.8 46.44 L31.86 43.74 L28.62 42.66 Z"
|
||||
],
|
||||
"省": [
|
||||
"M25.92 11.88 L25.92 5.4 L27 2.7 L23.76 0.54 L24.3 2.16 L24.84 5.4 L24.84 11.88 L23.76 15.12 L25.92 17.28 Z",
|
||||
"M17.82 6.48 L16.2 10.26 L12.96 14.04 L5.94 21.06 L7.56 20.52 L11.88 16.74 L17.82 10.8 L19.44 9.72 L19.44 9.18 Z",
|
||||
"M41.04 12.96 L43.74 16.2 L44.28 12.42 L38.88 8.64 L34.56 6.48 Z",
|
||||
"M34.02 9.72 L32.94 11.34 L28.62 15.66 L21.06 21.06 L17.28 23.76 L9.72 28.08 L2.7 31.32 L3.78 31.32 L7.02 30.24 L11.34 28.08 L17.28 24.84 L22.68 21.6 L29.16 16.74 L35.1 12.96 Z",
|
||||
"M17.28 45.36 L17.28 28.62 L17.28 27.54 L15.66 24.84 L15.66 27 L16.2 28.62 L16.2 45.36 L15.12 48.06 L16.74 50.76 L17.28 47.52 Z",
|
||||
"M36.72 39.42 L37.26 25.38 L38.88 23.76 L36.72 21.6 L34.56 23.76 L17.28 26.46 L17.28 27.54 L35.64 24.84 L35.1 39.42 L34.56 43.2 L34.02 46.98 L35.64 49.68 L36.18 45.9 Z",
|
||||
"M16.74 32.94 L17.82 34.02 L24.84 32.94 L32.4 32.4 L30.78 30.78 L24.84 31.86 Z",
|
||||
"M16.74 39.42 L17.82 40.5 L24.84 39.42 L31.86 38.88 L30.24 37.26 L24.84 38.34 Z",
|
||||
"M17.28 45.9 L18.36 46.98 L25.92 46.44 L34.02 45.9 L31.86 44.28 L29.7 44.82 L25.92 45.36 Z"
|
||||
],
|
||||
"边": [
|
||||
"M38.34 28.62 L40.5 17.82 L42.12 14.58 L39.96 12.96 L38.34 15.12 L24.3 17.28 L23.76 17.28 L18.9 17.82 L20.52 18.9 L23.76 18.36 L24.3 18.36 L38.88 16.2 L36.72 28.62 L34.56 36.72 L34.02 37.26 L32.4 36.18 L30.24 34.02 L28.62 33.48 L30.24 35.1 L32.4 38.34 L34.02 41.04 L36.72 35.1 Z",
|
||||
"M31.32 10.8 L31.32 6.48 L32.94 3.78 L29.16 0.54 L29.7 6.48 L29.7 10.8 L29.16 15.66 L27.54 24.3 L25.38 29.7 L22.14 36.18 L18.9 39.42 L19.98 39.42 L22.68 36.72 L27.54 28.62 L28.62 25.38 L30.78 15.66 Z",
|
||||
"M9.72 6.48 L9.18 6.48 L11.88 9.72 L13.5 11.34 L15.66 14.58 L16.74 10.8 Z",
|
||||
"M1.62 23.22 L3.78 24.84 L14.04 22.68 L11.88 30.24 L14.04 34.56 L13.5 37.8 L11.88 40.5 L12.96 40.5 L14.58 36.18 L15.12 35.1 L15.12 33.48 L12.96 29.7 L16.2 22.68 L17.28 21.06 L15.66 19.44 L12.96 22.14 Z",
|
||||
"M11.88 40.5 L2.7 40.5 L4.86 42.66 L8.1 41.58 L12.42 41.58 L22.68 43.74 L27.54 44.82 L39.96 49.14 L48.6 46.44 L42.66 46.44 L38.88 46.44 L35.1 45.9 L25.38 43.2 L17.28 41.58 L12.96 40.5 Z"
|
||||
],
|
||||
"兰": [
|
||||
"M15.12 3.78 L14.58 3.78 L16.74 7.02 L20.52 15.12 L22.14 11.88 Z",
|
||||
"M34.02 1.08 L34.02 2.7 L33.48 4.86 L28.62 17.82 L29.7 17.82 L34.56 7.56 L36.18 5.94 Z",
|
||||
"M8.64 19.98 L10.26 21.6 L26.46 19.44 L43.2 18.9 L40.5 16.74 L37.8 17.82 L26.46 18.36 Z",
|
||||
"M12.96 31.32 L14.58 32.94 L25.92 31.32 L37.26 30.24 L35.1 28.62 L32.94 29.16 L25.92 30.24 Z",
|
||||
"M3.78 45.36 L5.4 46.98 L25.38 45.9 L45.9 44.82 L43.74 42.66 L39.96 43.74 L25.38 44.82 Z"
|
||||
],
|
||||
"掌": [
|
||||
"M25.92 11.88 L25.92 4.86 L27 2.16 L23.76 0 L24.3 1.62 L24.84 4.86 L24.84 11.88 Z",
|
||||
"M18.9 10.8 L19.44 7.56 L14.04 3.24 L13.5 3.24 Z",
|
||||
"M35.1 1.62 L34.56 2.7 L34.02 5.4 L30.24 11.34 L30.78 11.34 L34.56 6.48 L36.72 4.86 Z",
|
||||
"M10.26 9.72 L9.72 11.88 L9.18 15.12 L7.02 19.98 L8.1 22.68 L9.18 20.52 L10.26 15.12 Z",
|
||||
"M42.66 10.8 L27 11.88 L9.72 14.04 L10.8 15.12 L27 12.96 L42.525 11.88 L43.2 11.88 L40.5 17.82 L44.28 12.96 L46.98 10.8 L44.82 8.64 L43.2 10.26 Z",
|
||||
"M18.9 23.76 L18.9 19.98 L18.9 18.9 L17.28 16.2 L17.28 18.36 L17.82 19.98 L17.82 23.76 L17.28 25.38 L18.9 27 Z",
|
||||
"M32.4 21.6 L32.94 17.82 L34.56 16.2 L32.4 14.58 L30.78 16.2 L18.9 17.82 L18.9 18.9 L31.86 17.28 L31.32 21.6 Z",
|
||||
"M18.36 23.76 L19.44 24.84 L26.46 23.76 L33.48 22.68 L32.4 21.6 L26.46 22.68 Z",
|
||||
"M9.18 31.32 L16.2 30.78 L37.8 26.46 L35.64 24.84 L31.32 26.46 Z",
|
||||
"M12.96 34.56 L14.04 35.64 L26.46 33.48 L38.88 32.4 L36.72 30.78 L34.56 31.32 L26.46 32.4 Z",
|
||||
"M4.86 39.42 L6.48 41.04 L25.92 38.88 L45.36 37.26 L43.2 35.1 L39.42 36.18 L25.92 37.8 Z",
|
||||
"M26.46 47.52 L26.46 28.62 L24.84 28.62 L24.84 47.52 L24.3 48.06 L17.28 44.82 L25.38 51.3 Z"
|
||||
],
|
||||
"从": [
|
||||
"M18.9 9.72 L18.9 8.1 L20.52 5.4 L16.74 2.16 L17.28 8.1 L17.28 9.72 L16.2 17.82 L15.12 24.3 L12.96 30.78 L10.8 36.18 L5.4 44.28 L2.16 47.52 L3.78 46.98 L6.48 44.28 L11.88 36.18 L14.04 30.78 L16.74 22.68 Z",
|
||||
"M16.2 22.68 L16.74 24.3 L20.52 28.08 L23.76 31.86 L24.3 28.62 Z",
|
||||
"M34.02 13.5 L34.02 6.48 L35.64 3.78 L31.86 0.54 L32.4 6.48 L32.4 13.5 L31.32 18.9 L30.24 25.38 L27.54 32.94 L24.84 38.88 L21.06 44.28 L16.2 49.14 L17.82 48.6 L23.22 43.2 L27.54 36.72 L30.24 29.16 L32.4 22.14 Z",
|
||||
"M42.12 45.36 L49.68 44.82 L45.9 43.74 L43.2 42.66 L38.88 35.1 L32.4 22.14 L31.86 23.22 Z"
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -56,9 +56,11 @@
|
||||
li {
|
||||
background: url(images/bg12.jpg);
|
||||
}
|
||||
|
||||
.afterpage {
|
||||
page-break-after: always;
|
||||
}
|
||||
|
||||
.noprint {
|
||||
display: none;
|
||||
}
|
||||
@@ -80,76 +82,148 @@
|
||||
</div>
|
||||
<script id="myword" charset="UTF-8" src="mwords.json"></script>
|
||||
<script>
|
||||
// 测试输出:打印汉字"舨"的笔画数据
|
||||
console.log(words["舨"]);
|
||||
|
||||
// ========== 模板定义 ==========
|
||||
// SVG田字格模板:每个格子包含一个54x54的SVG画布
|
||||
let template_li = '<li class="svg"><svg width="54" height="54" version="1.1" xmlns="http://www.w3.org/2000/svg">##paths##</svg></li>';
|
||||
|
||||
// 空白格子模板:用于填充行末
|
||||
let empty_li1 = '<li class="f"> </li>';
|
||||
let empty_li2 = '';
|
||||
for (let i = 0; i < 11; i++) { empty_li2 += "<li> </li>" }
|
||||
let first_paths = '<path d="##path_d##" style="fill:rgb(0,0,0);stroke:rgb(0,0,0);" stroke-width="1.5"></path>';
|
||||
let second_paths = '<path d="##path_d##" style="fill:rgb(184,184,184);stroke:rgb(184,184,184);" stroke-width="1.6"></path>';
|
||||
second_paths = '<path d="##path_d##" style="fill:rgb(184,184,184);stroke:#999;" stroke-width="1.6"></path>';
|
||||
let d_key = '##path_d##';
|
||||
let p_key = '##paths##';
|
||||
u_printf.innerHTML = "";
|
||||
let preview = (c) => {
|
||||
// 生成11个空白格子,用于填充到每行末尾(每行总共12个格子)
|
||||
for (let i = 0; i < 11; i++) {
|
||||
empty_li2 += "<li> </li>"
|
||||
}
|
||||
|
||||
// 笔画路径模板定义
|
||||
// 完整汉字路径:黑色填充和描边,用于预览格
|
||||
let first_paths = '<path d="##path_d##" style="fill:rgb(0,0,0);stroke:rgb(0,0,0);" stroke-width="1.5"></path>';
|
||||
// 练习汉字路径:灰色填充和描边,用于练习格
|
||||
let second_paths = '<path d="##path_d##" style="fill:rgb(184,184,184);stroke:#999;" stroke-width="1.6"></path>';
|
||||
|
||||
// 占位符定义
|
||||
let d_key = '##path_d##'; // 笔画路径占位符
|
||||
let p_key = '##paths##'; // SVG路径组占位符
|
||||
|
||||
// 清空输出区域
|
||||
u_printf.innerHTML = "";
|
||||
|
||||
// ========== 核心函数定义 ==========
|
||||
|
||||
/**
|
||||
* 生成汉字的预览格(完整汉字显示)
|
||||
* @param {string} c - 汉字字符
|
||||
* @returns {string} - 预览格的HTML
|
||||
*/
|
||||
let preview = (c) => {
|
||||
let pre_li = "";
|
||||
|
||||
// 获取汉字的笔画数据
|
||||
ds = words[c];
|
||||
if (!ds) {
|
||||
return;
|
||||
return; // 如果没有找到笔画数据,直接返回
|
||||
}
|
||||
len = ds.length;
|
||||
f_path = "";
|
||||
|
||||
len = ds.length; // 笔画总数
|
||||
f_path = ""; // 完整路径字符串
|
||||
|
||||
// 遍历所有笔画,生成完整的SVG路径
|
||||
for (let j = 0; j < len; j++) {
|
||||
// 将每个笔画路径添加到完整路径中,使用黑色样式
|
||||
f_path += first_paths.replace(d_key, ds[j]);
|
||||
}
|
||||
pre_li = template_li.replace(p_key, f_path);;
|
||||
|
||||
// 将完整路径插入到SVG模板中
|
||||
pre_li = template_li.replace(p_key, f_path);
|
||||
|
||||
return pre_li;
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成汉字的练习格(逐笔画显示)
|
||||
* @param {string} c - 汉字字符
|
||||
* @returns {string} - 练习格的HTML
|
||||
*/
|
||||
let practice = (c) => {
|
||||
// 获取汉字的笔画数据
|
||||
ds = words[c];
|
||||
if (!ds) {
|
||||
return;
|
||||
return; // 如果没有找到笔画数据,直接返回
|
||||
}
|
||||
len = ds.length;
|
||||
let bihua_li = "";
|
||||
let s_path = "";
|
||||
|
||||
len = ds.length; // 笔画总数
|
||||
let bihua_li = ""; // 练习格HTML字符串
|
||||
let s_path = ""; // 当前路径字符串
|
||||
|
||||
// 生成逐笔画练习格:每个格子显示到当前笔画为止的汉字
|
||||
for (let bihua = 0; bihua < len; bihua++) {
|
||||
s_path = "";
|
||||
s_path = ""; // 重置路径
|
||||
|
||||
// 累加从第1笔到当前笔画的所有路径
|
||||
for (let i = 0; i <= bihua; i++) {
|
||||
// 将每个笔画路径添加到当前路径中,使用灰色样式
|
||||
s_path += second_paths.replace(d_key, ds[i]);
|
||||
}
|
||||
|
||||
// 将当前路径插入到SVG模板中,生成一个练习格
|
||||
bihua_li += template_li.replace(p_key, s_path);
|
||||
}
|
||||
|
||||
// 填充剩余空白格:如果汉字笔画少于23个,用最后一个练习格填充
|
||||
for (let i = 0; i < 23 - len; i++) {
|
||||
bihua_li += template_li.replace(p_key, s_path);
|
||||
}
|
||||
|
||||
// 添加行末填充:1个特殊空白格 + 11个普通空白格
|
||||
bihua_li += empty_li1;
|
||||
bihua_li += empty_li2;
|
||||
|
||||
return bihua_li;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成单个汉字的完整练字格(预览格 + 练习格)
|
||||
* @param {string} c - 汉字字符
|
||||
* @returns {string} - 完整练字格的HTML
|
||||
*/
|
||||
let print_char = (c) => {
|
||||
// 组合预览格和练习格
|
||||
return preview(c) + practice(c);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成整个句子的练字格
|
||||
* @param {string} s - 汉字字符串
|
||||
* @returns {string} - 整个句子的练字格HTML
|
||||
*/
|
||||
let print_sentence = (s) => {
|
||||
sen_li = "";
|
||||
sen_li = ""; // 句子HTML字符串
|
||||
|
||||
// 遍历每个汉字,生成对应的练字格
|
||||
for (let i in s) {
|
||||
sen_li += print_char(s[i]);
|
||||
}
|
||||
|
||||
return sen_li;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 生成按钮点击事件处理函数
|
||||
* 根据用户输入的文本生成练字贴
|
||||
*/
|
||||
onclick_gen = () => {
|
||||
// 清空输出区域
|
||||
u_printf.innerHTML = "";
|
||||
|
||||
lis = print_sentence(to_print.value.replace(/ /g,"").trim());
|
||||
// 获取用户输入,去除空格并生成练字贴
|
||||
lis = print_sentence(to_print.value.replace(/ /g, "").trim());
|
||||
|
||||
// 将生成的HTML插入到页面中
|
||||
u_printf.innerHTML = lis;
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
# SVG 路径分析 - 汉字笔画解析
|
||||
|
||||
## 📝 概述
|
||||
|
||||
本文档详细解析汉字练字贴中使用的 SVG 路径数据,帮助理解汉字笔画的数字化表示方法。
|
||||
|
||||
## 🎯 示例路径分析
|
||||
|
||||
### 路径片段
|
||||
|
||||
```
|
||||
"M15.66 8.1 L15.66 7.56 L17.28 4.86 L13.5 1.62 L14.04 7.56 L14.04 8.1 L9.18 32.4 L16.2 36.18 L23.76 42.12 L23.76 38.88 L10.8 31.86 Z"
|
||||
```
|
||||
|
||||
## 🔧 SVG 路径命令
|
||||
|
||||
### 基本命令
|
||||
|
||||
- **M** = MoveTo(移动到)- 开始一个新的子路径,不绘制线条
|
||||
- **L** = LineTo(画线到)- 从当前位置画直线到指定点
|
||||
- **Z** = ClosePath(闭合路径)- 闭合当前子路径,连接起点和终点
|
||||
|
||||
### 坐标系统
|
||||
|
||||
- 使用 54x54 的视窗坐标系
|
||||
- 坐标原点 (0,0) 在左上角
|
||||
- X 轴向右递增,Y 轴向下递增
|
||||
- 坐标范围:0-54
|
||||
|
||||
## 📊 逐点分析
|
||||
|
||||
| 命令 | 坐标 | 描述 | 移动方向 |
|
||||
| ---- | -------------- | -------------- | ---------- |
|
||||
| M | (15.66, 8.1) | 起始点 | - |
|
||||
| L | (15.66, 7.56) | 向上移动 | ↑ |
|
||||
| L | (17.28, 4.86) | 向右上移动 | ↗ |
|
||||
| L | (13.5, 1.62) | 向左上移动 | ↖ |
|
||||
| L | (14.04, 7.56) | 向右下移动 | ↘ |
|
||||
| L | (14.04, 8.1) | 向下移动 | ↓ |
|
||||
| L | (9.18, 32.4) | 向左下大幅移动 | ↙ |
|
||||
| L | (16.2, 36.18) | 向右下移动 | ↘ |
|
||||
| L | (23.76, 42.12) | 向右下移动 | ↘ |
|
||||
| L | (23.76, 38.88) | 向上移动 | ↑ |
|
||||
| L | (10.8, 31.86) | 向左上移动 | ↖ |
|
||||
| Z | - | 闭合路径 | 回到起始点 |
|
||||
|
||||
## 🖌️ 笔画特征分析
|
||||
|
||||
### 形状特征
|
||||
|
||||
1. **起始区域**:在坐标 (15.66, 8.1) 附近,位于画布上方
|
||||
2. **转折点**:包含多个转折,形成复杂的曲线
|
||||
3. **主要移动**:从上方区域移动到下方区域
|
||||
4. **闭合性**:使用 Z 命令闭合路径
|
||||
|
||||
### 书法特征
|
||||
|
||||
- **复杂笔画**:包含多个方向的线条变化
|
||||
- **曲线特征**:有明显的弯曲和转折
|
||||
- **可能类型**:撇捺组合、弯钩笔画或复合笔画
|
||||
|
||||
## 🎨 在汉字中的应用
|
||||
|
||||
### 汉字"好"的笔画结构
|
||||
|
||||
这个路径很可能是汉字"好"字中的某个重要笔画:
|
||||
|
||||
#### 可能的组成部分
|
||||
|
||||
1. **女字旁**:左边的"女"字的一部分
|
||||
2. **子字旁**:右边的"子"字的一部分
|
||||
3. **复合笔画**:多个基本笔画的组合
|
||||
|
||||
### 笔画顺序
|
||||
|
||||
在汉字书写中,这个笔画可能:
|
||||
|
||||
- 不是第一笔(起始位置不在左上角)
|
||||
- 包含多个书写方向的变化
|
||||
- 需要一定的书写技巧
|
||||
|
||||
## 🔍 技术实现
|
||||
|
||||
### 在代码中的使用
|
||||
|
||||
```typescript
|
||||
// SVG 路径数据
|
||||
const strokes = [
|
||||
'M15.66 8.1 L15.66 7.56 L17.28 4.86 L13.5 1.62 L14.04 7.56 L14.04 8.1 L9.18 32.4 L16.2 36.18 L23.76 42.12 L23.76 38.88 L10.8 31.86 Z',
|
||||
// ... 更多笔画
|
||||
];
|
||||
|
||||
// 绘制函数
|
||||
function drawSvgPathCommands(ctx, pathD, offsetX, offsetY, scale) {
|
||||
// 解析并绘制 SVG 路径
|
||||
}
|
||||
```
|
||||
|
||||
### 坐标变换
|
||||
|
||||
- **缩放**:根据田字格大小进行缩放
|
||||
- **偏移**:根据田字格位置进行偏移
|
||||
- **适配**:适配不同的显示尺寸
|
||||
|
||||
## 📚 相关文档
|
||||
|
||||
- [SVG 路径规范](https://www.w3.org/TR/SVG/paths.html)
|
||||
- [汉字笔画分类](https://zh.wikipedia.org/wiki/汉字笔画)
|
||||
- [田字格练字原理](./tianzige-principles.md)
|
||||
|
||||
## 🎯 总结
|
||||
|
||||
这个 SVG 路径片段展示了汉字数字化的精确表示方法:
|
||||
|
||||
1. **精确性**:每个转折点都有精确的坐标
|
||||
2. **完整性**:完整描述了笔画的形状和走向
|
||||
3. **可缩放性**:通过坐标系统支持不同尺寸的显示
|
||||
4. **书法性**:保持了汉字书法的艺术特征
|
||||
|
||||
通过这种数字化表示,我们可以在计算机上精确重现汉字的书写过程,为练字教学提供技术支持。
|
||||
|
||||
---
|
||||
|
||||
_文档创建时间:2024年_
|
||||
_最后更新:2024年_
|
||||
@@ -1,5 +1,9 @@
|
||||
{
|
||||
"navigationBarTitleText": "练字",
|
||||
"navigationBarBackgroundColor": "#FFD719",
|
||||
"homeButton": true,
|
||||
"backgroundColor": "#F6F6F6",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"toy-button": "/ui/button/button",
|
||||
"van-icon": "@vant/weapp/icon/index",
|
||||
@@ -6,6 +6,10 @@ page {
|
||||
background-color: #f6f6f6;
|
||||
padding: 0 24rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
.empty {
|
||||
height: 120rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
@@ -144,4 +148,38 @@ page {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.canvas-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 400rpx;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12rpx;
|
||||
border: 2rpx dashed #dee2e6;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.canvas-content {
|
||||
max-width: 100%;
|
||||
border-radius: 8rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bottom-btn-box {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 120rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: #fff;
|
||||
box-shadow: 0px -3.9rpx 5.2rpx rgba(0, 0, 0, 0.15);
|
||||
padding: 0 24rpx;
|
||||
box-sizing: border-box;
|
||||
border-radius: 16rpx 16rpx 0 0;
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
import WordDrawService from '../../service/wordDrawService';
|
||||
import { checkAndSaveImage } from '../../utils/saveImage';
|
||||
import { PAPER_SIZE } from '../../constants/colors';
|
||||
|
||||
Page({
|
||||
canvas: null as Canvas | null,
|
||||
ctx: null as RenderingContext | null,
|
||||
wordDrawService: null as WordDrawService | null,
|
||||
svgWords: {} as Record<string, string[]>,
|
||||
|
||||
data: {
|
||||
// 选中的汉字列表
|
||||
words: ['赢', '张', '政'] as string[],
|
||||
showSelectWordPopup: false,
|
||||
showColorPopup: false,
|
||||
currentKey: 0,
|
||||
currentColor: '',
|
||||
|
||||
// 田字格配置
|
||||
// rows: 10,
|
||||
// cols: 12,
|
||||
// rowsArray: [] as number[],
|
||||
// colsArray: [] as number[],
|
||||
boxWidth: 0,
|
||||
boxHeight: 0,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
// this.initGrid();
|
||||
this.loadSvgWords();
|
||||
},
|
||||
|
||||
onReady() {
|
||||
this.setCanvasBoxSize();
|
||||
},
|
||||
|
||||
loadSvgWords() {
|
||||
const fs = wx.getFileSystemManager();
|
||||
fs.readFile({
|
||||
filePath: 'constants/svgWords.json', // JSON文件路径,相对于项目根目录
|
||||
encoding: 'utf8', // 重要:指定编码为utf8以读取文本内容
|
||||
success: (res) => {
|
||||
try {
|
||||
wx.showToast({ title: '加载中...', icon: 'loading' });
|
||||
const parsedData = JSON.parse(res.data as string);
|
||||
this.svgWords = parsedData;
|
||||
console.log('this.svgWords.length----:', Object.keys(this.svgWords).length)
|
||||
wx.hideToast();
|
||||
console.log('SVG words loaded successfully');
|
||||
const { words } = this.data as any;
|
||||
if (words && words.length > 0) {
|
||||
this.drawPracticeSheet().catch(console.error);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('解析JSON失败', e);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('读取文件失败', err);
|
||||
wx.hideToast();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 输入组件回调
|
||||
onConfirmInput(e: any) {
|
||||
const value: string = (e && e.detail && e.detail.value) || '';
|
||||
const text = (value || '').trim();
|
||||
if (!text) return;
|
||||
|
||||
if (!this.isChineseText(text)) {
|
||||
wx.showToast({ title: '请输入汉字', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
const chars = this.splitToSingleChars(text);
|
||||
const { words } = this.data as any;
|
||||
const exists = new Set(words);
|
||||
const newWords = [...words];
|
||||
chars.forEach((ch) => {
|
||||
if (!exists.has(ch)) {
|
||||
newWords.push(ch);
|
||||
}
|
||||
});
|
||||
this.setData({ words: newWords }, () => this.drawPracticeSheet().catch(console.error));
|
||||
},
|
||||
|
||||
// 选择面板
|
||||
openSelectWordPopup() {
|
||||
this.setData({ showSelectWordPopup: true });
|
||||
},
|
||||
closeSelectWordPopup() {
|
||||
this.setData({ showSelectWordPopup: false });
|
||||
},
|
||||
onChangeWord(e: any) {
|
||||
const selectedWords = e.detail.selectedWords as string[];
|
||||
const newWords = [...this.data.words, ...selectedWords];
|
||||
this.setData({ words: newWords, showSelectWordPopup: false }, () => this.drawPracticeSheet().catch(console.error));
|
||||
},
|
||||
|
||||
// 删除与颜色
|
||||
deleteWordCard(e: any) {
|
||||
const { word } = e.detail;
|
||||
const { words } = this.data as any;
|
||||
const next = words.filter((w: string) => w !== word);
|
||||
this.setData({ words: next }, () => this.drawPracticeSheet().catch(console.error));
|
||||
},
|
||||
|
||||
// 校验 & 分割
|
||||
isChineseText(text: string): boolean {
|
||||
const chineseRegex = /^[\u4e00-\u9fff]+$/;
|
||||
return chineseRegex.test(text);
|
||||
},
|
||||
splitToSingleChars(text: string): string[] {
|
||||
const chineseRegex = /[\u4e00-\u9fff]/g;
|
||||
const matches = text.match(chineseRegex);
|
||||
return matches || [];
|
||||
},
|
||||
|
||||
// 田字格
|
||||
/* initGrid() {
|
||||
const { rows, cols } = this.data as any;
|
||||
const rowsArray = Array.from({ length: rows }, (_, i) => i);
|
||||
const colsArray = Array.from({ length: cols }, (_, i) => i);
|
||||
console.log('rowsArray----:', rowsArray)
|
||||
console.log('colsArray----:', colsArray)
|
||||
this.setData({ rowsArray, colsArray });
|
||||
}, */
|
||||
|
||||
setCanvasBoxSize() {
|
||||
const query = wx.createSelectorQuery();
|
||||
query
|
||||
.select('#canvasWrapper')
|
||||
.boundingClientRect((rect) => {
|
||||
if (rect) {
|
||||
const { width, height } = PAPER_SIZE['A4'];
|
||||
const boxWidth = rect.width;
|
||||
const boxHeight = boxWidth / (width / height);
|
||||
this.setData({ boxWidth, boxHeight });
|
||||
}
|
||||
})
|
||||
.exec();
|
||||
},
|
||||
initCanvas() {
|
||||
// 然后初始化canvas
|
||||
wx.createSelectorQuery()
|
||||
.select('#canvasContent')
|
||||
.fields({
|
||||
node: true,
|
||||
size: true,
|
||||
})
|
||||
.exec((res) => {
|
||||
if (res[0] && res[0].node) {
|
||||
const canvas = res[0].node;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
this.canvas = canvas;
|
||||
this.ctx = ctx;
|
||||
this.wordDrawService = new WordDrawService(canvas, ctx, {
|
||||
appName: '涂鸦丫小程序',
|
||||
appHint: '练字|识字|打印',
|
||||
title: '田字格 练 字 贴',
|
||||
subTitle: '按笔画临摹练习'
|
||||
});
|
||||
this.drawPracticeSheet().catch(console.error);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 绘制练字贴
|
||||
async drawPracticeSheet() {
|
||||
if (!this.canvas || !this.wordDrawService) {
|
||||
this.initCanvas();
|
||||
}
|
||||
const { words } = this.data as any;
|
||||
if (!words || words.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建汉字笔画数据
|
||||
const wordsMap: Record<string, string[]> = {};
|
||||
console.log('this.svgWords----:', this.svgWords)
|
||||
words.forEach((word: string) => {
|
||||
if (this.svgWords[word]) {
|
||||
wordsMap[word] = this.svgWords[word];
|
||||
}
|
||||
});
|
||||
if (Object.keys(wordsMap).length === 0) {
|
||||
wx.showToast({ title: '暂不支持这些汉字', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
await this.wordDrawService?.draw(wordsMap);
|
||||
},
|
||||
|
||||
// 下载打印练字贴
|
||||
exportToPrint() {
|
||||
const { words } = this.data as any;
|
||||
if (!this.canvas || words.length <= 0) {
|
||||
wx.showToast({ title: '请先生成练字贴', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
checkAndSaveImage(this.canvas);
|
||||
},
|
||||
|
||||
// 分享功能
|
||||
onShareAppMessage() {
|
||||
return {
|
||||
title: '涂鸦丫-练字|识字|打印',
|
||||
path: '/pages/copyBook/index',
|
||||
};
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
return {
|
||||
title: '涂鸦丫-练字|识字|打印',
|
||||
query: '/pages/copyBook/index',
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -12,18 +12,18 @@
|
||||
</view>
|
||||
|
||||
<view class="word-card-box">
|
||||
<view class="word-cards-grid" wx:if="{{cardList.length > 0}}">
|
||||
<view class="word-cards-grid" wx:if="{{words.length > 0}}">
|
||||
<word-card
|
||||
disabledColor="{{true}}"
|
||||
wx:key="index"
|
||||
wx:for="{{cardList}}"
|
||||
wx:for-item="item"
|
||||
wx:for="{{words}}"
|
||||
wx:for-item="word"
|
||||
wx:for-index="index"
|
||||
word="{{item.word}}"
|
||||
color="{{item.color}}"
|
||||
word="{{word}}"
|
||||
bind:onDelete="deleteWordCard"
|
||||
bind:onColorTap="onColorTap" />
|
||||
</view>
|
||||
<view class="word-list-empty" wx:if="{{!cardList.length}}">
|
||||
<view class="word-list-empty" wx:if="{{!words.length}}">
|
||||
<view class="empty-icon">
|
||||
<text class="toy-icon toy-icon-translate"></text>
|
||||
</view>
|
||||
@@ -37,31 +37,46 @@
|
||||
|
||||
<view id="previewWrapper" class="wrapper">
|
||||
<text class="wrapper-title">预览练字田字格</text>
|
||||
<view class="tianzige" wx:if="{{rowHeaderWords.length > 0}}">
|
||||
<view
|
||||
class="tzg-row"
|
||||
wx:for="{{rowsArray}}"
|
||||
wx:for-index="rowIndex"
|
||||
wx:key="rowIndex">
|
||||
<view
|
||||
class="tzg-cell"
|
||||
wx:for="{{colsArray}}"
|
||||
wx:for-index="colIndex"
|
||||
wx:key="{{rowIndex}}-{{colIndex}}">
|
||||
<text wx:if="{{colIndex === 0}}" class="tzg-word"
|
||||
>{{rowHeaderWords[rowIndex]}}</text
|
||||
>
|
||||
<view class="mid-line h"></view>
|
||||
<view class="mid-line v"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view id="canvasWrapper" class="canvas-wrapper">
|
||||
<canvas
|
||||
wx:if="{{words}}"
|
||||
type="2d"
|
||||
id="canvasContent"
|
||||
class="canvas-content"
|
||||
style="width:{{boxWidth}}px;height:{{boxHeight}}px" />
|
||||
<view wx:else class="empty-tip">请选择或输入汉字后生成田字格</view>
|
||||
</view>
|
||||
<view wx:else class="empty-tip">请选择或输入汉字后生成田字格</view>
|
||||
</view>
|
||||
|
||||
<view class="bottom-btn-box">
|
||||
<toy-button
|
||||
openType="share"
|
||||
type="green"
|
||||
flat="{{true}}"
|
||||
bind:click="onShareAppMessage"
|
||||
width="220rpx"
|
||||
height="80rpx"
|
||||
icon="wechat"
|
||||
icon-class-prefix="toy-icon">
|
||||
分享
|
||||
</toy-button>
|
||||
<toy-button
|
||||
type="primary"
|
||||
flat="{{true}}"
|
||||
bind:click="exportToPrint"
|
||||
width="420rpx"
|
||||
height="80rpx"
|
||||
disabled="{{words.length <= 0}}"
|
||||
icon="download"
|
||||
icon-class-prefix="toy-icon">
|
||||
下载打印
|
||||
</toy-button>
|
||||
</view>
|
||||
<word-picker
|
||||
show="{{showSelectWordPopup}}"
|
||||
cardList="{{cardList}}"
|
||||
selectedWords="{{words}}"
|
||||
bind:onClose="closeSelectWordPopup"
|
||||
bind:onChange="onChangeWord" />
|
||||
|
||||
<view class="empty"></view>
|
||||
</view>
|
||||
@@ -1,91 +0,0 @@
|
||||
Page({
|
||||
data: {
|
||||
// 选中字卡列表,复用 index 页的结构 [{ word, color }]
|
||||
cardList: [] as { word: string; color: string }[],
|
||||
showSelectWordPopup: false,
|
||||
|
||||
// 田字格配置
|
||||
rows: 10,
|
||||
cols: 12,
|
||||
rowsArray: [] as number[],
|
||||
colsArray: [] as number[],
|
||||
rowHeaderWords: [] as string[],
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.initGrid();
|
||||
},
|
||||
|
||||
// 输入组件回调
|
||||
onConfirmInput(e: any) {
|
||||
const value: string = (e && e.detail && e.detail.value) || '';
|
||||
const text = (value || '').trim();
|
||||
if (!text) return;
|
||||
|
||||
if (!this.isChineseText(text)) {
|
||||
wx.showToast({ title: '请输入汉字', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
const chars = this.splitToSingleChars(text);
|
||||
const { cardList } = this.data as any;
|
||||
const exists = new Set(cardList.map((c: any) => c.word));
|
||||
const newList = [...cardList];
|
||||
chars.forEach((ch) => {
|
||||
if (!exists.has(ch)) {
|
||||
newList.push({ word: ch, color: '#141414' });
|
||||
}
|
||||
});
|
||||
this.setData({ cardList: newList }, () => this.updateRowHeaderWords());
|
||||
},
|
||||
|
||||
// 选择面板
|
||||
openSelectWordPopup() {
|
||||
this.setData({ showSelectWordPopup: true });
|
||||
},
|
||||
closeSelectWordPopup() {
|
||||
this.setData({ showSelectWordPopup: false });
|
||||
},
|
||||
onChangeWord(e: any) {
|
||||
const { cardList } = e.detail || {};
|
||||
this.setData({ cardList }, () => this.updateRowHeaderWords());
|
||||
},
|
||||
|
||||
// 删除与颜色
|
||||
deleteWordCard(e: any) {
|
||||
const index = e.detail && e.detail.index;
|
||||
const { cardList } = this.data as any;
|
||||
const next = cardList.filter((_: any, i: number) => i !== index);
|
||||
this.setData({ cardList: next }, () => this.updateRowHeaderWords());
|
||||
},
|
||||
onColorTap() { },
|
||||
|
||||
// 校验 & 分割
|
||||
isChineseText(text: string): boolean {
|
||||
const chineseRegex = /^[\u4e00-\u9fff]+$/;
|
||||
return chineseRegex.test(text);
|
||||
},
|
||||
splitToSingleChars(text: string): string[] {
|
||||
const chineseRegex = /[\u4e00-\u9fff]/g;
|
||||
const matches = text.match(chineseRegex);
|
||||
return matches || [];
|
||||
},
|
||||
|
||||
// 田字格
|
||||
initGrid() {
|
||||
const { rows, cols } = this.data as any;
|
||||
const rowsArray = Array.from({ length: rows }, (_, i) => i);
|
||||
const colsArray = Array.from({ length: cols }, (_, i) => i);
|
||||
this.setData({ rowsArray, colsArray });
|
||||
},
|
||||
updateRowHeaderWords() {
|
||||
const { rows, cardList } = this.data as any;
|
||||
const rowHeaderWords: string[] = [];
|
||||
for (let i = 0; i < rows; i++) {
|
||||
rowHeaderWords.push((cardList[i] && cardList[i].word) || '');
|
||||
}
|
||||
this.setData({ rowHeaderWords });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ Page({
|
||||
|
||||
data: {
|
||||
cardList: [] as CardList,
|
||||
selectedWords: [] as string[],
|
||||
showColorPopup: false,
|
||||
currentKey: 0,
|
||||
currentColor: '',
|
||||
@@ -43,6 +44,7 @@ Page({
|
||||
{ color: '#FFFF00', word: '迎' },
|
||||
{ color: '#8A2BE2', word: '您' },
|
||||
],
|
||||
selectedWords: ['涂', '鸦', '丫', '欢', '迎', '您'],
|
||||
showIntroductionPopup: true,
|
||||
env: getApp().globalData.env || 'release',
|
||||
});
|
||||
@@ -69,8 +71,8 @@ Page({
|
||||
const boxWidth = rect.width;
|
||||
const boxHeight = boxWidth / (width / height);
|
||||
|
||||
this.boxHeight = boxHeight;
|
||||
this.boxWidth = boxWidth;
|
||||
console.log('boxWidth----:', boxWidth)
|
||||
console.log('boxHeight----:', boxHeight)
|
||||
|
||||
// 然后初始化canvas
|
||||
wx.createSelectorQuery()
|
||||
@@ -108,7 +110,10 @@ Page({
|
||||
callback: () => void = () => { },
|
||||
) {
|
||||
if (updatedCardList) {
|
||||
this.setData({ cardList: updatedCardList }, () => {
|
||||
this.setData({
|
||||
cardList: updatedCardList,
|
||||
selectedWords: updatedCardList.map((item) => item.word)
|
||||
}, () => {
|
||||
// 如果cardList有内容且canvas未初始化,先初始化canvas
|
||||
if (!this.canvas) {
|
||||
this.initCanvas();
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
bind:onChange="onChangeColor" />
|
||||
<word-picker
|
||||
show="{{showSelectWordPopup}}"
|
||||
cardList="{{cardList}}"
|
||||
selectedWords="{{cardList}}"
|
||||
bind:onClose="closeSelectWordPopup"
|
||||
bind:onChange="onChangeWord" />
|
||||
</view>
|
||||
|
||||
@@ -71,7 +71,7 @@ class TextDrawService {
|
||||
this.paperSize = 'A4';
|
||||
this.options = {
|
||||
appName: '涂鸦丫小程序',
|
||||
appHint: '涂色|识字|画画|打印',
|
||||
appHint: '识字|识图|练字|打印',
|
||||
title: '找一找 涂 色',
|
||||
subTitle: '给文字涂上相同的颜色',
|
||||
...options,
|
||||
|
||||
@@ -1,82 +1,159 @@
|
||||
import { PAPER_SIZE } from '../constants/colors';
|
||||
import { getMiniCodeImage, getImage } from '../utils/index';
|
||||
|
||||
/**
|
||||
* 仅支持 M/L/Z 的简单 SVG 路径解析与绘制
|
||||
*/
|
||||
function drawSvgPathCommands(
|
||||
ctx: RenderingContext,
|
||||
pathD: string,
|
||||
offsetX: number,
|
||||
offsetY: number,
|
||||
scale: number,
|
||||
) {
|
||||
const tokens = pathD
|
||||
.replace(/,/g, ' ')
|
||||
.trim()
|
||||
.split(/\s+/);
|
||||
|
||||
let i = 0;
|
||||
let currentX = 0;
|
||||
let currentY = 0;
|
||||
while (i < tokens.length) {
|
||||
const cmd = tokens[i++];
|
||||
switch (cmd) {
|
||||
case 'M': {
|
||||
const x = parseFloat(tokens[i++]);
|
||||
const y = parseFloat(tokens[i++]);
|
||||
currentX = offsetX + x * scale;
|
||||
currentY = offsetY + y * scale;
|
||||
ctx.moveTo(currentX, currentY);
|
||||
break;
|
||||
}
|
||||
case 'L': {
|
||||
const x = parseFloat(tokens[i++]);
|
||||
const y = parseFloat(tokens[i++]);
|
||||
currentX = offsetX + x * scale;
|
||||
currentY = offsetY + y * scale;
|
||||
ctx.lineTo(currentX, currentY);
|
||||
break;
|
||||
}
|
||||
case 'Z': {
|
||||
ctx.closePath();
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// 遇到数字(可能因为路径省略了连续的 L),回退一步并按 L 处理
|
||||
if (!isNaN(parseFloat(cmd))) {
|
||||
i--;
|
||||
const x = parseFloat(tokens[i++]);
|
||||
const y = parseFloat(tokens[i++]);
|
||||
currentX = offsetX + x * scale;
|
||||
currentY = offsetY + y * scale;
|
||||
ctx.lineTo(currentX, currentY);
|
||||
break;
|
||||
}
|
||||
// 其他命令不支持,直接跳过
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
interface DrawSvgPathCommandsParams {
|
||||
ctx: RenderingContext;
|
||||
pathD: string;
|
||||
offsetX: number;
|
||||
offsetY: number;
|
||||
scale: number;
|
||||
}
|
||||
|
||||
function drawStrokes(
|
||||
ctx: RenderingContext,
|
||||
strokes: string[],
|
||||
offsetX: number,
|
||||
offsetY: number,
|
||||
size: number,
|
||||
uptoInclusive: number,
|
||||
fillStyle: string,
|
||||
strokeStyle: string,
|
||||
lineWidth: number,
|
||||
) {
|
||||
function drawSvgPathCommands({
|
||||
ctx,
|
||||
pathD,
|
||||
offsetX,
|
||||
offsetY,
|
||||
scale
|
||||
}: DrawSvgPathCommandsParams) {
|
||||
// console.log('drawSvgPathCommands 参数:', { pathD, offsetX, offsetY, scale });
|
||||
|
||||
// 精度处理函数:避免浮点数精度问题
|
||||
const roundToPrecision = (num: number, precision: number = 2): number => {
|
||||
return Math.round(num * Math.pow(10, precision)) / Math.pow(10, precision);
|
||||
};
|
||||
|
||||
// 新的解析方法:按命令片段解析
|
||||
// 使用正则表达式匹配从字母开头到下一个字母(或结尾)的片段
|
||||
const commandRegex = /([MLZ])([^MLZ]*?)(?=[MLZ]|$)/g;
|
||||
const commands: Array<{ cmd: string, coords: string }> = [];
|
||||
|
||||
let match;
|
||||
while ((match = commandRegex.exec(pathD)) !== null) {
|
||||
const cmd = match[1];
|
||||
const coords = match[2].trim();
|
||||
commands.push({ cmd, coords });
|
||||
}
|
||||
|
||||
// console.log('解析的命令片段:', commands);
|
||||
|
||||
let commandCount = 0;
|
||||
|
||||
// 优化的坐标解析和绘制函数
|
||||
const parseAndDrawCoords = (coords: string, cmd: string, drawFunction: (x: number, y: number) => void) => {
|
||||
// 更严格的坐标解析:支持负数和小数
|
||||
const coordParts = coords.split(/\s+/).filter(part => part.trim() !== '');
|
||||
|
||||
if (coordParts.length >= 2) {
|
||||
const x = parseFloat(coordParts[0]);
|
||||
const y = parseFloat(coordParts[1]);
|
||||
|
||||
if (!isNaN(x) && !isNaN(y) && isFinite(x) && isFinite(y)) {
|
||||
// 计算最终坐标并处理精度
|
||||
const finalX = roundToPrecision(offsetX + x * scale);
|
||||
const finalY = roundToPrecision(offsetY + y * scale);
|
||||
|
||||
// console.log(`${cmd}: 绘制到 (${finalX}, ${finalY}) [原始: (${x}, ${y})]`);
|
||||
drawFunction(finalX, finalY);
|
||||
} else {
|
||||
console.warn(`${cmd}: 坐标解析失败 - x=${x}, y=${y}, 原始坐标: "${coords}"`);
|
||||
}
|
||||
} else {
|
||||
console.warn(`${cmd}: 坐标数量不足,需要2个,实际${coordParts.length}个,原始: "${coords}"`);
|
||||
}
|
||||
};
|
||||
|
||||
// 命令处理映射表,提高可读性和可扩展性
|
||||
const commandHandlers: Record<string, (coords: string) => void> = {
|
||||
'M': (coords) => parseAndDrawCoords(coords, 'M', (x, y) => ctx.moveTo(x, y)),
|
||||
'L': (coords) => parseAndDrawCoords(coords, 'L', (x, y) => ctx.lineTo(x, y)),
|
||||
'Z': () => {
|
||||
// console.log('Z: 闭合路径');
|
||||
// 注意:不要在这里调用 closePath(),因为 drawStrokes 中会统一处理
|
||||
}
|
||||
};
|
||||
|
||||
for (const { cmd, coords } of commands) {
|
||||
commandCount++;
|
||||
// console.log(`命令 ${commandCount}: ${cmd}, 坐标: "${coords}"`);
|
||||
|
||||
const handler = commandHandlers[cmd];
|
||||
if (handler) {
|
||||
handler(coords);
|
||||
} else {
|
||||
console.warn(`未知命令: ${cmd}`);
|
||||
}
|
||||
}
|
||||
|
||||
// console.log(`总共处理了 ${commandCount} 个命令`);
|
||||
}
|
||||
|
||||
interface DrawStrokesParams {
|
||||
ctx: RenderingContext;
|
||||
strokes: string[];
|
||||
offsetX: number;
|
||||
offsetY: number;
|
||||
size: number;
|
||||
uptoInclusive: number;
|
||||
fillStyle: string;
|
||||
strokeStyle: string;
|
||||
lineWidth: number;
|
||||
}
|
||||
|
||||
function drawStrokes({
|
||||
ctx,
|
||||
strokes,
|
||||
offsetX,
|
||||
offsetY,
|
||||
size,
|
||||
uptoInclusive,
|
||||
fillStyle,
|
||||
strokeStyle,
|
||||
lineWidth
|
||||
}: DrawStrokesParams) {
|
||||
// 模板中使用 54x54 的视窗尺寸
|
||||
const viewBoxSize = 54;
|
||||
const scale = size / viewBoxSize;
|
||||
|
||||
// 添加内边距:在田字格四周预留空间,避免笔画贴边
|
||||
const padding = size * 0.1; // 内边距为田字格大小的10%
|
||||
const contentSize = size - padding * 2; // 实际绘制区域大小
|
||||
const contentScale = contentSize / viewBoxSize; // 调整后的缩放比例
|
||||
|
||||
// 计算内容区域的起始位置(居中)
|
||||
const contentOffsetX = offsetX + padding;
|
||||
const contentOffsetY = offsetY + padding;
|
||||
|
||||
console.log('drawStrokes 参数:', {
|
||||
strokesCount: strokes.length,
|
||||
uptoInclusive,
|
||||
offsetX,
|
||||
offsetY,
|
||||
size,
|
||||
padding,
|
||||
contentSize,
|
||||
contentOffsetX,
|
||||
contentOffsetY,
|
||||
originalScale: scale,
|
||||
contentScale,
|
||||
fillStyle,
|
||||
strokeStyle,
|
||||
lineWidth
|
||||
});
|
||||
|
||||
for (let s = 0; s <= uptoInclusive && s < strokes.length; s++) {
|
||||
// console.log(`绘制第 ${s} 个笔画:`, strokes[s]);
|
||||
ctx.beginPath();
|
||||
drawSvgPathCommands(ctx, strokes[s], offsetX, offsetY, scale);
|
||||
drawSvgPathCommands({
|
||||
ctx,
|
||||
pathD: strokes[s],
|
||||
offsetX: contentOffsetX,
|
||||
offsetY: contentOffsetY,
|
||||
scale: contentScale
|
||||
});
|
||||
ctx.fillStyle = fillStyle;
|
||||
ctx.strokeStyle = strokeStyle;
|
||||
ctx.lineWidth = lineWidth;
|
||||
@@ -86,14 +163,23 @@ function drawStrokes(
|
||||
}
|
||||
}
|
||||
|
||||
function drawTianZiGrid(
|
||||
ctx: RenderingContext,
|
||||
x: number,
|
||||
y: number,
|
||||
size: number,
|
||||
lineColor: string = '#e0e0e0',
|
||||
boldColor: string = '#cccccc',
|
||||
) {
|
||||
interface DrawTianZiGridParams {
|
||||
ctx: RenderingContext;
|
||||
x: number;
|
||||
y: number;
|
||||
size: number;
|
||||
lineColor?: string;
|
||||
boldColor?: string;
|
||||
}
|
||||
|
||||
function drawTianZiGrid({
|
||||
ctx,
|
||||
x,
|
||||
y,
|
||||
size,
|
||||
lineColor = '#e0e0e0',
|
||||
boldColor = '#cccccc'
|
||||
}: DrawTianZiGridParams) {
|
||||
// 外框
|
||||
ctx.strokeStyle = boldColor;
|
||||
ctx.lineWidth = 2;
|
||||
@@ -144,7 +230,7 @@ class WordDrawService {
|
||||
this.paperSize = 'A4';
|
||||
this.options = {
|
||||
appName: '涂鸦丫小程序',
|
||||
appHint: '练字|识字|打印',
|
||||
appHint: '识字|识图|练字|打印',
|
||||
title: '田字格 练 字 贴',
|
||||
subTitle: '按笔画临摹练习',
|
||||
...options,
|
||||
@@ -164,15 +250,19 @@ class WordDrawService {
|
||||
* 生成练字帖
|
||||
* @param wordsMap 形如 { "其": ["M.. L.. Z", ...], ... },不超过 10 个汉字
|
||||
*/
|
||||
draw(wordsMap: Record<string, string[]>) {
|
||||
async draw(wordsMap: Record<string, string[]>) {
|
||||
this.setPrintConfig();
|
||||
this.clear();
|
||||
this.setPaper();
|
||||
|
||||
// 等待页眉绘制完成,确保 this.currentY 被正确设置
|
||||
if (this.headerType !== 'minimal') {
|
||||
this.drawHeader();
|
||||
await this.drawHeader();
|
||||
} else {
|
||||
this.drawMiniHeader();
|
||||
await this.drawMiniHeader();
|
||||
}
|
||||
|
||||
// 页眉绘制完成后,再绘制内容
|
||||
this.drawContent(wordsMap);
|
||||
}
|
||||
|
||||
@@ -190,13 +280,25 @@ class WordDrawService {
|
||||
const logoWidth = 200;
|
||||
const logoHeight = 200;
|
||||
|
||||
// 这里沿用小程序码/Logo策略,保持与其它服务一致
|
||||
try {
|
||||
const { getMiniCodeImage } = await import('../utils/index');
|
||||
const image = await getMiniCodeImage(canvas);
|
||||
ctx.drawImage(image, logoX, logoY, logoWidth, logoHeight);
|
||||
} catch (_) {
|
||||
// 忽略加载失败
|
||||
switch (this.headerType) {
|
||||
case 'LogoImage': {
|
||||
const image = await getImage(canvas, '/assets/imgs/doodle-logo.png');
|
||||
ctx.drawImage(image, logoX, logoY, logoWidth, logoHeight);
|
||||
break;
|
||||
}
|
||||
case 'noLogoImage': {
|
||||
titleX = 120;
|
||||
break;
|
||||
}
|
||||
case 'minimal': {
|
||||
titleX = 120;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const image = await getMiniCodeImage(canvas);
|
||||
ctx.drawImage(image, logoX, logoY, logoWidth, logoHeight);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ctx.font = 'bold 64px "Microsoft Yahei"';
|
||||
@@ -217,7 +319,10 @@ class WordDrawService {
|
||||
ctx.fillStyle = '#666';
|
||||
ctx.fillText(subTitle, 1015, 186);
|
||||
|
||||
this.currentY = 304;
|
||||
// 计算页眉的实际高度,包括分割线
|
||||
const headerHeight = Math.max(logoY + logoHeight, titleY + 64 + 48 + 48) + 20; // 64px字体 + 48px间距 + 48px字体 + 20px边距
|
||||
// this.currentY = headerHeight;
|
||||
this.currentY += headerHeight;
|
||||
this.drawLine(this.currentY);
|
||||
}
|
||||
|
||||
@@ -230,12 +335,24 @@ class WordDrawService {
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(appName + ' ' + title, canvas.width / 2, titleY);
|
||||
this.currentY = 200;
|
||||
|
||||
// 计算迷你页眉的实际高度
|
||||
const miniHeaderHeight = titleY + 64 + 20; // 64px字体 + 20px边距
|
||||
this.currentY = miniHeaderHeight;
|
||||
this.drawLine(this.currentY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制正文内容:为每个汉字生成“预览 + 逐笔画临摹 + 空白格”的练习单元
|
||||
* 绘制正文内容:两阶段绘制 - 先绘制空田字格,再绘制练字内容
|
||||
*/
|
||||
/**
|
||||
* 绘制正文内容:包括田字格的排布和内容
|
||||
*
|
||||
* minGap 作用解释:
|
||||
* minGap(最小列间距)用于田字格水平方向(每一行格子之间的间距)的初始最小值。它保证多个田字格在一行内不会紧贴排布、而是有一个最小的间隔,整体看起来不会拥挤。后续还会结合画布实际剩余空间动态调整为更合适的实际间距 actualColGap。
|
||||
*
|
||||
* rowGap 作用解释:
|
||||
* rowGap(行间距)用于田字格竖直方向(每一行田字格之间的间隔)的初始最小值。它防止行与行的田字格上下贴得太紧,增加一定的可视呼吸空间。之后同样会结合剩余空间动态调整为实际间距 actualRowGap。
|
||||
*/
|
||||
drawContent(wordsMap: Record<string, string[]>) {
|
||||
const { canvas, ctx } = this;
|
||||
@@ -248,84 +365,285 @@ class WordDrawService {
|
||||
const rightMargin = 120;
|
||||
const bottomMargin = 120;
|
||||
const contentTop = this.currentY + topGap;
|
||||
console.log('contentTop----:', contentTop)
|
||||
const contentWidth = canvas.width - leftMargin - rightMargin;
|
||||
const contentHeight = canvas.height - contentTop - bottomMargin;
|
||||
|
||||
const cellSize = 140;
|
||||
const minGap = 24;
|
||||
const minGap = 24; // 最小格子列间距,用于保证横向不拥挤,后续可动态调整为实际间距
|
||||
const rowGap = 36; // 最小格子行间距,用于保证纵向不拥挤,后续可动态调整为实际间距
|
||||
|
||||
// 计算每行可容纳的田字格数量
|
||||
const maxPerRow = Math.max(1, Math.floor((contentWidth + minGap) / (cellSize + minGap)));
|
||||
const rowGap = 36;
|
||||
|
||||
// 为每个汉字生成一串单元:1个预览 + N个逐笔画 + 2个空白 (便于描摹)
|
||||
const unitsPerCharBuilder = (strokeCount: number) => {
|
||||
const preview = 1;
|
||||
const practice = Math.min(strokeCount, 8); // 最多展示 8 步逐笔画,避免过长
|
||||
const blanks = 2;
|
||||
return preview + practice + blanks;
|
||||
};
|
||||
// 计算每列可容纳的田字格数量
|
||||
const actualRows = Math.max(1, Math.floor((contentHeight + rowGap) / (cellSize + rowGap)));
|
||||
|
||||
let cursorX = leftMargin;
|
||||
let cursorY = contentTop + cellSize / 2;
|
||||
let usedInRow = 0;
|
||||
// 计算总需要的田字格数量
|
||||
const totalCellsNeeded = actualRows * maxPerRow;
|
||||
|
||||
characters.forEach((char) => {
|
||||
const strokes = wordsMap[char] || [];
|
||||
const totalUnits = unitsPerCharBuilder(strokes.length);
|
||||
let unitIndex = 0;
|
||||
// 计算实际需要的行数
|
||||
console.log('actualRows----:', actualRows)
|
||||
console.log('totalCellsNeeded----:', totalCellsNeeded)
|
||||
console.log('maxPerRow----:', maxPerRow)
|
||||
|
||||
while (unitIndex < totalUnits) {
|
||||
// 如果本行放不下,换行
|
||||
if (usedInRow >= maxPerRow) {
|
||||
cursorX = leftMargin;
|
||||
cursorY += cellSize + rowGap;
|
||||
usedInRow = 0;
|
||||
// 边界检查
|
||||
if (cursorY + cellSize / 2 > canvas.height - bottomMargin) return;
|
||||
}
|
||||
// 动态调整间距以充分利用空间(在允许的最小间距基础上弹性扩展以适配画布)
|
||||
const actualRowGap = actualRows > 1 ? (contentHeight - actualRows * cellSize) / (actualRows - 1) : 0;
|
||||
const actualColGap = maxPerRow > 1 ? (contentWidth - maxPerRow * cellSize) / (maxPerRow - 1) : 0;
|
||||
|
||||
// 绘制田字格
|
||||
drawTianZiGrid(ctx, cursorX + cellSize / 2, cursorY, cellSize);
|
||||
console.log(`布局信息: 总格数=${totalCellsNeeded}, 行数=${actualRows}, 列数=${maxPerRow}, 行间距=${actualRowGap.toFixed(1)}, 列间距=${actualColGap.toFixed(1)}`);
|
||||
|
||||
// 单元类型:0 预览;1..practice 逐笔画;最后 blanks 空白
|
||||
const practiceMax = Math.min(strokes.length, 8);
|
||||
if (unitIndex === 0) {
|
||||
// 预览:全部笔画使用较深颜色
|
||||
drawStrokes(
|
||||
ctx,
|
||||
strokes,
|
||||
cursorX + (cellSize - cellSize) / 2,
|
||||
cursorY - cellSize / 2,
|
||||
cellSize,
|
||||
strokes.length - 1,
|
||||
'rgb(0,0,0)',
|
||||
'#000',
|
||||
1.6,
|
||||
);
|
||||
} else if (unitIndex <= practiceMax) {
|
||||
// 逐笔画:累进显示到第 k 画,使用灰色/描边
|
||||
drawStrokes(
|
||||
ctx,
|
||||
strokes,
|
||||
cursorX + (cellSize - cellSize) / 2,
|
||||
cursorY - cellSize / 2,
|
||||
cellSize,
|
||||
unitIndex - 1,
|
||||
'rgb(184,184,184)',
|
||||
'#999',
|
||||
1.6,
|
||||
);
|
||||
} else {
|
||||
// 空白格:不画笔画
|
||||
}
|
||||
// 第一阶段:绘制所有空田字格
|
||||
this.drawEmptyGrids({
|
||||
ctx,
|
||||
startX: leftMargin,
|
||||
startY: contentTop,
|
||||
cellSize,
|
||||
colGap: actualColGap,
|
||||
rowGap: actualRowGap,
|
||||
maxPerRow,
|
||||
maxRows: actualRows
|
||||
});
|
||||
|
||||
// 前进到下一个单元
|
||||
cursorX += cellSize + minGap;
|
||||
usedInRow += 1;
|
||||
unitIndex += 1;
|
||||
// 第二阶段:绘制练字内容
|
||||
this.drawPracticeContent({
|
||||
ctx,
|
||||
wordsMap,
|
||||
startX: leftMargin,
|
||||
startY: contentTop,
|
||||
cellSize,
|
||||
colGap: actualColGap,
|
||||
rowGap: actualRowGap,
|
||||
maxPerRow
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算总需要的田字格数量
|
||||
*/
|
||||
// calculateTotalCellsNeeded(characters: string[], wordsMap: Record<string, string[]>): number {
|
||||
// let totalCells = 0;
|
||||
// characters.forEach((char) => {
|
||||
// const strokes = wordsMap[char] || [];
|
||||
// const strokeCount = strokes.length;
|
||||
// const preview = 1; // 预览格
|
||||
// const practice = Math.min(strokeCount, 8); // 最多8个练习格
|
||||
// const blanks = 2; // 空白格
|
||||
// totalCells += preview + practice + blanks;
|
||||
// });
|
||||
// return totalCells;
|
||||
// }
|
||||
|
||||
/**
|
||||
* 绘制空田字格网格
|
||||
*/
|
||||
drawEmptyGrids({
|
||||
ctx,
|
||||
startX,
|
||||
startY,
|
||||
cellSize,
|
||||
colGap,
|
||||
rowGap,
|
||||
maxPerRow,
|
||||
maxRows
|
||||
}: {
|
||||
ctx: RenderingContext;
|
||||
startX: number;
|
||||
startY: number;
|
||||
cellSize: number;
|
||||
colGap: number;
|
||||
rowGap: number;
|
||||
maxPerRow: number;
|
||||
maxRows: number;
|
||||
}) {
|
||||
console.log('drawEmptyGrids startY----:', startY)
|
||||
for (let row = 0; row < maxRows; row++) {
|
||||
for (let col = 0; col < maxPerRow; col++) {
|
||||
const x = startX + col * (cellSize + colGap) + cellSize / 2;
|
||||
const y = startY + row * (cellSize + rowGap) + cellSize / 2;
|
||||
drawTianZiGrid({ ctx, x, y, size: cellSize });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制练字内容(参考 template.html 的实现)
|
||||
*/
|
||||
drawPracticeContent({
|
||||
ctx,
|
||||
wordsMap,
|
||||
startX,
|
||||
startY,
|
||||
cellSize,
|
||||
colGap,
|
||||
rowGap,
|
||||
maxPerRow
|
||||
}: {
|
||||
ctx: RenderingContext;
|
||||
wordsMap: Record<string, string[]>;
|
||||
startX: number;
|
||||
startY: number;
|
||||
cellSize: number;
|
||||
colGap: number;
|
||||
rowGap: number;
|
||||
maxPerRow: number;
|
||||
}) {
|
||||
const characters = Object.keys(wordsMap);
|
||||
let cellIndex = 0;
|
||||
|
||||
console.log('开始绘制练字内容,汉字数量:', characters.length);
|
||||
|
||||
characters.forEach((char, charIndex) => {
|
||||
const strokes = wordsMap[char] || [];
|
||||
const strokeCount = strokes.length;
|
||||
|
||||
console.log(`绘制第 ${charIndex + 1} 个汉字 "${char}",笔画数: ${strokeCount}`);
|
||||
|
||||
// 1. 预览格:显示完整汉字(黑色)
|
||||
this.drawPreviewCell({
|
||||
ctx,
|
||||
strokes,
|
||||
startX,
|
||||
startY,
|
||||
cellSize,
|
||||
colGap,
|
||||
rowGap,
|
||||
maxPerRow,
|
||||
cellIndex
|
||||
});
|
||||
cellIndex++;
|
||||
|
||||
// 2. 练习格:逐笔画显示(红色)
|
||||
for (let strokeIndex = 0; strokeIndex < strokeCount; strokeIndex++) {
|
||||
// 先绘制当前笔画
|
||||
this.drawPracticeCell({
|
||||
ctx,
|
||||
strokes,
|
||||
strokeIndex,
|
||||
startX,
|
||||
startY,
|
||||
cellSize,
|
||||
colGap,
|
||||
rowGap,
|
||||
maxPerRow,
|
||||
cellIndex
|
||||
});
|
||||
cellIndex++;
|
||||
|
||||
console.log(`笔画 ${strokeIndex + 1} 绘制完成,当前 cellIndex: ${cellIndex}`);
|
||||
}
|
||||
|
||||
// 3. 汉字间留空:每个汉字后留2个空白格
|
||||
cellIndex += 0;
|
||||
|
||||
// 4. 强制下一个汉字换行到新行的第一个田字格
|
||||
if (charIndex < characters.length - 1) { // 不是最后一个汉字
|
||||
const currentRow = Math.floor(cellIndex / maxPerRow);
|
||||
const nextRow = currentRow + 1;
|
||||
cellIndex = nextRow * maxPerRow;
|
||||
console.log(`汉字 "${char}" 绘制完成,强制换行到新行,下一个汉字从 cellIndex: ${cellIndex} 开始`);
|
||||
} else {
|
||||
console.log(`汉字 "${char}" 绘制完成,这是最后一个汉字`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('所有汉字绘制完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制预览格(完整汉字)
|
||||
*/
|
||||
drawPreviewCell({
|
||||
ctx,
|
||||
strokes,
|
||||
startX,
|
||||
startY,
|
||||
cellSize,
|
||||
colGap,
|
||||
rowGap,
|
||||
maxPerRow,
|
||||
cellIndex
|
||||
}: {
|
||||
ctx: RenderingContext;
|
||||
strokes: string[];
|
||||
startX: number;
|
||||
startY: number;
|
||||
cellSize: number;
|
||||
colGap: number;
|
||||
rowGap: number;
|
||||
maxPerRow: number;
|
||||
cellIndex: number;
|
||||
}) {
|
||||
const row = Math.floor(cellIndex / maxPerRow);
|
||||
const col = cellIndex % maxPerRow;
|
||||
const x = startX + col * (cellSize + colGap);
|
||||
const y = startY + row * (cellSize + rowGap);
|
||||
|
||||
console.log(`预览格 [${cellIndex}] 坐标: (${x}, ${y}), 行列: (${row}, ${col}), 笔画数: ${strokes.length}`);
|
||||
|
||||
// 绘制完整汉字(黑色,较粗)
|
||||
drawStrokes({
|
||||
ctx,
|
||||
strokes,
|
||||
offsetX: x,
|
||||
offsetY: y,
|
||||
size: cellSize,
|
||||
uptoInclusive: strokes.length - 1,
|
||||
fillStyle: 'rgb(0,0,0)', // 黑色填充
|
||||
strokeStyle: 'rgb(0,0,0)', // 黑色描边
|
||||
lineWidth: 4, // 较粗的线条
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制练习格(逐笔画)
|
||||
*/
|
||||
drawPracticeCell({
|
||||
ctx,
|
||||
strokes,
|
||||
strokeIndex,
|
||||
startX,
|
||||
startY,
|
||||
cellSize,
|
||||
colGap,
|
||||
rowGap,
|
||||
maxPerRow,
|
||||
cellIndex
|
||||
}: {
|
||||
ctx: RenderingContext;
|
||||
strokes: string[];
|
||||
strokeIndex: number;
|
||||
startX: number;
|
||||
startY: number;
|
||||
cellSize: number;
|
||||
colGap: number;
|
||||
rowGap: number;
|
||||
maxPerRow: number;
|
||||
cellIndex: number;
|
||||
}) {
|
||||
const row = Math.floor(cellIndex / maxPerRow);
|
||||
const col = cellIndex % maxPerRow;
|
||||
const x = startX + col * (cellSize + colGap);
|
||||
const y = startY + row * (cellSize + rowGap);
|
||||
|
||||
console.log(`练习格 [${cellIndex}] 坐标: (${x}, ${y}), 行列: (${row}, ${col}), 笔画: ${strokeIndex + 1}/${strokes.length}`);
|
||||
|
||||
// 绘制到指定笔画的汉字(红色,中等粗细)
|
||||
drawStrokes({
|
||||
ctx,
|
||||
strokes,
|
||||
offsetX: x,
|
||||
offsetY: y,
|
||||
size: cellSize,
|
||||
uptoInclusive: strokeIndex,
|
||||
fillStyle: 'rgb(220, 20, 20)', // 深红色填充
|
||||
strokeStyle: 'rgb(220, 20, 20)', // 深红色描边
|
||||
lineWidth: 3, // 中等粗细
|
||||
});
|
||||
}
|
||||
|
||||
drawLine(linY: number) {
|
||||
console.log('drawLine linY----:', linY)
|
||||
const { canvas, ctx } = this;
|
||||
ctx.strokeStyle = '#000';
|
||||
ctx.lineWidth = 2;
|
||||
|
||||
Reference in New Issue
Block a user