├── favicon.png ├── ca_stats.png ├── assets ├── github.png ├── twitter.png ├── facebook.png ├── person-32x64.png ├── person-filled-32x64.png ├── person-src-512x512.png ├── person-filled-blue-32x64.png ├── foro_light │ ├── ForoLig-webfont.eot │ ├── ForoLig-webfont.ttf │ ├── ForoLig-webfont.woff │ ├── ForoLig-webfont.woff2 │ └── stylesheet.css └── person-filled-yellow-32x64.png ├── js ├── index.js ├── p3state.js ├── playable3.js ├── playable1.js └── playable2.js ├── css └── index.css └── index.html /favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonuy/firearms-and-deaths/HEAD/favicon.png -------------------------------------------------------------------------------- /ca_stats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonuy/firearms-and-deaths/HEAD/ca_stats.png -------------------------------------------------------------------------------- /assets/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonuy/firearms-and-deaths/HEAD/assets/github.png -------------------------------------------------------------------------------- /assets/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonuy/firearms-and-deaths/HEAD/assets/twitter.png -------------------------------------------------------------------------------- /assets/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonuy/firearms-and-deaths/HEAD/assets/facebook.png -------------------------------------------------------------------------------- /assets/person-32x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonuy/firearms-and-deaths/HEAD/assets/person-32x64.png -------------------------------------------------------------------------------- /assets/person-filled-32x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonuy/firearms-and-deaths/HEAD/assets/person-filled-32x64.png -------------------------------------------------------------------------------- /assets/person-src-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonuy/firearms-and-deaths/HEAD/assets/person-src-512x512.png -------------------------------------------------------------------------------- /assets/person-filled-blue-32x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonuy/firearms-and-deaths/HEAD/assets/person-filled-blue-32x64.png -------------------------------------------------------------------------------- /assets/foro_light/ForoLig-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonuy/firearms-and-deaths/HEAD/assets/foro_light/ForoLig-webfont.eot -------------------------------------------------------------------------------- /assets/foro_light/ForoLig-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonuy/firearms-and-deaths/HEAD/assets/foro_light/ForoLig-webfont.ttf -------------------------------------------------------------------------------- /assets/foro_light/ForoLig-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonuy/firearms-and-deaths/HEAD/assets/foro_light/ForoLig-webfont.woff -------------------------------------------------------------------------------- /assets/person-filled-yellow-32x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonuy/firearms-and-deaths/HEAD/assets/person-filled-yellow-32x64.png -------------------------------------------------------------------------------- /assets/foro_light/ForoLig-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonuy/firearms-and-deaths/HEAD/assets/foro_light/ForoLig-webfont.woff2 -------------------------------------------------------------------------------- /assets/foro_light/stylesheet.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Web Fonts from fontspring.com 3 | * 4 | * All OpenType features and all extended glyphs have been removed. 5 | * Fully installable fonts can be purchased at http://www.fontspring.com 6 | * 7 | * The fonts included in this stylesheet are subject to the End User License you purchased 8 | * from Fontspring. The fonts are protected under domestic and international trademark and 9 | * copyright law. You are prohibited from modifying, reverse engineering, duplicating, or 10 | * distributing this font software. 11 | * 12 | * (c) 2010-2015 Fontspring 13 | * 14 | * 15 | * 16 | * 17 | * The fonts included are copyrighted by the vendor listed below. 18 | * 19 | * Vendor: Hoftype 20 | * License URL: http://www.fontspring.com/licenses/hoftype/webfont 21 | * 22 | * 23 | */ 24 | 25 | @font-face { 26 | font-family: 'forolight'; 27 | src: url('ForoLig-webfont.eot'); 28 | src: url('ForoLig-webfont.eot?#iefix') format('embedded-opentype'), 29 | url('ForoLig-webfont.woff2') format('woff2'), 30 | url('ForoLig-webfont.woff') format('woff'), 31 | url('ForoLig-webfont.ttf') format('truetype'), 32 | url('ForoLig-webfont.svg#forolight') format('svg'); 33 | font-weight: normal; 34 | font-style: normal; 35 | 36 | } 37 | 38 | -------------------------------------------------------------------------------- /js/index.js: -------------------------------------------------------------------------------- 1 | var STATES = ['AK','AL','AR','AZ','CA','CO','CT','DE','FL','GA','HI','IA','ID', 2 | 'IL','IN','KS','KY','LA','MA','MD','ME','MI','MN','MO','MS','MT','NC','ND', 3 | 'NE','NH','NJ','NM','NV','NY','OH','OK','OR','PA','RI','SC','SD','TN','TX', 4 | 'UT','VA','VT','WA','WI','WV','WY']; 5 | 6 | var GLOBAL_SHOW_DEBUG = false; 7 | 8 | window.onload = function() {} 9 | 10 | window.onscroll = function() { 11 | var buffer; 12 | var canvas1Top; 13 | var canvas2Top; 14 | var canvas3Top; 15 | var canvas2Bottom; 16 | var canvas3Bottom; 17 | var screenTop; 18 | var screenBottom; 19 | 20 | // Only start playable1 once it comes into view 21 | buffer = 150; 22 | screenTop = window.scrollY; 23 | screenBottom = window.scrollY + window.innerHeight; 24 | 25 | canvas1Top = playable1Canvas.offsetTop + buffer; 26 | if (!playable1.hasStarted && screenBottom > canvas1Top) { 27 | playable1.run(); 28 | } 29 | 30 | canvas2Top = canvas2.offsetTop + buffer; 31 | canvas2Bottom = canvas2.offsetTop + canvas2.height; 32 | // Only init playable2 once it comes into view 33 | if (!playable2.hasStarted() && screenBottom > canvas2Top) { 34 | playable2.start(); 35 | } 36 | else if (playable2.hasStarted() && !playable2.isPaused() && 37 | (screenTop > canvas3Bottom || screenBottom < canvas2Top)) { 38 | playable2.pause(); 39 | } 40 | else if (playable2.isPaused() && screenBottom > canvas2Top && screenTop < canvas2Bottom) { 41 | playable2.resume(); 42 | } 43 | 44 | canvas3Top = canvas3.offsetTop + buffer; 45 | canvas3Bottom = canvas3.offsetTop + canvas3.height; 46 | if (!playable3.hasStarted() && screenBottom > canvas3Top) { 47 | playable3.start(); 48 | } 49 | else if (playable3.hasStarted() && !playable3.isPaused() && 50 | (screenTop > canvas3Bottom || screenBottom < canvas3Top)) { 51 | playable3.pause(); 52 | } 53 | else if (playable3.isPaused() && screenBottom > canvas3Top && screenTop < canvas3Bottom) { 54 | playable3.resume(); 55 | } 56 | 57 | } 58 | 59 | window.addEventListener('keyup', function(event) { 60 | // 68 = 'd' 61 | if (event.keyCode == 68) { 62 | GLOBAL_SHOW_DEBUG = !GLOBAL_SHOW_DEBUG; 63 | console.log('Show debug = ' + GLOBAL_SHOW_DEBUG); 64 | } 65 | 66 | }, false); -------------------------------------------------------------------------------- /js/p3state.js: -------------------------------------------------------------------------------- 1 | function P3State() { 2 | // X position 3 | this.x = 0; 4 | 5 | // Y position 6 | this.y = 0; 7 | 8 | // Draw size 9 | this.size = 56; 10 | 11 | // State name/label 12 | this.name = ''; 13 | 14 | // Number. Estimated deaths without law enacted. 15 | this.estDeaths = 0; 16 | 17 | // Number. Estimated lives saved with law enacted. 18 | this.estSaved = 0; 19 | 20 | // Boolean. True if simulating that this state has law enacted. 21 | this.lawEnacted = false; 22 | 23 | // Boolean. True if this state can be interacted with. 24 | this.enabled = true; 25 | 26 | this.ctx = undefined; 27 | this.canvas = undefined; 28 | 29 | this.isVisible = false; 30 | 31 | this.eventListener = undefined; 32 | } 33 | 34 | P3State.prototype.draw = function(mouseInBounds) { 35 | // Box fill color if law enacted 36 | if (this.lawEnacted) { 37 | if (this.enabled) { 38 | this.ctx.fillStyle = '#07a1c5'; 39 | } 40 | // These are the 4 states that already have the law enacted 41 | else { 42 | this.ctx.fillStyle = '#88ca41'; 43 | } 44 | 45 | this.ctx.fillRect(this.x+1, this.y+1, this.size-2, this.size-2); 46 | } 47 | 48 | // Change color and cursor style if being hovered over 49 | if (mouseInBounds) { 50 | if (this.enabled) { 51 | // Change cursor style 52 | this.canvas.style.cursor = 'pointer'; 53 | 54 | // Draw hover style 55 | if (this.lawEnacted) { 56 | this.ctx.fillStyle = '#63b3c5'; 57 | } 58 | else { 59 | this.ctx.fillStyle = '#cccccc'; 60 | } 61 | 62 | this.ctx.fillRect(this.x+1, this.y+1, this.size-2, this.size-2); 63 | } 64 | 65 | // Set as the hover event 66 | if (this.eventListener !== undefined) { 67 | this.eventListener.hoverEvent = this.name; 68 | } 69 | } 70 | 71 | // Draw outline 72 | this.ctx.strokeRect(this.x, this.y, this.size, this.size); 73 | 74 | // Draw label 75 | if (this.lawEnacted) { 76 | this.ctx.fillStyle = '#ffffff'; 77 | } 78 | else { 79 | this.ctx.fillStyle = '#000000'; 80 | } 81 | this.ctx.fillText(this.name, this.x + this.size / 2, this.y + this.size / 2); 82 | } 83 | 84 | P3State.prototype.onclick = function(event) { 85 | if (this.enabled) { 86 | this.lawEnacted = !this.lawEnacted; 87 | 88 | if (this.eventListener !== undefined) { 89 | this.eventListener.clickEvent = this.name; 90 | } 91 | } 92 | } 93 | 94 | P3State.prototype.isInBounds = function(xPos, yPos) { 95 | return xPos >= this.x && xPos <= this.x + this.size && 96 | yPos >= this.y && yPos <= this.y + this.size; 97 | } -------------------------------------------------------------------------------- /css/index.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'forolight'; 3 | src: url('../assets/foro_light/ForoLig-webfont.eot'); 4 | src: url('../assets/foro_light/ForoLig-webfont.eot?#iefix') format('embedded-opentype'), 5 | url('../assets/foro_light/ForoLig-webfont.woff2') format('woff2'), 6 | url('../assets/foro_light/ForoLig-webfont.woff') format('woff'), 7 | url('../assets/foro_light/ForoLig-webfont.ttf') format('truetype'), 8 | url('../assets/foro_light/ForoLig-webfont.svg#forolight') format('svg'); 9 | font-weight: normal; 10 | font-style: normal; 11 | 12 | } 13 | 14 | body { 15 | background-color: #fefefe; 16 | font-family: 'Helvetica'; 17 | margin: 0; 18 | padding: 0; 19 | } 20 | 21 | canvas { 22 | /*border: 1px solid black;*/ 23 | } 24 | 25 | footer { 26 | border-top: 1px solid #a0a0a0; 27 | font-family: 'forolight'; 28 | margin-bottom: 24px; 29 | padding: 24px 0; 30 | text-align: center; 31 | } 32 | 33 | footer a { 34 | color: black; 35 | text-decoration: none; 36 | } 37 | 38 | .content { 39 | margin: 64px auto; 40 | width: 648px; 41 | } 42 | 43 | .home-link { 44 | line-height: 42px; 45 | } 46 | 47 | .page-title { 48 | color: #fefefe; 49 | font-family: 'Helvetica'; 50 | font-size: 24px; 51 | letter-spacing: 8px; 52 | } 53 | 54 | .page-title-date { 55 | color: #fefefe; 56 | font-family: 'Helvetica'; 57 | font-style: italic; 58 | margin-top: 24px; 59 | letter-spacing: 1px 60 | } 61 | 62 | .page-title-section { 63 | background-color: #1e1e1e; 64 | padding: 64px 32px; 65 | text-align: center; 66 | } 67 | 68 | .playable-section-instructions { 69 | color: #a0a0a0; 70 | font-family: 'Helvetica'; 71 | font-size: 16px; 72 | font-style: italic; 73 | line-height: initial; 74 | margin: 6px 0; 75 | } 76 | .playable-section-title { 77 | font-family: 'Helvetica'; 78 | font-size: 18px; 79 | font-weight: 600; 80 | letter-spacing: 1px; 81 | } 82 | 83 | .resources { 84 | border-top: 1px solid #a0a0a0; 85 | font-family: 'forolight'; 86 | padding: 32px 0; 87 | } 88 | 89 | .resources li { 90 | overflow-wrap: break-word; 91 | } 92 | 93 | .section-body { 94 | font-family: 'forolight'; 95 | font-size: 18px; 96 | line-height: 32px; 97 | } 98 | 99 | .section-blockquote { 100 | font-family: 'forolight'; 101 | font-size: 18px; 102 | line-height: 32px; 103 | margin: 32px; 104 | text-align: justify; 105 | } 106 | 107 | .section-title { 108 | font-family: 'Helvetica'; 109 | font-size: 32px; 110 | font-weight: 600; 111 | letter-spacing: 4px; 112 | padding: 16px 0; 113 | } 114 | 115 | .share-links img { 116 | height: 42px; 117 | width: 42px; 118 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Firearms and Deaths 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 | FIREARMS AND DEATHS 23 |
24 |
25 | Posted: October 17, 2015
Updated: December 16, 2015 26 |
27 |
28 | 29 |
30 |
31 |

32 | This post was born out of frustration. There have been 24 school shootings so far in 2015. Combined they left 21 dead and 38 injured. That's 21 more deaths and 38 more injuries than there needed to be. 33 |

34 | 35 |

36 | I wanted to know if there was a solution. Something that both sides to the debate on guns in America could agree upon. I've also recently been inspired by explorable explanations - a way of visualizing and interacting with data that can help make both complex situations and abstract ideas more concrete. 37 |

38 | 39 |

40 | So this post is my first attempt at doing just that. I take a loaded and complex topic and try to make at least a little sense of it through the lens of one proposed solution. In the end, this turned out to not be about a solution to school shootings, at least not directly. But it is about decreasing the number of needless deaths due to firearms. And maybe that can be a first step to a healthier and safer gun culture in the US. 41 |

42 | 43 |

44 | Because the interactive parts for this will take a little bit of time to create, I'll be publishing updates to this post in 3 parts. 45 |

46 |
47 | 48 |
49 | PART 1: THE PROBLEM 50 |
51 | 52 |
53 |

54 | Guns are weapons that can and do kill people. There were 464,033 deaths due to firearms from 1999-2013. 55 |

56 |

57 | I'd initially thought this number would largely be attributed to gun violence, like the kind I see when I turn to the news on TV. Instead, I found that suicides accounted for 6 out of 10 firearm deaths in 2013. 60%! And that rate has remained fairly consistent for at least the past two decades. In addition to that, again between 1999 and 2013, nearly 10,000 people in the US died from unintentional shootings. Of that group, 2,260 tragically were young people between the ages of 0 and 19. 58 |

