Hexo+Icarus3+live2d给博客添加看板娘

补坑

之前写过一篇icarus添加看板娘的教程但是版本是<Icarus3的 Icarus3改版很大,完全使用了jsx来代替了ejs,不过添加看板娘不管是jsx还是ejs差别都不大

icarus3之前的教程博客 传送门

上一篇博客那时候拉的live2D还需要导入jQuery 2020年1月1日起,项目不再依赖于 jQuery。

这次我把live2d直接放到了主题文件夹下的source下面 跟js/css/img同级

下载live2D

进入博客根目录

cd theme/icarus/source && git clone https://github.com/stevenjoezhang/live2d-widget.git

修改配置

1. 导入css依赖

找到theme/icarus/layout/common/head.jsx 插入css依赖

大概是在一百四十多行的样子吧 或者可以在head.jsx内搜索<link>标签 然后插入这行

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/font-awesome/css/font-awesome.min.css"/>

修改后完整的head.jsx

const { Component } = require('inferno');
const MetaTags = require('hexo-component-inferno/lib/view/misc/meta');
const OpenGraph = require('hexo-component-inferno/lib/view/misc/open_graph');
const StructuredData = require('hexo-component-inferno/lib/view/misc/structured_data');
const Plugins = require('./plugins');

function getPageTitle(page, siteTitle, helper) {
    let title = page.title;

    if (helper.is_archive()) {
        title = helper._p('common.archive', Infinity);
        if (helper.is_month()) {
            title += ': ' + page.year + '/' + page.month;
        } else if (helper.is_year()) {
            title += ': ' + page.year;
        }
    } else if (helper.is_category()) {
        title = helper._p('common.category', 1) + ': ' + page.category;
    } else if (helper.is_tag()) {
        title = helper._p('common.tag', 1) + ': ' + page.tag;
    } else if (helper.is_categories()) {
        title = helper._p('common.category', Infinity);
    } else if (helper.is_tags()) {
        title = helper._p('common.tag', Infinity);
    }

    return [title, siteTitle].filter(str => typeof str !== 'undefined' && str.trim() !== '').join(' - ');
}

module.exports = class extends Component {
    render() {
        const { env, site, config, helper, page } = this.props;
        const { url_for, cdn, fontcdn, iconcdn, is_post } = helper;
        const {
            url,
            meta_generator = true,
            head = {},
            article,
            highlight,
            variant = 'default'
        } = config;
        const {
            meta = [],
            open_graph = {},
            structured_data = {},
            canonical_url = page.permalink,
            rss,
            favicon
        } = head;

        const language = page.lang || page.language || config.language;
        const fontCssUrl = {
            default: fontcdn('Ubuntu:wght@400;600&family=Source+Code+Pro', 'css2'),
            cyberpunk: fontcdn('Oxanium:wght@300;400;600&family=Roboto+Mono', 'css2')
        };

        let hlTheme, images;
        if (highlight && highlight.enable === false) {
            hlTheme = null;
        } else if (article && article.highlight && article.highlight.theme) {
            hlTheme = article.highlight.theme;
        } else {
            hlTheme = 'atom-one-light';
        }

        if (typeof page.og_image === 'string') {
            images = [page.og_image];
        } else if (helper.has_thumbnail(page)) {
            images = [helper.get_thumbnail(page)];
        } else if (article && typeof article.og_image === 'string') {
            images = [article.og_image];
        } else if (page.content && page.content.includes('<img')) {
            let img;
            images = [];
            const imgPattern = /<img [^>]*src=['"]([^'"]+)([^>]*>)/gi;
            while ((img = imgPattern.exec(page.content)) !== null) {
                images.push(img[1]);
            }
        } else {
            images = [url_for('/img/og_image.png')];
        }

        let adsenseClientId = null;
        if (Array.isArray(config.widgets)) {
            const widget = config.widgets.find(widget => widget.type === 'adsense');
            if (widget) {
                adsenseClientId = widget.client_id;
            }
        }

        let openGraphImages = images;
        if ((typeof open_graph === 'object' && open_graph !== null)
            && ((Array.isArray(open_graph.image) && open_graph.image.length > 0) || typeof open_graph.image === 'string')) {
            openGraphImages = open_graph.image;
        } else if ((Array.isArray(page.photos) && page.photos.length > 0) || typeof page.photos === 'string') {
            openGraphImages = page.photos;
        }

        let structuredImages = images;
        if ((typeof structured_data === 'object' && structured_data !== null)
            && ((Array.isArray(structured_data.image) && structured_data.image.length > 0) || typeof structured_data.image === 'string')) {
            structuredImages = structured_data.image;
        } else if ((Array.isArray(page.photos) && page.photos.length > 0) || typeof page.photos === 'string') {
            structuredImages = page.photos;
        }

        return <head>
            <meta charset="utf-8" />
            {meta_generator ? <meta name="generator" content={`Hexo ${env.version}`} /> : null}
            <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
            {meta && meta.length ? <MetaTags meta={meta} /> : null}
            <meta name="baidu-site-verification" content="mhWv4NNMfG" />
            <meta name="google-site-verification" content="aNBCEhnauRjyRi2s55JA4LemtzHTmgcHT43vigw9Qek" />
            <title>{getPageTitle(page, config.title, helper)}</title>

            {typeof open_graph === 'object' && open_graph !== null ? <OpenGraph
                type={open_graph.type || (is_post(page) ? 'article' : 'website')}
                title={open_graph.title || page.title || config.title}
                date={page.date}
                updated={page.updated}
                author={open_graph.author || config.author}
                description={open_graph.description || page.description || page.excerpt || page.content || config.description}
                keywords={page.keywords || (page.tags && page.tags.length ? page.tags : undefined) || config.keywords}
                url={open_graph.url || page.permalink || url}
                images={openGraphImages}
                siteName={open_graph.site_name || config.title}
                language={language}
                twitterId={open_graph.twitter_id}
                twitterCard={open_graph.twitter_card}
                twitterSite={open_graph.twitter_site}
                googlePlus={open_graph.google_plus}
                facebookAdmins={open_graph.fb_admins}
                facebookAppId={open_graph.fb_app_id} /> : null}

            {typeof structured_data === 'object' && structured_data !== null ? <StructuredData
                title={structured_data.title || config.title}
                description={structured_data.description || page.description || page.excerpt || page.content || config.description}
                url={structured_data.url || page.permalink || url}
                author={structured_data.author || config.author}
                date={page.date}
                updated={page.updated}
                images={structuredImages} /> : null}

            {canonical_url ? <link rel="canonical" href={canonical_url} /> : null}
            {rss ? <link rel="alternative" href={url_for(rss)} title={config.title} type="application/atom+xml" /> : null}
            {favicon ? <link rel="icon" href={url_for(favicon)} /> : null}
            <link rel="stylesheet" href={iconcdn()} />
            {hlTheme ? <link rel="stylesheet" href={cdn('highlight.js', '9.12.0', 'styles/' + hlTheme + '.css')} /> : null}
            <link rel="stylesheet" href={fontCssUrl[variant]} />
            <link rel="stylesheet" href={url_for('/css/' + variant + '.css')} />
            {/* 这行是live2d需要的css依赖 */}
            <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/font-awesome/css/font-awesome.min.css"/>
            <Plugins site={site} config={config} helper={helper} page={page} head={true} />

            {adsenseClientId ? <script data-ad-client={adsenseClientId}
                src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js" async={true}></script> : null}
        </head>;
    }
};

2. 修改刚下载的live2d-widget 下的autoload.js

注释掉第二行 //const live2d_path = "https://cdn.jsdelivr.net/gh/stevenjoezhang/live2d-widget@latest/";

放开第三行 const live2d_path = "/live2d-widget/";

修改后的autoload.js

// 注意:live2d_path 参数应使用绝对路径
//const live2d_path = "https://cdn.jsdelivr.net/gh/stevenjoezhang/live2d-widget@latest/";
const live2d_path = "/live2d-widget/";

// 封装异步加载资源的方法
function loadExternalResource(url, type) {
    return new Promise((resolve, reject) => {
        let tag;

        if (type === "css") {
            tag = document.createElement("link");
            tag.rel = "stylesheet";
            tag.href = url;
        }
        else if (type === "js") {
            tag = document.createElement("script");
            tag.src = url;
        }
        if (tag) {
            tag.onload = () => resolve(url);
            tag.onerror = () => reject(url);
            document.head.appendChild(tag);
        }
    });
}

// 加载 waifu.css live2d.min.js waifu-tips.js
if (screen.width >= 768) {
    Promise.all([
        loadExternalResource(live2d_path + "waifu.css", "css"),
        loadExternalResource(live2d_path + "live2d.min.js", "js"),
        loadExternalResource(live2d_path + "waifu-tips.js", "js")
    ]).then(() => {
        initWidget({
            waifuPath: live2d_path + "waifu-tips.json",
            //apiPath: "https://live2d.fghrsh.net/api/",
            cdnPath: "https://cdn.jsdelivr.net/gh/fghrsh/live2d_api/"
            //cdnPath: "https://live2d.fghrsh.net/api/"
        });
    });
}
// initWidget 第一个参数为 waifu-tips.json 的路径,第二个参数为 API 地址
// API 后端可自行搭建,参考 https://github.com/fghrsh/live2d_api
// 初始化看板娘会自动加载指定目录下的 waifu-tips.json
3. 在主题内导入autoload.js

前提是 live2d-widget 的位置在theme/icarus/source

找到theme/icarus/layout/common/scripts.jsx 在末尾处 <Fragment>标签内添加

<script src={url_for('/live2d-widget/autoload.js')}></script>

添加后完整的scripts.jsx

const {Component, Fragment} = require('inferno');
const Plugins = require('./plugins');

module.exports = class extends Component {
    render() {
        const {site, config, helper, page} = this.props;
        const {url_for, cdn} = helper;
        const {external_link, article} = config;
        const language = page.lang || page.language || config.language || 'en';

        let externalLink;
        if (typeof external_link === 'boolean') {
            externalLink = {enable: external_link, exclude: []};
        } else {
            externalLink = {
                enable: typeof external_link.enable === 'boolean' ? external_link.enable : true,
                exclude: external_link.exclude || []
            };
        }

        let fold = 'unfolded';
        let clipboard = true;
        if (article && article.highlight) {
            if (typeof article.highlight.clipboard !== 'undefined') {
                clipboard = !!article.highlight.clipboard;
            }
            if (typeof article.highlight.fold === 'string') {
                fold = article.highlight.fold;
            }
        }

        const embeddedConfig = `var IcarusThemeSettings = {
            site: {
                url: '${config.url}',
                external_link: ${JSON.stringify(externalLink)}
            },
            article: {
                highlight: {
                    clipboard: ${clipboard},
                    fold: '${fold}'
                }
            }
        };`;

        return <Fragment>
            <script src={cdn('jquery', '3.3.1', 'dist/jquery.min.js')}></script>
            <script src={cdn('moment', '2.22.2', 'min/moment-with-locales.min.js')}></script>
            <script dangerouslySetInnerHTML={{__html: `moment.locale("${language}");`}}></script>
            <script dangerouslySetInnerHTML={{__html: embeddedConfig}}></script>
            {clipboard ? <script src={cdn('clipboard', '2.0.4', 'dist/clipboard.min.js')} defer={true}></script> : null}
            <Plugins site={site} config={config} page={page} helper={helper} head={false}/>
            <script src={url_for('/js/main.js')} defer={true}></script>
            <script src={url_for('/live2d-widget/autoload.js')}></script>
        </Fragment>;
    }
};

4. 开启live2d

编辑主题配置文件_config.yml 添加

live2d:
  enable: true

大功告成!

备注

看板娘到这儿应该就可以出来了 但是会发现在icarus的样式下面 这时候需要把看板娘给置顶

找到live2d-widget 下的waifu.css 修改33行 id为#waifu的样式 把z-index:1 修改为z-index:1000;

修改后的waifu.css

#waifu-toggle {
    background-color: #fa0;
    border-radius: 5px;
    bottom: 66px;
    color: #fff;
    cursor: pointer;
    font-size: 12px;
    left: 0;
    margin-left: -100px;
    padding: 5px 2px 5px 5px;
    position: fixed;
    transition: margin-left 1s;
    width: 60px;
    writing-mode: vertical-rl;
}

#waifu-toggle.waifu-toggle-active {
    margin-left: -50px;
}

