diff options
author | pommicket <pommicket@gmail.com> | 2023-07-23 15:01:41 -0400 |
---|---|---|
committer | pommicket <pommicket@gmail.com> | 2023-07-23 15:01:41 -0400 |
commit | b83af09feb4597d3b50bf17a937569df7cdb1b04 (patch) | |
tree | 659a3f1a27d3df849c557afccdaf86184597f86a /fractiform.js | |
parent | 97c8b8a5a4e2212b369d046be239f21d131780b0 (diff) |
new system partly working
Diffstat (limited to 'fractiform.js')
-rw-r--r-- | fractiform.js | 224 |
1 files changed, 178 insertions, 46 deletions
diff --git a/fractiform.js b/fractiform.js index 710f706..d94244b 100644 --- a/fractiform.js +++ b/fractiform.js @@ -47,6 +47,7 @@ const builtin_widgets = [ //! .name: Buffer //! .description: outputs its input unaltered. useful for defining constants. //! x.name: input +//! x.id: input ` + ['float', 'vec2', 'vec3', 'vec4'].map((type) => ` ${type} buffer(${type} x) { return x; @@ -216,9 +217,9 @@ ${type} compare(float cmp1, float cmp2, ${type} less, ${type} greater) { //! nonneg.type: checkbox ` + ['float', 'vec2', 'vec3', 'vec4'].map((type) => ` -${type} sine_wave(${type} t, ${type} period, ${type} amp, ${type} phase, ${type} center, ${type} nonneg) { +${type} sine_wave(${type} t, ${type} period, ${type} amp, ${type} phase, ${type} center, int nonneg) { ${type} v = sin((t / period - phase) * 6.2831853); - if (nonneg) v = v * 0.5 + 0.5; + if (nonneg != 0) v = v * 0.5 + 0.5; return v; } `).join('\n'), @@ -268,10 +269,13 @@ vec3 hue_shift(vec3 color, float shift) { //! .name: Clamp //! .id: clamp //! .description: clamp a value between a minimum and maximum -//! val.name: value -//! val.description: input value +//! x.name: value +//! x.id: val +//! x.description: input value //! minimum.name: min +//! minimum.id: min //! maximum.name: max +//! maximum.id: max ` + ['float', 'vec2', 'vec3', 'vec4'].map((type) => ` ${type} clamp_(${type} x, ${type} minimum, ${type} maximum) { return clamp(x, minimum, maximum); @@ -279,8 +283,6 @@ ${type} clamp_(${type} x, ${type} minimum, ${type} maximum) { `).join('\n'), ]; -let widget_info = {}; - class Parser { constructor(string, line_number) { this.string = string; @@ -359,7 +361,7 @@ class Parser { } } -function install_widget(code) { +function parse_widget_definition(code) { code = code.trim(); let lines = code.split('\n'); let name = undefined; @@ -441,11 +443,20 @@ function install_widget(code) { } return x; }); + const parser = new Parser(lines.join('\n'), def_start + 1); + const definitions = []; + let function_name; while (!parser.error && !parser.eof()) { const definition_start = parser.i; let return_type = parser.parse_type(); - let name = parser.parse_ident(); + let fname = parser.parse_ident(); + if (!function_name) function_name = fname; + if (!parser.error && fname !== function_name) { + return {error: `function defined as both '${function_name}' and '${fname}'`}; + } + if (!id) id = function_name; + let definition_params = []; parser.expect('('); while (!parser.eof() && !parser.has(')')) { @@ -453,7 +464,49 @@ function install_widget(code) { const type = parser.parse_type(); const name = parser.parse_ident(); definition_params.push({type, name}); + + if (!params.has(name)) { + if (!definitions.size) { + params.set(name, {}); + } else if (!parser.error) { + return {error: `parameter ${name} does not exist`}; + } + } + } + + // we have all parameters now — fill out missing fields + if (!definitions.size) { + for (const param_name of params.keys()) { + const param = params.get(param_name); + if (!param.id) param.id = param_name; + if (!param.name) param.name = param.id; + if (!param.description) param.description = ''; + } } + + const input_types = new Map(); + const param_order = new Map(); + definition_params.forEach((p, index) => { + const is_control = p.type === 'int'; + const param = params.get(p.name); + if (param.type && !is_control) { + parser.set_error(`parameter ${p.name} should have type int since it's a ${param.type}, but it has type ${p.type}`); + } + if (!param.type && is_control) { + parser.set_error(`parameter ${p.name} has type int, so you should set a type for it, e.g. //! ${p.name}.type: checkbox`); + } + if (!is_control) { + input_types.set(param.id, p.type); + } + param_order.set(param.id, index); + }); + for (const param of params.values()) { + if (!input_types.has(param.id) && !param.type) { + parser.set_error(`parameter ${param.id} not specified in definition of ${function_name}`); + } + } + + parser.expect(')'); parser.expect('{'); let brace_depth = 1; @@ -465,28 +518,59 @@ function install_widget(code) { parser.advance(); } const definition_end = parser.i; - console.log('>>>',parser.string.substring(definition_start,definition_end)); + const definition = parser.string.substring(definition_start, definition_end); + definitions.push({ + input_types, + param_order, + return_type, + code: definition + }); } if (parser.error) { const error = parser.error; return {error: `on line ${error.line}: ${error.message}`}; } + if (!name) + name = id; + + const inputs = new Map(); + const controls = new Map(); + for (const param of params.values()) { + if (param.type) { + controls.set(param.id, param); + } else { + inputs.set(param.id, param); + } + } + + return { + id, + function_name, + name, + inputs, + controls, + description, + definitions + }; } +let widget_info = new Map(); for (const code of builtin_widgets) { - const result = install_widget(code); + const result = parse_widget_definition(code); if (result && result.error) { console.error(result.error); + } else { + widget_info.set(result.id, result); } } let widget_ids_sorted_by_name = []; -for (let id in widget_info) { - widget_ids_sorted_by_name.push(id); +for (const widget of widget_info.keys()) { + widget_ids_sorted_by_name.push(widget); } widget_ids_sorted_by_name.sort(function (a, b) { - a = widget_info[a].name; - b = widget_info[b].name; + a = widget_info.get(a).name; + b = widget_info.get(b).name; return a.localeCompare(b); }); @@ -567,12 +651,10 @@ uniform float ff_time; uniform vec2 ff_texture_size; varying vec2 ff_pos; -vec3 get_color() { - ${source} -} +${source} void main() { - gl_FragColor = vec4(get_color(), 1.0); + gl_FragColor = vec4(ff_get_color(), 1.0); } `; const vertex_code = ` @@ -716,11 +798,8 @@ function set_display_output_and_update_shader(to) { } function add_widget(func) { - let info = widget_info[func]; - console.assert(info !== undefined, 'bad widget name: ' + func); - console.assert('inputs' in info, `info for ${func} missing inputs member`); - console.assert('controls' in info, `info for ${func} missing controls member`); - console.assert('name' in info, `info for ${func} missing name member`); + let info = widget_info.get(func); + console.assert(info !== undefined, 'bad widget ID: ' + func); let root = document.createElement('div'); root.dataset.func = func; root.classList.add('widget'); @@ -765,7 +844,7 @@ function add_widget(func) { } // inputs - info.inputs.forEach(function (input) { + for (const input of info.inputs.values()) { let container = document.createElement('div'); container.classList.add('in'); console.assert('id' in input, 'input missing ID', input); @@ -796,10 +875,10 @@ function add_widget(func) { update_shader(); } }); - }); + } // controls - for (let control of info.controls) { + for (const control of info.controls.values()) { let container = document.createElement('div'); container.classList.add('control'); console.assert('id' in control, 'control missing ID', control); @@ -864,6 +943,7 @@ function add_widget(func) { class GLSLGenerationState { constructor(widgets) { this.widgets = widgets; + this.functions = new Set(); this.code = []; this.computing_inputs = new Set(); this.variable = 0; @@ -879,7 +959,12 @@ class GLSLGenerationState { } get_code() { - return this.code.join(''); + return ` +${Array.from(this.functions).join('')} +vec3 ff_get_color() { +${this.code.join('')} +}`; + } compute_input(input) { @@ -998,21 +1083,66 @@ class GLSLGenerationState { compute_widget_output(widget) { if (!('output' in widget)) { - let info = widget_info[widget.func]; - let inputs = {}; - for (let input in widget.inputs) { + const info = widget_info.get(widget.func); + const args = new Map(); + const input_types = new Map(); + for (const input in widget.inputs) { let value = this.compute_input(widget.inputs[input]); if ('error' in value) { widget.output = {error: value.error}; return {error: value.error}; } - inputs[input] = value; + args.set(input, value.code); + input_types.set(input, value.type); } - for (let control in widget.controls) { - inputs[control] = widget.controls[control]; + for (const control in widget.controls) { + args.set(control, widget.controls[control]); } - let output = info.func(this, inputs); - widget.output = output; + + let best_definition = undefined; + let best_score = -Infinity; + for (const definition of info.definitions) { + if (definition.input_types.length !== input_types.length) + continue; + if (definition.param_order.length !== args.length) + continue; + let score = 0; + for (const [input_name, input_type] of definition.input_types) { + let got_type = input_types.get(input_name); + if (got_type === input_type) { + score += 1; + } else if (got_type === 'float') { + // implicit conversion + } else { + score = -Infinity; + } + } + if (score > best_score) { + best_definition = definition; + } + } + + if (!best_definition) { + let s = []; + for (const [n, t] of input_types) { + s.push(`${n}:${t}`); + } + return {error: `bad types for ${info.name}: ${s.join(', ')}`}; + } + + const output_var = this.next_variable(); + const definition = best_definition; + const args_code = new Array(args.length); + for (const [arg_name, arg_code] of args) { + args_code[definition.param_order.get(arg_name)] = arg_code; + } + const type = definition.return_type; + this.functions.add(definition.code); + this.add_code(`${type} ${output_var} = ${info.function_name}(${args_code.join(',')});\n`); + widget.output = { + code: output_var, + type, + }; } let output = widget.output; @@ -1033,11 +1163,11 @@ function parse_widgets() { } let inputs = {}; let controls = {}; - for (let input of widget_div.getElementsByClassName('in')) { + for (const input of widget_div.getElementsByClassName('in')) { let id = input.dataset.id; inputs[id] = input.getElementsByClassName('entry')[0].innerText; } - for (let control of widget_div.getElementsByClassName('control')) { + for (const control of widget_div.getElementsByClassName('control')) { let id = control.dataset.id; let input = control.getElementsByClassName('control-input')[0]; let value; @@ -1111,7 +1241,7 @@ function import_widgets(string) { let parts = widget_str.split(';'); let func = parts[0]; let widget = {name: null, func: func, inputs: {}, controls: {}}; - let info = widget_info[func]; + const info = widget_info.get(func); parts.splice(0, 1); for (let part of parts) { let kv = part.split(':'); @@ -1154,7 +1284,7 @@ function import_widgets(string) { widgets_container.innerHTML = ''; for (let widget of widgets) { let name = widget.name; - if (!(widget.func in widget_info)) { + if (!widget_info.has(widget.func)) { return {error: `bad import string (widget type '${widget.func}' does not exist)`}; } let element = add_widget(widget.func); @@ -1177,13 +1307,13 @@ function import_widgets(string) { console.error('bad element', element); } } - for (let input in widget.inputs) { + for (const input in widget.inputs) { let container = Array.from(element.getElementsByClassName('in')).find( function (e) { return e.dataset.id === input; } ); assign_value(container, widget.inputs[input]); } - for (let control in widget.controls) { + for (const control in widget.controls) { let container = Array.from(element.getElementsByClassName('control')).find( function (e) { return e.dataset.id === control; } ); @@ -1249,9 +1379,10 @@ function update_widget_choices() { let search_term = widget_search.value.toLowerCase(); let choices = widget_choices.getElementsByClassName('widget-choice'); widget_ids_sorted_by_name.forEach(function (id, i) { - let name = widget_info[id].name; + let name = widget_info.get(id).name; let choice = choices[i]; let shown = name.toLowerCase().indexOf(search_term) !== -1; + // TODO }); } @@ -1349,14 +1480,15 @@ void main() { sampler_texture = gl.createTexture(); // add widget buttons - for (let id of widget_ids_sorted_by_name) { - let widget = widget_info[id]; + for (const id of widget_ids_sorted_by_name) { + let widget = widget_info.get(id); let button = document.createElement('button'); button.classList.add('widget-choice'); - if ('tooltip' in widget) { - button.title = widget.tooltip; + if ('description' in widget) { + button.title = widget.description; } button.appendChild(document.createTextNode(widget.name)); + button.dataset.id = id; widget_choices.appendChild(button); button.addEventListener('click', function (e) { add_widget(id); |