How to use detectTransformGestures but not consuming all pointer event - android-jetpack-compose

I was making a fullscreen photo viewer which contain a pager (used HorizontalPager) and each page, user can zoom in/out and pan the image, but still able to swipe through pages.
My idea is swiping page will occurs when the image is not zoomed in (scale factor = 1), if it's zoomed in (scale factor > 1) then dragging/swiping will pan the image around.
Here is the code for the HorizontalPager that contain my customized zoomable Image:
#ExperimentalPagerApi
#Composable
fun ViewPagerSlider(pagerState: PagerState, urls: List<String>) {
var scale = remember {
mutableStateOf(1f)
}
var transX = remember {
mutableStateOf(0f)
}
var transY = remember {
mutableStateOf(0f)
}
HorizontalPager(
count = urls.size,
state = pagerState,
modifier = Modifier
.padding(0.dp, 40.dp, 0.dp, 40.dp),
) { page ->
Image(
painter = rememberImagePainter(
data = urls[page],
emptyPlaceholder = R.drawable.img_default_post,
),
contentScale = ContentScale.FillHeight,
contentDescription = null,
modifier = Modifier
.fillMaxSize()
.graphicsLayer(
translationX = transX.value,
translationY = transY.value,
scaleX = scale.value,
scaleY = scale.value,
)
.pointerInput(scale.value) {
detectTransformGestures { _, pan, zoom, _ ->
scale.value = when {
scale.value < 1f -> 1f
scale.value > 3f -> 3f
else -> scale.value * zoom
}
if (scale.value > 1f) {
transX.value = transX.value + (pan.x * scale.value)
transY.value = transY.value + (pan.y * scale.value)
} else {
transX.value = 0f
transY.value = 0f
}
}
}
)
}
}
So my image is zoomed in maximum 3f, and cannot zoom out smaller than 0.
I cannot swipe to change to another page if detectTransformGestures is in my code. If I put the detectTransformGestures based on the factor (scale = 1, make it swipeable to another page if not zoomed in), then it will be a "deadlock" as I cannot zoom in because there is no listener.
I don't know if there is some how to make it possible...
Thank you guys for your time!

I had to do something similar, and came up with this:
private fun ZoomableImage(
modifier: Modifier = Modifier,
bitmap: ImageBitmap,
maxScale: Float = 1f,
minScale: Float = 3f,
contentScale: ContentScale = ContentScale.Fit,
isRotation: Boolean = false,
isZoomable: Boolean = true,
lazyState: LazyListState
) {
val scale = remember { mutableStateOf(1f) }
val rotationState = remember { mutableStateOf(1f) }
val offsetX = remember { mutableStateOf(1f) }
val offsetY = remember { mutableStateOf(1f) }
val coroutineScope = rememberCoroutineScope()
Box(
modifier = Modifier
.clip(RectangleShape)
.background(Color.Transparent)
.combinedClickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = { /* NADA :) */ },
onDoubleClick = {
if (scale.value >= 2f) {
scale.value = 1f
offsetX.value = 1f
offsetY.value = 1f
} else scale.value = 3f
},
)
.pointerInput(Unit) {
if (isZoomable) {
forEachGesture {
awaitPointerEventScope {
awaitFirstDown()
do {
val event = awaitPointerEvent()
scale.value *= event.calculateZoom()
if (scale.value > 1) {
coroutineScope.launch {
lazyState.setScrolling(false)
}
val offset = event.calculatePan()
offsetX.value += offset.x
offsetY.value += offset.y
rotationState.value += event.calculateRotation()
coroutineScope.launch {
lazyState.setScrolling(true)
}
} else {
scale.value = 1f
offsetX.value = 1f
offsetY.value = 1f
}
} while (event.changes.any { it.pressed })
}
}
}
}
) {
Image(
bitmap = bitmap,
contentDescription = null,
contentScale = contentScale,
modifier = modifier
.align(Alignment.Center)
.graphicsLayer {
if (isZoomable) {
scaleX = maxOf(maxScale, minOf(minScale, scale.value))
scaleY = maxOf(maxScale, minOf(minScale, scale.value))
if (isRotation) {
rotationZ = rotationState.value
}
translationX = offsetX.value
translationY = offsetY.value
}
}
)
}
}
It is zoomable, rotatable (if you want it), supports pan if the image is zoomed in, has support for double-click zoom-in and zoom-out and also supports being used inside a scrollable element. I haven't come up with a solution to limit how far can the user pan the image yet.
It uses combinedClickable so the double-click zoom works without interfering with the other gestures, and pointerInput for the zoom, pan and rotation.
It uses this extension function to control the LazyListState, but if you need it for ScrollState it shouldn't be hard to modify it to suit your needs:
suspend fun LazyListState.setScrolling(value: Boolean) {
scroll(scrollPriority = MutatePriority.PreventUserInput) {
when (value) {
true -> Unit
else -> awaitCancellation()
}
}
}
Feel free to modify it for your needs.

