Skip to content

Commit c71de93

Browse files
committed
feat: Draw shape (#44)
1 parent d4aedee commit c71de93

File tree

7 files changed

+563
-2
lines changed

7 files changed

+563
-2
lines changed

src/WebGPU/pointer.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ export default function initMouseController(
150150

151151
/* panning , supports both scroll and touch, expect Safari */
152152
canvas.addEventListener('wheel', (e) => {
153+
updatePointer(e)
153154
e.preventDefault()
154155
if (mouseMode === MouseMode.Zoom) {
155156
const delta = Math.abs(e.deltaY) > Math.abs(e.deltaX) ? e.deltaY : -e.deltaX
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Calculate real bounding box for a cubic Bézier curve by finding extrema
2+
function calculateCubicBezierRealBounds(p0: Point, p1: Point, p2: Point, p3: Point) {
3+
// Start with endpoints (t=0 and t=1)
4+
let minX = Math.min(p0.x, p3.x)
5+
let maxX = Math.max(p0.x, p3.x)
6+
let minY = Math.min(p0.y, p3.y)
7+
let maxY = Math.max(p0.y, p3.y)
8+
9+
// For cubic Bézier: B(t) = (1-t)³P0 + 3(1-t)²tP1 + 3(1-t)t²P2 + t³P3
10+
// Derivative: B'(t) = 3(1-t)²(P1-P0) + 6(1-t)t(P2-P1) + 3t²(P3-P2)
11+
// Setting B'(t) = 0 gives us extrema
12+
13+
// X component extrema
14+
const a_x = 3 * (p3.x - 3 * p2.x + 3 * p1.x - p0.x)
15+
const b_x = 6 * (p2.x - 2 * p1.x + p0.x)
16+
const c_x = 3 * (p1.x - p0.x)
17+
18+
const extrema_x = solveQuadratic(a_x, b_x, c_x)
19+
for (const t of extrema_x) {
20+
if (t > 0 && t < 1) {
21+
const x = evaluateCubicBezierComponent(t, p0.x, p1.x, p2.x, p3.x)
22+
minX = Math.min(minX, x)
23+
maxX = Math.max(maxX, x)
24+
}
25+
}
26+
27+
// Y component extrema
28+
const a_y = 3 * (p3.y - 3 * p2.y + 3 * p1.y - p0.y)
29+
const b_y = 6 * (p2.y - 2 * p1.y + p0.y)
30+
const c_y = 3 * (p1.y - p0.y)
31+
32+
const extrema_y = solveQuadratic(a_y, b_y, c_y)
33+
for (const t of extrema_y) {
34+
if (t > 0 && t < 1) {
35+
const y = evaluateCubicBezierComponent(t, p0.y, p1.y, p2.y, p3.y)
36+
minY = Math.min(minY, y)
37+
maxY = Math.max(maxY, y)
38+
}
39+
}
40+
41+
return { minX, minY, maxX, maxY }
42+
}
43+
44+
// Solve quadratic equation ax² + bx + c = 0
45+
function solveQuadratic(a: number, b: number, c: number): number[] {
46+
if (Math.abs(a) < 1e-10) {
47+
// Linear equation: bx + c = 0
48+
if (Math.abs(b) < 1e-10) return []
49+
return [-c / b]
50+
}
51+
52+
const discriminant = b * b - 4 * a * c
53+
if (discriminant < 0) return []
54+
if (Math.abs(discriminant) < 1e-10) return [-b / (2 * a)]
55+
56+
const sqrt_d = Math.sqrt(discriminant)
57+
return [(-b + sqrt_d) / (2 * a), (-b - sqrt_d) / (2 * a)]
58+
}
59+
60+
// Evaluate cubic Bézier curve at parameter t for a single component (x or y)
61+
function evaluateCubicBezierComponent(
62+
t: number,
63+
p0: number,
64+
p1: number,
65+
p2: number,
66+
p3: number
67+
): number {
68+
const t2 = t * t
69+
const t3 = t2 * t
70+
const oneMinusT = 1 - t
71+
const oneMinusT2 = oneMinusT * oneMinusT
72+
const oneMinusT3 = oneMinusT2 * oneMinusT
73+
74+
return p0 * oneMinusT3 + 3 * p1 * t * oneMinusT2 + 3 * p2 * t2 * oneMinusT + p3 * t3
75+
}
76+
77+
export default function getBoundingBox(curves: Point[], padding: number): Point[] {
78+
let minX = Infinity,
79+
minY = Infinity,
80+
maxX = -Infinity,
81+
maxY = -Infinity
82+
83+
// Calculate REAL bounding box for cubic Bézier curves
84+
// Assuming curves array contains groups of 4 points (p0, p1, p2, p3) for each cubic Bézier
85+
const numCubicCurves = Math.floor(curves.length / 3)
86+
87+
for (let i = 0; i < numCubicCurves; i++) {
88+
const p0 = curves[i * 3 + 0]
89+
const p1 = curves[i * 3 + 1]
90+
const p2 = curves[i * 3 + 2]
91+
const p3 = curves[i * 3 + 3]
92+
93+
// Calculate real bounding box for this cubic Bézier curve
94+
const bounds = calculateCubicBezierRealBounds(p0, p1, p2, p3)
95+
96+
minX = Math.min(minX, bounds.minX)
97+
minY = Math.min(minY, bounds.minY)
98+
maxX = Math.max(maxX, bounds.maxX)
99+
maxY = Math.max(maxY, bounds.maxY)
100+
}
101+
102+
return [
103+
{ x: minX - padding, y: minY - padding }, // bottom-left
104+
{ x: maxX + padding, y: minY - padding }, // bottom-right
105+
{ x: maxX + padding, y: maxY + padding }, // top-right
106+
{ x: minX - padding, y: maxY + padding }, // top-left
107+
]
108+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import shaderCode from './shader.wgsl'
2+
import getBoundingBox from './getBoundingBox'
3+
4+
export default function getDrawShape(
5+
device: GPUDevice,
6+
presentationFormat: GPUTextureFormat,
7+
canvasMatrixBuffer: GPUBuffer
8+
) {
9+
const shaderModule = device.createShaderModule({
10+
label: 'drawShape shader',
11+
code: shaderCode,
12+
})
13+
14+
const uniformBufferSize =
15+
(1 /*stroke width*/ + 4 /*stroke color*/ + 4 /*fill color*/ + /*padding*/ 3) * 4
16+
17+
const uniformBuffer = device.createBuffer({
18+
label: 'drawShape uniforms',
19+
size: uniformBufferSize,
20+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
21+
})
22+
23+
// Update uniforms
24+
const uniformValues = new Float32Array(uniformBufferSize / 4)
25+
26+
// offsets to the various uniform values in float32 indices
27+
let start = 0
28+
let end = 4 /* 1 of stroke + 3 of padding */
29+
const strokeWidthValue = uniformValues.subarray(start, (start = end))
30+
31+
end += 4
32+
const strokeColorValue = uniformValues.subarray(start, (start = end))
33+
34+
end += 4
35+
const fillColorValue = uniformValues.subarray(start, (start = end))
36+
37+
const bindGroupLayout = device.createBindGroupLayout({
38+
label: 'drawShape bind group layout',
39+
entries: [
40+
{
41+
binding: 0,
42+
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
43+
buffer: { type: 'uniform' },
44+
},
45+
{
46+
binding: 1,
47+
visibility: GPUShaderStage.FRAGMENT,
48+
buffer: { type: 'read-only-storage' },
49+
},
50+
{
51+
binding: 2,
52+
visibility: GPUShaderStage.VERTEX,
53+
buffer: { type: 'uniform' },
54+
},
55+
],
56+
})
57+
58+
const renderPipeline = device.createRenderPipeline({
59+
label: 'drawShape pipeline',
60+
layout: device.createPipelineLayout({
61+
bindGroupLayouts: [bindGroupLayout],
62+
}),
63+
vertex: {
64+
module: shaderModule,
65+
entryPoint: 'vs',
66+
buffers: [
67+
{
68+
arrayStride: 2 * 4, // position (2) + color (4)
69+
attributes: [
70+
{
71+
shaderLocation: 0,
72+
offset: 0,
73+
format: 'float32x2', // position
74+
},
75+
],
76+
},
77+
],
78+
},
79+
fragment: {
80+
module: shaderModule,
81+
entryPoint: 'fs',
82+
targets: [
83+
{
84+
format: presentationFormat,
85+
blend: {
86+
color: {
87+
srcFactor: 'src-alpha',
88+
dstFactor: 'one-minus-src-alpha',
89+
},
90+
alpha: {
91+
srcFactor: 'one',
92+
dstFactor: 'one-minus-src-alpha',
93+
},
94+
},
95+
},
96+
],
97+
},
98+
multisample: {
99+
count: 4,
100+
},
101+
})
102+
103+
return function drawShape(passEncoder: GPURenderPassEncoder, curves: Point[]) {
104+
const strokeWidth = 20
105+
const boundingBox = getBoundingBox(curves, strokeWidth / 2)
106+
107+
// Create curves buffer
108+
const curvesData = new Float32Array(curves.length * 2) // x y per point
109+
110+
for (let i = 0; i < curves.length; i++) {
111+
const point = curves[i]
112+
const offset = i * 2
113+
curvesData[offset + 0] = point.x
114+
curvesData[offset + 1] = point.y
115+
}
116+
117+
// Create vertex buffer
118+
// prettier-ignore
119+
const vertexData = new Float32Array([
120+
boundingBox[0].x, boundingBox[0].y,
121+
boundingBox[1].x, boundingBox[1].y,
122+
boundingBox[2].x, boundingBox[2].y,
123+
boundingBox[2].x, boundingBox[2].y,
124+
boundingBox[3].x, boundingBox[3].y,
125+
boundingBox[0].x, boundingBox[0].y,
126+
])
127+
128+
const vertexBuffer = device.createBuffer({
129+
label: 'drawShape vertex buffer',
130+
size: vertexData.byteLength,
131+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
132+
})
133+
device.queue.writeBuffer(vertexBuffer, 0, vertexData)
134+
135+
const curvesBuffer = device.createBuffer({
136+
label: 'drawShape curves buffer',
137+
size: curvesData.byteLength,
138+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
139+
})
140+
device.queue.writeBuffer(curvesBuffer, 0, curvesData)
141+
142+
strokeWidthValue.set([strokeWidth])
143+
strokeColorValue.set([1, 0, 0, 1]) // Red stroke color
144+
fillColorValue.set([0, 1, 0, 1]) // Green fill color
145+
device.queue.writeBuffer(uniformBuffer, 0, uniformValues)
146+
147+
passEncoder.setPipeline(renderPipeline)
148+
149+
const bindGroup = device.createBindGroup({
150+
label: 'drawShape bind group',
151+
layout: bindGroupLayout,
152+
entries: [
153+
{ binding: 0, resource: { buffer: uniformBuffer } },
154+
{ binding: 1, resource: { buffer: curvesBuffer } },
155+
{ binding: 2, resource: { buffer: canvasMatrixBuffer } },
156+
],
157+
})
158+
159+
passEncoder.setBindGroup(0, bindGroup)
160+
passEncoder.setVertexBuffer(0, vertexBuffer)
161+
passEncoder.draw(6) // Draw quad
162+
}
163+
}

0 commit comments

Comments
 (0)