├── circle.png
├── point.png
├── sprites.png
├── README.md
├── index.html
├── frame.js
└── frame.ts
/circle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mushayin/threejs-frame/HEAD/circle.png
--------------------------------------------------------------------------------
/point.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mushayin/threejs-frame/HEAD/point.png
--------------------------------------------------------------------------------
/sprites.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mushayin/threejs-frame/HEAD/sprites.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # threejs-frame
2 | threejs 播放序列帧图片
3 |
4 | ## 使用
5 |
6 | frame.ts 提供详细参数说明
7 |
8 | 查看示例:https://mushayin.github.io/threejs-frame
9 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
14 |
15 |
16 |
17 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------