Turn a Photo into a Painting

Build a browser app that turns photos into animated brushstroke paintings.

Introduction

30 Second Summary

A photo captures a moment with precise detail. A painting can turn those same shapes and colours into texture and movement.

In this project, you will build a web page that transforms any photo into an animated brushstroke painting. You will create the effect with the browser canvas element, Sobel edge detection, and plain JavaScript.

What You'll Build

Your public page turns a phone photo into a painting that builds from broad strokes to fine detail before saving as a PNG.

By the end of this project, you'll have:

  • Choose any photo and see a fitted canvas preview appear in your browser.
  • Reveal a black image with white edge outlines that trace the shapes in your photo.
  • Open a public photo painter on your phone to change the brush size and save the finished artwork.
  • Take on an optional Secret Mission to push your image-processing skills further.

Are there any prerequisites?

You need a browser and a text editor. Basic JavaScript experience with loops over pixel arrays helps you follow the image filter.

Before We Start

A brushstroke filter is easier to recreate after you have seen its full motion. A finished screenshot gives you a concrete target for every later check.

This step gets your local workspace ready. You also capture a finished painting from Quick Study as your visual target.

In this step, get ready to:
  • Confirm your desktop browser plus code editor are available.
  • Prepare the photo-to-painting folder with a source image.
  • Confirm access to GitHub before capturing your Quick Study reference.
Confirm your tools and GitHub access

Your browser displays the painting reference. Your code editor holds the local project files.

GitHub provides the publishing account you use later. Confirming access now keeps the final publishing step focused.

  • Use your operating system's application search to launch your preferred desktop browser.
  • Use the same application search to launch your code editor.
  • Visit GitHub in your browser.

Account setup can feel sensitive. Your GitHub password stays in GitHub's own sign-in flow.

  • Complete GitHub account access through the sign-in or account-creation flow.

You should reach a signed-in GitHub page with access to your account menu. That clears the publishing account hurdle before you write any project code.

Missing a Browser or Code Editor?

  • Install a current desktop browser from the provider's official website if your browser does not launch.
  • Install a desktop code editor from the provider's official website if your editor does not launch.

Use this if you need help choosing or installing either tool: Help me prepare a desktop browser and code editor for a local HTML project.

Prepare your folder and source photo

Keeping the project folder on your Desktop makes it easy to find in later steps. A local image also gives your page direct access to the photo data.

The source photo must be yours or licensed for your use. That choice keeps the public version safe to share.

  • Navigate to your Desktop in your operating system's file manager.
  • Use the new-folder control to create photo-to-painting on the Desktop.
  • Use your code editor's folder-opening control to select photo-to-painting.

Your code editor should now show photo-to-painting as the active folder. This confirms your local workspace is ready.

  • Choose one image file that you are allowed to use.
  • Copy the image into photo-to-painting.

You should see the image listed inside photo-to-painting in your editor. Your source material is now ready for the browser.

Image Missing From the Folder?

  • Refresh your editor's file view if the image is visible in the file manager only.
  • Copy the image into photo-to-painting again if it was placed beside the folder.

Use this if the image still does not appear: Help me find an image file that is missing from my code editor's file view.

Capture your painting target

The reference tool reveals the visual rhythm your own page needs to reproduce. It starts with broad strokes before adding smaller details.

Before the check, here is your prediction prompt: which parts of the image will the smallest strokes preserve.

  • Select Open a photo.
  • Choose your source image from photo-to-painting.
  • Wait for the fine strokes to finish.

You should see a finished painting that still resembles your source photo. Fine strokes should sharpen details inside the broad painted shapes.

Why Paint From Coarse to Fine?

Broad strokes establish the large colour regions quickly. Smaller strokes add detail after the main structure is visible.

This order keeps the painting readable as it develops. It also gives you a clear behaviour to recreate later.

  • Use your operating system's screenshot tool to capture the finished painting.
  • Save the screenshot inside photo-to-painting.

Your editor should now show the source image plus your reference screenshot. You have a visible target for the painting effect.

Your tools plus visual target are ready. Next up, you build index.html so your own photo appears on a browser canvas.

Study Your Painting Target

