├── README.md ├── circle.png ├── frame.js ├── frame.ts ├── index.html ├── point.png ├── sprites.png └── three.module.js /README.md: -------------------------------------------------------------------------------- 1 | # threejs-frame 2 | threejs 播放序列帧图片 3 | 4 | ## 使用 5 | 6 | frame.ts 提供详细参数说明 7 | 8 | 查看示例:https://mushayin.github.io/threejs-frame 9 | -------------------------------------------------------------------------------- /circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mushayin/threejs-frame/181b477cd77dec659f39c0477d44c346d446ca50/circle.png -------------------------------------------------------------------------------- /frame.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "./three.module.js"; 2 | export class Frame { 3 | constructor(img, frame, cb) { 4 | this.index = 0; 5 | this.paused = true; 6 | this.updateTime = 0; 7 | this.loop = true; 8 | this.nextFrame = () => { 9 | const x = this.index % this.column; 10 | const y = (this.index - x) / this.column; 11 | this.mesh.material.map.offset.set(this.offsetX * x, -this.offsetY * y); 12 | }; 13 | this.update = () => { 14 | if (this.paused) 15 | return; 16 | if (!this.loop && this.index >= this.count) 17 | return; 18 | if (++this.updateTime >= this.duration * (this.index + 1) / this.count) { 19 | if (++this.index >= this.count) { 20 | if (this.loop) { 21 | this.updateTime = 0; 22 | this.index = 0; 23 | } 24 | else { 25 | this.cb && this.cb(); 26 | } 27 | } 28 | this.nextFrame(); 29 | } 30 | }; 31 | this.play = (reset = false) => { 32 | this.paused = false; 33 | if (reset) { 34 | this.updateTime = 0; 35 | this.index = 0; 36 | } 37 | }; 38 | this.pause = () => { 39 | this.paused = true; 40 | }; 41 | this.faceTo = (position, normal) => { 42 | const pos = new THREE.Vector3().addVectors(position, normal); 43 | this.mesh.position.copy(normal).multiplyScalar(0.1); 44 | this.mesh.lookAt(pos); 45 | }; 46 | this.cb = cb; 47 | this.loop = frame.loop; 48 | this.count = img.count; 49 | this.column = img.width / img.fWidth; 50 | this.duration = frame.duration * 60; 51 | 52 | this.offsetX = img.fWidth / img.width; 53 | this.offsetY = img.fHeight / img.height; 54 | 55 | const geometry = new THREE.PlaneBufferGeometry(frame.width, frame.height, 1, 1); 56 | geometry.setAttribute('uv', new THREE.Float32BufferAttribute([0, 1, this.offsetX, 1, 0, 1 - this.offsetY, this.offsetX, 1 - this.offsetY], 2)); 57 | 58 | const material = new THREE.SpriteMaterial({ 59 | color: 0xffffff, 60 | map: new THREE.TextureLoader().load(img.src) 61 | }); 62 | 63 | this.mesh = new THREE.Sprite(material); 64 | this.mesh.geometry = geometry; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /frame.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from "./three.module"; 2 | 3 | type ImgInfo = { 4 | /** 5 | * 路径 6 | */ 7 | src: string; 8 | /** 9 | * 图片宽度 10 | */ 11 | width: number; 12 | /** 13 | * 图片高度 14 | */ 15 | height: number; 16 | 17 | /** 18 | * 帧图像宽度 19 | */ 20 | fWidth: number; 21 | /** 22 | * 帧图像高度 23 | */ 24 | fHeight: number; 25 | 26 | /** 27 | * 总共多少帧 28 | */ 29 | count: number 30 | } 31 | 32 | type FrameInfo = { 33 | /** 34 | * 面片宽度 35 | */ 36 | width: number; 37 | /** 38 | * 面片高度 39 | */ 40 | height: number; 41 | /** 42 | * 持续时间 43 | */ 44 | duration: number; 45 | /** 46 | * 循环播放,默认是 true 47 | */ 48 | loop?: boolean; 49 | } 50 | 51 | export class Frame { 52 | /** 53 | * 帧动画的mesh 54 | */ 55 | public readonly mesh: THREE.Mesh; 56 | 57 | private index: number = 0; 58 | private paused: boolean = true; 59 | private updateTime: number = 0; 60 | 61 | private readonly cb: Function; 62 | private readonly loop: boolean = true; 63 | private readonly count: number; 64 | private readonly column: number; 65 | private readonly duration: number; 66 | 67 | private readonly offsetX: number; 68 | private readonly offsetY: number; 69 | 70 | /** 71 | * 创建帧动画 72 | * @param img 帧图像信息 73 | * @param frame 帧动画信息 74 | * @param cb 回调 75 | */ 76 | constructor(img: ImgInfo, frame: FrameInfo, cb?: () => void) { 77 | this.cb = cb; 78 | this.loop = frame.loop; 79 | this.count = img.count; 80 | this.column = img.width/img.fWidth; 81 | this.duration = frame.duration * 60; 82 | 83 | this.offsetX = img.width / img.fWidth; 84 | this.offsetY = img.height / img.fHeight; 85 | 86 | const geometry = new THREE.PlaneBufferGeometry(frame.width, frame.height, 1, 1); 87 | geometry.setAttribute('uv', new THREE.Float32BufferAttribute([0, 1, this.offsetX, 1, 0, 1 - this.offsetY, this.offsetX, 1 - this.offsetY], 2)); 88 | 89 | const material = new THREE.MeshLambertMaterial({ 90 | transparent: true, 91 | emissive: 0xffffff, 92 | map: new THREE.TextureLoader().load(img.src) 93 | }); 94 | 95 | this.mesh = new THREE.Mesh(geometry, material); 96 | } 97 | 98 | private nextFrame = () => { 99 | const x = this.index % this.column; 100 | const y = (this.index - x) / this.column; 101 | (this.mesh.material as THREE.MeshLambertMaterial).map.offset.set(this.offsetX * x, -this.offsetY * y); 102 | } 103 | 104 | /** 105 | * 每帧更新 106 | */ 107 | public update = () => { 108 | if (this.paused) return; 109 | 110 | if (!this.loop && this.index >= this.count) return; 111 | 112 | if (++this.updateTime >= this.duration * (this.index + 1) / this.count) { 113 | if (++this.index >= this.count) { 114 | if (this.loop) { 115 | this.updateTime = 0; 116 | this.index = 0; 117 | } else { 118 | this.cb && this.cb(); 119 | } 120 | } 121 | this.nextFrame(); 122 | } 123 | 124 | } 125 | 126 | /** 127 | * 开始播放 128 | * @param reset 从头播放 129 | */ 130 | public play = (reset = false) => { 131 | this.paused = false; 132 | if (reset) { 133 | this.updateTime = 0; 134 | this.index = 0; 135 | } 136 | } 137 | 138 | /** 139 | * 停止播放 140 | */ 141 | public pause = () => { 142 | this.paused = true; 143 | } 144 | 145 | /** 146 | * 面片朝向 147 | * @param position 面片位置 148 | * @param normal 朝向位置 149 | */ 150 | public faceTo = (position: THREE.Vector3, normal: THREE.Vector3) => { 151 | const pos = new THREE.Vector3().addVectors(position, normal); 152 | // 防止闪烁 153 | this.mesh.position.copy(normal).multiplyScalar(0.1); 154 | this.mesh.lookAt(pos); 155 | } 156 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 14 | 15 | 16 | 17 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /point.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mushayin/threejs-frame/181b477cd77dec659f39c0477d44c346d446ca50/point.png -------------------------------------------------------------------------------- /sprites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mushayin/threejs-frame/181b477cd77dec659f39c0477d44c346d446ca50/sprites.png --------------------------------------------------------------------------------