Is there a way to have consistent strokeWidth across devices? - android-jetpack-compose

I have a multiplatform project where I draw some points with
drawPoints(dataPts, PointMode.Points, Color.Blue, strokeWidth = 5f)
The point size appear proper on Linux desktop but is too small on Android phone. Is there a way to have roughly similar presentation?

You can convert your dp to px before drawing with DrawScope
val strokeWidth = with(LocalDensity.current) {
2.dp.toPx()
}
Canvas(modifier=Modifier) {
// you can set here too
val strokeWidth = 2.dp.toPx()
drawPoints(listOf(Offset(100f,100f)), PointMode.Points, Color.Blue, strokeWidth = strokeWidth)
}

Related

How to draw ticket shape in Jetpack Compose

I would like to draw the ticket shape in the picture using Path in Jetpack Compose
Path().apply
Help is appreciated.
class TicketShape(private val cornerRadius: Float) : Shape {
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density
): Outline {
return Outline.Generic(
// Draw your custom path here
path = drawTicketPath(size = size, cornerRadius = cornerRadius)
)
}
}
fun drawTicketPath(size: Size, cornerRadius: Float): Path {
return Path().apply {
reset()
// Top left arc
arcTo(
rect = Rect(
left = -cornerRadius,
top = -cornerRadius,
right = cornerRadius,
bottom = cornerRadius
),
startAngleDegrees = 90.0f,
sweepAngleDegrees = -90.0f,
forceMoveTo = false
)
lineTo(x = size.width - cornerRadius, y = 0f)
// Top right arc
arcTo(
rect = Rect(
left = size.width - cornerRadius,
top = -cornerRadius,
right = size.width + cornerRadius,
bottom = cornerRadius
),
startAngleDegrees = 180.0f,
sweepAngleDegrees = -90.0f,
forceMoveTo = false
)
lineTo(x = size.width, y = size.height - cornerRadius)
// Bottom right arc
arcTo(
rect = Rect(
left = size.width - cornerRadius,
top = size.height - cornerRadius,
right = size.width + cornerRadius,
bottom = size.height + cornerRadius
),
startAngleDegrees = 270.0f,
sweepAngleDegrees = -90.0f,
forceMoveTo = false
)
lineTo(x = cornerRadius, y = size.height)
// Bottom left arc
arcTo(
rect = Rect(
left = -cornerRadius,
top = size.height - cornerRadius,
right = cornerRadius,
bottom = size.height + cornerRadius
),
startAngleDegrees = 0.0f,
sweepAngleDegrees = -90.0f,
forceMoveTo = false
)
lineTo(x = 0f, y = cornerRadius)
close()
}
}
Now use it with any Composable,
MyComp(
Modifier.clip(
TicketShape( 24.dp.toPx() )
)
)
Source: https://juliensalvi.medium.com/custom-shape-with-jetpack-compose-1cb48a991d42
We're basically inheriting from a Shape object, which is something the clip modifiers and other shape parameters accept to render the desired shape. Other examples are RectangleShape, CircleShape, RoundedCornerShape, etc, which are pre-built for compose. You need a distinct shape and so, you require to create your own Shape object. You didn't exactly need to create the class separately if you don't wish to use it again and again. In that case, an object at the place of usage should have sufficed, but it appears that it is not just a decoration so you might wanna create the class itself to not have to create the same objects at multiple places.

Add shader for SKShapeNode

I want to add drop shadow effect for a SKShapeNode. I found a Emboss shader here.
Here is my code:
let node = SKShapeNode(rectOf: CGSize(width: w, height: h),cornerRadius: w / 4.0)
node.position = CGPoint(x: x, y: frame.size.height + h / 2)
node.fillColor = color
node.strokeColor = .clear
node.fillShader = createColorEmboss()
let v = simd_make_float2(Float(w),Float(h))
node.setValue(SKAttributeValue(vectorFloat2: v), forAttribute: "a_size")
func createColorEmboss() -> SKShader {
let source = "void main() {" +
"vec4 current_color = SKDefaultShading();" +
"if (current_color.a > 0.0) {" +
"vec2 pixel_size = 1.0 / a_size;" +
"vec4 new_color = current_color;" +
"new_color += texture2D(u_texture, v_tex_coord + pixel_size) * u_strength;" +
"new_color -= texture2D(u_texture, v_tex_coord - pixel_size) * u_strength;" +
"gl_FragColor = vec4(new_color.rgb, 1) * current_color.a * v_color_mix.a;" +
"} else {" +
"gl_FragColor = current_color;" +
"}" +
"}"
let shader = SKShader(source: source, uniforms: [SKUniform(name: "u_strength", float: 1)])
shader.attributes = [SKAttribute(name: "a_size", type: .vectorFloat2)]
return shader
}
But nothing happened. Any idea?
There are some big differences between a textured (*) SKShapeNode and SKSpriteNode here:
If the SKSpriteNode has a texture and a custom shader (shader property), then the shader will have access to the original texture via the u_texture uniform, and it will be able use it for rendering the node, i.e. by applying various effects to it like emboss, blur, etc.
SKShapeNode may have a fillTexture and a fillShader, but the way they work is different:
fillTexture is a rectangular texture that's converted to grayscale and used to set the luminosity of fillColor in the areas of the shape node which are filled.
fillShader can only see fillTexture as this rectangular image before it gets used for the final render, with no way to modify, or even see, which areas of the rectangle will be visible (part of the fill), and how the final render will look.
If there is no explicit fillTexture set, the u_texture uniform seems to pretend that it's an endless expanse of whiteness: whatever coordinate you ask about, it will return the color white, even if you go out of its bounds.
(*) You can also create a SKSpriteNode with just a color; its behavior will be a weird mix of SKShapeNode and a textured SKSpriteNode, and it's not helpful for our discussion, so I'll be ignoring this kind.
What you can do is to rasterize your SKShapeNode into a texture, and create an SKSpriteNode from it. It's actually very simple:
let node = SKShapeNode(rectOf: CGSize(width: w, height: h),cornerRadius: w / 4.0)
node.fillColor = color
node.strokeColor = .clear
let theTexture = view.texture(from:node)
let spriteNode = SKSpriteNode(texture:theTexture)
spriteNode.position = CGPoint(x: x, y: frame.size.height + h / 2)
spriteNode.shader = createColorEmboss()
let v = simd_make_float2(Float(w),Float(h))
spriteNode.setValue(SKAttributeValue(vectorFloat2: v), forAttribute: "a_size")
As you can see, you don't even have to add the node to the scene; the view will render it into a texture, with a clear background. You can also use view.texture(from:crop:) to specify a larger crop that can accommodate a drop shadow if it extends beyond the original shape.
I must warn you, however, that this particular shader you are trying to use may not be what you need for a drop shadow…

Pixel Gap Between CGRect

