1 722 Codepen

Изображение точками, линиями, буквами, смайликами на 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,";
}

Комментарии

  • Facebook
  • Вконтакте

Похожие статьи