strudel-docker/website/src/docs/MicroRepl.jsx
2023-12-16 16:19:18 +01:00

165 lines
5.1 KiB
JavaScript

import { useState, useRef, useCallback, useMemo } from 'react';
import { Icon } from './Icon';
import { silence, getPunchcardPainter } from '@strudel.cycles/core';
import { transpiler } from '@strudel.cycles/transpiler';
import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio';
import { StrudelMirror } from '@strudel/codemirror';
import { prebake } from '@strudel/repl';
import { useInView } from 'react-hook-inview';
const initialSettings = {
keybindings: 'strudelTheme',
isLineNumbersDisplayed: true,
isActiveLineHighlighted: false,
isAutoCompletionEnabled: false,
isPatternHighlightingEnabled: true,
isFlashEnabled: true,
isTooltipEnabled: false,
isLineWrappingEnabled: false,
theme: 'strudelTheme',
fontFamily: 'monospace',
fontSize: 18,
};
export function MicroRepl({
code,
hideHeader = false,
canvasHeight = 200,
onTrigger,
onPaint,
punchcard,
punchcardLabels = true,
}) {
const id = useMemo(() => s4(), []);
const canvasId = useMemo(() => `canvas-${id}`, [id]);
const shouldDraw = !!punchcard;
const init = useCallback(({ code, shouldDraw }) => {
const drawTime = [-2, 2];
const drawContext = shouldDraw ? document.querySelector('#' + canvasId)?.getContext('2d') : null;
let onDraw;
if (shouldDraw) {
onDraw = (haps, time, frame, painters) => {
painters.length && drawContext.clearRect(0, 0, drawContext.canvas.width * 2, drawContext.canvas.height * 2);
painters?.forEach((painter) => {
// ctx time haps drawTime paintOptions
painter(drawContext, time, haps, drawTime, { clear: false });
});
};
}
const editor = new StrudelMirror({
defaultOutput: webaudioOutput,
getTime: () => getAudioContext().currentTime,
transpiler,
autodraw: !!shouldDraw,
root: containerRef.current,
initialCode: '// LOADING',
pattern: silence,
settings: initialSettings,
drawTime,
onDraw,
editPattern: (pat, id) => {
if (onTrigger) {
pat = pat.onTrigger(onTrigger, false);
}
if (onPaint) {
editor.painters.push(onPaint);
} else if (punchcard) {
editor.painters.push(getPunchcardPainter({ labels: !!punchcardLabels }));
}
return pat;
},
prebake,
onUpdateState: (state) => {
setReplState({ ...state });
},
});
// init settings
editor.updateSettings(initialSettings);
editor.setCode(code);
editorRef.current = editor;
}, []);
const [ref, isVisible] = useInView({
threshold: 0.01,
onEnter: () => {
if (!editorRef.current) {
init({ code, shouldDraw });
}
},
});
const [replState, setReplState] = useState({});
const { started, isDirty, error } = replState;
const editorRef = useRef();
const containerRef = useRef();
return (
<div className="overflow-hidden rounded-t-md bg-background border border-lineHighlight" ref={ref}>
{!hideHeader && (
<div className="flex justify-between bg-lineHighlight">
<div className="flex">
<button
className={cx(
'cursor-pointer w-16 flex items-center justify-center p-1 border-r border-lineHighlight text-foreground bg-lineHighlight hover:bg-background',
started ? 'animate-pulse' : '',
)}
onClick={() => editorRef.current?.toggle()}
>
<Icon type={started ? 'stop' : 'play'} />
</button>
<button
className={cx(
'w-16 flex items-center justify-center p-1 text-foreground border-lineHighlight bg-lineHighlight',
isDirty ? 'text-foreground hover:bg-background cursor-pointer' : 'opacity-50 cursor-not-allowed',
)}
onClick={() => editorRef.current?.evaluate()}
>
<Icon type="refresh" />
</button>
</div>
</div>
)}
<div className="overflow-auto relative p-1">
<div ref={containerRef}></div>
{error && <div className="text-right p-1 text-md text-red-200">{error.message}</div>}
</div>
{shouldDraw && (
<canvas
id={canvasId}
className="w-full pointer-events-none border-t border-lineHighlight"
height={canvasHeight}
ref={(el) => {
if (el && el.width !== el.clientWidth) {
el.width = el.clientWidth;
}
//const ratio = el.clientWidth / canvasHeight;
//const targetWidth = Math.round(el.width * ratio);
//if (el.width !== targetWidth) {
// el.width = targetWidth;
//}
}}
></canvas>
)}
{/* !!log.length && (
<div className="bg-gray-800 rounded-md p-2">
{log.map(({ message }, i) => (
<div key={i}>{message}</div>
))}
</div>
) */}
</div>
);
}
function cx(...classes) {
// : Array<string | undefined>
return classes.filter(Boolean).join(' ');
}
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}