Giter Club home page Giter Club logo

Comments (8)

AllanCameron avatar AllanCameron commented on July 24, 2024

I had a similar thought - I wonder if we should just convert vjust to an offset at an early stage and then use the calculation for the offset line that you asked about on SO

from geomtextpath.

teunbrand avatar teunbrand commented on July 24, 2024

Yeah I have tried that and while it's great for simplifying the calculation, that approach gave me minor imprecisions. E.g. in the unit tests, where angles should be -45, 0 and 45, I got -45.3..., 0 and 45.4.... See illustration below. The curvature approach has some rounding error imprecisions as well, but they are on a much smaller scale (somewhere around the 13th decimal place). The aim of this endeavour is to avoid calling .add_path_data() and .get_path_points in a loop for the same data, where only the offset changes.

image

from geomtextpath.

AllanCameron avatar AllanCameron commented on July 24, 2024

I think the offset path is just calculated as

  xout <- .data$x + cos(rads + pi / 2) * c(offset[1], offset)
  yout <- .data$y + sin(rads + pi / 2) * c(offset[1], offset)

and if you need the starting points it would be

xstart <- xout[1]
ystart <- yout[1]

The only problem is that you will need to convert the offset from text space to plotting space (i.e. the offset path ends up too far away from the original path)

from geomtextpath.

teunbrand avatar teunbrand commented on July 24, 2024

Alright, let me put the problem in terms of a reprex.

We can use this function based on the SO question (using a lag of 2 in diff() seems to work better than a lag of 1):

calc_offset_direct <- function(x, y, offset = 0) {
  n  <- length(x)
  dx <- diff(x, 2)
  dy <- diff(y, 2)
  ang  <- atan(dy / dx)
  if (length(ang) > 1) {
    dang <- diff(ang)
    dang <- ifelse(dang < - pi / 2, dang + pi, dang)
    dang <- ifelse(dang > + pi / 2, dang - pi, dang)
    ang  <- cumsum(c(ang[1], 0, dang))
  } else {
    ang <- rep(ang, 2)
  }
  ang <- c(ang, ang[length(ang)])
  xout <- x + cos(ang + pi / 2) * offset
  yout <- y + sin(ang + pi / 2) * offset
  list(x = xout, y = yout)
}

Whereas this attempts to do the same thing using curvature:

calc_offset_curvature <- function(x, y, offset = 0) {
  n  <- length(x)
  dx <- diff(x)
  dy <- diff(y)
  ang  <- atan(dy / dx)
  if (length(ang) > 1) {
    dang <- diff(ang)
    dang <- ifelse(dang < - pi / 2, dang + pi, dang)
    dang <- ifelse(dang > + pi / 2, dang - pi, dang)
    ang  <- cumsum(c(ang[1], 0, dang))
  } else {
    dang <- c(0, 0)
    ang <- rep(ang, 2)
  }
  
  xstart <- x[1] + cos(ang[1] + pi / 2) * offset
  ystart <- y[1] + sin(ang[1] + pi / 2) * offset
  
  lens <- sqrt(dx^2 + dy^2)
  
  dang <- approx(seq_along(dang), dang,
                 seq(1, length(dang), length.out = n - 1))$y
  
  curv <- (dang / lens)
  curv <- 1 - curv * offset
  
  eff_len <- c(0, curv * lens)
  xout <- cumsum(cos(ang) * eff_len) + rep(xstart, each = n)
  yout <- cumsum(sin(ang) * eff_len) + rep(ystart, each = n)

  list(
    x = xout,
    y = yout
  )
}

Now we can test this with a simple path:

x <- 1:5
y <- c(1:3,2,1)

off_direct <- calc_offset_direct(x, y, offset = 1)
off_curve  <- calc_offset_curvature(x, y, offset = 1)

