Skip to content

Commit 62f1cf4

Browse files
ayutazclaude
andcommitted
feat: ディザリング処理を完全実装
- Floyd-Steinbergディザリング: エラー拡散による自然なグラデーション - Ordered (Bayer)ディザリング: 4x4マトリックスによるレトロゲーム風 - Atkinsonディザリング: 75%エラー拡散で高コントラスト維持 - カスタムパレット対応: すべてのディザリング手法で動作 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 0795cb5 commit 62f1cf4

1 file changed

Lines changed: 144 additions & 2 deletions

File tree

src/js/app-v3.js

Lines changed: 144 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -678,8 +678,150 @@
678678
}
679679

680680
applyDithering(canvas, method, palette) {
681-
// Implementation would be similar to app-v2.js but with palette support
682-
return canvas;
681+
const ctx = canvas.getContext('2d');
682+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
683+
const data = imageData.data;
684+
const width = canvas.width;
685+
const height = canvas.height;
686+
687+
if (method === 'floyd-steinberg') {
688+
// Floyd-Steinberg dithering
689+
for (let y = 0; y < height; y++) {
690+
for (let x = 0; x < width; x++) {
691+
const idx = (y * width + x) * 4;
692+
const oldPixel = [data[idx], data[idx + 1], data[idx + 2]];
693+
694+
// Find nearest color from palette or quantized
695+
const newPixel = palette
696+
? this.findNearestColor(oldPixel, palette)
697+
: [
698+
Math.round(data[idx] / 32) * 32,
699+
Math.round(data[idx + 1] / 32) * 32,
700+
Math.round(data[idx + 2] / 32) * 32
701+
];
702+
703+
// Apply new color
704+
data[idx] = newPixel[0];
705+
data[idx + 1] = newPixel[1];
706+
data[idx + 2] = newPixel[2];
707+
708+
// Calculate error
709+
const error = [
710+
oldPixel[0] - newPixel[0],
711+
oldPixel[1] - newPixel[1],
712+
oldPixel[2] - newPixel[2]
713+
];
714+
715+
// Distribute error to neighboring pixels
716+
const distributions = [
717+
{ x: x + 1, y: y, factor: 7/16 },
718+
{ x: x - 1, y: y + 1, factor: 3/16 },
719+
{ x: x, y: y + 1, factor: 5/16 },
720+
{ x: x + 1, y: y + 1, factor: 1/16 }
721+
];
722+
723+
for (const dist of distributions) {
724+
if (dist.x >= 0 && dist.x < width && dist.y >= 0 && dist.y < height) {
725+
const nIdx = (dist.y * width + dist.x) * 4;
726+
data[nIdx] = Math.max(0, Math.min(255, data[nIdx] + error[0] * dist.factor));
727+
data[nIdx + 1] = Math.max(0, Math.min(255, data[nIdx + 1] + error[1] * dist.factor));
728+
data[nIdx + 2] = Math.max(0, Math.min(255, data[nIdx + 2] + error[2] * dist.factor));
729+
}
730+
}
731+
}
732+
}
733+
} else if (method === 'ordered') {
734+
// Ordered (Bayer) dithering
735+
const bayerMatrix = [
736+
[0, 8, 2, 10],
737+
[12, 4, 14, 6],
738+
[3, 11, 1, 9],
739+
[15, 7, 13, 5]
740+
];
741+
742+
for (let y = 0; y < height; y++) {
743+
for (let x = 0; x < width; x++) {
744+
const idx = (y * width + x) * 4;
745+
const threshold = (bayerMatrix[y % 4][x % 4] / 16 - 0.5) * 64;
746+
747+
for (let c = 0; c < 3; c++) {
748+
const oldVal = data[idx + c] + threshold;
749+
750+
if (palette) {
751+
// For custom palette, apply threshold then find nearest
752+
const adjustedPixel = [
753+
Math.max(0, Math.min(255, data[idx] + threshold)),
754+
Math.max(0, Math.min(255, data[idx + 1] + threshold)),
755+
Math.max(0, Math.min(255, data[idx + 2] + threshold))
756+
];
757+
const nearestColor = this.findNearestColor(adjustedPixel, palette);
758+
data[idx] = nearestColor[0];
759+
data[idx + 1] = nearestColor[1];
760+
data[idx + 2] = nearestColor[2];
761+
break; // Process all channels at once for palette
762+
} else {
763+
data[idx + c] = Math.round(Math.max(0, Math.min(255, oldVal)) / 32) * 32;
764+
}
765+
}
766+
}
767+
}
768+
} else if (method === 'atkinson') {
769+
// Atkinson dithering
770+
for (let y = 0; y < height; y++) {
771+
for (let x = 0; x < width; x++) {
772+
const idx = (y * width + x) * 4;
773+
const oldPixel = [data[idx], data[idx + 1], data[idx + 2]];
774+
775+
// Find nearest color
776+
const newPixel = palette
777+
? this.findNearestColor(oldPixel, palette)
778+
: [
779+
Math.round(data[idx] / 32) * 32,
780+
Math.round(data[idx + 1] / 32) * 32,
781+
Math.round(data[idx + 2] / 32) * 32
782+
];
783+
784+
// Apply new color
785+
data[idx] = newPixel[0];
786+
data[idx + 1] = newPixel[1];
787+
data[idx + 2] = newPixel[2];
788+
789+
// Calculate error (Atkinson uses 75% of error)
790+
const error = [
791+
(oldPixel[0] - newPixel[0]) * 0.75,
792+
(oldPixel[1] - newPixel[1]) * 0.75,
793+
(oldPixel[2] - newPixel[2]) * 0.75
794+
];
795+
796+
// Distribute error equally to 6 neighbors (1/8 each)
797+
const distributions = [
798+
{ x: x + 1, y: y, factor: 1/8 },
799+
{ x: x + 2, y: y, factor: 1/8 },
800+
{ x: x - 1, y: y + 1, factor: 1/8 },
801+
{ x: x, y: y + 1, factor: 1/8 },
802+
{ x: x + 1, y: y + 1, factor: 1/8 },
803+
{ x: x, y: y + 2, factor: 1/8 }
804+
];
805+
806+
for (const dist of distributions) {
807+
if (dist.x >= 0 && dist.x < width && dist.y >= 0 && dist.y < height) {
808+
const nIdx = (dist.y * width + dist.x) * 4;
809+
data[nIdx] = Math.max(0, Math.min(255, data[nIdx] + error[0] * dist.factor));
810+
data[nIdx + 1] = Math.max(0, Math.min(255, data[nIdx + 1] + error[1] * dist.factor));
811+
data[nIdx + 2] = Math.max(0, Math.min(255, data[nIdx + 2] + error[2] * dist.factor));
812+
}
813+
}
814+
}
815+
}
816+
}
817+
818+
const resultCanvas = document.createElement('canvas');
819+
resultCanvas.width = canvas.width;
820+
resultCanvas.height = canvas.height;
821+
const resultCtx = resultCanvas.getContext('2d');
822+
resultCtx.putImageData(imageData, 0, 0);
823+
824+
return resultCanvas;
683825
}
684826

685827
updatePreview() {

0 commit comments

Comments
 (0)