September 7, 2020 ⁓ 8 min read
Introduction
For a future project I want to simulate a CRT monitor for that nostalgic look. Back in the day, I had a Hazeltine Esprit terminal in my bedroom and worked with various other terminals at school and work. There was just something about that green glow that I miss. Let me see if I can recreate it.
So where to begin? Do I see what I can come up with? No way. I’m a programmer so that means that I am naturally lazy. Why reinvent the wheel if someone else has already done it?
I start my search looking for something I like. After a bunch of noes, I come across a demo page by Anders Evenrud. Wowsa! Here is his blog post describing it. I found my starting point. Anders went for hyperrealism. Having used dumb terminals for over 20 years, this terminal looks like something that was pulled out of the trash with its glitchy picture, rolling interference, and faded, out of focus tube. A little too much for me. I just want to give the feel of an old-time CRT without complete realism.
Using Anders’ code as a starting point, I started removing things. The animation had to go, it annoyed me too much. I took out some of the picture tube distortion and the scan lines. Again, I don’t need it too realistic and it allowed me to simplify the markup a bit. I also parameterized the settings with CSS custom properties (a.k.a., CSS variables).
The markup
The markup is pretty straightforward: just three nested divs containing the content.
<div class="crt-bezel">
<div class="crt">
<div class="crt-scan-area">
<em> West of House Score: 3 Moves: 17 </em>
You are in an open field west of a big white house with a boarded
front door.
There is a small mailbox here.
>OPEN MAILBOX
Opening the mailbox reveals:
A leaflet.
>APPLY THE BRAKES
The Frobozz Magic Go-Cart coasts to a stop.
Moss-Lined Tunnel, in the Go-Cart
This is a long east-west tunnel whose walls are covered with green and yellow mosses.
There is a jewel-studded monkey wrench here. (outside the Go-Cart)
A bent and rusty monkey wrench is lying here. (outside the Go-Cart)
>TAKE THE WRENCH
Which wrench do you mean, the jeweled monkey wrench or the rusty monkey wrench?
>JEWELED
You can't reach it from inside the Go-Cart.
>WEST
You're not going anywhere until you stand up.
</div>
</div>
</div>
The CSS
Before getting into the CSS itself, here are the custom properties. First, the phosphor colors:
$green-phosphor: #3f3; // P1
$amber-phosphor: #ffb000; // P3
$white-phosphor: #cce;
$phosphor-color: $amber-phosphor;
See them in action:
Here is the what was then the more common green:
And last, but least (in my opinion), white phosphor:
The remaining custom properties:
$crt-bg-color: #141414;
$crt-margin: 1em;
$crt-font-size: clamp(6px, 1.5vw, 20px); // these values need to be hand-tuned
$crt-line-height: 1.33333;
$crt-border-radius: 1.33333em;
$crt-char-width: 80; // add 1 if using vertical scroll bars
$crt-lines: 24; // number of lines or auto for full height
$crt-overflow-y: hidden; // auto or hidden (scroll bars or no)
$crt-bezel-width: 1.66667em;
$crt-overscan-width: 2.66667em;
Description
$crt-bg-color- The background color of the CRT. They were not really black. Someone suggested it should be
#282828, but I found that too light. $crt-margin- The margin outside the bezel. If you do not want it the same all the way around, set your values in appropriate place. This value must be whatever the top and bottom margin are for the height calculation to work.
$crt-font-size- The font size of the text. All of the sizing calculations are based off of this value (hence, all of the
emvalues in the CSS). I played around with having the CRT size scale with the browser width (1.5vw) and have a minimum and maximum value (theclamp()function). Change this to your liking: fixed size, media breakpoints, whatever. $crt-line-height- Line height of the text, obviously. I know that CRTs had pretty low line heights, but this seemed reasonable. I think
1.1is a more realistic value, but I went for better legibility. $crt-border-radius- The border radius of the bezel.
$crt-char-width- Character width of the terminal.
80was typical, the TRS-80 Model I was64, some terminals had a 132-character mode. Add 1 if you are using vertical scroll bars and want your full character width. $crt-lines- The number of lines on the CRT. Set it to
autoto display the CRT full screen height. $crt-overflow-y- Set to
autoto display a scroll bar orhiddento hide overflow. $crt-bezel-width- The width of the bezel.
$crt-overscan-width- The text on a CRT did not go all the way to the edge. This sets the padding between the bezel and the text.
Here is the CSS for the bezel:
.crt-bezel {
display: inline-block;
position: relative;
margin: $crt-margin;
box-shadow: inset 0 0 1px 0.66667em #000;
border-radius: $crt-border-radius;
background: #1D1D1D;
overflow: hidden;
font-size: $crt-font-size;
line-height: $crt-line-height;
&:before {
content: '';
position: absolute;
inset: 0;
z-index: 2;
background: linear-gradient(135deg, rgba(149,149,149,0.5) 0%,rgba(13,13,13,0.55) 19%,rgba(1,1,1,0.64) 50%,rgba(10,10,10,0.69) 69%,rgba(51,51,51,0.73) 84%,rgba(22,22,22,0.76) 93%,rgba(27,27,27,0.78) 100%);
opacity: .5;
}
&:after {
content: '';
position: absolute;
inset: 0;
z-index: 1;
background-color: #ddd;
opacity: .1;
}
}
The CSS for the CRT:
.crt {
position: relative;
margin: $crt-bezel-width;
border-radius: $crt-border-radius;
box-shadow: 0 0 1px 3px rgba(10, 10, 10, .7);
background: $crt-bg-color;
overflow: hidden;
z-index: 3;
&:before {
content: "";
position: absolute;
inset: 0;
box-shadow: inset 0 0 5px 5px rgba(255, 255, 255, .1);
border-radius: $crt-border-radius;
background: radial-gradient(ellipse at center, rgba($phosphor-color, 0.1) 0%, rgba(255,255,255,0) 100%);
z-index: -1;
}
}
The only thing of note here is the overlay that gives the screen a slight phosphor glow — a nice touch of realism without going overboard.
Finally, the CSS for the scan area (the text):
.crt-scan-area {
width: #{$crt-char-width * 1ch};
@if $crt-lines == 'auto' {
height: calc(100vh - #{$crt-margin * 2} - #{$crt-bezel-width * 2} - #{$crt-overscan-width * 2});
} @else {
height: ($crt-lines * 1em * $crt-line-height);
}
//scrollbar-width: none; // Maybe?
margin: $crt-overscan-width;
overflow-y: $crt-overflow-y;
white-space: pre-wrap;
font-family: monospace;
color: $phosphor-color;
text-shadow: 0 0 1px rgba($phosphor-color, .8);
em {
color: $crt-bg-color;
background-color: $phosphor-color;
font-style: normal;
}
}
When $crt-lines is set to auto, the height is set to the browser view height minus the top and bottom margin, minus the top and bottom bezel width, minus the top and bottom CRT overscan width. Notice the text shadow gives the text a slight glow effect. Without it, the text is too crisp and does not look like an old CRT.
The em element is used to set reverse-video text. (I do not remember any dumb terminals having italic characters.)
The result
Here is the final result with $crt-overflow-y set to auto:
Starting with a hyperrealistic CRT simulation from Anders Evenrud, I toned down the realism and added some options to get a nice reminiscence of a CRT. Enjoy. Feel free to offer any changes or ideas through codepen.
Credits: DEC VT100 terminal image by Jason Scott / CC BY