├── .github
└── workflows
│ ├── linter.yml
│ └── tester.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.yaml
├── LICENSE
├── README.en.md
├── README.md
├── build
├── cubism2
│ ├── LAppDefine.d.ts
│ ├── LAppDefine.js
│ ├── LAppLive2DManager.d.ts
│ ├── LAppLive2DManager.js
│ ├── LAppModel.d.ts
│ ├── LAppModel.js
│ ├── Live2DFramework.d.ts
│ ├── Live2DFramework.js
│ ├── PlatformManager.d.ts
│ ├── PlatformManager.js
│ ├── index.d.ts
│ ├── index.js
│ └── utils
│ │ ├── MatrixStack.d.ts
│ │ ├── MatrixStack.js
│ │ ├── ModelSettingJson.d.ts
│ │ └── ModelSettingJson.js
├── cubism5
│ ├── index.d.ts
│ └── index.js
├── drag.d.ts
├── drag.js
├── icons.d.ts
├── icons.js
├── index.d.ts
├── index.js
├── logger.d.ts
├── logger.js
├── message.d.ts
├── message.js
├── model.d.ts
├── model.js
├── tools.d.ts
├── tools.js
├── utils.d.ts
├── utils.js
├── waifu-tips.d.ts
├── waifu-tips.js
├── widget.d.ts
└── widget.js
├── demo
├── demo.html
├── login.html
└── screenshots
│ ├── screenshot-1.png
│ ├── screenshot-2.png
│ └── screenshot-3.png
├── dist
├── autoload.js
├── chunk
│ ├── index.js
│ ├── index.js.map
│ ├── index2.js
│ └── index2.js.map
├── live2d.min.js
├── waifu-tips.js
├── waifu-tips.js.map
├── waifu-tips.json
└── waifu.css
├── eslint.config.js
├── package.json
├── renovate.json
├── rollup.config.js
├── src
├── cubism2
│ ├── LAppDefine.js
│ ├── LAppLive2DManager.js
│ ├── LAppModel.js
│ ├── Live2DFramework.js
│ ├── PlatformManager.js
│ ├── index.js
│ └── utils
│ │ ├── MatrixStack.js
│ │ └── ModelSettingJson.js
├── cubism5
│ └── index.js
├── drag.ts
├── icons.ts
├── index.ts
├── logger.ts
├── message.ts
├── model.ts
├── tools.ts
├── types
│ ├── index.d.ts
│ ├── live2dApi.d.ts
│ └── window.d.ts
├── utils.ts
├── waifu-tips.ts
└── widget.ts
└── tsconfig.json
/.github/workflows/linter.yml:
--------------------------------------------------------------------------------
1 | name: Linter
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | linter:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v4
10 | - name: Use Node.js
11 | uses: actions/setup-node@v4
12 | - name: Install Dependencies
13 | run: npm install
14 | - run: npm run eslint
15 |
--------------------------------------------------------------------------------
/.github/workflows/tester.yml:
--------------------------------------------------------------------------------
1 | name: Tester
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | tester:
7 | runs-on: ${{ matrix.os }}
8 | strategy:
9 | matrix:
10 | os: [ubuntu-latest, windows-latest, macos-latest]
11 | fail-fast: false
12 | steps:
13 | - uses: actions/checkout@v4
14 | with:
15 | repository: hexojs/hexo-starter
16 | - name: Use Node.js
17 | uses: actions/setup-node@v4
18 | - name: Install Dependencies
19 | run: npm install
20 | - name: Test
21 | run: npm run build
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | package-lock.json
2 | node_modules/
3 | .DS_Store
4 | .idea/
5 | .vscode/
6 | backup/
7 | src/CubismSdkForWeb-*/
8 | build/CubismSdkForWeb-*/
9 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 |
--------------------------------------------------------------------------------
/.prettierrc.yaml:
--------------------------------------------------------------------------------
1 | tabWidth: 2
2 | semi: true
3 | singleQuote: true
4 | jsxSingleQuote: true
5 | trailingComma: all
6 | endOfLine: crlf
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Live2D Widget
2 |
3 | 
4 | 
5 | 
6 | 
7 | 
8 | 
9 |
10 | [English](README.en.md)
11 |
12 | ## 特性
13 |
14 | - 在网页中添加 Live2D 看板娘
15 | - 轻量级,除 Live2D Cubism Core 外无其他运行时依赖
16 | - 核心代码由 TypeScript 编写,易于集成
17 |
18 | 

19 |
20 | *注:以上人物模型仅供展示之用,本仓库并不包含任何模型。*
21 |
22 | 你也可以查看示例网页:
23 |
24 | - 在 [米米的博客](https://zhangshuqiao.org) 的左下角可查看效果
25 | - [demo/demo.html](https://live2d-widget.pages.dev/demo/demo),展现基础功能
26 | - [demo/login.html](https://live2d-widget.pages.dev/demo/login),仿 NPM 的登陆界面
27 |
28 | ## 使用
29 |
30 | 如果你是小白,或者只需要最基础的功能,那么只用将这一行代码加入 html 页面的 `head` 或 `body` 中,即可加载看板娘:
31 |
32 | ```html
33 |
34 | ```
35 |
36 | 添加代码的位置取决于你的网站的构建方式。例如,如果你使用的是 [Hexo](https://hexo.io),那么需要在主题的模版文件中添加以上代码。对于用各种模版引擎生成的页面,修改方法类似。
37 | 如果网站启用了 PJAX,由于看板娘不必每页刷新,需要注意将该脚本放到 PJAX 刷新区域之外。
38 |
39 | **但是!我们强烈推荐自己进行配置,让看板娘更加适合你的网站!**
40 | 如果你有兴趣自己折腾的话,请看下面的详细说明。
41 |
42 | ## 配置
43 |
44 | 你可以对照 `dist/autoload.js` 的源码查看可选的配置项目。`autoload.js` 会自动加载两个文件:`waifu.css` 和 `waifu-tips.js`。`waifu-tips.js` 会创建 `initWidget` 函数,这就是加载看板娘的主函数。`initWidget` 函数接收一个 Object 类型的参数,作为看板娘的配置。以下是配置选项:
45 |
46 | | 选项 | 类型 | 默认值 | 说明 |
47 | | - | - | - | - |
48 | | `waifuPath` | `string` | `https://fastly.jsdelivr.net/npm/live2d-widgets@1/dist/waifu-tips.json` | 看板娘资源路径,可自行修改 |
49 | | `cdnPath` | `string` | `https://fastly.jsdelivr.net/gh/fghrsh/live2d_api/` | CDN 路径 |
50 | | `cubism2Path` | `string` | `https://fastly.jsdelivr.net/npm/live2d-widgets@1/dist/live2d.min.js` | Cubism 2 Core 路径 |
51 | | `cubism5Path` | `string` | `https://cubism.live2d.com/sdk-web/cubismcore/live2dcubismcore.min.js` | Cubism 5 Core 路径 |
52 | | `modelId` | `number` | `0` | 默认模型 id |
53 | | `tools` | `string[]` | 见 `autoload.js` | 加载的小工具按钮 |
54 | | `drag` | `boolean` | `false` | 支持拖动看板娘 |
55 | | `logLevel` | `string` | `error` | 日志等级,支持 `error`,`warn`,`info`,`trace` |
56 |
57 | ## 模型仓库
58 |
59 | 本仓库中并不包含任何模型,需要单独配置模型仓库,并通过 `cdnPath` 选项进行设置。
60 | 旧版本的 `initWidget` 函数支持 `apiPath` 参数,这要求用户自行搭建后端,可以参考 [live2d_api](https://github.com/fghrsh/live2d_api)。后端接口会对模型资源进行整合并动态生成 JSON 描述文件。自 1.0 版本起,相关功能已通过前端实现,因此不再需要专门的 `apiPath`,所有模型资源都可通过静态方式提供。只要存在 `model_list.json` 和模型对应的 `textures.cache`,即可支持换装等功能。
61 |
62 | ## 开发
63 |
64 | 如果以上「配置」部分提供的选项还不足以满足你的需求,那么你可以自己进行修改。本仓库的目录结构如下:
65 |
66 | - `src` 目录下包含了各个组件的 TypeScript 源代码,例如按钮和对话框等;
67 | - `build` 目录下包含了基于 `src` 中源代码构建后的文件(请不要直接修改!);
68 | - `dist` 目录下包含了进一步打包后网页直接可用的文件,其中:
69 | - `autoload.js` 用于自动加载其它资源,例如样式表等;
70 | - `waifu-tips.js` 是由 `build/waifu-tips.js` 自动打包生成的,不建议直接修改;
71 | - `waifu.css` 是看板娘的样式表;
72 | - `waifu-tips.json` 中定义了触发条件(`selector`,CSS 选择器)和触发时显示的文字(`text`)。
73 | `waifu-tips.json` 中默认的 CSS 选择器规则是对 Hexo 的 [NexT 主题](https://github.com/next-theme/hexo-theme-next) 有效的,为了适用于你自己的网页,可能需要自行修改,或增加新内容。
74 | **警告:`waifu-tips.json` 中的内容可能不适合所有年龄段,或不宜在工作期间访问。在使用时,请自行确保它们是合适的。**
75 |
76 | 要在本地部署本项目的开发测试环境,你需要安装 Node.js 和 npm,然后执行以下命令:
77 |
78 | ```bash
79 | git clone https://github.com/stevenjoezhang/live2d-widget.git
80 | npm install
81 | ```
82 |
83 | 如果需要使用 Cubism 3 及更新的模型,请单独下载并解压 Cubism SDK for Web 到 `src` 目录下,例如 `src/CubismSdkForWeb-5-r.4`。受 Live2D 许可协议(包括 Live2D Proprietary Software License Agreement 和 Live2D Open Software License Agreement)限制,本项目无法包含 Cubism SDK for Web 的源码。
84 | 如果只需要使用 Cubism 2 版本的模型,可以跳过此步骤。本仓库使用的代码满足 Live2D 许可协议中 Redistributable Code 相关条款。
85 | 完成后,使用以下命令进行编译和打包。
86 |
87 | ```bash
88 | npm run build
89 | ```
90 |
91 | `src` 目录中的 TypeScript 代码会被编译到 `build` 目录中,`build` 目录中的代码会被进一步打包到 `dist` 目录中。
92 | 为了能够兼容 Cubism 2 和 Cubism 3 及更新的模型,并减小代码体积,Cubism Core 及相关的代码会根据检测到的模型版本动态加载。
93 |
94 | ## 部署
95 |
96 | 在本地完成了修改后,你可以将修改后的项目部署在自己的服务器上,或者通过 CDN 加载。为了方便自定义有关内容,可以把这个仓库 Fork 一份,然后把修改后的内容通过 git push 到你的仓库中。
97 |
98 | ### 使用 jsDelivr CDN
99 |
100 | 如果要通过 jsDelivr 加载 Fork 后的仓库,使用方法对应地变为
101 |
102 | ```html
103 |
104 | ```
105 |
106 | 将此处的 `username` 替换为你的 GitHub 用户名。为了使 CDN 的内容正常刷新,需要创建新的 git tag 并推送至 GitHub 仓库中,否则此处的 `@latest` 仍然指向更新前的文件。此外 CDN 本身存在缓存,因此改动可能需要一定的时间生效。
107 |
108 | ### 使用 Cloudflare Pages
109 |
110 | 也可以使用 Cloudflare Pages 来部署。在 Cloudflare Pages 中创建一个新的项目,选择你 Fork 的仓库。接下来,设置构建命令为 `npm run build`。完成后,Cloudflare Pages 会自动构建并部署你的项目。
111 |
112 | ### Self-host
113 |
114 | 你也可以直接把这些文件放到服务器上,而不是通过 CDN 加载。
115 |
116 | - 可以把修改后的代码仓库克隆到服务器上,或者通过 `ftp` 等方式将本地文件上传到服务器的网站的目录下;
117 | - 如果你是通过 Hexo 等工具部署的静态博客,请把本项目的代码放在博客源文件目录下(例如 `source` 目录)。重新部署博客时,相关文件就会自动上传到对应的路径下。为了避免这些文件被 Hexo 插件错误地修改,可能需要设置 `skip_render`。
118 |
119 | 这样,整个项目就可以通过你的域名访问了。不妨试试能否正常地通过浏览器打开 `autoload.js` 和 `live2d.min.js` 等文件,并确认这些文件的内容是完整和正确的。
120 | 一切正常的话,接下来修改 `autoload.js` 中的常量 `live2d_path` 为 `dist` 目录的 URL 即可。比如说,如果你能够通过
121 |
122 | ```
123 | https://example.com/path/to/live2d-widget/dist/live2d.min.js
124 | ```
125 |
126 | 访问到 `live2d.min.js`,那么就把 `live2d_path` 的值修改为
127 |
128 | ```
129 | https://example.com/path/to/live2d-widget/dist/
130 | ```
131 |
132 | 路径末尾的 `/` 一定要加上。
133 | 完成后,在你要添加看板娘的界面加入
134 |
135 | ```html
136 |
137 | ```
138 |
139 | 就可以加载了。
140 |
141 | ## 鸣谢
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 | > 感谢 BrowserStack 容许我们在真实的浏览器中测试此项目。
152 | > Thanks to [BrowserStack](https://www.browserstack.com/) for providing the infrastructure that allows us to test in real browsers!
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 | > 感谢 jsDelivr 提供的 CDN 服务。
163 | > Thanks jsDelivr for providing public CDN service.
164 |
165 | 感谢 fghrsh 提供的 API 服务。
166 |
167 | 感谢 [一言](https://hitokoto.cn) 提供的语句接口。
168 |
169 | 点击看板娘的纸飞机按钮时,会出现一个彩蛋,这来自于 [WebsiteAsteroids](http://www.websiteasteroids.com)。
170 |
171 | ## 更多
172 |
173 | 代码自这篇博文魔改而来:
174 | https://www.fghrsh.net/post/123.html
175 |
176 | 更多内容可以参考:
177 | https://nocilol.me/archives/lab/add-dynamic-poster-girl-with-live2d-to-your-blog-02
178 | https://github.com/guansss/pixi-live2d-display
179 |
180 | 更多模型仓库:
181 | https://github.com/zenghongtu/live2d-model-assets
182 |
183 | 除此之外,还有桌面版本:
184 | https://github.com/TSKI433/hime-display
185 | https://github.com/amorist/platelet
186 | https://github.com/akiroz/Live2D-Widget
187 | https://github.com/zenghongtu/PPet
188 | https://github.com/LikeNeko/L2dPetForMac
189 |
190 | 以及 Wallpaper Engine:
191 | https://github.com/guansss/nep-live2d
192 |
193 | Live2D 官方网站:
194 | https://www.live2d.com/en/
195 |
196 | ## 许可证
197 |
198 | 本仓库并不包含任何模型,用作展示的所有 Live2D 模型、图片、动作数据等版权均属于其原作者,仅供研究学习,不得用于商业用途。
199 |
200 | 本仓库的代码(不包括受 Live2D Proprietary Software License 和 Live2D Open Software License 约束的部分)基于 GNU General Public License v3 协议开源
201 | http://www.gnu.org/licenses/gpl-3.0.html
202 |
203 | Live2D 相关代码的使用请遵守对应的许可:
204 |
205 | Live2D Cubism SDK 2.1 的许可证:
206 | [Live2D SDK License Agreement (Public)](https://docs.google.com/document/d/10tz1WrycskzGGBOhrAfGiTSsgmyFy8D9yHx9r_PsN8I/)
207 |
208 | Live2D Cubism SDK 5 的许可证:
209 | Live2D Cubism Core は Live2D Proprietary Software License で提供しています。
210 | https://www.live2d.com/eula/live2d-proprietary-software-license-agreement_cn.html
211 | Live2D Cubism Components は Live2D Open Software License で提供しています。
212 | https://www.live2d.com/eula/live2d-open-software-license-agreement_cn.html
213 |
214 | ## 更新日志
215 |
216 | 2020年1月1日起,本项目不再依赖于 jQuery。
217 |
218 | 2022年11月1日起,本项目不再需要用户单独加载 Font Awesome。
219 |
--------------------------------------------------------------------------------
/build/cubism2/LAppDefine.d.ts:
--------------------------------------------------------------------------------
1 | export default LAppDefine;
2 | declare namespace LAppDefine {
3 | let VIEW_MAX_SCALE: number;
4 | let VIEW_MIN_SCALE: number;
5 | let VIEW_LOGICAL_LEFT: number;
6 | let VIEW_LOGICAL_RIGHT: number;
7 | let VIEW_LOGICAL_MAX_LEFT: number;
8 | let VIEW_LOGICAL_MAX_RIGHT: number;
9 | let VIEW_LOGICAL_MAX_BOTTOM: number;
10 | let VIEW_LOGICAL_MAX_TOP: number;
11 | let PRIORITY_NONE: number;
12 | let PRIORITY_IDLE: number;
13 | let PRIORITY_NORMAL: number;
14 | let PRIORITY_FORCE: number;
15 | let MOTION_GROUP_IDLE: string;
16 | let MOTION_GROUP_TAP_BODY: string;
17 | let MOTION_GROUP_FLICK_HEAD: string;
18 | let MOTION_GROUP_PINCH_IN: string;
19 | let MOTION_GROUP_PINCH_OUT: string;
20 | let MOTION_GROUP_SHAKE: string;
21 | let HIT_AREA_HEAD: string;
22 | let HIT_AREA_BODY: string;
23 | }
24 |
--------------------------------------------------------------------------------
/build/cubism2/LAppDefine.js:
--------------------------------------------------------------------------------
1 | const LAppDefine = {
2 | VIEW_MAX_SCALE: 2,
3 | VIEW_MIN_SCALE: 0.8,
4 | VIEW_LOGICAL_LEFT: -1,
5 | VIEW_LOGICAL_RIGHT: 1,
6 | VIEW_LOGICAL_MAX_LEFT: -2,
7 | VIEW_LOGICAL_MAX_RIGHT: 2,
8 | VIEW_LOGICAL_MAX_BOTTOM: -2,
9 | VIEW_LOGICAL_MAX_TOP: 2,
10 | PRIORITY_NONE: 0,
11 | PRIORITY_IDLE: 1,
12 | PRIORITY_NORMAL: 2,
13 | PRIORITY_FORCE: 3,
14 | MOTION_GROUP_IDLE: 'idle',
15 | MOTION_GROUP_TAP_BODY: 'tap_body',
16 | MOTION_GROUP_FLICK_HEAD: 'flick_head',
17 | MOTION_GROUP_PINCH_IN: 'pinch_in',
18 | MOTION_GROUP_PINCH_OUT: 'pinch_out',
19 | MOTION_GROUP_SHAKE: 'shake',
20 | HIT_AREA_HEAD: 'head',
21 | HIT_AREA_BODY: 'body',
22 | };
23 | export default LAppDefine;
24 |
--------------------------------------------------------------------------------
/build/cubism2/LAppLive2DManager.d.ts:
--------------------------------------------------------------------------------
1 | export default LAppLive2DManager;
2 | declare class LAppLive2DManager {
3 | model: LAppModel;
4 | reloading: boolean;
5 | getModel(): LAppModel;
6 | releaseModel(gl: any): void;
7 | changeModel(gl: any, modelSettingPath: any): Promise;
8 | changeModelWithJSON(gl: any, modelSettingPath: any, modelSetting: any): Promise;
9 | setDrag(x: any, y: any): void;
10 | maxScaleEvent(): void;
11 | minScaleEvent(): void;
12 | tapEvent(x: any, y: any): boolean;
13 | }
14 | import LAppModel from './LAppModel.js';
15 |
--------------------------------------------------------------------------------
/build/cubism2/LAppLive2DManager.js:
--------------------------------------------------------------------------------
1 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3 | return new (P || (P = Promise))(function (resolve, reject) {
4 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7 | step((generator = generator.apply(thisArg, _arguments || [])).next());
8 | });
9 | };
10 | import { Live2DFramework } from './Live2DFramework.js';
11 | import LAppModel from './LAppModel.js';
12 | import PlatformManager from './PlatformManager.js';
13 | import LAppDefine from './LAppDefine.js';
14 | import logger from '../logger.js';
15 | class LAppLive2DManager {
16 | constructor() {
17 | this.model = null;
18 | this.reloading = false;
19 | Live2D.init();
20 | Live2DFramework.setPlatformManager(new PlatformManager());
21 | }
22 | getModel() {
23 | return this.model;
24 | }
25 | releaseModel(gl) {
26 | if (this.model) {
27 | this.model.release(gl);
28 | this.model = null;
29 | }
30 | }
31 | changeModel(gl, modelSettingPath) {
32 | return __awaiter(this, void 0, void 0, function* () {
33 | return new Promise((resolve, reject) => {
34 | if (this.reloading)
35 | return;
36 | this.reloading = true;
37 | const oldModel = this.model;
38 | const newModel = new LAppModel();
39 | newModel.load(gl, modelSettingPath, () => {
40 | if (oldModel) {
41 | oldModel.release(gl);
42 | }
43 | this.model = newModel;
44 | this.reloading = false;
45 | resolve();
46 | });
47 | });
48 | });
49 | }
50 | changeModelWithJSON(gl, modelSettingPath, modelSetting) {
51 | return __awaiter(this, void 0, void 0, function* () {
52 | if (this.reloading)
53 | return;
54 | this.reloading = true;
55 | const oldModel = this.model;
56 | const newModel = new LAppModel();
57 | yield newModel.loadModelSetting(modelSettingPath, modelSetting);
58 | if (oldModel) {
59 | oldModel.release(gl);
60 | }
61 | this.model = newModel;
62 | this.reloading = false;
63 | });
64 | }
65 | setDrag(x, y) {
66 | if (this.model) {
67 | this.model.setDrag(x, y);
68 | }
69 | }
70 | maxScaleEvent() {
71 | logger.trace('Max scale event.');
72 | if (this.model) {
73 | this.model.startRandomMotion(LAppDefine.MOTION_GROUP_PINCH_IN, LAppDefine.PRIORITY_NORMAL);
74 | }
75 | }
76 | minScaleEvent() {
77 | logger.trace('Min scale event.');
78 | if (this.model) {
79 | this.model.startRandomMotion(LAppDefine.MOTION_GROUP_PINCH_OUT, LAppDefine.PRIORITY_NORMAL);
80 | }
81 | }
82 | tapEvent(x, y) {
83 | logger.trace('tapEvent view x:' + x + ' y:' + y);
84 | if (!this.model)
85 | return false;
86 | if (this.model.hitTest(LAppDefine.HIT_AREA_HEAD, x, y)) {
87 | logger.trace('Tap face.');
88 | this.model.setRandomExpression();
89 | }
90 | else if (this.model.hitTest(LAppDefine.HIT_AREA_BODY, x, y)) {
91 | logger.trace('Tap body.');
92 | this.model.startRandomMotion(LAppDefine.MOTION_GROUP_TAP_BODY, LAppDefine.PRIORITY_NORMAL);
93 | }
94 | return true;
95 | }
96 | }
97 | export default LAppLive2DManager;
98 |
--------------------------------------------------------------------------------
/build/cubism2/LAppModel.d.ts:
--------------------------------------------------------------------------------
1 | export default LAppModel;
2 | declare class LAppModel extends L2DBaseModel {
3 | modelHomeDir: string;
4 | modelSetting: ModelSettingJson;
5 | tmpMatrix: any[];
6 | loadJSON(callback: any): void;
7 | loadModelSetting(modelSettingPath: any, modelSetting: any): Promise;
8 | load(gl: any, modelSettingPath: any, callback: any): void;
9 | release(gl: any): void;
10 | preloadMotionGroup(name: any): void;
11 | update(): void;
12 | setRandomExpression(): void;
13 | startRandomMotion(name: any, priority: any): void;
14 | startMotion(name: any, no: any, priority: any): void;
15 | setFadeInFadeOut(name: any, no: any, priority: any, motion: any): void;
16 | setExpression(name: any): void;
17 | draw(gl: any): void;
18 | hitTest(id: any, testX: any, testY: any): boolean;
19 | }
20 | import { L2DBaseModel } from './Live2DFramework.js';
21 | import ModelSettingJson from './utils/ModelSettingJson.js';
22 |
--------------------------------------------------------------------------------
/build/cubism2/Live2DFramework.d.ts:
--------------------------------------------------------------------------------
1 | export class L2DBaseModel {
2 | live2DModel: any;
3 | modelMatrix: L2DModelMatrix;
4 | eyeBlink: any;
5 | physics: L2DPhysics;
6 | pose: L2DPose;
7 | initialized: boolean;
8 | updating: boolean;
9 | alpha: number;
10 | accAlpha: number;
11 | lipSync: boolean;
12 | lipSyncValue: number;
13 | accelX: number;
14 | accelY: number;
15 | accelZ: number;
16 | dragX: number;
17 | dragY: number;
18 | startTimeMSec: any;
19 | mainMotionManager: L2DMotionManager;
20 | expressionManager: L2DMotionManager;
21 | motions: {};
22 | expressions: {};
23 | isTexLoaded: boolean;
24 | getModelMatrix(): L2DModelMatrix;
25 | setAlpha(a: any): void;
26 | getAlpha(): number;
27 | isInitialized(): boolean;
28 | setInitialized(v: any): void;
29 | isUpdating(): boolean;
30 | setUpdating(v: any): void;
31 | getLive2DModel(): any;
32 | setLipSync(v: any): void;
33 | setLipSyncValue(v: any): void;
34 | setAccel(x: any, y: any, z: any): void;
35 | setDrag(x: any, y: any): void;
36 | getMainMotionManager(): L2DMotionManager;
37 | getExpressionManager(): L2DMotionManager;
38 | loadModelData(path: any, callback: any): void;
39 | loadTexture(no: any, path: any, callback: any): void;
40 | loadMotion(name: any, path: any, callback: any): void;
41 | loadExpression(name: any, path: any, callback: any): void;
42 | loadPose(path: any, callback: any): void;
43 | loadPhysics(path: any): void;
44 | hitTestSimple(drawID: any, testX: any, testY: any): boolean;
45 | }
46 | export class L2DViewMatrix extends L2DMatrix44 {
47 | screenLeft: any;
48 | screenRight: any;
49 | screenTop: any;
50 | screenBottom: any;
51 | maxLeft: any;
52 | maxRight: any;
53 | maxTop: any;
54 | maxBottom: any;
55 | max: number;
56 | min: number;
57 | getMaxScale(): number;
58 | getMinScale(): number;
59 | setMaxScale(v: any): void;
60 | setMinScale(v: any): void;
61 | isMaxScale(): boolean;
62 | isMinScale(): boolean;
63 | adjustTranslate(shiftX: any, shiftY: any): void;
64 | adjustScale(cx: any, cy: any, scale: any): void;
65 | setScreenRect(left: any, right: any, bottom: any, top: any): void;
66 | setMaxScreenRect(left: any, right: any, bottom: any, top: any): void;
67 | getScreenLeft(): any;
68 | getScreenRight(): any;
69 | getScreenBottom(): any;
70 | getScreenTop(): any;
71 | getMaxLeft(): any;
72 | getMaxRight(): any;
73 | getMaxBottom(): any;
74 | getMaxTop(): any;
75 | }
76 | export class L2DEyeBlink {
77 | nextBlinkTime: any;
78 | stateStartTime: any;
79 | blinkIntervalMsec: number;
80 | eyeState: string;
81 | closingMotionMsec: number;
82 | closedMotionMsec: number;
83 | openingMotionMsec: number;
84 | closeIfZero: boolean;
85 | eyeID_L: string;
86 | eyeID_R: string;
87 | calcNextBlink(): any;
88 | setInterval(blinkIntervalMsec: any): void;
89 | setEyeMotion(closingMotionMsec: any, closedMotionMsec: any, openingMotionMsec: any): void;
90 | updateParam(model: any): void;
91 | }
92 | export class Live2DFramework {
93 | static getPlatformManager(): any;
94 | static setPlatformManager(platformManager: any): void;
95 | }
96 | export namespace Live2DFramework {
97 | let platformManager: any;
98 | }
99 | export class L2DMatrix44 {
100 | static mul(a: any, b: any, dst: any): void;
101 | tr: Float32Array;
102 | identity(): void;
103 | getArray(): Float32Array;
104 | getCopyMatrix(): Float32Array;
105 | setMatrix(tr: any): void;
106 | getScaleX(): number;
107 | getScaleY(): number;
108 | transformX(src: any): number;
109 | transformY(src: any): number;
110 | invertTransformX(src: any): number;
111 | invertTransformY(src: any): number;
112 | multTranslate(shiftX: any, shiftY: any): void;
113 | translate(x: any, y: any): void;
114 | translateX(x: any): void;
115 | translateY(y: any): void;
116 | multScale(scaleX: any, scaleY: any): void;
117 | scale(scaleX: any, scaleY: any): void;
118 | }
119 | export class L2DTargetPoint {
120 | EPSILON: number;
121 | faceTargetX: number;
122 | faceTargetY: number;
123 | faceX: number;
124 | faceY: number;
125 | faceVX: number;
126 | faceVY: number;
127 | lastTimeSec: number;
128 | setPoint(x: any, y: any): void;
129 | getX(): number;
130 | getY(): number;
131 | update(): void;
132 | }
133 | export namespace L2DTargetPoint {
134 | let FRAME_RATE: number;
135 | }
136 | declare class L2DModelMatrix extends L2DMatrix44 {
137 | constructor(w: any, h: any);
138 | width: any;
139 | height: any;
140 | setPosition(x: any, y: any): void;
141 | setCenterPosition(x: any, y: any): void;
142 | top(y: any): void;
143 | bottom(y: any): void;
144 | left(x: any): void;
145 | right(x: any): void;
146 | centerX(x: any): void;
147 | centerY(y: any): void;
148 | setX(x: any): void;
149 | setY(y: any): void;
150 | setHeight(h: any): void;
151 | setWidth(w: any): void;
152 | }
153 | declare class L2DPhysics {
154 | static load(buf: any): L2DPhysics;
155 | physicsList: any[];
156 | startTimeMSec: any;
157 | updateParam(model: any): void;
158 | }
159 | declare class L2DPose {
160 | static load(buf: any): L2DPose;
161 | lastTime: number;
162 | lastModel: any;
163 | partsGroups: any[];
164 | updateParam(model: any): void;
165 | initParam(model: any): void;
166 | normalizePartsOpacityGroup(model: any, partsGroup: any, deltaTimeSec: any): void;
167 | copyOpacityOtherParts(model: any, partsGroup: any): void;
168 | }
169 | declare class L2DMotionManager {
170 | currentPriority: any;
171 | reservePriority: any;
172 | super: any;
173 | getCurrentPriority(): any;
174 | getReservePriority(): any;
175 | reserveMotion(priority: any): boolean;
176 | setReservePriority(val: any): void;
177 | updateParam(model: any): any;
178 | startMotionPrio(motion: any, priority: any): any;
179 | }
180 | export {};
181 |
--------------------------------------------------------------------------------
/build/cubism2/PlatformManager.d.ts:
--------------------------------------------------------------------------------
1 | export default PlatformManager;
2 | declare class PlatformManager {
3 | cache: {};
4 | loadBytes(path: any, callback: any): any;
5 | loadLive2DModel(path: any, callback: any): void;
6 | loadTexture(model: any, no: any, path: any, callback: any): void;
7 | jsonParseFromBytes(buf: any): any;
8 | }
9 |
--------------------------------------------------------------------------------
/build/cubism2/PlatformManager.js:
--------------------------------------------------------------------------------
1 | import logger from '../logger.js';
2 | class PlatformManager {
3 | constructor() {
4 | this.cache = {};
5 | }
6 | loadBytes(path, callback) {
7 | if (path in this.cache) {
8 | return callback(this.cache[path]);
9 | }
10 | fetch(path)
11 | .then(response => response.arrayBuffer())
12 | .then(arrayBuffer => {
13 | this.cache[path] = arrayBuffer;
14 | callback(arrayBuffer);
15 | });
16 | }
17 | loadLive2DModel(path, callback) {
18 | let model = null;
19 | this.loadBytes(path, buf => {
20 | model = Live2DModelWebGL.loadModel(buf);
21 | callback(model);
22 | });
23 | }
24 | loadTexture(model, no, path, callback) {
25 | const loadedImage = new Image();
26 | loadedImage.crossOrigin = 'anonymous';
27 | loadedImage.src = path;
28 | loadedImage.onload = () => {
29 | const canvas = document.getElementById('live2d');
30 | const gl = canvas.getContext('webgl2', { premultipliedAlpha: true, preserveDrawingBuffer: true });
31 | let texture = gl.createTexture();
32 | if (!texture) {
33 | logger.error('Failed to generate gl texture name.');
34 | return -1;
35 | }
36 | if (model.isPremultipliedAlpha() == false) {
37 | gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 1);
38 | }
39 | gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
40 | gl.activeTexture(gl.TEXTURE0);
41 | gl.bindTexture(gl.TEXTURE_2D, texture);
42 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, loadedImage);
43 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
44 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST);
45 | gl.generateMipmap(gl.TEXTURE_2D);
46 | model.setTexture(no, texture);
47 | texture = null;
48 | if (typeof callback == 'function')
49 | callback();
50 | };
51 | loadedImage.onerror = () => {
52 | logger.error('Failed to load image : ' + path);
53 | };
54 | }
55 | jsonParseFromBytes(buf) {
56 | let jsonStr;
57 | const bomCode = new Uint8Array(buf, 0, 3);
58 | if (bomCode[0] == 239 && bomCode[1] == 187 && bomCode[2] == 191) {
59 | jsonStr = String.fromCharCode.apply(null, new Uint8Array(buf, 3));
60 | }
61 | else {
62 | jsonStr = String.fromCharCode.apply(null, new Uint8Array(buf));
63 | }
64 | const jsonObj = JSON.parse(jsonStr);
65 | return jsonObj;
66 | }
67 | }
68 | export default PlatformManager;
69 |
--------------------------------------------------------------------------------
/build/cubism2/index.d.ts:
--------------------------------------------------------------------------------
1 | export default Cubism2Model;
2 | declare class Cubism2Model {
3 | live2DMgr: LAppLive2DManager;
4 | isDrawStart: boolean;
5 | gl: any;
6 | canvas: HTMLElement;
7 | dragMgr: L2DTargetPoint;
8 | viewMatrix: L2DViewMatrix;
9 | projMatrix: L2DMatrix44;
10 | deviceToScreen: L2DMatrix44;
11 | oldLen: number;
12 | _boundMouseEvent: any;
13 | _boundTouchEvent: any;
14 | initL2dCanvas(canvasId: any): void;
15 | init(canvasId: any, modelSettingPath: any, modelSetting: any): Promise;
16 | destroy(): void;
17 | _drawFrameId: number;
18 | startDraw(): void;
19 | draw(): void;
20 | changeModel(modelSettingPath: any): Promise;
21 | changeModelWithJSON(modelSettingPath: any, modelSetting: any): Promise;
22 | modelScaling(scale: any): void;
23 | modelTurnHead(event: any): void;
24 | followPointer(event: any): void;
25 | lookFront(): void;
26 | mouseEvent(e: any): void;
27 | touchEvent(e: any): void;
28 | transformViewX(deviceX: any): number;
29 | transformViewY(deviceY: any): number;
30 | transformScreenX(deviceX: any): number;
31 | transformScreenY(deviceY: any): number;
32 | }
33 | import LAppLive2DManager from './LAppLive2DManager.js';
34 | import { L2DTargetPoint } from './Live2DFramework.js';
35 | import { L2DViewMatrix } from './Live2DFramework.js';
36 | import { L2DMatrix44 } from './Live2DFramework.js';
37 |
--------------------------------------------------------------------------------
/build/cubism2/index.js:
--------------------------------------------------------------------------------
1 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3 | return new (P || (P = Promise))(function (resolve, reject) {
4 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7 | step((generator = generator.apply(thisArg, _arguments || [])).next());
8 | });
9 | };
10 | import { L2DMatrix44, L2DTargetPoint, L2DViewMatrix } from './Live2DFramework.js';
11 | import LAppDefine from './LAppDefine.js';
12 | import MatrixStack from './utils/MatrixStack.js';
13 | import LAppLive2DManager from './LAppLive2DManager.js';
14 | import logger from '../logger.js';
15 | function normalizePoint(x, y, x0, y0, w, h) {
16 | const dx = x - x0;
17 | const dy = y - y0;
18 | let targetX = 0, targetY = 0;
19 | if (dx >= 0) {
20 | targetX = dx / (w - x0);
21 | }
22 | else {
23 | targetX = dx / x0;
24 | }
25 | if (dy >= 0) {
26 | targetY = dy / (h - y0);
27 | }
28 | else {
29 | targetY = dy / y0;
30 | }
31 | return {
32 | vx: targetX,
33 | vy: -targetY
34 | };
35 | }
36 | class Cubism2Model {
37 | constructor() {
38 | this.live2DMgr = new LAppLive2DManager();
39 | this.isDrawStart = false;
40 | this.gl = null;
41 | this.canvas = null;
42 | this.dragMgr = null;
43 | this.viewMatrix = null;
44 | this.projMatrix = null;
45 | this.deviceToScreen = null;
46 | this.oldLen = 0;
47 | this._boundMouseEvent = this.mouseEvent.bind(this);
48 | this._boundTouchEvent = this.touchEvent.bind(this);
49 | }
50 | initL2dCanvas(canvasId) {
51 | this.canvas = document.getElementById(canvasId);
52 | if (this.canvas.addEventListener) {
53 | this.canvas.addEventListener('mousewheel', this._boundMouseEvent, false);
54 | this.canvas.addEventListener('click', this._boundMouseEvent, false);
55 | document.addEventListener('mousemove', this._boundMouseEvent, false);
56 | document.addEventListener('mouseout', this._boundMouseEvent, false);
57 | this.canvas.addEventListener('contextmenu', this._boundMouseEvent, false);
58 | this.canvas.addEventListener('touchstart', this._boundTouchEvent, false);
59 | this.canvas.addEventListener('touchend', this._boundTouchEvent, false);
60 | this.canvas.addEventListener('touchmove', this._boundTouchEvent, false);
61 | }
62 | }
63 | init(canvasId, modelSettingPath, modelSetting) {
64 | return __awaiter(this, void 0, void 0, function* () {
65 | this.initL2dCanvas(canvasId);
66 | const width = this.canvas.width;
67 | const height = this.canvas.height;
68 | this.dragMgr = new L2DTargetPoint();
69 | const ratio = height / width;
70 | const left = LAppDefine.VIEW_LOGICAL_LEFT;
71 | const right = LAppDefine.VIEW_LOGICAL_RIGHT;
72 | const bottom = -ratio;
73 | const top = ratio;
74 | this.viewMatrix = new L2DViewMatrix();
75 | this.viewMatrix.setScreenRect(left, right, bottom, top);
76 | this.viewMatrix.setMaxScreenRect(LAppDefine.VIEW_LOGICAL_MAX_LEFT, LAppDefine.VIEW_LOGICAL_MAX_RIGHT, LAppDefine.VIEW_LOGICAL_MAX_BOTTOM, LAppDefine.VIEW_LOGICAL_MAX_TOP);
77 | this.viewMatrix.setMaxScale(LAppDefine.VIEW_MAX_SCALE);
78 | this.viewMatrix.setMinScale(LAppDefine.VIEW_MIN_SCALE);
79 | this.projMatrix = new L2DMatrix44();
80 | this.projMatrix.multScale(1, width / height);
81 | this.deviceToScreen = new L2DMatrix44();
82 | this.deviceToScreen.multTranslate(-width / 2.0, -height / 2.0);
83 | this.deviceToScreen.multScale(2 / width, -2 / width);
84 | this.gl = this.canvas.getContext('webgl2', { premultipliedAlpha: true, preserveDrawingBuffer: true });
85 | if (!this.gl) {
86 | logger.error('Failed to create WebGL context.');
87 | return;
88 | }
89 | Live2D.setGL(this.gl);
90 | this.gl.clearColor(0.0, 0.0, 0.0, 0.0);
91 | yield this.changeModelWithJSON(modelSettingPath, modelSetting);
92 | this.startDraw();
93 | });
94 | }
95 | destroy() {
96 | if (this.canvas) {
97 | this.canvas.removeEventListener('mousewheel', this._boundMouseEvent, false);
98 | this.canvas.removeEventListener('click', this._boundMouseEvent, false);
99 | document.removeEventListener('mousemove', this._boundMouseEvent, false);
100 | document.removeEventListener('mouseout', this._boundMouseEvent, false);
101 | this.canvas.removeEventListener('contextmenu', this._boundMouseEvent, false);
102 | this.canvas.removeEventListener('touchstart', this._boundTouchEvent, false);
103 | this.canvas.removeEventListener('touchend', this._boundTouchEvent, false);
104 | this.canvas.removeEventListener('touchmove', this._boundTouchEvent, false);
105 | }
106 | if (this._drawFrameId) {
107 | window.cancelAnimationFrame(this._drawFrameId);
108 | this._drawFrameId = null;
109 | }
110 | this.isDrawStart = false;
111 | if (this.live2DMgr && typeof this.live2DMgr.release === 'function') {
112 | this.live2DMgr.release();
113 | }
114 | if (this.gl) {
115 | }
116 | this.canvas = null;
117 | this.gl = null;
118 | this.dragMgr = null;
119 | this.viewMatrix = null;
120 | this.projMatrix = null;
121 | this.deviceToScreen = null;
122 | }
123 | startDraw() {
124 | if (!this.isDrawStart) {
125 | this.isDrawStart = true;
126 | const tick = () => {
127 | this.draw();
128 | this._drawFrameId = window.requestAnimationFrame(tick, this.canvas);
129 | };
130 | tick();
131 | }
132 | }
133 | draw() {
134 | MatrixStack.reset();
135 | MatrixStack.loadIdentity();
136 | this.dragMgr.update();
137 | this.live2DMgr.setDrag(this.dragMgr.getX(), this.dragMgr.getY());
138 | this.gl.clear(this.gl.COLOR_BUFFER_BIT);
139 | MatrixStack.multMatrix(this.projMatrix.getArray());
140 | MatrixStack.multMatrix(this.viewMatrix.getArray());
141 | MatrixStack.push();
142 | const model = this.live2DMgr.getModel();
143 | if (model == null)
144 | return;
145 | if (model.initialized && !model.updating) {
146 | model.update();
147 | model.draw(this.gl);
148 | }
149 | MatrixStack.pop();
150 | }
151 | changeModel(modelSettingPath) {
152 | return __awaiter(this, void 0, void 0, function* () {
153 | yield this.live2DMgr.changeModel(this.gl, modelSettingPath);
154 | });
155 | }
156 | changeModelWithJSON(modelSettingPath, modelSetting) {
157 | return __awaiter(this, void 0, void 0, function* () {
158 | yield this.live2DMgr.changeModelWithJSON(this.gl, modelSettingPath, modelSetting);
159 | });
160 | }
161 | modelScaling(scale) {
162 | const isMaxScale = this.viewMatrix.isMaxScale();
163 | const isMinScale = this.viewMatrix.isMinScale();
164 | this.viewMatrix.adjustScale(0, 0, scale);
165 | if (!isMaxScale) {
166 | if (this.viewMatrix.isMaxScale()) {
167 | this.live2DMgr.maxScaleEvent();
168 | }
169 | }
170 | if (!isMinScale) {
171 | if (this.viewMatrix.isMinScale()) {
172 | this.live2DMgr.minScaleEvent();
173 | }
174 | }
175 | }
176 | modelTurnHead(event) {
177 | var _b;
178 | const rect = this.canvas.getBoundingClientRect();
179 | const { vx, vy } = normalizePoint(event.clientX, event.clientY, rect.left + rect.width / 2, rect.top + rect.height / 2, window.innerWidth, window.innerHeight);
180 | logger.trace('onMouseDown device( x:' +
181 | event.clientX +
182 | ' y:' +
183 | event.clientY +
184 | ' ) view( x:' +
185 | vx +
186 | ' y:' +
187 | vy +
188 | ')');
189 | this.dragMgr.setPoint(vx, vy);
190 | this.live2DMgr.tapEvent(vx, vy);
191 | if ((_b = this.live2DMgr) === null || _b === void 0 ? void 0 : _b.model.hitTest(LAppDefine.HIT_AREA_BODY, vx, vy)) {
192 | window.dispatchEvent(new Event('live2d:tapbody'));
193 | }
194 | }
195 | followPointer(event) {
196 | var _b;
197 | const rect = event.target.getBoundingClientRect();
198 | const { vx, vy } = normalizePoint(event.clientX, event.clientY, rect.left + rect.width / 2, rect.top + rect.height / 2, window.innerWidth, window.innerHeight);
199 | logger.trace('onMouseMove device( x:' +
200 | event.clientX +
201 | ' y:' +
202 | event.clientY +
203 | ' ) view( x:' +
204 | vx +
205 | ' y:' +
206 | vy +
207 | ')');
208 | this.dragMgr.setPoint(vx, vy);
209 | if ((_b = this.live2DMgr) === null || _b === void 0 ? void 0 : _b.model.hitTest(LAppDefine.HIT_AREA_BODY, vx, vy)) {
210 | window.dispatchEvent(new Event('live2d:hoverbody'));
211 | }
212 | }
213 | lookFront() {
214 | this.dragMgr.setPoint(0, 0);
215 | }
216 | mouseEvent(e) {
217 | e.preventDefault();
218 | if (e.type == 'mousewheel') {
219 | if (e.wheelDelta > 0)
220 | this.modelScaling(1.1);
221 | else
222 | this.modelScaling(1);
223 | }
224 | else if (e.type == 'click' || e.type == 'contextmenu') {
225 | this.modelTurnHead(e);
226 | }
227 | else if (e.type == 'mousemove') {
228 | this.followPointer(e);
229 | }
230 | else if (e.type == 'mouseout') {
231 | this.lookFront();
232 | }
233 | }
234 | touchEvent(e) {
235 | e.preventDefault();
236 | const touch = e.touches[0];
237 | if (e.type == 'touchstart') {
238 | if (e.touches.length == 1)
239 | this.modelTurnHead(touch);
240 | }
241 | else if (e.type == 'touchmove') {
242 | this.followPointer(touch);
243 | if (e.touches.length == 2) {
244 | const touch1 = e.touches[0];
245 | const touch2 = e.touches[1];
246 | const len = Math.pow(touch1.pageX - touch2.pageX, 2) +
247 | Math.pow(touch1.pageY - touch2.pageY, 2);
248 | if (this.oldLen - len < 0)
249 | this.modelScaling(1.025);
250 | else
251 | this.modelScaling(0.975);
252 | this.oldLen = len;
253 | }
254 | }
255 | else if (e.type == 'touchend') {
256 | this.lookFront();
257 | }
258 | }
259 | transformViewX(deviceX) {
260 | const screenX = this.deviceToScreen.transformX(deviceX);
261 | return this.viewMatrix.invertTransformX(screenX);
262 | }
263 | transformViewY(deviceY) {
264 | const screenY = this.deviceToScreen.transformY(deviceY);
265 | return this.viewMatrix.invertTransformY(screenY);
266 | }
267 | transformScreenX(deviceX) {
268 | return this.deviceToScreen.transformX(deviceX);
269 | }
270 | transformScreenY(deviceY) {
271 | return this.deviceToScreen.transformY(deviceY);
272 | }
273 | }
274 | export default Cubism2Model;
275 |
--------------------------------------------------------------------------------
/build/cubism2/utils/MatrixStack.d.ts:
--------------------------------------------------------------------------------
1 | export default MatrixStack;
2 | declare class MatrixStack {
3 | static reset(): void;
4 | static loadIdentity(): void;
5 | static push(): void;
6 | static pop(): void;
7 | static getMatrix(): number[];
8 | static multMatrix(matNew: any): void;
9 | }
10 | declare namespace MatrixStack {
11 | let depth: number;
12 | let matrixStack: number[];
13 | let currentMatrix: number[];
14 | let tmp: any[];
15 | }
16 |
--------------------------------------------------------------------------------
/build/cubism2/utils/MatrixStack.js:
--------------------------------------------------------------------------------
1 | class MatrixStack {
2 | static reset() {
3 | this.depth = 0;
4 | }
5 | static loadIdentity() {
6 | for (let i = 0; i < 16; i++) {
7 | this.currentMatrix[i] = i % 5 == 0 ? 1 : 0;
8 | }
9 | }
10 | static push() {
11 | const offset = this.depth * 16;
12 | const nextOffset = (this.depth + 1) * 16;
13 | if (this.matrixStack.length < nextOffset + 16) {
14 | this.matrixStack.length = nextOffset + 16;
15 | }
16 | for (let i = 0; i < 16; i++) {
17 | this.matrixStack[nextOffset + i] = this.currentMatrix[i];
18 | }
19 | this.depth++;
20 | }
21 | static pop() {
22 | this.depth--;
23 | if (this.depth < 0) {
24 | this.depth = 0;
25 | }
26 | const offset = this.depth * 16;
27 | for (let i = 0; i < 16; i++) {
28 | this.currentMatrix[i] = this.matrixStack[offset + i];
29 | }
30 | }
31 | static getMatrix() {
32 | return this.currentMatrix;
33 | }
34 | static multMatrix(matNew) {
35 | let i, j, k;
36 | for (i = 0; i < 16; i++) {
37 | this.tmp[i] = 0;
38 | }
39 | for (i = 0; i < 4; i++) {
40 | for (j = 0; j < 4; j++) {
41 | for (k = 0; k < 4; k++) {
42 | this.tmp[i + j * 4] +=
43 | this.currentMatrix[i + k * 4] * matNew[k + j * 4];
44 | }
45 | }
46 | }
47 | for (i = 0; i < 16; i++) {
48 | this.currentMatrix[i] = this.tmp[i];
49 | }
50 | }
51 | }
52 | MatrixStack.matrixStack = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
53 | MatrixStack.depth = 0;
54 | MatrixStack.currentMatrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
55 | MatrixStack.tmp = new Array(16);
56 | export default MatrixStack;
57 |
--------------------------------------------------------------------------------
/build/cubism2/utils/ModelSettingJson.d.ts:
--------------------------------------------------------------------------------
1 | export default ModelSettingJson;
2 | declare class ModelSettingJson {
3 | NAME: string;
4 | ID: string;
5 | MODEL: string;
6 | TEXTURES: string;
7 | HIT_AREAS: string;
8 | HIT_AREAS_CUSTOM: string;
9 | PHYSICS: string;
10 | POSE: string;
11 | EXPRESSIONS: string;
12 | MOTION_GROUPS: string;
13 | SOUND: string;
14 | FADE_IN: string;
15 | FADE_OUT: string;
16 | LAYOUT: string;
17 | INIT_PARAM: string;
18 | INIT_PARTS_VISIBLE: string;
19 | VALUE: string;
20 | FILE: string;
21 | json: {};
22 | loadModelSetting(path: any, callback: any): void;
23 | getTextureFile(n: any): any;
24 | getModelFile(): any;
25 | getTextureNum(): any;
26 | getHitAreaNum(): any;
27 | getHitAreaCustom(): any;
28 | getHitAreaID(n: any): any;
29 | getHitAreaName(n: any): any;
30 | getPhysicsFile(): any;
31 | getPoseFile(): any;
32 | getExpressionNum(): any;
33 | getExpressionFile(n: any): any;
34 | getExpressionName(n: any): any;
35 | getLayout(): any;
36 | getInitParamNum(): any;
37 | getMotionNum(name: any): any;
38 | getMotionFile(name: any, n: any): any;
39 | getMotionSound(name: any, n: any): any;
40 | getMotionFadeIn(name: any, n: any): any;
41 | getMotionFadeOut(name: any, n: any): any;
42 | getInitParamID(n: any): any;
43 | getInitParamValue(n: any): any;
44 | getInitPartsVisibleNum(): any;
45 | getInitPartsVisibleID(n: any): any;
46 | getInitPartsVisibleValue(n: any): any;
47 | }
48 |
--------------------------------------------------------------------------------
/build/cubism2/utils/ModelSettingJson.js:
--------------------------------------------------------------------------------
1 | import { Live2DFramework } from '../Live2DFramework.js';
2 | class ModelSettingJson {
3 | constructor() {
4 | this.NAME = 'name';
5 | this.ID = 'id';
6 | this.MODEL = 'model';
7 | this.TEXTURES = 'textures';
8 | this.HIT_AREAS = 'hit_areas';
9 | this.HIT_AREAS_CUSTOM = 'hit_areas_custom';
10 | this.PHYSICS = 'physics';
11 | this.POSE = 'pose';
12 | this.EXPRESSIONS = 'expressions';
13 | this.MOTION_GROUPS = 'motions';
14 | this.SOUND = 'sound';
15 | this.FADE_IN = 'fade_in';
16 | this.FADE_OUT = 'fade_out';
17 | this.LAYOUT = 'layout';
18 | this.INIT_PARAM = 'init_param';
19 | this.INIT_PARTS_VISIBLE = 'init_parts_visible';
20 | this.VALUE = 'val';
21 | this.FILE = 'file';
22 | this.json = {};
23 | }
24 | loadModelSetting(path, callback) {
25 | const pm = Live2DFramework.getPlatformManager();
26 | pm.loadBytes(path, buf => {
27 | const str = String.fromCharCode.apply(null, new Uint8Array(buf));
28 | this.json = JSON.parse(str);
29 | callback();
30 | });
31 | }
32 | getTextureFile(n) {
33 | if (this.json[this.TEXTURES] == null || this.json[this.TEXTURES][n] == null)
34 | return null;
35 | return this.json[this.TEXTURES][n];
36 | }
37 | getModelFile() {
38 | return this.json[this.MODEL];
39 | }
40 | getTextureNum() {
41 | if (this.json[this.TEXTURES] == null)
42 | return 0;
43 | return this.json[this.TEXTURES].length;
44 | }
45 | getHitAreaNum() {
46 | if (this.json[this.HIT_AREAS] == null)
47 | return 0;
48 | return this.json[this.HIT_AREAS].length;
49 | }
50 | getHitAreaCustom() {
51 | return this.json[this.HIT_AREAS_CUSTOM];
52 | }
53 | getHitAreaID(n) {
54 | if (this.json[this.HIT_AREAS] == null ||
55 | this.json[this.HIT_AREAS][n] == null)
56 | return null;
57 | return this.json[this.HIT_AREAS][n][this.ID];
58 | }
59 | getHitAreaName(n) {
60 | if (this.json[this.HIT_AREAS] == null ||
61 | this.json[this.HIT_AREAS][n] == null)
62 | return null;
63 | return this.json[this.HIT_AREAS][n][this.NAME];
64 | }
65 | getPhysicsFile() {
66 | return this.json[this.PHYSICS];
67 | }
68 | getPoseFile() {
69 | return this.json[this.POSE];
70 | }
71 | getExpressionNum() {
72 | return this.json[this.EXPRESSIONS] == null
73 | ? 0
74 | : this.json[this.EXPRESSIONS].length;
75 | }
76 | getExpressionFile(n) {
77 | if (this.json[this.EXPRESSIONS] == null)
78 | return null;
79 | return this.json[this.EXPRESSIONS][n][this.FILE];
80 | }
81 | getExpressionName(n) {
82 | if (this.json[this.EXPRESSIONS] == null)
83 | return null;
84 | return this.json[this.EXPRESSIONS][n][this.NAME];
85 | }
86 | getLayout() {
87 | return this.json[this.LAYOUT];
88 | }
89 | getInitParamNum() {
90 | return this.json[this.INIT_PARAM] == null
91 | ? 0
92 | : this.json[this.INIT_PARAM].length;
93 | }
94 | getMotionNum(name) {
95 | if (this.json[this.MOTION_GROUPS] == null ||
96 | this.json[this.MOTION_GROUPS][name] == null)
97 | return 0;
98 | return this.json[this.MOTION_GROUPS][name].length;
99 | }
100 | getMotionFile(name, n) {
101 | if (this.json[this.MOTION_GROUPS] == null ||
102 | this.json[this.MOTION_GROUPS][name] == null ||
103 | this.json[this.MOTION_GROUPS][name][n] == null)
104 | return null;
105 | return this.json[this.MOTION_GROUPS][name][n][this.FILE];
106 | }
107 | getMotionSound(name, n) {
108 | if (this.json[this.MOTION_GROUPS] == null ||
109 | this.json[this.MOTION_GROUPS][name] == null ||
110 | this.json[this.MOTION_GROUPS][name][n] == null ||
111 | this.json[this.MOTION_GROUPS][name][n][this.SOUND] == null)
112 | return null;
113 | return this.json[this.MOTION_GROUPS][name][n][this.SOUND];
114 | }
115 | getMotionFadeIn(name, n) {
116 | if (this.json[this.MOTION_GROUPS] == null ||
117 | this.json[this.MOTION_GROUPS][name] == null ||
118 | this.json[this.MOTION_GROUPS][name][n] == null ||
119 | this.json[this.MOTION_GROUPS][name][n][this.FADE_IN] == null)
120 | return 1000;
121 | return this.json[this.MOTION_GROUPS][name][n][this.FADE_IN];
122 | }
123 | getMotionFadeOut(name, n) {
124 | if (this.json[this.MOTION_GROUPS] == null ||
125 | this.json[this.MOTION_GROUPS][name] == null ||
126 | this.json[this.MOTION_GROUPS][name][n] == null ||
127 | this.json[this.MOTION_GROUPS][name][n][this.FADE_OUT] == null)
128 | return 1000;
129 | return this.json[this.MOTION_GROUPS][name][n][this.FADE_OUT];
130 | }
131 | getInitParamID(n) {
132 | if (this.json[this.INIT_PARAM] == null ||
133 | this.json[this.INIT_PARAM][n] == null)
134 | return null;
135 | return this.json[this.INIT_PARAM][n][this.ID];
136 | }
137 | getInitParamValue(n) {
138 | if (this.json[this.INIT_PARAM] == null ||
139 | this.json[this.INIT_PARAM][n] == null)
140 | return NaN;
141 | return this.json[this.INIT_PARAM][n][this.VALUE];
142 | }
143 | getInitPartsVisibleNum() {
144 | return this.json[this.INIT_PARTS_VISIBLE] == null
145 | ? 0
146 | : this.json[this.INIT_PARTS_VISIBLE].length;
147 | }
148 | getInitPartsVisibleID(n) {
149 | if (this.json[this.INIT_PARTS_VISIBLE] == null ||
150 | this.json[this.INIT_PARTS_VISIBLE][n] == null)
151 | return null;
152 | return this.json[this.INIT_PARTS_VISIBLE][n][this.ID];
153 | }
154 | getInitPartsVisibleValue(n) {
155 | if (this.json[this.INIT_PARTS_VISIBLE] == null ||
156 | this.json[this.INIT_PARTS_VISIBLE][n] == null)
157 | return NaN;
158 | return this.json[this.INIT_PARTS_VISIBLE][n][this.VALUE];
159 | }
160 | }
161 | export default ModelSettingJson;
162 |
--------------------------------------------------------------------------------
/build/cubism5/index.d.ts:
--------------------------------------------------------------------------------
1 | export class AppDelegate extends LAppDelegate {
2 | _drawFrameId: number;
3 | stop(): void;
4 | transformOffset(e: any): {
5 | x: number;
6 | y: number;
7 | };
8 | onMouseMove(e: any): void;
9 | onMouseEnd(e: any): void;
10 | onTap(e: any): void;
11 | mouseMoveEventListener: any;
12 | mouseEndedEventListener: any;
13 | tapEventListener: any;
14 | changeModel(modelSettingPath: string): void;
15 | get subdelegates(): import("@framework/type/csmvector.js").csmVector;
16 | }
17 | import { LAppDelegate } from '@demo/lappdelegate.js';
18 | import { LAppSubdelegate } from '@demo/lappsubdelegate.js';
19 |
--------------------------------------------------------------------------------
/build/cubism5/index.js:
--------------------------------------------------------------------------------
1 | import { LAppDelegate } from '@demo/lappdelegate.js';
2 | import { LAppSubdelegate } from '@demo/lappsubdelegate.js';
3 | import * as LAppDefine from '@demo/lappdefine.js';
4 | import { LAppModel } from '@demo/lappmodel.js';
5 | import { LAppPal } from '@demo/lapppal';
6 | import logger from '../logger.js';
7 | LAppPal.printMessage = () => { };
8 | class AppSubdelegate extends LAppSubdelegate {
9 | initialize(canvas) {
10 | if (!this._glManager.initialize(canvas)) {
11 | return false;
12 | }
13 | this._canvas = canvas;
14 | if (LAppDefine.CanvasSize === 'auto') {
15 | this.resizeCanvas();
16 | }
17 | else {
18 | canvas.width = LAppDefine.CanvasSize.width;
19 | canvas.height = LAppDefine.CanvasSize.height;
20 | }
21 | this._textureManager.setGlManager(this._glManager);
22 | const gl = this._glManager.getGl();
23 | if (!this._frameBuffer) {
24 | this._frameBuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING);
25 | }
26 | gl.enable(gl.BLEND);
27 | gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
28 | this._view.initialize(this);
29 | this._view._gear = {
30 | render: () => { },
31 | isHit: () => { },
32 | release: () => { }
33 | };
34 | this._view._back = {
35 | render: () => { },
36 | release: () => { }
37 | };
38 | this._live2dManager._subdelegate = this;
39 | this._resizeObserver = new window.ResizeObserver((entries, observer) => this.resizeObserverCallback.call(this, entries, observer));
40 | this._resizeObserver.observe(this._canvas);
41 | return true;
42 | }
43 | onResize() {
44 | this.resizeCanvas();
45 | this._view.initialize(this);
46 | }
47 | update() {
48 | if (this._glManager.getGl().isContextLost()) {
49 | return;
50 | }
51 | if (this._needResize) {
52 | this.onResize();
53 | this._needResize = false;
54 | }
55 | const gl = this._glManager.getGl();
56 | gl.clearColor(0.0, 0.0, 0.0, 0.0);
57 | gl.enable(gl.DEPTH_TEST);
58 | gl.depthFunc(gl.LEQUAL);
59 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
60 | gl.clearDepth(1.0);
61 | gl.enable(gl.BLEND);
62 | gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
63 | this._view.render();
64 | }
65 | }
66 | export class AppDelegate extends LAppDelegate {
67 | run() {
68 | const loop = () => {
69 | LAppPal.updateTime();
70 | for (let i = 0; i < this._subdelegates.getSize(); i++) {
71 | this._subdelegates.at(i).update();
72 | }
73 | this._drawFrameId = window.requestAnimationFrame(loop);
74 | };
75 | loop();
76 | }
77 | stop() {
78 | if (this._drawFrameId) {
79 | window.cancelAnimationFrame(this._drawFrameId);
80 | this._drawFrameId = null;
81 | }
82 | }
83 | release() {
84 | this.stop();
85 | this.releaseEventListener();
86 | this._subdelegates.clear();
87 | this._cubismOption = null;
88 | }
89 | transformOffset(e) {
90 | const subdelegate = this._subdelegates.at(0);
91 | const rect = subdelegate.getCanvas().getBoundingClientRect();
92 | const localX = e.pageX - rect.left;
93 | const localY = e.pageY - rect.top;
94 | const posX = localX * window.devicePixelRatio;
95 | const posY = localY * window.devicePixelRatio;
96 | const x = subdelegate._view.transformViewX(posX);
97 | const y = subdelegate._view.transformViewY(posY);
98 | return {
99 | x, y
100 | };
101 | }
102 | onMouseMove(e) {
103 | const lapplive2dmanager = this._subdelegates.at(0).getLive2DManager();
104 | const { x, y } = this.transformOffset(e);
105 | const model = lapplive2dmanager._models.at(0);
106 | lapplive2dmanager.onDrag(x, y);
107 | lapplive2dmanager.onTap(x, y);
108 | if (model.hitTest(LAppDefine.HitAreaNameBody, x, y)) {
109 | window.dispatchEvent(new Event('live2d:hoverbody'));
110 | }
111 | }
112 | onMouseEnd(e) {
113 | const lapplive2dmanager = this._subdelegates.at(0).getLive2DManager();
114 | const { x, y } = this.transformOffset(e);
115 | lapplive2dmanager.onDrag(0.0, 0.0);
116 | lapplive2dmanager.onTap(x, y);
117 | }
118 | onTap(e) {
119 | const lapplive2dmanager = this._subdelegates.at(0).getLive2DManager();
120 | const { x, y } = this.transformOffset(e);
121 | const model = lapplive2dmanager._models.at(0);
122 | if (model.hitTest(LAppDefine.HitAreaNameBody, x, y)) {
123 | window.dispatchEvent(new Event('live2d:tapbody'));
124 | }
125 | }
126 | initializeEventListener() {
127 | this.mouseMoveEventListener = this.onMouseMove.bind(this);
128 | this.mouseEndedEventListener = this.onMouseEnd.bind(this);
129 | this.tapEventListener = this.onTap.bind(this);
130 | document.addEventListener('mousemove', this.mouseMoveEventListener, {
131 | passive: true
132 | });
133 | document.addEventListener('mouseout', this.mouseEndedEventListener, {
134 | passive: true
135 | });
136 | document.addEventListener('pointerdown', this.tapEventListener, {
137 | passive: true
138 | });
139 | }
140 | releaseEventListener() {
141 | document.removeEventListener('mousemove', this.mouseMoveEventListener, {
142 | passive: true
143 | });
144 | this.mouseMoveEventListener = null;
145 | document.removeEventListener('mouseout', this.mouseEndedEventListener, {
146 | passive: true
147 | });
148 | this.mouseEndedEventListener = null;
149 | document.removeEventListener('pointerdown', this.tapEventListener, {
150 | passive: true
151 | });
152 | }
153 | initializeSubdelegates() {
154 | this._canvases.prepareCapacity(LAppDefine.CanvasNum);
155 | this._subdelegates.prepareCapacity(LAppDefine.CanvasNum);
156 | const canvas = document.getElementById('live2d');
157 | this._canvases.pushBack(canvas);
158 | canvas.style.width = canvas.width;
159 | canvas.style.height = canvas.height;
160 | for (let i = 0; i < this._canvases.getSize(); i++) {
161 | const subdelegate = new AppSubdelegate();
162 | const result = subdelegate.initialize(this._canvases.at(i));
163 | if (!result) {
164 | logger.error('Failed to initialize AppSubdelegate');
165 | return;
166 | }
167 | this._subdelegates.pushBack(subdelegate);
168 | }
169 | for (let i = 0; i < LAppDefine.CanvasNum; i++) {
170 | if (this._subdelegates.at(i).isContextLost()) {
171 | logger.error(`The context for Canvas at index ${i} was lost, possibly because the acquisition limit for WebGLRenderingContext was reached.`);
172 | }
173 | }
174 | }
175 | changeModel(modelSettingPath) {
176 | const segments = modelSettingPath.split('/');
177 | const modelJsonName = segments.pop();
178 | const modelPath = segments.join('/') + '/';
179 | const live2dManager = this._subdelegates.at(0).getLive2DManager();
180 | live2dManager.releaseAllModel();
181 | const instance = new LAppModel();
182 | instance.setSubdelegate(live2dManager._subdelegate);
183 | instance.loadAssets(modelPath, modelJsonName);
184 | live2dManager._models.pushBack(instance);
185 | }
186 | get subdelegates() {
187 | return this._subdelegates;
188 | }
189 | }
190 |
--------------------------------------------------------------------------------
/build/drag.d.ts:
--------------------------------------------------------------------------------
1 | declare function registerDrag(): void;
2 | export default registerDrag;
3 |
--------------------------------------------------------------------------------
/build/drag.js:
--------------------------------------------------------------------------------
1 | function registerDrag() {
2 | const element = document.getElementById('waifu');
3 | if (!element)
4 | return;
5 | let winWidth = window.innerWidth, winHeight = window.innerHeight;
6 | const imgWidth = element.offsetWidth, imgHeight = element.offsetHeight;
7 | element.addEventListener('mousedown', event => {
8 | if (event.button === 2) {
9 | return;
10 | }
11 | const canvas = document.getElementById('live2d');
12 | if (event.target !== canvas)
13 | return;
14 | event.preventDefault();
15 | const _offsetX = event.offsetX, _offsetY = event.offsetY;
16 | document.onmousemove = event => {
17 | const _x = event.clientX, _y = event.clientY;
18 | let _left = _x - _offsetX, _top = _y - _offsetY;
19 | if (_top < 0) {
20 | _top = 0;
21 | }
22 | else if (_top >= winHeight - imgHeight) {
23 | _top = winHeight - imgHeight;
24 | }
25 | if (_left < 0) {
26 | _left = 0;
27 | }
28 | else if (_left >= winWidth - imgWidth) {
29 | _left = winWidth - imgWidth;
30 | }
31 | element.style.top = _top + 'px';
32 | element.style.left = _left + 'px';
33 | };
34 | document.onmouseup = () => {
35 | document.onmousemove = null;
36 | };
37 | });
38 | window.onresize = () => {
39 | winWidth = window.innerWidth;
40 | winHeight = window.innerHeight;
41 | };
42 | }
43 | export default registerDrag;
44 |
--------------------------------------------------------------------------------
/build/icons.d.ts:
--------------------------------------------------------------------------------
1 | declare const fa_comment = "";
2 | declare const fa_paper_plane = "";
3 | declare const fa_street_view = "";
4 | declare const fa_shirt = "";
5 | declare const fa_camera_retro = "";
6 | declare const fa_info_circle = "";
7 | declare const fa_xmark = "";
8 | declare const fa_child = "";
9 | export { fa_comment, fa_paper_plane, fa_street_view, fa_shirt, fa_camera_retro, fa_info_circle, fa_xmark, fa_child };
10 |
--------------------------------------------------------------------------------
/build/icons.js:
--------------------------------------------------------------------------------
1 | const fa_comment = '';
2 | const fa_paper_plane = '';
3 | const fa_street_view = '';
4 | const fa_shirt = '';
5 | const fa_camera_retro = '';
6 | const fa_info_circle = '';
7 | const fa_xmark = '';
8 | const fa_child = '';
9 | export { fa_comment, fa_paper_plane, fa_street_view, fa_shirt, fa_camera_retro, fa_info_circle, fa_xmark, fa_child };
10 |
--------------------------------------------------------------------------------
/build/index.d.ts:
--------------------------------------------------------------------------------
1 | export { default as registerDrag } from './drag.js';
2 | export { default as logger, LogLevel } from './logger.js';
3 | export { default as Cubism2Model } from './cubism2/index.js';
4 | export * from './tools.js';
5 | export * from './message.js';
6 | export * from './model.js';
7 | export * from './utils.js';
8 | export * from './widget.js';
9 |
--------------------------------------------------------------------------------
/build/index.js:
--------------------------------------------------------------------------------
1 | export { default as registerDrag } from './drag.js';
2 | export { default as logger } from './logger.js';
3 | export { default as Cubism2Model } from './cubism2/index.js';
4 | export * from './tools.js';
5 | export * from './message.js';
6 | export * from './model.js';
7 | export * from './utils.js';
8 | export * from './widget.js';
9 |
--------------------------------------------------------------------------------
/build/logger.d.ts:
--------------------------------------------------------------------------------
1 | type LogLevel = 'error' | 'warn' | 'info' | 'trace';
2 | declare class Logger {
3 | private static levelOrder;
4 | private level;
5 | constructor(level?: LogLevel);
6 | setLevel(level: LogLevel | undefined): void;
7 | private shouldLog;
8 | error(message: string, ...args: any[]): void;
9 | warn(message: string, ...args: any[]): void;
10 | info(message: string, ...args: any[]): void;
11 | trace(message: string, ...args: any[]): void;
12 | }
13 | declare const logger: Logger;
14 | export default logger;
15 | export { LogLevel };
16 |
--------------------------------------------------------------------------------
/build/logger.js:
--------------------------------------------------------------------------------
1 | class Logger {
2 | constructor(level = 'info') {
3 | this.level = level;
4 | }
5 | setLevel(level) {
6 | if (!level)
7 | return;
8 | this.level = level;
9 | }
10 | shouldLog(level) {
11 | return Logger.levelOrder[level] <= Logger.levelOrder[this.level];
12 | }
13 | error(message, ...args) {
14 | if (this.shouldLog('error')) {
15 | console.error('[Live2D Widget][ERROR]', message, ...args);
16 | }
17 | }
18 | warn(message, ...args) {
19 | if (this.shouldLog('warn')) {
20 | console.warn('[Live2D Widget][WARN]', message, ...args);
21 | }
22 | }
23 | info(message, ...args) {
24 | if (this.shouldLog('info')) {
25 | console.log('[Live2D Widget][INFO]', message, ...args);
26 | }
27 | }
28 | trace(message, ...args) {
29 | if (this.shouldLog('trace')) {
30 | console.log('[Live2D Widget][TRACE]', message, ...args);
31 | }
32 | }
33 | }
34 | Logger.levelOrder = {
35 | error: 0,
36 | warn: 1,
37 | info: 2,
38 | trace: 3,
39 | };
40 | const logger = new Logger();
41 | export default logger;
42 |
--------------------------------------------------------------------------------
/build/message.d.ts:
--------------------------------------------------------------------------------
1 | type Time = {
2 | hour: string;
3 | text: string;
4 | }[];
5 | declare function showMessage(text: string | string[], timeout: number, priority: number, override?: boolean): void;
6 | declare function welcomeMessage(time: Time, welcomeTemplate: string, referrerTemplate: string): string;
7 | declare function i18n(template: string, ...args: string[]): string;
8 | export { showMessage, welcomeMessage, i18n, Time };
9 |
--------------------------------------------------------------------------------
/build/message.js:
--------------------------------------------------------------------------------
1 | import { randomSelection } from './utils.js';
2 | let messageTimer = null;
3 | function showMessage(text, timeout, priority, override = true) {
4 | let currentPriority = parseInt(sessionStorage.getItem('waifu-message-priority'), 10);
5 | if (isNaN(currentPriority)) {
6 | currentPriority = 0;
7 | }
8 | if (!text ||
9 | (override && currentPriority > priority) ||
10 | (!override && currentPriority >= priority))
11 | return;
12 | if (messageTimer) {
13 | clearTimeout(messageTimer);
14 | messageTimer = null;
15 | }
16 | text = randomSelection(text);
17 | sessionStorage.setItem('waifu-message-priority', String(priority));
18 | const tips = document.getElementById('waifu-tips');
19 | tips.innerHTML = text;
20 | tips.classList.add('waifu-tips-active');
21 | messageTimer = setTimeout(() => {
22 | sessionStorage.removeItem('waifu-message-priority');
23 | tips.classList.remove('waifu-tips-active');
24 | }, timeout);
25 | }
26 | function welcomeMessage(time, welcomeTemplate, referrerTemplate) {
27 | if (location.pathname === '/') {
28 | for (const { hour, text } of time) {
29 | const now = new Date(), after = hour.split('-')[0], before = hour.split('-')[1] || after;
30 | if (Number(after) <= now.getHours() &&
31 | now.getHours() <= Number(before)) {
32 | return text;
33 | }
34 | }
35 | }
36 | const text = i18n(welcomeTemplate, document.title);
37 | if (document.referrer !== '') {
38 | const referrer = new URL(document.referrer);
39 | if (location.hostname === referrer.hostname)
40 | return text;
41 | return `${i18n(referrerTemplate, referrer.hostname)}
${text}`;
42 | }
43 | return text;
44 | }
45 | function i18n(template, ...args) {
46 | return template.replace(/\$(\d+)/g, (_, idx) => {
47 | var _b;
48 | const i = parseInt(idx, 10) - 1;
49 | return (_b = args[i]) !== null && _b !== void 0 ? _b : '';
50 | });
51 | }
52 | export { showMessage, welcomeMessage, i18n };
53 |
--------------------------------------------------------------------------------
/build/model.d.ts:
--------------------------------------------------------------------------------
1 | import { LogLevel } from './logger.js';
2 | interface ModelList {
3 | name: string;
4 | paths: string[];
5 | message: string;
6 | }
7 | interface Config {
8 | waifuPath: string;
9 | apiPath?: string;
10 | cdnPath?: string;
11 | cubism2Path?: string;
12 | cubism5Path?: string;
13 | modelId?: number;
14 | tools?: string[];
15 | drag?: boolean;
16 | logLevel?: LogLevel;
17 | }
18 | declare class ModelManager {
19 | readonly useCDN: boolean;
20 | private readonly cdnPath;
21 | private readonly cubism2Path;
22 | private readonly cubism5Path;
23 | private _modelId;
24 | private _modelTexturesId;
25 | private modelList;
26 | private cubism2model;
27 | private cubism5model;
28 | private currentModelVersion;
29 | private loading;
30 | private modelJSONCache;
31 | private models;
32 | private constructor();
33 | static initCheck(config: Config, models?: ModelList[]): Promise;
34 | set modelId(modelId: number);
35 | get modelId(): number;
36 | set modelTexturesId(modelTexturesId: number);
37 | get modelTexturesId(): number;
38 | resetCanvas(): void;
39 | fetchWithCache(url: string): Promise;
40 | checkModelVersion(modelSetting: any): 2 | 3;
41 | loadLive2D(modelSettingPath: string, modelSetting: object): Promise;
42 | loadTextureCache(modelName: string): Promise;
43 | loadModel(message: string | string[]): Promise;
44 | loadRandTexture(successMessage?: string | string[], failMessage?: string | string[]): Promise;
45 | loadNextModel(): Promise;
46 | }
47 | export { ModelManager, Config, ModelList };
48 |
--------------------------------------------------------------------------------
/build/model.js:
--------------------------------------------------------------------------------
1 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3 | return new (P || (P = Promise))(function (resolve, reject) {
4 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7 | step((generator = generator.apply(thisArg, _arguments || [])).next());
8 | });
9 | };
10 | import { showMessage } from './message.js';
11 | import { loadExternalResource, randomOtherOption } from './utils.js';
12 | import logger from './logger.js';
13 | class ModelManager {
14 | constructor(config, models = []) {
15 | var _b;
16 | this.modelList = null;
17 | let { apiPath, cdnPath } = config;
18 | const { cubism2Path, cubism5Path } = config;
19 | let useCDN = false;
20 | if (typeof cdnPath === 'string') {
21 | if (!cdnPath.endsWith('/'))
22 | cdnPath += '/';
23 | useCDN = true;
24 | }
25 | else if (typeof apiPath === 'string') {
26 | if (!apiPath.endsWith('/'))
27 | apiPath += '/';
28 | cdnPath = apiPath;
29 | useCDN = true;
30 | logger.warn('apiPath option is deprecated. Please use cdnPath instead.');
31 | }
32 | else if (!models.length) {
33 | throw 'Invalid initWidget argument!';
34 | }
35 | let modelId = parseInt(localStorage.getItem('modelId'), 10);
36 | let modelTexturesId = parseInt(localStorage.getItem('modelTexturesId'), 10);
37 | if (isNaN(modelId) || isNaN(modelTexturesId)) {
38 | modelTexturesId = 0;
39 | }
40 | if (isNaN(modelId)) {
41 | modelId = (_b = config.modelId) !== null && _b !== void 0 ? _b : 0;
42 | }
43 | this.useCDN = useCDN;
44 | this.cdnPath = cdnPath || '';
45 | this.cubism2Path = cubism2Path || '';
46 | this.cubism5Path = cubism5Path || '';
47 | this._modelId = modelId;
48 | this._modelTexturesId = modelTexturesId;
49 | this.currentModelVersion = 0;
50 | this.loading = false;
51 | this.modelJSONCache = {};
52 | this.models = models;
53 | }
54 | static initCheck(config_1) {
55 | return __awaiter(this, arguments, void 0, function* (config, models = []) {
56 | const model = new ModelManager(config, models);
57 | if (model.useCDN) {
58 | const response = yield fetch(`${model.cdnPath}model_list.json`);
59 | model.modelList = yield response.json();
60 | if (model.modelId >= model.modelList.models.length) {
61 | model.modelId = 0;
62 | }
63 | const modelName = model.modelList.models[model.modelId];
64 | if (Array.isArray(modelName)) {
65 | if (model.modelTexturesId >= modelName.length) {
66 | model.modelTexturesId = 0;
67 | }
68 | }
69 | else {
70 | const modelSettingPath = `${model.cdnPath}model/${modelName}/index.json`;
71 | const modelSetting = yield model.fetchWithCache(modelSettingPath);
72 | const version = model.checkModelVersion(modelSetting);
73 | if (version === 2) {
74 | const textureCache = yield model.loadTextureCache(modelName);
75 | if (model.modelTexturesId >= textureCache.length) {
76 | model.modelTexturesId = 0;
77 | }
78 | }
79 | }
80 | }
81 | else {
82 | if (model.modelId >= model.models.length) {
83 | model.modelId = 0;
84 | }
85 | if (model.modelTexturesId >= model.models[model.modelId].paths.length) {
86 | model.modelTexturesId = 0;
87 | }
88 | }
89 | return model;
90 | });
91 | }
92 | set modelId(modelId) {
93 | this._modelId = modelId;
94 | localStorage.setItem('modelId', modelId.toString());
95 | }
96 | get modelId() {
97 | return this._modelId;
98 | }
99 | set modelTexturesId(modelTexturesId) {
100 | this._modelTexturesId = modelTexturesId;
101 | localStorage.setItem('modelTexturesId', modelTexturesId.toString());
102 | }
103 | get modelTexturesId() {
104 | return this._modelTexturesId;
105 | }
106 | resetCanvas() {
107 | document.getElementById('waifu-canvas').innerHTML = '';
108 | }
109 | fetchWithCache(url) {
110 | return __awaiter(this, void 0, void 0, function* () {
111 | let result;
112 | if (url in this.modelJSONCache) {
113 | result = this.modelJSONCache[url];
114 | }
115 | else {
116 | try {
117 | const response = yield fetch(url);
118 | result = yield response.json();
119 | }
120 | catch (_b) {
121 | result = null;
122 | }
123 | this.modelJSONCache[url] = result;
124 | }
125 | return result;
126 | });
127 | }
128 | checkModelVersion(modelSetting) {
129 | if (modelSetting.Version === 3 || modelSetting.FileReferences) {
130 | return 3;
131 | }
132 | return 2;
133 | }
134 | loadLive2D(modelSettingPath, modelSetting) {
135 | return __awaiter(this, void 0, void 0, function* () {
136 | if (this.loading) {
137 | logger.warn('Still loading. Abort.');
138 | return;
139 | }
140 | this.loading = true;
141 | try {
142 | const version = this.checkModelVersion(modelSetting);
143 | if (version === 2) {
144 | if (!this.cubism2model) {
145 | if (!this.cubism2Path) {
146 | logger.error('No cubism2Path set, cannot load Cubism 2 Core.');
147 | return;
148 | }
149 | yield loadExternalResource(this.cubism2Path, 'js');
150 | const { default: Cubism2Model } = yield import('./cubism2/index.js');
151 | this.cubism2model = new Cubism2Model();
152 | }
153 | if (this.currentModelVersion === 3) {
154 | this.cubism5model.release();
155 | this.resetCanvas();
156 | }
157 | if (this.currentModelVersion === 3 || !this.cubism2model.gl) {
158 | yield this.cubism2model.init('live2d', modelSettingPath, modelSetting);
159 | }
160 | else {
161 | yield this.cubism2model.changeModelWithJSON(modelSettingPath, modelSetting);
162 | }
163 | }
164 | else {
165 | if (!this.cubism5Path) {
166 | logger.error('No cubism5Path set, cannot load Cubism 5 Core.');
167 | return;
168 | }
169 | yield loadExternalResource(this.cubism5Path, 'js');
170 | const { AppDelegate: Cubism5Model } = yield import('./cubism5/index.js');
171 | this.cubism5model = new Cubism5Model();
172 | if (this.currentModelVersion === 2) {
173 | this.cubism2model.destroy();
174 | this.resetCanvas();
175 | }
176 | if (this.currentModelVersion === 2 || !this.cubism5model.subdelegates.at(0)) {
177 | this.cubism5model.initialize();
178 | this.cubism5model.changeModel(modelSettingPath);
179 | this.cubism5model.run();
180 | }
181 | else {
182 | this.cubism5model.changeModel(modelSettingPath);
183 | }
184 | }
185 | logger.info(`Model ${modelSettingPath} (Cubism version ${version}) loaded`);
186 | this.currentModelVersion = version;
187 | }
188 | catch (err) {
189 | console.error('loadLive2D failed', err);
190 | }
191 | this.loading = false;
192 | });
193 | }
194 | loadTextureCache(modelName) {
195 | return __awaiter(this, void 0, void 0, function* () {
196 | const textureCache = yield this.fetchWithCache(`${this.cdnPath}model/${modelName}/textures.cache`);
197 | return textureCache || [];
198 | });
199 | }
200 | loadModel(message) {
201 | return __awaiter(this, void 0, void 0, function* () {
202 | let modelSettingPath, modelSetting;
203 | if (this.useCDN) {
204 | let modelName = this.modelList.models[this.modelId];
205 | if (Array.isArray(modelName)) {
206 | modelName = modelName[this.modelTexturesId];
207 | }
208 | modelSettingPath = `${this.cdnPath}model/${modelName}/index.json`;
209 | modelSetting = yield this.fetchWithCache(modelSettingPath);
210 | const version = this.checkModelVersion(modelSetting);
211 | if (version === 2) {
212 | const textureCache = yield this.loadTextureCache(modelName);
213 | let textures = textureCache[this.modelTexturesId];
214 | if (typeof textures === 'string')
215 | textures = [textures];
216 | modelSetting.textures = textures;
217 | }
218 | }
219 | else {
220 | modelSettingPath = this.models[this.modelId].paths[this.modelTexturesId];
221 | modelSetting = yield this.fetchWithCache(modelSettingPath);
222 | }
223 | yield this.loadLive2D(modelSettingPath, modelSetting);
224 | showMessage(message, 4000, 10);
225 | });
226 | }
227 | loadRandTexture() {
228 | return __awaiter(this, arguments, void 0, function* (successMessage = '', failMessage = '') {
229 | const { modelId } = this;
230 | let noTextureAvailable = false;
231 | if (this.useCDN) {
232 | const modelName = this.modelList.models[modelId];
233 | if (Array.isArray(modelName)) {
234 | this.modelTexturesId = randomOtherOption(modelName.length, this.modelTexturesId);
235 | }
236 | else {
237 | const modelSettingPath = `${this.cdnPath}model/${modelName}/index.json`;
238 | const modelSetting = yield this.fetchWithCache(modelSettingPath);
239 | const version = this.checkModelVersion(modelSetting);
240 | if (version === 2) {
241 | const textureCache = yield this.loadTextureCache(modelName);
242 | if (textureCache.length <= 1) {
243 | noTextureAvailable = true;
244 | }
245 | else {
246 | this.modelTexturesId = randomOtherOption(textureCache.length, this.modelTexturesId);
247 | }
248 | }
249 | else {
250 | noTextureAvailable = true;
251 | }
252 | }
253 | }
254 | else {
255 | if (this.models[modelId].paths.length === 1) {
256 | noTextureAvailable = true;
257 | }
258 | else {
259 | this.modelTexturesId = randomOtherOption(this.models[modelId].paths.length, this.modelTexturesId);
260 | }
261 | }
262 | if (noTextureAvailable) {
263 | showMessage(failMessage, 4000, 10);
264 | }
265 | else {
266 | yield this.loadModel(successMessage);
267 | }
268 | });
269 | }
270 | loadNextModel() {
271 | return __awaiter(this, void 0, void 0, function* () {
272 | this.modelTexturesId = 0;
273 | if (this.useCDN) {
274 | this.modelId = (this.modelId + 1) % this.modelList.models.length;
275 | yield this.loadModel(this.modelList.messages[this.modelId]);
276 | }
277 | else {
278 | this.modelId = (this.modelId + 1) % this.models.length;
279 | yield this.loadModel(this.models[this.modelId].message);
280 | }
281 | });
282 | }
283 | }
284 | export { ModelManager };
285 |
--------------------------------------------------------------------------------
/build/tools.d.ts:
--------------------------------------------------------------------------------
1 | import type { Config, ModelManager } from './model.js';
2 | import type { Tips } from './widget.js';
3 | interface Tools {
4 | [key: string]: {
5 | icon: string;
6 | callback: (message: any) => void;
7 | };
8 | }
9 | declare class ToolsManager {
10 | tools: Tools;
11 | config: Config;
12 | constructor(model: ModelManager, config: Config, tips: Tips);
13 | registerTools(): void;
14 | }
15 | export { ToolsManager, Tools };
16 |
--------------------------------------------------------------------------------
/build/tools.js:
--------------------------------------------------------------------------------
1 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3 | return new (P || (P = Promise))(function (resolve, reject) {
4 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7 | step((generator = generator.apply(thisArg, _arguments || [])).next());
8 | });
9 | };
10 | import { fa_comment, fa_paper_plane, fa_street_view, fa_shirt, fa_camera_retro, fa_info_circle, fa_xmark } from './icons.js';
11 | import { showMessage, i18n } from './message.js';
12 | class ToolsManager {
13 | constructor(model, config, tips) {
14 | this.config = config;
15 | this.tools = {
16 | hitokoto: {
17 | icon: fa_comment,
18 | callback: () => __awaiter(this, void 0, void 0, function* () {
19 | const response = yield fetch('https://v1.hitokoto.cn');
20 | const result = yield response.json();
21 | const template = tips.message.hitokoto;
22 | const text = i18n(template, result.from, result.creator);
23 | showMessage(result.hitokoto, 6000, 9);
24 | setTimeout(() => {
25 | showMessage(text, 4000, 9);
26 | }, 6000);
27 | })
28 | },
29 | asteroids: {
30 | icon: fa_paper_plane,
31 | callback: () => {
32 | if (window.Asteroids) {
33 | if (!window.ASTEROIDSPLAYERS)
34 | window.ASTEROIDSPLAYERS = [];
35 | window.ASTEROIDSPLAYERS.push(new window.Asteroids());
36 | }
37 | else {
38 | const script = document.createElement('script');
39 | script.src =
40 | 'https://fastly.jsdelivr.net/gh/stevenjoezhang/asteroids/asteroids.js';
41 | document.head.appendChild(script);
42 | }
43 | }
44 | },
45 | 'switch-model': {
46 | icon: fa_street_view,
47 | callback: () => model.loadNextModel()
48 | },
49 | 'switch-texture': {
50 | icon: fa_shirt,
51 | callback: () => {
52 | let successMessage = '', failMessage = '';
53 | if (tips) {
54 | successMessage = tips.message.changeSuccess;
55 | failMessage = tips.message.changeFail;
56 | }
57 | model.loadRandTexture(successMessage, failMessage);
58 | }
59 | },
60 | photo: {
61 | icon: fa_camera_retro,
62 | callback: () => {
63 | const message = tips.message.photo;
64 | showMessage(message, 6000, 9);
65 | const canvas = document.getElementById('live2d');
66 | if (!canvas)
67 | return;
68 | const imageUrl = canvas.toDataURL();
69 | const link = document.createElement('a');
70 | link.style.display = 'none';
71 | link.href = imageUrl;
72 | link.download = 'live2d-photo.png';
73 | document.body.appendChild(link);
74 | link.click();
75 | document.body.removeChild(link);
76 | }
77 | },
78 | info: {
79 | icon: fa_info_circle,
80 | callback: () => {
81 | open('https://github.com/stevenjoezhang/live2d-widget');
82 | }
83 | },
84 | quit: {
85 | icon: fa_xmark,
86 | callback: () => {
87 | localStorage.setItem('waifu-display', Date.now().toString());
88 | const message = tips.message.goodbye;
89 | showMessage(message, 2000, 11);
90 | const waifu = document.getElementById('waifu');
91 | if (!waifu)
92 | return;
93 | waifu.classList.remove('waifu-active');
94 | setTimeout(() => {
95 | waifu.classList.add('waifu-hidden');
96 | const waifuToggle = document.getElementById('waifu-toggle');
97 | waifuToggle === null || waifuToggle === void 0 ? void 0 : waifuToggle.classList.add('waifu-toggle-active');
98 | }, 3000);
99 | }
100 | }
101 | };
102 | }
103 | registerTools() {
104 | var _b;
105 | if (!Array.isArray(this.config.tools)) {
106 | this.config.tools = Object.keys(this.tools);
107 | }
108 | for (const toolName of this.config.tools) {
109 | if (this.tools[toolName]) {
110 | const { icon, callback } = this.tools[toolName];
111 | const element = document.createElement('span');
112 | element.id = `waifu-tool-${toolName}`;
113 | element.innerHTML = icon;
114 | (_b = document
115 | .getElementById('waifu-tool')) === null || _b === void 0 ? void 0 : _b.insertAdjacentElement('beforeend', element);
116 | element.addEventListener('click', callback);
117 | }
118 | }
119 | }
120 | }
121 | export { ToolsManager };
122 |
--------------------------------------------------------------------------------
/build/utils.d.ts:
--------------------------------------------------------------------------------
1 | declare function randomSelection(obj: string[] | string): string;
2 | declare function randomOtherOption(total: number, excludeIndex: number): number;
3 | declare function loadExternalResource(url: string, type: string): Promise;
4 | export { randomSelection, loadExternalResource, randomOtherOption };
5 |
--------------------------------------------------------------------------------
/build/utils.js:
--------------------------------------------------------------------------------
1 | function randomSelection(obj) {
2 | return Array.isArray(obj) ? obj[Math.floor(Math.random() * obj.length)] : obj;
3 | }
4 | function randomOtherOption(total, excludeIndex) {
5 | const idx = Math.floor(Math.random() * (total - 1));
6 | return idx >= excludeIndex ? idx + 1 : idx;
7 | }
8 | function loadExternalResource(url, type) {
9 | return new Promise((resolve, reject) => {
10 | let tag;
11 | if (type === 'css') {
12 | tag = document.createElement('link');
13 | tag.rel = 'stylesheet';
14 | tag.href = url;
15 | }
16 | else if (type === 'js') {
17 | tag = document.createElement('script');
18 | tag.src = url;
19 | }
20 | if (tag) {
21 | tag.onload = () => resolve(url);
22 | tag.onerror = () => reject(url);
23 | document.head.appendChild(tag);
24 | }
25 | });
26 | }
27 | export { randomSelection, loadExternalResource, randomOtherOption };
28 |
--------------------------------------------------------------------------------
/build/waifu-tips.d.ts:
--------------------------------------------------------------------------------
1 | export {};
2 |
--------------------------------------------------------------------------------
/build/waifu-tips.js:
--------------------------------------------------------------------------------
1 | import { initWidget } from './widget.js';
2 | window.initWidget = initWidget;
3 |
--------------------------------------------------------------------------------
/build/widget.d.ts:
--------------------------------------------------------------------------------
1 | import { Config, ModelList } from './model.js';
2 | import { Time } from './message.js';
3 | interface Tips {
4 | message: {
5 | default: string[];
6 | console: string;
7 | copy: string;
8 | visibilitychange: string;
9 | changeSuccess: string;
10 | changeFail: string;
11 | photo: string;
12 | goodbye: string;
13 | hitokoto: string;
14 | welcome: string;
15 | referrer: string;
16 | hoverBody: string;
17 | tapBody: string;
18 | };
19 | time: Time;
20 | mouseover: {
21 | selector: string;
22 | text: string | string[];
23 | }[];
24 | click: {
25 | selector: string;
26 | text: string | string[];
27 | }[];
28 | seasons: {
29 | date: string;
30 | text: string | string[];
31 | }[];
32 | models: ModelList[];
33 | }
34 | declare function initWidget(config: string | Config): void;
35 | export { initWidget, Tips };
36 |
--------------------------------------------------------------------------------
/build/widget.js:
--------------------------------------------------------------------------------
1 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3 | return new (P || (P = Promise))(function (resolve, reject) {
4 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7 | step((generator = generator.apply(thisArg, _arguments || [])).next());
8 | });
9 | };
10 | import { ModelManager } from './model.js';
11 | import { showMessage, welcomeMessage } from './message.js';
12 | import { randomSelection } from './utils.js';
13 | import { ToolsManager } from './tools.js';
14 | import logger from './logger.js';
15 | import registerDrag from './drag.js';
16 | import { fa_child } from './icons.js';
17 | function registerEventListener(tips) {
18 | let userAction = false;
19 | let userActionTimer;
20 | const messageArray = tips.message.default;
21 | tips.seasons.forEach(({ date, text }) => {
22 | const now = new Date(), after = date.split('-')[0], before = date.split('-')[1] || after;
23 | if (Number(after.split('/')[0]) <= now.getMonth() + 1 &&
24 | now.getMonth() + 1 <= Number(before.split('/')[0]) &&
25 | Number(after.split('/')[1]) <= now.getDate() &&
26 | now.getDate() <= Number(before.split('/')[1])) {
27 | text = randomSelection(text);
28 | text = text.replace('{year}', String(now.getFullYear()));
29 | messageArray.push(text);
30 | }
31 | });
32 | let lastHoverElement;
33 | window.addEventListener('mousemove', () => (userAction = true));
34 | window.addEventListener('keydown', () => (userAction = true));
35 | setInterval(() => {
36 | if (userAction) {
37 | userAction = false;
38 | clearInterval(userActionTimer);
39 | userActionTimer = null;
40 | }
41 | else if (!userActionTimer) {
42 | userActionTimer = setInterval(() => {
43 | showMessage(messageArray, 6000, 9);
44 | }, 20000);
45 | }
46 | }, 1000);
47 | window.addEventListener('mouseover', (event) => {
48 | var _b;
49 | for (let { selector, text } of tips.mouseover) {
50 | if (!((_b = event.target) === null || _b === void 0 ? void 0 : _b.closest(selector)))
51 | continue;
52 | if (lastHoverElement === selector)
53 | return;
54 | lastHoverElement = selector;
55 | text = randomSelection(text);
56 | text = text.replace('{text}', event.target.innerText);
57 | showMessage(text, 4000, 8);
58 | return;
59 | }
60 | });
61 | window.addEventListener('click', (event) => {
62 | var _b;
63 | for (let { selector, text } of tips.click) {
64 | if (!((_b = event.target) === null || _b === void 0 ? void 0 : _b.closest(selector)))
65 | continue;
66 | text = randomSelection(text);
67 | text = text.replace('{text}', event.target.innerText);
68 | showMessage(text, 4000, 8);
69 | return;
70 | }
71 | });
72 | window.addEventListener('live2d:hoverbody', () => {
73 | const text = randomSelection(tips.message.hoverBody);
74 | showMessage(text, 4000, 8, false);
75 | });
76 | window.addEventListener('live2d:tapbody', () => {
77 | const text = randomSelection(tips.message.tapBody);
78 | showMessage(text, 4000, 9);
79 | });
80 | const devtools = () => { };
81 | console.log('%c', devtools);
82 | devtools.toString = () => {
83 | showMessage(tips.message.console, 6000, 9);
84 | };
85 | window.addEventListener('copy', () => {
86 | showMessage(tips.message.copy, 6000, 9);
87 | });
88 | window.addEventListener('visibilitychange', () => {
89 | if (!document.hidden)
90 | showMessage(tips.message.visibilitychange, 6000, 9);
91 | });
92 | }
93 | function loadWidget(config) {
94 | return __awaiter(this, void 0, void 0, function* () {
95 | var _b;
96 | localStorage.removeItem('waifu-display');
97 | sessionStorage.removeItem('waifu-message-priority');
98 | document.body.insertAdjacentHTML('beforeend', `
99 |
100 |
101 |
102 |
103 |
104 |
`);
105 | let models = [];
106 | let tips;
107 | if (config.waifuPath) {
108 | const response = yield fetch(config.waifuPath);
109 | tips = yield response.json();
110 | models = tips.models;
111 | registerEventListener(tips);
112 | showMessage(welcomeMessage(tips.time, tips.message.welcome, tips.message.referrer), 7000, 11);
113 | }
114 | const model = yield ModelManager.initCheck(config, models);
115 | yield model.loadModel('');
116 | new ToolsManager(model, config, tips).registerTools();
117 | if (config.drag)
118 | registerDrag();
119 | (_b = document.getElementById('waifu')) === null || _b === void 0 ? void 0 : _b.classList.add('waifu-active');
120 | });
121 | }
122 | function initWidget(config) {
123 | if (typeof config === 'string') {
124 | logger.error('Your config for Live2D initWidget is outdated. Please refer to https://github.com/stevenjoezhang/live2d-widget/blob/master/dist/autoload.js');
125 | return;
126 | }
127 | logger.setLevel(config.logLevel);
128 | document.body.insertAdjacentHTML('beforeend', `
129 | ${fa_child}
130 |
`);
131 | const toggle = document.getElementById('waifu-toggle');
132 | toggle === null || toggle === void 0 ? void 0 : toggle.addEventListener('click', () => {
133 | var _b;
134 | toggle === null || toggle === void 0 ? void 0 : toggle.classList.remove('waifu-toggle-active');
135 | if (toggle === null || toggle === void 0 ? void 0 : toggle.getAttribute('first-time')) {
136 | loadWidget(config);
137 | toggle === null || toggle === void 0 ? void 0 : toggle.removeAttribute('first-time');
138 | }
139 | else {
140 | localStorage.removeItem('waifu-display');
141 | (_b = document.getElementById('waifu')) === null || _b === void 0 ? void 0 : _b.classList.remove('waifu-hidden');
142 | setTimeout(() => {
143 | var _b;
144 | (_b = document.getElementById('waifu')) === null || _b === void 0 ? void 0 : _b.classList.add('waifu-active');
145 | }, 0);
146 | }
147 | });
148 | if (localStorage.getItem('waifu-display') &&
149 | Date.now() - Number(localStorage.getItem('waifu-display')) <= 86400000) {
150 | toggle === null || toggle === void 0 ? void 0 : toggle.setAttribute('first-time', 'true');
151 | setTimeout(() => {
152 | toggle === null || toggle === void 0 ? void 0 : toggle.classList.add('waifu-toggle-active');
153 | }, 0);
154 | }
155 | else {
156 | loadWidget(config);
157 | }
158 | }
159 | export { initWidget };
160 |
--------------------------------------------------------------------------------
/demo/demo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Live2D 看板娘 / Demo
6 |
23 |
24 |
25 |
26 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/demo/login.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 看板娘登陆平台
7 |
8 |
9 |
10 |
121 |
122 |
123 |
150 |
252 |
253 |
254 |
--------------------------------------------------------------------------------
/demo/screenshots/screenshot-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevenjoezhang/live2d-widget/8883dabea775cfebbefe94f0dee14620982d8933/demo/screenshots/screenshot-1.png
--------------------------------------------------------------------------------
/demo/screenshots/screenshot-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevenjoezhang/live2d-widget/8883dabea775cfebbefe94f0dee14620982d8933/demo/screenshots/screenshot-2.png
--------------------------------------------------------------------------------
/demo/screenshots/screenshot-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevenjoezhang/live2d-widget/8883dabea775cfebbefe94f0dee14620982d8933/demo/screenshots/screenshot-3.png
--------------------------------------------------------------------------------
/dist/autoload.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Live2D Widget
3 | * https://github.com/stevenjoezhang/live2d-widget
4 | */
5 |
6 | // Recommended to use absolute path for live2d_path parameter
7 | // live2d_path 参数建议使用绝对路径
8 | const live2d_path = 'https://fastly.jsdelivr.net/npm/live2d-widgets@1.0.0-rc.4/dist/';
9 | // const live2d_path = '/dist/';
10 |
11 | // Method to encapsulate asynchronous resource loading
12 | // 封装异步加载资源的方法
13 | function loadExternalResource(url, type) {
14 | return new Promise((resolve, reject) => {
15 | let tag;
16 |
17 | if (type === 'css') {
18 | tag = document.createElement('link');
19 | tag.rel = 'stylesheet';
20 | tag.href = url;
21 | }
22 | else if (type === 'js') {
23 | tag = document.createElement('script');
24 | tag.type = 'module';
25 | tag.src = url;
26 | }
27 | if (tag) {
28 | tag.onload = () => resolve(url);
29 | tag.onerror = () => reject(url);
30 | document.head.appendChild(tag);
31 | }
32 | });
33 | }
34 |
35 | (async () => {
36 | // If you are concerned about display issues on mobile devices, you can use screen.width to determine whether to load
37 | // 如果担心手机上显示效果不佳,可以根据屏幕宽度来判断是否加载
38 | // if (screen.width < 768) return;
39 |
40 | // Avoid cross-origin issues with image resources
41 | // 避免图片资源跨域问题
42 | const OriginalImage = window.Image;
43 | window.Image = function(...args) {
44 | const img = new OriginalImage(...args);
45 | img.crossOrigin = "anonymous";
46 | return img;
47 | };
48 | window.Image.prototype = OriginalImage.prototype;
49 | // Load waifu.css and waifu-tips.js
50 | // 加载 waifu.css 和 waifu-tips.js
51 | await Promise.all([
52 | loadExternalResource(live2d_path + 'waifu.css', 'css'),
53 | loadExternalResource(live2d_path + 'waifu-tips.js', 'js')
54 | ]);
55 | // For detailed usage of configuration options, see README.en.md
56 | // 配置选项的具体用法见 README.md
57 | initWidget({
58 | waifuPath: live2d_path + 'waifu-tips.json',
59 | // cdnPath: 'https://fastly.jsdelivr.net/gh/fghrsh/live2d_api/',
60 | cubism2Path: live2d_path + 'live2d.min.js',
61 | cubism5Path: 'https://cubism.live2d.com/sdk-web/cubismcore/live2dcubismcore.min.js',
62 | tools: ['hitokoto', 'asteroids', 'switch-model', 'switch-texture', 'photo', 'info', 'quit'],
63 | logLevel: 'warn',
64 | drag: false,
65 | });
66 | })();
67 |
68 | console.log(`\n%cLive2D%cWidget%c\n`, 'padding: 8px; background: #cd3e45; font-weight: bold; font-size: large; color: white;', 'padding: 8px; background: #ff5450; font-size: large; color: #eee;', '');
69 |
70 | /*
71 | く__,.ヘヽ. / ,ー、 〉
72 | \ ', !-─‐-i / /´
73 | /`ー' L//`ヽ、
74 | / /, /| , , ',
75 | イ / /-‐/ i L_ ハ ヽ! i
76 | レ ヘ 7イ`ト レ'ァ-ト、!ハ| |
77 | !,/7 '0' ´0iソ| |
78 | |.从" _ ,,,, / |./ |
79 | レ'| i>.、,,__ _,.イ / .i |
80 | レ'| | / k_7_/レ'ヽ, ハ. |
81 | | |/i 〈|/ i ,.ヘ | i |
82 | .|/ / i: ヘ! \ |
83 | kヽ>、ハ _,.ヘ、 /、!
84 | !'〈//`T´', \ `'7'ーr'
85 | レ'ヽL__|___i,___,ンレ|ノ
86 | ト-,/ |___./
87 | 'ー' !_,.:
88 | */
89 |
--------------------------------------------------------------------------------
/dist/waifu-tips.json:
--------------------------------------------------------------------------------
1 | {
2 | "mouseover": [{
3 | "selector": "#waifu-tool-hitokoto",
4 | "text": ["猜猜我要说些什么?", "我从青蛙王子那里听到了不少人生经验。"]
5 | }, {
6 | "selector": "#waifu-tool-asteroids",
7 | "text": ["要不要来玩飞机大战?", "这个按钮上写着「不要点击」。", "怎么,你想来和我玩个游戏?", "听说这样可以蹦迪!"]
8 | }, {
9 | "selector": "#waifu-tool-switch-model",
10 | "text": ["你是不是不爱人家了呀,呜呜呜~", "要见见我的姐姐嘛?", "想要看我妹妹嘛?", "要切换看板娘吗?"]
11 | }, {
12 | "selector": "#waifu-tool-switch-texture",
13 | "text": ["喜欢换装 PLAY 吗?", "这次要扮演什么呢?", "变装!", "让我们看看接下来会发生什么!"]
14 | }, {
15 | "selector": "#waifu-tool-photo",
16 | "text": ["你要给我拍照呀?一二三~茄子~", "要不,我们来合影吧!", "保持微笑就好了~"]
17 | }, {
18 | "selector": "#waifu-tool-info",
19 | "text": ["想要知道更多关于我的事么?", "这里记录着我搬家的历史呢。", "你想深入了解我什么呢?"]
20 | }, {
21 | "selector": "#waifu-tool-quit",
22 | "text": ["到了要说再见的时候了吗?", "呜呜 QAQ 后会有期……", "不要抛弃我呀……", "我们,还能再见面吗……", "哼,你会后悔的!"]
23 | }, {
24 | "selector": ".menu-item-home a",
25 | "text": ["点击前往首页,想回到上一页可以使用浏览器的后退功能哦。", "点它就可以回到首页啦!", "回首页看看吧。"]
26 | }, {
27 | "selector": ".menu-item-about a",
28 | "text": ["你想知道我家主人是谁吗?", "这里有一些关于我家主人的秘密哦,要不要看看呢?", "发现主人出没地点!"]
29 | }, {
30 | "selector": ".menu-item-tags a",
31 | "text": ["点击就可以看文章的标签啦!", "点击来查看所有标签哦。"]
32 | }, {
33 | "selector": ".menu-item-categories a",
34 | "text": ["文章都分类好啦~", "点击来查看文章分类哦。"]
35 | }, {
36 | "selector": ".menu-item-archives a",
37 | "text": ["翻页比较麻烦吗,那就来看看文章归档吧。", "文章目录都整理在这里啦!"]
38 | }, {
39 | "selector": ".menu-item-friends a",
40 | "text": ["这是我的朋友们哦ヾ(◍°∇°◍)ノ゙", "要去大佬们的家看看吗?", "要去拜访一下我的朋友们吗?"]
41 | }, {
42 | "selector": ".menu-item-search a",
43 | "text": ["找不到想看的内容?搜索看看吧!", "在找什么东西呢,需要帮忙吗?"]
44 | }, {
45 | "selector": ".menu-item a",
46 | "text": ["快看看这里都有什么呢?"]
47 | }, {
48 | "selector": ".site-author",
49 | "text": ["我家主人好看吗?", "这是我家主人(*´∇`*)"]
50 | }, {
51 | "selector": ".site-state",
52 | "text": ["这是文章的统计信息~", "要不要点进去看看?"]
53 | }, {
54 | "selector": ".feed-link a",
55 | "text": ["这里可以使用 RSS 订阅呢!", "利用 feed 订阅器,就能快速知道博客有没有更新了呢。"]
56 | }, {
57 | "selector": ".cc-opacity, .post-copyright-author",
58 | "text": ["要记得规范转载哦。", "所有文章均采用 CC BY-NC-SA 4.0 许可协议~", "转载前要先注意下文章的版权协议呢。"]
59 | }, {
60 | "selector": ".links-of-author",
61 | "text": ["这里是主人的常驻地址哦。", "这里有主人的联系方式!"]
62 | }, {
63 | "selector": ".followme",
64 | "text": ["手机扫一下就能继续看,很方便呢~", "扫一扫,打开新世界的大门!"]
65 | }, {
66 | "selector": ".fancybox img, img.medium-zoom-image",
67 | "text": ["点击图片可以放大呢!"]
68 | }, {
69 | "selector": ".copy-btn",
70 | "text": ["代码可以直接点击复制哟。"]
71 | }, {
72 | "selector": ".highlight .table-container, .gist",
73 | "text": ["GitHub!我是新手!", "PHP 是最好的语言!"]
74 | }, {
75 | "selector": "a[href^='mailto']",
76 | "text": ["邮件我会及时回复的!", "点击就可以发送邮件啦~"]
77 | }, {
78 | "selector": "a[href^='/tags/']",
79 | "text": ["要去看看 {text} 标签么?", "点它可以查看此标签下的所有文章哟!"]
80 | }, {
81 | "selector": "a[href^='/categories/']",
82 | "text": ["要去看看 {text} 分类么?", "点它可以查看此分类下的所有文章哟!"]
83 | }, {
84 | "selector": ".post-title-link",
85 | "text": ["要看看 {text} 这篇文章吗?"]
86 | }, {
87 | "selector": "a[rel='contents']",
88 | "text": ["点击来阅读全文哦。"]
89 | }, {
90 | "selector": "a[itemprop='discussionUrl']",
91 | "text": ["要去看看评论吗?"]
92 | }, {
93 | "selector": ".beian a",
94 | "text": ["我也是有户口的人哦。", "我的主人可是遵纪守法的好主人。"]
95 | }, {
96 | "selector": ".container a[href^='http'], .nav-link .nav-text",
97 | "text": ["要去看看 {text} 么?", "去 {text} 逛逛吧。", "到 {text} 看看吧。"]
98 | }, {
99 | "selector": ".back-to-top",
100 | "text": ["点它就可以回到顶部啦!", "又回到最初的起点~", "要回到开始的地方么?"]
101 | }, {
102 | "selector": ".reward-container",
103 | "text": ["我是不是棒棒哒~快给我点赞吧!", "要打赏我嘛?好期待啊~", "主人最近在吃土呢,很辛苦的样子,给他一些钱钱吧~"]
104 | }, {
105 | "selector": "#wechat",
106 | "text": ["这是我的微信二维码~"]
107 | }, {
108 | "selector": "#alipay",
109 | "text": ["这是我的支付宝哦!"]
110 | }, {
111 | "selector": "#bitcoin",
112 | "text": ["这是我的比特币账号!"]
113 | }, {
114 | "selector": "#needsharebutton-postbottom .btn",
115 | "text": ["好东西要让更多人知道才行哦。", "觉得文章有帮助的话,可以分享给更多需要的朋友呢。"]
116 | }, {
117 | "selector": ".need-share-button_weibo",
118 | "text": ["微博?来分享一波喵!"]
119 | }, {
120 | "selector": ".need-share-button_wechat",
121 | "text": ["分享到微信吧!"]
122 | }, {
123 | "selector": ".need-share-button_douban",
124 | "text": ["分享到豆瓣好像也不错!"]
125 | }, {
126 | "selector": ".need-share-button_qqzone",
127 | "text": ["QQ 空间,一键转发,耶~"]
128 | }, {
129 | "selector": ".need-share-button_twitter",
130 | "text": ["Twitter?好像是不存在的东西?"]
131 | }, {
132 | "selector": ".need-share-button_facebook",
133 | "text": ["emmm…FB 好像也是不存在的东西?"]
134 | }, {
135 | "selector": ".post-nav-item a[rel='next']",
136 | "text": ["来看看下一篇文章吧。", "点它可以看下一篇文章哦!", "要翻到下一篇文章吗?"]
137 | }, {
138 | "selector": ".post-nav-item a[rel='prev']",
139 | "text": ["来看看上一篇文章吧。", "点它可以看上一篇文章哦!", "要翻到上一篇文章吗?"]
140 | }, {
141 | "selector": ".extend.next",
142 | "text": ["去下一页看看吧。", "点它可以前进哦!", "要翻到下一页吗?"]
143 | }, {
144 | "selector": ".extend.prev",
145 | "text": ["去上一页看看吧。", "点它可以后退哦!", "要翻到上一页吗?"]
146 | }, {
147 | "selector": "input.vnick",
148 | "text": ["该怎么称呼你呢?", "留下你的尊姓大名!"]
149 | }, {
150 | "selector": ".vmail",
151 | "text": ["留下你的邮箱,不然就是无头像人士了!", "记得设置好 Gravatar 头像哦!", "为了方便通知你最新消息,一定要留下邮箱!"]
152 | }, {
153 | "selector": ".vlink",
154 | "text": ["快快告诉我你的家在哪里,好让我去参观参观!"]
155 | }, {
156 | "selector": ".veditor",
157 | "text": ["想要去评论些什么吗?", "要说点什么吗?", "觉得博客不错?快来留言和主人交流吧!"]
158 | }, {
159 | "selector": ".vcontrol a",
160 | "text": ["你会不会熟练使用 Markdown 呀?", "使用 Markdown 让评论更美观吧~"]
161 | }, {
162 | "selector": ".vemoji-btn",
163 | "text": ["要插入一个萌萌哒的表情吗?", "要来一发表情吗?"]
164 | }, {
165 | "selector": ".vpreview-btn",
166 | "text": ["要预览一下你的发言吗?", "快看看你的评论有多少负熵!"]
167 | }, {
168 | "selector": ".vsubmit",
169 | "text": ["评论没有审核,要对自己的发言负责哦~", "要提交了吗,请耐心等待回复哦~"]
170 | }, {
171 | "selector": ".vcontent",
172 | "text": ["哇,快看看这个精彩评论!", "如果有疑问,请尽快留言哦~"]
173 | }],
174 | "click": [{
175 | "selector": ".veditor",
176 | "text": ["要吐槽些什么呢?", "一定要认真填写喵~", "有什么想说的吗?"]
177 | }, {
178 | "selector": ".vsubmit",
179 | "text": ["输入验证码就可以提交评论啦~"]
180 | }],
181 | "seasons": [{
182 | "date": "01/01",
183 | "text": "元旦了呢,新的一年又开始了,今年是{year}年~"
184 | }, {
185 | "date": "02/14",
186 | "text": "又是一年情人节,{year}年找到对象了嘛~"
187 | }, {
188 | "date": "03/08",
189 | "text": "今天是国际妇女节!"
190 | }, {
191 | "date": "03/12",
192 | "text": "今天是植树节,要保护环境呀!"
193 | }, {
194 | "date": "04/01",
195 | "text": "悄悄告诉你一个秘密~今天是愚人节,不要被骗了哦~"
196 | }, {
197 | "date": "05/01",
198 | "text": "今天是五一劳动节,计划好假期去哪里了吗~"
199 | }, {
200 | "date": "06/01",
201 | "text": "儿童节了呢,快活的时光总是短暂,要是永远长不大该多好啊…"
202 | }, {
203 | "date": "09/03",
204 | "text": "中国人民抗日战争胜利纪念日,铭记历史、缅怀先烈、珍爱和平、开创未来。"
205 | }, {
206 | "date": "09/10",
207 | "text": "教师节,在学校要给老师问声好呀~"
208 | }, {
209 | "date": "10/01",
210 | "text": "国庆节到了,为祖国母亲庆生!"
211 | }, {
212 | "date": "11/05-11/12",
213 | "text": "今年的双十一是和谁一起过的呢~"
214 | }, {
215 | "date": "12/20-12/31",
216 | "text": "这几天是圣诞节,主人肯定又去剁手买买买了~"
217 | }],
218 | "time": [{
219 | "hour": "6-7",
220 | "text": "早上好!一日之计在于晨,美好的一天就要开始了~"
221 | }, {
222 | "hour": "8-11",
223 | "text": "上午好!工作顺利嘛,不要久坐,多起来走动走动哦!"
224 | }, {
225 | "hour": "12-13",
226 | "text": "中午了,工作了一个上午,现在是午餐时间!"
227 | }, {
228 | "hour": "14-17",
229 | "text": "午后很容易犯困呢,今天的运动目标完成了吗?"
230 | }, {
231 | "hour": "18-19",
232 | "text": "傍晚了!窗外夕阳的景色很美丽呢,最美不过夕阳红~"
233 | }, {
234 | "hour": "20-21",
235 | "text": "晚上好,今天过得怎么样?"
236 | }, {
237 | "hour": "22-23",
238 | "text": ["已经这么晚了呀,早点休息吧,晚安~", "深夜时要爱护眼睛呀!"]
239 | }, {
240 | "hour": "0-5",
241 | "text": "你是夜猫子呀?这么晚还不睡觉,明天起的来嘛?"
242 | }],
243 | "message": {
244 | "default": ["好久不见,日子过得好快呢……", "大坏蛋!你都多久没理人家了呀,嘤嘤嘤~", "嗨~快来逗我玩吧!", "拿小拳拳锤你胸口!", "记得把小家加入收藏夹哦!"],
245 | "console": "哈哈,你打开了控制台,是想要看看我的小秘密吗?",
246 | "copy": "你都复制了些什么呀,转载要记得加上出处哦!",
247 | "visibilitychange": "哇,你终于回来了~",
248 | "changeSuccess": "我的新衣服好看嘛?",
249 | "changeFail": "我还没有其他衣服呢!",
250 | "photo": "照好了嘛,是不是很可爱呢?",
251 | "goodbye": "愿你有一天能与重要的人重逢。",
252 | "hitokoto": "这句一言来自 「$1」,是 $2 在 hitokoto.cn 投稿的。",
253 | "welcome": "欢迎阅读「$1」",
254 | "referrer": "Hello!来自 $1 的朋友",
255 | "hoverBody": ["干嘛呢你,快把手拿开~~", "鼠…鼠标放错地方了!", "你要干嘛呀?", "喵喵喵?", "怕怕(ノ≧∇≦)ノ", "非礼呀!救命!", "这样的话,只能使用武力了!", "我要生气了哦", "不要动手动脚的!", "真…真的是不知羞耻!", "Hentai!"],
256 | "tapBody": [["是…是不小心碰到了吧…", "萝莉控是什么呀?", "你看到我的小熊了吗?", "再摸的话我可要报警了!⌇●﹏●⌇", "110 吗,这里有个变态一直在摸我(ó﹏ò。)", "不要摸我了,我会告诉老婆来打你的!", "干嘛动我呀!小心我咬你!", "别摸我,有什么好摸的!"]]
257 | },
258 | "models": [{
259 | "name": "Potion-Maker/Pio",
260 | "paths": ["https://fastly.jsdelivr.net/gh/fghrsh/live2d_api/model/Potion-Maker/Pio/index.json"],
261 | "message": "来自 Potion Maker 的 Pio 酱 ~"
262 | }, {
263 | "name": "Potion-Maker/Tia",
264 | "paths": ["https://fastly.jsdelivr.net/gh/fghrsh/live2d_api/model/Potion-Maker/Tia/index.json"],
265 | "message": "来自 Potion Maker 的 Tia 酱 ~"
266 | }, {
267 | "name": "HyperdimensionNeptunia",
268 | "paths": [
269 | "https://fastly.jsdelivr.net/gh/fghrsh/live2d_api/model/HyperdimensionNeptunia/neptune_classic/index.json",
270 | "https://fastly.jsdelivr.net/gh/fghrsh/live2d_api/model/HyperdimensionNeptunia/nepnep/index.json",
271 | "https://fastly.jsdelivr.net/gh/fghrsh/live2d_api/model/HyperdimensionNeptunia/neptune_santa/index.json",
272 | "https://fastly.jsdelivr.net/gh/fghrsh/live2d_api/model/HyperdimensionNeptunia/nepmaid/index.json",
273 | "https://fastly.jsdelivr.net/gh/fghrsh/live2d_api/model/HyperdimensionNeptunia/nepswim/index.json",
274 | "https://fastly.jsdelivr.net/gh/fghrsh/live2d_api/model/HyperdimensionNeptunia/noir_classic/index.json",
275 | "https://fastly.jsdelivr.net/gh/fghrsh/live2d_api/model/HyperdimensionNeptunia/noir/index.json",
276 | "https://fastly.jsdelivr.net/gh/fghrsh/live2d_api/model/HyperdimensionNeptunia/noir_santa/index.json",
277 | "https://fastly.jsdelivr.net/gh/fghrsh/live2d_api/model/HyperdimensionNeptunia/noireswim/index.json",
278 | "https://fastly.jsdelivr.net/gh/fghrsh/live2d_api/model/HyperdimensionNeptunia/blanc_classic/index.json",
279 | "https://fastly.jsdelivr.net/gh/fghrsh/live2d_api/model/HyperdimensionNeptunia/blanc_normal/index.json",
280 | "https://fastly.jsdelivr.net/gh/fghrsh/live2d_api/model/HyperdimensionNeptunia/blanc_swimwear/index.json",
281 | "https://fastly.jsdelivr.net/gh/fghrsh/live2d_api/model/HyperdimensionNeptunia/vert_classic/index.json",
282 | "https://fastly.jsdelivr.net/gh/fghrsh/live2d_api/model/HyperdimensionNeptunia/vert_normal/index.json",
283 | "https://fastly.jsdelivr.net/gh/fghrsh/live2d_api/model/HyperdimensionNeptunia/vert_swimwear/index.json",
284 | "https://fastly.jsdelivr.net/gh/fghrsh/live2d_api/model/HyperdimensionNeptunia/nepgear/index.json",
285 | "https://fastly.jsdelivr.net/gh/fghrsh/live2d_api/model/HyperdimensionNeptunia/nepgear_extra/index.json",
286 | "https://fastly.jsdelivr.net/gh/fghrsh/live2d_api/model/HyperdimensionNeptunia/nepgearswim/index.json",
287 | "https://fastly.jsdelivr.net/gh/fghrsh/live2d_api/model/HyperdimensionNeptunia/histoire/index.json",
288 | "https://fastly.jsdelivr.net/gh/fghrsh/live2d_api/model/HyperdimensionNeptunia/histoirenohover"
289 | ],
290 | "message": "Nep! Nep! 超次元游戏:海王星 系列"
291 | }, {
292 | "name": "Hiyori",
293 | "paths": ["https://fastly.jsdelivr.net/gh/Live2D/CubismWebSamples/Samples/Resources/Hiyori/Hiyori.model3.json"],
294 | "message": "是 Hiyori 哦 ~"
295 | }]
296 | }
297 |
--------------------------------------------------------------------------------
/dist/waifu.css:
--------------------------------------------------------------------------------
1 | #waifu-toggle {
2 | background-color: #fa0;
3 | border-radius: 5px;
4 | bottom: 66px;
5 | cursor: pointer;
6 | display: flex;
7 | justify-content: flex-end;
8 | left: 0;
9 | margin-left: -100px;
10 | padding: 5px;
11 | position: fixed;
12 | transition: margin-left 1s;
13 | width: 60px;
14 | }
15 |
16 | #waifu-toggle.waifu-toggle-active {
17 | margin-left: -50px;
18 | }
19 |
20 | #waifu-toggle.waifu-toggle-active:hover {
21 | margin-left: -30px;
22 | }
23 |
24 | #waifu-toggle svg {
25 | fill: #fff;
26 | height: 25px;
27 | }
28 |
29 | #waifu {
30 | bottom: -500px;
31 | left: 0;
32 | position: fixed;
33 | transform: translateY(25px);
34 | transition: transform .3s ease-in-out, bottom 3s ease-in-out;
35 | z-index: 1;
36 | }
37 |
38 | #waifu.waifu-active {
39 | bottom: 0;
40 | }
41 |
42 | #waifu.waifu-hidden {
43 | display: none;
44 | }
45 |
46 | #waifu:hover {
47 | transform: translateY(20px);
48 | }
49 |
50 | #waifu-tips {
51 | animation: waifu-shake 50s ease-in-out 5s infinite;
52 | background-color: rgba(236, 217, 188, .5);
53 | border: 1px solid rgba(224, 186, 140, .62);
54 | border-radius: 12px;
55 | box-shadow: 0 3px 15px 2px rgba(191, 158, 118, .2);
56 | font-size: 14px;
57 | line-height: 24px;
58 | margin: -30px 20px;
59 | min-height: 70px;
60 | opacity: 0;
61 | overflow: hidden;
62 | padding: 5px 10px;
63 | position: absolute;
64 | text-overflow: ellipsis;
65 | transition: opacity 1s;
66 | width: 250px;
67 | word-break: break-all;
68 | }
69 |
70 | #waifu-tips.waifu-tips-active {
71 | opacity: 1;
72 | transition: opacity .2s;
73 | }
74 |
75 | #waifu-tips span {
76 | color: #0099cc;
77 | }
78 |
79 | #live2d {
80 | cursor: grab;
81 | height: 300px;
82 | position: relative;
83 | width: 300px;
84 | }
85 |
86 | #live2d:active {
87 | cursor: grabbing;
88 | }
89 |
90 | #waifu-tool {
91 | align-items: center;
92 | display: flex;
93 | flex-direction: column;
94 | gap: 5px;
95 | opacity: 0;
96 | position: absolute;
97 | right: -10px;
98 | top: 70px;
99 | transition: opacity 1s;
100 | }
101 |
102 | #waifu:hover #waifu-tool {
103 | opacity: 1;
104 | }
105 |
106 | #waifu-tool svg {
107 | cursor: pointer;
108 | display: block;
109 | fill: #7b8c9d;
110 | height: 25px;
111 | transition: fill .3s;
112 | }
113 |
114 | #waifu-tool svg:hover {
115 | fill: #0684bd; /* #34495e */
116 | }
117 |
118 | @keyframes waifu-shake {
119 | 2% {
120 | transform: translate(.5px, -1.5px) rotate(-.5deg);
121 | }
122 | 4% {
123 | transform: translate(.5px, 1.5px) rotate(1.5deg);
124 | }
125 | 6% {
126 | transform: translate(1.5px, 1.5px) rotate(1.5deg);
127 | }
128 | 8% {
129 | transform: translate(2.5px, 1.5px) rotate(.5deg);
130 | }
131 | 10% {
132 | transform: translate(.5px, 2.5px) rotate(.5deg);
133 | }
134 | 12% {
135 | transform: translate(1.5px, 1.5px) rotate(.5deg);
136 | }
137 | 14% {
138 | transform: translate(.5px, .5px) rotate(.5deg);
139 | }
140 | 16% {
141 | transform: translate(-1.5px, -.5px) rotate(1.5deg);
142 | }
143 | 18% {
144 | transform: translate(.5px, .5px) rotate(1.5deg);
145 | }
146 | 20% {
147 | transform: translate(2.5px, 2.5px) rotate(1.5deg);
148 | }
149 | 22% {
150 | transform: translate(.5px, -1.5px) rotate(1.5deg);
151 | }
152 | 24% {
153 | transform: translate(-1.5px, 1.5px) rotate(-.5deg);
154 | }
155 | 26% {
156 | transform: translate(1.5px, .5px) rotate(1.5deg);
157 | }
158 | 28% {
159 | transform: translate(-.5px, -.5px) rotate(-.5deg);
160 | }
161 | 30% {
162 | transform: translate(1.5px, -.5px) rotate(-.5deg);
163 | }
164 | 32% {
165 | transform: translate(2.5px, -1.5px) rotate(1.5deg);
166 | }
167 | 34% {
168 | transform: translate(2.5px, 2.5px) rotate(-.5deg);
169 | }
170 | 36% {
171 | transform: translate(.5px, -1.5px) rotate(.5deg);
172 | }
173 | 38% {
174 | transform: translate(2.5px, -.5px) rotate(-.5deg);
175 | }
176 | 40% {
177 | transform: translate(-.5px, 2.5px) rotate(.5deg);
178 | }
179 | 42% {
180 | transform: translate(-1.5px, 2.5px) rotate(.5deg);
181 | }
182 | 44% {
183 | transform: translate(-1.5px, 1.5px) rotate(.5deg);
184 | }
185 | 46% {
186 | transform: translate(1.5px, -.5px) rotate(-.5deg);
187 | }
188 | 48% {
189 | transform: translate(2.5px, -.5px) rotate(.5deg);
190 | }
191 | 50% {
192 | transform: translate(-1.5px, 1.5px) rotate(.5deg);
193 | }
194 | 52% {
195 | transform: translate(-.5px, 1.5px) rotate(.5deg);
196 | }
197 | 54% {
198 | transform: translate(-1.5px, 1.5px) rotate(.5deg);
199 | }
200 | 56% {
201 | transform: translate(.5px, 2.5px) rotate(1.5deg);
202 | }
203 | 58% {
204 | transform: translate(2.5px, 2.5px) rotate(.5deg);
205 | }
206 | 60% {
207 | transform: translate(2.5px, -1.5px) rotate(1.5deg);
208 | }
209 | 62% {
210 | transform: translate(-1.5px, .5px) rotate(1.5deg);
211 | }
212 | 64% {
213 | transform: translate(-1.5px, 1.5px) rotate(1.5deg);
214 | }
215 | 66% {
216 | transform: translate(.5px, 2.5px) rotate(1.5deg);
217 | }
218 | 68% {
219 | transform: translate(2.5px, -1.5px) rotate(1.5deg);
220 | }
221 | 70% {
222 | transform: translate(2.5px, 2.5px) rotate(.5deg);
223 | }
224 | 72% {
225 | transform: translate(-.5px, -1.5px) rotate(1.5deg);
226 | }
227 | 74% {
228 | transform: translate(-1.5px, 2.5px) rotate(1.5deg);
229 | }
230 | 76% {
231 | transform: translate(-1.5px, 2.5px) rotate(1.5deg);
232 | }
233 | 78% {
234 | transform: translate(-1.5px, 2.5px) rotate(.5deg);
235 | }
236 | 80% {
237 | transform: translate(-1.5px, .5px) rotate(-.5deg);
238 | }
239 | 82% {
240 | transform: translate(-1.5px, .5px) rotate(-.5deg);
241 | }
242 | 84% {
243 | transform: translate(-.5px, .5px) rotate(1.5deg);
244 | }
245 | 86% {
246 | transform: translate(2.5px, 1.5px) rotate(.5deg);
247 | }
248 | 88% {
249 | transform: translate(-1.5px, .5px) rotate(1.5deg);
250 | }
251 | 90% {
252 | transform: translate(-1.5px, -.5px) rotate(-.5deg);
253 | }
254 | 92% {
255 | transform: translate(-1.5px, -1.5px) rotate(1.5deg);
256 | }
257 | 94% {
258 | transform: translate(.5px, .5px) rotate(-.5deg);
259 | }
260 | 96% {
261 | transform: translate(2.5px, -.5px) rotate(-.5deg);
262 | }
263 | 98% {
264 | transform: translate(-1.5px, -1.5px) rotate(-.5deg);
265 | }
266 | 0%, 100% {
267 | transform: translate(0, 0) rotate(0);
268 | }
269 | }
270 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import eslint from '@eslint/js';
4 | import tseslint from 'typescript-eslint';
5 |
6 | export default tseslint.config(
7 | eslint.configs.recommended,
8 | tseslint.configs.recommended,
9 | {
10 | rules: {
11 | '@typescript-eslint/no-explicit-any': 'off',
12 | quotes: ['error', 'single'],
13 | indent: ['error', 2],
14 | }
15 | },
16 | {
17 | ignores: [
18 | 'src/CubismSdkForWeb-*/**',
19 | 'dist/**',
20 | 'build/**',
21 | 'node_modules/**'
22 | ]
23 | }
24 | );
25 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "live2d-widgets",
3 | "version": "1.0.0-rc.4",
4 | "description": "Live2D widget for web pages",
5 | "main": "build/index.js",
6 | "files": [
7 | "build",
8 | "!build/CubismSdkForWeb*",
9 | "demo",
10 | "dist",
11 | "src",
12 | "!src/CubismSdkForWeb*",
13 | "README.en.md"
14 | ],
15 | "types": "build/index.d.ts",
16 | "type": "module",
17 | "scripts": {
18 | "build": "tsc && rollup -c rollup.config.js",
19 | "build-dev": "rollup -c rollup.config.js -w",
20 | "eslint": "eslint src/"
21 | },
22 | "repository": {
23 | "type": "git",
24 | "url": "git+https://github.com/stevenjoezhang/live2d-widget.git"
25 | },
26 | "keywords": [
27 | "Live2D",
28 | "WebGL"
29 | ],
30 | "author": "stevenjoezhang ",
31 | "license": "GPL-3.0-or-later",
32 | "bugs": {
33 | "url": "https://github.com/stevenjoezhang/live2d-widget/issues"
34 | },
35 | "homepage": "https://github.com/stevenjoezhang/live2d-widget#readme",
36 | "dependencies": {
37 | "@fortawesome/fontawesome-free": "6.7.2"
38 | },
39 | "devDependencies": {
40 | "@eslint/js": "9.27.0",
41 | "@rollup/plugin-alias": "5.1.1",
42 | "@rollup/plugin-terser": "0.4.4",
43 | "@types/node": "22.15.21",
44 | "eslint": "9.27.0",
45 | "rollup": "4.41.1",
46 | "typescript": "5.8.3",
47 | "typescript-eslint": "8.32.1"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:recommended"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import terser from '@rollup/plugin-terser';
2 | import alias from '@rollup/plugin-alias';
3 | import fs from 'fs';
4 | import path from 'path';
5 | import { fileURLToPath } from 'url';
6 |
7 | const __dirname = path.dirname(fileURLToPath(import.meta.url));
8 |
9 | function findCubismDir() {
10 | const buildDir = path.join(__dirname, 'build');
11 | let candidates = fs.readdirSync(buildDir)
12 | .filter(f => f.startsWith('CubismSdkForWeb-') && fs.statSync(path.join(buildDir, f)).isDirectory());
13 | if (candidates.length === 0) {
14 | candidates = ['CubismSdkForWeb-5-r.4'];
15 | }
16 | return path.join(buildDir, candidates[0]);
17 | }
18 |
19 | const cubismDir = findCubismDir();
20 |
21 | export default {
22 | input: 'build/waifu-tips.js',
23 | output: {
24 | dir: 'dist/',
25 | format: 'esm',
26 | chunkFileNames: 'chunk/[name].js',
27 | sourcemap: true,
28 | banner: `/*!
29 | * Live2D Widget
30 | * https://github.com/stevenjoezhang/live2d-widget
31 | */
32 | `
33 | },
34 | plugins: [
35 | alias({
36 | entries: [
37 | {
38 | find: '@demo',
39 | replacement: path.resolve(cubismDir, 'Samples/TypeScript/Demo/src/')
40 | },
41 | {
42 | find: '@framework',
43 | replacement: path.resolve(cubismDir, 'Framework/src/')
44 | }
45 | ]
46 | }),
47 | terser(),
48 | ],
49 | context: 'this',
50 | };
51 |
--------------------------------------------------------------------------------
/src/cubism2/LAppDefine.js:
--------------------------------------------------------------------------------
1 | const LAppDefine = {
2 | VIEW_MAX_SCALE: 2,
3 | VIEW_MIN_SCALE: 0.8,
4 |
5 | VIEW_LOGICAL_LEFT: -1,
6 | VIEW_LOGICAL_RIGHT: 1,
7 |
8 | VIEW_LOGICAL_MAX_LEFT: -2,
9 | VIEW_LOGICAL_MAX_RIGHT: 2,
10 | VIEW_LOGICAL_MAX_BOTTOM: -2,
11 | VIEW_LOGICAL_MAX_TOP: 2,
12 |
13 | PRIORITY_NONE: 0,
14 | PRIORITY_IDLE: 1,
15 | PRIORITY_NORMAL: 2,
16 | PRIORITY_FORCE: 3,
17 |
18 | MOTION_GROUP_IDLE: 'idle',
19 | MOTION_GROUP_TAP_BODY: 'tap_body',
20 | MOTION_GROUP_FLICK_HEAD: 'flick_head',
21 | MOTION_GROUP_PINCH_IN: 'pinch_in',
22 | MOTION_GROUP_PINCH_OUT: 'pinch_out',
23 | MOTION_GROUP_SHAKE: 'shake',
24 |
25 | HIT_AREA_HEAD: 'head',
26 | HIT_AREA_BODY: 'body',
27 | };
28 |
29 | export default LAppDefine;
30 |
--------------------------------------------------------------------------------
/src/cubism2/LAppLive2DManager.js:
--------------------------------------------------------------------------------
1 | /* global Live2D */
2 | import { Live2DFramework } from './Live2DFramework.js';
3 | import LAppModel from './LAppModel.js';
4 | import PlatformManager from './PlatformManager.js';
5 | import LAppDefine from './LAppDefine.js';
6 | import logger from '../logger.js';
7 |
8 | class LAppLive2DManager {
9 | constructor() {
10 | this.model = null;
11 | this.reloading = false;
12 |
13 | Live2D.init();
14 | Live2DFramework.setPlatformManager(new PlatformManager());
15 | }
16 |
17 | getModel() {
18 | return this.model;
19 | }
20 |
21 | releaseModel(gl) {
22 | if (this.model) {
23 | this.model.release(gl);
24 | this.model = null;
25 | }
26 | }
27 |
28 | async changeModel(gl, modelSettingPath) {
29 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
30 | return new Promise((resolve, reject) => {
31 | if (this.reloading) return;
32 | this.reloading = true;
33 |
34 | const oldModel = this.model;
35 | const newModel = new LAppModel();
36 |
37 | newModel.load(gl, modelSettingPath, () => {
38 | if (oldModel) {
39 | oldModel.release(gl);
40 | }
41 | this.model = newModel;
42 | this.reloading = false;
43 | resolve();
44 | });
45 | });
46 | }
47 |
48 | async changeModelWithJSON(gl, modelSettingPath, modelSetting) {
49 | if (this.reloading) return;
50 | this.reloading = true;
51 |
52 | const oldModel = this.model;
53 | const newModel = new LAppModel();
54 |
55 | await newModel.loadModelSetting(modelSettingPath, modelSetting);
56 | if (oldModel) {
57 | oldModel.release(gl);
58 | }
59 | this.model = newModel;
60 | this.reloading = false;
61 | }
62 |
63 | setDrag(x, y) {
64 | if (this.model) {
65 | this.model.setDrag(x, y);
66 | }
67 | }
68 |
69 | maxScaleEvent() {
70 | logger.trace('Max scale event.');
71 | if (this.model) {
72 | this.model.startRandomMotion(
73 | LAppDefine.MOTION_GROUP_PINCH_IN,
74 | LAppDefine.PRIORITY_NORMAL,
75 | );
76 | }
77 | }
78 |
79 | minScaleEvent() {
80 | logger.trace('Min scale event.');
81 | if (this.model) {
82 | this.model.startRandomMotion(
83 | LAppDefine.MOTION_GROUP_PINCH_OUT,
84 | LAppDefine.PRIORITY_NORMAL,
85 | );
86 | }
87 | }
88 |
89 | tapEvent(x, y) {
90 | logger.trace('tapEvent view x:' + x + ' y:' + y);
91 |
92 | if (!this.model) return false;
93 |
94 | if (this.model.hitTest(LAppDefine.HIT_AREA_HEAD, x, y)) {
95 | logger.trace('Tap face.');
96 | this.model.setRandomExpression();
97 | } else if (this.model.hitTest(LAppDefine.HIT_AREA_BODY, x, y)) {
98 | logger.trace('Tap body.');
99 | this.model.startRandomMotion(
100 | LAppDefine.MOTION_GROUP_TAP_BODY,
101 | LAppDefine.PRIORITY_NORMAL,
102 | );
103 | }
104 | return true;
105 | }
106 | }
107 |
108 | export default LAppLive2DManager;
109 |
--------------------------------------------------------------------------------
/src/cubism2/PlatformManager.js:
--------------------------------------------------------------------------------
1 | /* global Image, Live2DModelWebGL, document, fetch */
2 | /**
3 | *
4 | * You can modify and use this source freely
5 | * only for the development of application related Live2D.
6 | *
7 | * (c) Live2D Inc. All rights reserved.
8 | */
9 |
10 | import logger from '../logger.js';
11 | //============================================================
12 | //============================================================
13 | // class PlatformManager extend IPlatformManager
14 | //============================================================
15 | //============================================================
16 | class PlatformManager {
17 | constructor() {
18 | this.cache = {};
19 | }
20 | //============================================================
21 | // PlatformManager # loadBytes()
22 | //============================================================
23 | loadBytes(path /*String*/, callback) {
24 | if (path in this.cache) {
25 | return callback(this.cache[path]);
26 | }
27 | fetch(path)
28 | .then(response => response.arrayBuffer())
29 | .then(arrayBuffer => {
30 | this.cache[path] = arrayBuffer;
31 | callback(arrayBuffer);
32 | });
33 | }
34 |
35 | //============================================================
36 | // PlatformManager # loadLive2DModel()
37 | //============================================================
38 | loadLive2DModel(path /*String*/, callback) {
39 | let model = null;
40 |
41 | // load moc
42 | this.loadBytes(path, buf => {
43 | model = Live2DModelWebGL.loadModel(buf);
44 | callback(model);
45 | });
46 | }
47 |
48 | //============================================================
49 | // PlatformManager # loadTexture()
50 | //============================================================
51 | loadTexture(model /*ALive2DModel*/, no /*int*/, path /*String*/, callback) {
52 | // load textures
53 | const loadedImage = new Image();
54 | loadedImage.crossOrigin = 'anonymous';
55 | loadedImage.src = path;
56 |
57 | loadedImage.onload = () => {
58 | // create texture
59 | const canvas = document.getElementById('live2d');
60 | const gl = canvas.getContext('webgl2', { premultipliedAlpha: true, preserveDrawingBuffer: true });
61 | let texture = gl.createTexture();
62 | if (!texture) {
63 | logger.error('Failed to generate gl texture name.');
64 | return -1;
65 | }
66 |
67 | if (model.isPremultipliedAlpha() == false) {
68 | // 乗算済アルファテクスチャ以外の場合
69 | gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 1);
70 | }
71 | gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
72 | gl.activeTexture(gl.TEXTURE0);
73 | gl.bindTexture(gl.TEXTURE_2D, texture);
74 | gl.texImage2D(
75 | gl.TEXTURE_2D,
76 | 0,
77 | gl.RGBA,
78 | gl.RGBA,
79 | gl.UNSIGNED_BYTE,
80 | loadedImage,
81 | );
82 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
83 | gl.texParameteri(
84 | gl.TEXTURE_2D,
85 | gl.TEXTURE_MIN_FILTER,
86 | gl.LINEAR_MIPMAP_NEAREST,
87 | );
88 | gl.generateMipmap(gl.TEXTURE_2D);
89 |
90 | model.setTexture(no, texture);
91 |
92 | // テクスチャオブジェクトを解放
93 | texture = null;
94 |
95 | if (typeof callback == 'function') callback();
96 | };
97 |
98 | loadedImage.onerror = () => {
99 | logger.error('Failed to load image : ' + path);
100 | };
101 | }
102 |
103 | //============================================================
104 | // PlatformManager # parseFromBytes(buf)
105 |
106 | //============================================================
107 | jsonParseFromBytes(buf) {
108 | let jsonStr;
109 |
110 | const bomCode = new Uint8Array(buf, 0, 3);
111 | if (bomCode[0] == 239 && bomCode[1] == 187 && bomCode[2] == 191) {
112 | jsonStr = String.fromCharCode.apply(null, new Uint8Array(buf, 3));
113 | } else {
114 | jsonStr = String.fromCharCode.apply(null, new Uint8Array(buf));
115 | }
116 |
117 | const jsonObj = JSON.parse(jsonStr);
118 |
119 | return jsonObj;
120 | }
121 | }
122 |
123 | export default PlatformManager;
124 |
--------------------------------------------------------------------------------
/src/cubism2/index.js:
--------------------------------------------------------------------------------
1 | /* global document, window, Event, Live2D */
2 | import { L2DMatrix44, L2DTargetPoint, L2DViewMatrix } from './Live2DFramework.js';
3 | import LAppDefine from './LAppDefine.js';
4 | import MatrixStack from './utils/MatrixStack.js';
5 | import LAppLive2DManager from './LAppLive2DManager.js';
6 | import logger from '../logger.js';
7 |
8 | function normalizePoint(x, y, x0, y0, w, h) {
9 | const dx = x - x0;
10 | const dy = y - y0;
11 |
12 | let targetX = 0, targetY = 0;
13 |
14 | if (dx >= 0) {
15 | targetX = dx / (w - x0);
16 | } else {
17 | targetX = dx / x0;
18 | }
19 | if (dy >= 0) {
20 | targetY = dy / (h - y0);
21 | } else {
22 | targetY = dy / y0;
23 | }
24 | return {
25 | vx: targetX,
26 | vy: -targetY
27 | };
28 | }
29 |
30 | class Cubism2Model {
31 | constructor() {
32 | this.live2DMgr = new LAppLive2DManager();
33 |
34 | this.isDrawStart = false;
35 |
36 | this.gl = null;
37 | this.canvas = null;
38 |
39 | this.dragMgr = null; /*new L2DTargetPoint();*/
40 | this.viewMatrix = null; /*new L2DViewMatrix();*/
41 | this.projMatrix = null; /*new L2DMatrix44()*/
42 | this.deviceToScreen = null; /*new L2DMatrix44();*/
43 |
44 | this.oldLen = 0;
45 |
46 | this._boundMouseEvent = this.mouseEvent.bind(this);
47 | this._boundTouchEvent = this.touchEvent.bind(this);
48 | }
49 |
50 | initL2dCanvas(canvasId) {
51 | this.canvas = document.getElementById(canvasId);
52 |
53 | if (this.canvas.addEventListener) {
54 | this.canvas.addEventListener('mousewheel', this._boundMouseEvent, false);
55 | this.canvas.addEventListener('click', this._boundMouseEvent, false);
56 |
57 | document.addEventListener('mousemove', this._boundMouseEvent, false);
58 |
59 | document.addEventListener('mouseout', this._boundMouseEvent, false);
60 | this.canvas.addEventListener('contextmenu', this._boundMouseEvent, false);
61 |
62 | this.canvas.addEventListener('touchstart', this._boundTouchEvent, false);
63 | this.canvas.addEventListener('touchend', this._boundTouchEvent, false);
64 | this.canvas.addEventListener('touchmove', this._boundTouchEvent, false);
65 | }
66 | }
67 |
68 | async init(canvasId, modelSettingPath, modelSetting) {
69 | this.initL2dCanvas(canvasId);
70 | const width = this.canvas.width;
71 | const height = this.canvas.height;
72 |
73 | this.dragMgr = new L2DTargetPoint();
74 |
75 | const ratio = height / width;
76 | const left = LAppDefine.VIEW_LOGICAL_LEFT;
77 | const right = LAppDefine.VIEW_LOGICAL_RIGHT;
78 | const bottom = -ratio;
79 | const top = ratio;
80 |
81 | this.viewMatrix = new L2DViewMatrix();
82 |
83 | this.viewMatrix.setScreenRect(left, right, bottom, top);
84 |
85 | this.viewMatrix.setMaxScreenRect(
86 | LAppDefine.VIEW_LOGICAL_MAX_LEFT,
87 | LAppDefine.VIEW_LOGICAL_MAX_RIGHT,
88 | LAppDefine.VIEW_LOGICAL_MAX_BOTTOM,
89 | LAppDefine.VIEW_LOGICAL_MAX_TOP,
90 | );
91 |
92 | this.viewMatrix.setMaxScale(LAppDefine.VIEW_MAX_SCALE);
93 | this.viewMatrix.setMinScale(LAppDefine.VIEW_MIN_SCALE);
94 |
95 | this.projMatrix = new L2DMatrix44();
96 | this.projMatrix.multScale(1, width / height);
97 |
98 | this.deviceToScreen = new L2DMatrix44();
99 | this.deviceToScreen.multTranslate(-width / 2.0, -height / 2.0);
100 | this.deviceToScreen.multScale(2 / width, -2 / width);
101 |
102 | // https://stackoverflow.com/questions/26783586/canvas-todataurl-returns-blank-image
103 | this.gl = this.canvas.getContext('webgl2', { premultipliedAlpha: true, preserveDrawingBuffer: true });
104 | if (!this.gl) {
105 | logger.error('Failed to create WebGL context.');
106 | return;
107 | }
108 |
109 | Live2D.setGL(this.gl);
110 |
111 | this.gl.clearColor(0.0, 0.0, 0.0, 0.0);
112 |
113 | await this.changeModelWithJSON(modelSettingPath, modelSetting);
114 |
115 | this.startDraw();
116 | }
117 |
118 | destroy() {
119 | // 1. Unbind canvas events
120 | if (this.canvas) {
121 | this.canvas.removeEventListener('mousewheel', this._boundMouseEvent, false);
122 | this.canvas.removeEventListener('click', this._boundMouseEvent, false);
123 | document.removeEventListener('mousemove', this._boundMouseEvent, false);
124 | document.removeEventListener('mouseout', this._boundMouseEvent, false);
125 | this.canvas.removeEventListener('contextmenu', this._boundMouseEvent, false);
126 |
127 | this.canvas.removeEventListener('touchstart', this._boundTouchEvent, false);
128 | this.canvas.removeEventListener('touchend', this._boundTouchEvent, false);
129 | this.canvas.removeEventListener('touchmove', this._boundTouchEvent, false);
130 | }
131 |
132 | // 2. Stop animation
133 | if (this._drawFrameId) {
134 | window.cancelAnimationFrame(this._drawFrameId);
135 | this._drawFrameId = null;
136 | }
137 | this.isDrawStart = false;
138 |
139 | // 3. Release Live2D related resources
140 | if (this.live2DMgr && typeof this.live2DMgr.release === 'function') {
141 | this.live2DMgr.release();
142 | }
143 |
144 | // 4. Clean up WebGL resources (if any)
145 | if (this.gl) {
146 | // Implemented via resetCanvas
147 | }
148 |
149 | // 5. Clear references to assist GC
150 | this.canvas = null;
151 | this.gl = null;
152 | // this.live2DMgr = null;
153 | this.dragMgr = null;
154 | this.viewMatrix = null;
155 | this.projMatrix = null;
156 | this.deviceToScreen = null;
157 | }
158 |
159 | startDraw() {
160 | if (!this.isDrawStart) {
161 | this.isDrawStart = true;
162 | const tick = () => {
163 | this.draw();
164 | this._drawFrameId = window.requestAnimationFrame(tick, this.canvas);
165 | };
166 | tick();
167 | }
168 | }
169 |
170 | draw() {
171 | // logger.trace("--> draw()");
172 |
173 | MatrixStack.reset();
174 | MatrixStack.loadIdentity();
175 |
176 | this.dragMgr.update();
177 | this.live2DMgr.setDrag(this.dragMgr.getX(), this.dragMgr.getY());
178 |
179 | this.gl.clear(this.gl.COLOR_BUFFER_BIT);
180 |
181 | MatrixStack.multMatrix(this.projMatrix.getArray());
182 | MatrixStack.multMatrix(this.viewMatrix.getArray());
183 | MatrixStack.push();
184 |
185 | const model = this.live2DMgr.getModel();
186 |
187 | if (model == null) return;
188 |
189 | if (model.initialized && !model.updating) {
190 | model.update();
191 | model.draw(this.gl);
192 | }
193 |
194 | MatrixStack.pop();
195 | }
196 |
197 | async changeModel(modelSettingPath) {
198 | await this.live2DMgr.changeModel(this.gl, modelSettingPath);
199 | }
200 |
201 | async changeModelWithJSON(modelSettingPath, modelSetting) {
202 | await this.live2DMgr.changeModelWithJSON(this.gl, modelSettingPath, modelSetting);
203 | }
204 |
205 | modelScaling(scale) {
206 | const isMaxScale = this.viewMatrix.isMaxScale();
207 | const isMinScale = this.viewMatrix.isMinScale();
208 |
209 | this.viewMatrix.adjustScale(0, 0, scale);
210 |
211 | if (!isMaxScale) {
212 | if (this.viewMatrix.isMaxScale()) {
213 | this.live2DMgr.maxScaleEvent();
214 | }
215 | }
216 |
217 | if (!isMinScale) {
218 | if (this.viewMatrix.isMinScale()) {
219 | this.live2DMgr.minScaleEvent();
220 | }
221 | }
222 | }
223 |
224 | modelTurnHead(event) {
225 | const rect = this.canvas.getBoundingClientRect();
226 |
227 | const { vx, vy } = normalizePoint(event.clientX, event.clientY, rect.left + rect.width / 2, rect.top + rect.height / 2, window.innerWidth, window.innerHeight);
228 |
229 | logger.trace(
230 | 'onMouseDown device( x:' +
231 | event.clientX +
232 | ' y:' +
233 | event.clientY +
234 | ' ) view( x:' +
235 | vx +
236 | ' y:' +
237 | vy +
238 | ')',
239 | );
240 |
241 | this.dragMgr.setPoint(vx, vy);
242 | this.live2DMgr.tapEvent(vx, vy);
243 |
244 | if (this.live2DMgr?.model.hitTest(LAppDefine.HIT_AREA_BODY, vx, vy)) {
245 | window.dispatchEvent(new Event('live2d:tapbody'));
246 | }
247 | }
248 |
249 | followPointer(event) {
250 | const rect = event.target.getBoundingClientRect();
251 |
252 | const { vx, vy } = normalizePoint(event.clientX, event.clientY, rect.left + rect.width / 2, rect.top + rect.height / 2, window.innerWidth, window.innerHeight);
253 |
254 | logger.trace(
255 | 'onMouseMove device( x:' +
256 | event.clientX +
257 | ' y:' +
258 | event.clientY +
259 | ' ) view( x:' +
260 | vx +
261 | ' y:' +
262 | vy +
263 | ')',
264 | );
265 |
266 | this.dragMgr.setPoint(vx, vy);
267 |
268 | if (this.live2DMgr?.model.hitTest(LAppDefine.HIT_AREA_BODY, vx, vy)) {
269 | window.dispatchEvent(new Event('live2d:hoverbody'));
270 | }
271 | }
272 |
273 | lookFront() {
274 | this.dragMgr.setPoint(0, 0);
275 | }
276 |
277 | mouseEvent(e) {
278 | e.preventDefault();
279 |
280 | if (e.type == 'mousewheel') {
281 | if (e.wheelDelta > 0) this.modelScaling(1.1);
282 | else this.modelScaling(1);
283 | } else if (e.type == 'click' || e.type == 'contextmenu') {
284 | this.modelTurnHead(e);
285 | } else if (e.type == 'mousemove') {
286 | this.followPointer(e);
287 | } else if (e.type == 'mouseout') {
288 | this.lookFront();
289 | }
290 | }
291 |
292 | touchEvent(e) {
293 | e.preventDefault();
294 |
295 | const touch = e.touches[0];
296 |
297 | if (e.type == 'touchstart') {
298 | if (e.touches.length == 1) this.modelTurnHead(touch);
299 | // onClick(touch);
300 | } else if (e.type == 'touchmove') {
301 | this.followPointer(touch);
302 |
303 | if (e.touches.length == 2) {
304 | const touch1 = e.touches[0];
305 | const touch2 = e.touches[1];
306 |
307 | const len =
308 | Math.pow(touch1.pageX - touch2.pageX, 2) +
309 | Math.pow(touch1.pageY - touch2.pageY, 2);
310 | if (this.oldLen - len < 0) this.modelScaling(1.025);
311 | else this.modelScaling(0.975);
312 |
313 | this.oldLen = len;
314 | }
315 | } else if (e.type == 'touchend') {
316 | this.lookFront();
317 | }
318 | }
319 |
320 | transformViewX(deviceX) {
321 | const screenX = this.deviceToScreen.transformX(deviceX);
322 | return this.viewMatrix.invertTransformX(screenX);
323 | }
324 |
325 | transformViewY(deviceY) {
326 | const screenY = this.deviceToScreen.transformY(deviceY);
327 | return this.viewMatrix.invertTransformY(screenY);
328 | }
329 |
330 | transformScreenX(deviceX) {
331 | return this.deviceToScreen.transformX(deviceX);
332 | }
333 |
334 | transformScreenY(deviceY) {
335 | return this.deviceToScreen.transformY(deviceY);
336 | }
337 | }
338 |
339 | export default Cubism2Model;
340 |
--------------------------------------------------------------------------------
/src/cubism2/utils/MatrixStack.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * You can modify and use this source freely
4 | * only for the development of application related Live2D.
5 | *
6 | * (c) Live2D Inc. All rights reserved.
7 | */
8 |
9 | class MatrixStack {
10 | static reset() {
11 | this.depth = 0;
12 | }
13 |
14 | static loadIdentity() {
15 | for (let i = 0; i < 16; i++) {
16 | this.currentMatrix[i] = i % 5 == 0 ? 1 : 0;
17 | }
18 | }
19 |
20 | static push() {
21 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
22 | const offset = this.depth * 16;
23 | const nextOffset = (this.depth + 1) * 16;
24 |
25 | if (this.matrixStack.length < nextOffset + 16) {
26 | this.matrixStack.length = nextOffset + 16;
27 | }
28 |
29 | for (let i = 0; i < 16; i++) {
30 | this.matrixStack[nextOffset + i] = this.currentMatrix[i];
31 | }
32 |
33 | this.depth++;
34 | }
35 |
36 | static pop() {
37 | this.depth--;
38 | if (this.depth < 0) {
39 | this.depth = 0;
40 | }
41 |
42 | const offset = this.depth * 16;
43 | for (let i = 0; i < 16; i++) {
44 | this.currentMatrix[i] = this.matrixStack[offset + i];
45 | }
46 | }
47 |
48 | static getMatrix() {
49 | return this.currentMatrix;
50 | }
51 |
52 | static multMatrix(matNew) {
53 | let i, j, k;
54 |
55 | for (i = 0; i < 16; i++) {
56 | this.tmp[i] = 0;
57 | }
58 |
59 | for (i = 0; i < 4; i++) {
60 | for (j = 0; j < 4; j++) {
61 | for (k = 0; k < 4; k++) {
62 | this.tmp[i + j * 4] +=
63 | this.currentMatrix[i + k * 4] * matNew[k + j * 4];
64 | }
65 | }
66 | }
67 | for (i = 0; i < 16; i++) {
68 | this.currentMatrix[i] = this.tmp[i];
69 | }
70 | }
71 | }
72 |
73 | MatrixStack.matrixStack = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
74 |
75 | MatrixStack.depth = 0;
76 |
77 | MatrixStack.currentMatrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
78 |
79 | MatrixStack.tmp = new Array(16);
80 |
81 | export default MatrixStack;
82 |
--------------------------------------------------------------------------------
/src/cubism2/utils/ModelSettingJson.js:
--------------------------------------------------------------------------------
1 | import { Live2DFramework } from '../Live2DFramework.js';
2 |
3 | class ModelSettingJson {
4 | constructor() {
5 | this.NAME = 'name';
6 | this.ID = 'id';
7 | this.MODEL = 'model';
8 | this.TEXTURES = 'textures';
9 | this.HIT_AREAS = 'hit_areas';
10 | this.HIT_AREAS_CUSTOM = 'hit_areas_custom';
11 | this.PHYSICS = 'physics';
12 | this.POSE = 'pose';
13 | this.EXPRESSIONS = 'expressions';
14 | this.MOTION_GROUPS = 'motions';
15 | this.SOUND = 'sound';
16 | this.FADE_IN = 'fade_in';
17 | this.FADE_OUT = 'fade_out';
18 | this.LAYOUT = 'layout';
19 | this.INIT_PARAM = 'init_param';
20 | this.INIT_PARTS_VISIBLE = 'init_parts_visible';
21 | this.VALUE = 'val';
22 | this.FILE = 'file';
23 |
24 | this.json = {};
25 | }
26 |
27 | loadModelSetting(path, callback) {
28 | const pm = Live2DFramework.getPlatformManager();
29 | pm.loadBytes(path, buf => {
30 | const str = String.fromCharCode.apply(null, new Uint8Array(buf));
31 | this.json = JSON.parse(str);
32 | callback();
33 | });
34 | }
35 |
36 | getTextureFile(n) {
37 | if (this.json[this.TEXTURES] == null || this.json[this.TEXTURES][n] == null)
38 | return null;
39 |
40 | return this.json[this.TEXTURES][n];
41 | }
42 |
43 | getModelFile() {
44 | return this.json[this.MODEL];
45 | }
46 |
47 | getTextureNum() {
48 | if (this.json[this.TEXTURES] == null) return 0;
49 |
50 | return this.json[this.TEXTURES].length;
51 | }
52 |
53 | getHitAreaNum() {
54 | if (this.json[this.HIT_AREAS] == null) return 0;
55 |
56 | return this.json[this.HIT_AREAS].length;
57 | }
58 |
59 | getHitAreaCustom() {
60 | return this.json[this.HIT_AREAS_CUSTOM];
61 | }
62 |
63 | getHitAreaID(n) {
64 | if (
65 | this.json[this.HIT_AREAS] == null ||
66 | this.json[this.HIT_AREAS][n] == null
67 | )
68 | return null;
69 |
70 | return this.json[this.HIT_AREAS][n][this.ID];
71 | }
72 |
73 | getHitAreaName(n) {
74 | if (
75 | this.json[this.HIT_AREAS] == null ||
76 | this.json[this.HIT_AREAS][n] == null
77 | )
78 | return null;
79 |
80 | return this.json[this.HIT_AREAS][n][this.NAME];
81 | }
82 |
83 | getPhysicsFile() {
84 | return this.json[this.PHYSICS];
85 | }
86 |
87 | getPoseFile() {
88 | return this.json[this.POSE];
89 | }
90 |
91 | getExpressionNum() {
92 | return this.json[this.EXPRESSIONS] == null
93 | ? 0
94 | : this.json[this.EXPRESSIONS].length;
95 | }
96 |
97 | getExpressionFile(n) {
98 | if (this.json[this.EXPRESSIONS] == null) return null;
99 | return this.json[this.EXPRESSIONS][n][this.FILE];
100 | }
101 |
102 | getExpressionName(n) {
103 | if (this.json[this.EXPRESSIONS] == null) return null;
104 | return this.json[this.EXPRESSIONS][n][this.NAME];
105 | }
106 |
107 | getLayout() {
108 | return this.json[this.LAYOUT];
109 | }
110 |
111 | getInitParamNum() {
112 | return this.json[this.INIT_PARAM] == null
113 | ? 0
114 | : this.json[this.INIT_PARAM].length;
115 | }
116 |
117 | getMotionNum(name) {
118 | if (
119 | this.json[this.MOTION_GROUPS] == null ||
120 | this.json[this.MOTION_GROUPS][name] == null
121 | )
122 | return 0;
123 |
124 | return this.json[this.MOTION_GROUPS][name].length;
125 | }
126 |
127 | getMotionFile(name, n) {
128 | if (
129 | this.json[this.MOTION_GROUPS] == null ||
130 | this.json[this.MOTION_GROUPS][name] == null ||
131 | this.json[this.MOTION_GROUPS][name][n] == null
132 | )
133 | return null;
134 |
135 | return this.json[this.MOTION_GROUPS][name][n][this.FILE];
136 | }
137 |
138 | getMotionSound(name, n) {
139 | if (
140 | this.json[this.MOTION_GROUPS] == null ||
141 | this.json[this.MOTION_GROUPS][name] == null ||
142 | this.json[this.MOTION_GROUPS][name][n] == null ||
143 | this.json[this.MOTION_GROUPS][name][n][this.SOUND] == null
144 | )
145 | return null;
146 |
147 | return this.json[this.MOTION_GROUPS][name][n][this.SOUND];
148 | }
149 |
150 | getMotionFadeIn(name, n) {
151 | if (
152 | this.json[this.MOTION_GROUPS] == null ||
153 | this.json[this.MOTION_GROUPS][name] == null ||
154 | this.json[this.MOTION_GROUPS][name][n] == null ||
155 | this.json[this.MOTION_GROUPS][name][n][this.FADE_IN] == null
156 | )
157 | return 1000;
158 |
159 | return this.json[this.MOTION_GROUPS][name][n][this.FADE_IN];
160 | }
161 |
162 | getMotionFadeOut(name, n) {
163 | if (
164 | this.json[this.MOTION_GROUPS] == null ||
165 | this.json[this.MOTION_GROUPS][name] == null ||
166 | this.json[this.MOTION_GROUPS][name][n] == null ||
167 | this.json[this.MOTION_GROUPS][name][n][this.FADE_OUT] == null
168 | )
169 | return 1000;
170 |
171 | return this.json[this.MOTION_GROUPS][name][n][this.FADE_OUT];
172 | }
173 |
174 | getInitParamID(n) {
175 | if (
176 | this.json[this.INIT_PARAM] == null ||
177 | this.json[this.INIT_PARAM][n] == null
178 | )
179 | return null;
180 |
181 | return this.json[this.INIT_PARAM][n][this.ID];
182 | }
183 |
184 | getInitParamValue(n) {
185 | if (
186 | this.json[this.INIT_PARAM] == null ||
187 | this.json[this.INIT_PARAM][n] == null
188 | )
189 | return NaN;
190 |
191 | return this.json[this.INIT_PARAM][n][this.VALUE];
192 | }
193 |
194 | getInitPartsVisibleNum() {
195 | return this.json[this.INIT_PARTS_VISIBLE] == null
196 | ? 0
197 | : this.json[this.INIT_PARTS_VISIBLE].length;
198 | }
199 |
200 | getInitPartsVisibleID(n) {
201 | if (
202 | this.json[this.INIT_PARTS_VISIBLE] == null ||
203 | this.json[this.INIT_PARTS_VISIBLE][n] == null
204 | )
205 | return null;
206 | return this.json[this.INIT_PARTS_VISIBLE][n][this.ID];
207 | }
208 |
209 | getInitPartsVisibleValue(n) {
210 | if (
211 | this.json[this.INIT_PARTS_VISIBLE] == null ||
212 | this.json[this.INIT_PARTS_VISIBLE][n] == null
213 | )
214 | return NaN;
215 |
216 | return this.json[this.INIT_PARTS_VISIBLE][n][this.VALUE];
217 | }
218 | }
219 |
220 | export default ModelSettingJson;
221 |
--------------------------------------------------------------------------------
/src/cubism5/index.js:
--------------------------------------------------------------------------------
1 | /* global document, window, Event */
2 |
3 | import { LAppDelegate } from '@demo/lappdelegate.js';
4 | import { LAppSubdelegate } from '@demo/lappsubdelegate.js';
5 | import * as LAppDefine from '@demo/lappdefine.js';
6 | import { LAppModel } from '@demo/lappmodel.js';
7 | import { LAppPal } from '@demo/lapppal';
8 | import logger from '../logger.js';
9 |
10 | LAppPal.printMessage = () => {};
11 |
12 | // Custom subdelegate class, responsible for Canvas-related initialization and rendering management
13 | class AppSubdelegate extends LAppSubdelegate {
14 | /**
15 | * Initialize resources required by the application.
16 | * @param {HTMLCanvasElement} canvas The canvas object passed in
17 | */
18 | initialize(canvas) {
19 | // Initialize WebGL manager, return false if failed
20 | if (!this._glManager.initialize(canvas)) {
21 | return false;
22 | }
23 |
24 | this._canvas = canvas;
25 |
26 | // Canvas size setting, supports auto and specified size
27 | if (LAppDefine.CanvasSize === 'auto') {
28 | this.resizeCanvas();
29 | } else {
30 | canvas.width = LAppDefine.CanvasSize.width;
31 | canvas.height = LAppDefine.CanvasSize.height;
32 | }
33 |
34 | // Set the GL manager for the texture manager
35 | this._textureManager.setGlManager(this._glManager);
36 |
37 | const gl = this._glManager.getGl();
38 |
39 | // If the framebuffer object is not initialized, get the current framebuffer binding
40 | if (!this._frameBuffer) {
41 | this._frameBuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING);
42 | }
43 |
44 | // Enable blend mode for transparency
45 | gl.enable(gl.BLEND);
46 | gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
47 |
48 | // Initialize the view (AppView)
49 | this._view.initialize(this);
50 | this._view._gear = {
51 | render: () => {},
52 | isHit: () => {},
53 | release: () => {}
54 | };
55 | this._view._back = {
56 | render: () => {},
57 | release: () => {}
58 | };
59 | // this._view.initializeSprite();
60 |
61 | // Associate Live2D manager with the current subdelegate
62 | // this._live2dManager.initialize(this);
63 | this._live2dManager._subdelegate = this;
64 |
65 | // Listen for canvas size changes for responsive adaptation
66 | this._resizeObserver = new window.ResizeObserver(
67 | (entries, observer) =>
68 | this.resizeObserverCallback.call(this, entries, observer)
69 | );
70 | this._resizeObserver.observe(this._canvas);
71 |
72 | return true;
73 | }
74 |
75 | /**
76 | * Adjust and reinitialize the view when the canvas size changes
77 | */
78 | onResize() {
79 | this.resizeCanvas();
80 | this._view.initialize(this);
81 | // this._view.initializeSprite();
82 | }
83 |
84 | /**
85 | * Main render loop, called periodically to update the screen
86 | */
87 | update() {
88 | // Check if the WebGL context is lost, if so, stop rendering
89 | if (this._glManager.getGl().isContextLost()) {
90 | return;
91 | }
92 |
93 | // If resize is needed, call onResize
94 | if (this._needResize) {
95 | this.onResize();
96 | this._needResize = false;
97 | }
98 |
99 | const gl = this._glManager.getGl();
100 |
101 | // Initialize the canvas as fully transparent
102 | gl.clearColor(0.0, 0.0, 0.0, 0.0);
103 |
104 | // Enable depth test to ensure correct model occlusion
105 | gl.enable(gl.DEPTH_TEST);
106 |
107 | // Set depth function so nearer objects cover farther ones
108 | gl.depthFunc(gl.LEQUAL);
109 |
110 | // Clear color and depth buffers
111 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
112 | gl.clearDepth(1.0);
113 |
114 | // Enable blend mode again to ensure transparency
115 | gl.enable(gl.BLEND);
116 | gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
117 |
118 | // Render the view content
119 | this._view.render();
120 | }
121 | }
122 |
123 | // Main application delegate class, responsible for managing the main loop, canvas, model switching, and other global logic
124 | export class AppDelegate extends LAppDelegate {
125 | /**
126 | * Start the main loop.
127 | */
128 | run() {
129 | // Main loop function, responsible for updating time and all subdelegates
130 | const loop = () => {
131 | // Update time
132 | LAppPal.updateTime();
133 |
134 | // Iterate all subdelegates and call update for rendering
135 | for (let i = 0; i < this._subdelegates.getSize(); i++) {
136 | this._subdelegates.at(i).update();
137 | }
138 |
139 | // Recursive call for animation loop
140 | this._drawFrameId = window.requestAnimationFrame(loop);
141 | };
142 | loop();
143 | }
144 |
145 | stop() {
146 | if (this._drawFrameId) {
147 | window.cancelAnimationFrame(this._drawFrameId);
148 | this._drawFrameId = null;
149 | }
150 | }
151 |
152 | release() {
153 | this.stop();
154 | this.releaseEventListener();
155 | this._subdelegates.clear();
156 |
157 | this._cubismOption = null;
158 | }
159 |
160 | transformOffset(e) {
161 | const subdelegate = this._subdelegates.at(0);
162 | const rect = subdelegate.getCanvas().getBoundingClientRect();
163 | const localX = e.pageX - rect.left;
164 | const localY = e.pageY - rect.top;
165 | const posX = localX * window.devicePixelRatio;
166 | const posY = localY * window.devicePixelRatio;
167 | const x = subdelegate._view.transformViewX(posX);
168 | const y = subdelegate._view.transformViewY(posY);
169 | return {
170 | x, y
171 | };
172 | }
173 |
174 | onMouseMove(e) {
175 | const lapplive2dmanager = this._subdelegates.at(0).getLive2DManager();
176 | const { x, y } = this.transformOffset(e);
177 | const model = lapplive2dmanager._models.at(0);
178 |
179 | lapplive2dmanager.onDrag(x, y);
180 | lapplive2dmanager.onTap(x, y);
181 | if (model.hitTest(LAppDefine.HitAreaNameBody, x, y)) {
182 | window.dispatchEvent(new Event('live2d:hoverbody'));
183 | }
184 | }
185 |
186 | onMouseEnd(e) {
187 | const lapplive2dmanager = this._subdelegates.at(0).getLive2DManager();
188 | const { x, y } = this.transformOffset(e);
189 | lapplive2dmanager.onDrag(0.0, 0.0);
190 | lapplive2dmanager.onTap(x, y);
191 | }
192 |
193 | onTap(e) {
194 | const lapplive2dmanager = this._subdelegates.at(0).getLive2DManager();
195 | const { x, y } = this.transformOffset(e);
196 | const model = lapplive2dmanager._models.at(0);
197 |
198 | if (model.hitTest(LAppDefine.HitAreaNameBody, x, y)) {
199 | window.dispatchEvent(new Event('live2d:tapbody'));
200 | }
201 | }
202 |
203 | initializeEventListener() {
204 | this.mouseMoveEventListener = this.onMouseMove.bind(this);
205 | this.mouseEndedEventListener = this.onMouseEnd.bind(this);
206 | this.tapEventListener = this.onTap.bind(this);
207 |
208 | document.addEventListener('mousemove', this.mouseMoveEventListener, {
209 | passive: true
210 | });
211 | document.addEventListener('mouseout', this.mouseEndedEventListener, {
212 | passive: true
213 | });
214 | document.addEventListener('pointerdown', this.tapEventListener, {
215 | passive: true
216 | });
217 | }
218 |
219 | releaseEventListener() {
220 | document.removeEventListener('mousemove', this.mouseMoveEventListener, {
221 | passive: true
222 | });
223 | this.mouseMoveEventListener = null;
224 | document.removeEventListener('mouseout', this.mouseEndedEventListener, {
225 | passive: true
226 | });
227 | this.mouseEndedEventListener = null;
228 | document.removeEventListener('pointerdown', this.tapEventListener, {
229 | passive: true
230 | });
231 | }
232 |
233 | /**
234 | * Create canvas and initialize all Subdelegates
235 | */
236 | initializeSubdelegates() {
237 | // Reserve space to improve performance
238 | this._canvases.prepareCapacity(LAppDefine.CanvasNum);
239 | this._subdelegates.prepareCapacity(LAppDefine.CanvasNum);
240 |
241 | // Get the live2d canvas element from the page
242 | const canvas = document.getElementById('live2d');
243 | this._canvases.pushBack(canvas);
244 |
245 | // Set canvas style size to match actual size
246 | canvas.style.width = canvas.width;
247 | canvas.style.height = canvas.height;
248 |
249 | // For each canvas, create a subdelegate and complete initialization
250 | for (let i = 0; i < this._canvases.getSize(); i++) {
251 | const subdelegate = new AppSubdelegate();
252 | const result = subdelegate.initialize(this._canvases.at(i));
253 | if (!result) {
254 | logger.error('Failed to initialize AppSubdelegate');
255 | return;
256 | }
257 | this._subdelegates.pushBack(subdelegate);
258 | }
259 |
260 | // Check if the WebGL context of each subdelegate is lost
261 | for (let i = 0; i < LAppDefine.CanvasNum; i++) {
262 | if (this._subdelegates.at(i).isContextLost()) {
263 | logger.error(
264 | `The context for Canvas at index ${i} was lost, possibly because the acquisition limit for WebGLRenderingContext was reached.`
265 | );
266 | }
267 | }
268 | }
269 |
270 | /**
271 | * Switch model
272 | * @param {string} modelSettingPath Path to the model setting file
273 | */
274 | changeModel(modelSettingPath) {
275 | const segments = modelSettingPath.split('/');
276 | const modelJsonName = segments.pop();
277 | const modelPath = segments.join('/') + '/';
278 | // Get the current Live2D manager
279 | const live2dManager = this._subdelegates.at(0).getLive2DManager();
280 | // Release all old models
281 | live2dManager.releaseAllModel();
282 | // Create a new model instance, set subdelegate and load resources
283 | const instance = new LAppModel();
284 | instance.setSubdelegate(live2dManager._subdelegate);
285 | instance.loadAssets(modelPath, modelJsonName);
286 | // Add the new model to the model list
287 | live2dManager._models.pushBack(instance);
288 | }
289 |
290 | get subdelegates() {
291 | return this._subdelegates;
292 | }
293 | }
294 |
--------------------------------------------------------------------------------
/src/drag.ts:
--------------------------------------------------------------------------------
1 | function registerDrag() {
2 | const element = document.getElementById('waifu');
3 | if (!element) return;
4 | let winWidth = window.innerWidth,
5 | winHeight = window.innerHeight;
6 | const imgWidth = element.offsetWidth,
7 | imgHeight = element.offsetHeight;
8 | // Bind mousedown event to the element to be dragged
9 | element.addEventListener('mousedown', event => {
10 | if (event.button === 2) {
11 | // Right mouse button, just return, do not handle
12 | return;
13 | }
14 | const canvas = document.getElementById('live2d');
15 | if (event.target !== canvas) return;
16 | event.preventDefault();
17 | // Record the coordinates of the cursor when pressing down on the image
18 | const _offsetX = event.offsetX,
19 | _offsetY = event.offsetY;
20 | // Bind mousemove event
21 | document.onmousemove = event => {
22 | // Get the coordinates of the cursor in the viewport
23 | const _x = event.clientX,
24 | _y = event.clientY;
25 | // Calculate the position of the dragged image
26 | let _left = _x - _offsetX,
27 | _top = _y - _offsetY;
28 | // Check if within the window range
29 | if (_top < 0) { // Top
30 | _top = 0;
31 | } else if (_top >= winHeight - imgHeight) { // Bottom
32 | _top = winHeight - imgHeight;
33 | }
34 | if (_left < 0) { // Left
35 | _left = 0;
36 | } else if (_left >= winWidth - imgWidth) { // Right
37 | _left = winWidth - imgWidth;
38 | }
39 | // Set the position of the element during dragging
40 | element.style.top = _top + 'px';
41 | element.style.left = _left + 'px';
42 | }
43 | // Bind mouseup event
44 | document.onmouseup = () => {
45 | document.onmousemove = null;
46 | }
47 | });
48 | // Reset width and height when the browser window size changes
49 | window.onresize = () => {
50 | winWidth = window.innerWidth;
51 | winHeight = window.innerHeight;
52 | }
53 | }
54 |
55 | export default registerDrag;
56 |
--------------------------------------------------------------------------------
/src/icons.ts:
--------------------------------------------------------------------------------
1 | const fa_comment = '';
2 |
3 | const fa_paper_plane = '';
4 |
5 | const fa_street_view = '';
6 |
7 | const fa_shirt = '';
8 |
9 | const fa_camera_retro = '';
10 |
11 | const fa_info_circle = '';
12 |
13 | const fa_xmark = '';
14 |
15 | const fa_child = '';
16 |
17 | export {
18 | fa_comment,
19 | fa_paper_plane,
20 | fa_street_view,
21 | fa_shirt,
22 | fa_camera_retro,
23 | fa_info_circle,
24 | fa_xmark,
25 | fa_child
26 | }
27 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file Main module.
3 | * @module index
4 | */
5 |
6 | export { default as registerDrag } from './drag.js';
7 | export { default as logger, LogLevel } from './logger.js';
8 | export { default as Cubism2Model } from './cubism2/index.js';
9 |
10 | export * from './tools.js';
11 | export * from './message.js';
12 | export * from './model.js';
13 | export * from './utils.js';
14 | export * from './widget.js';
15 |
--------------------------------------------------------------------------------
/src/logger.ts:
--------------------------------------------------------------------------------
1 | type LogLevel = 'error' | 'warn' | 'info' | 'trace';
2 |
3 | class Logger {
4 | private static levelOrder: Record = {
5 | error: 0,
6 | warn: 1,
7 | info: 2,
8 | trace: 3,
9 | };
10 |
11 | private level: LogLevel;
12 |
13 | constructor(level: LogLevel = 'info') {
14 | this.level = level;
15 | }
16 |
17 | setLevel(level: LogLevel | undefined) {
18 | if (!level) return;
19 | this.level = level;
20 | }
21 |
22 | private shouldLog(level: LogLevel): boolean {
23 | return Logger.levelOrder[level] <= Logger.levelOrder[this.level];
24 | }
25 |
26 | error(message: string, ...args: any[]) {
27 | if (this.shouldLog('error')) {
28 | console.error('[Live2D Widget][ERROR]', message, ...args);
29 | }
30 | }
31 |
32 | warn(message: string, ...args: any[]) {
33 | if (this.shouldLog('warn')) {
34 | console.warn('[Live2D Widget][WARN]', message, ...args);
35 | }
36 | }
37 |
38 | info(message: string, ...args: any[]) {
39 | if (this.shouldLog('info')) {
40 | console.log('[Live2D Widget][INFO]', message, ...args);
41 | }
42 | }
43 |
44 | trace(message: string, ...args: any[]) {
45 | if (this.shouldLog('trace')) {
46 | console.log('[Live2D Widget][TRACE]', message, ...args);
47 | }
48 | }
49 | }
50 |
51 | const logger = new Logger();
52 |
53 | export default logger;
54 | export { LogLevel };
55 |
--------------------------------------------------------------------------------
/src/message.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file Contains functions for displaying waifu messages.
3 | * @module message
4 | */
5 |
6 | import { randomSelection } from './utils.js';
7 |
8 | type Time = {
9 | /**
10 | * Time period, format is "HH-HH", e.g. "00-06" means from 0 to 6 o'clock.
11 | * @type {string}
12 | */
13 | hour: string;
14 | /**
15 | * Message to display during this time period.
16 | * @type {string}
17 | */
18 | text: string;
19 | }[];
20 |
21 | let messageTimer: NodeJS.Timeout | null = null;
22 |
23 | /**
24 | * Display waifu message.
25 | * @param {string | string[]} text - Message text or array of texts.
26 | * @param {number} timeout - Timeout for message display (ms).
27 | * @param {number} priority - Priority of the message.
28 | * @param {boolean} [override=true] - Whether to override existing message.
29 | */
30 | function showMessage(
31 | text: string | string[],
32 | timeout: number,
33 | priority: number,
34 | override: boolean = true
35 | ) {
36 | let currentPriority = parseInt(sessionStorage.getItem('waifu-message-priority'), 10);
37 | if (isNaN(currentPriority)) {
38 | currentPriority = 0;
39 | }
40 | if (
41 | !text ||
42 | (override && currentPriority > priority) ||
43 | (!override && currentPriority >= priority)
44 | )
45 | return;
46 | if (messageTimer) {
47 | clearTimeout(messageTimer);
48 | messageTimer = null;
49 | }
50 | text = randomSelection(text) as string;
51 | sessionStorage.setItem('waifu-message-priority', String(priority));
52 | const tips = document.getElementById('waifu-tips')!;
53 | tips.innerHTML = text;
54 | tips.classList.add('waifu-tips-active');
55 | messageTimer = setTimeout(() => {
56 | sessionStorage.removeItem('waifu-message-priority');
57 | tips.classList.remove('waifu-tips-active');
58 | }, timeout);
59 | }
60 |
61 | /**
62 | * Show welcome message based on time.
63 | * @param {Time} time - Time message configuration.
64 | * @returns {string} Welcome message.
65 | */
66 | function welcomeMessage(time: Time, welcomeTemplate: string, referrerTemplate: string): string {
67 | if (location.pathname === '/') {
68 | // If on the homepage
69 | for (const { hour, text } of time) {
70 | const now = new Date(),
71 | after = hour.split('-')[0],
72 | before = hour.split('-')[1] || after;
73 | if (
74 | Number(after) <= now.getHours() &&
75 | now.getHours() <= Number(before)
76 | ) {
77 | return text;
78 | }
79 | }
80 | }
81 | const text = i18n(welcomeTemplate, document.title);
82 | if (document.referrer !== '') {
83 | const referrer = new URL(document.referrer);
84 | if (location.hostname === referrer.hostname) return text;
85 | return `${i18n(referrerTemplate, referrer.hostname)}
${text}`;
86 | }
87 | return text;
88 | }
89 |
90 | function i18n(template: string, ...args: string[]) {
91 | return template.replace(/\$(\d+)/g, (_, idx) => {
92 | const i = parseInt(idx, 10) - 1;
93 | return args[i] ?? '';
94 | });
95 | }
96 |
97 | export { showMessage, welcomeMessage, i18n, Time };
98 |
--------------------------------------------------------------------------------
/src/model.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file Contains classes related to waifu model loading and management.
3 | * @module model
4 | */
5 |
6 | import { showMessage } from './message.js';
7 | import { loadExternalResource, randomOtherOption } from './utils.js';
8 | import type Cubism2Model from './cubism2/index.js';
9 | import type { AppDelegate as Cubism5Model } from './cubism5/index.js';
10 | import logger, { LogLevel } from './logger.js';
11 |
12 | interface ModelListCDN {
13 | messages: string[];
14 | models: string | string[];
15 | }
16 |
17 | interface ModelList {
18 | name: string;
19 | paths: string[];
20 | message: string;
21 | }
22 |
23 | interface Config {
24 | /**
25 | * Path to the waifu configuration file.
26 | * @type {string}
27 | */
28 | waifuPath: string;
29 | /**
30 | * Path to the API, if you need to load models via API.
31 | * @type {string | undefined}
32 | */
33 | apiPath?: string;
34 | /**
35 | * Path to the CDN, if you need to load models via CDN.
36 | * @type {string | undefined}
37 | */
38 | cdnPath?: string;
39 | /**
40 | * Path to Cubism 2 Core, if you need to load Cubism 2 models.
41 | * @type {string | undefined}
42 | */
43 | cubism2Path?: string;
44 | /**
45 | * Path to Cubism 5 Core, if you need to load Cubism 3 and later models.
46 | * @type {string | undefined}
47 | */
48 | cubism5Path?: string;
49 | /**
50 | * Default model id.
51 | * @type {string | undefined}
52 | */
53 | modelId?: number;
54 | /**
55 | * List of tools to display.
56 | * @type {string[] | undefined}
57 | */
58 | tools?: string[];
59 | /**
60 | * Support for dragging the waifu.
61 | * @type {boolean | undefined}
62 | */
63 | drag?: boolean;
64 | /**
65 | * Log level.
66 | * @type {LogLevel | undefined}
67 | */
68 | logLevel?: LogLevel;
69 | }
70 |
71 | /**
72 | * Waifu model class, responsible for loading and managing models.
73 | */
74 | class ModelManager {
75 | public readonly useCDN: boolean;
76 | private readonly cdnPath: string;
77 | private readonly cubism2Path: string;
78 | private readonly cubism5Path: string;
79 | private _modelId: number;
80 | private _modelTexturesId: number;
81 | private modelList: ModelListCDN | null = null;
82 | private cubism2model: Cubism2Model | undefined;
83 | private cubism5model: Cubism5Model | undefined;
84 | private currentModelVersion: number;
85 | private loading: boolean;
86 | private modelJSONCache: Record;
87 | private models: ModelList[];
88 |
89 | /**
90 | * Create a Model instance.
91 | * @param {Config} config - Configuration options
92 | */
93 | private constructor(config: Config, models: ModelList[] = []) {
94 | let { apiPath, cdnPath } = config;
95 | const { cubism2Path, cubism5Path } = config;
96 | let useCDN = false;
97 | if (typeof cdnPath === 'string') {
98 | if (!cdnPath.endsWith('/')) cdnPath += '/';
99 | useCDN = true;
100 | } else if (typeof apiPath === 'string') {
101 | if (!apiPath.endsWith('/')) apiPath += '/';
102 | cdnPath = apiPath;
103 | useCDN = true;
104 | logger.warn('apiPath option is deprecated. Please use cdnPath instead.');
105 | } else if (!models.length) {
106 | throw 'Invalid initWidget argument!';
107 | }
108 | let modelId: number = parseInt(localStorage.getItem('modelId') as string, 10);
109 | let modelTexturesId: number = parseInt(
110 | localStorage.getItem('modelTexturesId') as string, 10
111 | );
112 | if (isNaN(modelId) || isNaN(modelTexturesId)) {
113 | modelTexturesId = 0;
114 | }
115 | if (isNaN(modelId)) {
116 | modelId = config.modelId ?? 0;
117 | }
118 | this.useCDN = useCDN;
119 | this.cdnPath = cdnPath || '';
120 | this.cubism2Path = cubism2Path || '';
121 | this.cubism5Path = cubism5Path || '';
122 | this._modelId = modelId;
123 | this._modelTexturesId = modelTexturesId;
124 | this.currentModelVersion = 0;
125 | this.loading = false;
126 | this.modelJSONCache = {};
127 | this.models = models;
128 | }
129 |
130 | public static async initCheck(config: Config, models: ModelList[] = []) {
131 | const model = new ModelManager(config, models);
132 | if (model.useCDN) {
133 | const response = await fetch(`${model.cdnPath}model_list.json`);
134 | model.modelList = await response.json();
135 | if (model.modelId >= model.modelList.models.length) {
136 | model.modelId = 0;
137 | }
138 | const modelName = model.modelList.models[model.modelId];
139 | if (Array.isArray(modelName)) {
140 | if (model.modelTexturesId >= modelName.length) {
141 | model.modelTexturesId = 0;
142 | }
143 | } else {
144 | const modelSettingPath = `${model.cdnPath}model/${modelName}/index.json`;
145 | const modelSetting = await model.fetchWithCache(modelSettingPath);
146 | const version = model.checkModelVersion(modelSetting);
147 | if (version === 2) {
148 | const textureCache = await model.loadTextureCache(modelName);
149 | if (model.modelTexturesId >= textureCache.length) {
150 | model.modelTexturesId = 0;
151 | }
152 | }
153 | }
154 | } else {
155 | if (model.modelId >= model.models.length) {
156 | model.modelId = 0;
157 | }
158 | if (model.modelTexturesId >= model.models[model.modelId].paths.length) {
159 | model.modelTexturesId = 0;
160 | }
161 | }
162 | return model;
163 | }
164 |
165 | public set modelId(modelId: number) {
166 | this._modelId = modelId;
167 | localStorage.setItem('modelId', modelId.toString());
168 | }
169 |
170 | public get modelId() {
171 | return this._modelId;
172 | }
173 |
174 | public set modelTexturesId(modelTexturesId: number) {
175 | this._modelTexturesId = modelTexturesId;
176 | localStorage.setItem('modelTexturesId', modelTexturesId.toString());
177 | }
178 |
179 | public get modelTexturesId() {
180 | return this._modelTexturesId;
181 | }
182 |
183 | resetCanvas() {
184 | document.getElementById('waifu-canvas').innerHTML = '';
185 | }
186 |
187 | async fetchWithCache(url: string) {
188 | let result;
189 | if (url in this.modelJSONCache) {
190 | result = this.modelJSONCache[url];
191 | } else {
192 | try {
193 | const response = await fetch(url);
194 | result = await response.json();
195 | } catch {
196 | result = null;
197 | }
198 | this.modelJSONCache[url] = result;
199 | }
200 | return result;
201 | }
202 |
203 | checkModelVersion(modelSetting: any) {
204 | if (modelSetting.Version === 3 || modelSetting.FileReferences) {
205 | return 3;
206 | }
207 | return 2;
208 | }
209 |
210 | async loadLive2D(modelSettingPath: string, modelSetting: object) {
211 | if (this.loading) {
212 | logger.warn('Still loading. Abort.');
213 | return;
214 | }
215 | this.loading = true;
216 | try {
217 | const version = this.checkModelVersion(modelSetting);
218 | if (version === 2) {
219 | if (!this.cubism2model) {
220 | if (!this.cubism2Path) {
221 | logger.error('No cubism2Path set, cannot load Cubism 2 Core.')
222 | return;
223 | }
224 | await loadExternalResource(this.cubism2Path, 'js');
225 | const { default: Cubism2Model } = await import('./cubism2/index.js');
226 | this.cubism2model = new Cubism2Model();
227 | }
228 | if (this.currentModelVersion === 3) {
229 | (this.cubism5model as any).release();
230 | // Recycle WebGL resources
231 | this.resetCanvas();
232 | }
233 | if (this.currentModelVersion === 3 || !this.cubism2model.gl) {
234 | await this.cubism2model.init('live2d', modelSettingPath, modelSetting);
235 | } else {
236 | await this.cubism2model.changeModelWithJSON(modelSettingPath, modelSetting);
237 | }
238 | } else {
239 | if (!this.cubism5Path) {
240 | logger.error('No cubism5Path set, cannot load Cubism 5 Core.')
241 | return;
242 | }
243 | await loadExternalResource(this.cubism5Path, 'js');
244 | const { AppDelegate: Cubism5Model } = await import('./cubism5/index.js');
245 | this.cubism5model = new (Cubism5Model as any)();
246 | if (this.currentModelVersion === 2) {
247 | this.cubism2model.destroy();
248 | // Recycle WebGL resources
249 | this.resetCanvas();
250 | }
251 | if (this.currentModelVersion === 2 || !this.cubism5model.subdelegates.at(0)) {
252 | this.cubism5model.initialize();
253 | this.cubism5model.changeModel(modelSettingPath);
254 | this.cubism5model.run();
255 | } else {
256 | this.cubism5model.changeModel(modelSettingPath);
257 | }
258 | }
259 | logger.info(`Model ${modelSettingPath} (Cubism version ${version}) loaded`);
260 | this.currentModelVersion = version;
261 | } catch (err) {
262 | console.error('loadLive2D failed', err);
263 | }
264 | this.loading = false;
265 | }
266 |
267 | async loadTextureCache(modelName: string): Promise {
268 | const textureCache = await this.fetchWithCache(`${this.cdnPath}model/${modelName}/textures.cache`);
269 | return textureCache || [];
270 | }
271 |
272 | /**
273 | * Load the specified model.
274 | * @param {string | string[]} message - Loading message.
275 | */
276 | async loadModel(message: string | string[]) {
277 | let modelSettingPath, modelSetting;
278 | if (this.useCDN) {
279 | let modelName = this.modelList.models[this.modelId];
280 | if (Array.isArray(modelName)) {
281 | modelName = modelName[this.modelTexturesId];
282 | }
283 | modelSettingPath = `${this.cdnPath}model/${modelName}/index.json`;
284 | modelSetting = await this.fetchWithCache(modelSettingPath);
285 | const version = this.checkModelVersion(modelSetting);
286 | if (version === 2) {
287 | const textureCache = await this.loadTextureCache(modelName);
288 | let textures = textureCache[this.modelTexturesId];
289 | if (typeof textures === 'string') textures = [textures];
290 | modelSetting.textures = textures;
291 | }
292 | } else {
293 | modelSettingPath = this.models[this.modelId].paths[this.modelTexturesId];
294 | modelSetting = await this.fetchWithCache(modelSettingPath);
295 | }
296 | await this.loadLive2D(modelSettingPath, modelSetting);
297 | showMessage(message, 4000, 10);
298 | }
299 |
300 | /**
301 | * Load a random texture for the current model.
302 | */
303 | async loadRandTexture(successMessage: string | string[] = '', failMessage: string | string[] = '') {
304 | const { modelId } = this;
305 | let noTextureAvailable = false;
306 | if (this.useCDN) {
307 | const modelName = this.modelList.models[modelId];
308 | if (Array.isArray(modelName)) {
309 | this.modelTexturesId = randomOtherOption(modelName.length, this.modelTexturesId);
310 | } else {
311 | const modelSettingPath = `${this.cdnPath}model/${modelName}/index.json`;
312 | const modelSetting = await this.fetchWithCache(modelSettingPath);
313 | const version = this.checkModelVersion(modelSetting);
314 | if (version === 2) {
315 | const textureCache = await this.loadTextureCache(modelName);
316 | if (textureCache.length <= 1) {
317 | noTextureAvailable = true;
318 | } else {
319 | this.modelTexturesId = randomOtherOption(textureCache.length, this.modelTexturesId);
320 | }
321 | } else {
322 | noTextureAvailable = true;
323 | }
324 | }
325 | } else {
326 | if (this.models[modelId].paths.length === 1) {
327 | noTextureAvailable = true;
328 | } else {
329 | this.modelTexturesId = randomOtherOption(this.models[modelId].paths.length, this.modelTexturesId);
330 | }
331 | }
332 | if (noTextureAvailable) {
333 | showMessage(failMessage, 4000, 10);
334 | } else {
335 | await this.loadModel(successMessage);
336 | }
337 | }
338 |
339 | /**
340 | * Load the next character's model.
341 | */
342 | async loadNextModel() {
343 | this.modelTexturesId = 0;
344 | if (this.useCDN) {
345 | this.modelId = (this.modelId + 1) % this.modelList.models.length;
346 | await this.loadModel(this.modelList.messages[this.modelId]);
347 | } else {
348 | this.modelId = (this.modelId + 1) % this.models.length;
349 | await this.loadModel(this.models[this.modelId].message);
350 | }
351 | }
352 | }
353 |
354 | export { ModelManager, Config, ModelList };
355 |
--------------------------------------------------------------------------------
/src/tools.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file Contains the configuration and functions for waifu tools.
3 | * @module tools
4 | */
5 |
6 | import {
7 | fa_comment,
8 | fa_paper_plane,
9 | fa_street_view,
10 | fa_shirt,
11 | fa_camera_retro,
12 | fa_info_circle,
13 | fa_xmark
14 | } from './icons.js';
15 | import { showMessage, i18n } from './message.js';
16 | import type { Config, ModelManager } from './model.js';
17 | import type { Tips } from './widget.js';
18 |
19 | interface Tools {
20 | /**
21 | * Key-value pairs of tools, where the key is the tool name.
22 | * @type {string}
23 | */
24 | [key: string]: {
25 | /**
26 | * Icon of the tool, usually an SVG string.
27 | * @type {string}
28 | */
29 | icon: string;
30 | /**
31 | * Callback function for the tool.
32 | * @type {() => void}
33 | */
34 | callback: (message: any) => void;
35 | };
36 | }
37 |
38 | /**
39 | * Waifu tools manager.
40 | */
41 | class ToolsManager {
42 | tools: Tools;
43 | config: Config;
44 |
45 | constructor(model: ModelManager, config: Config, tips: Tips) {
46 | this.config = config;
47 | this.tools = {
48 | hitokoto: {
49 | icon: fa_comment,
50 | callback: async () => {
51 | // Add hitokoto.cn API
52 | const response = await fetch('https://v1.hitokoto.cn');
53 | const result = await response.json();
54 | const template = tips.message.hitokoto;
55 | const text = i18n(template, result.from, result.creator);
56 | showMessage(result.hitokoto, 6000, 9);
57 | setTimeout(() => {
58 | showMessage(text, 4000, 9);
59 | }, 6000);
60 | }
61 | },
62 | asteroids: {
63 | icon: fa_paper_plane,
64 | callback: () => {
65 | if (window.Asteroids) {
66 | if (!window.ASTEROIDSPLAYERS) window.ASTEROIDSPLAYERS = [];
67 | window.ASTEROIDSPLAYERS.push(new window.Asteroids());
68 | } else {
69 | const script = document.createElement('script');
70 | script.src =
71 | 'https://fastly.jsdelivr.net/gh/stevenjoezhang/asteroids/asteroids.js';
72 | document.head.appendChild(script);
73 | }
74 | }
75 | },
76 | 'switch-model': {
77 | icon: fa_street_view,
78 | callback: () => model.loadNextModel()
79 | },
80 | 'switch-texture': {
81 | icon: fa_shirt,
82 | callback: () => {
83 | let successMessage = '', failMessage = '';
84 | if (tips) {
85 | successMessage = tips.message.changeSuccess;
86 | failMessage = tips.message.changeFail;
87 | }
88 | model.loadRandTexture(successMessage, failMessage);
89 | }
90 | },
91 | photo: {
92 | icon: fa_camera_retro,
93 | callback: () => {
94 | const message = tips.message.photo;
95 | showMessage(message, 6000, 9);
96 | const canvas = document.getElementById('live2d') as HTMLCanvasElement;
97 | if (!canvas) return;
98 | const imageUrl = canvas.toDataURL();
99 |
100 | const link = document.createElement('a');
101 | link.style.display = 'none';
102 | link.href = imageUrl;
103 | link.download = 'live2d-photo.png';
104 |
105 | document.body.appendChild(link);
106 | link.click();
107 | document.body.removeChild(link);
108 | }
109 | },
110 | info: {
111 | icon: fa_info_circle,
112 | callback: () => {
113 | open('https://github.com/stevenjoezhang/live2d-widget');
114 | }
115 | },
116 | quit: {
117 | icon: fa_xmark,
118 | callback: () => {
119 | localStorage.setItem('waifu-display', Date.now().toString());
120 | const message = tips.message.goodbye;
121 | showMessage(message, 2000, 11);
122 | const waifu = document.getElementById('waifu');
123 | if (!waifu) return;
124 | waifu.classList.remove('waifu-active');
125 | setTimeout(() => {
126 | waifu.classList.add('waifu-hidden');
127 | const waifuToggle = document.getElementById('waifu-toggle');
128 | waifuToggle?.classList.add('waifu-toggle-active');
129 | }, 3000);
130 | }
131 | }
132 | };
133 | }
134 |
135 | registerTools() {
136 | if (!Array.isArray(this.config.tools)) {
137 | this.config.tools = Object.keys(this.tools);
138 | }
139 | for (const toolName of this.config.tools) {
140 | if (this.tools[toolName]) {
141 | const { icon, callback } = this.tools[toolName];
142 | const element = document.createElement('span');
143 | element.id = `waifu-tool-${toolName}`;
144 | element.innerHTML = icon;
145 | document
146 | .getElementById('waifu-tool')
147 | ?.insertAdjacentElement(
148 | 'beforeend',
149 | element,
150 | );
151 | element.addEventListener('click', callback);
152 | }
153 | }
154 | }
155 | }
156 |
157 | export { ToolsManager, Tools };
158 |
--------------------------------------------------------------------------------
/src/types/index.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file Export all type definitions.
3 | * @module types/index
4 | */
5 | export * from './live2dApi';
6 | export * from './window';
7 |
--------------------------------------------------------------------------------
/src/types/live2dApi.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file Define types for Live2D API.
3 | * @module types/live2dApi
4 | */
5 | declare namespace Live2D {
6 | /**
7 | * Initialize the Live2D runtime environment.
8 | */
9 | export function init(): void;
10 | /**
11 | * Set the WebGL context
12 | * @param gl WebGL rendering context
13 | */
14 | export function setGL(gl: WebGLRenderingContext): void;
15 | }
16 |
17 | /**
18 | * Static class related to Live2D models.
19 | */
20 | declare class Live2DModelWebGL {
21 | /**
22 | * Load a Live2D model from a binary buffer
23 | * @param buf ArrayBuffer data of the model file
24 | */
25 | static loadModel(buf: ArrayBuffer): Live2DModelWebGL;
26 |
27 | /**
28 | * Bind a texture to the model
29 | * @param index Texture index
30 | * @param texture WebGL texture object
31 | */
32 | setTexture(index: number, texture: WebGLTexture): void;
33 |
34 | /**
35 | * Return the canvas width of the model
36 | */
37 | getCanvasWidth(): number;
38 |
39 | /**
40 | * Set the transformation matrix of the model
41 | * @param matrix 4x4 matrix array
42 | */
43 | setMatrix(matrix: number[]): void;
44 |
45 | /**
46 | * Set parameter values (e.g., animation parameters)
47 | * @param paramName Parameter name
48 | * @param value Parameter value
49 | */
50 | setParamFloat(paramName: string, value: number): void;
51 |
52 | /**
53 | * Refresh the internal data of the model
54 | */
55 | update(): void;
56 |
57 | /**
58 | * Draw the current frame
59 | */
60 | draw(): void;
61 |
62 | /**
63 | * Whether the current mode is premultiplied alpha
64 | */
65 | isPremultipliedAlpha?(): boolean;
66 | }
67 |
--------------------------------------------------------------------------------
/src/types/window.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file Define the type of the global window object.
3 | * @module types/window
4 | */
5 | interface Window {
6 | /**
7 | * Asteroids game class.
8 | * @type {any}
9 | */
10 | Asteroids: any;
11 | /**
12 | * Asteroids game player array.
13 | * @type {any[]}
14 | */
15 | ASTEROIDSPLAYERS: any[];
16 | /**
17 | * Function to initialize the Live2D widget.
18 | * @type {(config: Config) => void}
19 | */
20 | initWidget: (config: Config) => void;
21 | }
22 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file Contains utility functions.
3 | * @module utils
4 | */
5 |
6 | /**
7 | * Randomly select an element from an array, or return the original value if not an array.
8 | * @param {string[] | string} obj - The object or array to select from.
9 | * @returns {string} The randomly selected element or the original value.
10 | */
11 | function randomSelection(obj: string[] | string): string {
12 | return Array.isArray(obj) ? obj[Math.floor(Math.random() * obj.length)] : obj;
13 | }
14 |
15 | function randomOtherOption(total: number, excludeIndex: number): number {
16 | const idx = Math.floor(Math.random() * (total - 1));
17 | return idx >= excludeIndex ? idx + 1 : idx;
18 | }
19 |
20 | /**
21 | * Asynchronously load external resources.
22 | * @param {string} url - Resource path.
23 | * @param {string} type - Resource type.
24 | */
25 | function loadExternalResource(url: string, type: string): Promise {
26 | return new Promise((resolve: any, reject: any) => {
27 | let tag;
28 |
29 | if (type === 'css') {
30 | tag = document.createElement('link');
31 | tag.rel = 'stylesheet';
32 | tag.href = url;
33 | }
34 | else if (type === 'js') {
35 | tag = document.createElement('script');
36 | tag.src = url;
37 | }
38 | if (tag) {
39 | tag.onload = () => resolve(url);
40 | tag.onerror = () => reject(url);
41 | document.head.appendChild(tag);
42 | }
43 | });
44 | }
45 |
46 | export { randomSelection, loadExternalResource, randomOtherOption };
47 |
--------------------------------------------------------------------------------
/src/waifu-tips.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file Export initWidget function to window.
3 | * @module waifu-tips
4 | */
5 |
6 | import { initWidget } from './widget.js';
7 |
8 | window.initWidget = initWidget;
9 |
--------------------------------------------------------------------------------
/src/widget.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file Contains functions for initializing the waifu widget.
3 | * @module widget
4 | */
5 |
6 | import { ModelManager, Config, ModelList } from './model.js';
7 | import { showMessage, welcomeMessage, Time } from './message.js';
8 | import { randomSelection } from './utils.js';
9 | import { ToolsManager } from './tools.js';
10 | import logger from './logger.js';
11 | import registerDrag from './drag.js';
12 | import { fa_child } from './icons.js';
13 |
14 | interface Tips {
15 | /**
16 | * Default message configuration.
17 | */
18 | message: {
19 | /**
20 | * Default message array.
21 | * @type {string[]}
22 | */
23 | default: string[];
24 | /**
25 | * Console message.
26 | * @type {string}
27 | */
28 | console: string;
29 | /**
30 | * Copy message.
31 | * @type {string}
32 | */
33 | copy: string;
34 | /**
35 | * Visibility change message.
36 | * @type {string}
37 | */
38 | visibilitychange: string;
39 | changeSuccess: string;
40 | changeFail: string;
41 | photo: string;
42 | goodbye: string;
43 | hitokoto: string;
44 | welcome: string;
45 | referrer: string;
46 | hoverBody: string;
47 | tapBody: string;
48 | };
49 | /**
50 | * Time configuration.
51 | * @type {Time}
52 | */
53 | time: Time;
54 | /**
55 | * Mouseover message configuration.
56 | * @type {Array<{selector: string, text: string | string[]}>}
57 | */
58 | mouseover: {
59 | selector: string;
60 | text: string | string[];
61 | }[];
62 | /**
63 | * Click message configuration.
64 | * @type {Array<{selector: string, text: string | string[]}>}
65 | */
66 | click: {
67 | selector: string;
68 | text: string | string[];
69 | }[];
70 | /**
71 | * Season message configuration.
72 | * @type {Array<{date: string, text: string | string[]}>}
73 | */
74 | seasons: {
75 | date: string;
76 | text: string | string[];
77 | }[];
78 | models: ModelList[];
79 | }
80 |
81 | /**
82 | * Register event listeners.
83 | * @param {Tips} tips - Result configuration.
84 | */
85 | function registerEventListener(tips: Tips) {
86 | // Detect user activity and display messages when idle
87 | let userAction = false;
88 | let userActionTimer: any;
89 | const messageArray = tips.message.default;
90 | tips.seasons.forEach(({ date, text }) => {
91 | const now = new Date(),
92 | after = date.split('-')[0],
93 | before = date.split('-')[1] || after;
94 | if (
95 | Number(after.split('/')[0]) <= now.getMonth() + 1 &&
96 | now.getMonth() + 1 <= Number(before.split('/')[0]) &&
97 | Number(after.split('/')[1]) <= now.getDate() &&
98 | now.getDate() <= Number(before.split('/')[1])
99 | ) {
100 | text = randomSelection(text);
101 | text = (text as string).replace('{year}', String(now.getFullYear()));
102 | messageArray.push(text);
103 | }
104 | });
105 | let lastHoverElement: any;
106 | window.addEventListener('mousemove', () => (userAction = true));
107 | window.addEventListener('keydown', () => (userAction = true));
108 | setInterval(() => {
109 | if (userAction) {
110 | userAction = false;
111 | clearInterval(userActionTimer);
112 | userActionTimer = null;
113 | } else if (!userActionTimer) {
114 | userActionTimer = setInterval(() => {
115 | showMessage(messageArray, 6000, 9);
116 | }, 20000);
117 | }
118 | }, 1000);
119 |
120 | window.addEventListener('mouseover', (event) => {
121 | // eslint-disable-next-line prefer-const
122 | for (let { selector, text } of tips.mouseover) {
123 | if (!(event.target as HTMLElement)?.closest(selector)) continue;
124 | if (lastHoverElement === selector) return;
125 | lastHoverElement = selector;
126 | text = randomSelection(text);
127 | text = (text as string).replace(
128 | '{text}',
129 | (event.target as HTMLElement).innerText,
130 | );
131 | showMessage(text, 4000, 8);
132 | return;
133 | }
134 | });
135 | window.addEventListener('click', (event) => {
136 | // eslint-disable-next-line prefer-const
137 | for (let { selector, text } of tips.click) {
138 | if (!(event.target as HTMLElement)?.closest(selector)) continue;
139 | text = randomSelection(text);
140 | text = (text as string).replace(
141 | '{text}',
142 | (event.target as HTMLElement).innerText,
143 | );
144 | showMessage(text, 4000, 8);
145 | return;
146 | }
147 | });
148 | window.addEventListener('live2d:hoverbody', () => {
149 | const text = randomSelection(tips.message.hoverBody);
150 | showMessage(text, 4000, 8, false);
151 | });
152 | window.addEventListener('live2d:tapbody', () => {
153 | const text = randomSelection(tips.message.tapBody);
154 | showMessage(text, 4000, 9);
155 | });
156 |
157 | const devtools = () => {};
158 | console.log('%c', devtools);
159 | devtools.toString = () => {
160 | showMessage(tips.message.console, 6000, 9);
161 | };
162 | window.addEventListener('copy', () => {
163 | showMessage(tips.message.copy, 6000, 9);
164 | });
165 | window.addEventListener('visibilitychange', () => {
166 | if (!document.hidden)
167 | showMessage(tips.message.visibilitychange, 6000, 9);
168 | });
169 | }
170 |
171 | /**
172 | * Load the waifu widget.
173 | * @param {Config} config - Waifu configuration.
174 | */
175 | async function loadWidget(config: Config) {
176 | localStorage.removeItem('waifu-display');
177 | sessionStorage.removeItem('waifu-message-priority');
178 | document.body.insertAdjacentHTML(
179 | 'beforeend',
180 | `
181 |
182 |
183 |
184 |
185 |
186 |
`,
187 | );
188 | let models: ModelList[] = [];
189 | let tips: Tips | null;
190 | if (config.waifuPath) {
191 | const response = await fetch(config.waifuPath);
192 | tips = await response.json();
193 | models = tips.models;
194 | registerEventListener(tips);
195 | showMessage(welcomeMessage(tips.time, tips.message.welcome, tips.message.referrer), 7000, 11);
196 | }
197 | const model = await ModelManager.initCheck(config, models);
198 | await model.loadModel('');
199 | new ToolsManager(model, config, tips).registerTools();
200 | if (config.drag) registerDrag();
201 | document.getElementById('waifu')?.classList.add('waifu-active');
202 | }
203 |
204 | /**
205 | * Initialize the waifu widget.
206 | * @param {string | Config} config - Waifu configuration or configuration path.
207 | */
208 | function initWidget(config: string | Config) {
209 | if (typeof config === 'string') {
210 | logger.error('Your config for Live2D initWidget is outdated. Please refer to https://github.com/stevenjoezhang/live2d-widget/blob/master/dist/autoload.js');
211 | return;
212 | }
213 | logger.setLevel(config.logLevel);
214 | document.body.insertAdjacentHTML(
215 | 'beforeend',
216 | `
217 | ${fa_child}
218 |
`,
219 | );
220 | const toggle = document.getElementById('waifu-toggle');
221 | toggle?.addEventListener('click', () => {
222 | toggle?.classList.remove('waifu-toggle-active');
223 | if (toggle?.getAttribute('first-time')) {
224 | loadWidget(config as Config);
225 | toggle?.removeAttribute('first-time');
226 | } else {
227 | localStorage.removeItem('waifu-display');
228 | document.getElementById('waifu')?.classList.remove('waifu-hidden');
229 | setTimeout(() => {
230 | document.getElementById('waifu')?.classList.add('waifu-active');
231 | }, 0);
232 | }
233 | });
234 | if (
235 | localStorage.getItem('waifu-display') &&
236 | Date.now() - Number(localStorage.getItem('waifu-display')) <= 86400000
237 | ) {
238 | toggle?.setAttribute('first-time', 'true');
239 | setTimeout(() => {
240 | toggle?.classList.add('waifu-toggle-active');
241 | }, 0);
242 | } else {
243 | loadWidget(config as Config);
244 | }
245 | }
246 |
247 | export { initWidget, Tips };
248 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | "module": "esnext",
5 | "moduleResolution": "node",
6 | "declaration": true,
7 | "outDir": "build",
8 | "esModuleInterop": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "strict": false,
11 | "skipLibCheck": true,
12 | "removeComments": true,
13 | "allowJs": true,
14 | "paths": {
15 | "@framework/*": [
16 | "./src/CubismSdkForWeb-5-r.4/Framework/src/*"
17 | ],
18 | "@demo/*": [
19 | "./src/CubismSdkForWeb-5-r.4/Samples/TypeScript/Demo/src/*"
20 | ]
21 | }
22 | },
23 | "include": ["src"],
24 | "exclude": ["node_modules", "build", "dist"]
25 | }
26 |
--------------------------------------------------------------------------------