Jetpack Compose text background with indent - text

I'm new to jetpack compose and i wanat to know if this is possible to achive with jetpack compose. Giving a background to text is very easy with compose but if you want to give indent to background according to text position i don't know where to start an achive this effect.
Text background with indent

I did that:
#Composable
fun IBgText(
text: String,
modifier: Modifier = Modifier,
iBgStrokeWidth: Float? = null,
iBgStrokeColor: Color = Color.DarkGray,
iBgVerticalPadding: Dp = 0.dp,
iBgHorizontalPadding: Dp = 0.dp,
iBgCornerRadius: Dp = 0.dp,
iBgColor: Color = Color.Gray,
color: Color = Color.Unspecified,
fontSize: TextUnit = TextUnit.Unspecified,
fontStyle: FontStyle? = null,
fontWeight: FontWeight? = null,
fontFamily: FontFamily? = null,
letterSpacing: TextUnit = TextUnit.Unspecified,
textDecoration: TextDecoration? = null,
textAlign: TextAlign? = null,
lineHeight: TextUnit = TextUnit.Unspecified,
overflow: TextOverflow = TextOverflow.Clip,
softWrap: Boolean = true,
maxLines: Int = Int.MAX_VALUE,
style: TextStyle = LocalTextStyle.current
) {
val vSpace = with(LocalDensity.current) { iBgVerticalPadding.toPx() }
val hSpace = with(LocalDensity.current) { iBgHorizontalPadding.toPx() }
val corner = with(LocalDensity.current) { iBgCornerRadius.toPx() }
var path by remember { mutableStateOf(Path()) }
fun computePath(layoutResult: TextLayoutResult): Path {
fun isInnerCorner(
lr: TextLayoutResult,
i: Int,
top: Boolean = false,
right: Boolean
): Boolean {
if (top && i == 0) return false
if (!top && i == lr.lineCount - 1) return false
if (top && right) return lr.getLineRight(i - 1) > lr.getLineRight(i)
if (!top && right) return lr.getLineRight(i + 1) > lr.getLineRight(i)
if (top && !right) return lr.getLineLeft(i - 1) < lr.getLineLeft(i)
return lr.getLineLeft(i + 1) < lr.getLineLeft(i)
}
val nbLines = layoutResult.lineCount
for (i in 0 until nbLines) {
var top = layoutResult.getLineTop(i)
var bottom = layoutResult.getLineBottom(i)
val right = layoutResult.getLineRight(i) + hSpace
val topInner = isInnerCorner(layoutResult, i, top = true, right = true)
val bottomInner = isInnerCorner(layoutResult, i, top = false, right = true)
if (topInner) top += vSpace else top -= vSpace
if (bottomInner) bottom -= vSpace else bottom += vSpace
path.apply {
if (i == 0) {
moveTo(right - corner, top)
} else {
if (topInner) {
lineTo(right + corner, top)
} else {
lineTo(right - corner, top)
}
}
quadraticBezierTo(right, top, right, top + corner)
lineTo(right, bottom - corner)
if (bottomInner) {
quadraticBezierTo(right, bottom, right + corner, bottom)
} else {
quadraticBezierTo(right, bottom, right - corner, bottom)
}
}
}
for (i in (nbLines - 1) downTo 0) {
var top = layoutResult.getLineTop(i)
var bottom = layoutResult.getLineBottom(i)
val left = layoutResult.getLineLeft(i) - hSpace
val topInner = isInnerCorner(layoutResult, i, top = true, right = false)
val bottomInner = isInnerCorner(layoutResult, i, top = false, right = false)
if (topInner) top += vSpace else top -= vSpace
if (bottomInner) bottom -= vSpace else bottom += vSpace
path.apply {
if (bottomInner) {
lineTo(left - corner, bottom)
} else {
lineTo(left + corner, bottom)
}
quadraticBezierTo(left, bottom, left, bottom - corner)
lineTo(left, top + corner)
if (topInner) {
quadraticBezierTo(left, top, left - corner, top)
} else {
quadraticBezierTo(left, top, left + corner, top)
}
}
}
path.close()
return path
}
Text(
text,
onTextLayout = { layoutResult ->
path = computePath(layoutResult = layoutResult)
},
modifier = modifier.drawBehind {
drawPath(path, style = Fill, color = iBgColor)
if (iBgStrokeWidth != null) {
drawPath(path, style = Stroke(width = iBgStrokeWidth), color = iBgStrokeColor)
}
},
color = color,
fontSize = fontSize,
fontStyle = fontStyle,
fontWeight = fontWeight,
fontFamily = fontFamily,
letterSpacing = letterSpacing,
textDecoration = textDecoration,
textAlign = textAlign,
lineHeight = lineHeight,
overflow = overflow,
softWrap = softWrap,
maxLines = maxLines,
style = style
)
}
Usage:
#Preview
#Composable
fun Preview() {
Column (modifier = Modifier.fillMaxSize().background(Color.Black)){
IBgText(
text = "test\ntest test\ntest\ntest test",
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 6.dp, start = 10.dp, end = 10.dp, bottom = 6.dp),
iBgColor = Color.Blue.copy(alpha = .4f),
iBgStrokeWidth = 3f,
iBgCornerRadius = 2.dp,
iBgHorizontalPadding = 5.dp,
iBgStrokeColor = Color.Red,
iBgVerticalPadding = 1.dp,
color = Color.White
)
IBgText(
text = "This is a sample\ntext",
textAlign = TextAlign.Center,
modifier = Modifier.padding(20.dp, bottom = 6.dp).rotate(-15f),
iBgColor = Color(0xFFFFFFFF),
iBgCornerRadius = 2.dp,
iBgHorizontalPadding = 8.dp,
iBgVerticalPadding = 5.dp
)
IBgText(
text = "line 1\n-- line 2 --",
textAlign = TextAlign.End,
modifier = Modifier.padding(10.dp)
)
}
}
Result:
It is not pretty, but i guess it does the job.
onTextLayout = { layoutResult -> gives the boundaries of each line.
left, top, bottom, right
Then you can make a path going through each line.
I did a loop from the top right corner (1st line end) to the bottom right corner (last line end)
Then another loop from the bottom left (last line start) to the top left (first line start).
Then I added some rounded corners to match the picture.
Ps: I started Kotlin and Jetpack compose this week and spend all my Sunday on your question 😅

Related

Compose draw text with border and gradient

Is there any way to draw text like that in compose which will have border and shadow like drop. Font does not matter.
I have tried AnnotatedString to apply the same gradient to each letter with this code:
val colorStops = arrayOf(
0.0f to Color(0xffe2e145),
0.2f to Color(0xff7ab624)
)
Text(
text = buildAnnotatedString {
for (letter in "ANIMALS".toCharArray()) {
withStyle(
SpanStyle(
brush = Brush.linearGradient(colorStops = colorStops)
)
) {
append(letter)
}
}
},
fontSize = 60.sp
)
but it just gets parsed wrong and only gets applied to first letter only
Do you know what I could be doing wrong or is there a better way to do this?
One more thing would like text to be replacable.
If anyone has any ideas would be very grateful.
Edit
This is how stroke looks like on my Galaxy S10
You can use a Box to put more Text elements on top of another:
val colorStops = listOf(
Color(0xffe2e145),
Color(0xff7ab624)
)
Box(
modifier = Modifier
.fillMaxWidth()
) {
//Filled Text
Text(
text = longText,
color = Color(0xffe2e145),
fontSize = 64.sp
)
//Shadow text with a gradient color.
Text(
text = longText,
modifier = Modifier.offset(2.dp, 3.dp),
style = TextStyle(
brush = Brush.horizontalGradient(
colors = colorStops,
tileMode = TileMode.Mirror
)
),
fontSize = 64.sp
)
//Text with stroke border
Text(
text = longText,
color = Color(0xffB71C1C),
style = TextStyle.Default.copy(
fontSize = 64.sp,
drawStyle = Stroke(
miter = 10f,
width = 2f,
join = StrokeJoin.Round
)
)
)
}
Another option is to apply a shadow and a brush to the Text:
Box(
modifier = Modifier
.fillMaxWidth()
) {
//Shadow and Brush Text
Text(
text = longText,
style = TextStyle(
shadow = Shadow(
offset = Offset(8f, 8f),
blurRadius = 6f,
color = LightGray
),
brush = Brush.horizontalGradient(
colors = colorStops,
tileMode = TileMode.Mirror
)
),
fontSize = 64.sp
)
//Text with stroke border
Text(
text = longText,
color = Color(0xffB71C1C),
style = TextStyle.Default.copy(
fontSize = 64.sp,
drawStyle = Stroke(
miter = 10f,
width = 2f,
join = StrokeJoin.Round
)
)
)
}

Godot 3.5 parse_input_event resets mouse position

I have a point and click game for which I'm adapting joypad controls. I want the mouse to move with thumbsticks and trigger mouse click with joystick A button.
I can move the mouse using get_viewport().warp_mouse(mouse_pos)
I can trigger mouse click creating new mousebuttonevent and parsing it
var left_click = InputEventMouseButton.new()
left_click.pressed = pressed
left_click.button_index = button_index
left_click.position = get_viewport().get_mouse_position()
print("emulated: ", left_click.as_text())
Input.parse_input_event(left_click)
But after I call Input.parse_input_event(left_click) and move thumsticks again it has reset the mouse position to 0, 0.
Here is the full code. I'm quite confused.
var mouse_pos
func _ready():
$ColorRect/Button.connect("pressed", self, "pressed")
func pressed():
print("button pressed")
func _input(event):
if event is InputEventMouseButton:
print("from _input: ", event.as_text())
elif event is InputEventJoypadButton:
if event.is_action_pressed("a_button"):
print("pressed A")
emulate_click(BUTTON_LEFT , true)
# elif event.is_action_released("ui_accept"):
# print("released A")
# emulate_click(BUTTON_LEFT, false)
func emulate_click(button_index, pressed):
var left_click = InputEventMouseButton.new()
left_click.pressed = pressed
left_click.button_index = button_index
left_click.position = get_viewport().get_mouse_position()
print("emulated: ", left_click.as_text())
Input.parse_input_event(left_click)
# get_viewport().warp_mouse(cached_mouse_pos)
# pointer.global_position = previous_mouse_pos
func _process(delta):
var direction: Vector2
direction.x = Input.get_action_strength ("t_right", true) - Input.get_action_strength ("t_left", true)
direction.y = Input.get_action_strength ("t_down", true) - Input.get_action_strength ("t_up", true)
if abs(direction.x) == 1 and abs(direction.y) == 1:
direction = direction.normalized()
var thumbstick_sensitivity = 1000
var movement = thumbstick_sensitivity * direction * delta
if (movement):
mouse_pos = get_viewport().get_mouse_position() + movement
print(mouse_pos)
get_viewport().warp_mouse(mouse_pos)```
Any ideas are super welcome!
You need to set the global position (global_pos) of the new InputEventMouseButton object.
This is due to the L332-L344 of input_default.cpp in Godot's source code, which read:
Ref<InputEventMouseButton> mb = p_event;
if (mb.is_valid()) {
if (mb->is_pressed()) {
mouse_button_mask |= (1 << (mb->get_button_index() - 1));
} else {
mouse_button_mask &= ~(1 << (mb->get_button_index() - 1));
}
Point2 pos = mb->get_global_position();
if (mouse_pos != pos) {
set_mouse_position(pos);
}
If you don't set the global position, it defaults to (0,0) which is why you're seeing this unexpected behaviour.

Is it possible to generate calculated plots/data from an interactive Bokeh plot?

I have a plot where there are some fixed theoretical points and the experimental points are plotted using a combination of multi-select buttons. Is there a way I can generate data of the %error between the theoretical points and the experimental data?
An example of the plot is shown below:
This code displays the percent error as text next to the theoretical point. You can also draw a line on the canvas from theoretical to experimental point and the difference data will be displayed in a tooltip. The first click (button release) initiates the "difference" line. The next click ends it. The tooltip remains always on the plot but you could easily tweak it so it disappears after the second click.
import numpy as np
from bokeh.plotting import figure, show
from bokeh.models import ColumnDataSource, CustomJS
from bokeh.events import *
source = ColumnDataSource({'x': [], 'y': []})
p = figure(plot_width = 900)
x_data = np.arange(10)
source_expected = dict(x = x_data, y = np.random.random(10), color=['blue']*10)
source_measured = dict(x = x_data, y = np.random.random(10), color=['grey']*10)
line_expected = p.line('x', 'y', line_color = 'blue', source = source_expected)
circle_expected = p.circle('x', 'y', color = 'color', source = source_expected, size = 10)
line_measured = p.line('x', 'y', line_color = 'grey', source = source_measured)
# circle_measured = p.circle('x', 'y', color = 'color', fill_color = 'white', source = source_measured, size = 60)
circle_measured = p.circle('x', 'y', color = 'color', source = source_measured, size = 10)
source_error = {'x': [], 'y': [], 'error': [], 'percent': [], 'text': [], 'color': []}
i = 0
for expected, measured in zip(source_expected['y'], source_measured['y']):
source_error['x'].append(x_data[i])
source_error['y'].append(measured)
error = expected - measured
percent = (error/expected) * 100
source_error['error'].append(error)
source_error['percent'].append(percent)
source_error['text'].append('+{}%'.format('%.2f' % percent) if percent > 0 else '{}%'.format('%.2f' % percent))
source_error['color'].append('green' if expected == measured else 'red')
i = i + 1
# text_measured = p.text('x', 'y', text = 'text', text_color = 'color', text_font_size = '9pt', x_offset = -25, y_offset = 6, source = source_error)
text_measured = p.text('x', 'y', text = 'text', text_color = 'color', text_font_size = '9pt', x_offset = 10, y_offset = 5, source = source_error)
line = p.line('x', 'y', line_color = 'red', line_dash = 'dashed', source = source)
callback_tap = '''
if (typeof custom_tooltip == 'undefined') {
custom_tooltip = document.createElement('div');
custom_tooltip.setAttribute('id','tooltip_div');
custom_tooltip.style = 'position: absolute; left: 0px; top: 0px; z-index: 9999; border:1px solid black; padding: 10px; background: white; font-family: arial; font-size: 12px'
document.body.prepend(custom_tooltip);
}
if (true === Bokeh.drawing) {
Bokeh.drawing = false
}
else {
if (!Bokeh.drawing) {
src.data = {'x':[], 'y':[]}
src.change.emit()
}
src.data['x'].push(cb_obj.x)
src.data['y'].push(cb_obj.y)
Bokeh.drawing = true
Bokeh.sx_start = cb_obj.sx
Bokeh.x_start = cb_obj.x
Bokeh.sy_start = cb_obj.sy
Bokeh.y_start = cb_obj.y
}'''
callback_mousemove = '''
function print(...args) {
for (i in args) {
console.log(args[i])
}
}
if (Bokeh.drawing) {
if (src.data['x'].length > 1) {
src.data['x'].pop()
src.data['y'].pop()
}
src.data['x'].push(cb_obj.x)
src.data['y'].push(cb_obj.y)
src.change.emit()
tooltip = document.getElementById('tooltip_div')
tooltip.style.left = cb_obj.sx + 30 + 'px'
tooltip.style.top = cb_obj.sy + 10 + 'px'
var error = Math.round((Bokeh.y_start - cb_obj.y) * 100) / 100
var percent = Math.round(error * 100 / Bokeh.y_start)
tooltip.innerHTML = 'Distance X: ' + Math.round(Bokeh.sx_start - cb_obj.sx) + ' px' + ' (' + (Math.round((Bokeh.x_start - cb_obj.x) * 100) / 100) + ' units)' +
'<br />' +
'Distance Y: ' + Math.round(Bokeh.sy_start - cb_obj.sy) + ' px' + ' (' + (Math.round((Bokeh.y_start - cb_obj.y) * 100) / 100) + ' units)' +
'<br />' +
'Error: ' + error + ' units' +
'<br />' +
'% Error: ' + percent + ' %'
}'''
p.js_on_event('tap', CustomJS(args = {'src': source, }, code = callback_tap))
p.js_on_event('mousemove', CustomJS(args = {'src': source, }, code = callback_mousemove))
show(p)
Result:
Swapping the commented lines with the lines next to them makes the error percent to be displayed in a big circle representing the theoretical point. It looks then like this:
This script was written for Python v3.7.2 and Bokeh v1.3.0

How to get the average color of a specific area in a webcam feed (Processing/JavaScript)?

I'm using Processing to get a webcam feed from my laptop. In the top left corner, I have drawn a rectangle over the displayed feed. I'm trying to get the average color of the webcam, but only in the region contained by that rectangle.
I keep getting color (0, 0, 0), black, as the result.
Thank you all!
PS sorry if my code seems messy..I'm new at Processing and so I don't know if this might be hard to read or contain bad practices. Thank you.
import processing.video.*;
Capture webcam;
Capture cap;
PImage bg_img;
color bgColor = color(0, 0, 0);
int rMargin = 50;
int rWidth = 100;
color input = color(0, 0, 0);
color background = color(255, 255, 255);
color current;
int bgTolerance = 5;
void setup() {
size(1280,720);
// start the webcam
String[] inputs = Capture.list();
if (inputs.length == 0) {
println("Couldn't detect any webcams connected!");
exit();
}
webcam = new Capture(this, inputs[0]);
webcam.start();
}
void draw() {
if (webcam.available()) {
// read from the webcam
webcam.read();
image(webcam, 0,0);
webcam.loadPixels();
noFill();
strokeWeight(2);
stroke(255,255, 255);
rect(rMargin, rMargin, rWidth, rWidth);
int yCenter = (rWidth/2) + rMargin;
int xCenter = (rWidth/2) + rMargin;
// rectMode(CENTER);
int rectCenterIndex = (width* yCenter) + xCenter;
int r = 0, g = 0, b = 0;
//for whole image:
//for (int i=0; i<bg_img.pixels.length; i++) {
// color c = bg_img.pixels[i];
// r += c>>16&0xFF;
// g += c>>8&0xFF;
// b += c&0xFF;
//}
//r /= bg_img.pixels.length;
//g /= bg_img.pixels.length;
//b /= bg_img.pixels.length;
//CALCULATE AVG COLOR:
int i;
for(int x = 50; x <= 150; x++){
for(int y = 50; y <= 150; y++){
i = (width*y) + x;
color c = webcam.pixels[i];
r += c>>16&0xFF;
g += c>>8&0xFF;
b += c&0xFF;
}
}
r /= webcam.pixels.length;
g /= webcam.pixels.length;
b /= webcam.pixels.length;
println(r + " " + g + " " + b);
}
}
You're so close, but missing out one important aspect: the number of pixels you're sampling.
Notice in the example code that is commented out for a full image you're dividing by the full number of pixels (pixels.length).
However, in your adapted version you want to compute the average colour of only a subsection of the full image which means a smaller number of pixels.
You're only sampling an area that is 100x100 pixels meaning you need to divide by 10000 instead of webcam.pixels.length (1920x1000). That is why you get 0 as it's integer division.
This is what I mean in code:
int totalSampledPixels = rWidth * rWidth;
r /= totalSampledPixels;
g /= totalSampledPixels;
b /= totalSampledPixels;
Full tweaked sketch:
import processing.video.*;
Capture webcam;
Capture cap;
PImage bg_img;
color bgColor = color(0, 0, 0);
int rMargin = 50;
int rWidth = 100;
int rHeight = 100;
color input = color(0, 0, 0);
color background = color(255, 255, 255);
color current;
int bgTolerance = 5;
void setup() {
size(1280,720);
// start the webcam
String[] inputs = Capture.list();
if (inputs.length == 0) {
println("Couldn't detect any webcams connected!");
exit();
}
webcam = new Capture(this, inputs[0]);
webcam.start();
}
void draw() {
if (webcam.available()) {
// read from the webcam
webcam.read();
image(webcam, 0,0);
webcam.loadPixels();
noFill();
strokeWeight(2);
stroke(255,255, 255);
rect(rMargin, rMargin, rWidth, rHeight);
int yCenter = (rWidth/2) + rMargin;
int xCenter = (rWidth/2) + rMargin;
// rectMode(CENTER);
int rectCenterIndex = (width* yCenter) + xCenter;
int r = 0, g = 0, b = 0;
//for whole image:
//for (int i=0; i<bg_img.pixels.length; i++) {
// color c = bg_img.pixels[i];
// r += c>>16&0xFF;
// g += c>>8&0xFF;
// b += c&0xFF;
//}
//r /= bg_img.pixels.length;
//g /= bg_img.pixels.length;
//b /= bg_img.pixels.length;
//CALCULATE AVG COLOR:
int i;
for(int x = 0; x <= width; x++){
for(int y = 0; y <= height; y++){
if (x >= rMargin && x <= rMargin + rWidth && y >= rMargin && y <= rMargin + rHeight){
i = (width*y) + x;
color c = webcam.pixels[i];
r += c>>16&0xFF;
g += c>>8&0xFF;
b += c&0xFF;
}
}
}
//divide by just the area sampled (x >= 50 && x <= 150 && y >= 50 && y <= 150 is a 100x100 px area)
int totalSampledPixels = rWidth * rHeight;
r /= totalSampledPixels;
g /= totalSampledPixels;
b /= totalSampledPixels;
fill(r,g,b);
rect(rMargin + rWidth, rMargin, rWidth, rHeight);
println(r + " " + g + " " + b);
}
}
Bare in mind this is averaging in the RGB colour space which is not the same as perceptual colour space. For example, if you average red and yellow you'd expect orange, but in RGB, a bit of red and green makes yellow.
Hopefully the RGB average is good enough for what you need, otherwise you may need to convert from RGB to CIE XYZ colour space then to Lab colour space to compute the perceptual average (then convert back to XYZ and RGB to display on screen). If that is something you're interested in trying, you can find an older answer demonstrating this in openFrameworks (which you'll notice can be similar to Processing in simple scenarios).

How to connect 4 points on a plane so they do not fold on themselves(they always create a Quadrilateral)

I have created a small Raphael app to showcase my struggle.
I created four handles which can be moved. A 'sheet' is covering the entire screen except for the square between the 4 handles.
Whenever the handles are dragged the sheet is placed accordingly.
What ends up happening is that in certain situations, the sheet folds on itself.
It's best if you just see the fiddle. You'll get what I'm talking about
http://jsfiddle.net/8qtffq0s/
How can I avoid this?
Notice: The screen is white. The black part is the sheet, and the white part is a gap in the sheet and not the other way around.
//raphael object
var paper = Raphael(0, 0, 600, 600)
//create 4 handles
h1 = paper.circle(50, 50, 10).attr("fill","green")
h2 = paper.circle(300, 50, 10).attr("fill", "blue")
h3 = paper.circle(300, 300, 10).attr("fill", "yellow")
h4 = paper.circle(50, 300, 10).attr("fill", "red")
//create covering sheet
path = ["M", 0, 0, "L", 600, 0, 600, 600, 0, 600, 'z', "M", h1.attrs.cx, h1.attrs.cy,"L", h4.attrs.cx, h4.attrs.cy, h3.attrs.cx, h3.attrs.cy, h2.attrs.cx, h2.attrs.cy,'z']
sheet = paper.path(path).attr({ "fill": "black", "stroke": "white" }).toBack()
//keep starting position of each handle on dragStart
var startX,startY
function getPos(handle) {
startX= handle.attrs.cx
startY = handle.attrs.cy
}
//Redraw the sheet to match the new handle placing
function reDrawSheet() {
path = ["M", 0, 0, "L", 600, 0, 600, 600, 0, 600, 'z', "M", h1.attrs.cx, h1.attrs.cy, "L", h4.attrs.cx, h4.attrs.cy, h3.attrs.cx, h3.attrs.cy, h2.attrs.cx, h2.attrs.cy, 'z']
sheet.attr("path",path)
}
//enable handle dragging
h1.drag(function (dx, dy) {
this.attr("cx", startX + dx)
this.attr("cy", startY + dy)
reDrawSheet()
},
function () {
getPos(this)
})
h2.drag(function (dx, dy) {
this.attr("cx", startX + dx)
this.attr("cy", startY + dy)
reDrawSheet()
},
function () {
getPos(this)
})
h3.drag(function (dx, dy) {
this.attr("cx", startX + dx)
this.attr("cy", startY + dy)
reDrawSheet()
},
function () {
getPos(this)
})
h4.drag(function (dx, dy) {
this.attr("cx", startX + dx)
this.attr("cy", startY + dy)
reDrawSheet()
},
function () {
getPos(this)
})
Update: I improved the function "reDrawSheet" so now it can classify the points on the strings as top left, bottom left, bottom right, and top right
This solved many of my problems, but in some cases the sheet still folds on it self.
new fiddle: http://jsfiddle.net/1kj06co4/
new code:
function reDrawSheet() {
//c stands for coordinates
c = [{ x: h1.attrs.cx, y: h1.attrs.cy }, { x: h4.attrs.cx, y: h4.attrs.cy }, { x: h3.attrs.cx, y: h3.attrs.cy }, { x: h2.attrs.cx, y: h2.attrs.cy }]
//arrange the 4 points by height
c.sort(function (a, b) {
return a.y - b.y
})
//keep top 2 points
cTop = [c[0], c[1]]
//arrange them from left to right
cTop.sort(function (a, b) {
return a.x - b.x
})
//keep bottom 2 points
cBottom = [c[2], c[3]]
//arrange them from left to right
cBottom.sort(function (a, b) {
return a.x - b.x
})
//top left most point
tl = cTop[0]
//bottom left most point
bl = cBottom[0]
//top right most point
tr = cTop[1]
//bottom right most point
br = cBottom[1]
path = ["M", 0, 0, "L", 600, 0, 600, 600, 0, 600, 'z', "M", tl.x,tl.y, "L", bl.x,bl.y, br.x,br.y, tr.x,tr.y, 'z']
sheet.attr("path",path)
}
To make things super clear, this is what I'm trying to avoid:
Update 2:
I was able to avoid the vertices from crossing by checking which path out of the three possible paths is the shortest and choosing it.
To do so, I added a function that checks the distance between two points
function distance(a, b) {
return Math.sqrt(Math.pow(b.x - a.x, 2) + (Math.pow(b.y - a.y, 2)))
}
And altered the code like so:
function reDrawSheet() {
//c stands for coordinates
c = [{ x: h1.attrs.cx, y: h1.attrs.cy }, { x: h4.attrs.cx, y: h4.attrs.cy }, { x: h3.attrs.cx, y: h3.attrs.cy }, { x: h2.attrs.cx, y: h2.attrs.cy }]
//d stands for distance
d=distance
//get the distance of all possible paths
d1 = d(c[0], c[1]) + d(c[1], c[2]) + d(c[2], c[3]) + d(c[3], c[0])
d2 = d(c[0], c[2]) + d(c[2], c[3]) + d(c[3], c[1]) + d(c[1], c[0])
d3 = d(c[0], c[2]) + d(c[2], c[1]) + d(c[1], c[3]) + d(c[3], c[0])
//choose the shortest distance
if (d1 <= Math.min(d2, d3)) {
tl = c[0]
bl = c[1]
br = c[2]
tr = c[3]
}
else if (d2 <= Math.min(d1, d3)) {
tl = c[0]
bl = c[2]
br = c[3]
tr = c[1]
}
else if (d3 <= Math.min(d1, d2)) {
tl = c[0]
bl = c[2]
br = c[1]
tr = c[3]
}
path = ["M", 0, 0, "L", 600, 0, 600, 600, 0, 600, 'z', "M", tl.x,tl.y, "L", bl.x,bl.y, br.x,br.y, tr.x,tr.y, 'z']
sheet.attr("path",path)
}
Now the line does not cross itself like the image I attached about, but the sheet "flips" so everything turns black.
You can see the path is drawn correctly to connect the for points by the white stroke, but it does not leave a gap
new fiddle: http://jsfiddle.net/1kj06co4/1/
Picture of problem:
So... the trouble is to tell the inside from the outside.
You need the following functions:
function sub(a, b) {
return { x: a.x - b.x , y: a.y - b.y };
}
function neg(a) {
return { x: -a.x , y: -a.y };
}
function cross_prod(a, b) {
// 2D vecs, so z==0.
// Therefore, x and y components are 0.
// Return the only important result, z.
return (a.x*b.y - a.y*b.x);
}
And then you need to do the following once you've found tl,tr,br, and bl:
tlr = sub(tr,tl);
tbl = sub(bl,tl);
brl = sub(bl,br);
btr = sub(tr,br);
cropTL = cross_prod( tbl, tlr );
cropTR = cross_prod(neg(tlr),neg(btr));
cropBR = cross_prod( btr, brl );
cropBL = cross_prod(neg(brl),neg(tbl));
cwTL = cropTL > 0;
cwTR = cropTR > 0;
cwBR = cropBR > 0;
cwBL = cropBL > 0;
if (cwTL) {
tmp = tr;
tr = bl;
bl = tmp;
}
if (cwTR == cwBR && cwBR == cwBL && cwTR!= cwTL) {
tmp = tr;
tr = bl;
bl = tmp;
}
My version of the fiddle is here. :) http://jsfiddle.net/1kj06co4/39/

Resources