A native (browser-free) Scratch 3.0 runtime for LÖVE (Love2D)
Join our Discord community to discuss, share projects, and get help!
ScratchLove is a native reimplementation of the Scratch 3.0 runtime in Lua for the LÖVE framework. Created to break free from browser limitations, it delivers a superior native experience with access to hardware sensors, haptic feedback, and fine-grained performance control unconstrained by browser sandboxing. It supports a wide range of platforms (desktop, mobile) and devices including embedded Linux systems, gaming consoles, and handheld gaming devices.
- 🎮 Full Scratch Compatibility: Nearly 100% compatible with Scratch 3.0 .sb3 format
- 📱 Cross-Platform: Powered by Love2D's cross-platform capabilities, supports desktop and mobile operating systems
- ⚡ High Performance: Scratch blocks compiled to native Lua code, powered by LuaJIT for near-native execution speed
- IR-Based Compiler: Three-stage compilation (IR generation → optimization → Lua codegen)
- Optimization Passes: Constant folding, dead code elimination, control flow analysis
- Cooperative Threading: Efficient coroutine-based execution for concurrent scripts
- Lazy Loading: Costume textures load on-demand rather than all at startup, significantly reducing initial load time and memory footprint
- Automatic Cleanup: Time-based LRU cache automatically releases unused costume textures after configurable inactivity period
- Visual Effects: Color, fisheye, whirl, pixelate, mosaic, brightness, ghost
- SVG Rendering: Native SVG support via resvg library with intelligent caching
- Collision Detection: Color-based collision detection consistent with Scratch behavior
- Project Options: Compatible with TurboWarp project configuration (custom framerate, stage dimensions, runtime options like fencing, miscLimits, maxClones)
- Online Loading: Direct loading from scratch.mit.edu by project ID
- Pen Extension: Fully supported with GPU-accelerated rendering ✅
- Custom Reporters Extension: Fully supported ✅
- Custom procedures with return values (
procedures_return) - Reporter-style procedure calls (
procedures_call) - Compatible with TurboWarp custom reporters
- Custom procedures with return values (
- Text-to-Speech Extension: Supported with async HTTPS implementation ✅
- All 5 voices (alto, tenor, squeak, giant, kitten)
- 44 language locales matching Scratch
- Non-blocking async synthesis using background threads
- Limitation: Uses single worker thread - multiple concurrent requests are queued and processed serially (unlike native Scratch which processes them in parallel)
- Note: Requires lua-https to be compiled for HTTPS support
- Extensions: Only Pen, Custom Reporters, and Text-to-Speech extensions are supported. Other Scratch extensions (Music, Video Sensing, Translate, etc.) are not available as ScratchLove cannot run JavaScript-based extensions
- User Input: The "ask and wait" block is not yet supported (user input not implemented)
- Microphone Input: The loudness sensing block always returns 0 (microphone input not implemented)
- Cloud Variables: Only basic local storage is supported
- LÖVE (Love2D) 11.x: Download here
- LuaJIT (optional, for running tests): Included with LÖVE
# Clone the repository (with submodules)
git clone --recurse-submodules https://github.com/fox2d-engine/ScratchLove.git
cd ScratchLove
# If you already cloned without --recurse-submodules, initialize submodules:
git submodule update --init --recursive
# Run with a local .sb3 file
love . path/to/project.sb3
# Or run with a Scratch project ID (downloads from scratch.mit.edu)
love . 276932192
# Or drag & drop a .sb3 file into the windowScratchLove includes lua-https as a submodule for HTTPS support. This module must be compiled to enable:
- Online project loading from scratch.mit.edu
- Text-to-Speech extension (requires HTTPS API calls to Scratch synthesis service)
To compile lua-https:
- See detailed compilation instructions in lib/lua-https/readme.md
- The compiled library can be left in
lib/lua-https/src/(ScratchLove will automatically find it there) - Alternatively, copy it to a system Lua path like
/usr/local/lib/lua/5.1/or~/.luarocks/lib/lua/5.1/
Note: Without lua-https, online loading and Text-to-Speech will not work, but local .sb3 files can still be loaded and run normally.
ScratchLove includes an enhanced version of lua-cjson as a submodule, which provides 4-9x faster JSON parsing compared to the pure Lua implementation. By default, ScratchLove will fall back to a pure Lua JSON parser if the compiled library is not available.
To build lua-cjson for improved performance:
# Option 1: Build with LuaRocks (recommended)
cd lib/lua-cjson
luarocks --lua-version 5.1 make lua-cjson-local-1.rockspec
# Option 2: Build with Make (for development)
cd lib/lua-cjson
make clean && make
# The compiled cjson.so will be installed to ~/.luarocks/lib/lua/5.1/ (Option 1)
# or lib/lua-cjson/cjson.so (Option 2)Requirements for building:
- LuaJIT 2.1+ (included with LÖVE)
- LuaRocks (for Option 1)
- GCC or Clang compiler
- Development headers for LuaJIT
Note: Building lua-cjson is optional. ScratchLove will work perfectly fine with the pure Lua fallback, though JSON parsing will be slower for large projects.
# Run all tests
luajit tests/run.lua
# Run specific test category
luajit tests/run.lua control
luajit tests/run.lua data
luajit tests/run.lua motionScratchLove leverages LÖVE's cross-platform capabilities, making it possible to distribute your Scratch projects to various platforms including Windows, macOS, Linux, Android, iOS, and handheld gaming devices.
To package and distribute ScratchLove for different platforms:
- Follow the LÖVE Game Distribution Guide for platform-specific packaging instructions
- Place your .sb3 project file at
assets/game.sb3within the ScratchLove directory, then package everything into a .love archive - Build platform-specific executables or packages as needed
This enables you to turn Scratch projects into standalone native applications for virtually any platform that LÖVE supports.
ScratchLove/
├── main.lua # Application entry point
├── conf.lua # LÖVE configuration
│
├── compiler/ # Compilation pipeline
│ ├── irgen.lua # IR generation from Scratch blocks
│ ├── iroptimizer.lua # IR optimization passes
│ ├── luagen.lua # Lua code generation from IR
│ └── blocks/ # Block-specific IR generators
│
├── vm/ # Virtual machine runtime
│ ├── runtime.lua # Main runtime orchestration
│ ├── thread.lua # Script execution threads
│ ├── sprite.lua # Sprite objects
│ └── stage.lua # Stage object
│
├── renderer/ # Rendering system
│ ├── renderer.lua # Main renderer
│ ├── shaders/ # GLSL shaders for visual effects
│ └── collision/ # Collision detection system
│
├── pen/ # Pen drawing system
├── audio/ # Audio system
├── loader/ # Project loading
├── parser/ # .sb3 parsing
├── ui/ # User interface
├── utils/ # Utility modules
├── resvg/ # SVG rendering (FFI bindings)
├── tests/ # Test suite
├── lib/ # Third-party libraries
└── assets/ # Fonts, gamepad mappings, etc.
# Install dependencies
# Love2D should be installed system-wide
# Run in development mode
love .
# Run with debug logging
env LOG_LEVEL=debug love . project.sb3# Skip compilation and use existing project.lua (for debugging compiled code)
env SKIP_COMPILE=1 love . project.sb3
# Run with timeout (useful for testing)
timeout --kill-after=3s 10s love . project.sb3-- Example test
local SB3Builder = require("tests.sb3_builder")
it("should execute motion blocks correctly", function()
SB3Builder.resetCounter()
local stage = SB3Builder.createStage()
local sprite = SB3Builder.createSprite("TestSprite")
local hatId, hatBlock = SB3Builder.Events.whenFlagClicked()
local moveId, moveBlock = SB3Builder.Motion.moveSteps(10)
SB3Builder.addBlock(sprite, hatId, hatBlock)
SB3Builder.addBlock(sprite, moveId, moveBlock)
SB3Builder.linkBlocks(sprite, {hatId, moveId})
local projectJson = SB3Builder.createProject({stage, sprite})
local project = ProjectModel:new(projectJson, {})
local runtime = Runtime:new(project)
runtime:initialize()
runtime:broadcastGreenFlag()
-- Execute with safety limits
local maxIterations = 100
local iterations = 0
while #runtime:getActiveThreads() > 0 and iterations < maxIterations do
runtime:update(1/60)
iterations = iterations + 1
end
expect(runtime:getSpriteTargetByName("TestSprite").x).to.equal(10)
end)- Type Annotations: All code uses LuaLS type annotations for IDE support
- Testing: New features must include test coverage
- Documentation: Functions should have clear docstrings
- Code Style: Follow existing Lua conventions in the codebase
We welcome contributions! Here's how you can help:
- 🐛 Report bugs: Open an issue with reproduction steps
- 💡 Suggest features: Share ideas for improvements
- 📝 Improve docs: Help clarify documentation
- 🧪 Write tests: Increase test coverage
- 🔧 Fix issues: Submit pull requests
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Write tests for your changes
- Implement your changes
- Run the test suite (
luajit tests/run.lua) - Commit with clear messages (
git commit -m 'Add amazing feature') - Push to your fork (
git push origin feature/amazing-feature) - Open a Pull Request
- Use clear, descriptive commit messages
- Reference issues where applicable (
Fixes #123) - Keep commits atomic and focused
- Follow conventional commits format when possible
Copyright © 2025 Fox2D.com. All rights reserved.
This project is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0)
We are deeply grateful to the TurboWarp project and team. TurboWarp's innovative compiler architecture served as an invaluable reference during ScratchLove's development, helping us understand the nuances of Scratch VM behavior and optimization techniques. For running Scratch projects in the browser, we highly recommend using TurboWarp.
- Scratch Team: For creating an amazing platform that inspires creativity and learning
- LÖVE: For the excellent Lua game framework
- All contributors and users who have helped improve this project