59 |

60 | Seeing that "0" in the age range is particularly heart-wrenching as a new parent. And there were more numbers, lots of numbers, that came from reading reports and looking through data - this little web app from the CDC was particularly helpful. While wading through all this, it's worth remembering that these numbers are more than just a data point on a chart. These numbers are people. And even if that number is as low as 1, that "1" still represents a worth that's immeasurable. 61 |

62 |
63 | 64 |
65 | Firearm deaths by state, 2010 - 2013 66 |
67 |
68 | Click on a state to see its stats. 69 |
70 | 71 | 72 | 73 |
74 |

75 | Prevention of suicides and unintentional deaths feels like an area that's less contentious than say... gun ownership. Firearm owner or not, people should be able to agree that preventing these deaths is a worthy goal to strive for. So maybe this is one place for all to find common ground, draw up strategies and save lives. 76 |

77 |
78 | 79 |
80 | PART 2: A SOLUTION 81 |
82 | 83 |
84 |

85 | To form a solution, we can look at two points on the nature of suicides. 86 |

87 | 88 |

89 | First, not all suicide attempts result in death. Suffocation, poisoning and use of firearm are the three leading forms of suicide. Of these, firearms have the highest fatality ratio at 85%. Compare this to medication overdoses which, despite being the most common form of attempting suicide, result in a fatality 2% of the time. People who become suicidal are far far more likely to die if they make the attempt using a gun. 90 |

91 | 92 |

93 | Second, suicides are typically impulsive acts. In a 2001 study of people who survived near lethal suicide attempts, 1 out of 4 went from the decision to commit suicide to the actual attempt in less than 5 minutes. 7 out of 10 made the attempt in less than an hour. The impulsive vulnerability of the person along with the lethal nature of a gun is a big reason the death toll is as high as it is. 94 |

95 |
96 | 97 |
98 | Guns are an irreversible solution to what is often a passing crisis. Suicidal individuals who take pills or inhale car exhaust or use razors have time to reconsider their actions or summon help. With a firearm, once the trigger is pulled, there's no turning back. [src] 99 |
100 | 101 |
102 |

103 | Empirical evidence suggests that we can reduce the rate of suicides by making it more difficult for a person to die in an act of deliberate self-harm. Fortunately, we already have a method that's shown it can do this for firearms. Gun locks. From trigger locks to cable locks to gun safes, the means of safely storing a gun are widely known, available and cheap. When states enact laws that require their use for handgun storage, we see 68% fewer firearm suicides per capita than states without these laws. 104 |

105 |
106 | 107 |
108 | A simulation on gun lock effectiveness 109 |
110 |
111 | Adjust the sliders to observe changes when a gun lock is used. Press "Start" to begin. 112 |
113 | 114 | 115 | 116 |
117 | Note: The "chance at prevention" default value is only an estimate based on the success of states with gun lock laws. Because its actual effectiveness on a more inividual case-by-case basis is unknown (at least to me), I've left it adjustable. 118 |
119 | 120 |
121 |

122 | The simulation isn't perfect, but it gets the idea across. The use of gun locks on stored firearms reduces the number of attempted suicides and so reduces the number of deaths. 123 |

124 |
125 | 126 |
127 | PART 3: THE IMPACT 128 |
129 | 130 |
131 |

132 | So through the use of gun locks we've got a low-cost and easily attained means of reducing firearm fatalities. We have evidence that suggests putting policies in place to require their use does result in fewer suicides per capita. There's also public support for these laws with 67% of respondents in a 2013 survey saying they'd be in favor of a law that would require gun-owners to lock their firearms. 133 |

134 |

135 | Yet, only 11 states mandate that locks be provided with the sale of a firearm. And just 4 - California, Connecticut, Massachusetts and New York - have laws that require they be safely stored and locked when not in use. If more states enacted these policies, how many fewer gun suicides would we have seen? 136 |

137 |
138 | 139 |
140 | What if states had gun lock policies in place 141 |
142 |
143 | Click on states to see how many estimated lives could've been saved from 2010-2013. 144 |
145 | 146 | 147 |
148 |

149 | Working off of data showing that gun lock laws result in 68% fewer firearm suicides per capita, if all 50 states had some form of the policy in effect, an estimated 49,024 lives would've been saved. 150 |

151 |

152 | Learn more from the resources below. Share your knowledge with others. And support an organization to help push through smart gun laws in your state. 153 |

154 |
155 | 156 |
157 |
158 | RESOURCES 159 |
160 |

161 | This is a list of resources I came across while learning about this topic. Not all are necessarily referenced above, but they did help form and shape the opinions stated. 162 |

163 | 192 |
193 | 194 |
195 |
196 | DESIGN REFERENCES 197 |
198 |

199 | The design for the second and third interactives were inspired by the following works: 200 |

