This commit is contained in:
@@ -78,6 +78,7 @@ export default () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
<ScrollToTop />
|
||||||
<div className={cn ('w-screen min-h-screen',
|
<div className={cn ('w-screen min-h-screen',
|
||||||
colours[colourIndex],
|
colours[colourIndex],
|
||||||
'transition-colors duration-[3s] ease-linear')}>
|
'transition-colors duration-[3s] ease-linear')}>
|
||||||
@@ -105,7 +106,6 @@ export default () => {
|
|||||||
</header>
|
</header>
|
||||||
<main className="mb-8">
|
<main className="mb-8">
|
||||||
<Routes>
|
<Routes>
|
||||||
<ScrollToTop />
|
|
||||||
<Route path="/" element={<Navigate to="/threads" replace />} />
|
<Route path="/" element={<Navigate to="/threads" replace />} />
|
||||||
<Route path="/threads" element={<ThreadListPage />} />
|
<Route path="/threads" element={<ThreadListPage />} />
|
||||||
<Route path="/threads/:id" element={<ThreadDetailPage />} />
|
<Route path="/threads/:id" element={<ThreadDetailPage />} />
|
||||||
|
|||||||
@@ -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 ([])
|
updateActiveLayerFuture ([popped, ...activeLayer.future])
|
||||||
updateActiveLayerLines (newLines)
|
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)
|
const [redoStroke, ...rest] = activeLayer.future
|
||||||
updateActiveLayerHistory (activeLayer.history.slice (0, -1))
|
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 moveLayer = (direction: number) => {
|
const canMoveLayer = (direction: number) => {
|
||||||
setLayers(prev => {
|
const idx = layers.findIndex (l => l.id === activeLayerId)
|
||||||
const idx = prev.findIndex (l => l.id === activeLayerId)
|
|
||||||
if (idx < 0)
|
if (idx < 0)
|
||||||
return prev
|
return false
|
||||||
|
|
||||||
const target = idx + direction
|
const target = idx + direction
|
||||||
if (target < 0 || target >= prev.length)
|
return 0 <= target && target < layers.length
|
||||||
return prev
|
}
|
||||||
const next = [...prev]
|
|
||||||
const [item] = next.splice(idx, 1)
|
const moveLayer = (direction: number) => {
|
||||||
next.splice(target, 0, item)
|
if (!(canMoveLayer (direction)))
|
||||||
return next
|
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')
|
const raw = localStorage.getItem ('paint')
|
||||||
if (paintJSON == null)
|
if (!(raw))
|
||||||
throw new Error
|
throw new Error
|
||||||
|
|
||||||
const paint = JSON.parse (paintJSON)
|
const parsed: unknown = JSON.parse (raw)
|
||||||
if (!(paint instanceof Array))
|
if (!(Array.isArray (parsed)))
|
||||||
throw new Error
|
throw new Error
|
||||||
|
|
||||||
setLayers (paint)
|
const restored: Layer[] = parsed.filter (isLayer).map (l => ({
|
||||||
setActiveLayerId (paint[0].id)
|
...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={() => {
|
<button disabled={layers.length <= 1} onClick={() => {
|
||||||
if (layers.length === 1)
|
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)
|
||||||
}}>
|
}}>
|
||||||
上へ
|
上へ
|
||||||
|
|||||||
Reference in New Issue
Block a user