Files
archived-stlTexturizer/js/presetTextures.js
T
2026-03-16 20:37:32 +01:00

257 lines
8.0 KiB
JavaScript

import * as THREE from 'three';
const SIZE = 512; // texture resolution for both preview and sampling
// ── Helpers ──────────────────────────────────────────────────────────────────
function makeCanvas(size = SIZE) {
const c = document.createElement('canvas');
c.width = c.height = size;
return c;
}
function grayPixel(value255) {
return `rgb(${value255},${value255},${value255})`;
}
// Simple seeded LCG pseudo-random number generator (deterministic)
function lcg(seed) {
let s = seed >>> 0;
return () => {
s = (Math.imul(1664525, s) + 1013904223) >>> 0;
return s / 0xFFFFFFFF;
};
}
// ── Generators ───────────────────────────────────────────────────────────────
/** Horizontal sine waves */
function generateWaves(size = SIZE) {
const canvas = makeCanvas(size);
const ctx = canvas.getContext('2d');
const id = ctx.createImageData(size, size);
const d = id.data;
for (let y = 0; y < size; y++) {
const v = Math.sin((y / size) * Math.PI * 10) * 0.5 + 0.5;
const g = Math.round(v * 255);
for (let x = 0; x < size; x++) {
const i = (y * size + x) * 4;
d[i] = d[i+1] = d[i+2] = g;
d[i+3] = 255;
}
}
ctx.putImageData(id, 0, 0);
return canvas;
}
/** Fish-scale / overlapping circles */
function generateScales(size = SIZE) {
const canvas = makeCanvas(size);
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, size, size);
const r = size / 8;
const rStroke = r * 0.08;
ctx.strokeStyle = '#fff';
ctx.lineWidth = rStroke;
ctx.fillStyle = '#333';
const rows = Math.ceil(size / r) + 2;
const cols = Math.ceil(size / r) + 2;
for (let row = -1; row < rows; row++) {
for (let col = -1; col < cols; col++) {
const ox = col * r * 1.0 + (row % 2 === 0 ? 0 : r * 0.5);
const oy = row * r * 0.75;
ctx.beginPath();
ctx.arc(ox, oy, r * 0.92, Math.PI, 0);
ctx.closePath();
ctx.fill();
ctx.stroke();
}
}
return canvas;
}
/** Hexagonal grid */
function generateHex(size = SIZE) {
const canvas = makeCanvas(size);
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#222';
ctx.fillRect(0, 0, size, size);
const r = size / 8;
const w = Math.sqrt(3) * r;
const h = 2 * r;
ctx.strokeStyle = '#fff';
ctx.lineWidth = r * 0.12;
function hexPath(cx, cy) {
ctx.beginPath();
for (let i = 0; i < 6; i++) {
const angle = (Math.PI / 3) * i - Math.PI / 6;
const px = cx + r * 0.88 * Math.cos(angle);
const py = cy + r * 0.88 * Math.sin(angle);
if (i === 0) ctx.moveTo(px, py);
else ctx.lineTo(px, py);
}
ctx.closePath();
}
const cols = Math.ceil(size / w) + 2;
const rows = Math.ceil(size / (h * 0.75)) + 2;
for (let row = -1; row < rows; row++) {
for (let col = -1; col < cols; col++) {
const cx = col * w + (row % 2 === 0 ? 0 : w / 2);
const cy = row * h * 0.75;
hexPath(cx, cy);
ctx.fillStyle = `hsl(0,0%,${20 + Math.random() * 10}%)`;
ctx.fill();
ctx.stroke();
}
}
return canvas;
}
/** Diamond / crosshatch */
function generateDiamonds(size = SIZE) {
const canvas = makeCanvas(size);
const ctx = canvas.getContext('2d');
const id = ctx.createImageData(size, size);
const d = id.data;
const freq = 8;
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
const u = x / size;
const v = y / size;
const val = (Math.abs(Math.sin(u * Math.PI * freq)) +
Math.abs(Math.sin(v * Math.PI * freq))) / 2;
const g = Math.round(val * 255);
const i = (y * size + x) * 4;
d[i] = d[i+1] = d[i+2] = g;
d[i+3] = 255;
}
}
ctx.putImageData(id, 0, 0);
return canvas;
}
/** Smooth noise (value noise via bilinear interpolation of random grid) */
function generateNoise(size = SIZE) {
const canvas = makeCanvas(size);
const ctx = canvas.getContext('2d');
const id = ctx.createImageData(size, size);
const d = id.data;
const rand = lcg(0xdeadbeef);
// Generate random value grid at coarser resolution
const GRID = 16;
const grid = new Float32Array((GRID + 1) * (GRID + 1));
for (let i = 0; i < grid.length; i++) grid[i] = rand();
function bilerp(gx, gy) {
const x0 = Math.floor(gx) % GRID;
const y0 = Math.floor(gy) % GRID;
const x1 = (x0 + 1) % GRID;
const y1 = (y0 + 1) % GRID;
const fx = gx - Math.floor(gx);
const fy = gy - Math.floor(gy);
// Smoothstep
const sx = fx * fx * (3 - 2 * fx);
const sy = fy * fy * (3 - 2 * fy);
const v00 = grid[y0 * (GRID+1) + x0];
const v10 = grid[y0 * (GRID+1) + x1];
const v01 = grid[y1 * (GRID+1) + x0];
const v11 = grid[y1 * (GRID+1) + x1];
return v00 + sx * (v10 - v00) + sy * (v01 - v00) + sx * sy * (v00 - v10 - v01 + v11);
}
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
const gx = (x / size) * GRID;
const gy = (y / size) * GRID;
// Octave 1 + octave 2
let v = bilerp(gx, gy) * 0.65 + bilerp(gx * 2, gy * 2) * 0.25 + bilerp(gx * 4, gy * 4) * 0.10;
const g = Math.round(Math.max(0, Math.min(1, v)) * 255);
const i4 = (y * size + x) * 4;
d[i4] = d[i4+1] = d[i4+2] = g;
d[i4+3] = 255;
}
}
ctx.putImageData(id, 0, 0);
return canvas;
}
/** Brick pattern */
function generateBrick(size = SIZE) {
const canvas = makeCanvas(size);
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#555';
ctx.fillRect(0, 0, size, size);
const bw = size / 5; // brick width
const bh = size / 10; // brick height
const mortar = bw * 0.07;
ctx.fillStyle = '#ddd';
const rows = Math.ceil(size / bh) + 1;
const cols = Math.ceil(size / bw) + 2;
for (let row = 0; row < rows; row++) {
const offset = (row % 2 === 0 ? 0 : bw * 0.5);
for (let col = -1; col < cols; col++) {
const x = col * bw + offset + mortar / 2;
const y = row * bh + mortar / 2;
ctx.fillRect(x, y, bw - mortar, bh - mortar);
}
}
return canvas;
}
// ── Build PRESETS array ───────────────────────────────────────────────────────
const GENERATORS = [
{ name: 'Waves', gen: generateWaves },
{ name: 'Scales', gen: generateScales },
{ name: 'Hexagonal', gen: generateHex },
{ name: 'Diamonds', gen: generateDiamonds },
{ name: 'Noise', gen: generateNoise },
{ name: 'Brick', gen: generateBrick },
];
export const PRESETS = GENERATORS.map(({ name, gen }) => {
const fullCanvas = gen(SIZE);
const thumbCanvas = gen(80); // small canvas for swatch UI
const texture = new THREE.CanvasTexture(fullCanvas);
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
texture.name = name;
// Extract ImageData for CPU sampling
const ctx = fullCanvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, SIZE, SIZE);
return { name, thumbCanvas, fullCanvas, texture, imageData, width: SIZE, height: SIZE };
});
/**
* Build a THREE.CanvasTexture + ImageData from a user-uploaded image File.
*/
export function loadCustomTexture(file) {
return new Promise((resolve, reject) => {
const img = new Image();
const url = URL.createObjectURL(file);
img.onload = () => {
URL.revokeObjectURL(url);
const canvas = makeCanvas(SIZE);
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, SIZE, SIZE);
const imageData = ctx.getImageData(0, 0, SIZE, SIZE);
const texture = new THREE.CanvasTexture(canvas);
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
texture.name = file.name;
resolve({ name: file.name, fullCanvas: canvas, texture, imageData, width: SIZE, height: SIZE });
};
img.onerror = () => { URL.revokeObjectURL(url); reject(new Error('Failed to load image')); };
img.src = url;
});
}