JavaScript Development Space

Vector Image Service with Three.js and Vite

Add to your RSS feed11 February 202513 min read
Build a Vector Image Service Using ThreeJS and Vite | Tutorial

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:

npm create vite@latest image-vectorizer --template vanilla
cd image-vectorizer
npm install

Then, install Three.js:

npm install three @types/three

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:

npm install tailwindcss @tailwindcss/vite

2. Configure Vite

Open/create vite.config.ts and add the @tailwindcss/vite plugin:

ts
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:

css
1 @import 'tailwindcss';
2
3 :root {
4 font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
5 line-height: 1.5;
6 font-weight: 400;
7
8 color-scheme: light dark;
9 color: rgba(255, 255, 255, 0.87);
10 background-color: #242424;
11
12 font-synthesis: none;
13 text-rendering: optimizeLegibility;
14 -webkit-font-smoothing: antialiased;
15 -moz-osx-font-smoothing: grayscale;
16 }
17
18 body {
19 margin: 0;
20 display: flex;
21 flex-direction: column;
22 place-items: center;
23 min-width: 320px;
24 min-height: 100vh;
25 }
26
27 h1 {
28 font-size: 3.2em;
29 line-height: 1.1;
30 }
31
32 #app {
33 margin: 0 auto;
34 text-align: center;
35 }
36
37 @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:

pqsql
1 image-vectorizer/
2 │── public/
3 │── src/
4 │── index.html
5 │── package.json
6 │── tsconfig.json

3. HTML Structure

Modify index.html to include:

html
1 <!DOCTYPE html>
2 <html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 <meta
6 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 <meta
9 name="viewport"
10 content="width=device-width, initial-scale=1.0" />
11 <link
12 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 <script
19 type="module"
20 src="/src/main.ts"></script>
21 </body>
22 </html>

4. Create UI Components

Button Component

Create src/ui/button.ts file:

ts
1 type BtnVariant = 'primary' | 'secondary' | 'upload';
2
3 export function setupButton(element: HTMLButtonElement, text: string, href: string, variant: BtnVariant = 'primary') {
4 element.innerHTML = variant === 'primary' ? `
5 <a
6 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 <a
14 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 of a button based on the provided variant.
  • If 'primary' or 'secondary', it creates an <a> button linking to href.
  • 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:

ts
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>
12
13 <div class="flex items-center gap-4">
14 <button id="github__btn"></button>
15 </div>
16 </div>
17 </div>`;
18 }

Hero component:

ts
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 Vectorizer
9 </h1>
10
11 <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 <img
16 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:

ts
1 import * as THREE from 'three';
2 import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
3
4 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;
8
9 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);
17
18 const scene = new THREE.Scene();
19 const camera = new THREE.PerspectiveCamera(40, width / height, 0.1, 1000);
20 camera.position.set(0, 0, 300);
21
22 const group = new THREE.Group();
23 scene.add(group);
24
25 const downloadBtn = document.getElementById('download-btn') as HTMLButtonElement;
26 const fileInput = document.getElementById('fileInput') as HTMLInputElement | null;
27
28 if (fileInput) {
29 fileInput.addEventListener('change', (event: Event) => {
30 const uploadEvent = event.target as HTMLInputElement;
31 if (!uploadEvent?.files?.length) return;
32
33 const file = uploadEvent.files[0];
34 const reader = new FileReader();
35
36 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;
42
43 const size = 200;
44 canvas2d.width = size;
45 canvas2d.height = size;
46 ctx.drawImage(img, 0, 0, size, size);
47
48 const data = ctx.getImageData(0, 0, size, size).data;
49 group.clear();
50
51 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);
55
56 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;
61
62 vertices.set([j - 100, i - 100, data[colorIndex] / 10], j * 3);
63 colors.set([r, g, b], j * 3);
64 }
65
66 geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
67 geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
68
69 const material = new THREE.LineBasicMaterial({ vertexColors: true });
70 const line = new THREE.Line(geometry, material);
71 group.add(line);
72 }
73
74 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 }
83
84 group.rotateZ(-Math.PI / 2);
85 const controls = new OrbitControls(camera, renderer.domElement);
86
87 function animate(): void {
88 requestAnimationFrame(animate);
89 controls.update();
90 renderer.render(scene, camera);
91 }
92 animate();
93
94 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.

ts
1 export function setupCanvas(element: HTMLDivElement): void { }

This function initializes the Three.js scene within the provided HTMLDivElement.

Creating the WebGL Renderer

ts
1 const canvas = document.querySelector('canvas') as HTMLCanvasElement;
2 const width = window.innerWidth / 1.5;
3 const height = window.innerHeight / 1.5;
4
5 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.
ts
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

ts
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);
4
5 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

ts
1 const fileInput = document.getElementById('fileInput') as HTMLInputElement | null;
  • Finds the file input (<input type="file">) for image uploads.
ts
1 if (fileInput) {
2 fileInput.addEventListener('change', (event: Event) => {
3 const uploadEvent = event.target as HTMLInputElement;
4 if (!uploadEvent?.files?.length) return;
5
6 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

ts
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).
ts
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.
ts
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

ts
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.
ts
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;
6
7 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).
ts
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

ts
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

ts
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

ts
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

ts
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:

ts
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';
6
7 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');
33
34 header && setupHeader(header);
35 sectionHero && setupHero(sectionHero);
36 uploadBtn && setupButton(uploadBtn, 'Upload Image', '#', 'upload');
37 downloadBtn && setupButton(downloadBtn, 'Download', '#', 'primary');
38 content && setupCanvas(content);
39
40 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

ts
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

ts
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

ts
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:

npm run dev

Vite will launch the local server, and you can test your image vectorization service in the browser.

Vite + ThreeJS

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! 🚀

JavaScript Development Space

© 2025 JavaScript Development Space - Master JS and NodeJS. All rights reserved.