Изображение точками, линиями, буквами, смайликами на canvas
Оригинальная фотография рисуется точками, линиями, буквами, смайликами на canvas. Можно задать количество элементов из которых будет перерисовано новое изображение.
HTML
<h1>
<select id="elStyle">
<option>original</option>
<option selected="selected">char</option>
<option>dots</option>
<option>emoji</option>
<option>line</option>
</select>
<select id="elSample">
<option selected="selected">cat</option>
<option>face</option>
</select>
<select id="elKernel">
<option>3</option>
<option selected="selected">5</option>
<option>7</option>
<option>11</option>
<option>19</option>
</select>
</h1>
<div class="frame" id="elFrame"><img id="elImg"/></div>
CSS
body {
display:flex; flex-flow:column nowrap;
justify-content:center;
align-items:center;
min-height:100vh;
}
select {
margin-bottom:0.5em;
}
.frame {
position:relative;
width:90vw;
height:90vh;
}
.frame canvas, .frame img {
object-fit:contain;
width:100%;
height:100%;
display:block;
position:absolute;top:0;left:0;
}
#elImg {
opacity:0;
transition:all 1s;
}
#elImg.show {
opacity:1;
}
JS
Картинки вставлены в JS, через base64, но это из-за ограничений платформы codepen.const FPS = 60;
const DPF = 512; // dots per frame
const drawFnHash = {};
drawFnHash.emoji = function({ctx, cx, cy, s, kernel}) {
const r = (kernel) * (1 - s);
const ch = "?";
ctx.font = `${r}px monospace`;
ctx.fillText(ch, cx-kernel/2, cy+kernel/2);
}
drawFnHash.char = function({ctx, cx, cy, s, kernel}) {
const base = kernel*2;
const fontsize = base * (1 - s);
const ch = String.fromCodePoint(65 + Math.random() *26|0);
ctx.font = `${fontsize}px monospace`;
ctx.fillText(ch, cx-(fontsize*0.8)/2, cy+fontsize/2);
}
drawFnHash.dots = function({ctx, cx, cy, s, kernel}) {
const r = (kernel/2) * (1 - s); // no overlap
ctx.beginPath();
ctx.ellipse(cx, cy, r, r, 0, 0, 2* Math.PI)
ctx.fill();
ctx.closePath();
};
drawFnHash.line = function({ctx, cx, cy, s, kernel}) {
const r = (kernel/2) * (1 - s);
ctx.beginPath();
ctx.moveTo(cx-kernel/2, cy-kernel/2);
ctx.lineTo(cx+kernel/2, cy+kernel/2);
ctx.lineWidth = r;
ctx.stroke();
ctx.closePath();
}
//
// reg ui event
//
function onDone({onCanvas}) {
enableAllControls();
}
function onerror(err) {
console.error(err);
for (const optionEl of Array.from(elStyle.children))
optionEl.selected = false;
elStyle.children[0].selected = true; // original
enableAllControls();
}
function enableAllControls() {
elStyle.disabled = false;
elSample.disabled = false;
elKernel.disabled = false;
}
function disableAllControls() {
elStyle.disabled = true;
elSample.disabled = true;
elKernel.disabled = true;
}
function oninput() {
const index = elStyle.selectedIndex;
const imgsrc = imageDataUrl(elSample.children[elSample.selectedIndex].value);
const oldCanvas = elFrame.querySelector("canvas");
if (oldCanvas != null)
elFrame.removeChild(oldCanvas);
if (index === 0) {
elImg.classList.add("show");
elImg.src = imgsrc;
return;
}
const drawFn = drawFnHash[elStyle.children[index].value];
const kernel = Number(elKernel.children[elKernel.selectedIndex].value);
const frameEl = elFrame;
elImg.classList.remove("show");
disableAllControls();
fx({frameEl, drawFn, kernel, imgsrc})
.then(onDone)
.catch(onerror);
}
elStyle.addEventListener("input", oninput);
elSample.addEventListener("input", oninput);
elKernel.addEventListener("input", oninput);
oninput();
//
// fx flow
//
function fx({frameEl, drawFn, kernel, imgsrc}) {
const el = frameEl.querySelector("img");
//return localize(imgsrc) //-> blob url // already "429" from proxy. LOL.
return Promise.resolve(imgsrc)
.then(toImage()) //-> Image{}
.then(setup({el})) //-> {Canvas{} onCanvas, Image{}}
.then(halftone({kernel})) //-> {halftoneData{}, Canvas{} onCanvas}
.then(render({dotsPerFrame:DPF, fps:FPS, drawFn})); //-> Promise{}
}
//
// bypass cors protocol (not used)
//
function localize(src) {
const proxy = `https://cors-anywhere.herokuapp.com/`;
const origin = `${window.location.protocol}//${window.location.host}`;
return fetch(`${proxy}${src}`, {headers: {origin}})
.then(respond => respond.blob())
.then(blob => URL.createObjectURL(blob));
}
//
// create an Image{} with a blob url
//
function toImage() {
return function(url) {
return new Promise(function (resolve, reject) {
const image = new Image();
image.addEventListener("load", e => resolve(image));
image.addEventListener("error", e => reject(e));
image.src = url;
});
}
}
//
// halftone fx
//
function halftone({kernel=10}) { // support subregion maybe good (?)
function grayscale({r, g, b}) { // in 0..255
return 0.2 * r/255 + 0.7 * g/255 + 0.1 * b/255; // in 0..1
}
function samplize({x, y, imageData}) { // x, y = top left corner of kernel
const {width, height, data} = imageData;
const samples = [];
for (let i = y; i < y+kernel; i++) {
for (let j = x; j < x+kernel; j++) {
const at = (i * width + j) * 4;
const r = data[at];
const g = data[at + 1];
const b = data[at + 2];
samples.push({r, g, b});
}
}
let sum = 0;
for (const sample of samples)
sum += grayscale(sample);
return sum / samples.length; // avg
}
return function ({onCanvas, imageData}) {
const halftoneData = { // struct is inspired by ImageData{}
kernel: kernel, // kernel size, n x n
width: imageData.width/kernel|0, // in dot
height: imageData.height/kernel|0, // in dot
dаta:[] // array of num (normalized)
};
for (let y = 0; y <= imageData.height - kernel; y+=kernel) {
for (let x = 0; x <= imageData.width - kernel; x+=kernel) {
halftoneData.data.push(samplize({x, y, imageData}));
}
}
return {halftoneData, onCanvas};
};
}
//
// lazy rendering, draw partial dots on each frame
//
function render({dotsPerFrame=1, fps=10, drawFn}) {
return function({halftoneData, onCanvas}) {
return new Promise((resolve, reject) => {
const ctx = onCanvas.getContext("2d");
const {width, height, kernel, data} = halftoneData; console.log(width, height)
const dotsCount = width * height;
let dotsDrawn = 0;
(function tick() {
for(let i = 0; i < dotsPerFrame && dotsDrawn < dotsCount; i++) {
const cx = (dotsDrawn % width) * kernel + kernel/2;
const cy = (dotsDrawn / width | 0) * kernel + kernel/2;
const s = data[dotsDrawn];
drawFn({ctx, cx, cy, s, kernel});
dotsDrawn += 1;
}
if (dotsDrawn < dotsCount)
setTimeout(tick, 1000/fps);
else
resolve({onCanvas});
}());
});
}
}
//
// add onscreen canvas as a subs. of <img> to container
// create offscreen canvas to get pixel data of <img>
//
function setup({el}) {
return function (image) {
const onCanvas = document.createElement("canvas");
onCanvas.width = image.width;
onCanvas.height = image.height;
const onContext = onCanvas.getContext("2d");
onContext.fillStyle = "white";
onContext.fillRect(0,0,onCanvas.width,onCanvas.height);
onContext.fillStyle = "black";
el.parentNode.appendChild(onCanvas);
const offCanvas = document.createElement("canvas");
offCanvas.width = onCanvas.width;
offCanvas.height = onCanvas.height;
const offContext = offCanvas.getContext("2d");
offContext.drawImage(image, 0, 0, offCanvas.width, offCanvas.height);
const imageData = offContext.getImageData(0,0, offCanvas.width, offCanvas.height);
return {onCanvas, imageData};
}
}
//
// embed image data (just because proxy has been 429)
// for demo its fine. (codepen UI may lag)
//
function imageDataUrl(name) {
switch (name) {
case "cat": return sourceCat();
case "face": return sourceFace();
}
}
// тут две картинки в формате base64
function sourceCat() {
return "dаta:image/jpeg;base64,";
}
function sourceFace() {
return "dаta:image/jpeg;base64,";
}