more supersampling experiments

This commit is contained in:
Zack Buhman 2026-08-07 20:46:18 -05:00
parent a3d5b6650e
commit b1122bdbf5
8 changed files with 465 additions and 274 deletions

106
font.js
View File

@ -211,43 +211,46 @@ class FontRenderer {
});
console.log(canvasFormat);
this.renderPipeline = device.createRenderPipeline({
label: `${label} pipeline`,
layout: pipelineLayout,
vertex: {
module: shaderModule,
entryPoint: "vertexMain",
},
fragment: {
module: shaderModule,
entryPoint: "fragmentMain",
targets: [{
blend: {
color: {
dstFactor: "one-minus-src-alpha",
srcFactor: "src-alpha",
this.renderPipelines = {}
for (let i = 1; i <= 4; i += 3) {
this.renderPipelines[i] = device.createRenderPipeline({
label: `${label} pipeline sample count ${i}`,
layout: pipelineLayout,
vertex: {
module: shaderModule,
entryPoint: "vertexMain",
},
fragment: {
module: shaderModule,
entryPoint: "fragmentMain",
targets: [{
blend: {
color: {
dstFactor: "one-minus-src-alpha",
srcFactor: "src-alpha",
},
alpha: {
dstFactor: "one-minus-src-alpha",
srcFactor: "src-alpha",
},
operation: "add",
},
alpha: {
dstFactor: "one-minus-src-alpha",
srcFactor: "src-alpha",
},
operation: "add",
},
format: canvasFormat,
}]
},
primitive: {
topology: "triangle-list",
},
depthStencil: {
depthWriteEnabled: true,
depthCompare: "always",
format: "depth24plus",
},
multisample: {
count: 4,
}
});
format: canvasFormat,
}]
},
primitive: {
topology: "triangle-list",
},
depthStencil: {
depthWriteEnabled: true,
depthCompare: "always",
format: "depth24plus",
},
multisample: {
count: i,
}
});
}
//////////////////////////////////////////////////////////////////////
// sliders
@ -289,29 +292,36 @@ class FontRenderer {
//////////////////////////////////////////////////////////////////////
const configuration = updateSliders(this.sliders);
this.sliderArray[0] = configuration.scale;
this.sliderArray[1] = configuration.translateX;
this.sliderArray[2] = configuration.translateY;
this.sliderArray[0] = configuration.mode;
this.sliderArray[1] = configuration.scale;
this.sliderArray[2] = configuration.translateX;
this.sliderArray[3] = configuration.translateY;
this.sliderArray[4] = configuration.distanceThreshold;
this.sliderArray[5] = configuration.distanceScale;
//this.sliderArray[6] = configuration.distanceOffset;
this.sliderArray[6] = configuration.aliasingEnable;
this.sliderArray[7] = configuration.rampMin;
this.sliderArray[8] = configuration.outlineThreshold;
this.sliderArray[9] = configuration.outlineScale;
//this.sliderArray[10] = configuration.outlineOffset;
this.sliderArray[8] = configuration.subpixelEnable;
this.sliderArray[9] = configuration.subpixelStrength;
this.sliderArray[10] = configuration.subpixelOffset;
this.sliderArray[11] = configuration.rampMax;
this.sliderArray[12] = configuration.colorMix;
this.sliderArray[13] = configuration.colorScale;
this.sliderArray[14] = configuration.colorOffset;
this.sliderArray[12] = configuration.supersampleCount;
this.sliderArray[13] = configuration.supersampleOffset;
this.sliderArray[14] = configuration.outlineThreshold;
this.sliderArray[15] = configuration.outlineScale;
device.queue.writeBuffer(this.frames[frameNumber].configurationBuffer, 0,
this.sliderArray);
return configuration;
}
render(renderPass, frameNumber)
render(renderPass, frameNumber, sampleCount)
{
renderPass.setPipeline(this.renderPipeline);
renderPass.setPipeline(this.renderPipelines[sampleCount]);
renderPass.setIndexBuffer(this.buffer, "uint16");
renderPass.setBindGroup(0, this.viewBindGroup);
renderPass.setBindGroup(1, this.perFontBindGroup);

171
font.wgsl
View File

@ -7,22 +7,25 @@ struct View {
};
struct Config {
mode: f32,
scale: f32,
translateX: f32,
translateY: f32,
padding0: f32,
distanceThreshold: f32,
distanceScale: f32,
distanceOffset: f32,
padding1: f32,
aliasingEnable: f32,
rampMin: f32,
subpixelEnable: f32,
subpixelStrength: f32,
subpixelOffset: f32,
rampMax: f32,
supersampleCount: f32,
supersampleOffset: f32,
outlineThreshold: f32,
outlineScale: f32,
outlineOffset: f32,
padding2: f32,
colorMix: f32,
colorScale: f32,
colorOffset: f32,
padding3: f32,
};
struct GlyphBuffer {
@ -55,7 +58,6 @@ struct VertexOutput {
@location(0) @interpolate(linear, sample) texture: vec2f,
//@location(0) texture: vec2f,
@location(1) @interpolate(flat) glyphIndex: u32,
@location(2) foo: vec4f,
};
const vertices = array(
@ -80,18 +82,9 @@ fn vertexMain(input: VertexInput) -> VertexOutput
output.position = vec4f(screenPosition, 0.0, 1.0);
output.texture = texture;
output.glyphIndex = glyphIndex;
output.foo = vec4f(layoutBuffer[glyphIndex].position.xy, 0, 0);
return output;
}
fn blend(dst: vec4f, src: vec4f, distance: f32, threshold: f32, scale: f32, offset: f32) -> vec4f
{
let scaledDistance = (distance - threshold) * scale + offset;
let alpha = clamp(scaledDistance, 0.0, 1.0);
let opacity = clamp(src.a * alpha - dst.a, 0.0, 1.0);
return opacity * src + (1.0 - opacity) * dst;
}
fn palette(t: f32) -> vec4f
{
let a = vec3f(0.5, 0.5, 0.5);
@ -111,46 +104,130 @@ fn aliasedLinearSaturation(distance: f32, threshold: f32, scale: f32) -> f32
let gradientLength = length(vec2f(dx, dy));
let thresholdWidth = scale * gradientLength;
let aliased = saturate((distanceFromEdge / thresholdWidth) + 0.5);
return aliased;
return 1.0 - aliased;
}
@fragment
fn fragmentMain(input: VertexOutput) -> @location(0) vec4f
{
let p = glyphBuffer[input.glyphIndex].texPosition;
let s = glyphBuffer[input.glyphIndex].texSize;
struct FragmentOutput {
@location(0) color: vec4f,
};
let base = textureSample(fontTexture, linearSampler, input.texture * s + p);
fn sampleAt(texture: vec2f) -> f32
{
let base = textureSample(fontTexture, linearSampler, texture);
let distance = base.x;
let insideT = aliasedLinearSaturation(distance,
config.distanceThreshold,
config.distanceScale);
var t: f32;
if (config.aliasingEnable == 1.0) {
t = aliasedLinearSaturation(distance, config.distanceThreshold, config.distanceScale);
} else {
t = f32(distance > config.distanceThreshold);
}
let outlineT = aliasedLinearSaturation(distance,
config.outlineThreshold,
config.outlineScale);
return t;
}
fn sampleAt2(texture: vec2f,
threshold1: f32, scale1: f32,
threshold2: f32, scale2: f32) -> vec2f
{
let base = textureSample(fontTexture, linearSampler, texture);
let distance = base.x;
var t: vec2f;
if (config.aliasingEnable == 1.0) {
t.x = aliasedLinearSaturation(distance, threshold1, scale1);
t.y = aliasedLinearSaturation(distance, threshold2, scale2);
} else {
t.x = f32(distance > threshold1);
t.y = f32(distance > threshold2);
}
return t;
}
const offsets = array(
vec2f(0.125, 0.375),
vec2f(-0.125, -0.375),
vec2f(0.375, -0.125),
vec2f(-0.375, 0.125),
);
fn supersampleColor(texture: vec2f, s: vec2f, p: vec2f) -> vec4f
{
let insideColor = vec4f(1, 1, 1, 1);
let outlineColor = vec4f(0, 0, 0, 1);
let outsideColor = vec4f(0, 0, 0, 0);
let outline = mix(outlineColor, outsideColor, outlineT);
return mix(insideColor, outline, insideT);
let dx = dpdx(texture.x * s.x);
let dy = dpdy(texture.y * s.y);
/*
let insideColorW = vec4f(1, 1, 1, 1);
let insideColorP = palette(distance * config.colorScale + config.colorOffset);
let insideColor = mix(insideColorW, insideColorP, config.colorMix);
let outlineColor = vec4f(0, 0, 0, 1);
let fdim = config.supersampleCount;
let idim = i32(floor(fdim / 2.0));
var color = vec4f(0, 0, 0, 0);
color = blend(color, insideColor, distance,
config.distanceThreshold, config.distanceScale, config.distanceOffset);
var t = vec2f(0, 0);
if (fdim == 1) {
let coordinate = texture * s + p;
t = sampleAt2(coordinate,
config.distanceThreshold, config.distanceScale,
config.outlineThreshold, config.outlineScale);
} else {
for (var y = -idim; y <= idim; y++) {
for (var x = -idim; x <= idim; x++) {
//for (var i = 0; i < 4; i++) {
//let offset = vec2f(dx, dy) * offsets[i] * config.supersampleOffset;
let offset = vec2f(dx, dy) * vec2f(f32(x), f32(y)) * 0.1 * config.supersampleOffset;
let coordinate = texture * s + offset + p;
t += sampleAt2(coordinate,
config.distanceThreshold, config.distanceScale,
config.outlineThreshold, config.outlineScale);
//}
}
}
t *= (1.0 / (fdim * fdim));
}
color = blend(color, outlineColor, distance,
config.distanceThreshold - config.outlineThreshold,
config.outlineScale, config.outlineOffset);
return color;
*/
let color = mix(outsideColor, outlineColor, t.y);
return mix(color, insideColor, t.x);
}
fn subpixelColor(texture: vec2f, s: vec2f, p: vec2f) -> vec4f
{
let dx = dpdx(texture.x * s.x * 1/3);
let dy = dpdy(texture.y * s.y);
var subpixelColor = vec3f(0, 0, 0);
let insideColor = vec4f(1, 1, 1, 1);
let outsideColor = vec4f(0, 0, 0, 0);
if (config.subpixelEnable == 0) {
let coordinate = texture * s + p;
let t = sampleAt(coordinate);
return mix(outsideColor, insideColor, t);
} else {
for (var x = -1; x <= 1; x += 1) {
let offset = vec2f(dx * f32(x) * config.subpixelOffset, 0);
let coordinate = texture * s + offset + p;
subpixelColor[(x + 1)] = sampleAt(coordinate);
}
let mono = (subpixelColor.x + subpixelColor.y + subpixelColor.z) / 3;
let subpixelMix = mix(vec3f(mono, mono, mono), subpixelColor.xyz, config.subpixelStrength);
return vec4f(subpixelMix, length(subpixelMix));
}
}
@fragment
fn fragmentMain(input: VertexOutput) -> FragmentOutput
{
let s = glyphBuffer[input.glyphIndex].texSize;
let p = glyphBuffer[input.glyphIndex].texPosition;
var output: FragmentOutput;
if (config.mode == 0) {
output.color = subpixelColor(input.texture, s, p);
} else {
output.color = supersampleColor(input.texture, s, p);
}
return output;
}

View File

@ -1,23 +1,37 @@
import { Slider } from "./slider.js"
import { Select } from "./select.js"
import { RowHeader } from "./row-header.js"
function makeSliders()
{
const sliders = {
subpixelOptions: new RowHeader("global options"),
mode: new Select("mode", ["subpixel", "supersampling"]),
scale: new Slider("scale", 0.0, 0.01),
translateX: new Slider("translate x", -1, 1),
translateY: new Slider("translate y", -1, 1),
multisampleCount: new Slider("multisampleCount", 1, 4, 1),
distanceThreshold: new Slider("dist. threshold", 0.25, 0.75),
distanceScale: new Slider("distance scale", 0.0, 2.0),
//distanceOffset: new Slider("distance offset", -20.0, 20.0),
distanceThreshold: new Slider("distance threshold", 0.25, 0.75),
distanceScale: new Slider("distance scale", 0.0, 20.0),
aliasingEnable: new Slider("aliasing enable", 0, 1, 1),
//rampMin: new Slider("ramp min", 0, 1.0),
//rampMax: new Slider("ramp max", 0, 1.0),
subpixelOptions: new RowHeader("subpixel mode options"),
subpixelEnable: new Slider("subpixel enable", 0, 1, 1),
subpixelStrength: new Slider("subpixel strength", 0.0, 1.0),
subpixelOffset: new Slider("subpixel offset", 0.0, 10.0),
supersamplingOptions: new RowHeader("supersampling mode options"),
supersampleCount: new Slider("supersample count", 1.0, 15.0, 7),
supersampleOffset: new Slider("supersample offset", 0.0, 10.0),
outlineThreshold: new Slider("outline threshold", 0.0, 1.0),
outlineScale: new Slider("outline scale", 0.0, 50.0),
//outlineOffset: new Slider("outline offset", -20.0, 20.0),
outlineScale: new Slider("outline scale", 0.0, 20.0),
colorMix: new Slider("color mix", 0.0, 1.0),
colorScale: new Slider("color scale", 0.0, 10.0),
colorOffset: new Slider("color offset", 0.0, 3.1415),
//colorMix: new Slider("color mix", 0.0, 1.0),
//colorScale: new Slider("color scale", 0.0, 10.0),
//colorOffset: new Slider("color offset", 0.0, 3.1415),
};
/*

View File

@ -8,15 +8,71 @@
:root {
color-scheme: light dark;
}
div, span {
label {
font: 0.8rem sans-serif;
}
span {
padding-bottom: 0.1rem;
code {
font: 0.85rem monospace;
}
control-root {
position: fixed;
left: 0;
top: 0;
right: 0;
height: 0;
background: #eee;
}
control-main {
width: 350px;
float: right;
margin-right: 1rem;
background: #444;
}
control-grid {
display: grid;
grid-template-columns: auto auto minmax(4.25em, auto);
column-gap: 0.5rem;
align-items: center;
}
row {
display: contents;
}
row > *:first-child {
margin-left: 0.5rem;
justify-self: left;
}
row > *:last-child {
margin-right: 0.5rem;
justify-self: right;
}
row-header {
font: 0.9rem sans-serif;
font-weight: bold;
justify-self: center !important;
margin-top: 0.2em;
}
row-separator, row-header {
grid-column: 1 / span 3;
}
row-separator:not(:last-child) {
border-bottom: 1px solid #777;
}
row-header + row-separator {
border-bottom: 2px solid #888 !important;
}
select, option {
font: 0.8rem sans-serif;
}
input[type="range"] {
height: 1rem;
width: calc(100% - 0.5em);
}
.span-2 {
grid-column: span 2;
width: calc(100% - 0.5em);
}
canvas {
position: absolute;
left: 0;
@ -26,115 +82,17 @@
width: 100%;
height: 100%;
}
input[type="range"] {
height: 0.9rem;
width: 70%;
}
.control-root {
position: fixed;
left: 0;
top: 0;
right: 0;
height: 0;
background: #eee;
}
.control-main {
width: 340px;
float: right;
margin-right: 15px;
background: #444;
}
.label {
width: calc(35% - 0.3rem);
float: left;
clear: left;
overflow: hidden;
margin-left: 0.3rem;
margin-top: 0.25rem;
}
.value {
width: calc(65% - 0.2rem);
float: left;
margin-top: 0.1rem;
margin-right: 0.2rem;
}
.row:not(:last-child) {
border-bottom: 1px solid #999;
}
.row {
height: 1.5rem;
}
.code, code {
font: 0.85rem monospace;
padding-top: 0.15rem;
}
.float3 {
width: 30%;
display: inline-block;
text-align: right;
}
.float2 {
width: 40%;
display: inline-block;
text-align: right;
}
.range-span {
float: right;
}
</style>
<script src="index.js" type="module"></script>
<!--<script src="index2.js" type="module"></script>-->
</head>
<body>
<canvas></canvas>
<div class="control-root">
<div class="control-main" id="control-main">
<!--
<div class="row">
<span class="label">model</span>
<select class="value" id="model-select"></select>
</div>
<div class="row">
<span class="label">lighting</span>
<select class="value" id="lighting-select">
<option>unlit</option>
<option>diffuse</option>
</select>
</div>
-->
<!--
<div class="row">
<span class="label">eye</span>
<div class="value">
<code class="float3" id="eye-value-x">0.0</code>
<code class="float3" id="eye-value-y">0.0</code>
<code class="float3" id="eye-value-z">0.0</code>
</div>
</div>
<div class="row">
<span class="label">yaw/pitch</span>
<div class="value">
<code class="float2" id="yaw">0.0</code>
<code class="float2" id="pitch">0.0</code>
</div>
</div>
-->
<!--
<div class="row">
<span class="label">test2</span>
<select class="value">
<option value="test2">test2</option>
</select>
</div>
-->
<!--
<div class="row">
<span class="label">eye</span>
<input type="color" class="value">
</div>
-->
</div>
<control-root>
<control-main>
<control-grid id="control-parent">
</control-grid>
</control-main>
</control-root>
</body>
</html>

142
index.js
View File

@ -54,16 +54,8 @@ const module = await WebAssembly.instantiateStreaming(fetch("src/module.wasm"),
});
module.memory = memory;
function createDepthTexture() {
return device.createTexture({
size: [canvas.width, canvas.height],
format: 'depth24plus',
usage: GPUTextureUsage.RENDER_ATTACHMENT,
sampleCount: 4,
});
}
var depthTexture = undefined;
var multisampleTexture = undefined;
const eyeValueX = document.getElementById("eye-value-x");
const eyeValueY = document.getElementById("eye-value-y");
@ -72,16 +64,37 @@ const eyeValueZ = document.getElementById("eye-value-z");
const yawValue = document.getElementById("yaw");
const pitchValue = document.getElementById("pitch");
function recreateDepth(force)
function recreateDepth(canvasTexture, sampleCount)
{
if (force === true || canvas.clientWidth !== canvas.width || canvas.clientWidth !== canvas.height) {
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
const depthResized = depthTexture === undefined || canvasTexture.width != depthTexture.width || canvasTexture.height != depthTexture.height;
if (depthResized || depthTexture.sampleCount !== sampleCount) {
if (depthTexture !== undefined) {
depthTexture.destroy();
}
depthTexture = createDepthTexture();
depthTexture = device.createTexture({
format: 'depth24plus',
usage: GPUTextureUsage.RENDER_ATTACHMENT,
size: [canvasTexture.width, canvasTexture.height],
sampleCount: sampleCount,
});
}
if (sampleCount > 1) {
const multisampleResized = multisampleTexture === undefined || canvasTexture.width !== multisampleTexture.width || canvasTexture.height !== multisampleTexture.height;
if (multisampleResized || multisampleTexture.sampleCount !== sampleCount) {
if ((multisampleTexture !== undefined)) {
multisampleTexture.destroy();
}
multisampleTexture = device.createTexture({
format: canvasTexture.format,
usage: GPUTextureUsage.RENDER_ATTACHMENT,
size: [canvasTexture.width, canvasTexture.height],
sampleCount: sampleCount,
});
}
}
}
@ -217,6 +230,7 @@ const flatSliders = {
*/
const mousePosition = new Float32Array([0, 0, 0, 0]);
const mousePositionClick = new Float32Array([0, 0, 0, 0]);
/*
const snakeSliders = {
@ -229,10 +243,10 @@ const snakeSliders = {
function onClick(e)
{
mousePosition[0] = e.clientX / canvas.width;
mousePosition[1] = 1.0 - (e.clientY / canvas.height);
mousePositionClick[0] = e.clientX;
mousePositionClick[1] = e.clientY;
}
//canvas.addEventListener("click", onClick);
canvas.addEventListener("click", onClick);
function onMouseMove(e)
{
@ -245,44 +259,46 @@ canvas.addEventListener("mousemove", onMouseMove);
var canRender = false;
var frameNumber = 0;
var multisampleTexture = undefined;
var tempBuffer = undefined;
function render2()
{
if (canRender === true) {
canRender = false;
canvas.width = canvas.clientWidth * window.devicePixelRatio;
canvas.height = canvas.clientHeight * window.devicePixelRatio;
const configuration = fontRenderer.update(device, frameNumber);
const sampleCount = configuration.multisampleCount;
//const configuration = updateSliders(flatSliders);
//const configuration = updateSliders(snakeSliders);
canRender = false;
recreateDepth(false);
const canvasTexture = context.getCurrentTexture();
if ((multisampleTexture === undefined) ||
(multisampleTexture.width !== canvasTexture.width) ||
(multisampleTexture.height !== canvasTexture.height)) {
if ((multisampleTexture !== undefined)) {
multisampleTexture.destroy();
}
multisampleTexture = device.createTexture({
format: canvasTexture.format,
usage: GPUTextureUsage.RENDER_ATTACHMENT,
size: [canvasTexture.width, canvasTexture.height],
sampleCount: 4,
});
}
recreateDepth(canvasTexture, sampleCount);
updateView();
const clearValue = { r: 0.2, g: 0.2, b: 0.4, a: 1.0 };
const attachment0 = (sampleCount == 1) ? {
view: canvasTexture.createView(),
loadOp: "clear",
clearValue: clearValue,
storeOp: "store",
} : {
view: multisampleTexture.createView(),
resolveTarget: canvasTexture.createView(),
loadOp: "clear",
clearValue: clearValue,
storeOp: "store",
};
const encoder = device.createCommandEncoder();
const renderPass = encoder.beginRenderPass({
colorAttachments: [{
view: multisampleTexture.createView(),
//view: canvasTexture.createView(),
resolveTarget: canvasTexture.createView(),
loadOp: "clear",
clearValue: { r: 0.2, g: 0.2, b: 0.4, a: 1 },
storeOp: "store",
}],
colorAttachments: [attachment0],
depthStencilAttachment: {
view: depthTexture.createView(),
depthClearValue: 1.0,
@ -298,18 +314,51 @@ function render2()
//snakeRenderer.update(device, frameNumber, mousePosition, configuration);
//snakeRenderer.render(renderPass, frameNumber);
fontRenderer.update(device, frameNumber);
fontRenderer.render(renderPass, frameNumber);
fontRenderer.render(renderPass, frameNumber, sampleCount);
renderPass.end();
/*
encoder.copyTextureToBuffer({
texture: multisampleTexture,
}, {
buffer: tempBuffer,
bytesPerRow: canvasTexture.width * 4 * 4,
}, {
width: canvasTexture.width,
height: canvasTexture.height,
depthOrArrayLayers: 1,
});
*/
const commandBuffer = encoder.finish();
device.queue.submit([commandBuffer]);
frameNumber = (frameNumber + 1) % 2;
}
requestAnimationFrame(render2);
/*
tempBuffer.mapAsync(GPUMapMode.READ).then(() => {
const readBuffer = tempBuffer.getMappedRange(); // arraybuffer
//console.log(mousePositionClick);
const x = mousePositionClick[0];
const y = mousePositionClick[1];
const f32ReadBuffer = new Float32Array(readBuffer);
for (let i = 0; i < 2; i++) {
const a = f32ReadBuffer[((y + i) * canvasTexture.width + x) * 4 + 0];
const b = f32ReadBuffer[((y + i) * canvasTexture.width + x) * 4 + 1];
const c = f32ReadBuffer[((y + i) * canvasTexture.width + x) * 4 + 2];
const d = f32ReadBuffer[((y + i) * canvasTexture.width + x) * 4 + 3];
//console.log(a, b, c, d);
}
tempBuffer.unmap();
requestAnimationFrame(render2);
});
*/
requestAnimationFrame(render2);
} else {
requestAnimationFrame(render2);
}
}
function setCanRender()
{
@ -317,5 +366,4 @@ function setCanRender()
}
setInterval(setCanRender, 33.33); // 15 fps
recreateDepth(true);
requestAnimationFrame(render2);

26
row-header.js Normal file
View File

@ -0,0 +1,26 @@
const controlGrid = document.getElementById("control-parent");
class RowHeader {
constructor(label)
{
this.label = label;
const parent = controlGrid;
const rowHeaderE = document.createElement("row-header");
rowHeaderE.innerHTML = label;
const rowE = document.createElement("row");
const sepE = document.createElement("row-separator");
parent.appendChild(rowHeaderE);
parent.appendChild(sepE);
}
update()
{
return this.label;
}
}
export { RowHeader };

55
select.js Normal file
View File

@ -0,0 +1,55 @@
const controlGrid = document.getElementById("control-parent");
class Select {
constructor(label, options)
{
const parent = controlGrid;
const labelE = document.createElement("label");
labelE.innerHTML = label;
const selectE = document.createElement("select");
selectE.className = "span-2";
for (let i = 0; i < options.length; i++) {
const option = options[i];
const optionE = document.createElement("option");
optionE.setAttribute("value", i);
optionE.innerHTML = option;
selectE.appendChild(optionE);
}
const rowE = document.createElement("row");
const sepE = document.createElement("row-separator");
rowE.appendChild(labelE);
rowE.appendChild(selectE);
parent.appendChild(rowE);
parent.appendChild(sepE);
this.select = selectE;
this.options = options;
this.lastValue = undefined;
const storageValue = localStorage.getItem(this.label);
if (storageValue !== null) {
this.select.value = storageValue;
}
this.update();
}
update()
{
const rawValue = parseInt(this.select.value);
if (rawValue !== this.lastValue) {
this.lastValue = rawValue;
localStorage.setItem(this.label, rawValue);
}
return rawValue;
}
};
export { Select };

View File

@ -1,4 +1,4 @@
const controlMain = document.getElementById("control-main");
const controlGrid = document.getElementById("control-parent");
function remap(low1, high1, low2, high2, value)
{
@ -8,30 +8,33 @@ function remap(low1, high1, low2, high2, value)
class Slider {
constructor(label, low, high, max = 1024)
{
const labelE = document.createElement("span");
labelE.className = "label";
const parent = controlGrid;
const labelE = document.createElement("label");
labelE.innerHTML = label;
const valueE = document.createElement("div");
valueE.className = "value";
const inputE = document.createElement("input");
inputE.className = "range";
inputE.min = 0;
inputE.max = max;
inputE.type = "range";
const spanE = document.createElement("span");
spanE.className = "range-span code";
const rangeValueE = document.createElement("range-value");
const codeE = document.createElement("code");
rangeValueE.appendChild(codeE);
valueE.appendChild(inputE);
valueE.appendChild(spanE);
const rowE = document.createElement("row");
controlMain.appendChild(labelE);
controlMain.appendChild(valueE);
const sepE = document.createElement("row-separator");
rowE.appendChild(labelE);
rowE.appendChild(inputE);
rowE.appendChild(rangeValueE);
parent.appendChild(rowE);
parent.appendChild(sepE);
this.label = label;
this.input = inputE;
this.span = spanE;
this.code = codeE;
this.low = low;
this.high = high;
this.lastValue = undefined;
@ -47,15 +50,15 @@ class Slider {
update()
{
const rawValue = parseInt(this.input.value);
const value = remap(this.input.min, this.input.max, this.low, this.high, this.input.value);
const value = remap(this.input.min, this.input.max, this.low, this.high, rawValue);
if (rawValue !== this.lastValue) {
this.lastValue = rawValue;
localStorage.setItem(this.label, rawValue);
}
if (this.high > 10) {
this.span.innerHTML = value.toFixed(2);
this.code.innerHTML = value.toFixed(2);
} else {
this.span.innerHTML = value.toFixed(4);
this.code.innerHTML = value.toFixed(4);
}
return value;
}