Skip to content

Commit

Permalink
webgpu test: wip updated paintbrush logic
Browse files Browse the repository at this point in the history
  • Loading branch information
JackDotJS committed Aug 15, 2024
1 parent a9d9acc commit 4422662
Show file tree
Hide file tree
Showing 6 changed files with 433 additions and 196 deletions.
15 changes: 10 additions & 5 deletions src/renderer/src/state/StateController.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { deepEquals } from "../../../common/deepEquals";
import { trackDeep } from "@solid-primitives/deep";
import HistoryController from "./HistoryController";
import { VincentBaseTool } from '@renderer/api/VincentBaseTool';
import * as VincentTools from '../tools';
import './GlobalEventEmitter';

export interface VincentState {
Expand All @@ -27,6 +26,7 @@ export interface VincentState {
gpu: {
adapter: GPUAdapter | null,
device: GPUDevice | null
canvasFormat: GPUTextureFormat | null
},
tools: {
selected: number,
Expand Down Expand Up @@ -56,6 +56,7 @@ export const [ state, setState ] = createStore<VincentState>({
gpu: {
adapter: null,
device: null,
canvasFormat: null
},
tools: {
selected: 0,
Expand Down Expand Up @@ -99,7 +100,7 @@ export const StateController = (props: { children?: JSXElement }): JSXElement =>
window.electron.writeConfig(unwrap(config));
}, readConfig);

// load gpu adapter/device
// load gpu data
const adapter = await navigator.gpu.requestAdapter();
if (adapter == null) {
throw new Error(`could not get gpu adapter`);
Expand All @@ -110,14 +111,18 @@ export const StateController = (props: { children?: JSXElement }): JSXElement =>
throw new Error(`could not get gpu device`);
}

const cf = navigator.gpu.getPreferredCanvasFormat();

setState(`gpu`, `adapter`, adapter);
setState(`gpu`, `device`, device);
setState(`gpu`, `canvasFormat`, cf);

// load built-in tools
const keys = Object.keys(VincentTools);
keys.sort((a, b) => b.localeCompare(a));
const vincentTools = (await import(`../tools`));
const keys = Object.keys(vincentTools);
keys.sort((a, b) => a.localeCompare(b));
for (const key of keys) {
const tool = VincentTools[key].default;
const tool = vincentTools[key].default;
setState(`tools`, `list`, (old) => [...old, tool]);
}

Expand Down
204 changes: 110 additions & 94 deletions src/renderer/src/tools/Paintbrush/Paintbrush.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import getCursorPositionOnCanvas from "@renderer/util/getCursorPositionOnCanvas"
import { createSignal, JSXElement } from "solid-js";
import style from './Paintbrush.module.css';
import { state } from "@renderer/state/StateController";
import { commitCanvasChange } from "@renderer/util/commitCanvasChange";
// import { commitCanvasChange } from "@renderer/util/commitCanvasChange";

import shaderCode from './Paintbrush.wgsl?raw';

class PaintbrushTool extends VincentBaseTool {
drawing = false;
Expand All @@ -14,13 +16,6 @@ class PaintbrushTool extends VincentBaseTool {
lastPosY = 0;
lastSize = 0;

bbox = {
top: 0,
left: 0,
right: 0,
bottom: 0
};

cursorElem!: HTMLDivElement;

getBrushSize;
Expand All @@ -30,6 +25,15 @@ class PaintbrushTool extends VincentBaseTool {
getCursorVisible;
setCursorVisible;

shaderModule: GPUShaderModule;
renderPipeline: GPURenderPipeline;
uniformBufferSize =
4 * 4 + // color
2 * 4 + // scale
2 * 4; // position
uniformBuffer: GPUBuffer;
bindGroup: GPUBindGroup;

constructor() {
super({
name: `paintbrush`,
Expand All @@ -46,39 +50,55 @@ class PaintbrushTool extends VincentBaseTool {
this.setBrushColor = setBrushColor;
this.getCursorVisible = cursorVisible;
this.setCursorVisible = setCursorVisible;
}

_startDrawing(ev: PointerEvent): void {
const selectCtx = state.canvas.selection!.getContext(`2d`);

if (selectCtx == null) {
throw new Error(`could not get main/selection canvas context2d`);
}

const selectData = selectCtx.getImageData(0, 0, state.canvas.selection!.width, state.canvas.selection!.height);

let found = false;
const shaderModule = state.gpu.device!.createShaderModule({
label: `red tri shader`,
code: shaderCode
});

for (const int of selectData.data) {
if (int !== 0) {
console.debug(`data found`);
this.selectionArea = selectData;
found = true;
break;
const pipeline = state.gpu.device!.createRenderPipeline({
label: `red tri pipeline`,
layout: `auto`,
vertex: {
module: shaderModule
},
fragment: {
module: shaderModule,
targets: [
{
format: state.gpu.canvasFormat!
}
]
}
}
});

const uniformBuffer = state.gpu.device!.createBuffer({
label: `paintbrush uniform buffer`,
size: this.uniformBufferSize,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
});

if (!found) {
this.selectionArea = null;
console.debug(`no selection data found`);
}
const bindGroup = state.gpu.device!.createBindGroup({
label: `paintbrush bindgroup`,
layout: pipeline.getBindGroupLayout(0),
entries: [
{
binding: 0,
resource: {
buffer: uniformBuffer
}
}
]
});

this.shaderModule = shaderModule;
this.renderPipeline = pipeline;
this.uniformBuffer = uniformBuffer;
this.bindGroup = bindGroup;
}

_startDrawing(ev: PointerEvent): void {
const curPos = getCursorPositionOnCanvas(ev.pageX, ev.pageY);
this.bbox.top = curPos.y;
this.bbox.left = curPos.x;
this.bbox.right = curPos.x;
this.bbox.bottom = curPos.y;

this.lastPosX = curPos.x;
this.lastPosY = curPos.y;

Expand All @@ -90,36 +110,11 @@ class PaintbrushTool extends VincentBaseTool {
if (!this.drawing) return;
this.drawing = false;

commitCanvasChange();
// commitCanvasChange();
}

async _masktest(): Promise<void> {
if (this.selectionArea == null) return;
const ctxMain = state.canvas.main!.getContext(`2d`);
const ctxCommitted = state.canvas.committed!.getContext(`2d`);

if (ctxMain == null || ctxCommitted == null) {
throw new Error(`could not get canvas context2d`);
}

const canvasData = ctxMain.getImageData(0, 0, state.canvas.main!.width, state.canvas.main!.height);
const committedData = ctxCommitted.getImageData(0, 0, state.canvas.main!.width, state.canvas.main!.height);

for (let i = 0; i < this.selectionArea.data.length; i++) {
if (canvasData.data[i] !== committedData.data[i]) {
const mixMain = Math.min(canvasData.data[i], this.selectionArea.data[i]);
const mixCommitted = Math.min(committedData.data[i], 255 - this.selectionArea.data[i]);
const added = Math.min(Math.max(mixMain, mixCommitted), mixMain + mixCommitted);

//const lerp = canvasData.data[i] + (this.selectionArea.data[i] - canvasData.data[i]) * (this.selectionArea.data[i] / 255);

canvasData.data[i] = added;
}
}

ctxMain.reset();
ctxMain.putImageData(canvasData, 0, 0);
}
// async _masktest(): Promise<void> {
// }

// TODO: use coalesced events
// see: https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/getCoalescedEvents
Expand All @@ -132,6 +127,14 @@ class PaintbrushTool extends VincentBaseTool {
const curPos = getCursorPositionOnCanvas(ev.pageX, ev.pageY);
let curSize = this.getBrushSize();

const col = this.getBrushColor() as string;
const hexVals = col.substring(1).match(/.{2}/g) ?? [];
const rawColors: number[] = [];

for (const hex of hexVals) {
rawColors.push(parseInt(hex, 16) / 255);
}

// console.debug(curPos);

// ev.pressure is always either 0 or 0.5 for other pointer types
Expand All @@ -140,44 +143,57 @@ class PaintbrushTool extends VincentBaseTool {
curSize = ev.pressure * this.getBrushSize();
}

// console.debug(this.drawing);

if (this.drawing) {
const ctx = state.canvas.main!.getContext(`2d`);

const ctx = state.canvas.main!.getContext(`webgpu`);
if (ctx == null) {
throw new Error(`could not get canvas context2d!`);
throw new Error(`could not get webgpu context`);
}

ctx.globalCompositeOperation = `source-over`;

const dx = (this.lastPosX - curPos.x);
const dy = (this.lastPosY - curPos.y);
const steps = Math.hypot(dx, dy);

for (let i = 1; i < steps; i++) {
const stepLengthX = dx * (i / steps);
const stepLengthY = dy * (i / steps);
const aspect = state.canvas.main!.width / state.canvas.main!.height;
const uniformValues = new Float32Array(this.uniformBufferSize / 4);

const mx = this.lastPosX - stepLengthX;
const my = this.lastPosY - stepLengthY;
const ms = ((this.lastSize) - (curSize)) * (1 - (i / steps)) + (curSize);
// set color
uniformValues.set([
rawColors[0],
rawColors[1],
rawColors[2],
1
], 0);

ctx.beginPath();
ctx.fillStyle = this.getBrushColor();
ctx.arc(mx, my, ms / 2, 0, 2 * Math.PI);
ctx.fill();
}
// set scale
const temp = curSize / state.canvas.main!.height;
uniformValues.set([temp / aspect, temp], 4);

// draw end circle
ctx.beginPath();
ctx.fillStyle = this.getBrushColor();
ctx.arc(curPos.x, curPos.y, curSize / 2, 0, 2 * Math.PI);
ctx.fill();

if (this.selectionArea != null) {
this._masktest();
}
// set position
uniformValues.set([curPos.gpuX, curPos.gpuY], 6);

state.gpu.device!.queue.writeBuffer(this.uniformBuffer, 0, uniformValues);

const currentTexture = ctx.getCurrentTexture();

const renderpass: GPURenderPassDescriptor = {
label: `paintbrush renderpass`,
colorAttachments: [
{
view: currentTexture.createView(),
loadOp: `load`,
storeOp: `store`
}
]
};

const encoder = state.gpu.device!.createCommandEncoder({
label: `red tri encoder`
});

const pass = encoder.beginRenderPass(renderpass);
pass.setPipeline(this.renderPipeline);
pass.setBindGroup(0, this.bindGroup);
pass.draw(6);
pass.end();

const cbuffer = encoder.finish();
state.gpu.device!.queue.submit([ cbuffer ]);
}

this.lastPosX = curPos.x;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,25 @@ fn vs(

fn circle(uv: vec2f, radius: f32) -> f32 {
return 1.0 - smoothstep(
1 - (0.02),
1,
radius,
dot(uv, uv)
);
}

@fragment
fn fs(vsOutput: VertexOutput) -> @location(0) vec4f {
if (input.color[3] == 0) { discard; }

let ellipseMask = circle(vsOutput.texcoord, 1.0);

if (ellipseMask == 0) { discard; }

let alpha = input.color[3] * ellipseMask;
return vec4f(input.color[0], input.color[1], input.color[2], alpha);
return vec4f(
input.color[0] * alpha,
input.color[1] * alpha,
input.color[2] * alpha,
alpha
);
}
16 changes: 16 additions & 0 deletions src/renderer/src/tools/PaintbrushOLD/PaintbrushOLD.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.brushCursor {
pointer-events: none;
position: fixed;
aspect-ratio: 1;
border-radius: 100%;
box-sizing: border-box;
transform: translate(-50%, -50%);
backdrop-filter: invert();
mask-image: radial-gradient(closest-side, #000 calc(100% - 1.5px), #FFF 100%);
mask-mode: luminance;
z-index: 9999;
}

.brushCursor:not(.cursorVisible) {
display: none;
}
Loading

0 comments on commit 4422662

Please sign in to comment.