paint-by-numbers

Relaxing paint-by-numbers game  https://p.ce9e.org/paint-by-numbers/
git clone https://git.ce9e.org/paint-by-numbers.git

commit
d66c204a8a78b8791a93d156e7fd9ed46550dbd9
parent
cbb4c06b777ed3cdef6c20019a8a32649ac67e85
Author
Tobias Bengfort <tobias.bengfort@posteo.de>
Date
2023-02-09 07:58
init

Diffstat

A index.html 15 +++++++++++++++
A paint.js 216 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A style.css 16 ++++++++++++++++

3 files changed, 247 insertions, 0 deletions


diff --git a/index.html b/index.html

@@ -0,0 +1,15 @@
   -1     1 <!DOCTYPE html>
   -1     2 <html>
   -1     3 <head>
   -1     4 	<meta charset="utf-8" />
   -1     5 	<title>Paint by the numbers</title>
   -1     6 	<link rel="stylesheet" href="style.css">
   -1     7 </head>
   -1     8 <body>
   -1     9 	<div>
   -1    10 		<input type="file">
   -1    11 	</div>
   -1    12 	<canvas></canvas>
   -1    13 	<script src="paint.js" type="module"></script>
   -1    14 </body>
   -1    15 </html>

diff --git a/paint.js b/paint.js

@@ -0,0 +1,216 @@
   -1     1 var input = document.querySelector('input');
   -1     2 
   -1     3 var canvas = document.querySelector('canvas');
   -1     4 var ctx = canvas.getContext('2d');
   -1     5 
   -1     6 var offcanvas = document.createElement('canvas');
   -1     7 var octx = offcanvas.getContext('2d');
   -1     8 
   -1     9 var data;
   -1    10 var pencil = 0;
   -1    11 
   -1    12 var zoom = 1;
   -1    13 var dx = 0;
   -1    14 var dy = 0;
   -1    15 
   -1    16 var onAnimation = function(fn) {
   -1    17     var called = false;
   -1    18     return () => {
   -1    19         if (!called) {
   -1    20             called = true;
   -1    21             window.requestAnimationFrame(() => {
   -1    22                 fn();
   -1    23                 called = false;
   -1    24             });
   -1    25         }
   -1    26     };
   -1    27 };
   -1    28 
   -1    29 var file2img = function(file) {
   -1    30     return new Promise((resolve, reject) => {
   -1    31         var img = new Image();
   -1    32         var url = URL.createObjectURL(file);
   -1    33 
   -1    34         img.onerror = err => {
   -1    35             URL.revokeObjectURL(url);
   -1    36             reject(err);
   -1    37         };
   -1    38         img.onload = () => {
   -1    39             URL.revokeObjectURL(url);
   -1    40             resolve(img);
   -1    41         };
   -1    42 
   -1    43         img.src = url;
   -1    44     });
   -1    45 };
   -1    46 
   -1    47 var img2data = function(img, scale) {
   -1    48     var _canvas = document.createElement('canvas');
   -1    49     _canvas.height = Math.round(img.height * scale);
   -1    50     _canvas.width = Math.round(img.width * scale);
   -1    51     var _ctx = _canvas.getContext('2d');
   -1    52     _ctx.drawImage(img, 0, 0, _canvas.width, _canvas.height);
   -1    53     return _ctx.getImageData(0, 0, _canvas.width, _canvas.height);
   -1    54 };
   -1    55 
   -1    56 var round = function(c) {
   -1    57     return Math.floor(c / 51) * 3;
   -1    58 };
   -1    59 
   -1    60 var makecolor = function(a) {
   -1    61     return '#'
   -1    62         + round(a[0]).toString(16)
   -1    63         + round(a[1]).toString(16)
   -1    64         + round(a[2]).toString(16);
   -1    65 };
   -1    66 
   -1    67 var sRGB = function(c) {
   -1    68     var x = round(c) / 15;
   -1    69     if (x < 0.04045) {
   -1    70         return x / 12.92;
   -1    71     } else {
   -1    72         return Math.pow((x + 0.055) / 1.055, 2.4);
   -1    73     }
   -1    74 };
   -1    75 
   -1    76 var makeContrast = function(a) {
   -1    77     var l = 0.2126 * sRGB(a[0]) + 0.7152 * sRGB(a[1]) + 0.0722 * sRGB(a[2]);
   -1    78     return l > 0.18 ? '#000' : '#fff';
   -1    79 }
   -1    80 
   -1    81 var analyze = function(img) {
   -1    82     var c, i, j;
   -1    83     var colors = ['white'];
   -1    84     var contrasts = ['black'];
   -1    85     var out = [];
   -1    86     for (i = 0; i < img.data.length; i += 4) {
   -1    87         c = makecolor(img.data.slice(i, i + 3));
   -1    88         j = colors.indexOf(c);
   -1    89         if (j === -1) {
   -1    90             j = colors.length;
   -1    91             colors.push(c);
   -1    92             contrasts.push(makeContrast(img.data.slice(i, i + 3)));
   -1    93         }
   -1    94         out.push(j);
   -1    95     }
   -1    96     return {
   -1    97         width: img.width,
   -1    98         height: img.height,
   -1    99         colors: colors,
   -1   100         contrasts: contrasts,
   -1   101         data: out,
   -1   102     };
   -1   103 };
   -1   104 
   -1   105 var setPixel = function(x, y, color) {
   -1   106     var i = y * data.width + x;
   -1   107     octx.fillStyle = data.colors[color];
   -1   108     octx.fillRect(x * 10, y * 10, 10, 10);
   -1   109     if (color !== data.data[i]) {
   -1   110         octx.fillStyle = data.contrasts[color];
   -1   111         octx.fillText(data.data[i], x * 10 + 5, y * 10 + 5);
   -1   112     }
   -1   113 };
   -1   114 
   -1   115 input.addEventListener('change', () => {
   -1   116     file2img(input.files[0]).then(img => {
   -1   117         // FIXME: configurable size
   -1   118         data = analyze(img2data(img, 100 / img.width));
   -1   119         offcanvas.width = data.width * 10;
   -1   120         offcanvas.height = data.height * 10;
   -1   121         octx.textAlign = 'center';
   -1   122         octx.textBaseline = 'middle';
   -1   123 
   -1   124         zoom = canvas.height / offcanvas.height * 0.8;
   -1   125         dx = (canvas.width - offcanvas.width * zoom) / 2;
   -1   126         dy = (canvas.height - offcanvas.height * zoom) / 2;
   -1   127 
   -1   128         // FIXME: zoom to fit and center
   -1   129 
   -1   130         var x, y;
   -1   131         for (y = 0; y < data.height; y++) {
   -1   132             for (x = 0; x < data.width; x++) {
   -1   133                 setPixel(x, y, 0);
   -1   134             }
   -1   135         }
   -1   136 
   -1   137         render();
   -1   138     });
   -1   139 });
   -1   140 
   -1   141 var resizeCanvas = function() {
   -1   142     var rect = canvas.getBoundingClientRect();
   -1   143 
   -1   144     dx += (rect.width - canvas.width) / 2;
   -1   145     dy += (rect.height - canvas.height) / 2;
   -1   146 
   -1   147     canvas.width = rect.width;
   -1   148     canvas.height = rect.height;
   -1   149 
   -1   150     render();
   -1   151 };
   -1   152 
   -1   153 var render = onAnimation(function() {
   -1   154     ctx.clearRect(0, 0, canvas.width, canvas.height);
   -1   155     ctx.drawImage(offcanvas, dx, dy, offcanvas.width * zoom, offcanvas.height * zoom);
   -1   156 });
   -1   157 
   -1   158 window.addEventListener('resize', resizeCanvas);
   -1   159 resizeCanvas();
   -1   160 
   -1   161 window.addEventListener('wheel', event => {
   -1   162     var rect = canvas.getBoundingClientRect();
   -1   163     var cx = event.clientX - rect.x;
   -1   164     var cy = event.clientY - rect.y;
   -1   165 
   -1   166     var ocx = (cx - dx) / zoom;
   -1   167     var ocy = (cy - dy) / zoom;
   -1   168 
   -1   169     zoom *= Math.pow(2, -event.deltaY / 100 / 100);
   -1   170 
   -1   171     dx = cx - ocx * zoom;
   -1   172     dy = cy - ocy * zoom;
   -1   173 
   -1   174     render();
   -1   175 });
   -1   176 
   -1   177 window.addEventListener('keydown', event => {
   -1   178     // FIXME: kinetic movement;
   -1   179     var step = 10;
   -1   180     if (event.key === 'w') {
   -1   181         dy += step;
   -1   182     } else if (event.key === 'a') {
   -1   183         dx += step;
   -1   184     } else if (event.key === 's') {
   -1   185         dy -= step;
   -1   186     } else if (event.key === 'd') {
   -1   187         dx -= step;
   -1   188     } else if (event.key === 'q') {
   -1   189         pencil -= 1;
   -1   190     } else if (event.key === 'e') {
   -1   191         pencil += 1;
   -1   192     }
   -1   193     render();
   -1   194 });
   -1   195 
   -1   196 var onClick = function(event) {
   -1   197     if (event.buttons & 1) {
   -1   198         var rect = canvas.getBoundingClientRect();
   -1   199         var cx = event.clientX - rect.x;
   -1   200         var cy = event.clientY - rect.y;
   -1   201 
   -1   202         var ocx = (cx - dx) / zoom;
   -1   203         var ocy = (cy - dy) / zoom;
   -1   204 
   -1   205         var x = Math.floor(ocx / 10);
   -1   206         var y = Math.floor(ocy / 10);
   -1   207 
   -1   208         if (x >= 0 && x < data.width && y >= 0 && y < data.height) {
   -1   209             setPixel(x, y, pencil);
   -1   210             render();
   -1   211         }
   -1   212     }
   -1   213 };
   -1   214 
   -1   215 window.addEventListener('mousemove', onClick);
   -1   216 window.addEventListener('mousedown', onClick);

diff --git a/style.css b/style.css

@@ -0,0 +1,16 @@
   -1     1 html,
   -1     2 body {
   -1     3 	margin: 0;
   -1     4 	padding: 0;
   -1     5 }
   -1     6 
   -1     7 body {
   -1     8 	display: grid;
   -1     9 	grid-template-rows: min-content 1fr;
   -1    10 	height: 100vh;
   -1    11 }
   -1    12 
   -1    13 canvas {
   -1    14 	width: 100%;
   -1    15 	height: 100%;
   -1    16 }