前言
话不多说,先看效果!(在每一个网站打开时,都会送你一副新春联~ 春联会在 10s / 按ESC
后消失~)
需求
不知不觉的,一年又过去了,进入新年必不可少的是什么?那必然是贴春联!
虽然人在漂泊,家里的春联还没贴。但是我们可以先把我们每天访问的网站贴上春联呀!
接下来,让我带大家一步一步的做一个电脑贴春联脚本,在我们的浏览器中用起来吧~
需求分析
- 搭架子:将春联的架子搭起来
- 找福字:从公网找一个好看的福
- 改样式:根据我们的福字,适配我们的样式
- 加动画:根据样式做个好看的、合适的动画
- 拿数据:通过 AIGC 获取春联的内容
- 做适配:根据我们的代码适配到油猴脚本
开发
一步一步开始做呀,快乐的编码~
搭架子
我这里就不限技术栈,直接原生开始做了。
这里分两步做,第一步先把框子搭起来,第二步在框子里把上联、下联、横批渲染出来。
搭容器
由于我们后续要添加到油猴脚本中,因此我们的架子不能在 HTML 中,而是应该全部通过 JS 创建。
// 创建容器 DOM
function createContainer() {
const el = document.createElement("div");
el.className = "couplet-container";
return container;
}
// 放到 Loaded 回调里,避免由于 body 获取不到出现的 error
document.addEventListener("DOMContentLoaded", () => {
const coupletContainer = createContainer();
document.body.append(coupletContainer);
});
由于我们需要保证定位准确,因此通过 fixed + window size
固定将容器配置为覆盖整个页面,再通过一个极高的 z-index
保证我们的容器始终处在页面顶层。
同时我们还需要选一个字体,这里选择了没有那么规整的 楷体。
最开始是准备网络上下载一个艺术字,但考虑了电脑适配问题。从
windows
默认字体中选出了一个相对更符合春联风格的 楷体 。
.couplet-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
font-family: "楷体", "宋体", "Microsoft YaHei", sans-serif;
font-size: 32px;
font-weight: 700;
z-index: 1000000000;
}
上下联渲染
创建上下联和创建容器的方式基本保持一致,不同的是他们需要通过 innerHTML
插入对联的具体内容。
同时,我们创建后不再将 Element
插入到 body
中了,转而插入至 coupletContainer
中。
function createUp(str) {
const upEl = document.createElement("div");
upEl.className = "couplet-all couplet-up";
upEl.innerHTML = str;
return upEl;
}
function createDown(str) {
const downEl = document.createElement("div");
downEl.className = "couplet-all couplet-down";
downEl.innerHTML = str;
return downEl;
}
function createTop(str) {
const topEl = document.createElement("div");
topEl.innerHTML = str;
topEl.className = "couplet-all couplet-top";
return topEl;
}
document.addEventListener("DOMContentLoaded", () => {
const coupletContainer = createContainer();
const top = createTop("龍龍龍龍");
const up = createUp("龍龍龍龍龍龍龍");
const down = createDown("龍龍龍龍龍龍龍");
coupletContainer.append(up);
coupletContainer.append(down);
coupletContainer.append(top);
document.body.append(coupletContainer);
});
我没有采用 FlexBox
的方式来固定元素位置。因为我们现在的需求明显采用 absolute + transfrom
实现居中更合适一点。
同时我们分为两种文字方式,一种是横批(左 -> 右、右 -> 左),还有一种是上下联的纵向文字。纵向文字采用了 writing-mode: vertical-rl
来实现。
同时我们提取了公共的样式到 couplet-all
,其他各自需要的属性再基于权重进行覆盖。
没有最好的方法,面对不同的需求,我们应该综合考虑采用合理的方案。而不是
Flex
或者Grid
一把梭。虽然我并不认同面试应该考 居中的 N 种方法 ,但我们了解各类居中的实现方案,也更方便我们在需要使用时,快速选择对应方案。
.couplet-all {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: auto;
height: 300px;
padding: 32px 24px;
background-color: #ca3a2a;
text-align: center;
writing-mode: vertical-rl;
}
.couplet-top {
top: 5%;
left: 50%;
transform: translateX(-50%);
width: 200px;
height: auto;
padding: 16px 24px;
text-align: center;
writing-mode: horizontal-tb;
}
.couplet-up {
right: 5%;
}
.couplet-down {
left: 5%;
}
看看效果吧~
好像有点丑~ 没事儿,接下来我们一步一步来。
找福字
想到福字,我第一个想到的就是 阿里矢量图标库 ,因为我们是一个 script
脚本,我并不希望在其中再添加一些网络上的图片,最好是 svg
直接绘制出来的福字。
在一众的图标寻觅中,我最终选定了这一款。简单、干净、且没有复杂的样式。
大家可以前往 阿里矢量图标库 寻找自己喜欢的图标。
我们复制它的 svg
代码,然后插入到页面中。
function createFu() {
const fu = document.createElement("div");
fu.className = "couplet-fu";
fu.innerHTML =
'<svg">...</svg>'; // svg 太长啦~ 这里就不给大家完全放上啦,后续会贴出全部代码的码上掘金
return fu;
}
document.addEventListener("DOMContentLoaded", () => {
// ...
const fu = createFu();
coupletContainer.append(fu);
// ...
});
这里我们福字采用了 FlexBox
实现的居中。这么做的原因是,我打算后续添加动画,且福字的动画打算通过旋转实现特效, transfrom
会影响到我们的动画设计。
.couplet-fu {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
看看效果吧~
改样式
有了福字,我们的对联的样式就显得捉襟见肘了。我们便基于福字实现对联的样式吧~
我们需要调整的样式:
- 圆角
- 外边淡红色的边框
- 内部的轮廓框
- 文字的颜色
这里面比较麻烦的有两部分,分别是:淡红色的外边框、内部轮廓框。
- 淡红色的外边框:由于我们使用了圆角,直接使用
border
会导致border
内部缺少圆角,因此这里采用box-shadow
模拟边框 - 内部的轮廓框:这里采用
::before
伪元素实现内部的轮廓框
.couplet-container {
...
color: #fec401;
}
.couplet-all {
...
box-shadow: 0 0 0 6px #ff664d;
border-radius: 8px;
}
.couplet-all::before {
content: "";
position: absolute;
top: 16px;
left: 10px;
right: 10px;
bottom: 16px;
border: 4px solid #f3dab8;
border-radius: 8px;
}
.couplet-top::before {
top: 10px;
left: 16px;
right: 16px;
bottom: 10px;
}
看看效果吧~
到这步为止,我们的样式就基本完成啦~
加动画
动画分为两种,一种是春联本体的展开动画,另一种是福字的旋转动画。
- 展开动画:由于我们使用了
translateX
,因此如果直接使用width
实现动画,会出现问题。所以采用scale
来实现展开动画 - 旋转动画:旋转动画很简单,我们直接使用
rotate
实现。
注意:
由于我们的translate
的原因,我们必须通过transform-origin: pos
来更改我们的动画出现位置。
@keyframes horizontal-expand {
0% {
transform: scaleX(0) translateX(-50%);
}
100% {
transform: scaleX(1) translateX(-50%);
}
}
@keyframes vertical-expand {
0% {
transform: scaleY(0) translateY(-50%);
}
100% {
transform: scaleY(1) translateY(-50%);
}
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.couplet-top {
transform-origin: left;
animation: horizontal-expand 1s ease-in-out 0s 1 normal forwards;
}
.couplet-up {
transform-origin: top;
animation: vertical-expand 1s ease-in-out 0s 1 normal forwards;
}
.couplet-down {
transform-origin: top;
animation: vertical-expand 1s ease-in-out 0s 1 normal forwards;
}
.couplet-fu {
animation: spin 1s ease;
}
实现效果
到这步为止,我们的基本效果就出来了,接下来就只差数据的获取与油猴的适配了。
拿数据
这里我拿数据做了两种方案:
- 本地数据
- AIGC数据
本地数据
本地数据我们从找 N 副春联,然后通过一个随机数,从春联中随机取出一个,渲染给页面。
具体代码如下:
const coupletList = [
{
up: "龙飞凤舞春盈四海",
down: "岁稔年丰福满九州",
top: "四海同春",
},
{
up: "龙骧盛世财源广进",
down: "岁稔新春福运亨通",
top: "盛世财源",
},
{
up: "龙舞云天开新运财如泉涌",
down: "春回大地展宏图福满人间",
top: "龙运亨通",
},
{
up: "龙年百福临喜气祥和户户",
down: "春风万里送欢歌笑语声声",
top: "迎春接福",
},
{
up: "龙腾盛世展宏图业兴财旺",
down: "春满神州添锦绣福寿安康",
top: "大展鸿图",
},
{
up: "龙年百福临喜气祥和户户",
down: "春风万里送欢歌笑语声声",
top: "迎春接福",
},
{
up: "龙征万里",
down: "福满乾坤",
top: "万里乾坤福",
},
];
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
document.addEventListener("DOMContentLoaded", () => {
const c = coupletList[getRandomInt(0, coupletList.length - 1)];
const top = createTop(c.top);
const up = createUp(c.up);
const down = createDown(c.down);
...
});
AIGC数据
这里主要还是参考了站内文章的:# 🐲龙年大吉——AIGC生成龙年春联🐲
具体细节我这里简单讲讲。
获取免费套餐
进入 讯飞 领取免费套餐,然后进入后台获取 key
。
代码通过 websocket 调取接口
const apikey = '你的 apikey'
const appid = '你的 appid'
const apisecret = '你的 apisecret'
const uid = 'session uid'
const getWebsocketUrl = () => {
const apiKey = apikey;
const apiSecret = apisecret;
const url = "wss://spark-api.xf-yun.com/v3.1/chat";
const host = location.host;
const date = new Date().toGMTString();
const algorithm = "hmac-sha256";
const headers = "host date request-line";
const signatureOrigin = `host: ${host}\ndate: ${date}\nGET /v3.1/chat HTTP/1.1`;
const signatureSha = CryptoJS.HmacSHA256(signatureOrigin, apiSecret);
const signature = CryptoJS.enc.Base64.stringify(signatureSha);
const authorizationOrigin = `api_key="${apiKey}", algorithm="${algorithm}", headers="${headers}", signature="${signature}"`;
const authorization = btoa(authorizationOrigin);
return `${url}?authorization=${authorization}&date=${date}&host=${host}`;
};
const send = async () => {
return new Promise((resolve, reject) => {
const url = getWebsocketUrl();
const instance = new WebSocket(url);
instance.onopen = async () => {
const prompt =
"你是一个春联专家。我希望你给我一个春联,春联分为上下联和横批。请给我一个春联。要求:上下联字数为 7;主题为 龙年;横批中不要出现龙;上联、下联、横批之间用逗号隔开;不要换行;对联中间不要出现标点符号;";
const params = {
header: {
app_id: appid,
uid: uid,
},
parameter: {
chat: {
domain: "generalv3",
temperature: 0.5,
max_tokens: 1024,
},
},
payload: {
message: {
text: [{ role: "user", content: prompt }],
},
},
};
instance.send(JSON.stringify(params));
};
let str = "";
instance.onmessage = (msg) => {
const data = JSON.parse(msg.data);
const status = data.payload.choices.status;
const content = data.payload.choices.text[0].content;
str = str + content;
if (status === 2) {
resolve(str.split(",").map((s) => s.slice(3, -1)));
}
};
setTimeout(() => {
reject();
}, 5000);
});
};
这是我们封装好的 send
方法,然后通过调用 send
方法获取对应的内容。
使用示例:
send()
.then((r) => {
console.log(r)
const top = createTop(r[2]);
const up = createUp(r[0]);
const down = createDown(r[1]);
...
})
做适配
油猴 userScript
- license:开源协议,我一般会选
MIT
,和github
开源协议一致,自己选择即可 - name:脚本名称(AIGC春联)
- namespace:命名空间(
http://tampermonkey.net/
) - version:版本号(
2024-01-26
) - description:描述,写你的脚本是做什么用的
- author:开发者(
Sincenir
) - match:匹配作用域(
*://*/*
全部) - grant:需要使用的接口(
GM_addStyle
) - require:引入的外部包(
https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js
)
CSS 引入转化
CSS 在油猴中不能直接定义,需要通过 GM_addStyle + String
将 CSS 引入。
配置与发布
- 进入 油猴主页
- 注册登录账号
- 点击用户名进入个人主页
- 点击按钮发布你编写的脚本
- 粘贴你的代码
- 写声明(你的脚本的描述)
- 选择脚本类型并发布
这些做完,我们的春联脚本就算是正式完结撒花了~
最终效果
最后
我的脚本已发布至油猴平台(随机版,非 AIGC 版)。
科大讯飞的流量太少了,光测试就用了我 1/10
T T
大家感兴趣可以打开该链接直接下载使用: AIGC春联 (greasyfork.org)
希望大家在新的一年里,能开开心心的,不为工作、生活而苦恼~