diff --git a/gallium-live/src/BPMSelector.js b/gallium-live/src/BPMSelector.js
index ad447ca..96ca0a7 100644
--- a/gallium-live/src/BPMSelector.js
+++ b/gallium-live/src/BPMSelector.js
@@ -81,10 +81,10 @@ const Input = styled.input`
font-family: monospace;
cursor: pointer;
outline: none;
- color: #252525;
+ color: white;
&:active,
&:hover {
- box-shadow: 0 0 0 1px #252525;
+ box-shadow: 0 0 0 1px white;
}
margin-left: 10px;
`;
diff --git a/gallium-live/src/Editor.js b/gallium-live/src/Editor.js
index 5e532ca..67b76c6 100644
--- a/gallium-live/src/Editor.js
+++ b/gallium-live/src/Editor.js
@@ -15,6 +15,7 @@ import * as Playback from "./playback";
import * as AppActions from "./app_actions";
import { BPMSelector } from "./BPMSelector";
import { ToggleInvert } from "./ToggleInvert";
+import * as Shader from "./shader";
type OwnProps = {};
@@ -44,7 +45,9 @@ export class _Editor extends React.Component<
};
}
- textarea: ?HTMLTextAreaElement;
+ textarea: HTMLTextAreaElement;
+
+ textCanvas: HTMLCanvasElement;
componentDidMount() {
this.props.dispatch(AppActions.initialize());
@@ -57,10 +60,14 @@ export class _Editor extends React.Component<
}
onChange = (e: *) => {
+ const text = e.target.value;
this.setState({
- text: e.target.value
+ text
});
- this.updateABT(e.target.value);
+ this.updateABT(text);
+ if (this.textCanvas) {
+ this.drawText(text);
+ }
};
updateABT(text: string) {
@@ -112,14 +119,43 @@ export class _Editor extends React.Component<
}
};
- onTextareaRefLoad = (ref: HTMLTextAreaElement) => {
- this.textarea = ref;
- if (!this.textarea) {
+ registerContent = (ref: HTMLElement) => {
+ if (!ref) {
+ return;
+ }
+ const canvas: HTMLCanvasElement = (ref.children[0]: any);
+ const textCanvas: HTMLCanvasElement = (ref.children[1]: any);
+
+ (textCanvas.width = Math.max(window.innerHeight, window.innerWidth)),
+ (textCanvas.height = textCanvas.width);
+
+ this.textCanvas = textCanvas;
+ this.drawText(this.state.text);
+ Shader.registerWebGL({ canvas, textCanvas });
+ };
+
+ registerTextarea = (ref: HTMLTextAreaElement) => {
+ if (!ref) {
return;
}
- this.textarea.focus();
+ ref.focus();
+ this.textarea = ref;
};
+ drawText(text: string) {
+ const ctx = this.textCanvas.getContext("2d");
+ ctx.clearRect(0, 0, this.textCanvas.width, this.textCanvas.height);
+ ctx.font = "16px mono";
+ ctx.fillStyle = "white";
+ const lineHeight = ctx.measureText("M").width * 1.8;
+ const lines = text.split("\n");
+ let y = 0;
+ for (const line of lines) {
+ ctx.fillText(line, 0, y);
+ y += lineHeight;
+ }
+ }
+
render() {
const barStyle = this.state.error ? "dotted" : "solid";
return (
@@ -127,35 +163,37 @@ export class _Editor extends React.Component<
isInitialized={this.state.isInitialized}
style={{ filter: this.props.invert ? "invert()" : "" }}
>
-
-
- gallium.live
-
-
- source
-
-
-
+
+
+
+
+
+ gallium.live
+
+
+ source
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
);
}
@@ -168,21 +206,25 @@ export const Container: React$ComponentType<{
}> = styled.div`
width: 100%;
height: 100%;
- display: flex;
- flex-direction: column;
opacity: ${props => (props.isInitialized ? 1 : 0)};
transition: opacity 500ms ease-in-out;
- background-color: white;
`;
+const paneHeight = 60;
+
const Pane = styled.div`
- flex: 0 1 auto;
- min-height: 50px;
+ height: ${paneHeight}px;
display: flex;
+ width: 100%;
justify-content: flex-end;
align-items: center;
- padding: 0px 20px;
${Styles.transition};
+ position: fixed;
+ background-color: transparent;
+ z-index: 1;
+ pointer-events: none;
+ color: white;
+ mix-blend-mode: exclusion;
opacity: 0.5;
&:hover {
opacity: 1;
@@ -191,34 +233,54 @@ const Pane = styled.div`
const PaneChild = styled.div`
padding: 10px 20px;
+ pointer-events: all;
`;
const Content = styled.div`
- padding: 10vh 10vw;
- flex-grow: 1;
- flex-shrink: 0;
+ align-items: center;
+ background-color: transparent;
+ height: 100%;
+ width: 100%;
+ position: absolute;
display: flex;
- background-color: white;
+ justify-content: center;
+ align-items: center;
+`;
+
+const Canvas = styled.canvas`
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ margin: 0;
+ padding: 0;
+ top: 0;
+ left: 0;
+ background-color: black;
+`;
+
+const TextCanvas = styled.canvas`
+ display: none;
`;
export const Textarea: React.ComponentType<{
barStyle: string
}> = styled.textarea`
- ${Styles.transition};
border: 0;
font-size: 16px;
- background-color: transparent;
margin: 0;
- flex-grow: 1;
+ background-color: transparent;
font-family: monospace;
outline: none;
padding: 0;
padding-left: 0.2em;
- border-left: 1px ${props => props.barStyle} #000;
- opacity: 0.75;
- &:focus {
- opacity: 1;
- }
+ border-left: 1px ${props => props.barStyle} #fff;
+ opacity: 1;
+ width: calc(100% - 10px);
+ margin-left: 10px;
+ color: white;
+ mix-blend-mode: exclusion;
+ height: calc(100% - 2*${paneHeight}px);
+ z-index: 1;
`;
const Description = styled.div`
diff --git a/gallium-live/src/OutputSelector.js b/gallium-live/src/OutputSelector.js
index f0b1fee..608eff2 100644
--- a/gallium-live/src/OutputSelector.js
+++ b/gallium-live/src/OutputSelector.js
@@ -59,9 +59,9 @@ const Selector = styled.select`
font-family: monospace;
cursor: pointer;
outline: none;
- color: #252525;
+ color: white;
&:active,
&:hover {
- box-shadow: 0 0 0 1px #252525;
+ box-shadow: 0 0 0 1px #white;
}
`;
diff --git a/gallium-live/src/playback.js b/gallium-live/src/playback.js
index f4adc6e..2f19235 100644
--- a/gallium-live/src/playback.js
+++ b/gallium-live/src/playback.js
@@ -10,6 +10,9 @@ function getBeatLength(bpm: number): number {
return 1000 * 60 / bpm;
}
+//invariant: kickQueue is ordered by timestamp
+window.kickQueue = [];
+
export class Player {
+state: AppState;
@@ -23,7 +26,6 @@ export class Player {
now + (event.start - this.state.beat) * getBeatLength(this.state.bpm);
const timestampOff =
now + (event.end - this.state.beat) * getBeatLength(this.state.bpm) - 1;
-
this.state.output.send(
MIDIUtils.noteOn({
channel: event.value.channel,
@@ -41,6 +43,11 @@ export class Player {
}),
timestampOff
);
+ if (event.value.pitch === 127) {
+ window.kickQueue.push({ value: 1.0, timestamp: timestampOn });
+ const kickOff = timestampOn + Math.min(timestampOff - timestampOn, 30);
+ window.kickQueue.push({ value: 0.0, timestamp: kickOff });
+ }
}
queryAndSend(): void {
diff --git a/gallium-live/src/shader.js b/gallium-live/src/shader.js
new file mode 100644
index 0000000..ddeb97b
--- /dev/null
+++ b/gallium-live/src/shader.js
@@ -0,0 +1,235 @@
+// @flow
+
+const vert = `
+attribute vec4 aVertexPosition;
+varying vec2 uv;
+
+void main() {
+ gl_Position = aVertexPosition;
+ uv = aVertexPosition.xy;
+}
+`;
+
+const frag = `
+precision mediump float;
+
+varying vec2 uv;
+uniform float time;
+uniform float kick;
+uniform sampler2D text;
+
+void main() {
+ float x = clamp(cos(time * 2.33333 * 8. + cos(uv.x) + abs((uv.y))) * 2., -1., 1.);
+ x = sin(x)/2. + 0.5;
+ x = max(kick, x);
+ vec4 val = vec4(x, x, x, 1.0);
+ float t = time * 0.01;
+ gl_FragColor = vec4(val.xyz, 1.0);
+}
+`;
+
+type ProgramInfo = {|
+ program: WebGLProgram,
+ attribLocations: {
+ vertexPosition: number
+ },
+ uniformLocations: {
+ time: WebGLUniformLocation,
+ kick: WebGLUniformLocation,
+ text: WebGLUniformLocation
+ }
+|};
+
+export function registerWebGL(input: {
+ canvas: HTMLCanvasElement,
+ textCanvas: HTMLCanvasElement
+}) {
+ const { canvas, textCanvas } = input;
+ const gl = canvas.getContext("webgl");
+ if (!gl) {
+ throw new Error("Your browser doesn't seem to support webgl");
+ }
+ resize(gl);
+ gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
+
+ const program = createProgram(gl, {
+ fragmentShader: frag,
+ vertexShader: vert
+ });
+
+ // TODO: type
+ const programInfo = {
+ program,
+ attribLocations: {
+ vertexPosition: gl.getAttribLocation(program, "aVertexPosition")
+ },
+ uniformLocations: {
+ time: gl.getUniformLocation(program, "time"),
+ kick: gl.getUniformLocation(program, "kick"),
+ text: gl.getUniformLocation(program, "text")
+ }
+ };
+
+ const state = initState({ gl });
+
+ const animate = () => {
+ drawScene({ gl, programInfo, state, textCanvas });
+ requestAnimationFrame(animate);
+ };
+
+ requestAnimationFrame(animate);
+}
+
+const createShader = (
+ gl: WebGLRenderingContext,
+ source: string,
+ shaderType: any
+): WebGLShader => {
+ const shader = gl.createShader(shaderType);
+ gl.shaderSource(shader, source);
+ gl.compileShader(shader);
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
+ console.error(gl.getShaderInfoLog(shader));
+ gl.deleteShader(shader);
+ }
+ return shader;
+};
+
+const createProgram = (
+ gl: WebGLRenderingContext,
+ {
+ fragmentShader,
+ vertexShader
+ }: { fragmentShader: string, vertexShader: string }
+): WebGLProgram => {
+ const program = gl.createProgram();
+ gl.attachShader(program, createShader(gl, vertexShader, gl.VERTEX_SHADER));
+ gl.attachShader(
+ program,
+ createShader(gl, fragmentShader, gl.FRAGMENT_SHADER)
+ );
+ gl.linkProgram(program);
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
+ console.error(gl.getProgramInfoLog(program));
+ gl.deleteProgram(program);
+ }
+ return program;
+};
+
+type State = {
+ buffers: {
+ position: WebGLBuffer
+ },
+ textCanvasTexture: WebGLTexture
+};
+
+function initState(input: { gl: WebGLRenderingContext }): State {
+ const { gl } = input;
+
+ // positionBuffer
+ const positionBuffer: WebGLBuffer = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
+ const positions = new Float32Array([1, 1, -1, 1, 1, -1, -1, -1]);
+ gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
+
+ // textCanvas
+ const textCanvasTexture = gl.createTexture();
+ gl.bindTexture(gl.TEXTURE_2D, textCanvasTexture);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
+
+ return {
+ buffers: {
+ position: positionBuffer
+ },
+ textCanvasTexture
+ };
+}
+
+function drawScene(input: {
+ gl: WebGLRenderingContext,
+ programInfo: ProgramInfo,
+ textCanvas: HTMLCanvasElement,
+ state: State
+}) {
+ const { gl, programInfo, state, textCanvas } = input;
+ const { buffers } = state;
+
+ gl.clearColor(0.0, 0.0, 0.0, 1.0);
+ gl.clear(gl.COLOR_BUFFER_BIT);
+
+ {
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffers.position);
+ gl.vertexAttribPointer(
+ programInfo.attribLocations.vertexPosition,
+ 2, // numComponents: pull out 2 values per iteration
+ gl.FLOAT, // type: the data in the buffer is 32bit floats
+ false, // normalize
+ 0, // stride: how many bytes to get from one set of values to the next
+ 0 // offset: how many bytes inside the buffer to start from
+ );
+ gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition);
+ }
+
+ gl.useProgram(programInfo.program);
+
+ const now = performance.now();
+ gl.uniform1f(programInfo.uniformLocations.time, now / 1000);
+
+ {
+ let kick = null;
+ for (let i = 0; i < window.kickQueue.length; i += 1) {
+ const { timestamp, value } = window.kickQueue[i];
+ if (timestamp > now) {
+ window.kickQueue = window.kickQueue.slice(i);
+ break;
+ }
+ kick = value;
+ }
+ if (kick != null) {
+ gl.uniform1f(programInfo.uniformLocations.kick, kick);
+ }
+ }
+
+ {
+ gl.bindTexture(gl.TEXTURE_2D, state.textCanvasTexture);
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
+ gl.texImage2D(
+ gl.TEXTURE_2D,
+ 0,
+ gl.RGBA,
+ gl.RGBA,
+ gl.UNSIGNED_BYTE,
+ textCanvas
+ );
+
+ // compute text texture
+ gl.uniform1i(programInfo.uniformLocations.text, 0);
+ gl.activeTexture(gl.TEXTURE0 + 0);
+ gl.bindTexture(gl.TEXTURE_2D, state.textCanvasTexture);
+ }
+
+ {
+ const offset = 0;
+ const vertexCount = 4;
+ gl.drawArrays(gl.TRIANGLE_STRIP, offset, vertexCount);
+ }
+}
+
+function resize(gl: WebGLRenderingContext) {
+ const realToCSSPixels = window.devicePixelRatio;
+
+ const maxDim = Math.max(
+ Math.floor(gl.canvas.clientWidth * realToCSSPixels),
+ Math.floor(gl.canvas.clientHeight * realToCSSPixels)
+ );
+ const displayWidth = maxDim;
+ const displayHeight = maxDim;
+
+ if (gl.canvas.width !== displayWidth || gl.canvas.height !== displayHeight) {
+ gl.canvas.width = displayWidth;
+ gl.canvas.height = displayHeight;
+ }
+}
diff --git a/gallium-live/src/styles.js b/gallium-live/src/styles.js
index 9a4dc73..249b184 100644
--- a/gallium-live/src/styles.js
+++ b/gallium-live/src/styles.js
@@ -6,6 +6,7 @@ export function applyGlobalStyles() {
injectGlobal`
${styledNormalize}
body {
+ height: 100%;
}
`;
}