SvgRadialGradientServer.cs 16.5 KB
Newer Older
James Welle's avatar
James Welle committed
1
2
using System;
using System.Diagnostics;
davescriven's avatar
davescriven committed
3
using System.Drawing;
Eric Domke's avatar
Eric Domke committed
4
using System.Collections.Generic;
davescriven's avatar
davescriven committed
5
using System.Drawing.Drawing2D;
James Welle's avatar
James Welle committed
6
using System.Linq;
davescriven's avatar
davescriven committed
7
8
9

namespace Svg
{
10
    [SvgElement("radialGradient")]
davescriven's avatar
davescriven committed
11
12
13
14
15
    public sealed class SvgRadialGradientServer : SvgGradientServer
    {
        [SvgAttribute("cx")]
        public SvgUnit CenterX
        {
James Welle's avatar
James Welle committed
16
17
18
19
20
21
22
23
            get
            {
                return this.Attributes.GetAttribute<SvgUnit>("cx");
            }
            set
            {
                this.Attributes["cx"] = value;
            }
davescriven's avatar
davescriven committed
24
25
26
27
28
        }

        [SvgAttribute("cy")]
        public SvgUnit CenterY
        {
James Welle's avatar
James Welle committed
29
30
31
32
33
34
35
36
            get
            {
                return this.Attributes.GetAttribute<SvgUnit>("cy");
            }
            set
            {
                this.Attributes["cy"] = value;
            }
davescriven's avatar
davescriven committed
37
38
39
40
41
        }

        [SvgAttribute("r")]
        public SvgUnit Radius
        {
James Welle's avatar
James Welle committed
42
43
44
45
46
47
48
49
            get
            {
                return this.Attributes.GetAttribute<SvgUnit>("r");
            }
            set
            {
                this.Attributes["r"] = value;
            }
davescriven's avatar
davescriven committed
50
51
52
53
54
        }

        [SvgAttribute("fx")]
        public SvgUnit FocalX
        {
55
56
57
58
59
60
61
62
63
64
65
            get
            {
                var value = this.Attributes.GetAttribute<SvgUnit>("fx");

                if (value.IsEmpty || value.IsNone)
                {
                    value = this.CenterX;
                }

                return value;
            }
James Welle's avatar
James Welle committed
66
67
68
69
            set
            {
                this.Attributes["fx"] = value;
            }
davescriven's avatar
davescriven committed
70
71
72
73
74
        }

        [SvgAttribute("fy")]
        public SvgUnit FocalY
        {
75
76
77
78
79
80
81
82
83
84
85
            get
            {
                var value = this.Attributes.GetAttribute<SvgUnit>("fy");

                if (value.IsEmpty || value.IsNone)
                {
                    value = this.CenterY;
                }

                return value;
            }
James Welle's avatar
James Welle committed
86
87
88
89
            set
            {
                this.Attributes["fy"] = value;
            }
davescriven's avatar
davescriven committed
90
91
92
93
        }

        public SvgRadialGradientServer()
        {
James Welle's avatar
James Welle committed
94
95
96
            CenterX = new SvgUnit(SvgUnitType.Percentage, 50F);
            CenterY = new SvgUnit(SvgUnitType.Percentage, 50F);
            Radius = new SvgUnit(SvgUnitType.Percentage, 50F);
davescriven's avatar
davescriven committed
97
98
        }

Eric Domke's avatar
Eric Domke committed
99
100
        private object _lockObj = new Object();

Eric Domke's avatar
Eric Domke committed
101
102
103
104
105
106
107
108
        private SvgUnit NormalizeUnit(SvgUnit orig)
        {
            return (orig.Type == SvgUnitType.Percentage && this.GradientUnits == SvgCoordinateUnits.ObjectBoundingBox ?
                    new SvgUnit(SvgUnitType.User, orig.Value / 100) :
                    orig);
        }

        public override Brush GetBrush(SvgVisualElement renderingElement, ISvgRenderer renderer, float opacity, bool forStroke = false)
davescriven's avatar
davescriven committed
109
        {
110
            LoadStops(renderingElement);
James Welle's avatar
James Welle committed
111

112
            try
113
            {
Eric Domke's avatar
Eric Domke committed
114
                if (this.GradientUnits == SvgCoordinateUnits.ObjectBoundingBox) renderer.SetBoundable(renderingElement);
Eric Domke's avatar
Eric Domke committed
115

Eric Domke's avatar
Eric Domke committed
116
                // Calculate the path and transform it appropriately
Eric Domke's avatar
Eric Domke committed
117
118
119
120
121
                var center = new PointF(NormalizeUnit(CenterX).ToDeviceValue(renderer, UnitRenderingType.Horizontal, this),
                                        NormalizeUnit(CenterY).ToDeviceValue(renderer, UnitRenderingType.Vertical, this));
                var focals = new PointF[] {new PointF(NormalizeUnit(FocalX).ToDeviceValue(renderer, UnitRenderingType.Horizontal, this),
                                                      NormalizeUnit(FocalY).ToDeviceValue(renderer, UnitRenderingType.Vertical, this)) };
                var specifiedRadius = NormalizeUnit(Radius).ToDeviceValue(renderer, UnitRenderingType.Other, this);
Eric Domke's avatar
Eric Domke committed
122
123
                var path = new GraphicsPath();
                path.AddEllipse(
Eric Domke's avatar
Eric Domke committed
124
                    center.X - specifiedRadius, center.Y - specifiedRadius,
Eric Domke's avatar
Eric Domke committed
125
126
                    specifiedRadius * 2, specifiedRadius * 2
                );
Eric Domke's avatar
Eric Domke committed
127
128
129

                using (var transform = EffectiveGradientTransform)
                {
Tebjan Halm's avatar
Tebjan Halm committed
130
                    var bounds = renderer.GetBoundable().Bounds;
Eric Domke's avatar
Eric Domke committed
131
132
133
134
135
136
137
138
                    transform.Translate(bounds.X, bounds.Y, MatrixOrder.Prepend);
                    if (this.GradientUnits == SvgCoordinateUnits.ObjectBoundingBox)
                    {
                        transform.Scale(bounds.Width, bounds.Height, MatrixOrder.Prepend);
                    }
                    path.Transform(transform);
                    transform.TransformPoints(focals);
                }
Eric Domke's avatar
Eric Domke committed
139
140
141


                // Calculate any required scaling
Eric Domke's avatar
Eric Domke committed
142
                var scaleBounds = RectangleF.Inflate(renderingElement.Bounds, renderingElement.StrokeWidth, renderingElement.StrokeWidth);
143
								var scale = CalcScale(scaleBounds, path);
Eric Domke's avatar
Eric Domke committed
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179

                // Not ideal, but this makes sure that the rest of the shape gets properly filled or drawn
                if (scale > 1.0f && SpreadMethod == SvgGradientSpreadMethod.Pad)
                {
                    var stop = Stops.Last();
                    var origColor = stop.GetColor(renderingElement);
                    var renderColor = System.Drawing.Color.FromArgb((int)(opacity * stop.GetOpacity() * 255), origColor);

                    var origClip = renderer.GetClip();
                    try
                    {
                        using (var solidBrush = new SolidBrush(renderColor))
                        {
                            var newClip = origClip.Clone();
                            newClip.Exclude(path);
                            renderer.SetClip(newClip);

                            var renderPath = (GraphicsPath)renderingElement.Path(renderer);
                            if (forStroke)
                            {
                                using (var pen = new Pen(solidBrush, renderingElement.StrokeWidth.ToDeviceValue(renderer, UnitRenderingType.Other, renderingElement)))
                                {
                                    renderer.DrawPath(pen, renderPath);
                                }
                            }
                            else
                            {
                                renderer.FillPath(solidBrush, renderPath);
                            }
                        }
                    }
                    finally
                    {
                        renderer.SetClip(origClip);
                    }
                }
Eric Domke's avatar
Eric Domke committed
180
181
182
183
184
185
186

                // Get the color blend and any tweak to the scaling
                var blend = CalculateColorBlend(renderer, opacity, scale, out scale);

                // Transform the path based on the scaling
                var gradBounds = path.GetBounds();
                var transCenter = new PointF(gradBounds.Left + gradBounds.Width / 2, gradBounds.Top + gradBounds.Height / 2);
Eric Domke's avatar
Eric Domke committed
187
188
189
190
191
192
193
                using (var scaleMat = new Matrix())
                {
                    scaleMat.Translate(-1 * transCenter.X, -1 * transCenter.Y, MatrixOrder.Append);
                    scaleMat.Scale(scale, scale, MatrixOrder.Append);
                    scaleMat.Translate(transCenter.X, transCenter.Y, MatrixOrder.Append);
                    path.Transform(scaleMat);
                }
Eric Domke's avatar
Eric Domke committed
194
195
196

                // calculate the brush
                var brush = new PathGradientBrush(path);
Eric Domke's avatar
Eric Domke committed
197
                brush.CenterPoint = focals[0];
Eric Domke's avatar
Eric Domke committed
198
                brush.InterpolationColors = blend;
199
200
201
202
203
204
205

                return brush;
            }
            finally
            {
                if (this.GradientUnits == SvgCoordinateUnits.ObjectBoundingBox) renderer.PopBoundable();
            }
James Welle's avatar
James Welle committed
206
207
        }

Eric Domke's avatar
Eric Domke committed
208
209
210
211
212
213
214
215
216
217
        /// <summary>
        /// Determine how much (approximately) the path must be scaled to contain the rectangle
        /// </summary>
        /// <param name="bounds">Bounds that the path must contain</param>
        /// <param name="path">Path of the gradient</param>
        /// <returns>Scale factor</returns>
        /// <remarks>
        /// This method continually transforms the rectangle (fewer points) until it is contained by the path
        /// and returns the result of the search.  The scale factor is set to a constant 95%
        /// </remarks>
218
        private float CalcScale(RectangleF bounds, GraphicsPath path, Graphics graphics = null)
James Welle's avatar
James Welle committed
219
        {
Eric Domke's avatar
Eric Domke committed
220
221
222
223
224
225
226
227
            var points = new PointF[] {
                new PointF(bounds.Left, bounds.Top), 
                new PointF(bounds.Right, bounds.Top), 
                new PointF(bounds.Right, bounds.Bottom), 
                new PointF(bounds.Left, bounds.Bottom) 
            };
            var pathBounds = path.GetBounds();
            var pathCenter = new PointF(pathBounds.X + pathBounds.Width / 2, pathBounds.Y + pathBounds.Height / 2);
Eric Domke's avatar
Eric Domke committed
228
            using (var transform = new Matrix())
Eric Domke's avatar
Eric Domke committed
229
            {
Eric Domke's avatar
Eric Domke committed
230
231
232
233
234
235
236
                transform.Translate(-1 * pathCenter.X, -1 * pathCenter.Y, MatrixOrder.Append);
                transform.Scale(.95f, .95f, MatrixOrder.Append);
                transform.Translate(pathCenter.X, pathCenter.Y, MatrixOrder.Append);

                while (!(path.IsVisible(points[0]) && path.IsVisible(points[1]) &&
                         path.IsVisible(points[2]) && path.IsVisible(points[3])))
                {
237
238
239
240
241
242
243
244
										var previousPoints = new PointF[] 
										{
												new PointF(points[0].X, points[0].Y), 
												new PointF(points[1].X, points[1].Y), 
												new PointF(points[2].X, points[2].Y), 
												new PointF(points[3].X, points[3].Y) 
										};

Eric Domke's avatar
Eric Domke committed
245
                    transform.TransformPoints(points);
246
247
248
249
250

										if (Enumerable.SequenceEqual(previousPoints, points))
										{
											break;
										}
Eric Domke's avatar
Eric Domke committed
251
                }
Eric Domke's avatar
Eric Domke committed
252
253
            }
            return bounds.Height / (points[2].Y - points[1].Y);
James Welle's avatar
James Welle committed
254
255
        }

Eric Domke's avatar
Eric Domke committed
256
257
258
259
260
        //New plan:
        // scale the outer rectangle to always encompass ellipse
        // cut the ellipse in half (either vertical or horizontal) 
        // determine the region on each side of the ellipse
        private static IEnumerable<GraphicsPath> GetDifference(RectangleF subject, GraphicsPath clip)
James Welle's avatar
James Welle committed
261
        {
Eric Domke's avatar
Eric Domke committed
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
            var clipFlat = (GraphicsPath)clip.Clone();
            clipFlat.Flatten();
            var clipBounds = clipFlat.GetBounds();
            var bounds = RectangleF.Union(subject, clipBounds);
            bounds.Inflate(bounds.Width * .3f, bounds.Height * 0.3f);

            var clipMidPoint = new PointF((clipBounds.Left + clipBounds.Right) / 2, (clipBounds.Top + clipBounds.Bottom) / 2);
            var leftPoints = new List<PointF>();
            var rightPoints = new List<PointF>();
            foreach (var pt in clipFlat.PathPoints)
            {
                if (pt.X <= clipMidPoint.X)
                {
                    leftPoints.Add(pt);
                }
                else
                {
                    rightPoints.Add(pt);
                }
            }
            leftPoints.Sort((p, q) => p.Y.CompareTo(q.Y));
            rightPoints.Sort((p, q) => p.Y.CompareTo(q.Y));

            var point = new PointF((leftPoints.Last().X + rightPoints.Last().X) / 2,
                                   (leftPoints.Last().Y + rightPoints.Last().Y) / 2);
            leftPoints.Add(point);
            rightPoints.Add(point);
            point = new PointF(point.X, bounds.Bottom);
            leftPoints.Add(point);
            rightPoints.Add(point);

            leftPoints.Add(new PointF(bounds.Left, bounds.Bottom));
            leftPoints.Add(new PointF(bounds.Left, bounds.Top));
            rightPoints.Add(new PointF(bounds.Right, bounds.Bottom));
            rightPoints.Add(new PointF(bounds.Right, bounds.Top));

            point = new PointF((leftPoints.First().X + rightPoints.First().X) / 2, bounds.Top);
            leftPoints.Add(point);
            rightPoints.Add(point);
            point = new PointF(point.X, (leftPoints.First().Y + rightPoints.First().Y) / 2);
            leftPoints.Add(point);
            rightPoints.Add(point);

            var path = new GraphicsPath(FillMode.Winding);
            path.AddPolygon(leftPoints.ToArray());
            yield return path;

            path.Reset();
            path.AddPolygon(rightPoints.ToArray());
            yield return path;
James Welle's avatar
James Welle committed
312
313
314
315
316
        }

