Vector Image Service with Three.js and Vite
Add to your RSS feed11 February 202513 min read
Table of Contents
Image vectorization is the process of converting raster images into vector formats like SVG. This is useful for improving scalability without losing quality. In this article, we will explore how to build an image vectorization service using Three.js, a powerful JavaScript library for working with 3D graphics in the browser.
Demo: image-vectorizer.vercel.app
Code: github.com
Why Three.js?
Three.js makes it easy to work with WebGL, enabling complex visualizations and image manipulations. We will use it to process images and transform them into vector-based shapes.
Advantages of Using Three.js for Vectorization:
✅ WebGL support for fast rendering
✅ Flexible image processing capabilities
✅ Ability to use shaders and post-processing
1. Setting Up the Project
First, initialize a Vite project:
Then, install Three.js:
Adding Tailwind CSS to a Vite Project
To integrate Tailwind CSS into your Vite + Three.js image vectorization project, follow these steps:
1. Install Tailwind CSS
Run the following command in your project directory:
2. Configure Vite
Open/create vite.config.ts
and add the @tailwindcss/vite
plugin:
1 import { defineConfig } from 'vite'2 import tailwindcss from '@tailwindcss/vite'3 export default defineConfig({4 plugins: [5 tailwindcss(),6 ],7 })
3. Add Tailwind to CSS
Replace style.css
with a new file src/index.css
and add Tailwind's base styles:
1 @import 'tailwindcss';23 :root {4 font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;5 line-height: 1.5;6 font-weight: 400;78 color-scheme: light dark;9 color: rgba(255, 255, 255, 0.87);10 background-color: #242424;1112 font-synthesis: none;13 text-rendering: optimizeLegibility;14 -webkit-font-smoothing: antialiased;15 -moz-osx-font-smoothing: grayscale;16 }1718 body {19 margin: 0;20 display: flex;21 flex-direction: column;22 place-items: center;23 min-width: 320px;24 min-height: 100vh;25 }2627 h1 {28 font-size: 3.2em;29 line-height: 1.1;30 }3132 #app {33 margin: 0 auto;34 text-align: center;35 }3637 @media (prefers-color-scheme: light) {38 :root {39 color: #213547;40 background-color: #ffffff;41 }42 }
It contains all the styles for the entire project.
2. Project Structure
Your project folder should look like this:
1 image-vectorizer/2 │── public/3 │── src/4 │── index.html5 │── package.json6 │── tsconfig.json
3. HTML Structure
Modify index.html
to include:
1 <!DOCTYPE html>2 <html lang="en">3 <head>4 <meta charset="UTF-8" />5 <meta6 name="description"7 content="mage Vectirized is an open-source Three.js tool that converts images into scalable 3D vector graphics. Explore real-time vectorization and the power of WebGL for interactive design." />8 <meta9 name="viewport"10 content="width=device-width, initial-scale=1.0" />11 <link12 href="/src/style.css"13 rel="stylesheet" />14 <title>Image Vectirized - Open-Source ThreeJS Tool</title>15 </head>16 <body class="overflow-x-hidden">17 <div id="app"></div>18 <script19 type="module"20 src="/src/main.ts"></script>21 </body>22 </html>
4. Create UI Components
Button Component
Create src/ui/button.ts
file:
1 type BtnVariant = 'primary' | 'secondary' | 'upload';23 export function setupButton(element: HTMLButtonElement, text: string, href: string, variant: BtnVariant = 'primary') {4 element.innerHTML = variant === 'primary' ? `5 <a6 class="inline-block rounded-sm px-8 py-3 text-sm font-medium text-white bg-gray-800 transition hover:scale-105 hover:shadow-xl focus:ring-3 focus:outline-hidden cursor-pointer"7 href=${href}8 >9 ${text}10 </a>11 ` : variant === 'secondary' ?12 `13 <a14 class="inline-block rounded-sm bg-gray-500 px-8 py-3 text-sm font-medium text-white transition hover:scale-105 hover:shadow-xl focus:ring-3 focus:outline-hidden cursor-pointer"15 href=${href}16 >17 ${text}18 </a>19 ` :20 `21 <input type="file" id="fileInput" value="Upload Image" style="display: none"22 accept="image/*" />23 <label for="fileInput" class="inline-block rounded-sm px-8 py-3 text-sm font-medium text-gray-800 bg-gray-100 transition hover:scale-105 hover:shadow-xl focus:ring-3 focus:outline-hidden cursor-pointer"> Upload Image </label>24 `;25 }
- The function sets the
innerHTML
ofa
button based on the provided variant. - If
'primary'
or'secondary'
, it creates an<a>
button linking tohref
. - If
'upload'
, it creates a file upload input with a styled label. - Uses Tailwind CSS classes for styling and hover effects.
This approach enhances reusability and maintains clean, structured code for different button types. 🚀
Header component:
Create a header.ts
file inside the src
folder:
1 export function setupHeader(header: HTMLElement) {2 header.className = 'bg-white';3 header.innerHTML = `4 <div class="mx-auto px-4 sm:px-6 lg:px-8 w-screen">5 <div class="flex h-16 items-center justify-between">6 <div class="flex-1 md:flex md:items-center md:gap-12">7 <a class="group flex items-center gap-4" href="/"><span class="sr-only">Home</span>8 <svg class="group-hover:stroke-gray-700 stroke-black" width="30px" height="30px" viewBox="0 0 48 48" id="a" xmlns="http://www.w3.org/2000/svg"><defs><style>.b{fill:none;stroke-linecap:round;stroke-linejoin:round;}</style></defs><path class="b" d="M29.4995,12.3739c.7719-.0965,1.5437,.4824,1.5437,1.2543h0l2.5085,23.8312c.0965,.7719-.4824,1.5437-1.2543,1.5437l-23.7347,2.5085c-.7719,.0965-1.5437-.4824-1.5437-1.2543h0l-2.5085-23.7347c-.0965-.7719,.4824-1.5437,1.2543-1.5437l23.7347-2.605Z"/><path class="b" d="M12.9045,18.9347c-1.7367,.193-3.0874,1.7367-2.8945,3.5699,.193,1.7367,1.7367,3.0874,3.5699,2.8945,1.7367-.193,3.0874-1.7367,2.8945-3.5699s-1.8332-3.0874-3.5699-2.8945h0Zm8.7799,5.596l-4.6312,5.6925c-.193,.193-.4824,.2894-.6754,.0965h0l-1.0613-.8683c-.193-.193-.5789-.0965-.6754,.0965l-5.0171,6.1749c-.193,.193-.193,.5789,.0965,.6754-.0965,.0965,.0965,.0965,.193,.0965l19.9719-2.1226c.2894,0,.4824-.2894,.4824-.5789,0-.0965-.0965-.193-.0965-.2894l-7.8151-9.0694c-.2894-.0965-.5789-.0965-.7719,.0965h0Z"/><path class="b" d="M16.2814,13.8211l.6754-6.0784c.0965-.7719,.7719-1.3508,1.5437-1.2543l23.7347,2.5085c.7719,.0965,1.3508,.7719,1.2543,1.5437h0l-2.5085,23.7347c0,.6754-.7719,1.2543-1.5437,1.2543l-6.1749-.6754"/><path class="b" d="M32.7799,29.9337l5.3065,.5789c.2894,0,.4824-.193,.5789-.4824,0-.0965,0-.193-.0965-.2894l-5.789-10.5166c-.0965-.193-.4824-.2894-.6754-.193h0l-.3859,.3859"/></svg>9 <span class="text-black group-hover:text-gray-700">Image Vectorizer</span>10 </a>11 </div>1213 <div class="flex items-center gap-4">14 <button id="github__btn"></button>15 </div>16 </div>17 </div>`;18 }
Hero component:
1 export function setupHero(element: HTMLElement) {2 element.className = 'mt-10';3 element.innerHTML = `4 <div class="mx-auto max-w-screen-xl px-4 py-8 sm:px-6 lg:px-8">5 <div class="flex flex-col md:flex-row items-center gap-4 md:gap-8 justify-center">6 <div class="max-w-lg md:max-w-none">7 <h1 class="text-3xl font-semibold text-gray-900 sm:text-4xl">8 3D Image Vectorizer9 </h1>1011 <p class="mt-4 text-gray-700 max-w-2xl">12 3D Image Vectorizer converts raster images into high-quality 3D vector graphics. It enhances scalability, precision, and editability for 3D modeling, printing, and animation.13 </p>14 </div>15 <img16 src="vectorized-image.png"17 class="rounded w-80 sm:w-96 max-w-96 mt-10"18 alt="Image Vectorizer Example"19 />20 </div>21 </div>22 `;23 }
Canvas Component:
canvas.ts
- the most complicated component in our app:
1 import * as THREE from 'three';2 import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';34 export function setupCanvas(element: HTMLDivElement): void {5 const canvas = document.querySelector('canvas') as HTMLCanvasElement;6 const width = window.innerWidth / 1.5;7 const height = window.innerHeight / 1.5;89 const renderer = new THREE.WebGLRenderer({10 canvas,11 antialias: true,12 preserveDrawingBuffer: true,13 });14 renderer.setPixelRatio(window.devicePixelRatio > 1 ? 2 : 1);15 renderer.setSize(width, height);16 renderer.setClearColor(0x000000);1718 const scene = new THREE.Scene();19 const camera = new THREE.PerspectiveCamera(40, width / height, 0.1, 1000);20 camera.position.set(0, 0, 300);2122 const group = new THREE.Group();23 scene.add(group);2425 const downloadBtn = document.getElementById('download-btn') as HTMLButtonElement;26 const fileInput = document.getElementById('fileInput') as HTMLInputElement | null;2728 if (fileInput) {29 fileInput.addEventListener('change', (event: Event) => {30 const uploadEvent = event.target as HTMLInputElement;31 if (!uploadEvent?.files?.length) return;3233 const file = uploadEvent.files[0];34 const reader = new FileReader();3536 reader.onload = (e) => {37 const img = new Image();38 img.onload = () => {39 const canvas2d = document.createElement('canvas');40 const ctx = canvas2d.getContext('2d');41 if (!ctx) return;4243 const size = 200;44 canvas2d.width = size;45 canvas2d.height = size;46 ctx.drawImage(img, 0, 0, size, size);4748 const data = ctx.getImageData(0, 0, size, size).data;49 group.clear();5051 for (let i = 0; i < size; ++i) {52 const geometry = new THREE.BufferGeometry();53 const vertices = new Float32Array(size * 3);54 const colors = new Float32Array(size * 3);5556 for (let j = 0; j < size; ++j) {57 const colorIndex = (j * size + i) * 4;58 const r = data[colorIndex] / 255;59 const g = data[colorIndex + 1] / 255;60 const b = data[colorIndex + 2] / 255;6162 vertices.set([j - 100, i - 100, data[colorIndex] / 10], j * 3);63 colors.set([r, g, b], j * 3);64 }6566 geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));67 geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));6869 const material = new THREE.LineBasicMaterial({ vertexColors: true });70 const line = new THREE.Line(geometry, material);71 group.add(line);72 }7374 element.classList.remove('hidden');75 downloadBtn.classList.remove('hidden');76 downloadBtn.disabled = false;77 };78 if (e.target?.result) img.src = e.target.result as string;79 };80 reader.readAsDataURL(file);81 });82 }8384 group.rotateZ(-Math.PI / 2);85 const controls = new OrbitControls(camera, renderer.domElement);8687 function animate(): void {88 requestAnimationFrame(animate);89 controls.update();90 renderer.render(scene, camera);91 }92 animate();9394 downloadBtn.addEventListener('click', () => {95 renderer.render(scene, camera);96 const link = document.createElement('a');97 link.href = canvas.toDataURL('image/png');98 link.download = 'vectorized-image.png';99 link.click();100 });101 }
Explanation of the Code
This TypeScript code defines a function setupCanvas, which sets up a Three.js scene, processes an uploaded image, vectorizes it, and allows users to download the vectorized result as a PNG image.
1 export function setupCanvas(element: HTMLDivElement): void { }
This function initializes the Three.js scene within the provided HTMLDivElement.
Creating the WebGL Renderer
1 const canvas = document.querySelector('canvas') as HTMLCanvasElement;2 const width = window.innerWidth / 1.5;3 const height = window.innerHeight / 1.5;45 const renderer = new THREE.WebGLRenderer({6 canvas,7 antialias: true,8 preserveDrawingBuffer: true,9 });
- Finds an existing
<canvas>
element in the DOM. - Sets canvas dimensions to ⅔ of the window size.
Creates a WebGL renderer with:
antialias: true
for smooth rendering.preserveDrawingBuffer: true
to enable downloading images.
1 renderer.setPixelRatio(window.devicePixelRatio > 1 ? 2 : 1);2 renderer.setSize(width, height);3 renderer.setClearColor(0x000000);
- Adjusts pixel ratio for high-resolution screens.
- Sets canvas size.
- Uses a black background (
0x000000
).
Setting Up Scene, Camera, and Group
1 const scene = new THREE.Scene();2 const camera = new THREE.PerspectiveCamera(40, width / height, 0.1, 1000);3 camera.position.set(0, 0, 300);45 const group = new THREE.Group();6 scene.add(group);
- Creates a Three.js scene.
- Defines a Perspective Camera (FOV: 40°, aspect ratio based on canvas size).
- Moves the camera back (z = 300) to view the scene.
- Creates an empty group (
group
) to hold objects and adds it to the scene.
File Upload Handling
1 const fileInput = document.getElementById('fileInput') as HTMLInputElement | null;
- Finds the file input (
<input type="file">
) for image uploads.
1 if (fileInput) {2 fileInput.addEventListener('change', (event: Event) => {3 const uploadEvent = event.target as HTMLInputElement;4 if (!uploadEvent?.files?.length) return;56 const file = uploadEvent.files[0];7 const reader = new FileReader();
- Listens for file selection.
- Reads the uploaded file using
FileReader
.
Processing Image into Vector Format
1 reader.onload = (e) => {2 const img = new Image();3 img.onload = () => {4 const canvas2d = document.createElement('canvas');5 const ctx = canvas2d.getContext('2d');6 if (!ctx) return;
- Creates a temporary 2D
<canvas>
to process the image. - Gets the 2D rendering context (
ctx
).
1 const size = 200;2 canvas2d.width = size;3 canvas2d.height = size;4 ctx.drawImage(img, 0, 0, size, size);
- Resizes the image to 200×200 pixels for performance.
1 const data = ctx.getImageData(0, 0, size, size).data;2 group.clear();
- Extracts pixel data from the canvas.
- Clears the 3D scene before drawing new content.
Generating the 3D Vectorized Image
1 for (let i = 0; i < size; ++i) {2 const geometry = new THREE.BufferGeometry();3 const vertices = new Float32Array(size * 3);4 const colors = new Float32Array(size * 3);
- Loops over each row (
i
) of the image. - Creates geometry (
BufferGeometry
) for each row of pixels.
1 for (let j = 0; j < size; ++j) {2 const colorIndex = (j * size + i) * 4;3 const r = data[colorIndex] / 255;4 const g = data[colorIndex + 1] / 255;5 const b = data[colorIndex + 2] / 255;67 vertices.set([j - 100, i - 100, data[colorIndex] / 10], j * 3);8 colors.set([r, g, b], j * 3);9 }
- Reads RGB values from the pixel data.
- Converts pixel positions into 3D coordinates (
x, y, z
).
1 geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));2 geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));3 const material = new THREE.LineBasicMaterial({ vertexColors: true });4 const line = new THREE.Line(geometry, material);5 group.add(line);
- Creates a 3D line from the pixel data.
- Sets vertex positions and colors.
- Adds the line to the scene.
Making the Canvas and Download Button Visible
1 element.classList.remove('hidden');2 downloadBtn.classList.remove('hidden');3 downloadBtn.disabled = false;
- Unhides the canvas and download button after processing the image.
Adding Camera Controls
1 group.rotateZ(-Math.PI / 2);2 const controls = new OrbitControls(camera, renderer.domElement);
- Rotates the group to align correctly.
- Enables mouse-based navigation using
OrbitControls
.
Animation Loop
1 function animate(): void {2 requestAnimationFrame(animate);3 controls.update();4 renderer.render(scene, camera);5 }6 animate();
- Uses requestAnimationFrame to continuously update the scene.
- Renders the 3D vectorized image.
Adding Image Download Functionality
1 downloadBtn.addEventListener('click', () => {2 renderer.render(scene, camera);3 const link = document.createElement('a');4 link.href = canvas.toDataURL('image/png');5 link.download = 'vectorized-image.png';6 link.click();7 });
- Captures the rendered canvas as a PNG.
- Creates a download link and triggers a click event.
- Saves the image as
vectorized-image.png
.
This implementation is a simple yet powerful way to transform raster images into vectorized 3D representations using Three.js! 🚀
5. Combine all Component in main.ts
Edit main.ts
file:
1 import { setupCanvas } from './canvas';2 import { setupHeader } from './header';3 import { setupHero } from './hero';4 import './style.css'5 import { setupButton } from './ui/button';67 document.querySelector<HTMLDivElement>('#app')!.innerHTML = `8 <header></header>9 <section id="hero"></section>10 <section>11 <button id="upload-btn"></button>12 </section>13 <div class="flex flex-col items-center justify-center my-10 hidden" id="content">14 <canvas></canvas>15 <button id="download-btn" class="my-4">Download</button>16 </div>17 <section class="ml-4 mt-28 mb-10 flex flex-col gap-4 justify-center items-center max-w-[90%]">18 <div>19 <h2 class="text-2xl">About</h2>20 <p class="max-w-2xl my-4">21 3D Image Vectorizer is a simple open-source tool designed to showcase the capabilities of Three.js by transforming raster images into scalable 3D vector graphics. Developed as part of an article on JSDev Space, this tool demonstrates real-time image processing, depth mapping, and smooth vectorization using JavaScript and WebGL.22 </p>23 </div>24 </section>25 <footer class="flex items-center justify-center">3D Image Vectorizer -- All Rights Reserved ${new Date(Date.now()).getFullYear()}</footer>26 `27 const header = document.querySelector<HTMLDivElement>('header')!;28 const sectionHero = document.querySelector<HTMLDivElement>('#hero')!;29 const uploadBtn = document.querySelector<HTMLButtonElement>('#upload-btn')!;30 const downloadBtn = document.querySelector<HTMLButtonElement>('#download-btn')!;31 const content = document.querySelector<HTMLDivElement>('#content');32 let githubBtn = document.querySelector<HTMLButtonElement>('#github__btn');3334 header && setupHeader(header);35 sectionHero && setupHero(sectionHero);36 uploadBtn && setupButton(uploadBtn, 'Upload Image', '#', 'upload');37 downloadBtn && setupButton(downloadBtn, 'Download', '#', 'primary');38 content && setupCanvas(content);3940 if (githubBtn) {41 setupButton(githubBtn, 'github', '#');42 } else {43 githubBtn = document.querySelector<HTMLButtonElement>('#github__btn');44 githubBtn && setupButton(githubBtn, 'github', '#');45 }
This TypeScript code sets up the user interface for a 3D Image Vectorizer using modular components, Tailwind CSS, and Three.js. It structures the page, initializes UI elements, and sets up interactive features like file uploads and downloads.
Setting Up the Page Structure
1 document.querySelector<HTMLDivElement>('#app')!.innerHTML = `2 <header></header>3 <section id="hero"></section>4 <section>5 <button id="upload-btn"></button>6 </section>7 <div class="flex flex-col items-center justify-center my-10 hidden" id="content">8 <canvas></canvas>9 <button id="download-btn" class="my-4">Download</button>10 </div>11 <section class="ml-4 mt-28 mb-10 flex flex-col gap-4 justify-center items-center max-w-[90%]">12 <div>13 <h2 class="text-2xl">About</h2>14 <p class="max-w-2xl my-4">15 3D Image Vectorizer is a simple open-source tool designed to showcase the capabilities of Three.js by transforming raster images into scalable 3D vector graphics. Developed as part of an article on JSDev Space, this tool demonstrates real-time image processing, depth mapping, and smooth vectorization using JavaScript and WebGL.16 </p>17 </div>18 </section>19 <footer class="flex items-center justify-center">20 3D Image Vectorizer -- All Rights Reserved ${new Date(Date.now()).getFullYear()}21 </footer>22 `;
- Creates an HTML template dynamically and inserts it into the
#app
<div>
. <header>
→ Placeholder for the site header.<section id="hero">
→ The hero section.- Upload Button (
#upload-btn
) → Users can upload an image. - #content Section (Hidden by Default)
- "About" Section → Describes the purpose of the tool.
- Footer → Displays current year dynamically using JavaScript.
Selecting and Configuring Elements
1 const header = document.querySelector<HTMLDivElement>('header')!;2 const sectionHero = document.querySelector<HTMLDivElement>('#hero')!;3 const uploadBtn = document.querySelector<HTMLButtonElement>('#upload-btn')!;4 const downloadBtn = document.querySelector<HTMLButtonElement>('#download-btn')!;5 const content = document.querySelector<HTMLDivElement>('#content');6 let githubBtn = document.querySelector<HTMLButtonElement>('#github__btn');
- Finds and stores references to key elements in the DOM.
Initializing Components
1 header && setupHeader(header);2 sectionHero && setupHero(sectionHero);3 uploadBtn && setupButton(uploadBtn, 'Upload Image', '#', 'upload');4 downloadBtn && setupButton(downloadBtn, 'Download', '#', 'primary');5 content && setupCanvas(content);
Safely initializes components if their elements exist:
setupHeader(header)
→ Calls a function to set up the page header.setupHero(sectionHero)
→ Calls a function to set up the hero section.setupButton(uploadBtn, 'Upload Image', '#', 'upload')
→ Converts the upload button into a functional upload input.setupButton(downloadBtn, 'Download', '#', 'primary')
→ Configures the download button.setupCanvas(content)
→ Calls setupCanvas to initialize Three.js rendering.
Summary
🔹 What This Code Does
✅ Dynamically builds an HTML structure inside #app.
✅ Sets up key UI components like header, hero section, upload/download buttons.
✅ Uses TypeScript for strong typing and safety.
✅ Integrates Three.js rendering via setupCanvas()
.
✅ Uses modular functions (setupButton
, setupHeader
, setupHero
) to keep code clean.
6. Start the Project
Run Vite’s development server:
Vite will launch the local server, and you can test your image vectorization service in the browser.

7. Explanation of Key Features
✅ Three.js Rendering: Converts pixel data into 3D points and lines.
✅ OrbitControls: Allows users to rotate and zoom the 3D view.
✅ File Upload: Loads an image and extracts pixel color data.
✅ Vectorization: Transforms the image into a 3D point cloud representation.
✅ Download Option: Captures the rendered scene and saves it as a PNG.
8. Conclusion
Using Three.js and Vite, we created a simple image vectorization service that loads an image, processes its pixels, and displays it in 3D space. You can enhance this by adding:
- Different vectorization styles (dots, edges, triangles)
- More color processing techniques
- WebGL performance optimizations
This is just a foundation—feel free to expand and experiment! 🚀