A social distance simulator based on the incredible work done by the Washington Post. This simulator assumes a 1% mortality rate.
Wash your hands, be safe, don't be an ass.
A Pen by HARUN PEHLİVAN on CodePen.
| <section> | |
| <canvas id="population" width="2000" height="1200"></canvas> | |
| <div class="info"> | |
| <canvas id="graph" height="400" width="1500"></canvas> | |
| <ul> | |
| <li><span>Recovered</span><span id="recovered">0</span></li> | |
| <li><span>Healthy</span><span id="healthy">0</span></li> | |
| <li><span>Sick</span><span id="sick">0</span></li> | |
| <li><span>Dead</span><span id="dead">0</span></li> | |
| </ul> | |
| </div> | |
| <div id="overlay" class="active"> | |
| <p> | |
| A social distance simulator based on <a href="https://www.washingtonpost.com/graphics/2020/world/corona-simulator/">the incredible work done by the Washington Post</a>. | |
| This simulator assumes a 1% mortality rate. | |
| </p> | |
| <div class="field is-grouped"> | |
| <span class="select"> | |
| <select id="quarantine"> | |
| <option value="0" selected>No Social Distancing</option> | |
| <option value="0.1">One-Tenth Self-Quarantine</option> | |
| <option value="0.25">One Quarter Self-Quarantine</option> | |
| <option value="0.33">One Third Self-Quarantine</option> | |
| <option value="0.5">One Half Self-Quarantine</option> | |
| <option value="0.75">Three Quarters Self-Quarantine</option> | |
| <option value="1">Everyone Self-Quarantines</option> | |
| </select> | |
| </span> | |
| | |
| <button class="button is-primary" id="run">Run</button> | |
| </div> | |
| </div> | |
| </section> |
| console.clear(); | |
| const PI = Math.PI; | |
| const PI2 = PI * 2; | |
| const SPEED = 2; | |
| const SICK_TIMEFRAME = 275; | |
| const COLOR = { | |
| recovered: "violet", | |
| sick: "tomato", | |
| dead: "white", | |
| healthy: "#00d1b2" | |
| }; | |
| const NORMAL = degrees => { | |
| const radians = (degrees * PI) / 180; | |
| return { x: Math.sin(radians), y: -Math.cos(radians) }; | |
| }; | |
| const WALLS = { | |
| N: { velocity: NORMAL(0) }, | |
| S: { velocity: NORMAL(0) }, | |
| E: { velocity: NORMAL(90) }, | |
| W: { velocity: NORMAL(90) } | |
| }; | |
| class Vector { | |
| constructor(x, y) { | |
| this.x = x; | |
| this.y = y; | |
| } | |
| } | |
| class Person { | |
| constructor(id, radius, width, height, sick, quarantined, vulnerable) { | |
| this.id = id; | |
| this.width = width; | |
| this.height = height; | |
| this.radius = radius; | |
| this.position = new Vector( | |
| Math.random() * (this.width - this.radius * 2) + this.radius, | |
| Math.random() * (this.height - this.radius * 2) + this.radius | |
| ); | |
| this.quarantined = quarantined; | |
| this.sick = sick; | |
| this.vulnerable = vulnerable; | |
| this.sickFrame = 0; | |
| this.recovered = false; | |
| this.angle = Math.random() * 360; | |
| this.updateVelocity(); | |
| } | |
| updateVelocity() { | |
| const radians = (this.angle * PI) / 180; | |
| this.velocity = { | |
| x: Math.sin(radians) * SPEED, | |
| y: -Math.cos(radians) * SPEED | |
| }; | |
| } | |
| get edge() { | |
| return { | |
| bottom: this.position.y + this.radius, | |
| left: this.position.x - this.radius, | |
| right: this.position.x + this.radius, | |
| top: this.position.y - this.radius | |
| }; | |
| } | |
| reflect({ velocity }) { | |
| const x = this.velocity.x * velocity.x; | |
| const y = this.velocity.y * velocity.y; | |
| const d = 2 * (x + y); | |
| this.velocity.x -= d * velocity.x; | |
| this.velocity.y -= d * velocity.y; | |
| } | |
| collide({ id, position }) { | |
| const dx = this.position.x - position.x; | |
| const dy = this.position.y - position.y; | |
| const d = Math.sqrt(dx * dx + dy * dy); | |
| return id !== this.id && d < this.radius * 2; | |
| } | |
| tick(population) { | |
| if (this.edge.left <= 0) this.reflect(WALLS.W); | |
| if (this.edge.right >= this.width) this.reflect(WALLS.E); | |
| if (this.edge.top <= 0) this.reflect(WALLS.N); | |
| if (this.edge.bottom >= this.height) this.reflect(WALLS.S); | |
| if (this.sick) { | |
| population.forEach(person => { | |
| if (!person.recovered && !person.sick && this.collide(person)) { | |
| person.sick = true; | |
| } | |
| }); | |
| } | |
| if (!this.quarantined && !this.dead) { | |
| this.position.x += this.velocity.x; | |
| this.position.y += this.velocity.y; | |
| } | |
| if (this.sick) this.sickFrame++; | |
| if (this.sickFrame >= SICK_TIMEFRAME) { | |
| this.sick = false; | |
| this.recovered = true; | |
| if (this.vulnerable) this.dead = true; | |
| } | |
| } | |
| handleReflection(person) { | |
| if (this.edge.left <= person.edge.right) this.reflect(WALLS.W); | |
| if (this.edge.right >= person.edge.left) this.reflect(WALLS.E); | |
| if (this.edge.top <= person.edge.bottom) this.reflect(WALLS.N); | |
| if (this.edge.bottom >= person.edge.top) this.reflect(WALLS.S); | |
| } | |
| } | |
| class Population { | |
| constructor(size, quarantineRate, patientZeroes) { | |
| this.people = []; | |
| this.canvas = document.getElementById("population"); | |
| this.context = this.canvas.getContext("2d"); | |
| this.width = this.canvas.width; | |
| this.height = this.canvas.height; | |
| this.radius = size < 250 ? 12 : size < 500 ? 8 : 5; | |
| for (let i = 0; i < size; i++) { | |
| const sick = i > size - patientZeroes - 1; | |
| const quarantined = i / size <= quarantineRate; | |
| const vulnerable = Math.random() < 0.01; | |
| this.people.push( | |
| new Person( | |
| i, | |
| this.radius, | |
| this.width, | |
| this.height, | |
| sick, | |
| quarantined, | |
| vulnerable | |
| ) | |
| ); | |
| } | |
| } | |
| tick() { | |
| this.context.clearRect(0, 0, this.width, this.height); | |
| this.people.forEach(p => { | |
| this.draw(p); | |
| p.tick(this.people); | |
| }); | |
| } | |
| draw({ dead, position, recovered, sick }) { | |
| this.context.fillStyle = sick | |
| ? COLOR.sick | |
| : dead | |
| ? COLOR.dead | |
| : recovered | |
| ? COLOR.recovered | |
| : COLOR.healthy; | |
| this.context.beginPath(); | |
| this.context.arc(position.x, position.y, this.radius, 0, PI2); | |
| this.context.fill(); | |
| } | |
| } | |
| class Graph { | |
| constructor(population) { | |
| this.population = population; | |
| this.canvas = document.getElementById("graph"); | |
| this.context = this.canvas.getContext("2d"); | |
| this.height = this.canvas.height; | |
| this.width = this.canvas.width; | |
| this.$recovered = document.getElementById("recovered"); | |
| this.$healthy = document.getElementById("healthy"); | |
| this.$sick = document.getElementById("sick"); | |
| this.$dead = document.getElementById("dead"); | |
| this.$recovered.style.color = COLOR.recovered; | |
| this.$healthy.style.color = COLOR.healthy; | |
| this.$sick.style.color = COLOR.sick; | |
| this.reset(); | |
| } | |
| reset() { | |
| this.x = 0; | |
| this.done = false; | |
| } | |
| tick() { | |
| let recovered = 0; | |
| let healthy = 0; | |
| let sick = 0; | |
| let dead = 0; | |
| this.population.people.forEach(person => { | |
| if (person.dead) dead++; | |
| else if (person.recovered) recovered++; | |
| else if (person.sick) sick++; | |
| else healthy++; | |
| }); | |
| const total = dead + recovered + healthy + sick; | |
| const recoveredH = (recovered / total) * this.height; | |
| const healthyH = (healthy / total) * this.height; | |
| const sickH = (sick / total) * this.height; | |
| const deadH = (dead / total) * this.height; | |
| this.$recovered.innerText = recovered; | |
| this.$healthy.innerText = healthy; | |
| this.$sick.innerText = sick; | |
| this.$dead.innerText = dead; | |
| this.context.strokeStyle = COLOR.recovered; | |
| this.context.beginPath(); | |
| this.context.moveTo(this.x, 0); | |
| this.context.lineTo(this.x, recoveredH); | |
| this.context.stroke(); | |
| this.context.strokeStyle = COLOR.healthy; | |
| this.context.beginPath(); | |
| this.context.moveTo(this.x, recoveredH); | |
| this.context.lineTo(this.x, recoveredH + healthyH); | |
| this.context.stroke(); | |
| this.context.strokeStyle = COLOR.sick; | |
| this.context.beginPath(); | |
| this.context.moveTo(this.x, recoveredH + healthyH); | |
| this.context.lineTo(this.x, recoveredH + healthyH + sickH); | |
| this.context.stroke(); | |
| this.context.strokeStyle = COLOR.dead; | |
| this.context.beginPath(); | |
| this.context.moveTo(this.x, recoveredH + healthyH + sickH); | |
| this.context.lineTo(this.x, recoveredH + healthyH + sickH + deadH); | |
| this.context.stroke(); | |
| this.x++; | |
| if (this.x >= this.width) this.done = true; | |
| } | |
| } | |
| const $overlay = document.getElementById("overlay"); | |
| const $quarantine = document.getElementById("quarantine"); | |
| const $run = document.getElementById("run"); | |
| $run.addEventListener("click", () => { | |
| const q = parseFloat($quarantine.value); | |
| const pop = new Population(1000, q, 3); | |
| const graph = new Graph(pop); | |
| graph.context.clearRect(0, 0, graph.width, graph.height); | |
| $overlay.classList.remove("active"); | |
| function run() { | |
| pop.tick(); | |
| graph.tick(); | |
| if (graph.done) $overlay.classList.add("active"); | |
| else requestAnimationFrame(run); | |
| } | |
| run(); | |
| }); |
A social distance simulator based on the incredible work done by the Washington Post. This simulator assumes a 1% mortality rate.
Wash your hands, be safe, don't be an ass.
A Pen by HARUN PEHLİVAN on CodePen.
| html, | |
| body { | |
| background: #111; | |
| } | |
| canvas { | |
| background: black; | |
| display: block; | |
| width: 100%; | |
| height: auto; | |
| } | |
| section { | |
| margin: 10vh auto; | |
| max-width: 1000px; | |
| width: 95%; | |
| position: relative; | |
| } | |
| #overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| bottom: 25%; | |
| padding: 3rem; | |
| box-sizing: border-box; | |
| width: 100%; | |
| transition: opacity 250ms ease-in-out; | |
| opacity: 0; | |
| color: white; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| background: rgba(0, 0, 0, 0.9); | |
| } | |
| #overlay.active { | |
| opacity: 1; | |
| } | |
| #overlay p { | |
| margin-bottom: 1rem; | |
| text-align: center; | |
| } | |
| #overlay a { | |
| color: #00d1b2; | |
| text-decoration: none; | |
| } | |
| section > div.info { | |
| display: flex; | |
| align-content: center; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| border-top: 1px solid #111; | |
| background: black; | |
| } | |
| section > div.info canvas { | |
| flex: 1; | |
| background: transparent; | |
| } | |
| section > div.info ul { | |
| list-style: none; | |
| margin: 0; | |
| padding: 0; | |
| text-align: center; | |
| color: white; | |
| padding: 1rem; | |
| width: 100%; | |
| box-sizing: border-box; | |
| font-size: 0.8rem; | |
| text-transform: uppercase; | |
| } | |
| @media (min-width: 600px) { | |
| section > div.info canvas { | |
| width: calc(100% - 200px - 1rem); | |
| } | |
| section > div.info ul { | |
| width: 200px; | |
| } | |
| } | |
| li { | |
| width: 100%; | |
| display: flex; | |
| justify-content: space-between; | |
| } |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.8.0/css/bulma.min.css" rel="stylesheet" /> |