Public
Edited
Oct 3, 2023
Importers
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
class ShaderFun {
constructor(meta, strings, args) {
this.meta = meta;
this.strings = strings;
this.args = args;
}
mapDeps(fun, cache) {
if (!cache.has(this)) {
const newArgs = this.args.map(arg => fun(arg.mapDeps(fun, cache)));
const sameArgs = this.args.every((arg, num) => arg === newArgs[num]);
const res = sameArgs ? this : new ShaderFun(this.meta, this.strings, newArgs);
cache.set(this, res);
}
return cache.get(this);
}
remix(newMeta, ...pairs) {
const map = new Map(pairs);
const res = this.mapDeps(fun => {
if (map.has(fun)) {
return map.get(fun);
}
return fun;
}, new Map());
res.meta = {...this.meta, ...newMeta};
const html = res.getHtml();
html.value = res;
return html;
}
getBaseName() {
const res = this.meta.name;
if (!res) {
throw new Error(`Shader meta object should have "name" property`);
}
return res;
}
getDeps() {
const res = [];
const resSet = new Set();
resSet.add(this);
for (const arg of this.args) {
for (const dep of arg.getDeps()) {
if (resSet.has(dep)) continue;
res.push(dep);
resSet.add(dep);
}
}
res.push(this);
return res;
}
setDepsNames() {
const depsOrder = this.getDeps();
const namesSet = new Set();
for (const dep of depsOrder) {
dep.name = '';
}
for (const dep of depsOrder) {
if (dep.name) {
namesSet.add(dep.name);
continue;
}
const baseName = dep.getBaseName();
let counter = 1;
while (true) {
let curName = `${baseName}${counter}`;
if (counter === 1) {
curName = baseName;
} else if (/[0-9]/.test(baseName[baseName.length - 1])) {
curName = `${baseName}_${counter}`;
}
if (!namesSet.has(curName)) {
dep.name = curName;
namesSet.add(dep.name);
break;
}
counter += 1;
}
}
return depsOrder;
}
getGenericSource(argMapFun, strMapFun=x=>x, name=null) {
if (this.strings.length !== this.args.length + 1) {
throw new Error(`Invalid number of arguments and strings of the template literal`);
}
name = name || this.name;
let res = strMapFun(this.strings[0]);
for (let i=0; i<this.args.length; i++) {
const arg = this.args[i];
const str = this.strings[i + 1];
if (arg instanceof ShaderFun) {
res += argMapFun(arg);
res += strMapFun(str);
} else {
throw new Error(`Shader placeholders in template literals should be instances of ShaderFun`);
}
}
res = res.replace(/(\s)([a-zA-Z_]+)(\s*\()/, `$1${name}$3`);
return res;
}
getReturnType() {
const substr = '@_NAME_@';
return this.getGenericSource(x => x.getBaseName(), x => x, substr).split(substr)[0].trim();
}
getFullSource() {
const depsOrder = this.setDepsNames();
let res = '';
for (const dep of depsOrder) {
res += dep.getGenericSource(x => x.name);
res += '\n\n';
}
return res;
}
getHtml() {
let isFullSource = false;
const spanStyle = {
'border-radius': '3px',
'background': '#fafafa',
'color': 'black',
'font-family': 'sans-serif',
'font-weight': '400',
'padding': '0 4px',
'border': '1px solid gray',
'position': 'relative',
'top': '0px',
};
const source = this.getGenericSource(arg => {
return `<span style="${styleToStr(spanStyle)}">${arg.meta.title || arg.getBaseName()}</span>`;
}, stringToHtml, `<span style="${styleToStr(spanStyle)}">${this.meta.title || this.getBaseName()}</span>`);
const divStyle = {
'font-family': 'monospace',
'white-space': 'pre',
'font-weight': '600',
'border': '1px solid gray',
'border-radius': '6px',
'font-size': '15px',
};
const toggleButtonStyle = {
'position': 'absolute',
'top': '2px',
'right': '0px',
'display': 'inline-block',
'border': '1px solid gray',
'border-radius': '3px',
'background': 'white',
'font-family': 'sans-serif',
'padding': '0 3px',
'margin-top': '2px',
'cursor': 'pointer',
'margin-right': '5px',
'float': 'right',
'font-size': '13px',
}
const titleStyle = {
'position': 'relative',
'font-family': 'sans-serif',
'border-bottom': '1px solid gray',
'padding': '4px 120px 4px 7px',
'font-size': '14px',
'font-weight': '400',
'background': '#ffd87b',
'border-radius': '5px 5px 0 0',
'white-space': 'initial',
};
const res = html`<div></div>`;
const getInnerRes = () => {
const toggleButton = html`<div style="${styleToStr(toggleButtonStyle)}" class="toggle-button">${isFullSource ? 'View short source' : 'View full source'}</div>`;
const title = html`<div style="${styleToStr(titleStyle)}">${this.meta.description || '&nbsp;'}${toggleButton}</div>`;
toggleButton.onclick = () => {
isFullSource = !isFullSource;
res.innerHTML = '';
res.appendChild(getInnerRes());
};
return html`<div style="${styleToStr(divStyle)}">${title}<div style="margin: 4px">${isFullSource ? this.getFullSource().trim() : source}</div></div>`;
}
res.appendChild(getInnerRes());
return res;
}
getMainSource() {
const type = this.getReturnType();
let resExpr;
if (type === 'vec4') {
resExpr = 'res'
} else if (type === 'vec3') {
resExpr = 'vec4(res, 1)';
} else if (type === 'vec2') {
resExpr = 'vec4(res, 0, 1)';
} else if (type === 'float') {
resExpr = 'vec4(res, res, res, 1)';
} else if (type === 'bool') {
resExpr = 'vec4(vec3(float(res)), 1)';
} else {
throw new Error(`Return type "${type}" is not supported`);
}
return `${type} res = ${this.name}();\ngl_FragColor = ${resExpr};`;
}
getCanvas(h, w) {
const fullSource = this.getFullSource();
const canvas = makeRegl(
this.getMainSource(),
fullSource,
h, w,
);
return canvas;
}
}
Insert cell
function styleToStr(style) {
return Object.entries(style).map(([k, v]) => `${k}: ${v}`).join('; ');
}
Insert cell
function stringToHtml(s) {
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
Insert cell
function shaderFun(meta) {
return (strings, ...args) => {
const shaderFun = new ShaderFun(meta, strings, args);
const res = shaderFun.getHtml();
res.value = shaderFun;
return res;
};
}
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more