Moduuli:Kitarakirja/Otekaavio

Wikikirjastosta

Tämän moduulin ohjeistuksen voi tehdä sivulle Moduuli:Kitarakirja/Otekaavio/ohje

--- Moduuli kitaran otekaavioiden piirtämiseen. Generoi timeline-lisäosan avulla kuvan.
local p = {}

local tekstipohja = require "Moduuli:Tekstipohja"

local timeline_template = [=[

Define $width = {{{Width}}}
Define $height = 176
Define $start = 0
Define $end = {{{TableHeight}}}
Define $first = 21
Define $last = {{{LastString}}}

ImageSize = width:$width height:$height
PlotArea  = right:{{{RightMargin}}} left:5 bottom:0 top:5
Period    = from:$start till:$end
TimeAxis  = orientation:vertical
AlignBars = justify

Colors =
     id:bg              value:white
     id:string          value:black
     id:fret            value:gray(0.7)
     id:text            value:black
     id:fing1           value:{{{Fing1Color}}}
     id:fing2           value:{{{Fing2Color}}}
     id:fing3           value:{{{Fing3Color}}}
     id:fing4           value:{{{Fing4Color}}}
     id:fingp           value:{{{FingPColor}}}
     {{{Colors
     }}}

BackgroundColors = canvas:bg

PlotData=

  {{{Strings
  }}}

  bar:o color:bg from:start till:7 width:12
      {{{FretPos}}}

LineData =

   layer:back color:fret width:1 frompos:$first tillpos:$last
      {{{Frets
      }}}

   {{{Nut}}}

   layer:front
   {{{Barres
   }}}
   
   # {{{DUMMY}}}
]=]




--- Kieltä esittävien viivojen kohdat. Käytetään barrejen piirtämiseen.
--local stringpos = { 26, 54, 82, 109, 137, 165 }
local stringpos = { 20, 40, 60, 80, 100, 120 }


--- Timeline-koodin riviä kuvaava olio. Rivin tiedot asetetaan taulukkoon, esim. ["at"] = 0.
--  tostring palauttaa rivin tekstinä.
local function new_timeline_object(tab)
    tab = tab or {}
    
    setmetatable(tab, {
                     __tostring = function (tab)
                         local out = {}
                         for k,v in pairs(tab) do
                             if k ~= "text" then
                                 table.insert(out, k .. ":" .. v)
                             end
                         end

                         -- text-parametri jätetään viimeiseksi, että tyhjät merkkijonot toimii
                         if tab.text then
                             table.insert(out, "text:" .. tab.text)
                         end
                         
                         return table.concat(out, " ")
                     end
    })
    return tab
end

--- Alustaa annetunkokoisen taulukon annetulla arvolla.
--
-- @param size: taulukon koko
-- @param init: alkioihin alustettava arvo
local function make_array(size, init)
    local arr = {}

    for i = 1, size do
        arr[i] = init
    end

    return arr
end



--------------------------------------------
-- Timeline-merkkausta tuottavat funktot. --
--------------------------------------------

--- Sormitusnumeron merkkaus
--
-- @param number: sormen numero, 1–4 ja "p"
local function get_fingering_markup(number)
    if not number then
        return ""
    end
    
    local markup = new_timeline_object{
        ["at"]        = 0.5,
        ["text"]      = number,
        ["fontsize"]  = 10,
        ["shift"]     = "(10,0)",
        ["textcolor"] = "text",
        ["align"]     = "center"
    }

    return tostring(markup)
end

--- Avoimen kielen merkkaus.
--
-- @param text: joko "X", "O", "(X)" tai "(O)"
local function get_open_string_markup(text)
    if not text then
        return ""
    end
    
    local markup = new_timeline_object{
        ["at"]        = 6.5,
        ["text"]      = text,
        ["fontsize"]  = 12,
        ["shift"]     = "(10,8)",
        ["textcolor"] = "text",
        ["align"]     = "center"
    }

    return tostring(markup)
end

