@kieransimkin/dance-rudiments 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +116 -0
- package/dist/typescript/index.d.ts +19 -0
- package/dist/typescript/index.js +26 -0
- package/dist/wasm/dancerudiments.js +2880 -0
- package/dist/wasm/dancerudiments.wasm +0 -0
- package/harness/app.js +49 -0
- package/harness/dancerudiments.js +67 -0
- package/harness/index.html +4 -0
- package/harness/style.css +1 -0
- package/package.json +29 -0
|
Binary file
|
package/harness/app.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { catalogue, sample, wrapPip } from "./dancerudiments.js";
|
|
2
|
+
|
|
3
|
+
const grid = document.querySelector("#grid");
|
|
4
|
+
const bpm = document.querySelector("#bpm");
|
|
5
|
+
const amplitude = document.querySelector("#amplitude");
|
|
6
|
+
const playButton = document.querySelector("#play");
|
|
7
|
+
const pipSlider = document.querySelector("#pip");
|
|
8
|
+
const pipText = document.querySelector("#pip-text");
|
|
9
|
+
let playing = !matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
10
|
+
let absolutePip = 0, lastTime = performance.now(), accumulator = 0;
|
|
11
|
+
|
|
12
|
+
const cards = catalogue.map(info => {
|
|
13
|
+
const card = document.createElement("article");
|
|
14
|
+
card.className = "card";
|
|
15
|
+
card.innerHTML = `<header><h2>${info.name.replaceAll("_"," ")}</h2><span>${info.periodPips} pips · ${info.dimensions}D</span></header><p>${info.description}</p><div class="stage"><div class="axes"></div><div class="subject" aria-hidden="true"></div><svg class="trail" viewBox="0 0 240 160" aria-hidden="true"><polyline/></svg></div><output></output>`;
|
|
16
|
+
grid.append(card);
|
|
17
|
+
const points = Array.from({length: info.periodPips}, (_, p) => {
|
|
18
|
+
const v = sample(info.name, p); return `${120+v.x*88},${80+v.y*58}`;
|
|
19
|
+
}).join(" ");
|
|
20
|
+
card.querySelector("polyline").setAttribute("points", points);
|
|
21
|
+
return { info, subject: card.querySelector(".subject"), output: card.querySelector("output") };
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
function render() {
|
|
25
|
+
const amp = Number(amplitude.value);
|
|
26
|
+
pipSlider.value = String(wrapPip(absolutePip, 256));
|
|
27
|
+
pipText.textContent = String(absolutePip);
|
|
28
|
+
for (const card of cards) {
|
|
29
|
+
const local = wrapPip(absolutePip, card.info.periodPips), v = sample(card.info.name, absolutePip);
|
|
30
|
+
card.subject.style.transform = `translate3d(${v.x*amp}px, ${v.y*amp}px, 0) scale(${1 + v.z*.08})`;
|
|
31
|
+
card.output.textContent = `pip ${local} x ${v.x.toFixed(3)} y ${v.y.toFixed(3)} z ${v.z.toFixed(3)}`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function frame(now) {
|
|
36
|
+
const elapsed = Math.min(100, now-lastTime); lastTime = now;
|
|
37
|
+
if (playing) {
|
|
38
|
+
accumulator += elapsed;
|
|
39
|
+
const pipMs = 60000 / Number(bpm.value) / 64;
|
|
40
|
+
while (accumulator >= pipMs) { absolutePip++; accumulator -= pipMs; }
|
|
41
|
+
render();
|
|
42
|
+
}
|
|
43
|
+
requestAnimationFrame(frame);
|
|
44
|
+
}
|
|
45
|
+
playButton.addEventListener("click", () => { playing=!playing; playButton.textContent=playing?"Pause":"Play"; lastTime=performance.now(); });
|
|
46
|
+
document.querySelector("#previous").addEventListener("click", () => { playing=false; playButton.textContent="Play"; absolutePip--; render(); });
|
|
47
|
+
document.querySelector("#next").addEventListener("click", () => { playing=false; playButton.textContent="Play"; absolutePip++; render(); });
|
|
48
|
+
pipSlider.addEventListener("input", () => { playing=false; playButton.textContent="Play"; absolutePip=Number(pipSlider.value); render(); });
|
|
49
|
+
playButton.textContent=playing?"Pause":"Play"; render(); requestAnimationFrame(frame);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const PI = Math.PI;
|
|
2
|
+
export const wrapPip = (pip, period) => ((pip % period) + period) % period;
|
|
3
|
+
const phase = (pip, period) => wrapPip(pip, period) / period;
|
|
4
|
+
const smooth = t => t * t * (3 - 2 * t);
|
|
5
|
+
const lerp = (a, b, t) => a + (b - a) * smooth(t);
|
|
6
|
+
|
|
7
|
+
const clayKeys = [
|
|
8
|
+
[0,0,0],[23,2/14,-.1],[33,13/14,-.8],[49,5/14,-.4],[87,-3/14,.2],
|
|
9
|
+
[100,-1,.9],[118,-5/14,.5],[154,1/14,0],[169,12/14,1],[187,4/14,.5],
|
|
10
|
+
[215,-2/14,-.2],[228,-10/14,-.9],[243,-3/14,-.3],[256,0,0]
|
|
11
|
+
];
|
|
12
|
+
const clayTable = Array.from({length: 256}, (_, pip) => {
|
|
13
|
+
let i = 0;
|
|
14
|
+
while (i + 1 < clayKeys.length && pip > clayKeys[i + 1][0]) i++;
|
|
15
|
+
const a = clayKeys[i], b = clayKeys[i + 1], t = (pip - a[0]) / (b[0] - a[0]);
|
|
16
|
+
return {x: lerp(a[1], b[1], t), y: lerp(a[2], b[2], t), z: 0};
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const strokeMotion = (pip, period, strokes) => {
|
|
20
|
+
let x = 0, y = 0;
|
|
21
|
+
for (const [start, hand, strength, width] of strokes) {
|
|
22
|
+
const distance = wrapPip(pip - start, period);
|
|
23
|
+
if (distance >= width) continue;
|
|
24
|
+
const s = Math.sin(PI * distance / width);
|
|
25
|
+
const amount = strength * s * s;
|
|
26
|
+
x += hand * amount; y -= amount * .72;
|
|
27
|
+
}
|
|
28
|
+
return {x: Math.max(-1, Math.min(1, x)), y: Math.max(-1, Math.min(1, y)), z: 0};
|
|
29
|
+
};
|
|
30
|
+
const alternating = [[0,1,.92,8],[8,-1,.92,8],[16,1,.92,8],[24,-1,.92,8],[32,1,.92,8],[40,-1,.92,8],[48,1,.92,8],[56,-1,.92,8]];
|
|
31
|
+
const doubles = [[0,1,.88,8],[8,1,.72,8],[16,-1,.88,8],[24,-1,.72,8],[32,1,.88,8],[40,1,.72,8],[48,-1,.88,8],[56,-1,.72,8]];
|
|
32
|
+
const paradiddle = [[0,1,1,8],[8,-1,.72,8],[16,1,.72,8],[24,1,.72,8],[32,-1,1,8],[40,1,.72,8],[48,-1,.72,8],[56,-1,.72,8],
|
|
33
|
+
[64,1,1,8],[72,-1,.72,8],[80,1,.72,8],[88,1,.72,8],[96,-1,1,8],[104,1,.72,8],[112,-1,.72,8],[120,-1,.72,8]];
|
|
34
|
+
|
|
35
|
+
export const catalogue = [
|
|
36
|
+
["bounce","One-beat vertical bounce",64,1],["sway","Two-beat side-to-side sway",128,1],
|
|
37
|
+
["circle","One-beat circular orbit",64,2],["figure_eight","Two-beat figure eight",128,2],
|
|
38
|
+
["step_touch","Two-beat side step with a small lift",128,2],["box_step","Four-beat softened square path",256,2],
|
|
39
|
+
["helix","Four-beat double-turn helix",256,3],["clay_background","Clay/Stars four-beat accented background drift",256,2],
|
|
40
|
+
["single_stroke_roll","Alternating R/L strokes with smooth attack and decay",64,2],
|
|
41
|
+
["double_stroke_roll","RRLL strokes with a softer second motion",64,2],
|
|
42
|
+
["multiple_bounce_roll","Decaying same-hand bounce clusters",64,2],
|
|
43
|
+
["single_paradiddle","RLRR LRLL with accented lead strokes",128,2],
|
|
44
|
+
["flam","Grace motion flowing into an opposite-hand primary motion",64,2],
|
|
45
|
+
["drag","Two grace motions flowing into an opposite-hand primary motion",64,2],
|
|
46
|
+
["five_stroke_roll","Two diddles resolving to an accented fifth motion",128,2]
|
|
47
|
+
].map(([name, description, periodPips, dimensions]) => ({name, description, periodPips, dimensions}));
|
|
48
|
+
|
|
49
|
+
export function sample(name, pip) {
|
|
50
|
+
const p64 = phase(pip, 64), p128 = phase(pip, 128), p256 = phase(pip, 256);
|
|
51
|
+
if (name === "bounce") return {x: 1 - 4 * Math.abs(p64 - .5), y:0, z:0};
|
|
52
|
+
if (name === "sway") return {x: Math.sin(2*PI*p128), y:0, z:0};
|
|
53
|
+
if (name === "circle") { const a=2*PI*p64-PI/2; return {x:Math.cos(a),y:Math.sin(a),z:0}; }
|
|
54
|
+
if (name === "figure_eight") { const a=2*PI*p128; return {x:Math.sin(a),y:Math.sin(2*a),z:0}; }
|
|
55
|
+
if (name === "step_touch") { const q=Math.floor(wrapPip(pip,128)/32),t=wrapPip(pip,32)/32,x0=[-1,0,1,0],x1=[0,1,0,-1]; return {x:lerp(x0[q],x1[q],t),y:-Math.sin(PI*t)*.35,z:0}; }
|
|
56
|
+
if (name === "box_step") { const e=Math.floor(wrapPip(pip,256)/64),t=wrapPip(pip,64)/64,x0=[-1,1,1,-1],y0=[-1,-1,1,1],x1=[1,1,-1,-1],y1=[-1,1,1,-1]; return {x:lerp(x0[e],x1[e],t),y:lerp(y0[e],y1[e],t),z:0}; }
|
|
57
|
+
if (name === "helix") { const a=4*PI*p256; return {x:Math.cos(a),y:Math.sin(a),z:Math.sin(2*PI*p256)}; }
|
|
58
|
+
if (name === "clay_background") return {...clayTable[wrapPip(pip,256)]};
|
|
59
|
+
if (name === "single_stroke_roll") return strokeMotion(pip,64,alternating);
|
|
60
|
+
if (name === "double_stroke_roll") return strokeMotion(pip,64,doubles);
|
|
61
|
+
if (name === "multiple_bounce_roll") return strokeMotion(pip,64,[[0,1,.92,6],[6,1,.68,6],[12,1,.48,6],[18,1,.32,6],[32,-1,.92,6],[38,-1,.68,6],[44,-1,.48,6],[50,-1,.32,6]]);
|
|
62
|
+
if (name === "single_paradiddle") return strokeMotion(pip,128,paradiddle);
|
|
63
|
+
if (name === "flam") return strokeMotion(pip,64,[[0,-1,.34,10],[5,1,.9,14],[32,1,.34,10],[37,-1,.9,14]]);
|
|
64
|
+
if (name === "drag") return strokeMotion(pip,64,[[0,-1,.28,8],[5,-1,.32,8],[10,1,.88,14],[32,1,.28,8],[37,1,.32,8],[42,-1,.88,14]]);
|
|
65
|
+
if (name === "five_stroke_roll") return strokeMotion(pip,128,[[0,1,.72,9],[8,1,.62,9],[16,-1,.72,9],[24,-1,.62,9],[36,1,1,14],[64,-1,.72,9],[72,-1,.62,9],[80,1,.72,9],[88,1,.62,9],[100,-1,1,14]]);
|
|
66
|
+
throw new RangeError(`Unknown dance rudiment: ${name}`);
|
|
67
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>DanceRudiments motion lab</title><link rel="stylesheet" href="style.css"></head>
|
|
2
|
+
<body><main><header class="hero"><p class="eyebrow">INTEGER-PIP MOTION LIBRARY</p><h1>DanceRudiments</h1><p>Rhythmic position functions, sampled exactly once per 1/64-beat pip.</p></header>
|
|
3
|
+
<section class="controls" aria-label="Playback controls"><button id="previous" aria-label="Previous pip">−1</button><button id="play">Pause</button><button id="next" aria-label="Next pip">+1</button><label>BPM <input id="bpm" type="number" min="20" max="300" value="100"></label><label>Amplitude <input id="amplitude" type="range" min="8" max="64" value="42"></label><label class="pip-control">Pip <input id="pip" type="range" min="0" max="255" value="0"><strong id="pip-text">0</strong></label></section>
|
|
4
|
+
<section id="grid" class="grid" aria-label="Rudiment previews"></section><footer>Offsets are local to each bounded subject. Reduced-motion preferences start the lab paused.</footer></main><script type="module" src="app.js"></script></body></html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{color-scheme:dark;--ink:#f5efe4;--muted:#a9a29a;--gold:#e0b45b;--panel:#171819;--line:#343536;background:#0b0c0d;font-family:Inter,ui-sans-serif,system-ui,sans-serif}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 50% -10%,#282113 0,transparent 36rem),#0b0c0d;color:var(--ink)}main{max-width:1280px;margin:auto;padding:4rem 1.25rem}.hero{max-width:48rem}.eyebrow{color:var(--gold);font-size:.72rem;letter-spacing:.24em}.hero h1{font:700 clamp(3rem,9vw,7rem)/.85 Georgia,serif;margin:.18em 0}.hero>p:last-child{color:var(--muted);font-size:1.1rem}.controls{position:sticky;top:.75rem;z-index:2;display:flex;flex-wrap:wrap;gap:.7rem;align-items:center;padding:.8rem;margin:2.5rem 0;background:#111d;border:1px solid var(--line);border-radius:1rem;backdrop-filter:blur(14px)}button,input{accent-color:var(--gold)}button{border:1px solid #5a4c31;border-radius:.55rem;background:#292419;color:var(--ink);padding:.65rem 1rem;font-weight:700}.controls label{display:flex;gap:.45rem;align-items:center;color:var(--muted);font-size:.82rem}.controls input[type=number]{width:4.6rem;background:#080909;border:1px solid var(--line);color:var(--ink);padding:.45rem}.pip-control{flex:1;min-width:14rem}.pip-control input{flex:1}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(270px,1fr));gap:1rem}.card{overflow:hidden;background:linear-gradient(145deg,#1b1c1d,#111213);border:1px solid var(--line);border-radius:1rem;padding:1rem}.card header{display:flex;justify-content:space-between;gap:1rem;align-items:start}.card h2{margin:0;font-size:1rem;text-transform:capitalize}.card header span,.card p,.card output{color:var(--muted);font-size:.75rem}.card p{min-height:2.3em}.stage{position:relative;height:160px;overflow:hidden;border:1px solid #292a2b;border-radius:.7rem;background:radial-gradient(circle,#202124,#111214 70%)}.axes{position:absolute;inset:0;background:linear-gradient(90deg,transparent calc(50% - .5px),#ffffff12 50%,transparent calc(50% + .5px)),linear-gradient(transparent calc(50% - .5px),#ffffff12 50%,transparent calc(50% + .5px))}.subject{position:absolute;z-index:2;left:calc(50% - 9px);top:calc(50% - 9px);width:18px;height:18px;border-radius:50%;background:var(--gold);box-shadow:0 0 18px #e0b45b88;will-change:transform}.trail{position:absolute;inset:0;width:100%;height:100%}.trail polyline{fill:none;stroke:#e0b45b44;stroke-width:1.2}.card output{display:block;margin-top:.75rem;font-family:ui-monospace,monospace}footer{color:var(--muted);margin-top:2rem;font-size:.78rem}@media(prefers-reduced-motion:reduce){.subject{will-change:auto}}@media(max-width:600px){main{padding:2rem .8rem}.controls{top:.4rem}}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kieransimkin/dance-rudiments",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Integer-pip rhythmic position functions with C++/WASM bindings.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/typescript/index.js",
|
|
8
|
+
"types": "dist/typescript/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/typescript/index.d.ts",
|
|
12
|
+
"import": "./dist/typescript/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./wasm": "./dist/wasm/dancerudiments.js"
|
|
15
|
+
},
|
|
16
|
+
"files": ["dist/typescript/", "dist/wasm/", "harness/", "README.md", "LICENSE"],
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/kieransimkin/DanceRudiments.git"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/kieransimkin/DanceRudiments#readme",
|
|
22
|
+
"bugs": "https://github.com/kieransimkin/DanceRudiments/issues",
|
|
23
|
+
"publishConfig": { "access": "public", "provenance": true },
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build:harness": "tsc -p tsconfig.json",
|
|
26
|
+
"test": "npm run build:harness && node --test --test-isolation=none tests/typescript/*.test.js"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": { "typescript": "^5.9.0" }
|
|
29
|
+
}
|