@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.
- package/README.md +169 -0
- package/dist/index.css +921 -0
- package/dist/index.js +14076 -0
- package/package.json +49 -0
- package/src/editor/css/main.css +1030 -0
- package/src/editor/js/Animation.js +618 -0
- package/src/editor/js/AnimationResizer.js +73 -0
- package/src/editor/js/Command.js +40 -0
- package/src/editor/js/Config.js +86 -0
- package/src/editor/js/Editor.js +1006 -0
- package/src/editor/js/EditorControls.js +489 -0
- package/src/editor/js/GLTFImportDialog.js +112 -0
- package/src/editor/js/History.js +321 -0
- package/src/editor/js/Loader.js +1162 -0
- package/src/editor/js/LoaderUtils.js +90 -0
- package/src/editor/js/Menubar.Add.js +559 -0
- package/src/editor/js/Menubar.Edit.js +155 -0
- package/src/editor/js/Menubar.File.js +585 -0
- package/src/editor/js/Menubar.Help.js +73 -0
- package/src/editor/js/Menubar.Render.js +858 -0
- package/src/editor/js/Menubar.Status.js +51 -0
- package/src/editor/js/Menubar.View.js +193 -0
- package/src/editor/js/Menubar.js +29 -0
- package/src/editor/js/Player.js +53 -0
- package/src/editor/js/Resizer.js +65 -0
- package/src/editor/js/Script.js +532 -0
- package/src/editor/js/Selector.js +129 -0
- package/src/editor/js/Sidebar.Geometry.BoxGeometry.js +121 -0
- package/src/editor/js/Sidebar.Geometry.BufferGeometry.js +124 -0
- package/src/editor/js/Sidebar.Geometry.CapsuleGeometry.js +109 -0
- package/src/editor/js/Sidebar.Geometry.CircleGeometry.js +97 -0
- package/src/editor/js/Sidebar.Geometry.CylinderGeometry.js +121 -0
- package/src/editor/js/Sidebar.Geometry.DodecahedronGeometry.js +73 -0
- package/src/editor/js/Sidebar.Geometry.ExtrudeGeometry.js +196 -0
- package/src/editor/js/Sidebar.Geometry.IcosahedronGeometry.js +73 -0
- package/src/editor/js/Sidebar.Geometry.LatheGeometry.js +98 -0
- package/src/editor/js/Sidebar.Geometry.Modifiers.js +73 -0
- package/src/editor/js/Sidebar.Geometry.OctahedronGeometry.js +74 -0
- package/src/editor/js/Sidebar.Geometry.PlaneGeometry.js +97 -0
- package/src/editor/js/Sidebar.Geometry.RingGeometry.js +121 -0
- package/src/editor/js/Sidebar.Geometry.ShapeGeometry.js +76 -0
- package/src/editor/js/Sidebar.Geometry.SphereGeometry.js +133 -0
- package/src/editor/js/Sidebar.Geometry.TetrahedronGeometry.js +74 -0
- package/src/editor/js/Sidebar.Geometry.TextGeometry.js +136 -0
- package/src/editor/js/Sidebar.Geometry.TorusGeometry.js +109 -0
- package/src/editor/js/Sidebar.Geometry.TorusKnotGeometry.js +121 -0
- package/src/editor/js/Sidebar.Geometry.TubeGeometry.js +135 -0
- package/src/editor/js/Sidebar.Geometry.js +471 -0
- package/src/editor/js/Sidebar.Material.BooleanProperty.js +60 -0
- package/src/editor/js/Sidebar.Material.ColorProperty.js +87 -0
- package/src/editor/js/Sidebar.Material.ConstantProperty.js +62 -0
- package/src/editor/js/Sidebar.Material.MapProperty.js +277 -0
- package/src/editor/js/Sidebar.Material.NumberProperty.js +60 -0
- package/src/editor/js/Sidebar.Material.Program.js +73 -0
- package/src/editor/js/Sidebar.Material.RangeValueProperty.js +63 -0
- package/src/editor/js/Sidebar.Material.js +751 -0
- package/src/editor/js/Sidebar.Object.js +892 -0
- package/src/editor/js/Sidebar.Project.App.js +218 -0
- package/src/editor/js/Sidebar.Project.Materials.js +82 -0
- package/src/editor/js/Sidebar.Project.Renderer.js +193 -0
- package/src/editor/js/Sidebar.Project.Resources.js +242 -0
- package/src/editor/js/Sidebar.Project.js +21 -0
- package/src/editor/js/Sidebar.Properties.js +73 -0
- package/src/editor/js/Sidebar.Scene.js +643 -0
- package/src/editor/js/Sidebar.Script.js +129 -0
- package/src/editor/js/Sidebar.Settings.History.js +146 -0
- package/src/editor/js/Sidebar.Settings.Shortcuts.js +198 -0
- package/src/editor/js/Sidebar.Settings.js +58 -0
- package/src/editor/js/Sidebar.js +41 -0
- package/src/editor/js/Storage.js +98 -0
- package/src/editor/js/Strings.js +2718 -0
- package/src/editor/js/TextureParametersDialog.js +293 -0
- package/src/editor/js/Toolbar.js +75 -0
- package/src/editor/js/Viewport.Controls.js +98 -0
- package/src/editor/js/Viewport.Info.js +144 -0
- package/src/editor/js/Viewport.Pathtracer.js +27 -0
- package/src/editor/js/Viewport.ViewHelper.js +41 -0
- package/src/editor/js/Viewport.XR.js +7 -0
- package/src/editor/js/Viewport.js +982 -0
- package/src/editor/js/commands/AddObjectCommand.js +68 -0
- package/src/editor/js/commands/AddScriptCommand.js +75 -0
- package/src/editor/js/commands/Commands.js +24 -0
- package/src/editor/js/commands/MoveObjectCommand.js +117 -0
- package/src/editor/js/commands/MultiCmdsCommand.js +85 -0
- package/src/editor/js/commands/RemoveObjectCommand.js +88 -0
- package/src/editor/js/commands/RemoveScriptCommand.js +81 -0
- package/src/editor/js/commands/SetColorCommand.js +73 -0
- package/src/editor/js/commands/SetGeometryCommand.js +86 -0
- package/src/editor/js/commands/SetGeometryValueCommand.js +70 -0
- package/src/editor/js/commands/SetMaterialColorCommand.js +87 -0
- package/src/editor/js/commands/SetMaterialCommand.js +80 -0
- package/src/editor/js/commands/SetMaterialMapCommand.js +144 -0
- package/src/editor/js/commands/SetMaterialRangeCommand.js +92 -0
- package/src/editor/js/commands/SetMaterialValueCommand.js +91 -0
- package/src/editor/js/commands/SetMaterialVectorCommand.js +88 -0
- package/src/editor/js/commands/SetPositionCommand.js +84 -0
- package/src/editor/js/commands/SetRotationCommand.js +84 -0
- package/src/editor/js/commands/SetScaleCommand.js +84 -0
- package/src/editor/js/commands/SetSceneCommand.js +104 -0
- package/src/editor/js/commands/SetScriptValueCommand.js +80 -0
- package/src/editor/js/commands/SetShadowValueCommand.js +73 -0
- package/src/editor/js/commands/SetTextureParametersCommand.js +143 -0
- package/src/editor/js/commands/SetUuidCommand.js +70 -0
- package/src/editor/js/commands/SetValueCommand.js +75 -0
- package/src/editor/js/libs/acorn/acorn.js +3236 -0
- package/src/editor/js/libs/acorn/acorn_loose.js +1299 -0
- package/src/editor/js/libs/acorn/walk.js +344 -0
- package/src/editor/js/libs/app/index.html +51 -0
- package/src/editor/js/libs/app.js +14 -0
- package/src/editor/js/libs/codemirror/addon/dialog.css +32 -0
- package/src/editor/js/libs/codemirror/addon/dialog.js +163 -0
- package/src/editor/js/libs/codemirror/addon/show-hint.css +36 -0
- package/src/editor/js/libs/codemirror/addon/show-hint.js +529 -0
- package/src/editor/js/libs/codemirror/addon/tern.css +87 -0
- package/src/editor/js/libs/codemirror/addon/tern.js +750 -0
- package/src/editor/js/libs/codemirror/codemirror.css +344 -0
- package/src/editor/js/libs/codemirror/codemirror.js +9849 -0
- package/src/editor/js/libs/codemirror/mode/glsl.js +233 -0
- package/src/editor/js/libs/codemirror/mode/javascript.js +959 -0
- package/src/editor/js/libs/codemirror/theme/monokai.css +41 -0
- package/src/editor/js/libs/esprima.js +6401 -0
- package/src/editor/js/libs/jsonlint.js +453 -0
- package/src/editor/js/libs/signals.min.js +14 -0
- package/src/editor/js/libs/tern-threejs/build-defs.js +233 -0
- package/src/editor/js/libs/ternjs/comment.js +87 -0
- package/src/editor/js/libs/ternjs/def.js +588 -0
- package/src/editor/js/libs/ternjs/doc_comment.js +401 -0
- package/src/editor/js/libs/ternjs/infer.js +1635 -0
- package/src/editor/js/libs/ternjs/polyfill.js +80 -0
- package/src/editor/js/libs/ternjs/signal.js +26 -0
- package/src/editor/js/libs/ternjs/tern.js +993 -0
- package/src/editor/js/libs/ui.js +1305 -0
- package/src/editor/js/libs/ui.three.js +946 -0
- package/src/events.js +273 -0
- package/src/index.js +284 -0
- 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 };
|