Source: Graphics.js

Graphics.js

  1. //-----------------------------------------------------------------------------
  2. /**
  3. * The static class that carries out graphics processing.
  4. *
  5. * @namespace
  6. */
  7. function Graphics() {
  8. throw new Error("This is a static class");
  9. }
  10. /**
  11. * Initializes the graphics system.
  12. *
  13. * @returns {boolean} True if the graphics system is available.
  14. */
  15. Graphics.initialize = function() {
  16. this._width = 0;
  17. this._height = 0;
  18. this._defaultScale = 1;
  19. this._realScale = 1;
  20. this._errorPrinter = null;
  21. this._tickHandler = null;
  22. this._canvas = null;
  23. this._fpsCounter = null;
  24. this._loadingSpinner = null;
  25. this._stretchEnabled = this._defaultStretchMode();
  26. this._app = null;
  27. this._effekseer = null;
  28. this._wasLoading = false;
  29. /**
  30. * The total frame count of the game screen.
  31. *
  32. * @type number
  33. * @name Graphics.frameCount
  34. */
  35. this.frameCount = 0;
  36. /**
  37. * The width of the window display area.
  38. *
  39. * @type number
  40. * @name Graphics.boxWidth
  41. */
  42. this.boxWidth = this._width;
  43. /**
  44. * The height of the window display area.
  45. *
  46. * @type number
  47. * @name Graphics.boxHeight
  48. */
  49. this.boxHeight = this._height;
  50. this._updateRealScale();
  51. this._createAllElements();
  52. this._disableContextMenu();
  53. this._setupEventHandlers();
  54. this._createPixiApp();
  55. this._createEffekseerContext();
  56. return !!this._app;
  57. };
  58. /**
  59. * The PIXI.Application object.
  60. *
  61. * @readonly
  62. * @type PIXI.Application
  63. * @name Graphics.app
  64. */
  65. Object.defineProperty(Graphics, "app", {
  66. get: function() {
  67. return this._app;
  68. },
  69. configurable: true
  70. });
  71. /**
  72. * The context object of Effekseer.
  73. *
  74. * @readonly
  75. * @type EffekseerContext
  76. * @name Graphics.effekseer
  77. */
  78. Object.defineProperty(Graphics, "effekseer", {
  79. get: function() {
  80. return this._effekseer;
  81. },
  82. configurable: true
  83. });
  84. /**
  85. * Register a handler for tick events.
  86. *
  87. * @param {function} handler - The listener function to be added for updates.
  88. */
  89. Graphics.setTickHandler = function(handler) {
  90. this._tickHandler = handler;
  91. };
  92. /**
  93. * Starts the game loop.
  94. */
  95. Graphics.startGameLoop = function() {
  96. if (this._app) {
  97. this._app.start();
  98. }
  99. };
  100. /**
  101. * Stops the game loop.
  102. */
  103. Graphics.stopGameLoop = function() {
  104. if (this._app) {
  105. this._app.stop();
  106. }
  107. };
  108. /**
  109. * Sets the stage to be rendered.
  110. *
  111. * @param {Stage} stage - The stage object to be rendered.
  112. */
  113. Graphics.setStage = function(stage) {
  114. if (this._app) {
  115. this._app.stage = stage;
  116. }
  117. };
  118. /**
  119. * Shows the loading spinner.
  120. */
  121. Graphics.startLoading = function() {
  122. if (!document.getElementById("loadingSpinner")) {
  123. document.body.appendChild(this._loadingSpinner);
  124. }
  125. };
  126. /**
  127. * Erases the loading spinner.
  128. *
  129. * @returns {boolean} True if the loading spinner was active.
  130. */
  131. Graphics.endLoading = function() {
  132. if (document.getElementById("loadingSpinner")) {
  133. document.body.removeChild(this._loadingSpinner);
  134. return true;
  135. } else {
  136. return false;
  137. }
  138. };
  139. /**
  140. * Displays the error text to the screen.
  141. *
  142. * @param {string} name - The name of the error.
  143. * @param {string} message - The message of the error.
  144. * @param {Error} [error] - The error object.
  145. */
  146. Graphics.printError = function(name, message, error = null) {
  147. if (!this._errorPrinter) {
  148. this._createErrorPrinter();
  149. }
  150. this._errorPrinter.innerHTML = this._makeErrorHtml(name, message, error);
  151. this._wasLoading = this.endLoading();
  152. this._applyCanvasFilter();
  153. };
  154. /**
  155. * Displays a button to try to reload resources.
  156. *
  157. * @param {function} retry - The callback function to be called when the button
  158. * is pressed.
  159. */
  160. Graphics.showRetryButton = function(retry) {
  161. const button = document.createElement("button");
  162. button.id = "retryButton";
  163. button.innerHTML = "Retry";
  164. // [Note] stopPropagation() is required for iOS Safari.
  165. button.ontouchstart = e => e.stopPropagation();
  166. button.onclick = () => {
  167. Graphics.eraseError();
  168. retry();
  169. };
  170. this._errorPrinter.appendChild(button);
  171. button.focus();
  172. };
  173. /**
  174. * Erases the loading error text.
  175. */
  176. Graphics.eraseError = function() {
  177. if (this._errorPrinter) {
  178. this._errorPrinter.innerHTML = this._makeErrorHtml();
  179. if (this._wasLoading) {
  180. this.startLoading();
  181. }
  182. }
  183. this._clearCanvasFilter();
  184. };
  185. /**
  186. * Converts an x coordinate on the page to the corresponding
  187. * x coordinate on the canvas area.
  188. *
  189. * @param {number} x - The x coordinate on the page to be converted.
  190. * @returns {number} The x coordinate on the canvas area.
  191. */
  192. Graphics.pageToCanvasX = function(x) {
  193. if (this._canvas) {
  194. const left = this._canvas.offsetLeft;
  195. return Math.round((x - left) / this._realScale);
  196. } else {
  197. return 0;
  198. }
  199. };
  200. /**
  201. * Converts a y coordinate on the page to the corresponding
  202. * y coordinate on the canvas area.
  203. *
  204. * @param {number} y - The y coordinate on the page to be converted.
  205. * @returns {number} The y coordinate on the canvas area.
  206. */
  207. Graphics.pageToCanvasY = function(y) {
  208. if (this._canvas) {
  209. const top = this._canvas.offsetTop;
  210. return Math.round((y - top) / this._realScale);
  211. } else {
  212. return 0;
  213. }
  214. };
  215. /**
  216. * Checks whether the specified point is inside the game canvas area.
  217. *
  218. * @param {number} x - The x coordinate on the canvas area.
  219. * @param {number} y - The y coordinate on the canvas area.
  220. * @returns {boolean} True if the specified point is inside the game canvas area.
  221. */
  222. Graphics.isInsideCanvas = function(x, y) {
  223. return x >= 0 && x < this._width && y >= 0 && y < this._height;
  224. };
  225. /**
  226. * Shows the game screen.
  227. */
  228. Graphics.showScreen = function() {
  229. this._canvas.style.opacity = 1;
  230. };
  231. /**
  232. * Hides the game screen.
  233. */
  234. Graphics.hideScreen = function() {
  235. this._canvas.style.opacity = 0;
  236. };
  237. /**
  238. * Changes the size of the game screen.
  239. *
  240. * @param {number} width - The width of the game screen.
  241. * @param {number} height - The height of the game screen.
  242. */
  243. Graphics.resize = function(width, height) {
  244. this._width = width;
  245. this._height = height;
  246. this._updateAllElements();
  247. };
  248. /**
  249. * The width of the game screen.
  250. *
  251. * @type number
  252. * @name Graphics.width
  253. */
  254. Object.defineProperty(Graphics, "width", {
  255. get: function() {
  256. return this._width;
  257. },
  258. set: function(value) {
  259. if (this._width !== value) {
  260. this._width = value;
  261. this._updateAllElements();
  262. }
  263. },
  264. configurable: true
  265. });
  266. /**
  267. * The height of the game screen.
  268. *
  269. * @type number
  270. * @name Graphics.height
  271. */
  272. Object.defineProperty(Graphics, "height", {
  273. get: function() {
  274. return this._height;
  275. },
  276. set: function(value) {
  277. if (this._height !== value) {
  278. this._height = value;
  279. this._updateAllElements();
  280. }
  281. },
  282. configurable: true
  283. });
  284. /**
  285. * The default zoom scale of the game screen.
  286. *
  287. * @type number
  288. * @name Graphics.defaultScale
  289. */
  290. Object.defineProperty(Graphics, "defaultScale", {
  291. get: function() {
  292. return this._defaultScale;
  293. },
  294. set: function(value) {
  295. if (this._defaultScale !== value) {
  296. this._defaultScale = value;
  297. this._updateAllElements();
  298. }
  299. },
  300. configurable: true
  301. });
  302. Graphics._createAllElements = function() {
  303. this._createErrorPrinter();
  304. this._createCanvas();
  305. this._createLoadingSpinner();
  306. this._createFPSCounter();
  307. };
  308. Graphics._updateAllElements = function() {
  309. this._updateRealScale();
  310. this._updateErrorPrinter();
  311. this._updateCanvas();
  312. this._updateVideo();
  313. };
  314. Graphics._onTick = function(deltaTime) {
  315. this._fpsCounter.startTick();
  316. if (this._tickHandler) {
  317. this._tickHandler(deltaTime);
  318. }
  319. if (this._canRender()) {
  320. this._app.render();
  321. }
  322. this._fpsCounter.endTick();
  323. };
  324. Graphics._canRender = function() {
  325. return !!this._app.stage;
  326. };
  327. Graphics._updateRealScale = function() {
  328. if (this._stretchEnabled && this._width > 0 && this._height > 0) {
  329. const h = this._stretchWidth() / this._width;
  330. const v = this._stretchHeight() / this._height;
  331. this._realScale = Math.min(h, v);
  332. window.scrollTo(0, 0);
  333. } else {
  334. this._realScale = this._defaultScale;
  335. }
  336. };
  337. Graphics._stretchWidth = function() {
  338. if (Utils.isMobileDevice()) {
  339. return document.documentElement.clientWidth;
  340. } else {
  341. return window.innerWidth;
  342. }
  343. };
  344. Graphics._stretchHeight = function() {
  345. if (Utils.isMobileDevice()) {
  346. // [Note] Mobile browsers often have special operations at the top and
  347. // bottom of the screen.
  348. const rate = Utils.isLocal() ? 1.0 : 0.9;
  349. return document.documentElement.clientHeight * rate;
  350. } else {
  351. return window.innerHeight;
  352. }
  353. };
  354. Graphics._makeErrorHtml = function(name, message /*, error*/) {
  355. const nameDiv = document.createElement("div");
  356. const messageDiv = document.createElement("div");
  357. nameDiv.id = "errorName";
  358. messageDiv.id = "errorMessage";
  359. nameDiv.innerHTML = Utils.escapeHtml(name || "");
  360. messageDiv.innerHTML = Utils.escapeHtml(message || "");
  361. return nameDiv.outerHTML + messageDiv.outerHTML;
  362. };
  363. Graphics._defaultStretchMode = function() {
  364. return Utils.isNwjs() || Utils.isMobileDevice();
  365. };
  366. Graphics._createErrorPrinter = function() {
  367. this._errorPrinter = document.createElement("div");
  368. this._errorPrinter.id = "errorPrinter";
  369. this._errorPrinter.innerHTML = this._makeErrorHtml();
  370. document.body.appendChild(this._errorPrinter);
  371. };
  372. Graphics._updateErrorPrinter = function() {
  373. const width = 640 * this._realScale;
  374. const height = 100 * this._realScale;
  375. this._errorPrinter.style.width = width + "px";
  376. this._errorPrinter.style.height = height + "px";
  377. };
  378. Graphics._createCanvas = function() {
  379. this._canvas = document.createElement("canvas");
  380. this._canvas.id = "gameCanvas";
  381. this._updateCanvas();
  382. document.body.appendChild(this._canvas);
  383. };
  384. Graphics._updateCanvas = function() {
  385. this._canvas.width = this._width;
  386. this._canvas.height = this._height;
  387. this._canvas.style.zIndex = 1;
  388. this._centerElement(this._canvas);
  389. };
  390. Graphics._updateVideo = function() {
  391. const width = this._width * this._realScale;
  392. const height = this._height * this._realScale;
  393. Video.resize(width, height);
  394. };
  395. Graphics._createLoadingSpinner = function() {
  396. const loadingSpinner = document.createElement("div");
  397. const loadingSpinnerImage = document.createElement("div");
  398. loadingSpinner.id = "loadingSpinner";
  399. loadingSpinnerImage.id = "loadingSpinnerImage";
  400. loadingSpinner.appendChild(loadingSpinnerImage);
  401. this._loadingSpinner = loadingSpinner;
  402. };
  403. Graphics._createFPSCounter = function() {
  404. this._fpsCounter = new Graphics.FPSCounter();
  405. };
  406. Graphics._centerElement = function(element) {
  407. const width = element.width * this._realScale;
  408. const height = element.height * this._realScale;
  409. element.style.position = "absolute";
  410. element.style.margin = "auto";
  411. element.style.top = 0;
  412. element.style.left = 0;
  413. element.style.right = 0;
  414. element.style.bottom = 0;
  415. element.style.width = width + "px";
  416. element.style.height = height + "px";
  417. };
  418. Graphics._disableContextMenu = function() {
  419. const elements = document.body.getElementsByTagName("*");
  420. const oncontextmenu = () => false;
  421. for (const element of elements) {
  422. element.oncontextmenu = oncontextmenu;
  423. }
  424. };
  425. Graphics._applyCanvasFilter = function() {
  426. if (this._canvas) {
  427. this._canvas.style.opacity = 0.5;
  428. this._canvas.style.filter = "blur(8px)";
  429. this._canvas.style.webkitFilter = "blur(8px)";
  430. }
  431. };
  432. Graphics._clearCanvasFilter = function() {
  433. if (this._canvas) {
  434. this._canvas.style.opacity = 1;
  435. this._canvas.style.filter = "";
  436. this._canvas.style.webkitFilter = "";
  437. }
  438. };
  439. Graphics._setupEventHandlers = function() {
  440. window.addEventListener("resize", this._onWindowResize.bind(this));
  441. document.addEventListener("keydown", this._onKeyDown.bind(this));
  442. };
  443. Graphics._onWindowResize = function() {
  444. this._updateAllElements();
  445. };
  446. Graphics._onKeyDown = function(event) {
  447. if (!event.ctrlKey && !event.altKey) {
  448. switch (event.keyCode) {
  449. case 113: // F2
  450. event.preventDefault();
  451. this._switchFPSCounter();
  452. break;
  453. case 114: // F3
  454. event.preventDefault();
  455. this._switchStretchMode();
  456. break;
  457. case 115: // F4
  458. event.preventDefault();
  459. this._switchFullScreen();
  460. break;
  461. }
  462. }
  463. };
  464. Graphics._switchFPSCounter = function() {
  465. this._fpsCounter.switchMode();
  466. };
  467. Graphics._switchStretchMode = function() {
  468. this._stretchEnabled = !this._stretchEnabled;
  469. this._updateAllElements();
  470. };
  471. Graphics._switchFullScreen = function() {
  472. if (this._isFullScreen()) {
  473. this._cancelFullScreen();
  474. } else {
  475. this._requestFullScreen();
  476. }
  477. };
  478. Graphics._isFullScreen = function() {
  479. return (
  480. document.fullScreenElement ||
  481. document.mozFullScreen ||
  482. document.webkitFullscreenElement
  483. );
  484. };
  485. Graphics._requestFullScreen = function() {
  486. const element = document.body;
  487. if (element.requestFullScreen) {
  488. element.requestFullScreen();
  489. } else if (element.mozRequestFullScreen) {
  490. element.mozRequestFullScreen();
  491. } else if (element.webkitRequestFullScreen) {
  492. element.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
  493. }
  494. };
  495. Graphics._cancelFullScreen = function() {
  496. if (document.cancelFullScreen) {
  497. document.cancelFullScreen();
  498. } else if (document.mozCancelFullScreen) {
  499. document.mozCancelFullScreen();
  500. } else if (document.webkitCancelFullScreen) {
  501. document.webkitCancelFullScreen();
  502. }
  503. };
  504. Graphics._createPixiApp = function() {
  505. try {
  506. this._setupPixi();
  507. this._app = new PIXI.Application({
  508. view: this._canvas,
  509. autoStart: false
  510. });
  511. this._app.ticker.remove(this._app.render, this._app);
  512. this._app.ticker.add(this._onTick, this);
  513. } catch (e) {
  514. this._app = null;
  515. }
  516. };
  517. Graphics._setupPixi = function() {
  518. PIXI.utils.skipHello();
  519. PIXI.settings.GC_MAX_IDLE = 600;
  520. };
  521. Graphics._createEffekseerContext = function() {
  522. if (this._app && window.effekseer) {
  523. try {
  524. this._effekseer = effekseer.createContext();
  525. if (this._effekseer) {
  526. this._effekseer.init(this._app.renderer.gl);
  527. }
  528. } catch (e) {
  529. this._app = null;
  530. }
  531. }
  532. };
  533. //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  534. // FPSCounter
  535. //
  536. // This is based on Darsain's FPSMeter which is under the MIT license.
  537. // The original can be found at https://github.com/Darsain/fpsmeter.
  538. Graphics.FPSCounter = function() {
  539. this.initialize(...arguments);
  540. };
  541. Graphics.FPSCounter.prototype.initialize = function() {
  542. this._tickCount = 0;
  543. this._frameTime = 100;
  544. this._frameStart = 0;
  545. this._lastLoop = performance.now() - 100;
  546. this._showFps = true;
  547. this.fps = 0;
  548. this.duration = 0;
  549. this._createElements();
  550. this._update();
  551. };
  552. Graphics.FPSCounter.prototype.startTick = function() {
  553. this._frameStart = performance.now();
  554. };
  555. Graphics.FPSCounter.prototype.endTick = function() {
  556. const time = performance.now();
  557. const thisFrameTime = time - this._lastLoop;
  558. this._frameTime += (thisFrameTime - this._frameTime) / 12;
  559. this.fps = 1000 / this._frameTime;
  560. this.duration = Math.max(0, time - this._frameStart);
  561. this._lastLoop = time;
  562. if (this._tickCount++ % 15 === 0) {
  563. this._update();
  564. }
  565. };
  566. Graphics.FPSCounter.prototype.switchMode = function() {
  567. if (this._boxDiv.style.display === "none") {
  568. this._boxDiv.style.display = "block";
  569. this._showFps = true;
  570. } else if (this._showFps) {
  571. this._showFps = false;
  572. } else {
  573. this._boxDiv.style.display = "none";
  574. }
  575. this._update();
  576. };
  577. Graphics.FPSCounter.prototype._createElements = function() {
  578. this._boxDiv = document.createElement("div");
  579. this._labelDiv = document.createElement("div");
  580. this._numberDiv = document.createElement("div");
  581. this._boxDiv.id = "fpsCounterBox";
  582. this._labelDiv.id = "fpsCounterLabel";
  583. this._numberDiv.id = "fpsCounterNumber";
  584. this._boxDiv.style.display = "none";
  585. this._boxDiv.appendChild(this._labelDiv);
  586. this._boxDiv.appendChild(this._numberDiv);
  587. document.body.appendChild(this._boxDiv);
  588. };
  589. Graphics.FPSCounter.prototype._update = function() {
  590. const count = this._showFps ? this.fps : this.duration;
  591. this._labelDiv.textContent = this._showFps ? "FPS" : "ms";
  592. this._numberDiv.textContent = count.toFixed(0);
  593. };