This commit is contained in:
@@ -16,19 +16,94 @@ type Layer = {
|
||||
future: Line[][]
|
||||
image?: ImageItem }
|
||||
|
||||
type Line = {
|
||||
mode: Mode
|
||||
type Line =
|
||||
| { mode: Mode.Pen | Mode.Rubber
|
||||
points: number[]
|
||||
stroke: string
|
||||
strokeWidth: number }
|
||||
| { mode: Mode.Paint
|
||||
points: number[]
|
||||
stroke: string,
|
||||
imageSrc: string }
|
||||
|
||||
const Mode = {
|
||||
Pen: 'Pen',
|
||||
Rubber: 'Rubber',
|
||||
Image: 'Image' } as const
|
||||
Image: 'Image',
|
||||
Paint: 'Paint' } as const
|
||||
type Mode = (typeof Mode)[keyof typeof Mode]
|
||||
|
||||
|
||||
const colourDiffMax = (c1: readonly number[], c2: readonly number[]) => (
|
||||
Math.max (...[0, 1, 2, 3].map (i => Math.abs (c1[i] - c2[i]))))
|
||||
|
||||
|
||||
const floodFill = (
|
||||
imgData: ImageData,
|
||||
sx: number,
|
||||
sy: number,
|
||||
fill: { r: number
|
||||
g: number
|
||||
b: number
|
||||
a: number },
|
||||
tolerance: number = 16) => {
|
||||
const { data, width: W, height: H } = imgData
|
||||
const start = getRGBA (data, sx, sy, W)
|
||||
const visited = new Uint8Array (W * H)
|
||||
|
||||
if (colourDiffMax (start, [fill.r, flil.g, fill.b, fill.a]) <= tolerance)
|
||||
return
|
||||
|
||||
const stack: [number, number][] = [[sx, sy]]
|
||||
while (stack.length > 0)
|
||||
{
|
||||
const [x, y] = stack.pop ()!
|
||||
let lx = x
|
||||
while (lx >= 0
|
||||
&& !(visited[y * W + lx])
|
||||
&& colourDiffMax (getRGBA (data, lx, y, W), start) <= tolerance)
|
||||
--lx
|
||||
++lx
|
||||
let rx = x
|
||||
while (rx < W
|
||||
&& !(visited[y * W + rx])
|
||||
&& colourDiffMax (getRGBA (data, rx, y, W), start) <= tolerance)
|
||||
++rx
|
||||
for (let i = lx; i < rx; ++i)
|
||||
{
|
||||
const idx = y * W + i
|
||||
visited[idx] = 1
|
||||
const p = idx * 4
|
||||
data[p] = fill.r
|
||||
data[p + 1] = fill.g
|
||||
data[p + 2] = fill.b
|
||||
data[p + 3] = fill.a
|
||||
if (y > 0
|
||||
&& !(visited[(y - 1) * W + i])
|
||||
&& colourDiffMax (getRGBA (data, i, y - 1, W), start) <= tolerance)
|
||||
stack.push ([i, y - 1])
|
||||
if (y < H - 1
|
||||
&& !(visited[(y + 1) * W + i])
|
||||
&& colourDiffMax (getRGBA (data, i, y + 1, W), start) <= tolerance)
|
||||
stack.push ([i, y + 1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const getRGBA = (data: Uint8ClampedArray, x: number, y: number, w: number) => {
|
||||
const i = (y * w + x) * 4
|
||||
return [data[i], data[i + 1], data[i + 2], data[i + 3]] as const
|
||||
}
|
||||
|
||||
|
||||
const hexToRgba = (hex: string, alpha = 255): [number, number, number, number] => {
|
||||
const m = /^#?([0-9a-f]{6})$/i.exec (hex)
|
||||
const n = parseInt (m[1], 16)
|
||||
return [(n >> 16) & 255, (n >> 8) & 255, n & 255, alpha]
|
||||
}
|
||||
|
||||
|
||||
const isLayer = (obj: unknown): obj is Layer => (
|
||||
typeof obj === 'object'
|
||||
&& obj !== null
|
||||
@@ -147,6 +222,51 @@ export default () => {
|
||||
fileInputRef.current.value = ''
|
||||
}
|
||||
|
||||
const handlePaint = async (ev: Konva.KonvaEventObject<MouseEvent | TouchEvent>) => {
|
||||
const layer = activeLayer
|
||||
if (!(layer) || !(stageRef.current))
|
||||
return
|
||||
|
||||
const stage = stageRef.current
|
||||
const pos = stage.getPointerPosition ()
|
||||
if (!(pos))
|
||||
return
|
||||
|
||||
const dataURL = stage.toDataURL ({ mimeType: 'image/png', pixelRatio: 1 })
|
||||
|
||||
const img = new window.Image
|
||||
img.crossOrigin = 'anonymous'
|
||||
await new Promise<void> (res => {
|
||||
img.onload = () => res ()
|
||||
img.src = dataURL
|
||||
})
|
||||
|
||||
const $off = document.createElement ('canvas')
|
||||
$off.width = stageWidth
|
||||
$off.height = stageHeight
|
||||
const ctx = $off.getContext ('2d')
|
||||
ctx.drawImage (img, 0, 0, stageWidth, stageHeight)
|
||||
|
||||
const imgData = ctx.getImageData (0, 0, stageWidth, stageHeight)
|
||||
const [r, g, b, a] = hexToRgba (colour, 255)
|
||||
floodFill (imgData, Math.floor (pos.x), Math.floor (pos.y), { r, g, b, a })
|
||||
ctx.putImageData (imgData, 0, 0)
|
||||
|
||||
const prevImageSrc = layer.image?.src
|
||||
const nextSrc = $off.toDataURL ('image/png')
|
||||
|
||||
updateActiveLayerHistory ([...layer.history, layer.lines])
|
||||
updateActiveLayerFuture ([])
|
||||
|
||||
setLayers (prev => prev.map (l => l.id === activeLayerId ? ({
|
||||
...l,
|
||||
lines: [...l.lines,
|
||||
{ mode: Mode.Paint,
|
||||
points: [pos.x, pos.y],
|
||||
stroke: colour,
|
||||
imageSrc: nextSrc }] }) : l))
|
||||
}
|
||||
|
||||
const updateActiveLayerLines = (lines: Line[]) => {
|
||||
setLayers (prev => (
|
||||
prev.map (layer => (layer.id === activeLayerId
|
||||
@@ -258,6 +378,18 @@ export default () => {
|
||||
setImages (prev => ({ ...prev, [layer.id]: image }))
|
||||
}
|
||||
}
|
||||
|
||||
layer.lines.forEach (line => {
|
||||
if (line.mode === Mode.Paint && !(line.imageSrc in images))
|
||||
{
|
||||
const image = new window.Image
|
||||
image.crossOrigin = 'anonymous'
|
||||
image.src = line.imageSrc
|
||||
image.onload = () => {
|
||||
setImages (prev => ({ ...prev, [line.imageSrc]: image }))
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}, [layers])
|
||||
|
||||
@@ -330,6 +462,7 @@ export default () => {
|
||||
onChange={() => setMode (Mode.Pen)} />
|
||||
ペン
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="radio"
|
||||
name="mode"
|
||||
@@ -338,6 +471,15 @@ export default () => {
|
||||
onChange={() => setMode (Mode.Rubber)} />
|
||||
消しゴム
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="radio"
|
||||
name="mode"
|
||||
value="paint"
|
||||
checked={mode === Mode.Paint}
|
||||
onChange={() => setMode (Mode.Paint)} />
|
||||
塗りつぶし
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -421,18 +563,19 @@ export default () => {
|
||||
}}
|
||||
width={stageWidth}
|
||||
height={stageHeight}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
onTouchStart={handleMouseDown}
|
||||
onTouchMove={handleMouseMove}
|
||||
onTouchEnd={handleMouseUp}>
|
||||
onMouseDown={mode === Mode.Paint ? handlePaint : handleMouseDown}
|
||||
onMouseMove={mode === Mode.Paint ? undefined : handleMouseMove}
|
||||
onMouseUp={mode === Mode.Paint ? undefined : handleMouseUp}
|
||||
onTouchStart={mode === Mode.Paint ? handlePaint : handleMouseDown}
|
||||
onTouchMove={mode === Mode.Paint ? undefined : handleMouseMove}
|
||||
onTouchEnd={mode === Mode.Paint ? undefined : handleMouseUp}>
|
||||
<Layer>
|
||||
<Rect x={0}
|
||||
y={0}
|
||||
width={stageWidth}
|
||||
height={stageHeight}
|
||||
fill="white" />
|
||||
fill="white"
|
||||
listening={false} />
|
||||
</Layer>
|
||||
|
||||
{layers.map (layer => (
|
||||
@@ -444,7 +587,12 @@ export default () => {
|
||||
scaleX={stageWidth / images[layer.id].width}
|
||||
scaleY={stageHeight / images[layer.id].height} />)}
|
||||
|
||||
{layer.lines.map ((line, i) => (
|
||||
{layer.lines.map ((line, i) => {
|
||||
switch (line.mode)
|
||||
{
|
||||
case Mode.Pen:
|
||||
case Mode.Rubber:
|
||||
return (
|
||||
<Line key={i}
|
||||
points={line.points}
|
||||
stroke={line.mode === Mode.Rubber ? 'black' : line.stroke}
|
||||
@@ -453,7 +601,16 @@ export default () => {
|
||||
lineCap="round"
|
||||
globalCompositeOperation={line.mode === Mode.Rubber
|
||||
? 'destination-out'
|
||||
: 'source-over'} />))}
|
||||
: 'source-over'} />)
|
||||
case Mode.Paint:
|
||||
return (
|
||||
<Image image={images[line.imageSrc]}
|
||||
x={0}
|
||||
y={0}
|
||||
scaleX={1}
|
||||
scaleY={1} />)
|
||||
}
|
||||
})}
|
||||
</Layer>))}
|
||||
</Stage>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user