initial implementation of WebGL-based brush renderer
This commit is contained in:
		
							parent
							
								
									b4c3260f49
								
							
						
					
					
						commit
						bb55e23979
					
				
					 14 changed files with 385 additions and 247 deletions
				
			
		| 
						 | 
				
			
			@ -77,13 +77,7 @@ pub fn resolve(arity: SystemFnArity, name: &str) -> Option<u8> {
 | 
			
		|||
        (Nary, "reduce") => 0x95,
 | 
			
		||||
        (Nary, "flatten") => 0x96,
 | 
			
		||||
 | 
			
		||||
        (Nary, "toShape") => 0xc0,
 | 
			
		||||
        (Nary, "line") => 0xc1,
 | 
			
		||||
        (Nary, "rect") => 0xc2,
 | 
			
		||||
        (Nary, "circle") => 0xc3,
 | 
			
		||||
 | 
			
		||||
        (Nary, "stroke") => 0xe0,
 | 
			
		||||
        (Nary, "fill") => 0xe1,
 | 
			
		||||
 | 
			
		||||
        (Nary, "withDotter") => 0xf0,
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,41 +8,21 @@ pub const Canvas = opaque {
 | 
			
		|||
        if (!status) return error.Draw;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn begin(c: *Canvas) !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 {
 | 
			
		||||
    pub fn stroke(c: *Canvas, color: value.Rgba8, thickness: f32, from: value.Vec2, to: value.Vec2) !void {
 | 
			
		||||
        const r, const g, const b, const a = color;
 | 
			
		||||
        try wrap(__haku2_canvas_fill(c, r, g, b, a));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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));
 | 
			
		||||
        try wrap(__haku2_canvas_stroke(c, r, g, b, a, thickness, from[0], from[1], to[0], to[1]));
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
extern fn __haku2_canvas_begin(c: *Canvas) bool;
 | 
			
		||||
extern fn __haku2_canvas_line(c: *Canvas, x1: f32, y1: f32, x2: f32, y2: f32) bool;
 | 
			
		||||
extern fn __haku2_canvas_rectangle(c: *Canvas, x: f32, y: f32, width: f32, height: f32) bool;
 | 
			
		||||
extern fn __haku2_canvas_circle(c: *Canvas, x: f32, y: f32, r: f32) bool;
 | 
			
		||||
extern fn __haku2_canvas_fill(c: *Canvas, r: u8, g: u8, b: u8, a: u8) bool;
 | 
			
		||||
extern fn __haku2_canvas_stroke(c: *Canvas, r: u8, g: u8, b: u8, a: u8, thickness: f32) bool;
 | 
			
		||||
extern fn __haku2_canvas_stroke(
 | 
			
		||||
    c: *Canvas,
 | 
			
		||||
    r: u8,
 | 
			
		||||
    g: u8,
 | 
			
		||||
    b: u8,
 | 
			
		||||
    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.*) {
 | 
			
		||||
        .scribble => {
 | 
			
		||||
            try canvas.begin();
 | 
			
		||||
 | 
			
		||||
            switch (val.ref.scribble.shape) {
 | 
			
		||||
                .point => |point| try canvas.line(point, point),
 | 
			
		||||
                .line => |line| try canvas.line(line.start, line.end),
 | 
			
		||||
                .rect => |rect| try canvas.rect(rect.top_left, rect.size),
 | 
			
		||||
                .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)),
 | 
			
		||||
            switch (val.ref.scribble) {
 | 
			
		||||
                .stroke => |stroke| try canvas.stroke(
 | 
			
		||||
                    value.rgbaTo8(stroke.color),
 | 
			
		||||
                    stroke.thickness,
 | 
			
		||||
                    value.vec2From4(stroke.from),
 | 
			
		||||
                    value.vec2From4(stroke.to),
 | 
			
		||||
                ),
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -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),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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");
 | 
			
		||||
            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 val = cx.args[i];
 | 
			
		||||
            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) },
 | 
			
		||||
    .{ 0x95, erase("reduce", reduce) },
 | 
			
		||||
    .{ 0x96, erase("flatten", flatten) },
 | 
			
		||||
    .{ 0xc0, erase("toShape", valueToShape) },
 | 
			
		||||
    .{ 0xc1, erase("line", line) },
 | 
			
		||||
    .{ 0xc2, erase("rect", rect) },
 | 
			
		||||
    .{ 0xc3, erase("circle", circle) },
 | 
			
		||||
    .{ 0xe0, erase("stroke", stroke) },
 | 
			
		||||
    .{ 0xe1, erase("fill", fill) },
 | 
			
		||||
    .{ 0xf0, erase("withDotter", withDotter) },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -738,51 +725,14 @@ fn flatten(list: value.List, cx: Context) Vm.Error!value.Ref {
 | 
			
		|||
    return .{ .list = flattened_list };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn toShape(val: value.Value) ?value.Shape {
 | 
			
		||||
    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 {
 | 
			
		||||
fn stroke(thickness: f32, color: Rgba, from: Vec4, to: Vec4) value.Ref {
 | 
			
		||||
    return .{ .scribble = .{
 | 
			
		||||
        .shape = shape,
 | 
			
		||||
        .action = .{ .stroke = .{
 | 
			
		||||
        .stroke = .{
 | 
			
		||||
            .thickness = thickness,
 | 
			
		||||
            .color = color.value,
 | 
			
		||||
        } },
 | 
			
		||||
            .from = from.value,
 | 
			
		||||
            .to = to.value,
 | 
			
		||||
        },
 | 
			
		||||
    } };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -62,7 +62,6 @@ pub const Value = union(enum) {
 | 
			
		|||
            .ref => |r| switch (r.*) {
 | 
			
		||||
                .closure => "function",
 | 
			
		||||
                .list => "list",
 | 
			
		||||
                .shape => "shape",
 | 
			
		||||
                .scribble => "scribble",
 | 
			
		||||
                .reticle => "reticle",
 | 
			
		||||
            },
 | 
			
		||||
| 
						 | 
				
			
			@ -86,7 +85,7 @@ pub const Value = union(enum) {
 | 
			
		|||
                    }
 | 
			
		||||
                    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) {
 | 
			
		||||
    closure: Closure,
 | 
			
		||||
    list: List,
 | 
			
		||||
    shape: Shape,
 | 
			
		||||
    scribble: Scribble,
 | 
			
		||||
    reticle: Reticle,
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -161,44 +159,14 @@ pub const Closure = struct {
 | 
			
		|||
 | 
			
		||||
pub const List = []Value;
 | 
			
		||||
 | 
			
		||||
pub const Shape = union(enum) {
 | 
			
		||||
    point: Vec2,
 | 
			
		||||
    line: Line,
 | 
			
		||||
    rect: Rect,
 | 
			
		||||
    circle: Circle,
 | 
			
		||||
pub const Scribble = union(enum) {
 | 
			
		||||
    stroke: Stroke,
 | 
			
		||||
 | 
			
		||||
    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,
 | 
			
		||||
        fill: Fill,
 | 
			
		||||
 | 
			
		||||
        pub const Stroke = struct {
 | 
			
		||||
            thickness: f32,
 | 
			
		||||
            color: Rgba,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        pub const Fill = struct {
 | 
			
		||||
            color: Rgba,
 | 
			
		||||
        };
 | 
			
		||||
    pub const Stroke = struct {
 | 
			
		||||
        thickness: f32,
 | 
			
		||||
        color: Rgba,
 | 
			
		||||
        from: Vec4,
 | 
			
		||||
        to: Vec4,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										168
									
								
								static/brush-renderer.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										168
									
								
								static/brush-renderer.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,168 @@
 | 
			
		|||
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)
 | 
			
		||||
 | 
			
		||||
    void main() {
 | 
			
		||||
        float thickness = a_properties.x;
 | 
			
		||||
        float hardness = a_properties.y;
 | 
			
		||||
 | 
			
		||||
        vec2 xAxis = a_line.zw - a_line.xy;
 | 
			
		||||
        vec2 direction = normalize(xAxis);
 | 
			
		||||
        vec2 yAxis = vec2(-direction.y, direction.x) * thickness;
 | 
			
		||||
 | 
			
		||||
        vec2 localPosition = a_line.xy + 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;
 | 
			
		||||
    }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const linesFragmentShader = `#version 300 es
 | 
			
		||||
    precision highp float;
 | 
			
		||||
 | 
			
		||||
    out vec4 f_color;
 | 
			
		||||
 | 
			
		||||
    void main() {
 | 
			
		||||
        f_color = vec4(vec3(0.0), 1.0);
 | 
			
		||||
    }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
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);
 | 
			
		||||
 | 
			
		||||
        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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -2,6 +2,8 @@ import { listen, Pool } from "rkgk/framework.js";
 | 
			
		|||
import { Viewport } from "rkgk/viewport.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 {
 | 
			
		||||
    viewport = new Viewport();
 | 
			
		||||
| 
						 | 
				
			
			@ -74,7 +76,7 @@ class CanvasRenderer extends HTMLElement {
 | 
			
		|||
    // Renderer initialization
 | 
			
		||||
 | 
			
		||||
    #initializeRenderer() {
 | 
			
		||||
        console.groupCollapsed("initializeRenderer");
 | 
			
		||||
        console.group("initializeRenderer");
 | 
			
		||||
 | 
			
		||||
        console.info("vendor", this.gl.getParameter(this.gl.VENDOR));
 | 
			
		||||
        console.info("renderer", this.gl.getParameter(this.gl.RENDERER));
 | 
			
		||||
| 
						 | 
				
			
			@ -93,7 +95,9 @@ class CanvasRenderer extends HTMLElement {
 | 
			
		|||
        // 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.
 | 
			
		||||
        const maxRects = 64;
 | 
			
		||||
        let renderChunksProgramId = this.#compileProgram(
 | 
			
		||||
        let renderChunksProgramId = compileProgram(
 | 
			
		||||
            this.gl,
 | 
			
		||||
 | 
			
		||||
            // Vertex
 | 
			
		||||
            `#version 300 es
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -200,47 +204,13 @@ class CanvasRenderer extends HTMLElement {
 | 
			
		|||
        this.batches = [];
 | 
			
		||||
        this.batchPool = new Pool();
 | 
			
		||||
 | 
			
		||||
        this.brushRenderer = new BrushRenderer(this.gl, this.atlasAllocator.canvasSource());
 | 
			
		||||
 | 
			
		||||
        console.debug("GL error state", this.gl.getError());
 | 
			
		||||
 | 
			
		||||
        console.groupEnd();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #compileShader(kind, source) {
 | 
			
		||||
        let shader = this.gl.createShader(kind);
 | 
			
		||||
 | 
			
		||||
        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
 | 
			
		||||
 | 
			
		||||
    #render() {
 | 
			
		||||
| 
						 | 
				
			
			@ -256,6 +226,7 @@ class CanvasRenderer extends HTMLElement {
 | 
			
		|||
        }
 | 
			
		||||
 | 
			
		||||
        this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
 | 
			
		||||
        this.gl.scissor(0, 0, this.canvas.width, this.canvas.height);
 | 
			
		||||
 | 
			
		||||
        this.gl.clearColor(1, 1, 1, 1);
 | 
			
		||||
        this.gl.clear(this.gl.COLOR_BUFFER_BIT);
 | 
			
		||||
| 
						 | 
				
			
			@ -303,8 +274,9 @@ class CanvasRenderer extends HTMLElement {
 | 
			
		|||
 | 
			
		||||
                this.#resetRectBuffer();
 | 
			
		||||
                for (let chunk of chunks) {
 | 
			
		||||
                    let { i, allocation } = chunk.allocation;
 | 
			
		||||
                    let atlas = this.atlasAllocator.atlases[i];
 | 
			
		||||
                    let atlasIndex = this.atlasAllocator.getAtlasIndex(chunk.id);
 | 
			
		||||
                    let allocation = this.atlasAllocator.getAllocation(chunk.id);
 | 
			
		||||
                    let atlas = this.atlasAllocator.atlases[atlasIndex];
 | 
			
		||||
                    this.#pushRect(
 | 
			
		||||
                        chunk.x * this.wall.chunkSize,
 | 
			
		||||
                        chunk.y * this.wall.chunkSize,
 | 
			
		||||
| 
						 | 
				
			
			@ -358,15 +330,14 @@ class CanvasRenderer extends HTMLElement {
 | 
			
		|||
                for (let chunkX = left; chunkX < right; ++chunkX) {
 | 
			
		||||
                    let chunk = layer.getChunk(chunkX, chunkY);
 | 
			
		||||
                    if (chunk != null) {
 | 
			
		||||
                        let allocation = chunk.id;
 | 
			
		||||
 | 
			
		||||
                        let array = batch.get(allocation.i);
 | 
			
		||||
                        let atlasIndex = this.atlasAllocator.getAtlasIndex(chunk.id);
 | 
			
		||||
                        let array = batch.get(atlasIndex);
 | 
			
		||||
                        if (array == null) {
 | 
			
		||||
                            array = [];
 | 
			
		||||
                            batch.set(allocation.i, array);
 | 
			
		||||
                            batch.set(atlasIndex, array);
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        array.push({ layerId: layer.id, x: chunkX, y: chunkY, allocation });
 | 
			
		||||
                        array.push({ layerId: layer.id, x: chunkX, y: chunkY, id: chunk.id });
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
class Atlas {
 | 
			
		||||
    static getInitBuffer(chunkSize, nChunks) {
 | 
			
		||||
        let imageSize = chunkSize * nChunks;
 | 
			
		||||
        return new Uint8Array(imageSize * imageSize * 4).fill(0xaa);
 | 
			
		||||
        return new Uint8Array(imageSize * imageSize * 4).fill(0x00);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    constructor(gl, chunkSize, nChunks, initBuffer) {
 | 
			
		||||
| 
						 | 
				
			
			@ -44,8 +44,12 @@ class Atlas {
 | 
			
		|||
        gl.bindFramebuffer(gl.FRAMEBUFFER, null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    alloc() {
 | 
			
		||||
        return this.free.pop();
 | 
			
		||||
    alloc(gl, initBuffer) {
 | 
			
		||||
        let xy = this.free.pop();
 | 
			
		||||
        if (xy != null) {
 | 
			
		||||
            this.upload(gl, xy, initBuffer);
 | 
			
		||||
        }
 | 
			
		||||
        return xy;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    dealloc(xy) {
 | 
			
		||||
| 
						 | 
				
			
			@ -81,11 +85,24 @@ class Atlas {
 | 
			
		|||
        );
 | 
			
		||||
        gl.bindFramebuffer(gl.FRAMEBUFFER, null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getFramebufferRect({ x, y }) {
 | 
			
		||||
        return {
 | 
			
		||||
            x: x * this.chunkSize,
 | 
			
		||||
            y: y * this.chunkSize,
 | 
			
		||||
            width: this.chunkSize,
 | 
			
		||||
            height: this.chunkSize,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class AtlasAllocator {
 | 
			
		||||
    atlases = [];
 | 
			
		||||
 | 
			
		||||
    // Allocation names
 | 
			
		||||
    #ids = new Map();
 | 
			
		||||
    #idCounter = 1;
 | 
			
		||||
 | 
			
		||||
    // Download buffers
 | 
			
		||||
    #pboPool = [];
 | 
			
		||||
    #downloadBufferPool = [];
 | 
			
		||||
| 
						 | 
				
			
			@ -98,6 +115,28 @@ export class AtlasAllocator {
 | 
			
		|||
        this.initBuffer = Atlas.getInitBuffer(chunkSize, nChunks);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #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
 | 
			
		||||
| 
						 | 
				
			
			@ -105,33 +144,35 @@ export class AtlasAllocator {
 | 
			
		|||
 | 
			
		||||
        for (let i = 0; i < this.atlases.length; ++i) {
 | 
			
		||||
            let atlas = this.atlases[i];
 | 
			
		||||
            let allocation = atlas.alloc();
 | 
			
		||||
            let allocation = atlas.alloc(this.gl, this.initBuffer);
 | 
			
		||||
            if (allocation != null) {
 | 
			
		||||
                return { i, allocation };
 | 
			
		||||
                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();
 | 
			
		||||
        let allocation = atlas.alloc(this.gl, this.initBuffer);
 | 
			
		||||
        this.atlases.push(atlas);
 | 
			
		||||
 | 
			
		||||
        return { i, allocation };
 | 
			
		||||
        return this.#obtainId({ i, allocation });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    dealloc(id) {
 | 
			
		||||
        let { i, allocation } = id;
 | 
			
		||||
        let { i, allocation } = this.#getAllocInfo(id);
 | 
			
		||||
        let atlas = this.atlases[i];
 | 
			
		||||
        atlas.dealloc(allocation);
 | 
			
		||||
        this.#releaseId(id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    upload(id, source) {
 | 
			
		||||
        let { i, allocation } = id;
 | 
			
		||||
        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
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -146,7 +187,7 @@ export class AtlasAllocator {
 | 
			
		|||
        // Initiate download
 | 
			
		||||
 | 
			
		||||
        gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbo);
 | 
			
		||||
        this.atlases[id.i].download(gl, id);
 | 
			
		||||
        this.atlases[allocInfo.i].download(gl, allocInfo.allocation);
 | 
			
		||||
        let fence = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0);
 | 
			
		||||
 | 
			
		||||
        gl.bindBuffer(gl.PIXEL_PACK_BUFFER, 0);
 | 
			
		||||
| 
						 | 
				
			
			@ -191,4 +232,30 @@ export class AtlasAllocator {
 | 
			
		|||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +1,6 @@
 | 
			
		|||
let panicImpl;
 | 
			
		||||
let logImpl, log2Impl;
 | 
			
		||||
let canvasBeginImpl,
 | 
			
		||||
    canvasLineImpl,
 | 
			
		||||
    canvasRectangleImpl,
 | 
			
		||||
    canvasCircleImpl,
 | 
			
		||||
    canvasFillImpl,
 | 
			
		||||
    canvasStrokeImpl;
 | 
			
		||||
let currentBrushRenderer;
 | 
			
		||||
 | 
			
		||||
function allocCheck(p) {
 | 
			
		||||
    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_debug: makeLogFunction2("debug"),
 | 
			
		||||
 | 
			
		||||
            __haku2_canvas_begin: (c) => canvasBeginImpl(c),
 | 
			
		||||
            __haku2_canvas_line: (c, x1, y1, x2, y2) => canvasLineImpl(c, 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),
 | 
			
		||||
            __haku2_canvas_stroke: (c, r, g, b, a, thickness, x1, y1, x2, y2) =>
 | 
			
		||||
                currentBrushRenderer.stroke(c, r, g, b, a, thickness, x1, y1, x2, y2),
 | 
			
		||||
        },
 | 
			
		||||
    }),
 | 
			
		||||
]);
 | 
			
		||||
| 
						 | 
				
			
			@ -151,13 +140,6 @@ 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();
 | 
			
		||||
 | 
			
		||||
export const ContKind = {
 | 
			
		||||
| 
						 | 
				
			
			@ -386,9 +368,12 @@ export class Haku {
 | 
			
		|||
        else return ContKind.Scribble;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    contScribble(pixmap, translationX, translationY) {
 | 
			
		||||
        w.haku_pixmap_set_translation(pixmap.ptr, translationX, translationY);
 | 
			
		||||
        let ok = w2.haku2_render(this.#pVm2, pixmap.ptr, this.#renderMaxDepth);
 | 
			
		||||
    contScribble(renderer, canvas) {
 | 
			
		||||
        console.assert(currentBrushRenderer == null);
 | 
			
		||||
        currentBrushRenderer = renderer;
 | 
			
		||||
        let ok = w2.haku2_render(this.#pVm2, canvas, this.#renderMaxDepth);
 | 
			
		||||
        currentBrushRenderer = null;
 | 
			
		||||
 | 
			
		||||
        if (!ok) {
 | 
			
		||||
            return this.#exceptionResult();
 | 
			
		||||
        } else {
 | 
			
		||||
| 
						 | 
				
			
			@ -415,8 +400,8 @@ export class Haku {
 | 
			
		|||
        while (true) {
 | 
			
		||||
            switch (this.expectedContKind()) {
 | 
			
		||||
                case ContKind.Scribble:
 | 
			
		||||
                    result = await runScribble((pixmap, translationX, translationY) => {
 | 
			
		||||
                        return this.contScribble(pixmap, translationX, translationY);
 | 
			
		||||
                    result = await runScribble((renderer, canvas, translationX, translationY) => {
 | 
			
		||||
                        return this.contScribble(renderer, canvas, translationX, translationY);
 | 
			
		||||
                    });
 | 
			
		||||
                    return result;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -147,6 +147,7 @@ function readUrl(urlString) {
 | 
			
		|||
 | 
			
		||||
    let currentUser = wall.onlineUsers.getUser(session.sessionId);
 | 
			
		||||
    let chunkAllocator = canvasRenderer.atlasAllocator;
 | 
			
		||||
    let brushRenderer = canvasRenderer.brushRenderer;
 | 
			
		||||
 | 
			
		||||
    // Event loop
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -188,7 +189,7 @@ function readUrl(urlString) {
 | 
			
		|||
            }
 | 
			
		||||
 | 
			
		||||
            if (wallEvent.kind.event == "interact") {
 | 
			
		||||
                user.simulate(wall, wallEvent.kind.interactions);
 | 
			
		||||
                user.simulate(chunkAllocator, wall, wallEvent.kind.interactions);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			@ -254,7 +255,7 @@ function readUrl(urlString) {
 | 
			
		|||
 | 
			
		||||
        let layer = currentUser.getScratchLayer(wall);
 | 
			
		||||
        let result = await currentUser.haku.evalBrush(
 | 
			
		||||
            selfController(interactionQueue, wall, layer, event),
 | 
			
		||||
            selfController(interactionQueue, chunkAllocator, brushRenderer, wall, layer, event),
 | 
			
		||||
        );
 | 
			
		||||
        brushEditor.renderHakuResult(result);
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			@ -263,9 +264,8 @@ function readUrl(urlString) {
 | 
			
		|||
        let scratchLayer = currentUser.commitScratchLayer(wall);
 | 
			
		||||
        if (scratchLayer == null) return;
 | 
			
		||||
 | 
			
		||||
        canvasRenderer.deallocateChunks(scratchLayer);
 | 
			
		||||
        let edits = await scratchLayer.toEdits();
 | 
			
		||||
        scratchLayer.destroy();
 | 
			
		||||
        scratchLayer.destroy(chunkAllocator);
 | 
			
		||||
 | 
			
		||||
        let editRecords = [];
 | 
			
		||||
        let dataParts = [];
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -42,7 +42,7 @@ export class User {
 | 
			
		|||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    simulate(wall, interactions) {
 | 
			
		||||
    simulate(chunkAllocator, wall, interactions) {
 | 
			
		||||
        console.group("simulate", this.nickname);
 | 
			
		||||
        for (let interaction of interactions) {
 | 
			
		||||
            if (interaction.kind == "setBrush") {
 | 
			
		||||
| 
						 | 
				
			
			@ -71,6 +71,7 @@ export class User {
 | 
			
		|||
 | 
			
		||||
                if (interaction.kind == "scribble" && this.#expectContKind(ContKind.Scribble)) {
 | 
			
		||||
                    renderToChunksInArea(
 | 
			
		||||
                        chunkAllocator,
 | 
			
		||||
                        this.getScratchLayer(wall),
 | 
			
		||||
                        this.simulation.renderArea,
 | 
			
		||||
                        (pixmap, translationX, translationY) => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)) {
 | 
			
		||||
        let chunk = layer.getOrCreateChunk(chunkX, chunkY);
 | 
			
		||||
        let chunk = layer.getOrCreateChunk(chunkAllocator, chunkX, chunkY);
 | 
			
		||||
        if (chunk == null) continue;
 | 
			
		||||
 | 
			
		||||
        let translationX = -chunkX * layer.chunkSize;
 | 
			
		||||
        let translationY = -chunkY * layer.chunkSize;
 | 
			
		||||
        let result = renderToPixmap(chunk.pixmap, translationX, translationY);
 | 
			
		||||
        chunk.markModified();
 | 
			
		||||
        brushRenderer.setTranslation(translationX, translationY);
 | 
			
		||||
        let result = renderToCanvas(brushRenderer, chunk.id, translationX, translationY);
 | 
			
		||||
        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;
 | 
			
		||||
    return {
 | 
			
		||||
        async runScribble(renderToPixmap) {
 | 
			
		||||
        async runScribble(renderToCanvas) {
 | 
			
		||||
            interactionQueue.push({ kind: "scribble" });
 | 
			
		||||
            if (renderArea != null) {
 | 
			
		||||
                let numChunksToRender = numChunksInRectangle(renderArea, layer.chunkSize);
 | 
			
		||||
                let result = renderToChunksInArea(layer, renderArea, renderToPixmap);
 | 
			
		||||
                let result = renderToChunksInArea(
 | 
			
		||||
                    chunkAllocator,
 | 
			
		||||
                    brushRenderer,
 | 
			
		||||
                    layer,
 | 
			
		||||
                    renderArea,
 | 
			
		||||
                    renderToCanvas,
 | 
			
		||||
                );
 | 
			
		||||
                return result;
 | 
			
		||||
            } else {
 | 
			
		||||
                console.debug("render area is empty, nothing will be rendered");
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -32,9 +32,9 @@ export class Layer {
 | 
			
		|||
        console.info("created layer", this.id, this.name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    destroy() {
 | 
			
		||||
    destroy(chunkAllocator) {
 | 
			
		||||
        for (let { chunk } of this.chunks.values()) {
 | 
			
		||||
            chunk.destroy();
 | 
			
		||||
            chunk.destroy(chunkAllocator);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										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