Skip to content

Commit 25a8e62

Browse files
IIIvan37Ivan Duchauffour
andauthored
chore: bump version to 1.8.1 (#297)
* feat(dithering): improve blue noise quality with decorrelated RGB channels - Replace blue noise texture with high-quality 64x64 from Christoph Peters - Add getBlueNoiseThresholdRGB() for per-channel decorrelation - Use spatial offsets (17,31 for G, 41,23 for B) to prevent pixel clustering - Update map-and-dither.ts and quantize-mode-r.ts for new RGB thresholds - Fix preprocessImageForRaster dithering condition logic * chore: bump version to 1.8.1 --------- Co-authored-by: Ivan Duchauffour <iduchauffour@welcomr.com>
1 parent c3e8721 commit 25a8e62

File tree

7 files changed

+364
-385
lines changed

7 files changed

+364
-385
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "pixsaur",
33
"private": true,
4-
"version": "1.8.0",
4+
"version": "1.8.1",
55
"type": "module",
66
"engines": {
77
"node": ">=24.11.0",

src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
33
"productName": "Pixsaur",
4-
"version": "1.8.0",
4+
"version": "1.8.1",
55
"identifier": "com.iiiivan37.pixsaur",
66
"build": {
77
"removeUnusedCommands": true,

src/app/store/raster/use-auto-regenerate-rasters.ts

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -76,19 +76,25 @@ export function useAutoRegenerateRasters() {
7676
previousRasterDitheringRef.current = rasterDitheringIntensity
7777
previousMaxChangesRef.current = maxChangesPerLine
7878

79-
// Skip if nothing changed, raster not enabled, or no auto-generated rasters
80-
if (
81-
!rasterEnabled ||
82-
!hasGeneratedRasters ||
83-
(!selectionChanged && !rasterDitheringChanged && !maxChangesChanged)
84-
) {
85-
logger.debug('[RASTER-REGEN] Skipping regeneration:', {
86-
reason: !rasterEnabled
87-
? 'raster not enabled'
88-
: !hasGeneratedRasters
89-
? 'no generated rasters'
90-
: 'no changes detected'
91-
})
79+
// Skip if nothing changed or raster not enabled
80+
// Note: We don't check hasGeneratedRasters for dithering/maxChanges changes
81+
// because user expects immediate feedback when adjusting these sliders
82+
if (!rasterEnabled) {
83+
logger.debug('[RASTER-REGEN] Skipping: raster not enabled')
84+
return
85+
}
86+
87+
// For selection changes, only regenerate if rasters were already generated
88+
if (selectionChanged && !hasGeneratedRasters) {
89+
logger.debug(
90+
'[RASTER-REGEN] Skipping selection change: no generated rasters'
91+
)
92+
return
93+
}
94+
95+
// Skip if nothing actually changed
96+
if (!selectionChanged && !rasterDitheringChanged && !maxChangesChanged) {
97+
logger.debug('[RASTER-REGEN] Skipping: no changes detected')
9298
return
9399
}
94100

src/libs/pixsaur-color/src/map/blue-noise-texture.ts

Lines changed: 287 additions & 341 deletions
Large diffs are not rendered by default.

src/libs/pixsaur-color/src/map/map-and-dither.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getBlueNoiseThresholdCentered } from './blue-noise-texture'
1+
import { getBlueNoiseThresholdRGB } from './blue-noise-texture'
22

33
const BAYER_MATRICES: Record<
44
'bayer2x2' | 'bayer4x4' | 'bayer8x8' | 'atkinson' | 'halftone4x4',
@@ -406,20 +406,22 @@ export function applyBlueNoiseDither(
406406
): Uint8ClampedArray {
407407
const out = new Uint8ClampedArray(width * height * 4)
408408
const pixelCS = new Float32Array(3)
409+
const scale = intensity * 255
409410

410411
for (let y = 0; y < height; y++) {
411412
for (let x = 0; x < width; x++) {
412413
const i = y * width + x
413414
const i3 = i * 3
414415
const i4 = i * 4
415416

416-
// Get blue noise threshold (-0.5 to 0.5, centered)
417-
const threshold = getBlueNoiseThresholdCentered(x, y) * intensity * 255
417+
// Get decorrelated blue noise thresholds for each RGB channel
418+
// This prevents pixels from clustering together
419+
const [tR, tG, tB] = getBlueNoiseThresholdRGB(x, y)
418420

419-
// Clamp to valid range to avoid dark artifacts
420-
pixelCS[0] = Math.max(0, Math.min(255, bufCS[i3] + threshold))
421-
pixelCS[1] = Math.max(0, Math.min(255, bufCS[i3 + 1] + threshold))
422-
pixelCS[2] = Math.max(0, Math.min(255, bufCS[i3 + 2] + threshold))
421+
// Apply per-channel dithering
422+
pixelCS[0] = Math.max(0, Math.min(255, bufCS[i3] + tR * scale))
423+
pixelCS[1] = Math.max(0, Math.min(255, bufCS[i3 + 1] + tG * scale))
424+
pixelCS[2] = Math.max(0, Math.min(255, bufCS[i3 + 2] + tB * scale))
423425

424426
const bestIndex = findClosestColorIndex(pixelCS, paletteCS, distFn)
425427

@@ -922,6 +924,7 @@ function applyBlueNoiseDitherWithDynamicPalette(
922924
): Uint8ClampedArray {
923925
const out = new Uint8ClampedArray(width * height * 4)
924926
const pixel = new Float32Array(3)
927+
const scale = intensity * 255
925928

926929
for (let y = 0; y < height; y++) {
927930
const palette = getPaletteForLine(y)
@@ -934,13 +937,13 @@ function applyBlueNoiseDitherWithDynamicPalette(
934937
const i3 = idx * 3
935938
const i4 = idx * 4
936939

937-
// Get blue noise threshold (-0.5 to 0.5, centered)
938-
const noise = getBlueNoiseThresholdCentered(x, y) * intensity * 255
940+
// Get decorrelated blue noise thresholds for each RGB channel
941+
const [tR, tG, tB] = getBlueNoiseThresholdRGB(x, y)
939942

940-
// Get pixel with blue noise, clamped to valid range
941-
pixel[0] = Math.max(0, Math.min(255, bufCS[i3] + noise))
942-
pixel[1] = Math.max(0, Math.min(255, bufCS[i3 + 1] + noise))
943-
pixel[2] = Math.max(0, Math.min(255, bufCS[i3 + 2] + noise))
943+
// Apply per-channel dithering
944+
pixel[0] = Math.max(0, Math.min(255, bufCS[i3] + tR * scale))
945+
pixel[1] = Math.max(0, Math.min(255, bufCS[i3 + 1] + tG * scale))
946+
pixel[2] = Math.max(0, Math.min(255, bufCS[i3 + 2] + tB * scale))
944947

945948
// Find nearest palette color for this line
946949
const bestIndex = findClosestColorIndex(pixel, paletteCS, distFn)

src/libs/pixsaur-mode-r/quantize-mode-r.ts

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828

2929
import { logger } from '@/core/logger'
3030
import type { DitheringMode } from '@/libs/pixsaur-color/src'
31-
import { getBlueNoiseThresholdCentered } from '@/libs/pixsaur-color/src/map/blue-noise-texture'
31+
import { getBlueNoiseThresholdRGB } from '@/libs/pixsaur-color/src/map/blue-noise-texture'
3232
import { getOstromoukhovCoefficients } from '@/libs/pixsaur-color/src/map/ostromoukhov-coefficients'
3333
import type { Vector } from '@/libs/pixsaur-color/src/type'
3434
import { colorDistance } from './blend'
@@ -122,16 +122,17 @@ function isBlueNoiseMode(mode: DitheringMode | 'none'): boolean {
122122
}
123123

124124
/**
125-
* Get Blue Noise threshold for a pixel position
126-
* Uses the same formula as applyBlueNoiseDither in pixsaur-color:
127-
* threshold = getBlueNoiseThresholdCentered(x, y) * intensity * 255
125+
* Get Blue Noise thresholds for a pixel position (decorrelated per RGB channel)
126+
* Uses the same formula as applyBlueNoiseDither in pixsaur-color
128127
*/
129-
function getBlueNoiseThreshold(
128+
function getBlueNoiseThresholds(
130129
x: number,
131130
y: number,
132131
intensity: number
133-
): number {
134-
return getBlueNoiseThresholdCentered(x, y) * intensity * 255
132+
): [number, number, number] {
133+
const [tR, tG, tB] = getBlueNoiseThresholdRGB(x, y)
134+
const scale = intensity * 255
135+
return [tR * scale, tG * scale, tB * scale]
135136
}
136137

137138
/**
@@ -354,23 +355,23 @@ export function quantizeModeR(
354355
threshold
355356
)
356357
} else if (useBlueNoise) {
357-
// Blue Noise dithering: apply threshold-based color adjustment
358+
// Blue Noise dithering: apply per-channel threshold-based color adjustment
358359
// Use OUTPUT coordinates (x, y) for the dithering pattern
359-
const threshold = getBlueNoiseThreshold(x, y, ditherIntensity)
360+
const thresholds = getBlueNoiseThresholds(x, y, ditherIntensity)
360361

361-
colorA = getSourceColorWithThreshold(
362+
colorA = getSourceColorWithRGBThresholds(
362363
imageData,
363364
width,
364365
srcXA,
365366
y,
366-
threshold
367+
thresholds
367368
)
368-
colorB = getSourceColorWithThreshold(
369+
colorB = getSourceColorWithRGBThresholds(
369370
imageData,
370371
width,
371372
srcXB,
372373
y,
373-
threshold
374+
thresholds
374375
)
375376
} else {
376377
// Error diffusion or no dithering: use error buffer
@@ -517,6 +518,29 @@ function getSourceColorWithThreshold(
517518
]
518519
}
519520

521+
/**
522+
* Get source pixel color with per-channel RGB thresholds applied
523+
* Used for decorrelated blue noise dithering
524+
*/
525+
function getSourceColorWithRGBThresholds(
526+
imageData: Uint8ClampedArray,
527+
width: number,
528+
x: number,
529+
y: number,
530+
thresholds: [number, number, number]
531+
): Vector<'RGB'> {
532+
const pixelIdx = (y * width + x) * 4
533+
const r = imageData[pixelIdx]
534+
const g = imageData[pixelIdx + 1]
535+
const b = imageData[pixelIdx + 2]
536+
537+
return [
538+
Math.max(0, Math.min(255, r + thresholds[0])),
539+
Math.max(0, Math.min(255, g + thresholds[1])),
540+
Math.max(0, Math.min(255, b + thresholds[2]))
541+
]
542+
}
543+
520544
/**
521545
* Check if two colors are very similar (used to detect margin areas)
522546
* Margin areas should use uniform pairs to avoid flicker

src/libs/pixsaur-raster/optimize-line-palettes.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1431,8 +1431,8 @@ export function preprocessImageForRaster(
14311431
.fill(null)
14321432
.map(() => [0, 0, 0])
14331433

1434-
if (shouldApplyDithering) {
1435-
// Line already has ≤nColors colors: direct mapping without dithering
1434+
if (!shouldApplyDithering) {
1435+
// Line already has ≤nColors colors AND no dithering requested: direct mapping
14361436
for (let x = 0; x < width; x++) {
14371437
const pixelIdx = lineStart + x * 4
14381438
const sourceColor: Vector<'RGB'> = [
@@ -1460,7 +1460,7 @@ export function preprocessImageForRaster(
14601460
// Reset vertical error for next line (no error to propagate)
14611461
// Keep it at zero
14621462
} else {
1463-
// Line has >4 colors: apply dithering
1463+
// Apply dithering (either line has >nColors or user wants dithering)
14641464
// Horizontal error buffer for this line (floating-point)
14651465
const horizError: [number, number, number][] = new Array(width)
14661466
.fill(null)

0 commit comments

Comments
 (0)