├── .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 | 
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 |
--------------------------------------------------------------------------------