texture smoothing on mac

This commit is contained in:
CNCKitchen
2026-04-01 10:59:23 +02:00
parent f7187bc1df
commit bc5c4625fc
4 changed files with 90 additions and 15 deletions
+79 -4
View File
@@ -59,6 +59,83 @@ const settings = {
useDisplacement: false,
};
// ── Canvas filter support (Safari / iOS WebView don't support ctx.filter) ────
const CANVAS_FILTER_SUPPORTED = (() => {
try {
const ctx = document.createElement('canvas').getContext('2d');
ctx.filter = 'blur(1px)';
return ctx.filter === 'blur(1px)';
} catch (e) { return false; }
})();
/**
* Box-blur one row of RGBA pixels (horizontal pass).
* Operates in-place reading from `src` and writing to `dst`.
*/
function _boxBlurH(src, dst, w, h, r) {
const iarr = 1 / (2 * r + 1);
for (let y = 0; y < h; y++) {
const row = y * w;
for (let ch = 0; ch < 4; ch++) {
let val = 0;
// Seed with left-edge pixel repeated r+1 times plus the first r pixels
for (let x = -r; x <= r; x++) val += src[(row + Math.max(0, Math.min(x, w - 1))) * 4 + ch];
for (let x = 0; x < w; x++) {
val += src[(row + Math.min(x + r, w - 1)) * 4 + ch]
- src[(row + Math.max(x - r - 1, 0)) * 4 + ch];
dst[(row + x) * 4 + ch] = Math.round(val * iarr);
}
}
}
}
/** Box-blur one column of RGBA pixels (vertical pass). */
function _boxBlurV(src, dst, w, h, r) {
const iarr = 1 / (2 * r + 1);
for (let x = 0; x < w; x++) {
for (let ch = 0; ch < 4; ch++) {
let val = 0;
for (let y = -r; y <= r; y++) val += src[(Math.max(0, Math.min(y, h - 1)) * w + x) * 4 + ch];
for (let y = 0; y < h; y++) {
val += src[(Math.min(y + r, h - 1) * w + x) * 4 + ch]
- src[(Math.max(y - r - 1, 0) * w + x) * 4 + ch];
dst[(y * w + x) * 4 + ch] = Math.round(val * iarr);
}
}
}
}
/**
* Apply an approximate Gaussian blur (sigma px) to `canvas` in-place.
* Uses the native CSS filter on Chrome/Firefox; falls back to a 3-pass
* separable box blur for Safari / iOS WebKit.
*/
function blurCanvas(canvas, sigma) {
if (sigma <= 0) return;
if (CANVAS_FILTER_SUPPORTED) {
const tmp = document.createElement('canvas');
tmp.width = canvas.width; tmp.height = canvas.height;
const tc = tmp.getContext('2d');
tc.filter = `blur(${sigma}px)`;
tc.drawImage(canvas, 0, 0);
canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
canvas.getContext('2d').drawImage(tmp, 0, 0);
} else {
// 3 passes of box blur ≈ Gaussian; radius r where r(r+1) ≈ sigma²
const r = Math.max(1, Math.round((Math.sqrt(4 * sigma * sigma + 1) - 1) / 2));
const ctx = canvas.getContext('2d');
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const a = imgData.data;
const b = new Uint8ClampedArray(a.length);
const w = canvas.width, h = canvas.height;
for (let pass = 0; pass < 3; pass++) {
_boxBlurH(a, b, w, h, r);
_boxBlurV(b, a, w, h, r);
}
ctx.putImageData(imgData, 0, 0);
}
}
// ── Displacement preview state ────────────────────────────────────────────────
let dispPreviewGeometry = null; // subdivided geometry with smoothNormal attribute
let dispPreviewBusy = false; // true while async subdivision is running
@@ -1332,10 +1409,8 @@ function getEffectiveMapEntry() {
const blurred = document.createElement('canvas');
blurred.width = width * 3;
blurred.height = height * 3;
const bc = blurred.getContext('2d');
bc.filter = `blur(${settings.textureSmoothing}px)`;
bc.drawImage(tiled, 0, 0);
bc.filter = 'none';
blurred.getContext('2d').drawImage(tiled, 0, 0);
blurCanvas(blurred, settings.textureSmoothing);
const offscreen = document.createElement('canvas');
offscreen.width = width;
offscreen.height = height;
+11 -11
View File
@@ -35,24 +35,24 @@ const IMAGE_PRESETS = [
{ name: 'Carbon Fiber', url: 'textures/carbonFiber.jpg', defaultScale: 0.5 },
{ name: 'Crystal', url: 'textures/crystal.jpg', defaultScale: 0.5 },
{ name: 'Dots', url: 'textures/dots.jpg', defaultScale: 0.1 },
{ name: 'Grid', url: 'textures/grid.png', defaultScale: 1.0 },
{ name: 'Grip Surface', url: 'textures/gripSurface.jpg', defaultScale: 0.5 },
{ name: 'Hexagon', url: 'textures/hexagon.jpg', defaultScale: 0.5 },
{ name: 'Hexagon', url: 'textures/hexagon.jpg', defaultScale: 0.5 },
{ name: 'Hexagons', url: 'textures/hexagons.jpg', defaultScale: 1.0 },
{ name: 'Isogrid', url: 'textures/isogrid.png', defaultScale: 0.5 },
{ name: 'Isogrid', url: 'textures/isogrid.png', defaultScale: 0.5 },
{ name: 'Knitting', url: 'textures/knitting.jpg', defaultScale: 0.25 },
{ name: 'Knurling', url: 'textures/knurling.jpg', defaultScale: 0.15 },
{ name: 'Leather', url: 'textures/leather.jpg', defaultScale: 0.5 },
{ name: 'Leather 2', url: 'textures/leather2.jpg', defaultScale: 0.5 },
{ name: 'Noise', url: 'textures/noise.jpg', defaultScale: 0.3 },
{ name: 'Stripes', url: 'textures/stripes.png', defaultScale: 0.5 },
{ name: 'Stripes 02', url: 'textures/stripes_02.png', defaultScale: 1.0 },
{ name: 'Stripes 1', url: 'textures/stripes.png', defaultScale: 0.5 },
{ name: 'Stripes 2', url: 'textures/stripes_02.png', defaultScale: 1.0 },
{ name: 'Voronoi', url: 'textures/voronoi.jpg', defaultScale: 0.5 },
{ name: 'Weave', url: 'textures/weave.jpg', defaultScale: 0.5 },
{ name: 'Weave 02', url: 'textures/weave_02.jpg', defaultScale: 0.5 },
{ name: 'Weave 03', url: 'textures/weave_03.jpg', defaultScale: 0.5 },
{ name: 'Wood', url: 'textures/wood.jpg', defaultScale: 0.5 },
{ name: 'Wood Grain 02',url: 'textures/woodgrain_02.jpg', defaultScale: 1.0 },
{ name: 'Wood Grain 03',url: 'textures/woodgrain_03.jpg', defaultScale: 1.0 },
{ name: 'Weave 1', url: 'textures/weave.jpg', defaultScale: 0.5 },
{ name: 'Weave 2', url: 'textures/weave_02.jpg', defaultScale: 0.5 },
{ name: 'Weave 3', url: 'textures/weave_03.jpg', defaultScale: 0.5 },
{ name: 'Wood 1', url: 'textures/wood.jpg', defaultScale: 0.5 },
{ name: 'Wood 2', url: 'textures/woodgrain_02.jpg', defaultScale: 1.0 },
{ name: 'Wood 3', url: 'textures/woodgrain_03.jpg', defaultScale: 1.0 },
];
function loadImagePreset(preset) {