processing 0.5.34 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -9,9 +9,10 @@ module Processing
9
9
 
10
10
  # @private
11
11
  def initialize(polygon = nil, children = nil, context: nil)
12
- @polygon, @children = polygon, children
13
- @context = context || Context.context__
14
- @visible, @fill, @matrix = true, nil, nil
12
+ @polygon, @children = polygon, children
13
+ @context = context || Context.context__
14
+ @visible = true
15
+ @fill = @stroke = @strokeWeight = @strokeCap = @strokeJoin = @matrix = nil
15
16
  @type = @points = @curvePoints = @colors = @texcoords = @close = nil
16
17
  @contours = @contourPoints = @contourColors = @contourTexCoords = nil
17
18
  end
@@ -72,6 +73,7 @@ module Processing
72
73
  #
73
74
  def beginShape(type = nil)
74
75
  raise "beginShape() cannot be called twice" if drawingShape__
76
+ @fill = @stroke = @strokeWeight = @strokeCap = @strokeJoin = nil
75
77
  @type = type
76
78
  @points ||= []
77
79
  @curvePoints = []
@@ -91,7 +93,13 @@ module Processing
91
93
  #
92
94
  def endShape(close = nil)
93
95
  raise "endShape() must be called after beginShape()" unless drawingShape__
94
- @close = close == GraphicsContext::CLOSE || @contours.size > 0
96
+ painter = @context.getPainter__
97
+ @fill ||= painter.fill
98
+ @stroke ||= painter.stroke
99
+ @strokeWeight ||= painter.stroke_width
100
+ @strokeCap ||= painter.stroke_cap
101
+ @strokeJoin ||= painter.stroke_join
102
+ @close = close == GraphicsContext::CLOSE || @contours.size > 0
95
103
  if @close && @curvePoints.size >= 8
96
104
  x, y = @curvePoints[0, 2]
97
105
  2.times {curveVertex x, y}
@@ -147,7 +155,7 @@ module Processing
147
155
  raise "Either 'u' or 'v' is missing" if (u == nil) != (v == nil)
148
156
  u ||= x
149
157
  v ||= y
150
- color = @fill || @context.getFill__
158
+ color = @fill || @context.getPainter__.fill
151
159
  if drawingContour__
152
160
  @contourPoints << x << y
153
161
  @contourColors << color
@@ -250,7 +258,30 @@ module Processing
250
258
  # @see https://p5js.org/reference/#/p5/fill
251
259
  #
252
260
  def fill(*args)
253
- @fill = @context.rawColor__(*args)
261
+ @fill = @context.toRawColor__(*args)
262
+ nil
263
+ end
264
+
265
+ # Sets stroke color.
266
+ #
267
+ # @overload stroke(gray)
268
+ # @overload stroke(gray, alpha)
269
+ # @overload stroke(r, g, b)
270
+ # @overload stroke(r, g, b, alpha)
271
+ #
272
+ # @param gray [Integer] gray value (0..255)
273
+ # @param r [Integer] red value (0..255)
274
+ # @param g [Integer] green value (0..255)
275
+ # @param b [Integer] blue value (0..255)
276
+ # @param alpha [Integer] alpha value (0..255)
277
+ #
278
+ # @return [nil] nil
279
+ #
280
+ # @see https://processing.org/reference/stroke_.html
281
+ # @see https://p5js.org/reference/#/p5/stroke
282
+ #
283
+ def stroke(*args)
284
+ @stroke = @context.toRawColor__(*args)
254
285
  nil
255
286
  end
256
287
 
@@ -300,12 +331,12 @@ module Processing
300
331
  @points.size / 2
301
332
  end
302
333
 
303
- # Sets the fill color.
334
+ # Sets the fill color for all vertices.
304
335
  #
305
- # @overload fill(gray)
306
- # @overload fill(gray, alpha)
307
- # @overload fill(r, g, b)
308
- # @overload fill(r, g, b, alpha)
336
+ # @overload setFill(gray)
337
+ # @overload setFill(gray, alpha)
338
+ # @overload setFill(r, g, b)
339
+ # @overload setFill(r, g, b, alpha)
309
340
  #
310
341
  # @param gray [Integer] gray value (0..255)
311
342
  # @param r [Integer] red value (0..255)
@@ -318,26 +349,76 @@ module Processing
318
349
  # @see https://processing.org/reference/PShape_setFill_.html
319
350
  #
320
351
  def setFill(*args)
321
- color = @context.rawColor__(*args)
352
+ fill(*args)
322
353
  count = getVertexCount
323
354
  if count > 0
324
355
  if @colors
325
- @colors.fill color
356
+ @colors.fill @fill
326
357
  else
327
- @colors = [color] * count
358
+ @colors = [@fill] * count
328
359
  end
329
360
  clearCache__
330
361
  elsif @polygon
331
362
  @polygon = @polygon.transform do |polylines|
332
- polylines.map {|pl| pl.with colors: pl.points.map {color}}
363
+ polylines.map {|pl| pl.with colors: pl.points.map {@fill}}
333
364
  end
334
365
  end
335
366
  nil
336
367
  end
337
368
 
338
- # @private
339
- def setStroke__()
340
- raise NotImplementedError
369
+ # Sets the stroke color.
370
+ #
371
+ # @overload setStroke(gray)
372
+ # @overload setStroke(gray, alpha)
373
+ # @overload setStroke(r, g, b)
374
+ # @overload setStroke(r, g, b, alpha)
375
+ #
376
+ # @param gray [Integer] gray value (0..255)
377
+ # @param r [Integer] red value (0..255)
378
+ # @param g [Integer] green value (0..255)
379
+ # @param b [Integer] blue value (0..255)
380
+ # @param alpha [Integer] alpha value (0..255)
381
+ #
382
+ # @return [nil] nil
383
+ #
384
+ # @see https://processing.org/reference/PShape_setStroke_.html
385
+ #
386
+ def setStroke(*args)
387
+ stroke(*args)
388
+ nil
389
+ end
390
+
391
+ # Sets the stroke weight.
392
+ #
393
+ # @param weight [Numeric] stroke weight
394
+ #
395
+ # @return [nil] nil
396
+ #
397
+ def setStrokeWeight(weight)
398
+ @strokeWeight = weight
399
+ nil
400
+ end
401
+
402
+ # Sets the stroke cap.
403
+ #
404
+ # @param cap [ROUND, SQUARE, PROJECT] stroke cap
405
+ #
406
+ # @return [nil] nil
407
+ #
408
+ def setStrokeCap(cap)
409
+ @strokeCap = cap
410
+ nil
411
+ end
412
+
413
+ # Sets the stroke join.
414
+ #
415
+ # @param join [MITER, BEVEL, ROUND] stroke join
416
+ #
417
+ # @return [nil] nil
418
+ #
419
+ def setStrokeJoin(join)
420
+ @strokeJoin = join
421
+ nil
341
422
  end
342
423
 
343
424
  # Adds a new child shape.
@@ -505,24 +586,36 @@ module Processing
505
586
 
506
587
  # @private
507
588
  def draw__(painter, x, y, w = nil, h = nil)
508
- poly = getInternal__
589
+ p, poly = painter, getInternal__
509
590
 
510
- backup = nil
591
+ matrix_ = nil
511
592
  if @matrix && (poly || @children)
512
- backup = painter.matrix
513
- painter.matrix = backup * @matrix
593
+ matrix_ = p.matrix
594
+ p.matrix = matrix_ * @matrix
514
595
  end
515
596
 
516
597
  if poly
598
+ f_ = s_ = sw_ = sc_ = sj_ = nil
599
+ f_, p.fill = p.fill, '#fff' if @fill
600
+ s_, p.stroke = p.stroke, @stroke if @stroke
601
+ sw_, p.stroke_width = p.stroke_width, @strokeWeight if @strokeWeight
602
+ sc_, p.stroke_cap = p.stroke_cap, @strokeCap if @strokeCap
603
+ sj_, p.stroke_join = p.stroke_join, @strokeJoin if @strokeJoin
517
604
  if w || h
518
- painter.polygon poly, x, y, w,h
605
+ p.polygon poly, x, y, w,h
519
606
  else
520
- painter.polygon poly, x, y
607
+ p.polygon poly, x, y
521
608
  end
609
+ p.fill = f_ if f_
610
+ p.stroke = s_ if s_
611
+ p.stroke_width = sw_ if sw_
612
+ p.stroke_cap = sc_ if sc_
613
+ p.stroke_join = sj_ if sj_
522
614
  end
523
- @children&.each {|o| o.draw__ painter, x, y, w, h}
524
615
 
525
- painter.matrix = backup if backup
616
+ @children&.each {|o| o.draw__ p, x, y, w, h}
617
+
618
+ p.matrix = matrix_ if matrix_
526
619
  end
527
620
 
528
621
  # @private
@@ -0,0 +1,248 @@
1
+ module Processing
2
+
3
+
4
+ # @private
5
+ class SVGLoader
6
+
7
+ def initialize(context)
8
+ @c, @cc = context, context.class
9
+ end
10
+
11
+ def load(filename)
12
+ parse File.read(filename)
13
+ end
14
+
15
+ def parse(xml)
16
+ addGroup nil, REXML::Document.new(xml).elements.first
17
+ end
18
+
19
+ def addGroup(parent, e, **attribs)
20
+ group = @c.createShape @cc::GROUP
21
+ attribs = getAttribs e, attribs
22
+ e.elements.each do |child|
23
+ case child.name.to_sym
24
+ when :g, :a then addGroup group, child, **attribs
25
+ when :line then addLine group, child, **attribs
26
+ when :rect then addRect group, child, **attribs
27
+ when :circle then addCircle group, child, **attribs
28
+ when :ellipse then addEllipse group, child, **attribs
29
+ when :polyline then addPolyline group, child, **attribs
30
+ when :polygon then addPolyline group, child, true, **attribs
31
+ when :path then addPath group, child, **attribs
32
+ end
33
+ end
34
+ parent.addChild group if parent
35
+ group
36
+ end
37
+
38
+ def addLine(parent, e, **attribs)
39
+ x1, y1 = float(e, :x1), float(e, :y1)
40
+ x2, y2 = float(e, :x2), float(e, :y2)
41
+ s = @c.createLineShape__ x1, y1, x2, y2
42
+ applyAttribs s, e, attribs
43
+ parent.addChild s
44
+ end
45
+
46
+ def addRect(parent, e, **attribs)
47
+ x, y = float(e, :x), float(e, :y)
48
+ w, h = float(e, :width), float(e, :height)
49
+ rx, ry = float(e, :rx), float(e, :ry)
50
+ s = @c.createRectShape__ x, y, w, h, (rx || ry), mode: @cc::CORNER
51
+ applyAttribs s, e, attribs
52
+ parent.addChild s
53
+ end
54
+
55
+ def addCircle(parent, e, **attribs)
56
+ cx, cy = float(e, :cx), float(e, :cy)
57
+ r = float(e, :r)
58
+ s = @c.createEllipseShape__ cx, cy, r * 2, r * 2, mode: @cc::CENTER
59
+ applyAttribs s, e, attribs
60
+ parent.addChild s
61
+ end
62
+
63
+ def addEllipse(parent, e, **attribs)
64
+ cx, cy = float(e, :cx), float(e, :cy)
65
+ rx, ry = float(e, :rx), float(e, :ry)
66
+ s = @c.createEllipseShape__ cx, cy, rx * 2, ry * 2, mode: @cc::CENTER
67
+ applyAttribs s, e, attribs
68
+ parent.addChild s
69
+ end
70
+
71
+ def addPolyline(parent, e, close = false, **attribs)
72
+ points = e[:points] or raise Error, "missing 'points'"
73
+ scanner = StringScanner.new points
74
+ child = @c.createShape
75
+ child.beginShape
76
+
77
+ skipSpaces scanner
78
+ until scanner.eos?
79
+ child.vertex(*nextPos(scanner))
80
+ skipSpaces scanner
81
+ end
82
+
83
+ child.endShape close ? @cc::CLOSE : @cc::OPEN
84
+ applyAttribs child, e, attribs
85
+ parent.addChild child
86
+ end
87
+
88
+ def applyAttribs(shape, e, attribs)
89
+ a = getAttribs e, attribs
90
+ shape.setFill a[:fill] || :black
91
+ shape.setStroke a[:stroke] || :none
92
+ shape.setStrokeWeight a[:strokeWeight] || 1
93
+ shape.setStrokeCap a[:strokeCap] || @cc::SQUARE
94
+ shape.setStrokeJoin a[:strokeJoin] || @cc::MITER
95
+ end
96
+
97
+ def getAttribs(e, attribs)
98
+ @caps ||= {
99
+ 'butt' => @cc::SQUARE,
100
+ 'round' => @cc::ROUND,
101
+ 'square' => @cc::PROJECT
102
+ }
103
+ @joins ||= {
104
+ 'miter' => @cc::MITER,
105
+ 'miter-clip' => @cc::MITER,
106
+ 'round' => @cc::ROUND,
107
+ 'bevel' => @cc::BEVEL,
108
+ 'arcs' => @cc::MITER
109
+ }
110
+ attribs.merge({
111
+ fill: e[:fill],
112
+ stroke: e[:stroke],
113
+ strokeWeight: e[:'stroke-width'],
114
+ strokeCap: @caps[ e[:'stroke-linecap']],
115
+ strokeJoin: @joins[e[:'stroke-linejoin']]
116
+ }.compact)
117
+ end
118
+
119
+ def int(e, key, defval = 0)
120
+ e[key]&.to_i || defval
121
+ end
122
+
123
+ def float(e, key, defval = 0)
124
+ e[key]&.to_f || defval
125
+ end
126
+
127
+ def addPath(parent, e, **attribs)
128
+ data = e[:d] or raise Error, "missing 'd'"
129
+ scanner = StringScanner.new data
130
+ skipSpaces scanner
131
+
132
+ child = nil
133
+ close = false
134
+ beginChild = -> {
135
+ close = false
136
+ child = @c.createShape
137
+ child.beginShape
138
+ }
139
+ endChild = -> {
140
+ if child# && child.getVertexCount >= 2
141
+ child.endShape close ? @cc::CLOSE : @cc::OPEN
142
+ applyAttribs child, e, attribs
143
+ parent.addChild child
144
+ end
145
+ }
146
+
147
+ lastCommand = nil
148
+ lastX, lastY = 0, 0
149
+ lastCX, lastCY = 0, 0
150
+ until scanner.eos?
151
+ command = nextCommand scanner
152
+ if command =~ /^[Mm]$/
153
+ endChild.call
154
+ beginChild.call
155
+ end
156
+ raise Error, "no leading 'M' or 'm'" unless child
157
+
158
+ command ||= lastCommand
159
+ case command
160
+ when 'M', 'm'
161
+ lastX, lastY = nextPos scanner, lastX, lastY, command == 'm'
162
+ child.vertex lastX, lastY
163
+ when 'L', 'l'
164
+ lastX, lastY = nextPos scanner, lastX, lastY, command == 'l'
165
+ child.vertex lastX, lastY
166
+ when 'H', 'h'
167
+ lastX = nextNum scanner, lastX, command == 'h'
168
+ child.vertex lastX, lastY
169
+ when 'V', 'v'
170
+ lastY = nextNum scanner, lastY, command == 'v'
171
+ child.vertex lastX, lastY
172
+ when 'Q', 'q'
173
+ relative = command == 'q'
174
+ lastCX, lastCY = nextPos scanner, lastX, lastY, relative
175
+ lastX, lastY = nextPos scanner, lastX, lastY, relative
176
+ child.quadraticVertex lastCX, lastCY, lastX, lastY
177
+ when 'T', 't'
178
+ lastCX, lastCY =
179
+ if lastCommand =~ /[QqTt]/
180
+ [lastX + (lastX - lastCX), lastY + (lastY - lastCY)]
181
+ else
182
+ [lastX, lastY]
183
+ end
184
+ lastX, lastY = nextPos scanner, lastX, lastY, command == 't'
185
+ child.quadraticVertex lastCX, lastCY, lastX, lastY
186
+ when 'C', 'c'
187
+ relative = command == 'c'
188
+ cx, cy = nextPos scanner, lastX, lastY, relative
189
+ lastCX, lastCY = nextPos scanner, lastX, lastY, relative
190
+ lastX, lastY = nextPos scanner, lastX, lastY, relative
191
+ child.bezierVertex cx, cy, lastCX, lastCY, lastX, lastY
192
+ when 'S', 's'
193
+ cx, cy =
194
+ if lastCommand =~ /[CcSs]/
195
+ [lastX + (lastX - lastCX), lastY + (lastY - lastCY)]
196
+ else
197
+ [lastX, lastY]
198
+ end
199
+ relative = command == 's'
200
+ lastCX, lastCY = nextPos scanner, lastX, lastY, relative
201
+ lastX, lastY = nextPos scanner, lastX, lastY, relative
202
+ child.bezierVertex cx, cy, lastCX, lastCY, lastX, lastY
203
+ when 'A', 'a'
204
+ rx, ry = nextPos scanner
205
+ a, b, c = nextNum(scanner), nextNum(scanner), nextNum(scanner)
206
+ lastX, lastY = nextPos scanner, lastX, lastY, command == 'a'
207
+ child.vertex lastX, lastY
208
+ when 'Z', 'z'
209
+ v0 = child.getVertex 0
210
+ lastX, lastY = v0 ? [v0.x, v0.y] : [0, 0]
211
+ close = true
212
+ end
213
+ lastCommand = command
214
+ end
215
+ endChild.call
216
+ end
217
+
218
+ def nextCommand(scanner)
219
+ w = scanner.scan(/[[:alpha:]]/)
220
+ skipSpaces scanner
221
+ w
222
+ end
223
+
224
+ def nextNum(scanner, base = 0, relative = true)
225
+ n = scanner.scan(/(?:[\+\-]\s*)?\d*(?:\.\d+)?/)&.strip&.to_f
226
+ raise Error, 'invalid path' unless n
227
+ skipSpaces scanner
228
+ n + (relative ? base : 0)
229
+ end
230
+
231
+ def nextPos(scanner, baseX = 0, baseY = 0, relative = true)
232
+ [nextNum(scanner, baseX, relative), nextNum(scanner, baseY, relative)]
233
+ end
234
+
235
+ def skipSpaces(scanner)
236
+ scanner.scan(/\s*,?\s*/)
237
+ end
238
+
239
+ class Error < StandardError
240
+ def initialize(message = "error")
241
+ super "SVG: #{message}"
242
+ end
243
+ end# Error
244
+
245
+ end# SVG
246
+
247
+
248
+ end# Processing
data/processing.gemspec CHANGED
@@ -25,10 +25,10 @@ Gem::Specification.new do |s|
25
25
  s.platform = Gem::Platform::RUBY
26
26
  s.required_ruby_version = '>= 3.0.0'
27
27
 
28
- s.add_runtime_dependency 'xot', '~> 0.1.42'
29
- s.add_runtime_dependency 'rucy', '~> 0.1.44'
30
- s.add_runtime_dependency 'rays', '~> 0.1.49'
31
- s.add_runtime_dependency 'reflexion', '~> 0.1.57'
28
+ s.add_runtime_dependency 'xot', '~> 0.2'
29
+ s.add_runtime_dependency 'rucy', '~> 0.2'
30
+ s.add_runtime_dependency 'rays', '~> 0.2'
31
+ s.add_runtime_dependency 'reflexion', '~> 0.2'
32
32
 
33
33
  s.files = `git ls-files`.split $/
34
34
  s.test_files = s.files.grep %r{^(test|spec|features)/}
@@ -13,6 +13,30 @@ def browser(width, height, headless: true)
13
13
  hash[key] ||= Ferrum::Browser.new headless: headless, window_size: [width, height]
14
14
  end
15
15
 
16
+ def get_svg_html(width, height, svg_xml)
17
+ <<~END
18
+ <html>
19
+ <head>
20
+ <style type="text/css">
21
+ body {margin: 0;}
22
+ </style>
23
+ <script type="text/javascript">
24
+ window.onload = function() {
25
+ let e = document.createElement("span");
26
+ e.id = 'completed';
27
+ document.body.appendChild(e);
28
+ }
29
+ </script>
30
+ </head>
31
+ <body>
32
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 #{width} #{height}">
33
+ #{svg_xml}
34
+ </svg>
35
+ </body>
36
+ </html>
37
+ END
38
+ end
39
+
16
40
  def get_p5rb_html(width, height, draw_src, webgl: false)
17
41
  <<~END
18
42
  <html>
@@ -57,17 +81,17 @@ def sleep_until(try: 3, timeout: 10, &block)
57
81
  limit = now[timeout]
58
82
  try -= 1
59
83
  next if try > 0
60
- raise 'Drawing timed out in p5.rb'
84
+ raise 'Drawing timed out in browser'
61
85
  end
62
86
  sleep 0.1
63
87
  end
64
88
  end
65
89
 
66
- def draw_p5rb(width, height, draw_src, path, headless: true, webgl: false)
90
+ def draw_on_browser(width, height, path, html, headless: true)
67
91
  b = browser width, height, headless: headless
68
92
  unless File.exist? path
69
93
  b.reset
70
- b.main_frame.content = get_p5rb_html width, height, draw_src, webgl: webgl
94
+ b.main_frame.content = html
71
95
  sleep_until do
72
96
  b.evaluate 'document.querySelector("#completed") != null'
73
97
  end
@@ -75,3 +99,13 @@ def draw_p5rb(width, height, draw_src, path, headless: true, webgl: false)
75
99
  end
76
100
  b.device_pixel_ratio
77
101
  end
102
+
103
+ def draw_svg(width, height, svg_xml, path, headless: true)
104
+ html = get_svg_html width, height, svg_xml
105
+ draw_on_browser width, height, path, html, headless: headless
106
+ end
107
+
108
+ def draw_p5rb(width, height, draw_src, path, headless: true, webgl: false)
109
+ html = get_p5rb_html width, height, draw_src, webgl: webgl
110
+ draw_on_browser width, height, path, html, headless: headless
111
+ end
data/test/helper.rb CHANGED
@@ -20,9 +20,11 @@ DEFAULT_DRAW_HEADER = <<~END
20
20
  strokeWeight 50
21
21
  END
22
22
 
23
+ THRESHOLD_TO_BE_FIXED = 0.0
23
24
 
24
- def test_with_p5?()
25
- ENV['TEST_WITH_P5'] == '1'
25
+
26
+ def test_with_browser?()
27
+ (ENV['TEST_WITH_BROWSER'] || '0') != '0'
26
28
  end
27
29
 
28
30
  def md5(s)
@@ -107,16 +109,47 @@ def assert_equal_draw(
107
109
  assert_equal_pixels e, a, threshold: threshold
108
110
  end
109
111
 
112
+ def assert_svg_draw(
113
+ svg_xml,
114
+ width: 1000, height: 1000, threshold: 0.99, label: test_label, **kwargs)
115
+
116
+ source = <<~END
117
+ background 255
118
+ shape Processing::SVGLoader.new(self).parse <<~SVG
119
+ <?xml version="1.0" encoding="UTF-8"?>
120
+ <svg xmlns="http://www.w3.org/2000/svg"
121
+ xmlns:xlink="http://www.w3.org/1999/xlink"
122
+ viewBox="0 0 100 100">
123
+ #{svg_xml}
124
+ </svg>
125
+ SVG
126
+ END
127
+ assert_draw_on_browser(
128
+ source, width, height, threshold, label, **kwargs
129
+ ) do |path|
130
+ draw_svg width, height, svg_xml, path, **kwargs
131
+ end
132
+ end
133
+
110
134
  def assert_p5_draw(
111
135
  *sources, default_header: DEFAULT_DRAW_HEADER,
112
136
  width: 1000, height: 1000, threshold: 0.99, label: test_label, **kwargs)
113
137
 
114
- return unless test_with_p5?
115
-
116
138
  source = [default_header, *sources].compact.join("\n")
117
- path = draw_output_path "#{label}_expected", source
139
+ assert_draw_on_browser(
140
+ source, width, height, threshold, label, **kwargs
141
+ ) do |path|
142
+ draw_p5rb width, height, source, path, **kwargs
143
+ end
144
+ end
118
145
 
119
- pd = draw_p5rb width, height, source, path, **kwargs
146
+ def assert_draw_on_browser(
147
+ source, width, height, threshold, label, **kwargs, &draw_on_browser)
148
+
149
+ return unless test_with_browser?
150
+
151
+ path = draw_output_path "#{label}_expected", source
152
+ pd = draw_on_browser.call path
120
153
  actual = test_draw source, width: width, height: height, pixelDensity: pd
121
154
  actual.save path.sub('_expected', '_actual')
122
155
 
@@ -136,4 +169,4 @@ def assert_p5_fill_stroke(*sources, **kwargs)
136
169
  end
137
170
 
138
171
 
139
- require_relative 'p5' if test_with_p5?
172
+ require_relative 'browser' if test_with_browser?