Skip to content
Permalink
main
Go to file
 
 
Cannot retrieve contributors at this time
378 lines (345 sloc) 10.3 KB
local ShapeEditBuffer = {}
function ShapeEditBuffer.new(shape)
local buffer = {
shape = shape,
values = {},
dirty = false,
compare = false
}
return setmetatable(buffer, ShapeEditBuffer)
end
function ShapeEditBuffer:__index(index)
if self.compare then
return ShapeEditBuffer[index] or self.shape[index]
end
return self.values[index] or ShapeEditBuffer[index] or self.shape[index]
end
function ShapeEditBuffer:__newindex(index, value)
if self.compare then
self:reset()
end
self.values[index] = value
if index == 'note' then
self.values.midi_note, self.values.note_name, self.values.note_freq = self.shape:get_note_values(value)
end
self.dirty = true
end
function ShapeEditBuffer:apply()
if not self.compare then
self.shape.note = self.note
self.shape.output_mode = self.output_mode
self.shape.midi_device = self.midi_device
self.shape.midi_channel = self.midi_channel
end
self:reset()
end
function ShapeEditBuffer:undo()
self.compare = not self.compare
end
function ShapeEditBuffer:reset()
self.values.note = nil
self.values.midi_note = nil
self.values.note_name = nil
self.values.note_freq = nil
self.values.output_mode = nil
self.values.midi_device = nil
self.values.midi_channel = nil
self.dirty = false
self.compare = false
end
local Shape = {}
local next_id = 1
function Shape.new(note, n, r, x, rate)
local shape = {
id = next_id,
_note = 1,
note_name = 'A3',
note_freq = 440,
output_mode = o_ENGINE,
midi_note = 69,
midi_device = 1,
midi_channel = 1,
mute = true,
_n = 0,
area = 0,
_r = r,
delta_x = 0,
x = x,
nx = x,
rate = rate,
theta = 0,
vertices = {},
side_levels = {},
voices = {}
}
shape.edits = ShapeEditBuffer.new(shape)
setmetatable(shape, Shape)
-- initialize with 'n' sides and note 'note'
shape.r = r
shape.n = n
shape.note = note
next_id = next_id + 1
return shape
end
function Shape:get_note_values(note)
local scale_degrees = #scale
local degree = (note - 1) % #scale + 1
local octave = math.floor((note - 1) / #scale)
local note_num = util.clamp(scale[degree] + octave * 12, 0, 127)
-- font 2 doesn't have a real 'sharp' character
local note_name = string.gsub(musicutil.note_num_to_name(note_num, true), '', '#')
local note_freq = musicutil.note_num_to_freq(note_num)
return note_num, note_name, note_freq
end
function Shape:__newindex(index, value)
if index == 'n' then
self._n = value
self:calculate_points()
self:calculate_area()
elseif index == 'r' then
self._r = value
self:calculate_area()
elseif index == 'note' then
self._note = value -- TODO: clamp here instead of in get_note_values()
self.midi_note, self.note_name, self.note_freq = self:get_note_values(value)
end
end
function Shape:__index(index)
if index == 'n' then
return self._n
elseif index == 'r' then
return self._r
elseif index == 'note' then
return self._note
end
return Shape[index]
end
function Shape:calculate_points()
local vertex_angle = tau / self.n
for v = 1, self.n do
local vertex = self.vertices[v]
-- initialize if necessary
if vertex == nil then
vertex = {
level = 0
}
self.vertices[v] = vertex
self.side_levels[v] = self.side_levels[v] or 0
end
-- calculate next x and y
local nx = self.nx + math.cos(self.theta + v * vertex_angle) * self.r
local ny = y_center + math.sin(self.theta + v * vertex_angle) * self.r
-- apply previous frame's 'next' values, if any
vertex.x = vertex.nx or nx
vertex.y = vertex.ny or ny
-- save next values for next frame
vertex.nx = nx
vertex.ny = ny
end
end
function Shape:calculate_area()
local area = 0
for v = 1, self.n do
local vertex = self.vertices[v]
local vertex2 = self.vertices[v % self.n + 1]
area = area + vertex.x * vertex2.y - vertex2.x * vertex.y
end
self.area = area
end
function Shape:tick()
self.x = self.nx
self.nx = self.nx + self.delta_x
self.delta_x = 0
self.theta = self.theta + self.rate
while self.theta > tau do
self.theta = self.theta - tau
end
self:calculate_points()
end
function Shape:draw_lines(selected, dim)
if self.mute then
return
end
local n = self.n
if n == 2 then
n = 1
end
for v = 1, n do
local vertex1 = self.vertices[v]
local vertex2 = self.vertices[v % self.n + 1]
local level = self.side_levels[v]
if n == 2 then
level = math.max(self.side_levels[v + 1])
end
if selected then
level = 1 - (1 - level) * 0.6
end
screen.move(vertex1.x, vertex1.y)
screen.line(vertex2.x, vertex2.y)
if dim then
screen.level(math.floor(2 + level * 4))
else
screen.level(math.floor(2 + level * 13))
end
screen.line_width(math.max(1, level * 2.5))
screen.stroke()
end
end
function Shape:draw_points(selected, dim)
for v = 1, self.n do
local vertex = self.vertices[v]
local level = vertex.level
if selected then
level = 1 - (1 - level) * 0.8
end
screen.circle(vertex.x, vertex.y, 0.5 + level * 3)
if dim then
screen.level(math.floor(3 + level * 9))
else
screen.level(math.floor(6 + level * 9))
end
screen.fill()
end
if selected then
local x_clamped = util.clamp(self.x, 0, 128)
if self.mute then
screen.circle(x_clamped, y_center, 1.55)
screen.level(4)
screen.stroke()
else
screen.circle(x_clamped, y_center, 1.1)
screen.level(10)
screen.fill()
end
end
end
-- check whether a moving point will intercept a moving line between now and
-- the next animation frame
function calculate_point_segment_intersection(v1, v2a, v2b, x_center, n)
-- two vectors expressible in terms of t (time), using nx,ny and x,y: v2a to v1, and v2a to v2b
-- if their cross product is zero at any point in time, that's when they collide
local t, vel
if v2a.nx == v2a.x and v2a.ny == v2a.y and v2b.nx == v2b.x and v2b.ny == v2b.y then
-- special case if v2a and v2b aren't moving: cross product won't involve t^2, so quadratic
-- formula won't work; we can just solve for t:
local d1x = v1.x - v2a.x
local d1y = v1.y - v2a.y
local dd1x = v1.nx - v1.x
local dd1y = v1.ny - v1.y
local d2x = v2b.x - v2a.x
local d2y = v2b.y - v2a.y
-- coefficients of t and t^0, just like below ('a' would be 0)
local b = dd1x * d2y - dd1y * d2x
local c = d1x * d2y - d1y * d2x
t = -c / b
if t < 0 or t > 1 then
return nil
end
-- velocity is, as below, the derivative of the cross product
vel = b
else
-- if everything's moving, we'll have to do this the hard way
-- distances used repeatedly below
local d1x = v1.x - v2a.x
local d1y = v1.y - v2a.y
local dd1x = v1.nx - v2a.nx - d1x
local dd1y = v1.ny - v2a.ny - d1y
local d2x = v2b.x - v2a.x
local d2y = v2b.y - v2a.y
local dd2x = v2b.nx - v2a.nx - d2x
local dd2y = v2b.ny - v2a.ny - d2y
-- coefficients of t^2, t, and t^0 in cross product, worked out by hand
local a = dd1x * dd2y - dd1y * dd2x
local b = dd2y * d1x + dd1x * d2y - dd2x * d1y - dd1y * d2x
local c = d2y * d1x - d2x * d1y
-- now we'll plug all of this into the quadratic formula...
-- a negative discriminant means there's no solution (no intersection). bail.
local discriminant = b * b - 4 * a * c
if discriminant < 0 then
return nil
end
local sqrt = math.sqrt(discriminant)
t = (-b + sqrt) / (2 * a)
-- we're looking for a solution in the range [0, 1], so if one of the two possible solutions
-- doesn't fit, try the other, and if that doesn't fit, give up
if t < 0 or t > 1 then
t = (-b - sqrt) / (2 * a)
end
if t < 0 or t > 1 then
return nil
end
-- velocity is the derivative of the cross product
vel = 2 * a * t + b
end
-- now check that, at time t, v1 actually intersects with line segment v2a v2b (as opposed to
-- somewhere else on the line described by the two points)
local v1xt = v1.x + t * ( v1.nx - v1.x)
local v1yt = v1.y + t * ( v1.ny - v1.y)
local v2axt = v2a.x + t * (v2a.nx - v2a.x)
local v2ayt = v2a.y + t * (v2a.ny - v2a.y)
local v2bxt = v2b.x + t * (v2b.nx - v2b.x)
local v2byt = v2b.y + t * (v2b.ny - v2b.y)
local pos = (v1xt - math.min(v2axt, v2bxt)) / math.abs(v2axt - v2bxt)
if pos >= 0 and pos <= 1 then
-- it's a hit! was v1 moving into or out of the shape whose vertices include v2a and v2b?
-- but first: a special case for 2-sided 'polygons' (lines), where the center product below
-- "should" be exactly zero, but may not be due to rounding error: there's no such thing as
-- moving into or out of the shape anyway, so we should count all collisions
if n > 2 then
-- find direction (inward or outward) by comparing the signs of the velocity and the cross
-- product between the side and the shape's center point
local center_product = (x_center - v2axt) * (v2byt - v2ayt) - (y_center - v2ayt) * (v2bxt - v2axt)
local inward = (vel > 0 and center_product > 0) or (vel < 0 and center_product < 0)
-- skip inner- or outer-moving collisions if the params tell us to
if (trigger_style == s_IN) and not inward then
return nil
elseif (trigger_style == s_OUT) and inward then
return nil
end
end
return t, pos, vel, v1xt, v1yt
end
return nil
end
-- check whether any of this shape's points will touch another shape's sides
-- between now and the next animation frame
function Shape:check_intersection(other)
-- if either shape is muted, skip calculation
if (mute_style == m_BOTH and self.mute) or other.mute then
return
end
-- if shapes are too far apart to intersect, skip calculation
if math.max(self.x, self.nx) + self.r < math.min(other.x, other.nx) - other.r then
return
elseif math.max(other.x, other.nx) + other.r < math.min(self.x, self.nx) - self.r then
return
end
for v = 1, self.n do
local vertex1 = self.vertices[v]
local sides = other.n
-- special case for "two-sided" "polygon": that's a line, and if we counted
-- both sides, we'd be counting it twice
if sides == 2 then
sides = 1
end
for s = 1, sides do
-- TODO: it's probably a waste of time to do this for every pair of segments...
local vertex2a = other.vertices[s]
local vertex2b = other.vertices[s % other.n + 1]
local t, pos, vel, x, y = calculate_point_segment_intersection(vertex1, vertex2a, vertex2b, other.x, other.n)
if t ~= nil then
if t > 0 then
clock.run(function()
clock.sleep(t * rate)
handle_strike(other, s, pos, vel, x, y, self, v)
end)
else
handle_strike(other, s, pos, vel, x, y, self, v)
end
other.side_levels[s] = 1
vertex1.level = 1
end
end
end
end
return Shape