Skip to content

Commit ae0ebfb

Browse files
decoration: Allow dynamic amplitudes (#756)
* decoration: Allow dynamic amplitudes * decoration: Sample amplitude twice per segment * decoration: Add square decoration * decoration: Refine amplitude array
1 parent 9977631 commit ae0ebfb

File tree

4 files changed

+125
-72
lines changed

4 files changed

+125
-72
lines changed

src/lib/decorations.typ

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
#import "decorations/brace.typ": brace, brace-default-style, flat-brace, flat-brace-default-style
2-
#import "decorations/path.typ": zigzag, wave, coil
2+
#import "decorations/path.typ": zigzag, wave, coil, square

src/lib/decorations/path.typ

Lines changed: 99 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@
1414
/// Length of a single segments
1515
segment-length: none,
1616

17-
/// Amplitude of a segment in the direction of the segments normal
17+
/// Amplitude of a segment in the direction of the segments normal.
18+
/// The following types are supported:
19+
/// - float
20+
/// - function ratio -> float (the segment ratio is given as argument)
21+
/// - array of floats (the rounded down segment number is used as index modulo the array length)
1822
amplitude: 1,
1923
/// Decoration start
2024
start: 0%,
@@ -59,6 +63,24 @@
5963
factor: 150%,
6064
)
6165

66+
// Square default style
67+
#let square-default-style = (
68+
..default-style,
69+
/// Midpoint factor
70+
factor: 50%,
71+
)
72+
73+
#let resolve-amplitude(amplitude, segment, num-segments) = {
74+
segment = calc.max(0, calc.min(segment, num-segments))
75+
return if type(amplitude) == function {
76+
(amplitude)(segment / num-segments * 100%)
77+
} else if type(amplitude) == array {
78+
amplitude.at(calc.rem(int(2*segment), amplitude.len()), default: 0)
79+
} else {
80+
amplitude
81+
}
82+
}
83+
6284
#let resolve-style(ctx, segments, style) = {
6385
assert(not (style.segments == none and style.segment-length == none),
6486
message: "Only one of segments or segment-length must be set, while the other must be auto")
@@ -169,12 +191,12 @@
169191

170192
(p0, p1) = util.revert-transform(ctx.transform, p0, p1)
171193

172-
let dir = vector.sub(p1, p0)
173-
let norm = vector.norm(vector.cross(dir, if p0.at(2) != p1.at(2) {
194+
let dir = vector.norm(vector.sub(p1, p0))
195+
let norm = vector.cross(dir, if p0.at(2) != p1.at(2) {
174196
style.z-up
175197
} else {
176198
style.xy-up
177-
}))
199+
})
178200

179201
pts += fn(i, p0, p1, norm)
180202
}
@@ -221,11 +243,10 @@
221243
// list of points for the line-strip.
222244
let fn(i, a, b, norm) = {
223245
let ab = vector.sub(b, a)
224-
225246
let f = .25 - (50% - style.factor) / 50% * .25
226247
let q-dir = vector.scale(ab, f)
227-
let up = vector.scale(norm, style.amplitude / 2)
228-
let down = vector.scale(up, -1)
248+
let up = vector.scale(norm, resolve-amplitude(style.amplitude, i + .25, num-segments) / 2)
249+
let down = vector.scale(norm, -resolve-amplitude(style.amplitude, i + .75, num-segments) / 2)
229250

230251
let m1 = vector.add(vector.add(a, q-dir), up)
231252
let m2 = vector.add(vector.sub(b, q-dir), down)
@@ -301,7 +322,8 @@
301322
//
302323
let fn(i, a, b, norm) = {
303324
let ab = vector.sub(b, a)
304-
let up = vector.scale(norm, style.amplitude / 2)
325+
let amplitude = resolve-amplitude(style.amplitude, i, num-segments)
326+
let up = vector.scale(norm, amplitude / 2)
305327
let dist = vector.dist(a, b)
306328

307329
let d = vector.norm(ab)
@@ -381,9 +403,8 @@
381403
//
382404
let fn(i, a, b, norm) = {
383405
let ab = vector.sub(b, a)
384-
let up = vector.scale(norm, style.amplitude / 2)
385-
let down = vector.scale(
386-
up, -1)
406+
let up = vector.scale(norm, +resolve-amplitude(style.amplitude, i + .25, num-segments) / 2)
407+
let down = vector.scale(norm, -resolve-amplitude(style.amplitude, i + .75, num-segments) / 2)
387408

388409
let ma = vector.add(vector.add(a, vector.scale(ab, .25)), up)
389410
let m = vector.add(a, vector.scale(ab, .50))
@@ -408,3 +429,70 @@
408429
close: close,
409430
..style)
410431
})
432+
433+
/// Draw a square-wave along a path using a line-strip
434+
///
435+
/// The number of phases can be controlled via the `segments` or `segment-length` style key, and the width via `amplitude`.
436+
///
437+
/// ```typc example
438+
/// line((0,0), (2,1), stroke: gray)
439+
/// cetz.decorations.square(line((0,0), (2,1)), amplitude: .25, start: 10%, stop: 90%)
440+
/// ```
441+
///
442+
/// - target (drawable): Target path
443+
/// - close (auto,bool): Close the path
444+
/// - name (none,string): Element name
445+
/// - ..style (style): Style
446+
///
447+
/// ## Styling
448+
/// *Root*: `squre`
449+
///
450+
/// - factor (ratio) = 50% Square-Wave midpoint
451+
#let square(target, close: auto, name: none, ..style) = draw.get-ctx(ctx => {
452+
let style = styles.resolve(ctx, merge: style.named(),
453+
base: square-default-style, root: "square")
454+
455+
let (segments, close) = get-segments(ctx, target)
456+
let style = resolve-style(ctx, segments, style)
457+
let num-segments = style.segments
458+
let factor = calc.max(0, calc.min(style.factor / 100%, 1))
459+
460+
// Return a list of points for the line-strip
461+
//
462+
// +----+ ▲
463+
// | | │ Up
464+
// ..a....m....b.. '
465+
// | |
466+
// +----+
467+
//
468+
let fn(i, a, b, norm) = {
469+
let ab = vector.sub(b, a)
470+
let up = vector.scale(norm, +resolve-amplitude(style.amplitude, i + .25, num-segments) / 2)
471+
let down = vector.scale(norm, -resolve-amplitude(style.amplitude, i + .75, num-segments) / 2)
472+
let m = vector.add(a, vector.scale(ab, factor))
473+
474+
if not close {
475+
if i == 0 {
476+
return (a, vector.add(a, up),
477+
vector.add(m, up), vector.add(m, down),
478+
vector.add(b, down))
479+
} else if i == num-segments - 1 {
480+
return (vector.add(a, up),
481+
vector.add(m, up), vector.add(m, down),
482+
vector.add(b, down), b)
483+
}
484+
}
485+
486+
return (vector.add(a, up),
487+
vector.add(m, up), vector.add(m, down),
488+
vector.add(b, down))
489+
}
490+
491+
return draw.merge-path(
492+
finalize-path(ctx, segments, style, draw.line(
493+
.._path-effect(ctx, segments, fn, close: close, style),
494+
close: close), close: close) ,
495+
name: name,
496+
close: close,
497+
..style)
498+
})

tests/decorations/path/ref/1.png

25.5 KB
Loading

tests/decorations/path/test.typ

Lines changed: 25 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -2,55 +2,23 @@
22
#import "/src/lib.typ": *
33
#import "/tests/helper.typ": *
44

5-
#import decorations: zigzag, coil, wave
5+
#import decorations: zigzag, coil, wave, square
66

7-
#box(stroke: 2pt + red, canvas(length: 1cm, {
8-
import draw: *
9-
10-
zigzag(line((0,0), (4,0)))
11-
zigzag(line((0,1), (4,1)), amplitude: .5)
12-
}))
13-
14-
#box(stroke: 2pt + red, canvas(length: 1cm, {
15-
import draw: *
16-
17-
wave(line((0,0), (4,0)))
18-
wave(line((0,1), (4,1)), amplitude: .5)
19-
}))
20-
21-
#box(stroke: 2pt + red, canvas(length: 1cm, {
22-
import draw: *
23-
24-
coil(line((0,0), (4,0)))
25-
coil(line((0,1), (4,1)), amplitude: .5)
26-
}))
7+
#let all-fns = (zigzag, coil, wave, square)
278

28-
#box(stroke: 2pt + red, canvas(length: 1cm, {
9+
#test-case(fn => {
2910
import draw: *
3011

31-
zigzag(hobby((0,0), (4,0), (6,2)))
32-
}))
12+
fn(line((0,0), (4,0)))
13+
fn(line((0,1), (4,1)), amplitude: .5)
14+
fn(line((0,2), (4,2)), amplitude: t => { 1 - .5 * t / 50% })
15+
fn(line((0,3), (4,3)), amplitude: (0, .5, 1))
16+
}, args: all-fns)
3317

34-
#box(stroke: 2pt + red, canvas(length: 1cm, {
18+
#test-case(fn => {
3519
import draw: *
36-
37-
coil(hobby((0,0), (4,0), (6,2)))
38-
}))
39-
40-
#box(stroke: 2pt + red, canvas(length: 1cm, {
41-
import draw: *
42-
43-
wave(hobby((0,0), (4,0), (6,2)))
44-
}))
45-
46-
#box(stroke: 2pt + red, canvas(length: 1cm, {
47-
import draw: *
48-
49-
set-style(radius: .9)
50-
zigzag(circle((0,0)), amplitude: .2, segments: 20, factor: 0%)
51-
zigzag(circle((0,2)), amplitude: .2, segments: 20, factor: 50%, stroke: blue)
52-
zigzag(circle((0,4)), amplitude: .2, segments: 20, factor: 100%, stroke: red)
53-
}))
20+
fn(hobby((0,0), (4,0), (6,2)))
21+
}, args: all-fns)
5422

5523
#box(stroke: 2pt + red, canvas(length: 1cm, {
5624
import draw: *
@@ -70,33 +38,30 @@
7038
wave(circle((0,4)), amplitude: .2, segments: 20, tension: 1, stroke: red)
7139
}))
7240

73-
74-
#test-case({
41+
#box(stroke: 2pt + red, canvas(length: 1cm, {
7542
import draw: *
7643

77-
zigzag(line((0,0), (3,0)), start: 10%, stop: 90%)
78-
zigzag(line((0,2), (3,2)), start: 1, stop: 2)
79-
})
44+
set-style(radius: .9)
45+
square(circle((0,2)), amplitude: .2, segments: 20)
46+
}))
8047

81-
#test-case({
48+
#test-case(fn => {
8249
import draw: *
8350

84-
coil(line((0,0), (3,0)), start: 10%, stop: 90%)
85-
coil(line((0,2), (3,2)), start: 1, stop: 2)
86-
})
51+
fn(line((0,0), (3,0)), start: 10%, stop: 90%, amplitude: .5)
52+
fn(line((0,1), (3,1)), start: 1, stop: 2, amplitude: .5)
53+
}, args: all-fns)
8754

88-
#test-case({
55+
#test-case(fn => {
8956
import draw: *
9057

91-
wave(line((0,0), (3,0)), start: 10%, stop: 90%)
92-
wave(line((0,2), (3,2)), start: 1, stop: 2)
93-
})
58+
fn(line((0,0,-1), (0,0,1)), start: 10%, stop: 90%)
59+
}, args: all-fns)
9460

95-
#test-case({
61+
#test-case(factor => {
9662
import draw: *
97-
98-
wave(line((0,0,-1), (0,0,1)), start: 10%, stop: 90%)
99-
})
63+
square(line((0,0), (3,0)), factor: factor)
64+
}, args: (25%, 50%, 75%))
10065

10166
#test-case({
10267
import draw: *

0 commit comments

Comments
 (0)