Last active
October 10, 2021 17:05
-
-
Save rgchris/6b33462e6894f431d8e09c349565aee5 to your computer and use it in GitHub Desktop.
Red SVG loader/converter
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
Red [ | |
Title: "SVG Tools" | |
Date: 27-Jan-2020 | |
Author: "Christopher Ross-Gill" | |
Rights: http://opensource.org/licenses/Apache-2.0 | |
Version: 0.3.2 | |
History: [ | |
0.3.2 27-Jan-2020 "Better handling of text whitespace; bold/italic" | |
0.3.1 24-Jan-2020 "PATH model rewrite; VIEW wrapper to view an SVG" | |
0.3.0 23-Jan-2020 "Reorganise PATH handling; render whole/partial object; further refactoring" | |
0.2.2 13-Sep-2019 "Some functions for manipulating paths; refactoring" | |
0.2.1 26-Aug-2019 "Set Stroke/Fill off by default; handle numbers with units; open paths" | |
0.2.0 25-Aug-2019 "Text support in TO-DRAW" | |
0.1.0 23-Dec-2018 "Rudimentary Shape Support" | |
] | |
Notes: { | |
v0.3.2 | |
There are still many ways in which this could be more efficient, but for now the focus is | |
merely having everything work. PUSH per-shape is expensive, and I'd much prefer to reuse | |
font objects. | |
There's still more functionality to figure out too: gradient fills, <use>, <textpath> to | |
name but a few. | |
Note: View/VID-related functions are now contained within the SVG/VID sub-object: | |
view svg/vid/quick-layout load-svg read %my-svg.svg | |
Red Draw Docs: https://github.com/red/docs/blob/master/en/draw.adoc | |
Baking transforms: https://stackoverflow.com/questions/5149301 | |
Paths: | |
MDN Overview: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d | |
SVG (1.1): https://www.w3.org/TR/SVG11/paths.html#PathDataBNF | |
SVG (2--incomplete as-of 22-Jan-2020): https://www.w3.org/TR/SVG/paths.html#PathDataBNF | |
Length: evaluate https://github.com/MadLittleMods/svg-curve-lib | |
} | |
] | |
#macro ['_] func [s e] [none] | |
do either exists? %altxml.red [%altxml.red] [ | |
https://raw.githubusercontent.com/rgchris/Scripts/master/experimental/altxml.red | |
] | |
do either exists? %rsp.red [%rsp.red] [ | |
https://raw.githubusercontent.com/rgchris/Scripts/master/red/rsp.red | |
] | |
neaten: func [ | |
block [block! paren!] | |
/pairs | |
/triplets | |
/flat | |
/words | |
/first | |
][ | |
case [ | |
words [ | |
forall block [ | |
new-line block to logic! all [ | |
find [word! set-word! lit-word!] type?/word block/1 | |
not find/same [off] block/1 | |
] | |
] | |
] | |
first [ | |
new-line new-line/all block false true | |
] | |
<else> [ | |
new-line/all/skip block not flat case [ | |
pairs [2] | |
triplets [3] | |
<else> [1] | |
] | |
] | |
] | |
block | |
] | |
svg: make object! [ | |
; quickie number parsing | |
; number*: charset "-.0123456789eE" | |
name*: complement charset {^-^/^L^M "',;} | |
spacers*: charset "^-^/^L^M " | |
digit*: charset "0123456789" | |
hex*: charset "0123456789abcdefABCDEF" | |
lower-alpha*: charset [#"a" - #"z"] | |
space*: [ | |
some spacers* | |
] | |
comma*: [ | |
space* | |
opt #"," | |
opt space* | |
| | |
#"," | |
opt space* | |
] | |
unsigned*: [ | |
[ | |
some digit* | |
opt #"." | |
any digit* | |
| | |
#"." | |
some digit* | |
] | |
opt [ | |
[#"e" | #"E"] | |
opt [#"-" | #"+"] | |
some digit* | |
] | |
] | |
number*: [ | |
opt [#"+" | #"-"] | |
unsigned* | |
] | |
as-number: func [ | |
value [string! integer! float! percent!] | |
][ | |
if string? value [ | |
value: load value | |
] | |
switch type?/word value [ | |
; default rounding is bad, it has a destructive | |
; influence on relative path values | |
; | |
float! percent! [ | |
round/to value 0.001 | |
] | |
integer! [ | |
value | |
] | |
] | |
] | |
is-value-from: func [ | |
value [string!] | |
list [block!] | |
][ | |
if find list value [ | |
load value | |
] | |
] | |
colors: #( | |
black: 0.0.0 | |
navy: 0.0.128 | |
darkblue: 0.0.139 | |
mediumblue: 0.0.205 | |
blue: 0.0.255 | |
darkgreen: 0.100.0 | |
green: 0.128.0 | |
teal: 0.128.128 | |
darkcyan: 0.139.139 | |
deepskyblue: 0.191.255 | |
darkturquoise: 0.206.209 | |
mediumspringgreen: 0.250.154 | |
lime: 0.255.0 | |
springgreen: 0.255.127 | |
cyan: 0.255.255 | |
aqua: 0.255.255 | |
midnightblue: 25.25.112 | |
dodgerblue: 30.144.255 | |
lightseagreen: 32.178.170 | |
forestgreen: 34.139.34 | |
seagreen: 46.139.87 | |
darkslategray: 47.79.79 | |
darkslategrey: 47.79.79 | |
limegreen: 50.205.50 | |
mediumseagreen: 60.179.113 | |
turquoise: 64.224.208 | |
royalblue: 65.105.225 | |
steelblue: 70.130.180 | |
darkslateblue: 72.61.139 | |
mediumturquoise: 72.209.204 | |
indigo: 75.0.130 | |
darkolivegreen: 85.107.47 | |
cadetblue: 95.158.160 | |
cornflowerblue: 100.149.237 | |
mediumaquamarine: 102.205.170 | |
dimgrey: 105.105.105 | |
dimgray: 105.105.105 | |
slateblue: 106.90.205 | |
olivedrab: 107.142.35 | |
slategrey: 112.128.144 | |
slategray: 112.128.144 | |
lightslategray: 119.136.153 | |
lightslategrey: 119.136.153 | |
mediumslateblue: 123.104.238 | |
lawngreen: 124.252.0 | |
chartreuse: 127.255.0 | |
aquamarine: 127.255.212 | |
maroon: 128.0.0 | |
purple: 128.0.128 | |
olive: 128.128.0 | |
gray: 128.128.128 | |
grey: 128.128.128 | |
skyblue: 135.206.235 | |
lightskyblue: 135.206.250 | |
blueviolet: 138.43.226 | |
darkred: 139.0.0 | |
darkmagenta: 139.0.139 | |
saddlebrown: 139.69.19 | |
darkseagreen: 143.188.143 | |
lightgreen: 144.238.144 | |
mediumpurple: 147.112.219 | |
darkviolet: 148.0.211 | |
palegreen: 152.251.152 | |
darkorchid: 153.50.204 | |
yellowgreen: 154.205.50 | |
sienna: 160.82.45 | |
brown: 165.42.42 | |
darkgray: 169.169.169 | |
darkgrey: 169.169.169 | |
lightblue: 173.216.230 | |
greenyellow: 173.255.47 | |
paleturquoise: 175.238.238 | |
lightsteelblue: 176.196.222 | |
powderblue: 176.224.230 | |
firebrick: 178.34.34 | |
darkgoldenrod: 184.134.11 | |
mediumorchid: 186.85.211 | |
rosybrown: 188.143.143 | |
darkkhaki: 189.183.107 | |
silver: 192.192.192 | |
mediumvioletred: 199.21.133 | |
indianred: 205.92.92 | |
peru: 205.133.63 | |
chocolate: 210.105.30 | |
tan: 210.180.140 | |
lightgray: 211.211.211 | |
lightgrey: 211.211.211 | |
thistle: 216.191.216 | |
orchid: 218.112.214 | |
goldenrod: 218.165.32 | |
palevioletred: 219.112.147 | |
crimson: 220.20.60 | |
gainsboro: 220.220.220 | |
plum: 221.160.221 | |
burlywood: 222.184.135 | |
lightcyan: 224.255.255 | |
lavender: 230.230.250 | |
darksalmon: 233.150.122 | |
violet: 238.130.238 | |
palegoldenrod: 238.232.170 | |
lightcoral: 240.128.128 | |
khaki: 240.230.140 | |
aliceblue: 240.248.255 | |
honeydew: 240.255.240 | |
azure: 240.255.255 | |
sandybrown: 244.164.96 | |
wheat: 245.222.179 | |
beige: 245.245.220 | |
whitesmoke: 245.245.245 | |
mintcream: 245.255.250 | |
ghostwhite: 248.248.255 | |
salmon: 250.128.114 | |
antiquewhite: 250.235.215 | |
linen: 250.240.230 | |
lightgoldenrodyellow: 250.250.210 | |
oldlace: 253.245.230 | |
red: 255.0.0 | |
fuchsia: 255.0.255 | |
magenta: 255.0.255 | |
deeppink: 255.20.147 | |
orangered: 255.69.0 | |
tomato: 255.99.71 | |
hotpink: 255.105.180 | |
coral: 255.127.80 | |
darkorange: 255.140.0 | |
lightsalmon: 255.160.122 | |
orange: 255.165.0 | |
lightpink: 255.182.193 | |
pink: 255.192.203 | |
gold: 255.215.0 | |
peachpuff: 255.218.185 | |
navajowhite: 255.222.173 | |
moccasin: 255.228.181 | |
bisque: 255.228.196 | |
mistyrose: 255.228.225 | |
blanchedalmond: 255.235.205 | |
papayawhip: 255.239.213 | |
lavenderblush: 255.240.245 | |
seashell: 255.245.238 | |
cornsilk: 255.248.220 | |
lemonchiffon: 255.250.205 | |
floralwhite: 255.250.240 | |
snow: 255.250.250 | |
yellow: 255.255.0 | |
lightyellow: 255.255.224 | |
ivory: 255.255.240 | |
white: 255.255.255 | |
) | |
underscore: to word! "_" | |
set :underscore none | |
lit-underscore: to lit-word! underscore | |
fail-mark: _ | |
fail-if-not-end: [ | |
end | |
| | |
fail-mark: ( | |
do make error! rejoin [ | |
"Parse Error: " copy/part mold fail-mark 30 | |
] | |
) | |
] | |
paths: make object! [ | |
comment { | |
MDN Overview: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d | |
SVG (1.1): https://www.w3.org/TR/SVG11/paths.html#PathDataBNF | |
SVG (2--incomplete as-of 22-Jan-2020): https://www.w3.org/TR/SVG/paths.html#PathDataBNF | |
Length: evaluate https://github.com/MadLittleMods/svg-curve-lib | |
} | |
command: _ | |
relative?: _ | |
implicit?: _ | |
offset: _ | |
origin: _ | |
params: _ | |
stack: make block! 8 | |
precision: 0.001 | |
mark: value: _ | |
commands: #( | |
#"M" move #"m" 'move | |
#"Z" close #"z" 'close | |
#"L" line #"l" 'line | |
#"H" hline #"h" 'hline | |
#"V" vline #"v" 'vline | |
#"C" curve #"c" 'curve | |
#"S" curv #"s" 'curv | |
#"Q" qcurve #"q" 'qcurve | |
#"T" qcurv #"t" 'qcurv | |
#"A" arc #"a" 'arc | |
) | |
fail: func [message [string!]] [ | |
do make error! rejoin [ | |
message ": (" any [command #"_"] ") " | |
mold copy/part mark 30 | |
] | |
] | |
; Unit Products | |
; | |
flag: [ | |
[ | |
#"0" | |
(append stack false) | |
| | |
#"1" | |
(append stack true) | |
] | |
| | |
(fail "Could not consume flag") | |
] | |
nonnegative-number: [ | |
copy value unsigned* | |
(append stack as-number value) | |
| | |
(fail "Could not consume non-negative number") | |
] | |
number: [ | |
copy value number* | |
(append stack as-number value) | |
| | |
mark: (fail "Could not consume number") | |
] | |
; Parameter Templates | |
coordinate: [ | |
number | |
] | |
coordinate-pair: [ | |
coordinate | |
opt comma* | |
coordinate | |
] | |
coordinate-pair-double: [ | |
coordinate-pair | |
opt comma* | |
coordinate-pair | |
] | |
coordinate-pair-triple: [ | |
coordinate-pair | |
opt comma* | |
coordinate-pair | |
opt comma* | |
coordinate-pair | |
] | |
elliptical-arc-sequence: [ | |
nonnegative-number | |
opt comma* | |
nonnegative-number | |
opt comma* | |
number | |
comma* | |
flag | |
opt comma* | |
flag | |
opt comma* | |
coordinate-pair | |
] | |
; Commands | |
move-to: [ | |
[#"M" | #"m"] | |
(params: coordinate-pair) | |
] | |
close-path: [ | |
[#"Z" | #"z"] | |
(params: []) | |
] | |
line-to: [ | |
[#"L" | #"l"] | |
(params: coordinate-pair) | |
] | |
horizontal-line-to: [ | |
[#"H" | #"h"] | |
(params: coordinate) | |
] | |
vertical-line-to: [ | |
[#"V" | #"v"] | |
(params: coordinate) | |
] | |
curve-to: [ | |
[#"C" | #"c"] | |
(params: coordinate-pair-triple) | |
] | |
smooth-curve-to: [ | |
[#"S" | #"s"] | |
(params: coordinate-pair-double) | |
] | |
quadratic-bezier-curve-to: [ | |
[#"Q" | #"q"] | |
(params: coordinate-pair-double) | |
] | |
smooth-quadratic-bezier-curve-to: [ | |
[#"T" | #"t"] | |
(params: coordinate-pair) | |
] | |
elliptical-arc: [ | |
[#"A" | #"a"] | |
(params: elliptical-arc-sequence) | |
] | |
; Structure | |
keep-params: [ | |
( | |
switch command [ | |
move [ | |
if relative? [ | |
stack/1: round/to stack/1 + offset/1 precision | |
stack/2: round/to stack/2 + offset/2 precision | |
] | |
origin/1: offset/1: stack/1 | |
origin/2: offset/2: stack/2 | |
] | |
hline [ | |
if relative? [ | |
stack/1: round/to stack/1 + offset/1 precision | |
] | |
offset/1: stack/1 | |
] | |
vline [ | |
if relative? [ | |
stack/1: round/to stack/1 + offset/2 precision | |
] | |
offset/2: stack/1 | |
] | |
line qcurv [ | |
if relative? [ | |
stack/1: round/to stack/1 + offset/1 precision | |
stack/2: round/to stack/2 + offset/2 precision | |
] | |
offset/1: stack/1 | |
offset/2: stack/2 | |
] | |
curv qcurve [ | |
if relative? [ | |
stack/1: round/to stack/1 + offset/1 precision | |
stack/2: round/to stack/2 + offset/2 precision | |
stack/3: round/to stack/3 + offset/1 precision | |
stack/4: round/to stack/4 + offset/2 precision | |
] | |
offset/1: stack/3 | |
offset/2: stack/4 | |
] | |
curve [ | |
if relative? [ | |
stack/1: round/to stack/1 + offset/1 precision | |
stack/2: round/to stack/2 + offset/2 precision | |
stack/3: round/to stack/3 + offset/1 precision | |
stack/4: round/to stack/4 + offset/2 precision | |
stack/5: round/to stack/5 + offset/1 precision | |
stack/6: round/to stack/6 + offset/2 precision | |
] | |
offset/1: stack/5 | |
offset/2: stack/6 | |
] | |
arc [ | |
if relative? [ | |
stack/6: round/to stack/6 + offset/1 precision | |
stack/7: round/to stack/7 + offset/2 precision | |
] | |
offset/1: stack/6 | |
offset/2: stack/7 | |
] | |
close [ | |
offset/1: origin/1 | |
offset/2: origin/2 | |
] | |
] | |
) | |
keep (command) | |
keep (take/part stack tail stack) | |
] | |
expand: [ | |
( | |
command: _ | |
relative?: _ | |
implicit?: _ | |
offset: copy [0 0] | |
origin: copy [0 0] | |
clear stack | |
) | |
opt space* | |
collect opt [ | |
mark: | |
set command move-to | |
opt space* | |
params | |
( | |
command: commands/:command | |
implicit?: false | |
relative?: lit-word? command | |
command: to word! command | |
) | |
keep-params | |
any [ | |
opt space* | |
set command [ | |
move-to | |
| | |
close-path | |
| | |
line-to | |
| | |
horizontal-line-to | |
| | |
vertical-line-to | |
| | |
curve-to | |
| | |
smooth-curve-to | |
| | |
quadratic-bezier-curve-to | |
| | |
smooth-quadratic-bezier-curve-to | |
| | |
elliptical-arc | |
] | |
opt space* params | |
( | |
command: commands/:command | |
implicit?: false | |
relative?: lit-word? command | |
command: to word! command | |
) | |
keep-params | |
| | |
opt space* end | |
| | |
opt comma* params | |
( | |
implicit?: true | |
if command = 'move [ | |
command: 'line | |
] | |
) | |
keep-params | |
] | |
] | |
fail-if-not-end | |
] | |
; Interpret | |
command-name: [ | |
'move | 'line | 'hline | 'vline | 'arc | 'curve | 'qcurve | 'curv | 'qcurv | 'close | |
] | |
draw-params: #( | |
move: [pair!] | |
line: [pair!] | |
hline: [number!] | |
vline: [number!] | |
arc: [pair! 3 number! opt 'sweep opt 'large] | |
curve: [3 pair!] | |
qcurve: [2 pair!] | |
curv: [2 pair!] | |
qcurv: [pair!] | |
close: [] | |
) | |
draw-path-to-path: func [ | |
path [block!] | |
/local here command offset origin params relative? implicit? value | |
][ | |
command: _ | |
offset: [0 0] | |
origin: [0 0] | |
implicit?: false | |
relative?: false | |
neaten/words collect [ | |
origin/1: 0 | |
origin/2: 0 | |
offset/1: 0 | |
offset/2: 0 | |
if not parse path [ | |
some [ | |
end break | |
| | |
[ | |
here: | |
command-name ( | |
command: to word! here/1 | |
implicit?: false | |
relative?: lit-word? here/1 | |
params: select draw-params here/1 | |
) | |
| | |
( | |
implicit?: true | |
params: any [params [fail]] | |
if command = 'move [ | |
command: 'line | |
] | |
) | |
] | |
copy part params ( | |
switch command [ | |
move [ | |
if relative? [ | |
part/1: part/1 + offset/1 | |
part/2: part/2 + offset/2 | |
] | |
origin/1: offset/1: part/1 | |
origin/2: offset/2: part/2 | |
] | |
hline [ | |
if relative? [ | |
part/1: part/1 + offset/1 | |
] | |
offset/1: part/1 | |
] | |
vline [ | |
if relative? [ | |
part/1: part/1 + offset/2 | |
] | |
offset/2: part/1 | |
] | |
line qcurv [ | |
if relative? [ | |
part/1: part/1 + offset/1 | |
part/2: part/2 + offset/2 | |
] | |
offset/1: part/1 | |
offset/2: part/2 | |
] | |
curv qcurve [ | |
if relative? [ | |
part/1: part/1 + offset/1 | |
part/2: part/2 + offset/2 | |
part/3: part/3 + offset/1 | |
part/4: part/4 + offset/2 | |
] | |
offset/1: part/4 | |
offset/2: part/5 | |
] | |
curve [ | |
if relative? [ | |
part/1: part/1 + offset/1 | |
part/2: part/2 + offset/2 | |
part/3: part/3 + offset/1 | |
part/4: part/4 + offset/2 | |
part/5: part/5 + offset/1 | |
part/6: part/6 + offset/2 | |
] | |
offset/1: part/5 | |
offset/2: part/6 | |
] | |
arc [ | |
part: reduce [ | |
part/2 | |
part/3 | |
part/4 | |
block? find part 'large | |
block? find part 'sweep | |
part/1/x | |
part/1/y | |
] | |
if relative? [ | |
part/6: part/6 + offset/1 | |
part/7: part/7 + offset/2 | |
] | |
offset/1: part/6 | |
offset/2: part/7 | |
] | |
close [ | |
offset/1: origin/1 | |
offset/2: origin/2 | |
] | |
] | |
keep reduce [command part] | |
) | |
] | |
][ | |
do make error! rejoin ["Could not parse path (at #" copy/part here 8 ")"] | |
] | |
] | |
] | |
next-command: func [ | |
here [block!] | |
/local params part implicit? | |
][ | |
if head? here [ | |
current/command: none | |
current/position: 0x0 | |
implicit?: false | |
] | |
comment { | |
Return format is: [ | |
command relative? implicit? offset params new-position | |
] | |
} | |
case [ | |
tail? here [ | |
_ | |
] | |
parse here [ | |
[ | |
command-name ( | |
current/command: here/1 | |
implicit?: false | |
params: command-params/:current-command | |
) | |
| | |
( | |
implicit?: true | |
params: either none? current/command [ | |
[fail] | |
][ | |
if current/command = 'move [ | |
current/command: 'line | |
] | |
select path-params current/command | |
] | |
) | |
] | |
copy part params | |
here: to end | |
][ | |
reduce [ | |
to word! current/command | |
lit-word? current/command | |
part | |
here | |
] | |
] | |
<else> [ | |
do make error! rejoin [ | |
"Could not parse path (at #" index? here ")" | |
] | |
] | |
] | |
] | |
] | |
unit-to-number: func [ | |
number [string!] | |
/local value unit | |
][ | |
if parse number [ | |
copy value number* | |
copy unit opt [ | |
"%" | "px" | "mm" | "cm" | "in" | "pt" | |
] | |
][ | |
switch unit [ | |
"" "px" [ | |
as-number value | |
] | |
"%" [ | |
1% * to float! value | |
] | |
; not yet supported | |
; "mm" "cm" "in" "pt" [none] | |
] | |
] | |
] | |
parse-numeric-list: func [ | |
list [string!] | |
/local part | |
][ | |
parse list [ | |
collect any [ | |
part: | |
space* | |
opt #"," | |
opt space* | |
| | |
#"," | |
opt space* | |
| | |
copy part number* | |
keep (as-number part) | |
] | |
fail-if-not-end | |
] | |
] | |
parse-path: func [ | |
path [string!] | |
/local out | |
][ | |
if out: parse/case path paths/expand [ | |
neaten/words out | |
] | |
] | |
load-style: func [ | |
style [string!] | |
/local key value | |
][ | |
; quickie parsing for now | |
style: split style charset ":;" | |
if even? index? tail style [ | |
remove back tail style | |
] | |
make map! collect [ | |
foreach [key value] style [ | |
keep to word! trim/head/tail key | |
keep trim/head/tail value | |
] | |
] | |
] | |
parse-transformation: func [ | |
transformation [string!] | |
/local type part value | |
][ | |
collect [ | |
parse transformation [ | |
opt space* | |
any [ | |
copy type [ | |
"matrix" | "rotate" | "scale" | "translate" | "skewX" | "skewY" | |
] | |
opt space* | |
"(" copy part to ")" | |
skip | |
opt comma* ; shouldn't accept trailing commas, but -- oh well.. | |
( | |
keep to word! type | |
keep/only to paren! parse-numeric-list part | |
) | |
] | |
fail-if-not-end | |
] | |
] | |
] | |
parse-font-name: func [ | |
names [string!] | |
/local name | |
][ | |
collect [ | |
if not parse names [ | |
some [ | |
[ | |
copy name [ | |
some name* | |
any [ | |
" " | |
some name* | |
] | |
] | |
( | |
name: any [ | |
is-value-from name [ | |
"serif" "sans-serif" "cursive" "fantasy" "monospace" | |
] | |
name | |
] | |
) | |
| | |
{"} | |
copy name some [ | |
some name* | |
| | |
" " | "," | "'" | |
] | |
{"} | |
| | |
"'" | |
copy name some [ | |
some name* | |
| | |
" " | "," | {"} | |
] | |
"'" | |
] | |
(keep name) | |
opt space* [ | |
"," | |
opt space* | |
| | |
end | |
] | |
] | |
][ | |
keep 'sans-serif | |
] | |
] | |
] | |
handle-attributes: func [ | |
node [object!] | |
/local attribute handler value style part | |
][ | |
attributes: make map! collect [ | |
; Should support these | |
; https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/Presentation | |
; | |
foreach attribute node/attributes [ | |
keep either attribute/namespace [ | |
to word! rejoin [ | |
form attribute/namespace | |
"|" | |
form attribute/name | |
] | |
][ | |
attribute/name | |
] | |
keep/only switch/default attribute/name [ | |
viewbox [ | |
if all [ | |
value: parse-numeric-list attribute/value | |
parse value [ | |
4 integer! | |
] | |
][ | |
value | |
] | |
] | |
id [ | |
to issue! replace/all attribute/value " " "_" | |
] | |
class [ | |
collect [ | |
foreach part split attribute/value " " [ | |
if not empty? trim/head/tail part [ | |
attempt [ | |
keep to word! part | |
] | |
] | |
] | |
] | |
] | |
fill stroke stop-color [ | |
either parse/case trim/head/tail attribute/value [ | |
["transparent" | "none"] | |
end | |
(value: 'transparent) | |
| | |
copy value [some lower-alpha*] | |
end | |
( | |
value: to word! value | |
if not tuple? select colors value [ | |
value: none | |
] | |
) | |
| | |
copy value [ | |
"#" [ | |
8 hex* | 6 hex* | 3 hex* | |
] | |
] | |
end | |
(value: hex-to-rgb load value) | |
| | |
"rgb(" | |
copy value [ | |
some digit* | |
comma* | |
some digit* | |
comma* | |
some digit* | |
] | |
")" | |
end | |
( | |
parse value [ | |
some digit* change comma* "." | |
some digit* change comma* "." | |
some digit* | |
] | |
value: attempt [ | |
to tuple! value | |
] | |
) | |
; | ; handle-URL | |
; "url(" ")" | |
][ | |
value | |
][ | |
attribute/value | |
black | |
] | |
] | |
d [ | |
parse-path attribute/value | |
] | |
points [ | |
parse-numeric-list attribute/value | |
] | |
cx cy dx dy fr fx fy r rx ry x x1 x2 y y1 y2 z | |
width height offset rotate scale | |
stroke-width stroke-miterlimit [ | |
any [ | |
is-value-from attribute/value [ | |
"auto" | |
] | |
unit-to-number attribute/value | |
] | |
] | |
fill-rule clip-rule [ | |
is-value-from attribute/value [ | |
"nonzero" "evenodd" | |
] | |
] | |
stroke-dasharray [ | |
either attribute/value = "none" [ | |
none | |
][ | |
parse-numeric-list attribute/value | |
] | |
] | |
stroke-linecap [ | |
is-value-from attribute/value [ | |
"butt" "round" "square" | |
] | |
] | |
stroke-linejoin [ | |
is-value-from attribute/value [ | |
"arcs" "bevel" "miter" "miter-clip" "round" | |
] | |
] | |
font-size [ | |
any [ | |
is-value-from attribute/value [ | |
"xx-small" "x-small" "small" "medium" "large" "x-large" "xx-large" "xxx-large" | |
"larger" "smaller" | |
] | |
unit-to-number attribute/value | |
] | |
] | |
font-family [ | |
parse-font-name attribute/value | |
] | |
font-style [ | |
is-value-from attribute/value [ | |
"normal" "italic" "oblique" | |
] | |
] | |
font-weight [ | |
is-value-from attribute/value [ | |
"normal" "bold" "lighter" "bolder" | |
"100" "200" "300" "400" "500" "600" "700" "800" "900" | |
] | |
] | |
text-anchor [ | |
is-value-from attribute/value [ | |
"start" "middle" "end" | |
] | |
] | |
transform [ | |
parse-transformation attribute/value | |
] | |
href [ | |
case [ | |
find/match attribute/value #"#" [ | |
to issue! next attribute/value | |
] | |
find attribute/value #":" [ | |
to url! attribute/value | |
] | |
<else> [ | |
to file! attribute/value | |
] | |
] | |
] | |
cursor [ | |
is-value-from attribute/value [ | |
"auto" "crosshair" "default" "pointer" "move" "text" "wait" "help" | |
"e-resize" "ne-resize" "nw-resize" "n-resize" | |
"se-resize" "sw-resize" "s-resize" "w-resize" | |
] | |
] | |
display [ | |
is-value-from attribute/value [ | |
"contents" "none" | |
] | |
] | |
visible [ | |
is-value-from attribute/value [ | |
"visible" "hidden" "collapse" | |
] | |
] | |
; ; messy, but needs to be supported | |
; clip-path | |
; ; would set 'currentcolor parameter somewhere | |
; color | |
opacity stroke-opacity fill-opacity [ | |
unit-to-number attribute/value | |
] | |
; ; urls | |
; filter mask | |
pointer-events [ | |
is-value-from attribute/value [ | |
"bounding-box" "visible" "painted" "fill" "stroke" "all" "none" | |
"visiblePainted" "visibleFill" "visibleStroke" | |
] | |
] | |
style [ | |
load-style attribute/value | |
] | |
][ | |
any [ | |
attempt [ | |
load attribute/value | |
] | |
attribute/value | |
] | |
] | |
] | |
] | |
if map? attributes/style [ | |
; Style attributes have greater precedence: | |
; https://www.w3.org/TR/2008/REC-CSS2-20080411/cascade.html#q12 | |
foreach attribute words-of attributes/style [ | |
value: attributes/style/:attribute | |
if value: switch/default attribute [ | |
; this is largely a repeat of the above, need to hang out to DRY. | |
fill stroke [ | |
either parse/case trim/head/tail value [ | |
["transparent" | "none"] | |
end | |
(value: 'transparent) | |
| | |
copy value [ | |
some lower-alpha* | |
] | |
end | |
if ( | |
value: to word! value | |
tuple? select colors value | |
) | |
| | |
copy value [ | |
"#" [ | |
8 hex* | 6 hex* | 3 hex* | |
] | |
] | |
end | |
(value: hex-to-rgb load value) | |
| | |
"rgb(" copy part to ")" skip | |
(value: to tuple! parse-numeric-list part) | |
][ | |
value | |
][ | |
probe attributes/style/:attribute | |
value: black | |
] | |
] | |
stroke-dasharray [ | |
parse-numeric-list value | |
] | |
stroke-width stroke-miterlimit font-size [ | |
unit-to-number value | |
] | |
fill-rule clip-rule [ | |
is-value-from value [ | |
"nonzero" "evenodd" | |
] | |
] | |
stroke-linecap [ | |
is-value-from value [ | |
"butt" "round" "square" | |
] | |
] | |
stroke-linejoin [ | |
is-value-from value [ | |
"arcs" "bevel" "miter" "miter-clip" "round" | |
] | |
] | |
font-family [ | |
parse-font-name value | |
] | |
font-size [ | |
any [ | |
is-value-from value [ | |
"xx-small" "x-small" "small" "medium" "large" "x-large" "xx-large" "xxx-large" | |
"larger" "smaller" | |
] | |
unit-to-number value | |
] | |
] | |
font-style [ | |
is-value-from value [ | |
"normal" "italic" "oblique" | |
] | |
] | |
font-weight [ | |
is-value-from value [ | |
"normal" "bold" "lighter" "bolder" | |
"100" "200" "300" "400" "500" "600" "700" "800" "900" | |
] | |
] | |
text-anchor [ | |
is-value-from value [ | |
"start" "middle" "end" | |
] | |
] | |
transform [ | |
parse-transformation value | |
] | |
][ | |
value | |
][ | |
remove/key attributes/style attribute | |
attributes/(attribute): value | |
] | |
] | |
if empty? attributes/style [ | |
remove/key attributes 'style | |
] | |
] | |
attributes | |
] | |
open-tags: _ | |
handle-kid: func [ | |
node [object!] | |
/local attributes kids kid | |
][ | |
neaten/words collect [ | |
keep switch/default node/name [ | |
g [ | |
'group | |
] | |
][ | |
node/name | |
] | |
keep attributes: handle-attributes node | |
keep/only either empty? kids: neaten/triplets collect [ | |
kids: node/children | |
while [not tail? kids] [ | |
kid: kids/1 | |
switch/default kid/type [ | |
element [ | |
insert open-tags kid/name | |
keep handle-kid kid | |
remove open-tags | |
] | |
text [ | |
keep quote 'text | |
keep underscore | |
keep either node/name = 'text [ | |
kid: copy kid/value | |
if head? kids [ | |
trim/head kid | |
] | |
if tail? next kids [ | |
trim/tail kid | |
] | |
kid | |
][ | |
copy kid/value | |
] | |
] | |
whitespace [ | |
if all [ | |
find open-tags 'text | |
not head? kids | |
not tail? next kids | |
][ | |
keep quote 'text | |
keep _ | |
keep copy " " | |
] | |
] | |
][ | |
probe kids/1/type | |
] | |
kids: next kids | |
] | |
][ | |
underscore | |
][ | |
kids | |
] | |
] | |
] | |
load-svg: func [ | |
svg [string! binary!] | |
/local kid kids desc defs | |
][ | |
case/all [ | |
binary? svg [ | |
svg: to string! svg | |
] | |
string? svg [ | |
svg: load-xml/dom svg | |
; probe svg/as-block | |
] | |
] | |
open-tags: make block! 8 | |
neaten/words collect [ | |
keep 'svg | |
keep handle-attributes svg | |
keep/only either empty? kids: collect [ | |
foreach kid svg/children [ | |
if kid/type = 'element [ | |
insert open-tags kid/name | |
; switch/default kid/name [ | |
; ; defs [defs: handle-defs kid] | |
; desc [desc: kid/text] | |
; ][ | |
keep handle-kid kid | |
; ] | |
remove open-tags | |
] | |
] | |
][ | |
underscore | |
][ | |
kids | |
] | |
] | |
] | |
; Draw | |
; | |
vid: context [ | |
as-pair: func [ | |
x [number!] | |
y [number!] | |
][ | |
make pair! reduce [round/to x 1 round/to y 1] | |
] | |
adjust-font-size: func [ | |
parent [map!] | |
size [word! integer! float!] | |
][ | |
switch/default size [ | |
xx-small [8] | |
x-small [9] | |
small [10] | |
medium [12] | |
large [14] | |
x-large [16] | |
xx-large [18] | |
xxx-large [24] | |
larger [ | |
round/to parent/size + 1 1 | |
] | |
smaller [ | |
round/to parent/size - 1 1 | |
] | |
][ | |
either number? size [ | |
; round/to size * 72.0 / 96 1 | |
round/to size * 75.0 / 96 1 | |
][ | |
do make error! rejoin [ | |
"Invalid Font-Size value: " mold size | |
] | |
] | |
] | |
] | |
to-draw-path: func [ | |
path [block!] | |
/local command params open? | |
][ | |
comment { | |
SVG Path/D: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d | |
(with links to various W3 specs) | |
Draw (Red): https://github.com/red/docs/blob/master/en/draw.adoc#shape-commands | |
(was): https://doc.red-lang.org/en/draw.html#_shape_commands | |
Draw (Rebol 3): http://www.rebol.com/r3/docs/view/draw-shapes.html | |
Draw (Rebol 2): http://www.rebol.com/docs/draw-ref.html#section-74 | |
Some incongruities: | |
SVG calls it PATH with D attribute, Draw calls it SHAPE. | |
In SVG, MOVE may have one or more coordinate pairs. The first moves the pen | |
without a mark, the following are implicit LINE commands. This is an error in | |
Red (does work in Rebol 2). | |
SVG Path commands are fixed arity, however Draw's ARC has optional LARGE and | |
SWEEP keywords. | |
} | |
neaten/words collect [ | |
open?: true | |
foreach [command params] path [ | |
keep command | |
switch command [ | |
move [ | |
keep as-pair params/1 params/2 | |
open?: true | |
] | |
line qcurv [ | |
keep as-pair params/1 params/2 | |
] | |
hline vline [ | |
keep round params/1 | |
] | |
arc [ | |
keep as-pair params/6 params/7 | |
keep round params/1 | |
keep round params/2 | |
keep params/3 | |
if find [true #[true]] params/5 [ | |
keep 'sweep | |
] | |
if find [true #[true]] params/4 [ | |
keep 'large | |
] | |
] | |
curve [ | |
keep as-pair params/1 params/2 | |
keep as-pair params/3 params/4 | |
keep as-pair params/5 params/6 | |
] | |
curv qcurve [ | |
keep as-pair params/1 params/2 | |
keep as-pair params/3 params/4 | |
] | |
close [ | |
open?: false | |
] | |
] | |
] | |
; prevents Red's auto-closing behaviour | |
; | |
if open? [ | |
keep [ | |
move -1x-1 | |
] | |
] | |
] | |
] | |
numbers-to-points: func [ | |
numbers [block!] | |
/local mark | |
][ | |
parse numbers [ | |
collect [ | |
some [ | |
mark: number! number! | |
keep (as-pair mark/1 mark/2) | |
] | |
] | |
] | |
] | |
default-style: #( | |
pen: off | |
fill-pen: off | |
line-width: 0 | |
) | |
default-font: #( | |
name: system | |
size: 12 | |
style: #[none] | |
angle: 0 | |
color: 0.0.0 | |
) | |
viewport-of: func [ | |
svg-block [block!] | |
][ | |
either parse svg-block [ | |
'svg map! block! | |
][ | |
if all [ | |
number? select svg-block/2 'width | |
number? select svg-block/2 'height | |
][ | |
as-pair round svg-block/2/width round svg-block/2/height | |
] | |
][ | |
make error! "Not an SVG drawing: No Viewport" | |
] | |
] | |
viewbox-of: func [ | |
svg-block [block!] | |
][ | |
either parse svg-block [ | |
'svg map! block! | |
][ | |
if all [ | |
block? select svg-block/2 'viewbox | |
parse svg-block/2/viewbox [ | |
4 number! | |
] | |
][ | |
as-pair | |
round (svg-block/2/viewbox/3 - svg-block/2/viewbox/1) | |
round (svg-block/2/viewbox/4 - svg-block/2/viewbox/2) | |
] | |
][ | |
make error! "Not an SVG drawing: No Viewbox" | |
] | |
] | |
dimensions-of: func [ | |
svg-block [block!] | |
][ | |
any [ | |
viewbox-of svg-block | |
viewport-of svg-block | |
] | |
] | |
facets-of: func [ | |
facets [map!] | |
/local facet | |
][ | |
neaten/pairs collect [ | |
foreach facet words-of facets/style [ | |
if facets/style/:facet [ | |
keep to word! facet | |
keep facets/style/:facet | |
] | |
] | |
] | |
] | |
font!: make object! [ | |
name: _ | |
size: _ | |
style: _ | |
angle: 0 | |
color: _ | |
parent: _ | |
] | |
font-facets-of: func [ | |
facets [map!] | |
][ | |
make font! body-of facets/font | |
] | |
change-font-style-flag: func [ | |
font [map!] | |
flag [word!] | |
status [logic!] | |
][ | |
either status [ | |
if not block? font/style [ | |
font/style: make block! 2 | |
] | |
if not find font/style flag [ | |
append font/style flag | |
] | |
][ | |
if all [ | |
block? font/style | |
flag: find font/style flag | |
][ | |
remove flag | |
] | |
if empty? font/style [ | |
font/style: none | |
] | |
] | |
] | |
handle-facets: func [ | |
inherited [map!] | |
attributes [map!] | |
/local facets value facet keepers function params | |
][ | |
facets: make map! reduce [ | |
'style copy inherited/style | |
'font copy/deep inherited/font | |
] | |
; display attributes | |
; | |
foreach [facet keepers] [ | |
stroke [ | |
facets/style/pen: switch/default type?/word value [ | |
word! [ | |
any [ | |
select colors value | |
'off | |
] | |
] | |
tuple! issue! [ | |
value | |
] | |
][ | |
'off | |
] | |
] | |
fill [ | |
facets/style/fill-pen: switch/default type?/word value [ | |
word! [ | |
any [ | |
select colors value | |
'off | |
] | |
] | |
tuple! issue! [ | |
value | |
] | |
][ | |
probe value | |
'off | |
] | |
] | |
stroke-width [ | |
facets/style/line-width: value | |
] | |
stroke-linejoin [ | |
facets/style/line-join: value | |
] | |
transform [ | |
; need to expand on this for multiple transforms | |
; | |
foreach [function params] value [ | |
switch function [ | |
matrix [ | |
facets/style/matrix: reduce [ | |
to block! params | |
] | |
] | |
translate [ | |
facets/style/translate: as-pair params/1 params/2 | |
] | |
scale [ | |
facets/style/scale: to block! :params | |
] | |
] | |
] | |
] | |
; stroke-dasharray | |
][ | |
value: any [ | |
select attributes facet | |
select default-style facet | |
] | |
if value keepers | |
] | |
; font attributes | |
; | |
foreach [facet keepers] [ | |
fill [ | |
facets/font/color: case [ | |
tuple? facets/style/fill-pen [ | |
facets/style/fill-pen | |
] | |
issue? facets/style/fill-pen [ | |
hex-to-rgb facets/style/fill-pen | |
] | |
] | |
] | |
font-family [ | |
facets/font/name: any [ | |
case [ | |
block? value [ | |
first value | |
] | |
string? value [ | |
value | |
] | |
word? value [ | |
switch/default value [ | |
serif cursive fantasy [ | |
'serif | |
] | |
sans-serif [ | |
'sans-serif | |
] | |
monospace [ | |
'monospace | |
] | |
] | |
] | |
] | |
'system | |
] | |
] | |
font-size [ | |
facets/font/size: adjust-font-size inherited/font value | |
] | |
font-style [ | |
switch value [ | |
normal [ | |
change-font-style-flag facets/font 'italic off | |
] | |
italic [ | |
change-font-style-flag facets/font 'italic on | |
] | |
] | |
] | |
font-weight [ | |
switch value [ | |
normal 100 200 300 400 500 lighter [ | |
change-font-style-flag facets/font 'bold off | |
] | |
bold 600 700 800 900 bolder [ | |
change-font-style-flag facets/font 'bold on | |
] | |
] | |
] | |
text-anchor [ | |
switch value [ | |
start middle end [] | |
] | |
] | |
][ | |
if value: select attributes facet keepers | |
] | |
facets | |
] | |
text-sizer: make face! [ | |
type: 'text-sizer | |
] | |
width-of: func [ | |
text [string!] | |
font [none! object!] | |
][ | |
first size-text make text-sizer compose [ | |
text: (text) | |
font: (any [font font!]) | |
] | |
; ; crashing for some reason | |
; text-sizer/text: text | |
; text-sizer/font: any [font font!] ; either probe font [make font! font][font!] | |
; first size-text text-sizer | |
] | |
text-offset: 0x0 | |
text-to-draw: func [ | |
svg-block [block!] | |
inherited [map!] | |
/at position [pair!] | |
/local node attributes style kids reset | |
][ | |
if at [ | |
text-offset: :position | |
] | |
neaten/words collect [ | |
foreach [node attributes kids] svg-block [ | |
switch/default node [ | |
'text [ | |
keep reduce [ | |
; 'font probe style: font-facets-of inherited | |
'font neaten/first to paren! reduce [ | |
'make 'object! neaten/pairs body-of style: font-facets-of inherited | |
] | |
'text subtract text-offset as-pair 0 style/size kids | |
] | |
text-offset/x: text-offset/x + width-of kids style | |
] | |
tspan [ | |
case [ | |
number? attributes/x [ | |
text-offset/x: round/to attributes/x 1 | |
] | |
number? attributes/dx [ | |
text-offset/x: text-offset/x + round/to attributes/dx 1 | |
] | |
] | |
case [ | |
number? attributes/y [ | |
text-offset/y: round/to attributes/y 1 | |
] | |
number? attributes/dy [ | |
text-offset/y: text-offset/y + round/to attributes/dy 1 | |
] | |
] | |
keep 'push | |
keep/only compose [ | |
(facets-of style: handle-facets inherited attributes) | |
(text-to-draw kids style) | |
] | |
] | |
a textpath [ | |
keep compose [ | |
(facets-of style: handle-facets inherited attributes) | |
(text-to-draw kids style) | |
] | |
] | |
][ | |
probe reduce [ | |
"Unsupported text node:" node | |
] | |
] | |
] | |
] | |
] | |
nodes-to-draw: func [ | |
nodes [block!] | |
inherited [map!] | |
/local node attributes kids pushed? | |
][ | |
neaten/words collect [ | |
foreach [node attributes kids] nodes [ | |
switch/default node [ | |
svg [ | |
keep compose [ | |
(facets-of style: handle-facets inherited attributes) | |
(nodes-to-draw kids style) | |
] | |
] | |
defs [ | |
; to follow | |
] | |
clippath [ | |
; to follow | |
] | |
group a [ | |
keep 'push | |
keep/only compose [ | |
(facets-of style: handle-facets inherited attributes) | |
(nodes-to-draw kids style) | |
] | |
] | |
text [ | |
; Here be dragons | |
; | |
position: 0x0 | |
case [ | |
number? attributes/x [ | |
position/x: to integer! attributes/x | |
] | |
number? attributes/dx [ | |
position/x: position/x + to integer! attributes/dx | |
] | |
] | |
case [ | |
number? attributes/y [ | |
position/y: to integer! attributes/y | |
] | |
number? attributes/dy [ | |
position/y: position/y + to integer! attributes/dy | |
] | |
] | |
keep/only compose [ | |
(facets-of style: handle-facets inherited attributes) | |
(text-to-draw/at kids style position) | |
] | |
] | |
rect [ | |
keep 'push | |
keep/only neaten/words collect [ | |
keep facets-of handle-facets inherited attributes | |
keep 'box | |
keep as-pair attributes/x attributes/y | |
keep as-pair attributes/x + attributes/width attributes/y + attributes/height | |
; only one radius dimension allowed... | |
; | |
if any [ | |
attributes/rx | |
attributes/ry | |
][ | |
keep any [ | |
attributes/rx | |
attributes/ry | |
] | |
] | |
] | |
] | |
circle [ | |
keep 'push | |
keep/only neaten/words compose [ | |
(facets-of handle-facets inherited attributes) | |
circle | |
(as-pair attributes/cx attributes/cy) | |
(attributes/r) | |
] | |
] | |
ellipse [ | |
keep 'push | |
keep/only neaten/words compose [ | |
(facets-of handle-facets inherited attributes) | |
circle | |
(as-pair attributes/cx attributes/cy) | |
(attributes/rx) | |
(attributes/ry) | |
] | |
] | |
line [ | |
keep 'push | |
keep/only neaten/words compose [ | |
(facets-of handle-facets inherited attributes) | |
line | |
(as-pair attributes/x1 attributes/y1) | |
(as-pair attributes/x2 attributes/y2) | |
] | |
] | |
polyline [ | |
keep 'push | |
keep/only neaten/words compose [ | |
(facets-of handle-facets inherited attributes) | |
line (numbers-to-points attributes/points) | |
] | |
] | |
polygon [ | |
keep 'push | |
keep/only neaten/words compose [ | |
(facets-of handle-facets inherited attributes) | |
polygon (numbers-to-points attributes/points) | |
] | |
] | |
path [ | |
keep 'push | |
keep/only neaten/words compose/deep [ | |
(facets-of handle-facets inherited attributes) | |
shape [(to-draw-path attributes/d)] | |
] | |
] | |
use [ | |
keep [ | |
text 0x0 "`USE` TO FOLLOW" | |
] | |
] | |
style [] | |
][ | |
probe to-tag node | |
probe attributes | |
] | |
] | |
] | |
] | |
to-draw: func [ | |
nodes [block!] | |
"Block hewn from LOAD-SVG function" | |
/with style [map! none!] | |
"Style inherited from parent" | |
; /local drawing | |
][ | |
comment { | |
Challenges facing TO-DRAW include: | |
* SVG elements inherit style from their parents where Draw | |
elements inherit the current style, hence PUSH must be used | |
to isolate an element to prevent subsequent sibling elements | |
from inheriting its style | |
* New font objects must be created for every change in style | |
facets pertaining to text | |
* Elements in SVG <def> also inherit from parents and when such | |
elements are applied within a Draw block must retain all | |
style from its parent. e.g. all kids of <def fill="yellow"> | |
will still be yellow when placed inside a <g fill="purple"> | |
} | |
compose [ | |
fill-pen off | |
pen off | |
line-width 0 | |
( | |
nodes-to-draw nodes make map! reduce [ | |
'style any [ | |
style | |
default-style | |
] | |
'font default-font | |
] | |
) | |
] | |
] | |
quick-layout: func [ | |
svg-block [block!] "SVG to wrap" | |
][ | |
probe 'quick-layout | |
copy/deep compose/deep [ | |
title "SVG Viewer" | |
backdrop coal | |
panel 400x400 [ | |
backdrop silver | |
origin 0x0 space 0x0 | |
box loose ( | |
any [ | |
dimensions-of svg-block | |
500x500 | |
] | |
) | |
draw compose/deep [ | |
scale 1 1 [ | |
( | |
; probe copy/deep load mold/all ; ugh, getting access violation errors without this | |
to-draw svg-block | |
) | |
] | |
] | |
on-drag [ | |
face/offset: min 0x0 max face/offset face/parent/size - face/size | |
] | |
] | |
do [ | |
self/actors: make object! [ | |
on-resize: func [ | |
face event | |
/local fixed sizes gutters | |
][ | |
face/pane/1/size: face/size - 20 | |
] | |
on-key-up: func [ | |
face event | |
][ | |
switch event/key [ | |
#"w" #"W" [ | |
unview/all | |
] | |
] | |
] | |
] | |
] | |
] | |
] | |
] | |
; Transform/Render | |
form-number: func [ | |
number [number!] | |
][ | |
switch type?/word number [ | |
integer! percent! [ | |
form number | |
] | |
float! [ | |
number: form number | |
case [ | |
parse number [thru ".0" end] [ | |
clear find number ".0" | |
] | |
find/match number "0." [ | |
remove number | |
] | |
find/match number "-0." [ | |
remove next number | |
] | |
] | |
number | |
] | |
] | |
] | |
form-color: func [ | |
value [tuple!] | |
][ | |
case [ | |
3 = length? value [ | |
rejoin [ | |
"rgb(" value/1 #"," value/2 #"," value/3 ")" | |
] | |
] | |
4 = length? value [ | |
rejoin [ | |
"rgba(" value/1 #"," value/2 #"," value/3 #"," form-number value/4 / 256 ")" | |
] | |
] | |
] | |
] | |
round-params: func [ | |
command [word!] | |
params [block!] | |
precision [integer! float!] | |
/local param | |
][ | |
params: copy params | |
foreach param switch command [ | |
vline hline [ | |
[1] | |
] | |
move line qcurv [ | |
[1 2] | |
] | |
curv qcurve [ | |
[1 2 3 4] | |
] | |
curve [ | |
[1 2 3 4 5 6] | |
] | |
arc [ | |
[1 2 3 6 7] | |
] | |
][ | |
poke params param round/to pick params param precision | |
] | |
params | |
] | |
form-path: func [ | |
path [block!] | |
/precise precision [number!] | |
/local out emit command params value type last offset origin | |
][ | |
precision: any [ | |
precision paths/precision | |
] | |
offset: copy [0 0] | |
origin: copy [0 0] | |
last: _ | |
out: make string! 16 * length? path ; approx. pre-allocation | |
emit: func [values] [ | |
append out collect [ | |
foreach value reduce values [ | |
switch type?/word value [ | |
char! [ | |
keep value | |
last: 'word | |
] | |
logic! [ | |
if find [integer float] last [ | |
keep " " | |
] | |
keep pick [1 0] value | |
last: 'logic | |
] | |
word! [ | |
value: value = 'true | |
if find [integer float] last [ | |
keep " " | |
] | |
keep pick [1 0] value | |
last: 'logic | |
] | |
float! integer! [ | |
value: form-number round/to value precision | |
case [ | |
; can always append negative numbers without padding | |
find/match value "-" [ | |
keep value | |
] | |
find/match value "." [ | |
if find [integer] last [ | |
keep #" " | |
] | |
keep value | |
] | |
find [integer float] last [ | |
keep " " | |
keep value | |
] | |
<else> [ | |
keep value | |
] | |
] | |
last: either find value "." [ | |
'float | |
][ | |
'integer | |
] | |
] | |
] | |
] | |
] | |
] | |
foreach [command params] path [ | |
; note that FORM-PATH creates an SVG path with relative coordinates | |
if not command = 'close [ | |
params: round-params command params precision | |
] | |
switch command [ | |
move [ | |
emit [ | |
#"m" | |
params/1 - offset/1 | |
params/2 - offset/2 | |
] | |
origin/1: offset/1: params/1 | |
origin/2: offset/2: params/2 | |
] | |
hline [ | |
emit [ | |
#"h" | |
params/1 - offset/1 | |
] | |
offset/1: params/1 | |
] | |
vline [ | |
emit [ | |
#"v" | |
params/1 - offset/2 | |
] | |
offset/2: params/1 | |
] | |
line qcurv [ | |
emit [ | |
select [line #"l" qcurv #"t"] command | |
params/1 - offset/1 params/2 - offset/2 | |
] | |
offset/1: params/1 | |
offset/2: params/2 | |
] | |
curv qcurve [ | |
emit [ | |
select [curv #"s" qcurve #"q"] command | |
params/1 - offset/1 params/2 - offset/2 | |
params/3 - offset/1 params/4 - offset/2 | |
] | |
offset/1: params/3 | |
offset/2: params/4 | |
] | |
curve [ | |
emit [ | |
#"c" | |
params/1 - offset/1 params/2 - offset/2 | |
params/3 - offset/1 params/4 - offset/2 | |
params/5 - offset/1 params/6 - offset/2 | |
] | |
offset/1: params/5 | |
offset/2: params/6 | |
] | |
arc [ | |
emit [ | |
#"a" | |
params/1 params/2 | |
params/3 | |
params/4 params/5 | |
params/6 - offset/1 params/7 - offset/2 | |
] | |
offset/1: params/6 | |
offset/2: params/7 | |
] | |
close [ | |
append out #"z" | |
offset/1: origin/1 | |
offset/2: origin/2 | |
] | |
] | |
] | |
uppercase/part out 1 | |
] | |
; experimental, unfinished | |
; would be nice to support TRANSFORM attribute *and* a general ability | |
; to arbitrarily transform shapes, e.g. move shape 100x100 or resize shape 150% | |
; | |
apply-transform: func [ | |
point [pair! block!] | |
matrix [block!] | |
][ | |
point: collect [ | |
switch type?/word point [ | |
pair! [ | |
keep to float! point/x | |
keep to float! point/y | |
] | |
block! [ | |
parse point [ | |
2 [ | |
point: [ | |
integer! | |
(keep to float! point/1) | |
| | |
float! | |
(keep point/1) | |
] | |
] | |
] | |
] | |
] | |
] | |
if all [ | |
parse point [2 float!] | |
parse matrix [6 number!] | |
][ | |
reduce [ | |
(x * matrix/1) + (y * matrix/3) + matrix/5 | |
(x * matrix/2) + (y * matrix/4) + matrix/6 | |
] | |
] | |
] | |
wrap-shape: func [ | |
size [pair!] | |
shape [string! tag!] | |
/window | |
top-left [pair!] | |
bottom-right [pair!] | |
][ | |
top-left: any [ | |
top-left | |
0x0 | |
] | |
bottom-right: any [ | |
bottom-right | |
size | |
] | |
trim/auto rejoin [ | |
{ | |
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" } | |
{width="} size/x {" } | |
{height="} size/y {" } | |
{viewBox="} reform [top-left/x top-left/y bottom-right/x bottom-right/y] {" } | |
{xmlns:xlink="http://www.w3.org/1999/xlink"> | |
} shape { | |
</svg> | |
} | |
] | |
] | |
listify: func [list [block! paren!] /with-comma] [ | |
with-comma: either with-comma [","] [" "] | |
rejoin back change collect [ | |
forall list [ | |
keep with-comma | |
keep case [ | |
number? list/1 [ | |
form-number list/1 | |
] | |
issue? list/1 [ | |
sanitize mold list/1 | |
] | |
none? list/1 [ | |
do make error! rejoin [ | |
"Should not be NONE! here: " mold list | |
] | |
] | |
<else> [ | |
sanitize form list/1 | |
] | |
] | |
] | |
] "" | |
] | |
render-precision: 0.001 | |
render-node: func [ | |
name [word!] | |
attributes [map! none!] | |
kids [block! none!] | |
/precise | |
precision [number!] | |
/pretty | |
/local out rule attribute part mark | |
][ | |
open-tags: make block! 8 | |
render-precision: any [ | |
precision | |
0.001 | |
] | |
out: make string! 1024 | |
rule: [ | |
mark: | |
set name word! | |
[ | |
set attributes map! | |
| | |
lit-underscore | |
(attributes: #()) | |
] | |
[ | |
ahead set kids block! | |
| | |
(kids: none) | |
] | |
( | |
if not any [ | |
head? mark ; first child indenting handled by parent | |
find open-tags 'text | |
][ | |
append out newline | |
if pretty [ | |
append/dup out " " length? open-tags | |
] | |
] | |
append out build-tag collect [ | |
keep name: switch/default name [ | |
group ['g] | |
][ | |
name | |
] | |
foreach attribute words-of attributes [ | |
if not none? attributes/:attribute [ | |
keep either find form attribute #"|" [ | |
to path! replace form attribute #"|" #"/" | |
][ | |
attribute | |
] | |
keep case [ | |
all [ | |
name = 'path | |
attribute = 'd | |
block? attributes/:attribute | |
][ | |
form-path/precise attributes/:attribute render-precision | |
] | |
all [ | |
attribute = 'id | |
issue? attributes/:attribute | |
][ | |
to string! attributes/:attribute | |
] | |
all [ | |
; tranform attribute, others? | |
block? attributes/:attribute | |
parse attributes/:attribute [ | |
some [word! paren!] | |
] | |
][ | |
listify/with-comma collect [ | |
parse attributes/:attribute [ | |
some [ | |
copy part [word! paren!] | |
( | |
keep rejoin [ | |
form part/1 "(" listify part/2 ")" | |
] | |
) | |
] | |
] | |
] | |
] | |
all [ | |
attribute = 'transform | |
parse attributes/:attribute [ | |
copy part [word! some number!] | |
] | |
][ | |
rejoin [ | |
form part/1 "(" form next part ")" | |
] | |
] | |
block? attributes/:attribute [ | |
listify/with-comma attributes/:attribute | |
] | |
float? part: attributes/:attribute [ | |
if find [x x1 x2 y y1 y2 cx cy dx dy r rx ry width height] attribute [ | |
part: round/to part render-precision | |
] | |
form-number part | |
] | |
tuple? attributes/:attribute [ | |
form-color attributes/:attribute | |
] | |
<else> [ | |
attributes/:attribute | |
] | |
] | |
] | |
] | |
if any [ | |
none? kids | |
empty? kids | |
][ | |
keep [/] | |
] | |
] | |
) | |
[ | |
none! | 'none | lit-underscore () | into [] | |
; already closed this tag, we're done | |
| | |
( | |
if not find open-tags 'text [ | |
append out newline | |
if pretty [ | |
append/dup out " " 1 + length? open-tags | |
] | |
] | |
insert open-tags name | |
) | |
into [some rule] | |
( | |
name: take open-tags | |
if not find open-tags 'text [ | |
append out newline | |
if pretty [ | |
append/dup out " " length? open-tags | |
] | |
] | |
append out rejoin [ | |
"</" name ">" | |
] | |
) | |
] | |
| | |
ahead lit-word! 'text | |
[none! | 'none | lit-underscore] | |
set kids string! | |
(append out sanitize kids) | |
| | |
mark: | |
( | |
do make error! rejoin [ | |
"Could not parse document, at: " | |
mold neaten/flat copy/part mark 12 | |
] | |
) | |
] | |
if parse reduce [name attributes kids] rule [ | |
out | |
] | |
] | |
render: func [ | |
"Convert SVG Model to SVG" | |
svg-block [block!] "SVG Model" | |
/precise "Constrain numbers to a specified precision" | |
precision [number!] "Precision value (see ROUND function's SCALE parameter)" | |
/pretty "Use indents on the resultant XML output" | |
][ | |
render-precision: any [ | |
precision | |
0.001 | |
] | |
open-tags: make block! 8 | |
either parse svg-block [ | |
'svg map! [block! | none!] | |
][ | |
rejoin [ | |
render-node/precise/pretty 'svg svg-block/2 svg-block/3 render-precision | |
] | |
][ | |
do make error! "Not an SVG document" | |
] | |
] | |
; more things to consider: | |
; https://stackoverflow.com/questions/5149301/baking-transforms-into-svg-path-element-commands | |
; wrt. 'apply-transform | |
] | |
load-svg: get in svg 'load-svg | |
to-svg: get in svg 'render | |
convert-svg: func [ | |
source [file!] | |
/local target content | |
][ | |
target: append copy source %.red | |
save/header target content: load-svg read source make object! [ | |
Title: "SVG Converter" | |
Date: now/date | |
] | |
content | |
] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment