diff --git a/Extensions/BBText/bbtextruntimeobject-pixi-renderer.ts b/Extensions/BBText/bbtextruntimeobject-pixi-renderer.ts index 46c835dfcea3..498e71124f98 100644 --- a/Extensions/BBText/bbtextruntimeobject-pixi-renderer.ts +++ b/Extensions/BBText/bbtextruntimeobject-pixi-renderer.ts @@ -125,28 +125,27 @@ namespace gdjs { } updatePosition(): void { - if (this._object.isWrapping() && this._pixiObject.width !== 0) { - const alignmentX = - this._object._textAlign === 'right' - ? 1 - : this._object._textAlign === 'center' - ? 0.5 - : 0; - - const width = this._object.getWrappingWidth(); - - // A vector from the custom size center to the renderer center. - const centerToCenterX = - (width - this._pixiObject.width) * (alignmentX - 0.5); - - this._pixiObject.position.x = this._object.x + width / 2; - this._pixiObject.anchor.x = - 0.5 - centerToCenterX / this._pixiObject.width; - } else { - this._pixiObject.position.x = - this._object.x + this._pixiObject.width / 2; - this._pixiObject.anchor.x = 0.5; - } + const alignmentX = + this._object._textAlign === 'right' + ? 1 + : this._object._textAlign === 'center' + ? 0.5 + : 0; + const objectWidth = this._object.isWrapping() + ? this._object.getWrappingWidth() + : this._pixiObject.width; + const textLeftOffset = this._object.isWrapping() + ? (objectWidth - this._pixiObject.width) * alignmentX + : 0; + const centerX = this._object._rotationCenter + ? this._object._rotationCenter[0] + : objectWidth / 2; + + this._pixiObject.position.x = this._object.getDrawableX() + centerX; + this._pixiObject.anchor.x = + this._pixiObject.width !== 0 + ? (centerX - textLeftOffset) / this._pixiObject.width + : 0; const alignmentY = this._object._verticalTextAlignment === 'bottom' @@ -154,9 +153,14 @@ namespace gdjs { : this._object._verticalTextAlignment === 'center' ? 0.5 : 0; - this._pixiObject.position.y = - this._object.y + this._pixiObject.height * (0.5 - alignmentY); - this._pixiObject.anchor.y = 0.5; + const textTopOffset = this._pixiObject.height * alignmentY; + const centerY = this._object._rotationCenter + ? this._object._rotationCenter[1] + : this._pixiObject.height / 2; + + this._pixiObject.position.y = this._object.y + centerY - textTopOffset; + this._pixiObject.anchor.y = + this._pixiObject.height !== 0 ? centerY / this._pixiObject.height : 0; } updateAngle(): void { diff --git a/Extensions/BBText/bbtextruntimeobject.ts b/Extensions/BBText/bbtextruntimeobject.ts index 05437b713c38..4b50c98a1440 100644 --- a/Extensions/BBText/bbtextruntimeobject.ts +++ b/Extensions/BBText/bbtextruntimeobject.ts @@ -76,6 +76,7 @@ namespace gdjs { _verticalTextAlignment: string; _renderer: gdjs.BBTextRuntimeObjectRenderer; + _rotationCenter: FloatPoint | null = null; // While this should rather be exposed as a property for all objects, honor the "visible" // property that is specific to this object. @@ -241,6 +242,9 @@ namespace gdjs { * Set the markup text to display. */ setBBText(text: string): void { + if (this.angle !== 0) { + this._lockRotationCenter(); + } this._text = text; this._renderer.updateText(); this.invalidateHitboxes(); @@ -336,6 +340,11 @@ namespace gdjs { override setAngle(angle: float): void { super.setAngle(angle); + if (angle === 0 && this._rotationCenter) { + this._rotationCenter = null; + this._renderer.updatePosition(); + this.invalidateHitboxes(); + } this._renderer.updateAngle(); } @@ -414,6 +423,24 @@ namespace gdjs { : 0) ); } + + private _lockRotationCenter() { + if (this._rotationCenter) return; + + this._rotationCenter = [this.getCenterX(), this.getCenterY()]; + } + + override getCenterX(): float { + return this._rotationCenter + ? this._rotationCenter[0] + : super.getCenterX(); + } + + override getCenterY(): float { + return this._rotationCenter + ? this._rotationCenter[1] + : super.getCenterY(); + } } // @ts-ignore gdjs.registerObject('BBText::BBText', gdjs.BBTextRuntimeObject); diff --git a/Extensions/BitmapText/bitmaptextruntimeobject-pixi-renderer.ts b/Extensions/BitmapText/bitmaptextruntimeobject-pixi-renderer.ts index db056616b409..f3b1c4e0b2b9 100644 --- a/Extensions/BitmapText/bitmaptextruntimeobject-pixi-renderer.ts +++ b/Extensions/BitmapText/bitmaptextruntimeobject-pixi-renderer.ts @@ -156,26 +156,26 @@ namespace gdjs { } updatePosition(): void { - if (this._object.isWrapping() && this.getWidth() !== 0) { - const alignmentX = - this._object._textAlign === 'right' - ? 1 - : this._object._textAlign === 'center' - ? 0.5 - : 0; - - const width = this._object.getWrappingWidth(); - const renderedWidth = this.getWidth(); - - // A vector from the custom size center to the renderer center. - const centerToCenterX = (width - renderedWidth) * (alignmentX - 0.5); - - this._pixiObject.position.x = this._object.x + width / 2; - this._pixiObject.anchor.x = 0.5 - centerToCenterX / renderedWidth; - } else { - this._pixiObject.position.x = this._object.x + this.getWidth() / 2; - this._pixiObject.anchor.x = 0.5; - } + const alignmentX = + this._object._textAlign === 'right' + ? 1 + : this._object._textAlign === 'center' + ? 0.5 + : 0; + const renderedWidth = this.getWidth(); + const objectWidth = this._object.isWrapping() + ? this._object.getWrappingWidth() + : renderedWidth; + const textLeftOffset = this._object.isWrapping() + ? (objectWidth - renderedWidth) * alignmentX + : 0; + const centerX = this._object._rotationCenter + ? this._object._rotationCenter[0] + : objectWidth / 2; + + this._pixiObject.position.x = this._object.getDrawableX() + centerX; + this._pixiObject.anchor.x = + renderedWidth !== 0 ? (centerX - textLeftOffset) / renderedWidth : 0; const alignmentY = this._object._verticalTextAlignment === 'bottom' @@ -183,9 +183,15 @@ namespace gdjs { : this._object._verticalTextAlignment === 'center' ? 0.5 : 0; - this._pixiObject.position.y = - this._object.y + this.getHeight() * (0.5 - alignmentY); - this._pixiObject.anchor.y = 0.5; + const renderedHeight = this.getHeight(); + const textTopOffset = renderedHeight * alignmentY; + const centerY = this._object._rotationCenter + ? this._object._rotationCenter[1] + : renderedHeight / 2; + + this._pixiObject.position.y = this._object.y + centerY - textTopOffset; + this._pixiObject.anchor.y = + renderedHeight !== 0 ? centerY / renderedHeight : 0; } updateAngle(): void { diff --git a/Extensions/BitmapText/bitmaptextruntimeobject.ts b/Extensions/BitmapText/bitmaptextruntimeobject.ts index 9fdf1ce01e11..81057b659a65 100644 --- a/Extensions/BitmapText/bitmaptextruntimeobject.ts +++ b/Extensions/BitmapText/bitmaptextruntimeobject.ts @@ -81,6 +81,7 @@ namespace gdjs { _verticalTextAlignment: string; _renderer: gdjs.BitmapTextRuntimeObjectPixiRenderer; + _rotationCenter: FloatPoint | null = null; /** * @param instanceContainer The container the object belongs to. @@ -251,6 +252,9 @@ namespace gdjs { * Set the text to display. */ setText(text: string): void { + if (this.angle !== 0) { + this._lockRotationCenter(); + } this._text = text; this._renderer.updateTextContent(); this.invalidateHitboxes(); @@ -385,6 +389,11 @@ namespace gdjs { override setAngle(angle: float): void { super.setAngle(angle); + if (angle === 0 && this._rotationCenter) { + this._rotationCenter = null; + this._renderer.updatePosition(); + this.invalidateHitboxes(); + } this._renderer.updateAngle(); } @@ -459,6 +468,24 @@ namespace gdjs { : 0) ); } + + private _lockRotationCenter() { + if (this._rotationCenter) return; + + this._rotationCenter = [this.getCenterX(), this.getCenterY()]; + } + + override getCenterX(): float { + return this._rotationCenter + ? this._rotationCenter[0] + : super.getCenterX(); + } + + override getCenterY(): float { + return this._rotationCenter + ? this._rotationCenter[1] + : super.getCenterY(); + } } gdjs.registerObject( 'BitmapText::BitmapTextObject', diff --git a/Extensions/TextObject/tests/textruntimeobject.pixiruntimegamewithassets.spec.js b/Extensions/TextObject/tests/textruntimeobject.pixiruntimegamewithassets.spec.js new file mode 100644 index 000000000000..0e5ee2345923 --- /dev/null +++ b/Extensions/TextObject/tests/textruntimeobject.pixiruntimegamewithassets.spec.js @@ -0,0 +1,128 @@ +// @ts-check + +describe('gdjs.TextRuntimeObject (using a PixiJS RuntimeGame)', function () { + /** + * @param {gdjs.RuntimeScene} runtimeScene + */ + const loadScene = (runtimeScene) => { + runtimeScene.loadFromScene({ + sceneData: { + layers: [ + { + name: '', + visibility: true, + effects: [], + cameras: [], + ambientLightColorR: 0, + ambientLightColorG: 0, + ambientLightColorB: 0, + isLightingLayer: false, + followBaseLayerCamera: true, + }, + ], + r: 0, + v: 0, + b: 0, + mangledName: 'Scene1', + name: 'Scene1', + stopSoundsOnStartup: false, + title: '', + behaviorsSharedData: [], + objects: [], + objectsGroups: [], + instances: [], + variables: [], + usedResources: [], + uiSettings: { + grid: false, + gridType: 'rectangular', + gridWidth: 10, + gridHeight: 10, + gridDepth: 10, + gridOffsetX: 0, + gridOffsetY: 0, + gridOffsetZ: 0, + gridColor: 0, + gridAlpha: 1, + snap: false, + }, + }, + usedExtensionsWithVariablesData: [], + }); + }; + + /** + * @param {string} text + * @returns {gdjs.TextObjectData} + */ + const makeTextObjectData = (text) => ({ + name: 'score', + type: 'TextObject::Text', + variables: [], + behaviors: [], + effects: [], + content: { + characterSize: 40, + font: '', + bold: false, + italic: false, + underlined: false, + color: '255;255;255', + text, + textAlignment: 'left', + verticalTextAlignment: 'top', + lineHeight: 0, + isOutlineEnabled: false, + outlineThickness: 0, + outlineColor: '0;0;0', + isShadowEnabled: false, + shadowColor: '0;0;0', + shadowOpacity: 255, + shadowDistance: 0, + shadowAngle: 90, + shadowBlurRadius: 0, + }, + }); + + const makeTextRuntimeObject = async () => { + const runtimeGame = await gdjs.getPixiRuntimeGameWithAssets(); + const runtimeScene = new gdjs.RuntimeScene(runtimeGame); + loadScene(runtimeScene); + + const gameContainer = document.createElement('div'); + runtimeGame.getRenderer().createStandardCanvas(gameContainer); + + const object = new gdjs.TextRuntimeObject( + runtimeScene, + makeTextObjectData('9') + ); + runtimeScene.addObject(object); + object.setPosition(200, 200); + object.setAngle(90); + runtimeScene.renderAndStep(1000 / 60); + + return { runtimeScene, object }; + }; + + it('keeps the same rotated origin when the text changes size', async () => { + const { runtimeScene, object } = await makeTextRuntimeObject(); + const originalWidth = object.getWidth(); + const originalRotatedOrigin = object.getHitBoxes()[0].vertices[0].slice(); + + object.setText('100000'); + runtimeScene.renderAndStep(1000 / 60); + + expect(object.getWidth()).to.be.greaterThan(originalWidth); + const updatedRotatedOrigin = object.getHitBoxes()[0].vertices[0]; + expect(updatedRotatedOrigin[0]).to.be.within( + originalRotatedOrigin[0] - 0.001, + originalRotatedOrigin[0] + 0.001 + ); + expect(updatedRotatedOrigin[1]).to.be.within( + originalRotatedOrigin[1] - 0.001, + originalRotatedOrigin[1] + 0.001 + ); + + runtimeScene.unloadScene(); + }); +}); diff --git a/Extensions/TextObject/textruntimeobject-pixi-renderer.ts b/Extensions/TextObject/textruntimeobject-pixi-renderer.ts index f8e73bc9a529..18c10e57eaf3 100644 --- a/Extensions/TextObject/textruntimeobject-pixi-renderer.ts +++ b/Extensions/TextObject/textruntimeobject-pixi-renderer.ts @@ -99,25 +99,27 @@ namespace gdjs { } updatePosition(): void { - if (this._object.isWrapping() && this._text.width !== 0) { - const alignmentX = - this._object._textAlign === 'right' - ? 1 - : this._object._textAlign === 'center' - ? 0.5 - : 0; - - const width = this._object.getWrappingWidth(); - - // A vector from the custom size center to the renderer center. - const centerToCenterX = (width - this._text.width) * (alignmentX - 0.5); + const alignmentX = + this._object._textAlign === 'right' + ? 1 + : this._object._textAlign === 'center' + ? 0.5 + : 0; + const objectWidth = this._object.isWrapping() + ? this._object.getWrappingWidth() + : this._text.width; + const textLeftOffset = this._object.isWrapping() + ? (objectWidth - this._text.width) * alignmentX + : 0; + const centerX = this._object._rotationCenter + ? this._object._rotationCenter[0] + : objectWidth / 2; - this._text.position.x = this._object.x + width / 2; - this._text.anchor.x = 0.5 - centerToCenterX / this._text.width; - } else { - this._text.position.x = this._object.x + this._text.width / 2; - this._text.anchor.x = 0.5; - } + this._text.position.x = this._object.getDrawableX() + centerX; + this._text.anchor.x = + this._text.width !== 0 + ? (centerX - textLeftOffset) / this._text.width + : 0; const alignmentY = this._object._verticalTextAlignment === 'bottom' @@ -125,9 +127,14 @@ namespace gdjs { : this._object._verticalTextAlignment === 'center' ? 0.5 : 0; - this._text.position.y = - this._object.y + this._text.height * (0.5 - alignmentY); - this._text.anchor.y = 0.5; + const textTopOffset = this._text.height * alignmentY; + const centerY = this._object._rotationCenter + ? this._object._rotationCenter[1] + : this._text.height / 2; + + this._text.position.y = this._object.y + centerY - textTopOffset; + this._text.anchor.y = + this._text.height !== 0 ? centerY / this._text.height : 0; } updateAngle(): void { diff --git a/Extensions/TextObject/textruntimeobject.ts b/Extensions/TextObject/textruntimeobject.ts index 339c35938462..8b24262e3f61 100644 --- a/Extensions/TextObject/textruntimeobject.ts +++ b/Extensions/TextObject/textruntimeobject.ts @@ -123,6 +123,7 @@ namespace gdjs { _padding: integer = 5; _str: string; _renderer: gdjs.TextRuntimeObjectRenderer; + _rotationCenter: FloatPoint | null = null; // We can store the scale as nothing else can change it. _scaleX: number = 1; @@ -389,6 +390,12 @@ namespace gdjs { this._renderer.updatePosition(); } + private _lockRotationCenter() { + if (this._rotationCenter) return; + + this._rotationCenter = [this.getCenterX(), this.getCenterY()]; + } + override setX(x: float): void { super.setX(x); this._updateTextPosition(); @@ -401,6 +408,10 @@ namespace gdjs { override setAngle(angle: float): void { super.setAngle(angle); + if (angle === 0 && this._rotationCenter) { + this._rotationCenter = null; + this._updateTextPosition(); + } this._renderer.updateAngle(); } @@ -457,6 +468,9 @@ namespace gdjs { if (text === this._str) { return; } + if (this.angle !== 0) { + this._lockRotationCenter(); + } this._str = text; this._renderer.updateString(); this._updateTextPosition(); @@ -736,6 +750,18 @@ namespace gdjs { ); } + override getCenterX(): float { + return this._rotationCenter + ? this._rotationCenter[0] + : super.getCenterX(); + } + + override getCenterY(): float { + return this._rotationCenter + ? this._rotationCenter[1] + : super.getCenterY(); + } + /** * Set the outline for the text object. * @param rgbOrHexColor color as a "R;G;B" string, for example: "255;0;0"