-
);
}
diff --git a/src/components/ActionButtons.tsx b/src/components/ActionButtons.tsx
new file mode 100644
index 0000000..aef1c38
--- /dev/null
+++ b/src/components/ActionButtons.tsx
@@ -0,0 +1,240 @@
+import { useStore } from '@nanostores/react';
+import { useCallback } from 'react';
+import {
+ isCompiling,
+ compileImage,
+ resetToOriginal,
+ originalFileData,
+ modifiedFileData,
+ pushToUndoStack,
+ undo,
+ canUndo,
+ hasModifications,
+ fileMetadata,
+} from '../stores/imageStore';
+import { allGlitchEffects } from '../utils/glitchEffects';
+
+interface ActionButtonsProps {
+ handleRandomize?: (count: number) => void;
+ handleGlobalRandomize?: (count: number) => void;
+ virtualScrollData?: any;
+}
+
+export default function ActionButtons({
+ handleRandomize,
+ handleGlobalRandomize,
+ virtualScrollData
+}: ActionButtonsProps) {
+ const compiling = useStore(isCompiling);
+ const originalData = useStore(originalFileData);
+ const modifiedData = useStore(modifiedFileData);
+ const canUndoState = useStore(canUndo);
+ const metadata = useStore(fileMetadata);
+
+ // Default handlers for mobile (when not provided)
+ const defaultHandleRandomize = useCallback((count: number) => {
+ if (!originalData || !metadata) return;
+
+ const currentData = modifiedData || new Uint8Array(originalData);
+ pushToUndoStack(currentData);
+
+ const newData = new Uint8Array(currentData);
+
+ for (let i = 0; i < count; i++) {
+ const randomIndex = Math.floor(Math.random() * metadata.fileSize);
+ const randomValue = Math.floor(Math.random() * 256);
+ newData[randomIndex] = randomValue;
+ }
+
+ modifiedFileData.set(newData);
+ hasModifications.set(true);
+ setTimeout(() => compileImage(), 100);
+ }, [originalData, modifiedData, metadata]);
+
+ const defaultHandleGlobalRandomize = useCallback((count: number) => {
+ if (!originalData || !metadata) return;
+
+ const currentData = modifiedData || new Uint8Array(originalData);
+ pushToUndoStack(currentData);
+
+ const newData = new Uint8Array(currentData);
+
+ for (let i = 0; i < count; i++) {
+ const randomIndex = Math.floor(Math.random() * metadata.fileSize);
+ const randomValue = Math.floor(Math.random() * 256);
+ newData[randomIndex] = randomValue;
+ }
+
+ modifiedFileData.set(newData);
+ setTimeout(() => compileImage(), 100);
+ }, [originalData, modifiedData, metadata]);
+
+ const handleGlitchEffect = useCallback(async (effectApply: (data: Uint8Array) => void) => {
+ const currentData = modifiedData || originalData;
+ if (!currentData) return;
+
+ pushToUndoStack(currentData);
+
+ const newData = new Uint8Array(currentData);
+ effectApply(newData);
+
+ modifiedFileData.set(newData);
+ hasModifications.set(true);
+
+ setTimeout(() => compileImage(), 100);
+ }, [originalData, modifiedData]);
+
+ const handleUndoAction = useCallback(() => {
+ undo();
+ setTimeout(() => compileImage(), 100);
+ }, []);
+
+ const handleResetAction = useCallback(() => {
+ resetToOriginal();
+ setTimeout(() => compileImage(), 100);
+ }, []);
+
+ const randomizeHandler = handleRandomize || defaultHandleRandomize;
+ const globalRandomizeHandler = handleGlobalRandomize || defaultHandleGlobalRandomize;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/BinaryEditor.tsx b/src/components/BinaryEditor.tsx
index 2dc964e..b2f0c61 100644
--- a/src/components/BinaryEditor.tsx
+++ b/src/components/BinaryEditor.tsx
@@ -15,6 +15,7 @@ import {
undo,
canUndo,
} from '../stores/imageStore';
+import ActionButtons from './ActionButtons';
export default function BinaryEditor() {
const metadata = useStore(fileMetadata);
@@ -26,8 +27,6 @@ export default function BinaryEditor() {
const chunkSize = useStore(hexChunkSize);
const canUndoState = useStore(canUndo);
- const [jumpToChunk, setJumpToChunk] = useState('');
- const [jumpToAddress, setJumpToAddress] = useState('');
const [hexInput, setHexInput] = useState('');
const [editingByte, setEditingByte] = useState<{ row: number; col: number; value: string; globalOffset: number } | null>(null);
const [editingAscii, setEditingAscii] = useState<{ row: number; col: number; value: string; globalOffset: number } | null>(null);
@@ -148,6 +147,7 @@ export default function BinaryEditor() {
setTimeout(() => compileImage(), 100);
}, [originalData, modifiedData, metadata]);
+
const handleHexInputChange = (
event: React.ChangeEvent
) => {
@@ -465,40 +465,6 @@ export default function BinaryEditor() {
return rows;
};
- const handleJumpToChunk = (e: React.FormEvent) => {
- e.preventDefault();
- const chunkIndex = parseInt(jumpToChunk) - 1;
- if (!isNaN(chunkIndex) && chunkIndex >= 0 && chunkIndex < totalChunks) {
- // Calculate the address of the chunk and scroll to it
- const chunkAddress = chunkIndex * chunkSize;
- const targetRow = Math.floor(chunkAddress / BYTES_PER_ROW);
- const targetScrollTop = targetRow * ROW_HEIGHT;
-
- if (scrollContainerRef.current) {
- scrollContainerRef.current.scrollTop = targetScrollTop;
- }
- setScrollTop(targetScrollTop);
- setJumpToChunk('');
- }
- };
-
- const handleJumpToAddress = (e: React.FormEvent) => {
- e.preventDefault();
- const address = parseInt(jumpToAddress, 16);
- if (!isNaN(address) && metadata) {
- if (address >= 0 && address < metadata.fileSize) {
- // For virtual scrolling, calculate the row and scroll to it
- const targetRow = Math.floor(address / BYTES_PER_ROW);
- const targetScrollTop = targetRow * ROW_HEIGHT;
-
- if (scrollContainerRef.current) {
- scrollContainerRef.current.scrollTop = targetScrollTop;
- }
- setScrollTop(targetScrollTop);
- }
- setJumpToAddress('');
- }
- };
if (!metadata) {
@@ -548,30 +514,6 @@ export default function BinaryEditor() {
-
-
-
-
-
-
File: {metadata.fileSize} bytes • Scroll position: {Math.floor(scrollTop / ROW_HEIGHT * BYTES_PER_ROW).toString(16).toUpperCase()}
@@ -579,80 +521,11 @@ export default function BinaryEditor() {
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/src/components/GlitchControls.tsx b/src/components/GlitchControls.tsx
new file mode 100644
index 0000000..47ad12a
--- /dev/null
+++ b/src/components/GlitchControls.tsx
@@ -0,0 +1,101 @@
+import { useStore } from '@nanostores/react';
+import {
+ fileMetadata,
+ modifiedFileData,
+ pushToUndoStack,
+ undo,
+ canUndo,
+ isCompiling,
+ hasModifications,
+ compileImage
+} from '../stores/imageStore';
+import { allGlitchEffects } from '../utils/glitchEffects';
+
+export default function GlitchControls() {
+ const metadata = useStore(fileMetadata);
+ const compiling = useStore(isCompiling);
+ const undoAvailable = useStore(canUndo);
+
+ const applyGlitchEffect = async (effectApply: (data: Uint8Array) => void) => {
+ const currentData = modifiedFileData.get();
+ if (!currentData) return;
+
+ pushToUndoStack(currentData);
+
+ const newData = new Uint8Array(currentData);
+ effectApply(newData);
+
+ modifiedFileData.set(newData);
+ hasModifications.set(true);
+
+ await compileImage();
+ };
+
+ const handleUndo = async () => {
+ undo();
+ await compileImage();
+ };
+
+ if (!metadata) return null;
+
+ return (
+
+
+
Quick Effects
+
+ {allGlitchEffects.slice(0, 3).map((effect) => (
+
+ ))}
+
+
+
+
+
Advanced Effects
+
+ {allGlitchEffects.slice(3).filter(effect =>
+ effect.name !== "Color Shift" && effect.name !== "Blocks"
+ ).map((effect) => (
+
+ ))}
+
+ {allGlitchEffects.filter(effect =>
+ effect.name === "Color Shift" || effect.name === "Blocks"
+ ).map((effect) => (
+
+ ))}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/ImageUpload.tsx b/src/components/ImageUpload.tsx
index 8fd7f96..f221b3c 100644
--- a/src/components/ImageUpload.tsx
+++ b/src/components/ImageUpload.tsx
@@ -95,7 +95,7 @@ export default function ImageUpload() {
}, [compiledUrl, modifiedData, metadata]);
return (
- <>
+
-
);
}
diff --git a/src/components/MobileInterface.tsx b/src/components/MobileInterface.tsx
new file mode 100644
index 0000000..ea09daa
--- /dev/null
+++ b/src/components/MobileInterface.tsx
@@ -0,0 +1,52 @@
+import { useStore } from '@nanostores/react';
+import {
+ fileMetadata,
+ isCompiling
+} from '../stores/imageStore';
+import ImageUpload from './ImageUpload';
+import ImagePreview from './ImagePreview';
+import ActionButtons from './ActionButtons';
+
+export default function MobileInterface() {
+ const metadata = useStore(fileMetadata);
+ const compiling = useStore(isCompiling);
+
+ return (
+
+
+
+
+ {metadata ? (
+ <>
+
+ {compiling ? (
+
+ ) : (
+
+ )}
+
+ >
+ ) : (
+
+
+ Load an image to start creating glitch art
+
+
+ )}
+
+
+ {metadata && (
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/src/hooks/useIsMobile.ts b/src/hooks/useIsMobile.ts
new file mode 100644
index 0000000..c4dda20
--- /dev/null
+++ b/src/hooks/useIsMobile.ts
@@ -0,0 +1,20 @@
+import { useState, useEffect } from 'react';
+
+export const useIsMobile = (breakpoint: number = 768): boolean => {
+ const [isMobile, setIsMobile] = useState
(
+ typeof window !== 'undefined' ? window.innerWidth <= breakpoint : false
+ );
+
+ useEffect(() => {
+ const handleResize = () => {
+ setIsMobile(window.innerWidth <= breakpoint);
+ };
+
+ window.addEventListener('resize', handleResize);
+ handleResize();
+
+ return () => window.removeEventListener('resize', handleResize);
+ }, [breakpoint]);
+
+ return isMobile;
+};
\ No newline at end of file
diff --git a/src/hooks/useKeyboardShortcuts.ts b/src/hooks/useKeyboardShortcuts.ts
new file mode 100644
index 0000000..a9b8e89
--- /dev/null
+++ b/src/hooks/useKeyboardShortcuts.ts
@@ -0,0 +1,117 @@
+import { useEffect } from 'react';
+import { useStore } from '@nanostores/react';
+import {
+ isCompiling,
+ canUndo,
+ undo,
+ resetToOriginal,
+ compileImage,
+ originalFileData,
+ modifiedFileData,
+ pushToUndoStack,
+ hasModifications
+} from '../stores/imageStore';
+import { allGlitchEffects } from '../utils/glitchEffects';
+
+export function useKeyboardShortcuts() {
+ const compiling = useStore(isCompiling);
+ const undoAvailable = useStore(canUndo);
+ const originalData = useStore(originalFileData);
+ const modifiedData = useStore(modifiedFileData);
+
+ const handleGlitchEffect = (effectApply: (data: Uint8Array) => void) => {
+ const currentData = modifiedData || originalData;
+ if (!currentData) return;
+
+ pushToUndoStack(currentData);
+
+ const newData = new Uint8Array(currentData);
+ effectApply(newData);
+
+ modifiedFileData.set(newData);
+ hasModifications.set(true);
+
+ setTimeout(() => compileImage(), 100);
+ };
+
+ useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ // Don't trigger shortcuts if we're typing in an input field
+ if (event.target instanceof HTMLInputElement ||
+ event.target instanceof HTMLTextAreaElement ||
+ compiling || !originalData) {
+ return;
+ }
+
+ // Detect platform for modifier key
+ const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
+ const modifierKey = isMac ? event.metaKey : event.ctrlKey;
+
+ // Backspace or Delete key for undo
+ if ((event.key === 'Backspace' || event.key === 'Delete') && !modifierKey) {
+ if (undoAvailable) {
+ event.preventDefault();
+ undo();
+ setTimeout(() => compileImage(), 100);
+ }
+ return;
+ }
+
+ // Ctrl/Cmd + Backspace for reset
+ if ((event.key === 'Backspace') && modifierKey) {
+ event.preventDefault();
+ resetToOriginal();
+ setTimeout(() => compileImage(), 100);
+ return;
+ }
+
+ // Glitch effect shortcuts (only if no modifier keys are pressed)
+ if (!modifierKey && !event.shiftKey && !event.altKey) {
+ const key = event.key.toLowerCase();
+
+ switch (key) {
+ case 'r':
+ event.preventDefault();
+ handleGlitchEffect(allGlitchEffects[0].apply); // Random
+ break;
+ case 'p':
+ event.preventDefault();
+ handleGlitchEffect(allGlitchEffects[1].apply); // Pattern
+ break;
+ case 't':
+ event.preventDefault();
+ handleGlitchEffect(allGlitchEffects[2].apply); // Points
+ break;
+ case 'c':
+ event.preventDefault();
+ handleGlitchEffect(allGlitchEffects[3].apply); // Color Shift
+ break;
+ case 'b':
+ event.preventDefault();
+ handleGlitchEffect(allGlitchEffects[4].apply); // Blocks
+ break;
+ case 'h':
+ event.preventDefault();
+ handleGlitchEffect(allGlitchEffects[5].apply); // Header
+ break;
+ case 's':
+ event.preventDefault();
+ handleGlitchEffect(allGlitchEffects[6].apply); // Scanlines
+ break;
+ case 'i':
+ event.preventDefault();
+ handleGlitchEffect(allGlitchEffects[7].apply); // Bit Shift
+ break;
+ }
+ }
+ };
+
+ // Add event listener
+ document.addEventListener('keydown', handleKeyDown);
+
+ // Cleanup
+ return () => {
+ document.removeEventListener('keydown', handleKeyDown);
+ };
+ }, [compiling, undoAvailable, originalData, modifiedData]);
+}
\ No newline at end of file
diff --git a/src/index.css b/src/index.css
index 08a3ac9..3c1d810 100644
--- a/src/index.css
+++ b/src/index.css
@@ -36,7 +36,7 @@ h1 {
}
button {
- border-radius: 8px;
+ border-radius: 0;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
diff --git a/src/stores/imageStore.ts b/src/stores/imageStore.ts
index 869c50f..dffc38b 100644
--- a/src/stores/imageStore.ts
+++ b/src/stores/imageStore.ts
@@ -201,6 +201,9 @@ export const initializeHexEditor = async (file: File) => {
// Load first chunk
loadCurrentChunk();
+
+ // Compile the image immediately for preview
+ compileImage();
};
// Update chunk data from hex editor
diff --git a/src/styles/glitch-controls.css b/src/styles/glitch-controls.css
new file mode 100644
index 0000000..941c172
--- /dev/null
+++ b/src/styles/glitch-controls.css
@@ -0,0 +1,162 @@
+/* Glitch Controls Styles */
+
+.glitch-controls {
+ padding: 1rem;
+ background: #2a2a2a;
+ border-bottom: 1px solid #444;
+ max-height: 300px;
+ overflow-y: auto;
+}
+
+.glitch-section {
+ margin-bottom: 1.5rem;
+}
+
+.glitch-section:last-child {
+ margin-bottom: 0;
+}
+
+.section-title {
+ font-size: 0.875rem;
+ font-weight: 600;
+ color: #ccc;
+ margin: 0 0 0.75rem 0;
+ text-transform: uppercase;
+ letter-spacing: 0.03rem;
+}
+
+/* Glitch Buttons Grid */
+.glitch-buttons-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
+ gap: 0.5rem;
+}
+
+.glitch-button {
+ background: #333;
+ color: #fff;
+ border: 1px solid #555;
+ padding: 0.5rem 0.75rem;
+ font-size: 0.75rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ min-height: 2rem;
+ border-radius: 0;
+}
+
+.glitch-button:hover:not(:disabled) {
+ background: #444;
+ border-color: #666;
+}
+
+.glitch-button:active:not(:disabled) {
+ background: #555;
+ transform: translateY(1px);
+}
+
+.glitch-button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+/* History Controls */
+.history-controls {
+ display: flex;
+ gap: 0.5rem;
+}
+
+.history-button {
+ background: #2a2a2a;
+ color: #fff;
+ border: 1px solid #444;
+ padding: 0.5rem 1rem;
+ font-size: 0.75rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ border-radius: 0;
+}
+
+.history-button:hover:not(:disabled) {
+ background: #333;
+ border-color: #555;
+}
+
+.history-button:active:not(:disabled) {
+ background: #444;
+ transform: translateY(1px);
+}
+
+.history-button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+/* Remove rounded corners from all buttons */
+.glitch-controls button {
+ border-radius: 0 !important;
+}
+
+/* Unified Controls */
+.unified-controls {
+ background: #2a2a2a;
+ border-top: 1px solid #444;
+ padding: 0.75rem;
+}
+
+.unified-buttons {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ justify-content: center;
+}
+
+.unified-button {
+ background: #333;
+ color: #fff;
+ border: 1px solid #555;
+ padding: 0;
+ font-size: 0.7rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ border-radius: 0;
+ width: 60px;
+ height: 60px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ word-break: break-word;
+ hyphens: auto;
+}
+
+.unified-button:hover:not(:disabled) {
+ background: #444;
+ border-color: #666;
+}
+
+.unified-button:active:not(:disabled) {
+ background: #555;
+ transform: translateY(1px);
+}
+
+.unified-button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+/* Hex Controls Chunk Info */
+.hex-controls .chunk-info {
+ text-align: center;
+ padding: 0.5rem 0;
+ border-bottom: 1px solid #444;
+ margin-bottom: 0.75rem;
+}
+
+.hex-controls .chunk-info small {
+ color: #ccc;
+ font-size: 0.75rem;
+}
+
diff --git a/src/styles/mobile.css b/src/styles/mobile.css
new file mode 100644
index 0000000..ef381e4
--- /dev/null
+++ b/src/styles/mobile.css
@@ -0,0 +1,286 @@
+/* Mobile Interface Styles */
+
+.mobile-interface {
+ width: 100vw;
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+ background: #1a1a1a;
+ color: #fff;
+ overflow: hidden;
+}
+
+/* Mobile Header */
+.mobile-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1rem;
+ background: #2a2a2a;
+ border-bottom: 1px solid #444;
+ min-height: 3.5rem;
+}
+
+.mobile-title {
+ font-size: 1.25rem;
+ font-weight: 600;
+ margin: 0;
+}
+
+.mobile-buttons {
+ display: flex;
+ gap: 0.5rem;
+}
+
+/* Mobile Content */
+.mobile-content {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ position: relative;
+}
+
+/* Mobile Image Container */
+.mobile-image-container {
+ flex: 1;
+ position: relative;
+ overflow: hidden;
+ background: #111;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.mobile-image-container .image-container {
+ width: 100%;
+ height: 100%;
+}
+
+/* Mobile Controls Toggle */
+.mobile-controls-toggle {
+ position: fixed;
+ bottom: 1rem;
+ left: 50%;
+ transform: translateX(-50%);
+ background: #007acc;
+ color: white;
+ border: none;
+ padding: 0.75rem 1.5rem;
+ border-radius: 0;
+ font-size: 1rem;
+ font-weight: 500;
+ box-shadow: 0 4px 12px rgba(0, 122, 204, 0.4);
+ z-index: 100;
+ transition: all 0.2s ease;
+}
+
+.mobile-controls-toggle:active {
+ transform: translateX(-50%) scale(0.95);
+}
+
+/* Mobile Controls Panel */
+.mobile-controls {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ background: #2a2a2a;
+ border-top: 1px solid #444;
+ padding: 1.5rem 1rem 2rem;
+ box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.3);
+ z-index: 99;
+ animation: slideUp 0.3s ease-out;
+}
+
+@keyframes slideUp {
+ from {
+ transform: translateY(100%);
+ }
+ to {
+ transform: translateY(0);
+ }
+}
+
+/* Glitch Buttons */
+.glitch-buttons {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 0.75rem;
+ margin-bottom: 1rem;
+}
+
+.glitch-button {
+ background: #333;
+ color: white;
+ border: 1px solid #555;
+ padding: 0.75rem;
+ border-radius: 0;
+ font-size: 0.875rem;
+ font-weight: 500;
+ min-height: 3rem;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 0.25rem;
+ transition: all 0.2s ease;
+}
+
+.glitch-button:active:not(:disabled) {
+ background: #444;
+ transform: scale(0.95);
+}
+
+.glitch-button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+/* History Buttons */
+.history-buttons {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 0.75rem;
+}
+
+.history-button {
+ background: #2a2a2a;
+ color: white;
+ border: 1px solid #444;
+ padding: 0.75rem;
+ border-radius: 0;
+ font-size: 0.875rem;
+ font-weight: 500;
+ min-height: 2.5rem;
+ transition: all 0.2s ease;
+}
+
+.history-button:active:not(:disabled) {
+ background: #333;
+ transform: scale(0.95);
+}
+
+.history-button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+/* Mobile Welcome */
+.mobile-welcome {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 2rem;
+ text-align: center;
+}
+
+.mobile-instructions {
+ font-size: 1.125rem;
+ color: #888;
+ max-width: 20rem;
+}
+
+/* Mobile Loading */
+.mobile-loading {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 2rem;
+ text-align: center;
+}
+
+.mobile-loading p {
+ font-size: 1.125rem;
+ color: #888;
+ animation: pulse 1.5s ease-in-out infinite alternate;
+}
+
+@keyframes pulse {
+ from {
+ opacity: 0.6;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+/* Override desktop styles on mobile */
+@media (max-width: 768px) {
+ .app > header,
+ .app > main {
+ display: none;
+ }
+
+ .mobile-interface {
+ display: flex;
+ }
+}
+
+@media (min-width: 769px) {
+ .mobile-interface {
+ display: none;
+ }
+}
+
+/* Mobile-specific button overrides */
+@media (max-width: 768px) {
+ .load-button {
+ min-height: 2.5rem;
+ padding: 0.5rem 1rem;
+ font-size: 0.875rem;
+ }
+
+ .export-button {
+ min-height: 2.5rem;
+ padding: 0.5rem 1rem;
+ font-size: 0.875rem;
+ background: #28a745;
+ color: white;
+ border: none;
+ border-radius: 0;
+ font-weight: 500;
+ }
+
+ .export-button:active:not(:disabled) {
+ background: #218838;
+ }
+
+ .export-button:disabled {
+ background: #444;
+ opacity: 0.5;
+ }
+
+ /* Mobile Controls Container */
+ .mobile-controls-container {
+ background: #2a2a2a;
+ border-top: 1px solid #444;
+ padding: 0.75rem;
+ }
+
+ /* Mobile Action Buttons - Two Lines */
+ .mobile-controls-container .action-buttons {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.25rem;
+ max-width: 100%;
+ }
+
+ .mobile-controls-container .action-button {
+ flex: 1 1 calc(11.11% - 0.25rem);
+ min-width: 0;
+ padding: 0.5rem 0.25rem;
+ font-size: 0.6rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ border-radius: 0;
+ min-height: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ }
+}
\ No newline at end of file
diff --git a/src/styles/responsive.css b/src/styles/responsive.css
new file mode 100644
index 0000000..2390894
--- /dev/null
+++ b/src/styles/responsive.css
@@ -0,0 +1,184 @@
+/* Responsive Design System */
+
+/* Base font size for rem calculations */
+:root {
+ font-size: 16px;
+}
+
+/* Mobile-first media queries */
+/* Small devices (phones, 576px and down) */
+@media (max-width: 576px) {
+ :root {
+ font-size: 14px;
+ }
+
+ .app-container {
+ flex-direction: column !important;
+ padding: 0.5rem !important;
+ }
+
+ .editor-section,
+ .preview-section {
+ width: 100% !important;
+ max-width: 100% !important;
+ height: auto !important;
+ }
+
+ .hex-editor {
+ font-size: 0.75rem !important;
+ }
+
+ .top-bar {
+ flex-wrap: wrap;
+ height: auto !important;
+ padding: 0.5rem !important;
+ }
+
+ .button-group {
+ order: 2;
+ width: 100%;
+ margin-top: 0.5rem;
+ }
+
+ .virtual-scroll-container {
+ height: 50vh !important;
+ }
+}
+
+/* Medium devices (tablets, 768px and up) */
+@media (min-width: 577px) and (max-width: 768px) {
+ :root {
+ font-size: 15px;
+ }
+
+ .app-container {
+ flex-direction: column !important;
+ padding: 1rem !important;
+ }
+
+ .editor-section,
+ .preview-section {
+ width: 100% !important;
+ max-width: 100% !important;
+ }
+
+ .virtual-scroll-container {
+ height: 60vh !important;
+ }
+}
+
+/* Large devices (desktops, 992px and up) */
+@media (min-width: 769px) and (max-width: 1200px) {
+ .editor-section {
+ width: 60% !important;
+ }
+
+ .preview-section {
+ width: 40% !important;
+ }
+}
+
+/* Extra large devices (large desktops, 1200px and up) */
+@media (min-width: 1201px) {
+ .editor-section {
+ width: 50% !important;
+ max-width: 50rem !important;
+ }
+
+ .preview-section {
+ width: 50% !important;
+ max-width: 50rem !important;
+ }
+}
+
+/* Responsive utilities */
+.hide-mobile {
+ display: block;
+}
+
+.show-mobile {
+ display: none;
+}
+
+@media (max-width: 768px) {
+ .hide-mobile {
+ display: none !important;
+ }
+
+ .show-mobile {
+ display: block !important;
+ }
+}
+
+/* Touch-friendly sizes for mobile */
+@media (max-width: 768px) {
+ button {
+ min-height: 44px;
+ min-width: 44px;
+ padding: 0.75rem 1rem !important;
+ }
+
+ input[type="file"] {
+ padding: 0.75rem !important;
+ }
+
+ .hex-editor .hex-byte,
+ .hex-editor .ascii-char {
+ padding: 0.25rem !important;
+ min-width: 2rem;
+ }
+}
+
+/* Responsive typography */
+@media (max-width: 576px) {
+ h1 {
+ font-size: 1.5rem !important;
+ }
+
+ h2 {
+ font-size: 1.25rem !important;
+ }
+
+ .section-title {
+ font-size: 1rem !important;
+ }
+}
+
+/* Responsive image preview */
+@media (max-width: 768px) {
+ .image-preview {
+ max-height: 40vh !important;
+ }
+
+ .image-preview img {
+ max-width: 100% !important;
+ height: auto !important;
+ }
+}
+
+/* Responsive scrollbars */
+@media (max-width: 768px) {
+ ::-webkit-scrollbar {
+ width: 6px !important;
+ height: 6px !important;
+ }
+}
+
+/* Print styles */
+@media print {
+ .top-bar,
+ .button-group,
+ .upload-section {
+ display: none !important;
+ }
+
+ .app-container {
+ flex-direction: column !important;
+ }
+
+ .editor-section,
+ .preview-section {
+ width: 100% !important;
+ max-width: 100% !important;
+ }
+}
\ No newline at end of file
diff --git a/src/utils/glitchEffects.ts b/src/utils/glitchEffects.ts
new file mode 100644
index 0000000..8654c6a
--- /dev/null
+++ b/src/utils/glitchEffects.ts
@@ -0,0 +1,178 @@
+export interface GlitchEffect {
+ name: string;
+ description: string;
+ apply: (data: Uint8Array) => void;
+}
+
+// Random byte corruption - scattered chaos
+export const randomCorruption: GlitchEffect = {
+ name: "Random",
+ description: "Random byte corruption",
+ apply: (data: Uint8Array) => {
+ const intensity = Math.floor(Math.random() * 50) + 10;
+ for (let i = 0; i < intensity; i++) {
+ const index = Math.floor(Math.random() * data.length);
+ data[index] = Math.floor(Math.random() * 256);
+ }
+ }
+};
+
+// Pattern injection - structured corruption
+export const patternInjection: GlitchEffect = {
+ name: "Pattern",
+ description: "Inject repeating patterns",
+ apply: (data: Uint8Array) => {
+ const patterns = [
+ [0xFF, 0x00, 0xFF, 0x00],
+ [0xAA, 0x55, 0xAA, 0x55],
+ [0x0F, 0xF0, 0x0F, 0xF0],
+ [0xDE, 0xAD, 0xBE, 0xEF],
+ [0x00, 0xFF, 0x00, 0xFF],
+ [0x80, 0x40, 0x20, 0x10]
+ ];
+ const pattern = patterns[Math.floor(Math.random() * patterns.length)];
+ const startOffset = Math.floor(Math.random() * data.length * 0.5);
+ const repetitions = Math.floor(Math.random() * 100) + 50;
+
+ for (let i = 0; i < repetitions * pattern.length; i++) {
+ const index = startOffset + i;
+ if (index < data.length) {
+ data[index] = pattern[i % pattern.length];
+ }
+ }
+ }
+};
+
+// Point corruption - precise byte hits
+export const pointCorruption: GlitchEffect = {
+ name: "Points",
+ description: "Corrupt individual bytes",
+ apply: (data: Uint8Array) => {
+ const points = Math.floor(Math.random() * 20) + 5;
+ for (let i = 0; i < points; i++) {
+ const index = Math.floor(Math.random() * data.length);
+ data[index] = Math.floor(Math.random() * 256);
+ }
+ }
+};
+
+// Color channel shift - swap RGB values
+export const colorShift: GlitchEffect = {
+ name: "Color Shift",
+ description: "Shift color channels",
+ apply: (data: Uint8Array) => {
+ // Find likely RGB data (skip headers)
+ const start = Math.floor(data.length * 0.1);
+ const end = Math.floor(data.length * 0.9);
+
+ for (let i = start; i < end - 2; i += 3) {
+ if (i + 2 < data.length) {
+ // Swap R and B channels randomly
+ if (Math.random() > 0.7) {
+ const temp = data[i];
+ data[i] = data[i + 2];
+ data[i + 2] = temp;
+ }
+ }
+ }
+ }
+};
+
+// Block corruption - corrupt rectangular regions
+export const blockCorruption: GlitchEffect = {
+ name: "Blocks",
+ description: "Corrupt data blocks",
+ apply: (data: Uint8Array) => {
+ const blocks = Math.floor(Math.random() * 5) + 2;
+
+ for (let b = 0; b < blocks; b++) {
+ const blockStart = Math.floor(Math.random() * data.length * 0.8);
+ const blockSize = Math.floor(Math.random() * 200) + 50;
+ const corruptionValue = Math.floor(Math.random() * 256);
+
+ for (let i = 0; i < blockSize && blockStart + i < data.length; i++) {
+ // Create gradient or solid blocks
+ if (Math.random() > 0.5) {
+ data[blockStart + i] = corruptionValue;
+ } else {
+ data[blockStart + i] = (corruptionValue + i) % 256;
+ }
+ }
+ }
+ }
+};
+
+// Header corruption - break file structure
+export const headerCorruption: GlitchEffect = {
+ name: "Header",
+ description: "Corrupt file headers",
+ apply: (data: Uint8Array) => {
+ // Corrupt early bytes that are likely header data
+ const headerSize = Math.min(100, data.length * 0.05);
+ const corruptionPoints = Math.floor(Math.random() * 10) + 3;
+
+ for (let i = 0; i < corruptionPoints; i++) {
+ const index = Math.floor(Math.random() * headerSize);
+ data[index] = Math.floor(Math.random() * 256);
+ }
+ }
+};
+
+// Scanline corruption - horizontal line effects
+export const scanlineCorruption: GlitchEffect = {
+ name: "Scanlines",
+ description: "Horizontal line corruption",
+ apply: (data: Uint8Array) => {
+ // Estimate width based on sqrt of data size
+ const estimatedWidth = Math.floor(Math.sqrt(data.length / 3)) * 3;
+ const lines = Math.floor(Math.random() * 10) + 3;
+
+ for (let l = 0; l < lines; l++) {
+ const lineStart = Math.floor(Math.random() * (data.length - estimatedWidth));
+ const lineStart_aligned = lineStart - (lineStart % estimatedWidth);
+
+ for (let i = 0; i < estimatedWidth && lineStart_aligned + i < data.length; i++) {
+ // Shift bytes horizontally
+ if (Math.random() > 0.3) {
+ const shift = Math.floor(Math.random() * 20) - 10;
+ const sourceIndex = lineStart_aligned + i + shift;
+ if (sourceIndex >= 0 && sourceIndex < data.length) {
+ data[lineStart_aligned + i] = data[sourceIndex];
+ }
+ }
+ }
+ }
+ }
+};
+
+// Bit shifting - shift bits instead of bytes
+export const bitShift: GlitchEffect = {
+ name: "Bit Shift",
+ description: "Shift individual bits",
+ apply: (data: Uint8Array) => {
+ const affectedBytes = Math.floor(Math.random() * 100) + 50;
+
+ for (let i = 0; i < affectedBytes; i++) {
+ const index = Math.floor(Math.random() * data.length);
+ const shifts = Math.floor(Math.random() * 4) + 1;
+
+ // Shift bits left or right
+ if (Math.random() > 0.5) {
+ data[index] = (data[index] << shifts) & 0xFF;
+ } else {
+ data[index] = data[index] >> shifts;
+ }
+ }
+ }
+};
+
+export const allGlitchEffects: GlitchEffect[] = [
+ randomCorruption,
+ patternInjection,
+ pointCorruption,
+ colorShift,
+ blockCorruption,
+ headerCorruption,
+ scanlineCorruption,
+ bitShift
+];
\ No newline at end of file