├── LICENSE ├── README.md ├── css ├── bootstrap-theme.css ├── bootstrap-theme.css.map ├── bootstrap-theme.min.css ├── bootstrap.css ├── bootstrap.css.map ├── bootstrap.min.css ├── codemirror.css ├── main.css ├── normalize.css └── normalize.min.css ├── favicon.ico ├── fonts ├── glyphicons-halflings-regular.eot ├── glyphicons-halflings-regular.svg ├── glyphicons-halflings-regular.ttf ├── glyphicons-halflings-regular.woff └── glyphicons-halflings-regular.woff2 ├── img ├── car.png ├── difficulty1.png ├── difficulty2.png ├── difficulty3.png ├── difficulty4.png ├── level_thumbnail │ ├── Airplane.png │ ├── Ball-on-Platform.png │ ├── Driving.png │ ├── Inverted-Double-Pendulum.png │ ├── Inverted-Pendulum.png │ ├── Multirotor.png │ ├── Rocket-Landing.png │ └── Tutorial.png ├── multirotor_lowres.png ├── speedometer.png └── track.png ├── index.html ├── js ├── ImageDataCache.js ├── bootstrap.js ├── codemirror │ ├── activebookmark.js │ ├── codemirror.js │ ├── css.js │ ├── htmlmixed.js │ ├── javascript.js │ ├── matchbrackets.js │ └── xml.js ├── jquery-1.11.2.js ├── levels │ ├── AirplaneIntro.js │ ├── AirplaneLanding.js │ ├── BallOnPlatformBalance.js │ ├── BallOnPlatformBounce.js │ ├── BallOnPlatformEdgeBalance.js │ ├── CruiseControl2.js │ ├── CruiseControlIntro.js │ ├── MultirotorFlip.js │ ├── MultirotorIntro.js │ ├── MultirotorObstacles.js │ ├── RocketLandingHoverslam.js │ ├── RocketLandingMulti.js │ ├── RocketLandingNormal.js │ ├── RocketLandingUpsideDown.js │ ├── StabilizeDoublePendulum.js │ ├── StabilizeSinglePendulum.js │ ├── SwingUpDoublePendulum.js │ ├── SwingUpSinglePendulum.js │ ├── TutorialBlockOnSlope.js │ ├── TutorialBlockWithFriction.js │ ├── TutorialBlockWithoutFriction.js │ ├── VehicleRacing.js │ └── VehicleSteeringSimple.js ├── models │ ├── Airplane.js │ ├── BlockOnSlope.js │ ├── BouncingBallPlatform.js │ ├── CruiseControl.js │ ├── DoublePendulum.js │ ├── Multirotor.js │ ├── RocketLanding.js │ ├── SinglePendulum.js │ └── Vehicle.js ├── numeric-1.2.6.js ├── plugins.js ├── shortcut.js ├── ui.js └── utils.js └── misc └── doublependulum.m /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 janismac 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ControlChallenges 2 | 3 | A set of interactive control systems exercies 4 | 5 | https://janismac.github.io/ControlChallenges/ 6 | -------------------------------------------------------------------------------- /css/codemirror.css: -------------------------------------------------------------------------------- 1 | /* BASICS */ 2 | 3 | .CodeMirror { 4 | /* Set height, width, borders, and global font properties here */ 5 | font-family: monospace; 6 | height: 300px; 7 | color: black; 8 | } 9 | 10 | /* PADDING */ 11 | 12 | .CodeMirror-lines { 13 | padding: 4px 0; /* Vertical padding around content */ 14 | } 15 | .CodeMirror pre { 16 | padding: 0 4px; /* Horizontal padding of content */ 17 | } 18 | 19 | .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { 20 | background-color: white; /* The little square between H and V scrollbars */ 21 | } 22 | 23 | /* GUTTER */ 24 | 25 | .CodeMirror-gutters { 26 | border-right: 1px solid #ddd; 27 | background-color: #f7f7f7; 28 | white-space: nowrap; 29 | } 30 | .CodeMirror-linenumbers {} 31 | .CodeMirror-linenumber { 32 | padding: 0 3px 0 5px; 33 | min-width: 20px; 34 | text-align: right; 35 | color: #999; 36 | white-space: nowrap; 37 | } 38 | 39 | .CodeMirror-guttermarker { color: black; } 40 | .CodeMirror-guttermarker-subtle { color: #999; } 41 | 42 | /* CURSOR */ 43 | 44 | .CodeMirror-cursor { 45 | border-left: 1px solid black; 46 | border-right: none; 47 | width: 0; 48 | } 49 | /* Shown when moving in bi-directional text */ 50 | .CodeMirror div.CodeMirror-secondarycursor { 51 | border-left: 1px solid silver; 52 | } 53 | .cm-fat-cursor .CodeMirror-cursor { 54 | width: auto; 55 | border: 0; 56 | background: #7e7; 57 | } 58 | .cm-fat-cursor div.CodeMirror-cursors { 59 | z-index: 1; 60 | } 61 | 62 | .cm-animate-fat-cursor { 63 | width: auto; 64 | border: 0; 65 | -webkit-animation: blink 1.06s steps(1) infinite; 66 | -moz-animation: blink 1.06s steps(1) infinite; 67 | animation: blink 1.06s steps(1) infinite; 68 | background-color: #7e7; 69 | } 70 | @-moz-keyframes blink { 71 | 0% {} 72 | 50% { background-color: transparent; } 73 | 100% {} 74 | } 75 | @-webkit-keyframes blink { 76 | 0% {} 77 | 50% { background-color: transparent; } 78 | 100% {} 79 | } 80 | @keyframes blink { 81 | 0% {} 82 | 50% { background-color: transparent; } 83 | 100% {} 84 | } 85 | 86 | /* Can style cursor different in overwrite (non-insert) mode */ 87 | .CodeMirror-overwrite .CodeMirror-cursor {} 88 | 89 | .cm-tab { display: inline-block; text-decoration: inherit; } 90 | 91 | .CodeMirror-ruler { 92 | border-left: 1px solid #ccc; 93 | position: absolute; 94 | } 95 | 96 | /* DEFAULT THEME */ 97 | 98 | .cm-s-default .cm-header {color: blue;} 99 | .cm-s-default .cm-quote {color: #090;} 100 | .cm-negative {color: #d44;} 101 | .cm-positive {color: #292;} 102 | .cm-header, .cm-strong {font-weight: bold;} 103 | .cm-em {font-style: italic;} 104 | .cm-link {text-decoration: underline;} 105 | .cm-strikethrough {text-decoration: line-through;} 106 | 107 | .cm-s-default .cm-keyword {color: #708;} 108 | .cm-s-default .cm-atom {color: #219;} 109 | .cm-s-default .cm-number {color: #164;} 110 | .cm-s-default .cm-def {color: #00f;} 111 | .cm-s-default .cm-variable, 112 | .cm-s-default .cm-punctuation, 113 | .cm-s-default .cm-property, 114 | .cm-s-default .cm-operator {} 115 | .cm-s-default .cm-variable-2 {color: #05a;} 116 | .cm-s-default .cm-variable-3 {color: #085;} 117 | .cm-s-default .cm-comment {color: #a50;} 118 | .cm-s-default .cm-string {color: #a11;} 119 | .cm-s-default .cm-string-2 {color: #f50;} 120 | .cm-s-default .cm-meta {color: #555;} 121 | .cm-s-default .cm-qualifier {color: #555;} 122 | .cm-s-default .cm-builtin {color: #30a;} 123 | .cm-s-default .cm-bracket {color: #997;} 124 | .cm-s-default .cm-tag {color: #170;} 125 | .cm-s-default .cm-attribute {color: #00c;} 126 | .cm-s-default .cm-hr {color: #999;} 127 | .cm-s-default .cm-link {color: #00c;} 128 | 129 | .cm-s-default .cm-error {color: #f00;} 130 | .cm-invalidchar {color: #f00;} 131 | 132 | .CodeMirror-composing { border-bottom: 2px solid; } 133 | 134 | /* Default styles for common addons */ 135 | 136 | div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;} 137 | div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} 138 | .CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); } 139 | .CodeMirror-activeline-background {background: #e8f2ff;} 140 | 141 | /* STOP */ 142 | 143 | /* The rest of this file contains styles related to the mechanics of 144 | the editor. You probably shouldn't touch them. */ 145 | 146 | .CodeMirror { 147 | position: relative; 148 | overflow: hidden; 149 | background: white; 150 | } 151 | 152 | .CodeMirror-scroll { 153 | overflow: scroll !important; /* Things will break if this is overridden */ 154 | /* 30px is the magic margin used to hide the element's real scrollbars */ 155 | /* See overflow: hidden in .CodeMirror */ 156 | margin-bottom: -30px; margin-right: -30px; 157 | padding-bottom: 30px; 158 | height: 100%; 159 | outline: none; /* Prevent dragging from highlighting the element */ 160 | position: relative; 161 | } 162 | .CodeMirror-sizer { 163 | position: relative; 164 | border-right: 30px solid transparent; 165 | } 166 | 167 | /* The fake, visible scrollbars. Used to force redraw during scrolling 168 | before actuall scrolling happens, thus preventing shaking and 169 | flickering artifacts. */ 170 | .CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { 171 | position: absolute; 172 | z-index: 6; 173 | display: none; 174 | } 175 | .CodeMirror-vscrollbar { 176 | right: 0; top: 0; 177 | overflow-x: hidden; 178 | overflow-y: scroll; 179 | } 180 | .CodeMirror-hscrollbar { 181 | bottom: 0; left: 0; 182 | overflow-y: hidden; 183 | overflow-x: scroll; 184 | } 185 | .CodeMirror-scrollbar-filler { 186 | right: 0; bottom: 0; 187 | } 188 | .CodeMirror-gutter-filler { 189 | left: 0; bottom: 0; 190 | } 191 | 192 | .CodeMirror-gutters { 193 | position: absolute; left: 0; top: 0; 194 | z-index: 3; 195 | } 196 | .CodeMirror-gutter { 197 | white-space: normal; 198 | height: 100%; 199 | display: inline-block; 200 | margin-bottom: -30px; 201 | /* Hack to make IE7 behave */ 202 | *zoom:1; 203 | *display:inline; 204 | } 205 | .CodeMirror-gutter-wrapper { 206 | position: absolute; 207 | z-index: 4; 208 | background: none !important; 209 | border: none !important; 210 | } 211 | .CodeMirror-gutter-background { 212 | position: absolute; 213 | top: 0; bottom: 0; 214 | z-index: 4; 215 | } 216 | .CodeMirror-gutter-elt { 217 | position: absolute; 218 | cursor: default; 219 | z-index: 4; 220 | } 221 | .CodeMirror-gutter-wrapper { 222 | -webkit-user-select: none; 223 | -moz-user-select: none; 224 | user-select: none; 225 | } 226 | 227 | .CodeMirror-lines { 228 | cursor: text; 229 | min-height: 1px; /* prevents collapsing before first draw */ 230 | } 231 | .CodeMirror pre { 232 | /* Reset some styles that the rest of the page might have set */ 233 | -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; 234 | border-width: 0; 235 | background: transparent; 236 | font-family: inherit; 237 | font-size: inherit; 238 | margin: 0; 239 | white-space: pre; 240 | word-wrap: normal; 241 | line-height: inherit; 242 | color: inherit; 243 | z-index: 2; 244 | position: relative; 245 | overflow: visible; 246 | -webkit-tap-highlight-color: transparent; 247 | } 248 | .CodeMirror-wrap pre { 249 | word-wrap: break-word; 250 | white-space: pre-wrap; 251 | word-break: normal; 252 | } 253 | 254 | .CodeMirror-linebackground { 255 | position: absolute; 256 | left: 0; right: 0; top: 0; bottom: 0; 257 | z-index: 0; 258 | } 259 | 260 | .CodeMirror-linewidget { 261 | position: relative; 262 | z-index: 2; 263 | overflow: auto; 264 | } 265 | 266 | .CodeMirror-widget {} 267 | 268 | .CodeMirror-code { 269 | outline: none; 270 | } 271 | 272 | /* Force content-box sizing for the elements where we expect it */ 273 | .CodeMirror-scroll, 274 | .CodeMirror-sizer, 275 | .CodeMirror-gutter, 276 | .CodeMirror-gutters, 277 | .CodeMirror-linenumber { 278 | -moz-box-sizing: content-box; 279 | box-sizing: content-box; 280 | } 281 | 282 | .CodeMirror-measure { 283 | position: absolute; 284 | width: 100%; 285 | height: 0; 286 | overflow: hidden; 287 | visibility: hidden; 288 | } 289 | 290 | .CodeMirror-cursor { position: absolute; } 291 | .CodeMirror-measure pre { position: static; } 292 | 293 | div.CodeMirror-cursors { 294 | visibility: hidden; 295 | position: relative; 296 | z-index: 3; 297 | } 298 | div.CodeMirror-dragcursors { 299 | visibility: visible; 300 | } 301 | 302 | .CodeMirror-focused div.CodeMirror-cursors { 303 | visibility: visible; 304 | } 305 | 306 | .CodeMirror-selected { background: #d9d9d9; } 307 | .CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } 308 | .CodeMirror-crosshair { cursor: crosshair; } 309 | .CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; } 310 | .CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; } 311 | 312 | .cm-searching { 313 | background: #ffa; 314 | background: rgba(255, 255, 0, .4); 315 | } 316 | 317 | /* IE7 hack to prevent it from returning funny offsetTops on the spans */ 318 | .CodeMirror span { *vertical-align: text-bottom; } 319 | 320 | /* Used to force a border model for a node */ 321 | .cm-force-border { padding-right: .1px; } 322 | 323 | @media print { 324 | /* Hide the cursor when printing */ 325 | .CodeMirror div.CodeMirror-cursors { 326 | visibility: hidden; 327 | } 328 | } 329 | 330 | /* See issue #2901 */ 331 | .cm-tab-wrap-hack:after { content: ''; } 332 | 333 | /* Help users use markselection to safely style text background */ 334 | span.CodeMirror-selectedtext { background: none; } 335 | -------------------------------------------------------------------------------- /css/main.css: -------------------------------------------------------------------------------- 1 | /*! HTML5 Boilerplate v5.0 | MIT License | http://h5bp.com/ */ 2 | 3 | html { 4 | color: #222; 5 | font-size: 1em; 6 | line-height: 1.4; 7 | } 8 | 9 | ::-moz-selection { 10 | background: #b3d4fc; 11 | text-shadow: none; 12 | } 13 | 14 | ::selection { 15 | background: #b3d4fc; 16 | text-shadow: none; 17 | } 18 | 19 | hr { 20 | display: block; 21 | height: 1px; 22 | border: 0; 23 | border-top: 1px solid #ccc; 24 | margin: 1em 0; 25 | padding: 0; 26 | } 27 | 28 | audio, 29 | canvas, 30 | iframe, 31 | img, 32 | svg, 33 | video { 34 | vertical-align: middle; 35 | } 36 | 37 | fieldset { 38 | border: 0; 39 | margin: 0; 40 | padding: 0; 41 | } 42 | 43 | textarea { 44 | resize: vertical; 45 | } 46 | 47 | .browserupgrade { 48 | margin: 0.2em 0; 49 | background: #ccc; 50 | color: #000; 51 | padding: 0.2em 0; 52 | } 53 | 54 | 55 | /* ========================================================================== 56 | Author's custom styles 57 | ========================================================================== */ 58 | 59 | /* 60 | canvas,#left-coln,#right-coln,#info,#editor{ border:1px solid black; 61 | padding:1px; 62 | margin:1px; 63 | }*/ 64 | 65 | #left-coln { position:absolute; width:48%; left: 1%; height:96%; top:2%;} 66 | #right-coln { position:absolute; width:48%; left:51%; height:96%; top:2%;} 67 | #buttons {position:absolute; width:100%;} 68 | #variableInfo{margin-top:1em;} 69 | button.btn {margin-right:4px;} 70 | 71 | canvas{width:99%;height:99%;} 72 | body{background-color:#eef;} 73 | .CodeMirror { border: 1px solid #ddd; } 74 | .CodeMirror pre { padding-left: 7px; line-height: 1.25; } 75 | 76 | #editor .CodeMirror, #editor form { height: 100%; } 77 | 78 | #editor.smallErrorBox{height: 80%;} 79 | #errorsBox.smallErrorBox{height: 19%;} 80 | #editor.bigErrorBox{height: 32%;} 81 | #errorsBox.bigErrorBox{height: 67%;} 82 | 83 | #errorsBox { 84 | background-color: #ddf; 85 | padding:1em; 86 | margin-top:1em; 87 | } 88 | 89 | #errorsBox pre{ 90 | height: calc(100% - 2.3em); 91 | } 92 | 93 | .popup{ 94 | position:absolute; 95 | /*border: 2px solid #ddd; */ 96 | left:10%; width:80%; top:15%; height:70%; 97 | background-color:#ddd; 98 | padding:5em; 99 | border-radius:2em; 100 | z-index: 9000; 101 | } 102 | 103 | .popup h1 { text-align: center; font-size:4em; margin-top:20px; margin-bottom:20px;} 104 | .popup button{text-align: center; margin-bottom:.5em;} 105 | .popup .centered {text-align: center;margin:auto;} 106 | .popup p{font-size:1.4em; margin-bottom:.8em;} 107 | 108 | .popup .closeButton {float:right;} 109 | 110 | #levelMenuPopup { 111 | padding:2em; 112 | left:10%; width:80%; top:10%; height:80%; 113 | } 114 | 115 | #levelMenuPopup h1 { 116 | margin: .2em 0; 117 | } 118 | 119 | #levelList { 120 | overflow-y: auto; 121 | height: calc(100% - 40px - 4em); 122 | text-align: center; 123 | } 124 | 125 | .level-group { 126 | /*border: 2px solid #f00;*/ 127 | display: inline-block; 128 | vertical-align: top; 129 | } 130 | 131 | .level-group > div { 132 | /*border: 2px solid #00f;*/ 133 | width: 28em; 134 | margin: 2em; 135 | padding: 1em; 136 | background: #A0B1C9; 137 | border-radius:.5em; 138 | } 139 | 140 | .level-group > div > img { 141 | width: 19em; 142 | padding: 1em; 143 | margin: 0.4em; 144 | } 145 | 146 | .level-btn { 147 | width: 21em; 148 | height: 3em; 149 | margin: 0.4em; 150 | } 151 | 152 | .level-btn div { 153 | text-align: left; 154 | } 155 | 156 | .difficulty-icon { 157 | width: 1.6em; 158 | height: 1.6em; 159 | margin: 0 0.4em 0 0; 160 | } 161 | 162 | /* ========================================================================== 163 | Media Queries 164 | ========================================================================== */ 165 | 166 | @media only screen and (min-width: 35em) { 167 | 168 | } 169 | 170 | @media print, 171 | (-o-min-device-pixel-ratio: 5/4), 172 | (-webkit-min-device-pixel-ratio: 1.25), 173 | (min-resolution: 120dpi) { 174 | 175 | } 176 | 177 | /* ========================================================================== 178 | Helper classes 179 | ========================================================================== */ 180 | 181 | .hidden { 182 | display: none !important; 183 | visibility: hidden; 184 | } 185 | 186 | .visuallyhidden { 187 | border: 0; 188 | clip: rect(0 0 0 0); 189 | height: 1px; 190 | margin: -1px; 191 | overflow: hidden; 192 | padding: 0; 193 | position: absolute; 194 | width: 1px; 195 | } 196 | 197 | .visuallyhidden.focusable:active, 198 | .visuallyhidden.focusable:focus { 199 | clip: auto; 200 | height: auto; 201 | margin: 0; 202 | overflow: visible; 203 | position: static; 204 | width: auto; 205 | } 206 | 207 | .invisible { 208 | visibility: hidden; 209 | } 210 | 211 | .clearfix:before, 212 | .clearfix:after { 213 | content: " "; 214 | display: table; 215 | } 216 | 217 | .clearfix:after { 218 | clear: both; 219 | } 220 | 221 | .clearfix { 222 | *zoom: 1; 223 | } 224 | 225 | /* ========================================================================== 226 | Print styles 227 | ========================================================================== */ 228 | 229 | @media print { 230 | *, 231 | *:before, 232 | *:after { 233 | background: transparent !important; 234 | color: #000 !important; 235 | box-shadow: none !important; 236 | text-shadow: none !important; 237 | } 238 | 239 | a, 240 | a:visited { 241 | text-decoration: underline; 242 | } 243 | 244 | a[href]:after { 245 | content: " (" attr(href) ")"; 246 | } 247 | 248 | abbr[title]:after { 249 | content: " (" attr(title) ")"; 250 | } 251 | 252 | a[href^="#"]:after, 253 | a[href^="javascript:"]:after { 254 | content: ""; 255 | } 256 | 257 | pre, 258 | blockquote { 259 | border: 1px solid #999; 260 | page-break-inside: avoid; 261 | } 262 | 263 | thead { 264 | display: table-header-group; 265 | } 266 | 267 | tr, 268 | img { 269 | page-break-inside: avoid; 270 | } 271 | 272 | img { 273 | max-width: 100% !important; 274 | } 275 | 276 | p, 277 | h2, 278 | h3 { 279 | orphans: 3; 280 | widows: 3; 281 | } 282 | 283 | h2, 284 | h3 { 285 | page-break-after: avoid; 286 | } 287 | } -------------------------------------------------------------------------------- /css/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling 6 | * user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 29 | * and Firefox. 30 | * Correct `block` display not defined for `main` in IE 11. 31 | */ 32 | 33 | article, 34 | aside, 35 | details, 36 | figcaption, 37 | figure, 38 | footer, 39 | header, 40 | hgroup, 41 | main, 42 | menu, 43 | nav, 44 | section, 45 | summary { 46 | display: block; 47 | } 48 | 49 | /** 50 | * 1. Correct `inline-block` display not defined in IE 8/9. 51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 52 | */ 53 | 54 | audio, 55 | canvas, 56 | progress, 57 | video { 58 | display: inline-block; /* 1 */ 59 | vertical-align: baseline; /* 2 */ 60 | } 61 | 62 | /** 63 | * Prevent modern browsers from displaying `audio` without controls. 64 | * Remove excess height in iOS 5 devices. 65 | */ 66 | 67 | audio:not([controls]) { 68 | display: none; 69 | height: 0; 70 | } 71 | 72 | /** 73 | * Address `[hidden]` styling not present in IE 8/9/10. 74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 75 | */ 76 | 77 | [hidden], 78 | template { 79 | display: none; 80 | } 81 | 82 | /* Links 83 | ========================================================================== */ 84 | 85 | /** 86 | * Remove the gray background color from active links in IE 10. 87 | */ 88 | 89 | a { 90 | background-color: transparent; 91 | } 92 | 93 | /** 94 | * Improve readability when focused and also mouse hovered in all browsers. 95 | */ 96 | 97 | a:active, 98 | a:hover { 99 | outline: 0; 100 | } 101 | 102 | /* Text-level semantics 103 | ========================================================================== */ 104 | 105 | /** 106 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 107 | */ 108 | 109 | abbr[title] { 110 | border-bottom: 1px dotted; 111 | } 112 | 113 | /** 114 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 115 | */ 116 | 117 | b, 118 | strong { 119 | font-weight: bold; 120 | } 121 | 122 | /** 123 | * Address styling not present in Safari and Chrome. 124 | */ 125 | 126 | dfn { 127 | font-style: italic; 128 | } 129 | 130 | /** 131 | * Address variable `h1` font-size and margin within `section` and `article` 132 | * contexts in Firefox 4+, Safari, and Chrome. 133 | */ 134 | 135 | h1 { 136 | font-size: 2em; 137 | margin: 0.67em 0; 138 | } 139 | 140 | /** 141 | * Address styling not present in IE 8/9. 142 | */ 143 | 144 | mark { 145 | background: #ff0; 146 | color: #000; 147 | } 148 | 149 | /** 150 | * Address inconsistent and variable font size in all browsers. 151 | */ 152 | 153 | small { 154 | font-size: 80%; 155 | } 156 | 157 | /** 158 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 159 | */ 160 | 161 | sub, 162 | sup { 163 | font-size: 75%; 164 | line-height: 0; 165 | position: relative; 166 | vertical-align: baseline; 167 | } 168 | 169 | sup { 170 | top: -0.5em; 171 | } 172 | 173 | sub { 174 | bottom: -0.25em; 175 | } 176 | 177 | /* Embedded content 178 | ========================================================================== */ 179 | 180 | /** 181 | * Remove border when inside `a` element in IE 8/9/10. 182 | */ 183 | 184 | img { 185 | border: 0; 186 | } 187 | 188 | /** 189 | * Correct overflow not hidden in IE 9/10/11. 190 | */ 191 | 192 | svg:not(:root) { 193 | overflow: hidden; 194 | } 195 | 196 | /* Grouping content 197 | ========================================================================== */ 198 | 199 | /** 200 | * Address margin not present in IE 8/9 and Safari. 201 | */ 202 | 203 | figure { 204 | margin: 1em 40px; 205 | } 206 | 207 | /** 208 | * Address differences between Firefox and other browsers. 209 | */ 210 | 211 | hr { 212 | -moz-box-sizing: content-box; 213 | box-sizing: content-box; 214 | height: 0; 215 | } 216 | 217 | /** 218 | * Contain overflow in all browsers. 219 | */ 220 | 221 | pre { 222 | overflow: auto; 223 | } 224 | 225 | /** 226 | * Address odd `em`-unit font size rendering in all browsers. 227 | */ 228 | 229 | code, 230 | kbd, 231 | pre, 232 | samp { 233 | font-family: monospace, monospace; 234 | font-size: 1em; 235 | } 236 | 237 | /* Forms 238 | ========================================================================== */ 239 | 240 | /** 241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 242 | * styling of `select`, unless a `border` property is set. 243 | */ 244 | 245 | /** 246 | * 1. Correct color not being inherited. 247 | * Known issue: affects color of disabled elements. 248 | * 2. Correct font properties not being inherited. 249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 250 | */ 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | color: inherit; /* 1 */ 258 | font: inherit; /* 2 */ 259 | margin: 0; /* 3 */ 260 | } 261 | 262 | /** 263 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 264 | */ 265 | 266 | button { 267 | overflow: visible; 268 | } 269 | 270 | /** 271 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 272 | * All other form control elements do not inherit `text-transform` values. 273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 274 | * Correct `select` style inheritance in Firefox. 275 | */ 276 | 277 | button, 278 | select { 279 | text-transform: none; 280 | } 281 | 282 | /** 283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 284 | * and `video` controls. 285 | * 2. Correct inability to style clickable `input` types in iOS. 286 | * 3. Improve usability and consistency of cursor style between image-type 287 | * `input` and others. 288 | */ 289 | 290 | button, 291 | html input[type="button"], /* 1 */ 292 | input[type="reset"], 293 | input[type="submit"] { 294 | -webkit-appearance: button; /* 2 */ 295 | cursor: pointer; /* 3 */ 296 | } 297 | 298 | /** 299 | * Re-set default cursor for disabled elements. 300 | */ 301 | 302 | button[disabled], 303 | html input[disabled] { 304 | cursor: default; 305 | } 306 | 307 | /** 308 | * Remove inner padding and border in Firefox 4+. 309 | */ 310 | 311 | button::-moz-focus-inner, 312 | input::-moz-focus-inner { 313 | border: 0; 314 | padding: 0; 315 | } 316 | 317 | /** 318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 319 | * the UA stylesheet. 320 | */ 321 | 322 | input { 323 | line-height: normal; 324 | } 325 | 326 | /** 327 | * It's recommended that you don't attempt to style these elements. 328 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 329 | * 330 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 331 | * 2. Remove excess padding in IE 8/9/10. 332 | */ 333 | 334 | input[type="checkbox"], 335 | input[type="radio"] { 336 | box-sizing: border-box; /* 1 */ 337 | padding: 0; /* 2 */ 338 | } 339 | 340 | /** 341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 342 | * `font-size` values of the `input`, it causes the cursor style of the 343 | * decrement button to change from `default` to `text`. 344 | */ 345 | 346 | input[type="number"]::-webkit-inner-spin-button, 347 | input[type="number"]::-webkit-outer-spin-button { 348 | height: auto; 349 | } 350 | 351 | /** 352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 354 | * (include `-moz` to future-proof). 355 | */ 356 | 357 | input[type="search"] { 358 | -webkit-appearance: textfield; /* 1 */ 359 | -moz-box-sizing: content-box; 360 | -webkit-box-sizing: content-box; /* 2 */ 361 | box-sizing: content-box; 362 | } 363 | 364 | /** 365 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 366 | * Safari (but not Chrome) clips the cancel button when the search input has 367 | * padding (and `textfield` appearance). 368 | */ 369 | 370 | input[type="search"]::-webkit-search-cancel-button, 371 | input[type="search"]::-webkit-search-decoration { 372 | -webkit-appearance: none; 373 | } 374 | 375 | /** 376 | * Define consistent border, margin, and padding. 377 | */ 378 | 379 | fieldset { 380 | border: 1px solid #c0c0c0; 381 | margin: 0 2px; 382 | padding: 0.35em 0.625em 0.75em; 383 | } 384 | 385 | /** 386 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 387 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 388 | */ 389 | 390 | legend { 391 | border: 0; /* 1 */ 392 | padding: 0; /* 2 */ 393 | } 394 | 395 | /** 396 | * Remove default vertical scrollbar in IE 8/9/10/11. 397 | */ 398 | 399 | textarea { 400 | overflow: auto; 401 | } 402 | 403 | /** 404 | * Don't inherit the `font-weight` (applied by a rule above). 405 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 406 | */ 407 | 408 | optgroup { 409 | font-weight: bold; 410 | } 411 | 412 | /* Tables 413 | ========================================================================== */ 414 | 415 | /** 416 | * Remove most spacing between table cells. 417 | */ 418 | 419 | table { 420 | border-collapse: collapse; 421 | border-spacing: 0; 422 | } 423 | 424 | td, 425 | th { 426 | padding: 0; 427 | } 428 | -------------------------------------------------------------------------------- /css/normalize.min.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0} -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janismac/ControlChallenges/ac0e6a83d2613fce80b2a144f0eaa00d34cd84d2/favicon.ico -------------------------------------------------------------------------------- /fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janismac/ControlChallenges/ac0e6a83d2613fce80b2a144f0eaa00d34cd84d2/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janismac/ControlChallenges/ac0e6a83d2613fce80b2a144f0eaa00d34cd84d2/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janismac/ControlChallenges/ac0e6a83d2613fce80b2a144f0eaa00d34cd84d2/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janismac/ControlChallenges/ac0e6a83d2613fce80b2a144f0eaa00d34cd84d2/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /img/car.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janismac/ControlChallenges/ac0e6a83d2613fce80b2a144f0eaa00d34cd84d2/img/car.png -------------------------------------------------------------------------------- /img/difficulty1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janismac/ControlChallenges/ac0e6a83d2613fce80b2a144f0eaa00d34cd84d2/img/difficulty1.png -------------------------------------------------------------------------------- /img/difficulty2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janismac/ControlChallenges/ac0e6a83d2613fce80b2a144f0eaa00d34cd84d2/img/difficulty2.png -------------------------------------------------------------------------------- /img/difficulty3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janismac/ControlChallenges/ac0e6a83d2613fce80b2a144f0eaa00d34cd84d2/img/difficulty3.png -------------------------------------------------------------------------------- /img/difficulty4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janismac/ControlChallenges/ac0e6a83d2613fce80b2a144f0eaa00d34cd84d2/img/difficulty4.png -------------------------------------------------------------------------------- /img/level_thumbnail/Airplane.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janismac/ControlChallenges/ac0e6a83d2613fce80b2a144f0eaa00d34cd84d2/img/level_thumbnail/Airplane.png -------------------------------------------------------------------------------- /img/level_thumbnail/Ball-on-Platform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janismac/ControlChallenges/ac0e6a83d2613fce80b2a144f0eaa00d34cd84d2/img/level_thumbnail/Ball-on-Platform.png -------------------------------------------------------------------------------- /img/level_thumbnail/Driving.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janismac/ControlChallenges/ac0e6a83d2613fce80b2a144f0eaa00d34cd84d2/img/level_thumbnail/Driving.png -------------------------------------------------------------------------------- /img/level_thumbnail/Inverted-Double-Pendulum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janismac/ControlChallenges/ac0e6a83d2613fce80b2a144f0eaa00d34cd84d2/img/level_thumbnail/Inverted-Double-Pendulum.png -------------------------------------------------------------------------------- /img/level_thumbnail/Inverted-Pendulum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janismac/ControlChallenges/ac0e6a83d2613fce80b2a144f0eaa00d34cd84d2/img/level_thumbnail/Inverted-Pendulum.png -------------------------------------------------------------------------------- /img/level_thumbnail/Multirotor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janismac/ControlChallenges/ac0e6a83d2613fce80b2a144f0eaa00d34cd84d2/img/level_thumbnail/Multirotor.png -------------------------------------------------------------------------------- /img/level_thumbnail/Rocket-Landing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janismac/ControlChallenges/ac0e6a83d2613fce80b2a144f0eaa00d34cd84d2/img/level_thumbnail/Rocket-Landing.png -------------------------------------------------------------------------------- /img/level_thumbnail/Tutorial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janismac/ControlChallenges/ac0e6a83d2613fce80b2a144f0eaa00d34cd84d2/img/level_thumbnail/Tutorial.png -------------------------------------------------------------------------------- /img/multirotor_lowres.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janismac/ControlChallenges/ac0e6a83d2613fce80b2a144f0eaa00d34cd84d2/img/multirotor_lowres.png -------------------------------------------------------------------------------- /img/speedometer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janismac/ControlChallenges/ac0e6a83d2613fce80b2a144f0eaa00d34cd84d2/img/speedometer.png -------------------------------------------------------------------------------- /img/track.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janismac/ControlChallenges/ac0e6a83d2613fce80b2a144f0eaa00d34cd84d2/img/track.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |20 | 21 | 22 | Errors & Warnings 23 |
24 | 25 |[Description]
47 |Note: Your code for each level is saved in your browser.
48 | 49 |You solved the level in seconds.
59 | 60 | 61 |You can view your own variables by calling:
monitor('name',value);71 |
If you need persistent state, you can use global variables. You will have to declare them outside your function with the 'var' keyword.
72 |Your code for each level is saved in your browser.
73 |[{x:-2,y:0},{x:-1,y:0},{x:-1,y:2},{x:0,y:2},{x:0,y:0}]"; 12 | ImageDataCache.load('img/multirotor_lowres.png'); 13 | this.model = new Models.Multirotor({imgURL:'img/multirotor_lowres.png', theta: 0,dtheta: 0,x: -2,dx: 0,y: 1,dy: 0}); 14 | } 15 | 16 | 17 | Levels.MultirotorObstacles.prototype.levelComplete = function(){ 18 | return Math.abs(this.model.x) < 0.01 19 | && Math.abs(this.model.y) < 0.01 20 | && Math.abs(this.model.dx) < 0.01 21 | && Math.abs(this.model.dy) < 0.01 22 | && Math.abs(this.model.theta) < 0.01 23 | && Math.abs(this.model.dtheta) < 0.01; 24 | } 25 | 26 | Levels.MultirotorObstacles.prototype.levelFailed = function(){ 27 | return (Math.abs(this.model.x+1.5) < 0.1 && this.model.y > 0.8) 28 | || (Math.abs(this.model.x+0.5) < 0.1 && this.model.y < 1.2); 29 | } 30 | 31 | Levels.MultirotorObstacles.prototype.simulate = function (dt, controlFunc) 32 | { this.model.simulate (dt, controlFunc); } 33 | 34 | Levels.MultirotorObstacles.prototype.getSimulationTime = function() {return this.model.T;} 35 | 36 | Levels.MultirotorObstacles.prototype.draw = function(ctx, canvas) { 37 | ctx.scale(2,2); 38 | ctx.translate(1,-1); 39 | ctx.fillStyle="#88ff88"; 40 | var s = this.model.Length; 41 | ctx.fillRect(-2*s,-.8*s,4*s,1.6*s); 42 | 43 | 44 | ctx.fillStyle="#883300"; 45 | var d = 0.1; 46 | ctx.fillRect(-1.5-d,.5-d,2*d,20); 47 | ctx.fillRect(-0.5-d,1.5+d-20,2*d,20); 48 | 49 | this.model.drawVehicle(ctx, canvas); 50 | 51 | if(this.levelFailed()){ 52 | ctx.save(); 53 | ctx.translate(-1,0); 54 | ctx.scale(0.005,-0.005); 55 | ctx.font="10px Verdana"; 56 | ctx.textAlign="center"; 57 | ctx.fillStyle="#990000"; 58 | ctx.fillText("CRASHED!",0,0); 59 | ctx.restore(); 60 | } 61 | } 62 | 63 | Levels.MultirotorObstacles.prototype.infoText = function(ctx, canvas){return this.model.infoText();} -------------------------------------------------------------------------------- /js/levels/RocketLandingHoverslam.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | if (typeof Levels === 'undefined') var Levels = {}; 3 | 4 | Levels.RocketLandingHoverslam = function() 5 | { 6 | this.name = "RocketLandingHoverslam"; 7 | this.title = "Rocket Landing Hoverslam"; 8 | this.boilerPlateCode = "function controlFunction(rocket)\n{\n return {throttle:1,gimbalAngle:0};\n}"; 9 | this.sampleSolution = "function controlFunction(rocket)\n{\n // Horizontal position control\n var x_target = 0.0;\n var dx_target = 0.2 * (x_target - rocket.x);\n if(Math.abs(x_target - rocket.x) < 20.0) dx_target = 0.0;\n var theta_target = 0.05 * (dx_target - rocket.dx);\n \n // Pitch control with limits on angle and angular rate\n theta_target = Math.max(-0.6, Math.min(0.6, theta_target));\n var dtheta_target = 1.0 * (theta_target - rocket.theta);\n dtheta_target = Math.max(-1.0, Math.min(1.0, dtheta_target));\n var gimbalAngle = 10.0 * (rocket.dtheta - dtheta_target);\n \n // Vertical speed profile for constant acceleration landing\n var dy_target = -Math.sqrt(10.0 * Math.max(1e-6, rocket.y - 25));\n \n // Maintain height until positioned over the landing pad\n if(Math.abs(rocket.x) > 35.0 || Math.abs(rocket.dx) > 12.0) dy_target = 0.5;\n \n // Vertical speed control\n var throttle = 0.5 + 0.5 * (dy_target - rocket.dy);\n \n // Set high throttle while spinning to guarantee control authority\n if(Math.abs(rocket.theta) > 0.6 || Math.abs(rocket.dtheta) > 0.5) throttle = 0.9;\n throttle = Math.max(0.25, throttle);\n \n return {throttle:throttle, gimbalAngle:gimbalAngle};\n}"; 10 | this.difficultyRating = 3; 11 | this.description = "In this level the rocket can not hover, because the engines can not throttle low enough. The thrust-to-weight ratio can range from 1.2 to 2 (throttle of 0.6 to 1)."; 12 | this.model = new Models.RocketLanding({TWR: 2,theta: -0.1,dtheta: 0,Length: 40,Width: 5,x: -80,dx: 0,y: 400,dy: -70,T: 0,throttleLimit: .6}); 13 | } 14 | 15 | Levels.RocketLandingHoverslam.prototype.levelComplete = function(){return this.model.landed();} 16 | 17 | 18 | Levels.RocketLandingHoverslam.prototype.levelFailed = function(){return this.model.crashed();} 19 | 20 | Levels.RocketLandingHoverslam.prototype.simulate = function (dt, controlFunc) 21 | { 22 | this.model.simulate (dt, controlFunc); 23 | } 24 | 25 | Levels.RocketLandingHoverslam.prototype.getSimulationTime = function() {return this.model.T;} 26 | 27 | Levels.RocketLandingHoverslam.prototype.draw = function(ctx, canvas){this.model.draw(ctx, canvas);} 28 | 29 | Levels.RocketLandingHoverslam.prototype.infoText = function(ctx, canvas){return this.model.infoText();} -------------------------------------------------------------------------------- /js/levels/RocketLandingMulti.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | if (typeof Levels === 'undefined') var Levels = {}; 3 | 4 | Levels.RocketLandingMulti = function() 5 | { 6 | this.name = "RocketLandingMulti"; 7 | this.title = "Multiple Rocket Landing"; 8 | this.boilerPlateCode = "function controlFunction(rocket)\n{\n return {throttle:1,gimbalAngle:-0.2};\n}"; 9 | this.sampleSolution = "function controlFunction(rocket)\n{\n // Horizontal position control\n var x_target = 0.0;\n var dx_target = 0.2 * (x_target - rocket.x);\n if(Math.abs(x_target - rocket.x) < 20.0) dx_target = 0.0;\n var theta_target = 0.05 * (dx_target - rocket.dx);\n \n // Pitch control with limits on angle and angular rate\n theta_target = Math.max(-0.6, Math.min(0.6, theta_target));\n var dtheta_target = 1.0 * (theta_target - rocket.theta);\n dtheta_target = Math.max(-1.0, Math.min(1.0, dtheta_target));\n var gimbalAngle = 10.0 * (rocket.dtheta - dtheta_target);\n \n // Vertical speed profile for constant acceleration landing\n var dy_target = -Math.sqrt(10.0 * Math.max(1e-6, rocket.y - 25));\n \n // Maintain height until positioned over the landing pad\n if(Math.abs(rocket.x) > 35.0 || Math.abs(rocket.dx) > 12.0) dy_target = 0.5;\n \n // Vertical speed control\n var throttle = 0.5 + 0.5 * (dy_target - rocket.dy);\n \n // Set high throttle while spinning to guarantee control authority\n if(Math.abs(rocket.theta) > 0.6 || Math.abs(rocket.dtheta) > 0.5) throttle = 0.9;\n throttle = Math.max(0.25, throttle);\n \n return {throttle:throttle, gimbalAngle:gimbalAngle};\n}"; 10 | this.difficultyRating = 3; 11 | this.description = "Now there are multiple rockets. The point of this level is to see if the same controller can land the rocket from different initial conditions. Collision avoidance between the different rockets is not necessary. They pass through each other."; 12 | this.models = []; 13 | this.models.push(new Models.RocketLanding({TWR: 2,theta: 0,dtheta: -0.1,Length: 40,Width: 5,x: 100,dx: 20,y: 300,dy: -10,T: 0})); 14 | this.models.push(new Models.RocketLanding({TWR: 2,theta: 0,dtheta: 0, Length: 40,Width: 5,x: -180,dx: 50,y: 60,dy: 0,T: 0})); 15 | this.models.push(new Models.RocketLanding({TWR: 2,theta: 3.14,dtheta: -2, Length: 40,Width: 5,x: -180,dx: 0,y: 300,dy: 0,T: 0})); 16 | } 17 | 18 | 19 | Levels.RocketLandingMulti.prototype.levelComplete = function(){return this.models.every(function(m){return m.landed();});} 20 | 21 | Levels.RocketLandingMulti.prototype.levelFailed = function(){return this.models.some(function(m){return m.crashed();});} 22 | 23 | Levels.RocketLandingMulti.prototype.simulate = function (dt, controlFunc) 24 | { 25 | for (var i = 0; i < this.models.length; i++) 26 | this.models[i].simulate (dt, controlFunc); 27 | } 28 | 29 | Levels.RocketLandingMulti.prototype.getSimulationTime = function() {return this.models[0].T;} 30 | 31 | Levels.RocketLandingMulti.prototype.draw = function (ctx, canvas) 32 | { 33 | ctx.scale(0.014,0.014); 34 | ctx.translate(0,-250); 35 | 36 | for (var i = 0; i < this.models.length; i++){ 37 | this.models[i].drawRocket(ctx, canvas, i); 38 | this.models[i].drawGround(ctx, canvas); 39 | } 40 | 41 | if(this.levelFailed()) 42 | { 43 | ctx.save(); 44 | ctx.scale(1,-1); 45 | ctx.font="10px Verdana"; 46 | ctx.textAlign="center"; 47 | ctx.fillStyle="#990000"; 48 | ctx.fillText("CRASHED!",0,-80); 49 | ctx.restore(); 50 | } 51 | } 52 | 53 | Levels.RocketLandingMulti.prototype.infoText = function(ctx, canvas){ 54 | var s = ''; 55 | for (var i = 0; i < this.models.length; i++) { 56 | if (i!=0) s+="\n"; 57 | s += "Rocket #" + i + "\n"; 58 | s += this.models[i].infoText(); 59 | } 60 | return s; 61 | } -------------------------------------------------------------------------------- /js/levels/RocketLandingNormal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | if (typeof Levels === 'undefined') var Levels = {}; 3 | 4 | Levels.RocketLandingNormal = function() 5 | { 6 | this.name = "RocketLandingNormal"; 7 | this.title = "Rocket Landing"; 8 | this.boilerPlateCode = "function controlFunction(rocket)\n{\n return {throttle:1,gimbalAngle:-0.2};\n}"; 9 | this.sampleSolution = "function controlFunction(rocket)\n{\n // Horizontal position control\n var x_target = 0.0;\n var dx_target = 0.2 * (x_target - rocket.x);\n if(Math.abs(x_target - rocket.x) < 20.0) dx_target = 0.0;\n var theta_target = 0.05 * (dx_target - rocket.dx);\n \n // Pitch control with limits on angle and angular rate\n theta_target = Math.max(-0.6, Math.min(0.6, theta_target));\n var dtheta_target = 1.0 * (theta_target - rocket.theta);\n dtheta_target = Math.max(-1.0, Math.min(1.0, dtheta_target));\n var gimbalAngle = 10.0 * (rocket.dtheta - dtheta_target);\n \n // Vertical speed profile for constant acceleration landing\n var dy_target = -Math.sqrt(10.0 * Math.max(1e-6, rocket.y - 25));\n \n // Maintain height until positioned over the landing pad\n if(Math.abs(rocket.x) > 35.0 || Math.abs(rocket.dx) > 12.0) dy_target = 0.5;\n \n // Vertical speed control\n var throttle = 0.5 + 0.5 * (dy_target - rocket.dy);\n \n // Set high throttle while spinning to guarantee control authority\n if(Math.abs(rocket.theta) > 0.6 || Math.abs(rocket.dtheta) > 0.5) throttle = 0.9;\n throttle = Math.max(0.25, throttle);\n \n return {throttle:throttle, gimbalAngle:gimbalAngle};\n}"; 10 | this.difficultyRating = 2; 11 | this.description = "Land the rocket in the landing zone. The center of the landing zone has the coordinates (x, y) = (0, 0). Steer the rocket by calculating the engine thrust (range 0 to 1) and engine steering angle (range -0.2 to 0.2 radians). The rocket has a thrust to weight ratio of 2. A throttle value of 0.5 can make it hover. Touch down gently (less than 5 m/s)."; 12 | this.model = new Models.RocketLanding({TWR: 2,theta: -0.3,dtheta: 0,Length: 40,Width: 5,x: -100,dx: 0,y: 200,dy: -20,T: 0}); 13 | } 14 | 15 | 16 | Levels.RocketLandingNormal.prototype.levelComplete = function(){return this.model.landed();} 17 | 18 | 19 | Levels.RocketLandingNormal.prototype.levelFailed = function(){return this.model.crashed();} 20 | 21 | Levels.RocketLandingNormal.prototype.simulate = function (dt, controlFunc) 22 | { 23 | this.model.simulate (dt, controlFunc); 24 | } 25 | 26 | Levels.RocketLandingNormal.prototype.getSimulationTime = function() {return this.model.T;} 27 | 28 | Levels.RocketLandingNormal.prototype.draw = function(ctx, canvas){this.model.draw(ctx, canvas);} 29 | 30 | Levels.RocketLandingNormal.prototype.infoText = function(ctx, canvas){return this.model.infoText();} -------------------------------------------------------------------------------- /js/levels/RocketLandingUpsideDown.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | if (typeof Levels === 'undefined') var Levels = {}; 3 | 4 | Levels.RocketLandingUpsideDown = function() 5 | { 6 | this.name = "RocketLandingUpsideDown"; 7 | this.title = "Rocket Landing Upside Down"; 8 | this.boilerPlateCode = "function controlFunction(rocket)\n{\n return {throttle:1,gimbalAngle:-0.2};\n}"; 9 | this.sampleSolution = "function controlFunction(rocket)\n{\n // Horizontal position control\n var x_target = 0.0;\n var dx_target = 0.2 * (x_target - rocket.x);\n if(Math.abs(x_target - rocket.x) < 20.0) dx_target = 0.0;\n var theta_target = 0.05 * (dx_target - rocket.dx);\n \n // Pitch control with limits on angle and angular rate\n theta_target = Math.max(-0.6, Math.min(0.6, theta_target));\n var dtheta_target = 1.0 * (theta_target - rocket.theta);\n dtheta_target = Math.max(-1.0, Math.min(1.0, dtheta_target));\n var gimbalAngle = 10.0 * (rocket.dtheta - dtheta_target);\n \n // Vertical speed profile for constant acceleration landing\n var dy_target = -Math.sqrt(10.0 * Math.max(1e-6, rocket.y - 25));\n \n // Maintain height until positioned over the landing pad\n if(Math.abs(rocket.x) > 35.0 || Math.abs(rocket.dx) > 12.0) dy_target = 0.5;\n \n // Vertical speed control\n var throttle = 0.5 + 0.5 * (dy_target - rocket.dy);\n \n // Set high throttle while spinning to guarantee control authority\n if(Math.abs(rocket.theta) > 0.6 || Math.abs(rocket.dtheta) > 0.5) throttle = 0.9;\n throttle = Math.max(0.25, throttle);\n \n return {throttle:throttle, gimbalAngle:gimbalAngle};\n}"; 10 | this.difficultyRating = 3; 11 | this.description = "Someone managed to turn the rocket upside down... can you rescue it?"; 12 | this.resetModel(); 13 | } 14 | 15 | 16 | Levels.RocketLandingUpsideDown.prototype.levelComplete = function(){return this.model.landed();} 17 | 18 | 19 | Levels.RocketLandingUpsideDown.prototype.levelFailed = function(){return this.model.crashed();} 20 | 21 | Levels.RocketLandingUpsideDown.prototype.simulate = function (dt, controlFunc) 22 | { 23 | this.model.simulate (dt, controlFunc); 24 | } 25 | 26 | Levels.RocketLandingUpsideDown.prototype.getSimulationTime = function() {return this.model.T;} 27 | 28 | Levels.RocketLandingUpsideDown.prototype.resetModel = function() 29 | { 30 | this.model = new Models.RocketLanding({TWR: 2,theta: -3.14,dtheta: 0,Length: 40,Width: 5,x: 50,dx: 20,y: 200,dy: 60,T: 0}); 31 | } 32 | 33 | Levels.RocketLandingUpsideDown.prototype.draw = function(ctx, canvas){this.model.draw(ctx, canvas);} 34 | 35 | Levels.RocketLandingUpsideDown.prototype.infoText = function(ctx, canvas){return this.model.infoText();} -------------------------------------------------------------------------------- /js/levels/StabilizeDoublePendulum.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | if (typeof Levels === 'undefined') var Levels = {}; 3 | 4 | Levels.StabilizeDoublePendulum = function() 5 | { 6 | this.name = "StabilizeDoublePendulum"; 7 | this.title = "Inverted Double Pendulum: Stabilize"; 8 | this.sampleSolution = "function controlFunction(p)\n{\n // Controller coefficients optimized using this MATLAB script:\n // https://github.com/janismac/ControlChallenges/blob/gh-pages/misc/doublependulum.m \n return 18.961273*p.dtheta1 - 352.00565*p.dtheta2 - 98.065915*p.dx + 492.5419*p.theta1 - 764.62411*p.theta2 - 31.622777*p.x;\n}"; 9 | this.boilerPlateCode = "function controlFunction(pendulum)\n{\n return 10*Math.sin(8*pendulum.T);\n}"; 10 | this.difficultyRating = 3; 11 | this.description = "Stabilize the double pendulum so that it stays upright, moves to the center (x=0) and stays there in perfect balance. Calculate the horizontal force on the cart necessary to achieve this."; 12 | this.model = new Models.DoublePendulum({m0: 10,m1: 2,m2: 4,L1: 0.618,L2: 1,g: 2,theta1: -0.05,dtheta1: 0,theta2: 0,dtheta2: 0.05,x: -1,dx: 0,F: 0,T: 0}); 13 | } 14 | 15 | 16 | Levels.StabilizeDoublePendulum.prototype.levelComplete = function() 17 | { 18 | return Math.abs(this.model.x) < 0.01 19 | && Math.abs(this.model.dx) < 0.01 20 | && Math.abs(this.model.dtheta1) < 0.01 21 | && Math.abs(Math.sin(this.model.theta1)) < 0.001 22 | && Math.cos(this.model.theta1) > 0.999 23 | && Math.abs(this.model.dtheta2) < 0.01 24 | && Math.abs(Math.sin(this.model.theta2)) < 0.001 25 | && Math.cos(this.model.theta2) > 0.999; 26 | } 27 | 28 | Levels.StabilizeDoublePendulum.prototype.levelFailed = function() 29 | { 30 | return false; 31 | } 32 | 33 | 34 | Levels.StabilizeDoublePendulum.prototype.simulate = function (dt, controlFunc) 35 | { 36 | this.model.simulate (dt, controlFunc); 37 | } 38 | 39 | Levels.StabilizeDoublePendulum.prototype.getSimulationTime = function() {return this.model.T;} 40 | 41 | Levels.StabilizeDoublePendulum.prototype.draw = function(ctx, canvas){this.model.draw(ctx, canvas);} 42 | 43 | Levels.StabilizeDoublePendulum.prototype.infoText = function(ctx, canvas){return this.model.infoText();} -------------------------------------------------------------------------------- /js/levels/StabilizeSinglePendulum.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | if (typeof Levels === 'undefined') var Levels = {}; 3 | 4 | Levels.StabilizeSinglePendulum = function() 5 | { 6 | this.name = "StabilizeSinglePendulum"; 7 | this.title = "Inverted Pendulum: Stabilize"; 8 | this.sampleSolution = "function controlFunction(pendulum)\n{\n let L = 1.0;\n let v_pendulum = pendulum.dx + L * pendulum.dtheta;\n let v_pendulum_cmd = -0.6 * pendulum.x;\n monitor('v_pendulum', v_pendulum);\n let theta_cmd = Math.min(0.07, Math.max(-0.07, 0.3 * (v_pendulum_cmd - v_pendulum)));\n return 200 * pendulum.dtheta + 1500 * (Math.sin(pendulum.theta) - theta_cmd);\n}\n"; 9 | this.boilerPlateCode = "function controlFunction(pendulum)\n{\n return 10*Math.sin(8*pendulum.T);\n}"; 10 | this.difficultyRating = 2; 11 | this.description = "Stabilize the pendulum so that it stays upright, moves to the center (x=0) and stays there in perfect balance. Calculate the horizontal force on the cart necessary to achieve this."; 12 | this.model = new Models.SinglePendulum({m0: 10,m1: .5,L: 1,g: 9.81,theta: 0.001,dtheta: 0.001,x: -2.5,dx: 0.0,F: 0,T: 0}); 13 | } 14 | 15 | 16 | Levels.StabilizeSinglePendulum.prototype.levelComplete = function() 17 | { 18 | return Math.abs(this.model.x) < 0.01 19 | && Math.abs(this.model.dx) < 0.01 20 | && Math.abs(this.model.dtheta) < 0.01 21 | && Math.abs(Math.sin(this.model.theta)) < 0.001 22 | && Math.cos(this.model.theta) > 0.999; 23 | } 24 | 25 | Levels.StabilizeSinglePendulum.prototype.levelFailed = function() 26 | { 27 | return false; 28 | } 29 | 30 | 31 | Levels.StabilizeSinglePendulum.prototype.simulate = function (dt, controlFunc) 32 | { 33 | this.model.simulate (dt, controlFunc); 34 | } 35 | 36 | Levels.StabilizeSinglePendulum.prototype.getSimulationTime = function() {return this.model.T;} 37 | 38 | 39 | Levels.StabilizeSinglePendulum.prototype.draw = function(ctx, canvas){this.model.draw(ctx, canvas);} 40 | 41 | Levels.StabilizeSinglePendulum.prototype.infoText = function(ctx, canvas){return this.model.infoText();} -------------------------------------------------------------------------------- /js/levels/SwingUpDoublePendulum.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | if (typeof Levels === 'undefined') var Levels = {}; 3 | 4 | Levels.SwingUpDoublePendulum = function() 5 | { 6 | this.name = "SwingUpDoublePendulum"; 7 | this.title = "Inverted Double Pendulum: Swing Up"; 8 | this.sampleSolution = "function controlFunction(p)\n{\n var s = [[0.295, 49.5], [0.81, -49.5], [0.7, 0.0], [0.52, 50.0], [1.2, 0.0]];\n while(s.length != 0)\n {\n var [dt,F] = s.shift();\n p.T -= dt;\n if(p.T <= 0) return F;\n }\n \n var x_offset = Math.max(0, 2.5 - 3.0 * p.T);\n return 18.9*p.dtheta1 - 352.0*p.dtheta2 - 98.0*p.dx + 492.5*p.theta1 - 764.6*p.theta2 - 31.6*(p.x + x_offset);\n}"; 9 | this.boilerPlateCode = "function controlFunction(pendulum)\n{\n return 10*Math.sin(8*pendulum.T);\n}"; 10 | this.difficultyRating = 4; 11 | this.description = "Bring the pendulum into an upright position and keep it upright in the center (x=0)."; 12 | this.model = new Models.DoublePendulum({m0: 10,m1: 2,m2: 4,L1: 0.618,L2: 1,g: 2,theta1: -3.1415,dtheta1: 0,theta2: 3.1415,dtheta2: 0.0,x: -1,dx: 0,F: 0,T: 0}); 13 | } 14 | 15 | 16 | Levels.SwingUpDoublePendulum.prototype.levelComplete = function() 17 | { 18 | return Math.abs(this.model.x) < 0.01 19 | && Math.abs(this.model.dx) < 0.01 20 | && Math.abs(this.model.dtheta1) < 0.01 21 | && Math.abs(Math.sin(this.model.theta1)) < 0.001 22 | && Math.cos(this.model.theta1) > 0.999 23 | && Math.abs(this.model.dtheta2) < 0.01 24 | && Math.abs(Math.sin(this.model.theta2)) < 0.001 25 | && Math.cos(this.model.theta2) > 0.999; 26 | } 27 | 28 | Levels.SwingUpDoublePendulum.prototype.levelFailed = function() 29 | { 30 | return false; 31 | } 32 | 33 | 34 | Levels.SwingUpDoublePendulum.prototype.simulate = function (dt, controlFunc) 35 | { 36 | this.model.simulate (dt, controlFunc); 37 | } 38 | 39 | Levels.SwingUpDoublePendulum.prototype.getSimulationTime = function() {return this.model.T;} 40 | 41 | Levels.SwingUpDoublePendulum.prototype.draw = function(ctx, canvas){this.model.draw(ctx, canvas);} 42 | 43 | Levels.SwingUpDoublePendulum.prototype.infoText = function(ctx, canvas){return this.model.infoText();} -------------------------------------------------------------------------------- /js/levels/SwingUpSinglePendulum.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | if (typeof Levels === 'undefined') var Levels = {}; 3 | 4 | Levels.SwingUpSinglePendulum = function() 5 | { 6 | this.name = "SwingUpSinglePendulum"; 7 | this.title = "Inverted Pendulum: Swing up"; 8 | 9 | this.sampleSolution = "function controlFunction(pendulum)\n{\n if (pendulum.T < 2.0) return 50 * Math.cos(3 * pendulum.T);\n if (pendulum.T < 4.7) return 1000 * pendulum.dtheta;\n if (pendulum.T < 5.22) return -1000;\n if (pendulum.T < 6.2) return 0;\n \n let L = 1.0;\n let v_pendulum = pendulum.dx + L * pendulum.dtheta;\n let v_pendulum_cmd = -0.6 * pendulum.x;\n monitor('v_pendulum', v_pendulum);\n let theta_cmd = Math.min(0.07, Math.max(-0.07, 0.3 * (v_pendulum_cmd - v_pendulum)));\n return 200 * pendulum.dtheta + 1500 * (Math.sin(pendulum.theta) - theta_cmd);\n}\n"; 10 | this.boilerPlateCode = "function controlFunction(pendulum)\n{\n return 10*Math.sin(8*pendulum.T);\n}"; 11 | this.difficultyRating = 3; 12 | this.description = "Bring the pendulum into an upright position and keep it upright in the center (x=0)."; 13 | this.model = new Models.SinglePendulum({m0: 10,m1: .5,L: 1,g: 9.81,theta: 3.1415,dtheta: 0,x: 0,dx: 0,F: 0,T: 0}); 14 | } 15 | 16 | 17 | Levels.SwingUpSinglePendulum.prototype.levelComplete = function() 18 | { 19 | return Math.abs(this.model.x) < 0.01 20 | && Math.abs(this.model.dx) < 0.01 21 | && Math.abs(this.model.dtheta) < 0.01 22 | && Math.abs(Math.sin(this.model.theta)) < 0.001 23 | && Math.cos(this.model.theta) > 0.999; 24 | } 25 | 26 | Levels.SwingUpSinglePendulum.prototype.levelFailed = function() 27 | { 28 | return false; 29 | } 30 | 31 | 32 | Levels.SwingUpSinglePendulum.prototype.simulate = function (dt, controlFunc) 33 | { 34 | this.model.simulate (dt, controlFunc); 35 | } 36 | 37 | Levels.SwingUpSinglePendulum.prototype.getSimulationTime = function() {return this.model.T;} 38 | 39 | 40 | Levels.SwingUpSinglePendulum.prototype.draw = function(ctx, canvas){this.model.draw(ctx, canvas);} 41 | 42 | Levels.SwingUpSinglePendulum.prototype.infoText = function(ctx, canvas){return this.model.infoText();} -------------------------------------------------------------------------------- /js/levels/TutorialBlockOnSlope.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | if (typeof Levels === 'undefined') var Levels = {}; 3 | 4 | Levels.TutorialBlockOnSlope = function() 5 | { 6 | this.name = "TutorialBlockOnSlope"; 7 | this.title = "Tutorial #3"; 8 | 9 | this.sampleSolution = "var position_error_integral = 0;\nfunction controlFunction(block)\n{\n // Using the PD Controller form tutorial #2,\n // the block comes to rest. But the slope force\n // pulls it away from its target.\n // Idea: Measure the position error over time using an integral.\n // The longer the block is on the right of the\n // arrow, the harder it should be pulled to the left.\n // This is known as a proportional-integral-derivative (or PID) controller.\n \n var delta_t = 0.02; // The simulation time step\n position_error_integral += delta_t * block.x; \n monitor('position error integral ', position_error_integral); \n return -3*block.x -5*block.dx -2*position_error_integral;\n}\n"; 10 | this.boilerPlateCode = "function controlFunction(block)\n{\n return 0;\n}"; 11 | this.difficultyRating = 1; 12 | this.description = "Push the block under the arrow (x=0) and make it stop there. Calculate the horizontal force on the block necessary to achieve this. This time the block is on a slope. The PD controller alone will not work."; 13 | this.model = new Models.BlockOnSlope({g: 5,x: -2,dx: 0,slope: -0.4,friction: 0}); 14 | } 15 | 16 | 17 | Levels.TutorialBlockOnSlope.prototype.levelComplete = function() 18 | { 19 | return Math.abs(this.model.x) < 0.01 20 | && Math.abs(this.model.dx) < 0.01; 21 | } 22 | 23 | Levels.TutorialBlockOnSlope.prototype.levelFailed = function() 24 | { 25 | return false; 26 | } 27 | 28 | 29 | Levels.TutorialBlockOnSlope.prototype.simulate = function (dt, controlFunc) 30 | { 31 | this.model.simulate (dt, controlFunc); 32 | } 33 | 34 | Levels.TutorialBlockOnSlope.prototype.getSimulationTime = function() {return this.model.T;} 35 | 36 | Levels.TutorialBlockOnSlope.prototype.draw = function(ctx, canvas){this.model.draw(ctx, canvas);} 37 | 38 | Levels.TutorialBlockOnSlope.prototype.infoText = function(ctx, canvas){return this.model.infoText();} -------------------------------------------------------------------------------- /js/levels/TutorialBlockWithFriction.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | if (typeof Levels === 'undefined') var Levels = {}; 3 | 4 | Levels.TutorialBlockWithFriction = function() 5 | { 6 | this.name = "TutorialBlockWithFriction"; 7 | this.title = "Tutorial #1"; 8 | 9 | this.sampleSolution = "function controlFunction(block)\n{\n // Idea: If the block is on the left (negative x)\n // it should be pushed to the right (positive force),\n // and vice versa. The farther away the block is from\n // its target, the harder it should be pushed.\n // This is called a 'proportional controller'.\n \n return -3*block.x;\n}"; 10 | this.boilerPlateCode = "function controlFunction(block)\n{\n return 5*Math.sin(10*block.T);\n}"; 11 | this.difficultyRating = 1; 12 | this.description = "Push the block under the arrow (x=0) and make it stop there. Write a JavaScript function that calculates the horizontal force on the block necessary to achieve this."; 13 | this.model = new Models.BlockOnSlope({g: 0,x: -2,dx: 0,slope: 0,friction: 1}); 14 | } 15 | 16 | 17 | Levels.TutorialBlockWithFriction.prototype.levelComplete = function() 18 | { 19 | return Math.abs(this.model.x) < 0.01 20 | && Math.abs(this.model.dx) < 0.01; 21 | } 22 | 23 | Levels.TutorialBlockWithFriction.prototype.levelFailed = function() 24 | { 25 | return false; 26 | } 27 | 28 | 29 | Levels.TutorialBlockWithFriction.prototype.simulate = function (dt, controlFunc) 30 | { 31 | this.model.simulate (dt, controlFunc); 32 | } 33 | 34 | Levels.TutorialBlockWithFriction.prototype.getSimulationTime = function() {return this.model.T;} 35 | 36 | Levels.TutorialBlockWithFriction.prototype.draw = function(ctx, canvas){this.model.draw(ctx, canvas);} 37 | 38 | Levels.TutorialBlockWithFriction.prototype.infoText = function(ctx, canvas){return this.model.infoText();} -------------------------------------------------------------------------------- /js/levels/TutorialBlockWithoutFriction.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | if (typeof Levels === 'undefined') var Levels = {}; 3 | 4 | Levels.TutorialBlockWithoutFriction = function() 5 | { 6 | this.name = "TutorialBlockWithoutFriction"; 7 | this.title = "Tutorial #2"; 8 | 9 | this.sampleSolution = "function controlFunction(block)\n{\n // Idea: The block should slow down.\n // It should be pushed against its direction of movement.\n // We add this force to the proportional controller.\n // This is known as a proportional-derivative (or PD) controller.\n \n return -3*block.x -1.5*block.dx;\n}"; 10 | this.boilerPlateCode = "function controlFunction(block)\n{\n return 5*Math.sin(10*block.T);\n}"; 11 | this.difficultyRating = 1; 12 | this.description = "Push the block under the arrow (x=0) and make it stop there. Calculate the horizontal force on the block necessary to achieve this. This time the block has no friction (as if on ice). The proportional controller alone will not work."; 13 | this.model = new Models.BlockOnSlope({g: 0,x: -2,dx: 0,slope: 0,friction: 0}); 14 | } 15 | 16 | 17 | Levels.TutorialBlockWithoutFriction.prototype.levelComplete = function() 18 | { 19 | return Math.abs(this.model.x) < 0.01 20 | && Math.abs(this.model.dx) < 0.01; 21 | } 22 | 23 | Levels.TutorialBlockWithoutFriction.prototype.levelFailed = function() 24 | { 25 | return false; 26 | } 27 | 28 | 29 | Levels.TutorialBlockWithoutFriction.prototype.simulate = function (dt, controlFunc) 30 | { 31 | this.model.simulate (dt, controlFunc); 32 | } 33 | 34 | Levels.TutorialBlockWithoutFriction.prototype.getSimulationTime = function() {return this.model.T;} 35 | 36 | Levels.TutorialBlockWithoutFriction.prototype.draw = function(ctx, canvas){this.model.draw(ctx, canvas);} 37 | 38 | Levels.TutorialBlockWithoutFriction.prototype.infoText = function(ctx, canvas){return this.model.infoText();} -------------------------------------------------------------------------------- /js/levels/VehicleRacing.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | if (typeof Levels === 'undefined') var Levels = {}; 3 | 4 | Levels.VehicleRacing = function() 5 | { 6 | this.name = "VehicleRacing"; 7 | this.title = "Vehicle Racing"; 8 | 9 | this.sampleSolution = "function controlFunction(vehicle){ \n var v_ref = Math.max(.4*vehicle.lidarPoints[2].distance,10);\n var v = vehicle.speed;\n return {steering: 0.1*(vehicle.lidarPoints[0].distance - vehicle.lidarPoints[4].distance), acceleration: 10*(v_ref-v)};\n};"; 10 | this.boilerPlateCode = "function controlFunction(vehicle){ \n return {steering: 0, acceleration: 0};\n};"; 11 | this.difficultyRating = 2; 12 | this.description = "Calculate the appropriate steering angle [radians] and acceleration [m/s²] for the vehicle. The lateral acceleration is limited. If you go too fast into a turn you won't be able to steer through it."; 13 | ImageDataCache.load('img/track.png'); 14 | this.model = new Models.Vehicle({trackImgURL: 'img/track.png', constantSpeed:false}); 15 | } 16 | 17 | 18 | Levels.VehicleRacing.prototype.levelComplete = function() 19 | { 20 | return Math.abs(this.model.x - 82) < 4 21 | && Math.abs(this.model.y - 46) < 4; 22 | } 23 | 24 | Levels.VehicleRacing.prototype.levelFailed = function() 25 | { 26 | return this.model.detectCollision(); 27 | } 28 | 29 | 30 | Levels.VehicleRacing.prototype.simulate = function (dt, controlFunc) 31 | { 32 | this.model.simulate (dt, controlFunc); 33 | } 34 | 35 | Levels.VehicleRacing.prototype.getSimulationTime = function() {return this.model.T;} 36 | 37 | Levels.VehicleRacing.prototype.draw = function(ctx, canvas){this.model.draw(ctx, canvas);} 38 | 39 | Levels.VehicleRacing.prototype.infoText = function(ctx, canvas){return this.model.infoText();} -------------------------------------------------------------------------------- /js/levels/VehicleSteeringSimple.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | if (typeof Levels === 'undefined') var Levels = {}; 3 | 4 | Levels.VehicleSteeringSimple = function() 5 | { 6 | this.name = "VehicleSteeringSimple"; 7 | this.title = "Vehicle Steering"; 8 | 9 | this.sampleSolution = "function controlFunction(vehicle){\n return 0.1*(vehicle.lidarPoints[0].distance - vehicle.lidarPoints[4].distance);\n};"; 10 | this.boilerPlateCode = "function controlFunction(vehicle){\n return -0.03;\n};"; 11 | this.difficultyRating = 1; 12 | this.description = "Steer the vehicle along the track by calculating the necessary steering angle in radians. The vehicle speed is constant. The vehicle has sensors that tell the distance to the track's edge in different directions relative to the vehicle."; 13 | ImageDataCache.load('img/track.png'); 14 | this.model = new Models.Vehicle({trackImgURL: 'img/track.png', lateralAccelerationLimit: 40}); 15 | } 16 | 17 | 18 | Levels.VehicleSteeringSimple.prototype.levelComplete = function() 19 | { 20 | return Math.abs(this.model.x - 82) < 4 21 | && Math.abs(this.model.y - 46) < 4; 22 | } 23 | 24 | Levels.VehicleSteeringSimple.prototype.levelFailed = function() 25 | { 26 | return this.model.detectCollision(); 27 | } 28 | 29 | 30 | Levels.VehicleSteeringSimple.prototype.simulate = function (dt, controlFunc) 31 | { 32 | this.model.simulate (dt, controlFunc); 33 | } 34 | 35 | Levels.VehicleSteeringSimple.prototype.getSimulationTime = function() {return this.model.T;} 36 | 37 | Levels.VehicleSteeringSimple.prototype.draw = function(ctx, canvas){this.model.draw(ctx, canvas);} 38 | 39 | Levels.VehicleSteeringSimple.prototype.infoText = function(ctx, canvas){return this.model.infoText();} -------------------------------------------------------------------------------- /js/models/Airplane.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | if (typeof Models === 'undefined') var Models = {}; 3 | 4 | Models.Airplane = function(params) 5 | { 6 | var nVars = Object.keys(this.vars).length; 7 | for(var i = 0; i < nVars; i++) 8 | { 9 | var key = Object.keys(this.vars)[i]; 10 | this[key] = (typeof params[key] == 'undefined')?this.vars[key]:params[key]; 11 | } 12 | integrationStep(this, ['x', 'vx', 'y', 'vy', 'pitch', 'pitch_rate', 'elevator_angle', 'throttle', 'brake'], 0.02); 13 | } 14 | 15 | Models.Airplane.prototype.vars = 16 | { 17 | x: 0.0, 18 | vx: 0.0, 19 | y: 1.51, 20 | vy: 0.0, 21 | pitch: 0.01, 22 | pitch_rate: 0.0, 23 | elevator_angle: 0.0, 24 | throttle: 0.0, 25 | brake: 0.0, 26 | T: 0, 27 | elevator_command: 0.0, 28 | throttle_command: 0.0, 29 | brake_command: 0.0, 30 | max_load_factor: 0.0, 31 | thrust: 0.0, 32 | wings: [ 33 | { // Main wing 34 | px: -0.4, 35 | py: -0.2, 36 | incidence_angle: 4.25/60, 37 | area: 16.0, 38 | }, 39 | { // Horizontal stabilizer 40 | px: -5.0, 41 | py: 0.2, 42 | incidence_angle: -1.5/60, 43 | area: 3.0, 44 | } 45 | ], 46 | wheels: [ 47 | { // Main gear 48 | px: -0.7, 49 | py: -1.4, 50 | radius: 0.3, 51 | compression: 0.0, 52 | fx: 0.0, 53 | fy: 0.0, 54 | }, 55 | { // Nose gear 56 | px: 2.8, 57 | py: -1.4, 58 | radius: 0.2, 59 | compression: 0.0, 60 | fx: 0.0, 61 | fy: 0.0, 62 | } 63 | ], 64 | hitbox: [[2.5,0.9], [4.5,-0.2], [4.0,-0.65], [-2.5,-0.65], [-6.1,-0.0], [-7.0,2.35], [-5.8,2.35]], 65 | airplane_path: new Path2D("m 65.846587,46.472565 c 0,0 37.196543,-0.360388 49.806183,-0.5114 12.60964,-0.151012 25.29262,12.456859 8.922,12.480144 -16.37062,0.02329 -10.89443,0.01026 -18.14665,0.07498 -7.25222,0.06472 -16.942581,0.06569 -25.409316,-0.492473 C 72.552068,57.46565 69.803478,56.558812 64.840484,55.759541 59.87749,54.96027 51.067509,53.357967 51.067509,53.357967 l -7.325255,-18.810592 9.660984,-0.183924 12.496088,12.145508"), 66 | airfoil_path: new Path2D("m 21.677842,79.894736 c 0,0 94.793228,-8.938838 125.753348,-8.939856 30.96013,-0.001 51.96,3.796246 52.05,8.776878 0.09,4.980631 -22.51007,8.353451 -52.15923,8.967375 C 117.67281,89.313057 21.677842,79.894736 21.677842,79.894736 Z"), 67 | tree_top_path: new Path2D("m 120.05921,61.843985 c -1.65357,4.617685 -9.48191,3.214979 -7.25752,-2.171054 -4.9742,1.615489 -5.88787,-8.07854 -0.36777,-9.044457 -4.04463,-0.828636 -5.2462,-6.785239 0.57074,-7.081693 -4.68062,-3.854027 1.14553,-9.268045 6.39039,-6.339913 2.43053,-4.964987 11.68181,-1.747989 9.21899,3.292002 4.96647,-2.237211 9.25272,0.901048 5.7187,5.695746 5.3658,-1.177213 6.80193,4.035241 0.0864,6.499932 5.98069,-0.237782 5.52476,8.859962 0.93045,8.405075 0.37445,5.924168 -5.67999,5.280539 -7.38158,0.744362 -0.29224,5.440368 -7.04019,3.824441 -7.90883,0 z"), 68 | tree_stem_path: new Path2D("m 123.9023,92.632809 3.94757,-0.04386 c 0,0 -0.41981,-13.495462 -0.50284,-17.344903 -0.083,-3.849441 -0.68143,-4.849212 -0.68143,-4.849212 l 4.51778,-6.79859 -1.49131,-0.219311 -3.94757,4.824807 -1.62289,-8.246033 -1.97378,0.131585 1.27199,7.017901 -5.36361,-5.193492 -1.11031,1.416428 c 0,0 5.54169,4.122034 6.40125,6.741505 0.85956,2.619471 1.51329,3.890814 1.47625,7.781722 -0.037,3.890908 -0.9211,14.781453 -0.9211,14.781453 z"), 69 | }; 70 | 71 | Models.Airplane.prototype.aero_coefficients = function (cos_alpha, sin_alpha) 72 | { 73 | const [cos2, sin2] = double_angle(cos_alpha, sin_alpha); 74 | const [cos4, sin4] = double_angle(cos2, sin2); 75 | const [cos6, sin6] = angle_sum(cos2,sin2,cos4,sin4); 76 | 77 | const CL = 1.4405 * sin2 + 0.5092 * sin4 + 0.16 * sin6; 78 | const CD = 0.03 + 1.5 * (0.5*(1 - cos2)); 79 | return [CL, CD]; 80 | } 81 | 82 | Models.Airplane.prototype.wing_force = function (wing, vx, vy, pitch, pitch_rate) 83 | { 84 | vx -= pitch_rate * wing.py; 85 | vy += pitch_rate * wing.px; 86 | 87 | let speed_squared = vx*vx + vy*vy; 88 | let speed = Math.max(Math.sqrt(speed_squared), 0.0001); 89 | 90 | let evx = vx / speed; 91 | let evy = vy / speed; 92 | 93 | const [cos_alpha, sin_alpha] = angle_difference(Math.cos(wing.incidence_angle), Math.sin(wing.incidence_angle), evx, evy); 94 | 95 | const [CL, CD] = this.aero_coefficients(cos_alpha, sin_alpha); 96 | 97 | let qbarS = 0.5 * 1.2 * speed_squared * wing.area; 98 | let fx = qbarS * (-CD * evx - CL * evy); 99 | let fy = qbarS * (-CD * evy + CL * evx); 100 | 101 | return [fx, fy, wing.px * fy - wing.py * fx]; 102 | } 103 | 104 | Models.Airplane.prototype.simulate = function (dt, controlFunc) 105 | { 106 | var input = controlFunc({x: this.x, vx: this.vx, y: this.y, vy: this.vy, pitch: this.pitch, pitch_rate: this.pitch_rate, T: this.T}); // call user controller 107 | if(typeof input != 'object' 108 | || typeof input.elevator != 'number' 109 | || typeof input.throttle != 'number' 110 | || typeof input.brake != 'number' 111 | ) 112 | throw "Error: The controlFunction must return an object: {elevator:number, throttle:number, brake:number}"; 113 | this.elevator_command = Math.max(-0.3, Math.min(0.3, input.elevator)); 114 | this.throttle_command = Math.max(0.0, Math.min(1.0, input.throttle)); 115 | this.brake_command = Math.max(0.0, Math.min(1.0, input.brake)); 116 | integrationStep(this, ['x', 'vx', 'y', 'vy', 'pitch', 'pitch_rate', 'elevator_angle', 'throttle', 'brake'], dt); 117 | } 118 | 119 | Models.Airplane.prototype.ode = function (x) { 120 | 121 | let vx = x[1]; 122 | let py = x[2]; 123 | let vy = x[3]; 124 | let pitch = x[4]; 125 | let pitch_rate = x[5]; 126 | let elevator_angle = x[6]; 127 | let throttle = x[7]; 128 | let brake = x[8]; 129 | 130 | const g0 = 9.81; 131 | 132 | let C = Math.cos(pitch); 133 | let S = Math.sin(pitch); 134 | 135 | let vx_body = C * vx + S * vy; 136 | let vy_body = -S * vx + C * vy; 137 | 138 | this.wings[1].incidence_angle = -elevator_angle; 139 | 140 | const [fx0, fy0, M0] = this.wing_force(this.wings[0], vx_body, vy_body, pitch, pitch_rate); 141 | const [fx1, fy1, M1] = this.wing_force(this.wings[1], vx_body, vy_body, pitch, pitch_rate); 142 | 143 | let m = 1000.0; 144 | let J = 4000.0; 145 | 146 | this.max_load_factor = Math.max(this.max_load_factor, Math.sqrt(fx0*fx0 + fy0*fy0) / (m*g0)); 147 | this.max_load_factor = Math.max(this.max_load_factor, Math.sqrt(fx1*fx1 + fy1*fy1) / (m*g0)); 148 | 149 | this.thrust = throttle * m * 5.0; 150 | 151 | let ax_body = (fx0 + fx1 + this.thrust) / m; 152 | let ay_body = (fy0 + fy1) / m; 153 | let pitch_accel = (M0 + M1) / J; 154 | 155 | let ax = C * ax_body - S * ay_body; 156 | let ay = S * ax_body + C * ay_body - g0; 157 | 158 | for(let i = 0; i < this.wheels.length; i++) 159 | { 160 | let px_rel = C * this.wheels[i].px - S * this.wheels[i].py; 161 | let py_rel = S * this.wheels[i].px + C * this.wheels[i].py; 162 | let py_wheel = py + py_rel - this.wheels[i].radius; 163 | let vy_wheel = vy + pitch_rate * C * this.wheels[i].px - pitch_rate * S * this.wheels[i].py; 164 | this.wheels[i].compression = C * Math.max(-py_wheel, 0); 165 | this.wheels[i].fy = Math.max(-m * py_wheel * 40, 0); 166 | if(py_wheel < 0) this.wheels[i].fy += Math.max(-vy_wheel * m * 2.0, 0); 167 | if(i == 0) this.wheels[i].fx = -0.8 * brake * this.wheels[i].fy * vx / Math.max(Math.abs(vx), 0.3); 168 | else this.wheels[i].fx = 0.0; 169 | ax += this.wheels[i].fx / m; 170 | ay += this.wheels[i].fy / m; 171 | pitch_accel += (this.wheels[i].fy * px_rel - this.wheels[i].fx * py_rel) / J; 172 | 173 | this.max_load_factor = Math.max(this.max_load_factor, Math.sqrt(this.wheels[i].fx*this.wheels[i].fx + this.wheels[i].fy*this.wheels[i].fy) / (m*g0)); 174 | } 175 | 176 | return [ 177 | vx, 178 | ax, 179 | vy, 180 | ay, 181 | pitch_rate, 182 | pitch_accel, 183 | 30.0 * (this.elevator_command - elevator_angle), 184 | 10.0 * (this.throttle_command - throttle), 185 | 10.0 * (this.brake_command - brake), 186 | ]; 187 | } 188 | 189 | Models.Airplane.prototype.crashed = function () 190 | { 191 | if(this.y > 20) return false; 192 | let C = Math.cos(this.pitch); 193 | let S = Math.sin(this.pitch); 194 | for(let i = 0; i < this.hitbox.length; i++) 195 | { 196 | let py = this.y + S * this.hitbox[i][0] + C * this.hitbox[i][1]; 197 | if(py < 0) return true; 198 | } 199 | return false; 200 | } 201 | 202 | Models.Airplane.prototype.structural_overload = function () 203 | { 204 | return this.max_load_factor > 2.5; 205 | } 206 | 207 | Models.Airplane.prototype.draw = function (ctx, canvas) 208 | { 209 | ctx.scale(0.32, 0.32); 210 | ctx.translate(0,-3); 211 | 212 | const force_scale = 0.0005; 213 | 214 | // Background (back to front) 215 | const profiles = [ 216 | [0.00e+00,3.65e-02,1.22e-01,4.16e-01,5.02e-01,3.41e-01,5.69e-01,5.22e-01,4.70e-01,5.28e-01,2.48e-01,4.46e-01,8.12e-03,-1.55e-01,-2.75e-01,-3.72e-02,-1.60e-01,-6.88e-02,-1.63e-01,8.41e-02,2.15e-01,5.27e-01,5.18e-01,2.90e-01,3.37e-01,2.19e-01,-7.31e-02,-3.84e-01,6.35e-02,-8.37e-02,-1.62e-01,-3.74e-01,-4.66e-01,-8.53e-01,-9.89e-01,-1.00e+00,-9.83e-01,-8.37e-01,-9.67e-01,-8.81e-01,-3.58e-01,-5.03e-01,-4.74e-01,-3.13e-01,-1.38e-01,-1.67e-03,-1.69e-01,-1.35e-01,-5.94e-01,-8.36e-01,-6.02e-01,-3.62e-01,-3.48e-01,6.84e-03,-3.99e-01,6.19e-03,5.05e-01,5.52e-01,4.74e-01,3.80e-01,4.98e-01,1.62e-01,2.48e-01,4.84e-01,6.86e-01,1.92e-01,-3.51e-01,-5.89e-01,-6.26e-01,-4.76e-01,-6.52e-01,-7.35e-01,-7.17e-01,-5.34e-01,-7.51e-01,-7.77e-01,-6.96e-01,-8.93e-01,-8.77e-01,-4.08e-01,-4.52e-01,-3.28e-01,2.78e-01,2.30e-01,-2.43e-02,2.72e-01,2.16e-01,4.79e-01,7.23e-01,6.91e-01,9.18e-01,8.02e-01,7.51e-01,6.05e-01,6.47e-01,4.56e-01,3.34e-01,1.26e-01,3.51e-01,0.00e+00], 217 | [0.00e+00,3.42e-02,2.23e-02,-9.52e-02,-6.64e-02,-2.03e-01,-2.73e-01,-2.60e-01,-2.75e-01,-3.92e-01,-4.07e-01,-4.29e-01,-3.91e-01,-4.17e-01,-3.69e-01,-3.37e-01,-3.77e-01,-4.54e-01,-5.80e-01,-4.49e-01,-4.76e-01,-5.30e-01,-3.87e-01,-3.65e-01,-4.34e-01,-4.13e-01,-4.29e-01,-4.25e-01,-5.25e-01,-6.76e-01,-7.09e-01,-8.20e-01,-7.71e-01,-7.98e-01,-7.60e-01,-7.75e-01,-6.84e-01,-6.27e-01,-6.64e-01,-8.74e-01,-9.33e-01,-1.00e+00,-9.39e-01,-8.26e-01,-8.53e-01,-7.16e-01,-8.18e-01,-6.60e-01,-6.24e-01,-5.98e-01,-5.45e-01,-4.51e-01,-4.08e-01,-3.97e-01,-4.40e-01,-4.24e-01,-4.19e-01,-4.71e-01,-4.04e-01,-5.23e-01,-4.37e-01,-4.31e-01,-4.98e-01,-5.20e-01,-3.98e-01,-3.26e-01,-3.12e-01,-3.75e-01,-4.21e-01,-3.38e-01,-3.26e-01,-2.86e-01,-2.19e-01,-2.02e-01,-1.77e-01,-1.48e-01,-2.32e-01,-1.25e-01,-1.44e-01,-1.84e-01,-2.31e-01,-1.74e-01,-1.77e-01,-1.03e-01,-2.06e-01,-3.75e-02,4.11e-02,6.51e-02,2.40e-02,-9.62e-02,-1.01e-01,-1.09e-01,-2.91e-02,2.95e-02,9.23e-02,-1.44e-02,8.34e-02,8.71e-03,1.95e-02,0.00e+00], 218 | [0.00e+00,-1.51e-03,1.64e-01,9.90e-02,3.57e-01,1.80e-01,1.25e-02,1.03e-01,1.73e-01,7.11e-03,-3.31e-02,2.40e-01,1.34e-01,4.02e-01,4.97e-01,5.64e-01,5.35e-01,4.96e-01,4.77e-01,4.87e-01,6.55e-01,6.39e-01,4.08e-01,4.28e-01,4.97e-01,4.76e-01,3.15e-01,2.94e-01,1.34e-01,3.65e-01,2.10e-01,3.07e-01,2.80e-01,3.80e-01,1.74e-01,1.91e-01,1.89e-01,2.70e-01,-1.01e-01,-7.13e-02,9.58e-02,2.22e-01,1.96e-01,1.04e-01,3.12e-02,7.74e-03,1.54e-01,1.61e-01,1.00e-01,1.57e-01,1.28e-01,1.81e-01,3.99e-02,-9.85e-02,-3.06e-01,-4.78e-01,-2.24e-01,-1.76e-01,-1.84e-01,-4.26e-01,-5.31e-01,-5.74e-01,-6.29e-01,-6.47e-01,-7.31e-01,-6.49e-01,-2.53e-01,-5.54e-01,-6.49e-01,-6.16e-01,-6.98e-01,-8.05e-01,-6.08e-01,-6.48e-01,-8.33e-01,-7.70e-01,-8.26e-01,-9.43e-01,-7.06e-01,-5.56e-01,-6.36e-01,-3.96e-01,-5.17e-01,-8.40e-01,-8.38e-01,-9.62e-01,-1.00e+00,-8.98e-01,-8.87e-01,-8.74e-01,-4.78e-01,-4.19e-01,-5.33e-01,-6.01e-01,-5.77e-01,-5.87e-01,-5.62e-01,-3.03e-01,-4.15e-01,0.00e+00], 219 | [0.00e+00,1.59e-01,-1.14e-01,1.76e-01,3.41e-02,6.47e-02,3.55e-01,3.70e-01,3.99e-01,2.72e-01,6.56e-02,1.99e-01,2.80e-01,6.50e-02,5.76e-02,2.25e-01,2.08e-01,-2.01e-02,2.42e-01,1.88e-01,3.89e-01,6.97e-01,5.79e-01,6.04e-01,6.70e-01,3.75e-01,4.06e-01,6.97e-01,4.23e-01,3.03e-01,4.85e-01,2.84e-01,1.52e-01,9.80e-02,1.74e-01,5.20e-01,4.67e-01,4.81e-01,4.47e-01,5.27e-01,6.60e-01,9.46e-01,9.72e-01,9.71e-01,9.24e-01,8.73e-01,8.03e-01,9.47e-01,8.63e-01,7.31e-01,9.06e-01,7.13e-01,4.46e-01,4.95e-01,2.75e-01,2.24e-01,1.71e-01,-6.88e-02,9.95e-04,3.16e-01,3.22e-01,5.33e-01,5.36e-01,1.89e-01,1.24e-01,-2.26e-01,-2.67e-01,-4.51e-01,-2.62e-01,-6.15e-01,-4.93e-01,-5.02e-01,-6.97e-01,-6.64e-01,-9.32e-01,-1.00e+00,-9.19e-01,-3.63e-01,-5.26e-01,-2.72e-01,-2.78e-01,-3.77e-01,-2.00e-01,-6.35e-02,-1.05e-01,1.84e-02,2.79e-01,4.62e-01,9.03e-02,2.81e-01,3.06e-01,2.96e-01,5.30e-01,6.46e-01,3.59e-01,6.06e-01,5.41e-01,5.34e-01,3.71e-01,0.00e+00], 220 | [0.00e+00,5.40e-02,-1.64e-01,-5.51e-02,-2.59e-01,-2.18e-01,-2.89e-01,-5.92e-01,-8.96e-01,-8.44e-01,-8.53e-01,-8.13e-01,-8.14e-01,-8.55e-01,-6.60e-01,-8.29e-01,-8.75e-01,-9.09e-01,-7.82e-01,-1.00e+00,-9.52e-01,-8.74e-01,-8.81e-01,-8.35e-01,-7.11e-01,-6.63e-01,-4.96e-01,-5.18e-01,-4.78e-01,-4.62e-01,-5.68e-01,-7.15e-01,-8.03e-01,-8.60e-01,-9.17e-01,-6.34e-01,-4.88e-01,-4.43e-01,-4.81e-01,-5.52e-01,-4.64e-01,-5.22e-01,-4.21e-01,-6.42e-01,-3.88e-01,-4.51e-01,-7.06e-01,-7.38e-01,-6.49e-01,-3.52e-01,-1.65e-01,-3.88e-01,-5.79e-01,-5.10e-01,-4.90e-01,-4.98e-01,-5.76e-01,-5.85e-01,-3.38e-01,-3.56e-01,-5.21e-01,-6.76e-01,-8.73e-01,-7.94e-01,-5.73e-01,-4.48e-01,-4.94e-01,-5.01e-01,-2.70e-02,2.70e-02,-5.15e-02,4.24e-02,1.61e-01,3.41e-01,3.57e-01,2.94e-01,5.19e-01,4.03e-01,4.53e-01,1.84e-01,3.43e-02,2.49e-01,1.07e-01,3.66e-01,4.22e-01,3.38e-01,3.99e-01,3.95e-01,3.55e-01,2.31e-01,3.46e-01,3.86e-01,1.46e-01,-1.05e-01,-2.43e-01,-6.60e-02,2.45e-01,2.09e-02,1.78e-01,0.00e+00], 221 | ]; 222 | 223 | const palette = ["#7C9EC1", "#5B84B1", "#4B719D", "#2C496F", "#193252"]; 224 | 225 | for(let i = 0; i < profiles.length; i++) 226 | { 227 | const z = 6000 - i*1000; 228 | const delta_x = 60.0; 229 | const width = delta_x * (profiles[i].length-1); 230 | const x_mod = this.x % width; 231 | 232 | ctx.save(); 233 | ctx.scale(40/z, 40/z); 234 | ctx.translate(-x_mod - width/2, -this.y); 235 | ctx.fillStyle = palette[i]; 236 | ctx.beginPath(); 237 | ctx.moveTo(0, -2000); 238 | for(let k = 0; k < 2; k++) 239 | { 240 | for(let j = 0; j < profiles[i].length; j++) 241 | { 242 | ctx.lineTo(k * width + delta_x * j, 200.0 * (profiles[i][j] + 1.0)); 243 | } 244 | } 245 | ctx.lineTo(2*width, -2000); 246 | ctx.closePath(); 247 | ctx.fill(); 248 | ctx.restore(); 249 | } 250 | 251 | // Trees 252 | for(let i = 0; i < 2; i++) 253 | { 254 | ctx.save(); 255 | ctx.translate(-(this.x % 60.0 - i * 60.0) * 0.5, -this.y); 256 | ctx.scale(0.1, -0.1); 257 | ctx.translate(-126.0,-92.0); 258 | ctx.beginPath(); 259 | ctx.fillStyle="#541e00"; 260 | ctx.fill(this.tree_stem_path); 261 | ctx.beginPath(); 262 | ctx.fillStyle="#49842a"; 263 | ctx.fill(this.tree_top_path); 264 | ctx.restore(); 265 | } 266 | 267 | // Ground 268 | ctx.save(); 269 | ctx.translate(0, -this.y); 270 | ctx.fillStyle = "#5B3E2A"; 271 | ctx.beginPath(); 272 | ctx.moveTo(-100, 0); 273 | ctx.lineTo(100, 0); 274 | ctx.lineTo(100, -200); 275 | ctx.lineTo(-100, -200); 276 | ctx.closePath(); 277 | ctx.fill(); 278 | 279 | // Runway stripes 280 | const stripe_width = 12.0; 281 | for(let i = 0; i < 3; i++) 282 | { 283 | ctx.fillStyle = "#ccc"; 284 | ctx.beginPath(); 285 | let stripe_x = -(this.x % (2*stripe_width)) + (i-1)*(2*stripe_width); 286 | ctx.moveTo(stripe_x, 0); 287 | ctx.lineTo(stripe_x + stripe_width, 0); 288 | ctx.lineTo(stripe_x + stripe_width, -1); 289 | ctx.lineTo(stripe_x, -1); 290 | ctx.closePath(); 291 | ctx.fill(); 292 | } 293 | ctx.restore(); 294 | 295 | // Body coordinates 296 | ctx.save(); 297 | ctx.rotate(this.pitch); 298 | 299 | // Wheels 300 | for(let i = 0; i < this.wheels.length; i++) 301 | { 302 | ctx.strokeStyle="#555"; 303 | drawLine(ctx, this.wheels[i].px, this.wheels[i].py + this.wheels[i].compression, this.wheels[i].px, 0, 0.5 * this.wheels[i].radius); 304 | ctx.beginPath(); 305 | ctx.arc(this.wheels[i].px, this.wheels[i].py + this.wheels[i].compression, this.wheels[i].radius, 0, 2 * Math.PI, false); 306 | ctx.fillStyle = '#111'; 307 | ctx.fill(); 308 | ctx.strokeStyle="#eee"; 309 | ctx.lineWidth = 0.03; 310 | ctx.stroke(); 311 | } 312 | 313 | // Body outline 314 | ctx.save(); 315 | ctx.scale(0.13, -0.13); 316 | ctx.translate(-98,-53); 317 | ctx.beginPath(); 318 | ctx.fillStyle="#999999"; 319 | ctx.fill(this.airplane_path); 320 | ctx.restore(); 321 | 322 | let C = Math.cos(this.pitch); 323 | let S = Math.sin(this.pitch); 324 | let vx_body = C * this.vx + S * this.vy; 325 | let vy_body = -S * this.vx + C * this.vy; 326 | for(let i = 0; i < this.wings.length; i++) 327 | { 328 | // Airfoil 329 | ctx.save(); 330 | ctx.translate(this.wings[i].px,this.wings[i].py); 331 | let sq = Math.sqrt(this.wings[i].area); 332 | ctx.scale(0.004 * sq, 0.005 * sq); 333 | ctx.rotate(this.wings[i].incidence_angle); 334 | ctx.translate(-150.0,-80.0); 335 | ctx.beginPath(); 336 | ctx.fillStyle="#666666"; 337 | ctx.fill(this.airfoil_path); 338 | ctx.restore(); 339 | 340 | // Aero forces 341 | const [fx, fy, M] = this.wing_force(this.wings[i], vx_body, vy_body, this.pitch, this.pitch_rate); 342 | ctx.strokeStyle="#ff0000"; 343 | let fxs = force_scale * fx; 344 | let fys = force_scale * fy; 345 | if(fxs*fxs + fys*fys > 0.01) 346 | { 347 | drawArrow(ctx, this.wings[i].px, this.wings[i].py, fxs, fys, 0.15, 0.1); 348 | } 349 | } 350 | 351 | // Thrust force 352 | if(force_scale * this.thrust > 0.01) drawArrow(ctx, 1.0, 0, force_scale * this.thrust, 0, 0.15, 0.1); 353 | ctx.restore(); // End of body coordinates 354 | 355 | // Wheel forces 356 | for(let i = 0; i < this.wheels.length; i++) 357 | { 358 | let px_rel = C * this.wheels[i].px - S * (this.wheels[i].py + this.wheels[i].compression); 359 | let py_rel = S * this.wheels[i].px + C * (this.wheels[i].py + this.wheels[i].compression); 360 | ctx.strokeStyle="#ff0000"; 361 | let fxs = force_scale * this.wheels[i].fx; 362 | let fys = force_scale * this.wheels[i].fy; 363 | if(fxs*fxs + fys*fys > 0.01) 364 | { 365 | drawArrow(ctx, px_rel, py_rel, fxs, fys, 0.15, 0.1); 366 | } 367 | } 368 | 369 | if(this.crashed()) { 370 | ctx.save(); 371 | ctx.scale(0.07,-0.07); 372 | ctx.textAlign="center"; 373 | ctx.font="10px Verdana"; 374 | ctx.fillStyle="#000"; 375 | ctx.fillText("CRASHED!",0.4,0.4); 376 | ctx.fillStyle="#f00"; 377 | ctx.fillText("CRASHED!",0,0); 378 | ctx.restore(); 379 | } 380 | else if(this.structural_overload()) { 381 | ctx.save(); 382 | ctx.scale(0.07,-0.07); 383 | ctx.textAlign="center"; 384 | ctx.font="10px Verdana"; 385 | ctx.fillStyle="#000"; 386 | ctx.fillText("Structural overload!",0.4,0.4); 387 | ctx.fillStyle="#f00"; 388 | ctx.fillText("Structural overload!",0,0); 389 | ctx.restore(); 390 | } 391 | } 392 | 393 | Models.Airplane.prototype.infoText = function () 394 | { 395 | return "vehicle.x = " + round(this.x,2) 396 | + "\nvehicle.vx = " + round(this.vx,2) 397 | + "\nvehicle.y = " + round(this.y,2) 398 | + "\nvehicle.vy = " + round(this.vy,2) 399 | + "\nvehicle.pitch = " + round(this.pitch,2) 400 | + "\nvehicle.pitch_rate = " + round(this.pitch_rate,2) 401 | + "\nvehicle.T = " + round(this.T,2); 402 | } 403 | -------------------------------------------------------------------------------- /js/models/BlockOnSlope.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | if (typeof Models === 'undefined') var Models = {}; 3 | 4 | Models.BlockOnSlope = function(params) 5 | { 6 | var nVars = Object.keys(this.vars).length; 7 | for(var i = 0; i < nVars; i++) 8 | { 9 | var key = Object.keys(this.vars)[i]; 10 | this[key] = (typeof params[key] == 'undefined')?this.vars[key]:params[key]; 11 | } 12 | } 13 | 14 | Models.BlockOnSlope.prototype.vars = 15 | { 16 | g: 9.81, 17 | x: 0, 18 | dx: 0, 19 | slope: 1, 20 | F: 0, 21 | F_cmd: 0, 22 | friction: 0, 23 | T: 0, 24 | }; 25 | 26 | Models.BlockOnSlope.prototype.simulate = function (dt, controlFunc) 27 | { 28 | this.F_cmd = controlFunc({x:this.x,dx:this.dx,T:this.T}); 29 | if(typeof this.F_cmd != 'number' || isNaN(this.F_cmd)) throw "Error: The controlFunction must return a number."; 30 | this.F_cmd = Math.max(-20,Math.min(20,this.F_cmd)); 31 | integrationStep(this, ['x', 'dx', 'F'], dt); 32 | } 33 | 34 | Models.BlockOnSlope.prototype.ode = function (x) 35 | { 36 | return [ 37 | x[1], 38 | (x[2]) - (Math.sin(this.slope) * this.g) - (this.friction * x[1]), 39 | 20.0 * (this.F_cmd - x[2]) 40 | ]; 41 | } 42 | 43 | 44 | Models.BlockOnSlope.prototype.draw = function (ctx, canvas) 45 | { 46 | ctx.rotate(this.slope); 47 | 48 | ctx.strokeStyle="#333366"; 49 | drawLine(ctx,-10,-.025,10,-.025,0.05); 50 | 51 | var cartWidth = 0.4; 52 | var cartHeight = 0.7*cartWidth; 53 | 54 | // block 55 | ctx.fillStyle="#4444FF"; 56 | ctx.fillRect(this.x-cartWidth/2,0,cartWidth,cartHeight); 57 | 58 | // force arrow 59 | ctx.strokeStyle="#FF0000"; 60 | drawArrow(ctx, this.x, 0.5*cartHeight, 0.1*this.F, 0, 0.05, 0.025); 61 | 62 | // target arrow 63 | ctx.strokeStyle="#4444FF"; 64 | drawArrow(ctx, 0, 4.5*cartHeight, 0, -2.5*cartHeight, 0.05, 1/30); 65 | } 66 | 67 | Models.BlockOnSlope.prototype.infoText = function () 68 | { 69 | return "/* Position */ block.x = " + round(this.x,2) 70 | + "\n/* Velocity */ block.dx = " + round(this.dx,2) 71 | + "\n/* Simulation time */ block.T = " + round(this.T,2); 72 | } 73 | -------------------------------------------------------------------------------- /js/models/BouncingBallPlatform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | if (typeof Models === 'undefined') var Models = {}; 3 | 4 | Models.BouncingBallPlatform = function(params) 5 | { 6 | var nVars = Object.keys(this.vars).length; 7 | for(var i = 0; i < nVars; i++) 8 | { 9 | var key = Object.keys(this.vars)[i]; 10 | this[key] = (typeof params[key] == 'undefined')?this.vars[key]:params[key]; 11 | } 12 | } 13 | 14 | Models.BouncingBallPlatform.prototype.vars = 15 | { 16 | ball_x: -2.0, 17 | ball_y: -1.1, 18 | ball_dx: 1.5, 19 | ball_dy: 0.01, 20 | ball_radius: 0.2, 21 | platform_base_y: -5.0, 22 | piston_length: 2.0, 23 | piston_speed: 0.0, 24 | piston_length_min: 1.5, 25 | piston_length_max: 2.3, 26 | paddle_half_width: 1.5, 27 | paddle_thickness: 0.15, 28 | hinge_angle: 0.05, 29 | hinge_angular_speed: 0.0, 30 | hinge_angle_max: 0.7, 31 | g: 9.81, 32 | T: 0, 33 | contact_point_rel_x: 0, 34 | contact_distance: 10, 35 | edge_balance_win_condition_counter: 0, 36 | bounce_win_condition_counter: 0, 37 | show_zero_cross: false, 38 | }; 39 | 40 | 41 | Models.BouncingBallPlatform.prototype.simulate = function (dt, controlFunc) 42 | { 43 | var commands = controlFunc( 44 | { 45 | x:this.ball_x, 46 | y:this.ball_y, 47 | vx:this.ball_dx, 48 | vy:this.ball_dy, 49 | }, 50 | { 51 | length:this.piston_length, 52 | speed:this.piston_speed, 53 | }, 54 | { 55 | angle:this.hinge_angle, 56 | speed:this.hinge_angular_speed, 57 | }, 58 | this.T 59 | ); 60 | if(typeof commands != 'object' || typeof commands.pistonAcceleration != 'number' || typeof commands.hingeAcceleration != 'number') 61 | throw "Error: The controlFunction must return an object: {pistonAcceleration:number, hingeAcceleration:number}"; 62 | 63 | commands.pistonAcceleration = Math.max(-10.0, Math.min(10.0, commands.pistonAcceleration)); // input limits 64 | commands.hingeAcceleration = Math.max(-15.0, Math.min(15.0, commands.hingeAcceleration)); // input limits 65 | 66 | var n_substeps = 5; 67 | for (var i = 0; i < n_substeps; i++) 68 | { 69 | this.simulate_substep(dt/n_substeps, commands); 70 | } 71 | } 72 | 73 | Models.BouncingBallPlatform.prototype.simulate_substep = function (dt, commands) 74 | { 75 | this.ball_x += dt * this.ball_dx; 76 | this.ball_y += dt * this.ball_dy + (0.5*dt*dt)*(-this.g); 77 | this.ball_dy -= dt * this.g; 78 | 79 | if(this.ball_y - this.ball_radius < this.platform_base_y) this.ball_dy = Math.abs(this.ball_dy); 80 | 81 | this.hinge_angular_speed += dt * commands.hingeAcceleration; 82 | this.hinge_angle += dt * this.hinge_angular_speed; 83 | 84 | this.piston_speed += dt * commands.pistonAcceleration; 85 | this.piston_length += dt * this.piston_speed; 86 | 87 | if(this.piston_length > this.piston_length_max) 88 | { 89 | this.piston_length = this.piston_length_max; 90 | this.piston_speed = Math.min(0.0, this.piston_speed); 91 | } 92 | 93 | if(this.piston_length < this.piston_length_min) 94 | { 95 | this.piston_length = this.piston_length_min; 96 | this.piston_speed = Math.max(0.0, this.piston_speed); 97 | } 98 | 99 | if(this.hinge_angle > this.hinge_angle_max) 100 | { 101 | this.hinge_angle = this.hinge_angle_max; 102 | this.hinge_angular_speed = Math.min(0.0, this.hinge_angular_speed); 103 | } 104 | 105 | if(this.hinge_angle < -this.hinge_angle_max) 106 | { 107 | this.hinge_angle = -this.hinge_angle_max; 108 | this.hinge_angular_speed = Math.max(0.0, this.hinge_angular_speed); 109 | } 110 | 111 | // Collision calculations 112 | var C = Math.cos(this.hinge_angle); 113 | var S = Math.sin(this.hinge_angle); 114 | 115 | var dC_dt = -Math.sin(this.hinge_angle) * this.hinge_angular_speed; 116 | var dS_dt = Math.cos(this.hinge_angle) * this.hinge_angular_speed; 117 | 118 | var paddle_center_x = - S * this.paddle_thickness; 119 | var paddle_center_y = this.platform_base_y + this.piston_length + C * this.paddle_thickness; 120 | 121 | var paddle_center_velocity_x = - dS_dt * this.paddle_thickness; 122 | var paddle_center_velocity_y = this.piston_speed + dC_dt * this.paddle_thickness; 123 | 124 | var ball_rel_x = C * (this.ball_x - paddle_center_x) + S * (this.ball_y - paddle_center_y); 125 | var ball_rel_y = -S * (this.ball_x - paddle_center_x) + C * (this.ball_y - paddle_center_y); 126 | 127 | var contact_point_rel_x = Math.max(-this.paddle_half_width, Math.min(this.paddle_half_width, ball_rel_x)); 128 | this.contact_point_rel_x = contact_point_rel_x; 129 | 130 | var contact_point_x = paddle_center_x + C * contact_point_rel_x; 131 | var contact_point_y = paddle_center_y + S * contact_point_rel_x; 132 | 133 | var contact_point_velocity_x = paddle_center_velocity_x + dC_dt * contact_point_rel_x; 134 | var contact_point_velocity_y = paddle_center_velocity_y + dS_dt * contact_point_rel_x; 135 | 136 | var contact_normal_x = this.ball_x - contact_point_x; 137 | var contact_normal_y = this.ball_y - contact_point_y; 138 | 139 | var contact_distance = Math.sqrt(contact_normal_x*contact_normal_x + contact_normal_y*contact_normal_y); 140 | this.contact_distance = contact_distance; 141 | contact_normal_x /= contact_distance; 142 | contact_normal_y /= contact_distance; 143 | 144 | var ball_delta_velocity_x = this.ball_dx - contact_point_velocity_x; 145 | var ball_delta_velocity_y = this.ball_dy - contact_point_velocity_y; 146 | 147 | var ball_delta_velocity_normal = contact_normal_x * ball_delta_velocity_x + contact_normal_y * ball_delta_velocity_y; 148 | 149 | ball_delta_velocity_x += contact_normal_x * (-1.9 * ball_delta_velocity_normal); 150 | ball_delta_velocity_y += contact_normal_y * (-1.9 * ball_delta_velocity_normal); 151 | 152 | if(ball_delta_velocity_normal < 0 && contact_distance < this.ball_radius) 153 | { 154 | this.ball_dx = ball_delta_velocity_x + contact_point_velocity_x; 155 | this.ball_dy = ball_delta_velocity_y + contact_point_velocity_y; 156 | 157 | this.ball_x = contact_point_x + contact_normal_x * this.ball_radius; 158 | this.ball_y = contact_point_y + contact_normal_y * this.ball_radius; 159 | } 160 | 161 | // Balance level win condition 162 | if( 163 | (contact_distance < 2 * this.ball_radius) && 164 | (Math.abs(contact_point_rel_x + this.paddle_half_width) < 0.05) && 165 | (Math.abs(this.ball_dx) < 0.01) && 166 | (Math.abs(this.ball_dy) < 0.5) && 167 | (Math.abs(this.hinge_angular_speed) < 0.01) && 168 | (Math.abs(this.piston_speed) < 0.01) && 169 | (this.hinge_angle < -0.02) 170 | ) 171 | { 172 | this.edge_balance_win_condition_counter += 1; 173 | } 174 | else 175 | { 176 | this.edge_balance_win_condition_counter = 0; 177 | } 178 | 179 | // Bounce level win condition 180 | var apogee = this.ball_y + this.ball_dy*this.ball_dy / (2 * this.g); 181 | if( 182 | (Math.abs(apogee) < 0.01) && 183 | (Math.abs(this.ball_dx) < 0.01) && 184 | (Math.abs(this.ball_x) < 0.01) 185 | ) 186 | { 187 | this.bounce_win_condition_counter += 1; 188 | } 189 | else 190 | { 191 | this.bounce_win_condition_counter = 0; 192 | } 193 | 194 | this.T += dt; 195 | return this; 196 | } 197 | 198 | 199 | Models.BouncingBallPlatform.prototype.draw = function (ctx, canvas) 200 | { 201 | ctx.scale(0.7,0.7); 202 | 203 | if(this.show_zero_cross) 204 | { 205 | ctx.strokeStyle="#333366"; 206 | drawLine(ctx,0.1,0.1,-0.1,-0.1,0.05); 207 | drawLine(ctx,0.1,-0.1,-0.1,0.1,0.05); 208 | } 209 | 210 | 211 | // Piston and cylinder 212 | ctx.fillStyle = '#aaaaaa'; 213 | ctx.fillRect(-0.1, this.platform_base_y, 0.2, this.piston_length); 214 | ctx.fillStyle = '#555555'; 215 | ctx.fillRect(-0.2, this.platform_base_y, 0.4, this.piston_length_min - 0.2); 216 | 217 | // Floor 218 | ctx.strokeStyle="#222222"; 219 | drawLine(ctx,-1,this.platform_base_y,1,this.platform_base_y,1/40.0); 220 | 221 | ctx.save(); 222 | ctx.translate(0,this.platform_base_y); 223 | ctx.translate(0,this.piston_length); 224 | ctx.rotate(this.hinge_angle); 225 | 226 | // Paddle 227 | ctx.fillStyle = '#0000aa'; 228 | ctx.fillRect(-this.paddle_half_width, 0.0, 2*this.paddle_half_width, this.paddle_thickness); 229 | 230 | // Hinge 231 | ctx.beginPath(); 232 | ctx.arc(0, 0, 0.12, 0, 2 * Math.PI, false); 233 | ctx.fillStyle = '#ffffff'; 234 | ctx.fill(); 235 | ctx.beginPath(); 236 | ctx.arc(0, 0, 0.09, 0, 2 * Math.PI, false); 237 | ctx.fillStyle = '#000000'; 238 | ctx.fill(); 239 | 240 | ctx.restore(); 241 | 242 | 243 | // Ball 244 | ctx.beginPath(); 245 | ctx.arc(this.ball_x, this.ball_y, this.ball_radius, 0, 2 * Math.PI, false); 246 | ctx.fillStyle = '#4444FF'; 247 | ctx.fill(); 248 | 249 | 250 | /* 251 | 252 | // Collision debugging graphics 253 | 254 | var C = Math.cos(this.hinge_angle); 255 | var S = Math.sin(this.hinge_angle); 256 | 257 | var dC_dt = -Math.sin(this.hinge_angle) * this.hinge_angular_speed; 258 | var dS_dt = Math.cos(this.hinge_angle) * this.hinge_angular_speed; 259 | 260 | var paddle_center_x = - S * this.paddle_thickness; 261 | var paddle_center_y = this.platform_base_y + this.piston_length + C * this.paddle_thickness; 262 | 263 | var paddle_center_velocity_x = - dS_dt * this.paddle_thickness; 264 | var paddle_center_velocity_y = this.piston_speed + dC_dt * this.paddle_thickness; 265 | 266 | var ball_rel_x = C * (this.ball_x - paddle_center_x) + S * (this.ball_y - paddle_center_y); 267 | var ball_rel_y = -S * (this.ball_x - paddle_center_x) + C * (this.ball_y - paddle_center_y); 268 | 269 | var contact_point_rel_x = Math.max(-this.paddle_half_width, Math.min(this.paddle_half_width, ball_rel_x)); 270 | 271 | var contact_point_x = paddle_center_x + C * contact_point_rel_x; 272 | var contact_point_y = paddle_center_y + S * contact_point_rel_x; 273 | 274 | var contact_point_velocity_x = paddle_center_velocity_x + dC_dt * contact_point_rel_x; 275 | var contact_point_velocity_y = paddle_center_velocity_y + dS_dt * contact_point_rel_x; 276 | 277 | var contact_normal_x = this.ball_x - contact_point_x; 278 | var contact_normal_y = this.ball_y - contact_point_y; 279 | 280 | var contact_distance = Math.sqrt(contact_normal_x*contact_normal_x + contact_normal_y*contact_normal_y); 281 | contact_normal_x /= contact_distance; 282 | contact_normal_y /= contact_distance; 283 | 284 | var ball_delta_velocity_x = this.ball_dx - contact_point_velocity_x; 285 | var ball_delta_velocity_y = this.ball_dy - contact_point_velocity_y; 286 | 287 | var ball_delta_velocity_normal = contact_normal_x * ball_delta_velocity_x + contact_normal_y * ball_delta_velocity_y; 288 | 289 | ball_delta_velocity_x += contact_normal_x * (-1.9 * ball_delta_velocity_normal); 290 | ball_delta_velocity_y += contact_normal_y * (-1.9 * ball_delta_velocity_normal); 291 | 292 | 293 | var ball_dx_new = ball_delta_velocity_x + contact_point_velocity_x; 294 | var ball_dy_new = ball_delta_velocity_y + contact_point_velocity_y; 295 | 296 | 297 | ctx.strokeStyle="#ff0000"; 298 | var X = contact_point_x; 299 | var Y = contact_point_y; 300 | drawLine(ctx,X+0.1,Y+0.1,X-0.1,Y-0.1,0.02); 301 | drawLine(ctx,X+0.1,Y-0.1,X-0.1,Y+0.1,0.02); 302 | 303 | 304 | 305 | ctx.strokeStyle="#ff00ff"; 306 | drawLine(ctx, contact_point_x, contact_point_y, contact_point_x + contact_normal_x, contact_point_y + contact_normal_y,0.03); 307 | 308 | if(ball_delta_velocity_normal < 0) 309 | { 310 | ctx.strokeStyle="#00ffff"; 311 | drawLine(ctx, contact_point_x, contact_point_y, contact_point_x + ball_dx_new, contact_point_y + ball_dy_new, 0.03); 312 | }*/ 313 | 314 | 315 | } 316 | 317 | 318 | Models.BouncingBallPlatform.prototype.infoText = function () 319 | { 320 | return "ball.x = " + this.ball_x.toFixed(2).padStart(6,' ') 321 | + "\nball.vx = " + this.ball_dx.toFixed(2).padStart(6,' ') 322 | + "\nball.y = " + this.ball_y.toFixed(2).padStart(6,' ') 323 | + "\nball.vy = " + this.ball_dy.toFixed(2).padStart(6,' ') 324 | + "\npiston.length = " + this.piston_length.toFixed(2).padStart(6,' ') 325 | + "\npiston.speed = " + this.piston_speed.toFixed(2).padStart(6,' ') 326 | + "\nhinge.angle = " + this.hinge_angle.toFixed(2).padStart(6,' ') 327 | + "\nhinge.speed = " + this.hinge_angular_speed.toFixed(2).padStart(6,' ') 328 | + "\nT = " + this.T.toFixed(2).padStart(6,' '); 329 | } -------------------------------------------------------------------------------- /js/models/CruiseControl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | if (typeof Models === 'undefined') var Models = {}; 3 | 4 | Models.CruiseControl = function(params) 5 | { 6 | var nVars = Object.keys(this.vars).length; 7 | for(var i = 0; i < nVars; i++) 8 | { 9 | var key = Object.keys(this.vars)[i]; 10 | this[key] = (typeof params[key] == 'undefined')?this.vars[key]:params[key]; 11 | } 12 | } 13 | 14 | Models.CruiseControl.prototype.vars = 15 | { 16 | maxAcceleration: 6, 17 | throttle: 0, 18 | throttle_cmd: 0, 19 | targetSpeed: 50, 20 | speed: 30, 21 | speedErrorTolerance: .2, 22 | speedHoldTimer: 0, 23 | drag: 0.0015, 24 | g: 9.81, 25 | carImg:'', 26 | speedometerImg:'', 27 | inclination: 5./180*Math.PI, 28 | T: 0, 29 | }; 30 | 31 | Models.CruiseControl.prototype.simulate = function (dt, controlFunc) 32 | { 33 | var input = controlFunc({speed:this.speed,targetSpeed:this.targetSpeed,inclination:this.inclination,speedHoldTimer:this.speedHoldTimer,T:this.T}); // call user controller 34 | if(typeof input != 'number') 35 | throw "Error: The controlFunction must return a number."; 36 | 37 | // input limit 38 | this.throttle_cmd = Math.max(-1,Math.min(1,input)); 39 | integrationStep(this, ['speed', 'throttle'], dt); 40 | 41 | if(Math.abs(this.speed - this.targetSpeed) < this.speedErrorTolerance && Math.abs(this.throttle_cmd - this.throttle) < 0.1) 42 | this.speedHoldTimer += dt; 43 | else 44 | this.speedHoldTimer = 0; 45 | } 46 | 47 | Models.CruiseControl.prototype.ode = function (x) { 48 | return [ 49 | x[1] * this.maxAcceleration - this.drag * x[0] * Math.abs(x[0]) - Math.sin(this.inclination) * this.g, 50 | 2.0 * (this.throttle_cmd - x[1]) 51 | ]; 52 | } 53 | 54 | 55 | Models.CruiseControl.prototype.draw = function (ctx, canvas) { 56 | this.drawSpeedometer(ctx, canvas); 57 | this.drawInclination(ctx, canvas); 58 | } 59 | 60 | Models.CruiseControl.prototype.drawInclination = function (ctx, canvas) { 61 | ctx.save(); 62 | ctx.translate(-1.8,0); 63 | ctx.rotate(this.inclination); 64 | 65 | var imgData = ImageDataCache.get(this.carImg); 66 | if(imgData){ 67 | ctx.save(); 68 | ctx.scale(3.2/imgData.width,-3.2/imgData.width); 69 | ctx.translate(-imgData.width/2,-imgData.height/2); 70 | ctx.drawImage(imgData.image,0,0); 71 | ctx.restore(); 72 | } 73 | ctx.lineCap = 'round'; 74 | ctx.strokeStyle = '#ff0000'; 75 | var t = 1.3*this.throttle; 76 | var d = .1; 77 | var s = 2*Math.sign(this.throttle); 78 | drawLine(ctx,t,0,0,0,0.04); 79 | drawLine(ctx,t,0,t-s*d,-d,0.04); 80 | drawLine(ctx,t,0,t-s*d,d,0.04); 81 | 82 | ctx.restore(); 83 | } 84 | 85 | Models.CruiseControl.prototype.drawSpeedometer = function (ctx, canvas) { 86 | ctx.save(); 87 | ctx.translate(1.8,0); 88 | 89 | var speedometerimgData = ImageDataCache.get(this.speedometerImg); 90 | if(speedometerimgData){ 91 | ctx.save(); 92 | ctx.scale(3.2/speedometerimgData.width,-3.2/speedometerimgData.width); 93 | ctx.translate(-speedometerimgData.width/2,-speedometerimgData.height/2); 94 | ctx.drawImage(speedometerimgData.image,0,0); 95 | ctx.restore(); 96 | } 97 | ctx.lineCap = 'round'; 98 | 99 | // target speed indicator 100 | for(var i=0;i<1.2;i+=0.25){ 101 | ctx.strokeStyle = '#ff00ff'; 102 | var angle = Math.PI*(7/6-Math.abs(this.targetSpeed/120)); 103 | var s = Math.sin(angle); 104 | var c = Math.cos(angle); 105 | drawLine(ctx,c*i,s*i,c*(i+.1),s*(i+.1),0.02); 106 | } 107 | 108 | // speed indicator 109 | ctx.strokeStyle = '#ff0000'; 110 | var angle = Math.PI*(7/6-Math.abs(this.speed/120)); 111 | var s = Math.sin(angle); 112 | var c = Math.cos(angle); 113 | drawLine(ctx,c,s,0,0,0.05); 114 | 115 | // reverse indicator 116 | if(this.speed<0) { 117 | ctx.save(); 118 | ctx.translate(0,-1); 119 | ctx.scale(0.05,-0.05); 120 | ctx.font="10px Verdana"; 121 | ctx.textAlign="center"; 122 | ctx.fillStyle="#000000"; 123 | ctx.fillText("R",0,0); 124 | ctx.restore(); 125 | } 126 | 127 | 128 | ctx.restore(); 129 | } 130 | 131 | Models.CruiseControl.prototype.infoText = function () 132 | { 133 | return "vehicle.speed = " + round(this.speed,2) 134 | + "\nvehicle.targetSpeed = " + round(this.targetSpeed,2) 135 | + "\nvehicle.inclination = " + round(this.inclination,2) 136 | + "\nvehicle.speedHoldTimer = " + round(this.speedHoldTimer,2) 137 | + "\nvehicle.T = " + round(this.T,2); 138 | } 139 | -------------------------------------------------------------------------------- /js/models/DoublePendulum.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | if (typeof Models === 'undefined') var Models = {}; 3 | 4 | Models.DoublePendulum = function(params) 5 | { 6 | var nVars = Object.keys(this.vars).length; 7 | for(var i = 0; i < nVars; i++) 8 | { 9 | var key = Object.keys(this.vars)[i]; 10 | this[key] = (typeof params[key] == 'undefined')?this.vars[key]:params[key]; 11 | } 12 | } 13 | 14 | Models.DoublePendulum.prototype.vars = 15 | { 16 | m0: 10, 17 | m1: 2, 18 | m2: 4, 19 | L1: 0.618, 20 | L2: 1, 21 | g: 2, 22 | theta1: 0.001, 23 | dtheta1: 0.001, 24 | theta2: 0.001, 25 | dtheta2: 0.001, 26 | x: -1, 27 | dx: 0, 28 | F: 0, 29 | T: 0 30 | }; 31 | 32 | Models.DoublePendulum.prototype.simulate = function (dt, controlFunc) 33 | { 34 | this.F = controlFunc({x:this.x,dx:this.dx,theta1:this.theta1,dtheta1:this.dtheta1,theta2:this.theta2,dtheta2:this.dtheta2,T:this.T}); 35 | this.F = Math.max(-50,Math.min(50,this.F)); 36 | if(typeof this.F != 'number' || isNaN(this.F)) throw "Error: The controlFunction must return a number."; 37 | integrationStep(this, ['x', 'dx', 'theta1', 'dtheta1', 'theta2', 'dtheta2'], dt); 38 | } 39 | 40 | Models.DoublePendulum.prototype.ode = function (x) 41 | { 42 | let s1 = Math.sin(x[2]); 43 | let c1 = Math.cos(x[2]); 44 | let s2 = Math.sin(x[4]); 45 | let c2 = Math.cos(x[4]); 46 | let dthetasq1 = x[3] * x[3]; 47 | let dthetasq2 = x[5] * x[5]; 48 | let t = this; 49 | 50 | // colns:ddx0 ddtheta1 ddtheta2 ddx1 ddx2 ddy1 ddy2 T1 T2 51 | let M =[[t.m0, 0, 0, 0, 0, 0, 0, -s1, 0], 52 | [ 0, 0, 0, t.m1, 0, 0, 0, s1, -s2], 53 | [ 0, 0, 0, 0, t.m2, 0, 0, 0, s2], 54 | [ 0, 0, 0, 0, 0, t.m1, 0, c1, -c2], 55 | [ 0, 0, 0, 0, 0, 0, t.m2, 0, c2], 56 | [ -1,-t.L1*c1, 0, 1, 0, 0, 0, 0, 0], 57 | [ 0, 0, -t.L2*c2, -1, 1, 0, 0, 0, 0], 58 | [ 0, t.L1*s1, 0, 0, 0, 1, 0, 0, 0], 59 | [ 0, 0, t.L2*s2, 0, 0, -1, 1, 0, 0],]; 60 | 61 | let b = [ 62 | this.F, 63 | 0, 64 | 0, 65 | - this.m1* this.g, 66 | - this.m2* this.g, 67 | - this.L1*s1*dthetasq1, 68 | - this.L2*s2*dthetasq2, 69 | - this.L1*c1*dthetasq1, 70 | - this.L2*c2*dthetasq2 71 | ]; 72 | let ddx = numeric.solve(M,b); 73 | return [x[1],ddx[0],x[3],ddx[1],x[5],ddx[2]]; 74 | } 75 | 76 | Models.DoublePendulum.prototype.draw = function (ctx, canvas) 77 | { 78 | var L = this.L1 + this.L2; 79 | ctx.translate(0,-L); 80 | 81 | var cartWidth = 0.2*L; 82 | var cartHeight = 0.7*cartWidth; 83 | 84 | var tipX = this.x+this.L1*Math.sin(this.theta1); 85 | var tipY = this.L1*Math.cos(this.theta1)+cartHeight; 86 | var tip2X = this.L2*Math.sin(this.theta2) + tipX; 87 | var tip2Y = this.L2*Math.cos(this.theta2) + tipY; 88 | 89 | // ground 90 | ctx.strokeStyle="#333366"; 91 | drawLine(ctx,-100,-.025,100,-.025,0.05); 92 | 93 | // cart 94 | ctx.fillStyle="#4444FF"; 95 | ctx.fillRect(this.x-cartWidth/2,0,cartWidth,cartHeight); 96 | 97 | // shaft 98 | ctx.strokeStyle="#AAAAFF"; 99 | ctx.lineCap = 'round'; 100 | drawLine(ctx,this.x,cartHeight,tipX,tipY,L/20.0); 101 | drawLine(ctx,tipX,tipY,tip2X,tip2Y,L/20.0); 102 | 103 | // tip-mass 104 | ctx.beginPath(); 105 | ctx.arc(tipX, tipY, 0.1, 0, 2 * Math.PI, false); 106 | ctx.fillStyle = '#4444FF'; 107 | ctx.fill(); 108 | ctx.beginPath(); 109 | ctx.arc(tip2X, tip2Y, 0.1, 0, 2 * Math.PI, false); 110 | ctx.fillStyle = '#4444FF'; 111 | ctx.fill(); 112 | 113 | // force arrow 114 | ctx.strokeStyle="#FF0000"; 115 | ctx.lineCap = 'round'; 116 | drawArrow(ctx, this.x, 0.5*cartHeight, 0.1*this.F, 0, 0.05, 1/40.0); 117 | } 118 | 119 | Models.DoublePendulum.prototype.infoText = function () 120 | { 121 | return "/* Horizontal position */ pendulum.x = " + round(this.x,2) 122 | + "\n/* Horizontal velocity */ pendulum.dx = " + round(this.dx,2) 123 | + "\n/* Angle from vertical (lower) */ pendulum.theta1 = " + round(this.theta1,2) 124 | + "\n/* Angular velocity (lower) */ pendulum.dtheta1 = " + round(this.dtheta1,2) 125 | + "\n/* Angle from vertical (upper) */ pendulum.theta2 = " + round(this.theta2,2) 126 | + "\n/* Angular velocity (upper) */ pendulum.dtheta2 = " + round(this.dtheta2,2) 127 | + "\n/* Simulation time */ pendulum.T = " + round(this.T,2); 128 | } 129 | -------------------------------------------------------------------------------- /js/models/Multirotor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | if (typeof Models === 'undefined') var Models = {}; 3 | 4 | Models.Multirotor = function(params) 5 | { 6 | var nVars = Object.keys(this.vars).length; 7 | for(var i = 0; i < nVars; i++) 8 | { 9 | var key = Object.keys(this.vars)[i]; 10 | this[key] = (typeof params[key] == 'undefined')?this.vars[key]:params[key]; 11 | } 12 | } 13 | 14 | Models.Multirotor.prototype.vars = 15 | { 16 | mass: 1, 17 | I: 1/.12/100, // moment of inertia 18 | Length: .1, 19 | g: 9.81, 20 | theta: .01, 21 | dtheta: 0, 22 | thrustLeft: 9.81/2, 23 | thrustRight: 9.81/2, 24 | maxThrust: 20, 25 | x: 0, 26 | dx: 0, 27 | y: 0, 28 | dy: 0, 29 | T: 0, 30 | imgURL:'', 31 | }; 32 | 33 | Models.Multirotor.prototype.simulate = function (dt, controlFunc) 34 | { 35 | var input = controlFunc({x: this.x, dx: this.dx, y: this.y, dy: this.dy, theta: this.theta, dtheta: this.dtheta, T: this.T}); // call user controller 36 | if(typeof input != 'object' || typeof input.thrustLeft != 'number' || typeof input.thrustRight != 'number') 37 | throw "Error: The controlFunction must return an object: {thrustLeft:number, thrustRight:number}"; 38 | this.thrustLeft_cmd = Math.max(0,Math.min(this.maxThrust,input.thrustLeft)); 39 | this.thrustRight_cmd = Math.max(0,Math.min(this.maxThrust,input.thrustRight)); 40 | integrationStep(this, ['x', 'dx', 'y', 'dy', 'theta', 'dtheta', 'thrustLeft', 'thrustRight'], dt); 41 | } 42 | 43 | Models.Multirotor.prototype.ode = function (x) { 44 | return[ 45 | x[1], 46 | Math.sin(x[4]) / this.mass * (this.thrustLeft+this.thrustRight), 47 | x[3], 48 | Math.cos(x[4]) / this.mass * (this.thrustLeft+this.thrustRight) - this.g, 49 | x[5], 50 | (this.thrustLeft-this.thrustRight) * this.Length / this.I, 51 | 12.0 * (this.thrustLeft_cmd - x[6]), 52 | 12.0 * (this.thrustRight_cmd - x[7]) 53 | ]; 54 | } 55 | 56 | 57 | 58 | 59 | Models.Multirotor.prototype.drawVehicle = function (ctx, canvas) { 60 | ctx.save(); 61 | ctx.translate(this.x,this.y); 62 | ctx.rotate(-this.theta); 63 | ctx.scale(this.Length,this.Length); 64 | 65 | var imgData = ImageDataCache.get(this.imgURL); 66 | if(imgData){ 67 | ctx.save(); 68 | ctx.scale(3.6/imgData.width,-3.6/imgData.width); 69 | ctx.translate(-imgData.width/2,-imgData.height/2); 70 | ctx.drawImage(imgData.image,0,0); 71 | ctx.restore(); 72 | } else { 73 | ctx.lineJoin = 'round'; 74 | ctx.lineCap = 'round'; 75 | drawLine(ctx,-2,0,2,0,.02); 76 | drawLine(ctx,0,-1,0,1,.02); 77 | } 78 | 79 | // draw force vectors 80 | ctx.lineJoin = 'round'; 81 | ctx.lineCap = 'round'; 82 | ctx.strokeStyle="#FF0000"; 83 | var forceScale = 0.4; 84 | var arrowScale = 0.2; 85 | if(this.thrustLeft>0){ 86 | drawLine(ctx,-1,0.7,-1,0.7+forceScale*this.thrustLeft,.1); 87 | drawLine(ctx,-1,0.7+forceScale*this.thrustLeft,-1+arrowScale,-arrowScale+0.7+forceScale*this.thrustLeft,.1); 88 | drawLine(ctx,-1,0.7+forceScale*this.thrustLeft,-1-arrowScale,-arrowScale+0.7+forceScale*this.thrustLeft,.1); 89 | } 90 | 91 | if(this.thrustRight>0){ 92 | drawLine(ctx,1,0.7,1,0.7+forceScale*this.thrustRight,.1); 93 | drawLine(ctx,1,0.7+forceScale*this.thrustRight,1+arrowScale,-arrowScale+0.7+forceScale*this.thrustRight,.1); 94 | drawLine(ctx,1,0.7+forceScale*this.thrustRight,1-arrowScale,-arrowScale+0.7+forceScale*this.thrustRight,.1); 95 | } 96 | 97 | 98 | ctx.restore(); 99 | } 100 | 101 | Models.Multirotor.prototype.infoText = function () 102 | { 103 | return "/* Horizontal position */ vehicle.x = " + round(this.x,2) 104 | + "\n/* Horizontal velocity */ vehicle.dx = " + round(this.dx,2) 105 | + "\n/* Vertical position */ vehicle.y = " + round(this.y,2) 106 | + "\n/* Vertical velocity */ vehicle.dy = " + round(this.dy,2) 107 | + "\n/* Angle from vertical */ vehicle.theta = " + round(this.theta,2) 108 | + "\n/* Angular velocity */ vehicle.dtheta = " + round(this.dtheta,2) 109 | + "\n/* Simulation time */ vehicle.T = " + round(this.T,2); 110 | } 111 | -------------------------------------------------------------------------------- /js/models/RocketLanding.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | if (typeof Models === 'undefined') var Models = {}; 3 | 4 | Models.RocketLanding = function(params) 5 | { 6 | var nVars = Object.keys(this.vars).length; 7 | for(var i = 0; i < nVars; i++) 8 | { 9 | var key = Object.keys(this.vars)[i]; 10 | this[key] = (typeof params[key] == 'undefined')?this.vars[key]:params[key]; 11 | } 12 | } 13 | 14 | Models.RocketLanding.prototype.vars = 15 | { 16 | TWR: 2, 17 | throttle: 1, 18 | throttle_cmd: 1, 19 | g: 9.81, 20 | theta: 0, 21 | dtheta: 0, 22 | gimbalAngle: -0.1, 23 | gimbalAngle_cmd: -0.1, 24 | Length: 40, 25 | Width: 2, 26 | x: 0, 27 | dx: 0, 28 | y: 0, 29 | dy: 0, 30 | throttleLimit: 0, 31 | T: 0, 32 | landingConstraints: {dx:5,dy:5,dtheta:0.1,sinTheta:0.05}, 33 | }; 34 | 35 | Models.RocketLanding.prototype.detectCollision = function () 36 | { 37 | var L = this.Length; 38 | var W = this.Width; 39 | var s = Math.sin(this.theta); 40 | var c = Math.cos(this.theta); 41 | // points relative to the rockets CG that form a convex hull. 42 | var outerPoints = [{x:0,y:L/2},{x:1.8*W,y:-L/2-W},{x:-1.8*W,y:-L/2-W}]; 43 | for (var i = 0; i < outerPoints.length; i++) 44 | { 45 | var p = outerPoints[i]; 46 | if(p.x*s+p.y*c+this.y < 0) return true; 47 | } 48 | return false; 49 | } 50 | 51 | 52 | Models.RocketLanding.prototype.landed = function () 53 | { 54 | return this.detectCollision() 55 | && Math.abs(this.x) < 30 56 | && Math.abs(this.dx) < this.landingConstraints.dx 57 | && Math.abs(this.dy) < this.landingConstraints.dy 58 | && Math.abs(this.dtheta) < this.landingConstraints.dtheta 59 | && Math.abs(Math.sin(this.theta)) < this.landingConstraints.sinTheta 60 | && Math.cos(this.theta) > 0; 61 | } 62 | 63 | Models.RocketLanding.prototype.crashed = function () 64 | { 65 | return this.detectCollision() && !this.landed(); 66 | } 67 | 68 | Models.RocketLanding.prototype.simulate = function (dt, controlFunc) 69 | { 70 | if(!this.detectCollision()) 71 | { 72 | var input = controlFunc({x:this.x,dx:this.dx,y:this.y,dy:this.dy,theta:this.theta,dtheta:this.dtheta,T:this.T}); // call user controller 73 | if(typeof input != 'object' || typeof input.throttle != 'number' || typeof input.gimbalAngle != 'number') 74 | throw "Error: The controlFunction must return an object: {throttle:number, gimbalAngle:number}"; 75 | this.throttle_cmd = Math.max(this.throttleLimit,Math.min(1,input.throttle)); // input limits 76 | this.gimbalAngle_cmd = Math.max(-.2,Math.min(.2,input.gimbalAngle)); 77 | integrationStep(this, ['x','dx','y','dy','theta','dtheta','throttle','gimbalAngle'], dt); 78 | } 79 | } 80 | 81 | Models.RocketLanding.prototype.ode = function (x) 82 | { 83 | let currentTWR = this.TWR * x[6]; 84 | let gimbalAngle = x[7]; 85 | return [ 86 | x[1], 87 | this.g * currentTWR * Math.sin(x[4]+gimbalAngle), 88 | x[3], 89 | this.g * (currentTWR * Math.cos(x[4]+gimbalAngle)-1), 90 | x[5], 91 | -this.g * currentTWR * 6 / this.Length * Math.sin(gimbalAngle), 92 | 10.0 * (this.throttle_cmd - x[6]), 93 | 10.0 * (this.gimbalAngle_cmd - x[7]) 94 | ]; 95 | } 96 | 97 | 98 | Models.RocketLanding.prototype.draw = function (ctx, canvas) 99 | { 100 | ctx.scale(0.014,0.014); 101 | ctx.translate(0,-250); 102 | 103 | this.drawRocket(ctx, canvas, -1); 104 | this.drawGround(ctx, canvas); 105 | 106 | if(this.detectCollision()) 107 | { 108 | ctx.save(); 109 | ctx.scale(1,-1); 110 | ctx.font="10px Verdana"; 111 | ctx.textAlign="center"; 112 | if(this.landed()) 113 | { 114 | ctx.fillStyle="#009900"; 115 | ctx.fillText("Landed!",0,-80); 116 | } 117 | else 118 | { 119 | ctx.fillStyle="#990000"; 120 | ctx.fillText("CRASHED!",0,-80); 121 | } 122 | ctx.restore(); 123 | } 124 | } 125 | 126 | Models.RocketLanding.prototype.drawGround = function (ctx, canvas){ 127 | ctx.strokeStyle="#000055"; 128 | drawLine(ctx,-10000,-1,10000,-1,2); 129 | for(var x = -30; x <= 30; x+=5) 130 | { 131 | drawLine(ctx,x,-1,x,-5,1); 132 | } 133 | } 134 | 135 | Models.RocketLanding.prototype.drawRocket = function (ctx, canvas, i){ 136 | 137 | var L = this.Length; 138 | var W = this.Width; 139 | ctx.save(); 140 | ctx.translate(this.x,this.y); 141 | ctx.rotate(-this.theta); 142 | 143 | ctx.lineWidth=L/40; 144 | ctx.lineJoin = 'round'; 145 | ctx.lineCap = 'round'; 146 | 147 | // exhaust 148 | ctx.strokeStyle="#FF0000"; 149 | ctx.beginPath(); 150 | ctx.moveTo(W/4,-L/2); 151 | ctx.lineTo(-3*W*this.throttle*Math.sin(2*this.gimbalAngle),-L/2-3*W*this.throttle*Math.cos(2*this.gimbalAngle)); 152 | ctx.lineTo(-W/4,-L/2); 153 | ctx.stroke(); 154 | 155 | // hull 156 | ctx.strokeStyle="#4444FF"; 157 | ctx.beginPath(); 158 | ctx.moveTo(0,L/2); 159 | ctx.lineTo(-W/2,L/2-W); 160 | ctx.lineTo(-W/2,-L/2); 161 | ctx.lineTo(W/2,-L/2); 162 | ctx.lineTo(W/2,L/2-W); 163 | ctx.closePath(); 164 | ctx.stroke(); 165 | 166 | // left leg 167 | ctx.beginPath(); 168 | ctx.moveTo(-W/2,-L/2); 169 | ctx.lineTo(-1.8*W,-L/2-W); 170 | ctx.lineTo(-W/2,-L/2+W); 171 | ctx.stroke(); 172 | 173 | // right leg 174 | ctx.beginPath(); 175 | ctx.moveTo(W/2,-L/2); 176 | ctx.lineTo(1.8*W,-L/2-W); 177 | ctx.lineTo(W/2,-L/2+W); 178 | ctx.stroke(); 179 | 180 | if(i>=0){ 181 | ctx.save(); 182 | ctx.rotate(this.theta); 183 | ctx.scale(1,-1); 184 | ctx.font="10px Verdana"; 185 | ctx.textAlign="center"; 186 | ctx.fillStyle="#000000"; 187 | ctx.fillText(""+i,0,-40); 188 | ctx.restore(); 189 | } 190 | 191 | 192 | ctx.restore(); 193 | } 194 | 195 | Models.RocketLanding.prototype.infoText = function () 196 | { 197 | return "/* Horizontal position */ rocket.x = " + round(this.x,2) 198 | + "\n/* Horizontal velocity */ rocket.dx = " + round(this.dx,2) 199 | + "\n/* Vertical position */ rocket.y = " + round(this.y,2) 200 | + "\n/* Vertical velocity */ rocket.dy = " + round(this.dy,2) 201 | + "\n/* Angle from vertical */ rocket.theta = " + round(this.theta,2) 202 | + "\n/* Angular velocity */ rocket.dtheta = " + round(this.dtheta,2) 203 | + "\n/* Simulation time */ rocket.T = " + round(this.T,2); 204 | } 205 | -------------------------------------------------------------------------------- /js/models/SinglePendulum.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | if (typeof Models === 'undefined') var Models = {}; 3 | 4 | Models.SinglePendulum = function(params) 5 | { 6 | var nVars = Object.keys(this.vars).length; 7 | for(var i = 0; i < nVars; i++) 8 | { 9 | var key = Object.keys(this.vars)[i]; 10 | this[key] = (typeof params[key] == 'undefined')?this.vars[key]:params[key]; 11 | } 12 | } 13 | 14 | Models.SinglePendulum.prototype.vars = 15 | { 16 | m0: 10, 17 | m1: .5, 18 | L: 1, 19 | g: 9.81, 20 | theta: 0.2, 21 | dtheta: 0, 22 | x: 0, 23 | dx: 0, 24 | F: 0, 25 | F_cmd: 0, 26 | T: 0 27 | }; 28 | 29 | Models.SinglePendulum.prototype.simulate = function (dt, controlFunc) 30 | { 31 | this.F_cmd = controlFunc({x:this.x,dx:this.dx,theta:this.theta,dtheta:this.dtheta,T:this.T}); 32 | if(typeof this.F_cmd != 'number' || isNaN(this.F_cmd)) throw "Error: The controlFunction must return a number."; 33 | this.F_cmd = Math.max(-30,Math.min(30,this.F_cmd)); 34 | integrationStep(this, ['x', 'dx', 'theta', 'dtheta', 'F'], dt); 35 | } 36 | 37 | Models.SinglePendulum.prototype.ode = function (x) 38 | { 39 | var s = Math.sin(x[2]); 40 | var c = Math.cos(x[2]); 41 | var dthetasq = x[3] * x[3]; 42 | 43 | var M = [[this.m0,0,0,0,-s], 44 | [0,0,this.m1,0,s], 45 | [0,0,0,this.m1,c], 46 | [1,this.L*c,-1,0,0], 47 | [0,-this.L*s,0,-1,0]]; 48 | var b = [x[4],0,-this.m1*this.g,s*dthetasq*this.L,c*dthetasq*this.L]; 49 | var ddx = numeric.solve(M,b) 50 | return [x[1],ddx[0],x[3],ddx[1],40.0*(this.F_cmd - x[4])]; 51 | } 52 | 53 | 54 | Models.SinglePendulum.prototype.draw = function (ctx, canvas) 55 | { 56 | ctx.translate(0,-this.L); 57 | 58 | var cartWidth = 0.4*this.L; 59 | var cartHeight = 0.7*cartWidth; 60 | 61 | var tipX = this.x+this.L*Math.sin(this.theta); 62 | var tipY = this.L*Math.cos(this.theta)+cartHeight; 63 | 64 | // ground 65 | ctx.strokeStyle="#333366"; 66 | drawLine(ctx,-100,-.025,100,-.025,0.05); 67 | 68 | // cart 69 | ctx.fillStyle="#4444FF"; 70 | ctx.fillRect(this.x-cartWidth/2,0,cartWidth,cartHeight); 71 | 72 | // shaft 73 | ctx.strokeStyle="#AAAAFF"; 74 | ctx.lineCap = 'round'; 75 | drawLine(ctx,this.x,cartHeight,tipX,tipY,this.L/20.0); 76 | 77 | // tip-mass 78 | ctx.beginPath(); 79 | ctx.arc(tipX, tipY, this.L/7, 0, 2 * Math.PI, false); 80 | ctx.fillStyle = '#4444FF'; 81 | ctx.fill(); 82 | 83 | // force arrow 84 | ctx.strokeStyle="#FF0000"; 85 | ctx.lineCap = 'round'; 86 | drawArrow(ctx, this.x, 0.5*cartHeight, 0.1*this.F, 0, 0.05, this.L/40.0); 87 | } 88 | 89 | Models.SinglePendulum.prototype.infoText = function () 90 | { 91 | return "/* Horizontal position */ pendulum.x = " + round(this.x,2) 92 | + "\n/* Horizontal velocity */ pendulum.dx = " + round(this.dx,2) 93 | + "\n/* Angle from vertical (rad) */ pendulum.theta = " + round(this.theta,2) 94 | + "\n/* Angular velocity (rad/s) */ pendulum.dtheta = " + round(this.dtheta,2) 95 | + "\n/* Simulation time (s) */ pendulum.T = " + round(this.T,2); 96 | } 97 | -------------------------------------------------------------------------------- /js/models/Vehicle.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | if (typeof Models === 'undefined') var Models = {}; 3 | 4 | Models.Vehicle = function(params) 5 | { 6 | var nVars = Object.keys(this.vars).length; 7 | for(var i = 0; i < nVars; i++) 8 | { 9 | var key = Object.keys(this.vars)[i]; 10 | this[key] = (typeof params[key] == 'undefined')?this.vars[key]:params[key]; 11 | } 12 | this.updateLidarPoints(); 13 | } 14 | 15 | Models.Vehicle.prototype.vars = 16 | { 17 | x: 16, 18 | y: 9, 19 | speed: 20, 20 | heading: 0, 21 | acceleration: 0, 22 | steering: 0, 23 | length: 4.4, 24 | width: 2.2, 25 | Lf: 1.2, 26 | Lr: 1.4, 27 | lidarDirections: [1,.5,0,-.5,-1], 28 | trackImgURL: '', 29 | pixelSize: 0.1, 30 | lateralAccelerationLimit: 10, 31 | accelerationLimit: 7, 32 | drag: 1e-3, 33 | steeringLimit: NaN, 34 | constantSpeed: true, 35 | isObstacle: function(x){return x[3]<100;}, 36 | T: 0, 37 | }; 38 | 39 | Models.Vehicle.prototype.updateLidarPoints = function () 40 | { 41 | this.lidarPoints = []; 42 | for (var i = 0; i < this.lidarDirections.length; i++) { 43 | var lidarPoint = this.lineSearch(this.x,this.y,this.heading + this.lidarDirections[i]); 44 | lidarPoint.direction = this.lidarDirections[i]; 45 | this.lidarPoints.push(lidarPoint); 46 | }; 47 | } 48 | 49 | Models.Vehicle.prototype.simulate = function (dt, controlFunc) 50 | { 51 | var imgData = ImageDataCache.get(this.trackImgURL); 52 | 53 | if(imgData && !this.detectCollision()) 54 | { 55 | var input = controlFunc({x:this.x,y:this.y,speed:this.speed,acceleration:this.acceleration,heading:this.heading,steering:this.steering,steeringLimit:this.steeringLimit,lidarPoints:this.lidarPoints}); 56 | this.steeringLimit = Math.atan(this.lateralAccelerationLimit*(this.Lf+this.Lr)/(1e-6+this.speed*this.speed)); 57 | 58 | if(this.constantSpeed) 59 | { 60 | this.acceleration = 0; 61 | if(typeof input !== 'number') throw "Error: The controlFunction must return a number."; 62 | this.steering = Math.max(-this.steeringLimit,Math.min(this.steeringLimit,input)); 63 | } 64 | else 65 | { 66 | if(typeof input !== 'object' || typeof input.steering !== 'number' || typeof input.acceleration !== 'number') 67 | throw "Error: The controlFunction must return an object: {steering: number, acceleration: number}"; 68 | this.steering = Math.max(-this.steeringLimit,Math.min(this.steeringLimit,input.steering)); 69 | this.acceleration = Math.max(-this.accelerationLimit,Math.min(this.accelerationLimit,input.acceleration)); 70 | } 71 | 72 | integrationStep(this, ['x', 'y', 'heading', 'speed'], dt); 73 | this.updateLidarPoints(); 74 | } 75 | } 76 | 77 | Models.Vehicle.prototype.ode = function (x) { 78 | var s = Math.sin(x[2]); 79 | var c = Math.cos(x[2]); 80 | var v = x[3]; 81 | var Lr = this.Lr; 82 | var Lwb = this.Lf+this.Lr; 83 | var R = Lr/Lwb*Math.tan(this.steering); 84 | var norm = Math.sqrt((-R*s+c)*(-R*s+c)+(R*c+s)*(R*c+s)); 85 | if(this.constantSpeed) 86 | return [v*(-R*s+c)/norm,v*(R*c+s)/norm,Math.tan(this.steering)*v/Lwb,0]; 87 | else 88 | return [v*(-R*s+c)/norm,v*(R*c+s)/norm,Math.tan(this.steering)*v/Lwb,this.acceleration-this.drag*v*Math.abs(v)]; 89 | } 90 | 91 | Models.Vehicle.prototype.detectCollision = function (imgData) { 92 | var imgData = ImageDataCache.get(this.trackImgURL); 93 | 94 | if(imgData) 95 | { 96 | var L = this.length; 97 | var W = this.width; 98 | var s = Math.sin(this.heading); 99 | var c = Math.cos(this.heading); 100 | // points relative to the center that form a convex hull. 101 | var outerPoints = [{x:L/2,y:W/2},{x:-L/2,y:W/2},{x:L/2,y:-W/2},{x:-L/2,y:-W/2}]; 102 | for (var i = 0; i < outerPoints.length; i++) 103 | { 104 | var p = outerPoints[i]; 105 | var hull_x = p.x*c-p.y*s+this.x; 106 | var hull_y = p.x*s+p.y*c+this.y; 107 | if(this.coordIsObstacle(hull_x/this.pixelSize,hull_y/this.pixelSize,imgData)) return true; 108 | } 109 | return false; 110 | } else return false; 111 | } 112 | 113 | Models.Vehicle.prototype.draw = function (ctx, canvas) { 114 | var imgData = ImageDataCache.get(this.trackImgURL); 115 | 116 | if(imgData) 117 | { 118 | ctx.setTransform(1,0,0,1,0,0); 119 | ctx.clearRect(0,0,canvas.width,canvas.height); 120 | 121 | ctx.translate(0,canvas.height); 122 | var scale = Math.min(canvas.width / imgData.width,canvas.height / imgData.height); 123 | ctx.scale(scale,-scale); 124 | 125 | ctx.drawImage(imgData.image,0,0); 126 | 127 | ctx.scale(1/this.pixelSize,1/this.pixelSize); 128 | 129 | for (var i = 0; i < this.lidarPoints.length; i++) { 130 | ctx.strokeStyle = '#77ff00'; 131 | drawLine(ctx,this.x,this.y,this.lidarPoints[i].x,this.lidarPoints[i].y,0.1*this.width); 132 | }; 133 | 134 | 135 | ctx.translate(this.x,this.y); 136 | ctx.rotate(this.heading); 137 | 138 | ctx.fillStyle="#ffaa00"; 139 | ctx.fillRect(-this.length/2,-this.width/2,this.length,this.width); 140 | } 141 | } 142 | 143 | 144 | Models.Vehicle.prototype.coordIsObstacle = function(x,y,imgData) { 145 | x = x|0; // convert to int 146 | y = y|0; 147 | if(x<0 || y<0 || imgData.width<=x || imgData.height<= y) return true; 148 | return this.isObstacle(ImageDataCache.at(this.trackImgURL, x, y)); 149 | }; 150 | 151 | Models.Vehicle.prototype.lineSearch = function(x,y,direction) { 152 | var imgData = ImageDataCache.get(this.trackImgURL); 153 | 154 | if(imgData) 155 | { 156 | x /= this.pixelSize; 157 | y /= this.pixelSize; 158 | var c = Math.cos(direction); 159 | var s = Math.sin(direction); 160 | var step = 5; 161 | var distance = 0; 162 | 163 | var currentPointIsObstacle = this.coordIsObstacle(x,y,imgData); 164 | if(currentPointIsObstacle) return {x:x*this.pixelSize,y:y*this.pixelSize,distance:distance*this.pixelSize}; 165 | 166 | for (var i = 0; i < 10000; i++) { // This loop should always terminate early with return. Iteration limit for safety. 167 | var x2 = x + c*distance; 168 | var y2 = y + s*distance; 169 | 170 | var nextPointIsObstacle = this.coordIsObstacle(x2,y2,imgData); 171 | 172 | if(currentPointIsObstacle != nextPointIsObstacle) step *= -.5; 173 | 174 | if(Math.abs(step) < 1) return {x:x2*this.pixelSize,y:y2*this.pixelSize,distance:distance*this.pixelSize}; 175 | if(distance < 0) return {x:x*this.pixelSize,y:y*this.pixelSize,distance:distance*this.pixelSize}; 176 | 177 | currentPointIsObstacle = nextPointIsObstacle; 178 | distance += step; 179 | } 180 | alert('Oops, this should never happen. Ask a programmer to fix it. lineSearch() did not converge.'); 181 | } 182 | else return {x:x,y:y,distance:0}; 183 | }; 184 | 185 | Models.Vehicle.prototype.infoText = function () 186 | { 187 | var str = "/* Position */ vehicle.x = " + round(this.x,2) 188 | + "\n vehicle.y = " + round(this.y,2) 189 | + "\n/* Speed */ vehicle.speed = " + round(this.speed,2) 190 | + "\n/* Acceleration */ vehicle.acceleration = " + round(this.acceleration,2) 191 | + "\n/* Heading */ vehicle.heading = " + round(this.heading,2) 192 | + "\n/* Steering */ vehicle.steering = " + round(this.steering,2) 193 | + "\n/* Steering Limit */ vehicle.steeringLimit = " + round(this.steeringLimit,2) 194 | + "\n/* LIDAR sensors */"; 195 | 196 | for (var i = 0; i < this.lidarPoints.length; i++) { 197 | str += "\nvehicle.lidarPoints["+i+"] = {"; 198 | //str += "x: " + padSpaces(round(this.lidarPoints[i].x,2),8) + ", "; 199 | //str += "y: " + padSpaces(round(this.lidarPoints[i].y,2),8) + ", "; 200 | str += "distance: " + padSpaces(round(this.lidarPoints[i].distance,2),8) + ", "; 201 | str += "direction: " + padSpaces(round(this.lidarPoints[i].direction,2),8); 202 | str += "}"; 203 | }; 204 | 205 | str += "\n/* Simulation time */ vehicle.T = " + round(this.T,2); 206 | 207 | return str; 208 | } -------------------------------------------------------------------------------- /js/plugins.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // Avoid `console` errors in browsers that lack a console. 3 | (function() { 4 | var method; 5 | var noop = function () {}; 6 | var methods = [ 7 | 'assert', 'clear', 'count', 'debug', 'dir', 'dirxml', 'error', 8 | 'exception', 'group', 'groupCollapsed', 'groupEnd', 'info', 'log', 9 | 'markTimeline', 'profile', 'profileEnd', 'table', 'time', 'timeEnd', 10 | 'timeline', 'timelineEnd', 'timeStamp', 'trace', 'warn' 11 | ]; 12 | var length = methods.length; 13 | var console = (window.console = window.console || {}); 14 | 15 | while (length--) { 16 | method = methods[length]; 17 | 18 | // Only stub undefined methods. 19 | if (!console[method]) { 20 | console[method] = noop; 21 | } 22 | } 23 | }()); -------------------------------------------------------------------------------- /js/shortcut.js: -------------------------------------------------------------------------------- 1 | /** 2 | * http://www.openjs.com/scripts/events/keyboard_shortcuts/ 3 | * Version : 2.01.B 4 | * By Binny V A 5 | * License : BSD 6 | */ 7 | shortcut = { 8 | 'all_shortcuts':{},//All the shortcuts are stored in this array 9 | 'add': function(shortcut_combination,callback,opt) { 10 | //Provide a set of default options 11 | var default_options = { 12 | 'type':'keydown', 13 | 'propagate':false, 14 | 'disable_in_input':false, 15 | 'target':document, 16 | 'keycode':false 17 | } 18 | if(!opt) opt = default_options; 19 | else { 20 | for(var dfo in default_options) { 21 | if(typeof opt[dfo] == 'undefined') opt[dfo] = default_options[dfo]; 22 | } 23 | } 24 | 25 | var ele = opt.target; 26 | if(typeof opt.target == 'string') ele = document.getElementById(opt.target); 27 | var ths = this; 28 | shortcut_combination = shortcut_combination.toLowerCase(); 29 | 30 | //The function to be called at keypress 31 | var func = function(e) { 32 | e = e || window.event; 33 | 34 | if(opt['disable_in_input']) { //Don't enable shortcut keys in Input, Textarea fields 35 | var element; 36 | if(e.target) element=e.target; 37 | else if(e.srcElement) element=e.srcElement; 38 | if(element.nodeType==3) element=element.parentNode; 39 | 40 | if(element.tagName == 'INPUT' || element.tagName == 'TEXTAREA') return; 41 | } 42 | 43 | //Find Which key is pressed 44 | if (e.keyCode) code = e.keyCode; 45 | else if (e.which) code = e.which; 46 | var character = String.fromCharCode(code).toLowerCase(); 47 | 48 | if(code == 188) character=","; //If the user presses , when the type is onkeydown 49 | if(code == 190) character="."; //If the user presses , when the type is onkeydown 50 | 51 | var keys = shortcut_combination.split("+"); 52 | //Key Pressed - counts the number of valid keypresses - if it is same as the number of keys, the shortcut function is invoked 53 | var kp = 0; 54 | 55 | //Work around for stupid Shift key bug created by using lowercase - as a result the shift+num combination was broken 56 | var shift_nums = { 57 | "`":"~", 58 | "1":"!", 59 | "2":"@", 60 | "3":"#", 61 | "4":"$", 62 | "5":"%", 63 | "6":"^", 64 | "7":"&", 65 | "8":"*", 66 | "9":"(", 67 | "0":")", 68 | "-":"_", 69 | "=":"+", 70 | ";":":", 71 | "'":"\"", 72 | ",":"<", 73 | ".":">", 74 | "/":"?", 75 | "\\":"|" 76 | } 77 | //Special Keys - and their codes 78 | var special_keys = { 79 | 'esc':27, 80 | 'escape':27, 81 | 'tab':9, 82 | 'space':32, 83 | 'return':13, 84 | 'enter':13, 85 | 'backspace':8, 86 | 87 | 'scrolllock':145, 88 | 'scroll_lock':145, 89 | 'scroll':145, 90 | 'capslock':20, 91 | 'caps_lock':20, 92 | 'caps':20, 93 | 'numlock':144, 94 | 'num_lock':144, 95 | 'num':144, 96 | 97 | 'pause':19, 98 | 'break':19, 99 | 100 | 'insert':45, 101 | 'home':36, 102 | 'delete':46, 103 | 'end':35, 104 | 105 | 'pageup':33, 106 | 'page_up':33, 107 | 'pu':33, 108 | 109 | 'pagedown':34, 110 | 'page_down':34, 111 | 'pd':34, 112 | 113 | 'left':37, 114 | 'up':38, 115 | 'right':39, 116 | 'down':40, 117 | 118 | 'f1':112, 119 | 'f2':113, 120 | 'f3':114, 121 | 'f4':115, 122 | 'f5':116, 123 | 'f6':117, 124 | 'f7':118, 125 | 'f8':119, 126 | 'f9':120, 127 | 'f10':121, 128 | 'f11':122, 129 | 'f12':123 130 | } 131 | 132 | var modifiers = { 133 | shift: { wanted:false, pressed:false}, 134 | ctrl : { wanted:false, pressed:false}, 135 | alt : { wanted:false, pressed:false}, 136 | meta : { wanted:false, pressed:false} //Meta is Mac specific 137 | }; 138 | 139 | if(e.ctrlKey) modifiers.ctrl.pressed = true; 140 | if(e.shiftKey) modifiers.shift.pressed = true; 141 | if(e.altKey) modifiers.alt.pressed = true; 142 | if(e.metaKey) modifiers.meta.pressed = true; 143 | 144 | for(var i=0; k=keys[i],i