Skip to content

style: format code with Prettier and StandardJS#4

Open
deepsource-autofix[bot] wants to merge 11 commits intomainfrom
deepsource-transform-e9dc7356
Open

style: format code with Prettier and StandardJS#4
deepsource-autofix[bot] wants to merge 11 commits intomainfrom
deepsource-transform-e9dc7356

Conversation

@deepsource-autofix
Copy link
Contributor

@deepsource-autofix deepsource-autofix bot commented Jun 17, 2025

This commit fixes the style issues introduced in c83a21c according to the output
from Prettier and StandardJS.

Details: None

Summary by Sourcery

Format JavaScript source files with Prettier and StandardJS to enforce consistent styling across the application

Enhancements:

  • Apply consistent indentation, spacing, and semicolon usage in app.js
  • Normalize arrow function syntax, parentheses, and trailing commas in event handler setups
  • Reformat voidAPI module to align with StandardJS conventions

numbpill3d and others added 11 commits June 2, 2025 16:10
- Added main application script for Conjuration, initializing core components such as UI Manager, Theme Manager, and various tools (Brush Engine, Palette Tool, etc.).
- Set up event listeners for window controls, menu management, and tool interactions.
- Created a centralized MenuManager class to handle menu interactions and state management.
- Implemented canvas size selection dialog with visual previews and resizing functionality.
- Added project management features including new, open, and save project functionalities.
- Integrated GIF and PNG export capabilities.
- Added PixelCanvas class to handle drawing on a canvas with pixel manipulation.
- Implemented methods for drawing pixels, lines, rectangles, ellipses, and flood fill.
- Introduced undo/redo functionality with history management.
- Added support for various visual effects (grain, static, glitch, CRT, scan lines, vignette, noise, pixelate).
- Implemented zooming and grid display features.
- Included methods for exporting canvas as PNG and managing pixel data.
- Set up event listeners for mouse interactions and cursor position updates.
The [optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) operator can be used to perform null checks before accessing a property, or calling a function.
This commit fixes the style issues introduced in 814a7ee according to the output
from Prettier and StandardJS.

Details: #2
refactor: convert logical operator to optional chainining
This commit fixes the style issues introduced in c83a21c according to the output
from Prettier and StandardJS.

Details: None
@sourcery-ai
Copy link

sourcery-ai bot commented Jun 17, 2025

Reviewer's Guide

This PR reformats code across app.js and voidAPI.js to comply with Prettier and StandardJS style guidelines, making purely stylistic adjustments—no functional logic was altered.

File-Level Changes

Change Details Files
Reformatted app.js to adhere to Prettier and StandardJS
  • Removed trailing semicolons from statements and declarations
  • Standardized spacing and parentheses in function and arrow definitions
  • Enforced separate lines for chained method calls and callbacks
  • Removed trailing commas in objects, arrays, and argument lists
  • Ensured consistent indentation and added final newlines
src/scripts/app.js
Reformatted voidAPI.js to conform with style rules
  • Removed semicolons and streamlined the require statement
  • Formatted promise-based arrow functions with uniform parentheses and spacing
  • Eliminated trailing commas and ensured file ends with a newline
  • Adjusted indentation for multi-line chains and callback handlers
src/scripts/lib/voidAPI.js

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@deepsource-io
Copy link
Contributor

deepsource-io bot commented Jun 17, 2025

Here's the code health analysis summary for commits c83a21c..edfd668. View details on DeepSource ↗.

Analysis Summary

AnalyzerStatusSummaryLink
DeepSource JavaScript LogoJavaScript❌ Failure
❗ 217 occurences introduced
🎯 225 occurences resolved
View Check ↗

💡 If you’re a repository administrator, you can configure the quality gates from the settings.

