Recently, I have been trying to create code to fill a polygon of any shape with color. I have gotten as far as being able to fill a shape that has lines of only one border size correctly, though I have found myself unable to do anything more than that. The problem is that the code does not know when to consider a line of pixels greater than that which it expects as a vertical or horizontal border of the shape. I am going through each pixel of the shape from left to right and checking if any of the pixels have any form of color by checking if the alpha value is 0 or not. Once it finds a pixel that does have an alpha value of anything other than 0, it moves forward a single pixel and then uses the even/odd technique to determine whether the point is inside part of the polygon or not (it makes an infinite line to the right and determines if the number of collisions with colored lines is odd, and if it is, the point is inside the polygon). In general, we consider a single, lone pixel to count as a single line, and we consider a horizontal line of more than one pixel to be two lines because of how often horizontal lines will be part of a border or not. Take the following scenario:
Here, the red dot is the point (pixel) we begin testing from. If we did not consider that horizontal line in the middle to be two points (as is shown by the red lines and x's), we would only have two points of intersection and therefore would not fill the pixel despite the fact that we most definitely do want to fill that pixel. As stated earlier, however, this brings up another problem with a different scenario:
In this case, if we do count a horizontal line of more than one pixel to be two separate lines, we end up not filling any areas with borders that are thicker than the expected thickness. For your reference, the function to handle this is as follows:
//imgData is essentially a WebImage object (explained more below) and r, g, and b are the color values for the fill color
function fillWithColor(imgData, r, g, b) {
//Boolean determining whether we should color the given pixel(s) or not
var doColor = false;
//Booleans determining whether the last pixel found in the entire image was colored
var blackLast = false;
//Booleans determining whether the last 1 or 2 pixels found after a given pixel were colored
var foundBlackPrev, foundBlackPrev2 = false;
//The number of colored pixels found
var blackCount = 0;
//Loop through the entire canvas
for(var y = 0; y < imgData.height; y += IMG_SCALE) {
for(var x = 0; x < imgData.width; x += IMG_SCALE) {
//Test if given pixel is colored
if(getAlpha(imgData, x, y) != 0) {
//If the last pixel was black, begin coloring
if(!blackLast) {
blackLast = true;
doColor = true;
}
} else {
//If the current pixel is not colored, but the last one was, find all colored lines to the right
if(blackLast){
for(var i = x; i < imgData.width; i += IMG_SCALE) {
//If the pixel is colored...
if(getAlpha(imgData, i, y) != 0) {
//If no colored pixel was found before, add to the count
if(!foundBlackPrev){
blackCount++;
foundBlackPrev = true;
} else {
//Otherwise, at least 2 colored pixels have been found in a row
foundBlackPrev2 = true;
}
} else {
//If two or more colored pixels were found in a row, add to the count
if(foundBlackPrev2) {
blackCount++;
}
//Reset the booleans
foundBlackPrev2 = foundBlackPrev = false;
}
}
}
//If the count is odd, we start coloring
if(blackCount & 1) {
blackCount = 0;
doColor = true;
} else {
//If the last pixel in the entire image was black, we stop coloring
if(blackLast) {
doColor = false;
}
}
//Reset the boolean
blackLast = false;
//If we are to be coloring the pixel, color it
if(doColor) {
//Color the pixel
for(var j = 0; j < IMG_SCALE; j++) {
for(var k = 0; k < IMG_SCALE; k++) {
//This is the same as calling setRed, setGreen, setBlue and setAlpha functions from the WebImage API all at once (parameters in order are WebImage object equivalent, x position of pixel, y position of pixel, red value, green value, blue value, and alpha value)
setRGB(imgData, x + j, y + k, r, g, b, 255);
}
}
}
}
}
}
//Update the image (essentially the same as removing all elements from the given area and calling add on the image)
clearCanvas();
putImageData(imgData, 0, 0, imgData.width, imgData.height);
//Return the modified data
return imgData;
}
Where...
imgData is the collection of all of the pixels in the given area (essentially a WebImage object)
IMG_SCALE is the integer value by which the image has been scaled up (which gives us the scale of the pixels as well). In this example, it is equal to 4 because the image is scaled up to 192x256 (from 48x64). This means that every "pixel" you see in the image is actually comprised of a 4x4 block of identically-colored pixels.
So, what I'm really looking for here is a way to determine whether a given colored pixel that comes after another is part of a horizontal border or if it is just another piece comprising the thickness of a vertical border. In addition, if I have the wrong approach to this problem in general, I would greatly appreciate any suggestions as to how to do this more efficiently. Thank you.
I understand the problem and I think you would do better if you would switch your strategy here. We know the following:
the point of start is inside the shape
the color should be filled for every pixel inside the shape
So, we could always push the neighbors of the current point into a queue to be processed and be careful to avoid processing the same points twice, this way traversing all the useful pixels and including them into the coloring plan. The function below is untested.
function fillColor(pattern, startingPoint, color, boundaryColor) {
let visitQueue = [];
let output = {};
if (startingPoint.x - 1 >= 0) visitQueue.push({startingPoint.x - 1, startingPoint.y});
if (startingPoint.x + 1 < pattern.width) visitQueue.push({startingPoint.x + 1, startingPoint.y});
if (startingPoint.y + 1 < pattern.height) visitQueue.push({startingPoint.x, startingPoint.y + 1});
if (startingPoint.y - 1 >= 0) visitQueue.push({startingPoint.x, startingPoint.y - 1});
let visited = {};
while (visitQueue.length > 0) {
let point = visitQueue[0];
visitQueue.shift();
if ((!visited[point.x]) || (visited[point.x].indexOf(point.y) < 0)) {
if (!visited[point.x]) visited[point.x] = [];
visited[point.x].push(point.y);
if (isBlank(pattern, point)) { //you need to implement isBlank
if (!output[point.x]) output[point.x] = [];
output[point.x].push(point.y);
if (point.x + 1 < pattern.width) visitQueue.push({point.x + 1, point.y});
if (point.x - 1 >= 0) visitQueue.push({point.x - 1, point.y});
if (point.y + 1 < pattern.height) visitQueue.push({point.x, point.y + 1});
if (point.y - 1 >= 0) visitQueue.push({point.x, point.y - 1})
}
}
}
return output;
}
As far as I understood you cannot "consider a horizontal line of more than one pixel to be two lines". I don't think you need to count black pixels the way you do, rather count groups of 1 or more pixels.
I would also tidy the code by avoiding using the "doColor" boolean variable. You could rather move the coloring code to a new function color(x,y) and call it straight away.
const ctx = document.querySelector("canvas").getContext("2d");
//ctx.lineWidth(10);//-as you asked we are setting greater border or line width,BUT "LINEWIDTH" IS NOT WORKING IN INBUILT STACKOVERFLOW SNIPPET USE IT IN A FILE I THINK STACKOVERFLOW IS NOT UP-TO-DATE,IN ANY IDE UNCOMENT THIS
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();
function getMousePosition(canvas, event) {
let rect = canvas.getBoundingClientRect();
let mx = event.clientX - rect.left;
let my = event.clientY - rect.top;
console.log("Coordinate x: " + mx, "Coordinate y: " + my);
floodFill(ctx, mx, my, [155, 0, 255, 255], 128);
}
let canvasElem = document.querySelector("canvas");
canvasElem.addEventListener("mousedown", function(e) {
getMousePosition(canvasElem, e);
});
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>
This is based on other answers
note:"LINEWIDTH" IS NOT WORKING IN INBUILT STACKOVERFLOW SNIPPET USE IT IN A FILE I THINK STACKOVERFLOW IS NOT UP-TO-DATE,
But it works well in simple HTML,JS website
I am trying to get the rotation degree from div's I rotate to create a line pattern.
Only now I am running into a problem. I need the rotation(deg) from the div's to calculate where the next line needs to appear. But when I try to get the value from a div with style.transform and convert the matrix values I still get the wrong degrees.
In my testing case I have a div that is rotated 150deg, but I get 30 deg back and this will not work for me unfortunatly. Please help, how do I get the 150deg value back?
Here is the code:
HTML:
<div class="linesBox" id="linesBox">
<div class="line1 lines" id="line1" style="float: left;
margin: 200px 0 0 100px;
position: fixed;
border-top:0.5px solid black;
width: 100px;
transform: rotate(150deg);"></div>
<!-- <div class="line2 lines" id="line1" style="float: left;
margin: 174px 0 0 193.3px;
position: fixed;
border-top:0.5px solid black;
width:100px;
transform: rotate(0deg);"></div> -->
</div>
JavaScript:
const linesBox = document.getElementById('linesBox');
let lastLineWidth;
let angle;
let lineWidthCount;
let lineHeightCount;
function createLines(){
//Last line div in a var and a var to get the computated value
let lastLine = linesBox.lastElementChild;
var st = window.getComputedStyle(lastLine, null);
//Get the width and angle of the last line and place them in a var
lastLineWidth = parseFloat(lastLine.style.width);
lastLineXPos = parseFloat(lastLine.style.marginLeft);
lastLineYPos = parseFloat(lastLine.style.marginTop);
let lastLineAngle = st.getPropertyValue("transform");
console.log(lastLineWidth, lastLineXPos, lastLineYPos);
//Get and map the Matrix value from transform rotate() and set it to lastLineAngle
var values = lastLineAngle.split('(')[1],
values = values.split(')')[0],
values = values.split(',');
//Set each value of the matrix values to a var
let a = values[0];
let b = values[1];
let c = values[2];
let d = values[3];
//Take the correc value from the matrix values and place it in the formula and save the outcome in a var
angle = Math.round(Math.asin(b) * (180/Math.PI));
console.log(angle);
//Calculate margin left starting position for the next line
//Get sin en cos values by angle
let yChangeOne = lastLineWidth * Math.sin(angle / 180 * Math.PI) / 2;
let xChangeOne = parseFloat(lastLineWidth - lastLineWidth * Math.cos(angle / 180 * Math.PI)) / 2;
// let yChangeOne = lastLineWidth * sinOutcome / 2;
// let xChangeOne = lastLineWidth - lastLineWidth * cosOutcome;
console.log(yChangeOne, xChangeOne);
let newYPos;
let newXPos;
if( angle <= 89 && angle >= 1 ){
newYPos = lastLineYPos + yChangeOne;
} else if ( angle >= 91 && angle <= 179 ){
newYPos = lastLineYPos - yChangeOne;
}
console.log(newYPos);}
//Get the start position for the next line
createLines();
The angle should return 150deg not 30deg otherwise my if statement will not work. Please help :)
Both 30° and 150° have the same sine. You also need to take the cosine into account. Instead of Math.asin(b), use
Math.atan2(b, a)
Btw, if you are just calculating the angle to calculate its sine and cosine again, then spare this step (Math.sin(angle...)). You have sine and cosine right there, so just use them.
How can i create something like this in QML using javascript?
Actually I know how to create rectangles in QML but want to do something like this. QML canvas can be of any size but whenever QML section is loaded multiple squares are generated with random sizes and colors without overlapping. When I'm trying to do this rectangles are generated in a list form.
I'm a web developer(ruby on rails oriented) but new to such javascript stuff. Any help will be appreciated.
As #ddriver already noticed, the simpliest decision is to loop through all children to find a room to a new rectangle.
Rectangle {
id: container
anchors.fill: parent
property var items: [];
Component {
id: rect
Rectangle {
color: Qt.rgba(Math.random(),Math.random(),Math.random(),1);
border.width: 1
border.color: "#999"
width: 50
height: 50
}
}
Component.onCompleted: {
var cnt = 50;
for(var i = 0;i < cnt;i ++) {
for(var t = 0;t < 10;t ++) {
var _x = Math.round(Math.random() * (mainWindow.width - 200));
var _y = Math.round(Math.random() * (mainWindow.height - 200));
var _width = Math.round(50 + Math.random() * 150);
var _height = Math.round(50 + Math.random() * 150);
if(checkCoord(_x,_y,_width,_height)) {
var item = rect.createObject(container,{ x: _x, y: _y, width: _width, height: _height });
container.items.push(item);
break;
}
}
}
}
function checkCoord(_x,_y,_width,_height) {
if(container.items.length === 0)
return true;
for(var j = 0;j < container.items.length;j ++) {
var item = container.children[j];
if(!(_x > (item.x+item.width) || (_x+_width) < item.x || _y > (item.y+item.height) || (_y+_height) < item.y))
return false;
}
return true;
}
}
Yes, this is not so wise solution but it still can be improved.
If you want efficiency, it will come at the cost of complexity - you will have to use some space partition algorithm. Otherwise, you could just generate random values until you get enough that are not overlapping.
Checking whether two rectangles overlap is simple - if none of the corners of rectangle B is inside rectangle A, then they don't overlap. A corner/point is inside a rectangle if its x and y values are in the range of the rectangle's x and width and y and height respectively.
In JS Math.random() will give you a number between 0 and 1, so if you want to make a random value for example between 50 and 200, you can do that via Math.random() * 150 + 50.
Have an array, add the initial rectangle value to it, then generate new rectangle values, check if they overlap with those already in the array, if not - add them to the array as well. Once you get enough rectangle values, go ahead and create the actual rectangles. Since all your rectangles are squares, you can only go away with 3 values per square - x, y and size.
I'm playing around with a progress ring, and I can't seem to get it to run on a timer. I am trying to make the progress ring propagate automatically and take, say, 0.5 seconds to go from 0% to whatever percent I set (65% in this case).
I used this progress ring as a base: http://llinares.github.io/ring-progress-bar/
This is my fiddle: http://jsfiddle.net/gTtGW/
I tried using a timer function, but I may not have been integrating that properly. In the fiddle, I have added:
for (var i = 0; i< 65; i++){
range += i;
setTimeout(timer,800);
}
However, this breaks the progress ring. I thought that any time the range is updated (with the += i), the draw function would be called. What am I doing wrong? Thank you so much in advance.
If you're not planning to use the input[type=range] element, you can change your code to this:
(function (window) {
'use strict';
var document = window.document,
ring = document.getElementsByTagName('path')[0],
range = 0,
text = document.getElementsByTagName('text')[0],
Math = window.Math,
toRadians = Math.PI / 180,
r = 100;
function draw() {
// Update the wheel giving to it a value in degrees, getted from the percentage of the input value a.k.a. (value * 360) / 100
var degrees = range * 3.5999,
// Convert the degrees value to radians
rad = degrees * toRadians,
// Determine X and cut to 2 decimals
x = (Math.sin(rad) * r).toFixed(2),
// Determine Y and cut to 2 decimals
y = -(Math.cos(rad) * r).toFixed(2),
// The another half ring. Same as (deg > 180) ? 1 : 0
lenghty = window.Number(degrees > 180),
// Moveto + Arcto
descriptions = ['M', 0, 0, 'v', -r, 'A', r, r, 1, lenghty, 1, x, y, 'z'];
// Apply changes to the path
ring.setAttribute('d', descriptions.join(' '));
// Update the numeric display
text.textContent = range;
range++;
if(range > 100) {
clearInterval(timer);
}
}
// Translate the center axis to a half of total size
ring.setAttribute('transform', 'translate(' + r + ', ' + r + ')');
var timer = setInterval(draw,100);
}(this));
Basically changing range to a simple variable starting at 0, and increasing its value every time draw() is called. Creating an interval (named timer) to run every 0.1 seconds in this case (of course it's up to you), and clearing that interval from draw() when appropriate...
JSFiddle Demo
I think you want something like:
function inc() {
var val = parseInt(range.value, 10)
range.value = val + 1;
draw(); // updating the value doesn't cause `onchange`.
if (val < 100) { // don't go over 100
setTimeout(inc, 100);
}
}
inc();
This is baffling me slightly at the moment.
I'm a fairly experienced programmer who is looking to throw a bit of a surprise for my fiancee on the countdown page for our wedding. The desired effect essentially simulated confetti falling behind the main page. My setup is as follows:
I have an array of divs in javascript. Each div is absolutely positioned on the page. I have an interval set that updates every 50 or so milliseconds. On each tick, I run a for loop over my array of divs, and do calculations and update their positions.
Problem
The problem I am having, however, is that even though I can clearly see that all divs are created, stored into the array, and modified (in this case, each has a slight randomized rotation) during the initialization phase, once the tick to update their position starts, only the div stored in whatever the last slot of the array is gets its position updated. I even tried hard-coding each div by index for testing purposes, but it is strictly the last element that will actually update its position. Even upon printing the current "left" and "top" style values for each div before and after the calculations shows that the value has been changed, but no visible change is noticeable. Here's my basic code:
Javascript
var container; // the div that will have all the other divs attached to it
var conf = new Array(); // the array of divs to modify
var rots = new Array(); // the rotation of each div
var vels = new Array(); // the velocity of each div
var xPos = new Array(); // the current x value of position in pixels
var yPos = new Array(); // the current y value of position in pixels
var xVels = new Array(); // the x portion of that velocity
var yVels = new Array(); // the y portion of that velocity
function initialize() // this is attached to the onload event on my <body> tag
{
container = document.getElementById("conf_container");
confettiInit(); // creates, initializes, and displays each div
setInterval(updateConfetti, 42);
}
function confettiInit()
{
screenHeight = window.innerHeight;
screenWidth = window.innerWidth;
for (var i = 0; i < 25; i ++) // create 25 confetti flakes
{
// append a div in the container with a unique ID
container.innerHTML += "<div class='conf' id='conf_" + i + "'></div>";
// store the element in an array
conf[i] = document.getElementById("conf_" + i);
// ensure confetti is in the background, and each z index is unique
conf[i].style.zIndex = -i - 1;
xPos[i] = window.innerWidth * Math.random(); // calculate random x position
conf[i].style.left = xPos[i] + "px"; // set x position of div
yPos[i] = -40; // y position above screen
conf[i].style.top = yPos[i] + "px"; // set y position of div
// calculate the random amount of rotation (-30 to 30 degrees)
rots[i] = Math.random() * 60*(Math.PI/180) - 30*(Math.PI/180);
// set rotation of div
conf[i].style.webkitTransform = "rotate(" + -rots[i] + "rad)";
vels[i] = Math.random() * 3 + 2; // calculate random velocity (2-5)
xVels[i] = vels[i] * Math.sin(rots[i]); // get x portion of velocity
yVels[i] = vels[i] * Math.cos(rots[i]); // get y portion of velocity
}
}
function updateConfetti()
{
for (var i = 0; i < 25; i ++)
{
xPos[i] += xVels[i]; // calculate new x position
yPos[i] += yVels[i]; // calculate new y position
conf[i].style.left = xPos[i] + "px"; // set div's x position
conf[i].style.top = yPos[i] + "px"; // set div's y position
// if the confetti piece leaves the viewing area...
if (xPos[i] < -50 ||
xPos[i] > window.screenWidth + 10 ||
yPos[i] > window.screenHeight + 10)
{
// send it back to the top and reinitialize it
xPos[i] = window.innerWidth * Math.random();
conf[i].style.left = xPos[i] + "px";
yPos[i] = -40;
conf[i].style.top = yPos[i] + "px";
rots[i] = Math.random() * 60*(Math.PI/180) - 30*(Math.PI/180);
conf[i].style.webkitTransform = "rotate(" + -rots[i] + "rad)";
vels[i] = Math.random() * 3 + 2;
xVels[i] = vels[i] * Math.sin(rots[i]);
yVels[i] = vels[i] * Math.cos(rots[i]);
}
}
}
CSS
div.conf
{
height: 29px;
width: 50px;
background-image: url("images/ConfettiYellow.gif");
position: absolute;
}
#conf_container
{
left: 0px;
top: 0px;
width: 100%;
height: 100%;
position: absolute;
overflow: hidden;
}
It's something wrong with your divs creation. It start works if you do it in conventional way, like this:
var d = document.createElement("div");
d.className = "conf";
container.appendChild(d);
conf[i] = d;