UNPKG

phaser

Version:

A fast, free and fun HTML5 Game Framework for Desktop and Mobile web browsers from the team at Phaser Studio Inc.

515 lines (464 loc) 17.1 kB
/** * @author Benjamin D. Richards <benjamindrichards@gmail.com> * @copyright 2013-2026 Phaser Studio Inc. * @license {@link https://opensource.org/licenses/MIT|MIT License} */ var Linear = require('../math/Linear'); var Utils = require('../renderer/webgl/Utils'); var Class = require('../utils/Class'); var UUID = require('../utils/string/UUID'); var ColorBand = require('./ColorBand'); var getTint = Utils.getTintFromFloats; /** * @classdesc * The ColorRamp class represents a series of color transitions. * It is intended for use in a {@see Phaser.GameObjects.Gradient}. * * You should make sure that your bands are arranged end-to-end, * with no gaps. The Gradient shader assumes this is so. * You may leave gaps at the start and end. * Overlaps and gaps may not act as expected. * * By default, ColorRamp stores its data for use on the GPU * in a data texture. This is updated automatically on creation * and when you run `setBands()`, but if you edit the bands manually, * you should run `encode()` to rebuild the texture. * We don't update it automatically because we don't want to waste cycles * on rebuilds that you're about to overwrite. * * @class ColorRamp * @memberof Phaser.Display * @since 4.0.0 * @constructor * * @param {Phaser.Scene} scene - The current scene. * @param {Phaser.Types.Display.ColorBandConfig | Phaser.Display.ColorBand | Array<Phaser.Types.Display.ColorBandConfig | Phaser.Display.ColorBand>} bands - The bands which make up this ramp. This can be one entry or an array, and can be configs or existing instances. A band count over 1048576 may be unsafe. * @param {boolean} [gpuEncode=true] - Whether to create a data texture to use this ramp in shaders. */ var ColorRamp = new Class({ initialize: function ColorRamp (scene, bands, gpuEncode) { if (gpuEncode === undefined) { gpuEncode = true; } /** * The scene where the ColorRamp was created. * * @name Phaser.Display.ColorRamp#scene * @type {Phaser.Scene} * @since 4.0.0 * @readonly */ this.scene = scene; /** * The color bands that make up this ramp. * * @name Phaser.Display.ColorRamp#bands * @type {Phaser.Display.ColorBand[]} * @since 4.0.0 */ this.bands = []; /** * Whether to encode the ramp for shaders to use on the GPU. * An encoded ramp is stored as a texture. * * @name Phaser.Display.ColorRamp#gpuEncode * @type {boolean} * @since 4.0.0 * @default true */ this.gpuEncode = gpuEncode; /** * The Phaser Texture wrapping the GPU data texture for this ramp. * This is registered with the Scene's Texture Manager under a unique key * so it can be referenced elsewhere. It is not intended for display. * * @name Phaser.Display.ColorRamp#dataTexture * @type {?Phaser.Textures.Texture} * @since 4.0.0 * @readonly */ this.dataTexture = null; /** * The texture containing the ramp encoded for the GPU. * This is used internally by effects such as the Gradient game object * to read complex ramp data. * * @name Phaser.Display.ColorRamp#glTexture * @type {?Phaser.Renderer.WebGL.Wrappers.WebGLTextureWrapper} * @since 4.0.0 * @readonly */ this.glTexture = null; /** * The texel index which contains the first band data * in `glTexture` if it has been encoded. * This is used internally. * * @name Phaser.Display.ColorRamp#dataTextureFirstBand * @type {number} * @since 4.0.0 * @readonly */ this.dataTextureFirstBand = 0; /** * The number of levels in the band tree. * This is used internally to decode the data texture. * * @name Phaser.Display.ColorRamp#bandTreeDepth * @type {number} * @since 4.0.0 * @readonly */ this.bandTreeDepth = 0; /** * The resolution of the data texture, if it has been encoded. * This is used internally. * * @name Phaser.Display.ColorRamp#dataTextureResolution * @type {number[]} * @since 4.0.0 * @readonly */ this.dataTextureResolution = [ 0, 0 ]; this.setBands(bands); }, /** * Set or replace the color bands in this ramp. * Use this after creation to update the bands. * * This will re-encode the data texture if `gpuEncode` is set * and `encode` is not `false`. * * @method Phaser.Display.ColorRamp#setBands * @since 4.0.0 * * @param {Phaser.Types.Display.ColorBandConfig | Phaser.Display.ColorBand | Array<Phaser.Types.Display.ColorBandConfig | Phaser.Display.ColorBand>} bands - The bands to make up this ramp. This can be one entry or an array, and can be configs or existing instances. * @param {boolean} [encode=true] - Whether to encode the new ramp data to a data texture for use in shaders. * * @return {this} - This ColorRamp instance. */ setBands: function (bands, encode) { this.bands.length = 0; if (!Array.isArray(bands)) { bands = [ bands ]; } var lastEnd = 0; for (var i = 0; i < bands.length; i++) { var band = bands[i]; if (band.isColorBand) { this.bands.push(band); lastEnd = band.end; continue; } // `band` must be a config object. if (band.start === undefined) { band = Object.assign({ start: lastEnd }, band); } var newBand = new ColorBand(band); this.bands.push(newBand); lastEnd = newBand.end; } if ((encode !== false) && this.gpuEncode) { this.encode(); } return this; }, /** * Encode a data texture from the color ramp bands. * * This process runs automatically when `gpuEncode` is enabled * and the bands are set or updated with `setBands`. * If you modify the bands directly, you must call `encode` yourself. * * The data is encoded in texels as follows: * * Numbers are encoded in "RG.BA" form. * The number equals R * 255 + G + B / 255 + A / 255 / 255. * * - First 2 texels: start and end. * - Start is the start of the first band. * - End is the end of the last band. * - Next block of texels: binary symmetrical tree of band ranges, * represented as the end value of the midpoint band. * - Final block of texels, starting at `dataTextureFirstBand`: * the band data, in blocks of 3: * - colorStart * - colorEnd * - colorSpace * 255 + interpolation + (middle / 2) * * The binary symmetrical tree breaks the bands list in half * with every node. It is intended to quickly find the band corresponding * to a given progress along the ramp. * For example, a ramp with 10 bands would store the ends in this order: * * `[ 7, 3, 11, 1, 5, 9, 13, 0, 2, 4, 6, 8, 10, 12, 14 ]` * * But because it doesn't have bands from index 10 and up, it's actually: * * `[ 7, 3, 9, 1, 5, 9, 9, 0, 2, 4, 6, 8, 9, 9, 9 ]` * * Note that, if you change the number of bands in the ramp, * `dataTexture` may no longer have the correct resolution. * It is not intended for display. * * @method Phaser.Display.ColorRamp#encode * @since 4.0.0 */ encode: function () { var bandCount = this.bands.length; var bandTreeDepth = Math.ceil(Math.log2(bandCount)); this.bandTreeDepth = bandTreeDepth; // Binary tree nodes necessary to hold all bands symmetrically var bandTreeSize = Math.pow(2, bandTreeDepth) - 1; var BYTES_FOR_START_END = 8; // Start, end var BYTES_PER_BRANCH = 4; // Split point var BYTES_PER_BAND = 12; // Color start, color end, interpolation mode + color mode + middle var dataSize = BYTES_FOR_START_END + BYTES_PER_BRANCH * bandTreeSize + BYTES_PER_BAND * bandCount; var width = dataSize / 4; var height = 1; if (width > 4096) { height = Math.ceil(width / 4096); width = 4096; } this.dataTextureResolution[0] = width; this.dataTextureResolution[1] = height; var data = new ArrayBuffer(width * height * 4); var u32 = new Uint32Array(data); var u8 = new Uint8Array(data); var index32 = 0; var FLOAT_FACTOR = 256 * 256; // Encode floating point numbers as integers. // Encode start and end as RG.BA u32[index32++] = Math.round(this.bands[0].start * FLOAT_FACTOR); u32[index32++] = Math.round(this.bands[this.bands.length - 1].end * FLOAT_FACTOR); // Encode tree nodes. // Each tree node is a split point in RG.BA form. for (var depth = 0; depth <= bandTreeDepth; depth++) { var maxBreadth = Math.pow(2, depth); for (var breadth = 1; breadth < maxBreadth; breadth += 2) { var bandIndex = Math.floor(bandTreeSize * breadth / maxBreadth); var band = this.bands[Math.min(bandIndex, bandCount - 1)]; u32[index32++] = Math.round(band.end * FLOAT_FACTOR); } } // We are beginning the band encoding region. this.dataTextureFirstBand = index32; // Each band is encoded as colorStart, colorEnd, // and a composite value of colorMode, interpolationMode, middle. for (var i = 0; i < bandCount; i++) { band = this.bands[i]; // Encode colors so they'll appear correct in the texture. var a = band.colorStart; var b = band.colorEnd; u32[index32++] = getTint(a.blueGL, a.greenGL, a.redGL, a.alphaGL); u32[index32++] = getTint(b.blueGL, b.greenGL, b.redGL, b.alphaGL); // Encode other data as a composite in RG.BA form. var colorSpace = band.colorSpace * 255; var interpolation = band.interpolation; var middle = band.middle / 2; u32[index32++] = FLOAT_FACTOR * (colorSpace + interpolation + middle); } if (!this.glTexture) { var textureWrapper = this.scene.renderer.createUint8ArrayTexture(u8, width, height, false, false); this.glTexture = textureWrapper; var textureKey = UUID(); while (this.scene.textures.exists(textureKey)) { textureKey = UUID(); } this.dataTexture = this.scene.textures.addGLTexture(textureKey, textureWrapper); } else { var d = this.glTexture; d.update(u8, width, height, d.flipY, d.wrapS, d.wrapT, d.minFilter, d.magFilter, d.format); } }, /** * Fix the fit of bands within this ColorRamp. * * This sets the start of each band to the end of the previous band, * ensuring that there are no gaps. * * Optionally, you can define start and end values to stretch the ramp * to some specific range, e.g. 0-1. * * By default, any band that is now 0 length will be removed. * * @method Phaser.Display.ColorRamp#fixFit * @since 4.0.0 * @param {number} start - Override the start of the first band. * @param {number} end - Override the end of the last band. * @param {boolean} purgeZeroLength - Whether to discard bands that now have 0 size. * @param {boolean} encode - Whether to reencode the data texture. * @return {this} - This ColorRamp instance. */ fixFit: function (start, end, purgeZeroLength, encode) { var bands = this.bands; if (bands.length === 0) { return this; } if (purgeZeroLength === undefined) { purgeZeroLength = true; } if (encode === undefined) { encode = true; } if (start !== undefined) { bands[0].start = start; } if (end !== undefined) { bands[bands.length - 1].end = end; } for (var i = 0; i < bands.length - 1; i++) { var band = bands[i]; var bandNext = bands[i + 1]; bandNext.start = band.end; if (bandNext.start > bandNext.end) { bandNext.end = bandNext.start; } } if (purgeZeroLength) { this.bands = bands.filter(function (band) { return band.start < band.end; }); } if (encode) { this.encode(); } return this; }, /** * Split a band from this ramp into several bands, and insert them into the ramp. * * You can choose whether to "quantize" the bands, where each of them has * a flat color. * * @method Phaser.Display.ColorRamp#splitBand * @since 4.0.0 * @param {number | Phaser.Display.ColorBand} band - The band to split, either an index on this ramp or the band instance. The band must be on this ramp. * @param {number} steps - The number of bands to create. * @param {boolean} [quantize=false] - Whether to quantize the bands to a single color. * @param {boolean} [encode=true] - Whether to rebuild the data texture. * @return {this} This ColorRamp instance. */ splitBand: function (band, steps, quantize, encode) { if (steps === 0) { return this; } if (steps === undefined) { steps = 2; } if (encode === undefined) { encode = true; } var index = 0; if (typeof band === 'number') { index = band; band = this.bands[band]; } else { index = this.bands.indexOf(band); if (index === -1) { return this; } } if (!band) { return this; } this.bands.splice(index, 1); for (var i = 0; i < steps; i++) { var low = i / steps; var high = (i + 1) / steps; if (quantize) { low = i / (steps - 1); high = (i + 1) / (steps - 1); } var newColorStart = band.getColor(low); var newColorEnd = band.getColor(high); if (quantize) { newColorEnd = newColorStart; } var newBand = new ColorBand({ colorStart: [ newColorStart.r / 255, newColorStart.g / 255, newColorStart.b / 255, newColorStart.a / 255 ], colorEnd: [ newColorEnd.r / 255, newColorEnd.g / 255, newColorEnd.b / 255, newColorEnd.a / 255 ], start: Linear(band.start, band.end, low), end: Linear(band.start, band.end, high), middle: band.middle, interpolation: band.interpolation, colorSpace: band.colorSpace }); this.bands.splice(index++, 0, newBand); } if (encode) { this.encode(); } return this; }, /** * Get the color value at the given index within this ramp. * * If there is no band at that location, the color is transparent. * * @method Phaser.Display.ColorRamp#getColor * @param {number} index - Index of the color to get, from 0 (start) to 1 (end). * @return {Phaser.Types.Display.ColorObject} The color at that index. */ getColor: function (index) { var band; for (var i = 0; i < this.bands.length; i++) { var b = this.bands[i]; if (b.start <= index && b.end >= index) { band = b; break; } } if (!band) { return { r: 0, g: 0, b: 0, a: 0, color: 0x000000 }; } index = (index - band.start) / (band.end - band.start); return band.getColor(index); }, /** * Destroy this ColorRamp. * If it has a data texture, destroy it. * * @method Phaser.Display.ColorRamp#destroy * @since 4.0.0 */ destroy: function () { this.scene = null; if (this.dataTexture) { this.dataTexture.destroy(); } } }); module.exports = ColorRamp;