Jump to content

MediaWiki:Gadget-WebGL2.js: Difference between revisions

From Apogea Wiki
Dane (talk | contribs)
No edit summary
Dane (talk | contribs)
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.log("WebGL2 Test");
'use strict';
 
$('.webgl-mount').each(function() {
var DEFAULT_WIDTH = 800;
var mount = $(this);
var DEFAULT_HEIGHT = 600;
var scene = mount.data('scene');
 
/**
* 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.width = mount.data('width') || 800;
canvas.height = mount.data('height') || 600;
canvas.className = 'webgl-canvas';
canvas.className = 'webgl-canvas';
mount.append(canvas);
 
if (usePercentage) {
// Your WebGL initialization here
canvas.style.width = width.isPercent ? width.value : width.value + 'px';
console.log('WebGL canvas created for scene:', scene);
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);
	});
});