SvgRadialGradientServer.cs 16.1 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
130
131
132
133
134
135
136
137
138

                using (var transform = EffectiveGradientTransform)
                {
                    var bounds = renderer.GetBoundable().Bounds;
                    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
143
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
                var scaleBounds = RectangleF.Inflate(renderingElement.Bounds, renderingElement.StrokeWidth, renderingElement.StrokeWidth);
                var scale = CalcScale(scaleBounds, path);

                // 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
218
        /// <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>
        private float CalcScale(RectangleF bounds, GraphicsPath path)
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
237
238
239
                transform.Translate(-1 * pathCenter.X, -1 * pathCenter.Y, MatrixOrder.Append);
                transform.Scale(.95f, .95f, MatrixOrder.Append);
                transform.Translate(pathCenter.X, pathCenter.Y, MatrixOrder.Append);

                var boundsTest = RectangleF.Inflate(bounds, 0, 0);
                while (!(path.IsVisible(points[0]) && path.IsVisible(points[1]) &&
                         path.IsVisible(points[2]) && path.IsVisible(points[3])))
                {
                    transform.TransformPoints(points);
                }
Eric Domke's avatar
Eric Domke committed
240
241
            }
            return bounds.Height / (points[2].Y - points[1].Y);
James Welle's avatar
James Welle committed
242
243
        }

Eric Domke's avatar
Eric Domke committed
244
245
246
247
248
        //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
249
        {
Eric Domke's avatar
Eric Domke committed
250
251
252
253
254
255
256
257
258
259
260
261
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
            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
300
301
302
303
304
        }

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

James Welle's avatar
James Welle committed
306
307
308
309
310
311
            path.AddEllipse(
                origin.X + centerPoint.X - effectiveRadius,
                origin.Y + centerPoint.Y - effectiveRadius,
                effectiveRadius * 2,
                effectiveRadius * 2
            );
312

James Welle's avatar
James Welle committed
313
            return path;
davescriven's avatar
davescriven committed
314
        }
315

Eric Domke's avatar
Eric Domke committed
316
        private ColorBlend CalculateColorBlend(ISvgRenderer renderer, float opacity, float scale, out float outScale)
James Welle's avatar
James Welle committed
317
        {
318
            var colorBlend = GetColorBlend(renderer, opacity, true);
Eric Domke's avatar
Eric Domke committed
319
320
321
            float newScale;
            List<float> pos;
            List<Color> colors;
322

Eric Domke's avatar
Eric Domke committed
323
324
            outScale = scale;
            if (scale > 1)
James Welle's avatar
James Welle committed
325
            {
Eric Domke's avatar
Eric Domke committed
326
327
328
329
                switch (this.SpreadMethod)
                {
                    case SvgGradientSpreadMethod.Reflect:
                        newScale = (float)Math.Ceiling(scale);
Eric Domke's avatar
Eric Domke committed
330
                        pos = (from p in colorBlend.Positions select 1 + (p - 1) / newScale).ToList();
Eric Domke's avatar
Eric Domke committed
331
332
333
334
335
336
                        colors = colorBlend.Colors.ToList();

                        for (var i = 1; i < newScale; i++)
                        {
                            if (i % 2 == 1)
                            {
Eric Domke's avatar
Eric Domke committed
337
338
339
340
341
                                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
342
343
344
                            }
                            else
                            {
Eric Domke's avatar
Eric Domke committed
345
346
347
348
349
                                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
350
351
352
353
354
355
356
357
358
359
360
361
362
363
                            }
                        }

                        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
364
                            pos.AddRange(from p in colorBlend.Positions select (i + (p <= 0 ? 0.001f : p)) / newScale);
Eric Domke's avatar
Eric Domke committed
365
366
367
                            colors.AddRange(colorBlend.Colors);
                        }

Eric Domke's avatar
Eric Domke committed
368
369
370
                        colorBlend.Positions = pos.ToArray();
                        colorBlend.Colors = colors.ToArray();
                        outScale = newScale;
Eric Domke's avatar
Eric Domke committed
371
372
                        break;
                    default:
Eric Domke's avatar
Eric Domke committed
373
374
375
376
377
                        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
378

Eric Domke's avatar
Eric Domke committed
379
380
                        //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
381
382
383

                        break;
                }
James Welle's avatar
James Welle committed
384
            }
385

James Welle's avatar
James Welle committed
386
387
            return colorBlend;
        }
388

James Welle's avatar
James Welle committed
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
        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
406
    }
Eric Domke's avatar
Eric Domke committed
407
}