@thanhthbm/scene-editor 0.1.0

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.
Files changed (136) hide show
  1. package/README.md +169 -0
  2. package/dist/index.css +921 -0
  3. package/dist/index.js +14076 -0
  4. package/package.json +49 -0
  5. package/src/editor/css/main.css +1030 -0
  6. package/src/editor/js/Animation.js +618 -0
  7. package/src/editor/js/AnimationResizer.js +73 -0
  8. package/src/editor/js/Command.js +40 -0
  9. package/src/editor/js/Config.js +86 -0
  10. package/src/editor/js/Editor.js +1006 -0
  11. package/src/editor/js/EditorControls.js +489 -0
  12. package/src/editor/js/GLTFImportDialog.js +112 -0
  13. package/src/editor/js/History.js +321 -0
  14. package/src/editor/js/Loader.js +1162 -0
  15. package/src/editor/js/LoaderUtils.js +90 -0
  16. package/src/editor/js/Menubar.Add.js +559 -0
  17. package/src/editor/js/Menubar.Edit.js +155 -0
  18. package/src/editor/js/Menubar.File.js +585 -0
  19. package/src/editor/js/Menubar.Help.js +73 -0
  20. package/src/editor/js/Menubar.Render.js +858 -0
  21. package/src/editor/js/Menubar.Status.js +51 -0
  22. package/src/editor/js/Menubar.View.js +193 -0
  23. package/src/editor/js/Menubar.js +29 -0
  24. package/src/editor/js/Player.js +53 -0
  25. package/src/editor/js/Resizer.js +65 -0
  26. package/src/editor/js/Script.js +532 -0
  27. package/src/editor/js/Selector.js +129 -0
  28. package/src/editor/js/Sidebar.Geometry.BoxGeometry.js +121 -0
  29. package/src/editor/js/Sidebar.Geometry.BufferGeometry.js +124 -0
  30. package/src/editor/js/Sidebar.Geometry.CapsuleGeometry.js +109 -0
  31. package/src/editor/js/Sidebar.Geometry.CircleGeometry.js +97 -0
  32. package/src/editor/js/Sidebar.Geometry.CylinderGeometry.js +121 -0
  33. package/src/editor/js/Sidebar.Geometry.DodecahedronGeometry.js +73 -0
  34. package/src/editor/js/Sidebar.Geometry.ExtrudeGeometry.js +196 -0
  35. package/src/editor/js/Sidebar.Geometry.IcosahedronGeometry.js +73 -0
  36. package/src/editor/js/Sidebar.Geometry.LatheGeometry.js +98 -0
  37. package/src/editor/js/Sidebar.Geometry.Modifiers.js +73 -0
  38. package/src/editor/js/Sidebar.Geometry.OctahedronGeometry.js +74 -0
  39. package/src/editor/js/Sidebar.Geometry.PlaneGeometry.js +97 -0
  40. package/src/editor/js/Sidebar.Geometry.RingGeometry.js +121 -0
  41. package/src/editor/js/Sidebar.Geometry.ShapeGeometry.js +76 -0
  42. package/src/editor/js/Sidebar.Geometry.SphereGeometry.js +133 -0
  43. package/src/editor/js/Sidebar.Geometry.TetrahedronGeometry.js +74 -0
  44. package/src/editor/js/Sidebar.Geometry.TextGeometry.js +136 -0
  45. package/src/editor/js/Sidebar.Geometry.TorusGeometry.js +109 -0
  46. package/src/editor/js/Sidebar.Geometry.TorusKnotGeometry.js +121 -0
  47. package/src/editor/js/Sidebar.Geometry.TubeGeometry.js +135 -0
  48. package/src/editor/js/Sidebar.Geometry.js +471 -0
  49. package/src/editor/js/Sidebar.Material.BooleanProperty.js +60 -0
  50. package/src/editor/js/Sidebar.Material.ColorProperty.js +87 -0
  51. package/src/editor/js/Sidebar.Material.ConstantProperty.js +62 -0
  52. package/src/editor/js/Sidebar.Material.MapProperty.js +277 -0
  53. package/src/editor/js/Sidebar.Material.NumberProperty.js +60 -0
  54. package/src/editor/js/Sidebar.Material.Program.js +73 -0
  55. package/src/editor/js/Sidebar.Material.RangeValueProperty.js +63 -0
  56. package/src/editor/js/Sidebar.Material.js +751 -0
  57. package/src/editor/js/Sidebar.Object.js +892 -0
  58. package/src/editor/js/Sidebar.Project.App.js +218 -0
  59. package/src/editor/js/Sidebar.Project.Materials.js +82 -0
  60. package/src/editor/js/Sidebar.Project.Renderer.js +193 -0
  61. package/src/editor/js/Sidebar.Project.Resources.js +242 -0
  62. package/src/editor/js/Sidebar.Project.js +21 -0
  63. package/src/editor/js/Sidebar.Properties.js +73 -0
  64. package/src/editor/js/Sidebar.Scene.js +643 -0
  65. package/src/editor/js/Sidebar.Script.js +129 -0
  66. package/src/editor/js/Sidebar.Settings.History.js +146 -0
  67. package/src/editor/js/Sidebar.Settings.Shortcuts.js +198 -0
  68. package/src/editor/js/Sidebar.Settings.js +58 -0
  69. package/src/editor/js/Sidebar.js +41 -0
  70. package/src/editor/js/Storage.js +98 -0
  71. package/src/editor/js/Strings.js +2718 -0
  72. package/src/editor/js/TextureParametersDialog.js +293 -0
  73. package/src/editor/js/Toolbar.js +75 -0
  74. package/src/editor/js/Viewport.Controls.js +98 -0
  75. package/src/editor/js/Viewport.Info.js +144 -0
  76. package/src/editor/js/Viewport.Pathtracer.js +27 -0
  77. package/src/editor/js/Viewport.ViewHelper.js +41 -0
  78. package/src/editor/js/Viewport.XR.js +7 -0
  79. package/src/editor/js/Viewport.js +982 -0
  80. package/src/editor/js/commands/AddObjectCommand.js +68 -0
  81. package/src/editor/js/commands/AddScriptCommand.js +75 -0
  82. package/src/editor/js/commands/Commands.js +24 -0
  83. package/src/editor/js/commands/MoveObjectCommand.js +117 -0
  84. package/src/editor/js/commands/MultiCmdsCommand.js +85 -0
  85. package/src/editor/js/commands/RemoveObjectCommand.js +88 -0
  86. package/src/editor/js/commands/RemoveScriptCommand.js +81 -0
  87. package/src/editor/js/commands/SetColorCommand.js +73 -0
  88. package/src/editor/js/commands/SetGeometryCommand.js +86 -0
  89. package/src/editor/js/commands/SetGeometryValueCommand.js +70 -0
  90. package/src/editor/js/commands/SetMaterialColorCommand.js +87 -0
  91. package/src/editor/js/commands/SetMaterialCommand.js +80 -0
  92. package/src/editor/js/commands/SetMaterialMapCommand.js +144 -0
  93. package/src/editor/js/commands/SetMaterialRangeCommand.js +92 -0
  94. package/src/editor/js/commands/SetMaterialValueCommand.js +91 -0
  95. package/src/editor/js/commands/SetMaterialVectorCommand.js +88 -0
  96. package/src/editor/js/commands/SetPositionCommand.js +84 -0
  97. package/src/editor/js/commands/SetRotationCommand.js +84 -0
  98. package/src/editor/js/commands/SetScaleCommand.js +84 -0
  99. package/src/editor/js/commands/SetSceneCommand.js +104 -0
  100. package/src/editor/js/commands/SetScriptValueCommand.js +80 -0
  101. package/src/editor/js/commands/SetShadowValueCommand.js +73 -0
  102. package/src/editor/js/commands/SetTextureParametersCommand.js +143 -0
  103. package/src/editor/js/commands/SetUuidCommand.js +70 -0
  104. package/src/editor/js/commands/SetValueCommand.js +75 -0
  105. package/src/editor/js/libs/acorn/acorn.js +3236 -0
  106. package/src/editor/js/libs/acorn/acorn_loose.js +1299 -0
  107. package/src/editor/js/libs/acorn/walk.js +344 -0
  108. package/src/editor/js/libs/app/index.html +51 -0
  109. package/src/editor/js/libs/app.js +14 -0
  110. package/src/editor/js/libs/codemirror/addon/dialog.css +32 -0
  111. package/src/editor/js/libs/codemirror/addon/dialog.js +163 -0
  112. package/src/editor/js/libs/codemirror/addon/show-hint.css +36 -0
  113. package/src/editor/js/libs/codemirror/addon/show-hint.js +529 -0
  114. package/src/editor/js/libs/codemirror/addon/tern.css +87 -0
  115. package/src/editor/js/libs/codemirror/addon/tern.js +750 -0
  116. package/src/editor/js/libs/codemirror/codemirror.css +344 -0
  117. package/src/editor/js/libs/codemirror/codemirror.js +9849 -0
  118. package/src/editor/js/libs/codemirror/mode/glsl.js +233 -0
  119. package/src/editor/js/libs/codemirror/mode/javascript.js +959 -0
  120. package/src/editor/js/libs/codemirror/theme/monokai.css +41 -0
  121. package/src/editor/js/libs/esprima.js +6401 -0
  122. package/src/editor/js/libs/jsonlint.js +453 -0
  123. package/src/editor/js/libs/signals.min.js +14 -0
  124. package/src/editor/js/libs/tern-threejs/build-defs.js +233 -0
  125. package/src/editor/js/libs/ternjs/comment.js +87 -0
  126. package/src/editor/js/libs/ternjs/def.js +588 -0
  127. package/src/editor/js/libs/ternjs/doc_comment.js +401 -0
  128. package/src/editor/js/libs/ternjs/infer.js +1635 -0
  129. package/src/editor/js/libs/ternjs/polyfill.js +80 -0
  130. package/src/editor/js/libs/ternjs/signal.js +26 -0
  131. package/src/editor/js/libs/ternjs/tern.js +993 -0
  132. package/src/editor/js/libs/ui.js +1305 -0
  133. package/src/editor/js/libs/ui.three.js +946 -0
  134. package/src/events.js +273 -0
  135. package/src/index.js +284 -0
  136. package/src/presets/machines.js +239 -0
@@ -0,0 +1,858 @@
1
+ import * as THREE from 'three';
2
+
3
+ import { UIPanel, UIRow, UIButton, UIInteger, UISelect, UIText } from './libs/ui.js';
4
+
5
+ import { ViewportPathtracer } from './Viewport.Pathtracer.js';
6
+ import { APP } from './libs/app.js';
7
+
8
+ function MenubarRender( editor ) {
9
+
10
+ const strings = editor.strings;
11
+
12
+ const container = new UIPanel();
13
+ container.setClass( 'menu' );
14
+
15
+ const title = new UIPanel();
16
+ title.setClass( 'title' );
17
+ title.setTextContent( strings.getKey( 'menubar/render' ) );
18
+ container.add( title );
19
+
20
+ const options = new UIPanel();
21
+ options.setClass( 'options' );
22
+ container.add( options );
23
+
24
+ // Image
25
+
26
+ let option = new UIRow();
27
+ option.setClass( 'option' );
28
+ option.setTextContent( strings.getKey( 'menubar/render/image' ) );
29
+ option.onClick( function () {
30
+
31
+ showImageDialog();
32
+
33
+ } );
34
+ options.add( option );
35
+
36
+ // Video
37
+
38
+ if ( 'VideoEncoder' in window ) {
39
+
40
+ option = new UIRow();
41
+ option.setClass( 'option' );
42
+ option.setTextContent( strings.getKey( 'menubar/render/video' ) );
43
+ option.onClick( function () {
44
+
45
+ showVideoDialog();
46
+
47
+ } );
48
+ options.add( option );
49
+
50
+ }
51
+
52
+ // Image Dialog
53
+
54
+ function showImageDialog() {
55
+
56
+ const dialog = new RenderImageDialog( editor, strings );
57
+ document.body.appendChild( dialog.dom );
58
+
59
+ }
60
+
61
+ // Video Dialog
62
+
63
+ function showVideoDialog() {
64
+
65
+ const dialog = new RenderVideoDialog( editor, strings );
66
+ document.body.appendChild( dialog.dom );
67
+
68
+ }
69
+
70
+ return container;
71
+
72
+ }
73
+
74
+ class RenderImageDialog {
75
+
76
+ constructor( editor, strings ) {
77
+
78
+ const dom = document.createElement( 'div' );
79
+ dom.className = 'Dialog';
80
+ this.dom = dom;
81
+
82
+ const background = document.createElement( 'div' );
83
+ background.className = 'Dialog-background';
84
+ background.addEventListener( 'click', () => this.close() );
85
+ dom.appendChild( background );
86
+
87
+ const content = document.createElement( 'div' );
88
+ content.className = 'Dialog-content';
89
+ dom.appendChild( content );
90
+
91
+ // Title
92
+
93
+ const titleBar = document.createElement( 'div' );
94
+ titleBar.className = 'Dialog-title';
95
+ titleBar.textContent = strings.getKey( 'menubar/render' ) + ' ' + strings.getKey( 'menubar/render/image' );
96
+ content.appendChild( titleBar );
97
+
98
+ // Body
99
+
100
+ const body = document.createElement( 'div' );
101
+ body.className = 'Dialog-body';
102
+ content.appendChild( body );
103
+
104
+ // Shading
105
+
106
+ const shadingRow = new UIRow();
107
+ body.appendChild( shadingRow.dom );
108
+
109
+ shadingRow.add( new UIText( strings.getKey( 'sidebar/project/shading' ) ).setClass( 'Label' ) );
110
+
111
+ const shadingTypeSelect = new UISelect().setOptions( {
112
+ 'solid': 'SOLID',
113
+ 'realistic': 'REALISTIC'
114
+ } ).setWidth( '170px' ).onChange( refreshShadingRow ).setValue( 'solid' );
115
+ shadingRow.add( shadingTypeSelect );
116
+
117
+ const pathTracerMinSamples = 3;
118
+ const pathTracerMaxSamples = 65536;
119
+ const samplesNumber = new UIInteger( 16 ).setRange( pathTracerMinSamples, pathTracerMaxSamples );
120
+
121
+ const samplesRow = new UIRow();
122
+ samplesRow.add( new UIText( strings.getKey( 'sidebar/project/image/samples' ) ).setClass( 'Label' ) );
123
+ samplesRow.add( samplesNumber );
124
+ body.appendChild( samplesRow.dom );
125
+
126
+ function refreshShadingRow() {
127
+
128
+ samplesRow.setHidden( shadingTypeSelect.getValue() !== 'realistic' );
129
+
130
+ }
131
+
132
+ refreshShadingRow();
133
+
134
+ // Resolution
135
+
136
+ const resolutionRow = new UIRow();
137
+ body.appendChild( resolutionRow.dom );
138
+
139
+ resolutionRow.add( new UIText( strings.getKey( 'sidebar/project/resolution' ) ).setClass( 'Label' ) );
140
+
141
+ const imageWidth = new UIInteger( 1024 ).setTextAlign( 'center' ).setWidth( '28px' );
142
+ resolutionRow.add( imageWidth );
143
+
144
+ resolutionRow.add( new UIText( '\u00D7' ).setTextAlign( 'center' ).setFontSize( '12px' ).setWidth( '12px' ) );
145
+
146
+ const imageHeight = new UIInteger( 1024 ).setTextAlign( 'center' ).setWidth( '28px' );
147
+ resolutionRow.add( imageHeight );
148
+
149
+ // Buttons
150
+
151
+ const buttonsRow = document.createElement( 'div' );
152
+ buttonsRow.className = 'Dialog-buttons';
153
+ body.appendChild( buttonsRow );
154
+
155
+ const renderButton = new UIButton( strings.getKey( 'sidebar/project/render' ) );
156
+ renderButton.setWidth( '80px' );
157
+ renderButton.onClick( async () => {
158
+
159
+ if ( shadingTypeSelect.getValue() === 'realistic' ) {
160
+
161
+ let isMaterialsValid = true;
162
+
163
+ editor.scene.traverseVisible( ( object ) => {
164
+
165
+ if ( object.isMesh ) {
166
+
167
+ const materials = Array.isArray( object.material ) ? object.material : [ object.material ];
168
+
169
+ for ( let i = 0; i < materials.length; i ++ ) {
170
+
171
+ const material = materials[ i ];
172
+
173
+ if ( ! material.isMeshStandardMaterial ) {
174
+
175
+ isMaterialsValid = false;
176
+ return;
177
+
178
+ }
179
+
180
+ }
181
+
182
+ }
183
+
184
+ } );
185
+
186
+ if ( isMaterialsValid === false ) {
187
+
188
+ alert( strings.getKey( 'prompt/rendering/realistic/unsupportedMaterial' ) );
189
+ return;
190
+
191
+ }
192
+
193
+ }
194
+
195
+ //
196
+
197
+ const json = editor.toJSON();
198
+ const project = json.project;
199
+
200
+ //
201
+
202
+ const loader = new THREE.ObjectLoader();
203
+
204
+ const camera = await loader.parseAsync( json.camera );
205
+
206
+ const aspect = imageWidth.getValue() / imageHeight.getValue();
207
+
208
+ if ( camera.isPerspectiveCamera ) {
209
+
210
+ camera.aspect = aspect;
211
+
212
+ } else {
213
+
214
+ const frustumHeight = camera.top - camera.bottom;
215
+
216
+ camera.left = - frustumHeight * aspect / 2;
217
+ camera.right = frustumHeight * aspect / 2;
218
+
219
+ }
220
+
221
+ camera.updateProjectionMatrix();
222
+ camera.updateMatrixWorld();
223
+
224
+ const scene = await loader.parseAsync( json.scene );
225
+
226
+ const renderer = new THREE.WebGLRenderer( { antialias: true, reversedDepthBuffer: true } );
227
+ renderer.setSize( imageWidth.getValue(), imageHeight.getValue() );
228
+ renderer.setClearColor( editor.viewportColor );
229
+
230
+ if ( project.shadows !== undefined ) renderer.shadowMap.enabled = project.shadows;
231
+ if ( project.shadowType !== undefined ) renderer.shadowMap.type = project.shadowType;
232
+ if ( project.toneMapping !== undefined ) renderer.toneMapping = project.toneMapping;
233
+ if ( project.toneMappingExposure !== undefined ) renderer.toneMappingExposure = project.toneMappingExposure;
234
+
235
+ // popup
236
+
237
+ const width = imageWidth.getValue() / window.devicePixelRatio;
238
+ const height = imageHeight.getValue() / window.devicePixelRatio;
239
+
240
+ const left = ( screen.width - width ) / 2;
241
+ const top = ( screen.height - height ) / 2;
242
+
243
+ const output = window.open( '', '_blank', `location=no,left=${left},top=${top},width=${width},height=${height}` );
244
+
245
+ const meta = document.createElement( 'meta' );
246
+ meta.name = 'viewport';
247
+ meta.content = 'width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0';
248
+ output.document.head.appendChild( meta );
249
+
250
+ output.document.body.style.background = '#000';
251
+ output.document.body.style.margin = '0px';
252
+ output.document.body.style.overflow = 'hidden';
253
+
254
+ const canvas = renderer.domElement;
255
+ canvas.style.width = width + 'px';
256
+ canvas.style.height = height + 'px';
257
+ output.document.body.appendChild( canvas );
258
+
259
+ //
260
+
261
+ switch ( shadingTypeSelect.getValue() ) {
262
+
263
+ case 'solid':
264
+
265
+ renderer.render( scene, camera );
266
+ renderer.dispose();
267
+
268
+ break;
269
+
270
+ case 'realistic':
271
+
272
+ const status = document.createElement( 'div' );
273
+ status.style.position = 'absolute';
274
+ status.style.top = '10px';
275
+ status.style.left = '10px';
276
+ status.style.color = 'white';
277
+ status.style.fontFamily = 'system-ui';
278
+ status.style.fontSize = '12px';
279
+ output.document.body.appendChild( status );
280
+
281
+ const pathTracer = new ViewportPathtracer( renderer );
282
+ pathTracer.init( scene, camera );
283
+ pathTracer.setSize( imageWidth.getValue(), imageHeight.getValue() );
284
+
285
+ const maxSamples = Math.max( pathTracerMinSamples, Math.min( pathTracerMaxSamples, samplesNumber.getValue() ) );
286
+
287
+ function animate() {
288
+
289
+ if ( output.closed === true ) return;
290
+
291
+ const samples = Math.floor( pathTracer.getSamples() ) + 1;
292
+
293
+ if ( samples < maxSamples ) {
294
+
295
+ requestAnimationFrame( animate );
296
+
297
+ }
298
+
299
+ pathTracer.update();
300
+
301
+ const progress = Math.floor( samples / maxSamples * 100 );
302
+
303
+ status.textContent = `${ samples } / ${ maxSamples } ( ${ progress }% )`;
304
+
305
+ if ( progress === 100 ) {
306
+
307
+ status.textContent += ' \u2713';
308
+
309
+ }
310
+
311
+ }
312
+
313
+ animate();
314
+
315
+ break;
316
+
317
+ }
318
+
319
+ this.close();
320
+
321
+ } );
322
+ buttonsRow.appendChild( renderButton.dom );
323
+
324
+ const cancelButton = new UIButton( strings.getKey( 'menubar/render/cancel' ) );
325
+ cancelButton.setWidth( '80px' );
326
+ cancelButton.setMarginLeft( '8px' );
327
+ cancelButton.onClick( () => this.close() );
328
+ buttonsRow.appendChild( cancelButton.dom );
329
+
330
+ }
331
+
332
+ close() {
333
+
334
+ this.dom.remove();
335
+
336
+ }
337
+
338
+ }
339
+
340
+ class RenderVideoDialog {
341
+
342
+ constructor( editor, strings ) {
343
+
344
+ const dom = document.createElement( 'div' );
345
+ dom.className = 'Dialog';
346
+ this.dom = dom;
347
+
348
+ const background = document.createElement( 'div' );
349
+ background.className = 'Dialog-background';
350
+ background.addEventListener( 'click', () => this.close() );
351
+ dom.appendChild( background );
352
+
353
+ const content = document.createElement( 'div' );
354
+ content.className = 'Dialog-content';
355
+ dom.appendChild( content );
356
+
357
+ // Title
358
+
359
+ const titleBar = document.createElement( 'div' );
360
+ titleBar.className = 'Dialog-title';
361
+ titleBar.textContent = strings.getKey( 'menubar/render' ) + ' ' + strings.getKey( 'menubar/render/video' );
362
+ content.appendChild( titleBar );
363
+
364
+ // Body
365
+
366
+ const body = document.createElement( 'div' );
367
+ body.className = 'Dialog-body';
368
+ content.appendChild( body );
369
+
370
+ // Resolution
371
+
372
+ function toDiv2() {
373
+
374
+ this.setValue( 2 * Math.floor( this.getValue() / 2 ) );
375
+
376
+ }
377
+
378
+ const resolutionRow = new UIRow();
379
+ body.appendChild( resolutionRow.dom );
380
+
381
+ resolutionRow.add( new UIText( strings.getKey( 'sidebar/project/resolution' ) ).setClass( 'Label' ) );
382
+
383
+ const videoWidth = new UIInteger( 1024 ).setTextAlign( 'center' ).setWidth( '28px' ).setStep( 2 ).onChange( toDiv2 );
384
+ resolutionRow.add( videoWidth );
385
+
386
+ resolutionRow.add( new UIText( '\u00D7' ).setTextAlign( 'center' ).setFontSize( '12px' ).setWidth( '12px' ) );
387
+
388
+ const videoHeight = new UIInteger( 1024 ).setTextAlign( 'center' ).setWidth( '28px' ).setStep( 2 ).onChange( toDiv2 );
389
+ resolutionRow.add( videoHeight );
390
+
391
+ const videoFPS = new UIInteger( 30 ).setTextAlign( 'center' ).setWidth( '20px' );
392
+ resolutionRow.add( videoFPS );
393
+
394
+ resolutionRow.add( new UIText( 'fps' ).setFontSize( '12px' ) );
395
+
396
+ // Duration
397
+
398
+ const videoDurationRow = new UIRow();
399
+ videoDurationRow.add( new UIText( strings.getKey( 'sidebar/project/duration' ) ).setClass( 'Label' ) );
400
+ body.appendChild( videoDurationRow.dom );
401
+
402
+ const videoDuration = new UIInteger( 10 );
403
+ videoDurationRow.add( videoDuration );
404
+
405
+ // Quality
406
+
407
+ const qualityRow = new UIRow();
408
+ qualityRow.add( new UIText( strings.getKey( 'menubar/render/quality' ) ).setClass( 'Label' ) );
409
+ body.appendChild( qualityRow.dom );
410
+
411
+ const videoQuality = new UISelect().setOptions( {
412
+ 'low': 'Low',
413
+ 'medium': 'Medium',
414
+ 'high': 'High',
415
+ 'ultra': 'Ultra'
416
+ } ).setWidth( '170px' ).setValue( 'high' );
417
+ qualityRow.add( videoQuality );
418
+
419
+ // Buttons
420
+
421
+ const buttonsRow = document.createElement( 'div' );
422
+ buttonsRow.className = 'Dialog-buttons';
423
+ body.appendChild( buttonsRow );
424
+
425
+ const renderButton = new UIButton( strings.getKey( 'sidebar/project/render' ) );
426
+ renderButton.setWidth( '80px' );
427
+ renderButton.onClick( async () => {
428
+
429
+ const player = new APP.Player();
430
+ await player.load( editor.toJSON() );
431
+ player.setPixelRatio( 1 );
432
+ player.setSize( videoWidth.getValue(), videoHeight.getValue() );
433
+ player.setClearColor( editor.viewportColor );
434
+
435
+ //
436
+
437
+ const width = videoWidth.getValue() / window.devicePixelRatio;
438
+ const height = videoHeight.getValue() / window.devicePixelRatio;
439
+
440
+ const canvas = player.canvas;
441
+ canvas.style.width = width + 'px';
442
+ canvas.style.height = height + 'px';
443
+
444
+ const left = ( screen.width - width ) / 2;
445
+ const top = ( screen.height - height ) / 2;
446
+
447
+ const output = window.open( '', '_blank', `location=no,left=${left},top=${top},width=${width},height=${height}` );
448
+
449
+ const meta = document.createElement( 'meta' );
450
+ meta.name = 'viewport';
451
+ meta.content = 'width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0';
452
+ output.document.head.appendChild( meta );
453
+
454
+ output.document.body.style.background = '#000';
455
+ output.document.body.style.margin = '0px';
456
+ output.document.body.style.overflow = 'hidden';
457
+ output.document.body.appendChild( canvas );
458
+
459
+ const status = document.createElement( 'div' );
460
+ status.style.position = 'absolute';
461
+ status.style.top = '10px';
462
+ status.style.left = '10px';
463
+ status.style.color = 'white';
464
+ status.style.fontFamily = 'system-ui';
465
+ status.style.fontSize = '12px';
466
+ status.style.textShadow = '0 0 2px black';
467
+ output.document.body.appendChild( status );
468
+
469
+ const video = document.createElement( 'video' );
470
+ video.width = width;
471
+ video.height = height;
472
+ video.controls = true;
473
+ video.loop = true;
474
+ video.hidden = true;
475
+ output.document.body.appendChild( video );
476
+
477
+ output.addEventListener( 'unload', function () {
478
+
479
+ if ( video.src.startsWith( 'blob:' ) ) {
480
+
481
+ URL.revokeObjectURL( video.src );
482
+
483
+ }
484
+
485
+ } );
486
+
487
+ //
488
+
489
+ const fps = videoFPS.getValue();
490
+ const duration = videoDuration.getValue();
491
+ const frames = duration * fps;
492
+
493
+ const encodedChunks = [];
494
+ let codecConfig = null;
495
+
496
+ const videoEncoder = new VideoEncoder( {
497
+
498
+ output: ( chunk, metadata ) => {
499
+
500
+ if ( metadata?.decoderConfig?.description ) {
501
+
502
+ codecConfig = new Uint8Array( metadata.decoderConfig.description );
503
+
504
+ }
505
+
506
+ const chunkData = new Uint8Array( chunk.byteLength );
507
+ chunk.copyTo( chunkData );
508
+ encodedChunks.push( { data: chunkData, timestamp: chunk.timestamp, type: chunk.type } );
509
+
510
+ },
511
+ error: ( e ) => console.error( 'VideoEncoder error:', e )
512
+
513
+ } );
514
+
515
+ const qualityToBitrate = {
516
+ 'low': 2e6,
517
+ 'medium': 5e6,
518
+ 'high': 10e6,
519
+ 'ultra': 20e6
520
+ };
521
+
522
+ videoEncoder.configure( {
523
+ codec: 'avc1.640028',
524
+ width: videoWidth.getValue(),
525
+ height: videoHeight.getValue(),
526
+ bitrate: qualityToBitrate[ videoQuality.getValue() ],
527
+ framerate: fps,
528
+ avc: { format: 'avc' }
529
+ } );
530
+
531
+ let currentTime = 0;
532
+ let aborted = false;
533
+
534
+ for ( let i = 0; i < frames; i ++ ) {
535
+
536
+ if ( output.closed ) {
537
+
538
+ aborted = true;
539
+ break;
540
+
541
+ }
542
+
543
+ player.render( currentTime );
544
+
545
+ const bitmap = await createImageBitmap( canvas );
546
+ const frame = new VideoFrame( bitmap, { timestamp: i * ( 1e6 / fps ) } );
547
+
548
+ videoEncoder.encode( frame, { keyFrame: i % fps === 0 } );
549
+ frame.close();
550
+ bitmap.close();
551
+
552
+ currentTime += 1 / fps;
553
+
554
+ const progress = Math.floor( ( i + 1 ) / frames * 100 );
555
+ status.textContent = `${ i + 1 } / ${ frames } ( ${ progress }% )`;
556
+
557
+ }
558
+
559
+ if ( ! aborted ) {
560
+
561
+ await videoEncoder.flush();
562
+ videoEncoder.close();
563
+
564
+ output.document.body.removeChild( canvas );
565
+
566
+ const mp4Data = createMP4( encodedChunks, codecConfig, videoWidth.getValue(), videoHeight.getValue(), fps );
567
+
568
+ status.textContent = `${ frames } / ${ frames } ( 100% ) ${ formatFileSize( mp4Data.byteLength ) } \u2713`;
569
+
570
+ video.src = URL.createObjectURL( new Blob( [ mp4Data ], { type: 'video/mp4' } ) );
571
+ video.hidden = false;
572
+
573
+ }
574
+
575
+ player.dispose();
576
+
577
+ this.close();
578
+
579
+ } );
580
+ buttonsRow.appendChild( renderButton.dom );
581
+
582
+ const cancelButton = new UIButton( strings.getKey( 'menubar/render/cancel' ) );
583
+ cancelButton.setWidth( '80px' );
584
+ cancelButton.setMarginLeft( '8px' );
585
+ cancelButton.onClick( () => this.close() );
586
+ buttonsRow.appendChild( cancelButton.dom );
587
+
588
+ }
589
+
590
+ close() {
591
+
592
+ this.dom.remove();
593
+
594
+ }
595
+
596
+ }
597
+
598
+ // Simple MP4 muxer for H.264 encoded chunks
599
+
600
+ function createMP4( chunks, avcC, width, height, fps ) {
601
+
602
+ const timescale = 90000;
603
+ const frameDuration = timescale / fps;
604
+
605
+ function u32( value ) {
606
+
607
+ return new Uint8Array( [ ( value >> 24 ) & 0xFF, ( value >> 16 ) & 0xFF, ( value >> 8 ) & 0xFF, value & 0xFF ] );
608
+
609
+ }
610
+
611
+ function u16( value ) {
612
+
613
+ return new Uint8Array( [ ( value >> 8 ) & 0xFF, value & 0xFF ] );
614
+
615
+ }
616
+
617
+ function str( s ) {
618
+
619
+ return new TextEncoder().encode( s );
620
+
621
+ }
622
+
623
+ function concat( ...arrays ) {
624
+
625
+ const totalLength = arrays.reduce( ( sum, arr ) => sum + arr.length, 0 );
626
+ const result = new Uint8Array( totalLength );
627
+ let offset = 0;
628
+ for ( const arr of arrays ) {
629
+
630
+ result.set( arr, offset );
631
+ offset += arr.length;
632
+
633
+ }
634
+
635
+ return result;
636
+
637
+ }
638
+
639
+ function box( type, ...contents ) {
640
+
641
+ const data = concat( ...contents );
642
+ const size = data.length + 8;
643
+ return concat( u32( size ), str( type ), data );
644
+
645
+ }
646
+
647
+ function fullBox( type, version, flags, ...contents ) {
648
+
649
+ return box( type, new Uint8Array( [ version, ( flags >> 16 ) & 0xFF, ( flags >> 8 ) & 0xFF, flags & 0xFF ] ), ...contents );
650
+
651
+ }
652
+
653
+ // ftyp
654
+ const ftyp = box( 'ftyp',
655
+ str( 'isom' ),
656
+ u32( 512 ),
657
+ str( 'isom' ), str( 'iso2' ), str( 'avc1' ), str( 'mp41' )
658
+ );
659
+
660
+ // Collect sample info
661
+ const sampleSizes = [];
662
+ const syncSamples = [];
663
+
664
+ for ( let i = 0; i < chunks.length; i ++ ) {
665
+
666
+ sampleSizes.push( chunks[ i ].data.length );
667
+ if ( chunks[ i ].type === 'key' ) syncSamples.push( i + 1 );
668
+
669
+ }
670
+
671
+ // mdat
672
+ let mdatSize = 8;
673
+ for ( const chunk of chunks ) mdatSize += chunk.data.length;
674
+
675
+ // stsd - Sample Description
676
+ const avc1 = box( 'avc1',
677
+ new Uint8Array( 6 ), // reserved
678
+ u16( 1 ), // data reference index
679
+ new Uint8Array( 16 ), // pre-defined + reserved
680
+ u16( width ),
681
+ u16( height ),
682
+ u32( 0x00480000 ), // horizontal resolution 72 dpi
683
+ u32( 0x00480000 ), // vertical resolution 72 dpi
684
+ u32( 0 ), // reserved
685
+ u16( 1 ), // frame count
686
+ new Uint8Array( 32 ), // compressor name
687
+ u16( 0x0018 ), // depth
688
+ new Uint8Array( [ 0xFF, 0xFF ] ), // pre-defined
689
+ box( 'avcC', avcC )
690
+ );
691
+
692
+ const stsd = fullBox( 'stsd', 0, 0, u32( 1 ), avc1 );
693
+
694
+ // stts - Time-to-Sample
695
+ const stts = fullBox( 'stts', 0, 0,
696
+ u32( 1 ),
697
+ u32( chunks.length ),
698
+ u32( frameDuration )
699
+ );
700
+
701
+ // stsc - Sample-to-Chunk
702
+ const stsc = fullBox( 'stsc', 0, 0,
703
+ u32( 1 ),
704
+ u32( 1 ), u32( chunks.length ), u32( 1 )
705
+ );
706
+
707
+ // stsz - Sample Sizes
708
+ const stszData = [ u32( 0 ), u32( chunks.length ) ];
709
+ for ( const size of sampleSizes ) stszData.push( u32( size ) );
710
+ const stsz = fullBox( 'stsz', 0, 0, ...stszData );
711
+
712
+ // stco - Chunk Offsets (placeholder, will be updated)
713
+ const stco = fullBox( 'stco', 0, 0, u32( 1 ), u32( 0 ) );
714
+
715
+ // stss - Sync Samples
716
+ const stssData = [ u32( syncSamples.length ) ];
717
+ for ( const sync of syncSamples ) stssData.push( u32( sync ) );
718
+ const stss = fullBox( 'stss', 0, 0, ...stssData );
719
+
720
+ // stbl
721
+ const stbl = box( 'stbl', stsd, stts, stsc, stsz, stco, stss );
722
+
723
+ // dinf
724
+ const dref = fullBox( 'dref', 0, 0,
725
+ u32( 1 ),
726
+ fullBox( 'url ', 0, 1 )
727
+ );
728
+ const dinf = box( 'dinf', dref );
729
+
730
+ // vmhd
731
+ const vmhd = fullBox( 'vmhd', 0, 1, new Uint8Array( 8 ) );
732
+
733
+ // minf
734
+ const minf = box( 'minf', vmhd, dinf, stbl );
735
+
736
+ // hdlr
737
+ const hdlr = fullBox( 'hdlr', 0, 0,
738
+ u32( 0 ), // pre-defined
739
+ str( 'vide' ),
740
+ new Uint8Array( 12 ), // reserved
741
+ str( 'VideoHandler' ), new Uint8Array( 1 )
742
+ );
743
+
744
+ // mdhd
745
+ const durationInTimescale = chunks.length * frameDuration;
746
+ const mdhd = fullBox( 'mdhd', 0, 0,
747
+ u32( 0 ), // creation time
748
+ u32( 0 ), // modification time
749
+ u32( timescale ),
750
+ u32( durationInTimescale ),
751
+ u16( 0x55C4 ), // language (und)
752
+ u16( 0 ) // quality
753
+ );
754
+
755
+ // mdia
756
+ const mdia = box( 'mdia', mdhd, hdlr, minf );
757
+
758
+ // tkhd
759
+ const tkhd = fullBox( 'tkhd', 0, 3,
760
+ u32( 0 ), // creation time
761
+ u32( 0 ), // modification time
762
+ u32( 1 ), // track id
763
+ u32( 0 ), // reserved
764
+ u32( durationInTimescale ),
765
+ new Uint8Array( 8 ), // reserved
766
+ u16( 0 ), // layer
767
+ u16( 0 ), // alternate group
768
+ u16( 0 ), // volume
769
+ u16( 0 ), // reserved
770
+ // matrix
771
+ u32( 0x00010000 ), u32( 0 ), u32( 0 ),
772
+ u32( 0 ), u32( 0x00010000 ), u32( 0 ),
773
+ u32( 0 ), u32( 0 ), u32( 0x40000000 ),
774
+ u32( width << 16 ), // width (16.16 fixed point)
775
+ u32( height << 16 ) // height (16.16 fixed point)
776
+ );
777
+
778
+ // trak
779
+ const trak = box( 'trak', tkhd, mdia );
780
+
781
+ // mvhd
782
+ const mvhd = fullBox( 'mvhd', 0, 0,
783
+ u32( 0 ), // creation time
784
+ u32( 0 ), // modification time
785
+ u32( timescale ),
786
+ u32( durationInTimescale ),
787
+ u32( 0x00010000 ), // rate (1.0)
788
+ u16( 0x0100 ), // volume (1.0)
789
+ new Uint8Array( 10 ), // reserved
790
+ // matrix
791
+ u32( 0x00010000 ), u32( 0 ), u32( 0 ),
792
+ u32( 0 ), u32( 0x00010000 ), u32( 0 ),
793
+ u32( 0 ), u32( 0 ), u32( 0x40000000 ),
794
+ new Uint8Array( 24 ), // pre-defined
795
+ u32( 2 ) // next track id
796
+ );
797
+
798
+ // moov
799
+ const moov = box( 'moov', mvhd, trak );
800
+
801
+ // Calculate actual mdat offset and update stco
802
+ const mdatOffset = ftyp.length + moov.length;
803
+ const moovArray = new Uint8Array( moov );
804
+ // Find and update stco offset (search for 'stco' in moov)
805
+ for ( let i = 0; i < moovArray.length - 16; i ++ ) {
806
+
807
+ if ( moovArray[ i ] === 0x73 && moovArray[ i + 1 ] === 0x74 &&
808
+ moovArray[ i + 2 ] === 0x63 && moovArray[ i + 3 ] === 0x6F ) {
809
+
810
+ // Found 'stco', offset value is at i + 12
811
+ const offset = mdatOffset + 8;
812
+ moovArray[ i + 12 ] = ( offset >> 24 ) & 0xFF;
813
+ moovArray[ i + 13 ] = ( offset >> 16 ) & 0xFF;
814
+ moovArray[ i + 14 ] = ( offset >> 8 ) & 0xFF;
815
+ moovArray[ i + 15 ] = offset & 0xFF;
816
+ break;
817
+
818
+ }
819
+
820
+ }
821
+
822
+ // Update mdat size
823
+ const mdatSizeBytes = u32( mdatSize );
824
+
825
+ // Combine all parts
826
+ const result = new Uint8Array( ftyp.length + moovArray.length + mdatSize );
827
+ let offset = 0;
828
+ result.set( ftyp, offset ); offset += ftyp.length;
829
+ result.set( moovArray, offset ); offset += moovArray.length;
830
+ result.set( mdatSizeBytes, offset );
831
+ result.set( str( 'mdat' ), offset + 4 );
832
+ offset += 8;
833
+
834
+ for ( const chunk of chunks ) {
835
+
836
+ result.set( chunk.data, offset );
837
+ offset += chunk.data.length;
838
+
839
+ }
840
+
841
+ return result;
842
+
843
+ }
844
+
845
+ function formatFileSize( sizeB, K = 1024 ) {
846
+
847
+ if ( sizeB === 0 ) return '0B';
848
+
849
+ const sizes = [ sizeB, sizeB / K, sizeB / K / K ].reverse();
850
+ const units = [ 'B', 'KB', 'MB' ].reverse();
851
+ const index = sizes.findIndex( size => size >= 1 );
852
+
853
+ return new Intl.NumberFormat( 'en-us', { useGrouping: true, maximumFractionDigits: 1 } )
854
+ .format( sizes[ index ] ) + units[ index ];
855
+
856
+ }
857
+
858
+ export { MenubarRender };