summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpommicket <pommicket@gmail.com>2023-07-01 00:02:47 -0400
committerpommicket <pommicket@gmail.com>2023-07-01 00:02:47 -0400
commit6b0572abfe4c1a479a0fa8d217b792f47f8fb820 (patch)
tree2a3740cef9f56c5f5c6459be07a8aa597758a3de
parent31ab3bb7dfccea391764491325e04d5cef22f83c (diff)
jabascrit
-rw-r--r--fractiform.js198
-rw-r--r--index.html16
2 files changed, 156 insertions, 58 deletions
diff --git a/fractiform.js b/fractiform.js
index 6cfeb2b..131213b 100644
--- a/fractiform.js
+++ b/fractiform.js
@@ -11,6 +11,8 @@ TODO:
- buffer
- parse input expressions
- show which widget generated an error
+ - don't make the error a whole big pop-up
+ - switch 'change' event listener to 'input' for auto-update
*/
let gl;
@@ -36,10 +38,23 @@ let html_id = 0;
let widget_choices;
let widget_search;
let widgets_container;
+let display_output;
+let display_output_span = null;
+let auto_update = true;
let width = 1920, height = 1920;
const widget_info = {
+ 'buffer': {
+ name: 'Buffer',
+ tooltip: 'outputs its input unaltered. useful for defining constants.',
+ inputs: [{name: 'in', id: 'input'}],
+ outputs: [{name: 'out', id: 'out', tooltip: 'has the same value as in'}],
+ controls: [],
+ func: function(state, inputs) {
+ return {out: inputs.input};
+ },
+ },
'mix': {
name: 'Mix',
tooltip: 'weighted average of two inputs',
@@ -121,15 +136,6 @@ const widget_info = {
return {out: {type: 'vec3', code: v}};
},
},
- 'output': {
- name: 'Output color',
- inputs: [{name: 'value', id: 'value'}],
- controls: [],
- outputs: [],
- func: function(state, inputs) {
- return {out: inputs.value};
- }
- },
'wtadd': {
name: 'Add (weighted)',
tooltip: 'add two numbers or vectors (with weights)',
@@ -472,8 +478,9 @@ function update_key_modifiers_from_event(e) {
function update_shader() {
let source = get_shader_source();
- if (source === null)
+ if (source === null) {
return;
+ }
let fragment_code = `
#ifdef GL_ES
precision highp float;
@@ -599,6 +606,37 @@ function type_vec(base_type, component_count) {
}
}
+function get_widget_by_name(name) {
+ for (let w of document.getElementsByClassName('widget')) {
+ if (get_widget_name(w) === name) {
+ return w;
+ }
+ }
+ return null;
+}
+
+function set_display_output_and_update_shader(to) {
+ for (let out of document.getElementsByClassName('out')) {
+ out.dataset.active = '0';
+ }
+
+ display_output = to;
+ let parts = to.split('.');
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ display_output = null;
+ return;
+ }
+
+ let widget = get_widget_by_name(parts[0]);
+ for (let out of widget.getElementsByClassName('out')) {
+ if (out.dataset.id === parts[1]) {
+ out.dataset.active = '1';
+ }
+ }
+
+ update_shader();
+}
+
function add_widget(func) {
let info = widget_info[func];
console.assert(info !== undefined, 'bad widget name: ' + func);
@@ -659,6 +697,12 @@ function add_widget(func) {
container.appendChild(document.createTextNode(' '));
container.appendChild(label);
root.appendChild(container);
+
+ input_element.addEventListener('change', function (e) {
+ if (auto_update) {
+ update_shader();
+ }
+ });
});
// controls
@@ -686,6 +730,12 @@ function add_widget(func) {
console.error('bad control type');
}
+ input.addEventListener('change', function (e) {
+ if (auto_update) {
+ update_shader();
+ }
+ });
+
input.id = 'gen-control-' + (++html_id);
input.classList.add('control-input');
let label = document.createElement('label');
@@ -709,7 +759,13 @@ function add_widget(func) {
}
let span = document.createElement('span');
span.classList.add('out');
+ span.dataset.id = output.id;
span.appendChild(document.createTextNode(output.name));
+ span.addEventListener('click', function (e) {
+ let name = get_widget_name(root);
+ set_display_output_and_update_shader(name + '.' + output.id);
+ e.preventDefault();
+ });
if ('tooltip' in output) {
span.title = output.tooltip;
}
@@ -726,7 +782,7 @@ class GLSLGenerationState {
constructor(widgets) {
this.widgets = widgets;
this.code = [];
- this.computing_inputs = {};
+ this.computing_inputs = new Set();
this.variable = 0;
}
@@ -843,18 +899,17 @@ class GLSLGenerationState {
if (dot !== -1) {
input = input.substring(0, dot);
}
- let esc_input = '-' + input; // prevent wacky stuff if input is an Object built-in
- let widget = this.widgets[esc_input];
+ let widget = this.widgets.get(input);
if (widget === undefined) {
- return {error: 'cannot find ' + input};
+ return {error: `cannot find widget '${input}'`};
}
- if (esc_input in this.computing_inputs) {
+ if (this.computing_inputs.has(input)) {
return {error: 'circular dependency at ' + input};
}
- this.computing_inputs[esc_input] = true;
+ this.computing_inputs.add(input);
let value = this.compute_widget_output(widget, field);
- delete this.computing_inputs[esc_input];
+ this.computing_inputs.delete(input);
return value;
}
@@ -888,13 +943,20 @@ class GLSLGenerationState {
}
}
+function get_widget_name(widget_div) {
+ let names = widget_div.getElementsByClassName('widget-name');
+ console.assert(names.length === 1, 'there should be exactly one widget-name input per widget');
+ return names[0].value;
+}
+
function parse_widgets() {
- let widgets = {};
+ let widgets = new Map();
for (let widget_div of document.getElementsByClassName('widget')) {
- let names = widget_div.getElementsByClassName('widget-name');
- console.assert(names.length <= 1, 'multiple name inputs for widget');
- let name = names.length > 0 ? names[0].value : null;
+ let name = get_widget_name(widget_div);
let func = widget_div.dataset.func;
+ if (!name) {
+ return {error: `widget has no name. please give it one.`};
+ }
let inputs = {};
let controls = {};
for (let input of widget_div.getElementsByClassName('in')) {
@@ -912,26 +974,28 @@ function parse_widgets() {
}
controls[id] = value;
}
- let widget = {
+ if (widgets.has(name)) {
+ return {error: `duplicate widget name: ${name}`};
+ }
+ widgets.set(name, {
func: func,
inputs: inputs,
controls: controls,
- };
- if (name !== null) {
- widgets['-' + name] = widget;
- }
- if (func === 'output') {
- widgets.output = widget;
- }
+ });
}
return widgets;
}
function export_widgets() {
let widgets = parse_widgets();
+ if ('error' in widgets) {
+ return {error: widgets.error};
+ }
+ console.assert(widgets instanceof Map);
let data = [];
- for (let name in widgets) {
- let widget = widgets[name];
+ for (let kv of widgets) {
+ let name = kv[0];
+ let widget = kv[1];
data.push(widget.func);
data.push(';');
data.push('n:');
@@ -954,15 +1018,22 @@ function export_widgets() {
data.pop(); // remove terminal separator
data.push(';;');
}
- data.pop(); // remove terminal separator
+ data.push('_out=');
+ data.push(display_output);
return data.join('');
}
function import_widgets(string) {
let widgets = [];
- console.log(string);
+ let output = null;
if (string) {
+ console.log(string);
for (let widget_str of string.split(';;')) {
+ if (widget_str.startsWith('_out=')) {
+ output = widget_str.substring('_out='.length);
+ continue;
+ }
+
let parts = widget_str.split(';');
let func = parts[0];
let widget = {name: null, func: func, inputs: {}, controls: {}};
@@ -999,20 +1070,21 @@ function import_widgets(string) {
widgets = [
{
name: 'output',
- func: 'output',
- inputs: {value: '#acabff'},
+ func: 'buffer',
+ inputs: {input: '#acabff'},
controls: {},
}
];
}
widgets_container.innerHTML = '';
- widgets.forEach(function (widget) {
+ for (let widget of widgets) {
let name = widget.name;
- let element = add_widget(widget.func);
- if (name.startsWith('-')) {
- element.getElementsByClassName('widget-name')[0].value = name.substring(1);
+ if (!(widget.func in widget_info)) {
+ return {error: `bad import string (widget type '${widget.func}' does not exist)`};
}
+ let element = add_widget(widget.func);
+ element.getElementsByClassName('widget-name')[0].value = name;
function assign_value(container, value) {
let element = container.getElementsByTagName('input')[0];
if (element === undefined) {
@@ -1036,7 +1108,9 @@ function import_widgets(string) {
);
assign_value(container, widget.controls[control]);
}
- });
+ };
+
+ set_display_output_and_update_shader(output);
}
function import_widgets_from_local_storage() {
@@ -1044,27 +1118,46 @@ function import_widgets_from_local_storage() {
}
function export_widgets_to_local_storage() {
- localStorage.setItem('widgets', export_widgets());
+ let widget_str = export_widgets();
+ console.log('save', widget_str);
+ localStorage.setItem('widgets', widget_str);
}
function get_shader_source() {
+ if (!display_output) {
+ show_error('no output chosen');
+ return null;
+ }
let widgets = parse_widgets();
- let output_widget = widgets.output;
- let state = new GLSLGenerationState(widgets);
- if (output_widget === undefined) {
- show_error('no output color');
+ if ('error' in widgets) {
+ show_error(widgets.error);
return null;
}
- let output = state.compute_widget_output(output_widget, 'out');
+ let state = new GLSLGenerationState(widgets);
+ let output = state.compute_input(display_output);
if ('error' in output) {
show_error(output.error);
return null;
}
- if (output.type !== 'vec3') {
- show_error('output color should have type vec3, but it has type ' + output.type);
+
+ switch (output.type) {
+ case 'float':
+ state.add_code(`return vec3(${output.code});\n`);
+ break;
+ case 'vec2':
+ state.add_code(`return vec3(${output.code}, 0.0);\n`);
+ break;
+ case 'vec3':
+ state.add_code(`return ${output.code};\n`);
+ break;
+ case 'vec4':
+ state.add_code(`return ${output.code}.xyz;\n`);
+ break;
+ default:
+ show_error(`bad type for output: ${output.type}`);
return null;
}
- state.add_code(`return ${output.code};\n`);
+
let code = state.get_code();
console.log(code);
export_widgets_to_local_storage();
@@ -1078,10 +1171,6 @@ function update_widget_choices() {
let name = widget_info[id].name;
let choice = choices[i];
let shown = name.toLowerCase().indexOf(search_term) !== -1;
- if (id === 'output') {
- shown = false;
- }
- choice.style.display = shown ? 'block' : 'none';
});
}
@@ -1194,7 +1283,6 @@ void main() {
update_widget_choices();
});
import_widgets_from_local_storage();
- update_shader();
frame(0.0);
window.addEventListener('keydown', on_key_press);
@@ -1348,7 +1436,7 @@ function compile_shader(name, type, source) {
}
function show_error(error) {
- console.log('error', error);
+ console.log('error:', error);
document.getElementById('error-message').innerText = error;
document.getElementById('error-dialog').showModal();
}
diff --git a/index.html b/index.html
index c8e22d3..e0ada22 100644
--- a/index.html
+++ b/index.html
@@ -87,7 +87,7 @@
height: 100%;
flex-grow: 0;
flex-shrink: 0;
- flex-basis: 20%;
+ flex-basis: 20em;
padding: 0.2em;
}
input, button {
@@ -113,8 +113,15 @@
}
.out {
font-weight: bold;
- }
- input[type=number] {
+ cursor: pointer;
+
+ user-select: none; /* most browsers */
+ -webkit-user-select: none; /* old chrome/edge */
+ -moz-user-select: none; /* old firefox */
+ -khtml-user-select: none; /* konqueror apparently lol */
+ -ms-user-select: none; /* IE */
+ }
+ input[type="number"] {
/* disable increment/decrement buttons (firefox) */
-moz-appearance: textfield;
}
@@ -140,6 +147,9 @@
bottom: 0.2em;
right: 0.2em;
}
+ .out[data-active="1"] {
+ color: #ff0;
+ }
.widget-title {
font-weight: bold;
margin: 2px;