Showing posts with label ergonomics. Show all posts
Showing posts with label ergonomics. Show all posts

Friday, March 20, 2020

Improving a Laptop Keyboard with Custom Keycap Veneers

I admit that I'm picky about the keyboards I use.  I'm accustomed to the general ergonomics of classic keyboards and have become spoiled by a Model M for the last 20 years.  While I can hardly expect to get a comparable experience from a laptop keyboard, I do feel that some desirable aspects of design are painstakingly avoided for the sake of petty fashion.  The keyboard on my laptop has been one of the most frustrating I have ever attempted to use.  At some point, I decided that there must be something I could do to turn it into a more comfortable and efficient keyboard.

Since full-size keyboards are where I find the most comfort, perhaps it's worth describing what aspects of their design I feel are important to address in my efforts.  While that discussion is likely going to suggest a mechanical keyboard as a central ideal, I don't think that any particular mechanism is as important as what properties it can impart to the product.  Bear in mind that much of this is merely my preference.


An old XT keyboard, showing cylindrical crowns and overall contour.
The most commonly noted aspects in which laptop and typical desktop keyboards differ have to do with the key stroke.  The mechanical characteristics of the key stroke are primarily a comfort concern, though they may also provide forms of feedback which promote speed -- or at least confidence in motion at speed. Hysteretic mechanisms can provide tactile feedback, higher actuation force makes the motions assertive, and long stroke length allows for natural follow-through.  While users of discrete mechanical keyboards may have a broad range of stroke characteristics to explore, it's reasonable to expect that any practical laptop keyboard is going to necessarily have a restricted range of possible characteristics.

While an unfamiliar laptop key layout may certainly conflict with motor memory, even a familiar layout becomes difficult to use without some form of orienting information.  It is the physical geometry of the keyboard which provides spatial cues to reinforce and maintain the accuracy of learned motions when moving from key to key.  Consider a similar sensorimotor task; it's easy to walk through a familiar room in darkness if you can occasionally touch known surfaces along the way.  Doing the same without any references is an exercise in error accumulation.  In terms of a continuous process, these forms of feedback facilitate the error determination necessary for error correction to be possible; they allow the control loop to be closed. 

The most obvious and universal orienting features are identifier bumps.  They are usually only located on the F and J keys, as well as the numpad 5 key (on keyboards with a numpad).  Bumps are one of the few physical features of keycaps that people can easily customize.  While self-adhesive bumps can be bought for this purpose, other methods such as glue are common.  Custom bumps are especially useful in emphasizing keys associated with certain keyboard commands.

While the typical keyboard bumps are a mechanism to orient hand position within a general area of the keyboard, it is the feedback provided by the shape of the keycaps and the overall contour of the keyboard itself which reinforces the discreteness of all keys.  The center of each key can be emphasized by making the crown of the keycap slightly concave, or by making the crown smaller than the pitch distance between key centers.  Providing strong centering cues helps maintain spatial awareness and helps to reduce the tendency for edge strikes and two-key strikes.  Many keyboards have the rows laid out in either a curved or linear stairstep fashion, a feature which both adds to the distinctness of the rows and aids in comfortably reaching the upper rows.

You could at least pretend that ergonomics matter.
The keyboard on this old laptop (Acer 7736z) had none of that going for it. Being a laptop, the keyboard is flat (planar); that much is a necessity, though it isn't exactly helping.  The level of the keyboard is actually slightly lower than the body of the laptop, which makes the hand positions uncomfortably low, turning the overly-sensitve trackpad into an even worse nuisance.  The keystroke is light, short, and ambiguous.  Every keystroke is like the experience of stomping at the top of a long flight of stairs when you thought there was still one more step to go. The keycaps are perfectly flat, with no crown and no functional centering cues.  The key identifiers (bumps) are tiny and barely noticeable with dry hands.  Just trying to find home row after using the mouse is a tedious routine of sliding fingers around in a broad expanse of flat slippery plastic, all the while trying to maintain the lightest of touch to avoid errant keystrokes.  Once my hands move away from a known position, the lack of any discernable spatial references is immediately disorienting.  Combine that with the lack of feedback, and the whole experience becomes both tedious and precarious, like trying to touch-type with chopsticks.  Keyboard commands and programming both involve broader reaching motions than writing in simple prose, and are the most tiring of all.  The only way a keyboard could be ergonomically worse is if it were entirely featureless and truly devoid of feedback, like a touchscreen OSK or projection keyboard. 

