Initial commit. Basic drawing of non-moving dancers implemented.

This commit is contained in:
Daniel Perelman 2023-06-10 16:37:10 -07:00
commit 6fb6000bd4
18 changed files with 4538 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
www/js/*.js

3
external/README.md vendored Normal file
View File

@ -0,0 +1,3 @@
libfigure from
https://github.com/contradb/contra/tree/master/app/javascript/libfigure
is licensed under AGPLv3.0+

21
external/libfigure/after-figure.js vendored Normal file
View File

@ -0,0 +1,21 @@
// _ _____ _____ _____ ____ _____ ___ ____ _ _ ____ _____ _
// / \ | ___|_ _| ____| _ \ | ___|_ _/ ___| | | | _ \| ____| (_)___
// / _ \ | |_ | | | _| | |_) |____| |_ | | | _| | | | |_) | _| | / __|
// / ___ \| _| | | | |___| _ <_____| _| | | |_| | |_| | _ <| |___ _ | \__ \
// /_/ \_\_| |_| |_____|_| \_\ |_| |___\____|\___/|_| \_\_____(_)/ |___/
// |__/
import { moves, parameters, defineRelatedMove2Way } from "./define-figure"
import { param } from "./param"
// all moves with a balance are related to the move 'balance'
moves().forEach(function(move) {
parameters(move).forEach(function(the_param) {
if (
the_param.name === param("balance_true").name ||
the_param.name === param("balance_false").name
) {
defineRelatedMove2Way(move, "balance")
}
})
})

205
external/libfigure/chooser.js vendored Normal file
View File

@ -0,0 +1,205 @@
// ____ _ _ ___ ___ ____ _____ ____
// / ___| | | |/ _ \ / _ \/ ___|| ____| _ \
// | | | |_| | | | | | | \___ \| _| | |_) |
// | |___| _ | |_| | |_| |___) | |___| _ <
// \____|_| |_|\___/ \___/|____/|_____|_| \_\
//
//
// Choosers are UI elements without semantic ties to other choosers,
// who are given semantic ties by figures.
// So a figure could have two chooser_booleans
// or two chooser_dancers (e.g. GENTS roll away the NEIGHBORS)
// Choosers are obtained by calling, for example, `chooser('chooser_boolean')`
// You can read back a chooser's name property:
// `chooser('chooser_boolean').name => 'chooser_boolean'`
// Choosers can be compared with ===
var defined_choosers = {}
function defineChooser(name) {
"string" == typeof name || throw_up("first argument isn't a string")
"chooser_" == name.slice(0, 8) ||
throw_up("first argument doesn't begin with 'chooser_'")
defined_choosers[name] = { name: name }
}
export const chooser = name => {
const chooser = defined_choosers[name]
if (chooser) {
return chooser
} else {
throw_up(JSON.stringify(name) + " is not the name of a chooser")
}
}
defineChooser("chooser_boolean")
defineChooser("chooser_beats")
defineChooser("chooser_spin")
defineChooser("chooser_left_right_spin")
defineChooser("chooser_right_left_hand")
defineChooser("chooser_right_left_shoulder")
defineChooser("chooser_revolutions")
defineChooser("chooser_places")
defineChooser("chooser_text")
defineChooser("chooser_star_grip")
defineChooser("chooser_march_facing")
defineChooser("chooser_slide")
defineChooser("chooser_set_direction")
defineChooser("chooser_set_direction_acrossish")
defineChooser("chooser_set_direction_grid")
defineChooser("chooser_set_direction_figure_8")
defineChooser("chooser_gate_direction")
defineChooser("chooser_slice_return")
defineChooser("chooser_slice_increment")
defineChooser("chooser_down_the_hall_ender")
defineChooser("chooser_all_or_center_or_outsides")
defineChooser("chooser_zig_zag_ender")
defineChooser("chooser_go_back")
defineChooser("chooser_give")
defineChooser("chooser_half_or_full")
defineChooser("chooser_swing_prefix")
defineChooser("chooser_hey_length")
var _dancerMenuForChooser = {}
function defineDancerChooser(name, dancers) {
defineChooser(name)
_dancerMenuForChooser[name] = dancers
}
export const dancerMenuForChooser = function(chooser) {
// chooser object
return _dancerMenuForChooser[chooser.name]
}
export const dancerCategoryMenuForChooser = function(chooser) {
return libfigureUniq(dancerMenuForChooser(chooser).map(dancersCategory))
}
export const dancerChooserNames = function() {
return Object.keys(_dancerMenuForChooser)
}
var outOfSetDancers = [
"shadows",
"2nd shadows",
"prev neighbors",
"next neighbors",
"3rd neighbors",
"4th neighbors",
]
defineDancerChooser(
"chooser_dancers", // some collection of dancers
[
"everyone",
"gentlespoon",
"gentlespoons",
"ladle",
"ladles",
"partners",
"neighbors",
"ones",
"twos",
"same roles",
"first corners",
"second corners",
"first gentlespoon",
"first ladle",
"second gentlespoon",
"second ladle",
].concat(outOfSetDancers)
)
defineDancerChooser(
"chooser_pair", // 1 pair of dancers
["gentlespoons", "ladles", "ones", "twos", "first corners", "second corners"]
)
defineDancerChooser(
"chooser_pair_or_everyone", // 1 pair or everyone
["everyone"].concat(dancerMenuForChooser(chooser("chooser_pair")))
)
defineDancerChooser(
"chooser_pairc_or_everyone", // 1 pair or centers or everyone
[
"everyone",
"gentlespoons",
"ladles",
"centers",
"ones",
"twos",
// intentionally omitting 'first corners' and 'second corners', because 'centers' is clearer
]
)
defineDancerChooser(
"chooser_pairz", // 1-2 pairs of dancers
[
"gentlespoons",
"ladles",
"partners",
"neighbors",
"ones",
"twos",
"same roles",
"first corners",
"second corners",
].concat(outOfSetDancers)
)
defineDancerChooser(
"chooser_pairz_or_unspecified",
[""].concat(dancerMenuForChooser(chooser("chooser_pairz")))
)
defineDancerChooser(
"chooser_pairs", // 2 pairs of dancers
["partners", "neighbors", "same roles"].concat(outOfSetDancers)
)
defineDancerChooser(
"chooser_pairs_or_ones_or_twos",
["partners", "neighbors", "same roles", "ones", "twos"].concat(
outOfSetDancers
)
)
defineDancerChooser(
"chooser_pairs_or_everyone",
["everyone"].concat(dancerMenuForChooser(chooser("chooser_pairs")))
)
defineDancerChooser(
"chooser_dancer", // one dancer
["first gentlespoon", "first ladle", "second gentlespoon", "second ladle"]
)
defineDancerChooser(
"chooser_role", // ladles or gentlespoons
["gentlespoons", "ladles"]
)
defineDancerChooser(
"chooser_hetero", // partners or neighbors or shadows but not same-role
["partners", "neighbors"].concat(outOfSetDancers)
)
// nb: this hash is also accessed from ruby.
var dancersCategoryHash = {
// '1st shadows': 'shadows', // not sure if this needs to be included or not - for now: no
"2nd shadows": "shadows",
"prev neighbors": "neighbors",
"next neighbors": "neighbors",
// '2nd neighbors': 'neighbors', // not sure if this needs to be included or not - for now: no
"3rd neighbors": "neighbors",
"4th neighbors": "neighbors",
}
function dancersCategory(chooser) {
return dancersCategoryHash[chooser] || chooser
}
export const dancers = function() {
return dancerMenuForChooser(chooser("chooser_dancers"))
}
var heyLengthMenu = (function() {
var pairz = dancerMenuForChooser(chooser("chooser_pairz"))
var acc = ["full", "half"]
for (var i = 0; i < pairz.length; i++) {
acc.push(pairz[i] + "%%1")
acc.push(pairz[i] + "%%2")
}
return acc
})()

46
external/libfigure/dance.js vendored Normal file
View File

@ -0,0 +1,46 @@
var invertPairHash = {
ladles: "gentlespoons",
gentlespoons: "ladles",
ones: "twos",
twos: "ones",
"first corners": "second corners",
"second corners": "first corners",
"*": "*",
}
// If this names 2 dancers, this returns the names for the other 2 dancers
// it's sketchy, because it assumes 4 dancers, so only use it in contra moves.
// Formerly known as dancers complement.
function invertPair(whostr, dialect) {
if (!whostr) {
return "others"
} // undefined is ok
var dancers = invertPairHash[whostr]
if (dancers) {
return dancerSubstitution(dancers, dialect)
} else {
throw_up("bogus parameter to invertPairHash: " + whostr)
}
}
function labelForBeats(beats) {
if (beats % 16 == 0)
switch (beats / 16) {
case 0:
return "A1"
case 1:
return "A2"
case 2:
return "B1"
case 3:
return "B2"
case 4:
return "C1"
case 5:
return "C2"
case 6:
return "D1"
case 7:
return "D2"
}
return ""
}

583
external/libfigure/define-figure.js vendored Normal file
View File

@ -0,0 +1,583 @@
// ____ _____ _____ ___ _ _ _____ _____ ___ ____ _ _ ____ _____
// | _ \| ____| ___|_ _| \ | | ____| | ___|_ _/ ___| | | | _ \| ____|
// | | | | _| | |_ | || \| | _| _____| |_ | | | _| | | | |_) | _|
// | |_| | |___| _| | || |\ | |__|_____| _| | | |_| | |_| | _ <| |___
// |____/|_____|_| |___|_| \_|_____| |_| |___\____|\___/|_| \_\_____|
//
// language construct for defining dance moves
// and related support functions for dealing with figures
import { libfigureObjectCopy, throw_up } from "./util"
// always freshly allocated
function newFigure(optional_progression) {
var m = { move: "stand still", parameter_values: [8] }
if (optional_progression) {
m.progression = 1
}
return m
}
function figureBeats(f) {
var defaultBeats = 8
if (!f.move) return defaultBeats
var idx = find_parameter_index_by_name("beats", parameters(f.move))
return idx < 0 ? defaultBeats : f.parameter_values[idx]
}
function sumBeats(figures, optional_limit) {
var acc = 0
var n = Number.isInteger(optional_limit) ? optional_limit : figures.length
for (var i = 0; i < n; i++) {
acc += figureBeats(figures[i])
}
return acc
}
export const figureToHtml = (f, dialect) => {
return figureFlatten(f, dialect, FLATTEN_FORMAT_HTML)
}
function figureToUnsafeText(f, dialect) {
return figureFlatten(f, dialect, FLATTEN_FORMAT_UNSAFE_TEXT)
}
function figureToSafeText(f, dialect) {
return figureFlatten(f, dialect, FLATTEN_FORMAT_SAFE_TEXT)
}
function figureFlatten(f, dialect, flatten_format) {
var fig_def = defined_events[f.move]
if (fig_def) {
var func = fig_def.props.words || figureGenericWords
var main = func(alias(f), f.parameter_values, dialect)
var note = f.note
var pilcrow = f.progression ? "⁋" : false
if (note && note.trim()) {
var fancy_note = lingoLineWords(stringInDialect(note, dialect), dialect)
return words(main, fancy_note, pilcrow).flatten(flatten_format)
} else {
return words(main, pilcrow).flatten(flatten_format)
}
} else if (f.move) {
return "undefined figure '" + words(f.move).flatten(flatten_format) + "'!"
} else {
return "empty figure"
}
}
// Called if they don't specify a Words function in the figure definition:
function figureGenericWords(move, parameter_values, dialect) {
var ps = parameters(move)
var pwords = parameter_words(move, parameter_values, dialect)
var acc = []
var subject_index = find_parameter_index_by_name("who", ps)
var balance_index = find_parameter_index_by_name("bal", ps)
var beats_index = find_parameter_index_by_name("beats", ps)
if (subject_index >= 0) {
acc.push(pwords[subject_index])
}
if (balance_index >= 0) {
acc.push(pwords[balance_index])
}
acc.push(moveSubstitution(move, dialect))
ps.length == parameter_values.length ||
throw_up(
"parameter type mismatch. " +
ps.length +
" formals and " +
parameter_values.length +
" values"
)
for (var i = 0; i < parameter_values.length; i++) {
if (i != subject_index && i != balance_index && i != beats_index) {
acc.push(pwords[i])
}
}
return new Words(acc)
}
function find_parameter_index_by_name(name, parameters) {
var match_name_fn = function(p) {
return p.name === name
}
return parameters.findIndex(match_name_fn, parameters)
}
// ================
function parameter_strings(move, parameter_values, dialect) {
return parameter_strings_or_words(move, parameter_values, dialect, false)
}
function parameter_words(move, parameter_values, dialect) {
return parameter_strings_or_words(move, parameter_values, dialect, true)
}
function parameter_strings_or_words(move, parameter_values, dialect, words_ok) {
var formal_parameters = parameters(move)
var acc = []
for (var i = 0; i < parameter_values.length; i++) {
var pvi = parameter_values[i]
var term
if (pvi === undefined || pvi === null) {
term = "____"
} else if (formal_parameters[i].words && words_ok) {
// caller wants special html-enabled return type, and we support it, e.g. Custom
term = formal_parameters[i].words(pvi, move, dialect)
} else if (formal_parameters[i].string) {
term = formal_parameters[i].string(pvi, move, dialect)
} else {
term = String(pvi)
}
acc.push(parameterSubstitution(formal_parameters[i], term, dialect))
}
return acc
}
// called when we don't know if the parameter is a dancer
function parameterSubstitution(formal_parameter, actual_parameter, dialect) {
var term = actual_parameter
return (
(formalParamIsDancers(formal_parameter) && dialect.dancers[term]) ||
actual_parameter
)
}
// called when we do know the parameter is a dancer
function dancerSubstitution(dancer_term, dialect) {
return dialect.dancers[dancer_term] || dancer_term
}
export const dancerMenuLabel = function(dancer_term, dialect) {
if (dancer_term) {
return dancerSubstitution(dancer_term, dialect)
} else {
return "unspecified"
}
}
function heyLengthSubstitution(hey_length, dialect) {
var hey_arr = parseHeyLength(hey_length)
var hey0 = hey_arr[0]
if (hey0 === "full" || hey0 === "half") {
return hey0
} else {
hey_arr[1] === 1 ||
hey_arr[1] === 2 ||
throw_up("parseHeyLength()s second value is not 1 or 2: " + hey_arr[1])
var nth_time = hey_arr[1] === 2 ? " 2nd time" : ""
return dancerSubstitution(hey0, dialect) + " meet" + nth_time
}
}
var moveSubstitutionPercentSRegexp = / *%S */g
function moveSubstitution(move_term, dialect) {
var sub = moveSubstitutionWithEscape(move_term, dialect)
return sub.replace(moveSubstitutionPercentSRegexp, " ").trim()
}
function moveSubstitutionWithEscape(move_term, dialect) {
return dialect.moves[move_term] || move_term
}
// The basic applicaiton is a user substitution from 'form an ocean
// wave' to 'form a short wave' and makes it possible to extract the phrases
// 'a short wave' and 'short wave'.
//
// This takes a substitution that might be 'form a blahblah' and
// returns either 'a blahblah' or 'blahblah', depending on the
// optional add_article argument.
//
// Oh hey, the word 'form' and the word 'a' are both entered by the
// user, and so are optional.
// Check the specs for lots of examples.
function moveSubstitutionWithoutForm(
move_term,
dialect,
add_article,
adjectives
) {
if (undefined === add_article) {
add_article = false
}
if (undefined === adjectives) {
adjectives = false
}
var subst = moveSubstitution(move_term, dialect)
var match = subst.match(/(?:form )?(?:(an?) )?(.*)/i)
var root = match[2]
var adjectives_and_root = words(adjectives, root)
if (add_article) {
var article = /[aeiou]/.test(adjectives_and_root.peek()) ? "an" : "a"
return words(article, adjectives, root)
} else {
return adjectives_and_root
}
}
// === Related Moves =============
// Note that a lot of these are 'is composed of' relationships, and as such they
// might be moved to another representation later.
var _relatedMoves = {}
function defineRelatedMove1Way(from, to) {
_relatedMoves[from] = _relatedMoves[from] || []
_relatedMoves[from].push(to)
}
export const defineRelatedMove2Way = (from, to) => {
defineRelatedMove1Way(from, to)
defineRelatedMove1Way(to, from)
}
function relatedMoves(move) {
return _relatedMoves[move] || []
}
var defined_events = {}
////////////////////////////////////////////////
// defineFigure //
////////////////////////////////////////////////
export const defineFigure = (name, parameters, props) => {
var props2 = libfigureObjectCopy(props || {})
if (!props2.goodBeats) {
var beats_index = find_parameter_index_by_name("beats", parameters)
var beats_default = beats_index >= 0 && parameters[beats_index].value
if (beats_default || beats_default === 0) {
props2.goodBeats = goodBeatsEqualFn(beats_default)
}
}
defined_events[name] = { name: name, parameters: parameters, props: props2 }
}
export const defineFigureAlias = (
alias_name,
canonical_name,
parameter_defaults
) => {
"string" == typeof alias_name || throw_up("first argument isn't a string")
"string" == typeof canonical_name ||
throw_up("second argument isn't a string")
Array.isArray(parameter_defaults) ||
throw_up("third argument isn't an array aliasing " + alias_name)
var target =
defined_events[canonical_name] ||
throw_up(
"undefined figure alias '" + alias_name + "' to '" + canonical_name + "'"
)
if (target.parameters.length !== parameter_defaults.length) {
throw_up("wrong number of parameters to " + alias_name)
}
// defensively copy parameter_defaults[...]{...} into params
var params = new Array(target.parameters.length)
for (var i = 0; i < target.parameters.length; i++) {
params[i] = parameter_defaults[i] || target.parameters[i]
}
defined_events[alias_name] = {
name: canonical_name,
parameters: params,
alias_parameters: parameter_defaults,
props: target.props,
}
}
export const deAliasMove = function(move) {
return defined_events[move].name
}
export const isAlias = function(move_string) {
return defined_events[move_string].name !== move_string
}
// you can also use 'figure.move' in js - this is a late addition because we need a function -dm 04-20-2018
function move(figure) {
return figure.move
}
function alias(figure) {
var fn = moveProp(figure.move, "alias", move)
return fn(figure)
}
// does not include itself
function aliases(move) {
// loop through defined_events, returning all keys where value.name == move
var acc = []
Object.keys(defined_events).forEach(function(key) {
var value = defined_events[key]
if (value.name == move && key != move) {
acc.push(key)
}
})
return acc
}
export const aliasFilter = function(move_alias_string) {
if (move_alias_string === deAliasMove(move_alias_string)) {
throw_up(
"aliasFilter(someDeAliasedMove) would produce weirdly overly specific filters if we weren't raising this error - it's only defined for move aliases"
)
}
return aliasParameters(move_alias_string).map(function(param) {
return param ? param.value : "*"
})
}
// List all the moves known to contradb.
// See also: moveTermsAndSubstitutions
export const moves = () => {
return Object.keys(defined_events)
}
function moveTermsAndSubstitutions(dialect) {
if (!dialect) {
throw_up("must specify dialect to moveTermsAndSubstitutions")
}
var terms = Object.keys(defined_events)
var ms = terms.map(function(term) {
return { term: term, substitution: moveSubstitution(term, dialect) }
})
ms = ms.sort(function(a, b) {
var aa = a.substitution.toLowerCase()
var bb = b.substitution.toLowerCase()
if (aa < bb) {
return -1
} else if (aa > bb) {
return 1
} else {
return 0
}
})
return ms
}
export const moveTermsAndSubstitutionsForSelectMenu = dialect => {
if (!dialect) {
throw_up("must specify dialect to moveTermsAndSubstitutionsForSelectMenu")
}
var mtas = moveTermsAndSubstitutions(dialect)
var swing_index = mtas.findIndex(function(e) {
return "swing" === e.term
})
if (swing_index >= 5) {
mtas.unshift(mtas[swing_index]) // copy swing to front of the list
}
return mtas
}
function isMove(string) {
return !!defined_events[string]
}
export const parameterLabel = (move, index) => {
var fig_def = defined_events[move]
var ps = parameters(move)
return (
(fig_def &&
fig_def.props &&
fig_def.props.labels &&
fig_def.props.labels[index]) ||
(ps[index] && ps[index].name)
)
}
var issued_parameter_warning = false
// TODO: complete renaming to formalParameters
export const parameters = move => {
var fig = defined_events[move]
if (fig) {
return fig.parameters
}
if (move && !issued_parameter_warning) {
issued_parameter_warning = true
throw_up("could not find a figure definition for '" + move + "'. ")
}
return []
}
export const formalParameters = parameters
function aliasParameters(move) {
var fig = defined_events[move]
if (fig && fig.alias_parameters) {
return fig.alias_parameters
} else {
throw_up(
"call to aliasParameters('" +
move +
"') on a thing that doesn't seem to be an alias"
)
}
}
function moveProp(move_or_nil, property_name, default_value) {
if (move_or_nil) {
var fig_def = defined_events[move_or_nil]
return (fig_def && fig_def.props[property_name]) || default_value
} else {
return default_value
}
}
export const stringInDialect = (str, dialect) => {
if (textInDialect(dialect)) {
return str
} else {
// Since this is called a lot, performance might be helped by memoizing dialectRegExp(dialect)
return str.replace(dialectRegExp(dialect), function(
_whole_match,
left_whitespace,
match
) {
// conceptually this is a pretty straightforward lookup in one hash or another, and usually that's all it takes...
var substitution = dialect.moves[match] || dialect.dancers[match]
if (substitution) {
return stringInDialectHelper(
match,
substitution,
left_whitespace,
match
)
}
// ...but sometimes they've uppercased their text, so we've gotta look again in the hash (in lower case)...
var match_lower = match.toLowerCase()
substitution = dialect.moves[match_lower] || dialect.dancers[match_lower]
if (substitution) {
return stringInDialectHelper(
match_lower,
substitution,
left_whitespace,
match
)
}
// ...and sometimes the term itself is in mixed case, e.g. 'California twirl' or "Rory O'More"...
var term
for (term in dialect.moves) {
if (term.toLowerCase() === match_lower) {
return stringInDialectHelper(
term,
dialect.moves[term],
left_whitespace,
match
)
}
}
for (term in dialect.dancers) {
if (term.toLowerCase() === match_lower) {
return stringInDialectHelper(
term,
dialect.dancers[term],
left_whitespace,
match
)
}
}
/// ... I guess it's possible none of this worked, which is weird enough to explode.
throw_up("failed to look up " + match)
})
}
}
// polish the case and remove %S in the final substitution
function stringInDialectHelper(term, substitution, left_whitespace, match) {
var s
if (hasUpperCase(substitution)) {
s = substitution
} else if (moreCapitalizedThan(match, term)) {
s = capitalize(substitution)
} else {
s = substitution
}
return left_whitespace + s.replace(/%S/g, "")
}
function hasUpperCase(x) {
return x !== x.toLowerCase()
}
function moreCapitalizedThan(left, right) {
return hasUpperCase(left) && !hasUpperCase(right)
}
// Return a new string with the first lower case letter (if any) capitalized
function capitalize(s) {
// ugh, unicode is hard in JS
for (var i = 0; i < s.length; i++) {
var c = s[i]
var c_big = c.toUpperCase()
if (c_big !== c) {
var x = s.split("")
x.splice(i, 1, c_big)
return x.join("")
}
}
return s
}
function dialectRegExp(dialect) {
var move_strings = Object.keys(dialect.moves)
var dance_strings = Object.keys(dialect.dancers)
var term_strings = move_strings.concat(dance_strings).sort(longestFirstSortFn)
var big_re_string_center = term_strings.map(regExpEscape).join("|")
var big_re_string
if (big_re_string_center) {
big_re_string =
"(\\s|" +
PUNCTUATION_CHARSET_STRING +
"|^)(" +
big_re_string_center +
")(?=\\s|" +
PUNCTUATION_CHARSET_STRING +
"|$)"
} else {
big_re_string = "^[]" // unmatchable regexp - https://stackoverflow.com/a/25315586/5158597
}
return new RegExp(big_re_string, "gi")
}
////
function goodBeatsWithContext(figures, index) {
var figure = figures[index]
if (
0 !==
sumBeats(figures, index + 1) % moveProp(figure.move, "alignBeatsEnd", 1)
) {
return false
} else {
return goodBeats(figure)
}
}
function goodBeats(figure) {
var fn = moveProp(figure.move, "goodBeats", defaultGoodBeats)
return fn(figure)
}
export const goodBeatsMinMaxFn = (min, max) => {
return function(figure) {
var beats = figureBeats(figure)
return min <= beats && beats <= max
}
}
export const goodBeatsMinFn = min => {
return function(figure) {
var beats = figureBeats(figure)
return min <= beats
}
}
export const goodBeatsEqualFn = beats => {
return function(figure) {
return beats === figureBeats(figure)
}
}
export const defaultGoodBeats = goodBeatsEqualFn(8)

