有没有人在 javascript 中实现了洪水填充算法以与 HTML Canvas 一起使用?
我的要求很简单:从一个点开始用单一颜色填充,其中边界颜色是大于指定点颜色的某个增量的任何颜色。
var r1, r2; // red values
var g1, g2; // green values
var b1, b2; // blue values
var actualColorDelta = Math.sqrt((r1 - r2)*(r1 - r2) + (g1 - g2)*(g1 - g2) + (b1 - b2)*(b1 - b2))
function floodFill(canvas, x, y, fillColor, borderColorDelta) {
...
}
var ImageProcessing;
ImageProcessing = {
/* Convert HTML color (e.g. "#rrggbb" or "#rrggbbaa") to object with properties r, g, b, a.
* If no alpha value is given, 255 (0xff) will be assumed.
*/
toRGB: function (color) {
var r, g, b, a, html;
html = color;
// Parse out the RGBA values from the HTML Code
if (html.substring(0, 1) === "#")
{
html = html.substring(1);
}
if (html.length === 3 || html.length === 4)
{
r = html.substring(0, 1);
r = r + r;
g = html.substring(1, 2);
g = g + g;
b = html.substring(2, 3);
b = b + b;
if (html.length === 4) {
a = html.substring(3, 4);
a = a + a;
}
else {
a = "ff";
}
}
else if (html.length === 6 || html.length === 8)
{
r = html.substring(0, 2);
g = html.substring(2, 4);
b = html.substring(4, 6);
a = html.length === 6 ? "ff" : html.substring(6, 8);
}
// Convert from Hex (Hexidecimal) to Decimal
r = parseInt(r, 16);
g = parseInt(g, 16);
b = parseInt(b, 16);
a = parseInt(a, 16);
return {r: r, g: g, b: b, a: a};
},
/* Get the color at the given x,y location from the pixels array, assuming the array has a width and height as given.
* This interprets the 1-D array as a 2-D array.
*
* If useColor is defined, its values will be set. This saves on object creation.
*/
getColor: function (pixels, x, y, width, height, useColor) {
var redIndex = y * width * 4 + x * 4;
if (useColor === undefined) {
useColor = { r: pixels[redIndex], g: pixels[redIndex + 1], b: pixels[redIndex + 2], a: pixels[redIndex + 3] };
}
else {
useColor.r = pixels[redIndex];
useColor.g = pixels[redIndex + 1]
useColor.b = pixels[redIndex + 2];
useColor.a = pixels[redIndex + 3];
}
return useColor;
},
setColor: function (pixels, x, y, width, height, color) {
var redIndex = y * width * 4 + x * 4;
pixels[redIndex] = color.r;
pixels[redIndex + 1] = color.g,
pixels[redIndex + 2] = color.b;
pixels[redIndex + 3] = color.a;
},
/*
* fill: Flood a canvas with the given fill color.
*
* Returns a rectangle { x, y, width, height } that defines the maximum extent of the pixels that were changed.
*
* canvas .................... Canvas to modify.
* fillColor ................. RGBA Color to fill with.
* This may be a string ("#rrggbbaa") or an object of the form { r: red, g: green, b: blue, a: alpha }.
* x, y ...................... Coordinates of seed point to start flooding.
* bounds .................... Restrict flooding to this rectangular region of canvas.
* This object has these attributes: { x, y, width, height }.
* If undefined or null, use the whole of the canvas.
* stopFunction .............. Function that decides if a pixel is a boundary that should cause
* flooding to stop. If omitted, any pixel that differs from seedColor
* will cause flooding to stop. seedColor is the color under the seed point (x,y).
* Parameters: stopFunction(fillColor, seedColor, pixelColor).
* Returns true if flooding shoud stop.
* The colors are objects of the form { r: red, g: green, b: blue, a: alpha }
*/
fill: function (canvas, fillColor, x, y, bounds, stopFunction) {
// Supply default values if necessary.
var ctx, minChangedX, minChangedY, maxChangedX, maxChangedY, wasTested, shouldTest, imageData, pixels, currentX, currentY, currentColor, currentIndex, seedColor, tryX, tryY, tryIndex, boundsWidth, boundsHeight, pixelStart, fillRed, fillGreen, fillBlue, fillAlpha;
if (Object.isString(fillColor)) {
fillColor = ImageProcessing.toRGB(fillColor);
}
x = Math.round(x);
y = Math.round(y);
if (bounds === null || bounds === undefined) {
bounds = { x: 0, y: 0, width: canvas.width, height: canvas.height };
}
else {
bounds = { x: Math.round(bounds.x), y: Math.round(bounds.y), width: Math.round(bounds.y), height: Math.round(bounds.height) };
}
if (stopFunction === null || stopFunction === undefined) {
stopFunction = new function (fillColor, seedColor, pixelColor) {
return pixelColor.r != seedColor.r || pixelColor.g != seedColor.g || pixelColor.b != seedColor.b || pixelColor.a != seedColor.a;
}
}
minChangedX = maxChangedX = x - bounds.x;
minChangedY = maxChangedY = y - bounds.y;
boundsWidth = bounds.width;
boundsHeight = bounds.height;
// Initialize wasTested to false. As we check each pixel to decide if it should be painted with the new color,
// we will mark it with a true value at wasTested[row = y][column = x];
wasTested = new Array(boundsHeight * boundsWidth);
/*
$R(0, bounds.height - 1).each(function (row) {
var subArray = new Array(bounds.width);
wasTested[row] = subArray;
});
*/
// Start with a single point that we know we should test: (x, y).
// Convert (x,y) to image data coordinates by subtracting the bounds' origin.
currentX = x - bounds.x;
currentY = y - bounds.y;
currentIndex = currentY * boundsWidth + currentX;
shouldTest = [ currentIndex ];
ctx = canvas.getContext("2d");
//imageData = ctx.getImageData(bounds.x, bounds.y, bounds.width, bounds.height);
imageData = ImageProcessing.getImageData(ctx, bounds.x, bounds.y, bounds.width, bounds.height);
pixels = imageData.data;
seedColor = ImageProcessing.getColor(pixels, currentX, currentY, boundsWidth, boundsHeight);
currentColor = { r: 0, g: 0, b: 0, a: 1 };
fillRed = fillColor.r;
fillGreen = fillColor.g;
fillBlue = fillColor.b;
fillAlpha = fillColor.a;
while (shouldTest.length > 0) {
currentIndex = shouldTest.pop();
currentX = currentIndex % boundsWidth;
currentY = (currentIndex - currentX) / boundsWidth;
if (! wasTested[currentIndex]) {
wasTested[currentIndex] = true;
//currentColor = ImageProcessing.getColor(pixels, currentX, currentY, boundsWidth, boundsHeight, currentColor);
// Inline getColor for performance.
pixelStart = currentIndex * 4;
currentColor.r = pixels[pixelStart];
currentColor.g = pixels[pixelStart + 1]
currentColor.b = pixels[pixelStart + 2];
currentColor.a = pixels[pixelStart + 3];
if (! stopFunction(fillColor, seedColor, currentColor)) {
// Color the pixel with the fill color.
//ImageProcessing.setColor(pixels, currentX, currentY, boundsWidth, boundsHeight, fillColor);
// Inline setColor for performance
pixels[pixelStart] = fillRed;
pixels[pixelStart + 1] = fillGreen;
pixels[pixelStart + 2] = fillBlue;
pixels[pixelStart + 3] = fillAlpha;
if (minChangedX < currentX) { minChangedX = currentX; }
else if (maxChangedX > currentX) { maxChangedX = currentX; }
if (minChangedY < currentY) { minChangedY = currentY; }
else if (maxChangedY > currentY) { maxChangedY = currentY; }
// Add the adjacent four pixels to the list to be tested, unless they have already been tested.
tryX = currentX - 1;
tryY = currentY;
tryIndex = tryY * boundsWidth + tryX;
if (tryX >= 0 && ! wasTested[tryIndex]) {
shouldTest.push(tryIndex);
}
tryX = currentX;
tryY = currentY + 1;
tryIndex = tryY * boundsWidth + tryX;
if (tryY < boundsHeight && ! wasTested[tryIndex]) {
shouldTest.push(tryIndex);
}
tryX = currentX + 1;
tryY = currentY;
tryIndex = tryY * boundsWidth + tryX;
if (tryX < boundsWidth && ! wasTested[tryIndex]) {
shouldTest.push(tryIndex);
}
tryX = currentX;
tryY = currentY - 1;
tryIndex = tryY * boundsWidth + tryX;
if (tryY >= 0 && ! wasTested[tryIndex]) {
shouldTest.push(tryIndex);
}
}
}
}
//ctx.putImageData(imageData, bounds.x, bounds.y);
ImageProcessing.putImageData(ctx, imageData, bounds.x, bounds.y);
return { x: minChangedX + bounds.x, y: minChangedY + bounds.y, width: maxChangedX - minChangedX + 1, height: maxChangedY - minChangedY + 1 };
},
getImageData: function (ctx, x, y, w, h) {
return ctx.getImageData(x, y, w, h);
},
putImageData: function (ctx, data, x, y) {
ctx.putImageData(data, x, y);
}
};
stopFill : function (fillColor, seedColor, pixelColor) {
// Ignore alpha difference for now.
return Math.abs(pixelColor.r - seedColor.r) > this.colorTolerance || Math.abs(pixelColor.g - seedColor.g) > this.colorTolerance || Math.abs(pixelColor.b - seedColor.b) > this.colorTolerance;
},
最佳答案
要创建洪水填充,您需要能够查看已经存在的像素并检查它们是否不是您开始时使用的颜色,例如这样。
const ctx = document.querySelector("canvas").getContext("2d");
ctx.beginPath();
ctx.moveTo(20, 20);
ctx.lineTo(250, 70);
ctx.lineTo(270, 120);
ctx.lineTo(170, 140);
ctx.lineTo(190, 80);
ctx.lineTo(100, 60);
ctx.lineTo(50, 130);
ctx.lineTo(20, 20);
ctx.stroke();
floodFill(ctx, 40, 50, [255, 0, 0, 255]);
function getPixel(imageData, x, y) {
if (x < 0 || y < 0 || x >= imageData.width || y >= imageData.height) {
return [-1, -1, -1, -1]; // impossible color
} else {
const offset = (y * imageData.width + x) * 4;
return imageData.data.slice(offset, offset + 4);
}
}
function setPixel(imageData, x, y, color) {
const offset = (y * imageData.width + x) * 4;
imageData.data[offset + 0] = color[0];
imageData.data[offset + 1] = color[1];
imageData.data[offset + 2] = color[2];
imageData.data[offset + 3] = color[0];
}
function colorsMatch(a, b) {
return a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3];
}
function floodFill(ctx, x, y, fillColor) {
// read the pixels in the canvas
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
// get the color we're filling
const targetColor = getPixel(imageData, x, y);
// check we are actually filling a different color
if (!colorsMatch(targetColor, fillColor)) {
fillPixel(imageData, x, y, targetColor, fillColor);
// put the data back
ctx.putImageData(imageData, 0, 0);
}
}
function fillPixel(imageData, x, y, targetColor, fillColor) {
const currentColor = getPixel(imageData, x, y);
if (colorsMatch(currentColor, targetColor)) {
setPixel(imageData, x, y, fillColor);
fillPixel(imageData, x + 1, y, targetColor, fillColor);
fillPixel(imageData, x - 1, y, targetColor, fillColor);
fillPixel(imageData, x, y + 1, targetColor, fillColor);
fillPixel(imageData, x, y - 1, targetColor, fillColor);
}
} <canvas></canvas>fillPixel我们可以保留一系列我们想要查看的位置。我们将 4 个位置添加到该数组中,然后从数组中弹出内容直到它为空const ctx = document.querySelector("canvas").getContext("2d");
ctx.beginPath();
ctx.moveTo(20, 20);
ctx.lineTo(250, 70);
ctx.lineTo(270, 120);
ctx.lineTo(170, 140);
ctx.lineTo(190, 80);
ctx.lineTo(100, 60);
ctx.lineTo(50, 130);
ctx.lineTo(20, 20);
ctx.stroke();
floodFill(ctx, 40, 50, [255, 0, 0, 255]);
function getPixel(imageData, x, y) {
if (x < 0 || y < 0 || x >= imageData.width || y >= imageData.height) {
return [-1, -1, -1, -1]; // impossible color
} else {
const offset = (y * imageData.width + x) * 4;
return imageData.data.slice(offset, offset + 4);
}
}
function setPixel(imageData, x, y, color) {
const offset = (y * imageData.width + x) * 4;
imageData.data[offset + 0] = color[0];
imageData.data[offset + 1] = color[1];
imageData.data[offset + 2] = color[2];
imageData.data[offset + 3] = color[0];
}
function colorsMatch(a, b) {
return a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3];
}
function floodFill(ctx, x, y, fillColor) {
// read the pixels in the canvas
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
// get the color we're filling
const targetColor = getPixel(imageData, x, y);
// check we are actually filling a different color
if (!colorsMatch(targetColor, fillColor)) {
const pixelsToCheck = [x, y];
while (pixelsToCheck.length > 0) {
const y = pixelsToCheck.pop();
const x = pixelsToCheck.pop();
const currentColor = getPixel(imageData, x, y);
if (colorsMatch(currentColor, targetColor)) {
setPixel(imageData, x, y, fillColor);
pixelsToCheck.push(x + 1, y);
pixelsToCheck.push(x - 1, y);
pixelsToCheck.push(x, y + 1);
pixelsToCheck.push(x, y - 1);
}
}
// put the data back
ctx.putImageData(imageData, 0, 0);
}
} <canvas></canvas>colorsMatch检查是否足够接近,但是如果targetColor,您会遇到一个新问题和 fillColor也足够接近它会继续尝试填满自己。您可以通过创建另一个数组来解决这个问题,每个像素一个字节或一个位来跟踪您准备检查的位置。const ctx = document.querySelector("canvas").getContext("2d");
ctx.beginPath();
ctx.moveTo(20, 20);
ctx.lineTo(250, 70);
ctx.lineTo(270, 120);
ctx.lineTo(170, 140);
ctx.lineTo(190, 80);
ctx.lineTo(100, 60);
ctx.lineTo(50, 130);
ctx.lineTo(20, 20);
ctx.stroke();
floodFill(ctx, 40, 50, [255, 0, 0, 255], 128);
function getPixel(imageData, x, y) {
if (x < 0 || y < 0 || x >= imageData.width || y >= imageData.height) {
return [-1, -1, -1, -1]; // impossible color
} else {
const offset = (y * imageData.width + x) * 4;
return imageData.data.slice(offset, offset + 4);
}
}
function setPixel(imageData, x, y, color) {
const offset = (y * imageData.width + x) * 4;
imageData.data[offset + 0] = color[0];
imageData.data[offset + 1] = color[1];
imageData.data[offset + 2] = color[2];
imageData.data[offset + 3] = color[0];
}
function colorsMatch(a, b, rangeSq) {
const dr = a[0] - b[0];
const dg = a[1] - b[1];
const db = a[2] - b[2];
const da = a[3] - b[3];
return dr * dr + dg * dg + db * db + da * da < rangeSq;
}
function floodFill(ctx, x, y, fillColor, range = 1) {
// read the pixels in the canvas
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
// flags for if we visited a pixel already
const visited = new Uint8Array(imageData.width, imageData.height);
// get the color we're filling
const targetColor = getPixel(imageData, x, y);
// check we are actually filling a different color
if (!colorsMatch(targetColor, fillColor)) {
const rangeSq = range * range;
const pixelsToCheck = [x, y];
while (pixelsToCheck.length > 0) {
const y = pixelsToCheck.pop();
const x = pixelsToCheck.pop();
const currentColor = getPixel(imageData, x, y);
if (!visited[y * imageData.width + x] &&
colorsMatch(currentColor, targetColor, rangeSq)) {
setPixel(imageData, x, y, fillColor);
visited[y * imageData.width + x] = 1; // mark we were here already
pixelsToCheck.push(x + 1, y);
pixelsToCheck.push(x - 1, y);
pixelsToCheck.push(x, y + 1);
pixelsToCheck.push(x, y - 1);
}
}
// put the data back
ctx.putImageData(imageData, 0, 0);
}
} <canvas></canvas>colorsMatch有点天真。转换为 HSV 或其他东西可能会更好,或者您可能想按 alpha 加权。我不知道什么是匹配颜色的好指标。Uint32Array查看像素。这样查找一个像素并设置一个像素,只有一个 32 位值可供读取或写入。 Just that change makes it about 4x faster .尽管如此,填充 4096x4096 Canvas 仍然需要 4 秒。可能还有其他优化,比如不是调用 getPixels使其内联,但不要在我们的像素列表中推送新像素以检查它们是否超出范围。它可能会提高 10% 的速度(不知道),但不会使其足够快以达到交互速度。ticksPerUpdate加快或减慢填充率const ctx = document.querySelector("canvas").getContext("2d");
ctx.beginPath();
ctx.moveTo(20, 20);
ctx.lineTo(250, 70);
ctx.lineTo(270, 120);
ctx.lineTo(170, 140);
ctx.lineTo(100, 145);
ctx.lineTo(110, 105);
ctx.lineTo(130, 125);
ctx.lineTo(190, 80);
ctx.lineTo(100, 60);
ctx.lineTo(50, 130);
ctx.lineTo(20, 20);
ctx.stroke();
floodFill(ctx, 40, 50, 0xFF0000FF);
function getPixel(pixelData, x, y) {
if (x < 0 || y < 0 || x >= pixelData.width || y >= pixelData.height) {
return -1; // impossible color
} else {
return pixelData.data[y * pixelData.width + x];
}
}
async function floodFill(ctx, x, y, fillColor) {
// read the pixels in the canvas
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
// make a Uint32Array view on the pixels so we can manipulate pixels
// one 32bit value at a time instead of as 4 bytes per pixel
const pixelData = {
width: imageData.width,
height: imageData.height,
data: new Uint32Array(imageData.data.buffer),
};
// get the color we're filling
const targetColor = getPixel(pixelData, x, y);
// check we are actually filling a different color
if (targetColor !== fillColor) {
const ticksPerUpdate = 50;
let tickCount = 0;
const pixelsToCheck = [x, y];
while (pixelsToCheck.length > 0) {
const y = pixelsToCheck.pop();
const x = pixelsToCheck.pop();
const currentColor = getPixel(pixelData, x, y);
if (currentColor === targetColor) {
pixelData.data[y * pixelData.width + x] = fillColor;
// put the data back
ctx.putImageData(imageData, 0, 0);
++tickCount;
if (tickCount % ticksPerUpdate === 0) {
await wait();
}
pixelsToCheck.push(x + 1, y);
pixelsToCheck.push(x - 1, y);
pixelsToCheck.push(x, y + 1);
pixelsToCheck.push(x, y - 1);
}
}
}
}
function wait(delay = 0) {
return new Promise((resolve) => {
setTimeout(resolve, delay);
});
} <canvas></canvas>
关于javascript - 如何使用 HTML Canvas 执行洪水填充?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/2106995/
我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div
总的来说,我对ruby还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用
我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看rubyzip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d
类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc
很好奇,就使用rubyonrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提
假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于
我正在尝试使用ruby和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我
关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。
给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru
我在使用omniauth/openid时遇到了一些麻烦。在尝试进行身份验证时,我在日志中发现了这一点:OpenID::FetchingError:Errorfetchinghttps://www.google.com/accounts/o8/.well-known/host-meta?hd=profiles.google.com%2Fmy_username:undefinedmethod`io'fornil:NilClass重要的是undefinedmethodio'fornil:NilClass来自openid/fetchers.rb,在下面的代码片段中:moduleNetclass