Skip to main content

JavaScript Integration

Bilup supports powerful extension APIs and embedding workflows. Arbitrary “JavaScript blocks” are not part of Bilup; instead, use unsandboxed extensions for advanced behavior or the Packager to include custom JavaScript in standalone builds.

Understanding JavaScript in Bilup

What is supported?

  • Unsandboxed extensions: Write extensions that run in the editor context and interact with the VM, renderer, and runtime.
  • Packager custom JS: When exporting with the Packager, include custom JavaScript in the output for standalone deployments.
  • Embedding: Communicate with embedded projects via postMessage from the host page.

Security Model

  • Sandboxed extensions: Limited access; blocks may incur a per-frame delay.
  • Unsandboxed extensions: Full access to internal APIs; must remain stable and not block the main thread.
  • Packager JS: Runs in your packaged app’s context; follow CSP best practices.

Enabling Advanced Behavior

Unsandboxed Extensions

Load your extension via URL or register directly in an IIFE:

https://editor.bilup.org/?extension=https://example.com/extension.js

Packager

Use the Packager to inject custom JS into the exported app.

Security Notice

Unsandboxed extensions and Packager JS run with elevated privileges. Only load code you trust.

Unsandboxed Extension Patterns

Interacting with Scratch

Accessing Variables

Read and modify Scratch variables from an unsandboxed extension:

// Get variable value
const score = vm.runtime.getTargetForStage().variables[variableId].value;

// Set variable value
vm.runtime.getTargetForStage().variables[variableId].value = 100;

// Get variable by name
const scoreVar = vm.runtime.getTargetForStage().lookupVariableByNameAndType('score', '');
scoreVar.value = 200;

Controlling Sprites

Manipulate sprites programmatically:

// Get current sprite
const sprite = vm.runtime.getEditingTarget();

// Change position
sprite.setXY(100, 50);

// Change costume
sprite.setCostume(0);

// Change size
sprite.setSize(150);

// Set rotation
sprite.setDirection(90);

Broadcasting Events

Trigger Scratch broadcasts:

// Simple broadcast
vm.runtime.startHats('event_whenbroadcastreceived', {
BROADCAST_OPTION: 'my_event'
});

// Broadcast with data
vm.runtime.startHats('event_whenbroadcastreceived', {
BROADCAST_OPTION: 'data_received',
data: { score: 100, level: 5 }
});

Web API Integration (unsandboxed extensions)

Fetch API

Make HTTP requests:

// GET request
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);

// POST request
const response = await fetch('https://api.example.com/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Alice', score: 100 })
});

Local Storage

Persist data across sessions:

// Save data
localStorage.setItem('gameData', JSON.stringify({
highScore: 1000,
playerName: 'Alice',
level: 5
}));

// Load data
const gameData = JSON.parse(localStorage.getItem('gameData') || '{}');
const highScore = gameData.highScore || 0;

Geolocation

Access user location:

navigator.geolocation.getCurrentPosition(
(position) => {
const lat = position.coords.latitude;
const lon = position.coords.longitude;
console.log(`Location: ${lat}, ${lon}`);
},
(error) => {
console.error('Location access denied:', error);
}
);

Camera and Microphone

Access media devices:

// Get camera stream
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
const video = document.createElement('video');
video.srcObject = stream;
video.play();

// Get microphone stream
const audioStream = await navigator.mediaDevices.getUserMedia({ audio: true });

External Libraries (Packager or host page)

Including Libraries

Add external JavaScript libraries:

// Load library dynamically
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/chart.js';
document.head.appendChild(script);

// Wait for library to load
script.onload = () => {
// Library is now available
const chart = new Chart(ctx, config);
};

Chart.js for Data Visualization

// Create a chart
const ctx = document.createElement('canvas').getContext('2d');
const chart = new Chart(ctx, {
type: 'bar',
data: {
labels: ['Red', 'Blue', 'Yellow'],
datasets: [{
data: [12, 19, 3],
backgroundColor: ['red', 'blue', 'yellow']
}]
}
});

Three.js for 3D Graphics

// Create 3D scene
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();

// Add to page
document.body.appendChild(renderer.domElement);

Socket.io for Real-time Communication

// Connect to WebSocket server
const socket = io('https://your-server.com');

socket.on('connect', () => {
console.log('Connected to server');
});

