Module:Halberstadt SVG: Difference between revisions
From Xenharmonic Reference
Tag: Undo |
mNo edit summary |
||
| (93 intermediate revisions by 2 users not shown) | |||
| Line 9: | Line 9: | ||
accidentalValues = {-1, 1} -- number of keys that each accidental alters the pitch by | accidentalValues = {-1, 1} -- number of keys that each accidental alters the pitch by | ||
nominalOverride = false -- whether to ignore note names with an accidental if they land on the same key as a nominal; if false, show both names generated | nominalOverride = false -- whether to ignore note names with an accidental if they land on the same key as a nominal; if false, show both names generated | ||
keyDimensions = {20, | keyDimensions = {1, 20, 120, 10, 20, 20, 60, 10} -- all measured in pixels: stroke width, white key width, white key height, white key font size, vertical offset of black keys, black key width, largest black key height, black key font size | ||
keyColors = {"fff", "000", "333", "fff", "666", "fff"} -- "white" key color, "white" key font color, "black" key color, "black" key font color, subsequent values may be used to define the color and font color of any "black" keys above the first row | keyColors = {"000", "fff", "000", "333", "fff", "666", "fff"} -- outline color, "white" key color, "white" key font color, "black" key color, "black" key font color, subsequent values may be used to define the color and font color of any "black" keys above the first row | ||
--]] | --]] | ||
local yn = require("Module:Yesno") | |||
local p = {} | local p = {} | ||
| Line 35: | Line 36: | ||
-- Musical parameters | -- Musical parameters | ||
local nominals = splitCSV(args.nominals) | local nominals = splitCSV(args.nominals) or {"F", "C", "G", "D", "A", "E", "B"} | ||
local period = tonumber(args.period) | local period = tonumber(args.period) or 12 | ||
local generator = tonumber(args.generator) | local generator = tonumber(args.generator) or 7 | ||
local startIndex = tonumber(args.startIndex) or | local startIndex = tonumber(args.startIndex) or 5 | ||
local duplicateFirstKey = args.duplicateFirstKey | local duplicateFirstKey = yn(args.duplicateFirstKey) or true | ||
local accidentals = splitCSV(args.accidentals) | local accidentals = splitCSV(args.accidentals) or {"b", "#"} | ||
local accidentalValues = {} | local accidentalValues = {} | ||
for _, v in ipairs(splitCSV(args.accidentalValues)) do | for _, v in ipairs(splitCSV(args.accidentalValues)) do | ||
table.insert(accidentalValues, tonumber(v)) | table.insert(accidentalValues, tonumber(v)) | ||
end | end | ||
local nominalOverride = yn(args.nominalOverride) or false | |||
local nominalOverride = args.nominalOverride | |||
-- Geometry | -- Geometry | ||
local dims = {} | local dims = {} | ||
for _, v in ipairs(splitCSV(args.keyDimensions or " | for _, v in ipairs(splitCSV(args.keyDimensions or "1,40,240,25,80,30,160,15")) do | ||
table.insert(dims, tonumber(v)) | table.insert(dims, tonumber(v)) | ||
end | end | ||
local | local strokeWidth, wWhite, hWhite, fsWhite, offsetBlack, wBlack, hBlack, fsBlack = | ||
dims[1], dims[2], dims[3], dims[4], dims[5], dims[6], dims[7] | dims[1], dims[2], dims[3], dims[4], dims[5], dims[6], dims[7], dims[8] | ||
-- Colors | -- Colors | ||
local colors = splitCSV(args.keyColors or "fff,000,333,fff,666,fff") | local colors = splitCSV(args.keyColors or "#000,#fff,#000,#333,#fff,#666,#fff") | ||
-- Compute pitch classes for nominals | -- Compute pitch classes for nominals | ||
| Line 64: | Line 64: | ||
for i, name in ipairs(nominals) do | for i, name in ipairs(nominals) do | ||
local idx = mod((i - startIndex) * generator, period) | local idx = mod((i - startIndex) * generator, period) | ||
local idxthEntry = {} | |||
table.insert(nominalKeys[idx] | table.insert(idxthEntry, name) | ||
nominalKeys[idx] = idxthEntry | |||
end | end | ||
| Line 97: | Line 98: | ||
table.sort(whiteKeys) | table.sort(whiteKeys) | ||
-- Map pitch class -> x-position | -- Map pitch class -> x-position | ||
local xPos = {} | local xPos = {} | ||
for i, pc in ipairs(whiteKeys) do | for i, pc in ipairs(whiteKeys) do | ||
xPos[pc] = (i - 1) * | xPos[pc] = (i - 1) * wWhite | ||
end | end | ||
local svgWidth = # | local svgWidth = (#nominals + (duplicateFirstKey and 1 or 0)) * wWhite | ||
local svgHeight = | local svgHeight = hWhite | ||
-- Collect black keys between whites | -- Collect black keys between whites | ||
| Line 115: | Line 112: | ||
if not nominalKeys[pc] then | if not nominalKeys[pc] then | ||
local leftWhite | local leftWhite | ||
for i = # | for i = #nominals, 1, -1 do | ||
if whiteKeys[i] < pc then | if whiteKeys[i] < pc then | ||
leftWhite = whiteKeys[i] | leftWhite = whiteKeys[i] | ||
| Line 136: | Line 133: | ||
table.insert(out, | table.insert(out, | ||
frame:extensionTag{ | frame:extensionTag{ | ||
name = | name = 'htmltag', | ||
args = { | args = { | ||
tagname = 'rect', | |||
x = x, | x = x, | ||
y = 0, | y = 0, | ||
width = | width = wWhite, | ||
height = | height = svgHeight, | ||
fill | style = 'fill:' .. colors[2] .. '; stroke:' .. colors[1] | ||
} | } | ||
} | } | ||
| Line 152: | Line 148: | ||
frame:extensionTag{ | frame:extensionTag{ | ||
name = 'htmltag', | name = 'htmltag', | ||
content = | content = labels[pc][1], | ||
args = { | args = { | ||
tagname = 'text', | tagname = 'text', | ||
x = x + | x = x + wWhite / 2, | ||
y = | y = svgHeight - fsWhite*3/2, | ||
[ | ['text-anchor'] = 'middle', | ||
[ | ['font-size'] = fsWhite, | ||
fill = | style = 'fill:' .. colors[3] | ||
} | |||
} | |||
) | |||
end | |||
if duplicateFirstKey then | |||
table.insert(out, | |||
frame:extensionTag{ | |||
name = 'htmltag', | |||
args = { | |||
tagname = 'rect', | |||
x = wWhite * #nominals, | |||
y = 0, | |||
width = wWhite, | |||
height = svgHeight, | |||
style = 'fill:' .. colors[2] .. '; stroke:' .. colors[1] | |||
} | |||
} | |||
) | |||
table.insert(out, | |||
frame:extensionTag{ | |||
name = 'htmltag', | |||
content = nominals[startIndex], | |||
args = { | |||
tagname = 'text', | |||
x = wWhite * #nominals + wWhite / 2, | |||
y = svgHeight - fsWhite*3/2, | |||
['text-anchor'] = 'middle', | |||
['font-size'] = fsWhite, | |||
style = 'fill:' .. colors[3] | |||
} | } | ||
} | } | ||
| Line 170: | Line 195: | ||
table.sort(pcs) | table.sort(pcs) | ||
for i, pc in ipairs(pcs) do | for i, pc in ipairs(pcs) do | ||
local x = xPos[leftWhite] + | local x = xPos[leftWhite] + wWhite - wBlack/2 | ||
local | local fill = colors[(i-1)*2 + 4] or colors[4] | ||
local textCol = colors[(i-1)*2 + 5] or colors[5] | |||
local textCol = colors[ | |||
table.insert(out, | table.insert(out, | ||
frame:extensionTag{ | frame:extensionTag{ | ||
| Line 181: | Line 204: | ||
tagname = 'rect', | tagname = 'rect', | ||
x = x, | x = x, | ||
y = | y = 0, | ||
width = | width = wBlack, | ||
height = | height = hBlack - (i - 1) * (offsetBlack + strokeWidth), | ||
fill | style = 'fill:' .. fill .. '; stroke:' .. colors[1] | ||
} | } | ||
} | } | ||
) | ) | ||
table.insert(out, | for j, label in ipairs(labels[pc]) do | ||
table.insert(out, | |||
frame:extensionTag{ | |||
name = 'htmltag', | |||
content = label, | |||
args = { | |||
tagname = 'text', | |||
x = x + wBlack/2, | |||
y = hBlack - (i - 1)*(offsetBlack + strokeWidth) - (j - 1)*20 - 20, | |||
['text-anchor'] = 'middle', | |||
['font-size'] = fsBlack, | |||
style = 'fill:' .. textCol | |||
} | |||
} | } | ||
) | |||
end | |||
end | end | ||
end | end | ||
| Line 210: | Line 234: | ||
args = { | args = { | ||
tagname = 'svg', | tagname = 'svg', | ||
xmlns = 'http:// | xmlns = 'http://wWhitew.w3.org/2000/svg', | ||
width = svgWidth, | width = svgWidth, | ||
height = svgHeight | height = svgHeight, | ||
style = 'border: 1px solid #eef' | |||
} | } | ||
} | } | ||
Latest revision as of 22:53, 8 February 2026
Documentation for this module may be created at Module:Halberstadt SVG/doc
--[[
MediaWiki Lua module to generate diagrams of microtonal Halberstadt keyboards using the html <svg> element. If there is more than one "black" key in between two "white" keys, stack the higher ones above the lower ones vertically. Arguments with a 19edo example:
nominals = {"F", "C", "G", "D", "A", "E", "B"} -- natural notes, the "white" keys of the keyboard
period = 19 -- number of keys total
generator = 11 -- number of keys from one nominal to the next, putting each on key number n*generator % period
startIndex = 5 -- index of the nominal on the leftmost key, in this case A
duplicateFirstKey = true -- whether to add an additional key at the right side of the keyboard with the same note as the first key
accidentals = {"b", "#"}
accidentalValues = {-1, 1} -- number of keys that each accidental alters the pitch by
nominalOverride = false -- whether to ignore note names with an accidental if they land on the same key as a nominal; if false, show both names generated
keyDimensions = {1, 20, 120, 10, 20, 20, 60, 10} -- all measured in pixels: stroke width, white key width, white key height, white key font size, vertical offset of black keys, black key width, largest black key height, black key font size
keyColors = {"000", "fff", "000", "333", "fff", "666", "fff"} -- outline color, "white" key color, "white" key font color, "black" key color, "black" key font color, subsequent values may be used to define the color and font color of any "black" keys above the first row
--]]
local yn = require("Module:Yesno")
local p = {}
-- Convert comma-separated values into Lua table, trim whitespace
local function splitCSV(str)
if not str or str == "" then return {} end
local t = {}
for s in mw.text.gsplit(str, "%s*,%s*") do
table.insert(t, s)
end
return t
end
-- Modulo that handles negative numbers correctly
local function mod(a, b)
return ((a % b) + b) % b
end
-- Core renderer
function p.render(frame)
local args = frame.args
-- Musical parameters
local nominals = splitCSV(args.nominals) or {"F", "C", "G", "D", "A", "E", "B"}
local period = tonumber(args.period) or 12
local generator = tonumber(args.generator) or 7
local startIndex = tonumber(args.startIndex) or 5
local duplicateFirstKey = yn(args.duplicateFirstKey) or true
local accidentals = splitCSV(args.accidentals) or {"b", "#"}
local accidentalValues = {}
for _, v in ipairs(splitCSV(args.accidentalValues)) do
table.insert(accidentalValues, tonumber(v))
end
local nominalOverride = yn(args.nominalOverride) or false
-- Geometry
local dims = {}
for _, v in ipairs(splitCSV(args.keyDimensions or "1,40,240,25,80,30,160,15")) do
table.insert(dims, tonumber(v))
end
local strokeWidth, wWhite, hWhite, fsWhite, offsetBlack, wBlack, hBlack, fsBlack =
dims[1], dims[2], dims[3], dims[4], dims[5], dims[6], dims[7], dims[8]
-- Colors
local colors = splitCSV(args.keyColors or "#000,#fff,#000,#333,#fff,#666,#fff")
-- Compute pitch classes for nominals
local nominalKeys = {}
for i, name in ipairs(nominals) do
local idx = mod((i - startIndex) * generator, period)
local idxthEntry = {}
table.insert(idxthEntry, name)
nominalKeys[idx] = idxthEntry
end
-- Generate all labels per pitch class
local labels = {}
for pc, names in pairs(nominalKeys) do
labels[pc] = labels[pc] or {}
for _, n in ipairs(names) do
table.insert(labels[pc], n)
end
end
for pc, names in pairs(nominalKeys) do
for i, acc in ipairs(accidentals) do
local delta = accidentalValues[i]
local target = mod(pc + delta, period)
if not nominalOverride or not nominalKeys[target] then
labels[target] = labels[target] or {}
for _, n in ipairs(names) do
table.insert(labels[target], n .. acc)
end
end
end
end
-- Determine white keys (nominals only)
local whiteKeys = {}
for pc in pairs(nominalKeys) do
table.insert(whiteKeys, pc)
end
table.sort(whiteKeys)
-- Map pitch class -> x-position
local xPos = {}
for i, pc in ipairs(whiteKeys) do
xPos[pc] = (i - 1) * wWhite
end
local svgWidth = (#nominals + (duplicateFirstKey and 1 or 0)) * wWhite
local svgHeight = hWhite
-- Collect black keys between whites
local blackKeys = {}
for pc, lbls in pairs(labels) do
if not nominalKeys[pc] then
local leftWhite
for i = #nominals, 1, -1 do
if whiteKeys[i] < pc then
leftWhite = whiteKeys[i]
break
end
end
if leftWhite then
blackKeys[leftWhite] = blackKeys[leftWhite] or {}
table.insert(blackKeys[leftWhite], pc)
end
end
end
-- SVG assembly
local out = {}
-- White keys
for _, pc in ipairs(whiteKeys) do
local x = xPos[pc]
table.insert(out,
frame:extensionTag{
name = 'htmltag',
args = {
tagname = 'rect',
x = x,
y = 0,
width = wWhite,
height = svgHeight,
style = 'fill:' .. colors[2] .. '; stroke:' .. colors[1]
}
}
)
if labels[pc] then
table.insert(out,
frame:extensionTag{
name = 'htmltag',
content = labels[pc][1],
args = {
tagname = 'text',
x = x + wWhite / 2,
y = svgHeight - fsWhite*3/2,
['text-anchor'] = 'middle',
['font-size'] = fsWhite,
style = 'fill:' .. colors[3]
}
}
)
end
if duplicateFirstKey then
table.insert(out,
frame:extensionTag{
name = 'htmltag',
args = {
tagname = 'rect',
x = wWhite * #nominals,
y = 0,
width = wWhite,
height = svgHeight,
style = 'fill:' .. colors[2] .. '; stroke:' .. colors[1]
}
}
)
table.insert(out,
frame:extensionTag{
name = 'htmltag',
content = nominals[startIndex],
args = {
tagname = 'text',
x = wWhite * #nominals + wWhite / 2,
y = svgHeight - fsWhite*3/2,
['text-anchor'] = 'middle',
['font-size'] = fsWhite,
style = 'fill:' .. colors[3]
}
}
)
end
end
-- Black keys (stacked)
for leftWhite, pcs in pairs(blackKeys) do
table.sort(pcs)
for i, pc in ipairs(pcs) do
local x = xPos[leftWhite] + wWhite - wBlack/2
local fill = colors[(i-1)*2 + 4] or colors[4]
local textCol = colors[(i-1)*2 + 5] or colors[5]
table.insert(out,
frame:extensionTag{
name = 'htmltag',
args = {
tagname = 'rect',
x = x,
y = 0,
width = wBlack,
height = hBlack - (i - 1) * (offsetBlack + strokeWidth),
style = 'fill:' .. fill .. '; stroke:' .. colors[1]
}
}
)
for j, label in ipairs(labels[pc]) do
table.insert(out,
frame:extensionTag{
name = 'htmltag',
content = label,
args = {
tagname = 'text',
x = x + wBlack/2,
y = hBlack - (i - 1)*(offsetBlack + strokeWidth) - (j - 1)*20 - 20,
['text-anchor'] = 'middle',
['font-size'] = fsBlack,
style = 'fill:' .. textCol
}
}
)
end
end
end
return frame:extensionTag{
name = 'htmltag',
content = table.concat(out),
args = {
tagname = 'svg',
xmlns = 'http://wWhitew.w3.org/2000/svg',
width = svgWidth,
height = svgHeight,
style = 'border: 1px solid #eef'
}
}
end
return p
