How can I fill color between 4 lines when it connect in canvas?

Viewed 835

I am trying to fill color between lines when it connects in ionic. I want to fill color between line when four-line touch each other. For that, I created a canvas demo using a touch event. Please help to solve my issue.

We have 4 lines in the canvas box and we will drag them and connect each line and give a shape line box. that means our line is connected now fill color between line so the box is filled up with color.

html file:

 <canvas #canvasDraw width="300" height="300" (touchstart)="handleTouchStart($event)"
        (touchmove)="handleTouchmove($event)" 
        (touchend)="handleTouchEnd($event)">
        You need a browser that supports HTML5!
 </canvas>

ts file:

import { Component, ElementRef, ViewChild } from '@angular/core';

@Component({
    selector: 'app-home',
    templateUrl: 'home.page.html',
    styleUrls: ['home.page.scss'],
})
export class HomePage {
    @ViewChild('canvasDraw', { static: false }) canvas: ElementRef;

    canvasElement: any;
    lines: any[];
    isDown: boolean = false;
    startX: number;
    startY: number;
    nearest: any;
    offsetX: any;
    offsetY: any;

    constructor() {
    }

    ngAfterViewInit() {
        this.canvasElement = this.canvas.nativeElement;
        this.lines = [];
        this.lines.push({ x0: 75, y0: 25, x1: 125, y1: 25 });
        this.lines.push({ x0: 75, y0: 100, x1: 125, y1: 100 });
        this.lines.push({ x0: 50, y0: 35, x1: 50, y1: 85 });
        this.lines.push({ x0: 150, y0: 35, x1: 150, y1: 85 });
        this.draw();
        //this.reOffset();
        requestAnimationFrame(() => {
            this.reOffset()
        })
    }

    reOffset() {
        let BB = this.canvasElement.getBoundingClientRect();
        this.offsetX = BB.left;
        this.offsetY = BB.top;
    }

    // select the this.nearest line to the mouse
    closestLine(mx, my) {
        let dist = 100000000;
        let index, pt;
        for (let i = 0; i < this.lines.length; i++) {
            //
            let xy = this.closestXY(this.lines[i], mx, my);
            //
            let dx = mx - xy.x;
            let dy = my - xy.y;
            let thisDist = dx * dx + dy * dy;
            if (thisDist < dist) {
                dist = thisDist;
                pt = xy;
                index = i;
            }
        }
        let line = this.lines[index];
        return ({ pt: pt, line: line, originalLine: { x0: line.x0, y0: line.y0, x1: line.x1, y1: line.y1 } });
    }

    // linear interpolation -- needed in setClosestLine()
    lerp(a, b, x) {
        return (a + x * (b - a));
    }

    // find closest XY on line to mouse XY
    closestXY(line, mx, my) {
        let x0 = line.x0;
        let y0 = line.y0;
        let x1 = line.x1;
        let y1 = line.y1;
        let dx = x1 - x0;
        let dy = y1 - y0;
        let t = ((mx - x0) * dx + (my - y0) * dy) / (dx * dx + dy * dy);
        t = Math.max(0, Math.min(1, t));
        let x = this.lerp(x0, x1, t);
        let y = this.lerp(y0, y1, t);
        return ({ x: x, y: y });
    }

    // draw the scene
    draw() {
        let ctx = this.canvasElement.getContext('2d');
        let cw = this.canvasElement.width;
        let ch = this.canvasElement.height;
        ctx.clearRect(0, 0, cw, ch);
        // draw all lines at their current positions
        for (let i = 0; i < this.lines.length; i++) {
            this.drawLine(this.lines[i], 'black');
        }
        // draw markers if a line is being dragged
        if (this.nearest) {
            // point on line this.nearest to mouse
            ctx.beginPath();
            ctx.arc(this.nearest.pt.x, this.nearest.pt.y, 5, 0, Math.PI * 2);
            ctx.strokeStyle = 'red';
            ctx.stroke();
            // marker for original line before dragging
            this.drawLine(this.nearest.originalLine, 'red');
            // hightlight the line as its dragged
            this.drawLine(this.nearest.line, 'red');
        }        
    }

    drawLine(line, color) {
        let ctx = this.canvasElement.getContext('2d');
        ctx.beginPath();
        ctx.moveTo(line.x0, line.y0);
        ctx.lineTo(line.x1, line.y1);
        ctx.strokeStyle = color;
        ctx.stroke();
    }

    handleTouchStart(e) {
        // tell the browser we're handling this event
        let tch = e.touches[0];
        // tch.preventDefault();
        // tch.stopPropagation();
        // mouse position
        this.startX = tch.clientX - this.offsetX;
        this.startY = tch.clientY - this.offsetY;
        // find this.nearest line to mouse
        this.nearest = this.closestLine(this.startX, this.startY);
        this.draw();
        // set dragging flag
        this.isDown = true;
    }

    handleTouchEnd(e) {
        // tell the browser we're handling this event
        let tch = e.touches[0];
        // tch.preventDefault();
        // tch.stopPropagation();
        // clear dragging flag
        this.isDown = false;
        this.nearest = null;
        this.draw();
    }

    handleTouchmove(e) {
        if (!this.isDown) { return; }
        // tell the browser we're handling this event
        let tch = e.touches[0];
        // tch.preventDefault();
        // tch.stopPropagation();
        // mouse position
        const mouseX = tch.clientX - this.offsetX;
        const mouseY = tch.clientY - this.offsetY;
        // calc how far mouse has moved since last mousemove event
        let dx = mouseX - this.startX;
        let dy = mouseY - this.startY;
        this.startX = mouseX;
        this.startY = mouseY;
        // change this.nearest line vertices by distance moved
        let line = this.nearest.line;
        line.x0 += dx;
        line.y0 += dy;
        line.x1 += dx;
        line.y1 += dy;
        // redraw
        this.draw();

        let ctx = this.canvasElement.getContext('2d');
        ctx.beginPath();
        ctx.rect(line.x0, line.y0, line.x1, line.y1);
        ctx.fillStyle = "red";
        ctx.fill();
    }

}

enter image description here

How to fill color when four line touch or connect?

2 Answers

How to get a pixel's color

Basically, when you do your touch event, pixels are changing color. You can find out which pixel(s) are/were affected from the event. Having the pixel(s) as input you can find out what color a pixel has: https://jsfiddle.net/ourcodeworld/8swevoxo/

var canvas = document.getElementById("canvas");

function getPosition(obj) {
    var curleft = 0, curtop = 0;
    if (obj.offsetParent) {
        do {
            curleft += obj.offsetLeft;
            curtop += obj.offsetTop;
        } while (obj = obj.offsetParent);
        return { x: curleft, y: curtop };
    }
    return undefined;
}

function rgbToHex(r, g, b) {
    if (r > 255 || g > 255 || b > 255)
        throw "Invalid color component";
    return ((r << 16) | (g << 8) | b).toString(16);
}

function drawImageFromWebUrl(sourceurl){
      var img = new Image();

      img.addEventListener("load", function () {
          // The image can be drawn from any source
          canvas.getContext("2d").drawImage(img, 0, 0, img.width,    img.height, 0, 0, canvas.width, canvas.height);
          
      });

      img.setAttribute("src", sourceurl);
}
// Draw a base64 image because this is a fiddle, and if we try with an image from URL we'll get tainted canvas error
// Read more about here : http://ourcodeworld.com/articles/read/182/the-canvas-has-been-tainted-by-cross-origin-data-and-tainted-canvases-may-not-be-exported
drawImageFromWebUrl("data:image/gif;base64,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");

canvas.addEventListener("mousemove",function(e){
      var pos = getPosition(this);
    var x = e.pageX - pos.x;
    var y = e.pageY - pos.y;
    var coord = "x=" + x + ", y=" + y;
    var c = this.getContext('2d');
    var p = c.getImageData(x, y, 1, 1).data; 
        
    // If transparency on the image
    if((p[0] == 0) && (p[1] == 0) && (p[2] == 0) && (p[3] == 0)){
                coord += " (Transparent color detected, cannot be converted to HEX)";
    }
    
    var hex = "#" + ("000000" + rgbToHex(p[0], p[1], p[2])).slice(-6);
    
    document.getElementById("status").innerHTML = coord;
    document.getElementById("color").style.backgroundColor = hex;
},false);
<canvas id="canvas" width="150" height="150"></canvas>
<div id="status"></div><br>
<div id="color" style="width:30px;height:30px;"></div>
<p>
Move the mouse over the BUS !
</p>

This code was not written by me.

Understanding the problem

Now that we know how to get the color pixel by pixel, we need to convert our question to something that is equivalent, but it's easier to compute. I would define the problem as follows:

Traverse pixels starting from a given point in a continuous manner in order to find out whether a cycle can be formed, which contains pixels of a certain (default) color inside the boundary, which should change their color to some fill color.

How to solve it

  • Start a cycle of pixel traversing starting from a given point and always changing coordinate values in each step to a point neighboring the current point or some point visited earlier in the loop
  • Always store the coordinates of the currently visited point so if you would visit the same point, you just ignore it on the second time
  • Use a data structure, like a stack to store all neighbors to visit, but not yet visited, put in the stack all neighbors of each point you visit (unless the point was already visited)
  • If you ever arrive back to the starting point from a point which was not already visited, then register that this is a cycle
  • Always keep track of boundary points
  • When you found out that it's a cycle and you know where it is located, traverse each point in the region and if it has a default color, check whether there is a continuous line by which you could "leave" the cycle by visiting only neighbors of default color, pretty similarly as you have checked whether it is a cycle. If not, then paint the pixel (and all its neighbors of similar color) to the color you need

I wrote the following example using web technologies as im not familiar with ionic.

const canvas = document.querySelector('canvas');
const context = canvas.getContext('2d');
const width = canvas.width;
const height = canvas.height;

// compute signed triangle area of triangle abc. Negative if triangle is cw.
const area = (a, b, c) => {
  return (a[0] - c[0]) * (b[1] - c[1]) - (a[1] - c[1]) * (b[0] - c[0]);
};