Solution for HorizontalPager with better ux on swipe:
val pagerState = rememberPagerState()
val scrollEnabled = remember { mutableStateOf(true) }
HorizontalPager(
count = ,
state = pagerState,
userScrollEnabled = scrollEnabled.value,
) { }
#OptIn(ExperimentalFoundationApi::class)
#Composable
fun ZoomablePagerImage(
modifier: Modifier = Modifier,
painter: Painter,
scrollEnabled: MutableState<Boolean>,
minScale: Float = 1f,
maxScale: Float = 5f,
contentScale: ContentScale = ContentScale.Fit,
isRotation: Boolean = false,
) {
var targetScale by remember { mutableStateOf(1f) }
val scale = animateFloatAsState(targetValue = maxOf(minScale, minOf(maxScale, targetScale)))
var rotationState by remember { mutableStateOf(1f) }
var offsetX by remember { mutableStateOf(1f) }
var offsetY by remember { mutableStateOf(1f) }
val configuration = LocalConfiguration.current
val screenWidthPx = with(LocalDensity.current) { configuration.screenWidthDp.dp.toPx() }
Box(
modifier = Modifier
.clip(RectangleShape)
.background(Color.Transparent)
.combinedClickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = { },
onDoubleClick = {
if (targetScale >= 2f) {
targetScale = 1f
offsetX = 1f
offsetY = 1f
scrollEnabled.value = true
} else targetScale = 3f
},
)
.pointerInput(Unit) {
forEachGesture {
awaitPointerEventScope {
awaitFirstDown()
do {
val event = awaitPointerEvent()
val zoom = event.calculateZoom()
targetScale *= zoom
val offset = event.calculatePan()
if (targetScale <= 1) {
offsetX = 1f
offsetY = 1f
targetScale = 1f
scrollEnabled.value = true
} else {
offsetX += offset.x
offsetY += offset.y
if (zoom > 1) {
scrollEnabled.value = false
rotationState += event.calculateRotation()
}
val imageWidth = screenWidthPx * scale.value
val borderReached = imageWidth - screenWidthPx - 2 * abs(offsetX)
scrollEnabled.value = borderReached <= 0
if (borderReached < 0) {
offsetX = ((imageWidth - screenWidthPx) / 2f).withSign(offsetX)
if (offset.x != 0f) offsetY -= offset.y
}
}
} while (event.changes.any { it.pressed })
}
}
}
) {
Image(
painter = painter,
contentDescription = null,
contentScale = contentScale,
modifier = modifier
.align(Alignment.Center)
.graphicsLayer {
this.scaleX = scale.value
this.scaleY = scale.value
if (isRotation) {
rotationZ = rotationState
}
this.translationX = offsetX
this.translationY = offsetY
}
)
}
}

If you can create a mutable state variable that keeps track of the zoom factor, you can add the pointerInput modifier when the zoom factor is greater than one and leave it out when it is greater than one. Something like this:
var zoomFactorGreaterThanOne by remember { mutableStateOf(false) }
Image(
painter = rememberImagePainter(
data = urls[page],
emptyPlaceholder = R.drawable.img_default_post,
),
contentScale = ContentScale.FillHeight,
contentDescription = null,
modifier = Modifier
.fillMaxSize()
.graphicsLayer(
translationX = transX.value,
translationY = transY.value,
scaleX = scale.value,
scaleY = scale.value,
)
.run {
if (zoomFactorGreaterThanOne != 1.0f) {
this.pointerInput(scale.value) {
detectTransformGestures { _, pan, zoom, _ ->
zoomFactorGreaterThanOne = scale.value > 1
scale.value = when {
scale.value < 1f -> 1f
scale.value > 3f -> 3f
else -> scale.value * zoom
}
if (scale.value > 1f) {
transX.value = transX.value + (pan.x * scale.value)
transY.value = transY.value + (pan.y * scale.value)
} else {
transX.value = 0f
transY.value = 0f
}
}
}
} else {
this
}
}
)

Related

Jetpack Compose - TextField loses focus after typing

I have an AlertDialog with dynamic height. It has a TextField, on which some basic validation is performed, such as making sure the content is not empty. A warning message is displayed below it if validation fails. Once the user enters text in the text field, the warning message automatically disappears.
The problem is, when the user starts typing after the warning message is already displayed, the TextField loses focus for some reason. Does anyone know why, and how to prevent this from happening? Relevant code is below the gif. Currently using compose:1.2.0-alpha04
AlertDialog
AlertDialog(
properties = DialogProperties(usePlatformDefaultWidth = false),
modifier = Modifier.width(250.dp),
onDismissRequest = { showAddMatchDialog = false },
buttons = {
var player1Name by rememberSaveable { mutableStateOf("") }
var player1NameError by rememberSaveable { mutableStateOf(false) }
var player1Score by rememberSaveable { mutableStateOf("") }
var player1ScoreError by rememberSaveable { mutableStateOf(false) }
Column(
modifier = Modifier.padding(top = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(R.string.add_match_dialog_title),
fontSize = 20.sp,
fontWeight = FontWeight.SemiBold
)
Spacer(Modifier.height(4.dp))
PlayerRow(
nameLabel = stringResource(R.string.player1_name),
name = player1Name,
isNameError = player1NameError,
onNameClear = { player1Name = "" },
onNameChange = {
player1Name = it
player1NameError = false
},
score = player1Score,
isScoreError = player1ScoreError,
onScoreChange = {
player1Score = it
player1ScoreError = false
}
)
Spacer(Modifier.height(8.dp))
// Same thing for player 2
}
}
)
Player Row
#Composable
fun PlayerRow(
nameLabel: String,
name: String,
isNameError: Boolean,
onNameClear: () -> Unit,
onNameChange: (String) -> Unit,
score: String,
isScoreError: Boolean,
onScoreChange: (String) -> Unit
) {
Column {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
// Name TextField
Column {
Text(
text = nameLabel,
style = MaterialTheme.typography.subtitle2
)
Spacer(Modifier.height(4.dp))
Box {
BasicTextField(
modifier = Modifier
.width(TEXT_FIELD_WIDTH.dp)
.height(TEXT_FIELD_HEIGHT.dp)
.background(
color = GrayLight,
shape = roundedCornerShape
)
.then(
if (isNameError) {
Modifier.border(
width = 1.dp,
color = Warning,
shape = roundedCornerShape
)
} else {
Modifier
}
)
.padding(start = 8.dp, end = 8.dp, top = 6.dp),
value = name,
onValueChange = onNameChange,
singleLine = true,
)
Icon(
modifier = Modifier
.padding(start = (TEXT_FIELD_WIDTH - 25).dp, top = 3.dp)
.ripplelessClickable { onNameClear() },
imageVector = Icons.Default.Clear,
contentDescription = "",
tint = Gray
)
}
}
// Score TextField
Column {
Text(
text = stringResource(R.string.score),
style = MaterialTheme.typography.subtitle2
)
Spacer(Modifier.height(4.dp))
BasicTextField(
modifier = Modifier
.size(TEXT_FIELD_HEIGHT.dp)
.background(
color = GrayLight,
shape = roundedCornerShape
)
.then(
if (isScoreError) {
Modifier.border(
width = 1.dp,
color = Warning,
shape = roundedCornerShape
)
} else {
Modifier
}
)
.padding(start = 6.dp, end = 6.dp, top = 6.dp),
value = score,
onValueChange = onScoreChange,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number
),
singleLine = true,
)
}
}
if (isNameError) {
Text(
modifier = Modifier.padding(start = 20.dp),
text = stringResource(R.string.enter_player_name),
color = Warning,
fontSize = 14.sp
)
}
if (isScoreError) {
Text(
modifier = Modifier.padding(start = 20.dp),
text = stringResource(R.string.enter_player_score),
color = Warning,
fontSize = 14.sp
)
}
}
}
I had the exact same issue. The problem is this:
else {
Modifier
}
You cannot assign "Modifier" by itself when using .then()

