Преобразование растрового изображения в векторное изображение Tree.js и HTML (ознакомиться с полным кодом можно по ссылке на мой репозиторий )

Содержание

Введение

Проект, который мы будем сегодня реализовывать имеет несколько практических составляющих:

  • Научиться базовым принципам работы с библиотекой three.js;

  • Возможность преобразовывать картинки разных форматов в векторное изображение.

Создание HTML файла

Так как мы создаем проект именно в качестве веб-приложения, нам необходимо создать html файл и прописать в нем:

  1. пути к дополнительным ресурсам (файлам);

  2. canvas — это HTML-элемент, который используется для рисования графики с помощью JavaScript;

  3. тег input, необходимый для выбора интересующей пользователя картинки.

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="lab.css" rel="stylesheet">
    <title>3D-Project</title>
</head>
<body>
    <canvas id="myscene"></canvas>
    <input type="file" id="fileInput" accept="image/*">
    <script type="module" src="./lab.js"></script>
</body>
</html>

Работа с Node.js

Node.js — это среда выполнения JavaScript, построенная на движке V8 от Google Chrome. Она позволяет запускать JavaScript на сервере, что делает возможным создание серверных приложений с использованием JavaScript. Node.js использует неблокирующую, асинхронную модель ввода-вывода, что позволяет обрабатывать множество соединений одновременно, что делает его особенно подходящим для создания высокопроизводительных сетевых приложений.

Для скачивания и установки Node.js, выполните следующие команды:

  1. Для Ubuntu/Linux:

sudo apt install nodejs npm
  1. На Windows вы можете установить Node.js с помощью пакетного менеджера Chocolatey. Если у вас его еще нет, выполните следующие шаги:

    Установите Chocolatey:

    Откройте командную строку с правами администратора (нажмите Win, введите cmd, щелкните правой кнопкой мыши и выберите «Запуск от имени администратора»).

    Вставьте следующую команду и нажмите Enter:

@"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -InputFormat None -ExecutionPolicy Bypass -Command "iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))" && SET "PATH=%PATH%;%ALLUSERSPROFILE%\chocolatey\bin"

После установки Chocolatey в той же командной строке выполните:

choco install nodejs
  1. Для MacOS:

brew install node

Дождитесь завершения установки и идем дальше.

Создание package.json и webpack файлов

Начнем с того, что такое файл package.json.

Файл package.json используется в проектах на JavaScript для управления зависимостями, скриптами и метаданными проекта. Он обычно создается в корневой директории нашего проекта и содержит информацию о проекте, а также о необходимых пакетах и командах для его сборки и запуска.

После установки Node.js, мы запускаем команду npm init -y, которая создает нам файл package.json с базовыми настройками.
После, мы меняем содержимое файла на это:

{
    "name": "название нашего файла",
    "version": "1.0.0",
    "scripts": {
        "start": "webpack serve --open",
        "build": "webpack"
    },
    "devDependencies": {
        "html-webpack-plugin": "^5.6.2",
        "three": "^0.169.0",
        "webpack": "^5.95.0",
        "webpack-cli": "^5.1.4",
        "webpack-dev-server": "^5.1.0"
    }
} 

Теперь разберем, что такое Webpack.

Webpack — это мощный инструмент для сборки модулей JavaScript, который позволяет разработчикам управлять зависимостями и оптимизировать ресурсы для веб-приложений. Он используется для объединения различных файлов (JavaScript, CSS, изображения и т.д.) в один или несколько выходных файлов, которые могут быть загружены в браузере.

Чтобы создать файл конфигурации для Webpack, нам нужно создать файл с именем webpack.config.js в корневой директории нашего проекта. Этот файл будет содержать настройки, которые Webpack будет использовать для сборки.

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: './lab.js', // Указывает входной файл для сборки
    output: {
        filename: 'lab.js', // Имя выходного файла
        path: path.resolve(__dirname, 'dist'), // Путь к выходной директории
        clean: true, // Очищает выходную директорию перед каждой сборкой
    },
    devServer: {
        static: {
            directory: path.join(__dirname, 'dist'), // Указывает директорию для статических файлов
        },
        open: true, // Автоматически открывает браузер при запуске сервера
        port: 8080, // Порт, на котором будет запущен сервер
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: './lab.html', // Шаблон HTML-файла
        }),
    ],
    mode: 'development', // Режим сборки (development или production)
};

