理想の2Dベクトルクラスを考える
コード
require "forwardable"
class Vec2
# https://github.com/godotengine/godot/blob/44e399ed5fa895f760b2995e59788bdb49782666/core/math/math_defs.h#L53
UNIT_EPSILON = 0.00001
private_constant :UNIT_EPSILON
class << self
def [](...)
new(...)
end
def splat(v) = new(v, v)
def zero_element = 0.0
def one_element = 1.0
def zero = splat(zero_element)
def one = splat(one_element)
def neg_one = splat(-one_element)
def max = splat(Float::INFINITY)
def min = splat(-Float::INFINITY)
def infinity = max
def neg_infinity = min
def nan = splat(0.0 / 0.0)
def x = new(one_element, zero_element)
def y = new(zero_element, one_element)
def neg_x = new(-one_element, zero_element)
def neg_y = new(zero_element, -one_element)
def left = new(-one_element, zero_element)
def right = new(+one_element, zero_element)
def up = new(zero_element, -one_element)
def down = new(zero_element, +one_element)
def axes = [x, y]
def rand(...) = new(Kernel.rand(...), Kernel.rand(...))
def rand_norm(...) = new(*normal_rand(...))
private
def normal_rand(mu: 0, sigma: 1.0, random: Random.new)
r1 = random.rand
r2 = random.rand
z1 = Math.sqrt(-2 * Math.log(r1)) * Math.sin(2 * Math::PI * r2)
z2 = Math.sqrt(-2 * Math.log(r1)) * Math.cos(2 * Math::PI * r2)
n1 = mu + sigma * z1
n2 = mu + sigma * z2
[n1, n2]
end
end
extend Forwardable
def_delegators :to_a, :each
def_delegators :to_a, :==, :hash, :<=>
def_delegators :to_a, :to_ary, :sum, :[]
include Enumerable
attr_accessor :x, :y
def initialize(x = self.class.zero_element, y = self.class.zero_element)
@x = x
@y = y
end
def eql?(other)
self == other
end
def to_a
[x, y]
end
def as_json
{ x: x, y: y }
end
def to_s
"(#{x}, #{y})"
end
def to_h(prefix: "", suffix: "")
{
"#{prefix}x#{suffix}".to_sym => x,
"#{prefix}y#{suffix}".to_sym => y,
}
end
def inspect
to_s
end
# def scale(s)
# self * s
# end
def min(other) = self.class.new([x, other.x].min, [y, other.y].min)
def max(other) = self.class.new([x, other.x].max, [y, other.y].max)
def clamp(min, max)
min.cmple(max).all? or raise "clamp: expected min <= max"
max(min).min(max)
end
# https://github.com/godotengine/godot/blob/44e399ed5fa895f760b2995e59788bdb49782666/core/math/vector2.cpp#L138
def snapped(step)
self.class.new(Math.snapped(x, step.x), Math.snapped(y, step.y))
end
def min_element = to_a.min
def max_element = to_a.max
def cmpeq(other) = self.class.new(x == other.x, y == other.y)
def cmpne(other) = self.class.new(x != other.x, y != other.y)
def cmpge(other) = self.class.new(x >= other.x, y >= other.y)
def cmpgt(other) = self.class.new(x > other.x, y > other.y)
def cmple(other) = self.class.new(x <= other.x, y <= other.y)
def cmplt(other) = self.class.new(x < other.x, y < other.y)
def abs
self.class.new(x.abs, y.abs)
end
def sign
self.class.new(
x.negative? ? -1 : 1,
y.negative? ? -1 : 1,
)
end
def signum
sign
end
def copysign(other)
abs * other.signum
end
def finite?
all?(&:finite?)
end
def nan?
any?(&:nan?)
end
def nan_mask
self.class.new(x.nan?, y.nan?)
end
def zero? = all?(&:zero?)
def nonzero? = all?(&:nonzero?)
def length_squared = dot(self)
def length = Math.sqrt(dot(self))
def norm = length
def mag = length
def magnitude = length
def length_recip = 1.0 / length
def distance_to(other) = (other - self).length
def distance_squared_to(other) = (other - self).length_squared
################################################################################
def normalize
normalized = self * length_recip
normalized.finite? or raise
normalized
end
def try_normalize
rcp = length_recip
if rcp.finite? && rcp.positive?
self * rcp
end
end
def normalize_or_zero
try_normalize || self.class.zero
end
def normalized?
(length_squared - 1.0).abs < UNIT_EPSILON
end
################################################################################
def project_onto(other)
other_len_sq_rcp = 1.0 / other.length_squared
other_len_sq_rcp.finite? or raise
other * dot(other) * other_len_sq_rcp
end
def reject_from(other)
self - project_onto(other)
end
def project_onto_normalized(other)
other.normalized? or raise
other * dot(other)
end
def reject_from_normalized(other)
self - project_onto_normalized(other)
end
################################################################################
def slide(normal)
self - project_onto_normalized(normal)
end
def bounce(normal)
self - project_onto_normalized(normal) * 2
end
def reflect(normal)
project_onto_normalized(normal) * 2 - self
end
################################################################################
def round(...) = self.class.new(x.round(...), y.round(...))
def floor(...) = self.class.new(x.floor(...), y.floor(...))
def ceil(...) = self.class.new(x.ceil(...), y.ceil(...))
def truncate(...) = self.class.new(x.truncate(...), y.truncate(...))
def trunc(...) = truncate(...)
def fract = self - floor
################################################################################
def exp
self.class.new(Math.exp(x), Math.exp(y))
end
def pow(...)
self.class.new(x.pow(...), y.pow(...))
end
def recip
self.class.new(1.0 / x, 1.0 / y)
end
def lerp(other, s)
self + (other - self) * s
end
def mix(...)
lerp(...)
end
def abs_diff_eq(other, max_abs_diff)
sub(other).abs.cmple(self.class.splat(max_abs_diff)).all?
end
################################################################################
def clamp_length(min, max)
min <= max or raise
length_sq = length_squared
if length_sq < min * min
min * self / Math.sqrt(length_sq)
elsif length_sq > max * max
max * self / Math.sqrt(length_sq)
else
self
end
end
def clamp_length_max(max)
length_sq = length_squared
if length_sq > max * max
max * self / Math.sqrt(length_sq)
else
self
end
end
def clamp_length_min(min)
length_sq = length_squared
if length_sq < min * min
min * self / Math.sqrt(length_sq)
else
self
end
end
################################################################################
class << self
def from_angle(angle)
new(Math.cos(angle), Math.sin(angle))
end
end
# https://github.com/godotengine/godot/blob/44e399ed5fa895f760b2995e59788bdb49782666/core/math/vector2.cpp#L81
def angle_to(other)
Math.atan2(cross(other), dot(other))
end
def angle_between(other)
angle_to(other)
end
# https://github.com/godotengine/godot/blob/44e399ed5fa895f760b2995e59788bdb49782666/core/math/vector2.cpp#L84
def angle_to_point(other)
(other - self).angle
end
def angle
Math.atan2(y, x)
end
def rotate(other)
# https://docs.rs/nannou_core/0.18.0/src/nannou_core/math.rs.html#92
unless other.kind_of?(self.class)
other = V.from_angle(other)
end
# https://docs.rs/glam/lait/src/glam/f32/vec2.rs.html#662
self.class.new(
x * other.x - y * other.y,
y * other.x + x * other.y,
)
end
# Experimental
def rotate_angle_add(rad)
self.class.from_angle(angle + rad) * length
end
################################################################################
def dot(other)
x * other.x + y * other.y
end
def inner_product(...) = dot(...)
################################################################################
def cross(other)
x * other.y - y * other.x
end
def perp_dot(...) = cross(...)
def outer_product(...) = cross(...)
################################################################################
def perp
right90
end
def right90
self.class.new(-y, x)
end
def left90
self.class.new(y, -x)
end
################################################################################
def add(other)
if other.kind_of?(self.class)
self.class.new(x + other.x, y + other.y)
else
self.class.new(x + other, y + other)
end
end
def sub(other)
if other.kind_of?(self.class)
self.class.new(x - other.x, y - other.y)
else
self.class.new(x - other, y - other)
end
end
def mul(other)
if other.kind_of?(self.class)
self.class.new(x * other.x, y * other.y)
else
self.class.new(x * other, y * other)
end
end
# pow and `**` are different
# float doesn't have pow, but it does have `**`.
def **(other)
if other.kind_of?(self.class)
self.class.new(x**other.x, y**other.y)
else
self.class.new(x**other, y**other)
end
end
def /(other)
if other.kind_of?(self.class)
self.class.new(x / other.x, y / other.y)
else
self.class.new(x / other, y / other)
end
end
def div(other)
if other.kind_of?(self.class)
self.class.new(x.div(other.x), y.div(other.y))
else
self.class.new(x.div(other), y.div(other))
end
end
def fdiv(other)
if other.kind_of?(self.class)
self.class.new(x.fdiv(other.x), y.fdiv(other.y))
else
self.class.new(x.fdiv(other), y.fdiv(other))
end
end
def ceildiv(other)
if other.kind_of?(self.class)
self.class.new(x.ceildiv(other.x), y.ceildiv(other.y))
else
self.class.new(x.ceildiv(other), y.ceildiv(other))
end
end
def modulo(other)
if other.kind_of?(self.class)
self.class.new(x.modulo(other.x), y.modulo(other.y))
else
self.class.new(x.modulo(other), y.modulo(other))
end
end
def +(other)
add(other)
end
def -(other)
sub(other)
end
def *(other)
mul(other)
end
def %(other)
modulo(other)
end
def coerce(other)
[self, other]
end
def -@
self.class.new(-x, -y)
end
def +@
self
end
def neg
-self
end
# def reverse
# -self
# end
# def mul_add(a, b)
# self * a + b
# end
################################################################################
def xy = self
def yx = self.class.new(y, x)
def xx = self.class.new(x, x)
def yy = self.class.new(y, y)
################################################################################
def center = self * 0.5
def middle = self * 0.5
def cover?(other, padding: 0)
true &&
((x - padding)..(x + padding)).cover?(other.x) &&
((y - padding)..(y + padding)).cover?(other.y)
end
################################################################################
def replace(other)
self.x, self.y = other.to_a
self
end
# def add!(other) = replace(add(other))
# def sub!(other) = replace(sub(other))
# def mul!(other) = replace(mul(other))
# def div!(other) = replace(div(other))
# def fdiv!(other) = replace(fdiv(other))
# def modulo!(other) = replace(modulo(other))
if false
prepend Module.new {
def initialize(...)
super
freeze
end
}
end
################################################################################
module AngleConvert
TAU = 2 * Math::PI
ONE_TURN_DEGREES = 360.0
def rad_to_deg
self * ONE_TURN_DEGREES / TAU
end
def rad_to_turn
self / TAU
end
def rad_to_rad
self
end
def deg_to_rad
self * TAU / ONE_TURN_DEGREES
end
def deg_to_turn
self / ONE_TURN_DEGREES
end
def deg_to_deg
self
end
def turn_to_deg
self * ONE_TURN_DEGREES
end
def turn_to_rad
self * TAU
end
def turn_to_turn
self
end
end
################################################################################
module MathExt
# https://github.com/godotengine/godot/blob/44e399ed5fa895f760b2995e59788bdb49782666/core/math/math_funcs.cpp#L120
def snapped(value, step)
if step.nonzero?
value = (value / step + 0.5).floor * step
end
value
end
end
################################################################################
::Numeric.include(AngleConvert)
::Math.extend(MathExt)
::V = self
end
if $0 == __FILE__
require "rspec/autorun"
RSpec.configure do |config|
config.expect_with :test_unit
end
describe Vec2 do
it "sort" do
assert { [V[3, 4], V[1, 2]].sort == [V[1, 2], V[3, 4]] }
end
it "hash key" do
assert { V[3, 4] == V[3, 4] }
assert { V[3, 4].hash == V[3, 4].hash }
assert { V[3, 4].eql?(V[3, 4]) }
a = {V[3, 4] => true}
assert { a[V[3, 4]] }
end
it "rand_norm" do
assert { V.rand_norm.finite? }
end
describe "rotation" do
before do
@v0 = V.from_angle(45.deg_to_rad).mul(5)
end
it "rotate with radian" do
v1 = @v0.rotate(45.deg_to_rad)
assert { v1.angle.rad_to_deg.round(2) == 90.0 }
assert { v1.length == 5.0 }
end
it "rotate_angle_add" do
v1 = @v0.rotate_angle_add(45.deg_to_rad)
assert { v1.angle.rad_to_deg.round(2) == 90.0 }
assert { v1.length == 5.0 }
end
end
it "Enumerable" do
assert { V[true, true].all? }
assert { V[true, true].any? }
assert { V[false, false].none? }
assert { V.one.sum == 2 }
end
it "min, max" do
assert { V[3, 6].min(V[4, 5]) == V[3, 5] }
assert { V[3, 6].max(V[4, 5]) == V[4, 6] }
end
it "normalized?" do
assert { 1000.times.collect.all? { V.rand.normalize.normalized? } }
end
it "pow" do
assert { V[2, 3]**2 == V[4, 9] }
assert { V[2, 3].pow(2) == V[4, 9] }
end
it "**" do
assert { V[2.0, 3.0]**2.0 == V[4.0, 9.0] }
end
it "distance_to" do
a = V.zero
b = V.one
assert { a.distance_to(b) == b.distance_to(a) }
end
end
end
# >> .......
# >>
# >> Finished in 0.01079 seconds (files took 0.07285 seconds to load)
# >> 7 examples, 0 failures
# >>
短かく書けるようにする
V = Vec2
整数と同じぐらい手短に書きたいので、V = Vec2 として V を使いたい。
クラスメソッド
生成する
V.new # => (0.0, 0.0)
V.new(3, 4) # => (3, 4)
new は冗長なのであまり使いたくない。
[] が使える
V[] # => (0.0, 0.0)
V[3, 4] # => (3, 4)
基本的に []
を使う。
要素には何でも入れられる
V[true, false] # => (true, false)
V[3, 4] # => (3, 4)
V[3.0, 4.0] # => (3.0, 4.0)
V["Left", "Right"] # => (Left, Right)
といっても数値か論理値を想定している。数値が整数なら浮動小数点に変換したりなどしない。でも整数が途中で浮動小数点になることはある。
同じ値で生成する
V.splat(3) # => (3, 3)
角度から生成する
v = V.from_angle(45.deg_to_rad) # => (0.7071067811865476, 0.7071067811865475)
v.angle.rad_to_deg.round # => 45
あらかじめ決まった値で生成する
V.zero # => (0.0, 0.0)
V.one # => (1.0, 1.0)
V.neg_one # => (-1.0, -1.0)
V.nan # => (NaN, NaN)
V.max # => (Infinity, Infinity)
V.min # => (-Infinity, -Infinity)
V.x # => (1.0, 0.0)
V.neg_x # => (-1.0, 0.0)
V.y # => (0.0, 1.0)
V.neg_y # => (0.0, -1.0)
V.right # => (1.0, 0.0)
V.left # => (-1.0, 0.0)
V.down # => (0.0, 1.0)
V.up # => (0.0, -1.0)
V.axes # => [(1.0, 0.0), (0.0, 1.0)]
乱数で生成する
V.rand # => (0.5488135039273248, 0.7151893663724195)
V.rand(100) # => (67, 67)
引数は Kernel.rand と同じ
正規分布で生成する
V.rand_norm # => (0.40748125033346566, -2.074030950141654)
V.rand_norm(sigma: 2) # => (7.988729915610515, 3.3272191006819147)
V.rand_norm(mu: 100) # => (100.30702747675242, 99.31250729390302)
- sigma → 1σ区間の幅 (初期値: 1.0)
- mu → 中心 (初期値: 0.0)
インスタンスメソッド
配列化する
V[3, 4].to_a # => [3, 4]
次のメソッドたちは to_a に委譲している。
v = V.one
v.each # => #<Enumerator: [1.0, 1.0]:each>
v == v # => true
v.eql?(v) # => true
v.hash # => 1192593652170734305
v <=> v # => 0
v.to_ary # => [1.0, 1.0]
v[0] # => 1.0
Enumerable 対応
V[true, true].all? # => true
V[false, true].any? # => true
V[false, false].none? # => true
V.one.sum # => 2.0
V.one.count # => 2
文字列化する
V[3, 4].to_s # => "(3, 4)"
文字列化する (デバッグ用)
V[3, 4].inspect # => "(3, 4)"
[]
を使うと配列と区別がつかないため ()
を使う。
Hash 化する
V[3, 4].to_h # => {:x=>3, :y=>4}
V[3, 4].to_h(prefix: "a") # => {:ax=>3, :ay=>4}
V[3, 4].to_h(suffix: "1") # => {:x1=>3, :y1=>4}
suffix の指定で Ruby 2D のような座標をシンボルで書くタイプにも変換しやすくする。
各成分同士の最小・最大
V[3, 6].min(V[4, 5]) # => (3, 5)
V[3, 6].max(V[4, 5]) # => (4, 6)
指定の範囲内に入れる
V[1, 9].clamp(V[3, 3], V[4, 4]) # => (3, 4)
大きい方・小さい方の要素を得る
V[3, 4].min_element # => 3
V[3, 4].max_element # => 4
比較
v = V[3, 4]
v.cmpeq(v) # => (true, true)
v.cmpne(v) # => (false, false)
v.cmpge(v) # => (true, true)
v.cmpgt(v) # => (false, false)
v.cmple(v) # => (true, true)
v.cmplt(v) # => (false, false)
結果で条件分岐する場合はさらに any?
all?
none?
などを呼ぶ。
絶対値
V[-3, -4].abs # => (3, 4)
符号のみを保持したベクトルを返す
V[3, -4].signum # => (1, -1)
V[-3, 4].signum # => (-1, 1)
V[3, -4].sign # => (1, -1)
V[-3, 4].sign # => (-1, 1)
符号のみをコピーする
V[-3, 6].copysign(V[7, -8]) # => (3, -6)
コピーといっても破壊的ではない。
有限?
V[3, 4].finite? # => true
NaN か?
V.nan.nan? # => true
NaN の要素だけ true なベクトルを返す
V[1.0, 0.0 / 0.0].nan_mask # => (false, true)
二乗したときの長さ
V[3, 4].length_squared # => 25
長さ
V[3, 4].length # => 5.0
V[3, 4].norm # => 5.0
V[3, 4].mag # => 5.0
V[3, 4].magnitude # => 5.0
ライブラリによってメソッド名が異なるためいろんな alias を用意している。Ruby の Matrix の Vector にある r は半径と誤読するため alias にしない。
長さの逆数
V[3, 4].length_recip # => 0.2
これがあると割り算を掛け算に変換できる。
100 / V[3, 4].length # => 20.0
100 * V[3, 4].length_recip # => 20.0
対象との差分の長さの二乗
V[4, 5].distance_squared_to(V[1, 1]) # => 25
対象との差分の長さ
V[4, 5].distance_to(V[1, 1]) # => 5.0
正規化
V[3, 4].normalize # => (0.6000000000000001, 0.8)
V[3, 4].normalize.length # => 1.0
0ベクトルは正規化できない
V.zero.normalize rescue $! # => RuntimeError
正規化できない場合は nil を返す版
V.zero.try_normalize # => nil
正規化できない場合は zero を返す版
V.zero.normalize_or_zero # => (0.0, 0.0)
正規化してあるか?
V[3, 4].normalized? # => false
V[3, 4].normalize.normalized? # => true
射影
a = V[2.0, 5.0]
b = V[6.0, 3.0]
a から b への正射影
a.project_onto(b) # => (3.6, 1.8)
これは b を地面と考えたとき太陽視点での a の影または b を地面として垂直にジャンプした a の着地点に相当する。project_onto がやっていることは
- 地面の方を正規化して内積を求めると影の長さが求まる
- それを地面の方向に伸ばす(正規化した b だけスケールする)と位置が求まる
a.dot(b.normalize) * b.normalize # => (3.6, 1.8)
射影先が正規化されていればその処理を省けるので専用のメソッドがある。
a.project_onto_normalized(b.normalize) # => (3.6, 1.8)
射影できない場合は例外を出す
V.zero.project_onto(V.zero) rescue $! # => RuntimeError
垂直方向への射影
a.reject_from(b) # => (-1.6, 3.2)
これは b を地面と考えたときの真横から強い光を当てて壁にできる影または b を地面と考えたときのジャンプした a の高さに相当する。これは単に a - 着地点(a から b への投射)
でも求まる。
a - a.project_onto(b) # => (-1.6, 3.2)
上の射影と同様に地面が正規化されている版もある。
a.reject_from_normalized(b.normalize) # => (-1.6, 3.2)
小数部補正
V[3.4, 4.5].round # => (3, 5)
V[3.4, 4.5].floor # => (3, 4)
V[3.4, 4.5].ceil # => (4, 5)
V[3.4, 4.5].truncate # => (3, 4)
V[3.4, 4.5].trunc # => (3, 4)
小数部のみ残す
V[3.4, 4.5].fract # => (0.3999999999999999, 0.5)
指数関数
V[3, 4].exp # => (20.085536923187668, 54.598150033144236)
V[Math::E**3, Math::E**4] # => (20.085536923187664, 54.59815003314423)
Math::E を参照するより exp を使った方が精度が高いらしい
べき乗
V[3, 4]**2 # => (9, 16)
V[3, 4].pow(2) # => (9, 16)
V[3, 4].pow(2, 5) # => (4, 1)
逆数
V[3, 4].recip # => (0.3333333333333333, 0.25)
逆数を使うと割り算を掛け算に変換できる。
V.splat(100.0) / V[3, 4] # => (33.333333333333336, 25.0)
V.splat(100.0) * V[3, 4].recip # => (33.33333333333333, 25.0)
掛け算にすると右辺・左辺を気しなくてよくなるので逆にして
V[3, 4].recip * V.splat(100.0) # => (33.33333333333333, 25.0)
と書ける。もし割り算だと左右を入れ替えると当然結果が変わってしまう。
V[3, 4] / V.splat(100.0) # => (0.03, 0.04)
線形補間
V[3, 3].lerp(V[4, 4], 0.0) # => (3.0, 3.0)
V[3, 3].lerp(V[4, 4], 0.5) # => (3.5, 3.5)
V[3, 3].lerp(V[4, 4], 1.0) # => (4.0, 4.0)
alias
V[3, 3].mix(V[4, 4], 0.5) # => (3.5, 3.5)
誤差を許容した比較
V.zero.abs_diff_eq(V[0.0, 0.01], 0.00) # => false
V.zero.abs_diff_eq(V[0.0, 0.01], 0.01) # => true
長さ補正
V[3, 4].length # => 5.0
V[3, 4].clamp_length(6, 7) # => (3.6, 4.8)
V[3, 4].clamp_length(6, 7).length # => 6.0
V[3, 4].clamp_length_min(6) # => (3.6, 4.8)
V[3, 4].clamp_length_min(6).length # => 6.0
V[3, 4].clamp_length_max(4) # => (2.4, 3.2)
V[3, 4].clamp_length_max(4).length # => 4.0
長さを指定して new したいとき:
V.one.clamp_length_min(5) # => (3.5355339059327373, 3.5355339059327373)
指定のベクトルの倍数の近い方に四捨五入で合わせる
V[5.0, 14.0].snapped(V[10.0, 10.0]) # => (10.0, 10.0)
2点間の角度差
a = V[2.0, 0.0]
b = V[1.0, 0.0]
b.angle_to(a) # => 0.0
b.angle_between(a) # => 0.0
a.angle - b.angle # => 0.0
自分から相手を見たときの角度
a = V[2.0, 0.0]
b = V[1.0, 0.0]
a.angle_to_point(b) # => 3.141592653589793
両方を位置ベクトルと見なしたときの a から b 方向への角度を返す。
角度
V[2, 2].angle # => 0.7853981633974483
回転
v0 = V.from_angle(30.deg_to_rad) * 100
30度(長さ100)のベクトルを60度回転して90度になる例:
v1 = v0.rotate(60.deg_to_rad)
v1.round(2) # => (0.0, 100.0)
v1.angle.rad_to_deg.round # => 90
v1.length # => 100.0
rotate には方向ベクトルを渡してもよい。
v1 = v0.rotate(V.from_angle(60.deg_to_rad))
v1.round # => (0, 100)
回転される側の方向に角度を足して再度生成する方法もある。
v1 = V.from_angle(v0.angle + 60.deg_to_rad) * v0.length
v1.round(2) # => (0.0, 100.0)
v1.angle.rad_to_deg.round # => 90
v1.length # => 100.0
それを簡単にしたのが rotate_angle_add になる。
v1 = v0.rotate_angle_add(60.deg_to_rad)
v1.round(2) # => (0.0, 100.0)
v1.angle.rad_to_deg.round # => 90
v1.length # => 100.0
後者は glam のメソッドにはなかったが glam の方法より速かったので入れてある。
内積
V[2, 3].dot(V[4, 5]) # => 23
V[2, 3].inner_product(V[4, 5]) # => 23
2 * 4 + 3 * 5 # => 23
- a.dot(b) → a.x * b.x + a.y * b.y
- 底辺 = 斜辺.dot(地面.normalize)
外積
V[2, 3].cross(V[4, 5]) # => -2
V[2, 3].perp_dot(V[4, 5]) # => -2
V[2, 3].outer_product(V[4, 5]) # => -2
2 * 5 - 4 * 3 # => -2
- a.cross(b) → a.x * b.y - a.y * b.x
- 対辺 = 地面.normalize.cross(斜辺)
法線
V[2, 3].perp # => (-3, 2)
V[2, 3].right90 # => (-3, 2)
法線の逆向き
V[2, 3].left90 # => (3, -2)
left right の動作が画面下方向を正とする座標系に依存しているのがちょっと気持ち悪い。
壁ずり・反射・鏡像ベクトル
a = V[600.0, 340.0]
b = V[200.0, 60.0]
c = V[360.0, 52.0]
d = V[400.0, 200.0]
n = (b - a).perp.normalize # => (0.5734623443633283, -0.8192319205190405)
speed = d - c # => (40.0, 148.0)
speed.slide(n) # => (96.37583892617451, 67.46308724832214)
speed.bounce(n) # => (152.75167785234902, -13.073825503355721)
speed.reflect(n) # => (-152.75167785234902, 13.073825503355721)
引数には法線の正規化を渡す。
演算
v0 = V[2, 3]
v1 = V[4, 5]
演算子
v0 + v1 # => (6, 8)
v0 - v1 # => (-2, -2)
v0 * v1 # => (8, 15)
v0 / v1 # => (0, 0)
v0 % v1 # => (2, 3)
-v0 # => (-2, -3)
+v0 # => (2, 3)
メソッド版
v0.add(v1) # => (6, 8)
v0.sub(v1) # => (-2, -2)
v0.mul(v1) # => (8, 15)
v0.div(v1) # => (0, 0)
v0.fdiv(v1) # => (0.5, 0.6)
v0.ceildiv(v1) # => (1, 1)
v0.modulo(v1) # => (2, 3)
v0.neg # => (-2, -3)
/
と div
と fdiv
は数値に対する実行と同じで微妙に結果が異なる。
要素入れ替え
V.x.xy # => (1.0, 0.0)
V.x.yx # => (0.0, 1.0)
V.x.xx # => (1.0, 1.0)
V.x.yy # => (0.0, 0.0)
中央
v0 = V.one.clamp_length_min(5)
v0.length # => 5.0
v0.center.length # => 2.5
v0.middle.length # => 2.5
alias として middle もある。
位置ベクトルと見なしたとき対象が含まれるか?
v0 = V[3, 4]
v1 = V[4, 5]
v0.cover?(v1) # => false
v0.cover?(v1, padding: 1) # => true
v0 の先端との当たり判定を行いたいときに用いる。
配列のようにソートできる
[V[3, 4], V[1, 2]].sort # => [(1, 2), (3, 4)]
中身が似ていればハッシュキーは同じ
V[3, 4].hash # => 2814948660905148478
V[3, 4].hash # => 2814948660905148478
V[3, 4].eql?(V[3, 4]) # => true
h = { V[3, 4] => true } # => {(3, 4)=>true}
h[V[3, 4]] # => true
ついでに面倒な角度の扱い簡単にする
- 一周を 2π とする人間にはわかりにくいがコンピュータにはわかりやすい radian 単位
- 一周を 360 度とする人間にはわかりやすいがコンピュータはわかりにくい degree 単位
- 一周を 1.0 とする人間にはまぁまぁわかりやすいがコンピュータはわかりにくい単位
これらをメソッドチェインで相互変換するため Numeric を拡張する。
Math::PI.rad_to_rad # => 3.141592653589793
Math::PI.rad_to_deg # => 180.0
Math::PI.rad_to_turn # => 0.5
180.deg_to_rad # => 3.141592653589793
180.deg_to_deg # => 180
180.deg_to_turn # => 0.5
0.5.turn_to_rad # => 3.141592653589793
0.5.turn_to_deg # => 180.0
0.5.turn_to_turn # => 0.5
Immutable にすべきか Mutable にすべきか?
- Immutable
- メリット
- 予期せぬ変更による不具合を防げる
- インスタンスを使いまわせる
- デバッグが楽
- スレッドセーフ
- デメリット
- 値を変更するたびに新しいオブジェクトを生成するぶん遅い
- 更新するには根元から差し替えないといけないので参照を通して更新ができない
- メリット
- Mutable
- メリット
- 値を自由に変更できるのであらゆるケースに対応できる
- 値を変更する際に新しいオブジェクトを生成する必要がないぶん速い
- デメリット
- 予期しない変更による不整合が起きる場合がある
- スレッドセーフではない
- メリット
といったようにどちらも良し悪しがあり、人によってどちらのメリットを重視するかが異なるので、クラス設計として一方的に決められない。とはいえ、Immutable な制限をして実際に使ってみると参照を通して更新ができないのはつらいので Mutable とした。Ruby の思想としては利用者を信じる傾向にあったり、Immutable から Mutable にするのは難しいが、Mutable から Immutable にするのは簡単だからでもある。かといって破壊的メソッドを積極的には定義しない。やるのはデフォルトで freeze しないのと replace メソッドを用意しておくぐらい。必要であればコンストラクタを継承して freeze すればいい。
参考
Discussion