Skip to content

Instantly share code, notes, and snippets.

@jacygao
Created April 5, 2023 09:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jacygao/14872ee5ffc56be698be13a4a88d3c81 to your computer and use it in GitHub Desktop.
Save jacygao/14872ee5ffc56be698be13a4a88d3c81 to your computer and use it in GitHub Desktop.
Pure CSS 'organic-looking' EKG Animation
<div class="container">
<div class="grid">
<div class="col-10_sm-12">PATIENT ID #1888450</div>
<div class="col-2_sm-12">12/04/18 20:15</div>
</div>
</div>
<div class="container">
<div class="grid">
<div class="col-10_sm-12 graph">
<div class="cell cell-1"></div>
<div class="cell cell-2"></div>
<div class="cell cell-3"></div>
<div class="cell cell-4"></div>
<div class="cell cell-5"></div>
<div class="cell cell-6"></div>
</div>
<div class="col-2_sm-12">
<div class="number-1"><span>&#9829;</span></div>
</div>
</div>
</div>
<svg width="0" height="0" class="filters">
<defs>
<filter id="filter0_dd" x="0.858887" y="28.9809" width="644.262" height="124.108" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset/>
<feGaussianBlur stdDeviation="2"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.215686 0 0 0 0 0.686275 0 0 0 0 0.54902 0 0 0 1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset/>
<feGaussianBlur stdDeviation="4"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.215686 0 0 0 0 0.686275 0 0 0 0 0.54902 0 0 0 1 0"/>
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
</filter>
</defs>
</svg>

Pure CSS 'organic-looking' EKG Animation

Here's a quick explanation of what you see here. No JavaScript was harmed in the coding of this project.

The EKG unit requires just one external SVG file and two separate animation loops.

  • There are 6 different ‘EKG heartbeats’ provided by the SVG graphic. I’m switching their positions to make them look randomized (I’ve added tiny numbers at the bottom to make the order switching more obvious).

  • The first animation CSS animation simply loops the green gradient from left to right. An overlaid mask creates the EKG line shape.

  • A second animation treats each ‘heartbeat’ as a separate sprite and switches them – but the switch only happens in the dark section of the loop when it can’t be seen (the little numbers reveal the switch moment).

  • Even though each heartbeat uses exactly the same CSS animation (i.e. 163524), we can make them all appear different by offsetting their timings with negative animation-delays. Offsetting their beginnings obscures an otherwise obvious pattern.

  • It's important to remember that, although each 'heartbeat' unit can be as graphically unique and different as we like, the left and right edges MUST return to the same Y-axis point on each unit. In short, the graphics need to 'handshake' on their touching edges. That takes some tricky graphics making.

  • All animation is controlled by a single Sass variable at the top of the CSS window - $animation-time: 5s;. Changing that number will alter the big readout number and the EKG speed.

Next, I should try to build a little organic jitter into that readout number. It's too stable at the moment. Haven't thought about how yet..

It’s built for Chrome but should work anywhere.

(The little numbers at the base are just to aid the explanation)

MIT License

Copyright (c) 2019 Alex M Walker

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

A Pen by Alex on CodePen.

License.

$animation-time: 6s; /* This is the one-cycle time */
$animation-key: $animation-time/1s;
$newbpm: 360 / $animation-key;
$bpm: $animation-key*10s;
.number-1 {
font-size: 6rem;
text-align: center;
width: 6rem;
position: relative;
&:after {
display: block;
position: absolute;
content: $newbpm + "";
width: 12rem;
height: 10rem;
top: 0;
left: 0;
}
span {
font-size: 2rem;
position: relative;
top: -2rem;
//margin-left: 11rem;
left:-5rem;
opacity: 1;
animation: beat ($animation-time/6) infinite;
}
}
body {
background: #151515;
margin: 2rem 0;
background-position: top left, top right;
color: #37af8c;
font-family: "Exo 2", sans-serif;
font-size: 18px;
text-shadow: 0 0 4px #37af8c;
&:after {
/* Interlace effect */
background: linear-gradient(
0deg,
rgba(0, 0, 0, 0) 40%,
rgba(0, 0, 0, 0.2) 80%
);
background-size: 5px 3px;
display: block;
position: absolute;
content: "";
width: 100%;
height: 100%;
top: 0;
left: 0;
}
}
.container {
max-width: 900px;
margin: 0 auto;
}
.graph {
/* the green gradient */
outline: 1px #37af8c solid;
box-shadow: 0 0 4px #37af8c;
padding: 0;
background: linear-gradient(90deg, #231f20 0, #37af8c 50%, #231f20 50%);
animation: travel $animation-time infinite linear;
display: flex;
background-size: 100% auto;
background-position: -380px 0;
height: 200px;
}
.cell {
margin: 0;
display: flex;
width: 16.7%;
height: 100%;
background: url(https://gistcdn.githack.com/alexmwalker/0993e72c768ddcb45c1b1e41d3ffaffe/raw/ac804d1a4153c6a7595a9778626005feb34098a8/hr.svg);/* this SVG doesn't have the numbers */
background: url(https://gistcdn.githack.com/alexmwalker/ab0ffcafbeed4f91756a06531c5cba1d/raw/13a6b6d3b69316a8064f26dd9d341451c34f6bff/hr-with-numbers.svg);
background-size: 700% auto;
animation: shuffle $animation-time*6 steps(1) infinite;
&:hover {
}
}
.cell-1 {
background-position: 0, 0;
animation-delay: ($animation-time/6)*-2;
}
.cell-2 {
background-position: 16.6%, 0;
animation-delay: ($animation-time/6)*-13;
}
.cell-3 {
background-position: 33.3%, 0;
animation-delay: ($animation-time/6)*-30;
}
.cell-4 {
background-position: 50%, 0;
animation-delay: ($animation-time/6)*-5;
}
.cell-5 {
background-position: 66.6%, 0;
animation-delay: ($animation-time/6)*-22;
}
.cell-6 {
background-position: 83.3%, 0;
animation-delay: ($animation-time/6)*-9;
}
@keyframes shuffle {
0% {
background-position: 83.3%, 0;
}
16.6% {
background-position: 33.33%, 0;
}
33.3% {
background-position: 66.66%, 0;
}
50% {
background-position: 16.66%, 0;
}
66.6% {
background-position: 50%, 0;
}
83.3% {
background-position: 0%, 0;
}
}
@keyframes beat {
0% {
opacity: 1;
}
35% {
opacity: 1;
}
45% {
opacity: 0.3;
}
60% {
opacity: 1
}
100% {
opacity: 1;
}
}
@keyframes travel {
0% {
background-position: -380px 0;
}
100% {
background-position: 380px 0;
}
}
<link href="https://fonts.googleapis.com/css?family=Exo+2:700|Source+Code+Pro:400,600,700&amp;display=swap" rel="stylesheet" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/gridlex/2.7.1/gridlex.css" rel="stylesheet" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment