Midterm Project : Pixel Art

For my midterm project, I drew inspiration from the color-by-number games that are so popular on the internet. I used to be obsessed with them, so I decided to create my own pixelated version. In my project, users color cell by cell in a grid, where each cell corresponds to a letter, and each color on the palette is assigned to a letter as well. By following the key and filling in the grid, the design gradually comes to life, revealing the full picture bit by bit. There’s something incredibly relaxing about mindlessly clicking, watching the colors take shape, and seeing the image emerge one step at a time. It’s a simple yet satisfying process, where every small action contributes to a larger, beautiful result.

The game starts with a welcome screen where the user has two options: Start, to begin coloring immediately, or Help, which provides instructions on the various features the game offers. Once they click Start, they are taken to the selection screen, where they can choose a coloring page from the available options. After selecting a page, they arrive at the actual coloring screen, where the process is simple—choose a color from the palette above and start coloring. However, if the user accidentally fills in the wrong square, they can double-tap the cell to remove the color. If they wish to start over or choose another page, a Reset button clears the canvas. Once the user finishes coloring their chosen page, the game is considered complete, and they are prompted to restart if they wish to play/color again.


One aspect of my code that I’m particularly proud of is the implementation of the actual coloring page. The way the cells fill in over the image has a seamless and polished look. I also like how intuitive the overall user interface is—even first-time users can start playing without needing to read the instructions. I chose soothing shades of brown for the interface, paired with lofi background music, to enhance the game’s relaxing atmosphere.

