🐲龙年新气象!AIGC + 油猴 = 在访问网站时,送你一副新春联

1,923 阅读9分钟

前言

话不多说,先看效果!(在每一个网站打开时,都会送你一副新春联~ 春联会在 10s / 按ESC 后消失~)

ezgif-4-ce7a266ab9.gif

油猴地址:AIGC春联 (greasyfork.org)

需求

不知不觉的,一年又过去了,进入新年必不可少的是什么?那必然是贴春联!

虽然人在漂泊,家里的春联还没贴。但是我们可以先把我们每天访问的网站贴上春联呀!

接下来,让我带大家一步一步的做一个电脑贴春联脚本,在我们的浏览器中用起来吧~

需求分析

  1. 搭架子:将春联的架子搭起来
  2. 找福字:从公网找一个好看的福
  3. 改样式:根据我们的福字,适配我们的样式
  4. 加动画:根据样式做个好看的、合适的动画
  5. 拿数据:通过 AIGC 获取春联的内容
  6. 做适配:根据我们的代码适配到油猴脚本

开发

一步一步开始做呀,快乐的编码~

搭架子

我这里就不限技术栈,直接原生开始做了。

这里分两步做,第一步先把框子搭起来,第二步在框子里把上联、下联、横批渲染出来。

搭容器

由于我们后续要添加到油猴脚本中,因此我们的架子不能在 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%;
}

看看效果吧~

image.png

好像有点丑~ 没事儿,接下来我们一步一步来。

找福字

想到福字,我第一个想到的就是 阿里矢量图标库 ,因为我们是一个 script 脚本,我并不希望在其中再添加一些网络上的图片,最好是 svg 直接绘制出来的福字。

在一众的图标寻觅中,我最终选定了这一款。简单、干净、且没有复杂的样式。

大家可以前往 阿里矢量图标库 寻找自己喜欢的图标。

image.png

我们复制它的 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;
}

看看效果吧~

image.png

改样式

有了福字,我们的对联的样式就显得捉襟见肘了。我们便基于福字实现对联的样式吧~

我们需要调整的样式:

  • 圆角
  • 外边淡红色的边框
  • 内部的轮廓框
  • 文字的颜色

这里面比较麻烦的有两部分,分别是:淡红色的外边框、内部轮廓框。

  • 淡红色的外边框:由于我们使用了圆角,直接使用 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;
}

看看效果吧~

到这步为止,我们的样式就基本完成啦~

image.png

加动画

动画分为两种,一种是春联本体的展开动画,另一种是福字的旋转动画。

  • 展开动画:由于我们使用了 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;
}

实现效果

到这步为止,我们的基本效果就出来了,接下来就只差数据的获取与油猴的适配了。

ezgif-7-acd1083095.gif

拿数据

这里我拿数据做了两种方案:

  • 本地数据
  • 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

image.png

代码通过 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 引入。

配置与发布

  1. 进入 油猴主页
  2. 注册登录账号 image.png
  3. 点击用户名进入个人主页
  4. 点击按钮发布你编写的脚本 image.png
  5. 粘贴你的代码 image.png
  6. 写声明(你的脚本的描述) image.png
  7. 选择脚本类型并发布 image.png

这些做完,我们的春联脚本就算是正式完结撒花了~

最终效果

ezgif-4-ce7a266ab9.gif

最后

我的脚本已发布至油猴平台(随机版,非 AIGC 版)。

科大讯飞的流量太少了,光测试就用了我 1/10
T T

大家感兴趣可以打开该链接直接下载使用: AIGC春联 (greasyfork.org)

希望大家在新的一年里,能开开心心的,不为工作、生活而苦恼~