local I = 3
local V = 9
local X = 9
local fret_marks = {
    [1] = { text = "",      width = 0 },
    [2] = { text = "II",    width = 0+I+I },
    [3] = { text = "III",   width = 0+I+I+I },
    [4] = { text = "IV",    width = 0+I+V },
    [5] = { text = "V",     width = 0+V },
    [6] = { text = "VI",    width = 0+V+I },
    [7] = { text = "VII",   width = 0+V+I+I },
    [8] = { text = "VIII",  width = 0+V+I+I+I },
    [9] = { text = "IX",    width = 0+I+X },
    [10] = { text = "X",     width = 0+X },
    [11] = { text = "XI",    width = 0+X+I },
    [12] = { text = "XII",   width = 0+X+I+I },
    [13] = { text = "XIII",  width = 0+X+I+I+I },
    [14] = { text = "XIV",   width = 0+X+I+V },
    [15] = { text = "XV",    width = 0+X+V },
    [16] = { text = "XVI",   width = 0+X+V+I },
    [17] = { text = "XVII",  width = 0+X+V+I+I },
    [18] = { text = "XVIII", width = 0+X+V+I+I+I },
    [19] = { text = "XIX",   width = 0+X+I+X },
    [20] = { text = "XX",    width = 0+X+X },
    [21] = { text = "XXI",   width = 0+X+X+I },
    [22] = { text = "XXII",  width = 0+X+X+I+I },
    [23] = { text = "XXIII", width = 0+X+X+I+I+I },
    [24] = { text = "XXIV",  width = 0+X+X+I+V },
}

--- Nauhavälin numeron tulostus
local function get_fret_pos_markup(fret)
    if not fret or fret == 1 then
        return ""
    end

    assert(fret >= 1 and fret < 25, "Nauhavälin pitää olla väliltä 1–24")
    
    local markup = new_timeline_object{
        ["at"]        = 5.5,
        ["text"]      = fret_marks[fret].text,
        ["fontsize"]  = "M",
        ["shift"]     = "(23,8)",
        ["textcolor"] = "string",
    }

    return tostring(markup)
end


--- Palauttaa nauhanumerosta johtuvan leveyslisän.
local function get_right_margin(fret)
    assert(fret >= 1 and fret < 25, "Nauhavälin pitää olla väliltä 1–24")

    return fret_marks[fret].width
end


--- Painetun kielen merkkaus.
--
-- @param pos:          väli, johon merkintä tulee (suhteellinen; 1 = ensimmäinen väli)
-- @param finger_color: sormen väri, jota käytetään (esim. "fing1" tai "fingp")
-- @param string_color: (valinnainen) kielen väri (esim. "str1"), jos annettu tekee sormiympyrän
--                      sisälle pienen tämän värisen ympyrän
local function get_pressed_string_markup(conf, pos, finger, string_color)
    if pos < 1 then
        return ""
    end

    local fing_color
    if finger == "1" or finger == "2" or finger == "3" or finger == "4" or finger == "P" then
        fing_color = "fing" .. finger
    else
        fing_color = "fing1"
    end
    

    local mpos = tostring(7 - pos)
    local markup = new_timeline_object{
        ["at"]       = mpos - 0.5,
        ["text"]     = "&#x2022;", -- BULLET
        ["textcolor"] = fing_color,
        ["fontsize"] = 44,
        --["shift"]    = "(0,-12)" -- 190
        --["shift"]    = "(0,-8)" -- 120
        ["shift"]    = "(0,-9)" -- 140
    }

    --assert ( finger and finger ~= "", "Sormi puuttuu" )    
    
    if not string_color then
        return tostring(markup)
    end

    -- Tehdään merkki kahdesta päällekkäisestä bulletista, joista päällimmäinen on tyhjä keskeltä.
    local markup2 = new_timeline_object{
        ["at"]       = mpos - 0.5,
        ["text"]     = "&#x25E6;", -- WHITE BULLET
        ["textcolor"] = fing_color,
        ["fontsize"] = markup.fontsize,
        ["shift"]    = "(0,-8)"
    }
    
    markup["textcolor"] = string_color

    return tostring(markup) .. "\n" .. tostring(markup2)
end


--- Barre-otetta kuvaava viiva.
-- @param pos:      väli, johon viiva piirretään
-- @param finger:   sormen tunnus (1–4, p)
-- @param string_b: ensimmäinen kieli
-- @param string_e: jälkimmäinen kieli
local function get_barre_bar_markup(symbol, string, fret, finger)
    local mpos = tostring(7 - fret) .. ".5"
    local markup = new_timeline_object{
        ["at"]      = mpos - 0.5,
        ["width"]   = 10
    }

    if finger == "1" or finger == "2" or finger == "3" or finger == "4" or finger == "P" then
        markup.color = "fing" .. finger
    else
        markup.color = "fing1"
    end

    if symbol == "C" then
        markup.frompos = stringpos[string]
        markup.tillpos = stringpos[string] + 11
    elseif symbol == "D" then
        markup.frompos = stringpos[string] - 11
        markup.tillpos = stringpos[string]
    elseif symbol == "-" or symbol == "H" then
        markup.frompos = stringpos[string] - 11
        markup.tillpos = stringpos[string] + 11
    else
        error ( "Virheellinen tyyppi: " .. symbol )
    end 

    --assert ( finger and finger ~= "", "Sormi puuttuu" )
    
    return tostring(markup)
end

--- Barre-otteita kuvaavat viivat.
-- @param barres:      väli, johon viiva piirretään
local function get_barres_markup(barres)
    local out = {}

    for i, barre in ipairs(barres) do
        table.insert(out, get_barre_bar_markup(barre.type, barre.string, barre.fret, barre.finger))
    end

    return table.concat(out, "\n")
end


--- Kielten välin merkkaus
-- 
-- @param index: seuraavan kielen numero
local function get_string_space_markup(index)
    local markup = new_timeline_object{
        ["bar"]        = "space" .. index,
        ["width"]      = 5,
    }

    return tostring(markup)
end

--- Yksittäisen kielen merkkaus
-- 
-- @param index: kielen numero
local function get_string_markup(index, open_pos, pressed_pos, finger, string_color)

    local markup = new_timeline_object{
        ["bar"]   = "string" .. index,
        ["from"]  = 1.5,
        ["till"]  = 6.5,
        ["color"] = "black",
        ["width"] = 1,
    }
    
    return tostring(markup)
end

--- Kielten merkkaus
--
-- Barret piirretään erikseen, koska ne tulevat eri paikkaan koodia.
-- 
-- @param n_strings:     kielten määrä
-- @param open_poss:     avointen kielten tekstit
-- @param pressed_poss:  painettujen kielten nauhanumerot
-- @param fingering:     kielten sormitukset
-- @param string_colors: mahdolliset kielikohtaisten väriläikkien värit
local function get_strings_markup(conf, open_poss, pressed_poss, fingering, string_colors)
    local out = {}
    
    for i = 1, conf.n_strings do
        table.insert(out, get_string_space_markup(i))
        table.insert(out, "")
        table.insert(out, get_string_markup(i))
        table.insert(out, get_open_string_markup(open_poss[i]))
        table.insert(out, get_pressed_string_markup(conf, pressed_poss[i], fingering[i], string_colors[i] and ("str" .. i)))
        table.insert(out, get_fingering_markup(fingering[i]))
    end

    return table.concat(out, "\n")
end



--- Satulan piirto
-- 
-- @param fret_pos: jos 1, piirretään satula, muuten ei
local function get_nut_markup(conf, fret_pos)
    if fret_pos == 1 then
        local markup = new_timeline_object{
            ["at"]      = 6.6,
            ["frompos"] = 20,
            ["tillpos"] = conf.min_width - 20,
            ["color"]   = "black",
            ["width"]   = 4,
            ["layer"]   = "back",           
        }

        return tostring(markup)
    end

    return ""
end

--- Nauhojen piirto
local function get_fret_markup(conf)
    return "at:" .. table.concat({ 6.5, 5.5, 4.5, 3.5, 2.5, 1.5 }, "\nat:")
end

--- Väriä kuvaavan rivin.
-- @param id:     värin nimi
-- @param color:  väri
local function get_color_markup(id, value)
    local markup = new_timeline_object{
        ["id"]      = id,
        ["value"]   = value
    }
    
    return tostring(markup)
end


local function get_colors_markup(colors)
    local out = {}

    for i, value in pairs(colors) do
        table.insert(out, get_color_markup("str" .. i, value))
    end

    return table.concat(out, "\n")
end


--- Tekee otekaavion annetuista parametreista.
--
-- Parametrit `string_colors`, `pos0`, `pos` ja `fingering` ovat taulukoita, joissa on jokaiselle kielelle alkio.
-- @param conf:          konfiguraatio
-- @param fing_colors:   (valinnainen) sormien värit, taulukko { [1]–[4], ["p"] }
-- @param string_colors: (valinnainen) kielikohtaiset lisävärit
-- @param fret_pos:      ensimmäisen piirrettävän nauhan nauhaväli (merkitään marginaaliin jos > 1)
-- @param pos0:          avoimia kieliä kuvaavat tekstit (esim. "X", "O")
-- @param pos:           välit, joista kieliä painetaan (esim. 3, suhteellinen `fret_pos` -parametriin nähden)
-- @param fingering:     sormitukset
-- @return:              timeline-elementin teksti
function p.chord_diagram(args)
    local conf          = args.conf
    local fing_colors   = args.fing_colors or {}
    local fret_pos      = args.fret_pos
    local open_poss     = args.pos0s
    local pressed_poss  = args.poss
    local fingering     = args.fingering
    local string_colors = args.string_colors or {}
    local barres        = args.barres
    local dummy         = args.dummy


    local text = tekstipohja.korvaaMuuttujat(timeline_template, {
                                                 ["TableHeight"] = tostring(conf.cells_vertically),
                                                 ["Nut"]     = get_nut_markup(conf, fret_pos),
                                                 ["Frets"]   = get_fret_markup(conf),
                                                 ["FretPos"] = get_fret_pos_markup(fret_pos),
                                                 ["Colors"]  = get_colors_markup(string_colors),
                                                 ["Barres"]  = get_barres_markup(barres),
                                                 
                                                 ["Fing1Color"] = fing_colors[1] or "black",
                                                 ["Fing2Color"] = fing_colors[2] or "black",
                                                 ["Fing3Color"] = fing_colors[3] or "black",
                                                 ["Fing4Color"] = fing_colors[4] or "black",
                                                 ["FingPColor"] = fing_colors.p  or "black",

                                                 ["Strings"] = get_strings_markup(conf,
                                                                                  open_poss,
                                                                                  pressed_poss,
                                                                                  fingering,
                                                                                  string_colors),

                                                 ["RightMargin"] = tostring(5 + get_right_margin(fret_pos)),

                                                 ["Width"] = tostring(conf.min_width + get_right_margin(fret_pos)),
                                                 ["LastString"] = tostring(conf.min_width - 20),
                                                 ["DUMMY"]      = string.rep("X", tonumber(dummy or "0"))

    }) 
    return text
end


----------------------------------------
-- Syöteparametreja lukevat funktiot. --
----------------------------------------

local function read_finger_colors(args)
    local colors = {}

    if args.sormi1 then
        colors[1] = args.sormi1
    end

    if args.sormi2 then
        colors[2] = args.sormi2
    end

    if args.sormi3 then
        colors[3] = args.sormi3
    end

    if args.sormi4 then
        colors[4] = args.sormi4
    end

    if args.sormiP then
        colors.p = args.sormiP
    end

    return colors
end

local function read_string_colors(conf, args)
    local colors = {}

    for i = 1, conf.n_strings do
        if args["kieli" .. i] then
            colors[i] = args["kieli" .. i]
        end
    end
    
    return colors
end


