littlejsengine
Version:
LittleJS - Tiny and Fast HTML5 Game Engine
798 lines (715 loc) • 31 kB
HTML
<head>
<title>LittleJS Example Browser</title>
<link rel=icon type=image/png href=favicon.png>
<link rel=stylesheet href=style.css?9>
<meta charset=utf-8>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<!-- meta tags for social media -->
<meta name='twitter:title' content='LittleJS Example Browser'>
<meta property='og:title' content='LittleJS Example Browser'>
<meta name='description' content='Tiny fast HTML5 game engine!'>
<meta name='twitter:description' content='Tiny fast HTML5 game engine!'>
<meta property='og:description' content='Tiny fast HTML5 game engine!'>
<meta name='twitter:image' content='https://3d2k.com/images/LittleJSExamples.png'>
<meta property='og:image' content='https://3d2k.com/images/LittleJSExamples.png'>
<meta name='keywords' content='JavaScript, HTML5, Game, Engine'>
<meta name='twitter:creator' content='@KilledByAPixel'>
<meta name='twitter:card' content='summary_large_image'>
</head><body>
<div id=container1>
<div id=container3>
<p id=titleInfo>LittleJS Example Browser</p>
<p id=exampleInfo></p>
<a id=exampleLink target='_blank'></a>
<div id=divCodeOptions>
<hr>
Theme:
<select style=width:120px onchange=loadTheme() id=selectTheme></select>
<select style=width:40px onchange=loadTheme() id=selectFontSize>
<option value=10px>XS</option>
<option value=13px>S</option>
<option value=16px selected>M</option>
<option value=20px>L</option>
<option value=24px>XL</option>
</select>
<span class=nowrap><input type=checkbox id=checkboxLiveEdit onchange=writeSaveData() checked>Live Edit</span>
</div>
<div id=divTextareas>
<textarea id=textareaCode oninput=codeInput() spellcheck=false></textarea>
<textarea id=textareaError readonly spellcheck=false></textarea>
<textarea id=textareaConsole readonly spellcheck=false></textarea>
</div>
</div>
<div id=container2>
<div style='display:flex;justify-content:center'>
<div id=iframeContainer></div>
</div>
<div>
<input id=inputSearch placeholder='Search examples...' oninput=filterExamples() onkeydown='if(event.key===`Escape`)filterExamples(1)'>
<button id=buttonRestart onclick=restartCode()>Restart</button>
<span id=container4>
<button id=buttonPause>Pause</button>
<button id=buttonFullscreen>Fullscreen</button>
<button id=buttonScreenshot>Screenshot</button>
<span class=nowrap><input type=checkbox id=checkboxWebGL checked>WebGL</span>
</span>
</div>
<span id=selectExampleText class=nowrap>Select an example...</span>
<select id=selectExample onchange=setExample() size=2></select>
</div>
</div>
<script>;
class ExampleInfo
{
constructor(name, filename, description='', largeExample=false, tags='')
{
if (filename && !largeExample)
filename = 'shorts/' + filename;
this.name = name;
this.filename = filename;
this.description = description;
this.largeExample = largeExample;
this.tags = tags;
this.isHeading = !filename;
this.text = this.name;
if (this.description)
this.text += ' - ' + this.description;
this.selectText = this.text;
if (this.tags)
this.selectText += ' (' + this.tags + ')';
}
}
const exampleList =
[
new ExampleInfo('--- BASIC ---'),
new ExampleInfo('Hello World', 'helloWorld.js', 'Simple starter example', false, 'beginner, gradient, text, tiles'),
new ExampleInfo('Empty', 'empty.js', 'Empty example template', false, 'beginner, text'),
new ExampleInfo('Texture', 'texture.js', 'Texture display and manipulation', false, 'sprites, loading, tiles'),
new ExampleInfo('Animation', 'animation.js', 'Sprite animation system', false, 'frames, loop, tiles, sprites'),
new ExampleInfo('Shapes', 'shapes.js', 'Draw geometric shapes and primitives', false, 'circle, ellipse, rectangle, polygon, lines'),
new ExampleInfo('Colors', 'colors.js', 'Color manipulation and HSL', false, 'hue, saturation, blending, rectangle'),
new ExampleInfo('Sprite Atlas', 'spriteAtlas.js', 'Sprite atlas and tile rendering', false, 'sheet, frames, tiles'),
new ExampleInfo('Blending', 'blending.js', 'Additive blending and transparency', false, 'alpha, color, tiles, smooth'),
new ExampleInfo('Tile Layer', 'tileLayer.js', 'Tile layer rendering system', false, 'level, map, grid, particles'),
new ExampleInfo('Particles', 'particles.js', 'Particle system', false, 'effects, emitter, physics, fire, smoke'),
new ExampleInfo('Input', 'input.js', 'Input system demo for keyboard, mouse, touch, and gamepad.', false, 'input, control'),
new ExampleInfo('Timers', 'timers.js', 'Timer objects and UI', false, 'delay, interval, slider'),
new ExampleInfo('Sound Effects', 'sound.js', 'ZzFX sound effect generator', false, 'audio, volume, ui'),
new ExampleInfo('Music', 'music.js', 'Basic load, play, pause, and stop', false, 'music, sound, audio, streaming, volume, ui'),
new ExampleInfo('Video', 'videoPlayer.js', 'Basic video play, pause, and stop', false, 'movie, sound, audio, streaming, volume, ui'),
new ExampleInfo('Font Image', 'systemFont.js', 'Bitmap font system with built-in system font', false, 'text, characters'),
new ExampleInfo('Medals', 'medals.js', 'Achievement system', false, 'unlock, progress, newgrounds'),
new ExampleInfo('Tile Raycast', 'tileRaycast.js', 'Example of the tile layer raycast collision', false, 'level, map, grid'),
new ExampleInfo('Camera Mouse Drag', 'cameraDrag.js', 'Example of how to control camera with mose', false, 'ui, input, control'),
new ExampleInfo('Debug Drawing', 'debugDraw.js', 'Debug drawing system', false, 'debug, circle, line, rectangle'),
new ExampleInfo('--- ADVANCED ---'),
new ExampleInfo('Clock', 'clock.js', 'Animated analog clock', false, 'time, rotation, lines, rectangle'),
new ExampleInfo('Starfield', 'starfield.js', 'Animated parallax starfield', false, 'space, movement, depth, rectangle'),
new ExampleInfo('Parallax', 'parallax.js', 'Parallax scrolling mountains', false, 'generative, canvas, background'),
new ExampleInfo('Maze Generator', 'maze.js', 'Procedural maze generation', false, 'generative, level, tiles, map, grid'),
new ExampleInfo('Piano', 'piano.js', 'Interactive piano keyboard', false, 'music, sound, audio, notes, ui, instrument'),
new ExampleInfo('Step Sequencer', 'sequencer.js', 'Simple music loop creation tool', false, 'music, sound, audio, notes, ui, instrument'),
new ExampleInfo('Music Player', 'musicPlayer.js', 'Music player with audio seeking and drag and drop', false, 'sound, loading, audio, ui'),
new ExampleInfo('WebGL Shader', 'shader.js', 'Full canvas webgl shader', false, 'webgl, visual, effect'),
new ExampleInfo('--- MINI GAMES ---'),
new ExampleInfo('Pong Game', 'pongGame.js', 'Classic paddle ball bouncing', false, 'objects, collision'),
new ExampleInfo('Platformer Game', 'platformer.js', 'Jump and run side view', false, 'objects, gravity, level, tiles, camera'),
new ExampleInfo('Top Down Game', 'topDown.js', 'Top-down style camera', false, 'objects, movement, exploration'),
new ExampleInfo('Tilted View Game', 'tiltedView.js', 'Pseudo-3D oblique view', false, 'isometric, depth'),
new ExampleInfo('Flappy Game', 'flappyGame.js', 'Flappy bird style game', false, 'objects, obstacles'),
new ExampleInfo('Lander Game', 'landerGame.js', 'Lunar lander style game', false, 'objects, physics'),
new ExampleInfo('Hill Glide Game', 'hillGlideGame.js', 'Tiny wings style sliding game', false, 'objects, physics, speed'),
new ExampleInfo('Sliding Puzzle', 'slidingPuzzle.js', '15 tile sliding puzzle', false, 'objects, numbers, ui'),
new ExampleInfo('Space Game', 'spaceGame.js', 'Spaceship shooter with parallax', false, 'objects, weapons, stars, camera, rotation'),
new ExampleInfo('3D FPS Game', 'fps.js', 'Pseudo 3D raycasting demo', false, '3D, FPS, maze, camera'),
new ExampleInfo('--- PLUGINS ---'),
new ExampleInfo('Box2d Demo', 'box2d.js', 'Box2D physics plugin', false, 'objects, mouse'),
new ExampleInfo('Box2d Car', 'box2dCar.js', 'Drivable car with Box2D physics', false, 'objects, vehicle, suspension, wheels'),
new ExampleInfo('Box2d Pool', 'box2dPool.js', 'Billiard table pool game with physics', false, 'objects, game'),
new ExampleInfo('Post Processing', 'postProcess.js', 'Shader effects and filters', false, 'webgl, visual, effect'),
new ExampleInfo('UI System', 'uiSystem.js', 'Buttons, sliders, and checkboxes', false, 'objects, widgets, interactive'),
new ExampleInfo('Nine Slice', 'nineSlice.js', 'Scalable UI panels', false, 'three slice, stretch, corners, text, tiles'),
new ExampleInfo('--- FULL EXAMPLES ---'),
new ExampleInfo('Starter', 'starter', 'Clean project template', true, 'base, empty, particles'),
new ExampleInfo('Breakout Tutorial', 'breakoutTutorial', 'Step-by-step breakout game tutorial', true, 'objects, physics, score'),
new ExampleInfo('Breakout Game', 'breakout', 'Complete breakout game', true, 'objects, physics, score'),
new ExampleInfo('Platforming Game', 'platformer', 'Platformer with level loading', true, 'jump, world, tiles, pixel art, sprites'),
new ExampleInfo('Puzzle Game', 'puzzle', 'Match 3 style puzzle game', true, 'match, swap, sprites'),
new ExampleInfo('Stress Test', 'stress', 'Performance and music test', true, 'optimization, tiles, sprites'),
new ExampleInfo('Box2D Plugin', 'box2d', 'Full Box2D physics demo', true, 'objects, bodies, joints'),
new ExampleInfo('HTML Menus', 'htmlMenu', 'HTML UI integration', true, 'web, browser, overlay, button, slider, textbox'),
new ExampleInfo('UI System Plugin Demo', 'uiSystem', 'Complete UI system demo', true, 'menu, overlay, button, slider, checkbox'),
];
///////////////////////////////////////////////////////////////////////////////
// global variables
let iframeExample; // iframe of the current loaded example
let inputTimeout; // timeout to debounce input
let consoleAutoScroll; // allow console to scroll automatically
function initExampleBrowser()
{
// load the examples into the list
filterExamples();
// set the selected example from the URL parameters
const urlParams = new URLSearchParams(window.location.search);
const selectedExample = urlParams.get('example') || '';
let exampleIndex = exampleList.findIndex((example) => example.name === selectedExample);
if (exampleIndex < 0)
exampleIndex = 1;
selectExample.selectedIndex = exampleIndex;
setExample();
// apply responsive layout
addEventListener('resize', resizeWindow);
resizeWindow();
}
function resizeWindow()
{
// tweak layout for touch devices
const isTouchDevice = window.ontouchstart !== undefined;
if (isTouchDevice)
container4.style.display = 'none';
else
selectExampleText.style.display = 'none';
const windowAspect = innerWidth / innerHeight;
const verticalLayout = windowAspect < 1;
if (verticalLayout)
{
// vertical layout for thin screens
if (container2.parentNode != container3)
container3.insertBefore(container2, divCodeOptions);
// resize iframe to the window
setFrameSize(innerWidth-35);
// fix code mirror sizing glitch
codeMirror && codeMirror.setSize(innerWidth-35, null);
}
else
{
// horizontal layout for wide screens
if (container2.parentNode != container1)
container1.appendChild(container2);
// show full controls
container4.style.display = '';
// resize iframe to fit half the window
setFrameSize(innerWidth/2);
// fix code mirror sizing glitch
codeMirror && codeMirror.setSize(innerWidth/2 - 35, null);
}
function setFrameSize(w)
{
const aspect = 16 / 9; // HD aspect ratio
w = w | 0;
const h = w / aspect | 0;
iframeContainer.style.width = w + 'px';
iframeContainer.style.height = h + 'px';
if (iframeExample)
{
// ensure iframe fills container tightly
iframeExample.style.width = w + 'px';
iframeExample.style.height = h + 'px';
}
// fix code mirror after layout changes
if (codeMirror)
{
// fix glitch with code mirror sizing
setTimeout(()=>codeMirror.refresh(), 500);
}
}
}
///////////////////////////////////////////////////////////////////////////////
// setting examples
function setExample()
{
// get the original index if this is a filtered result
const selectedOption = selectExample.options[selectExample.selectedIndex];
let exampleIndex = selectedOption && selectedOption.originalIndex ?
parseInt(selectedOption.originalIndex) :
selectExample.selectedIndex;
// make sure we have a valid example
if (exampleIndex < 0 || exampleIndex >= exampleList.length)
exampleIndex = 1; // reset to default
if (!exampleList[exampleIndex].filename)
exampleIndex = 1;
// load the example
const example = exampleList[exampleIndex];
exampleInfo.innerText = example.text;
const filename = 'examples/' + example.filename;
exampleLink.href = 'https://github.com/KilledByAPixel/LittleJS/tree/main/' + filename;
exampleLink.innerText = 'View on GitHub: ' + example.filename;
// update URL parameter
const url = new URL(window.location);
url.searchParams.set('example', example.name);
window.history.replaceState({}, '', url);
loadFile(example.filename, example.largeExample);
}
async function loadFile(filename, largeExample)
{
if (codeMirror)
codeMirror.setOption('readOnly', largeExample);
else
textareaCode.disabled = largeExample;
// disable buttons in large example mode
setFrameControlsEnabled(!largeExample);
if (largeExample)
{
// Show message in code view that full examples can't be edited
const text = 'Code view not available for large examples.';
setCode(text, filename);
codeIsJS = false;
if (codeMirror)
{
codeMirror.off('change', codeInput);
codeMirror.setOption('mode', 'text');
codeMirror.setValue(text);
}
else
textareaCode.value = text;
clearTimeout(inputTimeout);
return;
}
try
{
const response = await fetch(filename);
if (!response.ok)
throw new Error('Could not load file: ' + filename);
const text = await response.text();
// set the code in both code mirror and textarea
codeIsJS = true;
if (codeMirror)
{
codeMirror.on('change', codeInput);
codeMirror.setOption('mode', 'javascript');
codeMirror.setValue(text);
}
else
textareaCode.value = text;
clearTimeout(inputTimeout);
setCode(text);
}
catch (error)
{
setErrorMessage(error.message);
}
}
function filterExamples(reset=0)
{
if (reset)
inputSearch.value = '';
// clear current options
selectExample.options.length = 0;
// filter and add matching examples
const searchTerm = inputSearch.value.toLowerCase().trim();
// first pass: find which examples match
const matchingIndices = [];
for (let i = 0; i < exampleList.length; i++)
{
const example = exampleList[i];
if (!example.filename)
continue;
// Check if search term matches name, description, or tags
if (example.name.toLowerCase().includes(searchTerm) ||
example.filename.toLowerCase().includes(searchTerm) ||
example.description.toLowerCase().includes(searchTerm) ||
example.tags.toLowerCase().includes(searchTerm))
matchingIndices.push(i);
}
// second pass: add headings and matching examples
let lastHeadingIndex = -1;
for (let i = 0; i < exampleList.length; i++)
{
const example = exampleList[i];
if (example.isHeading)
{
// check if there are any matches between this and the next
for (let j = i + 1; j < exampleList.length; j++)
{
if (exampleList[j].isHeading)
break; // hit next heading
if (matchingIndices.includes(j))
{
// only add heading if there are matches under it
lastHeadingIndex = i;
const o = new Option(example.text);
o.disabled = true;
selectExample.add(o);
break;
}
}
}
else if (matchingIndices.includes(i))
{
// add matching example
const o = new Option(example.selectText);
o.originalIndex = i;
selectExample.add(o);
}
}
if (!selectExample.options.length)
{
// show a message if no matches were found
const o = new Option(`No examples found matching '${searchTerm}'`);
o.disabled = true;
selectExample.add(o);
}
}
///////////////////////////////////////////////////////////////////////////////
// setting code
function codeInput()
{
if (!checkboxLiveEdit.checked)
return;
// debounce input - get content from code mirror if available, otherwise from textarea
clearTimeout(inputTimeout);
const code = codeMirror ? codeMirror.getValue() : textareaCode.value;
inputTimeout = setTimeout(() => setCode(code), 500);
}
function restartCode()
{
// manually restart/run the code regardless of live edit setting
const code = codeMirror ? codeMirror.getValue() : textareaCode.value;
setCode(code);
}
function setFrameControlsEnabled(enabled=true)
{
buttonPause.disabled = !enabled;
buttonRestart.disabled = !enabled;
buttonScreenshot.disabled = !enabled;
buttonFullscreen.disabled = !enabled;
checkboxWebGL.disabled = !enabled;
}
function setCode(code, filename)
{
const largeExample = !!filename;
filename = filename || 'shorts/base.html';
clearTimeout(inputTimeout);
if (iframeExample)
iframeContainer.removeChild(iframeExample);
unsetErrorMessage();
unsetConsoleMessage();
iframeExample = document.createElement('iframe');
iframeContainer.appendChild(iframeExample);
iframeExample.onload = ()=>
{
if (largeExample)
return;
// get the iframe content window and document
const iframeContent = iframeExample.contentWindow;
const iframeDocument = iframeContent.document;
if (!iframeContent.engineInit)
{
setErrorMessage(`Failed to load ${filename}`);
return;
}
// intercept errors
function getErrorLine(stack)
{
// try to extract line number from stack trace
// look for <anonymous> or injectedScript to find user code
const anonymousMatch = stack?.match(/(<anonymous>|injectedScript):(\d+)/);
return anonymousMatch ? parseInt(anonymousMatch[2]) : -1;
}
iframeContent.onerror = (message, source, lineno, colno, error) =>
{
let text = message;
if (lineno)
text += ` (Line:${lineno}, Column:${colno})`
if (error && error.stack)
text += `\n` + error.stack;
setErrorMessage(text);
setErrorLine(lineno);
}
iframeContent.onunhandledrejection = (event) =>
{
let text = event.reason;
setErrorMessage(text);
if (event.reason && event.reason.stack)
{
text = event.reason.stack;
const errorLine = getErrorLine(event.reason.stack);
if (errorLine >= 0)
setErrorLine(errorLine);
}
};
// intercept asserts
const originalAssert = iframeContent.console.assert;
iframeContent.console.assert = function (condition, ...output)
{
if (!condition)
{
const stack = (new Error).stack;
const errorLine = getErrorLine(stack);
if (errorLine >= 0)
setErrorLine(errorLine);
// format output parameters properly
const outputMessage = output.length > 0 ? output.map(m => stringifyMessage(m)).join(' ') : '';
setErrorMessage('Assertion failed!\n' + outputMessage + '\n' + stack);
}
originalAssert.apply(this, arguments);
};
// intercept console prints
const originalConsole = iframeContent.console;
function interceptConsole(method)
{
const original = originalConsole[method];
iframeContent.console[method] = function(...args)
{
const message = args.map(m=>stringifyMessage(m)).join('\n');
setConsoleMessage(message);
original.apply(originalConsole, args);
};
}
['log','info','warn','error','debug'].forEach(f=>interceptConsole(f));
{
// hook up buttons
buttonScreenshot.onclick = () =>
{
if (iframeContent.debugScreenshot)
iframeContent.debugScreenshot();
}
// pause/resume functionality
buttonPause.onclick = () =>
{
if (!iframeContent.getPaused || !iframeContent.setPaused)
return;
const paused = !iframeContent.getPaused();
iframeContent.setPaused(paused);
buttonPause.textContent = paused ? 'Resume' : 'Pause';
}
buttonPause.textContent = 'Pause';
// fullscreen functionality
buttonFullscreen.onclick = ()=> iframeContent.toggleFullscreen();
}
// create a script element that overrides the default functions
const overrideScript = iframeDocument.createElement('script');
iframeDocument.body.appendChild(overrideScript);
overrideScript.text = code;
if (textareaError.style.display === 'block')
return;
// start LittleJS engine
iframeContent.engineInit(iframeContent.gameInit, iframeContent.gameUpdate, iframeContent.gameUpdatePost, iframeContent.gameRender, iframeContent.gameRenderPost, ['tiles.png?'+Date.now()])
.catch(error =>
{
const errorLine = error ? getErrorLine(error.stack) : -1;
if (errorLine >= 0)
setErrorLine(errorLine);
let message = error;
if (error && error.message)
message = error.message;
if (error && error.stack)
message += '\n' + error.stack;
setErrorMessage(message);
throw error;
})
.then(()=>
{
// setup frame controls
setFrameControlsEnabled();
if (iframeContent.glCanEnable())
iframeContent.setGLEnable(checkboxWebGL.checked);
else
checkboxWebGL.disabled = true;
checkboxWebGL.onchange = ()=> iframeContent.setGLEnable(checkboxWebGL.checked);
})
}
iframeExample.src = filename + '?' + Date.now();
}
///////////////////////////////////////////////////////////////////////////////
// error and console messages
function stringifyMessage(message)
{
// make sure message is a string
if (message === null)
return 'null';
if (Number.isNaN(message))
return 'NaN';
if (message === undefined)
return 'undefined';
if (message === 0)
return '0';
if (message === false)
return 'false';
return message;
}
function setMessage(message, element, clear=true)
{
message = stringifyMessage(message);
if (clear || !element.value)
element.value = message;
else
element.value += '\n' + message;
element.style.display = message ? 'block' : '';
if (element === textareaConsole)
{
const maxConsoleLines = 100;
const lines = element.value.split('\n');
if (lines.length > maxConsoleLines)
{
// limit max lines, prevents slowdown from too many lines
const excessLines = lines.length - maxConsoleLines;
element.value = lines.slice(excessLines).join('\n');
}
// auto scroll to bottom only if user hasn't manually scrolled
if (consoleAutoScroll)
element.scrollTop = element.scrollHeight;
}
}
function unsetMessage(element)
{
element.style.display = '';
element.value = '';
}
function clearErrorLine()
{
if (!codeMirror || !errorLineMarker)
return;
codeMirror.getDoc().removeLineClass(errorLineMarker, 'background', 'error-line');
}
function setErrorLine(lineNumber)
{
if (!lineNumber || lineNumber <= 0)
return;
if (codeMirror)
{
clearErrorLine();
--lineNumber; // codeMirror uses 0-based line numbers
errorLineMarker = codeMirror.getDoc().addLineClass(lineNumber, 'background', 'error-line');
}
}
function setErrorMessage(message)
{
// prevent overwriting an existing error message
const errorMessageIsVisible = textareaError.style.display === 'block';
setFrameControlsEnabled(false);
if (!errorMessageIsVisible)
setMessage(message, textareaError);
}
function unsetErrorMessage() { unsetMessage(textareaError); clearErrorLine(); }
function setConsoleMessage(message) { setMessage(message, textareaConsole, false); }
function unsetConsoleMessage() { unsetMessage(textareaConsole); consoleAutoScroll = true; }
function onScrollConsole()
{
// only auto scroll console is it is scrolled to bottom
const tolerance = 5; // pixels of tolerance
consoleAutoScroll = Math.abs(textareaConsole.scrollTop - (textareaConsole.scrollHeight - textareaConsole.clientHeight)) < tolerance;
}
textareaConsole.addEventListener('scroll', onScrollConsole);
///////////////////////////////////////////////////////////////////////////////
// load saved preferences from localStorage
const saveName = 'LittleJSExamples';
let savedTheme;
function readSaveData()
{
// load saved preferences
const defaultTheme = 'littlejs';
const defaultFontSize = '16px';
const saveDataJSON = localStorage.getItem(saveName);
const saveData = saveDataJSON ? JSON.parse(saveDataJSON) : {};
selectTheme.value = savedTheme = saveData.theme ?? defaultTheme;
selectFontSize.value = saveData.fontSize ?? defaultFontSize;
}
readSaveData();
function writeSaveData()
{
// Save preferences to localStorage
const theme = selectTheme.value;
const fontSize = selectFontSize.value;
const saveData =
{
theme,
fontSize
};
const saveDataJSON = JSON.stringify(saveData);
localStorage.setItem(saveName, saveDataJSON);
}
function resetDefaults()
{
localStorage.removeItem(saveName);
readSaveData();
loadTheme();
}
///////////////////////////////////////////////////////////////////////////////
// setup code mirror
const useCodeMirror = true;
let codeMirror; // code mirror instance
let errorLineMarker; // marker for error line in code mirror
let codeIsJS; // is the current code javascript
const themes =
[
'littlejs',
'3024-night',
'abcdef',
'ambiance',
'blackboard',
'monokai',
'duotone-light',
'icecoder',
'lesser-dark',
'night',
'yonce'
];
// load theme and font size
for (const theme of themes)
{
if (theme != 'littlejs')
addCodeMirrorElement(`theme/${theme}.min.css`, 'link', 'stylesheet');
const o = new Option(theme);
o.selected = theme === savedTheme;
selectTheme.add(o);
}
if (useCodeMirror)
{
addCodeMirrorElement('codemirror.min.js', 'script').onload = ()=>
addCodeMirrorElement('codemirror.min.css', 'link', 'stylesheet').onload = ()=>
addCodeMirrorElement('addon/edit/matchbrackets.js', 'script').onload = ()=>
addCodeMirrorElement('mode/javascript/javascript.min.js', 'script').onload = ()=>
{
if (codeMirror)
return; // prevent duplicate initialization
const textareaCode = document.getElementById('textareaCode');
codeMirror = CodeMirror.fromTextArea(textareaCode,
{
theme: savedTheme,
indentUnit: 4,
mode: codeIsJS ? 'javascript' : 'text',
lineNumbers: true,
lineWrapping: true,
matchBrackets: true,
});
codeMirror.on('change', codeInput);
loadTheme();
resizeWindow(); // fix cursor positioning issue on startup
}
}
function addCodeMirrorElement(filename, type, rel)
{
// add element for code mirror
const e = document.createElement(type);
e.rel = rel;
filename = 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/' + filename;
if (type === 'link')
e.href = filename;
else
e.src = filename;
e.crossOrigin = 'anonymous';
document.head.appendChild(e);
return e;
}
function loadTheme()
{
// Apply the theme and font size
const theme = selectTheme.value;
const fontSize = selectFontSize.value;
textareaCode.style.fontSize = fontSize;
if (codeMirror)
{
codeMirror.getWrapperElement().style.fontSize = fontSize;
codeMirror.setOption('theme', theme);
}
// Save preferences to localStorage
writeSaveData();
}
///////////////////////////////////////////////////////////////////////////////
// start up the browser
initExampleBrowser()
</script>
</body>
</html>