diff --git a/StringPlottingRobot/utils/Plotter-BMP-Editor.exe.tsx b/StringPlottingRobot/utils/Plotter-BMP-Editor.exe.tsx new file mode 100644 index 0000000..8bd9a1d --- /dev/null +++ b/StringPlottingRobot/utils/Plotter-BMP-Editor.exe.tsx @@ -0,0 +1,584 @@ +/** + * BMP Editor - Image converter for String Plotter + */ + +import { FC, useState, ChangeEvent, useRef, useEffect, MouseEvent } from 'react'; +import Image from 'next/image'; +import { FaImage } from 'react-icons/fa'; + +type TabMode = 'upload' | 'draw'; + +// Drawing Canvas Component +interface DrawingCanvasProps { + width: number; + height: number; + onDrawingChange: (dataUrl: string) => void; +} + +const DrawingCanvas: FC = ({ width, height, onDrawingChange }) => { + const canvasRef = useRef(null); + const [isDrawing, setIsDrawing] = useState(false); + const [canvasData, setCanvasData] = useState(null); + const [brushColor, setBrushColor] = useState('#000000'); + const [brushSize, setBrushSize] = useState(2); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + ctx.strokeStyle = brushColor; + ctx.lineWidth = brushSize; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + if (canvasData) { + const img = new window.Image(); + img.onload = () => { + ctx.fillStyle = 'white'; + ctx.fillRect(0, 0, width, height); + ctx.drawImage(img, 0, 0, width, height); + onDrawingChange(canvas.toDataURL()); + }; + img.src = canvasData; + } else { + ctx.fillStyle = 'white'; + ctx.fillRect(0, 0, width, height); + onDrawingChange(canvas.toDataURL()); + } + }, [width, height, onDrawingChange, canvasData, brushColor, brushSize]); + + const startDrawing = (e: MouseEvent) => { + const canvas = canvasRef.current; + if (!canvas) return; + + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + setIsDrawing(true); + ctx.beginPath(); + ctx.moveTo(x, y); + }; + + const draw = (e: MouseEvent) => { + if (!isDrawing) return; + + const canvas = canvasRef.current; + if (!canvas) return; + + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + ctx.lineTo(x, y); + ctx.stroke(); + }; + + const stopDrawing = () => { + if (!isDrawing) return; + + setIsDrawing(false); + const canvas = canvasRef.current; + if (canvas) { + const dataUrl = canvas.toDataURL(); + setCanvasData(dataUrl); + onDrawingChange(dataUrl); + } + }; + + const clearCanvas = () => { + const canvas = canvasRef.current; + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + ctx.fillStyle = 'white'; + ctx.fillRect(0, 0, width, height); + const dataUrl = canvas.toDataURL(); + setCanvasData(dataUrl); + onDrawingChange(dataUrl); + }; + + return ( +
+
+
+ + setBrushColor(e.target.value)} + className='h-20 w-20 cursor-pointer' + style={{ backgroundColor: brushColor }} + /> +
+
+ + setBrushSize(Number(e.target.value))} className='w-80' /> +
+
+ + +
+ ); +}; + +// Scaling Controls Component +interface ScalingControlsProps { + scaleX: number; + scaleY: number; + onScaleXChange: (value: number) => void; + onScaleYChange: (value: number) => void; + onReset: () => void; +} + +const ScalingControls: FC = ({ scaleX, scaleY, onScaleXChange, onScaleYChange, onReset }) => { + const [preserveAspectRatio, setPreserveAspectRatio] = useState(false); + const aspectRatio = scaleX / scaleY; + + const handleScaleXChange = (value: number) => { + onScaleXChange(value); + if (preserveAspectRatio) { + onScaleYChange(Math.round(value / aspectRatio)); + } + }; + + const handleScaleYChange = (value: number) => { + onScaleYChange(value); + if (preserveAspectRatio) { + onScaleXChange(Math.round(value * aspectRatio)); + } + }; + + return ( +
+

