Swarm Art in the Browser
This article shows how ggWebGL can render swarm art in
the browser from renderer-neutral boids4R simulations. The
visual idea is generative rather than statistical: compact schools,
predator avoidance, obstacle corridors, murmurations, and mixed-species
flocks become animated point/vector timelines that can be panned,
zoomed, scrubbed, and inspected in WebGL.
The examples are deliberately browser-native. boids4R
computes the flocking frames, and ggWebGL turns those
frames into animated primitives with shader and camera controls. The
same handoff can be used for gallery pieces, teaching demonstrations, or
stress tests of timeline rendering.
Boundary
The ownership split is deliberately narrow:
-
boids4Rowns simulation semantics: flocking state, rules, scenarios, and frame export -
boids4R::as_ggwebgl_spec()translates simulation frames into renderer primitives -
ggWebGLowns WebGL rendering: the htmlwidget, point and vector primitives, shader choice, timeline controls, hover, pan, zoom, and 3D orbit interaction
boids4R is a suggested package for ggWebGL.
When it is unavailable, this vignette still builds and reports that the
live animation widgets were skipped.
Scenario Gallery: Swarm Art Motifs
These examples mirror the boids4R scenario gallery:
compact schools, obstacle corridors, predator avoidance, murmurations,
and mixed-species 3D flocks. Each widget is built from a
renderer-neutral boids_simulation object and animated by
the ggWebGL timeline.
if (!boids4r_available) {
cat("Scenario gallery widgets skipped.\n")
} else {
scenario_gallery <- list(
schooling_2d = boids4R::boids_scenario(
"schooling_2d",
n = 180L,
steps = 80L,
record_every = 2L,
seed = 111L
),
obstacle_corridor_2d = boids4R::boids_scenario(
"obstacle_corridor_2d",
n = 160L,
steps = 90L,
record_every = 2L,
seed = 112L
),
predator_avoidance_2d = boids4R::boids_scenario(
"predator_avoidance_2d",
n = 170L,
steps = 90L,
record_every = 2L,
seed = 113L
),
murmuration_3d = boids4R::boids_scenario(
"murmuration_3d",
n = 240L,
steps = 90L,
record_every = 3L,
seed = 114L
),
mixed_species_3d = boids4R::boids_scenario(
"mixed_species_3d",
n = 210L,
steps = 90L,
record_every = 3L,
seed = 115L
)
)
scenario_widgets <- lapply(names(scenario_gallery), function(name) {
sim <- scenario_gallery[[name]]
spec <- boids4R::as_ggwebgl_spec(
sim,
vector_every = if (identical(sim$dimension, "3d")) 16L else 12L,
vector_scale = if (identical(sim$dimension, "3d")) 0.11 else 0.13,
shader = "density_splat"
)
spec$labels$title <- paste("boids4R", name)
spec$render$timeline$autoplay <- TRUE
spec$render$timeline$loop <- TRUE
spec$render$timeline$speed <- 1.4
ggWebGL::ggWebGL(spec, height = if (identical(sim$dimension, "3d")) 540 else 500)
})
names(scenario_widgets) <- names(scenario_gallery)
}Schooling 2D
if (!boids4r_available) {
cat("Schooling widget skipped.\n")
} else {
scenario_widgets$schooling_2d
}Obstacle Corridor 2D
if (!boids4r_available) {
cat("Obstacle corridor widget skipped.\n")
} else {
scenario_widgets$obstacle_corridor_2d
}Predator Avoidance 2D
if (!boids4r_available) {
cat("Predator avoidance widget skipped.\n")
} else {
scenario_widgets$predator_avoidance_2d
}Murmuration 3D
if (!boids4r_available) {
cat("Murmuration widget skipped.\n")
} else {
scenario_widgets$murmuration_3d
}Mixed Species 3D
if (!boids4r_available) {
cat("Mixed-species widget skipped.\n")
} else {
scenario_widgets$mixed_species_3d
}Custom Workflow Animations
The custom workflow builds a corridor from low-level
boids4R constructors, then compares a baseline run with a
stronger obstacle/predator avoidance run.
if (!boids4r_available) {
cat("Custom workflow widgets skipped.\n")
} else {
bounds <- matrix(
c(-2.4, -1.35, 2.4, 1.35),
ncol = 2,
dimnames = list(c("x", "y"), c("min", "max"))
)
n_school <- 96L
n_scout <- 32L
n_boids <- n_school + n_scout
school_axis <- seq(0, 1, length.out = n_school)
scout_axis <- seq(0, 1, length.out = n_scout)
positions <- rbind(
cbind(-2.18 + 0.83 * school_axis, -0.70 + 0.95 * abs(sin(pi * school_axis))),
cbind(-2.22 + 0.77 * scout_axis, 0.28 + 0.64 * abs(cos(pi * scout_axis)))
)
velocity_phase <- seq(0, 2 * pi, length.out = n_boids)
velocities <- cbind(
0.35 + 0.20 * cos(velocity_phase),
0.08 * sin(velocity_phase)
)
custom_state <- boids4R::boids_state(
n_boids,
"2d",
bounds = bounds,
positions = positions,
velocities = velocities,
species = c(rep("school", n_school), rep("scout", n_scout))
)
custom_world <- boids4R::boids_world(
"2d",
bounds = bounds,
boundary = "reflect",
obstacles = data.frame(
x = c(-0.82, -0.05, 0.72),
y = c(0.42, -0.36, 0.48),
radius = c(0.30, 0.36, 0.31)
),
predators = data.frame(
x = -0.25,
y = 0.92,
radius = 0.58,
strength = 1.2
),
attractors = data.frame(
x = 2.08,
y = -0.86,
strength = 0.95
)
)
baseline_params <- boids4R::boids_params(
"2d",
separation_weight = 1.35,
alignment_weight = 0.94,
cohesion_weight = 0.62,
obstacle_weight = 2.5,
predator_weight = 2.3,
goal_weight = 0.16,
max_speed = 1.18,
max_force = 0.12,
noise = 0.001
)
avoidance_params <- boids4R::boids_params(
"2d",
separation_weight = 1.35,
alignment_weight = 0.94,
cohesion_weight = 0.62,
obstacle_weight = 2.8,
predator_weight = 3.2,
goal_weight = 0.20,
max_speed = 1.18,
max_force = 0.12,
noise = 0.001
)
custom_runs <- list(
baseline = boids4R::simulate_boids(
custom_state,
custom_world,
baseline_params,
steps = 95L,
record_every = 2L,
seed = 221L
),
stronger_avoidance = boids4R::simulate_boids(
custom_state,
custom_world,
avoidance_params,
steps = 95L,
record_every = 2L,
seed = 222L
)
)
custom_widgets <- lapply(names(custom_runs), function(name) {
spec <- boids4R::as_ggwebgl_spec(
custom_runs[[name]],
vector_every = 10L,
vector_scale = 0.14,
shader = "density_splat"
)
spec$labels$title <- paste("boids4R custom corridor:", name)
spec$render$timeline$autoplay <- TRUE
spec$render$timeline$loop <- TRUE
spec$render$timeline$speed <- 1.5
ggWebGL::ggWebGL(spec, height = 500)
})
names(custom_widgets) <- names(custom_runs)
}Baseline Corridor
if (!boids4r_available) {
cat("Baseline corridor widget skipped.\n")
} else {
custom_widgets$baseline
}Stronger Avoidance Corridor
if (!boids4r_available) {
cat("Stronger avoidance corridor widget skipped.\n")
} else {
custom_widgets$stronger_avoidance
}Regeneration
The same pattern can be used from an installed package:
sim <- boids4R::boids_scenario("mixed_species_3d", n = 210, steps = 90, seed = 115)
spec <- boids4R::as_ggwebgl_spec(sim, vector_every = 16, shader = "density_splat")
spec$render$timeline$autoplay <- TRUE
ggWebGL::ggWebGL(spec, height = 540)