Last active
September 16, 2023 19:13
-
-
Save tompng/150ce7470c99863dcd03d78c88a485c0 to your computer and use it in GitHub Desktop.
`ruby main.rb` sixel対応の端末で動きます(iTerm2とか) (https://github.com/tompng/sixel_canvas へ移動)
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
require_relative 'sixel' | |
class Canvas | |
TERMINAL_CLEAR_SCREEN = "\e[H\e[2J" | |
TERMINAL_CURSOR_RESET = "\e[H" | |
attr_reader :width, :height, :pixels, :color, :line_width | |
def initialize(width, height, colors: nil, antialias: 3) | |
@width = width | |
@height = height | |
@colors = colors | |
if @colors | |
self.color = 1.0 | |
else | |
self.color = 0xffffff | |
end | |
@alpha = 1.0 | |
@line_width = 1.0 | |
@antialias = antialias.to_i.clamp 1, 8 | |
@antialias_area = @antialias**2 | |
@pixels = @height.times.map do | |
[@colors ? 0.0 : 0] * @width | |
end | |
end | |
def clear(color = 0, alpha: 1) | |
alpha = alpha.clamp(0, 1).to_f | |
if @colors | |
color = color.clamp(0, 1).to_f | |
@pixels.each do |row| | |
row.map! do |c| | |
c * (1 - alpha) + color * alpha | |
end | |
end | |
else | |
color = color.to_i | |
cr = (color >> 16) & 0xff | |
cg = (color >> 8) & 0xff | |
cb = color & 0xff | |
@pixels.each do |row| | |
row.map! do |c| | |
r = (c >> 16) & 0xff | |
g = (c >> 8) & 0xff | |
b = c & 0xff | |
r = r * (1 - alpha) + cr * alpha | |
g = g * (1 - alpha) + cg * alpha | |
b = b * (1 - alpha) + cb * alpha | |
(r.round << 16) | (g.round << 8) | b.round | |
end | |
end | |
end | |
end | |
def color=(color) | |
if @colors | |
@color = color.clamp(0, 1).to_f | |
else | |
@color = color.to_i | |
@r = (@color >> 16) & 0xff | |
@g = (@color >> 8) & 0xff | |
@b = @color & 0xff | |
end | |
end | |
def alpha=(alpha) | |
@alpha = alpha.clamp(0, 1).to_f | |
end | |
def line_width=(line_width) | |
@line_width = [1, line_width].max | |
end | |
def bezier(a, b, c, d) | |
points = [] | |
_bezier_path a, b, c, d, points | |
_stroke points, @line_width * @antialias / 2 | |
end | |
def _bezier_path(pa, pb, pc, pd, points) | |
ax, ay = pa.map { _1 * @antialias } | |
bx, by = pb.map { _1 * @antialias } | |
cx, cy = pc.map { _1 * @antialias } | |
dx, dy = pd.map { _1 * @antialias } | |
dab = [(bx - ax).abs, (by - ay).abs].max | |
dcd = [(dx - cx).abs, (dy - cy).abs].max | |
dad = [(dx - ax).abs, (dy - ay).abs].max | |
step = [3 * dab, 3 * dcd, 1.5 * dad, 1].max.ceil | |
x0 = ax | |
x1 = 3 * (bx - ax) | |
x2 = 3 * (ax - 2 * bx + cx) | |
x3 = -ax + 3 * bx - 3 * cx + dx | |
y0 = ay | |
y1 = 3 * (by - ay) | |
y2 = 3 * (ay - 2 * by + cy) | |
y3 = -ay + 3 * by - 3 * cy + dy | |
(0..step).each do |i| | |
t = i.fdiv step | |
x = x0 + x1 * t + x2 * t**2 + x3 * t**3 | |
y = y0 + y1 * t + y2 * t**2 + y3 * t**3 | |
points << [x.round, y.round] | |
end | |
end | |
def line(p1, p2) | |
points = [] | |
_line_path p1, p2, points | |
_stroke points, @line_width * @antialias / 2 | |
end | |
def polygon(points) | |
render_points = [] | |
points.each_cons(2) do |p1, p2| | |
_line_path p1, p2, render_points | |
end | |
_stroke render_points, @line_width * @antialias / 2 | |
end | |
def _line_path(p1, p2, points) | |
x1, y1 = p1.map { _1 * @antialias } | |
x2, y2 = p2.map { _1 * @antialias } | |
step = [(x2 - x1).abs, (y2 - y1).abs, 1].max | |
(0..step).each do |i| | |
t = i.fdiv step | |
x = (x1 + (x2 - x1) * t).round | |
y = (y1 + (y2 - y1) * t).round | |
points << [x, y] | |
end | |
end | |
def each_overlapped_range(bounds, overlaps = 1) | |
return if bounds.empty? | |
level = 0 | |
prev = 0 | |
bounds.sort.each do |v| | |
if v % 1 == 0 # v % 1 == 0 is range begin | |
level += 1 | |
prev = v if level == overlaps | |
else # v % 1 == 0.5 is range end | |
if level == overlaps | |
yield prev, v.ceil | |
end | |
level -= 1 | |
end | |
end | |
end | |
def each_subtract_range(target_bounds, sub_bounds) | |
bounds = target_bounds + sub_bounds.map { _1 + (_1 % 1) * 0.5 - 0.125 } | |
level = 0 | |
sub_level = 0 | |
prev = 0 | |
bounds.sort.each do |v| | |
case v % 1 | |
when 0 | |
prev = v if level == 0 && sub_level == 0 | |
level += 1 | |
when 0.5 | |
level -= 1 | |
yield prev.ceil, v.ceil if level == 0 && sub_level == 0 | |
when 0.625 | |
sub_level -= 1 | |
prev = v if level > 0 && sub_level == 0 | |
when 0.875 | |
yield prev.ceil, v.ceil if level > 0 && sub_level == 0 | |
sub_level += 1 | |
end | |
end | |
end | |
def each_merged_range_value(bounds) | |
each_overlapped_range bounds do |from, to| | |
(from...to).each { yield _1 } | |
end | |
end | |
def compact_xor_range(xor_bounds) | |
ranges = [] | |
prev = -1 | |
fill = false | |
xor_bounds.sort.each do |v| | |
if fill | |
if ranges.last&.last == prev | |
ranges.last[1] = v | |
else | |
ranges << [prev, v] | |
end | |
prev = v | |
fill = false | |
else | |
prev = v | |
fill = true | |
end | |
end | |
ranges | |
end | |
def _fill(points) | |
row_xor_bounds_a = {} | |
dedup_points = [] | |
points.each do |p| | |
dedup_points << p if dedup_points.last != p | |
end | |
dedup_points.pop while dedup_points.first == dedup_points.last | |
return if dedup_points.empty? | |
dedup_points.size.times do |i| | |
x1, y1 = dedup_points[i - 1] | |
x2, y2 = dedup_points[i] | |
step = (y2 - y1).abs | |
(1...step).each do |j| | |
c1 = 2 * (step - j) | |
c2 = 2 * j | |
x = (c1 * x1 + c2 * x2 + step) / step / 2 | |
y = (c1 * y1 + c2 * y2 + step) / step / 2 | |
(row_xor_bounds_a[y] ||= []) << x | |
end | |
x0, y0 = dedup_points[i - 2] | |
if (y0 != y1 || y1 != y2) && ((y1 - y0) * (y2 - y1) > 0 || (y0 == y1 && ((x0 < x1) ^ (y1 < y2))) || (y1 == y2 && ((x1 > x2) ^ (y0 < y1)))) | |
(row_xor_bounds_a[y1] ||= []) << x1 | |
end | |
end | |
row_bounds = {} | |
row_xor_bounds_a.each do |y_a, xor_bounds| | |
x_bounds = [] | |
compact_xor_range(xor_bounds).each do |from, to| | |
x_bounds << from | |
x_bounds << to + 0.5 | |
end | |
(row_bounds[y_a / @antialias] ||= []) << x_bounds | |
end | |
row_bounds.each do |pixel_y, antialias_x_bounds| | |
_fill_antialias_bounds(pixel_y, antialias_x_bounds) | |
end | |
end | |
def _fill_antialias_bounds(pixel_y, antialias_x_bounds) | |
pixels_row = @pixels[pixel_y] | |
subtract_bounds = [] | |
updates = Hash.new 0 | |
each_overlapped_range antialias_x_bounds.flatten, @antialias do |x_from_a, x_to_a| | |
x_from = (x_from_a + @antialias - 1) / @antialias | |
x_to = (x_to_a - @antialias) / @antialias | |
subtract_bounds << x_from * @antialias | |
subtract_bounds << (x_to + 1) * @antialias + 0.5 | |
next if x_to < 0 || x_from >= @width | |
([x_from, 0].max..[x_to, @width - 1].min).each do |x| | |
updates[x] = @antialias_area if x >= 0 && x < @width | |
end | |
end | |
antialias_x_bounds.each do |x_bounds| | |
each_subtract_range x_bounds, subtract_bounds do |x_from_a, x_to_a| | |
x_from_a = x_from_a.clamp 0, @height * @antialias | |
x_to_a = x_to_a.clamp 0, @height * @antialias | |
x_from = x_from_a / @antialias | |
x_to = (x_to_a - 1) / @antialias | |
next if x_to < 0 || x_from >= @height | |
(x_from + 1..x_to - 1).each do |x| | |
updates[x] += @antialias | |
end | |
if x_from == x_to | |
updates[x_from] += x_to_a - x_from_a | |
else | |
updates[x_from] += @antialias - x_from_a % @antialias | |
updates[x_to] += (x_to_a - 1) % @antialias + 1 | |
end | |
end | |
end | |
if @colors | |
updates.each do |x, count| | |
alpha = @alpha * count / @antialias_area | |
pixels_row[x] = pixels_row[x] * (1 - alpha) + @color * alpha | |
end | |
else | |
updates.each do |x, count| | |
alpha = @alpha * count / @antialias_area | |
col = pixels_row[x] | |
r = (col >> 16) & 0xff | |
g = (col >> 8) & 0xff | |
b = col & 0xff | |
r = r * (1 - alpha) + @r * alpha | |
g = g * (1 - alpha) + @g * alpha | |
b = b * (1 - alpha) + @b * alpha | |
pixels_row[x] = (r.round << 16) | (g.round << 8) | b.round | |
end | |
end | |
end | |
def _stroke(points, radius) | |
x_by_y = {} | |
y_bounds = [] | |
radius_i = radius.ceil | |
points.each do |x, y| | |
(x_by_y[y] ||= {})[x] = true | |
y_bounds << y - radius_i | |
y_bounds << y + radius_i + 0.5 | |
end | |
pixel_y_set = {} | |
each_merged_range_value y_bounds do |target_y| | |
y = target_y / @antialias | |
pixel_y_set[y] = true if 0 <= y && y < @height | |
end | |
pixel_y_set.each_key do |pixel_y| | |
antialias_x_bounds = @antialias.times.map do |i| | |
target_y = pixel_y * @antialias + i | |
x_bounds = [] | |
(-radius_i..radius_i).each do |dy| | |
xs = x_by_y[target_y + dy] | |
next unless xs | |
dx2 = (radius + 0.5)**2 - dy**2 | |
next unless dx2 > 0 | |
dx = Math.sqrt(dx2).floor | |
xs.each_key do |x| | |
x_bounds << x - dx | |
x_bounds << x + dx + 0.5 | |
end | |
end | |
compact_x_bounds = [] | |
each_overlapped_range x_bounds do | |
compact_x_bounds << _1 | |
compact_x_bounds << _2 + 0.5 | |
end | |
compact_x_bounds | |
end | |
_fill_antialias_bounds(pixel_y, antialias_x_bounds) | |
end | |
end | |
def to_sixel | |
if @colors | |
max_color_index = @colors.size - 1 | |
image = @pixels.map do |row| | |
row.map { (_1 * max_color_index).round } | |
end | |
Sixel.build image, @colors | |
else | |
Sixel.build @pixels | |
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
require_relative 'canvas' | |
use_rgb = rand < 0.5 | |
# use_rgb: color = 0xRRGGBB | |
# indexed: color = 0.0..1.0 with color table | |
unless use_rgb | |
colors = 256.times.map do |i| | |
t = i.fdiv 255 | |
r, g, b = [t**3, t**2, t].map { (_1 * 255).round } | |
(r << 16) | (g << 8) | b | |
end | |
end | |
canvas = Canvas.new(240, 240, colors:) | |
puts Canvas::TERMINAL_CLEAR_SCREEN | |
loop do | |
canvas.clear 0, alpha: 0.1 | |
canvas.alpha = 1 | |
canvas.color = use_rgb ? rand(0xffffff) : 1.0 | |
canvas.line_width = 4 | |
canvas.line [rand(200), rand(200)], [rand(200), rand(200)] | |
canvas.line_width = 16 | |
canvas.color = canvas.color = use_rgb ? rand(0xffffff) : 0.8 | |
canvas.line [rand(200), rand(200)], [rand(200), rand(200)] | |
canvas.alpha = 0.8 | |
canvas.line [rand(200), rand(200)], [rand(200), rand(200)] | |
canvas.bezier [rand(180), rand(180)], [100, rand(180)], [rand(180), 40], [160 + rand(200), 160 + rand(200)] | |
path = [] | |
canvas._bezier_path([30, 30], [100, 280], [120, -40], [60, 220], path) | |
canvas._fill(path) | |
puts Canvas::TERMINAL_CURSOR_RESET + canvas.to_sixel | |
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
module Sixel | |
N = 4 | |
SENSIVITY = (N * N).times.map { _1 * 51 / (N * N) }.shuffle | |
def self.build(image, palette = nil) | |
header = "\ePq" | |
footer = "\e\\" | |
if palette | |
color_table = palette.map.with_index do |color, idx| | |
r, g, b = 3.times.map { ((color >> (16 - 8 * _1) & 0xff) * 100 / 255.0).round } | |
"##{idx};2;#{r};#{g};#{b}" | |
end | |
else | |
color_table = (6**3).times.map { "##{[_1, 2, _1/6/6*20, _1/6%6*20, _1%6*20] * ';'}" }.join | |
end | |
h = image.size | |
w = image.first.size | |
sixel_lines = (h.fdiv(6).ceil).times.map { {} } | |
h.times do |y| | |
w.times do |x| | |
color = image[y][x] | |
unless palette | |
sensivity = SENSIVITY[x % N * N + y % N] | |
r, g, b = 3.times.map do |k| | |
v = color >> (16 - 8 * k) & 0xff | |
(v + sensivity) / 51 | |
end | |
color = r * 36 + g * 6 + b | |
end | |
(sixel_lines[y / 6][color] ||= Hash.new(0))[x] |= 1 << (y % 6) | |
end | |
end | |
output = [header, color_table] | |
sixel_lines.each do |line| | |
linedata = [] | |
line.each do |color, bitmasks| | |
linedata << '$' unless linedata.empty? | |
linedata << "##{color}" | |
prev_x = -1 | |
bitmasks.sort_by{_1}.each do |x, bitmask| | |
linedata << (prev_x + 2 == x ? '?' : "!#{x - prev_x - 1}?") if prev_x + 1 < x | |
linedata << (63 + bitmask).chr | |
prev_x = x | |
end | |
end | |
output << linedata << '-' | |
end | |
[output, footer].join | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment