【问题标题】:Subdivision of Four-Sided, 2D Shape四面、二维形状的细分
【发布时间】:2018-06-10 13:34:50
【问题描述】:

我正在寻找一种将四边形分割成网格的方法。例如:

最终我需要能够将生成的形状转换为 SVG,但我很乐意处理与另一个库或坐标系之间的转换。我正在寻找的是如何进行计算。

假设形状是一个四边形,每边都可以是凹的或凸的,但没有边缘与其他边缘或它们自身重叠,并且四个边中的任何一个都可以弯曲。

四边形的相同方法(直边形状很简单),如果两条相对的边是直线,很容易找到相交点,因为它们将位于绘制的细分之间的直线上对立的双方。从那里可以相对容易地计算将它们连接到沿替代轴的前一个点所需的曲线:

但是,当没有两条相对的直边时(如上面的第三个示例),我不确定如何找到这些点,因为不再确定点位于一条直线上。

我花了很长时间寻找记录在案的方法,但无济于事。

这里是用SVG来描述的那种起始形状的例子(不用SVG处理,只要我能输出到SVG就行了。

<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
     viewBox="0 0 406.4 233.4" xml:space="preserve">
  <path class="st0" d="M394.3,232.7c-106-37.8-353.7,0-353.7,0s-90.4-151.2,0-207.3s353.7,0,353.7,0S420.3,154.7,394.3,232.7z"/>
</svg>

编辑:我在 Stack Exchange Maths 上询问了类似的 question,其中一个答案描述了一种方法 - 使用 Coons Patch。 Quora 解释here.

【问题讨论】:

  • 我不确定 二次多边形 是否存在(虽然我不是专家,很乐意得到纠正),但也许你的意思是非欧几里得空间中的多边形?如果是,那么这些多边形是否保证是圆柱表面上的几何形状或圆锥表面或椭圆几何形状(即球表面)上的几何形状,如第一张图片所示?
  • @sjaustirni 我的意思是二维多边形。当我说二次曲线时,我的意思是它们有四个由曲线连接的角,这些曲线可以用二次曲线来描述——即每条边都是一条曲线。边可以是凸面的或凹面的,尽管(如第一张图)它们看起来像是映射到 3d 对象上,但这不是我的意图。
  • 那么也许使用 polygons 这个词不是最好的主意(多边形的边是直线)。无论如何,如图所示,形状总是沿轴对称吗?另外,您对开头的形状了解多少,输入是什么? Cubic-Bezier-curve-like paths?
  • 目前只有移动设备,所以不能真正添加​​示例,但您可以假设三次贝塞尔曲线,并且该形状是用 SVG 描述的。
  • @sjaustirni 在问题中添加了一个 svg 示例。

标签: javascript svg grid quadratic cubic-bezier


【解决方案1】:

您可以在 Codepen here 上查看实时示例和完整代码。

数据表示

下图最简单的数据表示使用三次贝塞尔曲线。我相信这也将涵盖您的所有用例。为了不让各种特殊情况污染我们的代码,我们将要求输入始终采用四个后续三次 Bézier 曲线的格式,就像我们绘制它们一样。这意味着我们不能使用:

  • 二次贝塞尔曲线(通过镜像另一个控制点转换为三次)
  • (通过将控制点等距放置在线上的端点之间转换为三次贝塞尔曲线)
  • 关闭路径 [Z SVG 命令](通过计算给定的段然后从那里取出来转换为三次贝塞尔曲线)

More on the subject of paths in SVG

它的 SVG 表示

<path d=" M50 50
     C 100 100 400 100 450 50
     C 475 250 475 250 450 450
     C 250 300 250 300 50 450
     C 150 100 150 250 50 50"
 fill="transparent"
 stroke="black"
/>

不过,为方便起见,我们将定义自己的数据结构。

Point 只是一个普通的旧 Vector2D 类。

class Point {
  constructor (x, y) {
    this.x = x
    this.y = y
  }
}

Curve 是三次贝塞尔曲线。

class Curve {
  constructor (
    startPointX, startPointY,
    controlPointAX, controlPointAY,
    controlPointBX, controlPointBY,
    endPointX, endPointY) {
    this.start = new Point(startPointX, startPointY)
    this.controlA = new Point(controlPointAX, controlPointAY)
    this.controlB = new Point(controlPointBX, controlPointBY)
    this.end = new Point(endPointX, endPointY)
  }

}

Grid 只是曲线的容器。

class Grid {
  constructor (topSide, rightSide, bottomSide, leftSide, horizontalCuts, verticalCuts) {
    this.topSide = topSide
    this.rightSide = rightSide
    this.bottomSide = bottomSide
    this.leftSide = leftSide

    // These define how we want to slice our shape. Just ignore them for now
    this.verticalCuts = verticalCuts
    this.horizontalCuts = horizontalCuts
  }
}

让我们用相同的形状填充它。

let grid = new Grid(
  new Curve(50, 50, 100, 100, 400, 100, 450, 50),
  new Curve(450, 50, 475, 250, 475, 250, 450, 450),
  new Curve(450, 450, 250, 300, 250, 300, 50, 450),
  new Curve(50, 450, 150, 100, 150, 250, 50, 50),
  8,
  6
)

寻找交点

显然你已经实现了它using the t approach (as opposed to true curve splice length)所以我把它放在这里只是为了完整性。

注意 cuts 是您将获得的交点(红点)的实际数量。也就是说,起点和终点不存在(但对cut()稍作修改可以存在)

function cut (cuts, callback) {
  cuts++
  for (let j = 1; j < cuts; j++) {
    const t = j / cuts
    callback(t)
  }
}

class Curve {

// ...

  getIntersectionPoints (cuts) {
    let points = []
    cut(cuts, (t) => {
      points.push(new Point(this.x(t), this.y(t)))
    })
    return points
  }

  x (t) {
    return ((1 - t) * (1 - t) * (1 - t)) * this.start.x +
            3 * ((1 - t) * (1 - t)) * t * this.controlA.x +
            3 * (1 - t) * (t * t) * this.controlB.x +
            (t * t * t) * this.end.x
  }

  y (t) {
    return ((1 - t) * (1 - t) * (1 - t)) * this.start.y +
            3 * ((1 - t) * (1 - t)) * t * this.controlA.y +
            3 * (1 - t) * (t * t) * this.controlB.y +
            (t * t * t) * this.end.y
  }
}

寻找分裂曲线

function lerp (from, to, t) {
  return from * (1.0 - t) + (to * t)
}

class Curve {

// ...

  getSplitCurves (cuts, oppositeCurve, fromCurve, toCurve) {
    let curves = []
    cut(cuts, (t) => {
      let start = new Point(this.x(t), this.y(t))
      // NOTE1: We must go backwards
      let end = new Point(oppositeCurve.x(1 - t), oppositeCurve.y(1 - t))
      let controlA = new Point(
        // NOTE2: Interpolate control points
        lerp(fromCurve.controlA.x, toCurve.controlA.x, t),
        lerp(fromCurve.controlA.y, toCurve.controlA.y, t)
      )
      let controlB = new Point(
        // NOTE2: Interpolate control points
        lerp(fromCurve.controlB.x, toCurve.controlB.x, t),
        lerp(fromCurve.controlB.y, toCurve.controlB.y, t)
      )
      curves.push(new Curve(
        start.x, start.y,
        controlA.x, controlA.y,
        controlB.x, controlB.y,
        end.x, end.y
      ))
    })
    return curves
  }
}

上面的代码有一些可疑的地方。

NOTE1: 因为曲线是按照你绘制它们的顺序来表示的,所以相对的边朝向不同的方向。例如,顶部是从左到右绘制的,而底部是从右到左绘制的。也许一张图片会有所帮助:

NOTE2:这就是我们如何获得用于分割形状的贝塞尔曲线的控制点。 t在连接对边控制点的段上插值。

这里是那些片段。它们的端点是各自曲线的控制点。

这是我们渲染曲线时的最终结果:

您可以在 Codepen here 上查看实时示例和完整代码。

从这里去哪里

更多路口

这显然不是最终结果。我们仍然需要找到生成曲线的交点。然而,找到两条贝塞尔曲线的交点并非易事。 这是一个关于该主题的nice StackOverflow answer,它将引导你到这个neat library,它将为你完成繁重的工作(看看bezier3bezier3()code,你就会明白)

分割曲线

一旦找到交点,您就会想找到at which t they occur。你问为什么t?所以你可以split the curve

实际最终结果

最后,您需要选择曲线的这些部分并排列它们以表示网格的各个字段。

如您所见,您还有很长的路要走,我只走了一小部分(但仍然设法写了一个冗长的答案:D)。

【讨论】:

    【解决方案2】:

    如果你的四个边是三次贝塞尔曲线,那么相对简单的东西怎么样:

    要制作水平分隔线(从上到下),通过插值顶部和底部的控制点来制作新的三次贝塞尔曲线:

    然后,将左右两边分成相同数量的点..

    ..并拉伸分隔曲线,使它们连接到这些点:

    然后,从左到右执行相同的操作以创建垂直分隔线。

    这是一个测试不同形状的笔:https://codepen.io/Sphinxxxx/pen/pKddee

    重要的部分在BezierWrapper.lerpCurve()BezierWrapper.fitCurve() 中,还有从https://gamedev.stackexchange.com/a/5427 中获取的Bezier 类,以沿曲线(.samples)获得均匀分布的点:

    class BezierWrapper {
        constructor(controls, sampleCount, classname) {
            this.controls = controls;
            this.classname = classname;
    
            if(sampleCount) {
                function point2obj(p) {
                    return { x: p[0], y: p[1] };
                }
                //https://gamedev.stackexchange.com/a/5427
                const interpolator = new Bezier(point2obj(controls[0]),
                                                point2obj(controls[1]),
                                                point2obj(controls[2]),
                                                point2obj(controls[3]));
                const samples = this.samples = [];
                for(let i = 1; i <= sampleCount; i++) {
                    const t = i / (sampleCount+1);
                    samples.push([interpolator.mx(t), interpolator.my(t)]);
                }
            }
        }
    
        static lerpCurve(source, target, t) {
    
            function lerpCoord(from, to, t) {
                const diffX = to[0] - from[0],
                      diffY = to[1] - from[1],
                      lerpX = from[0] + (diffX * t),
                      lerpY = from[1] + (diffY * t);
                return [lerpX, lerpY];
            }
    
            const middle = source.map((c, i) => lerpCoord(c, target[i], t));
            return middle;
        }
    
        static fitCurve(source, start, end) {
    
            function distance(p1, p2) {
                const dx = p2[0] - p1[0],
                      dy = p2[1] - p1[1];
                return Math.sqrt(dx*dx + dy*dy);
            }
    
            //https://gist.github.com/conorbuck/2606166
            function angle(p1, p2) {
                const dx = p2[0] - p1[0],
                      dy = p2[1] - p1[1],
                      radians = Math.atan2(dy, dx);
                return radians;
            }
    
            //https://stackoverflow.com/questions/2259476/rotating-a-point-about-another-point-2d
            function rotate(p, radians) {
                const x = p[0],
                      y = p[1],
                      cos = Math.cos(radians),
                      sin = Math.sin(radians);
    
                return [cos*x - sin*y, sin*x + cos*y];
            }
    
            const sourceStart = source[0],
                  sourceEnd = source[3],
                  scale = distance(start, end)/distance(sourceStart, sourceEnd),
                  rot = angle(start, end) - angle(sourceStart, sourceEnd);
    
            //Translate, scale and rotate the source control points to make them fit the start and end points:
            const sourceNorm = source.map(c => [c[0] - sourceStart[0], c[1] - sourceStart[1]]),
                  fittedNorm = sourceNorm.map(c => rotate([c[0]*scale, c[1]*scale], rot)),
                  fitted = fittedNorm.map(c => [c[0] + start[0], c[1] + start[1]]);
    
            return fitted;
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2022-12-10
      • 2012-07-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多