├── dev.bat
├── src
├── index.js
├── assets
│ ├── icon.png
│ ├── manifest.json
│ ├── sw.js
│ └── favicon.svg
├── index.html
├── scripts
│ ├── timer.js
│ ├── utils.js
│ ├── matrix.js
│ ├── lang.js
│ ├── filters.js
│ ├── imageloader.js
│ ├── canvas.js
│ ├── processor.js
│ └── app.js
└── index.css
├── .gitignore
├── index
├── icon.png
├── index.html
├── manifest.json
├── sw.js
├── favicon.svg
├── style.css
└── script.js
├── .gitattributes
├── index.html
├── package.json
├── webpack.dev.config.js
├── webpack.single.config.js
├── arduino
└── main.cpp
├── webpack.host.config.js
├── gzip_h.js
└── README.md
/dev.bat:
--------------------------------------------------------------------------------
1 | npm run dev
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import './index.css'
2 | import './scripts/app'
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | package-lock.json
4 | .vscode
--------------------------------------------------------------------------------
/index/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexGyver/Bitmaper/HEAD/index/icon.png
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/src/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexGyver/Bitmaper/HEAD/src/assets/icon.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Bitmaper v<%= htmlWebpackPlugin.options.version %>
8 | <%= htmlWebpackPlugin.options.manifest %>
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/scripts/timer.js:
--------------------------------------------------------------------------------
1 | export default class Timer {
2 | #timer;
3 |
4 | start(handler, time) {
5 | this.stop();
6 | this.#timer = setTimeout(handler, time);
7 | }
8 |
9 | stop() {
10 | if (this.#timer) {
11 | clearTimeout(this.#timer);
12 | this.#timer = null;
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/src/scripts/utils.js:
--------------------------------------------------------------------------------
1 | export function constrain(x, min, max) {
2 | return (x < min) ? min : (x > max ? max : x);
3 | }
4 | export function round(x) {
5 | return x << 0;
6 | }
7 | export function RGBtoHEX(r, g, b) {
8 | return (r << 16) | (g << 8) | b;
9 | }
10 | export function HEXtoRGB(hex) {
11 | return [(hex >> 16) & 0xff, (hex >> 8) & 0xff, hex & 0xff];
12 | }
--------------------------------------------------------------------------------
/index/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Bitmaper v1.2.13
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/index/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "theme_color": "#478be6",
3 | "background_color": "#1C2128",
4 | "display": "standalone",
5 | "start_url": ".",
6 | "name": "Bitmaper",
7 | "short_name": "Bitmaper",
8 | "description": "Image to bitmap converter",
9 | "orientation": "portrait",
10 | "icons": [
11 | {
12 | "src": "icon.png",
13 | "sizes": "512x512",
14 | "type": "image/png",
15 | "purpose": "any maskable"
16 | }
17 | ]
18 | }
--------------------------------------------------------------------------------
/src/assets/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "theme_color": "#478be6",
3 | "background_color": "#1C2128",
4 | "display": "standalone",
5 | "start_url": ".",
6 | "name": "Bitmaper",
7 | "short_name": "Bitmaper",
8 | "description": "Image to bitmap converter",
9 | "orientation": "portrait",
10 | "icons": [
11 | {
12 | "src": "icon.png",
13 | "sizes": "512x512",
14 | "type": "image/png",
15 | "purpose": "any maskable"
16 | }
17 | ]
18 | }
--------------------------------------------------------------------------------
/index/sw.js:
--------------------------------------------------------------------------------
1 | const CACHE_NAME="bitmaper_bf5f21f28b5854aed796",CACHED_URLS=["/","favicon.svg","index.html","script.js","style.css"];self.addEventListener("install",(t=>{t.waitUntil(async function(){const t=await caches.open(CACHE_NAME);await t.addAll(CACHED_URLS)}())})),self.addEventListener("fetch",(t=>{const{request:a}=t;a.url.startsWith(self.location.href)&&("only-if-cached"===a.cache&&"same-origin"!==a.mode||t.respondWith(async function(){const e=await caches.open(CACHE_NAME),n=await e.match(a),i=fetch(a);return a.url.startsWith(self.location.origin)&&t.waitUntil(async function(){const t=await i;await e.put(a,t.clone())}()),n||i}()))})),self.addEventListener("activate",(t=>{t.waitUntil(async function(){const t=await caches.keys();await Promise.all(t.filter((t=>t!==CACHE_NAME)).map((t=>caches.delete(t))))}())}));
--------------------------------------------------------------------------------
/src/scripts/matrix.js:
--------------------------------------------------------------------------------
1 | export default class Matrix {
2 | matrix = new Int32Array();
3 | W = 0;
4 | H = 0;
5 |
6 | constructor(w, h) {
7 | this.resize(w, h);
8 | }
9 |
10 | resize(w, h) {
11 | if (w && h) {
12 | this.W = w;
13 | this.H = h;
14 | this.matrix = new Int32Array(w * h).fill(0);
15 | }
16 | }
17 |
18 | merge(mx) {
19 | if (this.matrix.length != mx.matrix.length) return;
20 | for (let i = 0; i < this.matrix.length; i++) {
21 | let v = mx.matrix[i];
22 | if (v) this.matrix[i] = (v == 1) ? 1 : 0;
23 | }
24 | }
25 |
26 | set(x, y, v) {
27 | this.matrix[y * this.W + x] = v;
28 | }
29 |
30 | get(x, y) {
31 | return this.matrix[y * this.W + x];
32 | }
33 |
34 | clear() {
35 | this.matrix.fill(0);
36 | }
37 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bitmaper",
3 | "version": "1.2.13",
4 | "main": "index.js",
5 | "scripts": {
6 | "build": "webpack --config ./webpack.single.config.js & webpack --config ./webpack.host.config.js",
7 | "postbuild": "node gzip_h.js",
8 | "dev": "webpack serve --config ./webpack.dev.config.js & webpack --config ./webpack.dev.config.js"
9 | },
10 | "keywords": [],
11 | "author": "",
12 | "license": "ISC",
13 | "description": "",
14 | "devDependencies": {
15 | "copy-webpack-plugin": "^12.0.2",
16 | "css-loader": "^7.1.2",
17 | "css-minimizer-webpack-plugin": "^7.0.0",
18 | "cyrillic-to-translit-js": "^3.2.1",
19 | "html-inline-css-webpack-plugin": "^1.11.2",
20 | "html-inline-script-webpack-plugin": "^3.2.1",
21 | "html-webpack-plugin": "^5.6.3",
22 | "mini-css-extract-plugin": "^2.9.2",
23 | "replace-hash-in-file-webpack-plugin": "^1.0.8",
24 | "style-loader": "^4.0.0",
25 | "webpack": "^5.97.1",
26 | "webpack-cli": "^6.0.1",
27 | "webpack-dev-server": "^5.2.0"
28 | },
29 | "dependencies": {
30 | "@alexgyver/ui": "^1.2.7"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/webpack.dev.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 | const MiniCssExtractPlugin = require("mini-css-extract-plugin");
4 |
5 | module.exports = {
6 | entry: {
7 | index: './src/index.js',
8 | },
9 |
10 | output: {
11 | filename: 'script.js',
12 | path: path.resolve(__dirname, 'dev'),
13 | clean: true,
14 | },
15 |
16 | module: {
17 | rules: [
18 | {
19 | test: /\.css$/,
20 | use: [
21 | MiniCssExtractPlugin.loader,
22 | "css-loader"
23 | ]
24 | }
25 | ]
26 | },
27 |
28 | plugins: [
29 | new HtmlWebpackPlugin({
30 | template: `./src/index.html`,
31 | filename: `index.html`,
32 | inject: true,
33 | minify: false,
34 | }),
35 | new MiniCssExtractPlugin({
36 | filename: 'style.css',
37 | }),
38 | ],
39 |
40 | devServer: {
41 | watchFiles: ['src/*.html'],
42 | static: path.resolve(__dirname, './dev'),
43 | hot: true,
44 | open: true,
45 | },
46 |
47 | watchOptions: {
48 | poll: 1000,
49 | // ignored: '/node_modules/',
50 | },
51 |
52 | mode: 'development',
53 | };
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | background-color: #1C2128;
4 | width: 100%;
5 | height: 100%;
6 | padding: 0;
7 | margin: 0;
8 | }
9 |
10 | .filters {
11 | width: 200px;
12 | height: 100%;
13 | float: right;
14 | position: relative;
15 | }
16 |
17 | .cv_cont {
18 | display: flex;
19 | justify-content: center;
20 | margin: 0px 220px;
21 | padding-top: 30px;
22 | }
23 |
24 | .cv_inner {
25 | max-width: 800px;
26 | width: 100%;
27 | display: flex;
28 | justify-content: center;
29 | }
30 |
31 | .canvas {
32 | display: block;
33 | border-radius: 5px;
34 | cursor: pointer;
35 | background: black;
36 | }
37 |
38 | .info_cont {
39 | display: block;
40 | width: 100%;
41 | height: 100%;
42 | position: absolute;
43 | top: 0;
44 | left: 0;
45 | z-index: 3;
46 | background: #0009;
47 | cursor: pointer;
48 | }
49 |
50 | .info_inner {
51 | max-width: 900px;
52 | margin: auto;
53 | margin-top: 50px;
54 | background: #2D333B;
55 | color: #ccc;
56 | padding: 15px;
57 | font-family: system-ui;
58 | border-radius: 7px;
59 | cursor: default;
60 | }
61 |
62 | .info_inner span {
63 | display: block;
64 | white-space: pre;
65 | text-wrap: wrap !important;
66 | }
67 |
68 | .info_inner button {
69 | background: none;
70 | border: none;
71 | font-size: 25px;
72 | color: red;
73 | cursor: pointer;
74 | float: right;
75 | }
--------------------------------------------------------------------------------
/src/assets/sw.js:
--------------------------------------------------------------------------------
1 | const CACHE_NAME = 'bitmaper_@cachename';
2 |
3 | const CACHED_URLS = [
4 | '/',
5 | 'favicon.svg',
6 | 'index.html',
7 | 'script.js',
8 | 'style.css',
9 | ]
10 |
11 | self.addEventListener('install', event => {
12 | event.waitUntil(async function () {
13 | const cache = await caches.open(CACHE_NAME);
14 | await cache.addAll(CACHED_URLS);
15 | }());
16 | });
17 |
18 | self.addEventListener('fetch', event => {
19 | const { request } = event;
20 | //if (!request.destination.length) return;
21 | if (!request.url.startsWith(self.location.href)) return;
22 | if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') return;
23 |
24 | event.respondWith(async function () {
25 | const cache = await caches.open(CACHE_NAME);
26 | const cachedResponsePromise = await cache.match(request);
27 | const networkResponsePromise = fetch(request);
28 | if (request.url.startsWith(self.location.origin)) {
29 | event.waitUntil(async function () {
30 | const networkResponse = await networkResponsePromise;
31 | await cache.put(request, networkResponse.clone());
32 | }());
33 | }
34 | return cachedResponsePromise || networkResponsePromise;
35 | }());
36 | });
37 |
38 | self.addEventListener('activate', event => {
39 | event.waitUntil(async function () {
40 | const cacheNames = await caches.keys()
41 | await Promise.all(
42 | cacheNames.filter((cacheName) => {
43 | const deleteThisCache = cacheName !== CACHE_NAME;
44 | return deleteThisCache;
45 | }).map(cacheName => caches.delete(cacheName))
46 | )
47 | }());
48 | });
--------------------------------------------------------------------------------
/webpack.single.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | var PACKAGE = require('./package.json');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 | const TerserPlugin = require("terser-webpack-plugin");
5 | const MiniCssExtractPlugin = require("mini-css-extract-plugin");
6 | const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
7 | const HTMLInlineCSSWebpackPlugin = require("html-inline-css-webpack-plugin").default;
8 | const HtmlInlineScriptPlugin = require('html-inline-script-webpack-plugin');
9 |
10 | module.exports = {
11 | entry: {
12 | index: './src/index.js',
13 | },
14 |
15 | output: {
16 | filename: 'script.js',
17 | path: path.resolve(__dirname, 'dist/single'),
18 | clean: true,
19 | publicPath: '',
20 | },
21 |
22 | optimization: {
23 | minimize: true,
24 | minimizer: [
25 | new CssMinimizerPlugin(),
26 | new TerserPlugin(),
27 | ],
28 | },
29 |
30 | module: {
31 | rules: [
32 | {
33 | test: /\.css$/,
34 | use: [
35 | MiniCssExtractPlugin.loader,
36 | "css-loader"
37 | ]
38 | }
39 | ]
40 | },
41 |
42 | plugins: [
43 | new HtmlWebpackPlugin({
44 | template: `./src/index.html`,
45 | filename: `bitmaper.html`,
46 | inject: true,
47 | minify: false,
48 | version: PACKAGE.version,
49 | }),
50 | new HtmlInlineScriptPlugin({
51 | htmlMatchPattern: [/bitmaper.html$/],
52 | }),
53 | new MiniCssExtractPlugin({
54 | filename: 'style.css',
55 | }),
56 | new HTMLInlineCSSWebpackPlugin(),
57 | ],
58 |
59 | mode: 'production',
60 | };
--------------------------------------------------------------------------------
/index/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/arduino/main.cpp:
--------------------------------------------------------------------------------
1 | // пример как захостить приложение на еспшке и принять данные
2 | // используется сервер GyverHTTP
3 |
4 | #include
5 | #define WIFI_SSID ""
6 | #define WIFI_PASS ""
7 |
8 | #ifdef ESP8266
9 | #include
10 | #else
11 | #include
12 | #endif
13 |
14 | #include
15 | ghttp::Server server(80);
16 |
17 | // файл из релиза, содержит приложение в бинарном gzip виде
18 | #include "bitmaper.h"
19 |
20 | void setup() {
21 | Serial.begin(115200);
22 |
23 | // STA
24 | WiFi.mode(WIFI_AP_STA);
25 | if (strlen(WIFI_SSID)) {
26 | WiFi.begin(WIFI_SSID, WIFI_PASS);
27 | uint8_t tries = 20;
28 | while (WiFi.status() != WL_CONNECTED) {
29 | delay(500);
30 | Serial.print(".");
31 | if (!--tries) break;
32 | }
33 | Serial.print("Connected: ");
34 | Serial.println(WiFi.localIP());
35 | }
36 |
37 | // AP
38 | WiFi.softAP("AP ESP");
39 | Serial.print("AP: ");
40 | Serial.println(WiFi.softAPIP());
41 |
42 | // server
43 | server.begin();
44 | server.onRequest([](ghttp::ServerBase::Request req) {
45 |
46 | switch (req.path().hash()) {
47 | case su::SH("/"):
48 | server.sendFile_P(bitmaper_index, sizeof(bitmaper_index), "text/html", false, true);
49 | break;
50 |
51 | case su::SH("/script.js"):
52 | server.sendFile_P(bitmaper_script, sizeof(bitmaper_script), "text/javascript", true, true);
53 | break;
54 |
55 | case su::SH("/style.css"):
56 | server.sendFile_P(bitmaper_style, sizeof(bitmaper_style), "text/css", true, true);
57 | break;
58 |
59 | case su::SH("/bitmap"): {
60 | uint16_t w = req.param("width");
61 | uint16_t h = req.param("height");
62 | if (w && h && req.body()) {
63 | // ваш битмап в
64 | // req.body().stream
65 | // req.body()
66 | }
67 | server.send(200);
68 | } break;
69 |
70 | default:
71 | server.send(200);
72 | break;
73 | }
74 | });
75 | }
76 |
77 | void loop() {
78 | server.tick();
79 | }
--------------------------------------------------------------------------------
/webpack.host.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require("webpack");
3 | const PACKAGE = require('./package.json');
4 |
5 | const ReplaceHashInFileWebpackPlugin = require('replace-hash-in-file-webpack-plugin');
6 | const HtmlWebpackPlugin = require('html-webpack-plugin');
7 | const TerserPlugin = require("terser-webpack-plugin");
8 | const MiniCssExtractPlugin = require("mini-css-extract-plugin");
9 | const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
10 | const CopyPlugin = require("copy-webpack-plugin");
11 |
12 | module.exports = {
13 | entry: {
14 | index: './src/index.js',
15 | },
16 |
17 | output: {
18 | filename: 'script.js',
19 | path: path.resolve(__dirname, 'index'),
20 | clean: true,
21 | publicPath: '',
22 | },
23 |
24 | optimization: {
25 | minimize: true,
26 | minimizer: [
27 | new CssMinimizerPlugin(),
28 | new TerserPlugin(),
29 | ],
30 | },
31 |
32 | module: {
33 | rules: [
34 | {
35 | test: /\.css$/,
36 | use: [
37 | MiniCssExtractPlugin.loader,
38 | "css-loader"
39 | ]
40 | },
41 | ]
42 | },
43 |
44 | plugins: [
45 | new HtmlWebpackPlugin({
46 | template: `./src/index.html`,
47 | filename: `index.html`,
48 | favicon: "./src/assets/favicon.svg",
49 | inject: true,
50 | minify: false,
51 | hash: true,
52 | version: PACKAGE.version,
53 | manifest: '',
54 | }),
55 | new MiniCssExtractPlugin({
56 | filename: 'style.css',
57 | }),
58 | new webpack.DefinePlugin({ USE_SW: JSON.stringify(true) }),
59 | new CopyPlugin({
60 | patterns: [
61 | { from: "src/assets/sw.js", to: "" },
62 | { from: "src/assets/manifest.json", to: "" },
63 | { from: "src/assets/icon.png", to: "" },
64 | ],
65 | }),
66 | new ReplaceHashInFileWebpackPlugin([{
67 | dir: 'index',
68 | files: ['sw.js'],
69 | rules: [{
70 | search: /@cachename/,
71 | replace: '[hash]'
72 | }]
73 | }])
74 | ],
75 |
76 | mode: 'production',
77 | };
--------------------------------------------------------------------------------
/gzip_h.js:
--------------------------------------------------------------------------------
1 | const pkg = require('./package.json');
2 | const in_folder = './dist/host';
3 | const out_folder = './dist/gzip';
4 |
5 | const fs = require('node:fs');
6 | const { promisify } = require('node:util');
7 | const { createGzip } = require('node:zlib');
8 | const { pipeline } = require('node:stream');
9 | const pipe = promisify(pipeline);
10 |
11 | async function gzip(input, output) {
12 | const gzip = createGzip();
13 | const source = fs.createReadStream(input);
14 | const destination = fs.createWriteStream(output);
15 | await pipe(source, gzip, destination);
16 | }
17 |
18 | async function compile() {
19 | const index_gz = out_folder + '/index.html.gz';
20 | const script_gz = out_folder + '/script.js.gz';
21 | const style_gz = out_folder + '/style.css.gz';
22 |
23 | try {
24 | if (!fs.existsSync(out_folder)) fs.mkdirSync(out_folder);
25 | await gzip(in_folder + '/index.html', index_gz);
26 | await gzip(in_folder + '/script.js', script_gz);
27 | await gzip(in_folder + '/style.css', style_gz);
28 | } catch (err) {
29 | console.error(err);
30 | return;
31 | }
32 |
33 | let index_len = fs.statSync(index_gz).size;
34 | let script_len = fs.statSync(script_gz).size;
35 | let style_len = fs.statSync(style_gz).size;
36 |
37 | let code = `#pragma once
38 | #include
39 |
40 | /*
41 | ${pkg.name}.h v${pkg.version}
42 | index: ${index_len} bytes
43 | script: ${script_len} bytes
44 | style: ${style_len} bytes
45 | total: ${((index_len + script_len + style_len) / 1024).toFixed(2)} kB
46 |
47 | Build: ${new Date()}
48 | */
49 | `;
50 |
51 | function addBin(fname, gzip) {
52 | let data = fs.readFileSync(gzip).toString('hex');
53 | let code = '\r\n' + `const uint8_t ${pkg.name}_${fname}[] PROGMEM = {`;
54 | for (let i = 0; i < data.length; i += 2) {
55 | if (i % 48 == 0) code += '\r\n ';
56 | code += '0x' + data[i] + data[i + 1];
57 | if (i < data.length - 2) code += ', ';
58 | }
59 | code += '\r\n};\r\n'
60 | code += `const size_t ${pkg.name}_${fname}_len = ${data.length / 2};`;
61 | code += '\r\n'
62 | return code;
63 | }
64 |
65 | code += addBin('index', index_gz);
66 | code += addBin('script', script_gz);
67 | code += addBin('style', style_gz);
68 |
69 | fs.writeFile(`${out_folder}/${pkg.name}.h`, code, err => {
70 | if (err) console.error(err);
71 | else console.log('Done!');
72 | });
73 | }
74 |
75 | compile();
--------------------------------------------------------------------------------
/src/scripts/lang.js:
--------------------------------------------------------------------------------
1 | export const lang = [
2 | {
3 | base: {
4 | file: 'Image file',
5 | link: 'Image link',
6 | name: 'Name',
7 | width: 'Width',
8 | height: 'Height',
9 | fit: 'Fit',
10 | rotate: 'Rotate',
11 | scale: 'Scale',
12 | mode: 'Mode',
13 | process: 'Process',
14 | result: 'Result',
15 | code: 'Code',
16 | copy: 'Copy',
17 | header: '.h',
18 | bin: 'bin',
19 | bulk: 'Bulk encode',
20 | send: 'Send',
21 | info: 'Info',
22 | bulk_info: 'Select multiple files first',
23 | },
24 | filters: {
25 | display: 'Display style',
26 | grid: 'Grid',
27 | invert_b: 'Invert background',
28 | preview: 'Preview',
29 | invert: 'Invert',
30 | brightness: 'Brightness',
31 | contrast: 'Contrast',
32 | saturate: 'Saturate',
33 | blur: 'Blur',
34 | edges: 'Edges',
35 | sobel: 'Sobel edges',
36 | dither: 'Dithering',
37 | threshold: 'Threshold',
38 | median: 'Median edges',
39 | editor: 'Editor',
40 | png: 'Save .png',
41 | reset: 'Reset',
42 | },
43 | display: ['Screen', 'Paper']
44 | },
45 | {
46 | base: {
47 | file: 'Файл',
48 | link: 'Ссылка',
49 | name: 'Имя',
50 | width: 'Ширина',
51 | height: 'Высота',
52 | fit: 'Центрировать',
53 | rotate: 'Повернуть',
54 | scale: 'Масштаб',
55 | mode: 'Режим',
56 | process: 'Процесс',
57 | result: 'Результат',
58 | code: 'Код',
59 | copy: 'Копировать',
60 | header: '.h',
61 | bin: 'bin',
62 | bulk: 'Массовая конвертация',
63 | send: 'Отправить',
64 | info: 'Инфо',
65 | bulk_info: 'Сначала выбери несколько файлов',
66 | },
67 | filters: {
68 | display: 'Стиль отображения',
69 | grid: 'Сетка',
70 | invert_b: 'Инвертировать фон',
71 | preview: 'Предпросмотр',
72 | invert: 'Инвертировать',
73 | brightness: 'Яркость',
74 | contrast: 'Контраст',
75 | saturate: 'Насыщенность',
76 | blur: 'Размытие',
77 | edges: 'Усилить края',
78 | sobel: 'Выделить края',
79 | dither: 'Dithering',
80 | threshold: 'Порог',
81 | median: 'Выделить край',
82 | editor: 'Редактор',
83 | png: 'Сохранить .png',
84 | reset: 'Сброс',
85 | },
86 | display: ['Дисплей', 'Бумага']
87 | }
88 | ];
--------------------------------------------------------------------------------
/src/scripts/filters.js:
--------------------------------------------------------------------------------
1 | import { constrain, round } from "./utils";
2 |
3 | export function threshold(arr, val) {
4 | for (let i = 0; i < arr.length; i++) {
5 | arr[i] = (arr[i] < val) ? 0 : 255;
6 | }
7 | return arr;
8 | }
9 |
10 | export function dither(arr, w, h) {
11 | for (let y = 0; y < h; y++) {
12 | for (let x = 0; x < w; x++) {
13 | let idx = y * w + x;
14 | let col = (arr[idx] < 128 ? 0 : 255);
15 | let err = arr[idx] - col;
16 | arr[idx] = col;
17 | if (x + 1 < w) arr[idx + 1] += (err * 7) >> 4;
18 | if (y + 1 == h) continue;
19 |
20 | if (x > 0) arr[idx + w - 1] += (err * 3) >> 4;
21 | arr[idx + w] += (err * 5) >> 4;
22 | if (x + 1 < w) arr[idx + w + 1] += (err * 1) >> 4;
23 | }
24 | }
25 | }
26 |
27 | export function edges_simple(arr, w, h) {
28 | const kernel = [[-1, -1, -1], [-1, 9, -1], [-1, -1, -1]];
29 | let t = [...arr];
30 |
31 | for (let x = 1; x < w - 1; x++) {
32 | for (let y = 1; y < h - 1; y++) {
33 | let sum = 0;
34 | for (let kx = -1; kx <= 1; kx++) {
35 | for (let ky = -1; ky <= 1; ky++) {
36 | let val = t[(x + kx) + (y + ky) * w];
37 | sum += kernel[ky + 1][kx + 1] * val;
38 | }
39 | }
40 | arr[x + y * w] = round(constrain(sum, 0, 255));
41 | }
42 | }
43 | }
44 |
45 | export function edges_median(arr, w, h) {
46 | // let kernel = [[0, 0], [0, 1], [0, -1], [1, 0], [-1, 0]];
47 | let kernel = [[0, 0], [0, 1], [1, 0]];
48 | let t = [...arr];
49 |
50 | for (let x = 0; x < w; x++) {
51 | for (let y = 0; y < h; y++) {
52 | if (!((x == 0) || (x == w - 1) || (y == 0) || (y == h - 1))) {
53 | let sum = [];
54 | for (let i = 0; i < kernel.length; i++) {
55 | sum.push(t[(x + kernel[i][0]) + (y + kernel[i][1]) * w]);
56 | }
57 | sum.sort();
58 | let v = sum[sum.length - 1] - sum[0];
59 | arr[x + y * w] = round(constrain(v, 0, 255));
60 | }
61 | }
62 | }
63 | }
64 |
65 | export function edges_sobel(arr, w, h, k) {
66 | const kernel_x = [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]];
67 | const kernel_y = [[-1, -2, -1], [0, 0, 0], [1, 2, 1]];
68 | let t = [...arr];
69 |
70 | for (let x = 0; x < w; x++) {
71 | for (let y = 0; y < h; y++) {
72 | let sum_x = 0;
73 | let sum_y = 0;
74 |
75 | if (!((x == 0) || (x == w - 1) || (y == 0) || (y == h - 1))) {
76 | for (let kx = -1; kx <= 1; kx++) {
77 | for (let ky = -1; ky <= 1; ky++) {
78 | let val = arr[(x + kx) + (y + ky) * w];
79 | sum_x += kernel_x[ky + 1][kx + 1] * val;
80 | sum_y += kernel_y[ky + 1][kx + 1] * val;
81 | }
82 | }
83 | }
84 |
85 | let sum = Math.sqrt(sum_x ** 2 + sum_y ** 2);
86 | t[x + y * w] = constrain(sum, 0, 255);
87 | }
88 | }
89 |
90 | for (let i = 0; i < arr.length; i++) {
91 | let val = arr[i] * (1 - k) + t[i] * k;
92 | arr[i] = round(val);
93 | }
94 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Bitmaper
2 | Программа для преобразования изображений в bitmap
3 |
4 | ## Как запустить
5 | - Веб-версия - https://alexgyver.github.io/Bitmaper/, можно установить как веб-приложение
6 | - Релиз [HTML версия](https://github.com/AlexGyver/Bitmaper/releases/latest/download/bitmaper.html) - открывать в браузере
7 | - Релиз [.h версия](https://github.com/AlexGyver/Bitmaper/releases/latest/download/bitmaper.h) - для вставки в ESP проект с сервером. Смотри пример в папке arduino
8 |
9 | ## Как собрать из исходников
10 | - Установить [VS Code](https://code.visualstudio.com/download)
11 | - Установить [Node JS](https://nodejs.org/en/download/prebuilt-installer)
12 | - Открыть папку в VS Code
13 | - Консоль **Ctrl + `**
14 | - `npm install`, дождаться установки зависимостей
15 | - `npm run build` или запустить скрипт *build.bat*
16 | - Проект соберётся в папку dist
17 |
18 | ## Описание
19 | ### Режимы изображения
20 | - `Mono` - монохромный (чёрно-белый)
21 | - `Gray` - оттенки серого
22 | - `RGB` - полноцветное изображение
23 |
24 | ### Алгоритмы кодирования
25 | - `1x pix/byte` - 1 пиксель в байте, строками слева направо сверху вниз: [data_0, ...data_n]
26 | - `8x Horizontal` - 8 пикселей в байте горизонтально (MSB слева), строками слева направо сверху вниз: [data_0, ...data_n]
27 | - `8x Vertical Col` - 8 пикселей в байте вертикально (MSB снизу), столбцами сверху вниз слева направо: [data_0, ...data_n]
28 | - `8x Vertical Row` - 8 пикселей в байте вертикально (MSB снизу), строками слева направо сверху вниз: [data_0, ...data_n] **Подходит для GyverOLED**
29 | - `GyverGFX BitMap` - для [GyverGFX2](https://github.com/GyverLibs/GyverGFX2), 8 пикселей вертикально (MSB снизу), столбцами сверху вниз слева направо: [widthLSB, widthMSB, heightLSB, heightMSB, data_0, ...data_n]
30 | - `GyverGFX BitPack` - для [GyverGFX2](https://github.com/GyverLibs/GyverGFX2), сжатый формат*: [widthLSB, widthMSB, heightLSB, heightMSB, data_0, ...data_n]
31 | - `GyverGFX Image` - для [GyverGFX2](https://github.com/GyverLibs/GyverGFX2), программа выберет лёгкий между BitMap и BitPack: [0 map | 1 pack, x, x, x, x, data_0, ...data_n]
32 | - `Grayscale` - 1 пиксель в байте, оттенки серого
33 | - `RGB888` - 1 пиксель на 3 байта (24 бит RGB): [r0, g0, b0, ...]
34 | - `RGB565` - 1 пиксель на 2 байта (16 бит RGB): [rrrrrggggggbbbbb, ...] тип `uint16_t`
35 | - `RGB233` - 1 пиксель в байте (8 бит RGB): [rrgggbbb, ...]
36 |
37 | \* На изображениях со сплошными участками BitPack может быть в разы эффективнее обычного BitMap. На изображениях с dithering работает неэффективно.
38 |
39 | > Как кодирует BitPack: младший бит - состояние пикселя, остальные - количество. Сканирование идёт столбцами сверху вниз слева направо. Один чанк - 6 бит (состояние + 5 бит количества), 4 чанка пакуются в три байта как aaaaaabb, bbbbcccc, ccdddddd
40 |
41 | ### Массовая конвертация
42 | Программа поддерживает конвертацию нескольких изображений сразу, удобно для создания анимаций:
43 | - Выбрать несколько файлов или перетащить их на кнопку выбора файлов. Отобразится первый файл
44 | - Настроить параметры кодирования и фильтры, они будут применены ко всем остальным файлам
45 | - Нажать "Массовая конвертация", дождаться окончания
46 | - Результат появится в окне вывода кода. Изображения будут иметь суффиксы с номером, также прогармма составит из них список
47 |
48 | #### Пример вывода на примере GyverOLED
49 | ```cpp
50 | // массивы...........
51 |
52 | const uint16_t bitmap_list_size = 3;
53 |
54 | const uint8_t* const bitmap_list_pgm[] PROGMEM = {
55 | bitmap_0_64x64, bitmap_1_64x64, bitmap_2_64x64
56 | };
57 |
58 | const uint8_t* const bitmap_list[] = {
59 | bitmap_0_64x64, bitmap_1_64x64, bitmap_2_64x64
60 | };
61 |
62 | // ........
63 |
64 | for (int i = 0; i < bitmap_list_size; i++) {
65 | oled.clear();
66 | oled.drawBitmap(0, 0, bitmap_list[i], 64, 64); // список из RAM
67 | //oled.drawBitmap(0, 0, (const uint8_t*)pgm_read_ptr(bitmap_list_pgm + i), 64, 64); // список из PGM
68 | oled.update();
69 | delay(500);
70 | }
71 | ```
72 |
73 | ### Редактор (Editor)
74 | - Действия кнопок мыши при включенном редакторе: ЛКМ - добавить точку, ПКМ - стереть точку, СКМ - отменить изменения на слое редактора
75 | - При изменении размера битмапа, при перемещении и масштабировании изображения слой редактора очищается
76 |
77 | ### Прочее
78 | - Активный пиксель на выбранном стиле отображения: `OLED` - голубой, `Paper` - чёрный
79 | - При открытии приложения с локального сервера (IP адрес в строке адреса), например с esp, появится кнопка Send - при нажатии приложение отправит битмап в выбранном формате через formData на url /bitmap с query string параметрами width и height, т.е. `/bitmap?width=N&height=N`
80 |
--------------------------------------------------------------------------------
/index/style.css:
--------------------------------------------------------------------------------
1 | body,html{background-color:#1c2128;height:100%;margin:0;padding:0;width:100%}.filters{float:right;height:100%;position:relative;width:200px}.cv_cont{margin:0 220px;padding-top:30px}.cv_cont,.cv_inner{display:flex;justify-content:center}.cv_inner{max-width:800px;width:100%}.canvas{background:#000;border-radius:5px}.canvas,.info_cont{cursor:pointer;display:block}.info_cont{background:#0009;height:100%;left:0;position:absolute;top:0;width:100%;z-index:3}.info_inner{background:#2d333b;border-radius:7px;color:#ccc;cursor:default;font-family:system-ui;margin:50px auto auto;max-width:900px;padding:15px}.info_inner span{display:block;text-wrap:wrap!important;white-space:pre}.info_inner button{background:none;border:none;color:red;cursor:pointer;float:right;font-size:25px}.ui_main.theme-light{--border:#aaa;--back:#fff;--mid:#ccc;--bright:#eee;--font:#000;--font-mid:#555}.ui_main.theme-dark{--border:#444c56;--back:#1e232a;--mid:#22272e;--bright:#2d333b;--font:#ccc;--font-mid:#999}.ui_main{background-color:var(--mid);border:none;box-shadow:5px 5px 8px rgba(0,0,0,.35);font:12px sans-serif;text-align:left;user-select:none;-webkit-user-select:none}.ui_content{background:var(--mid);overflow-y:auto}.ui_title_bar{font-weight:700;user-select:none;-webkit-user-select:none}.ui_container,.ui_title_bar{background:var(--bright);border:none;color:var(--font);padding:5px}.ui_container{margin:5px;position:relative}.ui_space{height:1px}.ui_range{-webkit-appearance:none;-moz-appearance:none;background-color:transparent;border:none;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;height:17px;margin:0;padding:0;width:100%}.ui_range:focus{border:none;outline:none}.ui_range::-webkit-slider-runnable-track{background:var(--back);-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;cursor:pointer;height:15px;width:100%}.ui_range:focus::-webkit-slider-runnable-track{background:var(--back)}.ui_range::-webkit-slider-thumb{-webkit-appearance:none;background:var(--border);-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;cursor:pointer;height:15px;margin-top:0;width:15px}.ui_range::-moz-range-track{background:var(--back);-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;cursor:pointer;height:15px;width:100%}.ui_range::-moz-range-thumb{background:var(--border);border:none;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;cursor:pointer;height:15px;width:15px}.ui_button{background:var(--mid);border:1px solid var(--border);color:var(--font-mid);cursor:pointer;font:12px sans-serif;height:26px;margin:2px}.ui_button:active{background:var(--back)}.ui_checkbox{cursor:pointer;display:inline}.ui_checkbox input{left:-99999px;position:absolute}.ui_checkbox span{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAALklEQVQ4T2OcOXPmfwYKACPIgLS0NLKMmDVrFsOoAaNhMJoOGBioFwZkZUWoJgApdFaxjUM1YwAAAABJRU5ErkJggg==) no-repeat;display:block;height:16px;text-indent:20px;width:100%}.ui_checkbox input:checked+span{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAvElEQVQ4T63Tyw2EIBAA0OFKBxBL40wDRovAUACcKc1IB1zZDAkG18GYZTmSmafzgTnnMgwchoDWGlJKheGcP3JtnPceCqCUAmttSZznuYtgchsXQrgC+77DNE0kUpPbmBOoJaBOIVQylnqWgAAeKhDve/AN+EaklJBzhhgjWRoJVGTbNjiOowAIret6a+4jYIwpX8aDwLIs74C2D0IIYIyVP6Gm898m9kbVm85ljHUTf16k4VUefkwDrxk+zoUEwCt0GbUAAAAASUVORK5CYII=) no-repeat}.ui_checkbox_label{left:30px;pointer-events:none;position:absolute;top:7px}.ui_label{cursor:default;font:12px sans-serif;margin-bottom:3px;user-select:none;-webkit-user-select:none}.ui_text_input{border:1px inset var(--border);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;font-size:12px;outline:none;padding:0 0 0 5px}.ui_select,.ui_text_input{background:var(--back);color:var(--font-mid);height:24px;width:100%}.ui_select{-webkit-appearance:none;-moz-appearance:none;appearance:none;border:1px solid var(--border);-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;cursor:pointer;-moz-outline:none;padding:0 5px}.ui_select,.ui_select option{font-size:14px}.ui_select:focus{outline:none}.ui_number{height:24px}.ui_textarea{background:var(--back);border:1px inset var(--border);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;color:var(--font-mid);font-size:12px;outline:none;padding:3px 5px;resize:vertical;width:100%}.ui_textarea::-webkit-scrollbar{height:7px;width:7px}.ui_textarea::-webkit-scrollbar-track{background:none}.ui_textarea::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}.ui_color{background:none;border:none;cursor:pointer;height:30px;margin:0;outline:none;padding:0;width:100%}.ui_file_chooser{left:-999999px;position:absolute}.ui_file_chooser_label{background:var(--back);border:1px solid var(--border);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;color:var(--font);cursor:pointer;display:block;font:12px sans-serif;height:30px;overflow:hidden;padding:7px;text-overflow:ellipsis;white-space:nowrap;width:100%}.ui_file_chooser_label.active{border:2px solid var(--font)}
--------------------------------------------------------------------------------
/src/scripts/imageloader.js:
--------------------------------------------------------------------------------
1 | import CyrillicToTranslit from 'cyrillic-to-translit-js';
2 | import { RGBtoHEX } from './utils';
3 |
4 | export default class ImageLoader {
5 | name = '';
6 | image;
7 | first = false;
8 | offset = { x: 0, y: 0, dx: 0, dy: 0, w: 0 };
9 |
10 | async load(img) {
11 | return new Promise((res, rej) => {
12 | const image = new Image();
13 | image.crossOrigin = "Anonymous";
14 |
15 | image.addEventListener('load', () => {
16 | if (image.width && image.height) {
17 | this.image = image;
18 | this.first = true;
19 | res();
20 | } else {
21 | rej("Image error");
22 | }
23 | });
24 | image.addEventListener('error', () => {
25 | rej("Image load error");
26 | });
27 |
28 | switch (typeof img) {
29 | case 'object':
30 | if (!img.type.toString().includes('image')) {
31 | rej("Not an image");
32 | }
33 | image.src = URL.createObjectURL(img);
34 | this.name = this._getName(img.name);
35 | break;
36 |
37 | case 'string':
38 | if (!img.startsWith('http')) {
39 | rej("Not a link");
40 | }
41 | image.src = img;
42 | this.name = this._getName(img);
43 | break;
44 |
45 | default:
46 | rej("Image error");
47 | break;
48 | }
49 | });
50 | }
51 |
52 | scale(dir) {
53 | if (!this.image) return;
54 | this.offset.w += dir * this.offset.w / 64;
55 | }
56 |
57 | pan(dx, dy, stop) {
58 | if (!this.image) return;
59 | this.offset.dx = dx;
60 | this.offset.dy = dy;
61 | if (stop) {
62 | this.offset.x -= this.offset.dx;
63 | this.offset.dx = 0;
64 | this.offset.y -= this.offset.dy;
65 | this.offset.dy = 0;
66 | }
67 | }
68 |
69 | show(cvbase, angle, background, filters) {
70 | if (!this.image) return;
71 | this._render(cvbase.cv, angle, background, filters);
72 | cvbase.grid();
73 | }
74 |
75 | fit(cvbase) {
76 | this.offset = { x: 0, y: 0, dx: 0, dy: 0, w: 0 };
77 | let w = cvbase.cv.width;
78 | let h = w * this.image.height / this.image.width;
79 | if (h > cvbase.cv.height) {
80 | w *= cvbase.cv.height / h;
81 | h = cvbase.cv.height;
82 | }
83 | this.offset.w = w / cvbase.cv.width;
84 | }
85 |
86 | setOffset(offset) {
87 | this.first = false;
88 | this.offset = offset;
89 | }
90 |
91 | render(cvbase, angle, background, filters) {
92 | if (!this.image) return;
93 | if (this.first) {
94 | this.first = false;
95 | this.fit(cvbase);
96 | }
97 |
98 | // copy of display canvas
99 | let cv = document.createElement("canvas");
100 | cv.width = cvbase.cv.width;
101 | cv.height = cvbase.cv.height;
102 | let cx = cv.getContext("2d");
103 | cx.fillStyle = background ? 'white' : 'black';
104 | cx.fillRect(0, 0, cv.width, cv.height);
105 | this._render(cv, angle, background, filters);
106 |
107 | // pixel to pixel output
108 | let cv2 = document.createElement("canvas");
109 | cv2.width = cvbase.W;
110 | cv2.height = cvbase.H;
111 | let cx2 = cv2.getContext("2d");
112 |
113 | cx2.drawImage(cv, 0, 0, cv2.width, cv2.height);
114 |
115 | cvbase.clear();
116 | let data = cx2.getImageData(0, 0, cvbase.W, cvbase.H).data;
117 | for (let i = 0, j = 0; i < data.length; i += 4, j++) {
118 | if (data[i + 3]) { // alpha
119 | cvbase.matrix[j] = RGBtoHEX(data[i], data[i + 1], data[i + 2]);
120 | }
121 | }
122 | }
123 |
124 | _render(dest, angle, background, filters) {
125 | let cv = document.createElement("canvas");
126 | let w = dest.width * this.offset.w;
127 | let h = w * this.image.height / this.image.width;
128 | let hypot = Math.sqrt(w ** 2 + h ** 2);
129 | cv.width = hypot;
130 | cv.height = hypot;
131 | let cx = cv.getContext("2d");
132 |
133 | cx.save();
134 | cx.translate(hypot / 2, hypot / 2);
135 | cx.rotate(-angle * 0.0174533);
136 | cx.filter = `invert(${filters.invert ?? 0}) brightness(${filters.brightness ?? 0}%) contrast(${filters.contrast ?? 0}%) saturate(${filters.saturate ?? 0}%) grayscale(${filters.grayscale ?? 0}%) blur(${(filters.blur ?? 0) * hypot / 64}px)`;
137 | cx.drawImage(this.image, -w / 2, -h / 2, w, h);
138 | cx.restore();
139 |
140 | let cxd = dest.getContext("2d");
141 | cxd.fillStyle = background ? 'white' : 'black';
142 | cxd.fillRect(0, 0, dest.width, dest.height);
143 | cxd.drawImage(
144 | cv,
145 | (this.offset.x - this.offset.dx) + (hypot - dest.width) / 2,
146 | (this.offset.y - this.offset.dy) + (hypot - dest.height) / 2,
147 | dest.width, dest.height,
148 | 0, 0, dest.width, dest.height
149 | );
150 | }
151 |
152 | _getName(str) {
153 | const translit = new CyrillicToTranslit();
154 | str = translit.transform(str, '_');
155 | str = str.substring(str.lastIndexOf('/') + 1, str.lastIndexOf('.')).replaceAll('-', '_').substring(0, 10);
156 | if (!str.length) str = 'bitmap';
157 | else if (str[0] >= '0' && str[0] <= '9') str = 'b' + str;
158 | return str;
159 | }
160 | }
--------------------------------------------------------------------------------
/src/scripts/canvas.js:
--------------------------------------------------------------------------------
1 | import Matrix from "./matrix";
2 |
3 | export default class CanvasMatrix extends Matrix {
4 | constructor(cv, onclick, ondrag, onwheel, active = '#478be6', back = 0) {
5 | super();
6 |
7 | if (!cv) return;
8 |
9 | this.cv = cv;
10 | this.cx = cv.getContext("2d");
11 | this.setColors(active, back);
12 |
13 | this._onclick = onclick;
14 | this._ondrag = ondrag;
15 | this._onwheel = onwheel;
16 |
17 | window.addEventListener('resize', () => this._resize());
18 | cv.addEventListener("mousedown", e => this._onMouseDown(e));
19 | cv.addEventListener("mousewheel", e => this._onMouseWheel(e));
20 | cv.addEventListener("contextmenu", e => e.preventDefault());
21 | document.addEventListener("mousemove", e => this._onMouseMove(e));
22 | document.addEventListener("mouseup", e => this._onMouseUp(e));
23 | }
24 |
25 | setColors(active, back) {
26 | this.active = active;
27 | this.back = back;
28 | this.cx.strokeStyle = back ? 'white' : 'black';
29 | }
30 |
31 | setMode(mode) {
32 | this.mode = mode;
33 | }
34 |
35 | resize(w, h) {
36 | super.resize(w, h);
37 | this._resize();
38 | }
39 |
40 | render(grid) {
41 | this._grid = grid;
42 | this.cx.fillStyle = this.back ? 'white' : 'black';
43 | this.cx.fillRect(0, 0, this.cv.width, this.cv.height);
44 |
45 | let b = this._blocksize;
46 | this.cx.fillStyle = this.active;
47 |
48 | for (let x = 0; x < this.W; x++) {
49 | for (let y = 0; y < this.H; y++) {
50 | let v = this.get(x, y);
51 | if (v) {
52 | switch (this.mode) {
53 | case 0:
54 | break;
55 | case 1:
56 | this.cx.fillStyle = this.active + v.toString(16).padStart(2, 0);
57 | break;
58 | case 2:
59 | this.cx.fillStyle = '#' + v.toString(16).padStart(6, 0);
60 | break;
61 | }
62 |
63 | this.cx.fillRect(x * b, y * b, b, b);
64 |
65 | if (grid && b > this._minsize) {
66 | this.cx.strokeRect(x * b, y * b, b, b);
67 | }
68 | }
69 | }
70 | }
71 | }
72 |
73 | grid() {
74 | let b = this._blocksize;
75 | if (b < this._minsize) return;
76 | for (let x = 0; x < this.W; x++) {
77 | this.cx.beginPath();
78 | this.cx.moveTo(x * b, 0);
79 | this.cx.lineTo(x * b, this.cv.height);
80 | this.cx.stroke();
81 | }
82 | for (let y = 0; y < this.H; y++) {
83 | this.cx.beginPath();
84 | this.cx.moveTo(0, y * b);
85 | this.cx.lineTo(this.cv.width, y * b);
86 | this.cx.stroke();
87 | }
88 | }
89 |
90 | _blocksize = 0;
91 | _scale = /*window.devicePixelRatio*/ 1;
92 | _pressed = false;
93 | _dragged = false;
94 | _pressXY = [];
95 | _minsize = 3;
96 | _realW = 0;
97 | _realH = 0;
98 | _grid = 1;
99 | _maxwidth = 800;
100 |
101 | // 0 mono, 1 gray, 2 rgb
102 | mode = 0;
103 |
104 | _resize() {
105 | if (!this.cv) return;
106 |
107 | let w = this.cv.parentNode.clientWidth;
108 | w = Math.floor(w / this.W) * this.W;
109 | if (!w) w = this.W;
110 | let h = w * this.H / this.W;
111 | this._blocksize = w / this.W;
112 |
113 | this.cv.width = w;
114 | this.cv.height = h;
115 | this.cx.lineWidth = this._blocksize / 64;
116 |
117 | w = this.cv.parentNode.clientWidth;
118 | h = w * this.H / this.W;
119 | if (h > this._maxwidth) {
120 | h = this._maxwidth;
121 | w = h * this.W / this.H;
122 | }
123 | this._realW = w;
124 | this._realH = h;
125 | this.cv.style.width = w + 'px';
126 | this.cv.style.height = h + 'px';
127 | this.render(this._grid);
128 | }
129 | _onMouseWheel(e) {
130 | this._onwheel(e.deltaY < 0 ? 1 : -1);
131 | }
132 | _onMouseDown(e) {
133 | this._pressed = true;
134 | this._dragged = false;
135 | this._pressXY = this._getXY(e);
136 | }
137 | _onMouseMove(e) {
138 | if (!this._pressed) return;
139 | if (!this._dragged) {
140 | this.cv.style.cursor = 'grab';
141 | document.body.style.cursor = 'grab';
142 | }
143 |
144 | this._dragged = true;
145 | let xy = this._getXY(e);
146 | this._ondrag(this._drag(xy, false), { 1: 0, 4: 1, 2: 2 }[e.buttons]);
147 | }
148 | _onMouseUp(e) {
149 | if (!this._pressed) return;
150 |
151 | this._pressed = false;
152 | let xy = this._getXY(e);
153 | if (this._dragged) {
154 | this.cv.style.cursor = 'pointer';
155 | document.body.style.cursor = 'default';
156 | this._ondrag(this._drag(xy, true), e.button);
157 | } else {
158 | this._onclick(this._blockXY(xy), e.button);
159 | }
160 | this._dragged = false;
161 | }
162 | _getXY(e) {
163 | let x = e.pageX;
164 | let y = e.pageY;
165 | if (this.cv.offsetParent.tagName.toUpperCase() === "BODY") {
166 | x -= this.cv.offsetLeft;
167 | y -= this.cv.offsetTop;
168 | } else {
169 | x -= this.cv.offsetParent.offsetLeft;
170 | y -= this.cv.offsetParent.offsetTop;
171 | }
172 | x *= this.cv.width / this._realW;
173 | y *= this.cv.height / this._realH;
174 | return { x: x, y: y }
175 | }
176 | _blockXY(xy) {
177 | return { x: Math.floor(xy.x / this.cv.width * this.W), y: Math.floor(xy.y / this.cv.height * this.H) };
178 | }
179 | _drag(xy, release) {
180 | return { dx: xy.x - this._pressXY.x, dy: xy.y - this._pressXY.y, block: this._blockXY(xy), release: release };
181 | }
182 | };
--------------------------------------------------------------------------------
/src/scripts/processor.js:
--------------------------------------------------------------------------------
1 | import { HEXtoRGB } from "./utils";
2 |
3 | //#region processes
4 | export const processes = {
5 | names: [
6 | '1 pix/byte',
7 | '8x Horizontal',
8 | '8x Vertical Col',
9 | '8x Vertical Row',
10 | 'GyverGFX Image',
11 | 'GyverGFX BitMap',
12 | 'GyverGFX BitPack',
13 | 'Grayscale',
14 | 'RGB888',
15 | 'RGB565',
16 | 'RGB233',
17 | ],
18 | prefix: [
19 | 'const uint8_t',
20 | 'const uint8_t',
21 | 'const uint8_t',
22 | 'const uint8_t',
23 | 'gfximage_t',
24 | 'gfxmap_t',
25 | 'gfxpack_t',
26 | 'const uint8_t',
27 | 'const uint8_t',
28 | 'const uint16_t',
29 | 'const uint8_t',
30 | ],
31 | extension: [
32 | '1p',
33 | '8h',
34 | '8vc',
35 | '8vr',
36 | 'img',
37 | 'map',
38 | 'pack',
39 | 'gray',
40 | 'rgb888',
41 | 'rgb565',
42 | 'rgb233',
43 | ],
44 | func: [
45 | make1bit,
46 | make8horiz,
47 | make8vert_col,
48 | make8vert_row,
49 | makeImage,
50 | makeBitmap,
51 | makeBitpack,
52 | makeGray,
53 | makeRGB888,
54 | makeRGB565,
55 | makeRGB233,
56 | ],
57 | mult: [
58 | 1,
59 | 1,
60 | 1,
61 | 1,
62 | 1,
63 | 1,
64 | 1,
65 | 1,
66 | 1,
67 | 2,
68 | 1,
69 | ]
70 | }
71 |
72 | //#region makers
73 | function make1bit(m) {
74 | return m.matrix.map(x => x ? 1 : 0);
75 | }
76 | function make8horiz(m) {
77 | let data = [];
78 | let chunk = Math.ceil(m.W / 8);
79 | for (let y = 0; y < m.H; y++) {
80 | for (let xx = 0; xx < chunk; xx++) {
81 | let byte = 0;
82 | for (let b = 0; b < 8; b++) {
83 | byte <<= 1;
84 | byte |= m.get(xx * 8 + b, y) ? 1 : 0;
85 | }
86 | data.push(byte);
87 | }
88 | }
89 | return data;
90 | }
91 | function make8vert_col(m) {
92 | let data = [];
93 | let chunk = Math.ceil(m.H / 8);
94 | for (let x = 0; x < m.W; x++) {
95 | for (let yy = 0; yy < chunk; yy++) {
96 | let byte = 0;
97 | for (let b = 0; b < 8; b++) {
98 | byte >>= 1;
99 | byte |= (m.get(x, yy * 8 + b) ? 1 : 0) << 7;
100 | }
101 | data.push(byte);
102 | }
103 | }
104 | return data;
105 | }
106 | function make8vert_row(m) {
107 | let data = [];
108 | let chunk = Math.ceil(m.H / 8);
109 | for (let yy = 0; yy < chunk; yy++) {
110 | for (let x = 0; x < m.W; x++) {
111 | let byte = 0;
112 | for (let b = 0; b < 8; b++) {
113 | byte >>= 1;
114 | byte |= (m.get(x, yy * 8 + b) ? 1 : 0) << 7;
115 | }
116 | data.push(byte);
117 | }
118 | }
119 | return data;
120 | }
121 | function makeBitpack(m) {
122 | let data = [(m.W & 0xff), (m.W >> 8) & 0xff, (m.H & 0xff), (m.H >> 8) & 0xff];
123 | let i = 0, value = 0, shift = 0;
124 |
125 | let push = () => {
126 | let chunk = (i << 1) | value;
127 | switch ((shift++) & 0b11) {
128 | case 0:
129 | data.push(chunk << 2);
130 | break;
131 | case 1:
132 | data[data.length - 1] |= chunk >> 4;
133 | data.push((chunk << 4) & 0b11110000);
134 | break;
135 | case 2:
136 | data[data.length - 1] |= chunk >> 2;
137 | data.push((chunk << 6) & 0b11000000);
138 | break;
139 | case 3:
140 | data[data.length - 1] |= chunk;
141 | break;
142 | }
143 | }
144 |
145 | for (let x = 0; x < m.W; x++) {
146 | for (let y = 0; y < m.H; y++) {
147 | let v = m.get(x, y) ? 1 : 0;
148 | if (!i) {
149 | i = 1;
150 | value = v;
151 | } else {
152 | if (value == v) {
153 | i++;
154 | if (i == 31) {
155 | push();
156 | i = 0;
157 | }
158 | } else {
159 | push();
160 | value = v;
161 | i = 1;
162 | }
163 | }
164 | }
165 | }
166 | if (i) push();
167 |
168 | return data;
169 | }
170 | function makeBitmap(m) {
171 | return [(m.W & 0xff), ((m.W >> 8) & 0xff), (m.H & 0xff), ((m.H >> 8) & 0xff)].concat(make8vert_col(m));
172 | }
173 | function makeImage(m) {
174 | let mapsize = Math.ceil(m.H / 8) * m.W + 4;
175 | let pack = makeBitpack(m);
176 | return (mapsize <= pack.length) ? [0].concat(makeBitmap(m)) : [1].concat(pack);
177 | }
178 | function makeGray(m) {
179 | return [...m.matrix];
180 | }
181 | function makeRGB888(m) {
182 | let data = [];
183 | for (let hex of m.matrix) {
184 | data.push(...HEXtoRGB(hex));
185 | }
186 | return data;
187 | }
188 | function makeRGB565(m) {
189 | let data = [];
190 | for (let hex of m.matrix) {
191 | let rgb = HEXtoRGB(hex);
192 | data.push(((rgb[0] & 0b11111000) << 8) | ((rgb[1] & 0b11111100) << 3) | (rgb[2] >> 3));
193 | }
194 | return data;
195 | }
196 | function makeRGB233(m) {
197 | let data = [];
198 | for (let hex of m.matrix) {
199 | let rgb = HEXtoRGB(hex);
200 | data.push((rgb[0] & 0b11000000) | ((rgb[1] & 0b11100000) >> 2) | ((rgb[2] & 0b11100000) >> 5));
201 | }
202 | return data;
203 | }
204 |
205 | //#region export
206 | function makeCodeArray(data, width = 16) {
207 | let code = '';
208 | for (let i = 0; i < data.length; i++) {
209 | if (i % width == 0) code += '\r\n\t';
210 | code += '0x' + data[i].toString(16).padStart(2, 0);
211 | if (i != data.length - 1) code += ', ';
212 | }
213 | return code;
214 | }
215 |
216 | export function makeBlob(m, type) {
217 | let data = processes.func[type](m);
218 | let bytes = Int8Array.from(data);
219 | return new Blob([bytes], { type: "application/octet-stream" });
220 | }
221 |
222 | export function downloadBin(m, type, name) {
223 | let blob = makeBlob(m, type);
224 | let link = document.createElement('a');
225 | link.href = window.URL.createObjectURL(blob);
226 | name += '.' + processes.extension[type];
227 | link.download = name;
228 | link.click();
229 | }
230 |
231 | export function downloadCode(m, type, name) {
232 | let code = makeCode(m, type, name);
233 | let str = `#pragma once
234 | #include
235 | ${(type >= 3 && type <= 5) ? '#include ' : ''}
236 |
237 | // ${name} (${code.size} bytes)
238 | // ${processes.names[type]}
239 |
240 | `;
241 | str += code.code;
242 | downloadTextH(name, str);
243 | }
244 |
245 | export function downloadTextH(name, str) {
246 | let enc = new TextEncoder();
247 | let bytes = enc.encode(str);
248 | let blob = new Blob([bytes], { type: "text/plain" });
249 | let link = document.createElement('a');
250 | link.href = window.URL.createObjectURL(blob);
251 |
252 | link.download = name + '.h';
253 | link.click();
254 | }
255 |
256 | export function makeCode(m, type, name) {
257 | let data = processes.func[type](m);
258 | let codename = `${name}_${m.W}x${m.H}`;
259 | let code = `${processes.prefix[type]} ${codename}[] PROGMEM = {`;
260 | code += makeCodeArray(data, 24);
261 | code += '\r\n};'
262 | return { code: code, size: data.length * processes.mult[type], name: codename };
263 | }
--------------------------------------------------------------------------------
/src/scripts/app.js:
--------------------------------------------------------------------------------
1 | import UI from '@alexgyver/ui'; import CanvasMatrix from './canvas'
2 | import Matrix from './matrix';
3 | import ImageLoader from './imageloader';
4 | import * as proc from './processor';
5 | import Timer from './timer';
6 | import { dither, edges_median, edges_simple, edges_sobel, threshold } from './filters';
7 | import { Component } from '@alexgyver/component';
8 | import { lang } from './lang';
9 |
10 | let base_ui = new UI(), filt_ui = new UI();
11 | const preview_delay = 400;
12 | let canvas = new CanvasMatrix();
13 | let image = new ImageLoader();
14 | let editor = new Matrix();
15 | let timer = new Timer();
16 | let files = null;
17 | let cur_lang = 0;
18 | let prev_s = 0;
19 |
20 | const displayModes = [
21 | { active: '#478be6', back: 0 },
22 | { active: '#000000', back: 1 }
23 | ];
24 |
25 | async function file_h(file) {
26 | if (file instanceof File || typeof file === 'string') {
27 | files = null;
28 | await processFile(file);
29 | } else if (file instanceof FileList) {
30 | files = file;
31 | await processFile(files[0]);
32 | } else {
33 | files = null;
34 | }
35 | }
36 | async function processFile(file) {
37 | try {
38 | await image.load(file);
39 | } catch (e) {
40 | alert(e);
41 | return;
42 | }
43 | editor.clear();
44 | base_ui.name = image.name;
45 | update_h();
46 | }
47 | async function link_h(link) {
48 | if (link.length) {
49 | await file_h(link);
50 | base_ui.link = '';
51 | }
52 | }
53 |
54 | function resize_h() {
55 | let wh = [base_ui.width, base_ui.height];
56 | canvas.resize(wh[0], wh[1]);
57 | editor.resize(wh[0], wh[1]);
58 | if (Math.max(wh[0], wh[1]) > 500) filt_ui.preview = false;
59 | update_h();
60 | }
61 |
62 | function fit_h() {
63 | if (image.image) image.fit(canvas);
64 | update_h();
65 | }
66 |
67 | function update_h() {
68 | update();
69 | render();
70 | }
71 |
72 | function update() {
73 | if (image.image) {
74 |
75 | image.render(
76 | canvas,
77 | base_ui.rotate,
78 | filt_ui.invert_b,
79 | {
80 | invert: displayModes[filt_ui.display].back ^ filt_ui.invert,
81 | brightness: filt_ui.brightness,
82 | contrast: filt_ui.contrast,
83 | saturate: filt_ui.saturate,
84 | blur: filt_ui.blur,
85 | grayscale: base_ui.mode == 2 ? 0 : 100,
86 | }
87 | );
88 |
89 | // grayscale
90 | if (base_ui.mode != 2) {
91 | for (let i = 0; i < canvas.matrix.length; i++) {
92 | canvas.matrix[i] &= 0xff;
93 | }
94 | }
95 |
96 | if (base_ui.mode <= 1) {
97 | if (filt_ui.edges) edges_simple(canvas.matrix, canvas.W, canvas.H);
98 | if (filt_ui.sobel) edges_sobel(canvas.matrix, canvas.W, canvas.H, filt_ui.sobel);
99 | if (filt_ui.dither) dither(canvas.matrix, canvas.W, canvas.H);
100 | if (base_ui.mode == 0) threshold(canvas.matrix, filt_ui.threshold * 2.56);
101 | if (filt_ui.median) edges_median(canvas.matrix, canvas.W, canvas.H);
102 | }
103 | }
104 | }
105 |
106 | function render() {
107 | canvas.merge(editor);
108 | canvas.render(filt_ui.grid);
109 | process();
110 | }
111 |
112 | function process() {
113 | let result = proc.makeCode(canvas, base_ui.process, base_ui.name);
114 | base_ui.code = result.code;
115 |
116 | let info = `${canvas.W}x${canvas.H} (${result.size} bytes)
`;
117 | info += Math.round(canvas.matrix.filter(v => v).length / canvas.matrix.length * 100) + '% pixels on'
118 | base_ui.result = info;
119 | }
120 |
121 | function show() {
122 | image.show(canvas,
123 | base_ui.rotate,
124 | displayModes[filt_ui.display].back ^ filt_ui.invert_b,
125 | {
126 | invert: filt_ui.invert,
127 | }
128 | );
129 | }
130 |
131 | function display_h() {
132 | let colors = displayModes[filt_ui.display];
133 | canvas.setColors(colors.active, colors.back);
134 | update_h();
135 | }
136 |
137 | function mode_h(mode) {
138 | filt_ui.control('edges').display(mode != 2);
139 | filt_ui.control('sobel').display(mode != 2);
140 | filt_ui.control('median').display(mode != 2);
141 |
142 | filt_ui.control('dither').display(mode == 0);
143 | filt_ui.control('threshold').display(mode == 0);
144 | filt_ui.control('editor').display(mode == 0);
145 |
146 | base_ui.process = (mode == 0) ? 0 : (mode == 1 ? 7 : 8);
147 | canvas.setMode(mode);
148 | resetFilt();
149 | reset_h();
150 | update_h();
151 | }
152 |
153 | function process_h(p) {
154 | update_h();
155 | }
156 |
157 | function reset_h() {
158 | base_ui.control('rotate').default();
159 | filt_ui.control('invert_b').default();
160 | filt_ui.control('invert').default();
161 | resetFilt();
162 | editor.clear();
163 | update_h();
164 | }
165 |
166 | function resetFilt() {
167 | filt_ui.control('brightness').default();
168 | filt_ui.control('contrast').default();
169 | filt_ui.control('saturate').default();
170 | filt_ui.control('blur').default();
171 | filt_ui.control('edges').default();
172 | filt_ui.control('sobel').default();
173 | filt_ui.control('dither').default();
174 | filt_ui.control('threshold').default();
175 | filt_ui.control('median').default();
176 | filt_ui.control('editor').default();
177 | }
178 |
179 | // ============== EDITOR ==============
180 | function btn_to_val(btn) {
181 | // 0 lmb, 1 mmb, 2 rmb
182 | return (btn == 0) ? 1 : (btn == 1 ? 0 : -1);
183 | }
184 | function click_h(v, button) {
185 | if (filt_ui.editor) {
186 | editor.set(v.x, v.y, btn_to_val(button));
187 | update_h();
188 | }
189 | }
190 |
191 | // ============== MOUSE ==============
192 | function drag_h(v, button) {
193 | if (filt_ui.editor) {
194 | editor.set(v.block.x, v.block.y, btn_to_val(button));
195 | update_h();
196 | return;
197 | }
198 |
199 | editor.clear();
200 | image.pan(v.dx, v.dy, v.release);
201 | if (filt_ui.preview || v.release) {
202 | update_h();
203 | } else {
204 | show();
205 | }
206 | }
207 | function scale_h(v) {
208 | wheel_h(prev_s < v ? 1 : -1);
209 | prev_s = v;
210 | }
211 | function wheel_h(v) {
212 | if (filt_ui.editor) return;
213 | timer.stop();
214 | editor.clear();
215 | image.scale(v);
216 |
217 | if (filt_ui.preview) {
218 | update_h();
219 | } else {
220 | show();
221 | timer.start(update_h, preview_delay);
222 | }
223 | }
224 | function rotate_h() {
225 | editor.clear();
226 |
227 | if (filt_ui.preview) {
228 | update_h();
229 | } else {
230 | show();
231 | timer.start(update_h, preview_delay);
232 | }
233 | }
234 |
235 | function sleep(time) {
236 | return new Promise(res => setTimeout(res, time));
237 | }
238 |
239 | async function bulk_h() {
240 | if (files == null) {
241 | alert(lang[cur_lang].base.bulk_info);
242 | return;
243 | }
244 |
245 | let str = '';
246 | let idx = 0;
247 | let size = 0;
248 | let process = base_ui.process;
249 | let name = base_ui.name;
250 | let names = '';
251 | let offset = Object.assign({}, image.offset);
252 |
253 | for (let file of files) {
254 | base_ui.result = 'Bulk process: ' + Math.round(idx / files.length * 100) + '%';
255 | try {
256 | await image.load(file);
257 | } catch (e) {
258 | alert(e);
259 | return;
260 | }
261 | editor.clear();
262 | image.setOffset(offset);
263 | update();
264 | canvas.merge(editor);
265 | canvas.render(filt_ui.grid);
266 |
267 | let result = proc.makeCode(canvas, process, name + '_' + idx);
268 | if (idx) {
269 | names += ', ';
270 | if (idx % 8 == 0) names += '\r\n\t';
271 | }
272 | names += result.name;
273 | idx++;
274 | size += result.size;
275 | str += result.code;
276 | str += '\r\n\r\n';
277 | }
278 | str += `const uint16_t ${name}_list_size = ${idx};\r\n\r\n`;
279 | str += `${proc.processes.prefix[process]}* const ${name}_list_pgm[] PROGMEM = {\r\n\t${names}\r\n};\r\n\r\n`;
280 | str += `${proc.processes.prefix[process]}* const ${name}_list[] = {\r\n\t${names}\r\n};\r\n\r\n`;
281 |
282 | base_ui.code = str;
283 | base_ui.result = 'Encoded ' + idx + ' files
' + (size / 1024.0).toFixed(2) + ' kB';
284 | return str;
285 | }
286 |
287 | async function bulkH_h() {
288 | let bulk = await bulk_h();
289 | if (!bulk) return;
290 |
291 | let str = `#pragma once
292 | #include
293 | ${(base_ui.process >= 3 && base_ui.process <= 5) ? '#include ' : ''}
294 |
295 | // Bitmaper bulk process ${files.length} files
296 |
297 | `;
298 | str += bulk;
299 | proc.downloadTextH('bulk', str);
300 | }
301 |
302 | // ============== SAVE ==============
303 | function copy_h() {
304 | navigator.clipboard.writeText(base_ui.code);
305 | }
306 | function saveH_h() {
307 | proc.downloadCode(canvas, base_ui.process, base_ui.name);
308 | }
309 | function saveBin_h() {
310 | proc.downloadBin(canvas, base_ui.process, base_ui.name);
311 | }
312 | function send_h() {
313 | let blob = proc.makeBlob(canvas, base_ui.process);
314 | let formData = new FormData();
315 | formData.append('bitmap', blob);
316 |
317 | fetch(window.location.href + `bitmap?width=${canvas.W}&height=${canvas.H}`, {
318 | method: 'POST',
319 | body: formData
320 | });
321 | }
322 | function png_h() {
323 | let link = document.createElement('a');
324 | link.href = canvas.cv.toDataURL('image/png');
325 | link.download = base_ui.name + '.png';
326 | link.click();
327 | }
328 |
329 | function lang_h(v) {
330 | base_ui.lang = v;
331 | base_ui.setLabels(lang[v].base);
332 | filt_ui.setLabels(lang[v].filters);
333 | filt_ui.control('display').options = lang[v].display;
334 | cur_lang = v;
335 | }
336 |
337 | function git_h() {
338 | window.open('https://github.com/AlexGyver/Bitmaper', '_blank').focus();
339 | }
340 |
341 | document.addEventListener("DOMContentLoaded", () => {
342 | if ('serviceWorker' in navigator && typeof USE_SW !== 'undefined') {
343 | navigator.serviceWorker.register('sw.js');
344 | }
345 |
346 | let filters = Component.make('div', {
347 | id: 'filters',
348 | class: 'filters',
349 | parent: document.body,
350 | });
351 |
352 | let ctx = {};
353 | Component.make('div', {
354 | class: 'cv_cont',
355 | context: ctx,
356 | parent: document.body,
357 | children: [
358 | {
359 | tag: 'div',
360 | class: 'cv_inner',
361 | children: [
362 | {
363 | tag: 'canvas',
364 | class: 'canvas',
365 | var: 'cv',
366 | }
367 | ]
368 | }
369 | ]
370 | });
371 |
372 | canvas = new CanvasMatrix(ctx.$cv, click_h, drag_h, wheel_h, displayModes[0].active, displayModes[0].back);
373 |
374 | let buttons = { copy: ["Copy", copy_h], header: [".h", saveH_h], bin: [".bin", saveBin_h] };
375 | if (window.location.hostname.match(/^((25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])(\.(?!$)|$)){4}$/)) {
376 | buttons.send = ['Send', send_h];
377 | }
378 |
379 | // base_ui
380 | base_ui = new UI({ title: "Bitmaper", theme: 'dark' })
381 | .addFile('file', '', file_h)
382 | .addText('link', '', '', link_h)
383 | .addText('name', '', '', update_h)
384 | .addNumber('width', '', 128, 1, resize_h)
385 | .addNumber('height', '', 64, 1, resize_h)
386 | .addButton('fit', '', fit_h)
387 | .addRange('scale', '', 0, -300, 300, 1, scale_h)
388 | .addRange('rotate', '', 0, -180, 180, 5, rotate_h)
389 | .addSelect('mode', '', ['Mono', 'Gray', 'RGB'], mode_h)
390 | .addSelect('process', '', proc.processes.names, process_h)
391 | .addHTML('result', '', '')
392 | .addButtons({ bulk: ['', bulk_h], bulk_h: ['.h', bulkH_h] })
393 | .addArea('code', '', '')
394 | .addButtons(buttons)
395 | .addSelect('lang', 'Language', ['English', 'Russian'], lang_h)
396 | .addButtons({ info: ['', info_h], github: ['GitHub', git_h] })
397 |
398 | base_ui.control('scale').input.addEventListener('mouseup', () => base_ui.scale = 0);
399 |
400 | filt_ui = new UI({ title: "Filters", theme: 'dark', parent: filters })
401 | .addSelect('display', '', [], display_h)
402 | .addSwitch('grid', '', 1, update_h)
403 | .addSwitch('invert_b', '', 0, update_h)
404 | .addSwitch('preview', '', 1)
405 | .addSwitch('invert', '', 0, update_h)
406 | .addRange('brightness', '', 100, 0, 250, 5, update_h)
407 | .addRange('contrast', '', 100, 0, 250, 5, update_h)
408 | .addRange('saturate', '', 100, 0, 250, 5, update_h)
409 | .addRange('blur', '', 0, 0, 1, 0.05, update_h)
410 | .addSwitch('edges', '', 0, update_h)
411 | .addRange('sobel', '', 0, 0, 1, 0.05, update_h)
412 | .addSwitch('dither', '', 0, update_h)
413 | .addRange('threshold', '11', 50, 0, 100, 1, update_h)
414 | .addSwitch('median', '', 0, update_h)
415 | .addSwitch('editor', '', 0)
416 | .addButton('png', '', png_h)
417 | .addButton('reset', '', reset_h);
418 |
419 | switch (navigator.language || navigator.userLanguage) {
420 | case 'ru-RU':
421 | case 'ru':
422 | lang_h(1);
423 | break;
424 | default:
425 | lang_h(0);
426 | break;
427 | }
428 | resize_h();
429 | });
430 |
431 | function info_h() {
432 | let text = `Bitmaper - программа для преобразования картинок в битмапы
433 |
434 | Режимы изображения
435 | - Mono - монохромный (чёрно-белый)
436 | - Gray - оттенки серого
437 | - RGB - полноцветное изображение
438 |
439 | Алгоритмы кодирования
440 | - 1x pix/byte - 1 пиксель в байте, строками слева направо сверху вниз: [data_0, ...data_n]
441 | - 8x Horizontal - 8 пикселей в байте горизонтально (MSB слева), строками слева направо сверху вниз: [data_0, ...data_n]
442 | - 8x Vertical Col - 8 пикселей в байте вертикально (MSB снизу), столбцами сверху вниз слева направо: [data_0, ...data_n]
443 | - 8x Vertical Row - 8 пикселей в байте вертикально (MSB снизу), строками слева направо сверху вниз: [data_0, ...data_n] Подходит для GyverOLED
444 | - GyverGFX BitMap - 8 пикселей вертикально (MSB снизу), столбцами сверху вниз слева направо: [widthLSB, widthMSB, heightLSB, heightMSB, data_0, ...data_n]
445 | - GyverGFX BitPack - сжатый формат*: [widthLSB, widthMSB, heightLSB, heightMSB, data_0, ...data_n]
446 | - GyverGFX Image - программа выберет лёгкий между BitMap и BitPack: [0 map | 1 pack, x, x, x, x, data_0, ...data_n]
447 | - Grayscale - 1 пиксель в байте, оттенки серого
448 | - RGB888 - 1 пиксель на 3 байта (24 бит RGB): [r0, g0, b0, ...]
449 | - RGB565 - 1 пиксель на 2 байта (16 бит RGB): [rrrrrggggggbbbbb, ...] тип uint16_t
450 | - RGB233 - 1 пиксель в байте (8 бит RGB): [rrgggbbb, ...]
451 |
452 | Редактор
453 | - Действия кнопок мыши при включенном редакторе: ЛКМ - добавить точку, ПКМ - стереть точку, СКМ - отменить изменения на слое редактора
454 | - При изменении размера битмапа, при перемещении и масштабировании изображения слой редактора очищается
455 |
456 | Массовая конвертация
457 | - Выбрать несколько файлов или перетащить их на кнопку выбора файлов. Отобразится первый файл
458 | - Настроить параметры кодирования и фильтры, они будут применены ко всем остальным файлам
459 | - Нажать "Массовая конвертация", дождаться окончания
460 | - Результат появится в окне вывода кода. Изображения будут иметь суффиксы с номером, также прогармма составит из них список
461 | - Смотри пример в readme на GitHub
462 |
463 | Прочее
464 | - На изображениях со сплошными участками BitPack может быть в разы эффективнее обычного BitMap. На изображениях с dithering работает неэффективно.
465 | - Как кодирует BitPack: младший бит - состояние пикселя, остальные - количество. Сканирование идёт столбцами сверху вниз слева направо. Один чанк - 6 бит (состояние + 5 бит количества), 4 чанка пакуются в три байта как aaaaaabb, bbbbcccc, ccdddddd
466 | - Активный пиксель на выбранном стиле отображения: OLED - голубой, Paper - чёрный
467 | - При открытии приложения с локального сервера (IP адрес в строке адреса), например с esp, появится кнопка Send - при нажатии приложение отправит битмап в выбранном формате через formData на url /bitmap с query string параметрами width и height, т.е. /bitmap?width=N&height=N`;
468 |
469 | let ctx = {};
470 | Component.make('div', {
471 | class: 'info_cont',
472 | var: 'info',
473 | parent: document.body,
474 | context: ctx,
475 | also(el) {
476 | el.addEventListener('click', () => el.remove());
477 | },
478 | children: [
479 | {
480 | tag: 'div',
481 | class: 'info_inner',
482 | also(el) {
483 | el.addEventListener('click', (e) => e.stopPropagation());
484 | },
485 | children: [
486 | {
487 | tag: 'button',
488 | text: 'x',
489 | also(el) {
490 | el.addEventListener('click', () => ctx.$info.remove());
491 | },
492 | },
493 | {
494 | tag: 'span',
495 | text: text,
496 | }
497 | ]
498 | }
499 | ]
500 | });
501 | }
--------------------------------------------------------------------------------
/index/script.js:
--------------------------------------------------------------------------------
1 | (()=>{var t={947:(t,e,n)=>{"use strict";t.exports=function(t){const e=n(94),r=t?t.preset:"ru",i={а:"a",б:"b",в:"v",д:"d",з:"z",й:"y",к:"k",л:"l",м:"m",н:"n",о:"o",п:"p",р:"r",с:"s",т:"t",у:"u",ф:"f",ь:""};let a;"ru"===r?Object.assign(i,{г:"g",и:"i",ъ:"",ы:"i",э:"e"}):"uk"===r?Object.assign(i,{г:"h",ґ:"g",е:"e",и:"y",і:"i","'":"","’":"",ʼ:""}):"mn"===r&&Object.assign(i,{г:"g",ө:"o",ү:"u",и:"i",ы:"y",э:"e",ъ:""}),"ru"===r?a=Object.assign(e(i),{i:"и","":""}):("uk"===r||"mn"===r)&&(a=Object.assign(e(i),{"":""}));const s="ru"===r?{е:"ye"}:{є:"ye",ї:"yi"},o={ё:"yo",ж:"zh",х:"kh",ц:"ts",ч:"ch",ш:"sh",щ:"shch",ю:"yu",я:"ya"},l=Object.assign({},o,s),c=Object.assign(e(l)),d=Object.assign(i,l),h=Object.assign({},i,{й:"i"});let u;"ru"===r?Object.assign(h,{е:"e"}):"uk"===r?Object.assign(h,{ї:"i"}):"mn"===r&&Object.assign(h,{е:"e"}),"ru"===r?u=Object.assign(e(i),{i:"и",y:"ы",e:"е","":""}):"uk"===r&&(u=Object.assign(e(i),{"":""}));let f={};"uk"===r&&(f={є:"ie",ю:"iu",я:"ia"});const g=Object.assign(o,f),p=Object.assign(e(g)),m=Object.assign(h,g);return{transform:function(t,e){if(!t)return"";const n=t.normalize();let i="",a=!1;for(let t=0;t1?i+=o[0].toUpperCase()+o.slice(1):i+=o.toUpperCase():i+=o):(i+=e||" ",a=!0)}return i},reverse:function(t,e){if(!t)return"";const n=t.normalize();let r="",i=!1,s=0;for(;s1?r+=o[0].toUpperCase()+o.slice(1):r+=o.toUpperCase():r+=o}return r}}}},94:t=>{var e=9007199254740991,n="[object Arguments]",r="[object Function]",i="[object GeneratorFunction]",a=/^(?:0|[1-9]\d*)$/;var s,o,l=Object.prototype,c=l.hasOwnProperty,d=l.toString,h=l.propertyIsEnumerable,u=(s=Object.keys,o=Object,function(t){return s(o(t))});function f(t,e){var r=_(t)||function(t){return function(t){return function(t){return!!t&&"object"==typeof t}(t)&&x(t)}(t)&&c.call(t,"callee")&&(!h.call(t,"callee")||d.call(t)==n)}(t)?function(t,e){for(var n=-1,r=Array(t);++n-1&&t%1==0&&t-1&&t%1==0&&t<=e}(t.length)&&!function(t){var e=function(t){var e=typeof t;return!!t&&("object"==e||"function"==e)}(t)?d.call(t):"";return e==r||e==i}(t)}var y,w,k,$=(y=function(t,e,n){t[e]=n},k=function(t){return t},w=function(){return k},function(t,e){return m(t,y,w(e),{})});function S(t){return x(t)?f(t):v(t)}t.exports=$}},e={};function n(r){var i=e[r];if(void 0!==i)return i.exports;var a=e[r]={exports:{}};return t[r](a,a.exports,n),a.exports}n.n=t=>{var e=t&&t.__esModule?()=>t.default:()=>t;return n.d(e,{a:e}),e},n.d=(t,e)=>{for(var r in e)n.o(e,r)&&!n.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:e[r]})},n.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),(()=>{"use strict";class t{constructor(e,n={}){n.context=this,this.$root=t.make(e,n)}static make(e,n={}){return e&&"object"==typeof n?n instanceof Node?n:t.config(document.createElement(e),n):null}static makeShadow(e,n={},r=null){if(!e||"object"!=typeof n)return null;let i=e instanceof Node?e:document.createElement(e);return i.attachShadow({mode:"open"}),t.config(i.shadowRoot,{context:n.context,children:[{tag:"style",textContent:r??""},n.child??{},...n.children??[]]}),delete n.children,delete n.child,t.config(i,n),i}static config(e,n){if(!(e instanceof Node)||"object"!=typeof n)return e;const r=n.context;let i=n=>{if(n)if(n instanceof Node)e.appendChild(n);else if(n instanceof t)e.appendChild(n.$root);else if("object"==typeof n){n.context||(n.context=r);let i=t.make(n.tag,n);i&&e.appendChild(i)}else"string"==typeof n&&(e.innerHTML+=n)};for(const[t,a]of Object.entries(n))if(a)switch(t){case"tag":case"context":continue;case"text":e.textContent=a;break;case"html":e.innerHTML=a;break;case"class":e.classList.add(...a.split(" "));break;case"also":r&&a.call(r,e);break;case"export":a[0]=e;break;case"var":r&&(r["$"+a]=e);break;case"events":for(let t in a)a[t]&&e.addEventListener(t,a[t].bind(r));break;case"parent":(a instanceof Node||a instanceof DocumentFragment)&&a.append(e);break;case"attrs":for(let t in a)e.setAttribute(t,a[t]);break;case"props":for(let t in a)e[t]=a[t];break;case"child":i(a);break;case"children":for(const t of a)i(t);break;case"style":if("string"==typeof a)e.style.cssText+=a+";";else for(let t in a)e.style[t]=a[t];break;default:e[t]=a}return e}static makeArray(e){return e&&Array.isArray(e)?e.map((e=>t.make(e.tag,e))):[]}}new Set,new Map;class e{_data;constructor(t){this._data=t}get label(){return this._data.$label.innerText}set label(t){return this._data.$label.innerText=t+""}get value(){return this._data.$control.value}set value(t){return this._data.$control.value=t+""}get input(){return this._data.$control}display(t){this._data.$container.style.display=t?"block":"none"}show(){this.display(1)}hide(){this.display(0)}remove(){this._data.$container.remove()}default(){this.value=this._data.default+"",this._data.$output&&(this._data.$output.innerText=this._data.default+"")}}class r extends e{constructor(t){super(t)}set value(t){return this._data.$output&&(this._data.$output.innerText=t+""),this._data.$control.value=t+""}get value(){return Number(this._data.$control.value)}}class i extends e{constructor(t){super(t)}get value(){return this._data.$control.checked}set value(t){return this._data.$control.checked=t}default(){this.value=this._data.default}}class a extends e{constructor(t){super(t)}get value(){return this._data.$control.innerHTML}set value(t){return this._data.$control.innerHTML=t+""}}class s extends e{constructor(t){super(t)}get value(){return this._data.$control.files[0]}set value(t){}}class o extends e{constructor(t){super(t)}set value(t){}get value(){return 1}}class l extends r{constructor(t){super(t)}set options(e){return this._data.$control.replaceChildren(...e.map(((e,n)=>t.make("option",{text:e,value:n+""}))))}}class c extends e{constructor(t){super(t)}set value(t){this._data.$control.innerText=t+""}get value(){return this._data.$control.innerText}}class d{constructor(t={}){return this.init(t)}init(e){return e&&"object"==typeof e?(this.autoVar=e.autoVar??!0,t.make("div",{class:"ui_main theme-"+(e.theme??"light"),style:{zIndex:e.zIndex??3,left:e.x??0,top:e.y??0,width:e.width?"string"==typeof e.width?e.width:e.width+"px":"200px",position:e.parent?"":"absolute"},parent:e.parent??document.body,children:[{tag:"div",class:"ui_title_bar",text:e.title??"UI"},{tag:"div",class:"ui_content",var:"content"}],var:"root",context:this}),this):this}setTheme(t){return this.$root.classList="ui_main theme-"+t,this}destroy(){this.$root&&(this.$root.remove(),this.$root=void 0,this.#t=new Map)}setLabels(t){for(let e in t)this.#t.has(e)&&(this.#t.get(e).label=t[e])}toObject(){let t={};return this.#t.forEach(((e,n)=>{!e.value||e instanceof o||(t[n]=e.value)})),t}toJson(){return JSON.stringify(this.toObject())}fromObject(t){for(let e in t)this.#t.has(e)&&(this.#t.get(e).value=t[e])}fromJson(t){this.fromObject(JSON.parse(t))}control(t){return this.#t.get(t)}get(t){if(this.#t.has(t))return this.#t.get(t).value}set(t,e){if(this.#t.has(t))return this.#t.get(t).value=e}remove(t){this.#t.has(t)&&(this.#t.get(t).remove(),this.#t.delete(t))}addSwitch(e,n,r,a){let s={default:r=r??!1};return t.make("div",{context:s,var:"container",class:"ui_container",parent:this.$content,children:[{tag:"label",class:"ui_checkbox_label",text:n,var:"label"},{tag:"label",class:"ui_checkbox",children:[{tag:"input",type:"checkbox",checked:r,var:"control",also(t){a&&t.addEventListener("click",(()=>a(t.checked)))}},{tag:"span"}]}]}),e&&this.#t.set(e,new i(s)),this._addSetGet(e),this}addNumber(e,n,i,a,s){i=i??0;let o=this._makeContainer(n);return o.default=i,t.make("input",{parent:o.$container,context:o,type:"number",class:"ui_text_input ui_number",step:(a??1)+"",value:i+"",var:"control",also(t){t.addEventListener("input",(()=>{s&&s(Number(t.value))})),t.addEventListener("mousewheel",(t=>{}))}}),e&&this.#t.set(e,new r(o)),this._addSetGet(e),this}addText(n,r,i,a){i=i??"";let s=this._makeContainer(r);return s.default=i,t.make("input",{parent:s.$container,context:s,type:"text",class:"ui_text_input",value:i+"",var:"control",also(t){a&&t.addEventListener("input",(()=>a(t.value)))}}),n&&this.#t.set(n,new e(s)),this._addSetGet(n),this}addRange(e,n,i,a,s,o,l){i=i??0;let c=this._makeContainerOut(n,i);return c.default=i,t.make("input",{parent:c.$container,context:c,type:"range",class:"ui_range",value:i+"",min:(a??0)+"",max:(s??100)+"",step:(o??1)+"",var:"control",also(t){t.addEventListener("input",(()=>{l&&l(Number(t.value)),c.$output.innerText=t.value})),t.addEventListener("mousewheel",(e=>{e.stopPropagation(),e.preventDefault(),t.value=Number(t.value)+Number(t.step)*(e.deltaY>0?-1:1),t.dispatchEvent(new Event("input"))}))}}),e&&this.#t.set(e,new r(c)),this._addSetGet(e),this}addArea(n,r,i,a){i=i??"";let s=this._makeContainer(r);return s.default=i,t.make("textarea",{parent:s.$container,context:s,class:"ui_textarea",rows:5,value:i+"",var:"control",also(t){a&&t.addEventListener("input",(()=>a(t.value)))}}),n&&this.#t.set(n,new e(s)),this._addSetGet(n),this}addHTML(e,n,r){r=r??"";let i=this._makeContainer(n);return i.default=r,t.make("div",{parent:i.$container,context:i,html:r+"",var:"control"}),e&&this.#t.set(e,new a(i)),this._addSetGet(e),this}addElement(t,e,n){let r=this._makeContainer(e);return r.default=n,r.$control=n,r.$container.append(n),t&&this.#t.set(t,new a(r)),this._addSetGet(t),this}addSelect(e,n,r,i){r=r??[];let a=this._makeContainer(n);return a.default=0,t.make("select",{parent:a.$container,context:a,class:"ui_select",var:"control",also(t){i&&t.addEventListener("change",(()=>i(Number(t.value))))},children:r.map(((e,n)=>t.make("option",{text:e,value:n+""})))}),e&&this.#t.set(e,new l(a)),this._addSetGet(e),this}addButton(e,n,r){let i={};return t.make("div",{context:i,var:"container",class:"ui_container",parent:this.$content,children:[this._makeButton(i,e,n,r)]}),i.$label=i.$control,e&&this.#t.set(e,new o(i)),this}addButtons(e){let n=t.make("div",{var:"container",class:"ui_container",parent:this.$content});for(let t in e){let r={$container:n};n.append(this._makeButton(r,t,e[t][0],e[t][1])),r.$label=r.$control,t&&this.#t.set(t,new o(r))}return this}addFile(e,n,r){let i=this._makeContainer(n),a=t=>{r&&r(1==t.length?t[0]:t),i.$filename.innerText=t[0].name};return i.$container.append(...t.makeArray([{tag:"input",context:i,class:"ui_file_chooser",type:"file",var:"control",attrs:{multiple:!0},also(t){t.addEventListener("change",(()=>a(t.files)))}},{tag:"label",context:i,class:"ui_file_chooser_label",text:"...",var:"filename",also(t){t.addEventListener("click",(()=>i.$control.click())),t.addEventListener("drop",(t=>a(t.dataTransfer.files)))}}])),e&&this.#t.set(e,new s(i)),["dragenter","dragover","dragleave","drop"].forEach((t=>{this.$root.addEventListener(t,(t=>{t.preventDefault(),t.stopPropagation()}),!1)})),["dragenter","dragover"].forEach((t=>{this.$root.addEventListener(t,(function(){i.$filename.classList.add("active")}),!1)})),["dragleave","drop"].forEach((t=>{this.$root.addEventListener(t,(function(){i.$filename.classList.remove("active")}),!1)})),this._addSetGet(e),this}addColor(e,n,i,a){i=i??"#000";let s=this._makeContainerOut(n,i);return s.default=i,s.$container.append(t.make("input",{context:s,type:"color",class:"ui_color",value:i,var:"control",attrs:{"colorpick-eyedropper-active":!1},also(t){t.addEventListener("input",(()=>{a&&a(t.value),s.$output.innerText=t.value}))}})),e&&this.#t.set(e,new r(s)),this._addSetGet(e),this}addLabel(t,e,n){n=n??"";let r=this._makeContainerOut(e,n);return r.$control=r.$output,t&&this.#t.set(t,new c(r)),this._addSetGet(t),this}addSpace(){return t.make("div",{class:"ui_space",parent:this.$content}),this}_addSetGet(t){this.autoVar&&t&&Object.defineProperty(this,t,{get:()=>this.get(t),set:e=>this.set(t,e)})}_checkID(t){return t||"_empty_"+this.#e++}_makeButton(e,n,r,i){return t.make("button",{context:e,class:"ui_button",var:"control",text:r+"",also(t){i&&t.addEventListener("click",(()=>i(1)))}})}_makeContainer(e){let n={};return t.make("div",{context:n,var:"container",class:"ui_container",parent:this.$content,children:[{tag:"div",class:"ui_label",children:[{tag:"b",var:"label",text:e}]}]}),n}_makeContainerOut(e,n){let r={};return t.make("div",{context:r,var:"container",class:"ui_container",parent:this.$content,children:[{tag:"div",class:"ui_label",children:[{tag:"b",text:e,var:"label"},{tag:"span",text:": "},{tag:"span",text:n+"",var:"output"}]}]}),r}#t=new Map;#e=0}class h{matrix=new Int32Array;W=0;H=0;constructor(t,e){this.resize(t,e)}resize(t,e){t&&e&&(this.W=t,this.H=e,this.matrix=new Int32Array(t*e).fill(0))}merge(t){if(this.matrix.length==t.matrix.length)for(let e=0;ethis._resize())),t.addEventListener("mousedown",(t=>this._onMouseDown(t))),t.addEventListener("mousewheel",(t=>this._onMouseWheel(t))),t.addEventListener("contextmenu",(t=>t.preventDefault())),document.addEventListener("mousemove",(t=>this._onMouseMove(t))),document.addEventListener("mouseup",(t=>this._onMouseUp(t))))}setColors(t,e){this.active=t,this.back=e,this.cx.strokeStyle=e?"white":"black"}setMode(t){this.mode=t}resize(t,e){super.resize(t,e),this._resize()}render(t){this._grid=t,this.cx.fillStyle=this.back?"white":"black",this.cx.fillRect(0,0,this.cv.width,this.cv.height);let e=this._blocksize;this.cx.fillStyle=this.active;for(let n=0;nthis._minsize&&this.cx.strokeRect(n*e,r*e,e,e)}}}grid(){let t=this._blocksize;if(!(tthis._maxwidth&&(e=this._maxwidth,t=e*this.W/this.H),this._realW=t,this._realH=e,this.cv.style.width=t+"px",this.cv.style.height=e+"px",this.render(this._grid)}_onMouseWheel(t){this._onwheel(t.deltaY<0?1:-1)}_onMouseDown(t){this._pressed=!0,this._dragged=!1,this._pressXY=this._getXY(t)}_onMouseMove(t){if(!this._pressed)return;this._dragged||(this.cv.style.cursor="grab",document.body.style.cursor="grab"),this._dragged=!0;let e=this._getXY(t);this._ondrag(this._drag(e,!1),{1:0,4:1,2:2}[t.buttons])}_onMouseUp(t){if(!this._pressed)return;this._pressed=!1;let e=this._getXY(t);this._dragged?(this.cv.style.cursor="pointer",document.body.style.cursor="default",this._ondrag(this._drag(e,!0),t.button)):this._onclick(this._blockXY(e),t.button),this._dragged=!1}_getXY(t){let e=t.pageX,n=t.pageY;return"BODY"===this.cv.offsetParent.tagName.toUpperCase()?(e-=this.cv.offsetLeft,n-=this.cv.offsetTop):(e-=this.cv.offsetParent.offsetLeft,n-=this.cv.offsetParent.offsetTop),e*=this.cv.width/this._realW,n*=this.cv.height/this._realH,{x:e,y:n}}_blockXY(t){return{x:Math.floor(t.x/this.cv.width*this.W),y:Math.floor(t.y/this.cv.height*this.H)}}_drag(t,e){return{dx:t.x-this._pressXY.x,dy:t.y-this._pressXY.y,block:this._blockXY(t),release:e}}}var f=n(947),g=n.n(f);function p(t,e,n){return tn?n:t}function m(t){return t|0}function v(t){return[t>>16&255,t>>8&255,255&t]}const b={names:["1 pix/byte","8x Horizontal","8x Vertical Col","8x Vertical Row","GyverGFX Image","GyverGFX BitMap","GyverGFX BitPack","Grayscale","RGB888","RGB565","RGB233"],prefix:["const uint8_t","const uint8_t","const uint8_t","const uint8_t","gfximage_t","gfxmap_t","gfxpack_t","const uint8_t","const uint8_t","const uint16_t","const uint8_t"],extension:["1p","8h","8vc","8vr","img","map","pack","gray","rgb888","rgb565","rgb233"],func:[function(t){return t.matrix.map((t=>t?1:0))},function(t){let e=[],n=Math.ceil(t.W/8);for(let r=0;r>=1,i|=(t.get(n,8*r+e)?1:0)<<7;e.push(i)}return e},function(t){let e=Math.ceil(t.H/8)*t.W+4,n=x(t);return e<=n.length?[0].concat(y(t)):[1].concat(n)},y,x,function(t){return[...t.matrix]},function(t){let e=[];for(let n of t.matrix)e.push(...v(n));return e},function(t){let e=[];for(let n of t.matrix){let t=v(n);e.push((248&t[0])<<8|(252&t[1])<<3|t[2]>>3)}return e},function(t){let e=[];for(let n of t.matrix){let t=v(n);e.push(192&t[0]|(224&t[1])>>2|(224&t[2])>>5)}return e}],mult:[1,1,1,1,1,1,1,1,1,2,1]};function _(t){let e=[],n=Math.ceil(t.H/8);for(let r=0;r>=1,n|=(t.get(r,8*i+e)?1:0)<<7;e.push(n)}return e}function x(t){let e=[255&t.W,t.W>>8&255,255&t.H,t.H>>8&255],n=0,r=0,i=0,a=()=>{let t=n<<1|r;switch(3&i++){case 0:e.push(t<<2);break;case 1:e[e.length-1]|=t>>4,e.push(t<<4&240);break;case 2:e[e.length-1]|=t>>2,e.push(t<<6&192);break;case 3:e[e.length-1]|=t}};for(let e=0;e>8&255,255&t.H,t.H>>8&255].concat(_(t))}function w(t,e){let n=b.func[e](t),r=Int8Array.from(n);return new Blob([r],{type:"application/octet-stream"})}function k(t,e){let n=(new TextEncoder).encode(e),r=new Blob([n],{type:"text/plain"}),i=document.createElement("a");i.href=window.URL.createObjectURL(r),i.download=t+".h",i.click()}function $(t,e,n){let r=b.func[e](t),i=`${n}_${t.W}x${t.H}`,a=`${b.prefix[e]} ${i}[] PROGMEM = {`;return a+=function(t,e=16){let n="";for(let r=0;r{const r=new Image;switch(r.crossOrigin="Anonymous",r.addEventListener("load",(()=>{r.width&&r.height?(this.image=r,this.first=!0,e()):n("Image error")})),r.addEventListener("error",(()=>{n("Image load error")})),typeof t){case"object":t.type.toString().includes("image")||n("Not an image"),r.src=URL.createObjectURL(t),this.name=this._getName(t.name);break;case"string":t.startsWith("http")||n("Not a link"),r.src=t,this.name=this._getName(t);break;default:n("Image error")}}))}scale(t){this.image&&(this.offset.w+=t*this.offset.w/64)}pan(t,e,n){this.image&&(this.offset.dx=t,this.offset.dy=e,n&&(this.offset.x-=this.offset.dx,this.offset.dx=0,this.offset.y-=this.offset.dy,this.offset.dy=0))}show(t,e,n,r){this.image&&(this._render(t.cv,e,n,r),t.grid())}fit(t){this.offset={x:0,y:0,dx:0,dy:0,w:0};let e=t.cv.width,n=e*this.image.height/this.image.width;n>t.cv.height&&(e*=t.cv.height/n,n=t.cv.height),this.offset.w=e/t.cv.width}setOffset(t){this.first=!1,this.offset=t}render(t,e,n,r){if(!this.image)return;this.first&&(this.first=!1,this.fit(t));let i=document.createElement("canvas");i.width=t.cv.width,i.height=t.cv.height;let a=i.getContext("2d");a.fillStyle=n?"white":"black",a.fillRect(0,0,i.width,i.height),this._render(i,e,n,r);let s=document.createElement("canvas");s.width=t.W,s.height=t.H;let o=s.getContext("2d");o.drawImage(i,0,0,s.width,s.height),t.clear();let l=o.getImageData(0,0,t.W,t.H).data;for(let e=0,n=0;e="0"&&t[0]<="9"&&(t="b"+t):t="bitmap",t}},O=new h,G=new class{#n;start(t,e){this.stop(),this.#n=setTimeout(t,e)}stop(){this.#n&&(clearTimeout(this.#n),this.#n=null)}},C=null,j=0,W=0;const R=[{active:"#478be6",back:0},{active:"#000000",back:1}];async function H(t){t instanceof File||"string"==typeof t?(C=null,await T(t)):t instanceof FileList?(C=t,await T(C[0])):C=null}async function T(t){try{await M.load(t)}catch(t){return void alert(t)}O.clear(),L.name=M.name,P()}async function z(t){t.length&&(await H(t),L.link="")}function N(){let t=[L.width,L.height];E.resize(t[0],t[1]),O.resize(t[0],t[1]),Math.max(t[0],t[1])>500&&(B.preview=!1),P()}function I(){M.image&&M.fit(E),P()}function P(){U(),E.merge(O),E.render(B.grid),function(){let t=$(E,L.process,L.name);L.code=t.code;let e=`${E.W}x${E.H} (${t.size} bytes)
`;e+=Math.round(E.matrix.filter((t=>t)).length/E.matrix.length*100)+"% pixels on",L.result=e}()}function U(){if(M.image){if(M.render(E,L.rotate,B.invert_b,{invert:R[B.display].back^B.invert,brightness:B.brightness,contrast:B.contrast,saturate:B.saturate,blur:B.blur,grayscale:2==L.mode?0:100}),2!=L.mode)for(let t=0;t>4),r+1!=n&&(i>0&&(t[a+e-1]+=3*o>>4),t[a+e]+=5*o>>4,i+1>4))}}(E.matrix,E.W,E.H),0==L.mode&&function(t,e){for(let n=0;n"+(n/1024).toFixed(2)+" kB",t}async function nt(){let t=await et();if(!t)return;let e=`#pragma once\n #include \n ${L.process>=3&&L.process<=5?"#include ":""}\n \n // Bitmaper bulk process ${C.length} files\n \n `;e+=t,k("bulk",e)}function rt(){navigator.clipboard.writeText(L.code)}function it(){!function(t,e,n){let r=$(t,e,n),i=`#pragma once\n#include \n${e>=3&&e<=5?"#include ":""}\n\n// ${n} (${r.size} bytes)\n// ${b.names[e]}\n\n`;i+=r.code,k(n,i)}(E,L.process,L.name)}function at(){!function(t,e,n){let r=w(t,e),i=document.createElement("a");i.href=window.URL.createObjectURL(r),n+="."+b.extension[e],i.download=n,i.click()}(E,L.process,L.name)}function st(){let t=w(E,L.process),e=new FormData;e.append("bitmap",t),fetch(window.location.href+`bitmap?width=${E.W}&height=${E.H}`,{method:"POST",body:e})}function ot(){let t=document.createElement("a");t.href=E.cv.toDataURL("image/png"),t.download=L.name+".png",t.click()}function lt(t){L.lang=t,L.setLabels(S[t].base),B.setLabels(S[t].filters),B.control("display").options=S[t].display,j=t}function ct(){window.open("https://github.com/AlexGyver/Bitmaper","_blank").focus()}function dt(){let e={};t.make("div",{class:"info_cont",var:"info",parent:document.body,context:e,also(t){t.addEventListener("click",(()=>t.remove()))},children:[{tag:"div",class:"info_inner",also(t){t.addEventListener("click",(t=>t.stopPropagation()))},children:[{tag:"button",text:"x",also(t){t.addEventListener("click",(()=>e.$info.remove()))}},{tag:"span",text:'Bitmaper - программа для преобразования картинок в битмапы\n\nРежимы изображения\n- Mono - монохромный (чёрно-белый)\n- Gray - оттенки серого\n- RGB - полноцветное изображение\n\nАлгоритмы кодирования\n- 1x pix/byte - 1 пиксель в байте, строками слева направо сверху вниз: [data_0, ...data_n]\n- 8x Horizontal - 8 пикселей в байте горизонтально (MSB слева), строками слева направо сверху вниз: [data_0, ...data_n]\n- 8x Vertical Col - 8 пикселей в байте вертикально (MSB снизу), столбцами сверху вниз слева направо: [data_0, ...data_n]\n- 8x Vertical Row - 8 пикселей в байте вертикально (MSB снизу), строками слева направо сверху вниз: [data_0, ...data_n] Подходит для GyverOLED\n- GyverGFX BitMap - 8 пикселей вертикально (MSB снизу), столбцами сверху вниз слева направо: [widthLSB, widthMSB, heightLSB, heightMSB, data_0, ...data_n]\n- GyverGFX BitPack - сжатый формат*: [widthLSB, widthMSB, heightLSB, heightMSB, data_0, ...data_n]\n- GyverGFX Image - программа выберет лёгкий между BitMap и BitPack: [0 map | 1 pack, x, x, x, x, data_0, ...data_n]\n- Grayscale - 1 пиксель в байте, оттенки серого\n- RGB888 - 1 пиксель на 3 байта (24 бит RGB): [r0, g0, b0, ...]\n- RGB565 - 1 пиксель на 2 байта (16 бит RGB): [rrrrrggggggbbbbb, ...] тип uint16_t\n- RGB233 - 1 пиксель в байте (8 бит RGB): [rrgggbbb, ...]\n\nРедактор\n- Действия кнопок мыши при включенном редакторе: ЛКМ - добавить точку, ПКМ - стереть точку, СКМ - отменить изменения на слое редактора\n- При изменении размера битмапа, при перемещении и масштабировании изображения слой редактора очищается\n\nМассовая конвертация\n- Выбрать несколько файлов или перетащить их на кнопку выбора файлов. Отобразится первый файл\n- Настроить параметры кодирования и фильтры, они будут применены ко всем остальным файлам\n- Нажать "Массовая конвертация", дождаться окончания\n- Результат появится в окне вывода кода. Изображения будут иметь суффиксы с номером, также прогармма составит из них список\n- Смотри пример в readme на GitHub\n\nПрочее\n- На изображениях со сплошными участками BitPack может быть в разы эффективнее обычного BitMap. На изображениях с dithering работает неэффективно.\n- Как кодирует BitPack: младший бит - состояние пикселя, остальные - количество. Сканирование идёт столбцами сверху вниз слева направо. Один чанк - 6 бит (состояние + 5 бит количества), 4 чанка пакуются в три байта как aaaaaabb, bbbbcccc, ccdddddd\n- Активный пиксель на выбранном стиле отображения: OLED - голубой, Paper - чёрный\n- При открытии приложения с локального сервера (IP адрес в строке адреса), например с esp, появится кнопка Send - при нажатии приложение отправит битмап в выбранном формате через formData на url /bitmap с query string параметрами width и height, т.е. /bitmap?width=N&height=N'}]}]})}document.addEventListener("DOMContentLoaded",(()=>{"serviceWorker"in navigator&&navigator.serviceWorker.register("sw.js");let e=t.make("div",{id:"filters",class:"filters",parent:document.body}),n={};t.make("div",{class:"cv_cont",context:n,parent:document.body,children:[{tag:"div",class:"cv_inner",children:[{tag:"canvas",class:"canvas",var:"cv"}]}]}),E=new u(n.$cv,q,K,Z,R[0].active,R[0].back);let r={copy:["Copy",rt],header:[".h",it],bin:[".bin",at]};switch(window.location.hostname.match(/^((25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])(\.(?!$)|$)){4}$/)&&(r.send=["Send",st]),L=new d({title:"Bitmaper",theme:"dark"}).addFile("file","",H).addText("link","","",z).addText("name","","",P).addNumber("width","",128,1,N).addNumber("height","",64,1,N).addButton("fit","",I).addRange("scale","",0,-300,300,1,Q).addRange("rotate","",0,-180,180,5,tt).addSelect("mode","",["Mono","Gray","RGB"],F).addSelect("process","",b.names,D).addHTML("result","","").addButtons({bulk:["",et],bulk_h:[".h",nt]}).addArea("code","","").addButtons(r).addSelect("lang","Language",["English","Russian"],lt).addButtons({info:["",dt],github:["GitHub",ct]}),L.control("scale").input.addEventListener("mouseup",(()=>L.scale=0)),B=new d({title:"Filters",theme:"dark",parent:e}).addSelect("display","",[],A).addSwitch("grid","",1,P).addSwitch("invert_b","",0,P).addSwitch("preview","",1).addSwitch("invert","",0,P).addRange("brightness","",100,0,250,5,P).addRange("contrast","",100,0,250,5,P).addRange("saturate","",100,0,250,5,P).addRange("blur","",0,0,1,.05,P).addSwitch("edges","",0,P).addRange("sobel","",0,0,1,.05,P).addSwitch("dither","",0,P).addRange("threshold","11",50,0,100,1,P).addSwitch("median","",0,P).addSwitch("editor","",0).addButton("png","",ot).addButton("reset","",Y),navigator.language||navigator.userLanguage){case"ru-RU":case"ru":lt(1);break;default:lt(0)}N()}))})()})();
--------------------------------------------------------------------------------