1927
external/libfigure/figure.js vendored Normal file

File diff suppressed because it is too large Load Diff

46
external/libfigure/libfigure.js vendored Normal file
View File

@ -0,0 +1,46 @@
// repackage exports in a single file, so that LibFigure consumers
// don't have to worry about what specific file a variable is defined
// in.
import { defaultDialect } from "./util.js"
import { param, wristGrips } from "./param.js"
import { degreesToWords, anglesForMove } from "./move.js"
import {
chooser,
dancerMenuForChooser,
dancerCategoryMenuForChooser,
dancerChooserNames,
} from "./chooser.js"
import {
aliasFilter,
isAlias,
deAliasMove,
moves,
moveTermsAndSubstitutionsForSelectMenu,
formalParameters,
parameterLabel,
dancerMenuLabel,
} from "./define-figure.js"
import {} from "./figure.js" // for side effect!
import {} from "./after-figure.js" // for ... side effect?
import {} from "./dance.js" // ?? for side effect??
export default {
defaultDialect,
param,
wristGrips,
degreesToWords,
anglesForMove,
chooser,
dancerMenuForChooser,
dancerCategoryMenuForChooser,
dancerChooserNames,
aliasFilter,
deAliasMove,
isAlias,
moves,
moveTermsAndSubstitutionsForSelectMenu,
formalParameters,
parameterLabel,
dancerMenuLabel,
}

