├── .gitignore
├── config.sample.js
├── map
└── index.html
├── _assets
├── css
│ ├── map.css
│ └── form.css
└── js
│ ├── form.js
│ ├── map.js
│ ├── leaflet-heat.js
│ └── particles.js
├── form
└── index.html
└── readme.md
/.gitignore:
--------------------------------------------------------------------------------
1 | config.js
2 | logo.png
--------------------------------------------------------------------------------
/config.sample.js:
--------------------------------------------------------------------------------
1 | var CONFIG = {
2 | map: {
3 | lat: 50.85,
4 | lon: -0.2,
5 | zoom: 12,
6 | minOpacity: 0.4,
7 | radius: 15,
8 | max: 10,
9 | },
10 | form: {
11 | id: "[Google form ID]",
12 | postcode: "[Postcode input name]",
13 | attendees: "[Attendees input name]",
14 | lat: "[Latitude input name]",
15 | lon: "[Longitude input name]"
16 | },
17 | spreadsheet: {
18 | id: "[Google spreadsheet ID]",
19 | apiKey: "[Google spreadsheet api key]",
20 | sheet: "[Google spreadsheet sheet name]",
21 | range: "[Start cell]:[End cell]"
22 | }
23 | };
24 |
--------------------------------------------------------------------------------
/map/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Map
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
0 postcodes
28 |
0 attendees
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/_assets/css/map.css:
--------------------------------------------------------------------------------
1 | /* Make the map fill the window */
2 | /* Defaults */
3 |
4 | html {
5 | box-sizing:border-box;
6 | }
7 |
8 | *,
9 | :after,
10 | :before {
11 | box-sizing:inherit;
12 | }
13 |
14 | html,
15 | body {
16 | margin: 0;
17 | padding: 0;
18 | height: 100%;
19 | }
20 |
21 | html {
22 | font-family: Helvetica, sans-serif;
23 | font-size: 16px;
24 | line-height: 1.5;
25 | }
26 |
27 | /* Set the map height to define the size of the map element */
28 |
29 | #map {
30 | height: 100%;
31 | position: relative;
32 | z-index: 1;
33 | }
34 |
35 | /* stats */
36 |
37 | .stats {
38 | position: absolute;
39 | bottom: 1rem;
40 | left: 1rem;
41 | z-index: 2;
42 | padding: 0.5rem 1rem;
43 | background: rgba(255, 255, 255, 0.6);
44 | }
45 |
46 | .stats p {
47 | margin: 0;
48 | padding: 0;
49 | font-size: 0.8rem;
50 | }
51 |
52 | /* log */
53 |
54 | .log {
55 | position: absolute;
56 | bottom: 1rem;
57 | left: 33%;
58 | right: 33%;
59 | z-index: 2;
60 | margin: 0;
61 | max-height: 6rem;
62 | overflow: scroll;
63 | text-align: center;
64 | list-style: none;
65 | }
66 |
67 | .log li {
68 | background: rgba(255, 255, 255, 0.6);
69 | width: auto;
70 | margin-bottom: 0.2rem;
71 | opacity: 0;
72 | animation: log 6s linear 1;
73 | }
74 |
75 | @keyframes log {
76 | 0%, 75% { opacity: 1; }
77 | 100% { opacity: 0; }
78 | }
79 |
80 | /* timer */
81 |
82 | .timer {
83 | position: absolute;
84 | top: 0;
85 | left: 0;
86 | right: 0;
87 | background: rgba(0, 0, 0, 0.3);
88 | height: 0.5rem;
89 | z-index: 2;
90 | }
91 |
92 | .timer.is-waiting::before {
93 | content: "";
94 | position: absolute;
95 | width: 50%;
96 | left: 0;
97 | top: 0;
98 | height: 100%;
99 | background: rgba(255, 128, 0, 0.8);
100 |
101 | animation-name: timer;
102 | animation-duration: 30s;
103 | animation-timing-function: linear;
104 | animation-delay: 0s;
105 | animation-direction: normal;
106 | animation-iteration-count: 1;
107 | animation-fill-mode: forwards;
108 | animation-play-state: running;
109 | }
110 |
111 | @keyframes timer {
112 | from { width: 0; }
113 | to { width: 100%; }
114 | }
--------------------------------------------------------------------------------
/form/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Attendee form
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |

22 |
Welcome!
23 |
Please let us know where you've come from
24 |
25 |
26 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Heatmap with Google Forms
2 |
3 | A custom HTML form that stores data in a Google Form, and uses this information to generate a heatmap using [Leaflet](https://leafletjs.com/) and [Leaflet.heat](https://github.com/Leaflet/Leaflet.heat).
4 |
5 |
6 | ## Set-up
7 |
8 | Create a config file by duplicating the file `config.sample.js` and calling it `config.js`.
9 |
10 | Customise the three blocks of values:
11 |
12 |
13 | ### Map
14 |
15 | - `lat`, `lon` and `zoom`: These are the default position for the map.
16 | - `minOpacity`, `radius` and `max`: These values change how the heatmap is rendered - see the [Leaflet.heat](https://github.com/Leaflet/Leaflet.heat) docs for an explanation.
17 |
18 |
19 | ### Form
20 |
21 | Create a new Google Form.
22 |
23 | The `id` field in the config should come from the URL of the form.
24 |
25 | The form should have four fields:
26 |
27 | - Postcode: This should be a text field
28 | - Lat and Lon: These fields should be optional. They won't be filled in by any users. Instead, [Postcodes.io](https://postcodes.io/) is used to geocode the postcode and retrieve a latitude and longitude.
29 | - Attendees: This will be a number, probably between 1-10.
30 |
31 | The four inputs *must* be in this order: postcode, lat, lon, attendees.
32 |
33 | Find out the IDs for these four input fields to add to the config. The easiest way to do this is to use the browser's webdev inspector. See [this link](https://github.com/jsdevel/google-form) for detail. They will probably be named `entry.xxxxx`.
34 |
35 |
36 | ### Spreadsheet
37 |
38 | Create a Google Spreadsheet from the Google Form responses.
39 |
40 | Make the spreadsheet visible publicly - read only.
41 |
42 | The `id` field in the config should come from the URL of the spreadsheet.
43 |
44 | Sign up for a new Google Spreadsheet API key - see [their documentation](https://developers.google.com/sheets/api/quickstart/js) for detail. You don't need a Client ID, just the [API key](https://console.developers.google.com/apis/credentials).
45 |
46 | While setting this up, it's worth restricting its use to specific domains, and only the _Google Sheets API_.
47 |
48 | The `sheet` field in the config should come from the name of the spreadsheet tab - this will be created automatically by the Google Form. Replace any spaces with _%20_, for example the value may be `Form%20Responses%201`.
49 |
50 | The `range` field in the config should include the fields that are populated by the form, for example `C2:E10000`.
51 |
52 | ### Logo
53 |
54 | Add an image file in the root called `logo.png` in order to add a logo to the front page of the form.
55 |
56 | ---
57 |
58 | ## Maintenance and support
59 |
60 | [](http://unmaintained.tech/)
61 |
62 | ---
63 |
64 | ## License
65 |
66 | This work is free. You can redistribute it and/or modify it under the
67 | terms of the Do What The Fuck You Want To Public License, Version 2,
68 | as published by Sam Hocevar. See http://www.wtfpl.net/ for more details.
69 |
70 | ```
71 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
72 | Version 2, December 2004
73 |
74 | Copyright (C) 2004 Sam Hocevar
75 |
76 | Everyone is permitted to copy and distribute verbatim or modified
77 | copies of this license document, and changing it is allowed as long
78 | as the name is changed.
79 |
80 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
81 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
82 |
83 | 0. You just DO WHAT THE FUCK YOU WANT TO.
84 |
85 | ```
--------------------------------------------------------------------------------
/_assets/js/form.js:
--------------------------------------------------------------------------------
1 | (function(config) {
2 |
3 | // Use Google Forms for data storage
4 | // Geocode postcode data using https://postcodes.io/
5 | //
6 | // https://github.com/jsdevel/google-form
7 | // https://www.codeproject.com/tips/721795/store-your-form-data-in-google-spreadsheet
8 |
9 | var wrapperEl;
10 | var formEl;
11 | var cancelEl;
12 | var inputEls;
13 | var postcodeEl;
14 |
15 | function init() {
16 | wrapper = document.querySelector('.Wrapper');
17 | formEl = document.querySelector('.Form');
18 | submitEl = formEl.querySelector('.Form-submit');
19 | cancelEl = formEl.querySelector('.Form-cancel');
20 | inputEls = formEl.querySelectorAll('.Form-input');
21 | postcodeEl = formEl.querySelector('#postcode');
22 |
23 | cancelEl.addEventListener('click', resetForm);
24 | formEl.addEventListener('submit', handleFormSubmit);
25 |
26 | inputEls.forEach(function(inputEl) {
27 | inputEl.addEventListener('input', checkInputValidity);
28 | });
29 |
30 | inputEls[0].focus();
31 | }
32 |
33 | function handleFormSubmit(e) {
34 | e.preventDefault();
35 | convertPostcodeToLatLon();
36 | }
37 |
38 | function checkInputValidity() {
39 | var valid = true;
40 | inputEls.forEach(function(inputEl) {
41 | if (!inputEl.validity.valid) {
42 | valid = false;
43 | }
44 | });
45 | submitEl.disabled = !valid;
46 | }
47 |
48 | // attempt geocoding for postcode - but store response regardless of success
49 | function convertPostcodeToLatLon() {
50 | var postcode = postcodeEl.value;
51 |
52 | var url = 'https://api.postcodes.io/postcodes/' + postcode;
53 |
54 | request = new XMLHttpRequest();
55 | request.open('GET', url, true);
56 | request.send();
57 | request.onreadystatechange = handlePostcodeResponse;
58 | }
59 |
60 | function handlePostcodeResponse() {
61 | if (this.readyState === 4) {
62 | var latLon = null;
63 |
64 | // carry on anyway, but if we get a lat/lon then use it
65 | if (this.status === 200 || this.status === 0) {
66 | var response = JSON.parse(this.response);
67 | if (response.result && response.result.latitude && response.result.longitude) {
68 | latLon = {
69 | 'lat': response.result.latitude,
70 | 'lon': response.result.longitude
71 | }
72 | }
73 | }
74 |
75 | prepareFormData(latLon);
76 | }
77 | }
78 |
79 | function prepareFormData(latLon) {
80 | var data = ['submit=Submit'];
81 |
82 | if (latLon) {
83 | data.push(config.form.lat + '=' + latLon.lat);
84 | data.push(config.form.lon + '=' + latLon.lon);
85 | }
86 |
87 | inputEls.forEach(function(inputEl) {
88 | data.push(config.form[inputEl.id] + '=' + encodeURIComponent(inputEl.value));
89 | });
90 |
91 | var url = "https://docs.google.com/forms/d/" + config.form.id + '/formResponse?' + data.join('&');
92 |
93 | processFormData(url, data);
94 | }
95 |
96 | function processFormData(url, data) {
97 | request = new XMLHttpRequest();
98 | request.open('GET', url, true);
99 | request.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
100 | request.send();
101 | request.onreadystatechange = handleFormResponse;
102 | }
103 |
104 | function handleFormResponse() {
105 | if (this.readyState === 4) {
106 | // show success state regardless, but if there was an error we'd know here
107 | if (this.status !== 200 && this.status !== 0) {
108 | console.log(this);
109 | }
110 |
111 | showSuccess();
112 | }
113 | }
114 |
115 | function showSuccess() {
116 | formEl.classList.add('is-complete');
117 | setTimeout(resetForm, 2000);
118 | }
119 |
120 | function resetForm() {
121 | formEl.classList.remove('is-complete');
122 | formEl.reset();
123 | checkInputValidity();
124 | inputEls[0].focus();
125 | }
126 |
127 | document.addEventListener('DOMContentLoaded', init);
128 |
129 | })(window.CONFIG);
130 |
--------------------------------------------------------------------------------
/_assets/js/map.js:
--------------------------------------------------------------------------------
1 | (function(config) {
2 |
3 | var map;
4 | var tiles;
5 | var heat;
6 | var hotspotCount = 0;
7 |
8 | var timerEl;
9 | var statPostcodesEl;
10 | var statAttendeesEl;
11 | var logEl;
12 |
13 | var options = {
14 | minOpacity: config.map.minOpacity,
15 | radius: config.map.radius,
16 | max: config.map.max,
17 | };
18 |
19 | function init() {
20 | findEls();
21 | createMap();
22 | loadData();
23 |
24 | // uncomment to test random heatmap population
25 | // setTimeout(addRandomPoint, 1000);
26 | }
27 |
28 | function findEls() {
29 | timerEl = document.querySelector('.timer');
30 | statPostcodesEl = document.querySelector('.stat-postcodes');
31 | statAttendeesEl = document.querySelector('.stat-attendees');
32 | logEl = document.querySelector('.log');
33 | }
34 |
35 | function createMap() {
36 | map = L.map('map').setView([config.map.lat, config.map.lon], config.map.zoom);
37 |
38 | tiles = L.tileLayer('https://{s}.tile.osm.org/{z}/{x}/{y}.png', {
39 | attribution: '© OpenStreetMap contributors',
40 | }).addTo(map);
41 |
42 | heat = L.heatLayer([], options).addTo(map);
43 | }
44 |
45 | function loadData() {
46 | timerEl.classList.remove('is-waiting');
47 |
48 | var url = 'https://sheets.googleapis.com/v4/spreadsheets/' + config.spreadsheet.id + '/values/' + config.spreadsheet.sheet + '!' + config.spreadsheet.range + '?key=' + config.spreadsheet.apiKey;
49 |
50 | request = new XMLHttpRequest();
51 | request.open('GET', url, true);
52 | request.send();
53 | request.onreadystatechange = updateMapData;
54 | }
55 |
56 | function updateMapData() {
57 | if (this.readyState === 4) {
58 | if (this.status === 200 || this.status === 0) {
59 | try {
60 | var response = JSON.parse(this.response);
61 | if (response.values) {
62 |
63 | // extract just the new responses to update the map
64 | if (response.values.length > hotspotCount) {
65 | var hotspots = response.values.slice(hotspotCount);
66 | hotspotCount = response.values.length;
67 | updateMap(hotspots);
68 | }
69 | }
70 | } catch (e) {
71 | console.log(e);
72 | }
73 | }
74 |
75 | // update every 30 seconds
76 | setTimeout(loadData, 1000 * 30);
77 | timerEl.classList.add('is-waiting');
78 | }
79 | }
80 |
81 | function updateMap(hotspots) {
82 | hotspots.forEach(function(hotspot) {
83 | logHotspot(hotspot);
84 |
85 | // remove postcode from data prior to plotting on map
86 | hotspot.shift();
87 | heat.addLatLng(hotspot);
88 | });
89 | }
90 |
91 | // hotspot array order: [postcode, lat, lon, attendees]
92 | function logHotspot(hotspot) {
93 | var postcodeValue = parseInt(statPostcodesEl.textContent, 10);
94 | statPostcodesEl.textContent = postcodeValue + 1;
95 |
96 | var attendeesValue = parseInt(statAttendeesEl.textContent, 10);
97 | statAttendeesEl.textContent = attendeesValue + parseInt(hotspot[3], 10);
98 |
99 | var logItemEl = document.createElement("li");
100 | var logItemText = document.createTextNode(hotspot[0] + " added");
101 | logItemEl.appendChild(logItemText);
102 | logEl.appendChild(logItemEl);
103 |
104 | setTimeout(emptyLog, 1000 * 10);
105 | }
106 |
107 | function emptyLog() {
108 | while (logEl.firstChild) {
109 | logEl.removeChild(logEl.firstChild);
110 | }
111 | }
112 |
113 | // test heatmap
114 | function addRandomPoint() {
115 | var lat = generateRandom(config.map.lat - 0.2, config.map.lat + 0.2, 8);
116 | var lng = generateRandom(config.map.lon - 0.2, config.map.lon + 0.2, 8);
117 | heat.addLatLng([lat, lng]);
118 | setTimeout(addRandomPoint, 1000);
119 | }
120 |
121 | function generateRandom(min, max, decimals) {
122 | var precision = '1' + '0'.repeat(decimals);
123 | return Math.floor(Math.random() * (max * precision - min * precision) + min * precision) / (1 * precision);
124 | }
125 |
126 | init();
127 | })(window.CONFIG);
128 |
--------------------------------------------------------------------------------
/_assets/css/form.css:
--------------------------------------------------------------------------------
1 | /* Defaults */
2 |
3 | html {
4 | box-sizing:border-box;
5 | }
6 |
7 | *,
8 | :after,
9 | :before {
10 | box-sizing:inherit;
11 | }
12 |
13 | html,
14 | body {
15 | margin: 0;
16 | padding: 0;
17 | height: 100%;
18 | }
19 |
20 | html {
21 | font-family: Helvetica, sans-serif;
22 | font-size: 16px;
23 | }
24 |
25 | body {
26 | display: flex;
27 | align-items: center;
28 | justify-content: center;
29 | }
30 |
31 | /* background */
32 |
33 | .Background {
34 | background: linear-gradient(90deg, #7fa5d7 0%, #6ac9cf 100%);
35 | position: fixed;
36 | z-index: 1;
37 | left: 0;
38 | right: 0;
39 | top: 0;
40 | bottom: 0;
41 | }
42 |
43 | /* Wrap */
44 |
45 | .Wrapper {
46 | height: 90%;
47 | width: 90%;
48 | display: flex;
49 | flex-direction: column;
50 | align-items: stretch;
51 | justify-content: center;
52 | position: relative;
53 | z-index: 2;
54 | }
55 |
56 | @media (min-width: 62.5rem) {
57 | .Wrapper {
58 | flex-direction: row;
59 | }
60 | }
61 |
62 | .Welcome,
63 | .Form {
64 | color: #333;
65 | flex-direction: column;
66 | display: flex;
67 | align-items: center;
68 | justify-content: center;
69 | text-align: center;
70 | padding: 1rem;
71 | }
72 |
73 | /* Welcome */
74 |
75 | .Welcome {
76 | background: rgba(173, 216, 230, 0.9);
77 | padding: 1rem;
78 | }
79 |
80 | @media (min-width: 62.5rem) {
81 | .Welcome {
82 | flex: 1;
83 | }
84 | }
85 |
86 | .Welcome-logo {
87 | max-width: 30%;
88 | margin-bottom: 0.5rem;
89 | }
90 |
91 | @media (min-width: 62.5rem) {
92 | .Welcome-logo {
93 | max-width: 90%;
94 | margin-bottom: 2rem;
95 | }
96 | }
97 |
98 | .Welcome-title {
99 | font-size: 1.2rem;
100 | margin: 0.5rem 0;
101 | }
102 |
103 | @media (min-width: 62.5rem) {
104 | .Welcome-title {
105 | font-size: 4rem;
106 | margin: 0.5rem 0;
107 | }
108 | }
109 |
110 | .Welcome-text {
111 | margin: 0.5rem 1rem;
112 | font-size: 1rem;
113 | line-height: 1.5;
114 | }
115 |
116 | @media (min-width: 62.5rem) {
117 | .Welcome-text {
118 | font-size: 1.6rem;
119 | }
120 | }
121 |
122 | /* Form */
123 |
124 | .Form {
125 | position: relative;
126 | background: rgba(255, 255, 255, 0.9);
127 | flex: 1;
128 | justify-content: flex-start;
129 | }
130 |
131 | @media (min-width: 62.5rem) {
132 | .Form {
133 | justify-content: center;
134 | }
135 | }
136 |
137 | .Form-label {
138 | display: block;
139 | font-size: 1rem;
140 | margin: 0.5rem 0;
141 | color: #999;
142 | }
143 |
144 | @media (min-width: 62.5rem) {
145 | .Form-label {
146 | font-size: 1.2rem;
147 | }
148 | }
149 |
150 | .Form-input {
151 | font-size: 1.8rem;
152 | border: 0;
153 | border-bottom: 2px solid #6ac9cf;
154 | outline: none;
155 | margin-bottom: 2rem;
156 | padding: 0.5rem;
157 | text-align: center;
158 | width: 50%;
159 | }
160 |
161 | @media (min-width: 62.5rem) {
162 | .Form-input {
163 | font-size: 3rem;
164 | margin-bottom: 3rem;
165 | }
166 | }
167 |
168 | .Form-input:focus {
169 | border-bottom-color: rgba(255, 204, 0, 1);
170 | }
171 |
172 | .Form-submit {
173 | background: rgba(255, 204, 0, 1);
174 | -webkit-appearance: none;
175 | border: none;
176 | display: block;
177 | margin-bottom: 2rem;
178 | padding: 1rem 3rem;
179 | font-size: 1.4rem;
180 | cursor: pointer;
181 | transition: all 0.5s;
182 | }
183 |
184 | @media (min-width: 62.5rem) {
185 | .Form-submit {
186 | font-size: 1.2rem;
187 | padding: 1.5rem 6rem;
188 | }
189 | }
190 |
191 | .Form-submit:disabled {
192 | background: rgba(255, 204, 0, 0.3);
193 | pointer-events: none;
194 | }
195 |
196 | .Form-submit:focus,
197 | .Form-submit:active {
198 | outline: none;
199 | }
200 |
201 | .Form-cancel {
202 | background: rgba(255, 255, 255, 0.8);
203 | -webkit-appearance: none;
204 | border: none;
205 | padding: 1rem 3rem;
206 | font-size: 0.8rem;
207 | }
208 |
209 | .Form-complete {
210 | position: absolute;
211 | display: flex;
212 | flex-direction: column;
213 | align-items: center;
214 | justify-content: center;
215 | left: 0;
216 | right: 0;
217 | top: 0;
218 | bottom: 0;
219 | background: rgba(255, 255, 255, 0.95);
220 | z-index: 2;
221 | opacity: 0;
222 | visibility: hidden;
223 | transition: all 0.5s;
224 | }
225 |
226 | .Form.is-complete .Form-complete {
227 | opacity: 1;
228 | top: 0;
229 | visibility: visible;
230 | }
231 |
232 | .Form-completeTitle {
233 | font-size: 3rem;
234 | margin-bottom: 1rem;
235 | }
236 |
237 | .Form-completeText {
238 | font-size: 1rem;
239 | }
--------------------------------------------------------------------------------
/_assets/js/leaflet-heat.js:
--------------------------------------------------------------------------------
1 | /*
2 | (c) 2014, Vladimir Agafonkin
3 | simpleheat, a tiny JavaScript library for drawing heatmaps with Canvas
4 | https://github.com/mourner/simpleheat
5 | */
6 | !function(){"use strict";function t(i){return this instanceof t?(this._canvas=i="string"==typeof i?document.getElementById(i):i,this._ctx=i.getContext("2d"),this._width=i.width,this._height=i.height,this._max=1,void this.clear()):new t(i)}t.prototype={defaultRadius:25,defaultGradient:{.4:"blue",.6:"cyan",.7:"lime",.8:"yellow",1:"red"},data:function(t,i){return this._data=t,this},max:function(t){return this._max=t,this},add:function(t){return this._data.push(t),this},clear:function(){return this._data=[],this},radius:function(t,i){i=i||15;var a=this._circle=document.createElement("canvas"),s=a.getContext("2d"),e=this._r=t+i;return a.width=a.height=2*e,s.shadowOffsetX=s.shadowOffsetY=200,s.shadowBlur=i,s.shadowColor="black",s.beginPath(),s.arc(e-200,e-200,t,0,2*Math.PI,!0),s.closePath(),s.fill(),this},gradient:function(t){var i=document.createElement("canvas"),a=i.getContext("2d"),s=a.createLinearGradient(0,0,0,256);i.width=1,i.height=256;for(var e in t)s.addColorStop(e,t[e]);return a.fillStyle=s,a.fillRect(0,0,1,256),this._grad=a.getImageData(0,0,1,256).data,this},draw:function(t){this._circle||this.radius(this.defaultRadius),this._grad||this.gradient(this.defaultGradient);var i=this._ctx;i.clearRect(0,0,this._width,this._height);for(var a,s=0,e=this._data.length;e>s;s++)a=this._data[s],i.globalAlpha=Math.max(a[2]/this._max,t||.05),i.drawImage(this._circle,a[0]-this._r,a[1]-this._r);var n=i.getImageData(0,0,this._width,this._height);return this._colorize(n.data,this._grad),i.putImageData(n,0,0),this},_colorize:function(t,i){for(var a,s=3,e=t.length;e>s;s+=4)a=4*t[s],a&&(t[s-3]=i[a],t[s-2]=i[a+1],t[s-1]=i[a+2])}},window.simpleheat=t}(),/*
7 | (c) 2014, Vladimir Agafonkin
8 | Leaflet.heat, a tiny and fast heatmap plugin for Leaflet.
9 | https://github.com/Leaflet/Leaflet.heat
10 | */
11 | L.HeatLayer=(L.Layer?L.Layer:L.Class).extend({initialize:function(t,i){this._latlngs=t,L.setOptions(this,i)},setLatLngs:function(t){return this._latlngs=t,this.redraw()},addLatLng:function(t){return this._latlngs.push(t),this.redraw()},setOptions:function(t){return L.setOptions(this,t),this._heat&&this._updateOptions(),this.redraw()},redraw:function(){return!this._heat||this._frame||this._map._animating||(this._frame=L.Util.requestAnimFrame(this._redraw,this)),this},onAdd:function(t){this._map=t,this._canvas||this._initCanvas(),t._panes.overlayPane.appendChild(this._canvas),t.on("moveend",this._reset,this),t.options.zoomAnimation&&L.Browser.any3d&&t.on("zoomanim",this._animateZoom,this),this._reset()},onRemove:function(t){t.getPanes().overlayPane.removeChild(this._canvas),t.off("moveend",this._reset,this),t.options.zoomAnimation&&t.off("zoomanim",this._animateZoom,this)},addTo:function(t){return t.addLayer(this),this},_initCanvas:function(){var t=this._canvas=L.DomUtil.create("canvas","leaflet-heatmap-layer leaflet-layer"),i=L.DomUtil.testProp(["transformOrigin","WebkitTransformOrigin","msTransformOrigin"]);t.style[i]="50% 50%";var a=this._map.getSize();t.width=a.x,t.height=a.y;var s=this._map.options.zoomAnimation&&L.Browser.any3d;L.DomUtil.addClass(t,"leaflet-zoom-"+(s?"animated":"hide")),this._heat=simpleheat(t),this._updateOptions()},_updateOptions:function(){this._heat.radius(this.options.radius||this._heat.defaultRadius,this.options.blur),this.options.gradient&&this._heat.gradient(this.options.gradient),this.options.max&&this._heat.max(this.options.max)},_reset:function(){var t=this._map.containerPointToLayerPoint([0,0]);L.DomUtil.setPosition(this._canvas,t);var i=this._map.getSize();this._heat._width!==i.x&&(this._canvas.width=this._heat._width=i.x),this._heat._height!==i.y&&(this._canvas.height=this._heat._height=i.y),this._redraw()},_redraw:function(){var t,i,a,s,e,n,h,o,r,d=[],_=this._heat._r,l=this._map.getSize(),m=new L.Bounds(L.point([-_,-_]),l.add([_,_])),c=void 0===this.options.max?1:this.options.max,u=void 0===this.options.maxZoom?this._map.getMaxZoom():this.options.maxZoom,f=1/Math.pow(2,Math.max(0,Math.min(u-this._map.getZoom(),12))),g=_/2,p=[],v=this._map._getMapPanePos(),w=v.x%g,y=v.y%g;for(t=0,i=this._latlngs.length;i>t;t++)if(a=this._map.latLngToContainerPoint(this._latlngs[t]),m.contains(a)){e=Math.floor((a.x-w)/g)+2,n=Math.floor((a.y-y)/g)+2;var x=void 0!==this._latlngs[t].alt?this._latlngs[t].alt:void 0!==this._latlngs[t][2]?+this._latlngs[t][2]:1;r=x*f,p[n]=p[n]||[],s=p[n][e],s?(s[0]=(s[0]*s[2]+a.x*r)/(s[2]+r),s[1]=(s[1]*s[2]+a.y*r)/(s[2]+r),s[2]+=r):p[n][e]=[a.x,a.y,r]}for(t=0,i=p.length;i>t;t++)if(p[t])for(h=0,o=p[t].length;o>h;h++)s=p[t][h],s&&d.push([Math.round(s[0]),Math.round(s[1]),Math.min(s[2],c)]);this._heat.data(d).draw(this.options.minOpacity),this._frame=null},_animateZoom:function(t){var i=this._map.getZoomScale(t.zoom),a=this._map._getCenterOffset(t.center)._multiplyBy(-i).subtract(this._map._getMapPanePos());L.DomUtil.setTransform?L.DomUtil.setTransform(this._canvas,a,i):this._canvas.style[L.DomUtil.TRANSFORM]=L.DomUtil.getTranslateString(a)+" scale("+i+")"}}),L.heatLayer=function(t,i){return new L.HeatLayer(t,i)};
--------------------------------------------------------------------------------
/_assets/js/particles.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Particles
3 | * © 2008
4 | */
5 | (function () {
6 |
7 | "use strict";
8 |
9 | /*
10 | * global variables
11 | */
12 | var
13 | // HTML canvas element
14 | canvas,
15 |
16 | // canvas draw context
17 | ctx,
18 |
19 | // collection of existing particles
20 | particles = [],
21 |
22 | // configurable options
23 | config = {
24 |
25 | // number of particles to draw
26 | particleCount : 50,
27 |
28 | // minimum distance for each particle to affect another
29 | minimumAffectingDistance : 50
30 | };
31 |
32 | /*
33 | * init
34 | */
35 | function init () {
36 | drawCanvas();
37 | createParticles();
38 | loop();
39 |
40 | // resize canvas on page resize
41 | window.addEventListener("resize", function (event) {
42 | drawCanvas();
43 | });
44 | }
45 |
46 |
47 | /*
48 | * start redraw loop logic
49 | */
50 | function loop () {
51 | clear();
52 | update();
53 | draw();
54 | queue();
55 | }
56 |
57 | /*
58 | * wipe canvas ready for next redraw
59 | */
60 | function clear () {
61 | ctx.clearRect(0, 0, canvas.width, canvas.height);
62 | }
63 |
64 | /*
65 | * update particle positions
66 | */
67 | function update () {
68 |
69 | // update each particle's position
70 | for (var count = 0; count < particles.length; count++) {
71 |
72 | var p = particles[count];
73 |
74 | // Change the velocities
75 | p.x += p.vx;
76 | p.y += p.vy;
77 |
78 | // Bounce a particle that hits the edge
79 | if(p.x + p.radius > canvas.width || p.x - p.radius < 0) {
80 | p.vx = -p.vx;
81 | }
82 |
83 | if(p.y + p.radius > canvas.height || p.y - p.radius < 0) {
84 | p.vy = -p.vy;
85 | }
86 |
87 | // Check particle attraction
88 | for (var next = count + 1; next < particles.length; next++) {
89 | var p2 = particles[next];
90 | calculateDistanceBetweenParticles(p, p2);
91 | }
92 | }
93 | }
94 |
95 | /*
96 | * update visual state - draw each particle
97 | */
98 | function draw () {
99 | for (var count = 0; count < particles.length; count++) {
100 | var p = particles[count];
101 | p.draw();
102 | }
103 | }
104 |
105 | /*
106 | * prepare next redraw when the browser is ready
107 | */
108 | function queue () {
109 | window.requestAnimationFrame(loop);
110 | }
111 |
112 | // go!
113 | init();
114 |
115 |
116 | /*
117 | * Objects
118 | */
119 |
120 | /*
121 | * Particle
122 | */
123 | function Particle () {
124 |
125 | // Position particle
126 | this.x = Math.random() * canvas.width;
127 | this.y = Math.random() * canvas.height;
128 |
129 | // Give particle velocity, between -1 and 1
130 | this.vx = -1 + Math.random() * 2;
131 | this.vy = -1 + Math.random() * 2;
132 |
133 | // Give particle a radius
134 | this.radius = 4;
135 |
136 | // draw particle
137 | this.draw = function () {
138 | ctx.fillStyle = "white";
139 | ctx.beginPath();
140 | ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
141 | ctx.fill();
142 | }
143 | }
144 |
145 | /*
146 | * Draw canvas
147 | */
148 | function drawCanvas () {
149 | canvas = document.querySelector("canvas");
150 | ctx = canvas.getContext("2d");
151 |
152 | // set canvas to full page dimensions
153 | canvas.width = window.innerWidth;
154 | canvas.height = window.innerHeight;
155 | }
156 |
157 | /*
158 | * Create particles
159 | */
160 | function createParticles () {
161 | for(var i = 0; i < config.particleCount; i++) {
162 | particles.push(new Particle());
163 | }
164 | }
165 |
166 |
167 | /*
168 | * Distance calculator between two particles
169 | */
170 | function calculateDistanceBetweenParticles (p1, p2) {
171 |
172 | var dist,
173 | dx = p1.x - p2.x,
174 | dy = p1.y - p2.y;
175 |
176 | dist = Math.sqrt(dx*dx + dy*dy);
177 |
178 | // Check whether distance is smaller than the min distance
179 | if(dist <= config.minimumAffectingDistance) {
180 |
181 | // set line opacity
182 | var opacity = 1 - dist/config.minimumAffectingDistance;
183 |
184 | // Draw connecting line
185 | ctx.beginPath();
186 | ctx.strokeStyle = "rgba(255, 255, 255, " + opacity +")";
187 | ctx.moveTo(p1.x, p1.y);
188 | ctx.lineTo(p2.x, p2.y);
189 | ctx.stroke();
190 | ctx.closePath();
191 |
192 | // Calculate particle acceleration
193 | var ax = dx / 2000,
194 | ay = dy / 2000;
195 |
196 | // Apply particle acceleration
197 | p1.vx -= ax;
198 | p1.vy -= ay;
199 |
200 | p2.vx += ax;
201 | p2.vy += ay;
202 | }
203 | }
204 | })();
205 |
--------------------------------------------------------------------------------