Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules
package-lock.json
output/
output/
dist/
129 changes: 129 additions & 0 deletions core/pix.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import * as jsonfile from "jsonfile";
import sharp from "sharp";

/**
* Pix data structure for custom .pix format
*/
export interface PixData {
width: number;
height: number;
channel: number;
depth: number;
compression: {
method: string;
};
hex_data: string;
label: Record<string, any>;
joint: Record<string, any>;
}

/**
* Pix wrapper structure (may contain data property)
*/
export interface PixWrapper {
data?: PixData;
width?: number;
height?: number;
channel?: number;
depth?: number;
hex_data?: string;
}

/**
* Pix class - Handles reading/writing custom .pix image format
*/
export class Pix {
/**
* Open a .pix file from disk
* @param path - File path to read from
* @returns Parsed pix data or null on error
*/
static open(path: string): PixData | null {
try {
const data = jsonfile.readFileSync(path);
return data;
} catch (err) {
console.error("Error reading pixData.json:", err);
return null;
}
}

/**
* Save pix data to disk
* @param pixData - Pix data to save
* @param path - File path to save to
*/
static save(pixData: PixData, path: string): void {
try {
jsonfile.writeFileSync(path, pixData, { spaces: 2 });
} catch (err) {
console.error("Error writing pixData.json:", err);
}
}

/**
* Parse pix data from buffer
* @param buffer - Buffer or string containing pix JSON data
* @returns Parsed pix data or null on error
*/
static openFromBuffer(buffer: Buffer | string): PixData | null {
try {
const text = Buffer.isBuffer(buffer) ? buffer.toString("utf-8") : buffer;
return JSON.parse(text);
} catch (err) {
console.error("Error parsing pix buffer:", err);
return null;
}
}

/**
* Convert pix data to Sharp buffer
* @param pixData - Pix data to convert
* @returns Sharp buffer with metadata or null on error
*/
static async toSharp(
pixData: PixWrapper
): Promise<{ data: Buffer; info: sharp.OutputInfo } | null> {
const payload = pixData?.data ?? pixData;
const { width, height, channel, depth, hex_data } = payload;
if (!width || !height || !hex_data) return null;

const channels = (channel ?? 4) as 1 | 2 | 3 | 4;
const raw = Buffer.from(hex_data, "hex");
const bytesPerPixel = Math.ceil((depth ?? 8) / 8) * channels;
const expectedLength = width * height * bytesPerPixel;
if (raw.length < expectedLength) raw.fill(0, raw.length, expectedLength);

return sharp(raw, {
raw: { width, height, channels },
})
.png()
.toBuffer({ resolveWithObject: true });
}

/**
* Convert image buffer to pix format
* @param buffer - Image buffer to convert
* @param info - Image metadata
* @returns Pix data structure
*/
static async toPix(buffer: Buffer, info: sharp.Metadata): Promise<PixData> {
const { data, info: rawInfo } = await sharp(buffer)
.raw()
.toBuffer({ resolveWithObject: true });

return {
width: info.width!,
height: info.height!,
channel: info.channels!,
depth: (info as any).bitsPerSample || 8,
compression: {
method: "none",
},
hex_data: data.toString("hex"),
label: {},
joint: {},
};
}
}

147 changes: 147 additions & 0 deletions features/image_mode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/**
* ImageModes - Unified mode system for image layer interactions
*
* Replaces individual flags (drawFlag, dragFlag, magnifyFlag) with a single mode state.
* This makes the code more maintainable and prevents conflicting states.
*/

import * as path from "path";

/**
* Available interaction modes for image layers
*/
export enum ImageMode {
/** Default mode - no special interaction */
NORMAL = "normal",

/** Drawing/painting mode - user is drawing on canvas */
DRAWING = "drawing",

/** Cropping mode - user is selecting a crop area (crosshair cursor) */
CROPPING = "cropping",

/** Magnifying glass mode - zoomed preview follows mouse (Alt + M) */
MAGNIFY = "magnify",

/** Color picker mode - user is picking a color from the image */
COLORPICKER = "colorpicker",
}

/**
* Mode manager utility class
* Provides helper methods for mode management
*/
export class ModeManager {
private currentMode: ImageMode = ImageMode.NORMAL;
private previousMode: ImageMode = ImageMode.NORMAL;

/**
* Set the current mode
* @param mode - The mode to set
*/
setMode(mode: ImageMode): void {
if (!Object.values(ImageMode).includes(mode)) {
console.warn(`Invalid mode: ${mode}. Using NORMAL mode.`);
mode = ImageMode.NORMAL;
}
this.previousMode = this.currentMode;
this.currentMode = mode;
}

/**
* Get the current mode
* @returns Current mode
*/
getMode(): ImageMode {
return this.currentMode;
}

/**
* Check if in a specific mode
* @param mode - Mode to check
* @returns True if in the specified mode
*/
isMode(mode: ImageMode): boolean {
return this.currentMode === mode;
}

/**
* Check if in normal mode
* @returns True if in normal mode
*/
isNormal(): boolean {
return this.currentMode === ImageMode.NORMAL;
}

/**
* Check if in drawing mode
* @returns True if in drawing mode
*/
isDrawing(): boolean {
return this.currentMode === ImageMode.DRAWING;
}

/**
* Check if in cropping mode
* @returns True if in cropping mode
*/
isCropping(): boolean {
return this.currentMode === ImageMode.CROPPING;
}

/**
* Check if in magnify mode
* @returns True if in magnify mode
*/
isMagnifying(): boolean {
return this.currentMode === ImageMode.MAGNIFY;
}

/**
* Check if in color picker mode
* @returns True if in color picker mode
*/
isColorPicker(): boolean {
return this.currentMode === ImageMode.COLORPICKER;
}

/**
* Reset to normal mode
*/
reset(): void {
this.setMode(ImageMode.NORMAL);
}

/**
* Restore previous mode
*/
restorePrevious(): void {
this.setMode(this.previousMode);
}

/**
* Get cursor style for current mode
* @returns CSS cursor value
*/
getCursor(): string {
switch (this.currentMode) {
case ImageMode.CROPPING:
return "crosshair";
case ImageMode.MAGNIFY:
return "zoom-in";
case ImageMode.DRAWING:
const cursorPathDraw = path
.join(__dirname, "../../assets/pencil.png")
.replace(/\\/g, "/");
return `url('file://${cursorPathDraw}') 0 32, crosshair`;
case ImageMode.COLORPICKER:
const cursorPathPicker = path
.join(__dirname, "../../assets/spoid.png")
.replace(/\\/g, "/");
return `url('file://${cursorPathPicker}') 0 24, crosshair`;
default:
return "default";
}
}
}

4 changes: 2 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
<div id="imgkit-container"></div>

<!-- Load ImgKit UI components -->
<script src="./utils/ui_loader.js"></script>
<script src="./renderer/main_renderer.js"></script>
<script src="./dist/utils/ui_loader.js"></script>
<script src="./dist/renderer/main_renderer.js"></script>
</body>
</html>
Loading