Large section of iPad OpenGL ES application not retina

I have an OpengGL ES app using a custom shader to display circles. See image below. (Opening it in a new window might be helpful)
If you look carefully you can see that the display seems to be non retina from about 50% of the width and about 75% of the height. This seems to be the case only on iPad 3 (clients device). Simulator and an other iPad Air 2 behave normally.
I used a the basic OpenGL ES game project bundled with XCode.
Update:
The pixelated areas are the ones highlighted in red:
Please also see closeup:
I must admit I do not know where to start debugging this,
since it only seems to bug on the given device.
Here is the code I used to setup the context.
func setup()
{
initTextures()
self.context = EAGLContext(api: .openGLES2)
if !(self.context != nil) {
print("Failed to create ES context")
}
let view = self.view as! GLKView
// Fix for "good" aspect ratio
var frameSize = view.frame.size
frameSize.height = frameSize.width / 1.43023255813953
view.frame.size = frameSize
// Should force the aspect ratio
print("-------------")
print("width \(view.frame.width) and height \(view.frame.height)")
print("aspect ratio w/h \(view.frame.width / view.frame.height)")
print("-------------")
view.context = self.context!
view.drawableColorFormat = .RGBA8888
view.drawableMultisample = .multisample4X
// Application specific code
self.setupGL()
}
Update
I am drawing the circles with a custom fragment shader:
precision highp float;
uniform vec4 iResolution; // z - texWidth, w - texHeight
uniform sampler2D textureUnit;
uniform sampler2D smallPointsTextureUnit;
uniform vec2 gridSize;
#define SMOOTH(r,R) (1.0-smoothstep(R-0.09,R+0.09, r))
#define black vec3(0.0)
#define white vec3(1.0)
float circle(vec2 st, in float _radius, float pct ){
float l = length(st - vec2(0.5));
return 1.-smoothstep(_radius-(_radius*0.005) * pct,
_radius+(_radius*0.005),
l);
}
float stroke(vec2 uv, vec2 center, float radius, float width)
{
float dist = length(uv-center);
float t = 1.0 + smoothstep(radius, radius+width, dist)
- smoothstep(radius-width, radius, dist);
return t;
}
void main()
{
vec2 resolution = vec2(iResolution.x, iResolution.y);
vec2 uv = gl_FragCoord.xy;
vec2 st = gl_FragCoord.xy/resolution;
float colWidth = iResolution.x / gridSize.x;
float rowHeight = (iResolution.y + 1.0) / gridSize.y;
float smallerSize = min(rowHeight, colWidth);
float largerSize = max(rowHeight, colWidth);
vec2 divider = resolution / smallerSize;
st.x *= divider.x;
st.y *= divider.y;
float pct = largerSize / smallerSize;
float texXPos = (floor(st.x * smallerSize / largerSize) + 0.5) / iResolution.z;
float texYPos = (floor(gridSize.y -st.y) + 0.5) / iResolution.w;
vec4 tex = texture2D(textureUnit, vec2(
texXPos,
texYPos));
vec4 texSmallPoints = texture2D(smallPointsTextureUnit, vec2((floor(st.x * 2.0 * smallerSize / largerSize) + 0.5) / 128.0,
(floor(gridSize.y * 2.0 -st.y * 2.0) + 0.5) / 128.0));
//texSmallPoints.r = 0.5;
vec3 fillColor = vec3(tex.x, tex.y, tex.z);
st.x = mod(st.x, pct);
st.x = step( fract(st.x * 1.0 / pct), 1.0 / pct) * fract(st.x);
st.x *= texSmallPoints.r * 2.0; // subdivide for small circles
st.x = fract(st.x);
// Divide by 4
st.y *= texSmallPoints.r * 2.0;
st.y = fract(st.y);
//float r = 0.425;
float r = 0.4;
float fillPct = circle(st, r, 1.0);
vec2 center = vec2(0.5);
float strokePct = stroke(st, center, r, 0.032 * texSmallPoints.r * 1.8);
vec3 finalColor = vec3(1.0);
vec3 strokeColor = fillColor;
// todo -refactor if slow
// todo - invert
if (tex.a > 0.99) {
strokeColor = black;
}
if (tex.a < 0.01) {
strokeColor = white;
}
finalColor = mix(white, fillColor, fillPct);
finalColor = mix(finalColor, strokeColor, 1. - strokePct);
gl_FragColor = vec4(finalColor, 1.0);
}
And GLKViewController:
//
// HomeOpenGLController.swift
// Kobi
//
// Created by Tibor Udvari on 14/06/16.
// Copyright © 2016 Tibor Udvari. All rights reserved.
//
import GLKit
import OpenGLES
import HEXColor
open class KobiOpenGLControllerBase: GLKViewController
{
// --- Small points texture ---
var gpuSmallColorsTexture = [GLubyte](repeating: 0, count: 0)
var currentSmallPointTextureData: [GLubyte]? = nil
// - allocated size
let smallPointsTextureWidth = 128
let smallPointsTextureHeight = 128
// --- Color texture ---
var gpuColorsTexture = [GLubyte](repeating: 0, count: 0)
var currentColorsTextureData: [GLubyte]? = nil // size of grid
// - allocated size
let texWidth: Int = 256
let texHeight: Int = 256
// Grid - circles
let cols = 31
let rows = 22
open let maxIdx: Int
open let circleCount: Int
// Grid - pixels
var width: CGFloat = 0.0
var height: CGFloat = 0.0
var circleWidth: CGFloat = 0.0
var circleHeight: CGFloat = 0.0
// OpenGL
var program: GLuint = 0
var circleProgram: GLuint = 0
var context: EAGLContext? = nil
required public init?(coder aDecoder: NSCoder) {
maxIdx = cols * rows
circleCount = cols * rows
super.init(coder: aDecoder)
}
// 0 is positive instead of 0
func sign(_ x: Int) -> Int {
let r = x < 0 ? -1 : 1
return r
}
// MARK: - Setup
override open func viewDidLoad() {
setupGridData()
}
override open func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
setup() // because width and height is not initiated yet
}
func setupGridData(){
//currentSmallPointTextureData = createCurrentSmallPointsTextureData()
}
func setup()
{
initTextures()
self.context = EAGLContext(api: .openGLES2)
if !(self.context != nil) {
print("Failed to create ES context")
}
let view = self.view as! GLKView
// Fix for "good" aspect ratio
var frameSize = view.frame.size
frameSize.height = frameSize.width / 1.43023255813953
view.frame.size = frameSize
// Should force the aspect ratio
print("-------------")
print("width \(view.frame.width) and height \(view.frame.height)")
print("aspect ratio w/h \(view.frame.width / view.frame.height)")
print("-------------")
view.context = self.context!
view.drawableColorFormat = .RGBA8888
view.drawableMultisample = .multisample4X
//view.drawableMultisample = .MultisampleNone
//view.multipleTouchEnabled = true
width = self.view.frame.size.width * self.view.contentScaleFactor
height = self.view.frame.size.height * self.view.contentScaleFactor
circleWidth = width / CGFloat(cols)
circleHeight = height / CGFloat(rows)
self.setupGL()
}
func initTextures()
{
gpuColorsTexture = [GLubyte](repeating: 0, count: Int(texWidth)*Int(texHeight)*4)
gpuSmallColorsTexture = [GLubyte](repeating: 128, count: Int(smallPointsTextureWidth)*Int(smallPointsTextureHeight))
}
// MARK: - GLKView and GLKViewController delegate methods
func sendTexturesToGPU() {
for i in 0..<currentColorsTextureData!.count / 4 {
let r = Int(i) / Int(cols)
let c = Int(i) % cols
let j = r * texWidth + c
gpuColorsTexture[j*4] = currentColorsTextureData![i * 4]; //= GLubyte(255); // red
gpuColorsTexture[j*4+1] = currentColorsTextureData![i * 4 + 1]; //GLubyte(random() % 255); // green
gpuColorsTexture[j*4+2] = currentColorsTextureData![i * 4 + 2]; //GLubyte(0); // blue
gpuColorsTexture[j*4+3] = currentColorsTextureData![i * 4 + 3]; // used for the stroke color
}
for i in 0..<currentSmallPointTextureData!.count{
let r = Int(i) / Int(31 * 2)
let c = Int(i) % (31 * 2)
let j = r * 128 + c
gpuSmallColorsTexture[j] = currentSmallPointTextureData![i];
}
glActiveTexture(GLenum(GL_TEXTURE1));
glTexImage2D(GLenum(GL_TEXTURE_2D), GLint(0), GL_LUMINANCE, GLsizei(smallPointsTextureWidth), GLsizei(smallPointsTextureHeight), GLint(0), GLenum(GL_LUMINANCE), GLenum(GL_UNSIGNED_BYTE), &gpuSmallColorsTexture)
glActiveTexture(GLenum(GL_TEXTURE0));
glTexImage2D(GLenum(GL_TEXTURE_2D), GLint(0), GL_RGBA, GLsizei(texWidth), GLsizei(texHeight), GLint(0), GLenum(GL_RGBA), GLenum(GL_UNSIGNED_BYTE), &gpuColorsTexture);
}
func update() {
print("update")
//todo
}
// todo send a uniform array
override open func glkView(_ view: GLKView, drawIn rect: CGRect) {
glClearColor(1.0, 1.0, 0.0, 1.0)
glClear(GLbitfield(GL_COLOR_BUFFER_BIT))
glEnable(GLenum(GL_DEPTH_TEST))
glEnable(GLenum(GL_POINT_SIZE));
glEnable(GLenum(GL_BLEND))
glBlendFunc(GLenum(GL_SRC_ALPHA), GLenum(GL_ONE_MINUS_SRC_ALPHA))
glEnable(GLenum(GL_POINT_SMOOTH))
// 22 x 15
var baseModelViewMatrix = GLKMatrix4MakeTranslation(0.0, 0.0, 0.0)
baseModelViewMatrix = GLKMatrix4Rotate(baseModelViewMatrix, 0.0, 0.0, 1.0, 0.0)
var modelViewMatrix = GLKMatrix4MakeTranslation(0.0, 0.0, 1.5)
modelViewMatrix = GLKMatrix4Rotate(modelViewMatrix, 0.0, 1.0, 1.0, 1.0)
modelViewMatrix = GLKMatrix4Multiply(baseModelViewMatrix, modelViewMatrix)
modelViewMatrix = GLKMatrix4Identity
glUseProgram(program)
/*
withUnsafePointer(to: &modelViewProjectionMatrix, {
$0.withMemoryRebound(to: Float.self, capacity: 16, {
glUniformMatrix4fv(uniforms[UNIFORM_MODELVIEWPROJECTION_MATRIX], 1, 0, $0)
})
})*/
withUnsafePointer(to: &modelViewMatrix, {
$0.withMemoryRebound(to: Float.self, capacity: 16, {
glUniformMatrix4fv(glGetUniformLocation(program, "modelViewProjectionMatrix"), 1, 0, UnsafePointer($0))
})
})
glVertexAttribPointer(0, 2, GLenum(GL_FLOAT), GLboolean(GL_FALSE), 0, squareVertices)
glUniform4f(glGetUniformLocation(program, "iResolution"), Float(width), Float(height), Float(texWidth), Float(texHeight))
glUniform2f(glGetUniformLocation(program, "gridSize"), Float(cols), Float(rows))
glDrawArrays(GLenum(GL_TRIANGLE_STRIP) , 0, 4)
glUseProgram(circleProgram)
}
// MARK: - Texture
func setupTextures()
{
let texInfo = try! GLKTextureLoader.texture(with: UIImage(named: "texture256")!.cgImage!, options: nil)
glActiveTexture(GLenum(GL_TEXTURE0))
glBindTexture(GLenum(GL_TEXTURE0), (texInfo.name))
//var dataTexture = (texInfo.name)
glUniform1i(glGetUniformLocation(program, "textureUnit"), 0)
glActiveTexture(GLenum(GL_TEXTURE1))
let _ = createSmallPointsTexture()
glUniform1i(glGetUniformLocation(program, "smallPointsTextureUnit"), 1)
}
func createSmallPointsTexture() -> GLuint {
var texture: GLuint = 1
glGenTextures(GLsizei(1), &texture)
glBindTexture(GLenum(GL_TEXTURE_2D), texture)
glActiveTexture(texture)
glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MIN_FILTER), GL_LINEAR);
glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MAG_FILTER), GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_S), GL_CLAMP_TO_EDGE);
glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_T), GL_CLAMP_TO_EDGE);
glGenerateMipmap(GLenum(GL_TEXTURE_2D));
return texture
}
// MARK: - OpenGL ES 2 shader compilation
func setupGL() {
EAGLContext.setCurrent(self.context)
let _ = self.loadShaders()
glUseProgram(program)
glEnableVertexAttribArray(0)
self.setupTextures()
}
func tearDownGL() {
EAGLContext.setCurrent(self.context)
if program != 0 {
glDeleteProgram(program)
program = 0
}
}
func loadShaders() -> Bool {
var vertShader: GLuint = 0
var fragShader: GLuint = 0
var vertShaderPathname: String
var fragShaderPathname: String
// Create shader program.
program = glCreateProgram()
// Create and compile vertex shader.
vertShaderPathname = Bundle.main.path(forResource: "Shader", ofType: "vsh")!
if self.compileShader(&vertShader, type: GLenum(GL_VERTEX_SHADER), file: vertShaderPathname) == false {
print("Failed to compile vertex shader")
return false
}
// Create and compile fragment shader.
fragShaderPathname = Bundle.main.path(forResource: "Shader", ofType: "fsh")!
if !self.compileShader(&fragShader, type: GLenum(GL_FRAGMENT_SHADER), file: fragShaderPathname) {
print("Failed to compile fragment shader")
/*
var fragInfoLength: GLint = 0
glGetShaderiv(fragShader, GLenum(GL_INFO_LOG_LENGTH), &fragInfoLength)
//let cstring = UnsafeMutablePointer<GLchar>(allocatingCapacity: Int(fragInfoLength))
var cstring = UnsafeMutablePointer<GLchar>(malloc(Int(fragInfoLength)))
glGetShaderInfoLog(fragShader, fragInfoLength, nil, cstring)
let shaderInfoLog = NSString(utf8String: cstring)
print(shaderInfoLog)
*/
return false
}
// Attach vertex shader to program.
glAttachShader(program, vertShader)
// Attach fragment shader to program.
glAttachShader(program, fragShader)
// Bind attribute locations.
// This needs to be done prior to linking.
glBindAttribLocation(program, 0, "position")
// Link program.
if !self.linkProgram(program) {
print("Failed to link program: \(program)")
if vertShader != 0 {
glDeleteShader(vertShader)
vertShader = 0
}
if fragShader != 0 {
glDeleteShader(fragShader)
fragShader = 0
}
if program != 0 {
glDeleteProgram(program)
program = 0
}
return false
}
// Release vertex and fragment shaders.
if vertShader != 0 {
glDetachShader(program, vertShader)
glDeleteShader(vertShader)
}
if fragShader != 0 {
glDetachShader(program, fragShader)
glDeleteShader(fragShader)
}
return true
}
func compileShader(_ shader: inout GLuint, type: GLenum, file: String) -> Bool {
var status: GLint = 0
var source: UnsafePointer<Int8>
do {
source = try NSString(contentsOfFile: file, encoding: String.Encoding.utf8.rawValue).utf8String!
} catch {
print("Failed to load vertex shader")
return false
}
//var castSource = UnsafePointer<GLchar>(source)
var castSource: UnsafePointer<GLchar>? = UnsafePointer<GLchar>(source)
shader = glCreateShader(type)
glShaderSource(shader, 1, &castSource, nil)
glCompileShader(shader)
var logLength: GLint = 0
glGetShaderiv(shader, GLenum(GL_INFO_LOG_LENGTH), &logLength)
if logLength > 0 {
//var log = UnsafeMutablePointer<GLchar>(malloc(Int(logLength)))
print("Log length gt 0")
/*
var log = UnsafeMutablePointer<GLchar>(malloc(Int(logLength)))
glGetShaderInfoLog(shader, logLength, &logLength, log)
NSLog("Shader compile log: \n%s", log)
free(log)
*/
}
glGetShaderiv(shader, GLenum(GL_COMPILE_STATUS), &status)
if status == 0 {
glDeleteShader(shader)
return false
}
return true
}
func linkProgram(_ prog: GLuint) -> Bool {
var status: GLint = 0
glLinkProgram(prog)
//#if defined(DEBUG)
// var logLength: GLint = 0
// glGetShaderiv(shader, GLenum(GL_INFO_LOG_LENGTH), &logLength)
// if logLength > 0 {
// var log = UnsafeMutablePointer<GLchar>(malloc(Int(logLength)))
// glGetShaderInfoLog(shader, logLength, &logLength, log)
// NSLog("Shader compile log: \n%s", log)
// free(log)
// }
//#endif
glGetProgramiv(prog, GLenum(GL_LINK_STATUS), &status)
if status == 0 {
return false
}
return true
}
func validateProgram(_ prog: GLuint) -> Bool {
var logLength: GLsizei = 0
var status: GLint = 0
glValidateProgram(prog)
glGetProgramiv(prog, GLenum(GL_INFO_LOG_LENGTH), &logLength)
if logLength > 0 {
var log: [GLchar] = [GLchar](repeating: 0, count: Int(logLength))
glGetProgramInfoLog(prog, logLength, &logLength, &log)
print("Program validate log: \n\(log)")
}
glGetProgramiv(prog, GLenum(GL_VALIDATE_STATUS), &status)
var returnVal = true
if status == 0 {
returnVal = false
}
return returnVal
}
// MARK : Cleanup
override open func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
if self.isViewLoaded && (self.view.window != nil) {
self.view = nil
self.tearDownGL()
if EAGLContext.current() === self.context {
EAGLContext.setCurrent(nil)
}
self.context = nil
}
}
deinit {
self.tearDownGL()
if EAGLContext.current() === self.context {
EAGLContext.setCurrent(nil)
}
}
}
var squareVertices: [GLfloat] = [
-1.0, -1.0,
1.0, -1.0,
-1.0, 1.0,
1.0, 1.0,
];
Thank you for updating. I am not sure exactly which part caused it but I am sure it must happen in your fragment shader code.
I guess vec2 st's value changing is not steady there while it is being calculated.
I want you to test this to see if it is your fragment shader.
just draw only 1 circle without uniform values except iResolution.
only using iResolution and gl_FragCoord, draw a circle.
I think it's going to properly show up. Then go over your FS.

