Module:Halberstadt SVG: Difference between revisions

From Xenharmonic Reference
m debug
Tag: Reverted
mNo edit summary
 
(73 intermediate revisions by the same user 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 = {1, 20, 80, 10, 40, 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, black key height, black key font size
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
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 1
local startIndex = tonumber(args.startIndex) or 5
local duplicateFirstKey = args.duplicateFirstKey == "yes"
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 = args.nominalOverride == "yes"
local nominalOverride = yn(args.nominalOverride) or false
-- Geometry
-- Geometry
local dims = {}
local dims = {}
for _, v in ipairs(splitCSV(args.keyDimensions or "1,40,120,25,80,40,120,15")) do
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
Line 63: 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)
nominalKeys[idx] = nominalKeys[idx] or {}
local idxthEntry = {}
table.insert(nominalKeys[idx], name)
table.insert(idxthEntry, name)
end
nominalKeys[idx] = idxthEntry
if duplicateFirstKey then
nominalKeys[period + 1] = nominalKeys[1]
end
end
Line 105: Line 104:
end
end
local svgWidth = #whiteKeys*wWhite
local svgWidth = (#nominals + (duplicateFirstKey and 1 or 0)) * wWhite
local svgHeight = hWhite + hBlack + offsetBlack
local svgHeight = hWhite
-- Collect black keys between whites
-- Collect black keys between whites
Line 113: Line 112:
if not nominalKeys[pc] then
if not nominalKeys[pc] then
local leftWhite
local leftWhite
for i = #whiteKeys, 1, -1 do
for i = #nominals, 1, -1 do
if whiteKeys[i] < pc then
if whiteKeys[i] < pc then
leftWhite = whiteKeys[i]
leftWhite = whiteKeys[i]
Line 138: Line 137:
tagname = 'rect',
tagname = 'rect',
x = x,
x = x,
y = svgHeight - hWhite,
y = 0,
width = wWhite,
width = wWhite,
height = hWhite,
height = svgHeight,
style = 'fill:' .. colors[2] .. '; stroke:' .. colors[1]
style = 'fill:' .. colors[2] .. '; stroke:' .. colors[1]
}
}
Line 149: Line 148:
frame:extensionTag{
frame:extensionTag{
name = 'htmltag',
name = 'htmltag',
content = #whiteKeys,
content = labels[pc][1],
args = {
args = {
tagname = 'text',
tagname = 'text',
x = x + wWhite / 2,
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,
y = svgHeight - fsWhite*3/2,
['text-anchor'] = 'middle',
['text-anchor'] = 'middle',
Line 168: Line 196:
for i, pc in ipairs(pcs) do
for i, pc in ipairs(pcs) do
local x = xPos[leftWhite] + wWhite - wBlack/2
local x = xPos[leftWhite] + wWhite - wBlack/2
local y = svgHeight - hWhite - i*(offsetBlack+strokeWidth)
local fill = colors[(i-1)*2 + 4] or colors[4]
local fill = colors[(i-1)*2 + 4] or colors[4]
local textCol = colors[(i-1)*2 + 5] or colors[5]
local textCol = colors[(i-1)*2 + 5] or colors[5]
Line 177: Line 204:
tagname = 'rect',
tagname = 'rect',
x = x,
x = x,
y = y,
y = 0,
width = wBlack,
width = wBlack,
height = hBlack,
height = hBlack - (i - 1) * (offsetBlack + strokeWidth),
style = 'fill:' .. fill .. '; stroke:' .. colors[1]
style = 'fill:' .. fill .. '; stroke:' .. colors[1]
}
}
}
}
)
)
table.insert(out,
for j, label in ipairs(labels[pc]) do
frame:extensionTag{
table.insert(out,
name = 'htmltag',
frame:extensionTag{
content = table.concat(labels[pc], "/"),
name = 'htmltag',
args = {
content = label,
tagname = 'text',
args = {
x = x + wBlack/2,
tagname = 'text',
y = y + hBlack - fsBlack*3/2,
x = x + wBlack/2,
['text-anchor'] = 'middle',
y = hBlack - (i - 1)*(offsetBlack + strokeWidth) - (j - 1)*20 - 20,
['font-size'] = fsBlack,
['text-anchor'] = 'middle',
style = 'fill:' .. textCol
['font-size'] = fsBlack,
style = 'fill:' .. textCol
}
}
}
}
)
)
end
end
end
end
end

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