Mobile-"ish" emscripten support
Full screen "wasm-game" for viewport compatibility between desktop and
web interfaces. Viewport modes ("fit", "center", and "stretch") should
now work the same way under WASM/SDL and SFML. This should also enable
android or web-for-mobile aspect ratios to be supported more easily.
This commit is contained in:
parent
24611c339c
commit
726a9cf09d
9 changed files with 282 additions and 46 deletions
|
|
@ -15,6 +15,9 @@
|
|||
#endif
|
||||
#include <cmath>
|
||||
#include <Python.h>
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten/html5.h>
|
||||
#endif
|
||||
|
||||
// Static member definitions for shader intermediate texture (#106)
|
||||
std::unique_ptr<sf::RenderTexture> GameEngine::shaderIntermediate;
|
||||
|
|
@ -84,7 +87,15 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg)
|
|||
render_target = &headless_renderer->getRenderTarget();
|
||||
} else {
|
||||
window = std::make_unique<sf::RenderWindow>();
|
||||
#ifdef __EMSCRIPTEN__
|
||||
// Read actual canvas size from HTML template (may be fullscreen or layout-constrained)
|
||||
// gameResolution stays at its default (1024x768) - the viewport system maps it to the canvas
|
||||
int cw = 1024, ch = 768;
|
||||
emscripten_get_canvas_element_size("#canvas", &cw, &ch);
|
||||
window->create(sf::VideoMode(cw, ch), window_title, sf::Style::Titlebar | sf::Style::Close | sf::Style::Resize);
|
||||
#else
|
||||
window->create(sf::VideoMode(1024, 768), window_title, sf::Style::Titlebar | sf::Style::Close | sf::Style::Resize);
|
||||
#endif
|
||||
window->setFramerateLimit(60);
|
||||
render_target = window.get();
|
||||
|
||||
|
|
@ -243,7 +254,9 @@ void GameEngine::changeScene(std::string sceneName, TransitionType transitionTyp
|
|||
}
|
||||
else
|
||||
{
|
||||
// Start transition
|
||||
// Start transition with current game resolution
|
||||
transition.width = gameResolution.x;
|
||||
transition.height = gameResolution.y;
|
||||
transition.start(transitionType, scene, sceneName, duration);
|
||||
|
||||
// Render current scene to texture
|
||||
|
|
@ -829,8 +842,14 @@ void GameEngine::initShaderIntermediate(unsigned int width, unsigned int height)
|
|||
|
||||
sf::RenderTexture& GameEngine::getShaderIntermediate() {
|
||||
if (!shaderIntermediateInitialized) {
|
||||
// Initialize with default resolution if not already done
|
||||
initShaderIntermediate(1024, 768);
|
||||
// Initialize with game resolution from the engine instance
|
||||
unsigned int w = 1024, h = 768;
|
||||
if (Resources::game) {
|
||||
auto res = Resources::game->getGameResolution();
|
||||
w = res.x;
|
||||
h = res.y;
|
||||
}
|
||||
initShaderIntermediate(w, h);
|
||||
}
|
||||
return *shaderIntermediate;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,15 +6,15 @@ void SceneTransition::start(TransitionType t, const std::string& from, const std
|
|||
toScene = to;
|
||||
duration = dur;
|
||||
elapsed = 0.0f;
|
||||
|
||||
|
||||
// Initialize render textures if needed
|
||||
if (!oldSceneTexture) {
|
||||
oldSceneTexture = std::make_unique<sf::RenderTexture>();
|
||||
oldSceneTexture->create(1024, 768);
|
||||
oldSceneTexture->create(width, height);
|
||||
}
|
||||
if (!newSceneTexture) {
|
||||
newSceneTexture = std::make_unique<sf::RenderTexture>();
|
||||
newSceneTexture->create(1024, 768);
|
||||
newSceneTexture->create(width, height);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -25,14 +25,17 @@ void SceneTransition::update(float dt) {
|
|||
|
||||
void SceneTransition::render(sf::RenderTarget& target) {
|
||||
if (type == TransitionType::None) return;
|
||||
|
||||
|
||||
float progress = getProgress();
|
||||
float easedProgress = easeInOut(progress);
|
||||
|
||||
|
||||
float w = static_cast<float>(width);
|
||||
float h = static_cast<float>(height);
|
||||
|
||||
// Update sprites with current textures
|
||||
oldSprite.setTexture(oldSceneTexture->getTexture());
|
||||
newSprite.setTexture(newSceneTexture->getTexture());
|
||||
|
||||
|
||||
switch (type) {
|
||||
case TransitionType::Fade:
|
||||
// Fade out old scene, fade in new scene
|
||||
|
|
@ -41,39 +44,39 @@ void SceneTransition::render(sf::RenderTarget& target) {
|
|||
target.draw(oldSprite);
|
||||
target.draw(newSprite);
|
||||
break;
|
||||
|
||||
|
||||
case TransitionType::SlideLeft:
|
||||
// Old scene slides out to left, new scene slides in from right
|
||||
oldSprite.setPosition(-1024 * easedProgress, 0);
|
||||
newSprite.setPosition(1024 * (1.0f - easedProgress), 0);
|
||||
oldSprite.setPosition(-w * easedProgress, 0);
|
||||
newSprite.setPosition(w * (1.0f - easedProgress), 0);
|
||||
target.draw(oldSprite);
|
||||
target.draw(newSprite);
|
||||
break;
|
||||
|
||||
|
||||
case TransitionType::SlideRight:
|
||||
// Old scene slides out to right, new scene slides in from left
|
||||
oldSprite.setPosition(1024 * easedProgress, 0);
|
||||
newSprite.setPosition(-1024 * (1.0f - easedProgress), 0);
|
||||
oldSprite.setPosition(w * easedProgress, 0);
|
||||
newSprite.setPosition(-w * (1.0f - easedProgress), 0);
|
||||
target.draw(oldSprite);
|
||||
target.draw(newSprite);
|
||||
break;
|
||||
|
||||
|
||||
case TransitionType::SlideUp:
|
||||
// Old scene slides up, new scene slides in from bottom
|
||||
oldSprite.setPosition(0, -768 * easedProgress);
|
||||
newSprite.setPosition(0, 768 * (1.0f - easedProgress));
|
||||
oldSprite.setPosition(0, -h * easedProgress);
|
||||
newSprite.setPosition(0, h * (1.0f - easedProgress));
|
||||
target.draw(oldSprite);
|
||||
target.draw(newSprite);
|
||||
break;
|
||||
|
||||
|
||||
case TransitionType::SlideDown:
|
||||
// Old scene slides down, new scene slides in from top
|
||||
oldSprite.setPosition(0, 768 * easedProgress);
|
||||
newSprite.setPosition(0, -768 * (1.0f - easedProgress));
|
||||
oldSprite.setPosition(0, h * easedProgress);
|
||||
newSprite.setPosition(0, -h * (1.0f - easedProgress));
|
||||
target.draw(oldSprite);
|
||||
target.draw(newSprite);
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
@ -82,4 +85,4 @@ void SceneTransition::render(sf::RenderTarget& target) {
|
|||
float SceneTransition::easeInOut(float t) {
|
||||
// Smooth ease-in-out curve
|
||||
return t < 0.5f ? 2 * t * t : -1 + (4 - 2 * t) * t;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ public:
|
|||
TransitionType type = TransitionType::None;
|
||||
float duration = 0.0f;
|
||||
float elapsed = 0.0f;
|
||||
unsigned int width = 1024;
|
||||
unsigned int height = 768;
|
||||
std::string fromScene;
|
||||
std::string toScene;
|
||||
|
||||
|
|
|
|||
|
|
@ -1144,7 +1144,8 @@ PyObject* UIGrid::get_grid_h(PyUIGridObject* self, void* closure) {
|
|||
}
|
||||
|
||||
PyObject* UIGrid::get_position(PyUIGridObject* self, void* closure) {
|
||||
return Py_BuildValue("(ff)", self->data->position.x, self->data->position.y);
|
||||
// #179 - Return position as Vector (consistent with get_size, get_grid_size)
|
||||
return PyVector(self->data->position).pyObject();
|
||||
}
|
||||
|
||||
int UIGrid::set_position(PyUIGridObject* self, PyObject* value, void* closure) {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@
|
|||
#include <emscripten.h>
|
||||
#include <Python.h>
|
||||
#include <string>
|
||||
#include "Resources.h"
|
||||
#include "GameEngine.h"
|
||||
|
||||
extern "C" {
|
||||
|
||||
|
|
@ -290,6 +292,16 @@ int get_python_globals_count() {
|
|||
return (int)PyDict_Size(main_dict);
|
||||
}
|
||||
|
||||
// Notify the engine that the browser canvas was resized (called from JS)
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
void notify_canvas_resize(int width, int height) {
|
||||
// Forward to SDL so it generates a proper resize event
|
||||
if (Resources::game) {
|
||||
auto& win = Resources::game->getWindow();
|
||||
win.setSize(sf::Vector2u(width, height));
|
||||
}
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
#endif // __EMSCRIPTEN__
|
||||
|
|
|
|||
|
|
@ -567,23 +567,10 @@ void RenderWindow::create(VideoMode mode, const std::string& title, uint32_t sty
|
|||
open_ = true;
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
// Force canvas size AFTER SDL window creation (SDL may have reset it)
|
||||
// Force canvas backing buffer size AFTER SDL window creation (SDL may have reset it)
|
||||
// CSS display size is controlled by the HTML shell template (fullscreen or layout-constrained)
|
||||
emscripten_set_canvas_element_size("#canvas", mode.width, mode.height);
|
||||
|
||||
// Also set the CSS size to match
|
||||
EM_ASM({
|
||||
var canvas = document.getElementById('canvas');
|
||||
if (canvas) {
|
||||
canvas.width = $0;
|
||||
canvas.height = $1;
|
||||
canvas.style.width = $0 + 'px';
|
||||
canvas.style.height = $1 + 'px';
|
||||
console.log('EM_ASM: Set canvas to ' + $0 + 'x' + $1);
|
||||
} else {
|
||||
console.error('EM_ASM: Canvas element not found!');
|
||||
}
|
||||
}, mode.width, mode.height);
|
||||
|
||||
// Re-make context current after canvas resize
|
||||
SDL_GL_MakeCurrent(window, context);
|
||||
#endif
|
||||
|
|
@ -997,6 +984,80 @@ void RenderTarget::draw(const VertexArray& vertices, const RenderStates& states)
|
|||
draw(&vertices[0], vertices.getVertexCount(), vertices.getPrimitiveType(), states);
|
||||
}
|
||||
|
||||
void RenderTarget::setView(const View& view) {
|
||||
view_ = view;
|
||||
|
||||
// Apply the view's viewport (normalized 0-1 coords) to OpenGL
|
||||
auto vp = view.getViewport();
|
||||
int px = static_cast<int>(vp.left * size_.x);
|
||||
// OpenGL viewport origin is bottom-left, SFML is top-left
|
||||
int py = static_cast<int>((1.0f - vp.top - vp.height) * size_.y);
|
||||
int pw = static_cast<int>(vp.width * size_.x);
|
||||
int ph = static_cast<int>(vp.height * size_.y);
|
||||
glViewport(px, py, pw, ph);
|
||||
|
||||
// Set projection to map view center/size to the viewport
|
||||
auto center = view.getCenter();
|
||||
auto sz = view.getSize();
|
||||
float left = center.x - sz.x / 2.0f;
|
||||
float right = center.x + sz.x / 2.0f;
|
||||
float top = center.y - sz.y / 2.0f;
|
||||
float bottom = center.y + sz.y / 2.0f;
|
||||
SDL2Renderer::getInstance().setProjection(left, right, bottom, top);
|
||||
}
|
||||
|
||||
IntRect RenderTarget::getViewport(const View& view) const {
|
||||
auto vp = view.getViewport();
|
||||
return IntRect(
|
||||
static_cast<int>(vp.left * size_.x),
|
||||
static_cast<int>(vp.top * size_.y),
|
||||
static_cast<int>(vp.width * size_.x),
|
||||
static_cast<int>(vp.height * size_.y)
|
||||
);
|
||||
}
|
||||
|
||||
Vector2f RenderTarget::mapPixelToCoords(const Vector2i& point) const {
|
||||
return mapPixelToCoords(point, view_);
|
||||
}
|
||||
|
||||
Vector2f RenderTarget::mapPixelToCoords(const Vector2i& point, const View& view) const {
|
||||
// Convert pixel position to world coordinates through the view
|
||||
auto viewport = getViewport(view);
|
||||
auto center = view.getCenter();
|
||||
auto sz = view.getSize();
|
||||
|
||||
// Normalize point within viewport (0-1)
|
||||
float nx = (static_cast<float>(point.x) - viewport.left) / viewport.width;
|
||||
float ny = (static_cast<float>(point.y) - viewport.top) / viewport.height;
|
||||
|
||||
// Map to view coordinates
|
||||
return Vector2f(
|
||||
center.x + sz.x * (nx - 0.5f),
|
||||
center.y + sz.y * (ny - 0.5f)
|
||||
);
|
||||
}
|
||||
|
||||
Vector2i RenderTarget::mapCoordsToPixel(const Vector2f& point) const {
|
||||
return mapCoordsToPixel(point, view_);
|
||||
}
|
||||
|
||||
Vector2i RenderTarget::mapCoordsToPixel(const Vector2f& point, const View& view) const {
|
||||
// Convert world coordinates to pixel position through the view
|
||||
auto viewport = getViewport(view);
|
||||
auto center = view.getCenter();
|
||||
auto sz = view.getSize();
|
||||
|
||||
// Normalize within view (0-1)
|
||||
float nx = (point.x - center.x) / sz.x + 0.5f;
|
||||
float ny = (point.y - center.y) / sz.y + 0.5f;
|
||||
|
||||
// Map to pixel coordinates within viewport
|
||||
return Vector2i(
|
||||
static_cast<int>(viewport.left + nx * viewport.width),
|
||||
static_cast<int>(viewport.top + ny * viewport.height)
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// RenderTexture Implementation
|
||||
// =============================================================================
|
||||
|
|
|
|||
|
|
@ -821,16 +821,16 @@ public:
|
|||
void draw(const Vertex* vertices, size_t vertexCount, PrimitiveType type, const RenderStates& states = RenderStates::Default);
|
||||
void draw(const VertexArray& vertices, const RenderStates& states = RenderStates::Default);
|
||||
|
||||
void setView(const View& view) { view_ = view; }
|
||||
void setView(const View& view); // Implemented in SDL2Renderer.cpp - applies glViewport + projection
|
||||
const View& getView() const { return view_; }
|
||||
const View& getDefaultView() const { return defaultView_; }
|
||||
|
||||
IntRect getViewport(const View& view) const { return IntRect(0, 0, size_.x, size_.y); }
|
||||
IntRect getViewport(const View& view) const; // Implemented in SDL2Renderer.cpp
|
||||
|
||||
Vector2f mapPixelToCoords(const Vector2i& point) const { return Vector2f(static_cast<float>(point.x), static_cast<float>(point.y)); }
|
||||
Vector2f mapPixelToCoords(const Vector2i& point, const View& view) const { return Vector2f(static_cast<float>(point.x), static_cast<float>(point.y)); }
|
||||
Vector2i mapCoordsToPixel(const Vector2f& point) const { return Vector2i(static_cast<int>(point.x), static_cast<int>(point.y)); }
|
||||
Vector2i mapCoordsToPixel(const Vector2f& point, const View& view) const { return Vector2i(static_cast<int>(point.x), static_cast<int>(point.y)); }
|
||||
Vector2f mapPixelToCoords(const Vector2i& point) const; // Implemented in SDL2Renderer.cpp
|
||||
Vector2f mapPixelToCoords(const Vector2i& point, const View& view) const; // Implemented in SDL2Renderer.cpp
|
||||
Vector2i mapCoordsToPixel(const Vector2f& point) const; // Implemented in SDL2Renderer.cpp
|
||||
Vector2i mapCoordsToPixel(const Vector2f& point, const View& view) const; // Implemented in SDL2Renderer.cpp
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
|
|
|
|||
138
src/shell_game.html
Normal file
138
src/shell_game.html
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=no">
|
||||
<title>McRogueFace</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body {
|
||||
width: 100%; height: 100%; overflow: hidden;
|
||||
background: #000;
|
||||
}
|
||||
body {
|
||||
padding: env(safe-area-inset-top) env(safe-area-inset-right)
|
||||
env(safe-area-inset-bottom) env(safe-area-inset-left);
|
||||
}
|
||||
#canvas {
|
||||
display: block; position: absolute;
|
||||
top: 0; left: 0; width: 100%; height: 100%;
|
||||
image-rendering: pixelated;
|
||||
image-rendering: crisp-edges;
|
||||
-ms-interpolation-mode: nearest-neighbor;
|
||||
outline: none;
|
||||
}
|
||||
.loading {
|
||||
position: fixed; inset: 0; display: flex;
|
||||
flex-direction: column; justify-content: center;
|
||||
align-items: center; background: #000; color: #eee;
|
||||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||
z-index: 10;
|
||||
}
|
||||
.loading.hidden { display: none; }
|
||||
.spinner {
|
||||
width: 50px; height: 50px; border: 5px solid #333;
|
||||
border-top-color: #e94560; border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
#status {
|
||||
margin-top: 15px;
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="canvas" oncontextmenu="event.preventDefault()" tabindex="-1"></canvas>
|
||||
<div class="loading" id="loading">
|
||||
<div class="spinner"></div>
|
||||
<p id="status">Loading...</p>
|
||||
</div>
|
||||
<script>
|
||||
var canvasElement = document.getElementById('canvas');
|
||||
var loadingElement = document.getElementById('loading');
|
||||
var statusElement = document.getElementById('status');
|
||||
|
||||
var runtimeReady = false;
|
||||
|
||||
function updateCanvasSize() {
|
||||
var w = Math.floor(window.innerWidth);
|
||||
var h = Math.floor(window.innerHeight);
|
||||
if (runtimeReady) {
|
||||
// Notify C++ engine, which updates SDL + viewport system
|
||||
Module.ccall('notify_canvas_resize', null, ['number', 'number'], [w, h]);
|
||||
} else {
|
||||
// Before runtime init, set canvas backing buffer directly
|
||||
canvasElement.width = w;
|
||||
canvasElement.height = h;
|
||||
}
|
||||
}
|
||||
updateCanvasSize();
|
||||
window.addEventListener('resize', updateCanvasSize);
|
||||
|
||||
// Focus canvas on click
|
||||
canvasElement.addEventListener('click', function() {
|
||||
canvasElement.focus();
|
||||
});
|
||||
canvasElement.addEventListener('mousedown', function() {
|
||||
if (document.activeElement !== canvasElement) {
|
||||
canvasElement.focus();
|
||||
}
|
||||
});
|
||||
|
||||
var Module = {
|
||||
print: function(text) {
|
||||
if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
|
||||
console.log(text);
|
||||
},
|
||||
printErr: function(text) {
|
||||
if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
|
||||
console.error(text);
|
||||
},
|
||||
canvas: canvasElement,
|
||||
setStatus: function(text) {
|
||||
if (!Module.setStatus.last) Module.setStatus.last = { time: Date.now(), text: '' };
|
||||
if (text === Module.setStatus.last.text) return;
|
||||
var m = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/);
|
||||
var now = Date.now();
|
||||
if (m && now - Module.setStatus.last.time < 30) return;
|
||||
Module.setStatus.last.time = now;
|
||||
Module.setStatus.last.text = text;
|
||||
if (m) {
|
||||
text = m[1];
|
||||
}
|
||||
statusElement.textContent = text;
|
||||
},
|
||||
totalDependencies: 0,
|
||||
monitorRunDependencies: function(left) {
|
||||
this.totalDependencies = Math.max(this.totalDependencies, left);
|
||||
Module.setStatus(left ? 'Preparing... (' + (this.totalDependencies - left) + '/' + this.totalDependencies + ')' : 'All downloads complete.');
|
||||
},
|
||||
onRuntimeInitialized: function() {
|
||||
runtimeReady = true;
|
||||
loadingElement.classList.add('hidden');
|
||||
canvasElement.focus();
|
||||
// Trigger resize so C++ picks up the actual canvas dimensions
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
}
|
||||
};
|
||||
|
||||
Module.setStatus('Downloading...');
|
||||
|
||||
window.onerror = function(event) {
|
||||
Module.setStatus('Error! See console.');
|
||||
};
|
||||
|
||||
if (typeof resolveGlobalSymbol === 'undefined') {
|
||||
window.resolveGlobalSymbol = function(name, direct) {
|
||||
return {
|
||||
sym: Module['_' + name] || Module[name],
|
||||
type: 'function'
|
||||
};
|
||||
};
|
||||
}
|
||||
</script>
|
||||
{{{ SCRIPT }}}
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue