(null);
43 |
44 | useEffect(() => {
45 | if (!canvasRef.current) return;
46 |
47 | const canvasEl = canvasRef.current;
48 |
49 | if (!tokenRef.current) {
50 | const context = canvasEl.getContext("webgl");
51 | if (!context) throw Error("WebGL not supported");
52 | tokenRef.current = new Token({
53 | gl: context,
54 | sideLength,
55 | });
56 | }
57 |
58 | const inputCallback = (e: MouseEvent | TouchEvent): void => {
59 | mouseInputEnabled = e.type === "mousedown" ? true : mouseInputEnabled;
60 | if (e instanceof window.MouseEvent && !mouseInputEnabled) return;
61 | const currentXYRatios = eventRatiosAndCoords(e);
62 | dispatch(
63 | controlPadSlice.actions.setCurrentCoordinateRatios({
64 | x: currentXYRatios.xRatio,
65 | y: currentXYRatios.yRatio,
66 | }),
67 | );
68 | tokenRef.current?.handleInput(currentXYRatios);
69 | };
70 |
71 | const inputEndCallback = () => {
72 | mouseInputEnabled = false;
73 | dispatch(controlPadSlice.actions.setCurrentCoordinateRatios(undefined));
74 | tokenRef.current?.handleInputEnd();
75 | };
76 |
77 | canvasEl.addEventListener("touchstart", inputCallback, { passive: false });
78 | canvasEl.addEventListener("touchmove", inputCallback, { passive: false });
79 | canvasEl.addEventListener("mousedown", inputCallback, { passive: false });
80 | canvasEl.addEventListener("mousemove", inputCallback, { passive: false });
81 | canvasEl.addEventListener("touchend", inputEndCallback, { passive: false });
82 | canvasEl.addEventListener("mouseup", inputEndCallback, { passive: false });
83 |
84 | canvasEl.oncontextmenu = (e) => e.preventDefault();
85 |
86 | return () => {
87 | canvasEl.removeEventListener("touchstart", inputCallback);
88 | canvasEl.removeEventListener("touchmove", inputCallback);
89 | canvasEl.removeEventListener("mousedown", inputCallback);
90 | canvasEl.removeEventListener("mousemove", inputCallback);
91 | canvasEl.removeEventListener("touchend", inputEndCallback);
92 | canvasEl.removeEventListener("mouseup", inputEndCallback);
93 | canvasEl.oncontextmenu = null;
94 | };
95 | }, []);
96 |
97 | useEffect(() => {
98 | tokenRef.current?.handleResize(sideLength);
99 | }, [sideLength]);
100 |
101 | return (
102 |
103 | {!hasBeenTouched && (
104 |
TOUCH / CLICK TO PLAY
105 | )}
106 |
112 |
113 | );
114 | }
115 |
--------------------------------------------------------------------------------
/src/components/organisms/ControlPad/style.css:
--------------------------------------------------------------------------------
1 | .ControlPad {
2 | cursor: crosshair;
3 | position: relative;
4 | }
5 |
6 | .ControlPad__Canvas {
7 | background: radial-gradient(
8 | circle,
9 | var(--blue-60) 0%,
10 | var(--gray-00) 17.5%,
11 | var(--gray-00) 91.67%,
12 | var(--blue-20) 100%
13 | );
14 | }
15 |
16 | .ControlPad__Message {
17 | align-items: center;
18 | animation-direction: alternate;
19 | animation-duration: var(--transition-slow);
20 | animation-iteration-count: infinite;
21 | animation-name: pulse;
22 | animation-timing-function: ease-out;
23 | bottom: 0;
24 | color: var(--blue-90);
25 | display: flex;
26 | font-size: 1.25rem;
27 | font-weight: bold;
28 | justify-content: center;
29 | left: 0;
30 | pointer-events: none;
31 | position: absolute;
32 | right: 0;
33 | text-shadow: var(--blue-10) 0.1rem 0.1rem;
34 | top: 0;
35 | transition: opacity 1s;
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/organisms/ControlPad/vertex_shader.glsl:
--------------------------------------------------------------------------------
1 | attribute vec4 a_position;
2 | attribute float a_color;
3 |
4 | uniform mat4 u_matrix;
5 |
6 | varying float v_color;
7 |
8 | mat4 zToW = mat4(
9 | 1., 0., 0., 0.,
10 | 0., 1., 0., 0.,
11 | 0., 0., 1., 0.75,
12 | 0., 0., 0., 1.
13 | );
14 |
15 | void main() {
16 | gl_Position = (zToW * u_matrix) * (a_position * vec4(32.0, 32.0, 32.0, 1.0));
17 | v_color = a_color;
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/organisms/Navigation.css:
--------------------------------------------------------------------------------
1 | .Navigation {
2 | display: flex;
3 | flex-direction: row;
4 | justify-content: center;
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/organisms/Navigation.tsx:
--------------------------------------------------------------------------------
1 | import HollowButton from "../atoms/HollowButton";
2 | import { NAV } from "../../constants";
3 |
4 | export default function Navigator() {
5 | return (
6 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/organisms/spinner.css:
--------------------------------------------------------------------------------
1 | .spinner {
2 | height: 8rem;
3 | margin: 8rem auto;
4 | position: relative;
5 | width: 8rem;
6 | }
7 |
8 | .spinner_el {
9 | height: 100%;
10 | left: 0;
11 | position: absolute;
12 | top: 0;
13 | width: 100%;
14 | }
15 |
16 | .spinner_el:before {
17 | animation: spinner-el-fade-delay 1.2s infinite ease-in-out both;
18 | background-color: var(--green-80);
19 | content: "";
20 | display: block;
21 | height: 5%;
22 | margin: 0 auto;
23 | width: 15%;
24 | }
25 |
26 | .spinner_el2 {
27 | transform: rotate(30deg);
28 | }
29 | .spinner_el3 {
30 | transform: rotate(60deg);
31 | }
32 | .spinner_el4 {
33 | transform: rotate(90deg);
34 | }
35 | .spinner_el5 {
36 | transform: rotate(120deg);
37 | }
38 | .spinner_el6 {
39 | transform: rotate(150deg);
40 | }
41 | .spinner_el7 {
42 | transform: rotate(180deg);
43 | }
44 | .spinner_el8 {
45 | transform: rotate(210deg);
46 | }
47 | .spinner_el9 {
48 | transform: rotate(240deg);
49 | }
50 | .spinner_el10 {
51 | transform: rotate(270deg);
52 | }
53 | .spinner_el11 {
54 | transform: rotate(300deg);
55 | }
56 | .spinner_el12 {
57 | transform: rotate(330deg);
58 | }
59 | .spinner_el2:before {
60 | animation-delay: -1.1s;
61 | }
62 | .spinner_el3:before {
63 | animation-delay: -1s;
64 | }
65 | .spinner_el4:before {
66 | animation-delay: -0.9s;
67 | }
68 | .spinner_el5:before {
69 | animation-delay: -0.8s;
70 | }
71 | .spinner_el6:before {
72 | animation-delay: -0.7s;
73 | }
74 | .spinner_el7:before {
75 | animation-delay: -0.6s;
76 | }
77 | .spinner_el8:before {
78 | animation-delay: -0.5s;
79 | }
80 | .spinner_el9:before {
81 | animation-delay: -0.4s;
82 | }
83 | .spinner_el10:before {
84 | animation-delay: -0.3s;
85 | }
86 | .spinner_el11:before {
87 | animation-delay: -0.2s;
88 | }
89 | .spinner_el12:before {
90 | animation-delay: -0.1s;
91 | }
92 |
93 | @keyframes spinner-el-fade-delay {
94 | 0%,
95 | 39%,
96 | 100% {
97 | opacity: 0;
98 | }
99 | 40% {
100 | opacity: 1;
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/components/organisms/table-center.css:
--------------------------------------------------------------------------------
1 | .table-center {
2 | margin-left: auto;
3 | margin-right: auto;
4 | }
5 |
--------------------------------------------------------------------------------
/src/components/pages/About/index.tsx:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 | import LinkExternal from "../../atoms/LinkExternal";
3 | import navSlice from "../../../store/navSlice";
4 |
5 | export default function About() {
6 | const lastDirection = useSelector(navSlice.selectors.lastDirection);
7 | return (
8 |
9 |
About
10 |
11 | Andromeda is a pluggable digital audio workstation built on open web
12 | technologies.
13 |
14 |
15 | All the code is open source and hosted on{" "}
16 |
17 | GitHub
18 |
19 | .
20 |
21 |
22 | If you would like to report a bug or request a feature you can raise an
23 | issue{" "}
24 |
25 | here
26 |
27 | .
28 |
29 |
Contributions, feedback and suggestions are all very welcome!
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/pages/About/style.css:
--------------------------------------------------------------------------------
1 | .About {
2 | color: var(--blue-90);
3 | margin: 1rem;
4 | text-align: justify;
5 | }
6 |
7 | @media (min-width: 32rem) {
8 | .About {
9 | margin-left: auto;
10 | margin-right: auto;
11 | max-width: 32rem;
12 | text-align: justify;
13 | }
14 | }
15 |
16 | .About__Title {
17 | font-weight: 500;
18 | text-align: center;
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/pages/AriadneSettings.tsx:
--------------------------------------------------------------------------------
1 | import { useDispatch, useSelector } from "react-redux";
2 | import { useNavigate } from "react-router-dom";
3 | import ButtonPrimary from "../atoms/ButtonPrimary";
4 | import ControlModule, { Range, Select } from "../organisms/ControlModule";
5 | import ariadneSlice from "../../store/ariadneSlice";
6 |
7 | export default function AriadneSettings() {
8 | const navigate = useNavigate();
9 | const dispatch = useDispatch();
10 | const carrierDetune = useSelector(ariadneSlice.selectors.carrierDetune);
11 | const carrierOscType = useSelector(ariadneSlice.selectors.carrierOscType);
12 | const masterGain = useSelector(ariadneSlice.selectors.masterGain);
13 | const masterPan = useSelector(ariadneSlice.selectors.masterPan);
14 | const modulatorDetune = useSelector(ariadneSlice.selectors.modulatorDetune);
15 | const modulatorOscType = useSelector(ariadneSlice.selectors.modulatorOscType);
16 | const modulatorRatio = useSelector(ariadneSlice.selectors.modulatorRatio);
17 |
18 | return (
19 | // TOOD: something about this reused classname...
20 |
21 |
22 |
Ariadne
23 |
24 |
29 | dispatch(
30 | ariadneSlice.actions.masterGainSet(
31 | Number(e.currentTarget.value),
32 | ),
33 | )
34 | }
35 | />
36 |
41 | dispatch(
42 | ariadneSlice.actions.masterPanSet(
43 | Number(e.currentTarget.value),
44 | ),
45 | )
46 | }
47 | />
48 |
64 |
80 |
86 | dispatch(
87 | ariadneSlice.actions.carrierDetuneSet(
88 | Number(e.currentTarget.value),
89 | ),
90 | )
91 | }
92 | />
93 |
99 | dispatch(
100 | ariadneSlice.actions.modulatorRatioSet(
101 | Number(e.currentTarget.value),
102 | ),
103 | )
104 | }
105 | />
106 |
112 | dispatch(
113 | ariadneSlice.actions.modulatorDetuneSet(
114 | Number(e.currentTarget.value),
115 | ),
116 | )
117 | }
118 | />
119 |
120 |
121 |
navigate(-1)}>OK
122 |
123 | );
124 | }
125 |
--------------------------------------------------------------------------------
/src/components/pages/ControlPadPage.css:
--------------------------------------------------------------------------------
1 | .ControlPadPage {
2 | align-items: center;
3 | display: flex;
4 | flex-direction: column;
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/pages/ControlPadPage.tsx:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 | import ControlPad from "../organisms/ControlPad";
3 | import ButtonPrimary from "../atoms/ButtonPrimary";
4 | import controlPadSlice from "../../store/controlPadSlice";
5 | import screenSlice from "../../store/screenSlice";
6 | import navSlice from "../../store/navSlice";
7 |
8 | export default function ControlPadPage() {
9 | const hasBeenTouched = useSelector(controlPadSlice.selectors.hasBeenTouched);
10 | const sideLength = useSelector(screenSlice.selectors.sideLength);
11 | const lastDirection = useSelector(navSlice.selectors.lastDirection);
12 |
13 | return (
14 |
15 |
16 |
17 |
18 |
19 | Options
20 |
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/pages/ControlPadSettings.css:
--------------------------------------------------------------------------------
1 | .ControlPadSettings {
2 | align-items: center;
3 | animation-duration: var(--transition-medium);
4 | animation-name: fade-in;
5 | display: flex;
6 | flex-direction: column;
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/pages/ControlPadSettings.tsx:
--------------------------------------------------------------------------------
1 | import { useDispatch, useSelector } from "react-redux";
2 | import ButtonPrimary from "../atoms/ButtonPrimary";
3 | import InputLabel from "../atoms/InputLabel";
4 | import CheckboxLabelled from "../molecules/CheckboxLabelled";
5 | import RangeLabelled from "../molecules/RangeLabelled";
6 | import InstrumentSelector from "../molecules/InstrumentSelector";
7 | import { capitalizeWords } from "../../utils/helpers";
8 | import controlPadSlice from "../../store/controlPadSlice";
9 | import noteNameFromPitch from "../../audioHelpers/noteNameFromPitch";
10 | import Selector from "../molecules/Selector";
11 | import { SCALES } from "../../constants";
12 | import { ScaleName } from "../../types";
13 |
14 | const isScaleName = (scaleName: string): scaleName is ScaleName =>
15 | Object.hasOwn(SCALES, scaleName);
16 |
17 | export default function ControlPadSettings() {
18 | const dispatch = useDispatch();
19 | const instrument = useSelector(controlPadSlice.selectors.instrument);
20 | const noScale = useSelector(controlPadSlice.selectors.noScale);
21 | const octave = useSelector(controlPadSlice.selectors.octave);
22 | const range = useSelector(controlPadSlice.selectors.range);
23 | const rootNote = useSelector(controlPadSlice.selectors.rootNote);
24 | const selectedScale = useSelector(controlPadSlice.selectors.selectedScale);
25 |
26 | return (
27 |
28 |
Control Pad Settings
29 |
{
32 | const { value } = e.currentTarget;
33 | if (value !== "Ariadne" && value !== "Prometheus")
34 | throw Error("Invalid instrument");
35 | dispatch(controlPadSlice.actions.instrumentSet(value));
36 | }}
37 | label="Instrument"
38 | />
39 |
43 | dispatch(
44 | controlPadSlice.actions.octaveSet(Number(e.currentTarget.value)),
45 | )
46 | }
47 | output={octave}
48 | value={octave}
49 | >
50 | Octave
51 |
52 |
56 | dispatch(
57 | controlPadSlice.actions.rangeSet(Number(e.currentTarget.value)),
58 | )
59 | }
60 | output={range}
61 | value={range}
62 | >
63 | Range
64 |
65 |
68 | dispatch(controlPadSlice.actions.noScaleSet(e.currentTarget.checked))
69 | }
70 | >
71 | No Scale
72 |
73 |
77 | dispatch(
78 | controlPadSlice.actions.rootNoteSet(Number(e.currentTarget.value)),
79 | )
80 | }
81 | output={noteNameFromPitch(rootNote)}
82 | value={rootNote}
83 | >
84 | Root Note
85 |
86 | {
89 | const { value } = e.currentTarget;
90 | if (!isScaleName(value)) throw Error("Invalid scale");
91 | dispatch(controlPadSlice.actions.selectedScaleSet(value));
92 | }}
93 | label="Scale"
94 | options={Object.keys(SCALES).map((value) => ({
95 | text: capitalizeWords(value),
96 | value,
97 | }))}
98 | />
99 |
100 |
101 | OK
102 |
103 |
104 | );
105 | }
106 |
--------------------------------------------------------------------------------
/src/components/pages/Instrument.css:
--------------------------------------------------------------------------------
1 | /* TODO: move this somewhere sensible */
2 | .Instrument {
3 | align-items: center;
4 | display: flex;
5 | flex-direction: column;
6 | }
7 |
--------------------------------------------------------------------------------
/src/components/pages/KeyboardSettings.css:
--------------------------------------------------------------------------------
1 | .KeyboardSettings {
2 | align-items: center;
3 | animation-duration: var(--transition-medium);
4 | animation-name: fade-in;
5 | display: flex;
6 | flex-direction: column;
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/pages/KeyboardSettings.tsx:
--------------------------------------------------------------------------------
1 | import InstrumentSelector from "../molecules/InstrumentSelector";
2 | import ButtonPrimary from "../atoms/ButtonPrimary";
3 | import InputLabel from "../atoms/InputLabel";
4 | import CheckboxLabelled from "../molecules/CheckboxLabelled";
5 | import RangeLabelled from "../molecules/RangeLabelled";
6 | import { useDispatch, useSelector } from "react-redux";
7 | import keyboardSlice from "../../store/keyboardSlice";
8 |
9 | export default function KeyboardSettings() {
10 | const dispatch = useDispatch();
11 | const instrument = useSelector(keyboardSlice.selectors.instrument);
12 | const monophonic = useSelector(keyboardSlice.selectors.monophonic);
13 | const octave = useSelector(keyboardSlice.selectors.octave);
14 | const volume = useSelector(keyboardSlice.selectors.volume);
15 |
16 | return (
17 |
18 |
Keyboard Settings
19 |
{
22 | if (
23 | e.currentTarget.value !== "Ariadne" &&
24 | e.currentTarget.value !== "Prometheus"
25 | )
26 | throw Error("Invalid instrument");
27 | dispatch(keyboardSlice.actions.instrumentSet(e.currentTarget.value));
28 | }}
29 | label="Instrument"
30 | />
31 |
35 | dispatch(
36 | keyboardSlice.actions.volumeSet(Number(e.currentTarget.value)),
37 | )
38 | }
39 | output={Math.round(volume * 100)}
40 | step={0.01}
41 | value={volume}
42 | >
43 | Volume
44 |
45 |
49 | dispatch(
50 | keyboardSlice.actions.octaveSet(Number(e.currentTarget.value)),
51 | )
52 | }
53 | output={octave}
54 | value={octave}
55 | >
56 | Octave
57 |
58 |
61 | dispatch(keyboardSlice.actions.monophonicSet(e.target.checked))
62 | }
63 | >
64 | Monophonic
65 |
66 |
67 |
68 | OK
69 |
70 |
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/src/components/pages/PrometheusSettings/ModuleFilter.tsx:
--------------------------------------------------------------------------------
1 | import ControlModule, { Range, Select } from "../../organisms/ControlModule";
2 | import { capitalizeFirst } from "../../../utils/helpers";
3 | import { useDispatch, useSelector } from "react-redux";
4 | import prometheusSlice from "../../../store/prometheusSlice";
5 |
6 | const paramsAll = ["frequency", "gain", "Q"];
7 | const paramsFrequencyGain = ["frequency", "gain"];
8 | const paramsFrequencyQ = ["frequency", "Q"];
9 |
10 | const typesToParams = {
11 | allpass: paramsFrequencyQ,
12 | bandpass: paramsFrequencyQ,
13 | highpass: paramsFrequencyQ,
14 | highshelf: paramsFrequencyGain,
15 | lowpass: paramsFrequencyQ,
16 | lowshelf: paramsFrequencyGain,
17 | notch: paramsFrequencyQ,
18 | peaking: paramsAll,
19 | };
20 |
21 | export default function ModuleFilter() {
22 | const { frequency, gain, Q, type } = useSelector(
23 | prometheusSlice.selectors.filter,
24 | );
25 | const dispatch = useDispatch();
26 |
27 | return (
28 |
29 |
46 | {typesToParams[type].map((param) =>
47 | param === "frequency" ? (
48 |
56 | dispatch(
57 | prometheusSlice.actions.filterFrequencySet(
58 | Math.exp(Number(e.currentTarget.value)),
59 | ),
60 | )
61 | }
62 | />
63 | ) : param === "gain" ? (
64 |
71 | dispatch(
72 | prometheusSlice.actions.filterGainSet(
73 | Number(e.currentTarget.value),
74 | ),
75 | )
76 | }
77 | />
78 | ) : (
79 |
85 | dispatch(
86 | prometheusSlice.actions.filterQSet(
87 | Number(e.currentTarget.value),
88 | ),
89 | )
90 | }
91 | />
92 | ),
93 | )}
94 |
95 | );
96 | }
97 |
--------------------------------------------------------------------------------
/src/components/pages/PrometheusSettings/ModuleLfo.tsx:
--------------------------------------------------------------------------------
1 | import { useDispatch, useSelector } from "react-redux";
2 | import ControlModule, { Range } from "../../organisms/ControlModule";
3 | import prometheusSlice from "../../../store/prometheusSlice";
4 | import SelectOscType from "../../shared/SelectOscType";
5 |
6 | export default function ModuleLfo() {
7 | const { gain, frequency, type } = useSelector(prometheusSlice.selectors.lfo);
8 | const dispatch = useDispatch();
9 |
10 | return (
11 |
12 |
15 | dispatch(
16 | prometheusSlice.actions.lfoTypeSet(
17 | e.target.value as OscillatorType,
18 | ),
19 | )
20 | }
21 | />
22 |
28 | dispatch(
29 | prometheusSlice.actions.lfoFrequencySet(
30 | Number(e.currentTarget.value),
31 | ),
32 | )
33 | }
34 | />
35 |
41 | dispatch(
42 | prometheusSlice.actions.lfoGainSet(Number(e.currentTarget.value)),
43 | )
44 | }
45 | />
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/pages/PrometheusSettings/ModuleMaster.tsx:
--------------------------------------------------------------------------------
1 | import { useDispatch, useSelector } from "react-redux";
2 | import ControlModule, { Range } from "../../organisms/ControlModule";
3 | import prometheusSlice from "../../../store/prometheusSlice";
4 |
5 | export default function ModuleMaster() {
6 | const dispatch = useDispatch();
7 | const { gain, pan } = useSelector(prometheusSlice.selectors.master);
8 |
9 | return (
10 |
11 |
12 |
17 | dispatch(
18 | prometheusSlice.actions.masterGainSet(
19 | Number(e.currentTarget.value),
20 | ),
21 | )
22 | }
23 | />
24 |
29 | dispatch(
30 | prometheusSlice.actions.masterPanSet(
31 | Number(e.currentTarget.value),
32 | ),
33 | )
34 | }
35 | />
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/pages/PrometheusSettings/ModuleOscSingle.tsx:
--------------------------------------------------------------------------------
1 | import { useDispatch } from "react-redux";
2 | import ControlModule, { Range } from "../../organisms/ControlModule";
3 | import prometheusSlice from "../../../store/prometheusSlice";
4 | import SelectOscType from "../../shared/SelectOscType";
5 |
6 | interface Props {
7 | detune: number;
8 | gain: number;
9 | id: number;
10 | pan: number;
11 | pitch: number;
12 | type: OscillatorType;
13 | }
14 |
15 | export default function ModuleOscSingle({
16 | detune,
17 | gain,
18 | id,
19 | pan,
20 | pitch,
21 | type,
22 | }: Props) {
23 | const dispatch = useDispatch();
24 |
25 | return (
26 |
27 |
30 | dispatch(
31 | prometheusSlice.actions.oscillatorSinglesPatch({
32 | id,
33 | type: e.target.value as OscillatorType,
34 | }),
35 | )
36 | }
37 | />
38 |
43 | dispatch(
44 | prometheusSlice.actions.oscillatorSinglesPatch({
45 | id,
46 | gain: Number(e.target.value),
47 | }),
48 | )
49 | }
50 | />
51 |
56 | dispatch(
57 | prometheusSlice.actions.oscillatorSinglesPatch({
58 | id,
59 | pan: Number(e.currentTarget.value),
60 | }),
61 | )
62 | }
63 | />
64 |
71 | dispatch(
72 | prometheusSlice.actions.oscillatorSinglesPatch({
73 | id,
74 | pitch: Number(e.currentTarget.value),
75 | }),
76 | )
77 | }
78 | step={1}
79 | />
80 |
87 | dispatch(
88 | prometheusSlice.actions.oscillatorSinglesPatch({
89 | id,
90 | detune: Number(e.currentTarget.value),
91 | }),
92 | )
93 | }
94 | />
95 |
96 | );
97 | }
98 |
--------------------------------------------------------------------------------
/src/components/pages/PrometheusSettings/ModuleOscSuper.tsx:
--------------------------------------------------------------------------------
1 | import { useDispatch } from "react-redux";
2 | import ControlModule, { Range, Select } from "../../organisms/ControlModule";
3 | import prometheusSlice from "../../../store/prometheusSlice";
4 |
5 | const SelectOscType = ({
6 | defaultValue,
7 | onChange,
8 | }: {
9 | defaultValue: string;
10 | onChange: (e: React.ChangeEvent) => void;
11 | }) => (
12 |
19 | );
20 |
21 | interface Props {
22 | detune: number;
23 | gain: number;
24 | id: number;
25 | numberOfOscillators: number;
26 | pan: number;
27 | pitch: number;
28 | spread: number;
29 | type: string;
30 | }
31 |
32 | export default function ModuleOscSuper({
33 | detune,
34 | gain,
35 | id,
36 | numberOfOscillators,
37 | pan,
38 | pitch,
39 | spread,
40 | type,
41 | }: Props) {
42 | const dispatch = useDispatch();
43 |
44 | return (
45 |
46 |
49 | dispatch(
50 | prometheusSlice.actions.oscillatorSupersPatch({
51 | id,
52 | type: e.target.value as OscillatorType,
53 | }),
54 | )
55 | }
56 | />
57 |
64 | dispatch(
65 | prometheusSlice.actions.oscillatorSupersPatch({
66 | id,
67 | numberOfOscillators: Number(e.currentTarget.value),
68 | }),
69 | )
70 | }
71 | step={1}
72 | />
73 |
80 | dispatch(
81 | prometheusSlice.actions.oscillatorSupersPatch({
82 | id,
83 | spread: Number(e.currentTarget.value),
84 | }),
85 | )
86 | }
87 | />
88 |
93 | dispatch(
94 | prometheusSlice.actions.oscillatorSupersPatch({
95 | id,
96 | gain: Number(e.target.value),
97 | }),
98 | )
99 | }
100 | />
101 |
106 | dispatch(
107 | prometheusSlice.actions.oscillatorSupersPatch({
108 | id,
109 | pan: Number(e.currentTarget.value),
110 | }),
111 | )
112 | }
113 | />
114 |
121 | dispatch(
122 | prometheusSlice.actions.oscillatorSupersPatch({
123 | id,
124 | pitch: Number(e.currentTarget.value),
125 | }),
126 | )
127 | }
128 | step={1}
129 | />
130 |
137 | dispatch(
138 | prometheusSlice.actions.oscillatorSupersPatch({
139 | id,
140 | detune: Number(e.currentTarget.value),
141 | }),
142 | )
143 | }
144 | />
145 |
146 | );
147 | }
148 |
--------------------------------------------------------------------------------
/src/components/pages/PrometheusSettings/index.tsx:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 | import { useNavigate } from "react-router-dom";
3 | import ButtonPrimary from "../../atoms/ButtonPrimary";
4 | import ModuleMaster from "./ModuleMaster";
5 | import ModuleFilter from "./ModuleFilter";
6 | import ModuleLfo from "./ModuleLfo";
7 | import ModuleOscSuper from "./ModuleOscSuper";
8 | import ModuleOscSingle from "./ModuleOscSingle";
9 | import prometheusSlice from "../../../store/prometheusSlice";
10 |
11 | export default function PrometheusSettings() {
12 | const navigate = useNavigate();
13 |
14 | const oscillatorSingles = useSelector(
15 | prometheusSlice.selectors.oscillatorSingles,
16 | );
17 | const oscillatorSupers = useSelector(
18 | prometheusSlice.selectors.oscillatorSupers,
19 | );
20 |
21 | return (
22 | // TOOD: something about this reused classname...
23 |
24 |
25 |
PROMETHEUS
26 |
27 |
28 |
29 |
30 |
31 |
32 | {oscillatorSupers.map(
33 | ({
34 | detune,
35 | gain,
36 | id,
37 | numberOfOscillators,
38 | pan,
39 | pitch,
40 | spread,
41 | type,
42 | }) => (
43 |
54 | ),
55 | )}
56 |
57 | {oscillatorSingles.map(({ detune, gain, id, pan, pitch, type }) => (
58 |
67 | ))}
68 |
69 |
navigate(-1)}>OK
70 |
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/src/components/pages/Settings/index.tsx:
--------------------------------------------------------------------------------
1 | import ButtonPrimary from "../../atoms/ButtonPrimary";
2 | import RangeLabelled from "../../molecules/RangeLabelled";
3 | import navSlice from "../../../store/navSlice";
4 | import { useDispatch, useSelector } from "react-redux";
5 | import settingsSlice from "../../../store/settingsSlice";
6 |
7 | export default function Settings() {
8 | const lastDirection = useSelector(navSlice.selectors.lastDirection);
9 | const bpm = useSelector(settingsSlice.selectors.bpm);
10 | const dispatch = useDispatch();
11 |
12 | return (
13 |
14 |
18 | dispatch(settingsSlice.actions.bpmSet(Number(e.currentTarget.value)))
19 | }
20 | value={bpm}
21 | >
22 | BPM
23 |
24 |
25 |
26 | Keyboard Settings
27 |
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/pages/Settings/style.css:
--------------------------------------------------------------------------------
1 | .Settings {
2 | align-items: center;
3 | display: flex;
4 | flex-direction: column;
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/shared/RedirectHome.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { useNavigate } from "react-router-dom";
3 |
4 | export default function RedirectHome() {
5 | const navigate = useNavigate();
6 | useEffect(() => navigate("/"));
7 | return null;
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/shared/SelectOscType.tsx:
--------------------------------------------------------------------------------
1 | import { Select } from "../organisms/ControlModule";
2 |
3 | interface Props {
4 | defaultValue: string;
5 | onChange: (e: React.ChangeEvent) => void;
6 | }
7 |
8 | export default function SelectOscType({ defaultValue, onChange }: Props) {
9 | return (
10 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const INSTRUMENTS = ["Ariadne", "Prometheus"] as const;
2 |
3 | export const KEY_CODES_TO_PITCHES = {
4 | 220: -10,
5 | 90: -9,
6 | 83: -8,
7 | 88: -7,
8 | 68: -6,
9 | 67: -5,
10 | 86: -4,
11 | 71: -3,
12 | 66: -2,
13 | 72: -1,
14 | 78: 0,
15 | 74: 1,
16 | 49: 1,
17 | 77: 2,
18 | 81: 2,
19 | 87: 3,
20 | 188: 3,
21 | 51: 4,
22 | 76: 4,
23 | 69: 5,
24 | 190: 5,
25 | 186: 6,
26 | 52: 6,
27 | 59: 6,
28 | 82: 7,
29 | 191: 7,
30 | 84: 8,
31 | 222: 9,
32 | 54: 9,
33 | 89: 10,
34 | 55: 11,
35 | 85: 12,
36 | 56: 13,
37 | 73: 14,
38 | 79: 15,
39 | 48: 16,
40 | 80: 17,
41 | 189: 18,
42 | 219: 19,
43 | 221: 20,
44 | } as const;
45 |
46 | export const NAV = [
47 | ["/", "Pad"],
48 | ["/settings", "🔧"],
49 | ["/about", "?"],
50 | ] as const;
51 |
52 | export const SCALES = {
53 | "aeolian (minor)": [0, 2, 3, 5, 7, 8, 10],
54 | dorian: [0, 2, 3, 5, 7, 9, 10],
55 | "harmonic minor": [0, 2, 3, 5, 7, 8, 11],
56 | "ionian (major)": [0, 2, 4, 5, 7, 9, 11],
57 | locrian: [0, 1, 3, 5, 6, 8, 10],
58 | lydian: [0, 2, 4, 6, 7, 9, 11],
59 | mixolydian: [0, 2, 4, 5, 7, 9, 10],
60 | pentatonic: [0, 3, 5, 7, 10],
61 | phrygian: [0, 1, 3, 5, 7, 8, 10],
62 | "phrygian dominant": [0, 1, 4, 5, 7, 8, 10],
63 | wholetone: [0, 2, 4, 6, 8, 10],
64 | } as const;
65 |
--------------------------------------------------------------------------------
/src/constants/colors.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --blue-05: hsl(210, 62%, 5%);
3 | --blue-10: hsl(213, 64%, 10%);
4 | --blue-20: hsl(216, 68%, 20%);
5 | --blue-30: hsl(219, 72%, 30%);
6 | --blue-40: hsl(222, 76%, 40%);
7 | --blue-50: hsl(225, 80%, 50%);
8 | --blue-60: hsl(228, 84%, 60%);
9 | --blue-70: hsl(231, 88%, 70%);
10 | --blue-80: hsl(234, 92%, 80%);
11 | --blue-90: hsl(237, 96%, 90%);
12 | --gray-00: hsl(0, 0%, 0%);
13 | --gray-10: hsl(0, 0%, 10%);
14 | --gray-20: hsl(0, 0%, 20%);
15 | --gray-30: hsl(0, 0%, 30%);
16 | --gray-40: hsl(0, 0%, 40%);
17 | --gray-50: hsl(0, 0%, 50%);
18 | --gray-60: hsl(0, 0%, 60%);
19 | --gray-70: hsl(0, 0%, 70%);
20 | --gray-80: hsl(0, 0%, 80%);
21 | --gray-90: hsl(0, 0%, 90%);
22 | --gray-99: hsl(0, 0%, 100%);
23 | --green-05: hsl(135, 62%, 5%);
24 | --green-10: hsl(138, 64%, 10%);
25 | --green-20: hsl(141, 68%, 20%);
26 | --green-30: hsl(144, 72%, 30%);
27 | --green-40: hsl(147, 76%, 40%);
28 | --green-50: hsl(150, 80%, 50%);
29 | --green-60: hsl(153, 84%, 60%);
30 | --green-70: hsl(156, 88%, 70%);
31 | --green-80: hsl(159, 92%, 80%);
32 | --green-90: hsl(162, 96%, 90%);
33 | --red-05: hsl(29, 62%, 5%);
34 | --red-10: hsl(27, 64%, 10%);
35 | --red-20: hsl(24, 68%, 20%);
36 | --red-30: hsl(21, 72%, 30%);
37 | --red-40: hsl(18, 76%, 40%);
38 | --red-50: hsl(15, 80%, 50%);
39 | --red-60: hsl(12, 84%, 60%);
40 | --red-70: hsl(9, 88%, 70%);
41 | --red-80: hsl(6, 92%, 80%);
42 | --red-90: hsl(3, 96%, 90%);
43 | --pink-50: #d6e;
44 | }
45 |
--------------------------------------------------------------------------------
/src/constants/timings.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --transition-fast: 0.2s;
3 | --transition-medium: 0.33s;
4 | --transition-slow: 1.5s;
5 | }
6 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
15 | Andromeda - make music in your browser
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import "./constants/colors.css";
2 | import "./constants/timings.css";
3 | import "./utils/keyframes.css";
4 | import "./utils/globals.css";
5 |
6 | import "./components/atoms/ButtonPrimary.css";
7 | import "./components/atoms/ButtonSecondary.css";
8 | import "./components/atoms/HollowButton.css";
9 | import "./components/atoms/InputLabel.css";
10 | import "./components/atoms/InputSelect.css";
11 | import "./components/atoms/LinkExternal.css";
12 | import "./components/molecules/CheckboxLabelled.css";
13 | import "./components/molecules/RangeLabelled.css";
14 | import "./components/organisms/ControlModule.css";
15 | import "./components/organisms/ControlPad/style.css";
16 | import "./components/organisms/Navigation.css";
17 | import "./components/organisms/spinner.css";
18 | import "./components/organisms/table-center.css";
19 | import "./components/pages/About/style.css";
20 | import "./components/pages/ControlPadPage.css";
21 | import "./components/pages/ControlPadSettings.css";
22 | import "./components/pages/Instrument.css";
23 | import "./components/pages/KeyboardSettings.css";
24 | import "./components/pages/Settings/style.css";
25 |
26 | import { createRoot } from "react-dom/client";
27 | import { Provider } from "react-redux";
28 | import store from "./store";
29 | import screenSlice from "./store/screenSlice";
30 | import App from "./components/App";
31 | import { StrictMode } from "react";
32 |
33 | const resizeHandler = () =>
34 | requestAnimationFrame(() =>
35 | store.dispatch(
36 | screenSlice.actions.screenResize({
37 | sideLength: innerWidth < innerHeight ? innerWidth : innerHeight * 0.8,
38 | width: innerWidth,
39 | }),
40 | ),
41 | );
42 |
43 | resizeHandler();
44 |
45 | addEventListener("resize", resizeHandler);
46 |
47 | const rootEl = document.getElementById("app");
48 | if (!(rootEl instanceof HTMLElement)) throw new Error("Root element not found");
49 | createRoot(rootEl).render(
50 |
51 |
52 |
53 |
54 | ,
55 | );
56 |
--------------------------------------------------------------------------------
/src/manifest.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "icons": [
3 | {
4 | "src": "assets/icon.svg",
5 | "sizes": "any",
6 | "type": "image/svg+xml"
7 | }
8 | ],
9 | "display": "minimal-ui",
10 | "lang": "en",
11 | "name": "Andromeda",
12 | "orientation": "portrait",
13 | "short_name": "Andromeda",
14 | "start_url": "/"
15 | }
16 |
--------------------------------------------------------------------------------
/src/store/ariadneAudioGraphSelector.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createNode,
3 | gain,
4 | oscillator,
5 | OUTPUT,
6 | stereoPanner,
7 | } from "virtual-audio-graph";
8 | import ariadneSlice from "./ariadneSlice";
9 | import { createSelector } from "@reduxjs/toolkit";
10 | import { Note } from "../types";
11 | import controlPadSlice from "./controlPadSlice";
12 | import keyboardSlice from "./keyboardSlice";
13 |
14 | const oscBank = createNode(
15 | ({
16 | carrierDetune,
17 | carrierOscType,
18 | gain: gainValue,
19 | frequency,
20 | masterGain,
21 | masterPan,
22 | modulatorDetune,
23 | modulatorOscType,
24 | modulatorRatio,
25 | }: {
26 | carrierDetune: number;
27 | carrierOscType: OscillatorType;
28 | gain: number;
29 | frequency: number;
30 | masterGain: number;
31 | masterPan: number;
32 | modulatorDetune: number;
33 | modulatorOscType: OscillatorType;
34 | modulatorRatio: number;
35 | }) => ({
36 | 0: gain(["masterPan"], { gain: gainValue / 2 }),
37 | 1: oscillator(0, {
38 | detune: carrierDetune,
39 | frequency,
40 | type: carrierOscType,
41 | }),
42 | 2: gain({ destination: "frequency", key: 1 }, { gain: 1024 }),
43 | 3: oscillator(2, {
44 | detune: modulatorDetune,
45 | frequency: frequency * modulatorRatio,
46 | type: modulatorOscType,
47 | }),
48 | masterGain: gain([OUTPUT], { gain: masterGain }),
49 | masterPan: stereoPanner(["masterGain"], { pan: masterPan }),
50 | }),
51 | );
52 |
53 | const ariadne = createNode(
54 | ({
55 | carrierDetune,
56 | carrierOscType,
57 | masterGain,
58 | masterPan,
59 | modulatorDetune,
60 | modulatorOscType,
61 | modulatorRatio,
62 | notes,
63 | }: {
64 | carrierDetune: number;
65 | carrierOscType: OscillatorType;
66 | masterGain: number;
67 | masterPan: number;
68 | modulatorDetune: number;
69 | modulatorOscType: OscillatorType;
70 | modulatorRatio: number;
71 | notes: Note[];
72 | }) =>
73 | notes.reduce(
74 | (acc, { frequency, gain, id }) =>
75 | Object.assign({}, acc, {
76 | [id]: oscBank(OUTPUT, {
77 | carrierDetune,
78 | carrierOscType,
79 | frequency,
80 | gain,
81 | masterGain,
82 | masterPan,
83 | modulatorDetune,
84 | modulatorOscType,
85 | modulatorRatio,
86 | }),
87 | }),
88 | {},
89 | ),
90 | );
91 |
92 | const ariadneActiveNotesSelector = createSelector(
93 | controlPadSlice.selectors.instrument,
94 | controlPadSlice.selectors.currentNote,
95 | keyboardSlice.selectors.instrument,
96 | keyboardSlice.selectors.currentNotes,
97 | (
98 | controlPadInstrument,
99 | controlPadNote,
100 | keyboardInstrument,
101 | keyboardNotes,
102 | ): Note[] => {
103 | let notes = keyboardInstrument === "Ariadne" ? keyboardNotes : [];
104 | if (controlPadInstrument === "Ariadne" && controlPadNote)
105 | notes = [...keyboardNotes, controlPadNote];
106 | return notes;
107 | },
108 | );
109 |
110 | export default createSelector(
111 | ariadneActiveNotesSelector,
112 | ariadneSlice.selectors.carrierDetune,
113 | ariadneSlice.selectors.carrierOscType,
114 | ariadneSlice.selectors.masterGain,
115 | ariadneSlice.selectors.masterPan,
116 | ariadneSlice.selectors.modulatorDetune,
117 | ariadneSlice.selectors.modulatorOscType,
118 | ariadneSlice.selectors.modulatorRatio,
119 | (
120 | notes,
121 | carrierDetune,
122 | carrierOscType,
123 | masterGain,
124 | masterPan,
125 | modulatorDetune,
126 | modulatorOscType,
127 | modulatorRatio,
128 | ) => {
129 | if (!notes.length) return;
130 | return ariadne(0, {
131 | carrierDetune,
132 | carrierOscType,
133 | masterGain,
134 | masterPan,
135 | modulatorDetune,
136 | modulatorOscType,
137 | modulatorRatio,
138 | notes,
139 | });
140 | },
141 | );
142 |
--------------------------------------------------------------------------------
/src/store/ariadneSlice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit";
2 |
3 | interface AriadneState {
4 | carrierDetune: number;
5 | carrierOscType: OscillatorType;
6 | masterGain: number;
7 | masterPan: number;
8 | modulatorDetune: number;
9 | modulatorOscType: OscillatorType;
10 | modulatorRatio: number;
11 | }
12 |
13 | const initialState: AriadneState = {
14 | carrierDetune: 0,
15 | carrierOscType: "sine",
16 | masterGain: 1,
17 | masterPan: 0,
18 | modulatorDetune: 0,
19 | modulatorOscType: "sine",
20 | modulatorRatio: 2.5,
21 | };
22 |
23 | export default createSlice({
24 | name: "ariadne",
25 | initialState,
26 | reducers: {
27 | carrierDetuneSet: (state, action: PayloadAction) => {
28 | state.carrierDetune = action.payload;
29 | },
30 | carrierOscTypeSet: (state, action: PayloadAction) => {
31 | state.carrierOscType = action.payload;
32 | },
33 | masterGainSet: (state, action: PayloadAction) => {
34 | state.masterGain = action.payload;
35 | },
36 | masterPanSet: (state, action: PayloadAction) => {
37 | state.masterPan = action.payload;
38 | },
39 | modulatorDetuneSet: (state, action: PayloadAction) => {
40 | state.modulatorDetune = action.payload;
41 | },
42 | modulatorOscTypeSet: (state, action: PayloadAction) => {
43 | state.modulatorOscType = action.payload;
44 | },
45 | modulatorRatioSet: (state, action: PayloadAction) => {
46 | state.modulatorRatio = action.payload;
47 | },
48 | },
49 | selectors: {
50 | carrierDetune: (state) => state.carrierDetune,
51 | carrierOscType: (state) => state.carrierOscType,
52 | masterGain: (state) => state.masterGain,
53 | masterPan: (state) => state.masterPan,
54 | modulatorDetune: (state) => state.modulatorDetune,
55 | modulatorOscType: (state) => state.modulatorOscType,
56 | modulatorRatio: (state) => state.modulatorRatio,
57 | },
58 | });
59 |
--------------------------------------------------------------------------------
/src/store/audioGraphSelector.ts:
--------------------------------------------------------------------------------
1 | import { createSelector } from "@reduxjs/toolkit";
2 | import ariadneAudioGraphSelector from "./ariadneAudioGraphSelector";
3 | import prometheusAudioGraphSelector from "./prometheusAudioGraphSelector";
4 | import {
5 | createNode,
6 | gain,
7 | OUTPUT,
8 | stereoPanner,
9 | dynamicsCompressor,
10 | INPUT,
11 | delay,
12 | biquadFilter,
13 | } from "virtual-audio-graph";
14 | import { IVirtualAudioNodeGraph } from "virtual-audio-graph/dist/types";
15 |
16 | const leveller = createNode(
17 | ({
18 | attack,
19 | gain: gainValue,
20 | knee,
21 | pan,
22 | ratio,
23 | release,
24 | threshold,
25 | }: {
26 | attack: number;
27 | gain: number;
28 | knee: number;
29 | pan: number;
30 | ratio: number;
31 | release: number;
32 | threshold: number;
33 | }) => ({
34 | 0: gain(OUTPUT, { gain: gainValue }),
35 | 1: stereoPanner(0, { pan }),
36 | 2: dynamicsCompressor(
37 | 1,
38 | { attack, knee, ratio, release, threshold },
39 | INPUT,
40 | ),
41 | }),
42 | );
43 |
44 | const pingPongDelay = createNode(
45 | ({
46 | delayTime,
47 | dryLevel,
48 | feedback,
49 | highCut,
50 | lowCut,
51 | maxDelayTime,
52 | pingPong,
53 | wetLevel,
54 | }: {
55 | delayTime: number;
56 | dryLevel: number;
57 | feedback: number;
58 | highCut: number;
59 | lowCut: number;
60 | maxDelayTime: number;
61 | pingPong: boolean;
62 | wetLevel: number;
63 | }) => ({
64 | 0: gain(OUTPUT, { gain: wetLevel }),
65 | 1: stereoPanner(0, { pan: -1 }),
66 | 2: stereoPanner(0, { pan: 1 }),
67 | 3: delay([2, 8], { delayTime, maxDelayTime }),
68 | 4: gain(3, { gain: feedback }),
69 | 5: delay(pingPong ? [1, 3] : [0, 8], { delayTime, maxDelayTime }),
70 | 6: biquadFilter(5, { frequency: highCut }),
71 | 7: biquadFilter(6, { frequency: lowCut, type: "highpass" }),
72 | 8: gain(7, { gain: feedback }),
73 | 9: gain(OUTPUT, { gain: dryLevel }),
74 | input: gain([8, 9], { gain: 1 }, INPUT),
75 | }),
76 | );
77 |
78 | export default createSelector(
79 | ariadneAudioGraphSelector,
80 | prometheusAudioGraphSelector,
81 | (ariadneAudioGraph, prometheusAudioGraph) => {
82 | const audioGraph: IVirtualAudioNodeGraph = {
83 | 0: pingPongDelay(OUTPUT, {
84 | delayTime: 1 / 3,
85 | dryLevel: 0.9,
86 | feedback: 0.25,
87 | highCut: 16000,
88 | lowCut: 50,
89 | maxDelayTime: 1.2,
90 | pingPong: true,
91 | wetLevel: 0.6,
92 | }),
93 | 1: leveller(0, {
94 | attack: 0,
95 | gain: 1,
96 | knee: 30,
97 | pan: 0,
98 | ratio: 12,
99 | release: 0.25,
100 | threshold: -50,
101 | }),
102 | };
103 | if (ariadneAudioGraph) audioGraph[2] = ariadneAudioGraph;
104 | if (prometheusAudioGraph) audioGraph[3] = prometheusAudioGraph;
105 |
106 | return audioGraph;
107 | },
108 | );
109 |
--------------------------------------------------------------------------------
/src/store/controlPadSlice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit";
2 | import { SCALES } from "../constants";
3 | import pitchToFrequency from "../audioHelpers/pitchToFrequency";
4 | import { Instrument, Note, ScaleName } from "../types";
5 |
6 | interface CurrentCoordinateRatios {
7 | x: number;
8 | y: number;
9 | }
10 |
11 | interface ControlPadState {
12 | currentCoordinateRatios: CurrentCoordinateRatios | undefined;
13 | hasBeenTouched: boolean;
14 | instrument: Instrument;
15 | noScale: boolean;
16 | octave: number;
17 | range: number;
18 | rootNote: number;
19 | selectedScale: ScaleName;
20 | }
21 |
22 | const initialState: ControlPadState = {
23 | currentCoordinateRatios: undefined,
24 | hasBeenTouched: false,
25 | instrument: "Ariadne",
26 | noScale: false,
27 | octave: 0,
28 | range: 1,
29 | rootNote: 0,
30 | selectedScale: "pentatonic",
31 | };
32 |
33 | export default createSlice({
34 | name: "controlPad",
35 | initialState,
36 | reducers: {
37 | instrumentSet: (state, action: PayloadAction) => {
38 | state.instrument = action.payload;
39 | },
40 | noScaleSet: (state, action: PayloadAction) => {
41 | state.noScale = action.payload;
42 | },
43 | octaveSet: (state, action: PayloadAction) => {
44 | state.octave = action.payload;
45 | },
46 | rangeSet: (state, action: PayloadAction) => {
47 | state.range = action.payload;
48 | },
49 | rootNoteSet: (state, action: PayloadAction) => {
50 | state.rootNote = action.payload;
51 | },
52 | selectedScaleSet: (state, action: PayloadAction) => {
53 | state.selectedScale = action.payload;
54 | },
55 | setCurrentCoordinateRatios: (
56 | state,
57 | action: PayloadAction,
58 | ) => {
59 | state.currentCoordinateRatios = action.payload;
60 | if (!state.hasBeenTouched) state.hasBeenTouched = true;
61 | },
62 | },
63 | selectors: {
64 | currentNote: (state): Note | undefined => {
65 | if (!state.currentCoordinateRatios) return undefined;
66 |
67 | let pitch: number;
68 | if (state.noScale)
69 | pitch = 12 * state.range * state.currentCoordinateRatios.x;
70 | else {
71 | const scale = SCALES[state.selectedScale];
72 | const { length } = scale;
73 | const i = Math.floor(
74 | (length + 1) * state.range * state.currentCoordinateRatios.x,
75 | );
76 | pitch =
77 | scale[((i % length) + length) % length] + 12 * Math.floor(i / length);
78 | }
79 |
80 | return {
81 | frequency: pitchToFrequency(pitch + 12 * state.octave + state.rootNote),
82 | gain: state.currentCoordinateRatios.y,
83 | id: "CONTROL_PAD",
84 | };
85 | },
86 | hasBeenTouched: (state) => state.hasBeenTouched,
87 | instrument: (state) => state.instrument,
88 | noScale: (state) => state.noScale,
89 | octave: (state) => state.octave,
90 | range: (state) => state.range,
91 | rootNote: (state) => state.rootNote,
92 | selectedScale: (state) => state.selectedScale,
93 | },
94 | });
95 |
--------------------------------------------------------------------------------
/src/store/index.ts:
--------------------------------------------------------------------------------
1 | import { configureStore } from "@reduxjs/toolkit";
2 | import ariadneSlice from "./ariadneSlice";
3 | import controlPadSlice from "./controlPadSlice";
4 | import keyboardSlice from "./keyboardSlice";
5 | import navSlice from "./navSlice";
6 | import prometheusSlice from "./prometheusSlice";
7 | import screenSlice from "./screenSlice";
8 | import settingsSlice from "./settingsSlice";
9 |
10 | export default configureStore({
11 | reducer: {
12 | ariadne: ariadneSlice.reducer,
13 | controlPad: controlPadSlice.reducer,
14 | keyboard: keyboardSlice.reducer,
15 | nav: navSlice.reducer,
16 | prometheus: prometheusSlice.reducer,
17 | screen: screenSlice.reducer,
18 | settings: settingsSlice.reducer,
19 | },
20 | });
21 |
--------------------------------------------------------------------------------
/src/store/keyboardSlice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit";
2 | import { Instrument, Note } from "../types";
3 | import { KEY_CODES_TO_PITCHES } from "../constants";
4 | import pitchToFrequency from "../audioHelpers/pitchToFrequency";
5 |
6 | interface KeyboardState {
7 | instrument: Instrument;
8 | monophonic: boolean;
9 | octave: number;
10 | pressedKeyCodes: number[];
11 | volume: number;
12 | }
13 |
14 | const initialState: KeyboardState = {
15 | instrument: "Prometheus",
16 | monophonic: false,
17 | octave: 0,
18 | pressedKeyCodes: [],
19 | volume: 0.2,
20 | };
21 |
22 | const isValidKeyCode = (
23 | keyCode: number,
24 | ): keyCode is keyof typeof KEY_CODES_TO_PITCHES =>
25 | keyCode in KEY_CODES_TO_PITCHES;
26 |
27 | export default createSlice({
28 | name: "keyboard",
29 | initialState,
30 | reducers: {
31 | instrumentSet: (state, action: PayloadAction) => {
32 | state.instrument = action.payload;
33 | },
34 | monophonicSet: (state, action: PayloadAction) => {
35 | state.monophonic = action.payload;
36 | },
37 | octaveSet: (state, action: PayloadAction) => {
38 | state.octave = action.payload;
39 | },
40 | pressedKeyCodesAdd: (state, action: PayloadAction) => {
41 | state.pressedKeyCodes.push(action.payload);
42 | },
43 | pressedKeyCodesRemove: (state, action: PayloadAction) => {
44 | return {
45 | ...state,
46 | pressedKeyCodes: state.pressedKeyCodes.filter(
47 | (keyCode) => keyCode !== action.payload,
48 | ),
49 | };
50 | },
51 | volumeSet: (state, action: PayloadAction) => {
52 | state.volume = action.payload;
53 | },
54 | },
55 | selectors: {
56 | currentNotes: (state): Note[] => {
57 | // TODO add monophony
58 | // if (state.keyboard.monophonic) {
59 | // for (const code of pressedKeyCodes)
60 | // if (isValidKeyCode(code) && code !== keyCode) stopAndRemoveNote(code);
61 | // }
62 | return state.pressedKeyCodes.filter(isValidKeyCode).map((keyCode) => {
63 | const pitch = KEY_CODES_TO_PITCHES[keyCode];
64 | return {
65 | frequency: pitchToFrequency(pitch + 12 * state.octave),
66 | gain: state.volume,
67 | id: `keyboard-pitch:${pitch}`,
68 | };
69 | });
70 | },
71 | instrument: (state) => state.instrument,
72 | monophonic: (state) => state.monophonic,
73 | octave: (state) => state.octave,
74 | pressedKeyCodes: (state) => state.pressedKeyCodes,
75 | volume: (state) => state.volume,
76 | },
77 | });
78 |
--------------------------------------------------------------------------------
/src/store/navSlice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit";
2 |
3 | interface NavState {
4 | lastDirection: "left" | "right";
5 | }
6 |
7 | const initialState: NavState = { lastDirection: "right" };
8 |
9 | export default createSlice({
10 | name: "nav",
11 | initialState,
12 | reducers: {
13 | lastDirectionSet: (
14 | state,
15 | action: PayloadAction,
16 | ) => {
17 | state.lastDirection = action.payload;
18 | },
19 | },
20 | selectors: {
21 | lastDirection: (state) => state.lastDirection,
22 | },
23 | });
24 |
--------------------------------------------------------------------------------
/src/store/prometheusAudioGraphSelector.ts:
--------------------------------------------------------------------------------
1 | import {
2 | biquadFilter,
3 | createNode,
4 | gain as gainNode,
5 | oscillator,
6 | OUTPUT,
7 | stereoPanner,
8 | } from "virtual-audio-graph";
9 | import pitchToFrequency from "../audioHelpers/pitchToFrequency";
10 | import prometheusSlice, { PrometheusState } from "./prometheusSlice";
11 | import { Note } from "../types";
12 | import { IVirtualAudioNodeGraph } from "virtual-audio-graph/dist/types";
13 |
14 | import { createSelector } from "@reduxjs/toolkit";
15 | import controlPadSlice from "./controlPadSlice";
16 | import keyboardSlice from "./keyboardSlice";
17 |
18 | const prometheusActiveNotesSelector = createSelector(
19 | controlPadSlice.selectors.instrument,
20 | controlPadSlice.selectors.currentNote,
21 | keyboardSlice.selectors.instrument,
22 | keyboardSlice.selectors.currentNotes,
23 | (
24 | controlPadInstrument,
25 | controlPadNote,
26 | keyboardInstrument,
27 | keyboardNotes,
28 | ): Note[] => {
29 | let notes = keyboardInstrument === "Prometheus" ? keyboardNotes : [];
30 | if (controlPadInstrument === "Prometheus" && controlPadNote)
31 | notes = [...keyboardNotes, controlPadNote];
32 | return notes;
33 | },
34 | );
35 |
36 | const frequencyToPitch = (frequency: number) => Math.log2(frequency / 440) * 12;
37 |
38 | const lfoNode = createNode(
39 | ({
40 | frequency,
41 | gain,
42 | type,
43 | }: {
44 | frequency: number;
45 | gain: number;
46 | type: OscillatorType;
47 | }) => ({
48 | 0: gainNode(OUTPUT, { gain }),
49 | 1: oscillator(0, { frequency, type }),
50 | }),
51 | );
52 |
53 | const osc = createNode(
54 | ({
55 | detune = 0,
56 | frequency,
57 | gain,
58 | pan,
59 | pitch,
60 | type,
61 | }: {
62 | detune: number;
63 | frequency: number;
64 | gain: number;
65 | pan: number;
66 | pitch: number;
67 | type: OscillatorType;
68 | }) => ({
69 | 0: gainNode(OUTPUT, { gain }),
70 | 1: stereoPanner(0, { pan }),
71 | 2: oscillator(1, {
72 | detune,
73 | frequency: pitchToFrequency(frequencyToPitch(frequency) + pitch),
74 | type,
75 | }),
76 | }),
77 | );
78 |
79 | const prometheus = createNode(
80 | ({
81 | filter,
82 | lfo,
83 | master,
84 | oscillatorSingles,
85 | oscillatorSupers,
86 | notes,
87 | }: PrometheusState & { notes: Note[] }) =>
88 | notes.reduce(
89 | (acc: IVirtualAudioNodeGraph, { frequency, gain, id }) => {
90 | const noteGainId = `noteGain-${id}`;
91 | acc[noteGainId] = gainNode("filter", { gain });
92 |
93 | for (let i = 0; i < oscillatorSingles.length; i++) {
94 | const oscillatorSingle = oscillatorSingles[i];
95 | acc[`oscSingle-${oscillatorSingle.id}-${id}`] = osc(
96 | noteGainId,
97 | Object.assign({}, oscillatorSingle, {
98 | frequency,
99 | }),
100 | );
101 | }
102 |
103 | for (let i = 0; i < oscillatorSupers.length; i++) {
104 | const oscillatorSuper = oscillatorSupers[i];
105 | const { numberOfOscillators, type } = oscillatorSuper;
106 | for (let j = 0; j < numberOfOscillators; j++) {
107 | acc[`oscSuper-${oscillatorSuper.id}-${j}-${id}`] = osc(noteGainId, {
108 | detune:
109 | oscillatorSuper.detune +
110 | (j - Math.floor(numberOfOscillators / 2)) *
111 | oscillatorSuper.spread,
112 | frequency,
113 | gain: oscillatorSuper.gain,
114 | pan: oscillatorSuper.pan,
115 | pitch: oscillatorSuper.pitch,
116 | type:
117 | type === "random"
118 | ? (["sawtooth", "sine", "square", "triangle"] as const)[
119 | Math.floor(Math.random() * 4)
120 | ]
121 | : type,
122 | });
123 | }
124 | }
125 |
126 | return acc;
127 | },
128 | {
129 | filter: biquadFilter("masterPan", filter),
130 | lfo: lfoNode({ destination: "frequency", key: "filter" }, lfo),
131 | masterGain: gainNode(OUTPUT, { gain: master.gain }),
132 | masterPan: stereoPanner("masterGain", { pan: master.pan }),
133 | },
134 | ),
135 | );
136 |
137 | export default createSelector(
138 | prometheusActiveNotesSelector,
139 | prometheusSlice.selectors.filter,
140 | prometheusSlice.selectors.lfo,
141 | prometheusSlice.selectors.master,
142 | prometheusSlice.selectors.oscillatorSingles,
143 | prometheusSlice.selectors.oscillatorSupers,
144 | (notes, filter, lfo, master, oscillatorSingles, oscillatorSupers) => {
145 | if (!notes.length) return;
146 | return prometheus(0, {
147 | filter,
148 | lfo,
149 | master,
150 | oscillatorSingles,
151 | oscillatorSupers,
152 | notes,
153 | });
154 | },
155 | );
156 |
--------------------------------------------------------------------------------
/src/store/prometheusSlice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit";
2 |
3 | export interface PrometheusState {
4 | filter: {
5 | frequency: number;
6 | gain: number;
7 | Q: number;
8 | type: BiquadFilterType;
9 | };
10 | lfo: {
11 | frequency: number;
12 | gain: number;
13 | type: OscillatorType;
14 | };
15 | master: {
16 | gain: number;
17 | pan: number;
18 | };
19 | oscillatorSingles: {
20 | detune: number;
21 | gain: number;
22 | id: number;
23 | pan: number;
24 | pitch: number;
25 | type: OscillatorType;
26 | }[];
27 | oscillatorSupers: {
28 | detune: number;
29 | gain: number;
30 | id: number;
31 | numberOfOscillators: number;
32 | pan: number;
33 | pitch: number;
34 | spread: number;
35 | type: OscillatorType | "random";
36 | }[];
37 | }
38 |
39 | const initialState: PrometheusState = {
40 | filter: {
41 | frequency: 1300,
42 | gain: -12,
43 | Q: 5,
44 | type: "lowpass",
45 | },
46 | lfo: {
47 | frequency: 0.3,
48 | gain: 400,
49 | type: "triangle",
50 | },
51 | master: {
52 | gain: 0.75,
53 | pan: 0,
54 | },
55 | oscillatorSingles: [
56 | { detune: 13, gain: 0.5, id: 0, pan: 0.4, pitch: -12, type: "triangle" },
57 | { detune: -7, gain: 0.8, id: 1, pan: 0.1, pitch: -12, type: "square" },
58 | { detune: 10, gain: 0.2, id: 2, pan: -0.4, pitch: 0, type: "square" },
59 | ],
60 | oscillatorSupers: [
61 | {
62 | detune: -3,
63 | gain: 0.35,
64 | id: 0,
65 | numberOfOscillators: 5,
66 | pan: -0.3,
67 | pitch: 0,
68 | spread: 6,
69 | type: "sawtooth",
70 | },
71 | ],
72 | };
73 |
74 | export default createSlice({
75 | name: "prometheus",
76 | initialState,
77 | reducers: {
78 | masterGainSet: (state, action: PayloadAction) => {
79 | state.master.gain = action.payload;
80 | },
81 | masterPanSet: (state, action: PayloadAction) => {
82 | state.master.pan = action.payload;
83 | },
84 | filterFrequencySet: (state, action: PayloadAction) => {
85 | state.filter.frequency = action.payload;
86 | },
87 | filterGainSet: (state, action: PayloadAction) => {
88 | state.filter.gain = action.payload;
89 | },
90 | filterQSet: (state, action: PayloadAction) => {
91 | state.filter.Q = action.payload;
92 | },
93 | filterTypeSet: (state, action: PayloadAction) => {
94 | state.filter.type = action.payload;
95 | },
96 | lfoFrequencySet: (state, action: PayloadAction) => {
97 | state.lfo.frequency = action.payload;
98 | },
99 | lfoGainSet: (state, action: PayloadAction) => {
100 | state.lfo.gain = action.payload;
101 | },
102 | lfoTypeSet: (state, action: PayloadAction) => {
103 | state.lfo.type = action.payload;
104 | },
105 | oscillatorSinglesPatch: (
106 | state,
107 | action: PayloadAction<
108 | Partial & { id: number }
109 | >,
110 | ) => {
111 | state.oscillatorSingles[action.payload.id] = {
112 | ...state.oscillatorSingles[action.payload.id],
113 | ...action.payload,
114 | };
115 | },
116 | oscillatorSupersPatch: (
117 | state,
118 | action: PayloadAction<
119 | Partial & { id: number }
120 | >,
121 | ) => {
122 | state.oscillatorSupers[action.payload.id] = {
123 | ...state.oscillatorSupers[action.payload.id],
124 | ...action.payload,
125 | };
126 | },
127 | },
128 | selectors: {
129 | filter: (state) => state.filter,
130 | lfo: (state) => state.lfo,
131 | master: (state) => state.master,
132 | oscillatorSingles: (state) => state.oscillatorSingles,
133 | oscillatorSupers: (state) => state.oscillatorSupers,
134 | },
135 | });
136 |
--------------------------------------------------------------------------------
/src/store/screenSlice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit";
2 |
3 | interface ScreenSize {
4 | sideLength: number;
5 | width: number;
6 | }
7 |
8 | const initialState: ScreenSize = {
9 | sideLength: 0,
10 | width: 0,
11 | };
12 |
13 | export default createSlice({
14 | name: "screen",
15 | initialState,
16 | reducers: {
17 | screenResize: (state, action: PayloadAction) => {
18 | return { ...state, ...action.payload };
19 | },
20 | },
21 | selectors: {
22 | sideLength: (state: ScreenSize) => state.sideLength,
23 | },
24 | });
25 |
--------------------------------------------------------------------------------
/src/store/settingsSlice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit";
2 |
3 | const bpm = 140;
4 |
5 | interface SettingsState {
6 | bpm: number;
7 | }
8 |
9 | const initialState: SettingsState = {
10 | bpm,
11 | };
12 |
13 | export default createSlice({
14 | name: "settings",
15 | initialState,
16 | reducers: {
17 | bpmSet: (state, action: PayloadAction) => {
18 | state.bpm = action.payload;
19 | },
20 | },
21 | selectors: {
22 | bpm: (state) => state.bpm,
23 | },
24 | });
25 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import { INSTRUMENTS, SCALES } from "./constants";
2 |
3 | export type Instrument = (typeof INSTRUMENTS)[number];
4 |
5 | export interface Note {
6 | id: string;
7 | gain: number;
8 | frequency: number;
9 | }
10 |
11 | export type ScaleName = keyof typeof SCALES;
12 |
--------------------------------------------------------------------------------
/src/utils/globals.css:
--------------------------------------------------------------------------------
1 | html {
2 | min-height: 100%;
3 |
4 | /* Disables pull down to refresh on mobile */
5 | overscroll-behavior-y: contain;
6 | }
7 |
8 | body {
9 | background: var(--gray-00);
10 | color: var(--gray-99);
11 | font-family: sans-serif;
12 | min-height: 100%;
13 | margin: 0;
14 | overflow-x: hidden;
15 | user-select: none;
16 | }
17 |
18 | .slide-in-left {
19 | animation-duration: var(--transition-medium);
20 | animation-name: slide-in-left;
21 | }
22 |
23 | .slide-in-right {
24 | animation-duration: var(--transition-medium);
25 | animation-name: slide-in-right;
26 | }
27 |
--------------------------------------------------------------------------------
/src/utils/helpers.ts:
--------------------------------------------------------------------------------
1 | export const noop = () => {};
2 |
3 | export const capitalizeFirst = (string: string) =>
4 | string[0].toUpperCase() + string.slice(1);
5 |
6 | export const capitalizeWords = (string: string) =>
7 | string.split(" ").map(capitalizeFirst).join(" ");
8 |
--------------------------------------------------------------------------------
/src/utils/keyframes.css:
--------------------------------------------------------------------------------
1 | @keyframes expand {
2 | 0% {
3 | transform: scale(0, 0);
4 | }
5 | 100% {
6 | transform: scale(1, 1);
7 | }
8 | }
9 |
10 | @keyframes fade-in {
11 | 0% {
12 | opacity: 0;
13 | }
14 | 100% {
15 | opacity: 1;
16 | }
17 | }
18 |
19 | @keyframes pulse {
20 | 0% {
21 | opacity: 0.25;
22 | }
23 | 100% {
24 | opacity: 1;
25 | }
26 | }
27 |
28 | @keyframes slide-in-left {
29 | 0% {
30 | transform: translateX(-100vw);
31 | }
32 | 100% {
33 | transform: translateX(0);
34 | }
35 | }
36 |
37 | @keyframes slide-in-right {
38 | 0% {
39 | transform: translateX(100vw);
40 | }
41 | 100% {
42 | transform: translateX(0);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/utils/webGl.ts:
--------------------------------------------------------------------------------
1 | export const compileShader = (
2 | gl: WebGLRenderingContext,
3 | src: string,
4 | shaderType: GLenum,
5 | ): WebGLShader => {
6 | const shader = gl.createShader(shaderType);
7 | if (shader === null) throw Error("Failed to create shader");
8 | gl.shaderSource(shader, src);
9 | gl.compileShader(shader);
10 | return shader;
11 | };
12 |
13 | export type Matrix16 = [
14 | number,
15 | number,
16 | number,
17 | number,
18 | number,
19 | number,
20 | number,
21 | number,
22 | number,
23 | number,
24 | number,
25 | number,
26 | number,
27 | number,
28 | number,
29 | number,
30 | ];
31 |
32 | export const mult = (a: Matrix16, b: Matrix16): Matrix16 => [
33 | a[0] * b[0] + a[1] * b[4] + a[2] * b[8] + a[3] * b[12],
34 | a[0] * b[1] + a[1] * b[5] + a[2] * b[9] + a[3] * b[13],
35 | a[0] * b[2] + a[1] * b[6] + a[2] * b[10] + a[3] * b[14],
36 | a[0] * b[3] + a[1] * b[7] + a[2] * b[11] + a[3] * b[15],
37 | a[4] * b[0] + a[5] * b[4] + a[6] * b[8] + a[7] * b[12],
38 | a[4] * b[1] + a[5] * b[5] + a[6] * b[9] + a[7] * b[13],
39 | a[4] * b[2] + a[5] * b[6] + a[6] * b[10] + a[7] * b[14],
40 | a[4] * b[3] + a[5] * b[7] + a[6] * b[11] + a[7] * b[15],
41 | a[8] * b[0] + a[9] * b[4] + a[10] * b[8] + a[11] * b[12],
42 | a[8] * b[1] + a[9] * b[5] + a[10] * b[9] + a[11] * b[13],
43 | a[8] * b[2] + a[9] * b[6] + a[10] * b[10] + a[11] * b[14],
44 | a[8] * b[3] + a[9] * b[7] + a[10] * b[11] + a[11] * b[15],
45 | a[12] * b[0] + a[13] * b[4] + a[14] * b[8] + a[15] * b[12],
46 | a[12] * b[1] + a[13] * b[5] + a[14] * b[9] + a[15] * b[13],
47 | a[12] * b[2] + a[13] * b[6] + a[14] * b[10] + a[15] * b[14],
48 | a[12] * b[3] + a[13] * b[7] + a[14] * b[11] + a[15] * b[15],
49 | ];
50 |
51 | export const rotateX = (a: number): Matrix16 => {
52 | const c = Math.cos(a);
53 | const s = Math.sin(a);
54 |
55 | return [1, 0, 0, 0, 0, c, s, 0, 0, -s, c, 0, 0, 0, 0, 1];
56 | };
57 |
58 | export const rotateY = (a: number): Matrix16 => {
59 | const c = Math.cos(a);
60 | const s = Math.sin(a);
61 |
62 | return [c, 0, -s, 0, 0, 1, 0, 0, s, 0, c, 0, 0, 0, 0, 1];
63 | };
64 |
65 | export const rotateZ = (a: number): Matrix16 => {
66 | const c = Math.cos(a);
67 | const s = Math.sin(a);
68 |
69 | return [c, s, 0, 0, -s, c, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
70 | };
71 |
72 | export const translate = (x: number, y: number, z: number): Matrix16 => [
73 | 1,
74 | 0,
75 | 0,
76 | 0,
77 | 0,
78 | 1,
79 | 0,
80 | 0,
81 | 0,
82 | 0,
83 | 1,
84 | 0,
85 | x,
86 | y,
87 | z,
88 | 1,
89 | ];
90 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowUnreachableCode": false,
4 | "forceConsistentCasingInFileNames": true,
5 | "jsx": "react-jsx",
6 | "module": "ES2022",
7 | "moduleResolution": "bundler",
8 | "noUnusedLocals": true,
9 | "sourceMap": true,
10 | "strict": true,
11 | "target": "ES2023"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------