color picker, text entry

This commit is contained in:
Zack Buhman 2026-08-07 21:59:49 -05:00
parent b1122bdbf5
commit c9a65e3bcf
7 changed files with 179 additions and 15 deletions

60
color-input.js Normal file
View File

@ -0,0 +1,60 @@
const controlGrid = document.getElementById("control-parent");
function parseColor(c)
{
const groups = c.match(/^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/i);
if (groups == null) {
throw new Error("invalid color", c);
} else {
return [parseInt(groups[1], 16), parseInt(groups[2], 16), parseInt(groups[3], 16)];
}
}
class ColorInput {
constructor(label, defaultValue)
{
const parent = controlGrid;
const labelE = document.createElement("label");
labelE.innerHTML = label;
const inputE = document.createElement("input");
inputE.type = "color";
inputE.className = "span-2";
const rowE = document.createElement("row");
const sepE = document.createElement("row-separator");
rowE.appendChild(labelE);
rowE.appendChild(inputE);
parent.appendChild(rowE);
parent.appendChild(sepE);
this.label = label;
this.input = inputE;
this.lastValue = undefined;
const storageValue = localStorage.getItem(this.label);
if (storageValue !== null) {
this.input.value = storageValue;
} else {
this.input.value = defaultValue;
}
this.update();
}
update()
{
const rawValue = this.input.value;
if (rawValue !== this.lastValue) {
this.lastValue = rawValue;
localStorage.setItem(this.label, rawValue);
}
return parseColor(rawValue);
}
};
export { ColorInput };

47
font.js
View File

