feat: 修改字母描红逻辑,添加字母手写字体
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 277 KiB |
@@ -0,0 +1,94 @@
|
|||||||
|
Copyright (c) 2012, Font Diner (www.fontdiner.com),
|
||||||
|
with Reserved Font Name "Codystar".
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2010 The Raleway Project Authors (impallari@gmail.com), with Reserved Font Name "Raleway".
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright (c) 2010, Matt McInerney (matt@pixelspread.com), Copyright (c) 2012, Pablo Impallari (www.impallari.com|impallari@gmail.com), Copyright (c) 2012, Brenda Gallo. (gbrenda1987@gmail.com), Copyright (c) 2012, Rodrigo Fuenzalida (www.rfuenzalida.com|hello@rfuenzalida.com), with Reserved Font Name "Raleway"
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
|
After Width: | Height: | Size: 324 KiB |
@@ -0,0 +1,22 @@
|
|||||||
|
## 核心推荐字体(最接近衡水体风格)
|
||||||
|
|
||||||
|
### 1、Print Clearly(最匹配)
|
||||||
|
|
||||||
|
风格:干净的手写印刷体,字母形态简单、间距均匀,模仿小学生练字本效果,提供 Regular/Bold/Dashed 三种样式
|
||||||
|
授权:100% 免费商用,无需署名
|
||||||
|
下载:Fontesk 官网 https://fontesk.com/print-clearly-font/
|
||||||
|
|
||||||
|
### 2、ZhiyongWrite(智勇手书英文体)(中文教学适配)
|
||||||
|
|
||||||
|
风格:专为中文用户设计的手写英文,笔画圆润、无连笔,可与中文手书体搭配使用
|
||||||
|
授权:免费商用(个人 / 企业),不可修改 / 转售 / 嵌入商标
|
||||||
|
下载:100font https://www.100font.com/thread-101.htm;
|
||||||
|
站酷 https://www.zcool.com.cn/work/ZMjkyMDMwODQ=.html
|
||||||
|
|
||||||
|
## 选择字体的网站
|
||||||
|
|
||||||
|
- https://fontesk.com/font/handwritten/page/2/?license=free-for-commercial-use
|
||||||
|
- https://fonts.google.com/?preview.lang=en_Latn&lang=en_Latn&categoryFilters=Feeling:%2FExpressive%2FChildlike
|
||||||
|
- https://www.100font.com/forum-1-1.htm?tagids=3_0_0_0
|
||||||
|
|
||||||
|
- https://www.nqez.com/ (可模仿的汉字字帖)
|
||||||
@@ -3,14 +3,20 @@
|
|||||||
"pages/home/home",
|
"pages/home/home",
|
||||||
"pages/age/age",
|
"pages/age/age",
|
||||||
"pages/favorites/favorites",
|
"pages/favorites/favorites",
|
||||||
"pages/profile/profile",
|
"pages/profile/profile"
|
||||||
"pages/index/index"
|
|
||||||
],
|
],
|
||||||
"subPackages": [
|
"subPackages": [
|
||||||
{
|
{
|
||||||
"root": "supportPages",
|
"root": "supportPages",
|
||||||
"name": "supportPages",
|
"name": "supportPages",
|
||||||
"pages": ["guide/guide", "debug/debug"],
|
"pages": [
|
||||||
|
"guide/guide",
|
||||||
|
"debug/debug",
|
||||||
|
"index/index",
|
||||||
|
"mathIndex/mathIndex",
|
||||||
|
"focusIndex/focusIndex",
|
||||||
|
"copyBook/copyBook"
|
||||||
|
],
|
||||||
"independent": false
|
"independent": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
@font-face {
|
|
||||||
font-family: 'j-icon';
|
|
||||||
/* Project id 4942823 */
|
|
||||||
src:
|
|
||||||
url('data:application/x-font-woff;charset=utf-8;base64,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') format('woff'),
|
|
||||||
url('iconfont.ttf?t=1749287800675') format('truetype');
|
|
||||||
}
|
|
||||||
|
|
||||||
.j-icon {
|
|
||||||
font-family: 'j-icon' !important;
|
|
||||||
font-size: 16px;
|
|
||||||
font-style: normal;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
.j-icon-editor:before {
|
|
||||||
content: '\e72a';
|
|
||||||
}
|
|
||||||
|
|
||||||
.j-icon-close:before {
|
|
||||||
content: '\e704';
|
|
||||||
}
|
|
||||||
|
|
||||||
.j-icon-download:before {
|
|
||||||
content: '\e716';
|
|
||||||
}
|
|
||||||
|
|
||||||
.j-icon-error:before {
|
|
||||||
content: '\e73b';
|
|
||||||
}
|
|
||||||
|
|
||||||
.j-icon-close-bold:before {
|
|
||||||
content: '\e70d';
|
|
||||||
}
|
|
||||||
|
|
||||||
.j-icon-translate:before {
|
|
||||||
content: '\e73c';
|
|
||||||
}
|
|
||||||
|
|
||||||
.j-icon-share:before {
|
|
||||||
content: '\e729';
|
|
||||||
}
|
|
||||||
|
|
||||||
.j-icon-share-fill:before {
|
|
||||||
content: '\e728';
|
|
||||||
}
|
|
||||||
|
|
||||||
.j-icon-wechat:before {
|
|
||||||
content: '\e727';
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
@font-face {
|
|
||||||
font-family: "toy-icon";
|
|
||||||
/* Project id 4942823 */
|
|
||||||
src:
|
|
||||||
url('data:application/x-font-woff;charset=utf-8;base64,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') format('woff'),
|
|
||||||
url('iconfont.ttf?t=1749610446637') format('truetype');
|
|
||||||
}
|
|
||||||
|
|
||||||
.toy-icon {
|
|
||||||
font-family: "toy-icon" !important;
|
|
||||||
font-size: 16px;
|
|
||||||
font-style: normal;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toy-icon-dustbin-o:before {
|
|
||||||
content: "\e71d";
|
|
||||||
}
|
|
||||||
|
|
||||||
.toy-icon-editor-o:before {
|
|
||||||
content: "\e72a";
|
|
||||||
}
|
|
||||||
|
|
||||||
.toy-icon-close:before {
|
|
||||||
content: "\e704";
|
|
||||||
}
|
|
||||||
|
|
||||||
.toy-icon-download:before {
|
|
||||||
content: "\e716";
|
|
||||||
}
|
|
||||||
|
|
||||||
.toy-icon-error-o:before {
|
|
||||||
content: "\e73b";
|
|
||||||
}
|
|
||||||
|
|
||||||
.toy-icon-close-bold:before {
|
|
||||||
content: "\e70d";
|
|
||||||
}
|
|
||||||
|
|
||||||
.toy-icon-translate:before {
|
|
||||||
content: "\e73c";
|
|
||||||
}
|
|
||||||
|
|
||||||
.toy-icon-share-o:before {
|
|
||||||
content: "\e729";
|
|
||||||
}
|
|
||||||
|
|
||||||
.toy-icon-share:before {
|
|
||||||
content: "\e728";
|
|
||||||
}
|
|
||||||
|
|
||||||
.toy-icon-wechat-o:before {
|
|
||||||
content: "\e727";
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 41 KiB |
@@ -14,7 +14,7 @@ import drawHeader from './drawHeader';
|
|||||||
* - 打印配置管理
|
* - 打印配置管理
|
||||||
*/
|
*/
|
||||||
const LINE_COLOR = 'rgba(50, 46, 37, 0.4)';
|
const LINE_COLOR = 'rgba(50, 46, 37, 0.4)';
|
||||||
const LINE_WIDTH = 2;
|
const LINE_WIDTH = 3;
|
||||||
|
|
||||||
export class BaseDrawService {
|
export class BaseDrawService {
|
||||||
canvas: WechatMiniprogram.Canvas;
|
canvas: WechatMiniprogram.Canvas;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const META_FONT_PX = 12;
|
|||||||
const META_UNDERLINE_GAP = 3;
|
const META_UNDERLINE_GAP = 3;
|
||||||
const TITLE_COLOR = '#322E25';
|
const TITLE_COLOR = '#322E25';
|
||||||
const LINE_COLOR = 'rgba(50, 46, 37, 0.4)';
|
const LINE_COLOR = 'rgba(50, 46, 37, 0.4)';
|
||||||
const LINE_WIDTH = 2;
|
const LINE_WIDTH = 3;
|
||||||
const META_COLOR = '#7C766A';
|
const META_COLOR = '#7C766A';
|
||||||
const META_ROW_MARGIN_X = 130;
|
const META_ROW_MARGIN_X = 130;
|
||||||
|
|
||||||
|
|||||||
@@ -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 '../../core/data/alphabet';
|
import { LETTERS_UPPER } from '../shared/data/alphabet';
|
||||||
|
|
||||||
/** 各 worksheetId 对应的页面标题和描述 */
|
/** 各 worksheetId 对应的页面标题和描述 */
|
||||||
const PAGE_META: Record<string, { title: string; desc: string }> = {
|
const PAGE_META: Record<string, { title: string; desc: string }> = {
|
||||||
@@ -99,7 +99,7 @@ createPage(
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 页面就绪:初始化 Canvas 和绘制服务,完成后触发首次绘制 */
|
/** 页面就绪:初始化 Canvas → 首次绘制(标题行前缀使用系统 emoji,无需 loadFontFace) */
|
||||||
onReady() {
|
onReady() {
|
||||||
this.initCanvas({
|
this.initCanvas({
|
||||||
createDrawService: (
|
createDrawService: (
|
||||||
|
|||||||
|
After Width: | Height: | Size: 9.6 KiB |
@@ -0,0 +1,124 @@
|
|||||||
|
import { BaseDrawService } from '../../../core/draw/baseDraw';
|
||||||
|
import type {
|
||||||
|
LetterTracingData,
|
||||||
|
TracingRow,
|
||||||
|
} from '../generators/letter-tracing-generator';
|
||||||
|
import { drawFourLineGrid } from './drawTools';
|
||||||
|
|
||||||
|
const LETTER_FACE =
|
||||||
|
'Helvetica, Arial, "Segoe UI", Roboto, sans-serif';
|
||||||
|
|
||||||
|
type CasePairingData = Extract<LetterTracingData, { mode: 'case-pairing' }>;
|
||||||
|
|
||||||
|
export default class CasePairingDraw extends BaseDrawService {
|
||||||
|
async draw(data: CasePairingData) {
|
||||||
|
this.prepareDraw();
|
||||||
|
await this.drawHeaderAndDivider();
|
||||||
|
this.drawContent(data.leftVisualRows, data.rightVisualRows);
|
||||||
|
this.drawPrintFooter();
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawContent(
|
||||||
|
leftVisualRows: TracingRow[][],
|
||||||
|
rightVisualRows: TracingRow[][],
|
||||||
|
) {
|
||||||
|
const M = 24;
|
||||||
|
const y0 = this.currentY + 6;
|
||||||
|
const bottom = this.canvasHeight - M;
|
||||||
|
const gapCol = 18;
|
||||||
|
const colW = (this.canvasWidth - M * 2 - gapCol) / 2;
|
||||||
|
const maxVisual = Math.max(
|
||||||
|
leftVisualRows.length,
|
||||||
|
rightVisualRows.length,
|
||||||
|
);
|
||||||
|
const rowSlotH = Math.max(40, (bottom - y0) / maxVisual - 6);
|
||||||
|
|
||||||
|
const drawSide = (
|
||||||
|
blocks: TracingRow[][],
|
||||||
|
startX: number,
|
||||||
|
width: number,
|
||||||
|
) => {
|
||||||
|
let y = y0;
|
||||||
|
for (const visualRow of blocks) {
|
||||||
|
const nSeg = visualRow.length;
|
||||||
|
const segGap = 6;
|
||||||
|
const segW = (width - (nSeg - 1) * segGap) / nSeg;
|
||||||
|
for (let s = 0; s < nSeg; s++) {
|
||||||
|
this.drawTracingRow(
|
||||||
|
visualRow[s],
|
||||||
|
startX + s * (segW + segGap),
|
||||||
|
y,
|
||||||
|
rowSlotH,
|
||||||
|
segW,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
y += rowSlotH + 8;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
drawSide(leftVisualRows, M, colW);
|
||||||
|
drawSide(rightVisualRows, M + colW + gapCol, colW);
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawTracingRow(
|
||||||
|
row: TracingRow,
|
||||||
|
startX: number,
|
||||||
|
y: number,
|
||||||
|
rowH: number,
|
||||||
|
totalWidth: number,
|
||||||
|
) {
|
||||||
|
const gap = 4;
|
||||||
|
const n = row.cells.length;
|
||||||
|
const cellW = n > 0 ? (totalWidth - gap * (n - 1)) / n : totalWidth;
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const cx = startX + i * (cellW + gap);
|
||||||
|
drawFourLineGrid(this.ctx, cx, y, cellW, rowH);
|
||||||
|
const cell = row.cells[i];
|
||||||
|
this.drawCharInCell(
|
||||||
|
cell.char,
|
||||||
|
cx,
|
||||||
|
y,
|
||||||
|
cellW,
|
||||||
|
rowH,
|
||||||
|
cell.opacity,
|
||||||
|
cell.isGuide,
|
||||||
|
cell.pairChar,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawCharInCell(
|
||||||
|
ch: string,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
w: number,
|
||||||
|
h: number,
|
||||||
|
opacity: number,
|
||||||
|
bold: boolean,
|
||||||
|
pairChar?: string,
|
||||||
|
) {
|
||||||
|
const ctx = this.ctx;
|
||||||
|
if (opacity <= 0) return;
|
||||||
|
ctx.save();
|
||||||
|
ctx.globalAlpha = opacity;
|
||||||
|
ctx.fillStyle = bold ? '#1a1a1a' : '#4a4a4a';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'alphabetic';
|
||||||
|
const baselineY = y + h * 0.72;
|
||||||
|
|
||||||
|
if (pairChar) {
|
||||||
|
const fs = Math.min(h * 0.5, w * 0.22);
|
||||||
|
ctx.font = `${bold ? 'bold ' : ''}${fs}px ${LETTER_FACE}`;
|
||||||
|
const gap = fs * 0.2;
|
||||||
|
const totalW = fs * 2 + gap;
|
||||||
|
const x0 = x + w / 2 - totalW / 2;
|
||||||
|
ctx.fillText(ch, x0 + fs * 0.5, baselineY);
|
||||||
|
ctx.fillText(pairChar, x0 + fs * 1.5 + gap, baselineY);
|
||||||
|
} else {
|
||||||
|
const fontSize = Math.min(h * 0.72, w * 0.85);
|
||||||
|
ctx.font = `${bold ? 'bold ' : ''}${fontSize}px ${LETTER_FACE}`;
|
||||||
|
ctx.fillText(ch, x + w / 2, baselineY);
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { BaseDrawService } from '../../../core/draw/baseDraw';
|
||||||
|
import type {
|
||||||
|
LetterTracingData,
|
||||||
|
TracingRow,
|
||||||
|
} from '../generators/letter-tracing-generator';
|
||||||
|
import { drawFourLineGrid } from './drawTools';
|
||||||
|
|
||||||
|
const LETTER_FACE =
|
||||||
|
'Helvetica, Arial, "Segoe UI", Roboto, sans-serif';
|
||||||
|
|
||||||
|
type ColumnCompareData = Extract<
|
||||||
|
LetterTracingData,
|
||||||
|
{ mode: 'column-compare' }
|
||||||
|
>;
|
||||||
|
|
||||||
|
export default class ColumnCompareDraw extends BaseDrawService {
|
||||||
|
async draw(data: ColumnCompareData) {
|
||||||
|
this.prepareDraw();
|
||||||
|
await this.drawHeaderAndDivider();
|
||||||
|
this.drawContent(data.leftRows, data.rightRows);
|
||||||
|
this.drawPrintFooter();
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawContent(leftRows: TracingRow[], rightRows: TracingRow[]) {
|
||||||
|
const M = 24;
|
||||||
|
const y0 = this.currentY + 6;
|
||||||
|
const bottom = this.canvasHeight - M;
|
||||||
|
const maxR = Math.max(leftRows.length, rightRows.length);
|
||||||
|
const rowH = Math.max(36, (bottom - y0) / maxR - 6);
|
||||||
|
const gapCol = 16;
|
||||||
|
const colW = (this.canvasWidth - M * 2 - gapCol) / 2;
|
||||||
|
|
||||||
|
for (let i = 0; i < leftRows.length; i++) {
|
||||||
|
const y = y0 + i * (rowH + 6);
|
||||||
|
this.drawTracingRow(leftRows[i], M, y, rowH, colW);
|
||||||
|
}
|
||||||
|
const xR = M + colW + gapCol;
|
||||||
|
for (let i = 0; i < rightRows.length; i++) {
|
||||||
|
const y = y0 + i * (rowH + 6);
|
||||||
|
this.drawTracingRow(rightRows[i], xR, y, rowH, colW);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawTracingRow(
|
||||||
|
row: TracingRow,
|
||||||
|
startX: number,
|
||||||
|
y: number,
|
||||||
|
rowH: number,
|
||||||
|
totalWidth: number,
|
||||||
|
) {
|
||||||
|
const gap = 4;
|
||||||
|
const n = row.cells.length;
|
||||||
|
const cellW = n > 0 ? (totalWidth - gap * (n - 1)) / n : totalWidth;
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const cx = startX + i * (cellW + gap);
|
||||||
|
drawFourLineGrid(this.ctx, cx, y, cellW, rowH);
|
||||||
|
const cell = row.cells[i];
|
||||||
|
this.drawCharInCell(
|
||||||
|
cell.char,
|
||||||
|
cx,
|
||||||
|
y,
|
||||||
|
cellW,
|
||||||
|
rowH,
|
||||||
|
cell.opacity,
|
||||||
|
cell.isGuide,
|
||||||
|
cell.pairChar,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawCharInCell(
|
||||||
|
ch: string,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
w: number,
|
||||||
|
h: number,
|
||||||
|
opacity: number,
|
||||||
|
bold: boolean,
|
||||||
|
pairChar?: string,
|
||||||
|
) {
|
||||||
|
const ctx = this.ctx;
|
||||||
|
if (opacity <= 0) return;
|
||||||
|
ctx.save();
|
||||||
|
ctx.globalAlpha = opacity;
|
||||||
|
ctx.fillStyle = bold ? '#1a1a1a' : '#4a4a4a';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'alphabetic';
|
||||||
|
const baselineY = y + h * 0.72;
|
||||||
|
|
||||||
|
if (pairChar) {
|
||||||
|
const fs = Math.min(h * 0.5, w * 0.22);
|
||||||
|
ctx.font = `${bold ? 'bold ' : ''}${fs}px ${LETTER_FACE}`;
|
||||||
|
const gap = fs * 0.2;
|
||||||
|
const totalW = fs * 2 + gap;
|
||||||
|
const x0 = x + w / 2 - totalW / 2;
|
||||||
|
ctx.fillText(ch, x0 + fs * 0.5, baselineY);
|
||||||
|
ctx.fillText(pairChar, x0 + fs * 1.5 + gap, baselineY);
|
||||||
|
} else {
|
||||||
|
const fontSize = Math.min(h * 0.72, w * 0.85);
|
||||||
|
ctx.font = `${bold ? 'bold ' : ''}${fontSize}px ${LETTER_FACE}`;
|
||||||
|
ctx.fillText(ch, x + w / 2, baselineY);
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* 绘制工具集合(后续可继续扩展其他通用绘制方法)
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type FourLineGridStyle = {
|
||||||
|
/** 顶线/底线颜色 */
|
||||||
|
ink?: string;
|
||||||
|
/** 中间两条虚线颜色 */
|
||||||
|
middleInk?: string;
|
||||||
|
/** 线宽 */
|
||||||
|
lineWidth?: number;
|
||||||
|
/** 虚线样式 */
|
||||||
|
dash?: number[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 四线三格(通用)
|
||||||
|
* - 顶/底:实线
|
||||||
|
* - 中间两条:虚线
|
||||||
|
*/
|
||||||
|
export function drawFourLineGrid(
|
||||||
|
ctx: RenderingContext,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
w: number,
|
||||||
|
h: number,
|
||||||
|
style?: FourLineGridStyle,
|
||||||
|
) {
|
||||||
|
const ink = style?.ink ?? '#322E25';
|
||||||
|
const middleInk = style?.middleInk ?? 'rgba(50, 46, 37, 0.3)';
|
||||||
|
const lineWidth = style?.lineWidth ?? 1;
|
||||||
|
const dash = style?.dash ?? [4, 4];
|
||||||
|
|
||||||
|
const yTop = y;
|
||||||
|
const y1 = y + h / 3;
|
||||||
|
const y2 = y + (h * 2) / 3;
|
||||||
|
const yBot = y + h;
|
||||||
|
|
||||||
|
ctx.strokeStyle = ink;
|
||||||
|
ctx.lineWidth = lineWidth;
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, yTop);
|
||||||
|
ctx.lineTo(x + w, yTop);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.strokeStyle = middleInk;
|
||||||
|
ctx.setLineDash(dash);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, y1);
|
||||||
|
ctx.lineTo(x + w, y1);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, y2);
|
||||||
|
ctx.lineTo(x + w, y2);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.strokeStyle = ink;
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, yBot);
|
||||||
|
ctx.lineTo(x + w, yBot);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,330 +1,24 @@
|
|||||||
import { BaseDrawService } from '../../../core/draw/baseDraw';
|
import { BaseDrawService } from '../../../core/draw/baseDraw';
|
||||||
import type {
|
import type { LetterTracingData } from '../generators/letter-tracing-generator';
|
||||||
LetterTracingData,
|
import PictureTracingDraw from './pictureTracingDraw';
|
||||||
TracingRow,
|
import CasePairingDraw from './casePairingDraw';
|
||||||
} from '../generators/letter-tracing-generator';
|
import ColumnCompareDraw from './columnCompareDraw';
|
||||||
|
import OverviewDraw from './overviewDraw';
|
||||||
const M = 24;
|
|
||||||
/** 四线三格线色:偏黑,实线与虚线统一色相,仅以线型区分 */
|
|
||||||
const LINE_INK = '#2a2a2a';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 字母描红绘制
|
* 字母描红绘制分发层。
|
||||||
* 四线三格:顶线、上中线、下中线为格内三线间距;最上最下为实线,中间两虚线(产品文档)
|
* 根据 data.mode 委托给对应的独立 DrawService 完成实际绘制。
|
||||||
*/
|
*/
|
||||||
export default class LetterTracingDraw extends BaseDrawService {
|
export default class LetterTracingDraw extends BaseDrawService {
|
||||||
constructor(
|
|
||||||
canvas: Canvas,
|
|
||||||
ctx: RenderingContext,
|
|
||||||
options?: Record<string, unknown>,
|
|
||||||
) {
|
|
||||||
super(canvas, ctx, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
async draw(data: LetterTracingData) {
|
async draw(data: LetterTracingData) {
|
||||||
this.prepareDraw();
|
const DrawClass = {
|
||||||
await this.drawHeaderAndDivider();
|
'picture-tracing': PictureTracingDraw,
|
||||||
|
'case-pairing': CasePairingDraw,
|
||||||
|
'column-compare': ColumnCompareDraw,
|
||||||
|
overview: OverviewDraw,
|
||||||
|
}[data.mode];
|
||||||
|
|
||||||
switch (data.mode) {
|
const delegate = new DrawClass(this.canvas, this.ctx, this.options);
|
||||||
case 'picture-tracing':
|
await delegate.draw(data as any);
|
||||||
this.drawPictureTracing(data);
|
|
||||||
this.drawPrintFooter();
|
|
||||||
break;
|
|
||||||
case 'case-pairing':
|
|
||||||
this.drawCasePairing(data.leftVisualRows, data.rightVisualRows);
|
|
||||||
this.drawPrintFooter();
|
|
||||||
break;
|
|
||||||
case 'column-compare':
|
|
||||||
this.drawColumnCompare(data.leftRows, data.rightRows);
|
|
||||||
this.drawPrintFooter();
|
|
||||||
break;
|
|
||||||
case 'overview':
|
|
||||||
this.drawOverview(data.upperRows, data.lowerRows);
|
|
||||||
this.drawPrintFooter();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 四线三格:y 顶 → y+h 底,中间两条虚线
|
|
||||||
*/
|
|
||||||
private drawFourLines(x: number, y: number, w: number, h: number) {
|
|
||||||
const ctx = this.ctx;
|
|
||||||
const yTop = y;
|
|
||||||
const yMid1 = y + h * 0.28;
|
|
||||||
const yMid2 = y + h * 0.58;
|
|
||||||
const yBot = y + h;
|
|
||||||
|
|
||||||
const strokeSolid = () => {
|
|
||||||
ctx.strokeStyle = LINE_INK;
|
|
||||||
ctx.lineWidth = 1;
|
|
||||||
ctx.setLineDash([]);
|
|
||||||
};
|
|
||||||
const strokeDash = () => {
|
|
||||||
ctx.strokeStyle = LINE_INK;
|
|
||||||
ctx.lineWidth = 1;
|
|
||||||
ctx.setLineDash([5, 5]);
|
|
||||||
};
|
|
||||||
|
|
||||||
strokeSolid();
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(x, yTop);
|
|
||||||
ctx.lineTo(x + w, yTop);
|
|
||||||
ctx.stroke();
|
|
||||||
|
|
||||||
strokeDash();
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(x, yMid1);
|
|
||||||
ctx.lineTo(x + w, yMid1);
|
|
||||||
ctx.stroke();
|
|
||||||
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(x, yMid2);
|
|
||||||
ctx.lineTo(x + w, yMid2);
|
|
||||||
ctx.stroke();
|
|
||||||
|
|
||||||
strokeSolid();
|
|
||||||
ctx.setLineDash([]);
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(x, yBot);
|
|
||||||
ctx.lineTo(x + w, yBot);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
private drawCharInCell(
|
|
||||||
ch: string,
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
w: number,
|
|
||||||
h: number,
|
|
||||||
opacity: number,
|
|
||||||
bold: boolean,
|
|
||||||
pairChar?: string,
|
|
||||||
) {
|
|
||||||
const ctx = this.ctx;
|
|
||||||
if (opacity <= 0) return;
|
|
||||||
ctx.save();
|
|
||||||
ctx.globalAlpha = opacity;
|
|
||||||
ctx.fillStyle = bold ? '#1a1a1a' : '#4a4a4a';
|
|
||||||
ctx.textAlign = 'center';
|
|
||||||
ctx.textBaseline = 'alphabetic';
|
|
||||||
const baselineY = y + h * 0.72;
|
|
||||||
|
|
||||||
if (pairChar) {
|
|
||||||
const fs = Math.min(h * 0.5, w * 0.22);
|
|
||||||
ctx.font = `${bold ? 'bold ' : ''}${fs}px sans-serif`;
|
|
||||||
const gap = fs * 0.2;
|
|
||||||
const totalW = fs * 2 + gap;
|
|
||||||
const x0 = x + w / 2 - totalW / 2;
|
|
||||||
ctx.fillText(ch, x0 + fs * 0.5, baselineY);
|
|
||||||
ctx.fillText(pairChar, x0 + fs * 1.5 + gap, baselineY);
|
|
||||||
} else {
|
|
||||||
const fontSize = Math.min(h * 0.72, w * 0.85);
|
|
||||||
ctx.font = `${bold ? 'bold ' : ''}${fontSize}px sans-serif`;
|
|
||||||
ctx.fillText(ch, x + w / 2, baselineY);
|
|
||||||
}
|
|
||||||
ctx.restore();
|
|
||||||
}
|
|
||||||
|
|
||||||
private drawTracingRow(
|
|
||||||
row: TracingRow,
|
|
||||||
startX: number,
|
|
||||||
y: number,
|
|
||||||
rowH: number,
|
|
||||||
totalWidth: number,
|
|
||||||
) {
|
|
||||||
const gap = 4;
|
|
||||||
const n = row.cells.length;
|
|
||||||
const cellW = n > 0 ? (totalWidth - gap * (n - 1)) / n : totalWidth;
|
|
||||||
|
|
||||||
for (let i = 0; i < n; i++) {
|
|
||||||
const cx = startX + i * (cellW + gap);
|
|
||||||
this.drawFourLines(cx, y, cellW, rowH);
|
|
||||||
const cell = row.cells[i];
|
|
||||||
this.drawCharInCell(
|
|
||||||
cell.char,
|
|
||||||
cx,
|
|
||||||
y,
|
|
||||||
cellW,
|
|
||||||
rowH,
|
|
||||||
cell.opacity,
|
|
||||||
cell.isGuide,
|
|
||||||
cell.pairChar,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 看图描红:上左超大字母+例句,上右「配图」大 emoji;下 6 行四线三格
|
|
||||||
*/
|
|
||||||
private drawPictureTracing(
|
|
||||||
data: Extract<LetterTracingData, { mode: 'picture-tracing' }>,
|
|
||||||
) {
|
|
||||||
const ctx = this.ctx;
|
|
||||||
const { teaching, rows } = data;
|
|
||||||
const cw = this.canvasWidth;
|
|
||||||
let y = this.currentY + 8;
|
|
||||||
const teachH = Math.min(240, (this.canvasHeight - y) * 0.38);
|
|
||||||
const innerW = cw - M * 2;
|
|
||||||
const splitX = M + innerW * 0.52;
|
|
||||||
|
|
||||||
ctx.fillStyle = '#f8fafc';
|
|
||||||
ctx.fillRect(M, y, innerW, teachH);
|
|
||||||
|
|
||||||
// 左侧:大小写 + 例句(短句在字母下方)
|
|
||||||
const leftPad = M + 20;
|
|
||||||
ctx.textAlign = 'left';
|
|
||||||
ctx.textBaseline = 'top';
|
|
||||||
ctx.fillStyle = '#111';
|
|
||||||
const bigFs = Math.min(56, teachH * 0.26);
|
|
||||||
ctx.font = `bold ${bigFs}px sans-serif`;
|
|
||||||
ctx.fillText(
|
|
||||||
`${teaching.upper} ${teaching.lower}`,
|
|
||||||
leftPad,
|
|
||||||
y + teachH * 0.12,
|
|
||||||
);
|
|
||||||
ctx.font = `${Math.min(22, teachH * 0.1)}px sans-serif`;
|
|
||||||
ctx.fillStyle = '#333';
|
|
||||||
ctx.fillText(teaching.sentence, leftPad, y + teachH * 0.48);
|
|
||||||
|
|
||||||
// 右侧:配图区(以 emoji 作为插图占位)
|
|
||||||
ctx.font = `${Math.min(120, teachH * 0.55)}px sans-serif`;
|
|
||||||
ctx.textAlign = 'center';
|
|
||||||
ctx.textBaseline = 'middle';
|
|
||||||
ctx.fillText(
|
|
||||||
teaching.emoji,
|
|
||||||
splitX + (cw - M - splitX) / 2,
|
|
||||||
y + teachH / 2,
|
|
||||||
);
|
|
||||||
|
|
||||||
y += teachH + 12;
|
|
||||||
this.drawDashedDivider(y, M, '#ccc');
|
|
||||||
y += 14;
|
|
||||||
|
|
||||||
const rest = this.canvasHeight - y - M;
|
|
||||||
const rowH = Math.max(34, (rest - 10) / rows.length);
|
|
||||||
const rowWidth = innerW;
|
|
||||||
for (let r = 0; r < rows.length; r++) {
|
|
||||||
this.drawTracingRow(
|
|
||||||
rows[r],
|
|
||||||
M,
|
|
||||||
y + r * (rowH + 8),
|
|
||||||
rowH,
|
|
||||||
rowWidth,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 大小写配对:左右两栏,每行最多 4 组子行并排
|
|
||||||
*/
|
|
||||||
private drawCasePairing(
|
|
||||||
leftVisualRows: TracingRow[][],
|
|
||||||
rightVisualRows: TracingRow[][],
|
|
||||||
) {
|
|
||||||
const y0 = this.currentY + 6;
|
|
||||||
const bottom = this.canvasHeight - M;
|
|
||||||
const gapCol = 18;
|
|
||||||
const colW = (this.canvasWidth - M * 2 - gapCol) / 2;
|
|
||||||
const maxVisual = Math.max(
|
|
||||||
leftVisualRows.length,
|
|
||||||
rightVisualRows.length,
|
|
||||||
);
|
|
||||||
const rowSlotH = Math.max(40, (bottom - y0) / maxVisual - 6);
|
|
||||||
|
|
||||||
const drawSide = (
|
|
||||||
blocks: TracingRow[][],
|
|
||||||
startX: number,
|
|
||||||
width: number,
|
|
||||||
) => {
|
|
||||||
let y = y0;
|
|
||||||
for (const visualRow of blocks) {
|
|
||||||
const nSeg = visualRow.length;
|
|
||||||
const segGap = 6;
|
|
||||||
const segW = (width - (nSeg - 1) * segGap) / nSeg;
|
|
||||||
for (let s = 0; s < nSeg; s++) {
|
|
||||||
this.drawTracingRow(
|
|
||||||
visualRow[s],
|
|
||||||
startX + s * (segW + segGap),
|
|
||||||
y,
|
|
||||||
rowSlotH,
|
|
||||||
segW,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
y += rowSlotH + 8;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
drawSide(leftVisualRows, M, colW);
|
|
||||||
drawSide(rightVisualRows, M + colW + gapCol, colW);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 分栏对照:左大写、右小写,逐行对齐 */
|
|
||||||
private drawColumnCompare(leftRows: TracingRow[], rightRows: TracingRow[]) {
|
|
||||||
const y0 = this.currentY + 6;
|
|
||||||
const bottom = this.canvasHeight - M;
|
|
||||||
const maxR = Math.max(leftRows.length, rightRows.length);
|
|
||||||
const rowH = Math.max(36, (bottom - y0) / maxR - 6);
|
|
||||||
const gapCol = 16;
|
|
||||||
const colW = (this.canvasWidth - M * 2 - gapCol) / 2;
|
|
||||||
|
|
||||||
for (let i = 0; i < leftRows.length; i++) {
|
|
||||||
const y = y0 + i * (rowH + 6);
|
|
||||||
this.drawTracingRow(leftRows[i], M, y, rowH, colW);
|
|
||||||
}
|
|
||||||
const xR = M + colW + gapCol;
|
|
||||||
for (let i = 0; i < rightRows.length; i++) {
|
|
||||||
const y = y0 + i * (rowH + 6);
|
|
||||||
this.drawTracingRow(rightRows[i], xR, y, rowH, colW);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 字母总览:Uppercase Letters / lowercase Letters,各行 9+9+8,四线三格
|
|
||||||
*/
|
|
||||||
private drawOverview(upperRows: string[][], lowerRows: string[][]) {
|
|
||||||
const ctx = this.ctx;
|
|
||||||
let y = this.currentY + 6;
|
|
||||||
const innerW = this.canvasWidth - M * 2;
|
|
||||||
|
|
||||||
const title = (t: string) => {
|
|
||||||
ctx.fillStyle = '#111';
|
|
||||||
ctx.font = 'bold 17px sans-serif';
|
|
||||||
ctx.textAlign = 'left';
|
|
||||||
ctx.textBaseline = 'top';
|
|
||||||
ctx.fillText(t, M, y);
|
|
||||||
y += 24;
|
|
||||||
};
|
|
||||||
|
|
||||||
const drawBand = (rows: string[][]) => {
|
|
||||||
for (const line of rows) {
|
|
||||||
const n = line.length;
|
|
||||||
const gap = 4;
|
|
||||||
const cellW = (innerW - (n - 1) * gap) / n;
|
|
||||||
const cellH = 30;
|
|
||||||
for (let i = 0; i < n; i++) {
|
|
||||||
const cx = M + i * (cellW + gap);
|
|
||||||
this.drawFourLines(cx, y, cellW, cellH);
|
|
||||||
this.drawCharInCell(
|
|
||||||
line[i],
|
|
||||||
cx,
|
|
||||||
y,
|
|
||||||
cellW,
|
|
||||||
cellH,
|
|
||||||
0.9,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
y += cellH + 8;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
title('Uppercase Letters');
|
|
||||||
drawBand(upperRows);
|
|
||||||
y += 6;
|
|
||||||
this.drawDashedDivider(y, M, '#bbb');
|
|
||||||
y += 12;
|
|
||||||
title('lowercase Letters');
|
|
||||||
drawBand(lowerRows);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { BaseDrawService } from '../../../core/draw/baseDraw';
|
||||||
|
import type { LetterTracingData } from '../generators/letter-tracing-generator';
|
||||||
|
import { drawFourLineGrid } from './drawTools';
|
||||||
|
|
||||||
|
const LETTER_FACE =
|
||||||
|
'Helvetica, Arial, "Segoe UI", Roboto, sans-serif';
|
||||||
|
|
||||||
|
type OverviewData = Extract<LetterTracingData, { mode: 'overview' }>;
|
||||||
|
|
||||||
|
export default class OverviewDraw extends BaseDrawService {
|
||||||
|
async draw(data: OverviewData) {
|
||||||
|
this.prepareDraw();
|
||||||
|
await this.drawHeaderAndDivider();
|
||||||
|
this.drawContent(data.upperRows, data.lowerRows);
|
||||||
|
this.drawPrintFooter();
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawContent(upperRows: string[][], lowerRows: string[][]) {
|
||||||
|
const ctx = this.ctx;
|
||||||
|
const M = 24;
|
||||||
|
let y = this.currentY + 6;
|
||||||
|
const innerW = this.canvasWidth - M * 2;
|
||||||
|
|
||||||
|
const drawTitle = (t: string) => {
|
||||||
|
ctx.fillStyle = '#111';
|
||||||
|
ctx.font = 'bold 17px sans-serif';
|
||||||
|
ctx.textAlign = 'left';
|
||||||
|
ctx.textBaseline = 'top';
|
||||||
|
ctx.fillText(t, M, y);
|
||||||
|
y += 24;
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawBand = (rows: string[][]) => {
|
||||||
|
for (const line of rows) {
|
||||||
|
const n = line.length;
|
||||||
|
const gap = 4;
|
||||||
|
const cellW = (innerW - (n - 1) * gap) / n;
|
||||||
|
const cellH = 30;
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const cx = M + i * (cellW + gap);
|
||||||
|
drawFourLineGrid(ctx, cx, y, cellW, cellH);
|
||||||
|
this.drawCharInCell(
|
||||||
|
line[i],
|
||||||
|
cx,
|
||||||
|
y,
|
||||||
|
cellW,
|
||||||
|
cellH,
|
||||||
|
0.9,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
y += cellH + 8;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
drawTitle('Uppercase Letters');
|
||||||
|
drawBand(upperRows);
|
||||||
|
y += 6;
|
||||||
|
this.drawDashedDivider(y, M, '#bbb');
|
||||||
|
y += 12;
|
||||||
|
drawTitle('lowercase Letters');
|
||||||
|
drawBand(lowerRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawCharInCell(
|
||||||
|
ch: string,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
w: number,
|
||||||
|
h: number,
|
||||||
|
opacity: number,
|
||||||
|
bold: boolean,
|
||||||
|
) {
|
||||||
|
const ctx = this.ctx;
|
||||||
|
if (opacity <= 0) return;
|
||||||
|
ctx.save();
|
||||||
|
ctx.globalAlpha = opacity;
|
||||||
|
ctx.fillStyle = bold ? '#1a1a1a' : '#4a4a4a';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'alphabetic';
|
||||||
|
const baselineY = y + h * 0.72;
|
||||||
|
const fontSize = Math.min(h * 0.72, w * 0.85);
|
||||||
|
ctx.font = `${bold ? 'bold ' : ''}${fontSize}px ${LETTER_FACE}`;
|
||||||
|
ctx.fillText(ch, x + w / 2, baselineY);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,386 @@
|
|||||||
|
import { BaseDrawService } from '../../../core/draw/baseDraw';
|
||||||
|
import { getImage } from '../../../utils/index';
|
||||||
|
import type {
|
||||||
|
LetterTracingData,
|
||||||
|
HighlightGrid,
|
||||||
|
} from '../generators/letter-tracing-generator';
|
||||||
|
import { drawFourLineGrid } from './drawTools';
|
||||||
|
|
||||||
|
const LINE_INK_ALPHA = 'rgba(50, 46, 37, 1)';
|
||||||
|
const ACCENT_RED = 'rgba(252, 46, 0, 1)';
|
||||||
|
//'Helvetica, Arial, "Segoe UI", Roboto, sans-serif';
|
||||||
|
const LETTER_FACE = 'Roboto, sans-serif';
|
||||||
|
const TITLE_EMOJI_FONT =
|
||||||
|
"16px 'Apple Color Emoji','Segoe UI Emoji','Noto Color Emoji',sans-serif";
|
||||||
|
const TITLE_TEXT_FONT = '14px sans-serif';
|
||||||
|
const TITLE_ICON_GAP = 4;
|
||||||
|
|
||||||
|
type PictureData = Extract<LetterTracingData, { mode: 'picture-tracing' }>;
|
||||||
|
|
||||||
|
export default class PictureTracingDraw extends BaseDrawService {
|
||||||
|
async draw(data: PictureData) {
|
||||||
|
this.prepareDraw();
|
||||||
|
await this.drawHeaderAndDivider();
|
||||||
|
await this.drawContent(data);
|
||||||
|
this.drawPrintFooter();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async drawContent(data: PictureData) {
|
||||||
|
const { teaching, highlightGrid, traceImgPath } = data;
|
||||||
|
const ctx = this.ctx;
|
||||||
|
|
||||||
|
// ── 上区:left:78 top:120 w:451 h:180 ──
|
||||||
|
const TOP_X = 78;
|
||||||
|
const TOP_Y = 120;
|
||||||
|
|
||||||
|
const letterImgX = TOP_X + 15;
|
||||||
|
const letterImgY = TOP_Y + 9;
|
||||||
|
const letterImgW = 180;
|
||||||
|
const letterImgH = 103.5;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const letterImg = (await getImage(
|
||||||
|
this.canvas,
|
||||||
|
teaching.letterImgPath,
|
||||||
|
)) as WechatMiniprogram.Image;
|
||||||
|
const aspect = (letterImg.width || 1) / (letterImg.height || 1);
|
||||||
|
let dw = letterImgW;
|
||||||
|
let dh = dw / aspect;
|
||||||
|
if (dh > letterImgH) {
|
||||||
|
dh = letterImgH;
|
||||||
|
dw = dh * aspect;
|
||||||
|
}
|
||||||
|
ctx.drawImage(
|
||||||
|
letterImg,
|
||||||
|
letterImgX + (letterImgW - dw) / 2,
|
||||||
|
letterImgY + (letterImgH - dh) / 2,
|
||||||
|
dw,
|
||||||
|
dh,
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
ctx.font = `bold 60px ${LETTER_FACE}`;
|
||||||
|
ctx.fillStyle = '#111';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.fillText(
|
||||||
|
`${teaching.upper} ${teaching.lower}`,
|
||||||
|
letterImgX + letterImgW / 2,
|
||||||
|
letterImgY + letterImgH / 2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.drawSentence(
|
||||||
|
teaching.sentence,
|
||||||
|
teaching.upper,
|
||||||
|
TOP_X,
|
||||||
|
TOP_Y + 9 + 120,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 右侧配图
|
||||||
|
const appleX = TOP_X + 271;
|
||||||
|
const appleY = TOP_Y;
|
||||||
|
const appleW = 180;
|
||||||
|
const appleH = 180;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const wordImg = (await getImage(
|
||||||
|
this.canvas,
|
||||||
|
teaching.wordImgPath,
|
||||||
|
)) as WechatMiniprogram.Image;
|
||||||
|
const aspect = (wordImg.width || 1) / (wordImg.height || 1);
|
||||||
|
let dw = appleW;
|
||||||
|
let dh = dw / aspect;
|
||||||
|
if (dh > appleH) {
|
||||||
|
dh = appleH;
|
||||||
|
dw = dh * aspect;
|
||||||
|
}
|
||||||
|
ctx.drawImage(
|
||||||
|
wordImg,
|
||||||
|
appleX + (appleW - dw) / 2,
|
||||||
|
appleY + (appleH - dh) / 2,
|
||||||
|
dw,
|
||||||
|
dh,
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
ctx.font = `${Math.min(80, appleH * 0.5)}px sans-serif`;
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.fillText(
|
||||||
|
teaching.emoji,
|
||||||
|
appleX + appleW / 2,
|
||||||
|
appleY + appleH / 2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 中区 ──
|
||||||
|
const HL_X = 26,
|
||||||
|
HL_Y = 320,
|
||||||
|
HL_W = 272,
|
||||||
|
HL_H = 181;
|
||||||
|
const TR_X = 298,
|
||||||
|
TR_Y = 320,
|
||||||
|
TR_W = 272,
|
||||||
|
TR_H = 181;
|
||||||
|
|
||||||
|
ctx.strokeStyle = LINE_INK_ALPHA;
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
ctx.strokeRect(HL_X, HL_Y, HL_W, HL_H);
|
||||||
|
ctx.strokeRect(TR_X, TR_Y, TR_W, TR_H);
|
||||||
|
|
||||||
|
this.drawHighlightSection(highlightGrid, HL_X, HL_Y);
|
||||||
|
await this.drawTraceItSection(
|
||||||
|
traceImgPath,
|
||||||
|
teaching.upper,
|
||||||
|
teaching.lower,
|
||||||
|
TR_X,
|
||||||
|
TR_Y,
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── 下区:4 行四线三格 + 字母临摹(倒置直角三角形)──
|
||||||
|
// 每行:left:24 w:547 h:43,top 分别 524.5 / 590.5 / 656.5 / 722.5
|
||||||
|
const lineX = 24;
|
||||||
|
const lineW = 547;
|
||||||
|
const lineH = 43;
|
||||||
|
const lineTops = [524.5, 590.5, 656.5, 722.5];
|
||||||
|
|
||||||
|
// 8 → 6 → 3 → 1 组(每组为 Upper+Lower),左对齐,形成倒置直角三角形
|
||||||
|
const groupsPerLine = [8, 6, 3, 1];
|
||||||
|
const slotW = lineW / groupsPerLine[0]; // 以第一行的 8 组为基准
|
||||||
|
|
||||||
|
for (let r = 0; r < lineTops.length; r++) {
|
||||||
|
const y = lineTops[r];
|
||||||
|
drawFourLineGrid(ctx, lineX, y, lineW, lineH);
|
||||||
|
|
||||||
|
const n = groupsPerLine[r] ?? 0;
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
// 第一组不透明,后面逐次更透明
|
||||||
|
const alpha = n <= 1 ? 1 : 1 - (i / (n - 1)) * 0.7; // 1 → 0.3
|
||||||
|
this.drawUpperLowerPairInFourLines(
|
||||||
|
teaching.upper,
|
||||||
|
teaching.lower,
|
||||||
|
lineX + i * slotW,
|
||||||
|
y,
|
||||||
|
slotW,
|
||||||
|
lineH,
|
||||||
|
alpha,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在四线三格中绘制 Upper+Lower(遵循书写基线规范)
|
||||||
|
*
|
||||||
|
* 四线三格的 4 条线(从上到下):
|
||||||
|
* Top = y ── 顶线
|
||||||
|
* Midline = y + h/3 ── 中线(虚线)
|
||||||
|
* Baseline = y + 2h/3 ── 基线(虚线)
|
||||||
|
* Bottom = y + h ── 底线
|
||||||
|
*
|
||||||
|
* 大写字母:高度占满 Top → Baseline(即第一格 + 第二格),字号 ≈ h * 2/3
|
||||||
|
* 小写字母:高度占满 Midline → Baseline(即第二格),字号 ≈ h * 1/3
|
||||||
|
* 两个字母共享 Baseline,间距远小于组间距
|
||||||
|
*/
|
||||||
|
private drawUpperLowerPairInFourLines(
|
||||||
|
upper: string,
|
||||||
|
lower: string,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
w: number,
|
||||||
|
h: number,
|
||||||
|
alpha: number,
|
||||||
|
) {
|
||||||
|
const ctx = this.ctx;
|
||||||
|
ctx.save();
|
||||||
|
ctx.globalAlpha = Math.max(0, Math.min(1, alpha));
|
||||||
|
ctx.fillStyle = 'rgba(26, 26, 26, 1)';
|
||||||
|
ctx.textBaseline = 'alphabetic';
|
||||||
|
|
||||||
|
const baselineY = y + (h * 2) / 3;
|
||||||
|
|
||||||
|
// 大写占满 Top→Baseline(2 格高),小写占满 Midline→Baseline(1 格高)
|
||||||
|
const upperFs = h * (2 / 3);
|
||||||
|
const lowerFs = h * (1 / 3);
|
||||||
|
|
||||||
|
// 字母间距:固定 3px,远小于组间距(slotW ≈ 68)
|
||||||
|
const pairGap = 3;
|
||||||
|
|
||||||
|
ctx.font = `${upperFs}px ${LETTER_FACE}`;
|
||||||
|
const upperW = ctx.measureText(upper).width;
|
||||||
|
|
||||||
|
ctx.font = `${lowerFs}px ${LETTER_FACE}`;
|
||||||
|
const lowerW = ctx.measureText(lower).width;
|
||||||
|
|
||||||
|
const totalPairW = upperW + pairGap + lowerW;
|
||||||
|
const pairStartX = x + (w - totalPairW) / 2;
|
||||||
|
|
||||||
|
ctx.font = `${upperFs}px ${LETTER_FACE}`;
|
||||||
|
ctx.textAlign = 'left';
|
||||||
|
ctx.fillText(upper, pairStartX, baselineY);
|
||||||
|
|
||||||
|
ctx.font = `${lowerFs}px ${LETTER_FACE}`;
|
||||||
|
ctx.textAlign = 'left';
|
||||||
|
ctx.fillText(lower, pairStartX + upperW + pairGap, baselineY);
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawSentence(
|
||||||
|
sentence: string,
|
||||||
|
letter: string,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
) {
|
||||||
|
const ctx = this.ctx;
|
||||||
|
ctx.font = `36px ${LETTER_FACE}`;
|
||||||
|
ctx.textAlign = 'left';
|
||||||
|
ctx.textBaseline = 'top';
|
||||||
|
|
||||||
|
let cx = x;
|
||||||
|
for (let i = 0; i < sentence.length; i++) {
|
||||||
|
const ch = sentence[i];
|
||||||
|
ctx.fillStyle =
|
||||||
|
ch.toUpperCase() === letter ? ACCENT_RED : 'rgba(0, 0, 0, 1)';
|
||||||
|
ctx.fillText(ch, cx, y);
|
||||||
|
cx += ctx.measureText(ch).width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawHighlightSection(grid: HighlightGrid, sx: number, sy: number) {
|
||||||
|
const ctx = this.ctx;
|
||||||
|
const titleX = sx + 16;
|
||||||
|
const titleY = sy + 12;
|
||||||
|
|
||||||
|
ctx.fillStyle = '#000';
|
||||||
|
ctx.textAlign = 'left';
|
||||||
|
ctx.textBaseline = 'top';
|
||||||
|
|
||||||
|
const hlIcon = '\u{1F50D}';
|
||||||
|
ctx.font = TITLE_EMOJI_FONT;
|
||||||
|
ctx.fillText(hlIcon, titleX, titleY + 1);
|
||||||
|
const hlIconW = ctx.measureText(hlIcon).width;
|
||||||
|
|
||||||
|
ctx.font = TITLE_TEXT_FONT;
|
||||||
|
ctx.fillText(
|
||||||
|
'Highlight it !',
|
||||||
|
titleX + hlIconW + TITLE_ICON_GAP,
|
||||||
|
titleY,
|
||||||
|
);
|
||||||
|
|
||||||
|
const gridX = sx + 16;
|
||||||
|
const gridY = sy + 45;
|
||||||
|
const cellSize = 40;
|
||||||
|
const cols = grid.rows[0]?.length ?? 6;
|
||||||
|
const rows = grid.rows.length;
|
||||||
|
|
||||||
|
for (let r = 0; r < rows; r++) {
|
||||||
|
for (let c = 0; c < cols; c++) {
|
||||||
|
const cx = gridX + c * cellSize;
|
||||||
|
const cy = gridY + r * cellSize;
|
||||||
|
ctx.strokeStyle = 'rgba(0, 0, 0, 1)';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
ctx.strokeRect(cx, cy, cellSize, cellSize);
|
||||||
|
|
||||||
|
ctx.font = `18px ${LETTER_FACE}`;
|
||||||
|
ctx.fillStyle = '#000';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.fillText(
|
||||||
|
grid.rows[r][c],
|
||||||
|
cx + cellSize / 2,
|
||||||
|
cy + cellSize / 2 + 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async drawTraceItSection(
|
||||||
|
traceImgPath: string,
|
||||||
|
upper: string,
|
||||||
|
lower: string,
|
||||||
|
sx: number,
|
||||||
|
sy: number,
|
||||||
|
) {
|
||||||
|
const ctx = this.ctx;
|
||||||
|
const titleX = sx + 25;
|
||||||
|
const titleY = sy + 12;
|
||||||
|
|
||||||
|
ctx.fillStyle = '#000';
|
||||||
|
ctx.textAlign = 'left';
|
||||||
|
ctx.textBaseline = 'top';
|
||||||
|
|
||||||
|
const traceIcon = '\u{1F58A}\u{FE0F}';
|
||||||
|
ctx.font = TITLE_EMOJI_FONT;
|
||||||
|
ctx.fillText(traceIcon, titleX, titleY);
|
||||||
|
const traceIconW = ctx.measureText(traceIcon).width;
|
||||||
|
|
||||||
|
ctx.font = TITLE_TEXT_FONT;
|
||||||
|
ctx.fillText(
|
||||||
|
'Trace It !',
|
||||||
|
titleX + traceIconW + TITLE_ICON_GAP,
|
||||||
|
titleY,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 四线三格(设计稿绝对坐标)
|
||||||
|
const lx = 316,
|
||||||
|
lw = 236;
|
||||||
|
const lines = [364.5, 404.5, 444.5, 484.5];
|
||||||
|
|
||||||
|
ctx.strokeStyle = LINE_INK_ALPHA;
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(lx, lines[0]);
|
||||||
|
ctx.lineTo(lx + lw, lines[0]);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.setLineDash([4, 4]);
|
||||||
|
for (const ly of [lines[1], lines[2]]) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(lx, ly);
|
||||||
|
ctx.lineTo(lx + lw, ly);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(lx, lines[3]);
|
||||||
|
ctx.lineTo(lx + lw, lines[3]);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// 笔顺图
|
||||||
|
const imgX = 337,
|
||||||
|
imgY = 365,
|
||||||
|
imgW = 159,
|
||||||
|
imgH = 80;
|
||||||
|
try {
|
||||||
|
const img = (await getImage(
|
||||||
|
this.canvas,
|
||||||
|
traceImgPath,
|
||||||
|
)) as WechatMiniprogram.Image;
|
||||||
|
const aspect = (img.width || 1) / (img.height || 1);
|
||||||
|
let dw = imgW;
|
||||||
|
let dh = dw / aspect;
|
||||||
|
if (dh > imgH) {
|
||||||
|
dh = imgH;
|
||||||
|
dw = dh * aspect;
|
||||||
|
}
|
||||||
|
ctx.drawImage(
|
||||||
|
img,
|
||||||
|
imgX + (imgW - dw) / 2,
|
||||||
|
imgY + (imgH - dh) / 2,
|
||||||
|
dw,
|
||||||
|
dh,
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
const fs = Math.min(imgH * 0.6, imgW * 0.18);
|
||||||
|
ctx.font = `bold ${fs}px ${LETTER_FACE}`;
|
||||||
|
ctx.fillStyle = '#222';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.fillText(`${upper} ${lower}`, imgX + imgW / 2, imgY + imgH / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ALPHABET_BY_LETTER, LETTERS_UPPER } from '../../../core/data/alphabet';
|
import { ALPHABET_BY_LETTER, LETTERS_UPPER } from '../data/alphabet';
|
||||||
|
|
||||||
/** 字母描红的布局模式(与产品文档四类练习一致) */
|
/** 字母描红的布局模式(与产品文档四类练习一致) */
|
||||||
export type LetterTracingMode =
|
export type LetterTracingMode =
|
||||||
@@ -43,6 +43,12 @@ export interface TracingRow {
|
|||||||
cells: TracingCell[];
|
cells: TracingCell[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Highlight It 格子数据(3×6 网格,混入目标字母) */
|
||||||
|
export interface HighlightGrid {
|
||||||
|
rows: string[][];
|
||||||
|
targetLetter: string;
|
||||||
|
}
|
||||||
|
|
||||||
/** 看图描红教学区 */
|
/** 看图描红教学区 */
|
||||||
export interface TeachingBlock {
|
export interface TeachingBlock {
|
||||||
upper: string;
|
upper: string;
|
||||||
@@ -50,13 +56,21 @@ export interface TeachingBlock {
|
|||||||
word: string;
|
word: string;
|
||||||
sentence: string;
|
sentence: string;
|
||||||
emoji: string;
|
emoji: string;
|
||||||
|
/** 字母笔顺图路径(/englishPages/shared/assets/letter/X.png) */
|
||||||
|
letterImgPath: string;
|
||||||
|
/** 配图路径(/englishPages/shared/assets/letterImgs/Word.png) */
|
||||||
|
wordImgPath: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LetterTracingData =
|
export type LetterTracingData =
|
||||||
| {
|
| {
|
||||||
mode: 'picture-tracing';
|
mode: 'picture-tracing';
|
||||||
teaching: TeachingBlock;
|
teaching: TeachingBlock;
|
||||||
rows: TracingRow[];
|
highlightGrid: HighlightGrid;
|
||||||
|
/** Trace It 区域使用的字母笔顺图路径(同 teaching.letterImgPath) */
|
||||||
|
traceImgPath: string;
|
||||||
|
/** 下部 4 行四线三格(无字符填充,纯空行) */
|
||||||
|
tracingLineCount: number;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
mode: 'case-pairing';
|
mode: 'case-pairing';
|
||||||
@@ -80,6 +94,33 @@ export function normalizeLetter(c: string): string {
|
|||||||
return u.length ? u[0] : 'A';
|
return u.length ? u[0] : 'A';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 构建 Highlight It 的 3×6 随机字母网格,保证混入 2~5 个目标字母 */
|
||||||
|
function buildHighlightGrid(
|
||||||
|
target: string,
|
||||||
|
gridRows: number,
|
||||||
|
gridCols: number,
|
||||||
|
): HighlightGrid {
|
||||||
|
const total = gridRows * gridCols;
|
||||||
|
const targetCount = 2 + Math.floor(Math.random() * 4); // 2~5
|
||||||
|
const others = LETTERS_UPPER.filter((l) => l !== target);
|
||||||
|
|
||||||
|
const chars: string[] = [];
|
||||||
|
for (let i = 0; i < targetCount; i++) chars.push(target);
|
||||||
|
for (let i = targetCount; i < total; i++) {
|
||||||
|
chars.push(others[Math.floor(Math.random() * others.length)]);
|
||||||
|
}
|
||||||
|
for (let i = chars.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[chars[i], chars[j]] = [chars[j], chars[i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows: string[][] = [];
|
||||||
|
for (let r = 0; r < gridRows; r++) {
|
||||||
|
rows.push(chars.slice(r * gridCols, (r + 1) * gridCols));
|
||||||
|
}
|
||||||
|
return { rows, targetLetter: target };
|
||||||
|
}
|
||||||
|
|
||||||
function buildCells(
|
function buildCells(
|
||||||
char: string,
|
char: string,
|
||||||
count: number,
|
count: number,
|
||||||
@@ -165,22 +206,32 @@ export function generateLetterTracing(
|
|||||||
const meta = ALPHABET_BY_LETTER[key] ?? ALPHABET_BY_LETTER.A;
|
const meta = ALPHABET_BY_LETTER[key] ?? ALPHABET_BY_LETTER.A;
|
||||||
const upper = key;
|
const upper = key;
|
||||||
const lower = key.toLowerCase();
|
const lower = key.toLowerCase();
|
||||||
|
|
||||||
|
const hasLetterAsset = key === 'A' || key === 'B';
|
||||||
|
const letterFile = hasLetterAsset ? key : 'A';
|
||||||
|
const letterImgPath = `/englishPages/shared/assets/letter/${letterFile}.png`;
|
||||||
|
|
||||||
|
const wordImgPath = `/englishPages/shared/assets/letterImgs/Apple.png`;
|
||||||
|
|
||||||
const teaching: TeachingBlock = {
|
const teaching: TeachingBlock = {
|
||||||
upper,
|
upper,
|
||||||
lower,
|
lower,
|
||||||
word: meta.word,
|
word: meta.word,
|
||||||
sentence: `${upper} is for ${meta.word}`,
|
sentence: `${upper} is for ${meta.word}`,
|
||||||
emoji: meta.emoji,
|
emoji: meta.emoji,
|
||||||
|
letterImgPath,
|
||||||
|
wordImgPath,
|
||||||
|
};
|
||||||
|
|
||||||
|
const highlightGrid = buildHighlightGrid(upper, 3, 6);
|
||||||
|
|
||||||
|
return {
|
||||||
|
mode: 'picture-tracing',
|
||||||
|
teaching,
|
||||||
|
highlightGrid,
|
||||||
|
traceImgPath: letterImgPath,
|
||||||
|
tracingLineCount: 8,
|
||||||
};
|
};
|
||||||
const rows: TracingRow[] = [
|
|
||||||
rowForChar(upper, repetitions, fadePattern),
|
|
||||||
rowForChar(upper, repetitions, fadePattern),
|
|
||||||
rowForChar(upper, repetitions, fadePattern),
|
|
||||||
rowForChar(lower, repetitions, fadePattern),
|
|
||||||
rowForChar(lower, repetitions, fadePattern),
|
|
||||||
rowForChar(lower, repetitions, fadePattern),
|
|
||||||
];
|
|
||||||
return { mode: 'picture-tracing', teaching, rows };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode === 'case-pairing') {
|
if (mode === 'case-pairing') {
|
||||||
@@ -189,12 +240,7 @@ export function generateLetterTracing(
|
|||||||
const pack = (letters: string[]) =>
|
const pack = (letters: string[]) =>
|
||||||
chunk(letters, 4).map((group) =>
|
chunk(letters, 4).map((group) =>
|
||||||
group.map((L) =>
|
group.map((L) =>
|
||||||
rowForChar(
|
rowForChar(L, repetitions, fadePattern, L.toLowerCase()),
|
||||||
L,
|
|
||||||
repetitions,
|
|
||||||
fadePattern,
|
|
||||||
L.toLowerCase(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
@@ -227,9 +273,7 @@ export function generateLetterTracing(
|
|||||||
return {
|
return {
|
||||||
mode: 'overview',
|
mode: 'overview',
|
||||||
upperRows: overviewRowsFrom([...LETTERS_UPPER]),
|
upperRows: overviewRowsFrom([...LETTERS_UPPER]),
|
||||||
lowerRows: overviewRowsFrom(
|
lowerRows: overviewRowsFrom(LETTERS_UPPER.map((L) => L.toLowerCase())),
|
||||||
LETTERS_UPPER.map((L) => L.toLowerCase()),
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<svg width="132" height="32" viewBox="0 0 132 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="132" height="32" rx="8" fill="white"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M27.19 6C28.47 6 29.69 6.51 30.59 7.41C31.49 8.31 32 9.53 32 10.81V21.19C32 23.85 29.85 26 27.19 26H16.81C15.53 26 14.32 25.49 13.41 24.59C12.51 23.69 12 22.47 12 21.19V10.81C12 9.53 12.51 8.31 13.41 7.41C14.32 6.51 15.53 6 16.81 6L27.19 6Z" fill="#333333"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.8972 9.33467C20.0472 9.06467 20.3972 8.95467 20.6772 9.10467C20.7772 9.15467 20.8472 9.23467 20.8972 9.33467L21.9972 11.3947L23.0972 9.33467C23.2372 9.06467 23.5872 8.95467 23.8572 9.10467C23.9572 9.15467 24.0372 9.23467 24.0972 9.33467L28.0373 16.7347C28.1673 16.9847 28.1673 17.2847 28.0373 17.5347L25.5372 22.2147C25.3872 22.4947 25.0972 22.6647 24.7872 22.6647H19.2172C18.8972 22.6647 18.6072 22.4947 18.4572 22.2147L15.9672 17.5347C15.8372 17.2847 15.8372 16.9847 15.9672 16.7347L19.8972 9.33467ZM24.5572 21.0247L26.5372 17.3347C26.5972 17.2047 26.5972 17.0547 26.5372 16.9347L23.8372 11.8847C23.7772 11.7447 23.5972 11.6947 23.4572 11.7747C23.4072 11.7947 23.3772 11.8347 23.3372 11.8847L22.8072 12.9047L21.9972 14.4247V14.4147L20.8872 16.4947V16.5047C20.8372 16.5847 20.8072 16.6547 20.7872 16.7447C20.7672 16.7847 20.7572 16.8247 20.7472 16.8647V16.8847C20.7372 16.9747 20.7272 17.0447 20.7272 17.1347C20.7272 17.8447 21.2872 18.4147 21.9972 18.4147C22.6972 18.4147 23.2672 17.8547 23.2872 17.1547V17.1347H23.2772C23.2772 16.9247 23.2272 16.7247 23.1272 16.5347L22.7872 15.9147L23.5972 14.3947L24.3472 15.8047C24.5772 16.1947 24.6972 16.6447 24.6972 17.1347C24.6972 18.6247 23.4872 19.8347 21.9972 19.8347C20.5072 19.8347 19.2972 18.6247 19.2972 17.1347C19.2972 16.8747 19.3372 16.6247 19.4072 16.3847L19.4372 16.3247V16.2947C19.4872 16.1447 19.5372 16.0047 19.6172 15.8647L21.1872 12.9047L20.6572 11.8847C20.5872 11.7447 20.4072 11.6947 20.2672 11.7747C20.2172 11.7947 20.1872 11.8347 20.1572 11.8847L17.4672 16.9347C17.3972 17.0547 17.3972 17.2047 17.4672 17.3347L19.4372 21.0247C19.5072 21.1647 19.6572 21.2447 19.8072 21.2447H24.1872C24.3372 21.2447 24.4872 21.1647 24.5572 21.0247Z" fill="white"/>
|
||||||
|
<path d="M38.534 13.916V15.54H41.474V13.916H38.534ZM41.474 12.726V11.186H38.534V12.726H41.474ZM37.12 22.05L36.658 20.776C37.036 20.636 37.232 20.412 37.232 20.09V9.996H42.706V16.772H38.534V20.342C39.598 20.034 40.55 19.712 41.39 19.376C41.152 18.984 40.914 18.606 40.676 18.214L41.684 17.57C42.552 18.9 43.21 20.062 43.672 21.042L42.636 21.77C42.44 21.35 42.23 20.93 42.006 20.482C40.578 21.084 38.94 21.602 37.12 22.05ZM47.48 20.524H46.36L46.01 19.236L47.046 19.278C47.438 19.278 47.634 19.082 47.634 18.718V11.256H45.492V22.4H44.162V10.01H48.964V19.012C48.964 20.02 48.46 20.524 47.48 20.524ZM55.278 10.486V20.972H53.99V20.104H52.31V21.308H51.008V10.486H55.278ZM52.31 18.872H53.99V15.834H52.31V18.872ZM52.31 14.588H53.99V11.69H52.31V14.588ZM57.378 14.714C58.232 16.016 58.89 17.15 59.338 18.13L58.232 18.9C57.742 17.864 57.084 16.688 56.272 15.4L57.378 14.714ZM60.43 22.302H57.952L57.658 21.014C58.47 21.056 59.24 21.084 59.982 21.084C60.318 21.084 60.486 20.902 60.486 20.538V13.258H55.824V11.984H60.486V9.632H61.844V11.984H63.412V13.258H61.844V20.874C61.844 21.826 61.368 22.302 60.43 22.302ZM66.366 9.73C67.262 10.402 68.004 11.088 68.62 11.76L67.71 12.684C67.178 12.026 66.422 11.326 65.456 10.598L66.366 9.73ZM64.574 13.72H67.71V19.642C68.144 19.278 68.606 18.858 69.11 18.382L69.474 19.782C68.564 20.622 67.598 21.378 66.576 22.036L66.072 20.832C66.296 20.636 66.422 20.384 66.422 20.09V14.994H64.574V13.72ZM69.572 15.386H76.362V16.604C75.956 17.85 75.284 18.914 74.346 19.81C75.242 20.398 76.334 20.93 77.594 21.378L76.88 22.526C75.48 21.966 74.318 21.322 73.366 20.622C72.274 21.406 70.916 22.036 69.32 22.512L68.578 21.392C70.062 20.958 71.308 20.412 72.316 19.74C71.364 18.816 70.706 17.794 70.37 16.674H69.572V15.386ZM71.616 16.674C71.938 17.528 72.484 18.298 73.268 18.998C74.024 18.312 74.612 17.542 75.018 16.674H71.616ZM70.552 9.982H75.508V12.922C75.508 13.23 75.676 13.398 76.026 13.398H77.258V14.616H75.676C74.682 14.616 74.192 14.14 74.192 13.216V11.228H71.868V11.648C71.868 13.104 71.126 14.238 69.656 15.036L68.774 14.014C69.95 13.482 70.552 12.74 70.552 11.788V9.982ZM82.97 13.608H86.638V9.506H88.01V13.608H91.482V14.924H88.01V22.47H86.638V14.924H82.97V13.608ZM80.422 9.632C81.332 10.318 82.102 11.004 82.732 11.704L81.822 12.628C81.262 11.97 80.492 11.27 79.498 10.542L80.422 9.632ZM78.574 13.692H81.808V19.656C82.256 19.292 82.718 18.872 83.208 18.41L83.558 19.768C82.662 20.608 81.696 21.364 80.674 22.022L80.17 20.832C80.408 20.636 80.534 20.398 80.534 20.118V14.966H78.574V13.692ZM97.81 15.134H95.206V20.468C95.206 20.804 95.388 20.986 95.766 20.986H98.202C98.622 20.986 98.888 20.874 98.986 20.664C99.098 20.44 99.182 19.964 99.238 19.25L100.484 19.67C100.372 20.888 100.204 21.63 99.952 21.882C99.728 22.106 99.266 22.218 98.594 22.218H95.192C94.31 22.218 93.876 21.798 93.876 20.958V14.308L93.204 14.98L92.42 13.902C93.75 12.684 94.968 11.242 96.074 9.59H97.054C98.286 10.878 99.462 12.222 100.554 13.622L99.686 14.532C98.482 12.936 97.488 11.704 96.676 10.85C95.948 11.928 95.136 12.964 94.212 13.958H99.112V17.822C99.098 18.592 98.678 18.998 97.838 19.054C97.292 19.054 96.774 19.04 96.284 19.012L95.962 17.766C96.48 17.822 96.928 17.85 97.32 17.85C97.642 17.85 97.81 17.682 97.81 17.374V15.134ZM101.184 10.752H102.486V19.39H101.184V10.752ZM103.606 22.316H101.492L101.184 21.042C101.87 21.098 102.528 21.126 103.144 21.126C103.48 21.126 103.648 20.916 103.648 20.51V9.562H104.978V20.846C104.978 21.826 104.516 22.316 103.606 22.316ZM112.916 12.684C112.272 14.028 111.544 15.19 110.746 16.17L109.794 15.022C110.956 13.664 111.95 11.788 112.762 9.408L114.106 9.702C113.91 10.29 113.7 10.85 113.49 11.382H119.258V12.684H114.764V14.686H118.698V15.974H114.764V18.032H118.726V19.306H114.764V22.47H113.42V12.684H112.916ZM108.142 14.994C107.75 15.47 107.316 15.946 106.868 16.408L106.42 14.98C107.862 13.398 108.926 11.55 109.612 9.45L110.872 10.038C110.508 11.102 110.046 12.11 109.486 13.062V22.47H108.142V14.994Z" fill="#333333"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6.0 KiB |