MediaWiki:Gadget-WebGL2.js
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
$(function() {
'use strict';
/**
* Fetch a scene script from the WebGL: namespace
*/
function fetchScene(sceneName) {
return $.ajax({
url: mw.util.wikiScript('api'),
data: {
action: 'query',
titles: 'WebGL:' + sceneName,
prop: 'revisions',
rvprop: 'content',
rvslots: 'main',
format: 'json'
}
}).then(function(response) {
var pages = response.query.pages;
var pageId = Object.keys(pages)[0];
if (pageId === '-1') {
throw new Error('Scene not found: WebGL:' + sceneName);
}
return pages[pageId].revisions[0].slots.main['*'];
});
}
/**
* Create a scene context object with lifecycle methods
*/
function createSceneContext(canvas, gl) {
return {
canvas: canvas,
gl: gl,
init: null,
render: null,
resize: null,
cleanup: null,
_running: false,
_animationId: null,
_resizeObserver: null
};
}
/**
* Execute a scene script in instance mode
*/
function executeScene(scriptText, context) {
try {
var factory = new Function('scene', scriptText);
factory(context);
} catch (e) {
console.error('WebGL scene execution error:', e);
throw e;
}
}
/**
* Start the render loop for a scene
*/
function startRenderLoop(context, gl) {
if (!context.render) return;
context._running = true;
var startTime = performance.now();
function loop(timestamp) {
if (!context._running) return;
var elapsed = (timestamp - startTime) / 1000;
context.render(gl, elapsed);
context._animationId = requestAnimationFrame(loop);
}
context._animationId = requestAnimationFrame(loop);
}
/**
* Check if a value is a percentage string
*/
function isPercentage(value) {
return typeof value === 'string' && value.indexOf('%') !== -1;
}
/**
* Sync canvas buffer size with display size
*/
function syncCanvasSize(canvas, gl, context) {
var displayWidth = canvas.clientWidth;
var displayHeight = canvas.clientHeight;
if (canvas.width !== displayWidth || canvas.height !== displayHeight) {
canvas.width = displayWidth;
canvas.height = displayHeight;
gl.viewport(0, 0, displayWidth, displayHeight);
if (context.resize) {
context.resize(gl, displayWidth, displayHeight);
}
}
}
/**
* Initialize a WebGL mount point
*/
function initMount(mount) {
var $mount = $(mount);
var sceneName = $mount.data('scene');
if (!sceneName) {
console.warn('WebGL mount missing data-scene attribute');
return;
}
var widthVal = $mount.data('width');
var heightVal = $mount.data('height');
var usePercentage = isPercentage(widthVal) || isPercentage(heightVal);
var canvas = document.createElement('canvas');
canvas.className = 'webgl-canvas';
if (usePercentage) {
// Apply as CSS for percentage sizing
canvas.style.width = widthVal || '100%';
canvas.style.height = heightVal || '100%';
canvas.style.display = 'block';
} else {
// Direct pixel values
canvas.width = widthVal || 800;
canvas.height = heightVal || 600;
}
$mount.append(canvas);
// For percentage sizing, set initial buffer size after append
if (usePercentage) {
canvas.width = canvas.clientWidth || 800;
canvas.height = canvas.clientHeight || 600;
}
var gl = canvas.getContext('webgl2');
if (!gl) {
$mount.append('<p class="webgl-error">WebGL2 not supported</p>');
return;
}
var context = createSceneContext(canvas, gl);
fetchScene(sceneName)
.then(function(scriptText) {
executeScene(scriptText, context);
if (context.init) {
context.init(gl, canvas);
}
startRenderLoop(context, gl);
// Handle resize
if (usePercentage) {
// Use ResizeObserver for container-based resizing
context._resizeObserver = new ResizeObserver(function() {
syncCanvasSize(canvas, gl, context);
});
context._resizeObserver.observe(canvas);
} else if (context.resize) {
$(window).on('resize.webgl-' + sceneName, function() {
context.resize(gl, canvas.width, canvas.height);
});
}
})
.catch(function(err) {
console.error('Failed to load scene:', sceneName, err);
$mount.append('<p class="webgl-error">Failed to load scene: ' + sceneName + '</p>');
});
// Cleanup when element is removed
$mount.on('remove', function() {
context._running = false;
if (context._animationId) {
cancelAnimationFrame(context._animationId);
}
if (context._resizeObserver) {
context._resizeObserver.disconnect();
}
if (context.cleanup) {
context.cleanup(gl);
}
$(window).off('resize.webgl-' + sceneName);
});
}
// Initialize all mount points
$('.webgl-mount').each(function() {
initMount(this);
});
});