みてるぞ 4 days ago
parent
commit
f1fde867fe
2 changed files with 76 additions and 35 deletions
  1. +1
    -1
      frontend/src/App.tsx
  2. +75
    -34
      frontend/src/components/threads/ThreadCanvas.tsx

+ 1
- 1
frontend/src/App.tsx View File

@@ -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 />} />


+ 75
- 34
frontend/src/components/threads/ThreadCanvas.tsx View File

@@ -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)
}}> }}>
上へ 上へ


Loading…
Cancel
Save