Files
sam-kd/draw/index1.html

345 lines
13 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>그리기 에디터 대화상자 (HTML 텍스트입력)</title>
<!-- Bootstrap 5.3 CSS + Icons -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" rel="stylesheet">
<style>
body { padding: 1rem; }
dialog {
width: 90vw; height: 90vh;
border: none; border-radius: .5rem;
padding: 0; overflow: hidden;
}
dialog::backdrop { background: rgba(0,0,0,.5); }
#editorHeader {
background: #f8f9fa; padding: .5rem 1rem;
border-bottom: 1px solid #dee2e6;
}
#editorHeader h5 { margin: 0; display: inline; }
#editorHeader .btn-close { float: right; }
#editorToolbar, #editorToolbar2 {
background: #fff;
padding: .5rem; gap: .25rem;
}
#editorToolbar { display: flex; align-items: center; flex-wrap: wrap; }
#editorToolbar2 { display: flex; align-items: center; }
.toolbar-btn { width: 40px; height: 40px; padding: 0; }
.toolbar-btn i { font-size: 1.2rem; }
.toolbar-btn.active { background-color: #0d6efd; color: #fff; }
.color-btn { width: 24px; height: 24px; padding: 0; border: 2px solid #fff; cursor: pointer; }
.color-btn.active { border-color: #000; }
#editorBody { position: relative; width: 100%; height: calc(100% - 128px); }
#editorBody canvas { width: 100%; height: 100%; cursor: crosshair; }
.form-range { width: 150px; }
/* HTML textarea 스타일 */
.canvas-text-input {
position: absolute;
border: 1px dashed #666;
background: transparent;
resize: none;
outline: none;
padding: 2px;
font-family: sans-serif;
}
</style>
</head>
<body>
<!-- 그리기 시작 버튼 -->
<button id="openEditorBtn" class="btn btn-primary mb-3">
<i class="bi bi-pencil-square"></i> 그리기 시작
</button>
<!-- 대화상자 -->
<dialog id="editorDialog">
<div id="editorHeader">
<h5>이미지 편집기</h5>
<button type="button" class="btn-close" aria-label="닫기" id="closeEditorBtn"></button>
</div>
<!-- 툴바 -->
<div id="editorToolbar">
<button id="polyBtn" class="btn btn-outline-primary toolbar-btn" title="폴리라인"><i class="bi bi-vector-pen"></i></button>
<button id="freeBtn" class="btn btn-outline-primary toolbar-btn" title="자유선"><i class="bi bi-brush"></i></button>
<button id="lineBtn" class="btn btn-outline-primary toolbar-btn" title="직선 (L키)"><i class="bi bi-slash-lg"></i></button>
<button id="textBtn" class="btn btn-outline-primary toolbar-btn" title="문자입력"><i class="bi bi-type"></i></button>
<button id="eraserBtn" class="btn btn-outline-warning toolbar-btn" title="지우개"><i class="bi bi-eraser-fill"></i></button>
<button id="selectBtn" class="btn btn-outline-secondary toolbar-btn" title="객체선택"><i class="bi bi-cursor-text"></i></button>
<div class="form-check form-switch ms-3">
<input class="form-check-input" type="checkbox" id="rightAngle" checked>
<label class="form-check-label" for="rightAngle">직각 고정</label>
</div>
<button id="clearBtn" class="btn btn-outline-danger ms-auto">전체 지우기</button>
</div>
<!-- 지우개 크기 & 색상 -->
<div id="editorToolbar2" class="d-flex align-items-center px-3 pb-2">
<div class="btn-group" role="group" id="colorPicker">
<button type="button" class="btn color-btn" data-color="#000000" style="background:#000;"></button>
<button type="button" class="btn color-btn" data-color="#ff0000" style="background:#f00;"></button>
<button type="button" class="btn color-btn" data-color="#0000ff" style="background:#00f;"></button>
<button type="button" class="btn color-btn" data-color="#00aa00" style="background:#0a0;"></button>
<button type="button" class="btn color-btn" data-color="#ff8800" style="background:#f80;"></button>
<button type="button" class="btn color-btn" data-color="#800080" style="background:#808;"></button>
<button type="button" class="btn color-btn" data-color="#888888" style="background:#888;"></button>
</div>
<label class="mb-0 me-2">지우개 크기</label>
<input type="range" class="form-range me-2" id="eraserRange" min="5" max="100" step="1" value="20">
<span id="eraserSizeLabel" class="fw-bold">20</span>px
</div>
<!-- 캔버스 영역 -->
<div id="editorBody">
<canvas id="c" width="800" height="600"></canvas>
</div>
</dialog>
<!-- 의존 스크립트 -->
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.0/fabric.min.js"></script>
<script>
$(function(){
const dialog = document.getElementById('editorDialog');
const editorBody = document.getElementById('editorBody');
// “그리기 시작” 버튼
$('#openEditorBtn').click(() => {
if (!dialog.open) dialog.showModal();
});
// 우측 X 버튼
$('#closeEditorBtn').click(() => dialog.close());
// ESC로 닫히는 기본 동작 막기
dialog.addEventListener('cancel', e => e.preventDefault());
// Fabric.js 초기화
const canvas = new fabric.Canvas('c',{ selection:false });
let mode='polyline', selectMode=false, isLine=false, lineObj=null, isRight=true;
let polyPoints=[], previewLine=null, isPreview=true, currentColor='#0000ff';
// 브러시 설정
const freeBrush = canvas.freeDrawingBrush; freeBrush.width=2; freeBrush.color=currentColor;
const eraserBrush = new fabric.PencilBrush(canvas);
eraserBrush.width = +$('#eraserRange').val(); eraserBrush.color='#ffffff';
function updateEraserCursor(r){
const svg=`<svg xmlns="http://www.w3.org/2000/svg" width="${r}" height="${r}"><circle cx="${r/2}" cy="${r/2}" r="${r/2}" stroke="black" fill="none"/></svg>`;
canvas.freeDrawingCursor = `url("data:image/svg+xml,${encodeURIComponent(svg)}") ${r/2} ${r/2}, auto`;
}
updateEraserCursor(eraserBrush.width);
// 배경 이미지
fabric.Image.fromURL('https://via.placeholder.com/800x600.png', img=>{
img.selectable = false;
canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas));
});
// path 생성 시 스타일
canvas.on('path:created', e=>{
e.path.selectable = selectMode;
e.path.stroke = (mode==='eraser')? '#ffffff' : currentColor;
});
// 버튼 강조
function highlight(){
$('.toolbar-btn').removeClass('active');
switch(mode){
case 'polyline': $('#polyBtn').addClass('active'); break;
case 'free': $('#freeBtn').addClass('active'); break;
case 'line': $('#lineBtn').addClass('active'); break;
case 'text': $('#textBtn').addClass('active'); break;
case 'eraser': $('#eraserBtn').addClass('active'); break;
case 'select': $('#selectBtn').addClass('active'); break;
}
}
// 모드 전환
function setMode(m){
if(mode==='polyline'&&m!=='polyline'){
if(previewLine){ canvas.remove(previewLine); previewLine=null; }
polyPoints=[];
}
mode=m; canvas.isDrawingMode=false;
if(m==='free'){ canvas.isDrawingMode=true; canvas.freeDrawingBrush=freeBrush; }
if(m==='eraser'){ canvas.isDrawingMode=true; canvas.freeDrawingBrush=eraserBrush; }
const el=canvas.upperCanvasEl;
el.style.cursor = (m==='eraser')? canvas.freeDrawingCursor : 'crosshair';
selectMode = (m==='select');
canvas.selection = selectMode;
canvas.forEachObject(o=>{
if(o===canvas.backgroundImage) return;
o.selectable=selectMode;
o.evented =selectMode;
});
highlight();
}
setMode('polyline');
// 툴바 클릭
$('#polyBtn').click(()=>setMode('polyline'));
$('#freeBtn').click(()=>setMode('free'));
$('#lineBtn').click(()=>setMode('line'));
$('#textBtn').click(()=>setMode('text'));
$('#eraserBtn').click(()=>setMode('eraser'));
$('#selectBtn').click(()=> setMode($('#selectBtn').hasClass('active')?'free':'select'));
$('#clearBtn').click(()=>{
canvas.clear();
fabric.Image.fromURL('https://via.placeholder.com/800x600.png',img=>{
img.selectable=false; canvas.setBackgroundImage(img,canvas.renderAll.bind(canvas));
});
polyPoints=[]; if(previewLine)canvas.remove(previewLine); previewLine=null;
canvas.upperCanvasEl.style.cursor='crosshair';
setMode('polyline');
});
// 색상 선택
$('#colorPicker .color-btn').click(function(){
$('#colorPicker .color-btn').removeClass('active');
$(this).addClass('active');
currentColor = $(this).data('color');
freeBrush.color = currentColor;
});
$('#rightAngle').change(function(){ isRight=this.checked; });
$('#eraserRange').on('input',function(){
const s=+this.value;
$('#eraserSizeLabel').text(s);
eraserBrush.width = s; updateEraserCursor(s);
if(mode==='eraser') canvas.upperCanvasEl.style.cursor=canvas.freeDrawingCursor;
});
// 전체 keydown: polyline ESC, text ESC, delete, shortcut L
$(document).keydown(e=>{
if(e.key.toLowerCase()==='l') setMode('line');
if(e.key==='Escape'&&mode==='polyline'){
isPreview=false;
if(previewLine){ canvas.remove(previewLine); previewLine=null; canvas.requestRenderAll(); }
}
if(e.key==='Escape'&&mode==='text'){
const existing = editorBody.querySelector('.canvas-text-input');
existing && existing.remove();
}
if(e.key==='Delete'&&selectMode){
const objs=canvas.getActiveObjects();
objs.forEach(o=>canvas.remove(o));
canvas.discardActiveObject();
canvas.requestRenderAll();
}
});
// 마우스 다운: line/poly/text
canvas.on('mouse:down',o=>{
const p=canvas.getPointer(o.e);
if(mode==='line'){
isLine=true;
lineObj=new fabric.Line([p.x,p.y,p.x,p.y],{
stroke:currentColor, strokeWidth:2,
originX:'center', originY:'center',
selectable:selectMode
});
canvas.add(lineObj);
}
else if(mode==='polyline'){
isPreview=true;
let x=p.x,y=p.y;
if(isRight&&polyPoints.length){
const L=polyPoints[polyPoints.length-1];
const dx=Math.abs(x-L.x),dy=Math.abs(y-L.y);
if(dx>dy) y=L.y; else x=L.x;
}
if(polyPoints.length){
const prev=polyPoints[polyPoints.length-1];
const seg=new fabric.Line([prev.x,prev.y,x,y],{
stroke:currentColor, strokeWidth:2,
originX:'center', originY:'center',
selectable:selectMode
});
canvas.add(seg);
}
polyPoints.push({x,y});
if(previewLine){ canvas.remove(previewLine); previewLine=null; }
}
else if (mode === 'text') {
// 기존에 떠 있는 textarea 제거
editorBody.querySelectorAll('.canvas-text-input').forEach(el => el.remove());
// HTML textarea 오버레이 생성
const ta = document.createElement('textarea');
ta.className = 'canvas-text-input';
ta.style.left = p.x + 'px';
ta.style.top = p.y + 'px';
ta.style.fontSize = '14px';
ta.rows = 1;
ta.cols = 20;
editorBody.appendChild(ta);
// 바로 포커스
setTimeout(() => {
ta.focus();
// 전체 텍스트를 선택하려면 아래를 추가하세요.
// ta.select();
}, 0);
// Enter / Esc 처리
ta.addEventListener('keydown', e => {
if (e.key === 'Enter') {
e.preventDefault();
const txt = ta.value.trim();
if (txt) {
const txtObj = new fabric.Text(txt, {
left: p.x,
top: p.y,
fontSize: 14,
fill: currentColor
});
canvas.add(txtObj);
}
ta.remove();
}
else if (e.key === 'Escape') {
ta.remove();
}
});
}
});
// 마우스 무브: line/poly preview
canvas.on('mouse:move',o=>{
const p=canvas.getPointer(o.e);
if(mode==='line'&&isLine){
let x2=p.x,y2=p.y;
if(isRight){
const dx=Math.abs(x2-lineObj.x1), dy=Math.abs(y2-lineObj.y1);
if(dx>dy) y2=lineObj.y1; else x2=lineObj.x1;
}
lineObj.set({x2,y2});
canvas.requestRenderAll();
}
else if(mode==='polyline'&&polyPoints.length&&isPreview){
const L=polyPoints[polyPoints.length-1];
let x2=p.x,y2=p.y;
if(isRight){
const dx=Math.abs(x2-L.x),dy=Math.abs(y2-L.y);
if(dx>dy) y2=L.y; else x2=L.x;
}
if(previewLine){
previewLine.set({x1:L.x,y1:L.y,x2,y2});
} else {
previewLine=new fabric.Line([L.x,L.y,x2,y2],{
stroke:'gray',strokeWidth:1,strokeDashArray:[5,5],selectable:false
});
canvas.add(previewLine);
}
canvas.requestRenderAll();
}
});
canvas.on('mouse:up',()=>{
if(mode==='line'){ isLine=false; lineObj=null; }
});
});
</script>
</body>
</html>