#waifu-toggle.waifu-toggle-active:hover {
    margin-left: -30px;
}

#waifu {
    bottom: -1000px;
    left: 0;
    line-height: 0;
    margin-bottom: -10px;
    position: fixed;
    transform: translateY(3px);
    transition: transform .3s ease-in-out, bottom 3s ease-in-out;
    z-index: 1000;
}

#waifu:hover {
    transform: translateY(0);
}

#waifu-tips {
    animation: shake 50s ease-in-out 5s infinite;
    background-color: rgba(236, 217, 188, .5);
    border: 1px solid rgba(224, 186, 140, .62);
    border-radius: 12px;
    box-shadow: 0 3px 15px 2px rgba(191, 158, 118, .2);
    font-size: 14px;
    line-height: 24px;
    margin: -30px 20px;
    min-height: 70px;
    opacity: 0;
    overflow: hidden;
    padding: 5px 10px;
    position: absolute;
    text-overflow: ellipsis;
    transition: opacity 1s;
    width: 250px;
    word-break: break-all;
}

#waifu-tips.waifu-tips-active {
    opacity: 1;
    transition: opacity .2s;
}

#waifu-tips span {
    color: #0099cc;
}

#waifu #live2d {
    cursor: grab;
    height: 280px;
    position: relative;
    width: 280px;
}

#waifu #live2d:active {
    cursor: grabbing;
}

#waifu-tool {
    color: #aaa;
    opacity: 0;
    position: absolute;
    right: -10px;
    top: 70px;
    transition: opacity 1s;
}

#waifu:hover #waifu-tool {
    opacity: 1;
}

#waifu-tool span {
    color: #7b8c9d;
    cursor: pointer;
    display: block;
    line-height: 30px;
    text-align: center;
    transition: color .3s;
}

#waifu-tool span:hover {
    color: #0684bd; /* #34495e */
}

@keyframes shake {
    2% {
        transform: translate(.5px, -1.5px) rotate(-.5deg);
    }

    4% {
        transform: translate(.5px, 1.5px) rotate(1.5deg);
    }

    6% {
        transform: translate(1.5px, 1.5px) rotate(1.5deg);
    }

    8% {
        transform: translate(2.5px, 1.5px) rotate(.5deg);
    }

    10% {
        transform: translate(.5px, 2.5px) rotate(.5deg);
    }

    12% {
        transform: translate(1.5px, 1.5px) rotate(.5deg);
    }

    14% {
        transform: translate(.5px, .5px) rotate(.5deg);
    }

    16% {
        transform: translate(-1.5px, -.5px) rotate(1.5deg);
    }

    18% {
        transform: translate(.5px, .5px) rotate(1.5deg);
    }

    20% {
        transform: translate(2.5px, 2.5px) rotate(1.5deg);
    }

    22% {
        transform: translate(.5px, -1.5px) rotate(1.5deg);
    }

    24% {
        transform: translate(-1.5px, 1.5px) rotate(-.5deg);
    }

    26% {
        transform: translate(1.5px, .5px) rotate(1.5deg);
    }

    28% {
        transform: translate(-.5px, -.5px) rotate(-.5deg);
    }

    30% {
        transform: translate(1.5px, -.5px) rotate(-.5deg);
    }

    32% {
        transform: translate(2.5px, -1.5px) rotate(1.5deg);
    }

    34% {
        transform: translate(2.5px, 2.5px) rotate(-.5deg);
    }

    36% {
        transform: translate(.5px, -1.5px) rotate(.5deg);
    }

    38% {
        transform: translate(2.5px, -.5px) rotate(-.5deg);
    }

    40% {
        transform: translate(-.5px, 2.5px) rotate(.5deg);
    }

    42% {
        transform: translate(-1.5px, 2.5px) rotate(.5deg);
    }

    44% {
        transform: translate(-1.5px, 1.5px) rotate(.5deg);
    }

    46% {
        transform: translate(1.5px, -.5px) rotate(-.5deg);
    }

    48% {
        transform: translate(2.5px, -.5px) rotate(.5deg);
    }

    50% {
        transform: translate(-1.5px, 1.5px) rotate(.5deg);
    }

    52% {
        transform: translate(-.5px, 1.5px) rotate(.5deg);
    }

    54% {
        transform: translate(-1.5px, 1.5px) rotate(.5deg);
    }

    56% {
        transform: translate(.5px, 2.5px) rotate(1.5deg);
    }

    58% {
        transform: translate(2.5px, 2.5px) rotate(.5deg);
    }

    60% {
        transform: translate(2.5px, -1.5px) rotate(1.5deg);
    }

    62% {
        transform: translate(-1.5px, .5px) rotate(1.5deg);
    }

    64% {
        transform: translate(-1.5px, 1.5px) rotate(1.5deg);
    }

    66% {
        transform: translate(.5px, 2.5px) rotate(1.5deg);
    }

    68% {
        transform: translate(2.5px, -1.5px) rotate(1.5deg);
    }

    70% {
        transform: translate(2.5px, 2.5px) rotate(.5deg);
    }

    72% {
        transform: translate(-.5px, -1.5px) rotate(1.5deg);
    }

    74% {
        transform: translate(-1.5px, 2.5px) rotate(1.5deg);
    }

    76% {
        transform: translate(-1.5px, 2.5px) rotate(1.5deg);
    }

    78% {
        transform: translate(-1.5px, 2.5px) rotate(.5deg);
    }

    80% {
        transform: translate(-1.5px, .5px) rotate(-.5deg);
    }

    82% {
        transform: translate(-1.5px, .5px) rotate(-.5deg);
    }

    84% {
        transform: translate(-.5px, .5px) rotate(1.5deg);
    }

    86% {
        transform: translate(2.5px, 1.5px) rotate(.5deg);
    }

    88% {
        transform: translate(-1.5px, .5px) rotate(1.5deg);
    }

    90% {
        transform: translate(-1.5px, -.5px) rotate(-.5deg);
    }

    92% {
        transform: translate(-1.5px, -1.5px) rotate(1.5deg);
    }

    94% {
        transform: translate(.5px, .5px) rotate(-.5deg);
    }

    96% {
        transform: translate(2.5px, -.5px) rotate(-.5deg);
    }

    98% {
        transform: translate(-1.5px, -1.5px) rotate(-.5deg);
    }

    0%, 100% {
        transform: translate(0, 0) rotate(0);
    }
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260