@ -110,6 +110,18 @@ class FontRenderer {
} }
} }
font_layout__draw(s)
{
const length = Math.min(s.length, this.maxStringLength - 1);
for (let i = 0; i < length; i++) {
const c = s.charCodeAt(i) % 256;
this.stringBuffer[i] = c;
}
this.stringBuffer[length] = 0;
this.exports.font_layout__draw(this.fontLayoutAddress, this.stringBufferAddress);
}
constructor(device, canvasFormat, viewUniformBuffer, wgsl, module, fontBufferSrc) constructor(device, canvasFormat, viewUniformBuffer, wgsl, module, fontBufferSrc)
{ {
const label = "font"; const label = "font";
@ -127,6 +139,12 @@ class FontRenderer {
this.maxGlyphs = 1024; this.maxGlyphs = 1024;
this.fontLayoutAddress = this.exports.font_layout__create(this.fontBufferAddress, this.maxGlyphs); this.fontLayoutAddress = this.exports.font_layout__create(this.fontBufferAddress, this.maxGlyphs);
// text buffer
this.maxStringLength = 1024;
this.stringBufferAddress = this.exports.mem_alloc(this.maxStringLength);
this.stringBuffer = new Uint8Array(this.module.memory.buffer, this.stringBufferAddress);
this.lastString = undefined;
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
// shader module // shader module
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
@ -257,7 +275,7 @@ class FontRenderer {
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
this.sliders = makeSliders(); this.sliders = makeSliders();
this.sliderArray = new Float32Array(4 * 4); this.sliderArray = new Float32Array(4 * 6);
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
// buffers // buffers
@ -281,6 +299,21 @@ class FontRenderer {
update(device, frameNumber) update(device, frameNumber)
{ {
const configuration = updateSliders(this.sliders);
//////////////////////////////////////////////////////////////////////
// string draw
//////////////////////////////////////////////////////////////////////
if (this.lastString === undefined || this.lastString !== configuration.text) {
this.lastString = configuration.text;
this.font_layout__draw(configuration.text);
}
//////////////////////////////////////////////////////////////////////
// layout buffer
//////////////////////////////////////////////////////////////////////
this.instanceCount = this.exports.font_layout__layout_buffer_index(this.fontLayoutAddress); this.instanceCount = this.exports.font_layout__layout_buffer_index(this.fontLayoutAddress);
const layoutBufferSize = this.instanceCount * 4 * 4; const layoutBufferSize = this.instanceCount * 4 * 4;
@ -291,8 +324,6 @@ class FontRenderer {
// sliders // sliders
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
const configuration = updateSliders(this.sliders);
this.sliderArray[0] = configuration.mode; this.sliderArray[0] = configuration.mode;
this.sliderArray[1] = configuration.scale; this.sliderArray[1] = configuration.scale;
this.sliderArray[2] = configuration.translateX; this.sliderArray[2] = configuration.translateX;
@ -313,6 +344,14 @@ class FontRenderer {
this.sliderArray[14] = configuration.outlineThreshold; this.sliderArray[14] = configuration.outlineThreshold;
this.sliderArray[15] = configuration.outlineScale; this.sliderArray[15] = configuration.outlineScale;
this.sliderArray[16] = configuration.insideColor[0] / 255.0;
this.sliderArray[17] = configuration.insideColor[1] / 255.0;
this.sliderArray[18] = configuration.insideColor[2] / 255.0;
this.sliderArray[20] = configuration.outlineColor[0] / 255.0;
this.sliderArray[21] = configuration.outlineColor[1] / 255.0;
this.sliderArray[22] = configuration.outlineColor[2] / 255.0;
device.queue.writeBuffer(this.frames[frameNumber].configurationBuffer, 0, device.queue.writeBuffer(this.frames[frameNumber].configurationBuffer, 0,
this.sliderArray); this.sliderArray);
@ -342,8 +381,6 @@ async function loadFont(device, canvasFormat, viewUniformBuffer, module)
const renderer = new FontRenderer(device, canvasFormat, viewUniformBuffer, fontWgsl, module, liberationBuffer); const renderer = new FontRenderer(device, canvasFormat, viewUniformBuffer, fontWgsl, module, liberationBuffer);
module.instance.exports.font_layout__draw(renderer.fontLayoutAddress);
return renderer; return renderer;
} }

View File

@ -26,6 +26,9 @@ struct Config {
supersampleOffset: f32, supersampleOffset: f32,
outlineThreshold: f32, outlineThreshold: f32,
outlineScale: f32, outlineScale: f32,
insideColor: vec4f,
outlineColor: vec4f,
}; };
struct GlyphBuffer { struct GlyphBuffer {
@ -154,8 +157,8 @@ const offsets = array(
fn supersampleColor(texture: vec2f, s: vec2f, p: vec2f) -> vec4f fn supersampleColor(texture: vec2f, s: vec2f, p: vec2f) -> vec4f
{ {
let insideColor = vec4f(1, 1, 1, 1); let insideColor = vec4f(config.insideColor.xyz, 1);
let outlineColor = vec4f(0, 0, 0, 1); let outlineColor = vec4f(config.outlineColor.xyz, 1);
let outsideColor = vec4f(0, 0, 0, 0); let outsideColor = vec4f(0, 0, 0, 0);
let dx = dpdx(texture.x * s.x); let dx = dpdx(texture.x * s.x);
@ -223,7 +226,7 @@ fn fragmentMain(input: VertexOutput) -> FragmentOutput
var output: FragmentOutput; var output: FragmentOutput;
if (config.mode == 0) { if (config.mode == 1) {
output.color = subpixelColor(input.texture, s, p); output.color = subpixelColor(input.texture, s, p);
} else { } else {
output.color = supersampleColor(input.texture, s, p); output.color = supersampleColor(input.texture, s, p);

View File

@ -1,12 +1,14 @@
import { Slider } from "./slider.js" import { Slider } from "./slider.js"
import { Select } from "./select.js" import { Select } from "./select.js"
import { RowHeader } from "./row-header.js" import { RowHeader } from "./row-header.js"
import { TextInput } from "./text-input.js"
import { ColorInput } from "./color-input.js"
function makeSliders() function makeSliders()
{ {
const sliders = { const sliders = {
subpixelOptions: new RowHeader("global options"), subpixelOptions: new RowHeader("global options"),
mode: new Select("mode", ["subpixel", "supersampling"]), mode: new Select("mode", ["supersampling", "subpixel"]),
scale: new Slider("scale", 0.0, 0.01), scale: new Slider("scale", 0.0, 0.01),
translateX: new Slider("translate x", -1, 1), translateX: new Slider("translate x", -1, 1),
translateY: new Slider("translate y", -1, 1), translateY: new Slider("translate y", -1, 1),
@ -15,8 +17,10 @@ function makeSliders()
distanceThreshold: new Slider("distance threshold", 0.25, 0.75), distanceThreshold: new Slider("distance threshold", 0.25, 0.75),
distanceScale: new Slider("distance scale", 0.0, 20.0), distanceScale: new Slider("distance scale", 0.0, 20.0),
aliasingEnable: new Slider("aliasing enable", 0, 1, 1), aliasingEnable: new Slider("aliasing enable", 0, 1, 1),
//rampMin: new Slider("ramp min", 0, 1.0),
//rampMax: new Slider("ramp max", 0, 1.0), text: new TextInput("text"),
insideColor: new ColorInput("inside color", "#ffffff"),
outlineColor: new ColorInput("outline color", "#000000"),
subpixelOptions: new RowHeader("subpixel mode options"), subpixelOptions: new RowHeader("subpixel mode options"),
subpixelEnable: new Slider("subpixel enable", 0, 1, 1), subpixelEnable: new Slider("subpixel enable", 0, 1, 1),

View File

@ -61,13 +61,22 @@
border-bottom: 2px solid #888 !important; border-bottom: 2px solid #888 !important;
} }
select, option { select, option, input[type="text"] {
font: 0.8rem sans-serif; font: 0.8rem sans-serif;
} }
input[type="range"] {
input[type="range"], input[type="text"] {
height: 1rem; height: 1rem;
}
input[type="color"] {
height: 1.25rem;
}
input[type="range"] {
width: calc(100% - 0.5em); width: calc(100% - 0.5em);
} }
input[type="text"] {
width: calc(100% - 1em);
}
.span-2 { .span-2 {
grid-column: span 2; grid-column: span 2;
width: calc(100% - 0.5em); width: calc(100% - 0.5em);

View File

@ -49,8 +49,9 @@ extern "C" {
return font_layout->layout_buffer_index; return font_layout->layout_buffer_index;
} }
void font_layout__draw(FontLayout * font_layout) void font_layout__draw(FontLayout * font_layout, char const * s)
{ {
font_layout->draw_string("hexagon grid"); font_layout->layout_buffer_index = 0;
font_layout->draw_string(s);
} }
}; };

50
text-input.js Normal file
View File

@ -0,0 +1,50 @@
const controlGrid = document.getElementById("control-parent");
class TextInput {
constructor(label)
{
const parent = controlGrid;
const labelE = document.createElement("label");
labelE.innerHTML = label;
const inputE = document.createElement("input");
inputE.type = "text";
inputE.className = "span-2";
const rowE = document.createElement("row");
const sepE = document.createElement("row-separator");
rowE.appendChild(labelE);
rowE.appendChild(inputE);
parent.appendChild(rowE);
parent.appendChild(sepE);
this.label = label;
this.input = inputE;
this.lastValue = undefined;
const storageValue = localStorage.getItem(this.label);
if (storageValue !== null) {
this.input.value = storageValue;
} else {
this.input.value = "hexagon grid";
}
this.update();
}
update()
{
const rawValue = this.input.value;
if (rawValue !== this.lastValue) {
this.lastValue = rawValue;
localStorage.setItem(this.label, rawValue);
}
return rawValue;
}
};
export { TextInput };