Commit a1599de3 authored by fisherdaddy's avatar fisherdaddy

feature: 新增手写字体生成器

parent e37980a7
......@@ -8,6 +8,7 @@
"preview": "vite preview"
},
"dependencies": {
"antd": "^5.21.6",
"html2canvas": "^1.4.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
......
......@@ -12,7 +12,7 @@ const UrlEncode = lazy(() => import('./components/UrlEncode'));
const About = lazy(() => import('./pages/About'));
const OpenAITimeline = lazy(() => import('./components/OpenAITimeline'));
const PricingCharts = lazy(() => import('./components/PricingCharts'));
const HandwriteGen = lazy(() => import('./components/HandwriteGen'));
function App() {
return (
......@@ -30,6 +30,7 @@ function App() {
<Route path="/about" element={<About />} />
<Route path="/openai-timeline" element={<OpenAITimeline />} />
<Route path="/llm-model-price" element={<PricingCharts />} />
<Route path="/handwriting" element={<HandwriteGen />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Suspense>
......
import React, { useState } from 'react';
import '../styles/HandwriteGen.css';
import html2canvas from 'html2canvas';
import {
Layout, Menu, Input, Select, Checkbox, Button, Slider, Typography, Row, Col
} from 'antd';
// 引入本地纸张背景图片
import Style1Img from '../data/handwrite/style1.png';
import Style2Img from '../data/handwrite/style2.png';
import Style3Img from '../data/handwrite/style3.png';
const { Header, Content, Sider } = Layout;
const { TextArea } = Input;
const { Option } = Select;
const { Title } = Typography;
function HandwritingGenerator() {
const [text, setText] = useState('');
const [font, setFont] = useState("'XINYE'");
const [paperType, setPaperType] = useState('Lined Paper'); // 默认值为横线纸
const [paperBackground, setPaperBackground] = useState('None'); // 新增状态
const [borderEnabled, setBorderEnabled] = useState(false);
const [topMargin, setTopMargin] = useState(50);
const [leftMargin, setLeftMargin] = useState(30);
const [rightMargin, setRightMargin] = useState(30);
const [fontSize, setFontSize] = useState(20);
const [fontColor, setFontColor] = useState('#000080');
const [textAlign, setTextAlign] = useState('left');
const [lineSpacing, setLineSpacing] = useState(1.25);
const [charSpacing, setCharSpacing] = useState(0);
const handleGenerate = () => {
const previewElement = document.querySelector('.preview-area');
html2canvas(previewElement, {
useCORS: true,
backgroundColor: null,
scale: 2, // 提高图片清晰度
onclone: (clonedDoc) => {
// 重新设置背景,确保CSS渐变被正确捕获
const clonedPreview = clonedDoc.querySelector('.preview-area');
clonedPreview.style.backgroundImage = getPaperBackground();
clonedPreview.style.backgroundSize = getBackgroundSize();
clonedPreview.style.backgroundRepeat = getBackgroundRepeat();
},
}).then(canvas => {
const link = document.createElement('a');
link.download = 'handwriting.png';
link.href = canvas.toDataURL();
link.click();
});
};
const getPaperBackground = () => {
let backgrounds = [];
// 根据纸张类型设置CSS渐变背景
switch(paperType) {
case 'Lined Paper':
backgrounds.push(`linear-gradient(to bottom, transparent ${lineSpacing * fontSize - 1}px, #c0c0c0 1px)`);
break;
case 'Grid Paper':
backgrounds.push(
`linear-gradient(to bottom, transparent ${lineSpacing * fontSize - 1}px, #c0c0c0 1px)`,
`linear-gradient(to right, transparent ${fontSize}px, #c0c0c0 1px)`
);
break;
default:
break;
}
// 如果用户选择了纸张背景,添加背景图片
if (paperBackground !== 'None') {
let backgroundImage;
switch(paperBackground) {
case 'Style1':
backgroundImage = `url(${Style1Img})`;
break;
case 'Style2':
backgroundImage = `url(${Style2Img})`;
break;
case 'Style3':
backgroundImage = `url(${Style3Img})`;
break;
default:
break;
}
if (backgroundImage) {
backgrounds.push(backgroundImage);
}
}
if (backgrounds.length === 0) {
return 'none';
} else {
return backgrounds.join(', ');
}
};
const getBackgroundSize = () => {
let sizes = [];
switch(paperType) {
case 'Lined Paper':
sizes.push(`100% ${lineSpacing * fontSize}px`);
break;
case 'Grid Paper':
sizes.push(`100% ${lineSpacing * fontSize}px`, `${lineSpacing * fontSize}px 100%`);
break;
default:
break;
}
if (paperBackground !== 'None') {
sizes.push('cover');
}
return sizes.join(', ');
};
const getBackgroundRepeat = () => {
let repeats = [];
// 对于纸张类型的CSS渐变背景,需要重复
switch(paperType) {
case 'Lined Paper':
repeats.push('repeat-y');
break;
case 'Grid Paper':
repeats.push('repeat-y', 'repeat-x');
break;
default:
break;
}
// 对于纸张背景图片,设置为不重复或根据需要重复
if (paperBackground !== 'None') {
repeats.push('no-repeat');
}
return repeats.join(', ');
};
const backgroundOffset = -(lineSpacing * fontSize - fontSize);
return (
<Layout style={{ minHeight: '100vh' }}>
<Sider width={300} className="site-layout-background">
<Menu mode="inline" defaultSelectedKeys={['1']}>
<Menu.Item key="1">手写字体生成器</Menu.Item>
</Menu>
<div className="settings-section">
<div className="form-group">
<label>手写字体</label>
<Select value={font} onChange={setFont} style={{ width: '100%' }}>
<Option value="'XINYE'">新叶念体</Option>
<Option value="'cicada'">CC 字体</Option>
<Option value="'xiongdi'">兄弟字体</Option>
<Option value="'qishan-zhong'">Zhong Qi Shan 体</Option>
</Select>
</div>
<div className="form-group">
<label>纸张类型</label>
<Select value={paperType} onChange={setPaperType} style={{ width: '100%' }}>
<Option value="No Paper">无纸张</Option>
<Option value="Lined Paper">横线纸</Option>
</Select>
</div>
{/* 新增纸张背景选项 */}
<div className="form-group">
<label>纸张背景</label>
<Select value={paperBackground} onChange={setPaperBackground} style={{ width: '100%' }}>
<Option value="None">无背景</Option>
<Option value="Style1">样式1</Option>
<Option value="Style2">样式2</Option>
<Option value="Style3">样式3</Option>
</Select>
</div>
<div className="form-group">
<Checkbox checked={borderEnabled} onChange={() => setBorderEnabled(!borderEnabled)}>
边框
</Checkbox>
</div>
<div className="form-group">
<label>边距设置 (px)</label>
<Row gutter={8}>
<Col span={8}>
<Input
type="number"
value={topMargin}
onChange={(e) => setTopMargin(e.target.value)}
placeholder="上"
/>
</Col>
<Col span={8}>
<Input
type="number"
value={leftMargin}
onChange={(e) => setLeftMargin(e.target.value)}
placeholder="左"
/>
</Col>
<Col span={8}>
<Input
type="number"
value={rightMargin}
onChange={(e) => setRightMargin(e.target.value)}
placeholder="右"
/>
</Col>
</Row>
</div>
<div className="form-group">
<label>字体大小 (px)</label>
<Slider
min={12}
max={72}
value={fontSize}
onChange={setFontSize}
/>
</div>
<div className="form-group">
<label>字体颜色</label>
<Input
type="color"
value={fontColor}
onChange={(e) => setFontColor(e.target.value)}
style={{ width: '100%' }}
/>
</div>
<div className="form-group">
<label>文字对齐</label>
<Select value={textAlign} onChange={setTextAlign} style={{ width: '100%' }}>
<Option value="left">居左</Option>
<Option value="center">居中</Option>
<Option value="right">居右</Option>
</Select>
</div>
<div className="form-group">
<label>行间距</label>
<Slider
min={1}
max={3}
step={0.1}
value={lineSpacing}
onChange={setLineSpacing}
/>
</div>
<div className="form-group">
<label>字符间距 (px)</label>
<Slider
min={0}
max={10}
value={charSpacing}
onChange={setCharSpacing}
/>
</div>
<Button type="primary" onClick={handleGenerate} style={{ width: '100%' }}>
生成图片
</Button>
</div>
</Sider>
<Layout>
<Content style={{ margin: '16px' }}>
<Row gutter={16}>
<Col xs={24} lg={12}>
<Title level={4}>输入文本</Title>
<TextArea
rows={15}
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="请输入您的文本..."
/>
</Col>
<Col xs={24} lg={12}>
<Title level={4}>预览</Title>
<div
className="preview-area"
style={{
fontFamily: font,
fontSize: `${fontSize}px`,
color: fontColor,
textAlign: textAlign,
marginTop: `${topMargin}px`,
marginLeft: `${leftMargin}px`,
marginRight: `${rightMargin}px`,
lineHeight: `${lineSpacing * fontSize}px`,
letterSpacing: `${charSpacing}px`,
border: borderEnabled ? '1px solid #000' : 'none',
backgroundImage: getPaperBackground(),
backgroundSize: getBackgroundSize(),
backgroundRepeat: getBackgroundRepeat(),
padding: '20px',
minHeight: '400px',
boxSizing: 'border-box',
backgroundPosition: `left ${backgroundOffset}px`,
}}
>
{text.split('\n').map((line, index) => (
<p key={index} style={{ margin: 0 }}>{line}</p>
))}
</div>
</Col>
</Row>
</Content>
</Layout>
</Layout>
);
}
export default HandwritingGenerator;
......@@ -2,7 +2,7 @@
"en": {
"title": "AI Toolbox",
"description": "AI Toolbox - A collection of AI tools including text cards, JSON formatter, URL decoder, OpenAI product releases summary, and global model price comparisons to help you accomplish various tasks effortlessly.",
"slogan": "Your collection of intelligent assistants, solving various AI needs in one place.",
"slogan": "Your intelligent development toolset, a one-stop solution for all AI tool needs.",
"keywords": "AI Toolbox, AI tools, text cards, JSON formatter, URL decoder, OpenAI products, model price comparison, online tools, free tools",
"tools": {
"text2image": {
......@@ -42,6 +42,10 @@
"modelPrice": {
"title": "Global Large Model Price Comparison",
"description": "Arena for comparing prices of various models worldwide"
},
"handwrite": {
"title": "Handwriting Font Generator",
"description": "Generates effects similar to writing on paper"
}
},
"notFound": {
......@@ -68,7 +72,7 @@
"zh": {
"title": "AI 工具箱",
"description": "AI工具箱 - 集合了多种 AI 工具,如文字卡片、JSON 格式化、URL 解码器、OpenAI 产品发布汇总、全球各大模型价格对比,帮助您轻松完成各类任务。",
"slogan": "您的智能助手集合,一站式解决各种 AI 需求。",
"slogan": "您的智能开发工具集合,一站式解决各种 AI 工具需求。",
"keywords": "AI工具箱,AI 工具,文字卡片,JSON 格式化,URL 解码器,OpenAI 产品,模型价格对比,在线工具,免费工具",
"tools": {
"text2image": {
......@@ -108,6 +112,10 @@
"modelPrice": {
"title": "全球大模型价格对比",
"description": "全球各模型价格对比竞技场"
},
"handwrite": {
"title": "手写字体生成器",
"description": "生成和纸上书写一样的效果"
}
},
"notFound": {
......@@ -134,7 +142,7 @@
"ja": {
"title": "AIツールボックス",
"description": "AIツールボックス - テキストカード、JSONフォーマッター、URLデコーダー、OpenAI製品のリリースまとめ、世界のモデル価格比較など、多様なAIツールを集めたサイトです。さまざまなタスクを簡単にこなせます。",
"slogan": "あなたのインテリジェントアシスタントコレクション、様々なAIニーズを一箇所で解決します。",
"slogan": "あなたのスマートな開発ツールセット、あらゆるAIツールのニーズをワンストップで解決します。",
"keywords": "AIツールボックス、AIツール、テキストカード、JSONフォーマッター、URLデコーダー、OpenAI製品、モデル価格比較、オンラインツール、無料ツール",
"tools": {
"text2image": {
......@@ -174,6 +182,10 @@
"modelPrice": {
"title": "世界の大規模モデル価格比較",
"description": "世界各国のモデル価格比較アリーナ"
},
"handwrite": {
"title": "手書きフォントジェネレーター",
"description": "紙に書くような効果を生成します"
}
},
"notFound": {
......@@ -200,7 +212,7 @@
"ko": {
"title": "AI 도구 상자",
"description": "AI 도구상자 - 텍스트 카드, JSON 포매터, URL 디코더, OpenAI 제품 출시 요약, 글로벌 모델 가격 비교 등 다양한 AI 도구를 모아 다양한 작업을 손쉽게 수행할 수 있도록 도와드립니다.",
"slogan": "당신의 지능형 어시스턴트 컬렉션, 다양한 AI 요구 사항을 한 곳에서 해결합니다.",
"slogan": "당신의 지능형 개발 도구 모음, 모든 AI 도구 요구 사항을 원스톱으로 해결합니다.",
"keywords": "AI 도구상자, AI 도구, 텍스트 카드, JSON 포매터, URL 디코더, OpenAI 제품, 모델 가격 비교, 온라인 도구, 무료 도구",
"tools": {
"text2image": {
......@@ -240,6 +252,10 @@
"modelPrice": {
"title": "전세계 대형 모델 가격 비교",
"description": "전 세계 각 모델 가격 비교 아레나"
},
"handwrite": {
"title": "손글씨 폰트 생성기",
"description": "종이에 쓴 것 같은 효과를 생성합니다"
}
},
"notFound": {
......
......@@ -8,6 +8,7 @@ const tools = [
{ id: 'jsonFormatter', icon: 'fa-jsonformat', path: '/json-formatter' },
{ id: 'urlDecode', icon: 'fa-decode', path: '/url-decode' },
{ id: 'urlEncode', icon: 'fa-encode', path: '/url-encode' },
{ id: 'handwrite', icon: 'fa-handwrite', path: '/handwriting' },
{ id: 'openAITimeline', icon: 'fa-openai-timeline', path: '/openai-timeline' },
{ id: 'modelPrice', icon: 'fa-model-price', path: '/llm-model-price' },
];
......
@import url('https://fonts.googleapis.com/css2?family=Dancing+Script&family=Indie+Flower&display=swap');
@font-face {
font-family: 'XINYE';
src: url('../data/handwrite/fonts/new-leaf.otf');
}
@font-face {
font-family: 'cicada';
src: url('../data/handwrite/fonts/cicada.ttf');
}
@font-face {
font-family: 'xiongdi';
src: url('../data/handwrite/fonts/xiongdi.ttf');
}
@font-face {
font-family: 'qishan-zhong';
src: url('../data/handwrite/fonts/qishan-zhong.ttf');
}
body, html, #root {
height: 100%;
margin: 0;
}
.site-layout-background {
background: #fff;
}
.preview-area {
background-color: #fff;
border-radius: 4px;
overflow: auto;
}
.settings-section {
padding: 24px;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
font-weight: bold;
}
.ant-input-number, .ant-input, .ant-select-selector {
width: 100%;
}
.ant-slider {
width: 100%;
}
.ant-btn {
margin-top: 16px;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment