Skip to content

Commit

Permalink
Merge pull request #232 from stringsync/cursor
Browse files Browse the repository at this point in the history
Create playback cursor
  • Loading branch information
jaredjj3 authored Jul 15, 2024
2 parents 2712364 + ab233e5 commit d8504dd
Show file tree
Hide file tree
Showing 55 changed files with 105,335 additions and 132 deletions.
6 changes: 6 additions & 0 deletions PuppeteerEnvironment.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import os from 'os';
const DIR = path.join(os.tmpdir(), 'jest_puppeteer_global_setup');

export default class PuppeteerEnvironment extends TestEnvironment {
constructor(...args) {
super(...args);

this.global.structuredClone = globalThis.structuredClone;
}

async setup() {
await super.setup();
// get the wsEndpoint
Expand Down
1 change: 0 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ export default {
testTimeout: 30000,
moduleNameMapper: {
'@/(.*)': '<rootDir>/src/$1',
'^vexflow$': '<rootDir>/node_modules/vxflw-early-access',
},
reporters: ['default', 'jest-image-snapshot/src/outdated-snapshot-reporter.js'],
};
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
},
"dependencies": {
"jszip": "3.10.1",
"vxflw-early-access": "5.0.0-alpha.11"
"vexflow": "5.0.0-alpha.4"
},
"devDependencies": {
"@babel/core": "^7.17.8",
Expand Down Expand Up @@ -64,7 +64,7 @@
"eslint-plugin-react-refresh": "^0.4.4",
"jest": "29.6.4",
"jest-diff": "^27.5.1",
"jest-environment-jsdom": "29.6.4",
"jest-environment-jsdom": "29.7.0",
"jest-extended": "^2.0.0",
"jest-image-snapshot": "^6.2.0",
"jquery": "^3.7.1",
Expand Down
48 changes: 45 additions & 3 deletions site/src/components/ConfigForm.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as vexml from '@/index';
import { CSSProperties, useId, useRef, useState } from 'react';
import { useTooltip } from '../hooks/useTooltip';

export type ConfigFormProps = {
defaultValue?: vexml.Config;
Expand Down Expand Up @@ -61,6 +62,7 @@ export const ConfigForm = (props: ConfigFormProps) => {
label={label}
value={get<string>(key)}
defaultValue={descriptor.defaultValue}
help={descriptor.help}
onChange={set<string>(key)}
/>
);
Expand All @@ -71,6 +73,7 @@ export const ConfigForm = (props: ConfigFormProps) => {
label={label}
value={get<number>(key)}
defaultValue={descriptor.defaultValue}
help={descriptor.help}
onChange={set<number>(key)}
/>
);
Expand All @@ -80,6 +83,7 @@ export const ConfigForm = (props: ConfigFormProps) => {
key={key}
label={label}
value={get<boolean>(key)}
help={descriptor.help}
onChange={set<boolean>(key, { immediate: true })}
/>
);
Expand All @@ -91,6 +95,7 @@ export const ConfigForm = (props: ConfigFormProps) => {
value={get<string>(key)}
defaultValue={descriptor.defaultValue}
choices={descriptor.choices}
help={descriptor.help}
onChange={set<string>(key, { immediate: true })}
/>
);
Expand Down Expand Up @@ -130,14 +135,26 @@ export const ConfigForm = (props: ConfigFormProps) => {
);
};

const StringInput = (props: { label: string; value: string; defaultValue: string; onChange(value: string): void }) => {
const StringInput = (props: {
label: string;
value: string;
defaultValue: string;
help: string;
onChange(value: string): void;
}) => {
const id = useId();

const tooltipRef = useRef<HTMLElement>(null);
useTooltip(tooltipRef, 'top', props.help);

return (
<div>
<label htmlFor={id} className="form-label">
{props.label}
</label>
<small ref={tooltipRef} className="ms-2">
<i className="bi bi-question-circle"></i>
</small>
<div className="input-group">
<input id={id} className="form-control" value={props.value} onChange={(e) => props.onChange(e.target.value)} />
<button
Expand All @@ -153,14 +170,26 @@ const StringInput = (props: { label: string; value: string; defaultValue: string
);
};

const NumberInput = (props: { label: string; value: number; defaultValue: number; onChange(value: number): void }) => {
const NumberInput = (props: {
label: string;
value: number;
defaultValue: number;
help: string;
onChange(value: number): void;
}) => {
const id = useId();

const tooltipRef = useRef<HTMLElement>(null);
useTooltip(tooltipRef, 'top', props.help);

return (
<div>
<label htmlFor={id} className="form-label">
{props.label}
</label>
<small ref={tooltipRef} className="ms-2">
<i className="bi bi-question-circle"></i>
</small>
<div className="d-flex align-items-center">
<input
id={id}
Expand All @@ -187,9 +216,12 @@ const NumberInput = (props: { label: string; value: number; defaultValue: number
);
};

const BooleanInput = (props: { label: string; value: boolean; onChange(value: boolean): void }) => {
const BooleanInput = (props: { label: string; value: boolean; help: string; onChange(value: boolean): void }) => {
const id = useId();

const tooltipRef = useRef<HTMLElement>(null);
useTooltip(tooltipRef, 'top', props.help);

return (
<div className="form-check">
<input
Expand All @@ -202,6 +234,9 @@ const BooleanInput = (props: { label: string; value: boolean; onChange(value: bo
<label htmlFor={id} className="form-check-label">
{props.label}
</label>
<small ref={tooltipRef} className="ms-2">
<i className="bi bi-question-circle"></i>
</small>
</div>
);
};
Expand All @@ -211,15 +246,22 @@ const EnumInput = (props: {
value: string;
defaultValue: string;
choices: readonly string[];
help: string;
onChange(value: string): void;
}) => {
const id = useId();

const tooltipRef = useRef<HTMLElement>(null);
useTooltip(tooltipRef, 'top', props.help);

return (
<div>
<label htmlFor={id} className="mb-2">
{props.label}
</label>
<small ref={tooltipRef} className="ms-2">
<i className="bi bi-question-circle"></i>
</small>
<div className="input-group">
<select id={id} className="form-select" value={props.value} onChange={(e) => props.onChange(e.target.value)}>
{props.choices.map((choice) => (
Expand Down
1 change: 0 additions & 1 deletion site/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export default defineConfig({
preserveSymlinks: true,
alias: {
'@': path.resolve(__dirname, '..', 'src'),
vexflow: path.resolve(__dirname, '..', 'node_modules', 'vxflw-early-access'),
},
},
});
1 change: 1 addition & 0 deletions src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- [events](./events/README.md): Exposes user interaction hooks.
- [cursors](./cursors/README.md): Different utilities for navigating a rendered music sheet.
- [components](./components/README.md): Creates UI components needed for vexml.
- [playback](./playback/README.md): Provide data structures for playing a rendering.
- `util`: Miscellaneous functionality that doesn't neatly fit into either library or needs to be shared.

[src/vexml.ts](./vexml.ts) is the entrypoint for MusicXML rendering.
82 changes: 67 additions & 15 deletions src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,73 @@ export type Schema = typeof CONFIG_SCHEMA;
export type Config = SchemaConfig<Schema>;

export const CONFIG_SCHEMA = {
DEFAULT_SYSTEM_DISTANCE: t.number(80),
DEFAULT_STAVE_DISTANCE: t.number(140),
TITLE_TOP_PADDING: t.number(40),
TITLE_FONT_FAMILY: t.string('Arial'),
TITLE_FONT_SIZE: t.string('36px'),
REHEARSAL_FONT_FAMILY: t.string('Times New Roman'),
REHEARSAL_FONT_SIZE: t.string('16px'),
PART_NAME_FONT_FAMILY: t.string('Arial'),
PART_NAME_FONT_SIZE: t.string('13px'),
PART_DISTANCE: t.number(80),
VOICE_PADDING: t.number(80),
MULTI_MEASURE_REST_WIDTH: t.number(200),
ENABLE_MEASURE_NUMBERS: t.boolean(true),
INPUT_TYPE: t.enum(['auto', 'none', 'mouse', 'touch', 'hybrid'] as const),
DEBUG_DRAW_TARGET_BOUNDS: t.boolean(false),
DEFAULT_SYSTEM_DISTANCE: t.number({
defaultValue: 80,
help:
'DEFAULT_SYSTEM_DISTANCE is the vertical distance between systems.' +
"It won't have an effect if there is only one system.",
}),
DEFAULT_STAVE_DISTANCE: t.number({
defaultValue: 140,
help:
'DEFAULT_STAVE_DISTANCE is the vertical distance between staves within the same part and system. ' +
"It won't have an effect if there is only one stave per part.",
}),
TITLE_TOP_PADDING: t.number({
defaultValue: 40,
help: 'TITLE_TOP_PADDING is the vertical distance between the title and the first system.',
}),
TITLE_FONT_FAMILY: t.string({
defaultValue: 'Arial',
help: 'TITLE_FONT_FAMILY is the font family for the title.',
}),
TITLE_FONT_SIZE: t.string({
defaultValue: '36px',
help: 'TITLE_FONT_SIZE is the font size for the title expressed in browser-compatible units.',
}),
REHEARSAL_FONT_FAMILY: t.string({
defaultValue: 'Times New Roman',
help: 'REHEARSAL_FONT_FAMILY is the font family for rehearsal marks.',
}),
REHEARSAL_FONT_SIZE: t.string({
defaultValue: '16px',
help: 'REHEARSAL_FONT_SIZE is the font size for rehearsal marks expressed in browser-compatible units.',
}),
PART_NAME_FONT_FAMILY: t.string({
defaultValue: 'Arial',
help: 'PART_NAME_FONT_FAMILY is the font family for part names.',
}),
PART_NAME_FONT_SIZE: t.string({
defaultValue: '13px',
help: 'PART_NAME_FONT_SIZE is the font size for part names expressed in browser-compatible units.',
}),
PART_DISTANCE: t.number({
defaultValue: 80,
help:
'PART_DISTANCE is the vertical distance between parts of a system. ' +
"It won't have an effect if there is only one part per system.",
}),
VOICE_PADDING: t.number({
defaultValue: 80,
help: 'VOICE_PADDING is how much extra width to give each voice in a measure.',
}),
MULTI_MEASURE_REST_WIDTH: t.number({
defaultValue: 200,
help: 'MULTI_MEASURE_REST_WIDTH is the width of multi-measure rests.',
}),
ENABLE_MEASURE_NUMBERS: t.boolean({
defaultValue: true,
help: 'ENABLE_MEASURE_NUMBERS enables measure numbers to be displayed.',
}),
INPUT_TYPE: t.enum({
choices: ['auto', 'none', 'mouse', 'touch', 'hybrid'] as const,
defaultValue: 'auto',
help: 'INPUT_TYPE is the type of inputs to use for interaction.',
}),
DEBUG_DRAW_TARGET_BOUNDS: t.boolean({
defaultValue: false,
help: 'DEBUG_DRAW_TARGET_BOUNDS enables drawing of target bounds for debugging.',
}),
} as const;

export const DEFAULT_CONFIG = t.defaultConfig(CONFIG_SCHEMA);
29 changes: 19 additions & 10 deletions src/config/t.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,24 +44,33 @@ export class t {
return config;
}

static string(defaultValue: string = '') {
return { type: 'string', defaultValue } as const;
static string(opts: { defaultValue: string; help: string }) {
const defaultValue = opts.defaultValue;
const help = opts.help;
return { type: 'string', defaultValue, help } as const;
}

static number(defaultValue: number = 0) {
return { type: 'number', defaultValue } as const;
static number(opts: { defaultValue: number; help: string }) {
const defaultValue = opts.defaultValue;
const help = opts.help;
return { type: 'number', defaultValue, help } as const;
}

static boolean(defaultValue: boolean = false) {
return { type: 'boolean', defaultValue } as const;
static boolean(opts: { defaultValue: boolean; help: string }) {
const defaultValue = opts.defaultValue;
const help = opts.help;
return { type: 'boolean', defaultValue, help } as const;
}

static enum<T extends readonly [string, ...string[]]>(choices: T, defaultValue = choices[0]) {
return { type: 'enum', choices, defaultValue } as const;
static enum<T extends readonly [string, ...string[]]>(opts: { choices: T; defaultValue: T[0]; help: string }) {
const defaultValue = opts.defaultValue;
const help = opts.help;
return { type: 'enum', choices: opts.choices, defaultValue, help } as const;
}

static debug<T extends TerminalSchemaDescriptor>(child: T) {
return { type: 'debug', child } as const;
static debug<T extends TerminalSchemaDescriptor>(opts: { child: T; help: string }) {
const help = opts.help;
return { type: 'debug', child: opts.child, help } as const;
}

private constructor() {
Expand Down
17 changes: 17 additions & 0 deletions src/playback/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# playback

## Intent

The intent of this module is to provide data structures that are useful for playback.

### Goals

- **DO** House the logic for constructing valid playback sequences.
- **DO** Provide data structures to discretely navigate a rendering.
- **DO** Expose tools to convert between ticks and time.

### Non-goals

- **DO NOT** Contain cursor-specific implementations.
- **DO NOT** Provide the machinery to interpolate between playback steps.
- **DO NOT** Commit a sequence to a prescribed bpm.
48 changes: 48 additions & 0 deletions src/playback/duration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Conversions to milliseconds
const ms = (v: number) => v;
const sec = (v: number) => ms(v * 1000);
const min = (v: number) => sec(v * 60);

export class Duration {
static zero() {
return new Duration(0);
}

static ms(v: number) {
return new Duration(ms(v));
}

static sec(v: number) {
return new Duration(sec(v));
}

static minutes(v: number) {
return new Duration(min(v));
}

private readonly _ms: number;

private constructor(ms: number) {
this._ms = ms;
}

eq(duration: Duration) {
return this.ms === duration.ms;
}

plus(duration: Duration) {
return Duration.ms(duration.ms + this.ms);
}

get ms() {
return this._ms;
}

get sec() {
return this.ms / 1000;
}

get minutes() {
return this.sec / 60;
}
}
1 change: 1 addition & 0 deletions src/playback/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './sequence';
Loading

0 comments on commit d8504dd

Please sign in to comment.