Skip to content

Instantly share code, notes, and snippets.

@XaydBayeck
Created October 16, 2020 07:22
Show Gist options
  • Save XaydBayeck/7157e81228059e741ac567660e6f4056 to your computer and use it in GitHub Desktop.
Save XaydBayeck/7157e81228059e741ac567660e6f4056 to your computer and use it in GitHub Desktop.
Luxor.jl 的简易介绍
### A Pluto.jl notebook ###
# v0.12.4
using Markdown
using InteractiveUtils
# This Pluto notebook uses @bind for interactivity. When running this notebook outside of Pluto, the following 'mock version' of @bind gives bound variables a default value (instead of an error).
macro bind(def, element)
quote
local el = $(esc(element))
global $(esc(def)) = Core.applicable(Base.get, el) ? Base.get(el) : missing
el
end
end
# ╔═╡ 82eb4e5c-0df9-11eb-2deb-f9cf18d8b297
using Luxor, PlutoUI
# ╔═╡ e17fc550-0f7d-11eb-1b4d-cb7ca447909a
begin
using Pkg
Pkg.add("Colors")
using Colors
end
# ╔═╡ 90b8a2dc-0e05-11eb-2a89-810a27dc9857
md"## 第一步"
# ╔═╡ a951c40e-0e05-11eb-320c-5724a36835cd
md"使用 `Luxor.jl` 的宏 `@png`、`@svg`、`@pdf` 来开始绘制图形。如果是在 `Atom`、`VSCode` 以及`Pluto.jl`、`Jyputer notebook` 中时使用 `@draw` 来生成图片但不保存文件而是直接显示在软件中。"
# ╔═╡ 532e7c92-0dfc-11eb-2779-6d3b751429ec
md"""
radius = $(@bind radius_s Slider(1:300))
action =
$(begin
actions = ["stroke"=>:stroke, "fill"=>:fill, "clip"=>:clip, "fill stroke"=>:fillstroke]
@bind action MultiSelect(actions)
end)
x = $(@bind x_r Slider(-100:0.1:100))
y = $(@bind y_r Slider(-100:0.1:100))
"""
# ╔═╡ bcdf4880-0dfa-11eb-1268-27fd60dcbb53
md"""
这个Luxor的例子中说明了以下几点:
- 有些默认值你可以不用设置(如 文件名,颜色,字体大小······)
- 绘画时的坐标定位由`Point`类型决定,有时你可以省略它
- 文字会被默认绘制到原点(0, 0),而且默认左对齐
- 有一些`action`可选,如`:stroke`、`:fill`、`:clip`、`:fillstroke`
- 第一次绘图需要花费一些时间?Cario绘图引擎需要预热,当它启动就会变得很快
"""
# ╔═╡ 3b2d2f3c-0dfa-11eb-0904-295fce297e31
@draw begin
text("Hello again, world!", Point(0,250))
circle(Point(x_r, y_r), radius_s, Dict(actions)[action[1]])
rulers()
end
# ╔═╡ 8be0eb68-0dfb-11eb-3341-3331f49dfa43
md"## 欧几里德鸡蛋"
# ╔═╡ b6223b3e-0e00-11eb-22b5-21743237aa43
@bind radius Slider(20:100)
# ╔═╡ e305681c-0e08-11eb-381b-33334609808b
md"
对于本教程的主要部分,我们将尝试绘制欧几里德的蛋,这涉及到一些几何学。
首先定义变量`radius` = $radius
"
# ╔═╡ c5a7d758-0e03-11eb-024b-6949df9a3a49
@draw begin
# 设置为灰色虚线
setdash("dot")
sethue("gray30")
# 取圆心的水平直径上的俩个端点。
A, B = [Point(x, 0) for x in [-radius, radius]]
# `O` 为圆心
O = Point(0, 0)
# 绘制一条从 `A` 到 `B` 的直线。
line(A, B, :stroke)
# 绘制圆
circle(O, radius, :stroke)
text("radius = $radius", Point(-20, -10))
end
# ╔═╡ 459a1b36-0e01-11eb-1174-735eeed8c031
md"## 标记和点"
# ╔═╡ 32a5fe06-0e0a-11eb-399e-834bb3920b1d
md"
在集合图形上绘制一些点并标记它会是一个好主意。点可以用很小的圆形来代表,标记可以使用`label()`函数来绘
制,使用指南针的角度和点来表示标记文字会在点的哪个方位。比如`:N`意味着标记文字在点的北方。
"
# ╔═╡ 543db472-0e01-11eb-02df-116866428a08
@draw begin
setdash("dot")
sethue("gray30")
line(A, B, :stroke)
circle(O, radius, :stroke)
# >>>>
# :NW 意味着 西北方
label("A", :NW, A)
label("O", :N, O)
# :NE 意味着 东北方
label("B", :NE, B)
# 绘制 点
circle.([A, O, B], 2, :fill)
# 以 A, B 为圆心绘制俩个圆
circle.([A, B], 2radius, :stroke)
end
# ╔═╡ 206c7660-0e0b-11eb-1a46-3b08305d6cce
md"
可以使用`julia`的`.`操作符将`circle()`函数广播到一个向量上去, 注意到我们并没有把`radius`和
`action`俩个参数写成向量。
"
# ╔═╡ a44969f4-0e04-11eb-3519-e9812b89f6d8
md"## 相交点"
# ╔═╡ b51489ce-0e0b-11eb-1893-1bc7a1148bac
md"
接下来我们要绘制俩个圆相交的点。`Luxor.jl`有一个函数叫`intersectionlinecircle()`可以找到一条直线
与圆相交的点。我们可以过O点作一条假象的垂线,然后用它找到这个垂线与A点为圆心的圆相交的俩个点。
"
# ╔═╡ 635967c8-0e04-11eb-1b1e-03936aefab89
@draw begin
setdash("dot")
sethue("gray30")
line(A, B, :stroke)
circle(O, radius, :stroke)
# >>>>
label("A", :NW, A)
label("O", :N, O)
label("B", :NE, B)
circle.([A, O, B], 2, :fill)
circle.([A, B], 2radius, :stroke)
# >>>>
nints, C, D =
intersectionlinecircle(Point(0, -2radius), Point(0, 2radius), A, 2radius)
# 当有俩个交点
if nints == 2
circle.([C, D], 2, :fill)
label.(["D", "C"], :N, [D, C])
end
end
# ╔═╡ 6bbab590-0e0c-11eb-2206-bbb05edf251c
md"`intersectionlinecircle()`需要四个参数,俩个点用来定义直线,一个点和半径的组合用来定义圆。函数会
返回交点和交点数(可能为0,1或者2)"
# ╔═╡ b19ce052-0e04-11eb-36c1-fb9753e64bc0
md"## The upper circle"
# ╔═╡ 2051cc5a-0e0d-11eb-018b-db5c7989725a
md"
继续用`intersectionlinecircle()`来找到过D、O俩点的垂线与O点为圆心的圆的交点。
分别过A,C1和B,C1做俩条直线找到俩条直线分别与圆A,圆B的四个交点。
用`distance()`函数计算俩个点的距离,从四个点中选取离C1距离更近的俩个点,然后以C1为圆心,C1到俩个点的距
离为半径作一个圆。
"
# ╔═╡ 289bbdc0-0e04-11eb-087a-9de6bda61dd4
@draw begin
setdash("dot")
sethue("gray30")
line(A, B, :stroke)
circle(O, radius, :stroke)
# >>>>
label("A", :NW, A)
label("O", :N, O)
label("B", :NE, B)
circle.([A, O, B], 2, :fill)
circle.([A, B], 2radius, :stroke)
# >>>>
if nints == 2
circle.([C, D], 2, :fill)
label.(["D", "C"], :N, [D, C])
end
# >>>>
# 过O,D的直线与圆O的交点
nints1, C1, C2 = intersectionlinecircle(O, D, O, radius)
if nints1 == 2
circle(C1, 3, :fill)
label("C1", :N, C1)
end
# 过A,C1的直线与圆A的交点
nints2, I3, I4 = intersectionlinecircle(A, C1, A, 2radius)
# 过B,C1的直线与圆B的交点
nints3, I1, I2 = intersectionlinecircle(B, C1, B, 2radius)
circle.([I1, I2, I3, I4], 2, :fill)
# >>>>
# 选择跟C1距离更近的俩个点
if distance(C1, I1) < distance(C1, I2)
ip1 = I1
else
ip1 = I2
end
if distance(C1, I3) < distance(C1, I4)
ip2 = I3
else
ip2 = I4
end
label("ip1", :N, ip1)
label("ip2", :N, ip2)
circle(C1, distance(C1, ip1), :stroke)
end
# ╔═╡ bbe266d0-0e04-11eb-1a54-d7b7d79dc44a
md"## Eggs at the ready"
# ╔═╡ 69be77c0-0e0e-11eb-2bff-2fcb86c603d8
md"
我们现在已经得到了画一个鸡蛋所需要的全部点和圆弧。用数次`arc2r()`函数绘制边界,这个函数需要一个中心点和
俩个点来定义一个圆弧,以及一个`action`。
鸡蛋的形状由四段曲线组成,所以我们将使用`:path`动作。而不是像`:fill`或`:stroke`那样立刻绘制图形,此操
作为当前节点添加路径。
"
# ╔═╡ caa71eb6-0e04-11eb-396e-3b7ca60cf0cb
@draw begin
setdash("dot")
sethue("gray30")
line(A, B, :stroke)
circle(O, radius, :stroke)
# >>>>
label("A", :NW, A)
label("O", :N, O)
label("B", :NE, B)
circle.([A, O, B], 2, :fill)
circle.([A, B], 2radius, :stroke)
# >>>>
if nints == 2
circle.([C, D], 2, :fill)
label.(["D", "C"], :N, [D, C])
end
# >>>>
if nints1 == 2
circle(C1, 3, :fill)
label("C1", :N, C1)
end
circle.([I1, I2, I3, I4], 2, :fill)
# >>>>
label("ip1", :N, ip1)
label("ip2", :N, ip2)
circle(C1, distance(C1, ip1), :stroke)
# >>>>
# 设置为实线
setline(5)
setdash("solid")
arc2r(B, A, ip1, :path) # centered at B, from A to ip1
arc2r(C1, ip1, ip2, :path)
arc2r(A, ip2, B, :path)
arc2r(O, B, A, :path)
# 保留路径
strokepreserve()
# 设置透明和颜色
setopacity(0.8)
sethue("ivory")
# 填充路径
fillpath()
end
# ╔═╡ ac502290-0e0f-11eb-1dbc-99a689b585d4
md"最后,一旦我们将所有四个部分添加到路径中,我们就可以描边并填充它。如果要对描边和填充使用单独的样式,可以使用第一个操作的保留版本。这将应用该操作,但保留路径可用于更多操作。"
# ╔═╡ ca0d8160-0e0f-11eb-145d-112c53ceb5cb
md"
## 鸡蛋画笔
为了更方便的使用,我们可以把上面的代码封装为函数
"
# ╔═╡ d85c0104-0e0f-11eb-1103-eb20ff0bd40c
function egg(radius, action=:none)
A, B = [Point(x, 0) for x in [-radius, radius]]
nints, C, D =
intersectionlinecircle(Point(0, -2radius), Point(0, 2radius), A, 2radius)
flag, C1 = intersectionlinecircle(C, D, O, radius)
nints, I3, I4 = intersectionlinecircle(A, C1, A, 2radius)
nints, I1, I2 = intersectionlinecircle(B, C1, B, 2radius)
if distance(C1, I1) < distance(C1, I2)
ip1 = I1
else
ip1 = I2
end
if distance(C1, I3) < distance(C1, I4)
ip2 = I3
else
ip2 = I4
end
newpath()
arc2r(B, A, ip1, :path)
arc2r(C1, ip1, ip2, :path)
arc2r(A, ip2, B, :path)
arc2r(O, B, A, :path)
closepath()
do_action(action)
end
# ╔═╡ 788f1e60-0e10-11eb-05c0-73638136ceea
md"
这样可以安全的隐藏中间代码。现在可以通过`egg(100, :stroke)`使用`:stroke`动作来绘制一个半径为
100的鸡蛋
(当然,这里没有任何错误检查。如果要用在真正的应用中就必须加上错误处理)
注意到该函数没有定义颜色和定位,它会继承当前绘图环境的比例、旋转、原点位置、线厚、颜色、样式等。
"
# ╔═╡ 7e155de2-0f76-11eb-3084-e7036eeb571d
md"
egg radius : $(@bind egg_radius Slider(-100:-20))
radiusx : $(@bind radiusx Slider(-100:100))
radiusy : $(@bind radiusy Slider(-200:-50))
"
# ╔═╡ 625c0d14-0e11-11eb-2798-a362c0d4d0bc
@draw begin
setopacity(0.7)
for θ in range(0, step=π/6, length=12)
@layer begin
rotate(θ)
translate(radiusx, radiusy)
egg(egg_radius, :path)
setline(10)
# 随机颜色填充鸡蛋
randomhue()
fillpreserve()
# 随机颜色填充鸡蛋边框
randomhue()
strokepath()
end
end
text("egg raius=$egg_radius", Point(-40, (-radiusy - 1.5egg_radius)))
text("radius=($radiusx, $radiusy)", Point(40, (-radiusy - 1.5egg_radius)))
end
# ╔═╡ 05f21006-0f73-11eb-2750-ab0db9828be4
md"
`θ` 从 `0` 每次增加 `π/6` 循环 12 次。但是每次绘制鸡蛋前,整个绘图环境会先选择 `θ` 个弧度,然后沿
着y轴移动-150个单位(y 轴值通常向下增加,因此,在发生任何旋转之前,-150 的移位看起来像向上移位)。
`randomhue()` 函数可以设置随机的色调,`fillpreserve()` 函数使用当前设定填充路径。
注意到单次的绘制步骤被封装到了一个 `@layer begin...end` 的壳子里面。这个壳子里面的绘图环境设定会在离
开 `end` 之后立刻被废弃掉。这能允许我们临时更改绘图环境设定,并在绘制一次后立刻废弃这些临时的设定。
旋转和角度通常在弧度中指定。正 x 轴(来自原点以 x 中增加的线)从原点向东开始,Y 轴从南方向开始,正角是顺时针(即将从正 x 轴向正 y 轴)。因此,在上一示例中的第二个鸡蛋是在轴由轴顺时针旋转π/6 弧度之后绘制的。
如果你仔细观察,你可以分辨出哪个鸡蛋是先画的——它每边都与随后的鸡蛋重叠。
### 思想实验
1. 如果转换是 `translate(0, 150)` ,而不是 `translater(0,-150)` ,会发生什么?
2. 如果转换是 `translate(150, 0)` ,而不是 `translater(0,-150)` ,会发生什么?
3. 如果在旋转绘图环境之前转移每个鸡蛋,会发生什么?
一些有用的工具,用于调查坐标和变换的重要方面包括:
- `rulers()` 绘制当前 x 轴和 y 轴 获取旋转
- `getrotation()` 获得当前旋转值
- `getscale()` 获得当前刻度
"
# ╔═╡ 51b1ce5e-0f79-11eb-2887-1d19164b2d8d
md"
## 聚合蛋
除了描边和填充操作,还可以将路径用作剪切区域(`:clip`) ,或用作更多形状移动的基础。
"
# ╔═╡ bc551710-0f7a-11eb-0788-2feb4fadbb96
@draw begin
setlinejoin("round")
egg(160, :path)
pgon = first(pathtopoly())
pc = polycentroid(pgon)
circle(pc, 5, :fill)
for pt in 1:2:length(pgon)
pgon[pt] = between(pc, pgon[pt], 0.5)
end
randomhue()
poly(pgon, :stroke)
end
# ╔═╡ 8f8b2d32-0f7a-11eb-00de-975994fcfeee
md"
`setlinejoin()` 设置俩条直线连接处的风格。
`egg()` 函数创建一个路径,并允许您对它应用一个操作。 也可以将路径转换为多边形(点数组),这样您就可使用该路径做更多操作。 以上代码将蛋的路径转换为多边形,然后将多边形的每个其他点向质心移动一半。
`pathtopoly()` 函数将 `egg (160, :p ath)` 的当前路径转换为多边形。这些平滑曲线被一系列直线段近似。使用`first()`函数是因为 `pathtopoly()` 返回一个或多个多边形的数组(路径可以由一系列循环组成),我们知道此处只需要单个路径。
`polycentroid()` 找到新多边形的质心
最后的循环遍历每一个奇数点并将其移动到距离质心一半距离的地方。 `between()` 函数用来寻找俩个确定的点之间的某个点。最后 `poly()` 函数将点数组绘制出来。
对于 `egg()` 函数最后的测试, 这是 Luxor 的 `offsetpoly()` 函数, 可以蛋为基础围绕它绘制高低不平的
多边形。
"
# ╔═╡ 18ce2dba-0f7d-11eb-2703-43e2eee33afb
@draw begin
egg(80, :path)
pgons = first(pathtopoly()) |> unique
pcs = polycentroid(pgons)
for pt in 1:2:length(pgons)
pgons[pt] = between(pcs, pgons[pt], 0.8)
end
for i in 30:-3:-8
randomhue()
ops = offsetpoly(pgons, i)
poly(ops, :stroke, close=true)
end
end
# ╔═╡ 89080f22-0f7d-11eb-21ee-11b84c39368a
md"路径到多边形转换所创建的点的规律性上的小变化及其创建的采样数不同,使得它的边不断扩大"
# ╔═╡ a4cd4e0a-0f7d-11eb-1927-bb8329f0af16
md"
## 蒙板
Luxor 的一个有用功能是,您可以使用形状作为剪裁蒙版。当图形偏离蒙版边界时,图形可能会隐藏。
如下例子,蛋被作为路径但是没有被绘制。您现在绘制的每个图形形状都夹在穿过蒙版的地方。这由 `:clip` 操作指定,该操作传递给 `egg()` 末尾的 `doaction()` 函数。
\图形由 `ngon()` 函数提供,该函数绘制常规的 n 面多边形。
"
# ╔═╡ c592caa4-0f7d-11eb-1629-c716b9563964
@draw begin
setopacity(0.5)
eg(a) = egg(150, a)
sethue("gold")
eg(:fill)
eg(:clip)
@layer begin
for i in 360:-4:1
sethue(Colors.HSV(i, 1.0, 0.8))
rotate(π/30)
ngon(O, i, 5, 0, :stroke)
end
end
clipreset()
sethue("red")
eg(:stroke)
end
# ╔═╡ 8457883a-0f7e-11eb-2a50-d587303ea173
md"
在剪辑完成后添加匹配的 `clipreset()` 是一种好的做法。不平衡的剪裁会导致不可预知的结果。
祝你的探索好运!
"
# ╔═╡ Cell order:
# ╟─90b8a2dc-0e05-11eb-2a89-810a27dc9857
# ╠═82eb4e5c-0df9-11eb-2deb-f9cf18d8b297
# ╟─a951c40e-0e05-11eb-320c-5724a36835cd
# ╟─532e7c92-0dfc-11eb-2779-6d3b751429ec
# ╟─bcdf4880-0dfa-11eb-1268-27fd60dcbb53
# ╠═3b2d2f3c-0dfa-11eb-0904-295fce297e31
# ╟─8be0eb68-0dfb-11eb-3341-3331f49dfa43
# ╟─e305681c-0e08-11eb-381b-33334609808b
# ╠═b6223b3e-0e00-11eb-22b5-21743237aa43
# ╠═c5a7d758-0e03-11eb-024b-6949df9a3a49
# ╟─459a1b36-0e01-11eb-1174-735eeed8c031
# ╟─32a5fe06-0e0a-11eb-399e-834bb3920b1d
# ╠═543db472-0e01-11eb-02df-116866428a08
# ╟─206c7660-0e0b-11eb-1a46-3b08305d6cce
# ╟─a44969f4-0e04-11eb-3519-e9812b89f6d8
# ╟─b51489ce-0e0b-11eb-1893-1bc7a1148bac
# ╠═635967c8-0e04-11eb-1b1e-03936aefab89
# ╟─6bbab590-0e0c-11eb-2206-bbb05edf251c
# ╟─b19ce052-0e04-11eb-36c1-fb9753e64bc0
# ╟─2051cc5a-0e0d-11eb-018b-db5c7989725a
# ╠═289bbdc0-0e04-11eb-087a-9de6bda61dd4
# ╟─bbe266d0-0e04-11eb-1a54-d7b7d79dc44a
# ╟─69be77c0-0e0e-11eb-2bff-2fcb86c603d8
# ╠═caa71eb6-0e04-11eb-396e-3b7ca60cf0cb
# ╟─ac502290-0e0f-11eb-1dbc-99a689b585d4
# ╟─ca0d8160-0e0f-11eb-145d-112c53ceb5cb
# ╠═d85c0104-0e0f-11eb-1103-eb20ff0bd40c
# ╟─788f1e60-0e10-11eb-05c0-73638136ceea
# ╠═625c0d14-0e11-11eb-2798-a362c0d4d0bc
# ╟─7e155de2-0f76-11eb-3084-e7036eeb571d
# ╟─05f21006-0f73-11eb-2750-ab0db9828be4
# ╠═51b1ce5e-0f79-11eb-2887-1d19164b2d8d
# ╠═bc551710-0f7a-11eb-0788-2feb4fadbb96
# ╟─8f8b2d32-0f7a-11eb-00de-975994fcfeee
# ╠═18ce2dba-0f7d-11eb-2703-43e2eee33afb
# ╟─89080f22-0f7d-11eb-21ee-11b84c39368a
# ╟─a4cd4e0a-0f7d-11eb-1927-bb8329f0af16
# ╟─e17fc550-0f7d-11eb-1b4d-cb7ca447909a
# ╠═c592caa4-0f7d-11eb-1629-c716b9563964
# ╟─8457883a-0f7e-11eb-2a50-d587303ea173
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment