UNPKG

vectorengine

Version:

This is a minimal WebGL vector rendering engine written for AssemblyScript.

589 lines (475 loc) 16.3 kB
const V_COLOR_LINE_SHADER = /*glsl*/`#version 300 es precision highp float; uniform uint u_color; uniform float u_scale_x; uniform float u_scale_y; uniform float u_rotation; uniform float u_loop_x; uniform float u_loop_y; in vec2 position; out vec4 c; void main() { vec2 pos = vec2(position.x * u_scale_x, position.y * u_scale_y); float cosine = cos(u_rotation); float sine = sin(u_rotation); float x = (cosine * pos.x) + (sine * pos.y); float y = (cosine * pos.y) - (sine * pos.x); pos.x = x + u_loop_x; pos.y = y + u_loop_y; gl_Position = vec4( pos, 0.0, 1.0 ); uint mask = uint(0xff); // byte mask // convert 32-bit hexadecimal color to four float color uint red = u_color >> 24; uint green = (u_color >> 16) & mask; uint blue = (u_color >> 8) & mask; uint alpha = u_color & mask; c = vec4( float(red) / 255.0, float(green) / 255.0, float(blue) / 255.0, float(alpha) / 255.0 ); } `; // THIS IS THE FRAGMENT SHADER const F_SHADER = /*glsl*/ `#version 300 es precision highp float; in vec4 c; out vec4 color; void main() { color = c; } `; var memory; var update; var pre_update; var post_update; var wasm_obj; var importObject; var gl; var color_line_program; var color_location; var scale_x_location; var scale_y_location; var rotation_location; var offset_x_location; var offset_y_location; var buffer; var position_al; //var inputHeapPtr; var keyPtr; var mouseDownPtr; var mouseUpPtr; /* var mouseLeftDownPtr; var mouseRightDownPtr; var mouseMiddleDownPtr; var mouseLeftUpPtr; var mouseRightUpPtr; var mouseMiddleUpPtr; */ var mouseXPtr; var mouseYPtr; var lastTime = 0; // can use variables below for fps // var frames = 0; // var timePassed = 0; function render() { if (update != null) { let delta = 0; if (lastTime !== 0) { delta = (new Date().getTime() - lastTime); /* timePassed += delta; frames++; if (timePassed > 1000) { frames = 0; time_passed = 0; } */ } lastTime = new Date().getTime(); clear(); if (pre_update != null) { pre_update(); } update(delta); if (post_update != null) { post_update(); } } requestAnimationFrame(render); } function clear() { gl.clearColor(0.0, 0.0, 0.0, 1.0); gl.clear(gl.COLOR_BUFFER_BIT); } function renderLine(line_data_pointer, len, x, y, rot, scale_x, scale_y, color, type) { const line_data = new Float32Array(memory.buffer, line_data_pointer, len); gl.bufferData(gl.ARRAY_BUFFER, line_data, gl.DYNAMIC_DRAW); gl.uniform1ui(color_location, color); gl.uniform1f(scale_x_location, scale_x); gl.uniform1f(scale_y_location, scale_y); gl.uniform1f(rotation_location, rot); gl.uniform1f(offset_x_location, x); gl.uniform1f(offset_y_location, y); gl.vertexAttribPointer(position_al, /*dimensions*/2, /*data type*/gl.FLOAT, /*normalize*/false, /*stride*/0, /*offset*/0); gl.drawArrays(type, 0, line_data.length / 2); } var keyDownPos; function keyDown(event) { event.preventDefault(); if (keyPtr == null) { return; } if (keyDownPos == null) { keyDownPos = new Uint8Array(memory.buffer, keyPtr, 100) } keyDownPos[event.keyCode] = true; } function keyUp(event) { event.preventDefault(); if (keyDownPos == null) { return; } keyDownPos[event.keyCode] = false; } let mousePtr; function mouseMove(event) { if (mousePtr == null) { mousePtr = new Int32Array(memory.buffer, mouseXPtr, 8); } mousePtr[0] = event.offsetX; mousePtr[1] = event.offsetY; } function onContext(event) { event.preventDefault(); } var mousePos; function mouseDown(event) { if (mousePos == null) { mousePos = new Uint8Array(memory.buffer, mouseDownPtr, 3); } mousePos[event.which - 1] = true; event.preventDefault(); } function mouseUp(event) { if (mousePos == null) { mousePos = new Uint8Array(memory.buffer, mouseDownPtr, 3); } mousePos[event.which - 1] = false; event.preventDefault(); } function getString(string_index) { const buffer = memory.buffer; const U32 = new Uint32Array(buffer); const id_addr = string_index / 4 - 2; const id = U32[id_addr]; if (id !== 0x01) throw Error(`not a string index=${string_index} id=${id}`); const len = U32[id_addr + 1]; const str = new TextDecoder('utf-16').decode(buffer.slice(string_index, string_index + len)); return str; } // AUDIO STUFF const SQUARE_WAVE = 0; const TRIANGLE_WAVE = 1; const SAW_WAVE = 2; const SIN_WAVE = 3; const NOISE_WAVE = 4; // I KNOW NOISE ISN'T A WAVE, BUT IT'S IN THIS GROUP const SLIDE_NONE = 0; const SLIDE_LIN = 1; const SLIDE_EXP = 2; var ACTX; var noise_init = false; var noise_data = new Float32Array(16384); var master_volume = 1.0; function envelope(attack_time, decay_time, sustain_time, release_time, attack_punch, input_node) { let envelope_node = ACTX.createGain(); envelope_node.gain.setValueAtTime(0.0, ACTX.currentTime); envelope_node.gain.linearRampToValueAtTime(attack_punch, ACTX.currentTime + attack_time); envelope_node.gain.linearRampToValueAtTime(1, ACTX.currentTime + attack_time + decay_time); envelope_node.gain.setValueAtTime(1, ACTX.currentTime + attack_time + decay_time + sustain_time); envelope_node.gain.linearRampToValueAtTime(0.0, ACTX.currentTime + attack_time + decay_time + sustain_time + release_time); input_node.connect(envelope_node); return envelope_node; } function noiseNode() { let noise_node = ACTX.createBufferSource(); let buffer = ACTX.createBuffer(1, 16384, ACTX.sampleRate); if (noise_init === false) { for (var i = 0; i < 16384; i += 10) { noise_data[i] = Math.random() * 2 - 1; for (var j = 1; j < 10; j++) { noise_data[i + j] = noise_data[i]; } } noise_init = true; } let data = buffer.getChannelData(0); data.set(noise_data); noise_node.buffer = buffer; noise_node.loop = true; return noise_node; } function dutyCycle(cycle_len, cycle_pct, total_time, input_node) { let t = 0; let start_mute = (1.0 - cycle_pct) * cycle_len; let duty_cycle_node = ACTX.createGain(); duty_cycle_node.gain.setValueAtTime(1, ACTX.currentTime); while (t < total_time) { duty_cycle_node.gain.setValueAtTime(1, ACTX.currentTime + t + start_mute * 0.98);// + start_mute duty_cycle_node.gain.linearRampToValueAtTime(0, ACTX.currentTime + t + start_mute);// + start_mute duty_cycle_node.gain.setValueAtTime(0, ACTX.currentTime + t + cycle_len * 0.98); duty_cycle_node.gain.linearRampToValueAtTime(1, ACTX.currentTime + t + cycle_len); t += cycle_len; // cycle_length; } input_node.connect(duty_cycle_node); return duty_cycle_node; } function getOscType(wave_type) { if (wave_type === SQUARE_WAVE) { return "square"; } else if (wave_type === TRIANGLE_WAVE) { return "triangle"; } else if (wave_type === SAW_WAVE) { return "sawtooth"; } else if (wave_type === SIN_WAVE) { return "sine"; } alert('invalid wave type'); return "square"; } function frequencySlide(frequency, time, input_node) { input_node.frequency.linearRampToValueAtTime(frequency, ACTX.currentTime + time); // value in hertz return input_node; } function delayedFrequencySlide(frequency, frequency_mult, delay_start, end_time, slide_type, input_node) { input_node.frequency.setValueAtTime(frequency, ACTX.currentTime + delay_start); if (slide_type === SLIDE_LIN) { input_node.frequency.linearRampToValueAtTime(frequency * frequency_mult, ACTX.currentTime + end_time); } else if (slide_type === SLIDE_NONE) { input_node.frequency.setValueAtTime(frequency * frequency_mult, ACTX.currentTime + delay_start); } else if (slide_type === SLIDE_EXP) { input_node.frequency.exponentialRampToValueAtTime(frequency * frequency_mult, ACTX.currentTime + end_time); } return input_node; } function oscillatorTone(frequency, wave_type) { var tone = ACTX.createOscillator(); tone.type = getOscType(wave_type); tone.frequency.setValueAtTime(frequency, ACTX.currentTime); // value in hertz return tone; } function vibrato(wave_type, vibrato_freq, shift_time, time, input_node) { let gain_node = ACTX.createGain(); let osc = ACTX.createOscillator(); osc.type = getOscType(wave_type); osc.frequency.setValueAtTime(vibrato_freq, ACTX.currentTime); // value in hertz osc.connect(gain_node); osc.start(ACTX.currentTime + shift_time); osc.stop(ACTX.currentTime + time); input_node.connect(gain_node); return gain_node; } function flange(delay_time, feedback_volume, input_node) { let delay_node = ACTX.createDelay(); delay_node.delayTime.value = delay_time; let feeback = ACTX.createGain(); feedback_volume.gain.value = feedback_volume; input_node.connect(delay_node); delay_node.connect(feedback) feedback.connect(input_node); return feedback; } function highPassFilter(hpf_freq, time, input_node) { let high_pass_filter = ACTX.createBiquadFilter(); high_pass_filter.type = "highpass"; high_pass_filter.frequency.value = hpf_freq; input_node.connect(high_pass_filter); return high_pass_filter; } function lowPassFilter(lpf_freq, time, input_node, ramp_freq = 0) { let low_pass_filter = ACTX.createBiquadFilter(); low_pass_filter.type = "lowpass"; low_pass_filter.frequency.value = lpf_freq; if (ramp_freq !== 0) { low_pass_filter.frequency.linearRampToValueAtTime(ramp_freq, ACTX.currentTime + time); } input_node.connect(low_pass_filter); return low_pass_filter; } function playSFX(wave_type, freq, freq_slide, delay_freq_start_time_pct, delay_freq_mult, vibrato_time, vibrato_shift_time, vibrato_freq, vibrato_wave_type, low_pass_freq, low_pass_freq_ramp, high_pass_freq, attack_time, decay_time, sustain_time, release_time, attack_punch_volume, duty_cycle_len, duty_cycle_pct, flange_delay_time, flange_feedback_volume, gain, noise_detune, noise_detune_slide, slide_type) { if (ACTX == null) { ACTX = new AudioContext() } const time = attack_time + decay_time + sustain_time + release_time; if (wave_type === NOISE_WAVE) { let noise_buffer = noiseNode(); noise_buffer.detune.setValueAtTime(noise_detune * 100, ACTX.currentTime); noise_buffer.detune.linearRampToValueAtTime(noise_detune_slide * 100, ACTX.currentTime + time); let gain_node = ACTX.createGain(); gain_node.gain.setValueAtTime(gain, ACTX.currentTime); noise_buffer.connect(gain_node); let audio = gain_node; if (high_pass_freq > 0) { audio = highPassFilter(high_pass_freq, time, audio); } if (low_pass_freq > 0) { audio = lowPassFilter(low_pass_freq, time, audio, low_pass_freq_ramp); } if (duty_cycle_len > 0) { audio = dutyCycle(duty_cycle_len, duty_cycle_pct, time, audio); } if (flange_delay_time > 0) { audio = flange(flange_delay_time, flange_feedback_volume, audio); } if (vibrato_time > 0) { audio = vibrato(vibrato_wave_type, vibrato_freq, vibrato_shift_time, time, audio); } audio = envelope(attack_time, decay_time, sustain_time, release_time, attack_punch_volume, audio); let master_volume_gain = ACTX.createGain(); master_volume_gain.value = master_volume; audio.connect(master_volume_gain); master_volume_gain.connect(ACTX.destination); noise_buffer.start(); noise_buffer.stop(ACTX.currentTime + time); return; } let tone = oscillatorTone(freq, wave_type); let audio = tone; if (freq_slide != 0) { if (delay_freq_start_time_pct != 0) { audio = frequencySlide(freq_slide, delay_freq_start_time_pct, audio); audio = delayedFrequencySlide(freq_slide, delay_freq_mult, delay_freq_start_time_pct, time, slide_type, audio); } else { audio = frequencySlide(freq_slide, time, audio); } } else if (delay_freq_start_time_pct != 0) { audio = delayedFrequencySlide(freq, delay_freq_mult, delay_freq_start_time_pct, time, slide_type, audio); } if (high_pass_freq > 0) { audio = highPassFilter(high_pass_freq, time, audio); } if (low_pass_freq > 0) { audio = lowPassFilter(low_pass_freq, time, audio, low_pass_freq_ramp); } let gain_node = ACTX.createGain(); gain_node.gain.value = gain; audio.connect(gain_node); audio = gain_node; audio = envelope(attack_time, decay_time, sustain_time, release_time, attack_punch_volume, audio); if (duty_cycle_len > 0) { audio = dutyCycle(duty_cycle_len, duty_cycle_pct, time, audio); } if (flange_delay_time > 0) { audio = flange(flange_delay_time, flange_feedback_volume, audio); } if (vibrato_time > 0) { audio = vibrato(vibrato_wave_type, vibrato_freq, vibrato_shift_time, time, audio); } let master_volume_gain = ACTX.createGain(); master_volume_gain.value = master_volume; audio.connect(master_volume_gain); master_volume_gain.connect(ACTX.destination); //audio.connect(ACTX.destination); tone.start(); tone.stop(ACTX.currentTime + time); } export function runVectorGame(canvas_id, wasm_file, update_name, memory_pages = 100) { const canvas = document.getElementById(canvas_id); var w = window.innerWidth * 0.99; var h = window.innerHeight * 0.99; if (w > h) { cnvs.width = h; cnvs.height = h; } else { cnvs.width = w; cnvs.height = w; } memory = new WebAssembly.Memory({ initial: memory_pages }); importObject = { env: { abort: (msg, file, line, colm) => { console.log(`${line}:${colm}`); console.log(`msg: ${getString(msg)}`); console.log(`file: ${getString(file)}`); }, memory: memory, seed: Date.now, renderLineData: renderLine, canvasWidth: canvas.width, canvasHeight: canvas.height, playSFX: playSFX, setInputPtrs: (keyboard_ptr, mouse_down_ptr, mouse_up_ptr, mouse_x_ptr, mouse_y_ptr) => { keyPtr = keyboard_ptr; mouseDownPtr = mouse_down_ptr; mouseUpPtr = mouse_up_ptr; mouseXPtr = mouse_x_ptr; mouseYPtr = mouse_y_ptr; }, logf32: (f) => console.log(`f32: ${f}`), logi32: (i) => console.log(`i32: ${i}`), } }; (async () => { console.log(`wasm_file=${wasm_file}`); wasm_obj = await WebAssembly.instantiateStreaming(fetch(wasm_file), importObject); update = wasm_obj.instance.exports[update_name]; pre_update = wasm_obj.instance.exports["VectorEngineExports.preUpdate"]; post_update = wasm_obj.instance.exports["VectorEngineExports.postUpdate"]; requestAnimationFrame(render); canvas.addEventListener('mousemove', mouseMove); canvas.addEventListener('mousedown', mouseDown); document.addEventListener('mouseup', mouseUp); canvas.oncontextmenu = onContext; document.addEventListener('keyup', keyUp); document.addEventListener('keydown', keyDown); })(); gl = canvas.getContext('webgl2'); const color_line_vertex_shader = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(color_line_vertex_shader, V_COLOR_LINE_SHADER); gl.compileShader(color_line_vertex_shader); const fragment_shader = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(fragment_shader, F_SHADER); gl.compileShader(fragment_shader); color_line_program = gl.createProgram(); gl.attachShader(color_line_program, color_line_vertex_shader); gl.attachShader(color_line_program, fragment_shader); gl.linkProgram(color_line_program); gl.useProgram(color_line_program); color_location = gl.getUniformLocation(color_line_program, "u_color"); scale_x_location = gl.getUniformLocation(color_line_program, "u_scale_x"); scale_y_location = gl.getUniformLocation(color_line_program, "u_scale_y"); rotation_location = gl.getUniformLocation(color_line_program, "u_rotation"); offset_x_location = gl.getUniformLocation(color_line_program, "u_loop_x"); offset_y_location = gl.getUniformLocation(color_line_program, "u_loop_y"); buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buffer); position_al = gl.getAttribLocation(color_line_program, 'position'); gl.enableVertexAttribArray(position_al); }