Создание JS файла

Как вы уже поняли из прошлого блока, нам необходимо создать файл lab.js.
Для начала, рекомендую в принципе ознакомиться с библиотекой Three.js на официальном сайте https://threejs.org. Там прекрасно описаны основные "3 кита Three.js" и есть каталог со всеми необходимыми методами.


Я постараюсь кратко описать основной алгоритм выполнения:

  1. Необходимо скачать библиотеку three :

    Чтобы установить библиотеку three.js на разных операционных системах (macOS, Windows и Linux), вы можете использовать пакетный менеджер npm, который идет в комплекте с Node.js.:

npm install three
  1. Импортируем все из этой библиотеки (это уже прописываем в файле):

import * as THREE from 'three';
  1. Для возможности манипулировать в дальнейшем картинкой (вращать ее), прописываем :

import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
  1. Создаем необходимый холст, где будет проецироваться 3d-фигура:

let canvas = document.getElementById('myscene');
let width = 1400;
let height = 800;
  1. Создаем экземпляр рендеринга, задавая ему значение холста и сглаживания;

  2. Устанавливаем соотношение пикселей с помощью метода:

setPixelRatio

( возвращает коэффициент пикселей устройства. Если он больше 1 (что означает, что устройство имеет высокое разрешение), мы устанавливаем соотношение пикселей в 2, иначе — в 1.). Потом задаем размеры холста и его цвет.

  1. Затем создаем сцену и устанавливаем камеру (это базовые настройки, которые можно корректировать в зависимости от наших предпочтений, поэтому расписывать это не буду)

  2. Чтобы в дальнейшем не добавлять по одному элементу на сцену, я создам группу, которая будет хранить в себе все элементы моего рисования (далее это поможет при вращении и перемещении всего объекта в целом):

let group = new THREE.Group();
  1. Далее идет основная часть этого приложения - рисование. В этом блоке мы обращаемся к элементу input и задаем ему функцию с собsтием нажатия, при котором он будет запускать по факту весь функционал нашей программы.

    Распишу это немного подробнее.

    Для начала мы создаем переменную, которая будет использоваться для получения первого файла, выбранного пользователем через элемент <input type="file"> в HTML.

    После того, как мы убедились, что файл не пустой, мы начинаем отрисовку:

  2. Создание объекта изображения: const img = new Image(); Здесь создается новый объект Image, который будет использоваться для загрузки изображения.

  3. Обработчик события onload:

img.onload = function() {
    // Код внутри этой функции выполнится, когда изображение будет загружено
};

Этот обработчик срабатывает, когда изображение успешно загружено. Внутри него будет выполняться основной код для обработки изображения.

  1. Создание канваса и контекста:

let canvas2d = document.createElement('canvas');
let ctx = canvas2d.getContext('2d');
canvas2d.width = 200;
canvas2d.height = 200;

Здесь создается временный элемент , который используется для рисования изображения в 2D. Устанавливаются размеры канваса на 200x200 пикселей.

  1. Рисование изображения на канвасе:

ctx.drawImage(img, 0, 0, 200, 200);
let data = ctx.getImageData(0, 0, size, size).data;

Изображение рисуется на канвасе, и затем получаются данные пикселей с помощью getImageData. Эти данные содержат информацию о цветах каждого пикселя в формате RGBA.

  1. Используем метод group.clear() для очистки всех объектов из группы. Таким образом, он помогает поддерживать сцену в чистоте и управлять объектами более эффективно, особенно при динамическом обновлении содержимого.

group.clear();
  1. Цикл для создания 3D-объектов и цикл для обработки пикселей:

