Skip to content

Commit 7373829

Browse files
authored
feat(ext/canvas): linear gradients (#86)
* feat(ext/canvas): gradients * fix: linear gradient * feat(ext/canvas): radial gradient * conic gradient * fix: spelling
1 parent fe38825 commit 7373829

File tree

10 files changed

+900
-356
lines changed

10 files changed

+900
-356
lines changed

examples/canvas_gradient.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
const canvas = new OffscreenCanvas(400, 400);
2+
const ctx = canvas.getContext("2d");
3+
4+
if (!ctx) {
5+
console.error("Failed to get 2D context");
6+
throw new Error("Canvas context not available");
7+
}
8+
9+
const linearGradient = ctx.createLinearGradient(0, 0, 200, 200)
10+
linearGradient.addColorStop(0, "red")
11+
linearGradient.addColorStop(1, "blue")
12+
13+
ctx.fillStyle = linearGradient
14+
ctx.fillRect(0, 0, 200, 200)
15+
16+
const radialGradient1 = ctx.createRadialGradient(250, 100, 50, 300, 100, 100)
17+
radialGradient1.addColorStop(0, "red")
18+
radialGradient1.addColorStop(1, "blue")
19+
20+
ctx.fillStyle = radialGradient1
21+
ctx.fillRect(200, 0, 200, 200)
22+
23+
const radialGradient2 = ctx.createRadialGradient(100, 300, 50, 100, 300, 100)
24+
radialGradient2.addColorStop(0, "red")
25+
radialGradient2.addColorStop(1, "blue")
26+
27+
ctx.fillStyle = radialGradient2
28+
ctx.fillRect(0, 200, 200, 200)
29+
30+
const conicGradient = ctx.createConicGradient(0, 300, 300)
31+
conicGradient.addColorStop(0, "red")
32+
conicGradient.addColorStop(1, "blue")
33+
34+
ctx.fillStyle = conicGradient
35+
ctx.fillRect(200, 200, 200, 200)
36+
37+
const saved = canvas.saveAsPng("test.demo.png");
38+
console.log(`Canvas save result: ${saved}`);

runtime/src/ext/canvas/context2d.rs

Lines changed: 78 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use super::FillStyle;
77
use super::Rid;
88
use super::renderer::{Point, Rect};
99
use crate::RuntimeMacroTask;
10+
use crate::ext::canvas::renderer::RenderState;
1011
use andromeda_core::HostData;
1112
use nova_vm::ecmascript::types::Number;
1213
use nova_vm::{
@@ -445,8 +446,18 @@ pub fn internal_canvas_clear_rect<'gc>(
445446

446447
// TODO: Implement proper clear operation in renderer
447448
// For now, render a transparent rectangle
448-
let transparent_color = [0.0, 0.0, 0.0, 0.0]; // Transparent black
449-
renderer.render_rect(clear_rect, transparent_color);
449+
renderer.render_rect(
450+
clear_rect,
451+
&RenderState {
452+
fill_style: FillStyle::Color {
453+
r: 0.0,
454+
g: 0.0,
455+
b: 0.0,
456+
a: 0.0,
457+
},
458+
global_alpha: 1.0,
459+
},
460+
);
450461
} else {
451462
// Fallback to command buffering if no renderer
452463
let mut data = res.canvases.get_mut(rid).unwrap();
@@ -541,13 +552,13 @@ pub fn internal_canvas_fill_rect<'gc>(
541552

542553
// Get the current fill style color
543554
let data = res.canvases.get(rid).unwrap();
544-
// Apply globalAlpha to fill color
545-
let color = match &data.fill_style {
546-
super::FillStyle::Color { r, g, b, a } => [*r, *g, *b, *a * data.global_alpha],
547-
_ => [0.0, 0.0, 0.0, data.global_alpha], // Default black with alpha
548-
};
549-
550-
res.renderers.get_mut(rid).unwrap().render_rect(rect, color);
555+
res.renderers.get_mut(rid).unwrap().render_rect(
556+
rect,
557+
&RenderState {
558+
fill_style: data.fill_style,
559+
global_alpha: data.global_alpha,
560+
},
561+
);
551562
} else {
552563
// Fallback to command storage if no renderer
553564
let mut data = res.canvases.get_mut(rid).unwrap();
@@ -670,16 +681,16 @@ pub fn internal_canvas_fill<'gc>(
670681
if let Some(mut renderer) = res.renderers.get_mut(rid) {
671682
let data = res.canvases.get(rid).unwrap();
672683

684+
// data;
685+
673686
if data.current_path.len() >= 3 {
674-
// Get the current fill style color
675-
// Apply globalAlpha to fill color
676-
let color = match &data.fill_style {
677-
super::FillStyle::Color { r, g, b, a } => [*r, *g, *b, *a * data.global_alpha],
678-
_ => [0.0, 0.0, 0.0, data.global_alpha], // Default black with alpha
679-
};
680-
681-
// Render the polygon using the GPU renderer
682-
renderer.render_polygon(data.current_path.clone(), color);
687+
renderer.render_polygon(
688+
data.current_path.clone(),
689+
&RenderState {
690+
fill_style: data.fill_style,
691+
global_alpha: data.global_alpha,
692+
},
693+
);
683694
}
684695
} else {
685696
// Fallback to command storage if no renderer
@@ -712,18 +723,17 @@ pub fn internal_canvas_stroke<'gc>(
712723
let data = res.canvases.get(rid).unwrap();
713724

714725
if data.current_path.len() >= 2 {
715-
// Get the current stroke style color
716-
// Apply globalAlpha to stroke color
717-
let color = match &data.stroke_style {
718-
super::FillStyle::Color { r, g, b, a } => [*r, *g, *b, *a * data.global_alpha],
719-
_ => [0.0, 0.0, 0.0, data.global_alpha], // Default black with alpha
720-
};
721-
722726
// Convert path to stroke polygon using line width
723727
let stroke_path = generate_stroke_path(&data.current_path, data.line_width);
724728

725729
// Render the stroke as a polygon using the GPU renderer
726-
renderer.render_polygon(stroke_path, color);
730+
renderer.render_polygon(
731+
stroke_path,
732+
&RenderState {
733+
fill_style: data.stroke_style,
734+
global_alpha: data.global_alpha,
735+
},
736+
);
727737
}
728738
} else {
729739
// Fallback to command storage if no renderer
@@ -1305,24 +1315,25 @@ pub fn process_all_commands<'gc>(
13051315
}
13061316
CanvasCommand::Fill => {
13071317
if !current_path.is_empty() {
1308-
let color = match fill_style {
1309-
crate::ext::canvas::FillStyle::Color { r, g, b, a } => {
1310-
[*r, *g, *b, *a * global_alpha]
1311-
}
1312-
_ => [0.0, 0.0, 0.0, global_alpha], // Default to black
1313-
};
1314-
renderer.render_polygon(current_path.clone(), color);
1318+
renderer.render_polygon(
1319+
current_path.clone(),
1320+
&RenderState {
1321+
fill_style: fill_style.clone(),
1322+
global_alpha,
1323+
},
1324+
);
13151325
}
13161326
}
13171327
CanvasCommand::Stroke => {
13181328
if !current_path.is_empty() {
1319-
let color = match stroke_style {
1320-
crate::ext::canvas::FillStyle::Color { r, g, b, a } => {
1321-
[*r, *g, *b, *a * global_alpha]
1322-
}
1323-
_ => [0.0, 0.0, 0.0, global_alpha], // Default to black
1324-
};
1325-
renderer.render_polyline(current_path.clone(), color, line_width);
1329+
renderer.render_polyline(
1330+
current_path.clone(),
1331+
&RenderState {
1332+
fill_style: stroke_style.clone(),
1333+
global_alpha,
1334+
},
1335+
line_width,
1336+
);
13261337
}
13271338
}
13281339
CanvasCommand::FillRect {
@@ -1336,21 +1347,20 @@ pub fn process_all_commands<'gc>(
13361347
let width_f64 = width.into_f64(agent);
13371348
let height_f64 = height.into_f64(agent);
13381349

1339-
let color = match fill_style {
1340-
crate::ext::canvas::FillStyle::Color { r, g, b, a } => {
1341-
[*r, *g, *b, *a * global_alpha]
1342-
}
1343-
_ => [0.0, 0.0, 0.0, global_alpha], // Default to black
1344-
};
1345-
13461350
let rect = crate::ext::canvas::renderer::Rect {
13471351
start: crate::ext::canvas::renderer::Point { x: x_f64, y: y_f64 },
13481352
end: crate::ext::canvas::renderer::Point {
13491353
x: x_f64 + width_f64,
13501354
y: y_f64 + height_f64,
13511355
},
13521356
};
1353-
renderer.render_rect(rect, color);
1357+
renderer.render_rect(
1358+
rect,
1359+
&RenderState {
1360+
fill_style: fill_style.clone(),
1361+
global_alpha,
1362+
},
1363+
);
13541364
}
13551365
CanvasCommand::StrokeRect {
13561366
x,
@@ -1363,13 +1373,6 @@ pub fn process_all_commands<'gc>(
13631373
let width_f64 = width.into_f64(agent);
13641374
let height_f64 = height.into_f64(agent);
13651375

1366-
let color = match stroke_style {
1367-
crate::ext::canvas::FillStyle::Color { r, g, b, a } => {
1368-
[*r, *g, *b, *a * global_alpha]
1369-
}
1370-
_ => [0.0, 0.0, 0.0, global_alpha], // Default to black
1371-
};
1372-
13731376
// Create rectangle outline as polyline
13741377
let rect_path = vec![
13751378
crate::ext::canvas::renderer::Point { x: x_f64, y: y_f64 },
@@ -1387,7 +1390,14 @@ pub fn process_all_commands<'gc>(
13871390
},
13881391
crate::ext::canvas::renderer::Point { x: x_f64, y: y_f64 }, // Close the rectangle
13891392
];
1390-
renderer.render_polyline(rect_path, color, line_width);
1393+
renderer.render_polyline(
1394+
rect_path,
1395+
&RenderState {
1396+
fill_style: stroke_style.clone(),
1397+
global_alpha,
1398+
},
1399+
line_width,
1400+
);
13911401
}
13921402
CanvasCommand::ClearRect {
13931403
x,
@@ -1408,7 +1418,18 @@ pub fn process_all_commands<'gc>(
14081418
y: y_f64 + height_f64,
14091419
},
14101420
};
1411-
renderer.render_rect(rect, [1.0, 1.0, 1.0, 1.0]); // White background
1421+
renderer.render_rect(
1422+
rect,
1423+
&RenderState {
1424+
fill_style: FillStyle::Color {
1425+
r: 1.0,
1426+
g: 1.0,
1427+
b: 1.0,
1428+
a: 1.0,
1429+
},
1430+
global_alpha: 1.0,
1431+
},
1432+
); // White background
14121433
}
14131434
// Handle other commands that don't directly affect rendering
14141435
CanvasCommand::Arc { .. }