iOS Swift Flood fill algorithm

I created this extension for "bucket fill" (flood fill) of touch point:
extension UIImageView {
func bucketFill(startPoint: CGPoint, newColor: UIColor) {
var newRed, newGreen, newBlue, newAlpha: CUnsignedChar
let pixelsWide = CGImageGetWidth(self.image!.CGImage)
let pixelsHigh = CGImageGetHeight(self.image!.CGImage)
let rect = CGRect(x:0, y:0, width:Int(pixelsWide), height:Int(pixelsHigh))
let bitmapBytesPerRow = Int(pixelsWide) * 4
var context = self.image!.createARGBBitmapContext()
//Clear the context
CGContextClearRect(context, rect)
// Draw the image to the bitmap context. Once we draw, the memory
// allocated for the context for rendering will then contain the
// raw image data in the specified color space.
CGContextDrawImage(context, rect, self.image!.CGImage)
var data = CGBitmapContextGetData(context)
var dataType = UnsafeMutablePointer<UInt8>(data)
let newColorRef = CGColorGetComponents(newColor.CGColor)
if(CGColorGetNumberOfComponents(newColor.CGColor) == 2) {
newRed = CUnsignedChar(newColorRef[0] * 255) // CUnsignedChar
newGreen = CUnsignedChar(newColorRef[0] * 255)
newBlue = CUnsignedChar(newColorRef[0] * 255)
newAlpha = CUnsignedChar(newColorRef[1])
} else {
newRed = CUnsignedChar(newColorRef[0] * 255)
newGreen = CUnsignedChar(newColorRef[1] * 255)
newBlue = CUnsignedChar(newColorRef[2] * 255)
newAlpha = CUnsignedChar(newColorRef[3])
}
let newColorStr = ColorRGB(red: newRed, green: newGreen, blue: newBlue)
var stack = Stack()
let offset = 4*((Int(pixelsWide) * Int(startPoint.y)) + Int(startPoint.x))
//let alpha = dataType[offset]
let startRed: UInt8 = dataType[offset+1]
let startGreen: UInt8 = dataType[offset+2]
let startBlue: UInt8 = dataType[offset+3]
stack.push(startPoint)
while(!stack.isEmpty()) {
let point: CGPoint = stack.pop() as! CGPoint
let offset = 4*((Int(pixelsWide) * Int(point.y)) + Int(point.x))
let alpha = dataType[offset]
let red: UInt8 = dataType[offset+1]
let green: UInt8 = dataType[offset+2]
let blue: UInt8 = dataType[offset+3]
if (red == newRed && green == newGreen && blue == newBlue) {
continue
}
if (red.absoluteDifference(startRed) < 4 && green.absoluteDifference(startGreen) < 4 && blue.absoluteDifference(startBlue) < 4) {
dataType[offset] = 255
dataType[offset + 1] = newRed
dataType[offset + 2] = newGreen
dataType[offset + 3] = newBlue
if (point.x > 0) {
stack.push(CGPoint(x: point.x - 1, y: point.y))
}
if (point.x < CGFloat(pixelsWide)) {
stack.push(CGPoint(x: point.x + 1, y: point.y))
}
if (point.y > 0) {
stack.push(CGPoint(x: point.x, y: point.y - 1))
}
if (point.y < CGFloat(pixelsHigh)) {
stack.push(CGPoint(x: point.x, y: point.y + 1))
}
} else {
}
}
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.PremultipliedFirst.rawValue)
let finalContext = CGBitmapContextCreate(data, pixelsWide, pixelsHigh, CLong(8), CLong(bitmapBytesPerRow), colorSpace, bitmapInfo)
let imageRef = CGBitmapContextCreateImage(finalContext)
self.image = UIImage(CGImage: imageRef, scale: self.image!.scale,orientation: self.image!.imageOrientation)
}
}
Now I would like to improve performance. How can I make this algorithm work faster? UInt8.absoluteDifference extension is my attempt to include almost same colors to flood fill and it's working but this could be really improve and I know it but I don't know how.
extension UInt8 {
func absoluteDifference(subtrahend: UInt8) -> UInt8 {
if (self > subtrahend) {
return self - subtrahend;
} else {
return subtrahend - self;
}
}
}
My Stack class:
class Stack {
var count: Int = 0
var head: Node = Node()
init() {
}
func isEmpty() -> Bool {
return self.count == 0
}
func push(value: Any) {
if isEmpty() {
self.head = Node()
}
var node = Node(value: value)
node.next = self.head
self.head = node
self.count++
}
func pop() -> Any? {
if isEmpty() {
return nil
}
var node = self.head
self.head = node.next!
self.count--
return node.value
}
}
Thanks for help