101
external/libfigure/move.js vendored Normal file
View File

@ -0,0 +1,101 @@
// __ __ _____ _______
// | \/ |/ _ \ \ / / ____|
// | |\/| | | | \ \ / /| _|
// | | | | |_| |\ V / | |___
// |_| |_|\___/ \_/ |_____|
//
//
// Properties of moves (strings).
// Few dependencies to the rest of the system.
var moveCaresAboutRotationsHash = {
"do si do": true,
allemande: true,
gyre: true,
"allemande orbit": true,
"star promenade": true,
"mad robin": true,
}
// it now seems to me that this should be defined by defineFigure -dm 03-07-2017
function moveCaresAboutRotations(move) {
return moveCaresAboutRotationsHash[deAliasMove(move)]
}
var moveCaresAboutPlacesHash = {
circle: true,
star: true,
"facing star": true,
"square through": true,
"box circulate": true,
}
// it now seems to me that this should be defined by defineFigure -dm 03-07-2017
function moveCaresAboutPlaces(move) {
return moveCaresAboutPlacesHash[deAliasMove(move)]
}
var degrees2rotations = {
90: "¼",
180: "½",
270: "¾",
360: "once",
450: "1¼",
540: "1½",
630: "1¾",
720: "twice",
810: "2¼",
900: "2½",
"*": "*",
}
var degrees2places = {
90: "1 place",
180: "2 places",
270: "3 places",
360: "4 places",
450: "5 places",
540: "6 places",
630: "7 places",
720: "8 places",
810: "9 places",
900: "10 places",
"*": "* places",
}
export const degreesToWords = function(degrees, optional_move) {
if (optional_move) {
if (moveCaresAboutRotations(optional_move) && degrees2rotations[degrees]) {
return degrees2rotations[degrees]
} else if (moveCaresAboutPlaces(optional_move) && degrees2places[degrees]) {
return degrees2places[degrees]
}
}
return degrees.toString() + " degrees"
}
function degreesToRotations(degrees) {
if (degrees) {
return degrees2rotations[degrees] || degrees.toString() + " degrees"
} else {
return "?"
}
}
function degreesToPlaces(degrees) {
if (degrees) {
return degrees2places[degrees] || degrees.toString() + " degrees"
} else {
return "? places"
}
}
var anglesForMoveArr = [90, 180, 270, 360, 450, 540, 630, 720, 810, 900]
export const anglesForMove = function(move) {
if (move === "square through") {
return [180, 270, 360]
} else if (move === "box circulate") {
return [90, 180, 270, 360]
} else {
return anglesForMoveArr
}
}

793
external/libfigure/param.js vendored Normal file
View File

@ -0,0 +1,793 @@
// ____ _ ____ _ __ __
// | _ \ / \ | _ \ / \ | \/ |
// | |_) / _ \ | |_) | / _ \ | |\/| |
// | __/ ___ \| _ < / ___ \| | | |
// |_| /_/ \_\_| \_\/_/ \_\_| |_|
//
// Params have semantic value specific to each figure.
// Params are returned by calls like "formalParameters('swing')".
// Some param patterns have emerged. Patterns like:
// figures have a subject telling who's acted on by the figure.
//
import { throw_up } from "./util"
import { chooser } from "./chooser"
const __params = {}
function defineParam(codeName, hash) {
const h = (__params[codeName] = { ...hash })
if (h.ui) {
h.ui = chooser(h.ui)
}
}
export const param = codeName => {
return (
__params[codeName] || throw_up("attempt to find non param: " + codeName)
)
}
function stringParamBalance(value) {
if (value === "*") {
return "optional balance & "
} else if (value) {
return "balance & "
} else return ""
}
defineParam("balance_true", {
name: "bal",
value: true,
ui: "chooser_boolean",
string: stringParamBalance,
})
defineParam("balance_false", {
name: "bal",
value: false,
ui: "chooser_boolean",
string: stringParamBalance,
})
// if we ever add just an undefaulted 'balance', we probably should add it to the thing that adds
// related moves in after-figure.js -dm 01-13-2019
defineParam("swing_prefix_none", {
name: "prefix",
value: "none",
ui: "chooser_swing_prefix",
})
defineParam("swing_prefix_meltdown", {
name: "prefix",
value: "meltdown",
ui: "chooser_swing_prefix",
})
function stringParamHalfSashay(value) {
if (value === "*") {
return "maybe with a half sashay"
} else if (value) {
return "with a half sashay"
} else return ""
}
// param_half_sashay_true = {name: "½sash", value: true, ui: 'chooser_boolean', string: stringParamHalfSashay} not used
defineParam("half_sashay_false", {
name: "½sash",
value: false,
ui: "chooser_boolean",
string: stringParamHalfSashay,
})
defineParam("subject_walk_in_true", {
name: "in",
value: true,
ui: "chooser_boolean",
})
defineParam("others_walk_out_false", {
name: "out",
value: false,
ui: "chooser_boolean",
})
defineParam("pass_through_true", {
name: "pass thru",
value: true,
ui: "chooser_boolean",
})
// angular is picky about duped addresses of params, so we give them unique addresses.
// if we move to another framework, these can all be compressed
defineParam("ricochet1_false", {
name: "rico",
value: false,
ui: "chooser_boolean",
})
defineParam("ricochet2_false", {
name: "rico",
value: false,
ui: "chooser_boolean",
})
defineParam("ricochet3_false", {
name: "rico",
value: false,
ui: "chooser_boolean",
})
defineParam("ricochet4_false", {
name: "rico",
value: false,
ui: "chooser_boolean",
})
function stringParamBeatsNotN(n) {
return function(value) {
return value.toString()
}
}
// do not use param_beats without a default number of beats - it causes dance validation explosions in the editor -dm 08-14-2018
// defineParam('beats', {name: "beats", ui: 'chooser_beats', string: stringParamBeatsNotN(-100)});
defineParam("beats_0", {
name: "beats",
value: 0,
ui: "chooser_beats",
string: stringParamBeatsNotN(0),
})
defineParam("beats_2", {
name: "beats",
value: 2,
ui: "chooser_beats",
string: stringParamBeatsNotN(2),
})
defineParam("beats_4", {
name: "beats",
value: 4,
ui: "chooser_beats",
string: stringParamBeatsNotN(4),
})
defineParam("beats_6", {
name: "beats",
value: 6,
ui: "chooser_beats",
string: stringParamBeatsNotN(6),
})
defineParam("beats_8", {
name: "beats",
value: 8,
ui: "chooser_beats",
string: stringParamBeatsNotN(8),
})
defineParam("beats_12", {
name: "beats",
value: 12,
ui: "chooser_beats",
string: stringParamBeatsNotN(12),
})
defineParam("beats_16", {
name: "beats",
value: 16,
ui: "chooser_beats",
string: stringParamBeatsNotN(16),
})
function makeTurnStringParam(left, right, asterisk, null_) {
return function(value) {
if (value) {
if ("*" === value) {
return asterisk
} else {
return left
}
} else {
if (null === value) {
return null_
} else {
return right
}
}
}
}
var stringParamClock = makeTurnStringParam(
"clockwise",
"counter-clockwise",
"*",
"____"
)
var stringParamLeftRight = makeTurnStringParam("left", "right", "*", "____")
var stringParamShoulders = makeTurnStringParam(
"right shoulders",
"left shoulders",
"* shoulders",
"____"
)
var stringParamShouldersTerse = makeTurnStringParam(
"rights",
"lefts",
"* shoulders",
"____"
)
var stringParamHandStarHand = makeTurnStringParam(
"right",
"left",
"* hand",
"____"
)
var stringParamHand = makeTurnStringParam("right", "left", "*", "____")
// spin = clockwise | ccw | undefined
defineParam("spin", {
name: "turn",
ui: "chooser_spin",
string: stringParamClock,
})
defineParam("spin_clockwise", {
name: "turn",
value: true,
ui: "chooser_spin",
string: stringParamClock,
})
defineParam("spin_ccw", {
name: "turn",
value: false,
ui: "chooser_spin",
string: stringParamClock,
})
defineParam("spin_left", {
name: "turn",
value: true,
ui: "chooser_left_right_spin",
string: stringParamLeftRight,
})
defineParam("spin_right", {
name: "turn",
value: false,
ui: "chooser_left_right_spin",
string: stringParamLeftRight,
})
defineParam("xhand_spin", {
name: "hand",
ui: "chooser_right_left_hand",
string: stringParamHandStarHand,
})
defineParam("right_hand_spin", {
name: "hand",
value: true,
ui: "chooser_right_left_hand",
string: stringParamHandStarHand,
})
defineParam("left_hand_spin", {
name: "hand",
value: false,
ui: "chooser_right_left_hand",
string: stringParamHandStarHand,
})
defineParam("xshoulders_spin", {
name: "shoulder",
ui: "chooser_right_left_shoulder",
string: stringParamShoulders,
})
defineParam("right_shoulders_spin", {
name: "shoulder",
value: true,
ui: "chooser_right_left_shoulder",
string: stringParamShoulders,
})
defineParam("rights_spin", {
name: "shoulder",
value: true,
ui: "chooser_right_left_shoulder",
string: stringParamShouldersTerse,
}) // shoulder
defineParam("left_shoulders_spin", {
name: "shoulder",
value: false,
ui: "chooser_right_left_shoulder",
string: stringParamShoulders,
})
defineParam("by_xhand", {
name: "c.hand",
ui: "chooser_right_left_hand",
string: stringParamHand,
})
defineParam("by_right_hand", {
name: "hand",
ui: "chooser_right_left_hand",
string: stringParamHand,
value: true,
})
function stringParamDegrees(value, move) {
// this second parameter should go away, it should be handled in figure.js,
// because that's the file that knows about figures and moves.
if (moveCaresAboutRotations(move)) {
return degreesToRotations(value)
} else if (moveCaresAboutPlaces(move)) {
return degreesToPlaces(value)
} else {
console.log(
"Warning: '" + move + "' doesn't care about either rotations or places"
)
return degreesToRotations(value)
}
}
defineParam("revolutions", {
name: "circling",
ui: "chooser_revolutions",
string: stringParamDegrees,
})
defineParam("half_around", {
name: "circling",
value: 180,
ui: "chooser_revolutions",
string: stringParamDegrees,
})
defineParam("once_around", {
name: "circling",
value: 360,
ui: "chooser_revolutions",
string: stringParamDegrees,
})
defineParam("once_and_a_half", {
name: "circling",
value: 540,
ui: "chooser_revolutions",
string: stringParamDegrees,
})
defineParam("places", {
name: "places",
ui: "chooser_places",
string: stringParamDegrees,
})
defineParam("two_places", {
name: "places",
value: 180,
ui: "chooser_places",
string: stringParamDegrees,
})
defineParam("three_places", {
name: "places",
value: 270,
ui: "chooser_places",
string: stringParamDegrees,
})
defineParam("four_places", {
name: "places",
value: 360,
ui: "chooser_places",
string: stringParamDegrees,
})
// always 'everyone' and never 'everybody'!
defineParam("subject", {
name: "who",
value: "everyone",
ui: "chooser_dancers",
})
defineParam("subject_pair", { name: "who", ui: "chooser_pair" }) // 1 pair of dancers
defineParam("subject_pair_ladles", {
name: "who",
value: "ladles",
ui: "chooser_pair",
})
defineParam("center_pair_ladles", {
name: "center",
value: "ladles",
ui: "chooser_pair",
})
defineParam("subject_pair_gentlespoons", {
name: "who",
value: "gentlespoons",
ui: "chooser_pair",
})
defineParam("subject_pair_ones", {
name: "who",
value: "ones",
ui: "chooser_pair",
})
defineParam("subject_pair_or_everyone", {
name: "who",
value: "everyone",
ui: "chooser_pair_or_everyone",
})
defineParam("subject_pairc_or_everyone", {
name: "who",
value: "everyone",
ui: "chooser_pairc_or_everyone",
}) // has `centers`
defineParam("subject_pairz", { name: "who", ui: "chooser_pairz" }) // 1-2 pairs of dancers
defineParam("subject_pairz_partners", {
name: "who",
value: "partners",
ui: "chooser_pairz",
})
defineParam("subject_pairz_ladles", {
name: "who",
value: "ladles",
ui: "chooser_pairz",
})
defineParam("subject2_pairz_or_unspecified", {
name: "who2",
value: "",
ui: "chooser_pairz_or_unspecified",
})
defineParam("subject_pairs", { name: "who", ui: "chooser_pairs" }) // 2 pairs of dancers
defineParam("sides_pairs_neighbors", {
name: "sides",
value: "neighbors",
ui: "chooser_pairs",
})
defineParam("subject2_pairs", { name: "who2", ui: "chooser_pairs" })
defineParam("subject_pairs_or_everyone", {
name: "who",
ui: "chooser_pairs_or_everyone",
})
defineParam("subject_pairs_partners", {
name: "who",
value: "partners",
ui: "chooser_pairs",
})
defineParam("subject_dancer", { name: "who", ui: "chooser_dancer" })
// param_subject_role = {name: "who", ui: 'chooser_role'}; // not used
defineParam("subject_role_ladles", {
name: "who",
value: "ladles",
ui: "chooser_role",
})
defineParam("subject_role_gentlespoons", {
name: "who",
value: "gentlespoons",
ui: "chooser_role",
})
// param_subject_partners = {name: "who", value: "partners", ui: 'chooser_pairs'}; // not used
// param_subject_hetero_partners = {name: "who", value: "partners", ui: 'chooser_hetero'}; // not used
defineParam("object_hetero_partners", {
name: "whom",
value: "partners",
ui: "chooser_hetero",
})
defineParam("object_dancer", { name: "whom", ui: "chooser_dancer" })
defineParam("object_pairs", { name: "whom", ui: "chooser_pairs" })
defineParam("object_pairs_or_ones_or_twos", {
name: "whom",
ui: "chooser_pairs_or_ones_or_twos",
})
defineParam("lead_dancer_l1", {
name: "lead",
value: "first ladle",
ui: "chooser_dancer",
})
function formalParamIsDancers(param) {
// harder to maintain implementation:
// return ['who', 'who2', 'whom', 'lead'].indexOf(param.name) >= 0;
return !!dancerMenuForChooser(param.ui)
}
function wordParamCustom(value, move_meh, dialect) {
return lingoLineWords(stringInDialect(value, dialect), dialect)
}
// so if you use param_custom_figure, you have to use parameter_words
// instead of parameter_strings. (unless you use the default figure
// printer, which takes care of it for you, c.f. contra corners)
defineParam("custom_figure", {
name: "custom",
value: "",
ui: "chooser_text",
words: wordParamCustom,
})
export const wristGrips = ["", "wrist grip", "hands across"]
function stringParamStarGrip(value) {
return "*" === value ? "any grip" : value
}
defineParam("star_grip", {
name: "grip",
value: wristGrips[0],
ui: "chooser_star_grip",
string: stringParamStarGrip,
})
function stringParamMarchForward(value) {
if (value) {
return value === "forward" ? "" : value
} else {
return "facing ____"
}
}
defineParam("march_facing", { name: "facing", ui: "chooser_march_facing" })
defineParam("march_forward", {
name: "facing",
ui: "chooser_march_facing",
value: "forward",
string: stringParamMarchForward,
})
function stringParamSlide(value) {
if (value === "*") {
return "*"
} else if (value) {
return "left"
} else {
return "right"
}
}
defineParam("slide", {
name: "slide",
ui: "chooser_slide",
string: stringParamSlide,
})
defineParam("slide_left", {
name: "slide",
value: true,
ui: "chooser_slide",
string: stringParamSlide,
})
defineParam("slide_right", {
name: "slide",
value: false,
ui: "chooser_slide",
string: stringParamSlide,
})
function stringParamSetDirection(value) {
if (value === "across") {
return "across the set"
} else if (value === "along") {
return "along the set"
} else if (
["right diagonal", "left diagonal", "*", undefined].indexOf(value) >= 0
) {
return value
} else {
throw_up("unexpected set direction value: " + value)
}
}
function stringParamSetDirectionSilencingDefault(value_default) {
return function(value) {
return value === value_default ? "" : stringParamSetDirection(value)
}
}
defineParam("set_direction", {
name: "dir",
ui: "chooser_set_direction",
string: stringParamSetDirection,
})
defineParam("set_direction_along", {
name: "dir",
ui: "chooser_set_direction",
value: "along",
string: stringParamSetDirectionSilencingDefault("along"),
})
defineParam("set_direction_across", {
name: "dir",
ui: "chooser_set_direction",
value: "across",
string: stringParamSetDirectionSilencingDefault("across"),
})
defineParam("set_direction_grid", {
name: "dir",
ui: "chooser_set_direction_grid",
string: stringParamSetDirectionSilencingDefault("nope"),
})
defineParam("set_direction_acrossish", {
name: "dir",
ui: "chooser_set_direction_acrossish",
value: "across",
string: stringParamSetDirectionSilencingDefault("across"),
})
defineParam("set_direction_figure_8", {
name: "dir",
ui: "chooser_set_direction_figure_8",
value: "",
}) // '', 'across', 'above', 'below'
function stringParamSliceReturn(value) {
if ("straight" === value) {
return "and straight back"
} else if ("diagonal" === value) {
return "and diagonal back"
} else if ("none" === value) {
return ""
} else if ("*" === value) {
return "and *"
} else {
throw_up("bad slice return value: " + value)
}
}
defineParam("slice_return", {
name: "slice return",
ui: "chooser_slice_return",
value: "straight",
string: stringParamSliceReturn,
})
function stringParamSliceIncrement(value) {
if ("couple" === value) {
return ""
} else if ("dancer" === value) {
return "one dancer"
} else if ("*" === value) {
return "one *"
} else {
throw_up("bad slice increment: " + value)
}
}
defineParam("slice_increment", {
name: "slice increment",
ui: "chooser_slice_increment",
value: "couple",
string: stringParamSliceIncrement,
})
defineParam("all_or_center_or_outsides", {
name: "moving",
ui: "chooser_all_or_center_or_outsides",
value: "all",
})
function stringParamDownTheHallEnder(value) {
if ("" === value) {
return ""
} else if ("turn-couple" === value) {
return "turn as a couple"
} else if ("turn-alone" === value) {
return "turn alone"
} else if ("circle" === value) {
return "bend into a ring"
} else if ("cozy" === value) {
return "form a cozy line"
} else if ("cloverleaf" === value) {
return "bend into a cloverleaf"
} else if ("thread-needle" === value) {
return "thread the needle"
} else if ("right-high" === value) {
return "right hand high, left hand low"
} else if ("sliding-doors" === value) {
return "slide doors"
} else if ("*" === value) {
return "end however"
} else {
throw_up("bad down the hall ender: " + value)
}
}
defineParam("down_the_hall_ender", {
name: "ender",
ui: "chooser_down_the_hall_ender",
value: "",
string: stringParamDownTheHallEnder,
})
defineParam("down_the_hall_ender_circle", {
name: "ender",
ui: "chooser_down_the_hall_ender",
value: "circle",
string: stringParamDownTheHallEnder,
})
defineParam("down_the_hall_ender_turn_couples", {
name: "ender",
ui: "chooser_down_the_hall_ender",
value: "turn-couple",
string: stringParamDownTheHallEnder,
})
function stringParamZigZagEnder(value) {
if ("" === value) {
return ""
} else if ("ring" === value) {
return "into a ring"
} else if ("allemande" === value) {
return "trailing two catching hands"
} else if ("*" === value) {
return "ending however"
} else {
throw_up("bad zig zag ender: " + value)
}
}
defineParam("zig_zag_ender", {
name: "ender",
ui: "chooser_zig_zag_ender",
value: "",
string: stringParamZigZagEnder,
})
defineParam("go_back", { name: "go", ui: "chooser_go_back", value: true })
defineParam("give", { name: "give", ui: "chooser_give", value: true })
var _stringParamGateFace = {
up: "up the set",
down: "down the set",
in: "into the set",
out: "out of the set",
"*": "any direction",
}
function stringParamGateFace(value) {
return _stringParamGateFace[value]
}
defineParam("gate_face", {
name: "face",
ui: "chooser_gate_direction",
string: stringParamGateFace,
})
function stringParamHalfOrFullNotN(default_value) {
return function(value) {
if (default_value === value) {
return ""
} else if (0.5 === value) {
return "half"
} else if (1.0 === value) {
return "full"
} else if ("*" === value) {
return "*"
} else {
throw_up(
"bad half_or_full parameter value: " +
value +
" of type " +
typeof value
)
}
}
}
defineParam("half_or_full", {
name: "half",
ui: "chooser_half_or_full",
string: stringParamHalfOrFullNotN(-100.0),
})
defineParam("half_or_full_half", {
name: "half",
value: 0.5,
ui: "chooser_half_or_full",
string: stringParamHalfOrFullNotN(0.5),
})
defineParam("half_or_full_half_chatty_half", {
name: "half",
value: 0.5,
ui: "chooser_half_or_full",
string: stringParamHalfOrFullNotN(1.0),
}) // hey is chatty about its halfness, but mum about fullness
defineParam("half_or_full_half_chatty_max", {
name: "half",
value: 0.5,
ui: "chooser_half_or_full",
string: stringParamHalfOrFullNotN(-100),
}) // poussette is chatty about both halfness and fullness
defineParam("half_or_full_full", {
name: "half",
value: 1.0,
ui: "chooser_half_or_full",
string: stringParamHalfOrFullNotN(1.0),
})
defineParam("hey_length_half", {
name: "until",
value: "half",
ui: "chooser_hey_length",
string: stringParamHeyLength,
})
function stringParamHeyLength(value, move, dialect) {
if (value === "full" || value === "half" || value === "*") {
return value
} else if (value === null) {
return "____"
} else if (value === "less than half") {
return "until someone meets"
} else if (value === "between half and full") {
return "until someone meets the second time"
} else {
var pair = parseHeyLength(value)
var dancer = pair[0]
var meeting = pair[1] === 2 ? " meet the second time" : " meet"
return "until " + dancerSubstitution(dancer, dialect) + meeting
}
}

25
external/libfigure/polyfill.js vendored Normal file
View File

@ -0,0 +1,25 @@
if (!Array.prototype.findIndex) {
Array.prototype.findIndex = function(predicate) {
"use strict"
if (this == null) {
throw new TypeError(
"Array.prototype.findIndex called on null or undefined"
)
}
if (typeof predicate !== "function") {
throw new TypeError("predicate must be a function")
}
var list = Object(this)
var length = list.length >>> 0
var thisArg = arguments[1]
var value
for (var i = 0; i < length; i++) {
value = list[i]
if (predicate.call(thisArg, value, i, list)) {
return i
}
}
return -1
}
}

209
external/libfigure/util.js vendored Normal file
View File

@ -0,0 +1,209 @@
export const PUNCTUATION_CHARSET_STRING =
"[\u2000-\u206F\u2E00-\u2E7F'!\"#$%&()*+,/:;<=>?@\\[\\]^_`{|}~\\.-]"
function set_if_unset(dict, key, value) {
if (!(key in dict)) dict[key] = value
}
// throw is a keyword and can't be in expressions, but function calls can be, so wrap throw.
export const throw_up = str => {
throw new Error(str)
}
// a little weird that this takes a Words now, not a string
function indefiniteArticleFor(w) {
var str = peek(w)
return /[aeiou]/.test(str) ? "an" : "a"
}
// text_in_dialect: <bool> property can still be missing...
export const defaultDialect = { moves: {}, dancers: {} }
export const testDialect = {
moves: {
gyre: "darcy",
allemande: "almond",
"see saw": "do si do left shoulder",
"form an ocean wave": "form a short wavy line",
"Rory O'More": "sliding doors",
},
dancers: {
ladle: "raven",
ladles: "ravens",
gentlespoon: "lark",
gentlespoons: "larks",
"first ladle": "first raven",
"second ladle": "second raven",
"first gentlespoon": "first lark",
"second gentlespoon": "second lark",
},
}
// ________________________________________________________________
function dialectIsOneToOne(dialect) {
return isEmpty(dialectOverloadedSubstitutions(dialect))
}
function dialectOverloadedSubstitutions(dialect) {
var substitutions = {}
var remember_as_itself = function(x) {
substitutions[x] = (substitutions[x] || []).concat([x])
}
moves().forEach(remember_as_itself)
dancers().forEach(remember_as_itself)
;[dialect.moves, dialect.dancers].forEach(function(hash) {
Object.keys(hash).forEach(function(term) {
var substitution = hash[term]
if (!substitutions[substitution]) {
substitutions[substitution] = []
}
if (-1 === substitutions[substitution].indexOf(term)) {
substitutions[substitution].push(term)
}
})
})
// delete substitutions that are 1-to-1
for (var substitution in substitutions) {
if (substitutions.hasOwnProperty(substitution)) {
if (substitutions[substitution].length === 1)
delete substitutions[substitution]
}
}
return substitutions
}
// ________________________________________________________________
export const longestFirstSortFn = function(a, b) {
return b.length - a.length
}
// ________________________________________________________________
function dialectForFigures(dialect, figures) {
var new_dialect = copyDialect(dialect)
if (figuresUseDancers(figures, "3rd neighbors")) {
new_dialect.dancers["neighbors"] = "1st neighbors"
new_dialect.dancers["next neighbors"] = "2nd neighbors"
}
if (figuresUseDancers(figures, "2nd shadows")) {
new_dialect.dancers["shadows"] = "1st shadows"
}
return new_dialect
}
function copyDialect(dialect) {
return {
dancers: libfigureObjectCopy(dialect.dancers),
moves: libfigureObjectCopy(dialect.moves),
text_in_dialect: !!dialect.text_in_dialect,
}
}
function textInDialect(dialect) {
// see also ruby-side implementation
return !!dialect.text_in_dialect
}
// I just called this function 'copy', but then I got scared and changed it.
export const libfigureObjectCopy = hash => {
var o = {}
Object.keys(hash).forEach(function(key) {
o[key] = hash[key]
})
return o
}
function libfigureUniq(array) {
// suboptimal O(n^2)
var output = []
for (var i = 0; i < array.length; i++) {
if (-1 === output.indexOf(array[i])) {
output.push(array[i])
}
}
return output
}
// // every element is ===
// function array_equal(a1, a2) {
// var l = a1.length;
// if (l !== a2.length) {
// return false;
// }
// for (var i=0; i<l; i++) {
// if (a1[i] !== a2[i]) {
// return false;
// }
// }
// return true;
// }
// used to determine if a dance uses the term 'shadow' or '3rd neighbor'
function figuresUseDancers(figures, dancers_term) {
for (var figi = 0; figi < figures.length; figi++) {
var figure = figures[figi]
var formals = parameters(figure.move)
for (var i = 0; i < formals.length; i++) {
var formal = formals[i]
var actual = figure.parameter_values[i]
if (formalParamIsDancers(formal) && dancers_term === actual) {
return true
}
}
}
return false
}
// unpacks a hey length into a pair, [dancer, meeting_count] or ['full', meeting_count]
function parseHeyLength(hey_length) {
if (hey_length === "full" || hey_length === "between half and full") {
return [hey_length, 2]
} else if (hey_length === "half" || hey_length === "less than half") {
return [hey_length, 1]
} else if (hey_length === "*" || hey_length === null) {
return [hey_length, hey_length] // kinda nonsense, but whatever
} else {
var match = /%%([12])$/.exec(hey_length)
if (match) {
var dancer = hey_length.slice(0, match.index)
var meeting = match[1] === "2" ? 2 : 1
return [dancer, meeting]
}
}
throw_up("unparseable hey length - " + hey_length)
}
function heyLengthMeetTimes(hey_length) {
return parseHeyLength(hey_length)[1]
}
function dancerIsPair(dancer) {
return dancerMenuForChooser(chooser("chooser_pair")).indexOf(dancer) >= 0
}
// see also the similar ruby-side function slugify_move
function slugifyTerm(term) {
return term
.toLowerCase()
.replace(/&/g, "and")
.replace(/ /g, "-")
.replace(/[^a-z0-9-]/g, "")
}
// ________________________________________________________________
const regExpEscape_regexp = /[-\/\\^$*+?.()|[\]{}]/g
export const regExpEscape = function(s) {
return s.replace(regExpEscape_regexp, "\\$&")
}
// source https://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript/3561711#3561711
function isEmpty(hash) {
// can't use: hash.length === 0
for (var x in hash) {
return false
}
return true
}

308
external/libfigure/words.js vendored Normal file
View File

@ -0,0 +1,308 @@
import {
longestFirstSortFn,
PUNCTUATION_CHARSET_STRING,
regExpEscape,
} from "./util"
import { dancers } from "./chooser"
import { moves } from "./define-figure"
function Words(arr) {
this.arr = arr
}
export const words = function() {
return new Words(Array.prototype.slice.call(arguments))
}
Words.prototype.scrunched = function() {
return false
}
// ____ ScrunchedWords are Words that don't insert spaces
// between arguments as agressively as regular words
function ScrunchedWords(arr) {
Words.call(this, arr)
}
ScrunchedWords.prototype = Object.create(Words.prototype)
ScrunchedWords.prototype.constructor = ScrunchedWords
ScrunchedWords.prototype.scrunched = function() {
return true
}
// ____ ScrunchedWords end
var wants_no_space_before = [false, null, ",", ".", ";"]
export const FLATTEN_FORMAT_MARKDOWN = 1001
export const FLATTEN_FORMAT_HTML = 1002
export const FLATTEN_FORMAT_UNSAFE_TEXT = 1003
export const FLATTEN_FORMAT_SAFE_TEXT = 1004
// returns *sanitized* html
Words.prototype.toHtml = function() {
return this.flatten(FLATTEN_FORMAT_HTML)
}
// returns *unsanitized* string with Tags (see below) lobotimized into text.
// May do whitespace dialation ala html, but at least it preserves newlines.
Words.prototype.toUnsafeText = function() {
return this.flatten(FLATTEN_FORMAT_UNSAFE_TEXT)
}
// returns *sanitized* e.g. <b>pb&j</b> → &lt;b&gt;pb&amp;a&lt;b&gt;
// but is different from toHtml because any Tags (see below) are
// flattened with no bracketing tags, they're all body.
Words.prototype.toSafeText = function() {
return this.flatten(FLATTEN_FORMAT_SAFE_TEXT)
}
// returns *unsanitized* string with Tags (see below) rendered as html (not Markdown)
// This preserves newlines, unlike toHtml.
// It's assumed to be headed for a markdown parser that takes care of that.
Words.prototype.toMarkdown = function() {
return this.flatten(FLATTEN_FORMAT_MARKDOWN)
}
Words.prototype.flatten = function(format) {
var arr = this.arr
var acc = []
var space_before = false
for (var i = 0; i < arr.length; i++) {
var wants_space_before =
!this.scrunched() && -1 === wants_no_space_before.indexOf(peek(arr[i]))
if (wants_space_before) {
acc.push(" ")
}
acc.push(flattenWordNode(arr[i], format))
}
return trimButLeaveNewlines(acc.join(""))
}
Words.prototype.peek = function() {
for (var i = 0; i < this.arr.length; i++) {
var p = peek(this.arr[i])
if (p) {
return p
}
}
return null
}
export const Tag = function(tag, attrs, body) {
this.tag = tag
this.attrs = attrs
this.body = body
}
export const tag = function(tag, body) {
return new Tag(tag, {}, body)
}
// function tag_attrs(tag, attrs, body) {
// return new Tag(tag, attrs, body);
// }
Tag.prototype.flatten = function(format) {
if (
format === FLATTEN_FORMAT_UNSAFE_TEXT ||
format === FLATTEN_FORMAT_SAFE_TEXT
) {
return flattenWordNode(this.body, format)
} else if (
format === FLATTEN_FORMAT_MARKDOWN ||
format === FLATTEN_FORMAT_HTML
) {
Object.keys(this.attrs).length === 0 ||
throw_up("attrs not yet implemented, but used")
return (
"<" +
this.tag +
">" +
flattenWordNode(this.body, format) +
"</" +
this.tag +
">"
)
} else {
throw_up("unexpected word flatten format :" + format.toString())
}
}
Tag.prototype.peek = function() {
return peek(this.body)
}
var sanitizationMap = {
"<": "&lt;",
">": "&gt;",
"&": "&amp;",
"&amp;": "&amp;",
}
function flattenWordNode(s, format) {
if (s.flatten) {
return s.flatten(format)
} else if ("string" === typeof s) {
if (format === FLATTEN_FORMAT_HTML || format === FLATTEN_FORMAT_SAFE_TEXT) {
var replacer = function(match) {
return (
sanitizationMap[match] ||
throw_up("Unexpected match during flatten sanitize")
)
}
return s.replace(/&amp;|&|<|>/g, replacer)
} else if (
format === FLATTEN_FORMAT_MARKDOWN ||
format === FLATTEN_FORMAT_UNSAFE_TEXT
) {
return s
} else {
throw_up("unexpected flatten format: " + format.toString())
}
} else if (comma === s) {
return ","
} else if (false === s) {
return ""
} else {
return "" + s
}
}
// returns first non-whitespace character
export const peek = function(thing) {
var m
if (thing.peek) {
return thing.peek()
} else if (typeof thing === "string" && (m = thing.match(/[\S\n]/))) {
return m[0]
} else if (thing == comma) {
return ","
} else if (thing == false) {
return null
} else {
return null
}
}
export const trimButLeaveNewlines = function(s) {
var start
var end
for (start = 0; start < s.length; start++) {
if (s[start].match(/[\S\n]/)) {
break
}
}
for (end = s.length - 1; end >= start; end--) {
if (s[end].match(/[\S\n]/)) {
break
}
}
return s.slice(start, end + 1)
}
////////////////////////////////////////////////////////////////
export const comma = Object.freeze([Object.freeze("comma")])
////////////////////////////////////////////////////////////////
// clamp words in <s> or <u> - string is already in-dialect at the point when this proceess it
export const lingoLineWords = function(string, dialect) {
// lookbehind doesn't work in all versions of js, so we've got to use capture groups for word boundaries, sigh
var underlines_and_strikes = underlinesAndStrikes(dialect)
var all_lingo_lines = underlines_and_strikes.underlines
.concat(underlines_and_strikes.strikes)
.sort(longestFirstSortFn)
var regex = new RegExp(
"(\\s|" +
PUNCTUATION_CHARSET_STRING +
"|^)(" +
all_lingo_lines.map(regExpEscape).join("|") +
")(\\s|" +
PUNCTUATION_CHARSET_STRING +
"|$)",
"ig"
)
var buffer = []
var last_match_ended_at
while (true) {
last_match_ended_at = regex.lastIndex
var match_info = regex.exec(string)
if (!match_info) break
buffer.push(string.slice(last_match_ended_at, match_info.index))
var is_strike =
underlines_and_strikes.strikes.indexOf(match_info[2].toLowerCase()) >= 0
buffer.push(match_info[1]) // its whitespace, but it might be a newline
buffer.push(tag(is_strike ? "s" : "u", match_info[2]))
regex.lastIndex = regex.lastIndex - match_info[3].length // put back trailing whitespace
}
buffer.push(string.slice(last_match_ended_at))
return new ScrunchedWords(buffer)
}
var bogusTerms = [
"men",
"women",
"man",
"woman",
"gentlemen",
"gentleman",
"gents",
"gent",
"ladies",
"lady",
"leads",
"lead",
"follows",
"follow",
"larks",
"lark",
"ravens",
"raven",
"sex",
"gypsy",
"yearn",
"rory o'moore",
"rollaway",
"roll-away",
"nn",
"n ",
"p ",
"l ",
"g ",
"m ",
"w ",
"n.",
"p.",
"l.",
"g.",
"m.",
"w.",
"g1",
"g2",
"l1",
"l2",
]
var terms_for_uands
// NB on return value: it is freshly allocated each time
function underlinesAndStrikes(dialect) {
if (!terms_for_uands) {
terms_for_uands = moves().concat(dancers())
}
var underlines = terms_for_uands.map(function(term) {
var substitution = dialect.dancers[term] || dialect.moves[term]
return (substitution ? stripPercentS(substitution) : term).toLowerCase()
})
var strikes = terms_for_uands.concat(bogusTerms).filter(function(s) {
return -1 === underlines.indexOf(s.toLowerCase())
})
return { underlines: underlines, strikes: strikes }
}
function stripPercentS(str) {
return str.replace(/%S/g, "").trim()
}

