Menu

Upgrade to 3.0 and Img32.Layers tweaks

2021-08-10
2021-09-28
1 2 > >> (Page 1 of 2)
  • Rob Lambden

    Rob Lambden - 2021-08-10

    Hi Angus,

    I took the plunge to update my UI code to the updated Image32 library. You may recall that I was using TLayeredImage32 code for my drawn components and manually assigning the result of one component to a layer in it's parent. The drilling down to which of my UI Elements is attached to a layer therefre had to search through multiple TLayeredImage32 objects ... so I decided to apply the upgrade.

    TLDR: I managed to port wihtout too much difficulty, the layers are better than before, I have made some tweaks which I am happy to pass back to you.

    As I was replacing an older verion of the library I haven't played with any of the new features (since v1) yet.

    I have TRectD, TPointD etc defined in a separate file which is based on System.Types. I noted that you have now included methods in your implementations which are not signature compatible. I think that's important as it seems likley to me that anyone working with 2D double based geometric objects will have probably ported the System.Types as I did.

    My use case is UI Element centric - the UI Elements map to Layers. When a control has it's parent changed it means it's Layer is moving. Potentially that means between TGroupLayer32 objects, so I have added some tweaks to do that ... otherwise I'm continually destroying and redrawing the images of the UI Elements which doesn't seem sensible.

    I note that you have a Tag on TImage32 - I hadn't noted this before, but have added an AppData: TObject member backed by fAppData which seems more universally useful.

    There was no support for opacity on the TLayeredImage32 - that is it could be set but was ignored - so I have added support for that.

    I have added back in the code to do a 'HitTest' based on the alpha value of a pixel. I realise that the Hit Test layers you have are significantly more powerful, but with an animated UI creating a mask for hit testing at the same time as drawing it seems unnecessary.

    Angles - I noted your comment that the direction had changed. I had always assumed that Image32 implemented the mathemtical convention that angles are positive anti-clockwise and 0 is in the direction of the positive X axis. Using an angle of 135 (3pi/4) in Draw3D gives the normal raised 'affordance' to Elements in the UI. I changed he flag to indicate the positive angles should still be anti-clockwise, but my images were drawn with incorrect lighting. I found that specifying an angle of 45 (pi/4) appears to give the right value. If angles are positive anticlockwise with my flag then it indicates the 0 angle is taken as being inthe direction of the positive y axis ...

    I think some clear doucmentation on this would be helpful (there may be some, but I didn't find it!)

    Personally I would much prefer the standard mathematical convention is used, but I realise other opinions are available.

    I also tried to remove dependence on Windows in the main library - so removing things with IFDEF MSWINDOWS - I have now got routines in Img32.VCL to implement CopyDCToImage32 and CopyImage32ToDC - perhaps this should be an Img32.Windows unit instead?

    New or altered routines:

    function TLayer32.SetIndex(nNewValue: Integer): Integer;
    var
      nNewIndex, nOldIndex: Integer;
    begin
      if (assigned(fGroupOwner) And (nNewValue<>Self.Index)) then
      begin
        nOldIndex:=Self.Index;
        nNewIndex:=MAX(0, nNewValue);
        nNewIndex:=MIN(nNewIndex, Self.fGroupOwner.ChildCount-1);
        if(nOldIndex<>nNewIndex) then
        begin
          Self.fGroupOwner.fChilds.Move(nOldIndex, nNewIndex);
          Self.fGroupOwner.ReindexChildsFrom(MIN(nOldIndex, nNewIndex));
        end;
      end;
      Result:=Self.Index;
    end;
    
    procedure TGroupLayer32.InsertGroup(pGroupLayer: TGroupLayer32; index: integer);
    begin
      if(pGroupLayer.fGroupOwner<>Self) then
      begin
        pGroupLayer.RemoveFromOwner;
        pGroupLayer.fGroupOwner:=Self;
        pGroupLayer.fLayeredImage:=Self.fLayeredImage;
        if index >= ChildCount then
        begin
          pGroupLayer.fIndex := ChildCount;
          fChilds.Add(pGroupLayer);
        end else
        begin
          pGroupLayer.fIndex := index;
          fChilds.Insert(index, pGroupLayer);
          ReindexChildsFrom(index +1);
        end;
        RefreshPending;
      end;
    end;
    //------------------------------------------------------------------------------
    procedure TGroupLayer32.RemoveFromOwner();
    begin
      if(Self.fGroupOwner<>nil) then
      begin
        Self.fGroupOwner.InternalDeleteChild(Self.Index, True);
        Self.fGroupOwner:=nil;
        Self.fLayeredImage:=nil;
      end
      else if((Self.fLayeredImage<>nil) And (Self.fLayeredImage.fRoot=Self)) then
      begin
        Self.fLayeredImage.fRoot:=nil;
        Self.fLayeredImage:=nil;
        Self.fName:='';
      end;
    end;
    //------------------------------------------------------------------------------
    procedure TGroupLayer32.RemoveGroup(pGroupLayer: TGroupLayer32);
    begin
      if(pGroupLayer.GroupOwner=Self) then
        pGroupLayer.RemoveFromOwner();
    end;
    //------------------------------------------------------------------------------
    procedure TGroupLayer32.InsertLayer(pLayer: TLayer32; index: integer);
    begin
      if(pLayer is TGroupLayer32) then
        Self.InsertGroup(TGroupLayer32(pLayer), index)
      else if(pLayer.fGroupOwner<>Self) then
      begin
        if(pLayer.fGroupOwner<>nil) then
          pLayer.fGroupOwner.RemoveLayer(pLayer);
        pLayer.fGroupOwner:=Self;
        pLayer.fLayeredImage:=Self.fLayeredImage;
        if index >= ChildCount then
        begin
          pLayer.fIndex := ChildCount;
          Self.fChilds.Add(pLayer);
        end else
        begin
          pLayer.fIndex := index;
          Self.fChilds.Insert(index, pLayer);
          Self.ReindexChildsFrom(index +1);
        end;
        RefreshPending;
      end;
    end;
    //------------------------------------------------------------------------------
    procedure TGroupLayer32.RemoveLayer(pLayer: TLayer32);
    begin
      if(pLayer is TGroupLayer32) then
        Self.RemoveGroup(TGroupLayer32(pLayer))
      else if(pLayer.fGroupOwner=Self) then
      begin
        Self.InternalDeleteChild(pLayer.Index, True);
        pLayer.fGroupOwner:=nil;
        pLayer.fLayeredImage:=nil;
      end;
    end;
    //------------------------------------------------------------------------------
    
    function TLayer32.HitTest(const pt: TPoint; AOpacity: Byte): Boolean;
    begin
      Result := PtInRect(Bounds, pt);
      if(Result And (AOpacity<>0)) then
        Result:=(((Self.fImage.Pixel[pt.x-left, pt.y-top] And $ff000000) shr 24) >= AOpacity);
    end;
    
    function TGroupLayer32.GetLayerAt(const pt: TPoint; AOpacity: Byte): TLayer32;
    var
      i: integer;
    begin
      Result := nil;
      i:=Self.ChildCount-1;
      while((Result=nil) And (i>=0)) do
      begin
        with Child[i] do
        if Visible and not Image.IsEmpty and HitTest(pt, AOpacity) then
        begin
          Result := Child[i];
          Break;
        end;
        Dec(i);
      end;
    end;
    
    function TLayeredImage32.GetLayerAt(const pt: TPoint; AOpacity: Byte): TLayer32;
    var
      i: integer;
      pLayer: TLayer32;
    begin
      Result := nil;
      i := count-1;
      while((Result=nil) And (i>=0))do
      begin
        pLayer:=Layer[i];
        if(pLayer is TGroupLayer32) then
          Result:=TGroupLayer32(pLayer).GetLayerAt(pt, AOpacity)
        else if ((pLayer.Visible) and (not(pLayer.Image.IsEmpty)) and (pLayer.HitTest(pt, AOpacity))) then
          Result := pLayer;
        Dec(i);
      end;
    end;
    
    
    New code in
    function TLayeredImage32.GetMergedImage(hideDesigners: Boolean;
      out updateRect: TRect): TImage32;
    
    At the end of the with Root do block:
    
          if fOpacity < 254 then //reduce layer opacity
            if(updateRect=Image.Bounds) then
              Image.ScaleAlpha(fOpacity/255)
            else
              Image.ScaleAlpha(fOpacity/255, updateRect);
    
    
    procedure TImage32.ScaleAlpha(scale: double; rec: TRect);
    var
      xLoop, yLoop: Integer;
      pb: PARGB;
    begin
      for yLoop := rec.Top to rec.Bottom-1 do
      begin
        xLoop:=rec.Left;
        pb:=PARGB(@fPixels[(yLoop*Width) + xLoop]);
        while (xLoop < rec.Right) do
        begin
          pb.A := ClampByte(Round(pb.A * scale));
          inc(pb);
          inc(xLoop);
        end;
      end;
      Changed;
    end;
    

    My rendering routines in Img32.VCL (I'm not sure if I changed these, but I think I added the bkColour support)

    procedure CopyDCToImage32(srcDc: HDC; dstImg: TImage32; const srcRect: TRect);
    var
      bi: TBitmapInfoHeader;
      bm, oldBm: HBitmap;
      dc, memDc: HDC;
      pixels: Pointer;
      w,h: integer;
      begin
        dstImg.BeginUpdate;
        try
          w := RectWidth(srcRect);
          h := RectHeight(srcRect);
          dstImg.SetSize(w, h);
          bi := Get32bitBitmapInfoHeader(w, h);
          dc := GetDC(0);
          memDc := CreateCompatibleDC(dc);
          try
            bm := CreateDIBSection(dc,
              PBITMAPINFO(@bi)^, DIB_RGB_COLORS, pixels, 0, 0);
            if bm = 0 then Exit;
            try
              oldBm := SelectObject(memDc, bm);
              BitBlt(memDc, 0, 0, w, h, srcDc, srcRect.Left,srcRect.Top, SRCCOPY);
              Move(pixels^, dstImg.Pixels[0], w * h * sizeOf(TColor32));
              SelectObject(memDc, oldBm);
            finally
              DeleteObject(bm);
            end;
          finally
            DeleteDc(memDc);
            ReleaseDc(0, dc);
          end;
          if dstImg.IsBlank then dstImg.SetAlpha(255);
          dstImg.FlipVertical;
        finally
          dstImg.EndUpdate;
        end;
      end;
    
      procedure CopyImage32ToDc(srcImg: TImage32; dstDc: HDC; x: Integer = 0; y: Integer = 0;
          transparent: Boolean = true; bkColor: TColor32 = clNone32); overload;
      begin
        CopyImage32ToDc(srcImg, srcImg.Bounds, Types.Rect(x,y, x+srcImg.Width, y+srcImg.Height),
          dstDc, transparent, bkColor);
      end;
    
      procedure CopyImage32ToDc(srcImg: TImage32; const srcRect: TRect; dstDc: HDC;
          x: Integer = 0; y: Integer = 0; transparent: Boolean = true; bkColor: TColor32 = clNone32); overload;
      begin
        CopyImage32ToDc(srcImg, srcRect, Types.Rect(x,y, x +RectWidth(srcRect),
          y +RectHeight(srcRect)), dstDc, transparent, bkColor);
      end;
    
      procedure CopyImage32ToDc(srcImg: TImage32; const srcRect, dstRect: TRect; dstDc: HDC;
          transparent: Boolean; bkColor: TColor32); overload;
      var
        i, x,y, wSrc ,hSrc, wDest, hDest: integer;
        rec: TRect;
        tmp: TImage32;
        bi: TBitmapInfoHeader;
        bm, oldBm: HBitmap;
        dibBits: Pointer;
        pc: PARGB;
        memDc: HDC;
        isTransparent: Boolean;
        bf: BLENDFUNCTION;
      begin
        Types.IntersectRect(rec, srcRect, srcImg.Bounds);
        if srcImg.IsEmpty or IsEmptyRect(rec) or IsEmptyRect(dstRect) then Exit;
        wSrc := RectWidth(rec);
        hSrc := RectHeight(rec);
        wDest := RectWidth(dstRect);
        hDest := RectHeight(dstRect);
        x := dstRect.Left;
        y := dstRect.Top;
        inc(x, rec.Left - srcRect.Left);
        inc(y, rec.Top - srcRect.Top);
    
        bi := Get32bitBitmapInfoHeader(wSrc, hSrc);
    
        isTransparent := transparent and (TARGB(bkColor).A < 255) and srcImg.HasTransparency;
    
        tmp:=TImage32.Create(srcImg);
        tmp.FlipVertical;
        tmp.PreMultiply;
        if bkColor <> 0 then
          tmp.SetBackgroundColor(bkColor);
        memDc := CreateCompatibleDC(0);
        try
          bm := CreateDIBSection(memDc, PBITMAPINFO(@bi)^,
            DIB_RGB_COLORS, dibBits, 0, 0);
          if bm = 0 then Exit;
    
          try
            //copy flipped Image to dibBits
            Move(tmp.PixelBase^, dibBits^, wSrc * hSrc * SizeOf(TColor32));
            oldBm := SelectObject(memDC, bm);
            if isTransparent then
            begin
              { Already pre-multiplied
              //premultiplied alphas are required when alpha blending
              pc := dibBits;
              for i := 0 to wSrc * hSrc -1 do
              begin
                if pc.A > 0 then
                begin
                  pc.R  := MulTable[pc.R, pc.A];
                  pc.G  := MulTable[pc.G, pc.A];
                  pc.B  := MulTable[pc.B, pc.A];
                end else
                  pc.Color := 0;
                inc(pc);
              end; }
    
              bf.BlendOp := AC_SRC_OVER;
              bf.BlendFlags := 0;
              bf.SourceConstantAlpha := 255;
              bf.AlphaFormat := AC_SRC_ALPHA;
              AlphaBlend(dstDc, x,y, wDest,hDest, memDC, 0,0, wSrc,hSrc, bf);
            end
            else if (wDest = wSrc) and (hDest = hSrc) then
              BitBlt(dstDc, x,y, wSrc, hSrc, memDc, 0,0, SRCCOPY)
            else
              StretchBlt(dstDc, x,y, wDest,hDest, memDc, 0,0, wSrc,hSrc, SRCCOPY);
    
            SelectObject(memDC, oldBm);
          finally
            DeleteObject(bm);
          end;
        finally
          DeleteDc(memDc);
        end;
        FreeAndNil(tmp);
      end;
    

    Thanks for your ongoing work on this fantastic library!

     
  • Angus Johnson

    Angus Johnson - 2021-08-10

    HI again Rob.
    Wow, thanks for the extensive feedback!
    I've only briefly considered what you've written so apolgies if I've missed something.
     

    TRectD, TPointD etc defined in a separate file which is based on System.Types. I noted that you have now included methods in your implementations which are not signature compatible

    OK. I'll look at that and likely amend.
     

    it means it's Layer is moving. Potentially that means between TGroupLayer32 objects

    Yes, deleting layers and recreating them would be very inefficient, and there's no a Move method for TLayer32s (only the BringForwardOne, SendBackOne, BringToFront, and SendToBack, which also work on groups). However, I don't really understand why you'd need to move a layer (or group) from one group to another, and especially why you might want to move the root layer as you seem to accommodate in your code samples.
     

    AppData: TObject member backed by fAppData which seems more universally useful.

    Yes, that does sound more useful. But is TObject better than a generic pointer?
     

    There was no support for opacity on the TLayeredImage32

    No, but you can set the opacity for the root layer with the same result.
    Also, to change an image's opacity, use TImage32.ReduceOpacity rather than TImage32.ScaleAlpha, though there's no way to undo ReduceOpacity.
     

    I have added back in the code to do a 'HitTest' based on the alpha value of a pixel.

    That's what TRasterLayer32 is for. Its HitText is already based on the pixel's alpha value.
     

    Angles ... I had always assumed that Image32 implemented the mathemtical convention that angles are positive anti-clockwise and 0 is in the direction of the positive X axis.

    This is complicated by the common use of the inverted Y axis in Windows graphics applications. Currently in the library, 0 should always be at 3 o'clock (direction of the positive X axis), and the default rotation direction should be consistently clockwise, though the default direction can easily be reversed. Anyhow, are you suggesting I reverse the default direction?
     

    I also tried to remove dependence on Windows in the main library - so removing things with IFDEF

    AFAICS, using conditional compiler statements doesn't make the Img32 unit dependent on Windows, but it's certainly not as tidy as your solution. However, the downside of your approach is that Windows applications would need both Img32 and Img32.VCL in all their uses clauses.

     

    Last edit: Angus Johnson 2021-08-10
  • Rob Lambden

    Rob Lambden - 2021-08-10

    Hi Angus,

    Moving TLayer32's ...

    I am building UI elements. These are naturally nested. So for a Button I start with a Panel (border and background) and add in an image and text. When I create the button it needs to have a TLayer32 to draw to (actually it doesn't until I actually draw it, but then any code which would access the drawing surface (to set size, border shape etc) must check if it has one, which becomes quite clumsy). So when a 'Composite Element' (one which holds others) is created in the AfterConstruction if it does not have a TGroupLayer32 it will create a TLayeredImage32, and use it's root property as it's TGroupLayer32. The elements that it parents will either be given layers which are added to it's TGroupLayer32 property or, if they are composites themselves, their TGroupLayer32 needs to be added. Later, when it is given a parent itself, it needs to add it's TGroupLayer32 as a child layer of that component. after it's done that it checks to see if it created a TLayeredImage32 previously and if so frees it. I did think about not using the root, but then I have another merge to do. For 100's of UI Elements on a form that's a lot of extra merges ...

    In my scenario the TLayer32's and the TGroupLayer32's are important, the TLayeredImage32 is not. (Well, ultimately one of them is somewhere!)

    (Incidentally I recall that in FireMonkey by default the user can drag and drop components between parents at run time (provided there's minimal application code to support the drag and drop) - anyhwere in windows you might reparent one window in another it's analagous to moving a TGroupLayer32 to another TGroupLayer32.

    AppData property ...

    If it's a TObject you can do if pLayer.AppData is TMyUsefulObject then which seems rather handy.

    Root Opacity ...

    My originally compiled app had lost opacity when it had been set on the root. On investigation I found the ScaleAlpha being used elswewhere ... so I looked to use that and wrote the one with the TRect to allow for the merge partially updating. I was calling GetMergedImage which merged the layers and returned the root's Image, but didn't reduce its opacity. I hadn't spotted the ScaleAlpha routine, thanks for the tip.

    TRasterLayer32 - good to know, I will investigate that.

    Angles ...

    I'm just reporting what I found. Using Draw3D to create the typical affordance of a raised button I previously used 135 to get the light appearing to come from the top left. Setting the flag for positive angles to be anti-clockwise (which is what I had expected) I now have to set the value to 45 to get the same result.

    Further feedback:

    Merging images - I spent some time chasing a bug which I thought was in some of my layout code causing an element to disappear. It turned out the TGroupLayer32 was having it's size set to zero in some checks in the code. I'm not sure why that would be helpful but I realise I am using the layers in a particular way where I want a defined size for each layer and to be in control of that size. TLayers outside their group should be clipped.

    Transparent merged images - when updating the merged image it is remerging onto what was there previously, so transparent areas are not resetting the previous contents. To get what I wanted I have put in code in TGroupLayer32.Merge that fills the stalerect with $00000000 which gives me the assumed startingpoint of an empty image. My code is recording what and how it wants to draw, and then in the draw routine it draws it. I gues I could just as easily put the clearing in my code but I didn't have to before. Or maybe I did and the code has got misplaced in the changes I have made for the new version.

    I was wondering about the stale area tracking. In my scenario I have a number of elements where the border is animated - so a button might pulse to draw attention to it. When this extends to the border of a main window we end up having to redraw the entire window every time which loses the benefit. At the moment all of my borders are represented by a single layer with a transparent centre - so if it changes it's the size of the layer that's noted. Also they are not always rectangular and can change shape dynamically ... I'm not too fussed at the moment but if I want to ensure a reasonable refresh rate in a busy interface I want to keep the amount of repainting to a minimum.

     
  • Angus Johnson

    Angus Johnson - 2021-08-11

    I am building UI elements.

    OK. I think I understand now, and can see how moving layers/groups could be very useful.
    Nevertheless I'm not persuaded that moving the root would be sensible.

    function TLayer32.Move(newGroupOwner: TGroupLayer32; idx: integer): Boolean;
    begin
      Result := assigned(fGroupOwner) and assigned(newGroupOwner);
      if not Result then Exit;
    
      with newGroupOwner do
        if idx < 0 then idx := 0
        else if idx >= ChildCount then idx := ChildCount;
    
      if newGroupOwner = fGroupOwner then
      begin
        if idx = fIndex then Exit;
        fGroupOwner.fChilds.Move(fIndex, idx);
        newGroupOwner.ReindexChildsFrom(Min(idx, fIndex));
      end else
      begin
        if Visible then
          fGroupOwner.Invalidate(Bounds);
        fGroupOwner.ReindexChildsFrom(fIndex);
    
        fIndex := idx;
        newGroupOwner.fChilds.Insert(idx, self);
        newGroupOwner.ReindexChildsFrom(idx +1);
      end;
      newGroupOwner.RefreshPending;
    end;
    

    If it's a TObject you can do if pLayer.AppData is TMyUsefulObject then which seems rather handy.

    Yes, that's probably more useful that a raw pointer.
     

    My originally compiled app had lost opacity when it had been set on the root.

    Yes, this needs fixing.
     

    I'm just reporting what I found. Using Draw3D to create the typical affordance of a raised button I previously used 135

    I've rechecked that, and I think I can see the problem, and how I've made a mistake.
    Without proper documentation the logic is undefined.
    I think the angle should indicate the direction of the light color, not the dark color.
    I'll change the code accordingly (and default back to -135degrees).
     

    It turned out the TGroupLayer32 was having it's size set to zero

    Yes, its size is entirely dependent on the layers it contains. Its image is simply the merged image of its children. This way, the group's image is only redrawn if one of its children is modified.
     

    I want a defined size for each layer and to be in control of that size

    While there's currently no way to do this, what I propose in my next comment would probably be all you need.
     

    TLayers outside their group should be clipped.

    Currently the only way to clip layers outside an abstract group region is to manually clip the individual child layers. However, I could add a ClipRect property to the TGroupLayer32 class which should be farily easy to do.
     

    Transparent merged images - when updating the merged image it is remerging onto what was there previously, so transparent areas are not resetting the previous contents.

    That shouldn't be happening.
    I'll have another look, but I might also need a simple example.
     

    . When this extends to the border of a main window we end up having to redraw the entire window every time which loses the benefit.

    The only way around this (to avoid redrawing the center) is to manually divide your rectangular outlne into 4 separate drawing regions. I can't see how this could be sensibly done by the library without a lot of work and complicated (and potentially inefficient) coding.

     

    Last edit: Angus Johnson 2021-08-12
  • Rob Lambden

    Rob Lambden - 2021-08-12

    Hi Angus,

    I understand your reluctance to move the root, it makes the TLayeredImage unusable. However in my sceanrio it does seem preferred to having another copy ...

    Yes, its size is entirely dependent on the layers it contains. Its image is simply the merged image
    of its children. This way, the group's image is only redrawn if one of its children is modified.

    The point for me was the children hadn't changed so they didn't need to be merged, but they were just removed from the final image as the TGroupLayer32 that owned them had its size set to 0. I thought the idea with the new structure was to prevent uneccesary merges ... surely if the group doesn't need to merge then it's contents are valid and it should be used without alteration? Also if an individual layer is valid and visible it should still be merged, shouldn't it?

    It seems to me that having invalid regions on the TLayers is useful as it tells us which regions need to be updated, but a visible TLayer32 with no invalid regions should still be part of the merged image.

    Currently the only way to clip layers outside an abstract group region is to manually clip the individual child layers. However, I could add a ClipRect property to the TGroupLayer32 class which should be farily easy to do.

    I have often seen UI code that temporarily hides a control (Windows handle) by moving it outside of it's parent. While the abstract view of a TGroupLayer32 is ideal for something like the Gimp, when it's mapped to a different paradigm it needs to work differently. Maybe there's a case for having an option (or a derived class) that implements fixed size and clipping while the standard class flexes its size to accomodate what's inside it.

    The only way around this (to avoid redrawing the center) is to manually divide your rectangular outlne into 4 separate drawing regions. I can't see how this could be sensibly done by the library without a lot of work and complicated (and potentially inefficient) coding.

    That approach on its own won't work as if I do log the update regions separately the library will UnionRect them. Windows tracks multiple InvalidRects per window, allowing applications to repaint just the invalid portions if they choose. If we want to avoid the unecessary copies we would need to be able to work with multiple invalid regions (I'm guessing per TLayer32 through to the TLayeredImage32). If the library always UnionRects the invalid areas then the value of specifying the different regions reduces (to less than zero is it costs something to do it) depending on what the multiple update regions are.

    Rob

     
  • Angus Johnson

    Angus Johnson - 2021-08-12

    The point for me was the children hadn't changed so they didn't need to be merged,

    If children haven't changed then they shouldn't be re-merged. If they are then there's a bug somewhere.
     

    It seems to me that having invalid regions on the TLayers is useful as it tells us which regions need to be updated

    Exactly.
     

    That approach on its own won't work as if I do log the update regions separately the library will UnionRect them.

    Yes, of course, you're right. 😨

     
  • Rob Lambden

    Rob Lambden - 2021-08-12

    In your quote of "It seems to me that having invalid regions on the TLayers is useful as it tells us which regions need to be updated" you missed out the final clause of the sentence: ", but a visible TLayer32 with no invalid regions should still be part of the merged image."

    If children haven't changed then they shouldn't be re-merged. If they are then there's a bug somewhere.

    But it the TGroupLayer32 sets it's size to zero ... the unchanged layers it contains will disappear from the final image. Or at least I'm fairly certain that's what I saw, I've altered my copy of the code now and my unchanged layers don't disappear any more.

     
  • Angus Johnson

    Angus Johnson - 2021-08-12

    you missed out the final clause of the sentence: ", but a visible TLayer32 with no invalid regions should still be part of the merged image."

    Yes, I probably assumed that visible TLayer32 with no invalid regions should implicitly be part of the merged image, and if they aren't then it's a bug. However, I'm not aware of this happening.
     

    But it the TGroupLayer32 sets it's size to zero ... the unchanged layers it contains will disappear from the final image. Or at least I'm fairly certain that's what I saw,

    I'm not aware of this happening either so it would be helpful if you could confirm that this continues to be a problem. Nevertheless, this part of the code is messy and needs fixing.

    Edit: I can confirm there was at least one bug in the layers module that needed fixing which I've just done and uploaded to the repository. I've also addressed most of the other issues raised in this thread. Nevertheless the layers code still needs work.

    Edit2: Yes, I think I've found another bug 😱 where nested groups aren't properly updating their bounds. Still checking ... 😁

    Edit3: Definitely some residual bugs with nested groups. Stay tuned.

     
    👍
    2

    Last edit: Angus Johnson 2021-08-13
  • Angus Johnson

    Angus Johnson - 2021-08-15

    Edit3: Definitely some residual bugs with nested groups. Stay tuned.

    OK, these bugs should be fixed now.
    I've also added a TGroupLayer32.ClipPath property

    The new version (3.1) has been uploaded.
    Sample Layers301 rather crudely demonstrates nesting together with clipping.

     

    Last edit: Angus Johnson 2021-08-15
  • Rob Lambden

    Rob Lambden - 2021-08-17

    Hi Angus, Just to let you know I can't look at your new code just yet as I have something I need to finish ... hopefully by the end of this week I'll be able to give you feedback. Thanks for your responsiveness, hard work and for sharing your code!

     
  • Angus Johnson

    Angus Johnson - 2021-08-17

    Hi Angus, Just to let you know I can't look at your new code just yet

    Hi Rob. No worries! But thanks too for the update. 👍

     
  • Rob Lambden

    Rob Lambden - 2021-09-05

    Hi Angus, I have downloaded and re-integrated the new code (including some changes that you just posted today!).

    I need to be able to move the root TGroupLayer32 of TLayeredImage32 - I realise you may not want to allow this in the core library. However this means I am using my previous routines, and not your Move routine.

    The angle for the Draw3D now seems to be working as it was before - thanks for that.

    The previous problem that I had of transparent areas not being preoprly merged now appears to be fixed - and I think my previous fix for that has been removed ... so that's good.

    I now am back to the problem of layers not being constrained by their parents. Again I appreciate that you may not ant the library to work like this, but it's going to be a big headche for me if the library doesn't behave that way. I will try and fix this but if I can't fix it quickly I will probably have to revert to my previous modified code for the time being. (Adopting the changes from the Merge routine took me a long time as the logic flow seems different ... so I'm not sure if my Sunday afternoon brain will be able to cope with tracing through it!) I would like to keep my code as close to yours as possible so that any future updates are easier to adopt.

    I will let you know how I have got on

     
  • Rob Lambden

    Rob Lambden - 2021-09-05

    OK, not as long as I had feared!

    I still had some problems with areas not being cleared properly - so some areas were not properly redrawn (artifacts from previous draws were left) so I have re-introduced this line at the start of the Merge routine (after the visible / opacity check (and after my call that fires an OnBeforeMerge event):

       image.FillRect(staleRect, $00000000);    //there's nothing there ...
    

    I have then taken out the code at the end of the TGroupLayer32.PreMerge routine which sets the bounds ( so I have commented out from if not Assigned(fGroupOwner) to the end of the routine.

    I have only done very preliminary testing, but it seems to be working as I expect.

     
  • Angus Johnson

    Angus Johnson - 2021-09-05

    Hi again Rob.

    I need to be able to move the root TGroupLayer32 of TLayeredImage32 - I realise you may not want to allow this in the core library. However this means I am using my previous routines, and not your Move routine.

    Yes, I still don't understand why you need to do this. If I was persuaded that this could be useful for quite a number of users, I might consider changing it.

    I now am back to the problem of layers not being constrained by their parents. Again I appreciate that you may not ant the library to work like this, but it's going to be a big headche for me if the library doesn't behave that way.

    The visibility of sublayers can be constrained (by using their group owner's clippath property) as is demonstrated in the Layers301 sample application (see attached image). However, this doesn't constrain sublayer positioning. That could be added, but would be tricky since the clippath isn't necessarily rectangular. Ensuring an object is within an irregular region is complicated and best left to the enduser to optimize since most often the region would be rectangular. (To implement for the general case would not only be difficult, it would dramatically impair performance.)

    Assuming your layers are rectangular, I suggest it wouldn't be too difficult to implement layer constraints external to the library, where you ensure during object creation and repositioning that these constraints are being maintained (and also managing constraints for contained child layers).

    I still had some problems with areas not being cleared properly - so some areas were not properly redrawn

    I haven't noticed that but I'll investigate. I may need you to upload a sample to demonstrate this if I can't duplicate this problem.

    I have then taken out the code at the end of the TGroupLayer32.PreMerge routine

    As I said above the root layer's bounds is supposed to be fixed to the size of the TLayeredImage32 object, but if this works for you then great 😁.

     

    Last edit: Angus Johnson 2021-09-05
  • Rob Lambden

    Rob Lambden - 2021-09-06

    Hi Angus,

    On the size of TGroupLayer32 - it really depends on your paradigm what makes sense in terms of determining the size. HTML / CSS allows designed to control overflow with good reason - there are scenarios where yyou want the display of something to be included, and there are other times when you don't. In my situation the entire form is a single TLayeredImage32. Individual controls are TGroupLayer32 objects. If you imagine Notepad, if the TGroupLayer32 object does not have its size constrained, when you scroll the content of the form, the text will scroll up into the menu bar and title bar. If these are not fully opaque then you will see the text, even if they are drawn on top of it.

    I would have thought an option or subclass with fixed size would be generally useful.

    If I am writing the Gimp then I would probably want the TGroupLayer32 object to always work as your does now. If I am writing something to render HTML (as an example) I would need to b able to fix the size of parts of what I'm drawing.

    On how I am using TGroupLayer32 - I have a number of classes to facilitate implementing a UI. At the bottom level I have an Element which always has a TLayer32 to render on. I then have a CompositeElement which has a TGroupLayer32 to hold the TLayer32 objects for the Elements which it contains. (There are of course other classes in the hierarchy which are superfluous to this discussion). A CompositeElement is an Element - and a TGroupLayer32 is a TLayer32. I allow effects to be applied to an Element, to be implemented after the TImage32 has been rendered. For example a Guassian blur or a recolour. Multiple effects are allowed. As a CompositeElement is an Element I need to allow this too. To enable this to happen I have a BeforeMerge and AfterMerge event on the TGroupLayer32 objects. BeforeMerge gets everything ready (including rendering to the TLayer32 for the various elements it contains ... which of course could be CompositeElements / TGroupLayer32s) In the AfterMerge event I can apply any post rendering effects to the TImage32 of the TGroupLayer32 object before it in turn gets merged.

    I have a Container which is a CompositeElement and has the TLayeredImage32 object, it still renders to its TGroupLayer32 object (the Root of the TLayeredImage32). A Container is implemented as a platfrom control - either a TForm or just a TControl (TWinConrol for VCL). This allows a control to be put onto an ordinary platfrom form if we want to.

    During the life of an Element it can have it's Parent (the CompositeElement that's responsible for showing it) changed many times. Exactly like reparenting a control in the VCL or FMX. It can move between different Containers - and when it does it is moving between different TLayeredImage32s. If the Element (CompositeElement) has the Root of the TLayeredImage32 as it's TGroupLayer32 object then it still needs to move, leaving the previous container (and it's TLayeredImage32) empty.

    At present the instantiation code is likley to create TLayeredImage32 objects that we don't need - because every CompositeElement must be created with a TGroupLayer32 object even though it does not yet have a parent. When it is parented its TLayeredImage32 is discarded.

    So I either need to move the root TGroupLayer32 objects, or I need to have an extra superflous layer and merge for the root that isn't moved.

    It seems to me that if it does make sense to move TGroupLayer32 objects between TLayeredImage32 objects then it would be expected to be able to move the Root object. This process could either create a new root in the previous TLayeredImage32, or dispose of it (which is what I do).

     
  • Angus Johnson

    Angus Johnson - 2021-09-06

    Wow, that's a long explanation. I'll need to read that tomorrow, I certainly won't be able to follow that tonight.

    Also, I've spotted anther bug with TGroupLayer32.ClipPath and hittesting that needs fixing. But to do that I'll also need to write a simpler clipping algorithm because the one I'm currently using isn't appropriate for the task. It's way to powerful (and slow) for what's needed.

     
  • Angus Johnson

    Angus Johnson - 2021-09-07

    If you imagine Notepad, if the TGroupLayer32 object does not have its size constrained, when you scroll the content of the form, the text will scroll up into the menu bar

    Yep, understood. The recently implemented ClipPath property of TGroupLayer32 should fix this (though the ClipPath implementation still needs fixing so it properly masks both drawing and hit-testing).

    I would have thought an option or subclass with fixed size would be generally useful.

    Possibly, or a property.

    As a CompositeElement is an Element I need to allow this too. To enable this to happen I have a BeforeMerge and AfterMerge event on the TGroupLayer32 objects

    I've tried to design Image32 layers so that TGroupLayer32 objects shouldn't need BeforeMerge and AfterMerge events. I don't really understand why you need these but evidently you do.

    So I either need to move the root TGroupLayer32 objects, or I need to have an extra superflous layer and merge for the root that isn't moved.

    Sorry, you've lost me. I still don't understand why you need more than a single TLayeredImage32 object. Surely one extra TGroupLayer32 object is better/simpler than multiple TLayeredImage32 objects?

     
  • Rob Lambden

    Rob Lambden - 2021-09-07

    I've tried to design Image32 layers so that TGroupLayer32 objects shouldn't need BeforeMerge and AfterMerge events. I don't really understand why you need these but evidently you do.

    It allows me to only render the image when I'm ready to update. lots of actions can caue an image to change, I note that a change has happened and signal that a UI refresh is required. Several changes can happen, and can trigger other elements to change as well. When the UI is rendered each CompositeElement (TGroupLayer32) needs to endure that it's children complete all of the updates before merging. That's the BeforeMerge event. The AfterMErge event is to allow post-rendering effects to be applied to the TLayer32 (which is actually a TGroupLayer32) for the CompositeElement if required.

    I still don't understand why you need more than a single TLayeredImage32 object

    You're right, I don't. The problem is rather that I need to have TGroupLayer32 objects before I have the TLayeredImage32 object that they're going to be rendered by. So I when I need the TGRoupLayer32 objet I create a TLayeredImage32 to get one (that I am exepcting to dispose of) and then when it's root is moved it is deleted.

     
  • Angus Johnson

    Angus Johnson - 2021-09-07

    I think I'm getting slightly closer to understanding what you're trying to do.

    The AfterMErge event is to allow post-rendering effects

    What about using layers where you toggle visibility for these effects?
    I'm not totally opposed to using events, but I'm trying to balance functionality with simplicity.

    So I when I need the TGRoupLayer32 objet I create a TLayeredImage32

    Again, couldn't you just create invisible TGRoupLayer32 objects that you later make visible?

    Anyhow, on an unrelated note, I'm now also considering making sublayer positioning relative to their parent layers.

     
  • Rob Lambden

    Rob Lambden - 2021-09-07

    Thanks for this reply - I missed it earlier.

    What about using layers where you toggle visibility for these effects?

    If I apply a Gaussian Blur to a blank Image I would expect to get a blank image ... surely I need to apply the effect to the merged image?

    Again, couldn't you just create invisible TGRoupLayer32 objects that you later make visible?

    There is no TLayeredImage32 in existence yet. From the Create chain we reach this:
    constructor TLayer32.Create(groupOwner: TGroupLayer32; const name: string); begin fGroupOwner := groupOwner; if assigned(groupOwner) then fLayeredImage := groupOwner.fLayeredImage else if name <> rsRoot then raise Exception.Create(rsCreateLayerError); ...

    So we would have to create all of our TGroup32Layers as root layers anyway ... and I would expect an orphaned TGroupLayer32 to have some unpredictable behaviour.

    Anyhow, on an unrelated note, I'm now also considering making sublayer positioning relative to their parent layers.

    Thanks for the heads up - this bit me last time when I was changing from having multiple TLayeredImage32's. I now have each Element (which maps to TLayer32) hold it's position (offest from it's parent) and set the TLayer32's position to it's 'ContainerOffset' which is the sum of the positions of itself and its parents. Setting the TLayer32 position to be relative to its parent is preferred for me.

     
  • Angus Johnson

    Angus Johnson - 2021-09-07

    If I apply a Gaussian Blur to a blank Image I would expect to get a blank image ... surely I need to apply the effect to the merged image?

    Yes, that's a good point.
    And I think you've convinced me to add a post merge event for TGroupLayer32 objects 😱.

    There is no TLayeredImage32 in existence yet.

    Obviously you'll eventually have a TLayeredImage32 object so I don't understand why don't/can't create this first, and add to it any number of TGroupLayer32 objects (making them invisible if they aren't ready to be drawn)?

    Setting the TLayer32 position to be relative to its parent is preferred for me.

    Yes, it makes sense and I don't know why I didn't do this in the first place. But when I change it, it'll be a nuisance for anyone who's already using layers.

     
  • Angus Johnson

    Angus Johnson - 2021-09-08

    Setting the TLayer32 position to be relative to its parent is preferred for me.

    Yes, it makes sense and I don't know why I didn't do this in the first place. But when I change it, it'll be a nuisance for anyone who's already using layers.

    Having given this further thought, the whole group layers concept really needs redesigning, such that TLayer32 objects can directly own other layers, essentially merging the TGroupLayer32 and TLayer32 classes. That might take a week.

     
  • Rob Lambden

    Rob Lambden - 2021-09-08

    Hi Angus,

    One of the things that I am thinking about for my UI code is to have additional layers which are separate from the CompositeElement (TGroupLayer32) that houses a control. This would be used for effects. For example all controls could cast a shadow on the form ... so the form background is the lowest layer, then the layers for the shadows, then the (Group) layers for the components. I may want to show a glow over the top of the controls, so there could be an effects layer at the top.

    I mention this only because it's an example of how I am using layers as you think about changing them. I am not expecting anything in the library to provide additional layer linking (but of course,I'am also not expecting anything in the library to prevent additional layer linking by the application ...

     
  • Rob Lambden

    Rob Lambden - 2021-09-08

    Obviously you'll eventually have a TLayeredImage32 object so I don't understand why don't/can't create this first, and add to it any number of TGroupLayer32 objects (making them invisible if they aren't ready to be drawn)?

    Whether or not I can create one first depends on the scenario. In the case where the TLayeredImage is for a Form, then it means I would always have to know the form that a control is going to be shown on - that's a restricition that's not in VCL or FMX ... the only thing that's needed at creation time is the Component that is responsible for freeing it.

    There are lots of scenarios where part of the UI is created without knowing what it's going to be shown on (and it may never be shown at all).

    In the VCL / FMX example I can only know the TLayeredImage32 when I set the Parent.
    Currently there are actions which can affect the TLayer32 (like setting Size) which can happen before the control has a parent.

    At the moment all of my creation happens in AfterConstruction (this is because I read some article somewhere that that was the Delphi way to do it, and I was porting lots of C++ code to Delphi and regularly tripping over things where Delphi didn't work the way I expected it to so I was trying to rigorously adopt a Delphiesque way of working). For a TForm this means any contents of the form are streamed before I have a chance to create a TLayeredImage32. I almost certainly could create the TLayeredImage32 in the constructor, but of course that's not the only thing I would need and I haven't had a chance to look at that. And that only helps me for the situation where I do know the PArent at the time of creating the control ...

    I do currently create a TLayeredImage32 object which is almost the same thing as you are saying - except that I expect to dispose of it. That's because may paradigm is UI centric, not TLayeredImage32 centric.

     
  • Angus Johnson

    Angus Johnson - 2021-09-08

    OK, I'm redesigning layers yet again 😱 so I'll keep in mind your request to allow layers to be disconnected from a TLayeredImage32 object. But no promises 😜.

     
1 2 > >> (Page 1 of 2)
MongoDB Logo MongoDB