diff --git a/js/main.js b/js/main.js index 0f2d87b..74d5146 100644 --- a/js/main.js +++ b/js/main.js @@ -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; diff --git a/js/presetTextures.js b/js/presetTextures.js index 1b80eff..7ad1295 100644 --- a/js/presetTextures.js +++ b/js/presetTextures.js @@ -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) { diff --git a/textures/grid.png b/textures/grid.png new file mode 100644 index 0000000..d4898e7 Binary files /dev/null and b/textures/grid.png differ diff --git a/textures/stripes.jpg b/textures/stripes.jpg deleted file mode 100644 index 7160d07..0000000 Binary files a/textures/stripes.jpg and /dev/null differ