7
tsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"compilerOptions": {
"module": "es2015",
"target": "es2015",
"lib": [ "DOM", "es2022" ]
}
}

17
www/index.html Normal file
View File

@ -0,0 +1,17 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Prototype Contra Dance Renderer</title>
<style>
canvas {
border: solid black 2px;
height: 100%;
width: 100%;
}
</style>
<script type="module" src="js/main.js"></script>
</head>
<body>
</body>
</html>

1
www/js/libfigure Symbolic link
View File

@ -0,0 +1 @@
../../external/libfigure

22
www/js/main.ts Normal file
View File

@ -0,0 +1,22 @@
import * as renderer from "./renderer.js";
const wrapperDiv = document.createElement('div');
const canvas = document.createElement('canvas');
wrapperDiv.appendChild(canvas);
document.querySelector('body')?.appendChild(wrapperDiv);
const ctx = canvas.getContext('2d')!;
// TODO Redo this on resize?
const w = canvas.offsetWidth;
const h = canvas.offsetHeight;
const s = window.devicePixelRatio;
canvas.width = w * s;
canvas.height = h * s;
ctx.translate(canvas.width/2.0, canvas.height/2.0);
const scale = 20;
ctx.scale(-canvas.height/scale,-canvas.height/scale);
const r = new renderer.Renderer(ctx);
r.drawSets(renderer.improperLongLines, 3, 3);