Comment on lines +9 to 34
document.addEventListener("DOMContentLoaded", () => {
// Initialize UI Manager
const uiManager = new UIManager();

// Initialize Theme Manager
const themeManager = new ThemeManager();

// Add data-text attributes to section titles for glitch effect
document.querySelectorAll('.section-title').forEach(title => {
title.setAttribute('data-text', title.textContent);
document.querySelectorAll(".section-title").forEach((title) => {
title.setAttribute("data-text", title.textContent);
});

// Initialize Menu System
const menuSystem = new MenuSystem();

// Initialize Canvas with temporary size (will be changed by user selection)
const pixelCanvas = new PixelCanvas({
canvasId: 'pixel-canvas',
effectsCanvasId: 'effects-canvas',
uiCanvasId: 'ui-canvas',
canvasId: "pixel-canvas",
effectsCanvasId: "effects-canvas",
uiCanvasId: "ui-canvas",
width: 64,
height: 64,
pixelSize: 8
pixelSize: 8,
});

// Show canvas size selection dialog on startup

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initialization of Components in DOMContentLoaded Event Listener

The initialization of multiple components such as UIManager, ThemeManager, MenuSystem, and PixelCanvas directly within the DOMContentLoaded event listener can lead to tightly coupled code, which is harder to maintain and test. Consider refactoring this initialization into a separate function or class that handles the setup. This approach would improve modularity and make the codebase more maintainable.

Recommendation:
Create a function like initializeApplicationComponents() that encapsulates all the initialization logic. This function can then be called within the DOMContentLoaded event listener.

Comment on lines 77 to 85
setupAnimationControls();
setupZoomControls();
setupMiscControls();

updateCanvasSizeDisplay();
uiManager.setActiveTool('brush-pencil');
uiManager.setActiveSymmetry('symmetry-none');
uiManager.setActivePalette('palette-monochrome');
uiManager.setActiveTool("brush-pencil");
uiManager.setActiveSymmetry("symmetry-none");
uiManager.setActivePalette("palette-monochrome");
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Event Listener Setup Function

The function setupEventListeners is responsible for setting up various event listeners across the application. However, this function is doing too much by handling the setup for multiple unrelated functionalities (window controls, menus, tools, etc.). This violates the Single Responsibility Principle and can make the function difficult to maintain as the application grows.

Recommendation:
Refactor setupEventListeners into multiple smaller functions, each handling a specific aspect of the application. For example, separate functions for setting up window controls, menu interactions, and tool activations would make the code more organized and maintainable.

Comment on lines 26 to 34
// Initialize Canvas with temporary size (will be changed by user selection)
const pixelCanvas = new PixelCanvas({
canvasId: 'pixel-canvas',
effectsCanvasId: 'effects-canvas',
uiCanvasId: 'ui-canvas',
canvasId: "pixel-canvas",
effectsCanvasId: "effects-canvas",
uiCanvasId: "ui-canvas",
width: 64,
height: 64,
pixelSize: 8
pixelSize: 8,
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initial Canvas Size Optimization

The canvas is initialized with a temporary size of 64x64 pixels, which is then expected to be changed by the user. This approach might not be optimal as it involves initializing with a size that might never be used, wasting resources.

Recommendation:
Consider delaying the initialization of the canvas until the user selects a size, or initialize it with a more commonly used size based on user data. This could improve the application's performance and resource usage.

Comment on lines +330 to 345
voidAPI.openProject().then((result) => {
if (result.success) {
try {
const projectData = result.data;
pixelCanvas.setDimensions(projectData.width, projectData.height);
timeline.loadFromData(projectData.frames);
menuSystem.closeAllMenus();
uiManager.showToast('Project loaded successfully', 'success');
uiManager.showToast("Project loaded successfully", "success");
} catch (error) {
uiManager.showToast('Failed to load project: ' + error.message, 'error');
uiManager.showToast(
"Failed to load project: " + error.message,
"error",
);
}
}
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error Handling in Project Opening

The function handleOpenProject uses promises to handle asynchronous file opening operations. However, it lacks error handling for scenarios where the file cannot be opened or is corrupted.

Recommendation:
Implement a .catch() method on the promise chain to handle errors gracefully. This will improve the application's robustness by informing the user of the issue and preventing the application from becoming unresponsive or crashing.

Comment on lines 79 to 196
setupAnimationControls();
setupZoomControls();
setupMiscControls();

updateCanvasSizeDisplay();
uiManager.setActiveTool('brush-pencil');
uiManager.setActiveSymmetry('symmetry-none');
uiManager.setActivePalette('palette-monochrome');
uiManager.setActiveTool("brush-pencil");
uiManager.setActiveSymmetry("symmetry-none");
uiManager.setActivePalette("palette-monochrome");
}

function setupWindowControls() {
document.getElementById('minimize-button').addEventListener('click', () => voidAPI.minimizeWindow());

document.getElementById('maximize-button').addEventListener('click', () => {
voidAPI.maximizeWindow().then(result => {
document.getElementById('maximize-button').textContent = result.isMaximized ? '□' : '[]';
document
.getElementById("minimize-button")
.addEventListener("click", () => voidAPI.minimizeWindow());

document.getElementById("maximize-button").addEventListener("click", () => {
voidAPI.maximizeWindow().then((result) => {
document.getElementById("maximize-button").textContent =
result.isMaximized ? "□" : "[]";
});
});

document.getElementById('close-button').addEventListener('click', () => voidAPI.closeWindow());

document
.getElementById("close-button")
.addEventListener("click", () => voidAPI.closeWindow());
}

function setupMenuManager() {
// Already handled by MenuManager
}

function setupFileMenu() {
document.getElementById('new-project').addEventListener('click', handleNewProject);
document.getElementById('open-project').addEventListener('click', handleOpenProject);
document.getElementById('save-project').addEventListener('click', handleSaveProject);
document
.getElementById("new-project")
.addEventListener("click", handleNewProject);
document
.getElementById("open-project")
.addEventListener("click", handleOpenProject);
document
.getElementById("save-project")
.addEventListener("click", handleSaveProject);
}

function setupEditMenu() {
document.getElementById('undo').addEventListener('click', handleUndo);
document.getElementById('redo').addEventListener('click', handleRedo);
document.getElementById('toggle-grid').addEventListener('click', handleToggleGrid);
document.getElementById('resize-canvas').addEventListener('click', handleResizeCanvas);
document.getElementById("undo").addEventListener("click", handleUndo);
document.getElementById("redo").addEventListener("click", handleRedo);
document
.getElementById("toggle-grid")
.addEventListener("click", handleToggleGrid);
document
.getElementById("resize-canvas")
.addEventListener("click", handleResizeCanvas);
}

function setupExportMenu() {
document.getElementById('export-png').addEventListener('click', handleExportPNG);
document.getElementById('export-gif').addEventListener('click', handleExportGIF);
document
.getElementById("export-png")
.addEventListener("click", handleExportPNG);
document
.getElementById("export-gif")
.addEventListener("click", handleExportGIF);
}

function setupThemeMenu() {
document.getElementById('theme-lain-dive').addEventListener('click', () => {
themeManager.setTheme('lain-dive');
document.getElementById("theme-lain-dive").addEventListener("click", () => {
themeManager.setTheme("lain-dive");
menuSystem.closeAllMenus();
});

document.getElementById('theme-morrowind-glyph').addEventListener('click', () => {
themeManager.setTheme('morrowind-glyph');
menuSystem.closeAllMenus();
});
document
.getElementById("theme-morrowind-glyph")
.addEventListener("click", () => {
themeManager.setTheme("morrowind-glyph");
menuSystem.closeAllMenus();
});

document.getElementById('theme-monolith').addEventListener('click', () => {
themeManager.setTheme('monolith');
document.getElementById("theme-monolith").addEventListener("click", () => {
themeManager.setTheme("monolith");
menuSystem.closeAllMenus();
});
}

function setupLoreMenu() {
document.getElementById('lore-option1').addEventListener('click', () => {
themeManager.setTheme('lain-dive');
document.getElementById("lore-option1").addEventListener("click", () => {
themeManager.setTheme("lain-dive");
menuSystem.closeAllMenus();
uiManager.showToast('Lore: Lain Dive activated', 'success');
uiManager.showToast("Lore: Lain Dive activated", "success");
});

document.getElementById('lore-option2').addEventListener('click', () => {
themeManager.setTheme('morrowind-glyph');
document.getElementById("lore-option2").addEventListener("click", () => {
themeManager.setTheme("morrowind-glyph");
menuSystem.closeAllMenus();
uiManager.showToast('Lore: Morrowind Glyph activated', 'success');
uiManager.showToast("Lore: Morrowind Glyph activated", "success");
});

document.getElementById('lore-option3').addEventListener('click', () => {
themeManager.setTheme('monolith');
document.getElementById("lore-option3").addEventListener("click", () => {
themeManager.setTheme("monolith");
menuSystem.closeAllMenus();
uiManager.showToast('Lore: Monolith activated', 'success');
uiManager.showToast("Lore: Monolith activated", "success");
});
}

function setupToolButtons() {
document.querySelectorAll('.tool-button').forEach(button => {
button.addEventListener('click', () => {
document.querySelectorAll(".tool-button").forEach((button) => {
button.addEventListener("click", () => {
const toolId = button.id;

if (toolId.startsWith('brush-')) {
const brushType = toolId.replace('brush-', '');
if (toolId.startsWith("brush-")) {
const brushType = toolId.replace("brush-", "");
brushEngine.setActiveBrush(brushType);
uiManager.setActiveTool(toolId);
}

if (toolId.startsWith('symmetry-')) {
const symmetryType = toolId.replace('symmetry-', '');
if (toolId.startsWith("symmetry-")) {
const symmetryType = toolId.replace("symmetry-", "");
symmetryTools.setSymmetryMode(symmetryType);
uiManager.setActiveSymmetry(toolId);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Event Listener Setup in a Loop

The setupToolButtons function (lines 181-198) adds event listeners to elements with the class .tool-button. Each button click triggers a check for the button's ID to determine the action, which involves string operations and conditional checks inside the event listener.

Recommendation:

  • Optimize by reducing redundancy: Instead of checking the ID for every click, categorize buttons beforehand and add specific event listeners for each category. This reduces the need for string operations and conditions during runtime, improving performance.

Comment on lines 747 to 772
// Checkerboard
[
[1, 0],
[0, 1]
[0, 1],
],
// Diagonal lines
[
[1, 0, 0],
[0, 1, 0],
[0, 0, 1]
[0, 0, 1],
],
// Dots
[
[0, 0, 0],
[0, 1, 0],
[0, 0, 0]
[0, 0, 0],
],
// Cross
[
[0, 1, 0],
[1, 1, 1],
[0, 1, 0]
]
[0, 1, 0],
],
];

// Select a pattern based on brush size

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The applyPatternBrush method exhibits high complexity due to multiple nested loops and conditional checks. This complexity can make the code hard to maintain or modify, especially when adding new patterns or changing brush behavior.

Recommendation: Refactor the method to simplify the logic, possibly by extracting pattern application into a separate function or by using a strategy pattern to handle different brush types more cleanly.


// Show welcome message
uiManager.showToast('Welcome to Conjuration', 'success');
uiManager.showToast('Welcome to Conjuration', 'success')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: Lack of Error Handling and Dependency Checks

The initialization of various managers (UIManager, ThemeManager, etc.) and systems (MenuSystem, PixelCanvas) is done sequentially without any error handling or checks for successful initialization. This could lead to runtime errors if any dependencies fail to load or initialize correctly.

Recommendation:

  • Implement error handling around each initialization step. Use try-catch blocks or conditional checks to ensure each component is loaded successfully before proceeding to the next. This will prevent the application from breaking if one part fails to initialize properly.


// Show welcome message
uiManager.showToast('Welcome to Conjuration', 'success');
uiManager.showToast('Welcome to Conjuration', 'success')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: Inefficient DOM Manipulations and Event Listener Management

The application frequently queries the DOM and attaches event listeners within the DOMContentLoaded event. This approach can lead to performance issues and potential memory leaks if the listeners are not managed properly.

Recommendation:

  • Optimize DOM access by caching frequently accessed elements in variables.
  • Ensure that event listeners are removed appropriately when they are no longer needed or when the associated elements are removed from the DOM. This can be managed through a centralized event handling system or by using more modern frameworks that handle such clean-ups implicitly.

Comment on lines +8 to +39
maximizeWindow: () =>
new Promise((resolve) => {
ipcRenderer.send('maximize-window')
ipcRenderer.once('maximize-reply', (_, result) => resolve(result))
}),

closeWindow: () => ipcRenderer.send('close-window'),

openProject: () => new Promise((resolve) => {
ipcRenderer.send('open-project');
ipcRenderer.once('open-project-reply', (_, result) => resolve(result));
}),

saveProject: (projectData) => new Promise((resolve) => {
ipcRenderer.send('save-project', projectData);
ipcRenderer.once('save-project-reply', (_, result) => resolve(result));
}),

exportPng: (dataUrl) => new Promise((resolve) => {
ipcRenderer.send('export-png', dataUrl);
ipcRenderer.once('export-png-reply', (_, result) => resolve(result));
}),

exportGif: (gifData) => new Promise((resolve) => {
ipcRenderer.send('export-gif', gifData);
ipcRenderer.once('export-gif-reply', (_, result) => resolve(result));
})
};

module.exports = voidAPI; No newline at end of file

openProject: () =>
new Promise((resolve) => {
ipcRenderer.send('open-project')
ipcRenderer.once('open-project-reply', (_, result) => resolve(result))
}),

saveProject: (projectData) =>
new Promise((resolve) => {
ipcRenderer.send('save-project', projectData)
ipcRenderer.once('save-project-reply', (_, result) => resolve(result))
}),

exportPng: (dataUrl) =>
new Promise((resolve) => {
ipcRenderer.send('export-png', dataUrl)
ipcRenderer.once('export-png-reply', (_, result) => resolve(result))
}),

exportGif: (gifData) =>
new Promise((resolve) => {
ipcRenderer.send('export-gif', gifData)
ipcRenderer.once('export-gif-reply', (_, result) => resolve(result))
})

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lack of Error Handling in Promise-based IPC Calls

The Promise-based functions (maximizeWindow, openProject, saveProject, exportPng, exportGif) do not handle errors that may occur during IPC communication. This can lead to unresolved Promises if the IPC main process encounters an error or does not respond, affecting the application's reliability and user experience.

Recommendation: Implement error handling by adding rejection conditions and handling IPC errors. For example, you can modify the IPC calls to handle error scenarios by using ipcRenderer.once for error events and rejecting the Promise accordingly:

ipcRenderer.once('maximize-window-error', (_, error) => reject(error));

Comment on lines +23 to +37

saveProject: (projectData) =>
new Promise((resolve) => {
ipcRenderer.send('save-project', projectData)
ipcRenderer.once('save-project-reply', (_, result) => resolve(result))
}),

exportPng: (dataUrl) =>
new Promise((resolve) => {
ipcRenderer.send('export-png', dataUrl)
ipcRenderer.once('export-png-reply', (_, result) => resolve(result))
}),

exportGif: (gifData) =>
new Promise((resolve) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security Concerns with User Data in IPC Calls

The functions saveProject and exportGif send user-provided data (projectData and gifData) through IPC without apparent validation or sanitization. This could lead to security vulnerabilities such as IPC event injection or unintended behavior if the data contains malicious structures.

Recommendation: Ensure that all user-provided data is validated and sanitized before being sent through IPC. Implement checks to ensure the data structure conforms to expected schemas and does not contain potentially harmful content:

if (isValidProjectData(projectData)) {
  ipcRenderer.send('save-project', projectData);
} else {
  throw new Error('Invalid project data');
}

Comment on lines +9 to 34
document.addEventListener("DOMContentLoaded", () => {
// Initialize UI Manager
const uiManager = new UIManager();

// Initialize Theme Manager
const themeManager = new ThemeManager();

// Add data-text attributes to section titles for glitch effect
document.querySelectorAll('.section-title').forEach(title => {
title.setAttribute('data-text', title.textContent);
document.querySelectorAll(".section-title").forEach((title) => {
title.setAttribute("data-text", title.textContent);
});

// Initialize Menu System
const menuSystem = new MenuSystem();

// Initialize Canvas with temporary size (will be changed by user selection)
const pixelCanvas = new PixelCanvas({
canvasId: 'pixel-canvas',
effectsCanvasId: 'effects-canvas',
uiCanvasId: 'ui-canvas',
canvasId: "pixel-canvas",
effectsCanvasId: "effects-canvas",
uiCanvasId: "ui-canvas",
width: 64,
height: 64,
pixelSize: 8
pixelSize: 8,
});

// Show canvas size selection dialog on startup

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initialization Order Dependency

The initialization order of the components (UIManager, ThemeManager, MenuSystem, PixelCanvas) is critical for the application's correct functioning. If any component relies on the state or methods of another during its initialization, it could lead to runtime errors or undefined behavior if the dependent component is not yet initialized.

Recommendation:
Ensure that no component's initialization depends on the state or methods of another component that is initialized later. Consider implementing a dependency injection pattern or ensuring that all components are fully initialized before they are used.

Comment on lines 77 to 194
setupAnimationControls();
setupZoomControls();
setupMiscControls();

updateCanvasSizeDisplay();
uiManager.setActiveTool('brush-pencil');
uiManager.setActiveSymmetry('symmetry-none');
uiManager.setActivePalette('palette-monochrome');
uiManager.setActiveTool("brush-pencil");
uiManager.setActiveSymmetry("symmetry-none");
uiManager.setActivePalette("palette-monochrome");
}

function setupWindowControls() {
document.getElementById('minimize-button').addEventListener('click', () => voidAPI.minimizeWindow());

document.getElementById('maximize-button').addEventListener('click', () => {
voidAPI.maximizeWindow().then(result => {
document.getElementById('maximize-button').textContent = result.isMaximized ? '□' : '[]';
document
.getElementById("minimize-button")
.addEventListener("click", () => voidAPI.minimizeWindow());

document.getElementById("maximize-button").addEventListener("click", () => {
voidAPI.maximizeWindow().then((result) => {
document.getElementById("maximize-button").textContent =
result.isMaximized ? "□" : "[]";
});
});

document.getElementById('close-button').addEventListener('click', () => voidAPI.closeWindow());

document
.getElementById("close-button")
.addEventListener("click", () => voidAPI.closeWindow());
}

function setupMenuManager() {
// Already handled by MenuManager
}

function setupFileMenu() {
document.getElementById('new-project').addEventListener('click', handleNewProject);
document.getElementById('open-project').addEventListener('click', handleOpenProject);
document.getElementById('save-project').addEventListener('click', handleSaveProject);
document
.getElementById("new-project")
.addEventListener("click", handleNewProject);
document
.getElementById("open-project")
.addEventListener("click", handleOpenProject);
document
.getElementById("save-project")
.addEventListener("click", handleSaveProject);
}

function setupEditMenu() {
document.getElementById('undo').addEventListener('click', handleUndo);
document.getElementById('redo').addEventListener('click', handleRedo);
document.getElementById('toggle-grid').addEventListener('click', handleToggleGrid);
document.getElementById('resize-canvas').addEventListener('click', handleResizeCanvas);
document.getElementById("undo").addEventListener("click", handleUndo);
document.getElementById("redo").addEventListener("click", handleRedo);
document
.getElementById("toggle-grid")
.addEventListener("click", handleToggleGrid);
document
.getElementById("resize-canvas")
.addEventListener("click", handleResizeCanvas);
}

function setupExportMenu() {
document.getElementById('export-png').addEventListener('click', handleExportPNG);
document.getElementById('export-gif').addEventListener('click', handleExportGIF);
document
.getElementById("export-png")
.addEventListener("click", handleExportPNG);
document
.getElementById("export-gif")
.addEventListener("click", handleExportGIF);
}

function setupThemeMenu() {
document.getElementById('theme-lain-dive').addEventListener('click', () => {
themeManager.setTheme('lain-dive');
document.getElementById("theme-lain-dive").addEventListener("click", () => {
themeManager.setTheme("lain-dive");
menuSystem.closeAllMenus();
});

document.getElementById('theme-morrowind-glyph').addEventListener('click', () => {
themeManager.setTheme('morrowind-glyph');
menuSystem.closeAllMenus();
});
document
.getElementById("theme-morrowind-glyph")
.addEventListener("click", () => {
themeManager.setTheme("morrowind-glyph");
menuSystem.closeAllMenus();
});

document.getElementById('theme-monolith').addEventListener('click', () => {
themeManager.setTheme('monolith');
document.getElementById("theme-monolith").addEventListener("click", () => {
themeManager.setTheme("monolith");
menuSystem.closeAllMenus();
});
}

function setupLoreMenu() {
document.getElementById('lore-option1').addEventListener('click', () => {
themeManager.setTheme('lain-dive');
document.getElementById("lore-option1").addEventListener("click", () => {
themeManager.setTheme("lain-dive");
menuSystem.closeAllMenus();
uiManager.showToast('Lore: Lain Dive activated', 'success');
uiManager.showToast("Lore: Lain Dive activated", "success");
});

document.getElementById('lore-option2').addEventListener('click', () => {
themeManager.setTheme('morrowind-glyph');
document.getElementById("lore-option2").addEventListener("click", () => {
themeManager.setTheme("morrowind-glyph");
menuSystem.closeAllMenus();
uiManager.showToast('Lore: Morrowind Glyph activated', 'success');
uiManager.showToast("Lore: Morrowind Glyph activated", "success");
});

document.getElementById('lore-option3').addEventListener('click', () => {
themeManager.setTheme('monolith');
document.getElementById("lore-option3").addEventListener("click", () => {
themeManager.setTheme("monolith");
menuSystem.closeAllMenus();
uiManager.showToast('Lore: Monolith activated', 'success');
uiManager.showToast("Lore: Monolith activated", "success");
});
}

function setupToolButtons() {
document.querySelectorAll('.tool-button').forEach(button => {
button.addEventListener('click', () => {
document.querySelectorAll(".tool-button").forEach((button) => {
button.addEventListener("click", () => {
const toolId = button.id;

if (toolId.startsWith('brush-')) {
const brushType = toolId.replace('brush-', '');
if (toolId.startsWith("brush-")) {
const brushType = toolId.replace("brush-", "");
brushEngine.setActiveBrush(brushType);
uiManager.setActiveTool(toolId);
}

if (toolId.startsWith('symmetry-')) {
const symmetryType = toolId.replace('symmetry-', '');
if (toolId.startsWith("symmetry-")) {
const symmetryType = toolId.replace("symmetry-", "");
symmetryTools.setSymmetryMode(symmetryType);
uiManager.setActiveSymmetry(toolId);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential Memory Leak in Event Listeners

The setup functions for various UI controls add event listeners but do not provide a mechanism to remove these listeners when they are no longer needed or when the UI components are destroyed. This can lead to memory leaks, especially if these components are frequently created and destroyed during the application's lifecycle.

Recommendation:
Implement a cleanup function that removes all event listeners added by the setup functions. This function should be called whenever the UI components are destroyed or re-initialized. Use named functions for event listeners to make them easier to remove.

Comment on lines +330 to 345
voidAPI.openProject().then((result) => {
if (result.success) {
try {
const projectData = result.data;
pixelCanvas.setDimensions(projectData.width, projectData.height);
timeline.loadFromData(projectData.frames);
menuSystem.closeAllMenus();
uiManager.showToast('Project loaded successfully', 'success');
uiManager.showToast("Project loaded successfully", "success");
} catch (error) {
uiManager.showToast('Failed to load project: ' + error.message, 'error');
uiManager.showToast(
"Failed to load project: " + error.message,
"error",
);
}
}
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error Handling in Asynchronous Operations

The function handleOpenProject uses asynchronous operations to open a project file and update the UI based on the result. However, the error handling is minimal and does not cover scenarios where the openProject function might fail before reaching the .then block, which could leave the application in an inconsistent state.

Recommendation:

  • Wrap the voidAPI.openProject() call within a try-catch block to handle potential rejections from the promise and ensure that the application can gracefully handle these errors.

Comment on lines 352 to 370
frames: timeline.getFramesData(),
palette: paletteTool.getCurrentPalette(),
effects: {
grain: document.getElementById('effect-grain').checked,
static: document.getElementById('effect-static').checked,
glitch: document.getElementById('effect-glitch').checked,
crt: document.getElementById('effect-crt').checked,
intensity: document.getElementById('effect-intensity').value
}
grain: document.getElementById("effect-grain").checked,
static: document.getElementById("effect-static").checked,
glitch: document.getElementById("effect-glitch").checked,
crt: document.getElementById("effect-crt").checked,
intensity: document.getElementById("effect-intensity").value,
},
};

voidAPI.saveProject(projectData).then(result => {
voidAPI.saveProject(projectData).then((result) => {
if (result.success) {
menuSystem.closeAllMenus();
uiManager.showToast('Project saved successfully', 'success');
uiManager.showToast("Project saved successfully", "success");
} else {
uiManager.showToast('Failed to save project', 'error');
uiManager.showToast("Failed to save project", "error");
}
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential Issue with Asynchronous Error Handling

The function voidAPI.saveProject is used to save project data asynchronously. However, the error handling is only done using a success flag from the response, and there's no .catch() method for handling possible promise rejections, which could lead to unhandled promise rejections if the API call fails at a network level or due to other reasons.

Recommendation:
Add a .catch() method to handle potential errors from the promise, ensuring that all possible failures are gracefully handled and reported to the user. This will improve the robustness of the application's error handling strategy.

Example:

voidAPI.saveProject(projectData).then((result) => {
  if (result.success) {
    uiManager.showToast("Project saved successfully", "success");
  } else {
    uiManager.showToast("Failed to save project", "error");
  }
}).catch((error) => {
  uiManager.showToast("Error saving project: " + error.message, "error");
});

Comment on lines 424 to 471
</div>
`;

uiManager.showModal('Resize Canvas', content, () => menuSystem.closeAllMenus());
uiManager.showModal("Resize Canvas", content, () =>
menuSystem.closeAllMenus(),
);

document.querySelectorAll('.preset-size-button').forEach(button => {
button.addEventListener('click', () => {
document.querySelectorAll(".preset-size-button").forEach((button) => {
button.addEventListener("click", () => {
const width = parseInt(button.dataset.width);
const height = parseInt(button.dataset.height);
document.getElementById('canvas-width').value = width;
document.getElementById('canvas-height').value = height;
document.getElementById("canvas-width").value = width;
document.getElementById("canvas-height").value = height;
});
});

const modalFooter = document.createElement('div');
modalFooter.className = 'modal-footer';
const modalFooter = document.createElement("div");
modalFooter.className = "modal-footer";

const cancelButton = document.createElement('button');
cancelButton.className = 'modal-button';
cancelButton.textContent = 'Cancel';
cancelButton.addEventListener('click', () => uiManager.hideModal());
const cancelButton = document.createElement("button");
cancelButton.className = "modal-button";
cancelButton.textContent = "Cancel";
cancelButton.addEventListener("click", () => uiManager.hideModal());

const resizeButton = document.createElement('button');
resizeButton.className = 'modal-button primary';
resizeButton.textContent = 'Resize';
resizeButton.addEventListener('click', () => {
const width = parseInt(document.getElementById('canvas-width').value);
const height = parseInt(document.getElementById('canvas-height').value);
const preserveContent = document.getElementById('preserve-content').checked;
const resizeButton = document.createElement("button");
resizeButton.className = "modal-button primary";
resizeButton.textContent = "Resize";
resizeButton.addEventListener("click", () => {
const width = parseInt(document.getElementById("canvas-width").value);
const height = parseInt(document.getElementById("canvas-height").value);
const preserveContent =
document.getElementById("preserve-content").checked;

if (width > 0 && height > 0 && width <= 1024 && height <= 1024) {
pixelCanvas.resize(width, height, preserveContent);
updateCanvasSizeDisplay();
uiManager.hideModal();
uiManager.showToast(`Canvas resized to ${width}×${height}`, 'success');
uiManager.showToast(`Canvas resized to ${width}×${height}`, "success");
} else {
uiManager.showToast('Invalid dimensions', 'error');
uiManager.showToast("Invalid dimensions", "error");
}
});

modalFooter.appendChild(cancelButton);
modalFooter.appendChild(resizeButton);
document.querySelector('.modal-dialog').appendChild(modalFooter);
document.querySelector(".modal-dialog").appendChild(modalFooter);
menuSystem.closeAllMenus();
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inefficient DOM Manipulation and Event Handling

The function handleResizeCanvas dynamically creates modal content and buttons every time it is called. This approach can lead to inefficient DOM manipulation and potential memory leaks if event listeners are not properly managed. Each call to this function creates new DOM elements and attaches event listeners to them, which can accumulate over time if not properly removed.

Recommendation:
Consider creating the modal content and buttons once and reusing them across calls. This can be achieved by checking if the elements already exist before creating new ones, and by properly removing event listeners when the modal is closed or no longer needed. This change will enhance the performance by reducing the number of DOM operations and preventing potential memory leaks.

Example:

if (!document.querySelector('.modal-footer')) {
  // Create and append modal footer and buttons
}
// Setup event listeners
// Reuse existing elements and update values as needed

Comment on lines 825 to 834
if (distance > lineOffset) continue;

// Get pattern value with proper centering
const patternX = (px + j + patternOffsetX + patternSize) % patternSize;
const patternY = (py + k + patternOffsetY + patternSize) % patternSize;
const patternX =
(px + j + patternOffsetX + patternSize) % patternSize;
const patternY =
(py + k + patternOffsetY + patternSize) % patternSize;

// Draw the pixel based on pattern
if (pattern[patternY][patternX]) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error Handling and Boundary Check

The pattern brush drawing logic does not handle potential errors or undefined behavior when accessing array indices that might be out of bounds. This could occur if the calculated patternX or patternY exceeds the dimensions of the pattern array due to incorrect modulo operations or negative indices.

Suggested Change:
Ensure that the indices patternX and patternY are always within the valid range of the pattern array dimensions by using proper modulo operations and handling negative indices correctly.

const patternX = Math.abs((px + j + patternSize) % patternSize);
const patternY = Math.abs((py + k + patternSize) % patternSize);

Comment on lines +474 to 508
function handleExportPNG () {
const pngDataUrl = pixelCanvas.exportToPNG()
try {
voidAPI.exportPng(pngDataUrl).then(result => {
if (result.success) {
menuSystem.closeAllMenus();
uiManager.showToast('PNG exported successfully', 'success');
} else {
uiManager.showToast('Failed to export PNG', 'error');
}
}).catch(error => {
voidAPI.exportPng(pngDataUrl).then(result => {
if (result.success) {
menuSystem.closeAllMenus();
uiManager.showToast('PNG exported successfully', 'success');
} else {
uiManager.showToast('Failed to export PNG', 'error');
}
}).catch(error => {
uiManager.showToast(`PNG export failed: ${error.message}`, 'error');
});
uiManager.showToast(`PNG export failed: ${error.message}`, 'error');
});
voidAPI
.exportPng(pngDataUrl)
.then((result) => {
if (result.success) {
menuSystem.closeAllMenus()
uiManager.showToast('PNG exported successfully', 'success')
} else {
uiManager.showToast('Failed to export PNG', 'error')
}
})
.catch((error) => {
voidAPI
.exportPng(pngDataUrl)
.then((result) => {
if (result.success) {
menuSystem.closeAllMenus()
uiManager.showToast('PNG exported successfully', 'success')
} else {
uiManager.showToast('Failed to export PNG', 'error')
}
})
.catch((error) => {
uiManager.showToast(
`PNG export failed: ${error.message}`,
'error'
)
})
uiManager.showToast(`PNG export failed: ${error.message}`, 'error')
})
} catch (error) {
uiManager.showToast(`PNG export error: ${error.message}`, 'error');
uiManager.showToast(`PNG export error: ${error.message}`, 'error')
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The handleExportPNG function attempts to retry the export operation in the catch block if an error occurs. This approach can lead to potential infinite loops if the error persists. Instead of retrying immediately, consider implementing a more robust error handling strategy. This could include logging the error, notifying the user of the failure, and potentially providing a limited number of retry attempts with a delay or different approach if feasible.

Comment on lines +511 to 616
function handleExportGIF () {
uiManager.showLoadingDialog('Generating GIF...')
const frameDelay = parseInt(document.getElementById('frame-delay').value)
try {
gifExporter
.generateGif(frameDelay)
.then((gifData) => {
voidAPI
.exportGif(gifData)
.then((result) => {
uiManager.hideLoadingDialog()
if (result.success) {
menuSystem.closeAllMenus()
uiManager.showToast('GIF exported successfully', 'success')
} else {
uiManager.showToast('Failed to export GIF', 'error')
}
})
.catch((error) => {
uiManager.hideLoadingDialog()
uiManager.showToast(
`GIF export failed: ${error.message}`,
'error'
)
})
})
.catch((error) => {
uiManager.hideLoadingDialog()
uiManager.showToast(
`GIF generation failed: ${error.message}`,
'error'
)
})
} catch (error) {
uiManager.hideLoadingDialog();
uiManager.showToast(`GIF export error: ${error.message}`, 'error');
uiManager.hideLoadingDialog()
uiManager.showToast(`GIF export error: ${error.message}`, 'error')
}
try {
gifExporter.generateGif(frameDelay).then(gifData => {
voidAPI.exportGif(gifData).then(result => {
uiManager.hideLoadingDialog();
if (result.success) {
menuSystem.closeAllMenus();
uiManager.showToast('GIF exported successfully', 'success');
} else {
uiManager.showToast('Failed to export GIF', 'error');
}
}).catch(error => {
uiManager.hideLoadingDialog();
uiManager.showToast(`GIF export failed: ${error.message}`, 'error');
});
}).catch(error => {
uiManager.hideLoadingDialog();
uiManager.showToast(`GIF generation failed: ${error.message}`, 'error');
});
try {
gifExporter
.generateGif(frameDelay)
.then((gifData) => {
voidAPI
.exportGif(gifData)
.then((result) => {
uiManager.hideLoadingDialog()
if (result.success) {
menuSystem.closeAllMenus()
uiManager.showToast('GIF exported successfully', 'success')
} else {
uiManager.showToast('Failed to export GIF', 'error')
}
})
.catch((error) => {
uiManager.hideLoadingDialog()
uiManager.showToast(
`GIF export failed: ${error.message}`,
'error'
)
})
})
.catch((error) => {
uiManager.hideLoadingDialog()
uiManager.showToast(
`GIF generation failed: ${error.message}`,
'error'
)
})
} catch (error) {
uiManager.hideLoadingDialog();
uiManager.showToast(`GIF export error: ${error.message}`, 'error');
uiManager.hideLoadingDialog()
uiManager.showToast(`GIF export error: ${error.message}`, 'error')
}

try {
gifExporter.generateGif(frameDelay).then(gifData => {
voidAPI.exportGif(gifData).then(result => {
uiManager.hideLoadingDialog();
if (result.success) {
menuSystem.closeAllMenus();
uiManager.showToast('GIF exported successfully', 'success');
} else {
uiManager.showToast('Failed to export GIF', 'error');
}
}).catch(error => {
uiManager.hideLoadingDialog();
uiManager.showToast(`GIF export failed: ${error.message}`, 'error');
});
}).catch(error => {
uiManager.hideLoadingDialog();
uiManager.showToast(`GIF generation failed: ${error.message}`, 'error');
});
gifExporter
.generateGif(frameDelay)
.then((gifData) => {
voidAPI
.exportGif(gifData)
.then((result) => {
uiManager.hideLoadingDialog()
if (result.success) {
menuSystem.closeAllMenus()
uiManager.showToast('GIF exported successfully', 'success')
} else {
uiManager.showToast('Failed to export GIF', 'error')
}
})
.catch((error) => {
uiManager.hideLoadingDialog()
uiManager.showToast(
`GIF export failed: ${error.message}`,
'error'
)
})
})
.catch((error) => {
uiManager.hideLoadingDialog()
uiManager.showToast(
`GIF generation failed: ${error.message}`,
'error'
)
})
} catch (error) {
uiManager.hideLoadingDialog();
uiManager.showToast(`GIF export error: ${error.message}`, 'error');
uiManager.hideLoadingDialog()
uiManager.showToast(`GIF export error: ${error.message}`, 'error')
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The handleExportGIF function contains repeated blocks of code for exporting GIFs, which not only bloats the codebase but also introduces potential performance issues. Refactor this function to eliminate redundancy and improve code maintainability. Consider abstracting the common functionality into a separate function or using a loop to handle retries, which would make the code cleaner and easier to manage.

Comment on lines +8 to +38
maximizeWindow: () =>
new Promise((resolve) => {
ipcRenderer.send('maximize-window')
ipcRenderer.once('maximize-reply', (_, result) => resolve(result))
}),

closeWindow: () => ipcRenderer.send('close-window'),

openProject: () => new Promise((resolve) => {
ipcRenderer.send('open-project');
ipcRenderer.once('open-project-reply', (_, result) => resolve(result));
}),

saveProject: (projectData) => new Promise((resolve) => {
ipcRenderer.send('save-project', projectData);
ipcRenderer.once('save-project-reply', (_, result) => resolve(result));
}),

exportPng: (dataUrl) => new Promise((resolve) => {
ipcRenderer.send('export-png', dataUrl);
ipcRenderer.once('export-png-reply', (_, result) => resolve(result));
}),

exportGif: (gifData) => new Promise((resolve) => {
ipcRenderer.send('export-gif', gifData);
ipcRenderer.once('export-gif-reply', (_, result) => resolve(result));
})
};

module.exports = voidAPI; No newline at end of file

openProject: () =>
new Promise((resolve) => {
ipcRenderer.send('open-project')
ipcRenderer.once('open-project-reply', (_, result) => resolve(result))
}),

saveProject: (projectData) =>
new Promise((resolve) => {
ipcRenderer.send('save-project', projectData)
ipcRenderer.once('save-project-reply', (_, result) => resolve(result))
}),

exportPng: (dataUrl) =>
new Promise((resolve) => {
ipcRenderer.send('export-png', dataUrl)
ipcRenderer.once('export-png-reply', (_, result) => resolve(result))
}),

exportGif: (gifData) =>
new Promise((resolve) => {
ipcRenderer.send('export-gif', gifData)
ipcRenderer.once('export-gif-reply', (_, result) => resolve(result))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The methods maximizeWindow, openProject, saveProject, exportPng, and exportGif use promises to handle IPC communication but do not include error handling mechanisms. This can lead to unhandled promise rejections if the main process fails to send a reply or encounters an error.

Recommended Solution:
Implement error handling by adding rejection scenarios in the promises. For example, you can use a timeout to reject the promise if no reply is received within a certain timeframe, and handle potential errors sent from the main process:

new Promise((resolve, reject) => {
  ipcRenderer.send('maximize-window');
  ipcRenderer.once('maximize-reply', (_, result) => resolve(result));
  setTimeout(() => reject(new Error('Timeout waiting for maximize reply')), 5000);
});

Comment on lines +9 to +38
maximizeWindow: () =>
new Promise((resolve) => {
ipcRenderer.send('maximize-window')
ipcRenderer.once('maximize-reply', (_, result) => resolve(result))
}),

closeWindow: () => ipcRenderer.send('close-window'),

openProject: () => new Promise((resolve) => {
ipcRenderer.send('open-project');
ipcRenderer.once('open-project-reply', (_, result) => resolve(result));
}),

saveProject: (projectData) => new Promise((resolve) => {
ipcRenderer.send('save-project', projectData);
ipcRenderer.once('save-project-reply', (_, result) => resolve(result));
}),

exportPng: (dataUrl) => new Promise((resolve) => {
ipcRenderer.send('export-png', dataUrl);
ipcRenderer.once('export-png-reply', (_, result) => resolve(result));
}),

exportGif: (gifData) => new Promise((resolve) => {
ipcRenderer.send('export-gif', gifData);
ipcRenderer.once('export-gif-reply', (_, result) => resolve(result));
})
};

module.exports = voidAPI; No newline at end of file

openProject: () =>
new Promise((resolve) => {
ipcRenderer.send('open-project')
ipcRenderer.once('open-project-reply', (_, result) => resolve(result))
}),

saveProject: (projectData) =>
new Promise((resolve) => {
ipcRenderer.send('save-project', projectData)
ipcRenderer.once('save-project-reply', (_, result) => resolve(result))
}),

exportPng: (dataUrl) =>
new Promise((resolve) => {
ipcRenderer.send('export-png', dataUrl)
ipcRenderer.once('export-png-reply', (_, result) => resolve(result))
}),

exportGif: (gifData) =>
new Promise((resolve) => {
ipcRenderer.send('export-gif', gifData)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of ipcRenderer.once to register event listeners for IPC replies without ensuring that these listeners are removed when no reply is received could lead to memory leaks. This is particularly concerning in scenarios where the main process might fail to send a reply, leaving the event listeners hanging indefinitely.

Recommended Solution:
Ensure that event listeners are removed appropriately by adding a cleanup mechanism. This could be implemented using a timeout to remove the listener if no reply is received within a reasonable timeframe:

new Promise((resolve, reject) => {
  const handler = (_, result) => {
    ipcRenderer.removeListener('maximize-reply', handler);
    resolve(result);
  };
  ipcRenderer.once('maximize-reply', handler);
  setTimeout(() => {
    ipcRenderer.removeListener('maximize-reply', handler);
    reject(new Error('Timeout waiting for maximize reply'));
  }, 5000);
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant