lighttroupe/luz

View on GitHub
engine/actor_canvas.rb

Summary

Maintainability
A
2 hrs
Test Coverage
multi_require 'cairo_canvas', 'actor_effect_canvas'

class ActorCanvas < Actor
    title                "Canvas"
    description "A canvas upon which certain effects can draw.\n\nThe contents are persistent for the lifetime of the actor, unless erased by effects."

    # setting 'pixels', :options => [:640x480 etc]

    def deep_clone(*args)
        @cairo_canvas, @image, @last_copy_to_texture_frame_number  = nil, nil, nil        # can't clone these
        super(*args)
    end

    def render
        # Copy Cairo surface to OpenGL texture
        if @last_copy_to_texture_frame_number != $env[:frame_number]

            cc = cairo_canvas
            dirty_rects = cc.dirty_rects_clear
            full_draw = cc.entire_canvas_dirty?

            if full_draw or @last_copy_to_texture_frame_number.nil?
                # Do a full copy
                image.from_bgra8(cc.string_data, cc.width, cc.height)
                cc.entire_canvas_dirty = false
            else
                dirty_rects.each { |r|
                    # r is [x1,y1,x2,y2] in range -0.5->0.5 (actually beyond this range, as Cairo doesn't clamp to canvas size)

                    # Convert to 0.0->1.0 (with clamping)
                    x1 = r[0].clamp(-0.5,0.5) + 0.5
                    x2 = r[2].clamp(-0.5,0.5) + 0.5

                    # Flip vertically, as pixel y=0 is at top, where our canvas has y=0.5 at top
                    y1 = 1.0 - (r[3].clamp(-0.5,0.5) + 0.5)
                    y2 = 1.0 - (r[1].clamp(-0.5,0.5) + 0.5)

#puts "=> [#{x1}, #{y1}, #{x2}, #{y2}]"

                    # Convert to pixel metric
                    x1 *= cc.width
                    y1 *= cc.height
                    x2 *= cc.width
                    y2 *= cc.height

                    # Calculate width/height, adding one pixel for each edge (to account for float->int rounding)
                    width = (x2-x1) + 2
                    width = (cc.width - x1) if (x1 + width) > cc.width            # OpenGL complains if x1+width goes outside texture
                    height = (y2-y1) + 2
                    height = (cc.height - y1) if (y1 + height) > cc.height

                    image.from_bgra8_partial(cc.string_data, cc.width, cc.height, x1, y1, width, height)
                }
            end
            @last_copy_to_texture_frame_number = $env[:frame_number]
        end

        # Fullscreen rect with our texture
        image.using { fullscreen_rectangle }
    end

    def with_canvas
        cairo_canvas.using { |context| yield context }
    end

    def cairo_canvas
        @cairo_canvas ||= new_cairo_canvas
    end

    def image
        @image ||= new_image
    end

    def new_image
        Image.new
    end

    def new_cairo_canvas
        w, h = 1024, 1024        # TODO: some way to choose the size?
        cc = CairoCanvas.new(w, h)
        cc.track_dirty_rects = true

        cc.using { |context|
            # Clear canvas to transparent
            context.save
                context.set_source_rgba(0.0, 0.0, 0.0, 0.0)
                context.set_operator(:source)
                context.paint
            context.restore

            # Default Cairo coordinates has 0,0 in the upper left, with 1 unit translating to 1 pixel
            # Scale it so that the whole canvas goes 0.0 -> 1.0 horizontally and 0.0 -> 1.0 vertically
            context.scale(w, -h)        # NOTE: height is multiplied by -1 to flip the canvas vertically, so bigger numbers go up (cartessian plane)

            # Move cursor to center
            context.translate(0.5, -0.5)

            context.set_source_rgba(1,1,1,1)
            context.set_antialias(Cairo::ANTIALIAS_GRAY)        # or DEFAULT or GRAY or SUBPIXEL or NONE
        }
        cc
    end
end