【问题标题】:How can I fill color between 4 lines when it connect in canvas?当它在画布中连接时,如何在 4 行之间填充颜色?
【发布时间】:2021-02-09 17:51:37
【问题描述】:

当它以离子连接时,我试图在线条之间填充颜色。当四线相互接触时,我想在线之间填充颜色。为此,我使用触摸事件创建了一个画布演示。请帮助解决我的问题。

我们在画布框中有 4 条线,我们将拖动它们并连接每条线并给出一个形状线框。这意味着我们的线已连接,现在在线之间填充颜色,因此框被颜色填充。

html 文件:

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

ts 文件:

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();
    }

}

四线接触或连接时如何填充颜色?

【问题讨论】:

标签: javascript angularjs ionic-framework


【解决方案1】:

如何获取像素的颜色

基本上,当您执行触摸事件时,像素会改变颜色。您可以找出事件影响了哪些像素。将像素作为输入,您可以找出像素的颜色: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>

这段代码不是我写的。

了解问题

现在我们知道如何逐个像素地获取颜色,我们需要将我们的问题转换为等价的东西,但它更容易计算。我将问题定义如下:

以连续方式从给定点开始遍历像素 为了找出是否可以形成一个循环,其中包含像素 边界内的某种(默认)颜色,应该改变 它们的颜色为某种填充颜色。

如何解决

  • 从给定点开始循环像素遍历,并始终在每一步中将坐标值更改为与当前点相邻的点或循环中较早访问的某个点
  • 始终存储当前访问点的坐标,因此如果您要访问同一点,则第二次忽略它
  • 使用数据结构,如堆栈来存储所有要访问但尚未访问的邻居,将您访问的每个点的所有邻居放入堆栈(除非该点已被访问)
  • 如果您从尚未访问过的点返回起点,请记录这是一个循环
  • 始终跟踪边界点
  • 当您发现它是一个循环并且您知道它的位置时,遍历该区域中的每个点,如果它具有默认颜色,请检查是否存在可以“离开”循环的连续线只访问默认颜色的邻居,就像你检查它是否是一个循环一样。如果没有,则将像素(及其所有相似颜色的相邻像素)绘制为您需要的颜色

【讨论】:

  • 没看懂,可以的话做个小demo吧。
  • @user9088454 上面的方法有什么难理解的?
【解决方案2】:

由于我不熟悉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();
&lt;canvas width="600" height="800"&gt;&lt;canvas&gt;

解决方案

真正的解决方案是intersect 函数。这是做什么的,它需要两条线段并计算它们相交的点。这真的是你唯一缺少的东西。 intersect 算法有点太长了,这里不展开讨论,但是你可以看看细节here。在我们在每个单独的线段之间进行了非常低效的O(n^2)intersection 查询之后。然后我们可以确定我们的矩形是否有足够的交点。我离散地计算 A -&gt; B 和 B -&gt; A 交叉点,而没有考虑剔除交叉点候选者,因为我正在处理 4 个线段。这意味着我们需要 8 个交叉点来闭合矩形。一旦我们知道我们有一个封闭的多边形,我们就可以使用所有交叉点的所有 x/y 分量的min(...)/max(...) 绘制一个矩形。这给了我们两个点P,Q,然后我们可以用它们来填充一个矩形。

如果你喜欢这种东西,我强烈推荐Real Time Collision Detection by Christer Ericson 或类似的书。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-08-11
    • 2013-12-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多