Your painting effect needs a clear visual benchmark before any code can be judged. A saved reference gives you something concrete to match when the browser starts painting your own photo.

The Quick Study tool turns a photo into a brushstroke rendering within a few seconds. Watching broad strokes give way to fine detail reveals the motion your own page needs to reproduce.

In this step, get ready to:
  • Load your chosen photo into Quick Study.
  • Review the coarse-to-fine painting sequence.
  • Save a screenshot of the finished painting as your visual target.
Load your chosen photo

Quick Study accepts a local image from your computer. Use the photo you selected for this project so every observation connects to the effect you are about to build.

  • Return to the Quick Study reference tool in your browser.
  • Find your chosen photo in Finder (macOS) or File Explorer (Windows).
  • Drag the photo onto the painting area in Quick Study.
  • Keep the painting area visible while the rendering runs for a few seconds.

You should see coarse strokes establish the image first. Fine strokes add detail near the end.

Your reference run is working: your chosen photo has become a finished painting you can study.

Does the photo fail to load?

Try dragging the image directly onto the painting area again. Make sure you are using an image file stored on your computer.

If the page still keeps its original image, help me troubleshoot why my local photo is not loading in Quick Study.

Study the painting sequence

The reference gives you more than a finished style. Its painting order shows how a recognizable image emerges from layers of marks.

  • Inspect the largest colour regions in the finished painting.
  • Trace one strong contour with your eyes to see how nearby strokes turn with its shape.
  • Compare a smooth area of the photo with its painted version.
  • Compare the widest brush marks with the smallest finishing marks.
  • Check whether the main subject remains recognizable after the photographic detail becomes brushwork.

What should you notice?

  • The first strokes establish broad blocks of colour.
  • The later strokes sharpen smaller features.
  • The stroke direction follows visible contours through the image.
  • The finished painting keeps the original composition readable.

Why save a visual target?

An image effect can feel subjective when you judge it from memory. A screenshot preserves the exact result you are aiming toward.

Later, you can compare your browser version against the same composition. That comparison keeps each technical change tied to something visible.

Save your visual target

The finished frame becomes your project benchmark. Capture the complete painting at a size where its broad marks remain easy to see.

macOS

  • Press Shift-Command-4 to start a selected-area screenshot.
  • Drag the crosshair around the complete finished painting.
  • Release your mouse or trackpad button to capture the screenshot.
  • Wait for the screenshot to save to your desktop.
  • In Finder, drag the saved screenshot into the photo-to-painting folder.

Windows

  • Press Windows logo key+Shift+S to start a selected-area screenshot.
  • Drag the selection around the complete finished painting.
  • Select the screenshot notification to view the capture in Snipping Tool.
  • Click Save in Snipping Tool.
  • Choose the photo-to-painting folder as the destination.
  • Click Save in the save window.

Before you inspect the saved file, do you expect the complete painting to remain readable at a glance?

  • Return to the photo-to-painting folder in Finder or File Explorer.
  • Open the screenshot you just saved.

You should see the complete brushstroke painting at a readable size. The image should provide a clear visual target for the page you build next.

That's your benchmark secured: you now have a finished painting to compare against throughout the project.

Can't find the saved screenshot?

  • On macOS, check your desktop for the most recent screenshot file.
  • On Windows, repeat the capture if you closed the screenshot notification before saving the image.
  • Capture the painting again if any part of the finished image is cropped.

If the file is still missing, help me save my Quick Study screenshot inside the photo-to-painting folder.

Your visual target is saved. Next up, you will create the first working page in your photo-to-painting folder and see your own photo appear in the browser.

Display a Selected Photo

Your saved Quick Study painting gives you a clear destination. Your page still needs a way to turn the original photo into pixels it can process.

The canvas element is that pixel workspace. This step builds the smallest working path from a local image file to the original photo on screen.

In this step, get ready to:
  • Create index.html with a photo picker.
  • Add a canvas for displaying the selected photo.
  • Display the selected photo at a usable size.
Create the photo workspace

HTML defines the controls that appear in the browser. The file input collects one local image while the canvas provides a drawing surface.

  • Switch to the photo-to-painting folder from earlier in your code editor.
  • Create index.html inside the photo-to-painting folder using your editor's new-file control.
  • Build the page controls and canvas by pasting this code into index.html:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Photo to Painting</title>
</head>
<body>
  <main>
    <h1>Photo to Painting</h1>
    <!-- Let the browser provide a local image file. -->
    <label for="photoInput">Choose a photo</label>
    <input id="photoInput" type="file" accept="image/*">
    <!-- Reserve a visible workspace for the selected photo. -->
    <canvas id="canvas" width="800" height="600" style="display: block; max-width: 100%; margin-top: 1rem; border: 1px solid #ccc;"></canvas>
  </main>
</body>
</html>

What Does This Page Structure Do?

  • The accept="image/*" attribute guides the file picker toward image files.
  • The canvas starts as a visible drawing surface measuring 800 by 600 pixels.
  • The canvas dimensions change later to match the selected photo's proportions.
  • Save index.html.
  • Use your browser's file opener to select index.html from the photo-to-painting folder.

You should see the Photo to Painting heading above a Choose a photo control. A bordered blank canvas should sit underneath it.

Page Missing Its Controls?

  • Confirm that the file is named index.html inside the photo-to-painting folder.
  • Check that the browser opened the saved file from that folder.
  • Ask for help with the page structure.
Load the selected image

JavaScript connects the file input to the canvas. An object URL lets the browser load the local file without uploading it anywhere.

The longest side is capped at 800 pixels. This preserves the photo's proportions while keeping later pixel processing manageable.

  • In index.html, locate the closing </body> tag.
  • Add the image-loading logic immediately above </body> by pasting this code:
<script>
  // Keep references to the controls and cap later pixel work.
  const photoInput = document.getElementById('photoInput');
  const canvas = document.getElementById('canvas');
  const context = canvas.getContext('2d');
  const MAX_SIZE = 800;

  photoInput.addEventListener('change', () => {
    const file = photoInput.files[0];
    if (!file) return;

    // Create a temporary browser URL for the selected file.
    const imageUrl = URL.createObjectURL(file);
    const image = new Image();

    image.onload = () => {
      // Preserve the aspect ratio while limiting the longest side.
      const scale = Math.min(1, MAX_SIZE / Math.max(image.naturalWidth, image.naturalHeight));
      canvas.width = Math.round(image.naturalWidth * scale);
      canvas.height = Math.round(image.naturalHeight * scale);

      // Draw the original photo into the pixel workspace.
      context.drawImage(image, 0, 0, canvas.width, canvas.height);
      URL.revokeObjectURL(imageUrl);
    };

    image.src = imageUrl;
  });
</script>

What Does This Image Loader Do?

  • The photoInput.files[0] expression reads the first image selected in the file picker.
  • The URL.createObjectURL(file) call creates a temporary address that the browser can load.
  • The scale calculation caps the longest side at 800 pixels without enlarging smaller photos.
  • The context.drawImage() call paints the loaded image across the resized canvas.
  • The URL.revokeObjectURL(imageUrl) call releases the temporary address after drawing finishes.
  • Save index.html.
  • Return to the browser from earlier.
  • Refresh the page.

Before you choose the photo, do you expect a portrait image to become square or keep its original shape?

  • Click Choose a photo.
  • Select the image file that you used in Quick Study.

You should see the original photo fill the canvas without stretching. Its longest side stays at or below 800 pixels.

That first browser pipeline is working. A local photo now becomes canvas pixels that the next filter can inspect.

Photo Missing From the Canvas?

  • Save index.html if your latest changes are still unsaved.
  • Refresh the browser after saving the file.
  • Confirm that photoInput matches the input's id value.
  • Confirm that canvas matches the canvas element's id value.
  • Ask for help debugging the photo loader.

The complete file is available below for a final comparison.

✔️ Awesome, I've got everything!

Your index.html file now contains the photo picker, canvas, and image-loading logic.

