@leafer-in/film 0.0.0-stage → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1 -3
- package/dist/film.cjs +955 -0
- package/dist/film.esm.js +949 -0
- package/dist/film.esm.min.js +2 -0
- package/dist/film.esm.min.js.map +1 -0
- package/dist/film.js +926 -0
- package/dist/film.min.cjs +2 -0
- package/dist/film.min.cjs.map +1 -0
- package/dist/film.min.js +2 -0
- package/dist/film.min.js.map +1 -0
- package/package.json +34 -3
- package/src/Film.ts +27 -0
- package/src/FilmDecoder.ts +201 -0
- package/src/OmgFilmDecoder.ts +143 -0
- package/src/PaintFilm.ts +56 -0
- package/src/data/FilmData.ts +9 -0
- package/src/helper/ext.ts +23 -0
- package/src/index.ts +68 -0
- package/types/index.d.ts +69 -0
package/package.json
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leafer-in/film",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "2.3.1",
|
|
4
|
+
"description": "@leafer-in/film",
|
|
5
|
+
"author": "Chao (Leafer) Wan",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "dist/film.esm.min.js",
|
|
9
|
+
"exports": {
|
|
10
|
+
"import": "./dist/film.esm.min.js",
|
|
11
|
+
"require": "./dist/film.min.cjs",
|
|
12
|
+
"types": "./types/index.d.ts"
|
|
13
|
+
},
|
|
14
|
+
"types": "types/index.d.ts",
|
|
15
|
+
"unpkg": "dist/film.min.js",
|
|
16
|
+
"jsdelivr": "dist/film.min.js",
|
|
17
|
+
"files": ["src","types","dist"],
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/leaferjs/leafer-in.git"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/leaferjs/leafer-in/tree/main/packages/film",
|
|
23
|
+
"bugs": "https://github.com/leaferjs/leafer-in/issues",
|
|
24
|
+
"keywords": [
|
|
25
|
+
"leafer film",
|
|
26
|
+
"leafer-film",
|
|
27
|
+
"leafer-in",
|
|
28
|
+
"film",
|
|
29
|
+
"leafer-ui",
|
|
30
|
+
"leaferjs"
|
|
31
|
+
],
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@leafer-ui/draw": "^2.2.4",
|
|
34
|
+
"@leafer-ui/interface": "^2.2.4",
|
|
35
|
+
"@leafer-in/interface": "^2.2.4"
|
|
36
|
+
}
|
|
6
37
|
}
|
package/src/Film.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { IFilm, IFilmInputData, IFilmData, } from '@leafer-ui/interface'
|
|
2
|
+
import { Image, registerUI, dataProcessor, useModule } from '@leafer-ui/draw'
|
|
3
|
+
|
|
4
|
+
import { FilmData } from './data/FilmData'
|
|
5
|
+
import { PaintFilm } from './PaintFilm'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@useModule(PaintFilm)
|
|
9
|
+
@registerUI()
|
|
10
|
+
export class Film<TInputData = IFilmInputData> extends Image<TInputData> implements IFilm {
|
|
11
|
+
|
|
12
|
+
public get __tag() { return 'Film' }
|
|
13
|
+
|
|
14
|
+
@dataProcessor(FilmData)
|
|
15
|
+
declare public __: IFilmData
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface Film {
|
|
19
|
+
|
|
20
|
+
togglePlay(): void
|
|
21
|
+
play(): void
|
|
22
|
+
pause(): void
|
|
23
|
+
stop(): void
|
|
24
|
+
|
|
25
|
+
seekFrame(frameIndex: number): void
|
|
26
|
+
|
|
27
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { isUndefined, Platform } from '@leafer-ui/draw'
|
|
2
|
+
import { ICanvasContext2D, IFilmDecoder, IFilmFileType, IFilmFrame, ILeaf, IPointData, IBooleanMap, IFilmPlayOptions } from '@leafer-ui/interface'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
interface IFilmDecoderMap {
|
|
6
|
+
[name: string]: { Decoder: any, force?: boolean }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class FilmDecoder implements IFilmDecoder {
|
|
10
|
+
|
|
11
|
+
public width: number // 宽度
|
|
12
|
+
public height: number // 高度
|
|
13
|
+
|
|
14
|
+
public total: number // 总的帧数
|
|
15
|
+
public loop: number // 循环次数,为0表示无限循环
|
|
16
|
+
|
|
17
|
+
public frames: IFilmFrame[] = [] // 帧数据
|
|
18
|
+
public decodedIndex = -1 // 当前解码到第几帧
|
|
19
|
+
|
|
20
|
+
public atlas?: any // 合并图
|
|
21
|
+
public atlasContext?: ICanvasContext2D // 合并图的上下文手柄
|
|
22
|
+
public atlasGrid?: IPointData // 横竖的排列数量
|
|
23
|
+
|
|
24
|
+
public decoder: ImageDecoder // 解码器
|
|
25
|
+
|
|
26
|
+
// 缓冲画布
|
|
27
|
+
public bufferCanvas?: any
|
|
28
|
+
public bufferContext?: ICanvasContext2D
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
static decoderMap: IFilmDecoderMap = {}
|
|
32
|
+
static supportMap: IBooleanMap = {}
|
|
33
|
+
static maxAtlasSize = Platform.image.maxPatternSize
|
|
34
|
+
|
|
35
|
+
static register(type: IFilmFileType, Decoder: any, force?: boolean): void {
|
|
36
|
+
FilmDecoder.decoderMap[type] = { Decoder, force }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static async canUse(mimeType: string) {
|
|
40
|
+
const { supportMap } = this
|
|
41
|
+
let canUse = supportMap[mimeType]
|
|
42
|
+
if (!isUndefined(canUse)) return canUse
|
|
43
|
+
if (typeof ImageDecoder === 'undefined') return false
|
|
44
|
+
canUse = await ImageDecoder.isTypeSupported(mimeType)
|
|
45
|
+
return supportMap[mimeType] = canUse
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
public createAtlas(): void {
|
|
50
|
+
const { width, height, total } = this
|
|
51
|
+
if (width * height * total <= FilmDecoder.maxAtlasSize) {
|
|
52
|
+
// 合并所有帧到一张大图里(画布能放下所有帧时)
|
|
53
|
+
const columns = Math.ceil(Math.sqrt(total * height / width))
|
|
54
|
+
const rows = Math.ceil(total / columns)
|
|
55
|
+
this.atlasGrid = { x: columns, y: rows }
|
|
56
|
+
this.atlas = Platform.origin.createCanvas(width * columns, height * rows)
|
|
57
|
+
this.atlasContext = this.atlas.getContext('2d')
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public async decodeHeader(data: ArrayBuffer, type: string): Promise<void> {
|
|
62
|
+
this.decoder = new ImageDecoder({ data, type })
|
|
63
|
+
|
|
64
|
+
await this.decoder.tracks.ready
|
|
65
|
+
const first = await this.decodeFrame(0)
|
|
66
|
+
|
|
67
|
+
const { displayWidth, displayHeight } = first.image, { selectedTrack } = this.decoder.tracks
|
|
68
|
+
this.width = displayWidth
|
|
69
|
+
this.height = displayHeight
|
|
70
|
+
this.total = selectedTrack.frameCount
|
|
71
|
+
this.loop = selectedTrack.repetitionCount
|
|
72
|
+
|
|
73
|
+
this.createAtlas()
|
|
74
|
+
if (this.atlas) this.mergeFrame(0) // 因为已经读取了第一帧,存在合并图时,必须再次合并进去
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public async decodeFrame(frameIndex: number): Promise<IFilmFrame> {
|
|
78
|
+
const { frames } = this, frame = frames[frameIndex]
|
|
79
|
+
if (frame && !frame.decoding) return frame
|
|
80
|
+
|
|
81
|
+
// 必须顺序解码
|
|
82
|
+
for (let i = this.decodedIndex + 1; i <= frameIndex; i++) {
|
|
83
|
+
await this.decodeOneFrame(i)
|
|
84
|
+
this.decodedIndex = i
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return frames[frameIndex]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public async decodeOneFrame(frameIndex: number): Promise<IFilmFrame> {
|
|
91
|
+
return new Promise((resolve) => {
|
|
92
|
+
this.decoder.decode({ frameIndex }).then(result => {
|
|
93
|
+
const { image } = result
|
|
94
|
+
const frame: IFilmFrame = { image, duration: image.duration / 1000 / 1000 } // duration单位为秒
|
|
95
|
+
this.frames[frameIndex] = frame
|
|
96
|
+
if (this.atlas) this.mergeFrame(frameIndex)
|
|
97
|
+
resolve(frame)
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 绘制当前帧到atlas合并图中
|
|
103
|
+
public mergeFrame(frameIndex: number, destoryFrameImage: boolean = true): void {
|
|
104
|
+
const frame = this.frames[frameIndex], { atlasGrid, width, height } = this
|
|
105
|
+
frame.x = (frameIndex % atlasGrid.x) * width
|
|
106
|
+
frame.y = Math.floor(frameIndex / atlasGrid.x) * height
|
|
107
|
+
this.atlasContext.drawImage(frame.image, frame.x, frame.y)
|
|
108
|
+
if (destoryFrameImage) this.destoryFrameImage(frame)
|
|
109
|
+
else delete frame.image
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
public render(canvas: ICanvasContext2D, x: number, y: number, width: number, height: number, leaf: ILeaf, playOptions: IFilmPlayOptions, _imageScaleX: number, _imageScaleY: number): void {
|
|
113
|
+
let { nowIndex, paused, speed, pauseIndex, loop, frameTimer } = playOptions
|
|
114
|
+
|
|
115
|
+
if (isUndefined(nowIndex)) nowIndex = 0
|
|
116
|
+
|
|
117
|
+
const { frames } = this, frame = frames[nowIndex]
|
|
118
|
+
|
|
119
|
+
if (this.renderFrame(frame, canvas, x, y, width, height)) {
|
|
120
|
+
|
|
121
|
+
if (frameTimer) return // 避免重复等待下一帧
|
|
122
|
+
if (!isUndefined(pauseIndex) && nowIndex === pauseIndex) { // 播放到暂停帧为止
|
|
123
|
+
playOptions.paused = true
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let nextIndex = nowIndex + 1
|
|
128
|
+
|
|
129
|
+
if (nextIndex >= this.total) {
|
|
130
|
+
if (this.loop && isFinite(this.loop)) { // 限制循环次数
|
|
131
|
+
loop = (loop as number || 0) + 1
|
|
132
|
+
if (loop > this.loop) return
|
|
133
|
+
playOptions.loop = loop
|
|
134
|
+
}
|
|
135
|
+
nextIndex = 0
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (nextIndex < this.total && !frames[nextIndex]) {
|
|
139
|
+
frames[nextIndex] = { duration: 0, decoding: true } // 先占位,表示正在解码中
|
|
140
|
+
this.decodeFrame(nextIndex).finally(() => {
|
|
141
|
+
if (this.decodedIndex === this.total - 1) this.destroyDecoder()
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (paused) return
|
|
146
|
+
|
|
147
|
+
playOptions.frameTimer = setTimeout(() => {
|
|
148
|
+
|
|
149
|
+
playOptions.nowIndex = nextIndex
|
|
150
|
+
delete playOptions.frameTimer
|
|
151
|
+
leaf.forceRender()
|
|
152
|
+
|
|
153
|
+
}, frame.duration * (speed || 1) * 1000)
|
|
154
|
+
|
|
155
|
+
} else {
|
|
156
|
+
|
|
157
|
+
this.renderFrame(frames[nowIndex - 1], canvas, x, y, width, height) // 暂时渲染上一帧顶替
|
|
158
|
+
leaf.forceRender() // 防止停掉
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private renderFrame(frame: IFilmFrame, canvas: ICanvasContext2D, x: number, y: number, width: number, height: number): boolean {
|
|
163
|
+
const canRender = frame && !frame.decoding, { atlas } = this
|
|
164
|
+
if (canRender) {
|
|
165
|
+
if (atlas) canvas.drawImage(atlas, frame.x, frame.y, this.width, this.height, x, y, width, height)
|
|
166
|
+
else canvas.drawImage(frame.image, x, y, width, height)
|
|
167
|
+
}
|
|
168
|
+
return canRender
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 销毁单帧
|
|
172
|
+
public destoryFrame(frameIndex: number, deleteIndex: boolean = true): void {
|
|
173
|
+
const { frames } = this, frame = frames[frameIndex]
|
|
174
|
+
if (frame) {
|
|
175
|
+
this.destoryFrameImage(frame)
|
|
176
|
+
if (deleteIndex) delete frames[frameIndex]
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 销毁单帧内的VideoFrame或canvas对象
|
|
181
|
+
public destoryFrameImage(frame: IFilmFrame): void {
|
|
182
|
+
const { image } = frame
|
|
183
|
+
if (image && image.close) image.close()
|
|
184
|
+
delete frame.image
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
public destroyDecoder(): void {
|
|
188
|
+
if (this.decoder) {
|
|
189
|
+
this.decoder.close()
|
|
190
|
+
this.decoder = null
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
public close(): void {
|
|
195
|
+
if (this.atlas) this.atlas = this.atlasContext = null
|
|
196
|
+
this.frames.forEach((_item, index) => this.destoryFrame(index, false))
|
|
197
|
+
this.frames.length = 0
|
|
198
|
+
this.destroyDecoder()
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { IFilmFrame } from '@leafer-ui/interface'
|
|
2
|
+
import { Platform } from '@leafer-ui/draw'
|
|
3
|
+
|
|
4
|
+
import { GifReader } from 'omggif'
|
|
5
|
+
|
|
6
|
+
import { FilmDecoder } from './FilmDecoder'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
export class OmgFilmDecoder extends FilmDecoder {
|
|
10
|
+
|
|
11
|
+
public decoder: GifReader | any
|
|
12
|
+
|
|
13
|
+
// 复用 RGBA buffer
|
|
14
|
+
private decodeBuffer!: Uint8ClampedArray
|
|
15
|
+
|
|
16
|
+
// 复用 ImageData(避免每帧 new)
|
|
17
|
+
private sharedImageData!: ImageData
|
|
18
|
+
|
|
19
|
+
// 只保留一个 restore 缓存(disposal=3)
|
|
20
|
+
private restoreImageData: ImageData | null = null
|
|
21
|
+
|
|
22
|
+
// --------------------------------------------------
|
|
23
|
+
// 1️⃣ decodeHeader
|
|
24
|
+
// --------------------------------------------------
|
|
25
|
+
|
|
26
|
+
public async decodeHeader(data: ArrayBuffer): Promise<void> {
|
|
27
|
+
const decoder = this.decoder = new GifReader(new Uint8Array(data))
|
|
28
|
+
const { width, height } = decoder
|
|
29
|
+
|
|
30
|
+
this.width = width
|
|
31
|
+
this.height = height
|
|
32
|
+
this.total = decoder.numFrames()
|
|
33
|
+
this.loop = decoder.loopCount() || 0
|
|
34
|
+
|
|
35
|
+
// 工作 canvas(永远只有一个)
|
|
36
|
+
this.bufferCanvas = Platform.origin.createCanvas(width, height)
|
|
37
|
+
this.bufferContext = this.bufferCanvas.getContext('2d')!
|
|
38
|
+
|
|
39
|
+
// 复用 RGBA buffer
|
|
40
|
+
this.decodeBuffer = new Uint8ClampedArray(width * height * 4)
|
|
41
|
+
|
|
42
|
+
// 复用 ImageData
|
|
43
|
+
this.sharedImageData = this.bufferContext.createImageData(width, height)
|
|
44
|
+
|
|
45
|
+
this.createAtlas()
|
|
46
|
+
|
|
47
|
+
// 首帧必须解
|
|
48
|
+
await this.decodeFrame(0)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// --------------------------------------------------
|
|
52
|
+
// 2️⃣ decodeFrame(顺序安全)
|
|
53
|
+
// --------------------------------------------------
|
|
54
|
+
|
|
55
|
+
public async decodeOneFrame(frameIndex: number): Promise<IFilmFrame> {
|
|
56
|
+
const { decoder, decodeBuffer, bufferCanvas, bufferContext, sharedImageData } = this
|
|
57
|
+
const info = decoder.frameInfo(frameIndex)
|
|
58
|
+
|
|
59
|
+
// disposal=3 需要保存当前完整状态
|
|
60
|
+
if (info.disposal === 3) this.restoreImageData = bufferContext.getImageData(0, 0, this.width, this.height)
|
|
61
|
+
|
|
62
|
+
// 解码到 decodeBuffer
|
|
63
|
+
decoder.decodeAndBlitFrameRGBA(frameIndex, decodeBuffer)
|
|
64
|
+
|
|
65
|
+
// 复用 ImageData
|
|
66
|
+
sharedImageData.data.set(decodeBuffer)
|
|
67
|
+
|
|
68
|
+
// 绘制到 buffer
|
|
69
|
+
bufferContext.putImageData(sharedImageData, 0, 0)
|
|
70
|
+
|
|
71
|
+
let image: any
|
|
72
|
+
|
|
73
|
+
if (this.atlas) {
|
|
74
|
+
image = bufferCanvas
|
|
75
|
+
} else {
|
|
76
|
+
image = Platform.origin.createCanvas(this.width, this.height)
|
|
77
|
+
const frameCtx = image.getContext('2d')!
|
|
78
|
+
frameCtx.drawImage(bufferCanvas, 0, 0)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const frame: IFilmFrame = { image, duration: (info.delay || 10) / 100 }
|
|
82
|
+
|
|
83
|
+
this.frames[frameIndex] = frame
|
|
84
|
+
|
|
85
|
+
if (this.atlas) this.mergeFrame(frameIndex, false) // false 表示不物理销毁frame.image
|
|
86
|
+
|
|
87
|
+
this.applyDisposal(info)
|
|
88
|
+
|
|
89
|
+
return frame
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// --------------------------------------------------
|
|
93
|
+
// 3️⃣ disposal 处理
|
|
94
|
+
// --------------------------------------------------
|
|
95
|
+
|
|
96
|
+
private applyDisposal(info: any): void {
|
|
97
|
+
const { bufferContext, restoreImageData } = this
|
|
98
|
+
const disposal = info.disposal
|
|
99
|
+
if (disposal === 2) {
|
|
100
|
+
bufferContext.clearRect(info.x, info.y, info.width, info.height) // 恢复透明(当前帧区域)
|
|
101
|
+
} else if (disposal === 3) {
|
|
102
|
+
if (restoreImageData) {
|
|
103
|
+
bufferContext.putImageData(restoreImageData, 0, 0)
|
|
104
|
+
this.restoreImageData = null
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// --------------------------------------------------
|
|
110
|
+
// 5️⃣ destoryFrameImage
|
|
111
|
+
// --------------------------------------------------
|
|
112
|
+
|
|
113
|
+
public destoryFrameImage(frame: IFilmFrame): void {
|
|
114
|
+
if (!frame.image) return
|
|
115
|
+
|
|
116
|
+
const canvas = frame.image as HTMLCanvasElement
|
|
117
|
+
|
|
118
|
+
// 强制释放 backing store
|
|
119
|
+
canvas.width = 0
|
|
120
|
+
canvas.height = 0
|
|
121
|
+
|
|
122
|
+
delete frame.image
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// --------------------------------------------------
|
|
126
|
+
// 释放解码资源
|
|
127
|
+
// --------------------------------------------------
|
|
128
|
+
|
|
129
|
+
public destroyDecoder(): void {
|
|
130
|
+
if (this.bufferCanvas) {
|
|
131
|
+
this.bufferCanvas.width = 0
|
|
132
|
+
this.bufferCanvas.height = 0
|
|
133
|
+
|
|
134
|
+
this.decoder = null
|
|
135
|
+
this.bufferCanvas = null
|
|
136
|
+
this.bufferContext = null
|
|
137
|
+
|
|
138
|
+
this.decodeBuffer = null
|
|
139
|
+
this.sharedImageData = null
|
|
140
|
+
this.restoreImageData = null
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
package/src/PaintFilm.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ILeafPaint, IFilmDecoder, IUI } from '@leafer-ui/interface'
|
|
2
|
+
import { isArray } from '@leafer-ui/draw'
|
|
3
|
+
|
|
4
|
+
export class PaintFilm {
|
|
5
|
+
|
|
6
|
+
public element: IUI
|
|
7
|
+
|
|
8
|
+
public get playerOptions(): ILeafPaint { const { fill } = (this.element || this as any).__; return isArray(fill) && fill.find(item => item.type === 'film') }
|
|
9
|
+
|
|
10
|
+
public get decoder(): IFilmDecoder { const { playerOptions } = this; return playerOptions && playerOptions.image && playerOptions.image.view }
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
constructor(element: IUI) {
|
|
14
|
+
this.element = element
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
public togglePlay(): void {
|
|
19
|
+
const { playerOptions } = this
|
|
20
|
+
if (playerOptions) {
|
|
21
|
+
if (playerOptions.paused) this.play()
|
|
22
|
+
else this.pause()
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public play(): void {
|
|
27
|
+
const { playerOptions } = this
|
|
28
|
+
if (playerOptions && playerOptions.paused) {
|
|
29
|
+
delete playerOptions.paused;
|
|
30
|
+
(this.element || this as any).forceRender()
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public pause(): void {
|
|
35
|
+
const { playerOptions } = this
|
|
36
|
+
if (playerOptions) playerOptions.paused = true
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public stop(): void {
|
|
40
|
+
const { playerOptions } = this
|
|
41
|
+
if (playerOptions) {
|
|
42
|
+
delete playerOptions.frameTime
|
|
43
|
+
playerOptions.nowIndex = 0
|
|
44
|
+
this.pause()
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public seekFrame(frameIndex: number): void {
|
|
49
|
+
const { playerOptions } = this
|
|
50
|
+
if (playerOptions) {
|
|
51
|
+
playerOptions.nowIndex = frameIndex;
|
|
52
|
+
(this.element || this as any).forceRender()
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function getFileExt(url: string): string {
|
|
2
|
+
if (!url) return ''
|
|
3
|
+
|
|
4
|
+
if (url.startsWith('data:')) {
|
|
5
|
+
const head = url.slice(5, url.indexOf(','))
|
|
6
|
+
const slash = head.indexOf('/')
|
|
7
|
+
const semi = head.indexOf(';')
|
|
8
|
+
return slash > -1
|
|
9
|
+
? head.slice(slash + 1, semi > -1 ? semi : undefined).toLowerCase()
|
|
10
|
+
: ''
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let end = url.length
|
|
14
|
+
const q = url.indexOf('?')
|
|
15
|
+
const h = url.indexOf('#')
|
|
16
|
+
if (q > -1 && q < end) end = q
|
|
17
|
+
if (h > -1 && h < end) end = h
|
|
18
|
+
|
|
19
|
+
const dot = url.lastIndexOf('.', end)
|
|
20
|
+
const slash = url.lastIndexOf('/', end)
|
|
21
|
+
|
|
22
|
+
return dot > slash ? url.slice(dot + 1, end).toLowerCase() : ''
|
|
23
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { LeaferFilm, Platform, Creator, FileHelper, PaintImage, Resource, Plugin } from '@leafer-ui/draw'
|
|
2
|
+
import { ILeaferFilm, ILeaferFilmConfig, IImageCrossOrigin, ILeaf, IFilmDecoder, ILeafPaint, IFilmFileType } from '@leafer-ui/interface'
|
|
3
|
+
|
|
4
|
+
import { FilmDecoder } from './FilmDecoder'
|
|
5
|
+
import { OmgFilmDecoder } from './OmgFilmDecoder'
|
|
6
|
+
import { getFileExt } from './helper/ext'
|
|
7
|
+
|
|
8
|
+
export { Film } from './Film'
|
|
9
|
+
export { FilmData } from './data/FilmData'
|
|
10
|
+
export { FilmDecoder } from './FilmDecoder'
|
|
11
|
+
export { PaintFilm } from './PaintFilm'
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
Plugin.add('flim')
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
PaintImage.film = function (paint: ILeafPaint): void {
|
|
18
|
+
paint.film = true
|
|
19
|
+
paint.nowIndex = 0
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
Creator.film = function (options?: ILeaferFilmConfig): ILeaferFilm {
|
|
23
|
+
return new LeaferFilm(options)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
Resource.loadFilm = function (key: string, format?: IFilmFileType): Promise<ILeaferFilm> {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const config: ILeaferFilmConfig = { url: key }
|
|
29
|
+
if (format) config.format = format
|
|
30
|
+
const film = Creator.film(config)
|
|
31
|
+
Resource.set(key, film)
|
|
32
|
+
film.load(() => resolve(film), (e: any) => reject(e))
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
LeaferFilm.prototype.render = function (canvas: any, x: number, y: number, width: number, height: number, leaf: ILeaf, paint: any, imageScaleX: number, imageScaleY: number): void {
|
|
37
|
+
this.view.render(canvas, x, y, width, height, leaf, paint, imageScaleX, imageScaleY)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
FilmDecoder.register('gif', OmgFilmDecoder) // OmgFilmDecoder
|
|
41
|
+
|
|
42
|
+
Platform.origin.loadFilm = async function (url: string, _crossOrigin?: IImageCrossOrigin, leaferFilm?: ILeaferFilm): Promise<any> {
|
|
43
|
+
const buffer = await Platform.origin.loadContent(url, 'arrayBuffer')
|
|
44
|
+
|
|
45
|
+
let ext = getFileExt(url) // 获取文件后缀名
|
|
46
|
+
|
|
47
|
+
if (!ext) {
|
|
48
|
+
if (leaferFilm) ext = leaferFilm.config.format
|
|
49
|
+
if (!ext) ext = 'gif'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const mimeType = FileHelper.mimeType(ext)
|
|
53
|
+
const canUseFilmDecoder = await FilmDecoder.canUse(mimeType)
|
|
54
|
+
|
|
55
|
+
let Decoder: any
|
|
56
|
+
const tool = FilmDecoder.decoderMap[ext]
|
|
57
|
+
|
|
58
|
+
if (!tool || (canUseFilmDecoder && !tool.force)) Decoder = FilmDecoder
|
|
59
|
+
else Decoder = tool.Decoder
|
|
60
|
+
|
|
61
|
+
if (!Decoder) throw new Error('no decoder: ' + mimeType)
|
|
62
|
+
|
|
63
|
+
const decoder: IFilmDecoder = new Decoder()
|
|
64
|
+
await decoder.decodeHeader(buffer, mimeType)
|
|
65
|
+
|
|
66
|
+
return decoder
|
|
67
|
+
}
|
|
68
|
+
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { IFilmInputData, IFilm, IFilmData, IMultimediaType, IFilmDecoder, IFilmFrame, ICanvasContext2D, IPointData, IBooleanMap, IFilmFileType, ILeaf, IFilmPlayOptions, IUI, ILeafPaint } from '@leafer-ui/interface';
|
|
2
|
+
import { Image, ImageData } from '@leafer-ui/draw';
|
|
3
|
+
|
|
4
|
+
declare class Film<TInputData = IFilmInputData> extends Image<TInputData> implements IFilm {
|
|
5
|
+
get __tag(): string;
|
|
6
|
+
__: IFilmData;
|
|
7
|
+
}
|
|
8
|
+
interface Film {
|
|
9
|
+
togglePlay(): void;
|
|
10
|
+
play(): void;
|
|
11
|
+
pause(): void;
|
|
12
|
+
stop(): void;
|
|
13
|
+
seekFrame(frameIndex: number): void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
declare class FilmData extends ImageData {
|
|
17
|
+
get __urlType(): IMultimediaType;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface IFilmDecoderMap {
|
|
21
|
+
[name: string]: {
|
|
22
|
+
Decoder: any;
|
|
23
|
+
force?: boolean;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
declare class FilmDecoder implements IFilmDecoder {
|
|
27
|
+
width: number;
|
|
28
|
+
height: number;
|
|
29
|
+
total: number;
|
|
30
|
+
loop: number;
|
|
31
|
+
frames: IFilmFrame[];
|
|
32
|
+
decodedIndex: number;
|
|
33
|
+
atlas?: any;
|
|
34
|
+
atlasContext?: ICanvasContext2D;
|
|
35
|
+
atlasGrid?: IPointData;
|
|
36
|
+
decoder: ImageDecoder;
|
|
37
|
+
bufferCanvas?: any;
|
|
38
|
+
bufferContext?: ICanvasContext2D;
|
|
39
|
+
static decoderMap: IFilmDecoderMap;
|
|
40
|
+
static supportMap: IBooleanMap;
|
|
41
|
+
static maxAtlasSize: number;
|
|
42
|
+
static register(type: IFilmFileType, Decoder: any, force?: boolean): void;
|
|
43
|
+
static canUse(mimeType: string): Promise<boolean>;
|
|
44
|
+
createAtlas(): void;
|
|
45
|
+
decodeHeader(data: ArrayBuffer, type: string): Promise<void>;
|
|
46
|
+
decodeFrame(frameIndex: number): Promise<IFilmFrame>;
|
|
47
|
+
decodeOneFrame(frameIndex: number): Promise<IFilmFrame>;
|
|
48
|
+
mergeFrame(frameIndex: number, destoryFrameImage?: boolean): void;
|
|
49
|
+
render(canvas: ICanvasContext2D, x: number, y: number, width: number, height: number, leaf: ILeaf, playOptions: IFilmPlayOptions, _imageScaleX: number, _imageScaleY: number): void;
|
|
50
|
+
private renderFrame;
|
|
51
|
+
destoryFrame(frameIndex: number, deleteIndex?: boolean): void;
|
|
52
|
+
destoryFrameImage(frame: IFilmFrame): void;
|
|
53
|
+
destroyDecoder(): void;
|
|
54
|
+
close(): void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
declare class PaintFilm {
|
|
58
|
+
element: IUI;
|
|
59
|
+
get playerOptions(): ILeafPaint;
|
|
60
|
+
get decoder(): IFilmDecoder;
|
|
61
|
+
constructor(element: IUI);
|
|
62
|
+
togglePlay(): void;
|
|
63
|
+
play(): void;
|
|
64
|
+
pause(): void;
|
|
65
|
+
stop(): void;
|
|
66
|
+
seekFrame(frameIndex: number): void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { Film, FilmData, FilmDecoder, PaintFilm };
|