diff --git a/Core/GDCore/Project/Project.h b/Core/GDCore/Project/Project.h index 59a300206168..6ba0ef36bca2 100644 --- a/Core/GDCore/Project/Project.h +++ b/Core/GDCore/Project/Project.h @@ -374,12 +374,14 @@ class GD_CORE_API Project { void SetVerticalSyncActivatedByDefault(bool enable) { verticalSync = enable; } /** - * Return the scale mode used by the game (usually "linear" or "nearest"). + * Return the scale mode used by the game (usually "linear", "magnified" or + * "nearest"). */ const gd::String& GetScaleMode() const { return scaleMode; } /** - * Set the scale mode used by the game (usually "linear" or "nearest"). + * Set the scale mode used by the game (usually "linear", "magnified" or + * "nearest"). */ void SetScaleMode(const gd::String& scaleMode_) { scaleMode = scaleMode_; } diff --git a/Extensions/TextInput/textinputruntimeobject-pixi-renderer.ts b/Extensions/TextInput/textinputruntimeobject-pixi-renderer.ts index 91f2ae3faf00..c09e697396ea 100644 --- a/Extensions/TextInput/textinputruntimeobject-pixi-renderer.ts +++ b/Extensions/TextInput/textinputruntimeobject-pixi-renderer.ts @@ -211,8 +211,8 @@ namespace gdjs { const isOutsideCanvas = canvasRight < 0 || canvasBottom < 0 || - canvasLeft > runtimeGame.getGameResolutionWidth() || - canvasTop > runtimeGame.getGameResolutionHeight(); + canvasLeft > runtimeGame.getRenderingResolutionWidth() || + canvasTop > runtimeGame.getRenderingResolutionHeight(); if (isOutsideCanvas) { this._form.style.display = 'none'; return; diff --git a/Extensions/TextObject/textruntimeobject-pixi-renderer.ts b/Extensions/TextObject/textruntimeobject-pixi-renderer.ts index 0fe94a9b8085..1cf12ceb3deb 100644 --- a/Extensions/TextObject/textruntimeobject-pixi-renderer.ts +++ b/Extensions/TextObject/textruntimeobject-pixi-renderer.ts @@ -4,6 +4,7 @@ namespace gdjs { _fontManager: any; _text: PIXI.Text; _justCreated: boolean = true; + _upscaleRatio: integer = 1; constructor( runtimeObject: gdjs.TextRuntimeObject, @@ -47,7 +48,7 @@ namespace gdjs { const style = this._text.style; style.fontStyle = this._object._italic ? 'italic' : 'normal'; style.fontWeight = this._object._bold ? 'bold' : 'normal'; - style.fontSize = this._object._characterSize; + style.fontSize = this._object._characterSize * this._upscaleRatio; style.fontFamily = fontName; if (this._object._useGradient) { style.fill = this._getGradientHex(); @@ -62,7 +63,7 @@ namespace gdjs { // @ts-ignore style.align = this._object._textAlign; style.wordWrap = this._object._wrapping; - style.wordWrapWidth = this._object._wrappingWidth; + style.wordWrapWidth = this._object._wrappingWidth * this._upscaleRatio; style.breakWords = true; style.stroke = gdjs.rgbToHexNumber( this._object._outlineColor[0], @@ -70,7 +71,7 @@ namespace gdjs { this._object._outlineColor[2] ); style.strokeThickness = this._object._isOutlineEnabled - ? this._object._outlineThickness + ? this._object._outlineThickness * this._upscaleRatio : 0; style.dropShadow = this._object._shadow; style.dropShadowColor = gdjs.rgbToHexNumber( @@ -79,13 +80,16 @@ namespace gdjs { this._object._shadowColor[2] ); style.dropShadowAlpha = this._object._shadowOpacity / 255; - style.dropShadowBlur = this._object._shadowBlur; + style.dropShadowBlur = this._object._shadowBlur * this._upscaleRatio; style.dropShadowAngle = gdjs.toRad(this._object._shadowAngle); - style.dropShadowDistance = this._object._shadowDistance; + style.dropShadowDistance = + this._object._shadowDistance * this._upscaleRatio; const extraPaddingForShadow = style.dropShadow - ? style.dropShadowDistance + style.dropShadowBlur + ? this._object._shadowDistance + this._object._shadowBlur : 0; - style.padding = Math.ceil(this._object._padding + extraPaddingForShadow); + style.padding = + Math.ceil(this._object._padding + extraPaddingForShadow) * + this._upscaleRatio; // Prevent spikey outlines by adding a miter limit style.miterLimit = 3; @@ -117,7 +121,6 @@ namespace gdjs { this._text.position.x = this._object.x + this._text.width / 2; this._text.anchor.x = 0.5; } - this._text.position.y = this._object.y + this._text.height / 2; const alignmentY = this._object._verticalTextAlignment === 'bottom' @@ -181,18 +184,30 @@ namespace gdjs { return gradient; } + /** + * Set the text object upscale ratio. + * @param upscaleRatio The new upscale ratio for the text object. + * @see gdjs.RuntimeGame.getZoomFactor + */ + setUpscaleRatio(upscaleRatio: integer): void { + this._upscaleRatio = upscaleRatio; + this._text.scale.x = this._object.getScaleX() / this._upscaleRatio; + this._text.scale.y = this._object.getScaleY() / this._upscaleRatio; + this.updateStyle(); + } + /** * Get x-scale of the text. */ getScaleX(): float { - return this._text.scale.x; + return this._object.getScaleX(); } /** * Get y-scale of the text. */ getScaleY(): float { - return this._text.scale.y; + return this._object.getScaleY(); } /** @@ -200,8 +215,8 @@ namespace gdjs { * @param newScale The new scale for the text object. */ setScale(newScale: float): void { - this._text.scale.x = newScale; - this._text.scale.y = newScale; + this._text.scale.x = newScale / this._upscaleRatio; + this._text.scale.y = newScale / this._upscaleRatio; } /** @@ -209,7 +224,7 @@ namespace gdjs { * @param newScale The new x-scale for the text object. */ setScaleX(newScale: float): void { - this._text.scale.x = newScale; + this._text.scale.x = newScale / this._upscaleRatio; } /** @@ -217,7 +232,7 @@ namespace gdjs { * @param newScale The new y-scale for the text object. */ setScaleY(newScale: float): void { - this._text.scale.y = newScale; + this._text.scale.y = newScale / this._upscaleRatio; } destroy() { diff --git a/Extensions/TextObject/textruntimeobject.ts b/Extensions/TextObject/textruntimeobject.ts index abd6ea72d914..e5b6437c2539 100644 --- a/Extensions/TextObject/textruntimeobject.ts +++ b/Extensions/TextObject/textruntimeobject.ts @@ -149,7 +149,7 @@ namespace gdjs { this.onCreated(); } - updateFromObjectData( + override updateFromObjectData( oldObjectData: TextObjectData, newObjectData: TextObjectData ): boolean { @@ -214,7 +214,7 @@ namespace gdjs { return true; } - getNetworkSyncData(): TextObjectNetworkSyncData { + override getNetworkSyncData(): TextObjectNetworkSyncData { return { ...super.getNetworkSyncData(), str: this._str, @@ -242,7 +242,7 @@ namespace gdjs { }; } - updateFromNetworkSyncData( + override updateFromNetworkSyncData( networkSyncData: TextObjectNetworkSyncData ): void { super.updateFromNetworkSyncData(networkSyncData); @@ -317,15 +317,15 @@ namespace gdjs { } } - getRendererObject() { + override getRendererObject() { return this._renderer.getRendererObject(); } - update(instanceContainer: gdjs.RuntimeInstanceContainer): void { + override update(instanceContainer: gdjs.RuntimeInstanceContainer): void { this._renderer.ensureUpToDate(); } - onDestroyed(): void { + override onDestroyed(): void { super.onDestroyed(); this._renderer.destroy(); } @@ -345,6 +345,12 @@ namespace gdjs { } } + override onGameZoomFactorChanged(): void { + this._renderer.setUpscaleRatio( + this.getRuntimeScene().getGame().getZoomFactor() + ); + } + /** * Update the rendered object position. */ @@ -353,27 +359,17 @@ namespace gdjs { this._renderer.updatePosition(); } - /** - * Set object position on X axis. - */ - setX(x: float): void { + override setX(x: float): void { super.setX(x); this._updateTextPosition(); } - /** - * Set object position on Y axis. - */ - setY(y: float): void { + override setY(y: float): void { super.setY(y); this._updateTextPosition(); } - /** - * Set the angle of the object. - * @param angle The new angle of the object - */ - setAngle(angle: float): void { + override setAngle(angle: float): void { super.setAngle(angle); this._renderer.updateAngle(); } @@ -499,14 +495,14 @@ namespace gdjs { /** * Get width of the text. */ - getWidth(): float { + override getWidth(): float { return this._wrapping ? this._wrappingWidth : this._renderer.getWidth(); } /** * Get height of the text. */ - getHeight(): float { + override getHeight(): float { return this._renderer.getHeight(); } @@ -685,11 +681,11 @@ namespace gdjs { } } - setWidth(width: float): void { + override setWidth(width: float): void { this.setWrappingWidth(width); } - getDrawableY(): float { + override getDrawableY(): float { return ( this.getY() - (this._verticalTextAlignment === 'center' diff --git a/GDJS/Runtime/CustomRuntimeObject.ts b/GDJS/Runtime/CustomRuntimeObject.ts index 3c597d8ce06c..f650460d4f63 100644 --- a/GDJS/Runtime/CustomRuntimeObject.ts +++ b/GDJS/Runtime/CustomRuntimeObject.ts @@ -229,6 +229,10 @@ namespace gdjs { */ onDestroy(parent: gdjs.RuntimeInstanceContainer) {} + override onGameZoomFactorChanged() { + this._instanceContainer.onGameZoomFactorChanged(); + } + override updatePreRender(parent: gdjs.RuntimeInstanceContainer): void { this._instanceContainer._updateObjectsPreRender(); this.getRenderer().ensureUpToDate(); diff --git a/GDJS/Runtime/RuntimeInstanceContainer.ts b/GDJS/Runtime/RuntimeInstanceContainer.ts index e4ef5eee2099..b33e06bddc04 100644 --- a/GDJS/Runtime/RuntimeInstanceContainer.ts +++ b/GDJS/Runtime/RuntimeInstanceContainer.ts @@ -547,6 +547,15 @@ namespace gdjs { return this._allInstancesList; } + /** + * Called when the game zoom factor is changed to adapt to a new resolution. + */ + onGameZoomFactorChanged(): void { + for (const instance of this.getAdhocListOfAllInstances()) { + instance.onGameZoomFactorChanged(); + } + } + /** * Update the objects before launching the events. */ diff --git a/GDJS/Runtime/pixi-renderers/DebuggerPixiRenderer.ts b/GDJS/Runtime/pixi-renderers/DebuggerPixiRenderer.ts index 1957181a9ff4..2df63a192a28 100644 --- a/GDJS/Runtime/pixi-renderers/DebuggerPixiRenderer.ts +++ b/GDJS/Runtime/pixi-renderers/DebuggerPixiRenderer.ts @@ -37,18 +37,10 @@ namespace gdjs { showPointsNames: boolean, showCustomPoints: boolean ) { - const pixiContainer = this._instanceContainer - .getRenderer() - .getRendererObject(); if (!this._debugDraw || !this._debugDrawContainer) { this._debugDrawContainer = new PIXI.Container(); this._debugDraw = new PIXI.Graphics(); - - // Add on top of all layers: this._debugDrawContainer.addChild(this._debugDraw); - if (pixiContainer) { - pixiContainer.addChild(this._debugDrawContainer); - } } const debugDraw = this._debugDraw; @@ -112,7 +104,8 @@ namespace gdjs { const polygon: float[] = []; polygon.push.apply( polygon, - layer.applyLayerTransformation( + this.applyLayerTransformation( + layer, aabb.min[0], aabb.min[1], 0, @@ -121,7 +114,8 @@ namespace gdjs { ); polygon.push.apply( polygon, - layer.applyLayerTransformation( + this.applyLayerTransformation( + layer, aabb.max[0], aabb.min[1], 0, @@ -130,7 +124,8 @@ namespace gdjs { ); polygon.push.apply( polygon, - layer.applyLayerTransformation( + this.applyLayerTransformation( + layer, aabb.max[0], aabb.max[1], 0, @@ -139,7 +134,8 @@ namespace gdjs { ); polygon.push.apply( polygon, - layer.applyLayerTransformation( + this.applyLayerTransformation( + layer, aabb.min[0], aabb.max[1], 0, @@ -185,7 +181,8 @@ namespace gdjs { // as this is for debug draw. const polygon: float[] = []; hitboxes[j].vertices.forEach((point) => { - point = layer.applyLayerTransformation( + point = this.applyLayerTransformation( + layer, point[0], point[1], 0, @@ -205,7 +202,8 @@ namespace gdjs { debugDraw.fill.alpha = 0.3; // Draw Center point - const centerPoint = layer.applyLayerTransformation( + const centerPoint = this.applyLayerTransformation( + layer, object.getCenterXInScene(), object.getCenterYInScene(), 0, @@ -221,7 +219,8 @@ namespace gdjs { ); // Draw position point - const positionPoint = layer.applyLayerTransformation( + const positionPoint = this.applyLayerTransformation( + layer, object.getX(), object.getY(), 0, @@ -245,7 +244,8 @@ namespace gdjs { Math.abs(originPoint[0] - positionPoint[0]) >= 1 || Math.abs(originPoint[1] - positionPoint[1]) >= 1 ) { - originPoint = layer.applyLayerTransformation( + originPoint = this.applyLayerTransformation( + layer, originPoint[0], originPoint[1], 0, @@ -270,7 +270,8 @@ namespace gdjs { for (const customPointName in animationFrame.points.items) { let customPoint = object.getPointPosition(customPointName); - customPoint = layer.applyLayerTransformation( + customPoint = this.applyLayerTransformation( + layer, customPoint[0], customPoint[1], 0, @@ -303,6 +304,27 @@ namespace gdjs { debugDraw.endFill(); } + private applyLayerTransformation( + layer: gdjs.RuntimeLayer, + x: float, + y: float, + cameraId: integer, + result: FloatPoint + ): FloatPoint { + layer.applyLayerTransformation(x, y, cameraId, result); + const gamePixiContainer = this._instanceContainer + .getRenderer() + .getRendererObject(); + if (!gamePixiContainer) { + return result; + } + // The scale is usually near 1 unless the 'magnified' scale mode is used. + // See gdjs.RuntimeGame.getZoomFactor + result[0] *= gamePixiContainer.scale.x; + result[1] *= gamePixiContainer.scale.y; + return result; + } + clearDebugDraw(): void { if (this._debugDraw) { this._debugDraw.clear(); diff --git a/GDJS/Runtime/pixi-renderers/layer-pixi-renderer.ts b/GDJS/Runtime/pixi-renderers/layer-pixi-renderer.ts index bdeff384f2ee..d8709d6b2c6d 100644 --- a/GDJS/Runtime/pixi-renderers/layer-pixi-renderer.ts +++ b/GDJS/Runtime/pixi-renderers/layer-pixi-renderer.ts @@ -262,8 +262,8 @@ namespace gdjs { if (game.getAntialiasingMode() !== 'none') { this._threeEffectComposer.addPass( new THREE_ADDONS.SMAAPass( - game.getGameResolutionWidth(), - game.getGameResolutionHeight() + game.getRenderingResolutionWidth(), + game.getRenderingResolutionHeight() ) ); } @@ -304,9 +304,12 @@ namespace gdjs { this._threePlaneTexture = texture; this._threePlaneTexture.generateMipmaps = false; + const scaleMode = this._layer + .getRuntimeScene() + .getGame() + .getScaleMode(); const filter = - this._layer.getRuntimeScene().getGame().getScaleMode() === - 'nearest' + scaleMode === 'nearest' || scaleMode === 'magnified' ? THREE.NearestFilter : THREE.LinearFilter; this._threePlaneTexture.minFilter = filter; @@ -501,8 +504,8 @@ namespace gdjs { if (this._threeEffectComposer) { const game = this._layer.getRuntimeScene().getGame(); this._threeEffectComposer.setSize( - game.getGameResolutionWidth(), - game.getGameResolutionHeight() + game.getRenderingResolutionWidth(), + game.getRenderingResolutionHeight() ); } } diff --git a/GDJS/Runtime/pixi-renderers/runtimegame-pixi-renderer.ts b/GDJS/Runtime/pixi-renderers/runtimegame-pixi-renderer.ts index 9c736c974d2d..e1d35994a2be 100644 --- a/GDJS/Runtime/pixi-renderers/runtimegame-pixi-renderer.ts +++ b/GDJS/Runtime/pixi-renderers/runtimegame-pixi-renderer.ts @@ -100,16 +100,16 @@ namespace gdjs { this._threeRenderer.useLegacyLights = true; this._threeRenderer.autoClear = false; this._threeRenderer.setSize( - this._game.getGameResolutionWidth(), - this._game.getGameResolutionHeight() + this._game.getRenderingResolutionWidth(), + this._game.getRenderingResolutionHeight() ); // Create a PixiJS renderer that use the same GL context as Three.js // so that both can render to the canvas and even have PixiJS rendering // reused in Three.js (by using a RenderTexture and the same internal WebGL texture). this._pixiRenderer = new PIXI.Renderer({ - width: this._game.getGameResolutionWidth(), - height: this._game.getGameResolutionHeight(), + width: this._game.getRenderingResolutionWidth(), + height: this._game.getRenderingResolutionHeight(), view: gameCanvas, // @ts-ignore - reuse the context from Three.js. context: this._threeRenderer.getContext(), @@ -124,8 +124,8 @@ namespace gdjs { // "preserveDrawingBuffer: true" is needed to avoid flickering // and background issues on some mobile phones (see #585 #572 #566 #463). this._pixiRenderer = PIXI.autoDetectRenderer({ - width: this._game.getGameResolutionWidth(), - height: this._game.getGameResolutionHeight(), + width: this._game.getRenderingResolutionWidth(), + height: this._game.getRenderingResolutionHeight(), view: gameCanvas, preserveDrawingBuffer: true, antialias: false, @@ -272,20 +272,20 @@ namespace gdjs { // There is no "smart" resizing to be done here: the rendering of the game // should be done with the size set on the game. if ( - this._pixiRenderer.width !== this._game.getGameResolutionWidth() || - this._pixiRenderer.height !== this._game.getGameResolutionHeight() + this._pixiRenderer.width !== this._game.getRenderingResolutionWidth() || + this._pixiRenderer.height !== this._game.getRenderingResolutionHeight() ) { // TODO (3D): It might be useful to resize pixi view in 3D depending on FOV value // to enable a mode where pixi always fills the whole screen. this._pixiRenderer.resize( - this._game.getGameResolutionWidth(), - this._game.getGameResolutionHeight() + this._game.getRenderingResolutionWidth(), + this._game.getRenderingResolutionHeight() ); if (this._threeRenderer) { this._threeRenderer.setSize( - this._game.getGameResolutionWidth(), - this._game.getGameResolutionHeight() + this._game.getRenderingResolutionWidth(), + this._game.getRenderingResolutionHeight() ); } } @@ -295,8 +295,8 @@ namespace gdjs { // only, so won't create visual artifacts during the rendering. const isFullPage = this._forceFullscreen || this._isFullPage || this._isFullscreen; - let canvasWidth = this._game.getGameResolutionWidth(); - let canvasHeight = this._game.getGameResolutionHeight(); + let canvasWidth = this._game.getRenderingResolutionWidth(); + let canvasHeight = this._game.getRenderingResolutionHeight(); let maxWidth = window.innerWidth - this._marginLeft - this._marginRight; let maxHeight = window.innerHeight - this._marginTop - this._marginBottom; if (maxWidth < 0) { @@ -523,10 +523,10 @@ namespace gdjs { // Handle the fact that the game is stretched to fill the canvas. pageCoords[0] = (canvasCoords[0] * this._canvasWidth) / - this._game.getGameResolutionWidth(); + this._game.getRenderingResolutionWidth(); pageCoords[1] = (canvasCoords[1] * this._canvasHeight) / - this._game.getGameResolutionHeight(); + this._game.getRenderingResolutionHeight(); return pageCoords; } diff --git a/GDJS/Runtime/pixi-renderers/runtimescene-pixi-renderer.ts b/GDJS/Runtime/pixi-renderers/runtimescene-pixi-renderer.ts index 603c1bb3436e..89ced94d85d0 100644 --- a/GDJS/Runtime/pixi-renderers/runtimescene-pixi-renderer.ts +++ b/GDJS/Runtime/pixi-renderers/runtimescene-pixi-renderer.ts @@ -275,6 +275,12 @@ namespace gdjs { pixiRenderer.render(this._pixiContainer, { clear: this._runtimeScene.getClearCanvas(), }); + const debugContainer = this._runtimeScene + .getDebuggerRenderer() + .getRendererObject(); + if (debugContainer) { + pixiRenderer.render(debugContainer, { clear: false }); + } this._layerRenderingMetrics.rendered2DLayersCount++; } diff --git a/GDJS/Runtime/runtimegame.ts b/GDJS/Runtime/runtimegame.ts index 7bb9cceabd01..abbb3484e3c2 100644 --- a/GDJS/Runtime/runtimegame.ts +++ b/GDJS/Runtime/runtimegame.ts @@ -141,7 +141,7 @@ namespace gdjs { _originalHeight: float; _resizeMode: 'adaptWidth' | 'adaptHeight' | string; _adaptGameResolutionAtRuntime: boolean; - _scaleMode: 'linear' | 'nearest'; + _scaleMode: ScaleMode; _pixelsRounding: boolean; _antialiasingMode: 'none' | 'MSAA'; _isAntialisingEnabledOnMobile: boolean; @@ -158,6 +158,11 @@ namespace gdjs { * When set to true, the scenes are notified that game resolution size changed. */ _notifyScenesForGameResolutionResize: boolean = false; + /** + * When set to true, the scenes are notified that game zoom factor changed. + */ + _notifyScenesForGameZoomFactorChange: boolean = false; + _zoomFactor: number = 1; /** * When paused, the game won't step and will be freezed. Useful for debugging. @@ -551,21 +556,45 @@ namespace gdjs { } /** - * Get the game resolution (the size at which the game is played and rendered) width. + * Get the game resolution width for events. * @returns The game resolution width, in pixels. */ getGameResolutionWidth(): float { - return this._gameResolutionWidth; + return this._gameResolutionWidth / this._zoomFactor; } /** - * Get the game resolution (the size at which the game is played and rendered) height. + * Get the game resolution height for events. * @returns The game resolution height, in pixels. */ getGameResolutionHeight(): float { + return this._gameResolutionHeight / this._zoomFactor; + } + + /** + * Get the game resolution width (the size at which the game is rendered). + * @returns The game resolution width, in pixels. + */ + getRenderingResolutionWidth(): float { + return this._gameResolutionWidth; + } + + /** + * Get the game resolution height (the size at which the game is rendered). + * @returns The game resolution height, in pixels. + */ + getRenderingResolutionHeight(): float { return this._gameResolutionHeight; } + /** + * The scale is usually near 1 unless the 'magnified' scale mode is used. + * @returns the factor between game resolution size and rendering resolution size. + */ + getZoomFactor() { + return this._zoomFactor; + } + /** * Change the game resolution. * @@ -577,17 +606,16 @@ namespace gdjs { this._gameResolutionWidth = width; this._gameResolutionHeight = height; - if (this._adaptGameResolutionAtRuntime) { - if ( - gdjs.RuntimeGameRenderer && - gdjs.RuntimeGameRenderer.getWindowInnerWidth && - gdjs.RuntimeGameRenderer.getWindowInnerHeight - ) { - const windowInnerWidth = - gdjs.RuntimeGameRenderer.getWindowInnerWidth(); - const windowInnerHeight = - gdjs.RuntimeGameRenderer.getWindowInnerHeight(); + if ( + gdjs.RuntimeGameRenderer && + gdjs.RuntimeGameRenderer.getWindowInnerWidth && + gdjs.RuntimeGameRenderer.getWindowInnerHeight + ) { + const windowInnerWidth = gdjs.RuntimeGameRenderer.getWindowInnerWidth(); + const windowInnerHeight = + gdjs.RuntimeGameRenderer.getWindowInnerHeight(); + if (this._adaptGameResolutionAtRuntime) { // Enlarge either the width or the eight to fill the inner window space. if (this._resizeMode === 'adaptWidth') { this._gameResolutionWidth = @@ -614,6 +642,35 @@ namespace gdjs { } } } + if ( + this._scaleMode === 'magnified' && + this._gameResolutionWidth > 0 && + this._gameResolutionHeight > 0 && + // Fall back on linear if magnified is used on a high resolution game. + this._originalWidth <= 960 && + this._originalHeight <= 540 + ) { + const pixelSize = Math.max( + 1, + Math.ceil( + this._zoomFactor * + Math.min( + windowInnerWidth / this._gameResolutionWidth, + windowInnerHeight / this._gameResolutionHeight + ) + ) + ); + this._gameResolutionWidth = Math.round( + (this._gameResolutionWidth * pixelSize) / this._zoomFactor + ); + this._gameResolutionHeight = Math.round( + (this._gameResolutionHeight * pixelSize) / this._zoomFactor + ); + if (this._zoomFactor !== pixelSize && pixelSize >= 1) { + this._zoomFactor = pixelSize; + this._notifyScenesForGameZoomFactorChange = true; + } + } } // Don't alter the game resolution. The renderer @@ -682,9 +739,9 @@ namespace gdjs { } /** - * Return the scale mode of the game ("linear" or "nearest"). + * Return the scale mode of the game ("linear", "magnified" or "nearest"). */ - getScaleMode(): 'linear' | 'nearest' { + getScaleMode(): ScaleMode { return this._scaleMode; } @@ -962,6 +1019,10 @@ namespace gdjs { this._sceneStack.onGameResolutionResized(); this._notifyScenesForGameResolutionResize = false; } + if (this._notifyScenesForGameZoomFactorChange) { + this._sceneStack.onGameZoomFactorChanged(); + this._notifyScenesForGameZoomFactorChange = false; + } // Render and step the scene. if (this._sceneStack.step(elapsedTime)) { diff --git a/GDJS/Runtime/runtimeobject.ts b/GDJS/Runtime/runtimeobject.ts index 47179079b686..0434373b14d5 100644 --- a/GDJS/Runtime/runtimeobject.ts +++ b/GDJS/Runtime/runtimeobject.ts @@ -658,6 +658,11 @@ namespace gdjs { */ onSceneResumed(runtimeScene: gdjs.RuntimeScene): void {} + /** + * Called when the game zoom factor is changed to adapt to a new resolution. + */ + onGameZoomFactorChanged(): void {} + //Rendering: /** * @return The internal object for a 2D rendering (PIXI.DisplayObject...) diff --git a/GDJS/Runtime/scenestack.ts b/GDJS/Runtime/scenestack.ts index b04bd5976187..1c3831b4fb9d 100644 --- a/GDJS/Runtime/scenestack.ts +++ b/GDJS/Runtime/scenestack.ts @@ -34,6 +34,15 @@ namespace gdjs { } } + /** + * Called when the game zoom factor is changed to adapt to a new resolution. + */ + onGameZoomFactorChanged(): void { + for (let i = 0; i < this._stack.length; ++i) { + this._stack[i].onGameZoomFactorChanged(); + } + } + step(elapsedTime: float): boolean { this._throwIfDisposed(); if (this._isNextLayoutLoading || this._stack.length === 0) { diff --git a/GDJS/Runtime/types/project-data.d.ts b/GDJS/Runtime/types/project-data.d.ts index 8f4947b17911..024939051aed 100644 --- a/GDJS/Runtime/types/project-data.d.ts +++ b/GDJS/Runtime/types/project-data.d.ts @@ -329,13 +329,15 @@ declare interface EffectNetworkSyncData { }; } +declare type ScaleMode = 'linear' | 'magnified' | 'nearest'; + declare interface ProjectPropertiesData { adaptGameResolutionAtRuntime: boolean; folderProject: boolean; orientation: string; packageName: string; projectFile: string; - scaleMode: 'linear' | 'nearest'; + scaleMode: ScaleMode; pixelsRounding: boolean; antialiasingMode: 'none' | 'MSAA'; antialisingEnabledOnMobile: boolean; diff --git a/newIDE/app/src/AssetStore/InstallAsset.js b/newIDE/app/src/AssetStore/InstallAsset.js index 86bfb6e90861..5b4c674cb63d 100644 --- a/newIDE/app/src/AssetStore/InstallAsset.js +++ b/newIDE/app/src/AssetStore/InstallAsset.js @@ -129,7 +129,9 @@ export const installResource = ( if (newResource.getKind() === 'image') { // $FlowExpectedError[prop-missing] - We know the resource is an ImageResource and has the setSmooth method. newResource.setSmooth( - project.getScaleMode() !== 'nearest' && !isPixelArt(asset) + project.getScaleMode() !== 'nearest' && + project.getScaleMode() !== 'magnified' && + !isPixelArt(asset) ); } diff --git a/newIDE/app/src/InstancesEditor/InstancesRenderer/LayerRenderer.js b/newIDE/app/src/InstancesEditor/InstancesRenderer/LayerRenderer.js index 625fce456066..53321d6387c6 100644 --- a/newIDE/app/src/InstancesEditor/InstancesRenderer/LayerRenderer.js +++ b/newIDE/app/src/InstancesEditor/InstancesRenderer/LayerRenderer.js @@ -655,7 +655,8 @@ export default class LayerRenderer { threePlaneTexture.generateMipmaps = false; const filter = - this.project.getScaleMode() === 'nearest' + this.project.getScaleMode() === 'nearest' || + this.project.getScaleMode() === 'magnified' ? THREE.NearestFilter : THREE.LinearFilter; threePlaneTexture.minFilter = filter; diff --git a/newIDE/app/src/ProjectManager/ProjectPropertiesDialog.js b/newIDE/app/src/ProjectManager/ProjectPropertiesDialog.js index 46a01a06b6aa..1ebd11b3bb8e 100644 --- a/newIDE/app/src/ProjectManager/ProjectPropertiesDialog.js +++ b/newIDE/app/src/ProjectManager/ProjectPropertiesDialog.js @@ -729,11 +729,15 @@ const ProjectPropertiesDialog = (props: Props) => { > <SelectOption value="linear" - label={t`Linear (antialiased rendering, good for most games)`} + label={t`Linear (smooth, good for most games)`} + /> + <SelectOption + value="magnified" + label={t`Magnified (sharp, good for modern pixel-art games)`} /> <SelectOption value="nearest" - label={t`Nearest (no antialiasing, good for pixel perfect games)`} + label={t`Nearest (aliased, good for pixel perfect games)`} /> </SelectField> <Checkbox @@ -749,7 +753,7 @@ const ProjectPropertiesDialog = (props: Props) => { notifyOfChange(); }} /> - {scaleMode === 'nearest' && ( + {(scaleMode === 'nearest' || scaleMode === 'magnified') && ( <DismissableAlertMessage identifier="use-non-smoothed-textures" kind="info" diff --git a/newIDE/app/src/ResourcesList/ResourceUtils.js b/newIDE/app/src/ResourcesList/ResourceUtils.js index 8341cde9c331..d7430751e50d 100644 --- a/newIDE/app/src/ResourcesList/ResourceUtils.js +++ b/newIDE/app/src/ResourcesList/ResourceUtils.js @@ -154,7 +154,10 @@ export const applyResourceDefaults = ( newResource: gdResource ) => { if (newResource instanceof gd.ImageResource) { - newResource.setSmooth(project.getScaleMode() !== 'nearest'); + newResource.setSmooth( + project.getScaleMode() !== 'nearest' && + project.getScaleMode() !== 'magnified' + ); } };