Punch a hole in a rectangle

I need to see through a hole in a solid rectangle, but cannot find a way to establish the hole.
If it was a rectangular hole it would be easy to make 4 mask rectangles around the hole, but that does not work with an ellipse.
A solution is of course to use a sprite with the hole, but I would prefer to do it in code.
Any suggestions ?

Create a rectange image of the size you want : img =image(100,50).
Color this image: setContext(img) background(mycolor) setContex().
Draw the hole pixel by pixel : img:set( x, y, color(0,0,0,0) ).

Now img is your code-created sprite, with a hole.

Is your rectangle a mesh or a sprite?

I think a possible solution is to do your drawing above your rect. Could just work… You could pull a miracle with clip…

Drawing an image pixel-by-pixel is slow. How about using a mesh? You could create a Codea class that holds the mesh and the code to draw it. For example:


--
-- RectWithHole example
--

supportedOrientations(LANDSCAPE_ANY)
function setup()
    -- RectWithHole(width, height, holeX, holeY, radius)
    myRwh = RectWithHole(400, 300, 80, 50, 90)
    myRwh:fill(color(255, 0, 0))
    -- Set up random backdrop
    churchImg = readImage("Small World:Church")
    p = {}
    for i = 1, 10 do
        p[i] = vec2(math.random(WIDTH), math.random(HEIGHT))
    end
end

function draw()
    background(0)
    -- Draw backdrop
    for i = 1, 10 do
        sprite(churchImg, p[i].x, p[i].y)
    end
    -- Setup position and rotation of myRwh
    local x = WIDTH / 3 * math.sin(ElapsedTime) + WIDTH / 2
    local y = HEIGHT / 3 * math.cos(ElapsedTime * 3) + HEIGHT / 2
    local angle = (ElapsedTime * 20) % 360
    myRwh.posX = x
    myRwh.posY = y
    myRwh.angle = angle
    -- Draw the rectangle with hole
    myRwh:draw()
end

--
-- Codea class for a rectangle with a circular hole
--

RectWithHole = class()

function RectWithHole:init(width, height, holeX, holeY, radius)
    -- Clamp inputs to sensible values, if necessary
    width = math.abs(width) / 2
    height = math.abs(height) / 2
    radius = math.min(math.abs(radius), width, height)
    holeX = math.min(math.max(holeX, radius - width),
        width - radius)
    holeY = math.min(math.max(holeY, radius - height),
        height - radius)
    -- Record the properties of the object
    self.width = width * 2
    self.height = height * 2
    self.holeX = holeX
    self.holeY = holeY
    self.radius = radius
    self.posX = 0
    self.posY = 0
    self.angle = 0
    self.fillColor = color(fill())
    self.mesh = mesh()
    -- Create the mesh
    local ver = {}
    local v = {}
    local function tri(pTable, r, p1, p2)
        local tolerance = 0.5
        local newP1 = r * p1:normalize()
        local dp = newP1 - p1
        local normDp12 = (p2 - p1):normalize()
        local dp1NewP2 = (dp:lenSqr()/dp:dot(normDp12)) * normDp12
        local area = dp:dot(dp1NewP2)
        if math.abs(area) < tolerance then return end
        local newP2 = p1 + dp1NewP2
        local newP3 = 2 * newP1 - newP2
        table.insert(pTable, p1)
        table.insert(pTable, newP2)
        table.insert(pTable, newP3)
        tri(pTable, r, newP2, newP1)
        tri(pTable, r, newP3, newP1)
    end
    tri(v, radius, vec2(radius, radius), vec2(radius, 0))
    local numV = #v
    local holeOrigin = vec2(holeX, holeY)
    for i = 1, numV do
        table.insert(ver, holeOrigin + v[i])
    end
    for i = 1, numV do
        table.insert(ver, holeOrigin - v[i])
    end
    for i = 1, numV do
        table.insert(ver, holeOrigin + v[i]:rotate90())
    end
    for i = 1, numV do
        table.insert(ver, holeOrigin - v[i]:rotate90())
    end
    local function addRect(pTable, x1, y1, x2, y2)
        if x1 == x2 or y1 == y2 then return end
        local p1 = vec2(x1, y1)
        local p2 = vec2(x2, y1)
        local p3 = vec2(x2, y2)
        local p4 = vec2(x1, y2)
        table.insert(pTable, p1)
        table.insert(pTable, p2)
        table.insert(pTable, p3)
        table.insert(pTable, p3)
        table.insert(pTable, p4)
        table.insert(pTable, p1)
    end
    addRect(ver, -width, -height, holeX - radius, height)
    addRect(ver, holeX - radius,
        holeY + radius, holeX + radius, height)
    addRect(ver, holeX + radius, -height, width, height)
    addRect(ver, holeX - radius, -height,
        holeX + radius, holeY - radius)
    self.mesh.vertices = ver
    -- Colour the mesh
    self:fill(self.fillColor)
