This commit is contained in:
@@ -78,6 +78,7 @@ export default () => {
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<ScrollToTop />
|
||||
<div className={cn ('w-screen min-h-screen',
|
||||
colours[colourIndex],
|
||||
'transition-colors duration-[3s] ease-linear')}>
|
||||
@@ -105,7 +106,6 @@ export default () => {
|
||||
</header>
|
||||
<main className="mb-8">
|
||||
<Routes>
|
||||
<ScrollToTop />
|
||||
<Route path="/" element={<Navigate to="/threads" replace />} />
|
||||
<Route path="/threads" element={<ThreadListPage />} />
|
||||
<Route path="/threads/:id" element={<ThreadDetailPage />} />
|
||||
|
||||
@@ -29,6 +29,27 @@ const 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 () => {
|
||||
const drawingRef = useRef (false)
|
||||
const fileInputRef = useRef<HTMLInputElement> (null)
|
||||
@@ -84,19 +105,21 @@ export default () => {
|
||||
const handleUndo = () => {
|
||||
if (!(activeLayer) || activeLayer.lines.length === 0)
|
||||
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])
|
||||
updateActiveLayerFuture ([])
|
||||
updateActiveLayerLines (newLines)
|
||||
updateActiveLayerFuture ([popped, ...activeLayer.future])
|
||||
updateActiveLayerLines (prev)
|
||||
}
|
||||
|
||||
const handleRedo = () => {
|
||||
if (!(activeLayer) || activeLayer.history.length === 0)
|
||||
if (!(activeLayer) || activeLayer.future.length === 0)
|
||||
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 => {
|
||||
@@ -152,19 +175,25 @@ export default () => {
|
||||
return layer
|
||||
}
|
||||
|
||||
const moveLayer = (direction: number) => {
|
||||
setLayers(prev => {
|
||||
const idx = prev.findIndex (l => l.id === activeLayerId)
|
||||
const canMoveLayer = (direction: number) => {
|
||||
const idx = layers.findIndex (l => l.id === activeLayerId)
|
||||
if (idx < 0)
|
||||
return prev
|
||||
return false
|
||||
|
||||
const target = idx + direction
|
||||
if (target < 0 || target >= prev.length)
|
||||
return prev
|
||||
const next = [...prev]
|
||||
return 0 <= target && target < layers.length
|
||||
}
|
||||
|
||||
const moveLayer = (direction: number) => {
|
||||
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)
|
||||
return next
|
||||
})
|
||||
setLayers(next)
|
||||
}
|
||||
|
||||
const clearCanvas = () => {
|
||||
@@ -183,16 +212,25 @@ export default () => {
|
||||
useEffect (() => {
|
||||
try
|
||||
{
|
||||
const paintJSON = localStorage.getItem ('paint')
|
||||
if (paintJSON == null)
|
||||
const raw = localStorage.getItem ('paint')
|
||||
if (!(raw))
|
||||
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
|
||||
|
||||
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
|
||||
{
|
||||
@@ -207,6 +245,7 @@ export default () => {
|
||||
if (layer.image && !(layer.id in images))
|
||||
{
|
||||
const image = new window.Image
|
||||
image.crossOrigin = 'anonymous'
|
||||
image.src = layer.image.src
|
||||
image.onload = () => {
|
||||
setImages (prev => ({ ...prev, [layer.id]: image }))
|
||||
@@ -256,7 +295,7 @@ export default () => {
|
||||
|
||||
<div>
|
||||
<label>
|
||||
幅:
|
||||
幅({stageWidth} px):
|
||||
<input type="range"
|
||||
min={32}
|
||||
max={640}
|
||||
@@ -264,7 +303,7 @@ export default () => {
|
||||
onChange={ev => setStageWidth (+ev.target.value)} />
|
||||
</label>
|
||||
<label>
|
||||
高さ:
|
||||
高さ({stageHeight} px):
|
||||
<input type="range"
|
||||
min={24}
|
||||
max={480}
|
||||
@@ -295,10 +334,12 @@ export default () => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button onClick={handleUndo} disabled={activeLayer?.lines.length === 0}>
|
||||
<button onClick={handleUndo}
|
||||
disabled={!(activeLayer) || activeLayer.lines.length === 0}>
|
||||
<FaUndo />
|
||||
</button>
|
||||
<button onClick={handleRedo} disabled={activeLayer?.history.length === 0}>
|
||||
<button onClick={handleRedo}
|
||||
disabled={!(activeLayer) || activeLayer.future.length === 0}>
|
||||
<FaRedo />
|
||||
</button>
|
||||
</div>
|
||||
@@ -331,8 +372,8 @@ export default () => {
|
||||
onChange={() => setActiveLayerId (layer.id)} />
|
||||
{layer.name}
|
||||
</label>))}
|
||||
<button onClick={() => {
|
||||
if (layers.length === 1)
|
||||
<button disabled={layers.length <= 1} onClick={() => {
|
||||
if (layers.length <= 1)
|
||||
{
|
||||
alert ('何を消そうというのだ.')
|
||||
return
|
||||
@@ -350,12 +391,12 @@ export default () => {
|
||||
}}>
|
||||
削除
|
||||
</button>
|
||||
<button onClick={() => {
|
||||
<button disabled={!(canMoveLayer (-1))} onClick={() => {
|
||||
moveLayer (-1)
|
||||
}}>
|
||||
下へ
|
||||
</button>
|
||||
<button onClick={() => {
|
||||
<button disabled={!(canMoveLayer (1))} onClick={() => {
|
||||
moveLayer (1)
|
||||
}}>
|
||||
上へ
|
||||
|
||||
Reference in New Issue
Block a user