ⓧ I'd like to double check the full code

  • Compare your saved index.html with this complete version.
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Photo to Painting</title>
</head>
<body>
  <main>
    <h1>Photo to Painting</h1>
    <!-- Let the browser provide a local image file. -->
    <label for="photoInput">Choose a photo</label>
    <input id="photoInput" type="file" accept="image/*">
    <!-- Reserve a visible workspace for the selected photo. -->
    <canvas id="canvas" width="800" height="600" style="display: block; max-width: 100%; margin-top: 1rem; border: 1px solid #ccc;"></canvas>
  </main>
  <script>
    // Keep references to the controls and cap later pixel work.
    const photoInput = document.getElementById('photoInput');
    const canvas = document.getElementById('canvas');
    const context = canvas.getContext('2d');
    const MAX_SIZE = 800;

    photoInput.addEventListener('change', () => {
      const file = photoInput.files[0];
      if (!file) return;

      // Create a temporary browser URL for the selected file.
      const imageUrl = URL.createObjectURL(file);
      const image = new Image();

      image.onload = () => {
        // Preserve the aspect ratio while limiting the longest side.
        const scale = Math.min(1, MAX_SIZE / Math.max(image.naturalWidth, image.naturalHeight));
        canvas.width = Math.round(image.naturalWidth * scale);
        canvas.height = Math.round(image.naturalHeight * scale);

        // Draw the original photo into the pixel workspace.
        context.drawImage(image, 0, 0, canvas.width, canvas.height);
        URL.revokeObjectURL(imageUrl);
      };

      image.src = imageUrl;
    });
  </script>
</body>
</html>

Your browser can now turn a chosen file into a canvas image. Next up, you'll convert those pixels to grayscale and reveal their edges.

Detect Photo Edges

Your page already loads a chosen photo through the Canvas 2D API. The browser can now inspect the pixels behind that image.

Brushstrokes need a map of the photo's boundaries. Sobel edge detection turns changes in brightness into edge strength plus edge direction.

In this step, get ready to:
  • Convert the photo's colour pixels into grayscale brightness values.
  • Calculate horizontal plus vertical gradients for every interior pixel.
  • Render edge strength as white outlines on black.
Build the edge detector

Colour differences can distract an edge detector from changes in brightness. A grayscale buffer gives the Sobel operator one consistent signal to compare.

How does Sobel edge detection work?

A canvas pixel occupies four consecutive channel slots. Average its three colour channels to create one grayscale brightness value.

The detector moves a three-by-three window across every pixel inside the outer border. This keeps each neighbourhood within the image.

  • The horizontal kernel has a top row of minus one, zero, plus one. Its middle row is minus two, zero, plus two. Its bottom row repeats the top row.
  • The vertical kernel has a top row of minus one, minus two, minus one. Its middle row contains three zeroes. Its bottom row is plus one, plus two, plus one.

Combine the horizontal plus vertical results with the square root of their squared sum. Store this magnitude as edge strength. Copy a clamped strength into all three colour channels to render white edges on black.

Use a two-argument arctangent with the vertical result followed by the horizontal result. Store the returned angle as edge direction.

  • Switch back to index.html in the code editor from earlier.
  • Locate the existing image-load path immediately after it draws the selected photo onto the canvas.
  • Add the complete grayscale plus Sobel processing stage described above at that location.
  • Save index.html.
Test the edge map

Before you test, predict which parts of your photo will produce the brightest lines.

  • Return to the local page already open in your browser.
  • Refresh the page.
  • Choose the same image with the photo file input.

You'll see a black canvas with white outlines tracing the strongest boundaries in your photo. Areas with little brightness change stay dark.

Don't see clear white outlines?

  • Place the processing stage after the existing photo draw if the original image remains visible.
  • Confirm that every source pixel contributes a grayscale value if the canvas is fully black.
  • Map each three-by-three neighbourhood back to its centre pixel if the outlines look shifted.
  • Leave the outer one-pixel border black if edge calculations read beyond the image.

If the result still looks wrong, ask for help debugging the grayscale buffer and Sobel calculations.

You have turned a photo into a contour map that the painting stage can follow. The stored direction values give each future brushstroke an angle to use.

Your edge detector now stores boundary strength plus direction. Next up, you will use both values to build the painting with animated brushstrokes.

Animate the Brushstrokes

Your Sobel operator edge map now reveals the contours that shape the photo. The canvas element currently stops at that analysis.

Colour makes each mark belong to the photo. Edge direction makes each mark follow a contour.

A coarse-to-fine sequence makes the image emerge in layers. This step turns those layers into a visible animation.

In this step, get ready to:
  • Sample original photo colours at random canvas positions.
  • Rotate brushstrokes along the local edge directions.
  • Animate a coarse pass before a fine pass with requestAnimationFrame.
Sample the source colours

Each random point maps to one pixel in the original photo. That pixel supplies the stroke's RGBA values.

  • Select index.html from the project sidebar in your code editor.
  • Locate the edge-map rendering logic that runs after the Sobel calculation.
  • Replace that rendering logic with a source-colour stroke routine for random canvas points.
  • Save index.html.
  • Return to the page from earlier in your browser.
  • Choose the same photo with the page's photo picker.

Good progress. The canvas is now sampling real photo colours.

You'll see those colours broken into scattered marks. The stroke directions remain mixed.

Still Seeing the Edge Map?

  • Confirm that the edge-map renderer now starts the source-colour stroke routine.
  • Keep every sampled point inside the canvas dimensions.
  • Read each stroke colour from the original photo pixels.

Help me debug why my canvas still shows the edge map or stays blank.

Follow the edge directions

The Sobel calculation already stores an edge direction for each image pixel. Using that direction rotates each brushstroke along the nearest contour.

  • Switch back to index.html in your code editor.
  • Update every stroke so its rotation comes from the edge direction at the same sampled point.
  • Save index.html.
  • Return to the page in your browser.
  • Choose the same photo again.

You'll see more strokes follow the curves within the photo. Strong contours should now guide nearby marks in a shared direction.

The browser uses requestAnimationFrame to schedule the next stroke batch for a future screen update. You can watch the image grow between those updates.

  • Switch back to index.html in your code editor.
  • Animate the coarse pass as batches of larger strokes scheduled through requestAnimationFrame.
  • Save index.html.
  • Return to the page in your browser.
  • Choose the same photo again.

You'll see broad contour-following strokes appear across multiple animation frames. The main shapes of the photo gradually become recognisable.

Do the Strokes Appear All at Once?

  • Schedule the next batch from inside the current animation callback.
  • Limit each callback to one batch of coarse strokes.
  • Use the edge direction from the same pixel that supplied each stroke colour.

Help me debug why my contour-following strokes are not animating across multiple frames.

Run coarse and fine passes

The coarse pass establishes the large shapes. The fine pass adds smaller marks over those shapes.

  • Switch back to index.html in your code editor.
  • Add a fine pass of smaller strokes that begins after the coarse pass completes.
  • Save index.html.

Before you choose the photo again, do you expect the painting to appear all at once?

  • Return to the page in your browser.
  • Choose the same photo with the page's photo picker.

Broad strokes appear first. Smaller contour-following strokes then sharpen the details.

You've got the painting pipeline working. Your canvas now rebuilds the photo through animated brushstrokes.

  • Compare the finished canvas with your saved Quick Study target screenshot.

You should see a similar broad-to-fine progression. The simulated strokes should follow recognisable contours from the original photo.

Does the Fine Pass Start Too Early?

  • Start the fine pass only after the coarse pass reports completion.
  • Track the progress of each pass separately.
  • Keep the fine stroke size smaller than the coarse stroke size.

Help me fix the handoff between my coarse brushstroke pass and fine brushstroke pass.

✔️ Awesome, I've got everything!

Your saved index.html now paints source-photo colours along local edge directions through coarse and fine animation passes.

ⓧ I'd like to double check the full code

Your finished index.html has one continuous image-processing and painting pipeline.

  • Confirm that the photo file input still loads the selected image onto the canvas.
  • Confirm that the original image pixels remain available before grayscale conversion.
  • Confirm that the Sobel calculation still produces edge strength plus edge direction values.
  • Confirm that the painting stage samples source-photo colours at random canvas positions.
  • Confirm that each simulated stroke follows the edge direction at its sampled position.
  • Confirm that requestAnimationFrame runs the coarse pass before the fine pass.

Your animated painter now follows the photo's colours and contours. Next up, you'll control the brush size and download the finished canvas as a PNG.

Add Style and Download Controls

Your brushstroke animation now follows the photo's contours. Its coarse pass builds the shape before its fine pass adds detail.

A fixed brush style limits what each photo can become. This step adds a slider to your Canvas API painting. It also adds a PNG export for the finished result.

In this step, get ready to:
  • Add a brush size slider to the painting controls.
  • Use the slider to change the width of future brushstrokes.
  • Download the finished canvas as a PNG file.
Add the painting controls

A range input gives the painting a bounded style control. The download button gives the finished image a clear exit from the browser.

  • In your code editor, switch to index.html.
  • Place the new controls immediately before the existing canvas element by pasting this code:
<!-- Add controls for stroke scale and image export -->
<div class="controls">
  <label for="brushSize">Brush size <output id="brushSizeValue">1.0×</output></label>
  <input id="brushSize" type="range" min="0.5" max="2" step="0.1" value="1">
  <button id="downloadButton" type="button">Download PNG</button>
</div>

What does this code do?

  • The type="range" input creates a slider between half size and double size.
  • The output element shows the scale selected for the next painting.
  • The Download PNG button provides the control that the export code uses later.
  • Save index.html.
  • Refresh the project page in your browser.

You should see the brush size slider above the canvas. You should also see the Download PNG button beside it.

Don't see the new controls?

Confirm that the new markup sits inside the page's body element. Markup pasted inside the existing script stays hidden or causes a syntax error.

Check that the closing </div> appears before the existing canvas element.

Help me fix controls that do not appear in my HTML painting app.

Connect the slider to future strokes

The coarse pass already uses wider lines than the fine pass. A scale keeps that relationship intact while making both passes heavier or lighter.

  • Inside the existing script in index.html, place this code before the photo file input's event listener:
const brushSizeInput = document.getElementById("brushSize");
const brushSizeValue = document.getElementById("brushSizeValue");
const originalStroke = CanvasRenderingContext2D.prototype.stroke;

// Scale future strokes while preserving each pass's original proportions.
CanvasRenderingContext2D.prototype.stroke = function (...args) {
  const originalWidth = this.lineWidth;
  this.lineWidth = originalWidth * Number(brushSizeInput.value);
  originalStroke.apply(this, args);
  this.lineWidth = originalWidth;
};

// Show the scale that the next painting will use.
brushSizeInput.addEventListener("input", () => {
  brushSizeValue.textContent = `${Number(brushSizeInput.value).toFixed(1)}×`;
});

How does the slider change the painting?

  • The code keeps a reference to the browser's original stroke() method.
  • The replacement method multiplies each future line width by the slider value.
  • The method restores the original width after drawing each stroke. This preserves the difference between the coarse pass and the fine pass.
  • The input listener updates the displayed scale while you drag the slider.
  • Save index.html.
  • Refresh the project page.
  • Drag the brush size slider to its far-right position.

Before you select the photo again, do you expect its new strokes to look narrower or wider?

  • Select the same photo again with the existing photo picker.

You should watch a new painting build with wider marks. Its main shapes should look bolder than the painting you made with the default setting.

  • Drag the brush size slider to its far-left position.
  • Select the same photo again with the existing photo picker.

You should now see narrower marks reveal more small details. Compare both versions with your saved Quick Study target screenshot to choose your preferred style.

Does the slider move without changing the strokes?

Check that the slider code runs before you select a photo. The scale only applies to strokes drawn after the code has loaded.

Confirm that brushSize matches the slider's id. Confirm that brushSizeValue matches the output element.

Help me debug a brush size slider that updates its value but does not change my canvas strokes.

Export the finished painting

The canvas stores the final painting as pixels inside the page. The export control converts those pixels into an image that the browser can save.

  • Near the bottom of the existing script in index.html, add the download behavior by pasting this code:
const downloadButton = document.getElementById("downloadButton");

// Export the current canvas pixels as a PNG download.
downloadButton.addEventListener("click", () => {
  const link = document.createElement("a");
  link.download = "painting.png";
  link.href = document.querySelector("canvas").toDataURL("image/png");
  link.click();
});

How does the download work?

  • The toDataURL("image/png") call converts the current canvas pixels into PNG data.
  • The temporary link proposes painting.png as the saved filename.
  • The click() call starts the browser's download workflow without sending the image to a server.
  • Save index.html.
  • Refresh the project page.
  • Select a photo with the existing photo picker.
  • Wait for the fine painting pass to finish.

Before you click the new button, do you expect the downloaded file to contain the source photo or the finished painted canvas?

  • Click Download PNG.
  • Check your browser's Downloads list.
  • Open painting.png from the browser's Downloads location.

You should see the completed brushstroke painting inside the downloaded image. The exported file should match the canvas at the moment you clicked the button.

Didn't get the PNG file?

Confirm that the painting completed before you clicked the button. An empty canvas produces an empty-looking export.

Check whether your browser is waiting for permission to save the download. Your download settings may also send the file to a location other than the usual Downloads folder.

Help me debug a canvas PNG download that does not save the current painting.

That's a big upgrade. Your canvas now responds to your preferred brush style and saves the result as an image. Next up, you will publish the page so it can paint photos from a public URL.

Publish Your Painting Site

Your completed painting app now turns photos into downloadable artwork on your desktop. A local file keeps that result tied to one device.

Publishing gives the app a public URL that your phone can open. A GitHub repository stores the page before GitHub Pages serves it.

In this step, get ready to:
  • Store your completed index.html in a public repository.
  • Publish the main branch through GitHub Pages.
  • Verify the public site plus its PNG download on your phone.
Create the GitHub repository

A repository keeps the finished page online as a tracked project. Its main branch becomes the source that GitHub Pages publishes.

  • Return to your GitHub account in the browser.
  • Select the create menu in the upper-right corner.
  • Select New repository.
  • Enter photo-to-painting in the Repository name field.

The repository form now shows photo-to-painting as the project name.

  • Select Public under Choose visibility.
  • Toggle Add README to On.

The form now shows a public repository with a starting file. That starting file creates the main branch needed for publishing.

  • Select Create repository.

Good progress. The photo-to-painting repository now exists on the main branch.

  • Select Add file above the repository file list.
  • Select Upload files.
  • Select choose your files.
  • Choose the existing index.html from your local photo-to-painting project.

You should see index.html in the upload queue.

  • Enter Publish painting app in the Commit message field.
  • Select Commit directly to the main branch.
  • Select Commit changes.

You should see index.html at the top level of the main branch. Your complete painting application is now stored in the repository.

Don't see index.html in the repository?

  • Confirm that index.html appears beside README.md in the repository file list.
  • Confirm that the branch selector above the file list shows main.
  • Repeat the upload if the file remains in the upload queue.

Help me fix an index.html upload that is missing from my GitHub repository.

Publish from the main branch

A GitHub Pages publishing source tells GitHub which branch contains the website. You will use the repository root because index.html already sits at its top level.

Why GitHub Pages for this app?

This app is one complete index.html file. GitHub Pages can serve that file directly from the repository.

A server host would add infrastructure the app does not use.

  • Select Settings under the repository name.
  • Select Pages in the sidebar.

The Build and deployment controls now appear for the repository.

  • Select Deploy from a branch from the Source menu.

The branch controls now appear below the publishing source.

  • Select main from the branch menu.
  • Select / (root) from the folder menu.

The publishing source now points to the main branch at / (root).

  • Select Save.

GitHub Pages starts a deployment from the saved publishing source.

This publish can take up to 10 minutes, so a short wait is expected while GitHub Pages builds the site.

  • Wait for the Visit site control to appear in the GitHub Pages section.
  • Select Visit site.
  • Record the public site URL here: https://<your-github-username>.github.io/photo-to-painting/.

At the public URL, you should see the photo file input above the canvas. The brush size slider plus the Download PNG button should also be visible.

Still waiting for the public site?

  • Confirm that Source shows Deploy from a branch.
  • Confirm that the branch menu shows main.
  • Confirm that the folder menu shows / (root).
  • Refresh the Pages settings after the publishing wait.

