├── .vscode └── settings.json ├── update.bat ├── .gitignore ├── scripts └── update.py ├── src ├── utils.ts ├── lapppal.ts ├── cache.ts ├── db.ts ├── lappmessagebox.ts ├── lappdefine.ts ├── touchmanager.ts ├── svg.ts ├── main.ts ├── lapptexturemanager.ts ├── lappview.ts ├── lapplive2dmanager.ts ├── lappdelegate.ts ├── lappwavfilehandler.ts ├── toolbox.ts └── lappmodel.ts ├── live2d-render.css ├── tsconfig.json ├── .eslintrc.yml ├── test └── index.html ├── package.json ├── webpack.config.js ├── webpack.config.web.js ├── index.html ├── README.md └── LICENSE /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /update.bat: -------------------------------------------------------------------------------- 1 | python scripts/update.py -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # 忽略所有生成的文件和文件夹 2 | /node_modules/ 3 | /dist/ 4 | /coverage/ 5 | 6 | # 忽略所有日志文件 7 | *.log 8 | 9 | # 忽略所有临时文件 10 | *.tmp 11 | 12 | # 忽略所有测试报告文件 13 | *.test.ts 14 | 15 | 16 | # 忽略所有编译生成的声明文件 17 | *.d.ts 18 | 19 | # 忽略所有编译生成的映射文件 20 | *.map 21 | 22 | cat/* -------------------------------------------------------------------------------- /scripts/update.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | live2d = open('./dist/live2d-render.js', 'r', encoding='utf-8').read() 4 | live2d = '/* eslint-disable */\n' + live2d 5 | 6 | vue_target_lib = os.path.realpath('../test-vue-live2d-render/src/lib') 7 | vue_live2d = os.path.join(vue_target_lib, 'live2d-render.js') 8 | open(vue_live2d, 'w', encoding='utf-8').write(live2d) -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | const pinkLogStyle = 'background-color: #CB81DA; color: white; padding: 3px; border-radius: 3px;'; 2 | const redLogStyle = 'background-color:rgb(227, 91, 49); color: white; padding: 3px; border-radius: 3px;'; 3 | 4 | export function pinkLog(message: string) { 5 | console.log('%c' + message, pinkLogStyle); 6 | } 7 | 8 | export function redLog(message: string) { 9 | console.log('%c' + message, redLogStyle); 10 | } -------------------------------------------------------------------------------- /live2d-render.css: -------------------------------------------------------------------------------- 1 | #live2dMessageBox-content { 2 | background-color: #706561; 3 | color: white; 4 | padding: 10px; 5 | height: fit-content; 6 | border-radius: .7em; 7 | word-break: break-all; 8 | border-right: 1px solid transparent; 9 | } 10 | 11 | .live2dMessageBox-content-hidden { 12 | opacity: 0; 13 | transition: all 0.5s ease-in; 14 | -moz-transition: all 0.5s ease-in; 15 | -webkit-transition: all 0.5s ease-in; 16 | } 17 | 18 | .live2dMessageBox-content-visible { 19 | opacity: 1; 20 | transition: all 0.5s ease-out; 21 | -moz-transition: all 0.5s ease-out; 22 | -webkit-transition: all 0.5s ease-out; 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "moduleResolution": "node", 5 | "esModuleInterop": true, 6 | "experimentalDecorators": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "outDir": "./dist", 9 | "removeComments": true, 10 | "sourceMap": true, 11 | "baseUrl": "./", 12 | "paths": { 13 | "@framework/*": [ 14 | "../../../Framework/src/*" 15 | ] 16 | }, 17 | "noImplicitAny": true, 18 | "useUnknownInCatchVariables": true, 19 | "allowJs": true 20 | }, 21 | "include": [ 22 | "src/**/*.ts", 23 | "../../../Core/*.ts" 24 | ], 25 | "exclude": [ 26 | "node_modules", 27 | "dist" 28 | ] 29 | } -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: 2 | - eslint:recommended 3 | - plugin:@typescript-eslint/eslint-recommended 4 | - plugin:@typescript-eslint/recommended 5 | - plugin:@typescript-eslint/recommended-requiring-type-checking 6 | - plugin:prettier/recommended 7 | - prettier 8 | plugins: 9 | - '@typescript-eslint' 10 | parser: '@typescript-eslint/parser' 11 | parserOptions: 12 | sourceType: module 13 | ecmaVersion: 2020 14 | project: ./tsconfig.json 15 | rules: 16 | prettier/prettier: 17 | - error 18 | - singleQuote: true 19 | trailingComma: none 20 | arrowParens: avoid 21 | 'camelcase': warn 22 | '@typescript-eslint/no-use-before-define': off 23 | '@typescript-eslint/ban-ts-comment': off 24 | '@typescript-eslint/unbound-method': off 25 | '@typescript-eslint/no-unsafe-assignment': off 26 | '@typescript-eslint/no-unsafe-return': off 27 | '@typescript-eslint/no-floating-promises': off 28 | camelcase: off -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 | 11 | 37 | 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "start": "webpack-cli serve --mode development", 5 | "build": "webpack --mode production", 6 | "test": "tsc --noEmit", 7 | "lint": "eslint src --ext .ts", 8 | "lint:fix": "eslint src --ext .ts --fix", 9 | "serve": "serve ../../.. -p 5000", 10 | "clean": "rimraf dist" 11 | }, 12 | "devDependencies": { 13 | "@typescript-eslint/eslint-plugin": "^5.59.5", 14 | "@typescript-eslint/parser": "^5.59.5", 15 | "eslint": "^8.40.0", 16 | "eslint-config-prettier": "^8.8.0", 17 | "eslint-plugin-prettier": "^4.2.1", 18 | "prettier": "^2.8.8", 19 | "rimraf": "^5.0.0", 20 | "serve": "^14.2.0", 21 | "ts-loader": "^9.4.2", 22 | "typescript": "^5.0.4", 23 | "webpack": "^5.82.1", 24 | "webpack-cli": "^5.1.4", 25 | "webpack-dev-server": "^4.15.0" 26 | }, 27 | "dependencies": { 28 | "live2d-render": "^0.0.1", 29 | "whatwg-fetch": "^3.6.2" 30 | }, 31 | "optionalDependencies": { 32 | "fsevents": "*" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | var path = require('path'); 3 | 4 | module.exports = { 5 | mode: 'production', 6 | target: ['web', 'es5'], 7 | entry: './src/main.ts', 8 | output: { 9 | path: path.resolve(__dirname, 'dist'), 10 | filename: 'live2d-render.js', 11 | publicPath: '/dist/', 12 | libraryTarget: 'commonjs2' 13 | }, 14 | resolve: { 15 | extensions: ['.ts', '.js'], 16 | alias: { 17 | '@framework': path.resolve(__dirname, '../../../Framework/src') 18 | } 19 | }, 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.ts$/, 24 | exclude: /node_modules/, 25 | loader: 'ts-loader' 26 | } 27 | ] 28 | }, 29 | devServer: { 30 | static: [ 31 | { 32 | directory: path.resolve(__dirname, './'), 33 | serveIndex: true, 34 | watch: true, 35 | } 36 | ], 37 | hot: true, 38 | port: 5000, 39 | host: '0.0.0.0', 40 | compress: true, 41 | devMiddleware: { 42 | writeToDisk: true, 43 | }, 44 | }, 45 | devtool: false 46 | } 47 | -------------------------------------------------------------------------------- /webpack.config.web.js: -------------------------------------------------------------------------------- 1 | 2 | var path = require('path'); 3 | 4 | module.exports = { 5 | mode: 'production', 6 | target: ['web', 'es5'], 7 | entry: './src/main.ts', 8 | output: { 9 | path: path.resolve(__dirname, 'dist'), 10 | filename: 'live2d-render.bundle.js', 11 | publicPath: '/dist/', 12 | libraryTarget: 'umd', 13 | library: 'Live2dRender' 14 | }, 15 | resolve: { 16 | extensions: ['.ts', '.js'], 17 | alias: { 18 | '@framework': path.resolve(__dirname, '../../../Framework/src') 19 | } 20 | }, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.ts$/, 25 | exclude: /node_modules/, 26 | loader: 'ts-loader' 27 | } 28 | ] 29 | }, 30 | devServer: { 31 | static: [ 32 | { 33 | directory: path.resolve(__dirname, './'), 34 | serveIndex: true, 35 | watch: true, 36 | } 37 | ], 38 | hot: true, 39 | port: 5000, 40 | host: '0.0.0.0', 41 | compress: true, 42 | devMiddleware: { 43 | writeToDisk: true, 44 | }, 45 | }, 46 | devtool: false 47 | } 48 | -------------------------------------------------------------------------------- /src/lapppal.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright(c) Live2D Inc. All rights reserved. 3 | * 4 | * Use of this source code is governed by the Live2D Open Software license 5 | * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. 6 | */ 7 | 8 | import { cacheFetch } from "./cache"; 9 | 10 | /** 11 | * プラットフォーム依存機能を抽象化する Cubism Platform Abstraction Layer. 12 | * 13 | * ファイル読み込みや時刻取得等のプラットフォームに依存する関数をまとめる。 14 | */ 15 | export class LAppPal { 16 | /** 17 | * ファイルをバイトデータとして読みこむ 18 | * 19 | * @param filePath 読み込み対象ファイルのパス 20 | * @return 21 | * { 22 | * buffer, 読み込んだバイトデータ 23 | * size ファイルサイズ 24 | * } 25 | */ 26 | public static loadFileAsBytes( 27 | filePath: string, 28 | callback: (arrayBuffer: ArrayBuffer, size: number) => void 29 | ): void { 30 | cacheFetch(filePath) 31 | .then(response => response.arrayBuffer()) 32 | .then(arrayBuffer => callback(arrayBuffer, arrayBuffer.byteLength)); 33 | } 34 | 35 | /** 36 | * デルタ時間(前回フレームとの差分)を取得する 37 | * @return デルタ時間[ms] 38 | */ 39 | public static getDeltaTime(): number { 40 | return this.s_deltaTime; 41 | } 42 | 43 | public static updateTime(): void { 44 | this.s_currentFrame = Date.now(); 45 | this.s_deltaTime = (this.s_currentFrame - this.s_lastFrame) / 1000; 46 | this.s_lastFrame = this.s_currentFrame; 47 | } 48 | 49 | /** 50 | * メッセージを出力する 51 | * @param message 文字列 52 | */ 53 | public static printMessage(message: string): void { 54 | // console.log(message); 55 | } 56 | 57 | static lastUpdate = Date.now(); 58 | 59 | static s_currentFrame = 0.0; 60 | static s_lastFrame = 0.0; 61 | static s_deltaTime = 0.0; 62 | } 63 | -------------------------------------------------------------------------------- /src/cache.ts: -------------------------------------------------------------------------------- 1 | import LAppDefine from "./lappdefine"; 2 | import { selectItemIndexDB, createItemIndexDB } from './db'; 3 | import { pinkLog } from "./utils"; 4 | 5 | function arrayBufferToBase64(buffer: ArrayBuffer): string { 6 | let binary = ''; 7 | const bytes = new Uint8Array(buffer); 8 | const len = bytes.byteLength; 9 | for (let i = 0; i < len; i++) { 10 | binary += String.fromCharCode(bytes[i]); 11 | } 12 | return window.btoa(binary); 13 | } 14 | 15 | function base64ToArrayBuffer(base64: string): ArrayBuffer { 16 | const binary_string = window.atob(base64); 17 | const len = binary_string.length; 18 | const bytes = new Uint8Array(len); 19 | for (let i = 0; i < len; i++) { 20 | bytes[i] = binary_string.charCodeAt(i); 21 | } 22 | return bytes.buffer; 23 | } 24 | 25 | interface FakeResponse { 26 | arrayBuffer: () => Promise 27 | } 28 | 29 | interface UrlDBItem { 30 | url: string 31 | arraybuffer: ArrayBuffer 32 | } 33 | 34 | interface ICacheSetting { 35 | refreshCache: boolean 36 | } 37 | 38 | export const CacheFetchSetting: ICacheSetting = { 39 | refreshCache: false 40 | }; 41 | 42 | /** 43 | * @description 获取 live2d 的数据,如果存在缓存,则从缓存中拿取,不会发生请求 44 | * @param url 需要请求的链接,如果 indexDB 中存在,则不会被请求 45 | */ 46 | export async function cacheFetch(url: string): Promise { 47 | if (!CacheFetchSetting.refreshCache && LAppDefine.LoadFromCache && LAppDefine.Live2dDB) { 48 | const item = await selectItemIndexDB('url', url); 49 | if (item !== undefined) { 50 | const arrayBuffer = item.arraybuffer; 51 | const response = { 52 | arrayBuffer: async () => { 53 | return arrayBuffer; 54 | } 55 | } 56 | return response; 57 | } 58 | } 59 | 60 | // 请求并编入缓存 61 | pinkLog('[Live2dRender] cacheFetch 请求并缓存 url ' + url) 62 | 63 | const orginalResponse = await fetch(url); 64 | const arraybuffer = await orginalResponse.arrayBuffer(); 65 | if (LAppDefine.LoadFromCache && LAppDefine.Live2dDB) { 66 | createItemIndexDB({ url, arraybuffer }); 67 | } 68 | return { 69 | arrayBuffer: async () => { 70 | return arraybuffer; 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | TypeScript HTML App 7 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | hello world 29 | 30 |
31 | 32 | 33 | 34 | 36 | 37 | 59 | 60 | -------------------------------------------------------------------------------- /src/db.ts: -------------------------------------------------------------------------------- 1 | import LAppDefine from "./lappdefine"; 2 | 3 | export function initialiseIndexDB(dbName: string, version: number, storeName?: string): Promise { 4 | return new Promise((resolve, reject) => { 5 | if (window.indexedDB === undefined) { 6 | resolve(undefined); 7 | } 8 | const dbRequest = indexedDB.open(dbName, version); 9 | dbRequest.onsuccess = (event) => { 10 | const db = (event.currentTarget as IDBOpenDBRequest).result; 11 | resolve(db); 12 | } 13 | dbRequest.onerror = (event) => { 14 | const error = (event.currentTarget as IDBOpenDBRequest).error; 15 | console.log('[live2d] 打开 indexDB 错误. ' + error.message); 16 | resolve(undefined); 17 | } 18 | dbRequest.onupgradeneeded = (event) => { 19 | const db = (event.currentTarget as IDBOpenDBRequest).result; 20 | if (storeName) { 21 | if (!db.objectStoreNames.contains(storeName)) { 22 | const store = db.createObjectStore(storeName, { autoIncrement: true }); 23 | store.createIndex('url', 'url', { unique: true }); 24 | store.createIndex('arraybuffer', 'arraybuffer'); 25 | } 26 | } 27 | } 28 | }); 29 | } 30 | 31 | export function selectItemIndexDB(key: string, value: string): Promise { 32 | return new Promise((resolve, reject) => { 33 | const tx = LAppDefine.Live2dDB.transaction('live2d', 'readonly'); 34 | const store = tx.objectStore('live2d'); 35 | const queryRequest = store.index(key).get(value); 36 | queryRequest.onsuccess = (event) => { 37 | const result = (event.currentTarget as IDBOpenDBRequest).result; 38 | if (result) { 39 | resolve(result as T); 40 | } else { 41 | resolve(undefined); 42 | } 43 | } 44 | 45 | queryRequest.onerror = (event) => { 46 | resolve(undefined); 47 | } 48 | }); 49 | } 50 | 51 | export function createItemIndexDB(value: T): Promise { 52 | return new Promise((resolve, reject) => { 53 | const tx = LAppDefine.Live2dDB.transaction('live2d', 'readwrite'); 54 | const store = tx.objectStore('live2d'); 55 | const queryRequest = store.add(value); 56 | 57 | queryRequest.onsuccess = () => resolve(true); 58 | queryRequest.onerror = () => resolve(false); 59 | }); 60 | } -------------------------------------------------------------------------------- /src/lappmessagebox.ts: -------------------------------------------------------------------------------- 1 | import LAppDefine from "./lappdefine"; 2 | 3 | 4 | export let s_instance: LAppMessageBox = null; 5 | export let messageBox: HTMLDivElement = null; 6 | 7 | export class LAppMessageBox { 8 | public static getInstance(): LAppMessageBox { 9 | if (s_instance == null) { 10 | s_instance = new LAppMessageBox(); 11 | } 12 | 13 | return s_instance; 14 | } 15 | 16 | public getMessageBox(): HTMLDivElement { 17 | if (this._messageBox == null) { 18 | this._messageBox = document.querySelector('#live2dMessageBox-content'); 19 | } 20 | return this._messageBox; 21 | } 22 | 23 | public initialize(canvas: HTMLCanvasElement): boolean { 24 | messageBox = document.createElement('div'); 25 | 26 | messageBox.id = LAppDefine.MessageBoxId; 27 | messageBox.style.position = 'fixed'; 28 | messageBox.style.padding = '10px'; 29 | messageBox.style.zIndex = '9999'; 30 | messageBox.style.display = 'flex'; 31 | messageBox.style.justifyContent = 'center'; 32 | 33 | messageBox.style.width = canvas.width + 'px'; 34 | messageBox.style.height = '20px'; 35 | 36 | if (LAppDefine.CanvasPosition === 'left') { 37 | messageBox.style.left = '0'; 38 | } else { 39 | messageBox.style.right = '0'; 40 | } 41 | 42 | messageBox.style.bottom = canvas.height + 50 + 'px'; 43 | messageBox.innerHTML = '
'; 44 | document.body.appendChild(messageBox); 45 | 46 | this.hideMessageBox(); 47 | return true; 48 | } 49 | 50 | public setMessage(message: string, duration: number=null) { 51 | const messageBox = this.getMessageBox(); 52 | 53 | this.hideMessageBox(); 54 | messageBox.textContent = message; 55 | 56 | setTimeout(() => { 57 | const wrapperDiv: HTMLDivElement = document.querySelector('#' + LAppDefine.MessageBoxId); 58 | wrapperDiv.style.bottom = (LAppDefine.CanvasSize === 'auto' ? 500: LAppDefine.CanvasSize.height) + messageBox.offsetHeight - 25 + 'px'; 59 | }, 10); 60 | 61 | this.revealMessageBox(); 62 | if (duration) { 63 | setTimeout(() => { 64 | this.hideMessageBox(); 65 | }, duration); 66 | } 67 | } 68 | 69 | // 隐藏对话框 70 | public hideMessageBox() { 71 | const messageBox = this.getMessageBox(); 72 | messageBox.classList.remove('live2dMessageBox-content-visible'); 73 | messageBox.classList.add('live2dMessageBox-content-hidden'); 74 | } 75 | 76 | // 展示对话框 77 | public revealMessageBox() { 78 | const messageBox = this.getMessageBox(); 79 | messageBox.classList.remove('live2dMessageBox-content-hidden'); 80 | messageBox.classList.add('live2dMessageBox-content-visible'); 81 | } 82 | 83 | _messageBox: HTMLDivElement = null 84 | } -------------------------------------------------------------------------------- /src/lappdefine.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright(c) Live2D Inc. All rights reserved. 3 | * 4 | * Use of this source code is governed by the Live2D Open Software license 5 | * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. 6 | */ 7 | 8 | import { LogLevel } from '@framework/live2dcubismframework'; 9 | 10 | /** 11 | * 配置参数 12 | */ 13 | 14 | interface ILAppDefine { 15 | Canvas: undefined | HTMLCanvasElement 16 | CanvasId: string 17 | MessageBoxId: string 18 | BackgroundRGBA: [number, number, number, number] 19 | 20 | CanvasSize: { width: number; height: number } | 'auto' 21 | LoadFromCache: boolean 22 | Live2dDB: IDBDatabase | undefined 23 | 24 | // 位置设置 25 | CanvasPosition: 'left' | 'right' 26 | 27 | // 画面参数 28 | ViewScale: number 29 | ViewMaxScale: number 30 | ViewMinScale: number 31 | 32 | ViewLogicalLeft: number 33 | ViewLogicalRight: number 34 | ViewLogicalBottom: number 35 | ViewLogicalTop: number 36 | 37 | ViewLogicalMaxLeft: number 38 | ViewLogicalMaxRight: number 39 | ViewLogicalMaxBottom: number 40 | ViewLogicalMaxTop: number 41 | 42 | // 资源路径,必须是model3文件的地址 43 | ResourcesPath: string 44 | 45 | // 结束按钮 46 | PowerImageName: string 47 | 48 | MotionGroupIdle: string // 49 | MotionGroupTapBody: string // 点击身体 50 | 51 | HitAreaNameHead: string 52 | HitAreaNameBody: string 53 | 54 | // 优先度 55 | PriorityNone: number 56 | PriorityIdle: number 57 | PriorityNormal: number 58 | PriorityForce: number 59 | 60 | // MOC3 一致性验证 61 | MOCConsistencyValidationEnable: boolean 62 | 63 | // 调试日志显示选项 64 | DebugLogEnable: boolean 65 | DebugTouchLogEnable: boolean 66 | 67 | // Framework 输出日志级别 68 | CubismLoggingLevel: LogLevel 69 | 70 | // 默认渲染大小 71 | RenderTargetWidth: number 72 | RenderTargetHeight: number 73 | 74 | showToolBox: boolean 75 | [property: string]: any 76 | } 77 | 78 | 79 | 80 | const LAppDefine: ILAppDefine = { 81 | Canvas: undefined, 82 | showToolBox: false, 83 | CanvasId: 'live2d', 84 | MessageBoxId: 'live2dMessageBox', 85 | BackgroundRGBA: [0.0, 0.0, 0.0, 0.0], 86 | CanvasSize: 'auto', 87 | LoadFromCache: false, 88 | Live2dDB: undefined, 89 | CanvasPosition: 'right', 90 | 91 | ViewScale: 1.0, 92 | ViewMaxScale: 2.0, 93 | ViewMinScale: 0.8, 94 | ViewLogicalLeft: - 1.0, 95 | ViewLogicalRight: 1.0, 96 | ViewLogicalBottom: - 1.0, 97 | ViewLogicalTop: 1.0, 98 | ViewLogicalMaxLeft: - 2.0, 99 | ViewLogicalMaxRight: 2.0, 100 | ViewLogicalMaxBottom: - 2.0, 101 | ViewLogicalMaxTop: 2.0, 102 | ResourcesPath: '', 103 | PowerImageName: '', 104 | MotionGroupIdle: 'Idle', 105 | MotionGroupTapBody: 'TapBody', 106 | HitAreaNameHead: 'Head', 107 | HitAreaNameBody: 'Body', 108 | 109 | PriorityNone: 0, 110 | PriorityIdle: 1, 111 | PriorityNormal: 2, 112 | PriorityForce: 3, 113 | 114 | MOCConsistencyValidationEnable: true, 115 | DebugLogEnable: true, 116 | DebugTouchLogEnable: false, 117 | CubismLoggingLevel: LogLevel.LogLevel_Verbose, 118 | 119 | RenderTargetWidth: 1900, 120 | RenderTargetHeight: 1000 121 | } 122 | 123 | 124 | export default LAppDefine; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Live2dRender](https://socialify.git.ci/LSTM-Kirigaya/Live2dRender/image?description=1&font=Jost&forks=1&issues=1&language=1&logo=https%3A%2F%2Fpic1.zhimg.com%2F80%2Fv2-16af50337a4ecec461aba0dade8e7403_1440w.png&name=1&pattern=Formal%20Invitation&pulls=1&stargazers=1&theme=Light) 2 | 3 |
4 | 5 | 👉 [Live2dRender 官方文档](https://document.kirigaya.cn/blogs/live2d-render/main.html) | [English Document](https://www.npmjs.com/package/live2d-render) 6 | 7 |
8 | 9 | # Live2dRender 10 | 11 | 适用于最新版本 Live2D 模型文件的 Javascript 渲染器。 12 | 13 | 申明: 14 | - 此项目仅适用于基于 webpack 构建的项目,如果是静态页面,自己想办法,反正也很简单。作者忙着打星际和写论文,没空回消息。 15 | - 此项目不适用于 IE 浏览器。 16 | - 这玩意儿是基于 `CubismSdkForWeb` 开发的,请不要商用! 17 | - 使用前请确保你的 Live2d 模型的文件都是英文,如果是中文可能会出问题。 18 | 19 | --- 20 | 21 | ## 普通用户请看 22 | 23 | 24 | ### 基本使用 (vue3 为例) 25 | 26 | 目前该项目支持任何 webpack 管理的打包项目,静态文件玩家可以参考下述的操作制作。下面以 vue3 项目为例,举例如何使用该库。 27 | 28 | #### 1. 新建 vue3 项目 并安装依赖 29 | 30 | 已有vue3项目直接跳过 31 | ```bash 32 | $ vue create test-live2d-render 33 | $ cd test-live2d-render 34 | ``` 35 | 36 | 安装 `live2d-render` 这个库: 37 | 38 | ``` 39 | $ npm install live2d-render 40 | ``` 41 | 42 | 43 | #### 2. 了解和准备 live2d 文件 44 | 45 | 先准备一个 live2d 模型,一个 live2d 模型通常是一个包含了如下几类文件的文件夹: 46 | 47 | - xxx.moc3 48 | - xxx.model3.json (配置文件,live2d 最先读取的就是这个文件,可以认为它是 live2d 模型的入口文件,里面列举了所有模型需要使用的静态资源的相对路径) 49 | - 其他 50 | 51 | 比如我的模型为一个小猫娘,文件夹为 cat,这个文件夹下包含了如下的文件: 52 | ``` 53 | /cat 54 | -| sdwhite cat b.model3.json 55 | -| SDwhite cat B.moc3 56 | -| xxx.exp3.json 57 | -| ... 58 | -| ... 59 | ``` 60 | 61 | > 模型版权申明:Lenore莱诺尔(B站:一个ayabe) 62 | 63 | 我把 `cat` 文件夹放在了 `./public` 文件夹下。那么我的模型的基本路径为:`./cat/sdwhite cat b.model3.json`,记住这个路径。 64 | 65 | #### 3. 使用 live2d-render 66 | 67 | 在 `./src/App.vue` 中: 68 | ```javascript 69 | 106 | ``` 107 | 108 | 109 | 运行项目: 110 | 111 | ```bash 112 | $ $Env:NODE_OPTIONS="--openssl-legacy-provider" 113 | $ npm run serve 114 | ``` 115 | 116 | 效果: 117 | 118 |
119 | 120 |
121 | 122 | #### 打包调优 123 | 124 | 如果你觉得 png 太大影响网络传输速度,可以考虑将 png 转化成 webp 后,然后把 model3.json 文件里面的 Textures 选项修改为 webp 的相对路径。 125 | 126 | --- 127 | 128 | ## 开发者请看 129 | 130 | ### 环境准备 131 | 132 | 如果你希望搭建开发环境,请: 133 | 1. 进入 [newest cubism-sdk](https://www.live2d.com/zh-CHS/download/cubism-sdk/download-web/) 134 | 2. 勾选 “同意软件使用授权协议以及隐私政策” 135 | 3. 找到 “Cubism SDK for Web”,选择 “下载最新版” 136 | 4. 下载后,解压为 `CubismSdkForWeb`. 137 | 138 | 然后 clone 本项目: 139 | ```bash 140 | $ cd ./CubismSdkForWeb/Samples/TypeScript 141 | $ git clone https://github.com/LSTM-Kirigaya/Live2dRender.git 142 | $ cd Live2dRender 143 | $ npm i 144 | ``` 145 | 146 | 然后,自己去折腾吧。 147 | 148 | --- 149 | 150 | ## Buy me a coffee 151 | 152 | Sponsor me in my own website: [https://kirigaya.cn/sponsor](https://kirigaya.cn/sponsor). 153 | 154 | --- 155 | 156 | ## CHANHELOG 157 | 158 | [2024.04.02] 159 | 160 | - 使用 indexDB 解决了 localStorage 只能存储 5MB 数据的问题 161 | 162 | [2024.04.02] 163 | 164 | - 将外部的两个库的载入融合进初始化内 165 | - 使用 `localStorage` 优化网络请求 166 | - 大幅度压缩库的大小: 2240 KB -> 190 KB 167 | 168 | [2023.10.01] 169 | 170 | - 完成主体工作 171 | -------------------------------------------------------------------------------- /src/touchmanager.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright(c) Live2D Inc. All rights reserved. 3 | * 4 | * Use of this source code is governed by the Live2D Open Software license 5 | * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. 6 | */ 7 | 8 | export class TouchManager { 9 | /** 10 | * コンストラクタ 11 | */ 12 | constructor() { 13 | this._startX = 0.0; 14 | this._startY = 0.0; 15 | this._lastX = 0.0; 16 | this._lastY = 0.0; 17 | this._lastX1 = 0.0; 18 | this._lastY1 = 0.0; 19 | this._lastX2 = 0.0; 20 | this._lastY2 = 0.0; 21 | this._lastTouchDistance = 0.0; 22 | this._deltaX = 0.0; 23 | this._deltaY = 0.0; 24 | this._scale = 1.0; 25 | this._touchSingle = false; 26 | this._flipAvailable = false; 27 | } 28 | 29 | public getCenterX(): number { 30 | return this._lastX; 31 | } 32 | 33 | public getCenterY(): number { 34 | return this._lastY; 35 | } 36 | 37 | public getDeltaX(): number { 38 | return this._deltaX; 39 | } 40 | 41 | public getDeltaY(): number { 42 | return this._deltaY; 43 | } 44 | 45 | public getStartX(): number { 46 | return this._startX; 47 | } 48 | 49 | public getStartY(): number { 50 | return this._startY; 51 | } 52 | 53 | public getScale(): number { 54 | return this._scale; 55 | } 56 | 57 | public getX(): number { 58 | return this._lastX; 59 | } 60 | 61 | public getY(): number { 62 | return this._lastY; 63 | } 64 | 65 | public getX1(): number { 66 | return this._lastX1; 67 | } 68 | 69 | public getY1(): number { 70 | return this._lastY1; 71 | } 72 | 73 | public getX2(): number { 74 | return this._lastX2; 75 | } 76 | 77 | public getY2(): number { 78 | return this._lastY2; 79 | } 80 | 81 | public isSingleTouch(): boolean { 82 | return this._touchSingle; 83 | } 84 | 85 | public isFlickAvailable(): boolean { 86 | return this._flipAvailable; 87 | } 88 | 89 | public disableFlick(): void { 90 | this._flipAvailable = false; 91 | } 92 | 93 | /** 94 | * タッチ開始時イベント 95 | * @param deviceX タッチした画面のxの値 96 | * @param deviceY タッチした画面のyの値 97 | */ 98 | public touchesBegan(deviceX: number, deviceY: number): void { 99 | this._lastX = deviceX; 100 | this._lastY = deviceY; 101 | this._startX = deviceX; 102 | this._startY = deviceY; 103 | this._lastTouchDistance = -1.0; 104 | this._flipAvailable = true; 105 | this._touchSingle = true; 106 | } 107 | 108 | /** 109 | * ドラッグ時のイベント 110 | * @param deviceX タッチした画面のxの値 111 | * @param deviceY タッチした画面のyの値 112 | */ 113 | public touchesMoved(deviceX: number, deviceY: number): void { 114 | this._lastX = deviceX; 115 | this._lastY = deviceY; 116 | this._lastTouchDistance = -1.0; 117 | this._touchSingle = true; 118 | } 119 | 120 | /** 121 | * フリックの距離測定 122 | * @return フリック距離 123 | */ 124 | public getFlickDistance(): number { 125 | return this.calculateDistance( 126 | this._startX, 127 | this._startY, 128 | this._lastX, 129 | this._lastY 130 | ); 131 | } 132 | 133 | /** 134 | * 点1から点2への距離を求める 135 | * 136 | * @param x1 1つ目のタッチした画面のxの値 137 | * @param y1 1つ目のタッチした画面のyの値 138 | * @param x2 2つ目のタッチした画面のxの値 139 | * @param y2 2つ目のタッチした画面のyの値 140 | */ 141 | public calculateDistance( 142 | x1: number, 143 | y1: number, 144 | x2: number, 145 | y2: number 146 | ): number { 147 | return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); 148 | } 149 | 150 | /** 151 | * 2つ目の値から、移動量を求める。 152 | * 違う方向の場合は移動量0。同じ方向の場合は、絶対値が小さい方の値を参照する。 153 | * 154 | * @param v1 1つ目の移動量 155 | * @param v2 2つ目の移動量 156 | * 157 | * @return 小さい方の移動量 158 | */ 159 | public calculateMovingAmount(v1: number, v2: number): number { 160 | if (v1 > 0.0 != v2 > 0.0) { 161 | return 0.0; 162 | } 163 | 164 | const sign: number = v1 > 0.0 ? 1.0 : -1.0; 165 | const absoluteValue1 = Math.abs(v1); 166 | const absoluteValue2 = Math.abs(v2); 167 | return ( 168 | sign * (absoluteValue1 < absoluteValue2 ? absoluteValue1 : absoluteValue2) 169 | ); 170 | } 171 | 172 | _startY: number; // タッチを開始した時のxの値 173 | _startX: number; // タッチを開始した時のyの値 174 | _lastX: number; // シングルタッチ時のxの値 175 | _lastY: number; // シングルタッチ時のyの値 176 | _lastX1: number; // ダブルタッチ時の一つ目のxの値 177 | _lastY1: number; // ダブルタッチ時の一つ目のyの値 178 | _lastX2: number; // ダブルタッチ時の二つ目のxの値 179 | _lastY2: number; // ダブルタッチ時の二つ目のyの値 180 | _lastTouchDistance: number; // 2本以上でタッチしたときの指の距離 181 | _deltaX: number; // 前回の値から今回の値へのxの移動距離。 182 | _deltaY: number; // 前回の値から今回の値へのyの移動距離。 183 | _scale: number; // このフレームで掛け合わせる拡大率。拡大操作中以外は1。 184 | _touchSingle: boolean; // シングルタッチ時はtrue 185 | _flipAvailable: boolean; // フリップが有効かどうか 186 | } 187 | -------------------------------------------------------------------------------- /src/svg.ts: -------------------------------------------------------------------------------- 1 | export const collapseIconRight = ` 2 | 3 | `; 4 | 5 | export const collapseIconLeft = ` 6 | 7 | `; 8 | 9 | export const expressionIcon = ` 10 | 11 | `; 12 | 13 | export const reloadIcon = ` 14 | 15 | `; 16 | 17 | export const starIcon = ` 18 | 19 | `; 20 | 21 | export const catIcon = ` 22 | 23 | `; -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright(c) Live2D Inc. All rights reserved. 3 | * 4 | * Use of this source code is governed by the Live2D Open Software license 5 | * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. 6 | */ 7 | 8 | import { LAppDelegate } from './lappdelegate'; 9 | import LAppDefine from './lappdefine'; 10 | import { LAppLive2DManager } from './lapplive2dmanager'; 11 | import { LAppMessageBox } from './lappmessagebox'; 12 | import { initialiseIndexDB } from './db'; 13 | import { addToolBox } from './toolbox'; 14 | 15 | interface Live2dRenderConfig { 16 | CanvasId?: string 17 | CanvasSize?: { height: number, width: number } | 'auto' 18 | CanvasPosition?: 'left' | 'right' 19 | BackgroundRGBA?: [number, number, number, number] 20 | ResourcesPath?: string 21 | LoadFromCache?: boolean 22 | ShowToolBox?: boolean 23 | MinifiedJSUrl: string 24 | Live2dCubismcoreUrl: string 25 | } 26 | 27 | async function launchLive2d() { 28 | const live2dModel = LAppDelegate.getInstance(); 29 | const ok = live2dModel.initialize(); 30 | if (!ok) { 31 | console.log('初始化失败,退出'); 32 | } else { 33 | // just run 34 | live2dModel.run(); 35 | // show 36 | if (LAppDefine.Canvas && LAppDefine.ShowToolBox) { 37 | // 下一个 tick 中注册 38 | setTimeout(() => { 39 | LAppDefine.Canvas.style.opacity = '1'; 40 | addToolBox(); 41 | }, 500); 42 | } 43 | } 44 | } 45 | 46 | 47 | function setExpression(name: string) { 48 | const manager = LAppLive2DManager.getInstance(); 49 | if (manager) { 50 | // 默认拿第一个模型 51 | const model = manager.getModel(0); 52 | model.setExpression(name); 53 | } 54 | } 55 | 56 | function setRandomExpression() { 57 | const manager = LAppLive2DManager.getInstance(); 58 | if (manager) { 59 | manager.model.setRandomExpression(); 60 | } 61 | } 62 | 63 | function setMessageBox(message: string, duration: number) { 64 | const messageBox = LAppMessageBox.getInstance(); 65 | messageBox.setMessage(message, duration); 66 | } 67 | 68 | function hideMessageBox() { 69 | const messageBox = LAppMessageBox.getInstance(); 70 | messageBox.hideMessageBox(); 71 | } 72 | 73 | function revealMessageBox() { 74 | const messageBox = LAppMessageBox.getInstance(); 75 | messageBox.revealMessageBox(); 76 | } 77 | 78 | 79 | /** 80 | * 81 | * @param src 82 | * @returns 83 | */ 84 | function load(src: string): Promise { 85 | const script = document.createElement('script'); 86 | script.src = src; 87 | 88 | return new Promise((resolve, reject) => { 89 | script.onload = () => { 90 | resolve(); 91 | }; 92 | script.onerror = (error) => { 93 | reject(error); 94 | }; 95 | document.head.appendChild(script); 96 | }); 97 | } 98 | 99 | async function loadLibs(urls: string[]) { 100 | const ps = []; 101 | 102 | for (const url of urls) { 103 | ps.push(load(url)); 104 | } 105 | 106 | for (const p of ps) { 107 | await p; 108 | } 109 | } 110 | 111 | 112 | async function initializeLive2D(config: Live2dRenderConfig) { 113 | // 如果有 live2d 就不创建了 114 | const els = document.querySelectorAll('#live2d'); 115 | if (els.length >= 1) { 116 | return; 117 | } 118 | 119 | if (config.MinifiedJSUrl === undefined) { 120 | config.MinifiedJSUrl = 'https://unpkg.com/core-js-bundle@3.6.1/minified.js'; 121 | } 122 | if (config.Live2dCubismcoreUrl === undefined) { 123 | config.Live2dCubismcoreUrl = 'https://cubism.live2d.com/sdk-web/cubismcore/live2dcubismcore.min.js'; 124 | } 125 | if (config.ShowToolBox === undefined) { 126 | config.ShowToolBox = false; 127 | } 128 | if (config.CanvasPosition === undefined) { 129 | config.CanvasPosition = 'right'; 130 | } 131 | 132 | LAppDefine.ShowToolBox = config.ShowToolBox; 133 | LAppDefine.CanvasPosition = config.CanvasPosition; 134 | 135 | await loadLibs([ 136 | config.MinifiedJSUrl, 137 | config.Live2dCubismcoreUrl 138 | ]); 139 | 140 | if (config.CanvasId) { 141 | LAppDefine.CanvasId = config.CanvasId; 142 | } 143 | if (config.CanvasSize) { 144 | LAppDefine.CanvasSize = config.CanvasSize; 145 | } 146 | if (config.BackgroundRGBA) { 147 | LAppDefine.BackgroundRGBA = config.BackgroundRGBA; 148 | } 149 | if (config.ResourcesPath) { 150 | LAppDefine.ResourcesPath = config.ResourcesPath; 151 | } 152 | if (config.LoadFromCache && window.indexedDB) { 153 | LAppDefine.LoadFromCache = config.LoadFromCache; 154 | // 初始化缓存数据库 155 | const db = await initialiseIndexDB('db', 1, 'live2d'); 156 | LAppDefine.Live2dDB = db; 157 | } 158 | 159 | return launchLive2d(); 160 | } 161 | 162 | if (window) { 163 | /** 164 | * 終了時の処理 165 | */ 166 | window.onbeforeunload = (): void => { 167 | const live2dModel = LAppDelegate.getInstance(); 168 | if (live2dModel) { 169 | live2dModel.release(); 170 | } 171 | } 172 | 173 | /** 174 | * Process when changing screen size. 175 | */ 176 | window.onresize = () => { 177 | const live2dModel = LAppDelegate.getInstance(); 178 | if (live2dModel && LAppDefine.CanvasSize === 'auto') { 179 | live2dModel.onResize(); 180 | } 181 | }; 182 | } 183 | 184 | 185 | export { 186 | initializeLive2D, 187 | setExpression, 188 | setMessageBox, 189 | setRandomExpression, 190 | hideMessageBox, 191 | revealMessageBox 192 | }; -------------------------------------------------------------------------------- /src/lapptexturemanager.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright(c) Live2D Inc. All rights reserved. 3 | * 4 | * Use of this source code is governed by the Live2D Open Software license 5 | * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. 6 | */ 7 | 8 | import { csmVector, iterator } from '@framework/type/csmvector'; 9 | 10 | import { gl } from './lappdelegate'; 11 | import LAppDefine from './lappdefine'; 12 | import { cacheFetch } from './cache'; 13 | 14 | /** 15 | * テクスチャ管理クラス 16 | * 画像読み込み、管理を行うクラス。 17 | */ 18 | export class LAppTextureManager { 19 | /** 20 | * コンストラクタ 21 | */ 22 | constructor() { 23 | this._textures = new csmVector(); 24 | } 25 | 26 | /** 27 | * 解放する。 28 | */ 29 | public release(): void { 30 | for ( 31 | let ite: iterator = this._textures.begin(); 32 | ite.notEqual(this._textures.end()); 33 | ite.preIncrement() 34 | ) { 35 | gl.deleteTexture(ite.ptr().id); 36 | } 37 | this._textures = null; 38 | } 39 | 40 | /** 41 | * 从远程载入 材质 文件,可以是 png,也可以是 webp 42 | * 43 | * @param fileName 読み込む画像ファイルパス名 44 | * @param usePremultiply Premult処理を有効にするか 45 | * @return 画像情報、読み込み失敗時はnullを返す 46 | */ 47 | public async createTextureFromFile( 48 | fileName: string, 49 | usePremultiply: boolean, 50 | callback: (textureInfo: TextureInfo) => void 51 | ): Promise { 52 | // search loaded texture already 53 | for ( 54 | let ite: iterator = this._textures.begin(); 55 | ite.notEqual(this._textures.end()); 56 | ite.preIncrement() 57 | ) { 58 | if ( 59 | ite.ptr().fileName == fileName && 60 | ite.ptr().usePremultply == usePremultiply 61 | ) { 62 | // 2回目以降はキャッシュが使用される(待ち時間なし) 63 | // WebKitでは同じImageのonloadを再度呼ぶには再インスタンスが必要 64 | // 詳細:https://stackoverflow.com/a/5024181 65 | ite.ptr().img = new Image(); 66 | ite.ptr().img.onload = (): void => callback(ite.ptr()); 67 | ite.ptr().img.src = fileName; 68 | return; 69 | } 70 | } 71 | 72 | // データのオンロードをトリガーにする 73 | const img = new Image(); 74 | 75 | img.onload = (): void => { 76 | // テクスチャオブジェクトの作成 77 | const tex: WebGLTexture = gl.createTexture(); 78 | 79 | // テクスチャを選択 80 | gl.bindTexture(gl.TEXTURE_2D, tex); 81 | 82 | // テクスチャにピクセルを書き込む 83 | gl.texParameteri( 84 | gl.TEXTURE_2D, 85 | gl.TEXTURE_MIN_FILTER, 86 | gl.LINEAR_MIPMAP_LINEAR 87 | ); 88 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 89 | 90 | // Premult処理を行わせる 91 | if (usePremultiply) { 92 | gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 1); 93 | } 94 | 95 | // テクスチャにピクセルを書き込む 96 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img); 97 | 98 | // ミップマップを生成 99 | gl.generateMipmap(gl.TEXTURE_2D); 100 | 101 | // テクスチャをバインド 102 | gl.bindTexture(gl.TEXTURE_2D, null); 103 | 104 | const textureInfo: TextureInfo = new TextureInfo(); 105 | if (textureInfo != null) { 106 | textureInfo.fileName = fileName; 107 | textureInfo.width = img.width; 108 | textureInfo.height = img.height; 109 | textureInfo.id = tex; 110 | textureInfo.img = img; 111 | textureInfo.usePremultply = usePremultiply; 112 | this._textures.pushBack(textureInfo); 113 | } 114 | 115 | callback(textureInfo); 116 | }; 117 | 118 | const imageFormat = fileName.split('.').at(-1).toLowerCase(); 119 | const response = await cacheFetch(fileName); 120 | const arraybuffer = await response.arrayBuffer(); 121 | const blob = new Blob([arraybuffer], { type: 'image/' + imageFormat }); 122 | const url = URL.createObjectURL(blob); 123 | img.src = url; 124 | } 125 | 126 | /** 127 | * 画像の解放 128 | * 129 | * 配列に存在する画像全てを解放する。 130 | */ 131 | public releaseTextures(): void { 132 | for (let i = 0; i < this._textures.getSize(); i++) { 133 | this._textures.set(i, null); 134 | } 135 | 136 | this._textures.clear(); 137 | } 138 | 139 | /** 140 | * 画像の解放 141 | * 142 | * 指定したテクスチャの画像を解放する。 143 | * @param texture 解放するテクスチャ 144 | */ 145 | public releaseTextureByTexture(texture: WebGLTexture): void { 146 | for (let i = 0; i < this._textures.getSize(); i++) { 147 | if (this._textures.at(i).id != texture) { 148 | continue; 149 | } 150 | 151 | this._textures.set(i, null); 152 | this._textures.remove(i); 153 | break; 154 | } 155 | } 156 | 157 | /** 158 | * 画像の解放 159 | * 160 | * 指定した名前の画像を解放する。 161 | * @param fileName 解放する画像ファイルパス名 162 | */ 163 | public releaseTextureByFilePath(fileName: string): void { 164 | for (let i = 0; i < this._textures.getSize(); i++) { 165 | if (this._textures.at(i).fileName == fileName) { 166 | this._textures.set(i, null); 167 | this._textures.remove(i); 168 | break; 169 | } 170 | } 171 | } 172 | 173 | _textures: csmVector; 174 | } 175 | 176 | /** 177 | * 画像情報構造体 178 | */ 179 | export class TextureInfo { 180 | img: HTMLImageElement; // 画像 181 | id: WebGLTexture = null; // テクスチャ 182 | width = 0; // 横幅 183 | height = 0; // 高さ 184 | usePremultply: boolean; // Premult処理を有効にするか 185 | fileName: string; // ファイル名 186 | } 187 | -------------------------------------------------------------------------------- /src/lappview.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright(c) Live2D Inc. All rights reserved. 3 | * 4 | * Use of this source code is governed by the Live2D Open Software license 5 | * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. 6 | */ 7 | 8 | import { CubismMatrix44 } from '@framework/math/cubismmatrix44'; 9 | import { CubismViewMatrix } from '@framework/math/cubismviewmatrix'; 10 | 11 | import LAppDefine from './lappdefine'; 12 | import { canvas, gl, LAppDelegate } from './lappdelegate'; 13 | import { LAppLive2DManager } from './lapplive2dmanager'; 14 | import { LAppPal } from './lapppal'; 15 | import { TouchManager } from './touchmanager'; 16 | 17 | /** 18 | * 描画クラス。 19 | */ 20 | export class LAppView { 21 | /** 22 | * コンストラクタ 23 | */ 24 | constructor() { 25 | this._programId = null; 26 | 27 | // タッチ関係のイベント管理 28 | this._touchManager = new TouchManager(); 29 | 30 | // デバイス座標からスクリーン座標に変換するための 31 | this._deviceToScreen = new CubismMatrix44(); 32 | 33 | // 画面の表示の拡大縮小や移動の変換を行う行列 34 | this._viewMatrix = new CubismViewMatrix(); 35 | } 36 | 37 | /** 38 | * 初期化する。 39 | */ 40 | public initialize(): void { 41 | const { width, height } = canvas; 42 | 43 | const ratio: number = width / height; 44 | const left: number = -ratio; 45 | const right: number = ratio; 46 | const bottom: number = LAppDefine.ViewLogicalLeft; 47 | const top: number = LAppDefine.ViewLogicalRight; 48 | 49 | this._viewMatrix.setScreenRect(left, right, bottom, top); // デバイスに対応する画面の範囲。 Xの左端、Xの右端、Yの下端、Yの上端 50 | this._viewMatrix.scale(LAppDefine.ViewScale, LAppDefine.ViewScale); 51 | 52 | this._deviceToScreen.loadIdentity(); 53 | if (width > height) { 54 | const screenW: number = Math.abs(right - left); 55 | this._deviceToScreen.scaleRelative(screenW / width, -screenW / width); 56 | } else { 57 | const screenH: number = Math.abs(top - bottom); 58 | this._deviceToScreen.scaleRelative(screenH / height, -screenH / height); 59 | } 60 | this._deviceToScreen.translateRelative(-width * 0.5, -height * 0.5); 61 | 62 | // 表示範囲の設定 63 | this._viewMatrix.setMaxScale(LAppDefine.ViewMaxScale); // 限界拡張率 64 | this._viewMatrix.setMinScale(LAppDefine.ViewMinScale); // 限界縮小率 65 | 66 | // 表示できる最大範囲 67 | this._viewMatrix.setMaxScreenRect( 68 | LAppDefine.ViewLogicalMaxLeft, 69 | LAppDefine.ViewLogicalMaxRight, 70 | LAppDefine.ViewLogicalMaxBottom, 71 | LAppDefine.ViewLogicalMaxTop 72 | ); 73 | } 74 | 75 | /** 76 | * 解放する 77 | */ 78 | public release(): void { 79 | this._viewMatrix = null; 80 | this._touchManager = null; 81 | this._deviceToScreen = null; 82 | 83 | gl.deleteProgram(this._programId); 84 | this._programId = null; 85 | } 86 | 87 | /** 88 | * 描画する。 89 | */ 90 | public render(): void { 91 | gl.useProgram(this._programId); 92 | gl.flush(); 93 | 94 | const live2DManager: LAppLive2DManager = LAppLive2DManager.getInstance(); 95 | 96 | live2DManager.setViewMatrix(this._viewMatrix); 97 | 98 | live2DManager.onUpdate(); 99 | } 100 | 101 | /** 102 | * 画像の初期化を行う。 103 | */ 104 | public initializeSprite(): void { 105 | const width: number = canvas.width; 106 | const height: number = canvas.height; 107 | 108 | const textureManager = LAppDelegate.getInstance().getTextureManager(); 109 | const resourcesPath = LAppDefine.ResourcesPath; 110 | 111 | 112 | // シェーダーを作成 113 | if (this._programId == null) { 114 | this._programId = LAppDelegate.getInstance().createShader(); 115 | } 116 | } 117 | 118 | /** 119 | * タッチされた時に呼ばれる。 120 | * 121 | * @param pointX スクリーンX座標 122 | * @param pointY スクリーンY座標 123 | */ 124 | public onTouchesBegan(pointX: number, pointY: number): void { 125 | this._touchManager.touchesBegan(pointX, pointY); 126 | } 127 | 128 | /** 129 | * タッチしているときにポインタが動いたら呼ばれる。 130 | * 131 | * @param pointX スクリーンX座標 132 | * @param pointY スクリーンY座標 133 | */ 134 | public onTouchesMoved(pointX: number, pointY: number): void { 135 | const viewX: number = this.transformViewX(this._touchManager.getX()); 136 | const viewY: number = this.transformViewY(this._touchManager.getY()); 137 | 138 | this._touchManager.touchesMoved(pointX, pointY); 139 | 140 | const live2DManager: LAppLive2DManager = LAppLive2DManager.getInstance(); 141 | live2DManager.onDrag(viewX, viewY); 142 | } 143 | 144 | /** 145 | * タッチが終了したら呼ばれる。 146 | * 147 | * @param pointX スクリーンX座標 148 | * @param pointY スクリーンY座標 149 | */ 150 | public onTouchesEnded(pointX: number, pointY: number): void { 151 | // タッチ終了 152 | const live2DManager: LAppLive2DManager = LAppLive2DManager.getInstance(); 153 | live2DManager.onDrag(0.0, 0.0); 154 | 155 | { 156 | // シングルタップ 157 | const x: number = this._deviceToScreen.transformX( 158 | this._touchManager.getX() 159 | ); // 論理座標変換した座標を取得。 160 | const y: number = this._deviceToScreen.transformY( 161 | this._touchManager.getY() 162 | ); // 論理座標変化した座標を取得。 163 | 164 | if (LAppDefine.DebugTouchLogEnable) { 165 | LAppPal.printMessage(`[APP]touchesEnded x: ${x} y: ${y}`); 166 | } 167 | live2DManager.onTap(x, y); 168 | } 169 | } 170 | 171 | /** 172 | * X座標をView座標に変換する。 173 | * 174 | * @param deviceX デバイスX座標 175 | */ 176 | public transformViewX(deviceX: number): number { 177 | const screenX: number = this._deviceToScreen.transformX(deviceX); // 論理座標変換した座標を取得。 178 | return this._viewMatrix.invertTransformX(screenX); // 拡大、縮小、移動後の値。 179 | } 180 | 181 | /** 182 | * Y座標をView座標に変換する。 183 | * 184 | * @param deviceY デバイスY座標 185 | */ 186 | public transformViewY(deviceY: number): number { 187 | const screenY: number = this._deviceToScreen.transformY(deviceY); // 論理座標変換した座標を取得。 188 | return this._viewMatrix.invertTransformY(screenY); 189 | } 190 | 191 | /** 192 | * X座標をScreen座標に変換する。 193 | * @param deviceX デバイスX座標 194 | */ 195 | public transformScreenX(deviceX: number): number { 196 | return this._deviceToScreen.transformX(deviceX); 197 | } 198 | 199 | /** 200 | * Y座標をScreen座標に変換する。 201 | * 202 | * @param deviceY デバイスY座標 203 | */ 204 | public transformScreenY(deviceY: number): number { 205 | return this._deviceToScreen.transformY(deviceY); 206 | } 207 | 208 | _touchManager: TouchManager; // タッチマネージャー 209 | _deviceToScreen: CubismMatrix44; // デバイスからスクリーンへの行列 210 | _viewMatrix: CubismViewMatrix; // viewMatrix 211 | _programId: WebGLProgram; // シェーダID 212 | _changeModel: boolean; // モデル切り替えフラグ 213 | _isClick: boolean; // クリック中 214 | } 215 | -------------------------------------------------------------------------------- /src/lapplive2dmanager.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright(c) Live2D Inc. All rights reserved. 3 | * 4 | * Use of this source code is governed by the Live2D Open Software license 5 | * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. 6 | */ 7 | 8 | import { CubismMatrix44 } from '@framework/math/cubismmatrix44'; 9 | import { ACubismMotion } from '@framework/motion/acubismmotion'; 10 | import { csmVector } from '@framework/type/csmvector'; 11 | 12 | import LAppDefine from './lappdefine'; 13 | import { canvas } from './lappdelegate'; 14 | import { LAppModel } from './lappmodel'; 15 | import { LAppPal } from './lapppal'; 16 | import { pinkLog, redLog } from './utils'; 17 | import { reloadToolBox } from './toolbox'; 18 | 19 | // 单例模式全局变量 20 | export let s_instance: LAppLive2DManager = null; 21 | 22 | /** 23 | * サンプルアプリケーションにおいてCubismModelを管理するクラス 24 | * モデル生成と破棄、タップイベントの処理、モデル切り替えを行う。 25 | */ 26 | export class LAppLive2DManager { 27 | /** 28 | * クラスのインスタンス(シングルトン)を返す。 29 | * インスタンスが生成されていない場合は内部でインスタンスを生成する。 30 | * 31 | * @return クラスのインスタンス 32 | */ 33 | public static getInstance(): LAppLive2DManager { 34 | if (s_instance == null) { 35 | s_instance = new LAppLive2DManager(); 36 | } 37 | 38 | return s_instance; 39 | } 40 | 41 | /** 42 | * クラスのインスタンス(シングルトン)を解放する。 43 | */ 44 | public static releaseInstance(): void { 45 | if (s_instance != null) { 46 | s_instance = void 0; 47 | } 48 | 49 | s_instance = null; 50 | } 51 | 52 | /** 53 | * 現在のシーンで保持しているモデルを返す。 54 | * 55 | * @param no モデルリストのインデックス値 56 | * @return モデルのインスタンスを返す。インデックス値が範囲外の場合はNULLを返す。 57 | */ 58 | public getModel(no: number): LAppModel { 59 | if (no < this._models.getSize()) { 60 | return this._models.at(no); 61 | } 62 | 63 | return null; 64 | } 65 | 66 | public get model(): LAppModel { 67 | return this.getModel(0); 68 | } 69 | 70 | /** 71 | * 現在のシーンで保持しているすべてのモデルを解放する 72 | */ 73 | public releaseAllModel(): void { 74 | for (let i = 0; i < this._models.getSize(); i++) { 75 | this._models.at(i).release(); 76 | this._models.set(i, null); 77 | } 78 | 79 | this._models.clear(); 80 | } 81 | 82 | /** 83 | * 画面をドラッグした時の処理 84 | * 85 | * @param x 画面のX座標 86 | * @param y 画面のY座標 87 | */ 88 | public onDrag(x: number, y: number): void { 89 | for (let i = 0; i < this._models.getSize(); i++) { 90 | const model: LAppModel = this.getModel(i); 91 | 92 | if (model) { 93 | model.setDragging(x, y); 94 | } 95 | } 96 | } 97 | 98 | /** 99 | * 画面をタップした時の処理 100 | * 101 | * @param x 画面のX座標 102 | * @param y 画面のY座標 103 | */ 104 | public onTap(x: number, y: number): void { 105 | if (LAppDefine.DebugLogEnable) { 106 | LAppPal.printMessage( 107 | `[APP]tap point: {x: ${x.toFixed(2)} y: ${y.toFixed(2)}}` 108 | ); 109 | } 110 | 111 | for (let i = 0; i < this._models.getSize(); i++) { 112 | if (this._models.at(i).hitTest(LAppDefine.HitAreaNameHead, x, y)) { 113 | if (LAppDefine.DebugLogEnable) { 114 | LAppPal.printMessage( 115 | `[APP]hit area: [${LAppDefine.HitAreaNameHead}]` 116 | ); 117 | } 118 | this._models.at(i).setRandomExpression(); 119 | } else if (this._models.at(i).hitTest(LAppDefine.HitAreaNameBody, x, y)) { 120 | if (LAppDefine.DebugLogEnable) { 121 | LAppPal.printMessage( 122 | `[APP]hit area: [${LAppDefine.HitAreaNameBody}]` 123 | ); 124 | } 125 | this._models 126 | .at(i) 127 | .startRandomMotion( 128 | LAppDefine.MotionGroupTapBody, 129 | LAppDefine.PriorityNormal, 130 | this._finishedMotion 131 | ); 132 | } 133 | } 134 | } 135 | 136 | /** 137 | * 画面を更新するときの処理 138 | * モデルの更新処理及び描画処理を行う 139 | */ 140 | public onUpdate(): void { 141 | const { width, height } = canvas; 142 | 143 | const modelCount: number = this._models.getSize(); 144 | 145 | for (let i = 0; i < modelCount; ++i) { 146 | const projection: CubismMatrix44 = new CubismMatrix44(); 147 | const model: LAppModel = this.getModel(i); 148 | 149 | if (model.getModel()) { 150 | if (model.getModel().getCanvasWidth() > 1.0 && width < height) { 151 | // 横に長いモデルを縦長ウィンドウに表示する際モデルの横サイズでscaleを算出する 152 | model.getModelMatrix().setWidth(2.0); 153 | projection.scale(1.0, width / height); 154 | } else { 155 | projection.scale(height / width, 1.0); 156 | } 157 | 158 | // 必要があればここで乗算 159 | if (this._viewMatrix != null) { 160 | projection.multiplyByMatrix(this._viewMatrix); 161 | } 162 | } 163 | 164 | model.update(); 165 | model.draw(projection); // 参照渡しなのでprojectionは変質する。 166 | } 167 | } 168 | 169 | /** 170 | * 次のシーンに切りかえる 171 | * サンプルアプリケーションではモデルセットの切り替えを行う。 172 | */ 173 | public nextScene(): void { 174 | 175 | } 176 | 177 | private getModelPath(modelJsonPath: string): string { 178 | if (!modelJsonPath.includes('/')) { 179 | return '.'; 180 | } 181 | const pathComponents: string[] = modelJsonPath.split('/'); 182 | pathComponents.pop(); 183 | const modelDir = pathComponents.join('/'); 184 | return modelDir + '/'; 185 | } 186 | 187 | /** 188 | * @description 重新加载 Live2d 模型 189 | * @returns 190 | */ 191 | public async loadLive2dModel(): Promise { 192 | const modelJsonPath = LAppDefine.ResourcesPath; 193 | if (!modelJsonPath.endsWith('.model3.json')) { 194 | redLog('无法加载模型!模型资源路径的结尾必须是.model3.json!'); 195 | return; 196 | } 197 | 198 | const modelPath = this.getModelPath(modelJsonPath); 199 | 200 | // 释放所有模型,此操作会清空 this._models 201 | this.releaseAllModel(); 202 | 203 | // 装载新的模型,并加载新的资源 204 | const appModel = new LAppModel(); 205 | 206 | // TODO: 支持更多的模型 207 | this._models.pushBack(appModel); 208 | 209 | // 装载模型 210 | await appModel.loadAssets(modelPath, modelJsonPath); 211 | 212 | // 下一个 tick 中,重新制作工具栏,更新表情 213 | setTimeout(() => { 214 | reloadToolBox(); 215 | }, 500); 216 | 217 | pinkLog("[Live2dRender] 模型重载完成,重载路径: " + modelPath); 218 | } 219 | 220 | public setViewMatrix(m: CubismMatrix44) { 221 | for (let i = 0; i < 16; i++) { 222 | this._viewMatrix.getArray()[i] = m.getArray()[i]; 223 | } 224 | } 225 | 226 | /** 227 | * コンストラクタ 228 | */ 229 | constructor() { 230 | this._viewMatrix = new CubismMatrix44(); 231 | this._models = new csmVector(); 232 | this.loadLive2dModel(); 233 | } 234 | 235 | _viewMatrix: CubismMatrix44; // モデル描画に用いるview行列 236 | _models: csmVector; // モデルインスタンスのコンテナ 237 | _sceneIndex: number; // 表示するシーンのインデックス値 238 | // モーション再生終了のコールバック関数 239 | _finishedMotion = (self: ACubismMotion): void => { 240 | LAppPal.printMessage('Motion Finished:'); 241 | console.log(self); 242 | }; 243 | } 244 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /src/lappdelegate.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | /** 3 | * Copyright(c) Live2D Inc. All rights reserved. 4 | * 5 | * Use of this source code is governed by the Live2D Open Software license 6 | * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. 7 | */ 8 | 9 | import { CubismFramework, Option } from '@framework/live2dcubismframework'; 10 | 11 | import LAppDefine from './lappdefine'; 12 | import { LAppLive2DManager } from './lapplive2dmanager'; 13 | import { LAppPal } from './lapppal'; 14 | import { LAppTextureManager } from './lapptexturemanager'; 15 | import { LAppView } from './lappview'; 16 | import { LAppMessageBox } from './lappmessagebox'; 17 | 18 | export let canvas: HTMLCanvasElement = null; 19 | export let messageBox: HTMLDivElement = null; 20 | export let s_instance: LAppDelegate = null; 21 | export let gl: WebGLRenderingContext = null; 22 | export let frameBuffer: WebGLFramebuffer = null; 23 | 24 | export type CanvasSize = { width: number; height: number } | 'auto'; 25 | 26 | 27 | /** 28 | * アプリケーションクラス。 29 | * Cubism SDKの管理を行う。 30 | */ 31 | export class LAppDelegate { 32 | /** 33 | * クラスのインスタンス(シングルトン)を返す。 34 | * インスタンスが生成されていない場合は内部でインスタンスを生成する。 35 | * 36 | * @return クラスのインスタンス 37 | */ 38 | public static getInstance(): LAppDelegate { 39 | if (s_instance == null) { 40 | s_instance = new LAppDelegate(); 41 | } 42 | 43 | return s_instance; 44 | } 45 | 46 | /** 47 | * クラスのインスタンス(シングルトン)を解放する。 48 | */ 49 | public static releaseInstance(): void { 50 | if (s_instance != null) { 51 | s_instance.release(); 52 | } 53 | 54 | s_instance = null; 55 | } 56 | 57 | /** 58 | * APPに必要な物を初期化する。 59 | */ 60 | public initialize(): boolean { 61 | // キャンバスの作成 62 | canvas = document.createElement('canvas'); 63 | 64 | canvas.id = LAppDefine.CanvasId; 65 | canvas.style.position = 'fixed'; 66 | canvas.style.bottom = '0'; 67 | 68 | if (LAppDefine.CanvasPosition === 'left') { 69 | canvas.style.left = '0'; 70 | } else { 71 | canvas.style.right = '0'; 72 | } 73 | 74 | canvas.style.zIndex = '9999'; 75 | LAppDefine.Canvas = canvas; 76 | 77 | if (LAppDefine.CanvasSize === 'auto') { 78 | this._resizeCanvas(); 79 | } else { 80 | canvas.width = LAppDefine.CanvasSize.width; 81 | canvas.height = LAppDefine.CanvasSize.height; 82 | } 83 | 84 | canvas.style.opacity = '0'; 85 | canvas.style.transition = '.7s cubic-bezier(0.23, 1, 0.32, 1)'; 86 | 87 | // glコンテキストを初期化 88 | // @ts-ignore 89 | gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); 90 | 91 | if (!gl) { 92 | console.log('Cannot initialize WebGL. This browser does not support.'); 93 | gl = null; 94 | 95 | // gl初期化失敗 96 | return false; 97 | } 98 | 99 | // キャンバスを DOM に追加 100 | document.body.appendChild(canvas); 101 | 102 | // 初始化对话框 103 | LAppMessageBox.getInstance().initialize(canvas); 104 | 105 | if (!frameBuffer) { 106 | frameBuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING); 107 | } 108 | 109 | // 透過設定 110 | gl.enable(gl.BLEND); 111 | gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); 112 | 113 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); 114 | 115 | const supportTouch: boolean = 'ontouchend' in canvas; 116 | 117 | if (supportTouch) { 118 | // タッチ関連コールバック関数登録 119 | canvas.ontouchstart = onTouchBegan; 120 | canvas.ontouchmove = onTouchMoved; 121 | canvas.ontouchend = onTouchEnded; 122 | canvas.ontouchcancel = onTouchCancel; 123 | } else { 124 | // マウス関連コールバック関数登録 125 | canvas.onmousedown = onClickBegan; 126 | canvas.onmousemove = onMouseMoved; 127 | canvas.onmouseup = onClickEnded; 128 | } 129 | 130 | // AppViewの初期化 131 | this._view.initialize(); 132 | 133 | // Cubism SDKの初期化 134 | this.initializeCubism(); 135 | 136 | return true; 137 | } 138 | 139 | /** 140 | * Resize canvas and re-initialize view. 141 | */ 142 | public onResize(): void { 143 | this._resizeCanvas(); 144 | this._view.initialize(); 145 | this._view.initializeSprite(); 146 | 147 | // キャンバスサイズを渡す 148 | const viewport: number[] = [0, 0, canvas.width, canvas.height]; 149 | 150 | gl.viewport(viewport[0], viewport[1], viewport[2], viewport[3]); 151 | } 152 | 153 | /** 154 | * 解放する。 155 | */ 156 | public release(): void { 157 | this._textureManager.release(); 158 | this._textureManager = null; 159 | 160 | this._view.release(); 161 | this._view = null; 162 | 163 | // リソースを解放 164 | LAppLive2DManager.releaseInstance(); 165 | 166 | // Cubism SDKの解放 167 | CubismFramework.dispose(); 168 | } 169 | 170 | /** 171 | * 実行処理。 172 | */ 173 | public run(): void { 174 | // メインループ 175 | const loop = (): void => { 176 | // インスタンスの有無の確認 177 | if (s_instance == null) { 178 | return; 179 | } 180 | 181 | // 時間更新 182 | LAppPal.updateTime(); 183 | 184 | // 画面の初期化 185 | gl.clearColor(...LAppDefine.BackgroundRGBA); 186 | 187 | // 深度テストを有効化 188 | gl.enable(gl.DEPTH_TEST); 189 | 190 | // 近くにある物体は、遠くにある物体を覆い隠す 191 | gl.depthFunc(gl.LEQUAL); 192 | 193 | // カラーバッファや深度バッファをクリアする 194 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); 195 | 196 | gl.clearDepth(1.0); 197 | 198 | // 透過設定 199 | gl.enable(gl.BLEND); 200 | gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); 201 | 202 | // 描画更新 203 | this._view.render(); 204 | 205 | // ループのために再帰呼び出し 206 | requestAnimationFrame(loop); 207 | }; 208 | loop(); 209 | } 210 | 211 | /** 212 | * シェーダーを登録する。 213 | */ 214 | public createShader(): WebGLProgram { 215 | // バーテックスシェーダーのコンパイル 216 | const vertexShaderId = gl.createShader(gl.VERTEX_SHADER); 217 | 218 | if (vertexShaderId == null) { 219 | LAppPal.printMessage('failed to create vertexShader'); 220 | return null; 221 | } 222 | 223 | const vertexShader: string = 224 | 'precision mediump float;' + 225 | 'attribute vec3 position;' + 226 | 'attribute vec2 uv;' + 227 | 'varying vec2 vuv;' + 228 | 'void main(void)' + 229 | '{' + 230 | ' gl_Position = vec4(position, 1.0);' + 231 | ' vuv = uv;' + 232 | '}'; 233 | 234 | gl.shaderSource(vertexShaderId, vertexShader); 235 | gl.compileShader(vertexShaderId); 236 | 237 | // フラグメントシェーダのコンパイル 238 | const fragmentShaderId = gl.createShader(gl.FRAGMENT_SHADER); 239 | 240 | if (fragmentShaderId == null) { 241 | LAppPal.printMessage('failed to create fragmentShader'); 242 | return null; 243 | } 244 | 245 | const fragmentShader: string = 246 | 'precision mediump float;' + 247 | 'varying vec2 vuv;' + 248 | 'uniform sampler2D texture;' + 249 | 'void main(void)' + 250 | '{' + 251 | ' gl_FragColor = texture2D(texture, vuv);' + 252 | '}'; 253 | 254 | gl.shaderSource(fragmentShaderId, fragmentShader); 255 | gl.compileShader(fragmentShaderId); 256 | 257 | // プログラムオブジェクトの作成 258 | const programId = gl.createProgram(); 259 | gl.attachShader(programId, vertexShaderId); 260 | gl.attachShader(programId, fragmentShaderId); 261 | 262 | gl.deleteShader(vertexShaderId); 263 | gl.deleteShader(fragmentShaderId); 264 | 265 | // リンク 266 | gl.linkProgram(programId); 267 | 268 | gl.useProgram(programId); 269 | 270 | return programId; 271 | } 272 | 273 | /** 274 | * View情報を取得する。 275 | */ 276 | public getView(): LAppView { 277 | return this._view; 278 | } 279 | 280 | public getTextureManager(): LAppTextureManager { 281 | return this._textureManager; 282 | } 283 | 284 | /** 285 | * コンストラクタ 286 | */ 287 | constructor() { 288 | this._captured = false; 289 | this._mouseX = 0.0; 290 | this._mouseY = 0.0; 291 | this._isEnd = false; 292 | 293 | this._cubismOption = new Option(); 294 | this._view = new LAppView(); 295 | this._textureManager = new LAppTextureManager(); 296 | } 297 | 298 | /** 299 | * Cubism SDKの初期化 300 | */ 301 | public initializeCubism(): void { 302 | // setup cubism 303 | this._cubismOption.logFunction = LAppPal.printMessage; 304 | this._cubismOption.loggingLevel = LAppDefine.CubismLoggingLevel; 305 | CubismFramework.startUp(this._cubismOption); 306 | 307 | // initialize cubism 308 | CubismFramework.initialize(); 309 | 310 | // load model 311 | LAppLive2DManager.getInstance(); 312 | 313 | LAppPal.updateTime(); 314 | } 315 | 316 | /** 317 | * Resize the canvas to fill the screen. 318 | */ 319 | private _resizeCanvas(): void { 320 | canvas.width = window.innerWidth; 321 | canvas.height = window.innerHeight; 322 | } 323 | 324 | _cubismOption: Option; // Cubism SDK Option 325 | _view: LAppView; // View情報 326 | _captured: boolean; // クリックしているか 327 | _mouseX: number; // マウスX座標 328 | _mouseY: number; // マウスY座標 329 | _isEnd: boolean; // APP終了しているか 330 | _textureManager: LAppTextureManager; // テクスチャマネージャー 331 | } 332 | 333 | let expressionCount = 0; 334 | 335 | let toggle = true; 336 | 337 | /** 338 | * クリックしたときに呼ばれる。 339 | */ 340 | function onClickBegan(e: MouseEvent): void { 341 | const live2dModel = LAppDelegate.getInstance(); 342 | if (!live2dModel._view) { 343 | LAppPal.printMessage('view notfound'); 344 | return; 345 | } 346 | 347 | const manager = LAppLive2DManager.getInstance(); 348 | const model = manager.model; 349 | 350 | model.setRandomExpression(); 351 | } 352 | 353 | /** 354 | * マウスポインタが動いたら呼ばれる。 355 | */ 356 | function onMouseMoved(e: MouseEvent): void { 357 | const live2dModel = LAppDelegate.getInstance(); 358 | 359 | if (!live2dModel._view) { 360 | LAppPal.printMessage('view notfound'); 361 | return; 362 | } 363 | 364 | const rect = (e.target as Element).getBoundingClientRect(); 365 | const posX: number = e.clientX - rect.left; 366 | const posY: number = e.clientY - rect.top; 367 | 368 | live2dModel._view.onTouchesMoved(posX, posY); 369 | } 370 | 371 | /** 372 | * クリックが終了したら呼ばれる。 373 | */ 374 | function onClickEnded(e: MouseEvent): void { 375 | LAppDelegate.getInstance()._captured = false; 376 | if (!LAppDelegate.getInstance()._view) { 377 | LAppPal.printMessage('view notfound'); 378 | return; 379 | } 380 | 381 | const rect = (e.target as Element).getBoundingClientRect(); 382 | const posX: number = e.clientX - rect.left; 383 | const posY: number = e.clientY - rect.top; 384 | 385 | LAppDelegate.getInstance()._view.onTouchesEnded(posX, posY); 386 | } 387 | 388 | /** 389 | * タッチしたときに呼ばれる。 390 | */ 391 | function onTouchBegan(e: TouchEvent): void { 392 | LAppPal.printMessage('touch event happens'); 393 | const live2dModel = LAppDelegate.getInstance(); 394 | if (!live2dModel._view) { 395 | LAppPal.printMessage('view notfound'); 396 | return; 397 | } 398 | 399 | const posX = e.changedTouches[0].pageX; 400 | const posY = e.changedTouches[0].pageY; 401 | 402 | live2dModel._view.onTouchesBegan(posX, posY); 403 | } 404 | 405 | /** 406 | * スワイプすると呼ばれる。 407 | */ 408 | function onTouchMoved(e: TouchEvent): void { 409 | if (!LAppDelegate.getInstance()._captured) { 410 | return; 411 | } 412 | 413 | if (!LAppDelegate.getInstance()._view) { 414 | LAppPal.printMessage('view notfound'); 415 | return; 416 | } 417 | 418 | const rect = (e.target as Element).getBoundingClientRect(); 419 | 420 | const posX = e.changedTouches[0].clientX - rect.left; 421 | const posY = e.changedTouches[0].clientY - rect.top; 422 | 423 | LAppDelegate.getInstance()._view.onTouchesMoved(posX, posY); 424 | } 425 | 426 | /** 427 | * タッチが終了したら呼ばれる。 428 | */ 429 | function onTouchEnded(e: TouchEvent): void { 430 | LAppDelegate.getInstance()._captured = false; 431 | 432 | if (!LAppDelegate.getInstance()._view) { 433 | LAppPal.printMessage('view notfound'); 434 | return; 435 | } 436 | 437 | const rect = (e.target as Element).getBoundingClientRect(); 438 | 439 | const posX = e.changedTouches[0].clientX - rect.left; 440 | const posY = e.changedTouches[0].clientY - rect.top; 441 | 442 | LAppDelegate.getInstance()._view.onTouchesEnded(posX, posY); 443 | } 444 | 445 | /** 446 | * タッチがキャンセルされると呼ばれる。 447 | */ 448 | function onTouchCancel(e: TouchEvent): void { 449 | LAppDelegate.getInstance()._captured = false; 450 | 451 | if (!LAppDelegate.getInstance()._view) { 452 | LAppPal.printMessage('view notfound'); 453 | return; 454 | } 455 | 456 | const rect = (e.target as Element).getBoundingClientRect(); 457 | 458 | const posX = e.changedTouches[0].clientX - rect.left; 459 | const posY = e.changedTouches[0].clientY - rect.top; 460 | 461 | LAppDelegate.getInstance()._view.onTouchesEnded(posX, posY); 462 | } 463 | -------------------------------------------------------------------------------- /src/lappwavfilehandler.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright(c) Live2D Inc. All rights reserved. 3 | * 4 | * Use of this source code is governed by the Live2D Open Software license 5 | * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. 6 | */ 7 | 8 | import { cacheFetch } from './cache'; 9 | import { LAppPal } from './lapppal'; 10 | 11 | export let s_instance: LAppWavFileHandler = null; 12 | 13 | export class LAppWavFileHandler { 14 | /** 15 | * クラスのインスタンス(シングルトン)を返す。 16 | * インスタンスが生成されていない場合は内部でインスタンスを生成する。 17 | * 18 | * @return クラスのインスタンス 19 | */ 20 | public static getInstance(): LAppWavFileHandler { 21 | if (s_instance == null) { 22 | s_instance = new LAppWavFileHandler(); 23 | } 24 | 25 | return s_instance; 26 | } 27 | 28 | /** 29 | * クラスのインスタンス(シングルトン)を解放する。 30 | */ 31 | public static releaseInstance(): void { 32 | if (s_instance != null) { 33 | s_instance = void 0; 34 | } 35 | 36 | s_instance = null; 37 | } 38 | 39 | public update(deltaTimeSeconds: number) { 40 | let goalOffset: number; 41 | let rms: number; 42 | 43 | // データロード前/ファイル末尾に達した場合は更新しない 44 | if ( 45 | this._pcmData == null || 46 | this._sampleOffset >= this._wavFileInfo._samplesPerChannel 47 | ) { 48 | this._lastRms = 0.0; 49 | return false; 50 | } 51 | 52 | // 経過時間後の状態を保持 53 | this._userTimeSeconds += deltaTimeSeconds; 54 | goalOffset = Math.floor( 55 | this._userTimeSeconds * this._wavFileInfo._samplingRate 56 | ); 57 | if (goalOffset > this._wavFileInfo._samplesPerChannel) { 58 | goalOffset = this._wavFileInfo._samplesPerChannel; 59 | } 60 | 61 | // RMS計測 62 | rms = 0.0; 63 | for ( 64 | let channelCount = 0; 65 | channelCount < this._wavFileInfo._numberOfChannels; 66 | channelCount++ 67 | ) { 68 | for ( 69 | let sampleCount = this._sampleOffset; 70 | sampleCount < goalOffset; 71 | sampleCount++ 72 | ) { 73 | const pcm = this._pcmData[channelCount][sampleCount]; 74 | rms += pcm * pcm; 75 | } 76 | } 77 | rms = Math.sqrt( 78 | rms / 79 | (this._wavFileInfo._numberOfChannels * 80 | (goalOffset - this._sampleOffset)) 81 | ); 82 | 83 | this._lastRms = rms; 84 | this._sampleOffset = goalOffset; 85 | return true; 86 | } 87 | 88 | public start(filePath: string): void { 89 | // サンプル位参照位置を初期化 90 | this._sampleOffset = 0; 91 | this._userTimeSeconds = 0.0; 92 | 93 | // RMS値をリセット 94 | this._lastRms = 0.0; 95 | 96 | if (!this.loadWavFile(filePath)) { 97 | return; 98 | } 99 | } 100 | 101 | public getRms(): number { 102 | return this._lastRms; 103 | } 104 | 105 | public loadWavFile(filePath: string): boolean { 106 | let ret = false; 107 | 108 | if (this._pcmData != null) { 109 | this.releasePcmData(); 110 | } 111 | 112 | // ファイルロード 113 | const asyncFileLoad = async () => { 114 | return cacheFetch(filePath).then(responce => { 115 | return responce.arrayBuffer(); 116 | }); 117 | }; 118 | 119 | const asyncWavFileManager = (async () => { 120 | this._byteReader._fileByte = await asyncFileLoad(); 121 | this._byteReader._fileDataView = new DataView(this._byteReader._fileByte); 122 | this._byteReader._fileSize = this._byteReader._fileByte.byteLength; 123 | this._byteReader._readOffset = 0; 124 | 125 | // ファイルロードに失敗しているか、先頭のシグネチャ"RIFF"を入れるサイズもない場合は失敗 126 | if ( 127 | this._byteReader._fileByte == null || 128 | this._byteReader._fileSize < 4 129 | ) { 130 | return false; 131 | } 132 | 133 | // ファイル名 134 | this._wavFileInfo._fileName = filePath; 135 | 136 | try { 137 | // シグネチャ "RIFF" 138 | if (!this._byteReader.getCheckSignature('RIFF')) { 139 | ret = false; 140 | throw new Error('Cannot find Signeture "RIFF".'); 141 | } 142 | // ファイルサイズ-8(読み飛ばし) 143 | this._byteReader.get32LittleEndian(); 144 | // シグネチャ "WAVE" 145 | if (!this._byteReader.getCheckSignature('WAVE')) { 146 | ret = false; 147 | throw new Error('Cannot find Signeture "WAVE".'); 148 | } 149 | // シグネチャ "fmt " 150 | if (!this._byteReader.getCheckSignature('fmt ')) { 151 | ret = false; 152 | throw new Error('Cannot find Signeture "fmt".'); 153 | } 154 | // fmtチャンクサイズ 155 | const fmtChunkSize = this._byteReader.get32LittleEndian(); 156 | // フォーマットIDは1(リニアPCM)以外受け付けない 157 | if (this._byteReader.get16LittleEndian() != 1) { 158 | ret = false; 159 | throw new Error('File is not linear PCM.'); 160 | } 161 | // チャンネル数 162 | this._wavFileInfo._numberOfChannels = 163 | this._byteReader.get16LittleEndian(); 164 | // サンプリングレート 165 | this._wavFileInfo._samplingRate = this._byteReader.get32LittleEndian(); 166 | // データ速度[byte/sec](読み飛ばし) 167 | this._byteReader.get32LittleEndian(); 168 | // ブロックサイズ(読み飛ばし) 169 | this._byteReader.get16LittleEndian(); 170 | // 量子化ビット数 171 | this._wavFileInfo._bitsPerSample = this._byteReader.get16LittleEndian(); 172 | // fmtチャンクの拡張部分の読み飛ばし 173 | if (fmtChunkSize > 16) { 174 | this._byteReader._readOffset += fmtChunkSize - 16; 175 | } 176 | // "data"チャンクが出現するまで読み飛ばし 177 | while ( 178 | !this._byteReader.getCheckSignature('data') && 179 | this._byteReader._readOffset < this._byteReader._fileSize 180 | ) { 181 | this._byteReader._readOffset += 182 | this._byteReader.get32LittleEndian() + 4; 183 | } 184 | // ファイル内に"data"チャンクが出現しなかった 185 | if (this._byteReader._readOffset >= this._byteReader._fileSize) { 186 | ret = false; 187 | throw new Error('Cannot find "data" Chunk.'); 188 | } 189 | // サンプル数 190 | { 191 | const dataChunkSize = this._byteReader.get32LittleEndian(); 192 | this._wavFileInfo._samplesPerChannel = 193 | (dataChunkSize * 8) / 194 | (this._wavFileInfo._bitsPerSample * 195 | this._wavFileInfo._numberOfChannels); 196 | } 197 | // 領域確保 198 | this._pcmData = new Array(this._wavFileInfo._numberOfChannels); 199 | for ( 200 | let channelCount = 0; 201 | channelCount < this._wavFileInfo._numberOfChannels; 202 | channelCount++ 203 | ) { 204 | this._pcmData[channelCount] = new Float32Array( 205 | this._wavFileInfo._samplesPerChannel 206 | ); 207 | } 208 | // 波形データ取得 209 | for ( 210 | let sampleCount = 0; 211 | sampleCount < this._wavFileInfo._samplesPerChannel; 212 | sampleCount++ 213 | ) { 214 | for ( 215 | let channelCount = 0; 216 | channelCount < this._wavFileInfo._numberOfChannels; 217 | channelCount++ 218 | ) { 219 | this._pcmData[channelCount][sampleCount] = this.getPcmSample(); 220 | } 221 | } 222 | 223 | ret = true; 224 | } catch (e) { 225 | console.log(e); 226 | } 227 | })(); 228 | 229 | return ret; 230 | } 231 | 232 | public getPcmSample(): number { 233 | let pcm32; 234 | 235 | // 32ビット幅に拡張してから-1~1の範囲に丸める 236 | switch (this._wavFileInfo._bitsPerSample) { 237 | case 8: 238 | pcm32 = this._byteReader.get8() - 128; 239 | pcm32 <<= 24; 240 | break; 241 | case 16: 242 | pcm32 = this._byteReader.get16LittleEndian() << 16; 243 | break; 244 | case 24: 245 | pcm32 = this._byteReader.get24LittleEndian() << 8; 246 | break; 247 | default: 248 | // 対応していないビット幅 249 | pcm32 = 0; 250 | break; 251 | } 252 | 253 | return pcm32 / 2147483647; //Number.MAX_VALUE; 254 | } 255 | 256 | public releasePcmData(): void { 257 | for ( 258 | let channelCount = 0; 259 | channelCount < this._wavFileInfo._numberOfChannels; 260 | channelCount++ 261 | ) { 262 | delete this._pcmData[channelCount]; 263 | } 264 | delete this._pcmData; 265 | this._pcmData = null; 266 | } 267 | 268 | constructor() { 269 | this._pcmData = null; 270 | this._userTimeSeconds = 0.0; 271 | this._lastRms = 0.0; 272 | this._sampleOffset = 0.0; 273 | this._wavFileInfo = new WavFileInfo(); 274 | this._byteReader = new ByteReader(); 275 | } 276 | 277 | _pcmData: Array; 278 | _userTimeSeconds: number; 279 | _lastRms: number; 280 | _sampleOffset: number; 281 | _wavFileInfo: WavFileInfo; 282 | _byteReader: ByteReader; 283 | _loadFiletoBytes = (arrayBuffer: ArrayBuffer, length: number): void => { 284 | this._byteReader._fileByte = arrayBuffer; 285 | this._byteReader._fileDataView = new DataView(this._byteReader._fileByte); 286 | this._byteReader._fileSize = length; 287 | }; 288 | } 289 | 290 | export class WavFileInfo { 291 | constructor() { 292 | this._fileName = ''; 293 | this._numberOfChannels = 0; 294 | this._bitsPerSample = 0; 295 | this._samplingRate = 0; 296 | this._samplesPerChannel = 0; 297 | } 298 | 299 | _fileName: string; ///< ファイル名 300 | _numberOfChannels: number; ///< チャンネル数 301 | _bitsPerSample: number; ///< サンプルあたりビット数 302 | _samplingRate: number; ///< サンプリングレート 303 | _samplesPerChannel: number; ///< 1チャンネルあたり総サンプル数 304 | } 305 | 306 | export class ByteReader { 307 | constructor() { 308 | this._fileByte = null; 309 | this._fileDataView = null; 310 | this._fileSize = 0; 311 | this._readOffset = 0; 312 | } 313 | 314 | /** 315 | * @brief 8ビット読み込み 316 | * @return Csm::csmUint8 読み取った8ビット値 317 | */ 318 | public get8(): number { 319 | const ret = this._fileDataView.getUint8(this._readOffset); 320 | this._readOffset++; 321 | return ret; 322 | } 323 | 324 | /** 325 | * @brief 16ビット読み込み(リトルエンディアン) 326 | * @return Csm::csmUint16 読み取った16ビット値 327 | */ 328 | public get16LittleEndian(): number { 329 | const ret = 330 | (this._fileDataView.getUint8(this._readOffset + 1) << 8) | 331 | this._fileDataView.getUint8(this._readOffset); 332 | this._readOffset += 2; 333 | return ret; 334 | } 335 | 336 | /** 337 | * @brief 24ビット読み込み(リトルエンディアン) 338 | * @return Csm::csmUint32 読み取った24ビット値(下位24ビットに設定) 339 | */ 340 | public get24LittleEndian(): number { 341 | const ret = 342 | (this._fileDataView.getUint8(this._readOffset + 2) << 16) | 343 | (this._fileDataView.getUint8(this._readOffset + 1) << 8) | 344 | this._fileDataView.getUint8(this._readOffset); 345 | this._readOffset += 3; 346 | return ret; 347 | } 348 | 349 | /** 350 | * @brief 32ビット読み込み(リトルエンディアン) 351 | * @return Csm::csmUint32 読み取った32ビット値 352 | */ 353 | public get32LittleEndian(): number { 354 | const ret = 355 | (this._fileDataView.getUint8(this._readOffset + 3) << 24) | 356 | (this._fileDataView.getUint8(this._readOffset + 2) << 16) | 357 | (this._fileDataView.getUint8(this._readOffset + 1) << 8) | 358 | this._fileDataView.getUint8(this._readOffset); 359 | this._readOffset += 4; 360 | return ret; 361 | } 362 | 363 | /** 364 | * @brief シグネチャの取得と参照文字列との一致チェック 365 | * @param[in] reference 検査対象のシグネチャ文字列 366 | * @retval true 一致している 367 | * @retval false 一致していない 368 | */ 369 | public getCheckSignature(reference: string): boolean { 370 | const getSignature: Uint8Array = new Uint8Array(4); 371 | const referenceString: Uint8Array = new TextEncoder().encode(reference); 372 | if (reference.length != 4) { 373 | return false; 374 | } 375 | for (let signatureOffset = 0; signatureOffset < 4; signatureOffset++) { 376 | getSignature[signatureOffset] = this.get8(); 377 | } 378 | return ( 379 | getSignature[0] == referenceString[0] && 380 | getSignature[1] == referenceString[1] && 381 | getSignature[2] == referenceString[2] && 382 | getSignature[3] == referenceString[3] 383 | ); 384 | } 385 | 386 | _fileByte: ArrayBuffer; ///< ロードしたファイルのバイト列 387 | _fileDataView: DataView; 388 | _fileSize: number; ///< ファイルサイズ 389 | _readOffset: number; ///< ファイル参照位置 390 | } 391 | -------------------------------------------------------------------------------- /src/toolbox.ts: -------------------------------------------------------------------------------- 1 | import { cacheFetch, CacheFetchSetting } from "./cache"; 2 | import LAppDefine from "./lappdefine"; 3 | import { LAppLive2DManager } from './lapplive2dmanager'; 4 | import * as svgIcon from "./svg"; 5 | 6 | // TODO: 适配到 live2dBoxItemCss 7 | const _defaultIconSize = 35; 8 | const _defaultIconBgColor = '#00A6ED'; 9 | const _defaultIconFgColor = 'white'; 10 | const _defaultHoverColor = 'rgb(224, 209, 41)'; 11 | const _defaultExpressionContainerWidth = 300; 12 | 13 | let container: undefined | HTMLDivElement = undefined; 14 | let containerTimer: NodeJS.Timeout | string | number | undefined = undefined; 15 | let collapse = false; 16 | let widthXoffset = 35; 17 | const live2dBoxItemCss = '__live2d-toolbox-item'; 18 | 19 | function addCssClass() { 20 | const style = document.createElement('style'); 21 | style.innerHTML = ` 22 | .${live2dBoxItemCss} { 23 | margin: 2px; 24 | padding: 2px; 25 | display: flex; 26 | height: ${_defaultIconSize}px; 27 | width: ${_defaultIconSize}px; 28 | justify-content: center; 29 | align-items: center; 30 | cursor: pointer; 31 | font-size: 0.7rem; 32 | background-color: ${_defaultIconBgColor}; 33 | color: ${_defaultIconFgColor}; 34 | border-radius: 0.5em; 35 | transition: all .35s cubic-bezier(0.23, 1, 0.32, 1); 36 | } 37 | 38 | .${live2dBoxItemCss}:hover { 39 | background-color: rgb(224, 209, 41); 40 | } 41 | 42 | .${live2dBoxItemCss}.button-item { 43 | display: flex; 44 | align-items: center; 45 | width: fit-content; 46 | padding: 5px 10px 0px; 47 | } 48 | 49 | .${live2dBoxItemCss}.button-item svg { 50 | height: 20px; 51 | } 52 | 53 | .${live2dBoxItemCss}.expression-item { 54 | display: flex; 55 | align-items: center; 56 | width: fit-content; 57 | padding: 3px 10px; 58 | } 59 | 60 | .${live2dBoxItemCss}.expression-item > span:last-child { 61 | width: 60px; 62 | overflow: hidden; 63 | white-space: nowrap; 64 | text-overflow: ellipsis; 65 | } 66 | 67 | .${live2dBoxItemCss}.expression-item svg { 68 | height: 20px; 69 | margin-right: 5px; 70 | } 71 | 72 | .${live2dBoxItemCss} svg path { 73 | fill: white; 74 | } 75 | 76 | `; 77 | document.head.appendChild(style); 78 | } 79 | 80 | function showContainer() { 81 | if (container) { 82 | if (containerTimer) { 83 | clearTimeout(containerTimer); 84 | } 85 | containerTimer = setTimeout(() => { 86 | container.style.opacity = '1'; 87 | }, 200); 88 | } 89 | } 90 | 91 | function hideContainer() { 92 | if (container && !collapse) { 93 | if (containerTimer) { 94 | clearTimeout(containerTimer); 95 | } 96 | containerTimer = setTimeout(() => { 97 | container.style.opacity = '0'; 98 | }, 200); 99 | } 100 | } 101 | 102 | /** 103 | * @description 生成一个普通的按钮元素 104 | * @param text 元素内的文本 105 | * @returns 106 | */ 107 | function createCommonIcon(svgString: string, extraString: string = '', cssClasses: string[] = []) { 108 | const div = document.createElement('div'); 109 | div.classList.add(live2dBoxItemCss); 110 | cssClasses.forEach(cssString => div.classList.add(cssString)); 111 | 112 | const firstSpan = document.createElement('span'); 113 | const secondSpan = document.createElement('span'); 114 | firstSpan.innerHTML = svgString; 115 | secondSpan.innerText = extraString; 116 | 117 | div.appendChild(firstSpan); 118 | div.appendChild(secondSpan); 119 | return div; 120 | } 121 | 122 | 123 | /** 124 | * @description 收起和展开 live2d 125 | * @param container 126 | * @returns 127 | */ 128 | function makeLive2dCollapseIcon(container: HTMLDivElement): HTMLDivElement { 129 | // 根据位置选择不同的 icon 130 | const iconSvg = LAppDefine.CanvasPosition === 'left' ? svgIcon.collapseIconLeft : svgIcon.collapseIconRight; 131 | const icon = createCommonIcon(iconSvg, '', ['button-item']); 132 | icon.style.backgroundColor = _defaultIconBgColor; 133 | icon.style.fontSize = '1.05rem'; 134 | 135 | // 注册 icon 的鼠标事件 136 | icon.addEventListener('mouseenter', () => { 137 | icon.style.backgroundColor = _defaultHoverColor; 138 | }); 139 | 140 | icon.addEventListener('mouseleave', () => { 141 | icon.style.backgroundColor = _defaultIconBgColor; 142 | }); 143 | 144 | let xoffset = 0; 145 | icon.onclick = async () => { 146 | const canvas = LAppDefine.Canvas; 147 | if (canvas) { 148 | const canvasWidth = Math.ceil(canvas.width); 149 | xoffset = (xoffset + canvasWidth) % (canvasWidth << 1); 150 | 151 | // 根据位置参数调整移动方向 152 | if (LAppDefine.CanvasPosition === 'left') { 153 | canvas.style.transform = `translateX(-${xoffset}px)`; 154 | container.style.transform = `translateX(-${Math.max(0, xoffset - widthXoffset)}px)`; 155 | } else { 156 | canvas.style.transform = `translateX(${xoffset}px)`; 157 | container.style.transform = `translateX(${Math.max(0, xoffset - widthXoffset)}px)`; 158 | } 159 | 160 | if (xoffset > 0) { 161 | // 收起 162 | collapse = true; 163 | icon.style.transform = 'rotate(180deg)'; 164 | setTimeout(() => { 165 | showContainer(); 166 | }, 500); 167 | } else { 168 | // 展开 169 | collapse = false; 170 | icon.style.transform = 'rotate(0)'; 171 | } 172 | } 173 | } 174 | return icon; 175 | } 176 | 177 | /** 178 | * @description 创建 【收起/展开 展示表情列表】 的按钮 179 | * @param container 180 | * @returns 181 | */ 182 | function makeExpressionListCollapseIcon(container: HTMLDivElement): HTMLDivElement { 183 | const icon = createCommonIcon(svgIcon.expressionIcon, '', ['button-item']); 184 | icon.style.backgroundColor = _defaultIconBgColor; 185 | icon.style.fontSize = '1.05rem'; 186 | icon.style.position = 'relative'; 187 | 188 | // 创建一个容器,来容纳所有的表情按钮 189 | const iconsWrapper = document.createElement('div'); 190 | const animationDurationMS = 7; 191 | iconsWrapper.style.position = 'absolute'; 192 | iconsWrapper.style.top = 0 + 'px'; 193 | iconsWrapper.style.flexDirection = 'column'; 194 | iconsWrapper.style.transition = 'all .75s cubic-bezier(0.23, 1, 0.32, 1)'; 195 | iconsWrapper.style.display = 'flex'; 196 | iconsWrapper.style.opacity = '0'; 197 | 198 | let currentTranslateY = 0; 199 | const translateX = LAppDefine.CanvasPosition === 'left' ? '75px' : '-75px'; 200 | iconsWrapper.style.transform = `translate(${translateX}, ${currentTranslateY - 50}px)`; 201 | 202 | // 注册 icon 的鼠标事件 203 | icon.addEventListener('mouseenter', () => { 204 | icon.style.backgroundColor = _defaultHoverColor; 205 | 206 | // 光标进入 207 | iconsWrapper.style.visibility = 'visible'; 208 | iconsWrapper.style.opacity = '1'; 209 | iconsWrapper.style.transform = `translate(${translateX}, ${currentTranslateY}px)`; 210 | }); 211 | 212 | icon.addEventListener('mouseleave', () => { 213 | icon.style.backgroundColor = _defaultIconBgColor; 214 | 215 | // 光标退出 216 | iconsWrapper.style.opacity = '0'; 217 | iconsWrapper.style.transform = `translate(${translateX}, ${currentTranslateY - 50}px)`; 218 | 219 | setTimeout(() => { 220 | iconsWrapper.style.visibility = 'hidden'; 221 | }, 500); 222 | }); 223 | 224 | // 容器滚动 225 | iconsWrapper.addEventListener('wheel', e => { 226 | // 获取当前的transform值 227 | const currentTransform = getComputedStyle(iconsWrapper).transform; 228 | const matrix = new WebKitCSSMatrix(currentTransform); 229 | 230 | // 获取当前y轴平移值 231 | let translateY = matrix.m42; // y轴平移量 232 | 233 | // 根据滚轮方向调整位置 234 | if(e.deltaY > 0) { // 滚轮向下滚动,元素上移 235 | translateY -= 50; 236 | } else { // 滚轮向上滚动,元素下移 237 | translateY += 50; 238 | } 239 | 240 | currentTranslateY = translateY; 241 | 242 | // 更新transform属性 243 | const translateX = LAppDefine.CanvasPosition === 'left' ? '75px' : '-75px'; 244 | iconsWrapper.style.transform = `translate(${translateX}, ${translateY}px)`; 245 | 246 | e.preventDefault(); // 阻止默认的滚动行为 247 | }); 248 | 249 | // 创建每一个表情的 icon 250 | const expressionIcons = makeExpressionListIcons(container); 251 | // 加入展开列表中 252 | for (const expression of expressionIcons) { 253 | iconsWrapper.appendChild(expression); 254 | } 255 | icon.appendChild(iconsWrapper); 256 | 257 | return icon; 258 | } 259 | 260 | 261 | /** 262 | * @description 展示所有的表情 263 | * @param container 264 | * @returns 265 | */ 266 | function makeExpressionListIcons(container: HTMLDivElement) { 267 | const manager = LAppLive2DManager.getInstance(); 268 | const canvas = LAppDefine.Canvas; 269 | const icons: HTMLDivElement[] = []; 270 | 271 | if (manager && canvas) { 272 | const maxExpNum = Math.max(0, Math.floor(canvas.height / _defaultIconSize) - 1); 273 | 274 | // TODO: 支持更多模型 275 | const model = manager.getModel(0); 276 | const expNum = Math.min(model._expressions.getSize(), maxExpNum); 277 | 278 | for (let i = 0; i < expNum; ++ i) { 279 | const name = model._expressions._keyValues[i].first; 280 | 281 | // 去除结尾的 json 282 | const renderName = name.replace('.exp3.json', ''); 283 | const icon = createCommonIcon(svgIcon.catIcon, renderName); 284 | 285 | icon.classList.add('expression-item'); 286 | 287 | icon.onclick = async() => { 288 | model.setExpression(name); 289 | } 290 | 291 | icons.push(icon); 292 | } 293 | } 294 | 295 | return icons; 296 | } 297 | 298 | 299 | /** 300 | * @description 创建强制刷新 live2d 的按钮 301 | * @param container 302 | * @returns 303 | */ 304 | function makeRefreshCacheIcon(container: HTMLDivElement): HTMLDivElement { 305 | const icon = createCommonIcon(svgIcon.reloadIcon, '', ['button-item']); 306 | icon.style.backgroundColor = _defaultIconBgColor; 307 | icon.style.fontSize = '1.05rem'; 308 | 309 | // 注册 icon 的鼠标事件 310 | icon.addEventListener('mouseenter', () => { 311 | icon.style.backgroundColor = _defaultHoverColor; 312 | }); 313 | 314 | icon.addEventListener('mouseleave', () => { 315 | icon.style.backgroundColor = _defaultIconBgColor; 316 | }); 317 | 318 | icon.onclick = async () => { 319 | CacheFetchSetting.refreshCache = true; 320 | const manager = LAppLive2DManager.getInstance(); 321 | manager.loadLive2dModel(); 322 | } 323 | 324 | return icon; 325 | } 326 | 327 | /** 328 | * @description 创建跳转到我的 github 仓库的按钮 329 | * @param container 330 | */ 331 | function makeStarIcon(container: HTMLDivElement): HTMLDivElement { 332 | const icon = createCommonIcon(svgIcon.starIcon, '', ['button-item']); 333 | icon.style.backgroundColor = _defaultIconBgColor; 334 | icon.style.fontSize = '1.05rem'; 335 | 336 | // 注册 icon 的鼠标事件 337 | icon.addEventListener('mouseenter', () => { 338 | icon.style.backgroundColor = _defaultHoverColor; 339 | }); 340 | 341 | icon.addEventListener('mouseleave', () => { 342 | icon.style.backgroundColor = _defaultIconBgColor; 343 | }); 344 | 345 | icon.onclick = async () => { 346 | window.open('https://github.com/LSTM-Kirigaya/Live2dRender', '_blank'); 347 | }; 348 | 349 | return icon; 350 | } 351 | 352 | function makeBoxItemContainer() { 353 | const container = document.createElement('div'); 354 | container.style.display = 'flex'; 355 | container.style.alignItems = 'center'; 356 | container.style.justifyContent = 'center'; 357 | container.style.flexDirection = 'column'; 358 | 359 | const canvas = LAppDefine.Canvas; 360 | container.style.zIndex = parseInt(canvas.style.zIndex) + 1 + ''; 361 | container.style.opacity = '0'; 362 | container.style.transition = '.7s cubic-bezier(0.23, 1, 0.32, 1)'; 363 | 364 | container.style.position = 'fixed'; 365 | 366 | // 根据位置参数设置工具箱位置 367 | if (LAppDefine.CanvasPosition === 'left') { 368 | container.style.left = canvas.width - widthXoffset + 'px'; 369 | } else { 370 | container.style.right = canvas.width - widthXoffset + 'px'; 371 | } 372 | 373 | container.style.top = window.innerHeight - canvas.height + 'px'; 374 | 375 | // 增加几个常用工具 376 | // 1. 收起 live2d 377 | const showLive2dIcon = makeLive2dCollapseIcon(container); 378 | // 2. 展示表情 379 | const showExpressionsIcon = makeExpressionListCollapseIcon(container); 380 | // 3. 刷新缓存 381 | const refreshCacheIcon = makeRefreshCacheIcon(container); 382 | // 4. 跳转到我的 github 383 | const starIcon = makeStarIcon(container); 384 | 385 | container.appendChild(showLive2dIcon); 386 | container.appendChild(showExpressionsIcon); 387 | container.appendChild(refreshCacheIcon); 388 | container.appendChild(starIcon); 389 | 390 | document.body.appendChild(container); 391 | 392 | return container; 393 | } 394 | 395 | export function reloadToolBox() { 396 | if (!container) { 397 | return; 398 | } 399 | 400 | hideContainer(); 401 | document.body.removeChild(container); 402 | container = makeBoxItemContainer(); 403 | showContainer(); 404 | 405 | // 添加工具栏的事件 406 | container.onmouseenter = async () => { 407 | showContainer(); 408 | } 409 | container.onmouseleave = async () => { 410 | hideContainer(); 411 | } 412 | } 413 | 414 | 415 | export function addToolBox() { 416 | addCssClass(); 417 | 418 | container = makeBoxItemContainer(); 419 | 420 | // 添加工具栏的事件 421 | hideContainer(); 422 | container.onmouseenter = async () => { 423 | showContainer(); 424 | } 425 | container.onmouseleave = async () => { 426 | hideContainer(); 427 | } 428 | 429 | // 添加 live2d 区域的光标事件 430 | const canvas = LAppDefine.Canvas; 431 | canvas.onmouseenter = async () => { 432 | showContainer(); 433 | } 434 | 435 | canvas.onmouseleave = async () => { 436 | hideContainer(); 437 | } 438 | } -------------------------------------------------------------------------------- /src/lappmodel.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright(c) Live2D Inc. All rights reserved. 3 | * 4 | * Use of this source code is governed by the Live2D Open Software license 5 | * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. 6 | */ 7 | 8 | import 'whatwg-fetch'; 9 | 10 | import { CubismDefaultParameterId } from '@framework/cubismdefaultparameterid'; 11 | import { CubismModelSettingJson } from '@framework/cubismmodelsettingjson'; 12 | import { 13 | BreathParameterData, 14 | CubismBreath 15 | } from '@framework/effect/cubismbreath'; 16 | import { CubismEyeBlink } from '@framework/effect/cubismeyeblink'; 17 | import { ICubismModelSetting } from '@framework/icubismmodelsetting'; 18 | import { CubismIdHandle } from '@framework/id/cubismid'; 19 | import { CubismFramework } from '@framework/live2dcubismframework'; 20 | import { CubismMatrix44 } from '@framework/math/cubismmatrix44'; 21 | import { CubismUserModel } from '@framework/model/cubismusermodel'; 22 | import { 23 | ACubismMotion, 24 | FinishedMotionCallback 25 | } from '@framework/motion/acubismmotion'; 26 | import { CubismMotion } from '@framework/motion/cubismmotion'; 27 | import { 28 | CubismMotionQueueEntryHandle, 29 | InvalidMotionQueueEntryHandleValue 30 | } from '@framework/motion/cubismmotionqueuemanager'; 31 | import { csmMap } from '@framework/type/csmmap'; 32 | import { csmRect } from '@framework/type/csmrectf'; 33 | import { csmString } from '@framework/type/csmstring'; 34 | import { csmVector } from '@framework/type/csmvector'; 35 | import { 36 | CSM_ASSERT, 37 | CubismLogError, 38 | CubismLogInfo 39 | } from '@framework/utils/cubismdebug'; 40 | 41 | import LAppDefine from './lappdefine'; 42 | import { canvas, frameBuffer, gl, LAppDelegate } from './lappdelegate'; 43 | import { LAppPal } from './lapppal'; 44 | import { TextureInfo } from './lapptexturemanager'; 45 | import { LAppWavFileHandler } from './lappwavfilehandler'; 46 | import { CubismMoc } from '@framework/model/cubismmoc'; 47 | import { cacheFetch } from './cache'; 48 | 49 | enum LoadStep { 50 | LoadAssets, 51 | LoadModel, 52 | WaitLoadModel, 53 | LoadExpression, 54 | WaitLoadExpression, 55 | LoadPhysics, 56 | WaitLoadPhysics, 57 | LoadPose, 58 | WaitLoadPose, 59 | SetupEyeBlink, 60 | SetupBreath, 61 | LoadUserData, 62 | WaitLoadUserData, 63 | SetupEyeBlinkIds, 64 | SetupLipSyncIds, 65 | SetupLayout, 66 | LoadMotion, 67 | WaitLoadMotion, 68 | CompleteInitialize, 69 | CompleteSetupModel, 70 | LoadTexture, 71 | WaitLoadTexture, 72 | CompleteSetup 73 | } 74 | 75 | /** 76 | * ユーザーが実際に使用するモデルの実装クラス
77 | * モデル生成、機能コンポーネント生成、更新処理とレンダリングの呼び出しを行う。 78 | */ 79 | export class LAppModel extends CubismUserModel { 80 | /** 81 | * model3.jsonが置かれたディレクトリとファイルパスからモデルを生成する 82 | * @param dir 83 | * @param filePath model3.json 文件的路径 84 | */ 85 | public async loadAssets(dir: string, filePath: string): Promise { 86 | this._modelHomeDir = dir; 87 | 88 | const response = await cacheFetch(filePath); 89 | const arrayBuffer = await response.arrayBuffer(); 90 | 91 | const setting: ICubismModelSetting = new CubismModelSettingJson( 92 | arrayBuffer, 93 | arrayBuffer.byteLength 94 | ); 95 | 96 | // ステートを更新 97 | this._state = LoadStep.LoadModel; 98 | 99 | // 結果を保存 100 | this.setupModel(setting); 101 | } 102 | 103 | /** 104 | * model3.jsonからモデルを生成する。 105 | * model3.jsonの記述に従ってモデル生成、モーション、物理演算などのコンポーネント生成を行う。 106 | * 107 | * @param setting ICubismModelSettingのインスタンス 108 | */ 109 | private setupModel(setting: ICubismModelSetting): void { 110 | this._updating = true; 111 | this._initialized = false; 112 | 113 | this._modelSetting = setting; 114 | 115 | // CubismModel 116 | if (this._modelSetting.getModelFileName() != '') { 117 | const modelFileName = this._modelSetting.getModelFileName(); 118 | 119 | cacheFetch(`${this._modelHomeDir}${modelFileName}`) 120 | .then(response => response.arrayBuffer()) 121 | .then(arrayBuffer => { 122 | this.loadModel(arrayBuffer, this._mocConsistency); 123 | this._state = LoadStep.LoadExpression; 124 | 125 | // callback 126 | loadCubismExpression(); 127 | }); 128 | 129 | this._state = LoadStep.WaitLoadModel; 130 | } else { 131 | LAppPal.printMessage('Model data does not exist.'); 132 | } 133 | 134 | // Expression 135 | const loadCubismExpression = (): void => { 136 | if (this._modelSetting.getExpressionCount() > 0) { 137 | const count: number = this._modelSetting.getExpressionCount(); 138 | 139 | for (let i = 0; i < count; ++ i) { 140 | const expressionName = this._modelSetting.getExpressionName(i); 141 | const expressionFileName = 142 | this._modelSetting.getExpressionFileName(i); 143 | 144 | cacheFetch(`${this._modelHomeDir}${expressionFileName}`) 145 | .then(response => response.arrayBuffer()) 146 | .then(arrayBuffer => { 147 | const motion: ACubismMotion = this.loadExpression( 148 | arrayBuffer, 149 | arrayBuffer.byteLength, 150 | expressionName 151 | ); 152 | 153 | if (this._expressions.getValue(expressionName) != null) { 154 | ACubismMotion.delete( 155 | this._expressions.getValue(expressionName) 156 | ); 157 | this._expressions.setValue(expressionName, null); 158 | } 159 | 160 | this._expressions.setValue(expressionName, motion); 161 | 162 | this._expressionCount++; 163 | 164 | if (this._expressionCount >= count) { 165 | this._state = LoadStep.LoadPhysics; 166 | 167 | // callback 168 | loadCubismPhysics(); 169 | } 170 | }); 171 | } 172 | this._state = LoadStep.WaitLoadExpression; 173 | } else { 174 | this._state = LoadStep.LoadPhysics; 175 | 176 | // callback 177 | loadCubismPhysics(); 178 | } 179 | }; 180 | 181 | // Physics 182 | const loadCubismPhysics = (): void => { 183 | if (this._modelSetting.getPhysicsFileName() != '') { 184 | const physicsFileName = this._modelSetting.getPhysicsFileName(); 185 | 186 | cacheFetch(`${this._modelHomeDir}${physicsFileName}`) 187 | .then(response => response.arrayBuffer()) 188 | .then(arrayBuffer => { 189 | this.loadPhysics(arrayBuffer, arrayBuffer.byteLength); 190 | 191 | this._state = LoadStep.LoadPose; 192 | 193 | // callback 194 | loadCubismPose(); 195 | }); 196 | this._state = LoadStep.WaitLoadPhysics; 197 | } else { 198 | this._state = LoadStep.LoadPose; 199 | 200 | // callback 201 | loadCubismPose(); 202 | } 203 | }; 204 | 205 | // Pose 206 | const loadCubismPose = (): void => { 207 | if (this._modelSetting.getPoseFileName() != '') { 208 | const poseFileName = this._modelSetting.getPoseFileName(); 209 | 210 | cacheFetch(`${this._modelHomeDir}${poseFileName}`) 211 | .then(response => response.arrayBuffer()) 212 | .then(arrayBuffer => { 213 | this.loadPose(arrayBuffer, arrayBuffer.byteLength); 214 | 215 | this._state = LoadStep.SetupEyeBlink; 216 | 217 | // callback 218 | setupEyeBlink(); 219 | }); 220 | this._state = LoadStep.WaitLoadPose; 221 | } else { 222 | this._state = LoadStep.SetupEyeBlink; 223 | 224 | // callback 225 | setupEyeBlink(); 226 | } 227 | }; 228 | 229 | // EyeBlink 230 | const setupEyeBlink = (): void => { 231 | if (this._modelSetting.getEyeBlinkParameterCount() > 0) { 232 | this._eyeBlink = CubismEyeBlink.create(this._modelSetting); 233 | this._state = LoadStep.SetupBreath; 234 | } 235 | 236 | // callback 237 | setupBreath(); 238 | }; 239 | 240 | // Breath 241 | const setupBreath = (): void => { 242 | this._breath = CubismBreath.create(); 243 | 244 | const breathParameters: csmVector = new csmVector(); 245 | breathParameters.pushBack( 246 | new BreathParameterData(this._idParamAngleX, 0.0, 15.0, 6.5345, 0.5) 247 | ); 248 | breathParameters.pushBack( 249 | new BreathParameterData(this._idParamAngleY, 0.0, 8.0, 3.5345, 0.5) 250 | ); 251 | breathParameters.pushBack( 252 | new BreathParameterData(this._idParamAngleZ, 0.0, 10.0, 5.5345, 0.5) 253 | ); 254 | breathParameters.pushBack( 255 | new BreathParameterData(this._idParamBodyAngleX, 0.0, 4.0, 15.5345, 0.5) 256 | ); 257 | breathParameters.pushBack( 258 | new BreathParameterData( 259 | CubismFramework.getIdManager().getId( 260 | CubismDefaultParameterId.ParamBreath 261 | ), 262 | 0.5, 263 | 0.5, 264 | 3.2345, 265 | 1 266 | ) 267 | ); 268 | 269 | this._breath.setParameters(breathParameters); 270 | this._state = LoadStep.LoadUserData; 271 | 272 | // callback 273 | loadUserData(); 274 | }; 275 | 276 | // UserData 277 | const loadUserData = (): void => { 278 | if (this._modelSetting.getUserDataFile() != '') { 279 | const userDataFile = this._modelSetting.getUserDataFile(); 280 | 281 | cacheFetch(`${this._modelHomeDir}${userDataFile}`) 282 | .then(response => response.arrayBuffer()) 283 | .then(arrayBuffer => { 284 | this.loadUserData(arrayBuffer, arrayBuffer.byteLength); 285 | 286 | this._state = LoadStep.SetupEyeBlinkIds; 287 | 288 | // callback 289 | setupEyeBlinkIds(); 290 | }); 291 | 292 | this._state = LoadStep.WaitLoadUserData; 293 | } else { 294 | this._state = LoadStep.SetupEyeBlinkIds; 295 | 296 | // callback 297 | setupEyeBlinkIds(); 298 | } 299 | }; 300 | 301 | // EyeBlinkIds 302 | const setupEyeBlinkIds = (): void => { 303 | const eyeBlinkIdCount: number = 304 | this._modelSetting.getEyeBlinkParameterCount(); 305 | 306 | for (let i = 0; i < eyeBlinkIdCount; ++i) { 307 | this._eyeBlinkIds.pushBack( 308 | this._modelSetting.getEyeBlinkParameterId(i) 309 | ); 310 | } 311 | 312 | this._state = LoadStep.SetupLipSyncIds; 313 | 314 | // callback 315 | setupLipSyncIds(); 316 | }; 317 | 318 | // LipSyncIds 319 | const setupLipSyncIds = (): void => { 320 | const lipSyncIdCount = this._modelSetting.getLipSyncParameterCount(); 321 | 322 | for (let i = 0; i < lipSyncIdCount; ++i) { 323 | this._lipSyncIds.pushBack(this._modelSetting.getLipSyncParameterId(i)); 324 | } 325 | this._state = LoadStep.SetupLayout; 326 | 327 | // callback 328 | setupLayout(); 329 | }; 330 | 331 | // Layout 332 | const setupLayout = (): void => { 333 | const layout: csmMap = new csmMap(); 334 | 335 | if (this._modelSetting == null || this._modelMatrix == null) { 336 | CubismLogError('Failed to setupLayout().'); 337 | return; 338 | } 339 | 340 | this._modelSetting.getLayoutMap(layout); 341 | this._modelMatrix.setupFromLayout(layout); 342 | this._state = LoadStep.LoadMotion; 343 | 344 | // callback 345 | loadCubismMotion(); 346 | }; 347 | 348 | // Motion 349 | const loadCubismMotion = (): void => { 350 | this._state = LoadStep.WaitLoadMotion; 351 | this._model.saveParameters(); 352 | this._allMotionCount = 0; 353 | this._motionCount = 0; 354 | const group: string[] = []; 355 | 356 | const motionGroupCount: number = this._modelSetting.getMotionGroupCount(); 357 | 358 | // モーションの総数を求める 359 | for (let i = 0; i < motionGroupCount; i++) { 360 | group[i] = this._modelSetting.getMotionGroupName(i); 361 | this._allMotionCount += this._modelSetting.getMotionCount(group[i]); 362 | } 363 | 364 | // モーションの読み込み 365 | for (let i = 0; i < motionGroupCount; i++) { 366 | this.preLoadMotionGroup(group[i]); 367 | } 368 | 369 | // モーションがない場合 370 | if (motionGroupCount == 0) { 371 | this._state = LoadStep.LoadTexture; 372 | 373 | // 全てのモーションを停止する 374 | this._motionManager.stopAllMotions(); 375 | 376 | this._updating = false; 377 | this._initialized = true; 378 | 379 | this.createRenderer(); 380 | this.setupTextures(); 381 | this.getRenderer().startUp(gl); 382 | } 383 | }; 384 | } 385 | 386 | /** 387 | * テクスチャユニットにテクスチャをロードする 388 | */ 389 | private setupTextures(): void { 390 | // iPhoneでのアルファ品質向上のためTypescriptではpremultipliedAlphaを採用 391 | const usePremultiply = true; 392 | 393 | if (this._state == LoadStep.LoadTexture) { 394 | // テクスチャ読み込み用 395 | const textureCount: number = this._modelSetting.getTextureCount(); 396 | 397 | for ( 398 | let modelTextureNumber = 0; 399 | modelTextureNumber < textureCount; 400 | modelTextureNumber++ 401 | ) { 402 | // テクスチャ名が空文字だった場合はロード・バインド処理をスキップ 403 | if (this._modelSetting.getTextureFileName(modelTextureNumber) == '') { 404 | console.log('getTextureFileName null'); 405 | continue; 406 | } 407 | 408 | // WebGLのテクスチャユニットにテクスチャをロードする 409 | let texturePath = 410 | this._modelSetting.getTextureFileName(modelTextureNumber); 411 | texturePath = this._modelHomeDir + texturePath; 412 | 413 | // ロード完了時に呼び出すコールバック関数 414 | const onLoad = (textureInfo: TextureInfo): void => { 415 | this.getRenderer().bindTexture(modelTextureNumber, textureInfo.id); 416 | 417 | this._textureCount++; 418 | 419 | if (this._textureCount >= textureCount) { 420 | // ロード完了 421 | this._state = LoadStep.CompleteSetup; 422 | } 423 | }; 424 | 425 | // 読み込み 426 | LAppDelegate.getInstance() 427 | .getTextureManager() 428 | .createTextureFromFile(texturePath, usePremultiply, onLoad); 429 | this.getRenderer().setIsPremultipliedAlpha(usePremultiply); 430 | } 431 | 432 | this._state = LoadStep.WaitLoadTexture; 433 | } 434 | } 435 | 436 | /** 437 | * レンダラを再構築する 438 | */ 439 | public reloadRenderer(): void { 440 | this.deleteRenderer(); 441 | this.createRenderer(); 442 | this.setupTextures(); 443 | } 444 | 445 | /** 446 | * 更新 447 | */ 448 | public update(): void { 449 | if (this._state != LoadStep.CompleteSetup) return; 450 | 451 | const deltaTimeSeconds: number = LAppPal.getDeltaTime(); 452 | this._userTimeSeconds += deltaTimeSeconds; 453 | 454 | this._dragManager.update(deltaTimeSeconds); 455 | this._dragX = this._dragManager.getX(); 456 | this._dragY = this._dragManager.getY(); 457 | 458 | // モーションによるパラメータ更新の有無 459 | let motionUpdated = false; 460 | 461 | //-------------------------------------------------------------------------- 462 | this._model.loadParameters(); // 前回セーブされた状態をロード 463 | if (this._motionManager.isFinished()) { 464 | // モーションの再生がない場合、待機モーションの中からランダムで再生する 465 | this.startRandomMotion( 466 | LAppDefine.MotionGroupIdle, 467 | LAppDefine.PriorityIdle 468 | ); 469 | } else { 470 | motionUpdated = this._motionManager.updateMotion( 471 | this._model, 472 | deltaTimeSeconds 473 | ); // モーションを更新 474 | } 475 | this._model.saveParameters(); // 状態を保存 476 | //-------------------------------------------------------------------------- 477 | 478 | // まばたき 479 | if (!motionUpdated) { 480 | if (this._eyeBlink != null) { 481 | // メインモーションの更新がないとき 482 | this._eyeBlink.updateParameters(this._model, deltaTimeSeconds); // 目パチ 483 | } 484 | } 485 | 486 | if (this._expressionManager != null) { 487 | this._expressionManager.updateMotion(this._model, deltaTimeSeconds); // 表情でパラメータ更新(相対変化) 488 | } 489 | 490 | // ドラッグによる変化 491 | // ドラッグによる顔の向きの調整 492 | this._model.addParameterValueById(this._idParamAngleX, this._dragX * 30); // -30から30の値を加える 493 | this._model.addParameterValueById(this._idParamAngleY, this._dragY * 30); 494 | this._model.addParameterValueById( 495 | this._idParamAngleZ, 496 | this._dragX * this._dragY * -30 497 | ); 498 | 499 | // ドラッグによる体の向きの調整 500 | this._model.addParameterValueById( 501 | this._idParamBodyAngleX, 502 | this._dragX * 10 503 | ); // -10から10の値を加える 504 | 505 | // ドラッグによる目の向きの調整 506 | this._model.addParameterValueById(this._idParamEyeBallX, this._dragX); // -1から1の値を加える 507 | this._model.addParameterValueById(this._idParamEyeBallY, this._dragY); 508 | 509 | // 呼吸など 510 | if (this._breath != null) { 511 | this._breath.updateParameters(this._model, deltaTimeSeconds); 512 | } 513 | 514 | // 物理演算の設定 515 | if (this._physics != null) { 516 | this._physics.evaluate(this._model, deltaTimeSeconds); 517 | } 518 | 519 | // リップシンクの設定 520 | if (this._lipsync) { 521 | let value = 0.0; // リアルタイムでリップシンクを行う場合、システムから音量を取得して、0~1の範囲で値を入力します。 522 | 523 | this._wavFileHandler.update(deltaTimeSeconds); 524 | value = this._wavFileHandler.getRms(); 525 | 526 | for (let i = 0; i < this._lipSyncIds.getSize(); ++i) { 527 | this._model.addParameterValueById(this._lipSyncIds.at(i), value, 0.8); 528 | } 529 | } 530 | 531 | // ポーズの設定 532 | if (this._pose != null) { 533 | this._pose.updateParameters(this._model, deltaTimeSeconds); 534 | } 535 | 536 | this._model.update(); 537 | } 538 | 539 | /** 540 | * 引数で指定したモーションの再生を開始する 541 | * @param group モーショングループ名 542 | * @param no グループ内の番号 543 | * @param priority 優先度 544 | * @param onFinishedMotionHandler モーション再生終了時に呼び出されるコールバック関数 545 | * @return 開始したモーションの識別番号を返す。個別のモーションが終了したか否かを判定するisFinished()の引数で使用する。開始できない時は[-1] 546 | */ 547 | public startMotion( 548 | group: string, 549 | no: number, 550 | priority: number, 551 | onFinishedMotionHandler?: FinishedMotionCallback 552 | ): CubismMotionQueueEntryHandle { 553 | if (priority == LAppDefine.PriorityForce) { 554 | this._motionManager.setReservePriority(priority); 555 | } else if (!this._motionManager.reserveMotion(priority)) { 556 | if (this._debugMode) { 557 | LAppPal.printMessage("[APP]can't start motion."); 558 | } 559 | return InvalidMotionQueueEntryHandleValue; 560 | } 561 | 562 | const motionFileName = this._modelSetting.getMotionFileName(group, no); 563 | 564 | // ex) idle_0 565 | const name = `${group}_${no}`; 566 | let motion: CubismMotion = this._motions.getValue(name) as CubismMotion; 567 | let autoDelete = false; 568 | 569 | if (motion == null) { 570 | cacheFetch(`${this._modelHomeDir}${motionFileName}`) 571 | .then(response => response.arrayBuffer()) 572 | .then(arrayBuffer => { 573 | motion = this.loadMotion( 574 | arrayBuffer, 575 | arrayBuffer.byteLength, 576 | null, 577 | onFinishedMotionHandler 578 | ); 579 | let fadeTime: number = this._modelSetting.getMotionFadeInTimeValue( 580 | group, 581 | no 582 | ); 583 | 584 | if (fadeTime >= 0.0) { 585 | motion.setFadeInTime(fadeTime); 586 | } 587 | 588 | fadeTime = this._modelSetting.getMotionFadeOutTimeValue(group, no); 589 | if (fadeTime >= 0.0) { 590 | motion.setFadeOutTime(fadeTime); 591 | } 592 | 593 | motion.setEffectIds(this._eyeBlinkIds, this._lipSyncIds); 594 | autoDelete = true; // 終了時にメモリから削除 595 | }); 596 | } else { 597 | motion.setFinishedMotionHandler(onFinishedMotionHandler); 598 | } 599 | 600 | //voice 601 | const voice = this._modelSetting.getMotionSoundFileName(group, no); 602 | if (voice.localeCompare('') != 0) { 603 | let path = voice; 604 | path = this._modelHomeDir + path; 605 | this._wavFileHandler.start(path); 606 | } 607 | 608 | if (this._debugMode) { 609 | LAppPal.printMessage(`[APP]start motion: [${group}_${no}`); 610 | } 611 | return this._motionManager.startMotionPriority( 612 | motion, 613 | autoDelete, 614 | priority 615 | ); 616 | } 617 | 618 | /** 619 | * ランダムに選ばれたモーションの再生を開始する。 620 | * @param group モーショングループ名 621 | * @param priority 優先度 622 | * @param onFinishedMotionHandler モーション再生終了時に呼び出されるコールバック関数 623 | * @return 開始したモーションの識別番号を返す。個別のモーションが終了したか否かを判定するisFinished()の引数で使用する。開始できない時は[-1] 624 | */ 625 | public startRandomMotion( 626 | group: string, 627 | priority: number, 628 | onFinishedMotionHandler?: FinishedMotionCallback 629 | ): CubismMotionQueueEntryHandle { 630 | if (this._modelSetting.getMotionCount(group) == 0) { 631 | return InvalidMotionQueueEntryHandleValue; 632 | } 633 | 634 | const no: number = Math.floor( 635 | Math.random() * this._modelSetting.getMotionCount(group) 636 | ); 637 | 638 | return this.startMotion(group, no, priority, onFinishedMotionHandler); 639 | } 640 | 641 | /** 642 | * 引数で指定した表情モーションをセットする 643 | * 644 | * @param expressionId 表情モーションのID 645 | */ 646 | public setExpression(expressionId: string): void { 647 | const motion: ACubismMotion = this._expressions.getValue(expressionId); 648 | 649 | if (this._debugMode) { 650 | LAppPal.printMessage(`[APP]expression: [${expressionId}]`); 651 | } 652 | 653 | if (motion != null) { 654 | this._expressionManager.startMotionPriority( 655 | motion, 656 | true, 657 | LAppDefine.PriorityForce 658 | ); 659 | } else { 660 | if (this._debugMode) { 661 | LAppPal.printMessage(`[APP]expression[${expressionId}] is null`); 662 | } 663 | } 664 | } 665 | 666 | /** 667 | * ランダムに選ばれた表情モーションをセットする 668 | */ 669 | public setRandomExpression(): void { 670 | if (this._expressions.getSize() == 0) { 671 | return; 672 | } 673 | 674 | const no: number = Math.floor(Math.random() * this._expressions.getSize()); 675 | 676 | const name: string = this._expressions._keyValues[no].first; 677 | LAppPal.printMessage('set expression: ' + name); 678 | this.setExpression(name); 679 | } 680 | 681 | /** 682 | * イベントの発火を受け取る 683 | */ 684 | public motionEventFired(eventValue: csmString): void { 685 | CubismLogInfo('{0} is fired on LAppModel!!', eventValue.s); 686 | } 687 | 688 | /** 689 | * 当たり判定テスト 690 | * 指定IDの頂点リストから矩形を計算し、座標をが矩形範囲内か判定する。 691 | * 692 | * @param hitArenaName 当たり判定をテストする対象のID 693 | * @param x 判定を行うX座標 694 | * @param y 判定を行うY座標 695 | */ 696 | public hitTest(hitArenaName: string, x: number, y: number): boolean { 697 | // 透明時は当たり判定無し。 698 | if (this._opacity < 1) { 699 | return false; 700 | } 701 | 702 | const count: number = this._modelSetting.getHitAreasCount(); 703 | 704 | for (let i = 0; i < count; i++) { 705 | if (this._modelSetting.getHitAreaName(i) == hitArenaName) { 706 | const drawId: CubismIdHandle = this._modelSetting.getHitAreaId(i); 707 | return this.isHit(drawId, x, y); 708 | } 709 | } 710 | 711 | return false; 712 | } 713 | 714 | /** 715 | * モーションデータをグループ名から一括でロードする。 716 | * モーションデータの名前は内部でModelSettingから取得する。 717 | * 718 | * @param group モーションデータのグループ名 719 | */ 720 | public preLoadMotionGroup(group: string): void { 721 | for (let i = 0; i < this._modelSetting.getMotionCount(group); i++) { 722 | const motionFileName = this._modelSetting.getMotionFileName(group, i); 723 | 724 | // ex) idle_0 725 | const name = `${group}_${i}`; 726 | if (this._debugMode) { 727 | LAppPal.printMessage( 728 | `[APP]load motion: ${motionFileName} => [${name}]` 729 | ); 730 | } 731 | 732 | cacheFetch(`${this._modelHomeDir}${motionFileName}`) 733 | .then(response => response.arrayBuffer()) 734 | .then(arrayBuffer => { 735 | const tmpMotion: CubismMotion = this.loadMotion( 736 | arrayBuffer, 737 | arrayBuffer.byteLength, 738 | name 739 | ); 740 | 741 | let fadeTime = this._modelSetting.getMotionFadeInTimeValue(group, i); 742 | if (fadeTime >= 0.0) { 743 | tmpMotion.setFadeInTime(fadeTime); 744 | } 745 | 746 | fadeTime = this._modelSetting.getMotionFadeOutTimeValue(group, i); 747 | if (fadeTime >= 0.0) { 748 | tmpMotion.setFadeOutTime(fadeTime); 749 | } 750 | tmpMotion.setEffectIds(this._eyeBlinkIds, this._lipSyncIds); 751 | 752 | if (this._motions.getValue(name) != null) { 753 | ACubismMotion.delete(this._motions.getValue(name)); 754 | } 755 | 756 | this._motions.setValue(name, tmpMotion); 757 | 758 | this._motionCount++; 759 | if (this._motionCount >= this._allMotionCount) { 760 | this._state = LoadStep.LoadTexture; 761 | 762 | // 全てのモーションを停止する 763 | this._motionManager.stopAllMotions(); 764 | 765 | this._updating = false; 766 | this._initialized = true; 767 | 768 | this.createRenderer(); 769 | this.setupTextures(); 770 | this.getRenderer().startUp(gl); 771 | } 772 | }); 773 | } 774 | } 775 | 776 | /** 777 | * すべてのモーションデータを解放する。 778 | */ 779 | public releaseMotions(): void { 780 | this._motions.clear(); 781 | } 782 | 783 | /** 784 | * 全ての表情データを解放する。 785 | */ 786 | public releaseExpressions(): void { 787 | this._expressions.clear(); 788 | } 789 | 790 | /** 791 | * モデルを描画する処理。モデルを描画する空間のView-Projection行列を渡す。 792 | */ 793 | public doDraw(): void { 794 | if (this._model == null) return; 795 | 796 | // キャンバスサイズを渡す 797 | const viewport: number[] = [0, 0, canvas.width, canvas.height]; 798 | 799 | this.getRenderer().setRenderState(frameBuffer, viewport); 800 | this.getRenderer().drawModel(); 801 | } 802 | 803 | /** 804 | * モデルを描画する処理。モデルを描画する空間のView-Projection行列を渡す。 805 | */ 806 | public draw(matrix: CubismMatrix44): void { 807 | if (this._model == null) { 808 | return; 809 | } 810 | 811 | // 各読み込み終了後 812 | if (this._state == LoadStep.CompleteSetup) { 813 | matrix.multiplyByMatrix(this._modelMatrix); 814 | 815 | this.getRenderer().setMvpMatrix(matrix); 816 | 817 | this.doDraw(); 818 | } 819 | } 820 | 821 | public async hasMocConsistencyFromFile() { 822 | CSM_ASSERT(this._modelSetting.getModelFileName().localeCompare(``)); 823 | 824 | // CubismModel 825 | if (this._modelSetting.getModelFileName() != '') { 826 | const modelFileName = this._modelSetting.getModelFileName(); 827 | 828 | const response = await cacheFetch(`${this._modelHomeDir}${modelFileName}`); 829 | const arrayBuffer = await response.arrayBuffer(); 830 | 831 | this._consistency = CubismMoc.hasMocConsistency(arrayBuffer); 832 | 833 | if (!this._consistency) { 834 | CubismLogInfo('Inconsistent MOC3.'); 835 | } else { 836 | CubismLogInfo('Consistent MOC3.'); 837 | } 838 | 839 | return this._consistency; 840 | } else { 841 | LAppPal.printMessage('Model data does not exist.'); 842 | } 843 | } 844 | 845 | /** 846 | * コンストラクタ 847 | */ 848 | public constructor() { 849 | super(); 850 | 851 | this._modelSetting = null; 852 | this._modelHomeDir = null; 853 | this._userTimeSeconds = 0.0; 854 | 855 | this._eyeBlinkIds = new csmVector(); 856 | this._lipSyncIds = new csmVector(); 857 | 858 | this._motions = new csmMap(); 859 | this._expressions = new csmMap(); 860 | 861 | this._hitArea = new csmVector(); 862 | this._userArea = new csmVector(); 863 | 864 | this._idParamAngleX = CubismFramework.getIdManager().getId( 865 | CubismDefaultParameterId.ParamAngleX 866 | ); 867 | this._idParamAngleY = CubismFramework.getIdManager().getId( 868 | CubismDefaultParameterId.ParamAngleY 869 | ); 870 | this._idParamAngleZ = CubismFramework.getIdManager().getId( 871 | CubismDefaultParameterId.ParamAngleZ 872 | ); 873 | this._idParamEyeBallX = CubismFramework.getIdManager().getId( 874 | CubismDefaultParameterId.ParamEyeBallX 875 | ); 876 | this._idParamEyeBallY = CubismFramework.getIdManager().getId( 877 | CubismDefaultParameterId.ParamEyeBallY 878 | ); 879 | this._idParamBodyAngleX = CubismFramework.getIdManager().getId( 880 | CubismDefaultParameterId.ParamBodyAngleX 881 | ); 882 | 883 | if (LAppDefine.MOCConsistencyValidationEnable) { 884 | this._mocConsistency = true; 885 | } 886 | 887 | this._state = LoadStep.LoadAssets; 888 | this._expressionCount = 0; 889 | this._textureCount = 0; 890 | this._motionCount = 0; 891 | this._allMotionCount = 0; 892 | this._wavFileHandler = new LAppWavFileHandler(); 893 | this._consistency = false; 894 | } 895 | 896 | _modelSetting: ICubismModelSetting; // モデルセッティング情報 897 | _modelHomeDir: string; // モデルセッティングが置かれたディレクトリ 898 | _userTimeSeconds: number; // デルタ時間の積算値[秒] 899 | 900 | _eyeBlinkIds: csmVector; // モデルに設定された瞬き機能用パラメータID 901 | _lipSyncIds: csmVector; // モデルに設定されたリップシンク機能用パラメータID 902 | 903 | _motions: csmMap; // 読み込まれているモーションのリスト 904 | _expressions: csmMap; // 読み込まれている表情のリスト 905 | 906 | _hitArea: csmVector; 907 | _userArea: csmVector; 908 | 909 | _idParamAngleX: CubismIdHandle; // パラメータID: ParamAngleX 910 | _idParamAngleY: CubismIdHandle; // パラメータID: ParamAngleY 911 | _idParamAngleZ: CubismIdHandle; // パラメータID: ParamAngleZ 912 | _idParamEyeBallX: CubismIdHandle; // パラメータID: ParamEyeBallX 913 | _idParamEyeBallY: CubismIdHandle; // パラメータID: ParamEyeBAllY 914 | _idParamBodyAngleX: CubismIdHandle; // パラメータID: ParamBodyAngleX 915 | 916 | _state: number; // 現在のステータス管理用 917 | _expressionCount: number; // 表情データカウント 918 | _textureCount: number; // テクスチャカウント 919 | _motionCount: number; // モーションデータカウント 920 | _allMotionCount: number; // モーション総数 921 | _wavFileHandler: LAppWavFileHandler; //wavファイルハンドラ 922 | _consistency: boolean; // MOC3一貫性チェック管理用 923 | } 924 | --------------------------------------------------------------------------------