I'm drawing bar graphs, and I have several stacked CGRects that are directly on top of each other (i.e. one rect's minY is the previous rect's maxY). However, there are still semi-transparent gaps between the rects. Is there any way to fix this? I've found that this also happens when drawing touching adjacent arcs.
Here's a screenshot of what I mean:
By zooming in, I've confirmed that this isn't just an optical illusion like one would find between adjacent red and blue rects. I would appreciate any input.
var upToNowSegmentTotal: CGFloat = 0
for k in 0..<data[i].bars[j].segments.count {
var segmentRect: CGRect = CGRect()
if barDirection == "vertical" {
let a: CGFloat = translateY(upToNowSegmentTotal)
let b: CGFloat = translateY(upToNowSegmentTotal + data[i].bars[j].segments[k].length)
upToNowSegmentTotal += data[i].bars[j].segments[k].length
var rectY: CGFloat
if a > b {
rectY = b
} else {
rectY = a
}
segmentRect = CGRect(
x: barWidthPosition,
y: rectY,
width: barWidthAbsolute,
height: abs(a - b)
)
}
}
Ignore the stuff about the width of the bars. Here's the translateY function. Basically, it translates coordinates from the graphing window into x/y stuff that's drawn. Remember that because the window/ graphing area does not change between drawn rects, the same y input will always produce the same result.
private func translateY(y: CGFloat) -> CGFloat {
if barsAreReversed {
return graphingArea.minY + graphingArea.height * (y - graphingWindow.startValue) / (graphingWindow.length)
} else {
return graphingArea.maxY - graphingArea.height * (y - graphingWindow.startValue) / (graphingWindow.length)
}
}
EDIT 2:
Here's a simplified version of my code that shows the problem:
override func drawRect(rect: CGRect) {
let rect1: CGRect = CGRect(
x: 0,
y: 0,
width: 40,
height: 33.7
)
let rect2: CGRect = CGRect(
x: 0,
y: rect1.height,
width: 40,
height: 33.5
)
let context: CGContextRef = UIGraphicsGetCurrentContext()
CGContextSetFillColorWithColor(context, UIColor(red: 1 / 255, green: 29 / 255, blue: 29 / 255, alpha: 1).CGColor)
CGContextAddRect(context, rect1)
CGContextFillRect(context, rect1)
CGContextSetFillColorWithColor(context, UIColor(red: 9 / 255, green: 47 / 255, blue: 46 / 255, alpha: 1).CGColor)
CGContextAddRect(context, rect2)
CGContextFillRect(context, rect2)
}
It produces this:
I suspect that in this particular case, the rects you are filling are not integral, i.e they might have origins/heights that are by default rendered with slightly transparent pixels (anti-aliasing). You could avoid this by properly rounding your Y-axis translation
private func translateY(y: CGFloat) -> CGFloat {
if barsAreReversed {
return round(graphingArea.minY + graphingArea.height * (y - graphingWindow.startValue) / (graphingWindow.length))
} else {
return round(graphingArea.maxY - graphingArea.height * (y - graphingWindow.startValue) / (graphingWindow.length))
}
}
With arcs and other shapes it is not as easy, however, you could try and get rid of it, by leaving a bit of overlap between shapes. Of course, as pointed out by matt, you could simply turn anti-aliasing off, in which case these transparent "half-pixels" will all be rendered as if they are actually fully-within the rect.
This is likely happening because the rectangle coordinates you are using to draw shapes are fractional values. As a result Core Graphics performs antialiasing at the edges of those rectangles when your coordinates land between pixel boundaries.
You could solve this by simply rounding the coordinates of the rectangles before drawing. You can use the CGRectIntegral function which performs this kind of rounding, for example:
CGContextFillRect(context, CGRectIntegral(rect1))
It's antialiasing. I can prevent this phenomenon by using your exact same code but drawing in a CGContext in which we have first called CGContextSetAllowsAntialiasing(context, false). Here it is without that call:
And here it is with that call:
But, as others have said, we can get the same result by changing your 33.7 and 33.5 to 40, so that we come down on pixel boundaries.

Using a Bezier Curve to draw a spiral

This is for an iPad application, but it is essentially a math question.
I need to draw a circular arc of varying (monotonically increasing) line width. At the beginning of the curve, it would have a starting thickness (let's say 2pts) and then the thickness would smoothly increase until the end of the arc where it would be at its greatest thickness (let's say 12pts).
I figure the best way to make this is by creating a UIBezierPath and filling the shape. My first attempt was to use two circular arcs (with offset centers), and that worked fine up to 90°, but the arc will often be between 90° and 180°, so that approach won't cut it.
My current approach is to make a slight spiral (one slightly growing from the circular arc and one slightly shrinking) using bezier quad or cubic curves. The question is where do I put the control points so that the deviation from the circular arc (aka the shape "thickness") is the value I want.
Constraints:
The shape must be able to start and end at an arbitrary angle (within 180° of each other)
The "thickness" of the shape (deviation from the circle) must start and end with the given values
The "thickness" must increase monotonically (it can't get bigger and then smaller again)
It has to look smooth to the eye, there can't be any sharp bends
I am open to other solutions as well.
My approach just constructs 2 circular arcs and fills the region in between. The tricky bit is figuring out the centers and radii of these arcs. Looks quite good provided the thicknesses are not too large. (Cut and paste and decide for yourself if it meet your needs.) Could possibly be improved by use of a clipping path.
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGMutablePathRef path = CGPathCreateMutable();
// As appropriate for iOS, the code below assumes a coordinate system with
// the x-axis pointing to the right and the y-axis pointing down (flipped from the standard Cartesian convention).
// Therefore, 0 degrees = East, 90 degrees = South, 180 degrees = West,
// -90 degrees = 270 degrees = North (once again, flipped from the standard Cartesian convention).
CGFloat startingAngle = 90.0; // South
CGFloat endingAngle = -45.0; // North-East
BOOL weGoFromTheStartingAngleToTheEndingAngleInACounterClockwiseDirection = YES; // change this to NO if necessary
CGFloat startingThickness = 2.0;
CGFloat endingThickness = 12.0;
CGPoint center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
CGFloat meanRadius = 0.9 * fminf(self.bounds.size.width / 2.0, self.bounds.size.height / 2.0);
// the parameters above should be supplied by the user
// the parameters below are derived from the parameters supplied above
CGFloat deltaAngle = fabsf(endingAngle - startingAngle);
// projectedEndingThickness is the ending thickness we would have if the two arcs
// subtended an angle of 180 degrees at their respective centers instead of deltaAngle
CGFloat projectedEndingThickness = startingThickness + (endingThickness - startingThickness) * (180.0 / deltaAngle);
CGFloat centerOffset = (projectedEndingThickness - startingThickness) / 4.0;
CGPoint centerForInnerArc = CGPointMake(center.x + centerOffset * cos(startingAngle * M_PI / 180.0),
center.y + centerOffset * sin(startingAngle * M_PI / 180.0));
CGPoint centerForOuterArc = CGPointMake(center.x - centerOffset * cos(startingAngle * M_PI / 180.0),
center.y - centerOffset * sin(startingAngle * M_PI / 180.0));
CGFloat radiusForInnerArc = meanRadius - (startingThickness + projectedEndingThickness) / 4.0;
CGFloat radiusForOuterArc = meanRadius + (startingThickness + projectedEndingThickness) / 4.0;
CGPathAddArc(path,
NULL,
centerForInnerArc.x,
centerForInnerArc.y,
radiusForInnerArc,
endingAngle * (M_PI / 180.0),
startingAngle * (M_PI / 180.0),
!weGoFromTheStartingAngleToTheEndingAngleInACounterClockwiseDirection
);
CGPathAddArc(path,
NULL,
centerForOuterArc.x,
centerForOuterArc.y,
radiusForOuterArc,
startingAngle * (M_PI / 180.0),
endingAngle * (M_PI / 180.0),
weGoFromTheStartingAngleToTheEndingAngleInACounterClockwiseDirection
);
CGContextAddPath(context, path);
CGContextSetFillColorWithColor(context, [UIColor redColor].CGColor);
CGContextFillPath(context);
CGPathRelease(path);
}
One solution could be to generate a polyline manually. This is simple but it has the disadvantage that you'd have to scale up the amount of points you generate if the control is displayed at high resolution. I don't know enough about iOS to give you iOS/ObjC sample code, but here's some python-ish pseudocode:
# lower: the starting angle
# upper: the ending angle
# radius: the radius of the circle
# we'll fill these with polar coordinates and transform later
innerSidePoints = []
outerSidePoints = []
widthStep = maxWidth / (upper - lower)
width = 0
# could use a finer step if needed
for angle in range(lower, upper):
innerSidePoints.append(angle, radius - (width / 2))
outerSidePoints.append(angle, radius + (width / 2))
width += widthStep
# now we have to flip one of the arrays and join them to make
# a continuous path. We could have built one of the arrays backwards
# from the beginning to avoid this.
outerSidePoints.reverse()
allPoints = innerSidePoints + outerSidePoints # array concatenation
xyPoints = polarToRectangular(allPoints) # if needed
A view with a spiral .. 2023
It's very easy to draw a spiral mathematically and there are plenty of examples around.
https://github.com/mabdulsubhan/UIBezierPath-Spiral/blob/master/UIBezierPath%2BSpiral.swift
Put it in a view in the obvious way:
class Example: UIView {
private lazy var spiral: CAShapeLayer = {
let s = CAShapeLayer()
s.strokeColor = UIColor.systemPurple.cgColor
s.fillColor = UIColor.clear.cgColor
s.lineWidth = 12.0
s.lineCap = .round
layer.addSublayer(s)
return s
}()
private lazy var sp: CGPath = {
let s = UIBezierPath.getSpiralPath(
center: bounds.centerOfCGRect(),
startRadius: 0,
spacePerLoop: 4,
startTheta: 0,
endTheta: CGFloat.pi * 2 * 5,
thetaStep: 10.radians)
return s.cgPath
}()
override func layoutSubviews() {
super.layoutSubviews()
clipsToBounds = true
spiral.path = sp
}
}