for (let i = 0; i < size; ++i) {
                    let geometry = new THREE.BufferGeometry();
                    let vertices = new Float32Array(size * 3);
                    let colors = new Float32Array(size * 3); 

                    for (let j = 0; j < size; ++j) {
                        let colorIndex = (j * size + i) * 4; 
                        let r = data[colorIndex] / 255; 
                        let g = data[colorIndex + 1] / 255; 
                        let b = data[colorIndex + 2] / 255;

                        vertices[j * 3] = j - 100; 
                        vertices[j * 3 + 1] = i - 100; 
                        vertices[j * 3 + 2] = data[colorIndex] / 10; 

                        colors[j * 3] = r;
                        colors[j * 3 + 1] = g; 
                        colors[j * 3 + 2] = b; 
                    }

                    geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
                    geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));

                    let material = new THREE.LineBasicMaterial({ vertexColors: true });
                    let line = new THREE.Line(geometry, material);
                    group.add(line);
                }

В этом цикле создается новая геометрия для каждой строки пикселей. vertices и colors — это массивы, которые будут содержать координаты вершин и цвета для каждой линии.

Внутренний цикл проходит по каждому пикселю в строке, значения красного, зеленого и синего компонентов извлекаются из массива данных и нормализуются (делятся на 255).

Далее создается материал LineBasicMaterial, который будет использоваться для отрисовки линий. Параметр { vertexColors: true } указывает, что цвета вершин, заданные в массиве colors, будут использоваться для окраски линий.

Создается объект THREE.Line, который принимает геометрию и материал. Этот объект представляет собой линию, которая будет отображаться в 3D-сцене.

Затем линия добавляется в группу group, что позволяет управлять всеми линиями как единым целым. Это может быть полезно для перемещения, вращения или масштабирования всех объектов в группе одновременно.

  1. Установка координат вершин и цветов:

vertices[j * 3] = j - 100;  //координата по x
vertices[j * 3 + 1] = i - 100; // координата по y
vertices[j * 3 + 2] = data[colorIndex] / 10; //координата по z 

colors[j * 3] = r;
colors[j * 3 + 1] = g; 
colors[j * 3 + 2] = b;

Здесь устанавливаются координаты вершин для 3D-объектов. z-координата устанавливается на основе значения красного канала, деленного на 10, чтобы создать некоторую высоту.

В принципе, это все. Нам осталось загрузить результат в объект img и отрисовать это все с помощью функции, которая вызывает саму себя и обновляет данные сцены и камеры:

function animation() {
    requestAnimationFrame(animation);
    controls.update();
    renderer.render(scene, camera);
}
animation();

Осталось только запустить наш проект командой npm start

В конечном итоге, загрузив фотографию, мы получим ее векторное представление:

Надеюсь, эта статья была полезной, и вам так же захочется развиваться в frontend-разработке, как и мне))

Данная статья была написана в соавторстве с @MedveDux и является результатом лабораторной работы, выполненной в МГППУ.

