工作原理
如文章所述,我们使用阴影来充当屏幕上的像素。我们可以放大或缩小这些阴影,使像素变大或变小。
由于每个阴影的大小为1×1,我们可以创建一个像素艺术作品。如果我们希望每个像素为20×20,我们只需使用transform将其放大20倍:
transform: scale(20);
Javascript
第一步是使用一个简单的循环生成像素网格:
let config = {
width: 40,
height: 40,
color: 'white',
drawing: true,
eraser: false
}
let events = {
mousedown: false
}
document.getElementById('pixel-art-area').style.width = `calc(${(0.825 * config.width)}rem + ${(config.height * 2)}px)`;
document.getElementById('pixel-art-area').style.height = `calc(${(0.825 * config.height)}rem + ${(config.width * 2)}px)`;
for(let i = 0; i < config.width; ++i) {
for(let j = 0; j < config.height; ++j) {
let createEl = document.createElement('div');
createEl.classList.add('pixel');
createEl.setAttribute('data-x-coordinate', j);
createEl.setAttribute('data-y-coordinate', i);
document.getElementById('pixel-art-area').appendChild(createEl);
}
}
最终会创建一个40×40像素的网格,即1600个元素。
跟踪用户的鼠标移动
然后,我们可以使用三个事件来跟踪用户的鼠标移动:pointerdown、pointermove和pointerup。由于我们需要将这些事件应用于所有像素点,我们使用一个循环来遍历每个像素并添加事件。
然后,如果用户按下鼠标,我们可以使用e.target来确定哪个像素点被点击。
document.querySelectorAll('.pixel').forEach(function(item) {
item.addEventListener('pointerdown', function(e) {
if(config.eraser === true) {
item.setAttribute('data-color', null);
item.style.background = `#191f2b`;
} else {
item.setAttribute('data-color', config.color);
item.style.background = `${config.color}`;
}
events.mousedown = true;
});
});
document.getElementById('pixel-art-area').addEventListener('pointermove', function(e) {
if(config.drawing === true && events.mousedown === true || config.eraser === true && events.mousedown === true) {
if(e.target.matches('.pixel')) {
if(config.eraser === true) {
e.target.setAttribute('data-color', null);
e.target.style.background = `#101532`;
} else {
e.target.setAttribute('data-color', config.color);
e.target.style.background = `${config.color}`;
}
}
}
});
document.body.addEventListener('pointerup', function(e) {
events.mousedown = false;
});
文件上传
当你将图片上传时,将图片内容绘制到一个画布上。绘制图片的画布是隐藏的,因此你看不到它。
一旦图片被添加到画布上,我们可以使用getImageData()来获取每个像素。我们遍历像素,并获取每个点的颜色。如果图片的像素超过40×40,它们将被忽略 。
在下面的代码中,像素数据以R、G、B、A格式返回。我们将像素数据存储在pixelData变量中。pixelData[3]是“A”,pixelData[0]到[2]分别是R、G和B。利用这些信息,我们可以确定每个像素的RGB颜色,并忽略透明度为0的像素,即pixelData[3]等于0的像素。
// 将事件附加到输入框
document.querySelector('.select-file').addEventListener('change', function(e) {
let files = e.target.files;
// 从输入框中获取已上传的文件
let f = files[0];
let reader = new FileReader();
// 隐藏任何错误
document.querySelector('.error').classList.remove('active');
reader.onload = (async function(file) {
// 我们只接受图片...因此检查图片类型
if(file.type == "image/png" || file.type == "image/jpg" || file.type == "image/gif" || file.type == "image/jpeg") {
// 从文件中创建图片
const bitmap = await createImageBitmap(file);
const canvas = document.querySelector("canvas");
canvas.width = bitmap.width;
canvas.height = bitmap.height;
const ctx = canvas.getContext("2d");
// 清除任何之前的图片...以防万一
ctx.clearRect(0, 0, 9999, 9999);
// 并将新元素绘制到画布上
ctx.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
let constructPixelData = []
// 然后遍历每个像素,获取每个点的颜色
for(let i = 0; i < config.width; ++i) {
for(let j = 0; j < config.height; ++j) {
let pixelData = canvas.getContext('2d').getImageData(i, j, 1, 1).data;
// 像素数据以R、G、B、A格式返回。pixelData[3]是“A”,pixelData[0]到[2]分别是R、G和B。
// 如果pixelData[3]为0,则该像素的透明度为0。因此我们不会显示它。
if(pixelData[3] !== 0) {
// 将其放入一个包含x、y和颜色的数组中
constructPixelData.push({ x: i, y: j, color: `rgb(${pixelData[0]} ${pixelData[1]} ${pixelData[2]})`});
}
}
}
// 然后更新像素艺术生成器以显示这些信息。
constructPixelData.forEach(function(i) {
let getPixel = document.querySelector(`.pixel[data-x-coordinate="${i.x}"][data-y-coordinate="${i.y}"]`);
if(getPixel !== null) {
getPixel.setAttribute('data-color', i.color);
getPixel.style.background = i.color;
}
});
}
else {
document.querySelector('.error').textContent = '请选择png、jpg或gif格式的文件进行上传。';
document.querySelector('.error').classList.add('active');
}
})(f);
});
最后,我们在颜色和橡皮擦上设置了一些事件,以便我们可以追踪正在选择的工具和颜色:
[ 'click', 'input' ].forEach(function(item) {
document.querySelector('.color-picker').addEventListener(item, function() {
config.color = this.value;
document.querySelectorAll('.colors > div').forEach(function(i) {
i.classList.remove('current');
});
this.classList.add('current');
config.eraser = false;
document.querySelector('.eraser-container').classList.remove('current');
});
});
document.querySelectorAll('.colors > div').forEach(function(item) {
item.addEventListener('click', function(e) {
document.querySelector('.color-picker').classList.remove('current');
document.querySelectorAll('.colors > div').forEach(function(i) {
i.classList.remove('current');
})
item.classList.add('current');
config.eraser = false;
config.color = `${item.getAttribute('data-color')}`;
document.querySelector('.eraser-container').classList.remove('current');
})
});