Spaces:
Runtime error
Runtime error
| <script lang="ts"> | |
| import { nanoid } from 'nanoid'; | |
| import PxBrush from 'px-brush'; | |
| import { onMount } from 'svelte'; | |
| import type { Brush } from '../types'; | |
| import UndoIcon from '$lib/Icons/Undo.svelte'; | |
| import { COLOR_LIST } from '../data'; | |
| import { selectedBrush, selectedImage, currentCanvas, drawingLayers } from '$lib/store'; | |
| let canvas: HTMLCanvasElement; | |
| let brush: HTMLCanvasElement; | |
| let brushCtx: CanvasRenderingContext2D; | |
| let ctx: CanvasRenderingContext2D; | |
| let startPosition: { x: number; y: number } = { x: 0, y: 0 }; | |
| let pxBrush: PxBrush; | |
| $: { | |
| if (brushCtx && $selectedBrush) { | |
| setBrush($selectedBrush); | |
| brush.style.top = `${10 + $selectedBrush.size / 2}px`; | |
| brush.style.left = `${10 + $selectedBrush.size / 2}px`; | |
| } | |
| } | |
| $: { | |
| if ($selectedImage) { | |
| drawImage(ctx, $selectedImage); | |
| $drawingLayers = new Map(); | |
| } | |
| } | |
| onMount(() => { | |
| ctx = canvas.getContext('2d') as CanvasRenderingContext2D; | |
| brushCtx = brush.getContext('2d') as CanvasRenderingContext2D; | |
| window.devicePixelRatio = 1; | |
| pxBrush = new PxBrush(canvas); | |
| canvas.style.height = 'unset'; | |
| canvas.style.width = 'unset'; | |
| $currentCanvas = canvas; | |
| clearCanvas(ctx); | |
| }); | |
| let mouseDown: boolean = false; | |
| let currLayerId: string; | |
| function pointerEnter() { | |
| // brush.hidden = false; | |
| } | |
| function pointerOut() { | |
| brush.style.top = `${10 + $selectedBrush.size / 2}px`; | |
| brush.style.left = `${10 + $selectedBrush.size / 2}px`; | |
| mouseDown = false; | |
| } | |
| function pointerDown(e: MouseEvent) { | |
| mouseDown = true; | |
| startPosition = getPosition(canvas, e); | |
| pxBrush.draw({ | |
| from: startPosition, | |
| to: startPosition, | |
| size: $selectedBrush.size, | |
| color: $selectedBrush.color | |
| }); | |
| currLayerId = nanoid(); | |
| drawingLayers.update((map) => { | |
| map.set(currLayerId, { | |
| brush: $selectedBrush, | |
| points: [{ from: startPosition, to: startPosition }] | |
| }); | |
| return map; | |
| }); | |
| // drawLayers(); | |
| } | |
| function pointerMove(e: MouseEvent) { | |
| const position = getPosition(canvas, e); | |
| brush.style.top = `${e.offsetY}px`; | |
| brush.style.left = `${e.offsetX}px`; | |
| if (!mouseDown) { | |
| return; | |
| } | |
| pxBrush.draw({ | |
| from: startPosition, | |
| to: position, | |
| size: $selectedBrush.size, | |
| color: $selectedBrush.color | |
| }); | |
| drawingLayers.update((map) => { | |
| const currentLayer = map.get(currLayerId); | |
| currentLayer?.points.push({ | |
| from: startPosition, | |
| to: position | |
| }); | |
| return map; | |
| }); | |
| startPosition = position; | |
| // drawLayers(); | |
| } | |
| function getPosition(canvas: HTMLCanvasElement, event: MouseEvent) { | |
| const rect = canvas.getBoundingClientRect(); | |
| return { | |
| x: (event.clientX - rect.left) * (canvas.width / rect.width), | |
| y: (event.clientY - rect.top) * (canvas.height / rect.height) | |
| }; | |
| } | |
| function setBrush(sBrush: Brush) { | |
| const { size, color } = sBrush; | |
| brush.width = size; | |
| brush.height = size; | |
| // brushCtx.clearRect(0, 0, brush.width, brush.height); | |
| // brushCtx.beginPath(); | |
| brushCtx.fillStyle = color; | |
| brushCtx.arc(size / 2, size / 2, size / 2, 0, 2 * Math.PI); | |
| brushCtx.fill(); | |
| } | |
| function clearCanvas(ctx: CanvasRenderingContext2D) { | |
| ctx.fillStyle = '#000000'; | |
| ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); | |
| } | |
| function drawPixels(ctx: CanvasRenderingContext2D) { | |
| COLOR_LIST.forEach((c, i) => { | |
| pxBrush.draw({ | |
| from: { x: i * 2, y: ctx.canvas.height }, | |
| to: { x: i * 2, y: ctx.canvas.height - 3 }, | |
| size: 2, | |
| color: `rgb(${c.color.join(',')})` | |
| }); | |
| }); | |
| } | |
| function drawImage(ctx: CanvasRenderingContext2D, img: HTMLImageElement | HTMLCanvasElement) { | |
| ctx.drawImage(img, 0, 0, ctx.canvas.width, ctx.canvas.height); | |
| } | |
| function rollBack() { | |
| if ($drawingLayers.size <= 0) { | |
| return; | |
| } | |
| // clear current layer | |
| const ids = Array.from($drawingLayers.keys()); | |
| drawingLayers.update((map) => { | |
| map.delete(ids[ids.length - 1]); | |
| return map; | |
| }); | |
| drawLayers(ctx); | |
| // drawPixels(ctx); | |
| } | |
| function drawLayers(ctx: CanvasRenderingContext2D) { | |
| const tempcanvas = document.createElement('canvas'); | |
| tempcanvas.width = 512; | |
| tempcanvas.height = 512; | |
| window.devicePixelRatio = 1; | |
| const pxBrush = new PxBrush(tempcanvas); | |
| clearCanvas(ctx); | |
| if ($selectedImage) { | |
| drawImage(ctx, $selectedImage); | |
| } | |
| Array.from($drawingLayers.values()).forEach((layer) => { | |
| // clear temp canvas | |
| layer.points.forEach((point, i) => { | |
| pxBrush.draw({ | |
| from: point.from, | |
| to: point.to, | |
| size: layer.brush.size, | |
| color: layer.brush.color | |
| }); | |
| }); | |
| }); | |
| requestAnimationFrame(() => { | |
| drawImage(ctx, tempcanvas); | |
| }); | |
| } | |
| </script> | |
| <div> | |
| <div class="relative overflow-clip"> | |
| <canvas | |
| bind:this={canvas} | |
| class="canvas" | |
| width="512" | |
| height="512" | |
| on:touchmove={(e) => e.preventDefault()} | |
| on:pointerenter={pointerEnter} | |
| on:pointerup={pointerOut} | |
| on:pointerleave={pointerOut} | |
| on:pointercancel={pointerOut} | |
| on:pointerout={pointerOut} | |
| on:pointermove={pointerMove} | |
| on:pointerdown={pointerDown} | |
| /> | |
| <canvas bind:this={brush} class="brush" width="10" height="10" /> | |
| <span class="label">{$selectedBrush?.label} </span> | |
| <button | |
| class="absolute bottom-0 left-0 p-3" | |
| on:click|preventDefault={() => rollBack()} | |
| disabled={$drawingLayers.size <= 0}><UndoIcon /></button | |
| > | |
| </div> | |
| </div> | |
| <style lang="postcss" scoped> | |
| .canvas { | |
| @apply max-w-full w-full z-0 border dark:border-gray-300 border-gray-500 aspect-[512/512]; | |
| } | |
| .brush { | |
| @apply z-10 absolute pointer-events-none -translate-x-1/2 -translate-y-1/2; | |
| } | |
| .label { | |
| @apply px-2 text-base z-20 absolute top-0 left-0 pointer-events-none text-white select-none; | |
| color: white; | |
| font-weight: bolder; | |
| -webkit-text-stroke: 1px black; | |
| -webkit-text-fill-color: white; | |
| } | |
| </style> | |