Комментарии (5)


  1. z00m
    28.10.2024 10:52

    Этой статье очень не хватает ссылки на репу с кодом, а ещё лучше ссылки на демо-страницу (благо бесплатных хостингов для статики много)


  1. excoder
    28.10.2024 10:52

    Про 3d, и ни одной картинки. Прогеры такие прогеры.


  1. dan_sw
    28.10.2024 10:52

    Тема разработки веб-приложений с помощью библиотеки Three.js для создания трёхмерной графики довольно интересна. Сейчас и игры делают с её помощью, коммерческие приложения или просто визуализации (например, для кластерного трёхмерного анализа это может быть применимо).

    Однако по ходу чтения статьи я обнаружил, что после её прочтения - мало что из неё понял и уяснил, хотя с трёхмерной графикой опыт работы уже имел. Хотел бы выразить ряд замечаний связанных с материалом самой статьи и её оформлением, с целью мотивировать авторов данной статьи её улучшить.

    Начнём с замечаний по материалу. В статье не ясно какой будет конечный результат, как мы к этому результату пришли, что для этого нужно сделать (нумерованный список с действиями это не объясняет, а только добавляет вопросов). Понятно, что в начале было (косвенно) о разработке механизма, с помощью которого можно будет преобразовать картинку в 3D модель (если я правильно понял):

    Возможность конвертировать картинки разных форматов в 3d визуал.

    Но, что-то, ни картинки, ни 3D модели в конечном итоге я не увидел в статье.

    Когда пишут статьи где подразумевается работа с графикой (2D, 3D, 4D), то в статье, как бы, хорошим тоном считается добавлять картинки начального, промежуточного и конечного результата. Особенно радует когда понятно с помощью каких алгоритмов, программного кода или подхода мы пришли от A к B и от B к C.

    Начнем с того, что такое файл JSON.

    Мы так и не узнали что такое файл JSON :) Возможно Вы имели ввиду файл package.json, т.к. после этого предложения идёт описание и предназначение файла package.json. В любом случае, наблюдается непоследовательность изложения. Файл package.json просто так появиться не может, он создаётся с помощью специальной утилиты npm (для командной строки это утилита, а вообще это пакетный менеджер). И после описания файла package.json идёт следующий текст:

    Для начала установите Node.js, если у вас его еще нет, а так же установим webpack (чуть позже он нам пригодится)

    Если вы хотите объяснить что такое package.json, то это следует делать только после того, как вы опишите что такое Node.js и NPM, потому что складывается ощущение что этот файл нужно создать самому, а Node.js это так - что-то стороннее. Файл package.json описывает в целом конфигурацию JS/TS модуля, с зависимостями и прочими вещами.

    Команда для macOS

    Мы живём в мире, где царствует кроссплатформенная и кроссбраузерная разработка. Не у всех читателей на Хабре компьютер с MacOS, поэтому если вы решаетесь написать команду для установки Node.js на свою ОС, то следует дать ссылку или написать где-нибудь рядом команды для установки под другие ОС (например, Ubuntu или Windows).

    Если же ваша работа целиком и полностью охватывает только MacOS, то об этом следует написать сразу в начале, чтобы пользователи понимали в какой среде будет происходить работа и, соответственно, сможет ли пользователь все эти эксперименты повторить.

    После этого мы запускаем команду npm init -y, которая создает нам файл json с базывыми настройками

    Данный текст требуется доработать. Есть ошибка в слове "базовыми" и не ясно что за файл создаёт нам утилита npm, при вызове команды npm init -y (а он создаёт как раз package.json).

    Я постараюсь кратко описать всю структуру и математику проекта, поэтому опишу основной алгоритм выполнения

    Формулировка данного текста довольна не точная, потому что структуру проекта Вы не описываете. Вы описываете основной алгоритм, который заложили в программный код на JavaScript, но не структуру проекта. Обычно под структурой проекта понимается набор файлов, папок (или каталогов), которые определённым образом упорядочены и это упорядочение имеет какую-то смысловую нагрузку. Например, в каталоге app у нас входная точка в программу, а в каталоге hooks специальные хуки. Или в каталоге models у нас модели, а в shader у нас шейдеры.

    Необходимо скачать библиотеку three так же через brew

    Здесь нужна также команда и также на более распространённые ОС чем MacOS (так вы увеличите число заинтересованных читателей).

    с собитием нажатия

    Стоит обнаружить и поправить побольше таких слов, потому что читается это забавно :)

    которая будет используется

    Это сюда же :)

    Очистка группы

    Что за группа? В представленном статье коде непонятно что под группой (переменной или константой group) подразумевается? Почему её нужно очистить? Зачем? Что она из себя представляет? Думаю здесь нужно подумать над тем, чтобы добавить описание этой ... ячейки (ячейкам) оперативной памяти, которую занял и выделил под ваш скрипт браузер и который контролирует доступ к этой ячейки памяти из вашего скрипта (через, например, движок V8 или любой другой, который есть в популярных MacOS браузерах). Переменная это или константа - непонятно, поэтому написал абстрактно :)

    Цикл для создания 3D-объектов

    Здесь в программном коде ошибка. Нужна закрывающая фигурная скобка в конце цикла. В общем-то дальше такие ошибки также встречаются (нужно вычитывать статью, перед публикацией, чтобы таких "мелких" недочётов не было, иначе из-за этого можно "нахватать минусов").

    В принципе, это все. Нам осталось загрузить результат в объект img и отрисовать это все с помощью функции, которая вызывает саму себя и обновляет данные сцены и камеры

    И дальше идёт код анимации, по видимому, а не отрисовки объекта img. Как я понял, в функции animation идёт рендеринг сцены и камеры, а не объекта img.

     Значения нормализуются, деля на 255

    Деля что на что, и зачем нужна нормализация в данном случае? Тема не раскрыта.

    В общем и целом, предлагаю авторам статьи ещё раз перечитать материал и его улучшить, ему это действительно необходимо, чтобы он лучше читался и было понятно о чём идёт речь. Тема векторизации также не раскрыта.

    По оформлению статьи есть также замечания, которые мне, как читателю, показались нуждающимися во внимании.

    Первое замечание касается не целевого использования цитаты для оформления списков:

    Список это цитата, список или список цитат?
    Список это цитата, список или список цитат?

    Списки всегда лучше оформлять списком. У хабра замечательный редактор (правда с обратной совместимостью прошлых статей проблемы есть, но в целом норм), его возможности можно использовать на максимум чтобы добиться хорошего оформления статьи. Есть даже специальные публикации о том, как нужно оформлять статьи. Советую их почитать, будет полезно.

    На хабре также есть редактор исходного текста программ. Если добавить программный код на страницу то рядом есть выпадающий список с возможными языками программирования. Стоит его активно использовать и читать исходный код будет легче:

    И JSON, и команды можно оформить как и код - будет выглядеть лучше
    И JSON, и команды можно оформить как и код - будет выглядеть лучше

    Программный код должен оформляться как программный код, а не как часть текста. То же относится и к командам:

    Код с текстом мешать не стоит, выглядит это очень не очень
    Код с текстом мешать не стоит, выглядит это очень не очень

    Нумерованные заголовки также не стоит выделять цитатами, для этого есть заголовки разного уровня в самом редакторе (как правило их хватает).

    Также не хватает библиографического списка (какими статьями вы руководствовались, когда писали данный материал?) и ссылку на исходный код (потому краткие его выжимки не дают понимания полной картины).

    Хорошая статья, если она что-то обозревает (например, результаты исследования), содержит минимум кода, но при этом максимум словесного описания, проектирования и визуализации. После прочтения такой статьи нет недопонимания ключевых вещей, а для ознакомлением с деталями можно всегда обратиться к исходному коду.

    Спасибо за статью, очень рекомендую её улучшить (моменты основные расписал), у этой темы определённо есть будущее.

    Рекомендую также ознакомиться с книгой про работу с графическими процессорами в браузере (WebGPU), может быть эта тема будет вам интересна)

    Желаю успехов в дальнейшем развитии во frontend-разработке и в освоении трёхмерной графики!


    1. strok_ova Автор
      28.10.2024 10:52

      Спасибо за развернутую обратную связь! В первый раз писала статью о проделанной работе, да и во frontend я не так давно. Обязательно учту это и постараюсь подкорректировать данную статью.


  1. shaman4d
    28.10.2024 10:52

    Что то вектора я и не приметил. Все что вы делаете, это рендерите в 3х мерном пространстве картинку с небольшим сдвигом по оси z. Векторизация изображений, это сложный вычислительный процесс, превращающий растровое изображение, состоящее из пикселей, в векторное, состоящее из линий, кривых и других геометрических фигур. Такое изображение можно увеличивать или уменьшать без потери качества. Иными словами если бы вы вашу картинку в SVG перевели, вот тогда это и была бы векторизация.