In all the time I've spent using this laptop, nearly every minute has been steeped in the thought that there must be a way to improve its keyboard.  As per the mentioned limits of practicality, there is little I can do to alter the keystroke or overall contour.  While I can't alter the layout, perhaps I can add some extra identifiers.  Adding various forms of spatial cues to the keycaps should be possible, though there are some limitations.  Of course, there's the limitation of the distance between the keyboard and screen when the laptop is closed.  It might also be desirable to make sure any alterations are removable; after all, this is likely to require some experimentation. 

The First Attempt at a Solution

My first thought was to use adhesive to add identifier bumps to certain keys.  After some thought, I came to the prior conclusions regarding more general spatial awareness reinforcement, ultimately deciding that a centering mark of some sort should be added to the most-used keys.  My thought was to simulate the effect of a concave keycap crown by adding a circular ridge on each key.  I could add ancillary bumps or ridges either for location, identification, or for avoidance. 

While a finalized design could be implemented using epoxy, I opted to prototype my ideas using a conformal coating made from clear Dap Sidewinder thinned with xylene.  This can be thinned to an appropriate consistency and provides a smooth, self-leveling finish.  It can also be completely removed once dry by simply redissolving in xylene; though in this case, sound material could be picked/peeled off cleanly without solvent.  It may also be possible to use something like E6000 if thinned appropriately. 

I applied the coating with a 1mL syringe and a 25ga dispensing needle bent to a comfortable angle.  There are a couple of important things that need to be considered in that effort.  Dispensing a viscous material through a fine needle requires a lot of pressure.  The pressure requirements can be lowered by reducing the viscosity or by using a shorter needle or one of larger diameter.  I chose a 1mL syringe because it allowed me to generate relatively high pressures, but care must be taken to avoid making a mess on the keyboard.  With firm hand strength, a 1mL syringe can produce upwards of 200 PSI -- more than enough to eject the needle and blurt a wad of glue across your work.  If you can use a syringe with a luer-lok type spigot, do so.  If all you can get are syringes with plain tapered spigots, pay attention to keep the taper clean and tight.  Practice first.

One also should consider the materials used.  Many adhesives and coatings will degrade when in constant contact with skin oils.  While I chose this coating for prototyping, it is wholly unsuited for long-term use.  After a week or so, it will begin to become sticky and will eventually get smeared everywhere.  Other products like E6000 will likely do the same on a longer timeframe, and I would expect the same of almost every common adhesive other than epoxy.

While I found it helpful to have more noticeable identifier bumps, the circular ridges left me relatively disappointed.  The rings were certainly better than nothing, but they were still a poor simulant of a key crown.  The fact that the keycap is the same height inside and outside the ring makes them fairly ineffective at suggesting a boundary.  With this effect in mind and considering the low height of the glue ridge, the ring diameter needed to be fairly small to present an unambiguous shape to the fingertip.  The smaller the rings are, the more awkward they feel, and the more often they're escaped. 

Placing my hands in the rings on home row felt awkward -- as if they're too straight.  Certainly, the wear marks on my main keyboard suggest that my fingertips naturally rest in an arc across the keys.  It followed that the landing points were generally centered laterally, but otherwise varied depending on what was most comfortable for a given finger.  I switched the circular glue rings out for elongated rectangular glue rings and noted a marginal improvement.  I felt that it was ultimately impractical to achieve much more with such a low-profile method. 

The Development of Keycap Veneers

Finding myself idle on my laptop away from home, I dared to spend some time on what I'd presumed would surely be a huge waste of time.  I figured I'd whip up a parametric model for 3D-printed keycaps.  The idea was to print some caps (if they would print flat and thin), clean them up (easier said than done), and glue them like veneers on top of the existing keys.  The modelling was done in OpenSCAD, a simple and enjoyable script-based parametric 3D CAD tool.  I conjured up several different variations on crown geometry, printing and testing along the way. 

// //////////////////////////////////////////////////////
// keycap veneers for shitty flat laptop keyboards

// cylinder cuts only strongly enforce x-positioning
// this allows fingertips to rest in a more natural arc across home row
// and may be more comfortable on keys which require more reaching or are otherwise habitually struck off-center
// they're also easier to make smooth (easier to print without fuzz, also easier to scrape/sand by hand)
// spherical cuts reinforce y-pos more than cylinder cuts do (important on flat kb)
// elliptical cuts are a compromise especially suited to flat profile kb

// proportions aren't fixed. some things require manual adjusting

// //////////////////////////////////////////////////////
// RENDERING & MULTIPART LAYOUT
nf=100;   // facet number (~50 for speed; ~200 for printing)
Nx=4;    // number of caps to tile along X-axis
Ny=2;    // number of caps to tile along Y-axis
tilegap=2;  // gap between tiles

