├── .gitignore ├── LightMeterScreenshot.png ├── LightMeter.roboFontExt ├── resources │ └── LightMeterButton.pdf ├── info.plist └── lib │ ├── scaleTools.py │ ├── gaussTools.py │ └── lightMeterTool.py ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.pyc 3 | -------------------------------------------------------------------------------- /LightMeterScreenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LettError/LightMeter/HEAD/LightMeterScreenshot.png -------------------------------------------------------------------------------- /LightMeter.roboFontExt/resources/LightMeterButton.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LettError/LightMeter/HEAD/LightMeter.roboFontExt/resources/LightMeterButton.pdf -------------------------------------------------------------------------------- /LightMeter.roboFontExt/info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | addToMenu 6 | 7 | 8 | path 9 | lightMeterTool.py 10 | preferredName 11 | lightMeterTool 12 | shortKey 13 | 14 | 15 | 16 | developer 17 | LettError 18 | developerURL 19 | http://letterror.com 20 | html 21 | 22 | launchAtStartUp 23 | 1 24 | mainScript 25 | lightMeterTool.py 26 | name 27 | LightMeter 28 | timeStamp 29 | 1450015215 30 | version 31 | 1.3 32 | com.robofontmechanic.Mechanic 33 | 34 | repository 35 | LettError/LightMeter 36 | summary 37 | A tool for calculating gray levels at any point in a glyph. 38 | 39 | extensionPath 40 | LightMeter.roboFontExt 41 | 42 | 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2004-2015, LettError and Erik van Blokland 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of LettEror nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LightMeter 2 | A tool for RoboFont for calculating gray levels at any point in a glyph. 3 | 4 | ![LightMeter Screenshot](https://github.com/LettError/LightMeter/blob/master/LightMeterScreenshot.png) 5 | 6 | Drag the tool over parts of a glyph to see the gray levels. The number in the blue box is a percentage. 7 | 8 | * Airy disk blurring is caused by diffraction in the eye. It is independent of focus, other factors also contribute, these are not part of this tool. 9 | * This blurring is small, but still typographically significant. 10 | * This code uses its own convolution kernel. It does not generate or process a bitmap. 11 | * Some tools for calculating the diameter of the Airy disk are in scaleTools.py 12 | * For each measurement the tool has to do a fair number of calculations. Works fine on a Mid 2014 MacBook Pro, but might be slower on older machines. Increase the chunkSize in the source if necessary. 13 | 14 | ## Example 15 | 16 | Suppose we are looking at 8 pt type from 40 cm, the angular size for the em is 24.26' arcminutes. [See on sizecalc.com](http://sizecalc.com/#distance=400millimeters&physical-size=8points&perceived-size-units=arcminutes). Also suppose the em for this font is 1000 units. 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
Pupilsize (mm)Airy disc diameter (arcminutes)(upm * angular size Airy diameter) / angular size em (em units)
8.20.65827.13
14.494185.28
32 | 33 | Airy disk diameter data from Review of Basic Principles in Review of Basic Principles in Optics, Wavefront and Wavefront Error by Austin Roorda, Ph.D. University of California, Berkeley. http://roorda.vision.berkeley.edu. 34 | 35 | ## So what does it mean? 36 | These numbers can be an indication about the physical limits of vision. As these calculations seem to show, letterforms at reading size and distance are at a scale where this kind of blurring is significant. This tool might assist thinking about the letterforms and what they should do. What's the weight in a stem, really? What happens in a counter when the radius changes? How dark is this margin? Etc. 37 | 38 | ## Controls 39 | * arrow up: increase diameter 40 | * arrow down: decrease diameter 41 | * command + arrow up: increase grid size 42 | * command + arrow dow: decrease grid size 43 | * c: clear trail 44 | * t: toggle trail 45 | * p: toggle grid 46 | * i: invert 47 | 48 | ## Install 49 | 50 | * 1 easy step from RoboFontMechanic 51 | 52 | ## To do 53 | * wire up sliders for eye distance, typesize and pupil size. 54 | * add support for non-circular kernels. 55 | 56 | -------------------------------------------------------------------------------- /LightMeter.roboFontExt/lib/scaleTools.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | """ 4 | 5 | 6 | """ 7 | 8 | AiryDiscRadius_pupilSize = [ 9 | (2.247320061255743, 1.0), 10 | (2.2052067381316998, 1.0510951391490957), 11 | (2.0482388973966312, 1.187348843546684), 12 | (1.7304747320061256, 1.4201155885592307), 13 | (1.5735068912710568, 1.562046530640052), 14 | (1.4280245022970903, 1.6755912843047087), 15 | (1.2978560490045943, 1.8175222263855297), 16 | (1.1944869831546707, 1.9310669800501867), 17 | (1.110260336906585, 2.0446117337148437), 18 | (1.037519142419602, 2.186542675795665), 19 | (0.9800918836140888, 2.300087429460322), 20 | (0.9341500765696784, 2.4193094208082115), 21 | (0.8767228177641654, 2.67478511655369), 22 | (0.8039816232771823, 2.930260812299168), 23 | (0.7465543644716692, 3.185736508044646), 24 | (0.6891271056661562, 3.4185032530571924), 25 | (0.6163859111791731, 3.929454644548149), 26 | (0.5436447166921899, 4.440406036039105), 27 | (0.501531393568147, 4.951357427530061), 28 | (0.45941807044410415, 5.462308819021017), 29 | (0.4287901990811639, 5.944874022095809), 30 | (0.37136294027565087, 6.94406785434479), 31 | (0.329249617151608, 8.0000340634261) 32 | ] 33 | 34 | maxAiryDiscDiameter = 2*max([a for a,b in AiryDiscRadius_pupilSize]) 35 | minAiryDiscDiameter = 2*min([a for a,b in AiryDiscRadius_pupilSize]) 36 | maxPupilDiameter = max([b for a,b in AiryDiscRadius_pupilSize]) 37 | minPupilDiameter = min([b for a,b in AiryDiscRadius_pupilSize]) 38 | 39 | 40 | epsilon = 1e-12 41 | 42 | def pupilSizeToAiryDiscRadius(pupilSize): 43 | """ Approximate the Airy disc radius in the eye, based on the 44 | values extracted from the Roorda graph. 45 | pupilSize in mm 46 | airyDisk in arc minutes 47 | """ 48 | if pupilSize <= AiryDiscRadius_pupilSize[0][1]: 49 | return AiryDiscRadius_pupilSize[0][0] 50 | elif pupilSize >= AiryDiscRadius_pupilSize[-1][1]: 51 | return AiryDiscRadius_pupilSize[-1][0] 52 | for i in range(1, len(AiryDiscRadius_pupilSize)-1, 1): 53 | current = AiryDiscRadius_pupilSize[i] 54 | next = AiryDiscRadius_pupilSize[i+1] 55 | #print(current[1], pupilSize, next[1]) 56 | if current[1] <= pupilSize <= next[1]: 57 | factor = (pupilSize-current[1])/(next[1]-current[1]) 58 | return current[0]+factor*(next[0]-current[0]) 59 | 60 | print("pupilSizeToAiryDiscRadius", pupilSizeToAiryDiscRadius(8)) 61 | print("pupilSizeToAiryDiscRadius", pupilSizeToAiryDiscRadius(1)) 62 | 63 | def distanceToAngular(eyeDistance, fontSize): 64 | """ 65 | eye distance in mm 66 | fontSize in point 67 | 68 | out: the angular size of the full em in arc minutes 69 | """ 70 | pt_mm = 0.352777778 71 | em_mm = (fontSize * pt_mm) 72 | print("em mm", em_mm) 73 | t = em_mm/eyeDistance 74 | return math.atan(t) * 60 75 | 76 | def pupilSizeEyeDistanceFontSizeUnitsPerEmToAiryDiameterInEm(pupilSize, eyeDistance, fontSize, unitsPerEm): 77 | angularSize = distanceToAngular(eyeDistance, fontSize) 78 | print("eyeDistance", eyeDistance) 79 | print("angularSize", math.degrees(angularSize)) 80 | print("fontSize", fontSize) 81 | airyDiameter = 2 * pupilSizeToAiryDiscRadius(pupilSize) 82 | print("airyDiameter", airyDiameter) 83 | airyFraction = unitsPerEm * airyDiameter/math.degrees(angularSize) 84 | print(airyFraction) 85 | 86 | 87 | a = distanceToAngular(400, 8) 88 | print('aa', math.degrees(a)) 89 | 90 | 91 | pupilSize = 2 92 | eyeDistance = 1000 93 | fontSize = 10 94 | unitsPerEm = 1000 95 | 96 | pupilSizeEyeDistanceFontSizeUnitsPerEmToAiryDiameterInEm(pupilSize, eyeDistance, fontSize, unitsPerEm) -------------------------------------------------------------------------------- /LightMeter.roboFontExt/lib/gaussTools.py: -------------------------------------------------------------------------------- 1 | import math 2 | from fontTools.misc.transform import Transform 3 | 4 | """ 5 | 6 | 7 | 8 | """ 9 | 10 | def getCircle(x0, y0, radius): 11 | """ 12 | http://en.wikipedia.org/wiki/Midpoint_circle_algorithm 13 | 14 | public static void DrawCircle(int x0, int y0, int radius) 15 | { 16 | int x = radius, y = 0; 17 | int radiusError = 1-x; 18 | 19 | while(x >= y) 20 | { 21 | DrawPixel(x + x0, y + y0); 22 | DrawPixel(y + x0, x + y0); 23 | DrawPixel(-x + x0, y + y0); 24 | DrawPixel(-y + x0, x + y0); 25 | DrawPixel(-x + x0, -y + y0); 26 | DrawPixel(-y + x0, -x + y0); 27 | DrawPixel(x + x0, -y + y0); 28 | DrawPixel(y + x0, -x + y0); 29 | y++; 30 | if (radiusError<0) 31 | { 32 | radiusError += 2 * y + 1; 33 | } 34 | else 35 | { 36 | x--; 37 | radiusError += 2 * (y - x + 1); 38 | } 39 | } 40 | } 41 | # Get a bitmapped circle as runlengths. 42 | >>> getCircle(0,0,5) 43 | {0: [-5, 5], 1: [-5, 5], 2: [-5, 5], 3: [-4, 4], 4: [-3, 3], 5: [-2, 2], -2: [-5, 5], -5: [-2, 2], -4: [-3, 3], -3: [-4, 4], -1: [-5, 5]} 44 | 45 | """ 46 | points = {} 47 | x = radius 48 | y = 0 49 | radiusError = 1-x 50 | while x >= y: 51 | points[y + y0] = [-x + x0, x + x0] 52 | points[x + y0] = [-y + x0, y + x0] 53 | points[-y + y0] = [-x + x0, x + x0] 54 | points[-x + y0] = [-y + x0, y + x0] 55 | y += 1 56 | if (radiusError<0): 57 | radiusError += 2 * y + 1 58 | else: 59 | x -= 1 60 | radiusError += 2 * (y - x + 1) 61 | return points 62 | 63 | def xyGaussian(x, y, a, bx, by, sigmax, sigmay): 64 | """ 65 | # Two dimensional gaussian function. 66 | # from https://en.wikipedia.org/wiki/Gaussian_function 67 | >>> xyGaussian(0, 0, 1, 0, 0, 10, 10) 68 | 1.0 69 | >>> xyGaussian(0, 0, 1, 1000, 0, 10, 10) 70 | 0.0 71 | """ 72 | return a * math.exp(-((x-bx)**2/(2*sigmax**2)+(y-by)**2/(2*sigmay**2))) 73 | 74 | def gaussian(x, amplitude, mu, sigma): 75 | """ 76 | # 77 | # from https://en.wikipedia.org/wiki/Gaussian_function 78 | >>> gaussian(0, 1, 0, 10) 79 | 1.0 80 | >>> gaussian(0, 1, 1000, 10) 81 | 0.0 82 | 83 | """ 84 | val = amplitude * math.exp(-(x - mu)**2 / sigma**2) 85 | return val 86 | 87 | def getArea(radius): 88 | """ 89 | >>> getArea(2) 90 | [(0, 1), (0, -2), (-2, 1), (0, 0), (-2, 0), (-1, -2), (-1, -1), (-1, 2), (-1, 1), (0, 2), (-2, -1), (0, -1), (1, 0), (1, -1), (1, 1), (-1, 0)] 91 | 92 | """ 93 | grid = set() 94 | lines = getCircle(0, 0, radius) 95 | for y, (xMin, xMax) in lines.items(): 96 | for x in range(xMin, xMax): 97 | grid.add((x,y)) 98 | return list(grid) 99 | 100 | def getKernel(radius, amplitude=1, depth=50, angle=0): 101 | """ 102 | >>> a = getKernel(5) 103 | >>> a[(0,0)] 104 | 0.03662899097662087 105 | >>> a[(2,0)] 106 | 0.02371649522786113 107 | """ 108 | t = Transform() 109 | t = t.rotate(angle) 110 | lines = getCircle(0, 0, radius) 111 | sigma = (radius+1) / math.sqrt(2*math.log(depth)) 112 | grid = {} 113 | total = 0 114 | for y, (xMin, xMax) in lines.items(): 115 | for x in range(xMin, xMax+1, 1): 116 | g = xyGaussian(x,y, amplitude, 0, 0, sigma, sigma) 117 | grid[(x,y)]=grid.get((x,y), 0)+g 118 | total += g 119 | # scale the amplitude based on the total 120 | grid = {k: float(v)/total for k, v in grid.items()} 121 | # rotate the grid 122 | grid = grid 123 | new = {} 124 | for k, v in grid.items(): 125 | k_ = t.transformPoint(k) 126 | new[k_] = v 127 | grid = new 128 | return grid 129 | 130 | if __name__ == "__main__": 131 | import doctest 132 | from pprint import pprint 133 | doctest.testmod() 134 | pprint(getKernel(7, 50)) 135 | -------------------------------------------------------------------------------- /LightMeter.roboFontExt/lib/lightMeterTool.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from AppKit import NSColor, NSFont, NSFontAttributeName, NSForegroundColorAttributeName, NSCursor 4 | from mojo.events import installTool, EditingTool, BaseEventTool, setActiveEventTool 5 | from mojo.drawingTools import * 6 | from mojo.UI import UpdateCurrentGlyphView 7 | from defconAppKit.windows.baseWindow import BaseWindowController 8 | 9 | from gaussTools import * 10 | import vanilla 11 | 12 | from mojo.extensions import ExtensionBundle 13 | shapeBundle = ExtensionBundle("LightMeter") 14 | toolbarIcon = shapeBundle.get("LightMeterButton") 15 | 16 | """ 17 | 18 | 19 | LightMeterTool 02 20 | 21 | This is a sort of a light meter for the glyph window. 22 | Running this script will install a tool in the toolbar. 23 | 24 | When selected, the tool draws trails of gray rectangles. 25 | The gray level of a pixel corresponds to the light 26 | contributed by the white and black areas around the cursor. 27 | The blue number indicates the percentage. 28 | 29 | 100 = white + dot 30 | n = some level of gray 31 | 0 = black + dot 32 | 33 | Keys: 34 | arrowkey up: increase the measuring diameter. 35 | arrowkey down: decrease the measuring diameter. 36 | 37 | Command + arrowkey up: increase the sampling size 38 | Command + arrowkey down: decrease the sampling size 39 | 40 | Command + click: clear all the samples. 41 | 42 | t show tail 43 | p fit to grid 44 | c clear tail 45 | i invert 46 | 47 | Theory: 48 | When the light passes through the optical system of the eye, 49 | it is diffracted a little bit. This is noticable on the edges. 50 | The amount of diffraction is small, but so are the letters. 51 | The diameter of the measurement can be related to the 52 | pupil size, the typesize and the reading distance. 53 | 54 | 8 pt type, at 40 cm = 24.26' arcminutes 55 | 56 | Pupilsize, Airy disc diameter, em units 57 | 8.2 mm: 0.658' arcminutes, 27.13 units 58 | 1 mm: 4.494' arcminutes, 185.28 units 59 | 60 | See pupilSizeToAiryDiscRadius() in scaleTools.py 61 | 62 | So it's all scientific and precise? 63 | No. This is a simple interpretation of the math and basic physics 64 | that are relevant for all this. This code does not simulate actual 65 | light reflecting on actual paper, while being held in shaky hands, 66 | and viewed with teary eyes. 67 | 68 | So why bother? 69 | Because trying to find the right values is better than applying a blur until it looks good. 70 | 71 | To Do: 72 | - invent some UI to incorporate sliders for pupil size and distance 73 | - 74 | """ 75 | 76 | 77 | class LightMeterTool(BaseEventTool): 78 | 79 | lightMeterToolPrefsLibKey = "com.letterror.lightMeter.prefs" 80 | textAttributes = { 81 | NSFontAttributeName : NSFont.systemFontOfSize_(10), 82 | NSForegroundColorAttributeName : NSColor.whiteColor(), 83 | } 84 | 85 | def setup(self): 86 | self.sliderWindow = None 87 | self.insides = {} 88 | self._hits = {} 89 | self._misses = {} 90 | self.kernel = None 91 | self.diameterStep = 5 92 | self.fullColorMarkerSize = 2 93 | self.diameterMarkerWidth = 0.4 94 | self.toolStep = 2 95 | self.isResizing = False 96 | self.pts = [] 97 | self.dupes = set() 98 | self.samples = {} 99 | self.lastPoint = None 100 | self._insideColor = (35/255.0,60/255.0,29/255.0) # photoshop 101 | self._outsideColor = (255/255.0,234/255.0,192/255.0) 102 | 103 | self.defaultPrefs = { 104 | 'drawTail': False, 105 | 'toolStyle': 'fluid', # grid 106 | 'invert': False, 107 | 'diameter': 200, 108 | 'toolDiameter': 30, 109 | 'chunkSize': 5, 110 | } 111 | self.prefs = {} 112 | self.getPrefs() 113 | 114 | def getPrefs(self): 115 | # read the prefs from the font lib 116 | # so we have consistent tool prefs 117 | # between glyphs 118 | g = self.getGlyph() 119 | if g is None: 120 | # no font? no parent? 121 | self.prefs.update(self.defaultPrefs) 122 | return 123 | parentLib = g.getParent().lib 124 | if self.lightMeterToolPrefsLibKey in parentLib: 125 | self.prefs.update(parentLib[self.lightMeterToolPrefsLibKey]) 126 | else: 127 | self.prefs.update(self.defaultPrefs) 128 | 129 | def storePrefs(self): 130 | # write the current tool preferences to the font.lib 131 | g = self.getGlyph() 132 | parentLib = g.getParent().lib 133 | if not self.lightMeterToolPrefsLibKey in parentLib: 134 | parentLib[self.lightMeterToolPrefsLibKey] = {} 135 | parentLib[self.lightMeterToolPrefsLibKey].update(self.prefs) 136 | 137 | def getKernel(self): 138 | kernelRadius = int(round((0.5*self.prefs['diameter'])/self.prefs['chunkSize'])) 139 | self.kernel = getKernel(kernelRadius, angle=math.radians(30)) 140 | 141 | def grid(self, pt): 142 | x, y = pt 143 | x = x - x%self.prefs['toolDiameter'] + 0.5*self.prefs['toolDiameter'] 144 | y = y - y%self.prefs['toolDiameter'] + 0.5*self.prefs['toolDiameter'] 145 | return x, y 146 | 147 | def draw(self, scale): 148 | # drawBackground(self 149 | if not hasattr(self, "prefs"): 150 | return 151 | s = self.prefs['toolDiameter'] # / scale 152 | last = None 153 | if not self.pts: return 154 | if self.prefs['drawTail']: 155 | drawThese = self.pts[:] 156 | else: 157 | drawThese = [self.pts[-1]] 158 | stroke(None) 159 | for (x,y), level, tdm in drawThese: 160 | key = (x,y),tdm 161 | if self.prefs['invert']: 162 | fill(1-level) 163 | else: 164 | fill(level) 165 | stroke(None) 166 | if self.prefs['toolStyle'] == "grid": 167 | rect(x-.5*tdm, y-0.5*tdm, tdm, tdm) 168 | else: 169 | oval(x-.5*tdm, y-0.5*tdm, tdm, tdm) 170 | if round(level, 3) == 0: 171 | stroke(None) 172 | fill(1) 173 | oval(x-.5*self.fullColorMarkerSize, y-.5*self.fullColorMarkerSize, self.fullColorMarkerSize, self.fullColorMarkerSize) 174 | elif round(level,3) == 1.0: 175 | stroke(None) 176 | fill(0) 177 | oval(x-.5*self.fullColorMarkerSize, y-.5*self.fullColorMarkerSize, self.fullColorMarkerSize, self.fullColorMarkerSize) 178 | 179 | self.drawDiameter(x, y, scale, showSize=self.isResizing) 180 | 181 | (x,y), level, tdm = drawThese[-1] 182 | if self.prefs['invert']: 183 | fill(1-level) 184 | else: 185 | fill(level) 186 | stroke(None) 187 | if self.prefs['toolStyle'] == "grid": 188 | rect(x-.5*tdm, y-0.5*tdm, tdm, tdm) 189 | else: 190 | oval(x-.5*tdm, y-0.5*tdm, tdm, tdm) 191 | 192 | tp, level, tdm = self.pts[-1] 193 | self.getNSView()._drawTextAtPoint( 194 | "%3.2f"%(100-100*level), 195 | self.textAttributes, 196 | tp, 197 | yOffset=-30, 198 | drawBackground=True, 199 | backgroundColor=NSColor.blueColor()) 200 | 201 | def drawDiameter(self, x, y, scale=1, showSize=False): 202 | 203 | # draw points contributing to the level. 204 | s = self.prefs['chunkSize'] 205 | stroke(None) 206 | for (px,py), v in self._hits.items(): 207 | fill(self._outsideColor[0],self._outsideColor[1],self._outsideColor[2], v*80) 208 | oval(px-0.5*s, py-0.5*s, s, s) 209 | for (px,py), v in self._misses.items(): 210 | fill(self._insideColor[0],self._insideColor[1],self._insideColor[2], v*80) 211 | oval(px-0.5*s, py-0.5*s, s, s) 212 | 213 | stroke(0.5) 214 | strokeWidth(self.diameterMarkerWidth*scale) 215 | fill(None) 216 | oval(x-0.5*self.prefs['diameter'], y-0.5*self.prefs['diameter'], self.prefs['diameter'], self.prefs['diameter']) 217 | 218 | if showSize: 219 | tp = x, y + 20*scale 220 | self.getNSView()._drawTextAtPoint( 221 | u"⌀ %3.2f"%(self.prefs['diameter']), 222 | self.textAttributes, 223 | tp, 224 | yOffset=(.5*self.prefs['diameter'])/scale, 225 | drawBackground=True, 226 | backgroundColor=NSColor.grayColor()) 227 | 228 | def mouseDown(self, point, event): 229 | mods = self.getModifiers() 230 | cmd = mods['commandDown'] > 0 231 | self.isResizing = False 232 | if cmd: 233 | self.clear() 234 | 235 | def clear(self): 236 | self.pts = [] 237 | self.dupes = set() 238 | self.samples = {} 239 | 240 | def drawMargins(self): 241 | # sample the whole box 242 | g = self.getGlyph() 243 | if g.box is None: 244 | return 245 | xMin, yMin, xMax, yMax = self.getGlyph().box 246 | for y in range(yMin, yMax+self.prefs['toolDiameter'], self.prefs['toolDiameter']): 247 | samplePoint = self.grid((xMin,y)) 248 | self.processPoint(samplePoint) 249 | samplePoint = self.grid((0,y)) 250 | self.processPoint(samplePoint) 251 | samplePoint = self.grid((xMax,y)) 252 | self.processPoint(samplePoint) 253 | samplePoint = self.grid((g.width,y)) 254 | self.processPoint(samplePoint) 255 | 256 | def keyDown(self, event): 257 | letter = event.characters() 258 | if letter == "i": 259 | # invert the paint color on drawing 260 | self.prefs['invert'] = not self.prefs['invert'] 261 | self.storePrefs() 262 | elif letter == "M": 263 | # draw the whole bounds 264 | self.drawMargins() 265 | elif letter == "p": 266 | if self.prefs['toolStyle'] == "grid": 267 | self.prefs['toolStyle'] = "fluid" 268 | else: 269 | self.prefs['toolStyle'] = 'grid' 270 | self.storePrefs() 271 | elif letter == "t": 272 | self.prefs['drawTail'] = not self.prefs['drawTail'] 273 | self.storePrefs() 274 | elif letter == "c": 275 | self.clear() 276 | 277 | mods = self.getModifiers() 278 | cmd = mods['commandDown'] > 0 279 | option = mods['optionDown'] > 0 280 | arrows = self.getArrowsKeys() 281 | if cmd: 282 | # change the grid size 283 | if arrows['up']: 284 | self.prefs['toolDiameter'] += self.toolStep 285 | elif arrows['down']: 286 | self.prefs['toolDiameter'] -= self.toolStep 287 | self.prefs['toolDiameter'] = abs(self.prefs['toolDiameter']) 288 | #self.clear() 289 | self.getKernel() 290 | self.calcSample(self.lastPoint) 291 | self.storePrefs() 292 | elif option: 293 | # change the chunk size 294 | if arrows['left']: 295 | self.prefs['chunkSize'] += 1 296 | elif arrows['right']: 297 | self.prefs['chunkSize'] = max(4, min(40, self.prefs['chunkSize'] - 1)) 298 | self.getKernel() 299 | self.calcSample(self.lastPoint) 300 | self.storePrefs() 301 | else: 302 | self.isResizing = True 303 | if arrows['up']: 304 | self.prefs['diameter'] += self.diameterStep 305 | elif arrows['down']: 306 | self.prefs['diameter'] -= self.diameterStep 307 | self.prefs['diameter'] = abs(self.prefs['diameter']) 308 | #self.clear() 309 | self.getKernel() 310 | self.calcSample(self.lastPoint) 311 | self.storePrefs() 312 | UpdateCurrentGlyphView() 313 | 314 | def mouseDragged(self, point, delta): 315 | """ Calculate the blurred gray level for this point. """ 316 | self.isResizing = False 317 | self.lastPoint = samplePoint = point.x, point.y 318 | if self.prefs['toolStyle'] == "grid": 319 | self.lastPoint = samplePoint = self.grid(samplePoint) 320 | self.processPoint(samplePoint) 321 | 322 | def processPoint(self, samplePoint): 323 | if (self.prefs['toolDiameter'], samplePoint) in self.dupes: 324 | level = self.samples.get((samplePoint, self.prefs['toolDiameter']))['level'] 325 | i = self.pts.index((samplePoint, level, self.prefs['toolDiameter'])) 326 | del self.pts[i] 327 | self.pts.append((samplePoint, level, self.prefs['toolDiameter'])) 328 | self.calcSample(samplePoint) 329 | 330 | def calcSample(self, samplePoint): 331 | if samplePoint is None: 332 | return 333 | if not self.kernel: 334 | self.getKernel() 335 | level = 0 336 | self._insides = {} 337 | self._hits = {} 338 | self._misses = {} 339 | 340 | nsPathObject = self.getGlyph().getRepresentation("defconAppKit.NSBezierPath") 341 | for pos, val in self.kernel.items(): 342 | thisPos = samplePoint[0]+pos[0]*self.prefs['chunkSize'], samplePoint[1]+pos[1]*self.prefs['chunkSize'] 343 | #a = math.atan2(pos[0], pos[1]) 344 | if thisPos not in self._insides: 345 | self._insides[thisPos] = nsPathObject.containsPoint_(thisPos) 346 | if not self._insides[thisPos]: 347 | level += self.kernel.get(pos) 348 | self._hits[thisPos] = self.kernel.get(pos) 349 | else: 350 | self._misses[thisPos] = self.kernel.get(pos) 351 | self.pts.append((samplePoint, level, self.prefs['toolDiameter'])) 352 | self.samples[(samplePoint, self.prefs['toolDiameter'])] = dict(level=level) 353 | self.dupes.add((self.prefs['toolDiameter'], samplePoint)) 354 | return level 355 | 356 | def getToolbarTip(self): 357 | return 'LightMeter' 358 | 359 | def getToolbarIcon(self): 360 | ## return the toolbar icon 361 | return toolbarIcon 362 | 363 | 364 | p = LightMeterTool() 365 | installTool(p) 366 | --------------------------------------------------------------------------------