<!DOCTYPE html>
<html lang="ja" >
<head>
<meta charset="UTF-8" >
<title>Fluffy Painter 1.0 </title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/paper.js/0.12.17/paper-full.min.js" ></script>
<style>
:root { --bg-dark: #202225; --bg-panel: #2f3136; --bg-hover: #393c43; --text-main: #dcddde; --text-muted: #8e9297; --accent: #7289da; --border: #202225; }
body { margin: 0 ; padding: 0 ; display: flex; flex-direction: column; height: 100 vh; background-color: var (--bg-dark); color: var (--text-main); font-family: 'Segoe UI' , Tahoma, Geneva, Verdana, sans-serif; font-size: 14 px; overflow: hidden; }
#menubar { display: flex; background-color: var(--bg-dark); padding: 5px 10px; border-bottom: 1px solid #111; user-select: none; }
.menu-item { position: relative; padding: 5 px 10 px; cursor: pointer; border-radius: 3 px; }
.menu-item:hover { background-color: var (--bg-hover); }
.dropdown { display: none; position: absolute; top: 100 %; left: 0 ; background-color: var (--bg-panel); border: 1 px solid #111; box-shadow: 0 4px 6px rgba(0,0,0,0.3); z-index: 100; min-width: 180px; }
.menu-item:hover .dropdown { display: block; }
.dropdown-item { padding: 8 px 15 px; cursor: pointer; display: flex; justify-content: space-between; }
.dropdown-item:hover { background-color: var (--accent); color: white; }
.shortcut-hint { color: var (--text-muted); font-size: 0.85 em; }
.dropdown-item:hover .shortcut-hint { color: #ddd; }
#main { display: flex; flex-grow: 1; overflow: hidden; }
#sidebar { width: 300px; background-color: var(--bg-panel); display: flex; flex-direction: column; border-right: 1px solid var(--border); }
#tabs { display: flex; border-bottom: 1px solid var(--border); background-color: var(--bg-dark); }
.tab-btn { flex: 1 ; padding: 10 px 0 ; text-align: center; cursor: pointer; border-bottom: 2 px solid transparent; color: var (--text-muted); }
.tab-btn.active { color: var (--text-main); border-bottom-color: var (--accent); background-color: var (--bg-panel); }
.tab-content { display: none; flex-grow: 1 ; padding: 15 px; overflow-y: auto; }
.tab-content.active { display: flex; flex-direction: column; gap: 15 px; }
.prop-group { display: flex; flex-direction: column; gap: 5 px; background: rgba(0 ,0 ,0 ,0.1 ); padding: 10 px; border-radius: 5 px; border: 1 px solid rgba (255 ,255 ,255 ,0.05 ) ; }
.prop-group label { font-size: 0.9 em; display: flex; justify-content: space-between; }
input[type="range" ] { width: 100 %; accent-color: var (--accent); }
input[type="color" ] { width: 100 %; height: 30 px; border: none; border-radius: 4 px; cursor: pointer; padding: 0 ; background: none; }
button.action-btn { background-color: var (--accent); color: white; border: none; padding: 10 px; border-radius: 4 px; cursor: pointer; width: 100 %; margin-top: 5 px; font-weight: bold; }
button.action-btn:hover { filter: brightness(1.2 ); }
button.action-btn-green { background-color: #43b581; }
button.action-btn-outline { background-color: transparent; border: 1 px solid var (--text-muted ) ; color: var (--text-main); }
button.action-btn-outline:hover { background-color: rgba(255 ,255 ,255 ,0.1 ); }
#workspace { flex-grow: 1; background-color: #1e1e1e; position: relative; display: flex; justify-content: center; align-items: center; overflow: hidden; }
#checkerboard { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-image: linear-gradient(45deg, #2a2a2a 25%, transparent 25%, transparent 75%, #2a2a2a 75%, #2a2a2a), linear-gradient(45deg, #2a2a2a 25%, transparent 25%, transparent 75%, #2a2a2a 75%, #2a2a2a); background-size: 20px 20px; background-position: 0 0, 10px 10px; z-index: 0; }
canvas { background-color: transparent; z-index: 1 ; box-shadow: 0 0 20 px rgba (0 ,0 ,0 ,0.8 ) ; }
#status-bar { position: absolute; bottom: 10px; left: 10px; background: rgba(0,0,0,0.7); padding: 5px 10px; border-radius: 5px; z-index: 10; font-size: 0.9em; pointer-events: none;}
.list-item { padding: 8 px; border-radius: 4 px; cursor: pointer; margin-bottom: 2 px; border: 1 px solid transparent; background-color: rgba(0 ,0 ,0 ,0.2 ); }
.list-item:hover { background-color: rgba(255 ,255 ,255 ,0.05 ); }
.list-item.selected { background-color: rgba(114 , 137 , 218 , 0.4 ); border-color: var (--accent); font-weight: bold; color: white;}
</style>
</head>
<body>
<div id="menubar" >
<div class ="menu-item" >ファイル
<div class ="dropdown" >
<div class ="dropdown-item" onclick="fileAction('new')" >新規作成</div>
<div class ="dropdown-item" onclick="fileAction('open')" >開く... (JSON)</div>
<div class ="dropdown-item" onclick="fileAction('save')" >上書き保存 (JSON) <span class ="shortcut-hint" >Ctrl+S</span></div>
<div class ="dropdown-item" onclick="fileAction('png')" >PNG出力</div>
<div class ="dropdown-item" onclick="fileAction('resize')" >キャンバスサイズ変更</div>
</div>
</div>
<div class ="menu-item" >編集
<div class ="dropdown" >
<div class ="dropdown-item" onclick="execAction('addSpot')" >スポットの追加 <span class ="shortcut-hint" >新規領域</span></div>
<div class ="dropdown-item" onclick="execAction('delete')" >削除 <span class ="shortcut-hint" >Del</span></div>
<div class ="dropdown-item" onclick="execAction('copy')" >コピー <span class ="shortcut-hint" >Ctrl+C</span></div>
<div class ="dropdown-item" onclick="execAction('paste')" >ペースト <span class ="shortcut-hint" >Ctrl+V</span></div>
<div class ="dropdown-item" onclick="execAction('unite')" >スポット結合 <span class ="shortcut-hint" >Ctrl+J</span></div>
</div>
</div>
<div class ="menu-item" >操作
<div class ="dropdown" >
<div class ="dropdown-item" onclick="setMode('select')" >選択・移動</div>
<div class ="dropdown-item" onclick="setMode('draw')" >ドロー (領域の追加)</div>
<div class ="dropdown-item" onclick="setMode('erase')" >ドロー (領域を削る)</div>
<div class ="dropdown-item" onclick="setMode('adjust')" >ベジェ曲線の調整</div>
</div>
</div>
<div class ="menu-item" >表示
<div class ="dropdown" >
<div class ="dropdown-item" onclick="changeZoom(1.2)" >ズームイン <span class ="shortcut-hint" >PageUp</span></div>
<div class ="dropdown-item" onclick="changeZoom(0.8)" >ズームアウト <span class ="shortcut-hint" >PageDown</span></div>
<div class ="dropdown-item" onclick="paper.view.zoom = 1" >100 %表示</div>
</div>
</div>
<div class ="menu-item" >設定</div>
</div>
<div id="main" >
<div id="sidebar" >
<div id="tabs" >
<div class ="tab-btn active" onclick="switchTab('tab-prop')" >プロパティ</div>
<div class ="tab-btn" onclick="switchTab('tab-layers')" >レイヤー</div>
<div class ="tab-btn" onclick="switchTab('tab-spots')" >スポット</div>
</div>
<div id="tab-prop" class ="tab-content active" >
<div class ="prop-group" >
<strong>現在のドロー設定</strong>
<label>ブラシサイズ: <span id="valBrush" >40 </span>px</label>
<input type="range" id="uiBrushSize" min="10" max="150" value ="40" >
</div>
<hr style="border-color: #444; width: 100%; margin: 5px 0;" >
<div id="spot-properties" style="opacity: 0.5; pointer-events: none;" >
<strong>選択中スポットの質感</strong>
<div class ="prop-group" >
<label>中心色 (ハイライト)</label>
<input type="color" id="uiCenterCol" value ="#8ab87a" >
</div>
<div class ="prop-group" >
<label>輪郭色 (影・エッジ)</label>
<input type="color" id="uiEdgeCol" value ="#2d4a22" >
</div>
<div class ="prop-group" >
<label>ふんわり感: <span id="valBlur" >25 </span></label>
<input type="range" id="uiBlur" min="0" max="80" value ="25" >
</div>
<div class ="prop-group" >
<label>影の深さ (きつさ): <span id="valDepth" >15 </span></label>
<input type="range" id="uiDepth" min="0" max="50" value ="15" >
</div>
<div style="display: flex; gap: 5px; margin-top: 10px;" >
<button class ="action-btn action-btn-outline" style="flex: 1; padding: 8px 5px; font-size: 0.9em;" onclick="execAction('vectorize')" >■ ベクター化</button>
<button class ="action-btn action-btn-green" style="flex: 1.5; padding: 8px 5px; margin-top: 0; font-size: 0.9em;" onclick="execAction('render')" >▶ 3 D化(立体化)</button>
</div>
</div>
</div>
<div id="tab-layers" class ="tab-content" >
<div class ="list-item selected" >レイヤー 1 (統合)</div>
</div>
<div id="tab-spots" class ="tab-content" >
<p style="color:#888; font-size:12px; margin:0 0 10px 0;" >※編集メニューから追加してください</p>
<div id="spot-list-container" ></div>
</div>
</div>
<div id="workspace" >
<div id="checkerboard" ></div>
<canvas id="myCanvas" width="1000" height="800" ></canvas>
<div id="status-bar" >モード: <span id="mode-text" style="color:var(--accent); font-weight:bold;" >選択・移動</span> | ズーム: <span id="zoom-text" >100 %</span> | スクロール: 方向キー</div>
</div>
</div>
<input type="file" id="fileLoader" style="display:none;" accept=".json" >
<script>
paper.setup('myCanvas' );
let mode = 'select' ;
let brushRadius = 40 ;
let selectedSpots = [];
let clipboard = null ;
let selectionRect = null ;
let spotCounter = 0 ;
let draftQueue = [];
const spotLayer = new paper.Layer({ name: 'spotLayer' });
const draftLayer = new paper.Layer({ name: 'draftLayer' });
const uiLayer = new paper.Layer({ name: 'uiLayer' });
const uiBrushSize = document.getElementById('uiBrushSize' );
const uiCenterCol = document.getElementById('uiCenterCol' );
const uiEdgeCol = document.getElementById('uiEdgeCol' );
const uiBlur = document.getElementById('uiBlur' );
const uiDepth = document.getElementById('uiDepth' );
const propPanel = document.getElementById('spot-properties' );
const spotListContainer = document.getElementById('spot-list-container' );
uiBrushSize.oninput = (e) => { brushRadius = parseInt(e.target.value ); document.getElementById('valBrush' ).innerText = brushRadius; };
const updateProps = (e) => {
if (e && e.target) document.getElementById(e.target.id.replace('ui' , 'val' )).innerText = e.target.value ;
if (selectedSpots.length > 0 ) {
selectedSpots.forEach(spot => {
spot.data.centerCol = uiCenterCol.value ; spot.data.edgeCol = uiEdgeCol.value ;
spot.data.blurAmt = parseInt(uiBlur.value ); spot.data.depthAmt = parseInt(uiDepth.value );
if (spot.data.isRendered) renderFluffySpot(spot);
});
}
};
uiCenterCol.oninput = updateProps; uiEdgeCol.oninput = updateProps; uiBlur.oninput = updateProps; uiDepth.oninput = updateProps;
window.switchTab = function(tabId) {
document.querySelectorAll('.tab-btn' ).forEach(btn => btn.classList.remove ('active' ));
document.querySelectorAll('.tab-content' ).forEach(content => content.classList.remove ('active' ));
event .target.classList.add ('active' );
document.getElementById(tabId).classList.add ('active' );
};
function commitVector ( ) {
if (draftQueue.length === 0 || selectedSpots.length === 0 ) return ;
let targetSpot = selectedSpots[0 ];
let basePath = targetSpot.children['basePath' ];
if (!basePath) return ;
spotLayer.activate();
let newBasePath = basePath.clone();
newBasePath.visible = true ;
for (let item of draftQueue) {
if (item.mode === 'draw' ) {
let temp = newBasePath.isEmpty() ? item.path.clone() : newBasePath.unite(item.path);
newBasePath.remove (); newBasePath = temp;
} else if (item.mode === 'erase' && !newBasePath.isEmpty()) {
let temp = newBasePath.subtract(item.path);
newBasePath.remove (); newBasePath = temp;
}
item.path.remove ();
}
draftQueue = [];
targetSpot.children['basePath' ].remove ();
newBasePath.name = 'basePath' ;
targetSpot.addChild(newBasePath);
if (targetSpot.data.isRendered) renderFluffySpot(targetSpot);
updateVisuals();
}
function updateVisuals ( ) {
spotListContainer.innerHTML = '' ;
let spots = spotLayer.children.filter(i => i.name && i.name.startsWith('Spot_' ));
for (let i = spots.length - 1 ; i >= 0 ; i--) {
let spot = spots[i];
let div = document.createElement('div' );
div.className = 'list-item' + (selectedSpots.includes(spot) ? ' selected' : '' );
div.innerText = spot.data.displayName || spot.name;
div.onclick = (e) => {
commitVector();
if (mode !== 'adjust' && mode !== 'draw' && mode !== 'erase' ) setMode('select' );
selectSpot(spot, e.ctrlKey || e.metaKey);
};
spotListContainer.appendChild(div);
}
spotLayer.children.forEach(spot => {
if (!spot.name || !spot.name.startsWith('Spot_' )) return ;
let base = spot.children['basePath' ];
let renderGrp = spot.children['renderGroup' ];
if (mode === 'adjust' && selectedSpots.includes(spot)) {
base .visible = true ; base .fullySelected = true ;
base .strokeColor = '#00aaff' ; base .strokeWidth = 1.5 ; base .fillColor = null ;
if (renderGrp) { renderGrp.visible = true ; renderGrp.opacity = 0.5 ; }
} else if ((mode === 'draw' || mode === 'erase' ) && selectedSpots.includes(spot)) {
base .visible = true ; base .fullySelected = false ;
base .strokeColor = '#00ff00' ; base .strokeWidth = 2 / paper.view.zoom;
base .fillColor = 'rgba(255,255,255,0.01)' ;
if (renderGrp) { renderGrp.visible = true ; renderGrp.opacity = 0.8 ; }
} else {
base .visible = false ; base .fullySelected = false ;
if (renderGrp) { renderGrp.visible = true ; renderGrp.opacity = 1.0 ; }
}
if (!spot.data.isRendered) {
base .visible = true ; base .fullySelected = false ;
base .strokeColor = selectedSpots.includes(spot) ? '#00ff00' : '#888888' ;
base .strokeWidth = 2 / paper.view.zoom;
base .fillColor = 'rgba(255,255,255,0.01)' ;
if (renderGrp) renderGrp.visible = false ;
}
});
if (mode === 'select' || mode === 'adjust' ) updateSelectionBounds();
else if (selectionRect) { selectionRect.remove (); selectionRect = null ; }
}
window.setMode = function(newMode) {
commitVector();
mode = newMode;
const texts = { 'draw' : 'ドロー (追加)' , 'erase' : 'ドロー (削る)' , 'select' : '選択・移動' , 'adjust' : 'ベジェ調整' };
document.getElementById('mode-text' ).innerText = texts[mode];
document.getElementById('myCanvas' ).style.cursor = mode === 'select' ? 'default' : 'crosshair' ;
updateVisuals();
};
function renderFluffySpot (spotGroup ) {
const oldRender = spotGroup.children['renderGroup' ];
if (oldRender) oldRender.remove ();
spotGroup.data.isRendered = true ;
const data = spotGroup.data;
const basePath = spotGroup.children['basePath' ];
if (!basePath || basePath.isEmpty()) return ;
const renderGroup = new paper.Group({ name: 'renderGroup' });
const baseFill = basePath.clone();
baseFill.visible = true ;
baseFill.fillColor = data.centerCol;
renderGroup.addChild(baseFill);
const clipGroup = new paper.Group();
const clipMask = basePath.clone();
clipMask.visible = true ;
clipGroup.addChild(clipMask);
clipGroup.clipped = true ;
const shadowWide = basePath.clone();
shadowWide.visible = true ;
shadowWide.fillColor = null ;
shadowWide.strokeColor = data.edgeCol;
shadowWide.strokeWidth = 2 ;
shadowWide.shadowColor = data.edgeCol;
shadowWide.shadowBlur = data.blurAmt;
shadowWide.shadowOffset = new paper.Point(0 , 0 );
const shadowDeep = basePath.clone();
shadowDeep.visible = true ;
shadowDeep.fillColor = null ;
shadowDeep.strokeColor = data.edgeCol;
shadowDeep.strokeWidth = 2 ;
shadowDeep.shadowColor = data.edgeCol;
shadowDeep.shadowBlur = data.depthAmt / 4 ;
shadowDeep.shadowOffset = new paper.Point(0 , 0 );
clipGroup.addChild(shadowWide);
clipGroup.addChild(shadowDeep);
renderGroup.addChild(clipGroup);
spotGroup.addChild(renderGroup);
if (selectedSpots.length === 1 && selectedSpots[0 ] === spotGroup) {
uiCenterCol.value = data.centerCol; uiEdgeCol.value = data.edgeCol;
uiBlur.value = data.blurAmt; uiDepth.value = data.depthAmt;
document.getElementById('valBlur' ).innerText = data.blurAmt;
document.getElementById('valDepth' ).innerText = data.depthAmt;
}
}
const tool = new paper.Tool();
let currentDraftPath = null ;
let startPoint = null ;
let activeSegment = null ; let activeHandle = null ;
tool.onMouseDown = function(event ) {
if (mode === 'select' ) {
const hitResult = spotLayer.hitTest(event .point, { fill: true , stroke: true , tolerance: 2 });
if (hitResult) {
let item = hitResult.item;
while (item.parent && item.parent !== spotLayer) { item = item.parent; }
selectSpot(item, event .modifiers.control || event .modifiers.command);
} else {
clearSelection();
}
return ;
}
if (mode === 'adjust' ) {
if (selectedSpots.length === 0 ) return ;
let targetSpot = selectedSpots[0 ];
let hitResult = targetSpot.children['basePath' ].hitTest(event .point, { segments: true , handles: true , tolerance: 8 });
if (hitResult) {
if (hitResult.type === 'segment' ) activeSegment = hitResult.segment;
else if (hitResult.type === 'handle-in' || hitResult.type === 'handle-out' ) {
activeHandle = hitResult.type; activeSegment = hitResult.segment;
}
}
return ;
}
if (selectedSpots.length === 0 ) {
alert("ドローするスポットをリストから選択するか、「編集」メニューからスポットを追加してください。" );
setMode('select' ); return ;
}
startPoint = event .point;
draftLayer.activate();
currentDraftPath = new paper.Path.Circle({
center: event .point, radius: brushRadius,
fillColor: mode === 'draw' ? 'rgba(200, 200, 200, 0.4)' : 'rgba(255, 68, 68, 0.4)' ,
strokeColor: mode === 'draw' ? '#ffffff' : '#ff0000' , strokeWidth: 1
});
};
tool.onMouseDrag = function(event ) {
if (mode === 'select' ) {
selectedSpots.forEach(spot => spot.position = spot.position.add (event .delta));
updateSelectionBounds(); return ;
}
if (mode === 'adjust' && activeSegment) {
if (activeHandle === 'handle-in' ) activeSegment.handleIn = activeSegment.handleIn.add (event .delta);
else if (activeHandle === 'handle-out' ) activeSegment.handleOut = activeSegment.handleOut.add (event .delta);
else activeSegment.point = activeSegment.point.add (event .delta);
return ;
}
if ((mode === 'draw' || mode === 'erase' ) && currentDraftPath) {
if (event .point.getDistance(startPoint) > brushRadius / 4 ) {
let circle = new paper.Path.Circle({ center: event .point, radius: brushRadius });
let newPath = currentDraftPath.unite(circle);
currentDraftPath.remove (); circle.remove ();
currentDraftPath = newPath;
currentDraftPath.fillColor = mode === 'draw' ? 'rgba(200, 200, 200, 0.4)' : 'rgba(255, 68, 68, 0.4)' ;
currentDraftPath.strokeColor = mode === 'draw' ? '#ffffff' : '#ff0000' ;
currentDraftPath.strokeWidth = 1 ;
startPoint = event .point;
}
}
};
tool.onMouseUp = function(event ) {
if (mode === 'adjust' && activeSegment) {
if (selectedSpots[0 ].data.isRendered) renderFluffySpot(selectedSpots[0 ]);
updateSelectionBounds(); activeSegment = null ; activeHandle = null ; return ;
}
if ((mode === 'draw' || mode === 'erase' ) && currentDraftPath) {
currentDraftPath.simplify(2 );
draftQueue.push({ path: currentDraftPath, mode: mode });
currentDraftPath = null ;
}
};
function selectSpot (spot, isMulti ) {
commitVector();
if (!isMulti) selectedSpots = [];
if (!selectedSpots.includes(spot)) selectedSpots.push(spot);
else if (isMulti) selectedSpots = selectedSpots.filter(s => s !== spot);
propPanel.style.opacity = selectedSpots.length > 0 ? '1' : '0.5' ;
propPanel.style.pointerEvents = selectedSpots.length > 0 ? 'auto' : 'none' ;
updateVisuals();
}
function clearSelection ( ) {
commitVector();
selectedSpots = [];
propPanel.style.opacity = '0.5' ;
propPanel.style.pointerEvents = 'none' ;
updateVisuals();
}
function updateSelectionBounds ( ) {
if (selectionRect) { selectionRect.remove (); selectionRect = null ; }
if (selectedSpots.length === 0 || mode === 'adjust' ) return ;
uiLayer.activate();
let bounds = selectedSpots[0 ].bounds;
for (let i=1 ; i<selectedSpots.length; i++) bounds = bounds.unite(selectedSpots[i].bounds);
selectionRect = new paper.Path.Rectangle(bounds);
selectionRect.strokeColor = '#7289da' ;
selectionRect.strokeWidth = 2 / paper.view.zoom;
selectionRect.dashArray = [4 , 4 ];
}
window.changeZoom = function(factor) {
paper.view.zoom *= factor;
document.getElementById('zoom-text' ).innerText = Math.round(paper.view.zoom * 100 ) + '%' ;
if (selectionRect) updateSelectionBounds();
updateVisuals();
}
window.execAction = function(action) {
commitVector();
if (action === 'addSpot' ) {
spotCounter++;
const spotGroup = new paper.Group({ name: 'Spot_' + Date.now() });
spotGroup.data = { isRendered: false , displayName: 'スポット ' + spotCounter, centerCol: uiCenterCol.value , edgeCol: uiEdgeCol.value , blurAmt: parseInt(uiBlur.value ), depthAmt: parseInt(uiDepth.value ) };
const basePath = new paper.Path({ name: 'basePath' , visible: false });
spotGroup.addChild(basePath);
spotLayer.addChild(spotGroup);
selectSpot(spotGroup, false );
setMode('draw' );
}
else if (action === 'vectorize' ) {
updateVisuals();
}
else if (action === 'render' ) {
if (selectedSpots.length > 0 ) {
selectedSpots.forEach(spot => renderFluffySpot(spot));
setMode('select' );
}
}
else if (action === 'delete' ) {
selectedSpots.forEach(s => s.remove ());
clearSelection();
} else if (action === 'copy' && selectedSpots.length > 0 ) {
clipboard = selectedSpots[0 ].exportJSON();
} else if (action === 'paste' && clipboard) {
spotLayer.activate();
let pasted = new paper.Group();
pasted.importJSON(clipboard);
pasted.name = 'Spot_' + Date.now();
pasted.data.displayName = pasted.data.displayName + ' (コピー)' ;
pasted.position.x += 20 ; pasted.position.y += 20 ;
spotLayer.addChild(pasted);
if (pasted.data.isRendered) renderFluffySpot(pasted);
selectSpot(pasted, false );
} else if (action === 'unite' && selectedSpots.length > 1 ) {
let combinedPath = selectedSpots[0 ].children['basePath' ].clone();
for (let i = 1 ; i < selectedSpots.length; i++) {
let path2 = selectedSpots[i].children['basePath' ].clone();
if (!path2.isEmpty()) {
let temp = combinedPath.isEmpty() ? path2.clone() : combinedPath.unite(path2);
combinedPath.remove ();
path2.remove ();
combinedPath = temp;
} else {
path2.remove ();
}
}
let newData = Object.assign({}, selectedSpots[0 ].data);
selectedSpots.forEach(s => s.remove ());
clearSelection();
spotCounter++;
let newSpot = new paper.Group({ name: 'Spot_' + Date.now() });
newSpot.data = newData;
newSpot.data.displayName = '統合スポット ' + spotCounter;
combinedPath.name = 'basePath' ;
newSpot.addChild(combinedPath);
if (newData.isRendered) renderFluffySpot(newSpot);
spotLayer.addChild(newSpot);
selectSpot(newSpot, false );
}
};
window.fileAction = function(action) {
commitVector();
if (action === 'new' ) {
if (confirm('現在の作業内容は失われます。新規作成しますか?' )) {
spotLayer.removeChildren(); draftLayer.removeChildren();
spotCounter = 0 ; clearSelection();
execAction('addSpot' );
}
} else if (action === 'save' ) {
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(spotLayer.exportJSON());
const a = document.createElement('a' );
a.setAttribute("href" , dataStr);
a.setAttribute("download" , "fluffy_project.json" );
a.click();
} else if (action === 'open' ) {
document.getElementById('fileLoader' ).click();
} else if (action === 'png' ) {
const oldMode = mode;
setMode('select' ); clearSelection();
spotLayer.children.forEach(spot => {
if (spot.name && spot.name.startsWith('Spot_' )) {
if (!spot.data.isRendered) renderFluffySpot(spot);
let base = spot.children['basePath' ];
if (base ) base .visible = false ;
let renderGrp = spot.children['renderGroup' ];
if (renderGrp) { renderGrp.visible = true ; renderGrp.opacity = 1.0 ; }
}
});
paper.view.update();
setTimeout(() => {
const a = document.createElement('a' );
a.href = document.getElementById('myCanvas' ).toDataURL('image/png' );
a.download = 'fluffy_export.png' ;
a.click();
setMode(oldMode);
}, 100 );
} else if (action === 'resize' ) {
const w = prompt('新しい幅を入力' , paper.view.viewSize.width);
const h = prompt('新しい高さを入力' , paper.view.viewSize.height);
if (w && h) { paper.view.viewSize = new paper.Size(parseInt(w), parseInt(h)); }
}
};
document.getElementById('fileLoader' ).addEventListener('change' , function(e) {
const file = e.target.files[0 ];
if (!file) return ;
const reader = new FileReader();
reader.onload = function(evt) {
spotLayer.removeChildren();
spotLayer.importJSON(evt.target.result);
spotLayer.children.forEach(spot => {
if (spot.name && spot.name.startsWith('Spot_' ) && spot.data.isRendered) renderFluffySpot(spot);
});
clearSelection();
};
reader.readAsText(file);
this .value = '' ;
});
document.addEventListener('keydown' , (e) => {
if (['ArrowUp' , 'ArrowDown' , 'ArrowLeft' , 'ArrowRight' ].includes(e.key)) {
e.preventDefault();
const panStep = 30 / paper.view.zoom;
if (e.key === 'ArrowUp' ) paper.view.center.y -= panStep;
if (e.key === 'ArrowDown' ) paper.view.center.y += panStep;
if (e.key === 'ArrowLeft' ) paper.view.center.x -= panStep;
if (e.key === 'ArrowRight' ) paper.view.center.x += panStep;
if (selectionRect) updateSelectionBounds();
return ;
}
if (e.key === 'PageUp' ) { e.preventDefault(); changeZoom(1.2 ); }
if (e.key === 'PageDown' ) { e.preventDefault(); changeZoom(1.1 / 1.2 ); }
if (e.key === 'Delete' ) execAction('delete' );
if (e.ctrlKey || e.metaKey) {
if (e.key.toLowerCase() === 'c' ) execAction('copy' );
if (e.key.toLowerCase() === 'v' ) execAction('paste' );
if (e.key.toLowerCase() === 'j' ) { e.preventDefault(); execAction('unite' ); }
if (e.key.toLowerCase() === 's' ) { e.preventDefault(); fileAction('save' ); }
}
});
execAction('addSpot' );
</script>
</body>
</html>
copy
コメント