Created
October 16, 2020 07:22
-
-
Save XaydBayeck/7157e81228059e741ac567660e6f4056 to your computer and use it in GitHub Desktop.
Luxor.jl 的简易介绍
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
### 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