|
@@ -29,6 +29,27 @@ const Mode = { |
|
|
type Mode = (typeof Mode)[keyof typeof Mode] |
|
|
type Mode = (typeof Mode)[keyof typeof Mode] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const isLayer = (obj: unknown): obj is Layer => ( |
|
|
|
|
|
typeof obj === 'object' |
|
|
|
|
|
&& obj !== null |
|
|
|
|
|
&& typeof (obj as Layer).id === 'string' |
|
|
|
|
|
&& typeof (obj as Layer).name === 'string' |
|
|
|
|
|
&& Array.isArray ((obj as Layer).lines) |
|
|
|
|
|
&& (obj as Layer).lines.every (isLine) |
|
|
|
|
|
&& Array.isArray ((obj as Layer).history) |
|
|
|
|
|
&& Array.isArray ((obj as Layer).future)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const isLine = (obj: unknown): obj is Line => ( |
|
|
|
|
|
typeof obj === 'object' |
|
|
|
|
|
&& obj !== null |
|
|
|
|
|
&& Array.isArray ((obj as Line).points) |
|
|
|
|
|
&& (obj as Line).points.every (n => typeof n === 'number') |
|
|
|
|
|
&& typeof (obj as Line).stroke === 'string' |
|
|
|
|
|
&& typeof (obj as Line).strokeWidth === 'number' |
|
|
|
|
|
&& Object.values (Mode).includes ((obj as Line).mode)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export default () => { |
|
|
export default () => { |
|
|
const drawingRef = useRef (false) |
|
|
const drawingRef = useRef (false) |
|
|
const fileInputRef = useRef<HTMLInputElement> (null) |
|
|
const fileInputRef = useRef<HTMLInputElement> (null) |
|
@@ -84,19 +105,21 @@ export default () => { |
|
|
const handleUndo = () => { |
|
|
const handleUndo = () => { |
|
|
if (!(activeLayer) || activeLayer.lines.length === 0) |
|
|
if (!(activeLayer) || activeLayer.lines.length === 0) |
|
|
return |
|
|
return |
|
|
const newLines = [...activeLayer.lines] |
|
|
|
|
|
newLines.pop() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const prev = activeLayer.lines.slice (0, -1) |
|
|
|
|
|
const popped = activeLayer.lines[activeLayer.lines.length - 1] |
|
|
updateActiveLayerHistory ([...activeLayer.history, activeLayer.lines]) |
|
|
updateActiveLayerHistory ([...activeLayer.history, activeLayer.lines]) |
|
|
updateActiveLayerFuture ([]) |
|
|
|
|
|
updateActiveLayerLines (newLines) |
|
|
|
|
|
|
|
|
updateActiveLayerFuture ([popped, ...activeLayer.future]) |
|
|
|
|
|
updateActiveLayerLines (prev) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const handleRedo = () => { |
|
|
const handleRedo = () => { |
|
|
if (!(activeLayer) || activeLayer.history.length === 0) |
|
|
|
|
|
|
|
|
if (!(activeLayer) || activeLayer.future.length === 0) |
|
|
return |
|
|
return |
|
|
const prev = activeLayer.history[activeLayer.history.length - 1] |
|
|
|
|
|
updateActiveLayerLines (prev) |
|
|
|
|
|
updateActiveLayerHistory (activeLayer.history.slice (0, -1)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const [redoStroke, ...rest] = activeLayer.future |
|
|
|
|
|
updateActiveLayerLines ([...activeLayer.lines, redoStroke]) |
|
|
|
|
|
updateActiveLayerFuture (rest) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const handleImportImage: ChangeEventHandler<HTMLInputElement> = ev => { |
|
|
const handleImportImage: ChangeEventHandler<HTMLInputElement> = ev => { |
|
@@ -152,19 +175,25 @@ export default () => { |
|
|
return layer |
|
|
return layer |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const canMoveLayer = (direction: number) => { |
|
|
|
|
|
const idx = layers.findIndex (l => l.id === activeLayerId) |
|
|
|
|
|
if (idx < 0) |
|
|
|
|
|
return false |
|
|
|
|
|
|
|
|
|
|
|
const target = idx + direction |
|
|
|
|
|
return 0 <= target && target < layers.length |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
const moveLayer = (direction: number) => { |
|
|
const moveLayer = (direction: number) => { |
|
|
setLayers(prev => { |
|
|
|
|
|
const idx = prev.findIndex (l => l.id === activeLayerId) |
|
|
|
|
|
if (idx < 0) |
|
|
|
|
|
return prev |
|
|
|
|
|
const target = idx + direction |
|
|
|
|
|
if (target < 0 || target >= prev.length) |
|
|
|
|
|
return prev |
|
|
|
|
|
const next = [...prev] |
|
|
|
|
|
const [item] = next.splice(idx, 1) |
|
|
|
|
|
next.splice(target, 0, item) |
|
|
|
|
|
return next |
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
if (!(canMoveLayer (direction))) |
|
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
|
|
const idx = layers.findIndex (l => l.id === activeLayerId) |
|
|
|
|
|
const target = idx + direction |
|
|
|
|
|
const next = [...layers] |
|
|
|
|
|
const [item] = next.splice (idx, 1) |
|
|
|
|
|
next.splice (target, 0, item) |
|
|
|
|
|
setLayers(next) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const clearCanvas = () => { |
|
|
const clearCanvas = () => { |
|
@@ -183,16 +212,25 @@ export default () => { |
|
|
useEffect (() => { |
|
|
useEffect (() => { |
|
|
try |
|
|
try |
|
|
{ |
|
|
{ |
|
|
const paintJSON = localStorage.getItem ('paint') |
|
|
|
|
|
if (paintJSON == null) |
|
|
|
|
|
|
|
|
const raw = localStorage.getItem ('paint') |
|
|
|
|
|
if (!(raw)) |
|
|
throw new Error |
|
|
throw new Error |
|
|
|
|
|
|
|
|
const paint = JSON.parse (paintJSON) |
|
|
|
|
|
if (!(paint instanceof Array)) |
|
|
|
|
|
|
|
|
const parsed: unknown = JSON.parse (raw) |
|
|
|
|
|
if (!(Array.isArray (parsed))) |
|
|
throw new Error |
|
|
throw new Error |
|
|
|
|
|
|
|
|
setLayers (paint) |
|
|
|
|
|
setActiveLayerId (paint[0].id) |
|
|
|
|
|
|
|
|
const restored: Layer[] = parsed.filter (isLayer).map (l => ({ |
|
|
|
|
|
...l, lines: l.lines.map (s => ({ |
|
|
|
|
|
...s, mode: (Object.values (Mode).includes (s.mode) |
|
|
|
|
|
? s.mode |
|
|
|
|
|
: Mode.Pen) })) })) |
|
|
|
|
|
if (restored.length === 0) |
|
|
|
|
|
throw new Error |
|
|
|
|
|
|
|
|
|
|
|
setLayers (restored) |
|
|
|
|
|
setActiveLayerId (restored[0].id) |
|
|
|
|
|
setLayerCnt (restored.length) |
|
|
} |
|
|
} |
|
|
catch |
|
|
catch |
|
|
{ |
|
|
{ |
|
@@ -207,6 +245,7 @@ export default () => { |
|
|
if (layer.image && !(layer.id in images)) |
|
|
if (layer.image && !(layer.id in images)) |
|
|
{ |
|
|
{ |
|
|
const image = new window.Image |
|
|
const image = new window.Image |
|
|
|
|
|
image.crossOrigin = 'anonymous' |
|
|
image.src = layer.image.src |
|
|
image.src = layer.image.src |
|
|
image.onload = () => { |
|
|
image.onload = () => { |
|
|
setImages (prev => ({ ...prev, [layer.id]: image })) |
|
|
setImages (prev => ({ ...prev, [layer.id]: image })) |
|
@@ -256,7 +295,7 @@ export default () => { |
|
|
|
|
|
|
|
|
<div> |
|
|
<div> |
|
|
<label> |
|
|
<label> |
|
|
幅: |
|
|
|
|
|
|
|
|
幅({stageWidth} px): |
|
|
<input type="range" |
|
|
<input type="range" |
|
|
min={32} |
|
|
min={32} |
|
|
max={640} |
|
|
max={640} |
|
@@ -264,7 +303,7 @@ export default () => { |
|
|
onChange={ev => setStageWidth (+ev.target.value)} /> |
|
|
onChange={ev => setStageWidth (+ev.target.value)} /> |
|
|
</label> |
|
|
</label> |
|
|
<label> |
|
|
<label> |
|
|
高さ: |
|
|
|
|
|
|
|
|
高さ({stageHeight} px): |
|
|
<input type="range" |
|
|
<input type="range" |
|
|
min={24} |
|
|
min={24} |
|
|
max={480} |
|
|
max={480} |
|
@@ -295,10 +334,12 @@ export default () => { |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div> |
|
|
<div> |
|
|
<button onClick={handleUndo} disabled={activeLayer?.lines.length === 0}> |
|
|
|
|
|
|
|
|
<button onClick={handleUndo} |
|
|
|
|
|
disabled={!(activeLayer) || activeLayer.lines.length === 0}> |
|
|
<FaUndo /> |
|
|
<FaUndo /> |
|
|
</button> |
|
|
</button> |
|
|
<button onClick={handleRedo} disabled={activeLayer?.history.length === 0}> |
|
|
|
|
|
|
|
|
<button onClick={handleRedo} |
|
|
|
|
|
disabled={!(activeLayer) || activeLayer.future.length === 0}> |
|
|
<FaRedo /> |
|
|
<FaRedo /> |
|
|
</button> |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
@@ -331,8 +372,8 @@ export default () => { |
|
|
onChange={() => setActiveLayerId (layer.id)} /> |
|
|
onChange={() => setActiveLayerId (layer.id)} /> |
|
|
{layer.name} |
|
|
{layer.name} |
|
|
</label>))} |
|
|
</label>))} |
|
|
<button onClick={() => { |
|
|
|
|
|
if (layers.length === 1) |
|
|
|
|
|
|
|
|
<button disabled={layers.length <= 1} onClick={() => { |
|
|
|
|
|
if (layers.length <= 1) |
|
|
{ |
|
|
{ |
|
|
alert ('何を消そうというのだ.') |
|
|
alert ('何を消そうというのだ.') |
|
|
return |
|
|
return |
|
@@ -350,12 +391,12 @@ export default () => { |
|
|
}}> |
|
|
}}> |
|
|
削除 |
|
|
削除 |
|
|
</button> |
|
|
</button> |
|
|
<button onClick={() => { |
|
|
|
|
|
|
|
|
<button disabled={!(canMoveLayer (-1))} onClick={() => { |
|
|
moveLayer (-1) |
|
|
moveLayer (-1) |
|
|
}}> |
|
|
}}> |
|
|
下へ |
|
|
下へ |
|
|
</button> |
|
|
</button> |
|
|
<button onClick={() => { |
|
|
|
|
|
|
|
|
<button disabled={!(canMoveLayer (1))} onClick={() => { |
|
|
moveLayer (1) |
|
|
moveLayer (1) |
|
|
}}> |
|
|
}}> |
|
|
上へ |
|
|
上へ |
|
|