Pluto.jl icon indicating copy to clipboard operation
Pluto.jl copied to clipboard

Does this physics interactive buttons can be done in Pluto.jl [1]

Open f0nzie opened this issue 1 year ago • 5 comments

I have been looking for examples in Julia and Pluto to reproduce these simulations done in Java a long time ago.

Screenshot from 2024-08-10 22-18-02

The "Start" button starts the simulation. Immediately after pressing "Start", the button changes its label to "Stop", and the two buttons on the right "Step" and "New" are greyed out (disabled). The button "Step" would allow the user to advance the simulation one $\Delta t$ at a time. The "New" button just enable the input panel for a new entry and offers the user the options of "Initialize" (stores the changes) and "Reset" (clear all values preset to default).

Screenshot from 2024-08-10 22-22-56

After the simulation is stopped, the buttons "Step" and "New" are enabled again, allowing the user to resume the simulation by clicking step by step, or start over with a new set of values with New.

Screenshot from 2024-08-10 22-29-18

The "New" button will bring a new front end, with the buttons "Initialize" and "Reset".

Screenshot from 2024-08-10 22-31-15

I have been playing a bit with the buttons and the JS custom made button but I am not versed enough in JS to make the label change (from Initialize) to a new one (Start), and then to another one (Stop), when the state of the simulation is changed by the user.

Any thoughts?

f0nzie avatar Aug 11 '24 03:08 f0nzie

I think I've got something:

using HypertextLiteral, PlutoUI
@htl("""
<!-- the wrapper span -->
<span>
	<button id="left">Start</button>
	<button id="center">Step</button>
 	<button id="right">New</button>

	<script>
		const wrapper_span = currentScript.parentElement
		const leftButton = wrapper_span.querySelector("button#left")
		const centerButton = wrapper_span.querySelector("button#center")
		const rightButton = wrapper_span.querySelector("button#right")

		// console.log(leftButton)

		leftButton.addEventListener("click", (e) => {
			console.log(leftButton.innerText + " button clicked")
			wrapper_span.dispatchEvent(new CustomEvent("input"))
			leftButton.innerText = "Stop"
			centerButton.disabled = true
			rightButton.disabled = true
			setTimeout(function(){
    			//do what you need here
				leftButton.innerText = "Start"
				centerButton.disabled = false
				rightButton.disabled = false
				}, 4000);
			e.preventDefault()
		})

		centerButton.addEventListener("click", (e) => {
			console.log(centerButton.innerText + " button clicked")
			wrapper_span.dispatchEvent(new CustomEvent("input"))
		
			e.preventDefault()
		})

		rightButton.addEventListener("click", (e) => {
			console.log(rightButton.innerText + " button clicked")
			wrapper_span.dispatchEvent(new CustomEvent("input"))
			e.preventDefault()
		})

	</script>
</span>
""")

This script will capture the click event for any of the three buttons. If the user clicks the leftButton, it will disable the buttons on the right for 4000 ms, then re-enable them right after. The leftButton also changes its inner text from Start to Stop for 4000 ms. After that, it is restored to Start.

image

image

f0nzie avatar Aug 11 '24 05:08 f0nzie

Hey @f0nzie !

That looks super fun! I love the picture on the top left :) Consider sending the notebook file when you're done, I would love to see!

The solution you came up with is exactly what I would suggest, great! I would say that this JS widget should also fire tick events, just like PlutoUI.Clock(). So when you click Step, wrapper_span.dispatchEvent(new CustomEvent("input")) gets called once, but when you click Start, an interval starts that fires wrapper_span.dispatchEvent(new CustomEvent("input")) every 100ms.

You can also set wrapper_span.value = 20 or something, and then if you do @bind x @htl(...), then x will get the value 20 when the input event is fired.

In your Julia code, you can use x as the current timestamp. The easiest way to use it is:

@bind x MyTimeWidget()
begin
	x # refernce for trigger
	update_simulation!(state)
end

Stateless

Something I prefer is to make it stateless:

states = Dict(0 => initial_state)
get_state_at(time::Integer) = 
	get!(states, time) do
		prev_state = get_state_at(time - 1)
		step_simulation(prev_state)
	end
@bind x MyTimeWidget()
get_state_at(x)

-fonzie

fonsp avatar Aug 11 '24 08:08 fonsp

Hi @fonsp ,

Consider sending the notebook file when you're done

This is the notebook: https://gist.github.com/f0nzie/5e6b69f7e561751093632c637c247b4a

and a newer one, passing an argument: https://gist.github.com/f0nzie/acd51ebda291abb1c97c49dd64f1a142

f0nzie avatar Aug 11 '24 15:08 f0nzie

You can also set wrapper_span.value = 20 or something, and then if you do @bind x @htl(...), then x will get the value 20 when the input event is fired.

What is the purpose of making the wrapper_span.value = 20?

f0nzie avatar Aug 11 '24 15:08 f0nzie

If you are interested in seeing how the simulation buttons work in the original Java application, I am sharing a JAR file with a whole bunch of simulation demos. The one I showed in the post is in Chapter 12. See screenshot. To run the app, simply unzip the file. On the location where you have osp_csm.jar open a terminal and run java -jar osp_csm.jar. A launcher will open. At the bottom, select the tab "Programs". Select the chapter on the sidebar. Finally, run the app by double-clicking on ClustersApp or PercolationApp.

image

osp_csm.zip

f0nzie avatar Aug 11 '24 16:08 f0nzie