| @@ -116,6 +116,7 @@ const isLayer = (obj: unknown): obj is Layer => ( | |||||
| export default () => { | export default () => { | ||||
| const drawingRef = useRef (false) | const drawingRef = useRef (false) | ||||
| const fileInputRef = useRef<HTMLInputElement> (null) | const fileInputRef = useRef<HTMLInputElement> (null) | ||||
| const layersRef = useRef<Record<string, Konva.Layer>> ({ }) | |||||
| const stageRef = useRef<Konva.Stage | null> (null) | const stageRef = useRef<Konva.Stage | null> (null) | ||||
| const [activeLayerId, setActiveLayerId] = useState<string | null> (null) | const [activeLayerId, setActiveLayerId] = useState<string | null> (null) | ||||
| @@ -125,6 +126,7 @@ export default () => { | |||||
| const [layers, setLayers] = useState<Layer[]> ([]) | const [layers, setLayers] = useState<Layer[]> ([]) | ||||
| const [mode, setMode] = useState<Mode> (Mode.Pen) | const [mode, setMode] = useState<Mode> (Mode.Pen) | ||||
| const [pointSize, setPointSize] = useState (3) | const [pointSize, setPointSize] = useState (3) | ||||
| const [singleLayer, setSingleLayer] = useState (false) | |||||
| const [stageHeight, setStageHeight] = useState (480) | const [stageHeight, setStageHeight] = useState (480) | ||||
| const [stageWidth, setStageWidth] = useState (480) | const [stageWidth, setStageWidth] = useState (480) | ||||
| @@ -210,37 +212,31 @@ export default () => { | |||||
| fileInputRef.current.value = '' | fileInputRef.current.value = '' | ||||
| } | } | ||||
| const handlePaint = async () => { | |||||
| const handlePaint = (ev: Konva.KonvaEventObject<PointerEvent | MouseEvent | TouchEvent>) => { | |||||
| const layer = activeLayer | const layer = activeLayer | ||||
| if (!(layer) || !(stageRef.current)) | |||||
| if (!(layer) || !(layersRef.current?.[layer.id])) | |||||
| return | return | ||||
| const stage = stageRef.current | |||||
| const pos = stage.getPointerPosition () | |||||
| const pos = ev.target.getStage ()?.getPointerPosition () | |||||
| if (!(pos)) | if (!(pos)) | ||||
| return | return | ||||
| const dataURL = stage.toDataURL ({ mimeType: 'image/png', pixelRatio: 1 }) | |||||
| const stage = layersRef.current[layer.id] | |||||
| if (!(stage)) | |||||
| return | |||||
| const img = new window.Image | |||||
| img.crossOrigin = 'anonymous' | |||||
| await new Promise<void> (res => { | |||||
| img.onload = () => res () | |||||
| img.src = dataURL | |||||
| }) | |||||
| if (drawingRef.current) | |||||
| return | |||||
| drawingRef.current = true | |||||
| const $off = document.createElement ('canvas') | |||||
| $off.width = stageWidth | |||||
| $off.height = stageHeight | |||||
| const ctx = $off.getContext ('2d') | |||||
| ctx!.drawImage (img, 0, 0, stageWidth, stageHeight) | |||||
| const off = stage.toCanvas ({ pixelRatio: 1 }) | |||||
| const ctx = off.getContext ('2d')! | |||||
| const imgData = ctx.getImageData (0, 0, off.width, off.height) | |||||
| 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) | |||||
| const nextSrc = $off.toDataURL ('image/png') | |||||
| ctx.putImageData (imgData, 0, 0) | |||||
| updateActiveLayerHistory ([...layer.history, layer.lines]) | updateActiveLayerHistory ([...layer.history, layer.lines]) | ||||
| updateActiveLayerFuture ([]) | updateActiveLayerFuture ([]) | ||||
| @@ -251,7 +247,9 @@ export default () => { | |||||
| { mode: Mode.Paint, | { mode: Mode.Paint, | ||||
| points: [pos.x, pos.y], | points: [pos.x, pos.y], | ||||
| stroke: colour, | stroke: colour, | ||||
| imageSrc: nextSrc }] }) : l)) | |||||
| imageSrc: off.toDataURL ('image/png') }] }) : l)) | |||||
| drawingRef.current = false | |||||
| } | } | ||||
| const updateActiveLayerLines = (lines: Line[]) => { | const updateActiveLayerLines = (lines: Line[]) => { | ||||
| @@ -537,11 +535,19 @@ export default () => { | |||||
| 追加 | 追加 | ||||
| </button> | </button> | ||||
| </div> | </div> | ||||
| <label> | |||||
| <input type="checkbox" | |||||
| checked={singleLayer} | |||||
| onChange={ev => setSingleLayer (ev.target.checked)} /> | |||||
| レイアを分けて表示 | |||||
| </label> | |||||
| </div> | </div> | ||||
| <div className="w-full flex items-center justify-center mb-16"> | <div className="w-full flex items-center justify-center mb-16"> | ||||
| <div className="border border-black dark:border-white"> | <div className="border border-black dark:border-white"> | ||||
| <Stage ref={node => { | |||||
| <Stage className="touch-none" | |||||
| ref={node => { | |||||
| stageRef.current = node | stageRef.current = node | ||||
| }} | }} | ||||
| width={stageWidth} | width={stageWidth} | ||||
| @@ -561,8 +567,11 @@ export default () => { | |||||
| listening={false} /> | listening={false} /> | ||||
| </Layer> | </Layer> | ||||
| {layers.map (layer => ( | |||||
| <Layer key={layer.id}> | |||||
| {layers.map (layer => (!(singleLayer) || layer.id === activeLayerId) && ( | |||||
| <Layer key={layer.id} ref={node => { | |||||
| if (node) | |||||
| layersRef.current = { ...layersRef.current, [layer.id]: node } | |||||
| }}> | |||||
| {images[layer.id] && ( | {images[layer.id] && ( | ||||
| <Image image={images[layer.id]} | <Image image={images[layer.id]} | ||||
| x={0} | x={0} | ||||
| @@ -587,7 +596,8 @@ export default () => { | |||||
| : 'source-over'} />) | : 'source-over'} />) | ||||
| case Mode.Paint: | case Mode.Paint: | ||||
| return ( | return ( | ||||
| <Image image={images[line.imageSrc]} | |||||
| <Image key={i} | |||||
| image={images[line.imageSrc]} | |||||
| x={0} | x={0} | ||||
| y={0} | y={0} | ||||
| scaleX={1} | scaleX={1} | ||||