├── .babelrc ├── .eslintrc.js ├── types ├── index.d.ts ├── const.d.ts ├── frame │ ├── MorphFrame.d.ts │ ├── LightFrame.d.ts │ ├── CameraFrame.d.ts │ └── BoneFrame.d.ts ├── stream │ ├── WriteBufferStream.d.ts │ └── ReadBufferStream.d.ts └── Vmd.d.ts ├── src ├── index.js ├── util.js ├── frame │ ├── MorphFrame.js │ ├── LightFrame.js │ ├── CameraFrame.js │ └── BoneFrame.js ├── const.js ├── Vmd.js └── stream │ ├── ReadBufferStream.js │ └── WriteBufferStream.js ├── config └── rollup.config.js ├── LICENSE ├── package.json ├── README-ZH.md ├── .gitignore └── README.md /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ] 5 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true 6 | }, 7 | extends: [ 8 | 'standard' 9 | ], 10 | parserOptions: { 11 | ecmaVersion: 12, 12 | sourceType: 'module' 13 | }, 14 | rules: { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import Vmd from './Vmd' 2 | 3 | export { default as Vmd, VERSION } from './Vmd' 4 | 5 | export const BONE_NAME: string[] 6 | 7 | export { default as BoneFrame } from './frame/BoneFrame' 8 | export { default as CameraFrame } from './frame/CameraFrame' 9 | export { default as LightFrame } from './frame/LightFrame' 10 | export { default as MorphFrame } from './frame/MorphFrame' 11 | 12 | export default Vmd -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Vmd from './Vmd.js' 2 | 3 | export { default as Vmd } from './Vmd' 4 | 5 | export { BONE_NAME, VERSION } from './const' 6 | 7 | export { default as BoneFrame } from './frame/BoneFrame' 8 | export { default as MorphFrame } from './frame/MorphFrame' 9 | export { default as CameraFrame } from './frame/CameraFrame' 10 | export { default as LightFrame } from './frame/LightFrame' 11 | 12 | export default Vmd 13 | -------------------------------------------------------------------------------- /types/const.d.ts: -------------------------------------------------------------------------------- 1 | import BoneFrame from './frame/BoneFrame' 2 | import CameraFrame from './frame/CameraFrame' 3 | import LightFrame from './frame/LightFrame' 4 | import MorphFrame from './frame/MorphFrame' 5 | 6 | /** 7 | * 数据类型 8 | */ 9 | export type Type = Uint16Array | Uint32Array | Uint8Array | Int8Array | Int16Array | Int32Array | Float32Array | Float64Array 10 | 11 | export type FrameType = BoneFrame | CameraFrame | LightFrame | MorphFrame -------------------------------------------------------------------------------- /types/frame/MorphFrame.d.ts: -------------------------------------------------------------------------------- 1 | import WriteBufferStream from '../stream/WriteBufferStream' 2 | import ReadBufferStream from '../stream/ReadBufferStream' 3 | export default class LightFrame { 4 | constructor(stream?: ReadBufferStream) 5 | 6 | /** 7 | * 将自身数据写入stream 8 | * @param stream 9 | */ 10 | writeBuffer(stream: WriteBufferStream) 11 | /** 12 | * 表情名称 MorphName 13 | * byte*15(ShiftJIS) 14 | */ 15 | morphName: string 16 | /** 17 | * 关键帧时间 FrameTime 18 | * uint32_t 19 | */ 20 | frameTime: number 21 | /** 22 | * 程度 Weight 23 | * float 24 | */ 25 | weight: number 26 | 27 | } -------------------------------------------------------------------------------- /types/frame/LightFrame.d.ts: -------------------------------------------------------------------------------- 1 | import WriteBufferStream from '../stream/WriteBufferStream' 2 | import ReadBufferStream from '../stream/ReadBufferStream' 3 | export default class LightFrame { 4 | constructor(stream?: ReadBufferStream) 5 | 6 | /** 7 | * 将自身数据写入stream 8 | * @param stream 9 | */ 10 | writeBuffer(stream: WriteBufferStream) 11 | /** 12 | * 关键帧时间 FrameTime 13 | * uint32_t 14 | */ 15 | frameTime: number 16 | /** 17 | * RGB颜色空间 color.rgb 18 | * float*3 19 | */ 20 | rgb: [number, number, number] 21 | /** 22 | * xyz投射方向 Direction.xyz 23 | * float*3 24 | */ 25 | direction: [number, number, number] 26 | 27 | } -------------------------------------------------------------------------------- /config/rollup.config.js: -------------------------------------------------------------------------------- 1 | import json from '@rollup/plugin-json' 2 | import babel from 'rollup-plugin-babel' 3 | import nodePolyfills from 'rollup-plugin-node-polyfills' 4 | import { terser } from 'rollup-plugin-terser' 5 | 6 | const output = { 7 | name: 'Vmd', 8 | file: 'dist/vmd.js', 9 | format: 'umd', 10 | exports: 'named', 11 | globals: { 12 | shiftjis: 'shiftjis' 13 | } 14 | } 15 | 16 | export default { 17 | input: 'src/index.js', 18 | output: [ 19 | output, 20 | { 21 | ...output, 22 | file: 'dist/vmd.min.js', 23 | plugins: [terser()] 24 | } 25 | ], 26 | external: ['shiftjis'], 27 | plugins: [ 28 | nodePolyfills(), 29 | json(), 30 | babel() 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /types/stream/WriteBufferStream.d.ts: -------------------------------------------------------------------------------- 1 | import { Type, FrameType } from '../const' 2 | export default class WriteBufferStream { 3 | constructor() 4 | 5 | bufferList: ArrayBuffer[] 6 | 7 | /** 8 | * 写入数组 9 | * @param array 10 | */ 11 | writeTypedFrameArray(array: FrameType[]): WriteBufferStream 12 | 13 | 14 | writeBytes(length?: number): WriteBufferStream 15 | 16 | writeInt(value: number): WriteBufferStream 17 | 18 | writeFloat(value: number): WriteBufferStream 19 | 20 | writeString(text: string, length?: number): WriteBufferStream 21 | 22 | writeByType(value: number, Type: Type, offset?: number, littleEndian?: boolean): WriteBufferStream 23 | 24 | writeArrayByType(value: number[], Type?: Type, offset?: number, littleEndian?: boolean): WriteBufferStream 25 | 26 | getArrayBuffer(): ArrayBuffer 27 | } -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 内部日语用的翻译器 3 | */ 4 | import { encode, decode } from 'shiftjis' 5 | 6 | /** 7 | * 生成一个Array 8 | * @param {number} length 9 | * @param {number} [initValue] 10 | * @returns {number[]} 11 | */ 12 | export function generateArray (length, initValue = 0) { 13 | if (!length) { 14 | throw new Error('length is required') 15 | } 16 | return Array.from(new Array(length)).map(() => initValue) 17 | } 18 | 19 | /** 20 | * buffer转string 21 | * @param {ArrayBuffer} arrayBuffer 22 | */ 23 | export function buffer2string (arrayBuffer) { 24 | const uint8Array = new Uint8Array(arrayBuffer) 25 | 26 | // 这里因为长度的问题,其实buffer里填充用的,需要过滤掉填充字符 27 | const emptyFillIndex = uint8Array.indexOf(0) 28 | const buffer = uint8Array.slice(0, emptyFillIndex === -1 ? undefined : emptyFillIndex) 29 | const text = decode(buffer) 30 | 31 | return text 32 | } 33 | 34 | export function string2buffer (text) { 35 | return encode(text) 36 | } 37 | -------------------------------------------------------------------------------- /src/frame/MorphFrame.js: -------------------------------------------------------------------------------- 1 | 2 | export default class MorphFrame { 3 | /** 4 | * @param {import('../stream/ReadBufferStream').default} stream 5 | */ 6 | constructor (stream) { 7 | /** 8 | * 表情名称 MorphName 9 | * byte*15(ShiftJIS) 10 | */ 11 | this.morphName = '' 12 | /** 13 | * 关键帧时间 FrameTime 14 | * uint32_t 15 | */ 16 | this.frameTime = 0 17 | /** 18 | * 程度 Weight 19 | * float 20 | */ 21 | this.weight = 0 22 | 23 | if (stream) { 24 | this.morphName = stream.readString(15) 25 | this.frameTime = stream.readInt() 26 | this.weight = stream.readFloat() 27 | } 28 | } 29 | 30 | /** 31 | * 将本身数据写入stream 32 | * @param {import('../stream/WriteBufferStream').default} stream 33 | */ 34 | writeBuffer (stream) { 35 | if (!stream) { 36 | throw new Error('no stream!') 37 | } 38 | stream.writeString(this.morphName, 15) 39 | stream.writeInt(this.frameTime) 40 | stream.writeFloat(this.weight) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /types/Vmd.d.ts: -------------------------------------------------------------------------------- 1 | import BoneFrame from './frame/BoneFrame' 2 | import CameraFrame from './frame/CameraFrame' 3 | import LightFrame from './frame/LightFrame' 4 | import MorphFrame from './frame/MorphFrame' 5 | 6 | export type VERSION = { 7 | V1: 'Vocaloid Motion Data file', 8 | V2: 'Vocaloid Motion Data 0002' 9 | } 10 | 11 | export default class Vmd { 12 | constructor(buffer: ArrayBuffer) 13 | 14 | version: VERSION[keyof VERSION]; 15 | 16 | /** 17 | * @type {string} 当前使用的模型名字 18 | */ 19 | modelName: string 20 | 21 | /** 22 | * 骨骼关键帧 23 | */ 24 | boneFrames: BoneFrame[] 25 | /** 26 | * 表情关键帧 27 | */ 28 | morphFrames: MorphFrame[] 29 | /** 30 | * 镜头关键帧 31 | */ 32 | cameraFrames: CameraFrame[] 33 | /** 34 | * 光线关键帧 35 | */ 36 | lightFrames: LightFrame[] 37 | 38 | get timeline(): { 39 | frameTime: number, 40 | boneFrames: BoneFrame[], 41 | cameraFrames: CameraFrame[], 42 | lightFrames: LightFrame[], 43 | morphFrames: MorphFrame[] 44 | }[] 45 | 46 | write(): ArrayBuffer 47 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Mizuka-wu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/frame/LightFrame.js: -------------------------------------------------------------------------------- 1 | import { TYPE } from '../const' 2 | export default class LightFrame { 3 | /** 4 | * @param {import('../stream/ReadBufferStream').default} stream 5 | */ 6 | constructor (stream) { 7 | /** 8 | * 关键帧时间 FrameTime 9 | * uint32_t 10 | */ 11 | this.frameTime = 0 12 | /** 13 | * RGB颜色空间 color.rgb 14 | * float*3 15 | */ 16 | this.rgb = [0, 0, 0] 17 | /** 18 | * xyz投射方向 Direction.xyz 19 | * float*3 20 | */ 21 | this.direction = [0, 0, 0] 22 | 23 | if (stream) { 24 | this.frameTime = stream.readInt() 25 | this.rgb = stream.readArrayByType(3, TYPE.float) 26 | this.direction = stream.readArrayByType(3, TYPE.float) 27 | } 28 | } 29 | 30 | /** 31 | * 将本身数据写入stream 32 | * @param {import('../stream/WriteBufferStream').default} stream 33 | */ 34 | writeBuffer (stream) { 35 | if (!stream) { 36 | throw new Error('no stream!') 37 | } 38 | stream.writeInt(this.frameTime) 39 | stream.writeArrayByType(this.rgb, TYPE.float) 40 | stream.writeArrayByType(this.direction, TYPE.float) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /types/frame/CameraFrame.d.ts: -------------------------------------------------------------------------------- 1 | import WriteBufferStream from '../stream/WriteBufferStream' 2 | import ReadBufferStream from '../stream/ReadBufferStream' 3 | export default class CameraFrame { 4 | constructor(stream?: ReadBufferStream) 5 | 6 | /** 7 | * 将自身数据写入stream 8 | * @param stream 9 | */ 10 | writeBuffer(stream: WriteBufferStream) 11 | /** 12 | * 关键帧时间 FrameTime 13 | * uint32_t 14 | */ 15 | frameTime: number 16 | /** 17 | * 距离 Distance 18 | * float 19 | */ 20 | distance: number 21 | /** 22 | * x,y,z空间坐标 Position.xyz 23 | * float*3 24 | */ 25 | position: [number, number, number] 26 | /** 27 | * 旋转角度(弧度制) Rotation.xyz 28 | * float*3 29 | */ 30 | rotation: [number, number, number] 31 | /** 32 | * 相机曲线 Curve 33 | * uint8_t*24 34 | */ 35 | curve: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number] 36 | /** 37 | * 镜头FOV角度 ViewAngle 38 | * float 39 | */ 40 | viewAngle: number 41 | /** 42 | * Orthographic相机 43 | * uint8_t 44 | */ 45 | orthographic: number 46 | 47 | } -------------------------------------------------------------------------------- /types/stream/ReadBufferStream.d.ts: -------------------------------------------------------------------------------- 1 | import { Type, FrameType } from '../const' 2 | export default class ReadBufferStream { 3 | /** 4 | * 默认的buffer 5 | * @param buffer 6 | */ 7 | constructor(buffer?: ArrayBuffer) 8 | 9 | /** 10 | * 读取关键帧数组 11 | * @param Constructor 12 | */ 13 | readArrayByConstructor(Constructor: FrameType): FrameType[] 14 | 15 | /** 16 | * 读取指定字节的数据 17 | * @param length 18 | */ 19 | readBytes(length: number): ArrayBuffer 20 | 21 | /** 22 | * 读取uint32_t 23 | */ 24 | readInt(): number 25 | 26 | /** 27 | * 读取float 28 | */ 29 | readFloat(): number 30 | 31 | /** 32 | * 读取指定长度的文字,会自动过滤 33 | * @param length 34 | */ 35 | readString(length: number): string 36 | 37 | /** 38 | * 根据类型来读取 39 | * @param Type 40 | * @param offset 41 | * @param littleEndian 42 | */ 43 | readByType(Type: Type, offset?: number, littleEndian?: boolean) 44 | 45 | /** 46 | * 根据类型来读取多个 47 | * @param length 48 | * @param Type 49 | * @param offset 50 | * @param littleEndian 51 | */ 52 | readArrayByType(length?: number, Type?: Type, offset?: number, littleEndian?: boolean) 53 | 54 | /** 55 | * 剩余未读字节 56 | */ 57 | get restBytes(): number 58 | 59 | /** 60 | * 清空 61 | */ 62 | close(): void 63 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vmd.js", 3 | "version": "1.0.2", 4 | "description": "Read and edit mmd's vmd file", 5 | "main": "dist/vmd.min.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/mizuka-wu/vmd-js.git" 9 | }, 10 | "typings": "types/index.d.ts", 11 | "files": [ 12 | "src/*", 13 | "dist/*", 14 | "types/*" 15 | ], 16 | "scripts": { 17 | "build": "rollup -c config/rollup.config.js", 18 | "test": "echo \"Error: no test specified\" && exit 1" 19 | }, 20 | "keywords": [ 21 | "mmd", 22 | "vmd", 23 | "mikumikudance", 24 | "js", 25 | "javascript" 26 | ], 27 | "author": "Mizuka.wu ", 28 | "license": "MIT", 29 | "dependencies": { 30 | "iconv-lite": "^0.6.2", 31 | "safer-buffer": "^2.1.2", 32 | "shiftjis": "^1.0.0" 33 | }, 34 | "devDependencies": { 35 | "@babel/core": "^7.12.9", 36 | "@babel/preset-env": "^7.12.7", 37 | "@rollup/plugin-json": "^4.1.0", 38 | "babel-preset-es2015-rollup": "^3.0.0", 39 | "dts-bundle-generator": "^5.5.0", 40 | "eslint": "^7.15.0", 41 | "eslint-config-standard": "^16.0.2", 42 | "eslint-plugin-import": "^2.22.1", 43 | "eslint-plugin-node": "^11.1.0", 44 | "eslint-plugin-promise": "^4.2.1", 45 | "rollup": "^2.34.1", 46 | "rollup-plugin-babel": "^4.4.0", 47 | "rollup-plugin-node-polyfills": "^0.2.1", 48 | "rollup-plugin-terser": "^7.0.2" 49 | }, 50 | "browserslist": [ 51 | "last 1 version", 52 | "> 1%", 53 | "maintained node versions", 54 | "not dead" 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /src/const.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 一些配置 3 | */ 4 | 5 | export const VERSION = { 6 | V1: 'Vocaloid Motion Data file', 7 | V2: 'Vocaloid Motion Data 0002' 8 | } 9 | 10 | export const VERSION_BUFFER_LENGTH = 30 11 | 12 | export const MODEL_NAME_LENGTH = { 13 | [VERSION.V1]: 10, 14 | [VERSION.V2]: 20 15 | } 16 | 17 | export const TYPE = { 18 | int8_t: Int8Array, 19 | uint8_t: Uint8Array, 20 | int16_t: Int16Array, 21 | uint16_t: Uint16Array, 22 | int32_t: Int32Array, 23 | uint32_t: Uint32Array, 24 | float: Float32Array, 25 | double: Float64Array 26 | } 27 | 28 | /** 29 | * 骨骼名称 30 | */ 31 | export const BONE_NAME = ['センター', '上半身', '首', '頭', '左目', '右目', 'ネクタイ1', 'ネクタイ2', 'ネクタイ3', '下半身', '腰飾り', '左髪1', '左髪2', '左髪3', '左髪4', '左髪5', '左髪6', '左肩', '左腕', '左腕捩', '左ひじ', '左手捩', '左手首', '左袖', '左親指1', '左親指2', '左人指1', '左人指2', '左人指3', '左中指1', '左中指2', '左中指3', '左薬指1', '左薬指2', '左薬指3', '左小指1', '左小指2', '左小指3', '左スカート前', '左スカート後', '左足', '左ひざ', '左足首', '右髪1', '右髪2', '右髪3', '右髪4', '右髪5', '右髪6', '右肩', '右腕', '右腕捩', '右ひじ', '右手捩', '右手首', '右袖', '右親指1', '右親指2', '右人指1', '右人指2', '右人指3', '右中指1', '右中指2', '右中指3', '右薬指1', '右薬指2', '右薬指3', '右小指1', '右小指2', '右小指3', '右スカート前', '右スカート後', '右足', '右ひざ', '右足首', '両目', '前髪1', '前髪2', '前髪3', '左目光', '右目光', 'ネクタイ4', '左髪7', '右髪7', '左つま先', '右つま先', 'ネクタイIK', '左髪IK', '右髪IK', '左足IK', '右足IK', '左つま先IK', '右つま先IK', '下半身先', '頭先', '左目先', '右目先', '腰飾り先', '左袖先', '左手先', '左親指先', '左人差指先', '左中指先', '左薬指先', '左小指先', '左スカート前先', '左スカート後先', '右袖先', '右手先', '右親指先', '右人差指先', '右中指先', '右薬指先', '右小指先', '右スカート前先', '右スカート後先', 'センター先', '両目先', 'ネクタイIK先', '左髪IK先', '右髪IK先', '左足IK先', '右足IK先', '左つま先IK先', '右つま先IK先', '前髪1先', '前髪2先', '前髪3先', '左目光先', '右目光先', '左腕捩先', '左手捩先', '右腕捩先', '右手捩先', '左腕捩1', '左腕捩2', '左腕捩3', '右腕捩1', '右腕捩2', '右腕捩3'] 32 | -------------------------------------------------------------------------------- /types/frame/BoneFrame.d.ts: -------------------------------------------------------------------------------- 1 | import WriteBufferStream from '../stream/WriteBufferStream' 2 | import ReadBufferStream from '../stream/ReadBufferStream' 3 | export default class BoneFrame { 4 | constructor(stream?: ReadBufferStream) 5 | 6 | /** 7 | * 将自身数据写入stream 8 | * @param stream 9 | */ 10 | writeBuffer(stream: WriteBufferStream) 11 | /** 12 | * 骨骼名称 BoneName 13 | * byte*15(ShiftJIS) 14 | */ 15 | boneName: string 16 | 17 | /** 18 | * 关键帧时间 FrameTime 19 | * uint32_t 20 | */ 21 | frameTime: number 22 | /** 23 | * x,y,z空间坐标 Translation.xyz 24 | * float*3 25 | */ 26 | translation: [number, number, number] 27 | /** 28 | * 旋转四元数x,y,z,w Rotation.xyzw 29 | * float*4 30 | */ 31 | rotation: [number, number, number, number] 32 | /** 33 | * 补间曲线x的坐标 XCurve 34 | * uint8_t*16 35 | */ 36 | curveX: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number] 37 | /** 38 | * 补间曲线y的坐标 YCurve 39 | * uint8_t*16 40 | */ 41 | curveY: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number] 42 | /** 43 | * 补间曲线z的坐标 ZCurve 44 | * uint8_t*16 45 | */ 46 | curveZ: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number] 47 | /** 48 | * 补间曲线旋转的坐标 RCurve 49 | * uint8_t*16 50 | */ 51 | curveR: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number] 52 | 53 | } -------------------------------------------------------------------------------- /src/frame/CameraFrame.js: -------------------------------------------------------------------------------- 1 | import { TYPE } from '../const' 2 | import { generateArray } from '../util' 3 | export default class CameraFrame { 4 | /** 5 | * @param {import('../stream/ReadBufferStream').default} stream 6 | */ 7 | constructor (stream) { 8 | /** 9 | * 关键帧时间 FrameTime 10 | * uint32_t 11 | */ 12 | this.frameTime = 0 13 | /** 14 | * 距离 Distance 15 | * float 16 | */ 17 | this.distance = 0 18 | /** 19 | * x,y,z空间坐标 Position.xyz 20 | * float*3 21 | */ 22 | this.position = generateArray(3) 23 | /** 24 | * 旋转角度(弧度制) Rotation.xyz 25 | * float*3 26 | */ 27 | this.rotation = generateArray(3) 28 | /** 29 | * 相机曲线 Curve 30 | * uint8_t*24 31 | */ 32 | this.curve = generateArray(24) 33 | /** 34 | * 镜头FOV角度 ViewAngle 35 | * float 36 | */ 37 | this.viewAngle = 0 38 | /** 39 | * Orthographic相机 40 | * uint8_t 41 | */ 42 | this.orthographic = 0 43 | 44 | if (stream) { 45 | this.frameTime = stream.readInt() 46 | this.distance = stream.readFloat() 47 | this.position = stream.readArrayByType(3, TYPE.float) 48 | this.rotation = stream.readArrayByType(3, TYPE.float) 49 | this.curve = stream.readArrayByType(24, TYPE.uint8_t) 50 | this.viewAngle = stream.readInt() 51 | this.orthographic = stream.readInt() 52 | } 53 | } 54 | 55 | /** 56 | * 将本身数据写入stream 57 | * @param {import('../stream/WriteBufferStream').default} stream 58 | */ 59 | writeBuffer (stream) { 60 | if (!stream) { 61 | throw new Error('no stream!') 62 | } 63 | stream.writeInt(this.frameTime) 64 | stream.writeFloat(this.distance) 65 | stream.writeArrayByType(this.position, TYPE.float) 66 | stream.writeArrayByType(this.rotation, TYPE.float) 67 | stream.writeInt(this.viewAngle) 68 | stream.writeInt(this.orthographic) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/frame/BoneFrame.js: -------------------------------------------------------------------------------- 1 | import { TYPE } from '../const' 2 | import { generateArray } from '../util' 3 | export default class BoneFrame { 4 | /** 5 | * @param {import('../stream/ReadBufferStream').default} [stream] 6 | */ 7 | constructor (stream) { 8 | /** 9 | * 骨骼名称 BoneName 10 | * byte*15(ShiftJIS) 11 | */ 12 | this.boneName = '' 13 | /** 14 | * 关键帧时间 FrameTime 15 | * uint32_t 16 | */ 17 | this.frameTime = 0 18 | /** 19 | * x,y,z空间坐标 Translation.xyz 20 | * float*3 21 | */ 22 | this.translation = generateArray(3) 23 | /** 24 | * 旋转四元数x,y,z,w Rotation.xyzw 25 | * float*4 26 | */ 27 | this.rotation = generateArray(4) 28 | /** 29 | * 补间曲线x的坐标 XCurve 30 | * uint8_t*16 31 | */ 32 | this.curveX = generateArray(16) 33 | /** 34 | * 补间曲线y的坐标 YCurve 35 | * uint8_t*16 36 | */ 37 | this.curveY = generateArray(16) 38 | /** 39 | * 补间曲线z的坐标 ZCurve 40 | * uint8_t*16 41 | */ 42 | this.curveZ = generateArray(16) 43 | /** 44 | * 补间曲线旋转的坐标 RCurve 45 | * uint8_t*16 46 | */ 47 | this.curveR = generateArray(16) 48 | 49 | if (stream) { 50 | this.boneName = stream.readString(15) 51 | this.frameTime = stream.readInt() 52 | this.translation = stream.readArrayByType(3, TYPE.float) 53 | this.rotation = stream.readArrayByType(4, TYPE.float) 54 | this.curveX = stream.readArrayByType(16, TYPE.uint8_t) 55 | this.curveY = stream.readArrayByType(16, TYPE.uint8_t) 56 | this.curveZ = stream.readArrayByType(16, TYPE.uint8_t) 57 | this.curveR = stream.readArrayByType(16, TYPE.uint8_t) 58 | } 59 | } 60 | 61 | /** 62 | * 将本身数据写入stream 63 | * @param {import('../stream/WriteBufferStream').default} stream 64 | */ 65 | writeBuffer (stream) { 66 | if (!stream) { 67 | throw new Error('no stream!') 68 | } 69 | stream.writeString(this.boneName, 15) 70 | stream.writeInt(this.frameTime) 71 | stream.writeArrayByType(this.translation, TYPE.float) 72 | stream.writeArrayByType(this.rotation, TYPE.float) 73 | stream.writeArrayByType(this.curveX, TYPE.uint8_t) 74 | stream.writeArrayByType(this.curveY, TYPE.uint8_t) 75 | stream.writeArrayByType(this.curveZ, TYPE.uint8_t) 76 | stream.writeArrayByType(this.curveR, TYPE.uint8_t) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /README-ZH.md: -------------------------------------------------------------------------------- 1 | # Vmd.js 2 | 3 | 用`js`加载和修改你的`Mikumikudance vmd`文件! 4 | 5 | ## 特性 6 | 7 | `Vmd`文件是一个二进制文件,目前除了使用各类加载器直接加载读取以外,只能通过`windows`上的`mikumikudance`工具进行编辑然后保存导出 8 | 9 | ### 纯js解析 10 | 11 | 根据大佬的文件格式解析文档,对`ArrayBuffer`直接读取二进制流然后解析,感谢 12 | 13 | * [官方文档](http://mikumikudance.wikia.com/wiki/VMD_file_format) 14 | * [【MMD】用python解析VMD格式读取](https://www.jianshu.com/p/ae312fb53fc3?from=groupmessage&isappinstalled=0) 15 | 16 | ### 时间轴 17 | 18 | vmd.js返回的对象会添加一个`timeline`属性,可以根据这个来查看`timeLine` 19 | 20 | ## 例子 21 | 22 | ### 安装 23 | 24 | ```bash 25 | npm i vmd.js 26 | # or 27 | yarn add vmd.js 28 | ``` 29 | 30 | ### 引入 31 | 32 | ```javascript 33 | 34 | import Vmd from 'vmd.js' 35 | 36 | // or 37 | 38 | const Vmd = require('vmd') 39 | ``` 40 | 41 | ### 从文件创建vmd 42 | 43 | ```javascript 44 | 45 | fetch('test.vmd') 46 | .then(res => res.blob()) 47 | .then(blob => blob.arrayBuffer()) 48 | .then(arrayBuffer => new Vmd(arrayBuffer)) 49 | 50 | ``` 51 | 52 | ### 将vmd导出为arrayBuffer 53 | 54 | ```javascript 55 | 56 | import { saveAs } from 'file-saver' 57 | const arrayBuffer = vmd.write() 58 | const url = URL.createObjectURL(new Blob([arrayBuffer])) 59 | saveAs(url, 'your.vmd') 60 | 61 | ``` 62 | 63 | ## 提供的一些工具类 64 | 65 | ### VERSION 66 | 67 | `V1`, `V2` 版本的字符串 68 | 69 | ### BONE_NAME 70 | 71 | 日语的骨骼名名单 类似于`['センター', '上半身', '首', '頭', ...]` 72 | 73 | ### BoneFrame 74 | 75 | 骨骼帧 76 | 77 | * boneName 78 | * frameTime 79 | * translation 80 | * rotation 81 | * curveX 82 | * curveY 83 | * curveZ 84 | * curveR 85 | 86 | ### MorphFrame 87 | 88 | 表情帧 89 | 90 | * morphName 91 | * frameTime 92 | * weight 93 | 94 | ### CameraFrame 95 | 96 | 摄影机帧 97 | 98 | * frameTime 99 | * distance 100 | * position 101 | * rotation 102 | * curve 103 | * viewAngle 104 | * orthographic 105 | 106 | ### LightFrame 107 | 108 | 光线帧 109 | 110 | * frameTime 111 | * rgb 112 | * direction 113 | 114 | ## Vmd类 115 | 116 | 包含了`vmd`文件的所有数据 117 | 118 | ### 变量 119 | 120 | * version - 版本,有V1 和 V2的区别 121 | * modelName - 模型名字,有长度限制 122 | * boneFrames - 骨骼关键帧 123 | * morphFrames - 表情关键帧 124 | * cameraFrames - 镜头关键帧 125 | * lightFrames - 光线关键帧 126 | 127 | * **timeline** - 只读变量,根据关键帧的时间自动排序 `{frameTime: number, boneFrames: BoneFrame[], morphFrames: MorphFrame[], cameraFrames: CameraFrame[], lightFrames: LightFrame[]}[]` 128 | 129 | ### 方法 130 | 131 | #### write 132 | 133 | 将数据重新生成二进制文件,可以将这个`ArrayBuffer`导出为文件 134 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/vscode,node 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=vscode,node 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # TypeScript v1 declaration files 50 | typings/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Microbundle cache 62 | .rpt2_cache/ 63 | .rts2_cache_cjs/ 64 | .rts2_cache_es/ 65 | .rts2_cache_umd/ 66 | 67 | # Optional REPL history 68 | .node_repl_history 69 | 70 | # Output of 'npm pack' 71 | *.tgz 72 | 73 | # Yarn Integrity file 74 | .yarn-integrity 75 | 76 | # dotenv environment variables file 77 | .env 78 | .env.test 79 | .env*.local 80 | 81 | # parcel-bundler cache (https://parceljs.org/) 82 | .cache 83 | .parcel-cache 84 | 85 | # Next.js build output 86 | .next 87 | 88 | # Nuxt.js build / generate output 89 | .nuxt 90 | dist 91 | 92 | # Gatsby files 93 | .cache/ 94 | # Comment in the public line in if your project uses Gatsby and not Next.js 95 | # https://nextjs.org/blog/next-9-1#public-directory-support 96 | # public 97 | 98 | # vuepress build output 99 | .vuepress/dist 100 | 101 | # Serverless directories 102 | .serverless/ 103 | 104 | # FuseBox cache 105 | .fusebox/ 106 | 107 | # DynamoDB Local files 108 | .dynamodb/ 109 | 110 | # TernJS port file 111 | .tern-port 112 | 113 | # Stores VSCode versions used for testing VSCode extensions 114 | .vscode-test 115 | 116 | ### vscode ### 117 | .vscode/* 118 | !.vscode/settings.json 119 | !.vscode/tasks.json 120 | !.vscode/launch.json 121 | !.vscode/extensions.json 122 | *.code-workspace 123 | 124 | # End of https://www.toptal.com/developers/gitignore/api/vscode,node -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vmd.js 2 | 3 | Load and modify your `Mikumikudance vmd` file with `js`! 4 | 5 | ## Features 6 | 7 | The `Vmd` file is a binary file. At present, it can only be edited by the `mikumikudance.exe` on `windows`. 8 | 9 | ### Only javascript 10 | 11 | read `ArrayBuffer` of the file, thanks 12 | 13 | * [Official document](http://mikumikudance.wikia.com/wiki/VMD_file_format) 14 | * [[MMD] Use python to parse VMD format to read](https://www.jianshu.com/p/ae312fb53fc3?from=groupmessage&isappinstalled=0) 15 | 16 | for the document of reading vmd file 17 | 18 | ### Timeline 19 | 20 | The object returned by vmd.js will add a `timeline` attribute, which can be used to view by `FrameTime` 21 | 22 | ## Examples 23 | 24 | ### Installation 25 | 26 | ```bash 27 | npm i vmd.js 28 | # or 29 | yarn add vmd.js 30 | ``` 31 | 32 | ### Import it 33 | 34 | ```javascript 35 | 36 | import Vmd from'vmd.js' 37 | 38 | // or 39 | 40 | const Vmd = require('vmd') 41 | ``` 42 | 43 | ### Create vmd from file 44 | 45 | ```javascript 46 | 47 | fetch('test.vmd') 48 | .then(res => res.blob()) 49 | .then(blob => blob.arrayBuffer()) 50 | .then(arrayBuffer => new Vmd(arrayBuffer)) 51 | 52 | ``` 53 | 54 | ### Export vmd as arrayBuffer 55 | 56 | ```javascript 57 | 58 | import {saveAs} from'file-saver' 59 | const arrayBuffer = vmd.write() 60 | const url = URL.createObjectURL(new Blob([arrayBuffer])) 61 | saveAs(url,'your.vmd') 62 | 63 | ``` 64 | 65 | ## Some Attribute provided 66 | 67 | ### VERSION 68 | 69 | `V1`, `V2` version string 70 | 71 | ### BONE_NAME 72 | 73 | The list of bone names in Japanese, like `['センター','upper body','头','头', ...]` 74 | 75 | ### BoneFrame 76 | 77 | Bone frame 78 | 79 | * boneName 80 | * frameTime 81 | * translation 82 | * rotation 83 | * curveX 84 | * curveY 85 | * curveZ 86 | * curveR 87 | 88 | ### MorphFrame 89 | 90 | Emoticon frame 91 | 92 | * morphName 93 | * frameTime 94 | * weight 95 | 96 | ### CameraFrame 97 | 98 | Camera frame 99 | 100 | * frameTime 101 | * distance 102 | * position 103 | * rotation 104 | * curve 105 | * viewAngle 106 | * orthographic 107 | 108 | ### LightFrame 109 | 110 | Light frame 111 | 112 | * frameTime 113 | * rgb 114 | * direction 115 | 116 | ## Vmd class 117 | 118 | Contains all the data of the `vmd` file 119 | 120 | ### Variable 121 | 122 | * version - version, there is V1 and V2 123 | * modelName - model name, have length limit 124 | * boneFrames - bone key frames 125 | * morphFrames - Emoji key frames 126 | * cameraFrames - camera key frames 127 | * lightFrames - light key frames 128 | 129 | * **timeline** - read-only variable, automatically sorted according to the key frame time`{frameTime: number, boneFrames: BoneFrame[], morphFrames: MorphFrame[], cameraFrames: CameraFrame[], lightFrames: LightFrame[]}[]` 130 | 131 | ### Method 132 | 133 | #### write 134 | 135 | Regenerate the data into a binary file, you can export this `ArrayBuffer` as a file 136 | -------------------------------------------------------------------------------- /src/Vmd.js: -------------------------------------------------------------------------------- 1 | import { MODEL_NAME_LENGTH, VERSION_BUFFER_LENGTH, VERSION } from './const' 2 | import ReadBufferStream from './stream/ReadBufferStream' 3 | import WriteBufferStream from './stream/WriteBufferStream' 4 | import BoneFrame from './frame/BoneFrame' 5 | import CameraFrame from './frame/CameraFrame' 6 | import LightFrame from './frame/LightFrame' 7 | import MorphFrame from './frame/MorphFrame' 8 | 9 | export default class Vmd { 10 | /** 11 | * @param { ArrayBuffer } [buffer] 12 | */ 13 | constructor (buffer) { 14 | /** 15 | * @type {string} 版本 16 | */ 17 | this.version = VERSION.V2 18 | 19 | /** 20 | * @type {string} 当前使用的模型名字 21 | */ 22 | this.modelName = '' 23 | 24 | /** 25 | * @type {BoneFrame[]} 骨骼关键帧 26 | */ 27 | this.boneFrames = [] // 骨骼关键帧 28 | this.morphFrames = [] // 表情关键帧 29 | this.cameraFrames = [] // 镜头关键帧 30 | this.lightFrames = [] // 光线关键帧 31 | 32 | /** 33 | * 从传入的文件流解析格式,生成配置信息,需要按照顺序读取buffer 34 | */ 35 | if (buffer) { 36 | const stream = new ReadBufferStream(buffer) 37 | 38 | this.version = stream.readString(VERSION_BUFFER_LENGTH) 39 | 40 | this.modelName = stream.readString(MODEL_NAME_LENGTH[this.version]) 41 | // 骨骼 42 | this.boneFrames = stream.readArrayByConstructor(BoneFrame) 43 | // 表情 44 | this.morphFrames = stream.readArrayByConstructor(MorphFrame) 45 | // 摄像机 46 | this.cameraFrames = stream.readArrayByConstructor(CameraFrame) 47 | // 光线 48 | this.lightFrames = stream.readArrayByConstructor(LightFrame) 49 | 50 | stream.close() 51 | } 52 | } 53 | 54 | /** 55 | * 时间线 56 | */ 57 | get timeline () { 58 | const maxFrameTime = this.boneFrames.reduce((_maxFrameTime, { frameTime }) => Math.max(_maxFrameTime, frameTime), 0) 59 | 60 | // frame类别的keys 61 | const frameTypeKeys = Object.keys(this).filter(key => key.includes('Frames')) 62 | 63 | const timeline = [] 64 | 65 | for (let frameTime = 0; frameTime < maxFrameTime; frameTime++) { 66 | /** 67 | * 生成对应的frame数据,根据frameTime过滤一次 68 | */ 69 | const frame = frameTypeKeys.reduce((_frame, key) => { 70 | /** 71 | * @type { BoneFrame[] | MorphFrame[] | CameraFrame[] | LightFrame[] } 72 | */ 73 | const typedFrames = this[key] 74 | _frame[key] = typedFrames.filter((typedFrame) => typedFrame.frameTime === frameTime) 75 | return _frame 76 | }, { frameTime }) 77 | 78 | timeline.push(frame) 79 | } 80 | 81 | return timeline 82 | } 83 | 84 | /** 85 | * 将内部状态导出 86 | * @returns {ArrayBuffer} 87 | */ 88 | write () { 89 | const stream = new WriteBufferStream() 90 | 91 | stream.writeString(this.version, VERSION_BUFFER_LENGTH) 92 | stream.writeString(this.modelName, MODEL_NAME_LENGTH[this.version]) 93 | 94 | // 骨骼 95 | stream.writeTypedFrameArray(this.boneFrames) 96 | // 表情 97 | stream.writeTypedFrameArray(this.morphFrames) 98 | // 摄像机 99 | stream.writeTypedFrameArray(this.cameraFrames) 100 | // 光线 101 | stream.writeArrayByType(this.lightFrames) 102 | 103 | const arrayBuffer = stream.getArrayBuffer() 104 | return arrayBuffer 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/stream/ReadBufferStream.js: -------------------------------------------------------------------------------- 1 | import { buffer2string } from '../util' 2 | import { TYPE } from '../const' 3 | export default class ReadBufferStream { 4 | /** 5 | * arrayBuffer 6 | * @param {ArrayBuffer} [buffer] 7 | */ 8 | constructor (buffer) { 9 | this.buffer = buffer || new ArrayBuffer(0) 10 | this.index = 0 11 | } 12 | 13 | /** 14 | * 获取数组 15 | * @param {*} Constructor 16 | * @returns {*[]} 17 | */ 18 | readArrayByConstructor (Constructor) { 19 | const totalNumber = this.readInt() 20 | const result = [] 21 | 22 | for (let i = 0; i < totalNumber; i++) { 23 | const data = new Constructor(this) 24 | result.push(data) 25 | } 26 | 27 | return result 28 | } 29 | 30 | /** 31 | * 读取指定字节的数据 32 | * @param {*} length 33 | */ 34 | readBytes (length = 0) { 35 | if (typeof length !== 'number') { 36 | throw new Error('readBytes failed, place check arg') 37 | } 38 | 39 | if (length <= 0) { 40 | return '' 41 | } 42 | 43 | if (this.index + length > this.buffer.byteLength) { 44 | throw new Error(`Stackoverflow ${this.index + length} / ${this.buffer.byteLength}`) 45 | } 46 | 47 | const buffer = this.buffer.slice(this.index, this.index + length) 48 | this.index = length + this.index 49 | return buffer 50 | } 51 | 52 | /** 53 | * 读取uint32_t 54 | * @returns number 55 | */ 56 | readInt () { 57 | return this.readByType(TYPE.uint32_t) 58 | } 59 | 60 | /** 61 | * 读取float 62 | * @returns number 63 | */ 64 | readFloat () { 65 | return this.readByType(TYPE.float) 66 | } 67 | 68 | /** 69 | * 读取文字 70 | * @param {number} [length] 71 | * @returns {string} 72 | */ 73 | readString (length = 0) { 74 | const buffer = this.readBytes(length) 75 | 76 | return buffer2string(buffer) 77 | } 78 | 79 | /** 80 | * 读取并格式化为具体类型 81 | * @param { Uint16ArrayConstructor | Uint32ArrayConstructor | Uint8ArrayConstructor | Float32ArrayConstructor | Float64ArrayConstructor } Type 82 | * @param {number} [offset] 83 | * @param {boolean} [littleEndian] 84 | * @returns { number } 85 | */ 86 | readByType (Type, offset = 0, littleEndian = true) { 87 | if (!Type) { 88 | throw new Error('Type is not define') 89 | } 90 | const buffer = this.readBytes(Type.BYTES_PER_ELEMENT) 91 | const view = new DataView(buffer, 0) 92 | const method = `get${Type.name.replace('Array', '')}` 93 | return view[method](offset, littleEndian) 94 | } 95 | 96 | /** 97 | * 返回一个Typed后的数组 98 | * @param {number} [length] 99 | * @param { Uint16ArrayConstructor | Uint32ArrayConstructor | Uint8ArrayConstructor | Float32ArrayConstructor | Float64ArrayConstructor } [Type] 100 | * @param {number} [offset] 101 | * @param {boolean} [littleEndian] 102 | * @returns {number[]} 103 | */ 104 | readArrayByType (length = 0, Type = TYPE.uint32_t, offset = 0, littleEndian = true) { 105 | const list = [] 106 | 107 | if (length < 0) { 108 | throw new Error('Invalid array length') 109 | } 110 | 111 | for (let i = 0; i < length; i++) { 112 | list.push(this.readByType(Type, offset, littleEndian)) 113 | } 114 | 115 | return list 116 | } 117 | 118 | /** 119 | * 获取剩余的bytes 120 | */ 121 | get restBytes () { 122 | return this.buffer.byteLength - this.index 123 | } 124 | 125 | /** 126 | * 清除保存的数据流 127 | */ 128 | close () { 129 | this.buffer = null 130 | this.index = 0 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/stream/WriteBufferStream.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import { string2buffer } from '../util' 3 | import { TYPE, VERSION } from '../const' 4 | export default class WriteBufferStream { 5 | constructor () { 6 | /** 7 | * @type { ArrayBuffer[] } 8 | */ 9 | this.bufferList = [] 10 | } 11 | 12 | /** 13 | * 写入数组 14 | * @param {*[]} array TypedFrames 15 | * @returns {WriteBufferStream} 16 | */ 17 | writeTypedFrameArray (array) { 18 | const totalNumber = array.length 19 | this.writeInt(totalNumber) 20 | array.forEach(typedFrame => { 21 | typedFrame.writeBuffer(this) 22 | }) 23 | 24 | return this 25 | } 26 | 27 | /** 28 | * 写入指定长度的ArrayBuffer 29 | * @param {ArrayBuffer} value 30 | * @param {number} [length] 长度,以传入的arrayBuffer和length最大为准 31 | * @returns {WriteBufferStream} 32 | */ 33 | writeBytes (value, length = 0) { 34 | const buffer = new Uint8Array(Math.max(value.byteLength, length)) 35 | buffer.set(value) 36 | this.bufferList.push(buffer.buffer) 37 | 38 | return this 39 | } 40 | 41 | /** 42 | * 写入uint32_t 43 | * @param {number} value 44 | * @returns { WriteBufferStream } 45 | */ 46 | writeInt (value) { 47 | return this.writeByType(value, TYPE.uint32_t) 48 | } 49 | 50 | /** 51 | * 写入float 52 | * @param {number} value 53 | * @returns { WriteBufferStream } 54 | */ 55 | writeFloat (value) { 56 | return this.writeByType(value, TYPE.float) 57 | } 58 | 59 | /** 60 | * 写入文字 文字默认为Uint8 61 | * @param {string} [text] 62 | * @param {number} [length] 63 | * @returns {WriteBufferStream} 64 | */ 65 | writeString (text = '', length = 0) { 66 | const textBuffer = string2buffer(text) 67 | const buffer = new Uint8Array(length) 68 | buffer.fill(253, textBuffer.length + 1) 69 | buffer.set(textBuffer) 70 | 71 | // 只有version是靠0填充的 72 | if (text === VERSION.V1 || text === VERSION.V2) { 73 | buffer.fill(0, textBuffer.length) 74 | } 75 | 76 | this.bufferList.push(buffer.buffer) 77 | return this 78 | } 79 | 80 | /** 81 | * 根据类型自动写入 82 | * @param { number } value 83 | * @param { Uint16ArrayConstructor | Uint32ArrayConstructor | Uint8ArrayConstructor | Float32ArrayConstructor | Float64ArrayConstructor } Type 84 | * @param {number} [offset] 85 | * @param {boolean} [littleEndian] 86 | * @returns { WriteBufferStream } 87 | */ 88 | writeByType (value, Type, offset = 0, littleEndian = true) { 89 | if (!Type) { 90 | throw new Error('Type is not define') 91 | } 92 | const view = new DataView(new ArrayBuffer(Type.BYTES_PER_ELEMENT), 0) 93 | const method = `set${Type.name.replace('Array', '')}` 94 | view[method](offset, value, littleEndian) 95 | this.bufferList.push(view.buffer) 96 | return this 97 | } 98 | 99 | /** 100 | * 将一个数组写入 101 | * @param {number[]} value 102 | * @param { Uint16ArrayConstructor | Uint32ArrayConstructor | Uint8ArrayConstructor | Float32ArrayConstructor | Float64ArrayConstructor } [Type] 103 | * @param {number} [offset] 104 | * @param {boolean} [littleEndian] 105 | * @returns {WriteBufferStream} 106 | */ 107 | writeArrayByType (value, Type = TYPE.uint32_t, offset = 0, littleEndian = true) { 108 | if (!Array.isArray(value)) { 109 | throw new Error('value is not array!') 110 | } 111 | value.forEach(number => { 112 | this.writeByType(number, Type, offset, littleEndian) 113 | }) 114 | return this 115 | } 116 | 117 | /** 118 | * 清除保存的数据流 119 | */ 120 | getArrayBuffer () { 121 | /** 122 | * 拼接 123 | */ 124 | const totalBytes = this.bufferList.reduce((_totalBytes, buffer) => { 125 | return _totalBytes + new Uint8Array(buffer).length 126 | }, 0) 127 | 128 | const result = new Uint8Array(totalBytes) 129 | let offset = 0 130 | 131 | for (const buffer of this.bufferList) { 132 | result.set(new Uint8Array(buffer), offset) 133 | offset += buffer.byteLength 134 | } 135 | 136 | const buffer = result.buffer 137 | 138 | this.bufferList.splice(0) 139 | 140 | return buffer 141 | } 142 | } 143 | --------------------------------------------------------------------------------