// compute intersections of line segments.
const intersect = (s0, s1) => {
  let result = null;
  const a1 = area(s0.p0, s0.p1, s1.p1);
  const a2 = area(s0.p0, s0.p1, s1.p0);

  if (a1 * a2 < 0) {
    const a3 = area(s1.p0, s1.p1, s0.p0);
    const a4 = a3 + a2 - a1;

    if (a3 * a4 < 0) {
      const t = a3 / (a3 - a4);

      result = [
        s0.p0[0] + t * (s0.p1[0] - s0.p0[0]),
        s0.p0[1] + t * (s0.p1[1] - s0.p0[1])
      ];
    }
  }

  return result;
};

const dot = (a, b) => a[0] * b[0] + a[1] * b[1];

const sub = (a, b) => [a[0] - b[0], a[1] - b[1]];

const clamp = (x, min, max) => Math.min(max, Math.max(x, min));

const min = (points) => {
  return points.reduce((r, p) => {
    return [
      Math.min(p[0], r[0]),
      Math.min(p[1], r[1])
    ];
  }, [Infinity, Infinity]);
};

const max = (points) => {
  return points.reduce((r, p) => {
    return [
      Math.max(p[0], r[0]),
      Math.max(p[1], r[1])
    ];
  }, [-Infinity, -Infinity]);
};

const state = {
  lines: [{
      p0: [75, 25],
      p1: [125, 25]
    },
    {
      p0: [75, 100],
      p1: [125, 100]
    },
    {
      p0: [50, 35],
      p1: [50, 85]
    },
    {
      p0: [150, 35],
      p1: [150, 85]
    }
  ],
  dragging: false,
  target: null,
  m: null
};

const draw = (state) => {
  context.clearRect(0, 0, width, height);

  const intersections = [];

  for (let i = 0; i < state.lines.length; i++) {
    const lines = state.lines;
    const l0 = lines[i];
    
    context.strokeStyle = "black";
    
    for (let j = 0; j < lines.length; j++) {
      if (j !== i) {
        const intersection = intersect(l0, lines[j]);

        if (intersection != null) {
          intersections.push(intersection);
          context.strokeStyle = "blue";
        }
      }
    }

    context.beginPath();
    context.moveTo(l0.p0[0], l0.p0[1]);
    context.lineTo(l0.p1[0], l0.p1[1]);
    context.stroke();
  }

  if (intersections.length == 8) {
    const p = min(intersections);
    const q = max(intersections);

    context.fillStyle = "red";
    context.fillRect(p[0], p[1], q[0] - p[0], q[1] - p[1]);
  }
};

const run = () => {
  requestAnimationFrame(() => {
    draw(state);
    run();
  });
};

canvas.addEventListener('mousedown', (e) => {
  const rect = e.target.getBoundingClientRect();
  const m = [e.x - rect.x, e.y - rect.y];

  const lines = state.lines
    .map((line) => {
      const ab = sub(line.p1, line.p0);
      const am = sub(m, line.p0);
      const bm = sub(m, line.p1);
      const e = dot(am, ab);

      if (e <= 0) {
        return dot(am, am);
      }

      const f = dot(ab, ab);

      if (e >= f) {
        return dot(bm, bm);
      }

      return dot(am, am) - e * e / f;
    })
    .map((d, i) => [d, state.lines[i]])
    .sort((a, b) => a[0] - b[0]);

  const first = lines[0];

  if (first[0] < 50) {
    state.dragging = true;
    state.target = first[1];
    state.m = m;
  }
});

canvas.addEventListener('mouseup', () => {
  state.dragging = false;
  state.target = null;
  state.m = null;
});

canvas.addEventListener('mousemove', (e) => {
  if (state.dragging) {
    const rect = e.target.getBoundingClientRect();
    const m = [e.x - rect.x, e.y - rect.y];
    const dx = m[0] - state.m[0];
    const dy = m[1] - state.m[1];

    state.target.p0[0] = dx + state.target.p0[0];
    state.target.p0[1] = dy + state.target.p0[1];
    state.target.p1[0] = dx + state.target.p1[0];
    state.target.p1[1] = dy + state.target.p1[1];
    state.m = m;
  }
}, {
  passive: true
});

run();
<canvas width="600" height="800"><canvas>

Solution

The real meat of the solution is the intersect function. What this does it it takes two linesegments and calculates the point that they intersect. This is really the only thing that you were missing. The intersect algorithm is a bit too long to discuss here but you can take a look at the details here. After we do a pretty inefficient O(n^2) intersection query between every single line segment. We then can figure out if we have have enough intersections for our rectangle. I compute both A -> B and B -> A intersections discretely with out any regard of culling the intersection candidate since i'm dealing with 4 line segments. What this means is we need 8 intersections to close our rectangle. Once we know we have a closed polygon we can draw a rectangle using the min(...)/max(...) of all x/y components of all intersections. This gives us two points P,Q that we can then use to fill in a rect.

If you are into this kind of thing I highly recommend the book Real Time Collision Detection by Christer Ericson or similar.

Related