diff --git a/steps/src/main.js b/steps/src/main.js
index 7cb2a81..55d9f54 100644
--- a/steps/src/main.js
+++ b/steps/src/main.js
@@ -31,57 +31,23 @@ import {
forceResyncWithOffset,
} from "./sync.js";
import { formatTime } from "./utils.js";
+import { initSessionManagement } from "./session.js";
+import { initUIEventListeners } from "./ui.js";
import { appState } from "./state.js";
import { debugFlags } from "./debug.js"; // Import the new debug flags
window.appState = appState; // exposing the appState to console
window.debugFlags = debugFlags; // Expose debug flags to the console for runtime toggling
import {
- canvasPlaceholder,
videoPlayer,
- videoPlaceholder,
loadJsonBtn,
loadVideoBtn,
jsonFileInput,
videoFileInput,
playPauseBtn,
stopBtn,
- timelineSlider,
offsetInput,
- speedSlider,
- speedDisplay,
- featureToggles,
- toggleSnrColor,
- toggleClusterColor,
- toggleInlierColor,
- toggleStationaryColor,
- toggleVelocity,
- toggleTracks,
- toggleEgoSpeed,
- toggleFrameNorm,
- toggleDebugOverlay,
- toggleDebug2Overlay,
- debugOverlay,
- snrMinInput,
- snrMaxInput,
- applySnrBtn,
autoOffsetIndicator,
clearCacheBtn,
- toggleCloseUp,
- updateDebugOverlay,
- timelineTooltip,
- saveSessionBtn,
- loadSessionBtn,
- sessionFileInput,
- togglePredictedPos,
- toggleCovariance,
- updatePersistentOverlays,
- collapsibleMenu,
- toggleMenuBtn,
- fullscreenBtn,
- closeMenuBtn,
- menuScrim,
- toggleConfirmedOnly,
- resetUIForNewLoad,
} from "./dom.js";
import { initializeTheme } from "./theme.js";
@@ -126,188 +92,6 @@ clearCacheBtn.addEventListener("click", async () => {
window.location.reload();
}
});
-// Event listener for saving the session
-saveSessionBtn.addEventListener("click", () => {
- // We can only save a session if at least one data file has been loaded.
- if (!appState.jsonFilename && !appState.videoFilename) {
- showModal("Nothing to save. Please load data files first.");
- return;
- }
-
- // Collect all relevant state into a single object.
- const sessionState = {
- version: 1,
- jsonFilename: appState.jsonFilename,
- videoFilename: appState.videoFilename,
- offset: offsetInput.value,
- playbackSpeed: speedSlider.value,
- snrMin: snrMinInput.value,
- snrMax: snrMaxInput.value,
- toggles: {
- snrColor: toggleSnrColor.checked,
- clusterColor: toggleClusterColor.checked,
- inlierColor: toggleInlierColor.checked,
- stationaryColor: toggleStationaryColor.checked,
- velocity: toggleVelocity.checked,
- tracks: toggleTracks.checked,
- egoSpeed: toggleEgoSpeed.checked,
- frameNorm: toggleFrameNorm.checked,
- debugOverlay: toggleDebugOverlay.checked,
- debug2Overlay: toggleDebug2Overlay.checked,
- closeUp: toggleCloseUp.checked,
- predictedPos: togglePredictedPos.checked,
- covariance: toggleCovariance.checked,
- },
- };
-
- const sessionString = JSON.stringify(sessionState, null, 2);
- const blob = new Blob([sessionString], { type: "application/json" });
- const url = URL.createObjectURL(blob);
-
- // --- Dynamic Filename Logic ---
- const now = new Date();
- const pad = (num) => String(num).padStart(2, "0");
- const date = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(
- now.getDate()
- )}`;
- const time = `${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(
- now.getSeconds()
- )}`;
- const timestamp = `${date}_${time}`;
- const defaultFilename = `visualizer-session_${timestamp}.json`;
-
- // --- Trigger "Save As" Dialog ---
- const a = document.createElement("a");
- a.href = url;
-
- // This is the key instruction for the browser. It suggests a filename
- // and signals that this should open a "Save As" dialog.
- a.download = defaultFilename;
-
- document.body.appendChild(a);
- a.click(); // Programmatically clicking the link triggers the download/save dialog.
-
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
-});
-
-//videoframecallback exported from sync.js
-
-// When "Load Session" is clicked, it triggers the hidden file input.
-loadSessionBtn.addEventListener("click", () => {
- sessionFileInput.click();
-});
-
-// This listener handles the selected session file.
-
-sessionFileInput.addEventListener("change", (event) => {
- const file = event.target.files[0];
- if (!file) return;
-
- const reader = new FileReader();
- reader.onload = async (e) => {
- // Make the function async to use 'await'
- try {
- const sessionState = JSON.parse(e.target.result);
-
- // Basic validation to ensure it's a valid session file.
- if (sessionState.version !== 1 || !sessionState.jsonFilename) {
- showModal("Error: Invalid or corrupted session file.");
- return;
- }
-
- // --- START: New Robust Session Check ---
-
- // 1. Before doing anything else, check if the required files exist in the cache.
- // We use the same 'loadFreshFileFromDB' function that the startup process uses.
- const videoBlob = await loadFreshFileFromDB(
- "video",
- sessionState.videoFilename
- );
- const jsonBlob = await loadFreshFileFromDB(
- "json",
- sessionState.jsonFilename
- );
-
- // 2. If either file is missing from the cache, show an informative error and stop.
- if (!jsonBlob || !videoBlob) {
- showModal(`Session load failed: The required data files are not in the application's cache.
-
- Please manually load '${sessionState.jsonFilename}' and '${sessionState.videoFilename}' before loading this session.`);
-
- event.target.value = ""; // Reset file input
- return;
- }
-
- // 3. If we get here, it means the files ARE in the cache and match the session!
- // It is now safe to set localStorage and reload the page.
-
- localStorage.setItem("jsonFilename", sessionState.jsonFilename || "");
- localStorage.setItem("videoFilename", sessionState.videoFilename || "");
- localStorage.setItem("visualizerOffset", sessionState.offset || "0");
- localStorage.setItem("playbackSpeed", sessionState.playbackSpeed || "1");
- localStorage.setItem("snrMin", sessionState.snrMin || "");
- localStorage.setItem("snrMax", sessionState.snrMax || "");
- if (sessionState.toggles) {
- localStorage.setItem(
- "togglesState",
- JSON.stringify(sessionState.toggles)
- );
- }
-
- // Inform the user and then reload the page to apply the session.
- showModal(
- "Session files found in cache. The application will now reload."
- ).then(() => {
- window.location.reload();
- });
- // --- END: New Robust Session Check ---
- } catch (error) {
- showModal("Error: Could not parse the session file. It may be invalid.");
- console.error("Session load error:", error);
- }
- };
- reader.readAsText(file);
- event.target.value = ""; // Clear the input for future loads.
-});
-
-// --- END: Add Session Management Logic ---
-
-// --- Collapsible Menu Logic (Overlay Version) ---
-function toggleMenu(show) {
- if (show) {
- collapsibleMenu.classList.remove("-translate-x-full");
- menuScrim.classList.remove("hidden"); // Show the scrim
- // The line that pushed the content has been REMOVED.
- } else {
- collapsibleMenu.classList.add("-translate-x-full");
- menuScrim.classList.add("hidden"); // Hide the scrim
- }
-}
-
-toggleConfirmedOnly.addEventListener("change", () => {
- if (appState.p5_instance) {
- appState.p5_instance.redraw();
- }
-});
-
-// Open the menu
-toggleMenuBtn.addEventListener("click", () => toggleMenu(true));
-
-// Close the menu with the 'X' button
-closeMenuBtn.addEventListener("click", () => toggleMenu(false));
-
-// NEW: Close the menu by clicking on the scrim
-menuScrim.addEventListener("click", () => toggleMenu(false));
-
-// --- Fullscreen Logic ---
-fullscreenBtn.addEventListener("click", () => {
- if (!document.fullscreenElement) {
- document.documentElement.requestFullscreen();
- } else if (document.exitFullscreen) {
- document.exitFullscreen();
- }
-});
// Event listener for offset input change.
offsetInput.addEventListener("input", () => {
@@ -315,26 +99,6 @@ offsetInput.addEventListener("input", () => {
// The value is now saved to localStorage only when 'Enter' is pressed.
});
-// Event listener for apply SNR button click.
-applySnrBtn.addEventListener("click", () => {
- const newMin = parseFloat(snrMinInput.value),
- newMax = parseFloat(snrMaxInput.value);
- if (isNaN(newMin) || isNaN(newMax) || newMin >= newMax) {
- showModal("Invalid SNR range.");
- return;
- }
- appState.globalMinSnr = newMin;
- appState.globalMaxSnr = newMax;
- toggleFrameNorm.checked = false;
- if (appState.p5_instance) {
- appState.p5_instance.drawSnrLegendToBuffer(
- appState.globalMinSnr,
- appState.globalMaxSnr
- );
- appState.p5_instance.redraw();
- }
-});
-
// Event listener for play/pause button click.
playPauseBtn.addEventListener("click", () => {
if (!appState.vizData && !videoPlayer.src) return;
@@ -359,115 +123,6 @@ stopBtn.addEventListener("click", () => {
if (appState.speedGraphInstance) appState.speedGraphInstance.redraw();
});
-// Event listener for timeline slider input.
-// --- Timeline Scroll-to-Seek Logic ---
-
-// In src/main.js, add this new block of event listeners
-// --- Timeline Scrub-to-Seek Preview Logic ---
-
-timelineSlider.addEventListener("mouseover", () => {
- if (appState.vizData) {
- timelineTooltip.classList.remove("hidden");
- }
-});
-
-timelineSlider.addEventListener("mouseout", () => {
- timelineTooltip.classList.add("hidden");
-});
-
-timelineSlider.addEventListener("mousemove", (event) => {
- if (!appState.vizData) return;
-
- // 1. Calculate the hover position as a fraction (0.0 to 1.0)
- const rect = timelineSlider.getBoundingClientRect();
- const hoverFraction = (event.clientX - rect.left) / rect.width;
-
- // 2. Calculate the corresponding frame index
- const sliderMax =
- parseInt(timelineSlider.max, 10) || appState.vizData.radarFrames.length - 1;
- let frameIndex = Math.round(hoverFraction * sliderMax);
- // The value is already clamped by this calculation, but an extra check is safe
- frameIndex = Math.max(0, Math.min(frameIndex, sliderMax));
-
- const frameData = appState.vizData.radarFrames[frameIndex];
- if (!frameData) return;
-
- // 3. Update the tooltip's content
- const formattedTime = formatTime(frameData.relativeTimeSec * 1000);
- timelineTooltip.innerHTML = `Frame: ${
- frameIndex + 1
- }
Time: ${formattedTime}`;
-
- // 4. Position the tooltip horizontally above the cursor
- // The horizontal position is the mouse's X relative to the slider's start
- const tooltipX = event.clientX - rect.left;
- timelineTooltip.style.left = `${tooltipX}px`;
-});
-
-// Event listener for speed slider input.
-speedSlider.addEventListener("input", (event) => {
- const speed = parseFloat(event.target.value);
- videoPlayer.playbackRate = speed;
- speedDisplay.textContent = `${speed.toFixed(1)}x`;
-});
-
-const colorToggles = [
- toggleSnrColor,
- toggleClusterColor,
- toggleInlierColor,
- toggleStationaryColor,
-];
-colorToggles.forEach((t) => {
- t.addEventListener("change", (e) => {
- if (e.target.checked) {
- colorToggles.forEach((o) => {
- if (o !== e.target) o.checked = false;
- });
- }
- if (appState.p5_instance) appState.p5_instance.redraw();
- updatePersistentOverlays(videoPlayer.currentTime);
- });
-});
-
-[
- toggleVelocity,
- toggleEgoSpeed,
- toggleFrameNorm,
- toggleTracks,
- toggleDebugOverlay,
- toggleDebug2Overlay,
-].forEach((t) => {
- t.addEventListener("change", () => {
- if (appState.p5_instance) {
- if (t === toggleFrameNorm && !toggleFrameNorm.checked)
- appState.p5_instance.drawSnrLegendToBuffer(
- appState.globalMinSnr,
- appState.globalMaxSnr
- );
- appState.p5_instance.redraw();
- }
- if (t === toggleDebugOverlay || t === toggleDebug2Overlay) {
- updateDebugOverlay(videoPlayer.currentTime);
- updatePersistentOverlays(videoPlayer.currentTime);
- }
- });
-});
-
-toggleCloseUp.addEventListener("change", () => {
- appState.isCloseUpMode = toggleCloseUp.checked;
- if (appState.p5_instance) {
- if (appState.isCloseUpMode) {
- if (appState.isPlaying) {
- playPauseBtn.click();
- }
- appState.p5_instance.loop();
- } else {
- appState.p5_instance.noLoop();
- appState.p5_instance.redraw();
- }
- }
-});
-
videoPlayer.addEventListener("ended", () => {
appState.isPlaying = false;
playPauseBtn.textContent = "Play";
@@ -494,9 +149,11 @@ autoOffsetIndicator.addEventListener("click", () => {
document.addEventListener("DOMContentLoaded", () => {
initializeTheme();
initializeDataExplorer();
+ initSessionManagement();
+ initUIEventListeners();
initKeyboardShortcuts();
initSyncUIHandlers();
-
+
// Await the database initialization before attempting to load any files.
// This resolves the race condition on initial load.
initDB().then(async () => {
diff --git a/steps/src/session.js b/steps/src/session.js
new file mode 100644
index 0000000..a3f6e8e
--- /dev/null
+++ b/steps/src/session.js
@@ -0,0 +1,131 @@
+import { appState } from "./state.js";
+import { showModal } from "./modal.js";
+import { loadFreshFileFromDB } from "./db.js";
+import {
+ offsetInput,
+ speedSlider,
+ snrMinInput,
+ snrMaxInput,
+ toggleSnrColor,
+ toggleClusterColor,
+ toggleInlierColor,
+ toggleStationaryColor,
+ toggleVelocity,
+ toggleTracks,
+ toggleEgoSpeed,
+ toggleFrameNorm,
+ toggleDebugOverlay,
+ toggleDebug2Overlay,
+ toggleCloseUp,
+ togglePredictedPos,
+ toggleCovariance,
+ toggleConfirmedOnly,
+ saveSessionBtn,
+ loadSessionBtn,
+ sessionFileInput,
+} from "./dom.js";
+
+function saveSession() {
+ if (!appState.jsonFilename && !appState.videoFilename) {
+ showModal("Nothing to save. Please load data files first.");
+ return;
+ }
+
+ const sessionState = {
+ version: 1,
+ jsonFilename: appState.jsonFilename,
+ videoFilename: appState.videoFilename,
+ offset: offsetInput.value,
+ playbackSpeed: speedSlider.value,
+ snrMin: snrMinInput.value,
+ snrMax: snrMaxInput.value,
+ toggles: {
+ snrColor: toggleSnrColor.checked,
+ clusterColor: toggleClusterColor.checked,
+ inlierColor: toggleInlierColor.checked,
+ stationaryColor: toggleStationaryColor.checked,
+ velocity: toggleVelocity.checked,
+ tracks: toggleTracks.checked,
+ egoSpeed: toggleEgoSpeed.checked,
+ frameNorm: toggleFrameNorm.checked,
+ debugOverlay: toggleDebugOverlay.checked,
+ debug2Overlay: toggleDebug2Overlay.checked,
+ closeUp: toggleCloseUp.checked,
+ predictedPos: togglePredictedPos.checked,
+ covariance: toggleCovariance.checked,
+ confirmedOnly: toggleConfirmedOnly.checked,
+ },
+ };
+
+ const sessionString = JSON.stringify(sessionState, null, 2);
+ const blob = new Blob([sessionString], { type: "application/json" });
+ const url = URL.createObjectURL(blob);
+
+ const now = new Date();
+ const pad = (num) => String(num).padStart(2, "0");
+ const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(
+ now.getDate()
+ )}_${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`;
+ const defaultFilename = `visualizer-session_${timestamp}.json`;
+
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = defaultFilename;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+}
+
+async function loadSession(file) {
+ if (!file) return;
+
+ const reader = new FileReader();
+ reader.onload = async (e) => {
+ try {
+ const sessionState = JSON.parse(e.target.result);
+
+ if (sessionState.version !== 1 || !sessionState.jsonFilename) {
+ showModal("Error: Invalid or corrupted session file.");
+ return;
+ }
+
+ const videoBlob = await loadFreshFileFromDB("video", sessionState.videoFilename);
+ const jsonBlob = await loadFreshFileFromDB("json", sessionState.jsonFilename);
+
+ if (!jsonBlob || (sessionState.videoFilename && !videoBlob)) {
+ showModal(`Session load failed: The required data files are not in the application's cache.
+
+ Please manually load '${sessionState.jsonFilename}' and '${sessionState.videoFilename}' before loading this session.`);
+ return;
+ }
+
+ localStorage.setItem("jsonFilename", sessionState.jsonFilename || "");
+ localStorage.setItem("videoFilename", sessionState.videoFilename || "");
+ localStorage.setItem("visualizerOffset", sessionState.offset || "0");
+ localStorage.setItem("playbackSpeed", sessionState.playbackSpeed || "1");
+ localStorage.setItem("snrMin", sessionState.snrMin || "");
+ localStorage.setItem("snrMax", sessionState.snrMax || "");
+ if (sessionState.toggles) {
+ localStorage.setItem("togglesState", JSON.stringify(sessionState.toggles));
+ }
+
+ showModal("Session files found in cache. The application will now reload.").then(() => {
+ window.location.reload();
+ });
+ } catch (error) {
+ showModal("Error: Could not parse the session file. It may be invalid.");
+ console.error("Session load error:", error);
+ }
+ };
+ reader.readAsText(file);
+}
+
+export function initSessionManagement() {
+ saveSessionBtn.addEventListener("click", saveSession);
+ loadSessionBtn.addEventListener("click", () => sessionFileInput.click());
+ sessionFileInput.addEventListener("change", (event) => {
+ loadSession(event.target.files[0]);
+ event.target.value = ""; // Clear the input for future loads.
+ });
+}
\ No newline at end of file
diff --git a/steps/src/ui.js b/steps/src/ui.js
new file mode 100644
index 0000000..9de0166
--- /dev/null
+++ b/steps/src/ui.js
@@ -0,0 +1,155 @@
+import { appState } from "./state.js";
+import { formatTime } from "./utils.js";
+import { showModal } from "./modal.js";
+import { pausePlayback } from "./sync.js";
+import {
+ videoPlayer,
+ timelineSlider,
+ speedSlider,
+ speedDisplay,
+ toggleSnrColor,
+ toggleClusterColor,
+ toggleInlierColor,
+ toggleStationaryColor,
+ toggleVelocity,
+ toggleEgoSpeed,
+ toggleFrameNorm,
+ toggleTracks,
+ toggleDebugOverlay,
+ toggleDebug2Overlay,
+ toggleCloseUp,
+ snrMinInput,
+ snrMaxInput,
+ applySnrBtn,
+ timelineTooltip,
+ playPauseBtn,
+ updatePersistentOverlays,
+ updateDebugOverlay,
+ collapsibleMenu,
+ toggleMenuBtn,
+ closeMenuBtn,
+ menuScrim,
+ fullscreenBtn,
+ toggleConfirmedOnly,
+} from "./dom.js";
+
+function toggleMenu(show) {
+ if (show) {
+ collapsibleMenu.classList.remove("-translate-x-full");
+ menuScrim.classList.remove("hidden");
+ } else {
+ collapsibleMenu.classList.add("-translate-x-full");
+ menuScrim.classList.add("hidden");
+ }
+}
+
+function handleColorToggles(e) {
+ const colorToggles = [
+ toggleSnrColor,
+ toggleClusterColor,
+ toggleInlierColor,
+ toggleStationaryColor,
+ ];
+ if (e.target.checked) {
+ colorToggles.forEach((o) => {
+ if (o !== e.target) o.checked = false;
+ });
+ }
+ if (appState.p5_instance) appState.p5_instance.redraw();
+ updatePersistentOverlays(videoPlayer.currentTime);
+}
+
+export function initUIEventListeners() {
+ // --- Menu and Fullscreen ---
+ toggleMenuBtn.addEventListener("click", () => toggleMenu(true));
+ closeMenuBtn.addEventListener("click", () => toggleMenu(false));
+ menuScrim.addEventListener("click", () => toggleMenu(false));
+ fullscreenBtn.addEventListener("click", () => {
+ if (!document.fullscreenElement) {
+ document.documentElement.requestFullscreen();
+ } else if (document.exitFullscreen) {
+ document.exitFullscreen();
+ }
+ });
+
+ // --- Timeline Tooltip ---
+ timelineSlider.addEventListener("mouseover", () => {
+ if (appState.vizData) timelineTooltip.classList.remove("hidden");
+ });
+ timelineSlider.addEventListener("mouseout", () => {
+ timelineTooltip.classList.add("hidden");
+ });
+ timelineSlider.addEventListener("mousemove", (event) => {
+ if (!appState.vizData) return;
+ const rect = timelineSlider.getBoundingClientRect();
+ const hoverFraction = (event.clientX - rect.left) / rect.width;
+ const sliderMax = parseInt(timelineSlider.max, 10) || appState.vizData.radarFrames.length - 1;
+ let frameIndex = Math.max(0, Math.min(Math.round(hoverFraction * sliderMax), sliderMax));
+ const frameData = appState.vizData.radarFrames[frameIndex];
+ if (!frameData) return;
+ const formattedTime = formatTime(frameData.relativeTimeSec * 1000);
+ timelineTooltip.innerHTML = `Frame: ${frameIndex + 1}
Time: ${formattedTime}`;
+ const tooltipX = event.clientX - rect.left;
+ timelineTooltip.style.left = `${tooltipX}px`;
+ });
+
+ // --- Speed Slider ---
+ speedSlider.addEventListener("input", (event) => {
+ const speed = parseFloat(event.target.value);
+ videoPlayer.playbackRate = speed;
+ speedDisplay.textContent = `${speed.toFixed(1)}x`;
+ });
+
+ // --- SNR Controls ---
+ applySnrBtn.addEventListener("click", () => {
+ const newMin = parseFloat(snrMinInput.value), newMax = parseFloat(snrMaxInput.value);
+ if (isNaN(newMin) || isNaN(newMax) || newMin >= newMax) {
+ showModal("Invalid SNR range.");
+ return;
+ }
+ appState.globalMinSnr = newMin;
+ appState.globalMaxSnr = newMax;
+ toggleFrameNorm.checked = false;
+ if (appState.p5_instance) {
+ appState.p5_instance.drawSnrLegendToBuffer(appState.globalMinSnr, appState.globalMaxSnr);
+ appState.p5_instance.redraw();
+ }
+ });
+
+ // --- Feature Toggles ---
+ [toggleSnrColor, toggleClusterColor, toggleInlierColor, toggleStationaryColor].forEach((t) => {
+ t.addEventListener("change", handleColorToggles);
+ });
+
+ [toggleVelocity, toggleEgoSpeed, toggleFrameNorm, toggleTracks, toggleDebugOverlay, toggleDebug2Overlay].forEach((t) => {
+ t.addEventListener("change", () => {
+ if (appState.p5_instance) appState.p5_instance.redraw();
+ if (t === toggleDebugOverlay || t === toggleDebug2Overlay) {
+ updateDebugOverlay(videoPlayer.currentTime);
+ updatePersistentOverlays(videoPlayer.currentTime);
+ }
+ });
+ });
+
+ toggleCloseUp.addEventListener("change", () => {
+ appState.isCloseUpMode = toggleCloseUp.checked;
+ if (appState.isCloseUpMode && appState.isPlaying) {
+ // If entering close-up mode while playing, automatically pause.
+ pausePlayback();
+ appState.isPlaying = false;
+ playPauseBtn.textContent = "Play";
+ }
+ if (appState.p5_instance) { // Handle p5 loop state
+ if (appState.isCloseUpMode) {
+ appState.p5_instance.loop(); // Start looping for mouse interaction.
+ } else {
+ appState.p5_instance.noLoop(); // Stop looping when exiting.
+ appState.p5_instance.redraw(); // Redraw one last time.
+ }
+ }
+ });
+
+ toggleConfirmedOnly.addEventListener("change", () => {
+ if (appState.p5_instance) appState.p5_instance.redraw();
+ });
+}
\ No newline at end of file