Sizing

+
+ +
+
+ + handleScaleXChange(Number(e.target.value))} + className='bg-gray-200 h-2 cursor-pointer appearance-none self-center rounded-lg' + /> +
+
+ + handleScaleYChange(Number(e.target.value))} + className='bg-gray-200 h-2 cursor-pointer appearance-none self-center rounded-lg' + /> +
+
+ +
+
+ ); +}; + +// Grayscale Controls Component +interface GrayscaleControlsProps { + onGrayscaleApply: (dataUrl: string) => void; + onReset: () => void; + imageUrl: string; +} + +const GrayscaleControls: FC = ({ onGrayscaleApply, onReset, imageUrl }) => { + const getGrayscaleValues = (shades: number) => { + const values = []; + for (let i = 0; i < shades; i += 1) { + const value = Math.round((i * 255) / (shades - 1)); + values.push(value); + } + return values; + }; + + const applyGrayscale = (shades: number) => { + if (!imageUrl) return; + + const img = new window.Image(); + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + ctx.drawImage(img, 0, 0); + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const { data } = imageData; + + for (let i = 0; i < data.length; i += 4) { + const gray = Math.round(0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2]); + const quantized = Math.floor(gray / (256 / shades)) * (255 / (shades - 1)); + + data[i] = quantized; + data[i + 1] = quantized; + data[i + 2] = quantized; + } + + ctx.putImageData(imageData, 0, 0); + onGrayscaleApply(canvas.toDataURL()); + }; + img.src = imageUrl; + }; + + return ( +
+

Grayscale

