diff --git a/bunny.cpp b/bunny.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..cd7a7e9598b8d2cbb3c384d2b28be2256e301ed9
--- /dev/null
+++ b/bunny.cpp
@@ -0,0 +1,264 @@
+#include "common/sketchbook.hpp"
+using namespace common;
+
+struct vertex
+{
+	float2 origin;
+	float2 normal;
+};
+
+struct polygon
+{
+	std::vector<vertex> vertices;
+	explicit operator range2f()
+	{
+		constexpr auto infinity = float2::one(std::numeric_limits<float>::infinity());
+		range2f ret {infinity, -infinity};
+		for(auto&& v : vertices)
+		{
+			ret.lower().min(v.origin);
+			ret.upper().max(v.origin);
+		}
+		return ret;
+	}
+};
+
+
+void update_normals(polygon& p)
+{
+	if(p.vertices.begin() == p.vertices.end())
+		return;
+
+	auto current = p.vertices.begin();
+	auto previous = p.vertices.end()-1;
+
+	do
+	{
+		auto direction = current->origin - previous->origin;
+		previous->normal = rotate(direction, protractor<>::tau(1.f/4));
+		previous = current++;
+	}
+	while(current != p.vertices.end());
+}
+
+bool convex_contains(polygon polygon, float2 point)
+{
+	return support::all_of(polygon.vertices, [&point](auto vertex)
+	{
+		return vertex.normal(point - vertex.origin) > 0;
+	});
+}
+
+void lerplerp_lerp(range<vertex*> buffer)
+{
+	auto start = (buffer.begin()+0)->origin;
+	auto middle = (buffer.begin()+1)->origin;
+	auto end = (buffer.begin()+2)->origin;
+	auto step = 1.f / (buffer.end() - buffer.begin());
+	auto ratio = 0.f;
+	for(auto&& vertex : buffer)
+	{
+		vertex.origin = lerp
+		(
+			lerp(start, middle, ratio),
+			lerp(middle, end, ratio),
+			ratio
+		), float2{};
+		ratio += step;
+	}
+}
+
+struct circle
+{
+	float2 center;
+	float radius;
+	explicit operator range2f()
+	{
+		return {center - radius, center + radius};
+	}
+};
+
+bool contain(range<circle*> circles, float2 point)
+{
+	for(auto [center, radius] : circles)
+		if(quadrance(center - point) < radius*radius)
+			return true;
+	return false;
+}
+
+polygon make_nose(float2 aspect)
+{
+	const std::array<vertex, 3> nose_control
+	{{
+		{aspect * float2(0.5f, 3.5f/4) + float2(-0.20f, -0.20f)},
+		{aspect * float2(0.5f, 3.5f/4) + float2(+0.20f, -0.20f)},
+		// {aspect * float2(0.5f, 3.f/4) + float2(+0.00f, +0.20f)},
+		{aspect * float2(0.5f, 3.5f/4) + float2(+0.00f, +0.00f)},
+	}};
+
+	polygon nose{};
+	for(size_t i = 0; i < nose_control.size(); ++i)
+	{
+		using support::wrap;
+		auto& curr = nose_control[i].origin;
+		auto& next = nose_control[wrap(i+1, nose_control.size())].origin;
+		auto& prev = nose_control[wrap(i-1, nose_control.size())].origin;
+		auto curve_control_prev = lerp(curr, prev, 1.f/2);
+		auto curve_control_curr = curr;
+		auto curve_control_next = lerp(curr, next, 1.f/2);
+		auto length = std::max(
+			geom::length(curve_control_curr - curve_control_prev),
+			geom::length(curve_control_curr - curve_control_next)
+		);
+		auto curve_vertex_count = std::max(40.f/length,3.f); // welp magic number 40
+		auto& vertices = nose.vertices;
+		auto start = vertices.size();
+		vertices.resize(start + curve_vertex_count);
+		vertices[start+0] = vertex{curve_control_prev};
+		vertices[start+1] = vertex{curve_control_curr};
+		vertices[start+2] = vertex{curve_control_next};
+		lerplerp_lerp({
+			vertices.data() + start,
+			vertices.data() + vertices.size()
+		});
+	}
+	update_normals(nose);
+	return nose;
+};
+
+void some_fur(frame& frame, rgb_pixel color, range<circle*> eyes, const polygon& nose)
+{
+	const auto aspect = frame.size/frame.size.x();
+	const auto fur_length = support::average(frame.size.x(),frame.size.y())*12/400;
+	auto fur = frame.begin_sketch();
+
+	for(int i = 0; i < (5000 * frame.size.x() / 400); ++i)
+	{
+		float2 root;
+		do
+			root = trand_float2() * aspect;
+		while(contain(eyes, root) || convex_contains(nose, root));
+		root *= frame.size.x();
+		auto angle = trand_float();
+
+		auto stem = rotate_scale(trand_float2() * fur_length,
+			protractor<>::tau(angle < 1 ? angle : 0));
+
+		fur.line(root, root + stem);
+	}
+	fur.outline(color);
+};
+
+const unsigned fur_seed = trand_int();
+
+std::array<circle,2> eyes;
+polygon nose;
+range2f nose_bounds;
+
+const framebuffer * furbuffer;
+
+using poke_motion = motion<float2, quadratic_curve>;
+symphony<poke_motion, poke_motion> poke;
+
+void start(Program& program)
+{
+	// program.size = int2{200,400};
+	// program.size = int2{300,400};
+	// program.size = int2{200,400}.mix<1,0>();
+	// program.size = int2{400,400};
+	program.fullscreen = true;
+	furbuffer = &program.request_framebuffer(
+		program.fullscreen ? program.display.size : program.size,
+		[](auto frame)
+		{
+			auto aspect = frame.size/frame.size.x();
+			auto radius = aspect.x()/10;
+			eyes = std::array<circle,2>
+			{{
+				{aspect/4, radius},
+				{aspect/float2(4.f/3, 4.f), radius},
+			}};
+			nose = make_nose(aspect);
+			nose_bounds = range2f(nose) * frame.size.x();
+			some_fur(frame, 0xbbbbbb_rgb, make_range(eyes), nose);
+			some_fur(frame, 0xcccccc_rgb, make_range(eyes), nose);
+			some_fur(frame, 0xdddddd_rgb, make_range(eyes), nose);
+			some_fur(frame, 0x777777_rgb, make_range(eyes), nose);
+			some_fur(frame, 0x888888_rgb, make_range(eyes), nose);
+		}
+	);
+
+	program.draw_loop = [](auto frame, auto delta)
+	{
+		tiny_rand.seed({fur_seed, 13});
+		frame.begin_sketch()
+			.rectangle(rect{frame.size})
+			.fill(0xaaaaaa_rgb)
+		;
+
+		for(auto&& eye : eyes)
+		{
+			auto blink = eye;
+			blink.radius /= 2;
+			blink.center -= blink.radius/2;
+			frame.begin_sketch()
+				.ellipse(range2f(eye) * frame.size.x())
+				.fill(paint::radial_gradient(
+					range2f(blink) * frame.size.x(),
+					{0.f, 1.f},
+					{
+						rgba_vector(0xffffff_rgb),
+						rgba_vector(0x000000_rgb)
+					}
+				))
+			;
+		}
+
+		float2 poke_value;
+		poke.move(poke_value, delta);
+
+		{ auto nose_sketch = frame.begin_sketch();
+			nose_sketch.move(nose.vertices.front().origin * frame.size.x() + poke_value);
+			for(size_t i = 1; i < nose.vertices.size(); ++i)
+				nose_sketch.vertex(nose.vertices[i].origin * frame.size.x() + poke_value);
+
+			auto size = nose_bounds.upper() - nose_bounds.lower();
+			nose_sketch.fill(paint::radial_gradient(
+				range2f{
+					nose_bounds.lower() + size*0.2,
+					nose_bounds.lower() + size*0.5
+				} + poke_value,
+				{0.f, 1.f},
+				{
+					rgba_vector(0xffffff_rgb),
+					rgba_vector(0x000000_rgb)
+				}
+			));
+		}
+
+		frame.begin_sketch()
+			.rectangle(rect2f{frame.size})
+			.fill(furbuffer->paint())
+		;
+	};
+
+	program.mouse_down = [&program](auto position, auto)
+	{
+		auto nose_half = (nose_bounds.upper() - nose_bounds.lower())/2;
+		auto offset = (nose_bounds.lower() + nose_half) - position;
+		if(abs(offset) < nose_half)
+		{
+			program.request_wave({[poke_ratio = nose_half.length()/offset.length()](auto ratio)
+			{
+				constexpr float fade_in = 2;
+				constexpr float fade_out = 2;
+				float fade = std::min(ratio*fade_in, (1-ratio)*fade_out);
+				return lerp(0.f,std::sin(ratio * 170 * poke_ratio), std::min(fade, 1.f));
+			}, 100ms}, 0);
+			poke = symphony(
+				poke_motion{float2::zero(), offset/2, 100ms},
+				poke_motion{offset/2, float2::zero(), 100ms}
+			);
+		}
+	};
+}