Created
November 11, 2018 09:56
-
-
Save yu59/e866c711b6700987ee84f5603a3b9be2 to your computer and use it in GitHub Desktop.
RTG
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule RtgWeb.Js.Canvas do | |
@moduledoc """ | |
HTMLCanvasElement context. | |
""" | |
alias ElixirScript.Core.Store | |
alias ElixirScript.JS | |
alias ElixirScript.Web | |
@dialyzer [:no_fail_call, :no_return, :no_unused] | |
@type t :: %{ | |
__id__: reference, | |
context: term, | |
element: term, | |
height: non_neg_integer, | |
width: non_neg_integer | |
} | |
@spec from_id(binary) :: t | |
def from_id(id) do | |
element = Web.Document.getElementById(id) | |
context = element.getContext("2d") | |
h = element.offsetHeight | |
w = element.offsetWidth | |
JS.mutate(element, "height", h) | |
JS.mutate(element, "width", w) | |
%{ | |
__id__: make_ref(), | |
context: context, | |
element: element, | |
height: h, | |
width: w | |
} | |
end | |
defmacro start(canvas, children) do | |
alias RtgWeb.Js.Macro, as: M | |
children = | |
for {module, args} <- children do | |
module_map = | |
M.module_to_map( | |
module, | |
id: [], | |
init: [:args], | |
area?: [:point, :state], | |
handle_cast: [:message, :state], | |
handle_click: [:point, :state], | |
handle_frame: [:canvas, :state] | |
) | |
{module_map, args} | |
end | |
quote do: RtgWeb.Js.Canvas.do_start(unquote(canvas), unquote(children)) | |
end | |
@spec set(t, binary, term) :: t | |
def set(canvas, property, value) do | |
JS.mutate(canvas.context, property, value) | |
canvas | |
end | |
def cast(id, dest, message) do | |
children = get_state(id) | |
children = | |
children.map( | |
fn {module, state}, _, _ -> | |
{:ok, state} = | |
if dest == module.id.(), | |
do: module.handle_cast.(message, state), | |
else: {:ok, state} | |
{module, state} | |
end, | |
children | |
) | |
put_state(id, children) | |
:ok | |
end | |
@doc false | |
def do_start(canvas, children) do | |
canvas.element.addEventListener("click", &handle_click(&1, canvas)) | |
children = | |
children.map( | |
fn child, _, _ -> | |
{module, args} = | |
case child do | |
{module, args} -> {module, args} | |
module -> {module, []} | |
end | |
safe_canvas = canvas |> Map.delete(:element) |> Map.delete(:context) | |
{:ok, state} = module.init.(args ++ [canvas: safe_canvas]) | |
{module, state} | |
end, | |
children | |
) | |
put_state(canvas, children) | |
Web.Window.requestAnimationFrame(fn _ -> loop(canvas) end) | |
canvas | |
end | |
defp loop(canvas) do | |
children = get_state(canvas) | |
h = canvas.element.offsetHeight | |
w = canvas.element.offsetWidth | |
canvas = | |
if canvas.width != w or canvas.height != h do | |
JS.mutate(canvas.element, "height", h) | |
JS.mutate(canvas.element, "width", w) | |
%{canvas | height: h, width: w} | |
else | |
canvas | |
end | |
canvas.context.clearRect(0, 0, w, h) | |
children = | |
children.map( | |
fn {module, state}, _, _ -> | |
{:ok, state} = module.handle_frame.(canvas, state) | |
{module, state} | |
end, | |
children | |
) | |
put_state(canvas, children) | |
Web.Window.requestAnimationFrame(fn _ -> loop(canvas) end) | |
end | |
defp handle_click(event, canvas) do | |
children = get_state(canvas) | |
event.preventDefault() | |
event.stopPropagation() | |
point = %{x: event.layerX, y: event.layerY} | |
children.reverse() | |
%{children: children} = | |
children.reduce( | |
fn accm, {module, state}, _, _ -> | |
{state, accm} = | |
if not accm.stop_propagation? do | |
{area?, stop_propagation?} = module.area?.(point, state) | |
{:ok, state} = if area?, do: module.handle_click.(point, state), else: {:ok, state} | |
{state, %{accm | stop_propagation?: stop_propagation?}} | |
else | |
{state, accm} | |
end | |
%{accm | children: [{module, state} | accm.children]} | |
end, | |
%{children: [], stop_propagation?: false} | |
) | |
put_state(canvas, children) | |
end | |
defp get_state(id) when is_reference(id) do | |
{:ok, state} = Map.fetch(Store.read(:rtg), id) | |
state | |
end | |
defp get_state(canvas), do: get_state(canvas.__id__) | |
defp put_state(id, state) when is_reference(id), | |
do: Store.update(:rtg, Map.put(Store.read(:rtg), id, state)) | |
defp put_state(canvas, state), do: put_state(canvas.__id__, state) | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule RtgWeb.Js.Enemy do | |
@moduledoc """ | |
相手 | |
""" | |
alias ElixirScript.JS | |
alias ElixirScript.Web | |
alias RtgWeb.Js.Canvas | |
alias RtgWeb.Js.Date | |
alias RtgWeb.Js.GameChannel | |
alias RtgWeb.Js.Gen2D | |
alias RtgWeb.Js.Math | |
use Gen2D | |
@dialyzer [:no_fail_call, :no_return] | |
@type t :: %{} | |
@pi :math.pi() | |
@radius 60 | |
@impl Gen2D | |
def init(args) do | |
{_, canvas} = args.find(fn {k, _}, _, _ -> k == :canvas end, args) | |
current = %{x: canvas.width / 2, y: canvas.height / 2} | |
state = %{ | |
current: current, | |
prev: current, | |
dest: current, | |
anim: %{start: 0, end: 0}, | |
hp: 1000 | |
} | |
GameChannel.on("move_to", fn msg, _, _ -> | |
msg = JS.object_to_map(msg) | |
Gen2D.cast(canvas.__id__, id(), {:move_to, msg.dest, msg.anim_end, msg.damage}) | |
end) | |
{:ok, state} | |
end | |
@impl Gen2D | |
def handle_cast({:move_to, dest, anim_end, damage}, state) do | |
now = Date.now() | |
state = %{state | prev: state.current, dest: dest, anim: %{start: now, end: anim_end, hp: state.hp - damage}} | |
Web.Console.log(damage) | |
{:ok, state} | |
end | |
@impl Gen2D | |
def handle_frame(canvas, state) do | |
state = next(state) | |
Canvas.set(canvas, "strokeStyle", "#F33") | |
canvas.context.beginPath() | |
canvas.context.arc(state.current.x, state.current.y, @radius, 0, @pi * 2) | |
canvas.context.stroke() | |
{:ok, state} | |
end | |
defp next(state) do | |
now = Date.now() | |
cond do | |
state.current == state.dest -> | |
state | |
state.anim.end <= now -> | |
%{state | current: state.dest, prev: state.dest} | |
true -> | |
time = Math.sin((now - state.anim.start) / (state.anim.end - state.anim.start) * @pi / 2) | |
x = state.prev.x + (state.dest.x - state.prev.x) * time | |
y = state.prev.y + (state.dest.y - state.prev.y) * time | |
%{state | current: %{x: x, y: y}} | |
end | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule RtgWeb.Js.Math do | |
@moduledoc false | |
use ElixirScript.FFI, global: true, name: Math | |
defexternal(abs(x)) | |
defexternal(sin(x)) | |
defexternal(acos(x)) | |
defexternal(sqrt(x, y)) | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule RtgWeb.Js.Player do | |
@moduledoc """ | |
自分 | |
""" | |
alias RtgWeb.Js.Canvas | |
alias RtgWeb.Js.Date | |
alias RtgWeb.Js.GameChannel | |
alias RtgWeb.Js.Gen2D | |
alias RtgWeb.Js.Math | |
alias ElixirScript.JS | |
alias ElixirScript.Web | |
use Gen2D | |
@dialyzer [:no_fail_call, :no_return] | |
@type t :: %{ | |
current: Gen2D.point(), | |
prev: Gen2D.point(), | |
dest: Gen2D.point(), | |
anim: %{start: non_neg_integer, end: non_neg_integer}, | |
enemy: %{x: 0, y: 0}, | |
is_collision: false, | |
hp: 1000 | |
} | |
@pi :math.pi() | |
@radius 60 | |
@anim_ms 600 | |
@impl Gen2D | |
def init(args) do | |
{_, canvas} = args.find(fn {k, _}, _, _ -> k == :canvas end, args) | |
current = %{x: canvas.width / 2, y: canvas.height / 2} | |
state = %{ | |
current: current, | |
prev: current, | |
dest: current, | |
anim: %{start: 0, end: 0}, | |
is_collision: false, | |
enemy: %{x: 0, y: 0} | |
} | |
GameChannel.on("move_to", fn msg, _, _ -> | |
msg = JS.object_to_map(msg) | |
Gen2D.cast(canvas.__id__, id(), {:move_to, msg.dest, msg.anim_end, msg.damage}) | |
end) | |
{:ok, state} | |
end | |
@impl Gen2D | |
def handle_cast({:move_to, dest, anim_end, damage}, state) do | |
Web.Console.log(dest) | |
state = %{state | enemy: dest, hp: state.hp - damage} | |
{:ok, state} | |
end | |
def collision?(point, state) do | |
dx = point.x - state.enemy.x | |
dy = point.y - state.enemy.y | |
dx * dx + dy * dy < 4 * @radius * @radius | |
end | |
@impl Gen2D | |
def area?(point, state) do | |
dx = state.current.x - point.x | |
dy = state.current.y - point.y | |
{dx * dx + dy * dy > @radius * @radius, false} | |
end | |
@impl Gen2D | |
def handle_click(point, state) do | |
now = Date.now() | |
anim_end = now + @anim_ms | |
state = %{state | prev: state.current, dest: point, anim: %{start: now, end: anim_end}} | |
damage = calc_damage(state.enemy, state) | |
Web.Console.log(damage) | |
GameChannel.push("move_to", %{ | |
"dest" => %{"x" => point.x, "y" => point.y}, | |
"anim_end" => anim_end, | |
"damage" => damage | |
}) | |
{:ok, state} | |
end | |
def calc_damage(point, state) do | |
x = point.x - state.prev.x | |
y = point.y - state.prev.y | |
r = @radius | |
dx = state.dest.x - state.prev.x | |
dy = state.dest.y - state.prev.y | |
dxy = dx * dx + dy * dy | |
#Web.Console.log(dxy) | |
cond do | |
true -> | |
#Web.Console.log(state) | |
#Web.Console.log(Math.sqrt(dxy)) | |
d = Math.abs(dx * y - dy * x) / Math.sqrt(dxy) | |
s = 2 * r * r * Math.acos(d / (2 * r)) - Math.sqrt(4 * d * d * r * r - d * d * d * d) / 2 | |
#Web.Console.log(dx) | |
#Web.Console.log(dy) | |
#Web.Console.log(x) | |
#Web.Console.log(y) | |
#Web.Console.log(d) | |
#Web.Console.log(s) | |
case s do | |
NaN -> 0 | |
_ -> round(s / 100) | |
end | |
end | |
end | |
@impl Gen2D | |
def handle_frame(canvas, state) do | |
state = next(state) | |
case state.is_collision do | |
true -> Canvas.set(canvas, "strokeStyle", "#00F") | |
false -> Canvas.set(canvas, "strokeStyle", "#FFF") | |
end | |
canvas.context.beginPath() | |
canvas.context.arc(state.current.x, state.current.y, @radius, 0, @pi * 2) | |
canvas.context.stroke() | |
{:ok, state} | |
end | |
@spec next(t) :: t | |
defp next(state) do | |
now = Date.now() | |
cond do | |
state.current == state.dest -> | |
state | |
state.anim.end <= now -> | |
%{state | current: state.dest, prev: state.dest} | |
true -> | |
time = Math.sin((now - state.anim.start) / (state.anim.end - state.anim.start) * @pi / 2) | |
x = state.prev.x + (state.dest.x - state.prev.x) * time | |
y = state.prev.y + (state.dest.y - state.prev.y) * time | |
%{state | current: %{x: x, y: y}, is_collision: collision?(%{x: x, y: y}, state)} | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment