canvas测试&方法
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>文章生成图片</title>
<style>
body {
margin: 0;
padding: 1rem;
background-color: #f0f0f0;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
text-align: center;
}
canvas {
border-radius: 12px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
max-width: 100%;
height: auto;
}
button {
margin-top: 1rem;
padding: 10px 20px;
font-size: 16px;
border: none;
border-radius: 8px;
background-color: #4caf50;
color: white;
cursor: pointer;
}
button:hover {
background-color: #45a049;
}
</style>
</head>
<body>
<canvas id="articleCanvas" style="display: none;" width="400"></canvas>
<br />
<!-- <button onclick="downloadImage()">生成并保存图片</button> -->
<script>
var jsonData = {
title: "这是文章的标题,可能会比较长,需要自动换行并居中",
author: "作者名:张三作者名:张三作者名:张三作者名:张三作者名:张三作者名:张三作者名:张三作者名:张三作者名:张三",
content: [{
sectionTitle: "第一部分:介绍",
text: ` React 项目说明
一个典型的 React 项目由多个文件和文件夹组成,每个部分都有其特定的作用。
本章节将对 React 项目代码做基本的说明,帮助大家理解项目的结构和各个部分的功能。
React 项目由多个文件和文件夹组成,核心文件包括 index.html、index.js 和 App.js。
React 组件是应用的基本构建块,可以是函数组件或类组件。
JSX 是 React 的核心语法,用于描述 UI。
Props 和 State 是 React 组件中管理数据的主要方式。
项目结构
一个 React 项目通常包含以下文件和文件夹:
my-react-app/
├── node_modules/ # 项目依赖的第三方库
├── public/ # 静态资源文件夹
│ ├── index.html # 应用的 HTML 模板
│ └── … # 其他静态资源(如图片、字体等)
├── src/ # 项目源代码
│ ├── App.js # 主组件
│ ├── App.css # 主组件的样式
│ ├── index.js # 项目入口文件
│ ├── index.css # 全局样式
│ └── … # 其他组件和资源
├── package.json # 项目配置和依赖管理
├── package-lock.json # 依赖的精确版本锁定文件
└── README.md # 项目说明文档
核心文件解析`
},
{
sectionTitle: "第二部分:细节阐述",
text: "这是第二部分的正文。正文区域应有内边距,标题要加粗,放在边框上方。边框带圆角,样式整洁。"
},
{
sectionTitle: "结语",
text: "以上便是全文内容的总结,感谢阅读。希望你喜欢本次的内容呈现方式,尤其是在移动端浏览的体验。"
}
]
};
jsonData = JSON.stringify(jsonData);
//源码转换成用反引号包裹的模板字符串形式
function getBase64(jsonData) {
//转移的时候注释
jsonData = JSON.parse(jsonData);
const canvas = document.getElementById("articleCanvas");
const ctx = canvas.getContext("2d");
function drawWatermark(ctx, canvasWidth, canvasHeight) {
const watermarkText = '小乌龟侦探社';
const fontSize = 24;
const color = 'rgba(150, 150, 150, 0.1)';
const angle = -0.15; // 倾斜角度(约等于 -8.6°)
const spacingX = 200; // 水平间距
const spacingY = 100; // 垂直间距
ctx.font = `bold ${fontSize}px "Microsoft YaHei", sans-serif`;
ctx.fillStyle = color;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const textWidth = ctx.measureText(watermarkText).width;
ctx.save();
const cols = Math.ceil(canvasWidth / spacingX) + 1;
const rows = Math.ceil(canvasHeight / spacingY) + 1;
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const offsetX = (row % 2) * (spacingX / 2);
const x = col * spacingX + offsetX;
const y = row * spacingY;
ctx.save();
ctx.translate(x, y);
ctx.rotate(angle);
ctx.fillText(watermarkText, 0, 0);
ctx.restore();
}
}
ctx.restore();
}
function drawText(ctx, text, x, y, maxWidth, lineHeight, font = '16px sans-serif', align = 'left') {
ctx.font = font;
ctx.textAlign = align;
const paragraphs = text.split(/\r?\n/);
let totalHeight = 0;
paragraphs.forEach((para, pIdx) => {
const chars = para.split('');
let line = '';
let lines = [];
for (let i = 0; i < chars.length; i++) {
const testLine = line + chars[i];
if (ctx.measureText(testLine).width > maxWidth && line.length > 0) {
lines.push(line);
line = chars[i];
} else {
line = testLine;
}
}
if (line) lines.push(line);
lines.forEach((lineText, idx) => {
ctx.fillText(lineText, x, y + totalHeight + idx * lineHeight + lineHeight *
0.8);
});
totalHeight += lines.length * lineHeight;
if (pIdx < paragraphs.length - 1) {
totalHeight += lineHeight * 0.18; // 段落间距
}
});
return totalHeight;
}
function measureLineCount(text, maxWidth, ctx, lineHeight) {
const paragraphs = text.split(/\r?\n/);
let totalLines = 0;
paragraphs.forEach(para => {
const chars = para.split('');
let line = '';
for (let i = 0; i < chars.length; i++) {
const testLine = line + chars[i];
if (ctx.measureText(testLine).width > maxWidth && line.length > 0) {
totalLines++;
line = chars[i];
} else {
line = testLine;
}
}
if (line.length > 0) totalLines++;
totalLines += 0.18;
});
return totalLines > 0 ? totalLines - 0.18 : 0;
}
function roundRect(ctx, x, y, width, height, radius, fill, stroke) {
if (typeof radius === 'number') {
radius = {
tl: radius,
tr: radius,
br: radius,
bl: radius
};
} else {
const defaultRadius = {
tl: 0,
tr: 0,
br: 0,
bl: 0
};
for (let side in defaultRadius) {
radius[side] = radius[side] || defaultRadius[side];
}
}
ctx.beginPath();
ctx.moveTo(x + radius.tl, y);
ctx.lineTo(x + width - radius.tr, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius.tr);
ctx.lineTo(x + width, y + height - radius.br);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius.br, y + height);
ctx.lineTo(x + radius.bl, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius.bl);
ctx.lineTo(x, y + radius.tl);
ctx.quadraticCurveTo(x, y, x + radius.tl, y);
ctx.closePath();
if (fill) ctx.fill();
if (stroke) ctx.stroke();
}
function renderArticle(jsonData) {
const padding = 20;
const canvasWidth = canvas.width;
const titleFont = "bold 24px sans-serif";
const authorFont = "14px sans-serif";
const sectionTitleFont = "bold 18px sans-serif";
const textFont = "16px sans-serif";
const lineHeight = 24;
const contentWidth = canvasWidth - padding * 2;
const boxPaddingVertical = 15; // 内上下 padding
const boxPaddingHorizontal = 12; // 内左右 padding
let y = padding + 15; // 顶部空15px
const measureCtx = document.createElement('canvas').getContext('2d');
measureCtx.font = titleFont;
y += measureLineCount(jsonData.title, contentWidth, measureCtx, lineHeight) * lineHeight + 10;
measureCtx.font = authorFont;
y += measureLineCount(jsonData.author, contentWidth, measureCtx, lineHeight) * lineHeight + 20;
jsonData.content.forEach((block, idx) => {
measureCtx.font = sectionTitleFont;
y += measureLineCount(block.sectionTitle, contentWidth, measureCtx, lineHeight) *
lineHeight;;
y += 5;
measureCtx.font = textFont;
const textHeight = measureLineCount(block.text, contentWidth - boxPaddingHorizontal * 2,
measureCtx, lineHeight) * lineHeight;
y += textHeight + boxPaddingVertical * 2;
if (idx !== jsonData.content.length - 1) {
y += 40;
}
});
y += 45;
canvas.height = y;
let canvasHeight = canvas.height;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, canvas.width, canvas.height);
y = padding + 15;
ctx.textAlign = "center";
ctx.font = titleFont;
ctx.fillStyle = "#000";
y += drawText(ctx, jsonData.title, canvasWidth / 2, y, contentWidth, lineHeight, titleFont, "center") +
10;
ctx.font = authorFont;
y += drawText(ctx, jsonData.author, canvasWidth / 2, y, contentWidth, lineHeight, authorFont,
"center") + 20;
ctx.textAlign = "left";
jsonData.content.forEach((block, idx) => {
ctx.font = sectionTitleFont;
ctx.fillStyle = "#000";
const sectionTitleHeight = drawText(ctx, block.sectionTitle, padding, y, contentWidth,
lineHeight, sectionTitleFont);
y += sectionTitleHeight;
y += 5;
const boxX = padding;
const boxY = y;
const boxWidth = contentWidth;
ctx.font = textFont;
const textLineCount = measureLineCount(block.text, contentWidth - boxPaddingHorizontal * 2,
ctx);
const textHeight = textLineCount * lineHeight;
const boxHeight = textHeight + boxPaddingVertical * 2;
ctx.fillStyle = "#FEFEFE";
roundRect(ctx, boxX, boxY, boxWidth, boxHeight, 10, true, false);
ctx.strokeStyle = "#ddd";
ctx.lineWidth = 1;
roundRect(ctx, boxX, boxY, boxWidth, boxHeight, 10, false, true);
ctx.fillStyle = "#212121";
y = boxY + boxPaddingVertical;
y += drawText(ctx, block.text, boxX + boxPaddingHorizontal, y, boxWidth -
boxPaddingHorizontal * 2, lineHeight, textFont);
y = boxY + boxHeight;
if (idx !== jsonData.content.length - 1) y += 40;
});
drawWatermark(ctx, canvasWidth, canvasHeight);
}
renderArticle(jsonData);
return canvas.toDataURL("image/png");
}
window.onload = function () {
const tangmianData = jsonData;
const tangdiData = jsonData;
const shareDiv = document.createElement('div');
shareDiv.className = 'share-container';
shareDiv.style.display = 'flex';
shareDiv.style.marginTop = '20px';
shareDiv.style.justifyContent = 'center';
shareDiv.style.gap = '15px';
shareDiv.innerHTML = `
<` + `span>分享<` + `/span>
<` + `a style="color:#059737" href="${getBase64(tangmianData)}" download="汤面.png">下载汤面<` + `/a>
<` + `a style="color:#059737" href="${getBase64(tangdiData)}" download="汤底.png">下载汤底<` + `/a>
`;
document.body.appendChild(shareDiv);
};
</script>
</body>
</html>