// //////////////////////////////////////////////////////
// BASIC KEYCAP GEOMETRY
h=17;      // keycap height (in key plane)
w=17;    // keycap width (in key plane)
th=1.7;    // maximum cap thickness (limited by kb-screen gap)
th_min=0.3;  // minimum cap thickness (limited by print strength)
draftx=45;  // draft angle (vertical taper on R,L faces)
drafty=45;  // draft angle (vertical taper on T,B faces)
cr=3;     // corner radius 

// //////////////////////////////////////////////////////
// PRIMARY RELIEF CUTS
// sphere & cylinder mimic legacy alpha key designs 
// lcylinder is the transverse version of 'cylinder', for wide keys
// wcylinder is a double-cylindrical hull, for wide keys
// sausage is a double-spherical hull, for wide/tall keys
// ysausage is the same as 'sausage', but each sphere location can be independently offset
// bumps is a series of spherical bumps (used as an avoidance indicator) 
// using 'none' will produce a flat keycap
style="sphere";

// these parameters may need tweaked when geometry is changed significantly
drc=20;     // relief cut radius for cylinder styles
drs=20;     // relief cut radius for sphere & sausage styles
// the following options only apply to spherical cuts
osx=0;     // offset x
osy=0;     // offset y
osz=0;     // offset z
scaley=1.3;   // stretch factor (elliptical cut)
// the following options only apply to ysausage cuts
os1=-.5;     // y-offset for top sphere
os2=-3;     // y-offset for bottom sphere
// the following options only apply to 'bumps'
brad=1;     // bump radius
bdepth=0.7;   // bump depth
blayout=[3,4];  // number of bumps [x,y]

// //////////////////////////////////////////////////////
// ADDITIONAL FEATURES
// bevel produces a single beveled edge (e.g. for bottom-row keys)
// multiple bevels can be specified (e.g. ["top","bottom"])
bevel="none";   // bottom top left right or none
bevangle=10;  // angle of beveled face
bevhos=0.6;   // height offset of bottom edge of bevel

// cuts can be made asymmetric so that a single cut spans multiple keys
// this allows certain keys to be grouped by touch (e.g. groups of four F-keys)
lowside="none";  // right left both or none

// identifier position may be in the center or bottom edge
identifier="none";  // center edge or none
idw=0.5;   // identifier width
idl=5;     // identifier length
idh=0.95;   // identifier height WRT cap height (used for 'edge')
idhc=0.5;   // height used for "center"


// //////////////////////////////////////////////////////
// //////////////////////////////////////////////////////
// THE MAGIC

module cap(){
 color("dimgray")
 render(){
  linear_extrude(height=th,scale=[1-2*th*tan(drafty)/w,1-2*th*tan(drafty)/h]){
   hull(){
    translate([(w/2-cr),(h/2-cr),0])
     circle(r=cr,center=true,$fn=nf/5);
    translate([-(w/2-cr),-(h/2-cr),0])
     circle(r=cr,center=true,$fn=nf/5);
    translate([(w/2-cr),-(h/2-cr),0])
     circle(r=cr,center=true,$fn=nf/5);
    translate([-(w/2-cr),(h/2-cr),0])
     circle(r=cr,center=true,$fn=nf/5);
   }
  }
    }
}

module relief_bumps(){
 render(){
   difference(){
    translate([-0.6*w,-0.6*h,th-bdepth])
     cube(1.2*[w,h,brad]);
    union(){
     for (m=[1:blayout[1]]){
      for (n=[1:blayout[0]]){
       translate([-(w-w/blayout[0])/2+w/blayout[0]*(n-1),-(h-h/blayout[1])/2+h/blayout[1]*(m-1),th-brad])
        sphere(r=brad,center=true,$fn=nf/5); 
      }
     }
    }
   }
 }
}

module relief_spherical(){
 render(){
  translate([osx,osy,drs+th_min+osz]){
   sphere(r=drs,center=true,$fn=nf);
   if (lowside=="right" || lowside=="both")
    translate([h/2+osx,osy,0])
     rotate([90,0,90])
      cylinder(r=drs,h=h,center=true,$fn=nf);
   if (lowside=="left" || lowside=="both")
    translate([-h/2+osx,osy,0])
     rotate([90,0,90])
      cylinder(r=drs,h=h,center=true,$fn=nf);
  }
 }
}

