Implement 2D video controls for non-VR devices
- Add complete 2D control panel with HTML structure and CSS styling - Implement JavaScript functionality for all control buttons: - Fullscreen toggle for immersive 16:9 video experience - Play/pause with dynamic icon switching - Back/forward 15-second skip controls - Mute/unmute toggle - Click-to-seek progress bar with real-time updates - Add auto-hide behavior (5-second timeout) with mouse/touch activation - Integrate with existing 2D mode - shows only when VR not supported - Include all button PNG assets (normal and hover states) - Responsive design for mobile devices - Professional styling matching design specifications
BIN
back-hover.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
forward-hover.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
forward.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
fullscreen-hover.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
fullscreen.png
Normal file
|
After Width: | Height: | Size: 796 B |
23
index.html
@@ -31,8 +31,29 @@
|
|||||||
<source src="sbs-video.mp4" type="video/mp4">
|
<source src="sbs-video.mp4" type="video/mp4">
|
||||||
</video>
|
</video>
|
||||||
<button id="playBtn" aria-label="Play video">
|
<button id="playBtn" aria-label="Play video">
|
||||||
<img src="play.png" alt="Play">
|
<img src="play.svg" alt="Play">
|
||||||
</button>
|
</button>
|
||||||
|
<div id="panel">
|
||||||
|
<div id="status">
|
||||||
|
<p id="video-title">Title</p>
|
||||||
|
<div id="progress">
|
||||||
|
<p id="current-time">00:00:00</p>
|
||||||
|
<div id="bar">
|
||||||
|
<div id="played"></div>
|
||||||
|
</div>
|
||||||
|
<p id="total-time">00:00:00</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="controls">
|
||||||
|
<button id="fullscreen"></button>
|
||||||
|
<div id="nav">
|
||||||
|
<button id="back"></button>
|
||||||
|
<button id="play2"></button>
|
||||||
|
<button id="forward"></button>
|
||||||
|
</div>
|
||||||
|
<button id="mute"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<script type="module" src="vr180-player.js"></script>
|
<script type="module" src="vr180-player.js"></script>
|
||||||
|
|||||||
BIN
mute-hover.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
pause-hover.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
play2-hover.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
unmute-hover.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
unmute.png
Normal file
|
After Width: | Height: | Size: 1014 B |
211
vr180-player.css
@@ -51,3 +51,214 @@
|
|||||||
height: 100px;
|
height: 100px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 2D Video Controls Panel */
|
||||||
|
#panel {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
max-width: 1000px;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 30px;
|
||||||
|
background: rgba(0, 0, 0, 0.70);
|
||||||
|
color: #fff;
|
||||||
|
font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||||
|
z-index: 100;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition: opacity 0.3s ease, visibility 0.3s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#panel.visible {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#panel img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#status {
|
||||||
|
margin: 0 12px 12px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#video-title {
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
#current-time,
|
||||||
|
#total-time {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
#progress {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: min-content 1fr min-content;
|
||||||
|
grid-gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: #666;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#played {
|
||||||
|
border-radius: 2px;
|
||||||
|
background: #fff;
|
||||||
|
height: 4px;
|
||||||
|
width: 0%;
|
||||||
|
transition: width 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#controls {
|
||||||
|
display: grid;
|
||||||
|
grid-template-areas: "full lflex nav rflex mute";
|
||||||
|
grid-template-columns: 44px 1fr 156px 1fr 44px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#panel button {
|
||||||
|
all: unset;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: opacity 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#panel button:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fullscreen {
|
||||||
|
grid-area: full;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
background-image: url(fullscreen.png);
|
||||||
|
background-size: 44px 44px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
transition: background-image 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fullscreen:hover {
|
||||||
|
background-image: url(fullscreen-hover.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mute {
|
||||||
|
grid-area: mute;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
background-image: url(mute.png);
|
||||||
|
background-size: 44px 44px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
transition: background-image 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mute:hover {
|
||||||
|
background-image: url(mute-hover.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav {
|
||||||
|
grid-area: nav;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 44px 44px 44px;
|
||||||
|
grid-gap: 12px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#back {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
background-image: url(back.png);
|
||||||
|
background-size: 44px 44px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
transition: background-image 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#back:hover {
|
||||||
|
background-image: url(back-hover.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
#play2 {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
background-image: url(play2.png);
|
||||||
|
background-size: 44px 44px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
transition: background-image 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#play2:hover {
|
||||||
|
background-image: url(play2-hover.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
#play2.paused {
|
||||||
|
background-image: url(play2.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
#play2.paused:hover {
|
||||||
|
background-image: url(play2-hover.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
#play2.playing {
|
||||||
|
background-image: url(pause2.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
#play2.playing:hover {
|
||||||
|
background-image: url(pause2-hover.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
#forward {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
background-image: url(forward.png);
|
||||||
|
background-size: 44px 44px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
transition: background-image 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#forward:hover {
|
||||||
|
background-image: url(forward-hover.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments for 2D controls */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
#panel {
|
||||||
|
max-width: 90%;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#controls {
|
||||||
|
grid-template-columns: 36px 1fr 132px 1fr 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fullscreen, #mute, #back, #play2, #forward {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
background-size: 36px 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav {
|
||||||
|
grid-template-columns: 36px 36px 36px;
|
||||||
|
grid-gap: 8px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
206
vr180-player.js
@@ -8,6 +8,13 @@ const tempMatrix = new THREE.Matrix4();
|
|||||||
let videoElement, playBtn;
|
let videoElement, playBtn;
|
||||||
let frameCounter = 0;
|
let frameCounter = 0;
|
||||||
|
|
||||||
|
// 2D Control Panel Elements
|
||||||
|
let controlPanel, videoTitle, currentTimeDisplay, totalTimeDisplay, progressBar, playedBar;
|
||||||
|
let fullscreenBtn, backBtn, play2Btn, forwardBtn, muteBtn;
|
||||||
|
let controlPanelTimeout;
|
||||||
|
let isControlPanelVisible = false;
|
||||||
|
const CONTROL_PANEL_HIDE_DELAY = 5000; // 5 seconds
|
||||||
|
|
||||||
let isXrLoopActive = false;
|
let isXrLoopActive = false;
|
||||||
let is2DMode = false;
|
let is2DMode = false;
|
||||||
let vrControlPanel;
|
let vrControlPanel;
|
||||||
@@ -547,6 +554,7 @@ function init() {
|
|||||||
updateSeekBarAppearance();
|
updateSeekBarAppearance();
|
||||||
updateVRPlayPauseButtonIcon();
|
updateVRPlayPauseButtonIcon();
|
||||||
updateVRVolumeButtonIcon();
|
updateVRVolumeButtonIcon();
|
||||||
|
update2DControlPanel();
|
||||||
};
|
};
|
||||||
video.oncanplaythrough = () => {
|
video.oncanplaythrough = () => {
|
||||||
if (playBtn && video.readyState >= video.HAVE_FUTURE_DATA) {
|
if (playBtn && video.readyState >= video.HAVE_FUTURE_DATA) {
|
||||||
@@ -557,13 +565,16 @@ function init() {
|
|||||||
video.ontimeupdate = () => {
|
video.ontimeupdate = () => {
|
||||||
if (isFinite(video.duration)) {
|
if (isFinite(video.duration)) {
|
||||||
updateSeekBarAppearance();
|
updateSeekBarAppearance();
|
||||||
|
update2DControlPanel();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
video.onplaying = () => {
|
video.onplaying = () => {
|
||||||
updateVRPlayPauseButtonIcon();
|
updateVRPlayPauseButtonIcon();
|
||||||
|
update2DPlayPauseButton();
|
||||||
};
|
};
|
||||||
video.onpause = () => {
|
video.onpause = () => {
|
||||||
updateVRPlayPauseButtonIcon();
|
updateVRPlayPauseButtonIcon();
|
||||||
|
update2DPlayPauseButton();
|
||||||
};
|
};
|
||||||
video.onerror = (e) => {
|
video.onerror = (e) => {
|
||||||
const videoError = video.error;
|
const videoError = video.error;
|
||||||
@@ -573,7 +584,11 @@ function init() {
|
|||||||
};
|
};
|
||||||
video.addEventListener('ended', onVideoEnded);
|
video.addEventListener('ended', onVideoEnded);
|
||||||
video.addEventListener('volumechange', updateVRVolumeButtonIcon);
|
video.addEventListener('volumechange', updateVRVolumeButtonIcon);
|
||||||
|
video.addEventListener('volumechange', update2DMuteButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize 2D control panel
|
||||||
|
init2DControlPanel();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("INIT_ERROR (Phase 3 - Event Listeners):", e);
|
console.error("INIT_ERROR (Phase 3 - Event Listeners):", e);
|
||||||
}
|
}
|
||||||
@@ -845,6 +860,191 @@ function render2D() {
|
|||||||
requestAnimationFrame(render2D);
|
requestAnimationFrame(render2D);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2D Control Panel Functions
|
||||||
|
function init2DControlPanel() {
|
||||||
|
// Get references to 2D control elements
|
||||||
|
controlPanel = document.getElementById('panel');
|
||||||
|
videoTitle = document.getElementById('video-title');
|
||||||
|
currentTimeDisplay = document.getElementById('current-time');
|
||||||
|
totalTimeDisplay = document.getElementById('total-time');
|
||||||
|
progressBar = document.getElementById('bar');
|
||||||
|
playedBar = document.getElementById('played');
|
||||||
|
fullscreenBtn = document.getElementById('fullscreen');
|
||||||
|
backBtn = document.getElementById('back');
|
||||||
|
play2Btn = document.getElementById('play2');
|
||||||
|
forwardBtn = document.getElementById('forward');
|
||||||
|
muteBtn = document.getElementById('mute');
|
||||||
|
|
||||||
|
if (!controlPanel) {
|
||||||
|
console.error("2D Control panel not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set initial video title
|
||||||
|
if (videoTitle && video) {
|
||||||
|
const title = video.getAttribute('title') ||
|
||||||
|
video.querySelector('source')?.src.split('/').pop()?.split('.')[0].replace(/-/g, ' ') ||
|
||||||
|
"Demo Video";
|
||||||
|
videoTitle.textContent = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listeners for 2D controls
|
||||||
|
if (fullscreenBtn) {
|
||||||
|
fullscreenBtn.addEventListener('click', toggle2DFullscreen);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backBtn) {
|
||||||
|
backBtn.addEventListener('click', () => {
|
||||||
|
if (video) {
|
||||||
|
video.currentTime = Math.max(0, video.currentTime - 15);
|
||||||
|
}
|
||||||
|
show2DControlPanel();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (play2Btn) {
|
||||||
|
play2Btn.addEventListener('click', () => {
|
||||||
|
togglePlayPause();
|
||||||
|
show2DControlPanel();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forwardBtn) {
|
||||||
|
forwardBtn.addEventListener('click', () => {
|
||||||
|
if (video && isFinite(video.duration)) {
|
||||||
|
video.currentTime = Math.min(video.duration, video.currentTime + 15);
|
||||||
|
}
|
||||||
|
show2DControlPanel();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (muteBtn) {
|
||||||
|
muteBtn.addEventListener('click', () => {
|
||||||
|
if (video) {
|
||||||
|
video.muted = !video.muted;
|
||||||
|
}
|
||||||
|
show2DControlPanel();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progressBar) {
|
||||||
|
progressBar.addEventListener('click', (e) => {
|
||||||
|
if (video && isFinite(video.duration)) {
|
||||||
|
const rect = progressBar.getBoundingClientRect();
|
||||||
|
const clickX = e.clientX - rect.left;
|
||||||
|
const progress = clickX / rect.width;
|
||||||
|
video.currentTime = progress * video.duration;
|
||||||
|
}
|
||||||
|
show2DControlPanel();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add mouse movement listener for 2D mode
|
||||||
|
document.addEventListener('mousemove', on2DMouseMove);
|
||||||
|
document.addEventListener('touchstart', on2DTouchStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
function show2DControlPanel() {
|
||||||
|
if (!is2DMode || !controlPanel) return;
|
||||||
|
|
||||||
|
clearTimeout(controlPanelTimeout);
|
||||||
|
controlPanel.classList.add('visible');
|
||||||
|
isControlPanelVisible = true;
|
||||||
|
|
||||||
|
controlPanelTimeout = setTimeout(hide2DControlPanel, CONTROL_PANEL_HIDE_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide2DControlPanel() {
|
||||||
|
if (!controlPanel) return;
|
||||||
|
|
||||||
|
clearTimeout(controlPanelTimeout);
|
||||||
|
controlPanel.classList.remove('visible');
|
||||||
|
isControlPanelVisible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function on2DMouseMove() {
|
||||||
|
if (is2DMode) {
|
||||||
|
show2DControlPanel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function on2DTouchStart() {
|
||||||
|
if (is2DMode) {
|
||||||
|
show2DControlPanel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function update2DControlPanel() {
|
||||||
|
if (!is2DMode || !video) return;
|
||||||
|
|
||||||
|
// Update time displays
|
||||||
|
if (currentTimeDisplay) {
|
||||||
|
currentTimeDisplay.textContent = formatTime(video.currentTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalTimeDisplay && isFinite(video.duration)) {
|
||||||
|
totalTimeDisplay.textContent = formatTime(video.duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update progress bar
|
||||||
|
if (playedBar && isFinite(video.duration) && video.duration > 0) {
|
||||||
|
const progress = (video.currentTime / video.duration) * 100;
|
||||||
|
playedBar.style.width = `${progress}%`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function update2DPlayPauseButton() {
|
||||||
|
if (!is2DMode || !play2Btn || !video) return;
|
||||||
|
|
||||||
|
if (video.paused || video.ended) {
|
||||||
|
play2Btn.classList.remove('playing');
|
||||||
|
play2Btn.classList.add('paused');
|
||||||
|
} else {
|
||||||
|
play2Btn.classList.remove('paused');
|
||||||
|
play2Btn.classList.add('playing');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function update2DMuteButton() {
|
||||||
|
if (!is2DMode || !muteBtn || !video) return;
|
||||||
|
|
||||||
|
// The CSS will handle the visual state based on the muted property
|
||||||
|
// We could add classes here if needed for different mute states
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle2DFullscreen() {
|
||||||
|
if (!document.fullscreenElement) {
|
||||||
|
// Enter fullscreen
|
||||||
|
const container = document.getElementById('vr-container');
|
||||||
|
if (container && container.requestFullscreen) {
|
||||||
|
container.requestFullscreen().catch(err => {
|
||||||
|
console.error('Error attempting to enable fullscreen:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Exit fullscreen
|
||||||
|
if (document.exitFullscreen) {
|
||||||
|
document.exitFullscreen().catch(err => {
|
||||||
|
console.error('Error attempting to exit fullscreen:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(seconds) {
|
||||||
|
if (!isFinite(seconds)) return '00:00:00';
|
||||||
|
|
||||||
|
const hours = Math.floor(seconds / 3600);
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60);
|
||||||
|
const secs = Math.floor(seconds % 60);
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||||||
|
} else {
|
||||||
|
return `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function hidePlayButton() {
|
function hidePlayButton() {
|
||||||
if (playBtn) {
|
if (playBtn) {
|
||||||
playBtn.classList.add('hidden');
|
playBtn.classList.add('hidden');
|
||||||
@@ -895,6 +1095,9 @@ function resetToOriginalState() {
|
|||||||
is2DMode = false;
|
is2DMode = false;
|
||||||
remove2DEventListeners();
|
remove2DEventListeners();
|
||||||
|
|
||||||
|
// Hide 2D control panel
|
||||||
|
hide2DControlPanel();
|
||||||
|
|
||||||
// Reset camera rotation
|
// Reset camera rotation
|
||||||
cameraRotation = { yaw: 0, pitch: 0 };
|
cameraRotation = { yaw: 0, pitch: 0 };
|
||||||
cameraVelocity = { yaw: 0, pitch: 0 };
|
cameraVelocity = { yaw: 0, pitch: 0 };
|
||||||
@@ -1050,6 +1253,9 @@ function start2DMode() {
|
|||||||
// Add event listeners for 2D controls
|
// Add event listeners for 2D controls
|
||||||
add2DEventListeners();
|
add2DEventListeners();
|
||||||
|
|
||||||
|
// Show 2D control panel
|
||||||
|
show2DControlPanel();
|
||||||
|
|
||||||
// Start 2D render loop
|
// Start 2D render loop
|
||||||
render2D();
|
render2D();
|
||||||
}
|
}
|
||||||
|
|||||||