Help me troubleshoot why my GitHub Pages site is not publishing.

Test the public site on your phone

The phone test proves that your project works beyond the computer where you built it. The public page should run the same painting pipeline inside a mobile browser.

  • Enter https://<your-github-username>.github.io/photo-to-painting/ in your phone browser's address bar.

Your phone should show the photo file input plus the brush size slider. The canvas and Download PNG button should also be visible.

Before you choose a photo, do you think the same painting pipeline will complete in a mobile browser?

Your phone may request access to your photo library when you use the file input.

  • Tap the page's photo file input.
  • Allow access to the photo you want to test if prompted.
  • Select a photo from your camera roll.

You should watch a coarse brushstroke pass build across the canvas. A fine pass follows the contours with smaller strokes.

  • Tap Download PNG after the painting finishes.

Your phone should confirm the PNG download through its usual download controls. This proves the full public workflow works on mobile.

  • Keep the completed painting PNG from your browser download workflow.
  • Keep the Quick Study target screenshot from your reference test.

Strong finish. Your photo-to-painting app now runs from a public URL on desktop or phone.

Your public URL is ready to share. Your painting PNG plus target screenshot remain saved for your portfolio.

Secret mission

Run a Mobile Painting Quality Audit

Test the published painter with three very different photos. Compare how orientation and contrast affect the guided brushstrokes. Download the final retest as evidence of your quality audit.

Clean Up Your Resources

Clean Up Your Resources

Choose whether to keep your work available, pause for now, or remove it completely. This project has no paid resources to shut down.

Resources you used:

  • Your GitHub repository named photo-to-painting with its published GitHub Pages site.
  • Your local photo-to-painting project containing index.html.
  • Your downloaded painting PNG.
  • Your Quick Study target screenshot.

Keep everything running

No action needed. Choose this if you want the painting page to remain available through its public URL.

  • Keep photo-to-painting on GitHub so the published page stays available.
  • Keep the local project so you can edit index.html later.
  • Keep the completed painting PNG as a finished export.
  • Keep the Quick Study target screenshot for visual comparison.

Pause - I'll come back to this later

There is no running server process to stop. Your files can stay in place without using memory or creating project costs.

  • Close the browser tab displaying the published painting page.
  • Close the index.html tab in your code editor.
  • Leave the photo-to-painting repository unchanged so the public page stays available.
  • Return later through the public URL from earlier.

Delete - I don't want to use this again

Remove the published page plus every local project artifact. Download any version you still value before continuing.

Deletion Is Permanent

Deleting these resources is a complete reset. Save any file you want to keep before continuing.

Deleting the GitHub repository also removes the public Pages site attached to it.

Remove the published site:

  • Return to the photo-to-painting repository on GitHub.
  • Open the repository settings page.
  • Select the general repository settings area.
  • Scroll to the repository deletion controls.
  • Choose the permanent repository deletion action.
  • Complete GitHub's on-screen confirmation for photo-to-painting.
  • Check the removal by loading the public URL from earlier.

You should no longer see the photo-to-painting application at the public URL.

Remove the local files:

  • Use Finder on macOS or File Explorer on Windows to locate the local project containing index.html.
  • Move the local project folder to the Trash on macOS or the Recycle Bin on Windows.
  • Locate the retained painting PNG plus the Quick Study target screenshot.
  • Move both image files to the Trash on macOS or the Recycle Bin on Windows.
  • Empty the Trash on macOS or the Recycle Bin on Windows after checking its contents.

The local project plus both retained images should no longer appear in their previous locations.

Nice Work!

Nice Work!

You did it! You built and published a browser app that transforms uploaded photos into animated brushstroke paintings. Your saved artwork shows the complete journey from source photo to finished painting.

You've learned how to:

  • Build a grayscale photo pipeline with the Canvas API in plain JavaScript.
  • Turn Sobel edge detection into animated coarse-to-fine brushstrokes that follow each photo's contours.
  • Publish a phone-ready site with GitHub Pages that exports each completed painting as a PNG.
  • Secret Mission: Complete an optional challenge to push your browser image-processing skills further.

Ready to quiz yourself?