module relief_cylindrical(){
 render(){
  translate([0,0,drc+th_min]){
   rotate([90,0,0])
    cylinder(r=drc,h=h+1,center=true,$fn=nf);
   if (lowside=="right" || lowside=="both")
    translate([h/2,0,0])
     rotate([90,0,90])
      cube([drc*2,drc*2,h],center=true);
   if (lowside=="left" || lowside=="both")
    translate([-h/2,0,0])
     rotate([90,0,90])
      cube([drc*2,drc*2,h],center=true);
  }
 }
}

module relief_cylindrical_long(){
 render(){
  translate([0,0,drc+th_min]){
   rotate([90,0,90])
    cylinder(r=drc,h=w+1,center=true,$fn=nf);
  }
 }
}

module relief_cylindrical_wide(){
 render(){
  translate([0,0,drc+th_min]){
   rotate([90,0,0])
    hull(){
     translate([(w-h)/2,0,0])
      cylinder(r=drc,h=h+1,center=true,$fn=nf);
     translate([-(w-h)/2,0,0])
      cylinder(r=drc,h=h+1,center=true,$fn=nf);
    }
   if (lowside=="right" || lowside=="both")
    translate([h/2,0,0])
     rotate([90,0,90])
      cube([drc*2,drc*2,h],center=true);
   if (lowside=="left" || lowside=="both")
    translate([-h/2,0,0])
     rotate([90,0,90])
      cube([drc*2,drc*2,h],center=true);
  }
 }
}

module relief_sausage(){
 render(){
  translate([0,0,drs+th_min]){
   hull(){
    translate([(w-h)/2,0,0])
     sphere(r=drs,center=true,$fn=nf);
    translate([-(w-h)/2,0,0])
     sphere(r=drs,center=true,$fn=nf);
   }
   if (lowside=="right" || lowside=="both")
    translate([h/2,0,0])
     rotate([90,0,90])
      cylinder(r=drs,h=h,center=true,$fn=nf);
   if (lowside=="left" || lowside=="both")
    translate([-h/2,0,0])
     rotate([90,0,90])
      cylinder(r=drs,h=h,center=true,$fn=nf);
  }
 }
}

module relief_ysausage(){
 render(){
  translate([0,0,drs+th_min]){
   hull(){
    translate([0,os1,0])
     sphere(r=drs,center=true,$fn=nf);
    translate([0,os2,0])
     sphere(r=drs,center=true,$fn=nf);
   }
   if (lowside=="right" || lowside=="both")
    translate([0,w/2,0])
     rotate([90,0,0])
      cylinder(r=drs,h=h,center=true,$fn=nf);
   if (lowside=="left" || lowside=="both")
    translate([0,-w/2,0])
     rotate([90,0,0])
      cylinder(r=drs,h=h,center=true,$fn=nf);
  }
 }
}

module relief_bevel(){
 union(){
  for (i=bevel){
   if (i=="top")
    translate([0,h/2,th_min+bevhos])
     rotate([bevangle,0,180])
     translate([0,h/4,w/2])
     rotate([90,0,0])
      cube([w,w,h/2],center=true);
   if (i=="bottom")
    translate([0,-h/2,th_min+bevhos])
     rotate([bevangle,0,0])
     translate([0,h/4,w/2])
     rotate([90,0,0])
      cube([w,w,h/2],center=true);
   if (i=="right")
    translate([w/2,0,th_min+bevhos])
     rotate([bevangle,0,90])
     translate([0,w/4,h/2])
     rotate([90,0,0])
      cube([h,h,w/2],center=true);
   if (i=="left")
    translate([-w/2,0,th_min+bevhos])
     rotate([bevangle,0,-90])
     translate([0,w/4,h/2])
     rotate([90,0,0])
      cube([h,h,w/2],center=true);
  }
 }
}

module drawcap(){
 difference(){
  cap();
  
  if (style=="sphere")
   if (scaley!=1)
    scale([1,scaley,1])
     relief_spherical();
   else
    relief_spherical();
  if (style=="cylinder")
   relief_cylindrical();
  if (style=="lcylinder")
   relief_cylindrical_long();
  if (style=="wcylinder")
   relief_cylindrical_wide();
  if (style=="sausage")
   relief_sausage();
  if (style=="ysausage")
   relief_ysausage();
  if (style=="bumps")
   relief_bumps();
  if (bevel!="none")
   relief_bevel();
 }
 
 if (identifier=="edge")
  translate([-idl/2,-idw/2-h*0.34,th_min])
   cube([idl,idw,th*idh-th_min+0.001]);
 else if (identifier=="center")
  translate([-idl/2,-idw/2,th_min])
   cube([idl,idw,th*idhc-th_min+0.001]);
}

