Compare commits
10 commits
39632f56a7
...
236d612c20
| Author | SHA1 | Date | |
|---|---|---|---|
| 236d612c20 | |||
| 6e666c0265 | |||
| 3999dd3012 | |||
| 2810fe248f | |||
| 63d5c04a0d | |||
| 85dce88ec2 | |||
| 410f82201d | |||
| 1bbf1b1d94 | |||
| bb55e23979 | |||
| b4c3260f49 |
26 changed files with 1009 additions and 687 deletions
8
Cargo.lock
generated
8
Cargo.lock
generated
|
|
@ -642,7 +642,6 @@ dependencies = [
|
||||||
"haku",
|
"haku",
|
||||||
"log",
|
"log",
|
||||||
"paste",
|
"paste",
|
||||||
"tiny-skia",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -886,12 +885,6 @@ version = "0.2.155"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
|
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libm"
|
|
||||||
version = "0.2.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libsqlite3-sys"
|
name = "libsqlite3-sys"
|
||||||
version = "0.30.1"
|
version = "0.30.1"
|
||||||
|
|
@ -1652,7 +1645,6 @@ checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayref",
|
"arrayref",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"libm",
|
|
||||||
"strict-num",
|
"strict-num",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ haku.path = "crates/haku"
|
||||||
haku2.path = "crates/haku2"
|
haku2.path = "crates/haku2"
|
||||||
log = "0.4.22"
|
log = "0.4.22"
|
||||||
rkgk-image-ops.path = "crates/rkgk-image-ops"
|
rkgk-image-ops.path = "crates/rkgk-image-ops"
|
||||||
tiny-skia = { version = "0.11.4", default-features = false }
|
|
||||||
|
|
||||||
[profile.dev.package.rkgk-image-ops]
|
[profile.dev.package.rkgk-image-ops]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|
|
||||||
2
Justfile
2
Justfile
|
|
@ -4,7 +4,7 @@ wasm_profile := "wasm-" + profile
|
||||||
log := ""
|
log := ""
|
||||||
|
|
||||||
serve: wasm
|
serve: wasm
|
||||||
RKGK_PORT={{port}} RKGK_WASM_PATH=target/wasm32-unknown-unknown/{{wasm_profile}} RUST_LOG={{log}} cargo run -p rkgk --profile {{profile}}
|
RKGK_PORT={{port}} RKGK_WASM_PATH=target/wasm32-unknown-unknown/{{wasm_profile}} RUST_LOG={{log}} RUST_BACKTRACE=1 cargo run -p rkgk --profile {{profile}}
|
||||||
|
|
||||||
wasm:
|
wasm:
|
||||||
cargo build -p haku-wasm --target wasm32-unknown-unknown --profile {{wasm_profile}}
|
cargo build -p haku-wasm --target wasm32-unknown-unknown --profile {{wasm_profile}}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ arrayvec = { version = "0.7.4", default-features = false }
|
||||||
dlmalloc = { version = "0.2.6", features = ["global"] }
|
dlmalloc = { version = "0.2.6", features = ["global"] }
|
||||||
haku.workspace = true
|
haku.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
tiny-skia = { workspace = true, features = ["no-std-float"] }
|
|
||||||
paste = "1.0.15"
|
paste = "1.0.15"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,6 @@ use haku::{
|
||||||
token::Lexis,
|
token::Lexis,
|
||||||
};
|
};
|
||||||
use log::{debug, info};
|
use log::{debug, info};
|
||||||
use tiny_skia::{
|
|
||||||
BlendMode, Color, FillRule, LineCap, Paint, PathBuilder, Pixmap, PremultipliedColorU8, Rect,
|
|
||||||
Shader, Stroke, Transform,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub mod logging;
|
pub mod logging;
|
||||||
#[cfg(not(feature = "std"))]
|
#[cfg(not(feature = "std"))]
|
||||||
|
|
@ -201,147 +197,6 @@ extern "C" fn haku_status_string(code: StatusCode) -> *const i8 {
|
||||||
.as_ptr()
|
.as_ptr()
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PixmapCanvas {
|
|
||||||
pixmap: Pixmap,
|
|
||||||
pb: PathBuilder,
|
|
||||||
transform: Transform,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
extern "C" fn haku_pixmap_new(width: u32, height: u32) -> *mut PixmapCanvas {
|
|
||||||
let ptr = Box::leak(Box::new(PixmapCanvas {
|
|
||||||
pixmap: Pixmap::new(width, height).expect("invalid pixmap size"),
|
|
||||||
pb: PathBuilder::new(),
|
|
||||||
transform: Transform::identity(),
|
|
||||||
})) as *mut _;
|
|
||||||
debug!("created pixmap with size {width}x{height}: {ptr:?}");
|
|
||||||
ptr
|
|
||||||
}
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
unsafe extern "C" fn haku_pixmap_destroy(c: *mut PixmapCanvas) {
|
|
||||||
debug!("destroying pixmap: {c:?}");
|
|
||||||
drop(Box::from_raw(c))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
unsafe extern "C" fn haku_pixmap_data(c: *mut PixmapCanvas) -> *mut u8 {
|
|
||||||
let c = &mut *c;
|
|
||||||
c.pixmap.pixels_mut().as_mut_ptr() as *mut u8
|
|
||||||
}
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
unsafe extern "C" fn haku_pixmap_clear(c: *mut PixmapCanvas) {
|
|
||||||
let c = &mut *c;
|
|
||||||
c.pixmap
|
|
||||||
.pixels_mut()
|
|
||||||
.fill(PremultipliedColorU8::TRANSPARENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
unsafe extern "C" fn haku_pixmap_set_translation(c: *mut PixmapCanvas, x: f32, y: f32) {
|
|
||||||
let c = &mut *c;
|
|
||||||
c.transform = Transform::from_translate(x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
unsafe extern "C" fn haku_pixmap_begin(c: *mut PixmapCanvas) -> bool {
|
|
||||||
let c = &mut *c;
|
|
||||||
c.pb.clear();
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
unsafe extern "C" fn haku_pixmap_line(
|
|
||||||
c: *mut PixmapCanvas,
|
|
||||||
x1: f32,
|
|
||||||
y1: f32,
|
|
||||||
x2: f32,
|
|
||||||
y2: f32,
|
|
||||||
) -> bool {
|
|
||||||
let c = &mut *c;
|
|
||||||
c.pb.move_to(x1, y1);
|
|
||||||
c.pb.line_to(x2, y2);
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
unsafe extern "C" fn haku_pixmap_rectangle(
|
|
||||||
c: *mut PixmapCanvas,
|
|
||||||
x: f32,
|
|
||||||
y: f32,
|
|
||||||
width: f32,
|
|
||||||
height: f32,
|
|
||||||
) -> bool {
|
|
||||||
let c = &mut *c;
|
|
||||||
if let Some(rect) = Rect::from_xywh(x, y, width, height) {
|
|
||||||
c.pb.push_rect(rect);
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
unsafe extern "C" fn haku_pixmap_circle(c: *mut PixmapCanvas, x: f32, y: f32, r: f32) -> bool {
|
|
||||||
let c = &mut *c;
|
|
||||||
c.pb.push_circle(x, y, r);
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_paint() -> Paint<'static> {
|
|
||||||
Paint {
|
|
||||||
shader: Shader::SolidColor(Color::BLACK),
|
|
||||||
blend_mode: BlendMode::SourceOver,
|
|
||||||
anti_alias: false,
|
|
||||||
force_hq_pipeline: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
unsafe extern "C" fn haku_pixmap_fill(c: *mut PixmapCanvas, r: u8, g: u8, b: u8, a: u8) -> bool {
|
|
||||||
let c = &mut *c;
|
|
||||||
let pb = mem::take(&mut c.pb);
|
|
||||||
if let Some(path) = pb.finish() {
|
|
||||||
let paint = Paint {
|
|
||||||
shader: Shader::SolidColor(Color::from_rgba8(r, g, b, a)),
|
|
||||||
..default_paint()
|
|
||||||
};
|
|
||||||
c.pixmap
|
|
||||||
.fill_path(&path, &paint, FillRule::EvenOdd, c.transform, None);
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
unsafe extern "C" fn haku_pixmap_stroke(
|
|
||||||
c: *mut PixmapCanvas,
|
|
||||||
r: u8,
|
|
||||||
g: u8,
|
|
||||||
b: u8,
|
|
||||||
a: u8,
|
|
||||||
thickness: f32,
|
|
||||||
) -> bool {
|
|
||||||
let c = &mut *c;
|
|
||||||
let pb = mem::take(&mut c.pb);
|
|
||||||
if let Some(path) = pb.finish() {
|
|
||||||
let paint = Paint {
|
|
||||||
shader: Shader::SolidColor(Color::from_rgba8(r, g, b, a)),
|
|
||||||
..default_paint()
|
|
||||||
};
|
|
||||||
c.pixmap.stroke_path(
|
|
||||||
&path,
|
|
||||||
&paint,
|
|
||||||
&Stroke {
|
|
||||||
width: thickness,
|
|
||||||
line_cap: LineCap::Round,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
c.transform,
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
#[unsafe(no_mangle)]
|
||||||
unsafe extern "C" fn haku_compile_brush(
|
unsafe extern "C" fn haku_compile_brush(
|
||||||
instance: *mut Instance,
|
instance: *mut Instance,
|
||||||
|
|
|
||||||
|
|
@ -77,13 +77,7 @@ pub fn resolve(arity: SystemFnArity, name: &str) -> Option<u8> {
|
||||||
(Nary, "reduce") => 0x95,
|
(Nary, "reduce") => 0x95,
|
||||||
(Nary, "flatten") => 0x96,
|
(Nary, "flatten") => 0x96,
|
||||||
|
|
||||||
(Nary, "toShape") => 0xc0,
|
|
||||||
(Nary, "line") => 0xc1,
|
|
||||||
(Nary, "rect") => 0xc2,
|
|
||||||
(Nary, "circle") => 0xc3,
|
|
||||||
|
|
||||||
(Nary, "stroke") => 0xe0,
|
(Nary, "stroke") => 0xe0,
|
||||||
(Nary, "fill") => 0xe1,
|
|
||||||
|
|
||||||
(Nary, "withDotter") => 0xf0,
|
(Nary, "withDotter") => 0xf0,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,41 +8,21 @@ pub const Canvas = opaque {
|
||||||
if (!status) return error.Draw;
|
if (!status) return error.Draw;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn begin(c: *Canvas) !void {
|
pub fn stroke(c: *Canvas, color: value.Rgba8, thickness: f32, from: value.Vec2, to: value.Vec2) !void {
|
||||||
try wrap(__haku2_canvas_begin(c));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn line(c: *Canvas, start: value.Vec2, end: value.Vec2) !void {
|
|
||||||
const x1, const y1 = start;
|
|
||||||
const x2, const y2 = end;
|
|
||||||
try wrap(__haku2_canvas_line(c, x1, y1, x2, y2));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rect(c: *Canvas, top_left: value.Vec2, size: value.Vec2) !void {
|
|
||||||
const x, const y = top_left;
|
|
||||||
const width, const height = size;
|
|
||||||
try wrap(__haku2_canvas_rectangle(c, x, y, width, height));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn circle(c: *Canvas, center: value.Vec2, r: f32) !void {
|
|
||||||
const x, const y = center;
|
|
||||||
try wrap(__haku2_canvas_circle(c, x, y, r));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fill(c: *Canvas, color: value.Rgba8) !void {
|
|
||||||
const r, const g, const b, const a = color;
|
const r, const g, const b, const a = color;
|
||||||
try wrap(__haku2_canvas_fill(c, r, g, b, a));
|
try wrap(__haku2_canvas_stroke(c, r, g, b, a, thickness, from[0], from[1], to[0], to[1]));
|
||||||
}
|
|
||||||
|
|
||||||
pub fn stroke(c: *Canvas, color: value.Rgba8, thickness: f32) !void {
|
|
||||||
const r, const g, const b, const a = color;
|
|
||||||
try wrap(__haku2_canvas_stroke(c, r, g, b, a, thickness));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
extern fn __haku2_canvas_begin(c: *Canvas) bool;
|
extern fn __haku2_canvas_stroke(
|
||||||
extern fn __haku2_canvas_line(c: *Canvas, x1: f32, y1: f32, x2: f32, y2: f32) bool;
|
c: *Canvas,
|
||||||
extern fn __haku2_canvas_rectangle(c: *Canvas, x: f32, y: f32, width: f32, height: f32) bool;
|
r: u8,
|
||||||
extern fn __haku2_canvas_circle(c: *Canvas, x: f32, y: f32, r: f32) bool;
|
g: u8,
|
||||||
extern fn __haku2_canvas_fill(c: *Canvas, r: u8, g: u8, b: u8, a: u8) bool;
|
b: u8,
|
||||||
extern fn __haku2_canvas_stroke(c: *Canvas, r: u8, g: u8, b: u8, a: u8, thickness: f32) bool;
|
a: u8,
|
||||||
|
thickness: f32,
|
||||||
|
from_x: f32,
|
||||||
|
from_y: f32,
|
||||||
|
to_x: f32,
|
||||||
|
to_y: f32,
|
||||||
|
) bool;
|
||||||
|
|
|
||||||
|
|
@ -24,18 +24,13 @@ fn renderRec(vm: *Vm, canvas: *Canvas, val: Value, depth: usize, max_depth: usiz
|
||||||
|
|
||||||
switch (val.ref.*) {
|
switch (val.ref.*) {
|
||||||
.scribble => {
|
.scribble => {
|
||||||
try canvas.begin();
|
switch (val.ref.scribble) {
|
||||||
|
.stroke => |stroke| try canvas.stroke(
|
||||||
switch (val.ref.scribble.shape) {
|
value.rgbaTo8(stroke.color),
|
||||||
.point => |point| try canvas.line(point, point),
|
stroke.thickness,
|
||||||
.line => |line| try canvas.line(line.start, line.end),
|
value.vec2From4(stroke.from),
|
||||||
.rect => |rect| try canvas.rect(rect.top_left, rect.size),
|
value.vec2From4(stroke.to),
|
||||||
.circle => |circle| try canvas.circle(circle.center, circle.radius),
|
),
|
||||||
}
|
|
||||||
|
|
||||||
switch (val.ref.scribble.action) {
|
|
||||||
.stroke => |stroke| try canvas.stroke(value.rgbaTo8(stroke.color), stroke.thickness),
|
|
||||||
.fill => |fill| try canvas.fill(value.rgbaTo8(fill.color)),
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -46,10 +41,6 @@ fn renderRec(vm: *Vm, canvas: *Canvas, val: Value, depth: usize, max_depth: usiz
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
.shape => {
|
|
||||||
return vm.throw("the brush returned a bare shape, which cannot be drawn. try wrapping your shape in a fill or a stroke: (fill #000 <shape>)", .{});
|
|
||||||
},
|
|
||||||
|
|
||||||
else => return notAScribble(vm, val),
|
else => return notAScribble(vm, val),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -118,14 +118,6 @@ fn fromArgument(cx: Context, comptime T: type, i: usize) Vm.Error!T {
|
||||||
if (val != .ref or val.ref.* != .list) return typeError(cx.vm, val, i, "list");
|
if (val != .ref or val.ref.* != .list) return typeError(cx.vm, val, i, "list");
|
||||||
return val.ref.list;
|
return val.ref.list;
|
||||||
},
|
},
|
||||||
value.Shape => {
|
|
||||||
const val = cx.args[i];
|
|
||||||
if (toShape(val)) |shape| {
|
|
||||||
return shape;
|
|
||||||
} else {
|
|
||||||
return typeError(cx.vm, val, i, "shape");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
*const value.Closure => {
|
*const value.Closure => {
|
||||||
const val = cx.args[i];
|
const val = cx.args[i];
|
||||||
if (val != .ref or val.ref.* != .closure) return typeError(cx.vm, val, i, "function");
|
if (val != .ref or val.ref.* != .closure) return typeError(cx.vm, val, i, "function");
|
||||||
|
|
@ -282,12 +274,7 @@ pub const fns = makeFnTable(&[_]SparseFn{
|
||||||
.{ 0x94, erase("filter", filter) },
|
.{ 0x94, erase("filter", filter) },
|
||||||
.{ 0x95, erase("reduce", reduce) },
|
.{ 0x95, erase("reduce", reduce) },
|
||||||
.{ 0x96, erase("flatten", flatten) },
|
.{ 0x96, erase("flatten", flatten) },
|
||||||
.{ 0xc0, erase("toShape", valueToShape) },
|
|
||||||
.{ 0xc1, erase("line", line) },
|
|
||||||
.{ 0xc2, erase("rect", rect) },
|
|
||||||
.{ 0xc3, erase("circle", circle) },
|
|
||||||
.{ 0xe0, erase("stroke", stroke) },
|
.{ 0xe0, erase("stroke", stroke) },
|
||||||
.{ 0xe1, erase("fill", fill) },
|
|
||||||
.{ 0xf0, erase("withDotter", withDotter) },
|
.{ 0xf0, erase("withDotter", withDotter) },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -738,51 +725,14 @@ fn flatten(list: value.List, cx: Context) Vm.Error!value.Ref {
|
||||||
return .{ .list = flattened_list };
|
return .{ .list = flattened_list };
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toShape(val: value.Value) ?value.Shape {
|
fn stroke(thickness: f32, color: Rgba, from: Vec4, to: Vec4) value.Ref {
|
||||||
return switch (val) {
|
|
||||||
.nil, .false, .true, .tag, .number, .rgba => null,
|
|
||||||
.vec4 => |v| .{ .point = value.vec2From4(v) },
|
|
||||||
.ref => |r| if (r.* == .shape) r.shape else null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `toShape`
|
|
||||||
fn valueToShape(val: value.Value) ?value.Ref {
|
|
||||||
if (toShape(val)) |shape| {
|
|
||||||
return .{ .shape = shape };
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn line(start: Vec4, end: Vec4) value.Ref {
|
|
||||||
return .{ .shape = .{ .line = .{
|
|
||||||
.start = value.vec2From4(start.value),
|
|
||||||
.end = value.vec2From4(end.value),
|
|
||||||
} } };
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect(top_left: Vec4, size: Vec4) value.Ref {
|
|
||||||
return .{ .shape = .{ .rect = .{
|
|
||||||
.top_left = value.vec2From4(top_left.value),
|
|
||||||
.size = value.vec2From4(size.value),
|
|
||||||
} } };
|
|
||||||
}
|
|
||||||
|
|
||||||
fn circle(center: Vec4, radius: f32) value.Ref {
|
|
||||||
return .{ .shape = .{ .circle = .{
|
|
||||||
.center = value.vec2From4(center.value),
|
|
||||||
.radius = radius,
|
|
||||||
} } };
|
|
||||||
}
|
|
||||||
|
|
||||||
fn stroke(thickness: f32, color: Rgba, shape: value.Shape) value.Ref {
|
|
||||||
return .{ .scribble = .{
|
return .{ .scribble = .{
|
||||||
.shape = shape,
|
.stroke = .{
|
||||||
.action = .{ .stroke = .{
|
|
||||||
.thickness = thickness,
|
.thickness = thickness,
|
||||||
.color = color.value,
|
.color = color.value,
|
||||||
} },
|
.from = from.value,
|
||||||
|
.to = to.value,
|
||||||
|
},
|
||||||
} };
|
} };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,6 @@ pub const Value = union(enum) {
|
||||||
.ref => |r| switch (r.*) {
|
.ref => |r| switch (r.*) {
|
||||||
.closure => "function",
|
.closure => "function",
|
||||||
.list => "list",
|
.list => "list",
|
||||||
.shape => "shape",
|
|
||||||
.scribble => "scribble",
|
.scribble => "scribble",
|
||||||
.reticle => "reticle",
|
.reticle => "reticle",
|
||||||
},
|
},
|
||||||
|
|
@ -86,7 +85,7 @@ pub const Value = union(enum) {
|
||||||
}
|
}
|
||||||
try writer.writeAll("]");
|
try writer.writeAll("]");
|
||||||
},
|
},
|
||||||
inline .shape, .scribble, .reticle => |x| try writer.print("{}", .{x}),
|
inline .scribble, .reticle => |x| try writer.print("{}", .{x}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -122,7 +121,6 @@ pub fn rgbaTo8(rgba: Rgba) Rgba8 {
|
||||||
pub const Ref = union(enum) {
|
pub const Ref = union(enum) {
|
||||||
closure: Closure,
|
closure: Closure,
|
||||||
list: List,
|
list: List,
|
||||||
shape: Shape,
|
|
||||||
scribble: Scribble,
|
scribble: Scribble,
|
||||||
reticle: Reticle,
|
reticle: Reticle,
|
||||||
};
|
};
|
||||||
|
|
@ -161,44 +159,14 @@ pub const Closure = struct {
|
||||||
|
|
||||||
pub const List = []Value;
|
pub const List = []Value;
|
||||||
|
|
||||||
pub const Shape = union(enum) {
|
pub const Scribble = union(enum) {
|
||||||
point: Vec2,
|
|
||||||
line: Line,
|
|
||||||
rect: Rect,
|
|
||||||
circle: Circle,
|
|
||||||
|
|
||||||
pub const Line = struct {
|
|
||||||
start: Vec2,
|
|
||||||
end: Vec2,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Rect = struct {
|
|
||||||
top_left: Vec2,
|
|
||||||
size: Vec2,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Circle = struct {
|
|
||||||
center: Vec2,
|
|
||||||
radius: f32,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Scribble = struct {
|
|
||||||
shape: Shape,
|
|
||||||
action: Action,
|
|
||||||
|
|
||||||
pub const Action = union(enum) {
|
|
||||||
stroke: Stroke,
|
stroke: Stroke,
|
||||||
fill: Fill,
|
|
||||||
|
|
||||||
pub const Stroke = struct {
|
pub const Stroke = struct {
|
||||||
thickness: f32,
|
thickness: f32,
|
||||||
color: Rgba,
|
color: Rgba,
|
||||||
};
|
from: Vec4,
|
||||||
|
to: Vec4,
|
||||||
pub const Fill = struct {
|
|
||||||
color: Rgba,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,8 @@ async fn fallible_websocket(api: Arc<Api>, ws: &mut WebSocket) -> eyre::Result<(
|
||||||
|
|
||||||
ws.send(to_message(&Version { version })).await?;
|
ws.send(to_message(&Version { version })).await?;
|
||||||
|
|
||||||
let login_request: LoginRequest = from_message(&recv_expect(ws).await?)?;
|
let login_request: LoginRequest =
|
||||||
|
from_message(&recv_expect(ws).await?).context("LoginRequest")?;
|
||||||
let user_id = login_request.user;
|
let user_id = login_request.user;
|
||||||
let secret = base64::engine::general_purpose::URL_SAFE
|
let secret = base64::engine::general_purpose::URL_SAFE
|
||||||
.decode(&login_request.secret)
|
.decode(&login_request.secret)
|
||||||
|
|
@ -284,7 +285,8 @@ impl SessionLoop {
|
||||||
loop {
|
loop {
|
||||||
select! {
|
select! {
|
||||||
Some(message) = ws.recv() => {
|
Some(message) = ws.recv() => {
|
||||||
let request = from_message(&message?)?;
|
let message = message?;
|
||||||
|
let request = from_message(&message).context("Request")?;
|
||||||
self.process_request(ws, request).await?;
|
self.process_request(ws, request).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,10 @@ async fn main() {
|
||||||
#[cfg(feature = "memory-profiling")]
|
#[cfg(feature = "memory-profiling")]
|
||||||
let _client = tracy_client::Client::start();
|
let _client = tracy_client::Client::start();
|
||||||
|
|
||||||
color_eyre::install().unwrap();
|
color_eyre::config::HookBuilder::new()
|
||||||
|
.theme(color_eyre::config::Theme::new())
|
||||||
|
.install()
|
||||||
|
.unwrap();
|
||||||
tracing_subscriber::registry()
|
tracing_subscriber::registry()
|
||||||
.with(tracing_subscriber::fmt::layer())
|
.with(tracing_subscriber::fmt::layer())
|
||||||
.with(
|
.with(
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { Haku } from "rkgk/haku.js";
|
||||||
import { randomId } from "rkgk/random.js";
|
import { randomId } from "rkgk/random.js";
|
||||||
import { SaveData } from "rkgk/framework.js";
|
import { SaveData } from "rkgk/framework.js";
|
||||||
import { ContextMenu, globalContextMenuSpace } from "rkgk/context-menu.js";
|
import { ContextMenu, globalContextMenuSpace } from "rkgk/context-menu.js";
|
||||||
|
import { BrushRenderer } from "rkgk/brush-renderer.js";
|
||||||
|
|
||||||
export const builtInPresets = [
|
export const builtInPresets = [
|
||||||
{
|
{
|
||||||
|
|
@ -15,7 +16,7 @@ color: #000
|
||||||
thickness: 8
|
thickness: 8
|
||||||
|
|
||||||
withDotter \\d ->
|
withDotter \\d ->
|
||||||
stroke thickness color (line d.From d.To)
|
stroke thickness color d.From d.To
|
||||||
`.trim(),
|
`.trim(),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -27,7 +28,7 @@ color: #000
|
||||||
thickness: 48
|
thickness: 48
|
||||||
|
|
||||||
withDotter \\d ->
|
withDotter \\d ->
|
||||||
stroke thickness color (line d.From d.To)
|
stroke thickness color d.From d.To
|
||||||
`.trim(),
|
`.trim(),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -43,7 +44,7 @@ duty: 0.5
|
||||||
withDotter \\d ->
|
withDotter \\d ->
|
||||||
visible? = d.Num |mod length < length * duty
|
visible? = d.Num |mod length < length * duty
|
||||||
if (visible?)
|
if (visible?)
|
||||||
stroke thickness color (line d.From d.To)
|
stroke thickness color d.From d.To
|
||||||
else
|
else
|
||||||
()
|
()
|
||||||
`.trim(),
|
`.trim(),
|
||||||
|
|
@ -57,7 +58,7 @@ color: #0003
|
||||||
thickness: 6
|
thickness: 6
|
||||||
|
|
||||||
withDotter \\d ->
|
withDotter \\d ->
|
||||||
stroke thickness color (line d.From d.To)
|
stroke thickness color d.From d.To
|
||||||
`.trim(),
|
`.trim(),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -75,7 +76,7 @@ withDotter \\d ->
|
||||||
a = sin (d.Num * wavelength / pi) + 1 / 2
|
a = sin (d.Num * wavelength / pi) + 1 / 2
|
||||||
range = maxThickness - minThickness
|
range = maxThickness - minThickness
|
||||||
thickness = a * range + minThickness
|
thickness = a * range + minThickness
|
||||||
stroke thickness color (line d.From d.To)
|
stroke thickness color d.From d.To
|
||||||
`.trim(),
|
`.trim(),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -104,7 +105,7 @@ withDotter \\d ->
|
||||||
clockwise = norm (perpClockwise d.To-d.From) * vec a a
|
clockwise = norm (perpClockwise d.To-d.From) * vec a a
|
||||||
from = d.From + clockwise
|
from = d.From + clockwise
|
||||||
to = d.To + clockwise
|
to = d.To + clockwise
|
||||||
stroke thickness color (line from to)
|
stroke thickness color from to
|
||||||
`.trim(),
|
`.trim(),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -125,7 +126,7 @@ withDotter \\d ->
|
||||||
g = colorCurve (d.Num * l + pi/3)
|
g = colorCurve (d.Num * l + pi/3)
|
||||||
b = colorCurve (d.Num * l + 2*pi/3)
|
b = colorCurve (d.Num * l + 2*pi/3)
|
||||||
color = rgba r g b 1
|
color = rgba r g b 1
|
||||||
stroke thickness color (line d.From d.To)
|
stroke thickness color d.From d.To
|
||||||
`.trim(),
|
`.trim(),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -134,7 +135,8 @@ withDotter \\d ->
|
||||||
|
|
||||||
function presetButton(info) {
|
function presetButton(info) {
|
||||||
let button = document.createElement("button");
|
let button = document.createElement("button");
|
||||||
let preview = button.appendChild(new BrushPreview(56, 56));
|
let preview = button.appendChild(document.createElement("div"));
|
||||||
|
preview.classList.add("preview");
|
||||||
let label = button.appendChild(document.createElement("p"));
|
let label = button.appendChild(document.createElement("p"));
|
||||||
label.classList.add("label");
|
label.classList.add("label");
|
||||||
label.innerText = info.name;
|
label.innerText = info.name;
|
||||||
|
|
@ -172,6 +174,14 @@ export class BrushBox extends HTMLElement {
|
||||||
|
|
||||||
initialize(wallLimits) {
|
initialize(wallLimits) {
|
||||||
this.haku = new Haku(wallLimits);
|
this.haku = new Haku(wallLimits);
|
||||||
|
|
||||||
|
this.brushesCanvas = this.appendChild(document.createElement("canvas"));
|
||||||
|
this.gl = this.brushesCanvas.getContext("webgl2");
|
||||||
|
let canvasResizeObserver = new ResizeObserver(() => this.renderBrushes());
|
||||||
|
canvasResizeObserver.observe(this);
|
||||||
|
|
||||||
|
this.brushRenderer = new BrushRenderer(this.gl, this.canvasSource());
|
||||||
|
|
||||||
this.renderBrushes();
|
this.renderBrushes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -290,10 +300,61 @@ export class BrushBox extends HTMLElement {
|
||||||
if (this.currentPresetId != null) this.setCurrentBrush(this.currentPresetId);
|
if (this.currentPresetId != null) this.setCurrentBrush(this.currentPresetId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canvasSource() {
|
||||||
|
let brushBox = this;
|
||||||
|
return {
|
||||||
|
useCanvas(gl, i) {
|
||||||
|
let brush = brushBox.brushes[i];
|
||||||
|
|
||||||
|
let canvasRect = brushBox.brushesCanvas.getBoundingClientRect();
|
||||||
|
let previewRect = brush.presetButton.preview.getBoundingClientRect();
|
||||||
|
let viewport = {
|
||||||
|
x: previewRect.x - canvasRect.x,
|
||||||
|
y: canvasRect.bottom - previewRect.bottom,
|
||||||
|
width: previewRect.width,
|
||||||
|
height: previewRect.height,
|
||||||
|
};
|
||||||
|
|
||||||
|
gl.enable(gl.SCISSOR_TEST);
|
||||||
|
gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height);
|
||||||
|
gl.scissor(viewport.x, viewport.y, viewport.width, viewport.height);
|
||||||
|
|
||||||
|
return viewport;
|
||||||
|
},
|
||||||
|
|
||||||
|
resetCanvas(gl) {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async renderBrushes() {
|
async renderBrushes() {
|
||||||
for (let brush of this.brushes) {
|
let gl = this.gl;
|
||||||
|
|
||||||
|
this.brushesCanvas.width = this.brushesCanvas.clientWidth;
|
||||||
|
this.brushesCanvas.height = this.brushesContainer.scrollHeight;
|
||||||
|
|
||||||
|
gl.clearColor(0, 0, 0, 0);
|
||||||
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||||
|
|
||||||
|
for (let i = 0; i < this.brushes.length; ++i) {
|
||||||
|
let brush = this.brushes[i];
|
||||||
|
let previewRect = brush.presetButton.preview.getBoundingClientRect();
|
||||||
|
|
||||||
this.haku.setBrush(brush.preset.code);
|
this.haku.setBrush(brush.preset.code);
|
||||||
await brush.presetButton.preview.renderBrush(this.haku);
|
await this.haku.evalBrush({
|
||||||
|
runDotter: async () => {
|
||||||
|
return {
|
||||||
|
fromX: previewRect.width / 2,
|
||||||
|
fromY: previewRect.height / 2,
|
||||||
|
toX: previewRect.width / 2,
|
||||||
|
toY: previewRect.height / 2,
|
||||||
|
num: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
runScribble: async (renderToCanvas) => {
|
||||||
|
renderToCanvas(this.brushRenderer, i, 0, 0);
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import { Pixmap } from "rkgk/haku.js";
|
|
||||||
|
|
||||||
export class BrushPreview extends HTMLElement {
|
export class BrushPreview extends HTMLElement {
|
||||||
constructor(width, height) {
|
constructor(width, height) {
|
||||||
super();
|
super();
|
||||||
|
|
@ -9,53 +7,11 @@ export class BrushPreview extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this.canvas = this.appendChild(document.createElement("canvas"));
|
// TODO
|
||||||
this.ctx = this.canvas.getContext("2d");
|
|
||||||
|
|
||||||
this.#resizeCanvas();
|
|
||||||
if (this.width == null || this.height == null) {
|
|
||||||
new ResizeObserver(() => this.#resizeCanvas()).observe(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#resizeCanvas() {
|
|
||||||
this.canvas.width = this.width ?? this.clientWidth;
|
|
||||||
this.canvas.height = this.height ?? this.clientHeight;
|
|
||||||
|
|
||||||
// This can happen if the element's `display: none`.
|
|
||||||
if (this.canvas.width == 0 || this.canvas.height == 0) return;
|
|
||||||
|
|
||||||
if (this.pixmap != null) {
|
|
||||||
this.pixmap.destroy();
|
|
||||||
}
|
|
||||||
this.pixmap = new Pixmap(this.canvas.width, this.canvas.height);
|
|
||||||
|
|
||||||
this.dispatchEvent(new Event(".pixmapLost"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async #renderBrushInner(haku) {
|
async #renderBrushInner(haku) {
|
||||||
this.pixmap.clear();
|
// TODO
|
||||||
let evalResult = await haku.evalBrush({
|
|
||||||
runDotter: async () => {
|
|
||||||
return {
|
|
||||||
fromX: this.canvas.width / 2,
|
|
||||||
fromY: this.canvas.height / 2,
|
|
||||||
toX: this.canvas.width / 2,
|
|
||||||
toY: this.canvas.height / 2,
|
|
||||||
num: 0,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
runScribble: async (renderToPixmap) => {
|
|
||||||
return renderToPixmap(this.pixmap, 0, 0);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (evalResult.status != "ok") {
|
|
||||||
return { status: "error", phase: "eval", result: evalResult };
|
|
||||||
}
|
|
||||||
|
|
||||||
this.ctx.putImageData(this.pixmap.getImageData(), 0, 0);
|
|
||||||
|
|
||||||
return { status: "ok" };
|
return { status: "ok" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
212
static/brush-renderer.js
Normal file
212
static/brush-renderer.js
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
import { compileProgram, orthographicProjection } from "rkgk/webgl.js";
|
||||||
|
|
||||||
|
const linesVertexShader = `#version 300 es
|
||||||
|
precision highp float;
|
||||||
|
|
||||||
|
uniform mat4 u_projection;
|
||||||
|
uniform vec2 u_translation;
|
||||||
|
|
||||||
|
layout (location = 0) in vec2 a_position;
|
||||||
|
// Instance
|
||||||
|
layout (location = 1) in vec4 a_line; // (x1, y1, x2, y2)
|
||||||
|
layout (location = 2) in vec4 a_color;
|
||||||
|
layout (location = 3) in vec2 a_properties; // (thickness, hardness)
|
||||||
|
|
||||||
|
out vec2 vf_localPosition;
|
||||||
|
out vec4 vf_line;
|
||||||
|
out vec4 vf_color;
|
||||||
|
out vec2 vf_properties;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
float thickness = a_properties.x;
|
||||||
|
|
||||||
|
vec2 from = a_line.xy;
|
||||||
|
vec2 to = a_line.zw;
|
||||||
|
vec2 direction = normalize(to - from);
|
||||||
|
if (to == from)
|
||||||
|
direction = vec2(1.0, 0.0);
|
||||||
|
|
||||||
|
// Extrude forward for caps
|
||||||
|
from -= direction * (thickness / 2.0);
|
||||||
|
to += direction * (thickness / 2.0);
|
||||||
|
|
||||||
|
vec2 xAxis = to - from;
|
||||||
|
vec2 yAxis = vec2(-direction.y, direction.x) * thickness;
|
||||||
|
|
||||||
|
vec2 localPosition = from + xAxis * a_position.x + yAxis * a_position.y;
|
||||||
|
vec4 screenPosition = vec4(localPosition + u_translation, 0.0, 1.0);
|
||||||
|
vec4 scenePosition = screenPosition * u_projection;
|
||||||
|
|
||||||
|
gl_Position = scenePosition;
|
||||||
|
vf_localPosition = localPosition;
|
||||||
|
vf_line = a_line;
|
||||||
|
vf_color = a_color;
|
||||||
|
vf_properties = a_properties;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const linesFragmentShader = `#version 300 es
|
||||||
|
precision highp float;
|
||||||
|
|
||||||
|
in vec2 vf_localPosition;
|
||||||
|
in vec4 vf_line;
|
||||||
|
in vec4 vf_color;
|
||||||
|
in vec2 vf_properties;
|
||||||
|
|
||||||
|
out vec4 f_color;
|
||||||
|
|
||||||
|
// https://iquilezles.org/articles/distfunctions2d/
|
||||||
|
|
||||||
|
float segmentSdf(vec2 uv, vec2 a, vec2 b) {
|
||||||
|
vec2 uva = uv - a;
|
||||||
|
vec2 ba = b - a;
|
||||||
|
float h = clamp(dot(uva, ba) / dot(ba, ba), 0.0, 1.0);
|
||||||
|
return length(uva - ba * h);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
float thickness = vf_properties.x;
|
||||||
|
float hardness = vf_properties.y;
|
||||||
|
float halfSoftness = (1.0 - hardness) / 2.0;
|
||||||
|
|
||||||
|
vec2 uv = vf_localPosition;
|
||||||
|
float alpha = -(segmentSdf(uv, vf_line.xy, vf_line.zw) - thickness) / thickness;
|
||||||
|
if (hardness > 0.999)
|
||||||
|
alpha = step(0.5, alpha);
|
||||||
|
else
|
||||||
|
alpha = smoothstep(0.5 - halfSoftness, 0.5001 + halfSoftness, alpha);
|
||||||
|
|
||||||
|
f_color = vec4(vec3(1.0), alpha) * vf_color;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const linesMaxInstances = 1;
|
||||||
|
const lineInstanceSize = 12;
|
||||||
|
const lineDataBufferSize = lineInstanceSize * linesMaxInstances;
|
||||||
|
|
||||||
|
export class BrushRenderer {
|
||||||
|
#translation = { x: 0, y: 0 };
|
||||||
|
|
||||||
|
constructor(gl, canvasSource) {
|
||||||
|
this.gl = gl;
|
||||||
|
this.canvasSource = canvasSource;
|
||||||
|
|
||||||
|
console.group("construct BrushRenderer");
|
||||||
|
|
||||||
|
// Lines
|
||||||
|
|
||||||
|
let linesProgramId = compileProgram(gl, linesVertexShader, linesFragmentShader);
|
||||||
|
this.linesProgram = {
|
||||||
|
id: linesProgramId,
|
||||||
|
u_projection: gl.getUniformLocation(linesProgramId, "u_projection"),
|
||||||
|
u_translation: gl.getUniformLocation(linesProgramId, "u_translation"),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.linesVao = gl.createVertexArray();
|
||||||
|
this.linesVbo = gl.createBuffer();
|
||||||
|
|
||||||
|
gl.bindVertexArray(this.linesVao);
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.linesVbo);
|
||||||
|
|
||||||
|
const lineRect = new Float32Array(
|
||||||
|
// prettier-ignore
|
||||||
|
[
|
||||||
|
0, -0.5,
|
||||||
|
1, -0.5,
|
||||||
|
0, 0.5,
|
||||||
|
1, -0.5,
|
||||||
|
1, 0.5,
|
||||||
|
0, 0.5,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
this.linesVboData = new Float32Array(lineRect.length + lineDataBufferSize);
|
||||||
|
this.linesVboData.set(lineRect, 0);
|
||||||
|
this.linesInstanceData = this.linesVboData.subarray(lineRect.length);
|
||||||
|
|
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, this.linesVboData, gl.DYNAMIC_DRAW, 0);
|
||||||
|
|
||||||
|
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 2 * 4, 0); // a_position
|
||||||
|
gl.vertexAttribPointer(1, 4, gl.FLOAT, false, lineInstanceSize, lineRect.byteLength); // a_line
|
||||||
|
gl.vertexAttribPointer(
|
||||||
|
2, // a_color
|
||||||
|
4,
|
||||||
|
gl.FLOAT,
|
||||||
|
false,
|
||||||
|
lineInstanceSize,
|
||||||
|
lineRect.byteLength + 4 * 4,
|
||||||
|
);
|
||||||
|
gl.vertexAttribPointer(
|
||||||
|
3, // a_properties
|
||||||
|
2,
|
||||||
|
gl.FLOAT,
|
||||||
|
false,
|
||||||
|
lineInstanceSize,
|
||||||
|
lineRect.byteLength + 4 * 4 * 2,
|
||||||
|
);
|
||||||
|
for (let i = 0; i < 4; ++i) gl.enableVertexAttribArray(i);
|
||||||
|
for (let i = 1; i < 4; ++i) gl.vertexAttribDivisor(i, 1);
|
||||||
|
|
||||||
|
console.debug("pipeline lines", {
|
||||||
|
linesVao: this.linesVao,
|
||||||
|
linesVbo: this.linesVbo,
|
||||||
|
linesVboSize: this.linesVboData.byteLength,
|
||||||
|
linesInstanceDataOffset: this.linesInstanceData.byteOffset,
|
||||||
|
});
|
||||||
|
|
||||||
|
gl.bindVertexArray(null);
|
||||||
|
|
||||||
|
console.groupEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
#drawLines(instanceCount) {
|
||||||
|
let gl = this.gl;
|
||||||
|
|
||||||
|
gl.bindVertexArray(this.linesVao);
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.linesVbo);
|
||||||
|
gl.bufferSubData(
|
||||||
|
gl.ARRAY_BUFFER,
|
||||||
|
this.linesInstanceData.byteOffset,
|
||||||
|
this.linesInstanceData.subarray(0, instanceCount * lineInstanceSize),
|
||||||
|
);
|
||||||
|
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, instanceCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTranslation(x, y) {
|
||||||
|
this.#translation.x = x;
|
||||||
|
this.#translation.y = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
stroke(canvas, r, g, b, a, thickness, x1, y1, x2, y2) {
|
||||||
|
let gl = this.gl;
|
||||||
|
|
||||||
|
let viewport = this.canvasSource.useCanvas(gl, canvas);
|
||||||
|
|
||||||
|
gl.useProgram(this.linesProgram.id);
|
||||||
|
gl.uniformMatrix4fv(
|
||||||
|
this.linesProgram.u_projection,
|
||||||
|
false,
|
||||||
|
orthographicProjection(0, viewport.width, viewport.height, 0, -1, 1),
|
||||||
|
);
|
||||||
|
gl.uniform2f(this.linesProgram.u_translation, this.#translation.x, this.#translation.y);
|
||||||
|
|
||||||
|
gl.enable(gl.BLEND);
|
||||||
|
|
||||||
|
let instances = this.linesInstanceData;
|
||||||
|
instances[0] = x1;
|
||||||
|
instances[1] = y1;
|
||||||
|
instances[2] = x2;
|
||||||
|
instances[3] = y2;
|
||||||
|
instances[4] = r / 255;
|
||||||
|
instances[5] = g / 255;
|
||||||
|
instances[6] = b / 255;
|
||||||
|
instances[7] = a / 255;
|
||||||
|
instances[8] = thickness;
|
||||||
|
instances[9] = 1; // hardness
|
||||||
|
this.#drawLines(1);
|
||||||
|
|
||||||
|
this.canvasSource.resetCanvas(gl);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import { listen, Pool } from "rkgk/framework.js";
|
import { listen, Pool } from "rkgk/framework.js";
|
||||||
import { Viewport } from "rkgk/viewport.js";
|
import { Viewport } from "rkgk/viewport.js";
|
||||||
import { Wall, chunkKey } from "rkgk/wall.js";
|
import { Wall, chunkKey } from "rkgk/wall.js";
|
||||||
|
import { AtlasAllocator } from "rkgk/chunk-allocator.js";
|
||||||
|
import { compileProgram } from "rkgk/webgl.js";
|
||||||
|
import { BrushRenderer } from "rkgk/brush-renderer.js";
|
||||||
|
|
||||||
class CanvasRenderer extends HTMLElement {
|
class CanvasRenderer extends HTMLElement {
|
||||||
viewport = new Viewport();
|
viewport = new Viewport();
|
||||||
|
|
@ -51,6 +54,7 @@ class CanvasRenderer extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
getWindowSize() {
|
getWindowSize() {
|
||||||
|
if (this.width == null || this.height == null) return { width: 0, height: 0 };
|
||||||
return {
|
return {
|
||||||
width: this.width,
|
width: this.width,
|
||||||
height: this.height,
|
height: this.height,
|
||||||
|
|
@ -73,14 +77,11 @@ class CanvasRenderer extends HTMLElement {
|
||||||
// Renderer initialization
|
// Renderer initialization
|
||||||
|
|
||||||
#initializeRenderer() {
|
#initializeRenderer() {
|
||||||
console.groupCollapsed("initializeRenderer");
|
console.group("initializeRenderer");
|
||||||
|
|
||||||
console.info("vendor", this.gl.getParameter(this.gl.VENDOR));
|
console.info("vendor", this.gl.getParameter(this.gl.VENDOR));
|
||||||
console.info("renderer", this.gl.getParameter(this.gl.RENDERER));
|
console.info("renderer", this.gl.getParameter(this.gl.RENDERER));
|
||||||
|
|
||||||
this.gl.enable(this.gl.BLEND);
|
|
||||||
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA);
|
|
||||||
|
|
||||||
// Due to an ANGLE bug on Windows, we can only render around 64 rectangles in a batch.
|
// Due to an ANGLE bug on Windows, we can only render around 64 rectangles in a batch.
|
||||||
//
|
//
|
||||||
// It seems that for DirectX it generates a horribly inefficient shader that the DirectX
|
// It seems that for DirectX it generates a horribly inefficient shader that the DirectX
|
||||||
|
|
@ -92,7 +93,9 @@ class CanvasRenderer extends HTMLElement {
|
||||||
// We also realistically don't need anymore, because (at least at the time I'm writing this)
|
// We also realistically don't need anymore, because (at least at the time I'm writing this)
|
||||||
// we store (8 * 8 = 64) chunks per texture atlas, so we can't batch more than that.
|
// we store (8 * 8 = 64) chunks per texture atlas, so we can't batch more than that.
|
||||||
const maxRects = 64;
|
const maxRects = 64;
|
||||||
let renderChunksProgramId = this.#compileProgram(
|
let renderChunksProgramId = compileProgram(
|
||||||
|
this.gl,
|
||||||
|
|
||||||
// Vertex
|
// Vertex
|
||||||
`#version 300 es
|
`#version 300 es
|
||||||
|
|
||||||
|
|
@ -130,12 +133,26 @@ class CanvasRenderer extends HTMLElement {
|
||||||
precision highp float;
|
precision highp float;
|
||||||
|
|
||||||
uniform sampler2D u_texture;
|
uniform sampler2D u_texture;
|
||||||
|
uniform int u_visAtlasIndex;
|
||||||
|
|
||||||
in vec2 vf_uv;
|
in vec2 vf_uv;
|
||||||
out vec4 f_color;
|
out vec4 f_color;
|
||||||
|
|
||||||
|
float goldNoise(vec2 xy, float seed) {
|
||||||
|
return fract(tan(distance(xy * 1.6180339, xy) * seed) * xy.x);
|
||||||
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
f_color = texture(u_texture, vf_uv);
|
vec4 color = texture(u_texture, vf_uv);
|
||||||
|
if (u_visAtlasIndex != 0) {
|
||||||
|
color = vec4(
|
||||||
|
goldNoise(vec2(float(u_visAtlasIndex), 0.0), 0.1),
|
||||||
|
goldNoise(vec2(float(u_visAtlasIndex), 0.0), 0.2),
|
||||||
|
goldNoise(vec2(float(u_visAtlasIndex), 0.0), 0.3),
|
||||||
|
1.0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
f_color = color;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
|
|
@ -146,6 +163,7 @@ class CanvasRenderer extends HTMLElement {
|
||||||
u_projection: this.gl.getUniformLocation(renderChunksProgramId, "u_projection"),
|
u_projection: this.gl.getUniformLocation(renderChunksProgramId, "u_projection"),
|
||||||
u_view: this.gl.getUniformLocation(renderChunksProgramId, "u_view"),
|
u_view: this.gl.getUniformLocation(renderChunksProgramId, "u_view"),
|
||||||
u_texture: this.gl.getUniformLocation(renderChunksProgramId, "u_texture"),
|
u_texture: this.gl.getUniformLocation(renderChunksProgramId, "u_texture"),
|
||||||
|
u_visAtlasIndex: this.gl.getUniformLocation(renderChunksProgramId, "u_visAtlasIndex"),
|
||||||
ub_rects: this.gl.getUniformBlockIndex(renderChunksProgramId, "ub_rects"),
|
ub_rects: this.gl.getUniformBlockIndex(renderChunksProgramId, "ub_rects"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -191,60 +209,32 @@ class CanvasRenderer extends HTMLElement {
|
||||||
uboRects: this.uboRects,
|
uboRects: this.uboRects,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.atlasAllocator = new AtlasAllocator(this.wall.chunkSize, 8);
|
this.atlasAllocator = new AtlasAllocator(this.gl, this.wall.chunkSize, 8);
|
||||||
this.chunkAllocations = new Map();
|
|
||||||
|
|
||||||
console.debug("initialized atlas allocator", this.atlasAllocator);
|
console.debug("initialized atlas allocator", this.atlasAllocator);
|
||||||
|
|
||||||
this.batches = [];
|
this.batches = [];
|
||||||
this.batchPool = new Pool();
|
this.batchPool = new Pool();
|
||||||
|
|
||||||
|
this.brushRenderer = new BrushRenderer(this.gl, this.atlasAllocator.canvasSource());
|
||||||
|
|
||||||
console.debug("GL error state", this.gl.getError());
|
console.debug("GL error state", this.gl.getError());
|
||||||
|
|
||||||
console.groupEnd();
|
console.groupEnd();
|
||||||
}
|
|
||||||
|
|
||||||
#compileShader(kind, source) {
|
// Flag that prevents the renderer from exploding in case any part of
|
||||||
let shader = this.gl.createShader(kind);
|
// initialisation throws an exception.
|
||||||
|
this.ok = true;
|
||||||
this.gl.shaderSource(shader, source);
|
|
||||||
this.gl.compileShader(shader);
|
|
||||||
|
|
||||||
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
|
|
||||||
let error = new Error(`failed to compile shader: ${this.gl.getShaderInfoLog(shader)}`);
|
|
||||||
this.gl.deleteShader(shader);
|
|
||||||
throw error;
|
|
||||||
} else {
|
|
||||||
return shader;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#compileProgram(vertexSource, fragmentSource) {
|
|
||||||
let vertexShader = this.#compileShader(this.gl.VERTEX_SHADER, vertexSource);
|
|
||||||
let fragmentShader = this.#compileShader(this.gl.FRAGMENT_SHADER, fragmentSource);
|
|
||||||
|
|
||||||
let program = this.gl.createProgram();
|
|
||||||
this.gl.attachShader(program, vertexShader);
|
|
||||||
this.gl.attachShader(program, fragmentShader);
|
|
||||||
this.gl.linkProgram(program);
|
|
||||||
|
|
||||||
this.gl.deleteShader(vertexShader);
|
|
||||||
this.gl.deleteShader(fragmentShader);
|
|
||||||
|
|
||||||
if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
|
|
||||||
let error = new Error(`failed to link program: ${this.gl.getProgramInfoLog(program)}`);
|
|
||||||
this.gl.deleteProgram(program);
|
|
||||||
throw error;
|
|
||||||
} else {
|
|
||||||
return program;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renderer
|
// Renderer
|
||||||
|
|
||||||
#render() {
|
#render() {
|
||||||
|
if (!this.ok) return;
|
||||||
|
|
||||||
// NOTE: We should probably render on-demand only when it's needed.
|
// NOTE: We should probably render on-demand only when it's needed.
|
||||||
requestAnimationFrame(() => this.#render());
|
requestAnimationFrame(() => this.#render());
|
||||||
|
this.atlasAllocator.tickDownloads();
|
||||||
this.#renderWall();
|
this.#renderWall();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -255,6 +245,10 @@ class CanvasRenderer extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
|
this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
this.gl.scissor(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
|
||||||
|
this.gl.enable(this.gl.BLEND);
|
||||||
|
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA);
|
||||||
|
|
||||||
this.gl.clearColor(1, 1, 1, 1);
|
this.gl.clearColor(1, 1, 1, 1);
|
||||||
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
|
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
|
||||||
|
|
@ -298,16 +292,14 @@ class CanvasRenderer extends HTMLElement {
|
||||||
for (let batch of this.batches) {
|
for (let batch of this.batches) {
|
||||||
for (let [i, chunks] of batch) {
|
for (let [i, chunks] of batch) {
|
||||||
let atlas = this.atlasAllocator.atlases[i];
|
let atlas = this.atlasAllocator.atlases[i];
|
||||||
this.gl.bindTexture(this.gl.TEXTURE_2D, atlas.id);
|
this.gl.bindTexture(this.gl.TEXTURE_2D, atlas.texture);
|
||||||
|
// this.gl.uniform1i(this.renderChunksProgram.u_visAtlasIndex, i + 1);
|
||||||
|
|
||||||
this.#resetRectBuffer();
|
this.#resetRectBuffer();
|
||||||
for (let chunk of chunks) {
|
for (let chunk of chunks) {
|
||||||
let { i, allocation } = this.getChunkAllocation(
|
let atlasIndex = this.atlasAllocator.getAtlasIndex(chunk.id);
|
||||||
chunk.layerId,
|
let allocation = this.atlasAllocator.getAllocation(chunk.id);
|
||||||
chunk.x,
|
let atlas = this.atlasAllocator.atlases[atlasIndex];
|
||||||
chunk.y,
|
|
||||||
);
|
|
||||||
let atlas = this.atlasAllocator.atlases[i];
|
|
||||||
this.#pushRect(
|
this.#pushRect(
|
||||||
chunk.x * this.wall.chunkSize,
|
chunk.x * this.wall.chunkSize,
|
||||||
chunk.y * this.wall.chunkSize,
|
chunk.y * this.wall.chunkSize,
|
||||||
|
|
@ -330,7 +322,7 @@ class CanvasRenderer extends HTMLElement {
|
||||||
let y = 0;
|
let y = 0;
|
||||||
for (let atlas of this.atlasAllocator.atlases) {
|
for (let atlas of this.atlasAllocator.atlases) {
|
||||||
this.#resetRectBuffer();
|
this.#resetRectBuffer();
|
||||||
this.gl.bindTexture(this.gl.TEXTURE_2D, atlas.id);
|
this.gl.bindTexture(this.gl.TEXTURE_2D, atlas.texture);
|
||||||
this.#pushRect(x, y, atlas.textureSize, atlas.textureSize, 0, 0, 1, 1);
|
this.#pushRect(x, y, atlas.textureSize, atlas.textureSize, 0, 0, 1, 1);
|
||||||
this.#drawRects();
|
this.#drawRects();
|
||||||
if (x > atlas.textureSize * 16) {
|
if (x > atlas.textureSize * 16) {
|
||||||
|
|
@ -339,29 +331,7 @@ class CanvasRenderer extends HTMLElement {
|
||||||
}
|
}
|
||||||
x += atlas.textureSize;
|
x += atlas.textureSize;
|
||||||
}
|
}
|
||||||
*/
|
// */
|
||||||
}
|
|
||||||
|
|
||||||
getChunkAllocation(layerId, chunkX, chunkY) {
|
|
||||||
let key = `${layerId}/${chunkKey(chunkX, chunkY)}`;
|
|
||||||
if (this.chunkAllocations.has(key)) {
|
|
||||||
return this.chunkAllocations.get(key);
|
|
||||||
} else {
|
|
||||||
let allocation = this.atlasAllocator.alloc(this.gl);
|
|
||||||
this.chunkAllocations.set(key, allocation);
|
|
||||||
return allocation;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deallocateChunks(layer) {
|
|
||||||
for (let chunkKey of layer.chunks.keys()) {
|
|
||||||
let key = `${layer.id}/${chunkKey}`;
|
|
||||||
if (this.chunkAllocations.has(key)) {
|
|
||||||
let allocation = this.chunkAllocations.get(key);
|
|
||||||
this.atlasAllocator.dealloc(allocation);
|
|
||||||
this.chunkAllocations.delete(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#collectChunksThisFrame() {
|
#collectChunksThisFrame() {
|
||||||
|
|
@ -383,20 +353,14 @@ class CanvasRenderer extends HTMLElement {
|
||||||
for (let chunkX = left; chunkX < right; ++chunkX) {
|
for (let chunkX = left; chunkX < right; ++chunkX) {
|
||||||
let chunk = layer.getChunk(chunkX, chunkY);
|
let chunk = layer.getChunk(chunkX, chunkY);
|
||||||
if (chunk != null) {
|
if (chunk != null) {
|
||||||
if (chunk.renderDirty) {
|
let atlasIndex = this.atlasAllocator.getAtlasIndex(chunk.id);
|
||||||
this.#updateChunkTexture(layer, chunkX, chunkY);
|
let array = batch.get(atlasIndex);
|
||||||
chunk.renderDirty = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let allocation = this.getChunkAllocation(layer.id, chunkX, chunkY);
|
|
||||||
|
|
||||||
let array = batch.get(allocation.i);
|
|
||||||
if (array == null) {
|
if (array == null) {
|
||||||
array = [];
|
array = [];
|
||||||
batch.set(allocation.i, array);
|
batch.set(atlasIndex, array);
|
||||||
}
|
}
|
||||||
|
|
||||||
array.push({ layerId: layer.id, x: chunkX, y: chunkY });
|
array.push({ layerId: layer.id, x: chunkX, y: chunkY, id: chunk.id });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -438,12 +402,6 @@ class CanvasRenderer extends HTMLElement {
|
||||||
this.gl.drawArraysInstanced(this.gl.TRIANGLES, 0, 6, this.uboRectsNum);
|
this.gl.drawArraysInstanced(this.gl.TRIANGLES, 0, 6, this.uboRectsNum);
|
||||||
}
|
}
|
||||||
|
|
||||||
#updateChunkTexture(layer, chunkX, chunkY) {
|
|
||||||
let allocation = this.getChunkAllocation(layer.id, chunkX, chunkY);
|
|
||||||
let chunk = layer.getChunk(chunkX, chunkY);
|
|
||||||
this.atlasAllocator.upload(this.gl, allocation, chunk.pixmap);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Behaviours
|
// Behaviours
|
||||||
|
|
||||||
sendViewportUpdate() {
|
sendViewportUpdate() {
|
||||||
|
|
@ -584,101 +542,3 @@ class InteractEvent extends Event {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Atlas {
|
|
||||||
static getInitBuffer(chunkSize, nChunks) {
|
|
||||||
let imageSize = chunkSize * nChunks;
|
|
||||||
return new Uint8Array(imageSize * imageSize * 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(gl, chunkSize, nChunks, initBuffer) {
|
|
||||||
this.id = gl.createTexture();
|
|
||||||
this.chunkSize = chunkSize;
|
|
||||||
this.nChunks = nChunks;
|
|
||||||
this.textureSize = chunkSize * nChunks;
|
|
||||||
|
|
||||||
this.free = Array(nChunks * nChunks);
|
|
||||||
for (let y = 0; y < nChunks; ++y) {
|
|
||||||
for (let x = 0; x < nChunks; ++x) {
|
|
||||||
this.free[x + y * nChunks] = { x, y };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
gl.bindTexture(gl.TEXTURE_2D, this.id);
|
|
||||||
gl.texImage2D(
|
|
||||||
gl.TEXTURE_2D,
|
|
||||||
0,
|
|
||||||
gl.RGBA8,
|
|
||||||
this.textureSize,
|
|
||||||
this.textureSize,
|
|
||||||
0,
|
|
||||||
gl.RGBA,
|
|
||||||
gl.UNSIGNED_BYTE,
|
|
||||||
initBuffer,
|
|
||||||
);
|
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
||||||
}
|
|
||||||
|
|
||||||
alloc() {
|
|
||||||
return this.free.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
dealloc(xy) {
|
|
||||||
this.free.push(xy);
|
|
||||||
}
|
|
||||||
|
|
||||||
upload(gl, { x, y }, pixmap) {
|
|
||||||
gl.bindTexture(gl.TEXTURE_2D, this.id);
|
|
||||||
gl.texSubImage2D(
|
|
||||||
gl.TEXTURE_2D,
|
|
||||||
0,
|
|
||||||
x * this.chunkSize,
|
|
||||||
y * this.chunkSize,
|
|
||||||
this.chunkSize,
|
|
||||||
this.chunkSize,
|
|
||||||
gl.RGBA,
|
|
||||||
gl.UNSIGNED_BYTE,
|
|
||||||
pixmap.getArrayBuffer(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AtlasAllocator {
|
|
||||||
atlases = [];
|
|
||||||
|
|
||||||
constructor(chunkSize, nChunks) {
|
|
||||||
this.chunkSize = chunkSize;
|
|
||||||
this.nChunks = nChunks;
|
|
||||||
this.initBuffer = Atlas.getInitBuffer(chunkSize, nChunks);
|
|
||||||
}
|
|
||||||
|
|
||||||
alloc(gl) {
|
|
||||||
// Right now we do a dumb linear scan through all atlases, but in the future it would be
|
|
||||||
// really nice to optimize this by storing information about which atlases have free slots
|
|
||||||
// precisely.
|
|
||||||
|
|
||||||
for (let i = 0; i < this.atlases.length; ++i) {
|
|
||||||
let atlas = this.atlases[i];
|
|
||||||
let allocation = atlas.alloc();
|
|
||||||
if (allocation != null) {
|
|
||||||
return { i, allocation };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let i = this.atlases.length;
|
|
||||||
let atlas = new Atlas(gl, this.chunkSize, this.nChunks, this.initBuffer);
|
|
||||||
let allocation = atlas.alloc();
|
|
||||||
this.atlases.push(atlas);
|
|
||||||
return { i, allocation };
|
|
||||||
}
|
|
||||||
|
|
||||||
dealloc({ i, allocation }) {
|
|
||||||
let atlas = this.atlases[i];
|
|
||||||
atlas.dealloc(allocation);
|
|
||||||
}
|
|
||||||
|
|
||||||
upload(gl, { i, allocation }, pixmap) {
|
|
||||||
this.atlases[i].upload(gl, allocation, pixmap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
446
static/chunk-allocator.js
Normal file
446
static/chunk-allocator.js
Normal file
|
|
@ -0,0 +1,446 @@
|
||||||
|
import { compileProgram } from "rkgk/webgl.js";
|
||||||
|
|
||||||
|
class Atlas {
|
||||||
|
static getInitBuffer(chunkSize, nChunks) {
|
||||||
|
let imageSize = chunkSize * nChunks;
|
||||||
|
return new Uint8Array(imageSize * imageSize * 4).fill(0x00);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(gl, chunkSize, nChunks, initBuffer) {
|
||||||
|
this.chunkSize = chunkSize;
|
||||||
|
this.nChunks = nChunks;
|
||||||
|
this.textureSize = chunkSize * nChunks;
|
||||||
|
|
||||||
|
this.free = Array(nChunks * nChunks);
|
||||||
|
for (let y = 0; y < nChunks; ++y) {
|
||||||
|
for (let x = 0; x < nChunks; ++x) {
|
||||||
|
this.free[x + y * nChunks] = { x, y };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.texture = gl.createTexture();
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, this.texture);
|
||||||
|
gl.texImage2D(
|
||||||
|
gl.TEXTURE_2D,
|
||||||
|
0,
|
||||||
|
gl.RGBA8,
|
||||||
|
this.textureSize,
|
||||||
|
this.textureSize,
|
||||||
|
0,
|
||||||
|
gl.RGBA,
|
||||||
|
gl.UNSIGNED_BYTE,
|
||||||
|
initBuffer,
|
||||||
|
);
|
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||||||
|
|
||||||
|
this.framebuffer = gl.createFramebuffer();
|
||||||
|
gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
|
||||||
|
gl.framebufferTexture2D(
|
||||||
|
gl.FRAMEBUFFER,
|
||||||
|
gl.COLOR_ATTACHMENT0,
|
||||||
|
gl.TEXTURE_2D,
|
||||||
|
this.texture,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
alloc(gl, initBuffer) {
|
||||||
|
let xy = this.free.pop();
|
||||||
|
if (xy != null) {
|
||||||
|
this.upload(gl, xy, initBuffer);
|
||||||
|
}
|
||||||
|
return xy;
|
||||||
|
}
|
||||||
|
|
||||||
|
dealloc(xy) {
|
||||||
|
this.free.push(xy);
|
||||||
|
}
|
||||||
|
|
||||||
|
upload(gl, { x, y }, source) {
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, this.texture);
|
||||||
|
gl.texSubImage2D(
|
||||||
|
gl.TEXTURE_2D,
|
||||||
|
0,
|
||||||
|
x * this.chunkSize,
|
||||||
|
y * this.chunkSize,
|
||||||
|
this.chunkSize,
|
||||||
|
this.chunkSize,
|
||||||
|
gl.RGBA,
|
||||||
|
gl.UNSIGNED_BYTE,
|
||||||
|
source,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assumes a pack PBO is bound.
|
||||||
|
download(gl, { x, y }) {
|
||||||
|
gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
|
||||||
|
gl.readPixels(
|
||||||
|
x * this.chunkSize,
|
||||||
|
y * this.chunkSize,
|
||||||
|
this.chunkSize,
|
||||||
|
this.chunkSize,
|
||||||
|
gl.RGBA,
|
||||||
|
gl.UNSIGNED_BYTE,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
getFramebufferRect({ x, y }) {
|
||||||
|
return {
|
||||||
|
x: x * this.chunkSize,
|
||||||
|
y: y * this.chunkSize,
|
||||||
|
width: this.chunkSize,
|
||||||
|
height: this.chunkSize,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const compositeVertexShader = `#version 300 es
|
||||||
|
precision highp float;
|
||||||
|
|
||||||
|
layout (location = 0) in vec2 a_position;
|
||||||
|
layout (location = 1) in vec2 a_uv;
|
||||||
|
|
||||||
|
out vec2 vf_uv;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
gl_Position = vec4(a_position, 0.0, 1.0);
|
||||||
|
vf_uv = a_uv;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const compositeFragmentShader = `#version 300 es
|
||||||
|
precision highp float;
|
||||||
|
|
||||||
|
uniform sampler2D u_chunk;
|
||||||
|
|
||||||
|
in vec2 vf_uv;
|
||||||
|
|
||||||
|
out vec4 f_color;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
f_color = texture(u_chunk, vf_uv);
|
||||||
|
// f_color = vec4(vec3(0.0), 1.0);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export class AtlasAllocator {
|
||||||
|
atlases = [];
|
||||||
|
|
||||||
|
// Allocation names
|
||||||
|
#ids = new Map();
|
||||||
|
#idCounter = 1;
|
||||||
|
|
||||||
|
// Download buffers
|
||||||
|
#pboPool = [];
|
||||||
|
#downloadBufferPool = [];
|
||||||
|
#pendingDownloads = [];
|
||||||
|
|
||||||
|
constructor(gl, chunkSize, nChunks) {
|
||||||
|
this.gl = gl;
|
||||||
|
this.chunkSize = chunkSize;
|
||||||
|
this.nChunks = nChunks;
|
||||||
|
this.initBuffer = Atlas.getInitBuffer(chunkSize, nChunks);
|
||||||
|
|
||||||
|
// Compositing pipeline
|
||||||
|
|
||||||
|
let compositeProgramId = compileProgram(gl, compositeVertexShader, compositeFragmentShader);
|
||||||
|
this.compositeProgram = {
|
||||||
|
id: compositeProgramId,
|
||||||
|
u_chunk: gl.getUniformLocation(compositeProgramId, "u_chunk"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
this.compositeRectData = new Float32Array([
|
||||||
|
// a_position
|
||||||
|
-1, 1, // 0: top left
|
||||||
|
1, 1, // 1: top right
|
||||||
|
1, -1, // 2: bottom right
|
||||||
|
-1, -1, // 3: bottom left
|
||||||
|
|
||||||
|
// a_uv - filled out later when compositing
|
||||||
|
0, 0,
|
||||||
|
0, 0,
|
||||||
|
0, 0,
|
||||||
|
0, 0,
|
||||||
|
]);
|
||||||
|
let compositeRectIndices = new Uint16Array([0, 1, 2, 2, 3, 0]);
|
||||||
|
this.compositeRectUv = this.compositeRectData.subarray(8);
|
||||||
|
|
||||||
|
this.compositeRectVao = gl.createVertexArray();
|
||||||
|
this.compositeRectVbo = gl.createBuffer();
|
||||||
|
this.compositeRectIbo = gl.createBuffer();
|
||||||
|
gl.bindVertexArray(this.compositeRectVao);
|
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.compositeRectVbo);
|
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, this.compositeRectData, gl.DYNAMIC_DRAW);
|
||||||
|
console.log(this.compositeRectData.byteLength);
|
||||||
|
|
||||||
|
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.compositeRectIbo);
|
||||||
|
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, compositeRectIndices, gl.DYNAMIC_DRAW);
|
||||||
|
|
||||||
|
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 2 * 4, 0);
|
||||||
|
gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 2 * 4, this.compositeRectUv.byteOffset);
|
||||||
|
for (let i = 0; i < 2; ++i) gl.enableVertexAttribArray(i);
|
||||||
|
|
||||||
|
this.compositeTexture = gl.createTexture();
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, this.compositeTexture);
|
||||||
|
gl.texImage2D(
|
||||||
|
gl.TEXTURE_2D,
|
||||||
|
0,
|
||||||
|
gl.RGBA8,
|
||||||
|
chunkSize,
|
||||||
|
chunkSize,
|
||||||
|
0,
|
||||||
|
gl.RGBA,
|
||||||
|
gl.UNSIGNED_BYTE,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||||
|
|
||||||
|
this.compositeFramebuffer = gl.createFramebuffer();
|
||||||
|
gl.bindFramebuffer(gl.FRAMEBUFFER, this.compositeFramebuffer);
|
||||||
|
gl.framebufferTexture2D(
|
||||||
|
gl.FRAMEBUFFER,
|
||||||
|
gl.COLOR_ATTACHMENT0,
|
||||||
|
gl.TEXTURE_2D,
|
||||||
|
this.compositeTexture,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
#obtainId(allocInfo) {
|
||||||
|
let id = this.#idCounter++;
|
||||||
|
this.#ids.set(id, allocInfo);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
#releaseId(id) {
|
||||||
|
this.#ids.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#getAllocInfo(id) {
|
||||||
|
return this.#ids.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAtlasIndex(id) {
|
||||||
|
return this.#getAllocInfo(id).i;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllocation(id) {
|
||||||
|
return this.#getAllocInfo(id).allocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
alloc() {
|
||||||
|
// Right now we do a dumb linear scan through all atlases, but in the future it would be
|
||||||
|
// really nice to optimize this by storing information about which atlases have free slots
|
||||||
|
// precisely.
|
||||||
|
|
||||||
|
for (let i = 0; i < this.atlases.length; ++i) {
|
||||||
|
let atlas = this.atlases[i];
|
||||||
|
let allocation = atlas.alloc(this.gl, this.initBuffer);
|
||||||
|
if (allocation != null) {
|
||||||
|
return this.#obtainId({ i, allocation });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let i = this.atlases.length;
|
||||||
|
let atlas = new Atlas(this.gl, this.chunkSize, this.nChunks, this.initBuffer);
|
||||||
|
let allocation = atlas.alloc(this.gl, this.initBuffer);
|
||||||
|
this.atlases.push(atlas);
|
||||||
|
|
||||||
|
return this.#obtainId({ i, allocation });
|
||||||
|
}
|
||||||
|
|
||||||
|
dealloc(id) {
|
||||||
|
let { i, allocation } = this.#getAllocInfo(id);
|
||||||
|
let atlas = this.atlases[i];
|
||||||
|
atlas.dealloc(allocation);
|
||||||
|
this.#releaseId(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
upload(id, source) {
|
||||||
|
let { i, allocation } = this.#getAllocInfo(id);
|
||||||
|
this.atlases[i].upload(this.gl, allocation, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
async download(id) {
|
||||||
|
let gl = this.gl;
|
||||||
|
let allocInfo = this.#getAllocInfo(id);
|
||||||
|
|
||||||
|
// Get PBO
|
||||||
|
|
||||||
|
let pbo = this.#pboPool.pop();
|
||||||
|
if (pbo == null) {
|
||||||
|
let dataSize = this.chunkSize * this.chunkSize * 4;
|
||||||
|
pbo = gl.createBuffer();
|
||||||
|
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbo);
|
||||||
|
gl.bufferData(gl.PIXEL_PACK_BUFFER, dataSize, gl.DYNAMIC_READ);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initiate download
|
||||||
|
|
||||||
|
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbo);
|
||||||
|
this.atlases[allocInfo.i].download(gl, allocInfo.allocation);
|
||||||
|
let fence = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||||
|
|
||||||
|
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
|
||||||
|
|
||||||
|
// Add for ticking
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.#pendingDownloads.push({ pbo, fence, resolve });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call after download() finishes running to give memory back to the allocator, for reuse in
|
||||||
|
// later pixel transfers.
|
||||||
|
freeDownload(arrayBuffer) {
|
||||||
|
this.#downloadBufferPool.push(arrayBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call every frame to poll for download completion.
|
||||||
|
tickDownloads() {
|
||||||
|
if (this.#pendingDownloads.length == 0) return;
|
||||||
|
|
||||||
|
let gl = this.gl;
|
||||||
|
|
||||||
|
for (let i = 0; i < this.#pendingDownloads.length; ++i) {
|
||||||
|
let pending = this.#pendingDownloads[i];
|
||||||
|
let status = gl.getSyncParameter(pending.fence, gl.SYNC_STATUS);
|
||||||
|
if (status == gl.SIGNALED) {
|
||||||
|
// Transfer complete, fetch pixels back to an array buffer.
|
||||||
|
let dataSize = this.chunkSize * this.chunkSize * 4;
|
||||||
|
let arrayBuffer = this.#downloadBufferPool.pop() ?? new ArrayBuffer(dataSize);
|
||||||
|
|
||||||
|
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pending.pbo);
|
||||||
|
gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, new Uint8Array(arrayBuffer));
|
||||||
|
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
|
||||||
|
gl.deleteSync(pending.fence);
|
||||||
|
|
||||||
|
pending.resolve({
|
||||||
|
width: this.chunkSize,
|
||||||
|
height: this.chunkSize,
|
||||||
|
data: arrayBuffer,
|
||||||
|
});
|
||||||
|
|
||||||
|
let last = this.#pendingDownloads.pop();
|
||||||
|
if (this.#pendingDownloads.length > 0) {
|
||||||
|
this.#pendingDownloads[i] = last;
|
||||||
|
--i;
|
||||||
|
} else {
|
||||||
|
break; // now empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
canvasSource() {
|
||||||
|
let useCanvas = (gl, id) => {
|
||||||
|
let allocInfo = this.#getAllocInfo(id);
|
||||||
|
let atlas = this.atlases[allocInfo.i];
|
||||||
|
|
||||||
|
let viewport = atlas.getFramebufferRect(allocInfo.allocation);
|
||||||
|
|
||||||
|
gl.enable(gl.SCISSOR_TEST);
|
||||||
|
gl.bindFramebuffer(gl.FRAMEBUFFER, atlas.framebuffer);
|
||||||
|
gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height);
|
||||||
|
gl.scissor(viewport.x, viewport.y, viewport.width, viewport.height);
|
||||||
|
|
||||||
|
return viewport;
|
||||||
|
};
|
||||||
|
|
||||||
|
let resetCanvas = (gl) => {
|
||||||
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||||||
|
gl.disable(gl.SCISSOR_TEST);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
useCanvas,
|
||||||
|
resetCanvas,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: I was thinking a bit about whether the chunk allocator is the right place to put
|
||||||
|
// compositing operations like this. After much consideration, I've decided that it's pretty
|
||||||
|
// much the only sensible place, because it's the only place concerned with the layout of
|
||||||
|
// chunks in memory, and rendering of laid out chunks is quite implementation dependent.
|
||||||
|
//
|
||||||
|
// This does break the purity of the "allocator" role a bit though, but I don't really know if
|
||||||
|
// there's a good way around that.
|
||||||
|
//
|
||||||
|
// Maybe. But I don't feel like breaking this apart to 10 smaller classes, either.
|
||||||
|
|
||||||
|
#drawComposite(u, v, uvScale) {
|
||||||
|
// Assumes bound source texture, destination framebuffer, and viewport set.
|
||||||
|
|
||||||
|
let gl = this.gl;
|
||||||
|
|
||||||
|
gl.bindVertexArray(this.compositeRectVao);
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.compositeRectVbo);
|
||||||
|
gl.useProgram(this.compositeProgram.id);
|
||||||
|
|
||||||
|
gl.uniform1i(this.compositeProgram.u_chunk, 0);
|
||||||
|
|
||||||
|
let uv = this.compositeRectUv;
|
||||||
|
uv[0] = u * uvScale;
|
||||||
|
uv[1] = v * uvScale;
|
||||||
|
uv[2] = (u + 1) * uvScale;
|
||||||
|
uv[3] = v * uvScale;
|
||||||
|
uv[4] = (u + 1) * uvScale;
|
||||||
|
uv[5] = (v + 1) * uvScale;
|
||||||
|
uv[6] = u * uvScale;
|
||||||
|
uv[7] = (v + 1) * uvScale;
|
||||||
|
gl.bufferSubData(gl.ARRAY_BUFFER, uv.byteOffset, uv);
|
||||||
|
|
||||||
|
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
composite(dstId, srcId, op) {
|
||||||
|
// NOTE: This has to go through an intermediate buffer in case the source and destination
|
||||||
|
// atlas are the same.
|
||||||
|
|
||||||
|
console.assert(op == "alphaBlend", "composite operation must be alphaBlend");
|
||||||
|
|
||||||
|
let gl = this.gl;
|
||||||
|
|
||||||
|
let dstAtlas = this.atlases[this.getAtlasIndex(dstId)];
|
||||||
|
let srcAtlas = this.atlases[this.getAtlasIndex(srcId)];
|
||||||
|
let dstAllocation = this.getAllocation(dstId);
|
||||||
|
let srcAllocation = this.getAllocation(srcId);
|
||||||
|
|
||||||
|
// Source -> intermediate buffer
|
||||||
|
|
||||||
|
gl.disable(gl.BLEND);
|
||||||
|
gl.disable(gl.SCISSOR_TEST);
|
||||||
|
|
||||||
|
gl.bindFramebuffer(gl.FRAMEBUFFER, this.compositeFramebuffer);
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, srcAtlas.texture);
|
||||||
|
gl.viewport(0, 0, this.chunkSize, this.chunkSize);
|
||||||
|
this.#drawComposite(srcAllocation.x, srcAllocation.y, 1 / this.nChunks);
|
||||||
|
|
||||||
|
// Intermediate buffer -> destination
|
||||||
|
|
||||||
|
gl.enable(gl.BLEND);
|
||||||
|
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
||||||
|
|
||||||
|
gl.bindFramebuffer(gl.FRAMEBUFFER, dstAtlas.framebuffer);
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, this.compositeTexture);
|
||||||
|
gl.viewport(
|
||||||
|
dstAllocation.x * this.chunkSize,
|
||||||
|
dstAllocation.y * this.chunkSize,
|
||||||
|
this.chunkSize,
|
||||||
|
this.chunkSize,
|
||||||
|
);
|
||||||
|
this.#drawComposite(0, 0, 1);
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
|
||||||
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -36,11 +36,14 @@ export class ConnectionStatus extends HTMLElement {
|
||||||
|
|
||||||
showError(error) {
|
showError(error) {
|
||||||
this.errorDialog.showModal();
|
this.errorDialog.showModal();
|
||||||
|
if (typeof error.error == "string") {
|
||||||
|
this.errorText.value = error.error.toString();
|
||||||
|
}
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
if (error.stack != null && error.stack != "") {
|
if (error.stack != null && error.stack != "") {
|
||||||
this.errorText.textContent = `${error.toString()}\n\n${error.stack}`;
|
this.errorText.value = `${error.toString()}\n\n${error.stack}`;
|
||||||
} else {
|
} else {
|
||||||
this.errorText.textContent = error.toString();
|
this.errorText.value = error.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,6 @@
|
||||||
let panicImpl;
|
let panicImpl;
|
||||||
let logImpl, log2Impl;
|
let logImpl, log2Impl;
|
||||||
let canvasBeginImpl,
|
let currentBrushRenderer;
|
||||||
canvasLineImpl,
|
|
||||||
canvasRectangleImpl,
|
|
||||||
canvasCircleImpl,
|
|
||||||
canvasFillImpl,
|
|
||||||
canvasStrokeImpl;
|
|
||||||
|
|
||||||
function allocCheck(p) {
|
function allocCheck(p) {
|
||||||
if (p == 0) throw new Error("out of memory");
|
if (p == 0) throw new Error("out of memory");
|
||||||
|
|
@ -47,14 +42,8 @@ let [hakuWasm, haku2Wasm] = await Promise.all([
|
||||||
__haku2_log_info: makeLogFunction2("info"),
|
__haku2_log_info: makeLogFunction2("info"),
|
||||||
__haku2_log_debug: makeLogFunction2("debug"),
|
__haku2_log_debug: makeLogFunction2("debug"),
|
||||||
|
|
||||||
__haku2_canvas_begin: (c) => canvasBeginImpl(c),
|
__haku2_canvas_stroke: (c, r, g, b, a, thickness, x1, y1, x2, y2) =>
|
||||||
__haku2_canvas_line: (c, x1, y1, x2, y2) => canvasLineImpl(c, x1, y1, x2, y2),
|
currentBrushRenderer.stroke(c, r, g, b, a, thickness, x1, y1, x2, y2),
|
||||||
__haku2_canvas_rectangle: (c, x, y, width, height) =>
|
|
||||||
canvasRectangleImpl(c, x, y, width, height),
|
|
||||||
__haku2_canvas_circle: (c, x, y, r) => canvasCircleImpl(c, x, y, r),
|
|
||||||
__haku2_canvas_fill: (c, r, g, b, a) => canvasFillImpl(c, r, g, b, a),
|
|
||||||
__haku2_canvas_stroke: (c, r, g, b, a, thickness) =>
|
|
||||||
canvasStrokeImpl(c, r, g, b, a, thickness),
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
@ -151,49 +140,8 @@ log2Impl = (level, pScope, scopeLen, pMsg, len) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
canvasBeginImpl = w.haku_pixmap_begin;
|
|
||||||
canvasLineImpl = w.haku_pixmap_line;
|
|
||||||
canvasRectangleImpl = w.haku_pixmap_rectangle;
|
|
||||||
canvasCircleImpl = w.haku_pixmap_circle;
|
|
||||||
canvasFillImpl = w.haku_pixmap_fill;
|
|
||||||
canvasStrokeImpl = w.haku_pixmap_stroke;
|
|
||||||
|
|
||||||
w.haku_init_logging();
|
w.haku_init_logging();
|
||||||
|
|
||||||
export class Pixmap {
|
|
||||||
#pPixmap = 0;
|
|
||||||
|
|
||||||
constructor(width, height) {
|
|
||||||
this.#pPixmap = allocCheck(w.haku_pixmap_new(width, height));
|
|
||||||
this.width = width;
|
|
||||||
this.height = height;
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
w.haku_pixmap_destroy(this.#pPixmap);
|
|
||||||
}
|
|
||||||
|
|
||||||
clear(r, g, b, a) {
|
|
||||||
w.haku_pixmap_clear(this.#pPixmap, r, g, b, a);
|
|
||||||
}
|
|
||||||
|
|
||||||
get ptr() {
|
|
||||||
return this.#pPixmap;
|
|
||||||
}
|
|
||||||
|
|
||||||
getArrayBuffer() {
|
|
||||||
return new Uint8ClampedArray(
|
|
||||||
memory.buffer,
|
|
||||||
w.haku_pixmap_data(this.#pPixmap),
|
|
||||||
this.width * this.height * 4,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getImageData() {
|
|
||||||
return new ImageData(this.getArrayBuffer(), this.width, this.height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ContKind = {
|
export const ContKind = {
|
||||||
Scribble: 0,
|
Scribble: 0,
|
||||||
Dotter: 1,
|
Dotter: 1,
|
||||||
|
|
@ -420,9 +368,12 @@ export class Haku {
|
||||||
else return ContKind.Scribble;
|
else return ContKind.Scribble;
|
||||||
}
|
}
|
||||||
|
|
||||||
contScribble(pixmap, translationX, translationY) {
|
contScribble(renderer, canvas) {
|
||||||
w.haku_pixmap_set_translation(pixmap.ptr, translationX, translationY);
|
console.assert(currentBrushRenderer == null);
|
||||||
let ok = w2.haku2_render(this.#pVm2, pixmap.ptr, this.#renderMaxDepth);
|
currentBrushRenderer = renderer;
|
||||||
|
let ok = w2.haku2_render(this.#pVm2, canvas, this.#renderMaxDepth);
|
||||||
|
currentBrushRenderer = null;
|
||||||
|
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
return this.#exceptionResult();
|
return this.#exceptionResult();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -449,8 +400,8 @@ export class Haku {
|
||||||
while (true) {
|
while (true) {
|
||||||
switch (this.expectedContKind()) {
|
switch (this.expectedContKind()) {
|
||||||
case ContKind.Scribble:
|
case ContKind.Scribble:
|
||||||
result = await runScribble((pixmap, translationX, translationY) => {
|
result = await runScribble((renderer, canvas, translationX, translationY) => {
|
||||||
return this.contScribble(pixmap, translationX, translationY);
|
return this.contScribble(renderer, canvas, translationX, translationY);
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -283,6 +283,8 @@ rkgk-reticle-cursor {
|
||||||
rkgk-brush-box {
|
rkgk-brush-box {
|
||||||
--button-size: 56px;
|
--button-size: 56px;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
height: var(--height);
|
height: var(--height);
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
|
||||||
|
|
@ -316,7 +318,7 @@ rkgk-brush-box {
|
||||||
border-color: var(--color-brand-blue);
|
border-color: var(--color-brand-blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
& > rkgk-brush-preview {
|
& > .preview {
|
||||||
width: var(--button-size);
|
width: var(--button-size);
|
||||||
aspect-ratio: 1 / 1;
|
aspect-ratio: 1 / 1;
|
||||||
background: none;
|
background: none;
|
||||||
|
|
@ -352,6 +354,18 @@ rkgk-brush-box {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& > canvas {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
margin: 12px;
|
||||||
|
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Code editor */
|
/* Code editor */
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,10 @@ function readUrl(urlString) {
|
||||||
},
|
},
|
||||||
|
|
||||||
async onDisconnect() {
|
async onDisconnect() {
|
||||||
|
if (session.errored) return; // Display the error screen
|
||||||
|
|
||||||
|
console.info("showing disconnected refresh screen");
|
||||||
|
|
||||||
let duration = 5000 + Math.random() * 1000;
|
let duration = 5000 + Math.random() * 1000;
|
||||||
while (true) {
|
while (true) {
|
||||||
console.info("waiting a bit for the server to come back up", duration);
|
console.info("waiting a bit for the server to come back up", duration);
|
||||||
|
|
@ -146,6 +150,8 @@ function readUrl(urlString) {
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentUser = wall.onlineUsers.getUser(session.sessionId);
|
let currentUser = wall.onlineUsers.getUser(session.sessionId);
|
||||||
|
let chunkAllocator = canvasRenderer.atlasAllocator;
|
||||||
|
let brushRenderer = canvasRenderer.brushRenderer;
|
||||||
|
|
||||||
// Event loop
|
// Event loop
|
||||||
|
|
||||||
|
|
@ -187,7 +193,7 @@ function readUrl(urlString) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wallEvent.kind.event == "interact") {
|
if (wallEvent.kind.event == "interact") {
|
||||||
user.simulate(wall, wallEvent.kind.interactions);
|
user.simulate(chunkAllocator, brushRenderer, wall, wallEvent.kind.interactions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -214,15 +220,13 @@ function readUrl(urlString) {
|
||||||
updatePromises.push(
|
updatePromises.push(
|
||||||
createImageBitmap(blob).then((bitmap) => {
|
createImageBitmap(blob).then((bitmap) => {
|
||||||
let chunk = wall.mainLayer.getOrCreateChunk(
|
let chunk = wall.mainLayer.getOrCreateChunk(
|
||||||
|
chunkAllocator,
|
||||||
info.position.x,
|
info.position.x,
|
||||||
info.position.y,
|
info.position.y,
|
||||||
);
|
);
|
||||||
if (chunk == null) return;
|
if (chunk == null) return;
|
||||||
|
|
||||||
chunk.ctx.globalCompositeOperation = "copy";
|
chunk.upload(chunkAllocator, bitmap);
|
||||||
chunk.ctx.drawImage(bitmap, 0, 0);
|
|
||||||
chunk.syncToPixmap();
|
|
||||||
chunk.markModified();
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -255,18 +259,17 @@ function readUrl(urlString) {
|
||||||
|
|
||||||
let layer = currentUser.getScratchLayer(wall);
|
let layer = currentUser.getScratchLayer(wall);
|
||||||
let result = await currentUser.haku.evalBrush(
|
let result = await currentUser.haku.evalBrush(
|
||||||
selfController(interactionQueue, wall, layer, event),
|
selfController(interactionQueue, chunkAllocator, brushRenderer, wall, layer, event),
|
||||||
);
|
);
|
||||||
brushEditor.renderHakuResult(result);
|
brushEditor.renderHakuResult(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
canvasRenderer.addEventListener(".commitInteraction", async () => {
|
canvasRenderer.addEventListener(".commitInteraction", async () => {
|
||||||
let scratchLayer = currentUser.commitScratchLayer(wall);
|
let scratchLayer = currentUser.commitScratchLayer(chunkAllocator, wall);
|
||||||
if (scratchLayer == null) return;
|
if (scratchLayer == null) return;
|
||||||
|
|
||||||
canvasRenderer.deallocateChunks(scratchLayer);
|
let edits = await scratchLayer.toEdits(chunkAllocator);
|
||||||
let edits = await scratchLayer.toEdits();
|
scratchLayer.destroy(chunkAllocator);
|
||||||
scratchLayer.destroy();
|
|
||||||
|
|
||||||
let editRecords = [];
|
let editRecords = [];
|
||||||
let dataParts = [];
|
let dataParts = [];
|
||||||
|
|
@ -282,7 +285,10 @@ function readUrl(urlString) {
|
||||||
cursor += edit.data.size;
|
cursor += edit.data.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
session.sendEdit(editRecords, new Blob(dataParts));
|
let data = new Blob(dataParts);
|
||||||
|
|
||||||
|
console.log("sending edit data. record count:", editRecords.length, "data blob:", data);
|
||||||
|
session.sendEdit(editRecords, data);
|
||||||
});
|
});
|
||||||
|
|
||||||
canvasRenderer.addEventListener(".viewportUpdate", () => reticleRenderer.render());
|
canvasRenderer.addEventListener(".viewportUpdate", () => reticleRenderer.render());
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ export class User {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
simulate(wall, interactions) {
|
simulate(chunkAllocator, brushRenderer, wall, interactions) {
|
||||||
console.group("simulate", this.nickname);
|
console.group("simulate", this.nickname);
|
||||||
for (let interaction of interactions) {
|
for (let interaction of interactions) {
|
||||||
if (interaction.kind == "setBrush") {
|
if (interaction.kind == "setBrush") {
|
||||||
|
|
@ -71,10 +71,17 @@ export class User {
|
||||||
|
|
||||||
if (interaction.kind == "scribble" && this.#expectContKind(ContKind.Scribble)) {
|
if (interaction.kind == "scribble" && this.#expectContKind(ContKind.Scribble)) {
|
||||||
renderToChunksInArea(
|
renderToChunksInArea(
|
||||||
|
chunkAllocator,
|
||||||
|
brushRenderer,
|
||||||
this.getScratchLayer(wall),
|
this.getScratchLayer(wall),
|
||||||
this.simulation.renderArea,
|
this.simulation.renderArea,
|
||||||
(pixmap, translationX, translationY) => {
|
(brushRenderer, canvas, translationX, translationY) => {
|
||||||
return this.haku.contScribble(pixmap, translationX, translationY);
|
return this.haku.contScribble(
|
||||||
|
brushRenderer,
|
||||||
|
canvas,
|
||||||
|
translationX,
|
||||||
|
translationY,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
console.info("ended simulation");
|
console.info("ended simulation");
|
||||||
|
|
@ -121,9 +128,9 @@ export class User {
|
||||||
// Returns the scratch layer committed to the wall, so that the caller may do additional
|
// Returns the scratch layer committed to the wall, so that the caller may do additional
|
||||||
// processing with the completed layer (i.e. send to the server.)
|
// processing with the completed layer (i.e. send to the server.)
|
||||||
// The layer has to be .destroy()ed once you're done working with it.
|
// The layer has to be .destroy()ed once you're done working with it.
|
||||||
commitScratchLayer(wall) {
|
commitScratchLayer(chunkAllocator, wall) {
|
||||||
if (this.scratchLayer != null) {
|
if (this.scratchLayer != null) {
|
||||||
wall.mainLayer.compositeAlpha(this.scratchLayer);
|
wall.mainLayer.composite(chunkAllocator, this.scratchLayer, "alphaBlend");
|
||||||
wall.removeLayer(this.scratchLayer);
|
wall.removeLayer(this.scratchLayer);
|
||||||
let scratchLayer = this.scratchLayer;
|
let scratchLayer = this.scratchLayer;
|
||||||
this.scratchLayer = null;
|
this.scratchLayer = null;
|
||||||
|
|
|
||||||
|
|
@ -22,15 +22,21 @@ function* chunksInRectangle(rect, chunkSize) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderToChunksInArea(layer, renderArea, renderToPixmap) {
|
export function renderToChunksInArea(
|
||||||
|
chunkAllocator,
|
||||||
|
brushRenderer,
|
||||||
|
layer,
|
||||||
|
renderArea,
|
||||||
|
renderToCanvas,
|
||||||
|
) {
|
||||||
for (let [chunkX, chunkY] of chunksInRectangle(renderArea, layer.chunkSize)) {
|
for (let [chunkX, chunkY] of chunksInRectangle(renderArea, layer.chunkSize)) {
|
||||||
let chunk = layer.getOrCreateChunk(chunkX, chunkY);
|
let chunk = layer.getOrCreateChunk(chunkAllocator, chunkX, chunkY);
|
||||||
if (chunk == null) continue;
|
if (chunk == null) continue;
|
||||||
|
|
||||||
let translationX = -chunkX * layer.chunkSize;
|
let translationX = -chunkX * layer.chunkSize;
|
||||||
let translationY = -chunkY * layer.chunkSize;
|
let translationY = -chunkY * layer.chunkSize;
|
||||||
let result = renderToPixmap(chunk.pixmap, translationX, translationY);
|
brushRenderer.setTranslation(translationX, translationY);
|
||||||
chunk.markModified();
|
let result = renderToCanvas(brushRenderer, chunk.id, translationX, translationY);
|
||||||
if (result.status != "ok") return result;
|
if (result.status != "ok") return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,14 +53,26 @@ export function dotterRenderArea(wall, dotter) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selfController(interactionQueue, wall, layer, event) {
|
export function selfController(
|
||||||
|
interactionQueue,
|
||||||
|
chunkAllocator,
|
||||||
|
brushRenderer,
|
||||||
|
wall,
|
||||||
|
layer,
|
||||||
|
event,
|
||||||
|
) {
|
||||||
let renderArea = null;
|
let renderArea = null;
|
||||||
return {
|
return {
|
||||||
async runScribble(renderToPixmap) {
|
async runScribble(renderToCanvas) {
|
||||||
interactionQueue.push({ kind: "scribble" });
|
interactionQueue.push({ kind: "scribble" });
|
||||||
if (renderArea != null) {
|
if (renderArea != null) {
|
||||||
let numChunksToRender = numChunksInRectangle(renderArea, layer.chunkSize);
|
let result = renderToChunksInArea(
|
||||||
let result = renderToChunksInArea(layer, renderArea, renderToPixmap);
|
chunkAllocator,
|
||||||
|
brushRenderer,
|
||||||
|
layer,
|
||||||
|
renderArea,
|
||||||
|
renderToCanvas,
|
||||||
|
);
|
||||||
return result;
|
return result;
|
||||||
} else {
|
} else {
|
||||||
console.debug("render area is empty, nothing will be rendered");
|
console.debug("render area is empty, nothing will be rendered");
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,7 @@ class Session extends EventTarget {
|
||||||
super();
|
super();
|
||||||
this.userId = userId;
|
this.userId = userId;
|
||||||
this.secret = secret;
|
this.secret = secret;
|
||||||
|
this.errored = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async #recvJson() {
|
async #recvJson() {
|
||||||
|
|
@ -106,6 +107,7 @@ class Session extends EventTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
#dispatchError(source, kind, message) {
|
#dispatchError(source, kind, message) {
|
||||||
|
this.errored = true;
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
Object.assign(new Event("error"), {
|
Object.assign(new Event("error"), {
|
||||||
source,
|
source,
|
||||||
|
|
@ -123,7 +125,7 @@ class Session extends EventTarget {
|
||||||
|
|
||||||
this.ws.addEventListener("error", (event) => {
|
this.ws.addEventListener("error", (event) => {
|
||||||
console.error("WebSocket connection error", error);
|
console.error("WebSocket connection error", error);
|
||||||
this.dispatchEvent(Object.assign(new Event("error"), event));
|
this.#dispatchError(event, "ws", "WebSocket connection error");
|
||||||
});
|
});
|
||||||
|
|
||||||
this.ws.addEventListener("message", (event) => {
|
this.ws.addEventListener("message", (event) => {
|
||||||
|
|
@ -288,6 +290,7 @@ class Session extends EventTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
sendViewport({ left, top, right, bottom }) {
|
sendViewport({ left, top, right, bottom }) {
|
||||||
|
console.trace({ left, top, right, bottom });
|
||||||
this.#sendJson({
|
this.#sendJson({
|
||||||
request: "viewport",
|
request: "viewport",
|
||||||
topLeft: { x: left, y: top },
|
topLeft: { x: left, y: top },
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,20 @@
|
||||||
import { Pixmap } from "rkgk/haku.js";
|
|
||||||
import { OnlineUsers } from "rkgk/online-users.js";
|
import { OnlineUsers } from "rkgk/online-users.js";
|
||||||
|
|
||||||
export class Chunk {
|
export class Chunk {
|
||||||
constructor(size) {
|
constructor(chunkAllocator) {
|
||||||
this.pixmap = new Pixmap(size, size);
|
this.id = chunkAllocator.alloc();
|
||||||
this.canvas = new OffscreenCanvas(size, size);
|
|
||||||
this.ctx = this.canvas.getContext("2d");
|
|
||||||
this.renderDirty = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy(chunkAllocator) {
|
||||||
this.pixmap.destroy();
|
chunkAllocator.dealloc(this.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
syncFromPixmap() {
|
upload(chunkAllocator, source) {
|
||||||
this.ctx.putImageData(this.pixmap.getImageData(), 0, 0);
|
chunkAllocator.upload(this.id, source);
|
||||||
}
|
}
|
||||||
|
|
||||||
syncToPixmap() {
|
download(chunkAllocator) {
|
||||||
let imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
return chunkAllocator.download(this.id);
|
||||||
this.pixmap.getImageData().data.set(imageData.data, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
markModified() {
|
|
||||||
this.renderDirty = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,9 +32,9 @@ export class Layer {
|
||||||
console.info("created layer", this.id, this.name);
|
console.info("created layer", this.id, this.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy(chunkAllocator) {
|
||||||
for (let { chunk } of this.chunks.values()) {
|
for (let { chunk } of this.chunks.values()) {
|
||||||
chunk.destroy();
|
chunk.destroy(chunkAllocator);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,41 +42,58 @@ export class Layer {
|
||||||
return this.chunks.get(chunkKey(x, y))?.chunk;
|
return this.chunks.get(chunkKey(x, y))?.chunk;
|
||||||
}
|
}
|
||||||
|
|
||||||
getOrCreateChunk(x, y) {
|
getOrCreateChunk(chunkAllocator, x, y) {
|
||||||
let key = chunkKey(x, y);
|
let key = chunkKey(x, y);
|
||||||
if (this.chunks.has(key)) {
|
if (this.chunks.has(key)) {
|
||||||
return this.chunks.get(key)?.chunk;
|
return this.chunks.get(key)?.chunk;
|
||||||
} else {
|
} else {
|
||||||
if (this.chunkLimit != null && this.chunks.size >= this.chunkLimit) return null;
|
if (this.chunkLimit != null && this.chunks.size >= this.chunkLimit) return null;
|
||||||
|
|
||||||
let chunk = new Chunk(this.chunkSize);
|
let chunk = new Chunk(chunkAllocator);
|
||||||
this.chunks.set(key, { x, y, chunk });
|
this.chunks.set(key, { x, y, chunk });
|
||||||
return chunk;
|
return chunk;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
compositeAlpha(src) {
|
composite(chunkAllocator, src, op) {
|
||||||
for (let { x, y, chunk: srcChunk } of src.chunks.values()) {
|
for (let { x, y, chunk: srcChunk } of src.chunks.values()) {
|
||||||
srcChunk.syncFromPixmap();
|
let dstChunk = this.getOrCreateChunk(chunkAllocator, x, y);
|
||||||
let dstChunk = this.getOrCreateChunk(x, y);
|
|
||||||
if (dstChunk == null) continue;
|
if (dstChunk == null) continue;
|
||||||
|
|
||||||
dstChunk.ctx.globalCompositeOperation = "source-over";
|
chunkAllocator.composite(dstChunk.id, srcChunk.id, op);
|
||||||
dstChunk.ctx.drawImage(srcChunk.canvas, 0, 0);
|
|
||||||
dstChunk.syncToPixmap();
|
|
||||||
dstChunk.markModified();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async toEdits() {
|
async toEdits(chunkAllocator) {
|
||||||
|
console.time("toEdits");
|
||||||
|
|
||||||
let edits = [];
|
let edits = [];
|
||||||
|
let encodeTime = 0;
|
||||||
let start = performance.now();
|
|
||||||
|
|
||||||
for (let { x, y, chunk } of this.chunks.values()) {
|
for (let { x, y, chunk } of this.chunks.values()) {
|
||||||
edits.push({
|
edits.push({
|
||||||
chunk: { x, y },
|
chunk: { x, y },
|
||||||
data: chunk.canvas.convertToBlob({ type: "image/png" }),
|
data: chunk.download(chunkAllocator).then(async (downloaded) => {
|
||||||
|
let start = performance.now();
|
||||||
|
|
||||||
|
let imageBitmap = await createImageBitmap(
|
||||||
|
new ImageData(
|
||||||
|
new Uint8ClampedArray(downloaded.data),
|
||||||
|
downloaded.width,
|
||||||
|
downloaded.height,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
chunkAllocator.freeDownload(downloaded.data);
|
||||||
|
let canvas = new OffscreenCanvas(downloaded.width, downloaded.height);
|
||||||
|
let ctx = canvas.getContext("bitmaprenderer");
|
||||||
|
ctx.transferFromImageBitmap(imageBitmap);
|
||||||
|
let blob = canvas.convertToBlob({ type: "image/png" });
|
||||||
|
|
||||||
|
let end = performance.now();
|
||||||
|
console.log("encoding image took", end - start, "ms");
|
||||||
|
encodeTime += end - start;
|
||||||
|
|
||||||
|
return blob;
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,8 +101,7 @@ export class Layer {
|
||||||
edit.data = await edit.data;
|
edit.data = await edit.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
let end = performance.now();
|
console.timeEnd("toEdits");
|
||||||
console.debug("toEdits done", end - start);
|
|
||||||
|
|
||||||
return edits;
|
return edits;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
45
static/webgl.js
Normal file
45
static/webgl.js
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
function compileShader(gl, kind, source) {
|
||||||
|
let shader = gl.createShader(kind);
|
||||||
|
|
||||||
|
gl.shaderSource(shader, source);
|
||||||
|
gl.compileShader(shader);
|
||||||
|
|
||||||
|
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
||||||
|
let error = new Error(`failed to compile shader: ${gl.getShaderInfoLog(shader)}`);
|
||||||
|
gl.deleteShader(shader);
|
||||||
|
throw error;
|
||||||
|
} else {
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function compileProgram(gl, vertexSource, fragmentSource) {
|
||||||
|
let vertexShader = compileShader(gl, gl.VERTEX_SHADER, vertexSource);
|
||||||
|
let fragmentShader = compileShader(gl, gl.FRAGMENT_SHADER, fragmentSource);
|
||||||
|
|
||||||
|
let program = gl.createProgram();
|
||||||
|
gl.attachShader(program, vertexShader);
|
||||||
|
gl.attachShader(program, fragmentShader);
|
||||||
|
gl.linkProgram(program);
|
||||||
|
|
||||||
|
gl.deleteShader(vertexShader);
|
||||||
|
gl.deleteShader(fragmentShader);
|
||||||
|
|
||||||
|
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
||||||
|
let error = new Error(`failed to link program: ${gl.getProgramInfoLog(program)}`);
|
||||||
|
gl.deleteProgram(program);
|
||||||
|
throw error;
|
||||||
|
} else {
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function orthographicProjection(left, right, top, bottom, near, far) {
|
||||||
|
// prettier-ignore
|
||||||
|
return [
|
||||||
|
2 / (right - left), 0, 0, -((right + left) / (right - left)),
|
||||||
|
0, 2 / (top - bottom), 0, -((top + bottom) / (top - bottom)),
|
||||||
|
0, 0, -2 / (far - near), -((far + near) / (far - near)),
|
||||||
|
0, 0, 0, 1,
|
||||||
|
];
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue