<!--{{{-->
<link rel='alternate' type='application/rss+xml' title='RSS' href='index.xml' />
<!--}}}-->
Background: #fff
Foreground: #000
PrimaryPale: #8cf
PrimaryLight: #18f
PrimaryMid: #04b
PrimaryDark: #014
SecondaryPale: #ffc
SecondaryLight: #fe8
SecondaryMid: #db4
SecondaryDark: #841
TertiaryPale: #eee
TertiaryLight: #ccc
TertiaryMid: #999
TertiaryDark: #666
Error: #f88
/*{{{*/
body {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}

a {color:[[ColorPalette::PrimaryMid]];}
a:hover {background-color:[[ColorPalette::PrimaryMid]]; color:[[ColorPalette::Background]];}
a img {border:0;}

h1,h2,h3,h4,h5,h6 {color:[[ColorPalette::SecondaryDark]]; background:transparent;}
h1 {border-bottom:2px solid [[ColorPalette::TertiaryLight]];}
h2,h3 {border-bottom:1px solid [[ColorPalette::TertiaryLight]];}

.button {color:[[ColorPalette::PrimaryDark]]; border:1px solid [[ColorPalette::Background]];}
.button:hover {color:[[ColorPalette::PrimaryDark]]; background:[[ColorPalette::SecondaryLight]]; border-color:[[ColorPalette::SecondaryMid]];}
.button:active {color:[[ColorPalette::Background]]; background:[[ColorPalette::SecondaryMid]]; border:1px solid [[ColorPalette::SecondaryDark]];}

.header {background:[[ColorPalette::PrimaryMid]];}
.headerShadow {color:[[ColorPalette::Foreground]];}
.headerShadow a {font-weight:normal; color:[[ColorPalette::Foreground]];}
.headerForeground {color:[[ColorPalette::Background]];}
.headerForeground a {font-weight:normal; color:[[ColorPalette::PrimaryPale]];}

.tabSelected{color:[[ColorPalette::PrimaryDark]];
	background:[[ColorPalette::TertiaryPale]];
	border-left:1px solid [[ColorPalette::TertiaryLight]];
	border-top:1px solid [[ColorPalette::TertiaryLight]];
	border-right:1px solid [[ColorPalette::TertiaryLight]];
}
.tabUnselected {color:[[ColorPalette::Background]]; background:[[ColorPalette::TertiaryMid]];}
.tabContents {color:[[ColorPalette::PrimaryDark]]; background:[[ColorPalette::TertiaryPale]]; border:1px solid [[ColorPalette::TertiaryLight]];}
.tabContents .button {border:0;}

#sidebar {}
#sidebarOptions input {border:1px solid [[ColorPalette::PrimaryMid]];}
#sidebarOptions .sliderPanel {background:[[ColorPalette::PrimaryPale]];}
#sidebarOptions .sliderPanel a {border:none;color:[[ColorPalette::PrimaryMid]];}
#sidebarOptions .sliderPanel a:hover {color:[[ColorPalette::Background]]; background:[[ColorPalette::PrimaryMid]];}
#sidebarOptions .sliderPanel a:active {color:[[ColorPalette::PrimaryMid]]; background:[[ColorPalette::Background]];}

.wizard {background:[[ColorPalette::PrimaryPale]]; border:1px solid [[ColorPalette::PrimaryMid]];}
.wizard h1 {color:[[ColorPalette::PrimaryDark]]; border:none;}
.wizard h2 {color:[[ColorPalette::Foreground]]; border:none;}
.wizardStep {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];
	border:1px solid [[ColorPalette::PrimaryMid]];}
.wizardStep.wizardStepDone {background:[[ColorPalette::TertiaryLight]];}
.wizardFooter {background:[[ColorPalette::PrimaryPale]];}
.wizardFooter .status {background:[[ColorPalette::PrimaryDark]]; color:[[ColorPalette::Background]];}
.wizard .button {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::SecondaryLight]]; border: 1px solid;
	border-color:[[ColorPalette::SecondaryPale]] [[ColorPalette::SecondaryDark]] [[ColorPalette::SecondaryDark]] [[ColorPalette::SecondaryPale]];}
.wizard .button:hover {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::Background]];}
.wizard .button:active {color:[[ColorPalette::Background]]; background:[[ColorPalette::Foreground]]; border: 1px solid;
	border-color:[[ColorPalette::PrimaryDark]] [[ColorPalette::PrimaryPale]] [[ColorPalette::PrimaryPale]] [[ColorPalette::PrimaryDark]];}

.wizard .notChanged {background:transparent;}
.wizard .changedLocally {background:#80ff80;}
.wizard .changedServer {background:#8080ff;}
.wizard .changedBoth {background:#ff8080;}
.wizard .notFound {background:#ffff80;}
.wizard .putToServer {background:#ff80ff;}
.wizard .gotFromServer {background:#80ffff;}

#messageArea {border:1px solid [[ColorPalette::SecondaryMid]]; background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]];}
#messageArea .button {color:[[ColorPalette::PrimaryMid]]; background:[[ColorPalette::SecondaryPale]]; border:none;}

.popupTiddler {background:[[ColorPalette::TertiaryPale]]; border:2px solid [[ColorPalette::TertiaryMid]];}

.popup {background:[[ColorPalette::TertiaryPale]]; color:[[ColorPalette::TertiaryDark]]; border-left:1px solid [[ColorPalette::TertiaryMid]]; border-top:1px solid [[ColorPalette::TertiaryMid]]; border-right:2px solid [[ColorPalette::TertiaryDark]]; border-bottom:2px solid [[ColorPalette::TertiaryDark]];}
.popup hr {color:[[ColorPalette::PrimaryDark]]; background:[[ColorPalette::PrimaryDark]]; border-bottom:1px;}
.popup li.disabled {color:[[ColorPalette::TertiaryMid]];}
.popup li a, .popup li a:visited {color:[[ColorPalette::Foreground]]; border: none;}
.popup li a:hover {background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; border: none;}
.popup li a:active {background:[[ColorPalette::SecondaryPale]]; color:[[ColorPalette::Foreground]]; border: none;}
.popupHighlight {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}
.listBreak div {border-bottom:1px solid [[ColorPalette::TertiaryDark]];}

.tiddler .defaultCommand {font-weight:bold;}

.shadow .title {color:[[ColorPalette::TertiaryDark]];}

.title {color:[[ColorPalette::SecondaryDark]];}
.subtitle {color:[[ColorPalette::TertiaryDark]];}

.toolbar {color:[[ColorPalette::PrimaryMid]];}
.toolbar a {color:[[ColorPalette::TertiaryLight]];}
.selected .toolbar a {color:[[ColorPalette::TertiaryMid]];}
.selected .toolbar a:hover {color:[[ColorPalette::Foreground]];}

.tagging, .tagged {border:1px solid [[ColorPalette::TertiaryPale]]; background-color:[[ColorPalette::TertiaryPale]];}
.selected .tagging, .selected .tagged {background-color:[[ColorPalette::TertiaryLight]]; border:1px solid [[ColorPalette::TertiaryMid]];}
.tagging .listTitle, .tagged .listTitle {color:[[ColorPalette::PrimaryDark]];}
.tagging .button, .tagged .button {border:none;}

.footer {color:[[ColorPalette::TertiaryLight]];}
.selected .footer {color:[[ColorPalette::TertiaryMid]];}

.sparkline {background:[[ColorPalette::PrimaryPale]]; border:0;}
.sparktick {background:[[ColorPalette::PrimaryDark]];}

.error, .errorButton {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::Error]];}
.warning {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::SecondaryPale]];}
.lowlight {background:[[ColorPalette::TertiaryLight]];}

.zoomer {background:none; color:[[ColorPalette::TertiaryMid]]; border:3px solid [[ColorPalette::TertiaryMid]];}

.imageLink, #displayArea .imageLink {background:transparent;}

.annotation {background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; border:2px solid [[ColorPalette::SecondaryMid]];}

.viewer .listTitle {list-style-type:none; margin-left:-2em;}
.viewer .button {border:1px solid [[ColorPalette::SecondaryMid]];}
.viewer blockquote {border-left:3px solid [[ColorPalette::TertiaryDark]];}

.viewer table, table.twtable {border:2px solid [[ColorPalette::TertiaryDark]];}
.viewer th, .viewer thead td, .twtable th, .twtable thead td {background:[[ColorPalette::SecondaryMid]]; border:1px solid [[ColorPalette::TertiaryDark]]; color:[[ColorPalette::Background]];}
.viewer td, .viewer tr, .twtable td, .twtable tr {border:1px solid [[ColorPalette::TertiaryDark]];}

.viewer pre {border:1px solid [[ColorPalette::SecondaryLight]]; background:[[ColorPalette::SecondaryPale]];}
.viewer code {color:[[ColorPalette::SecondaryDark]];}
.viewer hr {border:0; border-top:dashed 1px [[ColorPalette::TertiaryDark]]; color:[[ColorPalette::TertiaryDark]];}

.highlight, .marked {background:[[ColorPalette::SecondaryLight]];}

.editor input {border:1px solid [[ColorPalette::PrimaryMid]];}
.editor textarea {border:1px solid [[ColorPalette::PrimaryMid]]; width:100%;}
.editorFooter {color:[[ColorPalette::TertiaryMid]];}
.readOnly {background:[[ColorPalette::TertiaryPale]];}

#backstageArea {background:[[ColorPalette::Foreground]]; color:[[ColorPalette::TertiaryMid]];}
#backstageArea a {background:[[ColorPalette::Foreground]]; color:[[ColorPalette::Background]]; border:none;}
#backstageArea a:hover {background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; }
#backstageArea a.backstageSelTab {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}
#backstageButton a {background:none; color:[[ColorPalette::Background]]; border:none;}
#backstageButton a:hover {background:[[ColorPalette::Foreground]]; color:[[ColorPalette::Background]]; border:none;}
#backstagePanel {background:[[ColorPalette::Background]]; border-color: [[ColorPalette::Background]] [[ColorPalette::TertiaryDark]] [[ColorPalette::TertiaryDark]] [[ColorPalette::TertiaryDark]];}
.backstagePanelFooter .button {border:none; color:[[ColorPalette::Background]];}
.backstagePanelFooter .button:hover {color:[[ColorPalette::Foreground]];}
#backstageCloak {background:[[ColorPalette::Foreground]]; opacity:0.6; filter:'alpha(opacity=60)';}
/*}}}*/
/*{{{*/
* html .tiddler {height:1%;}

body {font-size:.75em; font-family:arial,helvetica; margin:0; padding:0;}

h1,h2,h3,h4,h5,h6 {font-weight:bold; text-decoration:none;}
h1,h2,h3 {padding-bottom:1px; margin-top:1.2em;margin-bottom:0.3em;}
h4,h5,h6 {margin-top:1em;}
h1 {font-size:1.35em;}
h2 {font-size:1.25em;}
h3 {font-size:1.1em;}
h4 {font-size:1em;}
h5 {font-size:.9em;}

hr {height:1px;}

a {text-decoration:none;}

dt {font-weight:bold;}

ol {list-style-type:decimal;}
ol ol {list-style-type:lower-alpha;}
ol ol ol {list-style-type:lower-roman;}
ol ol ol ol {list-style-type:decimal;}
ol ol ol ol ol {list-style-type:lower-alpha;}
ol ol ol ol ol ol {list-style-type:lower-roman;}
ol ol ol ol ol ol ol {list-style-type:decimal;}

.txtOptionInput {width:11em;}

#contentWrapper .chkOptionInput {border:0;}

.externalLink {text-decoration:underline;}

.indent {margin-left:3em;}
.outdent {margin-left:3em; text-indent:-3em;}
code.escaped {white-space:nowrap;}

.tiddlyLinkExisting {font-weight:bold;}
.tiddlyLinkNonExisting {font-style:italic;}

/* the 'a' is required for IE, otherwise it renders the whole tiddler in bold */
a.tiddlyLinkNonExisting.shadow {font-weight:bold;}

#mainMenu .tiddlyLinkExisting,
	#mainMenu .tiddlyLinkNonExisting,
	#sidebarTabs .tiddlyLinkNonExisting {font-weight:normal; font-style:normal;}
#sidebarTabs .tiddlyLinkExisting {font-weight:bold; font-style:normal;}

.header {position:relative;}
.header a:hover {background:transparent;}
.headerShadow {position:relative; padding:4.5em 0 1em 1em; left:-1px; top:-1px;}
.headerForeground {position:absolute; padding:4.5em 0 1em 1em; left:0px; top:0px;}

.siteTitle {font-size:3em;}
.siteSubtitle {font-size:1.2em;}

#mainMenu {position:absolute; left:0; width:10em; text-align:right; line-height:1.6em; padding:1.5em 0.5em 0.5em 0.5em; font-size:1.1em;}

#sidebar {position:absolute; right:3px; width:16em; font-size:.9em;}
#sidebarOptions {padding-top:0.3em;}
#sidebarOptions a {margin:0 0.2em; padding:0.2em 0.3em; display:block;}
#sidebarOptions input {margin:0.4em 0.5em;}
#sidebarOptions .sliderPanel {margin-left:1em; padding:0.5em; font-size:.85em;}
#sidebarOptions .sliderPanel a {font-weight:bold; display:inline; padding:0;}
#sidebarOptions .sliderPanel input {margin:0 0 0.3em 0;}
#sidebarTabs .tabContents {width:15em; overflow:hidden;}

.wizard {padding:0.1em 1em 0 2em;}
.wizard h1 {font-size:2em; font-weight:bold; background:none; padding:0; margin:0.4em 0 0.2em;}
.wizard h2 {font-size:1.2em; font-weight:bold; background:none; padding:0; margin:0.4em 0 0.2em;}
.wizardStep {padding:1em 1em 1em 1em;}
.wizard .button {margin:0.5em 0 0; font-size:1.2em;}
.wizardFooter {padding:0.8em 0.4em 0.8em 0;}
.wizardFooter .status {padding:0 0.4em; margin-left:1em;}
.wizard .button {padding:0.1em 0.2em;}

#messageArea {position:fixed; top:2em; right:0; margin:0.5em; padding:0.5em; z-index:2000; _position:absolute;}
.messageToolbar {display:block; text-align:right; padding:0.2em;}
#messageArea a {text-decoration:underline;}

.tiddlerPopupButton {padding:0.2em;}
.popupTiddler {position: absolute; z-index:300; padding:1em; margin:0;}

.popup {position:absolute; z-index:300; font-size:.9em; padding:0; list-style:none; margin:0;}
.popup .popupMessage {padding:0.4em;}
.popup hr {display:block; height:1px; width:auto; padding:0; margin:0.2em 0;}
.popup li.disabled {padding:0.4em;}
.popup li a {display:block; padding:0.4em; font-weight:normal; cursor:pointer;}
.listBreak {font-size:1px; line-height:1px;}
.listBreak div {margin:2px 0;}

.tabset {padding:1em 0 0 0.5em;}
.tab {margin:0 0 0 0.25em; padding:2px;}
.tabContents {padding:0.5em;}
.tabContents ul, .tabContents ol {margin:0; padding:0;}
.txtMainTab .tabContents li {list-style:none;}
.tabContents li.listLink { margin-left:.75em;}

#contentWrapper {display:block;}
#splashScreen {display:none;}

#displayArea {margin:1em 17em 0 14em;}

.toolbar {text-align:right; font-size:.9em;}

.tiddler {padding:1em 1em 0;}

.missing .viewer,.missing .title {font-style:italic;}

.title {font-size:1.6em; font-weight:bold;}

.missing .subtitle {display:none;}
.subtitle {font-size:1.1em;}

.tiddler .button {padding:0.2em 0.4em;}

.tagging {margin:0.5em 0.5em 0.5em 0; float:left; display:none;}
.isTag .tagging {display:block;}
.tagged {margin:0.5em; float:right;}
.tagging, .tagged {font-size:0.9em; padding:0.25em;}
.tagging ul, .tagged ul {list-style:none; margin:0.25em; padding:0;}
.tagClear {clear:both;}

.footer {font-size:.9em;}
.footer li {display:inline;}

.annotation {padding:0.5em; margin:0.5em;}

* html .viewer pre {width:99%; padding:0 0 1em 0;}
.viewer {line-height:1.4em; padding-top:0.5em;}
.viewer .button {margin:0 0.25em; padding:0 0.25em;}
.viewer blockquote {line-height:1.5em; padding-left:0.8em;margin-left:2.5em;}
.viewer ul, .viewer ol {margin-left:0.5em; padding-left:1.5em;}

.viewer table, table.twtable {border-collapse:collapse; margin:0.8em 1.0em;}
.viewer th, .viewer td, .viewer tr,.viewer caption,.twtable th, .twtable td, .twtable tr,.twtable caption {padding:3px;}
table.listView {font-size:0.85em; margin:0.8em 1.0em;}
table.listView th, table.listView td, table.listView tr {padding:0px 3px 0px 3px;}

.viewer pre {padding:0.5em; margin-left:0.5em; font-size:1.2em; line-height:1.4em; overflow:auto;}
.viewer code {font-size:1.2em; line-height:1.4em;}

.editor {font-size:1.1em;}
.editor input, .editor textarea {display:block; width:100%; font:inherit;}
.editorFooter {padding:0.25em 0; font-size:.9em;}
.editorFooter .button {padding-top:0px; padding-bottom:0px;}

.fieldsetFix {border:0; padding:0; margin:1px 0px;}

.sparkline {line-height:1em;}
.sparktick {outline:0;}

.zoomer {font-size:1.1em; position:absolute; overflow:hidden;}
.zoomer div {padding:1em;}

* html #backstage {width:99%;}
* html #backstageArea {width:99%;}
#backstageArea {display:none; position:relative; overflow: hidden; z-index:150; padding:0.3em 0.5em;}
#backstageToolbar {position:relative;}
#backstageArea a {font-weight:bold; margin-left:0.5em; padding:0.3em 0.5em;}
#backstageButton {display:none; position:absolute; z-index:175; top:0; right:0;}
#backstageButton a {padding:0.1em 0.4em; margin:0.1em;}
#backstage {position:relative; width:100%; z-index:50;}
#backstagePanel {display:none; z-index:100; position:absolute; width:90%; margin-left:3em; padding:1em;}
.backstagePanelFooter {padding-top:0.2em; float:right;}
.backstagePanelFooter a {padding:0.2em 0.4em;}
#backstageCloak {display:none; z-index:20; position:absolute; width:100%; height:100px;}

.whenBackstage {display:none;}
.backstageVisible .whenBackstage {display:block;}
/*}}}*/
/***
StyleSheet for use when a translation requires any css style changes.
This StyleSheet can be used directly by languages such as Chinese, Japanese and Korean which need larger font sizes.
***/
/*{{{*/
body {font-size:0.8em;}
#sidebarOptions {font-size:1.05em;}
#sidebarOptions a {font-style:normal;}
#sidebarOptions .sliderPanel {font-size:0.95em;}
.subtitle {font-size:0.8em;}
.viewer table.listView {font-size:0.95em;}
/*}}}*/
/*{{{*/
@media print {
#mainMenu, #sidebar, #messageArea, .toolbar, #backstageButton, #backstageArea {display: none !important;}
#displayArea {margin: 1em 1em 0em;}
noscript {display:none;} /* Fixes a feature in Firefox 1.5.0.2 where print preview displays the noscript content */
}
/*}}}*/
<!--{{{-->
<div class='header' macro='gradient vert [[ColorPalette::PrimaryLight]] [[ColorPalette::PrimaryMid]]'>
<div class='headerShadow'>
<span class='siteTitle' refresh='content' tiddler='SiteTitle'></span>
<span class='siteSubtitle' refresh='content' tiddler='SiteSubtitle'></span>
</div>
<div class='headerForeground'>
<span class='siteTitle' refresh='content' tiddler='SiteTitle'></span>
<span class='siteSubtitle' refresh='content' tiddler='SiteSubtitle'></span>
</div>
</div>
<div id='mainMenu' refresh='content' tiddler='MainMenu'></div>
<div id='sidebar'>
<div id='sidebarOptions' refresh='content' tiddler='SideBarOptions'></div>
<div id='sidebarTabs' refresh='content' force='true' tiddler='SideBarTabs'></div>
</div>
<div id='displayArea'>
<div id='messageArea'></div>
<div id='tiddlerDisplay'></div>
</div>
<!--}}}-->
<!--{{{-->
<div class='toolbar' macro='toolbar [[ToolbarCommands::ViewToolbar]]'></div>
<div class='title' macro='view title'></div>
<div class='subtitle'><span macro='view modifier link'></span>, <span macro='view modified date'></span> (<span macro='message views.wikified.createdPrompt'></span> <span macro='view created date'></span>)</div>
<div class='tagging' macro='tagging'></div>
<div class='tagged' macro='tags'></div>
<div class='viewer' macro='view text wikified'></div>
<div class='tagClear'></div>
<!--}}}-->
<!--{{{-->
<div class='toolbar' macro='toolbar [[ToolbarCommands::EditToolbar]]'></div>
<div class='title' macro='view title'></div>
<div class='editor' macro='edit title'></div>
<div macro='annotations'></div>
<div class='editor' macro='edit text'></div>
<div class='editor' macro='edit tags'></div><div class='editorFooter'><span macro='message views.editor.tagPrompt'></span><span macro='tagChooser excludeLists'></span></div>
<!--}}}-->
To get started with this blank [[TiddlyWiki]], you'll need to modify the following tiddlers:
* [[SiteTitle]] & [[SiteSubtitle]]: The title and subtitle of the site, as shown above (after saving, they will also appear in the browser title bar)
* [[MainMenu]]: The menu (usually on the left)
* [[DefaultTiddlers]]: Contains the names of the tiddlers that you want to appear when the TiddlyWiki is opened
You'll also need to enter your username for signing your edits: <<option txtUserName>>
These [[InterfaceOptions]] for customising [[TiddlyWiki]] are saved in your browser

Your username for signing your edits. Write it as a [[WikiWord]] (eg [[JoeBloggs]])

<<option txtUserName>>
<<option chkSaveBackups>> [[SaveBackups]]
<<option chkAutoSave>> [[AutoSave]]
<<option chkRegExpSearch>> [[RegExpSearch]]
<<option chkCaseSensitiveSearch>> [[CaseSensitiveSearch]]
<<option chkAnimate>> [[EnableAnimations]]

----
Also see [[AdvancedOptions]]
<<importTiddlers>>
//{{{
[
	{
		"pluginName": "$tw.VPM",
		"pluginScripts": [
			{
				"main": "nodejs/$tw.util.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.util.min",
				"description": "Plugin for general utilities.",
				"required": false,
				"toLoad": false,
				"developing": true,
				"deActivated": false
			},
			{
				"main": "$tw.ve.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.ve.min",
				"description": "Plugin for view mode editing features in TiddlyWiki.",
				"required": false,
				"developing": true,
				"toLoad": true
			},
			{
				"main": "$tw.LaTex.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.LaTex.min",
				"description": "Plugin for LaTex formula handling.<br>MUST have a real LaTex rendering engine, such as MathJax or KaTex, to work properly.",
				"required": false,
				"developing": true,
				"toLoad": true
			},
			{
				"main": "$tw.3D.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.3D.min",
				"description": "Plugin for 3D graphing in TiddlyWiki. (Default: THREE.js.)",
				"required": false,
				"developing": true,
				"toLoad": true
			},
			{
				"main": "$tw.data.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.data.min",
				"description": "Plugin for data processing in TiddlyWiki. (Default: D3.js.)",
				"required": false,
				"developing": true,
				"toLoad": true
			},
			{
				"main": "$tw.numeric.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.numeric.min",
				"description": "Plugin for numeric calculatons.",
				"required": false,
				"toLoad": true,
				"developing": true,
				"deActivated": false
			},
			{
				"main": "$tw.physics.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.physics.min",
				"description": "Plugin for physics simulation.",
				"required": false,
				"toLoad": true,
				"developing": true,
				"deActivated": false
			},
			{
				"name": "Parallel.js",
				"main": "js/Parallel-transferable.js",
				"fallback": "https://unpkg.com/paralleljs@1.0/lib/parallel.js",
				"website": "https://parallel.js.org/",
				"description": "The Parallel.js main source.",
				"developing": true,
				"required": false,
				"toLoad": true
			}
		],
		"debugging": false
	},
	{
		"pluginName": "$tw.ve",
		"pluginScripts": [
			{
				"main": "$tw.ve.core.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.ve.core.min",
				"description": "The core of $tw.ve.",
				"developing": true,
				"required": true,
				"toLoad": true
			},
			{
				"main": "$tw.ve.extra.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.ve.extra.min",
				"description": "The extended features of $tw.ve.",
				"developing": true,
				"required": false,
				"toLoad": true
			},
			{
				"main": "$tw.ve.table.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.ve.table.min",
				"description": "View mode editing for tables in $tw.ve.",
				"developing": true,
				"required": false,
				"toLoad": true
			},
			{
				"main": "$tw.ve.tcalc.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.ve.tcalc.min",
				"description": "Spreadsheet features in $tw.ve.",
				"developing": true,
				"required": false,
				"toLoad": true
			}
		],
		"debugging": false
	},
	{
		"pluginName": "$tw.LaTex",
		"pluginScripts": [
			{
				"name": "MathJax",
				"main": "$tw.LaTex.MathJax.js",
				"fallback": "https://twve.tiddlyspot.com/#$tw.LaTex.MathJax.min",
				"description": "Use MathJax as the rendering engine.",
				"developing": true,
				"required": false,
				"toLoad": true
			},
			{
				"name": "KaTex",
				"main": "$tw.LaTex.KaTex.js",
				"fallback": "https://twve.tiddlyspot.com/#$tw.LaTex.KaTex.min",
				"description": "Use KaTex as the rendering engine.",
				"developing": true,
				"required": false,
				"toLoad": false
			}
		],
		"debugging": false
	},
	{
		"pluginName": "$tw.3D",
		"pluginScripts": [
			{
				"name": "THREE.js",
				"main": "<script type='module'>import * as THREE from 'three';$tw.threeD.THREE=THREE;</script>",
				"description": "The THREE.js main source.",
				"required": true,
				"toLoad": true
			},
			{
				"main": "<script type='module'>import {CSS3DRenderer, CSS3DObject} from 'three/addons/renderers/CSS3DRenderer.js';$tw.threeD.CSS3DRenderer=CSS3DRenderer;$tw.threeD.CSS3DObject=CSS3DObject;</script>",
				"description": "The CSS 3D renderer.",
				"required": false,
				"toLoad": true
			},
			{
				"main": "<script type='module'>import {TrackballControls} from 'three/addons/controls/TrackballControls.js';$tw.threeD.TrackballControls = TrackballControls;</script>",
				"description": "The track ball control handler.",
				"required": false,
				"toLoad": true
			},
			{
				"main": "js/controls/FlyControls.min.js",
				"description": "The fly control handler.",
				"required": false,
				"toLoad": false
			},
			{
				"main": "js/controls/MouseControls.min.js",
				"description": "The mouse control handler.",
				"required": false,
				"toLoad": false
			},
			{
				"main": "js/controls/DragControls.min.js",
				"description": "The drag-and-drop handler.",
				"required": false,
				"toLoad": false
			},
			{
				"main": "js/controls/VRControls.min.js",
				"description": "The VR control handler.",
				"required": false,
				"toLoad": false
			},
			{
				"main": "<script type='module'>import {CSS3DRenderer} from 'https://unpkg.com/three@0.161.0/examples/jsm//renderers/CSS3DRenderer.js';$tw.threeD.CSS3DRenderer = CSS3DRenderer;</script>",
				"description": "The CSS 3D renderer.",
				"required": false,
				"toLoad": false
			},
			{
				"main": "js/controls/TrackballControls.min.js",
				"description": "The track ball control handler.",
				"required": false,
				"toLoad": false
			}
		],
		"debugging": false
	},
	{
		"pluginName": "$tw.data",
		"pluginScripts": [
			{
				"name": "plotly.js",
				"main": "https://cdn.plot.ly/plotly-2.18.0.min.js",
				"fallback": "plotly.min.js",
				"description": "The plotly.js library.",
				"required": false,
				"toLoad": false
			},
			{
				"name": "Apache ECharts",
				"main": "https://cdn.jsdelivr.net/npm/echarts@5.4.1/dist/echarts.min.js",
				"fallback": "echarts.min.js",
				"description": "The Apache ECharts library.",
				"required": false,
				"toLoad": false
			},
			{
				"name": "D3.js",
				"main": "https://d3js.org/d3.v7.min.js",
				"fallback": "d3.min.js",
				"description": "The D3 main source.",
				"required": true,
				"toLoad": true
			},
			{
				"main": "$tw.data.TypedArray.js",
				"fallback": "https://twve.tiddlyspot.com/#$tw.data.TypedArray.min",
				"description": "The typed array module of $tw.data.",
				"developing": true,
				"required": false,
				"toLoad": false
			}
		],
		"debugging": false
	},
	{
		"pluginName": "$tw.numeric",
		"pluginScripts": [
			{
				"main": "$tw.numeric.ODE.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.numeric.ODE.min",
				"description": "The ODE solver of $tw.numeric.",
				"developing": true,
				"required": false,
				"toLoad": false,
				"deActivated": true
			},
			{
				"main": "$tw.numeric.FFT.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.numeric.FFT.min",
				"description": "The Fast Fourier Transform (FFT) module of $tw.numeric.",
				"developing": true,
				"required": false,
				"toLoad": false,
				"deActivated": true
			},
			{
				"main": "$tw.numeric.parallel.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.numeric.parallel.min",
				"description": "The parallel library of $tw.numeric.",
				"developing": true,
				"required": false,
				"toLoad": false,
				"deActivated": true
			}
		],
		"debugging": false
	},
	{
		"pluginName": "$tw.physics",
		"pluginScripts": [
			{
				"main": "$tw.physics.electrodynamics.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.physics.electrodynamics.min",
				"description": "The physics simulation codes for electrodynamics.",
				"developing": true,
				"required": false,
				"toLoad": false
			},
			{
				"main": "$tw.physics.quantum.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.physics.quantum.min",
				"description": "The physics simulation codes for quantum.",
				"developing": true,
				"required": false,
				"deActivated": true,
				"toLoad": false
			},
			{
				"main": "$tw.physics.statisticaldynamics.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.physics.statisticaldynamics.min",
				"description": "The physics simulation codes for statistical dynamics.",
				"developing": true,
				"required": false,
				"deActivated": true,
				"toLoad": false
			},
			{
				"main": "$tw.physics.thermodynamics.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.physics.thermodynamics.min",
				"description": "The physics simulation codes for thermodynamics.",
				"developing": true,
				"required": false,
				"deActivated": true,
				"toLoad": false
			}
		],
		"debugging": false
	}
]
//}}}
//{{{
[
	{
		"pluginName": "$tw.ve.table",
		"pluginScripts": [
			{
				"main": "$tw.ve.tablelarge.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.ve.tablelarge.min",
				"description": "Large table support in $tw.ve.",
				"required": false,
				"toLoad": false
			},
			{
				"name": "TableSortingPlugin",
				"main": "TableSortingPlugin.min.js",
				"fallback": "",
				"description": "Add table sorting feature in $tw.ve.",
				"required": false,
				"toLoad": false
			},
			{
				"name": "SortableGridPlugin",
				"main": "SortableGridPlugin.min.js",
				"fallback": "",
				"description": "Add table sorting feature in $tw.ve.",
				"required": false,
				"toLoad": false
			}
		],
		"debugging": false
	},
	{
		"pluginName": "$tw.ve.tcalc",
		"pluginScripts": [
			{
				"name": "Date/Time Extension",
				"main": "$tw.ve.tcalc.datetime.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.ve.tcalc.datetime.min",
				"description": "Date/Time functions extension of $tw.ve.tcalc.",
				"developing": true,
				"required": false,
				"toLoad": true
			},
			{
				"name": "Statistics Extension",
				"main": "$tw.ve.tcalc.stats.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.ve.tcalc.stats.min",
				"description": "Statistics functions extension of $tw.ve.tcalc.",
				"developing": true,
				"required": false,
				"toLoad": true
			},
			{
				"name": "Financial Extension",
				"main": "$tw.ve.tcalc.fin.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.ve.tcalc.fin.min",
				"description": "Financial functions extension of $tw.ve.tcalc.",
				"developing": true,
				"required": false,
				"toLoad": false
			},
			{
				"name": "Bookkeeping Extension",
				"main": "$tw.ve.tcalc.bkkp.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.ve.tcalc.bkkp.min",
				"description": "Daily life bookkeeping functions extension of $tw.ve.tcalc.",
				"developing": true,
				"required": false,
				"toLoad": false
			}
		],
		"debugging": false
	},
	{
		"pluginName": "$tw.LaTex.MathJax",
		"pluginScripts": [
			{
				"name": "MathJax-3-CHTML",
				"id": "MathJax-script",
				"main": "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml-full.js",
				"type": "text/javascript",
				"description": "The MathJax rendering engine.",
				"default": true,
				"required": false,
				"toLoad": false
			},
			{
				"name": "MathJax-2",
				"main": "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-MML-AM_CHTML",
				"type": "text/javascript",
				"description": "The MathJax rendering engine.",
				"required": false,
				"toLoad": true
			}
		],
		"debugging": false
	}
]
//}}}
/***
[img(75%,)[https://upload.wikimedia.org/wikipedia/commons/thumb/e/e7/Drag_coefficient_on_a_sphere_vs._Reynolds_number_-_main_trends.svg/655px-Drag_coefficient_on_a_sphere_vs._Reynolds_number_-_main_trends.svg.png]]
***/
//{{{
'''
---------------------------------------------------------------------------------------------------------
開始:空氣阻力的計算函數
---------------------------------------------------------------------------------------------------------
'''

'''
-------------------------------------------------------------------------------
計算一個移動球體的空氣阻力。
Calculate air drag of a moving SPHERE.
如果物體不是球體,可以將球體的結果乘上一個和形狀有關的係數,這個係數可以從簡單
的模型估計,或者由實驗數據擬和出來。
For non-spherical objects, one simple way is to multiply the result by a
geometrical factor that can be obtained by simple modeling or fitting to
experimental data.
參考 Ref: https://en.wikipedia.org/wiki/Drag_(physics)
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
	用法 Usage:
	f_dra = airDragSphere(v, r)
	        v: 球體的速度向量 velocity vector of the spherical object,
	        r: 球體的半徑 radius of the spherical object.

	傳回值為此球體所受的空氣阻力(向量)
	The return value is the air drag force of that spherical object in vector form.
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
'''

eta_air = 1.81e-5   # at 15C, https://en.wikipedia.org/wiki/Viscosity
rho_air = 1.225     # at 15C sea level, https://en.wikipedia.org/wiki/Density_of_air

def ReynoldsNumber(speed,L):
    # 計算直徑為 L 的球狀物體在空氣中速率為 speed 時候的雷諾數
    # Calculates Reynolds number of a sphere with speed v and characteristic length L.
    return speed*L/(eta_air/rho_air);

def dragCoeff(Re):
    '''
    計算對應於雷諾數為 Re 的拖曳係數。拖曳係數是從下圖中估計出來:
    https://en.wikipedia.org/wiki/Drag_(physics)#/media/File:Drag_coefficient_on_a_sphere_vs._Reynolds_number_-_main_trends.svg
    雷諾數 Re 需要事先計算出來
    這個函數應該只在 Re > 20 的時候使用,因為上圖的拖曳係數只有在 Re > 20 的部分有值
    
    Calculates drag coefficient C_d according to the Reynolds number.
    The drag coefficient is determined according to the following figure:
    https://en.wikipedia.org/wiki/Drag_(physics)#/media/File:Drag_coefficient_on_a_sphere_vs._Reynolds_number_-_main_trends.svg
    The Reynolds number Re shall be calculated beforehand.
    This function shall be called only for Re > 20, because in the above figure
    there is no information about Cd for Re < 20.
    '''
    
    '''
    As of 2018/05/09, GlowScript 2.7 does not support log10(x), we modified
    the corresponding codes below to log(x)/log(10).        Vincent Yeh
    '''
    
    # 我們將圖中曲線簡化成幾段直線
    # We simplify the curve by viewing it as several linear segments
    if Re < 1e3:
        # Cd linear between 2.0 and 0.5
        # return 2.0+log10(0.5/2.0)/log10(1e3/20)*log10(Re/50)
        return 2.0+(log(0.25)/log(10))/(log(50)/log(10))*(log(Re/50)/log(10))
    elif Re <= 1.5e5:
        # Cd roughly constant
        return 0.5
    elif Re <= 3e5:
        # Cd linear between 0.5 and 0.08
        #return 0.5+log10(0.08/0.5)/log10(3e5/1.5e5)*log10(Re/1.5e5)
        return 0.5+(log(0.16)/log(10))/(log(2)/log(10))*(log(Re/1.5e5)/log(10))
    elif Re < 1.5e6:
        # Cd linear between 0.08 and 0.2
        # return 0.08+log10(0.2/0.08)/log10(1.5e6/3e5)*log10(Re/3e5)
        return 0.08+(log(2.5)/log(10))/(log(5)/log(10))*(log(Re/3e5)/log(10))
    else:
        # Cd roughly constant
        return 0.2

def airDragSphere(v,r):
    '''
    計算一個半徑為 r 速度為 v(向量)的光滑球體之空氣阻力
    Calculate air drag for a spherical object of radius r with velocity v (vector).
    '''
    
    # 首先要計算雷諾數
    # First we calculate the Reynolds number
    Re = ReynoldsNumber(v.mag,r*2)
    
    if Re <= 20:
        # 很低的雷諾數,傳回線性(一次方)阻力
        # very low Reynolds number, return the linear drag
        return -6.0*pi*eta_air*r*v;
    else:
        # 夠高的雷諾數,傳回二次方阻力
        # otherwise return the quadratic drag
        return -0.5*rho_air*v.mag2*dragCoeff(Re)*pi*r*r*v.norm()

'''
-------------------------------------------------------------------------------
結束:空氣阻力的計算函數
-------------------------------------------------------------------------------
'''
//}}}
[[新手開始]]

<!--{{{-->
<link rel="stylesheet" type="text/css" href="tw.qmo.css">
<script type="importmap">
	{
		"imports": {
			"three": "https://unpkg.com/three@0.161.0/build/three.module.js",
			"three/addons/": "https://unpkg.com/three@0.161.0/examples/jsm/"
		}
	}
</script>
<script type="text/javascript" src="$tw.plugins.min.js"></script>
<script type="text/javascript" src="$tw.VPM.js"></script>
<!--}}}-->

/***
!! Definition Section
Definition for the Rod, Bob, etc.
!!! The Rod
***/
//{{{
var Rod = {
	create : function(param){
		var rod = helix(param);
		rod.init = function(param){
			rod.mass = param ? (param.mass||param.M||0) : 0;
			rod.rcm = param ? (param.cm||param.CM||vector(0,-0.5,0)) : vector(0,-0.5,0);
			/*
			var preSetLength = rod.setLength;
			rod.setLength = function(L){
				var newrod = preSetLength.call(rod,L);
				Rod.prototype.init.call(newrod,rod.getParameters());
				newrod.mass = rod.mass;
				newrod.tension.copy(rod.tension);
				newrod.rcm.copy(rod.rcm).normalize().multiplyScalar(L/2);
				newrod.position.copy(rod.position);
				return newrod;
			};
			*/
			// 擺繩的張力 The tension in the string
			rod.tension = vector();
			return rod;
		};
		return rod.init(param);
	}
};
//}}}
/***
!!! The Bob
***/
//{{{
var Bob = {
	create : function(param){
		var bob = sphere(param);
		bob.init = function(param){
			bob.mass = param ? (param.mass||param.M||0.5) : 0.5;
			return bob;
		};
		return bob.init(param);
	}
};
//}}}
/***
!!! The Pendulum
***/
//{{{
var Pendulum = {
	create : function(param){
		var pendulum = group();
		scene.add(pendulum);
		pendulum.init = function(param){
			if(param){
				var vec = param.v || param.velocity;
				if(vec)
					pendulum.bob.velocity.copy(vec);
				else
					pendulum.bob.velocity.set(0,0,0);

				vec = param.r || param.pos || param.position;
				pendulum.pivot.position.copy(vec || pendulum.position);
				if(param.dir){
					pendulum.rod.setDirection(param.dir);
					if(param.L || param.length){
						pendulum.rod.setLength(param.L || param.length);
					}
				}else if(param.axis){
					pendulum.rod.setAxis(param.axis);
				}
				/*
				if(param.initialAngle){
					if(typeof param.initialAngle === 'number'){
						pendulum.initialAngle = vector(0,0,1);
						pendulum.initialAngle.value = param.initialAngle;
					}else{
						pendulum.initialAngle = param.initialAngle.clone();
						pendulum.initialAngle.value = pendulum.initialAngle.length();
						pendulum.initialAngle.normalize();
					}
				}
				*/
			}else{
				pendulum.bob.velocity.set(0,0,0);
				pendulum.pivot.position.copy(pendulum.position);
				pendulum.rod.setDirection(vector(0,-1,0));
				//pendulum.initialAngle = vector(0,0,1);
				//pendulum.initialAngle.value = 0;
			}
			pendulum.calculateLinearStatus();
			pendulum.lastT = simT;
			return pendulum;
		};

		pendulum.pivot = sphere({		// create a sphere to represent the pivot
			radius:0.01,
			opacity:0.2
		});

		pendulum.rod = param.rod || Rod.create({
			radius: 0.03,
			axis: vector(0,0,1),
			color: 0xff0000
		},'noadd');
		pendulum.add(pendulum.rod);

		pendulum.bob = param.bob || Bob.create({
			radius: 0.05,
			color: 0x00ff00,
			opacity: 0.3
		},'noadd');
		pendulum.add(pendulum.bob);

		pendulum.destroy = function(){
			scene.remove(pendulum);
			scene.remove(pendulum.spherecm);
			scene.remove(pendulum.pivot);
			scene.remove(pendulum.vertical);
		};

		pendulum.velocity = vector();
		pendulum.acceleration = vector();

		pendulum.setLength = function(L){
			pendulum.rod = pendulum.rod.setLength(L);
			pendulum.length = pendulum.rod.getLength();
			pendulum.setAngle();
			pendulum.setPosition();
		};
		pendulum.isSimple = function(){
			return pendulum.rod.mass === 0;
		};
//}}}
/***
!!!! pendulum.theoreticalPeriod ()
>計算此擺的理論週期,使用下列公式:
>Calculate the theoretical period of this pendulum, using the following formula:
>\[\begin{eqnarray*}T &=&& 2\pi \sqrt{L \over g}\left(1 + {1 \over 16}\theta_0^2 + {11 \over 3072}\theta_0^4 + \cdots\right) & \quad \text{單擺 Simple pendulum} \\ &\text{or}&& 2\pi \sqrt{I \over mL g}\left(1 + {1 \over 16}\theta_0^2 + {11 \over 3072}\theta_0^4 + \cdots\right) & \quad \text{實體擺(物理擺)Physical pendulum.}\end{eqnarray*}\] 參考文獻:
>Ref: [[Wikipedia Pendulum|https://en.wikipedia.org/wiki/Pendulum_(mathematics)]].
***/
//{{{
		pendulum.theoreticalPeriod = function(){
			var anglesquared = Math.pow(pendulum.initialAngle.value,2);
			var r0 = pendulum.pivotPosition();
			var r = pendulum.rcm.clone().sub(r0);
			return 2*Math.PI
				*Math.sqrt(
					(pendulum.isSimple()
						?	r.length()
						:	pendulum.I(
								r0,pendulum.axisOfRotation()
							)/pendulum.mass/r.length())
					/(-scene.g.y)
				)*(
					  1
					+ anglesquared/16
					+ Math.pow(anglesquared,2)*11/3072
				);
		};
//}}}
/***
!!!! pendulum.setPosition (\(\vec r_h, \vec r_{CM}\))
> 設定此擺的位置,視情況計算擺的角度或是質心位置。兩個引數都是非必要的,第一個引數 \(\vec r_h\) 為//懸掛//位置,如果有給,此函數便將擺的懸掛位置設定到給定的地方,如果沒有,則維持不變。第二個引數 \(\vec r_{CM}\) 為擺的質心位置,如果有,表示擺的質心位置已知,則擺的角度要據此計算出來。如果沒有,表示擺的角度已知,則擺的質心位置要根據角度計算出來。
> Sets the position of this pendulum, and calculates either its angle or center of mass position. Both arguments are optional. The first one \(\vec r_h\) is the //hanging// positio. If given, the hanging position will be set to the specified place. If not given, it remains unchanged. The second argument \(\vec r_{CM}\) is the center of mass position. If given, meaning the center of position is already known, then the angle of this pendulum shall be calculated accordingly. If not, meaning the angle of this pendulum is known, then the center of mass position shall be calculated accordingly.
***/
//{{{
		pendulum.setPosition = function(rh,cm){
			if(rh){
				pendulum.position.copy(rh);
			}
			if(cm){
				// 擺的質心位置已知,將擺旋轉到該有的方向
				// Pendulum's center of mass is known, rotate the rod to the desired orientation.
				pendulum.rod.setDirection(
					pendulum.rod.rcm.copy(cm)
						.sub(pendulum.position).normalize()
				);
				pendulum.rod.rcm.multiplyScalar(pendulum.length/2);
			}
			// 把擺繩放在正確位置 Put the rod at the right position.
			pendulum.rod.position.copy(pendulum.rod.rcm);
			// 把球放在擺繩的下端 Attach the bob to the bottom end of the rod.
			pendulum.bob.position.copy(pendulum.rod.position).add(pendulum.rod.rcm);
			if(cm){
				// 擺的質心位置已知,計算對應的角度及系統的質心
				// Pendulum's center of mass is known, calculate the corresponding
				// angular position and system's center of mass.
				pendulum.spherecm.position.copy(pendulum.rcm);
				pendulum.spherecm.visible = chkCM.checked;
				pendulum.calculateAngle();
 			}else{
				// 擺的質心位置未知,把它計算出來。
				// Pendulum's center of mass is unknown, calculate it.
				pendulum.calculateCM();
				//calculateSystemCM();
			}
			return pendulum;
		};
//}}}
/***
!!!! pendulum.shiftPosition (\(d\vec r\) )
> 移動此擺的懸掛位置到 \(\vec r_h + d\vec r\),其中 \(\vec r_h\) 為現在的懸掛位置。
> Shift the hanging position of this pendulum to \(\vec r_h + d\vec r\), where \(\vec r_h\) is its current hanging position.
***/
//{{{
		pendulum.shiftPosition = function(dr){
			if(dr) pendulum.position.add(dr);
			return pendulum.setPosition(null,pendulum.rcm);
		};
//}}}
/***
!!!! pendulum.calculateTension ()
> 計算擺繩張力 \(\vec T\),根據【沿著擺繩方向(\(\hat r\))的合力做為擺的向心力】這個想法來進行:
> Calculate the tension \(\vec T\) in the rod, following the idea that //"the net force along the rod direction (\(\hat r\)) serves as the centripetal force for the pendulum"//:\[\begin{eqnarray*} & \vec T &+& (m\vec g \cdot \hat r)\hat r = m \vec a_c = m |\vec r_{CM}-\vec r_0| \omega^2 (-\hat r) \\ \to \quad & \boxed{\vec T} &=& -(m\vec g \cdot \hat r)\hat r - m_|\vec r_{CM}-\vec r_0|\omega^2 \hat r \\ \to \quad &&=& \boxed{-m(\vec g \cdot \hat r + |\vec r_{CM} - \vec r_0|\omega^2)\hat r},\end{eqnarray*}\] 其中 \(m\) 是擺的質量,\(\hat r\) 是沿著擺繩,從懸掛點指向擺錘方向的單位向量,\(\vec a_c\) 為擺的向心加速度,\(\vec r_{CM}\) 是擺的質心位置,\(\vec r_0\) 為轉軸通過的位置,可能為懸掛點(如果天花板為固定),或者是系統整體的質心(如果天花板可移動);而 \(\vec \omega\) 則為擺的角速度。
>where \(m\) is the mass of the bob, \(\hat r\) is the unit vector along the rod, pointing from the hanging point towards the bob, \(\vec a_c\) is the centripetal acceleration of the bob, \(\vec r_{CM}\) is the center of mass position of the pendulum, \(\vec r_0\) is the pivot position, which could be the hanging point (if the ceiling is fixed), or the system's center of mass position (if the ceiling is movable); while \(\vec \omega\) is the angular velocity of the pendulum.
>
>//注意:\(\vec T\) 的方向為向上,而 \(\vec g\) 與 \(\hat r\) 是向下。//
>//Note: The direction of \(\vec T\) is upward while that of \(\vec g\) and \(\hat r\) are downward.//
***/
//{{{
		pendulum.calculateTension = function(){
			var dr = pendulum.rcm.clone().sub(pendulum.pivotPosition());
			var ur = dr.clone().normalize();

			pendulum.rod.tension.copy(ur).multiplyScalar(
				- fg.dot(ur)
				- pendulum.mass*dr.length()*pendulum.omega.lengthSq()
			);
			return pendulum;
		};
//}}}
/***
!!!! pendulum.calculateAcceleration (\(\vec r, \vec v\))
<<<
根據現在位置 \(\vec r\)(從圓周運動的中心量起)與速度 \(\vec v\) 計算現在所受的加速度,
Calculate the acceleration of this pendulum according to its current position \(\vec r\) (measured from the center of the circular motion) and velocity \(\vec v\), \[\vec a = \vec a_c + \vec a_t = {v^2 \over r}(-\hat r) + \vec\alpha \times \vec r,\] 其中 \(\vec a_c\) 及 \(\vec a_t\) 分別為向心與切線分量,\(\vec \alpha\) 為擺的角加速度。
where \(\vec a_c\) and \(\vec a_t\) are the centripetal and tangential components, respectively, while \(\vec \alpha\) is the angular acceleration of the pendulum.
一併計算擺繩的張力,
Also calculate the tension in the rod, \[\vec T + m\vec g = m\vec a \qquad \to \qquad \vec T = m\vec a - m\vec g.\]
<<<
***/
//{{{
		pendulum.calculateAcceleration = function(r,v){
			r = r.clone().sub(pendulum.pivotPosition());
			var ur = r.clone().normalize();
			var a = ur.clone().multiplyScalar(
				-v.lengthSq()/r.length()
			);
			pendulum.rod.tension.copy(a).multiplyScalar(pendulum.mass).sub(
				ur.multiplyScalar(fg.dot(ur))
			);
			a.add(pendulum.calculateAlpha(r).cross(r));
			pendulum.acceleration.copy(a);
			//pendulum.rod.tension.copy(a).multiplyScalar(pendulum.mass).sub(fg);
			return a;
		};

		return pendulum.init(param);
	}
};
//}}}
!! Pendulum Control
@@color:red;Pendulums:@@ <html><input type="number" title="Number of pendulums." id="txtNP" min="1" max="30" step="1" value="1" style="width:40px"></html> / [ =chkRandom] Random
!! Pivot Control
\(M_\text{pivot}\) (kg): <html><input type="number" title="Mass of pivot." id="txtPivotMass" min="0.01" max="1" step="0.001" value="0.1" style="width:55px"></html>
!! String Control
//L//~~0~~ (m): <html><input type="number" title="Length of string." id="txtStringLength" min="0.1" max="10" step="0.01" value="0.13" style="width:50px"></html> / \(M_\text{rod}\) (kg): <html><input type="number" title="Mass of string." id="txtStringMass" min="0.001" max="1" step="0.001" value="0.0001" style="width:60px"></html> / \(R_\text{string}\): <html><input type="number" title="Radius of the string." id="txtStringRadius" min="0.001" max="0.05" step="0.001" value="0.005" style="width:55px"></html>
!! Bob Control
\(r_\text{bob}\) (m): <html><input type="number" title="Radius of bob." id="txtBobRadius" min="1e-2" max="1" step="0.01" value="0.025" style="width:50px"></html> / \(M_\text{bob}\) (kg): <html><input type="number" title="Mass of bob." id="txtBobMass" min="0.001" max="1" step="0.01" value="0.029" style="width:55px"></html>
!! Initial Velocity
<<tw3DCommonPanel "Initial Speed">> / <<tw3DCommonPanel "Initial Theta">> / <<tw3DCommonPanel "Initial Phi">>
!! Period Label
<html>''T'' (s): <label id="labelPeriod" title="Period of oscillation." style="font-family:'Courier New'"></label></html>
//{{{
class PhysicalObject(frame):
	def __init__(self,**kwargs):
		frame.__init__(self,**kwargs)
		try:
			self.nseg = kwargs['nseg']
		except KeyError:
			self.nseg = 10
		self.segments = [None]*self.nseg
		try:
			self.__m = kwargs['mass']
		except KeyError:
			self.__m = 0
		try:
			self.__v = kwargs['velocity']
		except KeyError:
			self.__v = vector()
		self.__p = self.__v * self.__m
		try:
			self.__a = kwargs['acceleration']
		except KeyError:
			self.__a = vector()

		self.__Fex = vector()					# ext force on this obj
		self.__KE = 0.5*self.__m*self.__v.mag2	# kinetic energy
		self.__UE = self.__m*9.8*self.pos.y		# gravi potential energy

		self.__k = 0							# elastic constant
		self.__Uela = 0							# elastic energy

		self.__q = 0							# charge
		self.__Uele = 0							# electric potential energy

		def collideWith(self,obj):
			# Determines whether or not this object would collide with obj.
			# If yes, calculates the collision force.
			pass
//}}}
//{{{
class Spring(PhysicalObject):
	def __init__(self,**kwargs):
		PhysicalObject.__init__(self,**kwargs)
		try:
			self.nseg = kwargs['nseg']
		except KeyError:
			self.nseg = 1
		try:
			kwargs['coils'] = int(kwargs['coils']/self.nseg)
			if kwargs['coils'] < 1:
				kwargs['coils'] = 1
		except KeyError:
			kwargs['coils'] = 1
		try:
			kwargs['axis'] /= self.nseg
		except KeyError:
			kwargs['axis'] = vector(0,0,1.0/self.nseg)
		try:
			p = kwargs['pos']
		except KeyError:
			kwargs['pos'] = self.pos

		self.segments[0] = helix(**kwargs)
		for n in range(1,self.nseg):
			kwargs['pos'] += self.segments[n-1].axis
			self.segments[n] = helix(**kwargs)

	def setAxis(self,axis):
		axis_seg = axis / self.nseg
		self.segments[0].axis = axis_seg
		for n in range(1,self.nseg):
			self.segments[n].pos = self.segments[n-1].pos+axis_seg
			self.segments[n].axis = axis_seg

	def getAxis(self):
		return self.segments[self.nseg-1].pos +\
			self.segments[self.nseg-1].axis -\
			self.segments[0].pos
//}}}
!! Object Properties
\(m\) (kg): <html><input type="number" title="Mass of balls." id="txtMass" min="0" max="1" step="0.01" value="0.01" style="width:50px"></html> / \(R\) (m): <html><input type="number" title="Radius of balls." id="txtRadius" min="0" max="0.5" step="0.001" value="0.01" style="width:50px"></html>
/***
''These codes are to solve @@color:red;1^^st^^ order differential equations@@ using the 4^^th^^ order ~Runge-Kutta method, for the following cases:''
# single particle 1D motion;
# single particle 3D motion;
# many particles 1D motion;
# many particles 3D motion.
***/
/***
!! Single Particle 1D Python
***/
/*{{{*/
def RK4(r,V,t,dt,v_now = None):
	# Returns the next position or function value after time dt,
	# using the 4th order Runge-Kutta algorithms.
	#		r: current position or function value
	#		V: Function v(r,t) shall return the velocity or first derivative at
	#			position r and time t.
	#		t: current time
	#		dt: time step
	# 		v_now: current velocity or first derivative (optional).

	r1 = r
	v1 = v_now
	if v1 is None: v1 = V(r1,t)

	v2 = V(r+dt/2.0*v1,t+dt2)

	v3 = V(r+dt/2.0*v2,t+dt3)

	v4 = V(r+dt*v3,t+dt)

	r += (v1+(v2+v3)*2.0+v4)*dt/6.0

	v_now = V(r,t+dt)
	return (r,v_now)
/*}}}*/
/***
!! Single Particle 3D Python
***/
/*{{{*/
def RK4Vector(r,V,t,dt,v_now = None):
	# Returns the next position or function value after time dt,
	# using the 4th order Runge-Kutta algorithms.
	#		r: current position or function value
	#		V: Function v(r,t) shall return the velocity or first derivative at
	#			position r and time t.
	#		t: current time
	#		dt: time step
	# 		v_now: current velocity or first derivative (optional).

	// Python knows what to do with vectors.
	return RK4(r,V,t,dt,v_now)
/*}}}*/
/***
!! Many Particles 1D Python
***/
/*{{{*/
def RK4ListVector(r,v,A,t,dt,a_now = None):
	# Returns into the original arrays the next r's and v's, after time dt
	# has passed, using the 4th order Runge-Kutta algorithms.
	#		r: current positions, must be a vector array
	#		v: current velocities, must be a vector array
	#		A: function A(r,v,t) shall return the acceleration at position
	#			r, velocity v, and time t.
	#		t: current time
	#		dt: time step
	#		a_now: current acceleration (optional), must be a vector array
	#			if given.

	N = len(r)
	#rangeN = range(N)

	r1 = r
	v1 = v
	a1 = a_now
	if a1 is None: a1 = A(r1,v1,t)

	rt = [None]*N

	r2 = rt
	v2 = [None]*N
	dt2 = dt*0.5
	for n in xrange(N):
		r2[n] = r[n] + dt2*v1[n]
		v2[n] = v[n] + dt2*a1[n]
	a2 = A(r2,v2,t+dt2)

	r3 = rt
	v3 = [None]*N
	for n in xrange(N):
		r3[n] = r[n] + dt2*v2[n]
		v3[n] = v[n] + dt2*a2[n]
	a3 = A(r3,v3,t+dt3)

	r4 = rt
	v4 = [None]*N
	for n in xrange(N):
		r4[n] = r[n] + dt*v3[n]
		v4[n] = v[n] + dt*a3[n]
	a4 = A(r4,v4,t+dt)

	dt /= 6.0
	for n in xrange(N):
		r[n] += (v1[n]+(v2[n]+v3[n])*2.0+v4[n])*dt
		v[n] += (a1[n]+(a2[n]+a3[n])*2.0+a4[n])*dt
	a_now = A(r,v,t+dt)

	return (r,v,a_now)
/*}}}*/
/***
!! Many Particles 3D Python
***/
//{{{
	def rk4va(y, f, t, dt):
		// Returns the next y after time dt has passed, using the 4th
		// order Runge-Kutta algorithms.
		//		y: current value for all the particles, must be a vector array.
		//		f: function f(y,t) shall return the derivatives of y's at time t,
		//				both the argument y and the return value of this function
		//				must be vector arrays.
		//		t: current time
		//		dt: time step

		// Python knows what to do with vectors.
		return rk4a(y, f, t, dt)
//}}}
/***
!! nextValue(r,v,a,t,dt,a_now)
***/
//{{{
def nextValue(r,v,a,t,dt,a_now):
	typer = type(r)
	if typer is list:
		typer0 = type(r[0])
		if typer0 is tuple:
			return RK4ListTuple(r,v,a,t,dt,a_now)
		elif typer0 is numpy.ndarray:
			return RK4ListArray(r,v,a,t,dt,a_now)
		elif typer0 is list:
			return RK4ListArray(r,v,a,t,dt,a_now)
		else:
			return RK4ListVector(r,v,a,t,dt,a_now)
	elif typer is numpy.ndarray:
		return RK4ListArray(r,v,a,t,dt,a_now)
	else:
		return RK4(r,v,a,t,dt,a_now)
//}}}
/***
''These codes are to solve @@color:red;2^^nd^^ order differential equations@@ using the 4^^th^^ order ~Runge-Kutta method, for the following cases:''
# single particle 1D/3D motion;
# many particles 3D/1D motion.
!!! The Idea
The idea is shown in the figure below, @@which is captured from [[this video|https://youtu.be/smfX0Jt_f0I?t=325]]@@.
[img(60%,)[image/teaching/RK4 for 2ndODE.JPG]]
***/
/***
!! Single Particle 3D/1D Python
***/
/*{{{*/
	def rk42(r, v, a, t, dt, a_cur):
		# Returns a tuple containing the next r, next v, and the
		# next a, using the 4th order Runge-Kutta algorithm.
		#		r: current position/distance
		#		v: current velocity/speed
		#		a: function a(r,v,t) shall return the acceleration at
		#			position r, velocity v, and time t
		#		t: current time
		#		dt: time step
		#		a_cur: (optional) current acceleration

		r1 = r					# current postion
		v1 = v					# current velocity
		a1 = a_cur				# current acceleration
		if a1 == None: a1 = a(r1,v1,t,dt)

		dt2 = dt*0.5			# first half step, using v1, a1
		r2 = r+dt2*v1			# position at half step
		v2 = v+dt2*a1			# velocity at half step
		a2 = a(r2,v2,t+dt2)		# acceleration at half step

		r3 = r+dt2*v2			# position at half step
		v3 = v+dt2*a2			# velocity at half step
		a3 = a(r3,v3,t+dt2)		# acceleration at half step

								# using v3, a3 to calculate the next step
		r4 = r+dt*v3			# position at next step
		v4 = v+dt*a3			# velocity at next step
		a4 = a(r4,v4,t+dt)		# acceleration at the next step

		# use a "some kind of average" of a1, a2, a3 and a4 to
		# calculate the next v, and that of v1, v2, v3 and v4 to
		# calculate the next r.
		r += (v1+2*v2+2*v3+v4)/6*dt
		v += (a1+2*a2+2*a3+a4)/6*dt
		a_cur = a(r,v,t+dt)

		# returns
		#		the next position
		#		the next velocity
		#		the next acceleration
		return (r,v,a_cur)
/*}}}*/
/***
!! Many Particles 3D/1D Python
***/
/*{{{*/
	def nextValue(r, v, A, t, dt, a_cur):
		// Returns into the original arrays the next r's and v's at time t + dt,
		// using the 4th order Runge-Kutta algorithms.
		//		r: current positions/distances, must be an array of vectors/scalars
		//		v: current velocities/speeds, must be an array of vectors/scalars
		//		A: function A(r,v,t) shall return the accelerations at positions r,
		//			velocities v, and time t, the arguments r, v and the return value
		//			of this function must all be arrays of scalars/vectors
		//		t: current time
		//		dt: time step
		//		a_cur: (optional) current accelerations

		n=0
		N = len(r)

		r1 = r
		v1 = v
		a1 = a_cur
		if a1 == None: a1 = A(r1,v1,t)		# current acceleration

		dt2 = dt * 0.5
		r2 = [None]*N
		v2 = [None]*N
		for n in range(N):
			r2[n] = r[n] + dt2*v1[n]
			v2[n] = v[n] + dt2*a1[n]
		a2 = A(r2,v2,t+dt2)				# acceleration at half step

		r3 = [None]*N
		v3 = [None]*N
		for n in range(N):
			r3[n] = r[n] + dt2*v2[n]
			v3[n] = v[n] + dt2*a2[n]
		a3 = A(r3,v3,t+dt2)

		r4 = [None]*N
		v4 = [None]*N
		for n in range(N):
			r4[n] = r[n] + dt*v3[n]
			v4[n] = v[n] + dt*a3[n]
		a4 = A(r4,v4,t+dt)

		for n in range(N):
			r[n] = r[n] + (v1[n]+(v2[n]+v3[n])*2+v4[n])*dt/6
			v[n] = v[n] + (a1[n]+(a2[n]+a3[n])*2+a4[n])*dt/6

		return [r,v,A(r,v,t,dt,a_cur)]
/*}}}*/
 -- 遊戲學習、學習遊戲
好玩模擬
/***
|''Name:''|SlideShowPlugin|
|''Description:''|Creates a slide show from any number of tiddlers|
|''Author:''|Paulo Soares|
|''Contributors:''|John P. Rouillard|
|''Version:''|2.2.6|
|''Date:''|2010-11-17|
|''Source:''|http://www.math.ist.utl.pt/~psoares/addons.html|
|''Documentation:''|[[SlideShowPlugin Documentation|SlideShowPluginDoc]]|
|''License:''|[[Creative Commons Attribution-Share Alike 3.0 License|http://creativecommons.org/licenses/by-sa/3.0/]]|
|''~CoreVersion:''|2.5.0|
***/
//{{{
if(!version.extensions.SlideShowPlugin) { //# ensure that the plugin is only installed once
version.extensions.SlideShowPlugin = {installed: true};

(function($) {
config.macros.slideShow = {maxTOCLength: 30, separator:'-s-'};

config.formatters.push( {
	name: "SlideSeparator",
	match: "^"+config.macros.slideShow.separator+"+$\\n?",
	handler: function(w) {
		createTiddlyElement(w.output,"hr",null,'slideSeparator');
	}
});

config.macros.slideShow.text = {
  label: "slide show", tooltip: "Start slide show",
  quit: {label: "x", tooltip: "Quit the slide show"},
  firstSlide: {label: "<<", tooltip: "Go to first slide"},
  previous: {label: "<", tooltip: "Go back"},
  next: {label: ">", tooltip: "Advance"},
  lastSlide: {label: ">>", tooltip: "Go to last slide"},
  goto: {label: "Go to slide:"},
  resetClock: {tooltip: "Reset the clock"},
  overlay: "overlay"
};

config.macros.slideShow.handler = function(place,macroName,params,wikifier,paramString){
  var args = paramString.parseParams(null,null,false);
  this.label = getParam(args,"label",this.text.label);
  this.tooltip = getParam(args,"tooltip",this.text.tooltip);
  var onclick = function(){config.macros.slideShow.onClick(place,paramString); return false;};
  createTiddlyButton(place,this.label,this.tooltip,onclick);
  return false;
}

config.macros.slideShow.onClick = function(place,paramString) {
  var slide, cm = config.macros.slideShow;
  var title = story.findContainingTiddler(place);
  title = title ? title.getAttribute("tiddler") : null;
  var args = paramString.parseParams(null,null,false);
  title =  getParam(args,"tiddler",title);
  var argsArray = paramString.readMacroParams();
  this.single = ($.inArray('single',argsArray) > -1);
  this.clicks = ($.inArray('noClicks',argsArray) < 0);
  this.keyboard = ($.inArray('noKeyboard',argsArray) < 0);
  this.showAll = ($.inArray('showAll',argsArray) > -1);
  this.cycle = ($.inArray('cycle',argsArray) > -1);
  this.overlays = ($.inArray('noOverlays',argsArray) < 0);
  this.theme = getParam(args,"theme");
  this.tag = getParam(args,"tag");
  this.toc = getParam(args,"toc","headers");
  this.sort = getParam(args,"sort");
  this.clockFormat = getParam(args,"clockFormat",'0hh:0mm:0ss');
  this.auto = getParam(args,"auto",0);
  this.header = getParam(args,"header",title);
  this.footer = getParam(args,"footer","");
  this.clock = getParam(args,"clock");
  this.blocked = 0;
  if(this.clock){
    var startTime = new Date(0);
    this.clockCorrection=startTime.getTimezoneOffset()*60000;
    startTime = new Date();
    this.clockMultiplier = 1;
    this.clockInterval = 0;
    var clockType= parseFloat(this.clock);
    if(clockType < 0) {
      this.clockMultiplier = -1;
      this.clockInterval = -clockType*60000;
    } else if(clockType == 0){
      this.clockCorrection = 0;
      startTime = new Date(0);
    }
    this.clockStartTime=startTime.getTime();
  }
  this.slides = [];
  this.openTiddlers = [];
  $("#tiddlerDisplay > *").each(function(){cm.openTiddlers.push($(this).attr('tiddler'))});
  var count = 0;
  this.slideTOC=[];
  if(this.single){
    if(!store.tiddlerExists(title)) return;
    var newTiddler;
    var content = store.getTiddlerText(title).split(cm.separator);
    $.each(content, function(){
      count++;
      newTiddler = new Tiddler();
      newTiddler.title ="TempSlide" + count;
      newTiddler.tags[0] = "excludeLists";
      newTiddler.text = $.trim(this);
      newTiddler.fields['doNotSave']= true;
      store.addTiddler(newTiddler);
      cm.buildTOC(count,newTiddler.title);
      cm.slides.push(newTiddler.title);
    });
  } else {
    if(this.tag){
      var content = store.getTaggedTiddlers(this.tag,this.sort);
      $.each(content, function(){
        count++;
        cm.buildTOC(count,this.title);
        cm.slides.push(this.title);
      });
    } else {
      story.displayTiddler(null,title);
      var list = $('[tiddler='+title+']').find('.viewer').find('.tiddlyLinkExisting');
      $.each(list,function(){
        if(!$(this).parents().hasClass("exclude")){
          slide = $(this).attr('tiddlylink');
          count++;
          cm.buildTOC(count,slide);
          cm.slides.push(slide);
        }
      });
    }
  }
  this.nSlides = this.slides.length;
  if(this.nSlides==0) return false;
  clearMessage();
  this.toggleSlideStyles();
  if(!this.showAll){
    //Attach the key and mouse listeners
    if(this.keyboard && !$("#tiddlerDisplay").hasClass("noKeyboard")) $(document).keyup(cm.keys);
    if(this.clicks){
      $(document).mouseup(cm.clicker);
      document.oncontextmenu = function(){return false;}
    }
    if(this.clock) this.slideClock=setInterval(this.setClock, 1000);
    if(this.auto>0){
      this.autoAdvance=setInterval(cm.next, this.auto*1000);
    }
    this.showSlide(1);
  } else {
    story.closeAllTiddlers();
    story.displayTiddlers(null,this.slides);
    $(".tiddler").attr("ondblclick",null);
    $(document).keyup(cm.endSlideShow);
  }
  return false;
}

config.macros.slideShow.buildNavigator = function() {
  //Create the navigation bar
  var i, slidefooter = $("#controlBar")[0];
  if(!slidefooter) return;
  $(slidefooter).addClass("slideFooterOff noClicks");
  var navigator = createTiddlyElement(slidefooter,"SPAN","navigator");
  var buttonBar = createTiddlyElement(navigator,"SPAN","buttonBar");
  //Make it so that when the footer is hovered over the class will change to make it visible
  $(slidefooter).bind("mouseenter mouseleave", function(e){$(this).toggleClass("slideFooterOff");});
  //Create the control buttons for the navigation
 
  createTiddlyButton(buttonBar,this.text.firstSlide.label,this.text.firstSlide.tooltip,this.firstSlide,"button");
  createTiddlyButton(buttonBar,this.text.previous.label,this.text.previous.tooltip,this.previous,"button");
  createTiddlyButton(buttonBar,this.text.quit.label,this.text.quit.tooltip,this.endSlideShow,"button");
  createTiddlyButton(buttonBar,this.text.next.label,this.text.next.tooltip,this.next,"button");
  createTiddlyButton(buttonBar,this.text.lastSlide.label,this.text.lastSlide.tooltip,this.lastSlide,"button");
  if(this.clock){
    if(this.clock == 0){
       createTiddlyElement(navigator,"SPAN","slideClock");
    } else {
      createTiddlyButton(navigator," ",this.text.resetClock.tooltip,this.resetClock,"button","slideClock");
    }
    this.setClock();
  }
  var index = createTiddlyElement(slidefooter,"SPAN","slideCounter");
  index.onclick = this.toggleTOC;
  var toc = createTiddlyElement(slidefooter,"SPAN","toc");
  var tocLine;
  for(i=0; i<this.slideTOC.length; i++){
    $(toc).append(this.slideTOC[i][2]);
    tocLine = $(toc.lastChild);
    tocLine.addClass("tocLevel"+this.slideTOC[i][1]).css("cursor", "pointer").hover(function () {
        $(this).addClass("highlight");}, function () {
        $(this).removeClass("highlight");});
    tocLine.attr("slide",this.slideTOC[i][0]);
    tocLine.click(config.macros.slideShow.showSlideFromTOC);
  }
  //Input box to jump to specific slide
  var tocItem = createTiddlyElement(toc,"DIV","jumpItem",null,this.text.goto.label);
  var tocJumpInput = createTiddlyElement(tocItem,"INPUT","jumpInput");
  tocJumpInput.type="text";
  $(tocJumpInput).keyup(config.macros.slideShow.jumpToSlide);
}

//Used to shorten the TOC fields
config.macros.slideShow.abbreviate = function(label){
  if(label.length>this.maxTOCLength) {
    var temp = new Array();
    temp = label.split(' ');
    label = temp[0];
    for(var j=1; j<temp.length; j++){
      if((label.length+temp[j].length)<=this.maxTOCLength){
        label += " " + temp[j];
      } else {
        label += " ...";
        break;
      }
    }
  }
  return label;
}

config.macros.slideShow.buildTOC = function(count,title) {
  var level = 1, text;
  switch(this.toc){
  case "headers":
    var frag = wikifyStatic(store.getTiddlerText(title));
    text = frag.replace(/<div class="comment">.*<\/div>/mg,"");
    var matches =  text.match(/<h[123456]>.*?<\/h[123456]>/mgi);
    if(matches){
      for (var j=0; j<matches.length; j++){
        level = matches[j].charAt(2);
        text = matches[j].replace(/<\/?h[123456]>/gi,"");
        text = this.abbreviate(text);
        this.slideTOC.push([count,level,"<div>"+text+"</div>"]);
      }
    }
    break;
  case "titles":
    text = this.abbreviate(title);
    this.slideTOC.push([count,level,"<div>"+text+"</div>"]);
  }
}

config.macros.slideShow.showSlideFromTOC = function(e) {
  var cm = config.macros.slideShow;
  var slide = parseInt(e.target.getAttribute('slide'));
  $("#toc").hide();
  cm.showSlide(slide);
  return false;
}

config.macros.slideShow.toggleTOC = function(){
  $("#toc").toggle();
  $("#jumpInput").focus().val('');
  return false;
}

config.macros.slideShow.isInteger = function(s){
  for (var i = 0; i < s.length; i++){
    // Check that current character is number
    var c = s.charAt(i);
    if (((c < "0") || (c > "9"))) return false;
  }
  // All characters are numbers
  return true;
}

config.macros.slideShow.jumpToSlide = function(e){
  var cm = config.macros.slideShow;
  if(e.which==13){
    var input= $("#jumpInput").val();
    if(cm.isInteger(input) && input>0 && input<=cm.nSlides){
      $("#toc").hide();
      cm.showSlide(input);
    } else  {$("#jumpInput").val('');}
  }
  return false;
}

config.macros.slideShow.toggleSlideStyles = function(){
  var contentWrapper = $('#contentWrapper');
  if(contentWrapper.hasClass("slideShowMode")){
    refreshPageTemplate();
    removeStyleSheet("SlideShowStyleSheet");
    if(this.theme) removeStyleSheet(this.theme);
  } else {
    $("#displayArea").prepend('<div id="slideBlanker" style="display:none"></div><div id="slideHeader">'+this.header+'</div><div id="slideFooter">'+this.footer+'</div><div id="controlBar"></div>');
    setStylesheet(store.getRecursiveTiddlerText("SlideShowStyleSheet"),"SlideShowStyleSheet");
    if(this.theme && store.tiddlerExists(this.theme)){setStylesheet(store.getRecursiveTiddlerText(this.theme),this.theme);}
    this.buildNavigator();
  }
  contentWrapper.toggleClass("slideShowMode");
  return false;
}

config.macros.slideShow.showSlide = function(n){
  if(this.cycle) {
    if(n>this.nSlides) {
      n = 1;
    } else if(n<1) {
      n = this.nSlides;
    }
  } else {
    if(n>this.nSlides || n<1) return;
  }
  story.closeAllTiddlers();
  if(this.clock=='-'){this.resetClock();}
  story.displayTiddler(null,String(this.slides[n-1]));
  $(".tiddler").attr("ondblclick",null);
  $("body").removeClass("slide"+this.curSlide);
  this.curSlide = n;
  $("body").addClass("slide"+this.curSlide);
  $("#slideCounter").text(this.curSlide+"/"+this.nSlides);
  if(this.overlays){
    var contents = $(".viewer *");
    this.numOverlays = 1;
    while(1){
      if(contents.hasClass(this.text.overlay+this.numOverlays)){
        this.numOverlays++;
      } else {break;}
    }
    this.numOverlays--;
    this.showOverlay(0);
  }
  return false;
}

config.macros.slideShow.showOverlay = function(n){
  var i, set, cm = config.macros.slideShow;
  if(!cm.overlays || cm.numOverlays == 0 || n<0 || n>cm.numOverlays){return;}
  for(i=1; i<n; i++){
    set = $(".viewer "+"."+cm.text.overlay+i);
    set.removeClass("currentOverlay nextOverlay");
    set.addClass("previousOverlay");
  }
  set = $(".viewer "+"."+cm.text.overlay+n);
  set.removeClass("previousOverlay nextOverlay");
  set.addClass("currentOverlay");
  for(i=n; i<config.macros.slideShow.numOverlays; i++){
    set = $(".viewer "+"."+cm.text.overlay+(i+1));
    set.removeClass("previousOverlay currentOverlay");
    set.addClass("nextOverlay");
  }
  cm.curOverlay = n;
}

config.macros.slideShow.firstSlide = function(){
  config.macros.slideShow.showSlide(1);
  return false;
}

config.macros.slideShow.lastSlide = function(){
  config.macros.slideShow.showSlide(config.macros.slideShow.nSlides);
  return false;
}

config.macros.slideShow.next = function(){
  var cm = config.macros.slideShow;
  if(!cm.overlays || cm.numOverlays == 0 || cm.curOverlay == cm.numOverlays) {
    cm.showSlide(cm.curSlide+1);
  } else {
    cm.showOverlay(cm.curOverlay+1);
  }
  return false;
}

config.macros.slideShow.previous = function(){
  var cm = config.macros.slideShow;
  if(!cm.overlays || cm.numOverlays == 0 || cm.curOverlay == 0) {
    cm.showSlide(cm.curSlide-1);
    cm.showOverlay(cm.numOverlays);
  } else {
    cm.showOverlay(cm.curOverlay-1);
  }
  return false;
}

config.macros.slideShow.endSlideShow=function(){
  var cm = config.macros.slideShow;
  if(cm.autoAdvance) {clearInterval(cm.autoAdvance);}
  if(cm.clock) clearInterval(cm.slideClock);
  story.closeAllTiddlers();
  cm.toggleSlideStyles();
  story.displayTiddlers(null,cm.openTiddlers);
  $(document).unbind();
  document.oncontextmenu =  function(){};
  $("body").removeClass("slide"+cm.curSlide);
  return false;
}

// 'keys' code adapted from S5 which in turn was adapted from MozPoint (http://mozpoint.mozdev.org/)
config.macros.slideShow.keys = function(key) {
  var cm = config.macros.slideShow;
  switch(key.which) {
  case 32: // spacebar
    if(cm.auto>0 && cm.blocked==0){
      if(cm.autoAdvance){
        clearInterval(cm.autoAdvance);
        cm.autoAdvance = null;
      } else {
        cm.autoAdvance=setInterval(cm.next, cm.auto*1000);
      }
    } else {
      if(cm.blocked==0) cm.next();
    }
    break;
  case 34: // page down
    if(cm.blocked==0) cm.showSlide(cm.curSlide+1);
    break;
  case 39: // rightkey
    if(cm.blocked==0) cm.next();
    break;
  case 40: // downkey
    if(cm.blocked==0) cm.showOverlay(cm.numOverlays);
    break;
  case 33: // page up
    if(cm.blocked==0) cm.showSlide(cm.curSlide-1);
    break;
  case 37: // leftkey
    if(cm.blocked==0) cm.previous();
    break;
  case 38: // upkey
    if(cm.blocked==0) cm.showOverlay(0);
    break;
  case 36: // home
    if(cm.blocked==0) cm.firstSlide();
    break;
  case 35: // end
    if(cm.blocked==0) cm.lastSlide();
    break;
  case 27: // escape
    cm.endSlideShow();
    break;
  case 66: // B
    $("#slideBlanker").toggle();
    cm.blocked = (cm.blocked +1)%2;
    break;
  }
  return false;
}

config.macros.slideShow.clicker = function(e) {
  var cm = config.macros.slideShow;
  if(cm.blocked==1 || $(e.target).attr('href') || $(e.target).parents().andSelf().hasClass('noClicks')){
    return true;
  }
  if($("#toc").is(':visible')){
    cm.toggleTOC();
  } else {
    if((!e.which && e.button == 1) || e.which == 1) {
      cm.next();
    }
    if((!e.which && e.button == 2) || e.which == 3) {
      cm.previous();
    }
  }
  return false;
}

config.macros.slideShow.setClock = function(){
  var cm = config.macros.slideShow;
  var actualTime = new Date();
  var newTime = actualTime.getTime() - cm.clockStartTime;
  newTime = cm.clockMultiplier*newTime+cm.clockInterval+cm.clockCorrection;
  actualTime.setTime(newTime);
  newTime = actualTime.formatString(cm.clockFormat);
  $("#slideClock").text(newTime);
  return false;
}

config.macros.slideShow.resetClock = function(){
  var cm = config.macros.slideShow;
  if(cm.clock == 0) return;
  var time = new Date(0);
  if(cm.clockStartTime>time){
    var startTime = new Date();
    cm.clockStartTime=startTime.getTime();
  }
  return false;
}

config.shadowTiddlers.SlideShowStyleSheet="/*{{{*/\n.header, #mainMenu, #sidebar, #backstageButton, #backstageArea, .toolbar, .title, .subtitle, .tagging, .tagged, .tagClear, .comment{\n display:none !important\n}\n\n#slideBlanker{\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n z-index: 90; \n background-color: #000;\n opacity: 0.9;\n filter: alpha(opacity=90)\n}\n\n.nextOverlay{\n visibility: hidden\n}\n\n.previousOverlay,.currentOverlay{\n visibility: visible\n}\n\n#displayArea{\n font-size: 250%;\n margin: 0 !important;\n padding: 0\n}\n\n#controlBar{\n position: fixed;\n bottom: 2px;\n right: 2px;\n width: 100%;\n text-align: right\n}\n\n#controlBar .button{\n margin: 0 0.25em;\n padding: 0 0.25em\n}\n\n#slideHeader{\n font-size: 200%;\n font-weight: bold\n}\n\n#slideFooter{\n position: fixed;\n bottom: 2px\n}\n\n.slideFooterOff #navigator{\n visibility: hidden\n}\n\n#slideClock{\n margin: 0 5px 0 5px\n}\n\n#slideCounter{\n cursor: pointer;\n color: #aaa\n}\n\n#toc{\n display: none;\n position: absolute;\n font-size: .75em;\n bottom: 2em;\n right: 0;\n background: #fff;\n border: 1px solid #000;\n text-align: left\n}\n\n#jumpItem{\n padding-left:0.25em\n}\n\n#jumpInput{\n margin-left: 0.25em;\n width: 3em\n}\n\n.tocLevel1{\n font-size: .8em\n}\n\n.tocLevel2{\n margin-left: 1em;\n font-size: .75em\n}\n\n.tocLevel3{\n margin-left: 2em;\n font-size: .7em\n}\n\n.tocLevel4{\n margin-left: 3em;\n font-size: .65em\n}\n\n.tocLevel5{\n margin-left: 4em;\n font-size: .6em\n}\n\n.tocLevel6{\n margin-left: 5em;\n font-size: .55em\n}\n/*}}}*/";

config.shadowTiddlers.SlideShowPluginDoc="The documentation is available [[here|http://www.math.ist.utl.pt/~psoares/addons.html#SlideShowPluginDoc]].";
})(jQuery)
}
//}}}
!! Spring Control
Spring \(M\) (kg): <html><input type="number" title="Spring mass." id="txtSpringMass" min="0" max="2" step="0.001" value="0.1" style="width:55px"></html> / \(L_0\) (m): <html><input type="number" title="Spring length." id="txtSpringL0" min="0" max="2" step="0.01" value="0.1" style="width:50px"></html> / \(k\) (N/m): <html><input type="number" title="Spring constant." id="txtSpringK" min="0" max="100000" step="1" value="5" style="width:60px"></html>
!! Ball Control
Ball \(M\) (kg): <html><input type="number" title="Ball mass." id="txtBallMass" min="0" max="10" step="0.2" value="0.5" style="width:50px"></html> / \(R\) (m): <html><input type="number" title="Ball radius." id="txtBallRadius" min="0" max="1" step="0.01" value="0.05" style="width:50px"></html>
[[twD3CSS]]
/*{{{*/
.viewer th, .viewer thead td, .options th, .options thead td {
	background-color: #c9c9c9;
	color: black;
}

table.noborder, .noborder tr, .noborder th, .noborder td, .noborder thead td{
	border:0;
}

#displayArea {position:absolute; left:-12.5em; width:80%;}
/*}}}*/
/*{{{*/
* html .tiddler {height:1%;}

body {font-size:.75em; font-family:arial,helvetica; margin:0; padding:0;}

h1,h2,h3,h4,h5,h6 {font-weight:bold; text-decoration:none;}
h1,h2,h3 {padding-bottom:1px; margin-top:1.2em;margin-bottom:0.3em;}
h4,h5,h6 {margin-top:1em;}
h1 {font-size:1.35em;}
h2 {font-size:1.25em;}
h3 {font-size:1.1em;}
h4 {font-size:1em;}
h5 {font-size:.9em;}

hr {height:1px;}

a {text-decoration:none;}

dt {font-weight:bold;}

ol {list-style-type:decimal;}
ol ol {list-style-type:lower-alpha;}
ol ol ol {list-style-type:lower-roman;}
ol ol ol ol {list-style-type:decimal;}
ol ol ol ol ol {list-style-type:lower-alpha;}
ol ol ol ol ol ol {list-style-type:lower-roman;}
ol ol ol ol ol ol ol {list-style-type:decimal;}

.txtOptionInput {width:11em;}

#contentWrapper .chkOptionInput {border:0;}

.externalLink {text-decoration:underline;}

.indent {margin-left:3em;}
.outdent {margin-left:3em; text-indent:-3em;}
code.escaped {white-space:nowrap;}

.tiddlyLinkExisting {font-weight:bold;}
.tiddlyLinkNonExisting {font-style:italic;}

/* the 'a' is required for IE, otherwise it renders the whole tiddler in bold */
a.tiddlyLinkNonExisting.shadow {font-weight:bold;}

#mainMenu .tiddlyLinkExisting,
	#mainMenu .tiddlyLinkNonExisting,
	#sidebarTabs .tiddlyLinkNonExisting {font-weight:normal; font-style:normal;}
#sidebarTabs .tiddlyLinkExisting {font-weight:bold; font-style:normal;}

.header {position:relative;}
.header a:hover {background:transparent;}
.headerShadow {position:relative; padding:4.5em 0 1em 1em; left:-1px; top:-1px;}
.headerForeground {position:absolute; padding:4.5em 0 1em 1em; left:0px; top:0px;}

.siteTitle {font-size:3em;}
.siteSubtitle {font-size:1.2em;}

#mainMenu {position:absolute; left:0; width:10em; text-align:right; line-height:1.6em; padding:1.5em 0.5em 0.5em 0.5em; font-size:1.1em;}

#sidebar {position:absolute; right:3px; width:16em; font-size:.9em;}
#sidebarOptions {padding-top:0.3em;}
#sidebarOptions a {margin:0 0.2em; padding:0.2em 0.3em; display:block;}
#sidebarOptions input {margin:0.4em 0.5em;}
#sidebarOptions .sliderPanel {margin-left:1em; padding:0.5em; font-size:.85em;}
#sidebarOptions .sliderPanel a {font-weight:bold; display:inline; padding:0;}
#sidebarOptions .sliderPanel input {margin:0 0 0.3em 0;}
#sidebarTabs .tabContents {width:15em; overflow:hidden;}

.wizard {padding:0.1em 1em 0 2em;}
.wizard h1 {font-size:2em; font-weight:bold; background:none; padding:0; margin:0.4em 0 0.2em;}
.wizard h2 {font-size:1.2em; font-weight:bold; background:none; padding:0; margin:0.4em 0 0.2em;}
.wizardStep {padding:1em 1em 1em 1em;}
.wizard .button {margin:0.5em 0 0; font-size:1.2em;}
.wizardFooter {padding:0.8em 0.4em 0.8em 0;}
.wizardFooter .status {padding:0 0.4em; margin-left:1em;}
.wizard .button {padding:0.1em 0.2em;}

#messageArea {position:fixed; top:2em; right:0; margin:0.5em; padding:0.5em; z-index:2000; _position:absolute;}
.messageToolbar {display:block; text-align:right; padding:0.2em;}
#messageArea a {text-decoration:underline;}

.tiddlerPopupButton {padding:0.2em;}
.popupTiddler {position: absolute; z-index:300; padding:1em; margin:0;}

.popup {position:absolute; z-index:300; font-size:.9em; padding:0; list-style:none; margin:0;}
.popup .popupMessage {padding:0.4em;}
.popup hr {display:block; height:1px; width:auto; padding:0; margin:0.2em 0;}
.popup li.disabled {padding:0.4em;}
.popup li a {display:block; padding:0.4em; font-weight:normal; cursor:pointer;}
.listBreak {font-size:1px; line-height:1px;}
.listBreak div {margin:2px 0;}

.tabset {padding:1em 0 0 0.5em;}
.tab {margin:0 0 0 0.25em; padding:2px;}
.tabContents {padding:0.5em;}
.tabContents ul, .tabContents ol {margin:0; padding:0;}
.txtMainTab .tabContents li {list-style:none;}
.tabContents li.listLink { margin-left:.75em;}

#contentWrapper {display:block;}
#splashScreen {display:none;}

#displayArea {margin:1em 17em 0 14em;}

.toolbar {text-align:right; font-size:.9em;}

.tiddler {padding:1em 1em 0;}

.missing .viewer,.missing .title {font-style:italic;}

.title {font-size:1.6em; font-weight:bold;}

.missing .subtitle {display:none;}
.subtitle {font-size:1.1em;}

.tiddler .button {padding:0.2em 0.4em;}

.tagging {margin:0.5em 0.5em 0.5em 0; float:left; display:none;}
.isTag .tagging {display:block;}
.tagged {margin:0.5em; float:right;}
.tagging, .tagged {font-size:0.9em; padding:0.25em;}
.tagging ul, .tagged ul {list-style:none; margin:0.25em; padding:0;}
.tagClear {clear:both;}

.footer {font-size:.9em;}
.footer li {display:inline;}

.annotation {padding:0.5em; margin:0.5em;}

* html .viewer pre {width:99%; padding:0 0 1em 0;}
.viewer {line-height:1.4em; padding-top:0.5em;}
.viewer .button {margin:0 0.25em; padding:0 0.25em;}
.viewer blockquote {line-height:1.5em; padding-left:0.8em;margin-left:2.5em;}
.viewer ul, .viewer ol {margin-left:0.5em; padding-left:1.5em;}

.viewer table, table.twtable {border-collapse:collapse; margin:0.8em 1.0em;}
.viewer th, .viewer td, .viewer tr,.viewer caption,.twtable th, .twtable td, .twtable tr,.twtable caption {padding:3px;}
table.listView {font-size:0.85em; margin:0.8em 1.0em;}
table.listView th, table.listView td, table.listView tr {padding:0px 3px 0px 3px;}

.viewer pre {padding:0.5em; margin-left:0.5em; font-size:1.2em; line-height:1.4em; overflow:auto;}
.viewer code {font-size:1.2em; line-height:1.4em;}

.editor {font-size:1.1em;}
.editor input, .editor textarea {display:block; width:100%; font:inherit;}
.editorFooter {padding:0.25em 0; font-size:.9em;}
.editorFooter .button {padding-top:0px; padding-bottom:0px;}

.fieldsetFix {border:0; padding:0; margin:1px 0px;}

.sparkline {line-height:1em;}
.sparktick {outline:0;}

.zoomer {font-size:1.1em; position:absolute; overflow:hidden;}
.zoomer div {padding:1em;}

* html #backstage {width:99%;}
* html #backstageArea {width:99%;}
#backstageArea {display:none; position:relative; overflow: hidden; z-index:150; padding:0.3em 0.5em;}
#backstageToolbar {position:relative;}
#backstageArea a {font-weight:bold; margin-left:0.5em; padding:0.3em 0.5em;}
#backstageButton {display:none; position:absolute; z-index:175; top:0; right:0;}
#backstageButton a {padding:0.1em 0.4em; margin:0.1em;}
#backstage {position:relative; width:100%; z-index:50;}
#backstagePanel {display:none; z-index:100; position:absolute; width:90%; margin-left:3em; padding:1em;}
.backstagePanelFooter {padding-top:0.2em; float:right;}
.backstagePanelFooter a {padding:0.2em 0.4em;}
#backstageCloak {display:none; z-index:20; position:absolute; width:100%; height:100px;}

.whenBackstage {display:none;}
.backstageVisible .whenBackstage {display:block;}
/*}}}*/
/*{{{*/
@media print {
.header, .siteTitle, .siteSubtitle, #titleArea, .tagged, #mainMenu, #sidebar, #messageArea, .toolbar, #backstageButton, #backstageArea {display: none !important;}
.oddRow, evenRow {
	background-color: #fff;
}
#displayArea {margin: 0; width: 100%;}
#tiddlerDisplay {margin: 0 0 0 13em; padding: 0; width: 120%;}
noscript {display:none;} /* Fixes a feature in Firefox 1.5.0.2 where print preview displays the noscript content */
}
/*}}}*/
chkHOTEnabled: true
chktw3DEnabled: true
chktwDataEnabled: true
chktwLaTexEnabled: true
chktwLaTexIncludeDollarSignsDisplay: false
chktwLaTexIncludeDollarSignsInline: false
chktwLaTexMathJaxAutoNumber: true
chktwLaTexPluginEnabled: true
chktwNumericDebug: false
chktwNumericEnabled: true
chktwNumericForceRadix2: false
chktwPhysicsEnabled: true
chktwVEnabled: true
chktwVPMAsync: false
chktwVPMEnabled: true
chktwVPMUseParallelJS: false
chktwVPMUseTransferables: false
chktwmathAutoNumber: false
chktwmathEnabled: true
chktwmathMathJaxAutoNumber: true
chktwmathUseMathJax: true
chktwmathUsekamath: false
chktwveCoreClickAway: true
chktwveCoreConfirmToDelete: true
chktwveCoreEditWrappers: true
chktwveCoreEnabled: true
chktwveCoreManualSave: true
chktwveCoreManualUpload: true
chktwveCorePreview: true
chktwveCoreShowFocus: true
chktwveExtraCountText: true
chktwveExtraCyclicNavi: false
chktwveExtraCyclickNavi: false
chktwveExtraIncludeSubs: true
chktwveExtraInline: true
chktwveExtraLocateChar: false
chktwveExtraMathAutoNumber: true
chktwveExtraNoClick: false
chktwveExtracyclicNavi: false
chktwveNumericDebug: false
chktwveNumericEnabled: true
chktwveNumericForceRadix2: false
chktwvePluginEnabled: true
chktwveTableEditAll: false
chktwveTableEnabled: true
chktwveTableIncludeCSS: false
chktwveTableLargeEnabled: true
chktwveTableMultiLine: true
chktwveTableTranspose: true
chktwveTcalcAllTables: false
chktwveTcalcAsync: false
chktwveTcalcColorNegativeNumbers: false
chktwveTcalcDebugMode: false
chktwveTcalcEnabled: true
chktwveTcalcInTextCalc: false
chktwveTcalcThousandSeparated: false
txttwveCoreMinEditWidth: 6
txttwveCorePreviewCaret: %7C
txttwveCorePreviewHeight: 15
txttwveExtraCountHow: word
txttwveExtraPreviewOpacity: 1.00
txttwveTableFixCols: 0
txttwveTableFixRows: 0
txttwveTableMaxHeight: 70000px
txttwveTableMaxWidth: 100%25
txttwveTableMinCellWidth: 2
txttwveTcalcDecimalMark: .
txttwveTcalcNegativeNumberColor: red
txttwveTcalcThousandSeparator: %2C
txttwveTcalcWorkFlow: TopDownLeftRight
|~ViewToolbar|closeTiddler closeOthers editTiddler > fields syncing permalink references jump|
|~EditToolbar|+saveTiddler -cancelTiddler deleteTiddler|
<!--{{{-->
<div class='toolbar' macro='toolbar [[ToolbarCommands::ViewToolbar]]'></div>
<div class='title' macro='view title'></div>
<div class='subtitle'><span macro='view modifier link'></span>, <span macro='view modified date'></span> (<span macro='message views.wikified.createdPrompt'></span> <span macro='view created date'></span>)</div>
<div class='tagging' macro='tagging'></div>
<div class='viewer' macro='view text wikified'></div>
<div class='tagClear'></div>
<!--}}}-->
/*{{{*/
.axis path,
.axis line {
	fill: none;
	stroke: black;
	shape-rendering: crispEdges;
}
.axis text {
	font-family: sans-serif;
	font-size: 11px;
}
/*}}}*/
@@font-size:1.5em;''擺繩的張力''@@
----
!! 細步計算
<<tiddler "單擺運動一 JS Codes##細步計算">>
!!!! 線上模擬
* @@color:red;取消勾選右方 Auto CPF 可以慢很多,看清楚運動的每一步!@@
** 勾選起來可以快很多!
|單擺運動 JS 版本|c
|width:45%;<<tw3DCommonPanel "Flow Control">>|width:45%;<<tw3DCommonPanel "Label Statistics">>|
|<<tw3DCommonPanel "Simulation Time Control">>|<<tw3DCommonPanel "Air Drag">>|
|<<tw3DCommonPanel "Center of Mass Control">>|<<tw3DCommonPanel "Trail Control">>|
|<<tiddler "Pendulum Panel##Pivot Control">> / <<tiddler "Pendulum Panel##Bob Control">>|<<tiddler "Pendulum Panel##String Control">>|
|(CCW with downward vertical) <<tw3DCommonPanel "Initial Theta">> / <<tiddler "Pendulum Panel##Period Label">>||
|<<tw3DCommonPanel "Plot0 Control">> / <<tw3DCommonPanel "DAQ Control">> / <<tw3DCommonPanel "Gravity Control">><br><<twDataPlot id:dataPlot0>>|<<tw3DCommonPanel "Plot1 Control">> / <<tw3DCommonPanel "Message">><br><<twDataPlot id:dataPlot1>>|
|>|<<tw3DScene [[擺繩張力 JS Codes]]>>|
>@@font-sze:0.8em;此線上模擬版本係由瀏覽器內建的 Javascript 寫成,以 [[THREE.js|https://threejs.org/]] 做 3D 繪圖,有興趣學習者可以自行上網以【Javascript 學習】、【Javascript 初學】、【Javascript 3D 繪圖】等關鍵字搜尋相關教學文章或影片。
單擺運動也是力學運動中簡單的範例之一,它的主要結果:週期性擺動,''小角度''時擺動週期 \[T = 2\pi \sqrt{l \over g}\] 只和擺長 \(l\) 有關,和擺錘質量無關等,也是許多人知道的事情。本系列用單擺做第二個範例,除了它夠簡單之外,還要利用它的週期性來展現一件重要的事情:''數值計算的誤差''。

__做科學實驗之前,都得先了解儀器的誤差多大,做科學計算之前,當然也得先知道工具的限制何在。__

底下三個範例分別展示【平面擺 -- 簡單計算】、【平面擺 -- ~Runge-Kutta】,以及【非平面擺】等情況,__按一下標題就可以拉出相關的內容__,按按看吧。
!!! 單擺運動一
* [[單擺運動一]] 使用最簡單的數值方法(專業術語叫【[[歐拉法|https://zh.wikipedia.org/wiki/%E6%AC%A7%E6%8B%89%E6%96%B9%E6%B3%95]]】 或 【[[Euler method|https://en.wikipedia.org/wiki/Euler_method]]】),它的優點是容易理解,容易寫成程式碼,即使對高中生來說也不是問題。
* 但是這個簡單的方法有個重要的缺點:@@color:red;誤差太大@@,不夠實用!誤差有多大?把程式寫起來跑跑看,很快就會看到了。
<<<
# 簡單方法如果能把事情做好,自然沒有必要用複雜的方法,只是,@@現在這個簡單的方法,真的有把事情做好嗎?@@
# 把 \(dt\) 調小,模擬總時間不變,會看到誤差變小。試試看,這個方法要讓最後的誤差小到肉眼看不出來的話,得把 \(dt\) 調到多小才行?
<<<
!!! 單擺運動二
* [[單擺運動二]] 使用準確度較高的[[四階 Runge-Kutta 方法]],讓它跑久一點,你就會看出它為什麼廣被採用了。
* 這個方法雖然比歐拉法稍複雜一點,但應該是可以實用的方法裡面最簡單的了。
<<<
# 比照單擺運動一裡面的嘗試,在同樣總模擬時間的情況下,看看 \(dt\) 調到多小可以讓最後的誤差小到肉眼看不出來,跟第一種方法比起來如何?
# 用比較精準的那個範例,改變擺錘質量,看是否不影響週期?改變長度,看週期是否正比於擺長開根號 \(T \propto \sqrt l\)?
<<<
!!! 單擺運動三
* [[單擺運動三]] 採用 [[四階 Runge-Kutta 方法]],計算【非平面擺動】的結果。
* 關於非平面擺動,一般教科書中只會討論其中的一種特例:圓錐擺,理由當然是因為【它最簡單】,可以用手算,其它情況都無法用手算出結果,所以避開不談。
* 這個範例中我們會看到,用電腦做數值計算的話,''非平面跟平面擺動的差別只有【初始速度不一樣】而已'',毫無難易度的差別!
** 實際上我們如何讓一個單擺做平面擺動或者非平面擺動?不就是給不一樣的初速度而已嗎?
<<<
# 這個範例直接設定圓錐擺的初始速度,執行看看是否為圓錐擺的軌跡?
# 試著改變初始速度,包括大小與方向,看看有什麼不同的軌跡。
# 把 [[拋體運動三]] 裡的二次方空氣阻力加進來,對單擺軌跡有什麼影響?對週期有什麼影響?
<<<
!!! 學習成效
這幾個範例中我們學習到
# 歐拉法雖然簡單,但是誤差太大,在實際問題上並不適用。
** 歐拉法要算到誤差夠小也不是不行,只是要花掉太多的時間,不實用。
# [[四階 Runge-Kutta 方法]] 可以說是實用方法中最簡單的一種,可以在很合理的時間內算出夠精準的結果,不用等到地老天荒!
# 對數值模擬而言,平面擺與非平面擺只差在初始速度,完全沒有難易度的差別。
<<<
# 根據經驗,不少學生第一次接觸四階 ~Runge-Kutta 方法時都覺得麻煩而不願採用,寧可選擇簡單易懂的歐拉法。這個範例的目的便是展示出歐拉法的主要缺點,不需多做解釋就能一目了然,希望讓第一次接觸的學生能更容易接受,進而願意採用較為精準的演算法。
# 另外,不少學生看到可以模擬圓錐擺會感到驚奇,很可能是因為傳統用手做計算的學習方式很難處理非平面擺動,所以覺得那應該是很難的緣故。不過在發現原來只要改變初始速度就能擺出各種花樣時,就會覺得這一點也不是難事。
<<<
<<foldHeadings>>
@@font-size:1.5em;''模擬範例 -- 單擺運動一:最簡單的計算,注意誤差!''@@
----
這個例子裡我們要模擬【單擺】運動,主要想呈現出演算法本身的好壞。最簡單能夠用來模擬運動的公式,就是如 [[拋體運動一]] 裡使用的 \begin{aligned}\vec r_\text{next} &= \vec r_\text{now} + \vec v_\text{now} dt \\ \vec v_\text{next} &= \vec v_\text{now} + \vec a_\text{now} dt.\end{aligned} 這個公式極為容易理解,也極為容易寫成程式碼,非常適合讓初學者開始。我們在前面的 拋體運動 [[一|拋體運動一]]、[[二|拋體運動二]]、[[三|拋體運動三]] 等三個範例中都是使用上面這兩條公式。
>這個範例我們仍然會使用這組公式,目的是要跟更精準的方法做比較。
>這個方法稱為 [[歐拉法|https://zh.wikipedia.org/wiki/%E6%AC%A7%E6%8B%89%E6%96%B9%E6%B3%95]] 或 [[Euler method|https://en.wikipedia.org/wiki/Euler_method]],是數值方法中最簡單,大約在 1768 年左右發展出來的方法。
__這樣簡單容易的公式有個很重要的缺點:@@color:red;誤差太大!@@__ 下面的範例可以很容易看出,運動開始不久後,擺錘的軌跡就會明顯變粗,而且會越來越粗!那就是這個方法的計算誤差太大所造成的。

這組簡單公式因為誤差太大,在實際問題上我們通常不會採用,而是選擇更為精準的計算方法。更精準的方法當然不只一種,不過其中 ''夠精準'' 又 ''夠簡單'' 的,應該就是 [[四階 Runge-Kutta 方法]],我們會在 [[單擺運動二]] 這個範例中使用它,其結果可以拿來跟這個範例比較看看。

>@@如果你是第一次寫程式,下面的範例可能讓你覺得有點長,不過請放心,裡面有詳細的說明。''最下方的畫面可以試著跑跑看''。@@
@@color:red;font-size:1.5em;底下的標題列按一下可以顯示隱藏內容@@
! 設計場景
//{{{
# from vpython import *		# 使用安裝版的話,這是必要的第一行(得自己寫),開啟 3D 繪圖外掛
GlowScript 3.2 VPython		# 使用 GlowScript 的話,這行會自動產生(不一定是 3.2 無妨),不要更改這一行!

'''
---------------------------------------------------------------------------------------------------------
設計場景
---------------------------------------------------------------------------------------------------------
'''

scene.range=1.2                             # 設定攝影機範圍在正負 1.2 公尺
scene.g = vector(0,-9.8,0)                  # 重力加速度

ceiling = box(                              # 產生一個扁平盒子代表天花板,取名為 ceiling(就是天花板)
    width=2,                                # 寬度(z-方向)為 2 公尺
	height=0.022,                           # 高度(y-方向)為 0.022 公尺(2.2 公分)
	length=2,                               # 長度(x-方向)為 2 公尺
	opacity=0.3                             # 【不】透明度為 0.3(頗透明)
)

rod = cylinder(                             # 產生一個細圓柱代表擺繩,取名為 rod(桿)
	axis=vector(0,-1,0),                    # 指向 -y 方向(下方)
	radius=0.0005,                          # 半徑為 0.0005 公尺(0.5 公厘)
	opacity=0.5                             # 【不】透明度為 0.5(半透明)
)

bob = sphere(                               # 產生一個球代表擺錘,取名為 bob(就是擺錘)
	radius=0.025,                           # 半徑為 0.025 公尺(2.5 公分)
	make_trail=True,                        # 要留下軌跡
	opacity=0.5                             # 【不】透明度為 0.5
)
//}}}
!!!! 新手提示
<<<
嗯...這裡看來沒啥好提示的了...如果還沒熟悉這些,可以
# 看看 [[VPython 3D Objects|http://www.vpython.org/contents/docs_vp5/visual/primitives.html]] 這個網頁,或者
# 看看 [[拋體運動一]]、[[拋體運動二]] 裡面的新手提示。
<<<
! 初始條件

!!!! 定義計算加速度的函數
//{{{
def calcA(r,v,t):                           # 定義計算加速度的函數
    axis = r - ceiling.pos                  # 擺繩向量,從懸掛點到擺錘
    uaxis = axis.norm()                     # 單位向量,從懸掛點指向擺錘
    ac = mag2(v)/mag(axis)                  # 圓周運動所需要的向心加速度
    at = scene.g-scene.g.dot(uaxis)*uaxis   # 重力產生的切線加速度
    return at-ac*uaxis                      # 合起來便是擺錘的加速度
//}}}
!!!! 實際設定初始條件
//{{{
'''
---------------------------------------------------------------------------------------------------------
初始條件
---------------------------------------------------------------------------------------------------------
'''

rod.mass = 0.01                             # 擺繩質量 (kg)
rod.L0 = 1                                  # 擺繩原長 (m)
rod.theta0 = 20.0*pi/180.0			        # 初始 theta 角(從 -y 軸量起),換算為弧度
rod.theta0 += pi*3/2						# 換算成從 x-軸量起的角度(這是一般的角度定義,多數 3D 繪圖軟體是照此定義)
											#	-y 軸相對於 x-軸的角度就是 3/2 pi,所以加上這個角度

bob.pos=ceiling.pos + vector(				# 設定擺錘初始位置為天花板的位置(其中心)加上沿著擺繩的向量
    rod.L0*cos(rod.theta0),          		# 根據擺繩的初始角度來計算擺錘的初始位置
    rod.L0*sin(rod.theta0),
    0
)
bob.mass = 0.2                              # 擺錘質量 (kg)
bob.velocity = vector(0,0,0)                # 初速度 (m/s)
bob.acceleration = calcA(                   # 初始加速度 (m/s^2),calcA 是另外定義的一個函數(見下方細步計算)
    bob.pos,bob.velocity,0
)

rod.axis = bob.pos-ceiling.pos              # 調整擺繩讓它連結擺錘與天花板
//}}}
!!!! 新手提示
<<<
* 注意:通常討論單擺運動的 \(\theta\) 角是從 -\(z\)-軸開始量起的,而我們這裡的 \(\theta\) 是按照球座標的定義,從 +\(z\)-軸開始量起的。
* 不熟悉球座標定義的話,可以參考維基百科 [[球座標系|https://zh.wikipedia.org/wiki/%E7%90%83%E5%BA%A7%E6%A8%99%E7%B3%BB]] 或者 [[Spherical coordinate system|https://en.wikipedia.org/wiki/Spherical_coordinate_system]]。
* Python 的三角函數(可能所有電腦語言都一樣)一定要使用弧度而不是角度做計算,不熟悉弧度的話可以參考維基百科 [[弧度|https://zh.wikipedia.org/wiki/%E5%BC%A7%E5%BA%A6]] 或是 [[Radian|https://en.wikipedia.org/wiki/Radian]]。
<<<
! 細步計算
<<tiddler "單擺運動一 JS Codes##細步計算">>
!!!! 計算加速度
> 前面已經定義過,請參考【初始條件】一節。
!!!! 計算每一步
//{{{
t = 0
dt = 0.001                                  # 時間間隔
while t < 50:
    rate(1000)                              # 每秒最多畫面數,慢一點方便觀察
    bob.pos += bob.velocity*dt              # 計算下一個位置
    bob.velocity += bob.acceleration*dt     # 計算下一個速度
    bob.acceleration = calcA(               # 計算加速度
        bob.pos,bob.velocity
    )
    #scene.center = bob.pos                 # 攝影機聚焦在擺錘(可以極近距離觀察)
    rod.axis = bob.pos - ceiling.pos        # 更新擺繩讓它重新連結天花板與擺錘(試著把這行改成註解就可以明白)
											#	電腦不會自動更新擺繩,當擺錘位置改變時,擺繩依舊停在原地不動
											#	所以我們在擺錘位置更改之後,得重新讓擺繩連到擺錘上,不然畫面
											#	會變得很奇怪!
    t += dt                                 # 紀錄時間
//}}}
!!!! 新手提示
<<<
# 在【細步計算】的說明裡,有提到 \((\vec g \cdot \hat r)\hat r\) 是 \(\vec g\) 【''平行'' 於擺繩方向的分量】,這是因為向量計算中 \(\vec g \cdot \hat r\) 就是 \(\vec g\) 在 \(\hat r\) 方向的投影長度,而在這裡 \(\hat r\) 方向就是平行於擺繩的方向。
# 計算每一步的程式碼裡面,有一行 {{{scene.center = bob.pos}}},這是讓 ~VPython 的攝影機聚焦在擺錘身上,這樣的話就可以【按著滑鼠雙鍵前進】或用【滾輪】來將畫面拉到極近的距離觀察擺錘的軌跡。
** 在 [[GlowScript|http://www.glowscript.org]] 可以用滑鼠滾輪來拉近畫面,~VPython 6.0 好像不能用滾輪?
** 3D 繪圖軟體都有一台攝影機將畫面拍攝到螢幕上,~VPython 也是一樣。關於如何調整 ~VPython 的攝影機來改變畫面,可以參考 [[它的說明網頁|http://vpython.org/contents/docs/display.html]]。
<<<
<<foldHeadings>>
!!!! 線上模擬說明
<<<
底下的模擬程式可以玩玩看!
# 按【Start】按鈕開始模擬。過程中隨時可以按【Reset】按鈕重新模擬。
# 勾選 Auto CPF 可以加快模擬過程。
# 讓它跑一會,@@color:red;觀察擺錘的軌跡變粗的情形。@@
# 設著改變時間間隔 \(dt\),改大改小都試試,然後按 Reset 重新模擬,@@color:red;觀察看看不同的 \(dt\) 軌跡變粗的情形有什麼不一樣?@@
<<<
!!!! 線上模擬
* @@color:red;取消勾選右方 Auto CPF 可以慢很多,看清楚運動的每一步!@@
** 勾選起來可以快很多!
|單擺運動 JS 版本|c
|width:45%;<<tw3DCommonPanel "Flow Control">>|width:45%;<<tw3DCommonPanel "Label Statistics">>|
|<<tw3DCommonPanel "Simulation Time Control">>|<<tw3DCommonPanel "Air Drag">>|
|<<tw3DCommonPanel "Center of Mass Control">>|<<tw3DCommonPanel "Trail Control">>|
|<<tiddler "Pendulum Panel##Pivot Control">> / <<tiddler "Pendulum Panel##Bob Control">>|<<tiddler "Pendulum Panel##String Control">>|
|(CCW with downward vertical) <<tw3DCommonPanel "Initial Theta">> / <<tiddler "Pendulum Panel##Period Label">>||
|<<tw3DCommonPanel "Plot0 Control">> / <<tw3DCommonPanel "DAQ Control">> / <<tw3DCommonPanel "Gravity Control">><br><<twDataPlot id:dataPlot0>>|<<tw3DCommonPanel "Plot1 Control">> / <<tw3DCommonPanel "Message">><br><<twDataPlot id:dataPlot1>>|
|>|<<tw3DScene [[單擺運動一 JS Codes]]>>|
>@@font-sze:0.8em;此線上模擬版本係由瀏覽器內建的 Javascript 寫成,以 [[THREE.js|https://threejs.org/]] 做 3D 繪圖,有興趣學習者可以自行上網以【Javascript 學習】、【Javascript 初學】、【Javascript 3D 繪圖】等關鍵字搜尋相關教學文章或影片。
/***
!!! 設計場景
***/
//{{{
cartesian.show(false);			// 隱藏直角座標系
//scene.camera.position.z *= 3;

let __tmpV = vector(),			// 公用的暫時向量(計算過程中使用)
	ceiling = box({				// 產生一個扁平盒子代表天花板,取名為 ceiling(就是天花板)
		width:2,				// 寬度(x-方向)為 2 公尺
		height:0.02,			// 高度(y-方向)為 0.02 公尺(2 公分)
		depth:2,				// 長度(z-方向)為 2 公尺
		opacity:0.3				// 【不】透明度為 0.3(頗透明)
	}),
	rod = cylinder({			// 產生一個細圓柱代表擺繩,取名為 rod(桿)
		axis:vector(0,-1,0),		// 指向 -y 方向(下方)
		radius:0.01,			// 半徑為 0.01 公尺(1 公分)
		opacity:0.5				// 【不】透明度為 0.5(半透明)
	});
rod.position.copy(ceiling.position).add(
	rod.getAxis(__tmpV).multiplyScalar(0.5)
);

let bob = sphere({				// 產生一個球代表擺錘,取名為 bob(就是擺錘)
	radius:0.15,				// 半徑為 0.15 公尺(15 公分)
	make_trail:true,			// 要留下軌跡
	opacity:0.5					// 【不】透明度為 0.5
});

//spherical = SphericalCoordinateSystem().show(false).showComponents(true);
chkGravity.checked = true;		// 開啟模擬重力場
chkGravity.disabled = true;		// 禁止關閉重力場
chkAirDrag.checked = false;		// 關閉空氣阻力
chkAirDrag.disabled = true;		// 禁止開啟空氣阻力(後面的範例可以開啟)
txtTheta0.value = 30;			// 初始角度
txtdT.value = 0.001;			// 加大時間間隔,讓誤差更快顯現
txtTmax.value = 50;				// 模擬總時間
txtStringLength.value = 1;
//}}}
/***
!!! 初始條件
***/
//{{{
let __axis = vector();
scene.init = () => {
	rod.mass = +txtStringMass.value;			// 擺繩質量
	rod.L0 = +txtStringLength.value;			// 擺繩原長
	rod.theta0 = txtTheta0.value*Math.PI/180.0;	// 初始角度
	bob.setRadius(+txtBobRadius.value);		// 擺錘半徑
	bob.clearTrail();				// 清除軌跡
	getTrailParam(bob);
	bob.position.copy(				// 設定擺錘初始位置
		ceiling.position
	).add(__tmpV.set(					// 根據擺繩的初始角度來計算
		Math.cos(Math.PI*3/2+rod.theta0),
		Math.sin(Math.PI*3/2+rod.theta0),
		0
	).multiplyScalar(rod.L0));
	//spherical.updateStatus(bob.position);
	bob.mass = +txtBobMass.value;			// 擺錘質量
	bob.velocity = vector();			// 初速度
	bob.acceleration = vector();			// 加速度

	rod.setAxis(					// 調整擺繩讓它連結擺錘與天花板
		__axis.copy(bob.position).sub(ceiling.position)
	).position.copy(ceiling.position).add(__axis.multiplyScalar(0.5));
	calcA(						// 計算初始加速度
		bob.position,bob.velocity,bob.acceleration
	);
}
//}}}
/***
!!! 細步計算
單擺運動時擺錘的受力 \(\vec F_\text{bob}\),在【''不考慮空氣阻力''】的情況下,有重力 \(m \vec g\) 和擺繩的張力 \(\vec T\), \[\vec F_\text{bob} = m\vec g + \vec T.\] 其中擺繩張力,若【''假設擺長不變''】,可以寫為 \[\vec T = \left(mg\cos\theta + m{v^2 \over r}\right)(-\hat r),\] 其中 \(\vec r\) 是從支點到擺錘位置的向量。可以看出擺錘的加速度為 \begin{aligned}\vec a_\text{bob} = \vec g + {\vec T \over m} &= \vec g + \left(g\cos\theta+{v^2 \over r}\right)(-\hat r) \\ &= \vec g + \left(\vec g \cdot \hat r + {v^2 \over r}\right)(-\hat r) \\ &= (\vec g - (\vec g \cdot \hat r)\hat r) - {v^2 \over r}\hat r.\end{aligned} 上式第一項裡面的 \((\vec g \cdot \hat r)\hat r\) 就是【重力加速度 ''平行'' 於擺繩方向的分量】,所以 \(\vec g - (\vec g \cdot \hat r)\hat r\) 便是【重力加速度 ''垂直'' 於擺繩方向的分量】,也就是說,第一項的意義就是擺錘因重力而產生的 切線加速度。第二項則明顯是圓周運動所需的向心加速度。
!!!! 細步計算程式碼
這個範例裡我們將細步計算程式碼分成【計算加速度】以及【計算運動過程】
!!!!! 計算加速度
***/
//{{{
let __uaxis = vector(),
	__at = vector(),
	g = vector(0,-9.8,0);
const calcA = (r,v,a) => {				// 定義計算加速度的函數
	// 假設擺繩長度不變,那麼擺錘的 瞬時 運動都是圓周運動
	// 如此一來擺錘的加速度便是 圓周運動所需的向心加速度 加上
	// 重力產生的 切線加速度

	// 向心加速度是沿著擺繩的方向,所以我們先把擺繩的向量算出來
	__axis.copy(r).sub(ceiling.position);		// 擺繩向量,從懸掛點到擺錘
	__uaxis.copy(__axis).normalize();		// 其單位向量,從懸掛點到擺錘

	// 根據圓周運動公式計算向心加速度
	let ac = v.lengthSq()/__axis.length();		// 圓周運動所需要的向心加速度

	// 然後計算重力產生的切線加速度
	__at.copy(g).sub(				// 重力產生的切線加速度
		__tmpV.copy(__uaxis).multiplyScalar(
			g.dot(__uaxis)
		)
	);

	if(!a) a = vector();
	return a.copy(__uaxis)				// 合起來便是擺錘的加速度
		.multiplyScalar(-ac).add(__at);
}
//}}}
/***
!!!!! 計算運動過程
***/
//{{{
scene.update = (t_cur,dt) => {
	bob.velocity.add(				// 計算下一個速度
		__tmpV.copy(bob.acceleration).multiplyScalar(dt)
	);
	bob.position.add(				// 計算下一個位置
		__tmpV.copy(bob.velocity).multiplyScalar(dt)
	);
	//spherical.updateStatus(bob.position);
	rod.setAxis(					// 更新擺繩
		__axis.copy(bob.position).sub(ceiling.position)
	).position.copy(ceiling.position).add(__axis.multiplyScalar(0.5));
	calcA(						// 計算加速度
		bob.position,bob.velocity,bob.acceleration
	);
}
//}}}
@@font-size:1.5em;''模擬範例 -- 單擺運動三:三維的擺動''@@
----
在 [[單擺運動一]] 和 [[單擺運動二]] 裡面我們比較了【歐拉法】和 [[四階 Runge-Kutta 方法]],並由此確認 [[四階 Runge-Kutta 方法]] 是我們應該採用的。
>本系列後續所有的範例,我們都會採用 [[四階 Runge-Kutta 方法]] 做模擬計算。
這個範例我們要凸顯出另外一個數值計算的優勢:非平面擺動。傳統的物理教材裡對於非平面的擺動最多只談圓錐擺,因為它是唯一簡單到可以用手算的情況。對於其它非平面擺動的狀況,由於 \(\theta\) 角與 \(\phi\) 角都在改變,我們會採取【拉格朗日法(Lagrange's method)】來分別獲得 \(\theta\) 和 \(\phi\) 的運動方程,再對它們聯立求解。

''這個過程概念上簡單,做起來很煩。''

這個範例我們會看到,''只要初始速度設定正確''就是圓錐擺,__完全不用改變程式碼__。

其它非平面擺動的狀況呢?當然也是改變初始速度就行了!
>實際上要讓一個單擺做平面擺動或非平面擺動,不就是只要改變初始速度而已嗎?
@@color:red;font-size:1.5em;底下的標題列按一下可以顯示隱藏內容@@
! 設計場景
<<tiddler "單擺運動一##設計場景">>
!!!! 新手提示
<<<
這部分和[[單擺運動一]]的設計場景是一樣的。
<<<
! 初始條件
> 計算圓錐擺速率的函數
//{{{
def conicalSpeed(L,A):
    # calculate the speed required for a conical pendulum with
	# length L and at an angle A.
	return sqrt(L*9.8*sin(A)*tan(A))
//}}}
//{{{
rod.mass = 0.01                             # 擺繩質量
rod.L0 = 1                                  # 擺繩原長
rod.theta0 = 20.0*pi/180.0                  # 初始 theta 角(從 -z 軸量起),可隨意更改看效果
rod.phi0 = 0.0*pi/180.0                     # 初始 phi 角(經度),可隨意更改看效果

bob.pos=ceiling.pos+vector(                 # 設定擺錘初始位置
    rod.L0*sin(pi-rod.theta0)*cos(rod.phi0),# 根據擺繩的初始角度來計算
    rod.L0*sin(pi-rod.theta0)*sin(rod.phi0),# 由於單擺的 theta 角定義是從 -z 軸量起的,但這裡我們用的是球座標
    cos(pi-rod.theta0)						# 公式來計算位置,角度得從 +z 軸量起,所以要換算成 pi-theta
)
bob.mass = 0.2                              # 擺錘質量 0.15 kg (150 g)

rod.axis = bob.pos-ceiling.pos              # 調整擺繩讓它連結擺錘與天花板
//}}}
> 初速度和 [[單擺運動二]] 有所不同
//{{{
bob.velocity = conicalSpeed(                # 圓錐擺的速率
    rod.L0,rod.theta0
)*3*rod.axis.cross(scene.g).norm()          # 沿著 y-方向
//}}}
//{{{
bob.acceleration = calcA(                   # 初始加速度
    bob.pos,bob.velocity,0
)
//}}}
!!!! 新手提示
<<<
這部分和[[單擺運動一]]的初始條件只有一個地方不同,就是初始速度(以及定義了一個計算圓錐擺速率的函數)。
<<<
! 細步計算
在 [[單擺運動一]] 裡面我們解釋了單擺的加速度該如何計算,這裡就不再重複。這個範例的主要目標在於展示【非平面擺動】和平面擺動的差別就只是初始速度而已。
!!!! 四階 ~Runge-Kutta
//{{{
'''
開始:四階 Runge-Kutta 方法
'''

def nextValue(r,v,A,t,dt,a_now = None):
    # 根據現在的位置、速度,計算經過時間 dt 之後的位置、速度、
    # 使用四階榮格-庫塔(Runge-Kutta)演算法
    #       r: 現在位置
    #    	v: 現在速度
	#		A: 計算加速度的函數 function A(r,v,t),是根據現在的位置、速度、時間等因素來計算的
    #           加速度的計算來自受力,只要會計算受力,就會計算加速度
    #
    #           物理上和位能有關的力都和位置有關,例如:重力、彈力、電力、磁力等,所以計算
    #           加速度的函數需要知道物體的位置。
    #
    #           有些和位能無關的力,如空氣阻力,會和速度有關,所以計算加速度的函數可能需要
    #           知道物體的速度
    #
	#           還有一些力可能和位置、速度無關,如粗糙度很一致的接觸面摩擦力,這些可以直接
    #           寫在計算加速度的函數中
    #
    #           有些力會和時間有關,例如週期性的驅動力,如果計算中有這樣的情況,就會需要
    #           知道現在的時間
    #
    #		t: 現在時間
	#		dt: 時間間隔
	# 		a_now: (非必要)現在的壓速度,如果有傳進來的話,可以是純量也可以是向量。

	r1 = r
	v1 = v
	a1 = a_now
	if a1 is None: a1 = A(r1,v1,t)

	dt2 = dt*0.5
	r2 = r+dt2*v1
	v2 = v+dt2*a1
	a2 = A(r2,v2,t+dt2)

	r3 = r+dt2*v2
	v3 = v+dt2*a2
	a3 = A(r3,v3,t+dt2)

	r4 = r+dt*v3
	v4 = v+dt*a3
	a4 = A(r4,v4,t+dt)

	dt /= 6.0
	r += (v1+(v2+v3)*2.0+v4)*dt
	v += (a1+(a2+a3)*2.0+a4)*dt

	return (r,v,A(r,v,t+dt))

'''
結束:四階 Runge-Kutta 方法
'''
//}}}
!!!! 計算加速度
<<tiddler "單擺運動一##計算加速度">>
!!!! 計算每一步
//{{{
t = 0
dt = 0.001                                  # 時間間隔
while t < 50:
    rate(1000)                              # 每秒最多畫面數,慢一點方便觀察
    
    bob.pos,bob.velocity,bob.acceleration = nextValue(
        bob.pos,bob.velocity,calcA,t,dt
    )
    
    #scene.center = bob.pos                 # 攝影機聚焦在擺錘(可以極近距離觀察)
    rod.axis = bob.pos - ceiling.pos        # 更新擺繩
    t += dt                                 # 紀錄時間
//}}}
!!!! 新手提示
<<<
* 這裡的【計算加速度】以及【計算每一步】都和 [[單擺運動二]] 裡面是一樣的,完全沒有更動。
<<<
<<foldHeadings>>
!!!! 線上模擬說明
<<<
底下的模擬程式可以玩玩看!
# 按【Start】按鈕開始模擬。過程中隨時可以按【Reset】按鈕重新模擬。
# 勾選 Auto CPF 可以加快模擬過程。
# @@color:blue;直接按【Start】開始模擬,觀察軌跡是否為''圓錐擺''的軌跡。@@
# ''試著改變初始速度(包含@@color:red;大小與方向@@),按【Reset】重新模擬,比較看看結果有什麼不一樣?''
<<<
!!!! 線上模擬
* @@color:red;取消勾選右方 Auto CPF 可以慢很多,看清楚運動的每一步!@@
** 勾選起來可以快很多!
|單擺運動模擬|c
|width:45%;<<tw3DCommonPanel "Flow Control">>|width:45%;<<tw3DCommonPanel "Label Statistics">>|
|<<tw3DCommonPanel "Simulation Time Control">>|<<tw3DCommonPanel "Air Drag">>|
|<<tw3DCommonPanel "Center of Mass Control">>|<<tw3DCommonPanel "Trail Control">>|
|<<tiddler "Pendulum Panel##Pivot Control">> / <<tiddler "Pendulum Panel##Period Label">>|<<tiddler "Pendulum Panel##String Control">>|
|<<tiddler "Pendulum Panel##Bob Control">>|<<tiddler "Pendulum Panel##Initial Velocity">>|
|<<tw3DCommonPanel "Plot0 Control">> / <<tw3DCommonPanel "DAQ Control">> / <<tw3DCommonPanel "Gravity Control">><br><<twDataPlot id:dataPlot0>>|<<tw3DCommonPanel "Plot1 Control">> / <<tw3DCommonPanel "Message">><br><<twDataPlot id:dataPlot1>>|
|>|<<tw3DScene [[單擺運動三 JS Codes]]>>|
>@@font-sze:0.8em;此線上模擬版本係由瀏覽器內建的 Javascript 寫成,以 [[THREE.js|https://threejs.org/]] 做 3D 繪圖,有興趣學習者可以自行上網以【Javascript 學習】、【Javascript 初學】、【Javascript 3D 繪圖】等關鍵字搜尋相關教學文章或影片。
/***
!!! 設計場景
***/
//{{{
cartesian.show(false);			// 隱藏直角座標系
scene.camera.position.multiplyScalar(0.7);

let __tmpV = vector(),			// 公用的暫時向量(計算過程中使用)

	ceiling = box({				// 產生一個扁平盒子代表天花板,取名為 ceiling(就是天花板)
		width:2,				// 寬度(x-方向)為 2 公尺
		height:2,				// 高度(y-方向)為 2 公尺
		depth:0.02,				// 長度(z-方向)為 0.02 公尺(2 公分)
		opacity:0.3				// 【不】透明度為 0.3(頗透明)
	}),

	rod = cylinder({			// 產生一個細圓柱代表擺繩,取名為 rod(桿)
		axis:vector(0,0,-1),	// 指向 -z 方向(下方)
		radius:0.01,			// 半徑為 0.01 公尺(1 公分)
		opacity:0.5				// 【不】透明度為 0.5(半透明)
	});
rod.position.copy(ceiling.position).add(
	rod.getAxis(__tmpV).multiplyScalar(0.5)
);

let bob = sphere({				// 產生一個球代表擺錘,取名為 bob(就是擺錘)
	radius:0.15,				// 半徑為 0.15 公尺(15 公分)
	make_trail:true,			// 要留下軌跡
	opacity:0.5					// 【不】透明度為 0.5
});

chkGravity.checked = true;		// 開啟模擬重力場
chkGravity.disabled = true;		// 禁止關閉重力場
txtPhi0.value = 0				// 讓初始位置在 x-軸
txtTheta0.value = 20			// 初始角度 20 度
txtdT.value = 0.001				// 加大時間間隔,讓誤差更快顯現
txtTmax.value = 50				// 模擬總時間
txtStringLength.value = 1;
//}}}
/***
!!!! conicalSpeed(L,\(\theta\) )
<<<
Calculate the speed required for a conical pendulum with length \(L\) and at an angle \(\theta\),
\[v_\text{conical} = \sqrt{Lg\sin\theta\tan\theta}.\]
<<<
***/
//{{{
const conicalSpeed = (L,theta) => {
	return Math.sqrt(L*9.8*Math.sin(theta)*Math.tan(theta));
}
txtInitialSpeed.value = conicalSpeed(+txtStringLength.value,txtTheta0.value*Math.PI/180);
//}}}
/***
!!! 初始條件
***/
//{{{
let __axis = vector(),
	n_init = 0;
scene.init = () => {
	rod.mass = +txtStringMass.value;			// 擺繩質量
	rod.L0 = +txtStringLength.value;			// 擺繩原長
	rod.theta0 = txtTheta0.value*Math.PI/180.0;	// 初始 theta 角(從 -z 軸量起)
	rod.phi0 = txtPhi0.value*Math.PI/180.0;		// 初始 phi 角(經度)
	bob.setRadius(+txtBobRadius.value);			// 擺錘半徑
	bob.clearTrail();							// 清除軌跡
	bob.position.copy(							// 設定擺錘初始位置
		ceiling.position
	).add(__tmpV.set(							// 根據擺繩的初始角度來計算
		Math.sin(Math.PI-rod.theta0)
			*Math.cos(rod.phi0),				// 由於單擺的 theta 角定義是從 -z 軸量起的,	
		Math.sin(Math.PI-rod.theta0)
			*Math.sin(rod.phi0),				// 但這裡我們用的是球座標公式來計算位置,角度得從
		Math.cos(Math.PI-rod.theta0)			// +z 軸量起,所以要換算成 pi-theta
	).multiplyScalar(rod.L0));
	bob.mass = +txtBobMass.value;				// 擺錘質量

	rod.setAxis(								// 調整擺繩讓它連結擺錘與天花板
		__axis.copy(bob.position).sub(ceiling.position)
	).position.copy(ceiling.position).add(__axis.multiplyScalar(0.5));
	bob.velocity = __axis.clone().cross(scene.g).normalize()
		.multiplyScalar(+txtInitialSpeed.value);
	bob.acceleration = vector();				// 加速度
	calcA(										// 計算初始加速度
		bob.position,bob.velocity,0,bob.acceleration
	);
	return ++n_init > 1;
}
//}}}
/***
!!! 細步計算
<<tiddler "單擺運動一 JS Codes##細步計算">>
!!!! 細步計算程式碼
***/
//{{{
let __uaxis = vector(),
	__at = vector();
const calcA = (r,v,t,a) => {						// 定義計算加速度的函數
	// 假設擺繩長度不變,那麼擺錘的 瞬時 運動都是圓周運動
	// 如此一來擺錘的加速度便是 圓周運動所需的向心加速度 加上
	// 重力產生的 切線加速度

	// 向心加速度是沿著擺繩的方向,所以我們先把擺繩的向量算出來
	__axis.copy(r).sub(ceiling.position);			// 擺繩向量,從懸掛點到擺錘
	__uaxis.copy(__axis).normalize();				// 其單位向量,從懸掛點到擺錘

	// 根據圓周運動公式計算向心加速度
	let ac = v.lengthSq()/__axis.length();			// 圓周運動所需要的向心加速度

	// 然後計算重力產生的切線加速度
	__at.copy(scene.g).sub(							// 重力產生的切線加速度
		__tmpV.copy(__uaxis).multiplyScalar(
			scene.g.dot(__uaxis)
		)
	);

	if(!a) a = vector();
	a.copy(__uaxis).multiplyScalar(-ac).add(__at);	// 合起來便是擺錘的加速度

	if(chkAirDrag.checked)
		a.add(										// 空氣阻力
			$tw.physics.airDragSphere(v,bob.getRadius(),__tmpV)
				.multiplyScalar(1/bob.mass)
		);
	return a;
}

scene.update = (t_cur,dt) => {
	let e0 = +txtTolerance.value,
		adaptive = chkAdaptive.checked,
		delta = $tw.physics.nextPosition(
			bob,calcA,t_cur,dt,adaptive,e0
		);
	if(adaptive) setdT(delta[2]);
	rod.setAxis(									// 更新擺繩
		__axis.copy(bob.position).sub(ceiling.position)
	).position.copy(ceiling.position).add(__axis.multiplyScalar(0.5));
}
//}}}
@@font-size:1.5em;''模擬範例 -- 單擺運動二:夠實用的【四階 ~Runge-Kutta】方法''@@
----
在[[單擺運動一]]裡面我們凸顯出最簡單方法的問題:誤差太大,這裡我們要呈現的,是一個誤差夠小,夠實用方法中可能是最簡單的一個:[[四階 Runge-Kutta 方法]]。

這個方法誤差夠小,夠實用,也夠簡單,在實際問題中廣被採用。兩個方法差多少?把程式寫出來比較看看,很明顯的!

@@color:red;font-size:1.5em;底下的標題列按一下可以顯示隱藏內容@@
! 設計場景
<<tiddler "單擺運動一##設計場景">>
!!!! 新手提示
<<<
這部分和[[單擺運動一]]的設計場景是一樣的。
<<<
! 初始條件

!!!! 定義計算加速度的函數
<<tiddler "單擺運動一##定義計算加速度的函數">>
!!!! 實際設定初始條件
<<tiddler "單擺運動一##實際設定初始條件">>
!!!! 新手提示
<<<
這部分和[[單擺運動一]]的初始條件是一樣的。
<<<
! 細步計算
在 [[單擺運動一]] 裡面我們解釋了單擺的加速度該如何計算,這裡就不再重複。這個範例的主要目標在於展示 [[四階 Runge-Kutta 方法]] 的實用性:可以在合理時間內計算出夠精準的結果。
!!!! 四階 ~Runge-Kutta
//{{{
'''
開始:四階 Runge-Kutta 方法
'''

def nextValue(r,v,A,t,dt,a_now = None):
    # 根據現在的位置、速度,計算經過時間 dt 之後的位置、速度、
    # 使用四階榮格-庫塔(Runge-Kutta)演算法
    #       r: 現在位置
    #    	v: 現在速度
	#		A: 計算加速度的函數 function A(r,v,t),是根據現在的位置、速度、時間等因素來計算的
    #           加速度的計算來自受力,只要會計算受力,就會計算加速度
    #
    #           物理上和位能有關的力都和位置有關,例如:重力、彈力、電力、磁力等,所以計算
    #           加速度的函數需要知道物體的位置。
    #
    #           有些和位能無關的力,如空氣阻力,會和速度有關,所以計算加速度的函數可能需要
    #           知道物體的速度
    #
	#           還有一些力可能和位置、速度無關,如粗糙度很一致的接觸面摩擦力,這些可以直接
    #           寫在計算加速度的函數中
    #
    #           有些力會和時間有關,例如週期性的驅動力,如果計算中有這樣的情況,就會需要
    #           知道現在的時間
    #
    #		t: 現在時間
	#		dt: 時間間隔
	# 		a_now: (非必要)現在的壓速度,如果有傳進來的話,可以是純量也可以是向量。

	r1 = r
	v1 = v
	a1 = a_now
	if a1 is None: a1 = A(r1,v1,t)

	dt2 = dt*0.5
	r2 = r+dt2*v1
	v2 = v+dt2*a1
	a2 = A(r2,v2,t+dt2)

	r3 = r+dt2*v2
	v3 = v+dt2*a2
	a3 = A(r3,v3,t+dt2)

	r4 = r+dt*v3
	v4 = v+dt*a3
	a4 = A(r4,v4,t+dt)

	dt /= 6.0
	r += (v1+(v2+v3)*2.0+v4)*dt
	v += (a1+(a2+a3)*2.0+a4)*dt

	return (r,v,A(r,v,t+dt))

'''
結束:四階 Runge-Kutta 方法
'''
//}}}
!!!! 計算加速度
<<tiddler "單擺運動一##計算加速度">>
!!!! 計算每一步
//{{{
t = 0
dt = 0.001                                  # 時間間隔
while t < 50:
    rate(1000)                              # 每秒最多畫面數,慢一點方便觀察
    
    bob.pos,bob.velocity,bob.acceleration = nextValue(
        bob.pos,bob.velocity,calcA,t,dt
    )
    
    #scene.center = bob.pos                  # 攝影機聚焦在擺錘(可以極近距離觀察)
    rod.axis = bob.pos - ceiling.pos        # 更新擺繩
    t += dt                                 # 紀錄時間
//}}}
!!!! 新手提示
<<<
* 這裡的【計算加速度】和 [[單擺運動一]] 裡面是一樣的。
* 不過【計算每一步】裡面改成以 [[四階 Runge-Kutta 方法]] 寫成的 {{{nextValue(r,v,A,t,dt,a)}}} 來計算下一個時間點的位置與速度。
<<<
<<foldHeadings>>
!!!! 線上模擬說明
<<<
底下的模擬程式可以玩玩看!
# 按【Start】按鈕開始模擬。過程中隨時可以按【Reset】按鈕重新模擬。
# 勾選 Auto CPF 可以加快模擬過程。
# ''試著跟範例一採用同樣的參數,比較看看結果有什麼不一樣?''
<<<
!!!! 線上模擬
* @@color:red;取消勾選右方 Auto CPF 可以慢很多,看清楚運動的每一步!@@
** 勾選起來可以快很多!
|單擺運動模擬|c
|width:45%;<<tw3DCommonPanel "Flow Control">>|width:45%;<<tw3DCommonPanel "Label Statistics">>|
|<<tw3DCommonPanel "Simulation Time Control">>|<<tw3DCommonPanel "Air Drag">>|
|<<tw3DCommonPanel "Center of Mass Control">>|<<tw3DCommonPanel "Trail Control">>|
|<<tiddler "Pendulum Panel##Pivot Control">> / <<tiddler "Pendulum Panel##Bob Control">>|<<tiddler "Pendulum Panel##String Control">>|
|(CCW with downward vertical) <<tw3DCommonPanel "Initial Theta">> / <<tiddler "Pendulum Panel##Period Label">>||
|<<tw3DCommonPanel "Plot0 Control">> / <<tw3DCommonPanel "DAQ Control">> / <<tw3DCommonPanel "Gravity Control">><br><<twDataPlot id:dataPlot0>>|<<tw3DCommonPanel "Plot1 Control">> / <<tw3DCommonPanel "Message">><br><<twDataPlot id:dataPlot1>>|
|>|<<tw3DScene [[單擺運動二 JS Codes]]>>|
>@@font-sze:0.8em;此線上模擬版本係由瀏覽器內建的 Javascript 寫成,以 [[THREE.js|https://threejs.org/]] 做 3D 繪圖,有興趣學習者可以自行上網以【Javascript 學習】、【Javascript 初學】、【Javascript 3D 繪圖】等關鍵字搜尋相關教學文章或影片。
/***
!!! 設計場景
***/
//{{{
cartesian.show(false);			// 隱藏直角座標系
//scene.camera.position.z *= 3;

let __tmpV = vector(),			// 公用的暫時向量(計算過程中使用)
	ceiling = box({				// 產生一個扁平盒子代表天花板,取名為 ceiling(就是天花板)
		width:2,				// 寬度(x-方向)為 2 公尺
		height:0.02,			// 高度(y-方向)為 0.02 公尺(2 公分)
		depth:2,				// 長度(z-方向)為 2 公尺
		opacity:0.3				// 【不】透明度為 0.3(頗透明)
	}),
	rod = cylinder({			// 產生一個細圓柱代表擺繩,取名為 rod(桿)
		axis:vector(0,-1,0),	// 指向 -y 方向(下方)
		radius:0.01,			// 半徑為 0.01 公尺(1 公分)
		opacity:0.5				// 【不】透明度為 0.5(半透明)
	}),
	rod2 = cylinder({			// 產生一個細圓柱代表擺繩,取名為 rod(桿)
		axis:vector(0,-1,0),	// 指向 -y 方向(下方)
		radius:0.01,			// 半徑為 0.01 公尺(1 公分)
		color:0x00ffff,			// 青色
		opacity:0.5				// 【不】透明度為 0.5(半透明)
	}),
	bob = sphere({				// 產生一個球代表擺錘,取名為 bob(就是擺錘)
		radius:0.15,			// 半徑為 0.15 公尺(15 公分)
		make_trail:true,		// 要留下軌跡
		opacity:0.5				// 【不】透明度為 0.5
	}),
	bob2 = sphere({				// 產生一個球代表擺錘,取名為 bob(就是擺錘)
		radius:0.15,			// 半徑為 0.15 公尺(15 公分)
		make_trail:true,		// 要留下軌跡
		color:0x00ffff,			// 青色
		opacity:0.5				// 【不】透明度為 0.5
	});
rod.position.copy(ceiling.position).add(
	rod.getAxis(__tmpV).multiplyScalar(0.5)
);
rod2.position.copy(ceiling.position).add(
	rod.getAxis(__tmpV).multiplyScalar(0.5)
);

chkGravity.checked = true;		// 開啟模擬重力場
chkGravity.disabled = true;		// 禁止關閉重力場
chkAirDrag.checked = false;		// 關閉空氣阻力
chkAirDrag.disabled = true;		// 禁止開啟空氣阻力(後面的範例可以開啟)
txtTheta0.value = 30;			// 初始角度
txtdT.value = 0.001				// 加大時間間隔,讓誤差更快顯現
txtTmax.value = 50				// 模擬總時間
txtStringLength.value = 1;
txtTolerance.value = '1e-10';
//}}}
/***
!!! 初始條件
***/
//{{{
let __axis = vector(),
	n_init = 0,
	L2 = 0,
	theta = 0,
	omega = 0,
	theta2 = [],
	omega2 = [],
	alpha2 = [];
scene.init = () => {
	if(n_init > 1) n_init = 0;
	rod.mass = rod2.mass = +txtStringMass.value;				// 擺繩質量
	rod.L0 = rod2.L0 = +txtStringLength.value;					// 擺繩原長
	rod.theta0 = rod2.theta0 = +txtTheta0.value*Math.PI/180.0;	// 初始 theta 角(從 -z 軸量起)

	bob.setRadius(+txtBobRadius.value);							// 擺錘半徑
	bob.clearTrail();											// 清除軌跡
	bob.position.set(											// 根據擺繩的初始角度來設定擺錘初始位置
		rod.L0*Math.cos(Math.PI*3/2+rod.theta0),
		rod.L0*Math.sin(Math.PI*3/2+rod.theta0),
		0
	).add(ceiling.position);
	bob.mass = bob2.mass = +txtBobMass.value;					// 擺錘質量
	bob.velocity = vector();									// 初速度
	bob.acceleration = vector();								// 加速度
	calcA(														// 計算初始加速度
		bob.position,bob.velocity,0,bob.acceleration
	);

	bob2.setRadius(+txtBobRadius.value);						// 擺錘半徑
	bob2.clearTrail();											// 清除軌跡
	bob2.position.set(											// 根據擺繩的初始角度來設定擺錘初始位置
		rod2.L0*Math.cos(Math.PI*3/2+rod2.theta0),
		rod2.L0*Math.sin(Math.PI*3/2+rod2.theta0),
		0
	).add(ceiling.position);
	bob2.velocity = vector();									// 初速度
	bob2.acceleration = vector();								// 加速度
	calcA(														// 計算初始加速度
		bob2.position,bob2.velocity,0,bob2.acceleration
	);

	L2 = rod2.L0;
	theta = theta2[0] = rod2.theta0;
	omega = omega2[0] = 0;
	calcA2(theta2,omega2,0,alpha2);

	rod.setAxis(												// 調整擺繩讓它連結擺錘與天花板
		__axis.copy(bob.position).sub(ceiling.position)
	).position.copy(ceiling.position).add(__axis.multiplyScalar(0.5));
	rod2.setAxis(												// 調整擺繩讓它連結擺錘與天花板
		__axis.copy(bob2.position).sub(ceiling.position)
	).position.copy(ceiling.position).add(__axis.multiplyScalar(0.5));
	return ++n_init > 1;
}
//}}}
/***
!!! 細步計算
<<tiddler "單擺運動一 JS Codes##細步計算">>
!!!! 細步計算程式碼
***/
//{{{
let __uaxis = vector(),
	__at = vector(),
	g = vector(0,-9.8,0);
const calc_at = () =>
	// 計算重力產生的切線加速度
	__at.copy(g).sub(						// 重力產生的切線加速度
		__tmpV.copy(__uaxis).multiplyScalar(
			g.dot(__uaxis)
		)
	),
calcA = (r,v,t,a) => {					// 定義計算加速度的函數
	// 假設擺繩長度不變,那麼擺錘的 瞬時 運動都是圓周運動
	// 如此一來擺錘的加速度便是 圓周運動所需的向心加速度 加上
	// 重力產生的 切線加速度

	// 向心加速度是沿著擺繩的方向,所以我們先把擺繩的向量算出來
	__axis.copy(r).sub(ceiling.position);	// 擺繩向量,從懸掛點到擺錘
	__uaxis.copy(__axis).normalize();		// 其單位向量,從懸掛點到擺錘

	// 計算重力產生的切線加速度
	calc_at();

	if(!a) a = vector();
	// 根據圓周運動公式計算向心加速度
	let ac = v.lengthSq()/__axis.length();	// 圓周運動所需要的向心加速度
	return a.copy(__uaxis)					// 合起來便是擺錘的加速度
		.multiplyScalar(-ac).add(__at);
},
calcA1 = (theta,omega,t) => {
	__axis.set(
		L2*Math.cos(Math.PI*3/2+theta),
		L2*Math.sin(Math.PI*3/2+theta),
		0
	);
	__uaxis.copy(__axis).normalize();

	// 計算重力產生的切線加速度
	calc_at();

	L2 = __axis.length();
	return __at.length()/L2*(theta > 0 ? -1 : 1);
},
calcA2 = (theta,omega,t,alpha) => {
	alpha[0] = calcA1(theta[0],omega[0]);
	return alpha;
}
//}}}
//{{{
scene.update = (t_cur,dt) => {
	let e0 = +txtTolerance.value,
		adaptive = chkAdaptive.checked,
		delta_pos = $tw.physics.nextPosition(
			bob,calcA,t_cur,dt,adaptive,e0
		),
		delta_angle = null;
	rod.setAxis(								// 更新擺繩
		__axis.copy(bob.position).sub(ceiling.position)
	).position.copy(ceiling.position).add(__axis.multiplyScalar(0.5));

	/*
	delta_angle = $tw.numeric.ODE.nextValue(
		theta,omega,calcA1,t_cur,dt,0,adaptive,e0
	);
	theta += delta_angle[0];
	omega += delta_angle[1];
	//*/
	///*
	delta_angle = $tw.numeric.ODE.nextValue(
		theta2,omega2,calcA2,t_cur,dt,alpha2,adaptive,e0
	);
	theta = theta2[0];
	//*/
	bob2.position.set(
		L2*Math.cos(Math.PI*3/2+theta),
		L2*Math.sin(Math.PI*3/2+theta),
		0
	).add(ceiling.position);
	rod2.setAxis(								// 更新擺繩
		__axis.copy(bob2.position).sub(ceiling.position)
	).position.copy(ceiling.position).add(__axis.multiplyScalar(0.5));

	if(adaptive) setdT(Math.min(delta_pos[2],delta_angle[2]));
}
//}}}
! 針對一階微分方程的四階 ~Runge-Kutta 方法
在【微分方程的數值求解】過程中,關鍵點在於如何估計出「能減少誤差累積的斜率」,目前已知的方法不只一個,其中「四階 ~Runge-Kutta 方法」可能是 __夠實用__ 的方法中 __最簡單__ 的一個,其想法基本上是【''先推測幾個中間位置的斜率,和端點的斜率一起求出某種加權平均值,再用此平均斜率來推測下一個位置的值''】,至於各點斜率的權重則根據泰勒展開式來計算出。以最常用的四階做法來說,取兩個中間位置和兩個端點(現在位置及下一個位置)的斜率來求平均,這樣的做法經常出現兩個中間值一個是高估而另一個是低估的現象,使得誤差不至於快速累積,因而廣為使用。其公式並不困難,若按照 [[維基百科 The Runge-Kutta Method|https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods]] 網頁的寫法,我們__需要先定義一次微分的函數__以及__初始條件__:\[\begin{aligned}\dot y = f(t, y), \quad y(t_0) = y_0,\end{aligned}\] 並選擇適當的時間間隔 \(dt\) 進行迭代計算,\begin{aligned}y_{n+1} &= y_n + {dt \over 6}(k_1 + 2k_2 + 2k_3 + k_4) \\ t_{n+1} &= t_n + dt,\end{aligned} 其中 \begin{aligned} k_1 &= f(t_n, y_n) & 現在位置的斜率 \\ k_2 &= f\left(t_n+{dt \over 2},y_n+{dt \over 2}k_1\right), & 第一個中間點的斜率 \\ k_3 &= f\left(t_n+{dt \over 2},y_n+{dt \over 2}k_2\right), \quad & 第二個中間點的斜率 \\ k_4 &= f\left(t_n+dt,y_n+dtk_3\right). & 下一個位置的斜率 \end{aligned} 寫成程式碼也並不困難。
> @@color:red;''注意:''@@@@下列程式碼''未''經實際測試,除了最後的【針對二階微分方程的多粒子 1D 及 3D Javascript 版本】。@@
!!! 單粒子版本
對於單一粒子的一維運動,程式碼極為簡短:<<slider 'rk4vp' 'Runge-Kutta 1 Python##Single Particle 1D Python' 'Python codes: Single Particle'>>
!!! 多粒子版本
真正實用上我們經常需要處理多粒子的問題,可以使用陣列(array)來做多粒子的計算:<<slider 'rk4ap' 'Runge-Kutta 1 Python##Many Particles 1D Python' 'Python codes: Many Particles'>>
! 針對二階微分方程的四階 ~Runge-Kutta 方法
上面的範例都是__解一階微分方程的__(上面 (1) 式的定義是一階微分),而''物理上的微分方程經常都是二階的'',如牛頓第二定律、薛丁格方程等,所以要修改迭代過程使得一階微分以及函數本身可以在同一個副程式裡解出。

至於二階微分方程如何求解,有興趣者請參考 [[四階 Runge-Kutta 方法 -- 二階微分方程]]。
!! 參考文獻
# 維基百科 [[Runge-Kutta 網頁|https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods]]
# [[RK4 for 2nd order ODE|https://www.youtube.com/watch?v=smfX0Jt_f0I&feature=youtu.be&t=325]]
! 針對二階微分方程的四階 ~Runge-Kutta 方法
對二階微分方程求解,我們按照 [[四階 Runge-Kutta 方法]] 裡面求解一階微分方程的過程,''同樣先定義出二階微分的函數''以及''初始條件'':\[\begin{aligned}\ddot y = f(y, \dot y, t), \quad y(t_0) = y_0, \quad \dot y(t_0) = \dot y_0.\end{aligned}\]
>注意上面這個二階微分 \(\ddot y\) 在一般情況會是 \(y\) 及 \(\dot y\) 的函數,對一個力學系統來說,通常我們能夠知道【受力】,而根據牛頓第二定律,知道受力就可以算出【加速度】,而加速度正是位置的二次微分,這樣我們就知道
>>對力學系統而言上面 (2) 式中的 \(y\) 就是位置 \(\vec r\),而
>> \(\dot y\) 就是速度 \(\vec v\),
>>\(\ddot y = f(y,\dot y,t)\) 算出來的就是加速度 \(\vec a\),而加速度會是位置和速度的函數。
>物理上什麼情況下加速度是位置的函數呢?舉一個簡單的例子:彈簧-質量系統,在此系統裡物體的受力為 \(-kx\),其中的 //x// 就是位置(相對於平衡點),也就是說【受力和位置有關】,或者說【受力是位置的函數】,而受力可以讓我們算出加速度,所以理所當然【加速度是位置的函數】。
>
>至於什麼情況下加速度會是速度的函數呢?只要受力是速度的函數就是了,比如說空氣阻力 \(\vec f = -{1 \over 2} \rho v^2 C_{D}A \hat v\) 就是一個典型的例子。

{{FrameHalfRight{[>img(100%,)[image/teaching/RK4 for 2ndODE.JPG]]
來源:由 [[此影片|https://youtu.be/smfX0Jt_f0I?t=325]] 截出
}}}
接著按照右圖的做法寫成程式碼:
!!! 單粒子版本
對【單粒子一維運動】來說程式碼也不算複雜:<<slider 'rk42vp' 'Runge-Kutta 2 Python##Single Particle 3D/1D Python' 'Python codes: Single Particle'>>。
!!! 多粒子版本
@@更貼近實際的話我們應該是需要多粒子的版本:<<slider 'rk42vap' 'Runge-Kutta 2 Python##Many Particles 3D/1D Python' 'Python codes: Many Particles(未測試)'>>。@@
!! 參考文獻
# 維基百科 [[Runge-Kutta 網頁|https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods]]
# [[RK4 for 2nd order ODE|https://www.youtube.com/watch?v=smfX0Jt_f0I&feature=youtu.be&t=325]]
!!彈簧質量的影響 -- 有效質量為 \(\frac{1}{3} m_s\)
實際的彈簧是有質量的,當簡諧運動在進行的時候,彈簧本身也在運動,也會有動能,所以整個系統的動能會有一部分在彈簧本身,如果我們想要知道彈簧本身質量的影響有多大,可以從彈簧本身的動能下手來看出其影響。

假如有一個很均勻的彈簧(把座標原點放在彈簧固定端,讓 +\(x\) 軸朝右),
* 原長 \(L_0\)、質量 \(M_s\)、係數 \(K\) 。
我們把這個彈簧想像成是由 \(N\) 個完全一樣的小彈簧串聯起來,這其中每一段小彈簧
* 原長 \(l_0 = L_0 / N\)、質量 \(m_s = (M_s / L_0) l_0 = M_s / N\)、係數 \(k = NK\) 。
* 第 \(i\) 段小彈簧左端點位置 \(x_{i,0} = (i-1)l_0\),質心位置 \(x_{i,0,c} = x_{i,0}+l_0/2 = (i-1/2)l_0\) 。
|borderless|k
|width:35em;<<tw3DScene id:segments [[彈簧有效質量 Codes##Segments]] [[彈簧有效質量 Codes##L0 Indicators]] [[彈簧有效質量 Codes##x0 indicators]] [[彈簧有效質量 Codes##l0 indicators]] [[彈簧有效質量 Codes##ms indicators]]>>|width:35em;<<tw3DScene id:elongation [[彈簧有效質量 Codes##Segments]] [[彈簧有效質量 Codes##L0 Indicators]] [[彈簧有效質量 Codes##x0 indicators]] [[彈簧有效質量 Codes##Elongated]] [[彈簧有效質量 Codes##L Indicators]] [[彈簧有效質量 Codes##x indicators]] [[彈簧有效質量 Codes##dL indicators]] [[彈簧有效質量 Codes##dl indicators]]>>|
|圖一A:鬆弛狀態下的彈簧,將之視為由許多段小彈簧串聯起來。|圖一B:伸長狀態下的彈簧,總伸長量 \(dL\),每一段小彈簧伸長 \(dl\)。|
----
假設在某個時刻 \(t\) 彈簧受力為 \(F\),由於小彈簧是串聯起來的,我們知道它們的受力都是一樣的,因此每段小彈簧都會伸長\[dl = {F \over k},\] 而整根彈簧的伸長量會是 \[d L = Ndl = N{F \over k} = {F \over K}.\] 也就是每一段小彈簧的長度為 \(l = l_0 + dl\) 而整根彈簧的長度為 \(L = L_0 + dL\)。這個時候第 \(i\) 段小彈簧的左端點及質心位置分別為 \[x_i = (i-1)l, \quad x_{i,c} = (i-1/2)l .\] 如果我們用自己的質心動能來代表一段小彈簧的動能,那麼第 \(i\) 段小彈簧的動能就是 \[\epsilon_{k,i} = {1 \over 2}m_sv_{i,c}^2 = {1 \over 2}m_s\left((i-1/2){dl \over dt}\right)^2,\] 這裡 \[{dl \over dt} = {1 \over N}{dL \over dt}\] 是每一段小彈簧的【長度變化率】,應該每一段都一樣,因為任何時刻每段小彈簧的受力都一樣,且我們假設每段小彈簧都一樣。整根彈簧的動能就是所有小彈簧動能的加總,\begin{aligned}E_k = \sum_{i=1}^{N}\epsilon_{k,i} &= \sum_{i=1}^{N}{1 \over 2}m_s(i-1/2)^2\left(1 \over N\right)^2\left(dL \over dt\right)^2 \\ &= {1 \over 2}m_s\left(1 \over N\right)^2\left(dL \over dt\right)^2\sum_{i=1}^N(i^2-i+1/4) \\ &= {1 \over 2}m_s\left(1 \over N\right)^2\left(dL \over dt\right)^2\left[\left({1 \over 6}N(N+1)(2N+1)\right)-\left({1 \over 2}N(N+1)\right)+{N \over 4}\right] \\ &= {1 \over 2}m_s\left(1 \over N\right)^2\left(dL \over dt\right)^2\left[\left({1 \over 6}(N^2+N)(2N+1)\right)-\left({1 \over 2}(N^2+N)\right)+{N \over 4}\right] \\ &= {1 \over 2}m_s\left(1 \over N\right)^2\left(dL \over dt\right)^2\left[\left({1 \over 6}(2N^3+3N^2+N)\right)-\left({1 \over 2}(N^2+N)\right)+{N \over 4}\right] \\ &= {1 \over 2}m_s\left(1 \over N\right)^2\left(dL \over dt\right)^2\left[{N^3 \over 3}+{N^2 \over 2}+{N \over 6}-{1 \over 2}N^2-{N \over 2}+{N \over 4}\right] \\ &= {1 \over 2}m_s\left(1 \over N\right)^2\left(dL \over dt\right)^2\left[{N^3 \over 3}-{N \over 12}\right].\end{aligned} 我們把上面的結果稍作整理,並使用 \(m_s = M_s / N\),便得到 \[E_k = {1 \over 2}M_s v_\text{end}^2\left(1 \over 3\right)-{1 \over 2}m_sv_\text{end}^2\left(1 \over 12N\right) \to {1 \over 2}M_s v_\text{end}^2\left(1 \over 3\right) \text{ as } N \to \infty. \] 這裡 \[v_\text{end} = {dL \over dt}\] 是【彈簧的長度變化率】,也就是【活動端的速率】。
<<<
# 當''兩條完全一樣的彈簧串聯在一起''時,如果有受力的話,任何時刻它們所受到力會是一樣的(否則兩條就會分開了),這樣的話兩條彈簧都會伸長,而且伸長量是一樣的,所以總伸長量會是任何一條的兩倍。或者我們反過來說,''任何一條的伸長量都是總伸長量的二分之一''。
# 當 ''N 條一樣的彈簧串聯在一起''時,如果有受力的話,我們知道''每一小段的伸長量 \(X_i\) 都是總伸長量 \(X\) 的 N 分之一'' \((X_i = X / N)\) 。
# 第 \(1\) 小段彈簧伸長 \(X_1\) 時,會把它之後的所有小段彈簧往外推 \(X_1\) 的距離,同理,每一小段都是如此。
# 第 \(i\) 小段彈簧的位置將因為它的前面 \(i-1, i-2, \cdots, 1\) 段彈簧伸長而改變,改變的量為前面所有小段伸長量的總和 \[\Delta x_i = \sum_{n=1}^{i-1} X_n = (i-1){X \over N} = {(i-1)dx \over Ndx}X = {x \over l}X.\]
** 嚴格來講上式應該是 \(((x-dx/2)/l)X\),但微積分通常要求 \(dx \to 0\),所以直接寫成 \((x/l)X\)。
<<<
按照一般定義速度指的是「質心」的速度,也就是 \(v_x = \frac{dx}{dt}\),這樣一來這個小段速度就是
\begin{aligned}
v_x &= \frac{dx}{dt} = \frac{d}{dt} \left( \frac{X}{l} x\right) \\
&= \frac{x}{l} \frac{dX}{dt} + \frac{X}{l} \frac{dx}{dt} \\
&= \frac{x}{l} (2v_s) + \frac{X}{l} v_x,
\end{aligned} 也就是 \[\tag{1}\left(1 - \frac{X}{l}\right) v_x = 2\frac{x}{l} v_s,\] 其中 \(v_s = \frac{1}{2} \frac{dX}{dt} \) 是整個彈簧的質心速度。
> 整個彈簧質心位置為 \(x_\text{CM} = x_{\text{CM}, 0} + \frac{1}{2} X\), 其中 \(x_{\text{CM}, 0}\) 為彈簧沒有伸長或壓縮時候的質心位置,而 \(X\) 是彈簧總伸長量,故而整個彈簧質心速度為 \(v_s = \frac{1}{2} \frac{dX}{dt} \)。

如果我們假定整個彈簧的伸長量不算大,或者明確地說 \[\frac{X}{l} \ll 1 ,\] 也就是「這個時刻的伸長量比這個時刻彈簧長度的兩倍小很多」,那麼這個小段的速度可以簡單寫成 \[\tag{2} v_x = 2\frac{x}{l} v_s .\] 如此一來我們就可以簡化這個小段的動能為 \[dE_k = \frac{1}{2} dm_s\ v_x^2 = \frac{1}{2} \left(\frac{m_s}{l_0} dx\right) \left(2\frac{x}{l} v_s \right)^2 .\] 整個彈簧的動能就是把所有小段的動能加起來(加到這個時刻的長度 \(l\)):
\begin{aligned}
E_k = \int dE_k &= \int_0^l \frac{1}{2} \left(\frac{m_s}{l_0} dx \right) \left(2\frac{x}{l} v_s \right)^2 \\
&= \frac{1}{2} \frac{m_s}{l_0} \left(2\frac{v_s}{l}\right)^2 \int_0^l x^2 dx \\
&= \frac{1}{2} m_s v_s^2 \left(\frac{4}{l_0 l^2}\right) \left[\frac{x^3}{3} \right]_0^l,
\end{aligned} 代入上下限的結果為 \[\tag{3} E_k = {1 \over 2} m_s v_s^2 \left({1 \over 3}\right) \left({4l \over l_0}\right).\] 也就是說,一個質量為 \(m_s\) 的彈簧做簡諧運動時彈簧本身的動能相當於一個質量為 \(\frac{1}{3} \left(\frac{4l}{l_0}\right) m_s\) 的物體做同樣的簡諧運動(但使用無質量彈簧)的動能,如果我們更進一步簡化讓__最大伸長量比原長__小很多 \( X_\text{max} \ll l_0 \) (前面是說__這個時刻的伸長量比這個時刻彈簧長度的兩倍__小很多),也就是任何時候 \(l \sim l_0\),我們就會得到 \[\tag{4} E_k \sim \frac{1}{2} m_s v_s^2 \left(\frac{1}{3}\right) \] 這樣的結果,這也就是為什麼我們說「彈簧的有效質量為 \(\frac{1}{3} m_s\)」的緣故。

----

!!!! 注意
這裡我們應該注意一件事,要得到「彈簧的有效質量為 \(\frac{1}{3} m_s\)」這樣的結論必須在 \( X_\text{max} \ll l_0 \) 的前提下才成立,對很硬(\(k\) 值很大,不容易伸長或壓縮)或很長的彈簧這沒有太大問題,但教學實驗室經常使用的是很軟又不算長的彈簧,很容意讓最大伸長量接近甚至超過原長,這時若還使用 \(\frac{1}{3} m_s\) 來當做彈簧的有效質量恐怕是差異頗大的!

!!!! 假如不要簡化

假如不要做 \( X \ll 2l \) 這樣的假設,則該小段彈簧的速度應該為 \[v_x = \frac{x}{l} \frac{1}{1-X/(2l)} v_s .\] 而它的動能就會是 \begin{aligned}
dE_k &= \frac{1}{2} dm v_x^2 = \frac{1}{2} \left( \frac{m_s}{l_0} dx \right) \left( \frac{x}{l} \frac{1}{1-X/(2l)} \right)^2 v_s^2 \\
&= \frac{1}{2} m_s v_s^2 \frac{1}{l_0l^2} \left( \frac{1}{1-X/(2l)} \right)^2 x^2 dx .
\end{aligned} 如此則整條彈簧的動能就是 \begin{aligned}
E_k = \int_0^l dE_k &= \frac{1}{2} m_s v_s^2 \frac{1}{l_0l^2} \left( \frac{1}{1-X/(2l)} \right)^2 \int_0^l x^2 dx \\
&= \frac{1}{2} m_s v_s^2 \frac{1}{l_0l^2} \left( \frac{1}{1-X/(2l)} \right)^2 \frac{l^3}{3} \\
&= \frac{1}{2} m_s v_s^2 \frac{1}{3} \frac{l}{l_0} \left( \frac{1}{1-X/(2l)} \right)^2
\end{aligned} 也就是說「@@color:red;彈簧的動能和當時的長度 \(l\) 或是當時的伸長量 \(X\) 是有關的@@」,這樣一來,實驗上測量物體的速度來計算動能這件事,如果在不同的位置做會有不同的結果。

假設測量物體速度的地方,就是彈簧當時的長度 \(l\) 之處,我們代入 \(X = l - l_0\) 來看出動能和 \(l\) 的關係,\begin{aligned}
\left( \frac{1}{1-X/(2l)} \right)^2 &= \left( \frac{2l}{2l-(l-l_0)} \right)^2 \\
&= \left( \frac{2l}{l+l_0)} \right)^2 \\ &= 4 \left( \frac{l/l_0}{l/l_0+1} \right)^2 .
\end{aligned} 也就是說彈簧能量的式子可以整理成 \[\tag{5} E_k = \frac{1}{2} m_s v_s^2 \frac{1}{3} \frac{4 (l/l_0)^3}{(l/l_0+1)^2} .\] 這裡可以看出 \[\tag{6} \frac{4 (l/l_0)^3}{(l/l_0+1)^2} \] 便是實驗測量值和 \(\frac{1}{3}\) 的差異。''必須在剛剛好 \(l = l_0\) 的位置做測量,才會得到 \(1 \over 3\) 的結果。''

下圖顯示出「在不同位置測量動能所對應的彈簧有效質量」以及「只用 \(\frac{1}{3} m_s\) 當做彈簧有效質量所產生的誤差」,不難看出@@color:red;僅僅在原長 \(10 \%\) (\(0.9\) 到 \(1.1\) 倍原長)的範圍內做測量就已經可以產生最高 \( 20\% \) 的誤差,如果使用 \(\frac{1}{3} m_s\) 當做彈簧有效質量的話@@,這可以解釋為什麼這個實驗的能量守恆檢驗部份普遍都有很大的誤差。

{{divSubTitle{
@@color:blue;問題:為什麼誤差相對於平衡點不是對稱的?@@
}}}
!! Segments
//{{{
cartesian.setPosition(vector(-0.5,0,0));
scene.camera.position.z = sceneWidth*0.72;
var spring = [], N = 10, nx = 4;
for(var n=0; n<N; n++){
	spring[n] = helix({
		pos:vector(-0.5+n*0.1,0,0),
		radius:0.02,
		coils:5,
		axis:vector(0.1,0,0),
		color:0xFED162,
		opacity:(n===nx?1:0.4)
	});
}
//}}}
!!!! L0 Indicators
//{{{
var L0_bound = [
	line({
		vertices:[
			spring[0].position.clone(),
			spring[0].position.clone()
				.add(vector(0,-spring[0].getRadius()*10,0))
		],
		color:0xffffff
	}),
	line({
		vertices:[
			spring[N-1].position.clone().add(spring[N-1].axis),
			spring[N-1].position.clone()
				.add(spring[N-1].axis)
				.add(vector(0,-spring[N-1].getRadius()*10,0))
		],
		color:0xffffff
	})
];
//}}}
//{{{
var L0 = label({
	text:'\\(L_0\\)',
	color:0xffffff,
	size:16
});
L0.setPosition(
	spring[N/2].position.x+spring[N/2].getLength()/2,
	spring[N/2].position.y-spring[N/2].getRadius()*8,
	spring[N/2].position.z
);
var L0_indicator = [
	line({
		vertices:[
			spring[0].position.clone().add(
				vector(0,-spring[0].getRadius()*8,0)
			),
			spring[N/2].position.clone().add(
				vector(0,-spring[N/2].getRadius()*8,0)
			)
		],
		color:0xffffff
	}),
	line({
		vertices:[
			spring[N/2+1].position.clone().add(
				vector(0,-spring[N/2+1].getRadius()*8,0)
			),
			spring[N-1].position.clone().add(
				vector(0,-spring[N-1].getRadius()*8,0)
			).add(spring[N-1].axis)
		],
		color:0xffffff
	})
];
//}}}
!!!! x0 indicators
//{{{
var px0 = sphere({
	pos:spring[nx].position,
	radius:spring[nx].getRadius()/2,
	color:0xff00ff
});
px0.position.x += spring[nx].getLength()/2;
var x0 = label({
	text:'\\(x_{i,0,c} = (i-1/2)l_0\\)',
	color:px0.getColor(),
	size:12
});
x0.setPosition(
	spring[nx].position.x + spring[nx].getLength()/2,
	spring[nx].position.y - spring[nx].getRadius()*3,
	spring[nx].position.z
);
//}}}
!!!! l0 indicators
//{{{
var l0_bound = [
	line({
		vertices:[
			spring[nx].position.clone(),
			spring[nx].position.clone().add(vector(0,0.06,0))
		]
	}),
	line({
		vertices:[
			spring[nx].position.clone().add(spring[nx].axis),
			spring[nx].position.clone().add(spring[nx].axis).add(vector(0,0.06,0))
		]
	})
];
var l0 = label({
	text:'\\(l_0\\)',
	color:0xffff00,
	size:12
});
l0.setPosition(
	(l0_bound[0].getVertex(0).x+l0_bound[1].getVertex(1).x)/2,
	l0_bound[1].getVertex(1).y,
	l0_bound[0].getVertex(0).z
);
//}}}
!!!! ms indicators
//{{{
var ms = label({
	text:'\\(m_s = {M_s \\over L_0}l_0\\)',
	color:0xffffff,
	size:12
});
ms.setPosition(
	spring[nx].position.x + spring[nx].getLength()/2,
	spring[nx].position.y + spring[nx].getRadius()*7,
	spring[nx].position.z
);
//}}}
!! Elongated
//{{{
cartesian.show(false);
for(var n=0; n<N; n++){
	var l = spring[n].getLength()*1.2;
	spring[n].stretch(l);
	if(n>0) spring[n].position.x = spring[n-1].position.x+l;
}
//}}}
!!!! L Indicators
//{{{
var L_bound = [
	line({
		vertices:[
			spring[0].position.clone(),
			spring[0].position.clone()
				.add(vector(0,spring[0].getRadius()*10,0))
		],
		color:0xffffff
	}),
	line({
		vertices:[
			spring[N-1].position.clone().add(spring[N-1].axis),
			spring[N-1].position.clone()
				.add(spring[N-1].axis)
				.add(vector(0,spring[N-1].getRadius()*10,0))
		],
		color:0xffffff
	})
];
//}}}
//{{{
var L = label({
	text:'\\(L\\)',
	color:0xffffff,
	size:16
});
L.setPosition(
	spring[N/2].position.x+spring[N/2].getLength()/2,
	spring[N/2].position.y+spring[N/2].getRadius()*8,
	spring[N/2].position.z
);
var L_indicator = [
	line({
		vertices:[
			spring[0].position.clone().add(
				vector(0,spring[0].getRadius()*8,0)
			),
			spring[N/2].position.clone().add(
				vector(0,spring[N/2].getRadius()*8,0)
			)
		],
		color:0xffffff
	}),
	line({
		vertices:[
			spring[N/2+1].position.clone().add(
				vector(0,spring[N/2+1].getRadius()*8,0)
			),
			spring[N-1].position.clone().add(
				vector(0,spring[N-1].getRadius()*8,0)
			).add(spring[N-1].axis)
		],
		color:0xffffff
	})
];
//}}}
!!!! x indicators
//{{{
var px = sphere({
	pos:spring[nx].position,
	radius:spring[nx].getRadius()/2,
	color:0xff00ff
});
px.position.x += spring[nx].getLength()/2;
/*
var x = label({
	text:'\\((x,0,0)\\)',
	color:px.getColor(),
	size:12
});
x.setPosition(
	spring[nx].position.x + spring[nx].getLength()/2,
	spring[nx].position.y + spring[nx].getRadius()*3,
	spring[nx].position.z
);
*/
//}}}
!!!! dL indicators
//{{{
var dL_bound = [
	line({
		vertices:[
			L0_bound[1].getVertex(0),
			L0_bound[1].getVertex(1)
		]
	}),
	line({
		vertices:[
			L_bound[1].getVertex(0),
			L_bound[1].getVertex(0).add(vector(0,-spring[0].getRadius()*10,0))
		]
	})
];
var dL = label({
	text:'\\(dL\\)',
	color:0xffff00,
	size:12
});
dL.setPosition(
	(dL_bound[0].getVertex(0).x+dL_bound[1].getVertex(1).x)/2,
	L0_indicator[0].getVertex(0).y,
	dL_bound[0].getVertex(0).z
);
//}}}
!!!! dl indicators
//{{{
var dl_bound = [
	line({
		vertices:[
			px0.position.clone(),
			px0.position.clone().add(vector(0,0.06,0))
		]
	}),
	line({
		vertices:[
			px.position.clone(),
			px.position.clone().add(vector(0,0.06,0))
		]
	})
];
var dl = label({
	text:'\\(dx_{i,0,c} = (i-1/2)dl\\)',
	//text:'\\(dl = {l_0 \\over L_0}dL\\)',
	color:0xffff00,
	size:12,
	align:'left'
});
dl.setPosition(
	(dl_bound[0].getVertex(0).x+dl_bound[1].getVertex(1).x)/2,
	dl_bound[1].getVertex(1).y*1.5,
	dL_bound[0].getVertex(0).z
);
//}}}
彈簧運動在一般高中物理課裡面都會提到,它的主要結果:簡單諧和運動(簡諧運動),在【彈性限度內】、【''忽略阻力''】,且【''忽略彈簧質量''】情況下的運動週期為 \[T = 2\pi \sqrt{k \over m}\] 等也是高中物理內容就有談到的。

在前面的 [[拋體運動]] 範例中,我們提到人們在學習時永遠忽略阻力,真正的理由並非阻力永遠可忽略,而是【用手算做不出來】。__這個教案(以及後續的教案)裡我們都會把空氣阻力計算進去__。不過這個教案還有另外一件事情要學習,那就是【''數據繪圖''】,所以我們會先計算水平且沒有任何阻力的最簡單情況(位置對時間作圖:[[彈簧運動一]],能量對時間作圖:[[彈簧運動二]]),然後討論有摩擦力及空氣阻力時的情況([[彈簧運動三]])。
> 另外,從這個教案開始我們都會採用較為實用的 [[四階 Runge-Kutta 方法]]。
改成垂直的:[[彈簧運動四]]、[[彈簧運動五]]、[[彈簧運動六]]

覺得太無聊嗎?要再玩點更有趣的?看看 [[彈簧運動七]] 吧。

前面的範例裡面,彈簧的質量都被忽略,但實際上彈簧一定是有質量的,而且震盪的時候彈簧也是在運動的,有質量的物體在運動,一定具有動能,也就是說,要正確地計算彈簧-質量的震盪能量,必須將彈簧的動能考慮進去才行。[[彈簧運動八]] 可以學到這件事。
!!! 彈簧運動一
* [[彈簧運動一]] 模擬最簡單的情況:彈簧+質量在水平桌面上,忽略摩擦力,忽略空氣阻力。這個範例裡我們要畫出【位置】、【速度】以及【加速度】的大小對時間的關係圖。
<<<
# 一個彈簧的震盪叫做【簡單諧和震盪】簡稱【簡諧震盪】,因為它是【只有一個頻率的簡單__來回運動(諧和運動,harmonic motion)__】。
# 在 {{{VPython}}} 裡繪製簡單的數據圖其實非常容易,在這個範例裡只有幾行跟數據繪圖有關,有看出來是哪幾行嗎?
# 改變彈簧的伸長量,觀察震盪的週期有什麼變化?能夠確認是否符合 \(T \propto \sqrt l\) 嗎?
# 改變彈簧的力常數 \(k\),觀察震盪的週期有什麼變化?改變質量呢?
# 觀察【x 位置】與【x 速度】的數據圖,有什麼關連?和【x 加速度】比較的話呢?
<<<
!!! 彈簧運動二
* [[彈簧運動二]] 和 [[彈簧運動一]] 的情況是一樣的,只是這回數據圖改成【動能】、【位能】,以及【總力學能】。
<<<
# 【能量守恆】是物理學裡面一個很重要的定律,你的模擬結果有符合能量守恆這項要求嗎?
# 觀察動能與位能隨時間的變化,兩個能量之間有什麼關聯?
# 改變球的質量,觀察有什麼異同。
# 改變彈簧的伸長量,觀察有什麼異同。
# 改變彈簧的力常數 \(k\),觀察有什麼異同。
<<<
!!! 彈簧運動三
* [[彈簧運動三]] 和前兩個範例中的情況基本上是一樣,但是包含了摩擦力及空氣阻力,主要目標為觀察【有阻力時候的震盪(阻尼震盪)】。
<<<
# 有阻力時候的震盪就是所謂的 [[阻尼震盪|https://zh.wikipedia.org/zh-tw/%E9%98%BB%E5%B0%BC]] 或 [[damped oscillation|http://hyperphysics.phy-astr.gsu.edu/hbase/oscda.html]],觀察這種情況的位置-時間關係圖,有哪些特徵?
# 試著改變摩擦係數,看看這些特徵有什麼變化?
<<<
!!! 學習成效
這幾個範例中我們學習到
# 簡單數據圖的製作。
# 
<<<
# 根據經驗,不少學生第一次接觸四階 ~Runge-Kutta 方法時都覺得麻煩而不願採用,寧可選擇簡單易懂的歐拉法。這個範例的目的便是展示出歐拉法的主要缺點,不需多做解釋就能一目了然,希望讓第一次接觸的學生能更容易接受,進而願意採用較為精準的演算法。
# 另外,不少學生看到可以模擬圓錐擺會感到驚奇,很可能是因為傳統用手做計算的學習方式很難處理非平面擺動,所以覺得那應該是很難的緣故。不過在發現原來只要改變初始速度就能擺出各種花樣時,就會覺得這一點也不是難事。
<<<
<<foldHeadings>>
@@font-size:1.5em;''模擬範例 -- 彈簧運動一:簡諧運動的位置、速度、加速度''@@
----
這個例子裡我們要模擬【一個物體接上彈簧之後】最簡單的運動:單一頻率的來回震盪,也就是所謂的【簡諧運動】。

物體接上彈簧之後會有【來回震盪】的現象,是因為彈簧有【回復原來狀態】的趨勢。用白話文來說,就是彈簧如果被拉長,會有縮回原來長度的趨勢;反過來如果是被壓縮,就會有伸回原來長度的趨勢。這個趨勢會對接在彈簧上的物體產生一個力量,我們把它叫做【彈簧的回復力】。
> 後面我們都討論伸長的情況,如果要知道縮短的狀況,只要把討論裡的【伸長】改成【縮短】就可以了。
彈簧伸長越多,回復力越大,但如果伸長太多,可能沒辦法回復到原來的長度,也就是彈性疲乏了。

在不會產生彈性疲乏的情況下,回復力【正比】於伸長量,也就是說,兩倍的伸長量會導致兩倍的回復力,用數學來寫就是 \[F = -k\Delta x,\] 其中 \(\Delta x\) 就是伸長量,\(k\) 叫做彈力係數或者彈力常數。回復力的方向就沿著彈簧的軸,朝著【回復到彈簧原長】的方向。

這種情況下接在彈簧上物體的運動,如果沒有阻力的話,就會是單一頻率的來回震盪,也就是所謂的簡諧運動,震盪的週期為 \[\tag{1} T = 2\pi \sqrt{k \over m},\] 其中 \(m\) 是物體的質量。

@@color:red;font-size:1.5em;底下的標題列按一下可以顯示隱藏內容@@

! 設計場景
//{{{
# from vpython import *		# 使用安裝版的話,這是必要的第一行(得自己寫),開啟 3D 繪圖外掛
GlowScript 3.2 VPython		# 使用 GlowScript 的話,這行會自動產生(不一定是 3.2 無妨),不要更改這一行!

'''
---------------------------------------------------------------------------------------------------------
設計場景
---------------------------------------------------------------------------------------------------------
'''

floor = box(                                    # 產生一個地板
    width=1,                                    # 寬度 1 公尺
	height=0.02,                                # 高度 0.02 公尺(2 公分)
	length=2,                                   # 長度 2 公尺
	opacity=0.5                                 # 半透明
)

spring = helix(                                 # 產生一個彈簧
    pos=floor.pos,                              # 把固定端放在地板的中心
	axis=vector(floor.length/4,0,0),			# 水平放置
	radius=0.03,                                # 半徑 0.03 公尺(3 公分)
	coils=20                                    # 圈數 20
)

ball = sphere(                                  # 產生一顆球
    pos=spring.pos+spring.axis,                 # 放在彈簧的活動端
	radius=0.1,                                 # 半徑 0.1 公尺(10 公分)
    make_trail=False,                           # 要產生軌跡(但暫時不顯示)
    interval=30                                 # 每 30 次計算留下一個軌跡點
)
//}}}
!!!! 新手提示
<<<
# 關於 {{{axis}}} 屬性:
** 在 [[VPython 3D Objects|http://www.vpython.org/contents/docs_vp5/visual/primitives.html]] 這個網頁裡可以看到,具有圓柱形的物體(如[[圓柱體|http://www.vpython.org/contents/docs_vp5/visual/cylinder.html]]、[[箭頭|http://www.vpython.org/contents/docs_vp5/visual/arrow.html]]、以及這個範例裡使用到的[[螺線|http://www.vpython.org/contents/docs_vp5/visual/helix.html]]等,都有一個叫做 {{{axis}}}(軸)的屬性。
** 這個 {{{axis}}} 屬性代表的就是''從一個端點到另一個端點''的向量。
# 如果還有其它疑問,可以參考 [[拋體運動一]]、[[拋體運動二]] 裡面的新手提示。
<<<
! 初始條件
//{{{
'''
---------------------------------------------------------------------------------------------------------
初始條件
---------------------------------------------------------------------------------------------------------
'''

g = vector(0,-9.8,0)                                # 重力加速度(-y 方向)

spring.L0 = spring.axis.mag                         # 紀錄彈簧原長
spring.k = 50                                       # 設定彈簧常數

ball.mass = 0.5                                     # 球的質量
ball.mg = ball.mass*9.8                             # 球所受到的重力

ball.pos.y += ball.radius+floor.height/2            # 把球提升到地板上
spring.pos.y = ball.pos.y                           # 把彈簧接到球心
spring.r0 = spring.pos+spring.axis                  # 紀錄彈簧活動端的初始位置

ball.pos.x += spring.L0/2                           # 水平拉長彈簧
ball.velocity = vector(0,0,0)                       # 設定初始速度
ball.acceleration = calcA(ball.pos,ball.velocity,0) # 計算初始加速度
ball.make_trail = True                              # 開始顯示軌跡
//}}}
!!!! 新手提示
<<<
* 注意彈簧一定要先拉長或壓縮才有回復力,有回復力才能拉動球產生運動。
<<<
! 細步計算
在水平桌面上,接在彈簧上的物體受力雖然有重力、桌面的正向力,以及彈簧的回復力,因為正向力與重力相互抵消之故,只有彈簧回復力會讓物體產生加速度,也就是物體的運動方程為 \[-k\Delta x \hat x = m\vec a,\] 也就是 \[\vec a = {-k\Delta x \over m}\hat x.\]
!!!! 四階 ~Runge-Kutta
//{{{
'''
開始:四階 Runge-Kutta 方法
'''

def nextValue(r,v,A,t,dt,a_now = None):
    # 根據現在的位置、速度,計算經過時間 dt 之後的位置、速度、
    # 使用四階榮格-庫塔(Runge-Kutta)演算法
    #       r: 現在位置
    #		v: 現在速度
	#		A: 計算加速度的函數 function A(r,v,t),是根據現在的位置、速度、時間等因素來計算的
    #           加速度的計算來自受力,只要會計算受力,就會計算加速度
    #
    #           物理上和位能有關的力都和位置有關,例如:重力、彈力、電力、磁力等,所以計算
    #           加速度的函數需要知道物體的位置。
    #
    #           有些和位能無關的力,如空氣阻力,會和速度有關,所以計算加速度的函數可能需要
    #           知道物體的速度
    #
	#           還有一些力可能和位置、速度無關,如粗糙度很一致的接觸面摩擦力,這些可以直接
    #           寫在計算加速度的函數中
    #
    #           有些力會和時間有關,例如週期性的驅動力,如果計算中有這樣的情況,就會需要
    #           知道現在的時間
    #
    #		t: 現在時間
	#		dt: 時間間隔
	# 		a_now: (非必要)現在的壓速度,如果有傳進來的話,可以是純量也可以是向量。

	r1 = r
	v1 = v
	a1 = a_now
	if a1 is None: a1 = A(r1,v1,t)

	dt2 = dt*0.5
	r2 = r+dt2*v1
	v2 = v+dt2*a1
	a2 = A(r2,v2,t+dt2)

	r3 = r+dt2*v2
	v3 = v+dt2*a2
	a3 = A(r3,v3,t+dt2)

	r4 = r+dt*v3
	v4 = v+dt*a3
	a4 = A(r4,v4,t+dt)

	dt /= 6.0
	r += (v1+(v2+v3)*2.0+v4)*dt
	v += (a1+(a2+a3)*2.0+a4)*dt

	return (r,v,A(r,v,t+dt))

'''
結束:四階 Runge-Kutta 方法
'''
//}}}
!!!! 數據作圖
//{{{
'''
---------------------------------------------------------------------------------------------------------
數據繪圖
---------------------------------------------------------------------------------------------------------
'''

# 建立一張空白的曲線圖
x_t = gcurve(label='x 位置 x10',color=color.red)    # 建立位置-時間關係圖( x-t 圖)
v_t = gcurve(label='x 速度',color=color.green)      # 建立速度-時間關係圖( v-t 圖)
a_t = gcurve(label='x 加速度',color=color.blue)     # 建立加速度-時間關係圖( a-t 圖)
//}}}
!!!! 計算加速度
//{{{
def calcA(r,v,t):                                   # 定義計算加速度的函數
    return (
        -spring.k*(r-spring.r0)                     # 彈簧回復力
    )/ball.mass                                     # 除以球的質量
//}}}
!!!! 計算每一步
//{{{
dt = 0.001                                          # 每一次計算的時間間隔
t = 0                                               # 初始時間為 0
while t<3:
    rate(1000)
    # 以 四階 Runge-Kutta 方法計算下一個時間點的位置、速度,以及加速度。
    ball.pos,ball.velocity,ball.acceleration = nextValue(
        ball.pos,ball.velocity,calcA,t,dt,ball.acceleration
    )
    spring.axis = ball.pos-spring.pos               # 調整【畫面上】彈簧的長度
                                                    # 畫面上的彈簧並不會自動調整,所以要在這裡手動調整
    
    # 畫一個數據點
    x_t.plot(t,ball.pos.x*10)                       # 在 x-t 圖畫一個點
    v_t.plot(t,ball.velocity.x)                     # 在 v-t 圖畫一個點
    a_t.plot(t,ball.acceleration.x)                 # 在 a-t 圖畫一個點
    
    t += dt                                         # 時間加上 dt
//}}}
!!!! 新手提示
<<<
# 關於數據作圖的說明,可以參考 [[VPython 的說明網頁|http://www.glowscript.org/docs/VPythonDocs/graph.html]]
<<<
<<foldHeadings>>
!!!! 線上模擬說明
<<<
底下的模擬程式可以玩玩看!
# 按【Start】按鈕開始模擬。過程中隨時可以按【Reset】按鈕重新模擬。
# 勾選 Auto CPF 可以加快模擬過程。
# 讓它跑一會,@@color:red;觀察位置、速度、加速度等與時間的關係。@@
# 設著改變彈簧初始拉長的程度,然後按 Reset 重新模擬,@@color:red;觀察看看不同的伸長量對位置、速度、加速度的變化頻率(或者週期)有什麼影響?@@
# 試著改變球的質量 \(m\) 或是彈簧的彈力常數 \(k\)(一次只改變一個就好),@@color:blue;觀察看看不同的質量(或彈力常數)對位置、速度、加速度的變化頻率(或者週期)有什麼影響?@@
<<<
!!!! 線上模擬
* @@color:red;取消勾選右方 Auto CPF 可以慢很多,看清楚運動的每一步!@@
** 勾選起來可以快很多!
|彈簧運動模擬|c
|width:45%;<<tw3DCommonPanel "Flow Control">>|width:45%;<<tw3DCommonPanel "Label Statistics">>|
|<<tw3DCommonPanel "Simulation Time Control">>|<<tw3DCommonPanel "Air Drag">>|
|<<tw3DCommonPanel "Center of Mass Control">>|<<tw3DCommonPanel "Trail Control">>|
|<<tw3DCommonPanel "Display Control">> / <<tiddler "Spring Panel##Ball Control">>|<<tiddler "Spring Panel##Spring Control">>|
|<<tw3DCommonPanel "Plot0 Control">> / <<tw3DCommonPanel "DAQ Control">> / <<tw3DCommonPanel "Gravity Control">><br><<twDataPlot id:dataPlot0>>|<<tw3DCommonPanel "Plot1 Control">> / <<tw3DCommonPanel "Message">><br><<twDataPlot id:dataPlot1>>|
|>|<<tw3DScene [[彈簧運動一 JS Codes]]>>|
>@@font-sze:0.8em;此線上模擬版本係由瀏覽器內建的 Javascript 寫成,以 [[THREE.js|https://threejs.org/]] 做 3D 繪圖,有興趣學習者可以自行上網以【Javascript 學習】、【Javascript 初學】、【Javascript 3D 繪圖】等關鍵字搜尋相關教學文章或影片。
/***
!!! 設計場景
***/
//{{{
//scene.camera.position.z *= 2;

let __tmpV = vector(),										// 共用暫時向量(計算過程使用)

	floor = box({                               			// 產生一個地板
    	width:2,                                    		// 寬度 1 公尺
		height:0.02,                                		// 高度 0.02 公尺(2 公分)
		length:2,                                   		// 長度 2 公尺
		opacity:0.5                                 		// 半透明
	}),

	spring = helix({                            			// 產生一個彈簧
	    pos:floor.pos,                              		// 把固定端放在地板的中心
		axis:vector(floor.getLength()/4,0,0),				// 水平放置
		radius:0.03,                                		// 半徑 0.03 公尺(3 公分)
		coils:40                                    		// 圈數 20
	}),

	ball = sphere({                             			// 產生一顆球
    	pos:__tmpV.copy(spring.position).add(spring.axis),	// 放在彈簧的活動端
		radius:0.1,                                 		// 半徑 0.1 公尺(10 公分)
		opacity:0.5,										// 半透明
    	make_trail:false,                           		// 要產生軌跡(但暫時不顯示)
    	interval:30                                 		// 每 30 次計算留下一個軌跡點
	});

chkGravity.checked = true;									// 開啟模擬重力場
chkGravity.disabled = true;									// 禁止關閉重力場
chkAirDrag.checked = false;									// 關閉空氣阻力
chkAirDrag.disabled = true;									// 禁止開啟空氣阻力(後面的範例可以開啟)

txtSpringL0.value = spring.axis.length();
labelPlot[0].innerHTML = '\\(x(t), v_x(t)\\)';
dataPlot[0].setTitle('\\(x(t), v_x(t)\\)')
	.setYTitle('\\(x \\text{ (m)}, v_x \\text{ m/s}\\)').setXTitle('\\(t(s)\\)');
labelPlot[1].innerHTML = '\\(a_x(t)\\)';
dataPlot[1].setTitle('\\(a_x(t)\\)')
	.setYTitle('\\(a_x \\text{ (m/s}^2\\text{)}\\)').setXTitle('\\(t(s)\\)');

activateDAQChannels(3);
attachDAQBuffer(0,0);
attachDAQBuffer(0,1);
attachDAQBuffer(1,2);
//}}}
/***
!!! 初始條件
***/
//{{{
scene.init = () => {
	spring.L0 = +txtSpringL0.value;	                			// 紀錄彈簧原長
	spring.setLength(spring.L0);								// 調整彈簧長度
	spring.k = +txtSpringK.value;                      			// 設定彈簧常數

	ball.mass = +txtBallMass.value;                         	// 球的質量
	ball.mg = ball.mass*9.8;                            		// 球所受到的重力
	ball.setRadius(+txtBallRadius.value);                      	// 球的質量
	spring.setRadius(ball.getRadius()/4);						// 彈簧半徑

	spring.position.y = floor.position.y+floor.getHeight()/2+
			spring.getRadius()/2;								// 把彈簧提升到地板上
	spring.r0 = spring.axis.clone().add(spring.position);  		// 紀錄彈簧活動端的初始位置

	spring.setLength(spring.L0*1.5);							// 調整彈簧長度
console.log(spring.axis);
	ball.position.copy(spring.position).add(spring.axis);		// 將球放到彈簧的尾端

	ball.velocity = vector(0,0,0)                       		// 設定初始速度
	ball.acceleration = calcA(ball.position,ball.velocity,0);	// 計算初始加速度
	ball.clearTrail();
	ball.make_trail = true;                              		// 開始顯示軌跡
}
//}}}
/***
!!! 細步計算
彈簧伸長或者縮短都會產生回復力 \[\vec F = -k\Delta \vec r,\] 其中 \(\Delta \vec r\) 是彈簧的伸長(縮短)向量。
!!!! 細步計算程式碼
***/
//{{{
const calcA = (r,v,t,a) => {									// 定義計算加速度的函數
	if(!a) a = vector();
    return a.copy(r).sub(spring.r0).multiplyScalar(-spring.k)	// 彈簧回復力
    	.multiplyScalar(1/ball.mass);							// 除以球的質量
}

scene.update = (t_cur,dt) => {
	// 以 四階 Runge-Kutta 方法計算下一個時間點的位置、速度,以及加速度。
    $tw.numeric.ODE.nextValue(
		ball.position,											// 現在位置
		ball.velocity,											// 現在速度
		calcA,													// 計算加速度的函數
		t_cur,dt,												// 現在時間,時間間隔
		ball.acceleration										// 現在加速度
	);
	__tmpV.copy(ball.position).sub(spring.position);
    spring.setLength(__tmpV.length());							// 調整【畫面上】彈簧的長度
																// 畫面上的彈簧並不會自動調整,所以要在這裡手動調整
	recordData(scene.currentTime(),null,[ball.position.x,ball.velocity.x,ball.acceleration.x]);
}
//}}}
@@font-size:1.5em;''模擬範例 -- 彈簧運動三:有摩擦力的簡諧運動(阻尼震盪)''@@
----
在 [[彈簧運動一]] 這個例子裡我們模擬了【沒有摩擦力沒有阻力】的【簡諧運動】,這個例子裡我們要模擬有摩擦力有阻力時候地彈簧運動,也就是所謂的【阻尼震盪】。

在 [[拋體運動三]] 裡面我們了解到,現實生活中比較實際的空氣阻力不是一次方的,而是 \[\boxed{\vec f_{D} = -{1 \over 2}\rho v^2 C_{D}A\ \hat v},\] 因此這裡我們是使用這個阻力公式。

@@color:red;font-size:1.5em;底下的標題列按一下可以顯示隱藏內容@@
! 設計場景
//{{{
# from vpython import *		# 使用安裝版的話,這是必要的第一行(得自己寫),開啟 3D 繪圖外掛
GlowScript 3.2 VPython		# 使用 GlowScript 的話,這行會自動產生(不一定是 3.2 無妨),不要更改這一行!

'''
---------------------------------------------------------------------------------------------------------
設計場景
---------------------------------------------------------------------------------------------------------
'''

floor = box(                                    # 產生一個地板
    width=1,                                    # 寬度 1 公尺
    height=0.02,                                # 高度 0.02 公尺(2 公分)
    length=2,                                   # 長度 2 公尺
	opacity=0.5                                 # 半透明
)

spring = helix(                                 # 產生一個彈簧
    pos=floor.pos,                              # 把固定端放在地板的中心
	axis=vector(floor.length/4,0,0),			# 水平放置
	radius=0.03,                                # 半徑 0.03 公尺(3 公分)
	coils=40                                    # 圈數 40
)

ball = sphere(                                  # 產生一顆球
    pos=spring.pos+spring.axis,                 # 放在彈簧的活動端
	radius=0.1,                                 # 半徑 0.1 公尺(10 公分)
    opacity=0.5,                                # 不透明度 0.5
    make_trail=False,                           # 要產生軌跡(但暫時不顯示)
    interval=30                                 # 每 30 次計算留下一個軌跡點
)
//}}}
!!!! 新手提示
<<<
# 這個範例和 [[彈簧運動一]] 的差別只在有摩擦力有阻力,其它部分皆相同,所以如果有疑問,可以參考 [[彈簧運動一]] 裡面的新手提示。
<<<
! 初始條件
//{{{
'''
---------------------------------------------------------------------------------------------------------
初始條件
---------------------------------------------------------------------------------------------------------
'''
muk = 0.1                                           # 球與桌面的動摩擦係數
g = vector(0,-9.8,0)                                # 重力加速度(-y 方向)

spring.L0 = spring.axis.mag                         # 紀錄彈簧原長
spring.k = 50                                       # 設定彈簧常數

ball.mass = 0.5                                     # 球的質量
ball.mg = ball.mass*9.8                             # 球所受到的重力

ball.pos.y = floor.pos.y+floor.height/2+ball.radius # 把球提升到地板上
spring.pos.y = ball.pos.y                           # 把彈簧接到球心
spring.r0 = spring.pos+spring.axis                  # 紀錄彈簧活動端的初始位置

ball.pos.x = spring.pos.x+spring.L0*1.5             # 水平拉長彈簧
ball.velocity = vector(0,0,0)                       # 設定初始速度
ball.acceleration = calcA(ball.pos,ball.velocity,0) # 計算初始加速度
ball.make_trail = True                              # 開始顯示軌跡
//}}}
!!!! 新手提示
<<<
# 這個範例和 [[彈簧運動一]] 的差別只在有摩擦力有阻力,其它部分皆相同,所以如果有疑問,可以參考 [[彈簧運動一]] 裡面的新手提示。
<<<
! 細步計算
在水平桌面上,接在彈簧上的物體受力雖然有重力、桌面的正向力、摩擦力、空氣阻力,以及彈簧的回復力,因為正向力雨重力相互抵消之故,只有彈簧回復力、摩擦力、空氣阻力等會讓物體產生加速度,也就是物體的運動方程為 \[-k\Delta x \hat x - \mu_kmg \hat v - {1 \over 2}\rho v^2 C_{D}A\ \hat v = m\vec a,\] 也就是 \[\vec a = {-k\Delta x \hat x- \mu_kmg \hat v - {1 \over 2}\rho v^2 C_{D}A\ \hat v \over m}.\]
!!!! 空氣阻力
<<tiddler "Air Drag Sphere Python">>
!!!! 四階 ~Runge-Kutta
//{{{
'''
開始:四階 Runge-Kutta 方法
'''

def nextValue(r,v,A,t,dt,a_now = None):
    # 根據現在的位置、速度,計算經過時間 dt 之後的位置、速度、
    # 使用四階榮格-庫塔(Runge-Kutta)演算法
    #       r: 現在位置
    #    	v: 現在速度
	#		A: 計算加速度的函數 function A(r,v,t),是根據現在的位置、速度、時間等因素來計算的
    #           加速度的計算來自受力,只要會計算受力,就會計算加速度
    #
    #           物理上和位能有關的力都和位置有關,例如:重力、彈力、電力、磁力等,所以計算
    #           加速度的函數需要知道物體的位置。
    #
    #           有些和位能無關的力,如空氣阻力,會和速度有關,所以計算加速度的函數可能需要
    #           知道物體的速度
    #
	#           還有一些力可能和位置、速度無關,如粗糙度很一致的接觸面摩擦力,這些可以直接
    #           寫在計算加速度的函數中
    #
    #           有些力會和時間有關,例如週期性的驅動力,如果計算中有這樣的情況,就會需要
    #           知道現在的時間
    #
    #		t: 現在時間
	#		dt: 時間間隔
	# 		a_now: (非必要)現在的壓速度,如果有傳進來的話,可以是純量也可以是向量。

	r1 = r
	v1 = v
	a1 = a_now
	if a1 is None: a1 = A(r1,v1,t)

	dt2 = dt*0.5
	r2 = r+dt2*v1
	v2 = v+dt2*a1
	a2 = A(r2,v2,t+dt2)

	r3 = r+dt2*v2
	v3 = v+dt2*a2
	a3 = A(r3,v3,t+dt2)

	r4 = r+dt*v3
	v4 = v+dt*a3
	a4 = A(r4,v4,t+dt)

	dt /= 6.0
	r += (v1+(v2+v3)*2.0+v4)*dt
	v += (a1+(a2+a3)*2.0+a4)*dt

	return (r,v,A(r,v,t+dt))

'''
結束:四階 Runge-Kutta 方法
'''
//}}}
!!!! 數據作圖
//{{{
# 建立一張空白的曲線圖
x_t = gcurve(label='x 位置',color=color.red)    # 建立位置-時間關係圖( x-t 圖)
//}}}
!!!! 計算加速度
//{{{
def calcA(r,v,t):                                   # 定義計算加速度的函數
    return (
        -spring.k*(r-spring.r0)+                    # 彈簧回復力
        airDragSphere(v,ball.radius)+               # 空氣阻力
        (-muk*ball.mass*9.8)*v.norm()               # 球與桌面的摩擦力
    )/ball.mass                                     # 除以球的質量
//}}}
!!!! 計算每一步
//{{{
dt = 0.001                                          # 每一次計算的時間間隔
t = 0                                               # 初始時間為 0
while t<3:
    rate(1000)
    # 以 四階 Runge-Kutta 方法計算下一個時間點的位置、速度,以及加速度。
    ball.pos,ball.velocity,ball.acceleration = nextValue(
        ball.pos,ball.velocity,calcA,t,dt,ball.acceleration
    )
    spring.axis = ball.pos-spring.pos               # 調整【畫面上】彈簧的長度
                                                    # 畫面上的彈簧並不會自動調整,所以要在這裡手動調整
    
    # 畫一個數據點
    x_t.plot(t,ball.pos.x)                          # 在 x-t 圖畫一個點
    
    t += dt                                         # 時間加上 dt
//}}}
!!!! 新手提示
<<<
# 關於數據作圖的說明,可以參考 [[VPython 的說明網頁|http://www.glowscript.org/docs/VPythonDocs/graph.html]]
<<<
<<foldHeadings>>
!!!! 線上模擬說明
<<<
底下的模擬程式可以玩玩看!
# 按【Start】按鈕開始模擬。過程中隨時可以按【Reset】按鈕重新模擬。
# 勾選 Auto CPF 可以加快模擬過程。
# 讓它跑一會,@@color:red;觀察位置與時間的關係,@@了解所謂''@@color:blue;阻尼震盪的特徵:越來越小的震盪@@''。
# 試著改變球的質量 \(m\) 或是彈簧的彈力常數 \(k\) 或是摩擦係數 \(\mu_k\) (一次只改變一個就好),@@color:blue;觀察看看不同的質量(或彈力常數或摩擦係數)對阻尼震盪的特徵有什麼影響?@@
** 哪一個影響最明顯?
** 有沒有看到【完全不會震盪】的情況?
<<<
!!!! 線上模擬
* @@color:red;取消勾選右方 Auto CPF 可以慢很多,看清楚運動的每一步!@@
** 勾選起來可以快很多!
|彈簧運動模擬|c
|width:45%;<<tw3DCommonPanel "Flow Control">>|width:45%;<<tw3DCommonPanel "Label Statistics">>|
|<<tw3DCommonPanel "Simulation Time Control">>|<<tw3DCommonPanel "Air Drag">>|
|<<tw3DCommonPanel "Center of Mass Control">>|<<tw3DCommonPanel "Trail Control">>|
|<<tiddler "Spring Panel##Ball Control">> / <<tw3DCommonPanel "Friction Control">>|<<tiddler "Spring Panel##Spring Control">>|
|<<tw3DCommonPanel "Plot0 Control">> / <<tw3DCommonPanel "DAQ Control">> / <<tw3DCommonPanel "Gravity Control">><br><<twDataPlot id:dataPlot0>>|<<tw3DCommonPanel "Plot1 Control">> / <<tw3DCommonPanel "Message">><br><<twDataPlot id:dataPlot1>>|
|>|<<tw3DScene [[彈簧運動三 JS Codes]]>>|
>@@font-sze:0.8em;此線上模擬版本係由瀏覽器內建的 Javascript 寫成,以 [[THREE.js|https://threejs.org/]] 做 3D 繪圖,有興趣學習者可以自行上網以【Javascript 學習】、【Javascript 初學】、【Javascript 3D 繪圖】等關鍵字搜尋相關教學文章或影片。
/***
!!! 設計場景
***/
//{{{
//scene.camera.position.z *= 2;

let __tmpV = vector(),										// 共用暫時向量(計算過程使用)

	floor = box({                               			// 產生一個地板
    	width:2,                                    		// 寬度 1 公尺
		height:0.02,                                		// 高度 0.02 公尺(2 公分)
		length:2,                                   		// 長度 2 公尺
		opacity:0.5                                 		// 半透明
	}),

	spring = helix({                            			// 產生一個彈簧
	    pos:floor.pos,                              		// 把固定端放在地板的中心
		axis:vector(floor.getLength()/4,0,0),				// 水平放置
		radius:0.03,                                		// 半徑 0.03 公尺(3 公分)
		coils:40                                    		// 圈數 20
	}),

	ball = sphere({                             			// 產生一顆球
	    pos:__tmpV.copy(spring.position).add(spring.axis),	// 放在彈簧的活動端
		radius:0.1,                                 		// 半徑 0.1 公尺(10 公分)
		opacity:0.5,										// 半透明
	    make_trail:false,                           		// 要產生軌跡(但暫時不顯示)
	    interval:30                                 		// 每 30 次計算留下一個軌跡點
	});

chkGravity.checked = true;									// 開啟模擬重力場
chkGravity.disabled = true;									// 禁止關閉重力場
chkFriction.checked = true;									// 開啟摩擦力

txtTmax.value = 3;
txtSpringK.value = 50;
txtDAQRate.value = 100;
txtSpringL0.value = spring.axis.length();
labelPlot[0].innerHTML = '\\(x(t)\\)';
dataPlot[0].setTitle('\\(x(t)\\)')
	.setYTitle('\\(x \\text{ (m)}\\)').setXTitle('\\(t(s)\\)');
labelPlot[1].innerHTML = '\\(v_x(t)\\)';
dataPlot[1].setTitle('\\(v_x(t)\\)')
	.setYTitle('\\(v_x \\text{ (m/s)}\\)').setXTitle('\\(t(s)\\)');
//}}}
/***
!!! 初始條件
***/
//{{{
let muk = 0.1;
scene.init = () => {
	muk = +txtMuk.value;										// 動摩擦係數
	spring.L0 = +txtSpringL0.value;	                			// 紀錄彈簧原長
	spring.setLength(spring.L0);								// 調整彈簧長度
	spring.k = +txtSpringK.value;                      			// 設定彈簧常數

	ball.mass = +txtBallMass.value;                         	// 球的質量
	ball.mg = ball.mass*9.8;                            		// 球所受到的重力
	ball.setRadius(+txtBallRadius.value);                      	// 球的質量
	spring.setRadius(ball.getRadius()/4);						// 彈簧半徑

	ball.position.y = floor.position.y+floor.getHeight()/2+
			ball.getRadius();									// 把球提升到地板上
	spring.position.y = ball.position.y;                  		// 把彈簧接到球心
	spring.r0 = spring.axis.clone().add(spring.position);  		// 紀錄彈簧活動端的初始位置

	ball.position.x = spring.position.x+spring.L0*1.5;			// 水平拉長/壓縮彈簧
	spring.setLength(spring.L0*1.5);							// 調整彈簧長度

	ball.velocity = vector(0,0,0)                       		// 設定初始速度
	ball.acceleration = calcA(ball.position,ball.velocity,0);	// 計算初始加速度
	ball.clearTrail();
	ball.make_trail = true;                              		// 開始顯示軌跡
}
//}}}
/***
!!! 細步計算
彈簧伸長或者縮短都會產生回復力 \[\vec F = -k\Delta \vec r,\] 其中 \(\Delta \vec r\) 是彈簧的伸長(縮短)向量。
!!!! 細步計算程式碼
***/
//{{{
const calcA = (r,v,t,a) => {									// 定義計算加速度的函數
	if(!a) a = vector();
	a.copy(r).sub(spring.r0).multiplyScalar(-spring.k);			// 彈簧回復力
	if(chkAirDrag.checked)
		a.add($tw.physics.airDragSphere(v,ball.getRadius(),__tmpV));		// 空氣阻力
	if(chkFriction.checked)
		a.add(__tmpV.copy(v).normalize()						// 球與桌面的摩擦力
			.multiplyScalar(-muk*ball.mg)
		);
    return a.multiplyScalar(1/ball.mass);						// 除以球的質量
}

scene.update = (t_cur,dt) => {
	// 以 四階 Runge-Kutta 方法計算下一個時間點的位置、速度,以及加速度。
    $tw.numeric.ODE.nextValue(
		ball.position,											// 現在位置
		ball.velocity,											// 現在速度
		calcA,													// 計算加速度的函數
		t_cur,dt,												// 現在時間,時間間隔
		ball.acceleration										// 現在加速度
	);
	__tmpV.copy(ball.position).sub(spring.position);
    spring.setLength(__tmpV.length());							// 調整【畫面上】彈簧的長度
																// 畫面上的彈簧並不會自動調整,所以要在這裡手動調整
	recordData(scene.currentTime(),null,[ball.position.x,ball.velocity.x]);
}
//}}}
@@font-size:1.5em;''模擬範例 -- 彈簧運動二:簡諧運動的動能、位能、總力學能''@@
----
這個例子裡我們要展示的是【簡諧運動】中動能、位能,以及總力學能隨時間的關係。

我們曉得,【當一個系統(就是某些物體在一起,我們把它們當做一個合體來看)沒有受到外力的時候,這個系統的總力學能是守恆的(也就是不會隨時間而變)】。所謂的力學能包含【動能】以及【位能】,系統的動能很簡單,就是系統中所有質點動能的總和, \[KE = \sum_{i=1}^N{1 \over 2}m_iv_i^2,\] 其中 \(N\) 是系統中質點的個數。整個過程中物體的運動速度會隨時間改變,所以動能也會隨時間改變。

至於系統的位能也是質點位能的總和,\[U = \sum_{i=1}^N u_i,\] 其中 \(u_i\) 是第 \(i\) 個質點的位能。質點的位能,一般而言常見的有【重力位能】、【彈力位能】、【電位能】、【磁位能】等,在這個範例裡不會有電磁位能,加上我們讓整個系統做水平運動,高度沒有改變,所以重力位能是常數,所以過程中會變動的只有彈力位能一項。

彈簧的彈力位能為 \[u_\text{elastic} = {1 \over 2}k\Delta x^2.\]
@@color:red;font-size:1.5em;底下的標題列按一下可以顯示隱藏內容@@
! 設計場景
//{{{
# from vpython import *		# 使用安裝版的話,這是必要的第一行(得自己寫),開啟 3D 繪圖外掛
GlowScript 3.2 VPython		# 使用 GlowScript 的話,這行會自動產生(不一定是 3.2 無妨),不要更改這一行!

'''
---------------------------------------------------------------------------------------------------------
設計場景
---------------------------------------------------------------------------------------------------------
'''

floor = box(                                    # 產生一個地板
    width=1,                                    # 寬度 1 公尺
    height=0.02,                                # 高度 0.02 公尺(2 公分)
	length=2,                                   # 長度 2 公尺
	opacity=0.5                                 # 半透明
)

spring = helix(                                 # 產生一個彈簧
    pos=floor.pos,                              # 把固定端放在地板的中心
	axis=vector(floor.length/4,0,0),			# 水平放置
	radius=0.03,                                # 半徑 0.03 公尺(3 公分)
	coils=20                                    # 圈數 20
)

ball = sphere(                                  # 產生一顆球
    pos=spring.pos+spring.axis,                 # 放在彈簧的活動端
	radius=0.1,                                 # 半徑 0.1 公尺(10 公分)
    make_trail=False,                           # 要產生軌跡(但暫時不顯示)
    interval=30                                 # 每 30 次計算留下一個軌跡點
)
//}}}
!!!! 新手提示
<<<
# 這個範例和 [[彈簧運動一]] 唯一的差別只在數據圖,所以這裡如果有疑問,可以參考 [[彈簧運動一]] 裡面的新手提示。
<<<
! 初始條件
//{{{
'''
---------------------------------------------------------------------------------------------------------
初始條件
---------------------------------------------------------------------------------------------------------
'''
g = vector(0,-9.8,0)                                # 重力加速度(-y 方向)

spring.L0 = spring.axis.mag                         # 紀錄彈簧原長
spring.k = 50                                       # 設定彈簧常數

ball.mass = 0.5                                     # 球的質量
ball.mg = ball.mass*9.8                             # 球所受到的重力

ball.pos.y += ball.radius+floor.height/2            # 把球提升到地板上
spring.pos.y = ball.pos.y                           # 把彈簧接到球心
spring.r0 = spring.pos+spring.axis                  # 紀錄彈簧活動端的初始位置

ball.pos.x += spring.L0/2                           # 水平拉長彈簧
ball.velocity = vector(0,0,0)                       # 設定初始速度
ball.acceleration = calcA(ball.pos,ball.velocity,0) # 計算初始加速度
ball.make_trail = True                              # 開始顯示軌跡
//}}}
!!!! 新手提示
<<<
# 這個範例和 [[彈簧運動一]] 唯一的差別只在數據圖,所以這裡如果有疑問,可以參考 [[彈簧運動一]] 裡面的新手提示。
<<<
! 細步計算
<<tiddler "彈簧運動一##細步計算">>
!!!! 四階 ~Runge-Kutta
//{{{
'''
開始:四階 Runge-Kutta 方法
'''

def nextValue(r,v,A,t,dt,a_now = None):
    # 根據現在的位置、速度,計算經過時間 dt 之後的位置、速度、
    # 使用四階榮格-庫塔(Runge-Kutta)演算法
    #       r: 現在位置
    #    	v: 現在速度
	#		A: 計算加速度的函數 function A(r,v,t),是根據現在的位置、速度、時間等因素來計算的
    #           加速度的計算來自受力,只要會計算受力,就會計算加速度
    #
    #           物理上和位能有關的力都和位置有關,例如:重力、彈力、電力、磁力等,所以計算
    #           加速度的函數需要知道物體的位置。
    #
    #           有些和位能無關的力,如空氣阻力,會和速度有關,所以計算加速度的函數可能需要
    #           知道物體的速度
    #
	#           還有一些力可能和位置、速度無關,如粗糙度很一致的接觸面摩擦力,這些可以直接
    #           寫在計算加速度的函數中
    #
    #           有些力會和時間有關,例如週期性的驅動力,如果計算中有這樣的情況,就會需要
    #           知道現在的時間
    #
    #		t: 現在時間
	#		dt: 時間間隔
	# 		a_now: (非必要)現在的壓速度,如果有傳進來的話,可以是純量也可以是向量。

	r1 = r
	v1 = v
	a1 = a_now
	if a1 is None: a1 = A(r1,v1,t)

	dt2 = dt*0.5
	r2 = r+dt2*v1
	v2 = v+dt2*a1
	a2 = A(r2,v2,t+dt2)

	r3 = r+dt2*v2
	v3 = v+dt2*a2
	a3 = A(r3,v3,t+dt2)

	r4 = r+dt*v3
	v4 = v+dt*a3
	a4 = A(r4,v4,t+dt)

	dt /= 6.0
	r += (v1+(v2+v3)*2.0+v4)*dt
	v += (a1+(a2+a3)*2.0+a4)*dt

	return (r,v,A(r,v,t+dt))

'''
結束:四階 Runge-Kutta 方法
'''
//}}}
!!!! 數據作圖
//{{{
# 建立一張空白的曲線圖
ke_t = gcurve(label='動能',color=color.red)         # 建立動能-時間關係圖(ke-t 圖)
ue_t = gcurve(label='位能',color=color.green)       # 建立位能-時間關係圖(ue-t 圖)
E_t = gcurve(label='總力學能',color=color.blue)     # 建立總力學能-時間關係圖(E-t 圖)
//}}}
!!!! 計算加速度
//{{{
def calcA(r,v,t):                                   # 定義計算加速度的函數
    return (
        -spring.k*(r-spring.r0)                     # 彈簧回復力
    )/ball.mass                                     # 除以球的質量
//}}}
!!!! 計算每一步
//{{{
dt = 0.001                                          # 每一次計算的時間間隔
t = 0                                               # 初始時間為 0
while t<3:
    rate(1000)
    # 以 四階 Runge-Kutta 方法計算下一個時間點的位置、速度,以及加速度。
    ball.pos,ball.velocity,ball.acceleration = nextValue(
        ball.pos,ball.velocity,calcA,t,dt,ball.acceleration
    )
    spring.axis = ball.pos-spring.pos               # 調整【畫面上】彈簧的長度
                                                    # 畫面上的彈簧並不會自動調整,所以要在這裡手動調整
    
    # 畫一個數據點
    
    # 球的動能
    ball.ke = 0.5*ball.mass*ball.velocity.mag2              # 計算球的動能
    # 彈力位能
    spring.ue = 0.5*spring.k*(spring.axis.mag-spring.L0)**2 # 計算彈簧的彈力位能
    # 畫數據圖
    ke_t.plot(t,ball.ke)                                    # 在動能-時間圖畫一個點
    ue_t.plot(t,spring.ue)                                  # 在位能-時間圖畫一個點
    E_t.plot(t,ball.ke+spring.ue)                           # 在總力學能-時間圖畫一個點
    
    t += dt                                                 # 時間加上 dt
//}}}
!!!! 新手提示
<<<
# 關於數據作圖的說明,可以參考 [[VPython 的說明網頁|http://www.glowscript.org/docs/VPythonDocs/graph.html]]
<<<
<<foldHeadings>>
!!!! 線上模擬說明
<<<
底下的模擬程式可以玩玩看!
# 按【Start】按鈕開始模擬。過程中隨時可以按【Reset】按鈕重新模擬。
# 勾選 Auto CPF 可以加快模擬過程。
# 讓它跑一會,@@color:red;觀察動能、位能、總力學能與時間的關係。@@
# 設著改變彈簧初始拉長的程度,然後按 Reset 重新模擬,@@color:red;觀察看看不同的伸長量對動能、位能、總力學能的變化頻率(或者週期)有什麼影響?@@
# 試著改變球的質量 \(m\) 或是彈簧的彈力常數 \(k\)(一次只改變一個就好),@@color:blue;觀察看看不同的質量(或彈力常數)對動能、位能、總力學能的變化頻率(或者週期)有什麼影響?@@
<<<
!!!! 線上模擬
* @@color:red;取消勾選右方 Auto CPF 可以慢很多,看清楚運動的每一步!@@
** 勾選起來可以快很多!
|彈簧運動模擬|c
|width:45%;<<tw3DCommonPanel "Flow Control">>|width:45%;<<tw3DCommonPanel "Label Statistics">>|
|<<tw3DCommonPanel "Simulation Time Control">>|<<tw3DCommonPanel "Air Drag">>|
|<<tw3DCommonPanel "Center of Mass Control">>|<<tw3DCommonPanel "Trail Control">>|
|<<tw3DCommonPanel "Display Control">> / <<tiddler "Spring Panel##Ball Control">>|<<tiddler "Spring Panel##Spring Control">>|
|<<tw3DCommonPanel "Plot0 Control">> / <<tw3DCommonPanel "DAQ Control">> / <<tw3DCommonPanel "Gravity Control">><br><<twDataPlot id:dataPlot0>>|<<tw3DCommonPanel "Plot1 Control">> / <<tw3DCommonPanel "Message">><br><<twDataPlot id:dataPlot1>>|
|>|<<tw3DScene [[彈簧運動二 JS Codes]]>>|
>@@font-sze:0.8em;此線上模擬版本係由瀏覽器內建的 Javascript 寫成,以 [[THREE.js|https://threejs.org/]] 做 3D 繪圖,有興趣學習者可以自行上網以【Javascript 學習】、【Javascript 初學】、【Javascript 3D 繪圖】等關鍵字搜尋相關教學文章或影片。
/***
!!! 設計場景
***/
//{{{
//scene.camera.position.z *= 2;

let __tmpV = vector(),										// 共用暫時向量(計算過程使用)

	floor = box({											// 產生一個地板
	    width:2,                                    		// 寬度 1 公尺
		height:0.02,                                		// 高度 0.02 公尺(2 公分)
		length:2,                                   		// 長度 2 公尺
		opacity:0.5                                 		// 半透明
	}),

	spring = helix({                            			// 產生一個彈簧
	    pos:floor.pos,                              		// 把固定端放在地板的中心
		axis:vector(floor.getLength()/4,0,0),				// 水平放置
		radius:0.03,                                		// 半徑 0.03 公尺(3 公分)
		coils:40                                    		// 圈數 20
	}),

	ball = sphere({                             			// 產生一顆球
	    pos:__tmpV.copy(spring.position).add(spring.axis),	// 放在彈簧的活動端
		radius:0.1,                                 		// 半徑 0.1 公尺(10 公分)
		opacity:0.5,										// 半透明
	    make_trail:false,                           		// 要產生軌跡(但暫時不顯示)
	    interval:30                                 		// 每 30 次計算留下一個軌跡點
	});

chkGravity.checked = true;									// 開啟模擬重力場
chkGravity.disabled = true;									// 禁止關閉重力場
chkAirDrag.checked = false;									// 關閉空氣阻力
chkAirDrag.disabled = true;									// 禁止開啟空氣阻力(後面的範例可以開啟)

txtSpringL0.value = spring.axis.length();
labelPlot[0].innerHTML = '\\(KE(t) \\text{ and } UE(t)\\)';
dataPlot[0].setTitle('\\(KE(t) \\text{ and } UE(t)\\)')
	.setYTitle('\\(KE \\text{ and } UE \\text{ (J)}\\)').setXTitle('\\(t(s)\\)');
labelPlot[1].innerHTML = '\\(E(t)\\)';
dataPlot[1].setTitle('\\(E(t) = KE(t)+UE(t)\\)')
	.setYTitle('\\(E \\text{ (J)}\\)').setXTitle('\\(t(s)\\)');

activateDAQChannels(3);
attachDAQBuffer(0,0);
attachDAQBuffer(0,1);
attachDAQBuffer(1,2);
//}}}
/***
!!! 初始條件
***/
//{{{
scene.init = () => {
	spring.L0 = +txtSpringL0.value;	                			// 紀錄彈簧原長
	spring.setLength(spring.L0);								// 調整彈簧長度
	spring.k = +txtSpringK.value;                      			// 設定彈簧常數

	ball.mass = +txtBallMass.value;                         	// 球的質量
	ball.mg = ball.mass*9.8;                            		// 球所受到的重力
	ball.setRadius(+txtBallRadius.value);                      	// 球的質量
	spring.setRadius(ball.getRadius()/4);						// 彈簧半徑

	ball.position.y = floor.position.y+floor.getHeight()/2+
			ball.getRadius();									// 把球提升到地板上
	spring.position.y = ball.position.y;                  		// 把彈簧接到球心
	spring.r0 = spring.axis.clone().add(spring.position);  		// 紀錄彈簧活動端的初始位置

	ball.position.x = spring.position.x+spring.L0*1.5;			// 水平拉長彈簧
	spring.setLength(spring.L0*1.5);							// 調整彈簧長度

	ball.velocity = vector(0,0,0)                       		// 設定初始速度
	ball.acceleration = calcA(ball.position,ball.velocity,0);	// 計算初始加速度
	ball.clearTrail();
	ball.make_trail = true;                              		// 開始顯示軌跡
}
//}}}
/***
!!! 細步計算
彈簧伸長或者縮短都會產生回復力 \[\vec F = -k\Delta \vec r,\] 其中 \(\Delta \vec r\) 是彈簧的伸長(縮短)向量。
!!!! 細步計算程式碼
***/
//{{{
const calcA = (r,v,t,a) => {									// 定義計算加速度的函數
	if(!a) a = vector();
    return a.copy(r).sub(spring.r0).multiplyScalar(-spring.k)	// 彈簧回復力
    	.multiplyScalar(1/ball.mass);							// 除以球的質量
}

scene.update = (t_cur,dt) => {
	// 以 四階 Runge-Kutta 方法計算下一個時間點的位置、速度,以及加速度。
    $tw.numeric.ODE.nextValue(
		ball.position,											// 現在位置
		ball.velocity,											// 現在速度
		calcA,													// 計算加速度的函數
		t_cur,dt,												// 現在時間,時間間隔
		ball.acceleration										// 現在加速度
	);
	__tmpV.copy(ball.position).sub(spring.position);
    spring.setLength(__tmpV.length());							// 調整【畫面上】彈簧的長度
																// 畫面上的彈簧並不會自動調整,所以要在這裡手動調整
	ball.KE = 0.5*ball.mass*ball.velocity.lengthSq();
	spring.UE = 0.5*spring.k*Math.pow(spring.axis.length()-spring.L0,2);
	recordData(scene.currentTime(),null,[ball.KE,spring.UE,ball.KE+spring.UE]);
}
//}}}
@@font-size:1.5em;''模擬範例 -- 彈簧運動五:簡諧運動(垂直)的位置、速度、加速度''@@
----
垂直情況下的簡諧運動和水平時候([[彈簧運動二]])的能量差別,就是除了動能與彈力位能之外,還有重力位能 \[u_g = mg\Delta y,\] 其中 \(\Delta y\) 是物體''從【位能零點】量起的 \(y\) 位置'',__如果__我們把【__位能零點定在原點__】,那麼這個 \(\Delta y\) 就會剛好是物體的 \(y\) 位置。
! 設計場景
//{{{
floor = box(                                    # 產生一個地板
    width=1,                                    # 寬度 1 公尺
    height=0.02,                                # 高度 0.02 公尺(2 公分)
	length=2,                                   # 長度 2 公尺
	opacity=0.5                                 # 半透明
)

spring = helix(                                 # 產生一個彈簧
    pos=floor.pos,                              # 把固定端放在地板的中心
    axis=vector(0,-floor.length/4,0),			# 垂直向下
	radius=0.03,                                # 半徑 0.03 公尺(3 公分)
	coils=20                                    # 圈數 20
)

ball = sphere(                                  # 產生一顆球
    pos=spring.pos+spring.axis,                 # 放在彈簧的活動端
	radius=0.1,                                 # 半徑 0.1 公尺(10 公分)
    make_trail=False,                           # 要產生軌跡(但暫時不顯示)
    interval=30                                 # 每 30 次計算留下一個軌跡點
)
//}}}
!!!! 新手提示
<<<
# 關於 {{{axis}}} 屬性:
** 在 [[VPython 3D Objects|http://www.vpython.org/contents/docs_vp5/visual/primitives.html]] 這個網頁裡可以看到,具有圓柱形的物體(如[[圓柱體|http://www.vpython.org/contents/docs_vp5/visual/cylinder.html]]、[[箭頭|http://www.vpython.org/contents/docs_vp5/visual/arrow.html]]、以及這個範例裡使用到的[[螺線|http://www.vpython.org/contents/docs_vp5/visual/helix.html]]等,都有一個叫做 {{{axis}}}(軸)的屬性。
** 這個 {{{axis}}} 屬性代表的就是''從一個端點到另一個端點''的向量。
# 如果還有其它疑問,可以參考 [[拋體運動一]]、[[拋體運動二]] 裡面的新手提示。
<<<
! 初始條件
//{{{
g = vector(0,-9.8,0)                                # 重力加速度(-y 方向)

spring.L0 = spring.axis.mag                         # 紀錄彈簧原長
spring.k = 50                                       # 設定彈簧常數
spring.r0 = spring.pos+spring.axis                  # 紀錄彈簧活動端的初始位置

ball.mass = 0.5                                     # 球的質量
ball.Fg = ball.mass*g                               # 球所受到的重力

ball.pos.y -= -ball.mass*9.8/spring.k               # 置於平衡點

ball.velocity = vector(0,0,0)                       # 設定初始速度
ball.acceleration = calcA(ball.pos,ball.velocity,0) # 計算初始加速度
#ball.make_trail = True                              # 開始顯示軌跡
//}}}
!!!! 新手提示
<<<
* 注意彈簧一定要先拉長或壓縮才有回復力,有回復力才能拉動球產生運動。
<<<
! 細步計算
懸掛於垂直彈簧下的物體,由於沒有桌面來提供正向力抵銷重力,必須計算重力才行,加上彈簧的回復力,便是該物體所受的總力(暫時先忽略空氣阻力),也就是物體的運動方程為 \[-k\Delta y \hat y - mg \hat y = m\vec a,\] 也就是 \[\vec a = {-k\Delta y - mg \over m}\hat y,\] 其中 \(\Delta y\) 是從''平衡位置''量起的彈簧伸長(壓縮)量。注意彈簧垂直時候的平衡位置並非在彈簧原長,而是在【伸長到彈力可以抵銷物體重力】,也就是 \[\Delta l_0 = {mg \over k}\] 的地方。
!!!! 數據作圖
//{{{
# 建立一張空白的曲線圖
ke_t = gcurve(label='動能',color=color.red)    # 建立動能-時間關係圖(ke-t 圖)
ue_t = gcurve(label='彈性位能',color=color.green)      # 建立彈性位能-時間關係圖(ue-t 圖)
ug_t = gcurve(label='重力位能',color=color.green)      # 建立重力位能-時間關係圖(ue-t 圖)
E_t = gcurve(label='總力學能',color=color.blue)     # 建立總力學能-時間關係圖(E-t 圖)
//}}}
!!!! 計算加速度
//{{{
def calcA(r,v,t):                                   # 定義計算加速度的函數
    return (
        -spring.k*(r-spring.r0)+                    # 彈簧回復力
        ball.Fg                                     # 重力
    )/ball.mass                                     # 除以球的質量
//}}}
!!!! 計算每一步
//{{{
dt = 0.001                                          # 每一次計算的時間間隔
t = 0                                               # 初始時間為 0
while t<3:
    rate(1000)
    # 以 四階 Runge-Kutta 方法計算下一個時間點的位置、速度,以及加速度。
    ball.pos,ball.velocity,ball.acceleration = nextValue(
        ball.pos,ball.velocity,calcA,t,dt,ball.acceleration
    )
    spring.axis = ball.pos-spring.pos               # 調整【畫面上】彈簧的長度
                                                    # 畫面上的彈簧並不會自動調整,所以要在這裡手動調整
    
    # 畫一個數據點
    
    # 球的動能
    ball.ke = 0.5*ball.mass*ball.velocity.mag2              # 計算球的動能
    # 彈力位能
    spring.ue = 0.5*spring.k*(spring.axis.mag-spring.L0)**2 # 計算彈簧的彈力位能
	# 重力位能
	ball.ug = ball.mass*9.8*ball.position.y					# 重力位能零點定在原點
    # 畫數據圖
    ke_t.plot(t,ball.ke)                                    # 在動能-時間圖畫一個點
    ue_t.plot(t,spring.ue)									# 在位能-時間圖畫一個點
    ug_t.plot(t,ball.ug)									# 在位能-時間圖畫一個點
    E_t.plot(t,ball.ke+spring.ue+ball.ug)					# 在總力學能-時間圖畫一個點
    
    t += dt                                         # 時間加上 dt
//}}}
!!!! 新手提示
<<<
# 關於數據作圖的說明,可以參考 [[VPython 的說明網頁|http://www.glowscript.org/docs/VPythonDocs/graph.html]]
<<<
<<foldHeadings>>
!!!! JS 模擬說明
<<<
底下的模擬是使用 Javascript 寫成,以 [[THREE.js|https://threejs.org/]] 做 3D 繪圖,
# 按【Start】按鈕開始模擬。過程中隨時可以按【Reset】按鈕重新模擬。
# 勾選 Auto CPF 可以加快模擬過程。
# 讓它跑一會,@@color:red;觀察位置、速度、加速度等與時間的關係。@@
# 設著改變彈簧初始拉長的程度,然後按 Reset 重新模擬,@@color:red;觀察看看不同的伸長量對位置、速度、加速度的變化頻率(或者週期)有什麼影響?@@
# 試著改變球的質量 \(m\) 或是彈簧的彈力常數 \(k\)(一次只改變一個就好),@@color:blue;觀察看看不同的質量(或彈力常數)對位置、速度、加速度的變化頻率(或者週期)有什麼影響?@@
# @@color:red;''垂直跟水平的情況有什麼相同之處?有什麼相異之處?''@@
<<<
!!!! JS 模擬
|彈簧運動 JS 版本|c
|width:45%;<<tw3DCommonPanel "Flow Control">> / <<tw3DCommonPanel "Gravity Control">>|width:45%;<<tw3DCommonPanel "Label Statistics">>|
|<<tw3DCommonPanel "Simulation Time Control">>|<<tw3DCommonPanel "Air Drag">>|
|<<tw3DCommonPanel "Display Control">> / <<tiddler "Spring Panel##Ball Control">>|<<tiddler "Spring Panel##Spring Control">>|
|<<tw3DCommonPanel "Plot0 Control">> / <<tw3DCommonPanel "DAQ Control">><br><<twDataPlot id:dataPlot0>>|<<tw3DCommonPanel "Plot1 Control">><br><<twDataPlot id:dataPlot1>>|
|>|<<tw3DScene [[彈簧運動五 JS Codes]]>>|
/***
!!! 設計場景
***/
//{{{
let __tmpV = vector(),											// 共用暫時向量(計算過程使用)

	floor = box({                               				// 產生一個地板
	    width:2,                                    			// 寬度 1 公尺
		height:0.02,                                			// 高度 0.02 公尺(2 公分)
		length:2,                                   			// 長度 2 公尺
		opacity:0.5                                 			// 半透明
	}),

	spring = helix({                            				// 產生一個彈簧
	    pos:floor.pos,                              			// 把固定端放在地板的中心
		axis:vector(0,-floor.getLength()/4,0),					// 水平放置
		radius:0.03,                                			// 半徑 0.03 公尺(3 公分)
		coils:40                                    			// 圈數 20
	}),

	ball = sphere({                             				// 產生一顆球
	    //pos:__tmpV.copy(spring.position).add(spring.axis),	// 放在彈簧的活動端
		radius:0.1,                                 			// 半徑 0.1 公尺(10 公分)
		opacity:0.5,											// 半透明
	    make_trail:false,                           			// 要產生軌跡(但暫時不顯示)
	    interval:30                                 			// 每 30 次計算留下一個軌跡點
	});

chkGravity.checked = true;										// 開啟模擬重力場
chkGravity.disabled = true;										// 禁止關閉重力場
chkAirDrag.checked = false;										// 關閉空氣阻力
chkAirDrag.disabled = true;										// 禁止開啟空氣阻力(後面的範例可以開啟)
txtSpringK.value = 20;											// 設定彈力常數
scene.camera.position.z *= 2.5;									// 攝影機後退到可以看到整個場景的地方

txtSpringL0.value = spring.axis.length();
labelPlot[0].innerHTML = '\\(KE(t) \\text{ and } UE(t)\\)';
dataPlot[0].setTitle('\\(KE(t) \\text{ and } UE(t)\\)')
	.setYTitle('\\(KE \\text{ and } UE \\text{ (J)}\\)').setXTitle('\\(t(s)\\)');
labelPlot[1].innerHTML = '\\(E(t)\\)';
dataPlot[1].setTitle('\\(E(t) = KE(t)+UE(t)\\)')
	.setYTitle('\\(E \\text{ (J)}\\)').setXTitle('\\(t(s)\\)');

activateDAQChannels(4);
attachDAQBuffer(0,0);
attachDAQBuffer(0,1);
attachDAQBuffer(0,2);
attachDAQBuffer(1,3);
//}}}
/***
!!! 初始條件
***/
//{{{
scene.init = () => {
	spring.L0 = +txtSpringL0.value;	                			// 紀錄彈簧原長
	spring.setLength(spring.L0);								// 調整彈簧長度
	spring.k = +txtSpringK.value;                      			// 設定彈簧常數

	ball.mass = +txtBallMass.value;                         	// 球的質量
	ball.Fg = vector(0,-9.8,0).multiplyScalar(ball.mass);		// 球所受到的重力
	ball.setRadius(+txtBallRadius.value);                      	// 球的質量
	spring.setRadius(ball.getRadius()/4);						// 彈簧半徑

	ball.position.copy(spring.position).add(spring.axis);
	ball.position.y -= ball.mass*9.8/spring.k;					// 球置於平衡位置
	spring.r0 = ball.position.clone();							// 紀錄彈簧活動端的初始位置
    spring.setAxis(												// 調整【畫面上】彈簧的長度
		__tmpV.copy(ball.position).sub(spring.position)
	);															// 畫面上的彈簧並不會自動調整,所以要在這裡手動調整

	ball.velocity = vector(0,0,0)                       		// 設定初始速度
	ball.acceleration = calcA(ball.position,ball.velocity,0);	// 計算初始加速度
	ball.clearTrail();
	ball.make_trail = true;                              		// 開始顯示軌跡
}
//}}}
/***
!!! 細步計算
<<tiddler '彈簧運動四##細步計算'>>
!!!! 細步計算程式碼
***/
//{{{
const calcA = (r,v,t,a) => {									// 定義計算加速度的函數
	if(!a) a = vector();
    return a.copy(r).sub(spring.r0).multiplyScalar(-spring.k)	// 彈簧回復力
    	.add(ball.Fg)											// 物體重力
		.multiplyScalar(1/ball.mass);							// 除以球的質量
}

scene.update = (t_cur,dt) => {
	// 以 四階 Runge-Kutta 方法計算下一個時間點的位置、速度,以及加速度。
    $tw.numeric.ODE.nextValue(
		ball.position,											// 現在位置
		ball.velocity,											// 現在速度
		calcA,													// 計算加速度的函數
		t_cur,dt,												// 現在時間,時間間隔
		ball.acceleration										// 現在加速度
	);
    spring.setAxis(												// 調整【畫面上】彈簧的長度
		__tmpV.copy(ball.position).sub(spring.position)
	);															// 畫面上的彈簧並不會自動調整,所以要在這裡手動調整
	ball.KE = 0.5*ball.mass*ball.velocity.lengthSq();
	spring.UE = 0.5*spring.k*Math.pow(spring.axis.length()-spring.L0,2);
	ball.UG = ball.mass*9.8*ball.position.y;
	recordData(scene.currentTime(),null,[ball.KE,ball.UG,spring.UE,ball.KE+ball.UG+spring.UE]);
}
//}}}
@@font-size:1.5em;''模擬範例 -- 彈簧運動四:簡諧運動(垂直)的位置、速度、加速度''@@
----
這個例子裡我們要讓彈簧變成垂直的,也就是物體是懸掛在彈簧下端沒有桌面支撐的。這種情況下的運動仍然是【簡諧運動】,不過和水平狀([[彈簧運動一]])況不同的是,平衡點不是在彈簧原長的地方,而是在【彈簧伸長到回復力足以抵銷懸掛物體重力的位置】,也就是在伸長到 \[k \Delta l_0 = mg \quad \to \quad \Delta l_0 = {mg \over k}\] 的位置。

物體受力的計算,由於沒有桌面提供正向力來抵銷,必須將重力計算進去,加速度的計算公式在下方細步計算區塊中有列出。
! 設計場景
//{{{
floor = box(                                    # 產生一個地板
    width=1,                                    # 寬度 1 公尺
    height=0.02,                                # 高度 0.02 公尺(2 公分)
    length=2,                                   # 長度 2 公尺
	opacity=0.5                                 # 半透明
)

spring = helix(                                 # 產生一個彈簧
    pos=floor.pos,                              # 把固定端放在地板的中心
    axis=vector(0,-floor.length/4,0),			# 垂直向下
	radius=0.03,                                # 半徑 0.03 公尺(3 公分)
	coils=20                                    # 圈數 20
)

ball = sphere(                                  # 產生一顆球
    pos=spring.pos+spring.axis,                 # 放在彈簧的活動端
	radius=0.1,                                 # 半徑 0.1 公尺(10 公分)
    make_trail=False,                           # 要產生軌跡(但暫時不顯示)
    interval=30                                 # 每 30 次計算留下一個軌跡點
)
//}}}
!!!! 新手提示
<<<
# 關於 {{{axis}}} 屬性:
** 在 [[VPython 3D Objects|http://www.vpython.org/contents/docs_vp5/visual/primitives.html]] 這個網頁裡可以看到,具有圓柱形的物體(如[[圓柱體|http://www.vpython.org/contents/docs_vp5/visual/cylinder.html]]、[[箭頭|http://www.vpython.org/contents/docs_vp5/visual/arrow.html]]、以及這個範例裡使用到的[[螺線|http://www.vpython.org/contents/docs_vp5/visual/helix.html]]等,都有一個叫做 {{{axis}}}(軸)的屬性。
** 這個 {{{axis}}} 屬性代表的就是''從一個端點到另一個端點''的向量。
# 如果還有其它疑問,可以參考 [[拋體運動一]]、[[拋體運動二]] 裡面的新手提示。
<<<
! 初始條件
//{{{
g = vector(0,-9.8,0)                                # 重力加速度(-y 方向)

spring.L0 = spring.axis.mag                         # 紀錄彈簧原長
spring.k = 50                                       # 設定彈簧常數
spring.r0 = spring.pos+spring.axis                  # 紀錄彈簧活動端的初始位置

ball.mass = 0.5                                     # 球的質量
ball.Fg = ball.mass*g                               # 球所受到的重力

ball.pos.y -= -ball.mass*9.8/spring.k               # 置於平衡點

ball.velocity = vector(0,0,0)                       # 設定初始速度
ball.acceleration = calcA(ball.pos,ball.velocity,0) # 計算初始加速度
#ball.make_trail = True                             # 開始顯示軌跡
//}}}
!!!! 新手提示
<<<
* 注意彈簧一定要先拉長或壓縮才有回復力,有回復力才能拉動球產生運動。
<<<
! 細步計算
懸掛於垂直彈簧下的物體,由於沒有桌面來提供正向力抵銷重力,必須計算重力才行,加上彈簧的回復力,便是該物體所受的總力(暫時先忽略空氣阻力),也就是物體的運動方程為 \[-k\Delta y \hat y - mg \hat y = m\vec a,\] 也就是 \[\vec a = {-k\Delta y - mg \over m}\hat y,\] 其中 \(\Delta y\) 是從''平衡位置''量起的彈簧伸長(壓縮)量。注意彈簧垂直時候的平衡位置並非在彈簧原長,而是在【伸長到彈力可以抵銷物體重力】,也就是 \[\Delta l_0 = {mg \over k}\] 的地方。
!!!! 數據作圖
//{{{
# 建立一張空白的曲線圖
y_t = gcurve(label='y 位置',color=color.red)    # 建立位置-時間關係圖(y-t 圖)
//}}}
!!!! 計算加速度
//{{{
def calcA(r,v,t):                                   # 定義計算加速度的函數
    return (
        -spring.k*(r-spring.r0)+                    # 彈簧回復
        airDragSphere(ball.velocity,ball.radius)+   # 空氣阻力
        ball.Fg                                     # 重力
    )/ball.mass                                     # 除以球的質量
//}}}
!!!! 計算每一步
//{{{
dt = 0.001                                          # 每一次計算的時間間隔
t = 0                                               # 初始時間為 0
while t<3:
    rate(1000)
    # 以 四階 Runge-Kutta 方法計算下一個時間點的位置、速度,以及加速度。
    ball.pos,ball.velocity,ball.acceleration = nextValue(
        ball.pos,ball.velocity,calcA,t,dt,ball.acceleration
    )
    spring.axis = ball.pos-spring.pos               # 調整【畫面上】彈簧的長度
                                                    # 畫面上的彈簧並不會自動調整,所以要在這裡手動調整
    
    # 畫一個數據點
    y_t.plot(t,ball.pos.y)                          # 在 y-t 圖畫一個點
    
    t += dt                                         # 時間加上 dt
//}}}
!!!! 新手提示
<<<
# 關於數據作圖的說明,可以參考 [[VPython 的說明網頁|http://www.glowscript.org/docs/VPythonDocs/graph.html]]
<<<
<<foldHeadings>>
!!!! JS 模擬說明
<<<
底下的模擬是使用 Javascript 寫成,以 [[THREE.js|https://threejs.org/]] 做 3D 繪圖,
# 按【Start】按鈕開始模擬。過程中隨時可以按【Reset】按鈕重新模擬。
# 勾選 Auto CPF 可以加快模擬過程。
# 讓它跑一會,@@color:red;觀察位置、速度、加速度等與時間的關係。@@
# 設著改變彈簧初始拉長的程度,然後按 Reset 重新模擬,@@color:red;觀察看看不同的伸長量對位置、速度、加速度的變化頻率(或者週期)有什麼影響?@@
# 試著改變球的質量 \(m\) 或是彈簧的彈力常數 \(k\)(一次只改變一個就好),@@color:blue;觀察看看不同的質量(或彈力常數)對位置、速度、加速度的變化頻率(或者週期)有什麼影響?@@
# @@color:red;''垂直跟水平的情況有什麼相同之處?有什麼相異之處?''@@
<<<
!!!! JS 模擬
|彈簧運動 JS 版本|c
|width:45%;<<tw3DCommonPanel "Flow Control">> /  <<tw3DCommonPanel "Gravity Control">>|width:45%;<<tw3DCommonPanel "Label Statistics">>|
|<<tw3DCommonPanel "Simulation Time Control">>|<<tw3DCommonPanel "Air Drag">>|
|<<tw3DCommonPanel "Display Control">> / <<tiddler "Spring Panel##Ball Control">>|<<tiddler "Spring Panel##Spring Control">>|
|<<tw3DCommonPanel "Plot0 Control">> / <<tw3DCommonPanel "DAQ Control">><br><<twDataPlot id:dataPlot0>>|<<tw3DCommonPanel "Plot1 Control">><br><<twDataPlot id:dataPlot1>>|
|>|<<tw3DScene [[彈簧運動六 JS Codes]]>>|
/***
!!! 設計場景
***/
//{{{
let __tmpV = vector(),										// 共用暫時向量(計算過程使用)

	floor = box({                               			// 產生一個地板
	    width:2,                                    		// 寬度 1 公尺
		height:0.02,                                		// 高度 0.02 公尺(2 公分)
		length:2,                                   		// 長度 2 公尺
		opacity:0.5                                 		// 半透明
	}),

	spring = helix({                            			// 產生一個彈簧
	    pos:floor.pos,                              		// 把固定端放在地板的中心
		axis:vector(0,-floor.getLength()/4,0),				// 垂直放置
		radius:0.03,                                		// 半徑 0.03 公尺(3 公分)
		coils:40                                    		// 圈數 20
	}),

	ball = sphere({                             			// 產生一顆球
	    pos:__tmpV.copy(spring.position).add(spring.axis),	// 放在彈簧的活動端
		radius:0.1,                                 		// 半徑 0.1 公尺(10 公分)
		opacity:0.5,										// 半透明
	    make_trail:false,                           		// 要產生軌跡(但暫時不顯示)
	    interval:30                                 		// 每 30 次計算留下一個軌跡點
	});

chkGravity.checked = true;									// 開啟模擬重力場
chkGravity.disabled = true;									// 禁止關閉重力場
chkAirDrag.checked = true;									// 開啟空氣阻力
//chkAirDrag.disabled = true;								// 禁止關閉空氣阻力
txtSpringK.value = 20;										// 設定彈力常數
scene.camera.position.z *= 2.5;								// 攝影機後退到可以看到整個場景的地方

txtSpringL0.value = spring.axis.length();
labelPlot[0].innerHTML = '\\(y(t), v_y(t)\\)';
dataPlot[0].setTitle('\\(y(t), v_y(t)\\)')
	.setYTitle('\\(y \\text{ (m)}, v_y \\text{ m/s}\\)').setXTitle('\\(t(s)\\)');
labelPlot[1].innerHTML = '\\(a_y(t)\\)';
dataPlot[1].setTitle('\\(a_y(t)\\)')
	.setYTitle('\\(a_y \\text{ (m/s}^2\\text{)}\\)').setXTitle('\\(t(s)\\)');

activateDAQChannels(3);
attachDAQBuffer(0,0);
attachDAQBuffer(0,1);
attachDAQBuffer(1,2);
//}}}
/***
!!! 初始條件
***/
//{{{
scene.init = () => {
	spring.L0 = +txtSpringL0.value;	                			// 紀錄彈簧原長
	spring.setLength(spring.L0);								// 調整彈簧長度
	spring.k = +txtSpringK.value;                      			// 設定彈簧常數

	ball.mass = +txtBallMass.value;                         	// 球的質量
	ball.Fg = vector(0,-9.8,0).multiplyScalar(ball.mass);		// 球所受到的重力
	ball.setRadius(+txtBallRadius.value);                      	// 球的質量
	spring.setRadius(ball.getRadius()/4);						// 彈簧半徑

	ball.position.copy(spring.position).add(spring.axis);
	ball.position.y -= ball.mass*9.8/spring.k;					// 球置於平衡位置
	spring.r0 = ball.position.clone();							// 紀錄彈簧活動端的初始位置
    spring.setAxis(												// 調整【畫面上】彈簧的長度
		__tmpV.copy(ball.position).sub(spring.position)
	);															// 畫面上的彈簧並不會自動調整,所以要在這裡手動調整

	ball.velocity = vector(0,0,0)                       		// 設定初始速度
	ball.acceleration = calcA(ball.position,ball.velocity,0);	// 計算初始加速度
	ball.clearTrail();
	ball.make_trail = true;                              		// 開始顯示軌跡
}
//}}}
/***
!!! 細步計算
<<tiddler "彈簧運動四##細步計算">>
!!!! 細步計算程式碼
***/
//{{{
const calcA = (r,v,t,a) => {									// 定義計算加速度的函數
	if(!a) a = vector();
	a.copy(r).sub(spring.r0).multiplyScalar(-spring.k)			// 彈簧回復力
    	.add(ball.Fg);											// 物體重力
	if(chkAirDrag.checked)
		a.add($tw.physics.airDragSphere(v,ball.getRadius(),__tmpV));		// 空氣阻力
    return a.multiplyScalar(1/ball.mass);						// 除以球的質量
}

scene.update = (t_cur,dt) => {
	// 以 四階 Runge-Kutta 方法計算下一個時間點的位置、速度,以及加速度。
    $tw.numeric.ODE.nextValue(
		ball.position,											// 現在位置
		ball.velocity,											// 現在速度
		calcA,													// 計算加速度的函數
		t_cur,dt,												// 現在時間,時間間隔
		ball.acceleration										// 現在加速度
	);
    spring.setAxis(												// 調整【畫面上】彈簧的長度
		__tmpV.copy(ball.position).sub(spring.position)
	);															// 畫面上的彈簧並不會自動調整,所以要在這裡手動調整
	recordData(scene.currentTime(),null,[ball.position.y,ball.velocity.y,ball.acceleration.y]);
}
//}}}
@@font-size:1.5em;''模擬範例 -- 彈簧運動四:簡諧運動(垂直)的位置、速度、加速度''@@
----
這個例子裡我們要讓彈簧變成垂直的,也就是物體是懸掛在彈簧下端沒有桌面支撐的。這種情況下的運動仍然是【簡諧運動】,不過和水平狀([[彈簧運動一]])況不同的是,平衡點不是在彈簧原長的地方,而是在【彈簧伸長到回復力足以抵銷懸掛物體重力的位置】,也就是在伸長到 \[k \Delta l_0 = mg \quad \to \quad \Delta l_0 = {mg \over k}\] 的位置。

物體受力的計算,由於沒有桌面提供正向力來抵銷,必須將重力計算進去,加速度的計算公式在下方細步計算區塊中有列出。

@@color:red;font-size:1.5em;底下的標題列按一下可以顯示隱藏內容@@
! 設計場景
//{{{
# from vpython import *		# 使用安裝版的話,這是必要的第一行(得自己寫),開啟 3D 繪圖外掛
GlowScript 3.2 VPython		# 使用 GlowScript 的話,這行會自動產生(不一定是 3.2 無妨),不要更改這一行!

'''
---------------------------------------------------------------------------------------------------------
設計場景
---------------------------------------------------------------------------------------------------------
'''

floor = box(                                    # 產生一個地板
    width=1,                                    # 寬度 1 公尺
    height=0.02,                                # 高度 0.02 公尺(2 公分)
	length=2,                                   # 長度 2 公尺
	opacity=0.5                                 # 半透明
)

spring = helix(                                 # 產生一個彈簧
    pos=floor.pos,                              # 把固定端放在地板的中心
    axis=vector(0,-floor.length/4,0),			# 垂直向下
	radius=0.03,                                # 半徑 0.03 公尺(3 公分)
	coils=20                                    # 圈數 20
)

ball = sphere(                                  # 產生一顆球
    pos=spring.pos+spring.axis,                 # 放在彈簧的活動端
	radius=0.1,                                 # 半徑 0.1 公尺(10 公分)
    make_trail=False,                           # 要產生軌跡(但暫時不顯示)
    interval=30                                 # 每 30 次計算留下一個軌跡點
)
//}}}
!!!! 新手提示
<<<
# 關於 {{{axis}}} 屬性:
** 在 [[VPython 3D Objects|http://www.vpython.org/contents/docs_vp5/visual/primitives.html]] 這個網頁裡可以看到,具有圓柱形的物體(如[[圓柱體|http://www.vpython.org/contents/docs_vp5/visual/cylinder.html]]、[[箭頭|http://www.vpython.org/contents/docs_vp5/visual/arrow.html]]、以及這個範例裡使用到的[[螺線|http://www.vpython.org/contents/docs_vp5/visual/helix.html]]等,都有一個叫做 {{{axis}}}(軸)的屬性。
** 這個 {{{axis}}} 屬性代表的就是''從一個端點到另一個端點''的向量。
# 如果還有其它疑問,可以參考 [[拋體運動一]]、[[拋體運動二]] 裡面的新手提示。
<<<
! 初始條件
//{{{
'''
---------------------------------------------------------------------------------------------------------
初始條件
---------------------------------------------------------------------------------------------------------
'''
g = vector(0,-9.8,0)                                # 重力加速度(-y 方向)

spring.L0 = spring.axis.mag                         # 紀錄彈簧原長
spring.k = 50                                       # 設定彈簧常數
spring.r0 = spring.pos+spring.axis                  # 紀錄彈簧活動端的初始位置

ball.mass = 0.5                                     # 球的質量
ball.Fg = ball.mass*g                               # 球所受到的重力

ball.pos.y -= -ball.mass*9.8/spring.k               # 置於平衡點

ball.velocity = vector(0,0,0)                       # 設定初始速度
ball.acceleration = calcA(ball.pos,ball.velocity,0) # 計算初始加速度
#ball.make_trail = True                              # 開始顯示軌跡
//}}}
!!!! 新手提示
<<<
* 注意彈簧一定要先拉長或壓縮才有回復力,有回復力才能拉動球產生運動。
<<<
! 細步計算
懸掛於垂直彈簧下的物體,由於沒有桌面來提供正向力抵銷重力,必須計算重力才行,加上彈簧的回復力,便是該物體所受的總力(暫時先忽略空氣阻力),也就是物體的運動方程為 \[-k\Delta y \hat y - mg \hat y = m\vec a,\] 也就是 \[\vec a = {-k\Delta y - mg \over m}\hat y,\] 其中 \(\Delta y\) 是從''平衡位置''量起的彈簧伸長(壓縮)量。注意彈簧垂直時候的平衡位置並非在彈簧原長,而是在【伸長到彈力可以抵銷物體重力】,也就是 \[\Delta l_0 = {mg \over k}\] 的地方。
!!!! 四階 ~Runge-Kutta
//{{{
'''
開始:四階 Runge-Kutta 方法
'''

def nextValue(r,v,A,t,dt,a_now = None):
    # 根據現在的位置、速度,計算經過時間 dt 之後的位置、速度、
    # 使用四階榮格-庫塔(Runge-Kutta)演算法
    #       r: 現在位置
    #    	v: 現在速度
	#		A: 計算加速度的函數 function A(r,v,t),是根據現在的位置、速度、時間等因素來計算的
    #           加速度的計算來自受力,只要會計算受力,就會計算加速度
    #
    #           物理上和位能有關的力都和位置有關,例如:重力、彈力、電力、磁力等,所以計算
    #           加速度的函數需要知道物體的位置。
    #
    #           有些和位能無關的力,如空氣阻力,會和速度有關,所以計算加速度的函數可能需要
    #           知道物體的速度
    #
	#           還有一些力可能和位置、速度無關,如粗糙度很一致的接觸面摩擦力,這些可以直接
    #           寫在計算加速度的函數中
    #
    #           有些力會和時間有關,例如週期性的驅動力,如果計算中有這樣的情況,就會需要
    #           知道現在的時間
    #
    #		t: 現在時間
	#		dt: 時間間隔
	# 		a_now: (非必要)現在的壓速度,如果有傳進來的話,可以是純量也可以是向量。

	r1 = r
	v1 = v
	a1 = a_now
	if a1 is None: a1 = A(r1,v1,t)

	dt2 = dt*0.5
	r2 = r+dt2*v1
	v2 = v+dt2*a1
	a2 = A(r2,v2,t+dt2)

	r3 = r+dt2*v2
	v3 = v+dt2*a2
	a3 = A(r3,v3,t+dt2)

	r4 = r+dt*v3
	v4 = v+dt*a3
	a4 = A(r4,v4,t+dt)

	dt /= 6.0
	r += (v1+(v2+v3)*2.0+v4)*dt
	v += (a1+(a2+a3)*2.0+a4)*dt

	return (r,v,A(r,v,t+dt))

'''
結束:四階 Runge-Kutta 方法
'''
//}}}
!!!! 數據作圖
//{{{
# 建立一張空白的曲線圖
y_t = gcurve(label='y 位置 x10',color=color.red)    # 建立位置-時間關係圖( y-t 圖)
v_t = gcurve(label='y 速度',color=color.green)      # 建立速度-時間關係圖( v-t 圖)
a_t = gcurve(label='y 加速度',color=color.blue)     # 建立加速度-時間關係圖( a-t 圖)
//}}}
!!!! 計算加速度
//{{{
def calcA(r,v,t):                                   # 定義計算加速度的函數
    return (
        -spring.k*(r-spring.r0)+                    # 彈簧回復力
        ball.Fg                                     # 重力
    )/ball.mass                                     # 除以球的質量
//}}}
!!!! 計算每一步
//{{{
dt = 0.001                                          # 每一次計算的時間間隔
t = 0                                               # 初始時間為 0
while t<3:
    rate(1000)
    # 以 四階 Runge-Kutta 方法計算下一個時間點的位置、速度,以及加速度。
    ball.pos,ball.velocity,ball.acceleration = nextValue(
        ball.pos,ball.velocity,calcA,t,dt,ball.acceleration
    )
    spring.axis = ball.pos-spring.pos               # 調整【畫面上】彈簧的長度
                                                    # 畫面上的彈簧並不會自動調整,所以要在這裡手動調整
    
    # 畫一個數據點
    y_t.plot(t,ball.pos.y*10)                       # 在 y-t 圖畫一個點
    v_t.plot(t,ball.velocity.y)                     # 在 v-t 圖畫一個點
    a_t.plot(t,ball.acceleration.y)                 # 在 a-t 圖畫一個點
    
    t += dt                                         # 時間加上 dt
//}}}
!!!! 新手提示
<<<
# 關於數據作圖的說明,可以參考 [[VPython 的說明網頁|http://www.glowscript.org/docs/VPythonDocs/graph.html]]
<<<
<<foldHeadings>>
!!!! 線上模擬說明
<<<
底下的模擬程式可以玩看看!
# 按【Start】按鈕開始模擬。過程中隨時可以按【Reset】按鈕重新模擬。
# 勾選 Auto CPF 可以加快模擬過程。
# 讓它跑一會,@@color:red;觀察位置、速度、加速度等與時間的關係。@@
# 設著改變彈簧初始拉長的程度,然後按 Reset 重新模擬,@@color:red;觀察看看不同的伸長量對位置、速度、加速度的變化頻率(或者週期)有什麼影響?@@
# 試著改變球的質量 \(m\) 或是彈簧的彈力常數 \(k\)(一次只改變一個就好),@@color:blue;觀察看看不同的質量(或彈力常數)對位置、速度、加速度的變化頻率(或者週期)有什麼影響?@@
# @@color:red;''垂直跟水平的情況有什麼相同之處?有什麼相異之處?''@@
<<<
!!!! 線上模擬
* @@color:red;取消勾選右方 Auto CPF 可以慢很多,看清楚運動的每一步!@@
** 勾選起來可以快很多!
|彈簧運動模擬|c
|width:45%;<<tw3DCommonPanel "Flow Control">>|width:45%;<<tw3DCommonPanel "Label Statistics">>|
|<<tw3DCommonPanel "Simulation Time Control">>|<<tw3DCommonPanel "Air Drag">>|
|<<tw3DCommonPanel "Center of Mass Control">>|<<tw3DCommonPanel "Trail Control">>|
|<<tw3DCommonPanel "Display Control">> / <<tiddler "Spring Panel##Ball Control">>|<<tiddler "Spring Panel##Spring Control">>|
|<<tw3DCommonPanel "Plot0 Control">> / <<tw3DCommonPanel "DAQ Control">> / <<tw3DCommonPanel "Gravity Control">><br><<twDataPlot id:dataPlot0>>|<<tw3DCommonPanel "Plot1 Control">> / <<tw3DCommonPanel "Message">><br><<twDataPlot id:dataPlot1>>|
|>|<<tw3DScene [[彈簧運動四 JS Codes]]>>|
>@@font-sze:0.8em;此線上模擬版本係由瀏覽器內建的 Javascript 寫成,以 [[THREE.js|https://threejs.org/]] 做 3D 繪圖,有興趣學習者可以自行上網以【Javascript 學習】、【Javascript 初學】、【Javascript 3D 繪圖】等關鍵字搜尋相關教學文章或影片。
/***
!!! 設計場景
***/
//{{{
let __tmpV = vector(),											// 共用暫時向量(計算過程使用)

	floor = box({                               				// 產生一個地板
    	width:2,                                    			// 寬度 1 公尺
		height:0.02,                                			// 高度 0.02 公尺(2 公分)
		length:2,                                   			// 長度 2 公尺
		opacity:0.5                                 			// 半透明
	}),

	spring = helix({                            				// 產生一個彈簧
	    pos:floor.pos,                              			// 把固定端放在地板的中心
		axis:vector(0,-floor.getLength()/4,0),					// 水平放置
		radius:0.03,                                			// 半徑 0.03 公尺(3 公分)
		coils:40                                    			// 圈數 20
	}),

	ball = sphere({                             				// 產生一顆球
	    //pos:__tmpV.copy(spring.position).add(spring.axis),	// 放在彈簧的活動端
		radius:0.1,                                 			// 半徑 0.1 公尺(10 公分)
		opacity:0.5,											// 半透明
	    make_trail:false,                           			// 要產生軌跡(但暫時不顯示)
	    interval:30                                 			// 每 30 次計算留下一個軌跡點
	});

chkGravity.checked = true;										// 開啟模擬重力場
chkGravity.disabled = true;										// 禁止關閉重力場
chkAirDrag.checked = false;										// 關閉空氣阻力
chkAirDrag.disabled = true;										// 禁止開啟空氣阻力(後面的範例可以開啟)
txtSpringK.value = 20;											// 設定彈力常數
scene.camera.position.z *= 1.2;									// 攝影機後退到可以看到整個場景的地方

txtSpringL0.value = spring.axis.length();
labelPlot[0].innerHTML = '\\(y(t)\\)';
dataPlot[0].setTitle('\\(y(t)\\)')
	.setYTitle('\\(y \\text{ (m)}\\)').setXTitle('\\(t(s)\\)');
labelPlot[1].innerHTML = '\\(v_y(t)\\)';
dataPlot[1].setTitle('\\(v_y(t)\\)')
	.setYTitle('\\(v_y \\text{ (m/s)}\\)').setXTitle('\\(t(s)\\)');

activateDAQChannels(3);
attachDAQBuffer(0,0);
attachDAQBuffer(0,1);
attachDAQBuffer(1,2);
//}}}
/***
!!! 初始條件
***/
//{{{
scene.init = () => {
	spring.L0 = +txtSpringL0.value;	                			// 紀錄彈簧原長
	spring.setLength(spring.L0);								// 調整彈簧長度
	spring.k = +txtSpringK.value;                      			// 設定彈簧常數

	ball.mass = +txtBallMass.value;                         	// 球的質量
	ball.Fg = vector(0,-9.8,0).multiplyScalar(ball.mass);		// 球所受到的重力
	ball.setRadius(+txtBallRadius.value);                      	// 球的質量
	spring.setRadius(ball.getRadius()/4);						// 彈簧半徑

	ball.position.copy(spring.position).add(spring.axis);
	ball.position.y -= ball.mass*9.8/spring.k;					// 球置於平衡位置
	spring.r0 = ball.position.clone();							// 紀錄彈簧活動端的初始位置
    spring.setAxis(												// 調整【畫面上】彈簧的長度
		__tmpV.copy(ball.position).sub(spring.position)
	);															// 畫面上的彈簧並不會自動調整,所以要在這裡手動調整

	ball.velocity = vector(0,0,0)                       		// 設定初始速度
	ball.acceleration = calcA(ball.position,ball.velocity,0);	// 計算初始加速度
	ball.clearTrail();
	ball.make_trail = true;                              		// 開始顯示軌跡
}
//}}}
/***
!!! 細步計算
<<tiddler '彈簧運動四##細步計算'>>
!!!! 細步計算程式碼
***/
//{{{
const calcA = (r,v,t,a) => {									// 定義計算加速度的函數
	if(!a) a = vector();
    return a.copy(r).sub(spring.r0).multiplyScalar(-spring.k)	// 彈簧回復力
    	.add(ball.Fg)											// 物體重力
		.multiplyScalar(1/ball.mass);							// 除以球的質量
}

scene.update = (t_cur,dt) => {
	// 以 四階 Runge-Kutta 方法計算下一個時間點的位置、速度,以及加速度。
    $tw.numeric.ODE.nextValue(
		ball.position,											// 現在位置
		ball.velocity,											// 現在速度
		calcA,													// 計算加速度的函數
		t_cur,dt,												// 現在時間,時間間隔
		ball.acceleration										// 現在加速度
	);
    spring.setAxis(												// 調整【畫面上】彈簧的長度
		__tmpV.copy(ball.position).sub(spring.position)
	);															// 畫面上的彈簧並不會自動調整,所以要在這裡手動調整
	recordData(scene.currentTime(),null,[ball.position.y,ball.velocity.y,ball.acceleration.y]);
}
//}}}
在處理力學相關問題時,如果一個系統的運動方程是未知的,則我們通常會使用拉格朗日方法(Lagrange's Method)來獲得,它的 [[背後想法|拉格朗日方法的原理]] 有點抽象,但做法上卻很明確而直接,只有兩個步驟:
# 寫下系統的拉格朗日量(Lagrangian) \[\begin{equation}L(x, \dot x, t) = T - U,\end{equation}\] 其中 \(T\) 為系統的總動能,而 \(U\) 為系統的總位能。
** 通常動能是速度 \(\boxed{\dot x \equiv dx/dt}\) 的函數,而位能是位置 \(x\) 的函數,因此拉格朗日量會是速度及位置的函數。
** 當然一般情況下也可以是時間的函數,所以我們把它寫成 \(L = L(x, \dot x, t)\)。
# 系統的運動方程為(如果沒有耗散的話) \[\begin{equation}{d \over dt}{\partial L \over \partial \dot x} - {\partial L \over \partial x} = 0.\end{equation}\]
!! 範例一、彈簧系統
我們先用一個簡單且已知結果的系統,彈簧質量系統,當範例來看看如何使用拉格朗日法,這個系統的運動方程我們已經知道是 \[\begin{equation}f = -kx \quad \to \quad \boxed{m\ddot x = -kx},\end{equation}\] 拿它來做練習可以很容易確定有沒有做對。

我們按照上面的步驟來進行:
# 按照 (1) 式寫下拉格朗日量 \[L = T - U = {1 \over 2}m\dot x^2 - {1 \over 2}kx^2.\]
# 按照 (2) 式寫下運動方程 \begin{aligned}{d \over dt}{\partial L \over \partial \dot x} - {\partial L \over \partial x} &= 0 \nonumber\\ {d \over dt}m\dot x - (-kx) &= 0 \\ m\ddot x + kx &= 0.\end{aligned}
比較 (4) 式和 (3) 式我們知道沒有做錯。對於彈簧質量系統這個過程很容易,對吧!
!! 範例二、擺
接下來我們再看一個範例:擺,這個系統的運動方程也是已知的 \[\begin{equation}\ddot\theta = -{g \over l}\sin\theta.\end{equation}\] 我們一樣按照上述步驟:
# 按照 (1) 式寫下拉格朗日量 \[L = T - U = {1 \over 2}ml^2\dot\theta^2 - mg(1-\cos\theta).\]
# 按照 (2) 式寫下運動方程 \begin{aligned}{d \over dt}{\partial L \over \partial \dot\theta} - {\partial L \over \partial \theta} &= 0 \\ {d \over dt}(ml^2\dot\theta) - mgl(-\sin\theta) &= 0 \\ ml^2\ddot\theta + mgl\sin\theta &= 0 \\ \ddot \theta + {g \over l}\sin\theta &=& 0.\end{aligned}
比較 (6) 式和 (5) 式我們知道沒有做錯。對於擺而言這個過程也是很容易的,對吧!
!! 一般情況
上面兩個簡單的範例應該可以讓我們了解到如何使用拉格朗日法,對於實際上無法或者不容易事先知道運動方程的系統,此方法為目前公認得到運動方程的直接方法,有了運動方程我們就可以對其求解(如 [[微分方程的數值求解]]),進而了解該系統的運動狀況。
參考 [[維基百科拉格朗日力學(簡略)|https://zh.wikipedia.org/wiki/%E6%8B%89%E6%A0%BC%E6%9C%97%E6%97%A5%E5%8A%9B%E5%AD%A6]] 或 [[Wikipedia Lagrangian mechanics page(詳細)|https://en.wikipedia.org/wiki/Lagrangian_mechanics]]
拋體運動可以說是力學運動最簡單的範例之一,它的主要結果:拋物線軌跡、45&deg; 拋射距離最遠(不計空氣阻力的話)等等,讀過中學物理的人都會知道。由於它很簡單,非常適合最為力學模擬的第一個範例,所以本系列第一個範例便是拋體運動。

底下三個範例分別展示【忽略空氣阻力】、【一次方空氣阻力】,以及【二次方空氣阻力】等情況,__按一下標題就可以拉出相關的內容__,按按看吧。
!!! 拋體運動一
* [[拋體運動一]] 只做最簡單的計算,【不】包含空氣阻力。
* 這個程式很短,即便是沒有經驗的人也可以很快學起來,現在就從這個範例開始吧!
<<<
[[拋體運動一]]【沒有】包含空氣阻力,寫好之後可以試試看:
# 我們都知道【45&deg; 拋射距離最遠】這件事,確認一下程式的模擬結果是不是真的 45&deg; 拋射最遠。
# 丟鉛球的時候,鉛球出手的地方應該在頭部附近,絕對不是從地板開始,這樣的話還是 45&deg; 拋射最遠嗎?用你的程式丟看看結果如何?
<<<
!!! 拋體運動二
* [[拋體運動二]] 包含一次方(線性)空氣阻力 \(\vec f_D = -b\vec v\) 。
* 這個程式也不長,也不困難,照著做做看吧!
<<<
[[拋體運動二]] 包含一次方(線性)空氣阻力,理工科學生幾乎都知道,因為它夠簡單,上課時可以講解。
# 程式裡頭其實有兩顆球,一顆有計算空氣阻力,另一顆沒有,目的是要顯示空氣阻力的效果。__最後落地時有看到兩顆球嗎?__
# 把球的半徑、質量、初速率等套用''實際生活中常見的''球體,例如棒球、籃球、乒乓球等,''看看線性空氣阻力的效果明顯還是不明顯?''
** 程式裡的速率要使用【公尺 / 秒】為單位,對這個單位沒感覺的話,可以用【公里 / 小時】思考,除以 3.6 就是。例如:時速 36 公里就是 10 公尺 / 秒。
# 這個''線性阻力用在實際生活常見的物體''(如棒球、籃球、乒乓球等)的時候,''是否切合實際?''
<<<
!!! 拋體運動三
* [[拋體運動三]] 包含二次方的空氣阻力 \(\vec f_D = -{1 \over 2}\rho v^2 C_{D}A\ \hat v\) 。
* 這個程式比前兩個稍長,如果暫時對於程式碼的理解感到困難,先直接把計算空氣阻力的部分拿來用(其它部分還是應該練習自己寫出來)。
<<<
[[拋體運動三]] 包含二次方空氣阻力,不少理工科學生知道這件事,但很少人會算,因為【用手算做不出來】。這個範例可以很容易把二次方空氣阻力算進去。
# 把球的半徑、質量、初速率等套用''實際生活中常見的''球體,例如棒球、籃球、乒乓球等,''看看二次方空氣阻力的效果明顯還是不明顯?''
** 程式裡的速率要使用【公尺 / 秒】為單位,對這個單位沒感覺的話,可以用【公里 / 小時】思考,除以 3.6 就是。例如:時速 36 公里就是 10 公尺 / 秒。
# 【45&deg; 拋射距離最遠】這件事,在此時是否還是正確的?
# 丟鉛球的話呢?拋射最遠的角度是多少?(鉛球出手的地方應該在頭部附近,絕對不是從地板開始)
# ''這個阻力是否較符合實際生活中常見的現象?''
<<<
!!! 學習成效
從拋體運動 [[一|拋體運動一]]、[[二|拋體運動二]]、[[三|拋體運動三]] 這幾個範例中我們學習到
# 如何用 Python 做簡單的拋體模擬
** 是否永遠都是 45&deg; 拋射最遠呢?
# 如何把''空氣阻力加入計算中''
** 知道空氣阻力有兩種計算方式,其中二次方阻力的結果較符合實際生活常見的事物。
<<<
過去的人們在學習時永遠都是【忽略空氣阻力】,理由並非空氣阻力永遠可以忽略,腳踏車騎快一點就能感覺到空氣阻力的效果。其實忽略空氣阻力的真正理由是【用手算做不出來】!

以前人們只能用手算,而用手算只能算最簡單的情況,無法處理更複雜的狀況(如包含空氣阻力),所以不得不忽略空氣阻力。

現代人有電腦,而且是夠快的電腦,''我們應該練習做點更為貼近實際的計算'',而不是跟以前的人一樣只做最簡化的計算(不然我們憑什麼說現在比過去進步?)
> 我們過去經常將【最簡化】的情況稱為【理想狀況】,但是【理想】這個詞真的合適嗎?也許【幻想】比較恰當,因為__不切實際事情的我們通常稱之為幻想__,不是嗎?
<<<
<<foldHeadings>>
@@font-size:1.5em;''模擬範例 -- 拋體運動一:最簡單的情況,沒有空氣阻力''@@
----
這個例子裡我們要模擬【拋體】運動。所謂的運動,用很硬的話來說,就是物體位置會隨著時間變化,而模擬,就是要把這個變化計算出來。

我們從最簡單的計算開始,也就是使用下列公式 \begin{aligned}\vec r_\text{next} &= \vec r_\text{now} + \vec v_\text{now} dt \\ \vec v_\text{next} &= \vec v_\text{now} + \vec a_\text{now} dt\end{aligned} 從現在的位置 \(\vec r_\text{now}\)、現在的速度 \(\vec v_\text{now}\),以及現在的加速度 \(\vec a_\text{now}\) 來計算下一個時間點(時間經過 \(dt\) 之後)的位置 \(\vec r_\text{next}\) 與速度 \(\vec v_\text{next}\) 。

>@@如果你是第一次寫程式,也不用擔心,這個程式很簡單的。''最下方的模擬程式也可以試著跑跑看''。@@
@@color:red;font-size:1.5em;底下的標題列按一下可以顯示隱藏內容@@
! 設計場景
//{{{
# from vpython import *		# 使用安裝版的話,這是必要的第一行(得自己寫),開啟 3D 繪圖外掛
GlowScript 3.2 VPython		# 使用 GlowScript 的話,這行會自動產生(不一定是 3.2 無妨),不要更改這一行!

'''
---------------------------------------------------------------------------------------------------------
設計場景
---------------------------------------------------------------------------------------------------------
'''

# 用一個扁扁的盒子代表地板,長度(x-方向)15 米,寬度(z-方向)10 米,高度(y-方向)0.02 米(2 公分)
# 所有物理量皆採用 MKS 單位
floor = box(length=15, height=0.02, width=10)

# 圓球是我們要拋出的物體,半徑 0.05 米(5 公分),並且留下軌跡(make_trail=True)
ball = sphere(radius=0.05,make_trail=True)
# 將圓球放到地板左側邊
ball.pos.x = -floor.length/2
# 再放到地板表面
ball.pos.y = ball.radius+floor.height/2
# 紀錄這個初始高度
ball.y0 = ball.pos.y
//}}}
!!!! 新手提示
<<<
* Python 語言裡面,井字號 {{{#}}} 後面的{{{都是說明文字}}},是給人讀的,不是程式指令。
* __電腦程式指令的執行結果,會在那一行程式碼過後就「不見」了,除非給它取個名字__。這點任何程式語言都一樣,並非只有 Python 如此。
** 上面 {{{box(...)}}} 就是 ~VPython 產生盒子的指令,
*** 寫成 {{{floor = box(...)}}} 的意思就是把 {{{box()}}} 這個指令的結果取名為 {{{floor}}}。
** 同理 {{{sphere(...)}}} 是產生圓球的指令,我們把這顆球取名為 {{{ball}}}。
* __名字可以隨意取,自己好記就行__,但要記住一個名字只能代表一個東西。
** 如果不確定是否需要,先不取名字也無妨,後面要用到的時候再回頭取個名字就好。
* ~VPython 的指令說明網頁:[[box|http://vpython.org/contents/docs/box.html]], [[sphere|http://vpython.org/contents/docs/sphere.html]], [[基本 3D 幾何體|http://vpython.org/contents/docs/primitives.html]]。
<<<
! 初始條件
//{{{
'''
---------------------------------------------------------------------------------------------------------
初始條件
---------------------------------------------------------------------------------------------------------
'''

ball.m = 0.1                 		# 球的質量為 0.1 公斤(100 公克)

ball.v0 = 10              			# 球的初速率為 10 m/s(相當於 36 km/hr)
ball.theta = 40*pi/180       		# 拋射角為 40 度,換算成弧度

ball.v = vector(                    # 計算初速度向量,把它取名為 ball.v
	ball.v0*cos(ball.theta),
	ball.v0*sin(ball.theta),
	0
)

ball.a = vector(0,-9.8,0)           # 球的加速度,忽略空氣阻力的話,受力只有重力,所以加速度只有重力加速度
//}}}
!!!! 新手提示
<<<
* 在 Python 語言裡
** 星號 {{{*}}} 就是乘號,斜線 {{{/}}} 就是除號。(可能所有程式語言都是吧?)
** {{{pi}}} 就是圓周率 \(\pi\) ,{{{sin(x)}}} 及 {{{cos(x)}}} 就是正弦與餘弦函數,裡面的 {{{x}}} 要用弧度結果才會正確。
** {{{vector(x,y,z)}}} 指令可以產生一個向量。
** 名字可以「附屬」在已經存在的名字下,如上面的 {{{ball.m}}},就是把 {{{m}}} 附屬在 {{{ball}}} 之下,可以把它理解成「{{{ball}}} 的 {{{m}}}」。
*** 要這樣寫的話前面的名字必須已經存在才行,以上面 {{{ball.m}}} 這個例子來看,就是 {{{ball}}} 必須已經存在才行。
* 上面程式碼我們把初速率 {{{10 m/s}}} 跟拋射角 {{{40 度}}} 都取了名字,這樣做後續要改變初速度的時候會比較方便。在
<<<
! 細步計算
拋體運動的受力,在不考慮空氣阻力的情況下,只有重力,也就是 \[\vec a = \vec g,\] 最簡單的做法就是依據下列物理公式 \begin{aligned}\vec r_{n+1} &= \vec r_n + \vec v_n dt \\ v_{n+1} &= \vec v_n + \vec g dt\end{aligned} 來做計算。
//{{{
'''
---------------------------------------------------------------------------------------------------------
細步計算
---------------------------------------------------------------------------------------------------------
'''

dt = 0.001							# 每次計算的時間間隔,千分之一 秒是模擬物理狀況時常見的設定
									#	時間間隔越小,計算越準確,但計算時間就越長
while ball.pos.y >= ball.y0: 		# 迴圈開始,當球的高度(y 座標)還大於初始高度(表示球還在空中),執行下列計算

    ball.pos = ball.pos+ball.v*dt	# 用現在的速度計算下一個時間點的位置,並更新球的位置
    ball.v = ball.v+ball.a*dt		# 用現在的加速度計算下一個時間點的速度

	rate(1000)						# 每秒最多幾次計算與畫面更新(慢一點可以看清楚些,快一點也無妨)
//}}}
<<foldHeadings>>
!!!! 線上模擬說明
<<<
底下的模擬程式可以試著玩玩看!
# 按【Start】按鈕開始模擬。過程中隨時可以按【Reset】按鈕重新模擬。
# 勾選 Auto CPF 可以加快模擬過程。勾選/取消 \(g\) 可以開啟/關閉重力場模擬。
# 試著改變球的半徑、質量,(然後按 Reset 重新模擬),觀察看看有什麼不一樣?
<<<
!!!! 線上模擬
* @@color:red;取消勾選右方 Auto CPF 可以慢很多,看清楚運動的每一步!@@
** 勾選起來可以快很多!
|拋體運動模擬|c
|width:40%;<<tw3DCommonPanel "Flow Control">>|width:40%;<<tw3DCommonPanel "Label Statistics">>|
|<<tw3DCommonPanel "Simulation Time Control">>|<<tw3DCommonPanel "Air Drag">>|
|<<tw3DCommonPanel "Center of Mass Control">>|<<tw3DCommonPanel "Trail Control">>|
|<<tw3DCommonPanel "Initial Speed">> / <<tw3DCommonPanel "Initial Theta">>|<<tiddler "Projectile Panel##Object Properties">>|
|<<tw3DCommonPanel "Plot0 Control">> / <<tw3DCommonPanel "DAQ Control">> / <<tw3DCommonPanel "Gravity Control">><br><<twDataPlot id:dataPlot0>>|<<tw3DCommonPanel "Plot1 Control">> / <<tw3DCommonPanel "Message">><br><<twDataPlot id:dataPlot1>>|
|>|<<tw3DScene [[拋體運動一 JS Codes]]>>|
>@@font-sze:0.8em;此線上模擬版本係由瀏覽器內建的 Javascript 寫成,以 [[THREE.js|https://threejs.org/]] 做 3D 繪圖,有興趣學習者可以自行上網以【Javascript 學習】、【Javascript 初學】、【Javascript 3D 繪圖】等關鍵字搜尋相關教學文章或影片。
/***
這個例子裡我們要模擬拋體運動,而運動現象的模擬就是要計算物體位置隨時間的變化。我們從最簡單的計算開始,也就是使用下列公式 \begin{aligned}\vec r_{n+1} &= \vec r_n + \vec v_n dt \\ \vec v_{n+1} &= \vec v_n + \vec a_n dt\end{aligned} 從現在的位置 \(\vec r_n\)、現在的速度 \(\vec v_n\),以及現在的加速度 \(\vec a_n\) 來計算下一個時間點(時間經過 \(dt\) 之後)的位置 \(\vec r_{n+1}\) 與速度 \(\vec v_{n+1}\) 。

如果你是第一次寫程式,下面的範例可能讓你覺得有點長,不過請放心,很簡單的。''最下方的畫面可以試著跑跑看''。
!! 模擬範例-拋體運動-最簡單的情況:沒有空氣阻力

!!! 設計場景
***/
//{{{
let range = 2;
scene.setRange(range);						// 設定場景的範圍為正負 2 公尺
cartesian.setUnitLength(range/2.5);
//cartesian.position.set(-range*0.7,0,0);		// 將座標軸移到畫面左邊某處(可隨意)
//cartesian.showAxisLabels(true);				// 座標軸
//scene.camera.position.x = scene.scaleScalar(0.5);// 攝影機往右一些
scene.camera.position.z *= 1.5;				// 攝影機拉遠一些
scene.camera.position.y = scene.scaleScalar(0.1);	// 攝影機抬高一些

let floor = box({							// 產生一個扁方塊當做地板
	pos:cartesian.position,					// 放在坐標軸的位置(原點)
	width:range*3,							// 寬度(x-方向)為場景範圍的 3 倍
	height:0.01,							// 高度(y-方向)為 0.01 公尺(1 公分)
	depth:range*3,							// 深度(z-方向)為場景範圍的 3 倍
	opacity:0.3								// 【不】透明度為 0.3(頗透明)
});
floor.position.x+=range;					// 把地板的位置沿 x-軸移動一個場景的範圍

let ball = sphere({							// 產生一個球
	pos:cartesian.position,					// 放在坐標軸的位置(原點)
	radius:0.1,								// 半徑為 0.1 公尺(10 公分)
	color:0xffffff,							// 顏色為白色
	make_trail:true,						// 留下運動軌跡
	interval:10								// 每隔 10 個位置點留下 1 個軌跡點
});

txtInitialSpeed.value = 5;					// Initial speed (m/s)
chkGravity.checked = true;					// 開啟模擬重力場
chkAirDrag.checked = false;					// 關閉空氣阻力計算
chkAirDrag.disabled = true;					// 禁止重新開啟空氣阻力計算
//}}}
/***
!!! 初始條件
***/
//{{{
scene.init = () => {
	let v0 = +txtInitialSpeed.value;			// 取得初始速率的輸入值
	let theta0 = +txtTheta0.value/180*Math.PI;	// 取得初始 theta 角輸入值,換算成弧度

	ball.clearTrail();							// 清除球的運動軌跡
	ball.mass = +txtMass.value;;				// 設定球的質量
	ball.setRadius(+txtRadius.value);			// 設定球的半徑
	ball.position.copy(cartesian.position);		// 重新將球放回座標軸的位置(原點)
	ball.position.y += ball.getRadius();		// 提高球的位置讓底部(而不是球心)接觸地面
	ball.velocity = vector(						// 設定球的初速度
		v0*Math.cos(theta0),					// 初速度的 x-分量(依球座標定義計算)
		v0*Math.sin(theta0),					// y-分量(依球座標定義計算)
		0										// z-分量(依球座標定義計算)
	);
	ball.acceleration = vector();				// 設定球的初始加速度為 0
}
//}}}
/***
!!! 細步計算
拋體運動的受力,在不考慮空氣阻力的情況下,只有重力,也就是 \[\vec a = \vec g,\] 最簡單的做法就是依據下列物理公式 \begin{aligned}\vec r_{n+1} &= \vec r_n + \vec v_n dt \\ v_{n+1} &= \vec v_n + \vec g dt\end{aligned} 來做計算。
***/
//{{{
let g = vector(0,-9.8,0),
	tmp = vector(0,0,0);				// 做為計算過程中間值使用的向量
scene.update = (t_cur,dt) => {
	if (ball.position.y < ball.getRadius())
		// 如果球已經落地,不必再算
		// If the ball is already laded, no more calculations 
		return false;
	// 球還沒落地

	// 計算下一個位置
	// Calculate the next position
	ball.position.add(ball.velocity.clone().multiplyScalar(dt));

	// 計算下一個速度
	// Calculate the next velocity
	ball.velocity.add(tmp.copy(g).multiplyScalar(dt));
}
//}}}
@@font-size:1.5em;''模擬範例 -- 拋體運動三:二次方空氣阻力 -- 簡單版''@@
----
在前面兩個範例中,[[拋體運動一]] 我們忽略空氣阻力,[[拋體運動二]] 我們採用計算一次方的阻力 \[\vec f_{D} = -b\vec v, \quad b = 6\pi\eta r.\] ''這個範例裡我們將要計算二次方的空氣阻力,這也是在現實生活中較實際的阻力。''

在二次方流體阻力 \[\boxed{\vec f_{D} = -{1 \over 2}\rho v^2 C_{D}A\ \hat v}\] 的計算中,\(\rho\) 是流體密度,\(v\) 是物體相對於流體的速率,\(A\) 是物體的截面積,\(C_{D}\) 稱為 [[阻力係數|https://zh.wikipedia.org/wiki/%E9%98%BB%E5%8A%9B%E4%BF%82%E6%95%B8]] 或是 [[Drag coefficient|https://en.wikipedia.org/wiki/Drag_coefficient]],是一個表示物體在流體中受到阻礙程度有多大的無單位物理量,和流體的密度以及物體的速率有關,而 \(\hat v\) 則是速度 \(\vec v\) 的單位向量。其中 \(\rho\)、\(A\)、以及 \(v\) 都是很容易可以獲得的,只有 \(C_{D}\) 較為不易。因此''要計算二次方阻力,關鍵就是要會算這個阻力係數 \(C_{D}\) 。''

 [>img(35%,)[https://upload.wikimedia.org/wikipedia/commons/thumb/e/e7/Drag_coefficient_on_a_sphere_vs._Reynolds_number_-_main_trends.svg/655px-Drag_coefficient_on_a_sphere_vs._Reynolds_number_-_main_trends.svg.png]] [[右圖|https://en.wikipedia.org/wiki/Drag_coefficient#/media/File:Drag_coefficient_on_a_sphere_vs._Reynolds_number_-_main_trends.svg]]是從 維基百科 [[Drag coefficient|https://en.wikipedia.org/wiki/Drag_coefficient]] 網頁所獲得,球形物體在流體中的拖曳係數,可以看出拖曳係數隨著雷諾數的變化頗大(關於雷諾數的說明可參考維基百科 [[雷諾數|https://zh.wikipedia.org/wiki/%E9%9B%B7%E8%AF%BA%E6%95%B0]] 或者 [[Reynolds number|https://en.wikipedia.org/wiki/Reynolds_number]]),一般而言雷諾數越小,拖曳係數越大。
> @@注意拖曳係數在右圖中間區域(雷諾數大約介於 \(10^3 \sim 10^5\) 之間)大致上不變(接近 0.5),因此''很多情況可以簡化地使用 \(C_{D} = 0.5\) 來做計算。''@@
>這個程式目前只計算''光滑''球體,並未計算粗糙球體的空氣阻力。
@@color:red;font-size:1.5em;底下的標題列按一下可以顯示隱藏內容@@
! 設計場景
<<tiddler "拋體運動二##設計場景">>
!!!! 新手提示
<<<
這部份和[[拋體運動二]]的【設計場景】是一模一樣的,請參考裡面的新手提示。
<<<
! 初始條件
<<tiddler "拋體運動二##初始條件">>
!!!! 新手提示
<<<
這裡和[[拋體運動二]]的【初始條件】部份也幾乎一模一樣,請參考裡面的新手提示。
<<<
! 細步計算
''在這個簡單版的計算裡,我們直接使用 \(C_D = 0.5\) 來做計算。''
!!!! 計算每一步
//{{{
'''
---------------------------------------------------------------------------------------------------------
細步計算
---------------------------------------------------------------------------------------------------------
'''
dt = 0.001    			# 每一步計算的時間間隔
while True:				# 使用 while 迴圈來做【不斷重複】的計算

	rate(1000)			# 每秒最多幾次計算與畫面更新(慢一點可以看清楚些,快一點也無妨)

	if ball_0.pos.y < ball_0.y0 and ball_1.pos.y < ball_1.y0:
		# 如果兩顆球都已經落地,不必再算,跳出 while 迴圈
		break

	# 至少有一顆球還沒落地,分別判斷是否已經落地

	if ball_0.pos.y >= ball_0.y0:
		# 如果第一顆球(虛球)還沒落地,計算它的下一個位置
		ball_0.pos += ball_0.velocity*dt
		# 計算下一個速度
		ball_0.velocity += ball_0.acceleration*dt

	if ball_1.pos.y >= ball_1.y0:
		# 如果第二顆球(實球)還沒落地,計算它的下一個位置
		ball_1.pos += ball_1.velocity*dt
		# 計算現在的加速度(包含二次方空氣阻力)
		ball_1.acceleration = g + (-0.5*rho_air*ball_1.velocity.mag2*0.5*pi*ball_1.radius**2*ball_1.velocity.norm())/ball_1.mass
		# 計算下一個速度
		ball_1.velocity += ball_1.acceleration*dt
//}}}
!!!! 新手提示
<<<
這裡和[[拋體運動二]]的【細步計算】部份也幾乎一模一樣,只有空氣阻力的計算不同,請參考裡面的新手提示。
<<<
<<foldHeadings>>
!!!! 線上模擬說明
<<<
底下的模擬程式可以玩玩看!
# 勾選 \(\vec f_\text{air}\) (N) 來開啟空氣阻力的計算(\(\vec f_{D} = -{1 \over 2}\rho v^2 C_{D}A\ \hat v\)),然後按【Start】按鈕開始模擬。
# 過程中隨時可以按【Reset】按鈕重新模擬。
# 勾選 Auto CPF 可以加快模擬過程。勾選/取消 \(g\) 可以開啟/關閉重力場模擬。
# 試著改變球的半徑、質量,(然後按 Reset 重新模擬),觀察看看有什麼不一樣?(可能有,可能沒有喔!)
<<<
!!!! 線上模擬
* @@color:red;取消勾選右方 Auto CPF 可以慢很多,看清楚運動的每一步!@@
** 勾選起來可以快很多!
|拋體運動模擬|c
|width:45%;<<tw3DCommonPanel "FlowControl">>|width:45%;<<tw3DCommonPanel "Label Statistics">>|
|<<tw3DCommonPanel "Simulation Time Control">>|<<tw3DCommonPanel "Air Drag">>|
|<<tw3DCommonPanel "Center of Mass Control">>|<<tw3DCommonPanel "Trail Control">>|
|<<tw3DCommonPanel "Initial Speed">> / <<tw3DCommonPanel "Initial Theta">>|<<tiddler "Projectile Panel##Object Properties">>|
|<<tw3DCommonPanel "Plot0 Control">> / <<tw3DCommonPanel "DAQ Control">> / <<tw3DCommonPanel "Gravity Control">><br><<twDataPlot id:dataPlot0>>|<<tw3DCommonPanel "Plot1 Control">> / <<tw3DCommonPanel "Message">><br><<twDataPlot id:dataPlot1>>|
|>|<<tw3DScene [[拋體運動三 JS Codes]]>>|
>@@font-sze:0.8em;此線上模擬版本係由瀏覽器內建的 Javascript 寫成,以 [[THREE.js|https://threejs.org/]] 做 3D 繪圖,有興趣學習者可以自行上網以【Javascript 學習】、【Javascript 初學】、【Javascript 3D 繪圖】等關鍵字搜尋相關教學文章或影片。
//{{{
let range = 2;
scene.setRange(range);						// 設定場景的範圍為正負 2 公尺
cartesian.setUnitLength(range/2.5);
//cartesian.position.set(-range*0.7,0,0);	// 將坐標軸移到畫面左邊某處(可隨意)
//cartesian.showAxisLabels(false);			// 隱藏坐標軸的標示
scene.camera.position.z *= 1.5;				// 攝影機拉遠一些
scene.camera.position.y = scene.scaleScalar(0.1);	// 攝影機抬高一些

let floor = box({							// 產生一個扁方塊當做地板
	pos:cartesian.position,					// 放在坐標軸的位置(原點)
	width:range*3,							// 寬度(x-方向)為場景範圍的 3 倍
	height:0.01,							// 高度(y-方向)為 0.01 公尺(1 公分)
	depth:range*3,							// 深度(z-方向)為場景範圍的 3 倍
	opacity:0.3								// 【不】透明度為 0.3(頗透明)
});
floor.position.x+=range;					// 把地板的位置沿 x-軸移動一個場景的範圍

let ball = [									// 產生兩顆球,做比較用
	sphere({								// 第一顆球是虛的,透明,不受阻力
		pos:cartesian.position,				// 放在座標軸的位置(原點)
		radius:0.1,							// 半徑為 0.1 公尺(10 公分)
		color:0xffffff,						// 顏色為白色
		make_trail:true,					// 留下運動軌跡
		interval:10,						// 每隔 10 個位置點留下 1 個軌跡點
		opacity:0.3							// 【不】透明度 0.3
	}),
	sphere({								// 第二顆球是實的,不透明,受阻力
		pos:cartesian.position,				// 放在座標軸的位置(原點)
		radius:0.1,							// 半徑為 0.1 公尺(10 公分)
		color:0xffff00,						// 顏色為黃色
		make_trail:true,					// 留下運動軌跡
		interval:10							// 每隔 10 個位置點留下 1 個軌跡點
	})
];

txtInitialSpeed.value = 5;					// Initial speed (m/s)
chkGravity.checked = true;					// 開啟模擬重力場
//}}}
/***
!!! 初始條件
***/
//{{{
scene.init = () => {
	let v0 = +txtInitialSpeed.value;				// 取得初始速率的輸入值
	let theta0 = +txtTheta0.value/180*Math.PI;		// 取得初始 theta 角輸入值,換算成弧度
	let m = +txtMass.value;							// 取得球質量(kg)的輸入值
	let R = +txtRadius.value;						// 取得球半徑(m)的輸入值

	for(let n=0,N=ball.length; n<N; n++){
		ball[n].clearTrail();						// 清除球的運動軌跡
		ball[n].mass = m;							// 設定球的質量
		ball[n].setRadius(R);						// 設定球的半徑
		ball[n].position.copy(cartesian.position);	// 重新將球放回座標軸的位置(原點)
		ball[n].position.y += ball[n].getRadius();	// 提高球的位置讓底部(而不是球心)接觸地面
		ball[n].velocity = vector(					// 設定球的初速度
			v0*Math.cos(theta0),					// 初速度的 x-分量(依球座標定義計算)
			v0*Math.sin(theta0),					// y-分量(依球座標定義計算)
			0										// z-分量(依球座標定義計算)
		);
		ball[n].acceleration = vector();			// 設定球的初始加速度為 0
		ball[n].landed = false;
	}
}
//}}}
/***
!!! 細步計算
***/
//{{{
let g = vector(0,-9.8,0),
	tmp = vector(0,0,0);				// 做為計算過程中間值使用的向量
scene.update = (t_cur,dt) => {
	if (ball[0].landed && ball[1].landed)
		// 如果球已經落地,不必再算
		// If the ball is already laded, no more calculations 
		return false;

	// 球還沒落地

	for(let n=0,N=ball.length; n<N; n++){
		// 如果球已經落地,跳過
		// 如果球已經落地,跳過
		if(ball[n].position.y < ball[n].getRadius()){
			ball[n].landed = true;
			continue;
		}

		// 計算下一個位置
		// Calculate the next position
		ball[n].position.add(tmp.copy(ball[n].velocity).multiplyScalar(dt));

		ball[n].acceleration.copy(g);
		if(n===1 && chkAirDrag.checked)
			ball[n].acceleration.add(
				$tw.physics.airDragSphere(ball[n].velocity,ball[n].getRadius(),tmp)
					.multiplyScalar(1/ball[n].mass)
			);
		// 計算下一個速度
		// Calculate the next velocity
		ball[n].velocity.add(tmp.copy(ball[n].acceleration).multiplyScalar(dt));
	}
}
//}}}
@@font-size:1.5em;''模擬範例 -- 拋體運動二:一次方(線性)空氣阻力''@@
----
在[[拋體運動一]]這個範例中,我們計算最簡單的情況:忽略空氣阻力。大家可能很習慣忽略空氣阻力,但我們一定都知道''空氣阻力【不】應該忽略'',一直以來我們都習慣忽略它,其實真正的理由並不是它可以忽略,而是【用手算做不出來】。這個範例我們會看到,用電腦算的話,很容易可以把空氣阻力算進來。

一般討論空氣阻力時,例如大學普通物理,或者維基百科 [[阻力|https://zh.wikipedia.org/wiki/%E9%98%BB%E5%8A%9B]] 或 [[Drag|https://en.wikipedia.org/wiki/Drag_(physics)]] 等,都會把它分兩種情況討論,一種是速率一次方的,\[\boxed{\vec f_{D} = -b\vec v, \quad b = 6\pi\eta r,}\tag{1}\] 其中 \(\eta\) 是空氣的黏滯係數(參考維基百科 [[黏度|https://zh.wikipedia.org/wiki/%E9%BB%8F%E5%BA%A6]] 或 [[Viscosity|https://en.wikipedia.org/wiki/Viscosity]]),\(r\) 是運動物體的【半徑】。另一種情況是速率二次方的, \[\vec f_{D} = -{1 \over 2}\rho v^2 C_{D}A\ \hat v,\tag{2}\] 其中 \(\rho\) 是流體密度,\(v\) 是物體相對於流體的速率,\(A\) 是物體的截面積,\(C_{D}\) 稱為 [[阻力係數|https://zh.wikipedia.org/wiki/%E9%98%BB%E5%8A%9B%E4%BF%82%E6%95%B8]] 或是 [[Drag coefficient|https://en.wikipedia.org/wiki/Drag_coefficient]],是一個表示物體在流體中受到阻礙程度有多大的無單位物理量,和流體的密度以及物體的速率有關,而 \(\hat v\) 則是速度 \(\vec v\) 的單位向量。

一般而言,如果是像細菌塵埃這種小型物體在空氣中,可以用 (1) 式來計算其空氣阻力,而''日常生活中看得見摸得到的物體,通常得使用 (2) 式來計算空氣阻力''。覺得二次方不好算嗎?放心,反正是電腦算,幾次方都一樣的啦!

這個範例中我們先採用【看】起來較簡單的 (1) 式,[[拋體運動三]] 裡面我們再來採用 (2) 式。要使用 (1) 式,我們需要知道空氣的黏滯係數 \(\eta\),根據維基百科 [[黏度|https://zh.wikipedia.org/wiki/%E9%BB%8F%E5%BA%A6]] 或 [[Viscosity|https://en.wikipedia.org/wiki/Viscosity]] 網頁,這個值隨著溫度而變, \[\eta^{[1]} = \lambda{T^{3/2} \over (T+C)},\] 其中 \(\lambda=1.512041288 \times 10^{-6} \text{ kg/(m}\cdot\text{s)}\),\(C = 120\) K,\(T\) 為絕對溫度。''在 15&deg; C 時候 \(\eta = 1.81 \times 10^{-5} \text{ kg/(m}\cdot\text{s)}\) 。''
----
# [[黏度|https://zh.wikipedia.org/wiki/%E9%BB%8F%E5%BA%A6]] 或 [[Viscosity|https://en.wikipedia.org/wiki/Viscosity]] 網頁裡面使用的符號為 \(\mu\),不過根據內容的說明,其意義和 [[阻力|https://zh.wikipedia.org/wiki/%E9%98%BB%E5%8A%9B]] 或 [[Drag|https://en.wikipedia.org/wiki/Drag_(physics)]] 裡面的 \(\eta\) 是相同的。這裡我們選擇使用在講義裡較先出現的 \(\eta\) 這個符號。
@@color:red;font-size:1.5em;底下的標題列按一下可以顯示隱藏內容@@
! 設計場景
//{{{
# from vpython import *		# 使用安裝版的話,這是必要的第一行(得自己寫),開啟 3D 繪圖外掛
GlowScript 3.2 VPython		# 使用 GlowScript 的話,這行會自動產生(不一定是 3.2 無妨),不要更改這一行!

'''
---------------------------------------------------------------------------------------------------------
設計場景
---------------------------------------------------------------------------------------------------------
'''

scene.range = 2             		# 設定場景的範圍為正負 2 公尺
floor = box(						# 產生一個扁方塊當做地板
	pos=vector(0,0,0),				# 放在坐標軸的位置(原點)
	height=0.01,					# 高度(y-方向)為 0.01 公尺(1 公分)
	length=scene.range*3,			# 長度(x-方向)為場景範圍的 3 倍
	width=scene.range*2,			# 寬度(z-方向)為場景範圍的 2 倍
	opacity=0.3                     # 【不】透明度為 0.3(頗透明)
)
floor.top = floor.pos.y+floor.height/2          # 計算地板表面的高度

# 產生兩顆球,做比較用
ball_0 = sphere(                # 第一顆球是虛的,透明,不受阻力
	pos=vector(0,0,0),			# 放在坐標軸的位置(原點)
	radius=0.1,                 # 半徑為 0.1 公尺(10 公分)
	color=vector(1,1,1),		# 顏色為白色
	make_trail=True,			# 留下運動軌跡
	interval=10,				# 每隔 10 個位置點留下 1 個軌跡點
	opacity=0.3                 # 【不】透明度 0.3
)
ball_1 = sphere(                # 第二顆球是實的,不透明,受阻力
	pos=vector(0,0,0),			# 放在坐標軸的位置(原點)
	radius=0.1,                 # 半徑為 0.1 公尺(10 公分)
	color=vector(1,1,0),		# 顏色為黃色
	make_trail=True,			# 留下運動軌跡
	interval=10                 # 每隔 10 個位置點留下 1 個軌跡點
)
//}}}
!!!! 新手提示
<<<
* 在 ~VPython 裡一開始就會有一個【空空如也】的場景,名字叫做 {{{scene}}},這個字的意思就是 場景。
** 場景的名字叫做【場景】,好像我的名字叫做【我】一樣!
*** 上面例子裡我們也是把地板的名字叫做【floor(地板)】,球的名字叫做【ball(球)】。
*** 莎莎醬的名字叫做【莎莎】,而莎莎很明顯是西班牙文 salsa 的音譯,這個字的意思其實就是【醬】,所以這個醬的名字叫做【醬】。
*** 成龍電影玩過稍微不一樣的梗:我叫做 Yu,他叫做 Mi,所以 I am you and he is me,聽得 Carter 一頭霧水。
*** 見怪不怪!
* 場景的大小可以經由 {{{scene.range}}} 來指定,例如上面的 {{{scene.range = 2}}}。
* [[VPython 3D Objects|http://www.vpython.org/contents/docs_vp5/visual/primitives.html]] 裡面有內建的 3D 物體及使用說明,都是一個指令就可以產生的東西。''沒事可以多去逛逛''。
* 所有物體(應該吧?)都可以指定【不】透明度,用 {{{opacity = 0 到 1 之間的數字}}} 來表示,{{{1}}} 表示完全【不】透明,{{{0}}} 表示完全透明。
* 每一個物體都有紀錄位置,用 {{{.pos}}} 可以讀出或改變物體的位置。例如:上例中地板的名字叫做 {{{floor}}},它的位置就記錄在 {{{floor.pos}}} 裡。
** 如果要把地板挪到 3 米高的地方(變成二樓),可以寫 {{{floor.pos = vector(0,0,3)}}}。
* 每一個向量都有 {{{x, y, z}}} 三個分量,可以用 {{{.x}}}、{{{.y}}}、{{{.z}}} 個別讀出或改變分量。例如上面的 {{{floor.pos = vector(0,0,3)}}} 也可以寫成 {{{floor.pos.z = 3}}}。
<<<
! 初始條件
//{{{
'''
---------------------------------------------------------------------------------------------------------
初始條件
---------------------------------------------------------------------------------------------------------
'''

g = vector(0,-9.8,0)    	# 重力加速度

v0 = 5						# 設定初始速率 (m/s)
theta0 = 45*pi/180			# 設定初始 theta 角,換算成弧度
m = 0.005					# 設定球質量(kg)
R = 0.01					# 設定球半徑(m)

# 設定【虛球】的初始條件
ball_0.mass = m				# 設定球的質量
ball_0.radius = R			# 設定球的半徑
ball_0.pos.y = ball_0.radius+floor.top    # 球心設在在地板表面 + 球半徑的高度
ball_0.y0 = ball_0.pos.y                  # 紀錄球心的初始高度(後面判斷是否掉落地面用)
ball_0.velocity = vector(	# 設定球的初速度
	v0*cos(theta0),     	# 初速度的 x-分量(依球座標定義計算)
	v0*sin(theta0),     	# y-分量(依球座標定義計算)
	0           			# z-分量(依球座標定義計算)
)
ball_0.acceleration = g		# 設定球的初始加速度為 g

# 設定【實球】的初始條件
ball_1.mass = m				# 設定球的質量
ball_1.radius = R			# 設定球的半徑
ball_1.pos.y = ball_1.radius+floor.top    # 球心設在在地板表面 + 球半徑的高度
ball_1.y0 = ball_1.pos.y                  # 紀錄球心的初始高度(後面判斷是否掉落地面用)
ball_1.velocity = vector(	# 設定球的初速度
	v0*cos(theta0),     	# 初速度的 x-分量(依球座標定義計算)
	v0*sin(theta0),     	# y-分量(依球座標定義計算)
	0           			# z-分量(依球座標定義計算)
)
ball_1.acceleration = g		# 設定球的初始加速度為 g
//}}}
!!!! 新手提示
<<<
這一段應該不太需要說明,如果覺得有疑問,可以回頭再讀一下前面的新手提示,或者再讀一次[[拋體運動一]]喔!
<<<
! 細步計算
//{{{
'''
---------------------------------------------------------------------------------------------------------
細步計算
---------------------------------------------------------------------------------------------------------
'''

eta_air = 1.81e-5   				# at 15C, https://en.wikipedia.org/wiki/Viscosity
b = 6*pi*eta_air*ball_1.radius		# 一次方空氣阻力的係數

dt = 0.001    			# 每一步計算的時間間隔
while True:				# 使用 while 迴圈來做【不斷重複】的計算

	rate(1000)			# 每秒最多幾次計算與畫面更新(慢一點可以看清楚些,快一點也無妨)

	if ball_0.pos.y < ball_0.y0 and ball_1.pos.y < ball_1.y0:
		# 如果兩顆球都已經落地,不必再算,跳出 while 迴圈
		break

	# 至少有一顆球還沒落地,分別判斷是否已經落地

	if ball_0.pos.y >= ball_0.y0:
		# 如果第一顆球(虛球)還沒落地,計算它的下一個位置
		ball_0.pos += ball_0.velocity*dt
		# 計算下一個速度
		ball_0.velocity += ball_0.acceleration*dt

	if ball_1.pos.y >= ball_1.y0:
		# 如果第二顆球(實球)還沒落地,計算它的下一個位置
		ball_1.pos += ball_1.velocity*dt
		# 計算現在的加速度(包含一次方空氣阻力)
		ball_1.acceleration = g + (-b*ball_1.velocity/ball_1.mass)
		# 計算下一個速度
		ball_1.velocity += ball_1.acceleration*dt
//}}}
!!!! 新手提示
<<<
* 細步計算的 {{{while 迴圈}}} 裡面有一個 {{{rate()}}} 指令,這是 ~VPython 用來畫圖以及處理鍵盤滑鼠動作的。
* 沒有這條指令的話,~VPython 不會去畫圖,也不會處理鍵盤與滑鼠的動作,它就完全不理我們,自顧自地計算。
** 因為沒有畫圖也不理會鍵盤滑鼠的動作,電腦是把全部的可用時間都花在做計算。只是我們會看不到任何過程,必須等到最後結束時才看得到最後的結果。
** 3D 繪圖本身很耗時間,所以讓 ~VPython 繪圖其實會拖慢計算的速度,但是''模擬經常是要看到過程,而不是只看最後結果'',因此通常會加上這一條。
* {{{rate()}}} 括弧裡的參數是一個數字,代表【我們希望這個迴圈每秒可以執行幾次】。
** 一般而言數字越大表示計算越快,當然是越好,但是__電腦的速度不見得能達到我們的期望__。達不到的時候,動畫會出現不太順暢的情形,不過不影響計算結果的正確性。
** 一般中階筆電大概設定在 {{{rate(2000)}}} 左右還不至於影響動畫的流暢度,不過每台電腦的速度不同,可以根據自己的電腦調整看看,找出不影響動畫順暢度的最大值。
<<<
<<foldHeadings>>
!!!! 線上模擬說明
<<<
底下的線上模擬可以試著玩看看!
# 勾選 \(\vec f_\text{air}\) (N) 來開啟空氣阻力的計算(\(\vec f_\text{air} = -b\vec v\)),然後按【Start】按鈕開始模擬。
# 過程中隨時可以按【Reset】按鈕重新模擬。
# 勾選 Auto CPF 可以加快模擬過程。勾選/取消 \(g\) 可以開啟/關閉重力場模擬。
# 試著改變球的半徑、質量,(然後按 Reset 重新模擬),觀察看看有什麼不一樣?(可能有,可能沒有喔!)
<<<
!!!! 線上模擬
* @@color:red;取消勾選右方 Auto CPF 可以慢很多,看清楚運動的每一步!@@
** 勾選起來可以快很多!
|拋體運動模擬|c
|width:45%;<<tw3DCommonPanel "Flow Control">>|width:45%;<<tw3DCommonPanel "Label Statistics">>|
|<<tw3DCommonPanel "Simulation Time Control">>|<<tw3DCommonPanel "Air Drag">>|
|<<tw3DCommonPanel "Center of Mass Control">>|<<tw3DCommonPanel "Trail Control">>|
|<<tw3DCommonPanel "Initial Speed">> / <<tw3DCommonPanel "Initial Theta">> / <<tw3DCommonPanel "Initial Phi">>|<<tiddler "Projectile Panel##Object Properties">>|
|<<tw3DCommonPanel "Plot0 Control">> / <<tw3DCommonPanel "DAQ Control">> / <<tw3DCommonPanel "Gravity Control">><br><<twDataPlot id:dataPlot0>>|<<tw3DCommonPanel "Plot1 Control">> / <<tw3DCommonPanel "Message">><br><<twDataPlot id:dataPlot1>>|
|>|<<tw3DScene [[拋體運動二 JS Codes]]>>|
>@@font-sze:0.8em;此線上模擬版本係由瀏覽器內建的 Javascript 寫成,以 [[THREE.js|https://threejs.org/]] 做 3D 繪圖,有興趣學習者可以自行上網以【Javascript 學習】、【Javascript 初學】、【Javascript 3D 繪圖】等關鍵字搜尋相關教學文章或影片。
Type the text for '拋體運動二--阻力大不大'
//{{{
let range = 2;
scene.setRange(range);						// 設定場景的範圍為正負 2 公尺
cartesian.setUnitLength(range/2.5);
//cartesian.position.set(-range*0.7,0,0);	// 將坐標軸移到畫面左邊某處(可隨意)
//cartesian.showAxisLabels(false);			// 隱藏坐標軸的標示
scene.camera.position.z *= 1.5;				// 攝影機拉遠一些
scene.camera.position.y = scene.scaleScalar(0.1);	// 攝影機抬高一些

let floor = box({							// 產生一個扁方塊當做地板
	pos:cartesian.position,					// 放在坐標軸的位置(原點)
	width:range*3,							// 寬度(x-方向)為場景範圍的 3 倍
	height:0.01,							// 高度(y-方向)為 0.01 公尺(1 公分)
	depth:range*3,							// 深度(z-方向)為場景範圍的 3 倍
	opacity:0.3								// 【不】透明度為 0.3(頗透明)
});
floor.position.x+=range;					// 把地板的位置沿 x-軸移動一個場景的範圍

let ball = [									// 產生兩顆球,做比較用
	sphere({								// 第一顆球是虛的,透明,不受阻力
		pos:cartesian.position,				// 放在座標軸的位置(原點)
		radius:0.1,							// 半徑為 0.1 公尺(10 公分)
		color:0xffffff,						// 顏色為白色
		make_trail:true,					// 留下運動軌跡
		interval:10,						// 每隔 10 個位置點留下 1 個軌跡點
		opacity:0.3							// 【不】透明度 0.3
	}),
	sphere({								// 第二顆球是實的,不透明,受阻力
		pos:cartesian.position,				// 放在座標軸的位置(原點)
		radius:0.1,							// 半徑為 0.1 公尺(10 公分)
		color:0xffff00,						// 顏色為黃色
		make_trail:true,					// 留下運動軌跡
		interval:10							// 每隔 10 個位置點留下 1 個軌跡點
	})
];

txtInitialSpeed.value = 5;					// Initial speed (m/s)
chkGravity.checked = true;					// 開啟模擬重力場
//}}}
/***
!!! 初始條件
***/
//{{{
let eta = 1.81e-5,									// 15 度 C 時候的空氣黏滯係數
	b = 0;											// f = -bv 的係數 b = 6 pi eta r
scene.init = () => {
	let v0 = +txtInitialSpeed.value;				// 取得初始速率的輸入值
	let theta0 = +txtTheta0.value/180*Math.PI;		// 取得初始 theta 角輸入值,換算成弧度
	let R = +txtRadius.value;						// 取得球半徑的輸入值

	b = 6*Math.PI*eta*R;							// 計算空氣阻力的係數 b

	for(let n=0,N=ball.length; n<N; n++){
		ball[n].clearTrail();						// 清除球的運動軌跡
		ball[n].mass = +txtMass.value;;				// 設定球的質量
		ball[n].setRadius(R);						// 設定球的半徑
		ball[n].position.copy(cartesian.position);	// 重新將球放回座標軸的位置(原點)
		ball[n].position.y += ball[n].getRadius();	// 提高球的位置讓底部(而不是球心)接觸地面
		ball[n].velocity = vector(					// 設定球的初速度
			v0*Math.cos(theta0),					// 初速度的 x-分量(依球座標定義計算)
			v0*Math.sin(theta0),					// y-分量(依球座標定義計算)
			0										// z-分量(依球座標定義計算)
		);
		ball[n].acceleration = vector();			// 設定球的初始加速度為 0
		ball[n].landed = false;
	}
}
//}}}
/***
!!! 細步計算
***/
//{{{
let g = vector(0,-9.8,0),
	tmp = vector(0,0,0);				// 做為計算過程中間值使用的向量
scene.update = (t_cur,dt) => {
	if (ball[0].landed && ball[1].landed)
		// 如果球已經落地,不必再算
		// If the ball is already laded, no more calculations 
		return false;

	// 球還沒落地

	for(let n=0,N=ball.length; n<N; n++){
		// 如果球已經落地,跳過
		if(ball[n].position.y < ball[n].getRadius()){
			ball[n].landed = true;
			continue;
		}

		// 計算下一個位置
		// Calculate the next position
		ball[n].position.add(ball[n].velocity.clone().multiplyScalar(dt));

		ball[n].acceleration.copy(g);
		if(n===1 && chkAirDrag.checked)
			ball[n].acceleration.add(
				tmp.copy(ball[n].velocity)
					.multiplyScalar(-b/ball[n].mass)
			);
		// 計算下一個速度
		// Calculate the next velocity
		ball[n].velocity.add(tmp.copy(ball[n].acceleration).multiplyScalar(dt));
	}
}
//}}}
@@font-size:1.5em;''模擬範例 -- 拋體運動四:二次方空氣阻力 -- 不簡單版''@@
----
在前面兩個範例中,[[拋體運動一]] 我們忽略空氣阻力,[[拋體運動二]] 我們採用計算一次方的阻力 \[\vec f_{D} = -b\vec v, \quad b = 6\pi\eta r.\] ''這個範例裡我們將要計算二次方的空氣阻力,這也是在現實生活中較實際的阻力。''

在二次方流體阻力 \[\boxed{\vec f_{D} = -{1 \over 2}\rho v^2 C_{D}A\ \hat v}\] 的計算中,\(\rho\) 是流體密度,\(v\) 是物體相對於流體的速率,\(A\) 是物體的截面積,\(C_{D}\) 稱為 [[阻力係數|https://zh.wikipedia.org/wiki/%E9%98%BB%E5%8A%9B%E4%BF%82%E6%95%B8]] 或是 [[Drag coefficient|https://en.wikipedia.org/wiki/Drag_coefficient]],是一個表示物體在流體中受到阻礙程度有多大的無單位物理量,和流體的密度以及物體的速率有關,而 \(\hat v\) 則是速度 \(\vec v\) 的單位向量。其中 \(\rho\)、\(A\)、以及 \(v\) 都是很容易可以獲得的,只有 \(C_{D}\) 較為不易。因此''要計算二次方阻力,關鍵就是要會算這個阻力係數 \(C_{D}\) 。''

 [>img(35%,)[https://upload.wikimedia.org/wikipedia/commons/thumb/e/e7/Drag_coefficient_on_a_sphere_vs._Reynolds_number_-_main_trends.svg/655px-Drag_coefficient_on_a_sphere_vs._Reynolds_number_-_main_trends.svg.png]] [[右圖|https://en.wikipedia.org/wiki/Drag_coefficient#/media/File:Drag_coefficient_on_a_sphere_vs._Reynolds_number_-_main_trends.svg]]是從 維基百科 [[Drag coefficient|https://en.wikipedia.org/wiki/Drag_coefficient]] 網頁所獲得,球形物體在流體中的拖曳係數,可以看出拖曳係數隨著雷諾數的變化頗大(關於雷諾數的說明可參考維基百科 [[雷諾數|https://zh.wikipedia.org/wiki/%E9%9B%B7%E8%AF%BA%E6%95%B0]] 或者 [[Reynolds number|https://en.wikipedia.org/wiki/Reynolds_number]]),一般而言雷諾數越小,拖曳係數越大。
> 注意拖曳係數在右圖中間區域(雷諾數大約介於 \(10^3 \sim 10^5\) 之間)大致上不變(接近 0.5),因此''很多情況可以簡化地使用 \(C_{D} = 0.5\) 來做計算。''
此圖乍看之下變化頗大,但仔細看其實可以分成幾個近似直線的區間:圖中的 2 區間,3 區間,4 區間,4 和 5 之間,5 區間,以及 5 之後的區間。如果我們將這些區間用直線來近似,就可以估算出很大範圍內球體的拖曳係數,[[這裡|Air Drag Sphere Python]] 的程式碼便是一例,其使用方法也很簡單(參考程式碼裡面的說明),只要呼叫 {{{airDragSphere(v,r)}}} 便可獲得半徑為 {{{r}}} 的球體在速度為 {{{v}}} 時候的空氣阻力。
>這個程式目前只計算''光滑''球體,並未計算粗糙球體的空氣阻力。
@@color:red;font-size:1.5em;底下的標題列按一下可以顯示隱藏內容@@
! 設計場景
<<tiddler "拋體運動二##設計場景">>
!!!! 新手提示
<<<
這部份和[[拋體運動二]]的【設計場景】是一模一樣的,請參考裡面的新手提示。
<<<
! 初始條件
<<tiddler "拋體運動二##初始條件">>
!!!! 新手提示
<<<
這裡和[[拋體運動二]]的【初始條件】部份也幾乎一模一樣,請參考裡面的新手提示。
<<<
! 細步計算

!!!! 計算二次方空氣阻力
<<tiddler "Air Drag Sphere Python">>
!!!! 計算每一步
//{{{
'''
---------------------------------------------------------------------------------------------------------
細步計算
---------------------------------------------------------------------------------------------------------
'''
dt = 0.001    			# 每一步計算的時間間隔
while True:				# 使用 while 迴圈來做【不斷重複】的計算

	rate(1000)			# 每秒最多幾次計算與畫面更新(慢一點可以看清楚些,快一點也無妨)

	if ball_0.pos.y < ball_0.y0 and ball_1.pos.y < ball_1.y0:
		# 如果兩顆球都已經落地,不必再算,跳出 while 迴圈
		break

	# 至少有一顆球還沒落地,分別判斷是否已經落地

	if ball_0.pos.y >= ball_0.y0:
		# 如果第一顆球(虛球)還沒落地,計算它的下一個位置
		ball_0.pos += ball_0.velocity*dt
		# 計算下一個速度
		ball_0.velocity += ball_0.acceleration*dt

	if ball_1.pos.y >= ball_1.y0:
		# 如果第二顆球(實球)還沒落地,計算它的下一個位置
		ball_1.pos += ball_1.velocity*dt
		# 計算現在的加速度(包含二次方空氣阻力)
		ball_1.acceleration = g + airDragSphere(ball_1.velocity,ball_1.radius)/ball_1.mass
		# 計算下一個速度
		ball_1.velocity += ball_1.acceleration*dt
//}}}
!!!! 新手提示
<<<
這裡和[[拋體運動二]]的【細步計算】部份也幾乎一模一樣,只有空氣阻力的計算不同,請參考裡面的新手提示。
<<<
<<foldHeadings>>
!!!! 線上模擬說明
<<<
底下的模擬程式可以玩玩看!
# 勾選 \(\vec f_\text{air}\) (N) 來開啟空氣阻力的計算(\(\vec f_{D} = -{1 \over 2}\rho v^2 C_{D}A\ \hat v\)),然後按【Start】按鈕開始模擬。
# 過程中隨時可以按【Reset】按鈕重新模擬。
# 勾選 Auto CPF 可以加快模擬過程。勾選/取消 \(g\) 可以開啟/關閉重力場模擬。
# 試著改變球的半徑、質量,(然後按 Reset 重新模擬),觀察看看有什麼不一樣?(可能有,可能沒有喔!)
<<<
!!!! 線上模擬
* @@color:red;取消勾選右方 Auto CPF 可以慢很多,看清楚運動的每一步!@@
** 勾選起來可以快很多!
|拋體運動模擬|c
|width:45%;<<tw3DCommonPanel "FlowControl">>|width:45%;<<tw3DCommonPanel "Label Statistics">>|
|<<tw3DCommonPanel "Simulation Time Control">>|<<tw3DCommonPanel "Air Drag">>|
|<<tw3DCommonPanel "Center of Mass Control">>|<<tw3DCommonPanel "Trail Control">>|
|<<tw3DCommonPanel "Initial Speed">> / <<tw3DCommonPanel "Initial Theta">>|<<tiddler "Projectile Panel##Object Properties">>|
|<<tw3DCommonPanel "Plot0 Control">> / <<tw3DCommonPanel "DAQ Control">> / <<tw3DCommonPanel "Gravity Control">><br><<twDataPlot id:dataPlot0>>|<<tw3DCommonPanel "Plot1 Control">> / <<tw3DCommonPanel "Message">><br><<twDataPlot id:dataPlot1>>|
|>|<<tw3DScene [[拋體運動三 JS Codes]]>>|
>@@font-sze:0.8em;此線上模擬版本係由瀏覽器內建的 Javascript 寫成,以 [[THREE.js|https://threejs.org/]] 做 3D 繪圖,有興趣學習者可以自行上網以【Javascript 學習】、【Javascript 初學】、【Javascript 3D 繪圖】等關鍵字搜尋相關教學文章或影片。
/***
!!! 設計場景
***/
//{{{
cartesian.show(false);			// 隱藏直角座標系
//scene.camera.position.z *= 3;

let __tmpV = vector(),			// 公用的暫時向量(計算過程中使用)

	ceiling = box({				// 產生一個扁平盒子代表天花板,取名為 ceiling(就是天花板)
		width:2,				// 寬度(x-方向)為 2 公尺
		height:0.02,			// 高度(y-方向)為 0.02 公尺(2 公分)
		depth:2,				// 長度(z-方向)為 2 公尺
		opacity:0.3				// 【不】透明度為 0.3(頗透明)
	}),

	rod = cylinder({			// 產生一個細圓柱代表擺繩,取名為 rod(桿)
		axis:vector(0,-1,0),	// 指向 -y 方向(下方)
		radius:0.01,			// 半徑為 0.01 公尺(1 公分)
		opacity:0.5				// 【不】透明度為 0.5(半透明)
	});
rod.position.copy(ceiling.position).add(
	rod.getAxis(__tmpV).multiplyScalar(0.5)
);

let bob = sphere({				// 產生一個球代表擺錘,取名為 bob(就是擺錘)
	radius:0.15,				// 半徑為 0.15 公尺(15 公分)
	make_trail:true,			// 要留下軌跡
	opacity:0.5					// 【不】透明度為 0.5
});

//spherical = SphericalCoordinateSystem().show(false).showComponents(true);
chkGravity.checked = true;		// 開啟模擬重力場
chkGravity.disabled = true;		// 禁止關閉重力場
chkAirDrag.checked = false;		// 關閉空氣阻力
chkAirDrag.disabled = true;		// 禁止開啟空氣阻力(後面的範例可以開啟)
txtTheta0.value = 30;			// 初始角度
txtdT.value = 0.001;			// 加大時間間隔,讓誤差更快顯現
txtTmax.value = 50;				// 模擬總時間
txtStringLength.value = 1;
//}}}
/***
!!! 初始條件
***/
//{{{
let __axis = vector();
scene.init = () => {
	rod.mass = +txtStringMass.value;			// 擺繩質量
	rod.L0 = +txtStringLength.value;			// 擺繩原長
	rod.theta0 = txtTheta0.value*Math.PI/180.0;	// 初始角度
	bob.setRadius(+txtBobRadius.value);			// 擺錘半徑
	bob.clearTrail();							// 清除軌跡
	getTrailParam(bob);
	bob.position.copy(							// 設定擺錘初始位置
		ceiling.position
	).add(__tmpV.set(							// 根據擺繩的初始角度來計算
		Math.cos(Math.PI*3/2+rod.theta0),
		Math.sin(Math.PI*3/2+rod.theta0),
		0
	).multiplyScalar(rod.L0));
	//spherical.updateStatus(bob.position);
	bob.mass = +txtBobMass.value;				// 擺錘質量
	bob.velocity = vector();					// 初速度
	bob.acceleration = vector();				// 加速度

	rod.setAxis(								// 調整擺繩讓它連結擺錘與天花板
		__axis.copy(bob.position).sub(ceiling.position)
	).position.copy(ceiling.position).add(__axis.multiplyScalar(0.5));
	calcA(										// 計算初始加速度
		bob.position,bob.velocity,bob.acceleration
	);
//}}}
//{{{
	arrFg = arrow({
		axis: __axis,
		color: 0xffffff
	});
//}}}
//{{{
}
//}}}
@@font-size:2em;http://faculty.ndhu.edu.tw/~wcy2/wcy2.SimForFun.html@@
<<<
* [[前言|物理現象的數值模擬]],沒看過的,想看一下的,就點進去看看吧。
* 如果是程式經驗較少的''初學者'',可以先使用 [[GlowScript|http://www.glowscript.org/]],免安裝,上雲端,只需要登入即可使用。登入的話可以使用既有的 Google 帳號,也可以新建一個 ~GlowScript 帳號。
** 本系列教案的程式碼__都有在 ~GlowScript 測試過(測試版本為 ~GlowScript 2.7),都可以執行__。
* 如果對寫程式''有經驗'',想要學習更多的 Python 功能,則可以選擇安裝 ''[[VPython|http://vpython.org/]]'',它是 Python 語言的一個外掛,專門做 3D 電腦繪圖的,功能夠強大,使用起來夠容易,是很實用的工具。<<slider '' '物理現象的數值模擬##Note-1' '...'>>
<<<
!! 數值模擬三部曲
要對物理現象做數值模擬,基本上只要按照下列三個步驟進行即可:
# @@color:red;''設計場景''@@
** 場景要盡可能合乎事實,無法完全合乎事實的部分,也要實際且合理。
# @@color:red;''初始條件''@@
** 正確的初始條件才能得到正確的結果。
# @@color:red;''細步計算''@@
** 將整個範圍的時間(或空間)切割成很小的區間 \(dt\)(或 \(d\vec r\)),從頭開始一個小區間一個小區間地計算。
** 在每個小區監理,
*** 計算交互作用,然後
*** 根據交互作用計算下一個區間的運動狀況(位置、速度、加速度)
通常我們學習物理最先接觸的是力學,因此底下我們就以力學運動來說明這三步驟如何進行。
!! 力學運動
@@color:red;''設計場景''@@
>以力學運動來講,最簡單的情況場景只需要一個物體,就是在運動的那個物體,而且我們討論物理的時候可以不在乎美醜,只要一個像是球體、立方體、圓柱體這種簡單的形狀就可以了。複雜一點的場景就多用幾個簡單幾何體組合起來,也是可行的。
@@color:red;''初始條件''@@
> 最簡單的情況,就從靜止開始,也就是初速度 \(\vec v_0 = (0,0,0)\)。至於要從哪個位置開始,隨自己高興就好,如果沒有特別的想法就從原點開始(\(\vec r_0 = (0,0,0)\))也行。
>> 有些情況不會是從靜止開始,例如抛體運動一定要有初速度,這種情況就得搞清楚初速度怎麼計算才行。
@@color:red;''細步計算''@@
> 物理上討論運動,通常是要討論【從某個地方開始,經過多少時間,到達哪個位置】這樣的事情,用比較專業的術語來講,就是要看「物體位置隨時間的變化」,所以我們在__做力學運動模擬的時候,要做細步處理的是時間__,講明白點,也就是要將時間切割成很小的區間 \(dt\),從初始時間(\(t_0=0\))開始一個時間點一個時間點循序計算(\(t_1 = t_0 + dt, \quad t_2 = t_1 + dt, \quad \cdots, \quad t_{n+1} = t_n + dt, \quad \cdots\))。
>> 一般而言,\(dt = 0.001\) s,也就是千分之一秒的時間間隔,可以應付大部分日常生活可見的力學運動模擬。當然更精準的計算就需要更細微的時間間隔。
>前面提到,在力學的模擬裡面,我們要計算的就是運動物體''位置''隨時間的變化,位置會有變化自然是因為有速度,而速度可能也會有變化,如果有加速度的話。為什麼有加速度?因為有外力,__所以力學模擬的重點就是【力的分析】__。''力的分析做的正確,加速度計算就會正確;加速度正確,速度及位置的變化也就跟著正確了!''

我們來看幾個簡單的例子:
# [[拋體運動]] -- 還在忽略空氣阻力嗎?別再呼嚨自己了!
# [[單擺運動]] -- 看見簡單方法的限制,[[Runge-Kutta|四階 Runge-Kutta 方法]] 好好用!
# [[彈簧運動]] -- 搖啊搖!搖到外婆橋,外婆說我好無聊!
以上幾個簡單的範例足夠讓大家學到數值模擬的基本功夫,下面幾個較為進階,不過概念上也都跟上面那些簡單的一模一樣而已。
# [[行星繞日]] -- 一次打十個?不用問啦!
# [[氣體]] -- 理想?該停止幻想了啦!
* 在【設定場景】的倒數第二行,以及【初始條件】的中間,都有出現 {{{for n in range(len(ball)):}}} 這樣一行,這個 {{{for ...... }}} 開頭的指令,叫做 {{{for 迴圈}}}。
** 【迴圈】是告訴 Python(其他電腦語言也一樣)要做【重複的事情】,什麼重複的事情?就是迴圈指令下方【縮排進去的事情】。
*** 以【設計場景】的程式碼來看,迴圈指令在倒數第二行,之後只有一行縮排進去,也就是說,那最後一行就是迴圈要重複做的事情。
*** 以【初始條件】的程式碼來看,迴圈指令之後有好幾行縮排進去,那幾行都是要重複做的事情。
** 常見的迴圈指令有 {{{for}}} 以及 {{{while}}},一般稱為 {{{for 迴圈}}} 以及 {{{while 迴圈}}}。上面的 {{{for n in range(len(ball)):}}} 就是一個 {{{for 迴圈}}},而在[[拋體運動一]]的【細步計算】裡面,我們已經看到過 {{{while 迴圈}}} 了。
** 這個迴圈會重複幾次呢?那得看要重複做的事情是什麼。現在這個例子裡,我們要做的事情是【設定每顆球的初始條件】,所以很自然【{{{ball}}} 這個串列裡面有幾顆球,這個迴圈就該重複幾次】。上面的 {{{for n in range(len(ball)):}}} 便是以 {{{len(ball)}}}(串列裡有幾顆球)做為迴圈的重複次數。
** ''迴圈指令之後必須接著一個冒號'',表示下一行開始就是要重複做的事情。這個冒號是語法規定,沒有什麼理由的。
** 迴圈內要重複做的事情,都要【縮排】,上面的 {{{for ... :}}} 下面幾行都有內縮。這也只是語法規定,不必解釋的。__各個語言有自己的語法規定,不一定和 Python 一樣。__
** 縮排可以使用【Tab】或是【空白】,但同一段只能使用一種,不能混用。
** 縮排要縮到多裡面,隨意,但同一段必須一致。上面的迴圈內是使用【Tab】內縮【一次】。
** ''縮排結束就代表要重複做的事情結束''。上面的 {{{# 初始條件設定完成 ......}}} 這一行和迴圈指令 {{{for ... :}}} 是對齊的,表示這一行開始縮排結束,也就是迴圈裡要重複做的事情到此之前已經結束。
* 在【初始條件】的第三行,{{{def}}} 就是【定義,英文 define】,定義一個【函數/指令】。任何電腦語言都可以自訂函數/指令,用法不一定相同,在 Python 就是用 {{{def}}} 這個關鍵字。
** 任何一個自訂函數/指令,都要有名字,跟在 {{{def}}} 後面的文字,就是自訂函數/指令的名字。上面的 {{{def init():}}} 就是定義一個叫做 {{{init}}} 的函數/指令。''名字後面一定要跟著一對小括弧才行。''
** 函數/指令可以接受參數,就放在名字後面的小括弧裡。例如 {{{sin(x)}}} 這個函數,{{{sin}}} 是它的名字,小括弧裡的 {{{x}}} 就是它的參數。上面的 {{{init()}}} 括弧裡面沒有東西,表示我們定義的這個 {{{init}}} 【不】需要任何參數。
** ''小括弧後面必須接著一個冒號'',表示下一行開始就是函數/指令要做的事情。這個冒號也是語法規定,沒有什麼理由的。__各個語言有自己的語法規定,不一定和 Python 一樣。__
** 函數/指令要做的事情,都要【縮排】,上面的 {{{def init():}}} 下面幾行都有內縮,表示這幾行就是函數/指令要做的事情。
** 縮排的規則和迴圈內的縮排是一樣的。
* @@color:blue;光是定義函數/指令並不會產生結果,必須【叫用】那個函數/指令才會產生結果。@@
** 上面最後一行的 {{{init()}}} 就是在【叫用】 {{{init}}} 這個函數/指令,讓它實際執行定義在裡面的程式碼來產生結果。
*** 定義函數/指令就好像寫一段食譜,光是寫食譜並不會真的產生一道菜,必須使用食譜進行烹調才會真的產生一道菜。
* [>img(20%,)[image/QRcode-SimForFun.jpg]] @@font-size:2em;這個講義的 URL:<br><br>http://faculty.ndhu.edu.tw/~wcy2/wcy2.SimForFun.html@@

* @@font-size:1.5em;color:red;如果__幾秒鐘後仍看不到__下面這條看起來顯示得不錯的公式,試著重新載入頁面。@@ \[\large {d^2 \vec r \over dt^2} = {\vec F \over m} \quad \leftrightarrow \quad \vec F = m\vec a\]

* @@font-size:1.125em;按一下標題文字,可以展開隱藏內容,再按一下可以收起來。@@
!! 開始之前
|editable multilined|k
|width:5em;|width:25em;|width:25em;|
| 開始之前 |* ''初學者''可以直接開始使用網路版的 ~VPython:[[Web VPython|https://www.glowscript.org/]]<br>** 免安裝,上雲端,用自己的 Goolge 帳號登入就可以開始了!<br>** Web ~VPythong 有許多可以用[[一行指令就產生的 3D 物體|https://www.glowscript.org/docs/VPythonDocs/primitives.html]],非常方便!可以常去看看,多熟悉熟悉。<br>*** [[這個網頁|https://bob911017.github.io/nchu/vpython.html]]裡有相關的中文說明<br>**** @@不過裡面的英文說明網頁連結是連到舊版的英文說明@@,新版的英文說明在上面所列的[[一行指令就產生的 3D 物體|https://www.glowscript.org/docs/VPythonDocs/primitives.html]]這個網頁裡。|* [[物理現象的數值模擬]],沒看過的,想看一下的,就點進去看看吧。|
|~|* ''有經驗者'',安裝才有全部的功能喔!到 [[VPython.org|https://vpython.org/]] 來安裝吧!<br>** ~VPython(http://vpython.org/)是 Python 數以百計強大外掛的其中一個,專門處理 3D 電腦繪圖的所有細節,讓它變得極容易使用。|* 目前線上版還無法使用 Python 的其他外掛,只有安裝版才行哦!<br>* Python 的外掛太多、太好用了,進階使用者一定要用安裝版!|
| 新手說明 |* [[數值模擬一、二、三]]<br>## 設計場景<br>## 初始條件<br>## 細步計算|* 照著這三步走,你也可以很快上手哦!<br>* 怎麼學比較有效?邊做邊學最有效!|
!! 第一堂課
|editable multilined|k
|width:2em;|width:20em;|width:40em;|
| ''第一堂課'' |* [[拋體運動一]]<br>** ''最簡單''的狀況,沒有空氣阻力|* ''課後練習''<br>## 我們都知道【45&deg; 拋射距離最遠】這件事,確認一下程式的模擬結果是不是真的 45&deg; 拋射最遠。<br>## 丟鉛球的時候,鉛球出手的地方應該在頭部附近,絕對不是從地板開始,這樣的話還是 45&deg; 拋射最遠嗎?用你的程式丟看看結果如何?<br>|
|~|* [[拋體運動二]]<br>** 次簡單情況:一次方(線性)空氣阻力<br>\[\Large{\boxed{\vec f = -b\vec v}}\]|* ''課後練習''<br>## 把球的半徑、質量、初速率等套用''實際生活中常見的''球體,例如棒球、籃球、乒乓球等,''看看線性空氣阻力的效果明顯還是不明顯?''<br>## 這個''線性阻力用在實際生活常見的物體''(如棒球、籃球、乒乓球等)的時候,''是否切合實際?''(@@須呈現實際與模擬結果的比較@@)|
|~|* [[拋體運動三]]<br>** 更一般的情況:二次方阻力 -- 簡單版<br>\[\Large{\boxed{\vec f = -{1 \over 2}\rho v^2 C_{D}A\ \hat v, \quad C_D ~\sim 0.5}}\]|* ''課後練習''<br>## 把球的半徑、質量、初速率等套用''實際生活中常見的''球體,例如棒球、籃球、乒乓球等,''看看二次方空氣阻力的效果明顯還是不明顯?''<br>## 【45&deg; 拋射距離最遠】這件事,在此時是否還是正確的?<br>## 丟鉛球的話呢?拋射最遠的角度是多少?(鉛球出手的地方應該在頭部附近,絕對不是從地板開始)<br>## ''這個阻力是否較符合實際生活中常見的現象?''(@@須呈現實際與模擬結果的比較@@)<br>|
|~|* [[拋體運動四]]<br>** 更一般的情況:二次方阻力 -- 不簡單版<br>\[\Large{\boxed{\vec f = -{1 \over 2}\rho v^2 C_{D}A\ \hat v}}\]|* ''課後練習''<br>## 用同樣的球、同樣的初始條件,比較這個【不】簡單版和前面簡單版的結果,差別多大?<br>## 多使用幾種日常生活常見的球體,找出它們的尺寸與質量,比較一下不簡單版和簡單版的結果,差別多大?<br>|
!! 第二堂課
|editable multilined|k
|width:2em;|width:20em;|width:40em;|
| ''第二堂課'' |* [[單擺運動一]] 歐拉法<br>** 看看軌跡有沒有很粗?|過於簡單的做法,__程式很好寫,''誤差卻很大!''__<br>* ''課後練習''<br>## 矩形法(等速度運動公式,\(\vec r = \vec r+\vec v\ dt\))可以採用【左】矩形,也可以採用【右】矩形來計算,比較一下兩種做法的誤差。<br>## 比較矩形法和梯形法(等加速度運動公式,\(\vec r = \vec r + \vec v\ dt + (1/2)\ \vec a\ dt^2\))的誤差|
|~|* [[單擺運動二]]<br>** 榮格-庫塔(~Runge-Kutta)方法|計算量只多個幾倍,__準確度卻能''提高千倍萬倍!''__<br>這麼好康的事情,怎麼能放過呢?<br>* ''課後練習''<br>## 比較[[單擺運動一]]和[[單擺運動二]]的誤差。<br>## 找出模擬運動的擺動週期,和理論值做比較。|
|~|* [[單擺運動三]]<br>** 圓錐擺,開始玩點有趣的吧!|* ''課後練習''<br>** 改變圓錐擺的初始速度(速率、方向等),觀察運動軌跡有什麼變化?簡短描述自己的觀察結果。|
!! 第三堂課
|editable multilined|k
|width:2em;|width:20em;|width:40em;|
| ''第三堂課'' |* [[彈簧運動一]]<br>** 經典範例|* 畫數據圖原來這麼簡單,快來練習吧!|
|~|* [[彈簧運動二]]<br>** 能量守恆|* 能量守恆是什麼?可以吃嗎?<br>** 來吃吃看啊!|
|~|* [[彈簧運動三]]<br>** 摩擦摩擦|* 摩擦力無所不在,別一直忽略它哦!|
|~|* [[彈簧運動四]]<br>** 躺太久了,站起來動一動吧!|* 站著跟躺著有什麼不一樣呢?|
<<foldHeadings>>
sudo raspi-config

go to the advanced options menu

go to GL driver

Choose Enable(如果看到三個選項,選第一個)

Choose OK

It will prompt you to reboot, say Yes
!! 目標
現階段電腦硬體已經發展到相當好用的程度,__即使一台中階手機或是小樹莓派都可以勝任大學普通物理程度的數值模擬!__同時,__軟體的發展也到了非常容易將計算結果以 3D 繪圖以及圖表呈現出來的地步__,而且可選擇的軟體還很多個!現在網路資訊如此發達,只要挑一個很多人在用的,有問題時就可以很容易在網路上找到說明文件、論壇問答等資源,要獲得幫助相當容易。

也就是說,使用電腦做數值模擬的門檻已經降到很低了,現代理工科學生,甚至是理工傾向的高中生,都應該減少手算能力的練習,增加數值計算能力的培養,才能在未來有更好的發展機會。
!! 語言選擇
本系列教案便是基於上述想法,以輔助學習大學普通物理為目標所設計的。選用的程式語言為 ''Python'',因為它近年來人氣快速攀升,在學界及業界都是最夯的選擇之一,學會之後能發揮的地方很多,將來不論是繼續深造或是進入業界,都有很大的幫助。

如果是程式經驗較少的''初學者'',可以先使用 [[Web VPython|http://www.glowscript.org/]],免安裝,上雲端,只需要登入即可使用。登入的話可以使用既有的 Google 帳號,也可以新建一個 ~GlowScript 帳號。
> 本系列教案的程式碼都有在 ~GlowScript 測試過(測試版本為 ~GlowScript 2.7),都可以執行。
如果對寫程式''有經驗'',想要學習更多的 Python 功能,則可以選擇安裝 ''[[VPython|http://vpython.org/]]'',它是 Python 語言的一個外掛,專門做 3D 電腦繪圖的,功能夠強大,使用起來夠容易,是很好的輔助學習工具。<<slider '' '物理現象的數值模擬##Note-1' '...'>>(點一下可以展開 / 收合)
<<<
本系列教案的主要目標是【學習物理】,並且採取【邊做邊學】的方式編寫教案,__對於 Python 新手應該也''不至於''太困難__,因此不會先對 Python 程式語言做一個完整的交代才開始,只會針對直接需要的部份做說明。建議可以先試試看直接進入學習。<<slider '' '物理現象的數值模擬##Note-2' '...'>>(點一下可以展開 / 收合)
<<<
!! 開始之前 -- 座標系與單位
討論任何物理問題之前,都必須先講好座標系原點在哪裡,座標軸方向在哪裡。一般而言電腦 3D 繪圖程式在__一開始還沒做任何事的時候__,座標系都是
<<<
# [>img(25%,)[image/teaching/Sim-Fig-Coordinate-System.JPG]]原點在螢幕正中間或者左下角,
# 正 ''\(x\)-軸''朝向螢幕''右方'',
# 正 ''\(y\)-軸''朝向螢幕''上方'',
# 正 ''\(z\)-軸''朝向螢幕''前方''(就是指向你的臉)。
<<<
大部份情況直接使用 3D 繪圖程式的座標系就夠用,有需要的時候再定義自己的座標系就行。

本系列教案所有物理量一律採用 ''MKS 單位制'',也就是 ''SI 單位制''。

現在我們就從[[數值模擬一、二、三]]開始吧!
@@display:none;
!!!! Note-1
<<<
在開始撰寫此課程講義時選用的 ~VPython 為 6.0 版,然而該版本無法直接在瀏覽器顯示結果[1,2],不方便在網路上傳播,因此__這份講義的 Demo 是使用 Javascript 來實作__[2],以 [[THREE.js|https://threejs.org/]] 進行 3D 繪圖。
----
# ~VPython 6.0 及更早版本的結果,要在網頁上直接顯示結果就需要轉換成使用 [[GlowScript|http://www.glowscript.org/]] 才行,但是 ~GlowScript 和 ~VPython 似乎並不是 100% 相容,有些程式無法正確執行。
# 根據 [[VPython 官網|http://vpython.org/]] 說法,從 ~VPython 7.0 開始將使用瀏覽器做 3D 繪圖輸出,也許以後不需要 Javascript,只要使用 Python + ~VPython 7.0 就可以了?
<<<
!!!! Note-2

如果對 Python 語言真的有困難,可以上網自行搜尋相關學習文件,或者參考底下列出的幾個網頁。(__提醒:其實不需要讀完一大堆才開始做,簡單概念具備了就可以開始邊做邊學。__)
* [[Python 官網的學習文件|https://www.python.org/doc/]](英文)
* [[Gitbook 學習如何使用 Python 語言|https://www.gitbook.com/book/chusiang/using-python/details]]
* [[Python 新手免費學習資源|http://pala.tw/begin-to-learn-python/]]
* [[Python 第一次用就上手|http://wiki.python.org.tw/Python/%E7%AC%AC%E4%B8%80%E6%AC%A1%E7%94%A8%E5%B0%B1%E4%B8%8A%E6%89%8B]]
* [[用 Python 自學程式設計|https://happycoder.org/2017/08/10/learning-programming-and-coding-with-python-introduction/]]
@@

https://en.wikipedia.org/wiki/IEEE_754
/***
!! 問題
@@font-size:2em;兩個正三角形如下圖(填色部分),證明圖中的角度為 60&deg;。@@
!! 解
@@font-size:2em;只要把洋紅色的線段旋轉 60 度就可以看出來了。@@

|[X=chkPause] @@color:blue;''Pause''@@|
|<<tw3DScene width:'40em' height:'25em' [[金進的遊戲]]>>|
***/
//{{{
//scene.axes();
//scene.createTrackballControl();
cartesian.show(false);
scene.camera.position.z *= 1.9;
//}}}
/***
!!! Common variables
***/
//{{{
scene.scale.x *= 0.007;
scene.scale.y *= 0.007;
scene.scale.z *= 0.007;

let a1 = 280, a2 = 180,
	z = scene.zaxis,
	ang = 60*Math.PI/180;
//}}}
/***
!!! First triangle
***/
//{{{
let ang1 = 150*Math.PI/180,
	t1 = triangle({
		vertices: [vector(0,0,0), vector(a1,0,0), vector(a1*Math.cos(ang),a1*Math.sin(ang),0)],
		opacity: 0.5
	});
t1.rotation.z = ang1;
//}}}
/***
!!! Second triangle
***/
//{{{
let ang2 = -65*Math.PI/180,
	t2 = triangle({
		vertices: [vector(0,0,0), vector(a2,0,0), vector(a2*Math.cos(ang),a2*Math.sin(ang),0)],
		color: 0xffff00,
		opacity: 0.5
	});
t2.rotation.z = ang2;
//}}}
/***
!!! First moving triangle
***/
//{{{
let l2 = triangle({
	vertices: [t1.getVertex(0), t1.getVertex(1).applyAxisAngle(z,ang1), t2.getVertex(1).applyAxisAngle(z,ang2)],
	color: 0x00ffff,
	wireframe: true
});
//}}}
/***
!!! Second moving triangle
***/
//{{{
let l1 = triangle({
	vertices: [t1.getVertex(0), t1.getVertex(2).applyAxisAngle(z,ang1), t2.getVertex(2).applyAxisAngle(z,ang2)],
	color: 0xff00ff,
	wireframe: true
});
//}}}
/***
!!! Indicators
***/
//{{{
let indL2 = line({
	vertices: [t2.getVertex(1).applyAxisAngle(z,ang2), t1.getVertex(1).applyAxisAngle(z,ang1)],
	color: 0x00ffff
});

let indL1 = line({
	vertices: [t2.getVertex(2).applyAxisAngle(z,ang2), t1.getVertex(2).applyAxisAngle(z,ang1)],
	color: 0xff00ff
});

let indL3 = line({
	vertices: [t2.getVertex(2).applyAxisAngle(z,ang2), t1.getVertex(2).applyAxisAngle(z,ang1)],
	color: 0xff00ff
});

// Draw an arc to indicate the angle
let ang0 = indL1.getDirection().angleTo(vector(1,0,0));
if(ang0 > Math.PI/2) ang0 = Math.PI - ang0;
let cpos0 = indL2.intersectPoint(indL1);
let angPos = cpos0.clone();
let c = circle({
	radius: 20,
	pos: cpos0,
	thetaStart: ang0,
	thetaLength: -60*Math.PI/180
});

label({
	pos: vector(0,14,0),
	text: 'A',
	color: '#ffff00'
});
let labelTheta = label({
		text: '\\(\\theta =\\)',
		color: '#ffff00'
	}),
	labelAng = label({
		text: '?',
		color: '#ffff00'
	});
//}}}
/***
!!! setIndicatorPositions()
***/
//{{{
const setIndicatorPositions = (pos) => {
	if(pos){
		angPos.copy(pos);
		c.position.copy(angPos);
		angPos.x += 14*2.7;
		angPos.y -= 7;
		labelTheta.setPosition(angPos);
		labelAng.setPosition(angPos.x+14*2,angPos.y,angPos.z);
	}
}
setIndicatorPositions(cpos0);
//}}}
/***
!!! scene.update()
***/
//{{{
let angmax = -60*Math.PI/180,
	dang = angmax / 100, cn = 1,
	T0 = performance.now();
scene.update = function(){
	if(chkPause.checked){
		if(cn !== 1){
			l1.rotation.z = 0;
			labelAng.setText('?');
			T0 = performance.now();
			indL3.rotation.z = 0;
			cn = 1;
		}
		return;
	}

	if(l1.rotation.z > angmax){
		if(l1.rotation.z === 0){
			if(performance.now()-T0 < 1000) return;
		}
		l1.rotation.z += dang;
		indL3.rotation.z = l1.rotation.z;
		c.setAngle(l1.rotation.z);
		labelAng.setText(' '+$twV.ve.round(-l1.rotation.z/Math.PI*180,1));
		setIndicatorPositions(indL3.intersectPoint(indL1));
		cn++;
		T0 = performance.now();
	}else{
		if(performance.now()-T0 > 2000){
			l1.rotation.z = 0;
			labelAng.setText('?');
			T0 = performance.now();
			indL3.rotation.z = 0;
			cn = 1;
		}
	}
}
//}}}
~VPython is Python+visual
THREE.js

現在的軟體工具讓人們可以很容易做出 3D 的內容,這對於物理教材來說是一大利多,因為很多物理概念需要足夠的三維空間想像力才能理解,而這些概念若是在適當的 3D 繪圖呈現之下,會變得很容易理解,

目前個人電腦的硬體都有足夠的 3D 繪圖能力,加上相對容易使用的軟體,

電腦 3D 繪圖的概念跟拍電影很像,基本要素有
* 導演 -- 就是寫程式的人自己
* 場景 -- 畫面中的物體,以及燈光攝影機等。
* 腳本 -- 就是程式碼