xxxxxxxxxx
717
function keyPressed() {
// Toggle eraser mode with 'e' key
if (key === 'e' || key === 'E') {
toggleEraser();
}
}
var grid = 4;
var show = false;
var pixelCoords = new Set();
var animationIndex = 0;
var lastAnimationTime = 0;
var animationSpeed = 30; // Increased animation speed from 100 to 30 (faster)
var lastX = 0;
var lastY = 0;
var exportCount = {};
let refImg;
let imgLoaded = false;
let showRef = false;
let showGrid = false; // Flag for showing grid
let quantizedImg = null; // Initialize as null
let colorPicker, nameInput, clearButton, exportButton, refButton, fileInput, traceButton, gridButton;
// Cursor variables
let cursorX = 0;
let cursorY = 0;
// "Trace Section" feature variables
let traceSectionButton;
let circleSelectMode = false;
let isDraggingCircle = false;
let circleStartX = 0;
let circleStartY = 0;
let circleRadius = 0;
// Eraser feature variable
let eraserButton;
let eraserMode = false;
function setup() {
createCanvas(512, 256);
background(10);
// First column of buttons
colorPicker = createColorPicker("#FFFFFF");
colorPicker.position(10, height + 10);
colorPicker.size(colorPicker.width, 28);
nameInput = createInput('mySprite');
nameInput.position(colorPicker.x + colorPicker.width + 10, height + 10);
nameInput.size(150, 28);
clearButton = createButton("CLEAR");
clearButton.position(10, height + 48);
clearButton.size(100, 32);
clearButton.mousePressed(clean);
exportButton = createButton("EXPORT COORDS");
exportButton.position(clearButton.x + clearButton.width + 10, height + 48);
exportButton.size(116, 32);
exportButton.mousePressed(exportCoords);
// Second column of buttons (positioned to the right)
const secondColX = nameInput.x + nameInput.width + 20;
refButton = createButton("REF PICTURE");
refButton.position(secondColX, height + 10);
refButton.size(100, 32);
refButton.mousePressed(() => showRef = !showRef);
traceButton = createButton("TRACE");
traceButton.position(refButton.x + refButton.width + 10, height + 10);
traceButton.size(80, 32);
traceButton.mousePressed(traceImage);
// "Trace Section" button
traceSectionButton = createButton("TRACE SECTION (OFF)");
traceSectionButton.position(secondColX, height + 48);
traceSectionButton.size(140, 32);
traceSectionButton.mousePressed(enableTraceSection);
// Eraser button
eraserButton = createButton("ERASER (OFF)");
eraserButton.position(traceSectionButton.x + traceSectionButton.width + 10, height + 48);
eraserButton.size(100, 32);
eraserButton.mousePressed(toggleEraser);
// Grid button - place in the third row instead
gridButton = createButton("SHOW GRID (OFF)");
gridButton.position(10, height + 90);
gridButton.size(120, 32);
gridButton.mousePressed(toggleGrid);
// File input - position beside grid button
fileInput = createFileInput(handleFile);
fileInput.position(gridButton.x + gridButton.width + 10, height + 90);
fileInput.size(300);
let canvasElement = document.querySelector('canvas');
canvasElement.addEventListener('contextmenu', (e) => {
e.preventDefault();
});
}
function draw() {
background(10);
// Show reference image if toggled
if (showRef && imgLoaded) {
push();
tint(255, 127);
image(refImg, 0, 0, width, height);
pop();
}
// Draw grid if enabled
if (showGrid) {
drawPixelGrid();
}
// Draw all pixels from pixelCoords
for (let coordStr of pixelCoords) {
let coord = coordStr.split(',').map(Number);
noStroke();
fill(colorPicker.color());
square(coord[0] * grid, coord[1] * grid, grid);
}
// Simple animation of one highlighted pixel
if (pixelCoords.size > 0) {
let nowTime = millis();
if (nowTime - lastAnimationTime > animationSpeed) {
let coordArray = Array.from(pixelCoords);
let currentCoord = coordArray[animationIndex].split(',').map(Number);
noStroke();
fill(255, 0, 0);
square(currentCoord[0] * grid, currentCoord[1] * grid, grid);
animationIndex = (animationIndex + 1) % coordArray.length;
lastAnimationTime = nowTime;
} else {
let coordArray = Array.from(pixelCoords);
let currentCoord = coordArray[animationIndex].split(',').map(Number);
noStroke();
fill(255, 0, 0);
square(currentCoord[0] * grid, currentCoord[1] * grid, grid);
}
}
// Cross lines for reference
stroke(150);
line(width/2, 0, width/2, height);
line(0, height/2, width, height/2);
// Redraw logic to keep animation "blinking"
if (pixelCoords.size > 0) {
let nowTime = millis();
if (nowTime - lastAnimationTime > animationSpeed) {
if (showRef && imgLoaded) {
background(10);
push();
tint(255, 127);
image(refImg, 0, 0, width, height);
pop();
}
redrawAllPixels();
let coordArray = Array.from(pixelCoords);
let currentCoord = coordArray[animationIndex].split(',').map(Number);
noStroke();
fill(255, 0, 0);
square(currentCoord[0] * grid, currentCoord[1] * grid, grid);
animationIndex = (animationIndex + 1) % coordArray.length;
lastAnimationTime = nowTime;
}
}
// Update cursor position
cursorX = snap(mouseX);
cursorY = snap(mouseY);
// Draw cursor (if mouse is inside the canvas area)
if (mouseX >= 0 && mouseX < width && mouseY >= 0 && mouseY < height) {
drawCursor();
}
// If we are in "circleSelectMode", draw the selection circle while dragging
if (circleSelectMode) {
if (isDraggingCircle) {
stroke(255, 0, 0);
noFill();
circleRadius = dist(circleStartX, circleStartY, mouseX, mouseY);
ellipse(circleStartX, circleStartY, circleRadius * 2, circleRadius * 2);
}
}
else {
// Normal painting / erasing logic (only if we are NOT in circle select mode)
if (mouseIsPressed) {
var currentX = snap(mouseX);
var currentY = snap(mouseY);
if (lastX === 0 && lastY === 0) {
lastX = currentX;
lastY = currentY;
}
// Draw a line from lastX,lastY to currentX,currentY using Bresenham algorithm
if (mouseButton === LEFT && !eraserMode) {
drawBresenhamLine(
Math.floor(lastX / grid),
Math.floor(lastY / grid),
Math.floor(currentX / grid),
Math.floor(currentY / grid)
);
}
else if ((mouseButton === RIGHT) || (mouseButton === LEFT && eraserMode)) {
// Erase using the same algorithm to ensure clean erasure of diagonal lines
eraseBresenhamLine(
Math.floor(lastX / grid),
Math.floor(lastY / grid),
Math.floor(currentX / grid),
Math.floor(currentY / grid)
);
}
lastX = currentX;
lastY = currentY;
}
else {
lastX = 0;
lastY = 0;
}
}
}
// Bresenham line algorithm for drawing 1-pixel thick lines
function drawBresenhamLine(x0, y0, x1, y1) {
const dx = Math.abs(x1 - x0);
const dy = Math.abs(y1 - y0);
const sx = (x0 < x1) ? 1 : -1;
const sy = (y0 < y1) ? 1 : -1;
let err = dx - dy;
while (true) {
// Draw pixel at x0, y0
if (x0 >= 0 && x0 < 128 && y0 >= 0 && y0 < 64) {
noStroke();
fill(colorPicker.color());
square(x0 * grid, y0 * grid, grid);
pixelCoords.add(`${x0},${y0}`);
}
if (x0 === x1 && y0 === y1) break;
const e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x0 += sx;
}
if (e2 < dx) {
err += dx;
y0 += sy;
}
}
}
// Bresenham line algorithm for erasing
function eraseBresenhamLine(x0, y0, x1, y1) {
const dx = Math.abs(x1 - x0);
const dy = Math.abs(y1 - y0);
const sx = (x0 < x1) ? 1 : -1;
const sy = (y0 < y1) ? 1 : -1;
let err = dx - dy;
while (true) {
// Erase pixel at x0, y0
if (x0 >= 0 && x0 < 128 && y0 >= 0 && y0 < 64) {
pixelCoords.delete(`${x0},${y0}`);
}
if (x0 === x1 && y0 === y1) break;
const e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x0 += sx;
}
if (e2 < dx) {
err += dx;
y0 += sy;
}
}
}
// p5 mouse functions for "Trace Section" mode
function mousePressed() {
if (circleSelectMode && mouseButton === LEFT) {
isDraggingCircle = true;
circleStartX = mouseX;
circleStartY = mouseY;
circleRadius = 0;
}
}
function mouseReleased() {
if (circleSelectMode && mouseButton === LEFT && isDraggingCircle) {
isDraggingCircle = false;
// Perform partial trace on the selected circle region
let cxQuant = circleStartX / 4;
let cyQuant = circleStartY / 4;
let rQuant = circleRadius / 4;
let startPixel = findStartPixelInCircle(cxQuant, cyQuant, rQuant);
if (startPixel) {
let contourPoints = traceContourInCircle(startPixel, cxQuant, cyQuant, rQuant);
for (let point of contourPoints) {
pixelCoords.add(`${point.x},${point.y}`);
}
}
}
}
// Toggle the circle selection mode
function enableTraceSection() {
circleSelectMode = !circleSelectMode;
if (circleSelectMode) {
traceSectionButton.html("TRACE SECTION (ON)");
} else {
traceSectionButton.html("TRACE SECTION (OFF)");
}
}
// Toggle eraser mode
function toggleEraser() {
eraserMode = !eraserMode;
if (eraserMode) {
eraserButton.html("ERASER (ON)");
} else {
eraserButton.html("ERASER (OFF)");
}
}
// Toggle grid visibility
function toggleGrid() {
showGrid = !showGrid;
if (showGrid) {
gridButton.html("SHOW GRID (ON)");
} else {
gridButton.html("SHOW GRID (OFF)");
}
}
// Draw grid lines
function drawPixelGrid() {
stroke(50); // Dark gray lines
strokeWeight(0.5);
// Draw vertical grid lines
for (let x = 0; x <= width; x += grid) {
line(x, 0, x, height);
}
// Draw horizontal grid lines
for (let y = 0; y <= height; y += grid) {
line(0, y, width, y);
}
// Reset stroke weight
strokeWeight(1);
}
// Draw the cursor at current mouse position
function drawCursor() {
// Calculate the grid cell
let gridX = Math.floor(cursorX / grid);
let gridY = Math.floor(cursorY / grid);
// Draw the cursor outline (larger square to show the current pixel)
noFill();
if (eraserMode) {
// Red outline for eraser mode
stroke(255, 0, 0);
} else {
// White outline for paint mode
stroke(255);
}
strokeWeight(2);
square(gridX * grid, gridY * grid, grid);
strokeWeight(1);
// Show coordinates near the cursor
fill(255);
noStroke();
textSize(12);
text(`(${gridX}, ${gridY})`, gridX * grid + grid + 4, gridY * grid);
}
// Helper: check if a point (x,y) is inside the circle (cx,cy,r)
function insideCircle(cx, cy, r, x, y) {
let dx = x - cx;
let dy = y - cy;
return (dx*dx + dy*dy) <= (r*r);
}
// Find the first black pixel (bottom-to-top) within the circle
function findStartPixelInCircle(cx, cy, r) {
for (let x = 0; x < 128; x++) {
for (let y = 63; y >= 0; y--) {
if (insideCircle(cx, cy, r, x, y) && getPixelColor(x, y) < 127) {
return { x, y };
}
}
}
return null;
}
// Trace contour within the circle using Moore's boundary following
function traceContourInCircle(start, cx, cy, r) {
let contourPoints = [];
let visited = new Set();
let current = start;
let lastMove = { x: current.x, y: current.y + 1 };
do {
contourPoints.push(current);
let key = `${current.x},${current.y}`;
visited.add(key);
let neighbors = getMooreNeighbors(current);
let found = false;
let backtrackPoint = lastMove;
let startIdx = 0;
for (let i = 0; i < neighbors.length; i++) {
if (neighbors[i].x === backtrackPoint.x && neighbors[i].y === backtrackPoint.y) {
startIdx = (i + 1) % 8;
break;
}
}
for (let i = 0; i < 8; i++) {
let idx = (startIdx + i) % 8;
let neighbor = neighbors[idx];
// Check if neighbor is black AND inside the selection circle
if (insideCircle(cx, cy, r, neighbor.x, neighbor.y) && getPixelColor(neighbor.x, neighbor.y) < 127) {
lastMove = current;
current = neighbor;
found = true;
break;
}
}
if (!found) {
current = backtrackPoint;
}
} while (
!(current.x === start.x &&
current.y === start.y &&
contourPoints.length > 2)
);
return contourPoints;
}
function handleFile(file) {
// Check file extension
const fileName = file.name.toLowerCase();
// Handle text file for importing coordinates
if (fileName.endsWith('.txt')) {
let reader = new FileReader();
reader.onload = function(e) {
importCoordinatesFromText(e.target.result);
};
reader.readAsText(file.file);
return;
}
// Handle image file
imgLoaded = false;
quantizedImg = null; // Reset quantizedImg
if (file.type === 'image') {
refImg = createImg(file.data, 'Reference image', 'anonymous', imgCreated);
refImg.hide();
} else {
refImg = null;
}
}
// Function to import coordinates from text file
function importCoordinatesFromText(text) {
try {
// Clear current drawing
clean();
// Regular expressions to match the coordinate format
const coordRegex = /\{\s*(\d+),\s*(\d+)\s*\}/g;
let match;
// Extract all coordinates
while ((match = coordRegex.exec(text)) !== null) {
const x = parseInt(match[1]);
const y = parseInt(match[2]);
// Add to pixelCoords
pixelCoords.add(`${x},${y}`);
}
// Check if we found any coordinates
if (pixelCoords.size > 0) {
console.log(`Imported ${pixelCoords.size} coordinates from text file`);
// Try to extract name from the file for the nameInput field
const nameMatch = text.match(/const\s+uint8_t\s+([a-zA-Z0-9_]+)/);
if (nameMatch && nameMatch[1]) {
nameInput.value(nameMatch[1]);
}
} else {
console.log('No valid coordinates found in the text file');
}
} catch (error) {
console.error('Error importing coordinates:', error);
}
}
function imgCreated() {
refImg.hide();
let g = createGraphics(refImg.elt.width, refImg.elt.height);
g.image(refImg, 0, 0);
refImg.remove();
refImg = g.get(0, 0, g.width, g.height);
if (refImg.width / refImg.height > width / height) {
refImg.resize(width, 0);
} else {
refImg.resize(0, height);
}
// Create quantized version for tracing
quantizedImg = createGraphics(128, 64);
quantizedImg.pixelDensity(1);
quantizedImg.background(255);
quantizedImg.image(refImg, 0, 0, 128, 64);
quantizedImg.loadPixels();
// Threshold the image
for (let i = 0; i < quantizedImg.pixels.length; i += 4) {
let avg = (quantizedImg.pixels[i] + quantizedImg.pixels[i + 1] + quantizedImg.pixels[i + 2]) / 3;
let binary = avg < 127 ? 0 : 255;
quantizedImg.pixels[i] = binary;
quantizedImg.pixels[i + 1] = binary;
quantizedImg.pixels[i + 2] = binary;
quantizedImg.pixels[i + 3] = 255;
}
quantizedImg.updatePixels();
imgLoaded = true;
showRef = true;
}
function getPixelColor(x, y) {
if (!quantizedImg || x < 0 || x >= 128 || y < 0 || y >= 64) return 255;
let index = 4 * (y * 128 + x);
return quantizedImg.pixels[index];
}
function findStartPixel() {
if (!quantizedImg) return null;
for (let x = 0; x < 128; x++) {
for (let y = 63; y >= 0; y--) {
if (getPixelColor(x, y) < 127) {
return { x, y };
}
}
}
return null;
}
function getMooreNeighbors(pixel) {
return [
{ x: pixel.x + 1, y: pixel.y }, // right
{ x: pixel.x + 1, y: pixel.y - 1 }, // top-right
{ x: pixel.x, y: pixel.y - 1 }, // top
{ x: pixel.x - 1, y: pixel.y - 1 }, // top-left
{ x: pixel.x - 1, y: pixel.y }, // left
{ x: pixel.x - 1, y: pixel.y + 1 }, // bottom-left
{ x: pixel.x, y: pixel.y + 1 }, // bottom
{ x: pixel.x + 1, y: pixel.y + 1 } // bottom-right
];
}
function traceContour(start) {
let contourPoints = [];
let visited = new Set();
let current = start;
let lastMove = { x: current.x, y: current.y + 1 };
do {
contourPoints.push(current);
let key = `${current.x},${current.y}`;
visited.add(key);
let neighbors = getMooreNeighbors(current);
let found = false;
let backtrackPoint = lastMove;
let startIdx = 0;
for (let i = 0; i < neighbors.length; i++) {
if (neighbors[i].x === backtrackPoint.x && neighbors[i].y === backtrackPoint.y) {
startIdx = (i + 1) % 8;
break;
}
}
for (let i = 0; i < 8; i++) {
let idx = (startIdx + i) % 8;
let neighbor = neighbors[idx];
if (getPixelColor(neighbor.x, neighbor.y) < 127) {
lastMove = current;
current = neighbor;
found = true;
break;
}
}
if (!found) {
current = backtrackPoint;
}
} while (
!(current.x === start.x &&
current.y === start.y &&
contourPoints.length > 2)
);
return contourPoints;
}
function traceImage() {
if (!imgLoaded || !quantizedImg) {
console.log('No image loaded');
return;
}
clean();
let startPixel = findStartPixel();
if (startPixel) {
let contourPoints = traceContour(startPixel);
for (let point of contourPoints) {
pixelCoords.add(`${point.x},${point.y}`);
}
}
}
function snap(p) {
var cell = Math.round((p - grid / 2) / grid);
return cell * grid;
}
function redrawAllPixels() {
for (let coordStr of pixelCoords) {
let coord = coordStr.split(',').map(Number);
noStroke();
fill(colorPicker.color());
square(coord[0] * grid, coord[1] * grid, grid);
}
}
function clean() {
pixelCoords.clear();
animationIndex = 0;
lastAnimationTime = 0;
}
function exportCoords() {
let baseName = nameInput.value().trim() || 'mySprite';
if (!(baseName in exportCount)) {
exportCount[baseName] = 0;
}
exportCount[baseName]++;
let varName = baseName;
if (exportCount[baseName] > 1) {
varName = `${baseName}_${exportCount[baseName]}`;
}
let output = `const uint8_t ${varName}[][2] = {\n`;
let coordArray = Array.from(pixelCoords)
.map(coord => coord.split(',').map(Number));
coordArray.forEach((coord, index) => {
output += ` {${coord[0]}, ${coord[1]}}`;
if (index < coordArray.length - 1) output += ",";
output += "\n";
});
output += "};\n";
output += `const uint16_t ${varName}_length = ${coordArray.length};\n`;
let blob = new Blob([output], {type: 'text/plain'});
let url = window.URL.createObjectURL(blob);
let a = document.createElement('a');
a.href = url;
a.download = `${varName}.txt`;
a.click();
window.URL.revokeObjectURL(url);
}