diff --git a/arcade/gl/backends/webgl/framebuffer.py b/arcade/gl/backends/webgl/framebuffer.py
index a5bf4a18c..a4d30975b 100644
--- a/arcade/gl/backends/webgl/framebuffer.py
+++ b/arcade/gl/backends/webgl/framebuffer.py
@@ -256,7 +256,7 @@ 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
@@ -264,13 +264,7 @@ def viewport(self, value: tuple[int, int, int, int]):
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)
@@ -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)
diff --git a/webplayground/README.md b/webplayground/README.md
index df6b4b0df..1491686e6 100644
--- a/webplayground/README.md
+++ b/webplayground/README.md
@@ -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
diff --git a/webplayground/index.tpl b/webplayground/index.tpl
index 4c5a8e11b..66e42fd74 100644
--- a/webplayground/index.tpl
+++ b/webplayground/index.tpl
@@ -3,9 +3,32 @@
Arcade Examples
+
+ Arcade Examples
+ 🧪 Test Local Scripts
+ Built-in Examples:
% for item in examples:
- {{item}}
diff --git a/webplayground/local.tpl b/webplayground/local.tpl
new file mode 100644
index 000000000..395ee0596
--- /dev/null
+++ b/webplayground/local.tpl
@@ -0,0 +1,75 @@
+
+
+
+
+ Local Scripts
+
+
+
+
+ Local Test Scripts
+
+
+
How to use:
+
Place your Python scripts in the webplayground/local_scripts/ directory.
+
Scripts should have a main() function that will be called when loaded.
+
No need to restart the server - just refresh this page to see new scripts!
+
+
+ % if scripts:
+ Available Scripts:
+
+ % else:
+ No local scripts found. Add .py files to the local_scripts/ directory.
+ % end
+
+ ← Back to Examples
+
+
+
+
diff --git a/webplayground/local_run.tpl b/webplayground/local_run.tpl
new file mode 100644
index 000000000..5081809a8
--- /dev/null
+++ b/webplayground/local_run.tpl
@@ -0,0 +1,80 @@
+
+
+
+
+ {{script_name}}
+
+
+
+
+
+ Loading...
+
+
+
+
+
diff --git a/webplayground/local_scripts/README.md b/webplayground/local_scripts/README.md
new file mode 100644
index 000000000..0c998b449
--- /dev/null
+++ b/webplayground/local_scripts/README.md
@@ -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
+
diff --git a/webplayground/local_scripts/example_test.py b/webplayground/local_scripts/example_test.py
new file mode 100644
index 000000000..a2a70650b
--- /dev/null
+++ b/webplayground/local_scripts/example_test.py
@@ -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()
+
diff --git a/webplayground/server.py b/webplayground/server.py
index 1a4b1c756..742e4cec7 100644
--- a/webplayground/server.py
+++ b/webplayground/server.py
@@ -8,16 +8,25 @@
import sys
from pathlib import Path
+import bottle
from bottle import route, run, static_file, template # type: ignore
from arcade import examples
+# Disable template caching for development
+bottle.TEMPLATES.clear()
+bottle.debug(True)
+
here = Path(__file__).parent.resolve()
path_arcade = Path("../")
arcade_wheel_filename = "arcade-4.0.0.dev1-py3-none-any.whl"
path_arcade_wheel = path_arcade / "dist" / arcade_wheel_filename
+# Directory for local test scripts
+local_scripts_dir = here / "local_scripts"
+local_scripts_dir.mkdir(exist_ok=True)
+
def find_modules(module):
path_list = []
@@ -34,7 +43,7 @@ def find_modules(module):
return path_list
-@route("/static/")
+@route(r"/static/")
def whl(filepath):
return static_file(filepath, root="./")
@@ -55,6 +64,37 @@ def example(name="platform_tutorial.01_open_window"):
)
+@route("/local")
+def local_index():
+ """List available local scripts"""
+ local_scripts = []
+ if local_scripts_dir.exists():
+ for script in local_scripts_dir.glob("*.py"):
+ local_scripts.append(script.stem)
+ return template("local.tpl", scripts=local_scripts)
+
+
+@route("/local/")
+def local_script(script_name):
+ """Run a local script"""
+ return template(
+ "local_run.tpl",
+ script_name=script_name,
+ arcade_wheel=arcade_wheel_filename,
+ )
+
+
+@route("/local_scripts/")
+def serve_local_script(filename):
+ """Serve local script files with no-cache headers"""
+ from bottle import response
+
+ response.set_header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
+ response.set_header("Pragma", "no-cache")
+ response.set_header("Expires", "0")
+ return static_file(filename, root=local_scripts_dir)
+
+
def main():
# Get us in this file's parent directory
os.chdir(here)