Compare commits

...

2 Commits

Author SHA1 Message Date
e5dd455d14 Первая растеризация 2026-02-24 21:56:15 +03:00
ef768e9fe7 Первые попытки рисовать 2026-02-24 20:51:40 +03:00
10 changed files with 421 additions and 15 deletions

View File

@@ -3,7 +3,9 @@ const builtin = @import("builtin");
const dvui = @import("dvui"); const dvui = @import("dvui");
const Document = @import("models/Document.zig"); const Document = @import("models/Document.zig");
const RenderEngine = @import("render/RenderEngine.zig").RenderEngine; const RenderEngine = @import("render/RenderEngine.zig").RenderEngine;
const ImageRect = @import("models/basic_models.zig").ImageRect; const basic_models = @import("models/basic_models.zig");
const ImageRect = basic_models.ImageRect;
const ImageSize = basic_models.ImageSize;
const Point2 = @import("models/basic_models.zig").Point2; const Point2 = @import("models/basic_models.zig").Point2;
const Color = dvui.Color; const Color = dvui.Color;
@@ -25,6 +27,8 @@ _zoom: f32 = 1,
_redraw_pending: bool = false, _redraw_pending: bool = false,
_last_redraw_time_ms: i64 = 0, _last_redraw_time_ms: i64 = 0,
cursor_document_point: ?Point2 = null, cursor_document_point: ?Point2 = null,
/// true — рисовать документ (render), false — пример (gradient/squares).
draw_document: bool = true,
pub fn init(allocator: std.mem.Allocator, document: *Document, engine: RenderEngine) Canvas { pub fn init(allocator: std.mem.Allocator, document: *Document, engine: RenderEngine) Canvas {
return .{ return .{
@@ -41,7 +45,7 @@ pub fn deinit(self: *Canvas) void {
} }
} }
pub fn redrawExample(self: *Canvas) !void { fn redraw(self: *Canvas) !void {
const full = self.getZoomedImageSize(); const full = self.getZoomedImageSize();
const vis: ImageRect = self._visible_rect orelse ImageRect{ .x = 0, .y = 0, .w = 0, .h = 0 }; const vis: ImageRect = self._visible_rect orelse ImageRect{ .x = 0, .y = 0, .w = 0, .h = 0 };
@@ -54,7 +58,11 @@ pub fn redrawExample(self: *Canvas) !void {
return; return;
} }
const new_texture = self.render_engine.example(.{ .w = full.w, .h = full.h }, vis) catch null; const canvas_size: ImageSize = .{ .w = full.w, .h = full.h };
const new_texture = if (self.draw_document)
self.render_engine.render(self.document, canvas_size, vis) catch null
else
self.render_engine.example(canvas_size, vis) catch null;
if (new_texture) |tex| { if (new_texture) |tex| {
if (self.texture) |old_tex| { if (self.texture) |old_tex| {
@@ -68,7 +76,7 @@ pub fn redrawExample(self: *Canvas) !void {
pub fn exampleReset(self: *Canvas) !void { pub fn exampleReset(self: *Canvas) !void {
self.render_engine.exampleReset(); self.render_engine.exampleReset();
try self.redrawExample(); try self.redraw();
} }
pub fn setZoom(self: *Canvas, value: f32) void { pub fn setZoom(self: *Canvas, value: f32) void {
@@ -88,14 +96,14 @@ pub fn processPendingRedraw(self: *Canvas) !void {
if (!self._redraw_pending) return; if (!self._redraw_pending) return;
if (self.redraw_throttle_ms == 0) { if (self.redraw_throttle_ms == 0) {
self._redraw_pending = false; self._redraw_pending = false;
try self.redrawExample(); try self.redraw();
return; return;
} }
const now_ms = std.time.milliTimestamp(); const now_ms = std.time.milliTimestamp();
const elapsed: i64 = if (self._last_redraw_time_ms == 0) self.redraw_throttle_ms else now_ms - self._last_redraw_time_ms; const elapsed: i64 = if (self._last_redraw_time_ms == 0) self.redraw_throttle_ms else now_ms - self._last_redraw_time_ms;
if (elapsed < @as(i64, @intCast(self.redraw_throttle_ms))) return; if (elapsed < @as(i64, @intCast(self.redraw_throttle_ms))) return;
self._redraw_pending = false; self._redraw_pending = false;
try self.redrawExample(); try self.redraw();
} }
pub fn getZoomedImageSize(self: Canvas) ImageRect { pub fn getZoomedImageSize(self: Canvas) ImageRect {
@@ -134,9 +142,6 @@ pub fn updateVisibleImageRect(self: *Canvas, viewport: dvui.Rect, scroll_offset:
changed |= next.x != vis.x or next.y != vis.y or next.w != vis.w or next.h != vis.h; changed |= next.x != vis.x or next.y != vis.y or next.w != vis.w or next.h != vis.h;
} }
self._visible_rect = next; self._visible_rect = next;
if (changed) {
std.debug.print("Visible Image Rect: {{ x: {}, y: {}, w: {}, h: {} }}\n", .{ next.x, next.y, next.w, next.h });
}
if (changed or self.texture == null) { if (changed or self.texture == null) {
return true; return true;
} }

View File

@@ -2,7 +2,9 @@ const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const dvui = @import("dvui"); const dvui = @import("dvui");
const RenderEngine = @import("RenderEngine.zig").RenderEngine; const RenderEngine = @import("RenderEngine.zig").RenderEngine;
const Document = @import("../models/Document.zig");
const basic_models = @import("../models/basic_models.zig"); const basic_models = @import("../models/basic_models.zig");
const cpu_draw = @import("cpu/draw.zig");
const ImageSize = basic_models.ImageSize; const ImageSize = basic_models.ImageSize;
const ImageRect = basic_models.ImageRect; const ImageRect = basic_models.ImageRect;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
@@ -171,3 +173,16 @@ pub fn example(self: CpuRenderEngine, canvas_size: ImageSize, visible_rect: Imag
pub fn renderEngine(self: *CpuRenderEngine) RenderEngine { pub fn renderEngine(self: *CpuRenderEngine) RenderEngine {
return .{ .cpu = self }; return .{ .cpu = self };
} }
/// Растеризует документ: фон + рекурсивная отрисовка фигур через конвейер (трансформ, прозрачность, наложение).
pub fn renderDocument(self: *CpuRenderEngine, document: *const Document, canvas_size: ImageSize, visible_rect: ImageRect) !?dvui.Texture {
const width = visible_rect.w;
const height = visible_rect.h;
const pixels = try self._allocator.alloc(Color.PMA, @as(usize, width) * height);
defer self._allocator.free(pixels);
for (pixels) |*p| p.* = .{ .r = 255, .g = 255, .b = 255, .a = 255 };
cpu_draw.drawDocument(pixels, width, height, visible_rect, document, canvas_size);
return try dvui.textureCreate(pixels, width, height, .nearest);
}

View File

@@ -1,5 +1,6 @@
const dvui = @import("dvui"); const dvui = @import("dvui");
const CpuRenderEngine = @import("CpuRenderEngine.zig"); const CpuRenderEngine = @import("CpuRenderEngine.zig");
const Document = @import("../models/Document.zig");
const basic_models = @import("../models/basic_models.zig"); const basic_models = @import("../models/basic_models.zig");
pub const RenderEngine = union(enum) { pub const RenderEngine = union(enum) {
@@ -16,4 +17,11 @@ pub const RenderEngine = union(enum) {
.cpu => |cpu_r| cpu_r.example(canvas_size, visible_rect), .cpu => |cpu_r| cpu_r.example(canvas_size, visible_rect),
}; };
} }
/// Растеризует документ в текстуру (размер и видимая область в пикселях холста).
pub fn render(self: RenderEngine, document: *const Document, canvas_size: basic_models.ImageSize, visible_rect: basic_models.ImageRect) !?dvui.Texture {
return switch (self) {
.cpu => |cpu_r| cpu_r.renderDocument(document, canvas_size, visible_rect),
};
}
}; };

8
src/render/cpu/arc.zig Normal file
View File

@@ -0,0 +1,8 @@
const Document = @import("../../models/Document.zig");
const pipeline = @import("pipeline.zig");
const DrawContext = pipeline.DrawContext;
const Object = Document.Object;
/// Рисует дугу (заглушка: пока не реализовано).
pub fn draw(_: *DrawContext, _: *const Object) void {}

22
src/render/cpu/broken.zig Normal file
View File

@@ -0,0 +1,22 @@
const std = @import("std");
const Document = @import("../../models/Document.zig");
const pipeline = @import("pipeline.zig");
const line = @import("line.zig");
const DrawContext = pipeline.DrawContext;
const Color = @import("dvui").Color;
const Object = Document.Object;
const default_stroke: Color.PMA = .{ .r = 0, .g = 0, .b = 0, .a = 255 };
/// Рисует ломаную по точкам в локальных координатах. Обводка по stroke_rgba.
pub fn draw(ctx: *DrawContext, obj: *const Object) void {
const p_prop = obj.getProperty(.points) orelse return;
const pts = p_prop.points.items;
if (pts.len < 2) return;
const stroke = if (obj.getProperty(.stroke_rgba)) |s| pipeline.rgbaToPma(s.stroke_rgba) else default_stroke;
var i: usize = 0;
while (i + 1 < pts.len) : (i += 1) {
line.drawLine(ctx, pts[i].x, pts[i].y, pts[i + 1].x, pts[i + 1].y, stroke);
}
}

76
src/render/cpu/draw.zig Normal file
View File

@@ -0,0 +1,76 @@
const std = @import("std");
const Document = @import("../../models/Document.zig");
const pipeline = @import("pipeline.zig");
const line = @import("line.zig");
const ellipse = @import("ellipse.zig");
const broken = @import("broken.zig");
const arc = @import("arc.zig");
const basic_models = @import("../../models/basic_models.zig");
const ImageRect = basic_models.ImageRect;
const ImageSize = basic_models.ImageSize;
const Object = Document.Object;
const DrawContext = pipeline.DrawContext;
const Transform = pipeline.Transform;
fn getLocalTransform(obj: *const Object) Transform {
const pos = if (obj.getProperty(.position)) |p| p.position else basic_models.Point2{ .x = 0, .y = 0 };
const angle = if (obj.getProperty(.angle)) |p| p.angle else 0;
const scale = if (obj.getProperty(.scale)) |p| p.scale else basic_models.Scale2{ .scale_x = 1, .scale_y = 1 };
const opacity = if (obj.getProperty(.opacity)) |p| p.opacity else 1.0;
return .{
.position = pos,
.angle = angle,
.scale = scale,
.opacity = opacity,
};
}
fn isVisible(obj: *const Object) bool {
return if (obj.getProperty(.visible)) |p| p.visible else true;
}
fn drawObject(ctx: *DrawContext, obj: *const Object, parent_transform: Transform) void {
if (!isVisible(obj)) return;
const local = getLocalTransform(obj);
const world = Transform.compose(parent_transform, local);
ctx.setTransform(world);
switch (obj.shape) {
.line => line.draw(ctx, obj),
.ellipse => ellipse.draw(ctx, obj),
.broken => broken.draw(ctx, obj),
.arc => arc.draw(ctx, obj),
}
for (obj.children.items) |*child| {
drawObject(ctx, child, world);
}
}
/// Рекурсивно рисует документ в буфер: сначала корневые объекты по порядку, затем их потомков (каждый следующий поверх предыдущего).
pub fn drawDocument(
pixels: []@import("dvui").Color.PMA,
buf_width: u32,
buf_height: u32,
visible_rect: ImageRect,
document: *const Document,
canvas_size: ImageSize,
) void {
const scale_x: f32 = if (document.size.width > 0) @as(f32, @floatFromInt(canvas_size.w)) / document.size.width else 0;
const scale_y: f32 = if (document.size.height > 0) @as(f32, @floatFromInt(canvas_size.h)) / document.size.height else 0;
var ctx = DrawContext{
.pixels = pixels,
.buf_width = buf_width,
.buf_height = buf_height,
.visible_rect = visible_rect,
.scale_x = scale_x,
.scale_y = scale_y,
};
const identity = Transform{};
for (document.objects.items) |*obj| {
drawObject(&ctx, obj, identity);
}
}

View File

@@ -0,0 +1,59 @@
const std = @import("std");
const Document = @import("../../models/Document.zig");
const pipeline = @import("pipeline.zig");
const DrawContext = pipeline.DrawContext;
const Color = @import("dvui").Color;
const Object = Document.Object;
const default_stroke: Color.PMA = .{ .r = 0, .g = 0, .b = 0, .a = 255 };
/// Рисует эллипс: центр (0,0), полуоси radii. Растеризация в координатах буфера (обводка кольцом).
pub fn draw(ctx: *DrawContext, obj: *const Object) void {
const r_prop = obj.getProperty(.radii) orelse return;
const rx = r_prop.radii.x;
const ry = r_prop.radii.y;
if (rx <= 0 or ry <= 0) return;
const stroke = if (obj.getProperty(.stroke_rgba)) |s| pipeline.rgbaToPma(s.stroke_rgba) else default_stroke;
const corners = [_]struct { x: f32, y: f32 }{
.{ .x = -rx, .y = -ry },
.{ .x = rx, .y = -ry },
.{ .x = rx, .y = ry },
.{ .x = -rx, .y = ry },
};
const w0 = ctx.localToWorld(corners[0].x, corners[0].y);
const b0 = ctx.worldToBufferF(w0.x, w0.y);
var min_bx: f32 = b0.x;
var min_by: f32 = b0.y;
var max_bx: f32 = b0.x;
var max_by: f32 = b0.y;
for (corners[1..]) |c| {
const w = ctx.localToWorld(c.x, c.y);
const b = ctx.worldToBufferF(w.x, w.y);
min_bx = @min(min_bx, b.x);
min_by = @min(min_by, b.y);
max_bx = @max(max_bx, b.x);
max_by = @max(max_by, b.y);
}
const buf_w: i32 = @intCast(ctx.buf_width);
const buf_h: i32 = @intCast(ctx.buf_height);
const x0: i32 = @max(0, @as(i32, @intFromFloat(std.math.floor(min_bx))));
const y0: i32 = @max(0, @as(i32, @intFromFloat(std.math.floor(min_by))));
const x1: i32 = @min(buf_w, @as(i32, @intFromFloat(std.math.ceil(max_bx))) + 1);
const y1: i32 = @min(buf_h, @as(i32, @intFromFloat(std.math.ceil(max_by))) + 1);
var by: i32 = y0;
while (by < y1) : (by += 1) {
var bx: i32 = x0;
while (bx < x1) : (bx += 1) {
const w = ctx.bufferToWorld(@as(f32, @floatFromInt(bx)) + 0.5, @as(f32, @floatFromInt(by)) + 0.5);
const loc = ctx.worldToLocal(w.x, w.y);
const nx = loc.x / rx;
const ny = loc.y / ry;
const d = nx * nx + ny * ny;
if (d >= 0.9 and d <= 1.1) {
ctx.blendPixelAtBuffer(@intCast(bx), @intCast(by), stroke);
}
}
}
}

60
src/render/cpu/line.zig Normal file
View File

@@ -0,0 +1,60 @@
const std = @import("std");
const Document = @import("../../models/Document.zig");
const pipeline = @import("pipeline.zig");
const DrawContext = pipeline.DrawContext;
const Color = @import("dvui").Color;
const Object = Document.Object;
const default_stroke: Color.PMA = .{ .r = 0, .g = 0, .b = 0, .a = 255 };
/// Рисует линию в локальных координатах: от (0,0) до end_point. Растеризация в координатах буфера (без пробелов при зуме).
pub fn draw(ctx: *DrawContext, obj: *const Object) void {
const ep_prop = obj.getProperty(.end_point) orelse return;
const end_x = ep_prop.end_point.x;
const end_y = ep_prop.end_point.y;
const stroke = if (obj.getProperty(.stroke_rgba)) |s| pipeline.rgbaToPma(s.stroke_rgba) else default_stroke;
drawLine(ctx, 0, 0, end_x, end_y, stroke);
}
/// Линия по локальным координатам фигуры: переводит концы в буфер и рисует в пикселях буфера.
pub fn drawLine(ctx: *DrawContext, x0: f32, y0: f32, x1: f32, y1: f32, color: Color.PMA) void {
const w0 = ctx.localToWorld(x0, y0);
const w1 = ctx.localToWorld(x1, y1);
const b0 = ctx.worldToBufferF(w0.x, w0.y);
const b1 = ctx.worldToBufferF(w1.x, w1.y);
const bx0: i32 = @intFromFloat(std.math.round(b0.x));
const by0: i32 = @intFromFloat(std.math.round(b0.y));
const bx1: i32 = @intFromFloat(std.math.round(b1.x));
const by1: i32 = @intFromFloat(std.math.round(b1.y));
drawLineInBuffer(ctx, bx0, by0, bx1, by1, color);
}
/// Брезенхем в координатах буфера; пиксели вне [0, buf_width) x [0, buf_height) пропускаются.
pub fn drawLineInBuffer(ctx: *DrawContext, bx0: i32, by0: i32, bx1: i32, by1: i32, color: Color.PMA) void {
const bw: i32 = @intCast(ctx.buf_width);
const bh: i32 = @intCast(ctx.buf_height);
const dx: i32 = @intCast(@abs(bx1 - bx0));
const dy: i32 = -@as(i32, @intCast(@abs(by1 - by0)));
const sx: i32 = if (bx0 < bx1) 1 else -1;
const sy: i32 = if (by0 < by1) 1 else -1;
var err = dx + dy;
var x = bx0;
var y = by0;
while (true) {
if (x >= 0 and x < bw and y >= 0 and y < bh) {
ctx.blendPixelAtBuffer(@intCast(x), @intCast(y), color);
}
if (x == bx1 and y == by1) break;
const e2 = 2 * err;
if (e2 >= dy) {
err += dy;
x += sx;
}
if (e2 <= dx) {
err += dx;
y += sy;
}
}
}

148
src/render/cpu/pipeline.zig Normal file
View File

@@ -0,0 +1,148 @@
const std = @import("std");
const dvui = @import("dvui");
const basic_models = @import("../../models/basic_models.zig");
const Point2 = basic_models.Point2;
const Scale2 = basic_models.Scale2;
const ImageRect = basic_models.ImageRect;
const Color = dvui.Color;
/// Трансформ объекта в мировых координатах документа (позиция, угол, масштаб, непрозрачность).
pub const Transform = struct {
position: Point2 = .{},
angle: f32 = 0,
scale: Scale2 = .{},
opacity: f32 = 1.0,
/// Композиция: мировой трансформ = parent * local (local в пространстве родителя).
pub fn compose(parent: Transform, local: Transform) Transform {
const cos_a = std.math.cos(parent.angle);
const sin_a = std.math.sin(parent.angle);
const sx = parent.scale.scale_x * local.scale.scale_x;
const sy = parent.scale.scale_y * local.scale.scale_y;
const local_px = local.position.x * parent.scale.scale_x;
const local_py = local.position.y * parent.scale.scale_y;
const rx = cos_a * local_px - sin_a * local_py;
const ry = sin_a * local_px + cos_a * local_py;
return .{
.position = .{
.x = parent.position.x + rx,
.y = parent.position.y + ry,
},
.angle = parent.angle + local.angle,
.scale = .{ .scale_x = sx, .scale_y = sy },
.opacity = parent.opacity * local.opacity,
};
}
};
/// Единый конвейер: принимает позицию в локальных координатах фигуры и цвет пикселя,
/// применяет трансформ (вращение, масштаб, перенос) и непрозрачность, накладывает на буфер.
pub const DrawContext = struct {
pixels: []Color.PMA,
buf_width: u32,
buf_height: u32,
visible_rect: ImageRect,
scale_x: f32,
scale_y: f32,
transform: Transform = .{},
pub fn setTransform(self: *DrawContext, t: Transform) void {
self.transform = t;
}
/// Локальные координаты фигуры -> мировые (документ).
pub fn localToWorld(self: *const DrawContext, local_x: f32, local_y: f32) Point2 {
const t = &self.transform;
const cos_a = std.math.cos(t.angle);
const sin_a = std.math.sin(t.angle);
return .{
.x = t.position.x + (local_x * t.scale.scale_x) * cos_a - (local_y * t.scale.scale_y) * sin_a,
.y = t.position.y + (local_x * t.scale.scale_x) * sin_a + (local_y * t.scale.scale_y) * cos_a,
};
}
/// Мировые координаты документа -> координаты в буфере (могут быть вне [0, buf_w] x [0, buf_h]).
pub fn worldToBufferF(self: *const DrawContext, wx: f32, wy: f32) Point2 {
const canvas_x = wx * self.scale_x;
const canvas_y = wy * self.scale_y;
const vx = @as(f32, @floatFromInt(self.visible_rect.x));
const vy = @as(f32, @floatFromInt(self.visible_rect.y));
return .{
.x = canvas_x - vx,
.y = canvas_y - vy,
};
}
/// Координаты буфера -> мировые (документ). scale_x/scale_y не должны быть 0.
pub fn bufferToWorld(self: *const DrawContext, buf_x: f32, buf_y: f32) Point2 {
const vx = @as(f32, @floatFromInt(self.visible_rect.x));
const vy = @as(f32, @floatFromInt(self.visible_rect.y));
const canvas_x = buf_x + vx;
const canvas_y = buf_y + vy;
const sx = if (self.scale_x != 0) self.scale_x else 1.0;
const sy = if (self.scale_y != 0) self.scale_y else 1.0;
return .{
.x = canvas_x / sx,
.y = canvas_y / sy,
};
}
/// Мировые координаты -> локальные фигуры (обратное к localToWorld).
pub fn worldToLocal(self: *const DrawContext, wx: f32, wy: f32) Point2 {
const t = &self.transform;
const dx = wx - t.position.x;
const dy = wy - t.position.y;
const cos_a = std.math.cos(-t.angle);
const sin_a = std.math.sin(-t.angle);
const sx = if (t.scale.scale_x != 0) t.scale.scale_x else 1.0;
const sy = if (t.scale.scale_y != 0) t.scale.scale_y else 1.0;
return .{
.x = (dx * cos_a - dy * sin_a) / sx,
.y = (dx * sin_a + dy * cos_a) / sy,
};
}
/// Смешивает цвет в пикселе буфера (bx, by) с учётом opacity текущего трансформа. Bounds не проверяются.
pub fn blendPixelAtBuffer(self: *DrawContext, bx: u32, by: u32, color: Color.PMA) void {
if (bx >= self.buf_width or by >= self.buf_height) return;
const t = &self.transform;
const idx = by * self.buf_width + bx;
const dst = &self.pixels[idx];
const a = @as(f32, @floatFromInt(color.a)) / 255.0 * t.opacity;
const src_r = @as(f32, @floatFromInt(color.r)) * a;
const src_g = @as(f32, @floatFromInt(color.g)) * a;
const src_b = @as(f32, @floatFromInt(color.b)) * a;
const inv_a = 1.0 - a;
dst.r = @intFromFloat(std.math.clamp(src_r + inv_a * @as(f32, @floatFromInt(dst.r)), 0, 255));
dst.g = @intFromFloat(std.math.clamp(src_g + inv_a * @as(f32, @floatFromInt(dst.g)), 0, 255));
dst.b = @intFromFloat(std.math.clamp(src_b + inv_a * @as(f32, @floatFromInt(dst.b)), 0, 255));
dst.a = @intFromFloat(std.math.clamp(a * 255 + inv_a * @as(f32, @floatFromInt(dst.a)), 0, 255));
}
/// Записывает пиксель в локальных координатах фигуры с учётом трансформа и прозрачности (PMA blend).
pub fn blendPixelLocal(self: *DrawContext, local_x: f32, local_y: f32, color: Color.PMA) void {
const w = self.localToWorld(local_x, local_y);
const b = self.worldToBufferF(w.x, w.y);
const bx: i32 = @intFromFloat(b.x);
const by: i32 = @intFromFloat(b.y);
const vw = @as(i32, @intCast(self.visible_rect.w));
const vh = @as(i32, @intCast(self.visible_rect.h));
if (bx < 0 or bx >= vw or by < 0 or by >= vh) return;
self.blendPixelAtBuffer(@intCast(bx), @intCast(by), color);
}
};
pub fn rgbaToPma(rgba: u32) Color.PMA {
const r: u8 = @intCast((rgba >> 0) & 0xFF);
const g: u8 = @intCast((rgba >> 8) & 0xFF);
const b: u8 = @intCast((rgba >> 16) & 0xFF);
const a: u8 = @intCast((rgba >> 24) & 0xFF);
if (a == 0) return .{ .r = 0, .g = 0, .b = 0, .a = 0 };
const af: f32 = @as(f32, @floatFromInt(a)) / 255.0;
return .{
.r = @intFromFloat(@as(f32, @floatFromInt(r)) * af),
.g = @intFromFloat(@as(f32, @floatFromInt(g)) * af),
.b = @intFromFloat(@as(f32, @floatFromInt(b)) * af),
.a = a,
};
}

View File

@@ -112,13 +112,18 @@ pub fn leftPanel(ctx: *WindowContext) void {
if (active_doc) |doc| { if (active_doc) |doc| {
const canvas = &doc.canvas; const canvas = &doc.canvas;
if (dvui.checkbox(@src(), &canvas.native_scaling, "Scaling", .{})) {} if (dvui.checkbox(@src(), &canvas.native_scaling, "Scaling", .{})) {}
if (dvui.checkbox(@src(), &canvas.draw_document, "Draw document", .{})) {
canvas.requestRedraw();
}
if (!canvas.draw_document) {
if (dvui.button(@src(), if (doc.cpu_render.type == .Gradient) "Gradient" else "Squares", .{}, .{})) { if (dvui.button(@src(), if (doc.cpu_render.type == .Gradient) "Gradient" else "Squares", .{}, .{})) {
if (doc.cpu_render.type == .Gradient) { if (doc.cpu_render.type == .Gradient) {
doc.cpu_render.type = .Squares; doc.cpu_render.type = .Squares;
} else { } else {
doc.cpu_render.type = .Gradient; doc.cpu_render.type = .Gradient;
} }
canvas.redrawExample() catch {}; canvas.requestRedraw();
}
} }
} else { } else {
dvui.label(@src(), "No document", .{}, .{}); dvui.label(@src(), "No document", .{}, .{});