// @flow /** * This is a declaration of an extension for GDevelop 5. * * ℹ️ Changes in this file are watched and automatically imported if the editor * is running. You can also manually run `node import-GDJS-Runtime.js` (in newIDE/app/scripts). * * The file must be named "JsExtension.js", otherwise GDevelop won't load it. * ⚠️ If you make a change and the extension is not loaded, open the developer console * and search for any errors. * * More information on https://github.com/4ian/GDevelop/blob/master/newIDE/README-extensions.md */ /*:: // Import types to allow Flow to do static type checking on this file. // Extensions declaration are typed using Flow (like the editor), but the files // for the game engine are checked with TypeScript annotations. import { type ObjectsRenderingService, type ObjectsEditorService } from '../JsExtensionTypes.flow.js' */ module.exports = { createExtension: function ( _ /*: (string) => string */, gd /*: libGDevelop */ ) { const extension = new gd.PlatformExtension(); extension .setExtensionInformation( 'BitmapText', _('Bitmap Text'), _( 'Displays a text using a "Bitmap Font" (an image representing characters). This is more performant than a traditional Text object and it allows for complete control on the characters aesthetic.' ), 'Aurélien Vivet', 'Open source (MIT License)' ) .setExtensionHelpPath('/objects/bitmap_text'); const bitmapTextObject = new gd.ObjectJsImplementation(); // $FlowExpectedError bitmapTextObject.updateProperty = function ( objectContent, propertyName, newValue ) { if (propertyName in objectContent) { if (typeof objectContent[propertyName] === 'boolean') objectContent[propertyName] = newValue === '1'; else if (typeof objectContent[propertyName] === 'number') objectContent[propertyName] = parseFloat(newValue); else objectContent[propertyName] = newValue; return true; } return false; }; // $FlowExpectedError bitmapTextObject.getProperties = function (objectContent) { const objectProperties = new gd.MapStringPropertyDescriptor(); objectProperties .getOrCreate('text') .setValue(objectContent.text) .setType('textarea') .setLabel(_('Text')); objectProperties .getOrCreate('opacity') .setValue(objectContent.opacity.toString()) .setType('number') .setLabel(_('Opacity (0-255)')) .setGroup(_('Appearance')); objectProperties .getOrCreate('align') .setValue(objectContent.align) .setType('choice') .addExtraInfo('left') .addExtraInfo('center') .addExtraInfo('right') .setLabel(_('Alignment, when multiple lines are displayed')) .setGroup(_('Appearance')); objectProperties .getOrCreate('bitmapFontResourceName') .setValue(objectContent.bitmapFontResourceName) .setType('resource') .addExtraInfo('bitmapFont') //fnt or xml files .setLabel(_('Bitmap Font')) .setGroup(_("Font")); objectProperties .getOrCreate('textureAtlasResourceName') .setValue(objectContent.textureAtlasResourceName) .setType('resource') .addExtraInfo('image') .setLabel(_('Bitmap atlas image')) .setGroup(_("Font")); objectProperties .getOrCreate('scale') .setValue(objectContent.scale.toString()) .setType('number') .setLabel(_('Text scale')) .setGroup(_('Appearance')); objectProperties .getOrCreate('tint') .setValue(objectContent.tint) .setType('color') .setLabel(_('Font tint')) .setGroup(_("Font")); objectProperties .getOrCreate('wordWrap') .setValue(objectContent.wordWrap ? 'true' : 'false') .setType('boolean') .setLabel(_('Word wrapping')) .setGroup(_('Appearance')); return objectProperties; }; bitmapTextObject.setRawJSONContent( JSON.stringify({ text: 'This text use the default bitmap font.\nUse a custom Bitmap Font to create your own texts.', opacity: 255, scale: 1, fontSize: 20, tint: '255;255;255', bitmapFontResourceName: '', textureAtlasResourceName: '', align: 'left', wordWrap: true, }) ); // $FlowExpectedError bitmapTextObject.updateInitialInstanceProperty = function ( objectContent, instance, propertyName, newValue, project, layout ) { return false; }; // $FlowExpectedError bitmapTextObject.getInitialInstanceProperties = function ( content, instance, project, layout ) { var instanceProperties = new gd.MapStringPropertyDescriptor(); return instanceProperties; }; const object = extension .addObject( 'BitmapTextObject', _('Bitmap Text'), _( 'Displays a text using a "Bitmap Font" (an image representing characters). This is more performant than a traditional Text object and it allows for complete control on the characters aesthetic.' ), 'JsPlatform/Extensions/bitmapfont32.png', bitmapTextObject ) .setIncludeFile('Extensions/BitmapText/bitmaptextruntimeobject.js') .addIncludeFile( 'Extensions/BitmapText/bitmaptextruntimeobject-pixi-renderer.js' ) .setCategoryFullName(_('Texts')); object .addExpressionAndConditionAndAction( 'string', 'Text', _('Text'), _('the text'), _('the text'), '', 'res/conditions/text24.png' ) .addParameter('object', _('Bitmap text'), 'BitmapTextObject', false) .useStandardParameters('string') .setFunctionName('setText') .setGetter('getText'); object .addExpressionAndConditionAndAction( 'number', 'Opacity', _('Opacity'), _('the opacity, between 0 (fully transparent) and 255 (opaque)'), _('the opacity'), '', 'res/conditions/opacity24.png' ) .addParameter('object', _('Bitmap text'), 'BitmapTextObject', false) .useStandardParameters('number') .setFunctionName('setOpacity') .setGetter('getOpacity'); object .addExpressionAndCondition( 'number', 'FontSize', _('Font size'), _('the font size, defined in the Bitmap Font'), _('the font size'), '', 'res/conditions/characterSize24.png' ) .addParameter('object', _('Bitmap text'), 'BitmapTextObject', false) .useStandardParameters('number') .setFunctionName('getFontSize'); object .addExpressionAndConditionAndAction( 'number', 'Scale', _('Scale'), _('the scale (1 by default)'), _('the scale'), '', 'res/actions/scale24.png' ) .addParameter('object', _('Bitmap text'), 'BitmapTextObject', false) .useStandardParameters('number') .setFunctionName('setScale') .setGetter('getScale'); object .addExpressionAndCondition( 'string', 'FontName', _('Font name'), _('the font name (defined in the Bitmap font)'), _('the font name'), '', 'res/conditions/font24.png' ) .addParameter('object', _('Bitmap text'), 'BitmapTextObject', false) .useStandardParameters('string') .setFunctionName('getFontName'); object .addAction( 'SetTint', _('Tint'), _('Set the tint of the Bitmap Text object.'), _('Set tint of _PARAM0_ to _PARAM1_'), '', 'res/actions/color24.png', 'res/actions/color.png' ) .addParameter('object', _('Bitmap text'), 'BitmapTextObject', false) .addParameter('color', _('Color'), '', false) .getCodeExtraInformation() .setFunctionName('setTint'); object .addAction( 'SetBitmapFontAndTextureAtlasResourceName', _('Bitmap files resources'), _('Change the Bitmap Font and/or the atlas image used by the object.'), _( 'Set the bitmap font of _PARAM0_ to _PARAM1_ and the atlas to _PARAM2_' ), '', 'res/actions/font24.png', 'res/actions/font.png' ) .addParameter('object', _('Bitmap text'), 'BitmapTextObject', false) .addParameter( 'bitmapFontResource', _('Bitmap font resource name'), '', false ) .setParameterLongDescription( 'The resource name of the font file, without quotes.' ) .addParameter('string', _('Texture atlas resource name'), '', false) .setParameterLongDescription( 'The resource name of the image exported with the font, with quotes.' ) .getCodeExtraInformation() .setFunctionName('setBitmapFontAndTextureAtlasResourceName'); object .addExpressionAndCondition( 'string', 'Alignment', _('Alignment'), _('the text alignment'), _('the text alignment'), '', 'res/actions/textAlign24.png' ) .addParameter('object', _('Bitmap text'), 'BitmapTextObject', false) .useStandardParameters('string') .setFunctionName('getAlignment'); object .addAction( 'SetAlignment', _('Alignment'), _('Change the alignment of a Bitmap text object.'), _('Set the alignment of _PARAM0_ to _PARAM1_'), '', 'res/actions/textAlign24.png', 'res/actions/textAlign.png' ) .addParameter('object', _('Bitmap text'), 'BitmapTextObject', false) .addParameter( 'stringWithSelector', _('Alignment'), '["left", "center", "right"]', false ) .getCodeExtraInformation() .setFunctionName('setAlignment'); object .addCondition( 'WordWrap', _('Word wrap'), _('Check if word wrap is enabled.'), _('_PARAM0_ word wrap is enabled'), '', 'res/conditions/wordWrap24.png', 'res/conditions/wordWrap.png' ) .addParameter('object', _('Bitmap text'), 'BitmapTextObject', false) .getCodeExtraInformation() .setFunctionName('getWordWrap'); object .addAction( 'SetWordWrap', _('Word wrap'), _('De/activate word wrapping.'), _('Activate word wrap of _PARAM0_: _PARAM1_'), '', 'res/actions/wordWrap24.png', 'res/actions/wordWrap.png' ) .addParameter('object', _('Bitmap text'), 'BitmapTextObject', false) .addParameter('yesorno', _('Activate word wrap'), '', false) .getCodeExtraInformation() .setFunctionName('setWordWrap'); object .addExpressionAndConditionAndAction( 'number', 'WrappingWidth', _('Wrapping width'), _('the width, in pixels, after which the text is wrapped on next line'), _('the wrapping width'), '', 'res/actions/scaleWidth24.png' ) .addParameter('object', _('Bitmap text'), 'BitmapTextObject', false) .useStandardParameters('number') .setFunctionName('setWrappingWidth') .setGetter('getWrappingWidth'); return extension; }, /** * You can optionally add sanity tests that will check the basic working * of your extension behaviors/objects by instanciating behaviors/objects * and setting the property to a given value. * * If you don't have any tests, you can simply return an empty array. * * But it is recommended to create tests for the behaviors/objects properties you created * to avoid mistakes. */ runExtensionSanityTests: function ( gd /*: libGDevelop */, extension /*: gdPlatformExtension*/ ) { return []; }, /** * Register editors for objects. * * ℹ️ Run `node import-GDJS-Runtime.js` (in newIDE/app/scripts) if you make any change. */ registerEditorConfigurations: function ( objectsEditorService /*: ObjectsEditorService */ ) { objectsEditorService.registerEditorConfiguration( 'BitmapText::BitmapTextObject', objectsEditorService.getDefaultObjectJsImplementationPropertiesEditor({ helpPagePath: '/objects/bitmap_text', }) ); }, /** * Register renderers for instance of objects on the scene editor. * * ℹ️ Run `node import-GDJS-Runtime.js` (in newIDE/app/scripts) if you make any change. */ registerInstanceRenderers: function ( objectsRenderingService /*: ObjectsRenderingService */ ) { const RenderedInstance = objectsRenderingService.RenderedInstance; const PIXI = objectsRenderingService.PIXI; /** The bitmap font used in case another font can't be loaded. */ let defaultBitmapFont = null; const defaultBitmapFontInstallKey = 'GD-DEFAULT-BITMAP-FONT'; /** * Map counting the number of "reference" to a bitmap font. This is useful * to uninstall a bitmap font when not used anymore. */ const bitmapFontUsageCount = {}; /** * We patch the installed font to use a name that is unique for each font data and texture, * to avoid conflicts between different font files using the same font name (by default, the * font name used by Pixi is the one inside the font data, but this name is not necessarily unique. * For example, 2 resources can use the same font, or we can have multiple objects with the same * font data and different textures). */ const patchBitmapFont = (bitmapFont, bitmapFontInstallKey) => { const defaultName = bitmapFont.font; bitmapFont.font = bitmapFontInstallKey; PIXI.BitmapFont.available[bitmapFontInstallKey] = bitmapFont; delete PIXI.BitmapFont.available[defaultName]; return PIXI.BitmapFont.available[bitmapFontInstallKey]; }; /** * Return a default bitmap font to be used in case another font can't be loaded. */ const getDefaultBitmapFont = () => { if (defaultBitmapFont) return defaultBitmapFont; const defaultBitmapFontStyle = new PIXI.TextStyle({ fontFamily: 'Arial', fontSize: 20, padding: 5, align: 'left', fill: '#ffffff', wordWrap: true, lineHeight: 20, }); defaultBitmapFont = patchBitmapFont( PIXI.BitmapFont.from( defaultBitmapFontStyle.fontFamily, defaultBitmapFontStyle, { chars: [ [' ', '~'], // All the printable ASCII characters ], } ), defaultBitmapFontInstallKey ); return defaultBitmapFont; }; /** * Given a bitmap font resource name and a texture atlas resource name, returns the PIXI.BitmapFont * for it. * The font must be released with `releaseBitmapFont` when not used anymore - so that it can be removed * from memory when not used by any instance. * * @param pixiResourcesLoader * @param project * @param bitmapFontResourceName * @param textureAtlasResourceName */ const obtainBitmapFont = ( pixiResourcesLoader, project, bitmapFontResourceName, textureAtlasResourceName ) => { const bitmapFontInstallKey = bitmapFontResourceName + '@' + textureAtlasResourceName; if (PIXI.BitmapFont.available[bitmapFontInstallKey]) { // Return the existing BitmapFont that is already in memory and already installed. bitmapFontUsageCount[bitmapFontInstallKey] = (bitmapFontUsageCount[bitmapFontInstallKey] || 0) + 1; return Promise.resolve(PIXI.BitmapFont.available[bitmapFontInstallKey]); } // Get the atlas texture, the bitmap font data and install the font: const texture = pixiResourcesLoader.getPIXITexture( project, textureAtlasResourceName ); const loadBitmapFont = () => pixiResourcesLoader .getBitmapFontData(project, bitmapFontResourceName) .then((fontData) => { if (!texture.valid) throw new Error( 'Tried to install a BitmapFont with an invalid texture.' ); const bitmapFont = patchBitmapFont( PIXI.BitmapFont.install(fontData, texture), bitmapFontInstallKey ); bitmapFontUsageCount[bitmapFontInstallKey] = (bitmapFontUsageCount[bitmapFontInstallKey] || 0) + 1; return bitmapFont; }) .catch((err) => { console.warn('Unable to load font data:', err); console.info( 'Is the texture atlas properly set for the Bitmap Text object? The default font will be used instead.' ); const bitmapFont = getDefaultBitmapFont(); return bitmapFont; }); if (!texture.valid) { // Post pone texture update if texture is not loaded. // (otherwise, the bitmap font would not get updated when the // texture is loaded and udpated). return new Promise((resolve) => { texture.once('update', () => { resolve(loadBitmapFont()); }); }); } else { // We're ready to load the bitmap font now, as the texture // is already loaded. return loadBitmapFont(); } }; /** * When a font is not used by an object anymore (object destroyed or font changed), * call this function to decrease the internal count of objects using the font. * * Fonts are unloaded when not used anymore. */ const releaseBitmapFont = (bitmapFontInstallKey) => { if (bitmapFontInstallKey === defaultBitmapFontInstallKey) { // Never uninstall the default bitmap font. return; } if (!bitmapFontUsageCount[bitmapFontInstallKey]) { console.error( 'BitmapFont with name ' + bitmapFontInstallKey + ' was tried to be released but was never marked as used.' ); return; } bitmapFontUsageCount[bitmapFontInstallKey]--; if (bitmapFontUsageCount[bitmapFontInstallKey] === 0) { PIXI.BitmapFont.uninstall(bitmapFontInstallKey); console.info( 'Uninstalled BitmapFont "' + bitmapFontInstallKey + '" from memory.' ); } }; /** * Renderer for instances of BitmapText inside the IDE. * * @extends RenderedBitmapTextInstance * @class RenderedBitmapTextInstance * @constructor */ function RenderedBitmapTextInstance( project, layout, instance, associatedObject, pixiContainer, pixiResourcesLoader ) { RenderedInstance.call( this, project, layout, instance, associatedObject, pixiContainer, pixiResourcesLoader ); // We'll track changes of the font to trigger the loading of the new font. this._currentBitmapFontResourceName = ''; this._currentTextureAtlasResourceName = ''; this._pixiObject = new PIXI.BitmapText('', { // Use a default font. The proper font will be loaded in `update` method. fontName: getDefaultBitmapFont().font, }); this._pixiObject.anchor.x = 0.5; this._pixiObject.anchor.y = 0.5; this._pixiContainer.addChild(this._pixiObject); this.update(); } RenderedBitmapTextInstance.prototype = Object.create( RenderedInstance.prototype ); /** * Return the path to the thumbnail of the specified object. */ RenderedBitmapTextInstance.getThumbnail = function ( project, resourcesLoader, object ) { return 'JsPlatform/Extensions/bitmapfont24.png'; }; // This is called to update the PIXI object on the scene editor RenderedBitmapTextInstance.prototype.update = function () { const properties = this._associatedObject.getProperties(); // Update the rendered text properties (note: Pixi is only // applying changes if there were changed). const rawText = properties.get('text').getValue(); this._pixiObject.text = rawText; const opacity = properties.get('opacity').getValue(); this._pixiObject.alpha = opacity / 255; const align = properties.get('align').getValue(); this._pixiObject.align = align; const color = properties.get('tint').getValue(); this._pixiObject.tint = objectsRenderingService.rgbOrHexToHexNumber(color); const scale = properties.get('scale').getValue() || 1; this._pixiObject.scale.set(scale); // Track the changes in font to load the new requested font. const bitmapFontResourceName = properties .get('bitmapFontResourceName') .getValue(); const textureAtlasResourceName = properties .get('textureAtlasResourceName') .getValue(); if ( this._currentBitmapFontResourceName !== bitmapFontResourceName || this._currentTextureAtlasResourceName !== textureAtlasResourceName ) { // Release the old font (if it was installed). releaseBitmapFont(this._pixiObject.fontName); // Temporarily go back to the default font, as the PIXI.BitmapText // object does not support being displayed with a font not installed at all. // It will be replaced as soon as the proper font is loaded. this._pixiObject.fontName = getDefaultBitmapFont().font; this._currentBitmapFontResourceName = bitmapFontResourceName; this._currentTextureAtlasResourceName = textureAtlasResourceName; obtainBitmapFont( this._pixiResourcesLoader, this._project, this._currentBitmapFontResourceName, this._currentTextureAtlasResourceName ).then((bitmapFont) => { this._pixiObject.fontName = bitmapFont.font; this._pixiObject.fontSize = bitmapFont.size; this._pixiObject.dirty = true; }); } // Set up the wrapping width if enabled. const wordWrap = properties.get('wordWrap').getValue() === 'true'; if (wordWrap && this._instance.hasCustomSize()) { this._pixiObject.maxWidth = this._instance.getCustomWidth() / this._pixiObject.scale.x; this._pixiObject.dirty = true; } else { this._pixiObject.maxWidth = 0; this._pixiObject.dirty = true; } this._pixiObject.position.x = this._instance.getX() + (this._pixiObject.textWidth * scale) / 2; this._pixiObject.position.y = this._instance.getY() + (this._pixiObject.textHeight * scale) / 2; this._pixiObject.rotation = RenderedInstance.toRad( this._instance.getAngle() ); }; RenderedBitmapTextInstance.prototype.onRemovedFromScene = function () { RenderedInstance.prototype.onRemovedFromScene.call(this); releaseBitmapFont(this._pixiObject.fontName); this._pixiObject.destroy(); }; /** * Return the width of the instance, when it's not resized. */ RenderedBitmapTextInstance.prototype.getDefaultWidth = function () { return this._pixiObject.width; }; /** * Return the height of the instance, when it's not resized. */ RenderedBitmapTextInstance.prototype.getDefaultHeight = function () { return this._pixiObject.height; }; objectsRenderingService.registerInstanceRenderer( 'BitmapText::BitmapTextObject', RenderedBitmapTextInstance ); }, };