├── 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 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/assets/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /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()}))})()})(); --------------------------------------------------------------------------------