socket.on('gameUpdate', (data) => {
// Handle real-time game updates
updateGameState(data);
});

Advanced Patterns

Custom Extensions

Create project-specific extensions:

class MyCustomExtension {
getInfo() {
return {
id: 'myextension',
name: 'My Custom Extension',
blocks: [
{
opcode: 'customBlock',
blockType: 'reporter',
text: 'custom calculation [NUM]',
arguments: {
NUM: { type: 'number', defaultValue: 10 }
}
}
]
};
}

customBlock(args) {
return Math.pow(args.NUM, 2) + 1;
}
}

// Register extension (unsandboxed)
Scratch.extensions.register(new MyCustomExtension());

Event System

Create custom event systems:

class EventManager {
constructor() {
this.listeners = {};
}

on(event, callback) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(callback);
}

emit(event, data) {
if (this.listeners[event]) {
this.listeners[event].forEach(callback => callback(data));
}
}
}

const events = new EventManager();

// Usage in Scratch
events.on('scoreUpdate', (score) => {
vm.runtime.getTargetForStage().lookupVariableByNameAndType('score', '').value = score;
});

Performance Monitoring

Monitor project performance:

class PerformanceMonitor {
constructor() {
this.startTime = performance.now();
this.frameCount = 0;
}

measureFrame() {
this.frameCount++;
const currentTime = performance.now();
const elapsed = currentTime - this.startTime;

if (elapsed >= 1000) {
const fps = this.frameCount / (elapsed / 1000);
console.log(`FPS: ${fps.toFixed(1)}`);

this.frameCount = 0;
this.startTime = currentTime;
}
}
}

const monitor = new PerformanceMonitor();
vm.runtime.on('PROJECT_RUN_START', () => {
monitor.measureFrame();
});

Integration Patterns

Block-JavaScript Bridge

Create seamless integration between blocks and JavaScript:

// Create bridge object
window.ScratchBridge = {
// Call from blocks: (call js function [bridge.calculate] with [10])
calculate: (input) => {
return Math.complex.calculation(input);
},

// Call from blocks: (call js function [bridge.saveData] with [data])
saveData: (data) => {
localStorage.setItem('projectData', JSON.stringify(data));
return 'saved';
},

// Call from blocks: (call js function [bridge.loadData])
loadData: () => {
return JSON.parse(localStorage.getItem('projectData') || '{}');
}
};

State Synchronization

Keep JavaScript and Scratch state synchronized:

class StateSynchronizer {
constructor(vm) {
this.vm = vm;
this.jsState = {};
this.setupWatchers();
}

setupWatchers() {
// Watch Scratch variables
this.vm.runtime.on('VARIABLE_CHANGED', (variable) => {
this.jsState[variable.name] = variable.value;
this.onStateChange(variable.name, variable.value);
});
}

updateScratchVariable(name, value) {
const variable = this.vm.runtime.getTargetForStage()
.lookupVariableByNameAndType(name, '');
if (variable) {
variable.value = value;
}
}

onStateChange(name, value) {
// Custom logic when state changes
console.log(`${name} changed to ${value}`);
}
}

Security Best Practices

Input Validation

Always validate data from external sources:

function validateInput(input) {
// Check type
if (typeof input !== 'string') return false;

// Check length
if (input.length > 1000) return false;

// Check for dangerous patterns
if (/<script|javascript:|data:/i.test(input)) return false;

return true;
}

Sanitization

Sanitize data before using it:

function sanitizeHTML(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}

Error Handling

Implement robust error handling:

try {
const result = riskyOperation();
return result;
} catch (error) {
console.error('Operation failed:', error);
return 'Error occurred';
}

Debugging JavaScript

Console Logging

Use console methods for debugging:

console.log('Debug info:', data);
console.warn('Warning message');
console.error('Error occurred');
console.table(arrayData);

Browser Developer Tools

  • F12: Open developer tools
  • Console Tab: View logs and execute code
  • Sources Tab: Set breakpoints and debug
  • Network Tab: Monitor API calls

Performance Profiling

Profile JavaScript performance:

console.time('operation');
// Your code here
console.timeEnd('operation');

JavaScript integration in Bilup opens up unlimited possibilities for creating sophisticated, interactive projects. Use these features responsibly and always consider security implications when working with custom code!