forked from ari/mnist-classify
334 lines
13 KiB
HTML
334 lines
13 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Ari::web -> PPM</title>
|
|
|
|
<meta
|
|
name="description"
|
|
content="Create PPM files online: P6 and P3 format."
|
|
/>
|
|
<meta property="og:type" content="website" />
|
|
|
|
<meta name="color-scheme" content="dark" />
|
|
<meta name="theme-color" content="#121212" />
|
|
|
|
<meta name="foss:src" content="/git/" />
|
|
<meta name="license" content="AGPL-3.0-or-later" />
|
|
|
|
<link rel="manifest" href="/manifest.json" />
|
|
|
|
<script type="text/javascript">
|
|
<!--//--><![CDATA[//><!--
|
|
/**
|
|
* @licstart The following is the entire license notice for the JavaScript
|
|
* code in this page.
|
|
*
|
|
* Copyright (C) 2024 Ari Archer
|
|
*
|
|
* The JavaScript code in this page is free software: you can redistribute
|
|
* it and/or modify it under the terms of the GNU Affero General Public License
|
|
* (AGPL) as published by the Free Software Foundation, either version 3
|
|
* of the License, or (at your option) any later version. The code is
|
|
* distributed WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the AGPL
|
|
* for more details.
|
|
*
|
|
* As additional permission under AGPL version 3 section 7, you may
|
|
* distribute non-source (e.g., minimized or compacted) forms of that code
|
|
* without the copy of the AGPL normally required by section 4, provided
|
|
* you include this license notice and a URL through which recipients can
|
|
* access the Corresponding Source.
|
|
*
|
|
* @licend The above is the entire license notice for the JavaScript code
|
|
* in this page.
|
|
*/
|
|
|
|
//--><!]]>
|
|
</script>
|
|
|
|
<style>
|
|
#canvas {
|
|
border: 1px solid red;
|
|
image-rendering: pixelated;
|
|
cursor: crosshair;
|
|
display: block;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
#controls {
|
|
margin-top: 10px;
|
|
}
|
|
|
|
label,
|
|
input,
|
|
select,
|
|
button {
|
|
margin-right: 10px;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
#brush_size_input {
|
|
width: 50px;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<article>
|
|
<header>
|
|
<h1>Create P6 or P3 PPM Image online</h1>
|
|
|
|
<p>Enjoy. Created this in a rush.</p>
|
|
|
|
<div id="controls">
|
|
<label for="width_input">width:</label>
|
|
<input
|
|
type="number"
|
|
id="width_input"
|
|
value="100"
|
|
min="1"
|
|
max="1000"
|
|
/>
|
|
|
|
<label for="height_input">height:</label>
|
|
<input
|
|
type="number"
|
|
id="height_input"
|
|
value="100"
|
|
min="1"
|
|
max="1000"
|
|
/>
|
|
|
|
<label for="colour_picker">choose background colour:</label>
|
|
<input type="color" id="colour_picker_bg" value="#000000" />
|
|
|
|
<label for="scale_factor_input">scale factor:</label>
|
|
<input
|
|
type="number"
|
|
id="scale_factor_input"
|
|
value="10"
|
|
min="1"
|
|
max="20"
|
|
/>
|
|
|
|
<button id="save_canvas_btn">save canvas</button>
|
|
<br />
|
|
|
|
<label for="brush_size_input">brush size:</label>
|
|
<input
|
|
type="number"
|
|
id="brush_size_input"
|
|
value="3"
|
|
min="1"
|
|
max="50"
|
|
/>
|
|
<br />
|
|
|
|
<label for="format_select">choose format:</label>
|
|
<select id="format_select">
|
|
<option value="P6">P6 (binary)</option>
|
|
<option value="P3">P3 (ASCII)</option>
|
|
</select>
|
|
|
|
<button id="save_btn">save image</button>
|
|
<br /><br />
|
|
|
|
<label for="colour_picker">choose brush colour:</label>
|
|
<input type="color" id="colour_picker" value="#ffffff" />
|
|
</div>
|
|
</header>
|
|
<main>
|
|
<div align="center">
|
|
<canvas id="canvas" width="100" height="100"></canvas>
|
|
</div>
|
|
|
|
<script>
|
|
const canvas = document.getElementById("canvas");
|
|
const ctx = canvas.getContext("2d");
|
|
|
|
const width_input = document.getElementById("width_input");
|
|
const height_input =
|
|
document.getElementById("height_input");
|
|
const scale_factor_input =
|
|
document.getElementById("scale_factor_input");
|
|
const save_canvas_btn =
|
|
document.getElementById("save_canvas_btn");
|
|
const format_select =
|
|
document.getElementById("format_select");
|
|
const brush_size_input =
|
|
document.getElementById("brush_size_input");
|
|
const save_btn = document.getElementById("save_btn");
|
|
const colour_picker =
|
|
document.getElementById("colour_picker");
|
|
const colour_picker_bg =
|
|
document.getElementById("colour_picker_bg");
|
|
|
|
function clear_canvas() {
|
|
canvas.style.width =
|
|
canvas.width *
|
|
parseInt(scale_factor_input.value, 10) +
|
|
"px";
|
|
canvas.style.height =
|
|
canvas.height *
|
|
parseInt(scale_factor_input.value, 10) +
|
|
"px";
|
|
|
|
ctx.fillStyle = colour_picker_bg.value;
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
}
|
|
|
|
clear_canvas();
|
|
|
|
save_canvas_btn.addEventListener("click", () => {
|
|
const width = parseInt(width_input.value, 10);
|
|
const height = parseInt(height_input.value, 10);
|
|
if (isNaN(width) || width < 1 || width > 1000) {
|
|
alert("width must be a number between 1 and 1000");
|
|
return;
|
|
}
|
|
if (isNaN(height) || height < 1 || height > 1000) {
|
|
alert("height must be a number between 1 and 1000");
|
|
return;
|
|
}
|
|
canvas.width = width;
|
|
canvas.height = height;
|
|
|
|
clear_canvas();
|
|
});
|
|
|
|
let drawing = false;
|
|
let last_x = 0;
|
|
let last_y = 0;
|
|
|
|
function get_mouse_pos(canvas, event) {
|
|
const rect = canvas.getBoundingClientRect();
|
|
const scale_x = canvas.width / rect.width;
|
|
const scale_y = canvas.height / rect.height;
|
|
|
|
return {
|
|
x: Math.floor(
|
|
(event.clientX - rect.left) * scale_x,
|
|
),
|
|
y: Math.floor((event.clientY - rect.top) * scale_y),
|
|
};
|
|
}
|
|
|
|
canvas.addEventListener("mousedown", (e) => {
|
|
drawing = true;
|
|
const pos = get_mouse_pos(canvas, e);
|
|
last_x = pos.x;
|
|
last_y = pos.y;
|
|
});
|
|
|
|
canvas.addEventListener("mousemove", (e) => {
|
|
if (!drawing) return;
|
|
const pos = get_mouse_pos(canvas, e);
|
|
|
|
ctx.strokeStyle = colour_picker.value;
|
|
ctx.lineWidth =
|
|
parseInt(brush_size_input.value, 10) || 1;
|
|
ctx.lineCap = "square";
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(last_x + 0.5, last_y + 0.5);
|
|
ctx.lineTo(pos.x + 0.5, pos.y + 0.5);
|
|
ctx.stroke();
|
|
|
|
last_x = pos.x;
|
|
last_y = pos.y;
|
|
});
|
|
|
|
canvas.addEventListener("mouseup", () => {
|
|
drawing = false;
|
|
});
|
|
canvas.addEventListener("mouseleave", () => {
|
|
drawing = false;
|
|
});
|
|
|
|
function get_pixel_data() {
|
|
const image_data = ctx.getImageData(
|
|
0,
|
|
0,
|
|
canvas.width,
|
|
canvas.height,
|
|
);
|
|
const data = image_data.data;
|
|
const pixels = [];
|
|
for (let i = 0; i < data.length; i += 4) {
|
|
pixels.push([data[i], data[i + 1], data[i + 2]]);
|
|
}
|
|
return pixels;
|
|
}
|
|
|
|
function create_p3(width, height, pixels) {
|
|
let header = `P3\n${width} ${height}\n255\n`;
|
|
let body = "";
|
|
for (let i = 0; i < pixels.length; i++) {
|
|
const [r, g, b] = pixels[i];
|
|
body += `${r} ${g} ${b} `;
|
|
if ((i + 1) % width === 0) body += "\n";
|
|
}
|
|
return header + body;
|
|
}
|
|
|
|
function create_p6(width, height, pixels) {
|
|
const header = `P6\n${width} ${height}\n255\n`;
|
|
const header_bytes = new TextEncoder().encode(header);
|
|
const pixel_bytes = new Uint8Array(width * height * 3);
|
|
for (let i = 0; i < pixels.length; i++) {
|
|
const [r, g, b] = pixels[i];
|
|
pixel_bytes[i * 3] = r;
|
|
pixel_bytes[i * 3 + 1] = g;
|
|
pixel_bytes[i * 3 + 2] = b;
|
|
}
|
|
const combined = new Uint8Array(
|
|
header_bytes.length + pixel_bytes.length,
|
|
);
|
|
combined.set(header_bytes, 0);
|
|
combined.set(pixel_bytes, header_bytes.length);
|
|
return combined;
|
|
}
|
|
|
|
function save_file(data, filename, mime_type) {
|
|
const blob =
|
|
data instanceof Uint8Array
|
|
? new Blob([data], { type: mime_type })
|
|
: new Blob([data], { type: mime_type });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = filename;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
save_btn.addEventListener("click", () => {
|
|
const width = canvas.width;
|
|
const height = canvas.height;
|
|
const pixels = get_pixel_data();
|
|
const format = format_select.value;
|
|
|
|
if (format === "P3") {
|
|
const ppm_text = create_p3(width, height, pixels);
|
|
save_file(
|
|
ppm_text,
|
|
"image_p3.ppm",
|
|
"image/x-portable-pixmap",
|
|
);
|
|
} else if (format === "P6") {
|
|
const ppm_binary = create_p6(width, height, pixels);
|
|
save_file(
|
|
ppm_binary,
|
|
"image_p6.ppm",
|
|
"image/x-portable-pixmap",
|
|
);
|
|
}
|
|
});
|
|
</script>
|
|
</main>
|
|
</article>
|
|
</body>
|
|
</html>
|