Files
sam-kd/draw/index.html
hskwon aca1767eb9 초기 커밋: 5130 레거시 시스템
- URL 하드코딩 → .env APP_URL 기반 동적 URL로 변경
- DB 연결 하드코딩 → .env 기반으로 변경
- MySQL strict mode DATE 오류 수정
2025-12-10 20:14:31 +09:00

296 lines
11 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Fabric.js 이미지 편집기 (폴리라인 기본)</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>
#c { border:1px solid #dee2e6; cursor: crosshair; }
.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; }
</style>
</head>
<body class="bg-light">
<div class="container py-3">
<!-- 툴바 -->
<div class="d-flex align-items-center mb-3 gap-2">
<!-- 순서: 폴리라인 → 자유선 → 직선 → 문자 → 지우개 → 객체선택 -->
<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">전체&nbsp;지우기</button>
</div>
<!-- 색상 선택 -->
<div id="colorPicker" class="btn-group mb-3" role="group">
<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>
<!-- 지우개 크기 -->
<div class="row mb-3 align-items-center gx-2">
<div class="col-auto">
<label for="eraserRange" class="form-label mb-0">지우개&nbsp;크기</label>
</div>
<div class="col">
<input type="range" class="form-range" id="eraserRange" min="5" max="100" step="1" value="20">
</div>
<div class="col-auto">
<span id="eraserSizeLabel" class="fw-bold">20</span>px
</div>
</div>
<!-- 캔버스 -->
<div class="border bg-white">
<canvas id="c" width="800" height="600" class="w-100"></canvas>
</div>
</div>
<!-- jQuery, Bootstrap, Fabric.js -->
<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 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 created
canvas.on('path:created', e=>{
e.path.selectable=selectMode;
e.path.stroke = (mode==='eraser')? '#ffffff' : currentColor;
});
// 툴 활성화 표시
function highlightButton(){
$('.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 / 객체선택 가능
selectMode=(m==='select');
canvas.selection=selectMode;
canvas.forEachObject(o=>{
if(o===canvas.backgroundImage) return;
o.selectable=selectMode;
o.evented=selectMode;
});
highlightButton();
}
// 초기: 폴리라인
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;
});
// 키 이벤트
$(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 act=canvas.getActiveObject();
if(act&&act.isEditing) canvas.remove(act);
}
if(e.key==='Delete'&&selectMode){
const objs=canvas.getActiveObjects();
objs.forEach(o=>canvas.remove(o));
canvas.discardActiveObject();
canvas.requestRenderAll();
}
});
// 마우스 다운
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'){
const it=new fabric.IText('',{ left:p.x, top:p.y, fontSize:14, fill:currentColor, selectable:false });
canvas.add(it).setActiveObject(it);
it.enterEditing();
it.hiddenTextarea.focus();
it.on('editing:exited',()=>{
if(!it.text) canvas.remove(it);
else it.selectable=selectMode;
canvas.requestRenderAll();
});
}
});
// 마우스 무브
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>