| import type { ScanSettings } from '../types/scanner-effect-type.js'; |
| import type { AdjustColorsSettings } from '../types/adjust-colors-type.js'; |
|
|
| export function applyGreyscale(imageData: ImageData): void { |
| const data = imageData.data; |
| for (let j = 0; j < data.length; j += 4) { |
| const grey = Math.round( |
| 0.299 * data[j] + 0.587 * data[j + 1] + 0.114 * data[j + 2] |
| ); |
| data[j] = grey; |
| data[j + 1] = grey; |
| data[j + 2] = grey; |
| } |
| } |
|
|
| export function applyInvertColors(imageData: ImageData): void { |
| const data = imageData.data; |
| for (let j = 0; j < data.length; j += 4) { |
| data[j] = 255 - data[j]; |
| data[j + 1] = 255 - data[j + 1]; |
| data[j + 2] = 255 - data[j + 2]; |
| } |
| } |
|
|
| export function rgbToHsl( |
| r: number, |
| g: number, |
| b: number |
| ): [number, number, number] { |
| r /= 255; |
| g /= 255; |
| b /= 255; |
| const max = Math.max(r, g, b); |
| const min = Math.min(r, g, b); |
| const l = (max + min) / 2; |
| let h = 0; |
| let s = 0; |
|
|
| if (max !== min) { |
| const d = max - min; |
| s = l > 0.5 ? d / (2 - max - min) : d / (max + min); |
| if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6; |
| else if (max === g) h = ((b - r) / d + 2) / 6; |
| else h = ((r - g) / d + 4) / 6; |
| } |
|
|
| return [h, s, l]; |
| } |
|
|
| export function hslToRgb( |
| h: number, |
| s: number, |
| l: number |
| ): [number, number, number] { |
| if (s === 0) { |
| const v = Math.round(l * 255); |
| return [v, v, v]; |
| } |
|
|
| const hue2rgb = (p: number, q: number, t: number): number => { |
| if (t < 0) t += 1; |
| if (t > 1) t -= 1; |
| if (t < 1 / 6) return p + (q - p) * 6 * t; |
| if (t < 1 / 2) return q; |
| if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; |
| return p; |
| }; |
|
|
| const q = l < 0.5 ? l * (1 + s) : l + s - l * s; |
| const p = 2 * l - q; |
| return [ |
| Math.round(hue2rgb(p, q, h + 1 / 3) * 255), |
| Math.round(hue2rgb(p, q, h) * 255), |
| Math.round(hue2rgb(p, q, h - 1 / 3) * 255), |
| ]; |
| } |
|
|
| export function applyScannerEffect( |
| sourceData: ImageData, |
| canvas: HTMLCanvasElement, |
| settings: ScanSettings, |
| rotationAngle: number, |
| scale: number = 1 |
| ): void { |
| const ctx = canvas.getContext('2d')!; |
| const w = sourceData.width; |
| const h = sourceData.height; |
|
|
| const scaledBlur = settings.blur * scale; |
| const scaledNoise = settings.noise * scale; |
|
|
| const workCanvas = document.createElement('canvas'); |
| workCanvas.width = w; |
| workCanvas.height = h; |
| const workCtx = workCanvas.getContext('2d')!; |
|
|
| if (scaledBlur > 0) { |
| workCtx.filter = `blur(${scaledBlur}px)`; |
| } |
|
|
| workCtx.putImageData(sourceData, 0, 0); |
| if (scaledBlur > 0) { |
| const tempCanvas = document.createElement('canvas'); |
| tempCanvas.width = w; |
| tempCanvas.height = h; |
| const tempCtx = tempCanvas.getContext('2d')!; |
| tempCtx.filter = `blur(${scaledBlur}px)`; |
| tempCtx.drawImage(workCanvas, 0, 0); |
| workCtx.filter = 'none'; |
| workCtx.clearRect(0, 0, w, h); |
| workCtx.drawImage(tempCanvas, 0, 0); |
| } |
|
|
| const imageData = workCtx.getImageData(0, 0, w, h); |
| const data = imageData.data; |
|
|
| const contrastFactor = |
| settings.contrast !== 0 |
| ? (259 * (settings.contrast + 255)) / (255 * (259 - settings.contrast)) |
| : 1; |
|
|
| for (let i = 0; i < data.length; i += 4) { |
| let r = data[i]; |
| let g = data[i + 1]; |
| let b = data[i + 2]; |
|
|
| if (settings.grayscale) { |
| const grey = Math.round(0.299 * r + 0.587 * g + 0.114 * b); |
| r = grey; |
| g = grey; |
| b = grey; |
| } |
|
|
| if (settings.brightness !== 0) { |
| r += settings.brightness; |
| g += settings.brightness; |
| b += settings.brightness; |
| } |
|
|
| if (settings.contrast !== 0) { |
| r = contrastFactor * (r - 128) + 128; |
| g = contrastFactor * (g - 128) + 128; |
| b = contrastFactor * (b - 128) + 128; |
| } |
|
|
| if (settings.yellowish > 0) { |
| const intensity = settings.yellowish / 50; |
| r += 20 * intensity; |
| g += 12 * intensity; |
| b -= 15 * intensity; |
| } |
|
|
| if (scaledNoise > 0) { |
| const n = (Math.random() - 0.5) * scaledNoise; |
| r += n; |
| g += n; |
| b += n; |
| } |
|
|
| data[i] = Math.max(0, Math.min(255, r)); |
| data[i + 1] = Math.max(0, Math.min(255, g)); |
| data[i + 2] = Math.max(0, Math.min(255, b)); |
| } |
|
|
| workCtx.putImageData(imageData, 0, 0); |
|
|
| if (settings.border) { |
| const borderSize = Math.max(w, h) * 0.02; |
| const gradient1 = workCtx.createLinearGradient(0, 0, borderSize, 0); |
| gradient1.addColorStop(0, 'rgba(0,0,0,0.3)'); |
| gradient1.addColorStop(1, 'rgba(0,0,0,0)'); |
| workCtx.fillStyle = gradient1; |
| workCtx.fillRect(0, 0, borderSize, h); |
|
|
| const gradient2 = workCtx.createLinearGradient(w, 0, w - borderSize, 0); |
| gradient2.addColorStop(0, 'rgba(0,0,0,0.3)'); |
| gradient2.addColorStop(1, 'rgba(0,0,0,0)'); |
| workCtx.fillStyle = gradient2; |
| workCtx.fillRect(w - borderSize, 0, borderSize, h); |
|
|
| const gradient3 = workCtx.createLinearGradient(0, 0, 0, borderSize); |
| gradient3.addColorStop(0, 'rgba(0,0,0,0.3)'); |
| gradient3.addColorStop(1, 'rgba(0,0,0,0)'); |
| workCtx.fillStyle = gradient3; |
| workCtx.fillRect(0, 0, w, borderSize); |
|
|
| const gradient4 = workCtx.createLinearGradient(0, h, 0, h - borderSize); |
| gradient4.addColorStop(0, 'rgba(0,0,0,0.3)'); |
| gradient4.addColorStop(1, 'rgba(0,0,0,0)'); |
| workCtx.fillStyle = gradient4; |
| workCtx.fillRect(0, h - borderSize, w, borderSize); |
| } |
|
|
| if (rotationAngle !== 0) { |
| const rad = (rotationAngle * Math.PI) / 180; |
| const cos = Math.abs(Math.cos(rad)); |
| const sin = Math.abs(Math.sin(rad)); |
| const newW = Math.ceil(w * cos + h * sin); |
| const newH = Math.ceil(w * sin + h * cos); |
|
|
| canvas.width = newW; |
| canvas.height = newH; |
| ctx.fillStyle = '#ffffff'; |
| ctx.fillRect(0, 0, newW, newH); |
| ctx.translate(newW / 2, newH / 2); |
| ctx.rotate(rad); |
| ctx.drawImage(workCanvas, -w / 2, -h / 2); |
| ctx.setTransform(1, 0, 0, 1, 0, 0); |
| } else { |
| canvas.width = w; |
| canvas.height = h; |
| ctx.drawImage(workCanvas, 0, 0); |
| } |
| } |
|
|
| export function applyColorAdjustments( |
| sourceData: ImageData, |
| canvas: HTMLCanvasElement, |
| settings: AdjustColorsSettings |
| ): void { |
| const ctx = canvas.getContext('2d')!; |
| const w = sourceData.width; |
| const h = sourceData.height; |
|
|
| canvas.width = w; |
| canvas.height = h; |
|
|
| const imageData = new ImageData(new Uint8ClampedArray(sourceData.data), w, h); |
| const data = imageData.data; |
|
|
| const contrastFactor = |
| settings.contrast !== 0 |
| ? (259 * (settings.contrast + 255)) / (255 * (259 - settings.contrast)) |
| : 1; |
|
|
| const gammaCorrection = settings.gamma !== 1.0 ? 1 / settings.gamma : 1; |
| const sepiaAmount = settings.sepia / 100; |
|
|
| for (let i = 0; i < data.length; i += 4) { |
| let r = data[i]; |
| let g = data[i + 1]; |
| let b = data[i + 2]; |
|
|
| if (settings.brightness !== 0) { |
| const adj = settings.brightness * 2.55; |
| r += adj; |
| g += adj; |
| b += adj; |
| } |
|
|
| if (settings.contrast !== 0) { |
| r = contrastFactor * (r - 128) + 128; |
| g = contrastFactor * (g - 128) + 128; |
| b = contrastFactor * (b - 128) + 128; |
| } |
|
|
| if (settings.saturation !== 0 || settings.hueShift !== 0) { |
| const [hue, sat, lig] = rgbToHsl( |
| Math.max(0, Math.min(255, r)), |
| Math.max(0, Math.min(255, g)), |
| Math.max(0, Math.min(255, b)) |
| ); |
|
|
| let newHue = hue; |
| if (settings.hueShift !== 0) { |
| newHue = (hue + settings.hueShift / 360) % 1; |
| if (newHue < 0) newHue += 1; |
| } |
|
|
| let newSat = sat; |
| if (settings.saturation !== 0) { |
| const satAdj = settings.saturation / 100; |
| newSat = satAdj > 0 ? sat + (1 - sat) * satAdj : sat * (1 + satAdj); |
| newSat = Math.max(0, Math.min(1, newSat)); |
| } |
|
|
| [r, g, b] = hslToRgb(newHue, newSat, lig); |
| } |
|
|
| if (settings.temperature !== 0) { |
| const t = settings.temperature / 50; |
| r += 30 * t; |
| b -= 30 * t; |
| } |
|
|
| if (settings.tint !== 0) { |
| const t = settings.tint / 50; |
| g += 30 * t; |
| } |
|
|
| if (settings.gamma !== 1.0) { |
| r = Math.pow(Math.max(0, Math.min(255, r)) / 255, gammaCorrection) * 255; |
| g = Math.pow(Math.max(0, Math.min(255, g)) / 255, gammaCorrection) * 255; |
| b = Math.pow(Math.max(0, Math.min(255, b)) / 255, gammaCorrection) * 255; |
| } |
|
|
| if (settings.sepia > 0) { |
| const sr = 0.393 * r + 0.769 * g + 0.189 * b; |
| const sg = 0.349 * r + 0.686 * g + 0.168 * b; |
| const sb = 0.272 * r + 0.534 * g + 0.131 * b; |
| r = r + (sr - r) * sepiaAmount; |
| g = g + (sg - g) * sepiaAmount; |
| b = b + (sb - b) * sepiaAmount; |
| } |
|
|
| data[i] = Math.max(0, Math.min(255, r)); |
| data[i + 1] = Math.max(0, Math.min(255, g)); |
| data[i + 2] = Math.max(0, Math.min(255, b)); |
| } |
|
|
| ctx.putImageData(imageData, 0, 0); |
| } |
|
|