#!/usr/bin/wish -f ## ## SCRIPT: make_donut3D_shadedEdges.tk ## ## ## PURPOSE: This Tk GUI script 'draws' a color-filled 'donut shape' ## --- including shading to a background color, around the ## two edges (outer and inner) of the donut shape. ## ## This 'donut' could be made small, for example, to be ## used as a 'bullet' decorative item --- for a list of items ## on a web page or a Tk GUI. So, in that use-case, this ## shaded object can be thought of as a 'bullet' with a ## bullet-hole in it. ## ## Source of the bullet-hole metaphor: ## An advertising picture of a metal heart with ## a 3D-looking bullet hole in it gave me the idea ## of creating an object with an edge-shaded hole in it. ## ## (The bullet hole had an indented edge, and thus the shading.) ## ## Examples of use of this donut-shaped image: ## 1) As mentioned above, this 'holy-bullet' could be used as an ## unusual 'bullet' for line items on a web page or for menu ## items in a Tk GUI 'toolchest' --- if the image is shrunk to ## a small size. ## 2) At a large size, the 3D-donut image could be used as a ## background for a logo. ## 3) At a medium size, the image could be used as a background ## for an icon. ## ## METHOD: ## ## This script makes this shaded shape via a Tk image 'structure' ## placed on a Tk canvas widget. ## ## The image is put on the canvas via a Tk canvas 'image create' command ## --- and the image is generated via 'put -to $x $y' ## commands on the image. ## ## The rectangular image-structure covers the entire canvas widget. The ## donut-shape lies within the canvas (and the image structure) --- ## with a margin around it. In the margin, outside the ## donut-shape on the canvas, a user-selected 'background' color ## is applied. The background color is also applied to the hole ## in the donut shape. ## ## REFERENCES and CREDITS: ## ## This Tk script is based on my Tk script at http://wiki.tcl.tk/37156 ## - 'GUI for Drawing 'Super-formula' shapes, with nice shaded border'. ## ## In that script, I devised a 'color-metric' v at each point x,y. ## Then v and (1-v) could be applied to a user-selected background color ## and a user-selected 'fill' color for the super-formula shape, to get ## a 'weighted-average' of the two colors, for the color at the pixel x,y. ## ## The equation for the 'super-formula' is: ## ## R(theta) = ( |cos(m*theta/4)/a| ^ n2 + |sin(m*theta/4)/b| ^ n3 ) ^ -1/n1 ## ## where the points on the border of the super-formula shape are given by ## x = R(theta) cos(theta) ## y = R(theta) sin(theta). ## ## I defined a 'color-metric' at any point x,y on the rectangular image ## on the rectangular canvas by ## v = r(x,y) / R(theta) ## where r(x,y) = sqrt(x*x + y*y). ## ## v is 0.0 in the center of the super-formula shape and ## v is 1.0 on the border of the super-formula shape. ## ## The canvas 'create image' (and 'put' commands) technique used in the ## super-formula script is similar to the technique used in two of my ## scripts that make SHADED EDGES around 'super-ellipses' and around ## color-gradient-rectangles. ## ## See ## http://wiki.tcl.tk/37004 - ## GUI for Drawing 'Super-ellipses', with nice shaded edges ## and ## http://wiki.tcl.tk/37143 - ## GUI for Drawing Rectangular 'Buttons' with nice shaded edges ## ## Those 2 scripts also use a 'color metric' v --- and the factors ## v and (1 - v) were used get a weighted average of 2 colors to ## apply that color to a pixel at x,y. ## ##+##################### ## THE SHADING TECHNIQUE (metric) for the super-ellipse: ## ## For detail on the shading technique applied to super-ellipses, ## see wiki.tcl.tk/37004 - ## GUI for Drawing 'Super-ellipses', with nice shaded edges. ## ## The edge-shading effect for the super-ellipse benefited from the equation ## for a super-ellipse --- more precisely, the equation for its edge: ## |x/a|^n + |y/b|^n = 1 ## ## The interior of the super-ellipse is given by the inequality ## |x/a|^n + |y/b|^n <= 1 ## ## The edge shading (3D effect) was obtained by using a 'metric' on the ## points x,y in the super-ellipse --- a value 'v', between 0 and 1, ## given by: ## v = |x/a|^n + |y/b|^n ## for each point inside the super-ellipse. ## ## The equation for v dictates that the value of v is 1 on the border of ## the super-ellipse and declines to 0 towards the center. ## ## (1.0 - $v) is applied to the user-selected 'unshaded' RGB-color for ## the super-ellipse --- and $v applied to the user-selected RGB ## background color. The weighted-average of the pair of RGB values ## gives us the color at any x,y point in the super-ellipse. ## ## (Note that v is not a constant. The values of x and y gave us the ## 'v' value to apply to get the 'shaded' color at x,y --- i.e. ## v is a function of x and y.) ## ##+############################################## ## DERIVATION OF A 'COLOR METRIC' FOR THE 'DONUT': ## ## Let r be the radius of the circular hole in the donut and let ## R be the radious of the outer edge of the circular donut. ## ## Let any point x,y in the rectangular image area be measured ## from the center of the rectangular image. Let 'rho' denote ## the radial distance of the point x,y from that center. So ## ## rho(x,y) = sqrt (x*x + y*y) ## ## We note that a point x,y is on the circle in the middle of ## the donut (between the hole and the outer edge) if ## rho = (R + r) / 2 ## ## We note that the quadratic form v = (rho - (R+r)/2) ^ 2 ## is a parabola. If you plot v agains rho, the graph ## touches the rho axis at (R+r)/2. ## ## To make this a suitable metric, we need to apply a factor ## such that v is 1.0 at rho = r and at rho = R. ## ## A little algebra shows that our metric is given by ## ## v = a * (rho - (R+r)/2) ^ 2 ## ## where the coefficient a is given by 1 / ((R-r)/2) ^ 2. ## ## Now we have a suitable metric, v. ## ## At a point x,y, we determine the 'shaded color' at the point by ## using a color interpolated between the user-selected 'fill' color ## (color1) of the 'donut shape' and the user-selected ## background color (color2). ## ## We calculate the 'shaded color' at x,y by calulating a weighted average ## based on applying the factor (1.0 - $v) to color1 --- and applying $v ## to color2. That is, shaded-color = (1 - v) * color1 + v * color2. ## ## We actually calculate via formulas like ## shaded-R = (1 - v) * R1 + v * R2 ## shaded-G = (1 - v) * G1 + v * G2 ## shaded-B = (1 - v) * B1 + v * B2 ## ## Thus we will get the edge-shading (the 3D effect) for the ## 'donut shape'. ## ## Actually, it turns out that 1-v and v gives a rather washed-out (too ## gradual) shading effect. It is better if we raise v to a power N ## and use v^N and (1 - v^N). It turns out that N = 3 gives pretty nice ## shading for the donut shape, but rather than hard-code the value of N, ## we provide a scale widget on the GUI so that the user can set the ## value of N. ## ##+############## ## THE GUI DESIGN: ## ## The GUI made by this Tk script contains a rectangular ## canvas widget on which the color-filled donut shape ## will be drawn. ## ## The GUI includes 2 'scale' widgets whose slider-bars can ## be used to change the values of r and R. ## ## There is also a scale for the exponent N to control the extent ## of the shading --- by using the two color weighting factors ## v^N and (1.0 - v^N). ## ## The GUI also includes a button to call a color selector GUI ## to set the 'fill' color of the donut shape. ## ## Another button calls the same color selector GUI to set a ## background color --- which is also the color we gradiate ## toward at the inner and outer boundaries of the 'donut shape'. ## ## (Note that we could put another color selector button on the ## GUI to select the border color to be a different color from ## the background color of the canvas area.) ## ## A redraw includes clearing the canvas and redrawing the ## donut shape. ## ## A redraw should be done (a) whenever any of the 3 scales change, ## (b) whenever a color button is used to change a color, and ## (c) whenever the window (and thus the canvas) is resized --- ## so that the image/shape will be redrawn in the center of ## the canvas. ## ## NOTE: We could have let the size of the canvas determine the ## size of R and eliminated the R-scale. That would have ## been a little more convenient for the user, because if ## they resize the canvas, they probably want the donut ## to resize to fit inside the canvas with about a 10% margin. ## This anticipation of what the user wants (and eliminating ## the user's need to adjust the R-scale) would come at the ## expense of a little flexibility (if we eliminate the R-scale). ## ## Another alternative, would be to keep the R-scale but ## also automatically change the size of R if the user ## resizes the window (and thus the size of the canvas). ## ## PERFORMANCE CONSIDERATIONS: ## Since the redraw has a lot of pixels to color, especially when ## the canvas is expanded to a pretty large size, a redraw may ## take several seconds. ## ## So it is probably not going to be feasible/pleasing to do redraws ## 'dynamically' with the '-command' option of the 'scale' widgets. ## ## For now, I have defined a button1-release binding on the 3 ## scale widgets to trigger a redraw --- only when the user ## finishes dragging the sliderbar of any scale. ## ## However, it should be pointed out that if erasing the ## canvas and calculating-colors and putting the colors in the ## donut shape completes within a small fraction of a second, it ## would be feasible to do the redraws 'dynamically' with each ## sliderbar, via the '-command' option. (But it might heat up ## the CPU doing those operations --- by quite a few degrees.) ## ## USING THE GENERATED IMAGE: ## A screen/window capture utility (like 'gnome-screenshot' ## on Linux) can be used to capture the GUI image in a GIF ## or PNG file, say. ## ## If necessary, an image editor (like 'mtpaint' on Linux) ## can be used to crop the window capture image. The image ## could also be down-sized --- say to make a 'bullet' image ## file or an icon-background image file. ## ## The colored image file could be used with a utility (like the ## ImageMagick 'convert' command) to change the outer&inner background ## color to TRANSPARENT, making a partially transparent GIF ## (or PNG) file. Then the semi-transparent image file could be used, ## for 'bullets' in HTML pages or in Tk GUI's --- or for the ## background of icons or buttons for use in GUIs/web-pages. ## ## The image could also be taken into a scalable vector graphics ## (SVG) editor (like Inkscape on Linux) and the SVG editor used ## to add anti-aliased, scalable text to the image. OR, the shape can ## be used as an underlying pattern to reproduce the shape as a ## scalable shape, by using curve drawing tools of the SVG editor. ## The SVG image could then be saved as a totally scalable image, ## without a raster-image needing to be stored in the SVG file. ## ##+######################################################################## ## 'CANONICAL' STRUCTURE OF THIS TK CODE: ## ## 0) Set general window & widget parms (win-name, win-position, ## win-color-scheme, fonts, widget-geometry-parms, win-size-control). ## ## 1a) Define ALL frames (and sub-frames, if any). ## 1b) Pack ALL frames and sub-frames. ## ## 2) Define all widgets in the frames. Pack them. ## ## 3) Define keyboard or mouse/touchpad/touch-sensitive-screen action ## BINDINGS, if needed. ## ## 4) Define PROCS, if needed. ## ## 5) Additional GUI INITIALIZATION (typically with one or two of ## the procs), if needed. ## ## ## Some detail about the code structure of this particular script: ## ## 1a) Define ALL frames: ## ## Top-level : '.fRbuttons' ## '.fRscales' ## '.fRcanvas' ## No sub-frames. ## ## 1b) Pack ALL frames. ## ## 2) Define all widgets in the frames (and pack them): ## ## - In '.fRbuttons': ## 1 button widget ('Exit'), (perhaps 'Help' someday) ## and ## 2 buttons (for setting the donut shape's ## fill & the background/canvas color), ## and ## 1 label widget to display the current color ## values (in hex) --- and elapsed execution time, ## as well as a 'calculation in progress' msg. ## ## - In '.fRscales': ## 1 'label' and 1 'scale' widget each for r, R, and N ## (where N is an exponent used to control the ## extent of the edge-shading.) ## ## - In '.fRcanvas': 1 'canvas' widget ## ## 3) Define bindings: ## ## - a button1-release on each of the 3 scale widgets causes a redraw ## ## NOTE: The color changes should trigger a redraw, but we do not ## need bindings to do those redraws. ## The redraws can be done in procs that are used to ## set each of the colors. ## ## 4) Define procs: ## ## - 'ReDraw' - to clear the canvas and redraw the pixels ## in the image rectangle that contains the ## donut shape ## --- for the current values of the 3 scale ## parameters and the fill & background colors. ## ## - set_scale_r_lessThan_R_redraw - to move r slider, limited by value R. ## ## - set_scale_R_greaterThan_r_redraw - to move R slider, limited by value r. ## ## - 'set_shape_color1' - shows a color selector GUI and uses the ## user-selected color to 'fill' the ## donut shape on the canvas ## ## - 'set_color_background' - shows a color selector GUI and uses the ## user-selected color to reset the color of ## the canvas background ## ## - 'ReDraw_if_canvas_resized' - to do a redraw when a ## event is detected on the canvas --- ## but only if the canvas has been resized. ## ## 5) Additional GUI initialization: Execute proc 'ReDraw' once with ## an initial, example set of parms ## --- 3 scale vars, COLOR1hex, ## COLORbkGNDhex --- ## to start with a donut shape on ## the canvas rather than a blank canvas. ## ## Also in this section, we define a binding for ## - a (resize) event on the canvas --- to cause a redraw. ##+######################################################################## ## DEVELOPED WITH: ## Tcl-Tk 8.5 on Ubuntu 9.10 (2009-october release, 'Karmic Koala'). ## ## $ wish ## % puts "$tcl_version $tk_version" ## showed 8.5 8.5 on Ubuntu 9.10 ## after Tcl-Tk 8.4 was replaced by 8.5 --- to get anti-aliased fonts. ##+####################################################################### ## MAINTENANCE HISTORY: ## Created by: Blaise Montandon 2012oct23 ## Changed by: Blaise Montandon 2012oct24 Draw the background color around ## the square containing the 'donut' ## with 'scanlines' rather than ## pixel-by-pixel --- to speed up ## drawing by a factor of about 8 ## --- from about 8 secs to 1 sec. ##+####################################################################### ##+####################################################################### ## Set general window parms (win-title,win-position). ##+####################################################################### wm title . "Edge-shaded 'donut' shape, on a single-color canvas" wm iconname . "3Ddonut" wm geometry . +15+30 ##+###################################################### ## Set the color scheme for the window and its widgets --- ## and set the initial color for the donut-shape interior ## and the canvas background (outside the donut-shape). ##+###################################################### tk_setPalette "#e0e0e0" ## Initialize the donut shape 'fill' color. # set COLOR1r 255 # set COLOR1g 255 # set COLOR1b 255 set COLOR1r 255 set COLOR1g 0 set COLOR1b 255 set COLOR1hex [format "#%02X%02X%02X" $COLOR1r $COLOR1g $COLOR1b] ## Initialize the donut shape 'boundary' color ## to gradiate to, from the 'fill' color. ## NOT USED. ## We use the background color as the color to gradiate to. # # set COLOR2r 255 # # set COLOR2g 255 # # set COLOR2b 0 # # set COLOR2r 0 # set COLOR2g 255 # set COLOR2b 255 # set COLOR2hex [format "#%02X%02X%02X" $COLOR2r $COLOR2g $COLOR2b] ## Initialize the background color for the canvas. # set COLORbkGNDr 60 # set COLORbkGNDg 60 # set COLORbkGNDb 60 set COLORbkGNDr 0 set COLORbkGNDg 0 set COLORbkGNDb 0 set COLORbkGNDhex \ [format "#%02X%02X%02X" $COLORbkGNDr $COLORbkGNDg $COLORbkGNDb] # set listboxBKGD "#f0f0f0" # set entryBKGD "#f0f0f0" ##+######################################################## ## Use a VARIABLE-WIDTH FONT for label and button widgets. ## ## Use a FIXED-WIDTH FONT for listboxes (and ## entry fields, if any). ##+######################################################## font create fontTEMP_varwidth \ -family {comic sans ms} \ -size -14 \ -weight bold \ -slant roman font create fontTEMP_SMALL_varwidth \ -family {comic sans ms} \ -size -12 \ -weight bold \ -slant roman ## Some other possible (similar) variable width fonts: ## Arial ## Bitstream Vera Sans ## DejaVu Sans ## Droid Sans ## FreeSans ## Liberation Sans ## Nimbus Sans L ## Trebuchet MS ## Verdana font create fontTEMP_fixedwidth \ -family {liberation mono} \ -size -14 \ -weight bold \ -slant roman font create fontTEMP_SMALL_fixedwidth \ -family {liberation mono} \ -size -12 \ -weight bold \ -slant roman ## Some other possible fixed width fonts (esp. on Linux): ## Andale Mono ## Bitstream Vera Sans Mono ## Courier 10 Pitch ## DejaVu Sans Mono ## Droid Sans Mono ## FreeMono ## Nimbus Mono L ## TlwgMono ##+########################################################### ## SET GEOM VARS FOR THE VARIOUS WIDGET DEFINITIONS. ## (e.g. width and height of canvas, and padding for Buttons) ##+########################################################### set initCanWidthPx 400 set initCanHeightPx 300 set minCanHeightPx 24 # set BDwidthPx_canvas 2 set BDwidthPx_canvas 0 ## BUTTON geom parameters: set PADXpx_button 0 set PADYpx_button 0 set BDwidthPx_button 2 ## LABEL geom parameters: set BDwidthPx_label 2 ## SCALE geom parameters: set BDwidthPx_scale 2 # set initScaleLengthPx 100 set scaleWidthPx 10 ##+################################################################### ## Set a MINSIZE of the window. ## ## For width, allow for the minwidth of the '.fRbuttons' frame: ## about 3 buttons (Exit,Color1,ColorBkgnd). ## We want to at least be able to see the Exit button. ## ## For height, allow ## 2 chars high for the '.fRbuttons' frame, ## 1 char high for the '.fRscales' frame, ## 24 pixels high for the '.fRcanvas' frame. ##+################################################################### set minWinWidthPx [font measure fontTEMP_varwidth \ "Exit Fill Background"] ## Add some pixels to account for right-left-side window decoration ## (about 8 pixels), about 3 x 4 pixels/widget for borders/padding for ## 3 widgets --- 3 buttons. set minWinWidthPx [expr 20 + $minWinWidthPx] ## MIN HEIGHT --- ## for the 3 sub-frames '.fRbuttons', '.fRscales', ## and '.fRcanvas'. ## Allow ## 2 char high for 'fRbuttons' ## 1 char high for 'fRscales' ## 24 pixels high for 'fRcanvas' set CharHeightPx [font metrics fontTEMP_varwidth -linespace] set minWinHeightPx [expr 3 * $CharHeightPx] ## Add about 28 pixels for top-bottom window decoration, ## about 3x4 pixels for each of the 3 stacked frames and their ## widgets (their borders/padding). set minWinHeightPx [expr $minWinHeightPx + 40] ## FOR TESTING: # puts "minWinWidthPx = $minWinWidthPx" # puts "minWinHeightPx = $minWinHeightPx" wm minsize . $minWinWidthPx $minWinHeightPx ## We allow the window to be resizable and we pack the canvas with ## '-fill both -expand 1' so that the canvas can be enlarged by enlarging ## the window. ## If you want to make the window un-resizable, ## you can use the following statement. # wm resizable . 0 0 ##+################################################################ ## DEFINE *ALL* THE FRAMES: ## ## Top-level : '.fRbuttons' '.fRscales' '.fRcanvas' ##+################################################################ # set BDwidth_frame 2 # set RELIEF_frame raised set BDwidth_frame 0 set RELIEF_frame flat frame .fRbuttons -relief $RELIEF_frame -borderwidth $BDwidth_frame frame .fRscales -relief $RELIEF_frame -borderwidth $BDwidth_frame frame .fRcanvas -relief $RELIEF_frame -borderwidth $BDwidth_frame ##+############################## ## PACK the top-level FRAMES. ##+############################## pack .fRbuttons \ .fRscales \ -side top \ -anchor nw \ -fill x \ -expand 0 pack .fRcanvas \ -side top \ -anchor nw \ -fill both \ -expand 1 ##+######################################################### ## OK. Now we are ready to define the widgets in the frames. ##+######################################################### ##+##################################################################### ## In the '.fRbuttons' FRAME --- DEFINE-and-PACK ## - an exit-button, ## and ## - 2 buttons ( to specify colors) ## and ## - a label widget, to show current color values (in hex) ##+##################################################################### button .fRbuttons.buttEXIT \ -text "Exit" \ -font fontTEMP_varwidth \ -padx $PADXpx_button \ -pady $PADYpx_button \ -relief raised \ -bd $BDwidthPx_button \ -command {exit} ## Add this button someday? # button .fRbuttons.buttHELP \ # -text "Help" \ # -font fontTEMP_varwidth \ # -padx $PADXpx_button \ # -pady $PADYpx_button \ # -relief raised \ # -bd $BDwidthPx_button \ # -command {help} button .fRbuttons.buttCOLOR1 \ -text "\ Fill Color" \ -font fontTEMP_varwidth \ -padx $PADXpx_button \ -pady $PADYpx_button \ -relief raised \ -bd $BDwidthPx_button \ -command "set_shape_color1" ## Not used. Someday? # button .fRbuttons.buttCOLOR2 \ # -text "\ # Edge-gradient # Color" \ # -font fontTEMP_varwidth \ # -padx $PADXpx_button \ # -pady $PADYpx_button \ # -relief raised \ # -bd $BDwidthPx_button \ # -command "set_shape_color2" button .fRbuttons.buttCOLORbkGND \ -text "\ Background Color" \ -font fontTEMP_varwidth \ -padx $PADXpx_button \ -pady $PADYpx_button \ -relief raised \ -bd $BDwidthPx_button \ -command "set_background_color" ## The text for labelCOLORS is set in the ReDraw proc. ## It is done there to make sure that the colors used for ## the current drawing are displayed correctly. label .fRbuttons.labelCOLORS \ -text "" \ -font fontTEMP_SMALL_varwidth \ -justify left \ -anchor w \ -relief flat \ -bd $BDwidthPx_button ##+########################################### ## Pack the widgets in the 'fRbuttons' frame. ##+########################################### pack .fRbuttons.buttEXIT \ .fRbuttons.buttCOLOR1 \ .fRbuttons.buttCOLORbkGND \ .fRbuttons.labelCOLORS \ -side left \ -anchor w \ -fill none \ -expand 0 # .fRbuttons.buttHELP \ # .fRbuttons.buttCOLOR2 \ ##+################################################################## ## In the '.fRscales' FRAME ---- DEFINE-and-PACK ## 1 LABEL and 1 SCALE widget for r ## 1 LABEL and 1 SCALE widget for R ## 1 LABEL and 1 SCALE widget for N ##+################################################################### label .fRscales.label_r \ -text "\ Inner radius r (in pixels):" \ -font fontTEMP_SMALL_varwidth \ -justify left \ -anchor w \ -relief flat \ -bd $BDwidthPx_label ## Set this widget var in the GUI initialization section ## at the bottom of this script. # set r 100 scale .fRscales.scale_r \ -from 0 -to 500 \ -resolution 1 \ -bigincrement 1 \ -repeatdelay 1000 \ -length 100 \ -font fontTEMP_SMALL_varwidth \ -variable r \ -showvalue true \ -orient hor \ -bd $BDwidthPx_scale \ -width $scaleWidthPx # -command "set_scale_r_lessThan_R_redraw" pack .fRscales.label_r \ -side left \ -anchor w \ -fill none \ -expand 0 pack .fRscales.scale_r \ -side left \ -anchor w \ -fill x \ -expand 1 ## DEFINE SCALE for R. label .fRscales.label_R \ -text "\ Outer radius R (in pixels):" \ -font fontTEMP_SMALL_varwidth \ -justify left \ -anchor w \ -relief flat \ -bd $BDwidthPx_label ## Set this widget var in the GUI initialization section ## at the bottom of this script. # set R 200 scale .fRscales.scale_R \ -from 0 -to 800 \ -resolution 1 \ -bigincrement 1 \ -repeatdelay 1000 \ -length 100 \ -font fontTEMP_SMALL_varwidth \ -variable R \ -showvalue true \ -orient hor \ -bd $BDwidthPx_scale \ -width $scaleWidthPx # -command "set_scale_R_greaterThan_r_redraw" pack .fRscales.label_R \ -side left \ -anchor w \ -fill none \ -expand 0 pack .fRscales.scale_R \ -side left \ -anchor w \ -fill x \ -expand 1 ## DEFINE SCALE for exponent N. label .fRscales.label_N \ -text "\ exponent N to control extent of edge shading" \ -font fontTEMP_SMALL_varwidth \ -justify left \ -anchor w \ -relief flat \ -bd $BDwidthPx_label ## Set this widget var in the GUI initialization section ## at the bottom of this script. # set N 12 scale .fRscales.scale_N \ -from 1 -to 30 \ -resolution 1 \ -bigincrement 1 \ -repeatdelay 1000 \ -length 60 \ -font fontTEMP_SMALL_varwidth \ -variable N \ -showvalue true \ -orient hor \ -bd $BDwidthPx_scale \ -width $scaleWidthPx # -command "ReDraw" pack .fRscales.label_N \ -side left \ -anchor w \ -fill none \ -expand 0 pack .fRscales.scale_N \ -side left \ -anchor w \ -fill x \ -expand 1 ##+###################################################### ## In the '.fRcanvas' FRAME - ## DEFINE-and-PACK the 'canvas' widget ##+###################################################### ## We set highlightthickness & borderwidth of the canvas to ## zero, as suggested on page 558, Chapter 37, 'The Canvas ## Widget', in the 4th edition of the book 'Practical ## Programming in Tcl and Tk'. ##+###################################################### canvas .fRcanvas.can \ -width $initCanWidthPx \ -height $initCanHeightPx \ -relief flat \ -highlightthickness 0 \ -borderwidth 0 pack .fRcanvas.can \ -side top \ -anchor nw \ -fill both \ -expand 1 ##+######################################## ## END OF the DEFINITION OF THE GUI WIDGETS ##+######################################## ##+############################### ## BINDINGS SECTION: ##+############################### ## The following bind causes an extra ReDraw ## when the GUI is first configured via an 'update' below ## in the GUI initialization section. ## (And 'update' causes about 40 redraws if ## we use '.' instead of '.fRcanvas.can'.) ## We move this statement to the bottom of this script. # bind .fRcanvas.can "ReDraw 0" bind .fRscales.scale_r "set_scale_r_lessThan_R_redraw 0" bind .fRscales.scale_R "set_scale_R_greaterThan_r_redraw 0" bind .fRscales.scale_N "ReDraw 0" ##+###################################################################### ## PROCS SECTION: ## ## - ReDraw - Called by button1-release bindings on the ## 3 scale widgets, by the set-color procs, ## and in the GUI initialization section at the ## bottom of this script. ## ## Draws the donut shape on the canvas for ## the current scale parameter values and for the ## current color var values. ## ## - set_scale_r_lessThan_R_redraw - called by a button1-release binding for scale-r ## ## - set_scale_R_greaterThan_r_redraw - called by a button1-release binding for scale-R ## ## - set_shape_color1 - called by color1 (fill) button '-command' ## ## - set_shape_color2 - called by color2 (outline) button '-command' ## (NOT USED, yet) ## ## - set_background_color - called by background color button '-command' ## ## - ReDraw_if_canvas_resized - called by 'bind' to canvas ## ##+####################################################################### ##+##################################################################### ## proc ReDraw - ## ## PURPOSE: ## Draws the donut shape on the canvas. ## ## We will use symmetry in the 'donut shape' to draw the upper ## left quadrant of the image 'poking' hex-colors a pixel at a time ## with calls like: ## imgID put $hexcolor -to $x $y ## where $ $y is measured (in pixels) relative to the middle of ## the rectangular canvas/image area. ## ## CALLED BY: bindings (in the BINDINGS section) --- and by set-color procs ## --- and by the GUI initialization section at the bottom of ## this script. ## ## NOTE: The 'x' argument is to avoid an error when the scale '-command' ## passes a scale value as an argument to the command. This is in ## case we try using the '-command' option of the scale widgets to ## do the redraws 'dynamically' as a sliderbar is moved. ##+##################################################################### ## For reference, our 'color-metric' is given by ## ## v = a * (rho - (R+r)/2) ^ 2 ## where ## a = 1 / ((R-r)/2) ^ 2 ## and ## rho = sqrt(x*x + y*y) ## where x,y are measured from the middle of the canvas/image rectangle. ##+##################################################################### proc ReDraw {x} { global r R N \ COLOR1r COLOR1g COLOR1b COLOR1hex \ COLORbkGNDr COLORbkGNDg COLORbkGNDb COLORbkGNDhex ## COLOR2r COLOR2g COLOR2b COLOR2hex \ ## Set the current time, for determining elapsed ## time for building the 'photo' image. set t0 [clock milliseconds] ## Indicate that drawing calculations are starting. .fRbuttons.labelCOLORS configure -text "\ Current Colors: Fill - $COLOR1hex Background - $COLORbkGNDhex ** CALCULATIONS IN PROGRESS **" ## This 'update' makes sure that this label update is displayed. update ## Change the title of the window to show calculations are in process. ## (This shows how we could put a msg in the window title bar, ## instead of in a label in the GUI.) # wm title . \ # "** DRAWING CALCULATIONS ARE IN PROGRESS ** Please wait." ## Delete the current image structure. ## We especially need to do this when the canvas has been re-sized, ## so that we can redraw the image according to the new canvas size. (?) catch {image delete imgID} ## Get the current canvas size. set curCanWidthPx [winfo width .fRcanvas.can] set curCanHeightPx [winfo height .fRcanvas.can] ## Initialize the width & height of the image that we are going to create ## --- to the size of the canvas --- ## and let us make each dimension of the image an even integer (pixels). set imgWidthPx $curCanWidthPx set imgHeightPx $curCanHeightPx if {$imgWidthPx % 2 == 1} { incr imgWidthPx -1 } if {$imgHeightPx % 2 == 1} { incr imgHeightPx -1 } ## Make the new image structure. image create photo imgID -width $imgWidthPx -height $imgHeightPx ## Put the new image 'structure' on the canvas. ## (Note to myself: Should this statement be at top or bottom of this proc? ## Does this mainly matter the first time the canvas is used?) .fRcanvas.can create image 0 0 -anchor nw -image imgID ## Get the half width and height of the image rectangle --- with which ## which is the same as the pixel-coordinates of the origin. set xmidPx [expr {$imgWidthPx / 2}] set ymidPx [expr {$imgHeightPx / 2}] ######################################################################### ## HERE IS THE 'GUTS': ## In a loop over yPx and xPx, where yPx and xPx are measured from the ## top left of the canvas/image rectangle, we calculate the hex-color ## for each pixel, from x = xPx - xmidPx and y = yPx - ymidPx, ## according to our 'color-metric': ## ## v = a * (rho - (R+r)/2) ^ 2 ## where ## a = 1 / ((R-r)/2) ^ 2 ## and ## rho = sqrt( x^2 + y^2 ) ## ## We use the symmetry of the donut shape. We loop over the pixels ## of the upper left quadrant and set the colors in the other 3 ## quadrants using the calculated pixel color in the upper-right quad. ####################################################################### ## Calc. the parts of the metric that depend on R and r. set midR [expr { ($R + double($r))/2 }] set denom [expr { ($R - double($r))/2 }] set denom [expr { $denom * $denom }] ## Set the pixel distance to the left side and the ## top side of the square containing the 'donut'. set leftPx [expr { $xmidPx - $R }] set topPx [expr { $ymidPx - $R }] for {set yPx 0} {$yPx <= $ymidPx} {incr yPx} { ## If we are above the square containing the donut ## (and in the top left quadrant), draw the entire ## horizontal scanline across the image --- at height ## $yPx --- and below the square at height $imgHeightPx - $yPx. if { $yPx < $topPx } { ## Draw the scanline at the 2 heights. (We are depending on ## the 'put' command to 'tile' the color across the image.) ## across upper half of image imgID put $COLORbkGNDhex -to \ 0 $yPx \ $imgWidthPx [expr {$yPx + 1}] ## across lower half of image imgID put $COLORbkGNDhex -to \ 0 [expr {$imgHeightPx - $yPx}] \ $imgWidthPx [expr { ($imgHeightPx - $yPx) - 1}] ## FOR TESTING: (show one pair of horiz. scanlines at a time) # update ## Skip to the next y. continue } ## END OF if { $yPx < $topPx } ## If we are below the top of the square containing the donut ## (and in the top left quadrant), draw the 'partial' ## horizontal scanline from the left of the image (x=0) ## to the left side of the square (x=leftPx). Also, draw the ## other 3 partial-scanlines in the other 3 quadrants. if { $yPx >= $topPx } { ## upper-left quadrant (left-point to right-point) imgID put $COLORbkGNDhex -to \ 0 $yPx \ $leftPx [expr {$yPx + 1}] ## lower-left quadrant (left-point to right-point) imgID put $COLORbkGNDhex -to \ 0 [expr {$imgHeightPx - $yPx}] \ $leftPx [expr { ($imgHeightPx - $yPx) - 1}] ## upper-right quadrant (right-point to left-point) imgID put $COLORbkGNDhex -to \ $imgWidthPx $yPx \ [expr {$imgWidthPx - $leftPx}] [expr {$yPx + 1}] ## lower-right quadrant (right-point to left-point) imgID put $COLORbkGNDhex -to \ $imgWidthPx [expr {$imgHeightPx - $yPx}] \ [expr {$imgWidthPx - $leftPx}] [expr { ($imgHeightPx - $yPx) - 1}] ## FOR TESTING: (show the 4 horiz. scanline-'segments' that were drawn) # update } ## END OF if { $yPx >= $topPx } ## Now we just have to draw the pixels inside the square ## containing the donut. We calc. the colors for pixels between ## between leftPx and xmidPx --- and apply the same hexcolor ## to the corresponding pixel in the other 3 quadrants --- ## for each y below topPx (and above ymidPx). ## ## Here is where we need to calc. the color-metric, v, because ## we are in a square where it is NOT all background color. ## We poke the calculated color a pixel at a time, instead of ## using horizontal scanlines. for {set xPx $leftPx} {$xPx <= $xmidPx} {incr xPx} { ## Calculate the pixel coords relative to the center of ## the canvas/image rectangle --- and calculate 'rho'. set x [expr {$xPx - $xmidPx}] set y [expr {$yPx - $ymidPx}] set rho [expr {sqrt( ($x * $x) + ($y * $y) )}] ## Calculate our color-metric: ## v = (rho - (R+r)/2) ^ 2 / denom set num [expr { $rho - $midR }] set num [expr {$num * $num}] set v [expr { $num / $denom }] ## FOR TESTING: (show vars at top-mid donut-ring) # if { $xPx == $xmidPx && $yPx == [expr {$ymidPx - $midR}] } { # puts "ReDraw > xPx: $xPx yPx: $yPx xmidPx: $xmidPx ymidPx: $ymidPx" # puts " x: $x y: $y rho: $rho R: $R r: $r midR: $midR" # puts " v: $v num: $num denom: $denom" # } ## According to the value of v, set the pixel color that we will ## put at $xPx, $yPx. ## ## If v > 1.0, the point x,y is outside the 'donut shape' ## --- or inside the donut hole, ## so we set the pixel to the background color. ## ## The shading at the edges falls off too slowly if we use v. ## --- or even v squared. A power > 2 gives good shading. if {$v > 1.0} { imgID put $COLORbkGNDhex -to $xPx $yPx ## Put the same color in the other 3 quadrants --- ## top-right, bottom-left, bottom-right. imgID put $COLORbkGNDhex -to [expr {$imgWidthPx - $xPx}] $yPx imgID put $COLORbkGNDhex -to $xPx [expr {$imgHeightPx - $yPx}] imgID put $COLORbkGNDhex -to [expr {$imgWidthPx - $xPx}] [expr {$imgHeightPx - $yPx}] } else { ## We should be inside or on the 'donut shape'. set vpow [expr {pow($v,$N)}] set oneMinusVpow [expr {1.0 - $vpow}] set Red [expr {int(($vpow * $COLORbkGNDr) + ($oneMinusVpow * $COLOR1r))}] set Grn [expr {int(($vpow * $COLORbkGNDg) + ($oneMinusVpow * $COLOR1g))}] set Blu [expr {int(($vpow * $COLORbkGNDb) + ($oneMinusVpow * $COLOR1b))}] if {$Red > 255} {set Red 255} if {$Grn > 255} {set Grn 255} if {$Blu > 255} {set Blu 255} if {$Red < 0} {set Red 0} if {$Grn < 0} {set Grn 0} if {$Blu < 0} {set Blu 0} set hexcolor [format "#%02X%02X%02X" $Red $Grn $Blu] ## Put the color at $xPx $yPx. imgID put $hexcolor -to $xPx $yPx ## Put the same color in the other 3 quadrants --- ## top-right, bottom-left, bottom-right. imgID put $hexcolor -to [expr {$imgWidthPx - $xPx}] $yPx imgID put $hexcolor -to $xPx [expr {$imgHeightPx - $yPx}] imgID put $hexcolor -to [expr {$imgWidthPx - $xPx}] [expr {$imgHeightPx - $yPx}] } ## END OF if {$v > 1.0} ## FOR TESTING: (show the 4 pixels that have just been drawn) # update } ## END OF for {set xPx 0} {$xPx < $imgWidthPx} {incr xPx} ## FOR TESTING: (show what's been drawn so far for y=yPx --- ## 2 complete horiz-scanlines with the bkgd-color OR ## 4 horiz-scanline-segments outside the square containin ## the donut (with the bkgnd-color) PLUS the 2 horiz ## lines of pixels that were drawn inside the square with ## varying color. ## NOTE: This can cause lots of redraws due to the ## binding on any action on the canvas. ## Comment this out when done testing, or change ## the canvas binding. # update } ## END OF for {set yPx 0} {$yPx < $imgHeightPx} {incr yPx} ## Make sure the text on the COLORS and PROCESSING label widgets ## is up to date. .fRbuttons.labelCOLORS configure -text "\ Current Colors: Fill - $COLOR1hex Background - $COLORbkGNDhex DRAW TIME: [expr {[clock milliseconds] - $t0}] millisecs elapsed" update ## Change the title of the window to show execution time. ## (This shows how we could put a msg in the window title bar, ## instead of in a label in the GUI.) # wm title . \ # "Redraw DONE. [expr {[clock milliseconds] - $t0}] millisecs elapsed." } ## END OF proc 'ReDraw' ##+########################################################## ## proc set_scale_r_lessThan_R_redraw ## ## PURPOSE: Make sure r is strictly less than ## R before doing the redraw. ## ## CALLED BY: a button1-release binding on .fRscales.scale_r. ##+########################################################## proc set_scale_r_lessThan_R_redraw {x} { global r R if {$r >= $R} {set r [expr {$R - 1}]} ## We do the redraw via a button1-release binding on ## scale_r,rather than using '-command ReDraw' on the scale. ## If we ever use '-command', comment this statement. ReDraw 0 } ## END OF proc 'set_scale_r_lessThan_R_redraw' ##+########################################################## ## proc set_scale_R_greaterThan_r_redraw ## ## PURPOSE: Make sure R is strictly greater than ## r before doing the redraw. ## ## CALLED BY: a button1-release binding on .fRscales.scale_R. ##+########################################################## proc set_scale_R_greaterThan_r_redraw {x} { global r R if {$R <= $r} {set R [expr {$r - 1}]} ## We do the redraw via a button1-release binding on ## scale_R, rather than using '-command ReDraw' on the scale. ## If we ever use '-command', comment this statement. ReDraw 0 } ## END OF proc 'set_scale_R_greaterThan_r_redraw' ##+##################################################################### ## proc 'set_shape_color1' ##+##################################################################### ## PURPOSE: ## ## This procedure is invoked to get an RGB triplet ## via 3 RGB slider bars on the FE Color Selector GUI. ## ## Uses that RGB value to set a 'fill' color. ## ## Arguments: none ## ## CALLED BY: .fRbuttons.buttCOLOR1 button ##+##################################################################### proc set_shape_color1 {} { global COLOR1r COLOR1g COLOR1b COLOR1hex # global feDIR_tkguis ## FOR TESTING: # puts "COLOR1r: $COLOR1r" # puts "COLOR1g: $COLOR1g" # puts "COLOR1b: $COLOR1b" set TEMPrgb [ exec \ ./sho_colorvals_via_sliders3rgb.tk \ $COLOR1r $COLOR1g $COLOR1b] # $feDIR_tkguis/sho_colorvals_via_sliders3rgb.tk \ ## FOR TESTING: # puts "TEMPrgb: $TEMPrgb" if { "$TEMPrgb" == "" } { return } scan $TEMPrgb "%s %s %s %s" r255 g255 b255 hexRGB set COLOR1hex "#$hexRGB" set COLOR1r $r255 set COLOR1g $g255 set COLOR1b $b255 ## Redraw the geometry in the new interior color. ReDraw 0 } ## END OF proc 'set_shape_color1' ##+##################################################################### ## proc 'set_shape_color2' (NOT USED, yet) ##+##################################################################### ## PURPOSE: ## ## This procedure is invoked to get an RGB triplet ## via 3 RGB slider bars on the FE Color Selector GUI. ## ## Uses that RGB value to set an 'edge' color. ## ## Arguments: none ## ## CALLED BY: .fRbuttons.buttCOLOR2 button ##+##################################################################### proc set_shape_color2 {} { global COLOR2r COLOR2g COLOR2b COLOR2hex # global feDIR_tkguis ## FOR TESTING: # puts "COLOR2r: $COLOR2r" # puts "COLOR2g: $COLOR2g" # puts "COLOR2b: $COLOR2b" set TEMPrgb [ exec \ ./sho_colorvals_via_sliders3rgb.tk \ $COLOR2r $COLOR2g $COLOR2b] # $feDIR_tkguis/sho_colorvals_via_sliders3rgb.tk \ ## FOR TESTING: # puts "TEMPrgb: $TEMPrgb" if { "$TEMPrgb" == "" } { return } scan $TEMPrgb "%s %s %s %s" r255 g255 b255 hexRGB set COLOR2hex "#$hexRGB" set COLOR2r $r255 set COLOR2g $g255 set COLOR2b $b255 ## Redraw the geometry in the new edge color. ReDraw 0 } ## END OF proc 'set_shape_color2' ##+##################################################################### ## proc 'set_background_color' ##+##################################################################### ## PURPOSE: ## ## This procedure is invoked to get an RGB triplet ## via 3 RGB slider bars on the FE Color Selector GUI. ## ## Uses that RGB value to set the color of the canvas --- ## on which all the tagged items (lines) lie. ## ## Arguments: none ## ## CALLED BY: .fRbuttons.buttCOLORbkGND button ##+##################################################################### proc set_background_color {} { global COLORbkGNDr COLORbkGNDg COLORbkGNDb COLORbkGNDhex # global feDIR_tkguis ## FOR TESTING: # puts "COLORbkGNDr: $COLORbkGNDr" # puts "COLORbkGNDg: $COLORbkGNDb" # puts "COLORbkGNDb: $COLORbkGNDb" set TEMPrgb [ exec \ ./sho_colorvals_via_sliders3rgb.tk \ $COLORbkGNDr $COLORbkGNDg $COLORbkGNDb] # $feDIR_tkguis/sho_colorvals_via_sliders3rgb.tk \ ## FOR TESTING: # puts "TEMPrgb: $TEMPrgb" if { "$TEMPrgb" == "" } { return } scan $TEMPrgb "%s %s %s %s" r255 g255 b255 hexRGB set COLORbkGNDhex "#$hexRGB" set COLORbkGNDr $r255 set COLORbkGNDg $g255 set COLORbkGNDb $b255 ## Redraw the geometry in the new background color. ReDraw 0 } ## END OF proc 'set_background_color' ##+############################################################# ## proc ReDraw_if_canvas_resized ## ## PURPOSE: To make sure we are only doing redraws if the ## of the canvas resulted in a change ## in the size of the canvas. ## ## CALLED BY: bind .fRcanvas.can ## at bottom of this script. ##+############################################################# proc ReDraw_if_canvas_resized {} { global PREVcanWidthPx PREVcanHeightPx set CURcanWidthPx [winfo width .fRcanvas.can] set CURcanHeightPx [winfo height .fRcanvas.can] if { $CURcanWidthPx != $PREVcanWidthPx || \ $CURcanHeightPx != $PREVcanHeightPx} { ReDraw 0 set PREVcanWidthPx $CURcanWidthPx set PREVcanHeightPx $CURcanHeightPx } } ## END OF ReDraw_if_canvas_resized ##+##################################################### ## Additional GUI initialization, if needed (or wanted). ##+##################################################### ## Set initial scale widget variables. set r 80 set R 140 set N 3 ## Initialize the canvas with 'ReDraw'. ## Need 'update' here to set the size of the canvas, ## because 'ReDraw' uses 'winfo' to get the width and ## height of the canvas. ## See the 'bind ' command below. update ReDraw 0 ## After this script drops into the Tk event-handling loop, ## this bind command causes redraws whenever the canvas is resized. set PREVcanWidthPx [winfo width .fRcanvas.can] set PREVcanHeightPx [winfo height .fRcanvas.can] bind .fRcanvas.can "ReDraw_if_canvas_resized"