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