├── .prettierignore ├── demo ├── screenshots │ ├── screenshot-1.png │ ├── screenshot-2.png │ └── screenshot-3.png ├── demo.html ├── chat.html └── login.html ├── .prettierrc.yaml ├── renovate.json ├── .gitignore ├── src ├── types │ ├── index.d.ts │ ├── window.d.ts │ └── live2dApi.d.ts ├── waifu-tips.ts ├── index.ts ├── cubism2 │ ├── LAppDefine.js │ ├── utils │ │ ├── MatrixStack.js │ │ └── ModelSettingJson.js │ ├── LAppLive2DManager.js │ ├── PlatformManager.js │ ├── index.js │ └── LAppModel.js ├── logger.ts ├── utils.ts ├── drag.ts ├── message.ts ├── tools.ts ├── icons.ts ├── widget.ts ├── cubism5 │ └── index.js └── model.ts ├── .github └── workflows │ ├── linter.yml │ └── tester.yml ├── eslint.config.js ├── tsconfig.json ├── rollup.config.js ├── package.json ├── dist ├── autoload.js ├── waifu.css ├── waifu-tips.json ├── waifu-tips.js └── chunk │ └── index.js ├── README.md └── README.en.md /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /demo/screenshots/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenjoezhang/live2d-widget/HEAD/demo/screenshots/screenshot-1.png -------------------------------------------------------------------------------- /demo/screenshots/screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenjoezhang/live2d-widget/HEAD/demo/screenshots/screenshot-2.png -------------------------------------------------------------------------------- /demo/screenshots/screenshot-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenjoezhang/live2d-widget/HEAD/demo/screenshots/screenshot-3.png -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | tabWidth: 2 2 | semi: true 3 | singleQuote: true 4 | jsxSingleQuote: true 5 | trailingComma: all 6 | endOfLine: crlf 7 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | node_modules/ 3 | .DS_Store 4 | .idea/ 5 | .vscode/ 6 | backup/ 7 | src/CubismSdkForWeb-*/ 8 | build/CubismSdkForWeb-*/ 9 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /.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@v5 10 | - name: Use Node.js 11 | uses: actions/setup-node@v6 12 | - name: Install Dependencies 13 | run: npm install 14 | - run: npm run eslint 15 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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@v5 14 | with: 15 | repository: hexojs/hexo-starter 16 | - name: Use Node.js 17 | uses: actions/setup-node@v6 18 | - name: Install Dependencies 19 | run: npm install 20 | - name: Test 21 | run: npm run build 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 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 | -------------------------------------------------------------------------------- /src/cubism2/LAppDefine.js: -------------------------------------------------------------------------------- 1 | const LAppDefine = { 2 | VIEW_MAX_SCALE: 1.5, 3 | VIEW_MIN_SCALE: 1, 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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "live2d-widgets", 3 | "version": "1.0.0", 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 | "prepublishOnly": "npm run build" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/stevenjoezhang/live2d-widget.git" 26 | }, 27 | "keywords": [ 28 | "Live2D", 29 | "WebGL" 30 | ], 31 | "author": "stevenjoezhang ", 32 | "license": "GPL-3.0-or-later", 33 | "bugs": { 34 | "url": "https://github.com/stevenjoezhang/live2d-widget/issues" 35 | }, 36 | "homepage": "https://github.com/stevenjoezhang/live2d-widget#readme", 37 | "dependencies": { 38 | "@fortawesome/fontawesome-free": "6.7.2" 39 | }, 40 | "devDependencies": { 41 | "@eslint/js": "9.38.0", 42 | "@rollup/plugin-alias": "5.1.1", 43 | "@rollup/plugin-terser": "0.4.4", 44 | "@types/node": "22.18.11", 45 | "eslint": "9.38.0", 46 | "rollup": "4.52.5", 47 | "typescript": "5.9.3", 48 | "typescript-eslint": "8.46.1" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /demo/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Live2D 看板娘 / Demo 6 | 23 | 24 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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.6/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 | -------------------------------------------------------------------------------- /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 | * @param {string} [welcomeTemplate] - Welcome message template. 65 | * @param {string} [referrerTemplate] - Referrer message template. 66 | * @returns {string} Welcome message. 67 | */ 68 | function welcomeMessage(time: Time, welcomeTemplate?: string, referrerTemplate?: string): string { 69 | if (location.pathname === '/') { 70 | // If on the homepage 71 | for (const { hour, text } of time) { 72 | const now = new Date(), 73 | after = hour.split('-')[0], 74 | before = hour.split('-')[1] || after; 75 | if ( 76 | Number(after) <= now.getHours() && 77 | now.getHours() <= Number(before) 78 | ) { 79 | return text; 80 | } 81 | } 82 | } 83 | if (!welcomeTemplate) return ''; 84 | const text = i18n(welcomeTemplate, document.title); 85 | if (document.referrer === '' || !referrerTemplate) return text; 86 | 87 | const referrer = new URL(document.referrer); 88 | if (location.hostname === referrer.hostname) return text; 89 | return `${i18n(referrerTemplate, referrer.hostname)}
${text}`; 90 | } 91 | 92 | function i18n(template: string, ...args: string[]) { 93 | return template.replace(/\$(\d+)/g, (_, idx) => { 94 | const i = parseInt(idx, 10) - 1; 95 | return args[i] ?? ''; 96 | }); 97 | } 98 | 99 | export { showMessage, welcomeMessage, i18n, Time }; 100 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /demo/chat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 看板娘聊天平台 7 | 8 | 9 | 10 | 80 | 81 | 82 |
83 |
84 | 85 |
86 | 234 | 235 | 236 | -------------------------------------------------------------------------------- /demo/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 看板娘登陆平台 7 | 8 | 9 | 10 | 121 | 122 | 123 | 150 | 252 | 253 | 254 | -------------------------------------------------------------------------------- /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 | string[]; 47 | tapBody: string | 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 = this.canvas.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(0.9); 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | // this.loadTextureCache may return an empty array 289 | if (textureCache.length > 0) { 290 | let textures = textureCache[this.modelTexturesId]; 291 | if (typeof textures === 'string') textures = [textures]; 292 | modelSetting.textures = textures; 293 | } 294 | } 295 | } else { 296 | modelSettingPath = this.models[this.modelId].paths[this.modelTexturesId]; 297 | modelSetting = await this.fetchWithCache(modelSettingPath); 298 | } 299 | await this.loadLive2D(modelSettingPath, modelSetting); 300 | showMessage(message, 4000, 10); 301 | } 302 | 303 | /** 304 | * Load a random texture for the current model. 305 | */ 306 | async loadRandTexture(successMessage: string | string[] = '', failMessage: string | string[] = '') { 307 | const { modelId } = this; 308 | let noTextureAvailable = false; 309 | if (this.useCDN) { 310 | const modelName = this.modelList.models[modelId]; 311 | if (Array.isArray(modelName)) { 312 | this.modelTexturesId = randomOtherOption(modelName.length, this.modelTexturesId); 313 | } else { 314 | const modelSettingPath = `${this.cdnPath}model/${modelName}/index.json`; 315 | const modelSetting = await this.fetchWithCache(modelSettingPath); 316 | const version = this.checkModelVersion(modelSetting); 317 | if (version === 2) { 318 | const textureCache = await this.loadTextureCache(modelName); 319 | if (textureCache.length <= 1) { 320 | noTextureAvailable = true; 321 | } else { 322 | this.modelTexturesId = randomOtherOption(textureCache.length, this.modelTexturesId); 323 | } 324 | } else { 325 | noTextureAvailable = true; 326 | } 327 | } 328 | } else { 329 | if (this.models[modelId].paths.length === 1) { 330 | noTextureAvailable = true; 331 | } else { 332 | this.modelTexturesId = randomOtherOption(this.models[modelId].paths.length, this.modelTexturesId); 333 | } 334 | } 335 | if (noTextureAvailable) { 336 | showMessage(failMessage, 4000, 10); 337 | } else { 338 | await this.loadModel(successMessage); 339 | } 340 | } 341 | 342 | /** 343 | * Load the next character's model. 344 | */ 345 | async loadNextModel() { 346 | this.modelTexturesId = 0; 347 | if (this.useCDN) { 348 | this.modelId = (this.modelId + 1) % this.modelList.models.length; 349 | await this.loadModel(this.modelList.messages[this.modelId]); 350 | } else { 351 | this.modelId = (this.modelId + 1) % this.models.length; 352 | await this.loadModel(this.models[this.modelId].message); 353 | } 354 | } 355 | } 356 | 357 | export { ModelManager, Config, ModelList }; 358 | -------------------------------------------------------------------------------- /src/cubism2/LAppModel.js: -------------------------------------------------------------------------------- 1 | /* global UtSystem, document */ 2 | import { L2DBaseModel, Live2DFramework, L2DEyeBlink } from './Live2DFramework.js'; 3 | import ModelSettingJson from './utils/ModelSettingJson.js'; 4 | import LAppDefine from './LAppDefine.js'; 5 | import MatrixStack from './utils/MatrixStack.js'; 6 | import logger from '../logger.js'; 7 | 8 | //============================================================ 9 | //============================================================ 10 | // class LAppModel extends L2DBaseModel 11 | //============================================================ 12 | //============================================================ 13 | class LAppModel extends L2DBaseModel { 14 | constructor() { 15 | //L2DBaseModel.apply(this, arguments); 16 | super(); 17 | 18 | this.modelHomeDir = ''; 19 | this.modelSetting = null; 20 | this.tmpMatrix = []; 21 | } 22 | 23 | loadJSON(callback) { 24 | const path = this.modelHomeDir + this.modelSetting.getModelFile(); 25 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 26 | this.loadModelData(path, model => { 27 | for (let i = 0; i < this.modelSetting.getTextureNum(); i++) { 28 | const texPaths = 29 | this.modelHomeDir + this.modelSetting.getTextureFile(i); 30 | 31 | this.loadTexture(i, texPaths, () => { 32 | if (this.isTexLoaded) { 33 | if (this.modelSetting.getExpressionNum() > 0) { 34 | this.expressions = {}; 35 | 36 | for ( 37 | let j = 0; 38 | j < this.modelSetting.getExpressionNum(); 39 | j++ 40 | ) { 41 | const expName = this.modelSetting.getExpressionName(j); 42 | const expFilePath = 43 | this.modelHomeDir + 44 | this.modelSetting.getExpressionFile(j); 45 | 46 | this.loadExpression(expName, expFilePath); 47 | } 48 | } else { 49 | this.expressionManager = null; 50 | this.expressions = {}; 51 | } 52 | 53 | if (this.eyeBlink == null) { 54 | this.eyeBlink = new L2DEyeBlink(); 55 | } 56 | 57 | if (this.modelSetting.getPhysicsFile() != null) { 58 | this.loadPhysics( 59 | this.modelHomeDir + this.modelSetting.getPhysicsFile(), 60 | ); 61 | } else { 62 | this.physics = null; 63 | } 64 | 65 | if (this.modelSetting.getPoseFile() != null) { 66 | this.loadPose( 67 | this.modelHomeDir + this.modelSetting.getPoseFile(), 68 | () => { 69 | this.pose.updateParam(this.live2DModel); 70 | }, 71 | ); 72 | } else { 73 | this.pose = null; 74 | } 75 | 76 | if (this.modelSetting.getLayout() != null) { 77 | const layout = this.modelSetting.getLayout(); 78 | if (layout['width'] != null) 79 | this.modelMatrix.setWidth(layout['width']); 80 | if (layout['height'] != null) 81 | this.modelMatrix.setHeight(layout['height']); 82 | 83 | if (layout['x'] != null) this.modelMatrix.setX(layout['x']); 84 | if (layout['y'] != null) this.modelMatrix.setY(layout['y']); 85 | if (layout['center_x'] != null) 86 | this.modelMatrix.centerX(layout['center_x']); 87 | if (layout['center_y'] != null) 88 | this.modelMatrix.centerY(layout['center_y']); 89 | if (layout['top'] != null) 90 | this.modelMatrix.top(layout['top']); 91 | if (layout['bottom'] != null) 92 | this.modelMatrix.bottom(layout['bottom']); 93 | if (layout['left'] != null) 94 | this.modelMatrix.left(layout['left']); 95 | if (layout['right'] != null) 96 | this.modelMatrix.right(layout['right']); 97 | } 98 | 99 | for (let j = 0; j < this.modelSetting.getInitParamNum(); j++) { 100 | this.live2DModel.setParamFloat( 101 | this.modelSetting.getInitParamID(j), 102 | this.modelSetting.getInitParamValue(j), 103 | ); 104 | } 105 | 106 | for ( 107 | let j = 0; 108 | j < this.modelSetting.getInitPartsVisibleNum(); 109 | j++ 110 | ) { 111 | this.live2DModel.setPartsOpacity( 112 | this.modelSetting.getInitPartsVisibleID(j), 113 | this.modelSetting.getInitPartsVisibleValue(j), 114 | ); 115 | } 116 | 117 | this.live2DModel.saveParam(); 118 | // this.live2DModel.setGL(gl); 119 | 120 | this.preloadMotionGroup(LAppDefine.MOTION_GROUP_IDLE); 121 | this.mainMotionManager.stopAllMotions(); 122 | 123 | this.setUpdating(false); 124 | this.setInitialized(true); 125 | 126 | if (typeof callback == 'function') callback(); 127 | } 128 | }); 129 | } 130 | }); 131 | } 132 | 133 | async loadModelSetting(modelSettingPath, modelSetting) { 134 | this.setUpdating(true); 135 | this.setInitialized(false); 136 | 137 | this.modelHomeDir = modelSettingPath.substring( 138 | 0, 139 | modelSettingPath.lastIndexOf('/') + 1, 140 | ); 141 | 142 | this.modelSetting = new ModelSettingJson(); 143 | this.modelSetting.json = modelSetting; 144 | await new Promise(resolve => this.loadJSON(resolve)); 145 | } 146 | 147 | load(gl, modelSettingPath, callback) { 148 | this.setUpdating(true); 149 | this.setInitialized(false); 150 | 151 | this.modelHomeDir = modelSettingPath.substring( 152 | 0, 153 | modelSettingPath.lastIndexOf('/') + 1, 154 | ); 155 | 156 | this.modelSetting = new ModelSettingJson(); 157 | 158 | this.modelSetting.loadModelSetting(modelSettingPath, () => { 159 | this.loadJSON(callback); 160 | }); 161 | } 162 | 163 | release(gl) { 164 | // this.live2DModel.deleteTextures(); 165 | const pm = Live2DFramework.getPlatformManager(); 166 | 167 | gl.deleteTexture(pm.texture); 168 | } 169 | 170 | preloadMotionGroup(name) { 171 | for (let i = 0; i < this.modelSetting.getMotionNum(name); i++) { 172 | const file = this.modelSetting.getMotionFile(name, i); 173 | this.loadMotion(file, this.modelHomeDir + file, motion => { 174 | motion.setFadeIn(this.modelSetting.getMotionFadeIn(name, i)); 175 | motion.setFadeOut(this.modelSetting.getMotionFadeOut(name, i)); 176 | }); 177 | } 178 | } 179 | 180 | update() { 181 | // logger.trace("--> LAppModel.update()"); 182 | 183 | if (this.live2DModel == null) { 184 | logger.error('Failed to update.'); 185 | 186 | return; 187 | } 188 | 189 | const timeMSec = UtSystem.getUserTimeMSec() - this.startTimeMSec; 190 | const timeSec = timeMSec / 1000.0; 191 | const t = timeSec * 2 * Math.PI; 192 | 193 | if (this.mainMotionManager.isFinished()) { 194 | this.startRandomMotion( 195 | LAppDefine.MOTION_GROUP_IDLE, 196 | LAppDefine.PRIORITY_IDLE, 197 | ); 198 | } 199 | 200 | //----------------------------------------------------------------- 201 | 202 | this.live2DModel.loadParam(); 203 | 204 | const update = this.mainMotionManager.updateParam(this.live2DModel); 205 | if (!update) { 206 | if (this.eyeBlink != null) { 207 | this.eyeBlink.updateParam(this.live2DModel); 208 | } 209 | } 210 | 211 | this.live2DModel.saveParam(); 212 | 213 | //----------------------------------------------------------------- 214 | 215 | if ( 216 | this.expressionManager != null && 217 | this.expressions != null && 218 | !this.expressionManager.isFinished() 219 | ) { 220 | this.expressionManager.updateParam(this.live2DModel); 221 | } 222 | 223 | this.live2DModel.addToParamFloat('PARAM_ANGLE_X', this.dragX * 30, 1); 224 | this.live2DModel.addToParamFloat('PARAM_ANGLE_Y', this.dragY * 30, 1); 225 | this.live2DModel.addToParamFloat( 226 | 'PARAM_ANGLE_Z', 227 | this.dragX * this.dragY * -30, 228 | 1, 229 | ); 230 | 231 | this.live2DModel.addToParamFloat('PARAM_BODY_ANGLE_X', this.dragX * 10, 1); 232 | 233 | this.live2DModel.addToParamFloat('PARAM_EYE_BALL_X', this.dragX, 1); 234 | this.live2DModel.addToParamFloat('PARAM_EYE_BALL_Y', this.dragY, 1); 235 | 236 | this.live2DModel.addToParamFloat( 237 | 'PARAM_ANGLE_X', 238 | Number(15 * Math.sin(t / 6.5345)), 239 | 0.5, 240 | ); 241 | this.live2DModel.addToParamFloat( 242 | 'PARAM_ANGLE_Y', 243 | Number(8 * Math.sin(t / 3.5345)), 244 | 0.5, 245 | ); 246 | this.live2DModel.addToParamFloat( 247 | 'PARAM_ANGLE_Z', 248 | Number(10 * Math.sin(t / 5.5345)), 249 | 0.5, 250 | ); 251 | this.live2DModel.addToParamFloat( 252 | 'PARAM_BODY_ANGLE_X', 253 | Number(4 * Math.sin(t / 15.5345)), 254 | 0.5, 255 | ); 256 | this.live2DModel.setParamFloat( 257 | 'PARAM_BREATH', 258 | Number(0.5 + 0.5 * Math.sin(t / 3.2345)), 259 | 1, 260 | ); 261 | 262 | if (this.physics != null) { 263 | this.physics.updateParam(this.live2DModel); 264 | } 265 | 266 | if (this.lipSync == null) { 267 | this.live2DModel.setParamFloat('PARAM_MOUTH_OPEN_Y', this.lipSyncValue); 268 | } 269 | 270 | if (this.pose != null) { 271 | this.pose.updateParam(this.live2DModel); 272 | } 273 | 274 | this.live2DModel.update(); 275 | } 276 | 277 | setRandomExpression() { 278 | const tmp = []; 279 | for (const name in this.expressions) { 280 | tmp.push(name); 281 | } 282 | 283 | const no = parseInt(Math.random() * tmp.length); 284 | 285 | this.setExpression(tmp[no]); 286 | } 287 | 288 | startRandomMotion(name, priority) { 289 | const max = this.modelSetting.getMotionNum(name); 290 | const no = parseInt(Math.random() * max); 291 | this.startMotion(name, no, priority); 292 | } 293 | 294 | startMotion(name, no, priority) { 295 | // logger.trace("startMotion : " + name + " " + no + " " + priority); 296 | 297 | const motionName = this.modelSetting.getMotionFile(name, no); 298 | 299 | if (motionName == null || motionName == '') { 300 | return; 301 | } 302 | 303 | if (priority == LAppDefine.PRIORITY_FORCE) { 304 | this.mainMotionManager.setReservePriority(priority); 305 | } else if (!this.mainMotionManager.reserveMotion(priority)) { 306 | logger.trace('Motion is running.'); 307 | return; 308 | } 309 | 310 | let motion; 311 | 312 | if (this.motions[name] == null) { 313 | this.loadMotion(null, this.modelHomeDir + motionName, mtn => { 314 | motion = mtn; 315 | 316 | this.setFadeInFadeOut(name, no, priority, motion); 317 | }); 318 | } else { 319 | motion = this.motions[name]; 320 | 321 | this.setFadeInFadeOut(name, no, priority, motion); 322 | } 323 | } 324 | 325 | setFadeInFadeOut(name, no, priority, motion) { 326 | const motionName = this.modelSetting.getMotionFile(name, no); 327 | 328 | motion.setFadeIn(this.modelSetting.getMotionFadeIn(name, no)); 329 | motion.setFadeOut(this.modelSetting.getMotionFadeOut(name, no)); 330 | 331 | logger.trace('Start motion : ' + motionName); 332 | 333 | if (this.modelSetting.getMotionSound(name, no) == null) { 334 | this.mainMotionManager.startMotionPrio(motion, priority); 335 | } else { 336 | const soundName = this.modelSetting.getMotionSound(name, no); 337 | // var player = new Sound(this.modelHomeDir + soundName); 338 | 339 | const snd = document.createElement('audio'); 340 | snd.src = this.modelHomeDir + soundName; 341 | 342 | logger.trace('Start sound : ' + soundName); 343 | 344 | snd.play(); 345 | this.mainMotionManager.startMotionPrio(motion, priority); 346 | } 347 | } 348 | 349 | setExpression(name) { 350 | const motion = this.expressions[name]; 351 | 352 | logger.trace('Expression : ' + name); 353 | 354 | this.expressionManager?.startMotion(motion, false); 355 | } 356 | 357 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 358 | draw(gl) { 359 | //logger.trace("--> LAppModel.draw()"); 360 | 361 | // if(this.live2DModel == null) return; 362 | 363 | MatrixStack.push(); 364 | 365 | MatrixStack.multMatrix(this.modelMatrix.getArray()); 366 | 367 | this.tmpMatrix = MatrixStack.getMatrix(); 368 | this.live2DModel.setMatrix(this.tmpMatrix); 369 | this.live2DModel.draw(); 370 | 371 | MatrixStack.pop(); 372 | } 373 | 374 | hitTest(id, testX, testY) { 375 | const len = this.modelSetting.getHitAreaNum(); 376 | if (len == 0) { 377 | const hitAreasCustom = this.modelSetting.getHitAreaCustom(); 378 | if (hitAreasCustom) { 379 | const x = hitAreasCustom[id + '_x']; 380 | const y = hitAreasCustom[id + '_y']; 381 | 382 | if (testX > Math.min(...x) && testX < Math.max(...x) && 383 | testY > Math.min(...y) && testY < Math.max(...y)) { 384 | return true; 385 | } 386 | } 387 | } 388 | for (let i = 0; i < len; i++) { 389 | if (id == this.modelSetting.getHitAreaName(i)) { 390 | const drawID = this.modelSetting.getHitAreaID(i); 391 | 392 | return this.hitTestSimple(drawID, testX, testY); 393 | } 394 | } 395 | 396 | return false; 397 | } 398 | } 399 | 400 | export default LAppModel; 401 | -------------------------------------------------------------------------------- /README.en.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 | [中文](README.md) 11 | 12 | ## Features 13 | 14 | - Add Live2D widget to web page 15 | - Lightweight, with no runtime dependencies other than Live2D Cubism Core 16 | - Core code is written in TypeScript, making it easy to integrate 17 | 18 | 19 | 20 | *Note: The character models above are for demonstration purposes only and are not included in this repository.* 21 | 22 | You can also check out example web pages: 23 | 24 | - Check the effect in the lower left corner of [Mimi's Blog](https://zhangshuqiao.org) 25 | - [demo/demo.html](https://live2d-widget.pages.dev/demo/demo) to demonstrate basic functionality 26 | - [demo/login.html](https://live2d-widget.pages.dev/demo/login) to imitate the login interface of NPM 27 | 28 | ## Usage 29 | 30 | If you are a beginner or only need the basic functionality, you can simply add the following line of code to the `head` or `body` of your HTML page to load the widget: 31 | 32 | ```html 33 | 34 | ``` 35 | 36 | The placement of the code depends on how your website is built. For example, if you are using [Hexo](https://hexo.io), you need to add the above code to the template file of your theme. The modification process is similar for pages generated using various template engines. 37 | If your website uses PJAX, since the widget does not need to be refreshed on every page, make sure to place the script outside the PJAX refresh area. 38 | 39 | **However, we strongly recommend configuring the widget yourself to make it more suitable for your website!** 40 | If you are interested in customizing the widget, please refer to the detailed instructions below. 41 | 42 | ## Configuration 43 | 44 | You can refer to the source code of `dist/autoload.js` to see the available configuration options. `autoload.js` will automatically load two files: `waifu.css` and `waifu-tips.js`. `waifu-tips.js` creates the `initWidget` function, which is the main function for loading the widget. The `initWidget` function accepts an object-type parameter as the configuration for the widget. The following are the available options: 45 | 46 | | Option | Type | Default Value | Description | 47 | | ------ | ---- | ------------- | ----------- | 48 | | `waifuPath` | `string` | `https://fastly.jsdelivr.net/npm/live2d-widgets@1/dist/waifu-tips.json` | Path to the widget resources, can be modified | 49 | | `cdnPath` | `string` | `https://fastly.jsdelivr.net/gh/fghrsh/live2d_api/` | CDN path | 50 | | `cubism2Path` | `string` | `https://fastly.jsdelivr.net/npm/live2d-widgets@1/dist/live2d.min.js` | Path to Cubism 2 Core | 51 | | `cubism5Path` | `string` | `https://cubism.live2d.com/sdk-web/cubismcore/live2dcubismcore.min.js` | Path to Cubism 5 Core | 52 | | `modelId` | `number` | `0` | Default model id | 53 | | `tools` | `string[]` | see `autoload.js` | Buttons of the loaded tools | 54 | | `drag` | `boolean` | `false` | Make the widget draggable | 55 | | `logLevel` | `string` | `error` | Log level: `error`, `warn`, `info`, `trace` | 56 | 57 | ## Model Repository 58 | 59 | This repository does not include any models. You need to configure a separate model repository and set it via the `cdnPath` option. 60 | Older versions of the `initWidget` function supported the `apiPath` parameter, which required users to set up their own backend. You can refer to [live2d_api](https://github.com/fghrsh/live2d_api) for details. The backend interface would integrate model resources and dynamically generate JSON description files. Since version 1.0, these features have been implemented on the frontend, so a dedicated `apiPath` is no longer required. All model resources can be provided statically. As long as `model_list.json` and the corresponding `textures.cache` for each model exist, features such as outfit changing are supported. 61 | 62 | ## Development 63 | 64 | If the options provided in the "Configuration" section above are not enough to meet your needs, you can make modifications yourself. The directory structure of this repository is as follows: 65 | 66 | - `src` directory contains the TypeScript code for each component, e.g. the button and dialog box. 67 | - `build` directory contains files generated from the source code in `src` (please do not modify them directly!) 68 | - `dist` directory contains the files that can be directly used on web pages after packaging, including: 69 | - `autoload.js` is used to automatically load other resources such as style sheets. 70 | - `waifu-tips.js` is automatically generated by `build/waifu-tips.js` and it is not recommended to modify it directly. 71 | - `waifu.css` is the style sheet for the widget. 72 | - `waifu-tips.json` defines the triggering conditions (`selector`, CSS selector) and the displayed text when triggered (`text`). 73 | By default, the CSS selector rules in `waifu-tips.json` are effective for the Hexo [NexT theme](https://github.com/next-theme/hexo-theme-next), but you may need to modify or add new content to make it suitable for your own website. 74 | **Warning: The content in `waifu-tips.json` may not be suitable for all age groups or appropriate to access during work. Please ensure their suitability when using them.** 75 | 76 | To deploy the development testing environment of this project locally, you need to install Node.js and npm, then execute the following commands: 77 | 78 | ```bash 79 | git clone https://github.com/stevenjoezhang/live2d-widget.git 80 | npm install 81 | ``` 82 | 83 | If you need to use Cubism 3 or newer models, please download and extract the Cubism SDK for Web separately into the `src` directory, for example, `src/CubismSdkForWeb-5-r.4`. Due to Live2D license agreements (including the Live2D Proprietary Software License Agreement and Live2D Open Software License Agreement), this project cannot include the source code of Cubism SDK for Web. 84 | If you only need to use Cubism 2 models, you can skip this step. The code in this repository complies with the Redistributable Code terms of the Live2D license agreements. 85 | Once completed, use the following command to compile and bundle the project. 86 | 87 | 88 | ```bash 89 | npm run build 90 | ``` 91 | 92 | The TypeScript code in the `src` directory is compiled into the `build` directory, and the code in the `build` directory is further bundled into the `dist` directory. 93 | To support both Cubism 2 and Cubism 3 (and newer) models while minimizing code size, Cubism Core and related code are loaded dynamically based on the detected model version. 94 | 95 | ## Deploy 96 | 97 | After making modifications locally, you can deploy the modified project on a server or load it via a CDN. To make it easier to customize, you can fork this repository and push your modified content to your own repository using `git push`. 98 | 99 | ### Using jsDelivr CDN 100 | 101 | To load forked repository via jsDelivr, the usage method becomes: 102 | 103 | ```html 104 | 105 | ``` 106 | 107 | Replace `username` with your GitHub username. To ensure the content of the CDN is refreshed correctly, you need to create a new git tag and push it to the GitHub repository. Otherwise, `@latest` in the URL will still point to the previous version. Additionally, CDN itself has caching, so the changes may take some time to take effect. 108 | 109 | ### Using Cloudflare Pages 110 | 111 | You can also deploy using Cloudflare Pages. Create a new project in Cloudflare Pages and select your forked repository. Then, set the build command to `npm run build`. Once configured, Cloudflare Pages will automatically build and deploy your project. 112 | 113 | ### Self-host 114 | 115 | Alternatively, you can directly host these files on your server instead of loading them via CDN. 116 | 117 | - Clone the forked repository onto your server, or upload the local files to the website directory on the server using `ftp` or similar methods. 118 | - If you are deploying a static blog using Hexo or similar tools, place the code of this project in the blog's source file directory (e.g., the `source` directory). When redeploying the blog, the relevant files will be automatically uploaded to the corresponding paths. To prevent these files from being incorrectly modified by Hexo plugins, you may need to set `skip_render`. 119 | 120 | Afterwards, the entire project can be accessed through your domain name. You can try opening the `autoload.js` and `live2d.min.js` files in your browser and confirm that their content is complete and correct. 121 | If everything is normal, you can proceed to modify the constant `live2d_path` in `autoload.js` to the URL of the `dist` directory. For example, if you can access `live2d.min.js` through the following URL: 122 | 123 | ``` 124 | https://example.com/path/to/live2d-widget/dist/live2d.min.js 125 | ``` 126 | 127 | then modify the value of `live2d_path` to: 128 | 129 | ``` 130 | https://example.com/path/to/live2d-widget/dist/ 131 | ``` 132 | 133 | Make sure to include the trailing `/` in the path. 134 | Once done, add the following code to the interface where you want to add the live2d-widget: 135 | 136 | ```html 137 | 138 | ``` 139 | 140 | This will load the widget. 141 | 142 | ## Thanks 143 | 144 | 145 | 146 | 147 | 148 | BrowserStack Logo 149 | 150 | 151 | 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 | > Thanks to jsDelivr for providing public CDN service. 163 | 164 | Thanks fghrsh for providing API service. 165 | 166 | Thanks to [Hitokoto](https://hitokoto.cn) for providing the sentence API. 167 | 168 | When you click on the paper airplane button of the virtual assistant, a hidden surprise will appear. This feature is from [WebsiteAsteroids](http://www.websiteasteroids.com). 169 | 170 | ## More 171 | 172 | The code is modified based on this blog post: 173 | https://www.fghrsh.net/post/123.html 174 | 175 | For more information, you can refer to the following links: 176 | https://nocilol.me/archives/lab/add-dynamic-poster-girl-with-live2d-to-your-blog-02 177 | https://github.com/guansss/pixi-live2d-display 178 | 179 | For more models: 180 | https://github.com/zenghongtu/live2d-model-assets 181 | 182 | In addition to that, there are desktop versions available: 183 | https://github.com/TSKI433/hime-display 184 | https://github.com/amorist/platelet 185 | https://github.com/akiroz/Live2D-Widget 186 | https://github.com/zenghongtu/PPet 187 | https://github.com/LikeNeko/L2dPetForMac 188 | 189 | And also Wallpaper Engine: 190 | https://github.com/guansss/nep-live2d 191 | 192 | Official Live2D websites: 193 | https://www.live2d.com/en/ 194 | 195 | ## License 196 | 197 | This repository does not contain any models. The copyrights of all Live2D models, images, and motion data used for demonstration purposes belong to their respective original authors. They are provided for research and learning purposes only and should not be used for commercial purposes. 198 | 199 | The code in this repository (excluding parts covered by the Live2D Proprietary Software License and the Live2D Open Software License) is released under the GNU General Public License v3 200 | http://www.gnu.org/licenses/gpl-3.0.html 201 | 202 | Please comply with the relevant licenses when using any Live2D-related code: 203 | 204 | License for Live2D Cubism SDK 2.1: 205 | [Live2D SDK License Agreement (Public)](https://docs.google.com/document/d/10tz1WrycskzGGBOhrAfGiTSsgmyFy8D9yHx9r_PsN8I/) 206 | 207 | License for Live2D Cubism SDK 5: 208 | Live2D Cubism Core is provided under the Live2D Proprietary Software License. 209 | https://www.live2d.com/eula/live2d-proprietary-software-license-agreement_en.html 210 | Live2D Cubism Components are provided under the Live2D Open Software License. 211 | https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html 212 | 213 | ## Update Log 214 | 215 | Starting from January 1, 2020, this project no longer depends on jQuery. 216 | 217 | Starting from November 1, 2022, this project no longer requires users to separately load Font Awesome. 218 | -------------------------------------------------------------------------------- /dist/waifu-tips.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Live2D Widget 3 | * https://github.com/stevenjoezhang/live2d-widget 4 | */ 5 | function e(e){return Array.isArray(e)?e[Math.floor(Math.random()*e.length)]:e}function t(e,t){const s=Math.floor(Math.random()*(e-1));return s>=t?s+1:s}function s(e,t){return new Promise(((t,s)=>{let o;o=document.createElement("script"),o.src=e,o&&(o.onload=()=>t(e),o.onerror=()=>s(e),document.head.appendChild(o))}))}let o=null;function i(t,s,i,n=!0){let l=parseInt(sessionStorage.getItem("waifu-message-priority"),10);if(isNaN(l)&&(l=0),!t||n&&l>i||!n&&l>=i)return;o&&(clearTimeout(o),o=null),t=e(t),sessionStorage.setItem("waifu-message-priority",String(i));const a=document.getElementById("waifu-tips");a.innerHTML=t,a.classList.add("waifu-tips-active"),o=setTimeout((()=>{sessionStorage.removeItem("waifu-message-priority"),a.classList.remove("waifu-tips-active")}),s)}function n(e,...t){return e.replace(/\$(\d+)/g,((e,s)=>{var o;const i=parseInt(s,10)-1;return null!==(o=t[i])&&void 0!==o?o:""}))}class l{constructor(e="info"){this.level=e}setLevel(e){e&&(this.level=e)}shouldLog(e){return l.levelOrder[e]<=l.levelOrder[this.level]}error(e,...t){this.shouldLog("error")&&console.error("[Live2D Widget][ERROR]",e,...t)}warn(e,...t){this.shouldLog("warn")&&console.warn("[Live2D Widget][WARN]",e,...t)}info(e,...t){this.shouldLog("info")&&console.log("[Live2D Widget][INFO]",e,...t)}trace(e,...t){this.shouldLog("trace")&&console.log("[Live2D Widget][TRACE]",e,...t)}}l.levelOrder={error:0,warn:1,info:2,trace:3};const a=new l;class c{constructor(e,t=[]){var s;this.modelList=null;let{apiPath:o,cdnPath:i}=e;const{cubism2Path:n,cubism5Path:l}=e;let c=!1;if("string"==typeof i)i.endsWith("/")||(i+="/"),c=!0;else if("string"==typeof o)o.endsWith("/")||(o+="/"),i=o,c=!0,a.warn("apiPath option is deprecated. Please use cdnPath instead.");else if(!t.length)throw"Invalid initWidget argument!";let d=parseInt(localStorage.getItem("modelId"),10),r=parseInt(localStorage.getItem("modelTexturesId"),10);(isNaN(d)||isNaN(r))&&(r=0),isNaN(d)&&(d=null!==(s=e.modelId)&&void 0!==s?s:0),this.useCDN=c,this.cdnPath=i||"",this.cubism2Path=n||"",this.cubism5Path=l||"",this._modelId=d,this._modelTexturesId=r,this.currentModelVersion=0,this.loading=!1,this.modelJSONCache={},this.models=t}static async initCheck(e,t=[]){const s=new c(e,t);if(s.useCDN){const e=await fetch(`${s.cdnPath}model_list.json`);s.modelList=await e.json(),s.modelId>=s.modelList.models.length&&(s.modelId=0);const t=s.modelList.models[s.modelId];if(Array.isArray(t))s.modelTexturesId>=t.length&&(s.modelTexturesId=0);else{const e=`${s.cdnPath}model/${t}/index.json`,o=await s.fetchWithCache(e);if(2===s.checkModelVersion(o)){const e=await s.loadTextureCache(t);s.modelTexturesId>=e.length&&(s.modelTexturesId=0)}}}else s.modelId>=s.models.length&&(s.modelId=0),s.modelTexturesId>=s.models[s.modelId].paths.length&&(s.modelTexturesId=0);return s}set modelId(e){this._modelId=e,localStorage.setItem("modelId",e.toString())}get modelId(){return this._modelId}set modelTexturesId(e){this._modelTexturesId=e,localStorage.setItem("modelTexturesId",e.toString())}get modelTexturesId(){return this._modelTexturesId}resetCanvas(){document.getElementById("waifu-canvas").innerHTML=''}async fetchWithCache(e){let t;if(e in this.modelJSONCache)t=this.modelJSONCache[e];else{try{const s=await fetch(e);t=await s.json()}catch(e){t=null}this.modelJSONCache[e]=t}return t}checkModelVersion(e){return 3===e.Version||e.FileReferences?3:2}async loadLive2D(e,t){if(this.loading)a.warn("Still loading. Abort.");else{this.loading=!0;try{const o=this.checkModelVersion(t);if(2===o){if(!this.cubism2model){if(!this.cubism2Path)return void a.error("No cubism2Path set, cannot load Cubism 2 Core.");await s(this.cubism2Path);const{default:e}=await import("./chunk/index.js");this.cubism2model=new e}3===this.currentModelVersion&&(this.cubism5model.release(),this.resetCanvas()),3!==this.currentModelVersion&&this.cubism2model.gl?await this.cubism2model.changeModelWithJSON(e,t):await this.cubism2model.init("live2d",e,t)}else{if(!this.cubism5Path)return void a.error("No cubism5Path set, cannot load Cubism 5 Core.");await s(this.cubism5Path);const{AppDelegate:t}=await import("./chunk/index2.js");this.cubism5model=new t,2===this.currentModelVersion&&(this.cubism2model.destroy(),this.resetCanvas()),2!==this.currentModelVersion&&this.cubism5model.subdelegates.at(0)?this.cubism5model.changeModel(e):(this.cubism5model.initialize(),this.cubism5model.changeModel(e),this.cubism5model.run())}a.info(`Model ${e} (Cubism version ${o}) loaded`),this.currentModelVersion=o}catch(e){console.error("loadLive2D failed",e)}this.loading=!1}}async loadTextureCache(e){return await this.fetchWithCache(`${this.cdnPath}model/${e}/textures.cache`)||[]}async loadModel(e){let t,s;if(this.useCDN){let e=this.modelList.models[this.modelId];Array.isArray(e)&&(e=e[this.modelTexturesId]),t=`${this.cdnPath}model/${e}/index.json`,s=await this.fetchWithCache(t);if(2===this.checkModelVersion(s)){const t=await this.loadTextureCache(e);if(t.length>0){let e=t[this.modelTexturesId];"string"==typeof e&&(e=[e]),s.textures=e}}}else t=this.models[this.modelId].paths[this.modelTexturesId],s=await this.fetchWithCache(t);await this.loadLive2D(t,s),i(e,4e3,10)}async loadRandTexture(e="",s=""){const{modelId:o}=this;let n=!1;if(this.useCDN){const e=this.modelList.models[o];if(Array.isArray(e))this.modelTexturesId=t(e.length,this.modelTexturesId);else{const s=`${this.cdnPath}model/${e}/index.json`,o=await this.fetchWithCache(s);if(2===this.checkModelVersion(o)){const s=await this.loadTextureCache(e);s.length<=1?n=!0:this.modelTexturesId=t(s.length,this.modelTexturesId)}else n=!0}}else 1===this.models[o].paths.length?n=!0:this.modelTexturesId=t(this.models[o].paths.length,this.modelTexturesId);n?i(s,4e3,10):await this.loadModel(e)}async loadNextModel(){this.modelTexturesId=0,this.useCDN?(this.modelId=(this.modelId+1)%this.modelList.models.length,await this.loadModel(this.modelList.messages[this.modelId])):(this.modelId=(this.modelId+1)%this.models.length,await this.loadModel(this.models[this.modelId].message))}}class d{constructor(e,t,s){this.config=t,this.tools={hitokoto:{icon:'\x3c!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --\x3e',callback:async()=>{const e=await fetch("https://v1.hitokoto.cn"),t=await e.json(),o=n(s.message.hitokoto,t.from,t.creator);i(t.hitokoto,6e3,9),setTimeout((()=>{i(o,4e3,9)}),6e3)}},asteroids:{icon:'\x3c!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --\x3e',callback:()=>{if(window.Asteroids)window.ASTEROIDSPLAYERS||(window.ASTEROIDSPLAYERS=[]),window.ASTEROIDSPLAYERS.push(new window.Asteroids);else{const e=document.createElement("script");e.src="https://fastly.jsdelivr.net/gh/stevenjoezhang/asteroids/asteroids.js",document.head.appendChild(e)}}},"switch-model":{icon:'\x3c!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --\x3e',callback:()=>e.loadNextModel()},"switch-texture":{icon:'\x3c!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --\x3e',callback:()=>{let t="",o="";s&&(t=s.message.changeSuccess,o=s.message.changeFail),e.loadRandTexture(t,o)}},photo:{icon:'\x3c!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --\x3e',callback:()=>{i(s.message.photo,6e3,9);const e=document.getElementById("live2d");if(!e)return;const t=e.toDataURL(),o=document.createElement("a");o.style.display="none",o.href=t,o.download="live2d-photo.png",document.body.appendChild(o),o.click(),document.body.removeChild(o)}},info:{icon:'\x3c!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --\x3e',callback:()=>{open("https://github.com/stevenjoezhang/live2d-widget")}},quit:{icon:'\x3c!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --\x3e',callback:()=>{localStorage.setItem("waifu-display",Date.now().toString());i(s.message.goodbye,2e3,11);const e=document.getElementById("waifu");e&&(e.classList.remove("waifu-active"),setTimeout((()=>{e.classList.add("waifu-hidden");const t=document.getElementById("waifu-toggle");null==t||t.classList.add("waifu-toggle-active")}),3e3))}}}}registerTools(){var e;Array.isArray(this.config.tools)||(this.config.tools=Object.keys(this.tools));for(const t of this.config.tools)if(this.tools[t]){const{icon:s,callback:o}=this.tools[t],i=document.createElement("span");i.id=`waifu-tool-${t}`,i.innerHTML=s,null===(e=document.getElementById("waifu-tool"))||void 0===e||e.insertAdjacentElement("beforeend",i),i.addEventListener("click",o)}}}async function r(t){var s;localStorage.removeItem("waifu-display"),sessionStorage.removeItem("waifu-message-priority"),document.body.insertAdjacentHTML("beforeend",'
\n
\n
\n \n
\n
\n
');let o,l=[];if(t.waifuPath){const s=await fetch(t.waifuPath);o=await s.json(),l=o.models,function(t){let s,o=!1;const n=t.message.default;let l;t.seasons.forEach((({date:t,text:s})=>{const o=new Date,i=t.split("-")[0],l=t.split("-")[1]||i;Number(i.split("/")[0])<=o.getMonth()+1&&o.getMonth()+1<=Number(l.split("/")[0])&&Number(i.split("/")[1])<=o.getDate()&&o.getDate()<=Number(l.split("/")[1])&&(s=(s=e(s)).replace("{year}",String(o.getFullYear())),n.push(s))})),window.addEventListener("mousemove",(()=>o=!0)),window.addEventListener("keydown",(()=>o=!0)),setInterval((()=>{o?(o=!1,clearInterval(s),s=null):s||(s=setInterval((()=>{i(n,6e3,9)}),2e4))}),1e3),window.addEventListener("mouseover",(s=>{var o;for(let{selector:n,text:a}of t.mouseover)if(null===(o=s.target)||void 0===o?void 0:o.closest(n)){if(l===n)return;return l=n,a=e(a),a=a.replace("{text}",s.target.innerText),void i(a,4e3,8)}})),window.addEventListener("click",(s=>{var o;for(let{selector:n,text:l}of t.click)if(null===(o=s.target)||void 0===o?void 0:o.closest(n))return l=e(l),l=l.replace("{text}",s.target.innerText),void i(l,4e3,8)})),window.addEventListener("live2d:hoverbody",(()=>{i(e(t.message.hoverBody),4e3,8,!1)})),window.addEventListener("live2d:tapbody",(()=>{i(e(t.message.tapBody),4e3,9)}));const a=()=>{};console.log("%c",a),a.toString=()=>{i(t.message.console,6e3,9)},window.addEventListener("copy",(()=>{i(t.message.copy,6e3,9)})),window.addEventListener("visibilitychange",(()=>{document.hidden||i(t.message.visibilitychange,6e3,9)}))}(o),i(function(e,t,s){if("/"===location.pathname)for(const{hour:t,text:s}of e){const e=new Date,o=t.split("-")[0],i=t.split("-")[1]||o;if(Number(o)<=e.getHours()&&e.getHours()<=Number(i))return s}if(!t)return"";const o=n(t,document.title);if(""===document.referrer||!s)return o;const i=new URL(document.referrer);return location.hostname===i.hostname?o:`${n(s,i.hostname)}
${o}`}(o.time,o.message.welcome,o.message.referrer),7e3,11)}const a=await c.initCheck(t,l);await a.loadModel(""),new d(a,t,o).registerTools(),t.drag&&function(){const e=document.getElementById("waifu");if(!e)return;let t=window.innerWidth,s=window.innerHeight;const o=e.offsetWidth,i=e.offsetHeight;e.addEventListener("mousedown",(n=>{if(2===n.button)return;const l=document.getElementById("live2d");if(n.target!==l)return;n.preventDefault();const a=n.offsetX,c=n.offsetY;document.onmousemove=n=>{const l=n.clientX,d=n.clientY;let r=l-a,m=d-c;m<0?m=0:m>=s-i&&(m=s-i),r<0?r=0:r>=t-o&&(r=t-o),e.style.top=m+"px",e.style.left=r+"px"},document.onmouseup=()=>{document.onmousemove=null}})),window.onresize=()=>{t=window.innerWidth,s=window.innerHeight}}(),null===(s=document.getElementById("waifu"))||void 0===s||s.classList.add("waifu-active")}window.initWidget=function(e){if("string"==typeof e)return void a.error("Your config for Live2D initWidget is outdated. Please refer to https://github.com/stevenjoezhang/live2d-widget/blob/master/dist/autoload.js");a.setLevel(e.logLevel),document.body.insertAdjacentHTML("beforeend",'
\n \x3c!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --\x3e\n
');const t=document.getElementById("waifu-toggle");null==t||t.addEventListener("click",(()=>{var s;null==t||t.classList.remove("waifu-toggle-active"),(null==t?void 0:t.getAttribute("first-time"))?(r(e),null==t||t.removeAttribute("first-time")):(localStorage.removeItem("waifu-display"),null===(s=document.getElementById("waifu"))||void 0===s||s.classList.remove("waifu-hidden"),setTimeout((()=>{var e;null===(e=document.getElementById("waifu"))||void 0===e||e.classList.add("waifu-active")}),0))})),localStorage.getItem("waifu-display")&&Date.now()-Number(localStorage.getItem("waifu-display"))<=864e5?(null==t||t.setAttribute("first-time","true"),setTimeout((()=>{null==t||t.classList.add("waifu-toggle-active")}),0)):r(e)};export{a as l}; 6 | //# sourceMappingURL=waifu-tips.js.map 7 | -------------------------------------------------------------------------------- /dist/chunk/index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Live2D Widget 3 | * https://github.com/stevenjoezhang/live2d-widget 4 | */ 5 | import{l as t}from"../waifu-tips.js";class e{constructor(){this.live2DModel=null,this.modelMatrix=null,this.eyeBlink=null,this.physics=null,this.pose=null,this.initialized=!1,this.updating=!1,this.alpha=1,this.accAlpha=0,this.lipSync=!1,this.lipSyncValue=0,this.accelX=0,this.accelY=0,this.accelZ=0,this.dragX=0,this.dragY=0,this.startTimeMSec=null,this.mainMotionManager=new l,this.expressionManager=new l,this.motions={},this.expressions={},this.isTexLoaded=!1}getModelMatrix(){return this.modelMatrix}setAlpha(t){t>.999&&(t=1),t<.001&&(t=0),this.alpha=t}getAlpha(){return this.alpha}isInitialized(){return this.initialized}setInitialized(t){this.initialized=t}isUpdating(){return this.updating}setUpdating(t){this.updating=t}getLive2DModel(){return this.live2DModel}setLipSync(t){this.lipSync=t}setLipSyncValue(t){this.lipSyncValue=t}setAccel(t,e,i){this.accelX=t,this.accelY=e,this.accelZ=i}setDrag(t,e){this.dragX=t,this.dragY=e}getMainMotionManager(){return this.mainMotionManager}getExpressionManager(){return this.expressionManager}loadModelData(e,i){const s=M.getPlatformManager();t.info("Load model : "+e),s.loadLive2DModel(e,(e=>{this.live2DModel=e,this.live2DModel.saveParam();0==Live2D.getError()?(this.modelMatrix=new h(this.live2DModel.getCanvasWidth(),this.live2DModel.getCanvasHeight()),this.modelMatrix.setWidth(2),this.modelMatrix.setCenterPosition(0,0),i(this.live2DModel)):t.error("Error : Failed to loadModelData().")}))}loadTexture(e,s,n){i++;const a=M.getPlatformManager();t.info("Load Texture : "+s),a.loadTexture(this.live2DModel,e,s,(()=>{i--,0==i&&(this.isTexLoaded=!0),"function"==typeof n&&n()}))}loadMotion(e,i,s){const n=M.getPlatformManager();t.trace("Load Motion : "+i);let a=null;n.loadBytes(i,(t=>{a=Live2DMotion.loadMotion(t),null!=e&&(this.motions[e]=a),s(a)}))}loadExpression(e,i,n){const a=M.getPlatformManager();t.trace("Load Expression : "+i),a.loadBytes(i,(t=>{null!=e&&(this.expressions[e]=s.loadJson(t)),"function"==typeof n&&n()}))}loadPose(e,i){const s=M.getPlatformManager();t.trace("Load Pose : "+e);try{s.loadBytes(e,(t=>{this.pose=d.load(t),"function"==typeof i&&i()}))}catch(e){t.warn(e)}}loadPhysics(e){const i=M.getPlatformManager();t.trace("Load Physics : "+e);try{i.loadBytes(e,(t=>{this.physics=c.load(t)}))}catch(e){t.warn(e)}}hitTestSimple(t,e,i){const s=this.live2DModel.getDrawDataIndex(t);if(s<0)return!1;const n=this.live2DModel.getTransformedPoints(s);let a=this.live2DModel.getCanvasWidth(),r=0,o=this.live2DModel.getCanvasHeight(),h=0;for(let t=0;tr&&(r=e),ih&&(h=i)}const l=this.modelMatrix.invertTransformX(e),c=this.modelMatrix.invertTransformY(i);return a<=l&&l<=r&&o<=c&&c<=h}}let i=0;class s extends AMotion{constructor(){super(),this.paramList=[]}static loadJson(t){const e=new s,i=M.getPlatformManager().jsonParseFromBytes(t);if(e.setFadeIn(parseInt(i.fade_in)>0?parseInt(i.fade_in):1e3),e.setFadeOut(parseInt(i.fade_out)>0?parseInt(i.fade_out):1e3),null==i.params)return e;const a=i.params,r=a.length;e.paramList=[];for(let t=0;t=0;--e){const n=this.paramList[e];n.type==s.TYPE_ADD?t.addToParamFloat(n.id,n.value,i):n.type==s.TYPE_MULT?t.multParamFloat(n.id,n.value,i):n.type==s.TYPE_SET&&t.setParamFloat(n.id,n.value,i)}}}function n(){this.id="",this.type=-1,this.value=null}s.EXPRESSION_DEFAULT="DEFAULT",s.TYPE_SET=0,s.TYPE_ADD=1,s.TYPE_MULT=2;class a{constructor(){this.nextBlinkTime=null,this.stateStartTime=null,this.blinkIntervalMsec=null,this.eyeState=r.STATE_FIRST,this.blinkIntervalMsec=4e3,this.closingMotionMsec=100,this.closedMotionMsec=50,this.openingMotionMsec=150,this.closeIfZero=!0,this.eyeID_L="PARAM_EYE_L_OPEN",this.eyeID_R="PARAM_EYE_R_OPEN"}calcNextBlink(){return UtSystem.getUserTimeMSec()+Math.random()*(2*this.blinkIntervalMsec-1)}setInterval(t){this.blinkIntervalMsec=t}setEyeMotion(t,e,i){this.closingMotionMsec=t,this.closedMotionMsec=e,this.openingMotionMsec=i}updateParam(t){const e=UtSystem.getUserTimeMSec();let i,s=0;switch(this.eyeState){case r.STATE_CLOSING:s=(e-this.stateStartTime)/this.closingMotionMsec,s>=1&&(s=1,this.eyeState=r.STATE_CLOSED,this.stateStartTime=e),i=1-s;break;case r.STATE_CLOSED:s=(e-this.stateStartTime)/this.closedMotionMsec,s>=1&&(this.eyeState=r.STATE_OPENING,this.stateStartTime=e),i=0;break;case r.STATE_OPENING:s=(e-this.stateStartTime)/this.openingMotionMsec,s>=1&&(s=1,this.eyeState=r.STATE_INTERVAL,this.nextBlinkTime=this.calcNextBlink()),i=s;break;case r.STATE_INTERVAL:this.nextBlinkTime{};r.STATE_FIRST="STATE_FIRST",r.STATE_INTERVAL="STATE_INTERVAL",r.STATE_CLOSING="STATE_CLOSING",r.STATE_CLOSED="STATE_CLOSED",r.STATE_OPENING="STATE_OPENING";class o{constructor(){this.tr=new Float32Array(16),this.identity()}static mul(t,e,i){const s=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];let n,a,r;for(n=0;n<4;n++)for(a=0;a<4;a++)for(r=0;r<4;r++)s[n+4*a]+=t[n+4*r]*e[r+4*a];for(n=0;n<16;n++)i[n]=s[n]}identity(){for(let t=0;t<16;t++)this.tr[t]=t%5==0?1:0}getArray(){return this.tr}getCopyMatrix(){return new Float32Array(this.tr)}setMatrix(t){if(null!=this.tr&&this.tr.length==this.tr.length)for(let e=0;e<16;e++)this.tr[e]=t[e]}getScaleX(){return this.tr[0]}getScaleY(){return this.tr[5]}transformX(t){return this.tr[0]*t+this.tr[12]}transformY(t){return this.tr[5]*t+this.tr[13]}invertTransformX(t){return(t-this.tr[12])/this.tr[0]}invertTransformY(t){return(t-this.tr[13])/this.tr[5]}multTranslate(t,e){const i=[1,0,0,0,0,1,0,0,0,0,1,0,t,e,0,1];o.mul(i,this.tr,this.tr)}translate(t,e){this.tr[12]=t,this.tr[13]=e}translateX(t){this.tr[12]=t}translateY(t){this.tr[13]=t}multScale(t,e){const i=[t,0,0,0,0,e,0,0,0,0,1,0,0,0,0,1];o.mul(i,this.tr,this.tr)}scale(t,e){this.tr[0]=t,this.tr[5]=e}}class h extends o{constructor(t,e){super(),this.width=t,this.height=e}setPosition(t,e){this.translate(t,e)}setCenterPosition(t,e){const i=this.width*this.getScaleX(),s=this.height*this.getScaleY();this.translate(t-i/2,e-s/2)}top(t){this.setY(t)}bottom(t){const e=this.height*this.getScaleY();this.translateY(t-e)}left(t){this.setX(t)}right(t){const e=this.width*this.getScaleX();this.translateX(t-e)}centerX(t){const e=this.width*this.getScaleX();this.translateX(t-e/2)}centerY(t){const e=this.height*this.getScaleY();this.translateY(t-e/2)}setX(t){this.translateX(t)}setY(t){this.translateY(t)}setHeight(t){const e=t/this.height,i=-e;this.scale(e,i)}setWidth(t){const e=t/this.width,i=-e;this.scale(e,i)}}class l extends MotionQueueManager{constructor(){super(),this.currentPriority=null,this.reservePriority=null,this.super=MotionQueueManager.prototype}getCurrentPriority(){return this.currentPriority}getReservePriority(){return this.reservePriority}reserveMotion(t){return!(this.reservePriority>=t)&&(!(this.currentPriority>=t)&&(this.reservePriority=t,!0))}setReservePriority(t){this.reservePriority=t}updateParam(t){const e=MotionQueueManager.prototype.updateParam.call(this,t);return this.isFinished()&&(this.currentPriority=0),e}startMotionPrio(t,e){return e==this.reservePriority&&(this.reservePriority=0),this.currentPriority=e,this.startMotion(t,!1)}}class c{constructor(){this.physicsList=[],this.startTimeMSec=UtSystem.getUserTimeMSec()}static load(t){const e=new c,i=M.getPlatformManager().jsonParseFromBytes(t).physics_hair,s=i.length;for(let t=0;t=0)break;s=a,n=t.getPartsOpacity(r),n+=i/.5,n>1&&(n=1)}}s<0&&(s=0,n=1);for(let i=0;i.15&&(e=1-.15/(1-n)),i>e&&(i=e),t.setPartsOpacity(r,i)}}}copyOpacityOtherParts(t,e){for(let i=0;is)&&(h*=s/c,l*=s/c,c=s),this.faceVX+=h,this.faceVY+=l;{const t=.5*(Math.sqrt(s*s+16*s*r-8*s*r)-s),e=Math.sqrt(this.faceVX*this.faceVX+this.faceVY*this.faceVY);e>t&&(this.faceVX*=t/e,this.faceVY*=t/e)}this.faceX+=this.faceVX,this.faceY+=this.faceVY}}m.FRAME_RATE=30;class g extends o{constructor(){super(),this.screenLeft=null,this.screenRight=null,this.screenTop=null,this.screenBottom=null,this.maxLeft=null,this.maxRight=null,this.maxTop=null,this.maxBottom=null,this.max=Number.MAX_VALUE,this.min=0}getMaxScale(){return this.max}getMinScale(){return this.min}setMaxScale(t){this.max=t}setMinScale(t){this.min=t}isMaxScale(){return this.getScaleX()==this.max}isMinScale(){return this.getScaleX()==this.min}adjustTranslate(t,e){this.tr[0]*this.maxLeft+(this.tr[12]+t)>this.screenLeft&&(t=this.screenLeft-this.tr[0]*this.maxLeft-this.tr[12]),this.tr[0]*this.maxRight+(this.tr[12]+t)this.screenBottom&&(e=this.screenBottom-this.tr[5]*this.maxBottom-this.tr[13]);const i=[1,0,0,0,0,1,0,0,0,0,1,0,t,e,0,1];o.mul(i,this.tr,this.tr)}adjustScale(t,e,i){const s=i*this.tr[0];s0&&(i=this.min/this.tr[0]):s>this.max&&this.tr[0]>0&&(i=this.max/this.tr[0]);const n=[1,0,0,0,0,1,0,0,0,0,1,0,t,e,0,1],a=[i,0,0,0,0,i,0,0,0,0,1,0,0,0,0,1],r=[1,0,0,0,0,1,0,0,0,0,1,0,-t,-e,0,1];o.mul(r,this.tr,this.tr),o.mul(a,this.tr,this.tr),o.mul(n,this.tr,this.tr)}setScreenRect(t,e,i,s){this.screenLeft=t,this.screenRight=e,this.screenTop=s,this.screenBottom=i}setMaxScreenRect(t,e,i,s){this.maxLeft=t,this.maxRight=e,this.maxTop=s,this.maxBottom=i}getScreenLeft(){return this.screenLeft}getScreenRight(){return this.screenRight}getScreenBottom(){return this.screenBottom}getScreenTop(){return this.screenTop}getMaxLeft(){return this.maxLeft}getMaxRight(){return this.maxRight}getMaxBottom(){return this.maxBottom}getMaxTop(){return this.maxTop}}class M{static getPlatformManager(){return M.platformManager}static setPlatformManager(t){M.platformManager=t}}M.platformManager=null;const S=1.5,T=1,p=-1,E=1,v=-2,P=2,_=-2,I=2,x=1,f=2,A=3,y="idle",R="tap_body",O="pinch_in",D="pinch_out",L="head",N="body";class w{static reset(){this.depth=0}static loadIdentity(){for(let t=0;t<16;t++)this.currentMatrix[t]=t%5==0?1:0}static push(){this.depth;const t=16*(this.depth+1);this.matrixStack.length{const i=String.fromCharCode.apply(null,new Uint8Array(t));this.json=JSON.parse(i),e()}))}getTextureFile(t){return null==this.json[this.TEXTURES]||null==this.json[this.TEXTURES][t]?null:this.json[this.TEXTURES][t]}getModelFile(){return this.json[this.MODEL]}getTextureNum(){return null==this.json[this.TEXTURES]?0:this.json[this.TEXTURES].length}getHitAreaNum(){return null==this.json[this.HIT_AREAS]?0:this.json[this.HIT_AREAS].length}getHitAreaCustom(){return this.json[this.HIT_AREAS_CUSTOM]}getHitAreaID(t){return null==this.json[this.HIT_AREAS]||null==this.json[this.HIT_AREAS][t]?null:this.json[this.HIT_AREAS][t][this.ID]}getHitAreaName(t){return null==this.json[this.HIT_AREAS]||null==this.json[this.HIT_AREAS][t]?null:this.json[this.HIT_AREAS][t][this.NAME]}getPhysicsFile(){return this.json[this.PHYSICS]}getPoseFile(){return this.json[this.POSE]}getExpressionNum(){return null==this.json[this.EXPRESSIONS]?0:this.json[this.EXPRESSIONS].length}getExpressionFile(t){return null==this.json[this.EXPRESSIONS]?null:this.json[this.EXPRESSIONS][t][this.FILE]}getExpressionName(t){return null==this.json[this.EXPRESSIONS]?null:this.json[this.EXPRESSIONS][t][this.NAME]}getLayout(){return this.json[this.LAYOUT]}getInitParamNum(){return null==this.json[this.INIT_PARAM]?0:this.json[this.INIT_PARAM].length}getMotionNum(t){return null==this.json[this.MOTION_GROUPS]||null==this.json[this.MOTION_GROUPS][t]?0:this.json[this.MOTION_GROUPS][t].length}getMotionFile(t,e){return null==this.json[this.MOTION_GROUPS]||null==this.json[this.MOTION_GROUPS][t]||null==this.json[this.MOTION_GROUPS][t][e]?null:this.json[this.MOTION_GROUPS][t][e][this.FILE]}getMotionSound(t,e){return null==this.json[this.MOTION_GROUPS]||null==this.json[this.MOTION_GROUPS][t]||null==this.json[this.MOTION_GROUPS][t][e]||null==this.json[this.MOTION_GROUPS][t][e][this.SOUND]?null:this.json[this.MOTION_GROUPS][t][e][this.SOUND]}getMotionFadeIn(t,e){return null==this.json[this.MOTION_GROUPS]||null==this.json[this.MOTION_GROUPS][t]||null==this.json[this.MOTION_GROUPS][t][e]||null==this.json[this.MOTION_GROUPS][t][e][this.FADE_IN]?1e3:this.json[this.MOTION_GROUPS][t][e][this.FADE_IN]}getMotionFadeOut(t,e){return null==this.json[this.MOTION_GROUPS]||null==this.json[this.MOTION_GROUPS][t]||null==this.json[this.MOTION_GROUPS][t][e]||null==this.json[this.MOTION_GROUPS][t][e][this.FADE_OUT]?1e3:this.json[this.MOTION_GROUPS][t][e][this.FADE_OUT]}getInitParamID(t){return null==this.json[this.INIT_PARAM]||null==this.json[this.INIT_PARAM][t]?null:this.json[this.INIT_PARAM][t][this.ID]}getInitParamValue(t){return null==this.json[this.INIT_PARAM]||null==this.json[this.INIT_PARAM][t]?NaN:this.json[this.INIT_PARAM][t][this.VALUE]}getInitPartsVisibleNum(){return null==this.json[this.INIT_PARTS_VISIBLE]?0:this.json[this.INIT_PARTS_VISIBLE].length}getInitPartsVisibleID(t){return null==this.json[this.INIT_PARTS_VISIBLE]||null==this.json[this.INIT_PARTS_VISIBLE][t]?null:this.json[this.INIT_PARTS_VISIBLE][t][this.ID]}getInitPartsVisibleValue(t){return null==this.json[this.INIT_PARTS_VISIBLE]||null==this.json[this.INIT_PARTS_VISIBLE][t]?NaN:this.json[this.INIT_PARTS_VISIBLE][t][this.VALUE]}}class U extends e{constructor(){super(),this.modelHomeDir="",this.modelSetting=null,this.tmpMatrix=[]}loadJSON(t){const e=this.modelHomeDir+this.modelSetting.getModelFile();this.loadModelData(e,(e=>{for(let e=0;e{if(this.isTexLoaded){if(this.modelSetting.getExpressionNum()>0){this.expressions={};for(let t=0;t{this.pose.updateParam(this.live2DModel)})):this.pose=null,null!=this.modelSetting.getLayout()){const t=this.modelSetting.getLayout();null!=t.width&&this.modelMatrix.setWidth(t.width),null!=t.height&&this.modelMatrix.setHeight(t.height),null!=t.x&&this.modelMatrix.setX(t.x),null!=t.y&&this.modelMatrix.setY(t.y),null!=t.center_x&&this.modelMatrix.centerX(t.center_x),null!=t.center_y&&this.modelMatrix.centerY(t.center_y),null!=t.top&&this.modelMatrix.top(t.top),null!=t.bottom&&this.modelMatrix.bottom(t.bottom),null!=t.left&&this.modelMatrix.left(t.left),null!=t.right&&this.modelMatrix.right(t.right)}for(let t=0;tthis.loadJSON(t)))}load(t,e,i){this.setUpdating(!0),this.setInitialized(!1),this.modelHomeDir=e.substring(0,e.lastIndexOf("/")+1),this.modelSetting=new F,this.modelSetting.loadModelSetting(e,(()=>{this.loadJSON(i)}))}release(t){const e=M.getPlatformManager();t.deleteTexture(e.texture)}preloadMotionGroup(t){for(let e=0;e{i.setFadeIn(this.modelSetting.getMotionFadeIn(t,e)),i.setFadeOut(this.modelSetting.getMotionFadeOut(t,e))}))}}update(){if(null==this.live2DModel)return void t.error("Failed to update.");const e=2*((UtSystem.getUserTimeMSec()-this.startTimeMSec)/1e3)*Math.PI;this.mainMotionManager.isFinished()&&this.startRandomMotion(y,x),this.live2DModel.loadParam();this.mainMotionManager.updateParam(this.live2DModel)||null!=this.eyeBlink&&this.eyeBlink.updateParam(this.live2DModel),this.live2DModel.saveParam(),null==this.expressionManager||null==this.expressions||this.expressionManager.isFinished()||this.expressionManager.updateParam(this.live2DModel),this.live2DModel.addToParamFloat("PARAM_ANGLE_X",30*this.dragX,1),this.live2DModel.addToParamFloat("PARAM_ANGLE_Y",30*this.dragY,1),this.live2DModel.addToParamFloat("PARAM_ANGLE_Z",this.dragX*this.dragY*-30,1),this.live2DModel.addToParamFloat("PARAM_BODY_ANGLE_X",10*this.dragX,1),this.live2DModel.addToParamFloat("PARAM_EYE_BALL_X",this.dragX,1),this.live2DModel.addToParamFloat("PARAM_EYE_BALL_Y",this.dragY,1),this.live2DModel.addToParamFloat("PARAM_ANGLE_X",Number(15*Math.sin(e/6.5345)),.5),this.live2DModel.addToParamFloat("PARAM_ANGLE_Y",Number(8*Math.sin(e/3.5345)),.5),this.live2DModel.addToParamFloat("PARAM_ANGLE_Z",Number(10*Math.sin(e/5.5345)),.5),this.live2DModel.addToParamFloat("PARAM_BODY_ANGLE_X",Number(4*Math.sin(e/15.5345)),.5),this.live2DModel.setParamFloat("PARAM_BREATH",Number(.5+.5*Math.sin(e/3.2345)),1),null!=this.physics&&this.physics.updateParam(this.live2DModel),null==this.lipSync&&this.live2DModel.setParamFloat("PARAM_MOUTH_OPEN_Y",this.lipSyncValue),null!=this.pose&&this.pose.updateParam(this.live2DModel),this.live2DModel.update()}setRandomExpression(){const t=[];for(const e in this.expressions)t.push(e);const e=parseInt(Math.random()*t.length);this.setExpression(t[e])}startRandomMotion(t,e){const i=this.modelSetting.getMotionNum(t),s=parseInt(Math.random()*i);this.startMotion(t,s,e)}startMotion(e,i,s){const n=this.modelSetting.getMotionFile(e,i);if(null==n||""==n)return;if(s==A)this.mainMotionManager.setReservePriority(s);else if(!this.mainMotionManager.reserveMotion(s))return void t.trace("Motion is running.");let a;null==this.motions[e]?this.loadMotion(null,this.modelHomeDir+n,(t=>{a=t,this.setFadeInFadeOut(e,i,s,a)})):(a=this.motions[e],this.setFadeInFadeOut(e,i,s,a))}setFadeInFadeOut(e,i,s,n){const a=this.modelSetting.getMotionFile(e,i);if(n.setFadeIn(this.modelSetting.getMotionFadeIn(e,i)),n.setFadeOut(this.modelSetting.getMotionFadeOut(e,i)),t.trace("Start motion : "+a),null==this.modelSetting.getMotionSound(e,i))this.mainMotionManager.startMotionPrio(n,s);else{const a=this.modelSetting.getMotionSound(e,i),r=document.createElement("audio");r.src=this.modelHomeDir+a,t.trace("Start sound : "+a),r.play(),this.mainMotionManager.startMotionPrio(n,s)}}setExpression(e){var i;const s=this.expressions[e];t.trace("Expression : "+e),null===(i=this.expressionManager)||void 0===i||i.startMotion(s,!1)}draw(t){w.push(),w.multMatrix(this.modelMatrix.getArray()),this.tmpMatrix=w.getMatrix(),this.live2DModel.setMatrix(this.tmpMatrix),this.live2DModel.draw(),w.pop()}hitTest(t,e,i){const s=this.modelSetting.getHitAreaNum();if(0==s){const s=this.modelSetting.getHitAreaCustom();if(s){const n=s[t+"_x"],a=s[t+"_y"];if(e>Math.min(...n)&&eMath.min(...a)&&it.arrayBuffer())).then((i=>{this.cache[t]=i,e(i)}))}loadLive2DModel(t,e){let i=null;this.loadBytes(t,(t=>{i=Live2DModelWebGL.loadModel(t),e(i)}))}loadTexture(e,i,s,n){const a=new Image;a.crossOrigin="anonymous",a.src=s,a.onload=()=>{const s=document.getElementById("live2d").getContext("webgl2",{premultipliedAlpha:!0,preserveDrawingBuffer:!0});let r=s.createTexture();if(!r)return t.error("Failed to generate gl texture name."),-1;0==e.isPremultipliedAlpha()&&s.pixelStorei(s.UNPACK_PREMULTIPLY_ALPHA_WEBGL,1),s.pixelStorei(s.UNPACK_FLIP_Y_WEBGL,1),s.activeTexture(s.TEXTURE0),s.bindTexture(s.TEXTURE_2D,r),s.texImage2D(s.TEXTURE_2D,0,s.RGBA,s.RGBA,s.UNSIGNED_BYTE,a),s.texParameteri(s.TEXTURE_2D,s.TEXTURE_MAG_FILTER,s.LINEAR),s.texParameteri(s.TEXTURE_2D,s.TEXTURE_MIN_FILTER,s.LINEAR_MIPMAP_NEAREST),s.generateMipmap(s.TEXTURE_2D),e.setTexture(i,r),r=null,"function"==typeof n&&n()},a.onerror=()=>{t.error("Failed to load image : "+s)}}jsonParseFromBytes(t){let e;const i=new Uint8Array(t,0,3);e=239==i[0]&&187==i[1]&&191==i[2]?String.fromCharCode.apply(null,new Uint8Array(t,3)):String.fromCharCode.apply(null,new Uint8Array(t));return JSON.parse(e)}}class Y{constructor(){this.model=null,this.reloading=!1,Live2D.init(),M.setPlatformManager(new X)}getModel(){return this.model}releaseModel(t){this.model&&(this.model.release(t),this.model=null)}async changeModel(t,e){return new Promise(((i,s)=>{if(this.reloading)return;this.reloading=!0;const n=this.model,a=new U;a.load(t,e,(()=>{n&&n.release(t),this.model=a,this.reloading=!1,i()}))}))}async changeModelWithJSON(t,e,i){if(this.reloading)return;this.reloading=!0;const s=this.model,n=new U;await n.loadModelSetting(e,i),s&&s.release(t),this.model=n,this.reloading=!1}setDrag(t,e){this.model&&this.model.setDrag(t,e)}maxScaleEvent(){t.trace("Max scale event."),this.model&&this.model.startRandomMotion(O,f)}minScaleEvent(){t.trace("Min scale event."),this.model&&this.model.startRandomMotion(D,f)}tapEvent(e,i){return t.trace("tapEvent view x:"+e+" y:"+i),!!this.model&&(this.model.hitTest(L,e,i)?(t.trace("Tap face."),this.model.setRandomExpression()):this.model.hitTest(N,e,i)&&(t.trace("Tap body."),this.model.startRandomMotion(R,f)),!0)}}function j(t,e,i,s,n,a){const r=t-i,o=e-s;let h=0,l=0;return h=r>=0?r/(n-i):r/i,l=o>=0?o/(a-s):o/s,{vx:h,vy:-l}}class G{constructor(){this.live2DMgr=new Y,this.isDrawStart=!1,this.gl=null,this.canvas=null,this.dragMgr=null,this.viewMatrix=null,this.projMatrix=null,this.deviceToScreen=null,this.oldLen=0,this._boundMouseEvent=this.mouseEvent.bind(this),this._boundTouchEvent=this.touchEvent.bind(this)}initL2dCanvas(t){this.canvas=document.getElementById(t),this.canvas.addEventListener&&(this.canvas.addEventListener("mousewheel",this._boundMouseEvent,!1),this.canvas.addEventListener("click",this._boundMouseEvent,!1),document.addEventListener("mousemove",this._boundMouseEvent,!1),document.addEventListener("mouseout",this._boundMouseEvent,!1),this.canvas.addEventListener("contextmenu",this._boundMouseEvent,!1),this.canvas.addEventListener("touchstart",this._boundTouchEvent,!1),this.canvas.addEventListener("touchend",this._boundTouchEvent,!1),this.canvas.addEventListener("touchmove",this._boundTouchEvent,!1))}async init(e,i,s){this.initL2dCanvas(e);const n=this.canvas.width,a=this.canvas.height;this.dragMgr=new m;const r=a/n,h=p,l=E,c=-r,d=r;this.viewMatrix=new g,this.viewMatrix.setScreenRect(h,l,c,d),this.viewMatrix.setMaxScreenRect(v,P,_,I),this.viewMatrix.setMaxScale(S),this.viewMatrix.setMinScale(T),this.projMatrix=new o,this.projMatrix.multScale(1,n/a),this.deviceToScreen=new o,this.deviceToScreen.multTranslate(-n/2,-a/2),this.deviceToScreen.multScale(2/n,-2/n),this.gl=this.canvas.getContext("webgl2",{premultipliedAlpha:!0,preserveDrawingBuffer:!0}),this.gl?(Live2D.setGL(this.gl),this.gl.clearColor(0,0,0,0),await this.changeModelWithJSON(i,s),this.startDraw()):t.error("Failed to create WebGL context.")}destroy(){this.canvas&&(this.canvas.removeEventListener("mousewheel",this._boundMouseEvent,!1),this.canvas.removeEventListener("click",this._boundMouseEvent,!1),document.removeEventListener("mousemove",this._boundMouseEvent,!1),document.removeEventListener("mouseout",this._boundMouseEvent,!1),this.canvas.removeEventListener("contextmenu",this._boundMouseEvent,!1),this.canvas.removeEventListener("touchstart",this._boundTouchEvent,!1),this.canvas.removeEventListener("touchend",this._boundTouchEvent,!1),this.canvas.removeEventListener("touchmove",this._boundTouchEvent,!1)),this._drawFrameId&&(window.cancelAnimationFrame(this._drawFrameId),this._drawFrameId=null),this.isDrawStart=!1,this.live2DMgr&&"function"==typeof this.live2DMgr.release&&this.live2DMgr.release(),this.gl,this.canvas=null,this.gl=null,this.dragMgr=null,this.viewMatrix=null,this.projMatrix=null,this.deviceToScreen=null}startDraw(){if(!this.isDrawStart){this.isDrawStart=!0;const t=()=>{this.draw(),this._drawFrameId=window.requestAnimationFrame(t,this.canvas)};t()}}draw(){w.reset(),w.loadIdentity(),this.dragMgr.update(),this.live2DMgr.setDrag(this.dragMgr.getX(),this.dragMgr.getY()),this.gl.clear(this.gl.COLOR_BUFFER_BIT),w.multMatrix(this.projMatrix.getArray()),w.multMatrix(this.viewMatrix.getArray()),w.push();const t=this.live2DMgr.getModel();null!=t&&(t.initialized&&!t.updating&&(t.update(),t.draw(this.gl)),w.pop())}async changeModel(t){await this.live2DMgr.changeModel(this.gl,t)}async changeModelWithJSON(t,e){await this.live2DMgr.changeModelWithJSON(this.gl,t,e)}modelScaling(t){const e=this.viewMatrix.isMaxScale(),i=this.viewMatrix.isMinScale();this.viewMatrix.adjustScale(0,0,t),e||this.viewMatrix.isMaxScale()&&this.live2DMgr.maxScaleEvent(),i||this.viewMatrix.isMinScale()&&this.live2DMgr.minScaleEvent()}modelTurnHead(e){var i;const s=this.canvas.getBoundingClientRect(),{vx:n,vy:a}=j(e.clientX,e.clientY,s.left+s.width/2,s.top+s.height/2,window.innerWidth,window.innerHeight);t.trace("onMouseDown device( x:"+e.clientX+" y:"+e.clientY+" ) view( x:"+n+" y:"+a+")"),this.dragMgr.setPoint(n,a),this.live2DMgr.tapEvent(n,a),(null===(i=this.live2DMgr)||void 0===i?void 0:i.model.hitTest(N,n,a))&&window.dispatchEvent(new Event("live2d:tapbody"))}followPointer(e){var i;const s=this.canvas.getBoundingClientRect(),{vx:n,vy:a}=j(e.clientX,e.clientY,s.left+s.width/2,s.top+s.height/2,window.innerWidth,window.innerHeight);t.trace("onMouseMove device( x:"+e.clientX+" y:"+e.clientY+" ) view( x:"+n+" y:"+a+")"),this.dragMgr.setPoint(n,a),(null===(i=this.live2DMgr)||void 0===i?void 0:i.model.hitTest(N,n,a))&&window.dispatchEvent(new Event("live2d:hoverbody"))}lookFront(){this.dragMgr.setPoint(0,0)}mouseEvent(t){t.preventDefault(),"mousewheel"==t.type?t.wheelDelta>0?this.modelScaling(1.1):this.modelScaling(.9):"click"==t.type||"contextmenu"==t.type?this.modelTurnHead(t):"mousemove"==t.type?this.followPointer(t):"mouseout"==t.type&&this.lookFront()}touchEvent(t){t.preventDefault();const e=t.touches[0];if("touchstart"==t.type)1==t.touches.length&&this.modelTurnHead(e);else if("touchmove"==t.type){if(this.followPointer(e),2==t.touches.length){const e=t.touches[0],i=t.touches[1],s=Math.pow(e.pageX-i.pageX,2)+Math.pow(e.pageY-i.pageY,2);this.oldLen-s<0?this.modelScaling(1.025):this.modelScaling(.975),this.oldLen=s}}else"touchend"==t.type&&this.lookFront()}transformViewX(t){const e=this.deviceToScreen.transformX(t);return this.viewMatrix.invertTransformX(e)}transformViewY(t){const e=this.deviceToScreen.transformY(t);return this.viewMatrix.invertTransformY(e)}transformScreenX(t){return this.deviceToScreen.transformX(t)}transformScreenY(t){return this.deviceToScreen.transformY(t)}}export{G as default}; 6 | //# sourceMappingURL=index.js.map 7 | --------------------------------------------------------------------------------