GeezerVisonThe World Wide Web, restored to 800 pixels of glory. |
| Home|About|Gaming cabinet|Archives|How it works|Starter sites |
GeezerVison is showing github.com — plotdevice/plotdevice/context.py at main · plotdevice/plotdevice Expand file tree/ Copy pathcontext.pyMore file actions 1823 lines (1487 loc) · 73.3 KB / Copy pathcontext.pyFile metadata and controls1823 lines (1487 loc) · 73.3 KB 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 # encoding: utf-8 import os, re, types from contextlib import contextmanager from collections import namedtuple, OrderedDict from os.path import exists, expanduser from objc import super from .lib.cocoa import * from .lib import pathmatics from .util import _copy_attr, _copy_attrs, _flatten, trim_zeroes, numlike, autorelease from .gfx.geometry import Dimension, parse_coords from .gfx.typography import Layout from .gfx import * from . import gfx, lib, util, Halted, DeviceError __all__ = ('Context', 'Canvas') # default size for Canvas and GraphicsView objects DEFAULT_WIDTH, DEFAULT_HEIGHT = 512, 512 # named tuples for grouping state attrs PenStyle = namedtuple('PenStyle', ['nib', 'cap', 'join', 'dash']) GridUnits = namedtuple('GridUnits', ['unit', 'dpx', 'to_px', 'from_px']) ### NSGraphicsContext wrapper (whose methods are the business-end of the user-facing API) ### class Context(object): _state_vars = '_outputmode', '_colormode', '_colorrange', '_fillcolor', '_strokecolor', '_penstyle', '_font', '_effects', '_path', '_autoclosepath', '_grid', '_transform', '_transformmode', '_thetamode', '_transformstack', '_params', '_vars' def __init__(self, canvas=None, ns=None): """Initializes the context. Note that we have to pass the namespace of the executing script to allow for ximport's _ctx-passing magic. """ self.canvas = Canvas() if canvas is None else canvas self._ns = {} if ns is None else ns self._imagecache = {} self._statestack = [] self._vars = [] self._params = OrderedDict() self._resetContext() # initialize default graphics state self._resetEnvironment() # initialize namespace & canvas def _activate(self): """Pass a reference to this context to all the gfx objects and libs that have registered themselves as needing _ctx access.""" gfx.bind(self) lib.bind(self) def _resetEnvironment(self): """Set the namespace and canvas to factory defaults (preparing for a new run)""" # clean out the namespace. include just the plotdevice commands/types self._ns.clear() self._ns.update( (a,getattr(util,a)) for a in util.__all__ ) self._ns.update( (a,getattr(gfx,a)) for a in gfx.__all__ ) self._ns.update( (a,getattr(self,a)) for a in dir(self) if not a.startswith('_') ) self._ns["_ctx"] = self # clear the canvas and reset the dims/background self.canvas.reset() self.canvas.background = Color(1.0) self.canvas.speed = None self.canvas.unit = px # keep track of non-px canvas units self._grid = GridUnits(px, 1, Transform(), Transform()) # default output colorspace self._outputmode = RGB def _resetContext(self): """Do a thorough reset of all the state variables""" self._activate() # color state self._colormode = RGB self._colorrange = 1.0 self._fillcolor = Color() # can also be a Gradient or Pattern self._strokecolor = None # line style self._penstyle = PenStyle(nib=1.0, cap=BUTT, join=MITER, dash=None) # transformation state self._transform = Transform() self._transformmode = CENTER self._thetamode = DEGREES # compositing effects (blend, alpha, and shadow) self._effects = Effect() # type styles self._stylesheet = Stylesheet() self._font = Font(None) # bezier construction internals self._path = None self._autoclosepath = True self._autoplot = True # track new calls to var() so we can update self._params # (this is reset with every invocation but only checked after the full-module eval) self._vars = OrderedDict() # update existing variables' values from _params (so draw() can see updates) for name, p in self._params.items(): if p.type != BUTTON: # buttons don't actually create variables self._ns[name] = p.value # legacy internals self._transformstack = [] # only used by push/pop def _saveContext(self): cached = [_copy_attr(getattr(self, v)) for v in Context._state_vars] self._statestack.insert(0, cached) self.clear() def _restoreContext(self): try: cached = self._statestack.pop(0) except IndexError: raise DeviceError("Too many Context._restoreContext calls.") for attr, val in zip(Context._state_vars, cached): setattr(self, attr, val) self.canvas.unit = self._grid.unit def ximport(self, libName): lib = __import__(libName) self._ns[libName] = lib lib._ctx = self return lib ### Setup methods ### def size(self, width=None, height=None, unit=None): """Set the dimensions of the canvas The canvas defaults to 512x512 pixels, but this can be changed by calling size() as the first drawing-related command in your script. In addition to the `width` and `height` args, you can provide an optional `unit`. Use one of the constants: px, pica, inch, cm, or mm. Setting the canvas unit affects all subsequent drawing commands too. For instance, the following will create a 1" x 1" square in the top-left corner of a standard letter-sized page: size(8.5, 11, inch) rect(0,0,1,1) """ if not (width is None or height is None): self.canvas.width = width self.canvas.height = height if unit is not None: dpx = unit.basis self._grid = GridUnits(unit, dpx, Transform().scale(dpx), Transform().scale(1/dpx)) self.canvas.unit = unit return self.canvas.size @property def WIDTH(self): """The current canvas width (read-only)""" return Dimension('width') @property def HEIGHT(self): """The current canvas height (read-only)""" return Dimension('height') def speed(self, fps): """Set the target frame-rate for an animation Calling speed() signals to PlotDevice that your script is an animation containing a `draw()` method (and optionally, `setup()` and `stop()` methods to be called at the beginning and end of the run respectively). Your draw method will be called repeatedly until hitting command-period. If you set the speed to 0 your draw method will be called only once and the animation will terminate. """ if fps<0: timetraveler = "Sorry, can't animate at %r fps" % fps raise DeviceError(timetraveler) self.canvas.speed = fps def halt(self): """Cleanly terminates an animation when called from your draw() function""" raise Halted() def background(self, *args, **kwargs): """Set the canvas background color Arguments will be interpeted according to the current color-mode and range. For details: help(color) For a transparent background, call with a single arg of None If more than one color arg is included, a gradient will be drawn. Pass a list with the `steps` keyword arg to set the relative location of each color in the gradient (0-1.0). Setting an `angle` keyword will draw a linear gradient (otherwise it will be radial). Radial gradients will draw from the canvas center by default, but a relative center can be specified with the `center` keyword arg. If included, the center arg should be a 2-tuple with x,y values in the range -1 to +1. In addition to colors, you can also call background() with an image() as its sole argument. In this case the image will be tiled to fill the canvas. """ if len(args) > 0: if len(args) == 1 and args[0] is None: bg = None elif isinstance(args[0], Image): bg = Pattern(args[0]) self.canvas.clear(args[0]) elif isinstance(args[0],str) and (args[0].startswith('http') or exists(expanduser(args[0]))): bg = Pattern(args[0]) elif set(Gradient.kwargs) >= set(kwargs) and len(args)>1 and all(Color.recognized(c) for c in args): bg = Gradient(*args, **kwargs) else: bg = Color(args) self.canvas.background = bg return self.canvas.background def outputmode(self, mode=None): if mode is not None: self._outputmode = mode return self._outputmode ### Bezier Path Commands ### def bezier(self, x=None, y=None, **kwargs): """Create and plot a new bezier path.""" draw = self._should_plot(kwargs) try: # accept a Point as the first arg x, y = parse_coords([x], [Point]) except: pass Bezier.validate(kwargs) if isinstance(x, (list, tuple, Bezier)): # if the first arg is an iterable of point tuples or an existing Bezier, apply # the `close` kwarg immediately since the path is already fully-specified pth = Bezier(path=x, **kwargs) pth._autoclose() else: # otherwise start a new path with the presumption that it will be populated # in a `with` block or by adding points manually. begins with a moveto # element if an x,y coord was provided and relies on Bezier's __enter__ # method to update self._path if appropriate pth = Bezier(**kwargs) if numlike(x) and numlike(y): pth.moveto(x,y) if draw: pth.draw() return pth @contextmanager def _active_path(self, kwargs): """Provides a target Bezier object for drawing commands within the block. If a bezier is currently being constructed, drawing will be appended to it. Otherwise a new Bezier will be created and autoplot'ed as appropriate.""" draw = self._should_plot(kwargs) Bezier.validate(kwargs) p=Bezier(**kwargs) yield p if self._path is not None: # if a bezier is being built in a `with` block, add curves to it, but # ignore kwargs since the parent bezier's styles apply to all lines drawn xf = p._screen_transform self._path.extend(xf.apply(p)) elif draw: # if this is a one-off primitive, draw it depending on the kwargs & state p.draw() def moveto(self, *coords): """Update the current point in the active path without drawing a line to it Syntax: moveto(x, y) moveto(Point) """ (x,y) = parse_coords(coords, [Point]) if self._path is None: raise DeviceError("No active path. Use bezier() or beginpath() first.") self._path.moveto(x,y) def lineto(self, *coords, **kwargs): """Add a line from the current point in the active path to a destination point (and optionally close the subpath) Syntax: lineto(x, y, close=False) lineto(Point, close=False) """ close = kwargs.pop('close', False) (x,y) = parse_coords(coords, [Point]) if self._path is None: raise DeviceError("No active path. Use bezier() or beginpath() first.") self._path.lineto(x, y) if close: self._path.closepath() def curveto(self, *coords, **kwargs): """Draw a cubic bezier curve from the active path's current point to a destination point (x3,y3). The handles for the current and destination points will be set by (x1,y1) and (x2,y2) respectively. Syntax: curveto(x1, y1, x2, y2, x3, y3, close=False) curveto(Point, Point, Point, close=False) Calling with close=True will close the subpath after adding the curve. """ close = kwargs.pop('close', False) (x1,y1), (x2,y2), (x3,y3) = parse_coords(coords, [Point,Point,Point]) if self._path is None: raise DeviceError("No active path. Use bezier() or beginpath() first.") self._path.curveto(x1, y1, x2, y2, x3, y3) if close: self._path.closepath() def arcto(self, *coords, **kwargs): """Draw a circular arc from the current point in the active path to a destination point Syntax: arcto(x1, y1, ccw=False, close=False) arcto(Point, ...) arcto(x1, y1, x2, y2, radius=None, close=False) arcto(Point, Point, ...) To draw a semicircle to the destination point use one of: arcto(x,y) arcto(x,y, ccw=True) To draw a parabolic arc in the triangle between the current point, an intermediate control point, and the destination; choose a radius small enough to fit in that angle and call: arcto(mid_x, mid_y, dest_x, dest_y, radius) Calling with close=True will close the subpath after adding the arc. """ ccw = kwargs.pop('ccw', False) close = kwargs.pop('close', False) radius = kwargs.pop('radius', None) x2 = y2 = None try: (x1,y1), (x2,y2), radius = parse_coords(coords, [Point,Point,float]) except: try: (x1,y1), (x2,y2) = parse_coords(coords, [Point,Point]) except: (x1,y1) = parse_coords(coords, [Point]) if self._path is None: raise DeviceError("No active path. Use bezier() or beginpath() first.") self._path.arcto(x1, y1, x2, y2, radius, ccw) if close: self._path.closepath() ### Bezier Primitives ### def rect(self, *coords, **kwargs): """Draw a rectangle with a corner of (x,y) and size of (width, height) Syntax: rect(x, y, width, height, roundness=0.0, radius=None, plot=True, **kwargs): rect(Point, Size, ...) rect(Region, ...) The `roundness` arg lets you control corner radii in a size-independent way. It can range from 0 (sharp corners) to 1.0 (maximally round) and will ensure rounded corners are circular. The `radius` arg provides less abstract control over corner-rounding. It can be either a single float (specifying the corner radius in canvas units) or a 2-tuple with the radii for the x- and y-axis respectively. """ try: (x,y), (w,h), roundness = parse_coords(coords, [Point,Size,float]) except: (x,y), (w,h) = parse_coords(coords, [Point,Size]) roundness = 0 roundness = kwargs.pop('roundness', roundness) radius = kwargs.pop('radius', None) if roundness > 0: radius = min(w,h)/2.0 * min(roundness, 1.0) with self._active_path(kwargs) as p: p.rect(x, y, w, h, radius=radius) return p def oval(self, *coords, **kwargs): """Draw an ellipse within the rectangle specified by (x,y) & (width,height) Syntax: oval(x, y, width, height, range=None, ccw=False, close=False, plot=True, **kwargs): oval(Point, Size, ...) oval(Region, ...) The `range` arg can be either a number of degrees (from 0°) or a 2-tuple with a start- and stop-angle. Only that subsection of the oval will be drawn. The `ccw` arg flags whether to interpret ranges in a counter-clockwise direction. If `close` is True, a chord will be drawn between the unconnected endpoints. """ rng = kwargs.pop('range', None) ccw = kwargs.pop('ccw', False) close = kwargs.pop('close', False) (x,y), (w,h) = parse_coords(coords, [Point,Size]) with self._active_path(kwargs) as p: p.oval(x, y, w, h, rng, ccw, close) return p ellipse = oval def line(self, *coords, **kwargs): """Draw an unconnected line segment from (x1,y1) to (x2,y2) Syntax: line(x1, y1, x2, y2, ccw=None, plot=True, **kwargs) line(Point, Point, ...) line(x1, y1, dx=0, dy=0, ccw=None, plot=True, **kwargs) line(Point, dx=0, dy=0, ...) Ordinarily this will be a straight line (a simple MOVETO & LINETO), but if the `ccw` arg is set to True or False, a semicircular arc will be drawn between the points in the specified direction. """ dx, dy = kwargs.pop('dx',None), kwargs.pop('dy', None) if any(map(numlike, [dx, dy])): start = parse_coords(coords, [Point]) end = start + (dx or 0, dy or 0) (x1,y1), (x2,y2) = start, end else: (x1,y1), (x2,y2) = parse_coords(coords, [Point,Point]) ccw = kwargs.pop('ccw', None) with self._active_path(kwargs) as p: p.line(x1, y1, x2, y2, ccw) return p def poly(self, *coords, **kwargs): """Draw a regular polygon centered at (x,y) Syntax: poly(x, y, radius, sides=4, points=None, plot=True, **kwargs) poly(Point, radius, ...) The `sides` arg sets the type of polygon to draw. Regardless of the number, it will be oriented such that its base is horizontal. If a `points` keyword argument is passed instead of a `sides` argument, a regularized star polygon will be drawn at the given coordinates & radius, """ sides = kwargs.pop('sides', 4) points = kwargs.pop('points', None) if 'radius' in kwargs: coords = coords + (kwargs.pop('radius'),) (x,y), radius = parse_coords(coords, [Point,float]) with self._active_path(kwargs) as p: p.poly(x, y, radius, sides, points) return p def arc(self, *coords, **kwargs): """Draw a full circle or partial arc centered at (x,y) Syntax: arc(x, y, radius, range=None, ccw=False, close=False, plot=True, **kwargs) arc(Point, radius, ...) The `range` arg can be either a number of degrees (from 0°) or a 2-tuple with a start- and stop-angle. The `ccw` arg flags whether to interpret ranges in a counter-clockwise direction. If `close` is true, a pie-slice will be drawn to the origin from the ends. """ rng = kwargs.pop('range', None) ccw = kwargs.pop('ccw', False) close = kwargs.pop('close', False) if 'radius' in kwargs: coords = coords + (kwargs.pop('radius'),) (x,y), radius = parse_coords(coords, [Point,float]) with self._active_path(kwargs) as p: p.arc(x, y, radius, rng, ccw, close) return p def star(self, x, y, points=20, outer=100, inner=None, **kwargs): """Draw a star-shaped path centered at (x,y) The `outer` radius sets the distance of the star's points from the origin, while `inner` sets the radius of the notches between points. If `inner` is omitted, it will be set to half the outer radius. """ with self._active_path(kwargs) as p: p.star(x, y, points, outer, inner) return p def arrow(self, x, y, width=100, type=NORMAL, **kwargs): """Draws an arrow. Draws an arrow pointing at position (x,y), with a default width of 100. There are two different types of arrows: NORMAL and (the early-oughts favorite) FORTYFIVE. """ with self._active_path(kwargs) as p: p.arrow(x, y, width, type) return p ### ‘Classic’ Bezier API ### def beginpath(self, x=None, y=None): self._path = Bezier() self._pathclosed = False if x != None and y != None: self._path.moveto(x,y) def closepath(self): if self._path is None: raise DeviceError("No active path. Use bezier() or beginpath() first.") if not self._pathclosed: self._path.closepath() self._pathclosed = True def endpath(self, **kwargs): if self._path is None: raise DeviceError("No active path. Use bezier() or beginpath() first.") if self._autoclosepath: self.closepath() p = self._path draw = self._should_plot(kwargs) if draw: p.draw() self._path = None self._pathclosed = False return p def drawpath(self, path, **kwargs): Bezier.validate(kwargs) if isinstance(path, (list, tuple)): path = Bezier(path, **kwargs) else: # Set the values in the current bezier path with the kwargs for arg_key, arg_val in kwargs.items(): setattr(path, arg_key, _copy_attr(arg_val)) path.draw() def autoclosepath(self, close=True): self._autoclosepath = close def findpath(self, points, curvature=1.0): return pathmatics.findpath(points, curvature=curvature) ### Transformation Commands ### def push(self): """Legacy command. Equivalent to: `with transform():` Saves the transform state to be restored by a subsequent pop()""" self._transformstack.insert(0, self._transform.matrix) def pop(self): """Legacy command. Equivalent to: `with transform():` Restores the transform to the saved state from a prior push()""" try: self._transform = Transform(self._transformstack[0]) del self._transformstack[0] except IndexError as e: raise DeviceError("pop: too many pops!") def transform(self, mode=None, matrix=None): """Change the transform mode or begin a `with`-statement-scoped set of transformations Transformation Modes PlotDevice determines graphic objects' final screen positions using two factors: - the (x,y)/(w,h) point passed to rect(), text(), etc. at creation time - the ‘current transform’ which has accumulated as a result of prior calls to commands like scale() or rotate() By default these transformations are applied relative to the centermost point of the object (based on its x/y/w/h). This is convenient since it prevents scaling and rotation from changing the location of the object. If you prefer to apply transformations relative to the canvas's upper-left corner, use: transform(CORNER) You can then switch back to the default scheme with: transform(CENTER) Note that changing the mode does *not* affect the transformation matrix itself, just the origin-point that subsequently drawn objects will use when applying it. Transformation Matrices In addition to changing the mode, the transform() command also allows you to overwrite the underlying transformation matrix with a new set of values. Pass a 6-element list or tuple of the form [m11, m21, m12, m22, tX, tY] as the `matrix` argument to update the current transform. Note that you can omit the `mode` arg when setting a new matrix if you don't want the transformation origin to change. See Apple's docs for all the math-y details: http://tinyurl.com/cocoa-drawing-guide-transforms Transformation Context Manager When used as part of a `with` statement, the transform() command will ensure the mode and transformations applied during the indented code-block are reverted to their previous state once the block completes. """ if matrix is None and isinstance(mode, (list, tuple, NSAffineTransformStruct)): mode, matrix = None, mode if mode not in (CORNER, CENTER, None): badmode = "transform: mode must be CORNER or CENTER" raise DeviceError(badmode) rollback = {"_transformmode":self._transformmode, "_transform":self._transform.copy()} if mode: self._transformmode = mode if matrix: self._transform = Transform(matrix) xf = self._transform.copy() xf._rollback = rollback return xf def reset(self): """Discard any accumulated transformations from prior calls to translate, scale, rotate, or skew""" xf = self._transform.inverse xf._rollback = {'_transform':self._transform.copy()} self._transform = Transform() return xf def translate(self, x=0, y=0): x *= self._grid.dpx y *= self._grid.dpx return self._transform.translate(x,y, rollback=True) def scale(self, x=1, y=None): """Scale subsequent drawing operations by x- and y-factors When called with one argument, the factor will be applied to the x & y axes evenly. """ return self._transform.scale(x,y, rollback=True) def skew(self, x=0, y=0): """Applies a 1- or 2-axis skew distortion to subsequent drawing operations The `x` and `y` args are angles in the canvas's current geometry() unit that control the horizontal and vertical skew respectively. When called with only one argument, the skew will be purely horizontal. """ return self._transform.skew(x,y, rollback=True) def rotate(self, theta=None, **kwargs): """Rotate subsequent drawing operations Positional arg: `theta` is an angle in the canvas's current geometry() unit (degress by default) Keyword args: the angle can be specified units other than the geometry-default by using a keyword argument of `degrees`, `radians`, or `percent`. """ if theta is not None: kwargs[self._thetamode] = theta return self._transform.rotate(rollback=True, **kwargs) ### Ink Commands ### def color(self, *args, **kwargs): """Set the default mode/range for interpreting color values (or create a color object) Color State A number of plotdevice commands can take lists of numeric arguments to specify a color (see stroke(), fill(), background(), etc.). When called with keyword arguments alone, The color() command allows you to control how these numbers should be interpreted in subsequent color-modification commands. By default, color commands interpret groups of 3 or more numbers as r/g/b triplets (with an optional, final alpha arg). If the `mode` keyword arg is set to RGB, HSV, or CMYK, subsequent color commands will interpret numerical arguments according to that colorspace instead. The `range` keyword arg sets the maximum value for color channels. By default this is 1.0, but 255 and 100 are also sensible choices. For instance here are three equivalent ways of setting the fill color to ‘blue’: color(mode=HSV) fill(.666, 1.0, .76) color(mode=RGB, range=255) fill(0, 0, 190) color(mode=CMYK, range=100) fill(95, 89, 0, 0) Color mode & range changes can be constrained to a block of code using the `with` statement. e.g., background(.5, .5, .6) # interpteted as r/g/b (the default) with color(mode=CMYK): # temporarily change the mode: stroke(1, 0, 0, 0) # - interpteted as c/m/y/k fill(1, 0, 0) # outside the block, the mode is restored to r/g/b Making Colors When called with a sequence of color-channel values, color() will return a reusable Color object. These can be passed to color-related commands in lieu of numeric args. For example: red = color(1, 0, 0) # r/g/b glossy_black = color(15, 15, 15, 0.25) # r/g/b/a background(red) fill(glossy_black) You can also prefix the numeric args with a color mode as a convenience for setting one-off colors in a mode different from the current colorspace: color(mode=HSV) hsb_color = color(.7, 1, .8) # uses current mode (h/s/b) cmyk_color = color(CMYK, 0, .7, .9, 0) # interpreted as c/m/y/k still_hsb = color(1, .5, .25) # uses current mode (h/s/b) Greyscale colors can be created regardless of the current mode by passing only one or two values (for the brightness and alpha respectively): fill(1, .75) # translucent white stroke(0) # opaque black If you pass a string to a color command, it must either be a hex-string (beginning with a `#`) or a valid CSS color-name. The string can be followed by an optional alpha argument: background('#f00') # blindingly red fill('#74e9ff', 0.4) # translucent pale blue stroke('chartreuse') # electric bile """ # flatten any tuples in the arguments list args = _flatten(args) # if the first arg is a color mode, use that to interpret the args if args and args[0] in (RGB, HSV, CMYK, GREY): mode, args = args[0], args[1:] kwargs.setdefault('mode', mode) if not args: # if called without any component values update the global mode/range, # and return a context manager for `with color(mode=...)` usage if {'range', 'mode'}.intersection(kwargs): return PlotContext(self, **kwargs) # if we got at least one numerical/string arg, parse it return Color(*args, **kwargs) def colormode(self, mode=None, range=None): """Legacy command. Equivalent to: color(mode=...)""" if mode is not None: self._colormode = mode if range is not None: self._colorrange = float(range) return self._colormode def colorrange(self, range=None): """Legacy command. Equivalent to: color(range=...)""" if range is not None: self._colorrange = float(range) return self._colorrange def nofill(self): """Set the fill color to None""" clr = Color(None) setattr(clr, '_rollback', dict(fill=self._fillcolor)) self._fillcolor = None return clr def fill(self, *args, **kwargs): """Set the fill color Arguments will be interpeted according to the current color-mode and range. For details: help(color) If more than one color arg is included, a gradient will be drawn. Pass a list with the `steps` keyword arg to set the relative location of each color in the gradient (0-1.0). The relative locations are based on the bounding-box of the object being filled (not its convex hull). So for highly rounded objects, you'll need to adjust the steps to account for the dead-space in the corners of the bounding box. Including an `angle` keyword arg will draw a linear gradient (otherwise it will be radial). Radial gradients will draw from the object center by default, but a relative center can be specified with the `center` keyword arg. If included, the center arg should be a 2-tuple with x,y values in the range -1 to +1. In addition to colors, you can also call fill() with an image() as its sole argument. In this case the image will be tiled to fit the object being filled. Returns: the current fill color as a Color (or Gradient/Pattern) object. Context Manager: fill() can be used as part of a `with` statement in which case the fill will be reset to its previous value once the block completes """ if args: if args[0] is None: return self.nofill() if isinstance(args[0], Image): clr = Pattern(args[0]) self.canvas.clear(args[0]) elif isinstance(args[0],str) and (args[0].startswith('http') or exists(expanduser(args[0]))): clr = Pattern(args[0]) elif set(Gradient.kwargs) >= set(kwargs) and len(args)>1 and all(Color.recognized(c) for c in args): clr = Gradient(*args, **kwargs) else: clr = Color(*args) setattr(clr, '_rollback', dict(fill=self._fillcolor)) self._fillcolor = clr return self._fillcolor def nostroke(self): """Set the stroke color to None""" clr = Color(None) setattr(clr, '_rollback', dict(fill=self._strokecolor)) self._strokecolor = None return clr def stroke(self, *args): """Set the stroke color Arguments will be interpeted according to the current color-mode and range. For details: help(color) Returns: the current stroke color as a Color object. Context Manager: stroke() can be used as part of a `with` statement in which case the stroke will be reset to its previous value once the block completes """ if len(args) > 0: if args[0] is None: return self.nostroke() annotated = Color(*args) setattr(annotated, '_rollback', dict(stroke=self._strokecolor)) self._strokecolor = annotated return self._strokecolor ### Pen Commands ### def pen(self, nib=None, **spec): """Set the width or line-style used for stroking paths Positional arg: `nib` sets the stroke width (in points) Keyword args: `cap` sets the endcap style (BUTT, ROUND, or SQUARE). `join` sets the linejoin style (MITER, ROUND, or BEVEL) `dash` is either a single number or a list of them specifying on-off intervals for drawing a dashed line Returns: a context manager allowing pen changes to be constrained to the block of code following a `with` statement. """ # stroke width can be positional or keyword if numlike(nib): spec.setdefault('nib', nib) elif nib is not None: badnib = "the pen's nib size must be a number (not %r)" % type(nib) raise DeviceError(badnib) # validate the line-dash stepsize (if any) if numlike(spec.get('dash',None)): spec['dash'] = [spec['dash']] if len(spec.get('dash') or []) % 2: spec['dash'] += spec['dash'][-1:] # assume even spacing for omitted skip sizes # pull relevant kwargs into a PenStyle tuple (but pass other args as-is) newstyle = {k:spec.pop(k) for k in PenStyle._fields if k in spec} spec['pen'] = self._penstyle._replace(**newstyle) return PlotContext(self, **spec) def strokewidth(self, width=None): """Legacy command. Equivalent to: pen(nib=width)""" if width is not None: self._penstyle = self._penstyle._replace(nib=max(width, 0.0001)) return self._penstyle.nib def capstyle(self, style=None): """Legacy command. Equivalent to: pen(caps=style)""" if style is not None: if style not in (BUTT, ROUND, SQUARE): raise DeviceError('Line cap style should be BUTT, ROUND or SQUARE.') self._penstyle = self._penstyle._replace(cap=style) return self._penstyle.cap def joinstyle(self, style=None): """Legacy command. Equivalent to: pen(joins=style)""" if style is not None: if style not in (MITER, ROUND, BEVEL): raise DeviceError('Line join style should be MITER, ROUND or BEVEL.') self._penstyle = self._penstyle._replace(join=style) return self._penstyle.join ### Compositing Effects ### def alpha(self, *arg): """Set the global alpha Argument: a value between 0 and 1.0 that controls the opacity of any objects drawn to the canvas. Returns: When called with no arguments, returns the current global alpha. When called with a value, returns a context manager. Context Manager: When called as part of a `with` statement, the new alpha value will apply to all drawing within the block as a `layer' effect rather than affecting each object individually. As a result, all objects within the block will be drawn with an alpha of 1.0, then the opacity will set for the accumulated graphics as if it were a single object. """ if not arg: return self._effects.alpha a = arg[0] eff = Effect(alpha=a, rollback=True) if a==1.0: a = None self._effects.alpha = a return eff def blend(self, *arg): """Set the blend-mode used for overlapping `ink' Argument: `mode`: a string with the name of a valid blend mode. Valid Mode Names: normal, clear, copy, xor, multiply, screen, overlay, darken, lighten, difference, exclusion, color-dodge, color-burn, soft-light, hard-light, hue, saturation, color, luminosity, source-in, source-out, source-atop, plusdarker, pluslighter destination-over, destination-in, destination-out, destination-atop Returns: When called with no arguments, returns the current blend mode. When called with a value, returns a context manager. Context Manager: When called as part of a `with` statement, the new blend mode will apply to all drawing within the block as a `layer' effect rather than affecting each object individually. This means all drawing within the block will be composited using the 'normal' blend mode, then the accumulated graphics will be blended to the canvas as a single group. """ if not arg: return self._effects.blend mode = arg[0] eff = Effect(blend=mode, rollback=True) if mode=='normal': mode = None self._effects.blend = mode return eff def noshadow(self): """Disable the global dropshadow""" return self.shadow(None) def shadow(self, *args, **kwargs): """Set a global dropshadow Arguments: `color`: a Color object, string, or tuple of component values `blur`: a numeric value with the blur radius `offset`: a single value specifying the number of units to nudge the shadow (down and to the right), or a 2-tuple with x & y offsets To turn dropshadows off, pass None as the `color` argument or call noshadow() Returns: When called with no arguments, returns the current Shadow object (or None). When called with a value, returns a context manager. Context Manager: When called as part of a `with` statement, the shadow will apply to all drawing within the block as a `layer' effect rather than affecting each object individually. """ if not (args or kwargs): return self._effects.shadow s = None if None in args else Shadow(*args, **kwargs) eff = Effect(shadow=s, rollback=True) self._effects.shadow = s |
| You are visitor 01402503 — GeezerVison does not store the pages you look up. Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required. |