23

What I want to do?

Updates to this question: 7/10/2012 - "gradientTransform not quite" Inspired by Duopixel 7/11/2012 - "SVG Code from the example" 7/16/2012 - "@dignifiedquire take on this problem"

I'm trying to create a tool that let's the user dynamically resize polygons. Most of the polygons are filled with gradients fill="url(#top_surface_1_gradient)". The way I go about this is a simple JavaScript script that:

  1. looks for mousemove & click events over a certain polygon
  2. measures the amount of movement
  3. changes half of the coordinates of the polygon (in order to have the effect of stretching) using this algorithm to define new coordinates: x = x_movement, y = x_movement * Math.tan( 31 * (Math.PI/180) )
  4. polygons that are filled with a single colour are OK
  5. polygons that are filled with a gradient are not, let me demonstrate:

Visually

step_1

So this is step one, no stretching has been done by the user.

step_2

This is where the problem happens. Since I don't know how should I change the x1, y1 and x2, y2 coordinates for the gradient, it just stays hanging in it's old position while the polygon has been stretched. The result is a shape that fails to sustain the illusion of depth.

step_3

The end result I'm looking for. And bare in mind that the gradient might have a completely random angle from the get go. In this end result, that I'm looking for, both the x1, y1 and x2, y2 coordinates of the gradient have been changed. What algorithm should be used to calculate these positions? I'm looking for a solution that is completely blind to the angle of the gradient.

Below is the SVG with all the appropriate coordinates that was used to generate these examples:

Using SVG code

Step1:

<!-- Step 1 -->
<linearGradient id="top_surface_1_gradient" gradientUnits="userSpaceOnUse" x1="165.3425" y1="39.7002" x2="-49.991" y2="43.0337">
    <stop  offset="0" style="stop-color:#FFFFFF"/>
    <stop  offset="0.6687" style="stop-color:#CCCCCC"/>
    <stop  offset="1" style="stop-color:#FFFFFF"/>
</linearGradient>
<polygon id="top_surface_1" fill="url(#top_surface_1_gradient)" points="137.145,41.204 68.572,0 0,41.204 68.572,82.396"/>

Step 2

<!-- Step 2 --> 
<linearGradient id="top_surface_2_gradient" gradientUnits="userSpaceOnUse" x1="250.0491" y1="233.8115" x2="23.7637" y2="237.3146">
    <stop  offset="0" style="stop-color:#FFFFFF"/>
    <stop  offset="0.6687" style="stop-color:#CCCCCC"/>
    <stop  offset="1" style="stop-color:#FFFFFF"/>
</linearGradient>
<polygon id="top_surface_2" fill="url(#top_surface_2_gradient)" points="205.788,215.557 137.215,174.354 0.078,256.629 68.649,297.823"/>

Step 3

<!-- Step 3 --> 
<linearGradient id="top_surface_3_gradient" gradientUnits="userSpaceOnUse" x1="248.4543" y1="454.5225" x2="-75.535" y2="459.5381">
    <stop  offset="0" style="stop-color:#FFFFFF"/>
    <stop  offset="0.6687" style="stop-color:#CCCCCC"/>
    <stop  offset="1" style="stop-color:#FFFFFF"/>
</linearGradient>
<polygon id="top_surface_3" fill="url(#top_surface_3_gradient)" points="205.788,415.557 137.215,374.354 0.078,456.629 68.649,497.823"/>

I've spent countless hours developing solutions for this problem and I just couldn't get my head around it. Any help would be greatly appreciated.

Update: gradientTransform not quite

Using the gradientTransform attribute and no x1,y1; x2,y2 coordinates for the gradient, we achieve results that fill the polygon in a way that is almost as needed (This solution can be found here: http://jsfiddle.net/hqXx2/). The only place where the solution breaks is when the polygon is filled with a gradient that starts outside of the polygon and/or ends somewhere outside/inside. Let me illustrate:

This is what is achieved with the solution, that Duopixel is suggesting. step_4

This is the usage case that is impossible to achieve using the solution mentioned above. I changed the colouring in order to visibly amplify the angle and gradient stops. step_5

SVG Code from the example

Here's the code for the larger, correctly expanded group of polygons:

<g>
    <linearGradient id="surface_center_inside_bottom_1_" gradientUnits="userSpaceOnUse" x1="167.7629" y1="634.5986" x2="-72.9039" y2="599.2647">
        <stop  offset="0" style="stop-color:#FFFFFF"/>
        <stop  offset="0.8528" style="stop-color:#CCCCCC"/>
        <stop  offset="0.9954" style="stop-color:#CCCCCC"/>
    </linearGradient>
    <polygon id="surface_center_inside_bottom_9_" fill="url(#surface_center_inside_bottom_1_)" points="137.145,620.04 68.572,578.837 0,620.04 68.572,661.233"/>

    <linearGradient id="surface_right_inside_side_1_" gradientUnits="userSpaceOnUse" x1="178.8889" y1="600.1787" x2="33.103" y2="517.9229">
        <stop  offset="0" style="stop-color:#FFFFFF"/>
        <stop  offset="0.9816" style="stop-color:#A3A5A8"/>
    </linearGradient>
    <polygon id="surface_right_inside_side_3_" fill="url(#surface_right_inside_side_1_)" points="136.526,620.374 68.359,578.501 68.572,493.837 137.358,535.37"/>

    <linearGradient id="surface_right_inside_side_2_" gradientUnits="userSpaceOnUse" x1="126.2664" y1="563.249" x2="-28.4" y2="621.916">
        <stop  offset="0" style="stop-color:#FF0000"/>
        <stop  offset="0.6698" style="stop-color:#00FFFF"/>
        <stop  offset="1" style="stop-color:#FF0000"/>
    </linearGradient>
    <polygon id="surface_right_inside_side_5_" fill="url(#surface_right_inside_side_2_)" points="68.573,661.239 0,620.036 0,535.036 68.573,576.231"/>

    <linearGradient id="surface_center_outside_top_1_" gradientUnits="userSpaceOnUse" x1="167.3728" y1="533.5059" x2="-47.9608" y2="536.8394">
        <stop  offset="0.0016" style="stop-color:#FF0000"/>
        <stop  offset="0.6735" style="stop-color:#00FFFF"/>
        <stop  offset="1" style="stop-color:#FF0000"/>
    </linearGradient>
    <polygon id="surface_center_outside_top_3_" fill="url(#surface_center_outside_top_1_)" points="137.145,535.041 68.572,493.837 0,535.041 68.572,576.233"/>
</g>

And here's the SVG code for the smaller one, which I need to expand:

<g>
    <linearGradient id="surface_right_inside_side_4_" gradientUnits="userSpaceOnUse" x1="273.4377" y1="319.251" x2="78.0696" y2="209.0197">
        <stop  offset="0" style="stop-color:#FFFFFF"/>
        <stop  offset="0.9816" style="stop-color:#A3A5A8"/>
    </linearGradient>
    <polygon id="surface_right_inside_side_9_" fill="url(#surface_right_inside_side_4_)" points="205.112,366.797 136.945,324.924 137.157,156.261 205.731,197.464"/>

    <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="247.2952" y1="408.1992" x2="-103.1108" y2="356.7538">
        <stop  offset="0" style="stop-color:#FFFFFF"/>
        <stop  offset="0.8528" style="stop-color:#CCCCCC"/>
        <stop  offset="0.9954" style="stop-color:#CCCCCC"/>
    </linearGradient>
    <polygon fill="url(#SVGID_1_)" points="205.731,366.465 137.157,325.262 0.021,407.536 68.592,448.729"/>

    <linearGradient id="surface_right_inside_side_7_" gradientUnits="userSpaceOnUse" x1="160.3313" y1="296.623" x2="-52.0119" y2="377.1676">
        <stop  offset="0" style="stop-color:#FF0000"/>
        <stop  offset="0.6698" style="stop-color:#00FFFF"/>
        <stop  offset="1" style="stop-color:#FF0000"/>
    </linearGradient>
    <polygon id="surface_right_inside_side_6_" fill="url(#surface_right_inside_side_7_)" points="68.532,448.767 0,407.497 0.021,238.536 68.592,279.729"/>

    <linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="248.4749" y1="215.7417" x2="-75.5139" y2="220.7572">
        <stop  offset="0.0016" style="stop-color:#FF0000"/>
        <stop  offset="0.6735" style="stop-color:#00FFFF"/>
        <stop  offset="1" style="stop-color:#FF0000"/>
    </linearGradient>
    <polygon fill="url(#SVGID_2_)" points="205.731,197.464 137.157,156.261 68.592,197.333 0.021,238.536 68.592,279.729"/>
</g>

@dignifiedquire take on this problem

I've implemented @dignifiedquire suggested algo in a test site: Here's the test link. I did the absolute to relative conversion on my own and it just shows the same result I would have normally adding the same polygon x and y change values to the gradient x and y. That is the main issue - how to translate those values into such value, that transform the gradient as in my examples above?

More help is needed.

4

2 回答 2

7

更新 3 替代想法

另一种解决方案是根据梯度的两个端点计算百分比值。
在这张图片中,您可以看到原始多边形abcd、它的边界框a'b'c'd'和渐变g1g2。现在的目标是首先计算两个点g1g2绝对值,然后计算这两个点的相对值。

边界框

我推导出了一种算法,它可以完成我所描述的大部分内容,但它未能计算梯度和边界框之间的交集。我对如何解决这个问题有一个想法,但现在不是实施它的时候,所以我列出了相应的步骤。
基本思想是测试梯度是否与通过边界框角的线之一相交(a'b'b'c'c'd'd'a'),然后测试相交是否在所讨论的边缘上。现在有两种特殊情况需要处理。1. 梯度是垂直的,这意味着它的斜率是无穷大 2. 有问题的边是垂直的,这意味着它的斜率是无穷大 所有其他情况都可以用基本数学轻松解决(一条线的两点形式,两条线的交点)。

我的算法

_ = require('underscore')    

function intersectGradientWithPolygon(polygon, gradient){
  var sides = [
    ["a", "b"],
    ["b", "c"],
    ["c", "d"],
    ["d", "a"]
  ];

  var boundingBox = calculateBoundingBox(polygon);

  // intersect each side of the bounding box with the gradient
  var intersections = _.map(sides, function(side){
    var intersection = intersect(boundingBox[side[0]], boundingBox[side[1]], gradient.a, gradient.b);
    if(intersection){
      // calculate the percentages
      console.log(JSON.stringify(intersection));
      return calcPercentage(intersection, boundingBox);
    }
  });

  return intersections;

}


function Point(x,y){
  this.x = x;
  this.y = y;
}

function calcPercentage(intersection, boundingBox){
  var lengthX = (boundingBox.max.x - boundingBox.min.x),
      lengthY = (boundingBox.max.y - boundingBox.min.y),
      x = (intersection.x / lengthX) * 100,
      y = (intersection.y / lengthY) * 100;
}

function calculateBoundingBox(polygon){
  var xValues = _.pluck(polygon, 'x'),
      yValues = _.pluck(polygon, 'y'),
      maxX = _.max(xValues),
      maxY = _.max(yValues),
      minX = _.min(xValues),
      minY = _.min(yValues);


  return {
    "a": new Point(minX, maxY),
    "b": new Point(maxX, maxY),
    "c": new Point(maxX, minY),
    "d": new Point(minX, minY),
    "max": new Point(maxX, maxY),
    "min": new Point(minX, minY)
  };
}
// tests if the two lines a1, b1 and a2, b2 intersect and 
// returns the point of intersection if the do so
function intersect(a1, b1, a2, b2){

  var s = new Point( );

  // TODO
  // This is the part that is missing
  // one needs to implement what I described above at this point
  //

  if (isInIntervall(s.x, a1.x, b1.x) && isInIntervall(s.y, a2.y, b2.y)){
    return s;
  }
  else {
    return false;
  }
}

// test if a point is in the intervall [a,b]
function isInIntervall(point, a, b){
  return (point >= a) && (point <=b);
}

更新 2

问题:如果多边形在空间中作为一个整体移动而不是拉伸,梯度坐标应该如何变化?

答案:您计算在 x 和 y 中移动多边形的一个点的量,并将渐变的点移动完全相同的量。

我现在将算法更改为基于在多边形的一侧按绝对单位数量进行缩放。我还创建了一个图像来解释算法的作用

  1. 原始多边形
  2. 通过输入确定的比例因子缩放多边形
  3. 将多边形移回原来的位置

解释

更新 15.7.2012 我已经根据我提出的使用转换矩阵进行转换的想法推导出了一个算法。我没有时间测试它,但代码在 node.js 下运行,如果您在文档中包含underscore.jssylvester(矩阵操作),则应该在浏览器中运行。

设置

/* underscore for some helper methods
 * http:*http:*underscorejs.org
 */
_ = require("underscore");  

/* matrix operations
 * http:*sylvester.jcoglan.com
 */
require("sylvester");

输入

var gradient = {
  "a":{
    "x": 165.3425,
    "y": 39.7002
  },
  "b":{
    "x": -49.991,
    "y": 43.0337 
  }
};

var polygon = {
  "a": {
    "x": 137.145,
    "y": 41.204
  },
  "b": {
    "x": 68.572,
    "y": 0
  },
  "c": {
    "x": 0,
    "y": 41.204
  },
  "d": {
    "x": 68.572,
    "y": 82.396
  }
};
// the scales are now absolute values in the same units as the coordinates
var scaleAbsX = 100;
var scaleAbsY = 100 * Math.tan( 62/2 * (Math.PI/180) );

// this describes the side that is scaled
var side = ["a", "b"];

算法

scalePolyWithGradient = function(polygon, gradient, scaleAbsX, scaleAbsY, side){
  // 1. Scale by factor: derive factor from input scaling and then translate into scaling matrix
  // 2. Apply scale to the gradient
  // 3. Translate both back 

  // create a scaling matrix based of the input scales

  // get the two points of the scaled side
  var point1 = polygon[side[0]],
      point2 = polygon[side[1]];
  // scale these points
  var scaledPoint1 = { "x": point1.x + scaleAbsX,
                       "y": point1.y + scaleAbsY },
      scaledPoint2 = { "x": point2.x + scaleAbsX,
                       "y": point2.y + scaleAbsY };

  // calculate the relative scales
  var scaleRelX = scaledPoint1.x / point1.x,
      scaleRelY = scaledPoint1.y / point1.y;

  // create the scale matrix
  var scaleMatrix = $M([ [scaleRelX, 0],
                     [0, scaleRelY] ]);


  // scale both the polygon and the gradient
  // we iterate so that the translation is done on every point
  var scale = function(point){
    // convert the point into a matrix
    point = $M([[point.x], 
                [point.y]]);

    // scale
    var newPoint = scaleMatrix.multiply(point);

    return { "x": newPoint.elements[0][0],
             "y": newPoint.elements[1][0]};
  };

  var newPolygon  = {},
      newGradient = {};

  _.each(polygon, function(point, key){
    newPolygon[key] = scale(point);
  });
  _.each(gradient, function(point, key){
    newGradient[key] = scale(point);
  });

  // calculate the translation to move them to the original position
  // and move them back

  // we know that the points to move to their original position are the
  // ones not in the scale side
  var sides = _.keys(polygon),                   // all possible sides
      movePoints = _.difference(sides, side),    // the points not used in the scale
      point = movePoints[0];                     // the first of these points

  // we use these points to create the translation
  var oldPoint = polygon[point],
      newPoint = newPolygon[point];
  var translateMatrix = $M([ [newPoint.x - oldPoint.x],
                             [newPoint.y - oldPoint.y] ]);

  var translate = function(point){
    // convert the point into a matrix
    point = $M([[point.x], 
                [point.y]]);

    // translate
    var newPoint = point.add(translateMatrix);

    return { "x": newPoint.elements[0][0],
             "y": newPoint.elements[1][0]};
  };
  // apply the translation
  _.each(newPolygon, function(point, key){
     newPolygon[key] = translate(point);
   });
   _.each(newGradient, function(point, key){
     newGradient[key] = translate(point);
   });

  // return the new coordinates
  return [newPolygon, newGradient];
};
// apply the algorithm
var newPolygon, newGradient = null;
var result = scalePolyWithGradient(polygon, gradient, scaleAbsX, scaleAbsY, side);
newPolygon = result[0];
newGradient = result[1];

结果

 newPolygon = { "a": {
                   "x": 178.2885,
                   "y":82.405 
                 },
                 "b": {
                   "x": 96.00089999999999,
                   "y": 20.598999999999997
                 },
                 "c": {
                   "x": 13.714500000000001,
                   "y": 82.405
                 },
                 "d": { 
                   "x": 96.00089999999999,
                   "y":144.19299999999998
                 }
               }
 newGradient = { "a": {
                   "x": 212.12550000000005,
                   "y":80.14930000000001
                 },
                 "b": {
                     "x": -46.274699999999996,
                     "y": 85.14955
                 }
               }

旧答案

图片在这里是因为我无法将图片上传到 stackoverflow(声誉很低)

我抽象了多边形的边,所以我们可以专注于它。左图是缩放前。现在我已经绘制了“整体”渐变来显示需要缩放的内容。为了找出所需的坐标,只需按与多边形边相同的比例缩放梯度的平方。

我知道这张图片没有旋转,但这种方法可以扩展为也包含它。

我可以为这些东西推导出算法,但还没有时间这样做。因此,如果这是您想要的,请告诉我,我明天会处理。

于 2012-07-14T19:14:52.723 回答
4

您可以将转换应用于渐变,这意味着您可以执行诸如gradientTransform="rotate(45). 这解决了您的轮换问题。

您应该使用相对单位并将用户空间设置为,objectBoundingBox以便xy值对应于多边形的尺寸。你的 svg 看起来像这样。

<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
  <linearGradient id="top_surface_2_gradient" gradientUnits="objectBoundingBox" x1="0%" y1="0%" x2="100%" y2="100%" gradientTransform="rotate(0 0.5 0.5)">
    <stop  offset="0" style="stop-color:#000"/>
    <stop  offset="1" style="stop-color:#fff"/>
  </linearGradient>
  <polygon id="top_surface_2" fill="url(#top_surface_2_gradient)" points="205.788,215.557 137.215,174.354 0.078,256.629 68.649,297.823"/>
</svg>​

您可以在这里测试它在不同大小的多边形上的工作方式:http: //jsfiddle.net/hqXx2/

于 2012-07-09T18:13:02.547 回答