That said, I encountered a few challenges along the way, particularly with the coloring process itself. One major issue was ensuring that the grid, which tracks cell positions, perfectly aligned with the image. Initially, a double grid effect was visible on top of the image, which was distracting. To fix this, I had to experiment with different parameters before finally finding a formula that worked—provided the image was a perfect square with no empty or extra areas. This was implemented in the ColoringPage class. Once the grid was properly aligned, tracking mouse clicks and filling in the correct areas became much smoother.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class ColoringPage {
constructor(name, imagePath,thumbPath, rows, cols, palette,targetCells) {
this.name = name;
this.img = pageImages[name.toLowerCase()];
this.thumb = pageThumbnails[name.toLowerCase()];
this.rows = rows;
this.cols = cols;
this.cellSize = 600 / this.cols;
this.grid = Array.from({ length: this.rows }, () => Array(this.cols).fill(null));
this.palette = palette;
this.selectedColor = Object.values(palette)[0].color;
this.targetCells = targetCells;
this.filledCells = 0;
}
display() {
this.drawPalette();
image(this.img, 100, 90, 600, 600);
this.drawGrid();
this.drawColoredGrid();
}
drawGrid() {
stroke(0, 50);
noFill();
for (let row = 0; row < this.rows; row++) {
for (let col = 0; col < this.cols; col++) {
rect(100 + col * this.cellSize, 90 + row * this.cellSize, this.cellSize, this.cellSize);
}
}
}
drawColoredGrid() {
for (let row = 0; row < this.rows; row++) {
for (let col = 0; col < this.cols; col++) {
if (this.grid[row][col]) {
fill(this.grid[row][col]);
rect(100 + col * this.cellSize, 90 + row * this.cellSize, this.cellSize, this.cellSize);
}
}
}
}
drawPalette() {
let keys = Object.keys(this.palette);
let x = (width - keys.length * 60) / 2;
let y = 20;
noStroke();
for (let i = 0; i < keys.length; i++) {
let colorValue = this.palette[keys[i]].color;
let isSelected = this.selectedColor === colorValue;
let isHovered = mouseX > x + i * 60 && mouseX < x + i * 60 + 50 &&
mouseY > y && mouseY < y + 50;
let circleSize = 50;
if (isHovered) circleSize = 55;
let centerX = x + i * 60 + 30;
let centerY = y + 25;
if (isSelected) {
fill(255);
ellipse(centerX, centerY, circleSize + 8, circleSize + 8);
}
fill(colorValue);
ellipse(centerX, centerY, circleSize, circleSize);
let c = color(colorValue);
let brightnessValue = (red(c) * 0.299 + green(c) * 0.587 + blue(c) * 0.114);
fill(brightnessValue < 128 ? 255 : 0);
textSize(14);
textAlign(CENTER, CENTER);
let labelChar = this.palette[keys[i]].label;
text(labelChar, centerX, centerY);
}
}
selectColor() {
let keys = Object.keys(this.palette);
let x = (width - keys.length * 60) / 2;
for (let i = 0; i < keys.length; i++) {
if (mouseX > x + i * 60 && mouseX < x + i * 60 + 50 && mouseY > 20 && mouseY < 70) {
this.selectedColor = this.palette[keys[i]].color;
break;
}
}
}
fillCell() {
let col = floor((mouseX - 100) / this.cellSize);
let row = floor((mouseY - 90) / this.cellSize);
if (row >= 0 && row < this.rows && col >= 0 && col < this.cols) {
if (!this.grid[row][col]) {
this.grid[row][col] = this.selectedColor;
this.filledCells++;
console.log(this.filledCells)
if (this.isCompleted()) {
game.state = "final";
}
}
}
}
class ColoringPage { constructor(name, imagePath,thumbPath, rows, cols, palette,targetCells) { this.name = name; this.img = pageImages[name.toLowerCase()]; this.thumb = pageThumbnails[name.toLowerCase()]; this.rows = rows; this.cols = cols; this.cellSize = 600 / this.cols; this.grid = Array.from({ length: this.rows }, () => Array(this.cols).fill(null)); this.palette = palette; this.selectedColor = Object.values(palette)[0].color; this.targetCells = targetCells; this.filledCells = 0; } display() { this.drawPalette(); image(this.img, 100, 90, 600, 600); this.drawGrid(); this.drawColoredGrid(); } drawGrid() { stroke(0, 50); noFill(); for (let row = 0; row < this.rows; row++) { for (let col = 0; col < this.cols; col++) { rect(100 + col * this.cellSize, 90 + row * this.cellSize, this.cellSize, this.cellSize); } } } drawColoredGrid() { for (let row = 0; row < this.rows; row++) { for (let col = 0; col < this.cols; col++) { if (this.grid[row][col]) { fill(this.grid[row][col]); rect(100 + col * this.cellSize, 90 + row * this.cellSize, this.cellSize, this.cellSize); } } } } drawPalette() { let keys = Object.keys(this.palette); let x = (width - keys.length * 60) / 2; let y = 20; noStroke(); for (let i = 0; i < keys.length; i++) { let colorValue = this.palette[keys[i]].color; let isSelected = this.selectedColor === colorValue; let isHovered = mouseX > x + i * 60 && mouseX < x + i * 60 + 50 && mouseY > y && mouseY < y + 50; let circleSize = 50; if (isHovered) circleSize = 55; let centerX = x + i * 60 + 30; let centerY = y + 25; if (isSelected) { fill(255); ellipse(centerX, centerY, circleSize + 8, circleSize + 8); } fill(colorValue); ellipse(centerX, centerY, circleSize, circleSize); let c = color(colorValue); let brightnessValue = (red(c) * 0.299 + green(c) * 0.587 + blue(c) * 0.114); fill(brightnessValue < 128 ? 255 : 0); textSize(14); textAlign(CENTER, CENTER); let labelChar = this.palette[keys[i]].label; text(labelChar, centerX, centerY); } } selectColor() { let keys = Object.keys(this.palette); let x = (width - keys.length * 60) / 2; for (let i = 0; i < keys.length; i++) { if (mouseX > x + i * 60 && mouseX < x + i * 60 + 50 && mouseY > 20 && mouseY < 70) { this.selectedColor = this.palette[keys[i]].color; break; } } } fillCell() { let col = floor((mouseX - 100) / this.cellSize); let row = floor((mouseY - 90) / this.cellSize); if (row >= 0 && row < this.rows && col >= 0 && col < this.cols) { if (!this.grid[row][col]) { this.grid[row][col] = this.selectedColor; this.filledCells++; console.log(this.filledCells) if (this.isCompleted()) { game.state = "final"; } } } }
class ColoringPage {
  constructor(name, imagePath,thumbPath, rows, cols, palette,targetCells) {
    this.name = name;
    this.img = pageImages[name.toLowerCase()];  
    this.thumb = pageThumbnails[name.toLowerCase()];
    this.rows = rows;
    this.cols = cols;
    this.cellSize = 600 / this.cols;
    this.grid = Array.from({ length: this.rows }, () => Array(this.cols).fill(null));
    this.palette = palette;
    this.selectedColor = Object.values(palette)[0].color;
    this.targetCells = targetCells; 
    this.filledCells = 0;
  }

  display() {
    this.drawPalette();
    image(this.img, 100, 90, 600, 600);
    this.drawGrid();
    this.drawColoredGrid();
  }

  drawGrid() {
    stroke(0, 50);
    noFill();
    for (let row = 0; row < this.rows; row++) {
      for (let col = 0; col < this.cols; col++) {
        rect(100 + col * this.cellSize, 90 + row * this.cellSize, this.cellSize, this.cellSize);
      }
    }
  }

  drawColoredGrid() {
    for (let row = 0; row < this.rows; row++) {
      for (let col = 0; col < this.cols; col++) {
        if (this.grid[row][col]) {
          fill(this.grid[row][col]);
          rect(100 + col * this.cellSize, 90 + row * this.cellSize, this.cellSize, this.cellSize);
        }
      }
    }
  }

  drawPalette() {
    let keys = Object.keys(this.palette);
    let x = (width - keys.length * 60) / 2;
    let y = 20;

    noStroke();

    for (let i = 0; i < keys.length; i++) {
        let colorValue = this.palette[keys[i]].color;
        let isSelected = this.selectedColor === colorValue;
        let isHovered = mouseX > x + i * 60 && mouseX < x + i * 60 + 50 &&
                        mouseY > y && mouseY < y + 50;

        let circleSize = 50; 
        if (isHovered) circleSize = 55; 

        let centerX = x + i * 60 + 30;
        let centerY = y + 25;

        if (isSelected) {
            fill(255); 
            ellipse(centerX, centerY, circleSize + 8, circleSize + 8);
        }

        fill(colorValue);
        ellipse(centerX, centerY, circleSize, circleSize); 
      
      let c = color(colorValue);
      let brightnessValue = (red(c) * 0.299 + green(c) * 0.587 + blue(c) * 0.114); 

        fill(brightnessValue < 128 ? 255 : 0);
        textSize(14);
        textAlign(CENTER, CENTER);
        
        let labelChar = this.palette[keys[i]].label;  
        text(labelChar, centerX, centerY);
    }
}


  selectColor() {
    let keys = Object.keys(this.palette);
    let x = (width - keys.length * 60) / 2;
    for (let i = 0; i < keys.length; i++) {
      if (mouseX > x + i * 60 && mouseX < x + i * 60 + 50 && mouseY > 20 && mouseY < 70) {
        this.selectedColor = this.palette[keys[i]].color;
        break;
      }
    }
  }

  fillCell() {
    let col = floor((mouseX - 100) / this.cellSize);
    let row = floor((mouseY - 90) / this.cellSize);

    if (row >= 0 && row < this.rows && col >= 0 && col < this.cols) {
      if (!this.grid[row][col]) {
        this.grid[row][col] = this.selectedColor;
        this.filledCells++;
        console.log(this.filledCells)
        if (this.isCompleted()) {
          game.state = "final";
        }
      }
    }
  }

Another challenge I faced was determining the end condition for the game. Currently, the game ends when the total number of filled cells matches the number of cells corresponding to a color on the grid. However, this assumes that users only color the cells with assigned letters and don’t fill in any blank spaces. Additionally, since the game allows users to fill any cell with any color, the code does not track whether a specific cell is filled with the correct color. A possible solution would be to store a reference for each cell’s correct color in every coloring page, but I haven’t yet found a way to do this dynamically without hardcoding hundreds of cells, which would be inefficient and take up unnecessary space in the code. This is an area I would like to improve in the future.

Leave a Reply