在线汉字笔画练习工具(HTML文件版)
Hanzi Writer 是JavaScript 免费开源库,根据汉字书写时按照笔画顺序的特征,可以播放正确笔画顺序的描边动画和练习测试。 支持简体字和繁体字。适用汉字学习者、特别是小学生练习汉字笔画顺序。
可以使用script标签加载CDN:
<script src="https://cdn.jsdelivr.net/npm/hanzi-writer@3.0/dist/hanzi-writer.min.js"></script>
也可以将hanzi-writer.min.js下载到本地,这样就可以离线使用了。
可以根据用户输入的字,给出其笔画动画演示和手写练习。对于确实不支持的汉字也会给出明确的提示。支持的汉字范围:常用汉字(依赖hanzi-writer数据库)。
这个的JavaScript方案有很多优势:
使用现成的hanzi-writer库;
完全在浏览器端运行;
良好的交互体验;
部署简单(一个HTML文件即可)。
工作流程
输入一个汉字,点几“确认”,从hanzi-writer库中取出此汉字的笔画信息。当第一次请求某个汉字时,需要从 CDN 下载该汉字的笔画数据文件,所以会较慢。一旦下载后,数据会被浏览器缓存,再次使用同一个字就会较快。
点击“演示动画”可以看到笔顺演示。
点击“开始练习”可以进行手写描摹练习。
运行截图如下:
源码如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>汉字书写练习</title>
<script src="https://cdn.jsdelivr.net/npm/hanzi-writer@3.0/dist/hanzi-writer.min.js"></script>
<style>
.container {
text-align: center;
margin: 20px;
}
#character-target-div {
border: 1px solid #ddd;
margin: 20px auto;
}
.button {
padding: 10px 20px;
margin: 5px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.button:hover {
background-color: #45a049;
}
.button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.input-group {
margin: 10px;
display: inline-flex;
align-items: center;
gap: 10px;
}
input {
padding: 10px;
font-size: 16px;
border: 1px solid #ddd;
border-radius: 4px;
width: 50px;
text-align: center;
}
#confirmButton {
background-color: #007bff;
}
#confirmButton:hover {
background-color: #0056b3;
}
#status {
margin: 10px;
padding: 10px;
border-radius: 4px;
display: none;
}
.loading {
background-color: #fff3cd;
color: #856404;
border: 1px solid #ffeeba;
}
.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.loading-spinner {
display: inline-block;
width: 20px;
height: 20px;
margin-right: 10px;
border: 3px solid #f3f3f3;
border-top: 3px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="container">
<h1>在线汉字书写练习工具</h1>
<div class="input-group">
<input type="text" id="characterInput"
placeholder="汉字"
maxlength="1"
oninput="onCharacterInput()">
<button class="button" id="confirmButton" onclick="loadCharacter()">确认</button>
</div>
<br>
<button class="button" id="animateButton" onclick="showAnimation()" disabled>演示动画</button>
<button class="button" id="quizButton" onclick="startQuiz()" disabled>开始练习</button>
<br>
<div id="status"></div>
<div id="character-target-div"></div>
</div>
<script>
let writer = null;
let loadingTimeout = null;
function onCharacterInput() {
// 输入新字符时禁用功能按钮
document.getElementById('animateButton').disabled = true;
document.getElementById('quizButton').disabled = true;
// 清除之前的状态和内容
hideStatus();
if (writer) {
document.getElementById('character-target-div').innerHTML = '';
writer = null;
}
}
function showStatus(message, type = 'loading') {
const statusDiv = document.getElementById('status');
statusDiv.className = type;
statusDiv.style.display = 'block';
if (type === 'loading') {
statusDiv.innerHTML = `<div class="loading-spinner"></div>${message}`;
} else {
statusDiv.textContent = message;
}
if (type === 'error') {
setTimeout(() => {
statusDiv.style.display = 'none';
}, 3000);
}
}
function hideStatus() {
document.getElementById('status').style.display = 'none';
}
function setButtonsEnabled(enabled) {
document.getElementById('animateButton').disabled = !enabled;
document.getElementById('quizButton').disabled = !enabled;
document.getElementById('confirmButton').disabled = !enabled;
}
async function loadCharacter() {
const character = document.getElementById('characterInput').value;
if (!character) {
showStatus('请输入一个汉字', 'error');
return;
}
await initWriter(character);
}
// 检查汉字是否成功渲染
function checkCharacterRendered() {
const targetDiv = document.getElementById('character-target-div');
// 检查是否有SVG元素
const svgElement = targetDiv.querySelector('svg');
if (!svgElement) {
return false;
}
// 检查SVG是否有实际的笔画路径
const pathElements = svgElement.querySelectorAll('path');
if (pathElements.length === 0) {
return false;
}
// 检查路径是否有实际的绘制数据
for (let path of pathElements) {
const d = path.getAttribute('d');
if (d && d.trim() !== '') {
return true;
}
}
return false;
}
async function initWriter(character) {
setButtonsEnabled(false);
showStatus('正在加载汉字数据...请稍候');
document.getElementById('character-target-div').innerHTML = '';
try {
// 先检查汉字数据是否存在
const charDataPromise = HanziWriter.loadCharacterData(character);
const timeoutPromise = new Promise((_, reject) => {
loadingTimeout = setTimeout(() => {
reject(new Error('加载超时,请重试'));
}, 8000);
});
let charData;
try {
charData = await Promise.race([charDataPromise, timeoutPromise]);
if (!charData || !charData.strokes || charData.strokes.length === 0) {
throw new Error('汉字数据不完整');
}
} catch (error) {
clearTimeout(loadingTimeout);
throw new Error('抱歉,该汉字可能不存在或暂不支持');
}
// 创建writer实例
writer = HanziWriter.create('character-target-div', character, {
width: 300,
height: 300,
padding: 5,
showOutline: true
});
// 等待渲染完成并检查结果
await new Promise(resolve => setTimeout(resolve, 1000));
if (checkCharacterRendered()) {
clearTimeout(loadingTimeout);
showStatus('加载完成', 'success');
setTimeout(hideStatus, 1000);
setButtonsEnabled(true);
return writer;
} else {
throw new Error('抱歉,该汉字渲染失败或暂不支持');
}
} catch (error) {
clearTimeout(loadingTimeout);
showStatus(`${error.message}`, 'error');
document.getElementById('confirmButton').disabled = false;
console.error('加载失败:', error);
// 清理可能的残留内容
document.getElementById('character-target-div').innerHTML = '';
writer = null;
return null;
}
}
async function showAnimation() {
if (writer) {
writer.animateCharacter();
} else {
showStatus('请先确认加载汉字', 'error');
}
}
async function startQuiz() {
if (writer) {
writer.quiz({
onComplete: function(summaryData) {
showStatus('练习完成!', 'success');
setTimeout(hideStatus, 2000);
}
});
} else {
showStatus('请先确认加载汉字', 'error');
}
}
</script>
</body>
</html>
OK!