module tilecaps(){
 color("dimgray"){
  for (m=[1:Ny]){
   translate([0,(m-1)*(h+tilegap),0]){
    for (n=[1:Nx]){
     translate([(n-1)*(w+tilegap),0,0]){
      drawcap();
     }
    }
   }
  }
 }
}

tilecaps();

Much of the core focus of these experiments stems from the lessons of the glue ring experiment; that is, finding the balance between the axial components of the crown geometry.  The equivalent analog for the circular glue ring is of course a spherically concave crown.  By contrast, many keyboards such as the Model M have cylindrically concave crowns.  While the spherical crowns provide constraint cues both laterally (side to side) and transversely (across the rows), cylindrical crowns only provide strong constraint laterally.  Much like using elongated glue rings, this allows for more variation of finger placement in the transverse direction without affecting comfort.  While the cylindrical shapes are certainly easier to deburr and finish than spherical ones, the overall flatness of the keyboard and the tendency to use the laptop in awkward positions without a desk left me wanting more transverse constraint than they could provide.  After all, while the Model M uses cylindrical crowns, it also has a significant and unambiguous height difference between rows.  The solution was simply a deep ellipsoidal crown contour.  Using atypically deep crowns helps compensate for the flatness inherent to a laptop keyboard, and generally provides more constraint overall.  A few quick tweaks were required to find what I felt was a good balance.  Compared to either spherical or cylindrical reliefs of a similar depth, the final ellipsoidal crown allowed a dramatic improvement in speed, accuracy, and comfort within the alpha keys.

Caps with spherical, cylindrical, and ellipsoidal crowns (and an identifier bump)
Wary of losing track of home row (something that was easy to do on the original keyboard), I decided to use different caps for the numeric row.  In order to provide a sense of overall contour, I opted to reduce the degree to which these cap crowns were contoured themselves.  In other words, any concave relief should be shallower, allowing the cap to be effectively thicker in the strike location.  Again, I used wear marks on a well-worn keyboard to locate the crown features.

The F-keys are grouped into blocks of four, with each group sharing an elongated concave relief.  This addresses the fact that the keys are not spaced or located as they are on a standard keyboard.  Only ESC and DEL have their own spherical relief. 

Keys which I rarely use, or keys which are otherwise hit accidentally need some sort of avoidance identifier.  I opted for a lower-profile cap with a grid of bumps.  While it makes for frustrating print cleanup, it's an effective solution. 

Other keys were given either shallow concave reliefs or simple bevels.  In this way, they are brought up to a comparable height with the other modified keys, even if they otherwise do not require any particular improvement.

Other cap types for function-row and numeric-row keys
The caps were printed in PLA, though this makes them difficult to clean up and finish.  The surfaces require quite a bit of burr removal or "defuzzing".  Most of this can be done by scraping, though the shapes of the parts often makes it awkward and the process is generally tedious.  Sanding is likewise very tedious, especially corners and edges.  Cylindrical-relief keys can be printed in rows and sanded in a comfortable motion, though spherical or ellipsoidal reliefs don't allow this.  Sanding tends to leave more fine fuzz.

Improving the finish beyond that is difficult.  As the parts are very thin, heat polishing (flame or hot air) doesn't really work.  The features are either destroyed, or the part curls and requires restraint -- leading to more marring.  Solvent polishing PLA is not really a thing.  No, acetone, MEK, and ethyl acetate don't work.  They might slightly soften and deglaze the part, but they will not dissolve the surface enough to allow it to reflow.  I do not know of any other solvents that would, but if there were one, I have a feeling that the parts would be so thin that they would tend to curl anyway.

Sanded, fuzzy caps temporarily affixed during fitting
Using a rotary tool to buff or burnish the surface is fairly counterproductive.  The tool needs to run much slower than they can, otherwise the heat generated by friction melts and smears the material due to its very low melting point.  Wool buffs, brushes, cratex points, and scotch brite bobs all ended up just making a mess.  In practice, the first thing they do is erase edges and corners. 

I ended up opting to just coat the caps, though I know that coatings on a keyboard are likely going to eventually fail.  I figured that my best bet was either polyurethane or a good clear acrylic.  I didn't feel like applying the poly with a brush or sprayer, so I just used an aerosol acrylic spray. No, I did not just spray the keys in-situ.  I used double-sided tape to hold them down to a waste board when spraying. 


The keycaps after a sloppy spray job. It feels better than it looks.
The caps were originally prototyped using simple craft rubber cement for adhesion.  This allowed them to easily be removed with a knife, and the residual glue simply rolled off the surfaces cleanly.  For final reassembly, I used a more proper polychloroprene contact cement (e.g. Weldwood, Barge).  Removal may now be more difficult, but possible.  Instead of brushing the cement on, I used a syringe to dispense a small amount on each key.  There are plenty of other adhesives that could be used here.  I figured that replacement keyboards are cheap enough that I don't really need to worry about removing the caps once I've settled on the design. 

Final Thoughts

The keyboard as modified has proven to be about as good as a laptop keyboard can possibly be.  The height is much more comfortable, the alpha keys are distinct from surrounding keys.  The identifier bumps are distinct, and I added a couple extra where I wanted them.  The finish isn't very pretty, but beauty isn't necessary here. 

If you read all that blather without losing interest or the will to live, I should congratulate you.  Even I had a hard time enduring it. 

Monday, July 20, 2015

Toward a useful vision aid

Ever since cataract surgery, I've been struggling with being able to see well.  Part of the issue is a gradual increase in astigmatism since surgery (I just need to get my prescription updated), but the biggest hurdle is the loss of visual accommodation.

With the extraction of the eye's natural lens, goes the ability to adjust focus to accommodate for distance.  While corrective lenses can bring things into focus for any fixed distance (e.g. reading glasses), a fixed correction can't work over a broad range of distances.  As depth of field increases as a function of distance to subject, it stands to reason that most accommodation issues occur when working with nearby objects.  An older person feeling the effects of presbyopia can probably say a thing or two about the gradual frustration of needing bifocals.

This isn't about age-related problems or old people things. This is about my dumb broke ass trying to find a means to get by with my pointless daily existence. Anyone else would probably just buy some magnifier lamps or something, but those cost double-digits kind of money. If you've been paying attention, you'll know that I'll end up cobbling shit out of $0.99 Ebay garbage and then sitting in the dark at midnight writing rambling stories to myself about an experience that even I can barely care about. That's what I did. That's what I'm doing. Let us continue.

Presbyopia is common though.  Where do most people start?  Bifocals and reading glasses simply offer an increased optical power (increased lens convexity) so that focusing on near objects is possible.  While bifocals are convenient in that they're always at hand, their use requires a strong downward gaze which I can't effectively accomplish -- a consequence of an entirely separate ailment.

A drift of reading glasses and very uncomfortable Optivisor clone

Drug store reading glasses aren't meant to be used with prescription glasses, though it's ... possible.  Prescription reading glasses are a simple addition to the SPH portion of the prescription (or specification of a +ADD).  Of course, if you know your prescription and can use cheap readers to get an idea how much correction you need for a particular distance, you can do the math yourself and order any sort of odd double-reader bifocals or "computer glasses" you want.

For instance, let's say I wear plain single-vision glasses and I pick up a pair of +2.5 readers and slap them over the top.  "Hooray!" I say as I can again read my own handwriting at 12".  I can either add 2.5 to the SPH portion of both OD and OS lines on my prescription, or if it's appropriate, I can just use the +ADD entry on the order form.  In this way, you can order your own readers or bifocals online without dealing with extra expense.

Say I have an existing bifocal prescription and I want to make computer glasses that can focus at 30", but without changing the power of the secondary lens I normally use for reading at 15".  If I can determine that a +1.50 correction allows me to see at 30", I can just add that to the SPH section of the prescription and then subtract it from the existing +ADD which specified the original bifocals.   Zenni Optical actually covers these sorts of prescription adjustments in their FAQ here and here

Granted, buying glasses from Zenni beats paying $200 for glasses, but I'm not made of money.  Not only that, but like I mentioned, depth of field is a function of distance.  With glasses configured to focus at 20', you can focus at 200', but with glasses configured to focus at 2', you probably won't be able to see much at 20'.  I went though the motions I describe above when I got my glasses for the computer.  In fact, I collected the optimal focusing distance for a range of positive correction powers, as well as the minimum and maximum distance I could reasonably make out detailed edges in some text samples.  It seems subjective at a glance, but when plotted, the trend starts to reveal a nice rational relationship.


With some use-cases such as the computer where distance is fixed and repeatable, it's simple to come up with a correction that's tailored to the task.  Otherwise, shop work involving operations up close (checking pitch of a bolt with a thread gage) and at a distance (finding the bolt after you drop it) is hard to correct with a single prescription.  At some point one has to accept that there is no single solution, and that carrying numerous solutions around is impractical. 

So what's left?  If I can't accommodate and I can't effectively use bifocals, I'm pretty much doomed to do half of my work in a blurry world unless I'm constantly swapping glasses or fumbling a magnifier.  Some work patterns are harder to deal with than others.  Automotive work requires focus between about 12" and 60", and the area of focus is typically changing regularly.  On the other hand, lathe operations cover a smaller range of distances and most importantly, the critical areas of focus don't change as much.  It's simpler to try coming up with task-oriented magnification tools for cases like this.  ... simpler to try.

So I embark on a journey to come up with some positionable magnetic work magnifiers that I can stick down on the lathe or drill press or vise when I need them.  There are lots of options out there already if you want to spend money.  Certainly, I'd like a lighted magnifier on a Noga mag base, but that's not going to happen.


I found a MagniStitch magnifier that my late grandparents had purchased in the 80's.  The lens is well-shaped and wide.  The ball-jointed arm is much less useful than one might think.  It tends to pop apart when positioning.  I glued it to an old speaker magnet (because double-sided foam tape is bullshit).  These can still be purchased for about $13-$20.  I've considered buying others if perhaps I can CAD up a more appropriate articulated arm for it.

MagniStitch stuck on the drill press vise

The MagniStitch didn't work well for me on the lathe simply because the arm is too short to reach around the tool post or to reach around the cross slide to see the dial.  I considered making my own from an inexpensive pocket magnifier and a flexible arm of some sort.  The trouble with buying cheap magnifiers is that a lot of them are completely useless shapes.  That is to say that they are neither spherical or hyperbolic lens profiles, but they're molded or ground to some freehand wavy biconic shape that makes them about as useful as a fishbowl.  I found some glass pocket magnifiers on Ebay that have proven to be good.  I just popped the rivet out of one and made a wire arm for it.  It works well enough, but it tends to wobble if it's on a machine that vibrates.  It needs a stiffer wire or some dampening ... or maybe I just need to replace the bad belts in the lathe.  It's a promising candidate though.

Pretending to put a shear cut on some trashy porous cast aluminum bar

In the course of scouring china-mart for lens-shaped objects, I saw this thing and thought I had a brilliant idea.  Increasing illumination helps a lot, partly because pupilary contraction increases depth of field.  I could slap a USB power bank of some sort onto that and have a little portable magnifier light.  Well, not really.  The lenses in these things are absolutely useless.  Luckily, it wasn't molded directly into the lamp.


Freehand wavy biconic lens-shaped object

I ended up taking another one of my glass pocket magnifiers and grinding the lens down to replace it.  The gooseneck is barely able to hold the lamp up anyway, and the USB connector really is a lousy mechanical support.  By the time I had the replacement lens fitted, I had pretty much condemned the USB power bank and the whole idea.  I glued it to another magnet and figured I'll use it until I come up with something better.


It's easy to fix the lens.  Just inherit an old lapidary wet grinder...

These aren't the best solutions, but they'll keep me busy for now.  I plan on whipping up an articulated arm system in CAD.  I'd like something simple that I can machine, but it might be nice to come up with a printable modular system. 

If you're looking for a more substantial conclusion to this rambling post, there isn't one.  These are not unique problems.  Although it might be difficult for someone in good health to understand how frustrating and disempowering the experience of poor vision can be, plenty of people have it worse than I do. 

Thursday, September 11, 2014

Selectively Invert workspaces for readability

UPDATE October 2019: The script has been updated for multirow operation, better window identification, and reduced execution time.

In my recent experiences dealing with the rapid progression of cataracts and vision loss, I had to make a lot of adjustments to my computing environment to accommodate for the diffusion.  In my sight, bright regions in the field of view bleed over dark areas and obscure them extremely effectively.  The diffusion averages the brightness of the entire visual field, nearly eliminating local contrast.  This pretty much means that the thin black lines of text on a white background disappear.  White characters on a dark background bleed as well, but the impact on readability is much smaller.

OH GOD THE BRIGHTNESS

I do understand that other individuals with cataracts claim different experiences regarding contrast preference; perhaps it is the differences in the types of cataract structure that explains those things.  Regardless, my overall goal is to control bright areas of my workspace and enforce a relatively high contrast light-on-dark regime. 

This new quest covered the normal bases:
  • GTK 2/3 theme
  • QT4 color settings
  • Desktop wallpaper
  • Custom user styles for Firefox/Stylish
... but still, there are things that can't be properly themed.  Wine applications, as well as certain things like virtual machines and Matlab/Simulink all appear as horrible fuzzy bright rectangles of pain.  What the hell does one do about those?

If i could invert an individual window's colors, that would be sufficient.  The only method i can recall to do this is with Compiz, and that's not going to happen for various reasons.  Under Mint 14/XFCE, the only thing i could think to do is invert the entire X display.  Since i normally run these offending applications maximized, the amount of remaining bright area is mostly restricted to the xfce4-panel area at the top.  Inverting the entire display can be done with xcalib, but unless one wants to get blinded every time the workspace is switched, the application of the screen inversion should be automated.

#!/bin/bash

# This is an ad-hoc replacement for wm-specific workspace switching (ctrl-alt-left, ctrl-alt-right, ctrl-alt-number)
# Script conditionally inverts X display on workspaces containing offending windows
# Helps to enforce dark, hi-contrast UI despite unthemeable windows (virtual machines, wine apps, etc)
# Works best when applications are run full-screen
# directional usage: switchworkspace left|right|up|down
# explicit usage: switchworkspace workspacenumber

# this version is about 5% faster than the old 1-D version
# and does not invoke gamma enforcement and inversion checks at endpoints

# specify the workspace/pager layout
nwsx=3 # number of columns
nwsy=3 # number of rows


current=$(wmctrl -d | sed -n 's/^\([0-9]\+\) *\*.*/\1/p')

if [ $1 ]; then
 ex='^[0-9]+$'
 if ! [[ $1 =~ $ex ]]; then
  # argument is not a number
  if [ $1 == "right" ] || [ $1 == "left" ]; then
   if [ $1 == "right" ]; then
    if [ $(($current % $nwsx)) == $(($nwsx-1)) ]; then
     exit
    else
     target=$(($current+1))
    fi
   elif [ $1 == "left" ]; then
    if [ $(($current % $nwsx)) == 0 ]; then
     exit
    else
     target=$(($current-1))
    fi
   fi
  elif [ $1 == "up" ] || [ $1 == "down" ]; then
   if [ $1 == "down" ]; then
    if [ $(($current / $nwsx)) == $(($nwsy-1)) ]; then
     exit
    else
     target=$(($current+$nwsx))
    fi
   elif [ $1 == "up" ]; then
    if [ $(($current / $nwsx)) == 0 ]; then
     exit
    else
     target=$(($current-$nwsx))
    fi
   fi
  else
   echo "unknown direction"
   exit
  fi
 else
  # argument is numeric
  total=$(wmctrl -d | wc -l)
  if [ $1 -gt $total ]; then
   target=$(($total-1))
  elif [ $1 -lt 1 ]; then
   target=0
  else
   target=$(($1-1))
  fi
  
  if [ $current == $target ]; then exit; fi
 fi 
else
 echo "must specify a workspace number or direction (right/left/up/down)"
 exit
fi


# add other applications by window title keyword or window class
# if multiple windows match, all corresponding desktops will be affected
winlist=$(wmctrl -lx)
inv[0]=$(echo "$winlist" | grep "XFramePeer.com-mathworks-util-PostVMInit" | cut -d \  -f 3)
inv[1]=$(echo "$winlist" | grep "XFramePeer.MATLAB" | cut -d \  -f 3)
inv[2]=$(echo "$winlist" | grep "eagle.exe." | cut -d \  -f 3)
inv[3]=$(echo "$winlist" | grep "scad3.exe." | cut -d \  -f 3)
inv[4]=$(echo "$winlist" | grep "femm.exe." | cut -d \  -f 3)


function contains {
 case "${inv[@]}" in  *"$1"*) 
  echo 1 
  return 1 ;; 
 esac
 echo 0
}

wmctrl -s $target

A=$(contains $target)
B=$(contains $current)

#echo $current $target $A $B

if [ $A == 1 ] && [ $B != 1 ]; then

 xcalib -i -a
elif  [ $A == 1 ] && [ $B == 1 ]; then
 return
else
 xcalib -clear

 # i have gamma presets for each of my two monitors
 # issuing 'xcalib -clear' will reset the gamma to 1.00
 # so i'll need to reassert my preference depending on the active display
 # you might not need this
 # when running this from a terminal, $DISPLAY may be ":0" depending on what's happened during its session
 thisdisp=$(echo $DISPLAY)
 #echo $thisdisp
 if [ "$thisdisp" == ":0.0" ]; then
  xgamma -quiet -gamma 0.87
 else
  xgamma -quiet -gamma 1.2
 fi
fi


The script is relatively simple and a bit of effort did go into making it quick.  An array of workspace numbers is created by searching for specific WM_CLASS strings in the window list.  When switching workspaces with this script, the inversion state of the display is altered to correspond to the workspace contents.

Two workspaces: Terminator (normal), Matlab/Simulink (inverted)

Just add appropriate window title keywords and set your preferred wrapping behavior in the script.  Reassign the appropriate keybindings (Ctrl-Alt-Left, Ctrl-Alt-Right) so they execute this script instead of the inbuilt window manager functions.  Script accepts one parameter, either "prev" or "next".