일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- 서보모터
- 일론 머스크
- javascript
- AI 기술
- LLM
- 오픈AI
- 트랜스포머
- tts
- ControlNet
- 이미지 편집
- 아두이노
- AI
- 가상환경
- 메타
- 우분투
- ubuntu
- TRANSFORMER
- 확산 모델
- 딥마인드
- PYTHON
- LORA
- 오블완
- 시간적 일관성
- 인공지능
- 뉴럴링크
- 티스토리챌린지
- OpenAI
- ChatGPT
- 생성형 AI
- 멀티모달
- Today
- Total
AI 탐구노트
나만의 썸네일 메이커 만들기 - 2편 본문
지난 번에는 썸네일 메이커를 만들어 봤었습니다.
개선사항
당시 기능 가운데 아쉬운 부분 한가지가 랜덤 그라디언트의 다소 밋밋함이었습니다. 그래서, 기능 개선을 해 보려고 합니다. 랜덤 그라디언트는 사실 단색으로 된 화면 내용을 훨씬 더 멋지게 만들어주는 역할을 합니다. 하지만, 그것만 계속 사용하기에는 임팩트도 없고... 글의 맨 앞이 약간 심심해지기도 하죠. 그걸 안 하려면 직접 배경 이미지 찾아서 넣어야 하는데 이미지를 매번 찾아서 넣을 것도 아니고...
그래서, 기하학적인 문양을 코드로 만들어 이를 배경으로 사용해야겠다는 생각을 하게 됐습니다. 제가 구현하려는 것은 랜덤 그라디언트 대신에 기하학적인 변형과 그라디언트를 함께 적용하는 방식으로 원본 코드는 다음 URL에서 확인할 수 있습니다.
해당 기능이 작동하는 방식을 보면 다음과 같습니다.
일단 색상이 섞여 있는 격자 이미지를 생성하고, 연결점들의 위치를 조정한 후에, 만들어진 다각형에 2가지 선택된 색상으로 그라디언트를 적용하는 것입니다.
어떤가요? 좀 더 뭔가 있어 보이지 않나요?
추가) 2024.10.20
Drag & Drop 부분을 클릭했을 때 사진을 선택하고 업로드할 수 있는 기능을 추가했습니다.
구현
시행착오-1
github repo는 python 코드로 되어 있습니다. 이것을 Javascript로 변환해서 기존 코드 내에 넣을 생각이었습니다. ChatGPT, 구글링 등을 이용해서 이것저것 적용해 봤지만 제대로 된 답을 얻지 못했습니다. 질문을 잘못 했는지는 모르겠지만, ChatGPT도 알고리즘을 제대로 이해하지 못하고 엉뚱한 접근법만 제시했던 것 같습니다. 결국 Python 코드를 그대로 활용하는 쪽으로 방향을 틀었습니다. FastAPI는 python 코드이니 여기서 만들고 javascript에서 요청하면 그것을 전달해 주는 방식으로 말입니다.
구현 결과물
파일 구조는 다음과 같습니다.
이 가운데 main.py를 제외한 나머지 python 코드는 위에 소개한 wallpaper-generator github repo에서 가져온 것입니다.
전체 코드는 다음과 같습니다.
main.py 코드
아래 코드에서 추가된 부분은 generate_wallpaper() 함수 부분입니다. wallpaper-generator repo의 코드에서 대부분을 가져왔고 다만, 반환하는 데이터 타입 때문에 StreamingResponse를 이용하는 방식으로 변경했습니다.
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse, StreamingResponse
from os import path, makedirs
from math import ceil
from io import BytesIO
from PIL import Image, ImageDraw
from random import sample
from polylattice import PolyLattice
from colors import palettes
app = FastAPI()
# 정적 파일 제공 (HTML, CSS, JS)
app.mount("/static", StaticFiles(directory="static"), name="static")
@app.get("/")
def read_root():
return FileResponse("static/index.html")
@app.get("/wallpaper")
def generate_wallpaper():
## Configurations ##
palette = 'pastel_forest'
mutation_intensity = 30
resolution = [1920, 1080]
# Polygons have a fixed size in px. Higher resolution = more polygons
poly_sizes = (120, 100)
# Create an image of the size of the screen
im = Image.new("RGB", resolution, 0)
image_draw = ImageDraw.Draw(im)
# Initialise a PolyLattice
poly_count_x = (resolution[0] / poly_sizes[0])
poly_count_y = (resolution[1] / poly_sizes[1])
# Last polygons might be partly overflowing the image
polylattice = PolyLattice(
im.size,
(ceil(poly_count_x), ceil(poly_count_y)),
poly_sizes)
polylattice.initialise(separate_in_triangles=True)
# Choose two colors from the palette
colors = sample(palettes[palette], 2)
# Mutate PolyLattice and apply random gradient of colors
polylattice.mutate(mutation_intensity)
polylattice.gradient_colors_random_direction(colors[0], colors[1])
# Draw the polylattice on the image
polylattice.draw(image_draw)
# 이미지를 메모리에 저장하여 반환
img_io = BytesIO()
im.save(img_io, 'PNG')
img_io.seek(0)
return StreamingResponse(img_io, media_type="image/png")
app.js 코드
기존에 있던 부분 제외하면, generateWallpaper 관련 부분만 추가되었다고 보시면 됩니다.
fetchWallpaper 함수를 통해 FastAPI 서비스 호출해서 받아오면 그걸 canvas 상에 표출하는 방식입니다.
document.addEventListener("DOMContentLoaded", () => {
const canvas = document.getElementById("thumbnailCanvas");
const ctx = canvas.getContext("2d");
const titleInput = document.getElementById("title");
const subtitleInput = document.getElementById("subtitle");
const categoryInput = document.getElementById("category");
const titleColorPicker = document.getElementById("titleColor");
const subtitleColorPicker = document.getElementById("subtitleColor");
const categoryColorPicker = document.getElementById("categoryColor");
const generateBtn = document.getElementById("generate");
const generateWallpaperBtn = document.getElementById("generateWallpaper");
const saveBtn = document.getElementById("save");
const dragDropArea = document.getElementById("dragDropArea");
let backgroundGradient = null; // 현재 배경 그라디언트 저장용 변수
const backgroundCanvas = document.createElement("canvas"); // 배경 저장용 캔버스
const backgroundCtx = backgroundCanvas.getContext("2d");
async function fetchWallpaper() {
const response = await fetch('/wallpaper');
if (response.ok) {
const blob = await response.blob();
const url = URL.createObjectURL(blob);
// Canvas 요소 가져오기
const img = new Image();
img.src = url;
img.onload = function() {
// 이미지를 Canvas에 그리기
backgroundCanvas.width = canvas.width;
backgroundCanvas.height = canvas.height;
backgroundCtx.drawImage(img, 0, 0, canvas.width, canvas.height);
backgroundImage = img; // wallpaper 이미지를 저장하여 배경으로 사용
// 텍스트 다시 그리기
drawText();
};
} else {
console.error("Wallpaper fetch failed:", response.statusText);
}
}
// 텍스트 그리기
function drawText() {
const title = titleInput.value;
const subtitle = subtitleInput.value;
const category = categoryInput.value;
const titleColor = titleColorPicker.value;
const subtitleColor = subtitleColorPicker.value;
const categoryColor = categoryColorPicker.value;
// 캔버스 지우기
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 배경 그리기 - 현재 배경이 wallpaper인지 gradient인지 확인
if (backgroundGradient) {
// gradient 배경일 경우
ctx.drawImage(backgroundCanvas, 0, 0);
} else if (backgroundImage) {
// wallpaper 배경일 경우
ctx.drawImage(backgroundImage, 0, 0, canvas.width, canvas.height);
}
// 제목 텍스트
ctx.fillStyle = titleColor;
ctx.textAlign = 'center';
ctx.font = `bold ${textStyles.title.fontSize}px Arial`;
const titleYPos = canvas.height - (canvas.height * textStyles.title.yPosPercent) / 100;
ctx.fillText(title, canvas.width / 2, titleYPos);
// 제목 아래 줄 그리기
if (title) {
const textWidth = ctx.measureText(title).width;
const lineYPos = titleYPos + 10; // 제목 글자와의 간격 10px
ctx.strokeStyle = titleColor;
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo((canvas.width - textWidth) / 2, lineYPos);
ctx.lineTo((canvas.width + textWidth) / 2, lineYPos);
ctx.stroke();
}
// 세부제목 텍스트
ctx.fillStyle = subtitleColor;
ctx.font = `bold ${textStyles.subtitle.fontSize}px Arial`;
ctx.fillText(subtitle, canvas.width / 2, canvas.height - (canvas.height * textStyles.subtitle.yPosPercent) / 100);
// 분류 텍스트
ctx.fillStyle = categoryColor;
ctx.font = `bold ${textStyles.category.fontSize}px Arial`;
ctx.fillText(category, canvas.width / 2, canvas.height - (canvas.height * textStyles.category.yPosPercent) / 100);
}
// 모든 텍스트 입력 시 즉시 반영되도록 이벤트 리스너 추가 (배경은 다시 그리지 않음)
titleInput.addEventListener('input', drawText);
subtitleInput.addEventListener('input', drawText);
categoryInput.addEventListener('input', drawText);
titleColorPicker.addEventListener('input', drawText);
subtitleColorPicker.addEventListener('input', drawText);
categoryColorPicker.addEventListener('input', drawText);
// 숨겨진 파일 선택 input 요소 추가
const fileInput = document.createElement("input");
fileInput.type = "file";
fileInput.accept = "image/*";
fileInput.style.display = "none"; // 화면에 표시되지 않도록 설정
document.body.appendChild(fileInput);
// dragDropArea 클릭 시 파일 선택 창 열기
dragDropArea.addEventListener("click", () => {
fileInput.click();
});
// 파일 선택 시 파일을 배경으로 설정
fileInput.addEventListener("change", (event) => {
const file = event.target.files[0];
if (file && file.type.startsWith("image/")) {
const img = new Image();
const reader = new FileReader();
reader.onload = (e) => {
img.src = e.target.result;
img.onload = () => {
backgroundCanvas.width = canvas.width;
backgroundCanvas.height = canvas.height;
backgroundCtx.drawImage(img, 0, 0, canvas.width, canvas.height);
backgroundImage = img; // 이미지 배경 저장
backgroundGradient = null; // 그라디언트를 제거
drawText(); // 배경 이미지를 그리고 텍스트 다시 그리기
};
};
reader.readAsDataURL(file);
}
// 선택 후 input 초기화
fileInput.value = "";
});
const textStyles = {
title: { fontSize: 50, yPosPercent: 50 },
subtitle: { fontSize: 28, yPosPercent: 35 },
category: { fontSize: 20, yPosPercent: 10 }
};
// 랜덤 그라디언트 배경 생성
function generateRandomGradient() {
backgroundCanvas.width = canvas.width;
backgroundCanvas.height = canvas.height;
const gradient = backgroundCtx.createLinearGradient(0, 0, canvas.width, canvas.height);
gradient.addColorStop(0, `rgb(${Math.random() * 255}, ${Math.random() * 255}, ${Math.random() * 255})`);
gradient.addColorStop(1, `rgb(${Math.random() * 255}, ${Math.random() * 255}, ${Math.random() * 255})`);
backgroundCtx.fillStyle = gradient;
backgroundCtx.fillRect(0, 0, canvas.width, canvas.height);
backgroundGradient = gradient;
}
// 이미지 파일을 드래그 앤 드롭하여 캔버스에 배경으로 설정
dragDropArea.addEventListener('dragover', (event) => {
event.preventDefault(); // 기본 동작 방지
dragDropArea.classList.add('dragging');
});
dragDropArea.addEventListener('dragleave', () => {
dragDropArea.classList.remove('dragging');
});
dragDropArea.addEventListener('drop', (event) => {
event.preventDefault(); // 기본 동작 방지
event.stopPropagation(); // 브라우저의 기본 동작을 차단하여 탭에서 열리지 않도록 함
dragDropArea.classList.remove('dragging');
const file = event.dataTransfer.files[0];
if (file && file.type.startsWith('image/')) {
const img = new Image();
const reader = new FileReader();
reader.onload = (e) => {
img.src = e.target.result;
img.onload = () => {
backgroundCanvas.width = canvas.width;
backgroundCanvas.height = canvas.height;
backgroundCtx.drawImage(img, 0, 0, canvas.width, canvas.height);
backgroundImage = img; // 이미지 배경 저장
backgroundGradient = null; // 그라디언트를 제거
drawText(); // 배경 이미지를 그리고 텍스트 다시 그리기
};
};
reader.readAsDataURL(file);
}
});
// 랜덤 그라디언트, 월페이퍼 생성 버튼 이벤트 등록
generateBtn.addEventListener("click",() => {
generateRandomGradient();
backgroundChoice = 0;
drawText();
});
generateWallpaperBtn.addEventListener("click",() => {
fetchWallpaper('thumbnailCanvas', 100);
backgroundChoice = 1;
drawText();
});
saveBtn.addEventListener("click", () => {
const link = document.createElement("a");
link.download = "thumbnail.png";
link.href = canvas.toDataURL();
link.click();
});
// 페이지 로드 시 wallpaper 호출
window.onload = fetchWallpaper;
// 창 크기 변경 시에도 배경 재생성
window.onresize = fetchWallpaper;
// 초기 텍스트 그리기
drawText();
});
index.html 파일
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Thumbnail Maker</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="container">
<h1>Thumbnail Maker</h1>
<div class="thumbnail-preview">
<canvas id="thumbnailCanvas" width="800" height="400"></canvas>
</div>
<div class="controls">
<!-- 제목 관련 입력 필드 -->
<div class="input-group">
<label for="title">제목:</label>
<input type="text" id="title" placeholder="메일 제목">
<input type="color" id="titleColor" value="#FFFFFF"> <!-- 색상 선택기 -->
</div>
<!-- 세부제목 관련 입력 필드 -->
<div class="input-group">
<label for="subtitle">세부제목:</label>
<input type="text" id="subtitle" placeholder="세부 제목">
<input type="color" id="subtitleColor" value="#FFFFFF"> <!-- 색상 선택기 -->
</div>
<!-- 분류 관련 입력 필드 -->
<div class="input-group">
<label for="category">분류:</label>
<input type="text" id="category" placeholder="분류">
<input type="color" id="categoryColor" value="#FFFFFF"> <!-- 색상 선택기 -->
</div>
<button id="generate">랜덤 그라디언트 생성</button>
<button id="generateWallpaper">Wallpaper 생성</button>
<button id="save">이미지 저장</button>
</div>
<div class="drag-drop-area" id="dragDropArea">
<p>여기에 이미지를 끌어다 놓거나 클릭하여 파일을 선택하세요</p>
</div>
<!-- 숨겨진 파일 입력 필드 -->
<input type="file" id="fileInput" accept="image/*" style="display: none;">
</div>
<script src="/static/app.js"></script>
</body>
</html>
결과확인
위의 코드가 적용된 이후 사용해 보면 흠... 생각보다 괜찮습니다.
배경 이미지를 Unsplash에서 찾는 것보다는 이것을 더 많이 사용하게 될 것 같네요. ^^;
이번 글에서는 기존에 만들어 둔 썸네일 메이커에 새로운 배경 삽입 기능을 하나 추가해서 보여드렸습니다.
다음 번에도 재미난 것을 만들어서 소개해 드리겠습니다. ^^
'DIY 테스트' 카테고리의 다른 글
Headshot Tracking 따라하기 - 2편 (8) | 2024.10.14 |
---|---|
서보모터 (SG90 스탠다드) 테스트 (2) | 2024.10.14 |
썸네일 메이커 Electron 어플리케이션으로 전환하기 (1) | 2024.10.09 |
AudioCraft를 이용한 효과음 만들어 보기 (5) | 2024.10.09 |
나만의 썸네일 메이커 만들기 - 1편 (6) | 2024.10.09 |