runtime/src/ext/canvas/fill_style.rs

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,51 @@
22
// License, v. 2.0. If a copy of the MPL was not distributed with this
33
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
44

5+
use crate::ext::canvas::renderer::{ColorStop, Coordinate};
6+
57
/// Represents different fill styles for Canvas 2D operations
68
#[derive(Clone, Debug)]
79
pub enum FillStyle {
810
/// Solid color specified as RGBA values (0.0-1.0)
9-
Color { r: f32, g: f32, b: f32, a: f32 },
10-
/// Linear gradient (placeholder for future implementation)
11-
LinearGradient,
12-
/// Radial gradient (placeholder for future implementation)
13-
RadialGradient,
11+
Color {
12+
r: f32,
13+
g: f32,
14+
b: f32,
15+
a: f32,
16+
},
17+
LinearGradient(LinearGradient),
18+
RadialGradient(RadialGradient),
19+
ConicGradient(ConicGradient),
1420
/// Pattern (placeholder for future implementation)
1521
Pattern,
1622
}
1723

24+
#[derive(Clone, Debug)]
25+
pub struct LinearGradient {
26+
pub start: Coordinate,
27+
pub end: Coordinate,
28+
pub color_stops: Vec<ColorStop>,
29+
pub rid: u32,
30+
}
31+
32+
#[derive(Clone, Debug)]
33+
pub struct RadialGradient {
34+
pub start: Coordinate,
35+
pub end: Coordinate,
36+
pub start_radius: f32,
37+
pub end_radius: f32,
38+
pub color_stops: Vec<ColorStop>,
39+
pub rid: u32,
40+
}
41+
42+
#[derive(Clone, Debug)]
43+
pub struct ConicGradient {
44+
pub center: Coordinate,
45+
pub start_angle: f32,
46+
pub color_stops: Vec<ColorStop>,
47+
pub rid: u32,
48+
}
49+
1850
impl Default for FillStyle {
1951
fn default() -> Self {
2052
// Default to black color

0 commit comments

Comments
 (0)