Programmatic zoom on to artwork

Help!

I’ve been batting may head off this for days (maybe weeks)

I’m trying to zoom in on a sprite in specified areas (imagine comixology’s Comic reader app) where I specify a rect and the program zooms that artwork up big until that area is isolated alone. (for bonus points i’d Like to mask off areas outside the zoom area). I’ll have a bunch of these focus points, and the art will tween-zoom between them.

I seem to be struggling with figuring out how to translate and zoom in on the right area On the artwork.

Can supply code, but I feel like it’s actually simple, but conceptually, there’s something i’m Missing.

The problem, I think, is the origin for the zoom needs to be at the centre of the focus point of the art, but that won’t necessarily be at zero zero.

Say I have a sprite that’s 500 x 1000

And I have three focus points (measuring from left corner as an origin)

0,0,500,1000 (the full page, no zoom, no need to translate)
0,500,500,500 (half of the top page, no zoom likely, need to translate to the centre of focus)
0,300,100,100 (big zoom in on the 100x100 area - zoom is simply width of display / focus area)

I suspect I need to translate to the zoom centre, then offset the image to display the focus area as a centre.

But I welcome any thoughts!

(can supply code, but it’s not doing what I want so not sure if it’s useful!)

Thanks for any suggestions!

I have a little rectangle class that can handle such transformations. The code for that might help you work out what you want to do. You can find it on github.

In setup:

frame = Rectangle({lowerleft=vec2(0,300), size=vec2(100,100)})

In draw:

frame:TransformFromScreen()

now all drawing commands should take place inside frame.

@pjholden This might give you an idea. Tap on a section of the screen to zoom, tap again to un-zoom.

displayMode(FULLSCREEN)

function setup()
    img=readImage("Cargo Bot:Startup Screen")
end

function draw()
    background(40, 40, 50)
    if zoom then
        sprite(img1,WIDTH/2,HEIGHT/2,WIDTH*2,WIDTH*2)
    else
        sprite(img,WIDTH/2,HEIGHT/2)
    end   
end

function touched(t)
    if t.state==BEGAN then
        zoom=not zoom
        img1=img:copy(t.x//1-100,t.y//1-100,200,200)
    end
end

Translate to middle of screen, scale, then translate again to negative position coordinate:

translate(WIDTH/2,HEIGHT/2)
scale(3)
translate(-pos.x,-pos.y)
spriteMode(CORNER)
sprite("Cargo Bot:Startup Screen",0,0,WIDTH)

Ah, thank you em2! This combined with finally figuring out the maths required (I knew it would be simple) gives me exactly what I’m after (thanks for everyone else’s input too, good stuff to know.

The code looks like this

self.focusOffsetX = (self.w/2) - (self.focusX+ (self.focusW /2) )
    
    self.focusOffsetY = (self.h/2) - (self.focusY+ (self.focusH / 2))
    
    self.zoom = math.min( WIDTH/ self.focusW, HEIGHT/self.focusH) -- limit our zoom to the width of the page.
    pushMatrix()
    
    x = x or WIDTH /2 
    y = y or HEIGHT / 2
    -- translate to the screen co-ordinates to display
    translate(x,y)
    -- zoom in on the image using the calculated zoom

    scale(self.zoom)

    -- now translate the image is centred on the part we want to display
    
    translate(self.focusOffsetX, self.focusOffsetY)
    
      -- now offset the focus of the image
    spriteMode(CENTER)    
    sprite(self.src,0,0) 
   pushMatrix() -- return to our old translates 

Now just have to figure out how to maximise the screen (I think the math.min does the job here, except this is animated, and when animating from a horizontal to a vertical orientation it gets a bit ugly because the calculations change, there’s probably a nicer way to do it…)

@pjholden no problem. I spent hours racking my brains for the solution and I want to spare others the trouble.
For a bounding box defined with a bottom-left corner min and a top-right corner max (pardon my confusing naming conventions):

local center = vec2(max.x+min.x,max.y+min.y) * 0.5 -- average to find center, for translation
local cameraX,cameraY = WIDTH/2, HEIGHT/2 -- center the zoom
local width = max.x-min.x
local height = max.y-min.y
local zoom = math.min(WIDTH/width,HEIGHT/height) -- choose whichever is smaller, scale to fit width or scale to fit height

pushMatrix()
translate(cameraX,cameraY)
scale(zoom)
translate(center.x,center.y)
-- Draw your image here
popMatrix()

Here’s where I’ve gotten so far. It occurred to me that the problem I was having with the zoom is that it was calculating the zoom on the fly, whereas what I should have been doing was figuring out my start zoom and end zoom and tweeting between those points, and now I have a buttery smooth zoom between panels on a page.

PageClass = class()

FocusClass = class()

function FocusClass:init(x,y,w,h, bg)
    -- you can accept and set parameters here
    self.x = x
    self.y = y
    self.h = h
    self.w = w
    self.bg = bg or color(255,0) -- default to no background colour ...
    
end


VERTICAL = 1
HORIZONTAL = 2



function PageClass:addPanel(x,y,w,h)
    self.panelCount = self.panelCount + 1
    self.panels[self.panelCount] = FocusClass(x,y,w,h)
end


function PageClass:boxOut(x,y,w,h, bg)
    -- draw a box around x,y,w,h
    bg = bg or color(255, 0, 0, 0)
    pushStyle()
    stroke(0,0)
    strokeWidth(0)
    -- break it up into four chunks
    
    
    
    
    popStyle()
    
end
function PageClass:init(src)
    -- parameter src = the source image to add...
    self.src = src
    
    -- default focus on the entire page
    
    self.focusX = 0
    self.focusY = 0
    
    self.w, self.h = spriteSize(self.src)
    
    self.focusW = self.w
    self.focusH = self.h 

    self.focusHorizontal = self.w > self.h
    
    self.focusZoom = 1

    self.viewPortX = 0
    self.viewPortY = 0
    
    self.viewPortW = WIDTH
    self.viewPortH = HEIGHT
    
    self.panels = {}
    self.panelCount = 0
    
     
    self.animating = false
    self.currentPanel = 1


    
    -- create the first panel to cover the whole page
    
    self:addPanel(0,0,self.w,self.h)
    
   
end

function PageClass:panelMove(n)
    
    -- known bug, if we travel outside the limits of the panel count (-4, or panelcount+10 say
    -- then we loop back to panelcount (if -4) or 1 (if panelcount+10)
    
    if self.animating then
        -- stop animating and skip to the next panel.
        tween.stop(self.animating)
        self.animating = false
    end

    self.currentPanel = n
    
    if self.currentPanel < 1 then 
        self.currentPanel = self.panelCount
    end
    if self.currentPanel > self.panelCount then
        self.currentPanel = 1
    end
    
    -- now do animation
    -- fix the orientation for any animation
    
    self.focusHorizontal = self.panels[self.currentPanel].w > self.panels[self.currentPanel].h
 
    local zoom
    
    if self.focusHorizontal then
        zoom = WIDTH / self.panels[self.currentPanel].w
    else
        zoom = HEIGHT / self.panels[self.currentPanel].h
    end    
    
    
    
    self.animating = tween( 1, self, 
        { focusX = self.panels[self.currentPanel].x, 
          focusY = self.panels[self.currentPanel].y,
        focusW = self.panels[self.currentPanel].w,
         focusH = self.panels[self.currentPanel].h,
        focusZoom = zoom },
        tween.easing.In,
        function()
        self.animating = false
        end
    )
    
end

function PageClass:nextPanel()
    self:panelMove(self.currentPanel + 1) -- next
end

function PageClass:previousPanel()
    self:panelMove(self.currentPanel -1) -- move previous
end



function PageClass:deleteCurrentPanel()
    
    
end

function PageClass:draw(x,y)
    
    -- calculate from centre point
    self.focusOffsetX = (self.w/2) - (self.focusX+ (self.focusW /2) )
    
    self.focusOffsetY = (self.h/2) - (self.focusY+ (self.focusH / 2))
   
    
    
    pushMatrix()
    
    x = x or WIDTH /2 
    y = y or HEIGHT / 2
    -- translate to the screen co-ordinates to display
    translate(x,y)
    -- zoom in on the image using the calculated zoom

    scale(self.focusZoom)

    -- now translate the image is centred on the part we want to display
    
    translate(self.focusOffsetX, self.focusOffsetY)
    
        -- now offset the focus of the image
    spriteMode(CENTER)    
    sprite(self.src,0,0) 

    --Now we draw the panels in for bug hunting...
    
    translate(-1*self.w/2, -1*self.h/2)
    
    pushStyle()
    stroke(255, 247, 0, 255)
    fill(255, 0, 0, 42)
    rect(self.focusX,self.focusY,self.focusW,self.focusH)
    
    for i,v in pairs(self.panels) do
        stroke(255, 0, 0, 255)
        fill(255, 0, 0, 0)
        strokeWidth(5)
        rect(v.x, v.y, v.w, v.h)
        
        fontSize(60)
        fill(0, 0, 0, 255)
        
        text(i, v.x+v.w/2, v.y+v.h/2)
        
    end
    popMatrix()
    
     
    popMatrix()
end

function PageClass:touched(touch)
    -- Codea does not automatically call this method
end

My plan is to add in loading/editing of pages/panels, so I can create animated comics (simple frame to frame animation much like comixology does)

Here’s a link to what this looks like https://youtu.be/F6VSBkNZQNo
(the page is one I drew for 2000ad/Judge Dredd)

I had to eyeball the positions of all the panels, want to get something that lets me draw the boxes on the page and use those.

Discovered tween doesn’t work on colour, ideally those background images should be changing from white to transparent to black depending on what looks best for each panel) i think it’s an easy fix.

There’s a little thing I’m not quite able to figure out, I want a seemless border around the art. But there seems to be a bit of a problem for me getting it entirely seemlessly … the code looks like this

function PageClass:boxOut(x,y,w,h, bg)
    -- draw a box around x,y,w,h
    bg = bg or color(255, 255, 255, 0) — should probably just exit here without drawing anything if the big isn’t specified...
    pushStyle()
    stroke(0,0)
    strokeWidth(0)
    fill(bg)
    -- break it up into 8 boxes around the image...
    -- box A top ...
     rect(0, y+h, x, y) -- a
     rect(x, y+h, w, y) -- b
     rect(x+w, y+h, x, y) -- c
    
    rect(0,y,x,h) --d
    rect(x+w, y, self.w-x-w,h) --e (force to the edge of the art -self.w - )
    
    rect(0,0,x,y) -- f
    rect(x,0,w,y) -- g
    rect(x+w, 0, x,y)
    
    popStyle()
    
end

Maybe there’s a better way to do that?

Thanks for looking!

@pjholden

what I should have been doing was figuring out my start zoom and end zoom and tweeting between those points
I didn’t know you could “tweet” between points :stuck_out_tongue:

Discovered tween doesn’t work on colour, ideally those background images should be changing from white to transparent to black depending on what looks best for each panel) i think it’s an easy fix.
Tween does work on color. Just set the color as the target:

self.c = color(255)
tween(1, -- run for one second
    self.c, -- set the color as the target
    {a = 0}, -- tween it to a transparent white
    tween.easing.cubicInOut, -- cubic easing for smoothness
    function() -- callback function
        self.c = color(0,0) -- Change it to black; nobody notices because it is transparent. This is done for consistent and smoother transitions. You won't see any grey in between.
        -- Tween again
        tween(1, self.c, {a = 255}, tween.easing.cubicInOut)
    end
)

Rects side-by side are not seamless unless drawn with noSmooth. Alternatively, the faster option would be to use mesh with addRect.

@pjholden I was looking at your video of the comics page zooming to different panels and I created this similar to what was in the video. The center coordinates and panel sizes are just estimates because I didn’t want to spend a lot of time on this. Since I didn’t have any comics, I just created blank panels. Tap the screen once to start the tween. Maybe you can use something from this.

displayMode(FULLSCREEN)
supportedOrientations(PORTRAIT_ANY)

function setup()
    rectMode(CENTER)
    pos=0    
    tab={   {x=WIDTH/2+780,y=HEIGHT/2-950,s=WIDTH*3},
            {x=WIDTH/2+20,y=HEIGHT/2-950,s=WIDTH*3},
            {x=WIDTH/2-750,y=HEIGHT/2-950,s=WIDTH*3},
            {x=WIDTH/2+400,y=HEIGHT/2-100,s=WIDTH*2},    
            {x=WIDTH/2-350,y=HEIGHT/2-100,s=WIDTH*2},    
            {x=WIDTH/2+780,y=HEIGHT/2+860,s=WIDTH*3},
            {x=WIDTH/2+20,y=HEIGHT/2+860,s=WIDTH*3},
            {x=WIDTH/2-750,y=HEIGHT/2+860,s=WIDTH*3},    
            {x=WIDTH/2,y=HEIGHT/2,s=WIDTH} }

    circ={ix=WIDTH/2,iy=HEIGHT/2,s=WIDTH}    
    img=image(WIDTH,HEIGHT)
    setContext(img)
    background(255)
    stroke(0)
    strokeWidth(5)
    fill(255,0,0)
    rect(125,225,225,340)
    rect(375,225,225,340)
    rect(625,225,225,340)    
    rect(190,525,350,200)
    rect(565,525,350,200)
    rect(125,825,225,340)
    rect(375,825,225,340)
    rect(625,825,225,340)
    setContext()
    fill(0, 44, 255, 255)
end

function draw()
    background(255)
    sprite(img,circ.ix,circ.iy,circ.s)
    text("Tap the screen once to start",WIDTH/2,HEIGHT-15)
end

function touched(t)
    if t.state==BEGAN then 
        next()       
    end
end

function next()
    pos=pos+1
    if pos<=#tab then
        tween.delay(1,next1)
    end
end

function next1()
    t2=tween(.5,circ,{ix=tab[pos].x,iy=tab[pos].y,s=tab[pos].s},{loop=tween.loop.once},next)
end