Graphics Etch-A-Sketch

I was playing around with the graphics LUA example Martin posted a while ago (simple draw of box, filled rectangle, text)

One of the things I wanted to do (for use in another application) was to be able to recreate the Etch-A-Sketch feature using 2 controls - one to update the X position and one to update the Y position. My original attempt is here, still private

I have to source coord anchored at 200,200 to make it easier to see. As you can see, it sometimes erases the previous line, sometimes flashes the correct line, sometimes even draws the correct line.

I’m clearly not doing something correctly, so any pointers as to a way to smooth out the drawing would be helpful.

Alternately - a comment from Martin saying it was never intended to be a real-time feature would also work. lol

1 Like

A good one! There’s one thing I haven’t mentioned I guess. I will check and update the docs.

All paint functions (i.e., those that belong to the graphics library) must be called only within the paint callback functions. Any other functions that modify what is being painted should alter the data used for painting and call the repaint function of the custom control when repainting is needed.

This is the modified version of your preset (or see it Sketching / modified):

-- Preset compatibility check
assert(
    controller.isRequired(MODEL_MK2, "3.6.0"),
    "Version 3.6.0 or higher is required"
)

-- Shortcut for graphics object
local g = graphics

-- The XY pad control
local customControl = controls.get(1)

-- Initialize the preset
--
function preset.onLoad()
    resizeControl(customControl, 600, 400)
    customControl:setPaintCallback(myPaintCallback)
end

-- Resize the control
--
function resizeControl(control, width, height)
    local bounds = control:getBounds()
    bounds[WIDTH] = width
    bounds[HEIGHT] = height
    control:setBounds(bounds)
end

-- Paint callback function
--
function myPaintCallback(control)    
    local bounds = control:getBounds()
    g.setColor(0xFFFFFF)
    g.drawRect(0, 0, bounds[WIDTH], bounds[HEIGHT])
    g.drawLine(300,200,xPosD,yPosD)
    print(xPosD .. ", " .. yPosD)
end

function updateXpos(valueObject,value)
    xPosD = value
   customControl:repaint();    
end

function updateYpos(valueObject,value)
    yPosD = value
    customControl:repaint();
end

You can see here that the function callbacks modify xPosD and yPosD variables and schedule a repaint of the component. The request for the repaint is queued by the base OS which in turn will repaint the component in the screen refresh.

I will try to prevent calling the graphics functions outside of the paint callbacks. Calling the functions outside the paint callbacks throws the graphic operations to the base system without the context of the control component and screen refreshes and thus it will result in total mess.

Super excellent. Now it behaves as I thought it should. I was wondering about doing everything in the callback routine. I tried that as an early variation, but I didn’t have it set up properly.

The reason for wanting this is - the Waldorf XT (and others) have these time/level envelopes.

I thought it would be faster and more intuitive to have 4 or 8 (or whatever) dots on the screen and lines connecting them. Then you could use your finger to reposition the dots and I would display the new time/level values as you moved them. That should give you a quicker way to visualize and modify those kinds of envelopes.

2 Likes

I like this sketchy thing ! i tried to make an “arm” button :

-- Vérification de la compatibilité du preset
assert(
    controller.isRequired(MODEL_MK2, "3.6.0"),
    "Version 3.6.0 ou supérieure est requise"
)

-- Raccourci pour l'objet graphique
local g = graphics

-- Le contrôle XY personnalisé (pad)
local customControl = controls.get(1)

-- Initialisation du preset
function preset.onLoad()
    resizeControl(customControl, 130, 60)
    customControl:setPaintCallback(myPaintCallback)
    customControl:setValueChangeCallback(onValueChange)
end

-- Fonction pour redimensionner le contrĂ´le
function resizeControl(control, width, height)
    local bounds = control:getBounds()
    bounds[WIDTH] = width
    bounds[HEIGHT] = height
    control:setBounds(bounds)
end

-- Fonction de rappel pour la peinture
function myPaintCallback(control)
    local bounds = control:getBounds()
    local width = bounds[WIDTH]
    local height = bounds[HEIGHT]

    -- Utiliser la dimension minimale comme référence
    local minDimension = math.min(width, height)

    -- Calcul des marges et dimensions proportionnelles, arrondies Ă  l'entier le plus proche
    local margin = math.floor(0.1 * minDimension)
    local innerMargin = math.floor(0.05 * minDimension)
    local outerWidth = math.floor(width - 2 * margin)
    local outerHeight = math.floor(height - 2 * margin)
    local innerWidth = math.floor(outerWidth - 2 * innerMargin)
    local innerHeight = math.floor(outerHeight - 2 * innerMargin)
    local rectWidth = math.floor(innerWidth * 0.8)
    local rectHeight = math.floor(innerHeight * 0.8)
    local circleRadius = math.floor(math.min(rectWidth, rectHeight) * 0.4)

    -- Centrer les éléments
    local outerX = math.floor((width - outerWidth) / 2)
    local outerY = math.floor((height - outerHeight) / 2)
    local innerX = math.floor(outerX + innerMargin)
    local innerY = math.floor(outerY + innerMargin)
    local rectX = math.floor(innerX + (innerWidth - rectWidth) / 2)
    local rectY = math.floor(innerY + (innerHeight - rectHeight) / 2)
    local circleX = math.floor(rectX + rectWidth / 2)
    local circleY = math.floor(rectY + rectHeight / 2)

    -- Effacer la zone de dessin
    g.setColor(0x1E1E1E) -- Fond sombre
    g.fillRect(0, 0, width, height)

    -- Dessiner le cadre extérieur (gris clair)
    g.setColor(0xCCCCCC)
    g.fillRect(outerX, outerY, outerWidth, outerHeight)

    -- Dessiner la bordure intérieure (gris clair)
    g.setColor(0xCCCCCC)
    g.fillRect(innerX, innerY, innerWidth, innerHeight)

    -- Dessiner le rectangle, couleur en fonction de l'Ă©tat du pad
    if customControl:getValue() == 127 then
        g.setColor(0xFF0000) -- Rouge pour ON
    else
        g.setColor(0x0000FF) -- Bleu pour OFF
    end
    g.fillRect(rectX, rectY, rectWidth, rectHeight)

    -- Dessiner le cercle noir centré dans le rectangle
    g.setColor(0x000000)
    g.fillCircle(circleX, circleY, circleRadius)
end

-- Fonction appelée lorsque la valeur du pad change
function onValueChange(valueObject, value)
    customControl:repaint()
end

With chat GPT , i manage to make it resize of the drawing depending of le size of the customContro . resizeControl(customControl, x, y)
Now i wanted that i could push the drawing and then the color of the “arm” button change to red . I asked chat gpt and he gave me this code but it does not work :

if customControl:getValue() == 127 then
        g.setColor(0xFF0000) -- Rouge pour ON
    else
        g.setColor(0x0000FF) -- Bleu pour OFF
    end
    g.fillRect(rectX, rectY, rectWidth, rectHeight

it uses “getValue()” … not shure it work .
Is it possible yet to do such a thing ?
Also it’s really cool to be able to draw things ! :wink:
Thanks for all those improvment !
:tada: :star_struck: :partying_face:

1 Like

Awesome effort oldgearguy

I know I should be serious and work on the Patching/Mod Matrix stuff for the PCM 80, but I wanted to see about combining touch and draw in one thing.

So this sketch variation allows you to change the color by touching anywhere in the square. If you move the X, Y controls slowly, you can kind of do a color doodling thing. No erase, no black lines, just a test that is fun.

(preset is still private because I may end up doing more things that might break it, so grab it, play with it, maybe get some ideas stimulated in your head.)

1 Like

So this is rough, probably buggy, not sure it it will really be useful, but it’s definitely something
multi-stage envelope

1 Like

Brilliant! This is exactly why the feature was introduced.

There’s still some things to work out, but one that has to be mentioned is the overall resolution/mapping.
For a typical 4 stage or 6 stage envelope with range of 0-127 per stage, you can fit them in the E1 display with a one-to-one mapping of MIDI value to pixel.
An 8 stage envelope need 1024 pixels which is more than fits on the E1.
So you either do easy math and use a 2 MIDI -to- 1 pixel mapping or maybe harder math and do 3 MIDI -to- 2 pixel mapping.

Either way, using a pair of controls to fine tune to actual value becomes more necessary.

I also need a nice way to integrate “normal” controls with the custom graphics display.
I tried changing some values directly and repainting, but that was a bust.
I tried making each envelope stage it’s own custom control and then moving that around, but that didn’t work either.

Ideally, the envelope stage can be adjusted by touch or by a pair of X/Y controls and also a control to select the stage to be moved.
I have some ideas and some time this weekend to pursue this path. We’ll see how it turns out unless someone else beats me to it.

For completeness - I need to add some boundaries because (at least for envelopes) you can’t set stage 2 to occur before stage 1 in time, so there are implied x-axis boundaries for each stage based on the previous stage positions.