Butterbur: Handling zigzag lines
[sdk] / butterbur / src / butterbur.ec
1 import "ecere"
2
3 #define glTypeUnsignedShort     0x1403
4 #define glTypeFloat             0x1406
5
6 enum JoinType { miter, round, bevel };
7 enum CapType { butt, round, square };
8
9 define joinType = JoinType::miter;
10 define capType = CapType::round;
11
12 static double pointsArea(Array<Pointf> points)
13 {
14    double area = 0;
15    if(points.count >= 3)
16    {
17       int i;
18       for(i = 0; i < points.count - 1; i++)
19           area += points[i].x * points[i+1].y - points[i+1].x * points[i].y;
20       area += points[i].x * points[0].y - points[0].x * points[i].y;
21    }
22    return area;
23 }
24
25 class ButterburTest : Window
26 {
27    displayDriver = "OpenGL";
28    caption = $"Butterbur's Humble Beginnings";
29    borderStyle = sizable;
30    hasMaximize = true;
31    hasMinimize = true;
32    hasClose = true;
33    clientSize = { 632, 438 };
34
35    BBScene scene { };
36
37    BBRectangle square
38    {
39       scene,
40       box = { 450, 50, 550, 150 },
41       lineColor = { 230, green }, fillColor = { 126, magenta }, cap = capType, join = round, lineWidth = 10
42    };
43    BBRectangle rect
44    {
45       scene,
46       box = { 30, 30, 340, 190 }, rx = 20, ry = 20,
47       lineColor = { 230, red }, fillColor = { 126, black }, cap = capType, join = joinType, lineWidth = 4
48    };
49    BBCircle circle  { scene, lineColor = { 128, green }, fillColor = { 170, tomato }, center = { 390, 280 }, cap = capType, join = joinType, radius = 100, lineWidth = 8 };
50    BBEllipse ell  { scene, lineColor = { 128, yellow }, fillColor = { 100, black }, center = { 250, 310 }, k = 1.3, cap = capType, join = joinType, radius = 100, lineWidth = 4 };
51
52    BBPath triangle
53    {
54       scene, lineColor = { 128, blue }, fillColor = { 100, lime }, lineWidth = 20;
55       //closed = true;
56       join = bevel; //miter; //joinType;
57       cap = capType;
58       nodes.copySrc = [
59          // Pointf { 150, 80 },
60          Pointf { 210, 80 },
61          Pointf { 50, 220 },
62          Pointf { 240, 220 }
63       ];
64    };
65
66    BBPath test
67    {
68       scene, lineColor = red, lineWidth = 1;
69       join = miter;
70       closed = true;
71       cap = butt;
72       nodes.copySrc = [
73          Pointf { 210, 80 },
74          Pointf { 50, 220 },
75          Pointf { 240, 220 }
76       ];
77    };
78    BBPath triangle2
79    {
80       scene, lineColor = { 128, purple }, fillColor = { 100, orange }, lineWidth = 9;
81       closed = true;
82       cap = capType;
83       join = round; //joinType;
84       nodes.copySrc = [
85          Pointf { 510, 180 }, Pointf { 150, 320 }, Pointf { 340, 320 }
86          // Pointf { 340, 320 }, Pointf { 150, 320 }, Pointf { 510, 180 }
87       ];
88    };
89
90    BBPath dot
91    {
92       scene, lineColor = black, lineWidth = 20;
93       cap = square;
94       nodes.copySrc = [ Pointf { 300, 300 } ];
95    };
96
97    BBPath concave
98    {
99       scene, lineColor = { 128, black }, fillColor = { 128, orange }, lineWidth = 18;
100       closed = true;
101       needTesselation = true;
102       cap = round;
103       join = round;
104       nodes.copySrc = [
105
106          Pointf { 350, 80 },
107          Pointf { 210, 180 },
108          Pointf { 250, 120 },
109          Pointf { 140, 220 },
110          Pointf { 100, 80 }
111
112 /*
113          Pointf { 350, 80 },
114          Pointf { 260, 180 },
115          Pointf { 250, 120 },
116          Pointf { 140, 220 },
117          Pointf { 100, 80 }*/
118       ];
119    };
120
121    BBPath concaveOutline
122    {
123       scene, lineColor = blue, lineWidth = 1;
124       //closed = true;
125       needTesselation = true;
126       cap = butt;
127       join = miter;
128       nodes.copySrc = [
129          Pointf { 350, 80 },
130          Pointf { 210, 180 },
131          Pointf { 250, 120 },
132          Pointf { 140, 220 },
133          Pointf { 100, 80 }
134       ];
135    };
136
137    void OnRedraw(Surface surface)
138    {
139       display.antiAlias = true;
140       scene.render();
141    }
142 }
143
144 ButterburTest bbTest {};
145
146 class BBScene
147 {
148    List<BBObject> objects { };
149
150    void render()
151    {
152       for(o : objects)
153       {
154          if(o.needUpdate) { o.update(); o.needUpdate = false; }
155          o.render();
156       }
157    }
158 }
159
160 class BBObject
161 {
162    BBScene parent;
163
164    bool needUpdate;
165
166    needUpdate = true;
167
168    virtual void update();
169    virtual void render();
170
171    public property BBScene parent
172    {
173       set
174       {
175          if(parent)
176             parent.objects.TakeOut(this);
177          parent = value;
178          value.objects.Add(this);
179       }
180       get { return parent; }
181    }
182 }
183
184 class BBPath : BBObject
185 {
186    Array<Pointf> nodes { };
187    GLAB vbo { };
188    GLEAB fillIndices { };
189    GLEAB lineIndices { };
190
191    ColorAlpha lineColor;
192    ColorAlpha fillColor;
193    float lineWidth;
194    JoinType join;
195    CapType cap;
196    bool closed;
197    bool noJoin;
198    bool needTesselation;
199    int lineCount, fillCount;
200    lineColor = black;
201
202    ~BBPath()
203    {
204       fillIndices.free();
205       lineIndices.free();
206       vbo.free();
207    }
208
209    property ColorAlpha lineColor
210    {
211       set { lineColor = value; }
212       get { return lineColor; }
213    };
214
215    property ColorAlpha fillColor
216    {
217       set { fillColor = value; }
218       get { return fillColor; }
219    };
220
221    property float lineWidth
222    {
223       set { lineWidth = value; }
224       get { return lineWidth; }
225    };
226
227    void update()
228    {
229       Pointf * points;
230       uint16 * ix;
231       uint16 * ixFill = null;
232       uint16 i;
233       uint tc = nodes.count;
234       uint ixCount;
235       uint vboCount;
236
237       if(lineWidth > 1 && (tc > 1 || cap != butt))
238       {
239          int rCount = (!noJoin && join == round) ? 7 : (!noJoin && join == bevel) ? 2 : 1;
240          int capCount = (cap == round) ? 7 : 1;
241          uint16 startIX = 0;
242          uint d = 0;
243          bool flip = pointsArea(nodes) > 0;
244
245          vboCount = closed ? (tc * (rCount+1)) : (2*(capCount+1) + ((tc > 2) ? (tc-2) * (rCount+1) : 0));
246          points = new Pointf[vboCount];
247
248          ixCount = closed ? (tc * rCount*2 + closed*2) :
249             (2*(2*capCount) + ((tc > 2) ? (tc-2) * (2*rCount) : 0));
250          ix = new uint16[ixCount];
251          ixFill = new uint16[tc];
252
253          for(i = 0; i < tc + (tc == 1); i++)
254          {
255             bool end = false;
256             uint16 ni;
257             if(i == tc) { i = 0; end = true; }
258
259             #define DOFLIP(x) (flip ? (uint16)((tc-1)-(x)) : (x))
260
261             ni = DOFLIP(i);
262
263             {
264                bool isCap = false;
265                Pointf p = nodes[ni];
266                Pointf before = i > 0 ? nodes[DOFLIP(i-1)] : (closed ? nodes[DOFLIP(tc-1)] : (tc > 1 ? nodes[DOFLIP(1)] : nodes[DOFLIP(0)]));
267                Pointf after  = i < tc-1 ? nodes[DOFLIP(i+1)] : (closed ? nodes[DOFLIP(0)] : (tc > 1 ? nodes[DOFLIP(i-1)] : nodes[DOFLIP(0)]));
268                float ldx = p.x - before.x, ldy = p.y - before.y;
269                float rdx = after.x - p.x, rdy = after.y - p.y;
270                bool thisFlip = false;
271                double at1 = atan2(ldy, ldx);
272                double at2 = atan2(rdy, rdx);
273                float c, s;
274                double diffAngle = at2 - at1;
275                bool simpleMean = true;
276                int n;
277
278                if(!closed && (i == 0 || i == tc-1)) isCap = true;
279
280                if(Abs(diffAngle) >= Pi)
281                {
282                   simpleMean = false;
283
284                   if(diffAngle < 0)
285                      diffAngle += 2*Pi;
286                   else
287                      diffAngle -= 2*Pi;
288                }
289                if(Sgn(diffAngle) > 0)
290                {
291                   // Inside/outside changed (e.g. zig zag patterns)
292                   at1 = atan2(-ldy, -ldx);
293                   at2 = atan2(-rdy, -rdx);
294                   diffAngle = at2 - at1;
295                   simpleMean = true;
296                   thisFlip = true;
297                   if(Abs(diffAngle) >= Pi)
298                   {
299                      simpleMean = false;
300
301                      if(diffAngle < 0)
302                         diffAngle += 2*Pi;
303                      else
304                         diffAngle -= 2*Pi;
305                   }
306                }
307
308                if(isCap)
309                {
310                   // Caps
311                   switch(cap)
312                   {
313                      case round:
314                      {
315                         double r = lineWidth;
316                         int t;
317                         int add = 0;
318                         double angle = (i == 0 && !end) ? at1 : at2;
319                         if(end) angle += Pi;
320                         if(i || end)
321                         {
322                            double ang = angle - Pi/2 + Pi;
323                            c = (float)(cos(ang) * r/2), s = (float)(sin(ang) * r/2);
324                            points[startIX] = { p.x + c, p.y + s };
325                            add = 1;
326                         }
327                         for(t = 0; t < capCount; t++)
328                         {
329                            double ang = angle + (i ? -Pi/2 : Pi/2) - t * Pi/(capCount-1);
330                            c = (float)(cos(ang) * r/2), s = (float)(sin(ang) * r/2);
331                            points[startIX + add + t] = { p.x + c, p.y + s };
332                         }
333                         if(!i && !end)
334                         {
335                            double ang = angle + Pi/2 + Pi;
336                            c = (float)(cos(ang) * r/2), s = (float)(sin(ang) * r/2);
337                            points[startIX + 1 + capCount-1] = { p.x + c, p.y + s };
338                         }
339                         break;
340                      }
341                      case butt:
342                      {
343                         double r = lineWidth;
344                         double angle = (i == 0 && !end) ? at1 : at2;
345                         double ang = angle - Pi/2;
346                         c = (float)(cos(ang) * r/2), s = (float)(sin(ang) * r/2);
347                         points[startIX] = { p.x - c, p.y - s};
348                         points[startIX+1] = { p.x + c, p.y + s };
349                         break;
350                      }
351                      case square:
352                      {
353                         double r = lineWidth * sqrt(2);
354                         double angle = (i == 0 && !end) ? at1 : at2;
355                         double ang = (i || end) ? (angle - Pi/4) + Pi : (angle + Pi/4);
356                         c = (float)(cos(ang) * r/2), s = (float)(sin(ang) * r/2);
357                         points[startIX] = { p.x + c, p.y + s};
358
359                         ang = (i || end) ? (angle + Pi/4) + Pi : (angle - Pi/4);
360                         c = (float)(cos(ang) * r/2), s = (float)(sin(ang) * r/2);
361                         points[startIX+1] = { p.x + c, p.y + s };
362                         break;
363                      }
364                   }
365                }
366                else
367                {
368                   double r = lineWidth / cos(diffAngle/2);
369                   double angle;
370                   bool diffSigns = Sgn(at1) != Sgn(at2);
371                   if(simpleMean)
372                      angle = (double)(at1 + at2) / 2;
373                   else if(diffSigns && ldy > 0 && rdy < 0 && !ldx && !rdx)
374                      angle = Pi;
375                   else if(diffSigns && ldy < 0 && rdy > 0 && !ldx && !rdx)
376                      angle = 0;
377                   else
378                      angle = (double)(at1 + at2) / 2 + Pi;
379
380                   angle += Pi/2;
381
382                   c = (float)(cos(angle) * r/2), s = (float)(sin(angle) * r/2);
383
384                   points[startIX] = { p.x - c, p.y - s };
385                   ixFill[i] = startIX;
386                   if(rCount > 1)
387                   {
388                      int t;
389
390                      p = nodes[ni];
391                      r = lineWidth;
392                      for(t = 0; t < rCount; t++)
393                      {
394                         double ang = at2 - ((rCount-1-t) * diffAngle / (rCount-1)) + Pi/2;
395                         c = (float)(cos(ang) * r/2), s = (float)(sin(ang) * r/2);
396                         points[startIX+1+t] = { p.x + c, p.y + s };
397                      }
398                   }
399                   else
400                      points[startIX+1] = { p.x + c, p.y + s };
401
402                   if(thisFlip)
403                   {
404                      r = lineWidth*1.1;   // TODO: Handle this properly... 1.1 works around not adding an extra vertex
405                      p = nodes[ni];
406                      angle += Pi/2;
407                      c = (float)(cos(angle) * r/2), s = (float)(sin(angle) * r/2);
408                      points[startIX+1] = { p.x - c, p.y - s };
409                   }
410                }
411                for(n = 0; n < (isCap ? capCount : rCount); n++)
412                {
413                   if(thisFlip)
414                   {
415                      ix[d++] = (uint16)(startIX+n+1);
416                      ix[d++] = startIX;
417                   }
418                   else
419                   {
420                      ix[d++] = startIX;
421                      ix[d++] = (uint16)(startIX+n+1);
422                   }
423                }
424                startIX += (uint16)(isCap ? capCount : rCount) + 1;
425
426                if(end) break;
427             }
428          }
429          if(closed)
430          {
431             ix[d++] = 0;
432             ix[d++] = 1;
433          }
434       }
435       else
436       {
437          vboCount = tc;
438          points = nodes.array;
439          ixCount = tc + closed;
440          ix = new uint16[ixCount];
441          ixFill = ix;
442
443          for(i = 0; i < tc; i++)
444             ix[i] = i;
445          if(closed)
446             ix[i] = 0;
447       }
448       vbo.upload(vboCount*sizeof(Pointf), points);
449
450       lineIndices.upload(ixCount * sizeof(uint16), ix);
451       lineCount = ixCount;
452
453       if(points != nodes.array)
454          delete points;
455
456       if(closed)
457       {
458          fillIndices.upload(tc * sizeof(uint16), ixFill);
459          fillCount = tc;
460       }
461
462       if(ixFill != ix)
463          delete ix;
464       delete ixFill;
465    }
466
467    void render()
468    {
469       vbo.use(vertex, 2, glTypeFloat, 0, null);
470
471       // Fill
472       if(closed)
473       {
474          glimtkColor4f(fillColor.color.r/255.0f, fillColor.color.g/255.0f, fillColor.color.b/255.0f, fillColor.a/255.0f);
475          fillIndices.draw(GLIMTKMode::triangleFan, fillCount, glTypeUnsignedShort, null);
476       }
477
478       // Line
479       if(lineWidth)
480       {
481          glimtkColor4f(lineColor.color.r/255.0f, lineColor.color.g/255.0f, lineColor.color.b/255.0f, lineColor.a/255.0f);
482          lineIndices.draw(lineWidth > 1 ? GLIMTKMode::triangleStrip : GLIMTKMode::lineStrip, lineCount, glTypeUnsignedShort, null);
483
484          glimtkColor4f(1, 0, 0, 1);
485          // lineIndices.draw(GLIMTKMode::lineStrip, lineCount, glTypeUnsignedShort, null);
486       }
487    }
488 }
489
490 class BBRectangle : BBPath
491 {
492    float rx, ry;
493
494    closed = true;
495
496    Box box;
497    property Box box
498    {
499       set { box = value; needUpdate = true; }
500       get { value = box; }
501    }
502    property float rx
503    {
504       set { rx = value; needUpdate = true; }
505       get { return rx; }
506    }
507    property float ry
508    {
509       set { ry = value; needUpdate = true; }
510       get { return ry; }
511    }
512
513    void update()
514    {
515       if(!rx && !ry)
516       {
517          noJoin = false;
518          nodes.size = 4;
519          nodes[0] = { box.left, box.top };
520          nodes[1] = { box.left, box.bottom };
521          nodes[2] = { box.right, box.bottom };
522          nodes[3] = { box.right, box.top };
523       }
524       else
525       {
526          int i;
527          float rx = Min(this.rx, box.right - box.left);
528          float ry = Min(this.ry, box.bottom - box.top);
529          int res = 8;
530
531          noJoin = true;
532          nodes.size = 4*res;
533
534          for(i = 0; i < 4; i++)
535          {
536             int t;
537             Degrees angle;
538             Pointf p, o;
539             switch(i)
540             {
541                case 0: p = { box.left, box.top };     angle = 0; break;
542                case 1: p = { box.left, box.bottom };  angle = 270; break;
543                case 2: p = { box.right, box.bottom }; angle = 180; break;
544                case 3: p = { box.right, box.top };    angle = 90; break;
545             }
546             o = { (float)(p.x + cos(angle) * rx - sin(angle) * rx), (float)(p.y + sin(angle) * ry + cos(angle) * ry) };
547
548             for(t = 0; t < res; t++)
549             {
550                nodes[i*res + t] =
551                {
552                   (float)(o.x - rx * cos(angle + (res-1-t) * Degrees { 90 } / (res-1))),
553                   (float)(o.y - ry * sin(angle + (res-1-t) * Degrees { 90 } / (res-1)))
554                };
555             }
556          }
557       }
558
559       BBPath::update();
560       nodes.Free();
561    }
562 }
563
564 class BBCircle : BBPath
565 {
566    closed = true;
567    noJoin = true;
568
569    Pointf center;
570    float radius;
571
572    radius = 10;
573
574    property Pointf center
575    {
576       set { center = value; needUpdate = true; }
577       get { value = center; }
578    }
579
580    property float radius
581    {
582       set { radius = value; needUpdate = true; }
583       get { return radius; }
584    }
585
586    void update()
587    {
588       uint count = radius < 3 ? 6 : radius < 20 ? 12 : radius < 50 ? 30 : 100;
589       int i;
590
591       nodes.size = count;
592       for(i = 0; i < count; i++)
593       {
594          double a = i * 2*Pi / count;
595          nodes[i] = { center.x + (float)cos(a) * radius, center.y + (float)sin(a) * radius };
596       }
597
598       BBPath::update();
599       nodes.Free();
600    }
601 }
602
603 class BBEllipse : BBPath
604 {
605    closed = true;
606    noJoin = true;
607
608    Pointf center;
609    float radius;
610    float k;
611
612    radius = 10;
613    k = 1;
614
615    property Pointf center
616    {
617       set { center = value; needUpdate = true; }
618       get { value = center; }
619    }
620
621    property float radius
622    {
623       set { radius = value; needUpdate = true; }
624       get { return radius; }
625    }
626
627    void update()
628    {
629       uint count = radius < 3 ? 6 : radius < 20 ? 12 : radius < 50 ? 30 : 100;
630       int i;
631
632       nodes.size = count;
633       for(i = 0; i < count; i++)
634       {
635          double a = i * 2*Pi / (count+1);
636          nodes[i] = { center.x + (float)cos(a) * radius * k, center.y + (float)sin(a) * radius };
637       }
638
639       BBPath::update();
640       nodes.Free();
641    }
642 }