@@ -5,16 +5,47 @@ import {
5
5
customElement ,
6
6
createCSSSheet ,
7
7
css ,
8
- connectStore ,
9
8
RefObject ,
10
9
refobject ,
11
10
numattribute ,
11
+ attribute ,
12
12
} from '@mantou/gem' ;
13
13
import { BaseDirectory } from '@tauri-apps/api/fs' ;
14
14
import { Time } from 'duoyun-ui/lib/time' ;
15
15
import { saveFile } from 'src/utils' ;
16
16
17
- import { configure } from 'src/configure' ;
17
+ import { VideoFilter } from 'src/constants' ;
18
+ import { logger } from 'src/logger' ;
19
+ import normalVert from 'src/shaders/normal.vert?raw' ;
20
+
21
+ const getShader = ( filter : VideoFilter ) => {
22
+ switch ( filter ) {
23
+ case VideoFilter . NTSC : {
24
+ return import ( 'src/shaders/ntsc.frag?raw' ) ;
25
+ }
26
+ case VideoFilter . CRT : {
27
+ return import ( 'src/shaders/crt.frag?raw' ) ;
28
+ }
29
+ default : {
30
+ return import ( 'src/shaders/normal.frag?raw' ) ;
31
+ }
32
+ }
33
+ } ;
34
+
35
+ const ortho = ( left : number , right : number , bottom : number , top : number ) : number [ ] => {
36
+ // prettier-ignore
37
+ const m = [
38
+ 1.0 , 0.0 , 0.0 , 0.0 ,
39
+ 0.0 , 1.0 , 0.0 , 0.0 ,
40
+ 0.0 , 0.0 , 1.0 , 0.0 ,
41
+ 0.0 , 0.0 , 0.0 , 1.0 ,
42
+ ] ;
43
+ m [ 0 * 4 + 0 ] = 2.0 / ( right - left ) ;
44
+ m [ 1 * 4 + 1 ] = 2.0 / ( top - bottom ) ;
45
+ m [ 3 * 4 + 0 ] = ( ( right + left ) / ( right - left ) ) * - 1.0 ;
46
+ m [ 3 * 4 + 1 ] = ( ( top + bottom ) / ( top - bottom ) ) * - 1.0 ;
47
+ return m ;
48
+ } ;
18
49
19
50
const style = createCSSSheet ( css `
20
51
:host {
@@ -33,30 +64,184 @@ const style = createCSSSheet(css`
33
64
*/
34
65
@adoptedStyle ( style )
35
66
@customElement ( 'nesbox-canvas' )
36
- @connectStore ( configure )
37
67
export class NesboxCanvasElement extends GemElement {
38
68
@numattribute width : number ;
39
69
@numattribute height : number ;
70
+ @attribute filter : VideoFilter ;
40
71
@refobject canvasRef : RefObject < HTMLCanvasElement > ;
41
72
73
+ #scale = 2 ;
74
+
75
+ get #renderWidth( ) {
76
+ return this . width * this . #scale;
77
+ }
78
+
79
+ get #renderHeight( ) {
80
+ return this . height * this . #scale;
81
+ }
82
+
83
+ // https://github.com/lukexor/tetanes/blob/main/web/www/src/index.ts#L60
84
+ #webgl?: WebGL2RenderingContext ;
85
+ #time_uniform: WebGLUniformLocation | null ;
86
+ #frame_uniform: WebGLUniformLocation | null ;
87
+ #frame = 1 ;
88
+ #setupWebGL = async ( ) => {
89
+ const width = this . width ;
90
+ const height = this . height ;
91
+ const max_size = Math . max ( width , height ) ;
92
+
93
+ const webgl = this . canvasRef . element ! . getContext ( 'webgl2' ) ;
94
+
95
+ if ( ! webgl ) {
96
+ logger . error ( 'WebGL rendering context not found.' ) ;
97
+ return null ;
98
+ }
99
+
100
+ const vertShader = webgl . createShader ( webgl . VERTEX_SHADER ) ;
101
+ const fragShader = webgl . createShader ( webgl . FRAGMENT_SHADER ) ;
102
+
103
+ if ( ! vertShader || ! fragShader ) {
104
+ logger . error ( 'WebGL shader creation failed.' ) ;
105
+ return null ;
106
+ }
107
+
108
+ webgl . shaderSource ( vertShader , normalVert ) ;
109
+ webgl . shaderSource ( fragShader , ( await getShader ( this . filter ) ) . default ) ;
110
+ webgl . compileShader ( vertShader ) ;
111
+ webgl . compileShader ( fragShader ) ;
112
+
113
+ if ( ! webgl . getShaderParameter ( vertShader , webgl . COMPILE_STATUS ) ) {
114
+ logger . error ( 'WebGL vertex shader compilation failed:' , webgl . getShaderInfoLog ( vertShader ) ) ;
115
+ return null ;
116
+ }
117
+ if ( ! webgl . getShaderParameter ( fragShader , webgl . COMPILE_STATUS ) ) {
118
+ logger . error ( 'WebGL fragment shader compilation failed:' , webgl . getShaderInfoLog ( fragShader ) ) ;
119
+ return null ;
120
+ }
121
+
122
+ const program = webgl . createProgram ( ) ;
123
+ if ( ! program ) {
124
+ logger . error ( 'WebGL program creation failed.' ) ;
125
+ return null ;
126
+ }
127
+
128
+ webgl . attachShader ( program , vertShader ) ;
129
+ webgl . attachShader ( program , fragShader ) ;
130
+ webgl . linkProgram ( program ) ;
131
+ if ( ! webgl . getProgramParameter ( program , webgl . LINK_STATUS ) ) {
132
+ logger . error ( 'WebGL program linking failed!' ) ;
133
+ return null ;
134
+ }
135
+
136
+ webgl . useProgram ( program ) ;
137
+
138
+ const vertex_attr = webgl . getAttribLocation ( program , 'a_position' ) ;
139
+ const texcoord_attr = webgl . getAttribLocation ( program , 'a_texcoord' ) ;
140
+ webgl . enableVertexAttribArray ( vertex_attr ) ;
141
+ webgl . enableVertexAttribArray ( texcoord_attr ) ;
142
+
143
+ const samplerUint = 0 ;
144
+ const sampler_uniform = webgl . getUniformLocation ( program , 'u_sampler' ) ;
145
+ webgl . uniform1i ( sampler_uniform , samplerUint ) ;
146
+
147
+ const matrix_uniform = webgl . getUniformLocation ( program , 'u_matrix' ) ;
148
+ webgl . uniformMatrix4fv ( matrix_uniform , false , ortho ( 0.0 , width , height , 0.0 ) ) ;
149
+
150
+ const resolution_uniform = webgl . getUniformLocation ( program , 'u_resolution' ) ;
151
+ if ( resolution_uniform ) webgl . uniform3f ( resolution_uniform , this . width , this . height , 1 ) ;
152
+
153
+ // deps on frame uniforms
154
+ this . #time_uniform = webgl . getUniformLocation ( program , 'u_time' ) ;
155
+ this . #frame_uniform = webgl . getUniformLocation ( program , 'u_frame' ) ;
156
+
157
+ const texture = webgl . createTexture ( ) ;
158
+ webgl . activeTexture ( webgl . TEXTURE0 + samplerUint ) ;
159
+ webgl . bindTexture ( webgl . TEXTURE_2D , texture ) ;
160
+ webgl . texImage2D (
161
+ webgl . TEXTURE_2D ,
162
+ 0 ,
163
+ webgl . RGBA ,
164
+ max_size ,
165
+ max_size ,
166
+ 0 ,
167
+ webgl . RGBA ,
168
+ webgl . UNSIGNED_BYTE ,
169
+ new Uint8Array ( max_size * max_size * 4 ) ,
170
+ ) ;
171
+ webgl . texParameteri ( webgl . TEXTURE_2D , webgl . TEXTURE_MAG_FILTER , webgl . NEAREST ) ;
172
+ webgl . texParameteri ( webgl . TEXTURE_2D , webgl . TEXTURE_MIN_FILTER , webgl . NEAREST ) ;
173
+
174
+ const vertex_buffer = webgl . createBuffer ( ) ;
175
+ webgl . bindBuffer ( webgl . ARRAY_BUFFER , vertex_buffer ) ;
176
+ // prettier-ignore
177
+ const vertices = [
178
+ 0.0 , 0.0 ,
179
+ 0.0 , height ,
180
+ width , 0.0 ,
181
+ width , height ,
182
+ ] ;
183
+ webgl . bufferData ( webgl . ARRAY_BUFFER , new Float32Array ( vertices ) , webgl . STATIC_DRAW ) ;
184
+ webgl . vertexAttribPointer ( vertex_attr , 2 , webgl . FLOAT , false , 0 , 0 ) ;
185
+
186
+ const texcoord_buffer = webgl . createBuffer ( ) ;
187
+ webgl . bindBuffer ( webgl . ARRAY_BUFFER , texcoord_buffer ) ;
188
+ // prettier-ignore
189
+ const texcoords = [
190
+ 0.0 , 0.0 ,
191
+ 0.0 , height / width ,
192
+ 1.0 , 0.0 ,
193
+ 1.0 , height / width ,
194
+ ] ;
195
+ webgl . bufferData ( webgl . ARRAY_BUFFER , new Float32Array ( texcoords ) , webgl . STATIC_DRAW ) ;
196
+ webgl . vertexAttribPointer ( texcoord_attr , 2 , webgl . FLOAT , false , 0 , 0 ) ;
197
+
198
+ const index_buffer = webgl . createBuffer ( ) ;
199
+ webgl . bindBuffer ( webgl . ELEMENT_ARRAY_BUFFER , index_buffer ) ;
200
+ // vertices index
201
+ // prettier-ignore
202
+ const indices = [
203
+ 0 , 1 , 2 ,
204
+ 2 , 3 , 1 ,
205
+ ] ;
206
+ webgl . bufferData ( webgl . ELEMENT_ARRAY_BUFFER , new Uint16Array ( indices ) , webgl . STATIC_DRAW ) ;
207
+
208
+ webgl . clear ( webgl . COLOR_BUFFER_BIT ) ;
209
+ webgl . enable ( webgl . DEPTH_TEST ) ;
210
+ webgl . viewport ( 0 , 0 , this . #renderWidth, this . #renderHeight) ;
211
+
212
+ return webgl ;
213
+ } ;
214
+
42
215
paint = ( frame : Uint8Array , part ?: number [ ] ) => {
43
- if ( ! this . #imageData) return ;
44
- const { data } = this . #imageData;
45
- if ( part ) {
46
- const [ x , y , _ , h ] = part ;
47
- // NOTE: current only support full width
48
- const w = this . width ;
49
- for ( let i = 0 ; i < h ; i ++ ) {
50
- const line = new Uint8Array ( frame . buffer , frame . byteOffset + i * w * 4 , w * 4 ) ;
51
- data . set ( line , ( ( i + y ) * this . width + x ) * 4 ) ;
52
- }
53
- } else {
54
- data . set ( frame ) ;
216
+ if ( ! this . #webgl) return ;
217
+
218
+ if ( this . #time_uniform) {
219
+ this . #webgl. uniform1f ( this . #time_uniform, performance . now ( ) ) ;
220
+ }
221
+ if ( this . #frame_uniform) {
222
+ this . #webgl. uniform1i ( this . #frame_uniform, this . #frame++ ) ;
55
223
}
56
- this . canvasRef . element ! . getContext ( '2d' ) ! . putImageData ( this . #imageData, 0 , 0 ) ;
224
+
225
+ // only support full width
226
+ const [ x , y , __ , h ] = part || [ 0 , 0 , this . width , this . height ] ;
227
+ this . #webgl. texSubImage2D (
228
+ this . #webgl. TEXTURE_2D ,
229
+ 0 ,
230
+ x ,
231
+ y ,
232
+ this . width ,
233
+ h ,
234
+ this . #webgl. RGBA ,
235
+ this . #webgl. UNSIGNED_BYTE ,
236
+ frame ,
237
+ ) ;
238
+ this . #webgl. drawElements ( this . #webgl. TRIANGLES , 6 , this . #webgl. UNSIGNED_SHORT , 0 ) ;
239
+
240
+ this . #tasks. forEach ( ( task ) => task ( ) ) ;
241
+ this . #tasks. length = 0 ;
57
242
} ;
58
243
59
- screenshot = ( ) => {
244
+ # screenshot = ( ) => {
60
245
return new Promise < BaseDirectory | undefined > ( ( res ) => {
61
246
this . canvasRef . element ! . toBlob (
62
247
( blob ) => blob && res ( saveFile ( new File ( [ blob ] , `Screenshot ${ new Time ( ) . format ( ) } .png` ) ) ) ,
@@ -66,20 +251,42 @@ export class NesboxCanvasElement extends GemElement {
66
251
} ) ;
67
252
} ;
68
253
254
+ #tasks: ( ( ) => void ) [ ] = [ ] ;
255
+
256
+ screenshot = ( ) => {
257
+ return new Promise ( ( res , rej ) => {
258
+ this . #tasks. push ( async ( ) => {
259
+ try {
260
+ res ( await this . #screenshot( ) ) ;
261
+ } catch ( err ) {
262
+ rej ( err ) ;
263
+ }
264
+ } ) ;
265
+ } ) ;
266
+ } ;
267
+
69
268
captureThumbnail = ( ) => {
70
- return this . canvasRef . element ! . toDataURL ( 'image/png' , 0.5 ) ;
269
+ return new Promise < string > ( ( res ) => {
270
+ this . #tasks. push ( ( ) => {
271
+ res ( this . canvasRef . element ! . toDataURL ( 'image/png' , 0.5 ) ) ;
272
+ } ) ;
273
+ } ) ;
71
274
} ;
72
275
73
- #imageData?: ImageData ;
74
276
mounted = ( ) => {
75
- this . effect ( ( ) => {
277
+ this . effect ( async ( ) => {
76
278
if ( this . width ) {
77
- this . #imageData = new ImageData ( this . width , this . height ) ;
279
+ const webgl = await this . #setupWebGL( ) ;
280
+ if ( webgl ) {
281
+ this . #webgl = webgl ;
282
+ }
78
283
}
79
284
} ) ;
80
285
} ;
81
286
82
287
render = ( ) => {
83
- return html `< canvas class ="canvas " width =${ this . width } height =${ this . height } ref=${ this . canvasRef . ref } > </ canvas > ` ;
288
+ return html `
289
+ < canvas class ="canvas " width =${ this . #renderWidth} height =${ this . #renderHeight} ref=${ this . canvasRef . ref } > </ canvas >
290
+ ` ;
84
291
} ;
85
292
}
0 commit comments