end

function RectWithHole:draw()
    pushMatrix()
    resetMatrix()
    translate(self.posX, self.posY)
    rotate(self.angle)
    self.mesh:draw()
    popMatrix()
end

function RectWithHole:fill(fillColor)
    self.fillColor = fillColor
    self.mesh:setColors(fillColor)
end

The mesh is created as a circle in a square surrounded by up to four rectangles. The circle in a square is created by recursively cutting away ever-smaller tangential triangles in one quadrant (using function tri) and then using symmetry to create the triangles in the other three quadrants.

Thanks for the suggestions.
I was about to go the mesh route when I realized that the solution was right in front of me.

In the draw loop:

  • Set the background color
  • Draw an ellipse with noFill and strokeWidth set to cover what is needed and leaving a hole with the proper diameter (remember stroke goes inward from the ellipse diameter).
  • Add rectangles at the sides set to stroke color from ellipse (this can be left out if the ellipse stroke covers enough area).

An easy approach after all :slight_smile:

Re peter

I’ve a slightly related problem, which I’ve tried a few solutions for but not found any. I have a nasty feeling that there isn’t a clean answer.

I’ve created a small class (MatteMapButton) which can have its foreground colour changed on the fly - this lets me have the background of my app go all the way from black to white and the button will always appear in a contrasting shade of grey.

Having encountered the destruction of transparency in imported bitmaps, I switched to creating icon images at run-time, which also lets them be smoother. I’ve a defaultImage function which creates a filled circle in white, then tries to overlay a black letter on it. (Code below.) This currently works quite well, until the background of my app is white, at which point the contrasting grey is black, and the letter disappears.

My question: Is there some way to set how pixels are drawn/combined with the existing pixels in an image or on the screen? I would like to be able to set the style so that text rendered with zero alpha would blat out the existing pixels’ alpha, rather than blending to nothing.

And I recognise that this may be something which may be fixed with shaders in 1.5

Code for the defaultImage function and my draw function below. Note that the bg field of my class is a small anachronism, and is typically set to color(0,0,0,0).

Richard

function MatteMapButton:defaultImage(w,h,l)
    img = image(w,h)
    local r
    
    if w > h then
        r = h / 2 * 0.9
    else
        r = w / 2 * 0.9
    end
    -- the following fix gets around a quirk of how italic letters are rendered - the whitespace
    -- means that they are not clipped too aggressively
    l= l or "A"
    l=string.sub(l,1,1)
    l=" "..l.." "
    pushStyle()
    setContext(img)
    background(0, 0, 0, 0)
    fill(255, 255, 255, 255)
    strokeWidth(r*.2)
    stroke(255, 255, 255, 255)
    
    ellipseMode(RADIUS)
    ellipse(w/2,h/2,r)
    stroke(0, 0, 0, 255)
    fill(0, 0, 0, 255)
    --ellipse(w/2,h/2,r*.5)
    fontSize(r*1.4)
    textMode(CORNER)
    font("HoeflerText-BlackItalic")
    local tw,th
    tw,th = textSize(l)
    text(l, w/2-tw/2,h/2-th*0.6)
    popStyle()
    return img