+
+ +
+
+ +
+
+ +
+
+ +
+ +
+ ); +}; + +// Main BMP Editor Component +interface BMPEditorProps { + onImageConverted?: (bmpData: string) => void; +} + +const PlotterBMPEditor: FC = ({ onImageConverted }) => { + const [error, setError] = useState(''); + const [previewUrl, setPreviewUrl] = useState(''); + const [originalUrl, setOriginalUrl] = useState(''); + const [scaleX, setScaleX] = useState(250); + const [scaleY, setScaleY] = useState(250); + const [originalScaleX, setOriginalScaleX] = useState(250); + const [originalScaleY, setOriginalScaleY] = useState(250); + const [activeTab, setActiveTab] = useState('upload'); + + const createBMP = (imageData: ImageData): Uint8Array => { + const { width, height } = imageData; + const bmpSize = 54 + width * height * 3; + const data = new Uint8Array(bmpSize); + + // BMP File Header (14 bytes) + data[0] = 0x42; + data[1] = 0x4d; + data[2] = bmpSize % 256; + data[3] = Math.floor(bmpSize / 256) % 256; + data[4] = Math.floor(bmpSize / 65536) % 256; + data[5] = Math.floor(bmpSize / 16777216) % 256; + data[10] = 54; + + // BMP Info Header (40 bytes) + data[14] = 40; + data[18] = width % 256; + data[19] = Math.floor(width / 256) % 256; + data[22] = height % 256; + data[23] = Math.floor(height / 256) % 256; + data[26] = 1; + data[28] = 24; + + // Pixel data (BGR format, bottom-to-top) + let pos = 54; + const padding = (4 - ((width * 3) % 4)) % 4; + + for (let y = height - 1; y >= 0; y -= 1) { + for (let x = 0; x < width; x += 1) { + const pixelIndex = (y * width + x) * 4; + data[pos] = imageData.data[pixelIndex + 2]; + pos += 1; + data[pos] = imageData.data[pixelIndex + 1]; + pos += 1; + data[pos] = imageData.data[pixelIndex]; + pos += 1; + } + pos += padding; + } + + return data; + }; + + const convertToBmp = () => { + if (!previewUrl) return; + + const img = new window.Image(); + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = scaleX; + canvas.height = scaleY; + + const ctx = canvas.getContext('2d'); + if (!ctx) { + setError('Failed to get canvas context'); + return; + } + + ctx.fillStyle = 'white'; + ctx.fillRect(0, 0, scaleX, scaleY); + ctx.drawImage(img, 0, 0, scaleX, scaleY); + + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const bmpData = createBMP(imageData); + + // @ts-ignore + const blob = new Blob([bmpData], { type: 'image/bmp' }); + const url = URL.createObjectURL(blob); + const timestamp = Date.now(); + const filename = `string_plotter_image_${timestamp}.bmp`; + + const link = document.createElement('a'); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + + if (onImageConverted) { + const base64String = btoa(String.fromCharCode.apply(null, Array.from(bmpData))); + onImageConverted(`data:image/bmp;base64,${base64String}`); + } + }; + img.src = previewUrl; + }; + + const handleDrawingChange = (dataUrl: string) => { + setPreviewUrl(dataUrl); + setOriginalUrl(dataUrl); + setOriginalScaleX(scaleX); + setOriginalScaleY(scaleY); + }; + + const handleGrayscaleApply = (dataUrl: string) => { + setPreviewUrl(dataUrl); + }; + + const handleGrayscaleReset = () => { + if (originalUrl) { + setPreviewUrl(originalUrl); + } + }; + + const handleScaleReset = () => { + setScaleX(originalScaleX); + setScaleY(originalScaleY); + }; + + const handleFileChange = (event: ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + if (file.type.startsWith('image/')) { + setError(''); + const reader = new FileReader(); + reader.onload = (e) => { + const dataUrl = e.target?.result as string; + + const img = new window.Image(); + img.onload = () => { + const aspectRatio = img.width / img.height; + let newWidth = 250; + let newHeight = 250; + + if (aspectRatio > 1) { + newHeight = Math.round(newWidth / aspectRatio); + } else { + newWidth = Math.round(newHeight * aspectRatio); + } + + setScaleX(newWidth); + setScaleY(newHeight); + setOriginalScaleX(newWidth); + setOriginalScaleY(newHeight); + setPreviewUrl(dataUrl); + setOriginalUrl(dataUrl); + }; + img.src = dataUrl; + }; + reader.readAsDataURL(file); + } else { + setError('Please select an image file'); + } + } + }; + + return ( +
+ {/* Tabs */} +
+ + +
+ + {/* Tab Content */} +
+ {activeTab === 'upload' && ( +
+ + {error &&

{error}

} +
+ )} + + {activeTab === 'draw' && } + + {/* Preview - hidden in drawing mode */} + {activeTab !== 'draw' && ( +
+

Preview:

+ {previewUrl ? ( + Preview + ) : ( +
+ +
+ )} +
+ )} + + + + {previewUrl && activeTab !== 'draw' && ( + <> +
+ +
+ + )} + + +
+
+ ); +}; + +export default PlotterBMPEditor; diff --git a/TankPlant/utils/MatrixFaceEditor.exe.tsx b/TankPlant/utils/MatrixFaceEditor.exe.tsx new file mode 100644 index 0000000..7b0811c --- /dev/null +++ b/TankPlant/utils/MatrixFaceEditor.exe.tsx @@ -0,0 +1,356 @@ +/** + * Matrix Face Editor - Matrix painting app for Tank Plant's faces + */ + +import { useState, useCallback, useEffect, useMemo, FC, memo } from 'react'; + +const WIDTH = 16; +const HEIGHT = 9; +const PIXEL_COUNT = WIDTH * HEIGHT; + +// Pixel Component +type PixelProps = { + color: string; + index: number; + onMouseDown: (index: number) => void; + onMouseEnter: (index: number) => void; + onTouchStart: (index: number) => void; +}; + +const Pixel: FC = memo(({ color, index, onMouseDown, onMouseEnter, onTouchStart }) => ( +
onMouseDown(index)} + onMouseEnter={() => onMouseEnter(index)} + onTouchStart={(e) => { + e.preventDefault(); + onTouchStart(index); + }} + role='tree' + tabIndex={0} + /> +)); + +// Matrix Component +type MatrixProps = { + matrixData: number[]; + getBrightnessColor: (brightness: number) => string; + onPixelMouseDown: (index: number) => void; + onPixelMouseEnter: (index: number) => void; + onPixelTouchStart: (index: number) => void; +}; + +const Matrix: FC = ({ matrixData, getBrightnessColor, onPixelMouseDown, onPixelMouseEnter, onPixelTouchStart }) => ( +
+ {matrixData.map((brightness, index) => ( + + ))} +
+); + +// Tools Panel Component +type ToolsPanelProps = { + brightness: number; + setBrightness: (value: number) => void; + colorMode: string; + setColorMode: (mode: string) => void; + brightnessPreviewColor: string; + onClear: () => void; + onFill: () => void; +}; + +const ToolsPanel: FC = ({ brightness, setBrightness, colorMode, setColorMode, brightnessPreviewColor, onClear, onFill }) => ( +
+

Tools

+
+
+ + setBrightness(Number(e.target.value))} + className='max-w-[255px]' + /> +
+
+
+ + +
+
+ + +
+
+
+); + +// Color Palette Component +type ColorPaletteProps = { + paletteColors: number[]; + setPaletteColor: (index: number, color: number) => void; + selectPaletteColor: (color: number) => void; + getBrightnessColor: (brightness: number) => string; + currentBrightness: number; +}; + +const ColorPalette: FC = ({ paletteColors, setPaletteColor, selectPaletteColor, getBrightnessColor, currentBrightness }) => ( +
+

Color Palette

+
+ {paletteColors.map((color, index) => ( +
+
selectPaletteColor(color)} + onKeyUp={() => selectPaletteColor(color)} + tabIndex={0} + role='button' + /> + +
+ ))} +
+
+); + +// Export Controls Component +type ExportControlsProps = { + matrixData: number[]; + onLoad: (data: number[]) => void; +}; + +const ExportControls: FC = ({ matrixData, onLoad }) => { + const [exportText, setExportText] = useState(''); + const [copyStatus, setCopyStatus] = useState('Copy'); + const [loadStatus, setLoadStatus] = useState('Load'); + + const generateExportText = () => { + const header = `constexpr uint8_t matrixData[${PIXEL_COUNT}] PROGMEM = {\n`; + const footer = '\n};'; + + const rows = Array.from({ length: HEIGHT }, (_, rowIndex) => matrixData.slice(rowIndex * WIDTH, rowIndex * WIDTH + WIDTH)); + + const formattedRows = rows.map((row, i) => ` ${row.map((value) => String(value).padStart(3, ' ')).join(', ')}${i < HEIGHT - 1 ? ',' : ''}`); + + const body = formattedRows.join('\n'); + setExportText(header + body + footer); + }; + + const handleCopy = () => { + if (!exportText) return; + navigator.clipboard.writeText(exportText).then(() => { + setCopyStatus('Copied!'); + setTimeout(() => setCopyStatus('Copy'), 2000); + }); + }; + + const handleLoad = () => { + try { + const matches = exportText.match(/\{([^}]+)\}/); + if (!matches?.[1]) throw new Error('Could not find array data in {}.'); + + const numbers = matches[1].split(',').map((n) => parseInt(n.trim(), 10)); + if (numbers.some(isNaN) || numbers.length !== 144) { + throw new Error('Array must contain exactly 144 valid numbers.'); + } + onLoad(numbers); + setLoadStatus('Loaded!'); + setTimeout(() => setLoadStatus('Load'), 2000); + } catch (error) { + console.log(error instanceof Error ? error.message : 'Invalid array format.'); + } + }; + + return ( +
+

Export Matrix

+
+ + + +
+