initial rewrite
|
@ -20,12 +20,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||||
<title>Lemmings</title>
|
||||
<script language="JavaScript" src="pathseg.js"></script>
|
||||
<script language="JavaScript" src="mouse.js"></script>
|
||||
<script language="JavaScript" src="graphics.js"></script>
|
||||
<script language="JavaScript" src="utils.js"></script>
|
||||
<script language="JavaScript" src="sensors.js"></script>
|
||||
<script language="JavaScript" src="lemming_sim_student.js"></script>
|
||||
<script language="JavaScript" src="matter.js"></script>
|
||||
<script language="JavaScript" src="jquery-1.11.0.min.js"></script>
|
||||
<script language="JavaScript" src="pathseg.js"></script>
|
||||
<script language="JavaScript" src="decomp.js"></script>
|
||||
</head>
|
||||
<body onload="init();">
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Licensed as CC0 Public Domain Dedication, see https://creativecommons.org/publicdomain/zero/1.0/. -->
|
||||
|
||||
<svg
|
||||
xmlns:ns="http://creativecommons.org/"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
width="306.19147"
|
||||
height="186.78011"
|
||||
viewBox="0 0 306.19147 186.78011"
|
||||
xml:space="preserve"
|
||||
id="svg11"
|
||||
sodipodi:docname="robotbody_longerleft.svg"
|
||||
inkscape:version="0.92.2 (unknown)"><metadata
|
||||
id="metadata17"><rdf:RDF><ns:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></ns:Work><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs15" /><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1016"
|
||||
id="namedview13"
|
||||
showgrid="false"
|
||||
inkscape:zoom="1.5742187"
|
||||
inkscape:cx="193.78259"
|
||||
inkscape:cy="72.49728"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg11"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true"
|
||||
inkscape:snap-center="true"
|
||||
inkscape:snap-page="true"
|
||||
inkscape:snap-global="false"><sodipodi:guide
|
||||
position="87.480965,95.186771"
|
||||
orientation="1,0"
|
||||
id="guide863"
|
||||
inkscape:locked="false" /><sodipodi:guide
|
||||
position="87.480965,95.186771"
|
||||
orientation="0,1"
|
||||
id="guide867"
|
||||
inkscape:locked="false" /></sodipodi:namedview><path
|
||||
id="robotbody"
|
||||
d="m 21.840805,32.282261 c -29.4070742,38.075324 -29.2031002,89.304329 1.126302,124.879399 28.308167,33.20432 96.216923,41.60047 129.034143,9.08347 20.97597,3.07228 22.33436,4.02195 28.5988,-5.90513 5.06112,-8.02021 -4.86652,-25.70583 -4.67439,-28.06416 2.47109,-30.33143 1.21949,-67.484605 -2.03551,-83.758545 -0.8545,-4.27227 140.86445,3.74803 131.89259,-6.66691 C 295.93739,30.421398 158.23428,27.099346 155.90436,24.175764 133.57052,-3.8490539 58.313014,-14.940787 21.840805,32.282261 Z"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="sscssssss"
|
||||
style="stroke-width:0.99999988" /></svg>
|
After Width: | Height: | Size: 2.8 KiB |
|
@ -0,0 +1,65 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Licensed as CC0 Public Domain Dedication, see https://creativecommons.org/publicdomain/zero/1.0/. -->
|
||||
|
||||
<svg
|
||||
xmlns:ns="http://creativecommons.org/"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
width="316.32892"
|
||||
height="186.78011"
|
||||
viewBox="0 0 316.32892 186.78011"
|
||||
xml:space="preserve"
|
||||
id="svg11"
|
||||
sodipodi:docname="robotbody_longerleft_mediumright.svg"
|
||||
inkscape:version="0.92.2 (unknown)"><metadata
|
||||
id="metadata17"><rdf:RDF><ns:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></ns:Work><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs15" /><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1016"
|
||||
id="namedview13"
|
||||
showgrid="false"
|
||||
inkscape:zoom="1.5742187"
|
||||
inkscape:cx="193.78259"
|
||||
inkscape:cy="72.49728"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg11"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true"
|
||||
inkscape:snap-center="true"
|
||||
inkscape:snap-page="true"
|
||||
inkscape:snap-global="false"><sodipodi:guide
|
||||
position="87.480965,95.186771"
|
||||
orientation="1,0"
|
||||
id="guide863"
|
||||
inkscape:locked="false" /><sodipodi:guide
|
||||
position="87.480965,95.186771"
|
||||
orientation="0,1"
|
||||
id="guide867"
|
||||
inkscape:locked="false" /></sodipodi:namedview><path
|
||||
id="robotbody"
|
||||
d="m 21.840805,32.282261 c -29.4070742,38.075324 -29.2031002,89.304329 1.126302,124.879399 28.308167,33.20432 96.216923,41.60047 129.034143,9.08347 20.97597,3.07228 40.7562,3.38671 47.02064,-6.54037 5.06112,-8.02021 -23.28836,-25.07059 -23.09623,-27.42892 2.47109,-30.33143 1.21949,-67.484605 -2.03551,-83.758545 -0.8545,-4.27227 151.02822,1.84232 142.05636,-8.572618 C 306.10116,28.515691 158.23428,27.099346 155.90436,24.175764 133.57052,-3.8490539 58.313014,-14.940787 21.840805,32.282261 Z"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="sscssssss"
|
||||
style="stroke-width:0.99999988" /></svg>
|
After Width: | Height: | Size: 2.8 KiB |
|
@ -0,0 +1,65 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Licensed as CC0 Public Domain Dedication, see https://creativecommons.org/publicdomain/zero/1.0/. -->
|
||||
|
||||
<svg
|
||||
xmlns:ns="http://creativecommons.org/"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
width="239.8282"
|
||||
height="186.78011"
|
||||
viewBox="0 0 239.8282 186.78011"
|
||||
xml:space="preserve"
|
||||
id="svg11"
|
||||
sodipodi:docname="robotbody_longleft.svg"
|
||||
inkscape:version="0.92.2 (unknown)"><metadata
|
||||
id="metadata17"><rdf:RDF><ns:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></ns:Work><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs15" /><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1016"
|
||||
id="namedview13"
|
||||
showgrid="false"
|
||||
inkscape:zoom="1.5742187"
|
||||
inkscape:cx="193.78259"
|
||||
inkscape:cy="72.49728"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg11"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true"
|
||||
inkscape:snap-center="true"
|
||||
inkscape:snap-page="true"
|
||||
inkscape:snap-global="false"><sodipodi:guide
|
||||
position="87.480965,95.186771"
|
||||
orientation="1,0"
|
||||
id="guide863"
|
||||
inkscape:locked="false" /><sodipodi:guide
|
||||
position="87.480965,95.186771"
|
||||
orientation="0,1"
|
||||
id="guide867"
|
||||
inkscape:locked="false" /></sodipodi:namedview><path
|
||||
id="robotbody"
|
||||
d="m 21.840805,32.282261 c -29.4070742,38.075324 -29.2031002,89.304329 1.126302,124.879399 28.308167,33.20432 96.216923,41.60047 129.034143,9.08347 20.97597,3.07228 22.33436,4.02195 28.5988,-5.90513 5.06112,-8.02021 -4.86652,-25.70583 -4.67439,-28.06416 2.47109,-30.33143 1.21949,-67.484605 -2.03551,-83.758545 -0.8545,-4.27227 74.1647,3.74803 65.19284,-6.66691 C 229.23764,30.421398 158.23428,27.099346 155.90436,24.175764 133.57052,-3.8490539 58.313014,-14.940787 21.840805,32.282261 Z"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="sscssssss"
|
||||
style="stroke-width:0.99999988" /></svg>
|
After Width: | Height: | Size: 2.8 KiB |
|
@ -0,0 +1,65 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Licensed as CC0 Public Domain Dedication, see https://creativecommons.org/publicdomain/zero/1.0/. -->
|
||||
|
||||
<svg
|
||||
xmlns:ns="http://creativecommons.org/"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
width="239.19885"
|
||||
height="186.78011"
|
||||
viewBox="0 0 239.19885 186.78011"
|
||||
xml:space="preserve"
|
||||
id="svg11"
|
||||
sodipodi:docname="robotbody_longleft_mediumright.svg"
|
||||
inkscape:version="0.92.2 (unknown)"><metadata
|
||||
id="metadata17"><rdf:RDF><ns:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></ns:Work><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs15" /><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1016"
|
||||
id="namedview13"
|
||||
showgrid="false"
|
||||
inkscape:zoom="1.5742187"
|
||||
inkscape:cx="193.78259"
|
||||
inkscape:cy="72.49728"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg11"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true"
|
||||
inkscape:snap-center="true"
|
||||
inkscape:snap-page="true"
|
||||
inkscape:snap-global="false"><sodipodi:guide
|
||||
position="87.480964,95.18677"
|
||||
orientation="1,0"
|
||||
id="guide863"
|
||||
inkscape:locked="false" /><sodipodi:guide
|
||||
position="87.480964,95.18677"
|
||||
orientation="0,1"
|
||||
id="guide867"
|
||||
inkscape:locked="false" /></sodipodi:namedview><path
|
||||
id="robotbody"
|
||||
d="m 21.840805,32.282261 c -29.4070742,38.075324 -29.2031002,89.304329 1.126302,124.879399 28.308167,33.20432 96.216923,41.60047 129.034143,9.08347 20.97597,3.07228 40.7562,3.38671 47.02064,-6.54037 5.06112,-8.02021 -23.28836,-25.07059 -23.09623,-27.42892 2.47109,-30.33143 1.21949,-67.484605 -2.03551,-83.758545 -0.8545,-4.27227 73.52946,-1.96909 64.5576,-12.384033 C 228.6024,24.704276 158.23428,27.099346 155.90436,24.175764 133.57052,-3.8490539 58.313014,-14.940787 21.840805,32.282261 Z"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="sscssssss"
|
||||
style="stroke-width:0.99999988" /></svg>
|
After Width: | Height: | Size: 2.8 KiB |
|
@ -0,0 +1,147 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Licensed as CC0 Public Domain Dedication, see https://creativecommons.org/publicdomain/zero/1.0/. -->
|
||||
|
||||
<svg
|
||||
xmlns:ns="http://creativecommons.org/"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
width="186.78011"
|
||||
height="239.8282"
|
||||
viewBox="0 0 186.7801 239.82821"
|
||||
xml:space="preserve"
|
||||
id="svg11"
|
||||
sodipodi:docname="robotbody_longleft_rabbit.svg"
|
||||
inkscape:version="0.92.2 (unknown)"><metadata
|
||||
id="metadata17"><rdf:RDF><ns:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></ns:Work><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs15"><inkscape:path-effect
|
||||
effect="powerstroke"
|
||||
id="path-effect254"
|
||||
is_visible="true"
|
||||
offset_points="0,0.5"
|
||||
sort_points="true"
|
||||
interpolator_type="CubicBezierJohan"
|
||||
interpolator_beta="0.2"
|
||||
start_linecap_type="zerowidth"
|
||||
linejoin_type="extrp_arc"
|
||||
miter_limit="4"
|
||||
end_linecap_type="zerowidth" /><inkscape:path-effect
|
||||
effect="powerstroke"
|
||||
id="path-effect25"
|
||||
is_visible="true"
|
||||
offset_points="3,0.5"
|
||||
sort_points="true"
|
||||
interpolator_type="CubicBezierJohan"
|
||||
interpolator_beta="0.2"
|
||||
start_linecap_type="zerowidth"
|
||||
linejoin_type="extrp_arc"
|
||||
miter_limit="4"
|
||||
end_linecap_type="zerowidth" /><inkscape:path-effect
|
||||
effect="powerstroke"
|
||||
id="path-effect254-3"
|
||||
is_visible="true"
|
||||
offset_points="0,0.5"
|
||||
sort_points="true"
|
||||
interpolator_type="CubicBezierJohan"
|
||||
interpolator_beta="0.2"
|
||||
start_linecap_type="zerowidth"
|
||||
linejoin_type="extrp_arc"
|
||||
miter_limit="4"
|
||||
end_linecap_type="zerowidth" /></defs><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1016"
|
||||
id="namedview13"
|
||||
showgrid="true"
|
||||
inkscape:zoom="1.1131407"
|
||||
inkscape:cx="-110.31088"
|
||||
inkscape:cy="158.83002"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg11"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true"
|
||||
inkscape:snap-center="true"
|
||||
inkscape:snap-page="true"
|
||||
inkscape:snap-global="false"><sodipodi:guide
|
||||
position="89.835902,94.776877"
|
||||
orientation="0,1"
|
||||
id="guide867"
|
||||
inkscape:locked="false" /><inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid17" /></sodipodi:namedview><path
|
||||
id="robotbody"
|
||||
d="m 32.361665,218.1462 c 38.075324,29.40707 89.304325,29.2031 124.879395,-1.12631 33.20432,-28.30816 41.60047,-96.21692 9.08347,-129.034138 3.07228,-20.97597 4.02195,-22.33436 -5.90513,-28.5988 -8.02021,-5.06112 -25.70583,4.86652 -28.06416,4.67439 -30.33143,-2.47109 -67.484601,-1.21949 -83.758541,2.03551 -4.27227,0.8545 3.74803,-74.1646999 -6.66691,-65.19283985 C 30.500802,10.749362 27.17875,81.752722 24.255168,84.082642 -3.7696494,106.41648 -14.861383,181.67399 32.361665,218.1462 Z"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="sscssssss"
|
||||
style="stroke-width:0.99999988" /><path
|
||||
style="fill:#000000;fill-rule:nonzero;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 95.124644,167.33654 c 6.249246,0 16.791976,0 28.678396,0 -3.42704,2.30174 -8.60423,5.77896 -13.40834,9.0056 -6.21057,-4.10993 -11.750517,-7.77606 -14.994123,-9.92257 C 86.237314,160.35566 58.747523,142.1639 49.584259,136.09998 Z M 49.032393,136.93392 c 9.163263,6.06391 36.653054,24.25567 45.816318,30.31959 3.291923,2.17847 8.949109,5.92219 15.272109,10.10653 l 0.27798,0.18396 0.27673,-0.18586 c 4.88158,-3.27867 10.16593,-6.82786 15.04751,-10.10653 l 1.36244,-0.91507 h -1.64122 c -12.553,0 -23.78418,0 -30.319616,0 z"
|
||||
id="path23"
|
||||
inkscape:connector-curvature="0"
|
||||
inkscape:path-effect="#path-effect25"
|
||||
inkscape:original-d="m 95.124644,166.83654 h 30.319616 l -15.04751,10.10653 z" /><circle
|
||||
style="fill:#ffffff"
|
||||
id="path31"
|
||||
cx="114.10249"
|
||||
cy="114.61942"
|
||||
r="4.8286796" /><circle
|
||||
style="fill:#ffffff"
|
||||
id="path31-3"
|
||||
cx="64.355865"
|
||||
cy="114.95629"
|
||||
r="4.8286796" /><path
|
||||
style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 88.836131,156.72999 V 169.7562 Z"
|
||||
id="path48"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 43.91818,156.28081 v 17.29341 H 61.88536 V 154.93327 H 43.469 v 1.34754 z"
|
||||
id="path60"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="fill:#ffffff;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 73.82141,145.13271 30,-0.22312 -15.162041,12.27813 z"
|
||||
id="path27"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccc" /><rect
|
||||
style="fill:#ffffff;stroke-width:1.553774"
|
||||
id="rect81"
|
||||
width="1.0844152"
|
||||
height="14.149155"
|
||||
x="87.989937"
|
||||
y="156.28081" /><path
|
||||
style="fill:#ffffff;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 88.836131,170.42997 9.432769,8.53441 15.49669,-9.20818 -15.49669,8.98359 z"
|
||||
id="path91"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="fill:#ffffff;fill-rule:nonzero;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 88.56847,170.11108 c 0,0 -0.460602,-0.19454 -0.460602,-0.19454 -1.097812,2.59925 -3.173212,5.69885 -7.130345,7.65121 -2.915718,1.43854 -6.462603,2.02817 -10.134523,1.51988 -3.670134,-0.50805 -7.018082,-2.05238 -9.574048,-4.30369 -3.479573,-3.06484 -4.833237,-6.79321 -5.256314,-9.75475 0.421762,2.95233 1.689623,6.71395 5.123891,9.90118 2.525356,2.3437 5.881966,4.02058 9.630593,4.65147 3.746208,0.63048 7.448089,0.14174 10.555612,-1.28956 4.23834,-1.95215 6.535016,-5.21337 7.706338,-7.98666 0,0 -0.460602,-0.19454 -0.460602,-0.19454 z"
|
||||
id="path252"
|
||||
inkscape:connector-curvature="0"
|
||||
inkscape:path-effect="#path-effect254"
|
||||
inkscape:original-d="m 88.56847,170.11108 c -6.309095,14.93779 -30.208051,11.35258 -32.555832,-5.08189" /><path
|
||||
style="fill:#ffffff;fill-rule:nonzero;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 88.639962,170.16354 c 0,0 -0.460602,0.19454 -0.460602,0.19454 1.171322,2.77329 3.467997,6.03451 7.706337,7.98666 3.107522,1.4313 6.809403,1.92004 10.555603,1.28956 3.74863,-0.63089 7.10524,-2.30777 9.6306,-4.65147 3.43427,-3.18723 4.70213,-6.94885 5.12389,-9.90118 -0.42308,2.96154 -1.77674,6.68991 -5.25631,9.75475 -2.55597,2.25131 -5.90392,3.79564 -9.57405,4.30369 -3.67192,0.50829 -7.218805,-0.0813 -10.134523,-1.51988 -3.957132,-1.95236 -6.032531,-5.05196 -7.130343,-7.65121 0,0 -0.460602,0.19454 -0.460602,0.19454 z"
|
||||
id="path252-5"
|
||||
inkscape:connector-curvature="0"
|
||||
inkscape:path-effect="#path-effect254-3"
|
||||
inkscape:original-d="m 88.639962,170.16354 c 6.309093,14.93779 30.208048,11.35258 32.555828,-5.08189" /></svg>
|
After Width: | Height: | Size: 7.7 KiB |
|
@ -1,7 +1,7 @@
|
|||
/* Lemmings - robot and GUI script.
|
||||
*
|
||||
* Copyright 2016 Harmen de Weerd
|
||||
* Copyright 2017 Johannes Keyser, James Cooke, George Kachergis
|
||||
* Copyright 2017 Johannes Keyser, James Cooke, George Kachergis, Yorick van Pelt
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -16,104 +16,162 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// Description of robot(s), and attached sensor(s) used by InstantiateRobot()
|
||||
RobotInfo = [
|
||||
{body: null, // for MatterJS body, added by InstantiateRobot()
|
||||
color: [255, 255, 255], // color of the robot shape
|
||||
init: {x: 50, y: 50, angle: 0}, // initial position and orientation
|
||||
sensors: [ // define an array of sensors on the robot
|
||||
// define one sensor
|
||||
{sense: senseDistance, // function handle, determines type of sensor
|
||||
minVal: 0, // minimum detectable distance, in pixels
|
||||
maxVal: 50, // maximum detectable distance, in pixels
|
||||
attachAngle: Math.PI/4, // where the sensor is mounted on robot body
|
||||
lookAngle: 0, // direction the sensor is looking (relative to center-out)
|
||||
id: 'distR', // a unique, arbitrary ID of the sensor, for printing/debugging
|
||||
color: [150, 0, 0], // sensor color [in RGB], to distinguish them
|
||||
parent: null, // robot object the sensor is attached to, added by InstantiateRobot
|
||||
value: null // sensor value, i.e. distance in pixels; updated by sense() function
|
||||
},
|
||||
// define another sensor
|
||||
{sense: senseDistance, minVal: 0, maxVal: 50, attachAngle: -Math.PI/4,
|
||||
lookAngle: 0, id: 'distL', color: [0, 150, 0], parent: null, value: null
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
"use strict"
|
||||
// Simulation settings; please change anything that you think makes sense.
|
||||
simInfo = {
|
||||
var simInfo = {
|
||||
maxSteps: 50000, // maximal number of simulation steps to run
|
||||
airDrag: 0.1, // "air" friction of enviroment; 0 is vacuum, 0.9 is molasses
|
||||
boxFric: 0.005, // friction between boxes during collisions
|
||||
boxMass: 0.01, // mass of boxes
|
||||
boxSize: 20, // size of the boxes, in pixels
|
||||
boxSize: 15, // size of the boxes, in pixels
|
||||
robotSize: 13, // approximate robot radius, in pixels (note the SVG gets scaled down)
|
||||
robotMass: 0.4, // robot mass (a.u)
|
||||
gravity: 0, // constant acceleration in Y-direction
|
||||
bayRobot: null, // currently selected robot
|
||||
baySensor: null, // currently selected sensor
|
||||
bayScale: 3, // scale within 2nd, inset canvas showing robot in it's "bay"
|
||||
doContinue: true, // whether to continue simulation, set in HTML
|
||||
debugSensors: false, // plot sensor rays and mark detected objects
|
||||
debugMouse: false, // allow dragging any object with the mouse
|
||||
engine: null, // MatterJS 2D physics engine
|
||||
world: null, // world object (composite of all objects in MatterJS engine)
|
||||
runner: null, // object for running MatterJS engine
|
||||
height: null, // set in HTML file; height of arena (world canvas), in pixels
|
||||
width: null, // set in HTML file; width of arena (world canvas), in pixels
|
||||
curSteps: 0 // increased by simStep()
|
||||
bayScale: 2, // scale within 2nd, inset canvas showing robot in it's "bay"
|
||||
debugSensors: true, // plot sensor rays and mark detected objects
|
||||
debugMouse: true, // allow dragging any object with the mouse
|
||||
};
|
||||
class Lemming extends Robot {
|
||||
constructor(props) {
|
||||
super(Object.assign({
|
||||
sensors: [
|
||||
new DistanceSensor('distR', {
|
||||
attachAngle: Math.PI/(2.5), // where the sensor is mounted on robot body
|
||||
color: [150, 0, 0], // sensor color [in RGB], to distinguish them
|
||||
lookAngle: (Math.PI/7 - Math.PI/(2.5)),
|
||||
attachRadius: 20
|
||||
}),
|
||||
// define another sensor
|
||||
new DistanceSensor('distL', {
|
||||
attachAngle: -Math.PI/7,
|
||||
color: [0, 150, 0],
|
||||
attachRadius: 20
|
||||
}),
|
||||
new Gyroscope('gyro', {
|
||||
attachAngle: 0,
|
||||
attachRadius: 5,
|
||||
color: [100,100,0]
|
||||
}),
|
||||
new ColorSensor('carry', {
|
||||
attachAngle: 0,
|
||||
color: [255, 100, 0],
|
||||
attachRadius: 5
|
||||
}),
|
||||
new DistanceSensor('wallR', {
|
||||
attachAngle: Math.PI/2.5, // where the sensor is mounted on robot body
|
||||
color: [150, 0, 0], // sensor color [in RGB], to distinguish them
|
||||
filter: x => x.role == 'wall',
|
||||
lookAngle: (Math.PI/7 - Math.PI/2.5),
|
||||
attachRadius: 20,
|
||||
|
||||
robots = new Array();
|
||||
sensors = new Array();
|
||||
|
||||
function init() { // called once when loading HTML file
|
||||
const robotBay = document.getElementById("bayLemming"),
|
||||
arena = document.getElementById("arenaLemming"),
|
||||
height = arena.height,
|
||||
width = arena.width;
|
||||
simInfo.height = height;
|
||||
simInfo.width = width;
|
||||
}),
|
||||
// define another sensor
|
||||
new DistanceSensor('wallL', {
|
||||
attachAngle: -Math.PI/7,
|
||||
color: [0, 150, 0],
|
||||
filter: x => x.role == 'wall',
|
||||
attachRadius: 20
|
||||
}),
|
||||
].concat(props.sensors || [])
|
||||
}, props))
|
||||
}
|
||||
turnDeg(rads, cb) {
|
||||
if (Math.abs(rads) >= 320)
|
||||
return this.turnDeg(320, x => this.turnDeg(rads -320, cb))
|
||||
const torque = Math.sign(rads) * 0.01
|
||||
const start = this.getSensorValById('gyro')
|
||||
this.move = function() {
|
||||
const curAngle = this.getSensorValById('gyro')
|
||||
const turned = ((curAngle - start) * Math.sign(rads) + 360) % 360
|
||||
if (turned < 340 && (turned - Math.abs(rads) > 0)) {
|
||||
delete this.move
|
||||
if (cb) cb()
|
||||
} else {
|
||||
this.rotate(torque)
|
||||
}
|
||||
}
|
||||
}
|
||||
move() {
|
||||
//if (sim.curSteps % 250 == 0) this.turnDeg(-90)
|
||||
//return
|
||||
// TODO: Define Lemming program here.
|
||||
const vals = ['distL', 'distR', 'carry', 'wallL', 'wallR']
|
||||
.reduce(((p,c) => ((p[c] = this.getSensorValById(c)), p)), {})
|
||||
const [r,g,b] = vals.carry
|
||||
let block = 0
|
||||
if (r > (g+b)) {
|
||||
block = 'red'
|
||||
} else if (b > r+g) {
|
||||
block = 'blue'
|
||||
}
|
||||
const {distL, distR, wallL, wallR} = vals
|
||||
if (distL < wallL - 5 || distR < wallR - 5) {
|
||||
// if it senses a block
|
||||
if (!block) return this.drive(2e-4) // no block: drive towards block?
|
||||
if (block == 'blue') void 0; // if blue: ignore
|
||||
if (block == 'red') return this.turnDeg(-90) // if red: leave block
|
||||
} else if (wallL < Infinity || wallR < Infinity) {
|
||||
// no block: turn left or right
|
||||
if (!block) return this.turnDeg(90 * (Math.random() < 0.5 ? -1 : 1))
|
||||
if (block == 'blue') return this.turnDeg(-90) // blue: leave
|
||||
if (block == 'red') return this.turnDeg(90) // red: keep
|
||||
}
|
||||
// by default: wander
|
||||
this.rotate(+0.002);
|
||||
this.drive(0.0002);
|
||||
}
|
||||
}
|
||||
var sim = null
|
||||
class Simulation {
|
||||
constructor() {
|
||||
this.bay = null
|
||||
this.robots = null
|
||||
this.runner = null
|
||||
this.world = null
|
||||
this.engine = null
|
||||
this.curSteps = 0
|
||||
this.doContinue = false
|
||||
this.robots = []
|
||||
}
|
||||
init() {
|
||||
const arena = document.getElementById("arenaLemming"),
|
||||
{height, width} = arena
|
||||
this.elem = arena
|
||||
arena.style.backgroundColor = 'silver'
|
||||
Object.assign(this, {height, width})
|
||||
|
||||
/* Create a MatterJS engine and world. */
|
||||
simInfo.engine = Matter.Engine.create();
|
||||
simInfo.world = simInfo.engine.world;
|
||||
simInfo.world.gravity.y = simInfo.gravity;
|
||||
simInfo.engine.timing.timeScale = 1;
|
||||
this.engine = Matter.Engine.create();
|
||||
this.world = this.engine.world;
|
||||
this.world.gravity.y = simInfo.gravity;
|
||||
this.engine.timing.timeScale = 1;
|
||||
|
||||
/* Create walls and boxes, and add them to the world. */
|
||||
// note that "roles" are custom properties for rendering (not from MatterJS)
|
||||
function getWall(x, y, width, height) {
|
||||
return Matter.Bodies.rectangle(x, y, width, height,
|
||||
{isStatic: true, role: 'wall',
|
||||
color:[150, 150, 150]});
|
||||
return Matter.Bodies.rectangle(x, y, width, height, {
|
||||
isStatic: true, role: 'wall',
|
||||
color:[150, 150, 150]
|
||||
});
|
||||
};
|
||||
const wall_lo = getWall(width/2, height-5, width-5, 5),
|
||||
wall_hi = getWall(width/2, 5, width-5, 5),
|
||||
wall_le = getWall(5, height/2, 5, height-15),
|
||||
wall_ri = getWall(width-5, height/2, 5, height-15);
|
||||
Matter.World.add(simInfo.world, [wall_lo, wall_hi, wall_le, wall_ri]);
|
||||
Matter.World.add(this.world, [wall_lo, wall_hi, wall_le, wall_ri]);
|
||||
|
||||
/* Add a bunch of boxes in a neat grid. */
|
||||
function getBox(x, y) {
|
||||
// flip coin for red vs blue and add rgb
|
||||
colFlag = Math.round(Math.random()); // random 0,1 variable for box color
|
||||
if (colFlag == 1 ){
|
||||
color = [0, 0, 200];
|
||||
}
|
||||
else {
|
||||
color = [200, 0, 0];
|
||||
}
|
||||
box = Matter.Bodies.rectangle(x, y, simInfo.boxSize, simInfo.boxSize,
|
||||
{frictionAir: simInfo.airDrag,
|
||||
const color = (Math.random() < 0.5) ? [0, 0, 200] : [200, 0, 0]
|
||||
return Matter.Bodies.rectangle(x, y, simInfo.boxSize, simInfo.boxSize, {
|
||||
frictionAir: simInfo.airDrag,
|
||||
friction: simInfo.boxFric,
|
||||
mass: simInfo.boxMass,
|
||||
role: 'box',
|
||||
color: color});
|
||||
return box;
|
||||
};
|
||||
color
|
||||
});
|
||||
}
|
||||
|
||||
const startX = 100, startY = 100,
|
||||
nBoxX = 5, nBoxY = 5,
|
||||
|
@ -121,239 +179,151 @@ function init() { // called once when loading HTML file
|
|||
stack = Matter.Composites.stack(startX, startY,
|
||||
nBoxX, nBoxY,
|
||||
gapX, gapY, getBox);
|
||||
Matter.World.add(simInfo.world, stack);
|
||||
Matter.World.add(this.world, stack);
|
||||
|
||||
/* Add debugging mouse control for dragging objects. */
|
||||
/* Add debug ging mouse control for dragging objects. */
|
||||
if (simInfo.debugMouse){
|
||||
const mouseConstraint = Matter.MouseConstraint.create(simInfo.engine,
|
||||
{mouse: Matter.Mouse.create(arena),
|
||||
const mouseConstraint = Matter.MouseConstraint.create(this.engine, {
|
||||
mouse: Matter.Mouse.create(arena),
|
||||
// spring stiffness mouse ~ object
|
||||
constraint: {stiffness: 0.5}});
|
||||
Matter.World.add(simInfo.world, mouseConstraint);
|
||||
constraint: {stiffness: 0.5}
|
||||
});
|
||||
Matter.World.add(this.world, mouseConstraint);
|
||||
}
|
||||
// Add the tracker functions from mouse.js
|
||||
addMouseTracker(arena);
|
||||
addMouseTracker(robotBay);
|
||||
|
||||
/* Running the MatterJS physics engine (without rendering). */
|
||||
simInfo.runner = Matter.Runner.create({fps: 60, isFixed: false});
|
||||
Matter.Runner.start(simInfo.runner, simInfo.engine);
|
||||
this.runner = Matter.Runner.create({fps: 60, isFixed: false});
|
||||
// register function simStep() as callback to MatterJS's engine events
|
||||
Matter.Events.on(simInfo.engine, 'tick', simStep);
|
||||
Matter.Events.on(this.engine, 'tick', this.step.bind(this));
|
||||
|
||||
/* Create robot(s). */
|
||||
setRobotNumber(1); // requires defined simInfo.world
|
||||
loadBay(robots[0]);
|
||||
|
||||
};
|
||||
|
||||
function convrgb(values) {
|
||||
return 'rgb(' + values.join(', ') + ')';
|
||||
};
|
||||
|
||||
|
||||
function rotate(robot, torque=0) {
|
||||
/* Apply a torque to the robot to rotate it.
|
||||
*
|
||||
* Parameters
|
||||
* torque - rotational force to apply to the body.
|
||||
* Try values around +/- 0.005.
|
||||
*/
|
||||
robot.body.torque = torque;
|
||||
};
|
||||
|
||||
function drive(robot, force=0) {
|
||||
/* Apply a force to the robot to move it.
|
||||
*
|
||||
* Parameters
|
||||
* force - force to apply to the body.
|
||||
* Try values around +/- 0.0005.
|
||||
*/
|
||||
const orientation = robot.body.angle,
|
||||
force_vec = Matter.Vector.create(force, 0),
|
||||
move_vec = Matter.Vector.rotate(force_vec, orientation);
|
||||
Matter.Body.applyForce(robot.body, robot.body.position , move_vec);
|
||||
};
|
||||
|
||||
|
||||
function senseDistance() {
|
||||
/* Distance sensor simulation based on ray casting. Called from sensor
|
||||
* object, returns nothing, updates a new reading into this.value.
|
||||
*
|
||||
* Idea: Cast a ray with a certain length from the sensor, and check
|
||||
* via collision detection if objects intersect with the ray.
|
||||
* To determine distance, run a Binary search on ray length.
|
||||
* Note: Sensor ray needs to ignore robot (parts), or start outside of it.
|
||||
* The latter is easy with the current circular shape of the robots.
|
||||
* Note: Order of tests are optimized by starting with max ray length, and
|
||||
* then only testing the maximal number of initially resulting objects.
|
||||
* Note: The sensor's "ray" could have any other (convex) shape;
|
||||
* currently it's just a very thin rectangle.
|
||||
*/
|
||||
|
||||
const context = document.getElementById('arenaLemming').getContext('2d');
|
||||
var bodies = Matter.Composite.allBodies(simInfo.engine.world);
|
||||
|
||||
const robotAngle = this.parent.body.angle,
|
||||
attachAngle = this.attachAngle,
|
||||
rayAngle = robotAngle + attachAngle + this.lookAngle;
|
||||
|
||||
const rPos = this.parent.body.position,
|
||||
rSize = simInfo.robotSize,
|
||||
startPoint = {x: rPos.x + (rSize+1) * Math.cos(robotAngle + attachAngle),
|
||||
y: rPos.y + (rSize+1) * Math.sin(robotAngle + attachAngle)};
|
||||
|
||||
function getEndpoint(rayLength) {
|
||||
return {x: rPos.x + (rSize + rayLength) * Math.cos(rayAngle),
|
||||
y: rPos.y + (rSize + rayLength) * Math.sin(rayAngle)};
|
||||
};
|
||||
|
||||
function sensorRay(bodies, rayLength) {
|
||||
// Cast ray of supplied length and return the bodies that collide with it.
|
||||
const rayWidth = 1e-100,
|
||||
endPoint = getEndpoint(rayLength);
|
||||
rayX = (endPoint.x + startPoint.x) / 2,
|
||||
rayY = (endPoint.y + startPoint.y) / 2,
|
||||
rayRect = Matter.Bodies.rectangle(rayX, rayY, rayLength, rayWidth,
|
||||
{isSensor: true, isStatic: true,
|
||||
angle: rayAngle, role: 'sensor'});
|
||||
|
||||
var collidedBodies = [];
|
||||
for (var bb = 0; bb < bodies.length; bb++) {
|
||||
var body = bodies[bb];
|
||||
// coarse check on body boundaries, to increase performance:
|
||||
if (Matter.Bounds.overlaps(body.bounds, rayRect.bounds)) {
|
||||
for (var pp = body.parts.length === 1 ? 0 : 1; pp < body.parts.length; pp++) {
|
||||
var part = body.parts[pp];
|
||||
// finer, more costly check on actual geometry:
|
||||
if (Matter.Bounds.overlaps(part.bounds, rayRect.bounds)) {
|
||||
const collision = Matter.SAT.collides(part, rayRect);
|
||||
if (collision.collided) {
|
||||
collidedBodies.push(body);
|
||||
break;
|
||||
this.bay = new Bay()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return collidedBodies;
|
||||
};
|
||||
|
||||
// call 1x with full length, and check all bodies in the world;
|
||||
// in subsequent calls, only check the bodies resulting here
|
||||
var rayLength = this.maxVal;
|
||||
bodies = sensorRay(bodies, rayLength);
|
||||
// if some collided, search for maximal ray length without collisions
|
||||
if (bodies.length > 0) {
|
||||
var lo = 0,
|
||||
hi = rayLength;
|
||||
while (lo < rayLength) {
|
||||
if (sensorRay(bodies, rayLength).length > 0) {
|
||||
hi = rayLength;
|
||||
step() {
|
||||
// advance simulation by one step (except MatterJS engine's physics)
|
||||
if (this.curSteps < simInfo.maxSteps) {
|
||||
this.bay.repaint();
|
||||
this.draw();
|
||||
this.robots.forEach(robot => {
|
||||
robot.updateSensors();
|
||||
robot.move();
|
||||
// To enable selection by clicking (via mouse.js/graphics.js),
|
||||
// the position on the canvas needs to be defined in (x, y):
|
||||
const rSize = simInfo.robotSize;
|
||||
robot.x = robot.body.position.x - rSize;
|
||||
robot.y = robot.body.position.y - rSize;
|
||||
})
|
||||
// count and display number of steps
|
||||
this.curSteps += 1;
|
||||
document.getElementById("SimStepLabel").innerHTML =
|
||||
padnumber(this.curSteps, 5) +
|
||||
' of ' +
|
||||
padnumber(simInfo.maxSteps, 5);
|
||||
}
|
||||
else {
|
||||
lo = rayLength;
|
||||
}
|
||||
rayLength = Math.floor(lo + (hi-lo)/2);
|
||||
this.toggle()
|
||||
}
|
||||
}
|
||||
// increase length to (barely) touch closest body (if any)
|
||||
rayLength += 1;
|
||||
bodies = sensorRay(bodies, rayLength);
|
||||
draw() {
|
||||
const context = this.elem.getContext('2d')
|
||||
context.clearRect(0, 0, this.width, this.height);
|
||||
// draw objects within world
|
||||
const Composite = Matter.Composite,
|
||||
bodies = Composite.allBodies(this.world);
|
||||
bodies.forEach(({role, vertices, color}) => {
|
||||
if (role == 'robot') return
|
||||
if (color) {
|
||||
context.strokeStyle = convrgb(color);
|
||||
}
|
||||
drawVertices(context, vertices);
|
||||
})
|
||||
context.lineWidth = 1;
|
||||
|
||||
if (simInfo.debugSensors) { // if invisible, check order of object drawing
|
||||
// draw the resulting ray
|
||||
endPoint = getEndpoint(rayLength);
|
||||
context.beginPath();
|
||||
context.moveTo(startPoint.x, startPoint.y);
|
||||
context.lineTo(endPoint.x, endPoint.y);
|
||||
context.strokeStyle = this.parent.info.color;
|
||||
context.lineWidth = 0.5;
|
||||
context.stroke();
|
||||
// mark all objects's lines intersecting with the ray
|
||||
for (var bb = 0; bb < bodies.length; bb++) {
|
||||
var vertices = bodies[bb].vertices;
|
||||
context.moveTo(vertices[0].x, vertices[0].y);
|
||||
for (var vv = 1; vv < vertices.length; vv += 1) {
|
||||
context.lineTo(vertices[vv].x, vertices[vv].y);
|
||||
// draw all robots
|
||||
this.robots.forEach(robot => robot.plotRobot(context))
|
||||
}
|
||||
context.closePath();
|
||||
addRobot(robot) {
|
||||
this.robots.push(
|
||||
makeInteractiveElement(robot, this.elem))
|
||||
if (!this.bay.robot) this.bay.load(robot)
|
||||
}
|
||||
context.stroke();
|
||||
removeRobot(robot) {
|
||||
this.robots = this.robots.filter(x => x == robot)
|
||||
Matter.World.remove(this.world, robot.body)
|
||||
}
|
||||
|
||||
// indicate if the sensor exceeded its maximum length by returning infinity
|
||||
if (rayLength > this.maxVal) {
|
||||
rayLength = Infinity;
|
||||
start() {
|
||||
if (!this.doContinue) Matter.Runner.start(this.runner, this.engine);
|
||||
this.doContinue = true
|
||||
}
|
||||
else {
|
||||
// apply mild noise on the sensor reading, and clamp between valid values
|
||||
function gaussNoise(sigma=1) {
|
||||
const x0 = 1.0 - Math.random();
|
||||
const x1 = 1.0 - Math.random();
|
||||
return sigma * Math.sqrt(-2 * Math.log(x0)) * Math.cos(2 * Math.PI * x1);
|
||||
};
|
||||
rayLength = Math.floor(rayLength + gaussNoise(3));
|
||||
rayLength = Matter.Common.clamp(rayLength, this.minVal, this.maxVal);
|
||||
stop() {
|
||||
if (this.doContinue) Matter.Runner.stop(this.runner)
|
||||
this.doContinue = false
|
||||
}
|
||||
|
||||
this.value = rayLength;
|
||||
toggle() {
|
||||
if (this.doContinue) this.stop()
|
||||
else this.start()
|
||||
}
|
||||
}
|
||||
function init() { // called once when loading HTML file
|
||||
// the pathseg polyfill will run in the html document
|
||||
// but we need it in the svg. I changed the IIFE in to a
|
||||
// regular function and inject it into the svg using eval
|
||||
const svg = document.getElementById('robotbodySVG')
|
||||
svg.contentWindow.eval('(' + window.pathseg.toString() + ')()')
|
||||
sim = new Simulation()
|
||||
sim.init()
|
||||
sim.addRobot(new Lemming({
|
||||
color: [255, 255, 255], // color of the robot shape
|
||||
init: {x: 50, y: 50, angle: 0}, // initial position and orientation
|
||||
}))
|
||||
sim.start()
|
||||
};
|
||||
|
||||
function dragSensor(sensor, event) {
|
||||
const robotBay = document.getElementById('bayLemming'),
|
||||
bCenter = {x: robotBay.width/2,
|
||||
y: robotBay.height/2},
|
||||
rSize = simInfo.robotSize,
|
||||
bScale = simInfo.bayScale,
|
||||
sSize = sensor.getWidth(),
|
||||
mAngle = Math.atan2( event.mouse.x - bCenter.x,
|
||||
-(event.mouse.y - bCenter.y));
|
||||
sensor.info.attachAngle = mAngle;
|
||||
sensor.x = bCenter.x - sSize - bScale * rSize * Math.sin(-mAngle);
|
||||
sensor.y = bCenter.y - sSize - bScale * rSize * Math.cos( mAngle);
|
||||
repaintBay();
|
||||
}
|
||||
|
||||
function loadSensor(sensor, event) {
|
||||
loadSensorInfo(sensor.sensor);
|
||||
}
|
||||
|
||||
function loadSensorInfo(sensorInfo) {
|
||||
simInfo.baySensor = sensorInfo;
|
||||
}
|
||||
|
||||
function loadBay(robot) {
|
||||
simInfo.bayRobot = robot;
|
||||
sensors = new Array();
|
||||
const robotBay = document.getElementById("bayLemming");
|
||||
const bCenter = {x: robotBay.width/2,
|
||||
y: robotBay.height/2},
|
||||
rSize = simInfo.robotSize,
|
||||
bScale = simInfo.bayScale;
|
||||
|
||||
for (var ss = 0; ss < robot.info.sensors.length; ++ss) {
|
||||
const curSensor = robot.sensors[ss],
|
||||
attachAngle = curSensor.attachAngle;
|
||||
// put current sensor into global variable, make mouse-interactive
|
||||
sensors[ss] = makeInteractiveElement(new SensorGraphics(curSensor),
|
||||
document.getElementById("bayLemming"));
|
||||
const sSize = sensors[ss].getWidth();
|
||||
sensors[ss].x = bCenter.x - sSize - bScale * rSize * Math.sin(-attachAngle);
|
||||
sensors[ss].y = bCenter.y - sSize - bScale * rSize * Math.cos( attachAngle);
|
||||
sensors[ss].onDragging = dragSensor;
|
||||
sensors[ss].onDrag = loadSensor;
|
||||
class Bay {
|
||||
constructor() {
|
||||
this.elem = document.getElementById("bayLemming")
|
||||
this.context = this.elem.getContext('2d')
|
||||
this.center = {x: this.elem.width / simInfo.bayScale / 2,
|
||||
y: this.elem.height / simInfo.bayScale / 2}
|
||||
this.robot = null
|
||||
this.elem.style.backgroundColor = 'silver'
|
||||
addMouseTracker(this.elem);
|
||||
}
|
||||
load(robot) {
|
||||
this.robot = robot
|
||||
robot.sensors.forEach(sensor => {
|
||||
makeInteractiveElement(sensor, this.elem)
|
||||
})
|
||||
// todo: removeinteractiveelement?
|
||||
this.repaint()
|
||||
}
|
||||
repaint() {
|
||||
const {context, robot, elem: robotBay} = this
|
||||
// update inset canvas showing information about selected robot
|
||||
context.clearRect(0, 0, this.elem.width, this.elem.height);
|
||||
if (!robot) return
|
||||
context.save()
|
||||
context.scale(simInfo.bayScale, simInfo.bayScale)
|
||||
context.translate(this.center.x, this.center.y)
|
||||
context.rotate(-Math.PI/2)
|
||||
robot.plotRobot(context, 0, 0, 0);
|
||||
context.restore()
|
||||
// print sensor values of selected robot next to canvas
|
||||
if (!(sim.curSteps % 5)) { // update slow enough to read
|
||||
const sensorString = robot.sensors.map(({id, valueStr, color}) => {
|
||||
return `<br> <span style="color:${color ? convrgb(color) : 'black'}">id '${id}': ${valueStr}</span>`
|
||||
}).join('')
|
||||
|
||||
document.getElementById('SensorLabel').innerHTML = sensorString;
|
||||
}
|
||||
}
|
||||
transformMouse({x,y}) {
|
||||
// scale, translate, rotate by 90 degrees,
|
||||
return { y: (x / simInfo.bayScale) - this.center.x,
|
||||
x: -((y / simInfo.bayScale) - this.center.y) }
|
||||
}
|
||||
repaintBay();
|
||||
}
|
||||
|
||||
function SensorGraphics(sensorInfo) {
|
||||
this.info = sensorInfo;
|
||||
this.plotSensor = plotSensor;
|
||||
// add functions getWidth/getHeight for graphics.js & mouse.js,
|
||||
// to enable dragging the sensor in the robot bay
|
||||
this.getWidth = function() { return 6; };
|
||||
this.getHeight = function() { return 6; };
|
||||
}
|
||||
|
||||
function loadFromSVG() {
|
||||
|
@ -361,289 +331,134 @@ function loadFromSVG() {
|
|||
const svg = document.getElementById('robotbodySVG'),
|
||||
data = svg.contentDocument;
|
||||
|
||||
jQuery(data).find('path').each(function(_, path) {
|
||||
var points = Matter.Svg.pathToVertices(path, 30);
|
||||
for (const path of data.getElementsByTagName('path')) {
|
||||
const points = Matter.Svg.pathToVertices(path, 30);
|
||||
vertexSets.push(Matter.Vertices.scale(points, 0.2, 0.2));
|
||||
});
|
||||
}
|
||||
|
||||
return vertexSets;
|
||||
};
|
||||
|
||||
function InstantiateRobot(robotInfo) {
|
||||
function Robot(robotInfo) {
|
||||
// load robot's body shape from SVG file
|
||||
const bodySVGpoints = loadFromSVG();
|
||||
this.body = Matter.Bodies.fromVertices(robotInfo.init.x,
|
||||
robotInfo.init.y,
|
||||
bodySVGpoints,
|
||||
{frictionAir: simInfo.airDrag,
|
||||
bodySVGpoints, {
|
||||
frictionAir: simInfo.airDrag,
|
||||
mass: simInfo.robotMass,
|
||||
color: [255, 255, 255],
|
||||
role: 'robot'}, true);
|
||||
role: 'robot'
|
||||
}, true);
|
||||
|
||||
Matter.World.add(simInfo.world, this.body);
|
||||
Matter.World.add(sim.world, this.body);
|
||||
Matter.Body.setAngle(this.body, robotInfo.init.angle);
|
||||
|
||||
// instantiate its sensors
|
||||
this.sensors = robotInfo.sensors;
|
||||
for (var ss = 0; ss < this.sensors.length; ++ss) {
|
||||
this.sensors[ss].parent = this;
|
||||
}
|
||||
this.sensors.forEach(sensor => sensor.parent = this)
|
||||
|
||||
// attach its helper functions
|
||||
this.rotate = rotate;
|
||||
this.drive = drive;
|
||||
this.info = robotInfo;
|
||||
this.plotRobot = plotRobot;
|
||||
|
||||
// add functions getWidth/getHeight for graphics.js & mouse.js,
|
||||
// to enable selection by clicking the robot in the arena
|
||||
this.getWidth = function() { return 2 * simInfo.robotSize; };
|
||||
this.getHeight = function() { return 2 * simInfo.robotSize; };
|
||||
}
|
||||
Object.assign(Robot.prototype, {
|
||||
rotate(torque=0) {
|
||||
/* Apply a torque to the robot to rotate it.
|
||||
*
|
||||
* Parameters
|
||||
* torque - rotational force to apply to the body.
|
||||
* Try values around +/- 0.005.
|
||||
*/
|
||||
this.body.torque = torque;
|
||||
},
|
||||
|
||||
function robotUpdateSensors(robot) {
|
||||
// update all sensors of robot; puts new values into sensor.value
|
||||
for (var ss = 0; ss < robot.sensors.length; ss++) {
|
||||
robot.sensors[ss].sense();
|
||||
}
|
||||
};
|
||||
|
||||
function getSensorValById(robot, id) {
|
||||
for (var ss = 0; ss < robot.sensors.length; ss++) {
|
||||
if (robot.sensors[ss].id == id) {
|
||||
return robot.sensors[ss].value;
|
||||
}
|
||||
}
|
||||
return undefined; // if not returned yet, id doesn't exist
|
||||
};
|
||||
|
||||
function robotMove(robot) {
|
||||
drive(force=0) {
|
||||
/* Apply a force to the robot to move it.
|
||||
*
|
||||
* Parameters
|
||||
* force - force to apply to the body.
|
||||
* Try values around +/- 0.0005.
|
||||
*/
|
||||
const orientation = this.body.angle,
|
||||
force_vec = Matter.Vector.create(force, 0),
|
||||
move_vec = Matter.Vector.rotate(force_vec, orientation);
|
||||
Matter.Body.applyForce(this.body, this.body.position , move_vec);
|
||||
},
|
||||
updateSensors() {
|
||||
this.sensors.forEach(sensor => sensor.sense())
|
||||
},
|
||||
getSensorValById(id) {
|
||||
const sensor = this.sensors.find(sensor => sensor.id == id)
|
||||
return sensor ? sensor.value : undefined
|
||||
},
|
||||
move() {
|
||||
// TODO: Define Lemming program here.
|
||||
const distL = getSensorValById(robot, 'distL'),
|
||||
distR = getSensorValById(robot, 'distR');
|
||||
const distL = this.getSensorValById('distL'),
|
||||
distR = this.getSensorValById('distR');
|
||||
|
||||
robot.rotate(robot, +0.005);
|
||||
robot.drive(robot, 0.0005);
|
||||
};
|
||||
|
||||
function plotSensor(context, x = this.x, y = this.y) {
|
||||
context.beginPath();
|
||||
context.arc(x + this.getWidth()/2,
|
||||
y + this.getHeight()/2,
|
||||
this.getWidth()/2, 0, 2*Math.PI);
|
||||
context.closePath();
|
||||
context.fillStyle = 'black';
|
||||
context.strokeStyle = 'black';
|
||||
context.fill();
|
||||
context.stroke();
|
||||
}
|
||||
|
||||
function plotRobot(context,
|
||||
xTopLeft = this.body.position.x,
|
||||
yTopLeft = this.body.position.y) {
|
||||
var x, y, scale, angle, i, half, full,
|
||||
rSize = simInfo.robotSize;
|
||||
//robot.rotate(+0.005);
|
||||
//robot.drive(0.0005);
|
||||
},
|
||||
mouseHit(x, y) {
|
||||
return Vec2.distLess(this.body.position, {x,y}, this.getWidth()/2 + 1)
|
||||
},
|
||||
getWidth() { return 2 * simInfo.robotSize },
|
||||
getHeight() { return 2 * simInfo.robotSize },
|
||||
onDrop(robot, event) {
|
||||
this.isDragged = false
|
||||
},
|
||||
onDrag(robot, event) {
|
||||
this.isDragged = true
|
||||
sim.bay.load(this)
|
||||
return true
|
||||
},
|
||||
plotRobot(context,
|
||||
x = this.body.position.x,
|
||||
y = this.body.position.y,
|
||||
angle = this.body.angle) {
|
||||
const showInternalEdges = false;
|
||||
const body = this.body
|
||||
|
||||
if (context.canvas.id == "bayLemming") {
|
||||
scale = simInfo.bayScale;
|
||||
half = Math.floor(rSize/2*scale);
|
||||
full = half * 2;
|
||||
x = xTopLeft + full;
|
||||
y = yTopLeft + full;
|
||||
angle = -Math.PI / 2;
|
||||
} else {
|
||||
scale = 1;
|
||||
half = Math.floor(rSize/2*scale);
|
||||
full = half * 2;
|
||||
x = xTopLeft;
|
||||
y = yTopLeft;
|
||||
angle = this.body.angle;
|
||||
}
|
||||
// MatterJS thinks in world coords
|
||||
// translate the world canvas to compensate
|
||||
context.save();
|
||||
context.translate(x, y);
|
||||
context.rotate(angle);
|
||||
context.rotate(-body.angle + angle);
|
||||
context.translate(-body.position.x + x, -body.position.y + y);
|
||||
|
||||
if (context.canvas.id == "arenaLemming") {
|
||||
// draw into world canvas without transformations,
|
||||
// because MatterJS thinks in world coords...
|
||||
context.restore();
|
||||
|
||||
const body = this.body;
|
||||
// handle compound parts
|
||||
|
||||
context.beginPath();
|
||||
for (k = body.parts.length > 1 ? 1 : 0; k < body.parts.length; k++) {
|
||||
part = body.parts[k];
|
||||
context.moveTo(part.vertices[0].x,
|
||||
part.vertices[0].y);
|
||||
for (j = 1; j < part.vertices.length; j++) {
|
||||
if (!part.vertices[j - 1].isInternal || showInternalEdges) {
|
||||
context.lineTo(part.vertices[j].x,
|
||||
part.vertices[j].y);
|
||||
} else {
|
||||
context.moveTo(part.vertices[j].x,
|
||||
part.vertices[j].y);
|
||||
}
|
||||
|
||||
if (part.vertices[j].isInternal && !showInternalEdges) {
|
||||
context.moveTo(part.vertices[(j + 1) % part.vertices.length].x,
|
||||
part.vertices[(j + 1) % part.vertices.length].y);
|
||||
}
|
||||
}
|
||||
context.lineTo(part.vertices[0].x,
|
||||
part.vertices[0].y);
|
||||
}
|
||||
context.strokeStyle = convrgb(body.color);
|
||||
context.lineWidth = 1.5;
|
||||
context.stroke();
|
||||
body.parts.forEach(({vertices}, k) => {
|
||||
if (k == 0 && body.parts.length > 1) return
|
||||
context.moveTo(vertices[0].x, vertices[0].y);
|
||||
let fn, wasInternal = true
|
||||
vertices.forEach(fn = ({x,y, isInternal}, j) => {
|
||||
if (wasInternal) context.moveTo(x, y)
|
||||
else context.lineTo(x, y)
|
||||
wasInternal = isInternal && !showInternalEdges
|
||||
})
|
||||
fn(vertices[0])
|
||||
})
|
||||
context.stroke()
|
||||
|
||||
// to draw the rest, rotate & translate again
|
||||
context.restore()
|
||||
|
||||
context.save();
|
||||
context.translate(x, y);
|
||||
context.rotate(angle);
|
||||
}
|
||||
|
||||
// Plot sensor positions into world canvas.
|
||||
if (context.canvas.id == "arenaLemming") {
|
||||
for (ss = 0; ss < this.info.sensors.length; ++ss) {
|
||||
context.beginPath();
|
||||
context.arc(full * Math.cos(this.info.sensors[ss].attachAngle),
|
||||
full * Math.sin(this.info.sensors[ss].attachAngle),
|
||||
scale, 0, 2*Math.PI);
|
||||
context.closePath();
|
||||
context.fillStyle = 'black';
|
||||
context.strokeStyle = 'black';
|
||||
context.fill();
|
||||
context.stroke();
|
||||
}
|
||||
}
|
||||
this.sensors.forEach(sensor => sensor.plotSensor(context))
|
||||
context.restore();
|
||||
}
|
||||
|
||||
function simStep() {
|
||||
// advance simulation by one step (except MatterJS engine's physics)
|
||||
if (simInfo.curSteps < simInfo.maxSteps) {
|
||||
repaintBay();
|
||||
drawBoard();
|
||||
for (var rr = 0; rr < robots.length; ++rr) {
|
||||
robotUpdateSensors(robots[rr]);
|
||||
robotMove(robots[rr]);
|
||||
// To enable selection by clicking (via mouse.js/graphics.js),
|
||||
// the position on the canvas needs to be defined in (x, y):
|
||||
const rSize = simInfo.robotSize;
|
||||
robots[rr].x = robots[rr].body.position.x - rSize;
|
||||
robots[rr].y = robots[rr].body.position.y - rSize;
|
||||
}
|
||||
// count and display number of steps
|
||||
simInfo.curSteps += 1;
|
||||
document.getElementById("SimStepLabel").innerHTML =
|
||||
padnumber(simInfo.curSteps, 5) +
|
||||
' of ' +
|
||||
padnumber(simInfo.maxSteps, 5);
|
||||
}
|
||||
else {
|
||||
toggleSimulation();
|
||||
}
|
||||
}
|
||||
|
||||
function drawBoard() {
|
||||
var context = document.getElementById('arenaLemming').getContext('2d');
|
||||
context.fillStyle = "#444444";
|
||||
context.fillRect(0, 0, simInfo.width, simInfo.height);
|
||||
|
||||
// draw objects within world
|
||||
const Composite = Matter.Composite,
|
||||
bodies = Composite.allBodies(simInfo.world);
|
||||
|
||||
for (var bb = 0; bb < bodies.length; bb += 1) {
|
||||
var vertices = bodies[bb].vertices,
|
||||
vv;
|
||||
|
||||
// draw all non-robot bodies here (walls and boxes)
|
||||
// don't draw robot's bodies here; they're drawn in plotRobot()
|
||||
if (bodies[bb].role != 'robot') {
|
||||
context.beginPath();
|
||||
context.moveTo(vertices[0].x, vertices[0].y);
|
||||
for (vv = 1; vv < vertices.length; vv += 1) {
|
||||
context.lineTo(vertices[vv].x, vertices[vv].y);
|
||||
}
|
||||
if (bodies[bb].color) {
|
||||
context.strokeStyle = convrgb(bodies[bb].color);
|
||||
context.closePath();
|
||||
context.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
context.lineWidth = 1;
|
||||
|
||||
// draw all robots
|
||||
for (var rr = 0; rr < robots.length; ++rr) {
|
||||
robots[rr].plotRobot(context);
|
||||
}
|
||||
}
|
||||
|
||||
function repaintBay() {
|
||||
// update inset canvas showing information about selected robot
|
||||
const robotBay = document.getElementById('bayLemming'),
|
||||
context = robotBay.getContext('2d');
|
||||
context.clearRect(0, 0, robotBay.width, robotBay.height);
|
||||
simInfo.bayRobot.plotRobot(context, 10, 10);
|
||||
for (var ss = 0; ss < sensors.length; ss++) {
|
||||
sensors[ss].plotSensor(context);
|
||||
}
|
||||
|
||||
// print sensor values of selected robot next to canvas
|
||||
if (!(simInfo.curSteps % 5)) { // update slow enough to read
|
||||
var sensorString = '';
|
||||
const rsensors = simInfo.bayRobot.sensors;
|
||||
for (ss = 0; ss < rsensors.length; ss++) {
|
||||
sensorString += '<br> id \'' + rsensors[ss].id + '\': ' +
|
||||
padnumber(rsensors[ss].value, 2);
|
||||
}
|
||||
document.getElementById('SensorLabel').innerHTML = sensorString;
|
||||
}
|
||||
}
|
||||
|
||||
function setRobotNumber(newValue) {
|
||||
var n;
|
||||
while (robots.length > newValue) {
|
||||
n = robots.length - 1;
|
||||
Matter.World.remove(simInfo.world, robots[n].body);
|
||||
robots[n] = null;
|
||||
robots.length = n;
|
||||
}
|
||||
|
||||
while (robots.length < newValue) {
|
||||
if (newValue > RobotInfo.length) {
|
||||
console.warn('You request '+newValue+' robots, but only ' + RobotInfo.length +
|
||||
' are defined in RobotInfo!');
|
||||
toggleSimulation();
|
||||
return;
|
||||
}
|
||||
n = robots.length;
|
||||
robots[n] = makeInteractiveElement(new InstantiateRobot(RobotInfo[n]),
|
||||
document.getElementById("arenaLemming"));
|
||||
|
||||
robots[n].onDrop = function(robot, event) {
|
||||
robot.isDragged = false;
|
||||
};
|
||||
|
||||
robots[n].onDrag = function(robot, event) {
|
||||
robot.isDragged = true;
|
||||
loadBay(robot);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
function padnumber(number, size) {
|
||||
if (number == Infinity) {
|
||||
return 'inf';
|
||||
}
|
||||
const s = "000000" + number;
|
||||
return s.substr(s.length - size);
|
||||
if (number == 'Infinity') return 'inf'
|
||||
return (''+number).padStart(size, '0')
|
||||
}
|
||||
|
||||
function format(number) {
|
||||
|
@ -652,12 +467,6 @@ function format(number) {
|
|||
}
|
||||
|
||||
function toggleSimulation() {
|
||||
simInfo.doContinue = !simInfo.doContinue;
|
||||
if (simInfo.doContinue) {
|
||||
Matter.Runner.start(simInfo.runner, simInfo.engine);
|
||||
}
|
||||
else {
|
||||
Matter.Runner.stop(simInfo.runner);
|
||||
}
|
||||
sim.toggle()
|
||||
}
|
||||
|
||||
|
|
14
mouse.js
|
@ -58,17 +58,15 @@ function addMouseTracker(elem) {
|
|||
this.mouse.hasFocus = false;
|
||||
});
|
||||
elem.addInteractiveElement = function(obj) {
|
||||
if (!obj.mouseHit) obj.mouseHit = mouseHit
|
||||
this.mouse.interactiveElements.push(obj);
|
||||
};
|
||||
}
|
||||
|
||||
function mouseHit(x, y) {
|
||||
return (x < this.x + this.getWidth() && x > this.x &&
|
||||
y < this.y + this.getHeight() && y > this.y)
|
||||
}
|
||||
function findElement(x, y) {
|
||||
for (var i = this.mouse.interactiveElements.length - 1; i >= 0; --i) {
|
||||
if (x < this.mouse.interactiveElements[i].x + this.mouse.interactiveElements[i].getWidth() && x > this.mouse.interactiveElements[i].x &&
|
||||
y < this.mouse.interactiveElements[i].y + this.mouse.interactiveElements[i].getHeight() && y > this.mouse.interactiveElements[i].y) {
|
||||
return this.mouse.interactiveElements[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return this.mouse.interactiveElements.find(elem => elem.mouseHit(x, y))
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
// SVG2 (https://lists.w3.org/Archives/Public/www-svg/2015Jun/0044.html), including the latest spec
|
||||
// changes which were implemented in Firefox 43 and Chrome 46.
|
||||
|
||||
(function() { "use strict";
|
||||
window.pathseg = (function() { "use strict";
|
||||
if (!("SVGPathSeg" in window)) {
|
||||
// Spec: http://www.w3.org/TR/SVG11/single-page.html#paths-InterfaceSVGPathSeg
|
||||
window.SVGPathSeg = function(type, typeAsLetter, owningPathSegList) {
|
||||
|
@ -841,4 +841,4 @@
|
|||
return builder.pathSegList;
|
||||
}
|
||||
}
|
||||
}());
|
||||
});
|
||||
|
|
|
@ -13,13 +13,13 @@
|
|||
version="1.1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
width="300"
|
||||
height="300"
|
||||
viewBox="0 0 300 300"
|
||||
width="239.19885"
|
||||
height="186.78011"
|
||||
viewBox="0 0 239.19885 186.78011"
|
||||
xml:space="preserve"
|
||||
id="svg11"
|
||||
sodipodi:docname="robotbody.svg"
|
||||
inkscape:version="0.92.2 5c3e80d, 2017-08-06"><metadata
|
||||
sodipodi:docname="robotbody_longleft_mediumright.svg"
|
||||
inkscape:version="0.92.2 (unknown)"><metadata
|
||||
id="metadata17"><rdf:RDF><ns:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></ns:Work><cc:Work
|
||||
|
@ -34,32 +34,32 @@
|
|||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1676"
|
||||
inkscape:window-height="1011"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1016"
|
||||
id="namedview13"
|
||||
showgrid="false"
|
||||
inkscape:zoom="1.5742187"
|
||||
inkscape:cx="256.30163"
|
||||
inkscape:cy="152.71994"
|
||||
inkscape:window-x="1366"
|
||||
inkscape:window-y="18"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:cx="193.78259"
|
||||
inkscape:cy="72.49728"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg11"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true"
|
||||
inkscape:snap-center="true"
|
||||
inkscape:snap-page="true"
|
||||
inkscape:snap-global="false"><sodipodi:guide
|
||||
position="150,150"
|
||||
position="87.480964,95.18677"
|
||||
orientation="1,0"
|
||||
id="guide863"
|
||||
inkscape:locked="false" /><sodipodi:guide
|
||||
position="150,150"
|
||||
position="87.480964,95.18677"
|
||||
orientation="0,1"
|
||||
id="guide867"
|
||||
inkscape:locked="false" /></sodipodi:namedview><path
|
||||
id="robotbody"
|
||||
d="m 84.359841,90.688926 c -29.407074,38.075324 -29.2031,89.304324 1.126302,124.879394 28.308167,33.20432 96.216927,41.60047 129.034147,9.08347 20.97597,3.07228 22.33436,4.02195 28.5988,-5.90513 5.06112,-8.02021 -4.86652,-25.70583 -4.67439,-28.06416 2.47109,-30.33143 1.21949,-67.4846 -2.03551,-83.75854 -0.8545,-4.27227 15.72301,-15.309039 6.75115,-25.723984 C 233.31499,69.77099 220.75332,85.506011 218.4234,82.582429 196.08956,54.557611 120.83205,43.465878 84.359841,90.688926 Z"
|
||||
d="m 21.840805,32.282261 c -29.4070742,38.075324 -29.2031002,89.304329 1.126302,124.879399 28.308167,33.20432 96.216923,41.60047 129.034143,9.08347 20.97597,3.07228 40.7562,3.38671 47.02064,-6.54037 5.06112,-8.02021 -23.28836,-25.07059 -23.09623,-27.42892 2.47109,-30.33143 1.21949,-67.484605 -2.03551,-83.758545 -0.8545,-4.27227 73.52946,-1.96909 64.5576,-12.384033 C 228.6024,24.704276 158.23428,27.099346 155.90436,24.175764 133.57052,-3.8490539 58.313014,-14.940787 21.840805,32.282261 Z"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="sscssssss"
|
||||
style="stroke-width:0.99999988" /></svg>
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.8 KiB |
|
@ -0,0 +1,174 @@
|
|||
|
||||
// Description of robot(s), and attached sensor(s) used by InstantiateRobot()
|
||||
class Sensor {
|
||||
constructor(id, props) {
|
||||
Object.assign(this, {
|
||||
id,
|
||||
parent: null,
|
||||
lookAngle:0,
|
||||
attachRadius: simInfo.robotSize,
|
||||
value: null,
|
||||
valueStr: "<uninit>",
|
||||
minVal: 0,
|
||||
maxVal: 50
|
||||
}, props)
|
||||
this.info = this
|
||||
this.setAngle(this.attachAngle)
|
||||
}
|
||||
setAngle(attachAngle) {
|
||||
this.attachAngle = attachAngle
|
||||
Object.assign(this, Vec2.fromPolar(Vec2.zero, this.attachRadius, this.attachAngle))
|
||||
}
|
||||
plotSensor(context, x = this.x, y = this.y) {
|
||||
context.beginPath();
|
||||
context.arc(x, y, this.getWidth()/2, 0, 2*Math.PI);
|
||||
context.closePath();
|
||||
context.fillStyle = 'black';
|
||||
context.strokeStyle = this.color ? convrgb(this.color) : 'black';
|
||||
context.fill();
|
||||
context.stroke();
|
||||
}
|
||||
onDragging(sensor, event) {
|
||||
const {x, y} = sim.bay.transformMouse(event.mouse),
|
||||
mAngle = Math.atan2(y, x),
|
||||
mRadius = Math.sqrt(x*x + y*y);
|
||||
this.attachRadius = mRadius
|
||||
this.setAngle(mAngle)
|
||||
sim.bay.repaint()
|
||||
}
|
||||
getWidth() { return 2 }
|
||||
getHeight() { return 2 }
|
||||
mouseHit(x, y) {
|
||||
const mouse = sim.bay.transformMouse({x,y})
|
||||
return Vec2.distLess(this, mouse, this.getWidth()/2 + 1)
|
||||
}
|
||||
};
|
||||
class DistanceSensor extends Sensor {
|
||||
rayCast() {
|
||||
/* Distance sensor simulation based on ray casting. Called from sensor
|
||||
* object, returns nothing, updates a new reading into this.value.
|
||||
*
|
||||
* Idea: Cast a ray with a certain length from the sensor, and check
|
||||
* via collision detection if objects intersect with the ray.
|
||||
* To determine distance, run a Binary search on ray length.
|
||||
* Note: Sensor ray needs to ignore robot (parts), or start outside of it.
|
||||
* The latter is easy with the current circular shape of the robots.
|
||||
* Note: Order of tests are optimized by starting with max ray length, and
|
||||
* then only testing the maximal number of initially resulting objects.
|
||||
* Note: The sensor's "ray" could have any other (convex) shape;
|
||||
* currently it's just a very thin rectangle.
|
||||
*/
|
||||
|
||||
var bodies = Matter.Composite.allBodies(sim.engine.world);
|
||||
if (this.filter) bodies = bodies.filter(this.filter)
|
||||
|
||||
const robotAngle = this.parent.body.angle,
|
||||
rayAngle = robotAngle + this.attachAngle + this.lookAngle;
|
||||
|
||||
const rPos = this.parent.body.position,
|
||||
rSize = this.attachRadius,
|
||||
startPoint = Vec2.fromPolar(rPos, this.attachRadius, robotAngle+this.attachAngle)
|
||||
|
||||
function getEndpoint(rayLength) {
|
||||
return Vec2.fromPolar(startPoint, rayLength, rayAngle)
|
||||
};
|
||||
|
||||
function sensorRay(bodies, rayLength) {
|
||||
// Cast ray of supplied length and return the bodies that collide with it.
|
||||
const rayWidth = 1e-100,
|
||||
ray = Vec2.avg(startPoint, getEndpoint(rayLength)),
|
||||
rayRect = Matter.Bodies.rectangle(ray.x, ray.y, rayLength, rayWidth,
|
||||
{isSensor: true, isStatic: true,
|
||||
angle: rayAngle, role: 'sensor'});
|
||||
|
||||
return bodies.filter(body => {
|
||||
// coarse check on body boundaries, to increase performance:
|
||||
return Matter.Bounds.overlaps(body.bounds, rayRect.bounds) &&
|
||||
body.parts.some((part, pp) => {
|
||||
// skip the first part, if it's not the only one
|
||||
if (pp == 0 && body.parts.length > 1) return false
|
||||
// finer, more costly check on actual geometry:
|
||||
if (Matter.Bounds.overlaps(part.bounds, rayRect.bounds)) {
|
||||
const collision = Matter.SAT.collides(part, rayRect);
|
||||
if (collision.collided) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
// call 1x with full length, and check all bodies in the world;
|
||||
// in subsequent calls, only check the bodies resulting here
|
||||
var rayLength = this.maxVal;
|
||||
bodies = sensorRay(bodies, rayLength);
|
||||
function binarySearch(lo, hi, cond) {
|
||||
let x = hi
|
||||
while (lo < x) {
|
||||
if (cond(x)) {
|
||||
hi = x
|
||||
} else {
|
||||
lo = x
|
||||
}
|
||||
x = Math.floor(lo + (hi-lo)/2)
|
||||
}
|
||||
return x
|
||||
}
|
||||
// if some collided, search for maximal ray length without collisions
|
||||
if (bodies.length > 0) {
|
||||
rayLength = binarySearch(0, rayLength, len => sensorRay(bodies, len).length > 0)
|
||||
}
|
||||
// increase length to (barely) touch closest body (if any)
|
||||
rayLength += 1;
|
||||
bodies = sensorRay(bodies, rayLength);
|
||||
return [bodies, rayLength, startPoint, getEndpoint(rayLength)]
|
||||
}
|
||||
sense() {
|
||||
const context = document.getElementById('arenaLemming').getContext('2d');
|
||||
const [bodies, rayLength, startPoint, endPoint] = this.rayCast()
|
||||
if (simInfo.debugSensors) { // if invisible, check order of object drawing
|
||||
// draw the resulting ray
|
||||
context.strokeStyle = convrgb(this.parent.info.color)
|
||||
context.lineWidth = 0.5;
|
||||
drawVertices(context, [startPoint, endPoint])
|
||||
// mark all objects's lines intersecting with the ray
|
||||
bodies.forEach(({vertices}) => drawVertices(context, vertices))
|
||||
}
|
||||
let rl
|
||||
|
||||
// indicate if the sensor exceeded its maximum length by returning infinity
|
||||
if (rayLength > this.maxVal) {
|
||||
rl = Infinity;
|
||||
}
|
||||
else {
|
||||
rl = Math.floor(rayLength + gaussNoise(3));
|
||||
rl = Matter.Common.clamp(rl, this.minVal, this.maxVal);
|
||||
}
|
||||
|
||||
this.value = rl;
|
||||
this.valueStr = padnumber(this.value, 2);
|
||||
}
|
||||
}
|
||||
function clamp(x, min, max) {
|
||||
return x < min ? min : x > max ? max : x
|
||||
}
|
||||
class ColorSensor extends DistanceSensor {
|
||||
sense() {
|
||||
const [bodies, rayLength] = this.rayCast()
|
||||
let color
|
||||
if (bodies.length) {
|
||||
color = bodies[0].color.map(x => clamp(x + gaussNoise(), 0, 255))
|
||||
} else {
|
||||
color = [255,255,255]
|
||||
}
|
||||
this.value = color
|
||||
this.valueStr = `</span><span style="color: ${convrgb(color)}">` +
|
||||
color.map(x => format(x)).join(', ')
|
||||
}
|
||||
}
|
||||
class Gyroscope extends Sensor {
|
||||
sense() {
|
||||
this.value = (this.parent.body.angle * (180/Math.PI) + gaussNoise()) % 360
|
||||
this.valueStr = format(this.value) + '°'
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
"use strict"
|
||||
const Vec2 = {
|
||||
add({x:x1, y:y1}, {x:x2, y:y2}) {
|
||||
return Object.freeze({x:x1+x2, y:y1+y2})
|
||||
},
|
||||
avg({x:x1, y:y1}, {x:x2, y:y2}) {
|
||||
return Object.freeze({x:(x1+x2)/2, y:(y1+y2)/2})
|
||||
},
|
||||
fromPolar({x,y}, radius, angle) {
|
||||
return Object.freeze({x: x + radius * Math.cos(angle),
|
||||
y: y + radius * Math.sin(angle)})
|
||||
},
|
||||
sub({x:x1, y:y1}, {x:x2, y:y2}) {
|
||||
return Object.freeze({x:x1-x2, y: y1-y2})
|
||||
},
|
||||
distLess(a, b, radius) {
|
||||
const {x, y} = Vec2.sub(a, b)
|
||||
return ((x*x) + (y*y)) < (radius * radius)
|
||||
},
|
||||
zero: Object.freeze({x:0, y:0})
|
||||
};
|
||||
|
||||
function drawVertices(context, vertices, draw=true, close=true) {
|
||||
if (draw) context.beginPath();
|
||||
context.moveTo(vertices[0].x, vertices[0].y);
|
||||
vertices.slice(1).forEach(({x, y}) =>
|
||||
context.lineTo(x, y))
|
||||
if (close) context.lineTo(vertices[0].x, vertices[0].y);
|
||||
if (draw) context.stroke();
|
||||
}
|
||||
|
||||
function convrgb(values) {
|
||||
return 'rgb(' + values.map(x=>x|0).join(', ') + ')';
|
||||
};
|
||||
|
||||
// apply mild noise on the sensor reading, and clamp between valid values
|
||||
function gaussNoise(sigma=1) {
|
||||
const x0 = 1.0 - Math.random();
|
||||
const x1 = 1.0 - Math.random();
|
||||
return sigma * Math.sqrt(-2 * Math.log(x0)) * Math.cos(2 * Math.PI * x1);
|
||||
};
|