end

function MatteMapButton:draw()
    -- Codea does not automatically call this method
    pushStyle()
    fill(self.bg)
    tint(self.fg)
    sprite(self.img, self.x+self.w/2, self.y+self.h/2, self.w, self.h)
    popStyle()
end

Note that transparency is not lost if you import from photo library, but is lost in photostream.

I recently discovered an undocumented feature (bug?) of ellipse() - a negative radius causes it to render a square with a circular hole. The shorter code below makes use of that feature:


--
-- RectWithHole example
-- Version 2013.01.05.17.00
--

supportedOrientations(LANDSCAPE_ANY)
function setup()
    -- RectWithHole(width, height, holeX, holeY, radius)
    myRwh = RectWithHole(400, 300, 20, 20, 120)
    myRwh:fill(color(255, 0, 0))
    -- Set up random backdrop
    churchImg = readImage("Small World:Church")
    p = {}
    for i = 1, 10 do
        p[i] = vec2(math.random(WIDTH), math.random(HEIGHT))
    end
end

function draw()
    background(0)
    -- Draw backdrop
    for i = 1, 10 do
        sprite(churchImg, p[i].x, p[i].y)
    end
    -- Setup position and rotation
    local x = WIDTH / 3 * math.sin(ElapsedTime) + WIDTH / 2
    local y = HEIGHT / 3 * math.cos(ElapsedTime * 3) + HEIGHT / 2
    local angle = (ElapsedTime * 20) % 360
    myRwh.posX = x
    myRwh.posY = y
    myRwh.angle = angle
    -- Draw the rectangle with hole
    myRwh:draw()
end

--
-- Codea class for a rectangle with a hole
-- Version 2013.01.05.17.00
--

RectWithHole = class()

function RectWithHole:init(width, height, holeX, holeY, radius)
    -- Clamp inputs to sensible values, if necessary
    width = math.abs(width) / 2
    height = math.abs(height) / 2
    radius = math.min(math.abs(radius), width, height)
    holeX = math.min(math.max(holeX, radius - width),
        width - radius)
    holeY = math.min(math.max(holeY, radius - height),
        height - radius)
    -- Preserve inputs
    self.width = width * 2
    self.height = height * 2
    self.holeX = holeX
    self.holeY = holeY
    self.radius = radius
    self.posX = 0
    self.posY = 0
    self.angle = 0
    self.fillColor = color(fill())
    self.mesh = mesh()
    local ver = {}
    local function addRect(pTable, x1, y1, x2, y2)
        if x1 == x2 or y1 == y2 then return end
        local p1 = vec2(x1, y1)
        local p2 = vec2(x2, y1)
        local p3 = vec2(x2, y2)
        local p4 = vec2(x1, y2)
        table.insert(pTable, p1)
        table.insert(pTable, p2)
        table.insert(pTable, p3)
        table.insert(pTable, p3)
        table.insert(pTable, p4)
        table.insert(pTable, p1)
    end
    addRect(ver, -width, -height, holeX - radius, height)
    addRect(ver, holeX - radius,
        holeY + radius, holeX + radius, height)
    addRect(ver, holeX + radius, -height, width, height)
    addRect(ver, holeX - radius, -height,
        holeX + radius, holeY - radius)
    self.mesh.vertices = ver
    self:fill(self.fillColor)
end

function RectWithHole:draw()
    pushStyle()
    pushMatrix()
    resetMatrix()
    translate(self.posX, self.posY)
    rotate(self.angle)
    self.mesh:draw()
    fill(self.fillColor)
    noSmooth()
    noStroke()
    ellipseMode(CENTER)
    -- Draw ellipse (circle) with a negative radius
    ellipse(self.holeX, self.holeY, -self.radius * 2)
    popMatrix()
    popStyle()
end

function RectWithHole:fill(fillColor)
    self.fillColor = fillColor
    self.mesh:setColors(fillColor)
end

Great !
Just what I was looking for :slight_smile:
Maybe negative values has unexpected behavior in other places also…
Nice find, thanks.