├── 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 |
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 |
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 |
--------------------------------------------------------------------------------