UNPKG

shaku

Version:

A simple and effective JavaScript game development framework that knows its place!

369 lines (323 loc) 11.9 kB
/** * Implement texture asset type. * * |-- copyright and license --| * @module Shaku * @file shaku\src\assets\texture_asset.js * @author Ronen Ness (ronenness@gmail.com | http://ronenness.com) * @copyright (c) 2021 Ronen Ness * @license MIT * |-- end copyright and license --| * */ 'use strict'; const Color = require('../utils/color'); const TextureAssetBase = require("./texture_asset_base"); const _logger = require('../logger.js').getLogger('assets'); // the webgl context to use var gl = null; /** * A loadable texture asset. * This asset type loads an image from URL or source, and turn it into a texture. */ class TextureAsset extends TextureAssetBase { /** @inheritdoc */ constructor(url) { super(url); this._image = null; this._width = 0; this._height = 0; this._texture = null; this._ctxForPixelData = null; } /** * Set the WebGL context. * @private */ static _setWebGl(_gl) { gl = _gl; } /** * Load the texture from it's image URL. * @param {*} params Optional additional params. Possible values are: * - generateMipMaps (default=false): if true, will generate mipmaps for this texture. * - crossOrigin (default=undefined): if set, will set the crossOrigin property with this value. * - flipY (default=false): if true, will flip texture on Y axis. * - premultiplyAlpha (default=false): if true, will load texture with premultiply alpha flag set. * @returns {Promise} Promise to resolve when fully loaded. */ load(params) { // default params params = params || {}; return new Promise((resolve, reject) => { if (!gl) { return reject("Can't load textures before initializing gfx manager!"); } // create image to load const image = new Image(); if (params.crossOrigin !== undefined) { image.crossOrigin = params.crossOrigin; } image.onload = async () => { try { await this.create(image, params); this._notifyReady(); resolve(); } catch (e) { reject(e); } }; image.onerror = () => { reject("Failed to load texture image!"); } // initiate image load image.src = this.url; }); } /** * Create this texture as an empty render target. * @param {Number} width Texture width. * @param {Number} height Texture height. * @param {Number} channels Texture channels count. Defaults to 4 (RGBA). */ createRenderTarget(width, height, channels) { // reset flags gl.pixelStorei( gl.UNPACK_FLIP_Y_WEBGL, false ); gl.pixelStorei( gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false ); // create to render to const targetTextureWidth = width; const targetTextureHeight = height; const targetTexture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, targetTexture); // calculate format var _format = gl.RGBA; if (channels !== undefined) { switch (channels) { case 1: _format = gl.LUMINANCE; break; case 3: _format = gl.RGB; break; case 4: _format = gl.RGBA; break; default: throw new Error("Unknown render target format!"); } } { // create texture const level = 0; const internalFormat = _format; const border = 0; const format = _format; const type = gl.UNSIGNED_BYTE; const data = null; gl.texImage2D(gl.TEXTURE_2D, level, internalFormat, targetTextureWidth, targetTextureHeight, border, format, type, data); // set default wrap and filter modes gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); } // store texture this._width = width; this._height = height; this._texture = targetTexture; this._notifyReady(); } /** * Create texture from loaded image instance. * @see TextureAsset.load for params. * @param {Image} image Image to create texture from. Image must be loaded! * @param {*} params Optional additional params. See load() for details. */ fromImage(image, params) { if (image.width === 0) { throw new Error("Image to build texture from must be loaded and have valid size!"); } if (this.valid) { throw new Error("Texture asset is already initialized!"); } // default params params = params || {}; // set flip Y argument gl.pixelStorei( gl.UNPACK_FLIP_Y_WEBGL, Boolean(params.flipY) ); // set premultiply alpha params gl.pixelStorei( gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, Boolean(params.premultiplyAlpha) ); // store image this._image = image; this._width = image.width; this._height = image.height; // create texture const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); // set texture const level = 0; const internalFormat = gl.RGBA; const srcFormat = gl.RGBA; const srcType = gl.UNSIGNED_BYTE; gl.bindTexture(gl.TEXTURE_2D, texture); gl.texImage2D(gl.TEXTURE_2D, level, internalFormat, srcFormat, srcType, image); // WebGL1 has different requirements for power of 2 images // vs non power of 2 images so check if the image is a // power of 2 in both dimensions. if (params.generateMipMaps) { if (isPowerOf2(image.width) && isPowerOf2(image.height)) { _logger.warn("Tried to generate MipMaps for a texture with size that is *not* a power of two. This might not work as expected."); } gl.generateMipmap(gl.TEXTURE_2D); } // default wrap and filters gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); // success! this._texture = texture; this._notifyReady(); } /** * Create the texture from an image. * @see TextureAsset.load for params. * @param {Image|String} source Image or Image source URL to create texture from. * @param {*} params Optional additional params. See load() for details. * @returns {Promise} Promise to resolve when asset is ready. */ create(source, params) { return new Promise(async (resolve, reject) => { if (typeof source === "string") { let img = new Image(); img.onload = () => { this.fromImage(source, params); this._notifyReady(); resolve(); } if (params.crossOrigin !== undefined) { img.crossOrigin = params.crossOrigin; } img.src = source; } else { this.fromImage(source, params); resolve(); } }); } /** @inheritdoc */ get image() { return this._image; } /** @inheritdoc */ get width() { return this._width; } /** @inheritdoc */ get height() { return this._height; } /** @inheritdoc */ get _glTexture() { return this._texture; } /** * Get pixel color from image. * Note: this method is quite slow, if you need to perform multiple queries consider using `getPixelsData()` once to get all pixels data instead. * @param {Number} x Pixel X value. * @param {Number} y Pixel Y value. * @returns {Color} Pixel color. */ getPixel(x, y) { if (!this._image) { throw new Error("'getPixel()' only works on textures loaded from image!"); } // build internal canvas and context to get pixel data if (!this._ctxForPixelData) { let canvas = document.createElement('canvas'); canvas.width = 1; canvas.height = 1; this._ctxForPixelData = canvas.getContext('2d'); } // get pixel data let ctx = this._ctxForPixelData; ctx.drawImage(this._image, x, y, 1, 1, 0, 0, 1, 1); let pixelData = ctx.getImageData(0, 0, 1, 1).data; return Color.fromBytesArray(pixelData); } /** * Get a 2D array with pixel colors. * @param {Number=} x Offset X in texture to get. Defaults to 0. * @param {Number=} y Offset Y in texture to get. Defaults to 0. * @param {Number=} width How many pixels to get on X axis. Defaults to texture width - x. * @param {Number=} height How many pixels to get on Y axis. Defaults to texture height - y. * @returns {Array<Array<Color>>} A 2D array with all texture pixel colors. */ getPixelsData(x, y, width, height) { if (!this._image) { throw new Error("'getPixel()' only works on textures loaded from image!"); } // default x, y x = x || 0; y = y || 0; // default width / height width = width || (this.width - x); height = height || (this.height - y); // build internal canvas and context to get pixel data if (!this._ctxForPixelData) { let canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; this._ctxForPixelData = canvas.getContext('2d'); } // get pixel data let ctx = this._ctxForPixelData; ctx.drawImage(this._image, x, y, width, height, 0, 0, width, height); let pixelData = ctx.getImageData(x, y, width, height).data; // convert to colors let ret = []; for (let i = 0; i < width; ++i) { let currRow = []; ret.push(currRow); for (let j = 0; j < height; ++j) { currRow.push(Color.fromBytesArray(pixelData, i * 4 + (j * 4 * width))); } } return ret; } /** @inheritdoc */ get valid() { return Boolean(this._texture); } /** @inheritdoc */ destroy() { gl.deleteTexture(this._texture); this._image = null; this._width = this._height = 0; this._ctxForPixelData = null; this._texture = null; } } // check if value is a power of 2 function isPowerOf2(value) { return (value & (value - 1)) == 0; } // export the asset type. module.exports = TextureAsset;