Can't apply multiple transformation to UIImageView - xamarin

I can't find out why my transformations are not being applied to a UIImageView in Xamarin iOS.
Here's the code:
CGAffineTransform transform = new CGAffineTransform();
transform.Rotate((float)Math.PI / 2);
transform.Scale(0.8f, 0.8f);
this.imageViewOverlay = new UIImageView
{
ContentMode = UIViewContentMode.Center,
BackgroundColor = UIColor.Clear,
Opaque = false,
Frame = new CGRect(0,0,this.View.Bounds.Width, this.View.Bounds.Height),
Transform = transform
};
The way I was doing it before used to be like
Transform = CGAffineTransform.MakeRotation((float)Math.PI / 2)
But unfortunately when you want to use it twice, the second one is clearing the transformation before.
I'm using
Xamarin.iOS:
Version: 8.6.0.41 (Business Edition)
Operating System:
Mac OS X 10.10.3
XCode: Version 6.3.1 (6D1002)
Thanks !

Your original CGAffineTransform is empty (all 00), default for a struct, and not an identity transform, on top of which you can construct something. IOW that makes all subsequent calls (math ops) return an empty one.
You can either start with a non-empty transform, e.g.
var transform = CGAffineTransform.MakeRotation((float)Math.PI / 2);
transform.Scale(0.8f, 0.8f);
or you can start with an identity transform, e.g.
var transform = CGAffineTransform.MakeIdentity ();
transform.Rotate ((nfloat) Math.PI / 2);
transform.Scale ((nfloat) 0.8f, (nfloat) 0.8f);

Related

keep the center of a sprite fixed while rotating

I'm using Cocos2d-js v3 to create a jackpot-spinner. When I'm rotating the spinner wheel sprite, it wobbles, as in it moves in x,y axis slightly too while rotataing. How can I keep the sprite fixed while rotating? I'm new to Cocos2d-js.
Here is my creating the sprite code -
sprite = new cc.Sprite.create(res.wheel_png);
sprite.setPosition(cc.p(size.width/2, size.height/2));
this.addChild(sprite, 0);
And rotation code -
var rand = Math.random();
var sprite_action = cc.RotateBy.create(2, 1370);
var repeat_action = cc.Repeat.create(sprite_action, rand);
sprite.runAction(repeat_action);
Create method is deprecated, as are some others. Check out this page for list of all things deprecated.
As per the cocos docs for v3, (look up "rotateby")
Field Detail <static> {cc.RotateBy} cc.RotateBy.create
Please use cc.rotateBy instead. Rotates a cc.Node object clockwise a number of degrees by modifying it's rotation attribute. Relative to its properties to modify.
Deprecated:since v3.0
Please use cc.rotateBy instead.
You need to use it like this:
var rand = Math.random();
var sprite_action = cc.rotateBy(2, 1370);
var repeat_action = cc.repeat(sprite_action, rand);
sprite.runAction(repeat_action);

How can I get rid of artifacts in ImageSource created with SkiaSharp

I created an app in which I want to display text on top of google maps. I chose to use custom markers, but they can only be images, so I decided to create an image from my text utilizing SkiaSharp.
private static ImageSource CreateImageSource(string text)
{
int numberSize = 20;
int margin = 5;
SKBitmap bitmap = new SKBitmap(30, numberSize + margin * 2, SKImageInfo.PlatformColorType, SKAlphaType.Premul);
SKCanvas canvas = new SKCanvas(bitmap);
SKPaint paint = new SKPaint
{
Style = SKPaintStyle.StrokeAndFill,
TextSize = numberSize,
Color = SKColors.Red,
StrokeWidth = 1,
};
canvas.DrawText(text.ToString(), 0, numberSize, paint);
SKImage skImage = SKImage.FromBitmap(bitmap);
SKData data = skImage.Encode(SKEncodedImageFormat.Png, 100);
return ImageSource.FromStream(data.AsStream);
}
The images I create however have ugly artifacts on the top of the resulting image and my feeling is that they get worse if I create multiple images.
I built an example app, that shows the artifacts and the code I used to draw the text. It can be found here:
https://github.com/hot33331/SkiaSharpExample
How can I get rid of those artifacts. Am I using skia wrong?
I got the following answer from Matthew Leibowitz on the SkiaSharp GitHub:
The chances are you are not clearing the canvas/bitmap first.
You can either do bitmap.Erase(SKColors.Transparent) or canvas.Clear(SKColors.Transparent) (you can use any color).
The reason for this is performance. When creating a new bitmap, the computer has no way of knowing what background color you want. So, if it was to go transparent and you wanted white, then there would be two draw operations to clear the pixels (and this may be very expensive for large images).
During the allocation of the bitmap, the memory is provided, but the actual data is untouched. If there was anything there previously (which there will be), this data appears as colored pixels.
When I've seen that before, it's been because the memory passed to SkiaSharp was not zeroed. As an optimization, though, Skia assumes that the memory block passed to it is pre zeroed. Resultingly, if your first operation is a clear, it will ignore that operation, because it thinks that the state is already clean. To resolve this issue, you can manually zero the memory passed to SkiaSharp.
public static SKSurface CreateSurface(int width, int height)
{
// create a block of unmanaged native memory for use as the Skia bitmap buffer.
// unfortunately, this may not be zeroed in some circumstances.
IntPtr buff = System.Runtime.InteropServices.Marshal.AllocCoTaskMem(width * height * 4);
byte[] empty = new byte[width * height * 4];
// copy in zeroed memory.
// maybe there's a more sanctioned way to do this.
System.Runtime.InteropServices.Marshal.Copy(empty, 0, buff, width * height * 4);
// create the actual SkiaSharp surface.
var colorSpace = CGColorSpace.CreateDeviceRGB();
var bContext = new CGBitmapContext(buff, width, height, 8, width * 4, colorSpace, (CGImageAlphaInfo)bitmapInfo);
var surface = SKSurface.Create(width, height, SKColorType.Rgba8888, SKAlphaType.Premul, bitmap.Data, width * 4);
return surface;
}
Edit: btw, I assume this is a bug in SkiaSharp. The samples/apis that create the buffer for you should probably be zeroing it out. Depending on the platform it can be hard to repro as the memory alloc behaves differently. More or less likely to provide you untouched memory.

CGWindowListCreateImage yields blurred cgImage when zoomed

I'm developing a magnifying glass like application for mac. My goal is to be able to pinpoint individual pixels when zoomed in. I'm using this code in mouseMoved(with event: NSEvent):
let captureSize = self.frame.size.width / 9 //9 is the scale factor
let screenFrame = (NSScreen.main()?.frame)!
let x = floor(point.x) - floor(captureSize / 2)
let y = screenFrame.size.height - floor(point.y) - floor(captureSize / 2)
let windowID = CGWindowID(self.windowNumber)
cgImageExample = CGWindowListCreateImage(CGRect(x: x, y: y, width: captureSize,
height: captureSize), CGWindowListOption.optionOnScreenBelowWindow, windowID,
CGWindowImageOption.bestResolution)
The creation of the cgImage takes place in the CGWindowListCreateImage method. When I later draw this in an NSView, the result looks like this:
It looks blurred / like some anti-aliasing was applied during the creation of the cgImage. My goal is to get a razor sharp representation of each pixel. Can anyone point me in the right direction?
Ok, I figured it out. It was a matter of setting the interpolation quality to none on the drawing context:
context.interpolationQuality = .none
Result:
On request some more code:
//get the context
guard let context = NSGraphicsContext.current()?.cgContext else { return }
//get the CGImage
let image: CGImage = //pass the result from CGWindowListCreateImage call
//draw
context.draw(image, in: (CGRect of choice))

SWIFT OSX NSBezierPath Linewidth property not working

I am using the following code to draw a line graph on a custom NSView
for var index = 0; index < (dataPointsArray.count - 1); index++ {
NSBezierPath().lineWidth = 20.0
NSBezierPath.strokeLineFromPoint(dataPointsArray[index], toPoint: dataPointsArray[index + 1])
}
This snippet is contained within a function that is called by drawRect() within the Custom View.
The line draws correctly within the coordinate system of the view. However, the line is drawn at the same width (one pixel width) regardless of the .lineWidth setting (e.g., 5.0, 10.0, 20.0 etc) which seems to have no impact on the line that is actually drawn).
Is anyone able to advise what might be creating this issue for me. I haven't been able to find a previous question that raises this issue.
NSBezierPath().lineWidth = 20.0
The () means you are initializing a new instance of the class and set its lineWidth to 20.0. You should create a variable and use it to draw your path:
var bezierPath = NSBezierPath()
bezierPath.lineWidth = 20.0
bezierPath.moveToPoint(dataPointsArray[index])
bezierPath.lineToPoint(dataPointsArray[index + 1])

Core Text's CTFramesetterSuggestFrameSizeWithConstraints() returns incorrect size every time

According to the docs, CTFramesetterSuggestFrameSizeWithConstraints () "determines the frame size needed for a string range".
Unfortunately the size returned by this function is never accurate. Here is what I am doing:
NSAttributedString *string = [[[NSAttributedString alloc] initWithString:#"lorem ipsum" attributes:nil] autorelease];
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef) string);
CGSize textSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0,0), NULL, CGSizeMake(rect.size.width, CGFLOAT_MAX), NULL);
The returned size always has the correct width calculated, however the height is always slightly shorter than what is expected.
Is this the correct way to use this method?
Is there any other way to layout Core Text?
Seems I am not the only one to run into problems with this method. See https://devforums.apple.com/message/181450.
Edit:
I measured the same string with Quartz using sizeWithFont:, supplying the same font to both the attributed string, and to Quartz. Here are the measurements I received:
Core Text: 133.569336 x 16.592285
Quartz: 135.000000 x 31.000000
try this.. seem to work:
+(CGFloat)heightForAttributedString:(NSAttributedString *)attrString forWidth:(CGFloat)inWidth
{
CGFloat H = 0;
// Create the framesetter with the attributed string.
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString( (CFMutableAttributedStringRef) attrString);
CGRect box = CGRectMake(0,0, inWidth, CGFLOAT_MAX);
CFIndex startIndex = 0;
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, box);
// Create a frame for this column and draw it.
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(startIndex, 0), path, NULL);
// Start the next frame at the first character not visible in this frame.
//CFRange frameRange = CTFrameGetVisibleStringRange(frame);
//startIndex += frameRange.length;
CFArrayRef lineArray = CTFrameGetLines(frame);
CFIndex j = 0, lineCount = CFArrayGetCount(lineArray);
CGFloat h, ascent, descent, leading;
for (j=0; j < lineCount; j++)
{
CTLineRef currentLine = (CTLineRef)CFArrayGetValueAtIndex(lineArray, j);
CTLineGetTypographicBounds(currentLine, &ascent, &descent, &leading);
h = ascent + descent + leading;
NSLog(#"%f", h);
H+=h;
}
CFRelease(frame);
CFRelease(path);
CFRelease(framesetter);
return H;
}
For a single line frame, try this:
line = CTLineCreateWithAttributedString((CFAttributedStringRef) string);
CGFloat ascent;
CGFloat descent;
CGFloat width = CTLineGetTypographicBounds(line, &ascent, &descent, NULL);
CGFloat height = ascent+descent;
CGSize textSize = CGSizeMake(width,height);
For multiline frames, you also need to add the line's lead (see a sample code in Core Text Programming Guide)
For some reason, CTFramesetterSuggestFrameSizeWithConstraints() is using the difference in ascent and descent to calculate the height:
CGFloat wrongHeight = ascent-descent;
CGSize textSize = CGSizeMake(width, wrongHeight);
It could be a bug?
I'm having some other problems with the width of the frame; It's worth checking out as it only shows in special cases. See this question for more.
The problem is that you have to apply a paragraph style to the text before you measure it. If you don't then you get the default leading of 0.0. I provided a code sample for how to do this in my answer to a duplicate of this question here https://stackoverflow.com/a/10019378/1313863.
ing.conti's answer but in Swift 4:
var H:CGFloat = 0
// Create the framesetter with the attributed string.
let framesetter = CTFramesetterCreateWithAttributedString(attributedString as! CFMutableAttributedString)
let box:CGRect = CGRect.init(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude)
let startIndex:CFIndex = 0
let path:CGMutablePath = CGMutablePath()
path.addRect(box)
// Create a frame for this column and draw it.
let frame:CTFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(startIndex, 0), path, nil)
// Start the next frame at the first character not visible in this frame.
//CFRange frameRange = CTFrameGetVisibleStringRange(frame);
//startIndex += frameRange.length;
let lineArray:CFArray = CTFrameGetLines(frame)
let lineCount:CFIndex = CFArrayGetCount(lineArray)
var h:CGFloat = 0
var ascent:CGFloat = 0
var descent:CGFloat = 0
var leading:CGFloat = 0
for j in 0..<lineCount {
let currentLine = unsafeBitCast(CFArrayGetValueAtIndex(lineArray, j), to: CTLine.self)
CTLineGetTypographicBounds(currentLine, &ascent, &descent, &leading)
h = ascent + descent + leading;
H+=h;
}
return H;
I did try and keep it as 1:1 with the Objective C code but Swift is not as nice when handling pointers so some changes were required for casting.
I also did some benchmarks comparing this code (and it's ObjC counterpart) to another height methods. As a heads up, I used a HUGE and very complex attributed string as input and also did it on the sim so the times themselves are meaningless however the relative speeds are correct.
Runtime for 1000 iterations (ms) BoundsForRect: 8909.763097763062
Runtime for 1000 iterations (ms) layoutManager: 7727.7010679244995
Runtime for 1000 iterations (ms) CTFramesetterSuggestFrameSizeWithConstraints: 1968.9229726791382
Runtime for 1000 iterations (ms) CTFramesetterCreateFrame ObjC: 1941.6030206680298
Runtime for 1000 iterations (ms) CTFramesetterCreateFrame-Swift: 1912.694974899292
It might seem strange but I found that if you use ceil function first and then add +1 to the height it will always work. Many third party APIs use this trick.
Resurrecting.
When initially determining where lines should be placed within a frame, Core Text seems to massage the ascent+descent for the purposes of line origin calculation. In particular, it seems like 0.2*(ascent+descent) is added to the ascent, and then both the descent and resultant ascent are modified by floor(x + 0.5), and then the baseline positions are calculated based on these adjusted ascents and descents. Both of these steps are affected by certain conditions whose nature I am not sure, and I also already forgot at which point paragraph styles are taken into account, despite only looking into it a few days ago.
I've already resigned to just considering a line to start at its baseline and not trying to figure out what the actual lines land at. Unfortunately, this still does not seem to be enough: paragraph styles are not reflected in CTLineGetTypographicBounds(), and some fonts like Klee that have nonzero leadings wind up crossing the path rect! Not sure what to do about this... probably for another question.
UPDATE
It seems CTLineGetBoundsWithOptions(line, 0) does get the proper line bounds, but not quite fully: there's a gap between lines, and with some fonts (Klee again) the gap is negative and the lines overlap... Not sure what to do about this. :| At least we're slightly closer??
And even then it still does not take paragraph styles into consideration >:|
CTLineGetBoundsWithOptions() is not listed on Apple's documentation site, possibly due to a bug in the current version of their documentation generator. It is a fully documented API, however — you'll find it in the header files and it was discussed at length at WWDC 2012 session 226.
None of the options are relevant to us: they reduce the bounds rect by taking certain font design choices into consideration (or increase the bounds rect randomly, in the case of the new kCTLineBoundsIncludeLanguageExtents). One useful option in general, though, is kCTLineBoundsUseGlyphPathBounds, which is equivalent to CTLineGetImageBounds() but without needing to specify a CGContext (and thus without being subject to an existing text matrix or CTM).
After weeks of trying everything, any combination possible, I made a break through and found something that works. This issue seems to be more prominent on macOS than on iOS, but still appears on both.
What worked for me was to use a CATextLayer instead of a NSTextField (on macOS) or a UILabel (on iOS).
And using boundingRect(with:options:context:) instead of CTFramesetterSuggestFrameSizeWithConstraints. Even though in theory the latter should be more lower level than the former, and I was assuming would be more precise, the game changer turns out to be NSString.DrawingOptions.usesDeviceMetrics.
The frame size suggested fits like a charm.
Example:
let attributedString = NSAttributedString(string: "my string")
let maxWidth = CGFloat(300)
let size = attributedString.boundingRect(
with: .init(width: maxWidth,
height: .greatestFiniteMagnitude),
options: [
.usesFontLeading,
.usesLineFragmentOrigin,
.usesDeviceMetrics])
let textLayer = CATextLayer()
textLayer.frame = .init(origin: .zero, size: size)
textLayer.contentsScale = 2 // for retina
textLayer.isWrapped = true // for multiple lines
textLayer.string = attributedString
Then you can add the CATextLayer to any NSView/UIView.
macOS
let view = NSView()
view.wantsLayer = true
view.layer?.addSublayer(textLayer)
iOS
let view = UIView()
view.layer.addSublayer(textLayer)

Resources