UNPKG

shaku

Version:

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

1,229 lines (1,074 loc) 38.9 kB
/** * Implement the gfx manager. * * |-- copyright and license --| * @module Shaku * @file shaku\src\gfx\gfx.js * @author Ronen Ness (ronenness@gmail.com | http://ronenness.com) * @copyright (c) 2021 Ronen Ness * @license MIT * |-- end copyright and license --| * */ 'use strict'; const IManager = require('../manager.js'); const Color = require('../utils/color.js'); const { BlendModes } = require('./blend_modes.js'); const Rectangle = require('../utils/rectangle.js'); const { Effect, SpritesEffect, SpritesEffectNoVertexColor, MsdfFontEffect, ShapesEffect, SpritesWithOutlineEffect } = require('./effects'); const TextureAsset = require('../assets/texture_asset.js'); const { TextureFilterModes } = require('./texture_filter_modes.js'); const { TextureWrapModes } = require('./texture_wrap_modes.js'); const Matrix = require('../utils/matrix.js'); const Camera = require('./camera.js'); const Sprite = require('./sprite.js'); const SpritesGroup = require('./sprites_group.js'); const Vector2 = require('../utils/vector2.js'); const FontTextureAsset = require('../assets/font_texture_asset.js'); const { TextAlignment, TextAlignments } = require('./text_alignments.js'); const Circle = require('../utils/circle.js'); const SpriteBatch = require('./draw_batches/sprite_batch.js'); const TextSpriteBatch = require('./draw_batches/text_batch'); const Vertex = require('./vertex'); const DrawBatch = require('./draw_batches/draw_batch.js'); const ShapesBatch = require('./draw_batches/shapes_batch.js'); const LinesBatch = require('./draw_batches/lines_batch.js'); const Sprites3dEffect = require('./effects/sprites_3d.js'); const SpriteBatch3D = require('./draw_batches/sprite_batch_3d.js'); const TextureAtlasAsset = require('../assets/texture_atlas_asset.js'); const Camera3D = require('./camera3d.js'); const _logger = require('../logger.js').getLogger('gfx'); let _gl = null; let _initSettings = { antialias: true, alpha: true, depth: false, premultipliedAlpha: true, desynchronized: false }; let _canvas = null; let _lastBlendMode = null; let _activeEffect = null; let _activeEffectFlags = null; let _camera = null; let _projection = null; let _fb = null; let _renderTarget = null; let _drawCallsCount = 0; let _drawQuadsCount = 0; let _drawShapePolygonsCount = 0; let _cachedRenderingRegion = {}; let _webglVersion = 0; /** * Gfx is the graphics manager. * Everything related to rendering and managing your game canvas goes here. * * To access the Graphics manager you use `Shaku.gfx`. */ class Gfx extends IManager { /** * Create the manager. */ constructor() { super(); /** * A dictionary containing all built-in effect instances. * @type {Dictionary} * @name Gfx#builtinEffects */ this.builtinEffects = {}; /** * Default texture filter to use when no texture filter is set. * @type {TextureFilterModes} * @name Gfx#defaultTextureFilter */ this.defaultTextureFilter = TextureFilterModes.Nearest; /** * Default wrap modes to use when no wrap mode is set. * @type {TextureWrapModes} * @name Gfx#TextureWrapModes */ this.defaultTextureWrapMode = TextureWrapModes.Clamp; /** * A 1x1 white texture. * @type {TextureAsset} * @name Gfx#whiteTexture */ this.whiteTexture = null; /** * Provide access to Gfx internal stuff. * @private */ this._internal = new GfxInternal(this); // set self for effect and draw batch DrawBatch._gfx = this; Effect._gfx = this; } /** * Get the init WebGL version. * @returns {Number} WebGL version number. */ get webglVersion() { return _webglVersion; } /** * Maximum number of vertices we allow when drawing lines. * @returns {Number} max vertices per lines strip. */ get maxLineSegments() { return 512; } /** * Set WebGL init flags (passed as additional params to the getContext() call). * You must call this *before* initializing *Shaku*. * * By default, *Shaku* will init WebGL context with the following flags: * - antialias: true. * - alpha: true. * - depth: false. * - premultipliedAlpha: true. * - desynchronized: false. * @example * Shaku.gfx.setContextAttributes({ antialias: true, alpha: false }); * @param {Dictionary} flags WebGL init flags to set. */ setContextAttributes(flags) { if (_gl) { throw new Error("Can't call setContextAttributes() after gfx was initialized!"); } for (let key in flags) { _initSettings[key] = flags[key]; } } /** * Set the canvas element to initialize on. * You must call this *before* initializing Shaku. Calling this will prevent Shaku from creating its own canvas. * @example * Shaku.gfx.setCanvas(document.getElementById('my-canvas')); * @param {HTMLCanvasElement} element Canvas element to initialize on. */ setCanvas(element) { if (_gl) { throw new Error("Can't call setCanvas() after gfx was initialized!"); } _canvas = element; } /** * Get the canvas element controlled by the gfx manager. * If you didn't provide your own canvas before initialization, you must add this canvas to your document after initializing `Shaku`. * @example * document.body.appendChild(Shaku.gfx.canvas); * @returns {HTMLCanvasElement} Canvas we use for rendering. */ get canvas() { return _canvas; } /** * Get the draw batch base class. * @see DrawBatch */ get DrawBatch() { return DrawBatch; } /** * Get the sprites batch class. * @see SpriteBatch */ get SpriteBatch() { return SpriteBatch; } /** * Get the 3d sprites batch class. * @see SpriteBatch3D */ get SpriteBatch3D() { return SpriteBatch3D; } /** * Get the text sprites batch class. * @see TextSpriteBatch */ get TextSpriteBatch() { return TextSpriteBatch; } /** * Get the shapes batch class. * @see ShapesBatch */ get ShapesBatch() { return ShapesBatch; } /** * Get the lines batch class. * @see LinesBatch */ get LinesBatch() { return LinesBatch; } /** * Get the Effect base class, which is required to implement custom effects. * @see Effect */ get Effect() { return Effect; } /** * Get the default sprites effect class. * @see SpritesEffect */ get SpritesEffect() { return SpritesEffect; } /** * Get the default sprites effect class that is used when vertex colors is disabled. * @see SpritesEffectNoVertexColor */ get SpritesEffectNoVertexColor() { return SpritesEffectNoVertexColor; } /** * Get the default shapes effect class that is used to draw 2d shapes. * @see ShapesEffect */ get ShapesEffect() { return ShapesEffect; } /** * Get the default 3d sprites effect class that is used to draw 3d textured quads. * @see Sprites3dEffect */ get Sprites3dEffect() { return Sprites3dEffect } /** * Get the Effect for rendering fonts with an MSDF texture. * @see MsdfFontEffect */ get MsdfFontEffect() { return MsdfFontEffect; } /** * Get the sprite class. * @see Sprite */ get Sprite() { return Sprite; } /** * Get the sprites group object. * @see SpritesGroup */ get SpritesGroup() { return SpritesGroup; } /** * Get the matrix object. * @see Matrix */ get Matrix() { return Matrix; } /** * Get the vertex object. * @see Vertex */ get Vertex() { return Vertex; } /** * Get the text alignments options. * * Left: align text to the left. * * Right: align text to the right. * * Center: align text to center. * @see TextAlignments */ get TextAlignments() { return TextAlignments; } /** * Create and return a new camera instance. * @param {Boolean} withViewport If true, will create camera with viewport value equal to canvas' size. * @returns {Camera} New camera object. */ createCamera(withViewport) { let ret = new Camera(this); if (withViewport) { ret.viewport = this.getRenderingRegion(); } return ret; } /** * Create and return a new 3D camera instance. * @param {Boolean} withViewport If true, will create camera with viewport value equal to canvas' size. * @returns {Camera3D} New camera object. */ createCamera3D(withViewport) { let ret = new Camera3D(this); if (withViewport) { ret.viewport = this.getRenderingRegion(); } return ret; } /** * Set default orthographic camera from offset. * @param {Vector2} offset Camera top-left corner. * @returns {Camera} Camera instance. */ setCameraOrthographic(offset) { let camera = this.createCamera(); camera.orthographicOffset(offset); this.applyCamera(camera); return camera; } /** * Set resolution and canvas to the max size of its parent element or screen. * If the canvas is directly under document body, it will take the max size of the page. * @param {Boolean=} limitToParent if true, will use parent element size. If false, will stretch on entire document. * @param {Boolean=} allowOddNumbers if true, will permit odd numbers, which could lead to small artefacts when drawing pixel art. If false (default) will round to even numbers. */ maximizeCanvasSize(limitToParent, allowOddNumbers) { // new width and height let width = 0; let height = 0; // parent if (limitToParent) { let parent = _canvas.parentElement; width = parent.clientWidth - _canvas.offsetLeft; height = parent.clientHeight - _canvas.offsetTop; } // entire screen else { width = window.innerWidth; height = window.innerHeight; _canvas.style.left = '0px'; _canvas.style.top = '0px'; } // make sure even numbers if (!allowOddNumbers) { if (width % 2 !== 0) { width++; } if (height % 2 !== 0) { height++; } } // if changed, set resolution if ((_canvas.width !== width) || (_canvas.height !== height)) { this.setResolution(width, height, true); } } /** * Set a render target (texture) to render on. * @example * // create render target * let renderTarget = await Shaku.assets.createRenderTarget('_my_render_target', 800, 600); * * // use render target * Shaku.gfx.setRenderTarget(renderTarget); * // .. draw some stuff here * * // reset render target and present it on screen * // note the negative height - render targets end up with flipped Y axis * Shaku.gfx.setRenderTarget(null); * Shaku.gfx.draw(renderTarget, new Shaku.utils.Vector2(screenX / 2, screenY / 2), new Shaku.utils.Vector2(screenX, -screenY)); * @param {TextureAsset|Array<TextureAsset>|null} texture Render target texture to set as render target, or null to reset and render back on canvas. Can also be array for multiple targets, which will take layouts 0-15 by their order. * @param {Boolean=} keepCamera If true, will keep current camera settings. If false (default) will reset camera. */ setRenderTarget(texture, keepCamera) { // reset cached rendering size this.#_resetCachedRenderingRegion(); // if texture is null, remove any render target if (texture === null) { _renderTarget = null; _gl.bindFramebuffer(_gl.FRAMEBUFFER, null); if (!keepCamera) { this.resetCamera(); } return; } // convert texture to array if (!Array.isArray(texture)) { texture = [texture]; } // bind the framebuffer _gl.bindFramebuffer(_gl.FRAMEBUFFER, _fb); // set render targets var drawBuffers = []; for (let index = 0; index < texture.length; ++index) { // attach the texture as the first color attachment const attachmentPoint = _gl['COLOR_ATTACHMENT' + index]; _gl.framebufferTexture2D(_gl.FRAMEBUFFER, attachmentPoint, _gl.TEXTURE_2D, texture[index]._glTexture, 0); // index 0 is the "main" render target if (index === 0) { _renderTarget = texture[index]; } // to set drawBuffers in the end drawBuffers.push(attachmentPoint); } // set draw buffers _gl.drawBuffers(drawBuffers); // unbind frame buffer //_gl.bindFramebuffer(_gl.FRAMEBUFFER, null); // reset camera if (!keepCamera) { this.resetCamera(); } } /** * Set resolution and canvas size. * @example * // set resolution and size of 800x600. * Shaku.gfx.setResolution(800, 600, true); * @param {Number} width Resolution width. * @param {Number} height Resolution height. * @param {Boolean} updateCanvasStyle If true, will also update the canvas *css* size in pixels. */ setResolution(width, height, updateCanvasStyle) { _canvas.width = width; _canvas.height = height; if (width % 2 !== 0 || height % 2 !== 0) { _logger.warn("Resolution to set is not even numbers; This might cause minor artefacts when using texture atlases. Consider using even numbers instead."); } if (updateCanvasStyle) { _canvas.style.width = width + 'px'; _canvas.style.height = height + 'px'; } _gl.viewport(0, 0, width, height); this.resetCamera(); } /** * Reset camera properties to default camera. */ resetCamera() { _camera = this.createCamera(); let size = this.getRenderingSize(); _camera.orthographic(new Rectangle(0, 0, size.x, size.y)); this.applyCamera(_camera); } /** * Set viewport, projection and other properties from a camera instance. * Changing the camera properties after calling this method will *not* update the renderer, until you call applyCamera again. * @param {Camera} camera Camera to apply. */ applyCamera(camera) { // set viewport and projection this._viewport = camera.viewport; let viewport = this.#_getRenderingRegionInternal(true); _gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height); _projection = camera.projection.clone(); // update effect if (_activeEffect) { _activeEffect.setProjectionMatrix(_projection); } // reset cached rendering region this.#_resetCachedRenderingRegion(); } /** * Get current rendering region. * @private * @param {Boolean} includeOffset If true (default) will include viewport offset, if exists. * @returns {Rectangle} Rectangle with rendering region. */ #_getRenderingRegionInternal(includeOffset) { return this._internal.getRenderingRegionInternal(includeOffset) } /** * Reset cached rendering region values. * @private */ #_resetCachedRenderingRegion() { _cachedRenderingRegion.withoutOffset = _cachedRenderingRegion.withOffset = null; } /** * Get current rendering region. * @param {Boolean} includeOffset If true (default) will include viewport offset, if exists. * @returns {Rectangle} Rectangle with rendering region. */ getRenderingRegion(includeOffset) { return this.#_getRenderingRegionInternal(includeOffset).clone(); } /** * Get current rendering size. * Unlike 'canvasSize', this takes viewport and render target into consideration. * @returns {Vector2} rendering size. */ getRenderingSize() { let region = this.#_getRenderingRegionInternal(); return region.getSize(); } /** * Get canvas size as vector. * @returns {Vector2} Canvas size. */ getCanvasSize() { return new Vector2(_canvas.width, _canvas.height); } /** * @inheritdoc * @private */ setup() { return new Promise(async (resolve, reject) => { _logger.info("Setup gfx manager.."); // if no canvas is set, create one if (!_canvas) { _canvas = document.createElement('canvas'); } // get webgl context _gl = _canvas.getContext('webgl2', _initSettings); _webglVersion = 2; // no webgl2? try webgl1 if (!_gl) { _logger.warn("Failed to init WebGL2, attempt fallback to WebGL1."); _gl = _canvas.getContext('webgl', _initSettings); _webglVersion = 1; } // no webgl at all?? if (!_gl) { _webglVersion = 0; _logger.error("Can't get WebGL context!"); return reject("Failed to get WebGL context from canvas!"); } // create default effects this.builtinEffects.Sprites = new SpritesEffect(); this.builtinEffects.SpritesWithOutline = new SpritesWithOutlineEffect(); this.builtinEffects.SpritesNoVertexColor = new SpritesEffectNoVertexColor(); this.builtinEffects.MsdfFont = new MsdfFontEffect(); this.builtinEffects.Shapes = new ShapesEffect(); this.builtinEffects.Sprites3d = new Sprites3dEffect(); // setup textures assets gl context TextureAsset._setWebGl(_gl); TextureAtlasAsset._setWebGl(_gl); // create framebuffer (used for render targets) _fb = _gl.createFramebuffer(); // create a useful single white pixel texture let whitePixelImage = new Image(); whitePixelImage.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+ip1sAAAAASUVORK5CYII='; await new Promise((resolve, reject) => { whitePixelImage.onload = resolve; }); this.whiteTexture = new TextureAsset('__runtime_white_pixel__'); this.whiteTexture.fromImage(whitePixelImage); // create default camera _camera = this.createCamera(); this.applyCamera(_camera); // success! resolve(); }); } /** * Generate a sprites group to render a string using a font texture. * Take the result of this method and use with gfx.drawGroup() to render the text. * This is what you use when you want to draw texts with `Shaku`. * Note: its best to always draw texts with *batching* enabled. * @example * // load font texture * let fontTexture = await Shaku.assets.loadFontTexture('assets/DejaVuSansMono.ttf', {fontName: 'DejaVuSansMono'}); * * // generate 'hello world!' text (note: you don't have to regenerate every frame if text didn't change) * let text1 = Shaku.gfx.buildText(fontTexture, "Hello World!"); * text1.position.set(40, 40); * * // draw text * Shaku.gfx.drawGroup(text1, true); * @param {FontTextureAsset} fontTexture Font texture asset to use. * @param {String} text Text to generate sprites for. * @param {Number=} fontSize Font size, or undefined to use font texture base size. * @param {Color|Array<Color>=} color Text sprites color. If array is set, will assign each color to different vertex, starting from top-left. * @param {TextAlignment=} alignment Text alignment. * @param {Vector2=} offset Optional starting offset. * @param {Vector2=} marginFactor Optional factor for characters and line spacing. For example value of 2,1 will make double horizontal spacing. * @returns {SpritesGroup} Sprites group containing the needed sprites to draw the given text with its properties. */ buildText(fontTexture, text, fontSize, color, alignment, offset, marginFactor) { // make sure text is a string if (typeof text !== 'string') { text = '' + text; } // sanity if (!fontTexture || !fontTexture.valid) { throw new Error("Font texture is invalid!"); } // default alignment alignment = alignment || TextAlignments.Left; // default color color = color || Color.black; // default font size fontSize = fontSize || fontTexture.fontSize; // default margin factor marginFactor = marginFactor || Vector2.one(); // get character scale factor let scale = fontSize / fontTexture.fontSize; // current character offset let position = new Vector2(0, 0); // current line characters and width let currentLineSprites = []; let lineWidth = 0; // go line down function breakLine() { // add offset to update based on alignment let offsetX = 0; switch (alignment) { case TextAlignments.Right: offsetX = -lineWidth; break; case TextAlignments.Center: offsetX = -lineWidth / 2; break; } // if we need to shift characters for alignment, do it if (offsetX != 0) { for (let i = 0; i < currentLineSprites.length; ++i) { currentLineSprites[i].position.x += offsetX; } } // update offset position.x = 0; position.y += fontTexture.lineHeight * scale * marginFactor.y; // reset line width and sprites currentLineSprites = []; lineWidth = 0; } // create group to return and build sprites let ret = new SpritesGroup(); for (let i = 0; i < text.length; ++i) { // get character and source rect let character = text[i]; let sourceRect = fontTexture.getSourceRect(character); // special case - break line if (character === '\n') { breakLine(); continue; } // calculate character size let size = new Vector2(sourceRect.width * scale, sourceRect.height * scale); // create sprite (unless its space) if (character !== ' ') { // create sprite and add to group let sprite = new Sprite(fontTexture); sprite.sourceRectangle = sourceRect; sprite.size = size; let positionOffset = fontTexture.getPositionOffset(character); if (fontTexture.isMsdfFontTextureAsset) { sprite.position.copy(position).addSelf(positionOffset.mul(scale * 0.5)); } else { sprite.position.copy(position).addSelf(positionOffset.mul(scale)); } sprite.origin.set(0.5, 0.5); if (color.isColor) { sprite.color.copy(color); } else { sprite.color = []; for (let col of color) { sprite.color.push(col.clone()); } } sprite.origin.x = 0; ret.add(sprite); // add to current line sprites currentLineSprites.push(sprite); } let moveCursorAmount = fontTexture.getXAdvance(character) * scale * marginFactor.x; // update current line width lineWidth += moveCursorAmount; // set position for next character position.x += moveCursorAmount; } // call break line on last line, to adjust alignment for last line breakLine(); // set position if (offset) { ret.position.set(offset.x, offset.y); } // return group return ret; } /** * Make the renderer canvas centered. */ centerCanvas() { let canvas = _canvas; let parent = canvas.parentElement; let pwidth = Math.min(parent.clientWidth, window.innerWidth); let pheight = Math.min(parent.clientHeight, window.innerHeight); canvas.style.left = Math.round(pwidth / 2 - canvas.clientWidth / 2) + 'px'; canvas.style.top = Math.round(pheight / 2 - canvas.clientHeight / 2) + 'px'; canvas.style.display = 'block'; canvas.style.position = 'relative'; } /** * Check if a given shape is currently in screen bounds, not taking camera into consideration. * @param {Circle|Vector|Rectangle|Line} shape Shape to check. * @returns {Boolean} True if given shape is in visible region. */ inScreen(shape) { let region = this.#_getRenderingRegionInternal(); if (shape.isCircle) { return region.collideCircle(shape); } else if (shape.isVector2) { return region.containsVector(shape); } else if (shape.isRectangle) { return region.collideRect(shape); } else if (shape.isLine) { return region.collideLine(shape); } else { throw new Error("Unknown shape type to check!"); } } /** * Make a given vector the center of the camera. * @param {Vector2} position Camera position. * @param {Boolean} useCanvasSize If true, will always use cancas size when calculating center. If false and render target is set, will use render target's size. */ centerCamera(position, useCanvasSize) { let renderSize = useCanvasSize ? this.getCanvasSize() : this.getRenderingSize(); let halfScreenSize = renderSize.mul(0.5); let centeredPos = position.sub(halfScreenSize); this.setCameraOrthographic(centeredPos); } /** * Get the blend modes enum. * * AlphaBlend * * Opaque * * Additive * * Multiply * * Subtract * * Screen * * Overlay * * Invert * * DestIn * * DestOut * * ![Blend Modes](resources/blend-modes.png) * @see BlendModes */ get BlendModes() { return BlendModes; } /** * Get the wrap modes enum. * * Clamp: when uv position exceed texture boundaries it will be clamped to the nearest border, ie repeat the edge pixels. * * Repeat: when uv position exceed texture boundaries it will wrap back to the other side. * * RepeatMirrored: when uv position exceed texture boundaries it will wrap back to the other side but also mirror the texture. * * ![Wrap Modes](resources/wrap-modes.png) * @see TextureWrapModes */ get TextureWrapModes() { return TextureWrapModes; } /** * Get texture filter modes. * * Nearest: no filtering, no mipmaps (pixelated). * * Linear: simple filtering, no mipmaps (smooth). * * NearestMipmapNearest: no filtering, sharp switching between mipmaps, * * LinearMipmapNearest: filtering, sharp switching between mipmaps. * * NearestMipmapLinear: no filtering, smooth transition between mipmaps. * * LinearMipmapLinear: filtering, smooth transition between mipmaps. * * ![Filter Modes](resources/filter-modes.png) * @see TextureFilterModes */ get TextureFilterModes() { return TextureFilterModes; } /** * Get number of actual WebGL draw calls we performed since the beginning of the frame. * @returns {Number} Number of WebGL draw calls this frame. */ get drawCallsCount() { return _drawCallsCount; } /** * Get number of textured / colored quads we drawn since the beginning of the frame. * @returns {Number} Number of quads drawn in this frame. */ get quadsDrawCount() { return _drawQuadsCount; } /** * Get number of shape polygons we drawn since the beginning of the frame. * @returns {Number} Number of shape polygons drawn in this frame. */ get shapePolygonsDrawCount() { return _drawShapePolygonsCount; } /** * Clear screen to a given color. * @example * Shaku.gfx.clear(Shaku.utils.Color.cornflowerblue); * @param {Color=} color Color to clear screen to, or black if not set. */ clear(color) { color = color || Color.black; _gl.clearColor(color.r, color.g, color.b, color.a); _gl.clear(_gl.COLOR_BUFFER_BIT | _gl.DEPTH_BUFFER_BIT); } /** * Clear depth buffer. * Only relevant when depth is used. * @param {Number=} value Value to clear depth buffer to. */ clearDepth(value) { _gl.clearDepth((value !== undefined) ? value : 1.0); } /** * @inheritdoc * @private */ startFrame() { // reset some states _lastBlendMode = null; _drawCallsCount = 0; _drawQuadsCount = 0; _drawShapePolygonsCount = 0; // reset cached rendering region this.#_resetCachedRenderingRegion(); } /** * @inheritdoc * @private */ endFrame() { } /** * @inheritdoc * @private */ destroy() { _logger.warn("Cleaning up WebGL is not supported yet!"); } } /** * Internal Gfx stuff that should not be used or exposed externally. * @private */ class GfxInternal { constructor(gfx) { this._gfx = gfx; } get gl() { return _gl; } get drawQuadsCount() { return _drawQuadsCount; } set drawQuadsCount(value) { _drawQuadsCount = value; } get drawCallsCount() { return _drawCallsCount; } set drawCallsCount(value) { _drawCallsCount = value; } get drawShapePolygonsCount() { return _drawShapePolygonsCount; } set drawShapePolygonsCount(value) { _drawShapePolygonsCount = value; } useEffect(effect, overrideFlags) { // if null, use default if (effect === null) { effect = this._gfx.builtinEffects.Sprites; } // same effect? skip if ((_activeEffect === effect) && (_activeEffectFlags === overrideFlags)) { return; } // set effect effect.setAsActive(overrideFlags); _activeEffect = effect; _activeEffectFlags = overrideFlags; // set projection matrix if (_projection) { _activeEffect.setProjectionMatrix(_projection); } } getRenderingRegionInternal(includeOffset) { // cached with offset if (includeOffset && _cachedRenderingRegion.withOffset) { return _cachedRenderingRegion.withOffset; } // cached without offset if (!includeOffset && _cachedRenderingRegion.withoutOffset) { return _cachedRenderingRegion.withoutOffset; } // if we got viewport.. if (this._gfx._viewport) { // get region from viewport let ret = this._gfx._viewport.clone(); // if without offset, remove it if (includeOffset === false) { ret.x = ret.y = 0; _cachedRenderingRegion.withoutOffset = ret; return ret; } // else, include offset else { _cachedRenderingRegion.withOffset = ret; return ret; } } // if we don't have viewport.. let ret = new Rectangle(0, 0, (_renderTarget || _canvas).width, (_renderTarget || _canvas).height); _cachedRenderingRegion.withoutOffset = _cachedRenderingRegion.withOffset = ret; return ret; } setTextureFilter(filter) { if (!TextureFilterModes._values.has(filter)) { throw new Error("Invalid texture filter mode! Please pick a value from 'TextureFilterModes'."); } let glMode = _gl[filter]; _gl.texParameteri(_gl.TEXTURE_2D, _gl.TEXTURE_MIN_FILTER, glMode); _gl.texParameteri(_gl.TEXTURE_2D, _gl.TEXTURE_MAG_FILTER, glMode); } setTextureWrapMode(wrapX, wrapY) { if (wrapY === undefined) { wrapY = wrapX; } if (!TextureWrapModes._values.has(wrapX)) { throw new Error("Invalid texture wrap mode! Please pick a value from 'TextureWrapModes'."); } if (!TextureWrapModes._values.has(wrapY)) { throw new Error("Invalid texture wrap mode! Please pick a value from 'TextureWrapModes'."); } _gl.texParameteri(_gl.TEXTURE_2D, _gl.TEXTURE_WRAP_S, _gl[wrapX]); _gl.texParameteri(_gl.TEXTURE_2D, _gl.TEXTURE_WRAP_T, _gl[wrapY]); } setActiveTexture(texture) { if (_activeEffect.setTexture(texture)) { this.setTextureFilter(texture.filter || this._gfx.defaultTextureFilter); this.setTextureWrapMode(texture.wrapMode || this._gfx.defaultTextureWrapMode); } } setBlendMode(blendMode) { if (_lastBlendMode !== blendMode) { // get gl context and set defaults var gl = _gl; switch (blendMode) { case BlendModes.AlphaBlend: gl.enable(gl.BLEND); gl.blendEquation(gl.FUNC_ADD); gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); break; case BlendModes.Opaque: gl.disable(gl.BLEND); break; case BlendModes.Additive: gl.enable(gl.BLEND); gl.blendEquation(gl.FUNC_ADD); gl.blendFunc(gl.ONE, gl.ONE); break; case BlendModes.Multiply: gl.enable(gl.BLEND); gl.blendEquation(gl.FUNC_ADD); gl.blendFuncSeparate(gl.DST_COLOR, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA); break; case BlendModes.Screen: gl.enable(gl.BLEND); gl.blendEquation(gl.FUNC_ADD); gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_COLOR, gl.ONE, gl.ONE_MINUS_SRC_ALPHA); break; case BlendModes.Subtract: gl.enable(gl.BLEND); gl.blendEquation(gl.FUNC_ADD); gl.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE); gl.blendEquationSeparate(gl.FUNC_REVERSE_SUBTRACT, gl.FUNC_ADD); break; case BlendModes.Invert: gl.enable(gl.BLEND); gl.blendEquation(gl.FUNC_ADD); gl.blendFunc(gl.ONE_MINUS_DST_COLOR, gl.ZERO); gl.blendFuncSeparate(gl.ONE_MINUS_DST_COLOR, gl.ZERO, gl.ONE, gl.ONE_MINUS_SRC_ALPHA); break; case BlendModes.Overlay: gl.enable(gl.BLEND); if (gl.MAX) { gl.blendEquation(gl.MAX); gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); } else { gl.blendEquation(gl.FUNC_ADD); gl.blendFunc(gl.ONE, gl.ONE); } break; case BlendModes.Darken: gl.enable(gl.BLEND); gl.blendEquation(gl.MIN); gl.blendFuncSeparate(gl.DST_COLOR, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA); break; case BlendModes.DestIn: gl.enable(gl.BLEND); gl.blendEquation(gl.FUNC_ADD); gl.blendFunc(gl.ZERO, gl.SRC_ALPHA); break; case BlendModes.DestOut: gl.enable(gl.BLEND); gl.blendEquation(gl.FUNC_ADD); gl.blendFunc(gl.ZERO, gl.ONE_MINUS_SRC_ALPHA); // can also use: gl.blendFunc(gl.ONE_MINUS_DST_COLOR, gl.ONE_MINUS_SRC_COLOR); break; default: throw new Error(`Unknown blend mode '${blendMode}'!`); } // store last blend mode _lastBlendMode = blendMode; } } } // export main object module.exports = new Gfx();