--- Lukee vapaat kielet argumenteista ja palauttaa taulukon niiden teksteistä.
local function read_open_strings(conf, args)
    local pos0_text = make_array(conf.n_strings, "")
    local s_idx = 0
    local cur
    
    for i = 1, conf.n_strings do
        -- Poistetaan tyhjät alusta ja lopusta.
        cur = args[i]:gsub("^%s*(.-)%s*$", "%1")
        
        -- Kielen numero.
        s_idx = ((i - 1) % conf.n_strings) + 1
        
        if i >= 1 and i < 7 then
            if cur == "o" then
                pos0_text[s_idx] = "O"
            elseif cur == "x" then
                pos0_text[s_idx] = "X"
            else
                pos0_text[s_idx] = cur
            end
        end
    end

    return pos0_text
end

--- Lukee ei-vapaat kielet ja palauttaa niiden suurimmat arvot taulukkona.
local function read_fret_positions(conf, args, fingering)
    local pos = make_array(conf.n_strings, 0)
    local s_idx = 0
    local f_idx = 0
    local cur
    local bars = {}     -- Luettelo piirrettävistä barreista
    local from = conf.n_strings + 1
    local till = conf.n_strings * (1 + 5) -- avoimet kielet + 5 nauhaväliä
    
    for i = from, till do    

        -- Poistetaan tyhjät alusta ja lopusta.
        cur = args[i]:gsub("^%s*(.-)%s*$", "%1")
        
        -- Kielen ja nauhan numero.
        s_idx = ((i - 1) % conf.n_strings) + 1
        if s_idx == 1 then
            f_idx = f_idx + 1
        end
        
        if cur == "o" then
            pos[s_idx] = f_idx
        elseif cur == "C" then
            pos[s_idx] = f_idx
            table.insert(bars, { type = "C", string = s_idx, fret = f_idx, finger = fingering[s_idx] })
        elseif cur == "D" or cur == "H" then
            pos[s_idx] = f_idx
            assert ( (#bars == 0 or bars[#bars].fret == f_idx) and s_idx > 1, "Barresta puuttuu alkumerkki" )
            table.insert(bars, { type = cur, string = s_idx, fret = f_idx, finger = bars[#bars].finger })
        elseif cur == "-" then
            assert ( (#bars == 0 or bars[#bars].fret == f_idx) and s_idx > 1, "Barresta puuttuu alkumerkki" )
            table.insert(bars, { type = "-", string = s_idx, fret = f_idx, finger = bars[#bars].finger })
        end

    end
    
    return pos, bars
end


--- Lukee vapaat kielet argumenteista ja palauttaa taulukon niiden teksteistä.
local function read_fingering(conf, args)
    local fingering = make_array(conf.n_strings, 0)
    local s_idx = 0
    local cur
    local from = conf.n_strings * (1 + 5) + 1
    local till = from + conf.n_strings - 1
    
    for i = from, till do
        -- Poistetaan tyhjät alusta ja lopusta.
        cur = args[i]:gsub("^%s*(.-)%s*$", "%1")

        -- Kielen numero.
        s_idx = ((i - 1) % conf.n_strings) + 1

        fingering[s_idx] = cur
    end

    return fingering
end

function p.Otekaavio(frame)
    local n_strings = frame.args["kieliä"] or 6
    local conf = {
        n_strings = n_strings,             -- kielten määrä
        min_width = (n_strings + 1) * 20,  -- kaavion leveys pikseleinä; tähän lisätään mahdollisen nauhanumeron leveys
        cells_vertically = 8,              -- kaavion korkeus soluina eli nauhaväleinä
    }

    local fingering = read_fingering(conf, frame.args)
    local poss, bars = read_fret_positions(conf, frame.args, fingering)

    local text = p.chord_diagram{
        conf          = conf,
        fret_pos      = (tonumber(frame.args.nauhanumero) or 1),
        pos0s         = read_open_strings(conf, frame.args),
        poss          = poss,
        fingering     = fingering,
        fing_colors   = read_finger_colors(frame.args),
        string_colors = read_string_colors(conf, frame.args),
        barres        = bars,
        dummy         = frame.args.dummy
    }
    
    if frame.args.nowiki then
        return frame:extensionTag{ name = "pre", content = text }
    else
        return frame:extensionTag{ name = "timeline", content = text }
    end
end

return p