I need to draw something like a subway map (multiple routes along the same path) using PHP's image library. Here's an example:
*********
******** *
******* * *
* * *
* * *
* * *
* * *
* * *
* * *
* * *
* * *
* * *
* * *
* * ********************
* *********************
**********************
It's easy enough to draw one line along this path. I don't know how to draw multiple lines that follow the path but have an equal amount of space between them.
For a given point A, and more lines through it, for the first points you'll have to decide whether points go 'inside'(B) the track, or 'outside'(C):
********C
D******A *
Q*****B * *
* * *
* E *
Now, you can calculate the offset of your point B to point A as a path from with length=offset (5px for instance) along the angle the which is half the clockwise angle between AE & AD for the 'inside' B (or the clockwise angle from AD to AE for the 'outside' C, or just use a negative offset later on). You'll want point B on a distance of 5px from A along the line through A with an angle angle AE + ((angle AD - angle AE) / 2)
I'm by no means a Math wiz, and the only time I needed to calculate angles like those were in javascript, I'll give it as an example, rewrite to PHP as you please (anybody who does know math, feel free to laugh & correct when needed):
var dx = b.x - a.x;
var dy = b.y - a.y;
if(dx == 0 && dy == 0){
answer = 0;
} else if(dx > 0 && dy >= 0 ){
answer = Math.atan(dy/dx);
} else if(dx <= 0 && dy > 0){
answer = Math.atan(dx/dy) + (Math.PI * 0.5);
} else if(dx <= 0 && dy <= 0){
answer = Math.atan(dy/dx) + Math.PI;
} else if(dx >= 0 && dy <= 0){
answer = Math.atan(dy/dx) + (Math.PI * 1.5);
}
So, in a grid where D=(0,10),A=(10,10), E=(20,20):
The angle through AE = 45° (PI/4 rad),through AD = 180° (PI rad)
The angle through AB is then (45 + ((180-45)/2))=> 112.5° (5/8 PI rad)
5px offset from A=(10,10) through angle 112.5° gives you this location for B:
Bx = Ax + (cos(angle) * 5) = +/- 8.1
By = Ay + (sin(angle) * 5) = +/- 14.6
At the 'sibling' point Q next to starting point D you have no previous path to reference / calculate an angle from, so I'd take the perpendicular: angle DQ = angle DA + 90° (PI/2 rad) (in the example you could just do Dy+5, but maybe you don't always start parallel to one of the 2 axis)
Rinse and repeat for all other points, draw lines between the calculated coordinates.
To complement Wrikken's answer, here's an actual code sample using Objective-C and the cocos2d-iphone engine reconstructed from this thread and others. The atan is not needed, instead the cross product is used, see the C function at the end of the code sample and this link.
I also simply switched the sign of the offset vector from A to B in order to get the vector from A to C. This avoids calling cosf/sinf twice.
PS: This code runs in a for loop from i = 0 to i < numVertices.
CGPoint splinePoint = splinePoints[i];
CGPoint prevPoint = (i == 0) ? splinePoint : splinePoints[i - 1];
CGPoint railPoint = splinePoint;
CGPoint nextPoint = (i == (numVertices-1)) ? splinePoint : splinePoints[i + 1];
CGPoint toPrevPoint = ccpSub(railPoint, prevPoint);
CGPoint toNextPoint = ccpSub(railPoint, nextPoint);
float angleToPrevPoint = ccpAngleSigned(kAngleOriginVector, toPrevPoint);
float angleToNextPoint = ccpAngleSigned(kAngleOriginVector, toNextPoint);
float offsetAngle = 0.0f;
if (i > 0 && i < (numVertices - 1))
{
offsetAngle = angleToNextPoint + ((angleToPrevPoint-angleToNextPoint) / 2);
}
else if (i == 0)
{
offsetAngle = angleToNextPoint + M_PI_2;
}
else
{
offsetAngle = angleToPrevPoint + M_PI_2;
}
CGPoint offsetLeftRail, offsetRightRail, offsetRail;
offsetRail.x = cosf(offsetAngle) * railOffsetFromCenter;
offsetRail.y = sinf(offsetAngle) * railOffsetFromCenter;
offsetLeftRail = ccpAdd(railPoint, offsetRail);
offsetRightRail = ccpAdd(railPoint, ccpMult(offsetRail, -1.0f));
if (isPointToTheLeftOfLine(prevPoint, railPoint, offsetLeftRail))
{
leftRailSplinePoints[i] = offsetLeftRail;
rightRailSplinePoints[i] = offsetRightRail;
}
else
{
leftRailSplinePoints[i] = offsetRightRail;
rightRailSplinePoints[i] = offsetLeftRail;
}
BOOL isPointToTheLeftOfLine(CGPoint start, CGPoint end, CGPoint test)
{
return ((end.x - start.x) * (test.y - start.y) -
(end.y - start.y) * (test.x - start.x)) > 0;
}
This helped me to draw the rails on the railtrack:
Related
How do I calculate the percentage of increase or decrease of two numbers in PHP?
For example: (increase)100, (decrease)1 = -99%
(increase)1, (decrease)100 = +99%
Before anything else you need to have a solid understanding of the meaning of percentages and how they are computed.
The meaning of "x is 15% of y" is:
x = (15 * y) / 100
The arithmetic operations with percentages are similar. If a increases with 12% (of its current value) then:
a = a + (12 * a) / 10
Which is the same as:
a = 112 * a / 100
Subtracting 9% (of its current value) from b is:
b = b - (9 * b) / 100
or
b = b * 91 / 100
which actually is 91% of the value of b (100% - 9% of b).
Turn the above a, b, x, y into PHP variables (by placing $ in front of them), terminate the statements with semicolons (;) and you get valid PHP code that performs percentage operations.
PHP doesn't provide any particular function that helps working with percentages. As you can see above, there is no need for them.
My 2 cents ;)
Using PHP
function pctDiff($x1, $x2) {
$diff = ($x2 - $x1) / $x1;
return round($diff * 100, 2);
}
Usage:
$oldValue = 1000;
$newValue = 203.5;
$diff = pctDiff($oldValue, $newValue);
echo pctDiff($oldValue, $newValue) . '%'; // -79.65%
Using Swift 3
func pctDiff(x1: CGFloat, x2: CGFloat) -> Double {
let diff = (x2 - x1) / x1
return Double(round(100 * (diff * 100)) / 100)
}
let oldValue: CGFloat = 1000
let newValue: CGFloat = 203.5
print("\(pctDiff(x1: oldValue, x2: newValue))%") // -79.65%
We have two points (centers of two circles) and their radius in meters, those radius make the circle. We need to find intersection points. For example we have lat1 = 55.685025, lng1 = 21.118995, r1 = 150 and lat2 = 55.682393, lng2 = 21.121387, r2 = 250. Below you can find our current formula:
// Find a and h.
$a = ($circle_1_r * $circle_1_r - $circle_2_r * $circle_2_r + $distance * $distance) / (2 * $distance);
$h = sqrt($circle_1_r * $circle_1_r - $a * $a);
// Find P2.
$circle_3_x = $circle_1_x + $a * ($circle_2_x - $circle_1_x) / $distance;
$circle_3_y = $circle_1_y + $a * ($circle_2_y - $circle_1_y) / $distance;
// Get the points P3.
$intersection_1 = $this->newLatLngPoint(
($circle_3_x + $h * ($circle_2_y - $circle_1_y) / $distance),
($circle_3_y - $h * ($circle_2_x - $circle_1_x) / $distance)
);
$intersection_2 = $this->newLatLngPoint(
($circle_3_x - $h * ($circle_2_y - $circle_1_y) / $distance),
($circle_3_y + $h * ($circle_2_x - $circle_1_x) / $distance)
);
We find such intersection points (yellow markers), however those locations doesn't match in real world.
Someone, can help to find where the problem is and how to sort it ?
P.S.
Does the altitude (Height above mean sea level) affect the final result? I don't use it but may be should?
I'm looking for a way to combine a RGB color with a grayscale color.
I am looking into this for a gradient generator that uses stored colors and a template (pre-made style properties for most venders) to create a CSS3 gradient.
I am sure their is a simple solution, but I can not seem to find it. I am not looking for anyone to make me a custom function, I just need to know how to make the function.
Your general approach will probably consist of three parts:
Convert the RGB color to HSL.
Keep the hue and saturation, but apply the luminosity of the desired greyscale color.
Convert this HSL triple back to RGB.
Wikipedia has a great page on the HSL colorspace, including conversion formulas. This information was used to create some JavaScript conversion functions by Michael Jackson (yes, I'm serious):
rgbToHsl(r,g,b)
/**
* Converts an RGB color value to HSL. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes r, g, and b are contained in the set [0, 255] and
* returns h, s, and l in the set [0, 1].
*
* #param Number r The red color value
* #param Number g The green color value
* #param Number b The blue color value
* #return Array The HSL representation
*/
function rgbToHsl(r, g, b){
r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if(max == min){
h = s = 0; // achromatic
}else{
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch(max){
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return [h, s, l];
}
hslToRgb(h,s,l)
/**
* Converts an HSL color value to RGB. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes h, s, and l are contained in the set [0, 1] and
* returns r, g, and b in the set [0, 255].
*
* #param Number h The hue
* #param Number s The saturation
* #param Number l The lightness
* #return Array The RGB representation
*/
function hslToRgb(h, s, l){
var r, g, b;
if(s == 0){
r = g = b = l; // achromatic
}else{
function hue2rgb(p, q, t){
if(t < 0) t += 1;
if(t > 1) t -= 1;
if(t < 1/6) return p + (q - p) * 6 * t;
if(t < 1/2) return q;
if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
}
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
return [r * 255, g * 255, b * 255];
}
Since you said you're not looking for anyone to make you a custom function, I trust you'll find little trouble adapting these functions to PHP and leveraging them for your needs. ;)
You can subtract the RGB channels with the brightness of the greyscale colour.
In javascript:
var grey = {r:123,g:123,b:123};
var color = {r:216,g:205,b:120};
var clamp = function(a,b,x){return Math.min(Math.max(a,x),b);}
var mixed = {
r:clamp(0,255,color.r-(255-grey.r)),
g:clamp(0,255,color.g-(255-grey.g)),
b:clamp(0,255,color.b-(255-grey.b))
};// the final combined color.
Hope this can help you.
I m using GD library to create images on the fly.
But when i rotate image using imagerotate() function
it works fine but it gives very much irritating rough edges of image
which is rotated.
as it is shown in this picture.
So how to make these sides/edges of rotated image smooth ?
One way to avoid from getting the Jaggies effect when rotating images is by using another way to sample the pixels than just taking the adjusted pixels, for example to use Nearest-neighbor interpolation to make the edges smoother. You can see matlab code example:
im1 = imread('lena.jpg');imshow(im1);
[m,n,p]=size(im1);
thet = rand(1);
mm = m*sqrt(2);
nn = n*sqrt(2);
for t=1:mm
for s=1:nn
i = uint16((t-mm/2)*cos(thet)+(s-nn/2)*sin(thet)+m/2);
j = uint16(-(t-mm/2)*sin(thet)+(s-nn/2)*cos(thet)+n/2);
if i>0 && j>0 && i<=m && j<=n
im2(t,s,:)=im1(i,j,:);
end
end
end
figure;
imshow(im2);
taken from (here). Basically it means that when sampling the pixels in the original picture, we sample near pixels and interpolate them to get the target pixel value. This way
you can achive waht you want withourt installing any additiional packages.
EDIT
I've found some old code I once wrote in Java, which contains implementations of a couple of sampling algorithems. Here is the code:
Nearest Neighbor sampler:
/**
* #pre (this!=null) && (this.pixels!=null)
* #post returns the sampled pixel of (x,y) by nearest neighbor sampling
*/
private Pixel sampleNearestNeighbor(double x, double y) {
int X = (int) Math.round(x);
int Y = (int) Math.round(y);
if (X >= 0 && Y >= 0 && X < this.pixels.length
&& Y < this.pixels[0].length)
// (X,Y) is within this.pixels' borders
return new Pixel(pixels[X][Y].getRGB());
else
return new Pixel(255, 255, 255);
// sample color will be default white
}
Bilinear sampler:
/**
* #pre (this!=null) && (this.pixels!=null)
* #post returns the sampled pixel of (x,y) by bilinear interpolation
*/
private Pixel sampleBilinear(double x, double y) {
int x1, y1, x2, y2;
x1 = (int) Math.floor(x);
y1 = (int) Math.floor(y);
double weightX = x - x1;
double weightY = y - y1;
if (x1 >= 0 && y1 >= 0 && x1 + 1 < this.pixels.length
&& y1 + 1 < this.pixels[0].length) {
x2 = x1 + 1;
y2 = y1 + 1;
double redAX = (weightX * this.pixels[x2][y1].getRed())
+ (1 - weightX) * this.pixels[x1][y1].getRed();
double greenAX = (weightX * this.pixels[x2][y1].getGreen())
+ (1 - weightX) * this.pixels[x1][y1].getGreen();
double blueAX = (weightX * this.pixels[x2][y1].getBlue())
+ (1 - weightX) * this.pixels[x1][y1].getBlue();
// bilinear interpolation of A point
double redBX = (weightX * this.pixels[x2][y2].getRed())
+ (1 - weightX) * this.pixels[x1][y2].getRed();
double greenBX = (weightX * this.pixels[x2][y2].getGreen())
+ (1 - weightX) * this.pixels[x1][y2].getGreen();
double blueBX = (weightX * this.pixels[x2][y2].getBlue())
+ (1 - weightX) * this.pixels[x1][y2].getBlue();
// bilinear interpolation of B point
int red = (int) (weightY * redBX + (1 - weightY) * redAX);
int green = (int) (weightY * greenBX + (1 - weightY) * greenAX);
int blue = (int) (weightY * blueBX + (1 - weightY) * blueAX);
// bilinear interpolation of A and B
return new Pixel(red, green, blue);
} else if (x1 >= 0
&& y1 >= 0 // last row or column
&& (x1 == this.pixels.length - 1 || y1 == this.pixels[0].length - 1)) {
return new Pixel(this.pixels[x1][y1].getRed(), this.pixels[x1][y1]
.getGreen(), this.pixels[x1][y1].getBlue());
} else
return new Pixel(255, 255, 255);
// sample color will be default white
}
Gaussian sampler:
/**
* #pre (this!=null) && (this.pixels!=null)
* #post returns the sampled pixel of (x,y) by gaussian function
*/
private Pixel sampleGaussian(double u, double v) {
double w = 3; // sampling distance
double sqrSigma = Math.pow(w / 3.0, 2); // sigma^2
double normal = 0;
double red = 0, green = 0, blue = 0;
double minIX = Math.round(u - w);
double maxIX = Math.round(u + w);
double minIY = Math.round(v - w);
double maxIY = Math.round(v + w);
for (int ix = (int) minIX; ix <= maxIX; ix++) {
for (int iy = (int) minIY; iy <= maxIY; iy++) {
double sqrD = Math.pow(ix - u, 2) + Math.pow(iy - v, 2);
// squared distance between (ix,iy) and (u,v)
if (sqrD < Math.pow(w, 2) && ix >= 0 && iy >= 0
&& ix < pixels.length && iy < pixels[0].length) {
// gaussian function
double gaussianWeight = Math.pow(2, -1 * (sqrD / sqrSigma));
normal += gaussianWeight;
red += gaussianWeight * pixels[ix][iy].getRed();
green += gaussianWeight * pixels[ix][iy].getGreen();
blue += gaussianWeight * pixels[ix][iy].getBlue();
}
}
}
red /= normal;
green /= normal;
blue /= normal;
return new Pixel(red, green, blue);
}
Actual rotate:
/**
* #pre (this!=null) && (this.pixels!=null) && (1 <= samplingMethod <= 3)
* #post creates a new rotated-by-degrees Image and returns it
*/
public myImage rotate(double degrees, int samplingMethod) {
myImage outputImg = null;
int t = 0;
for (; degrees < 0 || degrees >= 180; degrees += (degrees < 0) ? 180
: -180)
t++;
int w = this.pixels.length;
int h = this.pixels[0].length;
double cosinus = Math.cos(Math.toRadians(degrees));
double sinus = Math.sin(Math.toRadians(degrees));
int width = Math.round((float) (w * Math.abs(cosinus) + h * sinus));
int height = Math.round((float) (h * Math.abs(cosinus) + w * sinus));
w--;
h--; // move from (1,..,k) to (0,..,1-k)
Pixel[][] pixelsArray = new Pixel[width][height];
double x = 0; // x coordinate in the source image
double y = 0; // y coordinate in the source image
if (degrees >= 90) { // // 270 or 90 degrees turn
double temp = cosinus;
cosinus = sinus;
sinus = -temp;
}
for (int i = 0; i < width; i++) {
for (int j = 0; j < height; j++) {
double x0 = i;
double y0 = j;
if (degrees >= 90) {
if ((t % 2 == 1)) { // 270 degrees turn
x0 = j;
y0 = width - i - 1;
} else { // 90 degrees turn
x0 = height - j - 1;
y0 = i;
}
} else if (t % 2 == 1) { // 180 degrees turn
x0 = width - x0 - 1;
y0 = height - y0 - 1;
}
// calculate new x/y coordinates and
// adjust their locations to the middle of the picture
x = x0 * cosinus - (y0 - sinus * w) * sinus;
y = x0 * sinus + (y0 - sinus * w) * cosinus;
if (x < -0.5 || x > w + 0.5 || y < -0.5 || y > h + 0.5)
// the pixels that does not have a source will be painted in
// default white
pixelsArray[i][j] = new Pixel(255, 255, 255);
else {
if (samplingMethod == 1)
pixelsArray[i][j] = sampleNearestNeighbor(x, y);
else if (samplingMethod == 2)
pixelsArray[i][j] = sampleBilinear(x, y);
else if (samplingMethod == 3)
pixelsArray[i][j] = sampleGaussian(x, y);
}
}
outputImg = new myImage(pixelsArray);
}
return outputImg;
}
It may sound rather hack-ish, but it is the simplest way to do it, and even big enterprise solutions use this.
The trick is to first create the image 2X the size of what you need, then do all the drawing calls and then resize it to required original size.
Not only it is really easy to do, but also it is as fast as it gets and it produces very nice results. I use this trick for all cases when I need to apply blur to edges.
Another advantate to this is that it does not include blur on the rest of the image and it remains crisp clear - only borders of rotated image gets smoothed.
One thing you could try is to use imageantialias() to smooth the edges.
If that doesn't suit your needs, GD itself will probably not suffice.
GD uses very fast methods for all it's abilites without any actual smoothing or anything like that involved. If you want some proper image editing, you could either look into ImageMagick (which requires extra-software on the server) or write your own functions based on GD.
Keep in mind though, that php is really slow with huge amounts of data, so writing your own functions might be disappointing. (From my experience, PHP is roughly 40 times slower than compiled code.)
I recommend using ImageMagick for any image work where result quality matters.
I'm playing with GD library for a while and more particuraly with Bezier curves atm.
I used some existant class which I modified a little (seriously eval()...). I found out it was a generic algorithm used in and convert for GD.
Now I want to take it to another level: I want some colors.
No problem for line color but with fill color it's harder.
My question is:
Is there any existant algorithm for that? I mean mathematical algorithm or any language doing it already so that I could transfer it to PHP + GD?
EDIT2
So, I tried #MizardX solution with a harder curve :
1st position : 50 - 50
final position : 50 - 200
1st control point : 300 - 225
2nd control point : 300 - 25
Which should show this :
And gives this :
EDIT
I already read about #MizardX solution. Using imagefilledpolygon to make it works.
But it doesn't work as expected. See the image below to see the problem.
Top graph is what I expect (w/o the blackline for now, only the red part).
Coordinates used:
first point is 100 - 100
final point is 300 - 100
first control point is 100 - 0
final control point is 300 - 200
Bottom part is what I get with that kind of algorithm...
Convert the Bezier curve to a polyline/polygon, and fill that. If you evaluate the Bezier polynomial at close enough intervals (~1 pixel) it will be identical to an ideal Bezier curve.
I don't know how familiar you are with Bezier curves, but here is a crash course:
<?php
// Calculate the coordinate of the Bezier curve at $t = 0..1
function Bezier_eval($p1,$p2,$p3,$p4,$t) {
// lines between successive pairs of points (degree 1)
$q1 = array((1-$t) * $p1[0] + $t * $p2[0],(1-$t) * $p1[1] + $t * $p2[1]);
$q2 = array((1-$t) * $p2[0] + $t * $p3[0],(1-$t) * $p2[1] + $t * $p3[1]);
$q3 = array((1-$t) * $p3[0] + $t * $p4[0],(1-$t) * $p3[1] + $t * $p4[1]);
// curves between successive pairs of lines. (degree 2)
$r1 = array((1-$t) * $q1[0] + $t * $q2[0],(1-$t) * $q1[1] + $t * $q2[1]);
$r2 = array((1-$t) * $q2[0] + $t * $q3[0],(1-$t) * $q2[1] + $t * $q3[1]);
// final curve between the two 2-degree curves. (degree 3)
return array((1-$t) * $r1[0] + $t * $r2[0],(1-$t) * $r1[1] + $t * $r2[1]);
}
// Calculate the squared distance between two points
function Point_distance2($p1,$p2) {
$dx = $p2[0] - $p1[0];
$dy = $p2[1] - $p1[1];
return $dx * $dx + $dy * $dy;
}
// Convert the curve to a polyline
function Bezier_convert($p1,$p2,$p3,$p4,$tolerance) {
$t1 = 0.0;
$prev = $p1;
$t2 = 0.1;
$tol2 = $tolerance * $tolerance;
$result []= $prev[0];
$result []= $prev[1];
while ($t1 < 1.0) {
if ($t2 > 1.0) {
$t2 = 1.0;
}
$next = Bezier_eval($p1,$p2,$p3,$p4,$t2);
$dist = Point_distance2($prev,$next);
while ($dist > $tol2) {
// Halve the distance until small enough
$t2 = $t1 + ($t2 - $t1) * 0.5;
$next = Bezier_eval($p1,$p2,$p3,$p4,$t2);
$dist = Point_distance2($prev,$next);
}
// the image*polygon functions expect a flattened array of coordiantes
$result []= $next[0];
$result []= $next[1];
$t1 = $t2;
$prev = $next;
$t2 = $t1 + 0.1;
}
return $result;
}
// Draw a Bezier curve on an image
function Bezier_drawfilled($image,$p1,$p2,$p3,$p4,$color) {
$polygon = Bezier_convert($p1,$p2,$p3,$p4,1.0);
imagefilledpolygon($image,$polygon,count($polygon)/2,$color);
}
?>
Edit:
I forgot to test the routine. It is indeed as you said; It doesn't give a correct result. Now I have fixed two bugs:
I unintentionally re-used the variable names $p1 and $p2. I renamed them $prev and $next.
Wrong sign in the while-loop. Now it loops until the distance is small enough, instead of big enough.
I checked the algorithm for generating a Polygon ensuring a bounded distance between successive parameter-generated points, and seems to work well for all the curves I tested.
Code in Mathematica:
pts={{50,50},{300,225},{300,25},{50,200}};
f=BezierFunction[pts];
step=.1; (*initial step*)
While[ (*get the final step - Points no more than .01 appart*)
Max[
EuclideanDistance ###
Partition[Table[f[t],{t,0,1,step}],2,1]] > .01,
step=step/2]
(*plot it*)
Graphics#Polygon#Table[f[t],{t,0,1,step}]
.
.
The algorithm could be optimized (ie. generate less points) if you don't require the same parameter increment between points, meaning you can chose a parameter increment at each point that ensures a bounded distance to the next.
Random examples:
Generate a list of successive points which lie along the curve (p_list)).
You create a line between the two end points of the curve (l1).
Then you are going to find the normal of the line (n1). Using this normal find the distance between the two furthest points (p_max1, and p_max2) along this normal (d1). Divide this distance into n discrete units (delta).
Now shift l1 along n1 by delta, and solve for the points of intersection (start with brute force and check for a solution between all the line segments in p_list). You should be able to get two points of intersection for each shift of l1, excepting boundaries and self intersection where you may have only have a single point. Hopefully the quad routine can have two points of the quad be at the same location (a triangle) and fill without complaint otherwise you'll need triangles in this case.
Sorry I didn't provide pseudo code but the idea is pretty simple. It's just like taking the two end points and joining them with a ruler and then keeping that ruler parallel to the original line start at one end and with successive very close pencil marks fill in the whole figure. You'll see that when you create your little pencil mark (a fine rectangle) that the rectangle it highly unlikely to use the points on the curve. Even if you force it to use a point on one side of the curve it would be quite the coincidence for it to exactly match a point on the other side, for this reason it is better to just calculate new points. At the time of calculating new points it would probably be a good idea to regenerate the curves p_list in terms of these points so you can fill it more quickly (if the curve is to stay static of course otherwise it wouldn't make any sense).
This answer is very similar to #MizardX's, but uses a different method to find suitable points along the Bezier for a polygonal approximation.
function split_cubic($p, $t)
{
$a_x = $p[0] + ($t * ($p[2] - $p[0]));
$a_y = $p[1] + ($t * ($p[3] - $p[1]));
$b_x = $p[2] + ($t * ($p[4] - $p[2]));
$b_y = $p[3] + ($t * ($p[5] - $p[3]));
$c_x = $p[4] + ($t * ($p[6] - $p[4]));
$c_y = $p[5] + ($t * ($p[7] - $p[5]));
$d_x = $a_x + ($t * ($b_x - $a_x));
$d_y = $a_y + ($t * ($b_y - $a_y));
$e_x = $b_x + ($t * ($c_x - $b_x));
$e_y = $b_y + ($t * ($c_y - $b_y));
$f_x = $d_x + ($t * ($e_x - $d_x));
$f_y = $d_y + ($t * ($e_y - $d_y));
return array(
array($p[0], $p[1], $a_x, $a_y, $d_x, $d_y, $f_x, $f_y),
array($f_x, $f_y, $e_x, $e_y, $c_x, $c_y, $p[6], $p[7]));
}
$flatness_sq = 0.25; /* flatness = 0.5 */
function cubic_ok($p)
{
global $flatness_sq;
/* test is essentially:
* perpendicular distance of control points from line < flatness */
$a_x = $p[6] - $p[0]; $a_y = $p[7] - $p[1];
$b_x = $p[2] - $p[0]; $b_y = $p[3] - $p[1];
$c_x = $p[4] - $p[6]; $c_y = $p[5] - $p[7];
$a_cross_b = ($a_x * $b_y) - ($a_y * $b_x);
$a_cross_c = ($a_x * $c_y) - ($a_y * $c_x);
$d_sq = ($a_x * $a_x) + ($a_y * $a_y);
return max($a_cross_b * $a_cross_b, $a_cross_c * $a_cross_c) < ($flatness_sq * $d_sq);
}
$max_level = 8;
function subdivide_cubic($p, $level)
{
global $max_level;
if (($level == $max_level) || cubic_ok($p)) {
return array();
}
list($q, $r) = split_cubic($p, 0.5);
$v = subdivide_cubic($q, $level + 1);
$v[] = $r[0]; /* add a point where we split the cubic */
$v[] = $r[1];
$v = array_merge($v, subdivide_cubic($r, $level + 1));
return $v;
}
function get_cubic_points($p)
{
$v[] = $p[0];
$v[] = $p[1];
$v = array_merge($v, subdivide_cubic($p, 0));
$v[] = $p[6];
$v[] = $p[7];
return $v;
}
function imagefilledcubic($img, $p, $color)
{
$v = get_cubic_points($p);
imagefilledpolygon($img, $v, count($v) / 2, $color);
}
The basic idea is to recursively split the cubic in half until the bits we're left with are almost flat. Everywhere we split the cubic, we stick a polygon point.
split_cubic splits the cubic in two at parameter $t. cubic_ok is the "are we flat enough?" test. subdivide_cubic is the recursive function. Note that we stick a limit on the recursion depth to avoid nasty cases really screwing us up.
Your self-intersecting test case:
$img = imagecreatetruecolor(256, 256);
imagefilledcubic($img, array(
50.0, 50.0, /* first point */
300.0, 225.0, /* first control point */
300.0, 25.0, /* second control point */
50.0, 200.0), /* last point */
imagecolorallocate($img, 255, 255, 255));
imagepng($img, 'out.png');
imagedestroy($img);
Gives this output:
I can't figure out how to make PHP nicely anti-alias this; imageantialias($img, TRUE); didn't seem to work.