├── .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 | 
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 | | Pupilsize (mm) | Airy disc diameter (arcminutes) | (upm * angular size Airy diameter) / angular size em (em units) |
21 |
22 |
23 |
24 | | 8.2 | 0.658 | 27.13 |
25 |
26 |
27 |
28 |
29 | | 1 | 4.494 | 185.28 |
30 |
31 |
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 |
--------------------------------------------------------------------------------