在之前的文章 自动化:小书签已经不够用了,上油猴脚本! 中有提到过使用油猴脚本生成网页的二维码分享卡片的方法,但是没有给出具体的细节。
之前是为了方便远程工作者公众号的文章便捷的插入二维码,使用之后,的确是提升了不少效率。我在老狗拾光里面分享像素美图的时候,其实也有用到类似的二维码卡片。
相比远程工作信息,这个卡片里面,还加入了作者的头像,更加复杂,通过 pixso 手工制作的时候,也是要来来回回复制粘贴信息,光切换标签页就十几次,非常耗时。
为了犯懒,于是利用周末,借助 AI 帮我也写了一个适用于像素图的油猴脚本,本文分享一下调教的过程。
我使用的是 VSCode + Github Copilot,当然其他的工具也是完全可以的。
1. 从 Tampermonkey 创建脚本模板
在要修改的网页上没点击 Tampermonkey 图标,然后选择创建新脚本。
它会生成一个新的脚本框架,修改名称(方便查找)和要匹配的网页(改为按照前缀匹配)后保存。然后打开 vscode,创建一个 js 文件,将模板的内容复制进去。
2. 打开 Github Copilot Chat,设置为 agent 模式,然后就正式开始调教了。
3. 在页面中新增按钮
我的预期是在这个位置加入一个按钮,于是审查网页元素后,给出了下面的提示词:
这是一个 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 中(以后每一次修改后都需要这么做)之后,刷新页面,按钮并没有出现。
原因是这个网页是动态构建的,脚本生成的代码则是监听页面载入完成事件来执行插入动作,并不能生效。
于是我改变了思路,在鼠标移动到侧边栏区域后再插入。
于是补充了提示词。
引入 jquery,页面是动态加载的,目前的脚本不能正常工作。换一个方案。
当检测到 mouse over 事件在 #art-preview-sidebar 时,再去增加按钮,注意不要添加重复了。
重新执行后,当鼠标移动到侧边栏,按钮成功插入了。
4. 点击按钮后再弹出窗口中绘制画板
按钮点击后,打开一个弹出窗口,在弹出窗口绘制一个尺寸为 1198x234 的 canvas,背景色位 #445271,圆角为 10
在 canvas 左侧绘制用户的头像图片,取自 #art-preview-sidebar 下的第一个 .profile-image
头像距离 canvas 左侧 15 像素,垂直方向居中。
因为我再 pixso 中有个一个简单的卡片设计,所以这些样式信息可以从工具中拿到。
AI 很快给出了方案,我本来预期是在弹出的浏览器窗口中绘制卡片,没想到它直接当前页面做了个对话框,算是意外之喜。
其实这一步,AI 还额外制作了一个图片下载按钮,不过我用不到,就把它删除了。
除此之外,AI 还有个理解错误的地方,就是它寻找头像时,找错了元素,导致绘制失败,还针对找不到的情况做了错误处理和日志,不过我自用的,不需要考虑这些情况,补充了提示词之后,才得以修正。
.profile-image 自身就是 img 标签,无需考虑 profile image 找不到的情况
更新后,效果如下图。
5. 绘制二维码
画板右侧绘制与头像堆成的二维码。
二维码使用如下服务。
https://api.qrserver.com/v1/create-qr-code/?size=${qrSize}x${qrSize}&data=${encodeURIComponent(url)}
这一步和上一步差别不大,AI 很快做好,没有需要修正的地方。
6. 绘制卡片文字
中部从上到下,显示三行文字。
第一行:标题,取自 .image-title-desc h2 的文本,字体颜色为 #A0BDFE 大小为 18 像素 第二行:作者名字,取自 #art-preview-sidebar .username-username 字体颜色为 #E0E0E0 大小为 14 像素 第三行:网页 url, 颜色为 #B8B8B8 大小为 12 像素
这三行文字,如果超长就自动截断,截断的地方显示省略号,文字与两侧的图片中间有 15 像素的留白。
出来的效果有点问题,字太小了,应该是我计算的问题。
于是做了调整。
文字的字体全部增加一倍
其实 AI 生成的结果,文字位置稍微有点奇怪,我就自己手动调整了,最终效果如下。
到这里其实就已经结束了,但是我又更近了一步。
AI 生成的代码,虽然能够工作,但是非常难以维护,方法和变量定义乱成一团,一个方法里面,又去嵌套定义另一个方法。为了方便以后继续修改,我让它自己重构了一下。
重构 createPopupWithCanvas
- 将各处逻辑,提取到独立的方法中,不要都在本方法中定义
- 用到的常量,在外层定义,方便各个方法引用。
最终得到相对干净整洁的代码。
// ==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