├── src ├── index.ts ├── resizer │ ├── index.ts │ ├── shader.ts │ └── resizer.ts ├── timeline │ ├── index.ts │ ├── shader.ts │ └── timeline.ts ├── imageFilter │ ├── index.ts │ └── imageFilter.ts └── coreControls │ ├── baseControl.ts │ └── elementToTexture.ts ├── www ├── assets │ ├── logo.png │ ├── favicon.ico │ ├── resizer.png │ ├── timeline.png │ ├── imageFilter.png │ ├── common.css │ └── logo.svg ├── timeline │ ├── assets │ │ ├── test.mp4 │ │ └── loading.png │ ├── index.ts │ └── index.html ├── resizer │ ├── index.ts │ ├── amp.html │ ├── index.html │ └── amp.ts ├── index.html └── imageFilter │ ├── index.html │ └── index.ts ├── dev.tsconfig.json ├── tsconfig.json ├── .gitignore ├── azure-pipelines-cd.yml ├── what's new.md ├── .vscode ├── settings.json ├── launch.json └── tasks.json ├── tools ├── versionUp.js └── checkWhatsNew.js ├── azure-pipelines-ci.yml ├── README.md ├── webpack.config.js ├── package.json ├── contributing.md └── license.md /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./resizer"; 2 | export * from "./timeline"; -------------------------------------------------------------------------------- /www/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/Controls/HEAD/www/assets/logo.png -------------------------------------------------------------------------------- /src/resizer/index.ts: -------------------------------------------------------------------------------- 1 | import { Resizer } from "./resizer"; 2 | 3 | export { 4 | Resizer, 5 | }; -------------------------------------------------------------------------------- /www/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/Controls/HEAD/www/assets/favicon.ico -------------------------------------------------------------------------------- /www/assets/resizer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/Controls/HEAD/www/assets/resizer.png -------------------------------------------------------------------------------- /www/assets/timeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/Controls/HEAD/www/assets/timeline.png -------------------------------------------------------------------------------- /www/assets/imageFilter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/Controls/HEAD/www/assets/imageFilter.png -------------------------------------------------------------------------------- /www/timeline/assets/test.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/Controls/HEAD/www/timeline/assets/test.mp4 -------------------------------------------------------------------------------- /www/timeline/assets/loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/Controls/HEAD/www/timeline/assets/loading.png -------------------------------------------------------------------------------- /src/timeline/index.ts: -------------------------------------------------------------------------------- 1 | import { ITimelineOptions, Timeline } from "./timeline"; 2 | 3 | export { 4 | ITimelineOptions, 5 | Timeline, 6 | }; -------------------------------------------------------------------------------- /src/imageFilter/index.ts: -------------------------------------------------------------------------------- 1 | import { IImageFilterOptions, ImageFilter } from "./imageFilter"; 2 | 3 | export { 4 | IImageFilterOptions, 5 | ImageFilter, 6 | }; -------------------------------------------------------------------------------- /dev.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esNext", 4 | "target": "es5", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "types" : [], 8 | "lib": ["dom", "es2015"] 9 | } 10 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esNext", 4 | "target": "es2020", 5 | "lib": ["es5", "es6", "dom"], 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "importHelpers": true, 9 | "types" : [] 10 | }, 11 | "exclude": ["dist"] 12 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build Folders (you can keep bin if you'd like, to store dlls and pdbs) 2 | # And OS or Editor folders 3 | [Bb]in/ 4 | *.tmp 5 | *.log 6 | *.DS_Store 7 | ._* 8 | Thumbs.db 9 | .cache 10 | .tmproj 11 | nbproject 12 | *.sublime-project 13 | *.sublime-workspace 14 | .idea 15 | .directory 16 | build 17 | .history 18 | .tempChromeProfileForDebug 19 | 20 | # Node Modules 21 | node_modules/* 22 | 23 | # Local dist 24 | dist/* 25 | www/scripts/* -------------------------------------------------------------------------------- /azure-pipelines-cd.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - deploy 3 | 4 | # no PR builds 5 | pr: none 6 | 7 | jobs: 8 | - job: Deploy 9 | displayName: 'Build and Publish' 10 | pool: 11 | vmImage: 'Ubuntu-latest' 12 | demands: npm 13 | steps: 14 | - script: 'npm install' 15 | displayName: 'Npm install' 16 | - script: 'npm run build' 17 | displayName: 'Tsc Build' 18 | - script: 'npm run versionUp' 19 | displayName: 'Npm version up' 20 | - task: Npm@1 21 | displayName: 'Npm publish' 22 | inputs: 23 | command: 'custom' 24 | customCommand: 'publish --tag preview --access public' 25 | customEndpoint: 'NPMWithAccessToken' 26 | -------------------------------------------------------------------------------- /what's new.md: -------------------------------------------------------------------------------- 1 | # 2.0.0-alpha.1 2 | - Getting ready for Babylon 5.0 3 | 4 | # 0.1.0 5 | 6 | ## Major updates 7 | - First release of the [Controls Library](https://doc.babylonjs.com/features/controls) ([Deltakosh](https://github.com/deltakosh/)) 8 | 9 | ## Controls 10 | 11 | ### Timeline 12 | - Scrollable Timeline control for videos [Doc](https://doc.babylonjs.com/features/timeline) / [Sebavan](https://github.com/sebavan/)) 13 | 14 | ### Resizer 15 | - Resizer control for images (Can be use to manage inputs of the timeline) [Doc](https://doc.babylonjs.com/features/resizer) / [Sebavan](https://github.com/sebavan/)) 16 | 17 | ### ImageFilter 18 | - Image filter control for images [Doc](https://doc.babylonjs.com/features/imageFilter) / [Sebavan](https://github.com/sebavan/)) -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | // Configure glob patterns for excluding files and folders. 4 | "files.exclude": { 5 | "**/.git": true, 6 | "**/.svn": true, 7 | "**/.hg": true, 8 | "**/.DS_Store": true, 9 | "**/.vs": true, 10 | "**/.tempChromeProfileForDebug": true, 11 | "**/node_modules": true, 12 | "**/.temp": true 13 | }, 14 | "search.exclude": { 15 | "**/.tempChromeProfileForDebug": true, 16 | "**/node_modules": true, 17 | "**/.temp": true, 18 | "**/.dist": true, 19 | "**/*.map": true 20 | }, 21 | "editor.tabSize": 4, 22 | "typescript.tsdk": "node_modules\\typescript\\lib" 23 | } -------------------------------------------------------------------------------- /tools/versionUp.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const { exec } = require('child_process'); 3 | 4 | exec("npm view @babylonjs/controls dist-tags.preview", (err, stdout, stderr) => { 5 | if (err) { 6 | console.error(err); 7 | throw err; 8 | } 9 | 10 | console.log("Current NPM Registry Version:", stdout); 11 | 12 | const version = stdout; 13 | const spl = version.split("."); 14 | spl[spl.length - 1]++; 15 | const newVersion = spl.join("."); 16 | 17 | console.log("New Requested Version:", newVersion); 18 | 19 | const packageText = fs.readFileSync("package.json"); 20 | 21 | const packageJSON = JSON.parse(packageText); 22 | packageJSON.version = newVersion; 23 | 24 | fs.writeFileSync("package.json", JSON.stringify(packageJSON, null, 4)); 25 | }); 26 | -------------------------------------------------------------------------------- /src/resizer/shader.ts: -------------------------------------------------------------------------------- 1 | 2 | // We are rendering only one picture fullscreen or on the full offscreen texture. 3 | 4 | const vertexShader = ` 5 | attribute vec2 position; 6 | 7 | varying vec2 uv; 8 | 9 | const vec2 scale = vec2(0.5, -0.5); 10 | 11 | void main(void) { 12 | uv = position * scale + 0.5; 13 | 14 | gl_Position = vec4(position, 0.0, 1.0); 15 | } 16 | `; 17 | 18 | const fragmentShader = ` 19 | varying vec2 uv; 20 | 21 | uniform sampler2D toResize; 22 | 23 | void main(void) { 24 | gl_FragColor = texture2D(toResize, uv); 25 | } 26 | `; 27 | 28 | export const ShaderConfiguration = { 29 | name: "resizer", 30 | vertexShader: vertexShader, 31 | fragmentShader, 32 | attributeNames: ["position"], 33 | samplerNames: ["toResize"], 34 | } -------------------------------------------------------------------------------- /azure-pipelines-ci.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - master 3 | 4 | pr: 5 | autoCancel: true 6 | branches: 7 | include: 8 | - master 9 | 10 | jobs: 11 | - job: WhatsNewUpdate 12 | displayName: '1. What s New Update' 13 | pool: 14 | vmImage: 'Ubuntu-latest' 15 | demands: npm 16 | steps: 17 | - script: 'npm install' 18 | displayName: 'Npm install' 19 | - script: 'npm run whatsNew' 20 | displayName: 'Whats new' 21 | env: 22 | AZURE_PULLREQUESTID: $(System.PullRequest.PullRequestId) 23 | NPM_USERNAME: $(babylon.npm.username) 24 | 25 | - job: TscBuild 26 | displayName: '2. Tsc Build' 27 | pool: 28 | vmImage: 'Ubuntu-latest' 29 | demands: npm 30 | steps: 31 | - script: 'npm install' 32 | displayName: 'Npm install' 33 | - script: 'npm run build' 34 | displayName: 'Tsc Build' 35 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch Index (Chrome)", 6 | "type": "chrome", 7 | "request": "launch", 8 | "url": "http://localhost:8080/test/dev/index.html", 9 | "webRoot": "${workspaceRoot}", 10 | "sourceMaps": true, 11 | "preLaunchTask": "devServer", 12 | "userDataDir": "${workspaceRoot}/.tempChromeProfileForDebug", 13 | "runtimeArgs": [ 14 | "--enable-unsafe-es3-apis" 15 | ] 16 | }, 17 | { 18 | "name": "Launch Timeline (Chrome)", 19 | "type": "chrome", 20 | "request": "launch", 21 | "url": "http://localhost:8080/test/dev/timeline/index.html", 22 | "webRoot": "${workspaceRoot}/", 23 | "sourceMaps": true, 24 | "preLaunchTask": "devServer", 25 | "userDataDir": "${workspaceRoot}/.tempChromeProfileForDebug", 26 | "runtimeArgs": [ 27 | "--enable-unsafe-es3-apis" 28 | ] 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /www/resizer/index.ts: -------------------------------------------------------------------------------- 1 | import { Resizer } from "../../src/resizer"; 2 | 3 | const beforePicture = document.getElementById("beforePicture") as HTMLImageElement; 4 | const afterPicture1 = document.getElementById("afterPicture1") as HTMLCanvasElement; 5 | const afterPicture2 = document.getElementById("afterPicture2") as HTMLCanvasElement; 6 | const afterPicture3 = document.getElementById("afterPicture3") as HTMLCanvasElement; 7 | const afterPicture4 = document.getElementById("afterPicture4") as HTMLCanvasElement; 8 | const startResizingButton = document.getElementById("startResizingButton"); 9 | 10 | const imageToResize = "../assets/logo.png"; 11 | 12 | function main() { 13 | beforePicture.src = imageToResize; 14 | 15 | const resizer1 = new Resizer(afterPicture1); 16 | const resizer2 = new Resizer(afterPicture2); 17 | const resizer3 = new Resizer(afterPicture3); 18 | const resizer4 = new Resizer(afterPicture4); 19 | 20 | startResizingButton.addEventListener("click", function() { 21 | resizer1.resize(imageToResize); 22 | resizer2.resize(imageToResize); 23 | resizer3.resize(imageToResize); 24 | resizer4.resize(imageToResize); 25 | }); 26 | } 27 | 28 | main(); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Babylon.js controls 2 | 3 | [![Build Status](https://dev.azure.com/babylonjs/ContinousIntegration/_apis/build/status/Controls%20CI?branchName=master)](https://dev.azure.com/babylonjs/ContinousIntegration/_build/latest?definitionId=4&branchName=master) 4 | [![Twitter](https://img.shields.io/twitter/follow/babylonjs.svg?style=social&label=Follow)](https://twitter.com/intent/follow?screen_name=babylonjs) 5 | 6 | **Any questions?** Here is our official [forum](https://forum.babylonjs.com/). 7 | 8 | You can find documentation for the controls [here](https://doc.babylonjs.com/features/featuresDeepDive/controls) 9 | 10 | ## Running locally 11 | 12 | After cloning the repo, running locally during development is all the simplest: 13 | 14 | ```bash 15 | npm install 16 | npm run start 17 | ``` 18 | 19 | For VSCode users, if you have installed the Chrome Debugging extension, you can start debugging within VSCode by using the appropriate launch menu. 20 | 21 | ## npm 22 | 23 | Babylon.js controls are published on npm with full typing support. To install, use: 24 | 25 | ```bash 26 | npm install @babylonjs/controls --save 27 | ``` 28 | 29 | ## Contributing 30 | 31 | Please see the [Contributing Guidelines](./contributing.md) 32 | -------------------------------------------------------------------------------- /tools/checkWhatsNew.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const https = require("https"); 3 | 4 | /** 5 | * Tests the whats new file to ensure changes have been made in the PR. 6 | */ 7 | 8 | // Check status on azure 9 | if (!process.env["AZURE_PULLREQUESTID"]) { 10 | console.log("Not a PR, no need to check.") 11 | return; 12 | } 13 | 14 | // Only on PR not once in. 15 | if (process.env.NPM_USERNAME !== "$(babylon.npm.username)") { 16 | console.log("On Safe Branch, no need to check.") 17 | return; 18 | }; 19 | 20 | // Compare what's new with the current one in the preview release folder. 21 | const url = "https://raw.githubusercontent.com/BabylonJS/Controls/master/what's%20new.md"; 22 | https.get(url, res => { 23 | res.setEncoding("utf8"); 24 | let oldData = ""; 25 | res.on("data", data => { 26 | oldData += data; 27 | }); 28 | res.on("end", () => { 29 | fs.readFile("./what's new.md", "utf-8", function(err, newData) { 30 | console.log(newData) 31 | if (err || oldData != newData) { 32 | return; 33 | } 34 | 35 | console.error("What's new file did not change."); 36 | process.exit(1); 37 | }); 38 | }); 39 | }); -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | var DIST_DIR = path.resolve(__dirname, "./www"); 4 | var DEV_DIR = path.resolve(__dirname, "./.temp"); 5 | 6 | var buildConfig = function(env) { 7 | const isProd = env.prod === true; 8 | return { 9 | context: __dirname, 10 | entry: { 11 | timeline: DIST_DIR + "/timeline/index.ts", 12 | resizer: DIST_DIR + "/resizer/index.ts", 13 | amp: DIST_DIR + "/resizer/amp.ts", 14 | imageFilter: DIST_DIR + "/imageFilter/index.ts", 15 | }, 16 | output: { 17 | path: (isProd ? DIST_DIR : DEV_DIR) + "/scripts/", 18 | filename: "[name].js", 19 | publicPath: '/scripts/', 20 | }, 21 | devtool: isProd ? false : 'source-map', 22 | devServer: { 23 | static: ['www'], 24 | }, 25 | resolve: { 26 | extensions: [".ts", ".js"] 27 | }, 28 | module: { 29 | rules: [{ 30 | test: /\.tsx?$/, 31 | loader: "ts-loader", 32 | options: { 33 | configFile: 'dev.tsconfig.json' 34 | } 35 | }, 36 | { 37 | test: /\.css$/, 38 | use: ["style-loader", "css-loader"], 39 | }] 40 | }, 41 | mode: isProd ? "production" : "development" 42 | }; 43 | } 44 | 45 | module.exports = buildConfig; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@babylonjs/controls", 3 | "version": "2.2.0", 4 | "description": "Babylon.js controls are a set of regular web controls that used hardware accelerated rendering through Babylon.js to provide blazing fast dedicated controls.", 5 | "author": { 6 | "name": "David CATUHE" 7 | }, 8 | "contributors": [ 9 | "Sebastien VANDENBERGHE" 10 | ], 11 | "keywords": [ 12 | "timeline", 13 | "controls", 14 | "acceleration", 15 | "3D", 16 | "2D", 17 | "javascript", 18 | "html5", 19 | "webgl", 20 | "webgl2", 21 | "webgpu" 22 | ], 23 | "license": "Apache2", 24 | "readme": "README.md", 25 | "typings": "dist/src/index.d.ts", 26 | "main": "dist/src/index.js", 27 | "files": [ 28 | "dist/src/**", 29 | "README.md" 30 | ], 31 | "scripts": { 32 | "build": "tsc --inlineSources --sourceMap true --declaration true --outDir ./dist", 33 | "versionUp": "node tools/versionUp.js", 34 | "preRelease": "node tools/preRelease.js", 35 | "start": "npx webpack-dev-server --open", 36 | "devServer": "npx webpack-dev-server", 37 | "whatsNew": "node tools/checkWhatsNew.js", 38 | "netlifyDist": "webpack --env=prod" 39 | }, 40 | "devDependencies": { 41 | "@babylonjs/core": "^7.11.2", 42 | "ts-loader": "^9.2.6", 43 | "typescript": "^5.4.5", 44 | "webpack": "^5.58.1", 45 | "webpack-cli": "^4.9.0", 46 | "webpack-dev-server": "^5.2.1" 47 | }, 48 | "dependencies": { 49 | "tslib": "^2.3.1" 50 | }, 51 | "peerDependencies": { 52 | "@babylonjs/core": ">=7.11.2" 53 | } 54 | } -------------------------------------------------------------------------------- /src/timeline/shader.ts: -------------------------------------------------------------------------------- 1 | 2 | // We are rendering a serie of Quad (one per thumbnail) already in UV space to simplify the shader. 3 | // 4 | // We therefore need to scale and offset the rendering for each thumbnail and then project in Clip space (-1 to 1) 5 | // 1. glPosition = (position * scale + offset) * 2. - 1.; 6 | // 7 | // and to optimize at most we would like only one madd so: 8 | // 2. glPosition = position * shaderScale + shaderOffset; 9 | // 10 | // We can then developped 1. to: 11 | // 3. glPosition = position * scale * 2. + offset * 2. - 1.; 12 | // 13 | // and finally infer from 2. and 3. that: 14 | // 15 | // ------------------------------------- 16 | // | | 17 | // | shaderScale = scale * 2.; | 18 | // | shaderOffset = offset * 2. - 1.; | 19 | // | | 20 | // ------------------------------------- 21 | 22 | const vertexShader = ` 23 | attribute vec2 position; 24 | 25 | uniform vec2 scale; 26 | uniform vec2 offset; 27 | 28 | varying vec2 uv; 29 | 30 | void main(void) { 31 | uv = position; 32 | 33 | vec2 canvasPosition = position * scale + offset; 34 | 35 | gl_Position = vec4(canvasPosition, 0.0, 1.0); 36 | } 37 | `; 38 | 39 | const fragmentShader = ` 40 | varying vec2 uv; 41 | 42 | uniform sampler2D thumbnail; 43 | 44 | void main(void) { 45 | vec3 color = texture2D(thumbnail, uv).rgb; 46 | 47 | gl_FragColor = vec4(color, 1.); 48 | } 49 | `; 50 | 51 | export const ShaderConfiguration = { 52 | name: "timeline", 53 | vertexShader, 54 | fragmentShader, 55 | attributeNames: ["position"], 56 | uniformNames: ["scale", "offset"], 57 | samplerNames: ["thumbnail"], 58 | } -------------------------------------------------------------------------------- /src/coreControls/baseControl.ts: -------------------------------------------------------------------------------- 1 | import { ThinEngine } from "@babylonjs/core/Engines/thinEngine"; 2 | 3 | /** 4 | * Represents a the base class of any Babylon.js controls. 5 | * It helps ensuring they can all be initialized the same way and share 6 | * one Babylon.js instance if wished. 7 | */ 8 | export abstract class BaseControl { 9 | private readonly _canvas: HTMLCanvasElement; 10 | private readonly _engine: ThinEngine; 11 | 12 | /** 13 | * Gets the current Babylon.js engine used by the control. 14 | */ 15 | public get engine(): ThinEngine { 16 | return this._engine; 17 | } 18 | 19 | /** 20 | * Gets the current Babylon.js engine used by the control. 21 | */ 22 | public get canvas(): HTMLCanvasElement { 23 | return this._canvas; 24 | } 25 | 26 | /** 27 | * Instantiates a baseControl babylon.js object. 28 | * @param parent defines the parent of the control. It could be either: 29 | * - A canvas element: the canvas we want to render the control in. 30 | * - An engine instance: the Babylon.js engine to use to render the control. 31 | * - Another Babylon.js control: this allows sharing the engine cross controls to mix and match them for instance. 32 | */ 33 | constructor(parent: BaseControl | ThinEngine | HTMLCanvasElement) { 34 | if (parent instanceof HTMLCanvasElement) { 35 | this._canvas = parent; 36 | this._engine = new ThinEngine(this._canvas, false); 37 | } 38 | else if (parent instanceof ThinEngine) { 39 | this._canvas = parent.getRenderingCanvas(); 40 | this._engine = parent; 41 | } 42 | else { 43 | this._canvas = parent.canvas; 44 | this._engine = parent.engine; 45 | } 46 | // Parallel Shader Compile turned off at the moment. 47 | this._engine.getCaps().parallelShaderCompile = null; 48 | } 49 | 50 | /** 51 | * Dispose all the associated resources with WebGL. 52 | */ 53 | public dispose(): void { 54 | // Clear the renderer resources. 55 | this.engine.dispose(); 56 | } 57 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "command": "npm", 6 | "type": "shell", 7 | "presentation": { 8 | "echo": true, 9 | "reveal": "always", 10 | "focus": false, 11 | "panel": "shared" 12 | }, 13 | "tasks": [ 14 | { 15 | "label": "start", 16 | "args": [ "start" ], 17 | "group": { 18 | "kind": "build", 19 | "isDefault": true 20 | }, 21 | "isBackground": true, 22 | "problemMatcher": { 23 | "owner": "typescript", 24 | "fileLocation": "relative", 25 | "pattern": { 26 | "regexp": "^([^\\s].*)\\((\\d+|\\,\\d+|\\d+,\\d+,\\d+,\\d+)\\):\\s+(error|warning|info)\\s+(TS\\d+)\\s*:\\s*(.*)$", 27 | "file": 1, 28 | "location": 2, 29 | "severity": 3, 30 | "code": 4, 31 | "message": 5 32 | }, 33 | "background": { 34 | "activeOnStart": true, 35 | "beginsPattern": "start", 36 | "endsPattern": "Compiled successfully" 37 | } 38 | } 39 | }, 40 | { 41 | "label": "devServer", 42 | "args": [ "run", "devServer" ], 43 | "group": { 44 | "kind": "build", 45 | "isDefault": true 46 | }, 47 | "isBackground": true, 48 | "problemMatcher": { 49 | "owner": "typescript", 50 | "fileLocation": "relative", 51 | "pattern": { 52 | "regexp": "^([^\\s].*)\\((\\d+|\\,\\d+|\\d+,\\d+,\\d+,\\d+)\\):\\s+(error|warning|info)\\s+(TS\\d+)\\s*:\\s*(.*)$", 53 | "file": 1, 54 | "location": 2, 55 | "severity": 3, 56 | "code": 4, 57 | "message": 5 58 | }, 59 | "background": { 60 | "activeOnStart": true, 61 | "beginsPattern": "devServer", 62 | "endsPattern": "Compiled successfully" 63 | } 64 | } 65 | } 66 | ] 67 | } -------------------------------------------------------------------------------- /www/assets/common.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --main-bg-color: #2a2342; 3 | --main-bg-color-dark: #201936; 4 | --main-txt-color: #ffffff; 5 | --main-padding: 15px; 6 | } 7 | 8 | html, 9 | body { 10 | width: 100%; 11 | padding: 0; 12 | margin: 0; 13 | background-color: var(--main-bg-color); 14 | color: var(--main-txt-color); 15 | font-family: "acumin-pro"; 16 | } 17 | 18 | .slider { 19 | -webkit-appearance: none; 20 | cursor: pointer; 21 | width: 100%; 22 | height: 100%; 23 | outline: none; 24 | margin-left: 10px; 25 | margin-right: 10px; 26 | background-color: transparent; 27 | } 28 | 29 | /*Chrome -webkit */ 30 | .slider::-webkit-slider-thumb { 31 | -webkit-appearance: none; 32 | width: 20px; 33 | height: 20px; 34 | border: 2px solid white; 35 | border-radius: 50%; 36 | background: var(--footer-background); 37 | margin-top: -10px; 38 | } 39 | .slider::-webkit-slider-runnable-track { 40 | height: 2px; 41 | -webkit-appearance: none; 42 | background-color: white; 43 | } 44 | 45 | /** FireFox -moz */ 46 | .slider::-moz-range-progress { 47 | background-color: white; 48 | height: 2px; 49 | } 50 | .slider::-moz-range-thumb{ 51 | width: 20px; 52 | height: 20px; 53 | border: 2px solid white; 54 | border-radius: 50%; 55 | background: var(--footer-background); 56 | } 57 | .slider::-moz-range-track { 58 | background: white; 59 | height: 2px; 60 | } 61 | 62 | /** IE -ms */ 63 | .slider::-ms-track { 64 | height: 2px; 65 | 66 | /*remove bg colour from the track, we'll use ms-fill-lower and ms-fill-upper instead */ 67 | background: transparent; 68 | 69 | /*leave room for the larger thumb to overflow with a transparent border */ 70 | border-color: transparent; 71 | border-width: 10px 0; 72 | 73 | /*remove default tick marks*/ 74 | color: transparent; 75 | } 76 | .slider::-ms-fill-lower { 77 | background: white; 78 | border-radius: 5px; 79 | } 80 | .slider::-ms-fill-upper { 81 | background: white; 82 | border-radius: 5px; 83 | } 84 | .slider::-ms-thumb { 85 | width: 16px; 86 | height: 16px; 87 | border: 2px solid white; 88 | border-radius: 50%; 89 | margin-top: 0px; 90 | } 91 | 92 | a { 93 | color: #BB464B; 94 | text-decoration: unset; 95 | } 96 | a:hover { 97 | color: #BB464B; 98 | } 99 | a:visited { 100 | color: #BB464B; 101 | } 102 | a:focus { 103 | color: #BB464B; 104 | } -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to Babylon.js controls 2 | 3 | ## Golden rules 4 | 5 | **Babylon.js controls** are built upon 3 golden rules: 6 | 7 | 1. You cannot add code that will break backward compatibility 8 | 2. You cannot add code that will slow down the rendering process 9 | 3. You cannot add code that will make things complex to use 10 | 11 | ### Backward compatibility 12 | 13 | The first golden rule is a really important one because we want our users to trust Babylon.js controls. And when we need to introduce something that will break backward compatibility, we know that it will imply more work for our customers to switch to a new version. So even if something could be simpler to do by breaking the backward compatibility, we will not do it (exceptions may apply of course if there is a problem with performance or if this is related to a bug). 14 | 15 | ### Performance 16 | 17 | Babylon.js controls are meant to be performant. So every piece of code has to be scrutinized to look for potential bottlenecks or slow downs. Ultimately the goal is to render more with less resources. 18 | 19 | ### Simplicity 20 | 21 | A developer should be able to quickly and easily learn to use the API. 22 | 23 | Simplicity and a low barrier to entry are must-have features of every API. If you have any second thoughts about the complexity of a design, it is almost always much better to cut the feature from the current release and spend more time to get the design right for the next release. 24 | 25 | You can always add to an API, you cannot ever remove anything from one. If the design does not feel right, and you ship it anyway, you are likely to regret having done so. 26 | 27 | ## Forum and Github issues 28 | 29 | Since the very beginning, Babylon.js and its extensions rely on a great forum and a tremendous community: https://forum.babylonjs.com/. 30 | Please use the forum for **ANY questions you may have**. 31 | 32 | Please use the Github issues (after discussing them on the forum) **only** for: 33 | - Bugs 34 | - Feature requests 35 | 36 | We will try to enforce these rules as we consider the forum is a better place for discussions and learnings. 37 | 38 | ## Pull requests 39 | 40 | We are not complicated people, but we still have some [coding guidelines](http://doc.babylonjs.com/how_to/approved_naming_conventions) 41 | 42 | To validate your PR, please follow these steps: 43 | - Run "gulp" locally and make sure that no error is generated 44 | - Make sure that all public functions and classes are commented using JSDoc syntax 45 | 46 | ## What should go where? 47 | 48 | In order to not bloat the control library with unwanted or unnecessary features (that we will need to maintain forever), here is a list of questions you could ask yourself before submitting a new feature (or feature request) for Babylon.js controls: 49 | - Does my feature belong to a framework library? 50 | - Can my feature be used by multiple different applications? 51 | - Is there a general use case for this feature? 52 | - Does this feature already exist in a similar framework? 53 | 54 | If your PR is does not fall into the controls category you can consider using our [Extensions repo](https://github.com/BabylonJS/Extensions) for more high level features. 55 | -------------------------------------------------------------------------------- /src/coreControls/elementToTexture.ts: -------------------------------------------------------------------------------- 1 | import { ThinTexture } from "@babylonjs/core/Materials/Textures/thinTexture"; 2 | // import { Texture } from "@babylonjs/core/Materials/Textures/texture"; 3 | import { HtmlElementTexture } from "@babylonjs/core/Materials/Textures/htmlElementTexture"; 4 | 5 | import { Constants } from "@babylonjs/core/Engines/constants"; 6 | import { ThinEngine } from "@babylonjs/core/Engines/thinEngine"; 7 | 8 | /** 9 | * Converts heterogenous texture types to a Babylon.js usable texture. 10 | * @param engine defines the engine the texture will be associated with. 11 | * @param textureData defines the texture data as a texture, a video, a canvas or a url. 12 | * @param name defines the name of the texture. 13 | * @param generateMipMaps defines if mipmaps needs to be generated for the texture. 14 | * @param textureData defines the type of filtering used for the texture (Constants.TEXTURE_NEAREST_NEAREST...). 15 | * @param invertY defines whether the default vertical orientation shoudl be reversed. 16 | * @returns the Babylon.js texture. 17 | */ 18 | export function elementToTexture(engine: ThinEngine, 19 | textureData: ThinTexture | HTMLCanvasElement | HTMLVideoElement | string, 20 | name: string, 21 | generateMipMaps: boolean = false, 22 | filteringType: number = Constants.TEXTURE_BILINEAR_SAMPLINGMODE, 23 | invertY = true): ThinTexture { 24 | 25 | let texture: ThinTexture; 26 | // In case of a texture do nothing. 27 | if (textureData instanceof ThinTexture) { 28 | texture = textureData; 29 | } 30 | // In case of string, load the texture from a URI. 31 | else if (typeof(textureData) === "string") { 32 | const internalTexture = engine.createTexture(textureData, !generateMipMaps, invertY, null, filteringType); 33 | texture = new ThinTexture(internalTexture); 34 | } 35 | else { 36 | // Else loads the provided video or canvas element. 37 | const htmlElementTexture = new HtmlElementTexture(name, textureData, { 38 | engine: engine, 39 | generateMipMaps: generateMipMaps, 40 | samplingMode: filteringType, 41 | scene: null 42 | }); 43 | texture = htmlElementTexture; 44 | 45 | const onTextureUpdated = () => { 46 | // Try to not release too soon, it looks like 47 | // it might cause glitches in older browsers. 48 | setTimeout(() => { 49 | htmlElementTexture.element = null; 50 | }, 500); 51 | } 52 | 53 | // If on a video element, wait for the video to be ready before updating the texture. 54 | if (!!(textureData as HTMLVideoElement).getVideoPlaybackQuality) { 55 | const videoElement = textureData as HTMLVideoElement; 56 | if (videoElement.readyState < videoElement.HAVE_ENOUGH_DATA) { 57 | const checkIsReady = (() => { 58 | if (videoElement.readyState < videoElement.HAVE_ENOUGH_DATA) { 59 | return; 60 | } 61 | engine.stopRenderLoop(checkIsReady); 62 | htmlElementTexture.update(!invertY); 63 | onTextureUpdated(); 64 | }).bind(this); 65 | 66 | engine.runRenderLoop(checkIsReady); 67 | } 68 | else { 69 | htmlElementTexture.update(!invertY); 70 | onTextureUpdated(); 71 | } 72 | } 73 | else { 74 | // Canvas element are considered already ready to be uploaded to GPU. 75 | htmlElementTexture.update(invertY); 76 | onTextureUpdated(); 77 | } 78 | } 79 | 80 | // Sets common texture parameters. 81 | texture.wrapU = Constants.TEXTURE_CLAMP_ADDRESSMODE; 82 | texture.wrapV = Constants.TEXTURE_CLAMP_ADDRESSMODE; 83 | 84 | return texture; 85 | } -------------------------------------------------------------------------------- /www/assets/logo.svg: -------------------------------------------------------------------------------- 1 | babylonjs_identity_color_dark -------------------------------------------------------------------------------- /www/timeline/index.ts: -------------------------------------------------------------------------------- 1 | import { Timeline } from "../../src/timeline"; 2 | import { Resizer } from "../../src/resizer"; 3 | 4 | const mainVideo = document.getElementById("mainVideo") as HTMLVideoElement; 5 | const timelineCanvas = document.getElementById("timelineCanvas") as HTMLCanvasElement; 6 | const startTimeElement = document.getElementById("startTime"); 7 | const endTimeElement = document.getElementById("endTime"); 8 | const sliderTime = document.getElementById("sliderTime") as any; 9 | const sliderZoom = document.getElementById("sliderZoom") as any; 10 | 11 | const getTimeString = function(time) { 12 | const minutes = Math.floor(time / 60); 13 | const seconds = Math.floor(time) % 60; 14 | return `${("0" + minutes).slice(-2)}:${("0" + seconds).slice(-2)}` 15 | } 16 | 17 | const setTimes = function(timeline) { 18 | startTimeElement.innerText = getTimeString(timeline.currentTime); 19 | endTimeElement.innerText = getTimeString(timeline.currentTime + timeline.visibleDuration); 20 | 21 | sliderTime.min = 0; 22 | sliderTime.max = Math.ceil(timeline.maxSettableTime); 23 | } 24 | 25 | const initSliders = function() { 26 | sliderTime.value = 0; 27 | sliderTime.steps = "any"; 28 | 29 | sliderZoom.min = 0; 30 | sliderZoom.max = 1000; 31 | sliderZoom.value = 0; 32 | sliderZoom.steps = "any"; 33 | } 34 | 35 | function main() { 36 | initSliders(); 37 | 38 | const resizer = new Resizer(timelineCanvas); 39 | const timeline = new Timeline(resizer, { 40 | totalDuration: 60, 41 | thumbnailWidth: 128, 42 | thumbnailHeight: 120, 43 | loadingTextureURI: "./assets/loading.png", 44 | getThumbnailCallback: (time: number, done: (element: any) => void) => { 45 | // This is strictly for demo purpose and should not be used in prod as it creates as many videos 46 | // as there are thumbnails all over the timeline. 47 | const hiddenVideo = document.createElement("video"); 48 | document.body.append(hiddenVideo); 49 | hiddenVideo.style.display = "none"; 50 | 51 | hiddenVideo.setAttribute("playsinline", ""); 52 | hiddenVideo.muted = true; 53 | hiddenVideo.autoplay = navigator.userAgent.indexOf("Edge") > 0 ? false : true; 54 | hiddenVideo.loop = false; 55 | 56 | hiddenVideo.onloadeddata = () => { 57 | if (time === 0) { 58 | done(resizer.getResizedTexture(hiddenVideo, { width: 128, height: 100 })); 59 | } 60 | else { 61 | hiddenVideo.onseeked = () => { 62 | done(resizer.getResizedTexture(hiddenVideo, { width: 128, height: 100 })); 63 | } 64 | hiddenVideo.currentTime = time; 65 | } 66 | } 67 | 68 | hiddenVideo.src = "./assets/test.mp4?" + time; 69 | hiddenVideo.load(); 70 | 71 | // done(hiddenVideo); 72 | 73 | // done("./assets/loading.png"); 74 | } 75 | }); 76 | 77 | setTimes(timeline); 78 | 79 | timeline.runRenderLoop(() => { 80 | if (!mainVideo.paused) { 81 | timeline.setCurrentTime(mainVideo.currentTime); 82 | } 83 | }); 84 | 85 | sliderTime.addEventListener("input", function() { 86 | if (!mainVideo.paused) { 87 | mainVideo.pause(); 88 | } 89 | var value = parseFloat(this.value); 90 | timeline.setCurrentTime(value); 91 | setTimes(timeline); 92 | }); 93 | 94 | sliderZoom.addEventListener("input", function() { 95 | if (!mainVideo.paused) { 96 | mainVideo.pause(); 97 | } 98 | var value = parseFloat(this.value) / 10; 99 | timeline.setVisibleDurationZoom(value); 100 | setTimes(timeline); 101 | 102 | sliderTime.value = 0; 103 | }); 104 | } 105 | 106 | main(); -------------------------------------------------------------------------------- /www/resizer/amp.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Resizer 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 76 | 77 | 78 | 79 |
80 | 85 |
86 | This demo use the resizer control to extract frames of a video played in AMP. 87 |
88 |
89 | One of the biggest advantage is that compared to drawImage, it might/should work on Safari. 90 |
91 |
92 | 97 |
98 |
99 | 100 |
101 |
102 | Start Generating 103 |
104 |
105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Babylon.js Controls 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 66 | 67 | 68 | 69 |
70 | 75 |
76 | This is the main entry point of the Babylon.js Controls library during development. 77 |
78 |
79 |

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 | 100 |
101 | 104 |
105 | 106 | 107 | -------------------------------------------------------------------------------- /www/resizer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Resizer 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 87 | 88 | 89 | 90 |
91 | 96 |
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 |
103 | 104 |
105 |
106 | 107 |
108 |
109 | 110 |
111 |
112 | 113 |
114 |
115 | 116 |
117 |
118 | Start Resizing 119 |
120 |
121 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /www/resizer/amp.ts: -------------------------------------------------------------------------------- 1 | import { Resizer } from "../../src/resizer"; 2 | import { HtmlElementTexture } from "@babylonjs/core/Materials/Textures/htmlElementTexture"; 3 | import { Constants } from "@babylonjs/core/Engines/constants"; 4 | 5 | const thumbnailsCanvas = document.getElementById("thumbnailsCanvas") as HTMLCanvasElement; 6 | 7 | const startButton = document.getElementById("startButton"); 8 | 9 | // Generate thumbnails from a video element inside a VideoJS control 10 | // This works even on Safari \o/ 11 | function generateThumbnails(ampVideo: HTMLVideoElement) { 12 | // Create a resizer control wrapping our destination canvas. 13 | const resizer = new Resizer(thumbnailsCanvas); 14 | 15 | const seek = (time) => { 16 | // Stop at 2 minutes. 17 | if (time > 120) { 18 | return; 19 | } 20 | 21 | // Else seek 22 | ampVideo.currentTime = time; 23 | } 24 | 25 | ampVideo.addEventListener("seeked", () => { 26 | // You think it is ready, but we were having black frames on Safari... 27 | // This did the trick. 28 | setTimeout(async () => { 29 | // Render the thumbnail. 30 | await resizer.resize(ampVideo); 31 | 32 | // You could here manipulate the thumbnailsCanvas to extract and deal with the thumbnails. 33 | 34 | // Seek to the next thumbnail. 35 | seek(ampVideo.currentTime + 1); 36 | }, 60); 37 | }) 38 | 39 | // Start seeking on click 40 | startButton.addEventListener("click", function() { 41 | // Seeking to 0 is know to have issue on some browsers 42 | // So stick with 0.01 instead 43 | seek(0.01); 44 | }); 45 | } 46 | 47 | // Generate thumbnails from a video element inside a VideoJS control 48 | // This works even on Safari \o/ 49 | // 50 | // This is a bit more complex but reduces the overall memory usage by reusing the 51 | // Same VideoTexture. 52 | function generateThumbnailsOptim(ampVideo: HTMLVideoElement) { 53 | // Create a resizer control wrapping our destination canvas. 54 | const resizer = new Resizer(thumbnailsCanvas); 55 | const generateMipMaps = resizer.engine.webGLVersion > 1; 56 | const textureFiltering = generateMipMaps ? Constants.TEXTURE_TRILINEAR_SAMPLINGMODE : Constants.TEXTURE_BILINEAR_SAMPLINGMODE; 57 | const videoTexture = new HtmlElementTexture("vid", ampVideo, { 58 | engine: resizer.engine, 59 | generateMipMaps: generateMipMaps, 60 | samplingMode: textureFiltering, 61 | scene: null 62 | }); 63 | 64 | const seek = (time) => { 65 | // Stop at 2 minutes. 66 | if (time > 120) { 67 | return; 68 | } 69 | 70 | // Else seek 71 | ampVideo.currentTime = time; 72 | } 73 | 74 | ampVideo.addEventListener("seeked", () => { 75 | // You think it is ready, but we were having black frames on Safari... 76 | // This did the trick. 77 | setTimeout(async () => { 78 | // As we are using a texture, manually update to the current video frame. 79 | videoTexture.update(true); 80 | 81 | // Render the thumbnail. 82 | await resizer.resize(videoTexture); 83 | 84 | // You could here manipulate the thumbnailsCanvas to extract and deal with the thumbnails. 85 | 86 | // Seek to the next thumbnail. 87 | seek(ampVideo.currentTime + 1); 88 | }, 60); 89 | }) 90 | 91 | // Start seeking on click 92 | startButton.addEventListener("click", function() { 93 | // Seeking to 0 is know to have issue on some browsers 94 | // So stick with 0.01 instead 95 | seek(0.01); 96 | }); 97 | } 98 | 99 | // I know, I know... ;-) but not the point of this demo. 100 | declare const amp: any; 101 | 102 | function main() { 103 | const myPlayer = amp('beforePicture', { 104 | "nativeControlsForTouch": false, 105 | autoplay: false, 106 | controls: true, 107 | width: "512", 108 | height: "512", 109 | poster: "", 110 | techOrder: ["azureHtml5JS", "html5"], 111 | }, function() { 112 | console.log('Ready to generate !'); 113 | 114 | const ampVideo = document.querySelector("video"); 115 | generateThumbnails(ampVideo); 116 | generateThumbnailsOptim(ampVideo); 117 | }); 118 | 119 | myPlayer.src([{ 120 | src: "//willzhanmswest.streaming.mediaservices.windows.net/1f2dd2dd-ee99-40be-aae9-d0c2209982eb/DroneFlightOverLasVegasStripH3Pro7.ism/Manifest", 121 | type: "application/vnd.ms-sstr+xml" 122 | }]); 123 | } 124 | 125 | main(); -------------------------------------------------------------------------------- /www/imageFilter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Image Filter 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 92 | 93 | 94 | 95 |
96 | 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 |
107 |
108 | 109 |
110 |
111 | 112 |
113 |
114 | 115 |
116 |
117 | 118 |
119 |
120 | 121 |
122 |
123 | 124 |
125 |
126 | Start Processing 127 |
128 |
129 | 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /www/timeline/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Timeline 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 104 | 105 | 106 | 107 |
108 | 113 |
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 | } --------------------------------------------------------------------------------