Logarithmic y-axis in swift?

I'm working on a financial application that has some quite large numbers.
One of my charts for example is growing from $25,000 to about $20,000,000,000 (exponentially)
Because of this, using any regular chart without log-scale causes the majority of my data to simply "flatline", while it's in fact growing quickly.
Here's an example
(Ignore that it's not set up yet)
The above linechart was made using SwiftChart
Now I have to be honest, I am not even at this point entirely sure if I'm even allowed to ask a question like this on SO. But I just have no idea where to begin.
After a couple of hours of searching I have not been able to find any libraries, tutorials, sample apps or anything IOS related that features a graph with a logarithmic axis (neither X or Y).
After I couldn't find anything, I realised I'd have to implement this feature on my own. I found the most suitable library for my application, and I've been trying to figure out how I can implement a way to allow the y-scale to be scaled with a log-scale (with the power of 10).
I've done a fair bit of research on how log-scales work, and used them plenty in my university. But I have never actually seen an example of one programmed.
I found one written in JavaScript (I think). But unfortunately I have no experience with web-development and wasn't able to properly translate it :(
I really don't feel comfortable putting someone elses code on SO, but I really have no clue where to begin.
If this isn't allowed, please post a comment, and I'll get rid of it immediately
All credits of this goes to this guy: https://github.com/gpbl/SwiftChart
Here's the code that I believe is where I need to implement a log-scale.
private func drawChart() {
drawingHeight = bounds.height - bottomInset - topInset
drawingWidth = bounds.width
let minMax = getMinMax()
min = minMax.min
max = minMax.max
highlightShapeLayer = nil
// Remove things before drawing, e.g. when changing orientation
for view in self.subviews {
view.removeFromSuperview()
}
for layer in layerStore {
layer.removeFromSuperlayer()
}
layerStore.removeAll()
// Draw content
for (index, series) in enumerate(self.series) {
// Separate each line in multiple segments over and below the x axis
var segments = Chart.segmentLine(series.data as ChartLineSegment)
for (i, segment) in enumerate(segments) {
let scaledXValues = scaleValuesOnXAxis( segment.map( { return $0.x } ) )
let scaledYValues = scaleValuesOnYAxis( segment.map( { return $0.y } ) )
if series.line {
drawLine(xValues: scaledXValues, yValues: scaledYValues, seriesIndex: index)
}
if series.area {
drawArea(xValues: scaledXValues, yValues: scaledYValues, seriesIndex: index)
}
}
}
drawAxes()
if xLabels != nil || series.count > 0 {
drawLabelsAndGridOnXAxis()
}
if yLabels != nil || series.count > 0 {
drawLabelsAndGridOnYAxis()
}
}
// MARK: - Scaling
private func getMinMax() -> (min: ChartPoint, max: ChartPoint) {
// Start with user-provided values
var min = (x: minX, y: minY)
var max = (x: maxX, y: maxY)
// Check in datasets
for series in self.series {
let xValues = series.data.map( { (point: ChartPoint) -> Float in
return point.x } )
let yValues = series.data.map( { (point: ChartPoint) -> Float in
return point.y } )
let newMinX = minElement(xValues)
let newMinY = minElement(yValues)
let newMaxX = maxElement(xValues)
let newMaxY = maxElement(yValues)
if min.x == nil || newMinX < min.x! { min.x = newMinX }
if min.y == nil || newMinY < min.y! { min.y = newMinY }
if max.x == nil || newMaxX > max.x! { max.x = newMaxX }
if max.y == nil || newMaxY > max.y! { max.y = newMaxY }
}
// Check in labels
if xLabels != nil {
let newMinX = minElement(xLabels!)
let newMaxX = maxElement(xLabels!)
if min.x == nil || newMinX < min.x { min.x = newMinX }
if max.x == nil || newMaxX > max.x { max.x = newMaxX }
}
if yLabels != nil {
let newMinY = minElement(yLabels!)
let newMaxY = maxElement(yLabels!)
if min.y == nil || newMinY < min.y { min.y = newMinY }
if max.y == nil || newMaxY > max.y { max.y = newMaxY }
}
if min.x == nil { min.x = 0 }
if min.y == nil { min.y = 0 }
if max.x == nil { max.x = 0 }
if max.y == nil { max.y = 0 }
return (min: (x: min.x!, y: min.y!), max: (x: max.x!, max.y!))
}
private func scaleValuesOnXAxis(values: Array<Float>) -> Array<Float> {
let width = Float(drawingWidth)
var factor: Float
if max.x - min.x == 0 { factor = 0 }
else { factor = width / (max.x - min.x) }
let scaled = values.map { factor * ($0 - self.min.x) }
return scaled
}
private func scaleValuesOnYAxis(values: Array<Float>) -> Array<Float> {
let height = Float(drawingHeight)
var factor: Float
if max.y - min.y == 0 { factor = 0 }
else { factor = height / (max.y - min.y) }
let scaled = values.map { Float(self.topInset) + height - factor * ($0 - self.min.y) }
return scaled
}
private func scaleValueOnYAxis(value: Float) -> Float {
let height = Float(drawingHeight)
var factor: Float
if max.y - min.y == 0 { factor = 0 }
else { factor = height / (max.y - min.y) }
let scaled = Float(self.topInset) + height - factor * (value - min.y)
return scaled
}
private func getZeroValueOnYAxis() -> Float {
if min.y > 0 {
return scaleValueOnYAxis(min.y)
}
else {
return scaleValueOnYAxis(0)
}
}
// MARK: - Drawings
private func isVerticalSegmentAboveXAxis(yValues: Array<Float>) -> Bool {
// YValues are "reverted" from top to bottom, so min is actually the maxz
let min = maxElement(yValues)
let zero = getZeroValueOnYAxis()
return min <= zero
}
private func drawLine(#xValues: Array<Float>, yValues: Array<Float>, seriesIndex: Int) -> CAShapeLayer {
let isAboveXAxis = isVerticalSegmentAboveXAxis(yValues)
let path = CGPathCreateMutable()
CGPathMoveToPoint(path, nil, CGFloat(xValues.first!), CGFloat(yValues.first!))
for i in 1..<yValues.count {
let y = yValues[i]
CGPathAddLineToPoint(path, nil, CGFloat(xValues[i]), CGFloat(y))
}
var lineLayer = CAShapeLayer()
lineLayer.frame = self.bounds
lineLayer.path = path
if isAboveXAxis {
lineLayer.strokeColor = series[seriesIndex].colors.above.CGColor
}
else {
lineLayer.strokeColor = series[seriesIndex].colors.below.CGColor
}
lineLayer.fillColor = nil
lineLayer.lineWidth = lineWidth
lineLayer.lineJoin = kCALineJoinBevel
self.layer.addSublayer(lineLayer)
layerStore.append(lineLayer)
return lineLayer
}
private func drawArea(#xValues: Array<Float>, yValues: Array<Float>, seriesIndex: Int) {
let isAboveXAxis = isVerticalSegmentAboveXAxis(yValues)
let area = CGPathCreateMutable()
let zero = CGFloat(getZeroValueOnYAxis())
CGPathMoveToPoint(area, nil, CGFloat(xValues[0]), zero)
for i in 0..<xValues.count {
CGPathAddLineToPoint(area, nil, CGFloat(xValues[i]), CGFloat(yValues[i]))
}
CGPathAddLineToPoint(area, nil, CGFloat(xValues.last!), zero)
var areaLayer = CAShapeLayer()
areaLayer.frame = self.bounds
areaLayer.path = area
areaLayer.strokeColor = nil
if isAboveXAxis {
areaLayer.fillColor = series[seriesIndex].colors.above.colorWithAlphaComponent(areaAlphaComponent).CGColor
}
else {
areaLayer.fillColor = series[seriesIndex].colors.below.colorWithAlphaComponent(areaAlphaComponent).CGColor
}
areaLayer.lineWidth = 0
self.layer.addSublayer(areaLayer)
layerStore.append(areaLayer)
}
private func drawAxes() {
let context = UIGraphicsGetCurrentContext()
CGContextSetStrokeColorWithColor(context, axesColor.CGColor)
CGContextSetLineWidth(context, 0.5)
// horizontal axis at the bottom
CGContextMoveToPoint(context, 0, drawingHeight + topInset)
CGContextAddLineToPoint(context, drawingWidth, drawingHeight + topInset)
CGContextStrokePath(context)
// horizontal axis at the top
CGContextMoveToPoint(context, 0, 0)
CGContextAddLineToPoint(context, drawingWidth, 0)
CGContextStrokePath(context)
// horizontal axis when y = 0
if min.y < 0 && max.y > 0 {
let y = CGFloat(getZeroValueOnYAxis())
CGContextMoveToPoint(context, 0, y)
CGContextAddLineToPoint(context, drawingWidth, y)
CGContextStrokePath(context)
}
// vertical axis on the left
CGContextMoveToPoint(context, 0, 0)
CGContextAddLineToPoint(context, 0, drawingHeight + topInset)
CGContextStrokePath(context)
// vertical axis on the right
CGContextMoveToPoint(context, drawingWidth, 0)
CGContextAddLineToPoint(context, drawingWidth, drawingHeight + topInset)
CGContextStrokePath(context)
}
Any hint or help as to where I need to implement this would be massively appreciated.
And I apologize for the awfulness that is this question begging for help or a direction.

How to get all Points Along a Line in Swift?

I am writing a program in Swift which requires a method that uses specification of two CGPoints and returns all points in a straight line in-between them. At the moment I am trialling the following method, but it is very glitchy and refuses to get all points for some lines. I was just wondering if there is a more efficient way of doing this?
func addPointsInLine(#start: CGPoint, end: CGPoint){
var speed = 0
var startNo = trackPoints.count
var endNo = startNo
var xDiff = start.x - end.x
var yDiff = start.y - end.y
var xChange = 0.0
var yChange = 0.0
var newPointX = start.x
var newPointY = start.y
var ended = false
xChange = Double(xDiff) / Double(yDiff)
yChange = Double(yDiff) / Double(xDiff)
/*if(xDiff > 0){
xChange = sqrt(xChange * xChange)
}
if(yDiff > 0){
yChange = sqrt(yChange * yChange)
}*/
println("xc \(xChange)")
println("yc \(yChange)")
var y = Double(start.y)
var x = Double(start.x)
while !ended {
println(trackPoints.count)
speed++
endNo++
if(CGPointMake(newPointX, newPointY) == end){
ended = true
}
if(yChange > xChange){
y++
x += xChange
trackPoints.append(TrackPoint(x: Int(x), y: Int(y)))
if(CGFloat(Int(y)) == end.y){
ended = true
println("end")
//break
}
/* if(yChange > 0){
if(CGFloat(Int(y)) > end.y){
ended = true
println("end>y")
// break
}
}
if(yChange < 0){
if(CGFloat(Int(y)) < end.y){
ended = true
println("end<y")
//break
}
}*/
if(xChange > 0){
if(CGFloat(Int(x)) >= end.x){
ended = true
println("end>x")
//break
}
}
if(xChange < 0){
if(CGFloat(Int(x)) <= end.x){
ended = true
println("end<x")
//break
}
}
} else {
x++
y += yChange
trackPoints.append(TrackPoint(x: Int(x), y: Int(y)))
if(CGFloat(Int(x)) == end.x){
ended = true
println("end")
//break
}
/* if(xChange > 0){
if(CGFloat(Int(x)) >= end.x){
ended = true
println("end>x")
//break
}
}
if(xChange < 0){
if(CGFloat(Int(x)) <= end.x){
ended = true
println("end<x")
//break
}
}*/
if(yChange > 0){
if(CGFloat(Int(y)) > end.y){
ended = true
println("end>y")
// break
}
}
if(yChange < 0){
if(CGFloat(Int(y)) < end.y){
ended = true
println("end<y")
//break
}
}
}
}
println("finished")
var i = startNo
println(startNo)
println(endNo)
while i < endNo {
trackPoints[i].speed = speed
i++
}
println("finish2")
}
I think firstly finding all points isn't possible because there are infinite points in a straight line. Take example of line joining (0,0) to (1, 0). All the following points and many more are on the said line (0.00001,0) (0.0000000000001,0) (0.01,0)
So you need to limit the amount of points you need to find like all the points with Integer co-ordinates. All the points 1 unit apart, starting from starting point etc.
Next you can use one of equations of line to get points: Equations of Line
Try this,
func findAllPointsBetweenTwoPoints(startPoint : CGPoint, endPoint : CGPoint) {
var allPoints :[CGPoint] = [CGPoint]()
let deltaX = fabs(endPoint.x - startPoint.x)
let deltaY = fabs(endPoint.y - startPoint.y)
var x = startPoint.x
var y = startPoint.y
var err = deltaX-deltaY
var sx = -0.5
var sy = -0.5
if(startPoint.x<endPoint.x){
sx = 0.5
}
if(startPoint.y<endPoint.y){
sy = 0.5;
}
repeat {
let pointObj = CGPoint(x: x, y: y)
allPoints.append(pointObj)
let e = 2*err
if(e > -deltaY)
{
err -= deltaY
x += CGFloat(sx)
}
if(e < deltaX)
{
err += deltaX
y += CGFloat(sy)
}
} while (round(x) != round(endPoint.x) && round(y) != round(endPoint.y));
allPoints.append(endPoint)
}

Resources