Babylon.js controls are a set of regular web controls that used hardware accelerated rendering through Babylon.js to provide blazing fast dedicated controls.
80 |
81 |
Babylon.js provides an unified API on top of WebGL, WebGL2 and WebGPU that controls can leverage to unleash the raw power of your GPU.
82 |
83 |
You can find below the current list of controls we support :
84 |
85 |
86 |
87 |
88 | Timeline: a video timeline helping to display scrolling thumbnails awesomely fast.
89 |
90 |
91 |
92 | Resizer: a resizer control helping to to adapt image size awesomely fast. It can be paired with the timeline.
93 |
94 |
95 |
96 | ImageFilter: an image filter control helping to apply some filter to images really fast.
97 |
98 |
97 | This demo represents a resizer control leveraging WebGL to speed up resizing images.
98 |
99 |
100 | One of the biggest advantage is that the output can directly be used as a Babylon.js Texture so that if you need to resize thumbnails, they do not need any extra copies a canvas2D would have.
101 |
102 | This demo represents an image filter control leveraging WebGL to apply some effects to an image.
103 |
104 |
105 | One of the biggest advantage is that it relies fully on WebGL to accelerate the processing which could else be pretty expensive to do on CPU.
106 |
114 | This demo represents a timeline rendered through WebGL to speed up thumbnails management.
115 |
116 |
117 | Using the power of the GPU can dramatically improve the user experience whilst scrolling accross thousands of pictures.
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 | Start
127 |
128 | End
129 |
130 |
131 | Zoom 0%
132 |
133 | 100%
134 |
135 |
136 |
137 |
138 |
139 |
140 |
--------------------------------------------------------------------------------
/www/imageFilter/index.ts:
--------------------------------------------------------------------------------
1 | import { ImageFilter } from "../../src/imageFilter";
2 |
3 | import { BlackAndWhitePostProcess } from "@babylonjs/core/PostProcesses/blackAndWhitePostProcess";
4 |
5 | import { ImageProcessingPostProcess } from "@babylonjs/core/PostProcesses/imageProcessingPostProcess";
6 | import { ImageProcessingConfiguration } from "@babylonjs/core/Materials/imageProcessingConfiguration";
7 |
8 | import { EffectWrapper } from "@babylonjs/core/Materials/effectRenderer";
9 | import { Texture } from "@babylonjs/core/Materials/Textures/texture";
10 | import { Engine } from "@babylonjs/core/Engines/engine";
11 |
12 | const beforePicture1 = document.getElementById("beforePicture1") as HTMLImageElement;
13 | const beforePicture2 = document.getElementById("beforePicture2") as HTMLCanvasElement;
14 | const beforePicture3 = document.getElementById("beforePicture3") as HTMLImageElement;
15 | const afterPicture1 = document.getElementById("afterPicture1") as HTMLCanvasElement;
16 | const afterPicture2 = document.getElementById("afterPicture2") as HTMLCanvasElement;
17 | const afterPicture3 = document.getElementById("afterPicture3") as HTMLCanvasElement;
18 | const startProcessingButton = document.getElementById("startProcessingButton");
19 |
20 | const imageToProcess = "../assets/logo.png";
21 |
22 | // Filter the image based on a button click
23 | // This simply applies the filter once.
24 | function oneTimeFilterWithPostProcess() {
25 | const engine = new Engine(afterPicture3, false);
26 | const imageProcessingFilter = new ImageFilter(engine);
27 |
28 | const imageProcessingConfiguration = new ImageProcessingConfiguration();
29 | imageProcessingConfiguration.colorCurvesEnabled = true;
30 | imageProcessingConfiguration.colorCurves.globalSaturation = 80;
31 | const imageProcessingPostProcess = new ImageProcessingPostProcess("ip", 1, null, undefined, engine, undefined, undefined, imageProcessingConfiguration);
32 |
33 | // One time filter apply.
34 | startProcessingButton.addEventListener("click", function(e) {
35 | imageProcessingFilter.filter(imageToProcess, imageProcessingPostProcess);
36 |
37 | e.preventDefault();
38 | e.stopPropagation();
39 | return false;
40 | });
41 | }
42 |
43 | // Filter a 2d canvas this can be handfull, if you want to filter some drawings for instance.
44 | // This simply applies the filter once.
45 | function oneTimeFilterFromCanvas() {
46 | const engine = new Engine(afterPicture2, false);
47 | const backAndWhiteFilter = new ImageFilter(engine);
48 | const blackAndWhitePostProcess = new BlackAndWhitePostProcess("bw", 1, null, undefined, engine);
49 |
50 | const image = document.createElement('img');
51 | image.src = imageToProcess;
52 | image.addEventListener('load', e => {
53 | const ctx = beforePicture2.getContext("2d");
54 | ctx.drawImage(image, 128, 128, 256, 256);
55 | backAndWhiteFilter.filter(beforePicture2, blackAndWhitePostProcess);
56 | });
57 | }
58 |
59 | // Filter one image in realtime by updating the effect variables.
60 | // It also demo the usage of a custom input texture.
61 | function realTimeRenderAndCustomShader() {
62 | const engine = new Engine(afterPicture1, false);
63 | const customFilter = new ImageFilter(engine);
64 | const customEffectWrapper = new EffectWrapper({
65 | name: "Custom",
66 | engine: customFilter.engine,
67 | fragmentShader: `
68 | varying vec2 vUV;
69 |
70 | // Default Sampler
71 | uniform sampler2D textureSampler;
72 |
73 | // Custom uniforms
74 | uniform sampler2D otherTexture;
75 | uniform vec3 colorOffset;
76 |
77 | const vec2 scale = vec2(0.25, 1.);
78 |
79 | void main(void)
80 | {
81 | gl_FragColor = texture2D(textureSampler, vUV);
82 |
83 | // Swizzle channels
84 | float r = gl_FragColor.r;
85 | gl_FragColor.r = gl_FragColor.b;
86 | gl_FragColor.b = r;
87 | gl_FragColor.rgb += clamp(colorOffset, 0., 1.);
88 |
89 | gl_FragColor.rgb *= texture2D(otherTexture, vUV * scale).rgb;
90 | }
91 | `,
92 | // Defines the list of existing samplers (default + customs).
93 | samplerNames: ["textureSampler", "otherTexture"],
94 | // Defines the list of existing uniform to be bound.
95 | uniformNames: ["colorOffset"],
96 | });
97 |
98 | // Creates the required input for the effect.
99 | const mainTexture = new Texture("../assets/logo.png", engine);
100 | const otherTexture = new Texture("../assets/timeline.png", engine);
101 | let time = 0;
102 |
103 | // Rely on the underlying engine render loop to update the filter result every frame.
104 | engine.runRenderLoop(() => {
105 | // Only render if the custom texture is ready (the default one is
106 | // checked for you by the render function)
107 | if (!otherTexture.isReady()) {
108 | return;
109 | }
110 |
111 | // Sets the custom values.
112 | time += engine.getDeltaTime() / 1000;
113 | customEffectWrapper.effect.setTexture("otherTexture", otherTexture);
114 | customEffectWrapper.effect.setFloat3("colorOffset", Math.cos(time) * 0.5 + 0.5, 0, Math.sin(time) * 0.5 + 0.5);
115 |
116 | // Render. Please note we are using render instead of filter to improve
117 | // performances of real time filter. filter is creating a promise and will therefore
118 | // generate some lags and garbage.
119 | customFilter.render(mainTexture, customEffectWrapper);
120 | });
121 |
122 | }
123 |
124 | function main() {
125 | beforePicture1.src = imageToProcess;
126 | beforePicture3.src = imageToProcess;
127 |
128 | oneTimeFilterWithPostProcess();
129 | oneTimeFilterFromCanvas();
130 | realTimeRenderAndCustomShader();
131 | }
132 |
133 | main();
--------------------------------------------------------------------------------
/src/resizer/resizer.ts:
--------------------------------------------------------------------------------
1 | import { ThinEngine } from "@babylonjs/core/Engines/thinEngine";
2 | import { EffectWrapper, EffectRenderer } from "@babylonjs/core/Materials/effectRenderer";
3 | import { Constants } from "@babylonjs/core/Engines/constants";
4 | import { ThinTexture } from "@babylonjs/core/Materials/Textures/thinTexture";
5 | import { ThinRenderTargetTexture } from "@babylonjs/core/Materials/Textures/thinRenderTargetTexture";
6 |
7 | import { ShaderConfiguration } from "./shader";
8 |
9 | import { BaseControl } from "../coreControls/baseControl";
10 | import { elementToTexture } from "../coreControls/elementToTexture";
11 |
12 | import "@babylonjs/core/Engines/Extensions/engine.renderTarget";
13 |
14 | /**
15 | * Represents a resizer control leveraging WebGL to speed up resizing images.
16 | *
17 | * One of the biggest advantage is that the output can directly be used as a
18 | * Babylon.js Texture so that if you need to resize thumbnails,
19 | * they do not need any extra copies a canvas2D would have.
20 | */
21 | export class Resizer extends BaseControl {
22 | private readonly _generateMipMaps: boolean;
23 | private readonly _textureFiltering: number;
24 | private _effectRenderer: EffectRenderer;
25 | private _effectWrapper: EffectWrapper;
26 |
27 | /**
28 | * Instantiates a resizer object able to efficiently resize a picture on the GPU.
29 | * @param parent defines the parent of the control. It could be either:
30 | * - A canvas element: the canvas we want to render the control in.
31 | * - An engine instance: the Babylon.js engine to use to render the control.
32 | * - Another Babylon.js control: this allows sharing the engine cross controls to mix and match them for instance.
33 | */
34 | constructor(parent: BaseControl | ThinEngine | HTMLCanvasElement) {
35 | super(parent);
36 |
37 | this._generateMipMaps = this.engine.webGLVersion > 1;
38 | this._textureFiltering = this._generateMipMaps ? Constants.TEXTURE_TRILINEAR_SAMPLINGMODE : Constants.TEXTURE_BILINEAR_SAMPLINGMODE;
39 |
40 | // Initializes the resizer control.
41 | this._initializeRenderer();
42 | }
43 |
44 | /**
45 | * Dispose all the associated resources with WebGL.
46 | */
47 | public dispose(): void {
48 | // Clear the renderer resources.
49 | this._effectWrapper.dispose();
50 | this._effectRenderer.dispose();
51 |
52 | super.dispose();
53 | }
54 |
55 | /**
56 | * This will resize the texture to fit in the canvas size.
57 | * @param input defines the picture input we want to resize. It can be the url of a texture, another canvas or a video element.
58 | * @returns a promise to know when the rendering is done.
59 | */
60 | public resize(textureData: ThinTexture | HTMLCanvasElement | HTMLVideoElement | string): Promise {
61 | // Converts the texture data to an actual babylon.js texture.
62 | const inputTexture = elementToTexture(this.engine, textureData, "input", this._generateMipMaps, this._textureFiltering, false);
63 |
64 | // Wraps the result in a promise to simplify usage.
65 | return new Promise((success, _) => {
66 | const checkIsReady = (() => {
67 | if (inputTexture.isReady()) {
68 | // Stops the check
69 | this.engine.stopRenderLoop(checkIsReady);
70 |
71 | // Once the input is ready, Render the texture as a full target quad.
72 | this._render(inputTexture);
73 |
74 | // Only dispose if needed
75 | if (!(textureData instanceof ThinTexture)) {
76 | // Free up memory resources from the input.
77 | inputTexture.dispose();
78 | }
79 |
80 | // Notify the promise of the overall completion.
81 | success();
82 | }
83 | }).bind(this);
84 |
85 | this.engine.runRenderLoop(checkIsReady);
86 | });
87 | }
88 |
89 | /**
90 | * Creates an offscreen texture if the chosen size to render to.
91 | * @param size defines the The chosen size of the texture on GPU.
92 | * @returns The Babylon texture to be used in other controls for instance. Be carefull, the texture might not be ready
93 | * as soon as you get it.
94 | */
95 | public createOffscreenTexture(size: { width: number, height: number }, samplingMode = Constants.TEXTURE_BILINEAR_SAMPLINGMODE): ThinRenderTargetTexture {
96 | // Creates an offscreen texture to render to.
97 | const outputTexture = new ThinRenderTargetTexture(this.engine, size, {
98 | format: Constants.TEXTUREFORMAT_RGBA,
99 | generateDepthBuffer: false,
100 | generateMipMaps: false,
101 | generateStencilBuffer: false,
102 | samplingMode,
103 | type: Constants.TEXTURETYPE_UNSIGNED_BYTE
104 | });
105 | outputTexture._texture.isReady = false;
106 | outputTexture.wrapU = Constants.TEXTURE_CLAMP_ADDRESSMODE;
107 | outputTexture.wrapV = Constants.TEXTURE_CLAMP_ADDRESSMODE;
108 |
109 | return outputTexture;
110 | }
111 |
112 | /**
113 | * Resizes an input babylon texture into a texture created with the createOffscreenTexture function.
114 | * This is helpfull in realtime use cases. The content of the outputTexture will be updated.
115 | * @param inputTexture defines the Base texture to resize.
116 | * @param outputTexture defines the Babylon texture to resize into.
117 | */
118 | public resizeToTexture(inputTexture: ThinTexture, outputTexture: ThinRenderTargetTexture): void {
119 | // Sets the output texture.
120 | this.engine.bindFramebuffer(outputTexture.renderTarget);
121 |
122 | // Sets the viewport to the render target texture size.
123 | this._effectRenderer.setViewport();
124 |
125 | // Render the texture as a full target quad.
126 | this._render(inputTexture);
127 |
128 | // Unsets the output texture.
129 | this.engine.unBindFramebuffer(outputTexture.renderTarget);
130 |
131 | // Resets the viewport to the canvas size.
132 | this._effectRenderer.setViewport();
133 |
134 | // Notify that the texture is ready for consumption.
135 | outputTexture._texture.isReady = true;
136 | }
137 |
138 | /**
139 | * This will return a Babylon texture resized to a chosen size.
140 | * @param textureData defines the picture input we want to resize. It can be the url of a texture, another canvas or a video element.
141 | * @param size defines the The chosen size of the texture on GPU.
142 | * @returns The Babylon texture to be used in other controls for instance. Be carefull, the texture might not be ready
143 | * as soon as you get it.
144 | */
145 | public getResizedTexture(textureData: ThinTexture | HTMLCanvasElement | HTMLVideoElement | string, size: { width: number, height: number }): ThinTexture {
146 | // Converts the texture data to an actual babylon.js texture.
147 | const inputTexture = elementToTexture(this.engine, textureData, "input", this._generateMipMaps, this._textureFiltering, false);
148 |
149 | // Creates an offscreen texture to render to.
150 | const outputTexture = this.createOffscreenTexture(size);
151 |
152 | // Simple render function using the effect wrapper as a simple pass through of
153 | // the input texture. The main difference with the previous function is that it renders
154 | // to an offscreen texture.
155 | const render = () => {
156 | // Renders to the texture.
157 | this.resizeToTexture(inputTexture, outputTexture);
158 |
159 | // Free up input and output resources.
160 | outputTexture.dispose(true);
161 | inputTexture.dispose();
162 | }
163 |
164 | const checkIsReady = (() => {
165 | if (inputTexture.isReady()) {
166 | this.engine.stopRenderLoop(checkIsReady);
167 | render();
168 | }
169 | }).bind(this);
170 |
171 | this.engine.runRenderLoop(checkIsReady);
172 |
173 | return outputTexture;
174 | }
175 |
176 | private _render(inputTexture: ThinTexture): void {
177 | this._effectRenderer.applyEffectWrapper(this._effectWrapper);
178 | this._effectWrapper.effect.setTexture("toResize", inputTexture);
179 | this._effectRenderer.draw();
180 | }
181 |
182 | private _initializeRenderer(): void {
183 | // Use the smallest module to render a quad on screen (no need for a full scene)
184 | this._effectRenderer = new EffectRenderer(this.engine);
185 |
186 | // Wraps a shader in a structure known to the Effect Renderer.
187 | this._effectWrapper = new EffectWrapper({
188 | engine: this.engine,
189 | ...ShaderConfiguration
190 | });
191 |
192 | // Initializes the viewport to the full canvas size.
193 | this._effectRenderer.setViewport();
194 | }
195 | }
--------------------------------------------------------------------------------
/license.md:
--------------------------------------------------------------------------------
1 | ## Apache License 2.0 (Apache)
2 |
3 | Apache License
4 | Version 2.0, January 2004
5 | http://www.apache.org/licenses/
6 |
7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
8 |
9 | ### Definitions.
10 |
11 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
16 |
17 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
18 |
19 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
20 |
21 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
22 |
23 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
24 |
25 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
26 |
27 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
28 |
29 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
30 |
31 | ### Grant of Copyright License.
32 |
33 | Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
34 |
35 | ### Grant of Patent License.
36 |
37 | Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
38 |
39 | ### Redistribution.
40 |
41 | You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
42 |
43 | 1. You must give any other recipients of the Work or Derivative Works a copy of this License; and
44 |
45 | 2. You must cause any modified files to carry prominent notices stating that You changed the files; and
46 |
47 | 3. You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
48 |
49 | 4. If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
50 |
51 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
52 |
53 | ### Submission of Contributions.
54 |
55 | Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
56 |
57 | ### Trademarks.
58 |
59 | This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
60 |
61 | ### Disclaimer of Warranty.
62 |
63 | Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
64 |
65 | ### Limitation of Liability.
66 |
67 | In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
68 |
69 | ### Accepting Warranty or Additional Liability.
70 |
71 | While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
72 |
73 | ## External dependencies
74 | - jQuery PEP: https://github.com/jquery/PEP
75 |
--------------------------------------------------------------------------------
/src/imageFilter/imageFilter.ts:
--------------------------------------------------------------------------------
1 | import { ThinEngine } from "@babylonjs/core/Engines/thinEngine";
2 | import { EffectWrapper, EffectRenderer } from "@babylonjs/core/Materials/effectRenderer";
3 | import { ThinTexture } from "@babylonjs/core/Materials/Textures/thinTexture";
4 | import { ThinRenderTargetTexture } from "@babylonjs/core/Materials/Textures/thinRenderTargetTexture";
5 |
6 | import { Constants } from "@babylonjs/core/Engines/constants";
7 | import { PostProcess } from "@babylonjs/core/PostProcesses/postProcess";
8 | import { Effect } from "@babylonjs/core/Materials/effect";
9 | import { Logger } from "@babylonjs/core/Misc/logger";
10 |
11 | import { BaseControl } from "../coreControls/baseControl";
12 | import { elementToTexture } from "../coreControls/elementToTexture";
13 |
14 | import "@babylonjs/core/Engines/Extensions/engine.renderTarget";
15 |
16 | /**
17 | * Defines a set of options provided to the image filter control.
18 | */
19 | export interface IImageFilterOptions {
20 | /**
21 | * Defines whether MipMaps are necessary for the filtering.
22 | */
23 | generateMipMaps?: boolean;
24 |
25 | /**
26 | * Defines whether the input image should be filtered linearly.
27 | */
28 | linearFiltering?: boolean;
29 | }
30 |
31 | /**
32 | * The image filter control can help applying effect through webGL shaders to a picture.
33 | * This can be the most efficient way to process images on the web.
34 | * Despite a 2d context being fast, applying processing in parallel on the GPU
35 | * is order of magnitudes faster than CPU (for a wide variety of effects).
36 | */
37 | export class ImageFilter extends BaseControl {
38 | private readonly _options: IImageFilterOptions;
39 | private readonly _generateMipMaps: boolean;
40 | private readonly _textureFiltering: number;
41 |
42 | private _effectRenderer: EffectRenderer;
43 |
44 | /**
45 | * Instantiates an image filter object able to efficiently apply effects to images.
46 | * @param parent defines the parent of the control. It could be either:
47 | * - A canvas element: the canvas we want to render the control in.
48 | * - An engine instance: the Babylon.js engine to use to render the control.
49 | * - Another Babylon.js control: this allows sharing the engine cross controls to mix and match them for instance.
50 | * @param options defines the set of options used by the control.
51 | */
52 | constructor(parent: BaseControl | ThinEngine | HTMLCanvasElement, options?: IImageFilterOptions) {
53 | super(parent);
54 |
55 | // Default options for the filter.
56 | this._options = options || { };
57 | if (this._options.generateMipMaps === undefined) {
58 | this._options.generateMipMaps = true;
59 | }
60 | if (this._options.linearFiltering === undefined) {
61 | this._options.linearFiltering = true;
62 | }
63 |
64 | // Initialiazes the filtering setup in ctor to allow the use of readonly variables.
65 | this._generateMipMaps = this._options.generateMipMaps && this.engine.webGLVersion > 1;
66 | if (this._options.linearFiltering) {
67 | this._textureFiltering = this._generateMipMaps ? Constants.TEXTURE_TRILINEAR_SAMPLINGMODE : Constants.TEXTURE_BILINEAR_SAMPLINGMODE;
68 | }
69 | else {
70 | this._textureFiltering = this._generateMipMaps ? Constants.TEXTURE_NEAREST_NEAREST_MIPLINEAR : Constants.TEXTURE_NEAREST_NEAREST;
71 | }
72 |
73 | // Initializes the control.
74 | this._initializeRenderer();
75 | }
76 |
77 | /**
78 | * This will filter the input and directly displays the result in the output.
79 | * @param input defines the picture input we want to filter. It can be the url of a texture, another canvas or a video element.
80 | * @param filter defines the effect to use to filter the image.
81 | * @returns a promise to know when the rendering is done.
82 | */
83 | public filter(textureData: ThinTexture | HTMLCanvasElement | HTMLVideoElement | string, filter: PostProcess | EffectWrapper): Promise {
84 | // Converts the texture data to an actual babylon.js texture.
85 | const inputTexture = elementToTexture(this.engine, textureData, "input", this._generateMipMaps, this._textureFiltering);
86 |
87 | // Wraps the result in a promise to simplify usage.
88 | return new Promise((success, _) => {
89 | const checkIsReady = (() => {
90 | if (inputTexture.isReady()) {
91 | // Stops the check
92 | this.engine.stopRenderLoop(checkIsReady);
93 |
94 | // Once the input is ready, Render the texture as a full target quad.
95 | this.render(inputTexture, filter);
96 |
97 | // Free up memory resources from the input.
98 | inputTexture.dispose();
99 |
100 | // Notify the promise of the overall completion.
101 | success();
102 | }
103 | }).bind(this);
104 |
105 | this.engine.runRenderLoop(checkIsReady);
106 | });
107 | }
108 |
109 | /**
110 | * This will return a filtered Babylon texture.
111 | * @param textureData defines the picture input we want to filter. It can be the url of a texture, another canvas or a video element.
112 | * @param size defines the The chosen size of the texture on GPU.
113 | * @param filter defines the effect to use to filter the image.
114 | * @returns The Babylon texture to be used in other controls for instance. Be carefull, the texture might not be ready
115 | * as soon as you get it.
116 | */
117 | public getFilteredTexture(textureData: ThinTexture | HTMLCanvasElement | HTMLVideoElement | string, size: { width: number, height: number }, filter: PostProcess | EffectWrapper): ThinTexture {
118 | // Converts the texture data to an actual babylon.js texture.
119 | const inputTexture = elementToTexture(this.engine, textureData, "input", this._generateMipMaps, this._textureFiltering);
120 |
121 | // Creates an offscreen texture to render to.
122 | const outputTexture = new ThinRenderTargetTexture(this.engine, size, {
123 | format: Constants.TEXTUREFORMAT_RGBA,
124 | generateDepthBuffer: false,
125 | generateMipMaps: false,
126 | generateStencilBuffer: false,
127 | samplingMode: Constants.TEXTURE_BILINEAR_SAMPLINGMODE,
128 | type: Constants.TEXTURETYPE_UNSIGNED_BYTE
129 | });
130 |
131 | // Ensure it is not ready so far.
132 | outputTexture._texture.isReady = false;
133 |
134 | // Simple render function using the effect wrapper as a simple pass through of
135 | // the input texture. The main difference with the previous function is that it renders
136 | // to an offscreen texture.
137 | const render = () => {
138 | // Sets the output texture.
139 | this.engine.bindFramebuffer(outputTexture.renderTarget);
140 |
141 | // Sets the viewport to the render target texture size.
142 | this._effectRenderer.setViewport();
143 |
144 | // Render the texture as a full target quad.
145 | this.render(inputTexture, filter);
146 |
147 | // Unsets the output texture.
148 | this.engine.unBindFramebuffer(outputTexture.renderTarget);
149 |
150 | // Resets the viewport to the canvas size.
151 | this._effectRenderer.setViewport();
152 |
153 | // Notify that the texture is ready for consumption.
154 | outputTexture._texture.isReady = true;
155 |
156 | // Free up input and output resources.
157 | outputTexture.dispose(true);
158 | inputTexture.dispose();
159 | }
160 |
161 | // Defers until the input texture is ready.
162 | const checkIsReady = (() => {
163 | if (inputTexture.isReady()) {
164 | this.engine.stopRenderLoop(checkIsReady);
165 | render();
166 | }
167 | }).bind(this);
168 |
169 | this.engine.runRenderLoop(checkIsReady);
170 |
171 | return outputTexture;
172 | }
173 |
174 | /**
175 | * This renders the effects using the current input babylon texture. This method
176 | * is better to use in realtime rendering of an effect as it does not generate any
177 | * promise or extra lamdas.
178 | * @param inputTexture defines the babylon texture to use as an input.
179 | * @param filter defines the effect to use to filter the image.
180 | */
181 | public render(inputTexture: ThinTexture, filter: PostProcess | EffectWrapper): void {
182 | if (!filter) {
183 | Logger.Error("Please, specify at least a post process or an effectWrapper in the options.");
184 | return;
185 | }
186 |
187 | if (!inputTexture.isReady()) {
188 | return;
189 | }
190 |
191 | let effect: Effect;
192 | if (filter instanceof EffectWrapper) {
193 | effect = filter.effect;
194 | this._effectRenderer.applyEffectWrapper(filter);
195 | }
196 | else {
197 | effect = filter.getEffect();
198 | this._effectRenderer.bindBuffers(effect)
199 | filter.apply();
200 | }
201 |
202 | effect.setTexture("textureSampler", inputTexture);
203 |
204 | this._effectRenderer.draw();
205 | }
206 |
207 | /**
208 | * Resizes the filter to adapt to the new canvas size.
209 | * The canvas has to be resized before hand.
210 | * Be carefull, the current time and visible duration might be impacted to ensure it always starts
211 | * at the beginning of the displayed thumbnails list.
212 | */
213 | public resize(): void {
214 | // Updates engine sizes.
215 | this.engine.resize();
216 | // Resets the viewport to the new canvas size.
217 | this._effectRenderer.setViewport();
218 | }
219 |
220 | /**
221 | * Dispose all the associated resources with WebGL.
222 | */
223 | public dispose(): void {
224 | // Clear the renderer resources.
225 | this._effectRenderer.dispose();
226 |
227 | super.dispose();
228 | }
229 |
230 | private _initializeRenderer(): void {
231 | // Use the smallest module to render a quad on screen (no need for a full scene)
232 | this._effectRenderer = new EffectRenderer(this.engine);
233 |
234 | // Initializes the viewport to the full canvas size.
235 | this._effectRenderer.setViewport();
236 | }
237 | }
238 |
--------------------------------------------------------------------------------
/src/timeline/timeline.ts:
--------------------------------------------------------------------------------
1 | import { ThinEngine } from "@babylonjs/core/Engines/thinEngine";
2 | import { EffectWrapper, EffectRenderer } from "@babylonjs/core/Materials/effectRenderer";
3 | import { ThinTexture } from "@babylonjs/core/Materials/Textures/thinTexture";
4 | import { Constants } from "@babylonjs/core/Engines/constants";
5 | import { Logger } from "@babylonjs/core/Misc/logger";
6 | import { Scalar } from "@babylonjs/core/Maths/math.scalar";
7 |
8 | import { ShaderConfiguration } from "./shader";
9 |
10 | import { BaseControl } from "../coreControls/baseControl";
11 | import { elementToTexture } from "../coreControls/elementToTexture";
12 |
13 | /**
14 | * Defines a set of options provided to the timeline.
15 | */
16 | export interface ITimelineOptions {
17 | /**
18 | * Defines the total length of the video. This helps computing where we are in the video.
19 | */
20 | totalDuration: number;
21 | /**
22 | * The width of the thumbnails.
23 | */
24 | thumbnailWidth: number;
25 | /**
26 | * The height of the thumbnails.
27 | */
28 | thumbnailHeight: number;
29 | /**
30 | * Defines the URI of loding texture used to replace thumbnail during loading.
31 | */
32 | loadingTextureURI: string;
33 | /**
34 | * Callback to implement to provide back the required picture info.
35 | *
36 | * This will be regularly called for each needed thumbnail by specifying the time
37 | * of the required picture. It can return either a texture, a video, a canvas or a url.
38 | *
39 | * The return value is passed through the done function to allow async operations.
40 | */
41 | getThumbnailCallback: (time: number, done: (input: ThinTexture | HTMLCanvasElement | HTMLVideoElement | string) => void) => void;
42 | /**
43 | * Defines whether the closest existing/loaded thumbnail should be use in place of the loading texture.
44 | * True by default.
45 | */
46 | useClosestThumbnailAsLoadingTexture?: boolean;
47 | }
48 |
49 | /**
50 | * Represents a timeline: a list of thumbnails for a video.
51 | * The thumbnails are evenly distributed along the visible duration from the video
52 | * The smallest granularity is the second to ensure that 128 width thumbnails would fit
53 | * in memory for a 1 hour long video.
54 | *
55 | * Thumbnail generation is out of scope of the control. They are the responsibility of the client code
56 | * which can dynamically generate or pre generate them on a server.
57 | */
58 | export class Timeline extends BaseControl {
59 | private readonly _options: ITimelineOptions;
60 |
61 | private _effectRenderer: EffectRenderer;
62 | private _effectWrapper: EffectWrapper;
63 | private _loadingThumbnail: ThinTexture;
64 | private _thumbnails: { [timespan: number]: ThinTexture };
65 | private _thumbnailsLoading: { [timespan: number]: boolean };
66 |
67 | private _totalThumbnails: number;
68 | private _visibleThumbnails: number;
69 |
70 | private _totalDuration: number;
71 | private _visibleDuration: number;
72 | private _currentTime: number;
73 | private _intervalDuration: number;
74 |
75 | private _widthScale: number;
76 | private _heightScale: number;
77 | private _heightOffset: number;
78 |
79 | private _shouldRender: boolean;
80 | private _renderFunction: () => void;
81 |
82 | /**
83 | * Gets the total duration of the video the canvas has been configured to
84 | * represent.
85 | */
86 | public get totalDuration(): number {
87 | return this._totalDuration;
88 | }
89 |
90 | /**
91 | * Gets the visible duration the timeline canvas can display without scrolling.
92 | * It depends on the total number of thumbnails configured.
93 | */
94 | public get visibleDuration(): number {
95 | return this._visibleDuration;
96 | }
97 |
98 | /**
99 | * Gets the max value that can be set as currentTime.
100 | */
101 | public get maxSettableTime(): number {
102 | return Math.max(this._totalDuration - this._visibleDuration, 0);
103 | }
104 |
105 | /**
106 | * Gets the current start time of the visible part of the timeline.
107 | */
108 | public get currentTime(): number {
109 | return this._currentTime;
110 | }
111 |
112 | /**
113 | * Gets the current end time of the visible part of the timeline.
114 | */
115 | public get endVisibleTime(): number {
116 | return this._currentTime + this._visibleDuration;
117 | }
118 |
119 | /**
120 | * Gets the current duration of the interval between two consecutive thumbnails.
121 | */
122 | public get intervalDuration(): number {
123 | return this._intervalDuration;
124 | }
125 |
126 | /**
127 | * Gets the total number of thumbnails the timeline has been set to display.
128 | * It depends mainly of the zoom level and the size of the canvas + desired thumbnail one.
129 | */
130 | public get totalThumbnails(): number {
131 | return this._totalThumbnails;
132 | }
133 |
134 | /**
135 | * Gets the number of thumbnails visible in the canvas without scrolling.
136 | * This is the ideal number when the start time is exactly equivalent to the
137 | * start of a thumbnail.
138 | */
139 | public get visibleThumbnails(): number {
140 | return this._visibleThumbnails;
141 | }
142 |
143 | /**
144 | * Instantiates a timeline object able to display efficiently a video timeline.
145 | * @param parent defines the parent of the control. It could be either:
146 | * - A canvas element: the canvas we want to render the control in.
147 | * - An engine instance: the Babylon.js engine to use to render the control.
148 | * - Another Babylon.js control: this allows sharing the engine cross controls to mix and match them for instance.
149 | * @param options defines the set of options used by the timeline control.
150 | */
151 | constructor(parent: BaseControl | ThinEngine | HTMLCanvasElement, options: ITimelineOptions) {
152 | super(parent);
153 |
154 | // Default options for the timeline.
155 | if (options.useClosestThumbnailAsLoadingTexture === undefined) {
156 | options.useClosestThumbnailAsLoadingTexture = true;
157 | }
158 | this._options = options;
159 |
160 | // Initializes all our
161 | this._initializeDurations();
162 | this._initializeTextures();
163 | this._initializeRenderer();
164 | }
165 |
166 | /**
167 | * Starts rendering the timeline in the canvas.
168 | * @param callback defines an optional callback that would be run during the RAF.
169 | */
170 | public runRenderLoop(callback?: () => void): void {
171 | this._shouldRender = true;
172 | // Keep track of the render function to isolate it from potentially other controls
173 | // Render loops. It helps being able to stop only one of them.
174 | this._renderFunction = () => {
175 | this.render(callback);
176 | };
177 |
178 | this.engine.runRenderLoop(this._renderFunction);
179 | }
180 |
181 | /**
182 | * Stops rendering the timeline in the canvas.
183 | */
184 | public stopRenderLoop(): void {
185 | this.engine.stopRenderLoop(this._renderFunction);
186 | }
187 |
188 | /**
189 | * Caches one thumbnail for a given time. This can be used to preload thumbnails if needed.
190 | * @param textureData defines the texture data as a texture, a video, a canvas or a url.
191 | * @param time defines the time the thumbnail should be used at.
192 | * @returns the thumbnail texture.
193 | */
194 | public addThumbnail(textureData: ThinTexture | HTMLCanvasElement | HTMLVideoElement | string, time: number): ThinTexture {
195 | // Converts the texture data to an actual babylon.js texture.
196 | let thumbnail = elementToTexture(this.engine, textureData, "" + time);
197 |
198 | // Store in cache.
199 | this._thumbnails[time] = thumbnail;
200 |
201 | return thumbnail;
202 | }
203 |
204 | /**
205 | * Renders the current state of the timeline to the canvas.
206 | * @param callback defines an optional callback that would be run during the RAF.
207 | */
208 | public render(callback?: () => void): void {
209 | callback && callback();
210 |
211 | // Prevents useless use of GPU improving efficiency.
212 | if (!this._shouldRender) {
213 | return;
214 | }
215 |
216 | // Only renders once the loading texture is ready.
217 | if (!this._loadingThumbnail.isReady()) {
218 | return;
219 | }
220 |
221 | // And the shader has been compiled.
222 | if (!this._effectWrapper.effect.isReady()) {
223 | return;
224 | }
225 |
226 | // Prevents rendering again if nothing happens.
227 | this._shouldRender = false;
228 |
229 | // Set the current shader for rendering.
230 | this._effectRenderer.applyEffectWrapper(this._effectWrapper);
231 |
232 | // Computes which thumbnail should be drawn first on screen.
233 | const thumbnailIndex = this._currentTime / this._intervalDuration;
234 | const startTime = Math.floor(thumbnailIndex) * this._intervalDuration;
235 |
236 | // Computes a filler offsets for the width to ensure the timeline is centered.
237 | let filler = 0;
238 | if (this._totalDuration < this._visibleThumbnails) {
239 | const offset = this._visibleThumbnails - this._totalDuration;
240 | filler = offset / 2;
241 | }
242 |
243 | // Renders all the visible thumbnails in the timeline.
244 | for (let i = 0; i < this._visibleThumbnails + 1; i++) {
245 | const time = startTime + this._intervalDuration * i;
246 | if (time >= this._totalDuration) {
247 | break;
248 | }
249 |
250 | // Set the texture corresponding to the current time.
251 | const texture = this._getTexture(time);
252 | this._effectWrapper.effect.setTexture("thumbnail", texture);
253 |
254 | // Computes the horizontal offset of the thumbnail dynamically by respecting
255 | // The shader optim defined at the top of the file:
256 | // shaderOffset = offset * 2. - 1.;
257 | const widthOffset = ((time - this._currentTime) + filler) / this._visibleDuration * 2 - 1;
258 | this._effectWrapper.effect.setFloat2("offset", widthOffset, this._heightOffset);
259 | this._effectWrapper.effect.setFloat2("scale", this._widthScale, this._heightScale);
260 |
261 | // Draws the current thumbnail in the canvas as a quad.
262 | this._effectRenderer.draw();
263 | }
264 | }
265 |
266 | /**
267 | * Sets the current time to display the timeline from.
268 | * @param time defines the desired time to start from.
269 | * @returns the clamped current time computed to ensure it fits in the available time range.
270 | */
271 | public setCurrentTime(time: number): void {
272 | // We need to ensure the time respects some boundaries the start of the video
273 | // and the max settable time to not display empty space on the right.
274 | this._currentTime = Scalar.Clamp(time, 0, this.maxSettableTime);
275 | // Re render on next RAF.
276 | this._shouldRender = true;
277 | }
278 |
279 | /**
280 | * Sets the amount of thumbnails the timeline should contain. It is all of them including the invisible ones due to scrolling.
281 | * Be carefull, the current time might be impacted to ensure it always starts
282 | * at the beginning of the displayed thumbnails list.
283 | * @param totalThumbnails defines the desired number of thumbnails desired.
284 | * @returns the clamped total thumbnails computed to ensure it fits in the available time range.
285 | */
286 | public setTotalThumbnails(totalThumbnails: number): number {
287 | // We need a round number to not see half a thumbnail on the latest one.
288 | this._totalThumbnails = Math.floor(totalThumbnails);
289 | // We also need to ensure it respects some boundaries regarding the min number of thumbnail and the max (equal to the total duration).
290 | this._totalThumbnails = Scalar.Clamp(this._totalThumbnails, this._visibleThumbnails, this._totalDuration);
291 |
292 | // We can now compute back the interval of time between thumbnails and the total visible time
293 | // on screen without scrolling.
294 | this._intervalDuration = this._totalDuration / this._totalThumbnails;
295 | this._visibleDuration = this._intervalDuration * this._visibleThumbnails;
296 |
297 | // Ensures the current time is within the new defined boundaries.
298 | this.setCurrentTime(this._currentTime);
299 |
300 | return this._totalThumbnails;
301 | }
302 |
303 | /**
304 | * Sets the amount of time we should see in the timeline as a zoom level in percentage.
305 | * Be carefull, the current time might be impacted to ensure it always starts
306 | * at the beginning of the displayed thumbnails list.
307 | * @param percent defines the desired level of zoom 0% means the entire video is visible without scrolling and 100% the smallest granularity.
308 | * @returns the clamped total thumbnails computed to ensure it fits in the available time range.
309 | */
310 | public setVisibleDurationZoom(percent: number): number {
311 | // Interpolate the number of thumbnails between the min number and the max
312 | // based on the given percentage.
313 | let totalThumbnail = this._visibleThumbnails + (this._totalDuration - this._visibleThumbnails) * percent / 100;
314 | return this.setTotalThumbnails(totalThumbnail);
315 | }
316 |
317 | /**
318 | * Resizes the timeline to adapt to the new canvas size.
319 | * The canvas has to be resized before hand.
320 | * Be carefull, the current time and visible duration might be impacted to ensure it always starts
321 | * at the beginning of the displayed thumbnails list.
322 | */
323 | public resize(): void {
324 | // Updates engine sizes.
325 | this.engine.resize();
326 | // Resets the viewport to the new canvas size.
327 | this._effectRenderer.setViewport();
328 | // Initializes the rest of the durations impacted by the canvas size.
329 | this._initializeCanvasRelativeDurations();
330 | }
331 |
332 | /**
333 | * Dispose all the associated resources with WebGL.
334 | */
335 | public dispose(): void {
336 | // Clear Thumbnails.
337 | for (let thumbnailIndex in this._thumbnails) {
338 | if (this._thumbnails.hasOwnProperty(thumbnailIndex)) {
339 | this._thumbnails[thumbnailIndex].dispose();
340 | }
341 | }
342 |
343 | // Clear the renderer resources.
344 | this._loadingThumbnail.dispose();
345 | this._effectWrapper.dispose();
346 | this._effectRenderer.dispose();
347 | this._renderFunction = null;
348 |
349 | super.dispose();
350 | }
351 |
352 | private _initializeDurations(): void {
353 | // Start at 0.
354 | this._currentTime = 0;
355 |
356 | // Ensures the provided total duration is meaningful.
357 | this._totalDuration = Math.floor(Math.max(0, this._options.totalDuration));
358 | if (this._totalDuration === 0) {
359 | Logger.Error("The total duration can not be 0. Nothing would be displayed.");
360 | return;
361 | }
362 |
363 | // Initializes the rest of the durations.
364 | this._initializeCanvasRelativeDurations();
365 | }
366 |
367 | private _initializeCanvasRelativeDurations(): void {
368 | // Compute the max number of thumbnails we can see in the canvas without scrolling.
369 | // It needs to be an integer for "UX purpose".
370 | this._visibleThumbnails = Math.ceil(this.canvas.clientWidth / this._options.thumbnailWidth);
371 |
372 | // Compute the scale to apply in the shader for each quads to ensure the
373 | // number of thumbnails fit in the canvas.
374 | // Due to shader optim detailled around the vertex shader code,
375 | // shaderScale = scale * 2.;
376 | this._widthScale = 1 / this._visibleThumbnails * 2;
377 |
378 | // Compute the height scale to apply on a thumbnail in the shader
379 | // in order to respect the provided sizes.
380 | const ratio = this._options.thumbnailHeight / this._options.thumbnailWidth;
381 | const effectiveWidth = this.canvas.width / this._visibleThumbnails;
382 | const effectiveHeight = effectiveWidth * ratio;
383 | // Due to shader optim detailled around the vertex shader code,
384 | // shaderScale = scale * 2.;
385 | this._heightScale = effectiveHeight / this.canvas.height * 2;
386 |
387 | // Compute a small offset for the height to center the thumbnail in the canvas
388 | // vertically.
389 | // The computation should be: (canvasH - effectiveH) / canvasH / 2
390 | // But due to shader optim detailled around the vertex shader code,
391 | // shaderOffset = offset * 2. - 1.;
392 | // shaderOffset = (canvasH - effectiveH) / canvasH - 1
393 | this._heightOffset = (this.canvas.height - effectiveHeight) / this.canvas.height - 1;
394 |
395 | // Reinitializes the total number of thumbnails as it might be impacted
396 | // during a resize.
397 | this.setTotalThumbnails(this._totalThumbnails || this._visibleThumbnails);
398 | }
399 |
400 | private _initializeTextures(): void {
401 | // Prepares the loading thumbnail.
402 | const internalTexture = this.engine.createTexture(this._options.loadingTextureURI, true, true, null, Constants.TEXTURE_BILINEAR_SAMPLINGMODE);
403 | this._loadingThumbnail = new ThinTexture(internalTexture);
404 | // And the thumbnails cache.
405 | this._thumbnails = { };
406 | this._thumbnailsLoading = { };
407 | }
408 |
409 | private _initializeRenderer(): void {
410 | // Use the smallest module to render a quad on screen (no need for a full scene)
411 | this._effectRenderer = new EffectRenderer(this.engine, {
412 | positions: [1, 1, 0, 1, 0, 0, 1, 0],
413 | indices: [0, 1, 2, 0, 2, 3]
414 | });
415 |
416 | // Wraps a shader in a structure known to the Effect Renderer.
417 | this._effectWrapper = new EffectWrapper({
418 | engine: this.engine,
419 | ...ShaderConfiguration
420 | });
421 |
422 | // Initializes the viewport to the full canvas size.
423 | this._effectRenderer.setViewport();
424 | }
425 |
426 | private _getTexture(time: number): ThinTexture {
427 | // Only gets rounded time close to the granularity.
428 | time = Math.floor(time);
429 |
430 | // Try grabbing the thumbnail from the cache.
431 | let thumbnail = this._thumbnails[time];
432 | // If not creates it from the given callback.
433 | if (!thumbnail && !this._thumbnailsLoading[time]) {
434 | // Flag the thubmnail as currently loading.
435 | this._thumbnailsLoading[time] = true;
436 |
437 | this._options.getThumbnailCallback(time, (textureData) => {
438 | this.addThumbnail(textureData, time);
439 | });
440 | }
441 |
442 | // Returns the thumbnail texture only if ready.
443 | if (thumbnail && thumbnail.isReady()) {
444 | return thumbnail;
445 | }
446 |
447 | // Else return the loading picture to not block the UI.
448 | // Render till ready to replace the loading textures by the loaded ones.
449 | this._shouldRender = true;
450 |
451 | // Returns the loading thumbnail.
452 | return this._getLoadingThumbnail(time);
453 | }
454 |
455 | private _getLoadingThumbnail(time: number): ThinTexture {
456 | // Returns loading thumbnail if closest match has been disabled.
457 | if (!this._options.useClosestThumbnailAsLoadingTexture) {
458 | return this._loadingThumbnail;
459 | }
460 |
461 | // Find the closest available and ready thumbnail.
462 | const maximumDistance = Math.max(this._totalDuration - time, time);
463 | for (let i = 1; i <= maximumDistance; i++) {
464 | const before = time - i;
465 | if (before > 0) {
466 | const thumbnail = this._thumbnails[before];
467 | if (thumbnail && thumbnail.isReady()) {
468 | return thumbnail;
469 | }
470 | }
471 |
472 | const after = time + i;
473 | if (after < this.totalDuration) {
474 | const thumbnail = this._thumbnails[after];
475 | if (thumbnail && thumbnail.isReady()) {
476 | return thumbnail;
477 | }
478 | }
479 | }
480 |
481 | // No closest match available:
482 | return this._loadingThumbnail;
483 | }
484 | }
--------------------------------------------------------------------------------