xlim <- range(c(x, off_direct$x, off_curve$x))
ylim <- range(c(y, off_direct$y, off_curve$y))
plot(x, y, type = 'b', xlim = xlim, ylim = ylim)
lines(off_direct$x, off_direct$y, type = 'b', col = 2)
lines(off_curve$x,  off_curve$y,  type = 'b', col = 3)

You can see that the 'direct' method in red places the middle point at the offset distance from the original data, but that it affects the angles, because the distance to the middle point should be sqrt(2) == 1.414214. On the other hand, the curvature method in green keeps the angles nice and consistent, but there is a small inaccuracy in there that I can't seem to wrap my head around.

# The curve method starts great but ends inaccurately
sqrt((off_curve$x - x)^2  + (off_curve$y - y)^2)
#> [1] 1.000000 1.000000 1.447972 1.048261 1.048261
# The direct method doesn't place the middle point correctly in terms of angles,
# but does respect the distance
sqrt((off_direct$x - x)^2 + (off_direct$y - y)^2)
#> [1] 1 1 1 1 1

Created on 2021-11-23 by the reprex package (v2.0.1)

from geomtextpath.

AllanCameron avatar AllanCameron commented on July 24, 2024

The only reason to calculate curvature should be to get a correct adj_length. I think there is probably a better way to do this. In an attempt to simplify, I have added another little utility file of trig helpers, which we might find useful. These include simple and safe calculations for calculating gradients, angles, arc lengths and offset paths. There is even a function to calculate the adjusted length ratios of a path at a given offset distance, that should remove the need for curvature calculations. I have not used these in the rest of the codebase at the moment in case that interferes with your current work, but it might be worth having a look to see if any of these could be useful.

from geomtextpath.

teunbrand avatar teunbrand commented on July 24, 2024

Please go ahead with anything you've planned, I've isolated my experiment in a separate branch and I'll have a look at how to incorporate this later once I've got it up and running.

I found another method using angle bisector that seems to do a good job in terms of angles and distances, and isn't extremely complicated:

offset_bisect <- function(x, y, offset = 0) {
  n  <- length(x)
  dx <- diff(x)
  dy <- diff(y)
  ang  <- atan(dy / dx)
  dang <- diff(ang)
  dang <- ifelse(dang < - pi / 2, dang + pi, dang)
  dang <- ifelse(dang > + pi / 2, dang - pi, dang)
  
  # Get orthogonal angles
  ang <- cumsum(c(ang[1], dang)) + pi / 2
  
  # Left / right aligned indices
  before <- c(1L, seq_along(ang))
  after  <- c(seq_along(ang), length(ang))
  
  # Calculate angle bisector
  bis <- (ang[before] + ang[after])/2
  
  # Calculate x position at angle bisector
  xx <- cos(ang)
  xx <- xx[before] * xx[after]
  
  # Calculate y position at angle bisector
  yy <- sin(ang)
  yy <- yy[before] * yy[after]
  
  # Find appropriate length along bisector
  len <- offset * sqrt(2) / sqrt(1 + xx + yy)
  
  # Project new points at the bisector
  xout <- len * cos(bis) + x
  yout <- len * sin(bis) + y
  
  return(list(x = xout, y = yout))
}

I'll be trying to incorporate this, but the issue is resolved :)

from geomtextpath.

AllanCameron avatar AllanCameron commented on July 24, 2024

I'm glad you've found a way round. To be honest, I'm a bit stumped by your example, since it is not obvious to me that the green offset is necessarily the "correct" one. It really depends on how we define it, and what visual effect would look best going round a shape with sharp angles. I would have thought that a curve with a radius of the offset would be best in this scenario - something like the blue line here, which I guess you would define as the set of points which can be connected to the path with a line segment of length one:

image

from geomtextpath.

teunbrand avatar teunbrand commented on July 24, 2024

Well a rounded corner also make sense for the round join type of lines, whereas I was looking for a miter style. I think both types are correct for offsetting a path, but it depends which is preferable. For this positive offset where the offset path goes around the angle, round make sense, whereas it doesn't if the offset was negative.

from geomtextpath.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.