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>
除非特别注明,本页内容采用以下授权方式: Creative Commons Attribution-ShareAlike 3.0 License