I need a function that accept 5 arguments (ctx, startX, startY, endX, endY). It should return pixels on a canvas that lay on the line, that starts on (startX, startY) and ends on (endX, endY). How can I implement it?
You can use Brensenham line algorithm. It will get each pixel without needing to check if you already have that pixel which many other line methods would need.
function getPixelsOnLine(ctx, startX, startY, endX, endY){
const imageData = ctx.getImageData(0,0,ctx.canvas.width,ctx.canvas.height);
const data = imageData.data;
const pixelCols = [];
const getPixel = (x,y) => {
if(x < 0 || x >= imageData.width || y < 0 || y >= imageData.height ){
return "rgba(0,0,0,0)";
}
const ind = (x + y * imageData.width) * 4;
return `rgba(${data[ind++]},${data[ind++]},${data[ind++]},${data[ind++]/255})`;
}
var x = Math.floor(startX);
var y = Math.floor(startY);
const xx = Math.floor(endX);
const yy = Math.floor(endY);
const dx = Math.abs(xx - x);
const sx = x < xx ? 1 : -1;
const dy = -Math.abs(yy - y);
const sy = y < yy ? 1 : -1;
var err = dx + dy;
var e2;
var end = false;
while (!end) {
pixelCols.push(getpixel(x,y));
if ((x === xx && y === yy)) {
end = true;
} else {
e2 = 2 * err;
if (e2 >= dy) {
err += dy;
x += sx;
}
if (e2 <= dx) {
err += dx;
y += sy;
}
}
}
return pixelCols;
}
Function returns array of pixel as CSS color values rgba(red,green,blue,alpha) on line from start to end.
Related
Here is the link to the codepen: https://codepen.io/Jsbbvk/pen/RwGBwOO
const edgePadding = 80;
const panSpeed = 5;
const expandCanvasEdge = (x, y) => {
let pan = {
x: 0,
y: 0,
};
const width = canvas.getWidth(),
height = canvas.getHeight();
if (x <= edgePadding) {
//left
const speedRatio = 1 - Math.max(0, x) / edgePadding;
pan.x = panSpeed * speedRatio;
} else if (x >= width - edgePadding) {
//right
const speedRatio =
1 - (width - Math.min(width, x)) / edgePadding;
pan.x = -panSpeed * speedRatio;
}
if (y <= edgePadding) {
//top
const speedRatio = 1 - Math.max(0, y) / edgePadding;
pan.y = panSpeed * speedRatio;
} else if (y >= height - edgePadding) {
//bottom
const speedRatio =
1 - (height - Math.min(height, y)) / edgePadding;
pan.y = -panSpeed * speedRatio;
}
if (pan.x || pan.y) {
canvas.relativePan(new fabric.Point(pan.x, pan.y));
}
}
canvas.on('mouse:move', function(opt) {
if (this.isMouseDown && this.isDrawingMode) {
let {x, y} = canvas.getPointer(opt.e, true);
expandCanvasEdge(x, y);
}
if (!this.isDrawingMode && this.isDragging) {
//panning
var e = opt.e;
var vpt = this.viewportTransform;
vpt[4] += e.clientX - this.lastPosX;
vpt[5] += e.clientY - this.lastPosY;
this.requestRenderAll();
this.lastPosX = e.clientX;
this.lastPosY = e.clientY;
}
});
In the demo, when you draw close to the edge of the canvas, the canvas should pan to allow more drawing space.
However, while the panning is happening, the drawing (path) is static on the canvas; it doesn't stretch as the canvas pans.
Is there a way to fix this issue?
I did some deep research for you and found a few examples.
You can overcome this situation by using the relativePan function.
One of the examples I have found:
function startPan(event) {
if (event.button != 2) {
return;
}
var x0 = event.screenX,
y0 = event.screenY;
function continuePan(event) {
var x = event.screenX,
y = event.screenY;
fc.relativePan({ x: x - x0, y: y - y0 });
x0 = x;
y0 = y;
}
function stopPan(event) {
$(window).off('mousemove', continuePan);
$(window).off('mouseup', stopPan);
};
$(window).mousemove(continuePan);
$(window).mouseup(stopPan);
$(window).contextmenu(cancelMenu);
};
function cancelMenu() {
$(window).off('contextmenu', cancelMenu);
return false;
}
$(canvasWrapper).mousedown(startPan);
You can determine a roadmap by examining the resources and demos here.
JSFiddle demo https://jsfiddle.net/tornado1979/up48rxLs/
I'm trying to get the points of the smallest polygons that the lines create around the starting point of the chart, that is, the innermost polygon. The lines vary depending on some parameters that can be changed.
Example of such a chart:
As is visible from the chart, the lines intersect many times and, thus, create multiple polygons. However, I am interested in getting only the smallest polygon that is within the starting point (the center) of the chart.
I was thinking about drawing multiple parallels with x-axis from the y-axis to left and right from the top and the bottom (the smallest intercept on +y and -y axis) and seeing which line gets "hit" first as to get the lines which would enclose this polygon, then getting their intersections for vertices which can be used to drawing the polygon. However, since it will take numerous points to check such lines with precision, I am wondering if there is perhaps a more elegant solution to the problem?
I managed to solve this with the help of Stef and his algorithm. Here is the code I used:
const getIntersections = async (
lines: IPoint[][]
): Promise<IIntersectLine[]> => {
let lineIntersects: IIntersectLine[] = [];
lines.forEach((line) => {
let lineIntersect: IIntersectLine = {
line: line,
intersects: [],
};
let x1 = line[1].x;
let y1 = line[1].y;
let x2 = line[2].x;
let y2 = line[2].y;
for (let i = 0; i < lines.length; i++) {
let x3 = lines[i][1].x;
let y3 = lines[i][1].y;
let x4 = lines[i][2].x;
let y4 = lines[i][2].y;
if ((x1 === x2 && y1 === y2) || (x3 === x4 && y3 === y4)) continue;
let denominator = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
if (denominator === 0) continue;
let ua =
((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denominator;
let ub =
((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denominator;
if (ua < 0 || ua > 1 || ub < 0 || ub > 1) continue;
let x = x1 + ua * (x2 - x1);
let y = y1 + ua * (y2 - y1);
lineIntersect.intersects.push({
x: +x.toFixed(4),
y: +y.toFixed(4),
});
}
lineIntersect.intersects.sort((a, b) => {
return a.x - b.x;
});
lineIntersects.push(lineIntersect);
});
return lineIntersects;
};
const getStartingPoint = async (intersects: IPoint[]) => {
let result: IPoint = intersects[0];
let distance = result.x * result.x + result.y * result.y;
intersects.forEach((i) => {
let newDistance = i.x * i.x + i.y * i.y;
if (newDistance < distance) {
distance = newDistance;
result = i;
}
});
return result;
};
const calcPolygonArea = async (polygon: IPoint[]) => {
let total = 0;
for (let i = 0, l = polygon.length; i < l; i++) {
let addX = polygon[i].x;
let addY = polygon[i == polygon.length - 1 ? 0 : i + 1].y;
let subX = polygon[i == polygon.length - 1 ? 0 : i + 1].x;
let subY = polygon[i].y;
total += addX * addY * 0.5;
total -= subX * subY * 0.5;
}
return Math.abs(total);
};
export const getPolygonVertices = async (lines: IPoint[][]) => {
let result: IPoint[] = [];
let intersections = await getIntersections(lines);
let intersectionVertices = intersections.map((x) => x.intersects).flat();
let startingPoint = await getStartingPoint(intersectionVertices);
let crossedLines = intersections.filter((x) =>
x.intersects.some(
(p) => p.x === startingPoint.x && p.y === startingPoint.y
)
);
let newPoints: IPoint[] = [];
const x0 = 0;
const y0 = 0;
crossedLines.forEach((line) => {
let x1 = startingPoint.x;
let y1 = startingPoint.y;
let pointIndex = line.intersects.findIndex(
(p) => p.x === startingPoint.x && p.y === startingPoint.y
);
let d;
if (line.intersects[pointIndex - 1]) {
let x2 = line.intersects[pointIndex - 1].x;
let y2 = line.intersects[pointIndex - 1].y;
d = (x0 - x1) * (y2 - y1) - (y0 - y1) * (x2 - x1);
if (d > 0) newPoints.push({ x: x2, y: y2 });
}
if (line.intersects[pointIndex + 1]) {
let x2 = line.intersects[pointIndex + 1].x;
let y2 = line.intersects[pointIndex + 1].y;
d = (x0 - x1) * (y2 - y1) - (y0 - y1) * (x2 - x1);
if (d > 0) newPoints.push({ x: x2, y: y2 });
}
});
let result1: IPoint[] = [];
let result2: IPoint[] = [];
for (let i = 0; i < newPoints.length; i++) {
let tempResult: IPoint[] = [];
tempResult.push(startingPoint, newPoints[i]);
for (let j = 0; j < 50; j++) {
const uniqueValues = new Set(tempResult.map((v) => v.x));
if (uniqueValues.size < tempResult.length) {
if (i === 0) result1 = tempResult;
else result2 = tempResult;
break;
}
let newCrossedLines = intersections.filter((x) =>
x.intersects.some(
(p) =>
p.x === tempResult[tempResult.length - 1].x &&
p.y === tempResult[tempResult.length - 1].y
)
);
let newLine = newCrossedLines.filter((l) =>
l.intersects.every(
(p) =>
p.x !== tempResult[tempResult.length - 2].x &&
p.y !== tempResult[tempResult.length - 2].y
)
)[0];
let x1 = tempResult[tempResult.length - 1].x;
let y1 = tempResult[tempResult.length - 1].y;
let pointIndex = newLine.intersects.findIndex(
(p) =>
p.x === tempResult[tempResult.length - 1].x &&
p.y === tempResult[tempResult.length - 1].y
);
let d;
if (newLine.intersects[pointIndex - 1]) {
let x2 = newLine.intersects[pointIndex - 1].x;
let y2 = newLine.intersects[pointIndex - 1].y;
d = (x0 - x1) * (y2 - y1) - (y0 - y1) * (x2 - x1);
if (d > 0) tempResult.push({ x: x2, y: y2 });
}
if (newLine.intersects[pointIndex + 1]) {
let x2 = newLine.intersects[pointIndex + 1].x;
let y2 = newLine.intersects[pointIndex + 1].y;
d = (x0 - x1) * (y2 - y1) - (y0 - y1) * (x2 - x1);
if (d > 0) tempResult.push({ x: x2, y: y2 });
}
}
}
const area1 = await calcPolygonArea(result1);
const area2 = await calcPolygonArea(result2);
area1 < area2 ? (result = result1) : (result = result2);
return result;
};
Essentially, first I get all the intersections of all the lines on the chart. Then I find the closest one to the chart's starting point (0,0) as the smallest polygon enclosing it should contain that vertex. After that, I begin moving along the two lines that make up the closest intersection. Repeating the process for those two starting lines, I move clockwise along the line up to the next intersection, where I then move along the next line, continuing the process until I get a duplicate vertex in my result array, that is, until the polygon is closed. In the end, I compare the two polygons and return the smaller one.
There is most likely a more efficient way to do this, but this works for now!
End result:
Here is a possible algorithm:
While a cycle was not found:
Start at some point (x,y) on some line L
Find next intersection point (x',y') on L in clockwise direction
If the origin (0, 0) is on the right of this line:
x = x'
y = y'
L = that new line
If a cycle was found: this cycle is the polygon.
I'm trying to create a hyperdrive effect, like from Star Wars, where the stars have a motion trail. I've gotten as far as creating the motion trail on a single circle, it still looks like the trail is going down in the y direction and not forwards or positive in the z direction.
Also, how could I do this with (many) randomly placed circles as if they were stars?
My code is on jsfiddle (https://jsfiddle.net/5m7x5zxu/) and below:
var canvas = document.querySelector("canvas");
var context = canvas.getContext("2d");
var xPos = 180;
var yPos = 100;
var motionTrailLength = 16;
var positions = [];
function storeLastPosition(xPos, yPos) {
// push an item
positions.push({
x: xPos,
y: yPos
});
//get rid of first item
if (positions.length > motionTrailLength) {
positions.pop();
}
}
function update() {
context.clearRect(0, 0, canvas.width, canvas.height);
for (var i = positions.length-1; i > 0; i--) {
var ratio = (i - 1) / positions.length;
drawCircle(positions[i].x, positions[i].y, ratio);
}
drawCircle(xPos, yPos, "source");
var k=2;
storeLastPosition(xPos, yPos);
// update position
if (yPos > 125) {
positions.pop();
}
else{
yPos += k*1.1;
}
requestAnimationFrame(update);
}
update();
function drawCircle(x, y, r) {
if (r == "source") {
r = 1;
} else {
r*=1.1;
}
context.beginPath();
context.arc(x, y, 3, 0, 2 * Math.PI, true);
context.fillStyle = "rgba(255, 255, 255, " + parseFloat(1-r) + ")";
context.fill();
}
Canvas feedback and particles.
This type of FX can be done many ways.
You could just use a particle systems and draw stars (as lines) moving away from a central point, as the speed increase you increase the line length. When at low speed the line becomes a circle if you set ctx.lineWidth > 1 and ctx.lineCap = "round"
To add to the FX you can use render feedback as I think you have done by rendering the canvas over its self. If you render it slightly larger you get a zoom FX. If you use ctx.globalCompositeOperation = "lighter" you can increase the stars intensity as you speed up to make up for the overall loss of brightness as stars move faster.
Example
I got carried away so you will have to sift through the code to find what you need.
The particle system uses the Point object and a special array called bubbleArray to stop GC hits from janking the animation.
You can use just an ordinary array if you want. The particles are independent of the bubble array. When they have moved outside the screen they are move to a pool and used again when a new particle is needed. The update function moves them and the draw Function draws them I guess LOL
The function loop is the main loop and adds and draws particles (I have set the particle count to 400 but should handle many more)
The hyper drive is operated via the mouse button. Press for on, let go for off. (It will distort the text if it's being displayed)
The canvas feedback is set via that hyperSpeed variable, the math is a little complex. The sCurce function just limits the value to 0,1 in this case to stop alpha from going over or under 1,0. The hyperZero is just the sCurve return for 1 which is the hyper drives slowest speed.
I have pushed the feedback very close to the limit. In the first few lines of the loop function you can set the top speed if(mouse.button){ if(hyperSpeed < 1.75){ Over this value 1.75 and you will start to get bad FX, at about 2 the whole screen will just go white (I think that was where)
Just play with it and if you have questions ask in the comments.
const ctx = canvas.getContext("2d");
// very simple mouse
const mouse = {x : 0, y : 0, button : false}
function mouseEvents(e){
mouse.x = e.pageX;
mouse.y = e.pageY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["down","up","move"].forEach(name => document.addEventListener("mouse"+name,mouseEvents));
// High performance array pool using buubleArray to separate pool objects and active object.
// This is designed to eliminate GC hits involved with particle systems and
// objects that have short lifetimes but used often.
// Warning this code is not well tested.
const bubbleArray = () => {
const items = [];
var count = 0;
return {
clear(){ // warning this dereferences all locally held references and can incur Big GC hit. Use it wisely.
this.items.length = 0;
count = 0;
},
update() {
var head, tail;
head = tail = 0;
while(head < count){
if(items[head].update() === false) {head += 1 }
else{
if(tail < head){
const temp = items[head];
items[head] = items[tail];
items[tail] = temp;
}
head += 1;
tail += 1;
}
}
return count = tail;
},
createCallFunction(name, earlyExit = false){
name = name.split(" ")[0];
const keys = Object.keys(this);
if(Object.keys(this).indexOf(name) > -1){ throw new Error(`Can not create function name '${name}' as it already exists.`) }
if(!/\W/g.test(name)){
let func;
if(earlyExit){
func = `var items = this.items; var count = this.getCount(); var i = 0;\nwhile(i < count){ if (items[i++].${name}() === true) { break } }`;
}else{
func = `var items = this.items; var count = this.getCount(); var i = 0;\nwhile(i < count){ items[i++].${name}() }`;
}
!this.items && (this.items = items);
this[name] = new Function(func);
}else{ throw new Error(`Function name '${name}' contains illegal characters. Use alpha numeric characters.`) }
},
callEach(name){var i = 0; while(i < count){ if (items[i++][name]() === true) { break } } },
each(cb) { var i = 0; while(i < count){ if (cb(items[i], i++) === true) { break } } },
next() { if (count < items.length) { return items[count ++] } },
add(item) {
if(count === items.length){
items.push(item);
count ++;
}else{
items.push(items[count]);
items[count++] = item;
}
return item;
},
getCount() { return count },
}
}
// Helpers rand float, randI random Int
// doFor iterator
// sCurve curve input -Infinity to Infinity out -1 to 1
// randHSLA creates random colour
// CImage, CImageCtx create image and image with context attached
const randI = (min, max = min + (min = 0)) => (Math.random() * (max - min) + min) | 0;
const rand = (min = 1, max = min + (min = 0)) => Math.random() * (max - min) + min;
const doFor = (count, cb) => { var i = 0; while (i < count && cb(i++) !== true); }; // the ; after while loop is important don't remove
const sCurve = (v,p) => (2 / (1 + Math.pow(p,-v))) -1;
const randHSLA = (h, h1, s = 100, s1 = 100, l = 50, l1 = 50, a = 1, a1 = 1) => { return `hsla(${randI(h,h1) % 360},${randI(s,s1)}%,${randI(l,l1)}%,${rand(a,a1)})` }
const CImage = (w = 128, h = w) => (c = document.createElement("canvas"),c.width = w,c.height = h, c);
const CImageCtx = (w = 128, h = w) => (c = CImage(w,h), c.ctx = c.getContext("2d"), c);
// create image to hold text
var textImage = CImageCtx(1024, 1024);
var c = textImage.ctx;
c.fillStyle = "#FF0";
c.font = "64px arial black";
c.textAlign = "center";
c.textBaseline = "middle";
const text = "HYPER,SPEED FX,VII,,Battle of Jank,,Hold the mouse,button to increase,speed.".split(",");
text.forEach((line,i) => { c.fillText(line,512,i * 68 + 68) });
const maxLines = text.length * 68 + 68;
function starWarIntro(image,x1,y1,x2,y2,pos){
var iw = image.width;
var ih = image.height;
var hh = (x2 - x1) / (y2 - y1); // Slope of left edge
var w2 = iw / 2; // half width
var z1 = w2 - x1; // Distance (z) to first line
var z2 = (z1 / (w2 - x2)) * z1 - z1; // distance (z) between first and last line
var sk,t3,t3a,z3a,lines, z3, dd = 0, a = 0, as = 2 / (y2 - y1);
for (var y = y1; y < y2 && dd < maxLines; y++) { // for each line
t3 = ((y - y1) * hh) + x1; // get scan line top left edge
t3a = (((y+1) - y1) * hh) + x1; // get scan line bottom left edge
z3 = (z1 / (w2 - t3)) * z1; // get Z distance to top of this line
z3a = (z1 / (w2 - t3a)) * z1; // get Z distance to bottom of this line
dd = ((z3 - z1) / z2) * ih; // get y bitmap coord
a += as;
ctx.globalAlpha = a < 1 ? a : 1;
dd += pos; // kludge for this answer to make text move
// does not move text correctly
lines = ((z3a - z1) / z2) * ih-dd; // get number of lines to copy
ctx.drawImage(image, 0, dd , iw, lines, t3, y, w - t3 * 2, 1.5);
}
}
// canvas settings
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
// diagonal distance used to set point alpha (see point update)
var diag = Math.sqrt(w * w + h * h);
// If window size is changed this is called to resize the canvas
// It is not called via the resize event as that can fire to often and
// debounce makes it feel sluggish so is called from main loop.
function resizeCanvas(){
points.clear();
canvas.width = innerWidth;
canvas.height = innerHeight;
w = canvas.width;
h = canvas.height;
cw = w / 2; // center
ch = h / 2;
diag = Math.sqrt(w * w + h * h);
}
// create array of points
const points = bubbleArray();
// create optimised draw function itterator
points.createCallFunction("draw",false);
// spawns a new star
function spawnPoint(pos){
var p = points.next();
p = points.add(new Point())
if (p === undefined) { p = points.add(new Point()) }
p.reset(pos);
}
// point object represents a single star
function Point(pos){ // this function is duplicated as reset
if(pos){
this.x = pos.x;
this.y = pos.y;
this.dead = false;
}else{
this.x = 0;
this.y = 0;
this.dead = true;
}
this.alpha = 0;
var x = this.x - cw;
var y = this.y - ch;
this.dir = Math.atan2(y,x);
this.distStart = Math.sqrt(x * x + y * y);
this.speed = rand(0.01,1);
this.col = randHSLA(220,280,100,100,50,100);
this.dx = Math.cos(this.dir) * this.speed;
this.dy = Math.sin(this.dir) * this.speed;
}
Point.prototype = {
reset : Point, // resets the point
update(){ // moves point and returns false when outside
this.speed *= hyperSpeed; // increase speed the more it has moved
this.x += Math.cos(this.dir) * this.speed;
this.y += Math.sin(this.dir) * this.speed;
var x = this.x - cw;
var y = this.y - ch;
this.alpha = (Math.sqrt(x * x + y * y) - this.distStart) / (diag * 0.5 - this.distStart);
if(this.alpha > 1 || this.x < 0 || this.y < 0 || this.x > w || this.h > h){
this.dead = true;
}
return !this.dead;
},
draw(){ // draws the point
ctx.strokeStyle = this.col;
ctx.globalAlpha = 0.25 + this.alpha *0.75;
ctx.beginPath();
ctx.lineTo(this.x - this.dx * this.speed, this.y - this.dy * this.speed);
ctx.lineTo(this.x, this.y);
ctx.stroke();
}
}
const maxStarCount = 400;
const p = {x : 0, y : 0};
var hyperSpeed = 1.001;
const alphaZero = sCurve(1,2);
var startTime;
function loop(time){
if(startTime === undefined){
startTime = time;
}
if(w !== innerWidth || h !== innerHeight){
resizeCanvas();
}
// if mouse down then go to hyper speed
if(mouse.button){
if(hyperSpeed < 1.75){
hyperSpeed += 0.01;
}
}else{
if(hyperSpeed > 1.01){
hyperSpeed -= 0.01;
}else if(hyperSpeed > 1.001){
hyperSpeed -= 0.001;
}
}
var hs = sCurve(hyperSpeed,2);
ctx.globalAlpha = 1;
ctx.setTransform(1,0,0,1,0,0); // reset transform
//==============================================================
// UPDATE the line below could be the problem. Remove it and try
// what is under that
//==============================================================
//ctx.fillStyle = `rgba(0,0,0,${1-(hs-alphaZero)*2})`;
// next two lines are the replacement
ctx.fillStyle = "Black";
ctx.globalAlpha = 1-(hs-alphaZero) * 2;
//==============================================================
ctx.fillRect(0,0,w,h);
// the amount to expand canvas feedback
var sx = (hyperSpeed-1) * cw * 0.1;
var sy = (hyperSpeed-1) * ch * 0.1;
// increase alpha as speed increases
ctx.globalAlpha = (hs-alphaZero)*2;
ctx.globalCompositeOperation = "lighter";
// draws feedback twice
ctx.drawImage(canvas,-sx, -sy, w + sx*2 , h + sy*2)
ctx.drawImage(canvas,-sx/2, -sy/2, w + sx , h + sy)
ctx.globalCompositeOperation = "source-over";
// add stars if count < maxStarCount
if(points.getCount() < maxStarCount){
var cent = (hyperSpeed - 1) *0.5; // pulls stars to center as speed increases
doFor(10,()=>{
p.x = rand(cw * cent ,w - cw * cent); // random screen position
p.y = rand(ch * cent,h - ch * cent);
spawnPoint(p)
})
}
// as speed increases make lines thicker
ctx.lineWidth = 2 + hs*2;
ctx.lineCap = "round";
points.update(); // update points
points.draw(); // draw points
ctx.globalAlpha = 1;
// scroll the perspective star wars text FX
var scrollTime = (time - startTime) / 5 - 2312;
if(scrollTime < 1024){
starWarIntro(textImage,cw - h * 0.5, h * 0.2, cw - h * 3, h , scrollTime );
}
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
canvas { position : absolute; top : 0px; left : 0px; }
<canvas id="canvas"></canvas>
Here's another simple example, based mainly on the same idea as Blindman67, concetric lines moving away from center at different velocities (the farther from center, the faster it moves..) also no recycling pool here.
"use strict"
var c = document.createElement("canvas");
document.body.append(c);
var ctx = c.getContext("2d");
var w = window.innerWidth;
var h = window.innerHeight;
var ox = w / 2;
var oy = h / 2;
c.width = w; c.height = h;
const stars = 120;
const speed = 0.5;
const trailLength = 90;
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, w, h);
ctx.fillStyle = "#fff"
ctx.fillRect(ox, oy, 1, 1);
init();
function init() {
var X = [];
var Y = [];
for(var i = 0; i < stars; i++) {
var x = Math.random() * w;
var y = Math.random() * h;
X.push( translateX(x) );
Y.push( translateY(y) );
}
drawTrails(X, Y)
}
function translateX(x) {
return x - ox;
}
function translateY(y) {
return oy - y;
}
function getDistance(x, y) {
return Math.sqrt(x * x + y * y);
}
function getLineEquation(x, y) {
return function(n) {
return y / x * n;
}
}
function drawTrails(X, Y) {
var count = 1;
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, w, h);
function anim() {
for(var i = 0; i < X.length; i++) {
var x = X[i];
var y = Y[i];
drawNextPoint(x, y, count);
}
count+= speed;
if(count < trailLength) {
window.requestAnimationFrame(anim);
}
else {
init();
}
}
anim();
}
function drawNextPoint(x, y, step) {
ctx.fillStyle = "#fff";
var f = getLineEquation(x, y);
var coef = Math.abs(x) / 100;
var dist = getDistance( x, y);
var sp = speed * dist / 100;
for(var i = 0; i < sp; i++) {
var newX = x + Math.sign(x) * (step + i) * coef;
var newY = translateY( f(newX) );
ctx.fillRect(newX + ox, newY, 1, 1);
}
}
body {
overflow: hidden;
}
canvas {
position: absolute;
left: 0;
top: 0;
}
I read the contents of a dxf-file (only 2D) in NodeJS with a dxf parser (https://github.com/bjnortier/dxf) and then i get an array with the following output:
LINE: start.x, start.y, end.x, end.y
CIRCLE: x, y, radius
ARC: x, y ,radius, startAngle, endAngle
I wrote 3 functions based on the Bresenham-Algorithm to set the needed pixels in an array, which i want to use later to draw an canvas.
The input-parameters are
data: the denorm dxf data in an array
coordSystem: the array where to set the needed pixels
module.exports: {
processLINE: function(data, coordSystem) {
var setPixel = function(x, y) {
x = Math.ceil(x);
y = Math.ceil(y);
coordSystem[x][y] = 1;
}
var line = function(x0, y0, x1, y1) {
var dx = Math.abs(x1-x0);
var dy = Math.abs(y1-y0);
var sx = (x0 < x1) ? 1 : -1;
var sy = (y0 < y1) ? 1 : -1;
var err = dx-dy;
var e2;
while(true) {
setPixel(x0,y0);
if ((x0===x1) && (y0===y1)) break;
e2 = 2*err;
if (e2 >-dy){ err -= dy; x0 += sx; }
if (e2 < dx){ err += dx; y0 += sy; }
}
}
line(Math.ceil(data.start.x), Math.ceil(data.start.y), Math.ceil(data.end.x), Math.ceil(data.end.y))
return coordSystem;
},
processCIRCLE: function(data, coordSystem) {
var setPixel = function(x, y) {
x = Math.ceil(x);
y = Math.ceil(y);
coordSystem[x][y] = 1;
}
var createCircle = function(x0, y0, radius)
{
var f = 1 - radius;
var ddF_x = 0;
var ddF_y = -2 * radius;
var x = 0;
var y = radius;
setPixel(x0, y0 + radius);
setPixel(x0, y0 - radius);
setPixel(x0 + radius, y0);
setPixel(x0 - radius, y0);
while(x < y)
{
if(f >= 0)
{
y--;
ddF_y += 2;
f += ddF_y;
}
x++;
ddF_x += 2;
f += ddF_x + 1;
setPixel(x0 + x, y0 + y);
setPixel(x0 - x, y0 + y);
setPixel(x0 + x, y0 - y);
setPixel(x0 - x, y0 - y);
setPixel(x0 + y, y0 + x);
setPixel(x0 - y, y0 + x);
setPixel(x0 + y, y0 - x);
setPixel(x0 - y, y0 - x);
}
}
createCircle(data.x, data.y, data.r);
return coordSystem;
},
processARC: function(data, coordSystem) {
var setPixel = function(x, y, coordinates) {
x = Math.ceil(x);
y = Math.ceil(y);
coordSystem[x][y] = 1;
}
var createPartialcircle = function()
{
startAngle = data.startAngle*180/Math.PI;
endAngle = data.endAngle*180/Math.PI;
if(startAngle>endAngle) {
for (var i=startAngle; i>endAngle; i--) {
var radians = i * Math.PI / 180;
var px = data.x - data.r * Math.cos(radians);
var py = data.y - data.r * Math.sin(radians);
setPixel(px, py, coordinates);
}
} else {
for (var i=startAngle; i<endAngle; i++) {
var radians = i * Math.PI / 180;
var px = data.x + data.r * Math.cos(radians);
var py = data.y + data.r * Math.sin(radians);
setPixel(px, py, coordinates);
}
}
}
createPartialcircle(data.x, data.y, data.r);
return coordSystem;
}
}
With this i get the following shape:
As you can see it works, but there are some "holes" and because of this my last function which should fill the hole shape (scan-line-algorithm), doesn't work well...
Here is how i fill the shape
I took this code from HERE and wrote it in JavaScript-Style.
function scanLineFill(config, data, x, y, fillColor) {
function getPixel(x,y) {
return data[x][y];
}
function setPixel(x,y) {
data[x][y] = fillColor;
}
// Config
var nMinX = 0;
var nMinY = 0;
var nMaxX = config.maxValues.x;
var nMaxY = config.maxValues.y;
var seedColor = getPixel(x,y);
function lineFill(x1, x2, y) {
var xL,xR;
if( y < nMinY || nMaxY < y || x1 < nMinX || nMaxX < x1 || x2 < nMinX || nMaxX < x2 )
return;
for( xL = x1; xL >= nMinX; --xL ) { // scan left
if( getPixel(xL,y) !== seedColor )
break;
setPixel(xL,y);
}
if( xL < x1 ) {
lineFill(xL, x1, y-1); // fill child
lineFill(xL, x1, y+1); // fill child
++x1;
}
for( xR = x2; xR <= nMaxX; ++xR ) { // scan right
console.log('FOR: xR --> ', xR)
if( getPixel(xR,y) !== seedColor )
break;
setPixel(xR,y);
}
if( xR > x2 ) {
lineFill(x2, xR, y-1); // fill child
lineFill(x2, xR, y+1); // fill child
--x2;
}
for( xR = x1; xR <= x2 && xR <= nMaxX; ++xR ) { // scan betweens
if( getPixel(xR,y) === seedColor )
setPixel(xR,y);
else {
if( x1 < xR ) {
// fill child
lineFill(x1, xR-1, y-1);
// fill child
lineFill(x1, xR-1, y+1);
x1 = xR;
}
// Note: This function still works if this step is removed.
for( ; xR <= x2 && xR <= nMaxX; ++xR) { // skip over border
if( getPixel(xR,y) === seedColor ) {
x1 = xR--;
break;
}
}
}
}
}
if( fillColor !== seedColor ) {
lineFill(x, x, y);
}
return data;
}
And the result is this:
I think if the shape has no holes, the fill-function would fill the shape correct. But how can i achieve this?
I did an experiment today to see what I can do with <div>s. So I made a simple Paint-like program, which you can draw with <div>s.
$(window).mousemove(function(e){
if(!mousedown){
return false;
}
var x = e.clientX,
y = e.clientY;
drawDot(x,y,ele);
lastX = x;
lastY = y;
});
This is part of the code. It works, but there are gaps between dots. So I created a function called fillDot which will draw a line from point A (last point) to point B (current point).
drawDot(x,y,ele);
fillDot(lastX,lastY,x,y,ele);
function fillDot(lx,ly,x,y,canvas){
var rise = y - ly,
run = x - lx,
slope = rise / run,
yInt = y - (slope * x);
if( lx < x ){
for(var i = lx; i < x ; i+=.5){
var y = slope * i + yInt;
drawDot(i,y,canvas);
}
}else if( x < lx ){
for(var i = x; i < lx ; i++){
var y = slope * i + yInt;
drawDot(i,y,canvas);
}
}
}
It works fine only when I am drawing horizontal-ish lines. When I draw from top to bottom or bottom to top, there will still be gaps. I found something called Bresenham's line algorithm which can do the same thing, but I don't know how to use it...
Any idea how to fill all points in between?
ps: I know there is <canvas>, but I am testing what I can do with <div>.
Nevermind, I translated the Bresenham's line algorithm into JavaScript and it works perfectly now!
function fillDot(x0, y0, x1, y1){
var dx = Math.abs(x1-x0);
var dy = Math.abs(y1-y0);
var sx = (x0 < x1) ? 1 : -1;
var sy = (y0 < y1) ? 1 : -1;
var err = dx-dy;
while(true){
drawDot(x0,y0);
if ((x0==x1) && (y0==y1)) break;
var e2 = 2*err;
if (e2>-dy){
err -= dy;
x0 += sx;
}
if (e2 < dx){
err += dx;
y0 += sy;
}
}
}