feat: add filter options for images, including black & white and selfie effects, and enhance UI elements

This commit is contained in:
mrkad@rpi
2026-01-23 17:30:12 +07:00
parent b2e263bb62
commit f1ceefd702
7 changed files with 383 additions and 17 deletions

View File

@@ -123,6 +123,65 @@ const addVignette = (ctx: CanvasRenderingContext2D, w: number, h: number, streng
ctx.fillRect(0, 0, w, h);
};
const applyBlackAndWhite = (context: CanvasRenderingContext2D, width: number, height: number, volume: number = 5) => {
const imageData = context.getImageData(0, 0, width, height);
const data = imageData.data;
// 1. คำนวณ Contrast ตาม Volume (ยิ่งเยอะ ยิ่งตัดกันเข้ม)
const contrast = (volume / 10) * 80; // ปรับความเข้มข้น (0-80)
const contrastFactor = (259 * (contrast + 255)) / (255 * (259 - contrast));
// 2. ปรับ Brightness (Exposure) ชดเชยเวลา Contrast สูงๆ แล้วภาพมืด
const brightness = volume * 2;
for (let i = 0; i < data.length; i += 4) {
const r = data[i]!;
const g = data[i + 1]!;
const b = data[i + 2]!;
// --- STEP 1: Grayscale Conversion (สูตร Portrait) ---
// ปกติ: r*0.3, g*0.59, b*0.11
// สูตรนี้: เพิ่มน้ำหนัก Red เป็น 0.4 เพื่อให้ "ผิวคน" ดูสว่างขึ้น (Skin Tone Brightening)
let gray = (r * 0.4) + (g * 0.5) + (b * 0.1);
// --- STEP 2: Apply Contrast & Brightness ---
gray = contrastFactor * (gray - 128) + 128 + brightness;
// --- STEP 3: Clamp Values ---
// บังคับค่าให้อยู่ในช่วง 0-255 และกำหนดให้ทั้ง R, G, B เท่ากัน (เพื่อเป็นสีเทา)
const finalGray = Math.max(0, Math.min(255, gray));
data[i] = finalGray; // R
data[i + 1] = finalGray; // G
data[i + 2] = finalGray; // B
// data[i+3] คือ Alpha ไม่ต้องยุ่ง
}
context.putImageData(imageData, 0, 0);
// --- STEP 4: Noir Effect (แสงฟุ้ง + ขอบมืด) ---
// ถ้าปรับ volume เยอะๆ ให้เติมขอบดำ เพื่อให้ดูเป็นสไตล์ Noir/Classic
if (volume > 3) {
addNoirVignette(context, width, height, volume);
}
};
// ฟังก์ชันสร้างขอบดำสำหรับขาวดำโดยเฉพาะ
const addNoirVignette = (ctx: CanvasRenderingContext2D, w: number, h: number, strength: number) => {
// ความเข้มของขอบดำ
const opacity = (strength / 10) * 0.8; // สูงสุด 0.8 (เข้มกว่าแบบฟิล์ม)
const radius = Math.max(w, h) * 0.7; // วงแคบกว่าฟิล์มหน่อย เพื่อเน้นหน้าคนตรงกลาง
const gradient = ctx.createRadialGradient(w/2, h/2, w/4, w/2, h/2, radius);
gradient.addColorStop(0, "rgba(0,0,0,0)");
gradient.addColorStop(1, `rgba(0,0,0,${opacity})`);
ctx.save();
ctx.globalCompositeOperation = 'multiply'; // ใช้ Multiply ให้เงามันซึมลงไปในภาพ
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, w, h);
ctx.restore();
};
const drawDateStamp = (context: CanvasRenderingContext2D, width: number, height: number) => {
const now = new Date();
@@ -206,7 +265,16 @@ const captureSinglePhoto = () => {
)
// Apply film effect with default volume
applyFilmEffect(context, finalWidth, finalHeight, 5);
// applyFilmEffect(context, finalWidth, finalHeight, 2);
const filter = localStorage.getItem('photobooth-filter') || 'normal'
if (filter === 'black&white') {
applyBlackAndWhite(context, finalWidth, finalHeight, 3);
} else {
applyFilmEffect(context, finalWidth, finalHeight, 5); // Default volume = 5
}
drawDateStamp(context, finalWidth, finalHeight);
// แปลงเป็น base64 ด้วย quality 0.7 เพื่อลดขนาด