UNPKG

littlejsengine

Version:

LittleJS - Tiny and Fast HTML5 Game Engine

798 lines (715 loc) 31 kB
<!DOCTYPE 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> 'use strict'; 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>