        private static GraphicsPath CreateGraphicsPath(PointF origin, PointF centerPoint, float effectiveRadius)
        {
            var path = new GraphicsPath();
317

James Welle's avatar
James Welle committed
318
319
320
321
322
323
            path.AddEllipse(
                origin.X + centerPoint.X - effectiveRadius,
                origin.Y + centerPoint.Y - effectiveRadius,
                effectiveRadius * 2,
                effectiveRadius * 2
            );
324

James Welle's avatar
James Welle committed
325
            return path;
davescriven's avatar
davescriven committed
326
        }
327

Eric Domke's avatar
Eric Domke committed
328
        private ColorBlend CalculateColorBlend(ISvgRenderer renderer, float opacity, float scale, out float outScale)
James Welle's avatar
James Welle committed
329
        {
330
            var colorBlend = GetColorBlend(renderer, opacity, true);
Eric Domke's avatar
Eric Domke committed
331
332
333
            float newScale;
            List<float> pos;
            List<Color> colors;
334

Eric Domke's avatar
Eric Domke committed
335
336
            outScale = scale;
            if (scale > 1)
James Welle's avatar
James Welle committed
337
            {
Eric Domke's avatar
Eric Domke committed
338
339
340
341
                switch (this.SpreadMethod)
                {
                    case SvgGradientSpreadMethod.Reflect:
                        newScale = (float)Math.Ceiling(scale);
Eric Domke's avatar
Eric Domke committed
342
                        pos = (from p in colorBlend.Positions select 1 + (p - 1) / newScale).ToList();
Eric Domke's avatar
Eric Domke committed
343
344
345
346
347
348
                        colors = colorBlend.Colors.ToList();

                        for (var i = 1; i < newScale; i++)
                        {
                            if (i % 2 == 1)
                            {
Eric Domke's avatar
Eric Domke committed
349
350
351
352
353
                                for (int j = 1; j < colorBlend.Positions.Length; j++)
                                {
                                    pos.Insert(0, (newScale - i - 1) / newScale + 1 - colorBlend.Positions[j]);
                                    colors.Insert(0, colorBlend.Colors[j]);
                                }
Eric Domke's avatar
Eric Domke committed
354
355
356
                            }
                            else
                            {
Eric Domke's avatar
Eric Domke committed
357
358
359
360
361
                                for (int j = 0; j < colorBlend.Positions.Length - 1; j++)
                                {
                                    pos.Insert(j, (newScale - i - 1) / newScale + colorBlend.Positions[j]);
                                    colors.Insert(j, colorBlend.Colors[j]);
                                }
Eric Domke's avatar
Eric Domke committed
362
363
364
365
366
367
368
369
370
371
372
373
374
375
                            }
                        }

                        colorBlend.Positions = pos.ToArray();
                        colorBlend.Colors = colors.ToArray();
                        outScale = newScale;
                        break;
                    case SvgGradientSpreadMethod.Repeat:
                        newScale = (float)Math.Ceiling(scale);
                        pos = (from p in colorBlend.Positions select p / newScale).ToList();
                        colors = colorBlend.Colors.ToList();

                        for (var i = 1; i < newScale; i++)
                        {
Eric Domke's avatar
Eric Domke committed
376
                            pos.AddRange(from p in colorBlend.Positions select (i + (p <= 0 ? 0.001f : p)) / newScale);
Eric Domke's avatar
Eric Domke committed
377
378
379
                            colors.AddRange(colorBlend.Colors);
                        }

Eric Domke's avatar
Eric Domke committed
380
381
382
                        colorBlend.Positions = pos.ToArray();
                        colorBlend.Colors = colors.ToArray();
                        outScale = newScale;
Eric Domke's avatar
Eric Domke committed
383
384
                        break;
                    default:
Eric Domke's avatar
Eric Domke committed
385
386
387
388
389
                        outScale = 1.0f;
                        //for (var i = 0; i < colorBlend.Positions.Length - 1; i++)
                        //{
                        //    colorBlend.Positions[i] = 1 - (1 - colorBlend.Positions[i]) / scale;
                        //}
Eric Domke's avatar
Eric Domke committed
390

Eric Domke's avatar
Eric Domke committed
391
392
                        //colorBlend.Positions = new[] { 0F }.Concat(colorBlend.Positions).ToArray();
                        //colorBlend.Colors = new[] { colorBlend.Colors.First() }.Concat(colorBlend.Colors).ToArray();
Eric Domke's avatar
Eric Domke committed
393
394
395

                        break;
                }
James Welle's avatar
James Welle committed
396
            }
397

James Welle's avatar
James Welle committed
398
399
            return colorBlend;
        }
400

James Welle's avatar
James Welle committed
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
        public override SvgElement DeepCopy()
        {
            return DeepCopy<SvgRadialGradientServer>();
        }

        public override SvgElement DeepCopy<T>()
        {
            var newObj = base.DeepCopy<T>() as SvgRadialGradientServer;

            newObj.CenterX = this.CenterX;
            newObj.CenterY = this.CenterY;
            newObj.Radius = this.Radius;
            newObj.FocalX = this.FocalX;
            newObj.FocalY = this.FocalY;

            return newObj;
        }
davescriven's avatar
davescriven committed
418
    }
Eric Domke's avatar
Eric Domke committed
419
}