f8668c1e |
1 | #!/usr/bin/env python |
2 | |
3 | import math |
4 | |
5 | # Python code which draws the PuTTY icon components at a range of |
6 | # sizes. |
7 | |
8 | # TODO |
9 | # ---- |
10 | # |
11 | # - use of alpha blending |
12 | # + try for variable-transparency borders |
13 | # |
14 | # - can we integrate the Mac icons into all this? Do we want to? |
15 | |
16 | def pixel(x, y, colour, canvas): |
17 | canvas[(int(x),int(y))] = colour |
18 | |
19 | def overlay(src, x, y, dst): |
20 | x = int(x) |
21 | y = int(y) |
22 | for (sx, sy), colour in src.items(): |
23 | dst[sx+x, sy+y] = blend(colour, dst.get((sx+x, sy+y), cT)) |
24 | |
25 | def finalise(canvas): |
26 | for k in canvas.keys(): |
27 | canvas[k] = finalisepix(canvas[k]) |
28 | |
29 | def bbox(canvas): |
30 | minx, miny, maxx, maxy = None, None, None, None |
31 | for (x, y) in canvas.keys(): |
32 | if minx == None: |
33 | minx, miny, maxx, maxy = x, y, x+1, y+1 |
34 | else: |
35 | minx = min(minx, x) |
36 | miny = min(miny, y) |
37 | maxx = max(maxx, x+1) |
38 | maxy = max(maxy, y+1) |
39 | return (minx, miny, maxx, maxy) |
40 | |
41 | def topy(canvas): |
42 | miny = {} |
43 | for (x, y) in canvas.keys(): |
44 | miny[x] = min(miny.get(x, y), y) |
45 | return miny |
46 | |
47 | def render(canvas, minx, miny, maxx, maxy): |
48 | w = maxx - minx |
49 | h = maxy - miny |
50 | ret = [] |
51 | for y in range(h): |
52 | ret.append([outpix(cT)] * w) |
53 | for (x, y), colour in canvas.items(): |
54 | if x >= minx and x < maxx and y >= miny and y < maxy: |
55 | ret[y-miny][x-minx] = outpix(colour) |
56 | return ret |
57 | |
58 | # Code to actually draw pieces of icon. These don't generally worry |
59 | # about positioning within a canvas; they just draw at a standard |
60 | # location, return some useful coordinates, and leave composition |
61 | # to other pieces of code. |
62 | |
63 | sqrthash = {} |
64 | def memoisedsqrt(x): |
65 | if not sqrthash.has_key(x): |
66 | sqrthash[x] = math.sqrt(x) |
67 | return sqrthash[x] |
68 | |
69 | BR, TR, BL, TL = range(4) # enumeration of quadrants for border() |
70 | |
71 | def border(canvas, thickness, squarecorners): |
72 | # I haven't yet worked out exactly how to do borders in a |
73 | # properly alpha-blended fashion. |
74 | # |
75 | # When you have two shades of dark available (half-dark H and |
76 | # full-dark F), the right sequence of circular border sections |
77 | # around a pixel x starts off with these two layouts: |
78 | # |
79 | # H F |
80 | # HxH FxF |
81 | # H F |
82 | # |
83 | # Where it goes after that I'm not entirely sure, but I'm |
84 | # absolutely sure those are the right places to start. However, |
85 | # every automated algorithm I've tried has always started off |
86 | # with the two layouts |
87 | # |
88 | # H HHH |
89 | # HxH HxH |
90 | # H HHH |
91 | # |
92 | # which looks much worse. This is true whether you do |
93 | # pixel-centre sampling (define an inner circle and an outer |
94 | # circle with radii differing by 1, set any pixel whose centre |
95 | # is inside the inner circle to F, any pixel whose centre is |
96 | # outside the outer one to nothing, interpolate between the two |
97 | # and round sensibly), _or_ whether you plot a notional circle |
98 | # of a given radius and measure the actual _proportion_ of each |
99 | # pixel square taken up by it. |
100 | # |
101 | # It's not clear what I should be doing to prevent this. One |
102 | # option is to attempt error-diffusion: Ian Jackson proved on |
103 | # paper that if you round each pixel's ideal value to the |
104 | # nearest of the available output values, then measure the |
105 | # error at each pixel, propagate that error outwards into the |
106 | # original values of the surrounding pixels, and re-round |
107 | # everything, you do get the correct second stage. However, I |
108 | # haven't tried it at a proper range of radii. |
109 | # |
110 | # Another option is that the automated mechanisms described |
111 | # above would be entirely adequate if it weren't for the fact |
112 | # that the human visual centres are adapted to detect |
113 | # horizontal and vertical lines in particular, so the only |
114 | # place you have to behave a bit differently is at the ends of |
115 | # the top and bottom row of pixels in the circle, and the top |
116 | # and bottom of the extreme columns. |
117 | # |
118 | # For the moment, what I have below is a very simple mechanism |
119 | # which always uses only one alpha level for any given border |
120 | # thickness, and which seems to work well enough for Windows |
121 | # 16-colour icons. Everything else will have to wait. |
122 | |
123 | thickness = memoisedsqrt(thickness) |
124 | |
125 | if thickness < 0.9: |
126 | darkness = 0.5 |
127 | else: |
128 | darkness = 1 |
129 | if thickness < 1: thickness = 1 |
130 | thickness = round(thickness - 0.5) + 0.3 |
131 | |
132 | dmax = int(round(thickness)) |
133 | if dmax < thickness: dmax = dmax + 1 |
134 | |
135 | cquadrant = [[0] * (dmax+1) for x in range(dmax+1)] |
136 | squadrant = [[0] * (dmax+1) for x in range(dmax+1)] |
137 | |
138 | for x in range(dmax+1): |
139 | for y in range(dmax+1): |
140 | if max(x, y) < thickness: |
141 | squadrant[x][y] = darkness |
142 | if memoisedsqrt(x*x+y*y) < thickness: |
143 | cquadrant[x][y] = darkness |
144 | |
145 | bvalues = {} |
146 | for (x, y), colour in canvas.items(): |
147 | for dx in range(-dmax, dmax+1): |
148 | for dy in range(-dmax, dmax+1): |
149 | quadrant = 2 * (dx < 0) + (dy < 0) |
150 | if (x, y, quadrant) in squarecorners: |
151 | bval = squadrant[abs(dx)][abs(dy)] |
152 | else: |
153 | bval = cquadrant[abs(dx)][abs(dy)] |
154 | if bvalues.get((x+dx,y+dy),0) < bval: |
155 | bvalues[(x+dx,y+dy)] = bval |
156 | |
157 | for (x, y), value in bvalues.items(): |
158 | if not canvas.has_key((x,y)): |
159 | canvas[(x,y)] = dark(value) |
160 | |
161 | def sysbox(size): |
162 | canvas = {} |
163 | |
164 | # The system box of the computer. |
165 | |
166 | height = int(round(3*size)) |
167 | width = int(round(17*size)) |
168 | depth = int(round(2*size)) |
169 | highlight = int(round(1*size)) |
170 | bothighlight = int(round(0.49*size)) |
171 | |
172 | floppystart = int(round(19*size)) # measured in half-pixels |
173 | floppyend = int(round(29*size)) # measured in half-pixels |
174 | floppybottom = height - bothighlight |
175 | floppyrheight = 0.7 * size |
176 | floppyheight = int(round(floppyrheight)) |
177 | if floppyheight < 1: |
178 | floppyheight = 1 |
179 | floppytop = floppybottom - floppyheight |
180 | |
181 | # The front panel is rectangular. |
182 | for x in range(width): |
183 | for y in range(height): |
184 | grey = 3 |
185 | if x < highlight or y < highlight: |
186 | grey = grey + 1 |
187 | if x >= width-highlight or y >= height-bothighlight: |
188 | grey = grey - 1 |
189 | if y < highlight and x >= width-highlight: |
190 | v = (highlight-1-y) - (x-(width-highlight)) |
191 | if v < 0: |
192 | grey = grey - 1 |
193 | elif v > 0: |
194 | grey = grey + 1 |
195 | if y >= floppytop and y < floppybottom and \ |
196 | 2*x+2 > floppystart and 2*x < floppyend: |
197 | if 2*x >= floppystart and 2*x+2 <= floppyend and \ |
198 | floppyrheight >= 0.7: |
199 | grey = 0 |
200 | else: |
201 | grey = 2 |
202 | pixel(x, y, greypix(grey/4.0), canvas) |
203 | |
204 | # The side panel is a parallelogram. |
205 | for x in range(depth): |
206 | for y in range(height+1): |
207 | pixel(x+width, y-(x+1), greypix(0.5), canvas) |
208 | |
209 | # The top panel is another parallelogram. |
210 | for x in range(width-1): |
211 | for y in range(depth): |
212 | grey = 3 |
213 | if x >= width-1 - highlight: |
214 | grey = grey + 1 |
215 | pixel(x+(y+1), -(y+1), greypix(grey/4.0), canvas) |
216 | |
217 | # And draw a border. |
218 | border(canvas, size, []) |
219 | |
220 | return canvas |
221 | |
222 | def monitor(size): |
223 | canvas = {} |
224 | |
225 | # The computer's monitor. |
226 | |
227 | height = int(round(9.55*size)) |
228 | width = int(round(11*size)) |
229 | surround = int(round(1*size)) |
230 | botsurround = int(round(2*size)) |
231 | sheight = height - surround - botsurround |
232 | swidth = width - 2*surround |
233 | depth = int(round(2*size)) |
234 | highlight = int(round(math.sqrt(size))) |
235 | shadow = int(round(0.55*size)) |
236 | |
237 | # The front panel is rectangular. |
238 | for x in range(width): |
239 | for y in range(height): |
240 | if x >= surround and y >= surround and \ |
241 | x < surround+swidth and y < surround+sheight: |
242 | # Screen. |
243 | sx = (float(x-surround) - swidth/3) / swidth |
244 | sy = (float(y-surround) - sheight/3) / sheight |
245 | shighlight = 1.0 - (sx*sx+sy*sy)*0.27 |
246 | pix = bluepix(shighlight) |
247 | if x < surround+shadow or y < surround+shadow: |
248 | pix = blend(cD, pix) # sharp-edged shadow on top and left |
249 | else: |
250 | # Complicated double bevel on the screen surround. |
251 | |
252 | # First, the outer bevel. We compute the distance |
253 | # from this pixel to each edge of the front |
254 | # rectangle. |
255 | list = [ |
256 | (x, +1), |
257 | (y, +1), |
258 | (width-1-x, -1), |
259 | (height-1-y, -1) |
260 | ] |
261 | # Now sort the list to find the distance to the |
262 | # _nearest_ edge, or the two joint nearest. |
263 | list.sort() |
264 | # If there's one nearest edge, that determines our |
265 | # bevel colour. If there are two joint nearest, our |
266 | # bevel colour is their shared one if they agree, |
267 | # and neutral otherwise. |
268 | outerbevel = 0 |
269 | if list[0][0] < list[1][0] or list[0][1] == list[1][1]: |
270 | if list[0][0] < highlight: |
271 | outerbevel = list[0][1] |
272 | |
273 | # Now, the inner bevel. We compute the distance |
274 | # from this pixel to each edge of the screen |
275 | # itself. |
276 | list = [ |
277 | (surround-1-x, -1), |
278 | (surround-1-y, -1), |
279 | (x-(surround+swidth), +1), |
280 | (y-(surround+sheight), +1) |
281 | ] |
282 | # Now we sort to find the _maximum_ distance, which |
283 | # conveniently ignores any less than zero. |
284 | list.sort() |
285 | # And now the strategy is pretty much the same as |
286 | # above, only we're working from the opposite end |
287 | # of the list. |
288 | innerbevel = 0 |
289 | if list[-1][0] > list[-2][0] or list[-1][1] == list[-2][1]: |
290 | if list[-1][0] >= 0 and list[-1][0] < highlight: |
291 | innerbevel = list[-1][1] |
292 | |
293 | # Now we know the adjustment we want to make to the |
294 | # pixel's overall grey shade due to the outer |
295 | # bevel, and due to the inner one. We break a tie |
296 | # in favour of a light outer bevel, but otherwise |
297 | # add. |
298 | grey = 3 |
299 | if outerbevel > 0 or outerbevel == innerbevel: |
300 | innerbevel = 0 |
301 | grey = grey + outerbevel + innerbevel |
302 | |
303 | pix = greypix(grey / 4.0) |
304 | |
305 | pixel(x, y, pix, canvas) |
306 | |
307 | # The side panel is a parallelogram. |
308 | for x in range(depth): |
309 | for y in range(height): |
310 | pixel(x+width, y-x, greypix(0.5), canvas) |
311 | |
312 | # The top panel is another parallelogram. |
313 | for x in range(width): |
314 | for y in range(depth-1): |
315 | pixel(x+(y+1), -(y+1), greypix(0.75), canvas) |
316 | |
317 | # And draw a border. |
318 | border(canvas, size, [(0,int(height-1),BL)]) |
319 | |
320 | return canvas |
321 | |
322 | def computer(size): |
323 | # Monitor plus sysbox. |
324 | m = monitor(size) |
325 | s = sysbox(size) |
326 | x = int(round((2+size/(size+1))*size)) |
327 | y = int(round(4*size)) |
328 | mb = bbox(m) |
329 | sb = bbox(s) |
330 | xoff = sb[0] - mb[0] + x |
331 | yoff = sb[3] - mb[3] - y |
332 | overlay(m, xoff, yoff, s) |
333 | return s |
334 | |
335 | def lightning(size): |
336 | canvas = {} |
337 | |
338 | # The lightning bolt motif. |
339 | |
340 | # We always want this to be an even number of pixels in span. |
341 | width = round(7*size) * 2 |
342 | height = round(8*size) * 2 |
343 | |
344 | # The outer edge of each side of the bolt goes to this point. |
345 | outery = round(8.4*size) |
346 | outerx = round(11*size) |
347 | |
348 | # And the inner edge goes to this point. |
349 | innery = height - 1 - outery |
350 | innerx = round(7*size) |
351 | |
352 | for y in range(int(height)): |
353 | list = [] |
354 | if y <= outery: |
355 | list.append(width-1-int(outerx * float(y) / outery + 0.3)) |
356 | if y <= innery: |
357 | list.append(width-1-int(innerx * float(y) / innery + 0.3)) |
358 | y0 = height-1-y |
359 | if y0 <= outery: |
360 | list.append(int(outerx * float(y0) / outery + 0.3)) |
361 | if y0 <= innery: |
362 | list.append(int(innerx * float(y0) / innery + 0.3)) |
363 | list.sort() |
364 | for x in range(int(list[0]), int(list[-1]+1)): |
365 | pixel(x, y, cY, canvas) |
366 | |
367 | # And draw a border. |
368 | border(canvas, size, [(int(width-1),0,TR), (0,int(height-1),BL)]) |
369 | |
370 | return canvas |
371 | |
372 | def document(size): |
373 | canvas = {} |
374 | |
375 | # The document used in the PSCP/PSFTP icon. |
376 | |
377 | width = round(13*size) |
378 | height = round(16*size) |
379 | |
380 | lineht = round(1*size) |
381 | if lineht < 1: lineht = 1 |
382 | linespc = round(0.7*size) |
383 | if linespc < 1: linespc = 1 |
384 | nlines = int((height-linespc)/(lineht+linespc)) |
385 | height = nlines*(lineht+linespc)+linespc # round this so it fits better |
386 | |
387 | # Start by drawing a big white rectangle. |
388 | for y in range(int(height)): |
389 | for x in range(int(width)): |
390 | pixel(x, y, cW, canvas) |
391 | |
392 | # Now draw lines of text. |
393 | for line in range(nlines): |
394 | # Decide where this line of text begins. |
395 | if line == 0: |
396 | start = round(4*size) |
397 | elif line < 5*nlines/7: |
398 | start = round((line - (nlines/7)) * size) |
399 | else: |
400 | start = round(1*size) |
401 | if start < round(1*size): |
402 | start = round(1*size) |
403 | # Decide where it ends. |
404 | endpoints = [10, 8, 11, 6, 5, 7, 5] |
405 | ey = line * 6.0 / (nlines-1) |
406 | eyf = math.floor(ey) |
407 | eyc = math.ceil(ey) |
408 | exf = endpoints[int(eyf)] |
409 | exc = endpoints[int(eyc)] |
410 | if eyf == eyc: |
411 | end = exf |
412 | else: |
413 | end = exf * (eyc-ey) + exc * (ey-eyf) |
414 | end = round(end * size) |
415 | |
416 | liney = height - (lineht+linespc) * (line+1) |
417 | for x in range(int(start), int(end)): |
418 | for y in range(int(lineht)): |
419 | pixel(x, y+liney, cK, canvas) |
420 | |
421 | # And draw a border. |
422 | border(canvas, size, \ |
423 | [(0,0,TL),(int(width-1),0,TR),(0,int(height-1),BL), \ |
424 | (int(width-1),int(height-1),BR)]) |
425 | |
426 | return canvas |
427 | |
428 | def hat(size): |
429 | canvas = {} |
430 | |
431 | # The secret-agent hat in the Pageant icon. |
432 | |
433 | topa = [6]*9+[5,3,1,0,0,1,2,2,1,1,1,9,9,10,10,11,11,12,12] |
434 | topa = [round(x*size) for x in topa] |
435 | botl = round(topa[0]+2.4*math.sqrt(size)) |
436 | botr = round(topa[-1]+2.4*math.sqrt(size)) |
437 | width = round(len(topa)*size) |
438 | |
439 | # Line equations for the top and bottom of the hat brim, in the |
440 | # form y=mx+c. c, of course, needs scaling by size, but m is |
441 | # independent of size. |
442 | brimm = 1.0 / 3.75 |
443 | brimtopc = round(4*size/3) |
444 | brimbotc = round(10*size/3) |
445 | |
446 | for x in range(int(width)): |
447 | xs = float(x) * (len(topa)-1) / (width-1) |
448 | xf = math.floor(xs) |
449 | xc = math.ceil(xs) |
450 | topf = topa[int(xf)] |
451 | topc = topa[int(xc)] |
452 | if xf == xc: |
453 | top = topf |
454 | else: |
455 | top = topf * (xc-xs) + topc * (xs-xf) |
456 | top = math.floor(top) |
457 | bot = round(botl + (botr-botl) * x/(width-1)) |
458 | |
459 | for y in range(int(top), int(bot)): |
460 | pixel(x, y, cK, canvas) |
461 | |
462 | # Now draw the brim. |
463 | for x in range(int(width)): |
464 | brimtop = brimtopc + brimm * x |
465 | brimbot = brimbotc + brimm * x |
466 | for y in range(int(math.floor(brimtop)), int(math.ceil(brimbot))): |
467 | tophere = max(min(brimtop - y, 1), 0) |
468 | bothere = max(min(brimbot - y, 1), 0) |
469 | grey = bothere - tophere |
470 | # Only draw brim pixels over pixels which are (a) part |
471 | # of the main hat, and (b) not right on its edge. |
472 | if canvas.has_key((x,y)) and \ |
473 | canvas.has_key((x,y-1)) and \ |
474 | canvas.has_key((x,y+1)) and \ |
475 | canvas.has_key((x-1,y)) and \ |
476 | canvas.has_key((x+1,y)): |
477 | pixel(x, y, greypix(grey), canvas) |
478 | |
479 | return canvas |
480 | |
481 | def key(size): |
482 | canvas = {} |
483 | |
484 | # The key in the PuTTYgen icon. |
485 | |
486 | keyheadw = round(9.5*size) |
487 | keyheadh = round(12*size) |
488 | keyholed = round(4*size) |
489 | keyholeoff = round(2*size) |
490 | # Ensure keyheadh and keyshafth have the same parity. |
491 | keyshafth = round((2*size - (int(keyheadh)&1)) / 2) * 2 + (int(keyheadh)&1) |
492 | keyshaftw = round(18.5*size) |
493 | keyhead = [round(x*size) for x in [12,11,8,10,9,8,11,12]] |
494 | |
495 | squarepix = [] |
496 | |
497 | # Ellipse for the key head, minus an off-centre circular hole. |
498 | for y in range(int(keyheadh)): |
499 | dy = (y-(keyheadh-1)/2.0) / (keyheadh/2.0) |
500 | dyh = (y-(keyheadh-1)/2.0) / (keyholed/2.0) |
501 | for x in range(int(keyheadw)): |
502 | dx = (x-(keyheadw-1)/2.0) / (keyheadw/2.0) |
503 | dxh = (x-(keyheadw-1)/2.0-keyholeoff) / (keyholed/2.0) |
504 | if dy*dy+dx*dx <= 1 and dyh*dyh+dxh*dxh > 1: |
505 | pixel(x + keyshaftw, y, cy, canvas) |
506 | |
507 | # Rectangle for the key shaft, extended at the bottom for the |
508 | # key head detail. |
509 | for x in range(int(keyshaftw)): |
510 | top = round((keyheadh - keyshafth) / 2) |
511 | bot = round((keyheadh + keyshafth) / 2) |
512 | xs = float(x) * (len(keyhead)-1) / round((len(keyhead)-1)*size) |
513 | xf = math.floor(xs) |
514 | xc = math.ceil(xs) |
515 | in_head = 0 |
516 | if xc < len(keyhead): |
517 | in_head = 1 |
518 | yf = keyhead[int(xf)] |
519 | yc = keyhead[int(xc)] |
520 | if xf == xc: |
521 | bot = yf |
522 | else: |
523 | bot = yf * (xc-xs) + yc * (xs-xf) |
524 | for y in range(int(top),int(bot)): |
525 | pixel(x, y, cy, canvas) |
526 | if in_head: |
527 | last = (x, y) |
528 | if x == 0: |
529 | squarepix.append((x, int(top), TL)) |
530 | if x == 0: |
531 | squarepix.append(last + (BL,)) |
532 | if last != None and not in_head: |
533 | squarepix.append(last + (BR,)) |
534 | last = None |
535 | |
536 | # And draw a border. |
537 | border(canvas, size, squarepix) |
538 | |
539 | return canvas |
540 | |
541 | def linedist(x1,y1, x2,y2, x,y): |
542 | # Compute the distance from the point x,y to the line segment |
543 | # joining x1,y1 to x2,y2. Returns the distance vector, measured |
544 | # with x,y at the origin. |
545 | |
546 | vectors = [] |
547 | |
548 | # Special case: if x1,y1 and x2,y2 are the same point, we |
549 | # don't attempt to extrapolate it into a line at all. |
550 | if x1 != x2 or y1 != y2: |
551 | # First, find the nearest point to x,y on the infinite |
552 | # projection of the line segment. So we construct a vector |
553 | # n perpendicular to that segment... |
554 | nx = y2-y1 |
555 | ny = x1-x2 |
556 | # ... compute the dot product of (x1,y1)-(x,y) with that |
557 | # vector... |
558 | nd = (x1-x)*nx + (y1-y)*ny |
559 | # ... multiply by the vector we first thought of... |
560 | ndx = nd * nx |
561 | ndy = nd * ny |
562 | # ... and divide twice by the length of n. |
563 | ndx = ndx / (nx*nx+ny*ny) |
564 | ndy = ndy / (nx*nx+ny*ny) |
565 | # That gives us a displacement vector from x,y to the |
566 | # nearest point. See if it's within the range of the line |
567 | # segment. |
568 | cx = x + ndx |
569 | cy = y + ndy |
570 | if cx >= min(x1,x2) and cx <= max(x1,x2) and \ |
571 | cy >= min(y1,y2) and cy <= max(y1,y2): |
572 | vectors.append((ndx,ndy)) |
573 | |
574 | # Now we have up to three candidate result vectors: (ndx,ndy) |
575 | # as computed just above, and the two vectors to the ends of |
576 | # the line segment, (x1-x,y1-y) and (x2-x,y2-y). Pick the |
577 | # shortest. |
578 | vectors = vectors + [(x1-x,y1-y), (x2-x,y2-y)] |
579 | bestlen, best = None, None |
580 | for v in vectors: |
581 | vlen = v[0]*v[0]+v[1]*v[1] |
582 | if bestlen == None or bestlen > vlen: |
583 | bestlen = vlen |
584 | best = v |
585 | return best |
586 | |
587 | def spanner(size): |
588 | canvas = {} |
589 | |
590 | # The spanner in the config box icon. |
591 | |
592 | headcentre = 0.5 + round(4*size) |
593 | headradius = headcentre + 0.1 |
594 | headhighlight = round(1.5*size) |
595 | holecentre = 0.5 + round(3*size) |
596 | holeradius = round(2*size) |
597 | holehighlight = round(1.5*size) |
598 | shaftend = 0.5 + round(25*size) |
599 | shaftwidth = round(2*size) |
600 | shafthighlight = round(1.5*size) |
601 | cmax = shaftend + shaftwidth |
602 | |
603 | # Define three line segments, such that the shortest distance |
604 | # vectors from any point to each of these segments determines |
605 | # everything we need to know about where it is on the spanner |
606 | # shape. |
607 | segments = [ |
608 | ((0,0), (holecentre, holecentre)), |
609 | ((headcentre, headcentre), (headcentre, headcentre)), |
610 | ((headcentre+headradius/math.sqrt(2), headcentre+headradius/math.sqrt(2)), |
611 | (cmax, cmax)) |
612 | ] |
613 | |
614 | for y in range(int(cmax)): |
615 | for x in range(int(cmax)): |
616 | vectors = [linedist(a,b,c,d,x,y) for ((a,b),(c,d)) in segments] |
617 | dists = [memoisedsqrt(vx*vx+vy*vy) for (vx,vy) in vectors] |
618 | |
619 | # If the distance to the hole line is less than |
620 | # holeradius, we're not part of the spanner. |
621 | if dists[0] < holeradius: |
622 | continue |
623 | # If the distance to the head `line' is less than |
624 | # headradius, we are part of the spanner; likewise if |
625 | # the distance to the shaft line is less than |
626 | # shaftwidth _and_ the resulting shaft point isn't |
627 | # beyond the shaft end. |
628 | if dists[1] > headradius and \ |
629 | (dists[2] > shaftwidth or x+vectors[2][0] >= shaftend): |
630 | continue |
631 | |
632 | # We're part of the spanner. Now compute the highlight |
633 | # on this pixel. We do this by computing a `slope |
634 | # vector', which points from this pixel in the |
635 | # direction of its nearest edge. We store an array of |
636 | # slope vectors, in polar coordinates. |
637 | angles = [math.atan2(vy,vx) for (vx,vy) in vectors] |
638 | slopes = [] |
639 | if dists[0] < holeradius + holehighlight: |
640 | slopes.append(((dists[0]-holeradius)/holehighlight,angles[0])) |
641 | if dists[1]/headradius < dists[2]/shaftwidth: |
642 | if dists[1] > headradius - headhighlight and dists[1] < headradius: |
643 | slopes.append(((headradius-dists[1])/headhighlight,math.pi+angles[1])) |
644 | else: |
645 | if dists[2] > shaftwidth - shafthighlight and dists[2] < shaftwidth: |
646 | slopes.append(((shaftwidth-dists[2])/shafthighlight,math.pi+angles[2])) |
647 | # Now we find the smallest distance in that array, if |
648 | # any, and that gives us a notional position on a |
649 | # sphere which we can use to compute the final |
650 | # highlight level. |
651 | bestdist = None |
652 | bestangle = 0 |
653 | for dist, angle in slopes: |
654 | if bestdist == None or bestdist > dist: |
655 | bestdist = dist |
656 | bestangle = angle |
657 | if bestdist == None: |
658 | bestdist = 1.0 |
659 | sx = (1.0-bestdist) * math.cos(bestangle) |
660 | sy = (1.0-bestdist) * math.sin(bestangle) |
661 | sz = math.sqrt(1.0 - sx*sx - sy*sy) |
662 | shade = sx-sy+sz / math.sqrt(3) # can range from -1 to +1 |
663 | shade = 1.0 - (1-shade)/3 |
664 | |
665 | pixel(x, y, yellowpix(shade), canvas) |
666 | |
667 | # And draw a border. |
668 | border(canvas, size, []) |
669 | |
670 | return canvas |
671 | |
672 | # Functions to draw entire icons by composing the above components. |
673 | |
674 | def xybolt(c1, c2, size, boltoffx=0, boltoffy=0): |
675 | # Two unspecified objects and a lightning bolt. |
676 | |
677 | canvas = {} |
678 | w = h = round(32 * size) |
679 | |
680 | bolt = lightning(size) |
681 | |
682 | # Position c2 against the top right of the icon. |
683 | bb = bbox(c2) |
684 | assert bb[2]-bb[0] <= w and bb[3]-bb[1] <= h |
685 | overlay(c2, w-bb[2], 0-bb[1], canvas) |
686 | # Position c1 against the bottom left of the icon. |
687 | bb = bbox(c1) |
688 | assert bb[2]-bb[0] <= w and bb[3]-bb[1] <= h |
689 | overlay(c1, 0-bb[0], h-bb[3], canvas) |
690 | # Place the lightning bolt artistically off-centre. (The |
691 | # rationale for this positioning is that it's centred on the |
692 | # midpoint between the centres of the two monitors in the PuTTY |
693 | # icon proper, but it's not really feasible to _base_ the |
694 | # calculation here on that.) |
695 | bb = bbox(bolt) |
696 | assert bb[2]-bb[0] <= w and bb[3]-bb[1] <= h |
697 | overlay(bolt, (w-bb[0]-bb[2])/2 - round((1-boltoffx)*size), \ |
698 | (h-bb[1]-bb[3])/2 - round((2-boltoffy)*size), canvas) |
699 | |
700 | return canvas |
701 | |
702 | def putty_icon(size): |
703 | return xybolt(computer(size), computer(size), size) |
704 | |
705 | def puttycfg_icon(size): |
706 | w = h = round(32 * size) |
707 | s = spanner(size) |
708 | canvas = putty_icon(size) |
709 | # Centre the spanner. |
710 | bb = bbox(s) |
711 | overlay(s, (w-bb[0]-bb[2])/2, (h-bb[1]-bb[3])/2, canvas) |
712 | return canvas |
713 | |
714 | def puttygen_icon(size): |
715 | return xybolt(computer(size), key(size), size, boltoffx=2) |
716 | |
717 | def pscp_icon(size): |
718 | return xybolt(document(size), computer(size), size, boltoffx=1) |
719 | |
720 | def pterm_icon(size): |
721 | # Just a really big computer. |
722 | |
723 | canvas = {} |
724 | w = h = round(32 * size) |
725 | |
726 | c = computer(size * 1.4) |
727 | |
728 | # Centre c in the return canvas. |
729 | bb = bbox(c) |
730 | assert bb[2]-bb[0] <= w and bb[3]-bb[1] <= h |
731 | overlay(c, (w-bb[0]-bb[2])/2, (h-bb[1]-bb[3])/2, canvas) |
732 | |
733 | return canvas |
734 | |
735 | def ptermcfg_icon(size): |
736 | w = h = round(32 * size) |
737 | s = spanner(size) |
738 | canvas = pterm_icon(size) |
739 | # Centre the spanner. |
740 | bb = bbox(s) |
741 | overlay(s, (w-bb[0]-bb[2])/2, (h-bb[1]-bb[3])/2, canvas) |
742 | return canvas |
743 | |
744 | def pageant_icon(size): |
745 | # A biggish computer, in a hat. |
746 | |
747 | canvas = {} |
748 | w = h = round(32 * size) |
749 | |
750 | c = computer(size * 1.3) |
751 | ht = hat(size) |
752 | |
753 | cbb = bbox(c) |
754 | hbb = bbox(ht) |
755 | |
756 | # Determine the relative y-coordinates of the computer and hat. |
757 | # We just centre the one on the other. |
758 | xrel = (cbb[0]+cbb[2]-hbb[0]-hbb[2])/2 |
759 | |
760 | # Determine the relative y-coordinates of the computer and hat. |
761 | # We do this by sitting the hat as low down on the computer as |
762 | # possible without any computer showing over the top. To do |
763 | # this we first have to find the minimum x coordinate at each |
764 | # y-coordinate of both components. |
765 | cty = topy(c) |
766 | hty = topy(ht) |
767 | yrelmin = None |
768 | for cx in cty.keys(): |
769 | hx = cx - xrel |
770 | assert hty.has_key(hx) |
771 | yrel = cty[cx] - hty[hx] |
772 | if yrelmin == None: |
773 | yrelmin = yrel |
774 | else: |
775 | yrelmin = min(yrelmin, yrel) |
776 | |
777 | # Overlay the hat on the computer. |
778 | overlay(ht, xrel, yrelmin, c) |
779 | |
780 | # And centre the result in the main icon canvas. |
781 | bb = bbox(c) |
782 | assert bb[2]-bb[0] <= w and bb[3]-bb[1] <= h |
783 | overlay(c, (w-bb[0]-bb[2])/2, (h-bb[1]-bb[3])/2, canvas) |
784 | |
785 | return canvas |
786 | |
787 | # Test and output functions. |
788 | |
789 | import os |
790 | import sys |
791 | |
792 | def testrun(func, fname): |
793 | canvases = [] |
794 | for size in [0.5, 0.6, 1.0, 1.2, 1.5, 4.0]: |
795 | canvases.append(func(size)) |
796 | wid = 0 |
797 | ht = 0 |
798 | for canvas in canvases: |
799 | minx, miny, maxx, maxy = bbox(canvas) |
800 | wid = max(wid, maxx-minx+4) |
801 | ht = ht + maxy-miny+4 |
802 | block = [] |
803 | for canvas in canvases: |
804 | minx, miny, maxx, maxy = bbox(canvas) |
805 | block.extend(render(canvas, minx-2, miny-2, minx-2+wid, maxy+2)) |
806 | p = os.popen("convert -depth 8 -size %dx%d rgb:- %s" % (wid,ht,fname), "w") |
807 | assert len(block) == ht |
808 | for line in block: |
809 | assert len(line) == wid |
810 | for r, g, b, a in line: |
811 | # Composite on to orange. |
812 | r = int(round((r * a + 255 * (255-a)) / 255.0)) |
813 | g = int(round((g * a + 128 * (255-a)) / 255.0)) |
814 | b = int(round((b * a + 0 * (255-a)) / 255.0)) |
815 | p.write("%c%c%c" % (r,g,b)) |
816 | p.close() |
817 | |
818 | def drawicon(func, width, fname, orangebackground = 0): |
819 | canvas = func(width / 32.0) |
820 | finalise(canvas) |
821 | minx, miny, maxx, maxy = bbox(canvas) |
822 | assert minx >= 0 and miny >= 0 and maxx <= width and maxy <= width |
823 | |
824 | block = render(canvas, 0, 0, width, width) |
825 | p = os.popen("convert -depth 8 -size %dx%d rgba:- %s" % (width,width,fname), "w") |
826 | assert len(block) == width |
827 | for line in block: |
828 | assert len(line) == width |
829 | for r, g, b, a in line: |
830 | if orangebackground: |
831 | # Composite on to orange. |
832 | r = int(round((r * a + 255 * (255-a)) / 255.0)) |
833 | g = int(round((g * a + 128 * (255-a)) / 255.0)) |
834 | b = int(round((b * a + 0 * (255-a)) / 255.0)) |
835 | a = 255 |
836 | p.write("%c%c%c%c" % (r,g,b,a)) |
837 | p.close() |
838 | |
839 | args = sys.argv[1:] |
840 | |
841 | orangebackground = test = 0 |
842 | colours = 1 # 0=mono, 1=16col, 2=truecol |
843 | doingargs = 1 |
844 | |
845 | realargs = [] |
846 | for arg in args: |
847 | if doingargs and arg[0] == "-": |
848 | if arg == "-t": |
849 | test = 1 |
850 | elif arg == "-it": |
851 | orangebackground = 1 |
852 | elif arg == "-2": |
853 | colours = 0 |
854 | elif arg == "-T": |
855 | colours = 2 |
856 | elif arg == "--": |
857 | doingargs = 0 |
858 | else: |
859 | sys.stderr.write("unrecognised option '%s'\n" % arg) |
860 | sys.exit(1) |
861 | else: |
862 | realargs.append(arg) |
863 | |
864 | if colours == 0: |
865 | # Monochrome. |
866 | cK=cr=cg=cb=cm=cc=cP=cw=cR=cG=cB=cM=cC=cD = 0 |
867 | cY=cy=cW = 1 |
868 | cT = -1 |
869 | def greypix(value): |
870 | return [cK,cW][int(round(value))] |
871 | def yellowpix(value): |
872 | return [cK,cW][int(round(value))] |
873 | def bluepix(value): |
874 | return cK |
875 | def dark(value): |
876 | return [cT,cK][int(round(value))] |
877 | def blend(col1, col2): |
878 | if col1 == cT: |
879 | return col2 |
880 | else: |
881 | return col1 |
882 | pixvals = [ |
883 | (0x00, 0x00, 0x00, 0xFF), # cK |
884 | (0xFF, 0xFF, 0xFF, 0xFF), # cW |
885 | (0x00, 0x00, 0x00, 0x00), # cT |
886 | ] |
887 | def outpix(colour): |
888 | return pixvals[colour] |
889 | def finalisepix(colour): |
890 | return colour |
891 | elif colours == 1: |
892 | # Windows 16-colour palette. |
893 | cK,cr,cg,cy,cb,cm,cc,cP,cw,cR,cG,cY,cB,cM,cC,cW = range(16) |
894 | cT = -1 |
895 | cD = -2 # special translucent half-darkening value used internally |
896 | def greypix(value): |
897 | return [cK,cw,cw,cP,cW][int(round(4*value))] |
898 | def yellowpix(value): |
899 | return [cK,cy,cY][int(round(2*value))] |
900 | def bluepix(value): |
901 | return [cK,cb,cB][int(round(2*value))] |
902 | def dark(value): |
903 | return [cT,cD,cK][int(round(2*value))] |
904 | def blend(col1, col2): |
905 | if col1 == cT: |
906 | return col2 |
907 | elif col1 == cD: |
908 | return [cK,cK,cK,cK,cK,cK,cK,cw,cK,cr,cg,cy,cb,cm,cc,cw,cD,cD][col2] |
909 | else: |
910 | return col1 |
911 | pixvals = [ |
912 | (0x00, 0x00, 0x00, 0xFF), # cK |
913 | (0x80, 0x00, 0x00, 0xFF), # cr |
914 | (0x00, 0x80, 0x00, 0xFF), # cg |
915 | (0x80, 0x80, 0x00, 0xFF), # cy |
916 | (0x00, 0x00, 0x80, 0xFF), # cb |
917 | (0x80, 0x00, 0x80, 0xFF), # cm |
918 | (0x00, 0x80, 0x80, 0xFF), # cc |
919 | (0xC0, 0xC0, 0xC0, 0xFF), # cP |
920 | (0x80, 0x80, 0x80, 0xFF), # cw |
921 | (0xFF, 0x00, 0x00, 0xFF), # cR |
922 | (0x00, 0xFF, 0x00, 0xFF), # cG |
923 | (0xFF, 0xFF, 0x00, 0xFF), # cY |
924 | (0x00, 0x00, 0xFF, 0xFF), # cB |
925 | (0xFF, 0x00, 0xFF, 0xFF), # cM |
926 | (0x00, 0xFF, 0xFF, 0xFF), # cC |
927 | (0xFF, 0xFF, 0xFF, 0xFF), # cW |
928 | (0x00, 0x00, 0x00, 0x80), # cD |
929 | (0x00, 0x00, 0x00, 0x00), # cT |
930 | ] |
931 | def outpix(colour): |
932 | return pixvals[colour] |
933 | def finalisepix(colour): |
934 | # cD is used internally, but can't be output. Convert to cK. |
935 | if colour == cD: |
936 | return cK |
937 | return colour |
938 | else: |
939 | # True colour. |
940 | cK = (0x00, 0x00, 0x00, 0xFF) |
941 | cr = (0x80, 0x00, 0x00, 0xFF) |
942 | cg = (0x00, 0x80, 0x00, 0xFF) |
943 | cy = (0x80, 0x80, 0x00, 0xFF) |
944 | cb = (0x00, 0x00, 0x80, 0xFF) |
945 | cm = (0x80, 0x00, 0x80, 0xFF) |
946 | cc = (0x00, 0x80, 0x80, 0xFF) |
947 | cP = (0xC0, 0xC0, 0xC0, 0xFF) |
948 | cw = (0x80, 0x80, 0x80, 0xFF) |
949 | cR = (0xFF, 0x00, 0x00, 0xFF) |
950 | cG = (0x00, 0xFF, 0x00, 0xFF) |
951 | cY = (0xFF, 0xFF, 0x00, 0xFF) |
952 | cB = (0x00, 0x00, 0xFF, 0xFF) |
953 | cM = (0xFF, 0x00, 0xFF, 0xFF) |
954 | cC = (0x00, 0xFF, 0xFF, 0xFF) |
955 | cW = (0xFF, 0xFF, 0xFF, 0xFF) |
956 | cD = (0x00, 0x00, 0x00, 0x80) |
957 | cT = (0x00, 0x00, 0x00, 0x00) |
958 | def greypix(value): |
959 | value = max(min(value, 1), 0) |
960 | return (int(round(0xFF*value)),) * 3 + (0xFF,) |
961 | def yellowpix(value): |
962 | value = max(min(value, 1), 0) |
963 | return (int(round(0xFF*value)),) * 2 + (0, 0xFF) |
964 | def bluepix(value): |
965 | value = max(min(value, 1), 0) |
966 | return (0, 0, int(round(0xFF*value)), 0xFF) |
967 | def dark(value): |
968 | value = max(min(value, 1), 0) |
969 | return (0, 0, 0, int(round(0xFF*value))) |
970 | def blend(col1, col2): |
971 | r1,g1,b1,a1 = col1 |
972 | r2,g2,b2,a2 = col2 |
973 | r = int(round((r1*a1 + r2*(0xFF-a1)) / 255.0)) |
974 | g = int(round((g1*a1 + g2*(0xFF-a1)) / 255.0)) |
975 | b = int(round((b1*a1 + b2*(0xFF-a1)) / 255.0)) |
976 | a = int(round((255*a1 + a2*(0xFF-a1)) / 255.0)) |
977 | return r, g, b, a |
978 | def outpix(colour): |
979 | return colour |
980 | if colours == 2: |
981 | # True colour with no alpha blending: we still have to |
982 | # finalise half-dark pixels to black. |
983 | def finalisepix(colour): |
984 | if colour[3] > 0: |
985 | return colour[:3] + (0xFF,) |
986 | return colour |
987 | else: |
988 | def finalisepix(colour): |
989 | return colour |
990 | |
991 | if test: |
992 | testrun(eval(realargs[0]), realargs[1]) |
993 | else: |
994 | drawicon(eval(realargs[0]), int(realargs[1]), realargs[2], orangebackground) |