├── .gitignore ├── LICENSE.txt ├── README.md ├── package.json ├── shaders ├── debug.fragment.glsl ├── debug.vertex.glsl ├── sprite.fragment.glsl ├── sprite.vertex.glsl ├── tilemap.fragment.glsl └── tilemap.vertex.glsl └── src ├── Component.js ├── Debug.js ├── Device.js ├── Engine.js ├── EngineWindow.js ├── Loading.js ├── RadixSort.js ├── StateMachine.js ├── Time.js ├── Utils.js ├── entity ├── AnimatedSprite.js ├── BitmapText.js ├── Camera.js ├── Entity.js ├── Renderable.js ├── Sprite.js └── Text.js ├── index.js ├── input ├── Gamepad.js ├── Input.js └── Key.js ├── math ├── AABB.js ├── Circle.js ├── Common.js ├── Matrix3.js ├── Matrix4.js ├── Random.js ├── Vector2.js ├── Vector3.js └── Vector4.js ├── mesh └── Mesh.js ├── physics └── Raycast.js ├── renderer ├── DebugDrawCommand.js ├── DrawCommand.js ├── Renderer.js ├── RendererWebGL.js └── Stage.js ├── resources ├── Animation.js ├── Audio.js ├── Content.js ├── Font.js ├── Frame.js ├── Graphics.js ├── Material.js ├── Resource.js ├── Resources.js ├── Sound.js ├── Spritesheet.js ├── Texture.js ├── Tiled.js └── Tileset.js ├── tilemap ├── Tilemap.js ├── TilemapIsometricLayer.js ├── TilemapLayer.js ├── TilemapOrthogonalLayer.js └── component │ └── TileBody.js └── tween ├── Easing.js ├── Tween.js └── TweenManager.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | renderer/* 4 | store/* 5 | projects/* 6 | renderer/* -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2015 Infinite Foundation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | META v0.9.0 2 | ==== 3 | 4 | Meta2D is open source WebGL 2D game engine for making cross platform games. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "meta2d", 3 | "version": "0.9.0", 4 | "description": "Meta2D is open source WebGL 2D game engine for making cross platform games.", 5 | "author": "Arturs Šefers ", 6 | "main": "src/index.js", 7 | "scripts": { 8 | "build": "build-replica src/index.js -i index.html -t -u", 9 | "dev": "build-replica src/index.js -i index.html -t -s" 10 | }, 11 | "keywords": [ "webgl", "game", "engine", "gamedev", "2d", "free", "ecma6" ], 12 | "license": "MIT" 13 | } -------------------------------------------------------------------------------- /shaders/debug.fragment.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | uniform vec4 color; 4 | 5 | void main() { 6 | gl_FragColor = color; 7 | } -------------------------------------------------------------------------------- /shaders/debug.vertex.glsl: -------------------------------------------------------------------------------- 1 | attribute vec2 position; 2 | 3 | uniform mat3 matrixProjection; 4 | uniform mat3 matrixView; 5 | uniform mat3 matrixModel; 6 | 7 | void main() { 8 | gl_Position = vec4((matrixProjection * matrixView * matrixModel * vec3(position, 1.0)).xy, 0.0, 1.0); 9 | } -------------------------------------------------------------------------------- /shaders/sprite.fragment.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | varying highp vec2 var_uv; 4 | 5 | uniform sampler2D albedo; 6 | uniform vec4 color; 7 | 8 | void main() { 9 | gl_FragColor = texture2D(albedo, var_uv) * color; 10 | } -------------------------------------------------------------------------------- /shaders/sprite.vertex.glsl: -------------------------------------------------------------------------------- 1 | attribute vec2 position; 2 | attribute vec2 uv; 3 | 4 | uniform mat3 matrixProjection; 5 | uniform mat3 matrixView; 6 | uniform mat3 matrixModel; 7 | 8 | varying highp vec2 var_uv; 9 | 10 | void main() { 11 | gl_Position = vec4((matrixProjection * matrixView * matrixModel * vec3(position, 1.0)).xy, 0.0, 1.0); 12 | var_uv = uv; 13 | } -------------------------------------------------------------------------------- /shaders/tilemap.fragment.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | varying highp vec2 var_uv; 4 | uniform vec4 color; 5 | 6 | uniform sampler2D albedo; 7 | 8 | void main() { 9 | gl_FragColor = texture2D(albedo, var_uv) * color; 10 | } -------------------------------------------------------------------------------- /shaders/tilemap.vertex.glsl: -------------------------------------------------------------------------------- 1 | attribute vec2 position; 2 | attribute vec2 uv; 3 | 4 | uniform mat3 matrixProjection; 5 | uniform mat3 matrixView; 6 | uniform mat3 matrixModel; 7 | 8 | varying highp vec2 var_uv; 9 | 10 | void main() { 11 | gl_Position = vec4((matrixProjection * matrixView * matrixModel * vec3(position, 1.0)).xy, 0.0, 1.0); 12 | var_uv = uv; 13 | } -------------------------------------------------------------------------------- /src/Component.js: -------------------------------------------------------------------------------- 1 | 2 | class Component 3 | { 4 | constructor() { 5 | this.parent = null 6 | } 7 | 8 | onEnable() {} 9 | 10 | onDisable() {} 11 | } 12 | 13 | export default Component -------------------------------------------------------------------------------- /src/Debug.js: -------------------------------------------------------------------------------- 1 | import Renderable from "./entity/Renderable" 2 | import Text from "./entity/Text" 3 | import Engine from "./Engine" 4 | import Time from "./Time" 5 | 6 | const debugLayer = 7 7 | let debugPanel = null 8 | let fpsText = null 9 | let active = false 10 | 11 | const load = () => { 12 | debugPanel = new Renderable() 13 | Engine.view.addChild(debugPanel) 14 | 15 | fpsText = new Text() 16 | fpsText.position.set(5, 5) 17 | debugPanel.addChild(fpsText) 18 | 19 | debugPanel.setLayer(7) 20 | } 21 | 22 | const update = () => { 23 | if(!active) { 24 | return 25 | } 26 | fpsText.text = `fps ${Time.fps} | ${Time.ms}ms` 27 | } 28 | 29 | const toggle = () => { 30 | active = !active 31 | if(active && !debugPanel) { 32 | load() 33 | update() 34 | } 35 | Engine.camera.setCullMask(debugLayer, active) 36 | } 37 | 38 | export { 39 | update, toggle 40 | } -------------------------------------------------------------------------------- /src/Device.js: -------------------------------------------------------------------------------- 1 | 2 | const listeners = {} 3 | let str_fullscreen = null 4 | let str_fullscreenEnabled = null 5 | let str_fullscreenElement = null 6 | let str_onfullscreenchange = null 7 | let str_onfullscreenerror = null 8 | let str_exitFullscreen = null 9 | let str_requestFullscreen = null 10 | let str_hidden = null 11 | let str_visibilityChange = null 12 | let str_requestPointerLock = null 13 | let str_exitPointerLock = null 14 | let str_onpointerlockchange = null 15 | let str_pointerLockElement = null 16 | 17 | const Device = { 18 | name: "Unknown", 19 | version: "0", 20 | versionBuffer: null, 21 | vendor: "", 22 | vendors: [ "", "webkit", "moz", "ms", "o" ], 23 | supports: {}, 24 | mobile: false, 25 | portrait: false, 26 | visible: true, 27 | pointerlock: false, 28 | audioFormats: [], 29 | backingStoreRatio: 1, 30 | 31 | pointerLock(element) { 32 | if(!Device.supports.pointerLock) { return } 33 | 34 | if(element) { 35 | element[str_requestPointerLock]() 36 | } 37 | else { 38 | document[str_exitPointerLock]() 39 | } 40 | }, 41 | 42 | get pointerLockElement() { 43 | if(!Device.supports.pointerLock) { return null } 44 | 45 | return document[str_pointerLockElement] 46 | }, 47 | 48 | set fullscreen(element) { 49 | if(Device.fullscreenEnabled) { 50 | element[str_requestFullscreen]() 51 | } 52 | else { 53 | console.warn("Device cannot use fullscreen right now.") 54 | } 55 | }, 56 | 57 | get fullscreen() { 58 | if(Device.fullscreenEnabled) { 59 | return document[str_fullscreen] 60 | } 61 | return false 62 | }, 63 | 64 | get fullscreenEnabled() { 65 | if(Device.supports.fullscreen && document[str_fullscreenEnabled]) { 66 | return true 67 | } 68 | return false 69 | }, 70 | 71 | get fullscreenElement() { 72 | if(!Device.fullscreenEnabled) { 73 | return null 74 | } 75 | return document[str_fullscreenElement] 76 | }, 77 | 78 | fullscreenExit() { 79 | if(Device.fullscreenEnabled) { 80 | document[str_exitFullscreen]() 81 | } 82 | }, 83 | 84 | on(event, func) { 85 | let buffer = listeners[event] 86 | if(buffer) { 87 | buffer.push(func) 88 | } 89 | else { 90 | buffer = [ func ] 91 | listeners[event] = buffer 92 | } 93 | }, 94 | 95 | off(event, func) { 96 | const buffer = listeners[event] 97 | if(!buffer) { return } 98 | 99 | const index = buffer.indexOf(func) 100 | if(index === -1) { return } 101 | 102 | buffer[index] = buffer[buffer.length - 1] 103 | buffer.pop() 104 | }, 105 | 106 | emit(event, arg) { 107 | const buffer = listeners[event] 108 | if(!buffer) { return } 109 | 110 | for(let n = 0; n < buffer.length; n++) { 111 | buffer[n](arg) 112 | } 113 | } 114 | } 115 | 116 | const load = () => { 117 | checkBrowser() 118 | checkMobileAgent() 119 | checkCanvas() 120 | checkWebGL() 121 | checkBackingStoreRatio() 122 | checkAudioFormats() 123 | checkAudioAPI() 124 | checkPageVisibility() 125 | checkFullscreen() 126 | checkConsoleCSS() 127 | checkFileAPI() 128 | checkFileSystemAPI() 129 | checkPointerLock() 130 | 131 | Device.supports.onloadedmetadata = (typeof window.onloadedmetadata === "object") 132 | Device.supports.onkeyup = (typeof window.onkeyup === "object") 133 | Device.supports.onkeydown = (typeof window.onkeydown === "object") 134 | 135 | Device.portrait = (window.innerHeight > window.innerWidth) 136 | 137 | polyfill() 138 | addEventListeners() 139 | } 140 | 141 | const checkBrowser = () => { 142 | const regexps = { 143 | "Chrome": [ /Chrome\/(\S+)/ ], 144 | "Firefox": [ /Firefox\/(\S+)/ ], 145 | "MSIE": [ /MSIE (\S+);/ ], 146 | "Opera": [ 147 | /OPR\/(\S+)/, 148 | /Opera\/.*?Version\/(\S+)/, /* Opera 10 */ 149 | /Opera\/(\S+)/ /* Opera 9 and older */ 150 | ], 151 | "Safari": [ /Version\/(\S+).*?Safari\// ] 152 | }; 153 | 154 | const userAgent = navigator.userAgent 155 | let name, currRegexp, match 156 | let numElements = 2 157 | 158 | for(name in regexps) { 159 | while(currRegexp = regexps[name].shift()) { 160 | if(match = userAgent.match(currRegexp)) { 161 | Device.version = (match[1].match(new RegExp("[^.]+(?:\.[^.]+){0," + --numElements + "}")))[0] 162 | Device.name = name 163 | 164 | const versionBuffer = Device.version.split(".") 165 | const versionBufferLength = versionBuffer.length 166 | Device.versionBuffer = new Array(versionBufferLength) 167 | for(let n = 0; n < versionBufferLength; n++) { 168 | Device.versionBuffer[n] = parseInt(versionBuffer[n]) 169 | } 170 | break 171 | } 172 | } 173 | } 174 | 175 | if(Device.versionBuffer === null || Device.name === "unknown") { 176 | console.warn("(Device) Could not detect browser.") 177 | } 178 | else { 179 | if(Device.name === "Chrome" || Device.name === "Safari" || Device.name === "Opera") { 180 | Device.vendor = "webkit" 181 | } 182 | else if(Device.name === "Firefox") { 183 | Device.vendor = "moz" 184 | } 185 | else if(Device.name === "MSIE") { 186 | Device.vendor = "ms" 187 | } 188 | } 189 | } 190 | 191 | const checkMobileAgent = () => { 192 | Device.mobile = (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) 193 | } 194 | 195 | const checkBackingStoreRatio = () => { 196 | if(!Device.supports.canvas) { return } 197 | 198 | const canvas = document.createElement("canvas") 199 | const ctx = canvas.getContext("2d") 200 | 201 | if(ctx.backingStorePixelRatio !== undefined) { 202 | Device.backingStoreRatio = ctx.backingStorePixelRatio 203 | } 204 | else if(ctx[Device.vendor + "BackingStorePixelRatio"]) { 205 | Device.backingStoreRatio = ctx[Device.vendor + "BackingStorePixelRatio"] 206 | } 207 | } 208 | 209 | const checkCanvas = () => { 210 | Device.supports.canvas = !!window.CanvasRenderingContext2D; 211 | } 212 | 213 | const checkWebGL = () => { 214 | Device.supports.webgl = (WebGLRenderingContext !== undefined) 215 | } 216 | 217 | const checkAudioFormats = () => { 218 | const audio = document.createElement("audio") 219 | if(audio.canPlayType("audio/mp4")) { 220 | Device.audioFormats.push("m4a") 221 | } 222 | if(audio.canPlayType("audio/ogg")) { 223 | Device.audioFormats.push("ogg") 224 | } 225 | if(audio.canPlayType("audio/mpeg")) { 226 | Device.audioFormats.push("mp3") 227 | } 228 | if(audio.canPlayType("audio/wav")) { 229 | Device.audioFormats.push("wav") 230 | } 231 | } 232 | 233 | const checkAudioAPI = () => { 234 | if(!window.AudioContext) 235 | { 236 | window.AudioContext = window.webkitAudioContext || 237 | window.mozAudioContext || 238 | window.oAudioContext || 239 | window.msAudioContext 240 | } 241 | 242 | if(window.AudioContext) { 243 | Device.supports.audioAPI = true 244 | } 245 | } 246 | 247 | const checkPageVisibility = () => { 248 | if(document.hidden !== undefined) { 249 | str_hidden = "hidden" 250 | str_visibilityChange = "visibilitychange" 251 | Device.supports.pageVisibility = true 252 | } 253 | else if(document[Device.vendor + "Hidden"] !== undefined) { 254 | str_hidden = Device.vendor + "Hidden" 255 | str_visibilityChange = Device.vendor + "visibilitychange" 256 | Device.supports.pageVisibility = true 257 | } 258 | else { 259 | Device.supports.pageVisibility = false 260 | } 261 | } 262 | 263 | const checkFullscreen = () => { 264 | // fullscreen 265 | if(document.fullscreen !== undefined) { 266 | str_fullscreen = "fullscreen" 267 | } 268 | else if(document[Device.vendor + "IsFullScreen"] !== undefined) { 269 | str_fullscreen = Device.vendor + "IsFullScreen" 270 | } 271 | else if(document[Device.vendor + "Fullscreen"] !== undefined) { 272 | str_fullscreen = Device.vendor + "Fullscreen" 273 | } 274 | else { 275 | Device.supports.fullscreen = false 276 | return; 277 | } 278 | 279 | Device.supports.fullscreen = true 280 | 281 | // fullscreenEnabled 282 | if(document.fullscreenEnabled !== undefined) { 283 | str_fullscreenEnabled = "fullscreenEnabled" 284 | } 285 | else if(document[Device.vendor + "FullscreenEnabled"] !== undefined) { 286 | str_fullscreenEnabled = Device.vendor + "FullscreenEnabled" 287 | } 288 | 289 | // fullscreenElement 290 | if(document.fullscreenElement !== undefined) { 291 | str_fullscreenElement = "fullscreenElement" 292 | } 293 | else if(document[Device.vendor + "FullscreenElement"] !== undefined) { 294 | str_fullscreenElement = Device.vendor + "FullscreenElement" 295 | } 296 | 297 | // exitFullscreen 298 | if(document.exitFullscreen !== undefined) { 299 | str_exitFullscreen = "exitFullscreen" 300 | } 301 | else if(document[Device.vendor + "ExitFullscreen"] !== undefined) { 302 | str_exitFullscreen = Device.vendor + "ExitFullscreen" 303 | } 304 | 305 | // requestFullscreen 306 | if(Element.prototype.requestFullscreen !== undefined) { 307 | str_requestFullscreen = "requestFullscreen"; 308 | } 309 | else if(Element.prototype[Device.vendor + "RequestFullscreen"] !== undefined) { 310 | str_requestFullscreen = Device.vendor + "RequestFullscreen"; 311 | } 312 | 313 | // onfullscreenchange 314 | if(document.onfullscreenchange !== undefined) { 315 | str_onfullscreenchange = "fullscreenchange" 316 | } 317 | else if(document["on" + Device.vendor + "fullscreenchange"] !== undefined) { 318 | str_onfullscreenchange = Device.vendor + "fullscreenchange" 319 | } 320 | 321 | // onfullscreenerror 322 | if(document.onfullscreenerror !== undefined) { 323 | str_onfullscreenerror = "fullscreenerror"; 324 | } 325 | else if(document["on" + Device.vendor + "fullscreenerror"] !== undefined) { 326 | str_onfullscreenerror = Device.vendor + "fullscreenerror" 327 | } 328 | } 329 | 330 | const checkConsoleCSS = () => { 331 | if(!Device.mobile && (Device.name === "Chrome" || Device.name === "Opera")) { 332 | Device.supports.consoleCSS = true 333 | } 334 | else { 335 | Device.supports.consoleCSS = false 336 | } 337 | } 338 | 339 | const checkFileAPI = () => { 340 | if(window.File && window.FileReader && window.FileList && window.Blob) { 341 | Device.supports.fileAPI = true 342 | } 343 | else { 344 | Device.supports.fileAPI = false 345 | } 346 | } 347 | 348 | const checkFileSystemAPI = () => { 349 | if(!window.requestFileSystem) 350 | { 351 | window.requestFileSystem = window.webkitRequestFileSystem || 352 | window.mozRequestFileSystem || 353 | window.oRequestFileSystem || 354 | window.msRequestFileSystem 355 | } 356 | 357 | if(window.requestFileSystem) { 358 | Device.supports.fileSystemAPI = true 359 | } 360 | } 361 | 362 | const polyfill = () => { 363 | if(!Number.MAX_SAFE_INTEGER) { 364 | Number.MAX_SAFE_INTEGER = 9007199254740991 365 | } 366 | supportConsole() 367 | supportRequestAnimFrame() 368 | supportPerformanceNow() 369 | } 370 | 371 | const supportConsole = () => { 372 | if(!window.console) 373 | { 374 | window.console = { 375 | log() {}, 376 | warn() {}, 377 | error() {} 378 | } 379 | } 380 | } 381 | 382 | const supportRequestAnimFrame = () => { 383 | if(!window.requestAnimationFrame) { 384 | window.requestAnimationFrame = (function() { 385 | return window.webkitRequestAnimationFrame || 386 | window.mozRequestAnimationFrame || 387 | window.oRequestAnimationFrame || 388 | window.msRequestAnimationFrame || 389 | 390 | function(callback, element) { 391 | window.setTimeout(callback, 1000 / 60) 392 | } 393 | })() 394 | } 395 | } 396 | 397 | const supportPerformanceNow = () => { 398 | if(window.performance === undefined) { 399 | window.performance = {} 400 | } 401 | 402 | if(window.performance.now === undefined) { 403 | window.performance.now = Date.now 404 | } 405 | } 406 | 407 | const addEventListeners = () => { 408 | window.addEventListener("resize", onResize, false) 409 | window.addEventListener("orientationchange", onOrientationChange, false); 410 | 411 | if(Device.supports.pageVisibility) { 412 | Device.visible = !document[str_hidden] 413 | document.addEventListener(str_visibilityChange, onVisibilityChange) 414 | } 415 | 416 | window.addEventListener("focus", onFocus) 417 | window.addEventListener("blur", onBlur) 418 | 419 | if(Device.supports.fullscreen) { 420 | document.addEventListener(str_onfullscreenchange, onFullscreenChange) 421 | document.addEventListener(str_onfullscreenerror, onFullscreenError) 422 | } 423 | 424 | if(Device.supports.pointerLock) { 425 | document.addEventListener(str_onpointerlockchange, onPointerLockChange) 426 | } 427 | } 428 | 429 | const onResize = (domEvent) => { 430 | Device.emit("resize", window) 431 | 432 | if(window.innerHeight > window.innerWidth) 433 | { 434 | if(!Device.portrait) { 435 | Device.portrait = true 436 | Device.emit("portrait", true) 437 | } 438 | } 439 | else if(Device.portrait) { 440 | Device.portrait = false 441 | Device.emit("portrait", false) 442 | } 443 | } 444 | 445 | const onOrientationChange = (domEvent) => { 446 | Device.emit("resize", window); 447 | 448 | if(window.innerHeight > window.innerWidth) { 449 | Device.portrait = true 450 | Device.emit("portrait", true) 451 | } 452 | else { 453 | Device.portrait = false 454 | Device.emit("portrait", false) 455 | } 456 | } 457 | 458 | const onFocus = (domEvent) => { 459 | Device.visible = true 460 | Device.emit("visible", true) 461 | } 462 | 463 | const onBlur = (domEvent) => { 464 | Device.visible = false 465 | Device.emit("visible", false) 466 | } 467 | 468 | const onVisibilityChange = (domEvent) => { 469 | Device.visible = !document[str_hidden] 470 | Device.emit("visible", Device.visible) 471 | } 472 | 473 | const onFullscreenChange = (domEvent) => { 474 | Device.emit("fullscreen", Device.fullscreenElement) 475 | } 476 | 477 | const onFullscreenError = (domEvent) => { 478 | console.error("Fullscreen denied.") 479 | } 480 | 481 | const onPointerLockChange = (domEvent) => {} 482 | 483 | const checkPointerLock = () => { 484 | const canvas = HTMLCanvasElement.prototype 485 | 486 | if(canvas.requestPointerLock !== undefined) { 487 | str_requestPointerLock = "requestPointerLock" 488 | } 489 | else if(document[Device.vendor + "RequestPointerLock"] !== undefined) { 490 | str_requestPointerLock = Device.vendor + "RequestPointerLock" 491 | } 492 | else { 493 | return 494 | } 495 | 496 | Device.supports.pointerLock = true 497 | 498 | if(document.exitPointerLock !== undefined) { 499 | str_exitPointerLock = "exitPointerLock" 500 | } 501 | else if(document[Device.vendor + "ExitPointerLock"] !== undefined) { 502 | str_exitPointerLock = Device.vendor + "ExitPointerLock" 503 | } 504 | 505 | if(document.onpointerlockchange !== undefined) { 506 | str_onpointerlockchange = "pointerlockchange" 507 | } 508 | else if(document["on" + Device.vendor + "pointerlockchange"] !== undefined) { 509 | str_onpointerlockchange = Device.vendor + "pointerlockchange" 510 | } 511 | 512 | if(document.pointerLockElement !== undefined) { 513 | str_pointerLockElement = "pointerLockElement" 514 | } 515 | else if(document[Device.vendor + "PointerLockElement"] !== undefined) { 516 | str_pointerLockElement = Device.vendor + "PointerLockElement" 517 | } 518 | } 519 | 520 | load() 521 | 522 | export default Device -------------------------------------------------------------------------------- /src/Engine.js: -------------------------------------------------------------------------------- 1 | 2 | const listeners = {} 3 | 4 | const Engine = { 5 | app: null, 6 | container: null, 7 | canvas: null, 8 | gl: null, 9 | window: null, 10 | camera: null, 11 | cameras: null, 12 | view: null, 13 | updating: [], 14 | updatingComponents: [], 15 | updatingRemove: [], 16 | updatingComponentsRemove: [], 17 | 18 | defaultSettings: { 19 | width: 0, 20 | height: 0, 21 | antialias: true, 22 | alpha: true, 23 | upscale: true, 24 | canvas: null 25 | }, 26 | 27 | addUpdating(entity) { 28 | this.updating.push(entity) 29 | }, 30 | 31 | removeUpdating(entity) { 32 | if(this.window._updating) { 33 | this.updatingRemove.push(entity) 34 | } 35 | else { 36 | const index = this.updating.indexOf(entity) 37 | if(index === -1) { return } 38 | 39 | this.updating[index] = this.updating[this.updating.length - 1] 40 | this.updating.pop() 41 | } 42 | }, 43 | 44 | addUpdatingComponent(component) { 45 | this.updatingComponents.push(component) 46 | }, 47 | 48 | removeUpdatingComponent(component) { 49 | if(this.window._updating) { 50 | this.updatingComponentsRemove.push(component) 51 | } 52 | else { 53 | const index = this.updatingComponents.indexOf(component) 54 | if(index === -1) { return } 55 | 56 | this.updatingComponents[index] = this.updatingComponents[this.updatingComponents.length - 1] 57 | this.updatingComponents.pop() 58 | } 59 | }, 60 | 61 | on(event, func) { 62 | const buffer = listeners[event] 63 | if(buffer) { 64 | buffer.push(func) 65 | } 66 | else { 67 | listeners[event] = [ func ] 68 | } 69 | }, 70 | 71 | off(event, func) { 72 | const buffer = listeners[event] 73 | if(!buffer) { return } 74 | 75 | const index = buffer.indexOf(func) 76 | if(index === -1) { return } 77 | 78 | buffer[index] = buffer[buffer.length - 1] 79 | buffer.pop() 80 | }, 81 | 82 | emit(event, arg) { 83 | const buffer = listeners[event] 84 | if(!buffer) { return } 85 | 86 | if(arg === undefined) { 87 | for(let n = 0; n < buffer.length; n++) { 88 | buffer[n]() 89 | } 90 | } 91 | else { 92 | for(let n = 0; n < buffer.length; n++) { 93 | buffer[n](arg) 94 | } 95 | } 96 | } 97 | } 98 | 99 | export default Engine -------------------------------------------------------------------------------- /src/EngineWindow.js: -------------------------------------------------------------------------------- 1 | import Engine from "./Engine" 2 | import Device from "./Device" 3 | import Time from "./Time" 4 | import Debug from "./Debug" 5 | import Input from "./input/Input" 6 | import Resources from "./resources/Resources" 7 | import Vector4 from "./math/Vector4" 8 | 9 | class EngineWindow { 10 | constructor() { 11 | this.width = 0 12 | this.height = 0 13 | this.offsetLeft = 0 14 | this.offsetTop = 0 15 | this.ratio = 1 16 | this.scaleX = 1 17 | this.scaleY = 1 18 | this.bgColor = new Vector4(0.8, 0.8, 0.8, 1) 19 | this._cursor = "auto" 20 | this._updating = false 21 | this._updateWorker = null 22 | this.readyFunc = null 23 | this.renderFunc = null 24 | } 25 | 26 | create() { 27 | if(Engine.app.init) { 28 | Engine.app.init() 29 | } 30 | 31 | let container = null 32 | let canvas = null 33 | 34 | if(Engine.settings.canvas) { 35 | canvas = (typeof Engine.settings.canvas === "string") ? document.querySelector(Engine.settings.canvas) : Engine.settings.canvas 36 | } 37 | if(!canvas) { 38 | container = document.body 39 | canvas = document.createElement("canvas") 40 | container.appendChild(canvas) 41 | } 42 | else { 43 | container = canvas.parentElement 44 | } 45 | 46 | const gl = canvas.getContext("webgl", { 47 | antialias: Engine.settings.antialias, 48 | alpha: Engine.settings.alpha 49 | }) 50 | if(!gl) { 51 | console.error("Unable to initialize WebgGL context. Your browser or machine may not support it.") 52 | return 53 | } 54 | 55 | Engine.container = container || document.body 56 | Engine.canvas = canvas 57 | Engine.gl = gl 58 | 59 | this.setup() 60 | Engine.emit("setup") 61 | 62 | if(Engine.app.setup) { 63 | Engine.app.setup() 64 | } 65 | 66 | this.updateScreenSize() 67 | Device.on("resize", this.updateScreenSize.bind(this)) 68 | Device.on("visible", this.handleVisible.bind(this)) 69 | this.handleVisible(Device.visible) 70 | 71 | this.readyFunc = this.ready.bind(this) 72 | this.renderFunc = this.render.bind(this) 73 | 74 | Resources.on("ready", this.readyFunc) 75 | if(!Resources.loading) { 76 | this.ready() 77 | } 78 | 79 | Input.on("keydown", (event) => { 80 | switch(event.keyCode) { 81 | case 192: // ` 82 | Debug.toggle() 83 | break 84 | } 85 | }) 86 | } 87 | 88 | setup() { 89 | const gl = Engine.gl 90 | 91 | gl.clearColor(this.bgColor.x, this.bgColor.y, this.bgColor.z, this.bgColor.w) 92 | gl.clearDepth(1.0) 93 | gl.enable(gl.DEPTH_TEST) 94 | gl.depthFunc(gl.LEQUAL) 95 | gl.enable(gl.BLEND) 96 | gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA) 97 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) 98 | } 99 | 100 | updateScreenSize() { 101 | const settings = Engine.settings 102 | const container = Engine.container 103 | const canvas = Engine.canvas 104 | 105 | const targetWidth = settings.width ? settings.width : container.clientWidth 106 | const targetHeight = settings.height ? settings.height : container.clientHeight 107 | 108 | const widthRatio = container.clientWidth / targetWidth 109 | const heightRatio = container.clientHeight / targetHeight 110 | const currRatio = (widthRatio < heightRatio) ? widthRatio : heightRatio 111 | 112 | if(settings.upscale) { 113 | this.ratio = currRatio 114 | } 115 | else { 116 | this.ratio = (currRatio > 1.0) ? 1.0 : currRatio 117 | } 118 | 119 | this.width = targetWidth | 0 120 | this.height = targetHeight | 0 121 | canvas.width = this.width 122 | canvas.height = this.height 123 | container.style.width = `${(targetWidth * this.ratio) | 0}px` 124 | container.style.height = `${(targetHeight * this.ratio) | 0}px` 125 | this.updateOffset() 126 | 127 | Engine.gl.viewport(0, 0, targetWidth, targetHeight) 128 | Engine.view.size.set(targetWidth, targetHeight) 129 | 130 | const cameras = Engine.cameras 131 | for(let n = 0; n < cameras.length; n++) { 132 | const camera = cameras[n] 133 | camera.size.set(targetWidth, targetHeight) 134 | camera.updateProjectionTransform() 135 | } 136 | 137 | Engine.emit("resize") 138 | } 139 | 140 | updateOffset() { 141 | this.offsetLeft = 0 142 | this.offsetTop = 0 143 | 144 | let element = Engine.container 145 | if(element.offsetParent) 146 | { 147 | do { 148 | this.offsetLeft += element.offsetLeft 149 | this.offsetTop += element.offsetTop 150 | } while(element = element.offsetParent); 151 | } 152 | 153 | let rect = Engine.container.getBoundingClientRect() 154 | this.offsetLeft += rect.left 155 | this.offsetTop += rect.top 156 | 157 | rect = Engine.canvas.getBoundingClientRect() 158 | this.offsetLeft += rect.left 159 | this.offsetTop += rect.top 160 | } 161 | 162 | ready() { 163 | Resources.off("ready", this.readyFunc) 164 | 165 | if(Engine.app.ready) { 166 | Engine.app.ready() 167 | } 168 | 169 | this.render(Time.deltaF) 170 | } 171 | 172 | update() { 173 | Time.start() 174 | 175 | while(Time.accumulator >= Time.updateFreq) { 176 | Time.accumulator -= Time.updateFreq 177 | Engine.emit("updateFixed", Time.updateFreq) 178 | } 179 | 180 | Time.alpha = Time.accumulator / Time.updateFreq 181 | 182 | this._updating = true 183 | const updatingComponents = Engine.updatingComponents 184 | for(let n = 0; n < updatingComponents.length; n++) { 185 | updatingComponents[n].update(Time.deltaF) 186 | } 187 | 188 | const updating = Engine.updating 189 | for(let n = 0; n < updating.length; n++) { 190 | updating[n].update(Time.deltaF) 191 | } 192 | this._updating = false 193 | 194 | if(Engine.updatingRemove.length > 0) { 195 | const buffer = Engine.updatingRemove.length 196 | for(let n = 0; n < buffer.length; n++) { 197 | Engine.removeUpdating(buffer[n]) 198 | } 199 | buffer.length = 0 200 | } 201 | if(Engine.updatingComponentsRemove.length > 0) { 202 | const buffer = Engine.updatingComponentsRemove.length 203 | for(let n = 0; n < buffer.length; n++) { 204 | Engine.removeUpdatingComponent(buffer[n]) 205 | } 206 | buffer.length = 0 207 | } 208 | 209 | Engine.emit("update", Time.deltaF) 210 | 211 | if(Engine.app.update) { 212 | Engine.app.update(Time.deltaF) 213 | } 214 | 215 | Time.end() 216 | } 217 | 218 | render() { 219 | this.update() 220 | 221 | Time.startRender() 222 | 223 | const gl = Engine.gl 224 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) 225 | 226 | Engine.emit("pre-render", Time.deltaRenderF) 227 | Engine.renderer.render() 228 | Engine.emit("render", Time.deltaRenderF) 229 | Engine.emit("post-render", Time.deltaRenderF) 230 | 231 | if(Engine.app.render) { 232 | Engine.app.render(Time.deltaRenderF) 233 | } 234 | 235 | Time.endRender() 236 | 237 | Debug.update() 238 | 239 | requestAnimationFrame(this.renderFunc) 240 | } 241 | 242 | handleVisible(visible) { 243 | if(visible) { 244 | if(this.updateWorker) { 245 | this.updateWorker.terminate() 246 | this.updateWorker = null 247 | } 248 | } 249 | else { 250 | const workerUpdateFunc = function() { 251 | setInterval(() => { 252 | postMessage("update") 253 | }, 1000 / 60) 254 | } 255 | this.updateWorker = new Worker(URL.createObjectURL( 256 | new Blob([ `(${workerUpdateFunc.toString()})()` ], { type: "text/javascript" }))) 257 | this.updateWorker.onmessage = (msg) => { 258 | this.update() 259 | } 260 | } 261 | } 262 | 263 | backgroundColor(r, g, b) { 264 | const weight = 1 / 255 265 | this.bgColor.set(r * weight, g * weight, b * weight, 1) 266 | Engine.gl.clearColor(this.bgColor.x, this.bgColor.y, this.bgColor.z, this.bgColor.w) 267 | } 268 | 269 | set cursor(type) { 270 | if(this._cursor === type) { return } 271 | this._cursor = type 272 | 273 | document.body.style.cursor = type 274 | } 275 | 276 | get cursor() { 277 | return this._cursor 278 | } 279 | } 280 | 281 | export default EngineWindow -------------------------------------------------------------------------------- /src/Loading.js: -------------------------------------------------------------------------------- 1 | import Engine from "./Engine" 2 | import Resources from "./resources/Resources" 3 | 4 | const loadingHolder = document.createElement("div") 5 | loadingHolder.style.cssText = "position:absolute; display:flex; width:100%; height:100%; background:black; align-items:center; justify-content: center;" 6 | 7 | const progressBar = document.createElement("div") 8 | progressBar.style.cssText = "width:25%; max-width: 120px; height:4px; background:#333;" 9 | loadingHolder.appendChild(progressBar) 10 | 11 | const progressBarCurrent = document.createElement("div") 12 | progressBarCurrent.style.cssText = "width:0%; height:100%; background:white;" 13 | progressBar.appendChild(progressBarCurrent) 14 | 15 | Resources.on("loading", () => { 16 | Engine.container.appendChild(loadingHolder) 17 | }) 18 | 19 | Resources.on("progress", (percents) => { 20 | progressBarCurrent.style.width = `${percents}%` 21 | }) 22 | 23 | Resources.on("ready", () => { 24 | Engine.container.removeChild(loadingHolder) 25 | }) 26 | -------------------------------------------------------------------------------- /src/RadixSort.js: -------------------------------------------------------------------------------- 1 | 2 | const numberOfBins = 256 3 | const bucket = new Array(numberOfBins) 4 | const startOfBin = new Array(numberOfBins) 5 | const endOfBin = new Array(numberOfBins) 6 | const radix = 8 7 | 8 | const extractDigit = function(a, bitMask, shiftRightAmount) { 9 | const digit = (a & bitMask) >>> shiftRightAmount 10 | return digit 11 | } 12 | 13 | const radixSortLSD = function(input, output, count) { 14 | let outputResult = false 15 | let bitMask = 255 16 | let shiftRightAmount = 0 17 | 18 | if(output.length < input.length) { 19 | output.length = input.length 20 | } 21 | 22 | while(bitMask != 0) { 23 | for(let n = 0; n < numberOfBins; n++) { 24 | bucket[n] = 0 25 | } 26 | for(let n = 0; n < count; n++) { 27 | const digit = (input[n].key & bitMask) >>> shiftRightAmount 28 | bucket[digit]++ 29 | } 30 | 31 | startOfBin[0] = endOfBin[0] = 0 32 | for(let n = 1; n < numberOfBins; n++) 33 | startOfBin[n] = endOfBin[n] = startOfBin[n - 1] + bucket[n - 1] 34 | for(let n = 0; n < count; n++) { 35 | const digit = (input[n].key & bitMask) >>> shiftRightAmount 36 | output[endOfBin[digit]++] = input[n] 37 | } 38 | 39 | bitMask <<= radix 40 | shiftRightAmount += radix 41 | outputResult = !outputResult 42 | 43 | let tmp = input 44 | input = output 45 | output = tmp 46 | } 47 | 48 | if(outputResult) { 49 | for(let n = 0; n < output; n++) { 50 | input[n] = output[n] 51 | } 52 | } 53 | } 54 | 55 | export default radixSortLSD -------------------------------------------------------------------------------- /src/StateMachine.js: -------------------------------------------------------------------------------- 1 | 2 | class StateMachine 3 | { 4 | constructor(config) { 5 | this.state = config.state || null 6 | this.states = config.states || {} 7 | this.transitions = config.transitions || { auto: {} } 8 | } 9 | 10 | create(data, prefix = "") { 11 | return new StateMachineState(this, data, this.state, prefix) 12 | } 13 | } 14 | 15 | class StateMachineState 16 | { 17 | constructor(machine, sprite, state, prefix = "") { 18 | this.machine = machine 19 | this.sprite = sprite || null 20 | this.state = null 21 | this.stateConfig = null 22 | this.prefix = prefix 23 | this._transitionFunc = () => { 24 | const state = this.machine.transitions.auto[this.state] 25 | if(state) { 26 | this.execute(state) 27 | } 28 | } 29 | this.execute(state) 30 | } 31 | 32 | execute(state) { 33 | if(this.state === state) { 34 | return 35 | } 36 | 37 | const stateConfig = this.machine.states[state] || null 38 | if(!stateConfig) { 39 | console.warn(`(StateMachineState.execute) Trying to execute invalid state: ${state}`) 40 | return 41 | } 42 | 43 | if(this.state) { 44 | if(this.stateConfig.flipX) { 45 | if(this.stateConfig.flipY) { 46 | this.sprite.scale.set(-this.sprite._scale.x, -this.sprite._scale.y) 47 | } 48 | else { 49 | this.sprite.scale.set(-this.sprite._scale.x, this.sprite._scale.y) 50 | } 51 | } 52 | else if(this.stateConfig.flipY) { 53 | this.sprite.scale.set(this.sprite._scale.x, -this.sprite._scale.y) 54 | } 55 | } 56 | 57 | this.state = state 58 | this.stateConfig = stateConfig 59 | 60 | if(this.state) { 61 | this.sprite.play(`${this.prefix}${stateConfig.animation}`) 62 | if(this.machine.transitions.auto[this.state]) { 63 | this.sprite.onAnimEnd = this._transitionFunc 64 | } 65 | else { 66 | this.sprite.onAnimEnd = null 67 | } 68 | 69 | if(this.stateConfig.flipX) { 70 | if(this.stateConfig.flipY) { 71 | this.sprite.scale.set(-this.sprite._scale.x, -this.sprite._scale.y) 72 | } 73 | else { 74 | this.sprite.scale.set(-this.sprite._scale.x, this.sprite._scale.y) 75 | } 76 | } 77 | else if(this.stateConfig.flipY) { 78 | this.sprite.scale.set(this.sprite._scale.x, -this.sprite._scale.y) 79 | } 80 | } 81 | } 82 | 83 | transition(transitionType = null) { 84 | if(transitionType) { 85 | const transitions = this.machine.transitions[transitionType] 86 | if(!transitions) { 87 | console.warn(`(StateMachineState.transition) Invalid transition type: ${transitionType}`) 88 | } 89 | else { 90 | const state = transitions[this.state] 91 | if(state) { 92 | this.execute(state) 93 | return 94 | } 95 | } 96 | } 97 | 98 | const state = this.machine.transitions.auto[this.state] 99 | if(state) { 100 | this.execute(state) 101 | } 102 | } 103 | } 104 | 105 | export default StateMachine -------------------------------------------------------------------------------- /src/Time.js: -------------------------------------------------------------------------------- 1 | 2 | let timerId = 0 3 | 4 | class Time 5 | { 6 | constructor() 7 | { 8 | this.delta = 0 9 | this.deltaF = 0.0 10 | this.deltaRender = 0 11 | this.deltaRenderF = 0.0 12 | this.totalRender = 0 13 | this.maxDelta = 250.0 14 | this.scale = 1.0 15 | this.fps = 0 16 | this.current = Date.now() 17 | this.prev = this.current 18 | this.currentRender = this.current 19 | this.prevRender = this.current 20 | this.accumulator = 0.0 21 | this.frameIndex = 0 22 | this.updateFreq = 1 / 40 23 | this.alpha = 0 24 | this.ms = 0 25 | 26 | this.timers = [] 27 | this.timersRemove = [] 28 | this.paused = false 29 | this.updating = false 30 | 31 | this._fpsCurrent = this.current 32 | this._fps = 0 33 | } 34 | 35 | start() { 36 | this.current = performance.now() 37 | 38 | if(this.paused) { 39 | this.delta = 0 40 | this.deltaF = 0.0 41 | } 42 | else { 43 | this.delta = this.current - this.prev 44 | if(this.delta > 250) { 45 | this.delta = 250 46 | } 47 | this.delta *= this.scale 48 | this.deltaF = this.delta / 1000 49 | this.accumulator += this.deltaF 50 | } 51 | 52 | this.updating = true 53 | 54 | for(let n = 0; n < this.timers.length; n++) { 55 | this.timers[n].update(this.delta) 56 | } 57 | 58 | this.updating = false 59 | 60 | if(this.timersRemove.length > 0) { 61 | for(let n = 0; n < this.timersRemove.length; n++) { 62 | const timerA = this.timersRemove[n] 63 | const timerB = this.timers[this.timers.length - 1] 64 | timerB.__index = timerA.__index 65 | timerA.__index = -1 66 | this.timers[timerB.__index] = timerB 67 | this.timers.pop() 68 | } 69 | this.timersRemove.length = 0 70 | } 71 | } 72 | 73 | end() { 74 | this.prev = this.current 75 | } 76 | 77 | startRender() { 78 | this.currentRender = Date.now() 79 | this.frameIndex++ 80 | this.alpha = 0 81 | 82 | if(this.paused) { 83 | this.deltaRender = 0 84 | this.deltaRenderF = 0.0 85 | } 86 | else { 87 | this.deltaRender = this.currentRender - this.prevRender 88 | this.totalRender += this.deltaRender 89 | if(this.deltaRender > 250) { 90 | this.deltaRender = 250 91 | } 92 | this.deltaRender *= this.scale 93 | this.deltaRenderF = this.deltaRender / 1000 94 | this.accumulator += this.deltaRenderF 95 | } 96 | 97 | if(this.currentRender - this._fpsCurrent >= 1000) { 98 | this._fpsCurrent = this.currentRender 99 | this.fps = this._fps 100 | this._fps = 0 101 | 102 | this.ms = this.totalRender / this.fps | 0 103 | this.totalRender = 0 104 | } 105 | } 106 | 107 | endRender() { 108 | this._fps++ 109 | this.prevRender = this.currentRender 110 | } 111 | 112 | timer(func, tDelta, numTimes) { 113 | if(!func || !tDelta) { 114 | console.warn("(Timer.create) Invalid params passed") 115 | return null 116 | } 117 | 118 | const timer = new Timer(this, func, tDelta, numTimes) 119 | timer.play() 120 | 121 | return timer 122 | } 123 | } 124 | 125 | class Timer { 126 | constructor(time, func, tDelta, numTimes) { 127 | this.time = time 128 | this.id = timerId++ 129 | this.func = func 130 | this.tDelta = tDelta 131 | this.numTimes = (numTimes !== undefined) ? numTimes : -1 132 | this.initNumTimes = this.numTimes 133 | this.onDone = null 134 | 135 | this.tAccumulator = 0.0 136 | this.tStart = Date.now() 137 | this.paused = false 138 | 139 | this.__index = -1 140 | } 141 | 142 | play() { 143 | if(this.__index !== -1) { return } 144 | this.__index = this.time.timers.push(this) - 1 145 | } 146 | 147 | _stop() { 148 | if(this.__index === -1) { return } 149 | if(this.updating) { 150 | this.time.timersRemove.push(this) 151 | } 152 | else { 153 | const timers = this.time.timers 154 | const timer = timers[timers.length - 1] 155 | timer.__index = this.__index 156 | timers[this.__index] = timer 157 | timers.pop() 158 | } 159 | 160 | this.__index = -1 161 | } 162 | 163 | stop() { 164 | this._stop() 165 | this.paused = false 166 | this.numTimes = 0 167 | 168 | if(this.onDone) { 169 | this.onDone(this) 170 | } 171 | } 172 | 173 | pause() { 174 | this._stop() 175 | this.paused = true 176 | } 177 | 178 | resume() { 179 | if(!this.paused) { return } 180 | this.paused = false 181 | this.tStart = Date.now() 182 | } 183 | 184 | reset() { 185 | this.tAccumulator = 0 186 | this.numTimes = this.initNumTimes 187 | this.paused = false 188 | this.play() 189 | } 190 | 191 | update(tDelta) { 192 | this.tAccumulator += tDelta 193 | while(this.tAccumulator >= this.tDelta) { 194 | this.tAccumulator -= this.tDelta 195 | 196 | if(this.numTimes !== 0) { 197 | this.func(this) 198 | } 199 | 200 | this.tStart += this.tDelta 201 | 202 | if(this.numTimes !== -1) { 203 | this.numTimes-- 204 | if(this.numTimes <= 0) { 205 | this.stop() 206 | } 207 | } 208 | } 209 | } 210 | } 211 | 212 | const instance = new Time() 213 | export default instance -------------------------------------------------------------------------------- /src/Utils.js: -------------------------------------------------------------------------------- 1 | 2 | const isPowerOf2 = (value) => { 3 | return (value & (value - 1)) == 0 4 | } 5 | 6 | const onDomLoad = (func) => { 7 | if((document.readyState === "interactive" || document.readyState === "complete")) { 8 | func() 9 | return 10 | } 11 | 12 | const callbackFunc = (event) => { 13 | func() 14 | window.removeEventListener("DOMContentLoaded", callbackFunc) 15 | } 16 | 17 | window.addEventListener("DOMContentLoaded", callbackFunc) 18 | } 19 | 20 | const getRootPath = (path) => { 21 | const index = path.lastIndexOf("/") 22 | const rootPath = path.slice(0, index + 1) 23 | return rootPath 24 | } 25 | 26 | const getExt = (path) => { 27 | const index = path.lastIndexOf(".") 28 | const ext = path.slice(index + 1) 29 | return ext 30 | } 31 | 32 | export { 33 | isPowerOf2, 34 | onDomLoad, 35 | getRootPath, 36 | getExt 37 | } -------------------------------------------------------------------------------- /src/entity/AnimatedSprite.js: -------------------------------------------------------------------------------- 1 | import Sprite from "./Sprite" 2 | import Time from "../Time" 3 | import Resources from "../resources/Resources" 4 | 5 | class AnimatedSprite extends Sprite { 6 | constructor(texture) { 7 | super(texture) 8 | this._frameIndex = 0 9 | this.tFrame = 0 10 | this.tFrameDelay = 0 11 | this.speed = 0 12 | this.loop = false 13 | this.reverse = false 14 | this.pauseLastFrame = false 15 | this.onAnimEnd = null 16 | } 17 | 18 | draw() { 19 | this.updateAnim() 20 | super.draw() 21 | } 22 | 23 | updateAnim() { 24 | if(this.speed === 0) { return } 25 | if(this.tFrameDelay === 0) { 26 | console.warn(`(AnimatedSprite.updateAnim) Frame delay is zero: ${this}`) 27 | return 28 | } 29 | 30 | const maxFrames = this.texture.frames.length 31 | 32 | this.tFrame += Time.deltaRender * this.speed 33 | 34 | while(this.tFrame > this.tFrameDelay) { 35 | this.tFrame -= this.tFrameDelay 36 | 37 | if(this.reverse) { 38 | this._frameIndex-- 39 | if(this._frameIndex === -1) { 40 | if(this.loop) { 41 | this._frameIndex = maxFrames - 1 42 | } 43 | else if(this.pauseLastFrame) { 44 | this._frameIndex = 0 45 | this.speed = 0 46 | } 47 | else { 48 | this._frameIndex = 0 49 | this.speed = 0 50 | } 51 | if(this.onAnimEnd) { 52 | this.onAnimEnd() 53 | } 54 | } 55 | } 56 | else { 57 | this._frameIndex++ 58 | if(this._frameIndex >= maxFrames) { 59 | if(this.loop) { 60 | this._frameIndex = 0 61 | } 62 | else if(this.pauseLastFrame) { 63 | this._frameIndex = maxFrames - 1 64 | this.speed = 0 65 | } 66 | else { 67 | this._frameIndex = 0 68 | this.speed = 0 69 | } 70 | if(this.onAnimEnd) { 71 | this.onAnimEnd() 72 | } 73 | } 74 | } 75 | 76 | this.frame = this.texture.getFrame(this._frameIndex) 77 | this.tFrameDelay = this.frame.delay 78 | } 79 | } 80 | 81 | play(animationId, loop, speed, reverse) { 82 | const newAnimation = Resources.get(animationId) 83 | if(!newAnimation) { 84 | console.error(`(AnimatedSprite.play) No such animation found: ${animationId}`) 85 | return 86 | } 87 | if(this.texture === newAnimation) { 88 | return 89 | } 90 | this.texture = newAnimation 91 | 92 | this.loop = loop || true 93 | this.speed = speed || 1 94 | this.reverse = reverse || false 95 | this.pauseLastFrame = this.texture.pauseLastFrame 96 | if(reverse) { 97 | this.frameIndex = this.texture.frames.length - 1 98 | } 99 | else { 100 | this.frameIndex = 0 101 | } 102 | } 103 | 104 | stop() { 105 | this.speed = 0 106 | if(reverse) { 107 | this.frameIndex = this.texture.frames.length - 1 108 | } 109 | else { 110 | this.frameIndex = 0 111 | } 112 | } 113 | 114 | set frameIndex(frameIndex) { 115 | this._frameIndex = frameIndex 116 | this.frame = this.texture.getFrame(this._frameIndex) 117 | this.tFrameDelay = this.frame.delay 118 | } 119 | 120 | get frameIndex() { 121 | return this._frameIndex 122 | } 123 | } 124 | 125 | export default AnimatedSprite -------------------------------------------------------------------------------- /src/entity/BitmapText.js: -------------------------------------------------------------------------------- 1 | import Sprite from "./Sprite" 2 | import Resources from "../resources/Resources" 3 | 4 | const generateIndices = (indices, offset) => { 5 | let indiceOffset = offset 6 | for(let n = (offset / 6) * 4; n < indices.length; n += 4) { 7 | indices[indiceOffset++] = n 8 | indices[indiceOffset++] = n + 2 9 | indices[indiceOffset++] = n + 1 10 | 11 | indices[indiceOffset++] = n 12 | indices[indiceOffset++] = n + 3 13 | indices[indiceOffset++] = n + 2 14 | } 15 | } 16 | 17 | class BitmapText extends Sprite 18 | { 19 | constructor(font, text = "") { 20 | super() 21 | this._font = null 22 | this._text = text 23 | this._lineBuffer = new Array(1) 24 | this._wordBuffer = null 25 | this._limitWidth = 0 26 | this.buffer = null 27 | this.indices = null 28 | 29 | if(font) { 30 | this.font = font 31 | } 32 | if(text) { 33 | this.text = text 34 | } 35 | } 36 | 37 | updateMesh() { 38 | this._maxWidth = 0 39 | 40 | const newlineIndex = this._text.indexOf("\n") 41 | if(newlineIndex !== -1) { 42 | this._lineBuffer = this._text.split("\n") 43 | } 44 | else { 45 | this._lineBuffer.length = 1 46 | this._lineBuffer[0] = this._text 47 | } 48 | 49 | if(this._limitWidth > 0) { 50 | this.calcLineBuffer() 51 | } 52 | 53 | const numChars = this._text.length - (this._lineBuffer.length - 1) 54 | if(!this.buffer) { 55 | this.buffer = new Float32Array(16 * numChars) 56 | this.indices = new Uint16Array(6 * numChars) 57 | generateIndices(this.indices, 0) 58 | this.drawCommand.mesh.uploadIndices(this.indices) 59 | } 60 | else { 61 | const newSize = numChars * 16 62 | if(this.buffer.length < newSize) { 63 | this.buffer = new Float32Array(newSize) 64 | const newIndices = new Uint16Array(numChars * 6) 65 | newIndices.set(this.indices, 0) 66 | generateIndices(newIndices, this.indices.length) 67 | this.indices = newIndices 68 | this.drawCommand.mesh.uploadIndices(this.indices) 69 | } 70 | } 71 | 72 | let x = 0 73 | let y = 0 74 | let bufferOffset = 0 75 | let prevChar = 0 76 | let sizeX = 0 77 | let sizeY = 0 78 | 79 | for(let a = 0; a < this._lineBuffer.length; a++) { 80 | const line = this._lineBuffer[a] 81 | for(let i = 0; i < line.length; i++) { 82 | const nextChar = line.charCodeAt(i) 83 | const frame = this._font.getFrame(nextChar) 84 | if(!frame) { continue } 85 | 86 | x += this._font.getKerning(prevChar, nextChar) 87 | 88 | this.buffer.set(frame.coords, bufferOffset) 89 | this.buffer[bufferOffset] += x 90 | this.buffer[bufferOffset + 1] += y 91 | this.buffer[bufferOffset + 4] += x 92 | this.buffer[bufferOffset + 5] += y 93 | this.buffer[bufferOffset + 8] += x 94 | this.buffer[bufferOffset + 9] += y 95 | this.buffer[bufferOffset + 12] += x 96 | this.buffer[bufferOffset + 13] += y 97 | 98 | if(this.buffer[bufferOffset + 1] > sizeY) { 99 | sizeY = this.buffer[bufferOffset + 1] 100 | } 101 | 102 | x += this._font.kerning[nextChar] - 1 103 | bufferOffset += 16 104 | prevChar = nextChar 105 | } 106 | if(sizeX < x) { 107 | sizeX = x 108 | } 109 | x = 0 110 | y += this._font.lineHeight 111 | prevChar = 0 112 | } 113 | 114 | this.drawCommand.mesh.numElements = numChars * 6 115 | this.drawCommand.mesh.upload(this.buffer) 116 | this.size.set(sizeX + 1, sizeY) 117 | this.needUpdateMesh = false 118 | } 119 | 120 | calcLineBuffer() { 121 | const lineBuffer = [] 122 | let text = "" 123 | let width = 0 124 | 125 | this._wordBuffer.length = this._lineBuffer.length 126 | for(let n = 0; n < this._lineBuffer.length; n++) { 127 | this._wordBuffer[n] = this._lineBuffer[n].split(" ") 128 | } 129 | 130 | for(let n = 0; n < this._lineBuffer.length; n++) { 131 | const words = this._wordBuffer[n] 132 | 133 | if(words.length === 1) { 134 | const word = words[0] 135 | const wordWidth = this.wordWidth(word) 136 | if(wordWidth > this._maxWidth) { 137 | this._maxWidth = wordWidth 138 | } 139 | lineBuffer.push(word) 140 | } 141 | else { 142 | for(let i = 0; i < words.length; i++) { 143 | const word = words[i] 144 | 145 | let wordWidth 146 | if(text) { 147 | wordWidth = this.wordWidth(" " + word) 148 | } 149 | else { 150 | wordWidth = this.wordWidth(word) 151 | } 152 | 153 | if((width + wordWidth) > this._limitWidth) { 154 | lineBuffer.push(text); 155 | if(this._maxWidth < width) { 156 | this._maxWidth = width 157 | } 158 | 159 | if(i === (words.length - 1)) { 160 | lineBuffer.push(word) 161 | if(this._maxWidth < wordWidth) { 162 | this._maxWidth = wordWidth 163 | } 164 | 165 | width = 0 166 | text = null 167 | } 168 | else { 169 | text = word 170 | width = wordWidth 171 | } 172 | } 173 | else { 174 | if(text) { 175 | text += " " + word 176 | } 177 | else { 178 | text = word 179 | } 180 | 181 | if(i === (words.length - 1)) { 182 | lineBuffer.push(text) 183 | if(this._maxWidth < (width + wordWidth)) { 184 | this._maxWidth = width + wordWidth 185 | } 186 | text = null 187 | width = 0 188 | } 189 | else { 190 | width += wordWidth 191 | } 192 | } 193 | } 194 | } 195 | } 196 | 197 | this._lineBuffer = lineBuffer 198 | } 199 | 200 | wordWidth(word) { 201 | let width = 0 202 | let prevChar = 0 203 | for(let n = 0; n < word.length; n++) { 204 | const nextChar = word.charCodeAt(n) 205 | width += this._font.getKerning(prevChar, nextChar) 206 | width += this._font.kerning[nextChar] 207 | prevChar = nextChar 208 | } 209 | return width 210 | } 211 | 212 | set text(text) { 213 | if(this._text === text) { return } 214 | this._text = text 215 | this.needUpdateMesh = true 216 | } 217 | 218 | get text() { 219 | return this._text 220 | } 221 | 222 | set font(font) { 223 | if(typeof font === "string") { 224 | const newFont = Resources.get(font) 225 | if(newFont === this._font) { return } 226 | this._font = newFont 227 | this.drawCommand.uniforms.albedo = newFont.instance 228 | } 229 | else { 230 | if(this._font === font) { return } 231 | this._font = font 232 | } 233 | this.needUpdateMesh = true 234 | } 235 | 236 | get font() { 237 | return this._font 238 | } 239 | 240 | set limitWidth(value) { 241 | if(this._limitWidth === value) { return } 242 | this._limitWidth = value 243 | if(value === 0) { 244 | this._wordBuffer = null 245 | } 246 | else { 247 | this._wordBuffer = [] 248 | } 249 | this.needUpdateMesh = true 250 | } 251 | 252 | get limitWidth() { 253 | return this._limitWidth 254 | } 255 | } 256 | 257 | export default BitmapText -------------------------------------------------------------------------------- /src/entity/Camera.js: -------------------------------------------------------------------------------- 1 | import Entity from "./Entity" 2 | import Engine from "../Engine" 3 | import Matrix3 from "../math/Matrix3" 4 | 5 | class Camera extends Entity 6 | { 7 | constructor() { 8 | super() 9 | this.projectionTransform = new Matrix3() 10 | this.cullMask = 0 11 | } 12 | 13 | updateVolume() { 14 | super.updateVolume() 15 | this.updateProjectionTransform() 16 | } 17 | 18 | updateProjectionTransform() { 19 | this.projectionTransform.identity() 20 | this.projectionTransform.projection(Engine.window.width, Engine.window.height) 21 | } 22 | 23 | setCullMask(layer, active = true) { 24 | if(active) { 25 | this.cullMask = this.cullMask | 1 << layer 26 | } 27 | else { 28 | this.cullMask = this.cullMask & ~(1 << layer) 29 | } 30 | } 31 | 32 | getCullMask(layer) { 33 | return ((this.cullMask >> layer) % 2 != 0) 34 | } 35 | 36 | updateTransform() { 37 | super.updateTransform() 38 | this._transform.m[6] = (this._transform.m[6] * -1 * this._scale.x) 39 | this._transform.m[7] = (this._transform.m[7] * -1 * this._scale.y) 40 | // if(this._transform.m[6] > 0) { 41 | // this._transform.m[6] = 0 42 | // } 43 | // if(this._transform.m[7] > 0) { 44 | // this._transform.m[7] = 0 45 | // } 46 | } 47 | } 48 | 49 | export default Camera -------------------------------------------------------------------------------- /src/entity/Entity.js: -------------------------------------------------------------------------------- 1 | import Engine from "../Engine" 2 | import Vector2 from "../math/Vector2" 3 | import Matrix3 from "../math/Matrix3" 4 | import AABB from "../math/AABB" 5 | 6 | class Entity 7 | { 8 | constructor() { 9 | this._volume = new AABB() 10 | this._transform = new Matrix3() 11 | this._position = new Vector2(0, 0) 12 | this._scale = new Vector2(1, 1) 13 | this._size = new Vector2(0, 0) 14 | this._pivot = new Vector2(0, 0) 15 | this._anchor = null 16 | this._rotation = 0 17 | 18 | this.parent = null 19 | this.children = null 20 | this.components = null 21 | 22 | this.enabled = false 23 | this.removed = false 24 | this._needUpdateTransform = true 25 | this.needUpdateVolume = true 26 | this.needUpdateFrame = true 27 | this.debug = false 28 | this.hidden = false 29 | } 30 | 31 | remove() { 32 | this.enable = false 33 | } 34 | 35 | draw() {} 36 | 37 | onEnable() { 38 | if(this.update) { 39 | Engine.addUpdating(this) 40 | } 41 | if(this.components) { 42 | for(let n = 0; n < this.components.length; n++) { 43 | const component = this.components[n] 44 | component.onEnable() 45 | if(component.update) { 46 | Engine.addUpdatingComponent(component) 47 | } 48 | } 49 | } 50 | } 51 | 52 | onDisable() { 53 | if(this.update) { 54 | Engine.removeUpdating(this) 55 | } 56 | if(this.components) { 57 | for(let n = 0; n < this.components.length; n++) { 58 | const component = this.components[n] 59 | component.onDisable() 60 | if(component.update) { 61 | Engine.removeUpdatingComponent(component) 62 | } 63 | } 64 | } 65 | } 66 | 67 | get position() { 68 | this.needUpdateTransform = true 69 | return this._position 70 | } 71 | 72 | set x(x) { 73 | this._position.x = x 74 | this.needUpdateTransform = true 75 | } 76 | 77 | set y(y) { 78 | this._position.y = y 79 | this.needUpdateTransform = true 80 | } 81 | 82 | get x() { 83 | return this._position.x 84 | } 85 | 86 | get y() { 87 | return this._position.y 88 | } 89 | 90 | move(x, y) { 91 | this._position.x += x 92 | this._position.y += y 93 | this.needUpdateTransform = true 94 | } 95 | 96 | set rotation(value) { 97 | this._rotation = (value * Math.PI) / 180 98 | this._transform.cos = Math.cos(this._rotation) 99 | this._transform.sin = Math.sin(this._rotation) 100 | this.needUpdateTransform = true 101 | } 102 | 103 | set rotationRad(value) { 104 | this._rotation = value 105 | this._transform.cos = Math.cos(this._rotation) 106 | this._transform.sin = Math.sin(this._rotation) 107 | this.needUpdateTransform = true 108 | } 109 | 110 | get rotation() { 111 | return (this._rotation * 180) / Math.PI 112 | } 113 | 114 | get rotationRad() { 115 | return this._rotation 116 | } 117 | 118 | get scale() { 119 | this.needUpdateTransform = true 120 | return this._scale 121 | } 122 | 123 | get size() { 124 | this.deepNeedUpdateTransform() 125 | this.needUpdateVolume = true 126 | return this._size 127 | } 128 | 129 | get width() { 130 | return this._size.x 131 | } 132 | 133 | get height() { 134 | return this._size.y 135 | } 136 | 137 | get anchor() { 138 | if(!this._anchor) { 139 | this._anchor = new Vector2(0, 0) 140 | } 141 | this.needUpdateTransform = true 142 | return this._anchor 143 | } 144 | 145 | get pivot() { 146 | this.needUpdateFrame = true 147 | return this._pivot 148 | } 149 | 150 | addChild(entity) { 151 | if(entity.parent) { 152 | console.warn(`(Entity.addChild) Entity already has parent: ${entity.parent}`) 153 | return false 154 | } 155 | 156 | if(!this.children) { 157 | this.children = [ entity ] 158 | } 159 | else { 160 | this.children.push(entity) 161 | } 162 | 163 | entity.parent = this 164 | entity.enable = this.enabled 165 | entity.needUpdateTransform = true 166 | entity.setLayer(this._layer) 167 | 168 | return true 169 | } 170 | 171 | removeChild(entity) { 172 | if(!this.children) { return false } 173 | 174 | if(entity.parent !== this) { 175 | console.warn(`(Entity.removeChild) Entity has different parent: ${entity.parent}`) 176 | return false 177 | } 178 | 179 | const index = this.children.indexOf(entity) 180 | if(index === -1) { return } 181 | 182 | this.children[index] = this.children[this.children.length - 1] 183 | this.children.pop() 184 | 185 | entity.parent = null 186 | entity.enable = false 187 | entity.needUpdateTransform = true 188 | 189 | return true 190 | } 191 | 192 | detach() 193 | { 194 | if(!this.parent) { return } 195 | 196 | this.parent.removeChild(this) 197 | } 198 | 199 | set enable(value) 200 | { 201 | if(this.enabled === value) { return } 202 | this.enabled = value 203 | 204 | if(value) { 205 | this.onEnable() 206 | } 207 | else { 208 | this.onDisable() 209 | } 210 | 211 | if(this.children) { 212 | for(let n = 0; n < this.children.length; n++) { 213 | this.children[n].enable = value 214 | } 215 | } 216 | } 217 | 218 | updateTransform() { 219 | const m = this._transform.m 220 | const a = this._transform.cos * this._scale.x 221 | const b = this._transform.sin * this._scale.x 222 | const c = -this._transform.sin * this._scale.y 223 | const d = this._transform.cos * this._scale.y 224 | let tx = this._position.x - ((this._pivot.x * a * this._size.x) + (this.pivot.y * c * this._size.y)) 225 | let ty = this._position.y - ((this._pivot.x * b * this._size.x) + (this.pivot.y * d * this._size.y)) 226 | 227 | if(this.parent) { 228 | if(this._anchor) { 229 | tx += this.parent._size.x * this._anchor.x 230 | ty += this.parent._size.y * this._anchor.y 231 | } 232 | const pm = this.parent.transform.m 233 | m[0] = (a * pm[0]) + (b * pm[3]) 234 | m[1] = (a * pm[1]) + (b * pm[4]) 235 | m[3] = (c * pm[0]) + (d * pm[3]) 236 | m[4] = (c * pm[1]) + (d * pm[4]) 237 | m[6] = (tx * pm[0]) + (ty * pm[3]) + pm[6] 238 | m[7] = (tx * pm[1]) + (ty * pm[4]) + pm[7] 239 | } 240 | else { 241 | m[0] = a 242 | m[1] = b 243 | m[3] = c 244 | m[4] = d 245 | m[6] = tx 246 | m[7] = ty 247 | } 248 | 249 | this._needUpdateTransform = false 250 | this.needUpdateVolume = true 251 | } 252 | 253 | set needUpdateTransform(value) { 254 | if(this._needUpdateTransform === value) { return } 255 | this._needUpdateTransform = value 256 | if(value && this.children) { 257 | for(let n = 0; n < this.children.length; n++) { 258 | this.children[n].needUpdateTransform = true 259 | } 260 | } 261 | } 262 | 263 | get needUpdateTransform() { 264 | return this._needUpdateTransform 265 | } 266 | 267 | get transform() { 268 | if(this.needUpdateTransform) { 269 | this.updateTransform() 270 | } 271 | return this._transform 272 | } 273 | 274 | updateVolume() { 275 | this._volume.set(this._transform.m[6], this._transform.m[7], 276 | this._size.x * this._scale.x, this._size.y * this._scale.y) 277 | this.needUpdateVolume = false 278 | } 279 | 280 | get volume() { 281 | if(this.needUpdateVolume) { 282 | this.updateVolume() 283 | } 284 | return this._volume 285 | } 286 | 287 | deepNeedUpdateTransform() { 288 | this.needUpdateTransform = true 289 | if(this.children) { 290 | for(let n = 0; n < this.children.length; n++) { 291 | this.children[n].deepNeedUpdateTransform() 292 | } 293 | } 294 | } 295 | 296 | addComponent(componentCls) 297 | { 298 | const component = new componentCls() 299 | component.parent = this 300 | 301 | if(!this.components) { 302 | this.components = [ component ] 303 | } 304 | else { 305 | this.components.push(component) 306 | } 307 | 308 | if(this.enabled) { 309 | component.onEnable() 310 | 311 | if(component.update) { 312 | Engine.addUpdatingComponent(component) 313 | } 314 | } 315 | 316 | return component 317 | } 318 | 319 | removeComponent(component) 320 | { 321 | if(!this.components) { return } 322 | 323 | const index = this.components.indexOf(component) 324 | if(index === -1) { return } 325 | 326 | component.remove() 327 | this.components[index] = this.components[this.components.length - 1] 328 | this.components.pop() 329 | 330 | component.onDisable() 331 | 332 | if(this.enabled) { 333 | if(component.update) { 334 | Engine.removeUpdatingComponent(component) 335 | } 336 | } 337 | } 338 | 339 | removeComponents() 340 | { 341 | if(!this.components) { return } 342 | 343 | for(let n = 0; n < this.components.length; n++) { 344 | this.components[n].remove() 345 | } 346 | this.components.length 347 | } 348 | } 349 | 350 | export default Entity -------------------------------------------------------------------------------- /src/entity/Renderable.js: -------------------------------------------------------------------------------- 1 | import Entity from "./Entity" 2 | import Engine from "../Engine" 3 | import Mesh from "../mesh/Mesh" 4 | import Resources from "../resources/Resources" 5 | import Stage from "../renderer/Stage" 6 | import DrawCommand from "../renderer/DrawCommand" 7 | 8 | class Renderable extends Entity 9 | { 10 | constructor(mesh) { 11 | super() 12 | if(!mesh) { 13 | mesh = new Mesh(null, null) 14 | } 15 | this.needUpdateMesh = true 16 | this.hidden = false 17 | this.drawCommand = new DrawCommand(this._transform, mesh, null, null) 18 | } 19 | 20 | draw() { 21 | if(this.hidden || !this.drawCommand.material) { 22 | return 23 | } 24 | if(this.needUpdateMesh) { 25 | this.updateMesh() 26 | } 27 | if(this.needUpdateTransform) { 28 | this.updateTransform() 29 | } 30 | Engine.renderer.draw(this.drawCommand) 31 | if(this.debug) { 32 | Engine.renderer.drawDebug(this.transform, this.volume, this._pivot) 33 | } 34 | } 35 | 36 | updateMesh() { 37 | this.needUpdateMesh = false 38 | } 39 | 40 | updateUniforms() { 41 | this.drawCommand.uniforms = Object.assign({}, this.drawCommand.material.uniforms) 42 | } 43 | 44 | set material(material) { 45 | if(typeof material === "string") { 46 | const newMaterial = Resources.get(material) 47 | if(!newMaterial) { 48 | console.warn(`(Sprite.material) Could not find resource with id: ${material}`) 49 | } 50 | else { 51 | this.drawCommand.material = material 52 | } 53 | } 54 | else { 55 | this.drawCommand.material = material 56 | } 57 | 58 | this.updateUniforms() 59 | } 60 | 61 | get material() { 62 | return this.drawCommand.material 63 | } 64 | 65 | set z(z) { 66 | this.drawCommand.key = z 67 | } 68 | 69 | get z() { 70 | return this.drawCommand.key 71 | } 72 | 73 | setLayer(layerId, recursive = true) { 74 | this.drawCommand.layer = layerId 75 | if(recursive && this.children) { 76 | for(let n = 0; n < this.children.length; n++) { 77 | this.children[n].setLayer(layerId, true) 78 | } 79 | } 80 | } 81 | 82 | getLayer() { 83 | return this.drawCommand.layer 84 | } 85 | 86 | onEnable() { 87 | Stage.add(this) 88 | super.onEnable() 89 | } 90 | 91 | onDisable() { 92 | Stage.remove(this) 93 | super.onDisable() 94 | } 95 | } 96 | 97 | export default Renderable -------------------------------------------------------------------------------- /src/entity/Sprite.js: -------------------------------------------------------------------------------- 1 | import Engine from "../Engine" 2 | import Renderable from "./Renderable" 3 | import Vector4 from "../math/Vector4" 4 | import Resources from "../resources/Resources" 5 | import Material from "../resources/Material" 6 | import Mesh from "../mesh/Mesh" 7 | import spriteVertexSrc from "../../shaders/sprite.vertex.glsl" 8 | import spriteFragmentSrc from "../../shaders/sprite.fragment.glsl" 9 | 10 | let spriteMaterial = null 11 | Engine.on("setup", () => { 12 | spriteMaterial = new Material() 13 | spriteMaterial.loadFromConfig({ 14 | vertexSrc: spriteVertexSrc, 15 | fragmentSrc: spriteFragmentSrc 16 | }) 17 | }) 18 | 19 | class Sprite extends Renderable { 20 | constructor(texture) { 21 | super(null) 22 | this._texture = null 23 | this._frame = null 24 | this.color = new Vector4(1, 1, 1, 1) 25 | 26 | this.material = spriteMaterial 27 | this.handleTextureFunc = this.handleTexture.bind(this) 28 | if(texture) { 29 | this.texture = texture 30 | } 31 | } 32 | 33 | onDisable() { 34 | super.onDisable() 35 | if(this.texture) { 36 | this.texture.unwatch(this.handleTextureFunc) 37 | } 38 | } 39 | 40 | updateMesh() { 41 | if(this._frame) { 42 | this.drawCommand.mesh.upload(this._frame.coords) 43 | } 44 | else { 45 | this.drawCommand.mesh.upload(Mesh.defaultBuffer) 46 | } 47 | this.needUpdateMesh = false 48 | } 49 | 50 | set texture(texture) { 51 | if(this._texture) { 52 | this.texture.unwatch(this.handleTextureFunc) 53 | } 54 | 55 | let frameName = null 56 | 57 | if(typeof texture === "string") { 58 | let newTexture 59 | 60 | const index = texture.indexOf("/") 61 | if(index === -1) { 62 | newTexture = Resources.get(texture) 63 | } 64 | else { 65 | const textureInfo = texture.split("/") 66 | newTexture = Resources.get(textureInfo[0]) 67 | frameName = textureInfo[1] 68 | } 69 | 70 | if(!newTexture) { 71 | console.warn(`(Sprite.texture) Could not find resource with id: ${texture}`) 72 | this._texture = null 73 | } 74 | else { 75 | this._texture = newTexture 76 | } 77 | } 78 | else { 79 | this._texture = texture 80 | } 81 | 82 | if(this._texture) { 83 | this._texture.watch(this.handleTextureFunc) 84 | if(this._texture.loaded) { 85 | this.frame = this._texture.getFrame(frameName || 0) 86 | } 87 | } 88 | else { 89 | this.size.set(0, 0) 90 | } 91 | } 92 | 93 | get texture() { 94 | return this._texture 95 | } 96 | 97 | set frame(frame) { 98 | if(this._frame === frame) { return } 99 | this._frame = frame 100 | this.needUpdateMesh = true 101 | 102 | if(frame) { 103 | this.drawCommand.uniforms.albedo = this._frame.texture.getInstance() 104 | this.size.set(frame.coords[0], frame.coords[1]) 105 | } 106 | else { 107 | this.drawCommand.uniforms.albedo = null 108 | this.size.set(0, 0) 109 | } 110 | } 111 | 112 | get frame() { 113 | return this._frame 114 | } 115 | 116 | updateUniforms() { 117 | this.drawCommand.uniforms = Object.assign({ 118 | color: this.color 119 | }, this.drawCommand.material.uniforms) 120 | } 121 | 122 | set alpha(value) { 123 | this.color.w = value 124 | } 125 | 126 | get alpha() { 127 | return this.color.w 128 | } 129 | 130 | handleTexture(event, texture) { 131 | this.frame = texture.getFrame(0) 132 | } 133 | } 134 | 135 | export default Sprite -------------------------------------------------------------------------------- /src/entity/Text.js: -------------------------------------------------------------------------------- 1 | import Sprite from "./Sprite" 2 | import Graphics from "../resources/Graphics" 3 | import Texture from "../resources/Texture" 4 | import Engine from "../Engine" 5 | 6 | class Text extends Sprite 7 | { 8 | constructor(text) { 9 | super() 10 | 11 | this._lineBuffer = new Array(1) 12 | this._wordBuffer = null 13 | this._text = "" 14 | this._limitWidth = 0 15 | this._maxWidth = 0 16 | 17 | this._font = "Tahoma" 18 | this._fontSize = 12 19 | this._fontSizePx = "12px" 20 | this._fontColor = "#000" 21 | this._style = "" 22 | 23 | this._outline = false 24 | this._outlineColor = "#000" 25 | this._outlineWidth = 1 26 | 27 | this._shadow = false 28 | this._shadowColor = "#000" 29 | this._shadowBlur = 3 30 | this._shadowOffsetX = 0 31 | this._shadowOffsetY = 0 32 | 33 | this.texture = new Graphics() 34 | this.texture._minFilter = Texture.NEAREST 35 | this.texture._magFilter = Texture.NEAREST 36 | 37 | if(text !== undefined) { 38 | this.text = text 39 | } 40 | } 41 | 42 | updateMesh() 43 | { 44 | const canvas = this._texture.canvas 45 | const ctx = this._texture.ctx 46 | 47 | ctx.font = this._style + " " + this._fontSizePx + " " + this._font 48 | 49 | const newlineIndex = this._text.indexOf("\n") 50 | if(newlineIndex !== -1) { 51 | this._lineBuffer = this._text.split("\n") 52 | } 53 | else { 54 | this._lineBuffer.length = 1 55 | this._lineBuffer[0] = this._text 56 | } 57 | 58 | this._maxWidth = 0 59 | if(this._limitWidth > 0) { 60 | this.calcLineBuffer() 61 | } 62 | else { 63 | for(let n = 0; n < this._lineBuffer.length; n++) { 64 | const metrics = ctx.measureText(this._lineBuffer[n]) 65 | if(metrics.width > this._maxWidth) { 66 | this._maxWidth = metrics.width 67 | } 68 | } 69 | } 70 | 71 | let posX = 0 72 | let posY = 0 73 | 74 | if(this._shadow) { 75 | this._maxWidth += this._shadowBlur * 2 76 | posX += this._shadowBlur 77 | } 78 | 79 | const fontHeight = this._fontSize * 1.3 80 | const width = Math.ceil(this._maxWidth) 81 | const height = Math.ceil(fontHeight * this._lineBuffer.length) 82 | this.size.set(width, height) 83 | this._texture.resize(width, height) 84 | 85 | ctx.clearRect(0, 0, this._texture.width, this._texture.height) 86 | ctx.font = this._style + " " + this._fontSizePx + " " + this._font 87 | ctx.fillStyle = this._fontColor 88 | ctx.textBaseline = "top" 89 | 90 | if(this._shadow) { 91 | ctx.shadowColor = this._shadowColor 92 | ctx.shadowOffsetX = this._shadowOffsetX 93 | ctx.shadowOffsetY = this._shadowOffsetY 94 | ctx.shadowBlur = this._shadowBlur 95 | } 96 | if(this._outline) { 97 | ctx.lineWidth = this._outlineWidth 98 | ctx.strokeStyle = this._outlineColor 99 | } 100 | 101 | for(let n = 0; n < this._lineBuffer.length; n++) { 102 | if(this._outline) { 103 | ctx.strokeText(this._lineBuffer[n], posX, posY) 104 | } 105 | 106 | ctx.fillText(this._lineBuffer[n], posX, posY) 107 | 108 | posY += fontHeight 109 | } 110 | 111 | if(width > 0) { 112 | this._texture.needUpdate = true 113 | this._frame = null 114 | this.frame = this._texture.getFrame("0") 115 | super.updateMesh() 116 | } 117 | else { 118 | this._size.set(0, height) 119 | } 120 | this.needUpdateMesh = false 121 | } 122 | 123 | calcLineBuffer() { 124 | const lineBuffer = [] 125 | const ctx = this._texture.ctx 126 | let text = "" 127 | let width = 0 128 | 129 | this._wordBuffer.length = this._lineBuffer.length 130 | for(let n = 0; n < this._lineBuffer.length; n++) { 131 | this._wordBuffer[n] = this._lineBuffer[n].split(" ") 132 | } 133 | 134 | for(let n = 0; n < this._lineBuffer.length; n++) { 135 | const words = this._wordBuffer[n] 136 | 137 | if(words.length === 1) { 138 | const word = words[0] 139 | const metrics = ctx.measureText(word) 140 | 141 | if(metrics.width > this._maxWidth) { 142 | this._maxWidth = metrics.width 143 | } 144 | lineBuffer.push(word) 145 | } 146 | else 147 | { 148 | for(let i = 0; i < words.length; i++) { 149 | const word = words[i] 150 | 151 | let metrics 152 | if(text) { 153 | metrics = ctx.measureText(" " + word) 154 | } 155 | else { 156 | metrics = ctx.measureText(word) 157 | } 158 | 159 | if((width + metrics.width) > this._limitWidth) { 160 | lineBuffer.push(text); 161 | if(this._maxWidth < width) { 162 | this._maxWidth = width 163 | } 164 | 165 | if(i === (words.length - 1)) { 166 | lineBuffer.push(word) 167 | if(this._maxWidth < metrics.width) { 168 | this._maxWidth = metrics.width 169 | } 170 | 171 | width = 0 172 | text = null 173 | } 174 | else { 175 | text = word 176 | width = metrics.width 177 | } 178 | } 179 | else 180 | { 181 | if(text) { 182 | text += " " + word 183 | } 184 | else { 185 | text = word 186 | } 187 | 188 | if(i === (words.length - 1)) { 189 | lineBuffer.push(text) 190 | if(this._maxWidth < (width + metrics.width)) { 191 | this._maxWidth = width + metrics.width 192 | } 193 | 194 | text = null 195 | width = 0 196 | } 197 | else { 198 | width += metrics.width 199 | } 200 | } 201 | } 202 | } 203 | } 204 | 205 | this._lineBuffer = lineBuffer 206 | } 207 | 208 | set text(text) 209 | { 210 | if(text === undefined) { 211 | this._text = "" 212 | this._lineBuffer[0] = this._text 213 | } 214 | else { 215 | if(typeof(text) === "number") { 216 | this._text = text + "" 217 | this._lineBuffer[0] = this._text 218 | } 219 | else { 220 | this._text = text 221 | } 222 | } 223 | 224 | this.needUpdateMesh = true 225 | } 226 | 227 | get text() { 228 | return this._text 229 | } 230 | 231 | set font(font) { 232 | if(this._font === font) { return } 233 | this._font = font 234 | this.needUpdateMesh = true 235 | } 236 | 237 | get font() { 238 | return this._font 239 | } 240 | 241 | set fontSize(size) { 242 | if(this._fontSize === size) { return } 243 | this._fontSize = size 244 | this._fontSizePx = `${size}px` 245 | this.needUpdateMesh = true 246 | } 247 | 248 | get fontSize() { 249 | return this._fontSize 250 | } 251 | 252 | set fontColor(fontColor) { 253 | if(this._fontColor === fontColor) { return } 254 | this._fontColor = fontColor 255 | this.needUpdateMesh = true 256 | } 257 | 258 | get fontColor() { 259 | return this._fontColor 260 | } 261 | 262 | set style(style) { 263 | if(this._style === style) { return } 264 | this._style = style 265 | this.needUpdateMesh = true 266 | } 267 | 268 | get style() { 269 | return this._style 270 | } 271 | 272 | set outlineColor(color) { 273 | if(this._outlineColor === color) { return } 274 | this._outlineColor = color 275 | this._outline = true 276 | this.needUpdateMesh = true 277 | } 278 | 279 | get outlineColor() { 280 | return this._outlineColor 281 | } 282 | 283 | set outlineWidth(width) { 284 | if(this._outlineWidth === width) { return } 285 | this._outlineWidth = width 286 | this._outline = true 287 | this.needUpdateMesh = true 288 | } 289 | 290 | get outlineWidth() { 291 | return this._outlineWidth 292 | } 293 | 294 | set outline(value) { 295 | if(this._outline === value) { return } 296 | this._outline = value 297 | this.needUpdateMesh = true 298 | } 299 | 300 | get outline() { 301 | return this._outline 302 | } 303 | 304 | set shadow(shadow) { 305 | if(this._shadow === shadow) { return } 306 | this._shadow = shadow 307 | this.needUpdateMesh = true 308 | } 309 | 310 | get shadow() { 311 | return this._shadow 312 | } 313 | 314 | set shadowColor(color) { 315 | if(this._shadowColor === shadowColor) { return } 316 | this._shadowColor = color 317 | this.needUpdateMesh = true 318 | } 319 | 320 | get shadowColor() { 321 | return this._shadowColor 322 | } 323 | 324 | set shadowOffsetX(offsetX) { 325 | if(this._shadowOffsetX === offsetX) { return } 326 | this._shadowOffsetX = offsetX 327 | this.needUpdateMesh = true 328 | } 329 | 330 | set shadowOffsetY(offsetY) { 331 | if(this._shadowOffsetY === offsetY) { return } 332 | this._shadowOffsetY = offsetY 333 | this.needUpdateMesh = true 334 | } 335 | 336 | get shadowOffsetX() { 337 | return this._shadowOffsetX 338 | } 339 | 340 | get shadowOffsetY() { 341 | return this._shadowOffsetY 342 | } 343 | 344 | set limitWidth(value) { 345 | if(this._limitWidth === value) { return } 346 | this._limitWidth = value 347 | if(value === 0) { 348 | this._wordBuffer = null 349 | } 350 | else { 351 | this._wordBuffer = [] 352 | } 353 | this.needUpdateMesh = true 354 | } 355 | 356 | get limitWidth() { 357 | return this._limitWidth 358 | } 359 | } 360 | 361 | export default Text -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Device from "./Device" 2 | import Engine from "./Engine" 3 | import EngineWindow from "./EngineWindow" 4 | import Time from "./Time" 5 | import StateMachine from "./StateMachine" 6 | import Input from "./input/Input" 7 | import Gamepad from "./input/Gamepad" 8 | import { onDomLoad } from "./Utils" 9 | import Entity from "./entity/Entity" 10 | import Sprite from "./entity/Sprite" 11 | import AnimatedSprite from "./entity/AnimatedSprite" 12 | import Camera from "./entity/Camera" 13 | import Text from "./entity/Text" 14 | import BitmapText from "./entity/BitmapText" 15 | import Tilemap from "./tilemap/Tilemap" 16 | import TilemapLayer from "./tilemap/TilemapLayer" 17 | import TilemapOrthogonalLayer from "./tilemap/TilemapOrthogonalLayer" 18 | import TilemapIsometricLayer from "./tilemap/TilemapIsometricLayer" 19 | import TileBody from "./tilemap/component/TileBody" 20 | import { radians, degrees, length, clamp, EPSILON } from "./math/Common" 21 | import Resources from "./resources/Resources" 22 | import Audio from "./resources/Audio" 23 | import Resource from "./resources/Resource" 24 | import Material from "./resources/Material" 25 | import Texture from "./resources/Texture" 26 | import Graphics from "./resources/Graphics" 27 | import Animation from "./resources/Animation" 28 | import Spritesheet from "./resources/Spritesheet" 29 | import Sound from "./resources/Sound" 30 | import Content from "./resources/Content" 31 | import Font from "./resources/Font" 32 | import Tiled from "./resources/Tiled" 33 | import Tileset from "./resources/Tileset" 34 | import Mesh from "./mesh/Mesh" 35 | import Raycast from "./physics/Raycast" 36 | import Vector2 from "./math/Vector2" 37 | import Vector3 from "./math/Vector3" 38 | import Vector4 from "./math/Vector4" 39 | import Matrix3 from "./math/Matrix3" 40 | import Matrix4 from "./math/Matrix4" 41 | import AABB from "./math/AABB" 42 | import Circle from "./math/Circle" 43 | import Random from "./math/Random" 44 | import Stage from "./renderer/Stage" 45 | import DrawCommand from "./renderer/DrawCommand" 46 | import Renderer from "./renderer/RendererWebGL" 47 | import RendererWebGL from "./renderer/RendererWebGL" 48 | import Tween from "./tween/Tween" 49 | import Easing from "./tween/Easing" 50 | import "./Loading" 51 | 52 | Engine.create = (app) => { 53 | if(Engine.app) { 54 | console.warn("(Engine.create) Creating multiple engine instances is not supported!") 55 | return 56 | } 57 | 58 | const copyDefaultSettings = Object.assign({}, Engine.defaultSettings) 59 | 60 | Engine.app = app 61 | Engine.settings = Object.assign(copyDefaultSettings, app.settings) 62 | Engine.window = new EngineWindow() 63 | Engine.renderer = new RendererWebGL() 64 | 65 | Engine.view = new Entity() 66 | Engine.camera = new Camera() 67 | Engine.camera.setCullMask(0, true) 68 | Engine.cameras = [ Engine.camera ] 69 | 70 | onDomLoad(() => { 71 | Engine.window.create() 72 | Engine.view.enable = true 73 | }) 74 | } 75 | 76 | Engine.addCamera = () => { 77 | const camera = new Camera() 78 | camera.size.set(Engine.window.width, Engine.window.height) 79 | camera.updateProjectionTransform() 80 | Engine.cameras.push(camera) 81 | return camera 82 | } 83 | 84 | Engine.removeCamera = (camera) => { 85 | const index = Engine.cameras.indexOf(camera) 86 | if(index === -1) { return } 87 | Engine.cameras.splice(index, 1) 88 | } 89 | 90 | export { 91 | Engine, Device, Time, StateMachine, 92 | Input, Gamepad, 93 | Entity, Sprite, AnimatedSprite, Camera, Text, BitmapText, 94 | Tilemap, TilemapLayer, TilemapOrthogonalLayer, TilemapIsometricLayer, TileBody, 95 | Resources, Audio, Resource, Material, Texture, Graphics, Animation, Spritesheet, Sound, Content, Tiled, Tileset, 96 | Mesh, 97 | Raycast, 98 | radians, degrees, length, clamp, EPSILON, 99 | Vector2, Vector3, Vector4, Matrix3, Matrix4, AABB, Circle, Random, 100 | Stage, DrawCommand, Renderer, RendererWebGL, 101 | Tween, Easing 102 | } -------------------------------------------------------------------------------- /src/input/Gamepad.js: -------------------------------------------------------------------------------- 1 | import Device from "../Device" 2 | 3 | function GamepadInfo(gamepad) { 4 | const buttons = gamepad.buttons 5 | this.buttons = new Array(buttons.length) 6 | for(let n = 0; n < buttons.length; n++) { 7 | this.buttons[n] = buttons[n].pressed 8 | } 9 | 10 | this.axes = gamepad.axes 11 | this.axesDefault = gamepad.axes 12 | } 13 | 14 | class Gamepad 15 | { 16 | constructor() { 17 | this.listeners = {} 18 | this.gamepads = new Array(null, null, null, null) 19 | 20 | this.updateFunc = this.update.bind(this) 21 | 22 | window.addEventListener("gamepadconnected", this.handleConnected.bind(this)) 23 | window.addEventListener("gamepaddisconnected", this.handleDisconnected.bind(this)) 24 | 25 | requestAnimationFrame(this.updateFunc) 26 | } 27 | 28 | update() { 29 | const gamepads = navigator.getGamepads() 30 | for(let n = 0; n < gamepads.length; n++) 31 | { 32 | const gamepad = gamepads[n] 33 | if(!gamepad) { continue } 34 | 35 | const gamepadInfo = this.gamepads[n] 36 | if(!gamepadInfo) { 37 | this.gamepads[n] = new GamepadInfo(gamepad) 38 | this.emit("connected", n) 39 | } 40 | else 41 | { 42 | if(!Device.visible) { continue } 43 | 44 | const prevButtons = gamepadInfo.buttons 45 | const currButtons = gamepad.buttons 46 | for(let i = 0; i < currButtons.length; i++) { 47 | const currButton = currButtons[i] 48 | if(prevButtons[i] !== currButton.pressed) { 49 | prevButtons[i] = currButton.pressed 50 | if(currButton.pressed) { 51 | this.emit("down", i, n) 52 | } 53 | else { 54 | this.emit("up", i, n) 55 | } 56 | } 57 | } 58 | 59 | let changed = false 60 | const axes = gamepad.axes 61 | const prevAxes = gamepadInfo.axes 62 | const defaultAxes = gamepadInfo.axesDefault 63 | for(let i = 0; i < axes.length; i++) 64 | { 65 | let value = axes[i] 66 | const defaultValue = defaultAxes[i] 67 | if(value === defaultValue) { 68 | value = 0 69 | } 70 | 71 | if(prevAxes[i] !== value) { 72 | prevAxes[i] = value 73 | this.emit("axis", i, n, value) 74 | changed = true 75 | } 76 | } 77 | 78 | if(changed) { 79 | this.emit("axes", prevAxes, n) 80 | } 81 | } 82 | } 83 | 84 | requestAnimationFrame(this.updateFunc) 85 | } 86 | 87 | handleConnected(event) { 88 | const gamepad = event.gamepad 89 | if(!this.gamepads[gamepad.index]) { 90 | this.gamepads[gamepad.index] = new GamepadInfo(gamepad) 91 | this.emit("connected", gamepad.index) 92 | } 93 | } 94 | 95 | handleDisconnected(event) { 96 | const index = event.gamepad.index 97 | this.gamepads[index] = null 98 | this.emit("disconnected", index) 99 | } 100 | 101 | on(event, func) { 102 | const buffer = this.listeners[event] 103 | if(!buffer) { 104 | this.listeners[event] = [ func ] 105 | } 106 | else { 107 | buffer.push(func) 108 | } 109 | } 110 | 111 | off(event, func) { 112 | const buffer = this.listeners[event] 113 | if(!buffer) { return } 114 | 115 | const index = buffer.indexOf(func) 116 | buffer[index] = buffer[buffer.length - 1] 117 | buffer.pop() 118 | } 119 | 120 | emit(event, key, index, value) { 121 | const buffer = this.listeners[event] 122 | if(!buffer) { return } 123 | 124 | for(let n = 0; n < buffer.length; n++) { 125 | buffer[n](key, index, value) 126 | } 127 | } 128 | 129 | pressed(key, index) { 130 | const gamepad = this.gamepads[index] 131 | return gamepad ? gamepad.buttons[key].pressed : false 132 | } 133 | 134 | axis(key, index) { 135 | const gamepad = this.gamepads[index] 136 | if(!gamepad) { 137 | return 0 138 | } 139 | 140 | const value = gamepad.axes[key] 141 | if(value === gamepad.axesDefault[key]) { 142 | return 0 143 | } 144 | 145 | return value 146 | } 147 | } 148 | 149 | const instance = new Gamepad() 150 | export default instance -------------------------------------------------------------------------------- /src/input/Input.js: -------------------------------------------------------------------------------- 1 | import Device from "../Device" 2 | import Engine from "../Engine" 3 | import Key from "./Key" 4 | 5 | const numKeys = 256 6 | const numTouches = 10 7 | const numTotalKeys = numKeys + numTouches + 1 8 | 9 | function Subscriber(owner, func) { 10 | this.owner = owner 11 | this.func = func 12 | } 13 | 14 | class Input 15 | { 16 | constructor() 17 | { 18 | this.listeners = {} 19 | this.ignoreKeys = {} 20 | this.cmdKeys = {} 21 | this.iframeKeys = {} 22 | 23 | this.enable = true 24 | this.stickyKeys = false 25 | this.metaPressed = false 26 | this.firstInputEvent = true 27 | 28 | this.inputs = new Array(numTotalKeys) 29 | this.touches = [] 30 | 31 | Device.on("visible", (value) => { 32 | if(!value) { 33 | this.reset() 34 | } 35 | }) 36 | 37 | this.x = 0 38 | this.y = 0 39 | this.screenX = 0 40 | this.screenY = 0 41 | this.prevX = 0 42 | this.prevY = 0 43 | this.prevScreenX = 0 44 | this.prevScreenY = 0 45 | 46 | Engine.on("setup", this.initialize.bind(this)) 47 | } 48 | 49 | initialize() { 50 | loadIgnoreKeys(this) 51 | addEventListeners(this) 52 | } 53 | 54 | handleKeyDown(domEvent) 55 | { 56 | this.checkIgnoreKey(domEvent) 57 | 58 | if(!this.enable) { return } 59 | 60 | const keyCode = domEvent.keyCode 61 | 62 | if(this.stickyKeys && this.inputs[keyCode]) { 63 | return 64 | } 65 | 66 | if(domEvent.keyIdentifier === "Meta") { 67 | this.metaPressed = true 68 | } 69 | else if(this.metaPressed) { 70 | return 71 | } 72 | 73 | this.inputs[keyCode] = 1 74 | 75 | const inputEvent = new Input.Event() 76 | inputEvent.domEvent = domEvent 77 | inputEvent.keyCode = keyCode 78 | this.emit("keydown", inputEvent) 79 | } 80 | 81 | handleKeyUp(domEvent) 82 | { 83 | this.checkIgnoreKey(domEvent) 84 | 85 | if(!this.enable) { return } 86 | 87 | const keyCode = domEvent.keyCode 88 | 89 | this.metaPressed = false 90 | this.inputs[keyCode] = 0 91 | 92 | const inputEvent = new Input.Event() 93 | inputEvent.domEvent = domEvent 94 | inputEvent.keyCode = keyCode 95 | 96 | this.emit("keyup", inputEvent) 97 | } 98 | 99 | handleMouseDown(domEvent) { 100 | this.handleMouseEvent("down", domEvent) 101 | } 102 | 103 | handleMouseUp(domEvent) { 104 | this.handleMouseEvent("up", domEvent) 105 | } 106 | 107 | handleMouseDblClick(domEvent) { 108 | this.handleMouseEvent("dblclick", domEvent) 109 | } 110 | 111 | handleMouseMove(domEvent) 112 | { 113 | if(document.activeElement === document.body) { 114 | domEvent.preventDefault() 115 | } 116 | 117 | this.handleMouseEvent("move", domEvent) 118 | } 119 | 120 | handleMouseWheel(domEvent) 121 | { 122 | if(document.activeElement === document.body) { 123 | domEvent.preventDefault() 124 | } 125 | 126 | this.handleMouseEvent("wheel", domEvent) 127 | } 128 | 129 | handleMouseEvent(eventType, domEvent) 130 | { 131 | if(!this.enable) { return } 132 | 133 | const wnd = Engine.window 134 | const camera = Engine.camera 135 | const transform = camera.transform 136 | 137 | this.prevScreenX = this.screenX 138 | this.prevScreenY = this.screenY 139 | this.screenX = ((domEvent.pageX - wnd.offsetLeft) * wnd.scaleX) / wnd.ratio 140 | this.screenY = ((domEvent.pageY - wnd.offsetTop) * wnd.scaleY) / wnd.ratio 141 | this.prevX = this.x 142 | this.prevY = this.y 143 | this.x = (this.screenX - transform.m[6]) / camera._scale.x / wnd.ratio | 0 144 | this.y = (this.screenY - transform.m[7]) / camera._scale.y / wnd.ratio | 0 145 | 146 | const inputEvent = new Input.Event() 147 | inputEvent.domEvent = domEvent 148 | inputEvent.screenX = this.screenX 149 | inputEvent.screenY = this.screenY 150 | inputEvent.x = this.x 151 | inputEvent.y = this.y 152 | 153 | switch(eventType) 154 | { 155 | case "down": 156 | case "dblclick": 157 | case "up": 158 | { 159 | const keyCode = domEvent.button + Key.BUTTON_ENUM_OFFSET 160 | this.inputs[keyCode] = (eventType === "up" || eventType === "dblclick") ? 0 : 1 161 | 162 | if(this.firstInputEvent) { 163 | inputEvent.deltaX = 0 164 | inputEvent.deltaY = 0 165 | this.firstInputEvent = false 166 | } 167 | else { 168 | inputEvent.deltaX = (this.prevScreenX - this.screenX) / wnd.ratio 169 | inputEvent.deltaY = (this.prevScreenY - this.screenY) / wnd.ratio 170 | } 171 | 172 | inputEvent.keyCode = keyCode 173 | } break 174 | 175 | case "move": 176 | { 177 | if(this.firstInputEvent) { 178 | inputEvent.deltaX = 0 179 | inputEvent.deltaY = 0 180 | this.firstInputEvent = false 181 | } 182 | else { 183 | inputEvent.deltaX = -domEvent.movementX / wnd.ratio 184 | inputEvent.deltaY = -domEvent.movementY / wnd.ratio 185 | } 186 | 187 | inputEvent.keyCode = 0 188 | } break 189 | 190 | case "wheel": 191 | { 192 | inputEvent.deltaX = Math.max(-1, Math.min(1, (domEvent.wheelDelta || -domEvent.detail))) 193 | inputEvent.deltaY = inputEvent.deltaX 194 | inputEvent.keyCode = 0 195 | } break 196 | } 197 | 198 | this.emit(eventType, inputEvent) 199 | } 200 | 201 | handleTouchDown(domEvent) { 202 | this.handleTouchEvent(domEvent, "down") 203 | } 204 | 205 | handleTouchUp(domEvent) { 206 | this.handleTouchEvent(domEvent, "up") 207 | } 208 | 209 | handleTouchMove(domEvent) { 210 | this.handleTouchEvent(domEvent, "move") 211 | } 212 | 213 | handleTouchEvent(domEvent, eventType) 214 | { 215 | if(domEvent.target !== Engine.canvas) { return } 216 | domEvent.preventDefault() 217 | 218 | const wnd = Engine.window 219 | const camera = Engine.camera 220 | const transform = camera.transform 221 | 222 | const changedTouches = domEvent.changedTouches 223 | for(let n = 0; n < changedTouches.length; n++) 224 | { 225 | const touch = changedTouches[n] 226 | 227 | let id 228 | switch(eventType) 229 | { 230 | case "down": 231 | id = this.touches.length 232 | this.touches.push(touch.identifier) 233 | break 234 | 235 | case "up": 236 | id = this.getTouchID(touch.identifier) 237 | if(id === -1) { continue } 238 | this.touches.splice(id, 1) 239 | break 240 | 241 | case "move": 242 | id = this.getTouchID(touch.identifier) 243 | break 244 | } 245 | 246 | const screenX = ((touch.pageX - wnd.offsetLeft) * wnd.scaleX) 247 | const screenY = ((touch.pageY - wnd.offsetTop) * wnd.scaleY) 248 | const x = (screenX - transform.m[6]) / camera._scale.x | 0 249 | const y = (screenY - transform.m[7]) / camera._scale.y | 0 250 | const keyCode = id + Key.BUTTON_ENUM_OFFSET 251 | 252 | const inputEvent = new Input.Event() 253 | inputEvent.domEvent = domEvent 254 | inputEvent.screenX = screenX 255 | inputEvent.screenY = screenY 256 | inputEvent.x = x 257 | inputEvent.y = y 258 | inputEvent.keyCode = keyCode 259 | 260 | if(id === 0) 261 | { 262 | this.prevX = this.x 263 | this.prevY = this.y 264 | this.prevScreenX = this.screenX 265 | this.prevScreenY = this.screenY 266 | this.x = x 267 | this.y = y 268 | this.screenX = screenX 269 | this.screenY = screenY 270 | 271 | if(this.firstInputEvent) { 272 | inputEvent.deltaX = 0 273 | inputEvent.deltaY = 0 274 | this.firstInputEvent = false 275 | } 276 | else { 277 | inputEvent.deltaX = this.prevScreenX - this.screenX 278 | inputEvent.deltaY = this.prevScreenY - this.screenY 279 | } 280 | } 281 | 282 | switch(eventType) 283 | { 284 | case "down": 285 | this.inputs[keyCode] = 1 286 | break 287 | case "up": 288 | this.inputs[keyCode] = 0 289 | break 290 | } 291 | 292 | this.emit(eventType, inputEvent) 293 | } 294 | } 295 | 296 | pressed(keyCode) { 297 | return this.inputs[keyCode] 298 | } 299 | 300 | on(event, func, owner) 301 | { 302 | const sub = new Subscriber(owner, func) 303 | 304 | let buffer = this.listeners[event] 305 | if(buffer) { 306 | buffer.push(sub) 307 | } 308 | else { 309 | buffer = [ sub ] 310 | this.listeners[event] = buffer 311 | } 312 | } 313 | 314 | off(event, func, owner) 315 | { 316 | const buffer = this.listeners[event] 317 | if(!buffer) { return } 318 | 319 | for(let n = 0; n < buffer.length; n++) 320 | { 321 | const sub = buffer[n] 322 | if(sub.func === func && sub.owner === owner) { 323 | buffer[n] = buffer[buffer.length - 1] 324 | buffer.pop() 325 | return 326 | } 327 | } 328 | } 329 | 330 | emit(event, arg) 331 | { 332 | const buffer = this.listeners[event] 333 | if(!buffer) { return } 334 | 335 | for(let n = buffer.length - 1; n >= 0; n--) { 336 | const sub = buffer[n] 337 | sub.func.call(sub.owner, arg) 338 | } 339 | } 340 | 341 | reset() 342 | { 343 | // Reset keys: 344 | for(let n = 0; n < numKeys.length; n++) 345 | { 346 | if(!this.inputs[n]) { continue } 347 | 348 | this.inputs[n] = 0 349 | 350 | const inputEvent = new Input.Event() 351 | inputEvent.domEvent = domEvent 352 | inputEvent.keyCode = keyCode 353 | 354 | this.emit("keyup", inputEvent) 355 | } 356 | 357 | // Reset inputs: 358 | for(let n = numKeys; n <= numKeys + numTouches; n++) 359 | { 360 | const keyCode = n + Key.BUTTON_ENUM_OFFSET 361 | if(!this.inputs[keyCode]) { continue } 362 | 363 | const inputEvent = new Input.Event() 364 | inputEvent.domEvent = domEvent 365 | inputEvent.keyCode = keyCode 366 | 367 | this.emit("up", inputEvent) 368 | } 369 | 370 | // Reset touches: 371 | for(let n = 0; n < this.touches.length; n++) 372 | { 373 | if(!this.touches[n]) { continue } 374 | 375 | const keyCode = n + Key.BUTTON_ENUM_OFFSET 376 | if(!this.inputs[keyCode]) { continue } 377 | 378 | const inputEvent = new Input.Event() 379 | inputEvent.domEvent = domEvent 380 | inputEvent.keyCode = keyCode 381 | 382 | this.emit("up", inputEvent) 383 | } 384 | } 385 | 386 | getTouchID(eventTouchID) 387 | { 388 | for(let n = 0; n < this.touches.length; n++) 389 | { 390 | if(this.touches[n] === eventTouchID) { 391 | return n 392 | } 393 | } 394 | 395 | return -1 396 | } 397 | 398 | checkIgnoreKey(domEvent) 399 | { 400 | const keyCode = domEvent.keyCode 401 | 402 | if(document.activeElement === document.body) 403 | { 404 | if(window.top && this.iframeKeys[keyCode]) { 405 | domEvent.preventDefault() 406 | } 407 | 408 | if(this.cmdKeys[keyCode] !== undefined) { 409 | this.numCmdKeysPressed++ 410 | } 411 | 412 | if(this.ignoreKeys[keyCode] !== undefined && this.numCmdKeysPressed <= 0) { 413 | domEvent.preventDefault() 414 | } 415 | } 416 | } 417 | 418 | set ignoreFKeys(flag) 419 | { 420 | if(flag) { 421 | ignoreFKeys(this, 1) 422 | } 423 | else { 424 | ignoreFKeys(this, 0) 425 | } 426 | } 427 | 428 | get ignoreFKeys() { 429 | return !!this.ignoreKeys[112] 430 | } 431 | } 432 | 433 | const addEventListeners = (input) => { 434 | const passiveTouches = { passive: false } 435 | 436 | window.addEventListener("mousedown", input.handleMouseDown.bind(input)) 437 | window.addEventListener("mouseup", input.handleMouseUp.bind(input)) 438 | window.addEventListener("mousemove", input.handleMouseMove.bind(input)) 439 | window.addEventListener("mousewheel", input.handleMouseWheel.bind(input)) 440 | window.addEventListener("dblclick", input.handleMouseDblClick.bind(input)) 441 | window.addEventListener("touchstart", input.handleTouchDown.bind(input), passiveTouches) 442 | window.addEventListener("touchend", input.handleTouchUp.bind(input), passiveTouches) 443 | window.addEventListener("touchmove", input.handleTouchMove.bind(input), passiveTouches) 444 | window.addEventListener("touchcancel", input.handleTouchUp.bind(input), passiveTouches) 445 | window.addEventListener("touchleave", input.handleTouchUp.bind(input), passiveTouches) 446 | 447 | if(Device.supports.onkeydown) { 448 | window.addEventListener("keydown", input.handleKeyDown.bind(input)) 449 | } 450 | 451 | if(Device.supports.onkeyup) { 452 | window.addEventListener("keyup", input.handleKeyUp.bind(input)) 453 | } 454 | } 455 | 456 | const loadIgnoreKeys = (input) => 457 | { 458 | input.ignoreKeys = {} 459 | input.ignoreKeys[8] = 1 460 | input.ignoreKeys[9] = 1 461 | input.ignoreKeys[13] = 1 462 | input.ignoreKeys[17] = 1 463 | input.ignoreKeys[91] = 1 464 | input.ignoreKeys[38] = 1 465 | input.ignoreKeys[39] = 1 466 | input.ignoreKeys[40] = 1 467 | input.ignoreKeys[37] = 1 468 | input.ignoreKeys[124] = 1 469 | input.ignoreKeys[125] = 1 470 | input.ignoreKeys[126] = 1 471 | 472 | input.cmdKeys[91] = 1 473 | input.cmdKeys[17] = 1 474 | 475 | input.iframeKeys[37] = 1 476 | input.iframeKeys[38] = 1 477 | input.iframeKeys[39] = 1 478 | input.iframeKeys[40] = 1 479 | } 480 | 481 | const ignoreFKeys = (input, value) => 482 | { 483 | for(let n = 112; n <= 123; n++) { 484 | input.ignoreKeys[n] = value 485 | } 486 | } 487 | 488 | Input.Event = function() 489 | { 490 | this.domEvent = null 491 | this.x = 0 492 | this.y = 0 493 | this.deltaX = 0 494 | this.deltaY = 0 495 | this.screenX = 0 496 | this.screenY = 0 497 | this.keyCode = 0 498 | this.entity = null 499 | } 500 | 501 | const instance = new Input 502 | instance.Key = Key 503 | 504 | export default instance -------------------------------------------------------------------------------- /src/input/Key.js: -------------------------------------------------------------------------------- 1 | const BUTTON_ENUM_OFFSET = 256 2 | 3 | const Key = { 4 | BUTTON_ENUM_OFFSET, 5 | BACKSPACE: 8, 6 | TAB: 9, 7 | ENTER: 13, 8 | SHIFT: 16, 9 | ESC: 27, 10 | SPACE: 32, 11 | LEFT: 37, 12 | UP: 38, 13 | RIGHT: 39, 14 | DOWN: 40, 15 | DELETE: 46, 16 | NUM_0: 48, 17 | NUM_1: 49, 18 | NUM_2: 50, 19 | NUM_3: 51, 20 | NUM_4: 52, 21 | NUM_5: 53, 22 | NUM_6: 54, 23 | NUM_7: 55, 24 | NUM_8: 56, 25 | NUM_9: 57, 26 | NUMPAD_0: 96, 27 | NUMPAD_1: 97, 28 | NUMPAD_2: 98, 29 | NUMPAD_3: 99, 30 | NUMPAD_4: 100, 31 | NUMPAD_5: 101, 32 | NUMPAD_6: 102, 33 | NUMPAD_7: 103, 34 | NUMPAD_8: 104, 35 | NUMPAD_9: 105, 36 | MULTIPLY: 106, 37 | ADD: 107, 38 | SUBTRACT: 109, 39 | DECIMAL: 110, 40 | DIVIDE: 111, 41 | A: 65, 42 | B: 66, 43 | C: 67, 44 | D: 68, 45 | E: 69, 46 | F: 70, 47 | G: 71, 48 | H: 72, 49 | I: 73, 50 | J: 74, 51 | K: 75, 52 | L: 76, 53 | M: 77, 54 | N: 78, 55 | O: 79, 56 | P: 80, 57 | Q: 81, 58 | R: 82, 59 | S: 83, 60 | T: 84, 61 | U: 85, 62 | V: 86, 63 | W: 87, 64 | X: 88, 65 | Y: 89, 66 | Z: 90, 67 | SQUARE_BRACKET_LEFT: 91, 68 | SQUARE_BRACKET_RIGHT: 91, 69 | PARENTHESES_LEFT: 91, 70 | PARENTHESES_RIGHT: 91, 71 | BRACES_LEFT: 91, 72 | BRACES_RIGHT: 92, 73 | F1: 112, 74 | F2: 113, 75 | F3: 114, 76 | F4: 115, 77 | F5: 116, 78 | F6: 117, 79 | F7: 118, 80 | F8: 119, 81 | F9: 120, 82 | F10: 121, 83 | F11: 122, 84 | F12: 123, 85 | PLUS: 187, 86 | MINUS: 189, 87 | TILDE: 192, 88 | APOSTROPHE: 222, 89 | BUTTON_LEFT: 0 + BUTTON_ENUM_OFFSET, 90 | BUTTON_MIDDLE: 1 + BUTTON_ENUM_OFFSET, 91 | BUTTON_RIGHT: 2 + BUTTON_ENUM_OFFSET 92 | } 93 | 94 | export default Key -------------------------------------------------------------------------------- /src/math/AABB.js: -------------------------------------------------------------------------------- 1 | import { clamp, VolumeType } from "./Common" 2 | 3 | class AABB 4 | { 5 | constructor(x = 0, y = 0, width = 0, height = 0) { 6 | this.minX = 0 7 | this.minY = 0 8 | this.maxX = 0 9 | this.maxY = 0 10 | this.width = 0 11 | this.height = 0 12 | this.set(x, y, width, height) 13 | } 14 | 15 | set(x, y, width, height) { 16 | this.width = width 17 | this.height = height 18 | this.minX = x 19 | this.minY = y 20 | this.maxX = x + width 21 | this.maxY = y + height 22 | } 23 | 24 | position(x, y) { 25 | this.minX = this.x 26 | this.minY = this.y 27 | this.maxX = this.minX + this.width 28 | this.maxY = this.minY + this.height 29 | } 30 | 31 | move(x, y) { 32 | this.minX += x 33 | this.minY += y 34 | this.maxX += x 35 | this.maxY += y 36 | } 37 | 38 | resize(width, height) 39 | { 40 | this.width = width 41 | this.height = height 42 | this.maxX = this.minX + this.width 43 | this.maxY = this.minY + this.height 44 | } 45 | 46 | vsAABB(src) 47 | { 48 | if(this.maxX < src.minX || this.minX > src.maxX) { return false } 49 | if(this.maxY < src.minY || this.minY > src.maxY) { return false } 50 | 51 | return true 52 | } 53 | 54 | vsBorderAABB(src) 55 | { 56 | if(this.maxX <= src.minX || this.minX >= src.maxX) { return false } 57 | if(this.maxY <= src.minY || this.minY >= src.maxY) { return false } 58 | 59 | return true 60 | } 61 | 62 | vsAABBIntersection(src) 63 | { 64 | if(this.maxX < src.minX || this.minX > src.maxX) { return 0 } 65 | if(this.maxY < src.minY || this.minY > src.maxY) { return 0 } 66 | 67 | if(this.minX > src.minX || this.minY > src.minY) { return 1 } 68 | if(this.maxX < src.maxX || this.maxY < src.maxY) { return 1 } 69 | 70 | return 2 71 | } 72 | 73 | vsCircle(circle) 74 | { 75 | const aabb_halfExtents_width = this.width * 0.5 76 | const aabb_halfExtents_height = this.height * 0.5 77 | const aabb_centerX = this.minX + aabb_halfExtents_width 78 | const aabb_centerY = this.minY + aabb_halfExtents_height 79 | 80 | let diffX = circle.x - aabb_centerX 81 | let diffY = circle.y - aabb_centerY 82 | diffX = clamp(diffX, -aabb_halfExtents_width, aabb_halfExtents_width) 83 | diffY = clamp(diffY, -aabb_halfExtents_height, aabb_halfExtents_height) 84 | 85 | const closestX = aabb_centerX + diffX 86 | const closestY = aabb_centerY + diffY 87 | 88 | diffX = closestX - circle.x 89 | diffY = closestY - circle.y 90 | 91 | return Math.sqrt((diffX * diffX) + (diffY * diffY)) < circle.radius 92 | } 93 | 94 | vsPoint(x, y) 95 | { 96 | if(this.minX > x || this.maxX < x) { return false } 97 | if(this.minY > y || this.maxY < y) { return false } 98 | 99 | return true 100 | } 101 | 102 | vsBorderPoint(x, y) 103 | { 104 | if(this.minX >= x || this.maxX <= x) { return false } 105 | if(this.minY >= y || this.maxY <= y) { return false } 106 | 107 | return true 108 | } 109 | 110 | getSqrDistance(x, y) 111 | { 112 | let tmp 113 | let sqDist = 0 114 | 115 | if(x < this.minX) { 116 | tmp = (this.minX - x) 117 | sqDist += tmp * tmp 118 | } 119 | if(x > this.maxX) { 120 | tmp = (x - this.maxX) 121 | sqDist += tmp * tmp 122 | } 123 | 124 | if(y < this.minY) { 125 | tmp = (this.minY - y) 126 | sqDist += tmp * tmp 127 | } 128 | if(y > this.maxY) { 129 | tmp = (y - this.maxY) 130 | sqDist += tmp * tmp 131 | } 132 | 133 | return sqDist 134 | } 135 | 136 | getDistanceVsAABB(aabb) 137 | { 138 | const centerX = this.minX + ((this.maxX - this.minX) / 2) 139 | const centerY = this.minY + ((this.maxY - this.minY) / 2) 140 | const srcCenterX = aabb.minX + ((aabb.maxY - aabb.minY) / 2) 141 | const srcCenterY = aabb.minY + ((aabb.maxY - aabb.minY) / 2) 142 | 143 | const diffX = srcCenterX - centerX 144 | const diffY = srcCenterY - centerY 145 | 146 | return Math.sqrt((diffX * diffX) + (diffY * diffY)) 147 | } 148 | 149 | isUndefined() { 150 | return (this.maxY === undefined) 151 | } 152 | 153 | print(str) 154 | { 155 | if(str) 156 | { 157 | console.log("(AABB) " + str + " minX: " + this.minX + " minY: " + this.minY 158 | + " maxX: " + this.maxX + " maxY: " + this.maxY) 159 | } 160 | else 161 | { 162 | console.log("(AABB) minX: " + this.minX + " minY: " + this.minY 163 | + " maxX: " + this.maxX + " maxY: " + this.maxY) 164 | } 165 | } 166 | } 167 | 168 | AABB.prototype.volumeType = VolumeType.AABB 169 | 170 | export default AABB -------------------------------------------------------------------------------- /src/math/Circle.js: -------------------------------------------------------------------------------- 1 | import { clamp, VolumeType } from "./Common" 2 | 3 | class Circle 4 | { 5 | constructor(x, y, radius) 6 | { 7 | this.x = x 8 | this.y = y 9 | this.radius = radius 10 | this.minX = x - radius 11 | this.minY = y - radius 12 | this.maxX = x + radius 13 | this.maxY = y + radius 14 | } 15 | 16 | position(x, y) { 17 | this.x = x 18 | this.y = y 19 | this.minX = x - this.radius 20 | this.minY = y - this.radius 21 | this.maxX = x + this.radius 22 | this.maxY = y + this.radius 23 | } 24 | 25 | move(addX, addY) { 26 | this.x += addX 27 | this.y += addY 28 | this.minX += addX 29 | this.minY += addY 30 | this.maxX += addX 31 | this.maxY += addY 32 | } 33 | 34 | vsPoint(x, y) { 35 | return ((this.x - x) * 2) + ((this.y - y) * 2) <= (radius * 2) 36 | } 37 | 38 | vsAABB(aabb) 39 | { 40 | const aabb_halfExtents_width = aabb.width * 0.5 41 | const aabb_halfExtents_height = aabb.height * 0.5 42 | const aabb_centerX = aabb.minX + aabb_halfExtents_width 43 | const aabb_centerY = aabb.minY + aabb_halfExtents_height 44 | 45 | let diffX = this.x - aabb_centerX 46 | let diffY = this.y - aabb_centerY 47 | diffX = clamp(diffX, -aabb_halfExtents_width, aabb_halfExtents_width) 48 | diffY = clamp(diffY, -aabb_halfExtents_height, aabb_halfExtents_height) 49 | 50 | const closestX = aabb_centerX + diffX 51 | const closestY = aabb_centerY + diffY 52 | 53 | diffX = closestX - this.x 54 | diffY = closestY - this.y 55 | 56 | return Math.sqrt((diffX * diffX) + (diffY * diffY)) < this.radius 57 | } 58 | 59 | vsCircle(circle) 60 | { 61 | const dx = circle.x - this.x 62 | const dy = circle.y - this.y 63 | const radii = this.radius + circle.radius 64 | 65 | if((dx * dx) + (dy * dy) < (radii * radii)) { 66 | return true 67 | } 68 | 69 | return false 70 | } 71 | 72 | overlapCircle(circle) 73 | { 74 | const distance = Math.sqrt((this.x - circle.x) * (this.y - circle.y)) 75 | 76 | // Does not contain: 77 | if(distance > (this.radius + circle.radius)) { 78 | return 0 79 | } 80 | // Overlap: 81 | else if(distance <= Math.abs(this.radius + circle.radius)) { 82 | return 1 83 | } 84 | 85 | // Contains 86 | return 2 87 | } 88 | 89 | print(str) 90 | { 91 | if(str) { 92 | console.log("[" + str + "] x:", this.x, "y:", this.y, "raidus:", this.radius) 93 | } 94 | else { 95 | console.log("x:", this.x, "y:", this.y, "raidus:", this.radius) 96 | } 97 | } 98 | } 99 | 100 | Circle.prototype.volumeType = VolumeType.Circle 101 | 102 | export default Circle -------------------------------------------------------------------------------- /src/math/Common.js: -------------------------------------------------------------------------------- 1 | 2 | const radians = (degrees) => { 3 | return degrees * Math.PI / 180 4 | } 5 | 6 | const degrees = (radians) => { 7 | return radians * 180 / Math.PI 8 | } 9 | 10 | const length = (x1, y1, x2, y2) => { 11 | const x = x2 - x1 12 | const y = y2 - y1 13 | return Math.sqrt((x * x) + (y * y)) 14 | } 15 | 16 | const clamp = (value, min, max) => { 17 | return Math.max(min, Math.min(max, value)) 18 | } 19 | 20 | const EPSILON = 0.000001 21 | 22 | const VolumeType = { 23 | Unknown: 0, 24 | AABB: 1, 25 | Circle: 2 26 | } 27 | 28 | export { 29 | radians, 30 | degrees, 31 | length, 32 | clamp, 33 | EPSILON, 34 | VolumeType 35 | } -------------------------------------------------------------------------------- /src/math/Matrix3.js: -------------------------------------------------------------------------------- 1 | 2 | class Matrix3 3 | { 4 | constructor(matrix) { 5 | if(matrix) { 6 | this.m = new Float32Array(matrix) 7 | } 8 | else { 9 | this.m = new Float32Array(9) 10 | this.m[0] = 1.0 11 | this.m[4] = 1.0 12 | this.m[8] = 1.0 13 | } 14 | this.cos = 1 15 | this.sin = 0 16 | } 17 | 18 | copy(src) { 19 | this.m.set(src.m) 20 | } 21 | 22 | clone() { 23 | const m = new Matrix3(this.m) 24 | return m 25 | } 26 | 27 | identity() { 28 | this.m[0] = 1.0 29 | this.m[1] = 0.0 30 | this.m[2] = 0.0 31 | 32 | this.m[3] = 0.0 33 | this.m[4] = 1.0 34 | this.m[5] = 0.0 35 | 36 | this.m[6] = 0.0 37 | this.m[7] = 0.0 38 | this.m[8] = 1.0 39 | } 40 | 41 | translate(x, y) { 42 | this.m[6] += x 43 | this.m[7] += y 44 | } 45 | 46 | scale(x, y) { 47 | this.m[0] *= x 48 | this.m[1] *= y 49 | this.m[3] *= x 50 | this.m[4] *= y 51 | this.m[6] *= x 52 | this.m[7] *= y 53 | } 54 | 55 | rotate(angle) { 56 | this.cos = Math.cos(angle) 57 | this.sin = Math.sin(angle) 58 | const a = this.m[0] 59 | const c = this.m[3] 60 | const tx = this.m[6] 61 | 62 | this.m[0] = (a * cos) - (this.b * sin) 63 | this.m[1] = (a * sin) + (this.b * cos) 64 | this.m[3] = (c * cos) - (this.d * sin) 65 | this.m[4] = (c * sin) + (this.d * cos) 66 | this.m[6] = (tx * cos) - (this.ty * sin) 67 | this.m[7] = (tx * sin) + (this.ty * cos) 68 | } 69 | 70 | invert() { 71 | const a = this.m[0] 72 | const b = this.m[1] 73 | const c = this.m[3] 74 | const d = this.m[4] 75 | const tx = this.m[6] 76 | const n = (a * d) - (b * c) 77 | 78 | this.m[0] = d / n 79 | this.m[1] = -b / n 80 | this.m[3] = -c / n 81 | this.m[4] = a / n 82 | this.m[6] = ((c * this.ty) - (d1 * tx)) / n 83 | this.m[7] = -((a * this.ty) - (b1 * tx)) / n 84 | } 85 | 86 | projection(width, height) { 87 | this.m[0] = 2 / width 88 | this.m[1] = 0 89 | this.m[2] = 0 90 | this.m[3] = 0 91 | this.m[4] = -2 / height 92 | this.m[5] = 0 93 | this.m[6] = -1 94 | this.m[7] = 1 95 | this.m[8] = 1 96 | } 97 | } 98 | 99 | export default Matrix3 -------------------------------------------------------------------------------- /src/math/Matrix4.js: -------------------------------------------------------------------------------- 1 | import { EPSILON } from "./Common" 2 | 3 | class Matrix4 4 | { 5 | constructor(matrix) { 6 | if(matrix) { 7 | this.m = new Float32Array(matrix) 8 | } 9 | else { 10 | this.m = new Float32Array(16) 11 | this.m[0] = 1.0 12 | this.m[5] = 1.0 13 | this.m[10] = 1.0 14 | this.m[15] = 1.0 15 | } 16 | } 17 | 18 | copy(src) { 19 | this.m.set(src.m) 20 | } 21 | 22 | clone() { 23 | const m = new Matrix4(this.m) 24 | return m 25 | } 26 | 27 | identity() 28 | { 29 | this.m[0] = 1.0 30 | this.m[1] = 0.0 31 | this.m[2] = 0.0 32 | this.m[3] = 0.0 33 | 34 | this.m[4] = 0.0 35 | this.m[5] = 1.0 36 | this.m[6] = 0.0 37 | this.m[7] = 0.0 38 | 39 | this.m[8] = 0.0 40 | this.m[9] = 0.0 41 | this.m[10] = 1.0 42 | this.m[11] = 0.0 43 | 44 | this.m[12] = 0.0 45 | this.m[13] = 0.0 46 | this.m[14] = 0.0 47 | this.m[15] = 1.0 48 | } 49 | 50 | set(matrix) { 51 | this.m.assing(matrix) 52 | } 53 | 54 | translate(x, y, z) 55 | { 56 | this.m[12] = this.m[0] * x + this.m[4] * y + this.m[8] * z + this.m[12] 57 | this.m[13] = this.m[1] * x + this.m[5] * y + this.m[9] * z + this.m[13] 58 | this.m[14] = this.m[2] * x + this.m[6] * y + this.m[10] * z + this.m[14] 59 | this.m[15] = this.m[3] * x + this.m[7] * y + this.m[11] * z + this.m[15] 60 | } 61 | 62 | rotate(rad, x, y, z) 63 | { 64 | const a00 = this.m[0] 65 | const a01 = this.m[1] 66 | const a02 = this.m[2] 67 | const a03 = this.m[3] 68 | const a10 = this.m[4] 69 | const a11 = this.m[5] 70 | const a12 = this.m[6] 71 | const a13 = this.m[7] 72 | const a20 = this.m[8] 73 | const a21 = this.m[9] 74 | const a22 = this.m[10] 75 | const a23 = this.m[11] 76 | 77 | let lenght = Math.sqrt(x * x + y * y + z * z) 78 | if(Math.abs(lenght) < Number.EPSILON) { return } 79 | 80 | lenght = 1.0 / lenght 81 | x *= lenght 82 | y *= lenght 83 | z *= lenght 84 | 85 | const s = Math.sin(rad) 86 | const c = Math.cos(rad) 87 | const t = 1 - c 88 | 89 | const b00 = x * x * t + c 90 | const b01 = y * x * t + z * s 91 | const b02 = z * x * t - y * s 92 | const b10 = x * y * t - z * s 93 | const b11 = y * y * t + c 94 | const b12 = z * y * t + x * s 95 | const b20 = x * z * t + y * s 96 | const b21 = y * z * t - x * s 97 | const b22 = z * z * t + c 98 | 99 | this.m[0] = a00 * b00 + a10 * b01 + a20 * b02 100 | this.m[1] = a01 * b00 + a11 * b01 + a21 * b02 101 | this.m[2] = a02 * b00 + a12 * b01 + a22 * b02 102 | this.m[3] = a03 * b00 + a13 * b01 + a23 * b02 103 | this.m[4] = a00 * b10 + a10 * b11 + a20 * b12 104 | this.m[5] = a01 * b10 + a11 * b11 + a21 * b12 105 | this.m[6] = a02 * b10 + a12 * b11 + a22 * b12 106 | this.m[7] = a03 * b10 + a13 * b11 + a23 * b12 107 | this.m[8] = a00 * b20 + a10 * b21 + a20 * b22 108 | this.m[9] = a01 * b20 + a11 * b21 + a21 * b22 109 | this.m[10] = a02 * b20 + a12 * b21 + a22 * b22 110 | this.m[11] = a03 * b20 + a13 * b21 + a23 * b22 111 | } 112 | 113 | scale(x, y, z) 114 | { 115 | this.m[0] *= x 116 | this.m[1] *= x 117 | this.m[2] *= x 118 | this.m[3] *= x 119 | 120 | this.m[4] *= y 121 | this.m[5] *= y 122 | this.m[6] *= y 123 | this.m[7] *= y 124 | 125 | this.m[8] *= z 126 | this.m[9] *= z 127 | this.m[10] *= z 128 | this.m[11] *= z 129 | } 130 | 131 | mul(src) 132 | { 133 | let a0 = this.m[0] 134 | let a1 = this.m[1] 135 | let a2 = this.m[2] 136 | let a3 = this.m[3] 137 | this.m[0] = a0 * src.m[0] + a1 * src.m[4] + a2 * src.m[8] + a3 * src.m[12] 138 | this.m[1] = a0 * src.m[1] + a1 * src.m[5] + a2 * src.m[9] + a3 * src.m[13] 139 | this.m[2] = a0 * src.m[2] + a1 * src.m[6] + a2 * src.m[10] + a3 * src.m[14] 140 | this.m[3] = a0 * src.m[3] + a1 * src.m[7] + a2 * src.m[11] + a3 * src.m[15] 141 | 142 | a0 = this.m[4] 143 | a1 = this.m[5] 144 | a2 = this.m[6] 145 | a3 = this.m[7] 146 | this.m[4] = a0 * src.m[0] + a1 * src.m[4] + a2 * src.m[8] + a3 * src.m[12] 147 | this.m[5] = a0 * src.m[1] + a1 * src.m[5] + a2 * src.m[9] + a3 * src.m[13] 148 | this.m[6] = a0 * src.m[2] + a1 * src.m[6] + a2 * src.m[10] + a3 * src.m[14] 149 | this.m[7] = a0 * src.m[3] + a1 * src.m[7] + a2 * src.m[11] + a3 * src.m[15] 150 | 151 | a0 = this.m[8] 152 | a1 = this.m[9] 153 | a2 = this.m[10] 154 | a3 = this.m[11] 155 | this.m[8] = a0 * src.m[0] + a1 * src.m[4] + a2 * src.m[8] + a3 * src.m[12] 156 | this.m[9] = a0 * src.m[1] + a1 * src.m[5] + a2 * src.m[9] + a3 * src.m[13] 157 | this.m[10] = a0 * src.m[2] + a1 * src.m[6] + a2 * src.m[10] + a3 * src.m[14] 158 | this.m[11] = a0 * src.m[3] + a1 * src.m[7] + a2 * src.m[11] + a3 * src.m[15] 159 | 160 | a0 = this.m[12] 161 | a1 = this.m[13] 162 | a2 = this.m[14] 163 | a3 = this.m[15] 164 | this.m[12] = a0 * src.m[0] + a1 * src.m[4] + a2 * src.m[8] + a3 * src.m[12] 165 | this.m[13] = a0 * src.m[1] + a1 * src.m[5] + a2 * src.m[9] + a3 * src.m[13] 166 | this.m[14] = a0 * src.m[2] + a1 * src.m[6] + a2 * src.m[10] + a3 * src.m[14] 167 | this.m[15] = a0 * src.m[3] + a1 * src.m[7] + a2 * src.m[11] + a3 * src.m[15] 168 | } 169 | 170 | perspective(fov, aspect, near, far) 171 | { 172 | const f = 1.0 / Math.tan(fov / 2) 173 | const nf = 1 / (near - far) 174 | 175 | this.m[0] = f / aspect 176 | this.m[1] = 0 177 | this.m[2] = 0 178 | this.m[3] = 0 179 | 180 | this.m[4] = 0 181 | this.m[5] = f 182 | this.m[6] = 0 183 | this.m[7] = 0 184 | 185 | this.m[8] = 0 186 | this.m[9] = 0 187 | this.m[10] = (far + near) * nf 188 | this.m[11] = -1 189 | 190 | this.m[12] = 0 191 | this.m[13] = 0 192 | this.m[14] = (2 * far * near) * nf 193 | this.m[15] = 0 194 | } 195 | 196 | ortho(left, right, bottom, top, zNear, zFar) 197 | { 198 | this.m[0] = 2.0 / (right - left) 199 | this.m[1] = 0.0 200 | this.m[2] = 0.0 201 | this.m[3] = 0.0 202 | 203 | this.m[4] = 0.0 204 | this.m[5] = 2.0 / (top - bottom) 205 | this.m[6] = 0.0 206 | this.m[7] = 0.0 207 | 208 | this.m[8] = 0.0 209 | this.m[9] = 0.0 210 | this.m[10] = -2.0 / (zFar - zNear) 211 | this.m[11] = 0.0 212 | 213 | this.m[12] = -(right + left) / (right - left) 214 | this.m[13] = -(top + bottom) / (top - bottom) 215 | this.m[14] = -(zFar + zNear) / (zFar - zNear) 216 | this.m[15] = 1.0 217 | } 218 | 219 | lookAt(position, target, up) 220 | { 221 | const Px = position.x 222 | const Py = position.y 223 | const Pz = position.z 224 | const targetX = target.x 225 | const targetY = target.y 226 | const targetZ = target.z 227 | const upX = up.x 228 | const upY = up.y 229 | const upZ = up.z 230 | 231 | // direction 232 | let Dx = Px - targetX 233 | let Dy = Py - targetY 234 | let Dz = Pz - targetZ 235 | 236 | if(Math.abs(Dx) < EPSILON && 237 | Math.abs(Dy) < EPSILON && 238 | Math.abs(Dz) < EPSILON) 239 | { 240 | return this.identity() 241 | } 242 | 243 | let lenght = 1 / Math.sqrt((Dx * Dx) + (Dy * Dy) + (Dz * Dz)) 244 | Dx *= lenght 245 | Dy *= lenght 246 | Dz *= lenght 247 | 248 | // right axis 249 | let Rx = upY * Dz - upZ * Dy 250 | let Ry = upZ * Dx - upX * Dz 251 | let Rz = upX * Dy - upY * Dx 252 | 253 | lenght = Math.sqrt((Rx * Rx) + (Ry * Ry) + (Rz * Rz)) 254 | if(!lenght) { 255 | Rx = 0 256 | Ry = 0 257 | Rz = 0 258 | } 259 | else { 260 | lenght = 1 / lenght 261 | Rx *= lenght 262 | Ry *= lenght 263 | Rz *= lenght 264 | } 265 | 266 | // up axis 267 | let Ux = Dy * Rz - Dz * Ry 268 | let Uy = Dz * Rx - Dx * Rz 269 | let Uz = Dx * Ry - Dy * Rx 270 | 271 | lenght = Math.sqrt((Ux * Ux) + (Uy * Uy) + (Uz * Uz)) 272 | if(!lenght) { 273 | Ux = 0 274 | Uy = 0 275 | Uz = 0 276 | } 277 | else { 278 | lenght = 1 / lenght 279 | Ux *= lenght 280 | Uy *= lenght 281 | Uz *= lenght 282 | } 283 | 284 | // 285 | this.m[0] = Rx 286 | this.m[1] = Ux 287 | this.m[2] = Dx 288 | this.m[3] = 0 289 | this.m[4] = Ry 290 | this.m[5] = Uy 291 | this.m[6] = Dy 292 | this.m[7] = 0 293 | this.m[8] = Rz 294 | this.m[9] = Uz 295 | this.m[10] = Dz 296 | this.m[11] = 0 297 | this.m[12] = -((Rx * Px) + (Ry * Py) + (Rz * Pz)) 298 | this.m[13] = -((Ux * Px) + (Uy * Py) + (Uz * Pz)) 299 | this.m[14] = -((Dx * Px) + (Dy * Py) + (Dz * Pz)) 300 | this.m[15] = 1 301 | } 302 | 303 | invert() 304 | { 305 | const a00 = this.m[0] 306 | const a01 = this.m[1] 307 | const a02 = this.m[2] 308 | const a03 = this.m[3] 309 | const a10 = this.m[4] 310 | const a11 = this.m[5] 311 | const a12 = this.m[6] 312 | const a13 = this.m[7] 313 | const a20 = this.m[8] 314 | const a21 = this.m[9] 315 | const a22 = this.m[10] 316 | const a23 = this.m[11] 317 | const a30 = this.m[12] 318 | const a31 = this.m[13] 319 | const a32 = this.m[14] 320 | const a33 = this.m[15] 321 | 322 | const b00 = a00 * a11 - a01 * a10 323 | const b01 = a00 * a12 - a02 * a10 324 | const b02 = a00 * a13 - a03 * a10 325 | const b03 = a01 * a12 - a02 * a11 326 | const b04 = a01 * a13 - a03 * a11 327 | const b05 = a02 * a13 - a03 * a12 328 | const b06 = a20 * a31 - a21 * a30 329 | const b07 = a20 * a32 - a22 * a30 330 | const b08 = a20 * a33 - a23 * a30 331 | const b09 = a21 * a32 - a22 * a31 332 | const b10 = a21 * a33 - a23 * a31 333 | const b11 = a22 * a33 - a23 * a32 334 | 335 | let det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06 336 | if(!det) { 337 | this.identity() 338 | return 339 | } 340 | det = 1.0 / det 341 | 342 | this.m[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det 343 | this.m[1] = (a02 * b10 - a01 * b11 - a03 * b09) * det 344 | this.m[2] = (a31 * b05 - a32 * b04 + a33 * b03) * det 345 | this.m[3] = (a22 * b04 - a21 * b05 - a23 * b03) * det 346 | this.m[4] = (a12 * b08 - a10 * b11 - a13 * b07) * det 347 | this.m[5] = (a00 * b11 - a02 * b08 + a03 * b07) * det 348 | this.m[6] = (a32 * b02 - a30 * b05 - a33 * b01) * det 349 | this.m[7] = (a20 * b05 - a22 * b02 + a23 * b01) * det 350 | this.m[8] = (a10 * b10 - a11 * b08 + a13 * b06) * det 351 | this.m[9] = (a01 * b08 - a00 * b10 - a03 * b06) * det 352 | this.m[10] = (a30 * b04 - a31 * b02 + a33 * b00) * det 353 | this.m[11] = (a21 * b02 - a20 * b04 - a23 * b00) * det 354 | this.m[12] = (a11 * b07 - a10 * b09 - a12 * b06) * det 355 | this.m[13] = (a00 * b09 - a01 * b07 + a02 * b06) * det 356 | this.m[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det 357 | this.m[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det 358 | } 359 | 360 | transpose() 361 | { 362 | const a01 = this.m[1] 363 | const a02 = this.m[2] 364 | const a03 = this.m[3] 365 | const a12 = this.m[6] 366 | const a13 = this.m[7] 367 | const a23 = this.m[11] 368 | 369 | this.m[1] = this.m[4] 370 | this.m[2] = this.m[8] 371 | this.m[3] = this.m[12] 372 | this.m[4] = a01 373 | this.m[6] = this.m[9] 374 | this.m[7] = this.m[13] 375 | this.m[8] = a02 376 | this.m[9] = a12 377 | this.m[11] = this.m[14] 378 | this.m[12] = a03 379 | this.m[13] = a13 380 | this.m[14] = a23 381 | } 382 | 383 | print() 384 | { 385 | return `Matrix4(${this.m[0]}, ${this.m[1]}, ${this.m[2]}, ${this.m[3]}, 386 | ${this.m[4]}, ${this.m[5]}, ${this.m[6]}, ${this.m[7]}, 387 | ${this.m[8]}, ${this.m[9]}, ${this.m[10]}, ${this.m[11]}, 388 | ${this.m[12]}, ${this.m[13]}, ${this.m[14]}, ${this.m[15]})` 389 | } 390 | } 391 | 392 | export default Matrix4 393 | -------------------------------------------------------------------------------- /src/math/Random.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | class Random 4 | { 5 | constructor() { 6 | this.seed = 0 7 | this.a = 0 8 | this.m = 0 9 | this.q = 0 10 | this.r = 0 11 | this.oneOverM = 0 12 | this.setSeed(3456789012, false) 13 | } 14 | 15 | generate() 16 | { 17 | const hi = Math.floor(this.seed / this.q) 18 | const lo = this.seed % this.q 19 | const test = this.a * lo - this.r * hi 20 | 21 | if(test > 0) { 22 | this.seed = test 23 | } 24 | else { 25 | this.seed = test + this.m 26 | } 27 | 28 | return (this.seed * this.oneOverM) 29 | } 30 | 31 | number(min, max) { 32 | const number = this.generate() 33 | return Math.round((max - min) * number + min) 34 | } 35 | 36 | numberF(min, max) { 37 | const number = this.generate() 38 | return ((max - min) * number + min) 39 | } 40 | 41 | setSeed(seed, useTime) 42 | { 43 | if(useTime === undefined) { 44 | useTime = true 45 | } 46 | 47 | if(useTime === true) { 48 | const date = new Date() 49 | this.seed = seed + (date.getSeconds() * 0xFFFFFF) + (date.getMinutes() * 0xFFFF) 50 | } 51 | else { 52 | this.seed = seed 53 | } 54 | 55 | this.a = 48271 56 | this.m = 2147483647 57 | this.q = Math.floor(this.m / this.a) 58 | this.r = this.m % this.a 59 | this.oneOverM = 1.0 / this.m 60 | } 61 | } 62 | 63 | const instance = new Random() 64 | export default instance -------------------------------------------------------------------------------- /src/math/Vector2.js: -------------------------------------------------------------------------------- 1 | 2 | class Vector2 3 | { 4 | constructor(x, y) { 5 | this.x = x || 0.0 6 | this.y = y || 0.0 7 | this.v = null 8 | } 9 | 10 | reset() { 11 | this.x = 0.0 12 | this.y = 0.0 13 | } 14 | 15 | set(x, y) { 16 | this.x = x 17 | this.y = y 18 | } 19 | 20 | copy(vec) { 21 | this.x = vec.x 22 | this.y = vec.y 23 | } 24 | 25 | add(x, y) { 26 | this.x += x 27 | this.y += y 28 | } 29 | 30 | addScalar(value) { 31 | this.x += value 32 | this.y += value 33 | } 34 | 35 | addValues(x, y) { 36 | this.x += x 37 | this.y += y 38 | } 39 | 40 | addVec(vec) { 41 | this.x += vec.x 42 | this.y += vec.y 43 | } 44 | 45 | sub(x, y) { 46 | this.x -= x 47 | this.y -= y 48 | } 49 | 50 | subScalar(value) { 51 | this.x -= value 52 | this.y -= value 53 | } 54 | 55 | subValues(x, y) { 56 | this.x -= x 57 | this.y -= y 58 | } 59 | 60 | subVec(vec) { 61 | this.x -= vec.x 62 | this.y -= vec.y 63 | } 64 | 65 | mul(x, y) { 66 | this.x *= x 67 | this.y *= y 68 | } 69 | 70 | mulScalar(value) { 71 | this.x *= value 72 | this.y *= value 73 | } 74 | 75 | mulValues(x, y) { 76 | this.x *= x 77 | this.y *= y 78 | } 79 | 80 | mulVec(vec) { 81 | this.x *= vec.x 82 | this.y *= vec.y 83 | } 84 | 85 | div(x, y) { 86 | this.x /= x 87 | this.y /= y 88 | } 89 | 90 | divScalar(value) { 91 | this.x /= value 92 | this.y /= value 93 | } 94 | 95 | divValues(x, y) { 96 | this.x /= x 97 | this.y /= y 98 | } 99 | 100 | divVec(vec) { 101 | this.x /= vec.x 102 | this.y /= vec.y 103 | } 104 | 105 | length() { 106 | return Math.sqrt((this.x * this.x) + (this.y * this.y)) 107 | } 108 | 109 | distance(x, y) { 110 | const diffX = this.x - x 111 | const diffY = this.y - y 112 | return Math.sqrt((diffX * diffX) + (diffY * diffY)) 113 | } 114 | 115 | normalize() 116 | { 117 | const length = Math.sqrt((this.x * this.x) + (this.y * this.y)) 118 | 119 | if(length > 0) { 120 | this.x /= length 121 | this.y /= length 122 | } 123 | else { 124 | this.x = 0 125 | this.y = 0 126 | } 127 | } 128 | 129 | dot(vec) { 130 | return ((this.x * vec.x) + (this.y * vec.y)) 131 | } 132 | 133 | truncate(max) 134 | { 135 | const length = Math.sqrt((this.x * this.x) + (this.y * this.y)) 136 | 137 | if(length > max) { 138 | this.x *= max / length 139 | this.y *= max / length 140 | } 141 | } 142 | 143 | limit(max) 144 | { 145 | if(this.x > max) { this.x = max } 146 | else if(this.x < -max) { this.x = -max } 147 | 148 | if(this.y > max) { this.y = max } 149 | else if(this.y < -max) { this.y = -max } 150 | } 151 | 152 | clamp(minX, minY, maxX, maxY) { 153 | this.x = Math.min(Math.max(this.x, minX), maxX) 154 | this.y = Math.min(Math.max(this.y, minY), maxY) 155 | } 156 | 157 | lengthSq() { 158 | return ((this.x * this.x) + (this.y * this.y)) 159 | } 160 | 161 | heading() { 162 | const constr = Math.atan2(-this.y, this.x) 163 | return -angle + Math.PI * 0.5 164 | } 165 | 166 | perp() 167 | { 168 | const tmpX = this.x 169 | this.x = -this.y 170 | this.y = tmpX 171 | } 172 | 173 | reflect(normal) 174 | { 175 | const value = this.dot(normal) 176 | this.x -= 2 * value * normal.x 177 | this.y -= 2 * value * normal.y 178 | } 179 | 180 | toFloat32Array() 181 | { 182 | if(!this.v) { 183 | this.v = new Float32Array([ this.x, this.y ]) 184 | } 185 | else { 186 | this.v[0] = this.x 187 | this.v[1] = this.y 188 | } 189 | 190 | return this.v 191 | } 192 | 193 | fromArray(array) { 194 | this.x = array[0] 195 | this.y = array[1] 196 | } 197 | 198 | print(text) 199 | { 200 | if(text) { 201 | console.log(`${text} Vector2(${this.x}, ${this.y})`) 202 | } 203 | else { 204 | console.log(`Vector2(${this.x}, ${this.y})`) 205 | } 206 | } 207 | } 208 | 209 | export default Vector2 -------------------------------------------------------------------------------- /src/math/Vector3.js: -------------------------------------------------------------------------------- 1 | 2 | class Vector3 3 | { 4 | constructor(x, y, z) { 5 | this.x = x || 0.0 6 | this.y = y || 0.0 7 | this.z = z || 0.0 8 | this.v = null 9 | } 10 | 11 | reset() { 12 | this.x = 0.0 13 | this.y = 0.0 14 | this.z = 0.0 15 | } 16 | 17 | set(x, y, z) { 18 | this.x = x 19 | this.y = (y === undefined) ? x : y 20 | this.z = (z === undefined) ? x : z 21 | } 22 | 23 | scalar(value) { 24 | this.x = value 25 | this.y = value 26 | this.z = value 27 | } 28 | 29 | clone() { 30 | return new Vector3(this.x, this.y, this.z) 31 | } 32 | 33 | copy(vec3) { 34 | this.x = vec3.x 35 | this.y = vec3.y 36 | this.z = vec3.z 37 | } 38 | 39 | add(vec3) { 40 | this.x += vec3.x 41 | this.y += vec3.y 42 | this.z += vec3.z 43 | } 44 | 45 | addScalar(value) { 46 | this.x += value 47 | this.y += value 48 | this.z += value 49 | } 50 | 51 | addValues(x, y, z) { 52 | this.x += x 53 | this.y += y 54 | this.z += z 55 | } 56 | 57 | sub(vec3) { 58 | this.x -= vec3.x 59 | this.y -= vec3.y 60 | this.z -= vec3.z 61 | } 62 | 63 | subScalar(value) { 64 | this.x -= value 65 | this.y -= value 66 | this.z -= value 67 | } 68 | 69 | subValues(x, y, z) { 70 | this.x -= x 71 | this.y -= y 72 | this.z -= z 73 | } 74 | 75 | mul(vec3) { 76 | this.x *= vec3.x 77 | this.y *= vec3.y 78 | this.z *= vec3.z 79 | } 80 | 81 | mulScalar(value) { 82 | this.x *= value 83 | this.y *= value 84 | this.z *= value 85 | } 86 | 87 | mulValues(x, y, z) { 88 | this.x *= x 89 | this.y *= y 90 | this.z *= z 91 | } 92 | 93 | div(vec3) { 94 | this.x /= vec3.x 95 | this.y /= vec3.y 96 | this.z /= vec3.z 97 | } 98 | 99 | divScalar(value) { 100 | this.x /= value 101 | this.y /= value 102 | this.z /= value 103 | } 104 | 105 | divValues(x, y, z) { 106 | this.x /= x 107 | this.y /= y 108 | this.z /= z 109 | } 110 | 111 | applyMatrix3(matrix3) 112 | { 113 | const m = matrix.m 114 | 115 | const x = this.x 116 | const y = this.y 117 | const z = this.z 118 | 119 | this.x = m[0] * x + m[3] * y + m[ 6 ] * z 120 | this.y = m[1] * x + m[4] * y + m[ 7 ] * z 121 | this.z = m[2] * x + m[5] * y + m[ 8 ] * z 122 | } 123 | 124 | applyMatrix4(matrix4) 125 | { 126 | const m = matrix.m 127 | 128 | const x = this.x 129 | const y = this.y 130 | const z = this.z 131 | 132 | this.x = m[0] * x + m[4] * y + m[8] * z + m[12] 133 | this.y = m[1] * x + m[5] * y + m[9] * z + m[13] 134 | this.z = m[2] * x + m[6] * y + m[10] * z + m[14] 135 | const w = m[3] * x + m[7] * y + m[11] * z + m[15] 136 | 137 | this.divScalar(w) 138 | } 139 | 140 | applyQuaternion(q) 141 | { 142 | const x = this.x 143 | const y = this.y 144 | const z = this.z 145 | 146 | const qx = q.x 147 | const qy = q.y 148 | const qz = q.z 149 | const qw = q.w 150 | 151 | const ix = qw * x + qy * z - qz * y 152 | const iy = qw * y + qz * x - qx * z 153 | const iz = qw * z + qx * y - qy * x 154 | const iw = - qx * x - qy * y - qz * z 155 | 156 | this.x = ix * qw + iw * - qx + iy * - qz - iz * - qy 157 | this.y = iy * qw + iw * - qy + iz * - qx - ix * - qz 158 | this.z = iz * qw + iw * - qz + ix * - qy - iy * - qx 159 | } 160 | 161 | min(vec3) { 162 | this.x = Math.min(this.x, vec3.x) 163 | this.y = Math.min(this.y, vec3.y) 164 | this.z = Math.min(this.z, vec3.z) 165 | } 166 | 167 | max() { 168 | this.x = Math.max(this.x, vec3.x) 169 | this.y = Math.max(this.y, vec3.y) 170 | this.z = Math.max(this.z, vec3.z) 171 | } 172 | 173 | clamp(min, max) { 174 | this.x = Math.max(min, Math.min(max, this.x)) 175 | this.y = Math.max(min, Math.min(max, this.y)) 176 | this.z = Math.max(min, Math.min(max, this.z)) 177 | } 178 | 179 | floor() { 180 | this.x = Math.floor(this.x) 181 | this.y = Math.floor(this.y) 182 | this.z = Math.floor(this.z) 183 | } 184 | 185 | ceil() { 186 | this.x = Math.ceil(this.x) 187 | this.y = Math.ceil(this.y) 188 | this.z = Math.ceil(this.z) 189 | } 190 | 191 | round() { 192 | this.x = Math.round(this.x) 193 | this.y = Math.round(this.y) 194 | this.z = Math.round(this.z) 195 | } 196 | 197 | roundToZero() { 198 | this.x = (this.x < 0) ? Math.ceil(this.x) : Math.floor(this.x) 199 | this.y = (this.y < 0) ? Math.ceil(this.y) : Math.floor(this.y) 200 | this.z = (this.z < 0) ? Math.ceil(this.z) : Math.floor(this.z) 201 | } 202 | 203 | negate() { 204 | this.x = -this.x 205 | this.y = -this.y 206 | this.z = -this.z 207 | } 208 | 209 | dot(vec) { 210 | return this.x * vec.x + this.y * vec.y + this.z * vec.z 211 | } 212 | 213 | length() { 214 | return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z) 215 | } 216 | 217 | lengthManhattan() { 218 | return Math.abs(this.x) + Math.abs(this.y) + Math.abs(this.z) 219 | } 220 | 221 | normalize() 222 | { 223 | let length = this.x * this.x + this.y * this.y + this.z * this.z 224 | if(length > 0) 225 | { 226 | length = 1 / Math.sqrt(length) 227 | this.x *= length 228 | this.y *= length 229 | this.z *= length 230 | } 231 | else 232 | { 233 | this.x = 0.0 234 | this.y = 0.0 235 | this.z = 0.0 236 | } 237 | } 238 | 239 | lerp(src, alpha) { 240 | this.x += (src.x - this.x) * alpha 241 | this.y += (src.y - this.y) * alpha 242 | this.z += (src.z - this.z) * alpha 243 | } 244 | 245 | cross(src) 246 | { 247 | const x = this.x 248 | const y = this.y 249 | const z = this.z 250 | 251 | this.x = y * src.z - z * src.y 252 | this.y = z * src.x - x * src.z 253 | this.z = x * src.y - y * src.x 254 | } 255 | 256 | distanceToSquared(src) 257 | { 258 | const dx = this.x - src.x 259 | const dy = this.y - src.y 260 | const dz = this.z - src.z 261 | 262 | return dx * dx + dy * dy + dz * dz 263 | } 264 | 265 | distanceTo(src) 266 | { 267 | const dx = this.x - src.x 268 | const dy = this.y - src.y 269 | const dz = this.z - src.z 270 | 271 | return Math.sqrt(dx * dx + dy * dy + dz * dz) 272 | } 273 | 274 | equals(src) { 275 | return ((this.x === src.x) && (this.y === src.y) && (this.z === src.z)) 276 | } 277 | 278 | toFloat32Array() 279 | { 280 | if(!this.v) { 281 | this.v = new Float32Array([ this.x, this.y, this.z ]) 282 | } 283 | else { 284 | this.v[0] = this.x 285 | this.v[1] = this.y 286 | this.v[2] = this.z 287 | } 288 | 289 | return this.v 290 | } 291 | 292 | fromArray(array) { 293 | this.x = array[0] 294 | this.y = array[1] 295 | this.z = array[2] 296 | } 297 | 298 | print(text) 299 | { 300 | if(text) { 301 | console.log(`${text} Vector3(${this.x}, ${this.y}, ${this.z})`) 302 | } 303 | else { 304 | console.log(`Vector3(${this.x}, ${this.y}, ${this.z})`) 305 | } 306 | } 307 | } 308 | 309 | export default Vector3 -------------------------------------------------------------------------------- /src/math/Vector4.js: -------------------------------------------------------------------------------- 1 | 2 | class Vector4 3 | { 4 | constructor(x, y, z, w) { 5 | this.x = x || 0.0 6 | this.y = y || 0.0 7 | this.z = z || 0.0 8 | this.w = w || 0.0 9 | this.v = null 10 | } 11 | 12 | reset() { 13 | this.x = 0.0 14 | this.y = 0.0 15 | this.z = 0.0 16 | this.w = 0.0 17 | } 18 | 19 | set(x, y, z, w) { 20 | this.x = x 21 | this.y = y 22 | this.z = z 23 | this.w = w 24 | } 25 | 26 | scalar(value) { 27 | this.x = value 28 | this.y = value 29 | this.z = value 30 | this.w = value 31 | } 32 | 33 | clone() { 34 | return new Vector4(this.x, this.y, this.z, this.w) 35 | } 36 | 37 | copy(vec4) { 38 | this.x = vec4.x 39 | this.y = vec4.y 40 | this.z = vec4.z 41 | this.w = vec4.w 42 | } 43 | 44 | add(vec4) { 45 | this.x += vec4.x 46 | this.y += vec4.y 47 | this.z += vec4.z 48 | this.w += vec4.w 49 | } 50 | 51 | addScalar(value) { 52 | this.x += value 53 | this.y += value 54 | this.z += value 55 | this.w += value 56 | } 57 | 58 | addValues(x, y, z, w) { 59 | this.x += x 60 | this.y += y 61 | this.z += z 62 | this.w += w 63 | } 64 | 65 | sub(vec4) { 66 | this.x -= vec4.x 67 | this.y -= vec4.y 68 | this.z -= vec4.z 69 | this.w -= vec4.w 70 | } 71 | 72 | subScalar(value) { 73 | this.x -= value 74 | this.y -= value 75 | this.z -= value 76 | this.w -= value 77 | } 78 | 79 | subValues(x, y, z, w) { 80 | this.x -= x 81 | this.y -= y 82 | this.z -= z 83 | this.w -= w 84 | } 85 | 86 | mul(vec4) { 87 | this.x *= vec4.x 88 | this.y *= vec4.y 89 | this.z *= vec4.z 90 | this.w *= vec4.w 91 | } 92 | 93 | mulScalar(value) { 94 | this.x *= value 95 | this.y *= value 96 | this.z *= value 97 | this.w *= value 98 | } 99 | 100 | mulValues(x, y, z, w) { 101 | this.x *= x 102 | this.y *= y 103 | this.z *= z 104 | this.w *= w 105 | } 106 | 107 | div(vec4) { 108 | this.x /= vec4.x 109 | this.y /= vec4.y 110 | this.z /= vec4.z 111 | this.w /= vec4.w 112 | } 113 | 114 | divScalar(value) { 115 | this.x /= value 116 | this.y /= value 117 | this.z /= value 118 | this.w /= value 119 | } 120 | 121 | divValues(x, y, z, w) { 122 | this.x /= x 123 | this.y /= y 124 | this.z /= z 125 | this.w /= w 126 | } 127 | 128 | min(min) { 129 | this.x = Math.min(this.x, min.x) 130 | this.y = Math.min(this.y, min.y) 131 | this.z = Math.min(this.z, min.z) 132 | this.w = Math.min(this.w, min.w) 133 | } 134 | 135 | max(max) { 136 | this.x = Math.max(this.x, max.x) 137 | this.y = Math.max(this.y, max.y) 138 | this.z = Math.max(this.z, max.z) 139 | this.w = Math.max(this.w, max.w) 140 | } 141 | 142 | clamp(min, max) { 143 | this.x = Math.max(min, Math.min(max, this.x)) 144 | this.y = Math.max(min, Math.min(max, this.y)) 145 | this.z = Math.max(min, Math.min(max, this.z)) 146 | this.w = Math.max(min, Math.min(max, this.z)) 147 | } 148 | 149 | floor() { 150 | this.x = Math.floor(this.x) 151 | this.y = Math.floor(this.y) 152 | this.z = Math.floor(this.z) 153 | this.w = Math.floor(this.w) 154 | } 155 | 156 | ceil() { 157 | this.x = Math.ceil(this.x) 158 | this.y = Math.ceil(this.y) 159 | this.z = Math.ceil(this.z) 160 | this.w = Math.ceil(this.w) 161 | } 162 | 163 | round() { 164 | this.x = Math.round(this.x) 165 | this.y = Math.round(this.y) 166 | this.z = Math.round(this.z) 167 | this.w = Math.round(this.w) 168 | } 169 | 170 | roundToZero() { 171 | this.x = (this.x < 0) ? Math.ceil(this.x) : Math.floor(this.x) 172 | this.y = (this.y < 0) ? Math.ceil(this.y) : Math.floor(this.y) 173 | this.z = (this.z < 0) ? Math.ceil(this.z) : Math.floor(this.z) 174 | this.w = (this.w < 0) ? Math.ceil(this.w) : Math.floor(this.w) 175 | } 176 | 177 | negate() { 178 | this.x = -this.x 179 | this.y = -this.y 180 | this.z = -this.z 181 | this.w = -this.w 182 | } 183 | 184 | dot(vec) { 185 | return this.x * vec.x + this.y * vec.y + this.z * vec.z 186 | } 187 | 188 | length() { 189 | return Math.sqrt((this.x * this.x) + (this.y * this.y) + (this.z * this.z) + (this.w * this.w)) 190 | } 191 | 192 | normalize() 193 | { 194 | let length = (this.x * this.x) + (this.y * this.y) + (this.z * this.z) + (this.w * this.w) 195 | if(length > 0) 196 | { 197 | length = 1 / Math.sqrt(length) 198 | this.x *= length 199 | this.y *= length 200 | this.z *= length 201 | this.w *= length 202 | } 203 | else 204 | { 205 | this.x = 0.0 206 | this.y = 0.0 207 | this.z = 0.0 208 | this.w = 0.0 209 | } 210 | } 211 | 212 | lerp(src, alpha) { 213 | this.x += (src.x - this.x) * alpha 214 | this.y += (src.y - this.y) * alpha 215 | this.z += (src.z - this.z) * alpha 216 | this.w += (src.w - this.w) * alpha 217 | } 218 | 219 | distanceToSquared(src) 220 | { 221 | const dx = this.x - src.x 222 | const dy = this.y - src.y 223 | const dz = this.z - src.z 224 | const dw = this.w - src.w 225 | 226 | return (dx * dx) + (dy * dy) + (dz * dz) + (dw * dw) 227 | } 228 | 229 | distanceTo(src) 230 | { 231 | const dx = this.x - src.x 232 | const dy = this.y - src.y 233 | const dz = this.z - src.z 234 | const dw = this.w - src.w 235 | 236 | return Math.sqrt((dx * dx) + (dy * dy) + (dz * dz) + (dw * dw)) 237 | } 238 | 239 | equals(src) { 240 | return ((this.x === src.x) && (this.y === src.y) && (this.z === src.z) && (this.w === src.w)) 241 | } 242 | 243 | toFloat32Array() 244 | { 245 | if(!this.v) { 246 | this.v = new Float32Array([ this.x, this.y, this.z, this.w ]) 247 | } 248 | else { 249 | this.v[0] = this.x 250 | this.v[1] = this.y 251 | this.v[2] = this.z 252 | this.v[3] = this.w 253 | } 254 | 255 | return this.v 256 | } 257 | 258 | fromArray(array) { 259 | this.x = array[0] 260 | this.y = array[1] 261 | this.z = array[2] 262 | this.w = array[3] 263 | } 264 | 265 | print(text) 266 | { 267 | if(text) { 268 | console.log(`${text} Vector4(${this.x}, ${this.y}, ${this.z}, ${this.w})`) 269 | } 270 | else { 271 | console.log(`Vector4(${this.x}, ${this.y}, ${this.z}, ${this.w})`) 272 | } 273 | } 274 | } 275 | 276 | export default Vector4 -------------------------------------------------------------------------------- /src/mesh/Mesh.js: -------------------------------------------------------------------------------- 1 | import Engine from "../Engine" 2 | 3 | class Mesh { 4 | constructor(buffer, indices) { 5 | const gl = Engine.gl 6 | 7 | if(!buffer) { 8 | buffer = Mesh.defaultBuffer 9 | } 10 | if(!indices) { 11 | indices = Mesh.defaultIndices 12 | } 13 | 14 | this.buffer = gl.createBuffer() 15 | Engine.renderer.updateBuffer(this.buffer, buffer) 16 | 17 | this.stride = 16 18 | 19 | if(indices) { 20 | this.numElements = indices.length 21 | this.indices = gl.createBuffer() 22 | Engine.renderer.updateIndices(this.indices, indices) 23 | } 24 | else { 25 | this.numElements = 0 26 | this.indices = null 27 | } 28 | } 29 | 30 | upload(data) { 31 | Engine.renderer.updateBuffer(this.buffer, data) 32 | } 33 | 34 | uploadIndices(indices) { 35 | this.numElements = indices.length 36 | Engine.renderer.updateIndices(this.indices, indices) 37 | } 38 | } 39 | 40 | Mesh.defaultBuffer = new Float32Array([ 41 | 1.0, 1.0, 1.0, 1.0, 42 | 0.0, 1.0, 0.0, 1.0, 43 | 0.0, 0.0, 0.0, 0.0, 44 | 1.0, 0.0, 1.0, 0.0 45 | ]) 46 | Mesh.defaultIndices = new Uint16Array([ 47 | 0, 2, 1, 0, 3, 2 48 | ]) 49 | 50 | export default Mesh -------------------------------------------------------------------------------- /src/physics/Raycast.js: -------------------------------------------------------------------------------- 1 | import Stage from "../renderer/Stage" 2 | 3 | const onBuffer = (x, y, buffer, result, depth = Number.MAX_SAFE_INTEGER) => 4 | { 5 | let currDepth = 0 6 | result.length = 0 7 | 8 | for(let n = 0; n < buffer.length; n++) 9 | { 10 | const node = buffer[n] 11 | const transform = node.transform 12 | 13 | if(node.rotation !== 0) { 14 | const offsetX = x - transform.m[6] 15 | const offsetY = y - transform.m[7] 16 | x = offsetX * transform.cos + offsetY * transform.sin + transform.tx 17 | y = offsetY * transform.cos - offsetX * transform.sin + transform.ty 18 | } 19 | 20 | if(node.volume.vsPoint(x, y)) { 21 | result.push(node) 22 | currDepth++ 23 | if(depth === currDepth) { 24 | break 25 | } 26 | } 27 | } 28 | 29 | return result 30 | } 31 | 32 | const onLayer = (x, y, layerId, result, depth = Number.MAX_SAFE_INTEGER) => { 33 | let currDepth = 0 34 | result.length = 0 35 | 36 | const buffer = Stage.buffer 37 | for(let n = 0; n < buffer.length; n++) 38 | { 39 | const node = buffer[n] 40 | if(!node.drawCommand || node.drawCommand.layer !== layerId) { 41 | continue 42 | } 43 | const transform = node.transform 44 | 45 | if(node.rotation !== 0) { 46 | const offsetX = x - transform.m[6] 47 | const offsetY = y - transform.m[7] 48 | x = offsetX * transform.cos + offsetY * transform.sin + transform.m[6] 49 | y = offsetY * transform.cos - offsetX * transform.sin + transform.m[7] 50 | } 51 | 52 | if(node.volume.vsPoint(x, y)) { 53 | result.push(node) 54 | currDepth++ 55 | if(depth === currDepth) { 56 | break 57 | } 58 | } 59 | } 60 | 61 | return result 62 | } 63 | 64 | export { onBuffer, onLayer } -------------------------------------------------------------------------------- /src/renderer/DebugDrawCommand.js: -------------------------------------------------------------------------------- 1 | 2 | function DebugDrawCommand() { 3 | this.transform = null 4 | this.volume = null 5 | this.pivot = null 6 | } 7 | 8 | export default DebugDrawCommand -------------------------------------------------------------------------------- /src/renderer/DrawCommand.js: -------------------------------------------------------------------------------- 1 | import Engine from "../Engine" 2 | 3 | function DrawCommand(transform, mesh, material, uniforms, mode) { 4 | this.key = 0 5 | this.layer = 0 6 | this.transform = transform 7 | this.mesh = mesh 8 | this.material = material 9 | this.uniforms = uniforms 10 | this.mode = mode || Engine.gl.TRIANGLES 11 | } 12 | 13 | export default DrawCommand -------------------------------------------------------------------------------- /src/renderer/Renderer.js: -------------------------------------------------------------------------------- 1 | import Stage from "./Stage" 2 | import Engine from "../Engine" 3 | import Radix from "../RadixSort" 4 | import DebugDrawCommand from "./DebugDrawCommand" 5 | 6 | const defaultBufferSize = 64 7 | const layers = 8 8 | 9 | function Layer() { 10 | this.count = 0 11 | this.buffer = new Array(defaultBufferSize) 12 | } 13 | 14 | class Renderer { 15 | constructor() { 16 | this.layers = new Array(layers) 17 | for(let n = 0; n < layers; n++) { 18 | this.layers[n] = new Layer() 19 | } 20 | this.buffer = new Array(defaultBufferSize) 21 | this.debugDrawCommands = new Array() 22 | this.debugCount = 0 23 | } 24 | 25 | render() { 26 | const buffer = Stage.buffer 27 | for(let n = 0; n < buffer.length; n++) { 28 | buffer[n].draw() 29 | } 30 | 31 | const cameras = Engine.cameras 32 | for(let nLayer = 0; nLayer < layers; nLayer++) { 33 | const layer = this.layers[nLayer] 34 | if(layer.count === 0) { continue } 35 | 36 | Radix(layer.buffer, this.buffer, layer.count) 37 | for(let nCamera = 0; nCamera < cameras.length; nCamera++) { 38 | const camera = cameras[nCamera] 39 | if((camera.cullMask >> nLayer) % 2 !== 0) { 40 | this.camera = camera 41 | for(let n = 0; n < layer.count; n++) { 42 | this.drawCommand(this.buffer[n]) 43 | } 44 | } 45 | } 46 | layer.count = 0 47 | } 48 | 49 | this.camera = Engine.camera 50 | for(let n = 0; n < this.debugCount; n++) { 51 | this.drawCommandDebug(this.debugDrawCommands[n]) 52 | } 53 | this.debugCount = 0 54 | this.reset() 55 | } 56 | 57 | draw(command) { 58 | const layer = this.layers[command.layer] 59 | if(layer.count >= layer.length) { 60 | layer.buffer.length *= 2 61 | } 62 | layer.buffer[layer.count++] = command 63 | } 64 | 65 | drawDebug(transform, volume, pivot) { 66 | if(this.debugCount >= this.debugDrawCommands.length) { 67 | const prevSize = this.debugDrawCommands.length 68 | if(prevSize === 0) { 69 | this.debugDrawCommands.length += defaultBufferSize 70 | } 71 | else { 72 | this.debugDrawCommands.length *= 2 73 | } 74 | for(var n = prevSize; n < this.debugDrawCommands.length; n++) { 75 | this.debugDrawCommands[n] = new DebugDrawCommand() 76 | } 77 | } 78 | const command = this.debugDrawCommands[this.debugCount++] 79 | command.transform = transform 80 | command.volume = volume 81 | command.pivot = pivot 82 | } 83 | } 84 | 85 | export default Renderer -------------------------------------------------------------------------------- /src/renderer/RendererWebGL.js: -------------------------------------------------------------------------------- 1 | import Renderer from "./Renderer" 2 | import Engine from "../Engine" 3 | import Vector3 from "../math/Vector3" 4 | import Vector4 from "../math/Vector4" 5 | import Matrix3 from "../math/Matrix3" 6 | import Matrix4 from "../math/Matrix4" 7 | import Mesh from "../mesh/Mesh" 8 | import Texture from "../resources/Texture" 9 | import Material from "../resources/Material" 10 | import debugVertexSrc from "../../shaders/debug.vertex.glsl" 11 | import debugFragmentSrc from "../../shaders/debug.fragment.glsl" 12 | 13 | let debugMaterial = null 14 | const emptyVector3 = new Vector3() 15 | const emptyVector4 = new Vector4() 16 | const emptyMatrix3 = new Matrix3() 17 | const emptyMatrix4 = new Matrix4() 18 | let emptyTexture = null 19 | 20 | class RendererWebGL extends Renderer 21 | { 22 | constructor() { 23 | super() 24 | this.prevProjection = null 25 | this.prevView = null 26 | this.camera = null 27 | this.material = null 28 | this.prevBuffer = null 29 | this.prevIndiceBuffer = null 30 | Engine.on("setup", this.setup.bind(this)) 31 | } 32 | 33 | setup() { 34 | emptyTexture = new Texture() 35 | emptyTexture.loadEmpty() 36 | 37 | this.debugVertices = new Float32Array(24) 38 | const indices = new Int16Array([ 39 | 0, 5, 1, 0, 4, 5, // bottom 40 | 0, 3, 4, 3, 7, 4, // right 41 | 3, 2, 6, 3, 6, 7, // top 42 | 2, 1, 6, 1, 5, 6, // left 43 | 8, 10, 9, 8, 11, 10 // pivot 44 | ]) 45 | this.debugMesh = new Mesh(this.debugVertices, indices) 46 | this.debugMesh.stride = 8 47 | 48 | const gl = Engine.gl 49 | this.vboBuffer = gl.createBuffer() 50 | this.vboIndices = gl.createBuffer() 51 | 52 | debugMaterial = new Material() 53 | debugMaterial.loadFromConfig({ 54 | vertexSrc: debugVertexSrc, 55 | fragmentSrc: debugFragmentSrc 56 | }) 57 | } 58 | 59 | reset() { 60 | this.prevProjection = null 61 | this.prevView = null 62 | } 63 | 64 | drawCommand(command) { 65 | const gl = Engine.gl 66 | const mesh = command.mesh 67 | 68 | this.useMaterial(command.material) 69 | this.updateAttribs(command.mesh, command.material) 70 | this.updateUniforms(command.material, command.uniforms, command.transform) 71 | 72 | if(this.prevIndiceBuffer !== mesh.indices) { 73 | this.prevIndiceBuffer = mesh.indices 74 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, mesh.indices) 75 | } 76 | gl.drawElements(command.mode, mesh.numElements, gl.UNSIGNED_SHORT, 0) 77 | } 78 | 79 | drawCommandDebug(command) { 80 | const gl = Engine.gl 81 | const minX = command.volume.minX - command.transform.m[6] 82 | const minY = command.volume.minY - command.transform.m[7] 83 | const maxX = command.volume.maxX - command.transform.m[6] 84 | const maxY = command.volume.maxY - command.transform.m[7] 85 | const pivotX = minX + (command.pivot.x * command.volume.width) 86 | const pivotY = minY + (command.pivot.y * command.volume.height) 87 | 88 | this.debugVertices[0] = maxX 89 | this.debugVertices[1] = maxY 90 | this.debugVertices[2] = minX 91 | this.debugVertices[3] = maxY 92 | this.debugVertices[4] = minX 93 | this.debugVertices[5] = minY 94 | this.debugVertices[6] = maxX 95 | this.debugVertices[7] = minY 96 | this.debugVertices[8] = maxX - 2 97 | this.debugVertices[9] = maxY - 2 98 | this.debugVertices[10] = minX + 2 99 | this.debugVertices[11] = maxY - 2 100 | this.debugVertices[12] = minX + 2 101 | this.debugVertices[13] = minY + 2 102 | this.debugVertices[14] = maxX - 2 103 | this.debugVertices[15] = minY + 2 104 | this.debugVertices[16] = pivotX + 3 105 | this.debugVertices[17] = pivotY + 3 106 | this.debugVertices[18] = pivotX - 3 107 | this.debugVertices[19] = pivotY + 3 108 | this.debugVertices[20] = pivotX - 3 109 | this.debugVertices[21] = pivotY - 3 110 | this.debugVertices[22] = pivotX + 3 111 | this.debugVertices[23] = pivotY - 3 112 | this.debugMesh.upload(this.debugVertices) 113 | 114 | this.useMaterial(debugMaterial) 115 | this.updateAttribs(this.debugMesh, debugMaterial) 116 | this.updateUniforms(debugMaterial, { 117 | color: new Vector4(1, 0, 0, 1) 118 | }, command.transform) 119 | 120 | if(this.prevIndiceBuffer !== this.debugMesh.indices) { 121 | this.prevIndiceBuffer = this.debugMesh.indices 122 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.debugMesh.indices) 123 | } 124 | gl.drawElements(gl.TRIANGLES, this.debugMesh.numElements, gl.UNSIGNED_SHORT, 0) 125 | } 126 | 127 | useMaterial(material) 128 | { 129 | if(this.activeMaterial === material) { return } 130 | 131 | const gl = Engine.gl 132 | 133 | gl.useProgram(material.program) 134 | 135 | this.prevProjection = null 136 | this.prevView = null 137 | 138 | const currNumAttribs = this.activeMaterial ? this.activeMaterial.numAttribs : 0 139 | const newNumAttribs = material.numAttribs 140 | if(currNumAttribs < newNumAttribs) { 141 | for(let n = currNumAttribs; n < newNumAttribs; n++) { 142 | gl.enableVertexAttribArray(n) 143 | } 144 | } 145 | else { 146 | for(let n = newNumAttribs; n < currNumAttribs; n++) { 147 | gl.disableVertexAttribArray(n) 148 | } 149 | } 150 | 151 | this.activeMaterial = material 152 | } 153 | 154 | updateAttribs(mesh, material) { 155 | const gl = Engine.gl 156 | const attribData = material.attribData 157 | 158 | if(this.prevBuffer !== mesh.buffer) { 159 | this.prevBuffer = mesh.buffer 160 | gl.bindBuffer(gl.ARRAY_BUFFER, mesh.buffer) 161 | } 162 | 163 | for(let n = 0; n < attribData.length; n++) { 164 | const attrib = attribData[n] 165 | switch(attrib.name) 166 | { 167 | case "position": 168 | gl.vertexAttribPointer(attrib.loc, 2, gl.FLOAT, false, mesh.stride, 0) 169 | break 170 | 171 | case "uv": 172 | gl.vertexAttribPointer(attrib.loc, 2, gl.FLOAT, false, mesh.stride, 8) 173 | break 174 | } 175 | } 176 | } 177 | 178 | updateUniforms(material, uniforms, transform) { 179 | let numSamplers = 0 180 | 181 | const gl = Engine.gl 182 | const uniformData = material.uniformData 183 | 184 | for(let n = 0; n < uniformData.length; n++) { 185 | const uniform = uniformData[n] 186 | switch(uniform.type) 187 | { 188 | case gl.FLOAT_MAT4: 189 | { 190 | const matrix = material.uniforms[uniform.name] 191 | if(matrix) { 192 | gl.uniformMatrix4fv(uniform.loc, false, matrix.m) 193 | } 194 | else { 195 | gl.uniformMatrix4fv(uniform.loc, false, emptyMatrix.m) 196 | console.warn(`(Renderer.updateUniforms) Empty FLOAT_MAT4 uniform used for: ${uniform.name}`) 197 | } 198 | } break 199 | 200 | case gl.FLOAT_MAT3: 201 | { 202 | let matrix 203 | switch(uniform.name) 204 | { 205 | case "matrixProjection": 206 | matrix = this.camera.projectionTransform 207 | if(this.prevProjection === matrix) { 208 | continue 209 | } 210 | this.prevProjection = matrix 211 | break 212 | case "matrixView": 213 | matrix = this.camera.transform 214 | if(this.prevView === matrix) { 215 | continue 216 | } 217 | this.prevView = matrix 218 | break 219 | case "matrixModel": 220 | matrix = transform 221 | break 222 | default: 223 | matrix = material.uniforms[uniform.name] 224 | break 225 | } 226 | 227 | if(matrix) { 228 | gl.uniformMatrix3fv(uniform.loc, false, matrix.m) 229 | } 230 | else { 231 | gl.uniformMatrix3fv(uniform.loc, false, emptyMatrix.m) 232 | console.warn(`(Renderer.updateUniforms) Empty FLOAT_MAT3 uniform used for: ${uniform.name}`) 233 | } 234 | } break 235 | 236 | case gl.FLOAT_VEC3: 237 | { 238 | const vec = uniforms[uniform.name] 239 | if(vec) { 240 | gl.uniform3fv(uniform.loc, vec.toFloat32Array()) 241 | } 242 | else { 243 | gl.uniform3fv(uniform.loc, emptyVector3.toFloat32Array()) 244 | console.warn(`(Renderer.updateUniforms) Empty FLOAT_VEC3 uniform used for: ${uniform.name}`) 245 | } 246 | } break 247 | 248 | case gl.FLOAT_VEC4: 249 | { 250 | const vec = uniforms[uniform.name] 251 | if(vec) { 252 | gl.uniform4fv(uniform.loc, vec.toFloat32Array()) 253 | } 254 | else { 255 | gl.uniform4fv(uniform.loc, emptyVector4.toFloat32Array()) 256 | console.warn(`(Renderer.updateUniforms) Empty FLOAT_VEC4 uniform used for: ${uniform.name}`) 257 | } 258 | } break 259 | 260 | case gl.SAMPLER_2D: 261 | { 262 | const texture = uniforms[uniform.name] 263 | gl.activeTexture(gl.TEXTURE0 + numSamplers) 264 | 265 | if(texture) { 266 | gl.bindTexture(gl.TEXTURE_2D, texture) 267 | } 268 | else { 269 | gl.bindTexture(gl.TEXTURE_2D, emptyTexture.instance) 270 | } 271 | 272 | gl.uniform1i(uniform.loc, numSamplers++) 273 | } break 274 | 275 | case gl.FLOAT: 276 | { 277 | const float = uniforms[uniform.name] 278 | gl.uniform1f(uniform.loc, float || 0) 279 | } break 280 | } 281 | } 282 | } 283 | 284 | updateBuffer(buffer, data) { 285 | const gl = Engine.gl 286 | if(this.prevBuffer !== buffer) { 287 | this.prevBuffer = buffer 288 | gl.bindBuffer(gl.ARRAY_BUFFER, buffer) 289 | } 290 | gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW) 291 | } 292 | 293 | updateIndices(buffer, indices) { 294 | const gl = Engine.gl 295 | if(this.prevIndiceBuffer !== buffer) { 296 | this.prevIndiceBuffer = buffer 297 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer) 298 | } 299 | gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.DYNAMIC_DRAW) 300 | } 301 | } 302 | 303 | export default RendererWebGL -------------------------------------------------------------------------------- /src/renderer/Stage.js: -------------------------------------------------------------------------------- 1 | 2 | class Stage { 3 | constructor() { 4 | this.buffer = [] 5 | } 6 | 7 | add(entity) { 8 | this.buffer.push(entity) 9 | } 10 | 11 | remove(entity) { 12 | const index = this.buffer.indexOf(entity) 13 | if(index === -1) { return } 14 | this.buffer[index] = this.buffer[this.buffer.length - 1] 15 | this.buffer.pop() 16 | } 17 | 18 | update(entity) { 19 | 20 | } 21 | } 22 | 23 | const instance = new Stage() 24 | export default instance -------------------------------------------------------------------------------- /src/resources/Animation.js: -------------------------------------------------------------------------------- 1 | import Resources from "./Resources" 2 | import Resource from "./Resource" 3 | import Texture from "./Texture" 4 | import Spritesheet from "./Spritesheet" 5 | import Frame from "./Frame" 6 | 7 | class Animation extends Resource 8 | { 9 | constructor() { 10 | super() 11 | this.loadLater = true 12 | this.frames = [] 13 | this.delay = 0 14 | this.pauseLastFrame = false 15 | } 16 | 17 | loadFromConfig(config) { 18 | if(config.fps) { 19 | config.delay = 1000 / config.fps 20 | } 21 | this.delay = config.delay || 100 22 | this.pauseLastFrame = config.pauseLastFrame || false 23 | this.loadFrames(config.frames) 24 | } 25 | 26 | loadFrames(frames) { 27 | for(let n = 0; n < frames.length; n++) { 28 | const frameInfo = frames[n] 29 | if(typeof frameInfo === "string") { 30 | this.loadFrame(frameInfo, this.delay) 31 | } 32 | else { 33 | this.loadFrame(frameInfo.texture, frameInfo.delay || this.delay, frameInfo.regex) 34 | } 35 | } 36 | } 37 | 38 | loadFrame(texture, delay, regex) { 39 | const index = texture.indexOf("/") 40 | if(index === -1) { 41 | const source = Resources.get(texture) 42 | if(source instanceof Texture) { 43 | const sourceFrames = source.frames 44 | if(regex) { 45 | for(let key in sourceFrames) { 46 | if(!key.match(regex)) { 47 | continue 48 | } 49 | const sourceFrame = sourceFrames[key] 50 | const frame = new Frame(sourceFrame.texture, sourceFrame.coords, delay) 51 | this.frames.push(frame) 52 | } 53 | } 54 | else { 55 | for(let key in sourceFrames) { 56 | const sourceFrame = sourceFrames[key] 57 | const frame = new Frame(sourceFrame.texture, sourceFrame.coords, delay) 58 | this.frames.push(frame) 59 | } 60 | } 61 | } 62 | else { 63 | console.warn(`(Animation.loadFrames) Unsupported source type: ${source.name}`) 64 | return 65 | } 66 | } 67 | else { 68 | const sourceInfo = texture.split("/") 69 | const source = Resources.get(sourceInfo[0]) 70 | if(source instanceof Texture) { 71 | const sourceFrame = source.getFrame(sourceInfo[1]) 72 | const frame = new Frame(sourceFrame.texture, sourceFrame.coords, delay) 73 | this.frames.push(frame) 74 | } 75 | else { 76 | console.warn(`(Animation.loadFrames) Unsupported source type: ${source.name}`) 77 | return 78 | } 79 | } 80 | } 81 | 82 | getFrame(frameIndex) { 83 | return this.frames[frameIndex] 84 | } 85 | } 86 | 87 | export default Animation -------------------------------------------------------------------------------- /src/resources/Audio.js: -------------------------------------------------------------------------------- 1 | import Resources from "./Resources" 2 | import Input from "../input/Input" 3 | 4 | class Audio 5 | { 6 | constructor() { 7 | this._volume = 1 8 | this._mute = false 9 | this.ctx = new AudioContext() 10 | this.gainNode = this.ctx.createGain() 11 | this.gainNode.connect(this.ctx.destination) 12 | 13 | this.resumeOnClickFunc = this.resumeOnClick.bind(this) 14 | Input.on("down", this.resumeOnClickFunc) 15 | } 16 | 17 | set volume(volume) 18 | { 19 | if(volume < 0) { volume = 0 } 20 | else if(volume > 1) { volume = 1 } 21 | 22 | if(this._volume === volume) { return } 23 | this._volume = volume 24 | 25 | this.gainNode.gain.setValueAtTime(volume, 0) 26 | } 27 | 28 | get volume() { 29 | return this._volume 30 | } 31 | 32 | set mute(mute) 33 | { 34 | if(this._mute === mute) { return } 35 | this._mute = mute 36 | 37 | if(mute) { 38 | this.gainNode.gain.value = 0 39 | } 40 | else { 41 | this.gainNode.gain.setValueAtTime(this._volume, 0) 42 | } 43 | } 44 | 45 | get mute() { 46 | return this._mute 47 | } 48 | 49 | resumeOnClick() { 50 | const soundCls = Resources.Resource.__inherit.Sound 51 | const resources = Resources.resources 52 | for(let key in resources) { 53 | const resource = resources[key] 54 | if(resource instanceof soundCls) { 55 | resource.resume() 56 | } 57 | } 58 | Input.off("down", this.resumeOnClickFunc) 59 | } 60 | } 61 | 62 | const instance = new Audio() 63 | export default instance -------------------------------------------------------------------------------- /src/resources/Content.js: -------------------------------------------------------------------------------- 1 | import Resource from "./Resource" 2 | 3 | class Content extends Resource 4 | { 5 | constructor() { 6 | super() 7 | this.text = null 8 | } 9 | 10 | loadFromConfig(config) { 11 | this.loading = true 12 | this.loadFromPath(config.path) 13 | } 14 | 15 | loadFromPath(path) { 16 | this.loading = true 17 | fetch(path) 18 | .then((response) => response.text()) 19 | .then(this.loadFromText.bind(this)) 20 | } 21 | 22 | loadFromText(text) { 23 | this.text = text 24 | this.loading = false 25 | } 26 | } 27 | 28 | export default Content -------------------------------------------------------------------------------- /src/resources/Font.js: -------------------------------------------------------------------------------- 1 | import Spritesheet from "./Spritesheet" 2 | import Frame from "./Frame" 3 | import Utils from "../Utils" 4 | 5 | class Font extends Spritesheet { 6 | constructor() { 7 | super() 8 | this.kerning = new Array(256) 9 | this.kerningData = {} 10 | this.lineHeight = 0 11 | } 12 | 13 | loadFromPath(path) { 14 | this.loading = true 15 | 16 | const ext = Utils.getExt(path) 17 | switch(ext) { 18 | case "fnt": 19 | this.path = path 20 | fetch(path) 21 | .then(response => response.text()) 22 | .then(this.loadFromFnt.bind(this)) 23 | break 24 | default: 25 | super.loadFromPath(path) 26 | break 27 | } 28 | } 29 | 30 | loadFromJson(data) { 31 | super.loadFromJson(data) 32 | for(let key in this.frames) { 33 | const frame = this.frames[key] 34 | this.kerning[parseInt(key)] = frame.coords[0] 35 | } 36 | } 37 | 38 | loadFromFnt(data) { 39 | let textureFilename = null 40 | let widthUV = 0 41 | let heightUV = 0 42 | this.loading = true 43 | this.lineHeight = 0 44 | 45 | const buffer = data.split("\n") 46 | for(let n = 0; n < buffer.length; n++) { 47 | const line = buffer[n].trim().split(/\s+/) 48 | switch(line[0]) 49 | { 50 | case "char": 51 | const index = parseInt(line[1].split("=")[1]) 52 | const x = parseInt(line[2].split("=")[1]) 53 | const y = parseInt(line[3].split("=")[1]) 54 | const width = parseInt(line[4].split("=")[1]) 55 | const height = parseInt(line[5].split("=")[1]) 56 | const offsetX = parseInt(line[6].split("=")[1]) 57 | const offsetY = parseInt(line[7].split("=")[1]) 58 | const kerning = parseInt(line[8].split("=")[1]) 59 | const minX = x * widthUV 60 | const minY = y * heightUV 61 | const maxX = (x + width) * widthUV 62 | const maxY = (y + height) * heightUV 63 | this.frames[index] = new Frame(this, [ 64 | width + offsetX, height + offsetY, maxX, maxY, 65 | 0.0 + offsetX, height + offsetY, minX, maxY, 66 | 0.0 + offsetX, 0.0 + offsetY, minX, minY, 67 | width + offsetX, 0.0 + offsetY, maxX, minY 68 | ], 0) 69 | this.kerning[index] = kerning 70 | break 71 | 72 | case "kerning": 73 | const firstChar = parseInt(line[1].split("=")[1]) 74 | const secondChar = parseInt(line[2].split("=")[1]) 75 | const amount = parseInt(line[3].split("=")[1]) 76 | let firstCharKerning = this.kerningData[firstChar] 77 | if(!firstCharKerning) { 78 | firstCharKerning = {} 79 | this.kerningData[firstChar] = firstCharKerning 80 | } 81 | firstCharKerning[secondChar] = amount 82 | break 83 | 84 | case "page": 85 | textureFilename = line[2].split("=")[1].replace(/"/g, "") 86 | break 87 | 88 | case "common": 89 | this.lineHeight = parseInt(line[1].split("=")[1]) 90 | this.width = parseInt(line[3].split("=")[1]) 91 | this.height = parseInt(line[4].split("=")[1]) 92 | widthUV = 1.0 / this.width 93 | heightUV = 1.0 / this.height 94 | break 95 | } 96 | } 97 | 98 | if(textureFilename) { 99 | const rootIndex = this.path.lastIndexOf("/") 100 | const root = this.path.slice(0, rootIndex) 101 | super.loadFromPath(`${root}/${textureFilename}`) 102 | } 103 | else { 104 | const filenameStartIndex = this.path.lastIndexOf("/") 105 | const filenameEndIndex = this.path.lastIndexOf(".") 106 | const filename = this.path.slice(filenameStartIndex + 1, filenameEndIndex) 107 | const rootPath = Utils.getRootPath(this.path) 108 | super.loadFromPath(`${rootPath}/${filename}.png`) 109 | } 110 | } 111 | 112 | getKerning(firstChar, secondChar) 113 | { 114 | const firstCharData = this.kerningData[firstChar] 115 | if(!firstCharData) { return 0 } 116 | 117 | const secondCharData = firstCharData[secondChar] 118 | return secondCharData ? secondCharData : 0 119 | } 120 | } 121 | 122 | export default Font -------------------------------------------------------------------------------- /src/resources/Frame.js: -------------------------------------------------------------------------------- 1 | 2 | function Frame(texture, coords, delay) { 3 | this.texture = texture 4 | this.coords = new Float32Array(coords) 5 | this.delay = delay 6 | } 7 | export default Frame -------------------------------------------------------------------------------- /src/resources/Graphics.js: -------------------------------------------------------------------------------- 1 | import Texture from "./Texture" 2 | 3 | class Graphics extends Texture { 4 | constructor() { 5 | super() 6 | this.canvas = document.createElement("canvas") 7 | this.ctx = this.canvas.getContext("2d") 8 | this.updateFrames() 9 | this.needUpdate = true 10 | this.loaded = true 11 | } 12 | 13 | resize(width, height) { 14 | this.width = width 15 | this.height = height 16 | this.canvas.width = width 17 | this.canvas.height = height 18 | this.updateFrames() 19 | } 20 | 21 | getInstance() { 22 | if(this.needUpdate) { 23 | this.loadFromCanvas(this.canvas, false) 24 | this.needUpdate = false 25 | } 26 | return this.instance 27 | } 28 | 29 | clearRect(x, y, width, height) { 30 | if(x === undefined) { x = 0 } 31 | if(y === undefined) { y = 0 } 32 | if(width === undefined) { width = this.width } 33 | if(height === undefined) { height = this.height} 34 | this.ctx.clearRect(x, y, width, height) 35 | } 36 | 37 | fillRect(x, y, width, height) { 38 | if(x === undefined) { x = 0 } 39 | if(y === undefined) { y = 0 } 40 | if(width === undefined) { width = this.width } 41 | if(height === undefined) { height = this.height} 42 | this.ctx.fillRect(x, y, width, height) 43 | } 44 | 45 | strokeRect(x, y, width, height) { 46 | if(x === undefined) { x = 0.5 } 47 | if(y === undefined) { y = 0.5 } 48 | if(width === undefined) { width = this.width - 1 } 49 | if(height === undefined) { height = this.height - 1} 50 | this.ctx.strokeRect(x, y, width, height) 51 | } 52 | 53 | arc(x, y, r = 2, sAngle = 0, eAngle = 2 * Math.PI, counterClockwise = false) { 54 | this.ctx.beginPath() 55 | this.ctx.arc(x, y, r, sAngle, eAngle, counterClockwise) 56 | } 57 | 58 | fill() { 59 | this.ctx.fill() 60 | } 61 | 62 | stroke() { 63 | this.ctx.stroke() 64 | } 65 | 66 | set strokeStyle(strokeStyle) { 67 | this.ctx.strokeStyle = strokeStyle 68 | } 69 | 70 | get strokeStyle() { 71 | return this.ctx.strokeStyle 72 | } 73 | 74 | set fillStyle(fillStyle) { 75 | this.ctx.fillStyle = fillStyle 76 | } 77 | 78 | get fillStyle() { 79 | return this.ctx.fillStyle 80 | } 81 | 82 | set lineWidth(value) { 83 | this.ctx.lineWidth = value 84 | } 85 | 86 | get lineWidth() { 87 | return this.ctx.lineWidth 88 | } 89 | } 90 | 91 | export default Graphics -------------------------------------------------------------------------------- /src/resources/Material.js: -------------------------------------------------------------------------------- 1 | import Engine from "../Engine" 2 | import Resource from "./Resource" 3 | 4 | const requestTextFunc = (request) => { return request.text() } 5 | 6 | function Attrib(name, loc) { 7 | this.name = name 8 | this.loc = loc 9 | } 10 | 11 | function Uniform(name, loc, type) { 12 | this.name = name 13 | this.loc = loc 14 | this.type = type 15 | } 16 | 17 | class Material extends Resource 18 | { 19 | constructor() { 20 | super() 21 | this.program = null 22 | this.attrib = null 23 | this.attribData = null 24 | this.uniform = null 25 | this.uniformData = null 26 | this.numAttribs = 0 27 | this.uniforms = {} 28 | } 29 | 30 | loadFromConfig(config) { 31 | this.loading = true 32 | this.createProgram(config.vertexSrc, config.fragmentSrc) 33 | } 34 | 35 | loadShader(type, source) 36 | { 37 | const gl = Engine.gl 38 | const shader = gl.createShader(type) 39 | gl.shaderSource(shader, source) 40 | gl.compileShader(shader) 41 | 42 | if(!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { 43 | let shaderType 44 | switch(type) { 45 | case gl.VERTEX_SHADER: 46 | shaderType = "VERTEX_SHADER" 47 | break 48 | case gl.FRAGMENT_SHADER: 49 | shaderType = "FRAGMENT_SHADER" 50 | break 51 | } 52 | console.error(`${shaderType}: ${gl.getShaderInfoLog(shader)}`) 53 | gl.deleteShader(shader) 54 | return null 55 | } 56 | 57 | return shader 58 | } 59 | 60 | createProgram(vertexSource, fragmentSource) 61 | { 62 | const gl = Engine.gl 63 | const vertexShader = this.loadShader(gl.VERTEX_SHADER, vertexSource) 64 | const fragmentShader = this.loadShader(gl.FRAGMENT_SHADER, fragmentSource) 65 | this.program = gl.createProgram() 66 | gl.attachShader(this.program, vertexShader) 67 | gl.attachShader(this.program, fragmentShader) 68 | gl.linkProgram(this.program) 69 | 70 | if(!gl.getProgramParameter(this.program, gl.LINK_STATUS)) { 71 | console.error(`Unable to initialize the shader program: ${gl.getProgramInfoLog(this.program)}`) 72 | gl.deleteProgram(this.program) 73 | gl.deleteShader(vertexShader) 74 | gl.deleteShader(fragmentShader) 75 | return 76 | } 77 | 78 | this.extractAttribs() 79 | this.extractUniforms() 80 | 81 | this.loading = false 82 | } 83 | 84 | extractAttribs() 85 | { 86 | this.attrib = {} 87 | this.attribData = [] 88 | 89 | const gl = Engine.gl 90 | this.numAttribs = gl.getProgramParameter(this.program, gl.ACTIVE_ATTRIBUTES) 91 | for(let n = 0; n < this.numAttribs; n++) { 92 | const attrib = gl.getActiveAttrib(this.program, n) 93 | const attribLoc = gl.getAttribLocation(this.program, attrib.name) 94 | this.attrib[attrib.name] = attribLoc 95 | this.attribData.push(new Attrib(attrib.name, attribLoc)) 96 | } 97 | } 98 | 99 | extractUniforms() 100 | { 101 | this.uniform = {} 102 | this.uniformData = [] 103 | 104 | const gl = Engine.gl 105 | const num = gl.getProgramParameter(this.program, gl.ACTIVE_UNIFORMS); 106 | for(let n = 0; n < num; n++) { 107 | const uniform = gl.getActiveUniform(this.program, n) 108 | const name = uniform.name.replace("[0]", "") 109 | const loc = gl.getUniformLocation(this.program, name) 110 | this.uniform[name] = loc 111 | this.uniformData.push(new Uniform(name, loc, uniform.type)) 112 | } 113 | } 114 | } 115 | 116 | export default Material -------------------------------------------------------------------------------- /src/resources/Resource.js: -------------------------------------------------------------------------------- 1 | import Resources from "./Resources" 2 | 3 | class Resource 4 | { 5 | constructor() { 6 | this.watchers = [] 7 | this.loadLater = false 8 | this._loaded = false 9 | this._loading = false 10 | } 11 | 12 | watch(func) { 13 | this.watchers.push(func) 14 | } 15 | 16 | unwatch(func) { 17 | const index = this.watchers.indexOf(func) 18 | if(index == -1) { return } 19 | this.watchers[index] = this.watchers[this.watchers.length - 1] 20 | this.watchers.pop() 21 | } 22 | 23 | emit(event) { 24 | for(let n = 0; n < this.watchers.length; n++) { 25 | this.watchers[n](event, this) 26 | } 27 | } 28 | 29 | set loaded(value) { 30 | if(value) { 31 | if(!this._loaded) { 32 | this._loaded = true 33 | this.loading = false 34 | } 35 | } 36 | else { 37 | if(this._loaded) { 38 | this._loaded = false 39 | this.emit("unloaded", this) 40 | } 41 | } 42 | } 43 | 44 | get loaded() { 45 | return this._loaded 46 | } 47 | 48 | set loading(value) { 49 | if(value) { 50 | if(!this._loading) { 51 | this._loading = true 52 | this._loaded = false 53 | this.emit("unloaded", this) 54 | Resources.resourceLoading(this) 55 | } 56 | } 57 | else { 58 | if(this._loading) { 59 | this._loading = false 60 | this._loaded = true 61 | this.emit("loaded", this) 62 | Resources.resourceLoaded(this) 63 | } 64 | } 65 | } 66 | 67 | get loading() { 68 | return this._loading 69 | } 70 | } 71 | 72 | Resources.Resource = Resource 73 | 74 | export default Resource -------------------------------------------------------------------------------- /src/resources/Resources.js: -------------------------------------------------------------------------------- 1 | 2 | class Resources 3 | { 4 | constructor() { 5 | this.resources = {} 6 | this.listeners = {} 7 | this.loadLaterBuffer = [] 8 | this.loading = false 9 | this.numToLoad = 0 10 | this.numToLoadMax = 0 11 | } 12 | 13 | loadFromConfig(config) { 14 | for(let key in config) { 15 | this.load(key, config[key]) 16 | } 17 | } 18 | 19 | load(id, config) 20 | { 21 | if(this.resources[id]) { 22 | console.warn(`(Resources.load) There is already resource with id: ${id}`) 23 | return 24 | } 25 | 26 | const classes = this.Resource.__inherit 27 | const cls = classes[config.type] 28 | if(!cls) { 29 | console.warn(`(Resources.load) No such resource type registered: ${config.type}`) 30 | return 31 | } 32 | 33 | const resource = new cls() 34 | if(resource.loadLater) { 35 | this.loadLaterBuffer.push({ resource, config }) 36 | } 37 | else { 38 | resource.loadFromConfig(config) 39 | } 40 | 41 | this.resources[id] = resource 42 | 43 | return resource 44 | } 45 | 46 | loadDelayed() { 47 | for(let n = 0; n < this.loadLaterBuffer.length; n++) { 48 | const info = this.loadLaterBuffer[n] 49 | info.resource.loadFromConfig(info.config) 50 | } 51 | this.loadLaterBuffer.length = 0 52 | } 53 | 54 | resourceLoading(resource) { 55 | if(this.numToLoad === 0) { 56 | this.loading = true 57 | this.emit("loading") 58 | } 59 | this.numToLoad++ 60 | this.numToLoadMax++ 61 | } 62 | 63 | resourceLoaded(resource) { 64 | this.numToLoad-- 65 | this.emit("progress", (100 / this.numToLoadMax) * (this.numToLoadMax - this.numToLoad)) 66 | if(this.numToLoad === 0) { 67 | this.loadDelayed() 68 | this.loading = false 69 | this.emit("ready") 70 | } 71 | } 72 | 73 | get(id) { 74 | const resource = this.resources[id] 75 | return resource || null 76 | } 77 | 78 | on(event, func) 79 | { 80 | const buffer = this.listeners[event] 81 | if(buffer) { 82 | buffer.push(func) 83 | } 84 | else { 85 | this.listeners[event] = [ func ] 86 | } 87 | } 88 | 89 | off(event, func) 90 | { 91 | const buffer = this.listeners[event] 92 | if(!buffer) { return } 93 | 94 | const index = buffer.indexOf(func) 95 | if(index === -1) { return } 96 | 97 | buffer[index] = buffer[buffer.length - 1] 98 | buffer.pop() 99 | } 100 | 101 | emit(event, arg) 102 | { 103 | const buffer = this.listeners[event] 104 | if(!buffer) { return } 105 | 106 | if(arg) { 107 | for(let n = 0; n < buffer.length; n++) { 108 | buffer[n](arg) 109 | } 110 | } 111 | else { 112 | for(let n = 0; n < buffer.length; n++) { 113 | buffer[n]() 114 | } 115 | } 116 | } 117 | } 118 | 119 | const instance = new Resources() 120 | 121 | export default instance -------------------------------------------------------------------------------- /src/resources/Sound.js: -------------------------------------------------------------------------------- 1 | import Audio from "./Audio" 2 | import Resource from "./Resource" 3 | 4 | class Sound extends Resource 5 | { 6 | constructor() { 7 | super() 8 | this.gainNode = Audio.ctx.createGain() 9 | this.gainNode.connect(Audio.gainNode) 10 | this.instances = [] 11 | this.instancesActive = 0 12 | this.buffer = null 13 | this._volume = 1 14 | this._mute = false 15 | } 16 | 17 | play(loop, offset) { 18 | if(this.instances.length === this.instancesActive) { 19 | this.instances.push(new SoundInstance(this, this.instances.length)) 20 | } 21 | const instance = this.instances[this.instancesActive++] 22 | instance.play(loop, offset) 23 | } 24 | 25 | stop() { 26 | for(let n = 0; n < this.instancesActive; n++) { 27 | this.instances[n].stop() 28 | } 29 | } 30 | 31 | pause() { 32 | for(let n = 0; n < this.instancesActive; n++) { 33 | this.instances[n].pause() 34 | } 35 | } 36 | 37 | resume() { 38 | Audio.ctx.resume() 39 | for(let n = 0; n < this.instancesActive; n++) { 40 | this.instances[n].resume() 41 | } 42 | } 43 | 44 | set volume(volume) { 45 | if(volume < 0) { volume = 0 } 46 | else if(volume > 1) { volume = 1 } 47 | 48 | if(this._volume === volume) { return } 49 | this._volume = volume 50 | 51 | this.gainNode.gain.setValueAtTime(volume, 0) 52 | } 53 | 54 | get volume() { 55 | return this._volume 56 | } 57 | 58 | set mute(mute) { 59 | if(this._mute === mute) { return } 60 | this._mute = mute 61 | 62 | if(mute) { 63 | this.gainNode.gain.setValueAtTime(0, 0) 64 | } 65 | else { 66 | this.gainNode.gain.setValueAtTime(this._volume, 0) 67 | } 68 | } 69 | 70 | get mute() { 71 | return this._mute 72 | } 73 | 74 | loadFromConfig(cfg) { 75 | this.loading = true 76 | this.loadFromPath(cfg.path) 77 | } 78 | 79 | loadFromPath(path) { 80 | this.loading = true 81 | fetch(path) 82 | .then(response => { return response.arrayBuffer() }) 83 | .then(this.decodeAudio.bind(this)) 84 | } 85 | 86 | decodeAudio(arrayBuffer) { 87 | Audio.ctx.decodeAudioData(arrayBuffer, (buffer) => { 88 | this.buffer = buffer 89 | this.loading = false 90 | }, this.handleError.bind(this)) 91 | } 92 | 93 | handleError(error) { 94 | this.loading = false 95 | } 96 | 97 | handleSoundEnded(instance) { 98 | if(!instance.loop) { 99 | this.instancesActive-- 100 | const prevInstance = this.instances[this.instancesActive] 101 | this.instances[instance.index] = prevInstance 102 | this.instances[this.instancesActive] = instance 103 | prevInstance.index = instance.index 104 | instance.index = this.instancesActive 105 | } 106 | this.emit("ended") 107 | } 108 | } 109 | 110 | class SoundInstance 111 | { 112 | constructor(parent, index) { 113 | this.parent = parent 114 | this.index = index 115 | this.source = null 116 | this.playing = false 117 | this.loop = false 118 | this.tPaused = -1 119 | this.tStart = 0 120 | this.onEndFunc = this.handleEnded.bind(this) 121 | } 122 | 123 | play(loop, offset) 124 | { 125 | offset = offset || 0 126 | 127 | this.loop = loop || false 128 | this.playing = true 129 | this.tPaused = -1 130 | 131 | this.source = Audio.ctx.createBufferSource() 132 | this.source.buffer = this.parent.buffer 133 | this.source.connect(this.parent.gainNode) 134 | this.source.onended = this.onEndFunc 135 | 136 | if(offset < 0) { 137 | offset = 0 138 | } 139 | else if(offset > this.source.buffer.duration) { 140 | offset = this.source.buffer.duration 141 | } 142 | this.source.start(0, offset) 143 | this.tStart = this.source.context.currentTime - offset 144 | } 145 | 146 | stop() { 147 | if(!this.playing) { return } 148 | this.playing = false 149 | this.loop = false 150 | if(this.tPaused !== -1) { 151 | this.tPaused = -1 152 | this.parent.handleSoundEnded(this) 153 | } 154 | else { 155 | this.source.stop(0) 156 | } 157 | } 158 | 159 | pause() { 160 | if(!this.playing) { return } 161 | if(this.tPaused !== -1) { return } 162 | this.tPaused = this.source.context.currentTime - this.tStart 163 | this.source.stop(0) 164 | } 165 | 166 | resume() { 167 | if(this.tPaused === -1) { return } 168 | this.play(this.loop, this.tPaused) 169 | this.tPaused = -1 170 | } 171 | 172 | set currentTime(offset) { 173 | this.stop() 174 | this.play(this.looping, offset) 175 | } 176 | 177 | get currentTime() { 178 | if(!this.playing) { 179 | return 0 180 | } 181 | return this.source.context.currentTime - this.tStart 182 | } 183 | 184 | handleEnded() { 185 | if(this.tPaused !== -1) { return } 186 | if(this.loop) { 187 | this.play(true, 0) 188 | } 189 | this.parent.handleSoundEnded(this) 190 | } 191 | } 192 | 193 | export default Sound -------------------------------------------------------------------------------- /src/resources/Spritesheet.js: -------------------------------------------------------------------------------- 1 | import Texture from "./Texture" 2 | import Frame from "./Frame" 3 | import Utils from "../Utils" 4 | 5 | class Spritesheet extends Texture { 6 | constructor() { 7 | super() 8 | this.path = null 9 | } 10 | 11 | loadFromPath(path) { 12 | this.loading = true 13 | this.path = path 14 | 15 | const ext = Utils.getExt(path) 16 | switch(ext) 17 | { 18 | case "json": { 19 | fetch(path) 20 | .then(response => response.json()) 21 | .then(this.loadFromJson.bind(this)) 22 | } break 23 | 24 | case "xml": { 25 | fetch(path) 26 | .then(response => response.text()) 27 | .then(str => (new DOMParser().parseFromString(str, "text/xml"))) 28 | .then(this.loadFromXml.bind(this)) 29 | } break 30 | 31 | default: 32 | this.loading = false 33 | console.warn(`(Spritesheet.loadFromPath) Unsupported file extenssion: ${ext}`) 34 | break 35 | } 36 | } 37 | 38 | loadFromJson(data) { 39 | const rootPath = Utils.getRootPath(this.path) 40 | 41 | this.width = data.meta.size.w 42 | this.height = data.meta.size.h 43 | super.loadFromPath(`${rootPath}${data.meta.image}`) 44 | 45 | const frames = data.frames 46 | if(Array.isArray(frames)) { 47 | for(let n = 0; n < frames.length; n++) { 48 | const frameInfo = frames[n] 49 | const frame = frameInfo.frame 50 | this.createFrame(frameInfo.filename, frame.x, frame.y, frame.w, frame.h) 51 | } 52 | } 53 | else { 54 | for(let key in frames) { 55 | const frameInfo = frames[key] 56 | const frame = frameInfo.frame 57 | this.createFrame(key, frame.x, frame.y, frame.w, frame.h) 58 | } 59 | } 60 | 61 | this.loading = true 62 | } 63 | 64 | loadFromXml(xml) { 65 | const rootPath = Utils.getRootPath(this.path) 66 | const document = xml.documentElement 67 | 68 | this.width = parseInt(document.getAttribute("width")) 69 | this.height = parseInt(document.getAttribute("height")) 70 | const imagePath = document.getAttribute("imagePath") 71 | super.loadFromPath(`${rootPath}${imagePath}`) 72 | 73 | const childNodes = document.childNodes 74 | for(let n = 0; n < childNodes.length; n++) { 75 | const node = childNodes[n] 76 | switch(node.nodeName) { 77 | case "#text": 78 | continue 79 | case "SubTexture": // Starling 80 | this.createFrame( 81 | node.getAttribute("name"), 82 | parseInt(node.getAttribute("x")), 83 | parseInt(node.getAttribute("y")), 84 | parseInt(node.getAttribute("width")), 85 | parseInt(node.getAttribute("height"))) 86 | break 87 | case "sprite": // Generic XML 88 | this.createFrame( 89 | node.getAttribute("n"), 90 | parseInt(node.getAttribute("x")), 91 | parseInt(node.getAttribute("y")), 92 | parseInt(node.getAttribute("width")), 93 | parseInt(node.getAttribute("height"))) 94 | break 95 | default: 96 | console.warn(`(Spritesheet.loadFromXml) Unsupported node: ${node.nodeName}`) 97 | break 98 | } 99 | } 100 | 101 | this.loading = true 102 | } 103 | 104 | createFrame(name, x, y, width, height) { 105 | const widthUV = 1.0 / this.width 106 | const heightUV = 1.0 / this.height 107 | const minX = x * widthUV 108 | const minY = y * heightUV 109 | const maxX = (x + width) * widthUV 110 | const maxY = (y + height) * heightUV 111 | this.frames[name] = new Frame(this, [ 112 | width, height, maxX, maxY, 113 | 0, height, minX, maxY, 114 | 0, 0, minX, minY, 115 | width, 0, maxX, minY 116 | ], 0) 117 | } 118 | } 119 | 120 | export default Spritesheet -------------------------------------------------------------------------------- /src/resources/Texture.js: -------------------------------------------------------------------------------- 1 | import Resource from "./Resource" 2 | import Frame from "./Frame" 3 | import Engine from "../Engine" 4 | import { isPowerOf2 } from "../Utils" 5 | 6 | const greenPixel = new Uint8Array([ 0, 255, 0, 255 ]) 7 | const pixels = new Uint8Array(4) 8 | 9 | class Texture extends Resource 10 | { 11 | constructor() { 12 | super() 13 | this.path = null 14 | this.instance = Engine.gl.createTexture() 15 | this.width = 1 16 | this.height = 1 17 | this.framesX = 1 18 | this.framesY = 1 19 | this.frames = {} 20 | this._minFilter = Texture.LINEAR 21 | this._magFilter = Texture.LINEAR 22 | this._wrapS = Texture.CLAMP_TO_EDGE 23 | this._wrapT = Texture.CLAMP_TO_EDGE 24 | } 25 | 26 | resize(width, height) { 27 | this.width = width 28 | this.height = height 29 | } 30 | 31 | getInstance() { 32 | return this.instance 33 | } 34 | 35 | loadFromConfig(config) { 36 | this.loading = true 37 | this.path = null 38 | this.framesX = config.framesX || 1 39 | this.framesY = config.framesY || 1 40 | this._minFilter = config.pixelated ? Texture.NEAREST : (config.minFilter || Texture.LINEAR) 41 | this._magFilter = config.pixelated ? Texture.NEAREST : (config.magFilter || Texture.LINEAR) 42 | this._wrapS = config.wrapS || Texture.CLAMP_TO_EDGE 43 | this._wrapT = config.wrapT || Texture.CLAMP_TO_EDGE 44 | if(config.path) { 45 | this.loadFromPath(config.path) 46 | } 47 | } 48 | 49 | loadFromPath(path) { 50 | this.path = path 51 | this.loading = true 52 | 53 | const image = new Image() 54 | image.onload = () => { 55 | this.loadFromImage(image) 56 | } 57 | image.onfailed = () => { 58 | this.loadEmpty() 59 | } 60 | image.src = path 61 | } 62 | 63 | loadEmpty() { 64 | this.resize(1, 1) 65 | 66 | const gl = Engine.gl 67 | gl.bindTexture(gl.TEXTURE_2D, this.instance) 68 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, this.width, this.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, greenPixel) 69 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, this._wrapS) 70 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, this._wrapT) 71 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, this._magFilter) 72 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, this._minFilter) 73 | 74 | this.updateFrames() 75 | this.loaded = true 76 | } 77 | 78 | loadFromImage(image) 79 | { 80 | const gl = Engine.gl 81 | 82 | this.resize(image.width, image.height) 83 | 84 | gl.bindTexture(gl.TEXTURE_2D, this.instance) 85 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image) 86 | 87 | if(isPowerOf2(image.width) && isPowerOf2(image.height)) { 88 | gl.generateMipmap(gl.TEXTURE_2D) 89 | } 90 | else { 91 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, this._wrapS) 92 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, this._wrapT) 93 | } 94 | 95 | this._minFilter = Texture.NEAREST 96 | this._magFilter = Texture.NEAREST 97 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, this._magFilter) 98 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, this._minFilter) 99 | gl.bindTexture(gl.TEXTURE_2D, null) 100 | 101 | this.updateFrames() 102 | this.loading = false 103 | } 104 | 105 | loadFromCanvas(canvas, resize = true) { 106 | const gl = Engine.gl 107 | 108 | if(resize) { 109 | this.resize(canvas.width, canvas.height) 110 | } 111 | 112 | gl.bindTexture(gl.TEXTURE_2D, this.instance) 113 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas) 114 | 115 | if(isPowerOf2(canvas.width) && isPowerOf2(canvas.height)) { 116 | gl.generateMipmap(gl.TEXTURE_2D) 117 | } 118 | else { 119 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, this._wrapS) 120 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, this._wrapT) 121 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, this._magFilter) 122 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, this._minFilter) 123 | } 124 | 125 | this.updateFrames() 126 | this.loaded = true 127 | } 128 | 129 | updateFrames() { 130 | const frameWidth = (this.width / this.framesX) | 0 131 | const frameHeight = (this.height / this.framesY) | 0 132 | const widthUV = 1.0 / this.width 133 | const heightUV = 1.0 / this.height 134 | let posX = 0 135 | let posY = 0 136 | let index = 0 137 | 138 | for(let y = 0; y < this.framesY; y++) { 139 | for(let x = 0; x < this.framesX; x++) { 140 | const minX = posX * widthUV 141 | const minY = posY * heightUV 142 | const maxX = (posX + frameWidth) * widthUV 143 | const maxY = (posY + frameHeight) * heightUV 144 | this.frames[index] = new Frame(this, [ 145 | frameWidth, frameHeight, maxX, maxY, 146 | 0, frameHeight, minX, maxY, 147 | 0, 0, minX, minY, 148 | frameWidth, 0, maxX, minY 149 | ], 0) 150 | posX += frameWidth 151 | index++ 152 | } 153 | posX = 0 154 | posY += frameHeight 155 | } 156 | } 157 | 158 | getFrame(index) { 159 | return this.frames[index] 160 | } 161 | 162 | getPixelAt(x, y) { 163 | const gl = Engine.gl 164 | gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels) 165 | return pixels 166 | } 167 | } 168 | 169 | Texture.TEXTURE_2D = WebGLRenderingContext.TEXTURE_2D 170 | Texture.TEXTURE_3D = WebGLRenderingContext.TEXTURE_3D 171 | Texture.NEAREST = WebGLRenderingContext.NEAREST 172 | Texture.LINEAR = WebGLRenderingContext.LINEAR 173 | Texture.CLAMP_TO_EDGE = WebGLRenderingContext.CLAMP_TO_EDGE 174 | Texture.REPEAT = WebGLRenderingContext.REPEAT 175 | 176 | export default Texture -------------------------------------------------------------------------------- /src/resources/Tiled.js: -------------------------------------------------------------------------------- 1 | import Resources from "./Resources" 2 | import Resource from "./Resource" 3 | import Tileset from "./Tileset" 4 | import Utils from "../Utils" 5 | 6 | function Layer() { 7 | this.name = null 8 | this.width = 0 9 | this.height = 0 10 | this.tileWidth = 0 11 | this.tileHeight = 0 12 | this.opacity = 1.0 13 | this.visible = 1 14 | this.data = null 15 | } 16 | 17 | class Tiled extends Resource 18 | { 19 | constructor() { 20 | super() 21 | this.width = 0 22 | this.height = 0 23 | this.orientation = null 24 | this.layers = null 25 | this.tilesets = null 26 | this.properties = {} 27 | this.path = null 28 | this._dependencies = 0 29 | this._dependenciesLoaded = 0 30 | } 31 | 32 | loadFromConfig(cfg) { 33 | this.loading = true 34 | this.loadFromPath(cfg.path) 35 | } 36 | 37 | loadFromPath(path) { 38 | this.loading = true 39 | this.path = path 40 | 41 | const ext = Utils.getExt(path) 42 | switch(ext) { 43 | case "json": { 44 | fetch(path) 45 | .then(response => { return response.json() }) 46 | .then(this.parseJson.bind(this)) 47 | } break 48 | 49 | case "tmx": { 50 | fetch(path) 51 | .then(response => response.text()) 52 | .then(str => new DOMParser().parseFromString(str, "text/xml")) 53 | .then(this.parseTmx.bind(this)) 54 | } break 55 | } 56 | } 57 | 58 | parseData(data, encoding) { 59 | if(encoding) { 60 | switch(encoding) { 61 | case "csv": 62 | return JSON.parse(`[${data}]`) 63 | case "base64": { 64 | data = atob(data) 65 | const result = new Array(data.length / 4) 66 | let index = 0 67 | for(let n = 0; n < result.length; n++) { 68 | result[n] = data.charCodeAt(index) | 69 | data.charCodeAt(index + 1) << 8 | 70 | data.charCodeAt(index + 2) << 16 | 71 | data.charCodeAt(index + 3) << 24 72 | index += 4 73 | } 74 | return result 75 | } 76 | default: 77 | console.error(`(Tiled.parseData) Unsupported encoding format for layer: ${encoding}`) 78 | return null 79 | } 80 | } 81 | return data 82 | } 83 | 84 | parseJson(data) { 85 | this.width = data.width 86 | this.height = data.height 87 | this.tileWidth = data.tilewidth 88 | this.tileHeight = data.tileheight 89 | this.orientation = data.orientation 90 | this.properties = data.properties 91 | 92 | const tilesets = data.tilesets 93 | const rootPath = Utils.getRootPath(this.path) 94 | this.tilesets = new Array(tilesets.length) 95 | 96 | for(let n = 0; n < tilesets.length; n++) { 97 | const tilesetInfo = tilesets[n] 98 | const gid = tilesetInfo.firstgid 99 | if(tilesetInfo.source) { 100 | this._dependencies++ 101 | fetch(`${rootPath}/${tilesetInfo.source}`) 102 | .then(response => response.text()) 103 | .then(str => (new DOMParser()).parseFromString(str, "text/xml")) 104 | .then((data) => { 105 | this.parseTsxTileset(data.documentElement, gid, rootPath) 106 | }) 107 | } 108 | else { 109 | const image = tilesetInfo.image 110 | const id = `${rootPath}${image}.${gid}` 111 | let tileset = Resources.get(id) 112 | if(!tileset) { 113 | const data = { 114 | type: "Tileset", 115 | gid, 116 | path: `${rootPath}${image}`, 117 | width: tilesetInfo.imagewidth, 118 | height: tilesetInfo.imageheight, 119 | tileWidth: tilesetInfo.tilewidth, 120 | tileHeight: tilesetInfo.tileheight, 121 | columns: tilesetInfo.columns, 122 | spacing: tilesetInfo.spacing | 0, 123 | margin: tilesetInfo.margin | 0, 124 | properties: tilesetInfo.tileproperties 125 | } 126 | if(tilesetInfo.offset) { 127 | data.offsetX = tilesetInfo.offset.x 128 | data.offsetY = tilesetInfo.offset.y 129 | } 130 | tileset = Resources.load(id, data) 131 | } 132 | this.tilesets[n] = tileset 133 | } 134 | } 135 | 136 | const numLayers = data.layers.length 137 | this.layers = new Array(numLayers) 138 | 139 | for(let n = 0; n < numLayers; n++) { 140 | const layerInfo = data.layers[n] 141 | const layerData = this.parseData(layerInfo.data, layerInfo.encoding) 142 | if(!layerData) { 143 | continue 144 | } 145 | const layer = new Layer() 146 | layer.name = layerInfo.name 147 | layer.width = layerInfo.width 148 | layer.height = layerInfo.height 149 | layer.data = layerData 150 | layer.visible = (layerInfo.visible !== undefined) ? layerInfo.visible : 1 151 | layer.opacity = (layerInfo.opacity !== undefined) ? layerInfo.opacity : 1.0 152 | this.layers[n] = layer 153 | } 154 | 155 | if(this._dependencies === 0) { 156 | this.loading = false 157 | } 158 | } 159 | 160 | parseTmx(data) { 161 | const node = data.documentElement 162 | this.width = parseInt(node.getAttribute("width")) 163 | this.height = parseInt(node.getAttribute("height")) 164 | this.tileWidth = parseInt(node.getAttribute("tilewidth")) 165 | this.tileHeight = parseInt(node.getAttribute("tileheight")) 166 | this.orientation = node.getAttribute("orientation") 167 | this.tilesets = new Array() 168 | this.layers = new Array() 169 | 170 | const rootPath = Utils.getRootPath(this.path) 171 | const children = node.childNodes 172 | for(let n = 0; n < children.length; n++) { 173 | const child = children[n] 174 | switch(child.nodeName) { 175 | case "tileset": 176 | this.parseTmxTileset(child, rootPath) 177 | break 178 | 179 | case "layer": { 180 | let layerData = null 181 | const children = child.childNodes 182 | for(let n = 0; n < children.length; n++) { 183 | const child = children[n] 184 | switch(child.nodeName) { 185 | case "data": 186 | const encoding = child.getAttribute("encoding") 187 | if(encoding) { 188 | layerData = this.parseData(child.textContent, encoding) 189 | } 190 | else { 191 | const children = child.children 192 | layerData = new Array(children.length) 193 | for(let n = 0; n < layerData.length; n++) { 194 | layerData[n] = parseInt(children[n].getAttribute("gid")) 195 | } 196 | } 197 | if(!layerData) { 198 | continue 199 | } 200 | break 201 | } 202 | } 203 | const visible = child.getAttribute("visible") 204 | const opacity = child.getAttribute("opacity") 205 | const layer = new Layer() 206 | layer.name = child.getAttribute("name") 207 | layer.width = parseInt(child.getAttribute("width")) 208 | layer.height = parseInt(child.getAttribute("height")) 209 | layer.visible = (visible !== null) ? parseInt(visible) : 1 210 | layer.opacity = (opacity !== null) ? parseFloat(opacity) : 1.0 211 | layer.data = layerData 212 | this.layers.push(layer) 213 | } break 214 | 215 | case "properties": 216 | const children = child.childNodes 217 | for(let n = 0; n < children.length; n++) { 218 | const child = children[n] 219 | if(child.nodeName === "#text") { continue } 220 | this.properties[child.getAttribute("name")] = child.getAttribute("value") 221 | } 222 | break 223 | } 224 | } 225 | 226 | if(this._dependencies === 0) { 227 | this.loading = false 228 | } 229 | } 230 | 231 | parseTmxTileset(node, rootPath) { 232 | const gid = parseInt(node.getAttribute("firstgid")) 233 | const source = node.getAttribute("source") 234 | if(source) { 235 | this._dependencies++ 236 | fetch(`${rootPath}/${source}`) 237 | .then(response => response.text()) 238 | .then(str => (new DOMParser()).parseFromString(str, "text/xml")) 239 | .then((data) => { 240 | this.parseTsxTileset(data.documentElement, gid, rootPath) 241 | }) 242 | } 243 | else { 244 | this.parseTsxTileset(node, gid, rootPath) 245 | } 246 | } 247 | 248 | parseTsxTileset(node, gid, rootPath) { 249 | const tileWidth = parseInt(node.getAttribute("tilewidth")) 250 | const tileHeight = parseInt(node.getAttribute("tileheight")) 251 | const columns = parseInt(node.getAttribute("columns")) 252 | const properties = {} 253 | const data = { 254 | type: "Tileset", 255 | gid, 256 | path: null, 257 | width: 0, 258 | height: 0, 259 | tileWidth, 260 | tileHeight, 261 | columns, 262 | offsetX: 0, 263 | offsetY: 0, 264 | spacing: 0, 265 | margin: 0, 266 | properties 267 | } 268 | let source = null 269 | 270 | const children = node.childNodes 271 | for(let n = 0; n < children.length; n++) { 272 | const child = children[n] 273 | switch(child.nodeName) { 274 | case "tile": 275 | const tileProperties = {} 276 | const id = parseInt(child.getAttribute("id")) 277 | const tileChildren = child.children 278 | for(let i = 0; i < tileChildren.length; i++) { 279 | const tileChild = tileChildren[i] 280 | switch(tileChild.nodeName) { 281 | case "properties": 282 | const propertiesChildren = tileChild.children 283 | for(let m = 0; m < propertiesChildren.length; m++) { 284 | const property = propertiesChildren[m] 285 | tileProperties[property.getAttribute("name")] = property.getAttribute("value") 286 | } 287 | break 288 | } 289 | } 290 | properties[id] = tileProperties 291 | break 292 | case "image": 293 | source = child.getAttribute("source") 294 | data.width = parseInt(child.getAttribute("width")) 295 | data.height = parseInt(child.getAttribute("height")) 296 | data.spacing = parseInt(node.getAttribute("spacing")) || 0 297 | data.margin = parseInt(node.getAttribute("margin")) || 0 298 | break 299 | case "tileoffset": 300 | data.offsetX = parseInt(child.getAttribute("x")) 301 | data.offsetY = parseInt(child.getAttribute("y")) 302 | break 303 | } 304 | } 305 | 306 | const id = `${source}.${gid}` 307 | let tileset = Resources.get(id) 308 | if(!tileset) { 309 | data.path = `${rootPath}${source}` 310 | tileset = Resources.load(id, data) 311 | } 312 | this.tilesets.push(tileset) 313 | 314 | if(this._dependencies > 0) { 315 | this._dependenciesLoaded++ 316 | if(this._dependencies === this._dependenciesLoaded) { 317 | this.loading = false 318 | } 319 | } 320 | } 321 | } 322 | 323 | export default Tiled -------------------------------------------------------------------------------- /src/resources/Tileset.js: -------------------------------------------------------------------------------- 1 | import Texture from "./Texture" 2 | import Frame from "./Frame" 3 | 4 | const FLIPPED_HORIZONTALLY_FLAG = 0x80000000 5 | const FLIPPED_VERTICALLY_FLAG = 0x40000000 6 | const FLIPPED_DIAGONALLY_FLAG = 0x20000000 7 | const ALL_FLAGS = (FLIPPED_HORIZONTALLY_FLAG | FLIPPED_VERTICALLY_FLAG | FLIPPED_DIAGONALLY_FLAG) 8 | const frameOutput = new Float32Array(16) 9 | 10 | class Tileset extends Texture 11 | { 12 | constructor() { 13 | super() 14 | this.gid = 1 15 | this.tileWidth = 0 16 | this.tileHeight = 0 17 | this.columns = 0 18 | this.offsetX = 0 19 | this.offsetY = 0 20 | this.spacing = 0 21 | this.margin = 0 22 | this.properties = {} 23 | } 24 | 25 | loadFromConfig(cfg) { 26 | super.loadFromConfig(cfg) 27 | this._minFilter = Texture.NEAREST 28 | this._magFilter = Texture.NEAREST 29 | this._wrapS = Texture.CLAMP_TO_EDGE 30 | this._wrapT = Texture.CLAMP_TO_EDGE 31 | this.gid = cfg.gid || 0 32 | this.tileWidth = cfg.tileWidth || 1 33 | this.tileHeight = cfg.tileHeight || 1 34 | this.columns = cfg.columns || 0 35 | this.offsetX = cfg.offsetX || 0 36 | this.offsetY = cfg.offsetY || 0 37 | this.spacing = cfg.spacing || 0 38 | this.margin = cfg.margin || 0 39 | this.properties = cfg.properties || {} 40 | } 41 | 42 | updateFrames() { 43 | const tilesX = this.columns || Math.floor(this.width / this.tileWidth) 44 | const tilesY = Math.floor(this.height / this.tileHeight) 45 | const widthUV = 1.0 / this.width 46 | const heightUV = 1.0 / this.height 47 | const innerSpacing = 0.00001 48 | this.frames = new Array(tilesX * tilesY) 49 | 50 | let index = 0 51 | let posX = this.spacing 52 | let posY = this.spacing 53 | for(let y = 0; y < tilesY; y++) { 54 | for(let x = 0; x < tilesX; x++) { 55 | const minX = widthUV * posX + innerSpacing 56 | const minY = heightUV * posY + innerSpacing 57 | const maxX = widthUV * (posX + this.tileWidth - this.margin) 58 | const maxY = heightUV * (posY + this.tileHeight - this.margin) 59 | this.frames[index] = new Frame(this, [ 60 | this.tileWidth, this.tileHeight, maxX, maxY, 61 | 0, this.tileHeight, minX, maxY, 62 | 0, 0, minX, minY, 63 | this.tileWidth, 0, maxX, minY 64 | ], 0) 65 | posX += this.tileWidth + this.spacing 66 | index++ 67 | } 68 | posX = this.spacing 69 | posY += this.tileHeight + this.spacing 70 | } 71 | } 72 | 73 | getTileFrame(gid) { 74 | if(gid < FLIPPED_DIAGONALLY_FLAG) { 75 | return this.frames[gid].coords 76 | } 77 | 78 | const frame = this.frames[gid & ~ALL_FLAGS] 79 | 80 | frameOutput.set(frame.coords, 0) 81 | 82 | if(gid & FLIPPED_HORIZONTALLY_FLAG) { 83 | const minX = frame.coords[6] 84 | const maxX = frame.coords[2] 85 | frameOutput[2] = minX 86 | frameOutput[6] = maxX 87 | frameOutput[10] = maxX 88 | frameOutput[14] = minX 89 | } 90 | if(gid & FLIPPED_VERTICALLY_FLAG) { 91 | const minY = frame.coords[11] 92 | const maxY = frame.coords[3] 93 | frameOutput[3] = minY 94 | frameOutput[7] = minY 95 | frameOutput[11] = maxY 96 | frameOutput[15] = maxY 97 | } 98 | if(gid & FLIPPED_DIAGONALLY_FLAG) { 99 | const tmp1 = frameOutput[2] 100 | const tmp2 = frameOutput[3] 101 | frameOutput[2] = frameOutput[10] 102 | frameOutput[3] = frameOutput[11] 103 | frameOutput[10] = tmp1 104 | frameOutput[11] = tmp2 105 | } 106 | 107 | return frameOutput 108 | } 109 | 110 | getProperties(gid) { 111 | const properties = this.properties[gid] 112 | return properties ? properties : null 113 | } 114 | } 115 | 116 | export default Tileset -------------------------------------------------------------------------------- /src/tilemap/Tilemap.js: -------------------------------------------------------------------------------- 1 | import Entity from "../entity/Entity" 2 | import Resources from "../resources/Resources" 3 | import Tileset from "../resources/Tileset" 4 | import Tiled from "../resources/Tiled" 5 | import TilemapOrthogonalLayer from "./TilemapOrthogonalLayer" 6 | import TilemapIsometricLayer from "./TilemapIsometricLayer" 7 | 8 | class Tilemap extends Entity { 9 | constructor(resource) { 10 | super() 11 | this.sizeX = 0 12 | this.sizeY = 0 13 | this.tileWidth = 0 14 | this.tileHeight = 0 15 | this.type = Tilemap.Type.Orthographic 16 | this.tilesets = [] 17 | this.properties = {} 18 | if(resource) { 19 | this.loadFromResource(resource) 20 | } 21 | } 22 | 23 | create(sizeX, sizeY, tileWidth, tileHeight, type = Tilemap.Type.Orthogonal, name = "Layer") { 24 | this.name = name 25 | this.sizeX = sizeX 26 | this.sizeY = sizeY 27 | this.tileWidth = tileWidth 28 | this.tileHeight = tileHeight 29 | this.type = type 30 | this.tilesets = [] 31 | this.size.set(sizeX * tileWidth, sizeY * tileHeight) 32 | } 33 | 34 | createLayer(data, name) { 35 | let layer = null 36 | switch(this.type) { 37 | case Tilemap.Type.Orthogonal: 38 | layer = new TilemapOrthogonalLayer() 39 | break 40 | case Tilemap.Type.Isometric: 41 | layer = new TilemapIsometricLayer() 42 | break 43 | default: 44 | console.warn(`(Tilemap.createLayer) Unsupported layer type: ${type}`) 45 | break 46 | } 47 | if(!layer) { 48 | return null 49 | } 50 | 51 | this.addChild(layer) 52 | layer.create(this.sizeX, this.sizeY, this.tileWidth, this.tileHeight, data, name) 53 | return layer 54 | } 55 | 56 | createTileset(config) { 57 | const tileset = new Tileset() 58 | tileset.loadFromConfig(config) 59 | this.tilesets.push(tileset) 60 | } 61 | 62 | loadFromResource(resource) { 63 | let ref = null 64 | if(typeof resource === "string") { 65 | ref = Resources.get(resource) 66 | if(!ref) { 67 | console.error(`(Tilemap.loadFromResource) No such resource found: ${resource}`) 68 | return 69 | } 70 | } 71 | else { 72 | ref = resource 73 | } 74 | 75 | if(ref instanceof Tiled) { 76 | this.loadFromTiled(ref) 77 | } 78 | } 79 | 80 | loadFromTiled(tiled) { 81 | this.create(tiled.width, tiled.height, tiled.tileWidth, tiled.tileHeight, tiled.orientation, tiled.name) 82 | this.tilesets = tiled.tilesets 83 | this.properties = tiled.properties 84 | const layers = tiled.layers 85 | for(let n = 0; n < layers.length; n++) { 86 | const layerInfo = layers[n] 87 | const layer = this.createLayer(layerInfo.data, layerInfo.name) 88 | if(layer) { 89 | if(layer.tileset) { 90 | layer.hidden = layerInfo.visible ? false : true 91 | } 92 | layer.color.set(1, 1, 1, layerInfo.opacity) 93 | } 94 | } 95 | } 96 | 97 | getLayer(name) { 98 | if(!this.children) { 99 | return null 100 | } 101 | for(let n = 0; n < this.children.length; n++) { 102 | const child = this.children[n] 103 | if(child.name === name) { 104 | return child 105 | } 106 | } 107 | return null 108 | } 109 | 110 | getTileFromWorld(x, y, output) { 111 | if(!this.children) { return } 112 | 113 | const child = this.children[0] 114 | return child.getTileFromWorld(x, y, output) 115 | } 116 | 117 | getWorldFromTile(x, y, output) { 118 | if(!this.children) { return } 119 | 120 | const child = this.children[0] 121 | return child.getWorldFromTile(x, y, output) 122 | } 123 | 124 | getProperties(gid) { 125 | for(let n = 0; n < this.tilesets.length; n++) { 126 | const tileset = this.tilesets[n] 127 | if(tileset.gid <= gid) { 128 | return tileset.getProperties(gid - tileset.gid) 129 | } 130 | } 131 | return null 132 | } 133 | } 134 | 135 | Tilemap.Type = { 136 | Orthogonal: "orthogonal", 137 | Isometric: "isometric", 138 | Hexagon: "hexagon" 139 | } 140 | 141 | Tilemap.Flag = { 142 | FlipHorizontally: 0x80000000, 143 | FlipVertically: 0x40000000, 144 | FlipDiagonally: 0x20000000 145 | } 146 | 147 | export default Tilemap -------------------------------------------------------------------------------- /src/tilemap/TilemapIsometricLayer.js: -------------------------------------------------------------------------------- 1 | import Engine from "../Engine" 2 | import TilemapLayer from "./TilemapLayer" 3 | 4 | class TilemapIsometricLayer extends TilemapLayer 5 | { 6 | constructor() { 7 | super() 8 | } 9 | 10 | updateSize() { 11 | this.startX = this.halfTileWidth * (this.sizeY - 1) 12 | this.startY = 0 13 | this.size.set( 14 | this.tileset.tileWidth + ((this.sizeX - 1) * this.halfTileWidth) + (this.sizeY - 1) * this.halfTileWidth, 15 | this.tileset.tileHeight + ((this.sizeX - 1) * this.halfTileHeight) + (this.sizeY - 1) * this.halfTileHeight) 16 | } 17 | 18 | getWorldFromTile(x, y, output, tileCenter = true) { 19 | if(tileCenter) { 20 | output.x = this.startX + (this.halfTileWidth * x) - (this.halfTileWidth * y) + this.halfTileWidth 21 | output.y = this.startY + this.halfTileHeight + (this.halfTileHeight * y) + this.halfTileHeight 22 | } 23 | else { 24 | output.x = this.startX + (this.halfTileWidth * x) - (this.halfTileWidth * y) 25 | output.y = this.startY + (this.halfTileHeight * x) + (this.halfTileHeight * y) 26 | } 27 | } 28 | 29 | getTileFromWorld(worldX, worldY, output) { 30 | const transform = this.transform 31 | worldX -= transform.m[6] + ((this.sizeX - 1) * this.halfTileWidth) - this.tileset.offsetX 32 | worldY -= transform.m[7] + this._size.y - this.halfTileHeight - this.tileset.offsetY 33 | output.x = Math.floor((worldX / this.halfTileWidth + (worldY / this.halfTileHeight)) / 2) + this.sizeX - 1 34 | output.y = Math.floor((worldY / this.halfTileHeight - (worldX / this.halfTileWidth)) / 2) + this.sizeY 35 | } 36 | } 37 | 38 | export default TilemapIsometricLayer 39 | -------------------------------------------------------------------------------- /src/tilemap/TilemapLayer.js: -------------------------------------------------------------------------------- 1 | import Engine from "../Engine" 2 | import Renderable from "../entity/Renderable" 3 | import Sprite from "../entity/Sprite" 4 | import Material from "../resources/Material" 5 | import Vector2 from "../math/Vector2" 6 | import Vector4 from "../math/Vector4" 7 | import tilemapVertexSrc from "../../shaders/tilemap.vertex.glsl" 8 | import tilemapFragmentSrc from "../../shaders/tilemap.fragment.glsl" 9 | 10 | let tilemapMaterial = null 11 | Engine.on("setup", () => { 12 | tilemapMaterial = new Material() 13 | tilemapMaterial.loadFromConfig({ 14 | vertexSrc: tilemapVertexSrc, 15 | fragmentSrc: tilemapFragmentSrc 16 | }) 17 | }) 18 | 19 | const FLIPPED_HORIZONTALLY_FLAG = 0x80000000 20 | const FLIPPED_VERTICALLY_FLAG = 0x40000000 21 | const FLIPPED_DIAGONALLY_FLAG = 0x20000000 22 | const ALL_FLAGS = (FLIPPED_HORIZONTALLY_FLAG | FLIPPED_VERTICALLY_FLAG | FLIPPED_DIAGONALLY_FLAG) 23 | 24 | const output = new Vector2(0, 0) 25 | 26 | class TilemapLayer extends Renderable 27 | { 28 | constructor() { 29 | super() 30 | this.name = "Layer" 31 | this.sizeX = 0 32 | this.sizeY = 0 33 | this.tileWidth = 0 34 | this.tileHeight = 0 35 | this.halfTileWidth = 0 36 | this.halfTileHeight = 0 37 | this.offsetX = 0 38 | this.offsetY = 0 39 | this.tileset = null 40 | this.color = new Vector4(1, 1, 1, 1) 41 | this.data = null 42 | this.material = tilemapMaterial 43 | this._entityMode = false 44 | } 45 | 46 | create(sizeX, sizeY, tileWidth, tileHeight, data, name = "Layer") { 47 | this.name = name 48 | this.sizeX = sizeX 49 | this.sizeY = sizeY 50 | this.tileWidth = tileWidth 51 | this.tileHeight = tileHeight 52 | this.halfTileWidth = tileWidth * 0.5 53 | this.halfTileHeight = tileHeight * 0.5 54 | this.dataInfo = new Array(sizeX * sizeY) 55 | this.updateData(data) 56 | this.extractTileset() 57 | if(this.tileset) { 58 | this.updateSize() 59 | } 60 | 61 | const numTiles = sizeX * sizeY 62 | this.indices = new Uint16Array(numTiles * 6) 63 | let indiceIndex = 0 64 | let verticeOffset = 0 65 | for(let n = 0; n < numTiles; n++) { 66 | this.indices[indiceIndex++] = verticeOffset 67 | this.indices[indiceIndex++] = verticeOffset + 2 68 | this.indices[indiceIndex++] = verticeOffset + 1 69 | this.indices[indiceIndex++] = verticeOffset 70 | this.indices[indiceIndex++] = verticeOffset + 3 71 | this.indices[indiceIndex++] = verticeOffset + 2 72 | verticeOffset += 4 73 | } 74 | this.drawCommand.mesh.uploadIndices(this.indices) 75 | } 76 | 77 | updateSize() {} 78 | 79 | updateData(data) { 80 | this.data = data 81 | this.needUpdateMesh = true 82 | } 83 | 84 | updateMesh() { 85 | if(this._entityMode) { 86 | return 87 | } 88 | this.buffer = new Float32Array(this.sizeX * this.sizeY * 16) 89 | 90 | let index = 0 91 | let numElements = 0 92 | for(let y = 0; y < this.sizeY; y++) { 93 | for(let x = 0; x < this.sizeX; x++) { 94 | const id = x + (y * this.sizeX) 95 | let gid = this.data[id] - this.tileset.gid 96 | if(gid > -1) { 97 | this.getWorldFromTile(x, y, output, false) 98 | const frame = this.tileset.getTileFrame(gid) 99 | const posX = output.x 100 | const posY = output.y 101 | this.buffer.set(frame, index) 102 | this.buffer[index + 0] += posX 103 | this.buffer[index + 1] += posY 104 | this.buffer[index + 4] += posX 105 | this.buffer[index + 5] += posY 106 | this.buffer[index + 8] += posX 107 | this.buffer[index + 9] += posY 108 | this.buffer[index + 12] += posX 109 | this.buffer[index + 13] += posY 110 | index += 16 111 | numElements++ 112 | } 113 | } 114 | } 115 | 116 | this.drawCommand.mesh.upload(this.buffer) 117 | this.drawCommand.mesh.numElements = numElements * 6 118 | this.needUpdateMesh = false 119 | } 120 | 121 | extractTileset() { 122 | const num = this.sizeX * this.sizeY 123 | const tilesets = this.parent.tilesets 124 | 125 | let tileset = null 126 | for(let n = 0; n < num; n++) { 127 | let gid = this.data[n] 128 | if(gid === 0) { continue } 129 | gid &= ~ALL_FLAGS 130 | 131 | tileset = tilesets[0] 132 | for(let n = 1; n < tilesets.length; n++) { 133 | if(gid < tilesets[n].gid) { 134 | break 135 | } 136 | tileset = tilesets[n] 137 | } 138 | break 139 | } 140 | 141 | if(!tileset) { 142 | tileset = tilesets[0] 143 | } 144 | 145 | this.tileset = tileset 146 | 147 | if(tileset) { 148 | this.drawCommand.uniforms.albedo = this.tileset.instance 149 | } 150 | else { 151 | this.drawCommand.uniforms.albedo = null 152 | this.hidden = true 153 | } 154 | } 155 | 156 | entityMode(flag) { 157 | this._entityMode = flag 158 | this.hidden = true 159 | this.createSprites() 160 | } 161 | 162 | createSprites() { 163 | for(let y = 0; y < this.sizeY; y++) { 164 | for(let x = 0; x < this.sizeX; x++) { 165 | const id = x + (y * this.sizeX) 166 | let gid = this.data[id] - this.tileset.gid 167 | if(gid > -1) { 168 | this.createSprite(gid, x, y) 169 | } 170 | } 171 | } 172 | } 173 | 174 | createSprite(gid, x, y) { 175 | const output = TilemapLayer.output 176 | const sprite = new Sprite() 177 | sprite.frame = this.tileset.getFrame(gid & ~ALL_FLAGS) 178 | sprite.z = x + (y * this.sizeX) 179 | this.addChild(sprite) 180 | this.getWorldFromTile(x, y) 181 | sprite.position.set(output.x, output.y) 182 | 183 | if(gid >= FLIPPED_DIAGONALLY_FLAG) { 184 | let scaleX = 1 185 | let scaleY = 1 186 | if(gid & FLIPPED_HORIZONTALLY_FLAG) { 187 | scaleX = -1 188 | } 189 | if(gid & FLIPPED_VERTICALLY_FLAG) { 190 | scaleY = -1 191 | } 192 | sprite.scale.set(scaleX, scaleY) 193 | if(scaleX === -1) { 194 | sprite.position.x += this.tileWidth 195 | } 196 | if(scaleY === -1) { 197 | sprite.position.y += this.tileHeight 198 | } 199 | } 200 | } 201 | 202 | saveData() {} 203 | 204 | setGid(x, y, gid) { 205 | const id = x + (y * this.sizeX) 206 | this.data[id] = gid 207 | this.needUpdateMesh = true 208 | } 209 | 210 | getGid(x, y) { 211 | const id = x + (y * this.sizeX) 212 | if(id < 0) { 213 | return 0 214 | } 215 | if(id >= this.data.length) { 216 | return 0 217 | } 218 | return this.data[id] - this.tileset.gid 219 | } 220 | 221 | getProperties(gid) { 222 | return this.tileset.getProperties(gid - this.tileset.gid) 223 | } 224 | 225 | getPropertiesFromTile(x, y) { 226 | const gid = this.getGid(x, y) 227 | return this.getProperties(gid) 228 | } 229 | 230 | updateUniforms() { 231 | this.drawCommand.uniforms = Object.assign({ 232 | color: this.color 233 | }, this.drawCommand.material.uniforms) 234 | } 235 | } 236 | 237 | export default TilemapLayer -------------------------------------------------------------------------------- /src/tilemap/TilemapOrthogonalLayer.js: -------------------------------------------------------------------------------- 1 | import Engine from "../Engine" 2 | import TilemapLayer from "./TilemapLayer" 3 | 4 | class TilemapOrthogonalLayer extends TilemapLayer 5 | { 6 | constructor() { 7 | super() 8 | } 9 | 10 | updateSize() { 11 | this.startX = -this.tileset.offsetX 12 | this.startY = -this.tileset.offsetY 13 | this.size.set( 14 | this.tileset.offsetX + (this.sizeX * this.tileWidth), 15 | this.tileset.offsetY + (this.sizeY * this.tileHeight)) 16 | } 17 | 18 | getWorldFromTile(tileX, tileY, output, tileCenter = false) { 19 | if(tileCenter) { 20 | output.x = this.startX + (tileX * this.tileWidth) + (this.tileWidth * 0.5) 21 | output.y = this.startY + (tileY * this.tileHeight) + (this.tileHeight * 0.5) 22 | } 23 | else { 24 | output.x = this.startX + (tileX * this.tileWidth) 25 | output.y = this.startY + (tileY * this.tileHeight) 26 | } 27 | } 28 | 29 | getTileFromWorld(worldX, worldY, output) { 30 | const transform = this.transform 31 | worldX += transform.m[6] 32 | worldY += transform.m[7] - this._size.y 33 | output.x = Math.floor(worldX / this.tileWidth) 34 | output.y = Math.floor(worldY / this.tileHeight) + this.sizeY 35 | } 36 | } 37 | 38 | export default TilemapOrthogonalLayer -------------------------------------------------------------------------------- /src/tilemap/component/TileBody.js: -------------------------------------------------------------------------------- 1 | import Component from "../../Component" 2 | import Time from "../../Time" 3 | import Vector2 from "../../math/Vector2" 4 | 5 | const point = new Vector2() 6 | 7 | class TileBody extends Component { 8 | constructor() { 9 | super() 10 | this.x = 0 11 | this.y = 0 12 | this.targetX = 0 13 | this.targetY = 0 14 | this.speed = 250 15 | this._path = null 16 | this.direction = new Vector2(0, 0) 17 | this.tStart = 0 18 | this.duration = 0 19 | 20 | this.onMoveStart = null 21 | this.onMoveDone = null 22 | this.onPathDone = null 23 | } 24 | 25 | onEnable() { 26 | this.parent.parent.getWorldFromTile(this.x, this.y, point, true) 27 | this.parent.position.set(point.x, point.y) 28 | } 29 | 30 | update(tDelta) { 31 | if(this.tStart > 0) { 32 | let elapsed = (Time.current - this.tStart) / this.duration 33 | if(elapsed >= 1) { 34 | this.tStart = 0 35 | this.direction.set(0, 0) 36 | this.parent.position.set(this.targetX, this.targetY) 37 | if(this.onMoveDone) { 38 | this.onMoveDone() 39 | } 40 | if(this.path && this.path.length > 0) { 41 | const node = this.path.pop() 42 | if(node) { 43 | this.moveTo(node.x, node.y) 44 | return 45 | } 46 | } 47 | if(this.onPathDone) { 48 | this.onPathDone() 49 | } 50 | } 51 | else { 52 | const x = this.startX + (this.targetX - this.startX) * elapsed 53 | const y = this.startY + (this.targetY - this.startY) * elapsed 54 | this.parent.position.set(x, y) 55 | } 56 | } 57 | else { 58 | if(this.path && this.path.length > 0) { 59 | const node = this.path.pop() 60 | if(node) { 61 | this.moveTo(node.x, node.y) 62 | return 63 | } 64 | } 65 | } 66 | } 67 | 68 | moveTo(x, y) { 69 | if(this.onMoveStart) { 70 | if(this.onMoveStart(x, y)) { 71 | return 72 | } 73 | } 74 | this.parent.parent.getWorldFromTile(x, y, point, true) 75 | this.x = x 76 | this.y = y 77 | this.startX = this.parent.x 78 | this.startY = this.parent.y 79 | this.targetX = point.x 80 | this.targetY = point.y 81 | 82 | this.direction.set(this.targetX - this.parent.x, this.targetY - this.parent.y) 83 | this.direction.normalize() 84 | 85 | this.tStart = Time.current 86 | this.duration = this.speed 87 | } 88 | 89 | setTile(x, y) { 90 | this.x = x 91 | this.y = y 92 | if(this.parent.parent) { 93 | this.parent.parent.getWorldFromTile(x, y, point) 94 | this.parent.position.set(point.x, point.y) 95 | } 96 | } 97 | 98 | set path(path) { 99 | this._path = path 100 | } 101 | 102 | get path() { 103 | return this._path 104 | } 105 | } 106 | 107 | export default TileBody -------------------------------------------------------------------------------- /src/tween/Easing.js: -------------------------------------------------------------------------------- 1 | 2 | const linear = (k) => { 3 | return k 4 | } 5 | 6 | const quadIn = (k) => { 7 | return k * k 8 | } 9 | 10 | const quadOut = (k) => { 11 | return k * (2 - k) 12 | } 13 | 14 | const quadInOut = (k) => { 15 | if((k *= 2) < 1) { 16 | return 0.5 * k * k 17 | } 18 | return -0.5 * (--k * (k - 2) - 1) 19 | } 20 | 21 | const cubicIn = (k) => { 22 | return k * k * k 23 | } 24 | 25 | const cubicOut = (k) => { 26 | return --k * k * k + 1 27 | } 28 | 29 | const cubicInOut = (k) => { 30 | if((k *= 2) < 1) { 31 | return 0.5 * k * k * k 32 | } 33 | return 0.5 * ((k -= 2) * k * k + 2) 34 | } 35 | 36 | const quartIn = (k) => { 37 | return k * k * k * k 38 | } 39 | 40 | const quartOut = (k) => { 41 | return 1 - (--k * k * k * k) 42 | } 43 | 44 | const quartInOut = (k) => { 45 | if((k *= 2) < 1) { 46 | return 0.5 * k * k * k * k 47 | } 48 | return -0.5 * ((k -= 2) * k * k * k - 2) 49 | } 50 | 51 | const quintIn = (k) => { 52 | return k * k * k * k * k 53 | } 54 | 55 | const quintOut = (k) => { 56 | return --k * k * k * k * k + 1 57 | } 58 | 59 | const quintInOut = (k) => { 60 | if((k *= 2) < 1) { 61 | return 0.5 * k * k * k * k * k 62 | } 63 | return 0.5 * ((k -= 2) * k * k * k * k + 2) 64 | } 65 | 66 | const sineIn = (k) => { 67 | return 1 - Math.cos(k * Math.PI / 2) 68 | } 69 | 70 | const sineOut = (k) => { 71 | return Math.sin(k * Math.PI / 2) 72 | } 73 | 74 | const sineInOut = (k) => { 75 | return 0.5 * (1 - Math.cos(Math.PI * k)) 76 | } 77 | 78 | const expoIn = (k) => { 79 | if(k === 0) { return 0 } 80 | return Math.pow(1024, k - 1) 81 | } 82 | 83 | const expoOut = (k) => { 84 | if(k === 1) { return 1 } 85 | return 1 - Math.pow(2, -10 * k) 86 | } 87 | 88 | const expoInOut = (k) => { 89 | if(k === 0) { return 0 } 90 | if(k === 1) { return 1 } 91 | if((k *= 2) < 1) { 92 | return 0.5 * Math.pow(1024, k - 1) 93 | } 94 | return 0.5 * (-Math.pow(2, - 10 * (k - 1)) + 2) 95 | } 96 | 97 | const circIn = (k) => { 98 | return 1 - Math.sqrt(1 - k * k) 99 | } 100 | 101 | const circOut = (k) => { 102 | return Math.sqrt(1 - (--k * k)) 103 | } 104 | 105 | const circInOut = (k) => { 106 | if((k *= 2) < 1) { 107 | return -0.5 * (Math.sqrt(1 - k * k) - 1) 108 | } 109 | return 0.5 * (Math.sqrt(1 - (k -= 2) * k) + 1) 110 | } 111 | 112 | const elasticIn = (k) => { 113 | if(k === 0) { return 0 } 114 | if(k === 1) { return 1 } 115 | 116 | let s 117 | let a = 0.1 118 | let p = 0.4 119 | 120 | if(!a || a < 1) { 121 | a = 1 122 | s = p / 4 123 | } 124 | else { 125 | s = p * Math.asin(1 / a) / (2 * Math.PI) 126 | } 127 | 128 | return -(a * Math.pow(2, 10 * (k -= 1)) * Math.sin((k - s) * (2 * Math.PI) / p)) 129 | } 130 | 131 | const elasticOut = (k) => { 132 | if(k === 0) { return 0 } 133 | if(k === 1) { return 1 } 134 | 135 | let s 136 | let a = 0.1 137 | let p = 0.4 138 | 139 | if(!a || a < 1) { 140 | a = 1 141 | s = p / 4 142 | } 143 | else { 144 | s = p * Math.asin(1 / a) / (2 * Math.PI) 145 | } 146 | 147 | return (a * Math.pow(2, - 10 * k) * Math.sin((k - s) * (2 * Math.PI) / p) + 1) 148 | } 149 | 150 | const elasticInOut = (k) => { 151 | if(k === 0) { return 0 } 152 | if(k === 1) { return 1 } 153 | 154 | let s 155 | let a = 0.1 156 | let p = 0.4 157 | 158 | if(!a || a < 1) { 159 | a = 1; 160 | s = p / 4 161 | } 162 | else { 163 | s = p * Math.asin(1 / a) / (2 * Math.PI) 164 | } 165 | 166 | if((k *= 2) < 1) { 167 | return -0.5 * (a * Math.pow(2, 10 * (k -= 1)) * Math.sin((k - s) * (2 * Math.PI) / p)) 168 | } 169 | 170 | return (a * Math.pow(2, -10 * (k -= 1)) * Math.sin((k - s) * (2 * Math.PI) / p) * 0.5 + 1) 171 | } 172 | 173 | const backIn = (k) => { 174 | const s = 1.70158 175 | return k * k * ((s + 1) * k - s) 176 | } 177 | 178 | const backOut = (k) => { 179 | const s = 1.70158 180 | return --k * k * ((s + 1) * k + s) + 1 181 | } 182 | 183 | const backInOut = (k) => { 184 | const s = 1.70158 * 1.525 185 | 186 | if((k *= 2) < 1) { 187 | return 0.5 * (k * k * ((s + 1) * k - s)) 188 | } 189 | 190 | return 0.5 * ((k -= 2) * k * ((s + 1) * k + s) + 2) 191 | } 192 | 193 | const bounceIn = (k) => { 194 | return 1 - bounceOut(1 - k) 195 | } 196 | 197 | const bounceOut = (k) => { 198 | if(k < (1 / 2.75)) { 199 | return 7.5625 * k * k 200 | } 201 | else if(k < (2 / 2.75)) { 202 | return 7.5625 * (k -= (1.5 / 2.75)) * k + 0.75 203 | } 204 | else if(k < (2.5 / 2.75)) { 205 | return 7.5625 * (k -= (2.25 / 2.75)) * k + 0.9375 206 | } 207 | 208 | return 7.5625 * (k -= (2.625 / 2.75)) * k + 0.984375 209 | } 210 | 211 | const bounceInOut = (k) => { 212 | if(k < 0.5) { 213 | return bounceIn(k * 2) * 0.5 214 | } 215 | return bounceOut(k * 2 - 1) * 0.5 + 0.5 216 | } 217 | 218 | export { 219 | linear, 220 | quadIn, quadOut, quadInOut, 221 | cubicIn, cubicOut, cubicInOut, 222 | quartIn, quartOut, quartInOut, 223 | quintIn, quintOut, quintInOut, 224 | sineIn, sineOut, sineInOut, 225 | expoIn, expoOut, expoInOut, 226 | circIn, circOut, circInOut, 227 | elasticIn, elasticOut, elasticInOut, 228 | backIn, backOut, backInOut, 229 | bounceIn, bounceOut, bounceInOut 230 | } -------------------------------------------------------------------------------- /src/tween/Tween.js: -------------------------------------------------------------------------------- 1 | import Component from "../Component" 2 | import Time from "../Time" 3 | import TweenManager from "./TweenManager" 4 | import Easing from "./Easing" 5 | 6 | function Link(endValues, duration, easing, repeat, onDone) { 7 | this.endValues = endValues 8 | this.duration = duration 9 | this.easing = easing 10 | this.repeat = repeat 11 | this.onDone = onDone 12 | } 13 | 14 | class Tween extends Component { 15 | constructor() { 16 | super() 17 | this.startValues = {} 18 | this.links = [] 19 | this.link = null 20 | this.linkIndex = -1 21 | this.tStart = 0 22 | this.rounding = false 23 | this.loop = false 24 | this._index = -1 25 | this._repeat = 0 26 | this.onStart = null 27 | this.onDone = null 28 | this.onUpdate = null 29 | } 30 | 31 | remove() { 32 | this.stop() 33 | } 34 | 35 | play(loop) { 36 | if(this.links.length === 0) { 37 | return 38 | } 39 | this.next() 40 | this.loop = loop || false 41 | TweenManager.add(this) 42 | if(this.onStart) { 43 | this.onStart() 44 | } 45 | } 46 | 47 | stop() { 48 | this.link = null 49 | this.linkIndex = -1 50 | TweenManager.remove(this) 51 | this._index = -1 52 | } 53 | 54 | updateLink(tDelta) { 55 | let tElapsed = (Time.current - this.tStart) / this.link.duration 56 | if(tElapsed > 1) { 57 | tElapsed = 1 58 | } 59 | 60 | const value = this.link.easing(tElapsed) 61 | const endValues = this.link.endValues 62 | for(let key in endValues) { 63 | const startValue = this.startValues[key] 64 | const endValue = endValues[key] 65 | 66 | if(typeof(startValue) === "string") { 67 | endValue = startValue + parseFloat(endValue, 4) 68 | } 69 | 70 | const result = startValue + (endValue - startValue) * value 71 | if(this.rounding) { 72 | result = Math.round(result) 73 | } 74 | 75 | this.parent[key] = result 76 | } 77 | 78 | if(this.onUpdate) { 79 | this.onUpdate() 80 | } 81 | 82 | if(tElapsed === 1) { 83 | this._repeat-- 84 | if(this._repeat === 0) { 85 | this.next() 86 | } 87 | else { 88 | this.tStart = Time.current 89 | } 90 | } 91 | } 92 | 93 | next() { 94 | this.linkIndex++ 95 | if(this.linkIndex >= this.links.length) { 96 | if(this.loop) { 97 | this.linkIndex = 0 98 | for(let key in this.startValues) { 99 | this.parent[key] = this.startValues[key] 100 | } 101 | } 102 | else { 103 | this.stop() 104 | if(this.onDone) { 105 | this.onDone() 106 | } 107 | return 108 | } 109 | } 110 | 111 | this.link = this.links[this.linkIndex] 112 | this.tStart = Time.current 113 | this._repeat = this.link.repeat 114 | 115 | const endValues = this.link.endValues 116 | for(let key in endValues) { 117 | this.startValues[key] = this.parent[key] 118 | } 119 | } 120 | 121 | reset() { 122 | this.linkIndex = 0 123 | this.link = this.links[this.linkIndex] 124 | this.tStart = Time.current 125 | this._repeat = this.link.repeat 126 | for(let key in this.startValues) { 127 | this.parent[key] = this.startValues[key] 128 | } 129 | TweenManager.add(this) 130 | if(this.onStart) { 131 | this.onStart() 132 | } 133 | } 134 | 135 | clear() { 136 | this.stop() 137 | this.links.length = 0 138 | } 139 | 140 | to(endValues, duration, easing, repeat, onDone) { 141 | const easingFunc = easing ? Easing[easing] : Easing.linear 142 | const link = new Link(endValues, duration, easingFunc || Easing.linear, repeat || 1, onDone || null) 143 | this.links.push(link) 144 | return this 145 | } 146 | 147 | wait(duration, onDone) { 148 | const link = new Link(null, duration, Easing.linear, 1, onDone || null) 149 | this.links.push(link) 150 | return this 151 | } 152 | } 153 | 154 | export default Tween -------------------------------------------------------------------------------- /src/tween/TweenManager.js: -------------------------------------------------------------------------------- 1 | import Engine from "../Engine" 2 | 3 | class TweenManager { 4 | constructor() { 5 | this.tweens = [] 6 | Engine.on("update", this.update.bind(this)) 7 | } 8 | 9 | update(tDeltaF) { 10 | for(let n = 0; n < this.tweens.length; n++) { 11 | this.tweens[n].updateLink() 12 | } 13 | } 14 | 15 | add(tween) { 16 | if(tween._index !== -1) { return } 17 | tween._index = this.tweens.length 18 | this.tweens.push(tween) 19 | } 20 | 21 | remove(tween) { 22 | if(tween._index === -1) { return } 23 | const tmp = this.tweens[this.tweens.length - 1] 24 | tmp._index = tween._index 25 | this.tweens[tween._index] = tmp 26 | this.tweens.pop() 27 | } 28 | } 29 | 30 | const instance = new TweenManager() 31 | export default instance --------------------------------------------------------------------------------