纯前端html英文字帖图片生成器自动段落和换行

纯前端html英文字帖图片生成器自动段落和换行,页边距定义等

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图立得英文字帖生成器 tulide.cn</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Arial', sans-serif;
background-color: #f5f5f5;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
display: grid;
grid-template-columns: 300px 1fr;
gap: 10px;
}
.form-panel {
background: white;
padding: 10px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
height: fit-content;
}
.preview-panel {
background: white;
padding: 10px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
text-align: center;
color: #333;
margin-bottom: 10px;
font-size: 24px;
}
.form-group {
margin-bottom: 8px;
}
.form-group label {
display: block;
margin-bottom: 2px;
font-weight: bold;
color: #555;
font-size: 12px;
}
.form-group input, .form-group textarea, .form-group select {
width: 100%;
padding: 5px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 12px;
}
.form-group textarea {
resize: vertical;
min-height: 100px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
}
.form-row-triple {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 6px;
}
.color-input {
width: 50px !important;
height: 25px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.submit-btn {
width: 100%;
padding: 12px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
margin-top: 20px;
}
.submit-btn:hover {
background: #0056b3;
}
.canvas-container {
width: 100%;
height: 1123px; /* A4 height in pixels at 72 DPI */
border: 1px solid #ddd;
background: white;
overflow: hidden;
position: relative;
}
#previewCanvas {
width: 100%;
height: 100%;
display: block;
}
.section-title {
font-size: 14px;
font-weight: bold;
color: #333;
margin: 20px 0 10px 0;
padding-bottom: 5px;
border-bottom: 2px solid #007bff;
}
.footer-text {
margin-top: 20px;
text-align: center;
font-size: 10px;
color: #666;
}
@media print {
body {
background: white;
padding: 0;
}
.container {
grid-template-columns: 1fr;
max-width: none;
}
.form-panel {
display: none;
}
.preview-panel {
box-shadow: none;
padding: 0;
}
}
</style>
</head>
<body>
<div class="container">
<div class="form-panel">
<h1>英文字帖生成器</h1>
<form id="copyBookForm">
<div class="section-title">页面设置</div>
<div class="form-row">
<div class="form-group">
<label>左边距 (mm)</label>
<input type="number" id="marginLeft" value="20" min="10" max="50">
</div>
<div class="form-group">
<label>右边距 (mm)</label>
<input type="number" id="marginRight" value="15" min="10" max="50">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>上边距 (mm)</label>
<input type="number" id="marginTop" value="15" min="10" max="50">
</div>
<div class="form-group">
<label>下边距 (mm)</label>
<input type="number" id="marginBottom" value="15" min="10" max="50">
</div>
</div>
<div class="section-title">标题设置</div>
<div class="form-group">
<label>标题内容</label>
<input type="text" id="titleText" value="English Handwriting Practice" placeholder="输入标题内容">
</div>
<div class="form-row-triple">
<div class="form-group">
<label>字体</label>
<select id="titleFont">
<option value="Arial">Arial</option>
<option value="Times New Roman">Times New Roman</option>
<option value="Georgia">Georgia</option>
<option value="Verdana">Verdana</option>
</select>
</div>
<div class="form-group">
<label>字号 (px)</label>
<input type="number" id="titleSize" value="24" min="12" max="48">
</div>
<div class="form-group">
<label>颜色</label>
<input type="color" id="titleColor" value="#000000" class="color-input">
</div>
</div>
<div class="section-title">姓名栏设置</div>
<div class="form-row-triple">
<div class="form-group">
<label>字体</label>
<select id="nameFont">
<option value="Arial">Arial</option>
<option value="Times New Roman">Times New Roman</option>
<option value="Georgia">Georgia</option>
<option value="Verdana">Verdana</option>
</select>
</div>
<div class="form-group">
<label>字号 (px)</label>
<input type="number" id="nameSize" value="14" min="10" max="24">
</div>
<div class="form-group">
<label>颜色</label>
<input type="color" id="nameColor" value="#000000" class="color-input">
</div>
</div>
<div class="section-title">正文设置</div>
<div class="form-group">
<label>练习内容</label>
<textarea id="contentText" placeholder="输入要练习的英文内容...">The quick brown fox jumps over the lazy dog. This sentence contains every letter of the alphabet and is commonly used for handwriting practice. Practice makes perfect, so keep writing to improve your handwriting skills.</textarea>
</div>
<div class="form-row-triple">
<div class="form-group">
<label>字体</label>
<select id="contentFont">
<option value="Arial">Arial</option>
<option value="Times New Roman" selected>Times New Roman</option>
<option value="Georgia">Georgia</option>
<option value="Verdana">Verdana</option>
</select>
</div>
<div class="form-group">
<label>字号 (px)</label>
<input type="number" id="contentSize" value="12" min="10" max="24">
</div>
<div class="form-group">
<label>颜色</label>
<input type="color" id="contentColor" value="#000000" class="color-input">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>行距 (px)</label>
<input type="number" id="lineHeight" value="60" min="40" max="100">
</div>
<div class="form-group">
<label>段首缩进</label>
<select id="indentFirst">
<option value="true">是</option>
<option value="false">否</option>
</select>
</div>
</div>
<!--button type="submit" class="submit-btn">生成PDF字帖</button-->
</form>
<div class="footer-text">
实时预览 • 支持A4打印 • 自动排版
</div>
</div>
<div class="preview-panel">
<div class="canvas-container">
<canvas id="previewCanvas" width="794" height="1123"></canvas>
</div>
</div>
</div>
<script>
class CopyBookGenerator {
constructor() {
this.canvas = document.getElementById('previewCanvas');
this.ctx = this.canvas.getContext('2d');
this.setupEventListeners();
this.render();
}
setupEventListeners() {
const inputs = ['marginLeft', 'marginRight', 'marginTop', 'marginBottom',
'titleText', 'titleFont', 'titleSize', 'titleColor',
'nameFont', 'nameSize', 'nameColor',
'contentText', 'contentFont', 'contentSize', 'contentColor',
'lineHeight', 'indentFirst'];
inputs.forEach(id => {
const element = document.getElementById(id);
if (element) {
element.addEventListener('input', () => this.render());
element.addEventListener('change', () => this.render());
}
});
document.getElementById('copyBookForm').addEventListener('submit', (e) => {
e.preventDefault();
this.submitForm();
});
}
getFormData() {
return {
marginLeft: parseInt(document.getElementById('marginLeft').value),
marginRight: parseInt(document.getElementById('marginRight').value),
marginTop: parseInt(document.getElementById('marginTop').value),
marginBottom: parseInt(document.getElementById('marginBottom').value),
titleText: document.getElementById('titleText').value,
titleFont: document.getElementById('titleFont').value,
titleSize: parseInt(document.getElementById('titleSize').value),
titleColor: document.getElementById('titleColor').value,
nameFont: document.getElementById('nameFont').value,
nameSize: parseInt(document.getElementById('nameSize').value),
nameColor: document.getElementById('nameColor').value,
contentText: document.getElementById('contentText').value,
contentFont: document.getElementById('contentFont').value,
contentSize: parseInt(document.getElementById('contentSize').value),
contentColor: document.getElementById('contentColor').value,
lineHeight: parseInt(document.getElementById('lineHeight').value),
indentFirst: document.getElementById('indentFirst').value === 'true'
};
}
mmToPx(mm) {
return Math.round(mm * 2.834645669); // 72 DPI conversion
}
render() {
const data = this.getFormData();
// Clear canvas
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.fillStyle = '#ffffff';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
const leftMargin = this.mmToPx(data.marginLeft);
const rightMargin = this.mmToPx(data.marginRight);
const topMargin = this.mmToPx(data.marginTop);
const bottomMargin = this.mmToPx(data.marginBottom);
const contentWidth = this.canvas.width - leftMargin - rightMargin;
let currentY = topMargin;
// Draw title
if (data.titleText.trim()) {
this.ctx.font = `${data.titleSize}px ${data.titleFont}`;
this.ctx.fillStyle = data.titleColor;
this.ctx.textAlign = 'center';
this.ctx.fillText(data.titleText, this.canvas.width / 2, currentY + data.titleSize);
currentY += data.titleSize + 30;
}
// Draw name line
this.ctx.font = `${data.nameSize}px ${data.nameFont}`;
this.ctx.fillStyle = data.nameColor;
this.ctx.textAlign = 'center';
const nameLineText = "姓名________班级________学号________日期________评分________";
this.ctx.fillText(nameLineText, this.canvas.width / 2, currentY + data.nameSize);
currentY += data.nameSize + 40;
// Process content text
this.renderContent(data, leftMargin, currentY, contentWidth, this.canvas.height - bottomMargin);
}
renderContent(data, startX, startY, maxWidth, maxY) {
this.ctx.font = `${data.contentSize * 2.5}px ${data.contentFont}`;
this.ctx.fillStyle = data.contentColor;
this.ctx.textAlign = 'left';
const words = data.contentText.split(/\s+/).filter(word => word.length > 0);
let currentY = startY;
let currentLine = '';
let isFirstParagraph = true;
for (let i = 0; i < words.length; i++) {
const word = words[i];
const testLine = currentLine + (currentLine ? ' ' : '') + word;
const lineWidth = this.ctx.measureText(testLine).width;
const indentWidth = (data.indentFirst && isFirstParagraph) ? data.contentSize * 2 : 0;
if (lineWidth + indentWidth > maxWidth && currentLine) {
// Draw current line with four-line background
this.drawFourLineBackground(startX, currentY, maxWidth, data.lineHeight);
this.ctx.fillStyle = data.contentColor;
this.ctx.fillText(currentLine, startX + indentWidth, currentY + data.lineHeight * 0.6);
currentY += data.lineHeight;
currentLine = word;
isFirstParagraph = false;
if (currentY + data.lineHeight > maxY) {
break;
}
} else {
currentLine = testLine;
}
}
// Draw last line
if (currentLine && currentY + data.lineHeight <= maxY) {
const indentWidth = (data.indentFirst && isFirstParagraph) ? data.contentSize * 2 : 0;
this.drawFourLineBackground(startX, currentY, maxWidth, data.lineHeight);
this.ctx.fillStyle = data.contentColor;
this.ctx.fillText(currentLine, startX + indentWidth, currentY + data.lineHeight * 0.6);
currentY += data.lineHeight;
}
// Fill remaining space with empty lines
while (currentY + data.lineHeight <= maxY) {
this.drawFourLineBackground(startX, currentY, maxWidth, data.lineHeight);
currentY += data.lineHeight;
}
}
drawFourLineBackground(x, y, width, height) {
this.ctx.strokeStyle = '#cccccc';
this.ctx.lineWidth = 1;
// Top line (solid, bold)
this.ctx.lineWidth = 2;
this.ctx.beginPath();
this.ctx.moveTo(x, y);
this.ctx.lineTo(x + width, y);
this.ctx.stroke();
// Middle lines (dashed)
this.ctx.lineWidth = 1;
this.ctx.setLineDash([5, 5]);
// First middle line
this.ctx.beginPath();
this.ctx.moveTo(x, y + height * 0.33);
this.ctx.lineTo(x + width, y + height * 0.33);
this.ctx.stroke();
// Second middle line
this.ctx.beginPath();
this.ctx.moveTo(x, y + height * 0.67);
this.ctx.lineTo(x + width, y + height * 0.67);
this.ctx.stroke();
// Bottom line (solid, bold)
this.ctx.setLineDash([]);
this.ctx.lineWidth = 2;
this.ctx.beginPath();
this.ctx.moveTo(x, y + height);
this.ctx.lineTo(x + width, y + height);
this.ctx.stroke();
}
submitForm() {
}
}
// Initialize when page loads
document.addEventListener('DOMContentLoaded', () => {
new CopyBookGenerator();
});
</script>
</body>
</html>
相关推荐
代码搬运媛13 小时前
Jest 测试框架详解与实现指南
前端
counterxing14 小时前
我把 Codex 里的 Skills 做成了一个 MCP,还支持分享
前端·agent·ai编程
wangqiaowq15 小时前
windows下nginx的安装
linux·服务器·前端
之歆15 小时前
DAY_12JavaScript DOM 完全指南(二):实战与性能篇
开发语言·前端·javascript·ecmascript
发现一只大呆瓜15 小时前
Vite凭什么这么快?3分钟带你彻底搞懂 Vite 热更新的幕后黑手
前端·面试·vite
Maimai1080815 小时前
React如何用 @microsoft/fetch-event-source 落地 SSE:比原生 EventSource 更灵活的实时推送方案
前端·javascript·react.js·microsoft·前端框架·reactjs·webassembly
kyriewen17 小时前
产品经理把PRD写成“天书”,我用AI半小时重写了一遍,他当场愣住
前端·ai编程·cursor
humcomm17 小时前
元框架的工作原理详解
前端·前端框架
canonical_entropy18 小时前
Attractor Before Harness: AI 大规模开发的方法论
前端·aigc·ai编程
zhangxingchao18 小时前
多 Agent 架构到底怎么选?从 Claude Agent Teams、Cognition/Devin 到工程落地原则
前端·人工智能·后端