223
www/js/renderer.ts Normal file
View File

@ -0,0 +1,223 @@
import * as libfigure from "./libfigure/libfigure.js";
enum DanceRole {
Lark = "Lark",
Robin = "Robin",
}
enum CoupleRole {
Ones = "Ones", // aka "Actives"
Twos = "Twos", // aka "Inactives"
}
enum Rotation {
Up = 0,
Down = 180,
Left = 90,
Right = 270,
}
export interface DancerIdentity {
danceRole: DanceRole,
coupleRole: CoupleRole, // TODO better name for this?
}
interface ExtendedDancerIdentity {
// Identity within own set.
setIdentity: DancerIdentity,
// 0 = focused line, -1 = line to left, +1 = line to right, ...
relativeLine: number,
// 0 = focused set, +1 = next set up, -1 = next set down.
relativeSet: number,
}
export interface Offset {
x: number;
y: number;
}
export const offsetZero : Offset = { x: 0, y: 0 };
// Distance from one side of the set to the other. Set so the left
// of the set is -1 and the right of the set is +1.
export const setWidth : number = 2;
// Distance from top of one hands four to its bottom.
// Equivalently, the distance between a dancer and their neighbor
// in improper position. Set so +1 is the top and -1 is the bottom.
export const setHeight : number = 2;
// Distance between two hands fours in neutral position.
const setSpacing : number = setHeight;
// y offset between any dancer and their previous/next neighbor
// of the same role.
const setDistance : number = setHeight + setSpacing;
const lineSpacing : number = setWidth;
// x offset between any dancer and their eqivalent in the line to
// to left/right.
const lineDistance : number = setWidth + lineSpacing;
const dancerWidth : number = 0.75; // Not sure about this value.
const dancerHeight : number = 0.25; // Not sure about this value.
const dancerHeightOffset : number = 0; //dancerHeight/4;
export const leftShoulder : Offset = { x: -dancerWidth/2, y: -dancerHeightOffset };
export const rightShoulder : Offset = { x: dancerWidth/2, y: -dancerHeightOffset };
export interface DancerSetPosition {
// Position of the dancer relative to the center of their set.
position: Offset;
// Rotation in degrees where 0 is up the hall,
// 90 is to the right, 180 is down the hall,
// and 270 is to the left.
rotation: number;
leftArmEnd?: Offset;
rightArmEnd?: Offset;
}
function degreesToRadians(degrees: number) {
return degrees * (Math.PI / 180);
}
function hueForDancer(identity: DancerIdentity) : number {
if (identity.coupleRole == CoupleRole.Ones) {
if (identity.danceRole == DanceRole.Lark) {
return 0; //red
} else {
return 39; //orange
}
} else {
if (identity.danceRole == DanceRole.Lark) {
return 240; //blue
} else {
return 180; //teal
}
}
}
function colorForDancer(identity: ExtendedDancerIdentity) : string {
const hue = hueForDancer(identity.setIdentity);
const sat = 100 - Math.abs(identity.relativeLine * 40);
const lum = 50 + identity.relativeSet * 20;
return `hsl(${hue}, ${sat}%, ${lum}%)`;
}
export const improperNeutral = new Map<DancerIdentity, DancerSetPosition>([
[{coupleRole: CoupleRole.Ones, danceRole: DanceRole.Lark}, {
position: {x: +1, y: -1},
rotation: Rotation.Right,
}],
[{coupleRole: CoupleRole.Ones, danceRole: DanceRole.Robin}, {
position: {x: -1, y: -1},
rotation: Rotation.Left,
}],
[{coupleRole: CoupleRole.Twos, danceRole: DanceRole.Lark}, {
position: {x: -1, y: +1},
rotation: Rotation.Left,
}],
[{coupleRole: CoupleRole.Twos, danceRole: DanceRole.Robin}, {
position: {x: +1, y: +1},
rotation: Rotation.Right,
}],
]);
export const improperLongLines = new Map<DancerIdentity, DancerSetPosition>([
[{coupleRole: CoupleRole.Ones, danceRole: DanceRole.Lark}, {
position: {x: +1, y: -1},
rotation: Rotation.Right,
leftArmEnd: {x: -1, y: 0},
rightArmEnd: {x: +1, y: 0},
}],
[{coupleRole: CoupleRole.Ones, danceRole: DanceRole.Robin}, {
position: {x: -1, y: -1},
rotation: Rotation.Left,
leftArmEnd: {x: -1, y: 0},
rightArmEnd: {x: +1, y: 0},
}],
[{coupleRole: CoupleRole.Twos, danceRole: DanceRole.Lark}, {
position: {x: -1, y: +1},
rotation: Rotation.Left,
leftArmEnd: {x: -1, y: 0},
rightArmEnd: {x: +1, y: 0},
}],
[{coupleRole: CoupleRole.Twos, danceRole: DanceRole.Robin}, {
position: {x: +1, y: +1},
rotation: Rotation.Right,
leftArmEnd: {x: -1, y: 0},
rightArmEnd: {x: +1, y: 0},
}],
]);
export class Renderer {
ctx: CanvasRenderingContext2D;
constructor(ctx: CanvasRenderingContext2D) {
this.ctx = ctx;
}
drawDancerBody() {
this.ctx.beginPath();
this.ctx.moveTo(leftShoulder.x, leftShoulder.y);
this.ctx.lineTo(rightShoulder.x, rightShoulder.y);
this.ctx.lineTo(0, dancerHeight-dancerHeightOffset);
this.ctx.fill();
// Draw dot at origin to identify "center" point of dancer.
const backupFillStyle = this.ctx.fillStyle;
this.ctx.fillStyle = 'black';
const pointSize = 0.05;
this.ctx.fillRect(-pointSize/2, -pointSize/2, pointSize, pointSize);
this.ctx.fillStyle = backupFillStyle;
}
drawDancer(position: DancerSetPosition, identity?: ExtendedDancerIdentity) {
this.ctx.save();
if (identity) {
this.ctx.translate(identity.relativeLine * lineDistance,
identity.relativeSet * setDistance);
this.ctx.fillStyle = this.ctx.strokeStyle = colorForDancer(identity);
}
this.ctx.translate(position.position.x, position.position.y);
this.ctx.rotate(-degreesToRadians(position.rotation));
this.drawDancerBody();
// Draw arms.
this.ctx.lineWidth = 0.03;
if (position.leftArmEnd) {
this.ctx.beginPath();
this.ctx.moveTo(leftShoulder.x, leftShoulder.y);
this.ctx.lineTo(position.leftArmEnd.x, position.leftArmEnd.y);
this.ctx.stroke();
}
if (position.rightArmEnd) {
this.ctx.beginPath();
this.ctx.moveTo(rightShoulder.x, rightShoulder.y);
this.ctx.lineTo(position.rightArmEnd.x, position.rightArmEnd.y);
this.ctx.stroke();
}
this.ctx.restore();
}
drawSet(positions: Map<DancerIdentity, DancerSetPosition>, relativeSet: number, relativeLine: number) {
for (const entry of positions.entries()) {
this.drawDancer(entry[1], { setIdentity: entry[0], relativeLine, relativeSet });
}
}
drawSets(positions: Map<DancerIdentity, DancerSetPosition>, extraSets: number, extraLines: number) {
for (var relativeLine = -extraLines; relativeLine <= extraLines; relativeLine++) {
for (var relativeSet = -extraSets; relativeSet <= extraSets; relativeSet++) {
this.drawSet(positions, relativeSet, relativeLine);
}
}
}
}