MediaWiki:Gadget-WebGL2.js: Difference between revisions
No edit summary |
Add scene.container for access to mount div (via update-page on MediaWiki MCP Server) |
||
| (5 intermediate revisions by the same user not shown) | |||
| Line 1: | Line 1: | ||
$(function() { | $(function() { | ||
console. | 'use strict'; | ||
var DEFAULT_WIDTH = 800; | |||
var mount = $( | var DEFAULT_HEIGHT = 600; | ||
var scene = mount.data(' | |||
/** | |||
* 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(container, canvas, gl) { | |||
return { | |||
container: container, | |||
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); | |||
} | |||
/** | |||
* Parse dimension value - returns { value, isPercent, pixels } | |||
*/ | |||
function parseDimension(val, defaultPx) { | |||
if (val === undefined || val === null || val === '') { | |||
return { value: defaultPx, isPercent: false, pixels: defaultPx }; | |||
} | |||
var str = String(val); | |||
var isPercent = str.indexOf('%') !== -1; | |||
var pixels = parseInt(str, 10) || defaultPx; | |||
return { value: str, isPercent: isPercent, pixels: pixels }; | |||
} | |||
/** | |||
* Sync canvas buffer size with display size | |||
*/ | |||
function syncCanvasSize(canvas, gl, context, minWidth, minHeight) { | |||
var displayWidth = Math.max(canvas.clientWidth, minWidth); | |||
var displayHeight = Math.max(canvas.clientHeight, minHeight); | |||
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 width = parseDimension($mount.data('width'), DEFAULT_WIDTH); | |||
var height = parseDimension($mount.data('height'), DEFAULT_HEIGHT); | |||
var usePercentage = width.isPercent || height.isPercent; | |||
// Explicit min dimensions override auto-calculated ones | |||
var minWidth = parseInt($mount.data('min-width'), 10) || (width.isPercent ? DEFAULT_WIDTH : width.pixels); | |||
var minHeight = parseInt($mount.data('min-height'), 10) || (height.isPercent ? DEFAULT_HEIGHT : height.pixels); | |||
// Ensure mount container has dimensions for percentage sizing | |||
if (usePercentage) { | |||
$mount.css({ | |||
'display': 'block', | |||
'min-width': minWidth + 'px', | |||
'min-height': minHeight + 'px' | |||
}); | |||
} | |||
var canvas = document.createElement('canvas'); | var canvas = document.createElement('canvas'); | ||
canvas.className = 'webgl-canvas'; | canvas.className = 'webgl-canvas'; | ||
mount.append(canvas); | |||
if (usePercentage) { | |||
// | canvas.style.width = width.isPercent ? width.value : width.value + 'px'; | ||
canvas.style.height = height.isPercent ? height.value : height.value + 'px'; | |||
canvas.style.display = 'block'; | |||
} else { | |||
canvas.width = width.pixels; | |||
canvas.height = height.pixels; | |||
} | |||
$mount.append(canvas); | |||
// For percentage sizing, set initial buffer size after append | |||
if (usePercentage) { | |||
canvas.width = Math.max(canvas.clientWidth, minWidth); | |||
canvas.height = Math.max(canvas.clientHeight, minHeight); | |||
} | |||
var gl = canvas.getContext('webgl2'); | |||
if (!gl) { | |||
$mount.append('<p class="webgl-error">WebGL2 not supported</p>'); | |||
return; | |||
} | |||
var context = createSceneContext(mount, canvas, gl); | |||
fetchScene(sceneName) | |||
.then(function(scriptText) { | |||
executeScene(scriptText, context); | |||
if (context.init) { | |||
context.init(gl, canvas); | |||
} | |||
startRenderLoop(context, gl); | |||
// Handle resize | |||
if (usePercentage) { | |||
context._resizeObserver = new ResizeObserver(function() { | |||
syncCanvasSize(canvas, gl, context, minWidth, minHeight); | |||
}); | |||
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); | |||
}); | }); | ||
}); | }); | ||
Latest revision as of 15:53, 1 February 2026
$(function() {
'use strict';
var DEFAULT_WIDTH = 800;
var DEFAULT_HEIGHT = 600;
/**
* 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(container, canvas, gl) {
return {
container: container,
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);
}
/**
* Parse dimension value - returns { value, isPercent, pixels }
*/
function parseDimension(val, defaultPx) {
if (val === undefined || val === null || val === '') {
return { value: defaultPx, isPercent: false, pixels: defaultPx };
}
var str = String(val);
var isPercent = str.indexOf('%') !== -1;
var pixels = parseInt(str, 10) || defaultPx;
return { value: str, isPercent: isPercent, pixels: pixels };
}
/**
* Sync canvas buffer size with display size
*/
function syncCanvasSize(canvas, gl, context, minWidth, minHeight) {
var displayWidth = Math.max(canvas.clientWidth, minWidth);
var displayHeight = Math.max(canvas.clientHeight, minHeight);
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 width = parseDimension($mount.data('width'), DEFAULT_WIDTH);
var height = parseDimension($mount.data('height'), DEFAULT_HEIGHT);
var usePercentage = width.isPercent || height.isPercent;
// Explicit min dimensions override auto-calculated ones
var minWidth = parseInt($mount.data('min-width'), 10) || (width.isPercent ? DEFAULT_WIDTH : width.pixels);
var minHeight = parseInt($mount.data('min-height'), 10) || (height.isPercent ? DEFAULT_HEIGHT : height.pixels);
// Ensure mount container has dimensions for percentage sizing
if (usePercentage) {
$mount.css({
'display': 'block',
'min-width': minWidth + 'px',
'min-height': minHeight + 'px'
});
}
var canvas = document.createElement('canvas');
canvas.className = 'webgl-canvas';
if (usePercentage) {
canvas.style.width = width.isPercent ? width.value : width.value + 'px';
canvas.style.height = height.isPercent ? height.value : height.value + 'px';
canvas.style.display = 'block';
} else {
canvas.width = width.pixels;
canvas.height = height.pixels;
}
$mount.append(canvas);
// For percentage sizing, set initial buffer size after append
if (usePercentage) {
canvas.width = Math.max(canvas.clientWidth, minWidth);
canvas.height = Math.max(canvas.clientHeight, minHeight);
}
var gl = canvas.getContext('webgl2');
if (!gl) {
$mount.append('<p class="webgl-error">WebGL2 not supported</p>');
return;
}
var context = createSceneContext(mount, canvas, gl);
fetchScene(sceneName)
.then(function(scriptText) {
executeScene(scriptText, context);
if (context.init) {
context.init(gl, canvas);
}
startRenderLoop(context, gl);
// Handle resize
if (usePercentage) {
context._resizeObserver = new ResizeObserver(function() {
syncCanvasSize(canvas, gl, context, minWidth, minHeight);
});
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);
});
});