Skip to content

Instantly share code, notes, and snippets.

@yu59
Created November 11, 2018 09:56
Show Gist options
  • Save yu59/e866c711b6700987ee84f5603a3b9be2 to your computer and use it in GitHub Desktop.
Save yu59/e866c711b6700987ee84f5603a3b9be2 to your computer and use it in GitHub Desktop.
RTG
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
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
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
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