Last active
October 23, 2025 23:34
-
-
Save joshuadutton/c6418a15bf3f51d97590f48ae63eeff1 to your computer and use it in GitHub Desktop.
Vexflow React Native Component using Expo Dom Components
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import VexflowComponent, { | |
| NoteConfig, | |
| StaveConfig, | |
| } from "@/components/vexflow-component"; | |
| import React from "react"; | |
| import { View } from "react-native"; | |
| const staveConfig: StaveConfig = { | |
| x: 10, | |
| y: 0, | |
| width: 190, | |
| clef: { | |
| name: "treble", | |
| }, | |
| timeSignature: { | |
| timeSpec: "4/4", | |
| }, | |
| }; | |
| const noteConfigs: NoteConfig[] = [ | |
| { keys: ["c/4"], duration: "q" }, | |
| { keys: ["d/4"], duration: "q" }, | |
| { keys: ["b/4"], duration: "qr" }, | |
| { keys: ["c/4", "e/4", "g/4"], duration: "q" }, | |
| ]; | |
| export default function HomeScreen() { | |
| return ( | |
| <View style={{ flex: 1 }}> | |
| <VexflowComponent | |
| width={200} | |
| height={200} | |
| staveConfig={staveConfig} | |
| noteConfigs={noteConfigs} | |
| onRender={(svg) => console.log(svg)} | |
| /> | |
| </View> | |
| ); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { FontInfo } from "vexflow"; | |
| import VexflowDOMComponent from "./vexflow-dom-component"; | |
| import { NoteConfig, StaveConfig } from "./vexflow-helpers"; | |
| export interface VexflowComponentProps { | |
| width: number; | |
| height: number; | |
| staveConfig: StaveConfig; | |
| noteConfigs: NoteConfig[]; | |
| font?: string | FontInfo; | |
| fontSize?: number; | |
| onRender?: (svg: string) => void; | |
| } | |
| export * from "./vexflow-helpers"; | |
| export default function VexflowComponent({ | |
| width, | |
| height, | |
| staveConfig, | |
| noteConfigs, | |
| font = "Arial", | |
| fontSize = 10, | |
| onRender, | |
| }: VexflowComponentProps) { | |
| return ( | |
| <VexflowDOMComponent | |
| width={width} | |
| height={height} | |
| staveConfig={staveConfig} | |
| noteConfigs={noteConfigs} | |
| font={font} | |
| fontSize={fontSize} | |
| onRender={onRender} | |
| dom={{ style: { width, height } }} | |
| /> | |
| ); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Uses Expo DOM Components https://docs.expo.dev/guides/dom-components/ | |
| // Needs serializable props, so I created StaveConfig and NoteConfig to pass | |
| // to the component and to create the Vexflow objects. | |
| "use dom"; | |
| import { useEffect, useRef } from "react"; | |
| import VexFlow, { FontInfo, Formatter } from "vexflow"; | |
| import { | |
| NoteConfig, | |
| notesFromConfig, | |
| StaveConfig, | |
| staveFromConfig, | |
| } from "./vexflow-helpers"; | |
| export interface VexflowDOMComponentProps { | |
| width: number; | |
| height: number; | |
| staveConfig: StaveConfig; | |
| noteConfigs: NoteConfig[]; | |
| dom: import("expo/dom").DOMProps; | |
| font?: string | FontInfo; | |
| fontSize?: number; | |
| onRender?: (svg: string) => void; | |
| } | |
| export default function DOMComponent({ | |
| width, | |
| height, | |
| staveConfig, | |
| noteConfigs, | |
| font, | |
| fontSize, | |
| onRender, | |
| }: VexflowDOMComponentProps) { | |
| const divRef = useRef<HTMLDivElement>(null); | |
| useEffect(() => { | |
| const div = divRef.current; | |
| if (div) { | |
| const width = div.clientWidth; | |
| const height = div.clientHeight; | |
| const renderer = new VexFlow.Renderer(div, VexFlow.Renderer.Backends.SVG); | |
| renderer.resize(width, height); | |
| const context = renderer.getContext(); | |
| if (font && fontSize) { | |
| context.setFont(font, fontSize); | |
| } | |
| const stave = staveFromConfig(staveConfig, context); | |
| stave.setContext(context).draw(); | |
| const notes = notesFromConfig(noteConfigs); | |
| Formatter.FormatAndDraw(context, stave, notes); | |
| const svgElement = div.firstChild as SVGSVGElement; | |
| const svgString = svgElement?.outerHTML; | |
| if (svgString) { | |
| onRender?.(svgString); | |
| } | |
| } | |
| }, [font, fontSize, staveConfig, noteConfigs, onRender]); | |
| return <div ref={divRef} style={{ width, height }}></div>; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { | |
| ElementStyle, | |
| RenderContext, | |
| Stave, | |
| StaveNote, | |
| StaveOptions, | |
| } from "vexflow"; | |
| export interface StaveConfig { | |
| x: number; | |
| y: number; | |
| width: number; | |
| options?: StaveOptions; | |
| style?: ElementStyle; | |
| clef?: { | |
| name: string; | |
| size?: string; | |
| annotation?: string; | |
| position?: number; | |
| }; | |
| endClef?: { | |
| name: string; | |
| size?: string; | |
| annotation?: string; | |
| }; | |
| keySignature?: { | |
| keySpec: string; | |
| cancelKeySpec?: string; | |
| position?: number; | |
| }; | |
| timeSignature?: { | |
| timeSpec: string; | |
| customPadding?: number; | |
| position?: number; | |
| }; | |
| endTimeSignature?: { | |
| timeSpec: string; | |
| customPadding?: number; | |
| }; | |
| } | |
| export interface NoteConfig { | |
| /** Array of pitches, e.g: `['c/4', 'e/4', 'g/4']` */ | |
| keys?: string[]; | |
| /** The time length (e.g., `q` for quarter, `h` for half, `8` for eighth etc.). */ | |
| duration: string; | |
| line?: number; | |
| /** The number of dots, which affects the duration. */ | |
| dots?: number; | |
| /** The note type (e.g., `r` for rest, `s` for slash notes, etc.). */ | |
| type?: string; | |
| alignCenter?: boolean; | |
| style?: ElementStyle; | |
| stemStyle?: ElementStyle; | |
| ledgerLineStyle?: ElementStyle; | |
| } | |
| export function staveFromConfig( | |
| config: StaveConfig, | |
| context: RenderContext | |
| ): Stave { | |
| const stave = new Stave(config.x, config.y, config.width, config.options); | |
| if (config.style) { | |
| stave.applyStyle(context, config.style); | |
| } | |
| if (config.clef) { | |
| stave.addClef(config.clef.name, config.clef.size, config.clef.annotation); | |
| } | |
| if (config.timeSignature) { | |
| stave.addTimeSignature( | |
| config.timeSignature.timeSpec, | |
| config.timeSignature.customPadding | |
| ); | |
| } | |
| if (config.keySignature) { | |
| stave.addKeySignature( | |
| config.keySignature.keySpec, | |
| config.keySignature.cancelKeySpec, | |
| config.keySignature.position | |
| ); | |
| } | |
| if (config.endClef) { | |
| stave.addEndClef( | |
| config.endClef.name, | |
| config.endClef.size, | |
| config.endClef.annotation | |
| ); | |
| } | |
| if (config.endTimeSignature) { | |
| stave.addEndTimeSignature( | |
| config.endTimeSignature.timeSpec, | |
| config.endTimeSignature.customPadding | |
| ); | |
| } | |
| return stave; | |
| } | |
| export function notesFromConfig(configs: NoteConfig[]): StaveNote[] { | |
| return configs.map((config) => { | |
| const note = new StaveNote({ | |
| keys: config.keys, | |
| duration: config.duration, | |
| line: config.line, | |
| dots: config.dots, | |
| type: config.type, | |
| }); | |
| if (config.style) { | |
| note.setStyle(config.style); | |
| } | |
| if (config.stemStyle) { | |
| note.setStemStyle(config.stemStyle); | |
| } | |
| if (config.ledgerLineStyle) { | |
| note.setLedgerLineStyle(config.ledgerLineStyle); | |
| } | |
| return note; | |
| }); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment