Happy Easter!

Started by Yacine, April 19, 2022, 01:29:19 PM

Previous topic - Next topic

0 Members and 1 Guest are viewing this topic.

Yacine

Happy Easter!
Feel happy for this title, my first idea was "He died for your sins". This would not have matched the mood of my topic.

So I felt like I ought to do something for Easter and having began being "artsy" with my Piet Mondrian series, I thought I continue on this path.
So the topic today are ornaments. I googled a lot of pictures for inspiration.

The first element needed would be a nice spline with smooth curvature transitions. The builtin tool for splines is not only difficult to handle, but the documentation of splines and bezier curves is bad. I haven't for instance found a definition of the knot cell in a spline row.
So I built a helper shape consisting of a straight line, with control points that I used as array with the drawspline function.

Since the sort order of the points matters I wanted to show their indices. I used smarttags. They display the hint text only when you hover over them, well better than no info at all.

def draw_constructor():
    x1 = W/2 - W/10
    x2 = W/2 + W/10
    y1 = y2 = H/2
    shp = vPg.DrawLine(x1,y1,x2,y2)

    shp.AddSection(visSectionProp)
    shp.AddSection(visSectionControls)
    shp.AddSection(visSectionSmartTag)

    row = shp.AddNamedRow(visSectionProp, 'num_points', visTagDefault)
    N = 10
    shp.Cells('prop.num_points.Label').Formula = chr(34) + 'num_points' + chr(34)
    shp.Cells('prop.num_points.Type').Formula = 1
    shp.Cells('prop.num_points.Format').Formula = chr(34) + ';'.join([str(i+1) for i in range(N)]) + chr(34)
    shp.Cells('prop.num_points').Formula = 1
    for i in range(N):
        shp.AddRow(visSectionControls, visRowLast, visTagDefault)
        shp.Cells('Controls.Row_' + str(i+1) + '.XCon').Formula = 'if(prop.num_points>=' + str(i+1) + ';0;6)'
        shp.AddRow(visSectionSmartTag, visRowLast, visTagDefault)
        shp.Cells('SmartTags.Row_' + str(i+1)).Formula = 'Controls.Row_' + str(i+1)
        shp.Cells('SmartTags.Row_' + str(i+1) + '.Y').Formula = 'Controls.Row_' + str(i+1) + '.Y'
        shp.Cells('SmartTags.Row_' + str(i+1) + '.DisplayMode').Formula = 2
        shp.Cells('SmartTags.Row_' + str(i+1) + '.Disabled').Formula = 'if(prop.num_points>=' + str(i+1) + ';False;True)'
        shp.Cells('SmartTags.Row_' + str(i+1) + '.Description').Formula = chr(34) + str(i+1) + chr(34)
    return shp


And the whole exercise makes only sense if you can randomize the points, so I added an according function.

def randomize_points(shp, N = 10, dy = None):
    len_ = ((shp.Cells('EndX').ResultIU - shp.Cells('BeginX').ResultIU) ** 2 + (shp.Cells('EndY').ResultIU - shp.Cells('BeginY').ResultIU) ** 2) ** 0.5
    d = len_ / N
    if not dy:
        dy = d
    distribute_points(shp, N)
    for i in range(N):
        shp.Cells('Controls.Row_' + str(i+1)).Formula = shp.Cells('Controls.Row_' + str(i+1)).ResultIU + d * (random.uniform(0,1))
        shp.Cells('Controls.Row_' + str(i+1) + '.Y').Formula =  dy * (0.5 - random.uniform(0,1))



The distribute_points function does what its name suggests. I place first the points in equal distances on the line, then I randomize their position.

def distribute_points(shp, N = 10):
    len_ = ((shp.Cells('EndX').ResultIU - shp.Cells('BeginX').ResultIU) ** 2 + (shp.Cells('EndY').ResultIU - shp.Cells('BeginY').ResultIU) ** 2) ** 0.5
    d = len_ / N
    for i in range(N):
        shp.Cells('Controls.Row_' + str(i+1)).Formula = d * i
        shp.Cells('Controls.Row_' + str(i+1) + '.Y').Formula = d


Now you may want to not use all the 10 points provided. So you chose the desired number in the props of the helper shape and write a function that returns only the points that are within the chosen range.
(You will recall from the code above, that the points not in use get a "XCon" value of 6, so checking if their value is zero means they are visible.)


def active_points(shp):
    N = 10
    L = []
    for i in range(N):
        if shp.Cells('Controls.Row_' + str(i+1) + '.XCon').ResultStr('') == '0':
            L.append([shp.Cells('Controls.Row_' + str(i+1)).ResultIU, shp.Cells('Controls.Row_' + str(i+1) + '.Y').ResultIU])
    return L


The coordinates you get back from your points are relative to their parent - the helper shape.
We need to translate them into absolute ones.
As long as I kept the line horizontal the conversion was easy.
But as soon you inclined it, the formulas broke. I needed to refer to matrix transformations.
Nice topic: https://en.wikipedia.org/wiki/Transformation_matrix
But I cheated and found a ready formula.

You will furthermore need a function to flatten the list of lists, as Visio expects an array of consecutive values xi,yi.


def ar_points(L):
    return [item for sublist in L for item in sublist]

def absolutify(shp):
    x1 = shp.Cells('BeginX').ResultIU
    y1 = shp.Cells('BeginY').ResultIU
    x2 = shp.Cells('EndX').ResultIU
    y2 = shp.Cells('EndY').ResultIU
    a = shp.Cells('Angle').Result(visRadians)
    L1 = active_points(shp)
    for i,pt in enumerate(L1):
        x_ = pt[0] * math.cos(a) - pt[1] * math.sin(a)
        y_ = pt[0] * math.sin(a) + pt[1] * math.cos(a)
        L1[i] = [x_,y_]
    L1 = [[x + x1, y + y1] for [x,y] in L1]       
    L = [x1,y1] + ar_points(L1) + [x2,y2]
    return L


That was a long introduction. Only now we come to drawing the spline.
And I made two versions of the drawing function. One draws the spline as you prepared it with its control points.
The second scrambles the points before drawing.


def spline_1(shp):
    pts = absolutify(shp)
    shp2 = shp.Parent.DrawSpline(pts,0.1,1)
    vApp.ActiveWindow.DeselectAll()
    vApp.ActiveWindow.Select(shp, visSelect)
    return shp2

def random_spline_1(shp, dy = None):
    randomize_points(shp, len(active_points(shp)),dy)
    pts = absolutify(shp)
    shp2 = vPg.DrawSpline(pts,0.1,1)
    vWin.DeselectAll()
    vWin.Select(shp, visSelect)
    return shp2
Yacine

Yacine

#1

A long journey until this point.
Now we'll draw a bunch of these lines.
We decide to have a unique source (starting point) of the splines.
And it would be nice to randomize the position. for both x and y the randomizer shall decide left/center/right respectively bottom/center/top of the page. That's the "x_dir = random.randint(-1,1)", (same for y).


The source shall also have a distance from the border of the page. That's the offset. "offset = 20 / 25.4", and "x_source = offset + (W-2*offset) * (1+x_dir)/2"


You'll then calculate how long the splines are allowed to be. That's the "amp" stuff.
The allowed angles of the helper shape defining the spline are defined in a kind of decision matrix. Way too complicated, ... too lazy to find a more elegant solution.


You'll then determine a random number of splines (N = random.randint(5,30) ) and draw accordingly.
Determine a new end of the helper shape, draw the splines.
At each drawing of the spline decide randomly whether to add a decoration at the end (a flower).
To influence the ratio of flower to no flower a list of zeros and ones is used (b_draw = random.choice([0]*2+[1]) = 2 zeros and 1 flower = in 1/3 of the cases a spline ends with a flower).


Almost done. We finish by formatting the splines.
The shape is defined by line styles. I've prepared two of them. Pointy with a smaller base (a triangle) and second one as very large and flat oval.
The colours are set by means of randomized values of hue, saturation and luminosity and the use of the HSL function.



# Angles
angs = [[0,0,0,2*math.pi],
       [0,-1,0,math.pi],
       [0,1,math.pi,2*math.pi],
       [-1,1,-math.pi/2,0],
       [-1,0,-math.pi/2,math.pi/2],
       [-1,-1,0,math.pi/2],
       [1,-1,math.pi/2,math.pi],
       [1,0,math.pi/2,math.pi*3/2],
       [1,1,math.pi,math.pi*3/2]]


def bouquet():
    x_dir = random.randint(-1,1)
    y_dir = random.randint(-1,1)
    print(x_dir,y_dir)
    offset = 20 / 25.4
    x_source = offset + (W-2*offset) * (1+x_dir)/2
    y_source = offset + (H-2*offset) * (1+y_dir)/2
    print(x_source,y_source)


    # Amplitude
    if x_dir == 0:
        x_amp = (W-2*offset) / 2
    else:
        x_amp = W-2*offset
    if y_dir == 0:
        y_amp = (H-2*offset) / 2
    else:
        y_amp = H-2*offset
    amp = min(x_amp,y_amp)
    print('Amplitude ', amp)
    ang = [[i[2],i[3]] for i in angs if i[0] == x_dir and i[1] == y_dir][0]
    print('Angle ',ang)
    N = random.randint(5,30)   
    shp0.Cells('BeginX').Formula = x_source
    shp0.Cells('BeginY').Formula = y_source


    vWin.SelectAll()
    if vWin.Selection.Count > 0:
        vWin.Selection.Delete()
   


    shps = []
    shp0.Cells('prop.num_points').Formula = random.randint(1,3)
    for i in range(N):
        ang2 = random.uniform(ang[0],ang[1])
        dr = random.uniform(0.5, 1.0)
        x2 = x_source + dr * amp * math.cos(ang2)
        y2 = y_source + dr * amp * math.sin(ang2)
        #print(ang2,x2,y2)
        shp0.Cells('EndX').Formula = x2
        shp0.Cells('EndY').Formula = y2
        shp = random_spline_1(shp0, dy = random.uniform(0.1,1.5))
        shp.Cells('LayerMember').Formula = chr(34) + '1' + chr(34)
        # decoration
        b_draw = random.choice([0]*2+[1])
        if b_draw == 1:
            flower(shp0)
       
        shps.append(shp)
    shp0.Cells('BeginX').Formula = shp0.Cells('EndX').Formula = W
    shp0.Cells('BeginY').Formula = 0
    shp0.Cells('EndY').Formula = H


    # Format the shapes
    hue_med = 65
    hue_d = 30
    for shp in shps:
        pat = random.choice(['leaf','branch'])
        shp.Cells('LinePattern').Formula = f'USE("{pat}")'


        hue = random.randint(hue_med-hue_d,hue_med+hue_d)
        sat = random.randint(100,220)
        lum = random.randint(90,180)
        hsl = f'HSL({hue};{sat};{lum})'
        #print(hsl)
        shp.Cells('LineColor').Formula = hsl
        shp.SendToBack()
Yacine

Yacine

#2
Some words about the flower function.
That's something I added last and is accordingly rudimentary.
I draw some circles around another one.
The center is colored "yellowish", the petals come in two ranges of color - red-orange and bluish. I needed to avoid the greens which are in the middle of the saturation range.
A better solution to circles would have been to prepare petals as masters and insert them. Well, I wanted to finish the job.


def flower(shp0):
    d0 = random.uniform(.2,1.)
    d1 =  d0 * random.uniform(0.2,1) #random.uniform(0,10/25.4)
    offset = d0
    x1 = shp0.Cells('BeginX').ResultIU
    y1 = shp0.Cells('BeginY').ResultIU
    x2 = shp0.Cells('EndX').ResultIU
    y2 = shp0.Cells('EndY').ResultIU
   
    # position of flower
    ang = math.atan((x2-x1)/(y2-y1))
    x3 = x2 + d0 * math.sin(ang)
    y3 = y2 + d0 * math.cos(ang)
    center_shp = vPg.DrawOval(x3-d0/2,y3-d0/2,x3+d0/2,y3+d0/2)
    grp = center_shp.Group()
    vWin.DeselectAll()
    num_petals = int((d0 + d1/2)*math.pi / d1)*2
    print('num_petals',num_petals)
    rad = d0 + d1/2
    x5 = grp.Cells('Width').ResultIU / 2
    y5 = grp.Cells('Height').ResultIU / 2
    petals = []
    for i in range(num_petals):
        ang4 = 2*math.pi/num_petals * i
        x4 =  x5 + rad * math.cos(ang4) * 0.5
        y4 =  y5 + rad * math.sin(ang4) * 0.5
        shp = grp.DrawOval(x4-d1/2,y4-d1/2,x4+d1/2,y4+d1/2)
        shp.Cells('LayerMember').Formula = chr(34) + '1' + chr(34)
       
       
        petals.append(shp)
    center_shp.BringToFront()

    # Formatting
    hue = random.randint(30,40)
    sat = random.randint(220,240)
    lum = random.randint(120,180)
    hsl = f'HSL({hue};{sat};{lum})'
    center_shp.Cells('FillForegnd').Formula = hsl
    center_shp.Cells('LinePattern').Formula = 0
    hue_rand = random.randint(0,1)
    if hue_rand == 0:
        hue = random.randint(0,46)
    else:
        hue = random.randint(145,239)
    sat = random.randint(190,240)
    lum = random.randint(110,180)
    hsl = f'HSL({hue};{sat};{lum})'
    for petal in petals:
        petal.Cells('Fillforegnd').Formula = hsl
        petal.Cells('LineColor').Formula = 'tint(FillForegnd;-30)'
        petal.Cells('LineColorTrans').Formula = '20%'


