|
@@ -17,11 +17,11 @@ type Layer = { |
|
|
image?: ImageItem } |
|
|
image?: ImageItem } |
|
|
|
|
|
|
|
|
type Line = |
|
|
type Line = |
|
|
| { mode: Mode.Pen | Mode.Rubber |
|
|
|
|
|
|
|
|
| { mode: typeof Mode.Pen | typeof Mode.Rubber |
|
|
points: number[] |
|
|
points: number[] |
|
|
stroke: string |
|
|
stroke: string |
|
|
strokeWidth: number } |
|
|
strokeWidth: number } |
|
|
| { mode: Mode.Paint |
|
|
|
|
|
|
|
|
| { mode: typeof Mode.Paint |
|
|
points: number[] |
|
|
points: number[] |
|
|
stroke: string, |
|
|
stroke: string, |
|
|
imageSrc: string } |
|
|
imageSrc: string } |
|
@@ -29,7 +29,6 @@ type Line = |
|
|
const Mode = { |
|
|
const Mode = { |
|
|
Pen: 'Pen', |
|
|
Pen: 'Pen', |
|
|
Rubber: 'Rubber', |
|
|
Rubber: 'Rubber', |
|
|
Image: 'Image', |
|
|
|
|
|
Paint: 'Paint' } as const |
|
|
Paint: 'Paint' } as const |
|
|
type Mode = (typeof Mode)[keyof typeof Mode] |
|
|
type Mode = (typeof Mode)[keyof typeof Mode] |
|
|
|
|
|
|
|
@@ -51,7 +50,7 @@ const floodFill = ( |
|
|
const start = getRGBA (data, sx, sy, W) |
|
|
const start = getRGBA (data, sx, sy, W) |
|
|
const visited = new Uint8Array (W * H) |
|
|
const visited = new Uint8Array (W * H) |
|
|
|
|
|
|
|
|
if (colourDiffMax (start, [fill.r, flil.g, fill.b, fill.a]) <= tolerance) |
|
|
|
|
|
|
|
|
if (colourDiffMax (start, [fill.r, fill.g, fill.b, fill.a]) <= tolerance) |
|
|
return |
|
|
return |
|
|
|
|
|
|
|
|
const stack: [number, number][] = [[sx, sy]] |
|
|
const stack: [number, number][] = [[sx, sy]] |
|
@@ -99,7 +98,7 @@ const getRGBA = (data: Uint8ClampedArray, x: number, y: number, w: number) => { |
|
|
|
|
|
|
|
|
const hexToRgba = (hex: string, alpha = 255): [number, number, number, number] => { |
|
|
const hexToRgba = (hex: string, alpha = 255): [number, number, number, number] => { |
|
|
const m = /^#?([0-9a-f]{6})$/i.exec (hex) |
|
|
const m = /^#?([0-9a-f]{6})$/i.exec (hex) |
|
|
const n = parseInt (m[1], 16) |
|
|
|
|
|
|
|
|
const n = parseInt (m![1], 16) |
|
|
return [(n >> 16) & 255, (n >> 8) & 255, n & 255, alpha] |
|
|
return [(n >> 16) & 255, (n >> 8) & 255, n & 255, alpha] |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
@@ -110,21 +109,10 @@ const isLayer = (obj: unknown): obj is Layer => ( |
|
|
&& typeof (obj as Layer).id === 'string' |
|
|
&& typeof (obj as Layer).id === 'string' |
|
|
&& typeof (obj as Layer).name === 'string' |
|
|
&& typeof (obj as Layer).name === 'string' |
|
|
&& Array.isArray ((obj as Layer).lines) |
|
|
&& Array.isArray ((obj as Layer).lines) |
|
|
&& (obj as Layer).lines.every (isLine) |
|
|
|
|
|
&& Array.isArray ((obj as Layer).history) |
|
|
&& Array.isArray ((obj as Layer).history) |
|
|
&& Array.isArray ((obj as Layer).future)) |
|
|
&& 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) |
|
@@ -143,7 +131,7 @@ export default () => { |
|
|
const activeLayer = layers.find (l => l.id === activeLayerId) |
|
|
const activeLayer = layers.find (l => l.id === activeLayerId) |
|
|
|
|
|
|
|
|
const handleMouseDown = (ev: Konva.KonvaEventObject<MouseEvent | TouchEvent>) => { |
|
|
const handleMouseDown = (ev: Konva.KonvaEventObject<MouseEvent | TouchEvent>) => { |
|
|
if (!(activeLayer)) |
|
|
|
|
|
|
|
|
if (!(activeLayer) || (mode !== Mode.Pen && mode !== Mode.Rubber)) |
|
|
return |
|
|
return |
|
|
|
|
|
|
|
|
drawingRef.current = true |
|
|
drawingRef.current = true |
|
@@ -222,7 +210,7 @@ export default () => { |
|
|
fileInputRef.current.value = '' |
|
|
fileInputRef.current.value = '' |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const handlePaint = async (ev: Konva.KonvaEventObject<MouseEvent | TouchEvent>) => { |
|
|
|
|
|
|
|
|
const handlePaint = async () => { |
|
|
const layer = activeLayer |
|
|
const layer = activeLayer |
|
|
if (!(layer) || !(stageRef.current)) |
|
|
if (!(layer) || !(stageRef.current)) |
|
|
return |
|
|
return |
|
@@ -245,14 +233,13 @@ export default () => { |
|
|
$off.width = stageWidth |
|
|
$off.width = stageWidth |
|
|
$off.height = stageHeight |
|
|
$off.height = stageHeight |
|
|
const ctx = $off.getContext ('2d') |
|
|
const ctx = $off.getContext ('2d') |
|
|
ctx.drawImage (img, 0, 0, stageWidth, stageHeight) |
|
|
|
|
|
|
|
|
ctx!.drawImage (img, 0, 0, stageWidth, stageHeight) |
|
|
|
|
|
|
|
|
const imgData = ctx.getImageData (0, 0, stageWidth, stageHeight) |
|
|
|
|
|
|
|
|
const imgData = ctx!.getImageData (0, 0, stageWidth, stageHeight) |
|
|
const [r, g, b, a] = hexToRgba (colour, 255) |
|
|
const [r, g, b, a] = hexToRgba (colour, 255) |
|
|
floodFill (imgData, Math.floor (pos.x), Math.floor (pos.y), { r, g, b, a }) |
|
|
floodFill (imgData, Math.floor (pos.x), Math.floor (pos.y), { r, g, b, a }) |
|
|
ctx.putImageData (imgData, 0, 0) |
|
|
|
|
|
|
|
|
ctx!.putImageData (imgData, 0, 0) |
|
|
|
|
|
|
|
|
const prevImageSrc = layer.image?.src |
|
|
|
|
|
const nextSrc = $off.toDataURL ('image/png') |
|
|
const nextSrc = $off.toDataURL ('image/png') |
|
|
|
|
|
|
|
|
updateActiveLayerHistory ([...layer.history, layer.lines]) |
|
|
updateActiveLayerHistory ([...layer.history, layer.lines]) |
|
@@ -347,11 +334,7 @@ export default () => { |
|
|
if (!(Array.isArray (parsed))) |
|
|
if (!(Array.isArray (parsed))) |
|
|
throw new Error |
|
|
throw new Error |
|
|
|
|
|
|
|
|
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) })) })) |
|
|
|
|
|
|
|
|
const restored: Layer[] = parsed.filter (isLayer) |
|
|
if (restored.length === 0) |
|
|
if (restored.length === 0) |
|
|
throw new Error |
|
|
throw new Error |
|
|
|
|
|
|
|
|