Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

Zoom library

edited January 2012 in Code Sharing Posts: 118

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\nPinch to zoom\nDouble 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

Comments

  • Posts: 2,820

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

  • Posts: 273

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

  • Posts: 118

    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\nPinch to zoom\nDouble 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
    
    
  • Posts: 118

    @Zoyt - This is for your competition

  • Posts: 2,820

    Great! Thanks! I was worried no one would enter

  • Posts: 118

    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\nPinch to zoom\nDouble 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
    
  • Posts: 2,820

    Thanks!

  • SimeonSimeon Admin Mod
    Posts: 4,889

    @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!

  • Posts: 118

    @Simeon @Zoyt Thanks guys!

  • Posts: 666

    This is great work! Thanks for sharing!

  • Posts: 118

    Thanks! Also works well with the new dependency feature...

  • Jmv38Jmv38 Mod
    Posts: 3,295

    @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.

  • Posts: 118

    @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
    
  • Jmv38Jmv38 Mod
    Posts: 3,295

    @Herwig thanks! Works very nicely!

  • Posts: 18

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

Sign In or Register to comment.