|
479 | 479 | canvas.width = settings.outputWidth; |
480 | 480 | canvas.height = outputHeight; |
481 | 481 |
|
482 | | - ctx.imageSmoothingEnabled = true; |
| 482 | + // Use nearest-neighbor scaling for sharper edges |
| 483 | + ctx.imageSmoothingEnabled = false; |
483 | 484 | ctx.drawImage(img, 0, 0, settings.outputWidth, outputHeight); |
484 | 485 |
|
485 | | - const pixelatedCanvas = this.pixelate(canvas, settings.pixelSize); |
| 486 | + // Apply edge enhancement before pixelation |
| 487 | + const enhancedCanvas = this.enhanceEdges(canvas); |
| 488 | + |
| 489 | + const pixelatedCanvas = this.pixelate(enhancedCanvas, settings.pixelSize); |
486 | 490 |
|
487 | 491 | // Apply custom palette or auto quantization |
488 | 492 | const quantizedCanvas = settings.customPalette |
|
539 | 543 | return nearestColor; |
540 | 544 | } |
541 | 545 |
|
542 | | - pixelate(canvas, pixelSize) { |
| 546 | + enhanceEdges(canvas) { |
543 | 547 | const ctx = canvas.getContext('2d'); |
| 548 | + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); |
| 549 | + const data = imageData.data; |
544 | 550 | const width = canvas.width; |
545 | 551 | const height = canvas.height; |
546 | 552 |
|
547 | | - const scaledWidth = Math.ceil(width / pixelSize); |
548 | | - const scaledHeight = Math.ceil(height / pixelSize); |
| 553 | + // Create a copy for edge detection |
| 554 | + const edgeData = new Uint8ClampedArray(data); |
549 | 555 |
|
550 | | - const smallCanvas = document.createElement('canvas'); |
551 | | - smallCanvas.width = scaledWidth; |
552 | | - smallCanvas.height = scaledHeight; |
553 | | - const smallCtx = smallCanvas.getContext('2d'); |
| 556 | + // Sobel operator for edge detection |
| 557 | + const sobelX = [-1, 0, 1, -2, 0, 2, -1, 0, 1]; |
| 558 | + const sobelY = [-1, -2, -1, 0, 0, 0, 1, 2, 1]; |
554 | 559 |
|
555 | | - smallCtx.imageSmoothingEnabled = true; |
556 | | - smallCtx.drawImage(canvas, 0, 0, scaledWidth, scaledHeight); |
| 560 | + for (let y = 1; y < height - 1; y++) { |
| 561 | + for (let x = 1; x < width - 1; x++) { |
| 562 | + let pixelX = 0; |
| 563 | + let pixelY = 0; |
| 564 | + |
| 565 | + // Apply Sobel operator |
| 566 | + for (let j = -1; j <= 1; j++) { |
| 567 | + for (let i = -1; i <= 1; i++) { |
| 568 | + const idx = ((y + j) * width + (x + i)) * 4; |
| 569 | + const gray = data[idx] * 0.299 + data[idx + 1] * 0.587 + data[idx + 2] * 0.114; |
| 570 | + const kernelIdx = (j + 1) * 3 + (i + 1); |
| 571 | + pixelX += gray * sobelX[kernelIdx]; |
| 572 | + pixelY += gray * sobelY[kernelIdx]; |
| 573 | + } |
| 574 | + } |
| 575 | + |
| 576 | + const magnitude = Math.sqrt(pixelX * pixelX + pixelY * pixelY); |
| 577 | + const idx = (y * width + x) * 4; |
| 578 | + |
| 579 | + // Enhance edges by darkening them slightly |
| 580 | + if (magnitude > 30) { |
| 581 | + const factor = Math.min(1.2, 1 + magnitude / 255); |
| 582 | + edgeData[idx] = Math.max(0, data[idx] * 0.8); |
| 583 | + edgeData[idx + 1] = Math.max(0, data[idx + 1] * 0.8); |
| 584 | + edgeData[idx + 2] = Math.max(0, data[idx + 2] * 0.8); |
| 585 | + } |
| 586 | + } |
| 587 | + } |
557 | 588 |
|
| 589 | + // Create result canvas |
558 | 590 | const resultCanvas = document.createElement('canvas'); |
559 | 591 | resultCanvas.width = width; |
560 | 592 | resultCanvas.height = height; |
561 | 593 | const resultCtx = resultCanvas.getContext('2d'); |
| 594 | + const resultImageData = resultCtx.createImageData(width, height); |
| 595 | + resultImageData.data.set(edgeData); |
| 596 | + resultCtx.putImageData(resultImageData, 0, 0); |
562 | 597 |
|
563 | | - resultCtx.imageSmoothingEnabled = false; |
564 | | - resultCtx.drawImage(smallCanvas, 0, 0, width, height); |
| 598 | + return resultCanvas; |
| 599 | + } |
| 600 | + |
| 601 | + pixelate(canvas, pixelSize) { |
| 602 | + const ctx = canvas.getContext('2d'); |
| 603 | + const width = canvas.width; |
| 604 | + const height = canvas.height; |
| 605 | + const imageData = ctx.getImageData(0, 0, width, height); |
| 606 | + const data = imageData.data; |
| 607 | + |
| 608 | + // Create result canvas |
| 609 | + const resultCanvas = document.createElement('canvas'); |
| 610 | + resultCanvas.width = width; |
| 611 | + resultCanvas.height = height; |
| 612 | + const resultCtx = resultCanvas.getContext('2d'); |
| 613 | + const resultImageData = resultCtx.createImageData(width, height); |
| 614 | + const resultData = resultImageData.data; |
| 615 | + |
| 616 | + // Process each pixel block |
| 617 | + for (let y = 0; y < height; y += pixelSize) { |
| 618 | + for (let x = 0; x < width; x += pixelSize) { |
| 619 | + const blockWidth = Math.min(pixelSize, width - x); |
| 620 | + const blockHeight = Math.min(pixelSize, height - y); |
| 621 | + |
| 622 | + // Calculate average color for this block |
| 623 | + let r = 0, g = 0, b = 0, a = 0; |
| 624 | + let count = 0; |
| 625 | + |
| 626 | + for (let dy = 0; dy < blockHeight; dy++) { |
| 627 | + for (let dx = 0; dx < blockWidth; dx++) { |
| 628 | + const index = ((y + dy) * width + (x + dx)) * 4; |
| 629 | + r += data[index]; |
| 630 | + g += data[index + 1]; |
| 631 | + b += data[index + 2]; |
| 632 | + a += data[index + 3]; |
| 633 | + count++; |
| 634 | + } |
| 635 | + } |
| 636 | + |
| 637 | + // Apply average color to entire block |
| 638 | + const avgR = Math.round(r / count); |
| 639 | + const avgG = Math.round(g / count); |
| 640 | + const avgB = Math.round(b / count); |
| 641 | + const avgA = Math.round(a / count); |
| 642 | + |
| 643 | + for (let dy = 0; dy < blockHeight; dy++) { |
| 644 | + for (let dx = 0; dx < blockWidth; dx++) { |
| 645 | + const index = ((y + dy) * width + (x + dx)) * 4; |
| 646 | + resultData[index] = avgR; |
| 647 | + resultData[index + 1] = avgG; |
| 648 | + resultData[index + 2] = avgB; |
| 649 | + resultData[index + 3] = avgA; |
| 650 | + } |
| 651 | + } |
| 652 | + } |
| 653 | + } |
565 | 654 |
|
| 655 | + resultCtx.putImageData(resultImageData, 0, 0); |
566 | 656 | return resultCanvas; |
567 | 657 | } |
568 | 658 |
|
|
0 commit comments