├── .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 | ![](https://forthebadge.com/images/badges/built-with-love.svg) 4 | ![](https://forthebadge.com/images/badges/made-with-typescript.svg) 5 | ![](https://forthebadge.com/images/badges/uses-css.svg) 6 | ![](https://forthebadge.com/images/badges/contains-cat-gifs.svg) 7 | ![](https://forthebadge.com/images/badges/powered-by-electricity.svg) 8 | ![](https://forthebadge.com/images/badges/makes-people-smile.svg) 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 | BrowserStack Logo 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 | jsDelivr Logo 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 | --------------------------------------------------------------------------------