The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.
The wild anatomy of a stick figure.
xkcdman() draws a stick figure defined entirely by
angles (in radians) for each body segment. The figure
has 8 parts:
| Parameter | Body part |
|---|---|
angleofneck |
neck — connects head to top of spine |
angleofspine |
spine — main torso |
anglerighthumerus |
right upper arm |
anglerightradius |
right forearm |
anglelefthumerus |
left upper arm |
angleleftradius |
left forearm |
anglerightleg |
right leg |
angleleftleg |
left leg |
Two other parameters control size and proportion:
scale — diameter of the head (all other segments are
proportional to it). Set it to roughly 10–15% of
diff(yrange).ratioxy — diff(xrange) / diff(yrange).
Corrects for axis scaling so the figure is not stretched horizontally or
vertically.All angles follow the standard mathematical convention: 0 = right, π/2 = up, π = left, 3π/2 (or −π/2) = down.
Each bone is drawn separately with its own color so you can see exactly what each parameter controls. The helper below draws a single labeled figure with colored segments.
# --- geometry helper: compute bone endpoints from the same formulas as xkcdman
make_figure <- function(x, y, scale, ratioxy,
angleofneck, angleofspine,
anglerighthumerus, anglerightradius,
anglelefthumerus, angleleftradius,
anglerightleg, angleleftleg) {
dh <- scale # head diameter
ls <- dh # spine length
ll <- ls * 1.2 # leg length
lh <- ls * 0.6 # humerus length
lr <- ls * 0.5 # radius length
# key joint positions
shoulder <- c(x, y) +
(dh / 2) * c(cos(angleofneck) * ratioxy, sin(angleofneck))
hip <- shoulder +
ls * c(cos(angleofspine) * ratioxy, sin(angleofspine))
end_rh <- shoulder +
lh * c(cos(anglerighthumerus) * ratioxy, sin(anglerighthumerus))
end_lh <- shoulder +
lh * c(cos(anglelefthumerus) * ratioxy, sin(anglelefthumerus))
seg <- function(from, angle, dist)
data.frame(
x = from[1],
y = from[2],
xend = from[1] + dist * cos(angle) * ratioxy,
yend = from[2] + dist * sin(angle)
)
list(
head = data.frame(x = x, y = y, diameter = dh),
spine = seg(shoulder, angleofspine, ls),
rarm1 = seg(shoulder, anglerighthumerus, lh),
rarm2 = seg(end_rh, anglerightradius, lr),
larm1 = seg(shoulder, anglelefthumerus, lh),
larm2 = seg(end_lh, angleleftradius, lr),
rleg = seg(hip, anglerightleg, ll),
lleg = seg(hip, angleleftleg, ll)
)
}
# Standard upright pose — large figure centred in the plot
xrange <- c(0, 10)
yrange <- c(0, 10)
rxy <- diff(xrange) / diff(yrange)
sc <- 2.5 # bigger figure fills the space
f <- make_figure(
x = 5, y = 7.2, scale = sc, ratioxy = rxy,
angleofneck = -pi / 2,
angleofspine = -pi / 2,
anglerighthumerus = -pi / 6,
anglerightradius = pi / 6,
anglelefthumerus = -pi / 2 - pi / 6,
angleleftradius = -pi / 6,
anglerightleg = 3 * pi / 2 - pi / 12,
angleleftleg = 3 * pi / 2 + pi / 12
)
colours <- c(
head = "#e41a1c",
spine = "#377eb8",
rarm1 = "#ff7f00",
rarm2 = "#ff7f00",
larm1 = "#4daf4a",
larm2 = "#4daf4a",
rleg = "#984ea3",
lleg = "#a65628"
)
set.seed(99)
p_anat <- ggplot() + coord_fixed(xlim = xrange, ylim = yrange)
# head
p_anat <- p_anat +
geom_xkcdpath(aes(x = x, y = y, diameter = diameter),
data = f$head, colour = colours["head"],
linewidth = 1.4, ratioxy = rxy, mask = FALSE)
# bones
bones <- c("spine", "rarm1", "rarm2", "larm1", "larm2", "rleg", "lleg")
for (b in bones) {
p_anat <- p_anat +
geom_xkcdpath(aes(x = x, y = y, xend = xend, yend = yend),
data = f[[b]], colour = colours[b],
linewidth = 1.4, mask = FALSE,
xjitteramount = 0.02, yjitteramount = 0.02)
}
library(ggrepel)
# Label origins at bone midpoints, nudged outward from figure centre
labels_df <- data.frame(
x = c(
f$head$x,
(f$spine$x + f$spine$xend) / 2,
(f$rarm1$x + f$rarm1$xend) / 2,
(f$rarm2$x + f$rarm2$xend) / 2,
(f$larm1$x + f$larm1$xend) / 2,
(f$larm2$x + f$larm2$xend) / 2,
(f$rleg$x + f$rleg$xend) / 2,
(f$lleg$x + f$lleg$xend) / 2
),
y = c(
f$head$y,
(f$spine$y + f$spine$yend) / 2,
(f$rarm1$y + f$rarm1$yend) / 2,
(f$rarm2$y + f$rarm2$yend) / 2,
(f$larm1$y + f$larm1$yend) / 2,
(f$larm2$y + f$larm2$yend) / 2,
(f$rleg$y + f$rleg$yend) / 2,
(f$lleg$y + f$lleg$yend) / 2
),
label = c("head / scale", "angleofspine",
"anglerighthumerus", "anglerightradius",
"anglelefthumerus", "angleleftradius",
"anglerightleg", "angleleftleg"),
col = unname(colours[c("head","spine","rarm1","rarm2","larm1","larm2","rleg","lleg")]),
# nudge labels away from figure: right side +x, left side -x, head up
nx = c( 2.0, 0.3, 2.5, 2.8, -2.5, -2.8, -2.0, 2.0),
ny = c( 1.2, 0, 0.4, -0.5, 0.4, -0.5, -0.5, -0.8)
)
p_anat <- p_anat +
geom_text_repel(
aes(x = x, y = y, label = label, colour = col),
data = labels_df,
nudge_x = labels_df$nx,
nudge_y = labels_df$ny,
family = "xkcd",
size = 4,
box.padding = unit(0.1, "lines"),
point.padding = unit(0.1, "lines"),
force = 1,
min.segment.length = 0.3,
segment.size = 0.4,
segment.colour = "grey50",
seed = 42,
show.legend = FALSE
) +
scale_colour_identity()
p_anat <- p_anat +
annotate("text", x = 5, y = 9.3,
label = "xkcdman anatomy", family = "xkcd", size = 6) +
annotate("text", x = 5, y = 8.9,
label = "angleofneck connects head to shoulder",
family = "xkcd", size = 3, colour = "grey40") +
theme_xkcd() +
theme(axis.text = element_blank(), axis.ticks = element_blank(),
axis.title = element_blank())
p_anatSix figures in a faceted layout, each with a different pose. Every figure uses the same helper so we only specify the angles that change.
# Helper: return list(segs = df, head = df) for one pose
pose_segments <- function(pose_name, x, y, scale, ratioxy,
angleofneck, angleofspine,
anglerighthumerus, anglerightradius,
anglelefthumerus, angleleftradius,
anglerightleg, angleleftleg) {
f <- make_figure(x, y, scale, ratioxy,
angleofneck, angleofspine,
anglerighthumerus, anglerightradius,
anglelefthumerus, angleleftradius,
anglerightleg, angleleftleg)
segs <- do.call(rbind, lapply(
c("spine","rarm1","rarm2","larm1","larm2","rleg","lleg"),
function(b) { d <- f[[b]]; d$part <- b; d }
))
segs$pose <- pose_name
head_df <- data.frame(x = f$head$x, y = f$head$y,
diameter = f$head$diameter,
xend = NA_real_, yend = NA_real_,
part = "head", pose = pose_name)
list(segs = segs, head = head_df)
}
# Helper: vary one angle across values, return list(segs=df, heads=df)
vary_angle <- function(param, values, labels) {
all_segs <- NULL
all_heads <- NULL
for (i in seq_along(values)) {
args <- neutral
args[[param]] <- values[i]
r <- pose_segments(
pose_name = labels[i],
x = 5, y = 7.2, scale = 1.4, ratioxy = 1,
angleofneck = args$angleofneck,
angleofspine = args$angleofspine,
anglerighthumerus = args$anglerighthumerus,
anglerightradius = args$anglerightradius,
anglelefthumerus = args$anglelefthumerus,
angleleftradius = args$angleleftradius,
anglerightleg = args$anglerightleg,
angleleftleg = args$angleleftleg
)
all_segs <- rbind(all_segs, r$segs)
all_heads <- rbind(all_heads, r$head)
}
list(segs = all_segs, heads = all_heads)
}
# Common geometry: square data space
xr <- c(0, 10); yr <- c(0, 10); rxy <- 1; sc <- 1.4
poses <- list(
# 1. Standing at rest
pose_segments("Standing",
x = 5, y = 7.2, scale = sc, ratioxy = rxy,
angleofneck = -pi/2,
angleofspine = -pi/2,
anglerighthumerus = -pi/6,
anglerightradius = pi/6,
anglelefthumerus = -pi/2 - pi/6,
angleleftradius = -pi/6,
anglerightleg = 3*pi/2 - pi/12,
angleleftleg = 3*pi/2 + pi/12),
# 2. Arms raised (cheering)
pose_segments("Arms up",
x = 5, y = 7.2, scale = sc, ratioxy = rxy,
angleofneck = -pi/2,
angleofspine = -pi/2,
anglerighthumerus = pi/3,
anglerightradius = pi/2,
anglelefthumerus = pi - pi/3,
angleleftradius = pi/2,
anglerightleg = 3*pi/2 - pi/12,
angleleftleg = 3*pi/2 + pi/12),
# 3. Walking (leaning forward, legs apart)
pose_segments("Walking",
x = 5, y = 7.4, scale = sc, ratioxy = rxy,
angleofneck = -pi/2 + pi/10,
angleofspine = -pi/2 + pi/10,
anglerighthumerus = pi/2 + pi/6,
anglerightradius = pi/2,
anglelefthumerus = -pi/6,
angleleftradius = 0,
anglerightleg = 3*pi/2 - pi/5,
angleleftleg = 3*pi/2 + pi/4),
# 4. Pointing right
pose_segments("Pointing",
x = 5, y = 7.2, scale = sc, ratioxy = rxy,
angleofneck = -pi/2,
angleofspine = -pi/2,
anglerighthumerus = 0,
anglerightradius = 0,
anglelefthumerus = -pi/2 - pi/6,
angleleftradius = -pi/6,
anglerightleg = 3*pi/2 - pi/12,
angleleftleg = 3*pi/2 + pi/12),
# 5. Sitting (spine horizontal, legs bent)
pose_segments("Sitting",
x = 5, y = 5.8, scale = sc, ratioxy = rxy,
angleofneck = -pi/2,
angleofspine = 0,
anglerighthumerus = -pi/2 - pi/6,
anglerightradius = -pi/2,
anglelefthumerus = -pi/2 + pi/6,
angleleftradius = -pi/2,
anglerightleg = pi/2,
angleleftleg = pi/2 + pi/2),
# 6. Falling / off-balance
pose_segments("Falling",
x = 5, y = 7.0, scale = sc, ratioxy = rxy,
angleofneck = -pi/2 - pi/4,
angleofspine = -pi/2 - pi/4,
anglerighthumerus = pi/4,
anglerightradius = pi/2,
anglelefthumerus = pi - pi/8,
angleleftradius = pi/2 + pi/4,
anglerightleg = 3*pi/2 + pi/6,
angleleftleg = 3*pi/2 - pi/3)
)
# Combine into one data frame
all_segs <- do.call(rbind, lapply(poses, `[[`, "segs"))
all_heads <- do.call(rbind, lapply(poses, `[[`, "head"))
part_colours <- c(
head = "#e41a1c",
spine = "#377eb8",
rarm1 = "#ff7f00", rarm2 = "#ff7f00",
larm1 = "#4daf4a", larm2 = "#4daf4a",
rleg = "#984ea3",
lleg = "#a65628"
)
all_segs$colour <- part_colours[all_segs$part]
all_heads$colour <- part_colours["head"]
# Fix facet order
pose_order <- c("Standing","Arms up","Walking","Pointing","Sitting","Falling")
all_segs$pose <- factor(all_segs$pose, levels = pose_order)
all_heads$pose <- factor(all_heads$pose, levels = pose_order)
set.seed(42)
p_poses <- ggplot() +
# draw each bone segment
geom_xkcdpath(
aes(x = x, y = y, xend = xend, yend = yend, colour = colour),
data = all_segs,
linewidth = 1.3, mask = FALSE,
xjitteramount = 0.04, yjitteramount = 0.04,
inherit.aes = FALSE
) +
# draw each head circle
geom_xkcdpath(
aes(x = x, y = y, diameter = diameter, colour = colour),
data = all_heads,
linewidth = 1.3, ratioxy = 1, mask = FALSE,
inherit.aes = FALSE
) +
scale_colour_identity(
guide = "legend",
name = "Body part",
breaks = part_colours[c("head","spine","rarm1","larm1","rleg","lleg")],
labels = c("head / scale","spine","right arm","left arm","right leg","left leg")
) +
facet_wrap(~ pose, ncol = 3) +
coord_fixed(xlim = xr, ylim = yr) +
theme_xkcd() +
theme(
axis.text = element_blank(),
axis.ticks = element_blank(),
axis.title = element_blank(),
strip.text = element_text(family = "xkcd", size = 13)
) +
labs(title = "xkcdman pose gallery")
p_posesThe figure below shows the same upright figure four times, varying one parameter at a time across a row, so you can see directly how each angle value changes the pose.
# Neutral (upright) pose used as baseline
neutral <- list(
angleofneck = -pi/2,
angleofspine = -pi/2,
anglerighthumerus = -pi/6,
anglerightradius = pi/6,
anglelefthumerus = -pi/2 - pi/6,
angleleftradius = -pi/6,
anglerightleg = 3*pi/2 - pi/12,
angleleftleg = 3*pi/2 + pi/12
)
params_to_vary <- list(
list(p = "angleofspine",
v = c(-pi/2, -pi/2 + pi/6, -pi/2 + pi/3, 0),
l = c("spine: -pi/2\n(upright)", "spine: -pi/3", "spine: -pi/6", "spine: 0\n(horizontal)")),
list(p = "anglerighthumerus",
v = c(-pi/6, 0, pi/4, pi/2),
l = c("rarm: -pi/6\n(rest)", "rarm: 0\n(right)", "rarm: pi/4", "rarm: pi/2\n(up)")),
list(p = "anglerightleg",
v = c(3*pi/2 - pi/12, 3*pi/2 - pi/4, 3*pi/2 + pi/4, 3*pi/2 - pi/2),
l = c("rleg: rest", "rleg: back", "rleg: forward", "rleg: up")),
list(p = "anglelefthumerus",
v = c(-pi/2-pi/6, pi-pi/6, pi/2+pi/6, pi),
l = c("larm: rest", "larm: left", "larm: up-left", "larm: out"))
)
ref_segs <- do.call(rbind, lapply(params_to_vary, function(pv) {
r <- vary_angle(pv$p, pv$v, pv$l)
r$segs$group_param <- pv$p
r$segs
}))
ref_heads <- do.call(rbind, lapply(params_to_vary, function(pv) {
r <- vary_angle(pv$p, pv$v, pv$l)
r$heads$group_param <- pv$p
r$heads
}))
ref_segs$colour <- part_colours[ref_segs$part]
ref_heads$colour <- part_colours["head"]
# highlighted part per param group
highlight_map <- c(
angleofspine = "#377eb8",
anglerighthumerus = "#ff7f00",
anglerightleg = "#984ea3",
anglelefthumerus = "#4daf4a"
)
ref_segs$colour <- ifelse(
(ref_segs$part == "spine" & ref_segs$group_param == "angleofspine") |
(ref_segs$part == "rarm1" & ref_segs$group_param == "anglerighthumerus") |
(ref_segs$part == "rarm2" & ref_segs$group_param == "anglerighthumerus") |
(ref_segs$part == "rleg" & ref_segs$group_param == "anglerightleg") |
(ref_segs$part == "larm1" & ref_segs$group_param == "anglelefthumerus") |
(ref_segs$part == "larm2" & ref_segs$group_param == "anglelefthumerus"),
ref_segs$colour,
"grey70"
)
# Facet label: combine group_param + pose
ref_segs$facet_label <- paste0(ref_segs$group_param, "\n", ref_segs$pose)
ref_heads$facet_label <- paste0(ref_heads$group_param, "\n", ref_heads$pose)
# Build ordered factor across all 16 facets
facet_order <- unlist(lapply(params_to_vary, function(pv)
paste0(pv$p, "\n", pv$l)
))
ref_segs$facet_label <- factor(ref_segs$facet_label, levels = facet_order)
ref_heads$facet_label <- factor(ref_heads$facet_label, levels = facet_order)
set.seed(7)
p_ref <- ggplot() +
geom_xkcdpath(
aes(x = x, y = y, xend = xend, yend = yend, colour = colour),
data = ref_segs,
linewidth = 1.2, mask = FALSE,
xjitteramount = 0.03, yjitteramount = 0.03,
inherit.aes = FALSE
) +
geom_xkcdpath(
aes(x = x, y = y, diameter = diameter),
data = ref_heads,
linewidth = 1.2, colour = "#e41a1c", ratioxy = 1, mask = FALSE,
inherit.aes = FALSE
) +
scale_colour_identity() +
facet_wrap(~ facet_label, ncol = 4) +
coord_fixed(xlim = xr, ylim = yr) +
theme_xkcd() +
theme(
axis.text = element_blank(),
axis.ticks = element_blank(),
axis.title = element_blank(),
strip.text = element_text(family = "xkcd", size = 9)
) +
labs(title = "Angle reference: one parameter varied per row")
p_refangleofspine -pi/2 upright | 0 horizontal | pi/2 upside down
angleofneck match angleofspine for natural look
anglerighthumerus -pi/6 hanging | 0 right | pi/2 raised
anglerightradius follow anglerighthumerus + pi/6 for natural elbow bend
anglelefthumerus mirror of right: -pi/2 - anglerighthumerus
angleleftradius mirror of right
anglerightleg 3*pi/2 - pi/12 (slight outward spread)
angleleftleg 3*pi/2 + pi/12 (slight outward spread)
scale ~10-15% of diff(yrange)
ratioxy diff(xrange) / diff(yrange) -- always set this!
These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.