Zoom library

Zoom library allowing you to easily pinch and zoom on a drawing. Also added an alternative ellipse implementation, a rounded rectangle implementation (using a custom clip implementation that takes the transformation matrix into account), and a custom text implementation (that scales when zoomed, and takes into account that textSize should not exceed 2048 pixels). The example below shows the various elements, compared to the standard implementation.

-- Zoom class example with
--   RoundedRectangle support
--   Ellipse support
--   zoomable text support
-- Herwig Van Marck

-- Use this function to perform your initial setup
function setup()
    zoom=Zoom()
end

function roundRect(x,y,w,h,r)
    pushStyle()
    ellipseMode(CORNER)
    smooth()
    zoom:clip(x,y,r+1,r+1)
    Ellipse(x,y,r*2):draw()
    zoom:clip(x,y+h-r,r+1,r+1)
    Ellipse(x,y+h-2*r,2*r):draw()
    zoom:clip(x+w-r-1,y,r+1,r+1)
    Ellipse(x+w-2*r,y,2*r):draw()
    zoom:clip(x+w-r,y+h-r,r+1,r+1)
    Ellipse(x+w-2*r,y+h-2*r,2*r):draw()
    clip()
    noSmooth()
    rect(x,y+r,w,h-2*r)
    rect(x+r,y,w-2*r,r)
    rect(x+r,y+h-r,w-2*r,r)
    popStyle()
end

function touched(touch)
    zoom:touched(touch)
end

-- This function gets called once every frame
function draw()
    zoom:draw()
    -- This sets a dark background color 
    background(0, 0, 0, 255)

    -- This sets the line thickness
    strokeWidth(1)
    stroke(255, 255, 255, 255)
    noSmooth()
    noStroke()
    fill(255,255,255,255)
    RoundRect(WIDTH/2-100,HEIGHT/2-30,200,60,20):draw()
    -- compare text implementations
    textMode(CENTER)
    font("TimesNewRomanPSMT")
    fontSize(20)
    fill(0, 8, 255, 157)
    text("Rounded rectangle",WIDTH/2,HEIGHT/2+15)
    zoom:text("Rounded rectangle",WIDTH/2,HEIGHT/2-15)
    -- compare ellipse implementations
    fill(255, 255, 255, 255)
    ellipse(WIDTH/2,HEIGHT/2-60,40)
    Ellipse(WIDTH/2,HEIGHT/2-110,40):draw()
    
end

-- Geometry part
Ellipse = class()

function Ellipse:init(x,y,w,h)
    -- you can accept and set parameters here
    self.x = x
    self.y = y
    self.w = w
    if (h==nil) then
        self.h=w
    else
        self.h=h
    end
end

function Ellipse:draw()
    local points={}
    local x=self.x
    local y=self.y
    local w=self.w
    local h=self.h
    if (ellipseMode()==CENTER) then
        x=x - self.w/2
        y=y - self.w/2
    elseif (ellipseMode()==RADIUS) then
        x=x - self.w
        y=y - self.w
        w=self.w*2
        h=self.h*2
    elseif (ellipseMode()==CORNERS) then
        w=self.w- self.x
        h=self.h- self.y
    end
    for a=0,2*math.pi,0.1 do
        table.insert(points,vec2(x+(1+math.cos(a))*w/2,y+(1+math.sin(a))*h/2))
    end
    local verts={}
    local center=vec2(x+w/2,y+h/2)
    for i=1,#points do
        table.insert(verts,center)
        table.insert(verts,vec2(points[(i % #points)+1].x,points[(i % #points)+1].y))
        table.insert(verts,vec2(points[i].x,points[i].y))
    end
    local m=mesh()
    m.vertices=verts
    m:draw()
end

RoundRect = class()

function RoundRect:init(x,y,w,h,r)
    -- you can accept and set parameters here
    self.x = x
    self.y = y
    self.w = w
    self.h = h
    self.r = r
end

function RoundRect:draw()
    local x=self.x
    local y=self.y
    local w=self.w
    local h=self.h
    local r=self.r
    if (rectMode()==CENTER) then
        x=x - self.w/2
        y=y - self.w/2
    elseif (rectMode()==RADIUS) then
        x=x - self.w
        y=y - self.w
        w=self.w*2
        h=self.h*2
    elseif (rectMode()==CORNERS) then
        w=self.w- self.x
        h=self.h- self.y
    end
    pushStyle()
    ellipseMode(CORNER)
    smooth()
    zoom:clip(x,y,r+1,r+1)
    Ellipse(x,y,r*2):draw()
    zoom:clip(x,y+h-r,r+1,r+1)
    Ellipse(x,y+h-2*r,2*r):draw()
    zoom:clip(x+w-r-1,y,r+1,r+1)
    Ellipse(x+w-2*r,y,2*r):draw()
    zoom:clip(x+w-r,y+h-r,r+1,r+1)
    Ellipse(x+w-2*r,y+h-2*r,2*r):draw()
    clip()
    noSmooth()
    rect(x,y+r,w,h-2*r)
    rect(x+r,y,w-2*r,r)
    rect(x+r,y+h-r,w-2*r,r)
    popStyle()
end

-- Zoom library
-- Herwig Van Marck
-- usage:
--[[
function setup()
    zoom=Zoom()
end
function touched(touch)
    zoom:touched(touch)
end
function draw()
    zoom:draw()
end
]]--

Zoom = class()

function Zoom:init()
    -- you can accept and set parameters here
    self.touches = {}
    self:clear()
    print("Tap and drag to move\
Pinch to zoom\
Double tap to reset")
end

function Zoom:clear()
    self.lastPinchDist = 0
    self.pinchDelta = 1.0
    self.center = vec2(0,0)
    self.offset = vec2(0,0)
    self.zoom = 1
    self.started = false
    self.started2 = false
end

function Zoom:touched(touch)
    -- Codea does not automatically call this method
    if touch.state == ENDED then
        self.touches[touch.id] = nil
    else
        self.touches[touch.id] = touch
        if (touch.tapCount==2) then
            self:clear()
        end
    end
end

function Zoom:processTouches()
    local touchArr = {}
    for k,touch in pairs(self.touches) do
        -- push touches into array
        table.insert(touchArr,touch)
    end

    if #touchArr == 2 then
        self.started = false
        local t1 = vec2(touchArr[1].x,touchArr[1].y)
        local t2 = vec2(touchArr[2].x,touchArr[2].y)

        local dist = t1:dist(t2)
        if self.started2 then
        --if self.lastPinchDist > 0 then 
            self.pinchDelta = dist/self.lastPinchDist          
        else
            self.offset= self.offset + ((t1 + t2)/2-self.center)/self.zoom
            self.started2 = true
        end
        self.center = (t1 + t2)/2
        self.lastPinchDist = dist
    elseif (#touchArr == 1) then
        self.started2 = false
        local t1 = vec2(touchArr[1].x,touchArr[1].y)
        self.pinchDelta = 1.0
        self.lastPinchDist = 0
        if not(self.started) then
            self.offset = self.offset + (t1-self.center)/self.zoom
            self.started = true
        end
        self.center=t1
    else
        self.pinchDelta = 1.0
        self.lastPinchDist = 0
        self.started = false
        self.started2 = false
    end
end

function Zoom:clip(x,y,w,h)
    clip(x*self.zoom+self.center.x- self.offset.x*self.zoom,
        y*self.zoom+self.center.y- self.offset.y*self.zoom,
        w*self.zoom+1,h*self.zoom+1)
end

function Zoom:text(str,x,y)
    local fSz = fontSize()
    local xt=x*self.zoom+self.center.x- self.offset.x*self.zoom
    local yt=y*self.zoom+self.center.y- self.offset.y*self.zoom
    fontSize(fSz*self.zoom)
    local xtsz,ytsz=textSize(str)
    tsz=xtsz
    if tsz<ytsz then tsz=ytsz end
    if (tsz>2048) then
        local eZoom= tsz/2048.0
        fontSize(fSz*self.zoom/eZoom)
        pushMatrix()
        resetMatrix()
        translate(xt,yt)
        scale(eZoom)
        text(str,0,0)
        popMatrix()
        fontSize(fSz)
    else
        pushMatrix()
        resetMatrix()
        fontSize(fSz*self.zoom)
        text(str,xt,yt)
        popMatrix()
        fontSize(fSz)
    end
end

function Zoom:draw()
    -- compute pinch delta
    self:processTouches()
    -- scale by pinch delta
    self.zoom = math.max( self.zoom*self.pinchDelta, 0.2 )

    translate(self.center.x- self.offset.x*self.zoom,
        self.center.y- self.offset.y*self.zoom)
    
    scale(self.zoom,self.zoom)

    self.pinchDelta = 1.0
end

Wonderful! Thanks! Zooming isn’t too easy to do, so it makes everything better!
Thanks!

Many thanks @Herwig! I was already impressed with the zoom in your Spirograph app and now this!

Thanks guys! In the meantime I made a small update to the Zoom library that takes an initial parameter specifying where to put the origin (needed that for my Strandbeest example).

-- Zoom class example with
--   RoundedRectangle support
--   Ellipse support
--   zoomable text support

-- Use this function to perform your initial setup
function setup()
    zoom=Zoom()
end

function roundRect(x,y,w,h,r)
    pushStyle()
    ellipseMode(CORNER)
    smooth()
    zoom:clip(x,y,r+1,r+1)
    Ellipse(x,y,r*2):draw()
    zoom:clip(x,y+h-r,r+1,r+1)
    Ellipse(x,y+h-2*r,2*r):draw()
    zoom:clip(x+w-r-1,y,r+1,r+1)
    Ellipse(x+w-2*r,y,2*r):draw()
    zoom:clip(x+w-r,y+h-r,r+1,r+1)
    Ellipse(x+w-2*r,y+h-2*r,2*r):draw()
    clip()
    noSmooth()
    rect(x,y+r,w,h-2*r)
    rect(x+r,y,w-2*r,r)
    rect(x+r,y+h-r,w-2*r,r)
    popStyle()
end

function touched(touch)
    zoom:touched(touch)
end

-- This function gets called once every frame
function draw()
    zoom:draw()
    -- This sets a dark background color 
    background(0, 0, 0, 255)

    -- This sets the line thickness
    strokeWidth(1)
    stroke(255, 255, 255, 255)
    noSmooth()
    noStroke()
    fill(255,255,255,255)
    RoundRect(WIDTH/2-100,HEIGHT/2-30,200,60,20):draw()
    -- compare text implementations
    textMode(CENTER)
    font("TimesNewRomanPSMT")
    fontSize(20)
    fill(0, 8, 255, 157)
    text("Rounded rectangle",WIDTH/2,HEIGHT/2+15)
    zoom:text("Rounded rectangle",WIDTH/2,HEIGHT/2-15)
    -- compare ellipse implementations
    fill(255, 255, 255, 255)
    ellipse(WIDTH/2,HEIGHT/2-60,40)
    Ellipse(WIDTH/2,HEIGHT/2-110,40):draw()
    
end

-- Geometry part
Ellipse = class()

function Ellipse:init(x,y,w,h)
    -- you can accept and set parameters here
    self.x = x
    self.y = y
    self.w = w
    if (h==nil) then
        self.h=w
    else
        self.h=h
    end
end

function Ellipse:draw()
    local points={}
    local x=self.x
    local y=self.y
    local w=self.w
    local h=self.h
    if (ellipseMode()==CENTER) then
        x=x - self.w/2
        y=y - self.w/2
    elseif (ellipseMode()==RADIUS) then
        x=x - self.w
        y=y - self.w
        w=self.w*2
        h=self.h*2
    elseif (ellipseMode()==CORNERS) then
        w=self.w- self.x
        h=self.h- self.y
    end
    for a=0,2*math.pi,0.1 do
        table.insert(points,vec2(x+(1+math.cos(a))*w/2,y+(1+math.sin(a))*h/2))
    end
    local verts={}
    local center=vec2(x+w/2,y+h/2)
    for i=1,#points do
        table.insert(verts,center)
        table.insert(verts,vec2(points[(i % #points)+1].x,points[(i % #points)+1].y))
        table.insert(verts,vec2(points[i].x,points[i].y))
    end
    local m=mesh()
    m.vertices=verts
    m:draw()
end

RoundRect = class()

function RoundRect:init(x,y,w,h,r)
    -- you can accept and set parameters here
    self.x = x
    self.y = y
    self.w = w
    self.h = h
    self.r = r
end

function RoundRect:draw()
    local x=self.x
    local y=self.y
    local w=self.w
    local h=self.h
    local r=self.r
    if (rectMode()==CENTER) then
        x=x - self.w/2
        y=y - self.w/2
    elseif (rectMode()==RADIUS) then
        x=x - self.w
        y=y - self.w
        w=self.w*2
        h=self.h*2
    elseif (rectMode()==CORNERS) then
        w=self.w- self.x
        h=self.h- self.y
    end
    pushStyle()
    ellipseMode(CORNER)
    smooth()
    zoom:clip(x,y,r+1,r+1)
    Ellipse(x,y,r*2):draw()
    zoom:clip(x,y+h-r,r+1,r+1)
    Ellipse(x,y+h-2*r,2*r):draw()
    zoom:clip(x+w-r-1,y,r+1,r+1)
    Ellipse(x+w-2*r,y,2*r):draw()
    zoom:clip(x+w-r,y+h-r,r+1,r+1)
    Ellipse(x+w-2*r,y+h-2*r,2*r):draw()
    clip()
    noSmooth()
    rect(x,y+r,w,h-2*r)
    rect(x+r,y,w-2*r,r)
    rect(x+r,y+h-r,w-2*r,r)
    popStyle()
end

-- Zoom library
-- Herwig Van Marck
-- usage:
--[[
function setup()
    zoom=Zoom(WIDTH/2,HEIGHT/2)
end
function touched(touch)
    zoom:touched(touch)
end
function draw()
    zoom:draw()
end
]]--

Zoom = class()

function Zoom:init(x,y)
    -- you can accept and set parameters here
    self.touches = {}
    self.initx=x or 0;
    self.inity=y or 0;
    self:clear()
    print("Tap and drag to move\
Pinch to zoom\
Double tap to reset")
end

function Zoom:clear()
    self.lastPinchDist = 0
    self.pinchDelta = 1.0
    self.center = vec2(self.initx,self.inity)
    self.offset = vec2(0,0)
    self.zoom = 1
    self.started = false
    self.started2 = false
end

function Zoom:touched(touch)
    -- Codea does not automatically call this method
    if touch.state == ENDED then
        self.touches[touch.id] = nil
    else
        self.touches[touch.id] = touch
        if (touch.tapCount==2) then
            self:clear()
        end
    end
end

function Zoom:processTouches()
    local touchArr = {}
    for k,touch in pairs(self.touches) do
        -- push touches into array
        table.insert(touchArr,touch)
    end

    if #touchArr == 2 then
        self.started = false
        local t1 = vec2(touchArr[1].x,touchArr[1].y)
        local t2 = vec2(touchArr[2].x,touchArr[2].y)

        local dist = t1:dist(t2)
        if self.started2 then
        --if self.lastPinchDist > 0 then 
            self.pinchDelta = dist/self.lastPinchDist          
        else
            self.offset= self.offset + ((t1 + t2)/2-self.center)/self.zoom
            self.started2 = true
        end
        self.center = (t1 + t2)/2
        self.lastPinchDist = dist
    elseif (#touchArr == 1) then
        self.started2 = false
        local t1 = vec2(touchArr[1].x,touchArr[1].y)
        self.pinchDelta = 1.0
        self.lastPinchDist = 0
        if not(self.started) then
            self.offset = self.offset + (t1-self.center)/self.zoom
            self.started = true
        end
        self.center=t1
    else
        self.pinchDelta = 1.0
        self.lastPinchDist = 0
        self.started = false
        self.started2 = false
    end
end

function Zoom:clip(x,y,w,h)
    clip(x*self.zoom+self.center.x- self.offset.x*self.zoom,
        y*self.zoom+self.center.y- self.offset.y*self.zoom,
        w*self.zoom+1,h*self.zoom+1)
end

function Zoom:text(str,x,y)
    local fSz = fontSize()
    local xt=x*self.zoom+self.center.x- self.offset.x*self.zoom
    local yt=y*self.zoom+self.center.y- self.offset.y*self.zoom
    fontSize(fSz*self.zoom)
    local xtsz,ytsz=textSize(str)
    tsz=xtsz
    if tsz<ytsz then tsz=ytsz end
    if (tsz>2048) then
        local eZoom= tsz/2048.0
        fontSize(fSz*self.zoom/eZoom)
        pushMatrix()
        resetMatrix()
        translate(xt,yt)
        scale(eZoom)
        text(str,0,0)
        popMatrix()
        fontSize(fSz)
    else
        pushMatrix()
        resetMatrix()
        fontSize(fSz*self.zoom)
        text(str,xt,yt)
        popMatrix()
        fontSize(fSz)
    end
end

function Zoom:draw()
    -- compute pinch delta
    self:processTouches()
    -- scale by pinch delta
    self.zoom = math.max( self.zoom*self.pinchDelta, 0.2 )

    translate(self.center.x- self.offset.x*self.zoom,
        self.center.y- self.offset.y*self.zoom)
    
    scale(self.zoom,self.zoom)

    self.pinchDelta = 1.0
end

@Zoyt - This is for your competition

Great! Thanks! I was worried no one would enter

Updated version, which also saves the last viewpoint:

-- Zoom library v1.2
-- Herwig Van Marck
-- usage:
--[[
function setup()
    zoom=Zoom(WIDTH/2,HEIGHT/2)
end
function touched(touch)
    zoom:touched(touch)
end
function draw()
    zoom:draw()
end
]]--

Zoom = class()

function Zoom:init(x,y)
    -- you can accept and set parameters here
    self.touches = {}
    self.initx=x or 0;
    self.inity=y or 0;
    self:clear()
    self:readLocalData()
    print("Tap and drag to move\
Pinch to zoom\
Double tap to reset zoom")
end

function Zoom:saveLocalData()
    saveLocalData("Zoom_center_x",self.center.x)
    saveLocalData("Zoom_center_y",self.center.y)
    saveLocalData("Zoom_offset_x",self.offset.x)
    saveLocalData("Zoom_offset_y",self.offset.y)
    saveLocalData("Zoom_zoom",self.zoom)
end

function Zoom:readLocalData()
    self.center.x=readLocalData("Zoom_center_x",self.center.x) or self.center.x
    self.center.y=readLocalData("Zoom_center_y",self.center.y) or self.center.y
    self.offset.x=readLocalData("Zoom_offset_x",self.offset.x) or self.offset.x
    self.offset.y=readLocalData("Zoom_offset_y",self.offset.y) or self.offset.y
    self.zoom=readLocalData("Zoom_zoom",self.zoom) or self.zoom
end

function Zoom:clear()
    self.lastPinchDist = 0
    self.pinchDelta = 1.0
    self.center = vec2(self.initx,self.inity)
    self.offset = vec2(0,0)
    self.zoom = 1
    self.started = false
    self.started2 = false
end

function Zoom:touched(touch)
    -- Codea does not automatically call this method
    if touch.state == ENDED then
        self.touches[touch.id] = nil
        self:saveLocalData()
    else
        self.touches[touch.id] = touch
        if (touch.tapCount==2) then
            self:clear()
        end
    end
end

function Zoom:processTouches()
    local touchArr = {}
    for k,touch in pairs(self.touches) do
        -- push touches into array
        table.insert(touchArr,touch)
    end

    if #touchArr == 2 then
        self.started = false
        local t1 = vec2(touchArr[1].x,touchArr[1].y)
        local t2 = vec2(touchArr[2].x,touchArr[2].y)

        local dist = t1:dist(t2)
        if self.started2 then
        --if self.lastPinchDist > 0 then 
            self.pinchDelta = dist/self.lastPinchDist          
        else
            self.offset= self.offset + ((t1 + t2)/2-self.center)/self.zoom
            self.started2 = true
        end
        self.center = (t1 + t2)/2
        self.lastPinchDist = dist
    elseif (#touchArr == 1) then
        self.started2 = false
        local t1 = vec2(touchArr[1].x,touchArr[1].y)
        self.pinchDelta = 1.0
        self.lastPinchDist = 0
        if not(self.started) then
            self.offset = self.offset + (t1-self.center)/self.zoom
            self.started = true
        end
        self.center=t1
    else
        self.pinchDelta = 1.0
        self.lastPinchDist = 0
        self.started = false
        self.started2 = false
    end
end

function Zoom:clip(x,y,w,h)
    clip(x*self.zoom+self.center.x- self.offset.x*self.zoom,
        y*self.zoom+self.center.y- self.offset.y*self.zoom,
        w*self.zoom+1,h*self.zoom+1)
end

function Zoom:text(str,x,y)
    local fSz = fontSize()
    local xt=x*self.zoom+self.center.x- self.offset.x*self.zoom
    local yt=y*self.zoom+self.center.y- self.offset.y*self.zoom
    fontSize(fSz*self.zoom)
    local xtsz,ytsz=textSize(str)
    tsz=xtsz
    if tsz<ytsz then tsz=ytsz end
    if (tsz>2048) then
        local eZoom= tsz/2048.0
        fontSize(fSz*self.zoom/eZoom)
        pushMatrix()
        resetMatrix()
        translate(xt,yt)
        scale(eZoom)
        text(str,0,0)
        popMatrix()
        fontSize(fSz)
    else
        pushMatrix()
        resetMatrix()
        fontSize(fSz*self.zoom)
        text(str,xt,yt)
        popMatrix()
        fontSize(fSz)
    end
end

function Zoom:draw()
    -- compute pinch delta
    self:processTouches()
    -- scale by pinch delta
    self.zoom = math.max( self.zoom*self.pinchDelta, 0.2 )

    translate(self.center.x- self.offset.x*self.zoom,
        self.center.y- self.offset.y*self.zoom)
    
    scale(self.zoom,self.zoom)

    self.pinchDelta = 1.0
end

function Zoom:getWorldPoint(pt)
    return vec2(self.offset.x-(self.center.x- pt.x)/self.zoom,
        self.offset.y-(self.center.y- pt.y)/self.zoom)
end

function Zoom:getLocalPoint(pt)
    return vec2(pt.x*self.zoom+self.center.x- self.offset.x*self.zoom,
        pt.y*self.zoom+self.center.y- self.offset.y*self.zoom)
end

Thanks!

@Herwig I finally got around to trying this and it’s really brilliant. So easy to use it’s almost as if it’s not even there. Great work!

@Simeon @Zoyt Thanks guys!

This is great work! Thanks for sharing!

Thanks! Also works well with the new dependency feature…

@Herwig would you mind to add a small piece of code showing the result? When i create a project out of this code and run it, i just have a black screen…? I guess this is a library with no example of usage? I am not an expert of codea, so i can’t easily enjoy your development,nor understand how to use it. Thanks anyhow.

@Jmv38 put this in the main of a project called ‘Zoom lib’, and the code above in a separate tab.

-- Use this function to perform your initial setup
function setup()
    zoom = Zoom()
end

function touched(touch)
    zoom:touched(touch)
end

function draw()
    -- process zoom
    zoom:draw()
    -- This sets the background color to black
    background(0, 0, 0)
    -- Do your drawing here
    fill(212, 86, 86, 255)
    rect(132,130,200,200)
    fill(111, 140, 189, 255)
    ellipse(0,0,300,300)
    sprite("Small World:Court",0,200)
end

@Herwig thanks! Works very nicely!

Thanks for a great library!
Is there some way we make scrolling bounds in an easy manner?