Есть картинка... пробую сделать hover... Логика такая:
Вроде бы получилось..., код ниже.
Однако, проблема возникла при попытке сделать холст адаптивными, к размеру экрана (и соответственно картинки)... моя реализация hover'a перестает работать...
За масштаб у меня отвечают переменные scale_X
и scale_Y
, в коде ниже определение масштаба закомментировано.
Кто может помочь в решении проблемы?
const WIDTH = 800;
const HEIGHT = 374;
let scale_X = 1; // масштаб
let scale_Y = 1; // масштаб
let ctxStyles;
let hover;
const canvas = document.getElementById('canvas');
//const help = document.getElementById('helper');
const help = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const helper = help.getContext('2d');
const real_width = document.documentElement.clientWidth;
const my_width = (real_width / 100) * 70;
const my_height = my_width / 2.14;
//scale_X = my_width / WIDTH;
//scale_Y = my_height / HEIGHT;
canvas.width = my_width;
canvas.height = my_height;
help.width = my_width;
help.height = my_height;
ctxStyles = canvas.getBoundingClientRect();
canvas.addEventListener('mousemove', handlerMousemove);
const img = new Image();
//img.src = 'https://i.stack.imgur.com/R5QJI.gif';
img.crossOrigin = "anonymous";
img.src = 'https://i.ibb.co/pfnXvqk/barraks-4-2.gif';
img.onload = function() {
img.coords = {
x: 40,
y: 30
};
getPixArr(img);
drawImgOnCanvas(img);
};
function drawImgOnCanvas(img) {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
const arr = [
img,
0,
0,
img.width / 2,
img.height,
img.coords.x * scale_X,
img.coords.y * scale_Y,
(img.width / 2) * scale_X,
img.height * scale_Y
];
ctx.drawImage(...arr);
// отрисовывает 2-ю часть картинки, на которой нарисован контур
if (hover) {
arr[1] = img.width / 2;
arr[3] = img.width;
arr[5] = img.coords.x * scale_X - 1;
arr[7] = img.width * scale_X;
ctx.drawImage(...arr);
}
}
function handlerMousemove(event) {
const mouseX = event.clientX - ctxStyles.left;
const mouseY = event.clientY - ctxStyles.top;
hover = false;
if (check_Mouse_On_Img(mouseX, mouseY, img)) {
const notTransparent = check_not_transparent_pixel(mouseX, mouseY, img);
if (notTransparent) {
hover = true;
drawImgOnCanvas(img);
} else {
drawImgOnCanvas(img);
}
}
}
function getPixArr(img) {
const ctx = helper;
const imgWidth = img.width;
const imgHeight = img.height;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.drawImage(
img,
0,
0,
imgWidth / 2,
imgHeight,
0,
0,
(imgWidth / 2) * scale_X,
imgHeight * scale_Y
);
const pixArr = ctx.getImageData(
0,
0,
(imgWidth / 2) * scale_X,
imgHeight * scale_Y
);
img.pixArr = pixArr;
}
function check_Mouse_On_Img(mouseX, mouseY, img) {
// const ctx = helper
const topX = img.coords.x * scale_X;
const topY = img.coords.y * scale_Y;
const leftX = topX;
const leftY = topY + img.pixArr.height;
const rigthX = topX + img.pixArr.width;
const rigthY = topY;
const bottomX = rigthX;
const bottomY = leftY;
ctx.beginPath();
// ctx.strokeStyle = 'red';
ctx.strokeStyle = "transparent";
ctx.moveTo(leftX, leftY);
ctx.lineTo(topX, topY);
ctx.lineTo(rigthX, rigthY);
ctx.lineTo(bottomX, bottomY);
ctx.lineTo(leftX, leftY);
ctx.stroke();
ctx.closePath();
return ctx.isPointInPath(mouseX, mouseY);
}
//определяет прозрачный ли пиксель
function check_not_transparent_pixel(clientX, clientY, img) {
const imgX = img.coords.x;
const imgY = img.coords.y;
// const imgX = img.coords.x * scale_X;
// const imgY = img.coords.y * scale_Y;
const mouseX = clientX - imgX; //т.к. массив пикселей построен из начальных координат 0:0, делаю сдвиг
const mouseY = clientY - imgY;
const pixArr = img.pixArr;
const index = Math.floor(get_Pix_Index(mouseX, mouseY, pixArr));
let alpha = pixArr.data[index + 3]; //прозрачность
if (index > pixArr.data.length) {
return false;
}
if (alpha > 0) {
return true;
}
return false;
//определяет индекс пикселя в массиве пикселей
function get_Pix_Index(mouseX, mouseY, pixArr) {
let pixel;
if (mouseX == 0 && mouseY > 0) {
pixel = (pixArr.width * scale_X) * mouseY + 1;
} else if (mouseY == 0 && mouseX > 0) {
pixel = mouseX;
} else {
pixel = mouseY * (pixArr.width* scale_X) + mouseX - 1;
}
let index = pixel * 4;
return index;
}
}
// просто для тестов
canvas.addEventListener('click', event => {
const mouseX = event.clientX - ctxStyles.left;
const mouseY = event.clientY - ctxStyles.top;
console.log(mouseX, mouseY);
ctx.beginPath();
ctx.arc(mouseX, mouseY, 2, 0, 2 * Math.PI);
ctx.fill();
ctx.closePath();
ctx.putImageData(img.pixArr, mouseX, mouseY);
});
body{
height: 100vh;
width: 100vw;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
canvas{
border: 1px solid;
}
<canvas id="canvas" ></canvas>
<canvas id="helper" style="position: fixed; left: -300%"></canvas>
П.С... и дополнительный вопрос: прозрачные пиксели картинки определяются как не прозрачные, если вспомогательный холст спрятать с помощью атрибута hidden
или display: none
- почему?
Это старый способ, я называю его «пикинг буфер», суть в дополнительной картинке, в которой все объекты покрашены в уникальные цвета.
При старте приложения я сделал дубликаты спрайтов, заменив в них все не полностью прозрачные пиксели на цвет уникальный для каждого спрайта (цвет-идентификатор) и рисую их параллельно с основной канвой на скрытой канве, с которой при перемещении мыши беру цвет и определяю что под мышкой.
В примере работает наведение и перетаскивание
let imgCtx = scene.getContext("2d");
let pickCtx = pick.getContext("2d");
// это важно для того чтобы сглаживанием не изменялось значение идентификатора
pickCtx.imageSmoothingEnabled = false;
let index = {};
let objects = Array(22).fill(0).map((e, i) => img(i));
let bounds = scene.getBoundingClientRect();
let hover, drag, x, y, sx, sy;
scene.addEventListener("mousemove", e => {
drag ? handleDrag(e) : handleHover(e);
draw();
});
scene.addEventListener("mousedown", e => {
if (hover) {
drag = hover;
x = drag.x - e.pageX + bounds.left;
y = drag.y - e.pageY + bounds.top;
objects.splice(objects.indexOf(drag), 1);
objects.push(drag)
draw();
}
});
scene.addEventListener("mouseup", e => drag = false);
draw();
function handleDrag(e) {
drag.x = x + e.pageX - bounds.left
drag.y = y + e.pageY - bounds.top
}
function handleHover(e) {
if (hover)
hover.active = false;
let id = pickCtx.getImageData(
e.pageX - bounds.left,
e.pageY - bounds.top,
1, 1
).data;
hover = id[3] > 200 ? index[`${id[0]}-${id[1]}-${id[2]}`] : null;
scene.style.cursor = hover ? "pointer" : "default";
if (hover)
hover.active = true;
}
function draw() {
pickCtx.clearRect(0, 0, scene.width, scene.height);
imgCtx.clearRect(0, 0, scene.width, scene.height);
objects.forEach(img => {
imgCtx.drawImage(img.img, img.x, img.y);
pickCtx.drawImage(img.pick, img.x, img.y);
});
hover && imgCtx.drawImage(hover.hover, hover.x, hover.y);
}
function rnd(){
return Math.round(Math.random()*255)
}
function img() {
let id;
while (!id || index[id.join('-')])
id = [rnd(), rnd(), rnd()]
let size = 100;
let img = document.createElement("canvas");
img.width = img.height = size;
let ctx = img.getContext("2d");
var grd = ctx.createLinearGradient(
size*0.1, Math.random() * size,
size*0.9, Math.random() * size
);
grd.addColorStop(0, `hsl(${Math.random() * 255},55%,55%)`);
grd.addColorStop(1, `hsl(${Math.random() * 255},55%,55%)`);
ctx.fillStyle = grd;
ctx.translate(size / 2, size / 2);
ctx.rotate(Math.random() * 6);
ctx.translate(-size / 2, -size / 2);
let s = 20 + Math.random() * 40;
ctx.fillRect((size - s) / 2, (size - s) / 2, s, s);
ctx.fillRect((size - s) / 4, (size - s) / 4, s, s);
return index[id.join('-')] = {
x: Math.random() * (scene.width - img.width),
y: Math.random() * (scene.height - img.height),
id: id,
img: img,
pick: createPickImage(img, id),
hover: ImageSDF(img)
};
}
// создает изображения для рисования в пикинг буфере
// красит все не полностью прозрачные пиксели в цвет-идентификатор
function createPickImage(img, pickColor) {
let pick = document.createElement("canvas");
let w = pick.width = img.width;
let h = pick.height = img.height;
let ctx = pick.getContext("2d");
ctx.drawImage(img, 0, 0);
var img = ctx.getImageData(0, 0, w, h);
for (var x = 0; x < w; x++) {
for (var y = 0; y < h; y++) {
let o = (y * w + x) * 4;
// если прозрачность не 0 - красим пиксель в цвет-идентификатор
if (img.data[o + 3]) {
img.data[o + 0] = pickColor[0];
img.data[o + 1] = pickColor[1];
img.data[o + 2] = pickColor[2];
// рисование полупрозрачных пикселей в пикинг буфер - источник ошибок
// пусть все не полностью прозрачные пиксели будут польностью непрозрачные
img.data[o + 3] = 255;
}
}
}
ctx.putImageData(img, 0, 0);
return pick;
}
// эта функция создает signed distance field до изображения на входе
// визуально это градиент, его мы будем использовать для обводки
function ImageSDF(image) {
let INF = 1e20;
let radius = 3;
let cutoff = 0.1;
let canvas = document.createElement('canvas');
let width = canvas.width = image.width;
let height = canvas.height = image.height;
let ctx = canvas.getContext('2d');
// temporary arrays for the distance transform
let gridOuter = new Float64Array(width * height);
let gridInner = new Float64Array(width * height);
let f = new Float64Array(height);
let d = new Float64Array(width);
let z = new Float64Array(width + 1);
let v = new Int16Array(width);
ctx.clearRect(0, 0, width, height);
ctx.drawImage(image, 0, 0, width, height);
var imgData = ctx.getImageData(0, 0, width, height);
for (i = 0; i < width * height; i++) {
imgData.data[i * 4 + 0] = imgData.data[i * 4 + 3];
imgData.data[i * 4 + 1] = imgData.data[i * 4 + 3];
imgData.data[i * 4 + 2] = imgData.data[i * 4 + 3];
}
for (var i = 0; i < width * height; i++) {
var a = imgData.data[i * 4 + 1] / 255; // green channe value
gridOuter[i] = a === 1 ? 0 : a === 0 ? INF : Math.pow(Math.max(0, 0.5 - a), 2);
gridInner[i] = a === 1 ? INF : a === 0 ? 0 : Math.pow(Math.max(0, a - 0.5), 2);
}
edt(gridOuter, width, height, f, d, v, z);
edt(gridInner, width, height, f, d, v, z);
for (i = 0; i < width * height; i++) {
var dd = gridOuter[i] - gridInner[i];
let v = Math.round(255 - 255 * (dd / radius + cutoff));
v = Math.max(0, Math.min(255, v));
imgData.data[i * 4 + 0] = v>10 && v<245 ? 255 :0;
imgData.data[i * 4 + 3] = v>10 && v<245 ? 255 :0;
}
ctx.putImageData(imgData, 0, 0);
return canvas;
}
// 2D Euclidean distance transform by Felzenszwalb & Huttenlocher https://cs.brown.edu/~pff/dt/
function edt(data, width, height, f, d, v, z) {
for (var x = 0; x < width; x++) {
for (var y = 0; y < height; y++)
f[y] = data[y * width + x];
edt1d(f, d, v, z, height);
for (y = 0; y < height; y++)
data[y * width + x] = d[y];
}
for (y = 0; y < height; y++) {
for (x = 0; x < width; x++)
f[x] = data[y * width + x];
edt1d(f, d, v, z, width);
for (x = 0; x < width; x++)
data[y * width + x] = Math.sqrt(d[x]);
}
}
// 1D squared distance transform
function edt1d(f, d, v, z, n) {
let INF = 1e20;
v[0] = 0;
z[0] = -INF;
z[1] = +INF;
for (var q = 1, k = 0; q < n; q++) {
var s = ((f[q] + q * q) - (f[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k]);
while (s <= z[k]) {
k--;
s = ((f[q] + q * q) - (f[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k]);
}
k++;
v[k] = q;
z[k] = s;
z[k + 1] = +INF;
}
for (q = 0, k = 0; q < n; q++) {
while (z[k + 1] < q) k++;
d[q] = (q - v[k]) * (q - v[k]) + f[v[k]];
}
}
<canvas id="scene" width="635" height="175" style="border: 1px solid"></canvas>
<canvas id="pick" width="635" height="175" style="display:none"></canvas>
PS: добавил программную обводку непрозрачной области спрайта при помощи signed distance field, зашитого в текстуру
UPD: сильно уменьшил вероятность ошибок и расширил возможное количество объектов, за счет использования 3х каналов цвета для пикинга
UPD2: усложнил форму элементов для наглядности
связанный ответ: https://ru.stackoverflow.com/a/962780/188366
Решил проблему... как я и предположил, в комментариях, проблема была в определении индекса пикселя. Однако, оказалось, что округлять необходимо не полученный индекс, а координаты мыши
const WIDTH = 800;
const HEIGHT = 374;
let scale_X = 1;
let scale_Y = 1;
let ctxStyles;
let hover;
const canvas = document.getElementById('canvas');
// const help = document.getElementById('helper');
const help = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const helper = help.getContext('2d');
const real_width = document.documentElement.clientWidth;
const my_width = (real_width / 100) * 70;
const my_height = my_width / 2.14;
scale_X = my_width / WIDTH;
scale_Y = my_height / HEIGHT;
canvas.width = my_width;
canvas.height = my_height;
help.width = my_width;
help.height = my_height;
ctxStyles = canvas.getBoundingClientRect();
canvas.addEventListener('mousemove', handlerMousemove);
const img = new Image();
img.crossOrigin = "anonymous";
img.src = 'https://i.ibb.co/pfnXvqk/barraks-4-2.gif';
img.onload = function() {
img.coords = {
x: 40,
y: 30
};
getPixArr(img);
drawImgOnCanvas(img);
};
function drawImgOnCanvas(img) {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
const arr = [
img,
0,
0,
img.width / 2,
img.height,
img.coords.x * scale_X,
img.coords.y * scale_Y,
(img.width / 2) * scale_X,
img.height * scale_Y
];
ctx.drawImage(...arr);
// отрисовывает 2-ю часть картинки, на которой нарисован контур
if (hover) {
arr[1] = img.width / 2;
arr[3] = img.width;
arr[5] = img.coords.x * scale_X - 1;
arr[7] = img.width * scale_X;
ctx.drawImage(...arr);
}
}
function handlerMousemove(event) {
const mouseX = event.clientX - ctxStyles.left;
const mouseY = event.clientY - ctxStyles.top;
hover = false;
if (check_Mouse_On_Img(mouseX, mouseY, img)) {
const notTransparent = check_not_transparent_pixel(mouseX, mouseY, img);
if (notTransparent) {
hover = true;
drawImgOnCanvas(img);
} else {
drawImgOnCanvas(img);
}
}
}
function getPixArr(img) {
const ctx = helper;
const imgWidth = img.width;
const imgHeight = img.height;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.drawImage(
img,
0,
0,
imgWidth / 2,
imgHeight,
0,
0,
(imgWidth / 2) * scale_X,
imgHeight * scale_Y
);
const pixArr = ctx.getImageData(
0,
0,
(imgWidth / 2) * scale_X,
imgHeight * scale_Y
);
img.pixArr = pixArr;
}
function check_Mouse_On_Img(mouseX, mouseY, img) {
// const ctx = helper
const topX = img.coords.x * scale_X;
const topY = img.coords.y * scale_Y;
const leftX = topX;
const leftY = topY + (img.pixArr.height);
const rigthX = topX + (img.pixArr.width);
const rigthY = topY;
const bottomX = rigthX;
const bottomY = leftY;
ctx.beginPath();
ctx.strokeStyle = 'red';
// ctx.strokeStyle = "transparent";
ctx.moveTo(leftX, leftY);
ctx.lineTo(topX, topY);
ctx.lineTo(rigthX, rigthY);
ctx.lineTo(bottomX, bottomY);
ctx.lineTo(leftX, leftY);
ctx.stroke();
ctx.closePath();
return ctx.isPointInPath(mouseX, mouseY);
}
//определяет прозрачный ли пиксель
function check_not_transparent_pixel(clientX, clientY, img) {
const imgX = img.coords.x * scale_X;
const imgY = img.coords.y * scale_Y;
const mouseX = Math.floor(clientX - imgX); //т.к. массив пикселей построен из начальных координат 0:0, делаю сдвиг
const mouseY = Math.floor(clientY - imgY);
const pixArr = img.pixArr;
const index = get_Pix_Index(mouseX, mouseY, pixArr);
let alpha = pixArr.data[index + 3]; //прозрачность
if (index > pixArr.data.length) {
return false;
}
if (alpha > 0) {
return true;
}
return false;
//определяет индекс пикселя в массиве пикселей
function get_Pix_Index(mouseX, mouseY, pixArr) {
let pixel;
if (mouseX == 0 && mouseY > 0) {
pixel = pixArr.width * mouseY + 1;
} else if (mouseY == 0 && mouseX > 0) {
pixel = mouseX;
} else {
pixel = mouseY * pixArr.width + mouseX - 1;
}
let index =pixel * 4;
return index;
}
}
// просто для тестов
canvas.addEventListener('click', event => {
const mouseX = event.clientX - ctxStyles.left;
const mouseY = event.clientY - ctxStyles.top;
console.log(mouseX, mouseY);
ctx.beginPath();
ctx.arc(mouseX, mouseY, 2, 0, 2 * Math.PI);
ctx.fill();
ctx.closePath();
ctx.putImageData(img.pixArr, mouseX, mouseY);
});
body{
height: 100vh;
width: 100vw;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
canvas{
border: 1px solid;
}
<canvas id="canvas" ></canvas>
Айфон мало держит заряд, разбираемся с проблемой вместе с AppLab
Перевод документов на английский язык: Важность и ключевые аспекты
Есть некоторые представленияНужно вывести их в QTableView, а так же запрос всех представления существующих в БД, результат это
Подскажите, как разрешить следующую ситуацию - у меня есть базовый контейнер от которого наследуется 2 различных:
Например в консольных программах на языке си или c++ я могу использовать работу с сетью постепенноНу тоесть
Выпустили мы программу в продажу,у кого-то крашит у кого-то нетУ разработчика - нет,но у некоторых юзеров - да