From 2dbd84e514e17c367a880cb092fd9d5244659c6a Mon Sep 17 00:00:00 2001 From: HKXluo Date: Sun, 4 May 2025 21:22:18 +0800 Subject: [PATCH] lib --- Demo/SanjuuniApi-Demo/demo2.lua | 2 +- Demo/SanjuuniApi-Demo/lib/32vid-player.lua | 657 --------------------- Demo/SanjuuniApi-Demo/lib/bimg-player.lua | 350 ++++++++--- 3 files changed, 285 insertions(+), 724 deletions(-) delete mode 100644 Demo/SanjuuniApi-Demo/lib/32vid-player.lua diff --git a/Demo/SanjuuniApi-Demo/demo2.lua b/Demo/SanjuuniApi-Demo/demo2.lua index 47ad1be..dc56744 100644 --- a/Demo/SanjuuniApi-Demo/demo2.lua +++ b/Demo/SanjuuniApi-Demo/demo2.lua @@ -56,7 +56,7 @@ luafile = http.get(download_url) if luafile then print("\nLua file downloaded successfully.") - -- 保存Lua文件 / Save Lua file + -- 保存bimg文件 / Save bimg file f = fs.open("demo.bimg", "w") f.write(luafile.readAll()) f.close() diff --git a/Demo/SanjuuniApi-Demo/lib/32vid-player.lua b/Demo/SanjuuniApi-Demo/lib/32vid-player.lua deleted file mode 100644 index b34d8ca..0000000 --- a/Demo/SanjuuniApi-Demo/lib/32vid-player.lua +++ /dev/null @@ -1,657 +0,0 @@ --- 32vid-player from sanjuuni --- Licensed under the MIT license - -local inflate do - - local floor = math.floor - local ceil = math.ceil - local min = math.min - local max = math.max - - -- utility functions - - local function memoize(f) - local cache = {} - return function(...) - local key = table.concat({...}, "-") - if not cache[key] then - cache[key] = f(...) - end - return cache[key] - end - end - - local function int(bytes) - local n = 0 - for i = 1, #bytes do - n = 256*n + bytes:sub(i, i):byte() - end - return n - end - int = memoize(int) - - local function bint(bits) - return tonumber(bits, 2) or 0 - end - bint = memoize(bint) - - local function bits(b, width) - local s = "" - if type(b) == "number" then - for i = 1, width do - s = b%2 .. s - b = floor(b/2) - end - else - for i = 1, #b do - s = s .. bits(b:byte(i), 8):reverse() - assert(#s == i * 8, s) - end - end - return s - end - bits = memoize(bits) - - local function fill(bytes, len) - return bytes:rep(floor(len / #bytes)) .. bytes:sub(1, len % #bytes) - end - - local function zip(t1, t2) - local zipped = {} - for i = 1, max(#t1, #t2) do - zipped[#zipped + 1] = {t1[i], t2[i]} - end - return zipped - end - - local function unzip(zipped) - local t1, t2 = {}, {} - for i = 1, #zipped do - t1[#t1 + 1] = zipped[i][1] - t2[#t2 + 1] = zipped[i][2] - end - return t1, t2 - end - - local function map(f, t) - local mapped = {} - for i = 1, #t do - mapped[#mapped + 1] = f(t[i], i) - end - return mapped - end - - local function filter(pred, t) - local filtered = {} - for i = 1, #t do - if pred(t[i], i) then - filtered[#filtered + 1] = t[i] - end - end - return filtered - end - - local function find(key, t) - if type(key) == "function" then - for i = 1, #t do - if key(t[i]) then - return i - end - end - return nil - else - return find(function(x) return x == key end, t) - end - end - - local function slice(t, i, j, step) - local sliced = {} - for k = i < 1 and 1 or i, i < 1 and #t + i or j or #t, step or 1 do - sliced[#sliced + 1] = t[k] - end - return sliced - end - - local function range(i, j) - local r = {} - for k = j and i or 0, j or i - 1 do - r[#r + 1] = k - end - return r - end - - -- streams - - local function output_stream() - local stream, buffer = {}, {} - local curr = 0 - - function stream:write(bytes) - for i = 1, #bytes do - buffer[#buffer + 1] = bytes:sub(i, i) - end - curr = curr + #bytes - end - - function stream:back_read(offset, n) - local read = {} - for i = curr - offset + 1, curr - offset + n do - read[#read + 1] = buffer[i] - end - return table.concat(read) - end - - function stream:back_copy(dist, len) - local start, copied = curr - dist + 1, {} - for i = start, min(start + len, curr) do - copied[#copied + 1] = buffer[i] - end - self:write(fill(table.concat(copied), len)) - end - - function stream:pos() - return curr - end - - function stream:raw() - return table.concat(buffer) - end - - return stream - end - - local function bit_stream(raw, offset) - local stream = {} - local curr = 0 - offset = offset or 0 - - function stream:read(n, reverse) - local start = floor(curr/8) + offset + 1 - local bb = "" - for i = start, start + ceil(n/8) do - local b = raw:byte(i) - for j = 0, 7 do bb = bb .. bit32.extract(b, j, 1) end - end - local b = bb:sub(curr%8 + 1, curr%8 + n) - curr = curr + n - return reverse and b or b:reverse() - end - - function stream:seek(n) - if n == "beg" then - curr = 0 - elseif n == "end" then - curr = #raw - else - curr = curr + n - end - return self - end - - function stream:is_empty() - return curr >= 8*#raw - end - - function stream:pos() - return curr - end - - return stream - end - - -- inflate - - local CL_LENS_ORDER = {16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15} - local MAX_BITS = 15 - local PT_WIDTH = 8 - - local function cl_code_lens(stream, hclen) - local code_lens = {} - for i = 1, hclen do - code_lens[#code_lens + 1] = bint(stream:read(3)) - end - return code_lens - end - - local function code_tree(lens, alphabet) - alphabet = alphabet or range(#lens) - local using = filter(function(x, i) return lens[i] and lens[i] > 0 end, alphabet) - lens = filter(function(x) return x > 0 end, lens) - local tree = zip(lens, using) - table.sort(tree, function(a, b) - if a[1] == b[1] then - return a[2] < b[2] - else - return a[1] < b[1] - end - end) - return unzip(tree) - end - - local function codes(lens) - local codes = {} - local code = 0 - for i = 1, #lens do - codes[#codes + 1] = bits(code, lens[i]) - if i < #lens then - code = (code + 1)*2^(lens[i + 1] - lens[i]) - end - end - return codes - end - - local prefix_table - local function handle_long_codes(codes, alphabet, pt) - local i = find(function(x) return #x > PT_WIDTH end, codes) - local long = slice(zip(codes, alphabet), i) - i = 0 - repeat - local prefix = long[i + 1][1]:sub(1, PT_WIDTH) - local same = filter(function(x) return x[1]:sub(1, PT_WIDTH) == prefix end, long) - same = map(function(x) return {x[1]:sub(PT_WIDTH + 1), x[2]} end, same) - pt[prefix] = {rest = prefix_table(unzip(same)), unused = 0} - i = i + #same - until i == #long - end - - function prefix_table(codes, alphabet) - local pt = {} - if #codes[#codes] > PT_WIDTH then - handle_long_codes(codes, alphabet, pt) - end - for i = 1, #codes do - local code = codes[i] - if #code > PT_WIDTH then - break - end - local entry = {value = alphabet[i], unused = PT_WIDTH - #code} - if entry.unused == 0 then - pt[code] = entry - else - for i = 0, 2^entry.unused - 1 do - pt[code .. bits(i, entry.unused)] = entry - end - end - end - return pt - end - - local function huffman_decoder(lens, alphabet) - local base_codes = prefix_table(codes(lens), alphabet) - return function(stream) - local codes = base_codes - local entry - repeat - entry = codes[stream:read(PT_WIDTH, true)] - stream:seek(-entry.unused) - codes = entry.rest - until not codes - return entry.value - end - end - - local function code_lens(stream, decode, n) - local lens = {} - repeat - local value = decode(stream) - if value < 16 then - lens[#lens + 1] = value - elseif value == 16 then - for i = 1, bint(stream:read(2)) + 3 do - lens[#lens + 1] = lens[#lens] - end - elseif value == 17 then - for i = 1, bint(stream:read(3)) + 3 do - lens[#lens + 1] = 0 - end - elseif value == 18 then - for i = 1, bint(stream:read(7)) + 11 do - lens[#lens + 1] = 0 - end - end - until #lens == n - return lens - end - - local function code_trees(stream) - local hlit = bint(stream:read(5)) + 257 - local hdist = bint(stream:read(5)) + 1 - local hclen = bint(stream:read(4)) + 4 - local cl_decode = huffman_decoder(code_tree(cl_code_lens(stream, hclen), CL_LENS_ORDER)) - local ll_decode = huffman_decoder(code_tree(code_lens(stream, cl_decode, hlit))) - local d_decode = huffman_decoder(code_tree(code_lens(stream, cl_decode, hdist))) - return ll_decode, d_decode - end - - local function extra_bits(value) - if value >= 4 and value <= 29 then - return floor(value/2) - 1 - elseif value >= 265 and value <= 284 then - return ceil(value/4) - 66 - else - return 0 - end - end - extra_bits = memoize(extra_bits) - - local function decode_len(value, bits) - assert(value >= 257 and value <= 285, "value out of range") - assert(#bits == extra_bits(value), "wrong number of extra bits") - if value <= 264 then - return value - 254 - elseif value == 285 then - return 258 - end - local len = 11 - for i = 1, #bits - 1 do - len = len + 2^(i+2) - end - return floor(bint(bits) + len + ((value - 1) % 4)*2^#bits) - end - decode_len = memoize(decode_len) - - local function a(n) - if n <= 3 then - return n + 2 - else - return a(n-1) + 2*a(n-2) - 2*a(n-3) - end - end - a = memoize(a) - - local function decode_dist(value, bits) - assert(value >= 0 and value <= 29, "value out of range") - assert(#bits == extra_bits(value), "wrong number of extra bits)") - return bint(bits) + a(value - 1) - end - decode_dist = memoize(decode_dist) - - function inflate(data) - local stream = bit_stream(data) - local ostream = output_stream() - repeat - local bfinal, btype = bint(stream:read(1)), bint(stream:read(2)) - assert(btype == 2, "compression method not supported") - local ll_decode, d_decode = code_trees(stream) - while true do - local value = ll_decode(stream) - if value < 256 then - ostream:write(string.char(value)) - elseif value == 256 then - break - else - local len = decode_len(value, stream:read(extra_bits(value))) - value = d_decode(stream) - local dist = decode_dist(value, stream:read(extra_bits(value))) - ostream:back_copy(dist, len) - end - end - until bfinal == 1 - return ostream:raw() - end - -end -local dfpwm = require "cc.audio.dfpwm" - -local hexstr = "0123456789abcdef" -local file, err = fs.open(shell.resolve(...), "rb") -if not file then error(err) end -if file.read(4) ~= "32VD" then file.close() error("Not a 32vid file") end -local width, height, fps, nStreams, flags = ("= nFrames - 10 then print(i, os.epoch "utc" - start) start = os.epoch "utc" sleep(0) end - local frame = {palette = {}} - local tmp, tmppos = 0, 0 - local use5bit, customcompress = bit32.btest(flags, 16), bit32.band(flags, 3) == 3 - local codetree = {} - local solidchar, runlen - local function readField(isColor) - if customcompress then - if runlen then - local c = solidchar - runlen = runlen - 1 - if runlen == 0 then runlen = nil end - return c - end - if not isColor and solidchar then return solidchar end - -- MARK: Huffman decoding - local node = codetree - while true do - local n = bit32.extract(tmp, tmppos, 1) - tmppos = tmppos - 1 - if tmppos < 0 then tmp, pos, tmppos = data:byte(pos), pos + 1, 7 end - if type(node) ~= "table" then error(("Invalid tree state: position %X, frame %d"):format(pos+file.seek()-size-1, i)) end - if type(node[n]) == "number" then - local c = node[n] - if isColor then - if c > 15 then runlen = 2^(c-15)-1 return assert(solidchar) - else solidchar = c end - end - return c - else node = node[n] end - end - else - local n - if tmppos * 5 + 5 > 32 then n = bit32.extract(math.floor(tmp / 0x1000000), tmppos * 5 - 24, 5) - else n = bit32.extract(tmp, tmppos * 5, 5) end - tmppos = tmppos - 1 - if tmppos < 0 then tmp, pos = (">I5"):unpack(data, pos) tmppos = 7 end - return n - end - end - if customcompress then - -- MARK: Huffman tree reconstruction - -- read bit lengths - local bitlen = {} - if use5bit then - for j = 0, 15 do - bitlen[j*2+1], bitlen[j*2+2] = {s = j*2, l = bit32.rshift(data:byte(pos+j), 4)}, {s = j*2+1, l = bit32.band(data:byte(pos+j), 0x0F)} - end - pos = pos + 16 - else - for j = 0, 7 do - tmp, pos = (">I5"):unpack(data, pos) - bitlen[j*8+1] = {s = j*8+1, l = math.floor(tmp / 0x800000000)} - bitlen[j*8+2] = {s = j*8+2, l = math.floor(tmp / 0x40000000) % 32} - for k = 3, 8 do bitlen[j*8+k] = {s = j*8+k, l = bit32.extract(tmp, (8-k)*5, 5)} end - end - end - do - local j = 1 - while j <= #bitlen do - if bitlen[j].l == 0 then table.remove(bitlen, j) - else j = j + 1 end - end - end - if #bitlen == 0 then - -- screen is solid character - solidchar = data:byte(pos) - pos = pos + 1 - else - -- reconstruct codes from bit lengths - table.sort(bitlen, function(a, b) if a.l == b.l then return a.s < b.s else return a.l < b.l end end) - bitlen[1].c = 0 - for j = 2, #bitlen do bitlen[j].c = bit32.lshift(bitlen[j-1].c + 1, bitlen[j].l - bitlen[j-1].l) end - -- create tree from codes - for j = 1, #bitlen do - local c = bitlen[j].c - local node = codetree - for k = bitlen[j].l - 1, 1, -1 do - local n = bit32.extract(c, k, 1) - if not node[n] then node[n] = {} end - node = node[n] - if type(node) == "number" then error(("Invalid tree state: position %X, frame %d, #bitlen = %d, current entry = %d"):format(pos, i, #bitlen, j)) end - end - local n = bit32.extract(c, 0, 1) - node[n] = bitlen[j].s - end - -- read first byte - tmp, tmppos, pos = data:byte(pos), 7, pos + 1 - end - else readField() end - for y = 1, height do - local line = {"", "", "", {}} - for x = 1, width do - if pos + 5 + 1 >= #data then print(i, pos, x, y) error() end - local n = readField() - line[1] = line[1] .. string.char(128 + (n % 0x20)) - line[4][x] = bit32.btest(n, 0x20) - end - frame[y] = line - end - if customcompress then - if tmppos == 7 then pos = pos - 1 end - codetree = {} - -- MARK: Huffman tree reconstruction - -- read bit lengths - local bitlen = {} - for j = 0, 11 do - bitlen[j*2+1], bitlen[j*2+2] = {s = j*2, l = bit32.rshift(data:byte(pos+j), 4)}, {s = j*2+1, l = bit32.band(data:byte(pos+j), 0x0F)} - end - pos = pos + 12 - do - local j = 1 - while j <= #bitlen do - if bitlen[j].l == 0 then table.remove(bitlen, j) - else j = j + 1 end - end - end - if #bitlen == 0 then - -- screen is solid color - solidchar = data:byte(pos) - pos = pos + 1 - runlen = math.huge - else - -- reconstruct codes from bit lengths - table.sort(bitlen, function(a, b) if a.l == b.l then return a.s < b.s else return a.l < b.l end end) - bitlen[1].c = 0 - for j = 2, #bitlen do bitlen[j].c = bit32.lshift(bitlen[j-1].c + 1, bitlen[j].l - bitlen[j-1].l) end - -- create tree from codes - for j = 1, #bitlen do - local c = bitlen[j].c - local node = codetree - for k = bitlen[j].l - 1, 1, -1 do - local n = bit32.extract(c, k, 1) - if not node[n] then node[n] = {} end - node = node[n] - if type(node) == "number" then error(("Invalid tree state: position %X, frame %d, #bitlen = %d, current entry = %d"):format(pos, i, #bitlen, j)) end - end - local n = bit32.extract(c, 0, 1) - node[n] = bitlen[j].s - end - -- read first byte - tmp, tmppos, pos = data:byte(pos), 7, pos + 1 - end - for y = 1, height do - local line = frame[y] - for x = 1, width do - local c = readField(true) - line[2] = line[2] .. hexstr:sub(c+1, c+1) - end - end - runlen = nil - for y = 1, height do - local line = frame[y] - for x = 1, width do - local c = readField(true) - line[3] = line[3] .. hexstr:sub(c+1, c+1) - end - end - if tmppos == 7 then pos = pos - 1 end - else - for y = 1, height do - local line = frame[y] - for x = 1, width do - local c = data:byte(pos) - pos = pos + 1 - if line[4][x] then c = bit32.bor(bit32.band(bit32.lshift(c, 4), 0xF0), bit32.rshift(c, 4)) end - line[2] = line[2] .. hexstr:sub(bit32.band(c, 15)+1, bit32.band(c, 15)+1) - line[3] = line[3] .. hexstr:sub(bit32.rshift(c, 4)+1, bit32.rshift(c, 4)+1) - end - line[4] = nil - end - end - for n = 1, 16 do frame.palette[n], pos = {data:byte(pos) / 255, data:byte(pos+1) / 255, data:byte(pos+2) / 255}, pos + 3 end - video[i] = frame - end - elseif frameType == 1 and not audio then - audio = {} - if bit32.band(flags, 12) == 0 then - for i = 1, math.ceil(size / 48000) do - local data - if jit then - data = {} - for j = 0, 5 do - local t = {file.read(math.min(size - (i-1) * 48000 - j * 8000, 8000)):byte(1, -1)} - if #t > 0 then for k = 1, #t do data[j*8000+k] = t[k] end end - end - else data = {file.read(math.min(size - (i-1) * 48000, 48000)):byte(1, -1)} end - for j = 1, #data do data[j] = data[j] - 128 end - audio[i] = data - end - elseif bit32.band(flags, 12) == 4 then - local decode = dfpwm.make_decoder() - for i = 1, math.ceil(size / 6000) do - local data = file.read(math.min(size - (i-1)*6000, 6000)) - if not data then break end - audio[i] = decode(data) - end - end - elseif frameType == 8 and not subtitles then - subtitles = {} - for _ = 1, nFrames do - local start, length, x, y, color, sz = ("") end -local file, err = fs.open(shell.resolve(path), "rb") -if not file then error("Could not open file: " .. err) end -local img = textutils.unserialize(file.readAll()) -file.close() -local function drawFrame(frame, term) - for y, row in ipairs(frame) do - term.setCursorPos(1, y) - term.blit(table.unpack(row)) - end - if frame.palette then for i = 0, #frame.palette do - local c = frame.palette[i] - if type(c) == "table" then term.setPaletteColor(2^i, table.unpack(c)) - else term.setPaletteColor(2^i, c) end - end end - if img.multiMonitor then term.setTextScale(img.multiMonitor.scale or 0.5) end +local bimg = {} + +local currentAnimation = nil +local running = false +local httpEnabled = false + +-- 检查并启用HTTP功能 +if http and http.get then + httpEnabled = true end -if img.multiMonitor then - local width, height = img.multiMonitor.width, img.multiMonitor.height - local monitors = settings.get('sanjuuni.multimonitor') - if not monitors or #monitors < height or #monitors[1] < width then - term.clear() - term.setCursorPos(1, 1) - print('This image needs monitors to be calibrated before being displayed. Please right-click each monitor in order, from the top left corner to the bottom right corner, going right first, then down.') - monitors = {} - local names = {} - for y = 1, height do - monitors[y] = {} - for x = 1, width do - local _, oy = term.getCursorPos() - for ly = 1, height do - term.setCursorPos(3, oy + ly - 1) - term.clearLine() - for lx = 1, width do term.blit('\x8F ', (lx == x and ly == y) and '00' or '77', 'ff') end - end - term.setCursorPos(3, oy + height) - term.write('(' .. x .. ', ' .. y .. ')') - term.setCursorPos(1, oy) - repeat - local _, name = os.pullEvent('monitor_touch') - monitors[y][x] = name - until not names[name] - names[monitors[y][x]] = true - sleep(0.25) + +-- 内部函数:绘制单帧 +local function drawFrame(frame, termObj) + for y, row in ipairs(frame) do + termObj.setCursorPos(1, y) + termObj.blit(table.unpack(row)) + end + if frame.palette then + for i = 0, #frame.palette do + local c = frame.palette[i] + if type(c) == "table" then + termObj.setPaletteColor(2^i, table.unpack(c)) + else + termObj.setPaletteColor(2^i, c) end + end + end +end + +-- 内部函数:加载图像数据 +local function loadImageData(data) + if type(data) == "string" then + -- 如果是字符串,尝试反序列化 + local success, result = pcall(textutils.unserialize, data) + if success then + return result + else + error("Invalid image data: " .. result) end - settings.set('sanjuuni.multimonitor', monitors) - settings.save() - print('Calibration complete. Settings have been saved for future use.') + elseif type(data) == "table" then + -- 如果是table,直接使用 + return data + else + error("Unsupported image data type") end - for i = 1, #img, width * height do - for y = 1, height do - for x = 1, width do - drawFrame(img[i + (y-1) * width + x-1], peripheral.wrap(monitors[y][x])) - end - end - if img.animation then sleep(img[i].duration or img.secondsPerFrame or 0.05) - else read() break end +end + +-- 从文件加载图像 +function bimg.loadFromFile(path) + local file, err = fs.open(shell.resolve(path), "rb") + if not file then error("Could not open file: " .. err) end + local data = file.readAll() + file.close() + return loadImageData(data) +end + +-- 从URL加载图像 +function bimg.loadFromURL(url) + if not httpEnabled then + error("HTTP API is not available") end -else - term.clear() - for _, frame in ipairs(img) do - drawFrame(frame, term) - if img.animation then sleep(frame.duration or img.secondsPerFrame or 0.05) - else read() break end + + local response = http.get(url) + if not response then + error("Failed to fetch from URL: " .. url) end - term.setBackgroundColor(colors.black) - term.setTextColor(colors.white) + + local data = response.readAll() + response.close() + return loadImageData(data) +end + +-- 内部函数:校准多显示器 +local function calibrateMonitors(width, height) term.clear() term.setCursorPos(1, 1) - for i = 0, 15 do term.setPaletteColor(2^i, term.nativePaletteColor(2^i)) end + print('This image needs monitors to be calibrated before being displayed.') + print('Please right-click each monitor in order, from the top left corner') + print('to the bottom right corner, going right first, then down.') + + local monitors = {} + local names = {} + + for y = 1, height do + monitors[y] = {} + for x = 1, width do + local _, oy = term.getCursorPos() + + -- 绘制校准界面 + for ly = 1, height do + term.setCursorPos(3, oy + ly - 1) + term.clearLine() + for lx = 1, width do + term.blit('\x8F ', (lx == x and ly == y) and '00' or '77', 'ff') + end + end + + term.setCursorPos(3, oy + height) + term.write('(' .. x .. ', ' .. y .. ')') + term.setCursorPos(1, oy) + + -- 等待用户点击显示器 + repeat + local _, name = os.pullEvent('monitor_touch') + monitors[y][x] = name + until not names[name] + + names[monitors[y][x]] = true + sleep(0.25) + end + end + + settings.set('sanjuuni.multimonitor', monitors) + settings.save() + print('Calibration complete. Settings have been saved for future use.') + + return monitors end + +-- 播放图像/动画 +function bimg.play(source, options) + options = options or {} + local termObj = options.terminal or term + local loop = options.loop or false + local isURL = options.isURL or false + + -- 停止当前播放 + bimg.stop() + + -- 加载图像数据 + local img + if type(source) == "string" then + if isURL then + img = bimg.loadFromURL(source) + else + img = bimg.loadFromFile(source) + end + else + img = loadImageData(source) + end + + -- 设置运行标志 + running = true + currentAnimation = { + img = img, + term = termObj, + running = true, + loop = loop + } + + local function playFrames() + repeat + if img.multiMonitor then + local width, height = img.multiMonitor.width, img.multiMonitor.height + local monitors = settings.get('sanjuuni.multimonitor') + + -- 如果需要校准显示器 + if not monitors or #monitors < height or #monitors[1] < width then + monitors = calibrateMonitors(width, height) + end + + -- 播放多显示器动画 + for i = 1, #img, width * height do + if not currentAnimation or not currentAnimation.running then break end + + -- 在所有显示器上绘制当前帧 + for y = 1, height do + for x = 1, width do + local frameIndex = i + (y-1) * width + x-1 + if frameIndex <= #img then + local monitor = peripheral.wrap(monitors[y][x]) + if monitor then + drawFrame(img[frameIndex], monitor) + end + end + end + end + + -- 等待帧间隔 + if img.animation then + sleep(img[i].duration or img.secondsPerFrame or 0.05) + else + break + end + end + else + -- 单显示器播放 + termObj.clear() + + -- 播放每一帧 + for _, frame in ipairs(img) do + if not currentAnimation or not currentAnimation.running then break end + + drawFrame(frame, termObj) + + -- 等待帧间隔 + if img.animation then + sleep(frame.duration or img.secondsPerFrame or 0.05) + else + break + end + end + end + + -- 如果不是循环播放,则退出 + if not currentAnimation or not currentAnimation.loop then + break + end + + until not currentAnimation or not currentAnimation.running + end + + -- 播放结束后的清理 + local function cleanup() + if currentAnimation and currentAnimation.running then + local termObj = currentAnimation.term + + -- 重置终端状态 + termObj.setBackgroundColor(colors.black) + termObj.setTextColor(colors.white) + termObj.clear() + termObj.setCursorPos(1, 1) + + -- 重置调色板 + for i = 0, 15 do + termObj.setPaletteColor(2^i, term.nativePaletteColor(2^i)) + end + end + + running = false + currentAnimation = nil + end + + -- 在新线程中播放 + parallel.waitForAny( + function() + local success, err = pcall(playFrames) + if not success then + printError("Error playing animation: " .. err) + end + cleanup() + end, + function() + while currentAnimation and currentAnimation.running do + os.pullEvent("bimg_stop") + currentAnimation.running = false + end + end + ) +end + +-- 停止播放 +function bimg.stop() + if currentAnimation then + currentAnimation.running = false + os.queueEvent("bimg_stop") + end + running = false +end + +-- 检查是否正在播放 +function bimg.isPlaying() + return running +end + +-- 获取当前播放状态 +function bimg.getStatus() + if not currentAnimation then + return { + playing = false, + looping = false, + frameCount = 0, + currentFrame = 0 + } + end + + return { + playing = currentAnimation.running, + looping = currentAnimation.loop, + frameCount = #currentAnimation.img, + currentFrame = currentAnimation.currentFrame or 0 + } +end + +return bimg \ No newline at end of file