As decorations I played also with the idea to draw a spiral at the end of the splines.
Need to get the right end angle of the splines before doing this step.
I was also not happy with the way the size and the number of loops is defined.
So this is a thing for future work.

d = 0.1
a = a_prev = 0
da = math.pi / 5
loops = 0
n_loops = 3
max_ = 10000
i = 1
L = [[0,0]]
while a < n_loops * 2 * math.pi:
   
    a += da
    r = a * 0.009 * i**2/1000
    #print(a)
    x = L[i-1][0] + r * math.cos(a)
    y = L[i-1][1] + r * math.sin(a)
    L.append([x,y])
    if a_prev > a:
        loops += 1
    a_prev = a
   
    i += 1
    if i > max_:
        break
print('Loops ', loops)
#print(L)
B = []
for i in L:
    B.append(i[0])
    B.append(i[1])
shp = vPg.DrawPolyline(B,8)
shp.Cells('Rounding').Formula = '10mm'
vWin.Select(shp, visSelect)
vWin.Selection.Join()
shp = list(vPg.Shapes)[-1]
wh_ratio = shp.Cells('Width').ResultIU / shp.Cells('Height').ResultIU
shp.Cells('LockAspect').Formula = 1

shp.Cells('Width').Formula = '50 mm'
shp.Cells('Height').Formula = shp.Cells('Width').ResultIU / wh_ratio
shp.Cells('PinX').Formula = 0
shp.Cells('PinY').Formula = 0
Yacine

Yacine

#3
And last but not least, you may want to save the results.

def save_():
    rect = vPg.DrawRectangle(0,0,W,H)
    rect.SendToBack()
    FullPath = vDoc.Path + vDoc.Name.split('.')[0] + '_' + strftime('%Y_%m_%d_%H_%M_%S',localtime()) + '.png'
    FullPath=FullPath.replace('"','')
    print(FullPath)
    visServiceVersion140 = 7
    visServiceVersion150 = 8

    DiagramServices = vDoc.DiagramServicesEnabled
    vDoc.DiagramServicesEnabled = visServiceVersion140 + visServiceVersion150

    visRasterUseCustomResolution = 3
    visRasterPixelsPerInch = 0
    visRasterFitToSourceSize = 2
    visRasterInch = 2
    visRasterInterlace = 0
    visRaster24Bit = 3
    visRasterNoRotation = 0
    visRasterNoFlip = 0

    #------------

    vSet = vApp.Settings

    vSet.SetRasterExportResolution(visRasterUseCustomResolution, 100, 100, visRasterPixelsPerInch)
    vSet.SetRasterExportSize(visRasterFitToSourceSize, 6, 4, visRasterInch)
    vSet.RasterExportDataFormat = visRasterInterlace
    vSet.RasterExportColorFormat = visRaster24Bit
    vSet.RasterExportRotation = visRasterNoRotation
    vSet.RasterExportFlip = visRasterNoFlip
    vSet.RasterExportBackgroundColor = 16777215
    vSet.RasterExportTransparencyColor = 16777215
    vSet.RasterExportUseTransparencyColor = False
    vWin.Page.Export(FullPath)

    #------------
    vDoc.DiagramServicesEnabled = DiagramServices
   
   
    rect.Delete()
    return FullPath


A final thought, I disliked the fact that I re-draw the whole drawing each time. I would have prefered to have an option to keep the parts I like and regenerate only the ones I disliked (eg number of flowers, colors, etc.).

So far for my Easter greetings.
Yacine

Yacine

#4
My easter bouquets to the members of the forum ...
Yacine

Yacine

#5
A neat animation of the build process.
Click the image and wait 2 seconds to get the animation started ... (it's a GIF with imperfect start - sorry)
Yacine

Yacine

#6
A part of my motivation collection from the internet ... and the target of my work.
Yacine

Yacine

#7
... and for those willing to try by themselves, here is the juypter notebook.
Yacine

wapperdude

Happy Easter. 

Seems like a lot of coding.  That has to be a labor of love!
Visio 2019 Pro

Yacine

#9
Quote from: wapperdude on April 19, 2022, 02:06:19 PM
Happy Easter. 

Seems like a lot of coding.  That has to be a labor of love!

It is indeed, but working with jupyter lab is so fast, it feels like merely slower than your mouse.
If VBA would require 1 time unit, C# would be guessed at 3-5 and Jupyter lab is at 0.1 or less.
Very enjoyable.

You never work at a whole program, but always at small snippets. This keeps you at task.

PS: re-thought your message ... No it is not a lot of coding!
It sums up to less than 100 lines of code. It is very little compared to the results achieved.
Yacine