generally better
This commit is contained in:
35
index.html
35
index.html
@ -2,9 +2,40 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Rayon Cosmique</title>
|
|
||||||
|
<!-- Primary Meta Tags -->
|
||||||
|
<title>Rayon Cosmique - Artistic Hex Editor for Image Corruption</title>
|
||||||
|
<meta name="title" content="Rayon Cosmique - Artistic Hex Editor for Image Corruption" />
|
||||||
|
<meta name="description" content="A hex editor designed for artists to create glitch art through controlled image corruption. Edit binary data in real-time with visual feedback." />
|
||||||
|
<meta name="keywords" content="hex editor, glitch art, image corruption, datamoshing, digital art, binary editor, creative coding" />
|
||||||
|
<meta name="author" content="Raphaël Forment (BuboBubo)" />
|
||||||
|
|
||||||
|
<!-- Open Graph / Facebook -->
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:url" content="https://cosmique.app/" />
|
||||||
|
<meta property="og:title" content="Rayon Cosmique - Artistic Hex Editor for Image Corruption" />
|
||||||
|
<meta property="og:description" content="A hex editor designed for artists to create glitch art through controlled image corruption. Edit binary data in real-time with visual feedback." />
|
||||||
|
<meta property="og:image" content="/og-image.png" />
|
||||||
|
|
||||||
|
<!-- Twitter -->
|
||||||
|
<meta property="twitter:card" content="summary_large_image" />
|
||||||
|
<meta property="twitter:url" content="https://cosmique.app/" />
|
||||||
|
<meta property="twitter:title" content="Rayon Cosmique - Artistic Hex Editor for Image Corruption" />
|
||||||
|
<meta property="twitter:description" content="A hex editor designed for artists to create glitch art through controlled image corruption. Edit binary data in real-time with visual feedback." />
|
||||||
|
<meta property="twitter:image" content="/og-image.png" />
|
||||||
|
|
||||||
|
<!-- Favicon -->
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
|
|
||||||
|
<!-- Theme Color -->
|
||||||
|
<meta name="theme-color" content="#1a1a1a" />
|
||||||
|
|
||||||
|
<!-- Manifest for PWA -->
|
||||||
|
<link rel="manifest" href="/manifest.json" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
4
public/favicon.svg
Normal file
4
public/favicon.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||||
|
<rect width="32" height="32" fill="#1a1a1a"/>
|
||||||
|
<text x="16" y="22" font-family="monospace" font-size="18" font-weight="bold" text-anchor="middle" fill="#007acc">RC</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 241 B |
32
public/manifest.json
Normal file
32
public/manifest.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "Rayon Cosmique",
|
||||||
|
"short_name": "Cosmique",
|
||||||
|
"description": "A hex editor designed for artists to create glitch art through controlled image corruption",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#1a1a1a",
|
||||||
|
"background_color": "#1a1a1a",
|
||||||
|
"orientation": "any",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/icon-192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icon-512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"categories": ["graphics", "photo", "utilities"],
|
||||||
|
"screenshots": [
|
||||||
|
{
|
||||||
|
"src": "/screenshot-1.png",
|
||||||
|
"sizes": "1280x720",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
54
src/App.css
54
src/App.css
@ -1,3 +1,5 @@
|
|||||||
|
@import './styles/responsive.css';
|
||||||
|
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@ -28,30 +30,30 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.top-bar {
|
.top-bar {
|
||||||
height: 40px;
|
height: 2.5rem;
|
||||||
background: #2a2a2a;
|
background: #2a2a2a;
|
||||||
border-bottom: 1px solid #444;
|
border-bottom: 1px solid #444;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 0 16px;
|
padding: 0 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-info {
|
.app-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-title {
|
.app-title {
|
||||||
font-size: 16px;
|
font-size: 1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-author {
|
.app-author {
|
||||||
font-size: 11px;
|
font-size: 0.7rem;
|
||||||
color: #888;
|
color: #888;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
@ -69,9 +71,9 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.editor-section {
|
.editor-section {
|
||||||
width: 700px;
|
width: 43.75rem;
|
||||||
min-width: 600px;
|
min-width: 37.5rem;
|
||||||
max-width: 800px;
|
max-width: 50rem;
|
||||||
background: #2a2a2a;
|
background: #2a2a2a;
|
||||||
border-left: 1px solid #444;
|
border-left: 1px solid #444;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -113,12 +115,12 @@ body {
|
|||||||
.image-panel::before {
|
.image-panel::before {
|
||||||
content: attr(data-label);
|
content: attr(data-label);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 8px;
|
top: 0.5rem;
|
||||||
left: 8px;
|
left: 0.5rem;
|
||||||
font-size: 12px;
|
font-size: 0.75rem;
|
||||||
color: #888;
|
color: #888;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.03rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-panel.modified::before {
|
.image-panel.modified::before {
|
||||||
@ -133,10 +135,10 @@ body {
|
|||||||
background: #007acc;
|
background: #007acc;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 6px 12px;
|
padding: 0.375rem 0.75rem;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 13px;
|
font-size: 0.8125rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,7 +162,6 @@ body {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
margin-left: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.export-button:hover:not(:disabled) {
|
.export-button:hover:not(:disabled) {
|
||||||
@ -178,6 +179,12 @@ body {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.top-bar-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -370,12 +377,12 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.action-button.undo {
|
.action-button.undo {
|
||||||
background: #6c757d;
|
background: #ff6b35;
|
||||||
border-color: #6c757d;
|
border-color: #ff8f66;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-button.undo:hover:not(:disabled) {
|
.action-button.undo:hover:not(:disabled) {
|
||||||
background: #5a6268;
|
background: #ff5722;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-button.global-random {
|
.action-button.global-random {
|
||||||
@ -387,6 +394,15 @@ body {
|
|||||||
background: #5a359a;
|
background: #5a359a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-button.glitch {
|
||||||
|
background: #17a2b8;
|
||||||
|
border-color: #17a2b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button.glitch:hover:not(:disabled) {
|
||||||
|
background: #138496;
|
||||||
|
}
|
||||||
|
|
||||||
/* Hex View */
|
/* Hex View */
|
||||||
.hex-view-container {
|
.hex-view-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@ -499,7 +515,7 @@ body {
|
|||||||
width: 380px;
|
width: 380px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.03rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|||||||
33
src/App.tsx
33
src/App.tsx
@ -1,31 +1,42 @@
|
|||||||
import './App.css';
|
import './App.css';
|
||||||
|
import './styles/mobile.css';
|
||||||
|
import './styles/glitch-controls.css';
|
||||||
import ImageUpload from './components/ImageUpload';
|
import ImageUpload from './components/ImageUpload';
|
||||||
import BinaryEditor from './components/BinaryEditor';
|
import BinaryEditor from './components/BinaryEditor';
|
||||||
import ImagePreview from './components/ImagePreview';
|
import ImagePreview from './components/ImagePreview';
|
||||||
|
import MobileInterface from './components/MobileInterface';
|
||||||
|
import { useIsMobile } from './hooks/useIsMobile';
|
||||||
|
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
useKeyboardShortcuts();
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
return <MobileInterface />;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="app" role="application" aria-label="Rayon Cosmique - Hex editor for glitch art">
|
||||||
<div className="top-bar">
|
<header className="top-bar" role="banner">
|
||||||
<div className="app-info">
|
<div className="app-info">
|
||||||
<h1 className="app-title">Rayon Cosmique</h1>
|
<h1 className="app-title">Rayon Cosmique</h1>
|
||||||
<span className="app-author">by Raphaël Forment (BuboBubo)</span>
|
<span className="app-author">by Raphaël Forment (BuboBubo)</span>
|
||||||
</div>
|
</div>
|
||||||
<ImageUpload />
|
<ImageUpload />
|
||||||
</div>
|
</header>
|
||||||
<div className="main-content">
|
<main className="main-content" role="main">
|
||||||
<div className="image-section">
|
<section className="image-section" aria-label="Image preview panels">
|
||||||
<div className="image-panel modified">
|
<div className="image-panel modified" role="img" aria-label="Modified image preview">
|
||||||
<ImagePreview type="modified" />
|
<ImagePreview type="modified" />
|
||||||
</div>
|
</div>
|
||||||
<div className="image-panel original">
|
<div className="image-panel original" role="img" aria-label="Original image preview">
|
||||||
<ImagePreview type="original" />
|
<ImagePreview type="original" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
<div className="editor-section">
|
<section className="editor-section" aria-label="Hex editor">
|
||||||
<BinaryEditor />
|
<BinaryEditor />
|
||||||
</div>
|
</section>
|
||||||
</div>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
240
src/components/ActionButtons.tsx
Normal file
240
src/components/ActionButtons.tsx
Normal file
@ -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 (
|
||||||
|
<div className="action-buttons">
|
||||||
|
<button
|
||||||
|
onClick={compileImage}
|
||||||
|
className="action-button compile"
|
||||||
|
disabled={compiling}
|
||||||
|
title="Update Image"
|
||||||
|
>
|
||||||
|
↻
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => randomizeHandler(1)}
|
||||||
|
className="action-button random"
|
||||||
|
disabled={compiling || !originalData}
|
||||||
|
title="Random (Visible)"
|
||||||
|
>
|
||||||
|
1
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => randomizeHandler(10)}
|
||||||
|
className="action-button random"
|
||||||
|
disabled={compiling || !originalData}
|
||||||
|
title="Random × 10 (Visible)"
|
||||||
|
>
|
||||||
|
10
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => randomizeHandler(100)}
|
||||||
|
className="action-button random"
|
||||||
|
disabled={compiling || !originalData}
|
||||||
|
title="Random × 100 (Visible)"
|
||||||
|
>
|
||||||
|
100
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => globalRandomizeHandler(1)}
|
||||||
|
className="action-button global-random"
|
||||||
|
disabled={compiling || !originalData}
|
||||||
|
title="Global Random"
|
||||||
|
>
|
||||||
|
1
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => globalRandomizeHandler(10)}
|
||||||
|
className="action-button global-random"
|
||||||
|
disabled={compiling || !originalData}
|
||||||
|
title="Global Random × 10"
|
||||||
|
>
|
||||||
|
10
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => globalRandomizeHandler(100)}
|
||||||
|
className="action-button global-random"
|
||||||
|
disabled={compiling || !originalData}
|
||||||
|
title="Global Random × 100"
|
||||||
|
>
|
||||||
|
100
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleUndoAction}
|
||||||
|
className="action-button undo"
|
||||||
|
disabled={compiling || !canUndoState}
|
||||||
|
title="Undo (Backspace/Delete)"
|
||||||
|
>
|
||||||
|
↶
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleResetAction}
|
||||||
|
className="action-button reset"
|
||||||
|
disabled={compiling}
|
||||||
|
title="Reset (Ctrl/Cmd + Backspace)"
|
||||||
|
>
|
||||||
|
⟲
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleGlitchEffect(allGlitchEffects[0].apply)}
|
||||||
|
className="action-button glitch"
|
||||||
|
disabled={compiling || !originalData}
|
||||||
|
title="Random Corruption (R)"
|
||||||
|
>
|
||||||
|
R
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleGlitchEffect(allGlitchEffects[1].apply)}
|
||||||
|
className="action-button glitch"
|
||||||
|
disabled={compiling || !originalData}
|
||||||
|
title="Pattern Injection (P)"
|
||||||
|
>
|
||||||
|
P
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleGlitchEffect(allGlitchEffects[2].apply)}
|
||||||
|
className="action-button glitch"
|
||||||
|
disabled={compiling || !originalData}
|
||||||
|
title="Point Corruption (T)"
|
||||||
|
>
|
||||||
|
T
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleGlitchEffect(allGlitchEffects[3].apply)}
|
||||||
|
className="action-button glitch"
|
||||||
|
disabled={compiling || !originalData}
|
||||||
|
title="Color Shift (C)"
|
||||||
|
>
|
||||||
|
C
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleGlitchEffect(allGlitchEffects[4].apply)}
|
||||||
|
className="action-button glitch"
|
||||||
|
disabled={compiling || !originalData}
|
||||||
|
title="Block Corruption (B)"
|
||||||
|
>
|
||||||
|
B
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleGlitchEffect(allGlitchEffects[5].apply)}
|
||||||
|
className="action-button glitch"
|
||||||
|
disabled={compiling || !originalData}
|
||||||
|
title="Header Corruption (H)"
|
||||||
|
>
|
||||||
|
H
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleGlitchEffect(allGlitchEffects[6].apply)}
|
||||||
|
className="action-button glitch"
|
||||||
|
disabled={compiling || !originalData}
|
||||||
|
title="Scanline Corruption (S)"
|
||||||
|
>
|
||||||
|
S
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleGlitchEffect(allGlitchEffects[7].apply)}
|
||||||
|
className="action-button glitch"
|
||||||
|
disabled={compiling || !originalData}
|
||||||
|
title="Bit Shift (I)"
|
||||||
|
>
|
||||||
|
I
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -15,6 +15,7 @@ import {
|
|||||||
undo,
|
undo,
|
||||||
canUndo,
|
canUndo,
|
||||||
} from '../stores/imageStore';
|
} from '../stores/imageStore';
|
||||||
|
import ActionButtons from './ActionButtons';
|
||||||
|
|
||||||
export default function BinaryEditor() {
|
export default function BinaryEditor() {
|
||||||
const metadata = useStore(fileMetadata);
|
const metadata = useStore(fileMetadata);
|
||||||
@ -26,8 +27,6 @@ export default function BinaryEditor() {
|
|||||||
const chunkSize = useStore(hexChunkSize);
|
const chunkSize = useStore(hexChunkSize);
|
||||||
const canUndoState = useStore(canUndo);
|
const canUndoState = useStore(canUndo);
|
||||||
|
|
||||||
const [jumpToChunk, setJumpToChunk] = useState('');
|
|
||||||
const [jumpToAddress, setJumpToAddress] = useState('');
|
|
||||||
const [hexInput, setHexInput] = useState('');
|
const [hexInput, setHexInput] = useState('');
|
||||||
const [editingByte, setEditingByte] = useState<{ row: number; col: number; value: string; globalOffset: number } | null>(null);
|
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);
|
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);
|
setTimeout(() => compileImage(), 100);
|
||||||
}, [originalData, modifiedData, metadata]);
|
}, [originalData, modifiedData, metadata]);
|
||||||
|
|
||||||
|
|
||||||
const handleHexInputChange = (
|
const handleHexInputChange = (
|
||||||
event: React.ChangeEvent<HTMLTextAreaElement>
|
event: React.ChangeEvent<HTMLTextAreaElement>
|
||||||
) => {
|
) => {
|
||||||
@ -465,40 +465,6 @@ export default function BinaryEditor() {
|
|||||||
return rows;
|
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) {
|
if (!metadata) {
|
||||||
@ -548,30 +514,6 @@ export default function BinaryEditor() {
|
|||||||
|
|
||||||
<div className="hex-editor-content">
|
<div className="hex-editor-content">
|
||||||
<div className="hex-controls">
|
<div className="hex-controls">
|
||||||
<div className="control-row">
|
|
||||||
<form onSubmit={handleJumpToChunk} className="jump-form">
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={jumpToChunk}
|
|
||||||
onChange={e => setJumpToChunk(e.target.value)}
|
|
||||||
placeholder="Jump to chunk..."
|
|
||||||
className="jump-input"
|
|
||||||
min="1"
|
|
||||||
max={totalChunks}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<form onSubmit={handleJumpToAddress} className="jump-form">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={jumpToAddress}
|
|
||||||
onChange={e => setJumpToAddress(e.target.value)}
|
|
||||||
placeholder="Go to address (hex)"
|
|
||||||
className="jump-input address-input"
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="chunk-info">
|
<div className="chunk-info">
|
||||||
<small>
|
<small>
|
||||||
File: {metadata.fileSize} bytes • Scroll position: {Math.floor(scrollTop / ROW_HEIGHT * BYTES_PER_ROW).toString(16).toUpperCase()}
|
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() {
|
|||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="action-buttons">
|
<ActionButtons
|
||||||
<button
|
handleRandomize={handleRandomize}
|
||||||
onClick={compileImage}
|
handleGlobalRandomize={handleGlobalRandomize}
|
||||||
className="action-button compile"
|
virtualScrollData={virtualScrollData}
|
||||||
disabled={compiling}
|
/>
|
||||||
title="Update Image"
|
|
||||||
>
|
|
||||||
↻
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleRandomize(1)}
|
|
||||||
className="action-button random"
|
|
||||||
disabled={compiling || !originalData}
|
|
||||||
title="Random (Visible)"
|
|
||||||
>
|
|
||||||
1
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleRandomize(10)}
|
|
||||||
className="action-button random"
|
|
||||||
disabled={compiling || !originalData}
|
|
||||||
title="Random × 10 (Visible)"
|
|
||||||
>
|
|
||||||
10
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleRandomize(100)}
|
|
||||||
className="action-button random"
|
|
||||||
disabled={compiling || !originalData}
|
|
||||||
title="Random × 100 (Visible)"
|
|
||||||
>
|
|
||||||
100
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleGlobalRandomize(1)}
|
|
||||||
className="action-button global-random"
|
|
||||||
disabled={compiling || !originalData}
|
|
||||||
title="Global Random"
|
|
||||||
>
|
|
||||||
1
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleGlobalRandomize(10)}
|
|
||||||
className="action-button global-random"
|
|
||||||
disabled={compiling || !originalData}
|
|
||||||
title="Global Random × 10"
|
|
||||||
>
|
|
||||||
10
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleGlobalRandomize(100)}
|
|
||||||
className="action-button global-random"
|
|
||||||
disabled={compiling || !originalData}
|
|
||||||
title="Global Random × 100"
|
|
||||||
>
|
|
||||||
100
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleUndo}
|
|
||||||
className="action-button undo"
|
|
||||||
disabled={compiling || !canUndoState}
|
|
||||||
title="Undo"
|
|
||||||
>
|
|
||||||
↶
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={resetToOriginal}
|
|
||||||
className="action-button reset"
|
|
||||||
disabled={compiling}
|
|
||||||
title="Reset"
|
|
||||||
>
|
|
||||||
⟲
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="hex-view-container">
|
<div className="hex-view-container">
|
||||||
|
|||||||
101
src/components/GlitchControls.tsx
Normal file
101
src/components/GlitchControls.tsx
Normal file
@ -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 (
|
||||||
|
<div className="glitch-controls">
|
||||||
|
<div className="glitch-section">
|
||||||
|
<h3 className="section-title">Quick Effects</h3>
|
||||||
|
<div className="glitch-buttons-grid">
|
||||||
|
{allGlitchEffects.slice(0, 3).map((effect) => (
|
||||||
|
<button
|
||||||
|
key={effect.name}
|
||||||
|
onClick={() => applyGlitchEffect(effect.apply)}
|
||||||
|
className="glitch-button"
|
||||||
|
disabled={compiling}
|
||||||
|
title={effect.description}
|
||||||
|
>
|
||||||
|
{effect.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="glitch-section">
|
||||||
|
<h3 className="section-title">Advanced Effects</h3>
|
||||||
|
<div className="glitch-buttons-grid">
|
||||||
|
{allGlitchEffects.slice(3).filter(effect =>
|
||||||
|
effect.name !== "Color Shift" && effect.name !== "Blocks"
|
||||||
|
).map((effect) => (
|
||||||
|
<button
|
||||||
|
key={effect.name}
|
||||||
|
onClick={() => applyGlitchEffect(effect.apply)}
|
||||||
|
className="glitch-button"
|
||||||
|
disabled={compiling}
|
||||||
|
title={effect.description}
|
||||||
|
>
|
||||||
|
{effect.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
onClick={handleUndo}
|
||||||
|
className="glitch-button"
|
||||||
|
disabled={!undoAvailable || compiling}
|
||||||
|
title="Undo last change"
|
||||||
|
>
|
||||||
|
Undo
|
||||||
|
</button>
|
||||||
|
{allGlitchEffects.filter(effect =>
|
||||||
|
effect.name === "Color Shift" || effect.name === "Blocks"
|
||||||
|
).map((effect) => (
|
||||||
|
<button
|
||||||
|
key={effect.name}
|
||||||
|
onClick={() => applyGlitchEffect(effect.apply)}
|
||||||
|
className="glitch-button"
|
||||||
|
disabled={compiling}
|
||||||
|
title={effect.description}
|
||||||
|
>
|
||||||
|
{effect.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -95,7 +95,7 @@ export default function ImageUpload() {
|
|||||||
}, [compiledUrl, modifiedData, metadata]);
|
}, [compiledUrl, modifiedData, metadata]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="top-bar-buttons">
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
@ -103,17 +103,19 @@ export default function ImageUpload() {
|
|||||||
id="imageUpload"
|
id="imageUpload"
|
||||||
className="hidden-input"
|
className="hidden-input"
|
||||||
disabled={compiling}
|
disabled={compiling}
|
||||||
|
aria-label="Upload an image file for hex editing"
|
||||||
/>
|
/>
|
||||||
<label htmlFor="imageUpload" className="load-button">
|
<label htmlFor="imageUpload" className="load-button" role="button" tabIndex={0} aria-label="Click to load an image file">
|
||||||
{compiling ? 'Compiling...' : 'Load Image'}
|
{compiling ? 'Compiling...' : 'Load Image'}
|
||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
onClick={handleExport}
|
onClick={handleExport}
|
||||||
className="export-button"
|
className="export-button"
|
||||||
disabled={compiling || (!compiledUrl && !modifiedData)}
|
disabled={compiling || (!compiledUrl && !modifiedData)}
|
||||||
|
aria-label="Export the modified image as PNG"
|
||||||
>
|
>
|
||||||
Export
|
Export
|
||||||
</button>
|
</button>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
52
src/components/MobileInterface.tsx
Normal file
52
src/components/MobileInterface.tsx
Normal file
@ -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 (
|
||||||
|
<div className="mobile-interface">
|
||||||
|
<header className="mobile-header">
|
||||||
|
<h1 className="mobile-title">Rayon Cosmique</h1>
|
||||||
|
<div className="mobile-buttons">
|
||||||
|
<ImageUpload />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="mobile-content">
|
||||||
|
{metadata ? (
|
||||||
|
<>
|
||||||
|
<div className="mobile-image-container">
|
||||||
|
{compiling ? (
|
||||||
|
<div className="mobile-loading">
|
||||||
|
<p>Compiling image...</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ImagePreview type="modified" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="mobile-welcome">
|
||||||
|
<p className="mobile-instructions">
|
||||||
|
Load an image to start creating glitch art
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{metadata && (
|
||||||
|
<div className="mobile-controls-container">
|
||||||
|
<ActionButtons />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
src/hooks/useIsMobile.ts
Normal file
20
src/hooks/useIsMobile.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
export const useIsMobile = (breakpoint: number = 768): boolean => {
|
||||||
|
const [isMobile, setIsMobile] = useState<boolean>(
|
||||||
|
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;
|
||||||
|
};
|
||||||
117
src/hooks/useKeyboardShortcuts.ts
Normal file
117
src/hooks/useKeyboardShortcuts.ts
Normal file
@ -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]);
|
||||||
|
}
|
||||||
@ -36,7 +36,7 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
border-radius: 8px;
|
border-radius: 0;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
padding: 0.6em 1.2em;
|
padding: 0.6em 1.2em;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
|
|||||||
@ -201,6 +201,9 @@ export const initializeHexEditor = async (file: File) => {
|
|||||||
|
|
||||||
// Load first chunk
|
// Load first chunk
|
||||||
loadCurrentChunk();
|
loadCurrentChunk();
|
||||||
|
|
||||||
|
// Compile the image immediately for preview
|
||||||
|
compileImage();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update chunk data from hex editor
|
// Update chunk data from hex editor
|
||||||
|
|||||||
162
src/styles/glitch-controls.css
Normal file
162
src/styles/glitch-controls.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
286
src/styles/mobile.css
Normal file
286
src/styles/mobile.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
184
src/styles/responsive.css
Normal file
184
src/styles/responsive.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
178
src/utils/glitchEffects.ts
Normal file
178
src/utils/glitchEffects.ts
Normal file
@ -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
|
||||||
|
];
|
||||||
Reference in New Issue
Block a user