📐

理想の2Dベクトルクラスを考える

2023/09/02に公開
コード
vec2.rb
``````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

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
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

################################################################################

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)
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

#   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

true &&
end

################################################################################

def replace(other)
self.x, self.y = other.to_a
self
end

# 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

self * ONE_TURN_DEGREES / TAU
end

self / TAU
end

self
end

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

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
end

assert { v1.angle.rad_to_deg.round(2) == 90.0 }
assert { v1.length == 5.0 }
end

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.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.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)
``````

正規分布で生成する

``````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]
``````

``````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)
``````

絶対値

``````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 がやっていることは

1. 地面の方を正規化して内積を求めると影の長さが求まる
2. それを地面の方向に伸ばす(正規化した 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
``````

``````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
``````

角度

``````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.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.length                  # => 100.0
``````

``````v1 = v0.rotate_angle_add(60.deg_to_rad)
v1.round(2)                # => (0.0, 100.0)
v1.length                  # => 100.0
``````

内積

``````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
``````
``````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 すればいい。