Ask and Learn

自动化:借助 AI 为 Pixilart 网站创建二维码分享卡片

在之前的文章 自动化:小书签已经不够用了,上油猴脚本! 中有提到过使用油猴脚本生成网页的二维码分享卡片的方法,但是没有给出具体的细节。

之前是为了方便远程工作者公众号的文章便捷的插入二维码,使用之后,的确是提升了不少效率。我在老狗拾光里面分享像素美图的时候,其实也有用到类似的二维码卡片。

Image

相比远程工作信息,这个卡片里面,还加入了作者的头像,更加复杂,通过 pixso 手工制作的时候,也是要来来回回复制粘贴信息,光切换标签页就十几次,非常耗时。

Image

为了犯懒,于是利用周末,借助 AI 帮我也写了一个适用于像素图的油猴脚本,本文分享一下调教的过程。

我使用的是 VSCode + Github Copilot,当然其他的工具也是完全可以的。

1. 从 Tampermonkey 创建脚本模板

在要修改的网页上没点击 Tampermonkey 图标,然后选择创建新脚本。

Image

它会生成一个新的脚本框架,修改名称(方便查找)和要匹配的网页(改为按照前缀匹配)后保存。然后打开 vscode,创建一个 js 文件,将模板的内容复制进去。

Image

2. 打开 Github Copilot Chat,设置为 agent 模式,然后就正式开始调教了。

Image

3. 在页面中新增按钮

Image

我的预期是在这个位置加入一个按钮,于是审查网页元素后,给出了下面的提示词:

这是一个 userscript, 页面加载后,在 .btn-wrapper 里追加一个按钮 “QRCode”.

相关的 html 结构如下。

 <div class="stats-wrapper mt-3">
   <div class="d-flex align-items-center justify-content-between">
   <div class="full"></div>
   <div class="btn-wrapper d-flex align-items-center justify-content-between">
     <button data-toggle="tooltip" data-placement="top" title="Replay" class="btn btn-light bt-light-dk mr-1"
       style="display: none;"><i aria-hidden="true" class="fa fa-play-circle"></i></button>
     <div class="btn btn-light bt-light-dk mr-1" data-toggle="tooltip" data-placement="top" title=""
        data-original-title="Repost"><i class="ft ft-icon-repeat"></i> <!----></div> <button title="L to like"
         class="btn btn-light bt-light-dk nowrap"><span><span><i aria-hidden="true"
              class="fa fa-heart color-red mr-1"></i> Liked
          </span></span></button>
    </div>
  </div>
</div>

这里给出代码片段式为了让 AI 参考,然后复制按钮的样式,AI 很快给出了修改的代码,我把它复制粘贴到 Tampermonkey 中(以后每一次修改后都需要这么做)之后,刷新页面,按钮并没有出现。

原因是这个网页是动态构建的,脚本生成的代码则是监听页面载入完成事件来执行插入动作,并不能生效。

于是我改变了思路,在鼠标移动到侧边栏区域后再插入。

Image

于是补充了提示词。

引入 jquery,页面是动态加载的,目前的脚本不能正常工作。换一个方案。

当检测到 mouse over 事件在 #art-preview-sidebar 时,再去增加按钮,注意不要添加重复了。

重新执行后,当鼠标移动到侧边栏,按钮成功插入了。

Image

4. 点击按钮后再弹出窗口中绘制画板

按钮点击后,打开一个弹出窗口,在弹出窗口绘制一个尺寸为 1198x234 的 canvas,背景色位 #445271,圆角为 10

在 canvas 左侧绘制用户的头像图片,取自 #art-preview-sidebar 下的第一个 .profile-image

头像距离 canvas 左侧 15 像素,垂直方向居中。

因为我再 pixso 中有个一个简单的卡片设计,所以这些样式信息可以从工具中拿到。

AI 很快给出了方案,我本来预期是在弹出的浏览器窗口中绘制卡片,没想到它直接当前页面做了个对话框,算是意外之喜。

其实这一步,AI 还额外制作了一个图片下载按钮,不过我用不到,就把它删除了。

除此之外,AI 还有个理解错误的地方,就是它寻找头像时,找错了元素,导致绘制失败,还针对找不到的情况做了错误处理和日志,不过我自用的,不需要考虑这些情况,补充了提示词之后,才得以修正。

.profile-image 自身就是 img 标签,无需考虑 profile image 找不到的情况

更新后,效果如下图。

Image

5. 绘制二维码

画板右侧绘制与头像堆成的二维码。

二维码使用如下服务。

https://api.qrserver.com/v1/create-qr-code/?size=${qrSize}x${qrSize}&data=${encodeURIComponent(url)}

这一步和上一步差别不大,AI 很快做好,没有需要修正的地方。

Image

6. 绘制卡片文字

中部从上到下,显示三行文字。

第一行:标题,取自 .image-title-desc h2 的文本,字体颜色为 #A0BDFE 大小为 18 像素 第二行:作者名字,取自 #art-preview-sidebar .username-username 字体颜色为 #E0E0E0 大小为 14 像素 第三行:网页 url, 颜色为 #B8B8B8 大小为 12 像素

这三行文字,如果超长就自动截断,截断的地方显示省略号,文字与两侧的图片中间有 15 像素的留白。

出来的效果有点问题,字太小了,应该是我计算的问题。

Image

于是做了调整。

文字的字体全部增加一倍

其实 AI 生成的结果,文字位置稍微有点奇怪,我就自己手动调整了,最终效果如下。

Image

到这里其实就已经结束了,但是我又更近了一步。

AI 生成的代码,虽然能够工作,但是非常难以维护,方法和变量定义乱成一团,一个方法里面,又去嵌套定义另一个方法。为了方便以后继续修改,我让它自己重构了一下。

重构 createPopupWithCanvas

  1. 将各处逻辑,提取到独立的方法中,不要都在本方法中定义
  2. 用到的常量,在外层定义,方便各个方法引用。

最终得到相对干净整洁的代码。

// ==UserScript==
// @name         Pixilart - 生成分享图
// @namespace    http://tampermonkey.net/
// @version      2025-05-18
// @description  try to take over the world!
// @author       You
// @match        https://www.pixilart.com/art/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=pixilart.com
// @grant        none
// @require      https://code.jquery.com/jquery-3.6.0.min.js
// ==/UserScript==
(function() {
    'use strict';
    
    // Constants for canvas dimensions and styling
    const CANVAS = {
        originalWidth: 1198,
        originalHeight: 234,
        backgroundColor: '#445271',
        cornerRadius: 10
    };
    
    // Constants for image positioning
    const IMAGE = {
        profileSize: 180,
        leftMargin: 20,
        qrSize: 180,
        qrRightMargin: 20
    };
    
    // Constants for text styling
    const TEXT = {
        title: {
            fontSize: 36,
            color: '#A0BDFE',
            y: 40
        },
        author: {
            fontSize: 28,
            color: '#E0E0E0',
            y: 90
        },
        url: {
            fontSize: 28,
            color: '#B8B8B8',
            y: 170
        },
        sidePadding: 15 // Padding on both sides of text
    };
    
    // Function to create the popup with canvas
    function createPopupWithCanvas() {
        const $overlay = createOverlay();
        const $popup = createPopupContainer($overlay);
        createCloseButton($popup, $overlay);
        
        const $canvasContainer = createCanvasContainer($popup);
        const canvas = createCanvas($canvasContainer);
        const ctx = canvas.getContext('2d');
        
        // Draw background
        drawBackground(ctx, canvas);
        
        // Get content for drawing
        const content = getContentFromPage();
        
        // Calculate positions
        const positions = calculatePositions(canvas);
        
        // Draw each component
        drawProfileImage(ctx, content.profileImageSrc, positions);
        drawQRCode(ctx, content.url, positions);
        drawText(ctx, content, positions);
    }
    
    // Create the dark overlay that covers the screen
    function createOverlay() {
        return $('<div>')
            .css({
                'position': 'fixed',
                'top': '0',
                'left': '0',
                'width': '100%',
                'height': '100%',
                'background-color': 'rgba(0,0,0,0.7)',
                'z-index': '10000',
                'display': 'flex',
                'justify-content': 'center',
                'align-items': 'center'
            })
            .appendTo('body');
    }
    
    // Create the white popup container
    function createPopupContainer($overlay) {
        return $('<div>')
            .css({
                'background-color': 'white',
                'padding': '20px',
                'border-radius': '5px',
                'max-width': '90%',
                'max-height': '90%',
                'position': 'relative'
            })
            .appendTo($overlay);
    }
    
    // Create close button for the popup
    function createCloseButton($popup, $overlay) {
        $('<button>')
            .text('×')
            .css({
                'position': 'absolute',
                'top': '5px',
                'right': '10px',
                'background': 'none',
                'border': 'none',
                'font-size': '24px',
                'cursor': 'pointer'
            })
            .on('click', function() {
                $overlay.remove();
            })
            .appendTo($popup);
    }
    
    // Create container for the canvas
    function createCanvasContainer($popup) {
        return $('<div>')
            .css({
                'margin-top': '20px'
            })
            .appendTo($popup);
    }
    
    // Create and setup the canvas
    function createCanvas($container) {
        const canvas = document.createElement('canvas');
        
        // Set canvas to original resolution but display at half size
        canvas.width = CANVAS.originalWidth;
        canvas.height = CANVAS.originalHeight;
        canvas.style.width = (CANVAS.originalWidth / 2) + 'px';
        canvas.style.height = (CANVAS.originalHeight / 2) + 'px';
        
        $container.append(canvas);
        return canvas;
    }
    
    // Draw the background with rounded corners
    function drawBackground(ctx, canvas) {
        ctx.fillStyle = CANVAS.backgroundColor;
        roundRect(ctx, 0, 0, canvas.width, canvas.height, CANVAS.cornerRadius, true);
    }
    
    // Get content from the page needed for drawing
    function getContentFromPage() {
        // Remove query string from URL
        return {
            title: $('.image-title-desc h2').text().trim(),
            author: $('#art-preview-sidebar .username-username').first().text().trim(),
            url: window.location.href.replace(/\?.*$/, ''),
            profileImageSrc: $('#art-preview-sidebar .profile-image').first().attr('src')
        };
    }
    
    // Calculate positions for all elements
    function calculatePositions(canvas) {
        const verticalPosition = (canvas.height - IMAGE.profileSize) / 2; // Vertically centered
        const qrX = canvas.width - IMAGE.qrSize - IMAGE.qrRightMargin;
        const qrY = verticalPosition;
        
        return {
            verticalPosition: verticalPosition,
            profileLeft: IMAGE.leftMargin,
            qrX: qrX,
            qrY: qrY,
            textStart: IMAGE.leftMargin + IMAGE.profileSize + TEXT.sidePadding,
            textWidth: qrX - (IMAGE.leftMargin + IMAGE.profileSize) - (TEXT.sidePadding * 2)
        };
    }
    
    // Draw the profile image as a circle
    function drawProfileImage(ctx, profileImageSrc, positions) {
        // Create image object for profile picture
        const profileImg = new Image();
        profileImg.crossOrigin = "anonymous"; // Try to avoid CORS issues
        
        // Set up image load handler
        profileImg.onload = function() {
            // Draw profile image
            ctx.save();
            
            // Create circular clipping path for round profile image
            ctx.beginPath();
            const radius = IMAGE.profileSize / 2;
            const centerX = positions.profileLeft + radius;
            const centerY = positions.verticalPosition + radius;
            ctx.arc(centerX, centerY, radius, 0, Math.PI * 2, true);
            ctx.closePath();
            ctx.clip();
            
            // Draw the image in the clipped circle
            ctx.drawImage(profileImg, positions.profileLeft, positions.verticalPosition, IMAGE.profileSize, IMAGE.profileSize);
            
            // Restore context
            ctx.restore();
        };
        
        // Set image source to start loading
        profileImg.src = profileImageSrc;
    }
    
    // Draw the QR code
    function drawQRCode(ctx, url, positions) {
        // Create QR code image
        const qrImg = new Image();
        qrImg.crossOrigin = "anonymous";
        
        // Set up image load handler
        qrImg.onload = function() {
            // Draw QR code image
            ctx.drawImage(qrImg, positions.qrX, positions.qrY, IMAGE.qrSize, IMAGE.qrSize);            
        };
        
        // Set QR code image source
        const qrCodeUrl = `https://api.qrserver.com/v1/create-qr-code/?size=${IMAGE.qrSize}x${IMAGE.qrSize}&data=${encodeURIComponent(url)}`;
        qrImg.src = qrCodeUrl;
    }
    
    // Draw the text in the middle
    function drawText(ctx, content, positions) {
        // Set up text properties
        ctx.textBaseline = 'top';
        
        // Title - First line
        ctx.font = `bold ${TEXT.title.fontSize}px Arial`;
        ctx.fillStyle = TEXT.title.color;
        drawTruncatedText(ctx, content.title, positions.textStart, TEXT.title.y, positions.textWidth);
        
        // Author - Second line
        ctx.font = `${TEXT.author.fontSize}px Arial`;
        ctx.fillStyle = TEXT.author.color;
        drawTruncatedText(ctx, content.author, positions.textStart, TEXT.author.y, positions.textWidth);
        
        // URL - Third line
        ctx.font = `${TEXT.url.fontSize}px Arial`;
        ctx.fillStyle = TEXT.url.color;
        drawTruncatedText(ctx, content.url, positions.textStart, TEXT.url.y, positions.textWidth);
    }
    
    // Helper function to draw truncated text with ellipsis
    function drawTruncatedText(ctx, text, x, y, maxWidth) {
        let truncated = text;
        let textWidth = ctx.measureText(text).width;
        
        // Check if text needs truncation
        if (textWidth > maxWidth) {
            let ellipsis = '...';
            let ellipsisWidth = ctx.measureText(ellipsis).width;
            
            // Keep removing characters until it fits with ellipsis
            while (textWidth + ellipsisWidth > maxWidth && truncated.length > 0) {
                truncated = truncated.slice(0, -1);
                textWidth = ctx.measureText(truncated).width;
            }
            
            truncated += ellipsis;
        }
        
        ctx.fillText(truncated, x, y);
    }
    
    // Helper function to draw rounded rectangle
    function roundRect(ctx, x, y, width, height, radius, fill) {
        ctx.beginPath();
        ctx.moveTo(x + radius, y);
        ctx.lineTo(x + width - radius, y);
        ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
        ctx.lineTo(x + width, y + height - radius);
        ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
        ctx.lineTo(x + radius, y + height);
        ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
        ctx.lineTo(x, y + radius);
        ctx.quadraticCurveTo(x, y, x + radius, y);
        ctx.closePath();
        if (fill) {
            ctx.fill();
        } else {
            ctx.stroke();
        }
    }
    
    // Function to add the QRCode button if it doesn't exist
    function addQRCodeButton() {
        // Find the .btn-wrapper element
        const $btnWrapper = $('.btn-wrapper');
        
        if ($btnWrapper.length && !$btnWrapper.find('.qrcode-btn').length) {
            // Create a new QRCode button using jQuery
            const $qrCodeBtn = $('<button>')
                .addClass('btn btn-light bt-light-dk mr-1 qrcode-btn')
                .attr({
                    'data-toggle': 'tooltip',
                    'data-placement': 'top',
                    'title': 'Generate QR Code'
                })
                .text('QRCode')
                .on('click', function() {
                    createPopupWithCanvas();
                });
            
            // Append the button to the .btn-wrapper
            $btnWrapper.prepend($qrCodeBtn);
            // Initialize tooltip (if Bootstrap is available)
            if ($.fn.tooltip) {
                $('[data-toggle="tooltip"]').tooltip();
            }
            
            console.log('QRCode button added successfully!');
        }
    }
    // Initialize when document is ready
    $(document).ready(function() {
        // Add hover event listener to #art-preview-sidebar
        $(document).on('mouseover', '#art-preview-sidebar', function() {
            // Add the QRCode button when hovering
            addQRCodeButton();
        });
    });
})();

其实也可以直接把设计图喂给 AI,它一次去生成,但是一次做的事情多了,出错了就更加混乱,不知道哪里改起,不如这样步步为营的方式稳妥,一次做一小步,AI 理解起来比较容易,犯错也更少一些,也更有 “创造” 的乐趣。

总体感觉,在 AI 的加持下,创建各种简单的小工具,简直得心应手。


文章同步发表于微信公众号老狗拾光,欢迎关注。

https://mp.weixin.qq.com/s/7sC5BlyTyt110l_wxr0yaw

微信公众号老狗拾光