Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 3 additions & 21 deletions arcade/gl/backends/webgl/framebuffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,21 +256,15 @@ def __init__(self, ctx: WebGLContext):

@DefaultFrameBuffer.viewport.setter
def viewport(self, value: tuple[int, int, int, int]):
# This is the exact same as the WebGLFramebuffer setter
# This is very similar to the OpenGL backend setter
# WebGL backend doesn't need to handle pixel scaling for the
# default framebuffer like desktop does, the browser does that
# for us. However we need a separate implementation for the
# function because of ABC
if not isinstance(value, tuple) or len(value) != 4:
raise ValueError("viewport shouldbe a 4-component tuple")

ratio = self.ctx.window.get_pixel_ratio()
self._viewport = (
int(value[0] * ratio),
int(value[1] * ratio),
int(value[2] * ratio),
int(value[3] * ratio),
)
self._viewport = value

if self._ctx.active_framebuffer == self:
self._ctx._gl.viewport(*self._viewport)
Expand All @@ -281,23 +275,11 @@ def viewport(self, value: tuple[int, int, int, int]):

@DefaultFrameBuffer.scissor.setter
def scissor(self, value):
# This is the exact same as the WebGLFramebuffer setter
# WebGL backend doesn't need to handle pixel scaling for the
# default framebuffer like desktop does, the browser does that
# for us. However we need a separate implementation for the
# function because of ABC
if value is None:
self._scissor = None
if self._ctx.active_framebuffer == self:
self._ctx._gl.scissor(*self._viewport)
else:
ratio = self.ctx.window.get_pixel_ratio()
self._scissor = (
int(value[0] * ratio),
int(value[1] * ratio),
int(value[2] * ratio),
int(value[3] * ratio),
)

self._scissor = value
if self._ctx.active_framebuffer == self:
self._ctx._gl.scissor(*self._scissor)
26 changes: 19 additions & 7 deletions webplayground/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,30 @@ An http server is provided with the `server.py` file. This file can be run with

The index page will provide a list of all Arcade examples. This is generated dynamically on the fly when the page is loaded, and will show all examples in the `arcade.examples` package. This generates links which can be followed to open any example in the browser.

There are some pre-requesites to running this server. It assumes that you have the `development` branch of Pyglet
checked out and in a folder named `pyglet` directly next to your Arcade repo directory. You will also need to have
the `build` and `flit` packages from PyPi installed. These are used by Pyglet and Arcade to build wheel files,
but are not generally installed for local development.
## Testing Local Scripts

Assuming you have Pyglet ready to go, you can then start the server. It will build wheels for both Pyglet and Arcade, and copy them
into this directory. This means that if you make any, you will need to restart this server in order to build new wheels.
You can now test your own local scripts **without restarting the server**!

1. Navigate to `http://localhost:8000/local` in your browser
2. Place your Python scripts in the `local_scripts/` directory
3. Scripts should have a `main()` function as the entry point
4. The page will automatically list all `.py` files in that directory
5. Click any script to run it in the browser
6. Edit your scripts and refresh the browser page to see changes - no server restart needed!

See `local_scripts/README.md` and `local_scripts/example_test.py` for more details and examples.

## Prerequisites

You will need to have `uv` installed to build the Arcade wheel. You can install it with:

When you start the server, it will automatically build an Arcade wheel and copy it into this directory.
This means that if you make any changes to Arcade code, you will need to restart the server to build a new wheel with your changes.

## How does this work?

The web server itself is built with a nice little HTTP server library named [Bottle](https://github.com/bottlepy/bottle). We need to run an HTTP server locally
to load anything into WASM in the browser, it will not work if we just server it files directly due to browser security constraints. For the Arcade examples specifically,
to load anything into WASM in the browser, as it will not work if we just serve files directly due to browser security constraints. For the Arcade examples specifically,
we are taking advantage of the fact that the example code is packaged directly inside of Arcade to enable executing them in the browser.

If we need to add extra code that is not part of the Arcade package, that will require extension of this server to handle packaging it properly for loading into WASM, and then
Expand Down
23 changes: 23 additions & 0 deletions webplayground/index.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,32 @@

<head>
<title>Arcade Examples</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
}
.local-link {
background-color: #4caf50;
color: white;
padding: 10px 20px;
text-decoration: none;
border-radius: 5px;
display: inline-block;
margin-bottom: 20px;
}
.local-link:hover {
background-color: #45a049;
}
</style>
</head>

<body>
<h1>Arcade Examples</h1>
<a href="/local" class="local-link">🧪 Test Local Scripts</a>
<h2>Built-in Examples:</h2>
<ul>
% for item in examples:
<li><a href="/example/{{item}}">{{item}}</a></li>
Expand Down
75 changes: 75 additions & 0 deletions webplayground/local.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<!DOCTYPE html>
<html>

<head>
<title>Local Scripts</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
}
h1 {
color: #333;
}
.info {
background-color: #f0f0f0;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
ul {
list-style-type: none;
padding: 0;
}
li {
margin: 10px 0;
}
a {
color: #0066cc;
text-decoration: none;
font-size: 16px;
}
a:hover {
text-decoration: underline;
}
.back-link {
margin-top: 30px;
display: block;
}
code {
background-color: #f4f4f4;
padding: 2px 6px;
border-radius: 3px;
font-family: monospace;
}
</style>
</head>

<body>
<h1>Local Test Scripts</h1>

<div class="info">
<p><strong>How to use:</strong></p>
<p>Place your Python scripts in the <code>webplayground/local_scripts/</code> directory.</p>
<p>Scripts should have a <code>main()</code> function that will be called when loaded.</p>
<p>No need to restart the server - just refresh this page to see new scripts!</p>
</div>

% if scripts:
<h2>Available Scripts:</h2>
<ul>
% for script in scripts:
<li><a href="/local/{{script}}">{{script}}</a></li>
% end
</ul>
% else:
<p>No local scripts found. Add .py files to the <code>local_scripts/</code> directory.</p>
% end

<a href="/" class="back-link">← Back to Examples</a>
</body>

</html>

80 changes: 80 additions & 0 deletions webplayground/local_run.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<!DOCTYPE html>
<html>

<head>
<title>{{script_name}}</title>
<script src="https://cdn.jsdelivr.net/pyodide/v0.29.0/full/pyodide.js"></script>
<style>
body {
margin: 0;
padding: 0;
}
#status {
position: fixed;
top: 10px;
right: 10px;
background-color: #f0f0f0;
padding: 10px;
border-radius: 5px;
font-family: Arial, sans-serif;
z-index: 1000;
}
.loading {
color: #ff9800;
}
.ready {
color: #4caf50;
}
.error {
color: #f44336;
}
</style>
</head>

<body>
<div id="status" class="loading">Loading...</div>
<script type="text/javascript">
async function main() {
const statusDiv = document.getElementById('status');

try {
statusDiv.textContent = 'Loading Pyodide...';
let pyodide = await loadPyodide();

statusDiv.textContent = 'Installing packages...';
await pyodide.loadPackage("micropip");
const micropip = pyodide.pyimport("micropip");
await pyodide.loadPackage("pillow");
await micropip.install("http://localhost:8000/static/{{arcade_wheel}}", pre=true);

statusDiv.textContent = 'Loading script...';

// Fetch the script content with cache-busting timestamp
const timestamp = new Date().getTime();
const response = await fetch("/local_scripts/{{script_name}}.py?t=" + timestamp);
const scriptContent = await response.text();

statusDiv.textContent = 'Running script...';
statusDiv.className = 'ready';

// Execute the script content (this will run the if __name__ == "__main__" block)
pyodide.runPython(scriptContent);


statusDiv.textContent = 'Ready';
setTimeout(() => {
statusDiv.style.display = 'none';
}, 2000);

} catch (error) {
statusDiv.textContent = 'Error: ' + error.message;
statusDiv.className = 'error';
console.error(error);
}
}
main();
</script>
</body>

</html>

59 changes: 59 additions & 0 deletions webplayground/local_scripts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Local Scripts Directory

This directory is for testing your own Arcade scripts in the web environment without restarting the server.

## How to Use

1. Place your Python script (`.py` file) in this directory
2. Write your script using the standard `if __name__ == "__main__":` pattern (see example below)
3. Navigate to `http://localhost:8000/local` in your browser to see the list of available scripts
4. Click on any script to run it in the browser
5. Edit your script and refresh the browser page to see changes - **no server restart needed!**
- Scripts are never cached, so refreshing always loads the latest version
- No need for hard refresh (Ctrl+F5) - a simple refresh is enough

## Example Script Structure

```python
import arcade


class MyWindow(arcade.Window):
def __init__(self):
super().__init__(800, 600, "My Test")

def on_draw(self):
self.clear()
# Your drawing code here
arcade.draw_text(
"Hello World!",
self.width // 2,
self.height // 2,
arcade.color.WHITE,
font_size=24,
anchor_x="center"
)


# Standard Python entry point - this will run when the script is loaded
if __name__ == "__main__":
window = MyWindow()
arcade.run()
```

**Note:** The `if __name__ == "__main__":` block is important! Your script's code will execute when loaded in the browser, just like running it normally with Python.

## Tips

- Scripts are loaded dynamically, so you can add, remove, or edit them while the server is running
- Just refresh the `/local` page to see newly added scripts
- Use the browser console (F12) to see any Python errors or debug output
- The example_test.py file shows a simple working example

## What Gets Loaded

- All `.py` files in this directory will appear in the local scripts list
- The web interface will load the Arcade wheel and execute your script
- When your script runs, the `if __name__ == "__main__":` block executes, just like running Python locally
- Pyodide environment is used to run Python in the browser

43 changes: 43 additions & 0 deletions webplayground/local_scripts/example_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""
Example local test script for the Arcade webplayground.

Place this file (or your own scripts) in the webplayground/local_scripts/ directory
to test them in the browser without restarting the server.

Your script should use the standard Python pattern with if __name__ == "__main__":
"""

import arcade


class MyWindow(arcade.Window):
def __init__(self):
super().__init__(800, 600, "Local Test Script")
self.background_color = arcade.color.AMAZON

def on_draw(self):
self.clear()
arcade.draw_text(
"Hello from local script!",
self.width // 2,
self.height // 2,
arcade.color.WHITE,
font_size=30,
anchor_x="center",
anchor_y="center"
)
arcade.draw_text(
"Edit this file and refresh to see changes",
self.width // 2,
self.height // 2 - 50,
arcade.color.WHITE,
font_size=16,
anchor_x="center",
anchor_y="center"
)


if __name__ == "__main__":
window = MyWindow()
arcade.run()

Loading
Loading