201 | 209 |
210 | 211 | 227 | 228 | 229 |
230 | 231 | 232 | 233 |
234 |
235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 253 | 254 | -------------------------------------------------------------------------------- /js/playable3.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 11 boxes across 3 | * 7 boxes high 4 | * 5 | * 56 x 56? 6 | */ 7 | 8 | var playable3; 9 | var canvas3 = document.getElementById('canvas-3'); 10 | 11 | playable3 = (function () { 12 | var CANVAS_HEIGHT = canvas3.height; 13 | var CANVAS_WIDTH = canvas3.width; 14 | var CANVAS_GRID_SIZE = 12; 15 | var STATE_SIZE = 56; 16 | var STATE_CANVAS_MARGIN = 16; 17 | 18 | // Colors 19 | var COLOR_GRID = '#cccccc'; 20 | var COLOR_CROSSHAIRS = '#cc3333'; 21 | 22 | // Canvas and context 23 | var canvas; 24 | var ctx; 25 | 26 | // Current mouse pointer positions 27 | var mouseX; 28 | var mouseY; 29 | 30 | // Timers 31 | var time; 32 | var timeLastChecked; 33 | 34 | // Vars to help with startup animation 35 | var startupCounter = 0; 36 | var startupCountdown = 10; 37 | var startupInterval = 10; 38 | 39 | // Bottom estimates bar animation 40 | var estBarCountdown = 0; 41 | var estBarInterval = 250; 42 | var estBarOld = undefined; 43 | 44 | // Array of state objects 45 | var p3States = []; 46 | var p3StatesData = [ 47 | { 48 | abbr: 'AK', 49 | fullname: 'Alaska', 50 | row: 6, 51 | col: 0, 52 | deaths: 424, 53 | saved: 288, 54 | lawEnacted: false, 55 | }, 56 | { 57 | abbr: 'AL', 58 | fullname: 'Alabama', 59 | row: 5, 60 | col: 6, 61 | deaths: 1910, 62 | saved: 1298, 63 | lawEnacted: false, 64 | }, 65 | { 66 | abbr: 'AR', 67 | fullname: 'Arkansas', 68 | row: 4, 69 | col: 4, 70 | deaths: 1194, 71 | saved: 811, 72 | lawEnacted: false, 73 | }, 74 | { 75 | abbr: 'AZ', 76 | fullname: 'Arizona', 77 | row: 4, 78 | col: 1, 79 | deaths: 2612, 80 | saved: 1776, 81 | lawEnacted: false, 82 | }, 83 | { 84 | abbr: 'CA', 85 | fullname: 'California', 86 | row: 3, 87 | col: 0, 88 | deaths: 6176, 89 | saved: 0, 90 | lawEnacted: true, 91 | }, 92 | { 93 | abbr: 'CO', 94 | fullname: 'Colorado', 95 | row: 3, 96 | col: 2, 97 | deaths: 1892, 98 | saved: 1286, 99 | lawEnacted: false, 100 | }, 101 | { 102 | abbr: 'CT', 103 | fullname: 'Connecticut', 104 | row: 2, 105 | col: 9, 106 | deaths: 424, 107 | saved: 0, 108 | lawEnacted: true, 109 | }, 110 | { 111 | abbr: 'DE', 112 | fullname: 'Delaware', 113 | row: 3, 114 | col: 8, 115 | deaths: 195, 116 | saved: 132, 117 | lawEnacted: false, 118 | }, 119 | { 120 | abbr: 'FL', 121 | fullname: 'Florida', 122 | row: 6, 123 | col: 9, 124 | deaths: 6060, 125 | saved: 4120, 126 | lawEnacted: false, 127 | }, 128 | { 129 | abbr: 'GA', 130 | fullname: 'Georgia', 131 | row: 5, 132 | col: 7, 133 | deaths: 2957, 134 | saved: 2010, 135 | lawEnacted: false, 136 | }, 137 | { 138 | abbr: 'HI', 139 | fullname: 'Hawaii', 140 | row: 6, 141 | col: 1, 142 | deaths: 150, 143 | saved: 102, 144 | lawEnacted: false, 145 | }, 146 | { 147 | abbr: 'IA', 148 | fullname: 'Iowa', 149 | row: 2, 150 | col: 4, 151 | deaths: 758, 152 | saved: 515, 153 | lawEnacted: false, 154 | }, 155 | { 156 | abbr: 'ID', 157 | fullname: 'Idaho', 158 | row: 1, 159 | col: 1, 160 | deaths: 725, 161 | saved: 493, 162 | lawEnacted: false, 163 | }, 164 | { 165 | abbr: 'IL', 166 | fullname: 'Illinois', 167 | row: 3, 168 | col: 5, 169 | deaths: 1889, 170 | saved: 1284, 171 | lawEnacted: false, 172 | }, 173 | { 174 | abbr: 'IN', 175 | fullname: 'Indiana', 176 | row: 2, 177 | col: 5, 178 | deaths: 1902, 179 | saved: 1293, 180 | lawEnacted: false, 181 | }, 182 | { 183 | abbr: 'KS', 184 | fullname: 'Kansas', 185 | row: 4, 186 | col: 3, 187 | deaths: 981, 188 | saved: 667, 189 | lawEnacted: false, 190 | }, 191 | { 192 | abbr: 'KY', 193 | fullname: 'Kentucky', 194 | row: 3, 195 | col: 6, 196 | deaths: 1779, 197 | saved: 1209, 198 | lawEnacted: false, 199 | }, 200 | { 201 | abbr: 'LA', 202 | fullname: 'Louisiana', 203 | row: 5, 204 | col: 4, 205 | deaths: 1545, 206 | saved: 1050, 207 | lawEnacted: false, 208 | }, 209 | { 210 | abbr: 'MA', 211 | fullname: 'Massachusetts', 212 | row: 1, 213 | col: 10, 214 | deaths: 517, 215 | saved: 0, 216 | lawEnacted: true, 217 | }, 218 | { 219 | abbr: 'MD', 220 | fullname: 'Maryland', 221 | row: 3, 222 | col: 7, 223 | deaths: 993, 224 | saved: 675, 225 | lawEnacted: false, 226 | }, 227 | { 228 | abbr: 'ME', 229 | fullname: 'Maine', 230 | row: 0, 231 | col: 10, 232 | deaths: 457, 233 | saved: 310, 234 | lawEnacted: false, 235 | }, 236 | { 237 | abbr: 'MI', 238 | fullname: 'Michigan', 239 | row: 1, 240 | col: 6, 241 | deaths: 2517, 242 | saved: 1711, 243 | lawEnacted: false, 244 | }, 245 | { 246 | abbr: 'MN', 247 | fullname: 'Minnesota', 248 | row: 1, 249 | col: 4, 250 | deaths: 1251, 251 | saved: 850, 252 | lawEnacted: false, 253 | }, 254 | { 255 | abbr: 'MO', 256 | fullname: 'Missouri', 257 | row: 3, 258 | col: 4, 259 | deaths: 2104, 260 | saved: 1430, 261 | lawEnacted: false, 262 | }, 263 | { 264 | abbr: 'MS', 265 | fullname: 'Mississippi', 266 | row: 5, 267 | col: 5, 268 | deaths: 1099, 269 | saved: 747, 270 | lawEnacted: false, 271 | }, 272 | { 273 | abbr: 'MT', 274 | fullname: 'Montana', 275 | row: 1, 276 | col: 2, 277 | deaths: 593, 278 | saved: 403, 279 | lawEnacted: false, 280 | }, 281 | { 282 | abbr: 'NC', 283 | fullname: 'North Carolina', 284 | row: 4, 285 | col: 8, 286 | deaths: 2915, 287 | saved: 1982, 288 | lawEnacted: false, 289 | }, 290 | { 291 | abbr: 'ND', 292 | fullname: 'North Dakota', 293 | row: 1, 294 | col: 3, 295 | deaths: 246, 296 | saved: 167, 297 | lawEnacted: false, 298 | }, 299 | { 300 | abbr: 'NE', 301 | fullname: 'Nebraska', 302 | row: 3, 303 | col: 3, 304 | deaths: 443, 305 | saved: 301, 306 | lawEnacted: false, 307 | }, 308 | { 309 | abbr: 'NH', 310 | fullname: 'New Hampshire', 311 | row: 1, 312 | col: 9, 313 | deaths: 356, 314 | saved: 242, 315 | lawEnacted: false, 316 | }, 317 | { 318 | abbr: 'NJ', 319 | fullname: 'New Jersey', 320 | row: 3, 321 | col: 9, 322 | deaths: 722, 323 | saved: 490, 324 | lawEnacted: false, 325 | }, 326 | { 327 | abbr: 'NM', 328 | fullname: 'New Mexico', 329 | row: 4, 330 | col: 2, 331 | deaths: 877, 332 | saved: 596, 333 | lawEnacted: false, 334 | }, 335 | { 336 | abbr: 'NV', 337 | fullname: 'Nevada', 338 | row: 2, 339 | col: 1, 340 | deaths: 1127, 341 | saved: 766, 342 | lawEnacted: false, 343 | }, 344 | { 345 | abbr: 'NY', 346 | fullname: 'New York', 347 | row: 2, 348 | col: 8, 349 | deaths: 1945, 350 | saved: 0, 351 | lawEnacted: true, 352 | }, 353 | { 354 | abbr: 'OH', 355 | fullname: 'Ohio', 356 | row: 2, 357 | col: 6, 358 | deaths: 3034, 359 | saved: 2063, 360 | lawEnacted: false, 361 | }, 362 | { 363 | abbr: 'OK', 364 | fullname: 'Oklahoma', 365 | row: 5, 366 | col: 3, 367 | deaths: 1660, 368 | saved: 1128, 369 | lawEnacted: false, 370 | }, 371 | { 372 | abbr: 'OR', 373 | fullname: 'Oregon', 374 | row: 2, 375 | col: 0, 376 | deaths: 1475, 377 | saved: 1003, 378 | lawEnacted: false, 379 | }, 380 | { 381 | abbr: 'PA', 382 | fullname: 'Pennsylvania', 383 | row: 2, 384 | col: 7, 385 | deaths: 3382, 386 | saved: 2299, 387 | lawEnacted: false, 388 | }, 389 | { 390 | abbr: 'RI', 391 | fullname: 'Rhode Island', 392 | row: 2, 393 | col: 10, 394 | deaths: 110, 395 | saved: 74, 396 | lawEnacted: false, 397 | }, 398 | { 399 | abbr: 'SC', 400 | fullname: 'South Carolina', 401 | row: 5, 402 | col: 8, 403 | deaths: 1721, 404 | saved: 1170, 405 | lawEnacted: false, 406 | }, 407 | { 408 | abbr: 'SD', 409 | fullname: 'South Dakota', 410 | row: 2, 411 | col: 3, 412 | deaths: 272, 413 | saved: 184, 414 | lawEnacted: false, 415 | }, 416 | { 417 | abbr: 'TN', 418 | fullname: 'Tennessee', 419 | row: 4, 420 | col: 5, 421 | deaths: 2478, 422 | saved: 1685, 423 | lawEnacted: false, 424 | }, 425 | { 426 | abbr: 'TX', 427 | fullname: 'Texas', 428 | row: 6, 429 | col: 3, 430 | deaths: 6910, 431 | saved: 4698, 432 | lawEnacted: false, 433 | }, 434 | { 435 | abbr: 'UT', 436 | fullname: 'Utah', 437 | row: 3, 438 | col: 1, 439 | deaths: 1121, 440 | saved: 762, 441 | lawEnacted: false, 442 | }, 443 | { 444 | abbr: 'VA', 445 | fullname: 'Virginia', 446 | row: 4, 447 | col: 7, 448 | deaths: 2368, 449 | saved: 1610, 450 | lawEnacted: false, 451 | }, 452 | { 453 | abbr: 'VT', 454 | fullname: 'Vermont', 455 | row: 1, 456 | col: 8, 457 | deaths: 245, 458 | saved: 166, 459 | lawEnacted: false, 460 | }, 461 | { 462 | abbr: 'WA', 463 | fullname: 'Washington', 464 | row: 1, 465 | col: 0, 466 | deaths: 1971, 467 | saved: 1340, 468 | lawEnacted: false, 469 | }, 470 | { 471 | abbr: 'WI', 472 | fullname: 'Wisconsin', 473 | row: 1, 474 | col: 5, 475 | deaths: 1514, 476 | saved: 1029, 477 | lawEnacted: false, 478 | }, 479 | { 480 | abbr: 'WV', 481 | fullname: 'West Virginia', 482 | row: 4, 483 | col: 6, 484 | deaths: 833, 485 | saved: 566, 486 | lawEnacted: false, 487 | }, 488 | { 489 | abbr: 'WY', 490 | fullname: 'Wyoming', 491 | row: 2, 492 | col: 2, 493 | deaths: 358, 494 | saved: 243, 495 | lawEnacted: false, 496 | }, 497 | ]; 498 | 499 | // Event listener 500 | var stateEventListener = { 501 | hoverEvent: undefined, 502 | clickEvent: undefined, 503 | }; 504 | 505 | // Playable state 506 | var hasStarted; 507 | var isPaused; 508 | 509 | function init() { 510 | var i; 511 | var p3s; 512 | 513 | canvas = canvas3; 514 | ctx = canvas.getContext('2d'); 515 | hasStarted = false; 516 | isPaused = false; 517 | 518 | // Initialize states 519 | for (i = 0; i < p3StatesData.length; i++) { 520 | p3s = new P3State(); 521 | p3s.ctx = ctx; 522 | p3s.canvas = canvas; 523 | p3s.eventListener = stateEventListener; 524 | p3s.x = STATE_CANVAS_MARGIN + p3StatesData[i].col * STATE_SIZE; 525 | p3s.y = STATE_CANVAS_MARGIN + p3StatesData[i].row * STATE_SIZE; 526 | p3s.name = p3StatesData[i].abbr; 527 | p3s.estDeaths = p3StatesData[i].deaths; 528 | p3s.estSaved = p3StatesData[i].saved; 529 | p3s.size = STATE_SIZE; 530 | p3s.lawEnacted = p3StatesData[i].lawEnacted; 531 | if (p3s.lawEnacted === true) { 532 | p3s.enabled = false; 533 | } 534 | 535 | p3States[p3States.length] = p3s; 536 | } 537 | 538 | canvas.addEventListener('click', onClick); 539 | canvas.addEventListener('mousemove', onMouseMove); 540 | } 541 | 542 | function start() { 543 | hasStarted = true; 544 | 545 | draw(); 546 | } 547 | 548 | function pause() { 549 | isPaused = true; 550 | } 551 | 552 | function resume() { 553 | isPaused = false; 554 | draw(); 555 | } 556 | 557 | /** 558 | * The main draw loop. 559 | */ 560 | function draw() { 561 | var deltaTime; 562 | 563 | // Break out of the draw loop if paused 564 | if (isPaused === true) { 565 | return; 566 | } 567 | 568 | time = new Date().getTime(); 569 | // skip first frame 570 | if (timeLastChecked === undefined) { 571 | timeLastChecked = time; 572 | window.requestAnimationFrame(draw); 573 | return; 574 | } else { 575 | deltaTime = time - timeLastChecked; 576 | timeLastChecked = time; 577 | } 578 | 579 | ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); 580 | 581 | if (GLOBAL_SHOW_DEBUG) { 582 | debugDrawGrid(); 583 | debugDrawPointer(); 584 | } 585 | 586 | // Startup animation logic 587 | if (startupCounter < p3States.length) { 588 | if (startupCountdown <= 0) { 589 | p3States[startupCounter].isVisible = true; 590 | startupCounter++; 591 | startupCountdown = startupInterval; 592 | } else { 593 | startupCountdown -= deltaTime; 594 | } 595 | } 596 | 597 | // Draw state boxes 598 | drawStates(); 599 | 600 | // Do whatever in response to some custom events 601 | runStateEvents(); 602 | 603 | // Draw bottom bar of estimates 604 | // HACK: because of how I've setup the events and when this thing draws, this needs 605 | // to happen after runStateEvents() otherwise you'll see a flicker each time a 606 | // state is clicked. Sure, I could refactor, but mehhhhhh. 607 | drawEstimatesBar(deltaTime); 608 | 609 | window.requestAnimationFrame(draw); 610 | } 611 | 612 | /** 613 | * Draw state boxes. 614 | */ 615 | function drawStates() { 616 | var i; 617 | var mouseInBounds = false; 618 | 619 | // Default cursor style 620 | canvas.style.cursor = 'default'; 621 | 622 | // Default outline stroke style 623 | ctx.lineWidth = '2'; 624 | ctx.strokeStyle = '#000000'; 625 | 626 | // Default fill text style 627 | ctx.font = '16px Helvetica'; 628 | ctx.textAlign = 'center'; 629 | ctx.textBaseline = 'middle'; 630 | 631 | for (i = 0; i < p3States.length; i++) { 632 | if (p3States[i].isVisible) { 633 | if (p3States[i].isInBounds(mouseX, mouseY)) { 634 | mouseInBounds = true; 635 | } else { 636 | mouseInBounds = false; 637 | } 638 | p3States[i].draw(mouseInBounds); 639 | } 640 | } 641 | } 642 | 643 | /** 644 | * Draw bottom bar displaying suicide estimates. 645 | */ 646 | function drawEstimatesBar(deltaTime) { 647 | var i; 648 | var label; 649 | var useDefault = true; 650 | var defaultTotal = 81187; 651 | var defaultLabel = 'Firearm Suicides (2010-2013): ' + defaultTotal; 652 | var estLabel = 'Est. Firearm Suicides (2010-2013): '; 653 | var estSavedLabel = ' / Est. Saved: '; 654 | var estTotal = defaultTotal; 655 | var estSaved = 0; 656 | var animProgress = 0; 657 | 658 | for (i = 0; i < p3States.length; i++) { 659 | if (p3States[i].lawEnacted && p3States[i].enabled) { 660 | useDefault = false; 661 | estTotal -= p3States[i].estSaved; 662 | estSaved += p3States[i].estSaved; 663 | } 664 | } 665 | 666 | if (estBarCountdown > 0) { 667 | animProgress = estBarCountdown / estBarInterval; 668 | estBarCountdown -= deltaTime; 669 | } 670 | 671 | if (estBarCountdown < 0) { 672 | estBarCountdown = 0; 673 | } 674 | 675 | // Draw label 676 | ctx.fillStyle = '#000000'; 677 | ctx.font = 'bold 14px Helvetica'; 678 | ctx.textAlign = 'center'; 679 | ctx.textBaseline = 'top'; 680 | 681 | if (useDefault) { 682 | label = defaultLabel; 683 | } else { 684 | label = estLabel + estTotal + estSavedLabel + estSaved; 685 | } 686 | ctx.fillText(label, CANVAS_WIDTH / 2, 424); 687 | 688 | var boxX = STATE_CANVAS_MARGIN; 689 | var boxY = 444; 690 | var boxW = CANVAS_WIDTH - STATE_CANVAS_MARGIN * 2; 691 | var boxH = 24; 692 | 693 | // Draw box fill 694 | var estWidth; 695 | var origWidth = boxW - 2; 696 | var drawTotal = estTotal; 697 | 698 | if (animProgress > 0) { 699 | drawTotal += Math.floor(animProgress * (estBarOld - estTotal)); 700 | } 701 | 702 | estWidth = Math.floor((drawTotal / defaultTotal) * origWidth); 703 | 704 | // Background color underneath the animated bar 705 | ctx.fillStyle = '#07a1c5'; 706 | ctx.fillRect(boxX + 1, boxY + 1, boxW - 2, boxH - 2); 707 | // Animated bar 708 | ctx.fillStyle = '#ef5f48'; 709 | ctx.fillRect(boxX + 1, boxY + 1, estWidth, boxH - 2); 710 | 711 | // Draw box outline 712 | ctx.lineWidth = '2'; 713 | ctx.strokeStyle = '#000000'; 714 | ctx.strokeRect(boxX, boxY, boxW, boxH); 715 | } 716 | 717 | /** 718 | * Draw state stats 719 | */ 720 | function runStateEvents() { 721 | var i; 722 | var hasLawEnacted; 723 | var stateInfo; 724 | var labelsX = STATE_CANVAS_MARGIN + 146; 725 | var labelsY = STATE_CANVAS_MARGIN + 2; 726 | var statsX = STATE_CANVAS_MARGIN + 380; 727 | var statsY = STATE_CANVAS_MARGIN + 2; 728 | 729 | if (stateEventListener.hoverEvent !== undefined) { 730 | stateInfo = getStateInfo(stateEventListener.hoverEvent); 731 | 732 | ctx.fillStyle = '#000000'; 733 | ctx.font = 'bold 18px Helvetica'; 734 | ctx.textAlign = 'left'; 735 | ctx.textBaseline = 'top'; 736 | 737 | ctx.fillText( 738 | stateInfo.fullname, 739 | STATE_CANVAS_MARGIN, 740 | STATE_CANVAS_MARGIN 741 | ); 742 | 743 | ctx.font = '14px Helvetica'; 744 | 745 | ctx.fillText('Firearm suicides from 2010-2013:', labelsX, labelsY); 746 | ctx.fillText(stateInfo.deaths, statsX, statsY); 747 | 748 | if (stateInfo.saved > 0) { 749 | ctx.fillText( 750 | 'Est. # of suicides if law was in place:', 751 | labelsX, 752 | labelsY + 20 753 | ); 754 | ctx.fillText(stateInfo.deaths - stateInfo.saved, statsX, statsY + 20); 755 | } 756 | } 757 | 758 | if (stateEventListener.clickEvent !== undefined) { 759 | stateInfo = getStateInfo(stateEventListener.clickEvent); 760 | 761 | // start countdown to new value 762 | estBarCountdown = estBarInterval; 763 | 764 | // Store the starting value for the animation 765 | estBarOld = 0; 766 | for (i = 0; i < p3States.length; i++) { 767 | // This one was just clicked, so use the opposite 768 | if (stateInfo.abbr == p3States[i].name) { 769 | hasLawEnacted = !p3States[i].lawEnacted; 770 | } else { 771 | hasLawEnacted = p3States[i].lawEnacted; 772 | } 773 | 774 | if (hasLawEnacted) { 775 | estBarOld += p3States[i].estDeaths - p3States[i].estSaved; 776 | } else { 777 | estBarOld += p3States[i].estDeaths; 778 | } 779 | } 780 | 781 | // Clear the click event 782 | stateEventListener.clickEvent = undefined; 783 | } 784 | } 785 | 786 | /** 787 | * Helper that gets state info for the given abbreviation. 788 | */ 789 | function getStateInfo(abbr) { 790 | var i; 791 | 792 | for (i = 0; i < p3StatesData.length; i++) { 793 | if (abbr == p3StatesData[i].abbr) { 794 | return p3StatesData[i]; 795 | } 796 | } 797 | 798 | return undefined; 799 | } 800 | 801 | /** 802 | * For debug. Draw grid. 803 | */ 804 | function debugDrawGrid() { 805 | var i; 806 | var xpos; 807 | var ypos; 808 | 809 | ctx.strokeStyle = COLOR_GRID; 810 | 811 | // Draw vertical lines 812 | i = 0; 813 | while (i * CANVAS_GRID_SIZE <= CANVAS_WIDTH) { 814 | xpos = i * CANVAS_GRID_SIZE; 815 | 816 | ctx.beginPath(); 817 | ctx.moveTo(xpos, 0); 818 | ctx.lineTo(xpos, CANVAS_HEIGHT); 819 | ctx.stroke(); 820 | 821 | i++; 822 | } 823 | 824 | // Draw horizontal lines 825 | i = 0; 826 | while (i * CANVAS_GRID_SIZE <= CANVAS_HEIGHT) { 827 | ypos = i * CANVAS_GRID_SIZE; 828 | 829 | ctx.beginPath(); 830 | ctx.moveTo(0, ypos); 831 | ctx.lineTo(CANVAS_WIDTH, ypos); 832 | ctx.stroke(); 833 | 834 | i++; 835 | } 836 | } 837 | 838 | /** 839 | * Debug. Draw crosshairs on the pointer position. 840 | */ 841 | function debugDrawPointer() { 842 | ctx.fillStyle = COLOR_CROSSHAIRS; 843 | ctx.strokeStyle = COLOR_CROSSHAIRS; 844 | 845 | ctx.beginPath(); 846 | 847 | // Vertical line 848 | ctx.moveTo(mouseX, 0); 849 | ctx.lineTo(mouseX, CANVAS_HEIGHT); 850 | 851 | // Horizontal line 852 | ctx.moveTo(0, mouseY); 853 | ctx.lineTo(CANVAS_WIDTH, mouseY); 854 | 855 | ctx.stroke(); 856 | 857 | // Draw positions 858 | ctx.font = '12px Helvetica'; 859 | ctx.textAlign = 'left'; 860 | ctx.textBaseline = 'top'; 861 | ctx.fillText('x: ' + mouseX, CANVAS_WIDTH - 40, 12); 862 | ctx.fillText('y: ' + mouseY, CANVAS_WIDTH - 40, 24); 863 | } 864 | 865 | /** 866 | * click event listener 867 | */ 868 | function onClick(event) { 869 | for (i = 0; i < p3States.length; i++) { 870 | if (p3States[i].isInBounds(mouseX, mouseY)) { 871 | p3States[i].onclick(); 872 | return; 873 | } 874 | } 875 | } 876 | 877 | /** 878 | * mousemove event listener 879 | */ 880 | function onMouseMove(event) { 881 | mouseX = event.offsetX; 882 | mouseY = event.offsetY; 883 | } 884 | 885 | return { 886 | hasStarted: function () { 887 | return hasStarted; 888 | }, 889 | isPaused: function () { 890 | return isPaused; 891 | }, 892 | init: init, 893 | start: start, 894 | pause: pause, 895 | resume: resume, 896 | }; 897 | })(); 898 | 899 | playable3.init(); 900 | -------------------------------------------------------------------------------- /js/playable1.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Controls the canvas canvas-1. 3 | * 4 | * The intent of this canvas is to just show stats. For each state, how many 5 | * deaths were there by firearms for a given year. Of those, what percentage 6 | * were accidental deaths and what percentage were by suicide. 7 | * 8 | * @todo: Possible consideration - instead of each being out of a 100 persons, 9 | * normalize the total number of persons displayed for each state. So if MD has 10 | * the most firearm deaths overall, that'll have 100 shown. And if VA has only 11 | * 10% of that, then only show 10 there. 12 | * 13 | * Canvas size: 648 x 440 (TBD) 14 | * 15 | */ 16 | var playable1; 17 | var playable1Canvas = document.getElementById('canvas-1'); 18 | 19 | playable1 = (function () { 20 | var canvas; 21 | var ctx; 22 | var selectedState; 23 | var STARTING_STATE = 'CA'; 24 | var CANVAS_WIDTH = 648; 25 | var CANVAS_HEIGHT = 440; 26 | var stateHitBoxes = []; 27 | var hasStarted = false; 28 | var hasStateHighlight = false; 29 | 30 | // Helper vars for drawing state numbers 31 | var drawNumsTotalAnimTime = 500; 32 | var drawNumsAnimInterval = 50; 33 | var drawNumsAnimTime = 0; 34 | var drawNumsIntervalId = 0; 35 | 36 | // Colors 37 | var blueColor = '#387567'; 38 | var yellowColor = '#D1A732'; 39 | var blackColor = '#000000'; 40 | 41 | // Data from 2010-2013 retrieved from http://webappa.cdc.gov/sasweb/ncipc/dataRestriction_inj.html 42 | var STATE_DATA = { 43 | AK: { 44 | name: 'Alaska', 45 | suicide_normalized: 4, 46 | accident_normalized: 1, 47 | total_normalized: 5, 48 | suicide_raw: 424, 49 | accident_raw: 11, 50 | total_raw: 546, 51 | }, 52 | AL: { 53 | name: 'Alabama', 54 | suicide_normalized: 16, 55 | accident_normalized: 1, 56 | total_normalized: 27, 57 | suicide_raw: 1910, 58 | accident_raw: 99, 59 | total_raw: 3258, 60 | }, 61 | AR: { 62 | name: 'Arkansas', 63 | suicide_normalized: 10, 64 | accident_normalized: 1, 65 | total_normalized: 16, 66 | suicide_raw: 1194, 67 | accident_raw: 50, 68 | total_raw: 1872, 69 | }, 70 | AZ: { 71 | name: 'Arizona', 72 | suicide_normalized: 21, 73 | accident_normalized: 1, 74 | total_normalized: 31, 75 | suicide_raw: 2612, 76 | accident_raw: 31, 77 | total_raw: 3782, 78 | }, 79 | CA: { 80 | name: 'California', 81 | suicide_normalized: 51, 82 | accident_normalized: 1, 83 | total_normalized: 100, 84 | suicide_raw: 6176, 85 | accident_raw: 115, 86 | total_raw: 12033, 87 | }, 88 | CO: { 89 | name: 'Colorado', 90 | suicide_normalized: 15, 91 | accident_normalized: 1, 92 | total_normalized: 20, 93 | suicide_raw: 1892, 94 | accident_raw: 35, 95 | total_raw: 2419, 96 | }, 97 | CT: { 98 | name: 'Connecticut', 99 | suicide_normalized: 4, 100 | accident_normalized: 0, 101 | total_normalized: 7, 102 | suicide_raw: 424, 103 | accident_raw: 0, 104 | total_raw: 807, 105 | }, 106 | DE: { 107 | name: 'Delaware', 108 | suicide_normalized: 2, 109 | accident_normalized: 0, 110 | total_normalized: 3, 111 | suicide_raw: 195, 112 | accident_raw: 0, 113 | total_raw: 361, 114 | }, 115 | FL: { 116 | name: 'Florida', 117 | suicide_normalized: 50, 118 | accident_normalized: 1, 119 | total_normalized: 79, 120 | suicide_raw: 6060, 121 | accident_raw: 89, 122 | total_raw: 9561, 123 | }, 124 | GA: { 125 | name: 'Georgia', 126 | suicide_normalized: 25, 127 | accident_normalized: 1, 128 | total_normalized: 42, 129 | suicide_raw: 2957, 130 | accident_raw: 129, 131 | total_raw: 5003, 132 | }, 133 | HI: { 134 | name: 'Hawaii', 135 | suicide_normalized: 2, 136 | accident_normalized: 0, 137 | total_normalized: 2, 138 | suicide_raw: 150, 139 | accident_raw: 0, 140 | total_raw: 183, 141 | }, 142 | IA: { 143 | name: 'Iowa', 144 | suicide_normalized: 7, 145 | accident_normalized: 0, 146 | total_normalized: 8, 147 | suicide_raw: 758, 148 | accident_raw: 12, 149 | total_raw: 903, 150 | }, 151 | ID: { 152 | name: 'Idaho', 153 | suicide_normalized: 6, 154 | accident_normalized: 0, 155 | total_normalized: 7, 156 | suicide_raw: 725, 157 | accident_raw: 21, 158 | total_raw: 824, 159 | }, 160 | IL: { 161 | name: 'Illinois', 162 | suicide_normalized: 16, 163 | accident_normalized: 1, 164 | total_normalized: 37, 165 | suicide_raw: 1889, 166 | accident_raw: 77, 167 | total_raw: 4473, 168 | }, 169 | IN: { 170 | name: 'Indiana', 171 | suicide_normalized: 16, 172 | accident_normalized: 1, 173 | total_normalized: 25, 174 | suicide_raw: 1902, 175 | accident_raw: 61, 176 | total_raw: 3004, 177 | }, 178 | KS: { 179 | name: 'Kansas', 180 | suicide_normalized: 8, 181 | accident_normalized: 1, 182 | total_normalized: 11, 183 | suicide_raw: 981, 184 | accident_raw: 29, 185 | total_raw: 1337, 186 | }, 187 | KY: { 188 | name: 'Kentucky', 189 | suicide_normalized: 15, 190 | accident_normalized: 1, 191 | total_normalized: 20, 192 | suicide_raw: 1779, 193 | accident_raw: 69, 194 | total_raw: 2449, 195 | }, 196 | LA: { 197 | name: 'Louisiana', 198 | suicide_normalized: 13, 199 | accident_normalized: 1, 200 | total_normalized: 29, 201 | suicide_raw: 1545, 202 | accident_raw: 146, 203 | total_raw: 3460, 204 | }, 205 | MA: { 206 | name: 'Massachusetts', 207 | suicide_normalized: 4, 208 | accident_normalized: 0, 209 | total_normalized: 8, 210 | suicide_raw: 517, 211 | accident_raw: 0, 212 | total_raw: 970, 213 | }, 214 | MD: { 215 | name: 'Maryland', 216 | suicide_normalized: 8, 217 | accident_normalized: 1, 218 | total_normalized: 19, 219 | suicide_raw: 993, 220 | accident_raw: 15, 221 | total_raw: 2247, 222 | }, 223 | ME: { 224 | name: 'Maine', 225 | suicide_normalized: 3, 226 | accident_normalized: 0, 227 | total_normalized: 4, 228 | suicide_raw: 457, 229 | accident_raw: 0, 230 | total_raw: 537, 231 | }, 232 | MI: { 233 | name: 'Michigan', 234 | suicide_normalized: 21, 235 | accident_normalized: 1, 236 | total_normalized: 39, 237 | suicide_raw: 2517, 238 | accident_raw: 43, 239 | total_raw: 4644, 240 | }, 241 | MN: { 242 | name: 'Minnesota', 243 | suicide_normalized: 10, 244 | accident_normalized: 1, 245 | total_normalized: 13, 246 | suicide_raw: 1251, 247 | accident_raw: 17, 248 | total_raw: 1570, 249 | }, 250 | MO: { 251 | name: 'Missouri', 252 | suicide_normalized: 18, 253 | accident_normalized: 1, 254 | total_normalized: 29, 255 | suicide_raw: 2104, 256 | accident_raw: 72, 257 | total_raw: 3462, 258 | }, 259 | MS: { 260 | name: 'Mississippi', 261 | suicide_normalized: 9, 262 | accident_normalized: 1, 263 | total_normalized: 17, 264 | suicide_raw: 1099, 265 | accident_raw: 67, 266 | total_raw: 2065, 267 | }, 268 | MT: { 269 | name: 'Montana', 270 | suicide_normalized: 5, 271 | accident_normalized: 0, 272 | total_normalized: 6, 273 | suicide_raw: 593, 274 | accident_raw: 17, 275 | total_raw: 674, 276 | }, 277 | NC: { 278 | name: 'North Carolina', 279 | suicide_normalized: 24, 280 | accident_normalized: 1, 281 | total_normalized: 39, 282 | suicide_raw: 2915, 283 | accident_raw: 115, 284 | total_raw: 4675, 285 | }, 286 | ND: { 287 | name: 'North Dakota', 288 | suicide_normalized: 2, 289 | accident_normalized: 0, 290 | total_normalized: 2, 291 | suicide_raw: 246, 292 | accident_raw: 0, 293 | total_raw: 285, 294 | }, 295 | NE: { 296 | name: 'Nebraska', 297 | suicide_normalized: 3, 298 | accident_normalized: 1, 299 | total_normalized: 5, 300 | suicide_raw: 443, 301 | accident_raw: 19, 302 | total_raw: 648, 303 | }, 304 | NH: { 305 | name: 'New Hampshire', 306 | suicide_normalized: 3, 307 | accident_normalized: 0, 308 | total_normalized: 3, 309 | suicide_raw: 356, 310 | accident_raw: 11, 311 | total_raw: 420, 312 | }, 313 | NJ: { 314 | name: 'New Jersey', 315 | suicide_normalized: 6, 316 | accident_normalized: 1, 317 | total_normalized: 16, 318 | suicide_raw: 722, 319 | accident_raw: 13, 320 | total_raw: 1888, 321 | }, 322 | NM: { 323 | name: 'New Mexico', 324 | suicide_normalized: 7, 325 | accident_normalized: 1, 326 | total_normalized: 10, 327 | suicide_raw: 877, 328 | accident_raw: 10, 329 | total_raw: 1256, 330 | }, 331 | NV: { 332 | name: 'Nevada', 333 | suicide_normalized: 10, 334 | accident_normalized: 1, 335 | total_normalized: 13, 336 | suicide_raw: 1127, 337 | accident_raw: 13, 338 | total_raw: 1526, 339 | }, 340 | NY: { 341 | name: 'New York', 342 | suicide_normalized: 16, 343 | accident_normalized: 1, 344 | total_normalized: 32, 345 | suicide_raw: 1945, 346 | accident_raw: 27, 347 | total_raw: 3848, 348 | }, 349 | OH: { 350 | name: 'Ohio', 351 | suicide_normalized: 25, 352 | accident_normalized: 1, 353 | total_normalized: 41, 354 | suicide_raw: 3034, 355 | accident_raw: 55, 356 | total_raw: 4927, 357 | }, 358 | OK: { 359 | name: 'Oklahoma', 360 | suicide_normalized: 14, 361 | accident_normalized: 1, 362 | total_normalized: 20, 363 | suicide_raw: 1660, 364 | accident_raw: 69, 365 | total_raw: 2417, 366 | }, 367 | OR: { 368 | name: 'Oregon', 369 | suicide_normalized: 12, 370 | accident_normalized: 1, 371 | total_normalized: 15, 372 | suicide_raw: 1475, 373 | accident_raw: 19, 374 | total_raw: 1783, 375 | }, 376 | PA: { 377 | name: 'Pennsylvania', 378 | suicide_normalized: 28, 379 | accident_normalized: 1, 380 | total_normalized: 47, 381 | suicide_raw: 3382, 382 | accident_raw: 139, 383 | total_raw: 5649, 384 | }, 385 | RI: { 386 | name: 'Rhode Island', 387 | suicide_normalized: 1, 388 | accident_normalized: 0, 389 | total_normalized: 1, 390 | suicide_raw: 110, 391 | accident_raw: 0, 392 | total_raw: 180, 393 | }, 394 | SC: { 395 | name: 'South Carolina', 396 | suicide_normalized: 15, 397 | accident_normalized: 1, 398 | total_normalized: 24, 399 | suicide_raw: 1721, 400 | accident_raw: 78, 401 | total_raw: 2841, 402 | }, 403 | SD: { 404 | name: 'South Dakota', 405 | suicide_normalized: 3, 406 | accident_normalized: 0, 407 | total_normalized: 3, 408 | suicide_raw: 272, 409 | accident_raw: 0, 410 | total_raw: 310, 411 | }, 412 | TN: { 413 | name: 'Tennessee', 414 | suicide_normalized: 20, 415 | accident_normalized: 1, 416 | total_normalized: 32, 417 | suicide_raw: 2478, 418 | accident_raw: 103, 419 | total_raw: 3905, 420 | }, 421 | TX: { 422 | name: 'Texas', 423 | suicide_normalized: 57, 424 | accident_normalized: 1, 425 | total_normalized: 90, 426 | suicide_raw: 6910, 427 | accident_raw: 194, 428 | total_raw: 10834, 429 | }, 430 | UT: { 431 | name: 'Utah', 432 | suicide_normalized: 10, 433 | accident_normalized: 0, 434 | total_normalized: 11, 435 | suicide_raw: 1121, 436 | accident_raw: 10, 437 | total_raw: 1285, 438 | }, 439 | VA: { 440 | name: 'Virginia', 441 | suicide_normalized: 20, 442 | accident_normalized: 1, 443 | total_normalized: 29, 444 | suicide_raw: 2368, 445 | accident_raw: 45, 446 | total_raw: 3447, 447 | }, 448 | VT: { 449 | name: 'Vermont', 450 | suicide_normalized: 2, 451 | accident_normalized: 0, 452 | total_normalized: 2, 453 | suicide_raw: 245, 454 | accident_raw: 0, 455 | total_raw: 269, 456 | }, 457 | WA: { 458 | name: 'Washington', 459 | suicide_normalized: 16, 460 | accident_normalized: 1, 461 | total_normalized: 21, 462 | suicide_raw: 1971, 463 | accident_raw: 33, 464 | total_raw: 2546, 465 | }, 466 | WI: { 467 | name: 'Wisconsin', 468 | suicide_normalized: 13, 469 | accident_normalized: 1, 470 | total_normalized: 17, 471 | suicide_raw: 1514, 472 | accident_raw: 19, 473 | total_raw: 1999, 474 | }, 475 | WV: { 476 | name: 'West Virginia', 477 | suicide_normalized: 7, 478 | accident_normalized: 1, 479 | total_normalized: 9, 480 | suicide_raw: 833, 481 | accident_raw: 23, 482 | total_raw: 1109, 483 | }, 484 | WY: { 485 | name: 'Wyoming', 486 | suicide_normalized: 3, 487 | accident_normalized: 0, 488 | total_normalized: 3, 489 | suicide_raw: 358, 490 | accident_raw: 13, 491 | total_raw: 407, 492 | }, 493 | }; 494 | 495 | /** 496 | * Setup initial view of the canvas. Not yet interactive. 497 | */ 498 | function init() { 499 | canvas = playable1Canvas; 500 | ctx = canvas.getContext('2d'); 501 | 502 | setupLegend(); 503 | drawStates(STARTING_STATE, undefined, true); 504 | } 505 | 506 | /** 507 | * Start interactive stuff. 508 | */ 509 | function run() { 510 | this.hasStarted = true; 511 | 512 | updatePersons(selectedState); 513 | 514 | canvas.addEventListener('click', onClick); 515 | canvas.addEventListener('mousemove', onMouseMove.bind({ this: this })); 516 | } 517 | 518 | /** 519 | * Draw the legend section of the canvas. 520 | */ 521 | function setupLegend() { 522 | ctx.font = '14px Helvetica'; 523 | ctx.textAlign = 'left'; 524 | 525 | // Suicide label 526 | ctx.fillStyle = blueColor; 527 | ctx.fillRect(476, 108, 24, 24); 528 | ctx.fillText('Suicide', 512, 126); 529 | 530 | // Unintentional label 531 | ctx.fillStyle = yellowColor; 532 | ctx.fillRect(476, 152, 24, 24); 533 | ctx.fillText('Unintentional Death', 512, 170); 534 | 535 | // Other 536 | ctx.strokeStyle = '#000000'; 537 | ctx.strokeRect(476, 196, 24, 24); 538 | ctx.strokeRect(477, 197, 22, 22); 539 | ctx.fillStyle = '#000000'; 540 | ctx.fillText('Other', 512, 214); 541 | } 542 | 543 | /** 544 | * Draws updates to the state boxes. Yea, there'll be a little copy/paste 545 | * action that's about to happen. 546 | * 547 | * @param selected State that's selected 548 | * @param highlight State to highlight 549 | * @param isInit true if this is the initial draw of the states 550 | */ 551 | function drawStates(selected, highlight, isInit) { 552 | var i; 553 | var currentX; 554 | var currentY; 555 | var hitBox; 556 | var startPosX = 24; 557 | var startPosY = 24; 558 | var boxSize = 24; 559 | 560 | if (highlight) { 561 | hasStateHighlight = true; 562 | } else { 563 | hasStateHighlight = false; 564 | } 565 | 566 | if (typeof selected === 'undefined') { 567 | console.log( 568 | 'WARNING: playable1.drawStates called without a selected state' 569 | ); 570 | } else if (selected && selectedState != selected) { 571 | selectedState = selected; 572 | 573 | // Draw state name 574 | ctx.clearRect(24, 346 - 16, 624, 200); //@todo figure out what that 200 height should actually be 575 | ctx.font = '600 16px Helvetica'; 576 | ctx.fillStyle = '#000000'; 577 | ctx.textAlign = 'left'; 578 | ctx.fillText(STATE_DATA[selected].name, 24, 346); 579 | 580 | // Clear any existing draw interval and start a new one 581 | if (drawNumsIntervalId > 0) { 582 | window.clearInterval(drawNumsIntervalId); 583 | } 584 | 585 | drawNumsAnimTime = 0; 586 | drawNumsIntervalId = window.setInterval( 587 | drawStateNumbers.bind({ state: selected }), 588 | drawNumsAnimInterval 589 | ); 590 | } 591 | 592 | ctx.clearRect(startPosX, startPosY, boxSize * 25, boxSize * 2); 593 | 594 | ctx.font = '12px Helvetica'; 595 | ctx.textAlign = 'center'; 596 | 597 | for (i = 0; i < STATES.length; i++) { 598 | // Reset styles 599 | ctx.strokeStyle = '#000000'; 600 | ctx.fillStyle = '#000000'; 601 | 602 | // First half of states on one line, second half below it 603 | if (i < STATES.length / 2) { 604 | currentX = i * boxSize + startPosX; 605 | currentY = startPosY; 606 | } else { 607 | currentX = (i - STATES.length / 2) * boxSize + startPosX; 608 | currentY = startPosY + boxSize; 609 | } 610 | 611 | // Draw box 612 | if (STATES[i] == highlight && highlight != selected) { 613 | ctx.strokeStyle = '#ff0000'; 614 | ctx.strokeRect(currentX + 1, currentY + 1, boxSize - 2, boxSize - 2); 615 | } 616 | 617 | if (STATES[i] == selected) { 618 | ctx.fillRect(currentX, currentY, boxSize, boxSize); 619 | } 620 | 621 | ctx.strokeRect(currentX, currentY, boxSize, boxSize); 622 | 623 | // Draw state abbreviation 624 | if (STATES[i] == selected) { 625 | ctx.fillStyle = '#fefefe'; 626 | } else if (STATES[i] == highlight) { 627 | ctx.fillStyle = '#ff0000'; 628 | } else { 629 | ctx.fillStyle = '#000000'; 630 | } 631 | 632 | ctx.fillText( 633 | STATES[i], 634 | currentX + boxSize / 2, 635 | currentY + boxSize * 0.75 636 | ); 637 | 638 | // Store coordinates for click events 639 | if (isInit) { 640 | hitBox = { 641 | state: STATES[i], 642 | xmin: currentX, 643 | xmax: currentX + boxSize, 644 | ymin: currentY, 645 | ymax: currentY + boxSize, 646 | }; 647 | stateHitBoxes[stateHitBoxes.length] = hitBox; 648 | } 649 | } 650 | } 651 | 652 | /** 653 | * Draw single frame of the state numbers updating. 654 | */ 655 | function drawStateNumbers() { 656 | var fontSize = 14; 657 | var paddingY = 10; 658 | var startY = 354; 659 | var labelX = 36; 660 | var numX = 200 + fontSize * 5; /*eh, the 5 is arbitrary*/ 661 | var totalNum; 662 | var suicideNum; 663 | var accidentNum; 664 | 665 | ctx.clearRect(labelX, startY, 624, 200); 666 | 667 | ctx.font = '14px Helvetica'; 668 | ctx.fillStyle = blackColor; 669 | ctx.textAlign = 'left'; 670 | 671 | var suicideLineY = startY + fontSize; 672 | var accidentLineY = startY + fontSize + paddingY + fontSize; 673 | var totalLineY = startY + (fontSize + paddingY) * 2 + fontSize; 674 | 675 | // Draw total # label 676 | ctx.fillText('Suicide:', labelX, suicideLineY); 677 | 678 | // Draw suicide # label 679 | ctx.fillText('Unintentional:', labelX, accidentLineY); 680 | 681 | // Draw accident # label 682 | ctx.fillText('All firearm deaths:', labelX, totalLineY); 683 | 684 | // Numbers align right 685 | ctx.textAlign = 'right'; 686 | 687 | if (drawNumsAnimTime > drawNumsTotalAnimTime) { 688 | // Just make sure the final #s are actually drawn 689 | ctx.fillStyle = blueColor; 690 | ctx.fillText( 691 | STATE_DATA[this.state].suicide_raw + ' persons', 692 | numX, 693 | suicideLineY 694 | ); 695 | 696 | ctx.fillStyle = yellowColor; 697 | ctx.fillText( 698 | STATE_DATA[this.state].accident_raw + ' persons', 699 | numX, 700 | accidentLineY 701 | ); 702 | 703 | ctx.fillStyle = blackColor; 704 | ctx.fillText( 705 | STATE_DATA[this.state].total_raw + ' persons', 706 | numX, 707 | totalLineY 708 | ); 709 | 710 | // Done animating 711 | window.clearInterval(drawNumsIntervalId); 712 | drawNumsIntervalId = 0; 713 | } else { 714 | drawNumsAnimTime += drawNumsAnimInterval; 715 | 716 | suicideNum = Math.floor( 717 | STATE_DATA[this.state].suicide_raw * 718 | (drawNumsAnimTime / drawNumsTotalAnimTime) 719 | ); 720 | ctx.fillStyle = blueColor; 721 | ctx.fillText(suicideNum + ' persons', numX, suicideLineY); 722 | 723 | accidentNum = Math.floor( 724 | STATE_DATA[this.state].accident_raw * 725 | (drawNumsAnimTime / drawNumsTotalAnimTime) 726 | ); 727 | ctx.fillStyle = yellowColor; 728 | ctx.fillText(accidentNum + ' persons', numX, accidentLineY); 729 | 730 | totalNum = Math.floor( 731 | STATE_DATA[this.state].total_raw * 732 | (drawNumsAnimTime / drawNumsTotalAnimTime) 733 | ); 734 | ctx.fillStyle = blackColor; 735 | ctx.fillText(totalNum + ' persons', numX, totalLineY); 736 | } 737 | } 738 | 739 | /** 740 | * Initial draw of the peoples. 741 | */ 742 | // @todo More properly handle these two vars 743 | var currPerson; 744 | var intervalId; 745 | function updatePersons(state) { 746 | // Reset person counter 747 | currPerson = 0; 748 | 749 | // Clear any persons drawn previously 750 | ctx.clearRect(24, 100, 448, 220); 751 | 752 | intervalId = window.setInterval(drawPerson.bind({ state: state }), 20); 753 | } 754 | 755 | /** 756 | * Function to be called on intervals to draw the person images. 757 | * Requires that an object with a `state` property be binded to the function call. 758 | */ 759 | function drawPerson() { 760 | var i; 761 | var currentX; 762 | var currentY; 763 | var row; 764 | var xAdjust; 765 | var haltDrawing = false; 766 | var startPosX = 24; 767 | var startPosY = 100; 768 | var imgWidth = 20; 769 | var imgHeight = 40; 770 | var paddingX = 2; 771 | var paddingY = 4; 772 | var totalPersons = 100; 773 | var onefifth = totalPersons / 5; 774 | var numSuicideAccident; 775 | var img; 776 | var imgPerson = document.getElementById('img-person'); 777 | var imgSuicide = document.getElementById('img-person-suicide'); 778 | var imgAccident = document.getElementById('img-person-accident'); 779 | 780 | // Ensure `state` property exists in the `this` 781 | if (typeof this.state !== 'string') { 782 | console.log('playable1.drawPerson() called without a `state` property'); 783 | haltDrawing = true; 784 | } 785 | // Ensure we actually have data for this `state` 786 | else if (typeof STATE_DATA[this.state] === 'undefined') { 787 | console.log('playable1.drawPerson() has no data for ' + this.state); 788 | haltDrawing = true; 789 | } 790 | // Stop drawing once we hit the total 791 | else { 792 | haltDrawing = currPerson >= STATE_DATA[this.state].total_normalized; 793 | } 794 | 795 | if (haltDrawing) { 796 | window.clearInterval(intervalId); 797 | intervalId = undefined; 798 | return; 799 | } 800 | 801 | i = currPerson; 802 | 803 | // Determine row 804 | if (i < onefifth) { 805 | row = 0; 806 | } else if (i < onefifth * 2) { 807 | row = 1; 808 | } else if (i < onefifth * 3) { 809 | row = 2; 810 | } else if (i < onefifth * 4) { 811 | row = 3; 812 | } else { 813 | row = 4; 814 | } 815 | 816 | // Determine image source 817 | img = imgPerson; 818 | if (i < STATE_DATA[this.state].suicide_normalized) { 819 | img = imgSuicide; 820 | } else if ( 821 | i >= STATE_DATA[this.state].suicide_normalized && 822 | i < 823 | STATE_DATA[this.state].suicide_normalized + 824 | STATE_DATA[this.state].accident_normalized 825 | ) { 826 | img = imgAccident; 827 | } 828 | 829 | xAdjust = onefifth * row; 830 | // + 2px for padding in between persons 831 | currentX = (i - xAdjust) * (imgWidth + paddingX) + startPosX; 832 | currentY = startPosY + row * (imgHeight + paddingY); 833 | 834 | ctx.drawImage(img, currentX, currentY, imgWidth, imgHeight); 835 | currPerson++; 836 | } 837 | 838 | /** 839 | * Listener for click events on the canvas. 840 | */ 841 | function onClick(event) { 842 | // Stop any current draw in progress 843 | if (typeof intervalId !== 'undefined') { 844 | window.clearInterval(intervalId); 845 | intervalId = undefined; 846 | } 847 | 848 | var hit = checkHit(event.offsetX, event.offsetY); 849 | if (hit) { 850 | updatePersons(hit); 851 | drawStates(hit, hit, false); 852 | } 853 | } 854 | 855 | /** 856 | * Listener for mouse movements on the canvas. 857 | */ 858 | function onMouseMove(event) { 859 | // Highlight states on hover over 860 | var hit = checkHit(event.offsetX, event.offsetY); 861 | if (hit) { 862 | drawStates(selectedState, hit, false); 863 | canvas.style.cursor = 'pointer'; 864 | } else if (hasStateHighlight) { 865 | drawStates(selectedState, undefined, false); 866 | canvas.style.cursor = 'default'; 867 | } 868 | } 869 | 870 | /** 871 | * Check if a state box was hit on a given set of coordinates 872 | * 873 | * @param x x position of the click 874 | * @param y y position of the click 875 | * @returns The state if a hit is found. Otherwise, false. 876 | */ 877 | function checkHit(x, y) { 878 | var i; 879 | 880 | for (i = 0; i < stateHitBoxes.length; i++) { 881 | if ( 882 | x > stateHitBoxes[i].xmin && 883 | x < stateHitBoxes[i].xmax && 884 | y > stateHitBoxes[i].ymin && 885 | y < stateHitBoxes[i].ymax 886 | ) { 887 | return stateHitBoxes[i].state; 888 | } 889 | } 890 | 891 | return false; 892 | } 893 | 894 | return { 895 | hasStarted: hasStarted, 896 | init: init, 897 | run: run, 898 | }; 899 | })(); 900 | 901 | playable1.init(); 902 | -------------------------------------------------------------------------------- /js/playable2.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Controls the canvas canvas-2. 3 | * 4 | * The intent of this canvas is to observe the effectiveness of gun locks in a 5 | * simulation of attempted suicides by firearm. 6 | * 7 | */ 8 | 9 | var playable2; 10 | var canvas2 = document.getElementById('canvas-2'); 11 | 12 | playable2 = (function () { 13 | var CANVAS_HEIGHT = canvas2.height; 14 | var CANVAS_WIDTH = canvas2.width; 15 | var CANVAS_GRID_SIZE = 12; 16 | 17 | // Colors 18 | var COLOR_BOUNDARIES = '#000000'; 19 | var COLOR_PERSONS_DEFAULT = '#777777'; 20 | var COLOR_PERSONS_W_LOCK = '#2c2863'; 21 | var COLOR_GUN_LOCK = '#07a1c5'; 22 | var COLOR_ATTEMPT = '#ef5f48'; 23 | var COLOR_SAVED = '#88ca41'; 24 | var COLOR_UNSUCCESSFUL = '#faa821'; 25 | var COLOR_FATAL = '#3a0031'; 26 | var COLOR_CROSSHAIRS = '#cc3333'; 27 | var COLOR_GRID = '#cccccc'; 28 | var COLOR_BUTTON_HOVER = '#cccccc'; 29 | var COLOR_BUTTON_OFF = '#333333'; 30 | var COLOR_BUTTON_ON = '#777777'; 31 | var COLOR_BUTTON_TEXT = '#ffffff'; 32 | var COLOR_SIM_DEFAULT = '#ff0000'; 33 | 34 | // Canvas and context 35 | var canvas; 36 | var ctx; 37 | 38 | // Boolean. True if this has already run init(). 39 | var hasStarted = false; 40 | 41 | // Boolean. True if started, but paused. 42 | var isPaused = false; 43 | 44 | // Current mouse pointer positions 45 | var mouseX; 46 | var mouseY; 47 | 48 | // Current slider variable values 49 | var startupInProgress = false; 50 | var startupDuration = 500; 51 | var startupTimeLeft = startupDuration; 52 | var startupLastTimeChecked = 0; 53 | var withLockStartVal = 50; 54 | var lockEffectStartVal = 68; 55 | var vWithLock = 0; 56 | var vLockEffect = 0; 57 | 58 | // Boolean. True if dragging one of the sliders. 59 | var isDraggingSlider1 = false; 60 | var isDraggingSlider2 = false; 61 | 62 | // Simulation system 63 | var simSystem; 64 | var simSize = 100; 65 | 66 | // Results from the simulation 67 | var vSimSaved = 0; 68 | var vSimUnsuccessful = 0; 69 | var vSimFatal = 0; 70 | 71 | // Event queue 72 | var eventQueue = []; 73 | 74 | // So much random code everywhere. oh welllll 75 | var hoverOnStartButton = false; 76 | 77 | /** 78 | * Utility console log that only prints when debug is on. 79 | */ 80 | function debugLog(msg) { 81 | if (GLOBAL_SHOW_DEBUG) { 82 | console.log(msg); 83 | } 84 | } 85 | 86 | /** 87 | * Setup initial view of the canvas 88 | */ 89 | function init() { 90 | canvas = canvas2; 91 | ctx = canvas.getContext('2d'); 92 | 93 | simSystem = new SimSystem(); 94 | 95 | canvas.addEventListener('click', onClick); 96 | canvas.addEventListener('mousemove', onMouseMove); 97 | canvas.addEventListener('mousedown', onMouseDown); 98 | canvas.addEventListener('mouseup', onMouseUp); 99 | 100 | debugDrawGrid(); 101 | 102 | window.requestAnimationFrame(draw); 103 | } 104 | 105 | /** 106 | * ¯\_(ツ)_/¯ 107 | */ 108 | function start() { 109 | startupInProgress = true; 110 | hasStarted = true; 111 | } 112 | 113 | function pause() { 114 | isPaused = true; 115 | } 116 | 117 | function resume() { 118 | isPaused = false; 119 | draw(); 120 | } 121 | 122 | /** 123 | * click event listener 124 | */ 125 | function onClick(event) { 126 | eventQueue.push(event); 127 | debugLog('click'); 128 | } 129 | 130 | /** 131 | * mousemove event listener 132 | */ 133 | function onMouseMove(event) { 134 | mouseX = event.offsetX; 135 | mouseY = event.offsetY; 136 | } 137 | 138 | /** 139 | * mousedown event listener 140 | */ 141 | function onMouseDown(event) { 142 | eventQueue.push(event); 143 | debugLog('mousedown'); 144 | } 145 | 146 | /** 147 | * mouseup event listener 148 | */ 149 | function onMouseUp(event) { 150 | eventQueue.push(event); 151 | debugLog('mouseup'); 152 | } 153 | 154 | /** 155 | * For debug. Draw grid. 156 | */ 157 | function debugDrawGrid() { 158 | var i; 159 | var xpos; 160 | var ypos; 161 | 162 | ctx.strokeStyle = COLOR_GRID; 163 | 164 | // Draw vertical lines 165 | i = 0; 166 | while (i * CANVAS_GRID_SIZE <= CANVAS_WIDTH) { 167 | xpos = i * CANVAS_GRID_SIZE; 168 | 169 | ctx.beginPath(); 170 | ctx.moveTo(xpos, 0); 171 | ctx.lineTo(xpos, CANVAS_HEIGHT); 172 | ctx.stroke(); 173 | 174 | i++; 175 | } 176 | 177 | // Draw horizontal lines 178 | i = 0; 179 | while (i * CANVAS_GRID_SIZE <= CANVAS_HEIGHT) { 180 | ypos = i * CANVAS_GRID_SIZE; 181 | 182 | ctx.beginPath(); 183 | ctx.moveTo(0, ypos); 184 | ctx.lineTo(CANVAS_WIDTH, ypos); 185 | ctx.stroke(); 186 | 187 | i++; 188 | } 189 | } 190 | 191 | /** 192 | * Debug. Draw crosshairs on the pointer position. 193 | */ 194 | function debugDrawPointer() { 195 | ctx.fillStyle = COLOR_CROSSHAIRS; 196 | ctx.strokeStyle = COLOR_CROSSHAIRS; 197 | 198 | ctx.beginPath(); 199 | 200 | // Vertical line 201 | ctx.moveTo(mouseX, 0); 202 | ctx.lineTo(mouseX, CANVAS_HEIGHT); 203 | 204 | // Horizontal line 205 | ctx.moveTo(0, mouseY); 206 | ctx.lineTo(CANVAS_WIDTH, mouseY); 207 | 208 | ctx.stroke(); 209 | 210 | // Draw positions 211 | ctx.font = '12px Helvetica'; 212 | ctx.textAlign = 'left'; 213 | ctx.textBaseline = 'top'; 214 | ctx.fillText('x: ' + mouseX, CANVAS_WIDTH - 40, 12); 215 | ctx.fillText('y: ' + mouseY, CANVAS_WIDTH - 40, 24); 216 | } 217 | 218 | /****************** 219 | * Main draw loop * 220 | ******************/ 221 | function draw() { 222 | var time; 223 | 224 | if (isPaused === true) { 225 | return; 226 | } 227 | 228 | ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); 229 | 230 | if (GLOBAL_SHOW_DEBUG) { 231 | debugDrawGrid(); 232 | debugDrawPointer(); 233 | } 234 | 235 | drawStartButton(); 236 | drawSliders(); 237 | drawSectionBounds(); 238 | 239 | if (simSystem && simSystem.isRunning()) { 240 | simSystem.run(); 241 | } 242 | 243 | // If nothing's consumed an event in the queue, then clear it all. 244 | // um... is this the right thing to do? Could we run into some weird 245 | // race condition where a click happens but nothing in this loop 246 | // actually gets a chance to see it? 247 | eventQueue = []; 248 | 249 | window.requestAnimationFrame(draw); 250 | } 251 | 252 | /** 253 | * Draw the "start simulation" button 254 | */ 255 | function drawStartButton() { 256 | var i; 257 | var isRunning; 258 | var text; 259 | var isClicked = false; 260 | var boxWidth = 200; 261 | var boxHeight = 36; 262 | var boxMargin = 12; 263 | 264 | var xPosBox = (CANVAS_WIDTH - boxWidth) / 2; 265 | var yPosBox = boxMargin; 266 | 267 | function inButtonBounds(x, y) { 268 | return ( 269 | mouseX >= xPosBox && 270 | mouseX <= xPosBox + boxWidth && 271 | mouseY >= yPosBox && 272 | mouseY <= yPosBox + boxHeight 273 | ); 274 | } 275 | 276 | // Check event queue for any clicks 277 | i = eventQueue.length; 278 | while (i--) { 279 | if (eventQueue[i].type == 'click') { 280 | if (inButtonBounds(eventQueue[i].offsetX, eventQueue[i].offsetY)) { 281 | eventQueue.splice(i, 1); 282 | isClicked = true; 283 | } 284 | } 285 | } 286 | 287 | // If there was a click, switch the system on or off 288 | if (isClicked) { 289 | if (simSystem.isRunning()) { 290 | if (simSystem.isDone()) { 291 | simSystem.start(); 292 | } else { 293 | simSystem.stop(); 294 | } 295 | } else { 296 | simSystem.start(); 297 | } 298 | } 299 | 300 | if (simSystem.isRunning()) { 301 | if (simSystem.isDone()) { 302 | ctx.fillStyle = COLOR_BUTTON_OFF; 303 | text = 'RESTART'; 304 | } else { 305 | ctx.fillStyle = COLOR_BUTTON_ON; 306 | text = 'STOP'; 307 | } 308 | } else { 309 | ctx.fillStyle = COLOR_BUTTON_OFF; 310 | text = 'START'; 311 | } 312 | 313 | // Is mouse currently hovering over 314 | if (inButtonBounds(mouseX, mouseY)) { 315 | ctx.fillStyle = COLOR_BUTTON_HOVER; 316 | hoverOnStartButton = true; 317 | canvas.style.cursor = 'pointer'; 318 | } else { 319 | hoverOnStartButton = false; 320 | canvas.style.cursor = 'default'; 321 | } 322 | 323 | // Draw the button 324 | ctx.fillRect(xPosBox, yPosBox, boxWidth, boxHeight); 325 | 326 | // Draw text 327 | ctx.font = '16px Helvetica'; 328 | ctx.fillStyle = COLOR_BUTTON_TEXT; 329 | ctx.textAlign = 'center'; 330 | ctx.textBaseline = 'middle'; 331 | ctx.fillText(text, CANVAS_WIDTH / 2, boxMargin + boxHeight / 2); 332 | 333 | // Draw progress label 334 | var progress = 335 | vSimSaved + vSimUnsuccessful + vSimFatal + ' / ' + simSize + ' persons'; 336 | ctx.font = '12px Helvetica'; 337 | ctx.fillStyle = '#000000'; 338 | ctx.textAlign = 'center'; 339 | ctx.textBaseline = 'middle'; 340 | ctx.fillText(progress, CANVAS_WIDTH / 2, boxMargin + boxHeight + 10); 341 | } 342 | 343 | /** 344 | * Draw variable sliders. 345 | */ 346 | function drawSliders() { 347 | var i; 348 | var lineSize = 3; 349 | var lineWidth = 156; 350 | var lineEdgeHeight = 24; 351 | var yTop = 86; 352 | var leftMargin; 353 | var xSlider1; 354 | var xSlider2; 355 | var xSliderGap = 72; 356 | var sliderWidth = 12; 357 | var sliderHeight = 24; 358 | var lockSliders = simSystem.isRunning() && !simSystem.isDone(); 359 | 360 | if (startupInProgress) { 361 | var startupPctProgress; 362 | 363 | time = new Date().getTime(); 364 | if (startupLastTimeChecked > 0) { 365 | startupTimeLeft -= time - startupLastTimeChecked; 366 | } 367 | 368 | startupLastTimeChecked = time; 369 | 370 | if (startupTimeLeft <= 0) { 371 | startupInProgress = false; 372 | vWithLock = withLockStartVal; 373 | vLockEffect = lockEffectStartVal; 374 | } else { 375 | startupPctProgress = 376 | (startupDuration - startupTimeLeft) / startupDuration; 377 | vWithLock = Math.floor(startupPctProgress * withLockStartVal); 378 | vLockEffect = Math.floor(startupPctProgress * lockEffectStartVal); 379 | } 380 | } 381 | 382 | ctx.fillStyle = lockSliders ? COLOR_BUTTON_ON : '#000'; 383 | ctx.font = '14px Helvetica'; 384 | ctx.textAlign = 'center'; 385 | ctx.textBaseline = 'middle'; 386 | 387 | // Slider 1 - % of people with gun locks 388 | leftMargin = (CANVAS_WIDTH - lineWidth * 2 - xSliderGap) / 2; 389 | xSlider1 = leftMargin; 390 | // horizontal line 391 | ctx.fillRect(xSlider1, yTop + 12 - 1, lineWidth, lineSize); 392 | // vertical lines 393 | ctx.fillRect(xSlider1, yTop, lineSize, lineEdgeHeight); 394 | ctx.fillRect(xSlider1 + lineWidth, yTop, lineSize, lineEdgeHeight); 395 | // text label 396 | ctx.fillText( 397 | 'Gun lock usage', 398 | xSlider1 + lineWidth / 2, 399 | yTop + lineEdgeHeight + 12 400 | ); 401 | 402 | // Slider 2 - % effectiveness of gun locks 403 | xSlider2 = leftMargin + lineWidth + xSliderGap; 404 | // horizontal line 405 | ctx.fillRect(xSlider2, yTop + 12 - 1, lineWidth, lineSize); 406 | // vertical lines 407 | ctx.fillRect(xSlider2, yTop, lineSize, lineEdgeHeight); 408 | ctx.fillRect(xSlider2 + lineWidth, yTop, lineSize, lineEdgeHeight); 409 | // text label 410 | ctx.fillText( 411 | 'Chance at preventing attempt', 412 | xSlider2 + lineWidth / 2, 413 | yTop + lineEdgeHeight + 12 414 | ); 415 | 416 | // Slider positions 417 | function calcSliderPos(xSliderPos, val) { 418 | return xSliderPos + (lineWidth * (val / 100) - sliderWidth / 2); 419 | } 420 | 421 | var s1Pos = calcSliderPos(xSlider1, vWithLock); 422 | var s2Pos = calcSliderPos(xSlider2, vLockEffect); 423 | 424 | function isCursorOnSlider(sliderPos, x, y) { 425 | return ( 426 | x >= sliderPos && 427 | x <= sliderPos + sliderWidth && 428 | y >= yTop && 429 | y <= yTop + sliderHeight 430 | ); 431 | } 432 | 433 | function isCursorOnLine(lx, ly, lw, lh, cx, cy) { 434 | return cx >= lx && cx <= lx + lw && cy >= ly && cy <= ly + lh; 435 | } 436 | 437 | // -2 and +4 to give a little more vertical buffer to clicky clicky 438 | var isOnLine1 = isCursorOnLine( 439 | xSlider1, 440 | yTop + 12 - 1 - 2, 441 | lineWidth, 442 | lineSize + 4, 443 | mouseX, 444 | mouseY 445 | ); 446 | var isOnLine2 = isCursorOnLine( 447 | xSlider2, 448 | yTop + 12 - 1 - 2, 449 | lineWidth, 450 | lineSize + 4, 451 | mouseX, 452 | mouseY 453 | ); 454 | var jumpLine1Val = false; 455 | var jumpLine2Val = false; 456 | 457 | // Did it click one of the sliders? 458 | i = eventQueue.length; 459 | while (i--) { 460 | if (eventQueue[i].type == 'mouseup') { 461 | // If we're dragging a slider, mouseup should end it 462 | if (isDraggingSlider1 || isDraggingSlider2) { 463 | isDraggingSlider1 = false; 464 | isDraggingSlider2 = false; 465 | eventQueue.splice(i, 1); 466 | } else if (isOnLine1) { 467 | jumpLine1Val = true; 468 | } else if (isOnLine2) { 469 | jumpLine2Val = true; 470 | } 471 | } 472 | // Otherwise we'll look for a click 473 | else if (eventQueue[i].type == 'mousedown') { 474 | // Is the click on a slider 1 475 | if ( 476 | isCursorOnSlider(s1Pos, eventQueue[i].offsetX, eventQueue[i].offsetY) 477 | ) { 478 | isDraggingSlider1 = true; 479 | eventQueue.splice(i, 1); 480 | } else if ( 481 | isCursorOnSlider(s2Pos, eventQueue[i].offsetX, eventQueue[i].offsetY) 482 | ) { 483 | isDraggingSlider2 = true; 484 | eventQueue.splice(i, 1); 485 | } 486 | } 487 | } 488 | 489 | if (!lockSliders) { 490 | // Change style of cursor if hovering over slider 491 | if ( 492 | isDraggingSlider1 || 493 | isDraggingSlider2 || 494 | isCursorOnSlider(s1Pos, mouseX, mouseY) || 495 | isCursorOnSlider(s2Pos, mouseX, mouseY) 496 | ) { 497 | canvas.style.cursor = 'ew-resize'; 498 | } 499 | // -2 and +4 to give a little more vertical buffer to clicky clicky 500 | else if (isOnLine1 || isOnLine2) { 501 | canvas.style.cursor = 'pointer'; 502 | } 503 | // Pointer styled as default as long as it's not also over the start button ... go see the drawStartButton code... sorryyyyyy 504 | else if (!hoverOnStartButton) { 505 | canvas.style.cursor = 'default'; 506 | } 507 | } 508 | 509 | // If we're currently dragging, update the positions of the slider 510 | if (!lockSliders && (isDraggingSlider1 || jumpLine1Val)) { 511 | vWithLock = Math.round(((mouseX - xSlider1) / lineWidth) * 100); 512 | if (vWithLock < 0) { 513 | vWithLock = 0; 514 | } else if (vWithLock > 100) { 515 | vWithLock = 100; 516 | } 517 | s1Pos = calcSliderPos(xSlider1, vWithLock); 518 | } else if (!lockSliders && (isDraggingSlider2 || jumpLine2Val)) { 519 | vLockEffect = Math.round(((mouseX - xSlider2) / lineWidth) * 100); 520 | if (vLockEffect < 0) { 521 | vLockEffect = 0; 522 | } else if (vLockEffect > 100) { 523 | vLockEffect = 100; 524 | } 525 | s2Pos = calcSliderPos(xSlider2, vLockEffect); 526 | } 527 | 528 | ctx.textBaseline = 'bottom'; 529 | // draw Slider 1 530 | ctx.fillStyle = COLOR_PERSONS_W_LOCK; 531 | ctx.fillRect(s1Pos, yTop, sliderWidth, sliderHeight); 532 | ctx.fillText(vWithLock + '%', s1Pos + sliderWidth / 2, yTop - 4); 533 | 534 | // draw Slider 2 535 | ctx.fillStyle = COLOR_GUN_LOCK; 536 | ctx.fillRect(s2Pos, yTop, sliderWidth, sliderHeight); 537 | ctx.fillText(vLockEffect + '%', s2Pos + sliderWidth / 2, yTop - 4); 538 | } 539 | 540 | /** 541 | * Draws the bounding areas for all the sections plus updates their 542 | * associated labels. 543 | */ 544 | function drawSectionBounds() { 545 | var i; 546 | var lineWidth = 3; 547 | var leftMargin = 12; 548 | var marginBwSections = 36; 549 | var area1Radius = 90; 550 | var area1X = 102; // basically, leftMargin + area1Radius 551 | var area1Y = 310; 552 | 553 | ctx.fillStyle = COLOR_BOUNDARIES; 554 | ctx.strokeStyle = COLOR_BOUNDARIES; 555 | 556 | // Area 1 - circle. Start persons deci 557 | ctx.beginPath(); 558 | for (i = 0; i < lineWidth; i++) { 559 | ctx.ellipse( 560 | area1X, 561 | area1Y, 562 | area1Radius - 1 + i, 563 | area1Radius - 1 + i, 564 | 0 /*rotation*/, 565 | 0 /*start angle*/, 566 | 2 * Math.PI /*end angle*/ 567 | ); 568 | } 569 | ctx.stroke(); 570 | // Label 571 | ctx.font = '14px Helvetica'; 572 | ctx.textAlign = 'center'; 573 | ctx.textBaseline = 'bottom'; 574 | ctx.fillText('Decision is made', area1X, area1Y - area1Radius - 20); 575 | ctx.fillText('to attempt suicide', area1X, area1Y - area1Radius - 4); 576 | 577 | // Area 2 - Gun lock 578 | var area2Width = 132; 579 | var area2Height = 72; 580 | var area2X = area1X + area1Radius + marginBwSections; 581 | var area2Y = 208; 582 | // Boundary box 583 | ctx.strokeStyle = COLOR_BOUNDARIES; 584 | ctx.strokeRect(area2X, area2Y, area2Width, area2Height); 585 | ctx.strokeRect(area2X - 1, area2Y - 1, area2Width + 2, area2Height + 2); 586 | ctx.strokeRect(area2X + 1, area2Y + 1, area2Width - 2, area2Height - 2); 587 | // Colored side 588 | ctx.strokeStyle = COLOR_GUN_LOCK; 589 | for (i = 0; i < 3; i++) { 590 | ctx.beginPath(); 591 | ctx.moveTo(area2X - 1 + i, area2Y - 2); 592 | ctx.lineTo(area2X - 1 + i, area2Y + area2Height + 2); 593 | ctx.stroke(); 594 | } 595 | // Label 596 | ctx.fillStyle = '#000'; 597 | ctx.font = '14px Helvetica'; 598 | ctx.textAlign = 'center'; 599 | ctx.textBaseline = 'bottom'; 600 | ctx.fillText( 601 | 'Need to bypass gun lock', 602 | area2X + area2Width / 2, 603 | area2Y - 20 604 | ); 605 | 606 | ctx.fillStyle = '#a0a0a0'; 607 | ctx.fillText( 608 | 'Chance at prevention: ', 609 | area2X - 4 + area2Width / 2, 610 | area2Y - 4 611 | ); 612 | 613 | ctx.fillStyle = COLOR_GUN_LOCK; 614 | ctx.textAlign = 'left'; 615 | ctx.fillText(vLockEffect + '%', area2X + area2Width - 4, area2Y - 4); 616 | 617 | // Area 3 - Attempt with firearm 618 | var area3Width = 132; 619 | var area3Height = 72; 620 | var area3X = area2X; 621 | var area3Y = 332; 622 | // Boundary box 623 | ctx.strokeStyle = COLOR_BOUNDARIES; 624 | ctx.strokeRect(area3X, area3Y, area3Width, area3Height); 625 | ctx.strokeRect(area3X - 1, area3Y - 1, area3Width + 2, area3Height + 2); 626 | ctx.strokeRect(area3X + 1, area3Y + 1, area3Width - 2, area3Height - 2); 627 | // Colored side 628 | ctx.strokeStyle = COLOR_ATTEMPT; 629 | for (i = 0; i < 3; i++) { 630 | ctx.beginPath(); 631 | ctx.moveTo(area3X - 1 + i, area3Y - 2); 632 | ctx.lineTo(area3X - 1 + i, area3Y + area3Height + 2); 633 | ctx.stroke(); 634 | } 635 | // Label 636 | ctx.fillStyle = '#000'; 637 | ctx.font = '14px Helvetica'; 638 | ctx.textAlign = 'center'; 639 | ctx.textBaseline = 'bottom'; 640 | ctx.fillText('Attempt at suicide', area3X + area3Width / 2, area3Y - 20); 641 | 642 | ctx.fillStyle = '#a0a0a0'; 643 | ctx.fillText( 644 | 'Chance of fatality: 85%', 645 | area3X + area3Width / 2, 646 | area3Y - 4 647 | ); 648 | 649 | // Area 4 - Saved 650 | var area4Width = 156; 651 | var area4Height = 70; 652 | var area4X = area2X + area2Width + marginBwSections; 653 | var area4Y = 196; 654 | // Boundary box 655 | ctx.strokeStyle = COLOR_BOUNDARIES; 656 | ctx.strokeRect(area4X, area4Y, area4Width, area4Height); 657 | ctx.strokeRect(area4X - 1, area4Y - 1, area4Width + 2, area4Height + 2); 658 | ctx.strokeRect(area4X + 1, area4Y + 1, area4Width - 2, area4Height - 2); 659 | // Colored side 660 | ctx.strokeStyle = COLOR_SAVED; 661 | for (i = 0; i < 3; i++) { 662 | ctx.beginPath(); 663 | ctx.moveTo(area4X - 1 + i, area4Y - 2); 664 | ctx.lineTo(area4X - 1 + i, area4Y + area4Height + 2); 665 | ctx.stroke(); 666 | } 667 | // Label 668 | ctx.fillStyle = COLOR_SAVED; 669 | ctx.textAlign = 'center'; 670 | ctx.font = 'bold 20px Helvetica'; 671 | ctx.textBaseline = 'bottom'; 672 | ctx.fillText(vSimSaved, area4X + area4Width + 52, area4Y + area4Height / 2); 673 | 674 | ctx.font = '14px Helvetica'; 675 | ctx.fillStyle = '#000'; 676 | ctx.textBaseline = 'top'; 677 | ctx.fillText( 678 | 'Lives Saved', 679 | area4X + area4Width + 52, 680 | area4Y + area4Height / 2 681 | ); 682 | 683 | // Area 5 - Unsuccessful 684 | var area5Width = 156; 685 | var area5Height = 70; 686 | var area5X = area4X; 687 | var area5Y = area4Y + area4Height + 5; 688 | // Boundary box 689 | ctx.strokeStyle = COLOR_BOUNDARIES; 690 | ctx.strokeRect(area5X, area5Y, area5Width, area5Height); 691 | ctx.strokeRect(area5X - 1, area5Y - 1, area5Width + 2, area5Height + 2); 692 | ctx.strokeRect(area5X + 1, area5Y + 1, area5Width - 2, area5Height - 2); 693 | // Colored side 694 | ctx.strokeStyle = COLOR_UNSUCCESSFUL; 695 | for (i = 0; i < 3; i++) { 696 | ctx.beginPath(); 697 | ctx.moveTo(area5X - 1 + i, area5Y - 2); 698 | ctx.lineTo(area5X - 1 + i, area5Y + area5Height + 2); 699 | ctx.stroke(); 700 | } 701 | // Label 702 | ctx.fillStyle = COLOR_UNSUCCESSFUL; 703 | ctx.textAlign = 'center'; 704 | ctx.font = 'bold 20px Helvetica'; 705 | ctx.textBaseline = 'bottom'; 706 | ctx.fillText( 707 | vSimUnsuccessful, 708 | area5X + area5Width + 52, 709 | area5Y + area5Height / 2 710 | ); 711 | 712 | ctx.font = '14px Helvetica'; 713 | ctx.fillStyle = '#000'; 714 | ctx.textBaseline = 'top'; 715 | ctx.fillText( 716 | 'Non-Fatal', 717 | area5X + area5Width + 52, 718 | area5Y + area5Height / 2 719 | ); 720 | ctx.fillText( 721 | 'Results', 722 | area5X + area5Width + 52, 723 | area5Y + area5Height / 2 + 14 724 | ); 725 | 726 | // Area 6 - Fatal 727 | var area6Width = 156; 728 | var area6Height = 70; 729 | var area6X = area4X; 730 | var area6Y = area5Y + area5Height + 5; 731 | // Boundary box 732 | ctx.strokeStyle = COLOR_BOUNDARIES; 733 | ctx.strokeRect(area6X, area6Y, area6Width, area6Height); 734 | ctx.strokeRect(area6X - 1, area6Y - 1, area6Width + 2, area6Height + 2); 735 | ctx.strokeRect(area6X + 1, area6Y + 1, area6Width - 2, area6Height - 2); 736 | // Colored side 737 | ctx.strokeStyle = COLOR_FATAL; 738 | for (i = 0; i < 3; i++) { 739 | ctx.beginPath(); 740 | ctx.moveTo(area6X - 1 + i, area6Y - 2); 741 | ctx.lineTo(area6X - 1 + i, area6Y + area6Height + 2); 742 | ctx.stroke(); 743 | } 744 | // Label 745 | ctx.fillStyle = COLOR_FATAL; 746 | ctx.textAlign = 'center'; 747 | ctx.font = 'bold 20px Helvetica'; 748 | ctx.textBaseline = 'bottom'; 749 | ctx.fillText(vSimFatal, area6X + area6Width + 52, area6Y + area6Height / 2); 750 | 751 | ctx.font = '14px Helvetica'; 752 | ctx.fillStyle = '#000'; 753 | ctx.textBaseline = 'top'; 754 | ctx.fillText('Fatal', area6X + area6Width + 52, area6Y + area6Height / 2); 755 | ctx.fillText( 756 | 'Results', 757 | area6X + area6Width + 52, 758 | area6Y + area6Height / 2 + 14 759 | ); 760 | } 761 | 762 | /** 763 | * Manages the simulated people going through the simulation. 764 | */ 765 | function SimSystem() { 766 | // Arrays of persons in diff states 767 | var pending = []; 768 | var active = []; 769 | var done = []; 770 | 771 | // System is currently running 772 | this.running = false; 773 | 774 | // Define the spawn area 775 | var spawnArea = { x: 102, y: 310, r: 90 }; 776 | 777 | // Timing things 778 | var SPAWN_INTERVAL = 200; 779 | var lastTimeUpdated = 0; 780 | var deltaTime = 0; 781 | var timeUntilNextSpawn = 0; 782 | 783 | function start() { 784 | var i; 785 | 786 | // Reset any persons still on the canvas 787 | pending = []; 788 | active = []; 789 | done = []; 790 | 791 | for (i = 0; i < simSize; i++) { 792 | pending[pending.length] = new Person(); 793 | } 794 | 795 | lastTimeUpdated = new Date().getTime(); 796 | timeUntilNextSpawn; 797 | 798 | this.running = true; 799 | 800 | debugLog('SimSystem.start at: ' + lastTimeUpdated); 801 | } 802 | 803 | function run() { 804 | var tmp; 805 | var spawnPos; 806 | var i; 807 | var time; 808 | var countSave = 0; 809 | var countUnsuccessful = 0; 810 | var countFatal = 0; 811 | 812 | time = new Date().getTime(); 813 | deltaTime = time - lastTimeUpdated; 814 | lastTimeUpdated = time; 815 | 816 | // If anyone's in pending, these persons should spawn every .2 seconds 817 | timeUntilNextSpawn -= deltaTime; 818 | if (pending.length > 0 && timeUntilNextSpawn <= 0) { 819 | // Spawn and add to the active array 820 | tmp = pending.pop(); 821 | active.push(tmp); 822 | timeUntilNextSpawn = SPAWN_INTERVAL; 823 | 824 | // Choose random spawn position 825 | spawnPos = _randomSpawnPosition( 826 | spawnArea.x, 827 | spawnArea.y, 828 | spawnArea.r - 10 829 | ); 830 | 831 | tmp.spawn(spawnPos.x, spawnPos.y); 832 | } 833 | 834 | // Draw any in done 835 | for (i = 0; i < done.length; i++) { 836 | done[i].run(deltaTime); 837 | 838 | if (done[i].isSaved()) { 839 | countSave++; 840 | } else if (done[i].isUnsuccessful()) { 841 | countUnsuccessful++; 842 | } else if (done[i].isFatal()) { 843 | countFatal++; 844 | } 845 | } 846 | 847 | // Update sim counts 848 | // vSimSaved = Math.floor((countSave / simSize) * 100); 849 | // vSimUnsuccessful = Math.floor((countUnsuccessful / simSize) * 100); 850 | // vSimFatal = Math.floor((countFatal / simSize) * 100); 851 | vSimSaved = countSave; 852 | vSimUnsuccessful = countUnsuccessful; 853 | vSimFatal = countFatal; 854 | 855 | // Draw any in active 856 | for (i = 0; i < active.length; i++) { 857 | active[i].run(deltaTime); 858 | 859 | if (active[i].isDone()) { 860 | tmp = active.pop(); 861 | done.push(tmp); 862 | } 863 | } 864 | } 865 | 866 | function stop() { 867 | debugLog('TODO: SimSystem.stop()'); 868 | 869 | this.running = false; 870 | } 871 | 872 | /** 873 | * Utility function to choose a random spawn position 874 | * 875 | * @param x Spawn area center x 876 | * @param y Spawn area center y 877 | * @param r Spawn area radius 878 | * 879 | * @return Object {int x, int y} 880 | */ 881 | function _randomSpawnPosition(x, y, r) { 882 | // Choose a random angle in radians 883 | var theta = Math.random() * Math.PI * 2; 884 | 885 | // Choose random position along the r 886 | var tmpR = Math.floor(Math.random() * r); 887 | 888 | var x1 = Math.floor(tmpR * Math.cos(theta)); 889 | var y1 = Math.floor(tmpR * Math.sin(theta)); 890 | 891 | return { 892 | x: x + x1, 893 | y: y + y1, 894 | }; 895 | } 896 | 897 | return { 898 | isRunning: function () { 899 | return this.running; 900 | }, 901 | isDone: function () { 902 | return vSimSaved + vSimFatal + vSimUnsuccessful == simSize; 903 | }, 904 | init: init, 905 | run: run, 906 | start: start, 907 | stop: stop, 908 | }; 909 | } 910 | 911 | /** 912 | * All things Person position, drawing and state related. 913 | */ 914 | function Person() { 915 | // possible states? inactive, active, moving, start, gunlock, attempt, saved, unsuccessful, fatal, done 916 | var States = { 917 | SPAWNING: 0, 918 | SPAWN_TRANSITION: 1, 919 | MOVE_TO_GUN_LOCK: 2, 920 | MOVE_TO_ATTEMPT: 3, 921 | TRY_LOCK: 4, 922 | TRY_ATTEMPT: 5, 923 | MOVE_TO_UNSUCCESSFUL: 6, 924 | MOVE_TO_FATAL: 7, 925 | MOVE_TO_SAVED: 8, 926 | DONE_UNSUCCESSFUL: 9, 927 | DONE_FATAL: 10, 928 | DONE_SAVED: 11, 929 | }; 930 | 931 | var size = 8; 932 | var spawnDuration = 250; 933 | var moveDuration = 500; 934 | var simDuration = 1500; 935 | var spawnTransitionDuration = 1000; 936 | var attemptRate = 85; 937 | 938 | var state; 939 | var timeUntilNextState; 940 | 941 | var position; 942 | var moveFromPosition; 943 | var moveToPosition; 944 | var hasGunLock; 945 | var simValue; 946 | 947 | function drawSpawn(deltaTime) { 948 | var currSize; 949 | var color = hasGunLock ? COLOR_PERSONS_W_LOCK : COLOR_PERSONS_DEFAULT; 950 | 951 | // Scale size based on how much time is left 952 | currSize = Math.floor( 953 | ((spawnDuration - timeUntilNextState) / spawnDuration) * size 954 | ); 955 | _drawPerson(position.x, position.y, currSize, color); 956 | } 957 | 958 | function drawSpawnTransition() { 959 | var color = hasGunLock ? COLOR_PERSONS_W_LOCK : COLOR_PERSONS_DEFAULT; 960 | 961 | _drawPerson(position.x, position.y, size, color); 962 | } 963 | 964 | function drawActive(colorOverride) { 965 | var color; 966 | 967 | if (typeof colorOverride === 'undefined') { 968 | color = hasGunLock ? COLOR_PERSONS_W_LOCK : COLOR_PERSONS_DEFAULT; 969 | } else { 970 | color = colorOverride; 971 | } 972 | 973 | _drawPerson(position.x, position.y, size, color); 974 | } 975 | 976 | function drawSim() { 977 | var arcPct; 978 | var colorSim; 979 | var color = hasGunLock ? COLOR_PERSONS_W_LOCK : COLOR_PERSONS_DEFAULT; 980 | var totalSimTime = simDuration * (simValue / 100); 981 | 982 | arcPct = 983 | ((totalSimTime - timeUntilNextState) / totalSimTime) * (simValue / 100); 984 | 985 | if (state == States.TRY_LOCK) { 986 | colorSim = COLOR_GUN_LOCK; 987 | } else if (state == States.TRY_ATTEMPT) { 988 | colorSim = COLOR_ATTEMPT; 989 | } else { 990 | colorSim = COLOR_SIM_DEFAULT; 991 | } 992 | 993 | _drawPerson(position.x, position.y, size, color, arcPct, colorSim); 994 | } 995 | 996 | function drawMove(colorOverride) { 997 | var pctMove = (moveDuration - timeUntilNextState) / moveDuration; 998 | position.x = 999 | moveFromPosition.x + (moveToPosition.x - moveFromPosition.x) * pctMove; 1000 | position.y = 1001 | moveFromPosition.y + (moveToPosition.y - moveFromPosition.y) * pctMove; 1002 | 1003 | drawActive(colorOverride); 1004 | } 1005 | 1006 | function reset() {} 1007 | 1008 | function run(deltaTime) { 1009 | timeUntilNextState -= deltaTime; 1010 | 1011 | if (state == States.SPAWNING) { 1012 | drawSpawn(deltaTime); 1013 | 1014 | if (timeUntilNextState < 0) { 1015 | state = States.SPAWN_TRANSITION; 1016 | timeUntilNextState = spawnTransitionDuration; 1017 | } 1018 | } else if (state == States.SPAWN_TRANSITION) { 1019 | drawSpawnTransition(); 1020 | 1021 | if (timeUntilNextState < 0) { 1022 | if (hasGunLock) { 1023 | state = States.MOVE_TO_GUN_LOCK; 1024 | } else { 1025 | state = States.MOVE_TO_ATTEMPT; 1026 | } 1027 | 1028 | timeUntilNextState = moveDuration; 1029 | _prepMove(state); 1030 | } 1031 | } else if (state == States.MOVE_TO_GUN_LOCK) { 1032 | drawMove(); 1033 | 1034 | if (timeUntilNextState < 0) { 1035 | state = States.TRY_LOCK; 1036 | 1037 | simValue = Math.floor(Math.random() * 100); 1038 | if (simValue <= vLockEffect) { 1039 | simValue = 100; 1040 | } 1041 | 1042 | timeUntilNextState = simDuration * (simValue / 100); 1043 | } 1044 | } else if (state == States.MOVE_TO_ATTEMPT) { 1045 | drawMove(); 1046 | 1047 | if (timeUntilNextState < 0) { 1048 | state = States.TRY_ATTEMPT; 1049 | 1050 | simValue = Math.floor(Math.random() * 100); 1051 | if (simValue > attemptRate) { 1052 | simValue = 100; 1053 | } 1054 | 1055 | timeUntilNextState = simDuration * (simValue / 100); 1056 | } 1057 | } else if (state == States.TRY_LOCK) { 1058 | drawSim(); 1059 | 1060 | if (timeUntilNextState < 0) { 1061 | // Gun lock prevented attempt 1062 | if (simValue == 100) { 1063 | state = States.MOVE_TO_SAVED; 1064 | } 1065 | // Move on to attempt 1066 | else { 1067 | state = States.MOVE_TO_ATTEMPT; 1068 | hasGunLock = false; 1069 | } 1070 | 1071 | timeUntilNextState = moveDuration; 1072 | _prepMove(state); 1073 | } 1074 | } else if (state == States.TRY_ATTEMPT) { 1075 | drawSim(); 1076 | 1077 | if (timeUntilNextState < 0) { 1078 | // Attempt was a success 1079 | if (simValue != 100) { 1080 | state = States.MOVE_TO_FATAL; 1081 | } 1082 | // Attempt was unsuccessful 1083 | else { 1084 | state = States.MOVE_TO_UNSUCCESSFUL; 1085 | } 1086 | 1087 | timeUntilNextState = moveDuration; 1088 | _prepMove(state); 1089 | } 1090 | } else if (state == States.MOVE_TO_SAVED) { 1091 | drawMove(COLOR_SAVED); 1092 | 1093 | if (timeUntilNextState < 0) { 1094 | state = States.DONE_SAVED; 1095 | } 1096 | } else if (state == States.MOVE_TO_UNSUCCESSFUL) { 1097 | drawMove(COLOR_UNSUCCESSFUL); 1098 | 1099 | if (timeUntilNextState < 0) { 1100 | state = States.DONE_UNSUCCESSFUL; 1101 | } 1102 | } else if (state == States.MOVE_TO_FATAL) { 1103 | drawMove(COLOR_FATAL); 1104 | 1105 | if (timeUntilNextState < 0) { 1106 | state = States.DONE_FATAL; 1107 | } 1108 | } else if (state == States.DONE_SAVED) { 1109 | drawActive(COLOR_SAVED); 1110 | } else if (state == States.DONE_UNSUCCESSFUL) { 1111 | drawActive(COLOR_UNSUCCESSFUL); 1112 | } else if (state == States.DONE_FATAL) { 1113 | drawActive(COLOR_FATAL); 1114 | } 1115 | } 1116 | 1117 | function spawn(x, y) { 1118 | debugLog('spawn: ' + x + ', ' + y); 1119 | position = { x: x, y: y }; 1120 | state = States.SPAWNING; 1121 | timeUntilNextState = spawnDuration; 1122 | 1123 | if (Math.random() > vWithLock / 100) { 1124 | hasGunLock = false; 1125 | } else { 1126 | hasGunLock = true; 1127 | } 1128 | } 1129 | 1130 | function _drawPerson(x, y, currSize, color, simPct, simColor) { 1131 | var arc; 1132 | 1133 | ctx.beginPath(); 1134 | ctx.arc(position.x, position.y, currSize, 0, 2 * Math.PI, false); 1135 | ctx.fillStyle = color; 1136 | ctx.fill(); 1137 | 1138 | if (typeof simPct !== 'undefined') { 1139 | arc = 2 * Math.PI * simPct - Math.PI / 2; 1140 | ctx.beginPath(); 1141 | ctx.arc( 1142 | position.x, 1143 | position.y, 1144 | currSize, 1145 | -1 * (Math.PI / 2), 1146 | arc, 1147 | false 1148 | ); 1149 | ctx.lineWidth = 2; 1150 | ctx.strokeStyle = simColor; 1151 | ctx.stroke(); 1152 | 1153 | // eh, feels kinda hacky, but there's a lot of places where stroke is used. 1154 | // reverting back to its default lineWidth = 1 1155 | ctx.lineWidth = 1; 1156 | } 1157 | } 1158 | 1159 | function _prepMove(nextState) { 1160 | var x; 1161 | var y; 1162 | var width; 1163 | var height; 1164 | var margin = 10; 1165 | 1166 | if (nextState == States.MOVE_TO_GUN_LOCK) { 1167 | x = 228; 1168 | y = 208; 1169 | width = 128; 1170 | height = 70; 1171 | } else if (nextState == States.MOVE_TO_ATTEMPT) { 1172 | x = 228; 1173 | y = 332; 1174 | width = 128; 1175 | height = 70; 1176 | } else if (nextState == States.MOVE_TO_UNSUCCESSFUL) { 1177 | x = 396; 1178 | y = 271; 1179 | width = 146; 1180 | height = 70; 1181 | } else if (nextState == States.MOVE_TO_FATAL) { 1182 | x = 396; 1183 | y = 346; 1184 | width = 146; 1185 | height = 70; 1186 | } else if (nextState == States.MOVE_TO_SAVED) { 1187 | x = 396; 1188 | y = 196; 1189 | width = 146; 1190 | height = 70; 1191 | } 1192 | 1193 | moveFromPosition = {}; 1194 | moveFromPosition.x = position.x; 1195 | moveFromPosition.y = position.y; 1196 | 1197 | moveToPosition = {}; 1198 | moveToPosition.x = 1199 | x + margin + Math.floor(Math.random() * (width - margin * 2)); 1200 | moveToPosition.y = 1201 | y + margin + Math.floor(Math.random() * (height - margin * 2)); 1202 | } 1203 | 1204 | function isDone() { 1205 | return ( 1206 | state == States.DONE_SAVED || 1207 | state == States.DONE_UNSUCCESSFUL || 1208 | state == States.DONE_FATAL 1209 | ); 1210 | } 1211 | 1212 | function isSaved() { 1213 | return state == States.DONE_SAVED; 1214 | } 1215 | 1216 | function isUnsuccessful() { 1217 | return state == States.DONE_UNSUCCESSFUL; 1218 | } 1219 | 1220 | function isFatal() { 1221 | return state == States.DONE_FATAL; 1222 | } 1223 | 1224 | return { 1225 | isDone: isDone, 1226 | isSaved: isSaved, 1227 | isUnsuccessful: isUnsuccessful, 1228 | isFatal: isFatal, 1229 | reset: reset, 1230 | run: run, 1231 | spawn: spawn, 1232 | }; 1233 | } 1234 | 1235 | return { 1236 | init: init, 1237 | start: start, 1238 | pause: pause, 1239 | resume: resume, 1240 | hasStarted: function () { 1241 | return hasStarted; 1242 | }, 1243 | isPaused: function () { 1244 | return isPaused; 1245 | }, 1246 | }; 1247 | })(); 1248 | 1249 | playable2.init(); 1250 | --------------------------------------------------------------------------------