Is the Bluetooth logo available as a character on iPhone?

Is there a font on iOS where there's a glyph for the Bluetooth logo? Some Dingbats, maybe, or Emoji? How about the WiFi logo?
EDIT: how about a third party font where there's such a character, the one that I could license and ship?
No, the Bluetooth logo is not a glyph or a font-face character.
Like Seva said, It's a combination of runic Hagall (ᚼ) and Bjarkan (ᛒ). I was trying to do the same thing by combining the two symbols.
I accomplished this by first finding a font that had these two characters. I ended up using LeedsUni. You can download it here: http://www.personal.leeds.ac.uk/~ecl6tam/.
You'll need to reference the path where the font is located in info.plist.
<key>UIAppFonts</key>
<array>
<string>Fonts/LeedsUni10-12-13.ttf</string>
</array>
I then created two UIView objects(UIButton objects in my case) which overlapped each other so that the two characters lined up properly. Depending on the font you use, you may need to adjust x and y values for the UIView frames.
My code is in C# because I'm using Xamarin, but you should be able to do the same thing in Objective C or Swift.
UIView bleView = new UIView(new CGRect(0, 0, 34.0f, 34.0f));
string fontName = "LeedsUni";
UIButton bluetoothToSerialButton = new UIButton(UIButtonType.RoundedRect);
bluetoothToSerialButton.Frame = new CGRect(0, 0, 34.0f, 34.0f);
bluetoothToSerialButton.SetTitleColor(UIApplication.SharedApplication.Delegate.GetWindow().TintColor, UIControlState.Normal);
bluetoothToSerialButton.SetTitle("ᛒ", UIControlState.Normal);
bluetoothToSerialButton.Font = UIFont.FromName(fontName, 34.0f);
UIButton bluetoothToSerialButton2 = new UIButton(UIButtonType.RoundedRect);
bluetoothToSerialButton2.Frame = new CGRect(-3.5f, 0, 34.0f, 34.0f);
bluetoothToSerialButton2.SetTitleColor(UIApplication.SharedApplication.Delegate.GetWindow().TintColor, UIControlState.Normal);
bluetoothToSerialButton2.SetTitle("ᚼ", UIControlState.Normal);
bluetoothToSerialButton2.Font = UIFont.FromName(fontName, 34.0f);
bleView.AddSubviews(new UIView[] { bluetoothToSerialButton, bluetoothToSerialButton2 });
Using Quartz2D in Swift 4.1
If you hate using external fonts or adding a bunch of .png files, you may prefer a simple class to get the same effect using Quartz2D.
This works for me with a logo of 20x20 points. You may want to optimize the geometry or line width. Also note that the frame's width should be equal the height.
Note that you will need to use setNeedsDisplay if the size changes.
import UIKit
class BluetoothLogo: UIView {
var color: UIColor!
convenience init(withColor color: UIColor, andFrame frame: CGRect) {
self.init(frame: frame)
self.backgroundColor = .clear
self.color = color
}
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
let h = self.frame.height
let y1 = h * 0.05
let y2 = h * 0.25
context?.move(to: CGPoint(x: y2, y: y2))
context?.addLine(to: CGPoint(x: h - y2, y: h - y2))
context?.addLine(to: CGPoint(x: h/2, y: h - y1))
context?.addLine(to: CGPoint(x: h/2, y: y1))
context?.addLine(to: CGPoint(x: h - y2, y: y2))
context?.addLine(to: CGPoint(x: y2, y: h - y2))
context?.setStrokeColor(color.cgColor)
context?.setLineCap(.round)
context?.setLineWidth(2)
context?.strokePath()
}
}
These are the icons from google material design you can download them from here https://material.io/tools/icons/?icon=bluetooth&style=baseline
No emoji, just checked on my iPad.
Just use a PDF or EPS of the bluetooth logo if you wan't it scalable, or just use a png otherwise.

Resources