🚀 2026新春限定 比比工房5重豪礼来袭!现金红包+免费插件+高价值抽奖,速进群抢福利!活动详情 →

[作品投稿]红薯笔记生成器
Moon-Chaser
Moon-Chaser
发表于3 周前 浏览 120

Moon-Chaser
Moon-Chaser
回复于3 周前
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>红薯笔记生成器</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      font-family: 'MiSans', sans-serif;
      background-color: #f4f5f7;
      display: flex;
      flex-direction: row;
      height: 100vh;
      overflow: hidden;
    }
    .pane {
      flex: 1;
      display: flex;
      flex-direction: column;
      padding: 20px;
      overflow: auto;
    }
    #editorPane {
      background: #fff;
      border-right: 1px solid #eee;
    }
    #editorPane h2, #previewPane h2 {
      font-size: 18px;
      font-weight: 600;
      margin-bottom: 10px;
    }
    #editorPane textarea {
      flex: 1;
      resize: none;
      padding: 12px;
      font-size: 14px;
      line-height: 1.6;
      border: 1px solid #ddd;
      border-radius: 8px;
      width: 100%;
      min-height: 200px;
      background-color: #fafafa;
    }

    .mode-container.desktop-only {
      display: block;
    }

    .mode-grid {
      display: flex;
      flex-direction: row;
      gap: 12px;
      overflow-x: auto;
      padding: 12px 0;
      scroll-snap-type: x mandatory;
      white-space: nowrap;
    }
    .mode-card {
      flex: 0 0 auto;
      scroll-snap-align: start;
      display: inline-block;
      min-width: 120px;
      max-width: 160px;
      cursor: pointer;
      border: 2px solid transparent;
      border-radius: 10px;
      background-color: #fff;
      box-shadow: 0 1px 5px rgba(0,0,0,0.05);
      text-align: center;
      transition: all 0.2s ease;
      padding: 14px 10px;
      font-size: 14px;
      font-weight: 500;
      color: #333;
      user-select: none;
    }
    .mode-card.active {
      border: 2px solid #007aff;
      background-color: #e6f0ff;
      color: #007aff;
    }

    .controls {
      margin-top: 12px;
      display: flex;
      flex-wrap: wrap;
      gap: 10px;
      align-items: center;
    }
    .controls input, .controls button, .controls select {
      padding: 8px 12px;
      font-size: 14px;
      border: 1px solid #ccc;
      border-radius: 6px;
      background: #fff;
      outline: none;
      transition: all 0.2s ease;
    }
    .controls button {
      background-color: #007aff;
      color: #fff;
      border: none;
      cursor: pointer;
    }
    .controls button:hover {
      background-color: #005fcc;
    }

    #previewPane {
      background-color: #f9fafb;
    }
    #previewContainer {
      display: flex;
      flex-wrap: wrap;
      gap: 16px;
      justify-content: center;
    }
    #previewContainer img {
      width: 360px;
      height: 480px;
      object-fit: cover;
      border-radius: 12px;
      box-shadow: 0 2px 12px rgba(0,0,0,0.08);
    }
    canvas.hidden-canvas {
      display: none;
    }

    .desktop-only { display: block; }
    .mobile-only { display: none; }

    @media (max-width: 768px) {
      body { flex-direction: column; }
      #editorPane, #previewPane {
        flex: none;
        height: auto;
        max-height: 50%;
      }
      #previewContainer img {
        width: 90vw;
        height: auto;
      }
      .desktop-only { display: none; }
      .mobile-only {
        display: inline-block;
        min-width: 120px;
        max-width: 100%;
        flex: 1;
      }
    }
  </style>
