Skip to content

Commit df73326

Browse files
committed
fix: support ellipse, support fill, support curves (#106)
1 parent 0a0531e commit df73326

File tree

16 files changed

+241
-126
lines changed

16 files changed

+241
-126
lines changed

src/WebGPU/getTexture/createCheckedImageData.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
const FAKE_MIPMAPS_COLORS = [
32
'#FF0000',
43
'#FF00C4',
@@ -13,7 +12,7 @@ const FAKE_MIPMAPS_COLORS = [
1312
'#FFBC00',
1413
]
1514

16-
const ctx = document.createElement('canvas').getContext('2d', {willReadFrequently: true})!
15+
const ctx = new OffscreenCanvas(0, 0).getContext('2d', { willReadFrequently: true })!
1716

1817
export default function createCheckedImageData(size: number, index: number): ImageData {
1918
ctx.canvas.width = size
@@ -33,10 +32,9 @@ export default function createCheckedImageData(size: number, index: number): Ima
3332
{ x: 0.25, y: 0.75 },
3433
{ x: 0.75, y: 0.75 },
3534
{ x: 0.75, y: 0.25 },
36-
].forEach(p => {
35+
].forEach((p) => {
3736
ctx.fillText(index.toString(), p.x * size, p.y * size)
3837
})
3938

40-
4139
return ctx.getImageData(0, 0, size, size)
4240
}

src/WebGPU/getTexture/index.ts

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import createCheckedImageData from "./createCheckedImageData"
2-
import generateMipmapsArray from "./generateMimapsArray"
1+
import createCheckedImageData from './createCheckedImageData'
2+
import generateMipmapsArray from './generateMimapsArray'
33

44
interface Options {
55
mips?: boolean
@@ -8,26 +8,26 @@ interface Options {
88
}
99

1010
type TextureSource =
11-
| ImageBitmap
12-
| HTMLVideoElement
13-
| HTMLCanvasElement
14-
| HTMLImageElement
15-
| OffscreenCanvas;
11+
| ImageBitmap
12+
| HTMLVideoElement
13+
| HTMLCanvasElement
14+
| HTMLImageElement
15+
| OffscreenCanvas
1616

1717
const numMipLevels = (...sizes: number[]) => {
1818
const maxSize = Math.max(...sizes)
19-
return 1 + Math.log2(maxSize) | 0
19+
return (1 + Math.log2(maxSize)) | 0
2020
}
2121

2222
export interface TextureSlice {
2323
source: GPUCopyExternalImageSource
2424
fakeMipmaps: boolean
2525
}
2626

27-
function createCheckedMipmap(levels: Array<{ size: number, color: string }>) {
28-
const ctx = document.createElement('canvas').getContext('2d', {willReadFrequently: true})!
27+
function createCheckedMipmap(levels: Array<{ size: number; color: string }>) {
28+
const ctx = new OffscreenCanvas(0, 0).getContext('2d', { willReadFrequently: true })!
2929

30-
return levels.map(({size, color}, i) => {
30+
return levels.map(({ size, color }, i) => {
3131
ctx.canvas.width = size
3232
ctx.canvas.height = size
3333
ctx.fillStyle = i & 1 ? '#000' : '#fff'
@@ -45,24 +45,29 @@ function createCheckedMipmap(levels: Array<{ size: number, color: string }>) {
4545
{ x: 0.25, y: 0.75 },
4646
{ x: 0.75, y: 0.75 },
4747
{ x: 0.75, y: 0.25 },
48-
].forEach(p => {
48+
].forEach((p) => {
4949
ctx.fillText(i.toString(), p.x * size, p.y * size)
5050
})
5151

52-
5352
return ctx.getImageData(0, 0, size, size)
5453
})
5554
}
5655

57-
export function createTextureArray(device: GPUDevice, width: number, height: number, slices: number) {
56+
export function createTextureArray(
57+
device: GPUDevice,
58+
width: number,
59+
height: number,
60+
slices: number
61+
) {
5862
return device.createTexture({
5963
label: '2d array texture',
6064
format: 'rgba8unorm',
6165
mipLevelCount: 1 + Math.log2(2048),
6266
size: [width, height, slices],
63-
usage: GPUTextureUsage.TEXTURE_BINDING |
64-
GPUTextureUsage.COPY_DST |
65-
GPUTextureUsage.RENDER_ATTACHMENT,
67+
usage:
68+
GPUTextureUsage.TEXTURE_BINDING |
69+
GPUTextureUsage.COPY_DST |
70+
GPUTextureUsage.RENDER_ATTACHMENT,
6671
})
6772
}
6873

@@ -79,7 +84,7 @@ export function attachSlice(
7984
device.queue.copyExternalImageToTexture(
8085
{ source },
8186
{ texture: textue2dArray, origin: { z: sliceIndex }, mipLevel: 0 },
82-
{ width, height },
87+
{ width, height }
8388
)
8489

8590
// if (texSlice.fakeMipmaps) {
@@ -102,28 +107,37 @@ export function attachSlice(
102107
// }
103108
}
104109

105-
106-
export function createTextureFromSource(device: GPUDevice, source: TextureSource, options: Options = {}) {
110+
export function createTextureFromSource(
111+
device: GPUDevice,
112+
source: TextureSource,
113+
options: Options = {}
114+
) {
107115
const texture = device.createTexture({
108116
format: 'rgba8unorm',
109117
mipLevelCount: options.mips ? numMipLevels(source.width, source.height) : 1,
110118
size: [source.width, source.height],
111-
usage: GPUTextureUsage.TEXTURE_BINDING |
112-
GPUTextureUsage.COPY_DST |
113-
GPUTextureUsage.RENDER_ATTACHMENT,
119+
usage:
120+
GPUTextureUsage.TEXTURE_BINDING |
121+
GPUTextureUsage.COPY_DST |
122+
GPUTextureUsage.RENDER_ATTACHMENT,
114123
})
115124
copySourceToTexture(device, texture, source, options)
116125
return texture
117126
}
118127

119-
function copySourceToTexture(device: GPUDevice, texture: GPUTexture, source: TextureSource, {flipY, depthOrArrayLayers}: Options = {}) {
120-
128+
function copySourceToTexture(
129+
device: GPUDevice,
130+
texture: GPUTexture,
131+
source: TextureSource,
132+
{ flipY, depthOrArrayLayers }: Options = {}
133+
) {
121134
device.queue.copyExternalImageToTexture(
122-
{ source, flipY, },
123-
{ texture,
135+
{ source, flipY },
136+
{
137+
texture,
124138
// premultipliedAlpha: true
125139
},
126-
{ width: source.width, height: source.height, depthOrArrayLayers },
140+
{ width: source.width, height: source.height, depthOrArrayLayers }
127141
)
128142

129143
// if (texture.mipLevelCount > 1) {
@@ -134,7 +148,10 @@ function copySourceToTexture(device: GPUDevice, texture: GPUTexture, source: Tex
134148
export async function loadImageBitmap(url: string) {
135149
const res = await fetch(url)
136150
const blob = await res.blob()
137-
return await createImageBitmap(blob, { colorSpaceConversion: 'none', premultiplyAlpha: 'premultiply' })
151+
return await createImageBitmap(blob, {
152+
colorSpaceConversion: 'none',
153+
premultiplyAlpha: 'premultiply',
154+
})
138155
}
139156

140157
export async function createTextureFromImage(device: GPUDevice, url: string, options: Options) {

src/WebGPU/programs/drawShape/getProgram.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,6 @@ export default function getDrawShape(
1414
const uniformBufferSize =
1515
(1 /*stroke width*/ + 4 /*stroke color*/ + 4 /*fill color*/ + /*padding*/ 3) * 4
1616

17-
const uniformBuffer = device.createBuffer({
18-
label: 'drawShape uniforms',
19-
size: uniformBufferSize,
20-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
21-
})
22-
2317
const bindGroupLayout = device.createBindGroupLayout({
2418
label: 'drawShape bind group layout',
2519
entries: [
@@ -108,7 +102,13 @@ export default function getDrawShape(
108102
device.queue.writeBuffer(curvesBuffer, 0, curvesDataView)
109103
buffersToDestroy.push(curvesBuffer)
110104

105+
const uniformBuffer = device.createBuffer({
106+
label: 'drawShape uniforms',
107+
size: uniformBufferSize,
108+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
109+
})
111110
device.queue.writeBuffer(uniformBuffer, 0, uniformDataView)
111+
buffersToDestroy.push(uniformBuffer)
112112

113113
passEncoder.setPipeline(renderPipeline)
114114

src/WebGPU/programs/drawShape/shader.wgsl

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
const STRAIGHT_LINE_THRESHOLD = 1e10;
2+
const EPSILON = 1e-10;
23

34
struct Uniforms {
45
stroke_width: f32,
5-
stroke_color: vec4f,
66
fill_color: vec4f,
7+
stroke_color: vec4f,
78
};
89

910
struct CubicBezier {
@@ -305,12 +306,14 @@ fn evaluate_shape(point: vec2f) -> ShapeInfo {
305306

306307
// Check if this is a straight line (handle points have x >= STRAIGHT_LINE_THRESHOLD)
307308
let is_straight_line = curve.p1.x > STRAIGHT_LINE_THRESHOLD && curve.p2.x > STRAIGHT_LINE_THRESHOLD;
308-
309-
var distance: f32;
310-
309+
310+
var distance: f32 = 1e+10;
311+
311312
if (is_straight_line) {
312313
// Handle as straight line from p0 to p3
313-
distance = distance_to_line_segment(point, curve.p0, curve.p3);
314+
if (u.stroke_width > EPSILON) {
315+
distance = distance_to_line_segment(point, curve.p0, curve.p3);
316+
}
314317

315318
// Simple ray casting for line segment
316319
if (ray_crosses_segment(point, curve.p0, curve.p3)) {
@@ -325,9 +328,12 @@ fn evaluate_shape(point: vec2f) -> ShapeInfo {
325328
}
326329
} else {
327330
// Handle as normal cubic Bézier curve
328-
let closest_t = closest_point_on_bezier(point, curve);
329-
let closest_point = bezier_point(curve, closest_t);
330-
distance = length(point - closest_point);
331+
332+
if (u.stroke_width > EPSILON) {
333+
let closest_t = closest_point_on_bezier(point, curve);
334+
let closest_point = bezier_point(curve, closest_t);
335+
distance = length(point - closest_point);
336+
}
331337

332338
// Ray casting for curve
333339
total_crossings += ray_cast_curve_crossing(point, curve);

src/logic/index.d.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ interface BoundingBox {
1717
max_y: number
1818
}
1919

20+
type ShapeProps = Partial<{
21+
fill_color: [number, number, number, number]
22+
stroke_color: [number, number, number, number]
23+
stroke_width: number
24+
}>
25+
2026
type ZigF32Array = { typedArray: Float32Array }
2127
type ZigAssetInput = {
2228
id: number
@@ -80,5 +86,8 @@ declare module '*.zig' {
8086

8187
export const import_icons: (data: number[]) => void
8288

83-
export const add_shape: (lines: Array<Array<[Point, Point, Point, Point]>>) => void
89+
export const add_shape: (
90+
paths: Array<Array<[Point, Point, Point, Point]>>,
91+
props: ShapeProps
92+
) => void
8493
}

src/logic/index.zig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -625,9 +625,9 @@ pub fn stop_drawing_shape() void {
625625
state.selected_asset_id = 0;
626626
}
627627

628-
pub fn add_shape(paths: []const []const [4]Types.Point) !void {
628+
pub fn add_shape(paths: []const []const [4]Types.Point, props: shapes.ShapeProps) !void {
629629
const id = generate_id();
630-
const shape = try shapes.Shape.new_from_points(id, paths, std.heap.page_allocator);
630+
const shape = try shapes.Shape.new_from_points(id, paths, props, std.heap.page_allocator);
631631
try state.assets.put(id, Asset{ .shape = shape });
632632
state.selected_asset_id = id;
633633
}

src/logic/shapes/shapes.zig

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,17 @@ fn getOppositeHandle(control_point: Point, handle: Point) Point {
2424
pub var cache_shape: *const fn (?u32, bounding_box.BoundingBox, DrawVertexOutput, f32, f32) u32 = undefined;
2525
pub var max_texture_size: f32 = 0.0;
2626

27+
pub const ShapeProps = struct {
28+
// f32 instead of u8 because Uniforms in wgsl doesn't support u8 anyway
29+
fill_color: [4]f32 = .{ 0.0, 0.0, 0.0, 1.0 }, // Default fill color (red)
30+
stroke_color: [4]f32 = .{ 0.0, 0.0, 0.0, 1.0 }, // Default stroke color (green)
31+
stroke_width: f32 = 0.0, // Default stroke width
32+
};
33+
2734
pub const Shape = struct {
2835
id: u32,
2936
paths: std.ArrayList(Path),
30-
stroke_width: f32,
37+
props: ShapeProps,
3138
preview_point: ?Point = null, // Optional preview points for rendering
3239
is_handle_preview: bool = false, // Whether to show the preview point as a handle
3340
active_path_index: ?usize = null, // Index of the active path for editing
@@ -46,7 +53,7 @@ pub const Shape = struct {
4653
const shape = Shape{
4754
.id = id,
4855
.paths = paths_list,
49-
.stroke_width = 10.0,
56+
.props = ShapeProps{},
5057
.is_handle_preview = true,
5158
.active_path_index = 0,
5259
};
@@ -57,7 +64,7 @@ pub const Shape = struct {
5764
// Arrays: Use &array to get a slice reference
5865
// Slices: Pass directly (they're already slices)
5966
// ArrayList: Use .items to get the underlying slice
60-
pub fn new_from_points(id: u32, input_paths: []const []const [4]Point, allocator: std.mem.Allocator) !Shape {
67+
pub fn new_from_points(id: u32, input_paths: []const []const [4]Point, props: ShapeProps, allocator: std.mem.Allocator) !Shape {
6168
var paths_list = std.ArrayList(Path).init(allocator);
6269

6370
for (input_paths) |input_path| {
@@ -68,7 +75,7 @@ pub const Shape = struct {
6875
const shape = Shape{
6976
.id = id,
7077
.paths = paths_list,
71-
.stroke_width = 10.0,
78+
.props = props,
7279
.is_handle_preview = false,
7380
.active_path_index = 0,
7481
};
@@ -212,7 +219,7 @@ pub const Shape = struct {
212219
if (curves_buffer.items.len > 0) {
213220
// Shape.prepare_half_straight_lines(curves);
214221

215-
const box = bounding_box.getBoundingBox(curves_buffer.items, self.stroke_width / 2.0);
222+
const box = bounding_box.getBoundingBox(curves_buffer.items, self.props.stroke_width / 2.0);
216223
const box_vertex = [6]Point{
217224
// First triangle
218225
.{ .x = box.min_x, .y = box.min_y }, // bottom-left
@@ -229,9 +236,9 @@ pub const Shape = struct {
229236
.curves = curves_buffer.items, // Transfer ownership directly
230237
.bounding_box = box_vertex,
231238
.uniform = Uniform{
232-
.stroke_width = self.stroke_width,
233-
.fill_color = [4]f32{ 1.0, 0.0, 0.0, 1.0 },
234-
.stroke_color = [4]f32{ 0.0, 1.0, 0.0, 1.0 },
239+
.stroke_width = self.props.stroke_width,
240+
.fill_color = self.props.fill_color,
241+
.stroke_color = self.props.stroke_color,
235242
},
236243
};
237244
} else {
File renamed without changes.

0 commit comments

Comments
 (0)