1# -*- coding: utf-8 -*-
2# cython: language_level=3
3
4"""
5Drawing Graphics
6"""
7
8
9from math import floor, ceil, log10, sin, cos, pi, sqrt, atan2, degrees, radians, exp
10import json
11import base64
12from itertools import chain
13
14from mathics.version import __version__  # noqa used in loading to check consistency.
15from mathics.builtin.base import (
16    Builtin,
17    InstanceableBuiltin,
18    BoxConstruct,
19    BoxConstructError,
20)
21from mathics.builtin.options import options_to_rules
22from mathics.core.expression import (
23    Expression,
24    Integer,
25    Rational,
26    Real,
27    String,
28    Symbol,
29    SymbolList,
30    SymbolN,
31    SymbolMakeBoxes,
32    system_symbols,
33    system_symbols_dict,
34    from_python,
35)
36from mathics.builtin.drawing.colors import convert as convert_color
37from mathics.core.numbers import machine_epsilon
38
39GRAPHICS_OPTIONS = {
40    "AspectRatio": "Automatic",
41    "Axes": "False",
42    "AxesStyle": "{}",
43    "Background": "Automatic",
44    "ImageSize": "Automatic",
45    "LabelStyle": "{}",
46    "PlotRange": "Automatic",
47    "PlotRangePadding": "Automatic",
48    "TicksStyle": "{}",
49    "$OptionSyntax": "Ignore",
50}
51
52
53class CoordinatesError(BoxConstructError):
54    pass
55
56
57class ColorError(BoxConstructError):
58    pass
59
60
61def get_class(name):
62    from mathics.builtin.drawing.graphics3d import GLOBALS3D
63
64    c = GLOBALS.get(name)
65    if c is None:
66        return GLOBALS3D.get(name)
67    else:
68        return c
69
70    # globals() does not work with Cython, otherwise one could use something
71    # like return globals().get(name)
72
73
74def coords(value):
75    if value.has_form("List", 2):
76        x, y = value.leaves[0].round_to_float(), value.leaves[1].round_to_float()
77        if x is None or y is None:
78            raise CoordinatesError
79        return (x, y)
80    raise CoordinatesError
81
82
83class Coords(object):
84    def __init__(self, graphics, expr=None, pos=None, d=None):
85        self.graphics = graphics
86        self.p = pos
87        self.d = d
88        if expr is not None:
89            if expr.has_form("Offset", 1, 2):
90                self.d = coords(expr.leaves[0])
91                if len(expr.leaves) > 1:
92                    self.p = coords(expr.leaves[1])
93                else:
94                    self.p = None
95            else:
96                self.p = coords(expr)
97
98    def pos(self):
99        p = self.graphics.translate(self.p)
100        p = (cut(p[0]), cut(p[1]))
101        if self.d is not None:
102            d = self.graphics.translate_absolute(self.d)
103            return (p[0] + d[0], p[1] + d[1])
104        return p
105
106    def add(self, x, y):
107        p = (self.p[0] + x, self.p[1] + y)
108        return Coords(self.graphics, pos=p, d=self.d)
109
110
111def cut(value):
112    "Cut values in graphics primitives (not displayed otherwise in SVG)"
113    border = 10 ** 8
114    if value < -border:
115        value = -border
116    elif value > border:
117        value = border
118    return value
119
120
121def create_css(
122    edge_color=None, face_color=None, stroke_width=None, font_color=None, opacity=1.0
123):
124    css = []
125    if edge_color is not None:
126        color, stroke_opacity = edge_color.to_css()
127        css.append("stroke: %s" % color)
128        css.append("stroke-opacity: %s" % stroke_opacity)
129    else:
130        css.append("stroke: none")
131    if stroke_width is not None:
132        css.append("stroke-width: %fpx" % stroke_width)
133    if face_color is not None:
134        color, fill_opacity = face_color.to_css()
135        css.append("fill: %s" % color)
136        css.append("fill-opacity: %s" % fill_opacity)
137    else:
138        css.append("fill: none")
139    if font_color is not None:
140        color, _ = font_color.to_css()
141        css.append("color: %s" % color)
142    css.append("opacity: %s" % opacity)
143    return "; ".join(css)
144
145
146def asy_number(value):
147    return "%.5g" % value
148
149
150def _to_float(x):
151    x = x.round_to_float()
152    if x is None:
153        raise BoxConstructError
154    return x
155
156
157def create_pens(
158    edge_color=None, face_color=None, stroke_width=None, is_face_element=False
159):
160    result = []
161    if face_color is not None:
162        brush, opacity = face_color.to_asy()
163        if opacity != 1:
164            brush += "+opacity(%s)" % asy_number(opacity)
165        result.append(brush)
166    elif is_face_element:
167        result.append("nullpen")
168    if edge_color is not None:
169        pen, opacity = edge_color.to_asy()
170        if opacity != 1:
171            pen += "+opacity(%s)" % asy_number(opacity)
172        if stroke_width is not None:
173            pen += "+linewidth(%s)" % asy_number(stroke_width)
174        result.append(pen)
175    elif is_face_element:
176        result.append("nullpen")
177    return ", ".join(result)
178
179
180def _data_and_options(leaves, defined_options):
181    data = []
182    options = defined_options.copy()
183    for leaf in leaves:
184        if leaf.get_head_name() == "System`Rule":
185            if len(leaf.leaves) != 2:
186                raise BoxConstructError
187            name, value = leaf.leaves
188            name_head = name.get_head_name()
189            if name_head == "System`Symbol":
190                py_name = name.get_name()
191            elif name_head == "System`String":
192                py_name = "System`" + name.get_string_value()
193            else:  # unsupported name type
194                raise BoxConstructError
195            options[py_name] = value
196        else:
197            data.append(leaf)
198    return data, options
199
200
201def _euclidean_distance(a, b):
202    return sqrt(sum((x1 - x2) * (x1 - x2) for x1, x2 in zip(a, b)))
203
204
205def _component_distance(a, b, i):
206    return abs(a[i] - b[i])
207
208
209def _cie2000_distance(lab1, lab2):
210    # reference: https://en.wikipedia.org/wiki/Color_difference#CIEDE2000
211    e = machine_epsilon
212    kL = kC = kH = 1  # common values
213
214    L1, L2 = lab1[0], lab2[0]
215    a1, a2 = lab1[1], lab2[1]
216    b1, b2 = lab1[2], lab2[2]
217
218    dL = L2 - L1
219    Lm = (L1 + L2) / 2
220    C1 = sqrt(a1 ** 2 + b1 ** 2)
221    C2 = sqrt(a2 ** 2 + b2 ** 2)
222    Cm = (C1 + C2) / 2
223
224    a1 = a1 * (1 + (1 - sqrt(Cm ** 7 / (Cm ** 7 + 25 ** 7))) / 2)
225    a2 = a2 * (1 + (1 - sqrt(Cm ** 7 / (Cm ** 7 + 25 ** 7))) / 2)
226
227    C1 = sqrt(a1 ** 2 + b1 ** 2)
228    C2 = sqrt(a2 ** 2 + b2 ** 2)
229    Cm = (C1 + C2) / 2
230    dC = C2 - C1
231
232    h1 = (180 * atan2(b1, a1 + e)) / pi % 360
233    h2 = (180 * atan2(b2, a2 + e)) / pi % 360
234    if abs(h2 - h1) <= 180:
235        dh = h2 - h1
236    elif abs(h2 - h1) > 180 and h2 <= h1:
237        dh = h2 - h1 + 360
238    elif abs(h2 - h1) > 180 and h2 > h1:
239        dh = h2 - h1 - 360
240
241    dH = 2 * sqrt(C1 * C2) * sin(radians(dh) / 2)
242
243    Hm = (h1 + h2) / 2 if abs(h2 - h1) <= 180 else (h1 + h2 + 360) / 2
244    T = (
245        1
246        - 0.17 * cos(radians(Hm - 30))
247        + 0.24 * cos(radians(2 * Hm))
248        + 0.32 * cos(radians(3 * Hm + 6))
249        - 0.2 * cos(radians(4 * Hm - 63))
250    )
251
252    SL = 1 + (0.015 * (Lm - 50) ** 2) / sqrt(20 + (Lm - 50) ** 2)
253    SC = 1 + 0.045 * Cm
254    SH = 1 + 0.015 * Cm * T
255
256    rT = (
257        -2
258        * sqrt(Cm ** 7 / (Cm ** 7 + 25 ** 7))
259        * sin(radians(60 * exp(-((Hm - 275) ** 2 / 25 ** 2))))
260    )
261    return sqrt(
262        (dL / (SL * kL)) ** 2
263        + (dC / (SC * kC)) ** 2
264        + (dH / (SH * kH)) ** 2
265        + rT * (dC / (SC * kC)) * (dH / (SH * kH))
266    )
267
268
269def _CMC_distance(lab1, lab2, l, c):
270    # reference https://en.wikipedia.org/wiki/Color_difference#CMC_l:c_.281984.29
271    L1, L2 = lab1[0], lab2[0]
272    a1, a2 = lab1[1], lab2[1]
273    b1, b2 = lab1[2], lab2[2]
274
275    dL, da, db = L2 - L1, a2 - a1, b2 - b1
276    e = machine_epsilon
277
278    C1 = sqrt(a1 ** 2 + b1 ** 2)
279    C2 = sqrt(a2 ** 2 + b2 ** 2)
280
281    h1 = (180 * atan2(b1, a1 + e)) / pi % 360
282    dC = C2 - C1
283    dH2 = da ** 2 + db ** 2 - dC ** 2
284    F = C1 ** 2 / sqrt(C1 ** 4 + 1900)
285    T = (
286        0.56 + abs(0.2 * cos(radians(h1 + 168)))
287        if (164 <= h1 and h1 <= 345)
288        else 0.36 + abs(0.4 * cos(radians(h1 + 35)))
289    )
290
291    SL = 0.511 if L1 < 16 else (0.040975 * L1) / (1 + 0.01765 * L1)
292    SC = (0.0638 * C1) / (1 + 0.0131 * C1) + 0.638
293    SH = SC * (F * T + 1 - F)
294    return sqrt((dL / (l * SL)) ** 2 + (dC / (c * SC)) ** 2 + dH2 / SH ** 2)
295
296
297def _extract_graphics(graphics, format, evaluation):
298    graphics_box = Expression(SymbolMakeBoxes, graphics).evaluate(evaluation)
299    # builtin = GraphicsBox(expression=False)
300    elements, calc_dimensions = graphics_box._prepare_elements(
301        graphics_box.leaves, {"evaluation": evaluation}, neg_y=True
302    )
303    xmin, xmax, ymin, ymax, _, _, _, _ = calc_dimensions()
304
305    # xmin, xmax have always been moved to 0 here. the untransformed
306    # and unscaled bounds are found in elements.xmin, elements.ymin,
307    # elements.extent_width, elements.extent_height.
308
309    # now compute the position of origin (0, 0) in the transformed
310    # coordinate space.
311
312    ex = elements.extent_width
313    ey = elements.extent_height
314
315    sx = (xmax - xmin) / ex
316    sy = (ymax - ymin) / ey
317
318    ox = -elements.xmin * sx + xmin
319    oy = -elements.ymin * sy + ymin
320
321    # generate code for svg or asy.
322
323    if format == "asy":
324        code = "\n".join(element.to_asy() for element in elements.elements)
325    elif format == "svg":
326        code = elements.to_svg()
327    else:
328        raise NotImplementedError
329
330    return xmin, xmax, ymin, ymax, ox, oy, ex, ey, code
331
332
333class _SVGTransform:
334    def __init__(self):
335        self.transforms = []
336
337    def matrix(self, a, b, c, d, e, f):
338        # a c e
339        # b d f
340        # 0 0 1
341        self.transforms.append("matrix(%f, %f, %f, %f, %f, %f)" % (a, b, c, d, e, f))
342
343    def translate(self, x, y):
344        self.transforms.append("translate(%f, %f)" % (x, y))
345
346    def scale(self, x, y):
347        self.transforms.append("scale(%f, %f)" % (x, y))
348
349    def rotate(self, x):
350        self.transforms.append("rotate(%f)" % x)
351
352    def apply(self, svg):
353        return '<g transform="%s">%s</g>' % (" ".join(self.transforms), svg)
354
355
356class _ASYTransform:
357    _template = """
358    add(%s * (new picture() {
359        picture saved = currentpicture;
360        picture transformed = new picture;
361        currentpicture = transformed;
362        %s
363        currentpicture = saved;
364        return transformed;
365    })());
366    """
367
368    def __init__(self):
369        self.transforms = []
370
371    def matrix(self, a, b, c, d, e, f):
372        # a c e
373        # b d f
374        # 0 0 1
375        # see http://asymptote.sourceforge.net/doc/Transforms.html#Transforms
376        self.transforms.append("(%f, %f, %f, %f, %f, %f)" % (e, f, a, c, b, d))
377
378    def translate(self, x, y):
379        self.transforms.append("shift(%f, %f)" % (x, y))
380
381    def scale(self, x, y):
382        self.transforms.append("scale(%f, %f)" % (x, y))
383
384    def rotate(self, x):
385        self.transforms.append("rotate(%f)" % x)
386
387    def apply(self, asy):
388        return self._template % (" * ".join(self.transforms), asy)
389
390
391class Show(Builtin):
392    """
393    <dl>
394      <dt>'Show[$graphics$, $options$]'
395      <dd>shows graphics with the specified options added.
396    </dl>
397    """
398
399    options = GRAPHICS_OPTIONS
400
401    def apply(self, graphics, evaluation, options):
402        """Show[graphics_, OptionsPattern[%(name)s]]"""
403
404        for option in options:
405            if option not in ("System`ImageSize",):
406                options[option] = Expression(SymbolN, options[option]).evaluate(
407                    evaluation
408                )
409
410        # The below could probably be done with graphics.filter..
411        new_leaves = []
412        options_set = set(options.keys())
413        for leaf in graphics.leaves:
414            new_leaf = leaf
415            leaf_name = leaf.get_head_name()
416            if leaf_name == "System`Rule" and str(leaf.leaves[0]) in options_set:
417                continue
418            new_leaves.append(leaf)
419
420        new_leaves += options_to_rules(options)
421        graphics = graphics.restructure(graphics.head, new_leaves, evaluation)
422
423        return graphics
424
425
426class Graphics(Builtin):
427    r"""
428    <dl>
429      <dt>'Graphics[$primitives$, $options$]'
430      <dd>represents a graphic.
431    </dl>
432
433    Options include:
434
435    <ul>
436      <li>Axes
437      <li>TicksStyle
438      <li>AxesStyle
439      <li>LabelStyle
440      <li>AspectRatio
441      <li>PlotRange
442      <li>PlotRangePadding
443      <li>ImageSize
444      <li>Background
445    </ul>
446
447    >> Graphics[{Blue, Line[{{0,0}, {1,1}}]}]
448     = -Graphics-
449
450    'Graphics' supports 'PlotRange':
451    >> Graphics[{Rectangle[{1, 1}]}, Axes -> True, PlotRange -> {{-2, 1.5}, {-1, 1.5}}]
452     = -Graphics-
453
454    >> Graphics[{Rectangle[],Red,Disk[{1,0}]},PlotRange->{{0,1},{0,1}}]
455     = -Graphics-
456
457    'Graphics' produces 'GraphicsBox' boxes:
458    >> Graphics[Rectangle[]] // ToBoxes // Head
459     = GraphicsBox
460
461    In 'TeXForm', 'Graphics' produces Asymptote figures:
462    >> Graphics[Circle[]] // TeXForm
463     = #<--#
464     . \begin{asy}
465     . usepackage("amsmath");
466     . size(5.8556cm, 5.8333cm);
467     . draw(ellipse((175,175),175,175), rgb(0, 0, 0)+linewidth(0.66667));
468     . clip(box((-0.33333,0.33333), (350.33,349.67)));
469     . \end{asy}
470    """
471
472    options = GRAPHICS_OPTIONS
473
474    box_suffix = "Box"
475
476    def apply_makeboxes(self, content, evaluation, options):
477        """MakeBoxes[%(name)s[content_, OptionsPattern[%(name)s]],
478        StandardForm|TraditionalForm|OutputForm]"""
479
480        def convert(content):
481            head = content.get_head_name()
482
483            if head == "System`List":
484                return Expression(
485                    SymbolList, *[convert(item) for item in content.leaves]
486                )
487            elif head == "System`Style":
488                return Expression(
489                    "StyleBox", *[convert(item) for item in content.leaves]
490                )
491
492            if head in element_heads:
493                if head == "System`Text":
494                    head = "System`Inset"
495                atoms = content.get_atoms(include_heads=False)
496                if any(
497                    not isinstance(atom, (Integer, Real))
498                    and not atom.get_name() in GRAPHICS_SYMBOLS
499                    for atom in atoms
500                ):
501                    if head == "System`Inset":
502                        inset = content.leaves[0]
503                        if inset.get_head_name() == "System`Graphics":
504                            opts = {}
505                            # opts = dict(opt._leaves[0].name:opt_leaves[1]   for opt in  inset._leaves[1:])
506                            inset = self.apply_makeboxes(
507                                inset._leaves[0], evaluation, opts
508                            )
509                        n_leaves = [inset] + [
510                            Expression(SymbolN, leaf).evaluate(evaluation)
511                            for leaf in content.leaves[1:]
512                        ]
513                    else:
514                        n_leaves = (
515                            Expression(SymbolN, leaf).evaluate(evaluation)
516                            for leaf in content.leaves
517                        )
518                else:
519                    n_leaves = content.leaves
520                return Expression(head + self.box_suffix, *n_leaves)
521            return content
522
523        for option in options:
524            if option not in ("System`ImageSize",):
525                options[option] = Expression(SymbolN, options[option]).evaluate(
526                    evaluation
527                )
528        from mathics.builtin.drawing.graphics3d import Graphics3DBox, Graphics3D
529
530        if type(self) is Graphics:
531            return GraphicsBox(
532                convert(content), evaluation=evaluation, *options_to_rules(options)
533            )
534        elif type(self) is Graphics3D:
535            return Graphics3DBox(
536                convert(content), evaluation=evaluation, *options_to_rules(options)
537            )
538
539
540class _GraphicsElement(InstanceableBuiltin):
541    def init(self, graphics, item=None, style=None, opacity=1.0):
542        if item is not None and not item.has_form(self.get_name(), None):
543            raise BoxConstructError
544        self.graphics = graphics
545        self.style = style
546        self.opacity = opacity
547        self.is_completely_visible = False  # True for axis elements
548
549    @staticmethod
550    def create_as_style(klass, graphics, item):
551        return klass(graphics, item)
552
553
554class _Color(_GraphicsElement):
555    formats = {
556        # we are adding ImageSizeMultipliers in the rule below, because we do _not_ want color boxes to
557        # diminish in size when they appear in lists or rows. we only want the display of colors this
558        # way in the notebook, so we restrict the rule to StandardForm.
559        (
560            ("StandardForm",),
561            "%(name)s[x__?(NumericQ[#] && 0 <= # <= 1&)]",
562        ): "Style[Graphics[{EdgeForm[Black], %(name)s[x], Rectangle[]}, ImageSize -> 16], "
563        + "ImageSizeMultipliers -> {1, 1}]"
564    }
565
566    rules = {"%(name)s[x_List]": "Apply[%(name)s, x]"}
567
568    components_sizes = []
569    default_components = []
570
571    def init(self, item=None, components=None):
572        super(_Color, self).init(None, item)
573        if item is not None:
574            leaves = item.leaves
575            if len(leaves) in self.components_sizes:
576                # we must not clip here; we copy the components, without clipping,
577                # e.g. RGBColor[-1, 0, 0] stays RGBColor[-1, 0, 0]. this is especially
578                # important for color spaces like LAB that have negative components.
579
580                components = [value.round_to_float() for value in leaves]
581                if None in components:
582                    raise ColorError
583
584                # the following lines always extend to the maximum available
585                # default_components, so RGBColor[0, 0, 0] will _always_
586                # become RGBColor[0, 0, 0, 1]. does not seem the right thing
587                # to do in this general context. poke1024
588
589                if len(components) < 3:
590                    components.extend(self.default_components[len(components) :])
591
592                self.components = components
593            else:
594                raise ColorError
595        elif components is not None:
596            self.components = components
597
598    @staticmethod
599    def create(expr):
600        head = expr.get_head_name()
601        cls = get_class(head)
602        if cls is None:
603            raise ColorError
604        return cls(expr)
605
606    @staticmethod
607    def create_as_style(klass, graphics, item):
608        return klass(item)
609
610    def to_css(self):
611        rgba = self.to_rgba()
612        alpha = rgba[3] if len(rgba) > 3 else 1.0
613        return (
614            r"rgb(%f%%, %f%%, %f%%)" % (rgba[0] * 100, rgba[1] * 100, rgba[2] * 100),
615            alpha,
616        )
617
618    def to_asy(self):
619        rgba = self.to_rgba()
620        alpha = rgba[3] if len(rgba) > 3 else 1.0
621        return (
622            r"rgb(%s, %s, %s)"
623            % (asy_number(rgba[0]), asy_number(rgba[1]), asy_number(rgba[2])),
624            alpha,
625        )
626
627    def to_js(self):
628        return self.to_rgba()
629
630    def to_expr(self):
631        return Expression(self.get_name(), *self.components)
632
633    def to_rgba(self):
634        return self.to_color_space("RGB")
635
636    def to_color_space(self, color_space):
637        components = convert_color(self.components, self.color_space, color_space)
638        if components is None:
639            raise ValueError(
640                "cannot convert from color space %s to %s."
641                % (self.color_space, color_space)
642            )
643        return components
644
645
646class RGBColor(_Color):
647    """
648    <dl>
649    <dt>'RGBColor[$r$, $g$, $b$]'
650        <dd>represents a color with the specified red, green and blue
651        components.
652    </dl>
653
654    >> Graphics[MapIndexed[{RGBColor @@ #1, Disk[2*#2 ~Join~ {0}]} &, IdentityMatrix[3]], ImageSize->Small]
655     = -Graphics-
656
657    >> RGBColor[0, 1, 0]
658     = RGBColor[0, 1, 0]
659
660    >> RGBColor[0, 1, 0] // ToBoxes
661     = StyleBox[GraphicsBox[...], ...]
662    """
663
664    color_space = "RGB"
665    components_sizes = [3, 4]
666    default_components = [0, 0, 0, 1]
667
668    def to_rgba(self):
669        return self.components
670
671
672class LABColor(_Color):
673    """
674    <dl>
675    <dt>'LABColor[$l$, $a$, $b$]'
676        <dd>represents a color with the specified lightness, red/green and yellow/blue
677        components in the CIE 1976 L*a*b* (CIELAB) color space.
678    </dl>
679    """
680
681    color_space = "LAB"
682    components_sizes = [3, 4]
683    default_components = [0, 0, 0, 1]
684
685
686class LCHColor(_Color):
687    """
688    <dl>
689    <dt>'LCHColor[$l$, $c$, $h$]'
690        <dd>represents a color with the specified lightness, chroma and hue
691        components in the CIELCh CIELab cube color space.
692    </dl>
693    """
694
695    color_space = "LCH"
696    components_sizes = [3, 4]
697    default_components = [0, 0, 0, 1]
698
699
700class LUVColor(_Color):
701    """
702    <dl>
703    <dt>'LCHColor[$l$, $u$, $v$]'
704        <dd>represents a color with the specified components in the CIE 1976 L*u*v* (CIELUV) color space.
705    </dl>
706    """
707
708    color_space = "LUV"
709    components_sizes = [3, 4]
710    default_components = [0, 0, 0, 1]
711
712
713class XYZColor(_Color):
714    """
715    <dl>
716    <dt>'XYZColor[$x$, $y$, $z$]'
717        <dd>represents a color with the specified components in the CIE 1931 XYZ color space.
718    </dl>
719    """
720
721    color_space = "XYZ"
722    components_sizes = [3, 4]
723    default_components = [0, 0, 0, 1]
724
725
726class CMYKColor(_Color):
727    """
728    <dl>
729    <dt>'CMYKColor[$c$, $m$, $y$, $k$]'
730        <dd>represents a color with the specified cyan, magenta,
731        yellow and black components.
732    </dl>
733
734    >> Graphics[MapIndexed[{CMYKColor @@ #1, Disk[2*#2 ~Join~ {0}]} &, IdentityMatrix[4]], ImageSize->Small]
735     = -Graphics-
736    """
737
738    color_space = "CMYK"
739    components_sizes = [3, 4, 5]
740    default_components = [0, 0, 0, 0, 1]
741
742
743class Hue(_Color):
744    """
745    <dl>
746    <dt>'Hue[$h$, $s$, $l$, $a$]'
747        <dd>represents the color with hue $h$, saturation $s$,
748        lightness $l$ and opacity $a$.
749    <dt>'Hue[$h$, $s$, $l$]'
750        <dd>is equivalent to 'Hue[$h$, $s$, $l$, 1]'.
751    <dt>'Hue[$h$, $s$]'
752        <dd>is equivalent to 'Hue[$h$, $s$, 1, 1]'.
753    <dt>'Hue[$h$]'
754        <dd>is equivalent to 'Hue[$h$, 1, 1, 1]'.
755    </dl>
756    >> Graphics[Table[{EdgeForm[Gray], Hue[h, s], Disk[{12h, 8s}]}, {h, 0, 1, 1/6}, {s, 0, 1, 1/4}]]
757     = -Graphics-
758
759    >> Graphics[Table[{EdgeForm[{GrayLevel[0, 0.5]}], Hue[(-11+q+10r)/72, 1, 1, 0.6], Disk[(8-r) {Cos[2Pi q/12], Sin[2Pi q/12]}, (8-r)/3]}, {r, 6}, {q, 12}]]
760     = -Graphics-
761    """
762
763    color_space = "HSB"
764    components_sizes = [1, 2, 3, 4]
765    default_components = [0, 1, 1, 1]
766
767    def hsl_to_rgba(self):
768        h, s, l = self.components[:3]
769        if l < 0.5:
770            q = l * (1 + s)
771        else:
772            q = l + s - l * s
773        p = 2 * l - q
774
775        rgb = (h + 1 / 3, h, h - 1 / 3)
776
777        def map(value):
778            if value < 0:
779                value += 1
780            if value > 1:
781                value -= 1
782            return value
783
784        def trans(t):
785            if t < 1 / 6:
786                return p + ((q - p) * 6 * t)
787            elif t < 1 / 2:
788                return q
789            elif t < 2 / 3:
790                return p + ((q - p) * 6 * (2 / 3 - t))
791            else:
792                return p
793
794        result = tuple([trans(list(map(t))) for t in rgb]) + (self.components[3],)
795        return result
796
797
798class GrayLevel(_Color):
799    """
800    <dl>
801    <dt>'GrayLevel[$g$]'
802        <dd>represents a shade of gray specified by $g$, ranging from
803        0 (black) to 1 (white).
804    <dt>'GrayLevel[$g$, $a$]'
805        <dd>represents a shade of gray specified by $g$ with opacity $a$.
806    </dl>
807    """
808
809    color_space = "Grayscale"
810    components_sizes = [1, 2]
811    default_components = [0, 1]
812
813
814def expression_to_color(color):
815    try:
816        return _Color.create(color)
817    except ColorError:
818        return None
819
820
821def color_to_expression(components, colorspace):
822    if colorspace == "Grayscale":
823        converted_color_name = "GrayLevel"
824    elif colorspace == "HSB":
825        converted_color_name = "Hue"
826    else:
827        converted_color_name = colorspace + "Color"
828
829    return Expression(converted_color_name, *components)
830
831
832class ColorDistance(Builtin):
833    """
834    <dl>
835    <dt>'ColorDistance[$c1$, $c2$]'
836        <dd>returns a measure of color distance between the colors $c1$ and $c2$.
837    <dt>'ColorDistance[$list$, $c2$]'
838        <dd>returns a list of color distances between the colors in $list$ and $c2$.
839    </dl>
840
841    The option DistanceFunction specifies the method used to measure the color
842    distance. Available options are:
843
844    CIE76: euclidean distance in the LABColor space
845    CIE94: euclidean distance in the LCHColor space
846    CIE2000 or CIEDE2000: CIE94 distance with corrections
847    CMC: Colour Measurement Committee metric (1984)
848    DeltaL: difference in the L component of LCHColor
849    DeltaC: difference in the C component of LCHColor
850    DeltaH: difference in the H component of LCHColor
851
852    It is also possible to specify a custom distance
853
854    >> ColorDistance[Magenta, Green]
855     = 2.2507
856    >> ColorDistance[{Red, Blue}, {Green, Yellow}, DistanceFunction -> {"CMC", "Perceptibility"}]
857     = {1.0495, 1.27455}
858    #> ColorDistance[Blue, Red, DistanceFunction -> "CIE2000"]
859     = 0.557976
860    #> ColorDistance[Red, Black, DistanceFunction -> (Abs[#1[[1]] - #2[[1]]] &)]
861     = 0.542917
862
863    """
864
865    options = {"DistanceFunction": "Automatic"}
866
867    messages = {
868        "invdist": "`1` is not Automatic or a valid distance specification.",
869        "invarg": "`1` and `2` should be two colors or a color and a lists of colors or "
870        + "two lists of colors of the same length.",
871    }
872
873    # the docs say LABColor's colorspace corresponds to the CIE 1976 L^* a^* b^* color space
874    # with {l,a,b}={L^*,a^*,b^*}/100. Corrections factors are put accordingly.
875
876    _distances = {
877        "CIE76": lambda c1, c2: _euclidean_distance(
878            c1.to_color_space("LAB")[:3], c2.to_color_space("LAB")[:3]
879        ),
880        "CIE94": lambda c1, c2: _euclidean_distance(
881            c1.to_color_space("LCH")[:3], c2.to_color_space("LCH")[:3]
882        ),
883        "CIE2000": lambda c1, c2: _cie2000_distance(
884            100 * c1.to_color_space("LAB")[:3], 100 * c2.to_color_space("LAB")[:3]
885        )
886        / 100,
887        "CIEDE2000": lambda c1, c2: _cie2000_distance(
888            100 * c1.to_color_space("LAB")[:3], 100 * c2.to_color_space("LAB")[:3]
889        )
890        / 100,
891        "DeltaL": lambda c1, c2: _component_distance(
892            c1.to_color_space("LCH"), c2.to_color_space("LCH"), 0
893        ),
894        "DeltaC": lambda c1, c2: _component_distance(
895            c1.to_color_space("LCH"), c2.to_color_space("LCH"), 1
896        ),
897        "DeltaH": lambda c1, c2: _component_distance(
898            c1.to_color_space("LCH"), c2.to_color_space("LCH"), 2
899        ),
900        "CMC": lambda c1, c2: _CMC_distance(
901            100 * c1.to_color_space("LAB")[:3], 100 * c2.to_color_space("LAB")[:3], 1, 1
902        )
903        / 100,
904    }
905
906    def apply(self, c1, c2, evaluation, options):
907        "ColorDistance[c1_, c2_, OptionsPattern[ColorDistance]]"
908
909        # If numpy is not installed, 100 * c1.to_color_space returns
910        # a list of 100 x 3 elements, instead of doing elementwise multiplication
911        try:
912            import numpy as np
913        except:
914            raise RuntimeError("NumPy needs to be installed for ColorDistance")
915
916        distance_function = options.get("System`DistanceFunction")
917        compute = None
918        if isinstance(distance_function, String):
919            compute = ColorDistance._distances.get(distance_function.get_string_value())
920            if not compute:
921                evaluation.message("ColorDistance", "invdist", distance_function)
922                return
923        elif distance_function.has_form("List", 2):
924            if distance_function.leaves[0].get_string_value() == "CMC":
925                if distance_function.leaves[1].get_string_value() == "Acceptability":
926                    compute = (
927                        lambda c1, c2: _CMC_distance(
928                            100 * c1.to_color_space("LAB")[:3],
929                            100 * c2.to_color_space("LAB")[:3],
930                            2,
931                            1,
932                        )
933                        / 100
934                    )
935                elif distance_function.leaves[1].get_string_value() == "Perceptibility":
936                    compute = ColorDistance._distances.get("CMC")
937
938                elif distance_function.leaves[1].has_form("List", 2):
939                    if isinstance(
940                        distance_function.leaves[1].leaves[0], Integer
941                    ) and isinstance(distance_function.leaves[1].leaves[1], Integer):
942                        if (
943                            distance_function.leaves[1].leaves[0].get_int_value() > 0
944                            and distance_function.leaves[1].leaves[1].get_int_value()
945                            > 0
946                        ):
947                            lightness = (
948                                distance_function.leaves[1].leaves[0].get_int_value()
949                            )
950                            chroma = (
951                                distance_function.leaves[1].leaves[1].get_int_value()
952                            )
953                            compute = (
954                                lambda c1, c2: _CMC_distance(
955                                    100 * c1.to_color_space("LAB")[:3],
956                                    100 * c2.to_color_space("LAB")[:3],
957                                    lightness,
958                                    chroma,
959                                )
960                                / 100
961                            )
962
963        elif (
964            isinstance(distance_function, Symbol)
965            and distance_function.get_name() == "System`Automatic"
966        ):
967            compute = ColorDistance._distances.get("CIE76")
968        else:
969
970            def compute(a, b):
971                return Expression(
972                    "Apply",
973                    distance_function,
974                    Expression(
975                        "List",
976                        Expression(
977                            "List", *[Real(val) for val in a.to_color_space("LAB")]
978                        ),
979                        Expression(
980                            "List", *[Real(val) for val in b.to_color_space("LAB")]
981                        ),
982                    ),
983                )
984
985        if compute == None:
986            evaluation.message("ColorDistance", "invdist", distance_function)
987            return
988
989        def distance(a, b):
990            try:
991                py_a = _Color.create(a)
992                py_b = _Color.create(b)
993            except ColorError:
994                evaluation.message("ColorDistance", "invarg", a, b)
995                raise
996            result = from_python(compute(py_a, py_b))
997            return result
998
999        try:
1000            if c1.get_head_name() == "System`List":
1001                if c2.get_head_name() == "System`List":
1002                    if len(c1.leaves) != len(c2.leaves):
1003                        evaluation.message("ColorDistance", "invarg", c1, c2)
1004                        return
1005                    else:
1006                        return Expression(
1007                            "List",
1008                            *[distance(a, b) for a, b in zip(c1.leaves, c2.leaves)],
1009                        )
1010                else:
1011                    return Expression(SymbolList, *[distance(c, c2) for c in c1.leaves])
1012            elif c2.get_head_name() == "System`List":
1013                return Expression(SymbolList, *[distance(c1, c) for c in c2.leaves])
1014            else:
1015                return distance(c1, c2)
1016        except ColorError:
1017            return
1018        except NotImplementedError:
1019            evaluation.message("ColorDistance", "invdist", distance_function)
1020            return
1021
1022
1023class _Size(_GraphicsElement):
1024    def init(self, graphics, item=None, value=None):
1025        super(_Size, self).init(graphics, item)
1026        if item is not None:
1027            self.value = item.leaves[0].round_to_float()
1028        elif value is not None:
1029            self.value = value
1030        else:
1031            raise BoxConstructError
1032        if self.value < 0:
1033            raise BoxConstructError
1034
1035
1036class _Thickness(_Size):
1037    pass
1038
1039
1040class AbsoluteThickness(_Thickness):
1041    """
1042    <dl>
1043    <dt>'AbsoluteThickness[$p$]'
1044        <dd>sets the line thickness for subsequent graphics primitives
1045        to $p$ points.
1046    </dl>
1047
1048    >> Graphics[Table[{AbsoluteThickness[t], Line[{{20 t, 10}, {20 t, 80}}], Text[ToString[t]<>"pt", {20 t, 0}]}, {t, 0, 10}]]
1049     = -Graphics-
1050    """
1051
1052    def get_thickness(self):
1053        return self.graphics.translate_absolute((self.value, 0))[0]
1054
1055
1056class Thickness(_Thickness):
1057    """
1058    <dl>
1059    <dt>'Thickness[$t$]'
1060        <dd>sets the line thickness for subsequent graphics primitives
1061        to $t$ times the size of the plot area.
1062    </dl>
1063
1064    >> Graphics[{Thickness[0.2], Line[{{0, 0}, {0, 5}}]}, Axes->True, PlotRange->{{-5, 5}, {-5, 5}}]
1065     = -Graphics-
1066    """
1067
1068    def get_thickness(self):
1069        return self.graphics.translate_relative(self.value)
1070
1071
1072class Thin(Builtin):
1073    """
1074    <dl>
1075    <dt>'Thin'
1076        <dd>sets the line width for subsequent graphics primitives to 0.5pt.
1077    </dl>
1078    """
1079
1080    rules = {"Thin": "AbsoluteThickness[0.5]"}
1081
1082
1083class Thick(Builtin):
1084    """
1085    <dl>
1086    <dt>'Thick'
1087        <dd>sets the line width for subsequent graphics primitives to 2pt.
1088    </dl>
1089    """
1090
1091    rules = {"Thick": "AbsoluteThickness[2]"}
1092
1093
1094class PointSize(_Size):
1095    """
1096    <dl>
1097    <dt>'PointSize[$t$]'
1098        <dd>sets the diameter of points to $t$, which is relative to the overall width.
1099    </dl>
1100    """
1101
1102    def get_size(self):
1103        return self.graphics.view_width * self.value
1104
1105
1106class FontColor(Builtin):
1107    """
1108    <dl>
1109    <dt>'FontColor'
1110        <dd>is an option for Style to set the font color.
1111    </dl>
1112    """
1113
1114    pass
1115
1116
1117class Offset(Builtin):
1118    pass
1119
1120
1121class Rectangle(Builtin):
1122    """
1123    <dl>
1124    <dt>'Rectangle[{$xmin$, $ymin$}]'
1125        <dd>represents a unit square with bottom-left corner at {$xmin$, $ymin$}.
1126    <dt>'Rectangle[{$xmin$, $ymin$}, {$xmax$, $ymax$}]
1127        <dd>is a rectange extending from {$xmin$, $ymin$} to {$xmax$, $ymax$}.
1128    </dl>
1129
1130    >> Graphics[Rectangle[]]
1131     = -Graphics-
1132
1133    >> Graphics[{Blue, Rectangle[{0.5, 0}], Orange, Rectangle[{0, 0.5}]}]
1134     = -Graphics-
1135    """
1136
1137    rules = {"Rectangle[]": "Rectangle[{0, 0}]"}
1138
1139
1140class Disk(Builtin):
1141    """
1142    <dl>
1143    <dt>'Disk[{$cx$, $cy$}, $r$]'
1144        <dd>fills a circle with center '($cx$, $cy$)' and radius $r$.
1145    <dt>'Disk[{$cx$, $cy$}, {$rx$, $ry$}]'
1146        <dd>fills an ellipse.
1147    <dt>'Disk[{$cx$, $cy$}]'
1148        <dd>chooses radius 1.
1149    <dt>'Disk[]'
1150        <dd>chooses center '(0, 0)' and radius 1.
1151    <dt>'Disk[{$x$, $y$}, ..., {$t1$, $t2$}]'
1152        <dd>is a sector from angle $t1$ to $t2$.
1153    </dl>
1154
1155    >> Graphics[{Blue, Disk[{0, 0}, {2, 1}]}]
1156     = -Graphics-
1157    The outer border can be drawn using 'EdgeForm':
1158    >> Graphics[{EdgeForm[Black], Red, Disk[]}]
1159     = -Graphics-
1160
1161    Disk can also draw sectors of circles and ellipses
1162    >> Graphics[Disk[{0, 0}, 1, {Pi / 3, 2 Pi / 3}]]
1163     = -Graphics-
1164    >> Graphics[{Blue, Disk[{0, 0}, {1, 2}, {Pi / 3, 5 Pi / 3}]}]
1165     = -Graphics-
1166    """
1167
1168    rules = {"Disk[]": "Disk[{0, 0}]"}
1169
1170
1171class Circle(Builtin):
1172    """
1173    <dl>
1174    <dt>'Circle[{$cx$, $cy$}, $r$]'
1175        <dd>draws a circle with center '($cx$, $cy$)' and radius $r$.
1176    <dt>'Circle[{$cx$, $cy$}, {$rx$, $ry$}]'
1177        <dd>draws an ellipse.
1178    <dt>'Circle[{$cx$, $cy$}]'
1179        <dd>chooses radius 1.
1180    <dt>'Circle[]'
1181        <dd>chooses center '(0, 0)' and radius 1.
1182    </dl>
1183
1184    >> Graphics[{Red, Circle[{0, 0}, {2, 1}]}]
1185     = -Graphics-
1186    >> Graphics[{Circle[], Disk[{0, 0}, {1, 1}, {0, 2.1}]}]
1187     = -Graphics-
1188    """
1189
1190    rules = {"Circle[]": "Circle[{0, 0}]"}
1191
1192
1193class Inset(Builtin):
1194    pass
1195
1196
1197class Text(Inset):
1198    """
1199    <dl>
1200    <dt>'Text["$text$", {$x$, $y$}]'
1201        <dd>draws $text$ centered on position '{$x$, $y$}'.
1202    </dl>
1203
1204    >> Graphics[{Text["First", {0, 0}], Text["Second", {1, 1}]}, Axes->True, PlotRange->{{-2, 2}, {-2, 2}}]
1205     = -Graphics-
1206
1207    #> Graphics[{Text[x, {0,0}]}]
1208     = -Graphics-
1209    """
1210
1211
1212class RectangleBox(_GraphicsElement):
1213    def init(self, graphics, style, item):
1214        super(RectangleBox, self).init(graphics, item, style)
1215        if len(item.leaves) not in (1, 2):
1216            raise BoxConstructError
1217        self.edge_color, self.face_color = style.get_style(_Color, face_element=True)
1218        self.p1 = Coords(graphics, item.leaves[0])
1219        if len(item.leaves) == 1:
1220            self.p2 = self.p1.add(1, 1)
1221        elif len(item.leaves) == 2:
1222            self.p2 = Coords(graphics, item.leaves[1])
1223
1224    def extent(self):
1225        l = self.style.get_line_width(face_element=True) / 2
1226        result = []
1227        for p in [self.p1, self.p2]:
1228            x, y = p.pos()
1229            result.extend(
1230                [(x - l, y - l), (x - l, y + l), (x + l, y - l), (x + l, y + l)]
1231            )
1232        return result
1233
1234    def to_svg(self, offset=None):
1235        l = self.style.get_line_width(face_element=True)
1236        x1, y1 = self.p1.pos()
1237        x2, y2 = self.p2.pos()
1238        xmin = min(x1, x2)
1239        ymin = min(y1, y2)
1240        w = max(x1, x2) - xmin
1241        h = max(y1, y2) - ymin
1242        if offset:
1243            x1, x2 = x1 + offset[0], x2 + offset[0]
1244            y1, y2 = y1 + offset[1], y2 + offset[1]
1245        style = create_css(self.edge_color, self.face_color, l)
1246        return '<rect x="%f" y="%f" width="%f" height="%f" style="%s" />' % (
1247            xmin,
1248            ymin,
1249            w,
1250            h,
1251            style,
1252        )
1253
1254    def to_asy(self):
1255        l = self.style.get_line_width(face_element=True)
1256        x1, y1 = self.p1.pos()
1257        x2, y2 = self.p2.pos()
1258        pens = create_pens(self.edge_color, self.face_color, l, is_face_element=True)
1259        x1, x2, y1, y2 = asy_number(x1), asy_number(x2), asy_number(y1), asy_number(y2)
1260        return "filldraw((%s,%s)--(%s,%s)--(%s,%s)--(%s,%s)--cycle, %s);" % (
1261            x1,
1262            y1,
1263            x2,
1264            y1,
1265            x2,
1266            y2,
1267            x1,
1268            y2,
1269            pens,
1270        )
1271
1272
1273class _RoundBox(_GraphicsElement):
1274    face_element = None
1275
1276    def init(self, graphics, style, item):
1277        super(_RoundBox, self).init(graphics, item, style)
1278        if len(item._leaves) not in (1, 2):
1279            raise BoxConstructError
1280        self.edge_color, self.face_color = style.get_style(
1281            _Color, face_element=self.face_element
1282        )
1283        self.c = Coords(graphics, item.leaves[0])
1284        if len(item.leaves) == 1:
1285            rx = ry = 1
1286        elif len(item.leaves) == 2:
1287            r = item.leaves[1]
1288            if r.has_form("List", 2):
1289                rx = r.leaves[0].round_to_float()
1290                ry = r.leaves[1].round_to_float()
1291            else:
1292                rx = ry = r.round_to_float()
1293        self.r = self.c.add(rx, ry)
1294
1295    def extent(self):
1296        l = self.style.get_line_width(face_element=self.face_element) / 2
1297        x, y = self.c.pos()
1298        rx, ry = self.r.pos()
1299        rx -= x
1300        ry = y - ry
1301        rx += l
1302        ry += l
1303        return [(x - rx, y - ry), (x - rx, y + ry), (x + rx, y - ry), (x + rx, y + ry)]
1304
1305    def to_svg(self, offset=None):
1306        x, y = self.c.pos()
1307        rx, ry = self.r.pos()
1308        rx -= x
1309        ry = y - ry
1310        l = self.style.get_line_width(face_element=self.face_element)
1311        style = create_css(self.edge_color, self.face_color, stroke_width=l)
1312        return '<ellipse cx="%f" cy="%f" rx="%f" ry="%f" style="%s" />' % (
1313            x,
1314            y,
1315            rx,
1316            ry,
1317            style,
1318        )
1319
1320    def to_asy(self):
1321        x, y = self.c.pos()
1322        rx, ry = self.r.pos()
1323        rx -= x
1324        ry -= y
1325        l = self.style.get_line_width(face_element=self.face_element)
1326        pen = create_pens(
1327            edge_color=self.edge_color,
1328            face_color=self.face_color,
1329            stroke_width=l,
1330            is_face_element=self.face_element,
1331        )
1332        cmd = "filldraw" if self.face_element else "draw"
1333        return "%s(ellipse((%s,%s),%s,%s), %s);" % (
1334            cmd,
1335            asy_number(x),
1336            asy_number(y),
1337            asy_number(rx),
1338            asy_number(ry),
1339            pen,
1340        )
1341
1342
1343class _ArcBox(_RoundBox):
1344    def init(self, graphics, style, item):
1345        if len(item.leaves) == 3:
1346            arc_expr = item.leaves[2]
1347            if arc_expr.get_head_name() != "System`List":
1348                raise BoxConstructError
1349            arc = arc_expr.leaves
1350            pi2 = 2 * pi
1351
1352            start_angle = arc[0].round_to_float()
1353            end_angle = arc[1].round_to_float()
1354
1355            if start_angle is None or end_angle is None:
1356                raise BoxConstructError
1357            elif end_angle >= start_angle + pi2:  # full circle?
1358                self.arc = None
1359            else:
1360                if end_angle <= start_angle:
1361                    self.arc = (end_angle, start_angle)
1362                else:
1363                    self.arc = (start_angle, end_angle)
1364
1365            item = Expression(item.get_head_name(), *item.leaves[:2])
1366        else:
1367            self.arc = None
1368        super(_ArcBox, self).init(graphics, style, item)
1369
1370    def _arc_params(self):
1371        x, y = self.c.pos()
1372        rx, ry = self.r.pos()
1373
1374        rx -= x
1375        ry -= y
1376
1377        start_angle, end_angle = self.arc
1378
1379        if end_angle - start_angle <= pi:
1380            large_arc = 0
1381        else:
1382            large_arc = 1
1383
1384        sx = x + rx * cos(start_angle)
1385        sy = y + ry * sin(start_angle)
1386
1387        ex = x + rx * cos(end_angle)
1388        ey = y + ry * sin(end_angle)
1389
1390        return x, y, abs(rx), abs(ry), sx, sy, ex, ey, large_arc
1391
1392    def to_svg(self, offset=None):
1393        if self.arc is None:
1394            return super(_ArcBox, self).to_svg(offset)
1395
1396        x, y, rx, ry, sx, sy, ex, ey, large_arc = self._arc_params()
1397
1398        def path(closed):
1399            if closed:
1400                yield "M %f,%f" % (x, y)
1401                yield "L %f,%f" % (sx, sy)
1402            else:
1403                yield "M %f,%f" % (sx, sy)
1404
1405            yield "A %f,%f,0,%d,0,%f,%f" % (rx, ry, large_arc, ex, ey)
1406
1407            if closed:
1408                yield "Z"
1409
1410        l = self.style.get_line_width(face_element=self.face_element)
1411        style = create_css(self.edge_color, self.face_color, stroke_width=l)
1412        return '<path d="%s" style="%s" />' % (" ".join(path(self.face_element)), style)
1413
1414    def to_asy(self):
1415        if self.arc is None:
1416            return super(_ArcBox, self).to_asy()
1417
1418        x, y, rx, ry, sx, sy, ex, ey, large_arc = self._arc_params()
1419
1420        def path(closed):
1421            if closed:
1422                yield "(%s,%s)--(%s,%s)--" % tuple(
1423                    asy_number(t) for t in (x, y, sx, sy)
1424                )
1425
1426            yield "arc((%s,%s), (%s, %s), (%s, %s))" % tuple(
1427                asy_number(t) for t in (x, y, sx, sy, ex, ey)
1428            )
1429
1430            if closed:
1431                yield "--cycle"
1432
1433        l = self.style.get_line_width(face_element=self.face_element)
1434        pen = create_pens(
1435            edge_color=self.edge_color,
1436            face_color=self.face_color,
1437            stroke_width=l,
1438            is_face_element=self.face_element,
1439        )
1440        command = "filldraw" if self.face_element else "draw"
1441        return "%s(%s, %s);" % (command, "".join(path(self.face_element)), pen)
1442
1443
1444class DiskBox(_ArcBox):
1445    face_element = True
1446
1447
1448class CircleBox(_ArcBox):
1449    face_element = False
1450
1451
1452class _Polyline(_GraphicsElement):
1453    def do_init(self, graphics, points):
1454        if not points.has_form("List", None):
1455            raise BoxConstructError
1456        if (
1457            points.leaves
1458            and points.leaves[0].has_form("List", None)
1459            and all(leaf.has_form("List", None) for leaf in points.leaves[0].leaves)
1460        ):
1461            leaves = points.leaves
1462            self.multi_parts = True
1463        else:
1464            leaves = [Expression(SymbolList, *points.leaves)]
1465            self.multi_parts = False
1466        lines = []
1467        for leaf in leaves:
1468            if leaf.has_form("List", None):
1469                lines.append(leaf.leaves)
1470            else:
1471                raise BoxConstructError
1472        self.lines = [
1473            [graphics.coords(graphics, point) for point in line] for line in lines
1474        ]
1475
1476    def extent(self):
1477        l = self.style.get_line_width(face_element=False)
1478        result = []
1479        for line in self.lines:
1480            for c in line:
1481                x, y = c.pos()
1482                result.extend(
1483                    [(x - l, y - l), (x - l, y + l), (x + l, y - l), (x + l, y + l)]
1484                )
1485        return result
1486
1487
1488class Point(Builtin):
1489    """
1490    <dl>
1491    <dt>'Point[{$point_1$, $point_2$ ...}]'
1492        <dd>represents the point primitive.
1493    <dt>'Point[{{$p_11$, $p_12$, ...}, {$p_21$, $p_22$, ...}, ...}]'
1494        <dd>represents a number of point primitives.
1495    </dl>
1496
1497    >> Graphics[Point[{0,0}]]
1498    = -Graphics-
1499
1500    >> Graphics[Point[Table[{Sin[t], Cos[t]}, {t, 0, 2. Pi, Pi / 15.}]]]
1501    = -Graphics-
1502
1503    >> Graphics3D[Point[Table[{Sin[t], Cos[t], 0}, {t, 0, 2. Pi, Pi / 15.}]]]
1504    = -Graphics3D-
1505    """
1506
1507    pass
1508
1509
1510class PointBox(_Polyline):
1511    def init(self, graphics, style, item=None):
1512        super(PointBox, self).init(graphics, item, style)
1513        self.edge_color, self.face_color = style.get_style(_Color, face_element=True)
1514        if item is not None:
1515            if len(item.leaves) != 1:
1516                raise BoxConstructError
1517            points = item.leaves[0]
1518            if points.has_form("List", None) and len(points.leaves) != 0:
1519                if all(not leaf.has_form("List", None) for leaf in points.leaves):
1520                    points = Expression(SymbolList, points)
1521            self.do_init(graphics, points)
1522        else:
1523            raise BoxConstructError
1524
1525    def to_svg(self, offset=None):
1526        point_size, _ = self.style.get_style(PointSize, face_element=False)
1527        if point_size is None:
1528            point_size = PointSize(self.graphics, value=0.005)
1529        size = point_size.get_size()
1530
1531        style = create_css(
1532            edge_color=self.edge_color, stroke_width=0, face_color=self.face_color
1533        )
1534        svg = ""
1535        for line in self.lines:
1536            for coords in line:
1537                svg += '<circle cx="%f" cy="%f" r="%f" style="%s" />' % (
1538                    coords.pos()[0],
1539                    coords.pos()[1],
1540                    size,
1541                    style,
1542                )
1543        return svg
1544
1545    def to_asy(self):
1546        pen = create_pens(face_color=self.face_color, is_face_element=False)
1547
1548        asy = ""
1549        for line in self.lines:
1550            for coords in line:
1551                asy += "dot(%s, %s);" % (coords.pos(), pen)
1552
1553        return asy
1554
1555
1556class Line(Builtin):
1557    """
1558    <dl>
1559    <dt>'Line[{$point_1$, $point_2$ ...}]'
1560        <dd>represents the line primitive.
1561    <dt>'Line[{{$p_11$, $p_12$, ...}, {$p_21$, $p_22$, ...}, ...}]'
1562        <dd>represents a number of line primitives.
1563    </dl>
1564
1565    >> Graphics[Line[{{0,1},{0,0},{1,0},{1,1}}]]
1566    = -Graphics-
1567
1568    >> Graphics3D[Line[{{0,0,0},{0,1,1},{1,0,0}}]]
1569    = -Graphics3D-
1570    """
1571
1572    pass
1573
1574
1575class LineBox(_Polyline):
1576    def init(self, graphics, style, item=None, lines=None):
1577        super(LineBox, self).init(graphics, item, style)
1578        self.edge_color, _ = style.get_style(_Color, face_element=False)
1579        if item is not None:
1580            if len(item.leaves) != 1:
1581                raise BoxConstructError
1582            points = item.leaves[0]
1583            self.do_init(graphics, points)
1584        elif lines is not None:
1585            self.lines = lines
1586        else:
1587            raise BoxConstructError
1588
1589    def to_svg(self, offset=None):
1590        l = self.style.get_line_width(face_element=False)
1591        style = create_css(edge_color=self.edge_color, stroke_width=l)
1592        svg = ""
1593        for line in self.lines:
1594            svg += '<polyline points="%s" style="%s" />' % (
1595                " ".join(["%f,%f" % coords.pos() for coords in line]),
1596                style,
1597            )
1598        return svg
1599
1600    def to_asy(self):
1601        l = self.style.get_line_width(face_element=False)
1602        pen = create_pens(edge_color=self.edge_color, stroke_width=l)
1603        asy = ""
1604        for line in self.lines:
1605            path = "--".join(["(%.5g,%5g)" % coords.pos() for coords in line])
1606            asy += "draw(%s, %s);" % (path, pen)
1607        return asy
1608
1609
1610def _svg_bezier(*segments):
1611    # see https://www.w3.org/TR/SVG/paths.html#PathDataCubicBezierCommands
1612    # see https://docs.webplatform.org/wiki/svg/tutorials/smarter_svg_shapes
1613
1614    while segments and not segments[0][1]:
1615        segments = segments[1:]
1616
1617    if not segments:
1618        return
1619
1620    forms = "LQC"  # SVG commands for line, quadratic bezier, cubic bezier
1621
1622    def path(max_degree, p):
1623        max_degree = min(max_degree, len(forms))
1624        while p:
1625            n = min(max_degree, len(p))  # 1, 2, or 3
1626            if n < 1:
1627                raise BoxConstructError
1628            yield forms[n - 1] + " ".join("%f,%f" % xy for xy in p[:n])
1629            p = p[n:]
1630
1631    k, p = segments[0]
1632    yield "M%f,%f" % p[0]
1633
1634    for s in path(k, p[1:]):
1635        yield s
1636
1637    for k, p in segments[1:]:
1638        for s in path(k, p):
1639            yield s
1640
1641
1642def _asy_bezier(*segments):
1643    # see http://asymptote.sourceforge.net/doc/Bezier-curves.html#Bezier-curves
1644
1645    while segments and not segments[0][1]:
1646        segments = segments[1:]
1647
1648    if not segments:
1649        return
1650
1651    def cubic(p0, p1, p2, p3):
1652        return "..controls(%.5g,%.5g) and (%.5g,%.5g)..(%.5g,%.5g)" % tuple(
1653            list(chain(p1, p2, p3))
1654        )
1655
1656    def quadratric(qp0, qp1, qp2):
1657        # asymptote only supports cubic beziers, so we convert this quadratic
1658        # bezier to a cubic bezier, see http://fontforge.github.io/bezier.html
1659
1660        # CP0 = QP0
1661        # CP3 = QP2
1662        # CP1 = QP0 + 2 / 3 * (QP1 - QP0)
1663        # CP2 = QP2 + 2 / 3 * (QP1 - QP2)
1664
1665        qp0x, qp0y = qp0
1666        qp1x, qp1y = qp1
1667        qp2x, qp2y = qp2
1668
1669        t = 2.0 / 3.0
1670        cp0 = qp0
1671        cp1 = (qp0x + t * (qp1x - qp0x), qp0y + t * (qp1y - qp0y))
1672        cp2 = (qp2x + t * (qp1x - qp2x), qp2y + t * (qp1y - qp2y))
1673        cp3 = qp2
1674
1675        return cubic(cp0, cp1, cp2, cp3)
1676
1677    def linear(p0, p1):
1678        return "--(%.5g,%.5g)" % p1
1679
1680    forms = (linear, quadratric, cubic)
1681
1682    def path(max_degree, p):
1683        max_degree = min(max_degree, len(forms))
1684        while p:
1685            n = min(max_degree, len(p) - 1)  # 1, 2, or 3
1686            if n < 1:
1687                break
1688            yield forms[n - 1](*p[: n + 1])
1689            p = p[n:]
1690
1691    k, p = segments[0]
1692    yield "(%.5g,%.5g)" % p[0]
1693
1694    connect = []
1695    for k, p in segments:
1696        for s in path(k, list(chain(connect, p))):
1697            yield s
1698        connect = p[-1:]
1699
1700
1701class BernsteinBasis(Builtin):
1702    attributes = ("Listable", "NumericFunction", "Protected")
1703    rules = {
1704        "BernsteinBasis[d_, n_, x_]": "Piecewise[{{Binomial[d, n] * x ^ n * (1 - x) ^ (d - n), 0 < x < 1}}, 0]"
1705    }
1706
1707
1708class BezierFunction(Builtin):
1709    rules = {
1710        "BezierFunction[p_]": "Function[x, Total[p * BernsteinBasis[Length[p] - 1, Range[0, Length[p] - 1], x]]]"
1711    }
1712
1713
1714class BezierCurve(Builtin):
1715    """
1716    <dl>
1717    <dt>'BezierCurve[{$p1$, $p2$ ...}]'
1718        <dd>represents a bezier curve with $p1$, $p2$ as control points.
1719    </dl>
1720
1721    >> Graphics[BezierCurve[{{0, 0},{1, 1},{2, -1},{3, 0}}]]
1722     = -Graphics-
1723
1724    >> Module[{p={{0, 0},{1, 1},{2, -1},{4, 0}}}, Graphics[{BezierCurve[p], Red, Point[Table[BezierFunction[p][x], {x, 0, 1, 0.1}]]}]]
1725     = -Graphics-
1726    """
1727
1728    options = {"SplineDegree": "3"}
1729
1730
1731class BezierCurveBox(_Polyline):
1732    def init(self, graphics, style, item, options):
1733        super(BezierCurveBox, self).init(graphics, item, style)
1734        if len(item.leaves) != 1 or item.leaves[0].get_head_name() != "System`List":
1735            raise BoxConstructError
1736        self.edge_color, _ = style.get_style(_Color, face_element=False)
1737        points = item.leaves[0]
1738        self.do_init(graphics, points)
1739        spline_degree = options.get("System`SplineDegree")
1740        if not isinstance(spline_degree, Integer):
1741            raise BoxConstructError
1742        self.spline_degree = spline_degree.get_int_value()
1743
1744    def to_svg(self, offset=None):
1745        l = self.style.get_line_width(face_element=False)
1746        style = create_css(edge_color=self.edge_color, stroke_width=l)
1747
1748        svg = ""
1749        for line in self.lines:
1750            s = " ".join(_svg_bezier((self.spline_degree, [xy.pos() for xy in line])))
1751            svg += '<path d="%s" style="%s"/>' % (s, style)
1752        return svg
1753
1754    def to_asy(self):
1755        l = self.style.get_line_width(face_element=False)
1756        pen = create_pens(edge_color=self.edge_color, stroke_width=l)
1757
1758        asy = ""
1759        for line in self.lines:
1760            for path in _asy_bezier((self.spline_degree, [xy.pos() for xy in line])):
1761                if path[:2] == "..":
1762                    path = "(0.,0.)" + path
1763                asy += "draw(%s, %s);" % (path, pen)
1764        return asy
1765
1766
1767class FilledCurve(Builtin):
1768    """
1769    <dl>
1770    <dt>'FilledCurve[{$segment1$, $segment2$ ...}]'
1771        <dd>represents a filled curve.
1772    </dl>
1773
1774    >> Graphics[FilledCurve[{Line[{{0, 0}, {1, 1}, {2, 0}}]}]]
1775    = -Graphics-
1776
1777    >> Graphics[FilledCurve[{BezierCurve[{{0, 0}, {1, 1}, {2, 0}}], Line[{{3, 0}, {0, 2}}]}]]
1778    = -Graphics-
1779    """
1780
1781    pass
1782
1783
1784class FilledCurveBox(_GraphicsElement):
1785    def init(self, graphics, style, item=None):
1786        super(FilledCurveBox, self).init(graphics, item, style)
1787        self.edge_color, self.face_color = style.get_style(_Color, face_element=True)
1788
1789        if item is not None and item.leaves and item.leaves[0].has_form("List", None):
1790            if len(item.leaves) != 1:
1791                raise BoxConstructError
1792            leaves = item.leaves[0].leaves
1793
1794            def parse_component(segments):
1795                for segment in segments:
1796                    head = segment.get_head_name()
1797
1798                    if head == "System`Line":
1799                        k = 1
1800                        parts = segment.leaves
1801                    elif head == "System`BezierCurve":
1802                        parts, options = _data_and_options(segment.leaves, {})
1803                        spline_degree = options.get("SplineDegree", Integer(3))
1804                        if not isinstance(spline_degree, Integer):
1805                            raise BoxConstructError
1806                        k = spline_degree.get_int_value()
1807                    elif head == "System`BSplineCurve":
1808                        raise NotImplementedError  # FIXME convert bspline to bezier here
1809                        # parts = segment.leaves
1810                    else:
1811                        raise BoxConstructError
1812
1813                    coords = []
1814
1815                    for part in parts:
1816                        if part.get_head_name() != "System`List":
1817                            raise BoxConstructError
1818                        coords.extend(
1819                            [graphics.coords(graphics, xy) for xy in part.leaves]
1820                        )
1821
1822                    yield k, coords
1823
1824            if all(x.get_head_name() == "System`List" for x in leaves):
1825                self.components = [list(parse_component(x)) for x in leaves]
1826            else:
1827                self.components = [list(parse_component(leaves))]
1828        else:
1829            raise BoxConstructError
1830
1831    def to_svg(self, offset=None):
1832        l = self.style.get_line_width(face_element=False)
1833        style = create_css(
1834            edge_color=self.edge_color, face_color=self.face_color, stroke_width=l
1835        )
1836
1837        def components():
1838            for component in self.components:
1839                transformed = [(k, [xy.pos() for xy in p]) for k, p in component]
1840                yield " ".join(_svg_bezier(*transformed)) + " Z"
1841
1842        return '<path d="%s" style="%s" fill-rule="evenodd"/>' % (
1843            " ".join(components()),
1844            style,
1845        )
1846
1847    def to_asy(self):
1848        l = self.style.get_line_width(face_element=False)
1849        pen = create_pens(edge_color=self.edge_color, stroke_width=l)
1850
1851        if not pen:
1852            pen = "currentpen"
1853
1854        def components():
1855            for component in self.components:
1856                transformed = [(k, [xy.pos() for xy in p]) for k, p in component]
1857                yield "fill(%s--cycle, %s);" % ("".join(_asy_bezier(*transformed)), pen)
1858
1859        return "".join(components())
1860
1861    def extent(self):
1862        l = self.style.get_line_width(face_element=False)
1863        result = []
1864        for component in self.components:
1865            for _, points in component:
1866                for p in points:
1867                    x, y = p.pos()
1868                    result.extend(
1869                        [(x - l, y - l), (x - l, y + l), (x + l, y - l), (x + l, y + l)]
1870                    )
1871        return result
1872
1873
1874class Polygon(Builtin):
1875    """
1876    <dl>
1877    <dt>'Polygon[{$point_1$, $point_2$ ...}]'
1878        <dd>represents the filled polygon primitive.
1879    <dt>'Polygon[{{$p_11$, $p_12$, ...}, {$p_21$, $p_22$, ...}, ...}]'
1880        <dd>represents a number of filled polygon primitives.
1881    </dl>
1882
1883    >> Graphics[Polygon[{{1,0},{0,0},{0,1}}]]
1884    = -Graphics-
1885
1886    >> Graphics3D[Polygon[{{0,0,0},{0,1,1},{1,0,0}}]]
1887    = -Graphics3D-
1888    """
1889
1890    pass
1891
1892
1893class PolygonBox(_Polyline):
1894    def init(self, graphics, style, item=None):
1895        super(PolygonBox, self).init(graphics, item, style)
1896        self.edge_color, self.face_color = style.get_style(_Color, face_element=True)
1897        if item is not None:
1898            if len(item.leaves) not in (1, 2):
1899                raise BoxConstructError
1900            points = item.leaves[0]
1901            self.do_init(graphics, points)
1902            self.vertex_colors = None
1903            for leaf in item.leaves[1:]:
1904                if not leaf.has_form("Rule", 2):
1905                    raise BoxConstructError
1906                name = leaf.leaves[0].get_name()
1907                self.process_option(name, leaf.leaves[1])
1908        else:
1909            raise BoxConstructError
1910
1911    def process_option(self, name, value):
1912        if name == "System`VertexColors":
1913            if not value.has_form("List", None):
1914                raise BoxConstructError
1915            black = RGBColor(components=[0, 0, 0, 1])
1916            self.vertex_colors = [[black] * len(line) for line in self.lines]
1917            colors = value.leaves
1918            if not self.multi_parts:
1919                colors = [Expression(SymbolList, *colors)]
1920            for line_index, line in enumerate(self.lines):
1921                if line_index >= len(colors):
1922                    break
1923                line_colors = colors[line_index]
1924                if not line_colors.has_form("List", None):
1925                    continue
1926                for index, color in enumerate(line_colors.leaves):
1927                    if index >= len(self.vertex_colors[line_index]):
1928                        break
1929                    try:
1930                        self.vertex_colors[line_index][index] = _Color.create(color)
1931                    except ColorError:
1932                        continue
1933        else:
1934            raise BoxConstructError
1935
1936    def to_svg(self, offset=None):
1937        l = self.style.get_line_width(face_element=True)
1938        if self.vertex_colors is None:
1939            face_color = self.face_color
1940        else:
1941            face_color = None
1942        style = create_css(
1943            edge_color=self.edge_color, face_color=face_color, stroke_width=l
1944        )
1945        svg = ""
1946        if self.vertex_colors is not None:
1947            mesh = []
1948            for index, line in enumerate(self.lines):
1949                data = [
1950                    [coords.pos(), color.to_js()]
1951                    for coords, color in zip(line, self.vertex_colors[index])
1952                ]
1953                mesh.append(data)
1954            svg += '<meshgradient data="%s" />' % json.dumps(mesh)
1955        for line in self.lines:
1956            svg += '<polygon points="%s" style="%s" />' % (
1957                " ".join("%f,%f" % coords.pos() for coords in line),
1958                style,
1959            )
1960        return svg
1961
1962    def to_asy(self):
1963        l = self.style.get_line_width(face_element=True)
1964        if self.vertex_colors is None:
1965            face_color = self.face_color
1966        else:
1967            face_color = None
1968        pens = create_pens(
1969            edge_color=self.edge_color,
1970            face_color=face_color,
1971            stroke_width=l,
1972            is_face_element=True,
1973        )
1974        asy = ""
1975        if self.vertex_colors is not None:
1976            paths = []
1977            colors = []
1978            edges = []
1979            for index, line in enumerate(self.lines):
1980                paths.append(
1981                    "--".join(["(%.5g,%.5g)" % coords.pos() for coords in line])
1982                    + "--cycle"
1983                )
1984
1985                # ignore opacity
1986                colors.append(
1987                    ",".join([color.to_asy()[0] for color in self.vertex_colors[index]])
1988                )
1989
1990                edges.append(
1991                    ",".join(["0"] + ["1"] * (len(self.vertex_colors[index]) - 1))
1992                )
1993
1994            asy += "gouraudshade(%s, new pen[] {%s}, new int[] {%s});" % (
1995                "^^".join(paths),
1996                ",".join(colors),
1997                ",".join(edges),
1998            )
1999        if pens and pens != "nullpen":
2000            for line in self.lines:
2001                path = (
2002                    "--".join(["(%.5g,%.5g)" % coords.pos() for coords in line])
2003                    + "--cycle"
2004                )
2005                asy += "filldraw(%s, %s);" % (path, pens)
2006        return asy
2007
2008
2009class RegularPolygon(Builtin):
2010    """
2011    <dl>
2012    <dt>'RegularPolygon[$n$]'
2013        <dd>gives the regular polygon with $n$ edges.
2014    <dt>'RegularPolygon[$r$, $n$]'
2015        <dd>gives the regular polygon with $n$ edges and radius $r$.
2016    <dt>'RegularPolygon[{$r$, $phi$}, $n$]'
2017        <dd>gives the regular polygon with radius $r$ with one vertex drawn at angle $phi$.
2018    <dt>'RegularPolygon[{$x, $y}, $r$, $n$]'
2019        <dd>gives the regular polygon centered at the position {$x, $y}.
2020    </dl>
2021
2022    >> Graphics[RegularPolygon[5]]
2023    = -Graphics-
2024
2025    >> Graphics[{Yellow, Rectangle[], Orange, RegularPolygon[{1, 1}, {0.25, 0}, 3]}]
2026    = -Graphics-
2027    """
2028
2029
2030class RegularPolygonBox(PolygonBox):
2031    def init(self, graphics, style, item):
2032        if len(item.leaves) in (1, 2, 3) and isinstance(item.leaves[-1], Integer):
2033            r = 1.0
2034            phi0 = None
2035
2036            if len(item.leaves) >= 2:
2037                rspec = item.leaves[-2]
2038                if rspec.get_head_name() == "System`List":
2039                    if len(rspec.leaves) != 2:
2040                        raise BoxConstructError
2041                    r = rspec.leaves[0].round_to_float()
2042                    phi0 = rspec.leaves[1].round_to_float()
2043                else:
2044                    r = rspec.round_to_float()
2045
2046            x = 0.0
2047            y = 0.0
2048            if len(item.leaves) == 3:
2049                pos = item.leaves[0]
2050                if not pos.has_form("List", 2):
2051                    raise BoxConstructError
2052                x = pos.leaves[0].round_to_float()
2053                y = pos.leaves[1].round_to_float()
2054
2055            n = item.leaves[-1].get_int_value()
2056
2057            if any(t is None for t in (x, y, r)) or n < 0:
2058                raise BoxConstructError
2059
2060            if phi0 is None:
2061                phi0 = -pi / 2.0
2062                if n % 1 == 0 and n > 0:
2063                    phi0 += pi / n
2064
2065            pi2 = pi * 2.0
2066
2067            def vertices():
2068                for i in range(n):
2069                    phi = phi0 + pi2 * i / float(n)
2070                    yield Expression(
2071                        "List", Real(x + r * cos(phi)), Real(y + r * sin(phi))
2072                    )
2073
2074            new_item = Expression(
2075                "RegularPolygonBox", Expression(SymbolList, *list(vertices()))
2076            )
2077        else:
2078            raise BoxConstructError
2079
2080        super(RegularPolygonBox, self).init(graphics, style, new_item)
2081
2082
2083class Arrow(Builtin):
2084    """
2085    <dl>
2086    <dt>'Arrow[{$p1$, $p2$}]'
2087        <dd>represents a line from $p1$ to $p2$ that ends with an arrow at $p2$.
2088    <dt>'Arrow[{$p1$, $p2$}, $s$]'
2089        <dd>represents a line with arrow that keeps a distance of $s$ from $p1$
2090        and $p2$.
2091    <dt>'Arrow[{$point_1$, $point_2$}, {$s1$, $s2$}]'
2092        <dd>represents a line with arrow that keeps a distance of $s1$ from $p1$
2093        and a distance of $s2$ from $p2$.
2094    </dl>
2095
2096    >> Graphics[Arrow[{{0,0}, {1,1}}]]
2097    = -Graphics-
2098
2099    >> Graphics[{Circle[], Arrow[{{2, 1}, {0, 0}}, 1]}]
2100    = -Graphics-
2101
2102    Keeping distances may happen across multiple segments:
2103
2104    >> Table[Graphics[{Circle[], Arrow[Table[{Cos[phi],Sin[phi]},{phi,0,2*Pi,Pi/2}],{d, d}]}],{d,0,2,0.5}]
2105     = {-Graphics-, -Graphics-, -Graphics-, -Graphics-, -Graphics-}
2106    """
2107
2108    pass
2109
2110
2111class Arrowheads(_GraphicsElement):
2112    """
2113    <dl>
2114    <dt>'Arrowheads[$s$]'
2115        <dd>specifies that Arrow[] draws one arrow of size $s$ (relative to width of image, defaults to 0.04).
2116    <dt>'Arrowheads[{$spec1$, $spec2$, ..., $specn$}]'
2117        <dd>specifies that Arrow[] draws n arrows as defined by $spec1$, $spec2$, ... $specn$.
2118    <dt>'Arrowheads[{{$s$}}]'
2119        <dd>specifies that one arrow of size $s$ should be drawn.
2120    <dt>'Arrowheads[{{$s$, $pos$}}]'
2121        <dd>specifies that one arrow of size $s$ should be drawn at position $pos$ (for the arrow to
2122        be on the line, $pos$ has to be between 0, i.e. the start for the line, and 1, i.e. the end
2123        of the line).
2124    <dt>'Arrowheads[{{$s$, $pos$, $g$}}]'
2125        <dd>specifies that one arrow of size $s$ should be drawn at position $pos$ using Graphics $g$.
2126    </dl>
2127
2128    Arrows on both ends can be achieved using negative sizes:
2129
2130    >> Graphics[{Circle[],Arrowheads[{-0.04, 0.04}], Arrow[{{0, 0}, {2, 2}}, {1,1}]}]
2131     = -Graphics-
2132
2133    You may also specify our own arrow shapes:
2134
2135    >> Graphics[{Circle[], Arrowheads[{{0.04, 1, Graphics[{Red, Disk[]}]}}], Arrow[{{0, 0}, {Cos[Pi/3],Sin[Pi/3]}}]}]
2136     = -Graphics-
2137
2138    >> Graphics[{Arrowheads[Table[{0.04, i/10, Graphics[Disk[]]},{i,1,10}]], Arrow[{{0, 0}, {6, 5}, {1, -3}, {-2, 2}}]}]
2139     = -Graphics-
2140    """
2141
2142    default_size = 0.04
2143
2144    symbolic_sizes = {
2145        "System`Tiny": 3,
2146        "System`Small": 5,
2147        "System`Medium": 9,
2148        "System`Large": 18,
2149    }
2150
2151    def init(self, graphics, item=None):
2152        super(Arrowheads, self).init(graphics, item)
2153        if len(item.leaves) != 1:
2154            raise BoxConstructError
2155        self.spec = item.leaves[0]
2156
2157    def _arrow_size(self, s, extent):
2158        if isinstance(s, Symbol):
2159            size = self.symbolic_sizes.get(s.get_name(), 0)
2160            return self.graphics.translate_absolute((size, 0))[0]
2161        else:
2162            return _to_float(s) * extent
2163
2164    def heads(self, extent, default_arrow, custom_arrow):
2165        # see https://reference.wolfram.com/language/ref/Arrowheads.html
2166
2167        if self.spec.get_head_name() == "System`List":
2168            leaves = self.spec.leaves
2169            if all(x.get_head_name() == "System`List" for x in leaves):
2170                for head in leaves:
2171                    spec = head.leaves
2172                    if len(spec) not in (2, 3):
2173                        raise BoxConstructError
2174                    size_spec = spec[0]
2175                    if (
2176                        isinstance(size_spec, Symbol)
2177                        and size_spec.get_name() == "System`Automatic"
2178                    ):
2179                        s = self.default_size * extent
2180                    elif size_spec.is_numeric():
2181                        s = self._arrow_size(size_spec, extent)
2182                    else:
2183                        raise BoxConstructError
2184
2185                    if len(spec) == 3 and custom_arrow:
2186                        graphics = spec[2]
2187                        if graphics.get_head_name() != "System`Graphics":
2188                            raise BoxConstructError
2189                        arrow = custom_arrow(graphics)
2190                    else:
2191                        arrow = default_arrow
2192
2193                    if not isinstance(spec[1], (Real, Rational, Integer)):
2194                        raise BoxConstructError
2195
2196                    yield s, _to_float(spec[1]), arrow
2197            else:
2198                n = max(1.0, len(leaves) - 1.0)
2199                for i, head in enumerate(leaves):
2200                    yield self._arrow_size(head, extent), i / n, default_arrow
2201        else:
2202            yield self._arrow_size(self.spec, extent), 1, default_arrow
2203
2204
2205def _norm(p, q):
2206    px, py = p
2207    qx, qy = q
2208
2209    dx = qx - px
2210    dy = qy - py
2211
2212    length = sqrt(dx * dx + dy * dy)
2213    return dx, dy, length
2214
2215
2216class _Line:
2217    def make_draw_svg(self, style):
2218        def draw(points):
2219            yield '<polyline points="'
2220            yield " ".join("%f,%f" % xy for xy in points)
2221            yield '" style="%s" />' % style
2222
2223        return draw
2224
2225    def make_draw_asy(self, pen):
2226        def draw(points):
2227            yield "draw("
2228            yield "--".join(["(%.5g,%5g)" % xy for xy in points])
2229            yield ", % s);" % pen
2230
2231        return draw
2232
2233    def arrows(self, points, heads):  # heads has to be sorted by pos
2234        def segments(points):
2235            for i in range(len(points) - 1):
2236                px, py = points[i]
2237                dx, dy, dl = _norm((px, py), points[i + 1])
2238                yield dl, px, py, dx, dy
2239
2240        seg = list(segments(points))
2241
2242        if not seg:
2243            return
2244
2245        i = 0
2246        t0 = 0.0
2247        n = len(seg)
2248        dl, px, py, dx, dy = seg[i]
2249        total = sum(segment[0] for segment in seg)
2250
2251        for s, t, draw in ((s, pos * total - t0, draw) for s, pos, draw in heads):
2252            if s == 0.0:  # ignore zero-sized arrows
2253                continue
2254
2255            if i < n:  # not yet past last segment?
2256                while t > dl:  # position past current segment?
2257                    t -= dl
2258                    t0 += dl
2259                    i += 1
2260                    if i == n:
2261                        px += dx  # move to last segment's end
2262                        py += dy
2263                        break
2264                    else:
2265                        dl, px, py, dx, dy = seg[i]
2266
2267            for shape in draw(px, py, dx / dl, dy / dl, t, s):
2268                yield shape
2269
2270
2271def _bezier_derivative(p):
2272    # see http://pomax.github.io/bezierinfo/, Section 12 Derivatives
2273    n = len(p[0]) - 1
2274    return [[n * (x1 - x0) for x1, x0 in zip(w, w[1:])] for w in p]
2275
2276
2277def _bezier_evaluate(p, t):
2278    # see http://pomax.github.io/bezierinfo/, Section 4 Controlling Bezier Curvatures
2279    n = len(p[0]) - 1
2280    if n == 3:
2281        t2 = t * t
2282        t3 = t2 * t
2283        mt = 1 - t
2284        mt2 = mt * mt
2285        mt3 = mt2 * mt
2286        return [
2287            w[0] * mt3 + 3 * w[1] * mt2 * t + 3 * w[2] * mt * t2 + w[3] * t3 for w in p
2288        ]
2289    elif n == 2:
2290        t2 = t * t
2291        mt = 1 - t
2292        mt2 = mt * mt
2293        return [w[0] * mt2 + w[1] * 2 * mt * t + w[2] * t2 for w in p]
2294    elif n == 1:
2295        mt = 1 - t
2296        return [w[0] * mt + w[1] * t for w in p]
2297    else:
2298        raise ValueError("cannot compute bezier curve of order %d" % n)
2299
2300
2301class _BezierCurve:
2302    def __init__(self, spline_degree=3):
2303        self.spline_degree = spline_degree
2304
2305    def make_draw_svg(self, style):
2306        def draw(points):
2307            s = " ".join(_svg_bezier((self.spline_degree, points)))
2308            yield '<path d="%s" style="%s"/>' % (s, style)
2309
2310        return draw
2311
2312    def make_draw_asy(self, pen):
2313        def draw(points):
2314            for path in _asy_bezier((self.spline_degree, points)):
2315                yield "draw(%s, %s);" % (path, pen)
2316
2317        return draw
2318
2319    def arrows(self, points, heads):  # heads has to be sorted by pos
2320        if len(points) < 2:
2321            return
2322
2323        # FIXME combined curves
2324
2325        cp = list(zip(*points))
2326        if len(points) >= 3:
2327            dcp = _bezier_derivative(cp)
2328        else:
2329            dcp = cp
2330
2331        for s, t, draw in heads:
2332            if s == 0.0:  # ignore zero-sized arrows
2333                continue
2334
2335            px, py = _bezier_evaluate(cp, t)
2336
2337            tx, ty = _bezier_evaluate(dcp, t)
2338            tl = -sqrt(tx * tx + ty * ty)
2339            tx /= tl
2340            ty /= tl
2341
2342            for shape in draw(px, py, tx, ty, 0.0, s):
2343                yield shape
2344
2345
2346class ArrowBox(_Polyline):
2347    def init(self, graphics, style, item=None):
2348        if not item:
2349            raise BoxConstructError
2350
2351        super(ArrowBox, self).init(graphics, item, style)
2352
2353        leaves = item.leaves
2354        if len(leaves) == 2:
2355            setback = self._setback_spec(leaves[1])
2356        elif len(leaves) == 1:
2357            setback = (0, 0)
2358        else:
2359            raise BoxConstructError
2360
2361        curve = leaves[0]
2362
2363        curve_head_name = curve.get_head_name()
2364        if curve_head_name == "System`List":
2365            curve_points = curve
2366            self.curve = _Line()
2367        elif curve_head_name == "System`Line":
2368            if len(curve.leaves) != 1:
2369                raise BoxConstructError
2370            curve_points = curve.leaves[0]
2371            self.curve = _Line()
2372        elif curve_head_name == "System`BezierCurve":
2373            if len(curve.leaves) != 1:
2374                raise BoxConstructError
2375            curve_points = curve.leaves[0]
2376            self.curve = _BezierCurve()
2377        else:
2378            raise BoxConstructError
2379
2380        self.setback = setback
2381        self.do_init(graphics, curve_points)
2382        self.graphics = graphics
2383        self.edge_color, _ = style.get_style(_Color, face_element=False)
2384        self.heads, _ = style.get_style(Arrowheads, face_element=False)
2385
2386    @staticmethod
2387    def _setback_spec(expr):
2388        if expr.get_head_name() == "System`List":
2389            leaves = expr.leaves
2390            if len(leaves) != 2:
2391                raise BoxConstructError
2392            return tuple(max(_to_float(l), 0.0) for l in leaves)
2393        else:
2394            s = max(_to_float(expr), 0.0)
2395            return s, s
2396
2397    @staticmethod
2398    def _default_arrow(polygon):
2399        # the default arrow drawn by draw() below looks looks like this:
2400        #
2401        #       H
2402        #      .:.
2403        #     . : .
2404        #    .  :  .
2405        #   .  .B.  .
2406        #  . .  :  . .
2407        # S.    E    .S
2408        #       :
2409        #       :
2410        #       :
2411        #
2412        # the head H is where the arrow's point is. at base B, the arrow spreads out at right angles from the line
2413        # it attaches to. the arrow size 's' given in the Arrowheads specification always specifies the length H-B.
2414        #
2415        # the spread out points S are defined via two constants: arrow_edge (which defines the factor to get from
2416        # H-B to H-E) and arrow_spread (which defines the factor to get from H-B to E-S).
2417
2418        arrow_spread = 0.3
2419        arrow_edge = 1.1
2420
2421        def draw(px, py, vx, vy, t1, s):
2422            hx = px + t1 * vx  # compute H
2423            hy = py + t1 * vy
2424
2425            t0 = t1 - s
2426            bx = px + t0 * vx  # compute B
2427            by = py + t0 * vy
2428
2429            te = t1 - arrow_edge * s
2430            ex = px + te * vx  # compute E
2431            ey = py + te * vy
2432
2433            ts = arrow_spread * s
2434            sx = -vy * ts
2435            sy = vx * ts
2436
2437            head_points = ((hx, hy), (ex + sx, ey + sy), (bx, by), (ex - sx, ey - sy))
2438
2439            for shape in polygon(head_points):
2440                yield shape
2441
2442        return draw
2443
2444    def _draw(self, polyline, default_arrow, custom_arrow, extent):
2445        if self.heads:
2446            heads = list(self.heads.heads(extent, default_arrow, custom_arrow))
2447            heads = sorted(heads, key=lambda spec: spec[1])  # sort by pos
2448        else:
2449            heads = ((extent * Arrowheads.default_size, 1, default_arrow),)
2450
2451        def setback(p, q, d):
2452            dx, dy, length = _norm(p, q)
2453            if d >= length:
2454                return None, length
2455            else:
2456                s = d / length
2457                return (s * dx, s * dy), d
2458
2459        def shrink_one_end(line, s):
2460            while s > 0.0:
2461                if len(line) < 2:
2462                    return []
2463                xy, length = setback(line[0].p, line[1].p, s)
2464                if xy is not None:
2465                    line[0] = line[0].add(*xy)
2466                else:
2467                    line = line[1:]
2468                s -= length
2469            return line
2470
2471        def shrink(line, s1, s2):
2472            return list(
2473                reversed(
2474                    shrink_one_end(list(reversed(shrink_one_end(line[:], s1))), s2)
2475                )
2476            )
2477
2478        for line in self.lines:
2479            if len(line) < 2:
2480                continue
2481
2482            # note that shrinking needs to happen in the Graphics[] coordinate space, whereas the
2483            # subsequent position calculation needs to happen in pixel space.
2484
2485            transformed_points = [xy.pos() for xy in shrink(line, *self.setback)]
2486
2487            for s in polyline(transformed_points):
2488                yield s
2489
2490            for s in self.curve.arrows(transformed_points, heads):
2491                yield s
2492
2493    def _custom_arrow(self, format, format_transform):
2494        def make(graphics):
2495            xmin, xmax, ymin, ymax, ox, oy, ex, ey, code = _extract_graphics(
2496                graphics, format, self.graphics.evaluation
2497            )
2498            boxw = xmax - xmin
2499            boxh = ymax - ymin
2500
2501            def draw(px, py, vx, vy, t1, s):
2502                t0 = t1
2503                cx = px + t0 * vx
2504                cy = py + t0 * vy
2505
2506                transform = format_transform()
2507                transform.translate(cx, cy)
2508                transform.scale(-s / boxw * ex, -s / boxh * ey)
2509                transform.rotate(90 + degrees(atan2(vy, vx)))
2510                transform.translate(-ox, -oy)
2511                yield transform.apply(code)
2512
2513            return draw
2514
2515        return make
2516
2517    def to_svg(self, offset=None):
2518        width = self.style.get_line_width(face_element=False)
2519        style = create_css(edge_color=self.edge_color, stroke_width=width)
2520        polyline = self.curve.make_draw_svg(style)
2521
2522        arrow_style = create_css(face_color=self.edge_color, stroke_width=width)
2523
2524        def polygon(points):
2525            yield '<polygon points="'
2526            yield " ".join("%f,%f" % xy for xy in points)
2527            yield '" style="%s" />' % arrow_style
2528
2529        extent = self.graphics.view_width or 0
2530        default_arrow = self._default_arrow(polygon)
2531        custom_arrow = self._custom_arrow("svg", _SVGTransform)
2532        return "".join(self._draw(polyline, default_arrow, custom_arrow, extent))
2533
2534    def to_asy(self):
2535        width = self.style.get_line_width(face_element=False)
2536        pen = create_pens(edge_color=self.edge_color, stroke_width=width)
2537        polyline = self.curve.make_draw_asy(pen)
2538
2539        arrow_pen = create_pens(face_color=self.edge_color, stroke_width=width)
2540
2541        def polygon(points):
2542            yield "filldraw("
2543            yield "--".join(["(%.5g,%5g)" % xy for xy in points])
2544            yield "--cycle, % s);" % arrow_pen
2545
2546        extent = self.graphics.view_width or 0
2547        default_arrow = self._default_arrow(polygon)
2548        custom_arrow = self._custom_arrow("asy", _ASYTransform)
2549        return "".join(self._draw(polyline, default_arrow, custom_arrow, extent))
2550
2551    def extent(self):
2552        width = self.style.get_line_width(face_element=False)
2553
2554        def polyline(points):
2555            for p in points:
2556                x, y = p
2557                yield x - width, y - width
2558                yield x - width, y + width
2559                yield x + width, y - width
2560                yield x + width, y + width
2561
2562        def polygon(points):
2563            for p in points:
2564                yield p
2565
2566        def default_arrow(px, py, vx, vy, t1, s):
2567            yield px, py
2568
2569        return list(self._draw(polyline, default_arrow, None, 0))
2570
2571
2572class InsetBox(_GraphicsElement):
2573    def init(
2574        self,
2575        graphics,
2576        style,
2577        item=None,
2578        content=None,
2579        pos=None,
2580        opos=(0, 0),
2581        opacity=1.0,
2582    ):
2583        super(InsetBox, self).init(graphics, item, style)
2584
2585        self.color = self.style.get_option("System`FontColor")
2586        if self.color is None:
2587            self.color, _ = style.get_style(_Color, face_element=False)
2588        self.opacity = opacity
2589
2590        if item is not None:
2591            if len(item.leaves) not in (1, 2, 3):
2592                raise BoxConstructError
2593            content = item.leaves[0]
2594            self.content = content.format(graphics.evaluation, "TraditionalForm")
2595            if len(item.leaves) > 1:
2596                self.pos = Coords(graphics, item.leaves[1])
2597            else:
2598                self.pos = Coords(graphics, pos=(0, 0))
2599            if len(item.leaves) > 2:
2600                self.opos = coords(item.leaves[2])
2601            else:
2602                self.opos = (0, 0)
2603        else:
2604            self.content = content
2605            self.pos = pos
2606            self.opos = opos
2607        self.content_text = self.content.boxes_to_text(
2608            evaluation=self.graphics.evaluation
2609        )
2610
2611    def extent(self):
2612        p = self.pos.pos()
2613        h = 25
2614        w = len(self.content_text) * 7  # rough approximation by numbers of characters
2615        opos = self.opos
2616        x = p[0] - w / 2.0 - opos[0] * w / 2.0
2617        y = p[1] - h / 2.0 + opos[1] * h / 2.0
2618        return [(x, y), (x + w, y + h)]
2619
2620    def to_svg(self, offset=None):
2621        x, y = self.pos.pos()
2622        if offset:
2623            x = x + offset[0]
2624            y = y + offset[1]
2625
2626        if hasattr(self.content, "to_svg"):
2627            content = self.content.to_svg(noheader=True, offset=(x, y))
2628            svg = "\n" + content + "\n"
2629        else:
2630            css_style = create_css(
2631                font_color=self.color,
2632                edge_color=self.color,
2633                face_color=self.color,
2634                opacity=self.opacity,
2635            )
2636            text_pos_opts = f'x="{x}" y="{y}" ox="{self.opos[0]}" oy="{self.opos[1]}"'
2637            # FIXME: don't hard code text_style_opts, but allow these to be adjustable.
2638            text_style_opts = "text-anchor:middle; dominant-baseline:middle;"
2639            content = self.content.boxes_to_text(evaluation=self.graphics.evaluation)
2640            svg = f'<text {text_pos_opts} style="{text_style_opts} {css_style}">{content}</text>'
2641
2642        # content = self.content.boxes_to_mathml(evaluation=self.graphics.evaluation)
2643        # style = create_css(font_color=self.color)
2644        # svg = (
2645        #    '<foreignObject x="%f" y="%f" ox="%f" oy="%f" style="%s">'
2646        #    "<math>%s</math></foreignObject>")
2647
2648        return svg
2649
2650    def to_asy(self):
2651        x, y = self.pos.pos()
2652        content = self.content.boxes_to_tex(evaluation=self.graphics.evaluation)
2653        pen = create_pens(edge_color=self.color)
2654        asy = 'label("$%s$", (%s,%s), (%s,%s), %s);' % (
2655            content,
2656            x,
2657            y,
2658            -self.opos[0],
2659            -self.opos[1],
2660            pen,
2661        )
2662        return asy
2663
2664
2665def total_extent(extents):
2666    xmin = xmax = ymin = ymax = None
2667    for extent in extents:
2668        for x, y in extent:
2669            if xmin is None or x < xmin:
2670                xmin = x
2671            if xmax is None or x > xmax:
2672                xmax = x
2673            if ymin is None or y < ymin:
2674                ymin = y
2675            if ymax is None or y > ymax:
2676                ymax = y
2677    return xmin, xmax, ymin, ymax
2678
2679
2680class EdgeForm(Builtin):
2681    """
2682    >> Graphics[{EdgeForm[{Thick, Green}], Disk[]}]
2683     = -Graphics-
2684
2685    >> Graphics[{Style[Disk[],EdgeForm[{Thick,Red}]], Circle[{1,1}]}]
2686     = -Graphics-
2687    """
2688
2689    pass
2690
2691
2692class FaceForm(Builtin):
2693    pass
2694
2695
2696def _style(graphics, item):
2697    head = item.get_head_name()
2698    if head in style_heads:
2699        klass = get_class(head)
2700        style = klass.create_as_style(klass, graphics, item)
2701    elif head in ("System`EdgeForm", "System`FaceForm"):
2702        style = graphics.get_style_class()(
2703            graphics, edge=head == "System`EdgeForm", face=head == "System`FaceForm"
2704        )
2705        if len(item.leaves) > 1:
2706            raise BoxConstructError
2707        if item.leaves:
2708            if item.leaves[0].has_form("List", None):
2709                for dir in item.leaves[0].leaves:
2710                    style.append(dir, allow_forms=False)
2711            else:
2712                style.append(item.leaves[0], allow_forms=False)
2713    else:
2714        raise BoxConstructError
2715    return style
2716
2717
2718class Style(object):
2719    def __init__(self, graphics, edge=False, face=False):
2720        self.styles = []
2721        self.options = {}
2722        self.graphics = graphics
2723        self.edge = edge
2724        self.face = face
2725        self.klass = graphics.get_style_class()
2726
2727    def append(self, item, allow_forms=True):
2728        self.styles.append(_style(self.graphics, item))
2729
2730    def set_option(self, name, value):
2731        self.options[name] = value
2732
2733    def extend(self, style, pre=True):
2734        if pre:
2735            self.styles = style.styles + self.styles
2736        else:
2737            self.styles.extend(style.styles)
2738
2739    def clone(self):
2740        result = self.klass(self.graphics, edge=self.edge, face=self.face)
2741        result.styles = self.styles[:]
2742        result.options = self.options.copy()
2743        return result
2744
2745    def get_default_face_color(self):
2746        return RGBColor(components=(0, 0, 0, 1))
2747
2748    def get_default_edge_color(self):
2749        return RGBColor(components=(0, 0, 0, 1))
2750
2751    def get_style(
2752        self, style_class, face_element=None, default_to_faces=True, consider_forms=True
2753    ):
2754        if face_element is not None:
2755            default_to_faces = consider_forms = face_element
2756        edge_style = face_style = None
2757        if style_class == _Color:
2758            if default_to_faces:
2759                face_style = self.get_default_face_color()
2760            else:
2761                edge_style = self.get_default_edge_color()
2762        elif style_class == _Thickness:
2763            if not default_to_faces:
2764                edge_style = AbsoluteThickness(self.graphics, value=0.5)
2765        for item in self.styles:
2766            if isinstance(item, style_class):
2767                if default_to_faces:
2768                    face_style = item
2769                else:
2770                    edge_style = item
2771            elif isinstance(item, Style):
2772                if consider_forms:
2773                    if item.edge:
2774                        edge_style, _ = item.get_style(
2775                            style_class, default_to_faces=False, consider_forms=False
2776                        )
2777                    elif item.face:
2778                        _, face_style = item.get_style(
2779                            style_class, default_to_faces=True, consider_forms=False
2780                        )
2781
2782        return edge_style, face_style
2783
2784    def get_option(self, name):
2785        return self.options.get(name, None)
2786
2787    def get_line_width(self, face_element=True):
2788        if self.graphics.pixel_width is None:
2789            return 0
2790        edge_style, _ = self.get_style(
2791            _Thickness, default_to_faces=face_element, consider_forms=face_element
2792        )
2793        if edge_style is None:
2794            return 0
2795        return edge_style.get_thickness()
2796
2797
2798def _flatten(leaves):
2799    for leaf in leaves:
2800        if leaf.get_head_name() == "System`List":
2801            flattened = leaf.flatten(Symbol("List"))
2802            if flattened.get_head_name() == "System`List":
2803                for x in flattened.leaves:
2804                    yield x
2805            else:
2806                yield flattened
2807        else:
2808            yield leaf
2809
2810
2811class _GraphicsElements(object):
2812    def __init__(self, content, evaluation):
2813        self.evaluation = evaluation
2814        self.elements = []
2815
2816        builtins = evaluation.definitions.builtin
2817
2818        def get_options(name):
2819            builtin = builtins.get(name)
2820            if builtin is None:
2821                return None
2822            return builtin.options
2823
2824        def stylebox_style(style, specs):
2825            new_style = style.clone()
2826            for spec in _flatten(specs):
2827                head_name = spec.get_head_name()
2828                if head_name in style_and_form_heads:
2829                    new_style.append(spec)
2830                elif head_name == "System`Rule" and len(spec.leaves) == 2:
2831                    option, expr = spec.leaves
2832                    if not isinstance(option, Symbol):
2833                        raise BoxConstructError
2834
2835                    name = option.get_name()
2836                    create = style_options.get(name, None)
2837                    if create is None:
2838                        raise BoxConstructError
2839
2840                    new_style.set_option(name, create(style.graphics, expr))
2841                else:
2842                    raise BoxConstructError
2843            return new_style
2844
2845        def convert(content, style):
2846            if content.has_form("List", None):
2847                items = content.leaves
2848            else:
2849                items = [content]
2850            style = style.clone()
2851            for item in items:
2852                if item.get_name() == "System`Null":
2853                    continue
2854                head = item.get_head_name()
2855                if head in style_and_form_heads:
2856                    style.append(item)
2857                elif head == "System`StyleBox":
2858                    if len(item.leaves) < 1:
2859                        raise BoxConstructError
2860                    for element in convert(
2861                        item.leaves[0], stylebox_style(style, item.leaves[1:])
2862                    ):
2863                        yield element
2864                elif head[-3:] == "Box":  # and head[:-3] in element_heads:
2865                    element_class = get_class(head)
2866                    if element_class is not None:
2867                        options = get_options(head[:-3])
2868                        if options:
2869                            data, options = _data_and_options(item.leaves, options)
2870                            new_item = Expression(head, *data)
2871                            element = get_class(head)(self, style, new_item, options)
2872                        else:
2873                            element = get_class(head)(self, style, item)
2874                        yield element
2875                    else:
2876                        raise BoxConstructError
2877                elif head == "System`List":
2878                    for element in convert(item, style):
2879                        yield element
2880                else:
2881                    raise BoxConstructError
2882
2883        self.elements = list(convert(content, self.get_style_class()(self)))
2884
2885    def create_style(self, expr):
2886        style = self.get_style_class()(self)
2887
2888        def convert(expr):
2889            if expr.has_form(("List", "Directive"), None):
2890                for item in expr.leaves:
2891                    convert(item)
2892            else:
2893                style.append(expr)
2894
2895        convert(expr)
2896        return style
2897
2898    def get_style_class(self):
2899        return Style
2900
2901
2902class GraphicsElements(_GraphicsElements):
2903    coords = Coords
2904
2905    def __init__(self, content, evaluation, neg_y=False):
2906        super(GraphicsElements, self).__init__(content, evaluation)
2907        self.neg_y = neg_y
2908        self.xmin = self.ymin = self.pixel_width = None
2909        self.pixel_height = self.extent_width = self.extent_height = None
2910        self.view_width = None
2911
2912    def translate(self, coords):
2913        if self.pixel_width is not None:
2914            w = self.extent_width if self.extent_width > 0 else 1
2915            h = self.extent_height if self.extent_height > 0 else 1
2916            result = [
2917                (coords[0] - self.xmin) * self.pixel_width / w,
2918                (coords[1] - self.ymin) * self.pixel_height / h,
2919            ]
2920            if self.neg_y:
2921                result[1] = self.pixel_height - result[1]
2922            return tuple(result)
2923        else:
2924            return (coords[0], coords[1])
2925
2926    def translate_absolute(self, d):
2927        if self.pixel_width is None:
2928            return (0, 0)
2929        else:
2930            l = 96.0 / 72
2931            return (d[0] * l, (-1 if self.neg_y else 1) * d[1] * l)
2932
2933    def translate_relative(self, x):
2934        if self.pixel_width is None:
2935            return 0
2936        else:
2937            return x * self.pixel_width
2938
2939    def extent(self, completely_visible_only=False):
2940        if completely_visible_only:
2941            ext = total_extent(
2942                [
2943                    element.extent()
2944                    for element in self.elements
2945                    if element.is_completely_visible
2946                ]
2947            )
2948        else:
2949            ext = total_extent([element.extent() for element in self.elements])
2950        xmin, xmax, ymin, ymax = ext
2951        if xmin == xmax:
2952            if xmin is None:
2953                return 0, 0, 0, 0
2954            xmin = 0
2955            xmax *= 2
2956        if ymin == ymax:
2957            if ymin is None:
2958                return 0, 0, 0, 0
2959            ymin = 0
2960            ymax *= 2
2961        return xmin, xmax, ymin, ymax
2962
2963    def to_svg(self, offset=None):
2964        return "\n".join(element.to_svg(offset) for element in self.elements)
2965
2966    def to_asy(self):
2967        return "\n".join(element.to_asy() for element in self.elements)
2968
2969    def set_size(
2970        self, xmin, ymin, extent_width, extent_height, pixel_width, pixel_height
2971    ):
2972
2973        self.xmin, self.ymin = xmin, ymin
2974        self.extent_width, self.extent_height = extent_width, extent_height
2975        self.pixel_width, self.pixel_height = pixel_width, pixel_height
2976
2977
2978class GraphicsBox(BoxConstruct):
2979    options = Graphics.options
2980
2981    attributes = ("HoldAll", "ReadProtected")
2982
2983    def __new__(cls, *leaves, **kwargs):
2984        instance = super().__new__(cls, *leaves, **kwargs)
2985        instance.evaluation = kwargs.get("evaluation", None)
2986        return instance
2987
2988    def boxes_to_text(self, leaves=None, **options):
2989        if not leaves:
2990            leaves = self._leaves
2991
2992        self._prepare_elements(leaves, options)  # to test for Box errors
2993        return "-Graphics-"
2994
2995    def _get_image_size(self, options, graphics_options, max_width):
2996        inside_row = options.pop("inside_row", False)
2997        inside_list = options.pop("inside_list", False)
2998        image_size_multipliers = options.pop("image_size_multipliers", None)
2999
3000        aspect_ratio = graphics_options["System`AspectRatio"]
3001
3002        if image_size_multipliers is None:
3003            image_size_multipliers = (0.5, 0.25)
3004
3005        if aspect_ratio == Symbol("Automatic"):
3006            aspect = None
3007        else:
3008            aspect = aspect_ratio.round_to_float()
3009
3010        image_size = graphics_options["System`ImageSize"]
3011        if isinstance(image_size, Integer):
3012            base_width = image_size.get_int_value()
3013            base_height = None  # will be computed later in calc_dimensions
3014        elif image_size.has_form("System`List", 2):
3015            base_width, base_height = (
3016                [x.round_to_float() for x in image_size.leaves] + [0, 0]
3017            )[:2]
3018            if base_width is None or base_height is None:
3019                raise BoxConstructError
3020            aspect = base_height / base_width
3021        else:
3022            image_size = image_size.get_name()
3023            base_width, base_height = {
3024                "System`Automatic": (400, 350),
3025                "System`Tiny": (100, 100),
3026                "System`Small": (200, 200),
3027                "System`Medium": (400, 350),
3028                "System`Large": (600, 500),
3029            }.get(image_size, (None, None))
3030        if base_width is None:
3031            raise BoxConstructError
3032        if max_width is not None and base_width > max_width:
3033            base_width = max_width
3034
3035        if inside_row:
3036            multi = image_size_multipliers[1]
3037        elif inside_list:
3038            multi = image_size_multipliers[0]
3039        else:
3040            multi = 1
3041
3042        return base_width, base_height, multi, aspect
3043
3044    def _prepare_elements(self, leaves, options, neg_y=False, max_width=None):
3045        if not leaves:
3046            raise BoxConstructError
3047        graphics_options = self.get_option_values(leaves[1:], **options)
3048        background = graphics_options["System`Background"]
3049        if (
3050            isinstance(background, Symbol)
3051            and background.get_name() == "System`Automatic"
3052        ):
3053            self.background_color = None
3054        else:
3055            self.background_color = _Color.create(background)
3056
3057        base_width, base_height, size_multiplier, size_aspect = self._get_image_size(
3058            options, graphics_options, max_width
3059        )
3060
3061        plot_range = graphics_options["System`PlotRange"].to_python()
3062        if plot_range == "System`Automatic":
3063            plot_range = ["System`Automatic", "System`Automatic"]
3064
3065        if not isinstance(plot_range, list) or len(plot_range) != 2:
3066            raise BoxConstructError
3067
3068        evaluation = options.get("evaluation", None)
3069        if evaluation is None:
3070            evaluation = self.evaluation
3071        elements = GraphicsElements(leaves[0], evaluation, neg_y)
3072        axes = []  # to be filled further down
3073
3074        def calc_dimensions(final_pass=True):
3075            """
3076            calc_dimensions gets called twice: In the first run
3077            (final_pass = False, called inside _prepare_elements), the extent
3078            of all user-defined graphics is determined.
3079            Axes are created accordingly.
3080            In the second run (final_pass = True, called from outside),
3081            the dimensions of these axes are taken into account as well.
3082            This is also important to size absolutely sized objects correctly
3083            (e.g. values using AbsoluteThickness).
3084            """
3085
3086            # always need to compute extent if size aspect is automatic
3087            if "System`Automatic" in plot_range or size_aspect is None:
3088                xmin, xmax, ymin, ymax = elements.extent()
3089            else:
3090                xmin = xmax = ymin = ymax = None
3091
3092            if (
3093                final_pass
3094                and any(x for x in axes)
3095                and plot_range != ["System`Automatic", "System`Automatic"]
3096            ):
3097                # Take into account the dimensions of axes and axes labels
3098                # (they should be displayed completely even when a specific
3099                # PlotRange is given).
3100                exmin, exmax, eymin, eymax = elements.extent(
3101                    completely_visible_only=True
3102                )
3103            else:
3104                exmin = exmax = eymin = eymax = None
3105
3106            def get_range(min, max):
3107                if max < min:
3108                    min, max = max, min
3109                elif min == max:
3110                    if min < 0:
3111                        min, max = 2 * min, 0
3112                    elif min > 0:
3113                        min, max = 0, 2 * min
3114                    else:
3115                        min, max = -1, 1
3116                return min, max
3117
3118            try:
3119                if plot_range[0] == "System`Automatic":
3120                    if xmin is None and xmax is None:
3121                        xmin = 0
3122                        xmax = 1
3123                    elif xmin == xmax:
3124                        xmin -= 1
3125                        xmax += 1
3126                elif isinstance(plot_range[0], list) and len(plot_range[0]) == 2:
3127                    xmin, xmax = list(map(float, plot_range[0]))
3128                    xmin, xmax = get_range(xmin, xmax)
3129                    xmin = elements.translate((xmin, 0))[0]
3130                    xmax = elements.translate((xmax, 0))[0]
3131                    if exmin is not None and exmin < xmin:
3132                        xmin = exmin
3133                    if exmax is not None and exmax > xmax:
3134                        xmax = exmax
3135                else:
3136                    raise BoxConstructError
3137
3138                if plot_range[1] == "System`Automatic":
3139                    if ymin is None and ymax is None:
3140                        ymin = 0
3141                        ymax = 1
3142                    elif ymin == ymax:
3143                        ymin -= 1
3144                        ymax += 1
3145                elif isinstance(plot_range[1], list) and len(plot_range[1]) == 2:
3146                    ymin, ymax = list(map(float, plot_range[1]))
3147                    ymin, ymax = get_range(ymin, ymax)
3148                    ymin = elements.translate((0, ymin))[1]
3149                    ymax = elements.translate((0, ymax))[1]
3150                    if ymin > ymax:
3151                        ymin, ymax = ymax, ymin
3152                    if eymin is not None and eymin < ymin:
3153                        ymin = eymin
3154                    if eymax is not None and eymax > ymax:
3155                        ymax = eymax
3156                else:
3157                    raise BoxConstructError
3158            except (ValueError, TypeError):
3159                raise BoxConstructError
3160
3161            w = 0 if (xmin is None or xmax is None) else xmax - xmin
3162            h = 0 if (ymin is None or ymax is None) else ymax - ymin
3163
3164            if size_aspect is None:
3165                aspect = h / w
3166            else:
3167                aspect = size_aspect
3168
3169            height = base_height
3170            if height is None:
3171                height = base_width * aspect
3172            width = height / aspect
3173            if width > base_width:
3174                width = base_width
3175                height = width * aspect
3176            height = height
3177
3178            width *= size_multiplier
3179            height *= size_multiplier
3180
3181            return xmin, xmax, ymin, ymax, w, h, width, height
3182
3183        xmin, xmax, ymin, ymax, w, h, width, height = calc_dimensions(final_pass=False)
3184
3185        elements.set_size(xmin, ymin, w, h, width, height)
3186
3187        xmin -= w * 0.02
3188        xmax += w * 0.02
3189        ymin -= h * 0.02
3190        ymax += h * 0.02
3191
3192        axes.extend(
3193            self.create_axes(elements, graphics_options, xmin, xmax, ymin, ymax)
3194        )
3195
3196        return elements, calc_dimensions
3197
3198    def boxes_to_tex(self, leaves=None, **options):
3199        if not leaves:
3200            leaves = self._leaves
3201        elements, calc_dimensions = self._prepare_elements(
3202            leaves, options, max_width=450
3203        )
3204
3205        xmin, xmax, ymin, ymax, w, h, width, height = calc_dimensions()
3206        elements.view_width = w
3207
3208        asy_completely_visible = "\n".join(
3209            element.to_asy()
3210            for element in elements.elements
3211            if element.is_completely_visible
3212        )
3213
3214        asy_regular = "\n".join(
3215            element.to_asy()
3216            for element in elements.elements
3217            if not element.is_completely_visible
3218        )
3219
3220        asy_box = "box((%s,%s), (%s,%s))" % (
3221            asy_number(xmin),
3222            asy_number(ymin),
3223            asy_number(xmax),
3224            asy_number(ymax),
3225        )
3226
3227        if self.background_color is not None:
3228            color, opacity = self.background_color.to_asy()
3229            asy_background = "filldraw(%s, %s);" % (asy_box, color)
3230        else:
3231            asy_background = ""
3232
3233        tex = r"""
3234\begin{asy}
3235usepackage("amsmath");
3236size(%scm, %scm);
3237%s
3238%s
3239clip(%s);
3240%s
3241\end{asy}
3242""" % (
3243            asy_number(width / 60),
3244            asy_number(height / 60),
3245            asy_background,
3246            asy_regular,
3247            asy_box,
3248            asy_completely_visible,
3249        )
3250
3251        return tex
3252
3253    def to_svg(self, leaves=None, **options):
3254        if not leaves:
3255            leaves = self._leaves
3256
3257        data = options.get("data", None)
3258        if data:
3259            elements, xmin, xmax, ymin, ymax, w, h, width, height = data
3260        else:
3261            elements, calc_dimensions = self._prepare_elements(
3262                leaves, options, neg_y=True
3263            )
3264            xmin, xmax, ymin, ymax, w, h, width, height = calc_dimensions()
3265
3266        elements.view_width = w
3267
3268        svg = elements.to_svg(offset=options.get("offset", None))
3269
3270        if self.background_color is not None:
3271            svg = '<rect x="%f" y="%f" width="%f" height="%f" style="fill:%s"/>%s' % (
3272                xmin,
3273                ymin,
3274                w,
3275                h,
3276                self.background_color.to_css()[0],
3277                svg,
3278            )
3279
3280        xmin -= 1
3281        ymin -= 1
3282        w += 2
3283        h += 2
3284
3285        if options.get("noheader", False):
3286            return svg
3287        svg_xml = """
3288            <svg xmlns:svg="http://www.w3.org/2000/svg"
3289                xmlns="http://www.w3.org/2000/svg"
3290                version="1.1"
3291                viewBox="%s">
3292                %s
3293            </svg>
3294        """ % (
3295            " ".join("%f" % t for t in (xmin, ymin, w, h)),
3296            svg,
3297        )
3298        return svg_xml  # , width, height
3299
3300    def boxes_to_mathml(self, leaves=None, **options):
3301        if not leaves:
3302            leaves = self._leaves
3303
3304        elements, calc_dimensions = self._prepare_elements(leaves, options, neg_y=True)
3305        xmin, xmax, ymin, ymax, w, h, width, height = calc_dimensions()
3306        data = (elements, xmin, xmax, ymin, ymax, w, h, width, height)
3307
3308        svg_xml = self.to_svg(leaves, data=data, **options)
3309        # mglyph, which is what we have been using, is bad because MathML standard changed.
3310        # metext does not work because the way in which we produce the svg images is also based on this outdated mglyph behaviour.
3311        # template = '<mtext width="%dpx" height="%dpx"><img width="%dpx" height="%dpx" src="data:image/svg+xml;base64,%s"/></mtext>'
3312        template = (
3313            '<mglyph width="%dpx" height="%dpx" src="data:image/svg+xml;base64,%s"/>'
3314            #'<mglyph  src="data:image/svg+xml;base64,%s"/>'
3315        )
3316        return template % (
3317            #        int(width),
3318            #        int(height),
3319            int(width),
3320            int(height),
3321            base64.b64encode(svg_xml.encode("utf8")).decode("utf8"),
3322        )
3323
3324    def axis_ticks(self, xmin, xmax):
3325        def round_to_zero(value):
3326            if value == 0:
3327                return 0
3328            elif value < 0:
3329                return ceil(value)
3330            else:
3331                return floor(value)
3332
3333        def round_step(value):
3334            if not value:
3335                return 1, 1
3336            sub_steps = 5
3337            try:
3338                shift = 10.0 ** floor(log10(value))
3339            except ValueError:
3340                return 1, 1
3341            value = value / shift
3342            if value < 1.5:
3343                value = 1
3344            elif value < 3:
3345                value = 2
3346                sub_steps = 4
3347            elif value < 8:
3348                value = 5
3349            else:
3350                value = 10
3351            return value * shift, sub_steps
3352
3353        step_x, sub_x = round_step((xmax - xmin) / 5.0)
3354        step_x_small = step_x / sub_x
3355        steps_x = int(floor((xmax - xmin) / step_x))
3356        steps_x_small = int(floor((xmax - xmin) / step_x_small))
3357
3358        start_k_x = int(ceil(xmin / step_x))
3359        start_k_x_small = int(ceil(xmin / step_x_small))
3360
3361        if xmin <= 0 <= xmax:
3362            origin_k_x = 0
3363        else:
3364            origin_k_x = start_k_x
3365        origin_x = origin_k_x * step_x
3366
3367        ticks = []
3368        ticks_small = []
3369        for k in range(start_k_x, start_k_x + steps_x + 1):
3370            if k != origin_k_x:
3371                x = k * step_x
3372                if x > xmax:
3373                    break
3374                ticks.append(x)
3375        for k in range(start_k_x_small, start_k_x_small + steps_x_small + 1):
3376            if k % sub_x != 0:
3377                x = k * step_x_small
3378                if x > xmax:
3379                    break
3380                ticks_small.append(x)
3381
3382        return ticks, ticks_small, origin_x
3383
3384    def create_axes(self, elements, graphics_options, xmin, xmax, ymin, ymax):
3385        axes = graphics_options.get("System`Axes")
3386        if axes.is_true():
3387            axes = (True, True)
3388        elif axes.has_form("List", 2):
3389            axes = (axes.leaves[0].is_true(), axes.leaves[1].is_true())
3390        else:
3391            axes = (False, False)
3392        ticks_style = graphics_options.get("System`TicksStyle")
3393        axes_style = graphics_options.get("System`AxesStyle")
3394        label_style = graphics_options.get("System`LabelStyle")
3395        if ticks_style.has_form("List", 2):
3396            ticks_style = ticks_style.leaves
3397        else:
3398            ticks_style = [ticks_style] * 2
3399        if axes_style.has_form("List", 2):
3400            axes_style = axes_style.leaves
3401        else:
3402            axes_style = [axes_style] * 2
3403
3404        ticks_style = [elements.create_style(s) for s in ticks_style]
3405        axes_style = [elements.create_style(s) for s in axes_style]
3406        label_style = elements.create_style(label_style)
3407        ticks_style[0].extend(axes_style[0])
3408        ticks_style[1].extend(axes_style[1])
3409
3410        def add_element(element):
3411            element.is_completely_visible = True
3412            elements.elements.append(element)
3413
3414        ticks_x, ticks_x_small, origin_x = self.axis_ticks(xmin, xmax)
3415        ticks_y, ticks_y_small, origin_y = self.axis_ticks(ymin, ymax)
3416
3417        axes_extra = 6
3418        tick_small_size = 3
3419        tick_large_size = 5
3420        tick_label_d = 2
3421
3422        ticks_x_int = all(floor(x) == x for x in ticks_x)
3423        ticks_y_int = all(floor(x) == x for x in ticks_y)
3424
3425        for (
3426            index,
3427            (min, max, p_self0, p_other0, p_origin, ticks, ticks_small, ticks_int),
3428        ) in enumerate(
3429            [
3430                (
3431                    xmin,
3432                    xmax,
3433                    lambda y: (0, y),
3434                    lambda x: (x, 0),
3435                    lambda x: (x, origin_y),
3436                    ticks_x,
3437                    ticks_x_small,
3438                    ticks_x_int,
3439                ),
3440                (
3441                    ymin,
3442                    ymax,
3443                    lambda x: (x, 0),
3444                    lambda y: (0, y),
3445                    lambda y: (origin_x, y),
3446                    ticks_y,
3447                    ticks_y_small,
3448                    ticks_y_int,
3449                ),
3450            ]
3451        ):
3452            if axes[index]:
3453                add_element(
3454                    LineBox(
3455                        elements,
3456                        axes_style[index],
3457                        lines=[
3458                            [
3459                                Coords(
3460                                    elements, pos=p_origin(min), d=p_other0(-axes_extra)
3461                                ),
3462                                Coords(
3463                                    elements, pos=p_origin(max), d=p_other0(axes_extra)
3464                                ),
3465                            ]
3466                        ],
3467                    )
3468                )
3469                ticks_lines = []
3470                tick_label_style = ticks_style[index].clone()
3471                tick_label_style.extend(label_style)
3472                for x in ticks:
3473                    ticks_lines.append(
3474                        [
3475                            Coords(elements, pos=p_origin(x)),
3476                            Coords(
3477                                elements, pos=p_origin(x), d=p_self0(tick_large_size)
3478                            ),
3479                        ]
3480                    )
3481                    if ticks_int:
3482                        content = String(str(int(x)))
3483                    elif x == floor(x):
3484                        content = String("%.1f" % x)  # e.g. 1.0 (instead of 1.)
3485                    else:
3486                        content = String("%g" % x)  # fix e.g. 0.6000000000000001
3487                    add_element(
3488                        InsetBox(
3489                            elements,
3490                            tick_label_style,
3491                            content=content,
3492                            pos=Coords(
3493                                elements, pos=p_origin(x), d=p_self0(-tick_label_d)
3494                            ),
3495                            opos=p_self0(1),
3496                            opacity=0.5,
3497                        )
3498                    )
3499                for x in ticks_small:
3500                    pos = p_origin(x)
3501                    ticks_lines.append(
3502                        [
3503                            Coords(elements, pos=pos),
3504                            Coords(elements, pos=pos, d=p_self0(tick_small_size)),
3505                        ]
3506                    )
3507                add_element(LineBox(elements, axes_style[0], lines=ticks_lines))
3508        return axes
3509
3510        """if axes[1]:
3511            add_element(LineBox(elements, axes_style[1], lines=[[Coords(elements, pos=(origin_x,ymin), d=(0,-axes_extra)),
3512                Coords(elements, pos=(origin_x,ymax), d=(0,axes_extra))]]))
3513            ticks = []
3514            tick_label_style = ticks_style[1].clone()
3515            tick_label_style.extend(label_style)
3516            for k in range(start_k_y, start_k_y+steps_y+1):
3517                if k != origin_k_y:
3518                    y = k * step_y
3519                    if y > ymax:
3520                        break
3521                    pos = (origin_x,y)
3522                    ticks.append([Coords(elements, pos=pos),
3523                        Coords(elements, pos=pos, d=(tick_large_size,0))])
3524                    add_element(InsetBox(elements, tick_label_style, content=Real(y), pos=Coords(elements, pos=pos,
3525                        d=(-tick_label_d,0)), opos=(1,0)))
3526            for k in range(start_k_y_small, start_k_y_small+steps_y_small+1):
3527                if k % sub_y != 0:
3528                    y = k * step_y_small
3529                    if y > ymax:
3530                        break
3531                    pos = (origin_x,y)
3532                    ticks.append([Coords(elements, pos=pos),
3533                        Coords(elements, pos=pos, d=(tick_small_size,0))])
3534            add_element(LineBox(elements, axes_style[1], lines=ticks))"""
3535
3536
3537class Directive(Builtin):
3538    attributes = ("ReadProtected",)
3539
3540
3541class Blend(Builtin):
3542    """
3543    <dl>
3544    <dt>'Blend[{$c1$, $c2$}]'
3545        <dd>represents the color between $c1$ and $c2$.
3546    <dt>'Blend[{$c1$, $c2$}, $x$]'
3547        <dd>represents the color formed by blending $c1$ and $c2$ with
3548        factors 1 - $x$ and $x$ respectively.
3549    <dt>'Blend[{$c1$, $c2$, ..., $cn$}, $x$]'
3550        <dd>blends between the colors $c1$ to $cn$ according to the
3551        factor $x$.
3552    </dl>
3553
3554    >> Blend[{Red, Blue}]
3555     = RGBColor[0.5, 0., 0.5]
3556    >> Blend[{Red, Blue}, 0.3]
3557     = RGBColor[0.7, 0., 0.3]
3558    >> Blend[{Red, Blue, Green}, 0.75]
3559     = RGBColor[0., 0.5, 0.5]
3560
3561    >> Graphics[Table[{Blend[{Red, Green, Blue}, x], Rectangle[{10 x, 0}]}, {x, 0, 1, 1/10}]]
3562     = -Graphics-
3563
3564    >> Graphics[Table[{Blend[{RGBColor[1, 0.5, 0, 0.5], RGBColor[0, 0, 1, 0.5]}, x], Disk[{5x, 0}]}, {x, 0, 1, 1/10}]]
3565     = -Graphics-
3566
3567    #> Blend[{Red, Green, Blue}, {1, 0.5}]
3568     : {1, 0.5} should be a real number or a list of non-negative numbers, which has the same length as {RGBColor[1, 0, 0], RGBColor[0, 1, 0], RGBColor[0, 0, 1]}.
3569     = Blend[{RGBColor[1, 0, 0], RGBColor[0, 1, 0], RGBColor[0, 0, 1]}, {1, 0.5}]
3570    """
3571
3572    messages = {
3573        "arg": (
3574            "`1` is not a valid list of color or gray-level directives, "
3575            "or pairs of a real number and a directive."
3576        ),
3577        "argl": (
3578            "`1` should be a real number or a list of non-negative "
3579            "numbers, which has the same length as `2`."
3580        ),
3581    }
3582
3583    rules = {"Blend[colors_]": "Blend[colors, ConstantArray[1, Length[colors]]]"}
3584
3585    def do_blend(self, colors, values):
3586        type = None
3587        homogenous = True
3588        for color in colors:
3589            if type is None:
3590                type = color.__class__
3591            else:
3592                if color.__class__ != type:
3593                    homogenous = False
3594                    break
3595        if not homogenous:
3596            colors = [RGBColor(components=color.to_rgba()) for color in colors]
3597            type = RGBColor
3598        total = sum(values)
3599        result = None
3600        for color, value in zip(colors, values):
3601            frac = value / total
3602            part = [component * frac for component in color.components]
3603            if result is None:
3604                result = part
3605            else:
3606                result = [r + p for r, p in zip(result, part)]
3607        return type(components=result)
3608
3609    def apply(self, colors, u, evaluation):
3610        "Blend[{colors___}, u_]"
3611
3612        colors_orig = colors
3613        try:
3614            colors = [_Color.create(color) for color in colors.get_sequence()]
3615            if not colors:
3616                raise ColorError
3617        except ColorError:
3618            evaluation.message("Blend", "arg", Expression(SymbolList, colors_orig))
3619            return
3620
3621        if u.has_form("List", None):
3622            values = [value.round_to_float(evaluation) for value in u.leaves]
3623            if None in values:
3624                values = None
3625            if len(u.leaves) != len(colors):
3626                values = None
3627            use_list = True
3628        else:
3629            values = u.round_to_float(evaluation)
3630            if values is None:
3631                pass
3632            elif values > 1:
3633                values = 1.0
3634            elif values < 0:
3635                values = 0.0
3636            use_list = False
3637        if values is None:
3638            return evaluation.message(
3639                "Blend", "argl", u, Expression(SymbolList, colors_orig)
3640            )
3641
3642        if use_list:
3643            return self.do_blend(colors, values).to_expr()
3644        else:
3645            x = values
3646            pos = int(floor(x * (len(colors) - 1)))
3647            x = (x - pos * 1.0 / (len(colors) - 1)) * (len(colors) - 1)
3648            if pos == len(colors) - 1:
3649                return colors[-1].to_expr()
3650            else:
3651                return self.do_blend(colors[pos : (pos + 2)], [1 - x, x]).to_expr()
3652
3653
3654class Lighter(Builtin):
3655    """
3656    <dl>
3657    <dt>'Lighter[$c$, $f$]'
3658        <dd>is equivalent to 'Blend[{$c$, White}, $f$]'.
3659    <dt>'Lighter[$c$]'
3660        <dd>is equivalent to 'Lighter[$c$, 1/3]'.
3661    </dl>
3662
3663    >> Lighter[Orange, 1/4]
3664     = RGBColor[1., 0.625, 0.25]
3665    >> Graphics[{Lighter[Orange, 1/4], Disk[]}]
3666     = -Graphics-
3667    >> Graphics[Table[{Lighter[Orange, x], Disk[{12x, 0}]}, {x, 0, 1, 1/6}]]
3668     = -Graphics-
3669    """
3670
3671    rules = {
3672        "Lighter[c_, f_]": "Blend[{c, White}, f]",
3673        "Lighter[c_]": "Lighter[c, 1/3]",
3674    }
3675
3676
3677class Darker(Builtin):
3678    """
3679    <dl>
3680    <dt>'Darker[$c$, $f$]'
3681        <dd>is equivalent to 'Blend[{$c$, Black}, $f$]'.
3682    <dt>'Darker[$c$]'
3683        <dd>is equivalent to 'Darker[$c$, 1/3]'.
3684    </dl>
3685
3686    >> Graphics[Table[{Darker[Yellow, x], Disk[{12x, 0}]}, {x, 0, 1, 1/6}]]
3687     = -Graphics-
3688    """
3689
3690    rules = {"Darker[c_, f_]": "Blend[{c, Black}, f]", "Darker[c_]": "Darker[c, 1/3]"}
3691
3692
3693class Tiny(Builtin):
3694    """
3695    <dl>
3696    <dt>'ImageSize' -> 'Tiny'
3697        <dd>produces a tiny image.
3698    </dl>
3699    """
3700
3701
3702class Small(Builtin):
3703    """
3704    <dl>
3705    <dt>'ImageSize' -> 'Small'
3706        <dd>produces a small image.
3707    </dl>
3708    """
3709
3710
3711class Medium(Builtin):
3712    """
3713    <dl>
3714    <dt>'ImageSize' -> 'Medium'
3715        <dd>produces a medium-sized image.
3716    </dl>
3717    """
3718
3719
3720class Large(Builtin):
3721    """
3722    <dl>
3723    <dt>'ImageSize' -> 'Large'
3724        <dd>produces a large image.
3725    </dl>
3726    """
3727
3728
3729element_heads = frozenset(
3730    system_symbols(
3731        "Rectangle",
3732        "Disk",
3733        "Line",
3734        "Arrow",
3735        "FilledCurve",
3736        "BezierCurve",
3737        "Point",
3738        "Circle",
3739        "Polygon",
3740        "RegularPolygon",
3741        "Inset",
3742        "Text",
3743        "Sphere",
3744        "Style",
3745    )
3746)
3747
3748styles = system_symbols_dict(
3749    {
3750        "RGBColor": RGBColor,
3751        "XYZColor": XYZColor,
3752        "LABColor": LABColor,
3753        "LCHColor": LCHColor,
3754        "LUVColor": LUVColor,
3755        "CMYKColor": CMYKColor,
3756        "Hue": Hue,
3757        "GrayLevel": GrayLevel,
3758        "Thickness": Thickness,
3759        "AbsoluteThickness": AbsoluteThickness,
3760        "Thick": Thick,
3761        "Thin": Thin,
3762        "PointSize": PointSize,
3763        "Arrowheads": Arrowheads,
3764    }
3765)
3766
3767style_options = system_symbols_dict(
3768    {"FontColor": _style, "ImageSizeMultipliers": (lambda *x: x[1])}
3769)
3770
3771style_heads = frozenset(styles.keys())
3772
3773style_and_form_heads = frozenset(
3774    style_heads.union(set(["System`EdgeForm", "System`FaceForm"]))
3775)
3776
3777GLOBALS = system_symbols_dict(
3778    {
3779        "Rectangle": Rectangle,
3780        "Disk": Disk,
3781        "Circle": Circle,
3782        "Polygon": Polygon,
3783        "RegularPolygon": RegularPolygon,
3784        "Inset": Inset,
3785        "Text": Text,
3786        "RectangleBox": RectangleBox,
3787        "DiskBox": DiskBox,
3788        "LineBox": LineBox,
3789        "BezierCurveBox": BezierCurveBox,
3790        "FilledCurveBox": FilledCurveBox,
3791        "ArrowBox": ArrowBox,
3792        "CircleBox": CircleBox,
3793        "PolygonBox": PolygonBox,
3794        "RegularPolygonBox": RegularPolygonBox,
3795        "PointBox": PointBox,
3796        "InsetBox": InsetBox,
3797    }
3798)
3799
3800GLOBALS.update(styles)
3801
3802GRAPHICS_SYMBOLS = frozenset(
3803    ["System`List", "System`Rule", "System`VertexColors"]
3804    + list(element_heads)
3805    + [element + "Box" for element in element_heads]
3806    + list(style_heads)
3807)
3808