</head>
<body>
  <div id="editorPane" class="pane">
    <h2>编辑文案 (粘贴)</h2>
    <textarea id="markdownInput" placeholder="在此输入文案..."></textarea>

    <div class="mode-container desktop-only">
      <div class="mode-grid" id="modeGrid">
        <div class="mode-card active" data-mode="light">白色备忘录</div>
        <div class="mode-card" data-mode="dark">黑色备忘录</div>
        <div class="mode-card" data-mode="bg1">新背景1</div>
        <div class="mode-card" data-mode="bg2">新背景2</div>
        <div class="mode-card" data-mode="bg3">新背景3</div>
        <div class="mode-card" data-mode="bg4">新背景4</div>
        <div class="mode-card" data-mode="bg5">新背景5</div>
        <div class="mode-card" data-mode="bg6">新背景6</div>
      </div>
    </div>

    <div class="controls">
      <select id="modeSelect" class="mobile-only">
        <option value="light">白色备忘录</option>
        <option value="dark">黑色备忘录</option>
        <option value="bg1">新背景1</option>
        <option value="bg2">新背景2</option>
        <option value="bg3">新背景3</option>
        <option value="bg4">新背景4</option>
        <option value="bg5">新背景5</option>
        <option value="bg6">新背景6</option>
      </select>
      <input id="titleSize" type="number" min="20" max="80" value="80" title="标题字号" />
      <button id="genBtn">生成图片</button>
      <button id="downloadBtn">下载全部</button>
    </div>
  </div>

  <div id="previewPane" class="pane">
    <h2>生成预览</h2>
    <div id="previewContainer"></div>
    <canvas id="canvas" width="1080" height="1440" class="hidden-canvas"></canvas>
  </div>

  <script src="img/marked.min.js"></script>
  <script>
    const bgImages = {
      light: 'img/bg1.webp',
      dark: 'img/bg2.webp',
      bg1: 'img/bg3.webp',
      bg2: 'img/bg4.webp',
      bg3: 'img/bg5.webp',
      bg4: 'img/bg6.webp',
      bg5: 'img/bg7.webp',
      bg6: 'img/bg8.webp'
    };

    function autoWrapText(ctx, text, maxW) {
      const paras = text.split(/\n|\r\n/), lines = [];
      paras.forEach(para => {
        let line = '';
        [...para].forEach(ch => {
          line += ch;
          if (ctx.measureText(line).width > maxW) {
            lines.push(line.slice(0, -1));
            line = ch;
          }
        });
        if (line) lines.push(line);
      });
      return lines;
    }

    function titleLinesHeight(lines, size) {
      return lines.length * (size + 10);
    }

    function render(pages, titleLines, titleSize, fontColor, backgroundImage) {
      const canvas = document.getElementById('canvas');
      const ctx = canvas.getContext('2d');
      const preview = document.getElementById('previewContainer');
      preview.innerHTML = '';
      const pw = 1080, ph = 1440, mTop = 200, mBottom = 50, tGap = 40, lh = 70, pL = 60;

      pages.forEach((lines, i) => {
        ctx.clearRect(0, 0, pw, ph);
        ctx.drawImage(backgroundImage, 0, 0, pw, ph);
        ctx.fillStyle = fontColor;
        ctx.textAlign = 'left';
        ctx.textBaseline = 'top';
        let y = mTop;

        if (i === 0 && titleLines.length) {
          ctx.font = `bold ${titleSize}px MiSans`;
          titleLines.forEach(l => {
            ctx.fillText(l, pL, y);
            y += titleSize + 10;
          });
          y += tGap;
        }

        ctx.font = `42px MiSans`;
        lines.forEach(l => {
          ctx.fillText(l, pL, y);
          y += lh;
        });

        const img = new Image();
        img.src = canvas.toDataURL();
        preview.appendChild(img);
      });
    }

    function getActiveMode() {
      const modeCard = document.querySelector('.mode-card.active');
      if (modeCard) return modeCard.dataset.mode;
      return document.getElementById('modeSelect').value;
    }

    function generateImages() {
      const md = document.getElementById('markdownInput').value;
      const lines = md.split(/\n/);
      const title = lines[0].replace(/^#+\s*/, '') || '';
      const contentMd = lines.slice(1).join('\n');
      const titleSize = +document.getElementById('titleSize').value;
      const mode = getActiveMode();
      const fontColor = ['dark'].includes(mode) ? '#fff' : '#333';
      const bgUrl = bgImages[mode];

      const canvas = document.getElementById('canvas');
      const ctx = canvas.getContext('2d');
      const pw = 1080, ph = 1440, mTop = 200, mBottom = 50, tGap = 40, lh = 70, cW = 960, pL = 60;

      ctx.font = `42px MiSans`;
      const plain = contentMd.replace(/\*\*(.*?)\*\*/g, '$1').replace(/\*(.*?)\*/g, '$1');
      const contentLines = autoWrapText(ctx, plain, cW);

      let titleLines = [];
      if (title) {
        ctx.font = `bold ${titleSize}px MiSans`;
        titleLines = autoWrapText(ctx, title, cW);
      }

      const firstCnt = Math.floor((ph - mTop - mBottom - titleLinesHeight(titleLines, titleSize) - tGap) / lh);
      const otherCnt = Math.floor((ph - mTop - mBottom) / lh);
      let pages = [];

      if (contentLines.length <= firstCnt) {
        pages = [contentLines];
      } else {
        pages = [contentLines.slice(0, firstCnt)];
        let rem = contentLines.slice(firstCnt);
        while (rem.length) {
          pages.push(rem.slice(0, otherCnt));
          rem = rem.slice(otherCnt);
        }
      }

      const bg = new Image();
      bg.crossOrigin = 'anonymous';
      bg.src = bgUrl;
      bg.onload = () => render(pages, titleLines, titleSize, fontColor, bg);
      if (bg.complete) render(pages, titleLines, titleSize, fontColor, bg);
    }

    function downloadAllImages() {
      document.querySelectorAll('#previewContainer img').forEach((img, i) => {
        const a = document.createElement('a');
        a.href = img.src;
        a.download = `memo_${i + 1}.png`;
        a.click();
      });
    }

    document.querySelectorAll('.mode-card').forEach(card => {
      card.addEventListener('click', () => {
        document.querySelectorAll('.mode-card').forEach(c => c.classList.remove('active'));
        card.classList.add('active');
        document.getElementById('modeSelect').value = card.dataset.mode;
        generateImages();
      });
    });

    document.getElementById('modeSelect').addEventListener('change', () => {
      const val = document.getElementById('modeSelect').value;
      document.querySelectorAll('.mode-card').forEach(c => {
        c.classList.toggle('active', c.dataset.mode === val);
      });
      generateImages();
    });

    document.getElementById("genBtn").onclick = generateImages;
    document.getElementById("downloadBtn").onclick = downloadAllImages;
  </script>
</body>
</html>

哎呀,回复话题必需登录。 还没账号?请先注册
  • 首页
  • 文件
  • 活动